UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

866 lines (865 loc) 45.1 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.MATERIAL_NAMES_TO_FIXUP = exports.STANDARD_NAME_TOKEN = void 0; const Log_1 = __importDefault(require("../core/Log")); const Utilities_1 = __importDefault(require("../core/Utilities")); const BlockTypeDefinition_1 = __importDefault(require("../minecraft/BlockTypeDefinition")); const Database_1 = __importDefault(require("../minecraft/Database")); const MinecraftDefinitions_1 = __importDefault(require("../minecraft/MinecraftDefinitions")); const ModelDesignUtilities_1 = __importDefault(require("../minecraft/ModelDesignUtilities")); const HttpStorage_1 = __importDefault(require("../storage/HttpStorage")); const StorageUtilities_1 = __importDefault(require("../storage/StorageUtilities")); const CreatorToolsHost_1 = __importDefault(require("./CreatorToolsHost")); const IGalleryItem_1 = require("./IGalleryItem"); const IProjectItemData_1 = require("./IProjectItemData"); const Project_1 = require("./Project"); const ProjectItemInference_1 = __importDefault(require("./ProjectItemInference")); const ProjectItemUtilities_1 = __importDefault(require("./ProjectItemUtilities")); exports.STANDARD_NAME_TOKEN = "_name_"; exports.MATERIAL_NAMES_TO_FIXUP = [ "cold", "warm", "body", "head", "legs", "eyes", "flower", "charging", "masked", "bioluminescent_layer", "armor", "charged", "ghost", "cape", "body_layer", "outer", "animated", "spectator", "overlay", "limbs", "breeze_eyes", "breeze_wind", "invisible", ]; class ProjectCreateManager { /** * Checks whether a name already exists in the project for the given item type. * Comparison is case-insensitive and normalizes underscores/spaces/hyphens. * Also handles file extensions (e.g., "frost_moose.behavior" matches "frost_moose"). */ static nameExistsInProject(project, name, itemType) { const normalized = name.toLowerCase().replace(/[- ]/g, "_"); const items = project.getItemsByType(itemType); for (const item of items) { const itemName = item.name.toLowerCase().replace(/[- ]/g, "_"); if (itemName === normalized || itemName.startsWith(normalized + ".") || itemName === normalized + ".json" || itemName === normalized + ".behavior") { return true; } // Also compare after stripping common extensions from item name const dotIndex = itemName.indexOf("."); if (dotIndex > 0) { const baseName = itemName.substring(0, dotIndex); if (baseName === normalized) { return true; } } } return false; } /** * Generates a unique name for a new item by appending " 2", " 3", etc. if the * base name already exists in the project. */ static getUniqueName(project, baseName, itemType) { if (!ProjectCreateManager.nameExistsInProject(project, baseName, itemType)) { return baseName; } for (let i = 2; i < 100; i++) { const candidate = baseName + " " + i; if (!ProjectCreateManager.nameExistsInProject(project, candidate, itemType)) { return candidate; } } return baseName + " " + Date.now(); } /** * Maps a ProjectItemType to the corresponding GalleryItemType for entity/block/item. */ static galleryItemTypeForProjectItemType(itemType) { switch (itemType) { case IProjectItemData_1.ProjectItemType.entityTypeBehavior: return IGalleryItem_1.GalleryItemType.entityType; case IProjectItemData_1.ProjectItemType.blockTypeBehavior: return IGalleryItem_1.GalleryItemType.blockType; case IProjectItemData_1.ProjectItemType.itemTypeBehavior: return IGalleryItem_1.GalleryItemType.itemType; default: return undefined; } } /** * Collects the file paths (project-relative) for all related files of a project item, * using the relationship graph built by project.processRelations(). * Returns paths relative to the project root (e.g., "behavior_packs/mypack_bp/entities/zombie.json"). */ static collectRelatedFilePaths(item) { const descendants = ProjectItemUtilities_1.default.collectAllDescendantItems(item); const paths = []; for (const descendant of descendants) { if (descendant.projectPath) { paths.push(descendant.projectPath); } } Log_1.default.debug(`[ProjectCreateManager] collectRelatedFilePaths for '${item.name}': ` + `${descendants.length} descendants, ${paths.length} paths` + (paths.length > 0 ? ": " + paths.join(", ") : "") + ` (childItems: ${item.childItems ? item.childItems.length : "undefined"})`); return paths; } /** * Extracts the short identifier from a project item's name. For entities/blocks/items, * this is the base name without any file extensions or suffixes like .behavior, .entity, .geo. * E.g., "biceson.behavior.json" → "biceson", "frost_moose.json" → "frost_moose". */ static getShortIdFromProjectItem(item) { if (item.projectPath) { let leaf = StorageUtilities_1.default.getLeafName(item.projectPath); // Strip ALL dot-suffixes to get the bare entity/block/item name. // Entity files have compound extensions like .behavior.json, .entity.json, .geo.json. // We want just the base name (e.g., "biceson" from "biceson.behavior.json"). const firstDot = leaf.indexOf("."); if (firstDot > 0) { leaf = leaf.substring(0, firstDot); } if (leaf) { return leaf.toLowerCase().replace(/[- ]/g, "_"); } } const name = item.name; // Also strip dot-suffixes from the name const firstDot = name.indexOf("."); if (firstDot > 0) { return name.substring(0, firstDot).toLowerCase().replace(/[- ]/g, "_"); } return name.toLowerCase().replace(/[- ]/g, "_"); } /** * Builds an IGalleryItem from an existing ProjectItem in the current project. * This allows project items to be displayed in the same gallery UI as vanilla items * and fed into the same copy pipeline. */ static buildGalleryItemFromProjectItem(item, project) { const galleryType = ProjectCreateManager.galleryItemTypeForProjectItemType(item.itemType); const shortId = ProjectCreateManager.getShortIdFromProjectItem(item); const friendlyName = Utilities_1.default.humanifyMinecraftName(shortId); // Prefer the rendered 3D model snapshot (cachedThumbnail) which is propagated // up from geometry items to entity RP and entity BP items by ProjectWorkerManager. let thumbnailDataUrl = item.cachedThumbnail; // Fall back to the item's own imageUrl (which also checks cachedThumbnail internally) if (!thumbnailDataUrl) { thumbnailDataUrl = item.imageUrl; } // Skip expensive descendant traversal at gallery-build time. // The file list is rebuilt fresh at copy time via _resolveProjectItemFilePaths. return { gitHubOwner: "", gitHubRepoName: "", thumbnailImage: "", localLogo: thumbnailDataUrl, title: friendlyName, description: "Project " + ProjectItemUtilities_1.default.getDescriptionForType(item.itemType).toLowerCase(), type: galleryType ?? IGalleryItem_1.GalleryItemType.entityType, id: shortId, nameReplacers: [shortId], fileList: [], isProjectItem: true, tags: ["project"], }; } /** * Returns an array of IGalleryItem adapters for all project items of a given gallery type. * Used to populate the "Your Project" section in the template picker. */ static getProjectItemsAsGalleryItems(project, galleryType) { let projectItemType; switch (galleryType) { case IGalleryItem_1.GalleryItemType.entityType: projectItemType = IProjectItemData_1.ProjectItemType.entityTypeBehavior; break; case IGalleryItem_1.GalleryItemType.blockType: projectItemType = IProjectItemData_1.ProjectItemType.blockTypeBehavior; break; case IGalleryItem_1.GalleryItemType.itemType: projectItemType = IProjectItemData_1.ProjectItemType.itemTypeBehavior; break; default: return []; } const items = project.getItemsByType(projectItemType); const galleryItems = []; for (const item of items) { // Skip vanilla/accessory items — only include items from the project's own packs. // Items loaded from accessory folders have source starting with "o." if (item.source && item.source.startsWith("o.")) { continue; } galleryItems.push(ProjectCreateManager.buildGalleryItemFromProjectItem(item, project)); } return galleryItems; } /** * Discovers newly created files by re-scanning only the project's BP and RP folders, * rather than the entire project tree. This prevents OOM on large projects (21K+ items). */ static async _inferNewItems(project) { const projectFolder = project.projectFolder; if (!projectFolder) { return; } const bpFolder = project.defaultBehaviorPackFolder; const rpFolder = project.defaultResourcePackFolder; if (bpFolder) { await ProjectItemInference_1.default.inferProjectItemsFromFolder(project, bpFolder, "", Project_1.FolderContext.behaviorPack, undefined, false, projectFolder, 0, undefined, true // force re-scan this folder ); } if (rpFolder) { await ProjectItemInference_1.default.inferProjectItemsFromFolder(project, rpFolder, "", Project_1.FolderContext.resourcePack, undefined, false, projectFolder, 0, undefined, true // force re-scan this folder ); } } static async addEntityTypeFromGallery(project, entityTypeProject, entityTypeName, addMode, messageUpdater, dontOverwriteExistingFiles) { await ProjectCreateManager.copyGalleryPackFilesAndFixupIds(project, entityTypeProject, entityTypeName, messageUpdater, dontOverwriteExistingFiles); // Discover the newly created files without re-scanning the entire project await ProjectCreateManager._inferNewItems(project); // Set runtimeIdentifier on the newly created entity if (!entityTypeProject.isProjectItem) { // For vanilla gallery items, scan for the new entity and set its runtimeIdentifier const items = project.getItemsCopy(); for (const item of items) { if (item.itemType === IProjectItemData_1.ProjectItemType.entityTypeBehavior) { let minecraftEntityType = (await MinecraftDefinitions_1.default.get(item)); if (minecraftEntityType) { const targetId = entityTypeName ? entityTypeName : entityTypeProject.id; if (minecraftEntityType.id?.endsWith(targetId)) { minecraftEntityType.runtimeIdentifier = entityTypeProject.targetRuntimeIdentifier ? entityTypeProject.targetRuntimeIdentifier : "minecraft:" + entityTypeProject.id; minecraftEntityType.persist(); } } } } } // For project items, runtimeIdentifier is already correct from the content copy. await project.save(); } static getReplacedCreationData(project, galleryItem, newName) { if (galleryItem.creationData === undefined) { return undefined; } try { let creationDataStr = JSON.stringify(galleryItem.creationData); creationDataStr = this.replaceNamesInContent(creationDataStr, project, galleryItem, newName, []); return JSON.parse(creationDataStr); } catch (e) { return galleryItem.creationData; } } static getReplacedCreationDataInObject(project, creationObject, newName) { if (creationObject === undefined) { return undefined; } try { let creationDataStr = JSON.stringify(creationObject); creationDataStr = this.replaceNamesInContentFromReplacers(creationDataStr, project, [exports.STANDARD_NAME_TOKEN], newName, []); return JSON.parse(creationDataStr); } catch (e) { return creationObject; } } static async addBlockTypeFromGallery(project, blockTypeProject, blockTypeName) { blockTypeName = await ProjectCreateManager.copyGalleryPackFilesAndFixupIds(project, blockTypeProject, blockTypeName); await ProjectCreateManager._inferNewItems(project); const blockTypeItem = ProjectItemUtilities_1.default.getItemByTypeAndName(project, blockTypeName, IProjectItemData_1.ProjectItemType.blockTypeBehavior); if (blockTypeItem) { if (!blockTypeItem.isContentLoaded) { await blockTypeItem.loadContent(); } if (blockTypeItem.primaryFile) { const blockType = await BlockTypeDefinition_1.default.ensureOnFile(blockTypeItem.primaryFile); const creationData = this.getReplacedCreationData(project, blockTypeProject, blockTypeName); if (blockType) { await blockType.ensureBlockAndTerrainLinks(project, creationData); } } } await project.save(); } static async addItemTypeFromGallery(project, itemTypeProject, itemTypeName) { await ProjectCreateManager.copyGalleryPackFilesAndFixupIds(project, itemTypeProject, itemTypeName); await ProjectCreateManager._inferNewItems(project); await project.save(); } /** * Adds a model design template to a project. * Creates both a .model.json file and the exported .geo.json + texture.png in the resource pack. * @param project The project to add the model design to * @param modelDesignItem The gallery item containing the model design template info * @param modelName The name for the new model */ static async addModelDesignFromGallery(project, modelDesignItem, modelName) { if (!modelName) { modelName = modelDesignItem.id; } // Get the model template from the gallery item // The template ID is stored in the gallery item's id field const templateId = modelDesignItem.id; const modelDesign = await Database_1.default.ensureModelTemplateLoaded(templateId); if (!modelDesign) { Log_1.default.fail(`Could not load model template: ${templateId}`); return; } // Clone the design and update the identifier with the new name const designCopy = JSON.parse(JSON.stringify(modelDesign)); designCopy.identifier = modelName; // Ensure we have a resource pack const rpFolder = await project.ensureDefaultResourcePackFolder(); if (!rpFolder) { Log_1.default.fail("Could not ensure resource pack folder for model design"); return; } // Create the model templates folder and save the model design JSON const modelTemplatesFolder = rpFolder.ensureFolder("model_templates"); await modelTemplatesFolder.ensureExists(); const modelJsonFile = modelTemplatesFolder.ensureFile(modelName + ".model.json"); modelJsonFile.setContent(JSON.stringify(designCopy, null, 2)); await modelJsonFile.saveContent(); // Convert to geometry const conversionResult = ModelDesignUtilities_1.default.convertToGeometry(designCopy); if (conversionResult.geometry) { // Create the models folder and save geometry const modelsFolder = rpFolder.ensureFolder("models"); await modelsFolder.ensureExists(); const entitiesFolder = modelsFolder.ensureFolder("entities"); await entitiesFolder.ensureExists(); const geoFile = entitiesFolder.ensureFile(modelName + ".geo.json"); geoFile.setContent(JSON.stringify(conversionResult.geometry, null, 2)); await geoFile.saveContent(); } // Note: Texture generation requires platform-specific code (ImageGenerationUtilities) // The model design JSON file in model_templates/ contains all the texture info // and can be used to regenerate textures using the MCP tools or export commands. await project.inferProjectItemsFromFiles(true); await project.save(); } static async copyGalleryPackFilesAndFixupIds(project, galleryProject, newTypeName, messagerUpdater, dontOverwriteExistingFiles) { const files = galleryProject.fileList; if (newTypeName === undefined) { newTypeName = galleryProject.id; } if (files === undefined) { Log_1.default.unexpectedUndefined("AETFLS"); return newTypeName; } let sourceBpFolder = undefined; let sourceRpFolder = undefined; // Project-local source: filePaths are project-relative, read from project's own folders. // Rebuild the file list fresh from the relationship graph since it may have been // stale when the gallery item was constructed (processRelations uses setTimeout batching // in the browser, so the fileList built at dialog-open time may be incomplete). if (galleryProject.isProjectItem) { // Find the source ProjectItem by matching the gallery item's id against project items const freshFilePaths = await ProjectCreateManager._resolveProjectItemFilePaths(project, galleryProject); return ProjectCreateManager._copyProjectItemFiles(project, galleryProject, freshFilePaths.length > 0 ? freshFilePaths : files, newTypeName, messagerUpdater, dontOverwriteExistingFiles); } else if (galleryProject.gitHubRepoName === "bedrock-samples") { sourceBpFolder = await Database_1.default.getReleaseVanillaBehaviorPackFolder(); sourceRpFolder = await Database_1.default.getReleaseVanillaResourcePackFolder(); } else { // Map GitHub repo names to local folder names. When samples are downloaded // during preparedevenv, the zip's root folder is renamed via the // "replaceFirstFolderWith" field in reslist/*.resources.json. The local // folder name doesn't match the "{repoName}-{branch}" pattern that GitHub // uses, so we need an explicit mapping here. const repoFolderMap = { "minecraft-samples": "samples", "minecraft-gametests": "gametests", "minecraft-scripting-samples": "script-samples", }; const repoFolder = repoFolderMap[galleryProject.gitHubRepoName] || galleryProject.gitHubRepoName + "-" + (galleryProject.gitHubBranch ? galleryProject.gitHubBranch : "main"); const relativePath = "res/samples/" + Utilities_1.default.ensureEndsWithSlash(galleryProject.gitHubOwner) + Utilities_1.default.ensureEndsWithSlash(repoFolder) + (galleryProject.gitHubFolder ? Utilities_1.default.ensureNotStartsWithSlash(galleryProject.gitHubFolder) : ""); let rootFolder; // Prefer local storage (CLI/Electron) over HTTP to avoid 404s on static hosts // that don't serve directory indexes. if (Database_1.default.local) { const storage = Database_1.default.local.createStorage(relativePath); if (storage) { const candidateFolder = storage.rootFolder; await candidateFolder.load(); // Only use local storage if it actually contains the expected content; // otherwise fall back to HTTP. if (candidateFolder.folders["behavior_packs"] && candidateFolder.folders["resource_packs"]) { rootFolder = candidateFolder; } } } if (!rootFolder) { const url = Utilities_1.default.ensureEndsWithSlash(CreatorToolsHost_1.default.getVanillaContentRoot()) + relativePath; const gh = HttpStorage_1.default.get(url); rootFolder = gh.rootFolder; } if (!rootFolder.isLoaded) { await rootFolder.load(); } const bps = rootFolder.folders["behavior_packs"]; const rps = rootFolder.folders["resource_packs"]; if (!bps || !rps) { Log_1.default.unexpectedUndefined("AETFLT"); return newTypeName; } if (!rps.isLoaded) { await rps.load(); } if (!bps.isLoaded) { await bps.load(); } if (rps.folderCount < 1 || bps.folderCount < 1) { Log_1.default.unexpectedUndefined("AETFLY"); return newTypeName; } sourceBpFolder = bps.getFolderByIndex(0); sourceRpFolder = rps.getFolderByIndex(0); } const targetBpFolder = await project.ensureDefaultBehaviorPackFolder(); const targetRpFolder = await project.ensureDefaultResourcePackFolder(); if (!sourceBpFolder || !sourceRpFolder || !sourceBpFolder || !sourceRpFolder || !targetBpFolder || !targetRpFolder) { Log_1.default.unexpectedUndefined("AETVA"); return newTypeName; } // note: '"identifier"', was in this list for entity types, but was removed. let contentReplacements = ['"materials"']; for (const filePath of files) { if (filePath.startsWith("/behavior_pack")) { let subPath = undefined; if (filePath.startsWith("/behavior_pack/")) { subPath = filePath.substring(14); } else { const nextSlash = filePath.indexOf("/", 16); if (nextSlash < 0) { Log_1.default.unexpectedUndefined("AETVB"); return newTypeName; } subPath = filePath.substring(nextSlash); } const targetPath = ProjectCreateManager.replaceNamesInPath(subPath, project, galleryProject, newTypeName); const sourceFile = await sourceBpFolder.getFileFromRelativePath(subPath); if (!sourceFile) { Log_1.default.debugAlert("Could not find file '" + subPath + "' (GPFFIA)"); } else { const targetFile = await targetBpFolder.ensureFileFromRelativePath(targetPath); let update = true; if (dontOverwriteExistingFiles) { const targetExists = await targetFile.exists(); if (targetExists) { update = false; } } if (update) { if (!sourceFile.isContentLoaded) { await sourceFile.loadContent(); } let content = sourceFile.content; if (typeof content === "string") { content = ProjectCreateManager.replaceNamesInContent(content, project, galleryProject, newTypeName, contentReplacements); } if (content !== null) { if (messagerUpdater) { messagerUpdater("Updating '" + targetFile.fullPath + "'"); } targetFile.setContent(content); } } } } else if (filePath.startsWith("/resource_pack")) { let subPath = undefined; if (filePath.startsWith("/resource_pack/")) { subPath = filePath.substring(14); } else { const nextSlash = filePath.indexOf("/", 16); if (nextSlash < 0) { Log_1.default.unexpectedUndefined("AETVC"); return newTypeName; } subPath = filePath.substring(nextSlash); } const targetPath = ProjectCreateManager.replaceNamesInPath(subPath, project, galleryProject, newTypeName); if (subPath.indexOf("cow.v2.render_controllers") >= 0) { subPath = subPath.replace("cow.v2.render_controllers", "cow.v2.render_controllres"); // misspelling in source } const sourceFile = await sourceRpFolder.getFileFromRelativePath(subPath); if (!sourceFile) { Log_1.default.debugAlert("Could not find file '" + subPath + "' (GPFFIB)"); } else { const targetFile = await targetRpFolder.ensureFileFromRelativePath(targetPath); let update = true; if (update) { if (dontOverwriteExistingFiles) { const targetExists = await targetFile.exists(); if (targetExists) { update = false; } } if (!sourceFile.isContentLoaded) { await sourceFile.loadContent(); } let content = sourceFile.content; if (typeof content === "string") { content = ProjectCreateManager.replaceNamesInContent(content, project, galleryProject, newTypeName, contentReplacements); } if (content !== null) { if (messagerUpdater) { messagerUpdater("Updating '" + targetFile.fullPath + "'"); } targetFile.setContent(content); } } } } } return newTypeName; } /** * Resolves the complete file list for a project item at copy time by finding the * source ProjectItem, ensuring its content and relations are loaded, and collecting * all descendant file paths. This avoids stale file lists from dialog-open time. */ static async _resolveProjectItemFilePaths(project, galleryProject) { const sourceId = galleryProject.id; // Map gallery type to project item type let itemType; switch (galleryProject.type) { case IGalleryItem_1.GalleryItemType.entityType: itemType = IProjectItemData_1.ProjectItemType.entityTypeBehavior; break; case IGalleryItem_1.GalleryItemType.blockType: itemType = IProjectItemData_1.ProjectItemType.blockTypeBehavior; break; case IGalleryItem_1.GalleryItemType.itemType: itemType = IProjectItemData_1.ProjectItemType.itemTypeBehavior; break; default: return []; } // Find the source item by matching the short ID against project items const candidates = project.getItemsByType(itemType); let sourceItem; for (const item of candidates) { const shortId = ProjectCreateManager.getShortIdFromProjectItem(item); if (shortId === sourceId) { sourceItem = item; break; } } if (!sourceItem) { Log_1.default.debug(`[ProjectCreateManager] Could not find source project item for '${sourceId}'`); return []; } // Check if relations are already built (from worker pipeline or previous processRelations call). // If so, just traverse the existing relationship graph — no expensive rebuild needed. if (sourceItem.childItems && sourceItem.childItems.length > 0) { const filePaths = ProjectCreateManager.collectRelatedFilePaths(sourceItem); Log_1.default.debug(`[ProjectCreateManager] Resolved ${filePaths.length} files using existing relations for '${sourceId}'`); return filePaths; } // Do a targeted relations calculation for just the source item and its children. // This is fast even for large projects since we only process a handful of items, // rather than waiting for the full project processRelations (which can take minutes for 21K+ items). Log_1.default.debug(`[ProjectCreateManager] Doing targeted relations calculation for '${sourceId}'`); // Ensure content is loaded for the source item if (!sourceItem.isContentLoaded) { await sourceItem.loadContent(); } const { default: ProjectItemRelations } = await Promise.resolve().then(() => __importStar(require("./ProjectItemRelations"))); await ProjectItemRelations.calculateForItem(sourceItem); // The entity BP's addChildItems finds entity RP items. Now process those children too. if (sourceItem.childItems) { for (const rel of sourceItem.childItems) { if (rel.childItem) { await ProjectItemRelations.calculateForItem(rel.childItem); } } } // Now collect related file paths const filePaths = ProjectCreateManager.collectRelatedFilePaths(sourceItem); Log_1.default.debug(`[ProjectCreateManager] Resolved ${filePaths.length} files via targeted calculation for '${sourceId}': ${filePaths.join(", ")}`); return filePaths; } /** * Copies files from the current project (project-item source) to new locations in the same project * with renamed IDs. Used when creating a new entity/block/item based on an existing one in the project. * * filePaths are project-relative (e.g., "behavior_packs/mypack_bp/entities/zombie.json"). * For each file, we determine whether it's in a BP or RP folder, extract the sub-path within * that pack, apply name replacements, and write to the project's default BP or RP folder. */ static async _copyProjectItemFiles(project, galleryProject, filePaths, newTypeName, messagerUpdater, dontOverwriteExistingFiles) { const projectFolder = project.projectFolder; Log_1.default.debug(`[ProjectCreateManager] _copyProjectItemFiles: copying ${filePaths.length} files for '${galleryProject.id}' → '${newTypeName}'`); if (!projectFolder) { Log_1.default.unexpectedUndefined("CPIF_PF"); return newTypeName; } const defaultBpFolder = await project.ensureDefaultBehaviorPackFolder(); const defaultRpFolder = await project.ensureDefaultResourcePackFolder(); if (!defaultBpFolder || !defaultRpFolder) { Log_1.default.unexpectedUndefined("CPIF_TR"); return newTypeName; } const contentReplacements = ['"materials"']; // Detect source pack folders from the file paths so we write to the same pack. // If we can find the source BP/RP pack folder, use it as target; otherwise fall back to defaults. let targetBpFolder = defaultBpFolder; let targetRpFolder = defaultRpFolder; for (const fp of filePaths) { const norm = fp.replace(/\\/g, "/"); const bpFolderMatch = norm.match(/^\/?(behavior_packs?)\/([^/]+)\//i); const rpFolderMatch = norm.match(/^\/?(resource_packs?)\/([^/]+)\//i); // Also detect base-packs layout: /base-packs/vanilla/behavior/... const altBpFolderMatch = !bpFolderMatch ? norm.match(/^\/?((?:base-packs|experimental-packs)\/[^/]+)\/behavior\//i) : null; const altRpFolderMatch = !rpFolderMatch ? norm.match(/^\/?((?:base-packs|experimental-packs)\/[^/]+)\/resource\//i) : null; if (bpFolderMatch && targetBpFolder === defaultBpFolder) { const packName = bpFolderMatch[2]; const bpContainer = projectFolder.folders["behavior_packs"] || projectFolder.folders["behavior_pack"]; if (bpContainer) { const packFolder = bpContainer.folders[packName]; if (packFolder) { targetBpFolder = packFolder; Log_1.default.debug(`[ProjectCreateManager] Using source BP pack folder: ${packName}`); } } } else if (altBpFolderMatch && targetBpFolder === defaultBpFolder) { // For base-packs layout, resolve the behavior subfolder as the BP target const basePath = altBpFolderMatch[1]; // e.g., "base-packs/vanilla" const behaviorFolder = await projectFolder.getFolderFromRelativePath("/" + basePath + "/behavior"); if (behaviorFolder) { targetBpFolder = behaviorFolder; Log_1.default.debug(`[ProjectCreateManager] Using base-packs BP folder: ${basePath}/behavior`); } } if (rpFolderMatch && targetRpFolder === defaultRpFolder) { const packName = rpFolderMatch[2]; const rpContainer = projectFolder.folders["resource_packs"] || projectFolder.folders["resource_pack"]; if (rpContainer) { const packFolder = rpContainer.folders[packName]; if (packFolder) { targetRpFolder = packFolder; Log_1.default.debug(`[ProjectCreateManager] Using source RP pack folder: ${packName}`); } } } else if (altRpFolderMatch && targetRpFolder === defaultRpFolder) { const basePath = altRpFolderMatch[1]; const resourceFolder = await projectFolder.getFolderFromRelativePath("/" + basePath + "/resource"); if (resourceFolder) { targetRpFolder = resourceFolder; Log_1.default.debug(`[ProjectCreateManager] Using base-packs RP folder: ${basePath}/resource`); } } } // We need to classify each file path as BP or RP and extract its sub-path within the pack. // Project-relative paths look like "behavior_packs/mypack_bp/entities/zombie.json" // or "resource_packs/mypack_rp/entity/zombie.entity.json". for (const filePath of filePaths) { let normalizedPath = filePath.replace(/\\/g, "/"); // Ensure the path starts with / for getFileFromRelativePath if (!normalizedPath.startsWith("/")) { normalizedPath = "/" + normalizedPath; } let isBp = false; let isRp = false; let subPath; // Determine if this is a BP or RP file and extract the sub-path within the pack. // Handles multiple path formats: // /behavior_packs/mypack_bp/entities/agent.json (standard project layout) // /base-packs/vanilla/behavior/entities/agent.json (vanilla/sample project layout) // /resource_packs/mypack_rp/entity/agent.entity.json // /base-packs/vanilla/resource/entity/agent.entity.json const bpMatch = normalizedPath.match(/^\/?(behavior_packs?)\/[^/]+\/(.*)/i); const rpMatch = normalizedPath.match(/^\/?(resource_packs?)\/[^/]+\/(.*)/i); // Also match paths like /base-packs/vanilla/behavior/... or /experimental-packs/.../behavior/... const altBpMatch = !bpMatch ? normalizedPath.match(/\/behavior\/(.*)/i) : null; const altRpMatch = !rpMatch ? normalizedPath.match(/\/resource\/(.*)/i) : null; if (bpMatch) { isBp = true; subPath = "/" + bpMatch[2]; } else if (rpMatch) { isRp = true; subPath = "/" + rpMatch[2]; } else if (altBpMatch) { isBp = true; subPath = "/" + altBpMatch[1]; } else if (altRpMatch) { isRp = true; subPath = "/" + altRpMatch[1]; } if (!subPath || (!isBp && !isRp)) { Log_1.default.debug("Skipping non-pack file in project item copy: " + filePath); continue; } const targetPath = ProjectCreateManager.replaceNamesInPath(subPath, project, galleryProject, newTypeName); const targetFolder = isBp ? targetBpFolder : targetRpFolder; const sourceFile = await projectFolder.getFileFromRelativePath(normalizedPath); if (!sourceFile) { Log_1.default.debugAlert("Could not find project file '" + normalizedPath + "' (CPIF_SF)"); continue; } Log_1.default.debug(`[ProjectCreateManager] Copying: ${normalizedPath}${targetPath} (${isBp ? "BP" : "RP"})`); const targetFile = await targetFolder.ensureFileFromRelativePath(targetPath); let update = true; if (dontOverwriteExistingFiles) { const targetExists = await targetFile.exists(); if (targetExists) { update = false; } } if (update) { if (!sourceFile.isContentLoaded) { await sourceFile.loadContent(); } let content = sourceFile.content; if (typeof content === "string") { content = ProjectCreateManager.replaceNamesInContent(content, project, galleryProject, newTypeName, contentReplacements); } if (content !== null) { if (messagerUpdater) { messagerUpdater("Updating '" + targetFile.fullPath + "'"); } targetFile.setContent(content); } } } return newTypeName; } static replaceNamesInPath(path, project, galleryProject, newName) { let pathReplacers = galleryProject.nameReplacers; if (!pathReplacers) { pathReplacers = [galleryProject.id]; } newName = newName.toLowerCase(); newName = newName.replace(/[- ]/g, "_"); const tempName = Utilities_1.default.createRandomId(10); for (const pathReplacer of pathReplacers) { path = Utilities_1.default.replaceAll(path, "/" + pathReplacer + ".", "/" + tempName + "."); path = Utilities_1.default.replaceAll(path, "\\" + pathReplacer + ".", "\\" + tempName + "."); path = Utilities_1.default.replaceAll(path, "/" + pathReplacer + "/", "/" + tempName + "/"); path = Utilities_1.default.replaceAll(path, "\\" + pathReplacer + "\\", "\\" + tempName + "\\"); path = Utilities_1.default.replaceAll(path, "\\" + pathReplacer + "_", "\\" + tempName + "_"); path = Utilities_1.default.replaceAll(path, "/" + pathReplacer + "_", "/" + tempName + "_"); path = Utilities_1.default.replaceAll(path, "/" + pathReplacer + "_ico.", "/" + tempName + "_ico."); path = Utilities_1.default.replaceAll(path, "\\" + pathReplacer + "_ico.", "\\" + tempName + "_ico."); path = Utilities_1.default.replaceAll(path, "/" + pathReplacer + "_ico/", "/" + tempName + "_ico/"); path = Utilities_1.default.replaceAll(path, "\\" + pathReplacer + "_ico\\", "\\" + tempName + "_ico\\"); } path = Utilities_1.default.replaceAll(path, tempName, newName); return path; } static replaceNamesInContent(content, project, galleryProject, newName, replaceAllExclusions) { let replacers = galleryProject.nameReplacers; if (!replacers) { replacers = [galleryProject.id]; } // copy & extend replacers = replacers.slice(); replacers.push(exports.STANDARD_NAME_TOKEN); return this.replaceNamesInContentFromReplacers(content, project, replacers, newName, replaceAllExclusions); } static replaceNamesInContentFromReplacers(content, project, replacers, newName, replaceAllExclusions) { newName = newName.toLowerCase(); newName = newName.replace(/-/g, "_"); newName = newName.replace(/ /g, "_"); const tempName = Utilities_1.default.createRandomLowerId(10); for (const replacer of replacers) { content = Utilities_1.default.replaceAll(content, "minecraft:" + replacer, project.effectiveDefaultNamespace + ":" + tempName); content = Utilities_1.default.replaceAll(content, "demo:" + replacer, project.effectiveDefaultNamespace + ":" + tempName); content = Utilities_1.default.replaceAll(content, "starter:" + replacer, project.effectiveDefaultNamespace + ":" + tempName); content = Utilities_1.default.replaceAll(content, "sample:" + replacer, project.effectiveDefaultNamespace + ":" + tempName); content = Utilities_1.default.replaceAllExceptInLines(content, ":" + replacer, ":" + tempName, replaceAllExclusions); content = Utilities_1.default.replaceAllExceptInLines(content, "/" + replacer, "/" + tempName, replaceAllExclusions); content = Utilities_1.default.replaceAllExceptInLines(content, "." + replacer, "." + tempName, replaceAllExclusions); content = Utilities_1.default.replaceAllExceptInLines(content, replacer + "_", tempName + "_", replaceAllExclusions); content = Utilities_1.default.replaceAllExceptInLines(content, '"' + replacer + '"', '"' + tempName + '"', replaceAllExclusions); for (const materialName of exports.MATERIAL_NAMES_TO_FIXUP) { content = Utilities_1.default.replaceAll(content, materialName + '": "' + tempName, materialName + '": "' + replacer); } } content = Utilities_1.default.replaceAll(content, tempName, newName); return content; } } exports.default = ProjectCreateManager;