UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

310 lines (309 loc) 14 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); /** * RelationsIndex: Pre-built lookup indexes for fast project item relation resolution. * * ## Problem * Without an index, each definition's `addChildItems()` method iterates ALL items of * cross-referenced types (e.g., each entity behavior scans ALL entity resources to find * matching IDs). With ~500 entities and ~500 entity resources, this is 250K iterations * with file loading — O(n²) behavior that causes relations to take 4+ minutes on vanilla. * * ## Solution * Build ID→ProjectItem[] maps in a single O(n) pass before processing relations. * Handlers then do O(1) lookups instead of O(n) scans. * * ## Indexed Types * - Entity type resources by ID (minecraft:pig → ProjectItem) * - Spawn rules by entity ID (minecraft:pig → ProjectItem) * - Entity behaviors by ID (minecraft:pig → ProjectItem) * - Attachable resources by ID * - Item types by ID * - Animations by animation ID (one file → multiple IDs) * - Animation controllers by controller ID * - Render controllers by controller ID * - Models by geometry identifier (one file → multiple identifiers) * - Loot tables by pack-relative path * - Textures by canonicalized relative path * * ## Usage * ```typescript * const index = new RelationsIndex(); * await index.build(project); * // Then pass to calculateForItem * await handler.addChildItems(project, item, index); * ``` */ const AnimationControllerResourceDefinition_1 = __importDefault(require("../minecraft/AnimationControllerResourceDefinition")); const AnimationResourceDefinition_1 = __importDefault(require("../minecraft/AnimationResourceDefinition")); const AttachableResourceDefinition_1 = __importDefault(require("../minecraft/AttachableResourceDefinition")); const EntityTypeDefinition_1 = __importDefault(require("../minecraft/EntityTypeDefinition")); const EntityTypeResourceDefinition_1 = __importDefault(require("../minecraft/EntityTypeResourceDefinition")); const FeatureDefinition_1 = __importDefault(require("../minecraft/FeatureDefinition")); const ItemTypeDefinition_1 = __importDefault(require("../minecraft/ItemTypeDefinition")); const ModelGeometryDefinition_1 = __importDefault(require("../minecraft/ModelGeometryDefinition")); const RenderControllerSetDefinition_1 = __importDefault(require("../minecraft/RenderControllerSetDefinition")); const SpawnRulesBehaviorDefinition_1 = __importDefault(require("../minecraft/SpawnRulesBehaviorDefinition")); const IProjectItemData_1 = require("./IProjectItemData"); /** Batch size for parallel content loading */ const PRELOAD_BATCH_SIZE = 100; class RelationsIndex { /** Entity type resources indexed by their definition ID (e.g., "minecraft:pig") */ entityResourcesById = new Map(); /** Spawn rules indexed by their entity ID (e.g., "minecraft:pig") */ spawnRulesById = new Map(); /** Entity type behaviors indexed by their definition ID */ entityBehaviorsById = new Map(); /** Attachable resources indexed by their ID */ attachablesById = new Map(); /** Item type behaviors indexed by their ID */ itemTypesById = new Map(); /** Feature behaviors indexed by their ID */ featureBehaviorsById = new Map(); /** Animation resources indexed by individual animation IDs (one file can have multiple) */ animationsById = new Map(); /** Animation controller resources indexed by individual controller IDs */ animationControllersById = new Map(); /** Render controllers indexed by individual controller IDs */ renderControllersById = new Map(); /** Model geometry items indexed by individual geometry identifiers */ modelsById = new Map(); /** Loot tables indexed by canonicalized pack-relative path */ lootTablesByPath = new Map(); /** Whether the index has been built */ isBuilt = false; /** * Build all indexes from the project's items. * Batch-loads content for all relatable item types, parses definitions, * and populates lookup maps. */ async build(project, onProgress) { // Collect all item types that participate in relations const typesToPreload = [ IProjectItemData_1.ProjectItemType.entityTypeBehavior, IProjectItemData_1.ProjectItemType.entityTypeResource, IProjectItemData_1.ProjectItemType.spawnRuleBehavior, IProjectItemData_1.ProjectItemType.attachableResourceJson, IProjectItemData_1.ProjectItemType.itemTypeBehavior, IProjectItemData_1.ProjectItemType.featureBehavior, IProjectItemData_1.ProjectItemType.animationResourceJson, IProjectItemData_1.ProjectItemType.animationControllerResourceJson, IProjectItemData_1.ProjectItemType.renderControllerJson, IProjectItemData_1.ProjectItemType.modelGeometryJson, IProjectItemData_1.ProjectItemType.lootTableBehavior, ]; // Phase 1: Batch-load all file contents in parallel chunks const allItems = []; for (const itemType of typesToPreload) { const items = project.getItemsByType(itemType); allItems.push(...items); } if (onProgress) { onProgress(`Pre-loading ${allItems.length} items for relations...`); } // Load content in batches to avoid overwhelming I/O. // PRELOAD_BATCH_SIZE is a fixed constant rather than dynamic based on hardware // because the bottleneck is file I/O (disk and network storage), not CPU or memory. // 100 concurrent loads is a reasonable ceiling for any system. for (let i = 0; i < allItems.length; i += PRELOAD_BATCH_SIZE) { const batch = allItems.slice(i, i + PRELOAD_BATCH_SIZE); await Promise.all(batch.map(async (item) => { if (!item.isContentLoaded) { await item.loadContent(); } // Also resolve file storage for ensureOnFile await item.ensureStorage(); })); } if (onProgress) { onProgress(`Building relation indexes...`); } // Phase 2: Parse definitions and build indexes. // Each method reads from a distinct item type and writes to a distinct map, // so they can safely run in parallel via Promise.all(). await Promise.all([ this._indexEntityResources(project), this._indexSpawnRules(project), this._indexEntityBehaviors(project), this._indexAttachables(project), this._indexItemTypes(project), this._indexFeatureBehaviors(project), this._indexAnimations(project), this._indexAnimationControllers(project), this._indexRenderControllers(project), this._indexModels(project), this._indexLootTables(project), ]); // Note: textures are not indexed here because they require pack-root-relative // path resolution that varies per handler. Handlers fall back to getItemsByType(). this.isBuilt = true; } static EMPTY_ITEMS = []; /** Look up items in a map, returning empty array if not found */ getItemsById(map, id) { return map.get(id) || RelationsIndex.EMPTY_ITEMS; } /** * Add unique child items from the index to a parent item. * Deduplicates when multiple IDs in `idList` resolve to the same ProjectItem. * Returns the set of IDs from `idList` that were successfully matched, so callers * can determine which IDs remain unfulfilled. */ addUniqueChildItems(parentItem, indexMap, idList) { const addedItems = new Set(); const matchedIds = new Set(); for (const id of idList) { const matchingItems = this.getItemsById(indexMap, id); if (matchingItems.length > 0) { matchedIds.add(id); } for (const candItem of matchingItems) { if (!addedItems.has(candItem)) { addedItems.add(candItem); parentItem.addChildItem(candItem); } } } return matchedIds; } _addToIndex(map, key, item) { let arr = map.get(key); if (!arr) { arr = []; map.set(key, arr); } arr.push(item); } async _indexEntityResources(project) { const items = project.getItemsByType(IProjectItemData_1.ProjectItemType.entityTypeResource); for (const item of items) { if (item.primaryFile) { const def = await EntityTypeResourceDefinition_1.default.ensureOnFile(item.primaryFile); if (def?.id) { this._addToIndex(this.entityResourcesById, def.id, item); } } } } async _indexSpawnRules(project) { const items = project.getItemsByType(IProjectItemData_1.ProjectItemType.spawnRuleBehavior); for (const item of items) { if (item.primaryFile) { const def = await SpawnRulesBehaviorDefinition_1.default.ensureOnFile(item.primaryFile); if (def?.id) { this._addToIndex(this.spawnRulesById, def.id, item); } } } } async _indexEntityBehaviors(project) { const items = project.getItemsByType(IProjectItemData_1.ProjectItemType.entityTypeBehavior); for (const item of items) { if (item.primaryFile) { const def = await EntityTypeDefinition_1.default.ensureOnFile(item.primaryFile); if (def?.id) { this._addToIndex(this.entityBehaviorsById, def.id, item); } } } } async _indexAttachables(project) { const items = project.getItemsByType(IProjectItemData_1.ProjectItemType.attachableResourceJson); for (const item of items) { if (item.primaryFile) { const def = await AttachableResourceDefinition_1.default.ensureOnFile(item.primaryFile); if (def?.id) { this._addToIndex(this.attachablesById, def.id, item); } } } } async _indexItemTypes(project) { const items = project.getItemsByType(IProjectItemData_1.ProjectItemType.itemTypeBehavior); for (const item of items) { if (item.primaryFile) { const def = await ItemTypeDefinition_1.default.ensureOnFile(item.primaryFile); if (def?.id) { this._addToIndex(this.itemTypesById, def.id, item); } } } } async _indexAnimations(project) { const items = project.getItemsByType(IProjectItemData_1.ProjectItemType.animationResourceJson); for (const item of items) { if (item.primaryFile) { const def = await AnimationResourceDefinition_1.default.ensureOnFile(item.primaryFile); if (def?.idList) { for (const id of def.idList) { this._addToIndex(this.animationsById, id, item); } } } } } async _indexAnimationControllers(project) { const items = project.getItemsByType(IProjectItemData_1.ProjectItemType.animationControllerResourceJson); for (const item of items) { if (item.primaryFile) { const def = await AnimationControllerResourceDefinition_1.default.ensureOnFile(item.primaryFile); if (def?.idList) { for (const id of def.idList) { this._addToIndex(this.animationControllersById, id, item); } } } } } async _indexRenderControllers(project) { const items = project.getItemsByType(IProjectItemData_1.ProjectItemType.renderControllerJson); for (const item of items) { if (item.primaryFile) { const def = await RenderControllerSetDefinition_1.default.ensureOnFile(item.primaryFile); if (def?.idList) { for (const id of def.idList) { this._addToIndex(this.renderControllersById, id, item); } } } } } async _indexModels(project) { const items = project.getItemsByType(IProjectItemData_1.ProjectItemType.modelGeometryJson); for (const item of items) { if (item.primaryFile) { const def = await ModelGeometryDefinition_1.default.ensureOnFile(item.primaryFile); if (def) { for (const id of def.identifiers) { this._addToIndex(this.modelsById, id, item); } } } } } async _indexLootTables(project) { const items = project.getItemsByType(IProjectItemData_1.ProjectItemType.lootTableBehavior); for (const item of items) { if (item.projectPath) { this.lootTablesByPath.set(item.projectPath, item); } } } async _indexFeatureBehaviors(project) { const items = project.getItemsByType(IProjectItemData_1.ProjectItemType.featureBehavior); for (const item of items) { if (item.primaryFile) { const def = await FeatureDefinition_1.default.ensureOnFile(item.primaryFile); if (def?.id) { this._addToIndex(this.featureBehaviorsById, def.id, item); } } } } } exports.default = RelationsIndex;