@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
310 lines (309 loc) • 14 kB
JavaScript
"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;