@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
866 lines (865 loc) • 45.1 kB
JavaScript
;
// 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;