@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
1,124 lines (1,123 loc) • 45.5 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 });
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;