@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
479 lines (478 loc) • 21.2 kB
TypeScript
/**
* MCWorld.ts
*
* ARCHITECTURE DOCUMENTATION
* ==========================
*
* MCWorld is the primary class for managing Minecraft world data, including:
* - World metadata (level.dat, level name, spawn position)
* - World chunks and blocks (via LevelDB)
* - Behavior/resource pack registrations
* - Dynamic properties and experiments
*
* REAL-TIME SYNCHRONIZATION:
* --------------------------
* MCWorld can listen to IStorage events to automatically update when external
* changes occur (e.g., file changes from a remote server):
*
* 1. Call startListeningToStorage() to subscribe to storage events
* 2. When onFileContentsUpdated fires, MCWorld reloads affected data
* 3. MCWorld fires appropriate events (onChunkUpdated, onDataReloaded, etc.)
* 4. WorldView and other UI components update in response
*
* EVENTS:
* -------
* - onLoaded: Fired when world initially loads
* - onDataLoaded: Fired when world data (chunks) finishes loading
* - onChunkUpdated: Fired when a specific chunk is updated/reloaded
* - onWorldDataReloaded: Fired when world files are externally modified and reloaded
* - onPropertyChanged: Fired when a world property changes
*
* DATA FLOW:
* ----------
* HttpStorage (WebSocket notifications) -> MCWorld (this) ->
* onChunkUpdated/onWorldDataReloaded -> WorldView (React update)
*
* RELATED FILES:
* --------------
* - IStorage.ts: Storage events (onFileContentsUpdated, etc.)
* - HttpStorage.ts: Client-side storage with WebSocket notifications
* - WorldView.tsx: UI component that displays world data
* - WorldChunk.ts: Individual chunk data
* - LevelDb.ts: LevelDB file parser
*/
import IFile from "../storage/IFile";
import ZipStorage from "../storage/ZipStorage";
import { IEventHandler } from "ste-events";
import IPackRegistration from "./IPackRegistration";
import IFolder from "./../storage/IFolder";
import IPackHistory from "./IPackHistory";
import WorldLevelDat from "./WorldLevelDat";
import IGetSetPropertyObject from "../dataform/IGetSetPropertyObject";
import IWorldManifest from "./IWorldManifest";
import LevelDb from "./LevelDb";
import WorldChunk from "./WorldChunk";
import BlockLocation from "./BlockLocation";
import BlockVolume from "./BlockVolume";
import IDimension from "./IDimension";
import Block from "./Block";
import Entity from "./Entity";
import { IPackageReference, IWorldSettings } from "./IWorldSettings";
import { StorageErrorStatus } from "../storage/IStorage";
import AnchorSet from "./AnchorSet";
import Project from "../app/Project";
import ActorItem from "./ActorItem";
import { IErrorMessage, IErrorable } from "../core/IErrorable";
import ProjectItem from "../app/ProjectItem";
import WorldChunkCache from "./WorldChunkCache";
export interface IWorldProcessingOptions {
maxNumberOfRecordsToProcess?: number;
progressCallback?: (phase: string, current: number, total: number) => void;
/**
* If true, unloads raw file content after parsing to reduce memory usage.
* Recommended for large worlds to prevent out-of-memory errors.
* Default: true (optimized for memory)
*/
unloadFilesAfterParse?: boolean;
/**
* If true, deletes LevelDB keys from the keys Map after they are processed
* and handed off to WorldChunks. This significantly reduces memory usage
* for large worlds by eliminating duplicate references.
* Default: true (optimized for memory)
*/
clearKeysAfterProcess?: boolean;
/**
* If true, uses lazy loading mode for LevelDB.
* Only manifest metadata is loaded initially; files are loaded on-demand.
* This dramatically reduces initial memory usage for large worlds.
*
* When enabled:
* - Initial load only parses manifest files
* - LDB/LOG files are loaded only when their keys are needed
* - Chunk cache manages memory by evicting least-recently-used chunks
*
* Default: false (full load for backwards compatibility)
*/
lazyLoad?: boolean;
/**
* Maximum number of chunks to keep in the LRU cache when using lazy loading.
* When exceeded, least-recently-used chunks have their parsed data cleared
* (but can be re-parsed on demand from raw LevelKeyValue data).
*
* Default: 20000
*/
maxChunksInCache?: number;
/**
* If true, skips the full "Phase 2" world data processing that creates
* WorldChunk objects for every chunk upfront. Instead, chunks will be
* created on-demand when they are first accessed.
*
* This is ideal for map viewing where only visible chunks need to be loaded.
* It dramatically reduces memory usage for very large worlds (100k+ chunks).
*
* When enabled:
* - World bounds (minX, maxX, minZ, maxZ) are calculated from key names
* - Chunk objects are created lazily when getChunkAt() is called
* - Full chunk data is only parsed when accessed
*
* Default: false (full processing for backwards compatibility)
*/
skipFullProcessing?: boolean;
}
export interface IRegion {
minX: number;
minZ: number;
maxX: number;
maxZ: number;
}
export default class MCWorld implements IGetSetPropertyObject, IDimension, IErrorable {
private _zipStorage?;
private _file?;
private _folder?;
private _project?;
private _autogenTsFile?;
private _anchors;
private _dynamicProperties;
private _levelNameText?;
private _manifest?;
private _isLoaded;
private _isDataLoaded;
private _onLoaded;
private _onDataLoaded;
private _onChunkUpdated;
/** Event fired when world data is externally modified and reloaded */
private _onWorldDataReloaded;
/** Whether we're listening to storage events for automatic updates */
private _isListeningToStorage;
private _hasDynamicProps;
private _hasCustomProps;
private _onPropertyChanged;
private _biomeData;
private _overworldData;
private _levelChunkMetaData;
private _generationSeed;
isInErrorState?: boolean;
errorMessages?: IErrorMessage[];
worldBehaviorPacks?: IPackRegistration[];
worldResourcePacks?: IPackRegistration[];
worldBehaviorPackHistory?: IPackHistory;
worldResourcePackHistory?: IPackHistory;
chunkCount: number;
private _chunkMinY;
imageBase64?: string;
levelDb?: LevelDb;
actorsById: {
[identifier: string]: ActorItem;
};
levelData?: WorldLevelDat;
private _minX;
private _maxX;
private _minZ;
private _maxZ;
regionsByDimension: {
[dim: number]: IRegion[];
};
chunks: Map<number, Map<number, Map<number, WorldChunk>>>;
/**
* All dimension IDs found in LevelDB chunk keys, including custom dimensions (>= 1000).
* Populated during processWorldData or buildMinimalWorldIndex.
*/
private _dimensionIdsInChunks;
/**
* Parsed DimensionNameIdTable from LevelDB: maps dimension name to numeric ID.
* Undefined if the DimensionNameIdTable key was not found.
*/
private _dimensionNameIdTable;
/** Whether the DimensionNameIdTable key exists in the LevelDB */
private _hasDimensionNameIdTable;
/** LRU cache for chunk data - manages memory by evicting old chunks */
private _chunkCache?;
/** Whether lazy loading mode is enabled for this world */
private _isLazyLoadMode;
/**
* 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.
*/
private _chunkExistsSet;
/**
* 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(): ReadonlySet<string>;
/**
* 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.
*/
private _chunkKeyIndex;
get project(): Project | undefined;
set project(newProject: Project | undefined);
get anchors(): AnchorSet;
get chunkMinY(): number;
set chunkMinY(newY: number);
get effectiveRootFolder(): IFolder | import("../storage/ZipFolder").default;
get manifest(): IWorldManifest;
get hasDynamicProps(): boolean;
get hasCustomProps(): boolean;
get minX(): number;
get maxX(): number;
get minZ(): number;
get maxZ(): number;
/** Get whether lazy loading mode is enabled */
get isLazyLoadMode(): boolean;
/** Get the chunk cache (only available when using chunk caching) */
get chunkCache(): WorldChunkCache | undefined;
/** All dimension IDs found in LevelDB chunk keys, including custom dimensions (>= 1000). */
get dimensionIdsInChunks(): ReadonlySet<number>;
/** Whether the DimensionNameIdTable key was found in the LevelDB. */
get hasDimensionNameIdTable(): boolean;
/** Parsed DimensionNameIdTable: maps dimension name to numeric ID. Undefined if not found. */
get dimensionNameIdTable(): ReadonlyMap<string, number> | undefined;
/** Parse DimensionNameIdTable NBT bytes into the name-to-ID map. */
private _parseDimensionNameIdTable;
/**
* 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: number, x: number, z: number): boolean | undefined;
/**
* Get a chunk by coordinates.
* If chunk caching is enabled, marks the chunk as recently accessed.
*/
getChunkAt(dim: number, x: number, z: number): WorldChunk | undefined;
/**
* Get a chunk by cache key (format: "dim_x_z").
* Used by WorldChunkCache for eviction callbacks.
*/
getChunkByKey(key: string): WorldChunk | undefined;
get generationSeed(): string;
copyAsFolderTo(targetFolder: IFolder): Promise<void>;
get storage(): import("../storage/IStorage").default | ZipStorage;
ensureZipStorage(): void;
get onPropertyChanged(): import("ste-events").IEvent<MCWorld, string>;
get storageErrorStatus(): StorageErrorStatus;
get storageErrorMessage(): string;
get storageFullPath(): string;
get deferredTechnicalPreviewExperiment(): boolean;
set deferredTechnicalPreviewExperiment(newVal: boolean);
get betaApisExperiment(): boolean;
set betaApisExperiment(newVal: boolean);
get dataDrivenItemsExperiment(): boolean;
set dataDrivenItemsExperiment(newVal: boolean);
get name(): string;
set name(newValue: string);
get file(): IFile | undefined;
set file(newFile: IFile | undefined);
get folder(): IFolder | undefined;
set folder(newFolder: IFolder | undefined);
get isLoaded(): boolean;
get spawnX(): number | undefined;
set spawnX(newX: number | undefined);
get spawnY(): number | undefined;
set spawnY(newY: number | undefined);
get spawnZ(): number | undefined;
set spawnZ(newZ: number | undefined);
get onLoaded(): import("ste-events").IEvent<MCWorld, MCWorld>;
get onDataLoaded(): import("ste-events").IEvent<MCWorld, MCWorld>;
get onChunkUpdated(): import("ste-events").IEvent<MCWorld, WorldChunk>;
/**
* Event fired when world data is externally modified and reloaded.
* Subscribe to this event to update UI when remote changes occur.
*/
get onWorldDataReloaded(): import("ste-events").IEvent<MCWorld, string>;
/**
* Whether this world is listening to storage events for automatic updates.
*/
get isListeningToStorage(): boolean;
/**
* 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(): void;
/**
* Stop listening to storage events.
*/
stopListeningToStorage(): void;
/**
* Handle a file update from storage.
*/
private _handleStorageFileUpdate;
/**
* Handle a new file being added to storage.
*/
private _handleStorageFileAdded;
/**
* Handle a file being removed from storage.
*/
private _handleStorageFileRemoved;
/**
* 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.
*/
private _handleIncrementalLevelDbUpdate;
/**
* 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.
*/
private _updateChunkFromLevelDb;
/**
* 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: WorldChunk): void;
static ensureMCWorldOnFolder(folder: IFolder, project?: Project, handler?: IEventHandler<MCWorld, MCWorld>): Promise<MCWorld>;
static ensureOnItem(projectItem: ProjectItem): Promise<MCWorld>;
static ensureOnFile(file: IFile, project?: Project, handler?: IEventHandler<MCWorld, MCWorld>): Promise<MCWorld>;
loadAnchorsFromDynamicProperties(): void;
_updateMeta(): void;
private _coalesceRegions;
private _pushError;
save(): Promise<void>;
private saveWorldManifest;
private saveLevelnameTxt;
private saveLevelDat;
getBytes(): Promise<Uint8Array<ArrayBufferLike>>;
syncFolderTo(folder: IFolder): Promise<void>;
saveToFile(): Promise<void>;
ensurePackReferenceSet(packRefSet: IPackageReference): void;
ensurePackReferenceInCollection(packRef: {
uuid: string;
version: number[];
priority?: number;
}, packRefs: IPackRegistration[]): void;
ensurePackReferenceInHistory(packRef: {
uuid: string;
version: number[];
priority?: number;
}, packHistory: IPackHistory, name: string): void;
private _loadFromNbt;
getProperty(id: string): any;
getBaseValue(): any;
setBaseValue(value: any): void;
setProperty(id: string, newVal: any): any;
loadMetaFiles(force?: boolean): Promise<void>;
ensureResourcePacksFromString(packStr: string): void;
ensureBehaviorPacksFromString(packStr: string): void;
ensureBehaviorPack(packId: string, version: number[], packName: string, packPriority?: number): boolean;
getBehaviorPack(packId: string): IPackRegistration;
getBehaviorPackHistory(packId: string): import("./IPackHistoryItem").default;
static sortPackRegByPriority(a: IPackRegistration, b: IPackRegistration): number;
static sortPackCollectionByPriority(packRefs: IPackRegistration[]): IPackRegistration[];
static freezePackRegistrationOrder(packRefs: IPackRegistration[]): void;
saveWorldBehaviorPacks(): Promise<void>;
static freezeAndStripPriorities(coll: IPackRegistration[]): IPackRegistration[];
saveWorldBehaviorPackHistory(): Promise<void>;
ensureResourcePack(packId: string, version: number[], packName: string, packPriority?: number): boolean;
getResourcePack(packId: string): IPackRegistration;
getResourcePackHistory(packId: string): import("./IPackHistoryItem").default;
saveWorldResourcePacks(): Promise<void>;
saveWorldResourcePackHistory(): Promise<void>;
loadFromBytes(content: Uint8Array): Promise<void>;
applyWorldSettings(worldSettings?: IWorldSettings): Promise<void>;
ensureLevelData(): WorldLevelDat;
loadFromFolder(rootFolder: IFolder): Promise<void>;
loadLevelDb(force?: boolean, options?: IWorldProcessingOptions): Promise<boolean>;
loadFromLevelDb(levelDb: LevelDb, options?: IWorldProcessingOptions): Promise<boolean>;
/**
* Builds a minimal world index without creating WorldChunk objects.
* This calculates world bounds and chunk count from key names only.
* Chunks are created on-demand when getChunkAt() or getOrCreateChunk() is called.
*
* This dramatically reduces memory usage for large worlds (100k+ chunks).
*
* IMPORTANT: Key filtering must use the same approach as processWorldData():
* explicit named-key prefix checks + keyname.length checks. Do NOT filter by
* checking if the first byte of keyBytes is in printable ASCII range, because
* chunk coordinate keys are binary little-endian integers whose low byte can
* legitimately be any value 0-255 (e.g., chunk X=32 → first byte 0x20 = space).
*/
private buildMinimalWorldIndex;
/**
* Gets or creates a chunk at the specified coordinates.
* If the chunk doesn't exist, creates it and populates it from LevelDB keys.
* This is used for on-demand chunk loading when skipFullProcessing is enabled.
*/
getOrCreateChunk(dim: number, x: number, z: number): WorldChunk | undefined;
/**
* Iterates over all chunks in a memory-efficient manner, calling the processor function
* for each chunk and optionally clearing chunk data after processing.
*
* @param processor - Async function to process each chunk. Receives the chunk and its coordinates.
* @param options - Optional configuration for iteration behavior.
* @param options.clearCacheAfterProcess - If true, clears parsed/cached data after processing but preserves
* raw LevelKeyValue data, allowing chunks to be re-parsed on demand.
* This is the recommended option for memory optimization.
* @param options.clearAllAfterProcess - If true, aggressively clears ALL chunk data including raw bytes.
* WARNING: Chunks cannot be re-parsed after this. Only use when
* the world data will never be accessed again.
* @param options.dimensionFilter - If specified, only iterate chunks in this dimension (0=overworld, 1=nether, 2=end).
* @param options.progressCallback - Optional callback for progress updates during iteration.
*/
forEachChunk(processor: (chunk: WorldChunk, x: number, z: number, dimension: number) => Promise<void>, options?: {
clearCacheAfterProcess?: boolean;
clearAllAfterProcess?: boolean;
dimensionFilter?: number;
progressCallback?: (processed: number, total: number) => Promise<void>;
}): Promise<void>;
/**
* Clears parsed/cached data from all chunks to free memory while preserving the ability
* to re-parse chunks on demand. This is the recommended approach for memory optimization
* when you may need to access chunk data again (e.g., for map rendering).
*/
clearAllChunkCaches(): void;
/**
* Clears the raw LevelDB data to free memory after world data has been processed.
* This can significantly reduce memory usage for large worlds.
* WARNING: After calling this, the world cannot be re-loaded from the LevelDb.
*/
clearLevelDbData(): void;
/**
* Clears all world data to free memory.
* Use this when the world is no longer needed.
* WARNING: The world cannot be used after calling this without reloading.
*/
clearAllData(): void;
/**
* Get statistics about memory usage for this world.
* Useful for debugging memory issues with large worlds.
*/
getMemoryStats(): {
chunkCount: number;
levelDbKeyCount: number;
isLazyMode: boolean;
chunkCacheSize?: number;
chunkCacheMaxSize?: number;
};
/**
* Clears all chunk data to free memory.
* WARNING: After calling this, chunk data cannot be accessed without reloading.
*/
clearAllChunkData(): void;
getTopBlockY(x: number, z: number, dim?: number): number;
getTopBlock(x: number, z: number, dim?: number): Block;
spawnEntity(entityTypeId: string, location: BlockLocation): Entity;
getBlock(blockLocation: BlockLocation, dim?: number): Block;
private processWorldData;
private notifyLoadEnded;
private saveAutoGenItems;
private getAutoGenScript;
getCube(from: BlockLocation, to: BlockLocation, dim?: number): BlockVolume;
getSubChunkCube(x: number, y: number, z: number, dim?: number): BlockVolume;
}