@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
584 lines (583 loc) • 26.3 kB
JavaScript
"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 });
const Log_1 = __importDefault(require("../core/Log"));
const Utilities_1 = __importDefault(require("../core/Utilities"));
const LevelDb_1 = __importDefault(require("../minecraft/LevelDb"));
const MCWorld_1 = __importDefault(require("../minecraft/MCWorld"));
const WorldLevelDat_1 = __importDefault(require("../minecraft/WorldLevelDat"));
const StorageUtilities_1 = __importStar(require("../storage/StorageUtilities"));
const IProjectData_1 = require("./IProjectData");
const IProjectItemData_1 = require("./IProjectItemData");
const minecraftIndicatorFolderNames = [
"development_behavior_packs",
"development_resource_packs",
"development_resource_packs",
"behavior_packs",
"resource_packs",
"behavior_pack",
"resource_pack",
"world_templates",
"com.mojang",
"minecraftworlds",
"worlds",
"world",
];
class ProjectIntegrator {
candidateFiles = {};
worlds = [];
isInErrorState;
errorMessages;
_pushError(message, contextIn) {
this.isInErrorState = true;
if (this.errorMessages === undefined) {
this.errorMessages = [];
}
message = message + contextIn ? " " + contextIn : "";
Log_1.default.error(message);
this.errorMessages.push({
message: message,
context: contextIn,
});
return message;
}
static async createProjectFromFolder(creatorTools, newProjectName, folder, operationDescriptor, log) {
let newProject = await creatorTools.createNewProject(newProjectName, undefined, undefined, undefined, IProjectData_1.ProjectFocus.general, true, IProjectData_1.ProjectScriptLanguage.typeScript);
await this.extendProjectFromCollectionFolder(creatorTools, newProject, folder, operationDescriptor, log);
await creatorTools.save();
return newProject;
}
static async extendProjectFromCollectionFolder(creatorTools, project, folder, operationDescriptor, log) {
// assume everything else is "loose files" that need to be reconstructed
const pi = new ProjectIntegrator();
await pi.addFromFolder(project, folder, true, log);
await pi.processResults(project, operationDescriptor, log);
await project.inferProjectItemsFromFiles(true);
}
async attachWorldDataToWorlds() {
for (const world of this.worlds) {
if (world.currentManifest) {
if (world.manifestFiles.length > 0) {
await this.addToWorldByFileIds(world);
}
}
}
for (const world of this.worlds) {
if (world.currentManifest) {
if (world.manifestFiles.length > 0) {
await this.findLevelDat(world);
}
}
}
}
async processResults(project, operationDescriptor, log) {
for (const world of this.worlds) {
// find our MANIFEST
if (world.currentManifest) {
for (const fileName in this.candidateFiles) {
const tokenToLookFor = world.currentManifest.substring(2).trim();
const file = this.candidateFiles[fileName];
if (file &&
!file.isUsed &&
file.type === IProjectItemData_1.ProjectItemType.levelDbManifest &&
fileName.indexOf(tokenToLookFor) >= 0) {
file.isUsed = true;
world.manifestFiles.push(file);
break;
}
}
if (world.manifestFiles.length > 0) {
await this.addToWorldByManifestData(world);
}
}
}
await this.attachWorldDataToWorlds();
// now that we've attached as many LDBs/LOGs to the "good" manifests connected to CURRENTs,
// let's see if we can make more worlds out of 'leftover' MANIFEST files (those without CURRENTs)
// in some cases their CURRENT might be missing, or in other cases they are older versions of "good" manifests
for (const fileName in this.candidateFiles) {
const file = this.candidateFiles[fileName];
if (file && !file.isUsed && file.type === IProjectItemData_1.ProjectItemType.levelDbManifest) {
file.isUsed = true;
const world = {
manifestFiles: [file],
ldbFiles: [],
logFiles: [],
};
if (world.manifestFiles.length > 0) {
await this.addToWorldByManifestData(world);
}
this.worlds.push(world);
}
}
await this.attachWorldDataToWorlds();
const leftoverWorld = {
manifestFiles: [],
ldbFiles: [],
logFiles: [],
};
for (const fileName in this.candidateFiles) {
const file = this.candidateFiles[fileName];
if (file && !file.isUsed && file.type === IProjectItemData_1.ProjectItemType.levelDbLdb) {
file.isUsed = true;
leftoverWorld.ldbFiles.push(file);
}
if (file && !file.isUsed && file.type === IProjectItemData_1.ProjectItemType.levelDbLog) {
file.isUsed = true;
leftoverWorld.logFiles.push(file);
}
if (file && !file.isUsed && file.type === IProjectItemData_1.ProjectItemType.levelDat && leftoverWorld.levelDatFile === undefined) {
file.isUsed = true;
leftoverWorld.levelDatFile = file;
}
}
if (leftoverWorld.ldbFiles.length > 0 || leftoverWorld.logFiles.length > 0) {
this.worlds.push(leftoverWorld);
}
await project.ensureLoadedProjectFolder();
if (project.projectFolder) {
let worldsFolder = await project.ensureWorldContainer();
if (worldsFolder) {
let index = 0;
for (const world of this.worlds) {
await this.copyFilesToWorld(world, project, worldsFolder, index);
index++;
}
}
}
project.appendErrors(this, operationDescriptor);
}
async addToWorldByManifestData(world) {
let keySets = [];
let logIndices = [];
let ldbIndices = [];
for (const manifestFile of world.manifestFiles) {
if (manifestFile.levelDbSubset &&
manifestFile.levelDbSubset.newFileNumber &&
manifestFile.levelDbSubset.newFileSmallest &&
manifestFile.levelDbSubset.newFileLargest) {
for (let i = 0; i < manifestFile.levelDbSubset.newFileSmallest.length; i++) {
const smallest = manifestFile.levelDbSubset.newFileSmallest[i];
const largest = manifestFile.levelDbSubset.newFileLargest[i];
if (smallest && largest) {
keySets.push({ smallest: smallest, largest: largest });
}
}
for (let i = 0; i < manifestFile.levelDbSubset.newFileNumber.length; i++) {
const index = manifestFile.levelDbSubset.newFileNumber[i];
if (!manifestFile.levelDbSubset.deletedFileNumber ||
!manifestFile.levelDbSubset.deletedFileNumber.includes(index)) {
ldbIndices.push(index);
}
}
if (manifestFile.levelDbSubset.logNumber !== undefined) {
logIndices.push(manifestFile.levelDbSubset.logNumber);
}
}
}
for (const fileName in this.candidateFiles) {
const file = this.candidateFiles[fileName];
if (!file.isUsed &&
(file.type === IProjectItemData_1.ProjectItemType.levelDbLdb || file.type === IProjectItemData_1.ProjectItemType.levelDbLog) &&
file.levelDbSubset) {
for (const keySet of keySets) {
if (file.levelDbSubset.keys.get(keySet.largest) !== undefined &&
file.levelDbSubset.keys.get(keySet.smallest) !== undefined) {
if (file.type === IProjectItemData_1.ProjectItemType.levelDbLog) {
file.isUsed = true;
world.logFiles.push(file);
break;
}
else if (file.type === IProjectItemData_1.ProjectItemType.levelDbLdb) {
file.isUsed = true;
world.ldbFiles.push(file);
break;
}
}
}
}
}
}
async addToWorldByFileIds(world) {
let logIndices = [];
let ldbIndices = [];
for (const manifestFile of world.manifestFiles) {
if (manifestFile.levelDbSubset && manifestFile.levelDbSubset.newFileNumber) {
for (let i = 0; i < manifestFile.levelDbSubset.newFileNumber.length; i++) {
const index = manifestFile.levelDbSubset.newFileNumber[i];
if (!manifestFile.levelDbSubset.deletedFileNumber ||
!manifestFile.levelDbSubset.deletedFileNumber.includes(index)) {
ldbIndices.push(index);
}
}
if (manifestFile.levelDbSubset.logNumber !== undefined) {
logIndices.push(manifestFile.levelDbSubset.logNumber);
}
}
}
for (const fileName in this.candidateFiles) {
const file = this.candidateFiles[fileName];
if (!file.isUsed &&
file.type === IProjectItemData_1.ProjectItemType.levelDbLdb &&
file.levelDbSubset &&
file.index !== undefined &&
ldbIndices.includes(file.index)) {
file.isUsed = true;
world.ldbFiles.push(file);
}
else if (!file.isUsed &&
file.type === IProjectItemData_1.ProjectItemType.levelDbLog &&
file.levelDbSubset &&
file.index !== undefined &&
logIndices.includes(file.index)) {
file.isUsed = true;
world.logFiles.push(file);
}
}
}
async findLevelDat(world) {
if ((world.ldbFiles.length > 0 || world.logFiles.length > 0) && world.manifestFiles.length > 0) {
for (const file of world.ldbFiles) {
if (file && file.levelDbSubset) {
const mcworld = new MCWorld_1.default();
await mcworld.loadFromLevelDb(file.levelDbSubset);
if (mcworld.generationSeed) {
world.seedFromLevelDb = mcworld.generationSeed;
break;
}
}
}
if (!world.seedFromLevelDb) {
for (const file of world.logFiles) {
if (file && file.levelDbSubset) {
const mcworld = new MCWorld_1.default();
await mcworld.loadFromLevelDb(file.levelDbSubset);
if (mcworld.generationSeed) {
world.seedFromLevelDb = mcworld.generationSeed;
break;
}
}
}
}
if (world.seedFromLevelDb) {
for (const fileName in this.candidateFiles) {
const file = this.candidateFiles[fileName];
if (!file.isUsed &&
file.type === IProjectItemData_1.ProjectItemType.levelDat &&
file.levelDat &&
file.levelDat.randomSeed === world.seedFromLevelDb) {
file.isUsed = true;
world.levelDatFile = file;
}
}
}
}
}
async copyFilesToWorld(world, project, worldsFolder, index) {
if (world.ldbFiles.length > 0 || world.logFiles.length > 0) {
// && world.manifestFiles.length > 0) {
let name = "world" + index;
if (world.levelDatFile && world.levelDatFile.levelDat?.levelName) {
name = world.levelDatFile.levelDat?.levelName;
let worldIndex = 0;
// ensure folder name is unique
while (worldsFolder.folders[name] !== undefined) {
worldIndex++;
name = world.levelDatFile.levelDat?.levelName + " " + worldIndex;
}
}
const worldFolder = worldsFolder.ensureFolder(name);
const newLevelDataFile = worldFolder.ensureFile("level.dat");
const newLevelDat = new WorldLevelDat_1.default();
newLevelDat.ensureDefaults();
newLevelDat.levelName = name;
if (world.levelDatFile &&
world.levelDatFile.file.content &&
world.levelDatFile.file.content instanceof Uint8Array) {
// we could probably just newLevelDat = world.levelDatFile.levelDat
// but we're going to modify it so use the new copy.
newLevelDat.loadFromNbtBytes(world.levelDatFile.file.content);
}
// note that we are re-writing level.dat rather than just copying it.
// add experiments flag
newLevelDat.experimentsEverUsed = true;
newLevelDat.savedWithToggledExperiments = true;
newLevelDat.persist();
const levelDatBytes = newLevelDat.getBytes();
if (levelDatBytes !== undefined) {
newLevelDataFile.setContent(levelDatBytes);
}
else if (world.levelDatFile &&
world.levelDatFile.file.content &&
world.levelDatFile.file.content instanceof Uint8Array) {
newLevelDataFile.setContent(world.levelDatFile.file.content);
}
const levelNameFile = worldFolder.ensureFile("levelname.txt");
levelNameFile.setContent(name);
let dbFolder = worldFolder.ensureFolder("db");
for (const file of world.ldbFiles) {
if (file && file.index && file.file.content) {
const newFileName = Utilities_1.default.frontPadToLength(file.index, 6, "0");
const newLdbFile = dbFolder.ensureFile(newFileName + ".ldb");
newLdbFile.setContent(file.file.content);
}
}
for (const file of world.logFiles) {
if (file && file.index && file.file.content) {
const newFileName = Utilities_1.default.frontPadToLength(file.index, 6, "0");
const newLogFile = dbFolder.ensureFile(newFileName + ".log");
newLogFile.setContent(file.file.content);
}
}
let manifestFileName = world.currentManifest;
for (const file of world.manifestFiles) {
if (file && file.index && file.file.content) {
const newFileName = "MANIFEST-" + Utilities_1.default.frontPadToLength(file.index, 6, "0");
if (!manifestFileName) {
manifestFileName = newFileName;
}
const newManifestFile = dbFolder.ensureFile(newFileName);
newManifestFile.setContent(file.file.content);
}
}
const currentFile = dbFolder.ensureFile("CURRENT");
currentFile.setContent(manifestFileName + "\n");
}
}
async addFromFolder(project, sourceFolder, processSubFolders, log) {
await sourceFolder.load();
const files = [];
for (const filePath in sourceFolder.files) {
const file = sourceFolder.files[filePath];
if (file) {
files.push(file);
}
}
await this.addLooseFiles(project, files, log);
if (processSubFolders) {
// first pass: see if the root folder contains any structured 'known' Minecraft Folders
for (const folderName in sourceFolder.folders) {
const folderNameToLower = folderName.toLowerCase().trim();
const childFolder = sourceFolder.folders[folderName];
if (childFolder) {
if (minecraftIndicatorFolderNames.includes(folderNameToLower)) {
// handle what looks like special minecraft folders in some way
}
}
}
for (const folderName in sourceFolder.folders) {
const folderNameToLower = folderName.toLowerCase().trim();
const childFolder = sourceFolder.folders[folderName];
if (childFolder) {
if (!minecraftIndicatorFolderNames.includes(folderNameToLower)) {
// treat non-Minecraft indicated folders as loose files
if (childFolder) {
await this.addFromFolder(project, childFolder, true);
}
}
}
}
}
}
async addLooseFiles(project, files, log) {
for (const file of files) {
const extension = StorageUtilities_1.default.getTypeFromName(file.name);
if (file.name.indexOf("evel") >= 0 && extension === "dat") {
if (!file.isContentLoaded) {
await file.loadContent();
}
if (file.content && file.content instanceof Uint8Array) {
const levelDat = new WorldLevelDat_1.default();
try {
levelDat.loadFromNbtBytes(file.content);
if (!levelDat.isInErrorState) {
this.candidateFiles[file.storageRelativePath] = {
file: file,
levelDat: levelDat,
type: IProjectItemData_1.ProjectItemType.levelDat,
};
}
}
catch (e) {
if (log) {
log("Error processing " + file.name + " (" + e.toString() + ")");
}
}
}
}
else if (file.name.indexOf("RRENT") >= 0) {
await this.considerCURRENTFile(file, log);
}
else if (file.name.indexOf("NIFEST") >= 0) {
await this.considerMANIFESTFile(file, log);
}
else {
switch (extension) {
case "ldb":
if (!file.isContentLoaded) {
await file.loadContent();
}
if (file.isBinary) {
const levelDb = new LevelDb_1.default([file], [], [], file.name);
try {
await levelDb.init(log);
Utilities_1.default.appendErrors(this, levelDb);
if (!levelDb.isInErrorState) {
this.candidateFiles[file.storageRelativePath] = {
file: file,
index: ProjectIntegrator.getIndexFromString(file.name),
levelDbSubset: levelDb,
type: IProjectItemData_1.ProjectItemType.levelDbLdb,
};
}
}
catch (e) {
const error = this._pushError("Error processing ldb file " + file.name + " (" + e.toString() + ")");
if (log) {
log(error);
}
}
}
break;
case "log":
await file.loadContent(false, StorageUtilities_1.EncodingType.ByteBuffer);
if (file.isBinary) {
const levelDb = new LevelDb_1.default([], [file], [], file.name);
try {
await levelDb.init(log);
Utilities_1.default.appendErrors(this, levelDb);
if (!levelDb.isInErrorState) {
this.candidateFiles[file.storageRelativePath] = {
file: file,
index: ProjectIntegrator.getIndexFromString(file.name),
levelDbSubset: levelDb,
type: IProjectItemData_1.ProjectItemType.levelDbLog,
};
}
}
catch (e) {
const error = this._pushError("Error processing log file " + file.name + " (" + e.toString() + ")");
if (log) {
log(error);
}
}
}
break;
}
}
}
}
async considerCURRENTFile(file, log) {
await file.loadContent(false, StorageUtilities_1.EncodingType.Utf8String);
if (file.content && typeof file.content === "string") {
if (file.content.startsWith("MANIFEST")) {
this.worlds.push({
currentFile: file,
currentManifest: file.content.trim(),
manifestFiles: [],
ldbFiles: [],
logFiles: [],
});
}
}
}
static getIndexFromString(fileName) {
fileName = StorageUtilities_1.default.getBaseFromName(fileName.trim());
const lastDash = fileName.lastIndexOf("-");
if (lastDash >= 0) {
fileName = fileName.substring(lastDash + 1);
}
let zeroStart = -1;
let adjustStr = "";
let index = 0;
for (const char of fileName) {
if (char >= "1" && char <= "9") {
if (zeroStart >= 0) {
adjustStr += char;
}
}
else if (char === "0") {
zeroStart = index;
adjustStr += char;
}
else if (zeroStart >= 0) {
break;
}
index++;
}
if (adjustStr.length > 4) {
fileName = adjustStr;
}
try {
const num = parseInt(fileName);
if (isNaN(num)) {
return undefined;
}
return num;
}
catch (e) { }
return undefined;
}
async considerMANIFESTFile(file, log) {
await file.loadContent(false, StorageUtilities_1.EncodingType.ByteBuffer);
if (file.isBinary) {
const levelDb = new LevelDb_1.default([], [], [file], file.name);
try {
await levelDb.init(log);
Utilities_1.default.appendErrors(this, levelDb);
if (!levelDb.isInErrorState) {
this.candidateFiles[file.storageRelativePath] = {
file: file,
index: ProjectIntegrator.getIndexFromString(file.name),
levelDbSubset: levelDb,
type: IProjectItemData_1.ProjectItemType.levelDbManifest,
};
}
}
catch (e) {
const error = this._pushError("Error processing MANIFEST file " + file.name + " (" + e.toString() + ")");
if (log) {
log(error);
}
}
}
}
}
exports.default = ProjectIntegrator;