UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

531 lines (530 loc) 23.2 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.addCommand = exports.AddCommand = void 0; exports.validateWizardFlags = validateWizardFlags; exports.shouldUseWizardMode = shouldUseWizardMode; exports.buildDefinitionFromFlags = buildDefinitionFromFlags; const IToolCommand_1 = require("../IToolCommand"); const AutocompleteProviders_1 = require("../AutocompleteProviders"); const ProjectItemCreateManager_1 = __importDefault(require("../../ProjectItemCreateManager")); const IProjectData_1 = require("../../IProjectData"); const IProjectItemData_1 = require("../../IProjectItemData"); // ============================================================================ // CONSTANTS // ============================================================================ /** Content types that support wizard-mode generation (traits + properties). */ const WIZARD_CONTENT_TYPES = ["entity", "block", "item"]; /** Entity body-type trait IDs (exclusive — at most one). */ const ENTITY_BODY_TYPES = ["humanoid", "quadruped", "quadruped_small", "flying", "aquatic", "arthropod", "slime"]; /** * Maps content type names to their preferred ProjectItemType for template matching. */ const CONTENT_TYPE_TO_ITEM_TYPE = { spawn_rule: IProjectItemData_1.ProjectItemType.spawnRuleBehavior, loot_table: IProjectItemData_1.ProjectItemType.lootTableBehavior, trade_table: IProjectItemData_1.ProjectItemType.tradingBehaviorJson, }; // ============================================================================ // AUTOCOMPLETE PROVIDERS FOR WIZARD FLAGS // ============================================================================ const bodyTypeProvider = (partial) => { const lower = partial.toLowerCase(); return ENTITY_BODY_TYPES.filter((t) => t.startsWith(lower)); }; function validateWizardFlags(contentType, flags) { const errors = []; const lowerType = contentType.toLowerCase(); const checkRange = (name, min, max) => { const val = flags[name]; if (val !== undefined) { const n = Number(val); if (isNaN(n) || n < min || n > max) { errors.push({ flag: name, message: `--${name} must be between ${min} and ${max} (got ${val})` }); } } }; const checkColor = (name) => { const val = flags[name]; if (val !== undefined && !/^#[0-9a-fA-F]{6}$/.test(val)) { errors.push({ flag: name, message: `--${name} must be a hex color like #FF0000 (got ${val})` }); } }; // Type-specific validation ranges if (lowerType === "entity") { checkRange("health", 1, 100); checkRange("damage", 0, 20); checkRange("speed", 0.1, 1.0); } else if (lowerType === "block") { checkRange("destroy-time", 0, 10); checkRange("light-emission", 0, 15); } else if (lowerType === "item") { checkRange("max-stack", 1, 64); checkRange("durability", 0, 2000); } checkColor("color"); checkColor("secondary-color"); // Validate traits belong to the correct content type const traits = flags.traits; if (traits && traits.length > 0) { let validTraits; switch (lowerType) { case "entity": validTraits = AutocompleteProviders_1.ENTITY_TRAITS; break; case "block": validTraits = AutocompleteProviders_1.BLOCK_TRAITS; break; case "item": validTraits = AutocompleteProviders_1.ITEM_TRAITS; break; default: validTraits = []; } for (const trait of traits) { if (!validTraits.includes(trait)) { errors.push({ flag: "traits", message: `Trait "${trait}" is not valid for ${contentType} type` }); } } // Check body-type exclusivity for entities if (lowerType === "entity") { const bodyTraits = traits.filter((t) => ENTITY_BODY_TYPES.includes(t)); if (bodyTraits.length > 1) { errors.push({ flag: "traits", message: `Only one body type trait allowed, got: ${bodyTraits.join(", ")}`, }); } } } return errors; } // ============================================================================ // DEFINITION BUILDER // ============================================================================ /** Flags specific to wizard mode that trigger the content generator path. */ const WIZARD_FLAG_NAMES = [ "health", "damage", "speed", "body-type", "color", "secondary-color", "destroy-time", "light-emission", "max-stack", "durability", "display-name", "namespace", ]; /** * Determine whether wizard-mode generation should be used instead of gallery mode. * Returns true if traits or any wizard-specific flag is provided. */ function shouldUseWizardMode(contentType, traits, flags) { if (!WIZARD_CONTENT_TYPES.includes(contentType.toLowerCase())) { return false; } if (traits && traits.length > 0) { return true; } return WIZARD_FLAG_NAMES.some((name) => flags[name] !== undefined); } /** * Build an IMinecraftContentDefinition from CLI args and flags. * Mirrors the _buildDefinition() logic in ContentWizard.tsx. */ function buildDefinitionFromFlags(contentType, name, traits, flags) { const namespace = flags.namespace || "custom"; const displayName = flags["display-name"] || name.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); const definition = { schemaVersion: "1.0.0", namespace, }; const lowerType = contentType.toLowerCase(); if (lowerType === "entity") { let entityTraits = [...(traits || [])]; // If --body-type flag is provided, ensure it's the only body type in the traits list const bodyType = flags["body-type"]; if (bodyType) { // Remove any existing body type traits (flag takes priority) entityTraits = entityTraits.filter((t) => !ENTITY_BODY_TYPES.includes(t)); if (!entityTraits.includes(bodyType)) { entityTraits.push(bodyType); } } definition.entityTypes = [ { id: name, displayName, traits: entityTraits.length > 0 ? entityTraits : undefined, health: flags.health !== undefined ? Number(flags.health) : 20, attackDamage: flags.damage !== undefined ? Number(flags.damage) : 3, movementSpeed: flags.speed !== undefined ? Number(flags.speed) : 0.25, appearance: { bodyType: bodyType || entityTraits.find((t) => ENTITY_BODY_TYPES.includes(t)) || "humanoid", primaryColor: flags.color || "#4A7BA5", secondaryColor: flags["secondary-color"] || "#2D4F6B", }, }, ]; } else if (lowerType === "block") { definition.blockTypes = [ { id: name, displayName, traits: traits && traits.length > 0 ? traits : undefined, destroyTime: flags["destroy-time"] !== undefined ? Number(flags["destroy-time"]) : 3, lightEmission: flags["light-emission"] !== undefined ? Number(flags["light-emission"]) : 0, mapColor: flags.color || undefined, }, ]; } else if (lowerType === "item") { const itemTraits = (traits || []).filter((t) => t !== "custom"); definition.itemTypes = [ { id: name, displayName, traits: itemTraits.length > 0 ? itemTraits : undefined, maxStackSize: flags["max-stack"] !== undefined ? Number(flags["max-stack"]) : 64, durability: flags.durability !== undefined ? Number(flags.durability) : undefined, color: flags.color || undefined, }, ]; } return definition; } // ============================================================================ // ADD COMMAND // ============================================================================ class AddCommand extends IToolCommand_1.ToolCommandBase { metadata = { name: "add", description: "Add new content to the current project", aliases: ["a"], category: "Content", arguments: [ { name: "type", description: "Type of content to add (entity, block, item, script, etc.) or gallery template ID", type: "string", required: true, autocompleteProvider: AutocompleteProviders_1.contentTypeProvider, }, { name: "name", description: "Name for the new content item", type: "identifier", required: true, }, ], flags: [ // --- Shared flags --- { name: "traits", shortName: "t", description: "Comma-separated list of traits to apply (e.g., hostile,melee_attacker)", type: "stringArray", autocompleteProvider: AutocompleteProviders_1.allTraitsProvider, }, { name: "template", description: "Specific gallery template ID to use (gallery mode only)", type: "string", autocompleteProvider: (0, AutocompleteProviders_1.createGalleryItemProvider)(), }, { name: "display-name", description: "Display name shown in-game (defaults to formatted version of name)", type: "string", }, { name: "namespace", shortName: "n", description: "Namespace for the content (defaults to 'custom')", type: "string", }, { name: "color", description: "Primary color as hex (e.g., #FF0000)", type: "string", }, // --- Entity-specific flags --- { name: "health", description: "Entity health points (1-100, default 20)", type: "number", }, { name: "damage", description: "Entity attack damage (0-20, default 3)", type: "number", }, { name: "speed", description: "Entity movement speed (0.1-1.0, default 0.25)", type: "number", }, { name: "body-type", description: "Entity body type (humanoid, quadruped, flying, aquatic, etc.)", type: "string", autocompleteProvider: bodyTypeProvider, }, { name: "secondary-color", description: "Entity secondary/accent color as hex (e.g., #00FF00)", type: "string", }, // --- Block-specific flags --- { name: "destroy-time", description: "Block mining time in seconds (0-10, default 3)", type: "number", }, { name: "light-emission", description: "Block light emission level (0-15, default 0)", type: "number", }, // --- Item-specific flags --- { name: "max-stack", description: "Item max stack size (1-64, default 64)", type: "number", }, { name: "durability", description: "Item durability (0-2000)", type: "number", }, ], isWriteCommand: true, examples: [ "/add entity my_mob", "/add entity my_orc --traits hostile,melee_attacker,humanoid --health 30 --damage 5", "/add block my_brick --traits solid --destroy-time 3", "/add item my_sword --traits sword --durability 500 --max-stack 1", "/add script my_behavior", "/add allay custom_allay", ], }; async execute(context, args, flags) { const validationError = this.validateRequiredArgs(args); if (validationError) { return validationError; } const typeOrTemplate = args[0]; const name = args[1]; const traits = flags.traits; const explicitTemplate = flags.template; if (!context.creatorTools) { return this.error("NO_CREATOR_TOOLS", "No CreatorTools instance available."); } const creatorTools = context.creatorTools; // If no project is open, auto-create an addon starter project first if (!context.project) { context.output.info("No project open. Creating a new Add-On Starter project..."); await creatorTools.loadGallery(); const starterTemplate = await creatorTools.getGalleryProjectById("addonstarter"); if (!starterTemplate) { return this.error("TEMPLATE_NOT_FOUND", "Could not find the Add-On Starter template."); } const newProjectName = await creatorTools.getNewProjectName("my-project"); let project = await creatorTools.createNewProject(newProjectName, undefined, undefined, undefined, IProjectData_1.ProjectFocus.general, false, undefined); if (!project) { return this.error("PROJECT_ERROR", "Could not create project."); } const ProjectExporter = (await Promise.resolve().then(() => __importStar(require("../../ProjectExporter")))).default; project = await ProjectExporter.syncProjectFromGitHub(true, creatorTools, starterTemplate.gitHubRepoName, starterTemplate.gitHubOwner, starterTemplate.gitHubBranch, starterTemplate.gitHubFolder, newProjectName, project, starterTemplate.fileList, async (message) => { context.output.debug(message); }, true); await project.save(); context.project = project; context.output.info(`Created project '${newProjectName}'.`); } // ================================================================ // WIZARD MODE — procedural generation via ContentGenerator // ================================================================ if (shouldUseWizardMode(typeOrTemplate, traits, flags)) { return this._executeWizardMode(context, typeOrTemplate, name, traits, flags); } // ================================================================ // GALLERY MODE — add from pre-built templates // ================================================================ return this._executeGalleryMode(context, typeOrTemplate, name, explicitTemplate, flags); } /** * Wizard mode: build an IMinecraftContentDefinition → ContentGenerator → ContentWriter. */ async _executeWizardMode(context, contentType, name, traits, flags) { // Validate inputs const validationErrors = validateWizardFlags(contentType, flags); if (validationErrors.length > 0) { const messages = validationErrors.map((e) => e.message).join("; "); return this.error("VALIDATION_ERROR", messages); } // Build the content definition const definition = buildDefinitionFromFlags(contentType, name, traits, flags); // Generate content const { ContentGenerator } = await Promise.resolve().then(() => __importStar(require("../../../minecraft/ContentGenerator"))); const { ContentWriter } = await Promise.resolve().then(() => __importStar(require("../../../minecraft/ContentWriter"))); const generator = new ContentGenerator(definition); const content = await generator.generate(); // Check for generation errors if (content.summary.errors.length > 0) { return this.error("GENERATION_ERROR", `Content generation failed: ${content.summary.errors.join("; ")}`); } // Write to project await ContentWriter.writeGeneratedContent(context.project, content); // Re-infer project items so the new files are tracked await context.project.inferProjectItemsFromFiles(true); // Report warnings for (const warning of content.summary.warnings) { context.output.warn(warning); } // Build summary const parts = []; if (content.summary.entityCount > 0) parts.push(`${content.summary.entityCount} entity`); if (content.summary.blockCount > 0) parts.push(`${content.summary.blockCount} block`); if (content.summary.itemCount > 0) parts.push(`${content.summary.itemCount} item`); if (content.summary.textureCount > 0) parts.push(`${content.summary.textureCount} texture(s)`); const summaryText = `Generated ${parts.join(", ")} for '${name}'`; context.output.success(summaryText); return this.success(summaryText, { name, type: contentType, traits: traits || [], mode: "wizard", generated: { entities: content.summary.entityCount, blocks: content.summary.blockCount, items: content.summary.itemCount, textures: content.summary.textureCount, }, }); } /** * Gallery mode: add from a pre-built gallery template. */ async _executeGalleryMode(context, typeOrTemplate, name, explicitTemplate, flags) { const creatorTools = context.creatorTools; // Load gallery await creatorTools.loadGallery(); if (!creatorTools.gallery) { return this.error("GALLERY_ERROR", "Could not load project gallery"); } // Try to find as direct gallery item first let galleryItem = await creatorTools.getGalleryProjectById(explicitTemplate || typeOrTemplate); // If not found, map content type to gallery type and find default template if (!galleryItem) { const galleryType = AutocompleteProviders_1.CONTENT_TYPE_TO_GALLERY[typeOrTemplate.toLowerCase()]; if (galleryType !== undefined) { const items = creatorTools.getGalleryProjectByType(galleryType); if (items && items.length > 0) { const preferredItemType = CONTENT_TYPE_TO_ITEM_TYPE[typeOrTemplate.toLowerCase()]; if (preferredItemType) { const match = items.find((item) => item.targetType === preferredItemType || item.id === typeOrTemplate.toLowerCase()); if (match) { galleryItem = match; context.output.debug(`Using matched template '${match.id}' for type '${typeOrTemplate}'`); } } if (!galleryItem) { galleryItem = items[0]; context.output.debug(`Using template '${galleryItem.id}' for type '${typeOrTemplate}'`); } } } } if (!galleryItem) { const gallery = creatorTools.gallery; const types = Object.keys(AutocompleteProviders_1.CONTENT_TYPE_TO_GALLERY).join(", "); const templates = gallery.items .slice(0, 10) .map((i) => i.id) .join(", "); return this.error("TYPE_NOT_FOUND", `Unknown type or template '${typeOrTemplate}'. Valid types: ${types}. Example templates: ${templates}...`); } context.output.info(`Adding '${name}' from template '${galleryItem.title}'...`); try { await ProjectItemCreateManager_1.default.addFromGallery(context.project, name, galleryItem); await context.project.save(); context.output.success(`Added '${name}' to project`); return this.success(`Added ${name}`, { name, template: galleryItem.id, type: typeOrTemplate, mode: "gallery", }); } catch (error) { const message = error instanceof Error ? error.message : String(error); return this.error("ADD_ERROR", `Failed to add content: ${message}`); } } /** * Custom completions based on argument position and type context. */ async getCompletions(context, args, partialArg, argIndex) { const lower = partialArg.toLowerCase(); if (argIndex === 0) { const types = Object.keys(AutocompleteProviders_1.CONTENT_TYPE_TO_GALLERY).filter((t) => t.startsWith(lower)); if (context.creatorTools) { await context.creatorTools.loadGallery(); const gallery = context.creatorTools.gallery; const templateIds = gallery?.items.filter((i) => i.id.toLowerCase().startsWith(lower)).map((i) => i.id) || []; return [...types, ...templateIds]; } return types; } return []; } } exports.AddCommand = AddCommand; exports.addCommand = new AddCommand();