@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
1,018 lines (1,017 loc) • 61 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 });
exports.SubChunkFormatType = void 0;
const Log_1 = __importDefault(require("../core/Log"));
const BlockPalette_1 = __importDefault(require("./BlockPalette"));
const DataUtilities_1 = __importDefault(require("../core/DataUtilities"));
const BlockVolume_1 = __importDefault(require("./BlockVolume"));
const Block_1 = __importDefault(require("./Block"));
const NbtBinary_1 = __importDefault(require("./NbtBinary"));
const GenericBlockActor_1 = __importDefault(require("./blockActors/GenericBlockActor"));
const BlockActorFactory_1 = __importDefault(require("./blockActors/BlockActorFactory"));
const Database_1 = __importDefault(require("./Database"));
const ChunkEntity_1 = __importDefault(require("./ChunkEntity"));
const CHUNK_X_SIZE = 16;
const CHUNK_Z_SIZE = 16;
const SUBCHUNK_Y_SIZE = 16;
const MAX_LEGACY_Y = 128;
// Subchunk index constants for extended height worlds (Caves & Cliffs, 1.18+)
// Stored subchunk indices use unsigned bytes: 0-63 for positive, 224-255 for negative (-32 to -1)
const MAX_POSITIVE_SUBCHUNK_STORED = 63; // Maximum positive subchunk index stored in LevelDB key
const MIN_NEGATIVE_SUBCHUNK_STORED = 224; // Minimum negative subchunk index (stored as unsigned byte)
const MAX_NEGATIVE_SUBCHUNK_STORED = 255; // Maximum negative subchunk index (stored as unsigned byte)
const NEGATIVE_SUBCHUNK_OFFSET = 224; // Offset to convert stored negative index to array index (224 → 0)
const POSITIVE_SUBCHUNK_OFFSET = 32; // Offset to convert stored positive index to array index (0 → 32)
const TOTAL_SUBCHUNK_SLOTS = 96; // Total subchunk array slots: 32 negative + 64 positive
var SubChunkFormatType;
(function (SubChunkFormatType) {
SubChunkFormatType[SubChunkFormatType["paletteFrom1dot2dot13"] = 0] = "paletteFrom1dot2dot13";
SubChunkFormatType[SubChunkFormatType["subChunk1dot0"] = 1] = "subChunk1dot0";
})(SubChunkFormatType || (exports.SubChunkFormatType = SubChunkFormatType = {}));
class WorldChunk {
checksumKey;
subChunks;
subChunkVersions;
chunkVersion;
biomesAndElevation;
finalizedState;
entity;
blockActorKeys = [];
_hasContent = undefined;
_blockActorsRelLoc = [];
_blockActors = [];
_entities = [];
_entitiesEnsured = false;
pendingTicks;
biomeState;
blockTops;
data3dRecord;
blockActorsEnsured = false;
absoluteZeroY = -512;
chunkMinY = 0;
legacyVersion;
world;
legacyTerrainBytes;
bitsPerBlock;
blockDataStart;
blockPalettes;
subChunkFormatType;
actorDigests;
auxBitsPerBlock;
auxBlockDataStart;
auxBlockPalettes;
pendingSubChunksToProcess;
x = 0;
z = 0;
maxSubChunkIndex = -512; // set it very low
minSubChunkIndex = 512; // set it very high
get absoluteMinY() {
this._checkNotCleared();
return this.absoluteZeroY;
}
get minY() {
this._checkNotCleared();
return this.absoluteZeroY + this.minSubChunkIndex * 16;
}
get maxY() {
this._checkNotCleared();
return this.absoluteZeroY + (this.maxSubChunkIndex + 1) * 16;
}
get absoluteMaxY() {
return this.absoluteZeroY + this.subChunks.length * 16;
}
get blockActors() {
this._checkNotCleared();
if (!this.blockActorsEnsured) {
this.ensureBlockActors();
}
return this._blockActors;
}
/**
* Gets the entities in this chunk.
* Entities are parsed lazily from the entity LevelKeyValue on first access.
*/
get entities() {
this._checkNotCleared();
if (!this._entitiesEnsured) {
this.ensureEntities();
}
return this._entities;
}
constructor(world, inX, inZ) {
this.world = world;
this.subChunks = [];
this.subChunkFormatType = [];
this.blockPalettes = [];
this.bitsPerBlock = [];
this.blockDataStart = [];
this.auxBlockPalettes = [];
this.auxBitsPerBlock = [];
this.auxBlockDataStart = [];
this.pendingSubChunksToProcess = [];
this.actorDigests = [];
for (let i = 0; i < TOTAL_SUBCHUNK_SLOTS; i++) {
this.pendingSubChunksToProcess[i] = false;
}
this.x = inX;
this.z = inZ;
}
_checkNotCleared() {
Log_1.default.assert(this._hasContent !== false, "Chunk data has been cleared - cannot access data in " + this.world.name);
}
/**
* Clears cached/parsed data to free memory while preserving the ability to re-parse later.
* Use this after processing a chunk to reduce memory usage.
* The raw LevelKeyValue data is preserved, allowing getBlock() to re-parse on demand.
*/
clearCachedData() {
for (let i = 0; i < TOTAL_SUBCHUNK_SLOTS; i++) {
if (this.subChunks[i] !== undefined) {
this.blockDataStart[i] = -1;
this.bitsPerBlock[i] = -1;
this.blockPalettes[i] = undefined;
this.pendingSubChunksToProcess[i] = true;
}
}
this.blockTops = undefined;
this.blockActorsEnsured = false;
this._blockActorsRelLoc = [];
this._blockActors = [];
this._entitiesEnsured = false;
this._entities = [];
// Note: _hasContent remains true since raw LevelKeyValue data is preserved
// and can be re-parsed on demand via pendingSubChunksToProcess
}
/**
* Aggressively clears all chunk data including raw LevelKeyValue bytes.
* Call this when the chunk data is no longer needed and will not be re-accessed.
* WARNING: After calling this, the chunk cannot be re-parsed from its data.
*/
clearAllData() {
this.clearCachedData();
// Mark as having no content since raw data is being cleared
this._hasContent = false;
// Clear subchunk LevelKeyValue data
for (let i = 0; i < this.subChunks.length; i++) {
if (this.subChunks[i] !== undefined) {
this.subChunks[i].clearValueData();
}
}
// Clear block actor key data
for (const blockActorKey of this.blockActorKeys) {
blockActorKey.clearValueData();
}
// Clear other LevelKeyValue data
if (this.entity) {
this.entity.clearValueData();
}
if (this.biomesAndElevation) {
this.biomesAndElevation.clearValueData();
}
if (this.pendingTicks) {
this.pendingTicks.clearValueData();
}
if (this.biomeState) {
this.biomeState.clearValueData();
}
if (this.chunkVersion) {
this.chunkVersion.clearValueData();
}
if (this.finalizedState) {
this.finalizedState.clearValueData();
}
if (this.checksumKey) {
this.checksumKey.clearValueData();
}
// Clear legacy terrain bytes
this.legacyTerrainBytes = undefined;
}
addActorDigest(digest) {
this.actorDigests.push(digest);
}
translateSubChunkIndex(storageSubChunk) {
// Valid range: 0-63 (positive subchunks) or 224-255 (negative subchunks stored as unsigned byte)
// Output range: 0-31 for negative subchunks (224-255), 32-95 for positive subchunks (0-63)
Log_1.default.assert((storageSubChunk >= 0 && storageSubChunk <= MAX_POSITIVE_SUBCHUNK_STORED) ||
(storageSubChunk >= MIN_NEGATIVE_SUBCHUNK_STORED && storageSubChunk <= MAX_NEGATIVE_SUBCHUNK_STORED), "Unexpected subchunk index (" + storageSubChunk + ")");
if (storageSubChunk >= MIN_NEGATIVE_SUBCHUNK_STORED) {
return storageSubChunk - NEGATIVE_SUBCHUNK_OFFSET; // 224→0, 255→31 (represents Y subchunks -32 to -1)
}
return storageSubChunk + POSITIVE_SUBCHUNK_OFFSET; // 0→32, 63→95 (represents Y subchunks 0 to 63)
}
processSubChunk(index) {
if (this.pendingSubChunksToProcess[index] === true) {
this.pendingSubChunksToProcess[index] = false;
this.parseSubChunk(index);
}
}
addKeyValue(keyValue) {
this._hasContent = true;
let keyBytes = keyValue.keyBytes;
let wasSuperceded = false; // Track if this update supercedes existing data
let isSignificantUpdate = false; // Track if this is a significant data addition (subchunk, blockTops, etc.)
if (keyBytes) {
const dimExtensionBytes = keyBytes.length >= 13 ? 4 : 0;
const val = keyBytes[8 + dimExtensionBytes];
// disabling the "duplicate unexpected versions" since this assumption is violated in C&C R17 world
switch (val) {
case 43:
Log_1.default.assert(keyValue.value !== undefined && keyValue.value.length > 512, "Unexpected length for a type 43 record.");
// Data3D contains a heightmap with 256 int16 values encoding block top heights.
// Previously this code set blockTops from these values using an offset (val - 65),
// but the offset was not verified and described as "arbitrary". In practice, when
// data3d arrives AFTER subchunk data (tag 47) in the LevelDB log — which is the
// typical write order — it would overwrite the cleared blockTops, preventing
// determineBlockTops() from computing heights from actual parsed subchunk data.
// This caused incorrect heights and missing blocks in the world map.
//
// We now store the raw data3d record for reference but do NOT set blockTops,
// so getTopBlockY() always calls determineBlockTops() which computes correct
// heights from the actual subchunk block palette data.
if (keyValue.value && keyValue.value.length >= 512) {
this.data3dRecord = keyValue;
}
break;
case 115: // not sure what chunk #115 is, or if this is a parsing bug. observed to be one byte with a value of 0
// Log.assert(false, "Unexpected type 115 record.");
break;
case 118: // 118 = legacy version
Log_1.default.assert(keyValue.value !== undefined && (keyValue.value.length === 0 || keyValue.value.length === 1), "Unexpected type 118 record.");
if (keyValue.value && keyValue.value.length === 1) {
if (this.legacyVersion !== undefined) {
wasSuperceded = true;
}
this.legacyVersion = keyValue.value[0];
}
else if (keyValue.value && keyValue.value.length === 0) {
this.legacyVersion = undefined;
}
break;
case 44: // version
if (this.chunkVersion !== undefined) {
wasSuperceded = true;
}
this.chunkVersion = keyValue;
break;
case 45: // data2d
if (this.biomesAndElevation !== undefined) {
wasSuperceded = true;
}
this.biomesAndElevation = keyValue;
break;
case 46: // data2d legacy
// Log.assert(false, "Data 2D legacy (NYI).");
break;
case 47: // subchunk prefix
let subChunkIndex = this.translateSubChunkIndex(keyBytes[9 + dimExtensionBytes]);
if (subChunkIndex < 0) {
Log_1.default.fail("Unexpected sub chunk index.");
return;
}
if (this.subChunks[subChunkIndex] !== undefined) {
// This subchunk is being superceded by newer data
wasSuperceded = true;
// Clear cached parsed data for this subchunk
this.blockDataStart[subChunkIndex] = -1;
this.bitsPerBlock[subChunkIndex] = -1;
this.blockPalettes[subChunkIndex] = undefined;
}
// Always clear blockTops when adding/updating subchunk data
// so it gets recalculated with the new data
this.blockTops = undefined;
if (!keyValue.value || keyValue.value.length <= 0) {
Log_1.default.assert(this.subChunks[subChunkIndex] === undefined, "Empty subchunk defined.");
return;
}
this.subChunks[subChunkIndex] = keyValue;
this.maxSubChunkIndex = Math.max(this.maxSubChunkIndex, subChunkIndex);
this.minSubChunkIndex = Math.min(this.minSubChunkIndex, subChunkIndex);
this.pendingSubChunksToProcess[subChunkIndex] = true;
isSignificantUpdate = true; // Subchunk data affects block rendering
break;
case 48: // legacy terrain
const bytes = keyValue.value;
if (bytes && bytes.length > 0) {
Log_1.default.assert(bytes.length === 83200, "LegacyTerrain record should be 83,200 bytes");
if (this.legacyTerrainBytes !== undefined) {
wasSuperceded = true;
}
this.legacyTerrainBytes = bytes;
isSignificantUpdate = true; // Legacy terrain data affects block rendering
}
break;
case 49: // block entity
this.blockActorKeys.push(keyValue);
this.blockActorsEnsured = false;
break;
case 50: // entity
// Log.assert(!this.entity, "Unexpected multiple entities.");
if (this.entity !== undefined) {
wasSuperceded = true;
}
this.entity = keyValue;
break;
case 51: // pending ticks
//Log.assert(!this.pendingTicks, "Unexpected multiple pending ticks.");
this.pendingTicks = keyValue;
break;
case 52: // legacy block extra data
// Log.assert(false, "Legacy block extra data - NYI");
break;
case 53: // biome state
//Log.assert(!this.biomeState, "Unexpected multiple biome states.");
if (this.biomeState !== undefined) {
wasSuperceded = true;
}
this.biomeState = keyValue;
break;
case 54: // finalized state
// Log.assert(!this.finalizedState, "Unexpected multiple states.");
this.finalizedState = keyValue;
break;
case 55: // conversion data. data that the converter provides, that are used at runtime for things like blending. no longer used?
break;
case 56: // EDU border blocks?
break;
case 57: // spawn areas (hard coded spawners)
break;
case 58: // random tick
break;
case 59: // check sums
// Log.assert(!this.checksumKey, "Unexpected multiple states.");
this.checksumKey = keyValue;
break;
case 60: // generation seed
break;
case 61: // generated pre caves and cliffs blending (unused)
break;
case 62: // blending biome height (unused)
break;
case 63: // metadata hash
break;
case 64: // blending data
break;
case 65: // actor digest version
break;
case 119: // ??
//Log.assert(false, "Unexpected type 119 data.");
break;
default:
Log_1.default.debugAlert("Unsupported chunk type: " + val);
}
}
// If data was superceded or significant new data was added, notify the world so map tiles can be refreshed
// This handles both: (1) old data being replaced by new, and (2) new data arriving for chunks that
// may have already been rendered as empty/air
// Only notify if the world has finished initial loading - otherwise we'd spam notifications
if (wasSuperceded || isSignificantUpdate) {
this.world.notifyChunkUpdated(this);
}
}
clearKeyValue(keyBytes) {
if (keyBytes) {
const dimExtensionBytes = keyBytes.length > 18 || keyBytes.length === 13 || keyBytes.length === 14 ? 4 : 0;
const val = keyBytes.charCodeAt(8 + dimExtensionBytes);
// disabling the "duplicate unexpected versions" since this assumption is violated in C&C R17 world
switch (val) {
case 43: // not sure what chunk #43 is, or if this is a parsing bug. observed to be 578 bytes. "data3d"
break;
case 115: // not sure what chunk #61 is, or if this is a parsing bug. observed to be one byte with a value of 0
break;
case 118: // 118 = legacy version
case 44: // version
// Log.assert(!this.chunkVersion, "Unexpected multiple chunk versions.");
this.chunkVersion = undefined;
break;
case 45: // data2d
// Log.assert(!this.biomesAndElevation, "Unexpected multiple biomes and elevations.");
this.biomesAndElevation = undefined;
break;
case 46: // data2d legacy
break;
case 47: // subchunk prefix
break;
case 48: // legacy terrain
this.legacyTerrainBytes = undefined;
break;
case 49: // block entity
break;
case 50: // entity
// Log.assert(!this.entity, "Unexpected multiple entities.");
this.entity = undefined;
break;
case 51: // pending ticks
//Log.assert(!this.pendingTicks, "Unexpected multiple pending ticks.");
this.pendingTicks = undefined;
break;
case 52: // legacy block extra data
break;
case 53: // biome state
//Log.assert(!this.biomeState, "Unexpected multiple biome states.");
this.biomeState = undefined;
break;
case 54: // finalized state
// Log.assert(!this.finalizedState, "Unexpected multiple states.");
this.finalizedState = undefined;
break;
case 55: // conversion data. data that the converter provides, that are used at runtime for things like blending. no longer used?
break;
case 56: // EDU border blocks?
break;
case 57: // spawn areas (hard coded spawners)
break;
case 58: // random tick
break;
case 59: // check sums
// Log.assert(!this.checksumKey, "Unexpected multiple states.");
this.checksumKey = undefined;
break;
case 60: // generation seed
break;
case 61: // generated pre caves and cliffs blending (unused)
break;
case 62: // blending biome height (unused)
break;
case 63: // metadata hash
break;
case 64: // blending data
break;
case 65: // actor digest version
break;
case 72: // actor digest version
break;
case 119: // ??
break;
default:
throw new Error("Unsupported chunk type: " + val);
}
}
}
ensureBlockActors() {
if (!this._blockActorsRelLoc || this.blockActorsEnsured) {
return;
}
this._checkNotCleared();
this._blockActorsRelLoc = [];
this._blockActors = [];
for (const keyValue of this.blockActorKeys) {
if (keyValue.value && keyValue.value.length > 0) {
const tag = new NbtBinary_1.default();
tag.context = this.world.name + " chunk at x:" + this.x * 16 + " z:" + this.z * 16;
try {
tag.fromBinary(keyValue.value, true, false, 0, true, true);
}
catch (e) {
Log_1.default.error("Could not parse a block actor.");
}
if (tag.roots) {
for (let i = 0; i < tag.roots.length; i++) {
const ba = new GenericBlockActor_1.default(tag.roots[i]);
if (ba.x !== undefined &&
ba.z !== undefined &&
ba.y !== undefined &&
ba.x >= this.x * 16 &&
ba.x < (this.x + 1) * 16 &&
ba.z >= this.z * 16 &&
ba.z < (this.z + 1) * 16) {
let actorRelX = ba.x % 16;
let actorRelZ = ba.z % 16;
if (actorRelX < 0) {
actorRelX = 16 + actorRelX;
}
if (actorRelZ < 0) {
actorRelZ = 16 + actorRelZ;
}
if (ba.id) {
const specificBa = BlockActorFactory_1.default.create(ba.id, tag.roots[i]);
if (specificBa) {
specificBa.load();
if (!this._blockActorsRelLoc[actorRelX]) {
this._blockActorsRelLoc[actorRelX] = [];
}
if (!this._blockActorsRelLoc[actorRelX][ba.y]) {
this._blockActorsRelLoc[actorRelX][ba.y] = [];
}
this._blockActorsRelLoc[actorRelX][ba.y][actorRelZ] = specificBa;
if (specificBa.x !== undefined && specificBa.y !== undefined && specificBa.z !== undefined) {
// this.removeBlockActorAtLoc(specificBa.x, specificBa.y, specificBa.z);
}
this._blockActors.push(specificBa);
}
Log_1.default.assert(specificBa !== undefined, "Could not find an actor implementation for '" + ba.id + "'");
}
}
}
}
}
}
this.blockActorsEnsured = true;
}
/**
* Parses entity data from the chunk's entity LevelKeyValue.
* This handles legacy entity storage (pre-1.18.30) where entity data
* is stored as concatenated NBT compound tags per chunk (type 50).
*
* Modern worlds (1.18.30+) store actors individually via actorprefix keys,
* which are parsed into MCWorld.actorsById instead.
*
* See: https://learn.microsoft.com/en-us/minecraft/creator/documents/actorstorage
*/
ensureEntities() {
if (this._entitiesEnsured) {
return;
}
this._checkNotCleared();
this._entities = [];
this._entitiesEnsured = true;
if (!this.entity || !this.entity.value || this.entity.value.length === 0) {
return;
}
try {
const entityNbt = new NbtBinary_1.default();
entityNbt.context = this.world.name + " entities at chunk x:" + this.x + " z:" + this.z;
entityNbt.fromBinary(this.entity.value, true, false, 0, true, true);
if (entityNbt.roots) {
for (const root of entityNbt.roots) {
// Each root is an entity compound tag - parse it into a ChunkEntity
const entity = ChunkEntity_1.default.fromNbtTag(root);
if (entity) {
this._entities.push(entity);
}
}
if (this._entities.length > 0) {
Log_1.default.verbose("Parsed " + this._entities.length + " legacy entities in chunk x:" + this.x + " z:" + this.z);
}
}
}
catch (e) {
Log_1.default.error("Could not parse legacy entities for chunk x:" + this.x + " z:" + this.z + " - " + e);
}
}
removeBlockActorAtLoc(x, y, z) {
this._checkNotCleared();
const newBlockActors = [];
for (const blockActor of this._blockActors) {
if (blockActor.x !== x || blockActor.y !== y || blockActor.z !== z) {
newBlockActors.push(blockActor);
}
}
this._blockActors = newBlockActors;
}
getSubChunkCube(subChunkId) {
this._checkNotCleared();
const bc = new BlockVolume_1.default();
bc.maxX = CHUNK_X_SIZE;
bc.maxY = SUBCHUNK_Y_SIZE;
bc.maxZ = CHUNK_Z_SIZE;
this.fillCube(bc, 0, 0, 0, 16, 16, 16, 0, subChunkId * SUBCHUNK_Y_SIZE, 0);
return bc;
}
fillCubeLegacy(cube, cubeX, cubeY, cubeZ, maxCubeX, maxCubeY, maxCubeZ, internalOffsetX, internalOffsetY, internalOffsetZ) {
Log_1.default.assert(cubeX >= 0 &&
cubeY >= 0 &&
cubeZ >= 0 &&
maxCubeX > cubeX &&
maxCubeY > cubeY &&
maxCubeZ > cubeZ &&
internalOffsetX < CHUNK_X_SIZE &&
internalOffsetZ < CHUNK_Z_SIZE &&
this.legacyTerrainBytes !== undefined, "Fill cube legacy not within bounds.");
if (!this.legacyTerrainBytes) {
return;
}
for (let iX = cubeX; iX < maxCubeX && iX - cubeX + internalOffsetX < CHUNK_X_SIZE; iX++) {
const inChunkX = iX - cubeX + internalOffsetX;
const plane = cube.x(iX);
for (let iY = cubeY; iY < maxCubeY && iY - cubeY + internalOffsetY < 128; iY++) {
const blockLine = plane.y(iY);
const inChunkY = iY - cubeY + internalOffsetY;
for (let iZ = cubeZ; iZ < maxCubeZ && iZ - cubeZ + internalOffsetZ < CHUNK_Z_SIZE; iZ++) {
const inChunkZ = iZ - cubeZ + internalOffsetZ;
const byte = this.legacyTerrainBytes[inChunkX * 128 * 16 + inChunkZ * 128 + inChunkY];
if (byte) {
blockLine.z(iZ).copyFrom(Block_1.default.fromLegacyId(byte));
}
}
}
}
}
fillCube(cube, cubeX, cubeY, cubeZ, maxCubeX, maxCubeY, maxCubeZ, internalOffsetX, internalOffsetY, internalOffsetZ) {
if (this.legacyTerrainBytes) {
this.fillCubeLegacy(cube, cubeX, cubeY, cubeZ, maxCubeX, maxCubeY, maxCubeZ, internalOffsetX, internalOffsetY, internalOffsetZ);
return;
}
Log_1.default.assert(cubeX >= 0 &&
cubeY >= 0 &&
cubeZ >= 0 &&
maxCubeX > cubeX &&
maxCubeY > cubeY &&
maxCubeZ > cubeZ &&
internalOffsetX < CHUNK_X_SIZE &&
internalOffsetZ < CHUNK_Z_SIZE, "Fill cube not within bounds.");
const zHeight = maxCubeY - cubeY;
const initialChunkId = this.getSubChunkIndexFromY(internalOffsetY);
const finalChunkId = this.getSubChunkIndexFromY(internalOffsetY + zHeight);
Log_1.default.assert(initialChunkId >= 0 && finalChunkId >= initialChunkId, "WCFC");
for (let i = 0; i <= finalChunkId - initialChunkId; i++) {
const subChunkId = initialChunkId + i;
const subChunk = this.subChunks[subChunkId];
if (subChunk) {
const subChunkYExtent = this.getStartYFromSubChunkIndex(subChunkId + 1);
let cubeYStartForThisSubChunk = 0;
if (i >= 1) {
cubeYStartForThisSubChunk = 16 - ((Math.abs(this.absoluteZeroY) + internalOffsetY) % 16);
}
if (i >= 2) {
cubeYStartForThisSubChunk += (i - 1) * 16;
}
if (this.subChunkFormatType[subChunkId] === SubChunkFormatType.subChunk1dot0) {
const blockTemplates = [];
const bytes = subChunk.value;
if (bytes) {
Log_1.default.assert(bytes.length === 10251 || bytes.length === 10241 || bytes.length === 6145, "Expected 6145 or 10241 bytes for a legacy subchunk. (" + bytes.length + ")");
for (let i = 0; i < 4096; i++) {
let blockTypeIndex = bytes[1 + i];
let blockAuxIndex = bytes[4097 + i];
let templateIndex = blockTypeIndex * 256 + blockAuxIndex;
if (!blockTemplates[templateIndex]) {
const blockType = Database_1.default.getBlockTypeByLegacyId(blockTypeIndex);
if (!blockType || !blockType.id) {
throw new Error("Expected a block type for index " + blockTypeIndex);
}
const block = new Block_1.default("minecraft:" + blockType.id);
block.data = blockAuxIndex;
blockTemplates[templateIndex] = block;
}
cube
.x(i % 16)
.y(Math.floor(i / 256))
.z(Math.floor(i / 16))
.copyFrom(blockTemplates[templateIndex]);
}
}
}
else {
const subChunkBitsPerBlock = this.bitsPerBlock[subChunkId];
const bpw = Math.floor(32 / subChunkBitsPerBlock);
const subChunkBlockDataStart = this.blockDataStart[subChunkId];
const bytes = subChunk.value;
const blockPalette = this.blockPalettes[subChunkId];
if (bytes && blockPalette) {
for (let iX = cubeX; iX < maxCubeX && iX - cubeX + internalOffsetX < CHUNK_X_SIZE; iX++) {
const inChunkX = iX - cubeX + internalOffsetX;
const plane = cube.x(iX);
const blockIndexXStart = inChunkX * 256;
for (let iY = cubeY + cubeYStartForThisSubChunk; iY < maxCubeY && iY - cubeY + internalOffsetY < subChunkYExtent; iY++) {
const inSubChunkY = (Math.abs(this.absoluteZeroY) + (iY - cubeY + internalOffsetY)) % 16;
Log_1.default.assert(inSubChunkY >= 0, "WCFCA");
const blockLine = plane.y(iY);
for (let iZ = cubeZ; iZ < maxCubeZ && iZ - cubeZ + internalOffsetZ < CHUNK_Z_SIZE; iZ++) {
const inChunkZ = iZ - cubeZ + internalOffsetZ;
const blockWordByteStart = subChunkBlockDataStart + Math.floor((blockIndexXStart + inChunkZ * 16 + inSubChunkY) / bpw) * 4;
const blocksIn = (blockIndexXStart + inChunkZ * 16 + inSubChunkY) % bpw;
let word = DataUtilities_1.default.getUnsignedInteger(bytes[blockWordByteStart], bytes[blockWordByteStart + 1], bytes[blockWordByteStart + 2], bytes[blockWordByteStart + 3], true);
word >>>= subChunkBitsPerBlock * blocksIn;
let value = 0;
for (let i = 0; i < subChunkBitsPerBlock; i++) {
let inc = word % 2;
inc <<= i;
value += inc;
word >>>= 1;
}
if (blockPalette.blocks.length > 0) {
Log_1.default.assert(value < blockPalette.blocks.length, "Unexpected block index.");
const block = blockPalette.blocks[value];
if (block) {
blockLine.z(iZ).copyFrom(block);
}
}
}
}
}
}
}
}
}
}
getTopBlockY(x, z) {
if (!this.blockTops) {
this.determineBlockTops();
}
if (!this.blockTops) {
throw new Error("Unexpected block top error.");
}
return this.blockTops[x][z];
}
getTopBlock(x, z) {
if (!this.blockTops) {
this.determineBlockTops();
}
this._checkNotCleared();
if (!this.blockTops) {
throw new Error("Unexpected block top error.");
}
return this.getBlock(x, this.blockTops[x][z], z);
}
_getBlockLegacy(x, y, z) {
if (!this.legacyTerrainBytes || z < 0 || x < 0 || y < 0) {
throw new Error();
}
this._checkNotCleared();
const byte = this.legacyTerrainBytes[x * 128 * 16 + z * 128 + y];
return Block_1.default.fromLegacyId(byte);
}
_getBlockLegacyList() {
if (!this.legacyTerrainBytes) {
throw new Error();
}
this._checkNotCleared();
const blocks = [];
for (let y = 0; y < MAX_LEGACY_Y; y++) {
for (let z = 0; z < 16; z++) {
for (let x = 0; x < 16; x++) {
const byte = this.legacyTerrainBytes[x * 128 * 16 + z * 128 + y];
blocks.push(Block_1.default.fromLegacyId(byte));
}
}
}
return blocks;
}
doesBlockPaletteExist(y) {
if (this.legacyTerrainBytes) {
return true;
}
this._checkNotCleared();
const subChunkId = this.getSubChunkIndexFromY(y);
const blockPalettes = this.blockPalettes[subChunkId];
if (blockPalettes) {
return true;
}
return false;
}
// x and z should be between 0 and 15
getBlock(x, y, z) {
if (y < this.absoluteZeroY) {
return undefined;
}
this._checkNotCleared();
Log_1.default.assert(x >= 0 && x < 16 && z >= 0 && z < 16, "Retrieving an out-of-range block from a chunk.");
if (this.legacyTerrainBytes) {
return this._getBlockLegacy(x, y, z);
}
const subChunkId = this.getSubChunkIndexFromY(y);
if (this.pendingSubChunksToProcess[subChunkId] === true) {
this.processSubChunk(subChunkId);
}
// legacy subchunk format 1.0 -> 1.2.13
if (this.subChunkFormatType[subChunkId] === SubChunkFormatType.subChunk1dot0) {
const subChunk = this.subChunks[subChunkId];
if (subChunk === undefined) {
return undefined;
}
const bytes = subChunk.value;
if (bytes) {
const inSubChunkY = y - this.getStartYFromSubChunkIndex(subChunkId);
Log_1.default.assert(inSubChunkY >= 0 && inSubChunkY < 16, "Unexpected Y for a sub chunk (" + inSubChunkY + ")");
Log_1.default.assert(bytes.length === 10251 || bytes.length === 10241 || bytes.length === 6145, "1.00 subchunk format should be 6145 or 10241 bytes. (" + bytes.length + ")");
const blockTypeIndex = bytes[1 + (inSubChunkY + z * 16 + x * 256)];
const blockAuxIndex = bytes[4097 + (inSubChunkY + z * 16 + x * 256)];
const baseType = Database_1.default.getBlockTypeByLegacyId(blockTypeIndex);
Log_1.default.assertDefined(baseType.id);
// Use air for unknown block types to gracefully handle missing legacy IDs
const blockId = baseType?.id ? "minecraft:" + baseType.id : "minecraft:air";
const block = new Block_1.default(blockId);
block.data = blockAuxIndex;
return block;
}
return undefined;
}
const index = this.getBlockPaletteIndex(x, y, z);
if (index === undefined) {
return undefined;
}
const blockPalettes = this.blockPalettes[subChunkId];
if (!blockPalettes) {
return undefined;
}
const blocks = blockPalettes.blocks;
Log_1.default.assert(index < blocks.length, "Unexpected block index");
return blocks[index];
}
/**
* Returns a count of block types in this chunk without allocating Block objects.
* This is much more memory-efficient than getBlockList() for statistical analysis.
* @returns A Map of block type names to their counts in this chunk
*/
countBlockTypes() {
const typeCounts = new Map();
if (this.legacyTerrainBytes) {
// Legacy terrain - count block type IDs directly
for (let i = 0; i < this.legacyTerrainBytes.length && i < 16 * 16 * MAX_LEGACY_Y; i++) {
const byte = this.legacyTerrainBytes[i];
if (byte !== 0) {
const blockType = Database_1.default.getBlockTypeByLegacyId(byte);
if (blockType && blockType.id) {
const typeName = "minecraft:" + blockType.id;
typeCounts.set(typeName, (typeCounts.get(typeName) || 0) + 1);
}
}
}
return typeCounts;
}
for (let subChunkId = this.minSubChunkIndex; subChunkId <= this.maxSubChunkIndex; subChunkId++) {
const subChunk = this.subChunks[subChunkId];
if (subChunk !== undefined) {
if (this.pendingSubChunksToProcess[subChunkId] === true) {
this.processSubChunk(subChunkId);
}
if (this.subChunkFormatType[subChunkId] === SubChunkFormatType.subChunk1dot0) {
// Legacy subchunk format - count from raw bytes
const bytes = subChunk.value;
if (bytes && bytes.length >= 4097) {
for (let i = 0; i < 4096; i++) {
const blockTypeIndex = bytes[1 + i];
if (blockTypeIndex !== 0) {
const blockType = Database_1.default.getBlockTypeByLegacyId(blockTypeIndex);
if (blockType && blockType.id) {
const typeName = "minecraft:" + blockType.id;
typeCounts.set(typeName, (typeCounts.get(typeName) || 0) + 1);
}
}
}
}
}
else {
// Modern palette-based format - count palette entries efficiently
const blockPalette = this.blockPalettes[subChunkId];
if (blockPalette && blockPalette.blocks.length > 0) {
// Count how many times each palette index appears
const paletteCounts = new Map();
const bytes = subChunk.value;
if (bytes) {
const subChunkBitsPerBlock = this.bitsPerBlock[subChunkId];
const bpw = Math.floor(32 / subChunkBitsPerBlock);
const blockDataStart = this.blockDataStart[subChunkId];
// Iterate through all 4096 blocks in this subchunk
for (let blockIndex = 0; blockIndex < 4096; blockIndex++) {
const byteStart = blockDataStart + Math.floor(blockIndex / bpw) * 4;
const blocksIn = blockIndex % bpw;
let word = DataUtilities_1.default.getUnsignedInteger(bytes[byteStart], bytes[byteStart + 1], bytes[byteStart + 2], bytes[byteStart + 3], true);
word >>>= subChunkBitsPerBlock * blocksIn;
let paletteIndex = 0;
for (let i = 0; i < subChunkBitsPerBlock; i++) {
let inc = word % 2;
inc <<= i;
paletteIndex += inc;
word >>>= 1;
}
paletteCounts.set(paletteIndex, (paletteCounts.get(paletteIndex) || 0) + 1);
}
// Convert palette counts to type counts
for (const [paletteIndex, count] of paletteCounts) {
if (paletteIndex < blockPalette.blocks.length) {
const block = blockPalette.blocks[paletteIndex];
if (block && block.typeName) {
typeCounts.set(block.typeName, (typeCounts.get(block.typeName) || 0) + count);
}
}
}
}
}
}
}
}
return typeCounts;
}
getBlockList() {
if (this.legacyTerrainBytes) {
return this._getBlockLegacyList();
}
const blocks = [];
for (let subChunkId = this.minSubChunkIndex; subChunkId <= this.maxSubChunkIndex; subChunkId++) {
const subChunk = this.subChunks[subChunkId];
if (subChunk !== undefined) {
if (this.pendingSubChunksToProcess[subChunkId] === true) {
this.processSubChunk(subChunkId);
}
if (this.subChunkFormatType[subChunkId] === SubChunkFormatType.subChunk1dot0) {
const blockTemplates = [];
const bytes = subChunk.value;
if (bytes) {
Log_1.default.assert(bytes.length === 10251 || bytes.length === 10241 || bytes.length === 6145 || bytes.length === 6155, "Expected 6145 or 10241 bytes for a legacy subchunk in getblock. (" + bytes.length + ")");
// 6145 bytes if the light information is omitted;
// 10241 bytes if there is 2kb + 2kb of light information
for (let i = 0; i < 4096; i++) {
let blockTypeIndex = bytes[1 + i];
let blockAuxIndex = bytes[4097 + i];
let templateIndex = blockTypeIndex * 256 + blockAuxIndex;
if (!blockTemplates[templateIndex]) {
const blockType = Database_1.default.getBlockTypeByLegacyId(blockTypeIndex);
if (!blockType || !blockType.id) {
throw new Error("Expected a block type for index " + blockTypeIndex);
}
// Use air for unknown block types to gracefully handle missing legacy IDs
const blockId = blockType?.id ? "minecraft:" + blockType.id : "minecraft:air";
const block = new Block_1.default(blockId);
block.data = blockAuxIndex;
blockTemplates[templateIndex] = block;
}
blocks.push(blockTemplates[templateIndex]);
}
}
}
else {
let indices = this.getBlockPaletteIndexList(subChunkId);
if (indices) {
const blockPalettes = this.blockPalettes[subChunkId];
if (blockPalettes) {
const blockTemplates = blockPalettes.blocks;
for (let i = 0; i < indices.length; i++) {
blocks.push(blockTemplates[indices[i]]);
}
}
}
}
}
}
return blocks;
}
_determineBlockTopsLegacy() {
if (!this.legacyTerrainBytes || !this.blockTops) {
return;
}
for (let iX = 0; iX < 16; iX++) {
const iXByte = iX * 128 * 16;
for (let iZ = 0; iZ < 16; iZ++) {
const iZByte = iZ * 128;
for (let iY = 127; iY >= 0; iY--) {
const byte = this.legacyTerrainBytes[iXByte + iZByte + iY];
if (byte !== 0) {
this.blockTops[iX][iZ] = iY;
iY = -1;
}
}
}
}
}
determineBlockTops() {
this.blockTops = [];
for (let i = 0; i < 16; i++) {
const arr = [];
for (let j = 0; j < 16; j++) {
arr.push(-32768);
}
this.blockTops.push(arr);
}
if (this.legacyTerrainBytes) {
this._determineBlockTopsLegacy();
return;
}
let matchCount = 0;
for (let subChunkId = this.maxSubChunkIndex; subChunkId >= 0; subChunkId--) {
if (this.pendingSubChunksToProcess[subChunkId] === true) {
this.parseSubChunk(subChunkId);
}
const subChunk = this.subChunks[subChunkId];
if (subChunk !== undefined) {
const bytes = subChunk.value;
if (bytes !== undefined) {
if (this.subChunkFormatType[subChunkId] === SubChunkFormatType.subChunk1dot0) {
for (let iY = 15; iY >= 0; iY--) {
for (let iZ = 0; iZ < 16; iZ++) {
for (let iX = 0; iX < 16; iX++) {
let blockTypeId = bytes[iX * 256 + iZ * CHUNK_Z_SIZE + iY];
if (blockTypeId !== 0 /* air */ &&
blockTypeId !== 37 /* flower */ &&
blockTypeId !== 31 /* tallgrass*/ &&
this.blockTops[iX][iZ] < -1024) {
const yIndex = iY + this.getStartYFromSubChunkIndex(subChunkId);
this.blockTops[iX][iZ] = yIndex - 1;
matchCount++;
if (matchCount === 256) {
return;
}
}
}
}
}
}
else {
//y z x
const subChunkBitsPerBlock = this.bitsPerBlock[subChunkId];
const bpw = Math.floor(32 / subChunkBitsPerBlock);
const disallowedIndices = [];
const blockPals = this.blockPalettes[subChunkId];
if (blockPals) {
Log_1.default.assert(blockPals.blocks !== undefined, "WCDBTA");
for (let iPal = 0; iPal < blockPals.blocks.length; iPal++) {
const block = blockPals.blocks[iPal];
if (block.shortTypeId === "air" ||
block.shortTypeId === "flower" ||
block.shortTypeId === "tallgrass" ||
block.shortTypeId === "barrier" ||