UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

479 lines (478 loc) 21.2 kB
/** * 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; }