@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
1,290 lines • 110 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.ContentGenerator = void 0;
exports.generateContent = generateContent;
const CreatorToolsHost_1 = __importDefault(require("../app/CreatorToolsHost"));
const ImageCodec_1 = __importDefault(require("../core/ImageCodec"));
const PngEncoder_1 = __importDefault(require("./PngEncoder"));
const ItemTextureTemplates_1 = require("./ItemTextureTemplates");
const traits_1 = require("./traits");
const ModelDesignUtilities_1 = __importDefault(require("./ModelDesignUtilities"));
const TexturedRectangleGenerator_1 = __importDefault(require("./TexturedRectangleGenerator"));
const TextureEffects_1 = require("./TextureEffects");
const ModelDesignTemplates_1 = require("./ModelDesignTemplates");
// Initialize trait registry on module load
let traitsInitialized = false;
function ensureTraitsInitialized() {
if (!traitsInitialized) {
(0, traits_1.registerAllEntityTraits)();
(0, traits_1.registerAllBlockTraits)();
(0, traits_1.registerAllItemTraits)();
traitsInitialized = true;
}
}
// ============================================================================
// TRAIT EXPANSION
// ============================================================================
/**
* Trait definitions - maps trait IDs to native Minecraft components.
*/
const ENTITY_TRAIT_COMPONENTS = {
// Body types (primarily affect geometry/animation, but also some components)
humanoid: {
"minecraft:can_climb": {},
"minecraft:jump.static": {},
},
quadruped: {
"minecraft:can_climb": {},
"minecraft:jump.static": {},
},
quadruped_small: {
"minecraft:can_climb": {},
"minecraft:jump.static": {},
},
flying: {
"minecraft:navigation.fly": {
can_path_over_water: true,
can_path_over_lava: false,
},
"minecraft:can_fly": {},
},
aquatic: {
"minecraft:navigation.swim": {
can_path_over_water: false,
can_swim: true,
},
"minecraft:underwater_movement": { value: 0.3 },
"minecraft:breathable": {
total_supply: 15,
suffocate_time: 0,
breathes_water: true,
breathes_air: false,
},
},
arthropod: {
"minecraft:can_climb": {},
"minecraft:mark_variant": { value: 0 },
},
slime: {
"minecraft:movement.sway": { sway_amplitude: 0.0 },
},
// Behavior archetypes
hostile: {
"minecraft:behavior.hurt_by_target": { priority: 1 },
"minecraft:behavior.nearest_attackable_target": {
priority: 2,
entity_types: [{ filters: { test: "is_family", subject: "other", value: "player" } }],
},
"minecraft:attack": { damage: 3 },
},
passive: {
"minecraft:behavior.panic": {
priority: 1,
speed_multiplier: 1.25,
},
},
neutral: {
"minecraft:behavior.hurt_by_target": {
priority: 1,
alert_same_type: true,
},
},
boss: {
"minecraft:boss": {
should_darken_sky: true,
hud_range: 55,
},
},
// Combat styles
melee_attacker: {
"minecraft:behavior.melee_attack": {
priority: 3,
speed_multiplier: 1.2,
track_target: true,
},
"minecraft:attack": { damage: 3 },
},
ranged_attacker: {
"minecraft:behavior.ranged_attack": {
priority: 3,
attack_interval_min: 1.0,
attack_interval_max: 3.0,
attack_radius: 15.0,
},
"minecraft:shooter": {
def: "minecraft:arrow",
},
},
exploder: {
"minecraft:explode": {
fuse_length: 1.5,
fuse_lit: false,
power: 3,
causes_fire: false,
},
},
// Interaction
trader: {
"minecraft:trade_table": {},
"minecraft:behavior.trade_with_player": { priority: 1 },
},
tameable: {
"minecraft:tameable": {
probability: 0.33,
tame_items: ["bone"],
},
"minecraft:is_tamed": {},
},
rideable: {
"minecraft:rideable": {
seat_count: 1,
family_types: ["player"],
interact_text: "action.interact.ride.horse",
seats: [{ position: [0.0, 1.1, -0.2] }],
},
"minecraft:input_ground_controlled": {},
},
breedable: {
"minecraft:breedable": {
require_tame: false,
breed_items: ["wheat"],
breeds_with: { mate_type: "self", baby_type: "self" },
},
"minecraft:behavior.breed": { priority: 3, speed_multiplier: 1.0 },
},
leasable: {
"minecraft:leashable": {
soft_distance: 4.0,
hard_distance: 6.0,
max_distance: 10.0,
},
},
// Special traits
undead: {
"minecraft:burns_in_daylight": {},
"minecraft:type_family": { family: ["undead", "monster"] },
},
illager: {
"minecraft:type_family": { family: ["illager", "monster"] },
"minecraft:behavior.raid_garden": { priority: 5 },
},
aquatic_only: {
"minecraft:breathable": {
total_supply: 15,
suffocate_time: -1,
breathes_water: true,
breathes_air: false,
generates_bubbles: false,
},
},
baby_variant: {
"minecraft:is_baby": {},
"minecraft:scale": { value: 0.5 },
"minecraft:ageable": {
duration: 1200,
grow_up: { event: "minecraft:ageable_grow_up" },
},
},
wanders: {
"minecraft:behavior.random_stroll": { priority: 6, speed_multiplier: 1.0 },
"minecraft:behavior.random_look_around": { priority: 7 },
},
patrols: {
"minecraft:behavior.move_to_poi": { priority: 3, speed_multiplier: 0.6 },
},
guards: {
"minecraft:behavior.defend_village_target": { priority: 1 },
},
flees_daylight: {
"minecraft:behavior.flee_sun": { priority: 2, speed_multiplier: 1.0 },
},
teleporter: {
"minecraft:teleport": {
random_teleports: true,
max_random_teleport_time: 30,
random_teleport_cube: [32, 16, 32],
target_distance: 16,
target_teleport_chance: 0.05,
},
},
};
/**
* Block trait components.
*/
const BLOCK_TRAIT_COMPONENTS = {
solid: {},
transparent: {
"minecraft:material_instances": {
"*": { render_method: "blend" },
},
},
leaves: {
"minecraft:material_instances": {
"*": { render_method: "alpha_test" },
},
"minecraft:destructible_by_mining": { seconds_to_destroy: 0.2 },
},
log: {
"minecraft:destructible_by_mining": { seconds_to_destroy: 2.0 },
"minecraft:flammable": { catch_chance_modifier: 5, destroy_chance_modifier: 5 },
},
slab: {
"minecraft:geometry": "minecraft:geometry.slab",
},
stairs: {
"minecraft:geometry": "minecraft:geometry.stairs",
},
fence: {
"minecraft:geometry": { identifier: "minecraft:geometry.fence" },
},
wall: {
"minecraft:geometry": { identifier: "minecraft:geometry.wall" },
},
door: {
"minecraft:geometry": { identifier: "minecraft:geometry.door" },
"minecraft:on_interact": {
event: "toggle_open",
},
},
trapdoor: {
"minecraft:geometry": { identifier: "minecraft:geometry.trapdoor" },
"minecraft:on_interact": {
event: "toggle_open",
},
},
workstation: {},
light_source: {
"minecraft:light_emission": 15,
},
gravity: {
"minecraft:gravity": {},
},
liquid: {
"minecraft:material_instances": {
"*": { render_method: "blend" },
},
},
redstone_signal: {
"minecraft:redstone_conductivity": {
redstone_conductor: true,
allows_wire_to_step_down: true,
},
},
redstone_receiver: {
"minecraft:redstone_conductivity": {
redstone_conductor: true,
},
},
button: {
"minecraft:geometry": { identifier: "minecraft:geometry.button" },
},
lever: {
"minecraft:geometry": { identifier: "minecraft:geometry.lever" },
},
pressure_plate: {
"minecraft:geometry": { identifier: "minecraft:geometry.pressure_plate" },
},
flammable: {
"minecraft:flammable": { catch_chance_modifier: 5, destroy_chance_modifier: 20 },
},
explosion_resistant: {
"minecraft:destructible_by_explosion": { explosion_resistance: 1200 },
},
slippery: {
"minecraft:friction": 0.1,
},
};
/**
* Item trait components.
*/
const ITEM_TRAIT_COMPONENTS = {
sword: {
"minecraft:hand_equipped": true,
"minecraft:damage": 4,
"minecraft:enchantable": { slot: "sword", value: 10 },
},
pickaxe: {
"minecraft:hand_equipped": true,
"minecraft:digger": {
use_efficiency: true,
destroy_speeds: [{ block: { tags: "q.any_tag('stone', 'metal')" }, speed: 4 }],
},
"minecraft:enchantable": { slot: "pickaxe", value: 10 },
},
axe: {
"minecraft:hand_equipped": true,
"minecraft:digger": {
use_efficiency: true,
destroy_speeds: [{ block: { tags: "q.any_tag('wood', 'pumpkin')" }, speed: 4 }],
},
"minecraft:enchantable": { slot: "axe", value: 10 },
},
shovel: {
"minecraft:hand_equipped": true,
"minecraft:digger": {
use_efficiency: true,
destroy_speeds: [{ block: { tags: "q.any_tag('dirt', 'sand', 'gravel')" }, speed: 4 }],
},
"minecraft:enchantable": { slot: "shovel", value: 10 },
},
hoe: {
"minecraft:hand_equipped": true,
"minecraft:enchantable": { slot: "hoe", value: 10 },
},
bow: {
"minecraft:use_duration": 72000,
"minecraft:enchantable": { slot: "bow", value: 1 },
},
crossbow: {
"minecraft:use_duration": 72000,
"minecraft:enchantable": { slot: "crossbow", value: 1 },
},
food: {
"minecraft:food": {
nutrition: 4,
saturation_modifier: "normal",
can_always_eat: false,
},
"minecraft:use_duration": 32,
},
armor_helmet: {
"minecraft:wearable": { slot: "slot.armor.head" },
"minecraft:enchantable": { slot: "armor_head", value: 10 },
},
armor_chestplate: {
"minecraft:wearable": { slot: "slot.armor.chest" },
"minecraft:enchantable": { slot: "armor_torso", value: 10 },
},
armor_leggings: {
"minecraft:wearable": { slot: "slot.armor.legs" },
"minecraft:enchantable": { slot: "armor_legs", value: 10 },
},
armor_boots: {
"minecraft:wearable": { slot: "slot.armor.feet" },
"minecraft:enchantable": { slot: "armor_feet", value: 10 },
},
throwable: {
"minecraft:throwable": {
do_swing_animation: true,
launch_power_scale: 1.0,
max_launch_power: 1.0,
},
"minecraft:projectile": {
projectile_entity: "minecraft:snowball",
},
},
placeable: {
"minecraft:block_placer": {
block: "minecraft:stone",
},
},
};
// ============================================================================
// BEHAVIOR PRESET EXPANSION
// ============================================================================
/**
* Maps behavior presets to AI goal components.
*/
const BEHAVIOR_PRESET_COMPONENTS = {
// Movement
wander: {
"minecraft:behavior.random_stroll": { priority: 6, speed_multiplier: 1.0 },
},
swim: {
"minecraft:behavior.random_swim": { priority: 4, speed_multiplier: 1.0 },
},
fly_around: {
"minecraft:behavior.random_fly": { priority: 6, xz_dist: 4, y_dist: 2 },
},
float: {
"minecraft:behavior.float": { priority: 0 },
},
climb: {
"minecraft:can_climb": {},
},
// Combat
melee_attack: {
"minecraft:behavior.melee_attack": { priority: 3, speed_multiplier: 1.0 },
"minecraft:attack": { damage: 3 },
},
ranged_attack: {
"minecraft:behavior.ranged_attack": { priority: 3, attack_radius: 15.0 },
},
target_players: {
"minecraft:behavior.nearest_attackable_target": {
priority: 2,
entity_types: [{ filters: { test: "is_family", subject: "other", value: "player" } }],
},
},
target_monsters: {
"minecraft:behavior.nearest_attackable_target": {
priority: 2,
entity_types: [{ filters: { test: "is_family", subject: "other", value: "monster" } }],
},
},
flee_when_hurt: {
"minecraft:behavior.panic": { priority: 1, speed_multiplier: 1.25 },
},
retaliate: {
"minecraft:behavior.hurt_by_target": { priority: 1 },
},
// Social
follow_owner: {
"minecraft:behavior.follow_owner": {
priority: 4,
speed_multiplier: 1.0,
start_distance: 10,
stop_distance: 2,
},
},
follow_parent: {
"minecraft:behavior.follow_parent": { priority: 5, speed_multiplier: 1.0 },
},
herd: {
"minecraft:behavior.move_towards_dwelling_restriction": { priority: 4 },
},
avoid_players: {
"minecraft:behavior.avoid_mob_type": {
priority: 1,
entity_types: [{ filters: { test: "is_family", subject: "other", value: "player" } }],
max_dist: 10,
walk_speed_multiplier: 0.8,
sprint_speed_multiplier: 1.2,
},
},
// Interaction
look_at_player: {
"minecraft:behavior.look_at_player": {
priority: 7,
look_distance: 6.0,
probability: 0.02,
},
},
beg: {
"minecraft:behavior.beg": {
priority: 8,
look_distance: 8.0,
items: ["bone"],
},
},
tempt: {
"minecraft:behavior.tempt": {
priority: 4,
speed_multiplier: 1.0,
items: ["wheat"],
},
},
sit_command: {
"minecraft:behavior.sit": { priority: 2 },
"minecraft:sittable": {},
},
// Actions
eat_grass: {
"minecraft:behavior.eat_block": {
priority: 6,
time_until_eat: 1.8,
eat_and_replace_block_pairs: [{ eat_block: "grass", replace_block: "dirt" }],
},
},
break_doors: {
"minecraft:behavior.break_door": { priority: 1 },
},
open_doors: {
"minecraft:behavior.open_door": { priority: 6, close_door_after: true },
},
pick_up_items: {
"minecraft:behavior.pickup_items": { priority: 7, max_dist: 3 },
},
sleep_in_bed: {
"minecraft:behavior.sleep": { priority: 3, speed_multiplier: 1.2 },
},
// Environment
hide_from_sun: {
"minecraft:behavior.flee_sun": { priority: 2, speed_multiplier: 1.0 },
},
go_home_at_night: {
"minecraft:behavior.go_home": { priority: 4, speed_multiplier: 1.0, goal_radius: 1.5 },
},
seek_water: {
"minecraft:behavior.go_and_give_items_to_noteblock": { priority: 5 },
},
seek_land: {
"minecraft:behavior.move_to_land": { priority: 1, search_range: 16 },
},
};
// ============================================================================
// MAIN GENERATOR CLASS
// ============================================================================
/**
* Generates native Minecraft content from meta-schema definitions.
*/
class ContentGenerator {
_definition;
_options;
_namespace;
_warnings = [];
_errors = [];
/**
* Sanitizes an ID for safe use in file paths.
* Strips path separators and traversal sequences to prevent directory escape.
*/
static _sanitizeIdForPath(id) {
// Remove null bytes, path separators, and traversal sequences
return id.replace(/\0/g, "").replace(/\.\./g, "").replace(/[/\\]/g, "_").replace(/:/g, "_");
}
constructor(definition) {
this._definition = definition;
this._options = definition.options || {};
this._namespace = definition.namespace || "custom";
}
// ============================================================================
// TRAIT VALIDATION
// ============================================================================
/**
* Validates trait combinations for entities, blocks, and items.
* Checks for conflicting traits and adds warnings to the result.
*
* @param result - The generation result to add warnings to
*/
_validateTraitCombinations() {
// Validate entity traits
if (this._definition.entityTypes) {
for (const entity of this._definition.entityTypes) {
if (entity.traits && entity.traits.length > 1) {
this._validateEntityTraits(entity.id, entity.traits);
}
}
}
// Validate block traits
if (this._definition.blockTypes) {
for (const block of this._definition.blockTypes) {
if (block.traits && block.traits.length > 1) {
this._validateBlockTraits(block.id, block.traits);
}
}
}
// Validate item traits
if (this._definition.itemTypes) {
for (const item of this._definition.itemTypes) {
if (item.traits && item.traits.length > 1) {
this._validateItemTraits(item.id, item.traits);
}
}
}
}
/**
* Validates entity trait combinations for conflicts.
*/
_validateEntityTraits(entityId, traits) {
const traitSet = new Set(traits);
for (const traitId of traits) {
const trait = traits_1.TraitRegistry.getEntityTrait(traitId);
if (trait) {
const traitData = trait.getData();
// Check for conflicts
if (traitData.conflicts) {
for (const conflictId of traitData.conflicts) {
if (traitSet.has(conflictId)) {
this._warnings.push(`Entity '${entityId}': Trait '${traitId}' conflicts with '${conflictId}'. ` +
`These traits may produce unexpected behavior when combined.`);
}
}
}
}
}
// Check common known conflicts not defined in trait data
this._checkKnownEntityConflicts(entityId, traitSet);
}
/**
* Validates block trait combinations for conflicts.
*/
_validateBlockTraits(blockId, traits) {
const traitSet = new Set(traits);
for (const traitId of traits) {
const trait = traits_1.TraitRegistry.getBlockTrait(traitId);
if (trait) {
const traitData = trait.getData();
// Check for conflicts
if (traitData.conflicts) {
for (const conflictId of traitData.conflicts) {
if (traitSet.has(conflictId)) {
this._warnings.push(`Block '${blockId}': Trait '${traitId}' conflicts with '${conflictId}'. ` +
`These traits may produce unexpected behavior when combined.`);
}
}
}
}
}
// Check common known conflicts
this._checkKnownBlockConflicts(blockId, traitSet);
}
/**
* Validates item trait combinations for conflicts.
*/
_validateItemTraits(itemId, traits) {
const traitSet = new Set(traits);
for (const traitId of traits) {
const trait = traits_1.TraitRegistry.getItemTrait(traitId);
if (trait) {
const traitData = trait.getData();
// Check for conflicts
if (traitData.conflicts) {
for (const conflictId of traitData.conflicts) {
if (traitSet.has(conflictId)) {
this._warnings.push(`Item '${itemId}': Trait '${traitId}' conflicts with '${conflictId}'. ` +
`These traits may produce unexpected behavior when combined.`);
}
}
}
}
}
// Check common known conflicts
this._checkKnownItemConflicts(itemId, traitSet);
}
/**
* Checks for common entity trait conflicts that may not be defined in trait data.
*/
_checkKnownEntityConflicts(entityId, traits) {
// Behavior conflicts: hostile, passive, neutral are mutually exclusive
const behaviorTraits = ["hostile", "passive", "neutral"].filter((t) => traits.has(t));
if (behaviorTraits.length > 1) {
this._warnings.push(`Entity '${entityId}': Multiple behavior traits (${behaviorTraits.join(", ")}) are mutually exclusive. ` +
`Only one behavior archetype should be used.`);
}
// Body type conflicts: only one body type should be selected
const bodyTypeTraits = [
"humanoid",
"quadruped",
"quadruped_small",
"flying",
"aquatic",
"arthropod",
"slime",
].filter((t) => traits.has(t));
if (bodyTypeTraits.length > 1) {
this._warnings.push(`Entity '${entityId}': Multiple body types (${bodyTypeTraits.join(", ")}) specified. ` +
`Only one body type should be selected.`);
}
// aquatic and flying don't mix well
if (traits.has("aquatic") && traits.has("flying")) {
this._warnings.push(`Entity '${entityId}': 'aquatic' and 'flying' traits may conflict. ` +
`Consider using one or the other for cleaner behavior.`);
}
// undead with passive is unusual
if (traits.has("undead") && traits.has("passive")) {
this._warnings.push(`Entity '${entityId}': 'undead' trait is typically used with hostile entities, not passive ones.`);
}
}
/**
* Checks for common block trait conflicts.
*/
_checkKnownBlockConflicts(blockId, traits) {
// Shape conflicts: only one shape should be selected
const shapeTraits = ["slab", "stairs", "fence", "wall", "door", "trapdoor", "button", "lever"].filter((t) => traits.has(t));
if (shapeTraits.length > 1) {
this._warnings.push(`Block '${blockId}': Multiple shape traits (${shapeTraits.join(", ")}) specified. ` +
`Only one shape type should be selected.`);
}
// transparent and solid are typically mutually exclusive
if (traits.has("transparent") && traits.has("solid")) {
this._warnings.push(`Block '${blockId}': 'transparent' and 'solid' traits may be redundant. ` +
`Consider which visual style you want.`);
}
}
/**
* Checks for common item trait conflicts.
*/
_checkKnownItemConflicts(itemId, traits) {
// Tool conflicts: items should typically be one tool type
const toolTraits = ["sword", "pickaxe", "axe", "shovel", "hoe"].filter((t) => traits.has(t));
if (toolTraits.length > 1) {
this._warnings.push(`Item '${itemId}': Multiple tool traits (${toolTraits.join(", ")}) specified. ` +
`Items are typically one tool type.`);
}
// Armor conflicts: items should be one armor slot
const armorTraits = ["armor_helmet", "armor_chestplate", "armor_leggings", "armor_boots"].filter((t) => traits.has(t));
if (armorTraits.length > 1) {
this._warnings.push(`Item '${itemId}': Multiple armor slot traits (${armorTraits.join(", ")}) specified. ` +
`Items can only occupy one armor slot.`);
}
// Food with weapon is unusual
if (traits.has("food") && toolTraits.length > 0) {
this._warnings.push(`Item '${itemId}': 'food' trait combined with tool trait (${toolTraits.join(", ")}). ` +
`This is unusual - consider if this is intentional.`);
}
}
/**
* Generate all content from the definition.
*/
async generate() {
// Ensure trait registry is initialized
ensureTraitsInitialized();
// Validate trait combinations before generation
this._validateTraitCombinations();
const result = {
entityBehaviors: [],
entityResources: [],
blockBehaviors: [],
blockResources: [],
itemBehaviors: [],
itemResources: [],
structures: [],
features: [],
featureRules: [],
lootTables: [],
recipes: [],
spawnRules: [],
textures: [],
geometries: [],
renderControllers: [],
sounds: [],
summary: {
namespace: this._namespace,
entityCount: 0,
blockCount: 0,
itemCount: 0,
structureCount: 0,
featureCount: 0,
lootTableCount: 0,
recipeCount: 0,
spawnRuleCount: 0,
textureCount: 0,
warnings: [],
errors: [],
},
};
// Generate manifests. Resource pack first so the behavior pack can declare
// a dependency on it — without this link, Bedrock will not load the RP at
// runtime and entities/blocks/items render invisible.
result.resourcePackManifest = this._generateResourceManifest();
const rpHeaderUuid = result.resourcePackManifest.content?.header?.uuid;
result.behaviorPackManifest = this._generateBehaviorManifest(rpHeaderUuid);
// Generate entities
if (this._definition.entityTypes) {
for (const entity of this._definition.entityTypes) {
await this._generateEntity(entity, result);
}
result.summary.entityCount = this._definition.entityTypes.length;
}
// Generate blocks
if (this._definition.blockTypes) {
for (const block of this._definition.blockTypes) {
await this._generateBlock(block, result);
}
result.summary.blockCount = this._definition.blockTypes.length;
}
// Generate items
if (this._definition.itemTypes) {
for (const item of this._definition.itemTypes) {
await this._generateItem(item, result);
}
result.summary.itemCount = this._definition.itemTypes.length;
}
// Generate loot tables
if (this._definition.lootTables) {
for (const lootTable of this._definition.lootTables) {
this._generateLootTable(lootTable, result);
}
result.summary.lootTableCount = this._definition.lootTables.length;
}
// Generate recipes
if (this._definition.recipes) {
for (const recipe of this._definition.recipes) {
this._generateRecipe(recipe, result);
}
result.summary.recipeCount = this._definition.recipes.length;
}
// Generate spawn rules
if (this._definition.spawnRules) {
for (const spawnRule of this._definition.spawnRules) {
this._generateSpawnRule(spawnRule, result);
}
result.summary.spawnRuleCount = this._definition.spawnRules.length;
}
// Generate features
if (this._definition.features) {
for (const feature of this._definition.features) {
this._generateFeature(feature, result);
}
result.summary.featureCount = this._definition.features.length;
}
// Generate structures
if (this._definition.structures) {
for (const structure of this._definition.structures) {
await this._generateStructure(structure, result);
}
result.summary.structureCount = this._definition.structures.length;
}
// Generate terrain_texture.json for blocks
if (this._definition.blockTypes && this._definition.blockTypes.length > 0) {
result.terrainTextures = this._generateTerrainTextures(this._definition.blockTypes);
result.blocksCatalog = this._generateBlocksCatalog(this._definition.blockTypes);
}
// Generate item_texture.json for items
if (this._definition.itemTypes && this._definition.itemTypes.length > 0) {
result.itemTextures = this._generateItemTextures(this._definition.itemTypes);
}
result.summary.textureCount = result.textures.length;
result.summary.warnings = this._warnings;
result.summary.errors = this._errors;
return result;
}
// ============================================================================
// MANIFEST GENERATION
// ============================================================================
_generateBehaviorManifest(resourcePackHeaderUuid) {
const uuid1 = this._generateUuid();
const uuid2 = this._generateUuid();
const content = {
format_version: 2,
header: {
name: this._definition.displayName || `${this._namespace} Behavior Pack`,
description: this._definition.description || `Generated content for ${this._namespace}`,
uuid: uuid1,
version: [1, 0, 0],
min_engine_version: [1, 21, 0],
},
modules: [
{
type: "data",
uuid: uuid2,
version: [1, 0, 0],
},
],
};
// BP must declare a dependency on its sibling RP, otherwise Bedrock won't
// load the RP and all entities/blocks render invisible. This was a recurring
// root-cause bug that historically required many server restarts to identify.
if (resourcePackHeaderUuid) {
content.dependencies = [
{
uuid: resourcePackHeaderUuid,
version: [1, 0, 0],
},
];
}
return {
path: "manifest.json",
pack: "behavior",
type: "json",
content,
};
}
_generateResourceManifest() {
const uuid1 = this._generateUuid();
const uuid2 = this._generateUuid();
return {
path: "manifest.json",
pack: "resource",
type: "json",
content: {
format_version: 2,
header: {
name: this._definition.displayName || `${this._namespace} Resource Pack`,
description: this._definition.description || `Resources for ${this._namespace}`,
uuid: uuid1,
version: [1, 0, 0],
min_engine_version: [1, 21, 0],
},
modules: [
{
type: "resources",
uuid: uuid2,
version: [1, 0, 0],
},
],
},
};
}
// ============================================================================
// ENTITY GENERATION
// ============================================================================
async _generateEntity(entity, result) {
const safeId = ContentGenerator._sanitizeIdForPath(entity.id);
const fullId = `${this._namespace}:${entity.id}`;
// Build components from traits first
let components = {
"minecraft:type_family": { family: entity.families || [entity.id] },
"minecraft:collision_box": {
width: entity.collisionWidth || 0.6,
height: entity.collisionHeight || 1.8,
},
"minecraft:physics": {},
"minecraft:pushable": { is_pushable: true, is_pushable_by_piston: true },
"minecraft:movement": { value: entity.movementSpeed || 0.25 },
"minecraft:movement.basic": {},
"minecraft:navigation.walk": {
can_path_over_water: true,
avoid_damage_blocks: true,
},
};
// Collect component groups and events from traits
let componentGroups = {};
let events = {};
let spawnEvent = undefined;
// Apply traits using the new trait system
if (entity.traits) {
for (const traitId of entity.traits) {
// First try the new registry-based traits
const trait = traits_1.TraitRegistry.getEntityTrait(traitId);
if (trait) {
const traitData = trait.getData({
attackDamage: entity.attackDamage,
tameItems: entity.tameItems,
tameChance: entity.tameChance,
});
// Merge components
if (traitData.components) {
components = { ...components, ...traitData.components };
}
// Merge component groups
if (traitData.componentGroups) {
componentGroups = { ...componentGroups, ...traitData.componentGroups };
}
// Merge events
if (traitData.events) {
events = { ...events, ...traitData.events };
}
// Collect spawn events (last one wins if multiple)
if (traitData.spawnEvent) {
if (!spawnEvent) {
spawnEvent = traitData.spawnEvent;
}
else {
// Merge spawn events - combine component groups to add
if (spawnEvent.add?.component_groups && traitData.spawnEvent.add?.component_groups) {
spawnEvent.add.component_groups = [
...spawnEvent.add.component_groups,
...traitData.spawnEvent.add.component_groups,
];
}
}
}
}
else {
// Fall back to legacy ENTITY_TRAIT_COMPONENTS lookup
const traitComponents = ENTITY_TRAIT_COMPONENTS[traitId];
if (traitComponents) {
components = { ...components, ...traitComponents };
}
}
}
}
// Apply behavior presets
if (entity.behaviors) {
for (const behavior of entity.behaviors) {
const behaviorComponents = BEHAVIOR_PRESET_COMPONENTS[behavior];
if (behaviorComponents) {
components = { ...components, ...behaviorComponents };
}
}
}
// Deduplicate movement controllers. Bedrock allows only ONE
// `minecraft:movement.*` component per entity; having both `movement.basic`
// (added as default above) and a trait-provided controller like
// `movement.sway` triggers a "Mobs can only have 1 Move Control Component"
// error at runtime. If a trait supplied a more specific movement.* the
// default basic controller must be dropped.
const movementKeys = Object.keys(components).filter((k) => k.startsWith("minecraft:movement."));
if (movementKeys.length > 1 && components["minecraft:movement.basic"] !== undefined) {
delete components["minecraft:movement.basic"];
}
// Apply simplified properties
if (entity.health !== undefined) {
components["minecraft:health"] = { value: entity.health, max: entity.health };
}
if (entity.attackDamage !== undefined) {
components["minecraft:attack"] = { damage: entity.attackDamage };
}
if (entity.followRange !== undefined) {
components["minecraft:follow_range"] = { value: entity.followRange };
}
if (entity.knockbackResistance !== undefined) {
components["minecraft:knockback_resistance"] = { value: entity.knockbackResistance };
}
if (entity.scale !== undefined) {
components["minecraft:scale"] = { value: entity.scale };
}
// appearance.scale overrides entity.scale if both are set (documented in schema).
if (entity.appearance?.scale !== undefined) {
components["minecraft:scale"] = { value: entity.appearance.scale };
}
// Apply native components (override everything)
if (entity.components) {
components = { ...components, ...entity.components };
}
// Generate behavior pack entity
const behaviorEntity = {
format_version: "1.21.0",
"minecraft:entity": {
description: {
identifier: fullId,
is_spawnable: true,
is_summonable: true,
is_experimental: false,
},
components,
},
};
// Merge component groups from traits with those specified directly
const mergedComponentGroups = { ...componentGroups };
if (entity.componentGroups) {
for (const [key, value] of Object.entries(entity.componentGroups)) {
mergedComponentGroups[key] = value;
}
}
if (Object.keys(mergedComponentGroups).length > 0) {
behaviorEntity["minecraft:entity"].component_groups = mergedComponentGroups;
}
// Merge events from traits with those specified directly
const mergedEvents = { ...events };
if (entity.events) {
for (const [key, value] of Object.entries(entity.events)) {
mergedEvents[key] = value;
}
}
// Add spawn event if we have one
if (spawnEvent) {
mergedEvents["minecraft:entity_spawned"] = spawnEvent;
}
if (Object.keys(mergedEvents).length > 0) {
behaviorEntity["minecraft:entity"].events = mergedEvents;
}
result.entityBehaviors.push({
path: `entities/${safeId}.json`,
pack: "behavior",
type: "json",
content: behaviorEntity,
});
// Generate resource pack entity
const resourceEntity = this._generateEntityResource(entity, fullId);
result.entityResources.push({
path: `entity/${safeId}.entity.json`,
pack: "resource",
type: "json",
content: resourceEntity,
});
// Try model-design-based generation for geometry + texture
const designResult = await this._generateEntityFromModelDesign(entity);
if (designResult) {
result.geometries.push({
path: `models/entity/${safeId}.geo.json`,
pack: "resource",
type: "json",
content: designResult.geometry,
});
if (designResult.texture) {
result.textures.push({
path: `textures/entity/${safeId}.png`,
pack: "resource",
type: "png",
content: designResult.texture,
});
}
}
else {
// Fallback: use legacy geometry + placeholder texture
const geometry = this._generateEntityGeometry(entity);
result.geometries.push({
path: `models/entity/${safeId}.geo.json`,
pack: "resource",
type: "json",
content: geometry,
});
const texture = await this._generateEntityTexturePlaceholder(entity);
if (texture) {
result.textures.push({
path: `textures/entity/${safeId}.png`,
pack: "resource",
type: "png",
content: texture,
});
}
}
// Generate render controller for the entity
const renderController = this._generateEntityRenderController(entity);
result.renderControllers.push({
path: `render_controllers/${safeId}.render_controllers.json`,
pack: "resource",
type: "json",
content: renderController,
});
// Generate loot table from drops if specified
if (entity.drops && entity.drops.length > 0) {
const lootTable = this._generateLootTableFromDrops(entity.id, entity.drops);
result.lootTables.push(lootTable);
// Update entity to reference loot table
behaviorEntity["minecraft:entity"].components["minecraft:loot"] = {
table: `loot_tables/entities/${entity.id}.json`,
};
}
// Generate spawn rule if specified
if (entity.spawning) {
const spawnRule = this._generateSpawnRuleFromConfig(entity.id, fullId, entity.spawning);
result.spawnRules.push(spawnRule);
}
}
// ============================================================================
// MODEL-DESIGN-BASED ENTITY GENERATION
// ============================================================================
/**
* Maps a content meta-schema bodyType to a model template type.
*/
_bodyTypeToTemplateType(bodyType) {
switch (bodyType) {
case "quadruped":
return "large_animal";
case "quadruped_small":
return "small_animal";
default:
return bodyType;
}
}
/**
* Maps an appearance.textureStyle value to the corresponding TexturedRectangleType
* used in model-design textures. Falls back to the template's existing background type
* (or 'stipple_noise') when no textureStyle is specified.
*/
_textureStyleToType(textureStyle, fallback) {
if (!textureStyle) {
return fallback || "stipple_noise";
}
switch (textureStyle) {
case "solid":
return "solid";
case "spotted":
return "stipple_noise";
case "striped":
return "dither_noise";
case "gradient":
return "gradient";
case "organic":
return "perlin_noise";
case "armored":
// Stippled base + an outset lighting effect is applied by the caller.
return "stipple_noise";
default:
return fallback || "stipple_noise";
}
}
/**
* Creates a recolored copy of a model design template using the user's primary/secondary colors.
* Each named texture in the template gets a distinct color derived from the user's choices,
* producing visually distinct faces that make it obvious how to customize the mob.
*/
_recolorModelDesign(template, primaryColor, secondaryColor, entityId, textureStyle) {
const parseHex = (hex) => {
const h = hex.startsWith("#") ? hex.slice(1) : hex;
return {
r: parseInt(h.slice(0, 2), 16) || 128,
g: parseInt(h.slice(2, 4), 16) || 128,
b: parseInt(h.slice(4, 6), 16) || 128,
};
};
const toHex = (c) => {
const cl = (v) => Math.max(0, Math.min(255, Math.round(v)));
return `#${cl(c.r).toString(16).padStart(2, "0")}${cl(c.g).toString(16).padStart(2, "0")}${cl(c.b).toString(16).padStart(2, "0")}`;
};
const primary = parseHex(primaryColor);
const secondary = parseHex(secondaryColor);
// Generate a palette of distinct colors derived from primary/secondary
const colorVariants = [
primary, // variant 0: primary as-is
secondary, // variant 1: secondary as-is
{ r: Math.min(255, primary.r + 40), g: Math.max(0, primary.g - 15), b: Math.max(0, primary.b - 25) }, // variant 2: warmer/brighter
{ r: Math.max(0, secondary.r - 20), g: Math.max(0, secondary.g - 20), b: Math.min(255, secondary.b + 30) }, // variant 3: cooler/darker
{
r: Math.min(255, Math.round((primary.r + secondary.r) / 2 + 30)), // variant 4: midtone lighter
g: Math.min(255, Math.round((primary.g + secondary.g) / 2 + 30)),
b: Math.min(255, Math.round((primary.b + secondary.b) / 2 + 30)),
},
{
r: Math.max(0, Math.round(primary.r * 0.6)), // variant 5: darkened primary
g: Math.max(0, Math.round(primary.g * 0.6)),
b: Math.max(0, Math.round(primary.b * 0.6)),
},
];
// Build new textures dict with recolored versions
const newTextures = {};
const textureNames = template.textures ? Object.keys(template.textures) : [];
for (let i = 0; i < textureNames.length; i++) {
const name = textureNames[i];
const original = template.textures[name];
const variant = colorVariants[i % colorVariants.length];
// Create three color variants for noise textures (base, slightly lighter, slightly darker)
const c1 = toHex(variant);
const c2 = toHex({
r: Math.min(255, variant.r + 15),
g: Math.min(255, variant.g + 15),
b: Math.min(255, variant.b + 15),
});
const c3 = toHex({
r: Math.max(0, variant.r - 15),
g: Math.max(0, variant.g - 15),
b: Math.max(0, variant.b - 15),
});
newTextures[name] = {
background: {
type: this._textureStyleToType(textureStyle, original.background?.type),
colors: [c1, c2, c3],
seed: (original.background?.seed || 1000) + i,
},
effects: textureStyle === "armored"
? { ...(original.effects || {}), lighting: { preset: "outset", intensity: 0.4 } }
: original.effects,
pixelArt: original.pixelArt,
};
}
// Deep clone the design and replace textures
const cloned = JSON.parse(JSON.stringify(template));
cloned.textures = newTextures;
cloned.identifier = `${this._namespace}_${entityId}`;
return cloned;
}
/**
* Generates entity geometry and texture using the model design pipeline.
* Uses model templates with per-face textured rectangles for high-quality output.
* Returns null if template loading fails, in which case the caller should use legacy generation.
*/
async _generateEntityFromModelDesign(entity) {
const appearance = entity.appearance || {};
const bodyType = appearance.bodyType || "humanoid";
const primaryColor = appearance.primaryColor || "#5B8C3E";
const secondaryColor = appearance.secondaryColor || "#3D6B2E";
// Map bodyType to template type and load template
const templateType = this._bodyTypeToTemplateType(bodyType);
let template;
try {
template = await (0, ModelDesignTemplates_1.getModelTemplateAsync)(templateType);
}
catch {
// Template not available - fall back to legacy
return null;
}
if (!template) {
return null;
}
// Recolor the template with the user's colors
const design = this._recolorModelDesign(template, primaryColor, secondaryColor, entity.id, appearance.textureStyle);
// Convert model design to geometry + atlas regions
const conversionResult = ModelDesignUtilities_1.default.convertToGeometry(design);
// Override the geometry identifier to use our namespace
const geometryId = `geometry.${this._namespace}.${entity.id}`;
const geoData = conversionResult.geometry;
if (geoData["minecraft:geometry"] && geoData["minecraft:geometry"][0]) {
geoData["minecraft:geometry"][0].description.identifier = geometryId;
}
// Render atlas regions into a pixel buffer (same approach as ImageGenerationUtilities)
const [texWidth, texHeight] = conversionResult.textureSize;
const pixels = new Uint8Array(texWidth * texHeight * 4);
// Initialize with transparent
for (let i = 0; i < pixels.length; i += 4) {
pixels[i + 3] = 0;
}
// Render each atlas region
for (const region of conversio