UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

1,106 lines 63.6 kB
#!/usr/bin/env node "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const commander_1 = require("commander"); const CartoApp_js_1 = require("./../app/CartoApp.js"); const process = require("process"); const NodeStorage_js_1 = require("../local/NodeStorage.js"); const StorageUtilities_js_1 = require("../storage/StorageUtilities.js"); const Constants_js_1 = require("../core/Constants.js"); const ProjectInfoSet_js_1 = require("../info/ProjectInfoSet.js"); const IInfoItemData_js_1 = require("../info/IInfoItemData.js"); const LocalEnvironment_js_1 = require("../local/LocalEnvironment.js"); const IProjectItemData_js_1 = require("../app/IProjectItemData.js"); const Log_js_1 = require("../core/Log.js"); const ClUtils_js_1 = require("./ClUtils.js"); const threads_1 = require("threads"); const ContentIndex_js_1 = require("../core/ContentIndex.js"); const inquirer = require("inquirer"); const IGalleryItem_js_1 = require("../app/IGalleryItem.js"); const ProjectUtilities_js_1 = require("../app/ProjectUtilities.js"); const Project_js_1 = require("../app/Project.js"); const ProjectExporter_js_1 = require("../app/ProjectExporter.js"); const Utilities_js_1 = require("../core/Utilities.js"); const IProjectInfoData_js_1 = require("../info/IProjectInfoData.js"); const MinecraftUtilities_js_1 = require("../minecraft/MinecraftUtilities.js"); const ServerManager_js_1 = require("../local/ServerManager.js"); if (typeof btoa === "undefined") { // @ts-ignore global.btoa = function (str) { return Buffer.from(str, "binary").toString("base64"); }; } if (typeof atob === "undefined") { // @ts-ignore global.atob = function (b64Encoded) { return Buffer.from(b64Encoded, "base64").toString("binary"); }; } CartoApp_js_1.default.hostType = CartoApp_js_1.HostType.toolsNodejs; const MAX_LINES_PER_CSV_FILE = 500000; const ERROR_INIT_ERROR = 44; const ERROR_VALIDATION_INTERNALPROCESSINGERROR = 53; const ERROR_VALIDATION_TESTFAIL = 56; const ERROR_VALIDATION_ERROR = 57; const program = new commander_1.Command(); let carto; const projectStarts = []; let localEnv; let mode; let type; let newName; let newDescription; let template; let creator; let serverHostPort; let suite; let exclusionList; let outputType; let serverFeatures; let serverTitle; let serverDomainName; let serverMessageOfTheDay; let serverCandidateAdminPasscode; let serverCandidateDisplayReadOnlyPasscode; let serverCandidateFullReadOnlyPasscode; let serverCandidateUpdateStatePasscode; let serverRunOnce; let aggregateReportsAfterValidation = false; let threads = 8; let errorLevel; let force = false; let executionTaskType = ClUtils_js_1.TaskType.noCommand; program .name("mct") .description("Minecraft Creator Tools v" + Constants_js_1.constants.version) .option("-i, --input-folder [path to folder]", "Path to the input folder. If not specified, the current working directory is used.") .option("-if, --input-file [path to file]", "Path to the input MCWorld, MCTemplate, MCPack, MCAddon or other zip file.") .option("-o, --output-folder <path to folder>", "Path to the output project folder. If not specified, the current working directory + 'out' is used.", "out") .option("-afs, --additional-files [path to file]", "Comma-separated list of additional files to add to projects.") .option("-of, --output-file [path to file]", "Path to the export file, if applicable for the command you are using.") .option("-ot, --output-type [output type]", "Type of output, if applicable for the command you are using.") .option("-updatepc, --update-passcode [update passcode]", "Sets update passcode.") .option("-bp, --behavior-pack <behavior pack uuid>", "Adds a set of behavior pack UUIDs as references for any worlds that are updated.") .option("-rp, --resource-pack <resource pack uuid>", "Adds a set of resources pack UUIDs as references for any worlds that are updated.") .option("-betaapis, --beta-apis", "Ensures that the Beta APIs experiment is set for any worlds that are updated.") .option("-no-betaapis, --no-beta-apis", "Removes the Beta APIs experiment if set.") .option("-f, --force", "Force any updates.") .option("-single, --single", "When pointed at at a folder via -i, force that folder to be processed as a single project.") .option("-editor", "Ensures that the world is an Editor world.") .option("-once", "When running as a server, only process one request and then shutdown.", false) .option("-no-editor", "Removes that the world is an editor.") .option("--threads [thread count]", "Targeted number of threads to use.") .option("-show, --display-only", "Whether to only show messages, vs. output report files.") .option("-lv, --log-verbose", "Whether to show verbose log messages."); program.addHelpText("before", "\x1b[32m┌─────┐\x1b[0m"); program.addHelpText("before", "\x1b[32m│ ▄ ▄ │\x1b[0m Minecraft Creator Tools (preview) command line"); program.addHelpText("before", "\x1b[32m│ ┏▀┓ │\x1b[0m See " + Constants_js_1.constants.homeUrl + " for more info."); program.addHelpText("before", "\x1b[32m└─────┘\x1b[0m"); program.addHelpText("before", " "); program .command("world") .alias("w") .description("Displays/sets world settings within a folder.") .addArgument(new commander_1.Argument("[mode]", "If set to 'set', will update the world with a set of properties that are specified.")) .action((modeIn) => { mode = modeIn; executionTaskType = ClUtils_js_1.TaskType.world; }); program .command("create") .alias("c") .description("Creates a new Minecraft project") .addArgument(new commander_1.Argument("[name]", "Desired project name")) .addArgument(new commander_1.Argument("[template]", "Template name")) .addArgument(new commander_1.Argument("[creator]", "Creator name")) .addArgument(new commander_1.Argument("[description]", "Project description")) .action((nameIn, templateIn, creatorIn, descriptionIn) => { newName = nameIn; template = templateIn; creator = creatorIn; newDescription = descriptionIn; executionTaskType = ClUtils_js_1.TaskType.create; }); program .command("minecrafteulaandprivacypolicy") .alias("eula") .description("See the Minecraft End User License Agreement.") .action(() => { executionTaskType = ClUtils_js_1.TaskType.minecraftEulaAndPrivacyPolicy; }); program .command("serve") .alias("srv") .description("Hosts a web service and site that can manage a dedicated server and/or validation.") .addArgument(new commander_1.Argument("[features]", "Specifies a title the Minecraft Http Server on.").choices([ "all", "allwebservices", "basicwebservices", "dedicatedserver", ])) .addArgument(new commander_1.Argument("[domain]", "Specifies which URL domain this server is hosted on.")) .addArgument(new commander_1.Argument("[host-port]", "Specifies which core port to host the Minecraft Http Server on.")) .addArgument(new commander_1.Argument("[title]", "Specifies a title the Minecraft Http Server on.")) .addArgument(new commander_1.Argument("[motd]", "Specifies a message of the day for the server.")) .action(async (features, domain, hostPort, title, motd) => { if (features) { switch (features.toLowerCase()) { case "all": serverFeatures = ServerManager_js_1.ServerManagerFeatures.all; break; case "allwebservices": serverFeatures = ServerManager_js_1.ServerManagerFeatures.allWebServices; break; case "basicwebservices": serverFeatures = ServerManager_js_1.ServerManagerFeatures.basicWebServices; break; } } serverHostPort = hostPort; serverTitle = title; serverDomainName = domain; serverMessageOfTheDay = motd; executionTaskType = ClUtils_js_1.TaskType.serve; }); program .command("setserverprops") .alias("setsrv") .description("Updates default properties for the Minecraft Http Server.") .addArgument(new commander_1.Argument("[domain]", "Specifies which URL domain this server is hosted on.")) .addArgument(new commander_1.Argument("[host-port]", "Specifies which core port to host the Minecraft Http Server on.")) .addArgument(new commander_1.Argument("[title]", "Specifies a title the Minecraft Http Server on.")) .addArgument(new commander_1.Argument("[motd]", "Specifies a message of the day for the server.")) .action(async (domain, hostPort, title, motd) => { serverHostPort = hostPort; serverTitle = title; serverDomainName = domain; serverMessageOfTheDay = motd; executionTaskType = ClUtils_js_1.TaskType.setServerProperties; }); program .command("info") .alias("i") .description("Displays information about the current project.") .action(() => { executionTaskType = ClUtils_js_1.TaskType.info; }); program .command("validate") .alias("val") .description("Validate the current project.") .addArgument(new commander_1.Argument("[suite]", "Specifies the type of validation suite to run.") .choices(["all", "default", "addon", "currentplatform", "main"]) .default("main", "main (same as default) - runs most available validation tests.")) .addArgument(new commander_1.Argument("[exclusions]", "Specifies a comma-separated list of tests to exclude, e.g., PATHLENGTH,PACKSIZE")) .addArgument(new commander_1.Argument("[aggregateReports]", "Whether to aggregate reports across projects at the end of the run.") .choices(["true", "false"]) .default("false", "false - does not aggregate reports at the end of processing.")) .action((suiteIn, exclusionListIn, aggregateReportsIn) => { suite = suiteIn; exclusionList = exclusionListIn; executionTaskType = ClUtils_js_1.TaskType.validate; if (aggregateReportsIn === "aggregate" || aggregateReportsIn === "true" || aggregateReportsIn === "1" || aggregateReportsIn === "t") { aggregateReportsAfterValidation = true; } else { aggregateReportsAfterValidation = false; } }); program .command("aggregatereports") .alias("aggr") .description("Aggregates exported metadata about projects.") .action(() => { executionTaskType = ClUtils_js_1.TaskType.aggregateReports; }); program .command("version") .alias("ver") .description("Shows version and general application information") .action(() => { executionTaskType = ClUtils_js_1.TaskType.version; }); program.parse(process.argv); const options = program.opts(); localEnv = new LocalEnvironment_js_1.default(true); let sm; if (options.force) { force = true; } if (options.threads) { try { let tc = parseInt(options.threads); if (tc > 0) { threads = tc; console.log("Using " + threads + " threads."); } } catch (e) { Log_js_1.default.error("Could not process the threads parameter: " + e); } } if (options.outputType && typeof options.outputType === "string") { switch (options.outputType.toLowerCase().trim()) { case "noreports": outputType = ClUtils_js_1.OutputType.noReports; } } if (options.displayOnly) { localEnv.displayInfo = true; if (options.outputFolder === "out") { options.outputFolder = undefined; } } else if (options.outputFolder === "out") { if (executionTaskType !== ClUtils_js_1.TaskType.serve) { Log_js_1.default.message("Outputting full results to the `out` folder."); } localEnv.displayInfo = true; } if (options.adminPasscode) { serverCandidateAdminPasscode = options.adminPasscode; } if (options.updatePasscode) { serverCandidateUpdateStatePasscode = options.updatePasscode; } if (options.displayPasscode) { serverCandidateDisplayReadOnlyPasscode = options.displayPasscode; } if (options.once || options.Once) { serverRunOnce = true; } if (options.fullReadOnlyPasscode) { serverCandidateFullReadOnlyPasscode = options.fullReadOnlyPasscode; } if (options.logVerbose) { localEnv.displayVerbose = true; } (async () => { carto = ClUtils_js_1.default.getCarto(localEnv); if (!carto) { return; } sm = new ServerManager_js_1.default(localEnv, carto); sm.runOnce = serverRunOnce; await carto.load(); carto.onStatusAdded.subscribe(ClUtils_js_1.default.handleStatusAdded); await loadProjects(); if (serverCandidateDisplayReadOnlyPasscode || serverCandidateUpdateStatePasscode || serverCandidateAdminPasscode || serverCandidateFullReadOnlyPasscode) { await setPasscode(serverCandidateDisplayReadOnlyPasscode, serverCandidateFullReadOnlyPasscode, serverCandidateUpdateStatePasscode, serverCandidateAdminPasscode); } switch (executionTaskType) { case ClUtils_js_1.TaskType.info: await displayInfo(); break; case ClUtils_js_1.TaskType.version: await displayVersion(); break; case ClUtils_js_1.TaskType.validate: await validate(); break; case ClUtils_js_1.TaskType.aggregateReports: await aggregateReports(); break; case ClUtils_js_1.TaskType.serve: hookInput(); await applyServerProps(); await serve(); break; case ClUtils_js_1.TaskType.create: try { for (const projectStart of projectStarts) { if (projectStart) { await create(ClUtils_js_1.default.createProject(carto, projectStart), projectStarts.length <= 1); } } } catch (e) { errorLevel = ERROR_INIT_ERROR; console.error("Error creating a project. " + e.toString()); } break; case ClUtils_js_1.TaskType.minecraftEulaAndPrivacyPolicy: await minecraftEulaAndPrivacyPolicy(); break; case ClUtils_js_1.TaskType.world: await setAndDisplayAllWorlds(); break; } })(); async function loadProjects() { if (!carto || !carto.ensureLocalFolder) { throw new Error("Not properly configured."); } const additionalFiles = []; if (options.inputFile && options.inputFolder) { throw new Error("Cannot specify both an input file and an input folder."); } if (options.additionalFiles && typeof options.additionalFiles === "string") { const paths = options.additionalFiles.split(","); for (const path of paths) { additionalFiles.push(path); } } if (options.inputFile) { const inputFolderPath = StorageUtilities_js_1.default.getFolderPath(options.inputFile); const inputFileName = StorageUtilities_js_1.default.getLeafName(options.inputFile); if (!inputFileName || inputFileName.length < 2 || !inputFolderPath || inputFolderPath.length < 2) { throw new Error("Could not process file with path: `" + options.inputFile + "`"); } const containingFolder = carto.ensureLocalFolder(inputFolderPath); const file = containingFolder.ensureFile(inputFileName); const fileExists = await file.exists(); if (!fileExists) { throw new Error("Could not find file with path: `" + options.inputFile + "`."); } const fileName = StorageUtilities_js_1.default.getLeafName(options.inputFile); const mainProject = { ctorProjectName: fileName, localFilePath: options.inputFile, accessoryFiles: additionalFiles.slice(), }; projectStarts.push(mainProject); return; } const workFolder = await ClUtils_js_1.default.getMainWorkFolder(executionTaskType, options.inputFolder, options.outputFolder); const name = StorageUtilities_js_1.default.getLeafName(workFolder.fullPath); let isMultiLevelMultiProject = true; let foundASubProject = false; // multilevel multi project is a one-level folder hierarchy of subfolders with zip and // metadata files in them. // root folder must be empty // foo\coolgame.zip <--- this is the project // foo\coolgame.data.json // bar\swellgame.zip <--- this is another project // bar\swellgame.data.json let storageItemCount = 0; if (workFolder.fileCount > 0) { for (const subFileName in workFolder.files) { const file = workFolder.files[subFileName]; if (file && !StorageUtilities_js_1.default.isFileStorageItem(file)) { isMultiLevelMultiProject = false; break; } } } if (!options.single) { if (isMultiLevelMultiProject) { for (const subFolderName in workFolder.folders) { const subFolder = workFolder.folders[subFolderName]; if (subFolder) { await subFolder.load(); for (const subFileName in subFolder.files) { const subFile = subFolder.files[subFileName]; if (subFile) { if (StorageUtilities_js_1.default.isFileStorageItem(subFile)) { storageItemCount++; } let typeFromName = StorageUtilities_js_1.default.getTypeFromName(subFileName); if (!StorageUtilities_js_1.default.isFileStorageItem(subFile) && typeFromName !== "json" && typeFromName !== "csv" && typeFromName !== "" && typeFromName !== "html") { isMultiLevelMultiProject = false; continue; } } } } } } if (storageItemCount < 2) { isMultiLevelMultiProject = false; } if (isMultiLevelMultiProject) { console.log("Working across subfolders with projects at '" + workFolder.fullPath + "'"); for (const subFolderName in workFolder.folders) { const subFolder = workFolder.folders[subFolderName]; if (subFolder && !subFolder.errorStatus) { await subFolder.load(); for (const fileName in subFolder.files) { const file = subFolder.files[fileName]; if (file && StorageUtilities_js_1.default.isFileStorageItem(file)) { const ps = { ctorProjectName: file.name, accessoryFiles: additionalFiles.slice() }; let baseName = StorageUtilities_js_1.default.getBaseFromName(file.name); if (subFolder.files[baseName + ".data.json"]) { ps.accessoryFiles?.push(baseName + ".data.json"); } let lastDash = baseName.lastIndexOf("-"); if (lastDash > 0) { baseName = baseName.substring(0, lastDash); if (subFolder.files[baseName + ".data.json"]) { ps.accessoryFiles?.push(baseName + ".data.json"); } } ps.localFilePath = file.fullPath; projectStarts.push(ps); } } } } if (projectStarts.length > 0) { return; } } else { // "children of folder multi project" scans for either zip files in the root folder and/or // every subfolder is a separate project let isChildrenOfFolderMultiProject = true; for (const fileName in workFolder.files) { const file = workFolder.files[fileName]; if (file) { if (!StorageUtilities_js_1.default.isFileStorageItem(file)) { isChildrenOfFolderMultiProject = false; continue; } else { foundASubProject = true; } } } for (const folderName in workFolder.folders) { if (MinecraftUtilities_js_1.default.pathLooksLikePackName(folderName) || MinecraftUtilities_js_1.default.pathLooksLikePackContainerName(folderName)) { isChildrenOfFolderMultiProject = false; continue; } } if (isChildrenOfFolderMultiProject) { for (const folderName in workFolder.folders) { const folder = workFolder.folders[folderName]; if (folder && !folder.errorStatus) { await folder.load(true); if (folder.files["manifest.json"] || folder.files["pack_manifest.json"] || folder.folders["content"] || folder.folders["Content"] || folder.folders["world_template"] || folder.folders["behavior_packs"]) { foundASubProject = true; } if (StorageUtilities_js_1.default.isMinecraftInternalFolder(folder)) { isChildrenOfFolderMultiProject = false; continue; } } } } if (!foundASubProject) { isChildrenOfFolderMultiProject = false; } if (isChildrenOfFolderMultiProject) { console.log("Working across subfolders/packages at '" + workFolder.fullPath + "'"); for (const fileName in workFolder.files) { const file = workFolder.files[fileName]; if (file && StorageUtilities_js_1.default.isFileStorageItem(file)) { const mainProject = { ctorProjectName: file.name, accessoryFiles: additionalFiles.slice(), }; mainProject.localFilePath = file.fullPath; projectStarts.push(mainProject); } } for (const folderName in workFolder.folders) { const folder = workFolder.folders[folderName]; if (folder && !folder.errorStatus && folder.name !== "out") { await folder.load(); if (folder.folderCount > 0) { const mainProject = { ctorProjectName: folder.name, accessoryFiles: additionalFiles.slice(), }; mainProject.localFolderPath = folder.fullPath; projectStarts.push(mainProject); } } } if (projectStarts.length > 0) { return; } } } } // OK, just assume this folder is a big single project then. const mainProject = { ctorProjectName: name, accessoryFiles: additionalFiles.slice() }; mainProject.localFolderPath = workFolder.fullPath; projectStarts.push(mainProject); } async function applyServerProps() { if (localEnv) { await localEnv.load(); if (serverHostPort) { localEnv.serverHostPort = serverHostPort; } if (serverTitle) { localEnv.serverTitle = serverTitle; } if (serverDomainName) { localEnv.serverDomainName = serverDomainName; } if (serverMessageOfTheDay) { localEnv.serverMessageOfTheDay = serverMessageOfTheDay; } await localEnv.save(); } } async function doExit() { if (sm) { sm.stopWebServer(); } if (!errorLevel) { process.exitCode = errorLevel; } } function hookInput() { process.stdin.setEncoding("utf-8"); process.stdin.on("data", async function (data) { if (data.startsWith("exit") || data.startsWith("stop")) { await doExit(); } }); } async function displayVersion() { console.log("\n" + Constants_js_1.constants.name + " Tools"); console.log("Version: " + Constants_js_1.constants.version); if (carto && carto.local) { const local = carto.local; console.log("\n"); console.log("Machine user data path: " + local.userDataPath); console.log("Machine app data path: " + local.localAppDataPath); console.log("Minecraft path: " + local.minecraftPath); console.log("Server working path: " + local.serversPath); console.log("Environment prefs path: " + local.envPrefsPath); } console.log("\n"); console.log(Constants_js_1.constants.copyright); console.log(Constants_js_1.constants.disclaimer); console.log("\n"); } async function setPasscode(displayReadOnlyPasscode, fullReadOnlyPasscode, updateStatePasscode, adminPasscode) { if (localEnv === undefined) { throw new Error(); } await localEnv.load(); if (localEnv.displayReadOnlyPasscode === undefined || localEnv.adminPasscode === undefined || localEnv.fullReadOnlyPasscode === undefined || localEnv.updateStatePasscode === undefined) { await localEnv.setDefaults(); } if (adminPasscode) { localEnv.adminPasscode = adminPasscode; } if (displayReadOnlyPasscode) { localEnv.displayReadOnlyPasscode = displayReadOnlyPasscode; } if (fullReadOnlyPasscode) { localEnv.fullReadOnlyPasscode = fullReadOnlyPasscode; } if (updateStatePasscode) { localEnv.updateStatePasscode = updateStatePasscode; } await localEnv.save(); } function getFriendlyPasscode(str) { if (str.length >= 4) { str = str.toUpperCase(); return str.substring(0, 4) + "-" + str.substring(4); } return str; } async function stop() { process.exit(); } async function minecraftEulaAndPrivacyPolicy() { if (!localEnv) { throw new Error(); } await localEnv.load(); const questions = []; console.log("To download the Minecraft Bedrock Dedicated Server, you must agree to the Minecraft End User License Agreement and Privacy Policy.\n"); console.log(" Minecraft End User License Agreement: https://minecraft.net/eula"); console.log(" Minecraft Privacy Policy: https://go.microsoft.com/fwlink/?LinkId=521839\n"); questions.push({ type: "confirm", default: false, name: "minecraftEulaAndPrivacyPolicy", message: "I agree to the Minecraft End User License Agreement and Privacy Policy", }); const answers = await inquirer.prompt(questions); const iaccept = answers["minecraftEulaAndPrivacyPolicy"]; if (iaccept === true || iaccept === false) { localEnv.iAgreeToTheMinecraftEndUserLicenseAgreementAndPrivacyPolicyAtMinecraftDotNetSlashEula = iaccept; await localEnv.save(); } } async function displayInfo() { for (const projectStart of projectStarts) { if (projectStart && carto) { const project = ClUtils_js_1.default.createProject(carto, projectStart); await project.inferProjectItemsFromFiles(); console.log("Project name: " + project.name); console.log("Project description: " + project.description); const bpFolder = await project.getDefaultBehaviorPackFolder(); if (bpFolder === null) { console.log("No default behavior pack."); } else { console.log("Default behavior pack folder: " + bpFolder.storageRelativePath); } const rpFolder = await project.getDefaultResourcePackFolder(); if (rpFolder === null) { console.log("No default resource pack."); } else { console.log("Default resource pack folder: " + rpFolder.storageRelativePath); } const itemsCopy = project.getItemsCopy(); for (let i = 0; i < itemsCopy.length; i++) { const item = itemsCopy[i]; console.log("=== " + item.typeTitle + ": " + item.projectPath); if (item.isWorld) { await setAndDisplayWorld(item, false); } } const pis = project.infoSet; await pis.generateForProject(); for (let i = 0; i < pis.items.length; i++) { const item = pis.items[i]; if (item.itemType !== IInfoItemData_js_1.InfoItemType.testCompleteFail && item.itemType !== IInfoItemData_js_1.InfoItemType.testCompleteSuccess) { console.log(pis.itemToString(item)); } } } } } async function setAndDisplayAllWorlds() { const isEnsure = mode === "set"; let ofName; if (options.outputFolder) { ofName = StorageUtilities_js_1.default.getLeafName(options.outputFolder); if (ofName) { ofName = StorageUtilities_js_1.default.canonicalizeName(ofName); } } for (const projectStart of projectStarts) { if (projectStart && carto) { const project = ClUtils_js_1.default.createProject(carto, projectStart); await project.inferProjectItemsFromFiles(); let setWorld = false; const itemsCopy = project.getItemsCopy(); for (const item of itemsCopy) { if (item.isWorld) { let shouldProcess = true; if (item.projectPath && ofName) { const name = StorageUtilities_js_1.default.canonicalizeName(StorageUtilities_js_1.default.getBaseFromName(StorageUtilities_js_1.default.getLeafName(item.projectPath))); shouldProcess = ofName === name; } if (shouldProcess) { await setAndDisplayWorld(item, isEnsure); setWorld = true; } else { await setAndDisplayWorld(item, false); } } } // create a new world if (isEnsure && !setWorld && options.outputFolder) { const wcf = await project.ensureWorldContainer(); if (wcf && project.projectFolder) { // create a new folder for the world. let destF = project.projectFolder; const targetName = destF.name; // if only an output folder is specified, put the world there // if an input and an output folder is specified, put the world at a subfolder of the input folder. if (options.outputFolder) { let targetFolder = options.outputFolder; if (options.inputFolder && targetFolder.startsWith(options.inputFolder)) { targetFolder = targetFolder.substring(options.inputFolder.length); } if (targetFolder.length > 2) { destF = await wcf.ensureFolderFromRelativePath(StorageUtilities_js_1.default.ensureEndsDelimited(targetFolder)); } } if (destF) { let path = destF.getFolderRelativePath(project.projectFolder); if (path) { path = StorageUtilities_js_1.default.ensureEndsWithDelimiter(StorageUtilities_js_1.default.absolutize(path)); const pi = project.ensureItemByProjectPath(path, IProjectItemData_js_1.ProjectItemStorageType.folder, targetName, IProjectItemData_js_1.ProjectItemType.worldFolder); await pi.ensureFolderStorage(); await setAndDisplayWorld(pi, true); } } } } } } } async function setAndDisplayWorld(item, isSettable) { if (item.isWorld) { const mcworld = await item.getManager(); if (mcworld) { await mcworld.load(false); if (isSettable) { if (mcworld.name === "" && mcworld.storageFullPath) { mcworld.name = StorageUtilities_js_1.default.getBaseFromName(StorageUtilities_js_1.default.getLeafName(mcworld.storageFullPath)); } console.log("Updating mcworld at '" + mcworld.storageFullPath + "'"); const levelDat = mcworld.ensureLevelData(); let hasSet = false; if (options.betaApis !== undefined && options.betaApis !== levelDat.betaApisExperiment) { levelDat.betaApisExperiment = options.betaApis; console.log("Set beta APIs to " + options.betaApis); hasSet = true; } if (options.editor !== undefined && options.editor !== levelDat.isCreatedInEditor) { levelDat.isCreatedInEditor = options.editor; console.log("Set is editor to " + options.editor); hasSet = true; } if (options.dataDrivenItems !== undefined && options.dataDrivenItems !== levelDat.dataDrivenItemsExperiment) { levelDat.dataDrivenItemsExperiment = options.dataDrivenItems; console.log("Set data driven items to " + options.dataDrivenItems); hasSet = true; } if (options.behaviorPack !== undefined) { mcworld.ensureBehaviorPacksFromString(options.behaviorPack); } if (options.resourcePack !== undefined) { mcworld.ensureBehaviorPacksFromString(options.resourcePack); } if (hasSet) { await mcworld.save(); } } console.log("World name: " + mcworld.name); console.log("World path: " + item.projectPath); if (mcworld.betaApisExperiment !== undefined) { console.log("Beta APIs: " + mcworld.betaApisExperiment); } if (mcworld.levelData) { if (mcworld.levelData.dataDrivenItemsExperiment !== undefined) { console.log("Data Driven items (holiday experimental): " + mcworld.levelData.dataDrivenItemsExperiment); } } } } } async function displayServerProps() { if (!localEnv) { return; } let domainName = localEnv.serverDomainName; if (!domainName) { domainName = "(unspecified; used for display purposes only)"; } let port = localEnv.serverHostPort; if (!port) { port = 80; } let title = localEnv.serverTitle; if (!title) { title = "(unspecified; used for display purposes only)"; } let motd = localEnv.serverMessageOfTheDay; if (!motd) { motd = "(unspecified; used for display purposes only)"; } console.log("Server host domain name: " + domainName); console.log("Server port: " + port); console.log("Server title: " + title); console.log("Server message of the day: " + motd); } async function validate() { if (!carto || !localEnv) { return; } const projectList = []; const pool = (0, threads_1.Pool)(() => (0, threads_1.spawn)(new threads_1.Worker("./TaskWorker"), { timeout: 25000, }), threads); const suiteConst = suite; const exclusionListConst = exclusionList; const aggregateReportsAfterValidationConst = aggregateReportsAfterValidation; const localEnvConst = localEnv; for (let i = 0; i < projectStarts.length; i++) { const ps = projectStarts[i]; pool.queue(async (doTask) => { const result = await doTask({ task: ClUtils_js_1.TaskType.validate, project: ps, arguments: { suite: suiteConst, exclusionList: exclusionListConst, outputMci: aggregateReportsAfterValidationConst || outputType === ClUtils_js_1.OutputType.noReports ? true : false, outputType: outputType, }, outputFolder: options.outputFolder, inputFolder: options.inputFolder, displayInfo: localEnvConst.displayInfo, displayVerbose: localEnvConst.displayVerbose, force: force, }); if (result !== undefined) { if (typeof result === "string") { if (ps) { Log_js_1.default.error(ps.ctorProjectName + " error: " + result); } } else { for (const metaState of result) { // clear out icons since the aggregation won't need them, and it should save memory. if (metaState.infoSetData && metaState.infoSetData.info && metaState.infoSetData.info["defaultIcon"]) { metaState.infoSetData.info["defaultIcon"] = undefined; } projectList.push(metaState); const infoSet = metaState.infoSetData; if (infoSet) { const items = infoSet.items; if (items) { for (const item of items) { if (item.iTp === IInfoItemData_js_1.InfoItemType.internalProcessingError) { console.error("Internal Processing Error: " + ProjectInfoSet_js_1.default.getEffectiveMessageFromData(infoSet, item)); setErrorLevel(ERROR_VALIDATION_INTERNALPROCESSINGERROR); } else if (item.iTp === IInfoItemData_js_1.InfoItemType.testCompleteFail && !options.outputFolder) { console.error("Test Fail: " + ProjectInfoSet_js_1.default.getEffectiveMessageFromData(infoSet, item)); setErrorLevel(ERROR_VALIDATION_TESTFAIL); } else if (item.iTp === IInfoItemData_js_1.InfoItemType.error && !options.outputFolder) { console.error("Error: " + ProjectInfoSet_js_1.default.getEffectiveMessageFromData(infoSet, item)); setErrorLevel(ERROR_VALIDATION_ERROR); } } } } } } } }); } await pool.settled(); await pool.terminate(); if (aggregateReportsAfterValidation) { Log_js_1.default.message("Aggregating reports across " + projectList.length + " projects."); await saveAggregatedReports(projectList); } } function setErrorLevel(newErrorLevel) { if (errorLevel === undefined || newErrorLevel < errorLevel) { errorLevel = newErrorLevel; process.exitCode = newErrorLevel; } } async function aggregateReports() { let inpFolder = await ClUtils_js_1.default.getMainWorkFolder(executionTaskType, options.inputFolder, options.outputFolder); const allFeatureSets = {}; const allFields = {}; const projectList = []; if (!inpFolder) { Log_js_1.default.message("No main input folder was specified."); } await inpFolder.load(); let projectsLoaded = 0; for (const fileName in inpFolder.files) { let file = inpFolder.files[fileName]; if (file && StorageUtilities_js_1.default.getTypeFromName(fileName) === "json") { await file.loadContent(true); let jsonO = await StorageUtilities_js_1.default.getJsonObject(file); if (jsonO.info && jsonO.items && jsonO.generatorName !== undefined && jsonO.generatorVersion !== undefined) { const pis = new ProjectInfoSet_js_1.default(undefined, undefined, undefined, jsonO.info, jsonO.items); let suite = undefined; let baseName = StorageUtilities_js_1.default.getBaseFromName(fileName); if (baseName.endsWith(".mcr")) { baseName = baseName.substring(0, baseName.length - 4); } if (baseName.endsWith("addon")) { suite = IProjectInfoData_js_1.ProjectInfoSuite.cooperativeAddOn; baseName = baseName.substring(0, baseName.length - 6); } if (baseName.endsWith("currentplatform")) { suite = IProjectInfoData_js_1.ProjectInfoSuite.currentPlatformVersions; baseName = baseName.substring(0, baseName.length - 16); } let title = StorageUtilities_js_1.default.getBaseFromName(fileName); let firstDash = title.indexOf("-"); let lastDash = title.lastIndexOf("-"); if (firstDash > 0 && lastDash > firstDash + 1) { title = title.substring(firstDash + 1, lastDash); } if (projectsLoaded > 0 && projectsLoaded % 500 === 0) { console.warn("Loaded " + projectsLoaded + " reports, @ " + title); } pis.mergeFeatureSetsAndFieldsTo(allFeatureSets, allFields); // clear out icons since the aggregation won't need them, and it should save memory. if (jsonO.info && jsonO.info["defaultIcon"]) { jsonO.info["defaultIcon"] = undefined; } projectList.push({ projectContainerName: baseName, projectPath: file.parentFolder.storageRelativePath, projectName: baseName, projectTitle: title, infoSetData: jsonO, suite: suite, }); projectsLoaded++; } } } if (projectList.length > 0) { await saveAggregatedReports(projectList); } else { Log_js_1.default.message("Did not find any report JSON files."); } } async function saveAggregatedReports(projectList) { let outputStorage; const csvHeader = ProjectInfoSet_js_1.default.CommonCsvHeader; let sampleProjectInfoSets = {}; const featureSetsByName = {}; const fieldsByName = {}; const issueLines = {}; const summaryLines = {}; const mciFileList = { files: [], folders: [] }; const megaContentIndex = new ContentIndex_js_1.default(); const measures = {}; const dataMeasures = {}; if (options.outputFolder) { outputStorage = new NodeStorage_js_1.default(options.outputFolder, ""); } let projectsConsidered = 0; for (const projectSet of projectList) { let suiteName = "all"; if (projectSet.suite !== undefined) { suiteName = ProjectInfoSet_js_1.default.getSuiteString(projectSet.suite); } const pisData = projectSet.infoSetData; const contentIndex = new ContentIndex_js_1.default(); if (pisData.index) { contentIndex.loadFromData(pisData.index); } const pis = new ProjectInfoSet_js_1.default(undefined, undefined, undefined, pisData.info, pisData.items, contentIndex); const projectBaseName = StorageUtilities_js_1.default.removeContainerExtension(projectSet.projectContainerName); if (pis.contentIndex) { megaContentIndex.mergeFrom(pis.contentIndex, projectBaseName); } if (projectSet.projectName) { mciFileList.files.push(projectBaseName.toLowerCase() + ".mci.json"); } if (projectsConsidered > 0 && projectsConsidered % 500 === 0) { console.warn("Processed " + projectsConsidered + " reports, @ " + projectBaseName); } if (featureSetsByName[suiteName] === undefined) { featureSetsByName[suiteName] = {}; } const featureSets = featureSetsByName[suiteName]; if (fieldsByName[suiteName] === undefined) { fieldsByName[suiteName] = {}; } const fields = fieldsByName[suiteName]; pis.mergeFeatureSetsAndFieldsTo(featureSets, fields); projectsConsidered++; } for (const setName in featureSetsByName) { const featureSets = featureSetsByName[setName]; if (featureSets) { for (const featureSetName in featureSets) { const featureSet = featureSets[featureSetName]; if (featureSet) { measures[featureSetName] = { name: featureSetName, items: {}, }; } } } } for (const projectSet of projectList) { let suiteName = "all"; if (projectSet.suite !== undefined) { suiteName = ProjectInfoSet_js_1.default.getSuiteString(projectSet.suite); } const pisData = projectSet.infoSetData; const projectBaseName = StorageUtilities_js_1.default.removeContainerExtension(projectSet.projectContainerName); const pis = new ProjectInfoSet_js_1.default(undefined, undefined, undefined, pisData.info, pisData.items); if (featureSetsByName[suiteName] === undefined) { featureSetsByName[suiteName] = {}; } const featureSets = featureSetsByName[suiteName]; if (fieldsByName[suiteName] === undefined) { fieldsByName[suiteName] = {}; } const fields = fieldsByName[suiteName]; pis.mergeFeatureSetsAndFieldsTo(featureSets, fields); sampleProjectInfoSets[suiteName] = pis; if (projectSet.suite === undefined || projectSet.suite === IProjectInfoData_js_1.ProjectInfoSuite.default) { if (projectSet.infoSetData.info) { for (const memberName in projectSet.infoSetData.info) { if (ProjectInfoSet_js_1.default.isAggregableFieldName(memberName)) { let data = dataMeasures[memberName]; if (data === undefined) { data = { name: memberName, items: {} }; dataMeasures[memberName] = data; } if (projectSet.infoSetData.info[memberName] !== undefined) { data.items[projectBaseName] = projectSet.infoSetData.info[memberName]; } } } } if (projectSet.infoSetData.info && projectSet.infoSetData.info.featureSets) { for (const featureSetName in featureSets) { const featureSet = projectSet.infoSetData.info.featureSets[featureSetName]; if (featureSet) { measures[featureSetName].items[projectBaseName] = featureSet; } } } } if (issueLines[suiteName] === undefined) { issueLines[suiteName] = []; } if (issueLines[suiteName].length <= MAX_LINES_PER_CSV_FILE) { const pisLines = pis.getItemCsvLines(); for (let j = 0; j < pisLines.length; j++) { issueLines[suiteName].push('"' + projectBaseName + '",' + pisLines[j]); } } if (summaryLines[suiteName] === undefined) { summaryLines[suiteName] = []; } if (outputStorage) { summaryLines[suiteName].push(pis.getSummaryCsvLine(projectBaseName, projectSet.projectTitle, featureSets)); } } if (outputStorage) { for (const issueLinesName in issueLines) { if (sampleProjectInfoSets[issueLinesName]) { const issueLinesSet = issueLines[issueLinesName]; if (issueLinesSet.length < MAX_LINES_PER_CSV_FILE) { let allCsvFile = outputStorage.rootFolder.ensureFile(issueLinesName + ".csv"); let allCsvContent = "Project," + csvHeader + "\r\n" + issueLinesSet.join("\n"); allCsvFile.setContent(allCsvContent); await allCsvFile.saveContent(); } } } for (const summaryLinesName in summaryLines) { if (featureSetsByName[summaryLinesName] === undefined) { featureSetsByName[summaryLinesName] = {}; } if (sampleProjectInfoSets[summaryLinesName]) { const featureSets = featureSetsByName[summaryLinesName]; const summaryLinesSet = summaryLines[summaryLinesName]; const projectsCsvFile = outputStorage.rootFolder.ensureFile(summaryLinesName + "projects.csv"); let projectsCsvContent = ProjectInfoSe