UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

1,124 lines (1,123 loc) 45.5 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 }); exports.DEFAULT_INFERRER_OPTIONS = void 0; const IProjectItemData_1 = require("../app/IProjectItemData"); const EntityTypeDefinition_1 = __importDefault(require("./EntityTypeDefinition")); const BlockTypeDefinition_1 = __importDefault(require("./BlockTypeDefinition")); const ItemTypeDefinition_1 = __importDefault(require("./ItemTypeDefinition")); const TraitDetector_1 = __importDefault(require("./TraitDetector")); /** * Default inferrer options. */ exports.DEFAULT_INFERRER_OPTIONS = { minTraitConfidence: 0.6, includeRawComponents: true, inferNamespace: true, includeBehaviorPresets: true, includeComponentGroups: false, includeEvents: false, }; // ============================================================================ // CONTENT SCHEMA INFERRER // ============================================================================ /** * ContentSchemaInferrer - Main class for analyzing content and inferring schema. */ class ContentSchemaInferrer { _options; constructor(options) { this._options = { ...exports.DEFAULT_INFERRER_OPTIONS, ...options }; } /** * Infer a content schema from a project. */ async inferFromProject(project) { const startTime = Date.now(); const warnings = []; const traitCounts = { entity: {}, block: {}, item: {}, }; // Ensure project items are loaded await project.inferProjectItemsFromFilesRootFolder(); // Analyze entities const entityItems = project.items.filter((item) => item.itemType === IProjectItemData_1.ProjectItemType.entityTypeBehavior); const entityTypes = []; for (const item of entityItems) { try { const entityDef = await this.inferEntityFromItem(item); if (entityDef) { entityTypes.push(entityDef.definition); // Count traits if (entityDef.definition.traits) { for (const trait of entityDef.definition.traits) { traitCounts.entity[trait] = (traitCounts.entity[trait] || 0) + 1; } } } } catch (e) { warnings.push(`Failed to analyze entity ${item.name}: ${e}`); } } // Analyze blocks const blockItems = project.items.filter((item) => item.itemType === IProjectItemData_1.ProjectItemType.blockTypeBehavior); const blockTypes = []; for (const item of blockItems) { try { const blockDef = await this.inferBlockFromItem(item); if (blockDef) { blockTypes.push(blockDef.definition); if (blockDef.definition.traits) { for (const trait of blockDef.definition.traits) { traitCounts.block[trait] = (traitCounts.block[trait] || 0) + 1; } } } } catch (e) { warnings.push(`Failed to analyze block ${item.name}: ${e}`); } } // Analyze items const itemItems = project.items.filter((item) => item.itemType === IProjectItemData_1.ProjectItemType.itemTypeBehavior); const itemTypes = []; for (const item of itemItems) { try { const itemDef = await this.inferItemFromItem(item); if (itemDef) { itemTypes.push(itemDef.definition); if (itemDef.definition.traits) { for (const trait of itemDef.definition.traits) { traitCounts.item[trait] = (traitCounts.item[trait] || 0) + 1; } } } } catch (e) { warnings.push(`Failed to analyze item ${item.name}: ${e}`); } } // Analyze spawn rules const spawnRuleItems = project.items.filter((item) => item.itemType === IProjectItemData_1.ProjectItemType.spawnRuleBehavior); const spawnRules = []; for (const item of spawnRuleItems) { try { const spawnRule = await this.inferSpawnRuleFromItem(item); if (spawnRule) { spawnRules.push(spawnRule); } } catch (e) { warnings.push(`Failed to analyze spawn rule ${item.name}: ${e}`); } } // Analyze loot tables const lootTableItems = project.items.filter((item) => item.itemType === IProjectItemData_1.ProjectItemType.lootTableBehavior); const lootTables = []; for (const item of lootTableItems) { try { const lootTable = await this.inferLootTableFromItem(item); if (lootTable) { lootTables.push(lootTable); } } catch (e) { warnings.push(`Failed to analyze loot table ${item.name}: ${e}`); } } // Analyze recipes const recipeItems = project.items.filter((item) => item.itemType === IProjectItemData_1.ProjectItemType.recipeBehavior); const recipes = []; for (const item of recipeItems) { try { const recipe = await this.inferRecipeFromItem(item); if (recipe) { recipes.push(recipe); } } catch (e) { warnings.push(`Failed to analyze recipe ${item.name}: ${e}`); } } // Analyze features const featureItems = project.items.filter((item) => item.itemType === IProjectItemData_1.ProjectItemType.featureBehavior); const features = []; for (const item of featureItems) { try { const feature = await this.inferFeatureFromItem(item); if (feature) { features.push(feature); } } catch (e) { warnings.push(`Failed to analyze feature ${item.name}: ${e}`); } } // Infer namespace let namespace; if (this._options.inferNamespace) { namespace = this.inferNamespace(entityTypes, blockTypes, itemTypes); } // Build the content definition const definition = { schemaVersion: "1.0.0", namespace, entityTypes: entityTypes.length > 0 ? entityTypes : undefined, blockTypes: blockTypes.length > 0 ? blockTypes : undefined, itemTypes: itemTypes.length > 0 ? itemTypes : undefined, spawnRules: spawnRules.length > 0 ? spawnRules : undefined, lootTables: lootTables.length > 0 ? lootTables : undefined, recipes: recipes.length > 0 ? recipes : undefined, features: features.length > 0 ? features : undefined, }; const endTime = Date.now(); return { definition, metadata: { entitiesAnalyzed: entityItems.length, blocksAnalyzed: blockItems.length, itemsAnalyzed: itemItems.length, spawnRulesAnalyzed: spawnRuleItems.length, lootTablesAnalyzed: lootTableItems.length, recipesAnalyzed: recipeItems.length, featuresAnalyzed: featureItems.length, warnings, allDetectedTraits: traitCounts, inferenceTimeMs: endTime - startTime, }, }; } /** * Infer entity type definition from a project item. */ async inferEntityFromItem(item) { if (!item.primaryFile) return null; await item.loadContent(); if (!item.primaryFile.isContentLoaded) { await item.primaryFile.loadContent(); } const entityDef = await EntityTypeDefinition_1.default.ensureOnFile(item.primaryFile); if (!entityDef || !entityDef.data) return null; return this.inferEntityFromDefinition(entityDef); } /** * Infer entity type from an EntityTypeDefinition. */ inferEntityFromDefinition(entityDef) { const data = entityDef.data; if (!data) return null; const wrapper = entityDef._wrapper; const entityData = wrapper?.["minecraft:entity"]; if (!entityData) return null; // Get identifier const fullId = entityData.description?.identifier || ""; const [namespace, shortId] = fullId.includes(":") ? fullId.split(":") : ["custom", fullId]; // Get components const components = entityData.components || {}; const componentGroups = entityData.component_groups || {}; // Detect traits const traitResults = TraitDetector_1.default.detectEntityTraits(components, componentGroups, this._options.minTraitConfidence); const traits = traitResults.map((r) => r.traitId); // Detect behavior presets let behaviors; if (this._options.includeBehaviorPresets) { const behaviorResults = TraitDetector_1.default.detectBehaviorPresets(components, this._options.minTraitConfidence); behaviors = behaviorResults.length > 0 ? behaviorResults.map((r) => r.traitId) : undefined; } // Extract simplified properties const props = TraitDetector_1.default.extractEntityProperties(components); // Get unexplained components let rawComponents; if (this._options.includeRawComponents) { const allDetections = [...traitResults]; const unexplained = TraitDetector_1.default.getUnexplainedComponents(components, allDetections); if (unexplained.length > 0) { rawComponents = {}; for (const compName of unexplained) { // Filter out very common components that are implicit if (!this.isImplicitComponent(compName)) { rawComponents[compName] = components[compName]; } } if (Object.keys(rawComponents).length === 0) { rawComponents = undefined; } } } // Build the inferred definition const definition = { id: shortId, displayName: this.formatDisplayName(shortId), }; // Add detected traits if (traits.length > 0) { definition.traits = traits; } // Add behaviors if (behaviors && behaviors.length > 0) { definition.behaviors = behaviors; } // Add simplified properties (only if different from defaults) if (props.health !== undefined && props.health !== 20) { definition.health = props.health; } if (props.attackDamage !== undefined && props.attackDamage !== 3) { definition.attackDamage = props.attackDamage; } if (props.movementSpeed !== undefined && props.movementSpeed !== 0.25) { definition.movementSpeed = props.movementSpeed; } if (props.scale !== undefined) { definition.scale = props.scale; } if (props.followRange !== undefined) { definition.followRange = props.followRange; } if (props.knockbackResistance !== undefined) { definition.knockbackResistance = props.knockbackResistance; } if (props.collisionWidth !== undefined || props.collisionHeight !== undefined) { definition.collisionWidth = props.collisionWidth; definition.collisionHeight = props.collisionHeight; } if (props.families && props.families.length > 0) { definition.families = props.families; } // Add raw components if any if (rawComponents) { definition.components = rawComponents; } // Add component groups if requested if (this._options.includeComponentGroups && Object.keys(componentGroups).length > 0) { definition.componentGroups = componentGroups; } // Add events if requested if (this._options.includeEvents) { const events = entityData.events; if (events && Object.keys(events).length > 0) { definition.events = events; } } return { definition, detectionDetails: traitResults, }; } /** * Infer block type definition from a project item. */ async inferBlockFromItem(item) { if (!item.primaryFile) return null; await item.loadContent(); if (!item.primaryFile.isContentLoaded) { await item.primaryFile.loadContent(); } const blockDef = await BlockTypeDefinition_1.default.ensureOnFile(item.primaryFile); if (!blockDef || !blockDef.data) return null; return this.inferBlockFromDefinition(blockDef); } /** * Infer block type from a BlockTypeDefinition. */ inferBlockFromDefinition(blockDef) { const wrapper = blockDef._wrapper; const blockData = wrapper?.["minecraft:block"]; if (!blockData) return null; // Get identifier const fullId = blockData.description?.identifier || ""; const [namespace, shortId] = fullId.includes(":") ? fullId.split(":") : ["custom", fullId]; // Get components const components = blockData.components || {}; // Detect block traits const detectedTraits = TraitDetector_1.default.detectBlockTraits(components, this._options.minTraitConfidence); const traitIds = detectedTraits.map((t) => t.traitId); // Extract simplified properties const props = TraitDetector_1.default.extractBlockProperties(components); // Build the inferred definition const definition = { id: shortId, displayName: this.formatDisplayName(shortId), }; // Add detected traits if (traitIds.length > 0) { definition.traits = traitIds; } // Add properties if (props.destroyTime !== undefined) { definition.destroyTime = props.destroyTime; } if (props.explosionResistance !== undefined) { definition.explosionResistance = props.explosionResistance; } if (props.lightEmission !== undefined && props.lightEmission > 0) { definition.lightEmission = props.lightEmission; } if (props.lightDampening !== undefined) { definition.lightDampening = props.lightDampening; } if (props.friction !== undefined && props.friction !== 0.6) { definition.friction = props.friction; } if (props.mapColor !== undefined) { definition.mapColor = props.mapColor; } // Add components if requested if (this._options.includeRawComponents) { // Filter to non-default components const significantComponents = {}; for (const [key, value] of Object.entries(components)) { if (!this.isImplicitBlockComponent(key)) { significantComponents[key] = value; } } if (Object.keys(significantComponents).length > 0) { definition.components = significantComponents; } } return { definition }; } /** * Infer item type definition from a project item. */ async inferItemFromItem(item) { if (!item.primaryFile) return null; await item.loadContent(); if (!item.primaryFile.isContentLoaded) { await item.primaryFile.loadContent(); } const itemDef = await ItemTypeDefinition_1.default.ensureOnFile(item.primaryFile); if (!itemDef || !itemDef.data) return null; return this.inferItemFromDefinition(itemDef); } /** * Infer item type from an ItemTypeDefinition. */ inferItemFromDefinition(itemDef) { const wrapper = itemDef._wrapper; const itemData = wrapper?.["minecraft:item"]; if (!itemData) return null; // Get identifier const fullId = itemData.description?.identifier || ""; const [namespace, shortId] = fullId.includes(":") ? fullId.split(":") : ["custom", fullId]; // Get components const components = itemData.components || {}; // Detect item traits const detectedTraits = TraitDetector_1.default.detectItemTraits(components, this._options.minTraitConfidence); const traitIds = detectedTraits.map((t) => t.traitId); // Extract simplified properties const props = TraitDetector_1.default.extractItemProperties(components); // Build the inferred definition const definition = { id: shortId, displayName: this.formatDisplayName(shortId), }; // Add detected traits if (traitIds.length > 0) { definition.traits = traitIds; } // Add properties if (props.maxStackSize !== undefined && props.maxStackSize !== 64) { definition.maxStackSize = props.maxStackSize; } if (props.durability !== undefined) { definition.durability = props.durability; } // Food properties if (props.nutrition !== undefined) { definition.food = { nutrition: props.nutrition, saturation: props.saturation, }; } // Weapon properties if (props.damage !== undefined) { definition.weapon = { damage: props.damage, }; } // Add components if requested if (this._options.includeRawComponents) { const significantComponents = {}; for (const [key, value] of Object.entries(components)) { if (!this.isImplicitItemComponent(key)) { significantComponents[key] = value; } } if (Object.keys(significantComponents).length > 0) { definition.components = significantComponents; } } return { definition }; } // ============================================================================ // WORLD GEN CONTENT INFERENCE // ============================================================================ /** * Infer spawn rule definition from a project item. */ async inferSpawnRuleFromItem(item) { if (!item.primaryFile) return null; await item.loadContent(); if (!item.primaryFile.isContentLoaded) { await item.primaryFile.loadContent(); } const content = item.primaryFile.content; if (typeof content !== "string") return null; try { const data = JSON.parse(content); const spawnRule = data["minecraft:spawn_rules"]; if (!spawnRule) return null; const description = spawnRule.description || {}; const conditions = spawnRule.conditions || []; const definition = { entity: description.identifier?.replace(/^[^:]+:/, "") || item.name.replace(".json", ""), }; // Extract spawn conditions from the first condition group if (conditions.length > 0) { const condition = conditions[0]; // Biome filter if (condition["minecraft:biome_filter"]) { const biomeFilter = condition["minecraft:biome_filter"]; const biomes = this.extractBiomesFromFilter(biomeFilter); if (biomes.length > 0) { definition.biomes = biomes; } } // Brightness filter (light level) if (condition["minecraft:brightness_filter"]) { const brightness = condition["minecraft:brightness_filter"]; definition.lightLevel = { min: brightness.min ?? 0, max: brightness.max ?? 15, }; } // Height filter if (condition["minecraft:height_filter"]) { const height = condition["minecraft:height_filter"]; definition.heightRange = { min: height.min ?? -64, max: height.max ?? 320, }; } // Weight if (condition["minecraft:weight"]) { definition.weight = condition["minecraft:weight"].default; } // Herd/group size if (condition["minecraft:herd"]) { const herd = condition["minecraft:herd"]; definition.groupSize = { min: herd.min_size ?? 1, max: herd.max_size ?? 1, }; } // Spawn on block if (condition["minecraft:spawns_on_block_filter"]) { const blocks = condition["minecraft:spawns_on_block_filter"]; if (Array.isArray(blocks)) { definition.spawnOn = blocks.map((b) => b.replace("minecraft:", "")); } else if (typeof blocks === "string") { definition.spawnOn = [blocks.replace("minecraft:", "")]; } } // Surface/underground if (condition["minecraft:spawns_on_surface"]) { definition.surface = true; } if (condition["minecraft:spawns_underground"]) { definition.surface = false; } // Time of day if (condition["minecraft:difficulty_filter"]) { // Use difficulty as a proxy for time preferences sometimes } } return definition; } catch { return null; } } /** * Extract biome names from a biome filter. */ extractBiomesFromFilter(filter) { const biomes = []; if (!filter) return biomes; if (filter.test === "has_biome_tag" && filter.value) { biomes.push(filter.value); } if (filter.any_of) { for (const subFilter of filter.any_of) { biomes.push(...this.extractBiomesFromFilter(subFilter)); } } if (filter.all_of) { for (const subFilter of filter.all_of) { biomes.push(...this.extractBiomesFromFilter(subFilter)); } } return biomes; } /** * Infer loot table definition from a project item. */ async inferLootTableFromItem(item) { if (!item.primaryFile) return null; await item.loadContent(); if (!item.primaryFile.isContentLoaded) { await item.primaryFile.loadContent(); } const content = item.primaryFile.content; if (typeof content !== "string") return null; try { const data = JSON.parse(content); const pools = data.pools || []; // Derive ID from file path const id = item.name.replace(".json", "").replace(/^loot_tables\//, ""); const definition = { id, pools: [], }; for (const pool of pools) { const rollsValue = pool.rolls; let rolls = 1; if (typeof rollsValue === "number") { rolls = rollsValue; } else if (typeof rollsValue === "object" && rollsValue !== null) { rolls = { min: rollsValue.min ?? 1, max: rollsValue.max ?? 1, }; } const entries = []; for (const entry of pool.entries || []) { if (entry.type === "item" && entry.name) { const lootEntry = { item: entry.name.replace("minecraft:", ""), }; if (entry.weight !== undefined) { lootEntry.weight = entry.weight; } // Extract count from functions if (entry.functions) { for (const fn of entry.functions) { if (fn.function === "set_count") { if (typeof fn.count === "number") { lootEntry.count = fn.count; } else if (typeof fn.count === "object") { lootEntry.count = { min: fn.count.min ?? 1, max: fn.count.max ?? 1, }; } } if (fn.function === "looting_enchant") { lootEntry.lootingBonus = fn.count?.max ?? 1; } } } entries.push(lootEntry); } } if (entries.length > 0) { definition.pools.push({ rolls, entries }); } } return definition.pools.length > 0 ? definition : null; } catch { return null; } } /** * Infer recipe definition from a project item. */ async inferRecipeFromItem(item) { if (!item.primaryFile) return null; await item.loadContent(); if (!item.primaryFile.isContentLoaded) { await item.primaryFile.loadContent(); } const content = item.primaryFile.content; if (typeof content !== "string") return null; try { const data = JSON.parse(content); // Determine recipe type if (data["minecraft:recipe_shaped"]) { return this.inferShapedRecipe(data["minecraft:recipe_shaped"], item.name); } else if (data["minecraft:recipe_shapeless"]) { return this.inferShapelessRecipe(data["minecraft:recipe_shapeless"], item.name); } else if (data["minecraft:recipe_furnace"]) { return this.inferFurnaceRecipe(data["minecraft:recipe_furnace"], item.name); } else if (data["minecraft:recipe_brewing_mix"] || data["minecraft:recipe_brewing_container"]) { const brewingData = data["minecraft:recipe_brewing_mix"] || data["minecraft:recipe_brewing_container"]; return this.inferBrewingRecipe(brewingData, item.name); } else if (data["minecraft:recipe_smithing_transform"]) { return this.inferSmithingRecipe(data["minecraft:recipe_smithing_transform"], item.name); } return null; } catch { return null; } } /** * Infer a shaped recipe. */ inferShapedRecipe(recipe, fileName) { const description = recipe.description || {}; const id = description.identifier?.replace(/^[^:]+:/, "") || fileName.replace(".json", ""); const result = recipe.result || recipe.output; let resultItem; if (typeof result === "string") { resultItem = result.replace("minecraft:", ""); } else if (result.item) { if (result.count && result.count > 1) { resultItem = { item: result.item.replace("minecraft:", ""), count: result.count }; } else { resultItem = result.item.replace("minecraft:", ""); } } else { resultItem = "unknown"; } const definition = { id, type: "shaped", result: resultItem, pattern: recipe.pattern || [], key: {}, }; // Convert key if (recipe.key) { for (const [symbol, value] of Object.entries(recipe.key)) { if (typeof value === "string") { definition.key[symbol] = value.replace("minecraft:", ""); } else if (value.item) { definition.key[symbol] = value.item.replace("minecraft:", ""); } } } return definition; } /** * Infer a shapeless recipe. */ inferShapelessRecipe(recipe, fileName) { const description = recipe.description || {}; const id = description.identifier?.replace(/^[^:]+:/, "") || fileName.replace(".json", ""); const result = recipe.result || recipe.output; let resultItem; if (typeof result === "string") { resultItem = result.replace("minecraft:", ""); } else if (result.item) { if (result.count && result.count > 1) { resultItem = { item: result.item.replace("minecraft:", ""), count: result.count }; } else { resultItem = result.item.replace("minecraft:", ""); } } else { resultItem = "unknown"; } const ingredients = []; for (const ing of recipe.ingredients || []) { if (typeof ing === "string") { ingredients.push(ing.replace("minecraft:", "")); } else if (ing.item) { ingredients.push(ing.item.replace("minecraft:", "")); } } return { id, type: "shapeless", result: resultItem, ingredients, }; } /** * Infer a furnace recipe. */ inferFurnaceRecipe(recipe, fileName) { const description = recipe.description || {}; const id = description.identifier?.replace(/^[^:]+:/, "") || fileName.replace(".json", ""); const input = typeof recipe.input === "string" ? recipe.input.replace("minecraft:", "") : recipe.input?.item?.replace("minecraft:", "") || "unknown"; const output = typeof recipe.output === "string" ? recipe.output.replace("minecraft:", "") : recipe.output?.item?.replace("minecraft:", "") || "unknown"; return { id, type: "furnace", result: output, input, }; } /** * Infer a brewing recipe. */ inferBrewingRecipe(recipe, fileName) { const description = recipe.description || {}; const id = description.identifier?.replace(/^[^:]+:/, "") || fileName.replace(".json", ""); return { id, type: "brewing", result: recipe.output?.replace("minecraft:", "") || "unknown", input: recipe.input?.replace("minecraft:", ""), }; } /** * Infer a smithing recipe. */ inferSmithingRecipe(recipe, fileName) { const description = recipe.description || {}; const id = description.identifier?.replace(/^[^:]+:/, "") || fileName.replace(".json", ""); return { id, type: "smithing", result: typeof recipe.result === "string" ? recipe.result.replace("minecraft:", "") : recipe.result?.item?.replace("minecraft:", "") || "unknown", }; } /** * Infer feature definition from a project item. */ async inferFeatureFromItem(item) { if (!item.primaryFile) return null; await item.loadContent(); if (!item.primaryFile.isContentLoaded) { await item.primaryFile.loadContent(); } const content = item.primaryFile.content; if (typeof content !== "string") return null; try { const data = JSON.parse(content); // Check for various feature types const featureTypes = [ "minecraft:ore_feature", "minecraft:scatter_feature", "minecraft:single_block_feature", "minecraft:aggregate_feature", "minecraft:tree_feature", "minecraft:vegetation_patch_feature", "minecraft:geode_feature", ]; for (const featureType of featureTypes) { if (data[featureType]) { return this.inferFeatureFromData(data[featureType], featureType, item.name); } } return null; } catch { return null; } } /** * Infer feature definition from raw data. */ inferFeatureFromData(featureData, featureType, fileName) { const description = featureData.description || {}; const id = description.identifier?.replace(/^[^:]+:/, "") || fileName.replace(".json", ""); const definition = { id, }; // Extract spread/placement info if (featureData.places_block) { const block = featureData.places_block; definition.spread = { places: [ { type: "block", id: typeof block === "string" ? block.replace("minecraft:", "") : block.name?.replace("minecraft:", ""), }, ], }; } // For ore features if (featureType === "minecraft:ore_feature") { const replaces = featureData.replace_rules?.[0]; if (replaces) { const places = featureData.replace_rules.map((rule) => ({ type: "ore", id: typeof rule.places_block === "string" ? rule.places_block.replace("minecraft:", "") : rule.places_block?.name?.replace("minecraft:", ""), replacesBlocks: Array.isArray(rule.may_replace) ? rule.may_replace.map((b) => b.replace("minecraft:", "")) : undefined, })); definition.spread = { places }; if (featureData.count) { definition.spread.count = typeof featureData.count === "number" ? featureData.count : { min: featureData.count.min ?? 1, max: featureData.count.max ?? 1 }; } } } // For scatter features if (featureType === "minecraft:scatter_feature") { if (featureData.scatter_chance) { if (!definition.spread) { definition.spread = { places: [] }; } definition.spread.rarity = 1 / (featureData.scatter_chance.numerator / featureData.scatter_chance.denominator); } } // Store native feature data if we have complex configurations if (this._options.includeRawComponents) { definition.nativeFeature = { [featureType.replace("minecraft:", "")]: featureData }; } return definition; } /** * Infer namespace from identifiers. */ inferNamespace(entities, blocks, items) { const namespaceCounts = {}; // This would need access to original identifiers which we stripped // For now, return undefined - could be improved later return undefined; } /** * Format an ID into a display name. */ formatDisplayName(id) { return id.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); } /** * Check if a component is implicit and doesn't need to be included. */ isImplicitComponent(componentName) { // Components that are usually added by default or are very common const implicitComponents = [ "minecraft:physics", "minecraft:pushable", "minecraft:collision_box", "minecraft:conditional_bandwidth_optimization", "minecraft:experience_reward", ]; return implicitComponents.includes(componentName); } /** * Check if a block component is implicit. */ isImplicitBlockComponent(componentName) { const implicitComponents = [ "minecraft:unit_cube", "minecraft:destructible_by_mining", "minecraft:destructible_by_explosion", ]; return implicitComponents.includes(componentName); } /** * Check if an item component is implicit. */ isImplicitItemComponent(componentName) { const implicitComponents = ["minecraft:max_stack_size", "minecraft:icon", "minecraft:display_name"]; return implicitComponents.includes(componentName); } // ============================================================================ // STATIC CONVENIENCE METHODS // ============================================================================ /** * Infer schema from a project (static convenience method). */ static async inferFromProject(project, options) { const inferrer = new ContentSchemaInferrer(options); return inferrer.inferFromProject(project); } /** * Infer schema from an entity definition (static convenience method). */ static inferEntityFromDefinition(entityDef, options) { const inferrer = new ContentSchemaInferrer(options); return inferrer.inferEntityFromDefinition(entityDef); } /** * Generate a lightweight summary of a project's content for AI context injection. * This produces an IProjectSchemaSummary that can be sent with chat messages * without bloating the context window. * * @param project The project to summarize * @returns A summary object suitable for AI context */ static async inferSummary(project) { await project.inferProjectItemsFromFiles(); const entityItems = project.getItemsByType(IProjectItemData_1.ProjectItemType.entityTypeBehavior); const blockItems = project.getItemsByType(IProjectItemData_1.ProjectItemType.blockTypeBehavior); const itemItems = project.getItemsByType(IProjectItemData_1.ProjectItemType.itemTypeBehavior); const recipeItems = project.getItemsByType(IProjectItemData_1.ProjectItemType.recipeBehavior); const lootItems = project.getItemsByType(IProjectItemData_1.ProjectItemType.lootTableBehavior); const spawnItems = project.getItemsByType(IProjectItemData_1.ProjectItemType.spawnRuleBehavior); // Extract IDs from items const entityIds = []; const blockIds = []; const itemIds = []; // Collect unique traits const entityTraits = new Set(); const blockTraits = new Set(); const itemTraits = new Set(); // Process entities to get IDs and traits for (const item of entityItems) { if (!item.primaryFile) continue; await item.loadContent(); const entityDef = await EntityTypeDefinition_1.default.ensureOnFile(item.primaryFile); if (entityDef) { const id = entityDef.id; if (id) { // Strip namespace for concise IDs const shortId = id.includes(":") ? id.split(":")[1] : id; entityIds.push(shortId); } // Detect traits from the wrapper's components const wrapper = entityDef._wrapper; const entityData = wrapper?.["minecraft:entity"]; const components = entityData?.components; if (components) { const componentGroups = entityData?.component_groups || {}; const detected = TraitDetector_1.default.detectEntityTraits(components, componentGroups, 0.6); for (const trait of detected) { entityTraits.add(trait.traitId); } } } } // Process blocks for (const item of blockItems) { if (!item.primaryFile) continue; await item.loadContent(); const blockDef = await BlockTypeDefinition_1.default.ensureOnFile(item.primaryFile); if (blockDef) { const id = blockDef.id; if (id) { const shortId = id.includes(":") ? id.split(":")[1] : id; blockIds.push(shortId); } // Detect traits const components = blockDef.getComponents(); if (components && Object.keys(components).length > 0) { const detected = TraitDetector_1.default.detectBlockTraits(components, 0.6); for (const trait of detected) { blockTraits.add(trait.traitId); } } } } // Process items for (const item of itemItems) { if (!item.primaryFile) continue; await item.loadContent(); const itemDef = await ItemTypeDefinition_1.default.ensureOnFile(item.primaryFile); if (itemDef) { const id = itemDef.id; if (id) { const shortId = id.includes(":") ? id.split(":")[1] : id; itemIds.push(shortId); } // Detect traits const components = itemDef.getComponents(); if (components && Object.keys(components).length > 0) { const detected = TraitDetector_1.default.detectItemTraits(components, 0.6); for (const trait of detected) { itemTraits.add(trait.traitId); } } } } // Infer namespace from first entity/block/item ID let namespace; const allItems = [...entityItems, ...blockItems, ...itemItems]; for (const item of allItems) { if (!item.primaryFile) continue; await item.loadContent(); // Try entity const entityDef = item.primaryFile.manager; if (entityDef?.id?.includes(":")) { namespace = entityDef.id.split(":")[0]; break; } // Try block const blockDef = item.primaryFile.manager; if (blockDef?.id?.includes(":")) { namespace = blockDef.id.split(":")[0]; break; } // Try item const itemDefCheck = item.primaryFile.manager; if (itemDefCheck?.id?.includes(":")) { namespace = itemDefCheck.id.split(":")[0]; break; } } // Prefer localFolderPath (the user's actual folder) over projectFolder (which may be a workspace path) const projectPath = project.localFolderPath || project.projectFolder?.fullPath; return { isSummarized: true, projectPath: projectPath, namespace, entityCount: entityItems.length, blockCount: blockItems.length, itemCount: itemItems.length, recipeCount: recipeItems.length, lootTableCount: lootItems.length, spawnRuleCount: spawnItems.length, entityIds: entityIds.length > 0 ? entityIds.slice(0, 50) : undefined, // Limit for context blockIds: blockIds.length > 0 ? blockIds.slice(0, 50) : undefined, itemIds: itemIds.length > 0 ? itemIds.slice(0, 50) : undefined, detectedEntityTraits: entityTraits.size > 0 ? Array.from(entityTraits) : undefined, detectedBlockTraits: blockTraits.size > 0 ? Array.from(blockTraits) : undefined, detectedItemTraits: itemTraits.size > 0 ? Array.from(itemTraits) : undefined, fullSchemaAvailableViaTool: "getEffectiveContentSchema", }; } } exports.default = ContentSchemaInferrer;