@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
1,249 lines • 133 kB
JavaScript
"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