UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

1,249 lines 133 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const ZipStorage_1 = __importDefault(require("../storage/ZipStorage")); const ste_events_1 = require("ste-events"); const Log_1 = __importDefault(require("./../core/Log")); const WorldLevelDat_1 = __importDefault(require("./WorldLevelDat")); const Utilities_1 = __importDefault(require("../core/Utilities")); const StorageUtilities_1 = __importDefault(require("../storage/StorageUtilities")); const LevelDb_1 = __importDefault(require("./LevelDb")); const DataUtilities_1 = __importDefault(require("../core/DataUtilities")); const WorldChunk_1 = __importDefault(require("./WorldChunk")); const BlockVolume_1 = __importDefault(require("./BlockVolume")); const Block_1 = __importDefault(require("./Block")); const Entity_1 = __importDefault(require("./Entity")); const IStorage_1 = require("../storage/IStorage"); const MinecraftUtilities_1 = __importDefault(require("./MinecraftUtilities")); const NbtBinary_1 = __importDefault(require("./NbtBinary")); const NbtBinaryTag_1 = require("./NbtBinaryTag"); const AnchorSet_1 = __importDefault(require("./AnchorSet")); const ActorItem_1 = __importDefault(require("./ActorItem")); const Status_1 = require("../app/Status"); const WorldChunkCache_1 = __importDefault(require("./WorldChunkCache")); const BEHAVIOR_PACKS_RELPATH = "/world_behavior_packs.json"; const BEHAVIOR_PACK_HISTORY_RELPATH = "/world_behavior_pack_history.json"; const RESOURCE_PACKS_RELPATH = "/world_resource_packs.json"; const RESOURCE_PACK_HISTORY_RELPATH = "/world_resource_pack_history.json"; const LEVELDAT_RELPATH = "/level.dat"; const LEVELDATOLD_RELPATH = "/level.dat_old"; const LEVELNAMETXT_RELPATH = "/levelname.txt"; const MANIFEST_RELPATH = "/manifest.json"; const CHUNK_X_SIZE = 16; const CHUNK_Z_SIZE = 16; const CREATOR_TOOLS_EDITOR_BPUUID = "5d2f0b91-ca29-49da-a275-e6c6262ea3de"; class MCWorld { // Where possible, if _file is defined we'd prefer to use // _file.fileContentStorage for zip manip. _zipStorage is only used from // a pure "zip bytes in memory" scenario (for generating downloads.) _zipStorage; _file; _folder; _project; _autogenTsFile; _anchors = new AnchorSet_1.default(); _dynamicProperties = {}; _levelNameText; _manifest; _isLoaded = false; _isDataLoaded = false; _onLoaded = new ste_events_1.EventDispatcher(); _onDataLoaded = new ste_events_1.EventDispatcher(); _onChunkUpdated = new ste_events_1.EventDispatcher(); /** Event fired when world data is externally modified and reloaded */ _onWorldDataReloaded = new ste_events_1.EventDispatcher(); /** Whether we're listening to storage events for automatic updates */ _isListeningToStorage = false; _hasDynamicProps = false; _hasCustomProps = false; _onPropertyChanged = new ste_events_1.EventDispatcher(); _biomeData; _overworldData; _levelChunkMetaData; _generationSeed; isInErrorState; errorMessages; worldBehaviorPacks; worldResourcePacks; worldBehaviorPackHistory; worldResourcePackHistory; chunkCount = 0; _chunkMinY = -64; imageBase64; levelDb; actorsById = {}; levelData; _minX; _maxX; _minZ; _maxZ; regionsByDimension = {}; chunks = new Map(); /** * All dimension IDs found in LevelDB chunk keys, including custom dimensions (>= 1000). * Populated during processWorldData or buildMinimalWorldIndex. */ _dimensionIdsInChunks = new Set(); /** * Parsed DimensionNameIdTable from LevelDB: maps dimension name to numeric ID. * Undefined if the DimensionNameIdTable key was not found. */ _dimensionNameIdTable; /** Whether the DimensionNameIdTable key exists in the LevelDB */ _hasDimensionNameIdTable = false; /** LRU cache for chunk data - manages memory by evicting old chunks */ _chunkCache; /** Whether lazy loading mode is enabled for this world */ _isLazyLoadMode = false; /** * Set of chunk keys that exist in the world (format: "dim_x_z"). * Built during buildMinimalWorldIndex for O(1) chunk existence checking. * This allows fast filtering of empty/non-existent chunks without scanning LevelDB keys. */ _chunkExistsSet = new Set(); /** * Returns the set of all known chunk keys (format: "dim_x_z"). * Used by WorldMap to ensure sparse worlds render all known chunks, * not just those hit by the sampling grid. */ get knownChunkKeys() { return this._chunkExistsSet; } /** * Index mapping chunk keys ("dim_x_z") to the list of LevelDB key names for that chunk. * Built during buildMinimalWorldIndex for O(1) chunk key lookup. * This eliminates the O(N) full-scan of LevelDB keys in getOrCreateChunk. */ _chunkKeyIndex = new Map(); get project() { return this._project; } set project(newProject) { this._project = newProject; } get anchors() { return this._anchors; } get chunkMinY() { return this._chunkMinY; } set chunkMinY(newY) { this._chunkMinY = newY; } get effectiveRootFolder() { if (this._folder) { return this._folder; } if (this._file && this._file.fileContainerStorage) { return this._file.fileContainerStorage.rootFolder; } if (this._zipStorage !== undefined) { return this._zipStorage.rootFolder; } return undefined; } get manifest() { return this._manifest; } get hasDynamicProps() { return this._hasDynamicProps; } get hasCustomProps() { return this._hasCustomProps; } get minX() { return this._minX; } get maxX() { return this._maxX; } get minZ() { return this._minZ; } get maxZ() { return this._maxZ; } /** Get whether lazy loading mode is enabled */ get isLazyLoadMode() { return this._isLazyLoadMode; } /** Get the chunk cache (only available when using chunk caching) */ get chunkCache() { return this._chunkCache; } /** All dimension IDs found in LevelDB chunk keys, including custom dimensions (>= 1000). */ get dimensionIdsInChunks() { return this._dimensionIdsInChunks; } /** Whether the DimensionNameIdTable key was found in the LevelDB. */ get hasDimensionNameIdTable() { return this._hasDimensionNameIdTable; } /** Parsed DimensionNameIdTable: maps dimension name to numeric ID. Undefined if not found. */ get dimensionNameIdTable() { return this._dimensionNameIdTable; } /** Parse DimensionNameIdTable NBT bytes into the name-to-ID map. */ _parseDimensionNameIdTable(tableBytes) { this._hasDimensionNameIdTable = true; const tableNbt = new NbtBinary_1.default(); tableNbt.context = this.name + " DimensionNameIdTable"; tableNbt.fromBinary(tableBytes, true, false, 0, true); if (tableNbt.singleRoot) { this._dimensionNameIdTable = new Map(); const children = tableNbt.singleRoot.getTagChildren(); for (const child of children) { if (child.name && (child.type === NbtBinaryTag_1.NbtTagType.int || child.type === NbtBinaryTag_1.NbtTagType.long)) { this._dimensionNameIdTable.set(child.name, child.valueAsInt); } } } } /** * Check if chunk data exists at the specified coordinates without loading it. * This is O(1) when buildMinimalWorldIndex has been called (skipFullProcessing mode). * Returns true if the chunk exists in the world's LevelDB data. * Returns undefined if existence cannot be determined (index not built). */ hasChunkData(dim, x, z) { // If we have the chunk exists set, use it for O(1) lookup if (this._chunkExistsSet.size > 0) { return this._chunkExistsSet.has(`${dim}_${x}_${z}`); } // If we already have the chunk loaded, it exists const existing = this.getChunkAt(dim, x, z); if (existing) { return true; } // Can't determine without scanning LevelDB return undefined; } /** * Get a chunk by coordinates. * If chunk caching is enabled, marks the chunk as recently accessed. */ getChunkAt(dim, x, z) { const dimMap = this.chunks.get(dim); if (!dimMap) return undefined; const xPlane = dimMap.get(x); if (!xPlane) return undefined; const chunk = xPlane.get(z); // Track access for LRU cache if (chunk && this._chunkCache) { this._chunkCache.access(dim, x, z); } return chunk; } /** * Get a chunk by cache key (format: "dim_x_z"). * Used by WorldChunkCache for eviction callbacks. */ getChunkByKey(key) { const coords = WorldChunkCache_1.default.parseKey(key); if (!coords) return undefined; const dimMap = this.chunks.get(coords.dim); if (!dimMap) return undefined; const xPlane = dimMap.get(coords.x); if (!xPlane) return undefined; return xPlane.get(coords.z); } get generationSeed() { if (this._generationSeed === undefined && this._levelChunkMetaData && this._levelChunkMetaData.singleRoot) { const tag = this._levelChunkMetaData.singleRoot.find("GenerationSeed"); if (tag !== null) { this._generationSeed = tag.valueAsBigInt.toString(); } } return this._generationSeed; } async copyAsFolderTo(targetFolder) { if (this._folder) { await StorageUtilities_1.default.syncFolderTo(this._folder, targetFolder, true, true, true); } else if (this._file) { const storage = this.storage; if (storage) { await StorageUtilities_1.default.syncFolderTo(this.storage.rootFolder, targetFolder, true, true, true); } } } get storage() { if (this._file) { if (!this._file.fileContainerStorage) { this._file.fileContainerStorage = new ZipStorage_1.default(); this._file.fileContainerStorage.containerFile = this._file; this._file.fileContainerStorage.storagePath = this._file.extendedPath + "#"; } return this._file.fileContainerStorage; } if (this._zipStorage === undefined) { this._zipStorage = new ZipStorage_1.default(); } return this._zipStorage; } ensureZipStorage() { if (this._zipStorage === undefined) { this._zipStorage = new ZipStorage_1.default(); } } get onPropertyChanged() { return this._onPropertyChanged.asEvent(); } get storageErrorStatus() { if (!this.storage) { return IStorage_1.StorageErrorStatus.none; } return this.storage.errorStatus; } get storageErrorMessage() { return this.storage.errorMessage; } get storageFullPath() { if (this._file) { return this._file.fullPath; } if (this._folder) { return this._folder.fullPath; } return undefined; } get deferredTechnicalPreviewExperiment() { if (this.levelData !== undefined) { const val = this.levelData.deferredTechnicalPreviewExperiment; if (val === undefined) { return false; } return val; } return false; } set deferredTechnicalPreviewExperiment(newVal) { if (this.levelData === undefined) { this.levelData = new WorldLevelDat_1.default(); } if (this.levelData !== undefined) { this.levelData.deferredTechnicalPreviewExperiment = newVal; } } get betaApisExperiment() { if (this.levelData !== undefined) { const val = this.levelData.betaApisExperiment; if (val === undefined) { return false; } return val; } return false; } set betaApisExperiment(newVal) { if (this.levelData === undefined) { this.levelData = new WorldLevelDat_1.default(); } if (this.levelData !== undefined) { this.levelData.betaApisExperiment = newVal; } } get dataDrivenItemsExperiment() { if (this.levelData !== undefined) { const val = this.levelData.dataDrivenItemsExperiment; if (val === undefined) { return false; } return val; } return false; } set dataDrivenItemsExperiment(newVal) { if (this.levelData === undefined) { this.levelData = new WorldLevelDat_1.default(); } if (this.levelData !== undefined) { this.levelData.dataDrivenItemsExperiment = newVal; } } get name() { if (this._levelNameText !== undefined) { return this._levelNameText; } if (this.levelData !== undefined && this.levelData.levelName !== undefined) { return this.levelData.levelName; } if (this._file !== undefined) { return this._file.name; } return ""; } set name(newValue) { this._levelNameText = newValue; if (this.levelData !== undefined) { this.levelData.levelName = newValue; } } get file() { return this._file; } set file(newFile) { this._file = newFile; } get folder() { return this._folder; } set folder(newFolder) { this._folder = newFolder; } get isLoaded() { return this._isLoaded; } get spawnX() { if (this.levelData === undefined) { return undefined; } return this.levelData.spawnX; } set spawnX(newX) { if (this.levelData === undefined) { this.levelData = new WorldLevelDat_1.default(); } this.levelData.spawnX = newX; this._onPropertyChanged.dispatch(this, "spawnX"); } get spawnY() { if (this.levelData === undefined) { return undefined; } return this.levelData.spawnY; } set spawnY(newY) { if (this.levelData === undefined) { this.levelData = new WorldLevelDat_1.default(); } this.levelData.spawnY = newY; this._onPropertyChanged.dispatch(this, "spawnY"); } get spawnZ() { if (this.levelData === undefined) { return undefined; } return this.levelData.spawnZ; } set spawnZ(newZ) { if (this.levelData === undefined) { this.levelData = new WorldLevelDat_1.default(); } this.levelData.spawnZ = newZ; this._onPropertyChanged.dispatch(this, "spawnZ"); } get onLoaded() { return this._onLoaded.asEvent(); } get onDataLoaded() { return this._onDataLoaded.asEvent(); } get onChunkUpdated() { return this._onChunkUpdated.asEvent(); } /** * Event fired when world data is externally modified and reloaded. * Subscribe to this event to update UI when remote changes occur. */ get onWorldDataReloaded() { return this._onWorldDataReloaded.asEvent(); } /** * Whether this world is listening to storage events for automatic updates. */ get isListeningToStorage() { return this._isListeningToStorage; } /** * Start listening to storage events for automatic world data updates. * When file changes are detected (via WebSocket or fs watcher), the world * will automatically reload affected data and fire appropriate events. */ startListeningToStorage() { Log_1.default.verbose("[MCWorld] startListeningToStorage called, _isListeningToStorage=" + this._isListeningToStorage); if (this._isListeningToStorage) { Log_1.default.verbose("[MCWorld] Already listening to storage events - returning"); return; } const storage = this.effectiveRootFolder?.storage; Log_1.default.verbose("[MCWorld] effectiveRootFolder=" + this.effectiveRootFolder?.fullPath + ", storage=" + (storage ? storage.constructor.name : "null")); if (!storage) { Log_1.default.verbose("[MCWorld] Cannot start listening: no storage available"); return; } Log_1.default.message(`[MCWorld] Starting to listen to storage events on ${storage.constructor.name}`); // Subscribe to file content updates storage.onFileContentsUpdated.subscribe((sender, event) => { Log_1.default.message(`[MCWorld] Received onFileContentsUpdated: ${event.file.storageRelativePath}`); this._handleStorageFileUpdate(event.file.storageRelativePath, event); }); // Subscribe to file additions storage.onFileAdded.subscribe((sender, file) => { Log_1.default.message(`[MCWorld] Received onFileAdded: ${file.storageRelativePath}`); this._handleStorageFileAdded(file.storageRelativePath); }); // Subscribe to file removals storage.onFileRemoved.subscribe((sender, path) => { Log_1.default.message(`[MCWorld] Received onFileRemoved: ${path}`); this._handleStorageFileRemoved(path); }); this._isListeningToStorage = true; Log_1.default.message("[MCWorld] Successfully subscribed to storage events"); } /** * Stop listening to storage events. */ stopListeningToStorage() { // Note: ste-events doesn't have a simple "unsubscribe all" mechanism // In a full implementation, we'd need to track subscription handles this._isListeningToStorage = false; Log_1.default.verbose("MCWorld: Stopped listening to storage events"); } /** * Handle a file update from storage. */ async _handleStorageFileUpdate(path, event) { if (!path) return; // The path coming from storage events may be like /world/db/000039.log // But MCWorld's effectiveRootFolder is already the world folder, // so we need to strip the /world prefix if present let relativePath = path; if (relativePath.startsWith("/world/")) { relativePath = relativePath.substring(6); // Keep the leading / after "world" } else if (relativePath.startsWith("world/")) { relativePath = "/" + relativePath.substring(6); } const pathLower = relativePath.toLowerCase(); const rootFolder = this.effectiveRootFolder; if (!rootFolder) return; try { // Check what type of file changed if (pathLower.endsWith("level.dat")) { // Reload level data const levelDatFile = await rootFolder.getFileFromRelativePath(LEVELDAT_RELPATH); if (levelDatFile) { await levelDatFile.loadContent(true); if (levelDatFile.content instanceof Uint8Array) { this.levelData = new WorldLevelDat_1.default(); this.levelData.loadFromNbtBytes(levelDatFile.content); this._loadFromNbt(); } } this._onWorldDataReloaded.dispatch(this, "level.dat"); } else if (pathLower.endsWith("levelname.txt")) { // Reload level name const levelNameFile = await rootFolder.getFileFromRelativePath(LEVELNAMETXT_RELPATH); if (levelNameFile) { await levelNameFile.loadContent(true); if (typeof levelNameFile.content === "string") { this.name = levelNameFile.content; } } this._onWorldDataReloaded.dispatch(this, "levelname.txt"); } else if (pathLower.includes("/db/") && (pathLower.endsWith(".ldb") || pathLower.endsWith(".log"))) { // LevelDB file changed - use incremental loading await this._handleIncrementalLevelDbUpdate(relativePath); } else if (pathLower.endsWith("world_behavior_packs.json")) { const packsFile = await rootFolder.getFileFromRelativePath(BEHAVIOR_PACKS_RELPATH); if (packsFile) { await packsFile.loadContent(true); if (typeof packsFile.content === "string") { this.worldBehaviorPacks = StorageUtilities_1.default.getJsonObject(packsFile); } } this._onWorldDataReloaded.dispatch(this, "behavior_packs"); } else if (pathLower.endsWith("world_resource_packs.json")) { const packsFile = await rootFolder.getFileFromRelativePath(RESOURCE_PACKS_RELPATH); if (packsFile) { await packsFile.loadContent(true); if (typeof packsFile.content === "string") { this.worldResourcePacks = StorageUtilities_1.default.getJsonObject(packsFile); } } this._onWorldDataReloaded.dispatch(this, "resource_packs"); } Log_1.default.verbose(`MCWorld: Handled file update: ${path}`); } catch (e) { Log_1.default.debug(`MCWorld: Error handling file update ${path}: ${e}`); } } /** * Handle a new file being added to storage. */ async _handleStorageFileAdded(path) { // For now, treat file additions similar to updates await this._handleStorageFileUpdate(path, null); } /** * Handle a file being removed from storage. */ async _handleStorageFileRemoved(path) { if (!path) return; const pathLower = path.toLowerCase(); // Handle removal of important files if (pathLower.includes("/db/") && pathLower.endsWith(".ldb")) { // LevelDB compaction - chunks may have moved to new files // This is a rare case where we might need a broader refresh, // but typically compaction doesn't lose data, just reorganizes it Log_1.default.verbose(`MCWorld: LDB file removed (compaction): ${path}`); } Log_1.default.verbose(`MCWorld: Handled file removal: ${path}`); } /** * Handle incremental LevelDB file updates. * * When a new .ldb or .log file is detected, this method: * 1. Parses just that file to extract new keys * 2. Identifies which chunks are affected by those keys * 3. Updates only those chunks with the new data * 4. Fires onChunkUpdated for each affected chunk (for UI updates) * * This is much more efficient than reloading the entire world. */ async _handleIncrementalLevelDbUpdate(path) { if (!this.levelDb) { // LevelDB not yet loaded - nothing to update incrementally return; } const rootFolder = this.effectiveRootFolder; if (!rootFolder) { return; } try { // Get the file that changed const file = await rootFolder.getFileFromRelativePath(path); if (!file) { Log_1.default.debug(`MCWorld: Could not find LDB file for incremental update: ${path}`); return; } // Parse the file and get affected chunk coordinates const affectedChunks = await this.levelDb.parseIncrementalFile(file); if (affectedChunks.length === 0) { return; } Log_1.default.verbose(`MCWorld: Incremental update - ${affectedChunks.length} chunks affected from ${path}`); // Update each affected chunk for (const coord of affectedChunks) { await this._updateChunkFromLevelDb(coord); } } catch (e) { Log_1.default.error(`MCWorld: Error in incremental LDB update ${path}: ${e}`); // Fall back to signaling a broader refresh this._onWorldDataReloaded.dispatch(this, "leveldb"); } } /** * Update a single chunk from LevelDB keys. * * This finds all keys for the specified chunk coordinates and either: * - Updates an existing chunk with the new data * - Creates a new chunk if one doesn't exist * * After updating, fires onChunkUpdated for UI refresh. */ async _updateChunkFromLevelDb(coord) { if (!this.levelDb) return; const { x, z, dimension } = coord; // Get or create the chunk let chunk = this.getChunkAt(dimension, x, z); const isNewChunk = !chunk; if (!chunk) { chunk = new WorldChunk_1.default(this, x, z); } else { // Clear cached/parsed data so it will be re-parsed from the updated keys chunk.clearCachedData(); } // Find all keys that belong to this chunk and add them const chunkKey = `${dimension}_${x}_${z}`; const indexedKeyNames = this._chunkKeyIndex.get(chunkKey); if (indexedKeyNames && indexedKeyNames.length > 0) { // Fast path: use the pre-built chunk key index for (const keyname of indexedKeyNames) { const keyValue = this.levelDb.keys.get(keyname); if (!keyValue || typeof keyValue === "boolean") continue; chunk.addKeyValue(keyValue); } } else { // Fallback: scan all LevelDB keys (used when index isn't built) const hasDim = dimension !== 0; for (const [keyname, keyValue] of this.levelDb.keys) { // Skip if keyValue is undefined, false (deleted marker), or not a LevelKeyValue if (!keyValue || typeof keyValue === "boolean") continue; const keyBytes = keyValue.keyBytes; if (!keyBytes) continue; if (keyBytes.length < 9 || keyBytes.length > 14) continue; const expectedLength = hasDim ? 13 : 9; if (keyBytes.length !== expectedLength && keyBytes.length !== expectedLength + 1) continue; const kx = DataUtilities_1.default.getSignedInteger(keyBytes[0], keyBytes[1], keyBytes[2], keyBytes[3], true); const kz = DataUtilities_1.default.getSignedInteger(keyBytes[4], keyBytes[5], keyBytes[6], keyBytes[7], true); if (kx !== x || kz !== z) continue; let kDim = 0; if (hasDim && keyBytes.length >= 13) { kDim = DataUtilities_1.default.getSignedInteger(keyBytes[8], keyBytes[9], keyBytes[10], keyBytes[11], true); } if (kDim !== dimension) continue; // This key belongs to this chunk - add it chunk.addKeyValue(keyValue); } } // Add new chunk to the chunks map if needed if (isNewChunk) { let dimMap = this.chunks.get(dimension); if (!dimMap) { dimMap = new Map(); this.chunks.set(dimension, dimMap); } let xPlane = dimMap.get(x); if (!xPlane) { xPlane = new Map(); dimMap.set(x, xPlane); } xPlane.set(z, chunk); this.chunkCount++; // Update bounds if (this._minX === undefined || x * 16 < this._minX) { this._minX = x * 16; } if (this._maxX === undefined || (x + 1) * 16 > this._maxX) { this._maxX = (x + 1) * 16; } if (this._minZ === undefined || z * 16 < this._minZ) { this._minZ = z * 16; } if (this._maxZ === undefined || (z + 1) * 16 > this._maxZ) { this._maxZ = (z + 1) * 16; } // Add to chunk exists set if (this._chunkExistsSet) { this._chunkExistsSet.add(`${dimension}_${x}_${z}`); } } // Track access for LRU cache if (this._chunkCache) { this._chunkCache.access(dimension, x, z); } // Notify listeners that this chunk was updated this._onChunkUpdated.dispatch(this, chunk); } /** * Called by WorldChunk when chunk data is superceded by newer LevelDB keys. * This notifies subscribers (like WorldMap) that they may need to redraw affected tiles. */ notifyChunkUpdated(chunk) { this._onChunkUpdated.dispatch(this, chunk); } static async ensureMCWorldOnFolder(folder, project, handler) { if (folder.manager === undefined) { const world = new MCWorld(); world.project = project; world.folder = folder; folder.manager = world; } if (folder.manager !== undefined && folder.manager instanceof MCWorld) { const mcworld = folder.manager; if (!mcworld.isLoaded) { if (handler) { mcworld.onLoaded.subscribe(handler); } await mcworld.loadMetaFiles(false); } else if (handler) { handler(mcworld, mcworld, { unsub: () => { }, stopPropagation: () => { } }); } return mcworld; } return undefined; } static async ensureOnItem(projectItem) { let mcworld = undefined; if (!projectItem.isContentLoaded) { await projectItem.loadContent(); } if (projectItem.defaultFolder) { mcworld = await MCWorld.ensureMCWorldOnFolder(projectItem.defaultFolder, projectItem.project); } else if (projectItem.primaryFile) { mcworld = await MCWorld.ensureOnFile(projectItem.primaryFile, projectItem.project); } if (!mcworld) { Log_1.default.debugAlert("Could not find respective world."); } return mcworld; } static async ensureOnFile(file, project, handler) { if (file.manager === undefined) { const world = new MCWorld(); world.project = project; world.file = file; file.manager = world; } if (file.manager !== undefined && file.manager instanceof MCWorld) { const mcworld = file.manager; if (!mcworld.isLoaded) { if (handler) { mcworld.onLoaded.subscribe(handler); } await mcworld.loadMetaFiles(false); } else if (handler) { handler(mcworld, mcworld, { unsub: () => { }, stopPropagation: () => { } }); } return mcworld; } return undefined; } loadAnchorsFromDynamicProperties() { if (this._dynamicProperties && this._dynamicProperties[CREATOR_TOOLS_EDITOR_BPUUID]) { this._anchors.clearAll(); const anchorStr = this._dynamicProperties && this._dynamicProperties[CREATOR_TOOLS_EDITOR_BPUUID]["anchors"]; if (anchorStr && typeof anchorStr === "string") { this._anchors.fromString(anchorStr); this.saveAutoGenItems(); } } } _updateMeta() { this.regionsByDimension = {}; const chunkDims = this.chunks.keys(); for (const dimNum of chunkDims) { const dim = this.chunks.get(dimNum); let regions = []; if (dim) { const xNums = dim.keys(); for (const xNum of xNums) { const xPlane = dim.get(xNum); if (xPlane) { const zNums = xPlane.keys(); for (const zNum of zNums) { let addedToRegion = false; for (const region of regions) { if (xNum >= region.minX && xNum <= region.maxX && zNum >= region.minZ && zNum <= region.maxZ) { region.minX = Math.min(region.minX, xNum - 1); region.minZ = Math.min(region.minZ, zNum - 1); region.maxX = Math.max(region.maxX, xNum + 1); region.maxZ = Math.max(region.maxZ, zNum + 1); addedToRegion = true; } } if (!addedToRegion) { regions.push({ minX: xNum - 1, minZ: zNum - 1, maxX: xNum + 1, maxZ: zNum + 1, }); } } } } } this.regionsByDimension[dimNum] = this._coalesceRegions(regions); } } _coalesceRegions(regions) { const newRegions = []; for (const region of regions) { let addedToRegion = false; for (const newRegion of newRegions) { if (region.minX >= newRegion.minX && region.minX <= newRegion.maxX && region.minZ >= newRegion.minZ && region.minZ <= newRegion.maxZ) { newRegion.minX = Math.min(newRegion.minX, region.minX - 1); newRegion.minZ = Math.min(newRegion.minZ, region.minZ - 1); newRegion.maxX = Math.max(newRegion.maxX, region.minX + 1); newRegion.maxZ = Math.max(newRegion.maxZ, region.minZ + 1); addedToRegion = true; break; } if (region.maxX >= newRegion.minX && region.maxX <= newRegion.maxX && region.minZ >= newRegion.minZ && region.minZ <= newRegion.maxZ) { newRegion.minX = Math.min(newRegion.minX, region.maxX - 1); newRegion.minZ = Math.min(newRegion.minZ, region.minZ - 1); newRegion.maxX = Math.max(newRegion.maxX, region.maxX + 1); newRegion.maxZ = Math.max(newRegion.maxZ, region.minZ + 1); addedToRegion = true; break; } if (region.minX >= newRegion.minX && region.minX <= newRegion.maxX && region.maxZ >= newRegion.minZ && region.maxZ <= newRegion.maxZ) { newRegion.minX = Math.min(newRegion.minX, region.minX - 1); newRegion.minZ = Math.min(newRegion.minZ, region.maxZ - 1); newRegion.maxX = Math.max(newRegion.maxX, region.minX + 1); newRegion.maxZ = Math.max(newRegion.maxZ, region.maxZ + 1); addedToRegion = true; break; } if (region.maxX >= newRegion.minX && region.maxX <= newRegion.maxX && region.maxZ >= newRegion.minZ && region.maxZ <= newRegion.maxZ) { newRegion.minX = Math.min(newRegion.minX, region.maxX - 1); newRegion.minZ = Math.min(newRegion.minZ, region.maxZ - 1); newRegion.maxX = Math.max(newRegion.maxX, region.maxX + 1); newRegion.maxZ = Math.max(newRegion.maxZ, region.maxZ + 1); addedToRegion = true; break; } } if (!addedToRegion) { newRegions.push(region); } } return newRegions; } _pushError(message, contextIn) { this.isInErrorState = true; if (this.errorMessages === undefined) { this.errorMessages = []; } Log_1.default.error(message + (contextIn ? " " + contextIn : "")); this.errorMessages.push({ message: message, context: contextIn, }); } async save() { if (this.storageErrorStatus === IStorage_1.StorageErrorStatus.unprocessable) { return; } await this.saveWorldManifest(); await this.saveLevelnameTxt(); await this.saveLevelDat(); await this.saveAutoGenItems(); await this.saveWorldBehaviorPacks(); await this.saveWorldBehaviorPackHistory(); await this.saveWorldResourcePacks(); await this.saveWorldResourcePackHistory(); } async saveWorldManifest() { if (this._manifest !== undefined && this.effectiveRootFolder !== undefined) { this._manifest.header.name = this.name; const manifestJsonFile = await this.effectiveRootFolder.ensureFileFromRelativePath(MANIFEST_RELPATH); if (manifestJsonFile !== undefined) { manifestJsonFile.setContent(JSON.stringify(this._manifest, null, 2)); await manifestJsonFile.saveContent(); } } } async saveLevelnameTxt() { const name = this.name; if (name !== undefined && this.effectiveRootFolder !== undefined) { const rootDataFile = await this.effectiveRootFolder.ensureFileFromRelativePath(LEVELNAMETXT_RELPATH); if (rootDataFile !== undefined) { rootDataFile.setContent(name); await rootDataFile.saveContent(); } } } async saveLevelDat() { if (this.levelData !== undefined && this.effectiveRootFolder !== undefined) { this.levelData.persist(); let rootDataFile = await this.effectiveRootFolder.ensureFileFromRelativePath(LEVELDAT_RELPATH); const bytes = this.levelData.getBytes(); if (rootDataFile !== undefined && bytes !== undefined) { rootDataFile.setContent(bytes); await rootDataFile.saveContent(); } rootDataFile = await this.effectiveRootFolder.ensureFileFromRelativePath(LEVELDATOLD_RELPATH); if (rootDataFile !== undefined && bytes !== undefined) { rootDataFile.setContent(bytes); await rootDataFile.saveContent(); } } } async getBytes() { if (this._file) { if (!this._file.fileContainerStorage) { this._file.fileContainerStorage = new ZipStorage_1.default(); this._file.fileContainerStorage.containerFile = this._file; this._file.fileContainerStorage.storagePath = this._file.extendedPath + "#"; } await this.save(); return await this._file.fileContainerStorage.generateUint8ArrayAsync(); } if (this._zipStorage === undefined) { return undefined; } await this.save(); return await this._zipStorage.generateUint8ArrayAsync(); } async syncFolderTo(folder) { await this.save(); const sourceFolder = this.effectiveRootFolder; if (!sourceFolder) { Log_1.default.unexpectedUndefined("SFT"); return; } await StorageUtilities_1.default.syncFolderTo(sourceFolder, folder, true, true, true); } async saveToFile() { if (this._zipStorage === undefined || this._file === undefined) { return; } const bytes = await this.getBytes(); if (bytes !== undefined) { this._file.setContent(bytes); } } ensurePackReferenceSet(packRefSet) { if (this.worldBehaviorPacks === undefined) { this.worldBehaviorPacks = []; } if (this.worldResourcePacks === undefined) { this.worldResourcePacks = []; } if (this.worldBehaviorPackHistory === undefined) { this.worldBehaviorPackHistory = { packs: [], }; } if (this.worldResourcePackHistory === undefined) { this.worldResourcePacks = []; } if (this.worldResourcePackHistory === undefined) { this.worldResourcePackHistory = { packs: [], }; } if (packRefSet.behaviorPackReferences) { for (let i = 0; i < packRefSet.behaviorPackReferences.length; i++) { this.ensurePackReferenceInCollection(packRefSet.behaviorPackReferences[i], this.worldBehaviorPacks); this.ensurePackReferenceInHistory(packRefSet.behaviorPackReferences[i], this.worldBehaviorPackHistory, packRefSet.name); } } if (packRefSet.resourcePackReferences) { for (let i = 0; i < packRefSet.resourcePackReferences.length; i++) { this.ensurePackReferenceInCollection(packRefSet.resourcePackReferences[i], this.worldResourcePacks); this.ensurePackReferenceInHistory(packRefSet.resourcePackReferences[i], this.worldResourcePackHistory, packRefSet.name); } } } ensurePackReferenceInCollection(packRef, packRefs) { Log_1.default.assert(packRef.version.length === 3, "Packref version not within bounds."); const compareUuid = Utilities_1.default.canonicalizeId(packRef.uuid); for (let i = 0; i < packRefs.length; i++) { if (Utilities_1.default.canonicalizeId(packRefs[i].pack_id) === compareUuid) { return; } } packRefs.push({ pack_id: packRef.uuid, version: packRef.version, priority: packRef.priority ? packRef.priority : 32767, }); } ensurePackReferenceInHistory(packRef, packHistory, name) { Log_1.default.assert(packRef.version.length === 3, "Packref version not within bounds."); if (packHistory.packs === undefined) { packHistory.packs = []; } const compareUuid = Utilities_1.default.canonicalizeId(packRef.uuid); for (let i = 0; i < packHistory.packs.length; i++) { if (Utilities_1.default.canonicalizeId(packHistory.packs[i].uuid) === compareUuid) { return; } } packHistory.packs.push({ can_be_redownloaded: false, name: name, uuid: packRef.uuid, version: packRef.version }); } _loadFromNbt() { } getProperty(id) { const ld = this.levelData; switch (id.toLowerCase()) { // World identity case "levelname": return ld?.levelName; case "gametype": return ld?.gameType; case "difficulty": return ld?.difficulty; case "generator": return ld?.generator; case "flatworldlayers": return ld?.flatWorldLayers ? JSON.stringify(ld.flatWorldLayers) : undefined; case "randomseed": return ld?.randomSeed; case "basegameversion": return ld?.baseGameVersion; case "inventoryversion": return ld?.inventoryVersion; // Spawn & bounds case "spawnx": return this.spawnX; case "spawny": return this.spawnY; case "spawnz": return this.spawnZ; case "limitedworldoriginx": return ld?.limitedWorldOriginX; case "limitedworldoriginy": return ld?.limitedWorldOriginY; case "limitedworldoriginz": return ld?.limitedWorldOriginZ; case "limitedworlddepth": return ld?.limitedWorldDepth; case "limitedworldwidth": return ld?.limitedWorldWidth; case "spawnradius": return ld?.spawnRadius; // Game rules (boolean) case "commandsenabled": return ld?.commandsEnabled; case "commandblocksenabled": return ld?.commandBlocksEnabled; case "commandblockoutput": return ld?.commandBlockOutput; case "cheatsenabled": return ld?.cheatsEnabled; case "dodaylightcycle": return ld?.doDaylightCycle; case "doentitydrops": return ld?.doEntityDrops; case "dofiretick": return ld?.doFireTick; case "doimmediaterespawn": return ld?.doImmediateRespawn; case "doinsomnia": return ld?.doInsomnia; case "domobloot": return ld?.doMobLoot; case "domobspawning": return ld?.doMobSpawning; case "dotiledrops": return ld?.doTileDrops; case "doweathercycle": return ld?.doWeatherCycle; case "drowningdamage": return ld?.drowningDamage; case "falldamage": return ld?.fallDamage; case "firedamage": return ld?.fireDamage; case "freezedamage": return ld?.freezeDamage; case "keepinventory": return ld?.keepInventory; case "mobgriefing": return ld?.mobGriefing; case "naturalregeneration": return ld?.naturalRegeneration; case "pvp": return ld?.pvp; case "respawnblocksexplode": return ld?.respawnBlocksExplode; case "sendcommandfeedback": return ld?.sendCommandFeedback; case "showcoordinates": return ld?.showCoordinates; case "showdeathmessages": return ld?.showDeathMessages; case "showtags": return ld?.showTags; case "showbordereffect": return ld?.showBorderEffect; case "tntexplodes": return ld?.tntExplodes; case "forcegametype": return ld?.forceGameType; case "immutableworld": return ld?.immutableWorld; case "spawnmobs": return ld?.spawnMobs; case "bonuschestenabled": return ld?.bonusChestEnabled; case "bonuschestspawned": return ld?.bonusChestSpawned; case "startwithmapenabled": return ld?.startWithMapEnabled; // Game rules (numeric) case "randomtickspeed": return ld?.randomTickSpeed; case "functioncommandlimit": return ld?.functionCommandLimit; case "maxcommandchainlength": return ld?.maxCommandChainLength; case "serverchunktickrange": return ld?.serverChunkTickRange; case "netherscale": return ld?.netherScale; // Multiplayer case "multiplayergame": return ld?.multiplayerGame; case "multiplayergameintent": return ld?.multiplayerGameIntent; case "lanbroadcast": return ld?.lanBroadcast; case "lanbroadcastintent": return ld?.lanBroadcastIntent; case "platformbroadcastintent": return ld?.platformBroadcastIntent; case "xblbroadcastintent": return ld?.xblBroadcastIntent; case "usemsagamertagsonly": return ld?.useMsaGamertagsOnly; case "texturepacksrequired": return ld?.texturePacksRequired; // Editor case "iscreatedineditor": return ld?.isCreatedInEditor; case "isexportedfromeditor": return ld?.isExportedFromEditor; case "editorworldtype": return ld?.editorWorldType; // Experiments case "experimentalgameplay": return ld?.experimentalGameplay; case "betaapisexperiment": return ld?.betaApisExperiment; case "deferredtechnicalpreviewexperiment": return ld?.deferredTechnicalPreviewExperiment; case "datadrivenitemsexperiment": return ld?.dataDrivenItemsExperiment; case "savedwithtoggledexperiments": return ld?.savedWithToggledExperiments; case "experimentseverused": return ld?.experimentsEverUsed; // Permissions & abilities case "permissionslevel": return ld?.permissionsLevel; case "playerpermissionslevel": return ld?.playerPermissionsLevel; case "attackmobs": return