77    protected const FINALISATION_NEEDS_INSTATICKING = 0;
 
   78    protected const FINALISATION_NEEDS_POPULATION = 1;
 
   79    protected const FINALISATION_DONE = 2;
 
   81    protected const ENTRY_FLAT_WORLD_LAYERS = 
"game_flatworldlayers";
 
   83    protected const CURRENT_LEVEL_CHUNK_VERSION = WorldDataVersions::CHUNK;
 
   84    protected const CURRENT_LEVEL_SUBCHUNK_VERSION = WorldDataVersions::SUBCHUNK;
 
   86    private const CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET = 4;
 
   88    protected \LevelDB $db;
 
   90    private static function checkForLevelDBExtension() : 
void{
 
   91        if(!extension_loaded(
'leveldb')){
 
   95        if(!defined(
'LEVELDB_ZLIB_RAW_COMPRESSION')){
 
  103    private static function createDB(
string $path) : \
LevelDB{
 
  104        return new \LevelDB(Path::join($path, 
"db"), [
 
  105            "compression" => LEVELDB_ZLIB_RAW_COMPRESSION,
 
  106            "block_size" => 64 * 1024 
 
  110    public function __construct(
string $path, \
Logger $logger){
 
  111        self::checkForLevelDBExtension();
 
  112        parent::__construct($path, $logger);
 
  115            $this->db = self::createDB($path);
 
  116        }
catch(\LevelDBException $e){
 
  134    public static function isValid(
string $path) : bool{
 
  135        return file_exists(Path::join($path, 
"level.dat")) && is_dir(Path::join($path, 
"db"));
 
  138    public static function generate(
string $path, 
string $name, 
WorldCreationOptions $options) : void{
 
  139        self::checkForLevelDBExtension();
 
  141        $dbPath = Path::join($path, 
"db");
 
  142        if(!file_exists($dbPath)){
 
  143            mkdir($dbPath, 0777, 
true);
 
  146        BedrockWorldData::generate($path, $name, $options);
 
  153        $bitsPerBlock = $stream->getByte() >> 1;
 
  156            $words = $stream->
get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
 
  157        }
catch(\InvalidArgumentException $e){
 
  160        $nbt = 
new LittleEndianNbtSerializer();
 
  163        if($bitsPerBlock === 0){
 
  176            $offset = $stream->getOffset();
 
  178            $stream->setOffset($offset); 
 
  180            if($byte1 !== NBT::TAG_Compound){ 
 
  181                $susLength = $stream->
getLInt();
 
  182                if($susLength !== 1){ 
 
  185                $logger->
error(
"Unexpected palette size for 0 bpb palette");
 
  188            $paletteSize = $stream->
getLInt();
 
  191        $blockDecodeErrors = [];
 
  193        for($i = 0; $i < $paletteSize; ++$i){
 
  195                $offset = $stream->getOffset();
 
  196                $blockStateNbt = $nbt->read($stream->getBuffer(), $offset)->mustGetCompoundTag();
 
  197                $stream->setOffset($offset);
 
  198            }
catch(NbtDataException $e){
 
  200                throw new CorruptedChunkException(
"Invalid blockstate NBT at offset $i in paletted storage: " . $e->getMessage(), 0, $e);
 
  205                $blockStateData = $this->blockDataUpgrader->upgradeBlockStateNbt($blockStateNbt);
 
  206            }
catch(BlockStateDeserializeException $e){
 
  208                $errorMessage = 
"Upgrade error: " . $e->getMessage() . 
", NBT: " . $blockStateNbt->toString();
 
  209                $blockDecodeErrors[$errorMessage][] = $i;
 
  210                $palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
 
  214                $palette[] = $this->blockStateDeserializer->deserialize($blockStateData);
 
  215            }
catch(UnsupportedBlockStateException $e){
 
  216                $blockDecodeErrors[$e->getMessage()][] = $i;
 
  217                $palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
 
  218            }
catch(BlockStateDeserializeException $e){
 
  219                $errorMessage = 
"Deserialize error: " . $e->getMessage() . 
", NBT: " . $blockStateNbt->toString();
 
  220                $blockDecodeErrors[$errorMessage][] = $i;
 
  221                $palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
 
  225        if(count($blockDecodeErrors) > 0){
 
  227            foreach(Utils::promoteKeys($blockDecodeErrors) as $errorMessage => $paletteOffsets){
 
  228                $finalErrors[] = 
"$errorMessage (palette offsets: " . implode(
", ", $paletteOffsets) . 
")";
 
  230            $logger->
error(
"Errors decoding blocks:\n - " . implode(
"\n - ", $finalErrors));
 
  234        return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
 
 
  237    private function serializeBlockPalette(BinaryStream $stream, PalettedBlockArray $blocks) : void{
 
  238        $stream->putByte($blocks->getBitsPerBlock() << 1);
 
  239        $stream->put($blocks->getWordArray());
 
  241        $palette = $blocks->getPalette();
 
  242        if($blocks->getBitsPerBlock() !== 0){
 
  243            $stream->putLInt(count($palette));
 
  246        foreach($palette as $p){
 
  247            $tags[] = new TreeRoot($this->blockStateSerializer->serialize($p)->toNbt());
 
  250        $stream->put((
new LittleEndianNbtSerializer())->writeMultiple($tags));
 
  256    private static function getExpected3dBiomesCount(
int $chunkVersion) : int{
 
  258            $chunkVersion >= ChunkVersion::v1_18_30 => 24,
 
  259            $chunkVersion >= ChunkVersion::v1_18_0_25_beta => 25,
 
  260            $chunkVersion >= ChunkVersion::v1_18_0_24_beta => 32,
 
  261            $chunkVersion >= ChunkVersion::v1_18_0_22_beta => 65,
 
  262            $chunkVersion >= ChunkVersion::v1_17_40_20_beta_experimental_caves_cliffs => 32,
 
  263            default => 
throw new CorruptedChunkException(
"Chunk version $chunkVersion should not have 3D biomes")
 
  270    private static function deserializeBiomePalette(BinaryStream $stream, 
int $bitsPerBlock) : PalettedBlockArray{
 
  272            $words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
 
  273        }
catch(\InvalidArgumentException $e){
 
  274            throw new CorruptedChunkException(
"Failed to deserialize paletted biomes: " . $e->getMessage(), 0, $e);
 
  277        $paletteSize = $bitsPerBlock === 0 ? 1 : $stream->getLInt();
 
  279        for($i = 0; $i < $paletteSize; ++$i){
 
  280            $palette[] = $stream->getLInt();
 
  284        return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
 
  287    private static function serializeBiomePalette(BinaryStream $stream, PalettedBlockArray $biomes) : void{
 
  288        $stream->putByte($biomes->getBitsPerBlock() << 1);
 
  289        $stream->put($biomes->getWordArray());
 
  291        $palette = $biomes->getPalette();
 
  292        if($biomes->getBitsPerBlock() !== 0){
 
  293            $stream->putLInt(count($palette));
 
  295        foreach($palette as $p){
 
  296            $stream->putLInt($p);
 
  305    private static function deserialize3dBiomes(BinaryStream $stream, 
int $chunkVersion, \
Logger $logger) : array{
 
  308        $nextIndex = Chunk::MIN_SUBCHUNK_INDEX;
 
  310        $expectedCount = self::getExpected3dBiomesCount($chunkVersion);
 
  311        for($i = 0; $i < $expectedCount; ++$i){
 
  313                $bitsPerBlock = $stream->getByte() >> 1;
 
  314                if($bitsPerBlock === 127){
 
  315                    if($previous === 
null){
 
  316                        throw new CorruptedChunkException(
"Serialized biome palette $i has no previous palette to copy from");
 
  318                    $decoded = clone $previous;
 
  320                    $decoded = self::deserializeBiomePalette($stream, $bitsPerBlock);
 
  322                $previous = $decoded;
 
  323                if($nextIndex <= Chunk::MAX_SUBCHUNK_INDEX){ 
 
  324                    $result[$nextIndex++] = $decoded;
 
  325                }elseif($stream->feof()){
 
  327                    $logger->
error(
"Wrong number of 3D biome palettes for this chunk version: expected $expectedCount, but got " . ($i + 1) . 
" - this is not a problem, but may indicate a corrupted chunk");
 
  330            }
catch(BinaryDataException $e){
 
  331                throw new CorruptedChunkException(
"Failed to deserialize biome palette $i: " . $e->getMessage(), 0, $e);
 
  334        if(!$stream->feof()){
 
  336            $logger->
error(
"Unexpected trailing data after 3D biomes data");
 
  345    private static function serialize3dBiomes(BinaryStream $stream, array $subChunks) : void{
 
  347        for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; $y++){
 
  350            self::serializeBiomePalette($stream, $subChunks[$y]->getBiomeArray());
 
  361            $x = ($key >> 12) & 0xf;
 
  362            $z = ($key >> 8) & 0xf;
 
  365            $x = ($key >> 11) & 0xf;
 
  366            $z = ($key >> 7) & 0xf;
 
 
  375        if(($extraRawData = $this->db->get($index . 
ChunkDataKey::LEGACY_BLOCK_EXTRA_DATA)) === false || $extraRawData === 
""){
 
  380        $extraDataLayers = [];
 
  382        $count = $binaryStream->getLInt();
 
  384        for($i = 0; $i < $count; ++$i){
 
  385            $key = $binaryStream->getLInt();
 
  386            $value = $binaryStream->getLShort();
 
  388            self::deserializeExtraDataKey($chunkVersion, $key, $x, $fullY, $z);
 
  390            $ySub = ($fullY >> SubChunk::COORD_BIT_SIZE);
 
  391            $y = $key & SubChunk::COORD_MASK;
 
  393            $blockId = $value & 0xff;
 
  394            $blockData = ($value >> 8) & 0xf;
 
  396                $blockStateData = $this->blockDataUpgrader->upgradeIntIdMeta($blockId, $blockData);
 
  400                $logger->
error(
"Failed to upgrade legacy extra block: " . $e->getMessage() . 
" ($blockId:$blockData)");
 
  404            $blockStateId = $this->blockStateDeserializer->deserialize($blockStateData);
 
  406            if(!isset($extraDataLayers[$ySub])){
 
  407                $extraDataLayers[$ySub] = 
new PalettedBlockArray(Block::EMPTY_STATE_ID);
 
  409            $extraDataLayers[$ySub]->set($x, $y, $z, $blockStateId);
 
  412        return $extraDataLayers;
 
 
  415    private function readVersion(
int $chunkX, 
int $chunkZ) : ?int{
 
  416        $index = self::chunkIndex($chunkX, $chunkZ);
 
  417        $chunkVersionRaw = $this->db->get($index . ChunkDataKey::NEW_VERSION);
 
  418        if($chunkVersionRaw === 
false){
 
  419            $chunkVersionRaw = $this->db->get($index . ChunkDataKey::OLD_VERSION);
 
  420            if($chunkVersionRaw === 
false){
 
  425        return ord($chunkVersionRaw);
 
  435    private function deserializeLegacyTerrainData(
string $index, 
int $chunkVersion, \
Logger $logger) : array{
 
  436        $convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion, $logger);
 
  438        $legacyTerrain = $this->db->get($index . ChunkDataKey::LEGACY_TERRAIN);
 
  439        if($legacyTerrain === 
false){
 
  440            throw new CorruptedChunkException(
"Missing expected LEGACY_TERRAIN tag for format version $chunkVersion");
 
  442        $binaryStream = 
new BinaryStream($legacyTerrain);
 
  444            $fullIds = $binaryStream->get(32768);
 
  445            $fullData = $binaryStream->get(16384);
 
  446            $binaryStream->get(32768); 
 
  447        }
catch(BinaryDataException $e){
 
  448            throw new CorruptedChunkException($e->getMessage(), 0, $e);
 
  452            $binaryStream->get(256); 
 
  454            $unpackedBiomeArray = unpack(
"N*", $binaryStream->get(1024)); 
 
  455            $biomes3d = ChunkUtils::extrapolate3DBiomes(ChunkUtils::convertBiomeColors(array_values($unpackedBiomeArray))); 
 
  456        }
catch(BinaryDataException $e){
 
  457            throw new CorruptedChunkException($e->getMessage(), 0, $e);
 
  459        if(!$binaryStream->feof()){
 
  460            $logger->
error(
"Unexpected trailing data in legacy terrain data");
 
  464        for($yy = 0; $yy < 8; ++$yy){
 
  465            $storages = [$this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $yy, 
new \
PrefixedLogger($logger, 
"Subchunk y=$yy"))];
 
  466            if(isset($convertedLegacyExtraData[$yy])){
 
  467                $storages[] = $convertedLegacyExtraData[$yy];
 
  469            $subChunks[$yy] = 
new SubChunk(Block::EMPTY_STATE_ID, $storages, clone $biomes3d);
 
  473        for($yy = Chunk::MIN_SUBCHUNK_INDEX; $yy <= Chunk::MAX_SUBCHUNK_INDEX; ++$yy){
 
  474            if(!isset($subChunks[$yy])){
 
  475                $subChunks[$yy] = 
new SubChunk(Block::EMPTY_STATE_ID, [], clone $biomes3d);
 
  485    private function deserializeNonPalettedSubChunkData(BinaryStream $binaryStream, 
int $chunkVersion, ?PalettedBlockArray $convertedLegacyExtraData, PalettedBlockArray $biomePalette, \
Logger $logger) : SubChunk{
 
  487            $blocks = $binaryStream->get(4096);
 
  488            $blockData = $binaryStream->get(2048);
 
  489        }
catch(BinaryDataException $e){
 
  490            throw new CorruptedChunkException($e->getMessage(), 0, $e);
 
  493        if($chunkVersion < ChunkVersion::v1_1_0){
 
  495                $binaryStream->get(4096); 
 
  496                if(!$binaryStream->feof()){
 
  497                    $logger->
error(
"Unexpected trailing data in legacy subchunk data");
 
  499            }
catch(BinaryDataException $e){
 
  500                $logger->
error(
"Failed to read legacy subchunk light info: " . $e->getMessage());
 
  504        $storages = [$this->palettizeLegacySubChunkXZY($blocks, $blockData, $logger)];
 
  505        if($convertedLegacyExtraData !== 
null){
 
  506            $storages[] = $convertedLegacyExtraData;
 
  509        return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
 
  518    private function deserializeSubChunkData(BinaryStream $binaryStream, 
int $chunkVersion, 
int $subChunkVersion, ?PalettedBlockArray $convertedLegacyExtraData, PalettedBlockArray $biomePalette, \
Logger $logger) : SubChunk{
 
  519        switch($subChunkVersion){
 
  520            case SubChunkVersion::CLASSIC:
 
  521            case SubChunkVersion::CLASSIC_BUG_2: 
 
  522            case SubChunkVersion::CLASSIC_BUG_3:
 
  523            case SubChunkVersion::CLASSIC_BUG_4:
 
  524            case SubChunkVersion::CLASSIC_BUG_5:
 
  525            case SubChunkVersion::CLASSIC_BUG_6:
 
  526            case SubChunkVersion::CLASSIC_BUG_7:
 
  527                return $this->deserializeNonPalettedSubChunkData($binaryStream, $chunkVersion, $convertedLegacyExtraData, $biomePalette, $logger);
 
  528            case SubChunkVersion::PALETTED_SINGLE:
 
  529                $storages = [$this->deserializeBlockPalette($binaryStream, $logger)];
 
  530                if($convertedLegacyExtraData !== 
null){
 
  531                    $storages[] = $convertedLegacyExtraData;
 
  533                return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
 
  534            case SubChunkVersion::PALETTED_MULTI:
 
  535            case SubChunkVersion::PALETTED_MULTI_WITH_OFFSET:
 
  538                $storageCount = $binaryStream->getByte();
 
  539                if($subChunkVersion >= SubChunkVersion::PALETTED_MULTI_WITH_OFFSET){
 
  541                    $binaryStream->getByte();
 
  545                for($k = 0; $k < $storageCount; ++$k){
 
  546                    $storages[] = $this->deserializeBlockPalette($binaryStream, $logger);
 
  548                return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
 
  551                throw new CorruptedChunkException(
"don't know how to decode LevelDB subchunk format version $subChunkVersion");
 
  555    private static function hasOffsetCavesAndCliffsSubChunks(
int $chunkVersion) : bool{
 
  556        return $chunkVersion >= ChunkVersion::v1_16_220_50_unused && $chunkVersion <= ChunkVersion::v1_16_230_50_unused;
 
  572    private function deserializeAllSubChunkData(
string $index, 
int $chunkVersion, 
bool &$hasBeenUpgraded, array $convertedLegacyExtraData, array $biomeArrays, \
Logger $logger) : array{
 
  575        $subChunkKeyOffset = self::hasOffsetCavesAndCliffsSubChunks($chunkVersion) ? self::CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET : 0;
 
  576        for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){
 
  577            if(($data = $this->db->get($index . ChunkDataKey::SUBCHUNK . chr($y + $subChunkKeyOffset))) === 
false){
 
  578                $subChunks[$y] = 
new SubChunk(Block::EMPTY_STATE_ID, [], $biomeArrays[$y]);
 
  582            $binaryStream = 
new BinaryStream($data);
 
  583            if($binaryStream->feof()){
 
  584                throw new CorruptedChunkException(
"Unexpected empty data for subchunk $y");
 
  586            $subChunkVersion = $binaryStream->getByte();
 
  587            if($subChunkVersion < self::CURRENT_LEVEL_SUBCHUNK_VERSION){
 
  588                $hasBeenUpgraded = 
true;
 
  591            $subChunks[$y] = $this->deserializeSubChunkData(
 
  595                $convertedLegacyExtraData[$y] ?? 
null,
 
  610    private function deserializeBiomeData(
string $index, 
int $chunkVersion, \
Logger $logger) : array{
 
  612        if(($maps2d = $this->db->get($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES)) !== 
false){
 
  613            $binaryStream = 
new BinaryStream($maps2d);
 
  616                $binaryStream->get(512); 
 
  617                $biomes3d = ChunkUtils::extrapolate3DBiomes($binaryStream->get(256)); 
 
  618                if(!$binaryStream->feof()){
 
  619                    $logger->
error(
"Unexpected trailing data after 2D biome data");
 
  621            }
catch(BinaryDataException $e){
 
  622                throw new CorruptedChunkException($e->getMessage(), 0, $e);
 
  624            for($i = Chunk::MIN_SUBCHUNK_INDEX; $i <= Chunk::MAX_SUBCHUNK_INDEX; ++$i){
 
  625                $biomeArrays[$i] = clone $biomes3d;
 
  627        }elseif(($maps3d = $this->db->get($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES)) !== 
false){
 
  628            $binaryStream = 
new BinaryStream($maps3d);
 
  631                $binaryStream->get(512);
 
  632                $biomeArrays = self::deserialize3dBiomes($binaryStream, $chunkVersion, $logger);
 
  633            }
catch(BinaryDataException $e){
 
  634                throw new CorruptedChunkException($e->getMessage(), 0, $e);
 
  637            $logger->
error(
"Missing biome data, using default ocean biome");
 
  638            for($i = Chunk::MIN_SUBCHUNK_INDEX; $i <= Chunk::MAX_SUBCHUNK_INDEX; ++$i){
 
  639                $biomeArrays[$i] = 
new PalettedBlockArray(BiomeIds::OCEAN); 
 
  650        $index = 
LevelDB::chunkIndex($chunkX, $chunkZ);
 
  652        $chunkVersion = $this->readVersion($chunkX, $chunkZ);
 
  653        if($chunkVersion === 
null){
 
  660        $logger = new \PrefixedLogger($this->logger, 
"Loading chunk x=$chunkX z=$chunkZ v$chunkVersion");
 
  662        $hasBeenUpgraded = $chunkVersion < self::CURRENT_LEVEL_CHUNK_VERSION;
 
  664        switch($chunkVersion){
 
  665            case ChunkVersion::v1_21_40:
 
  667            case ChunkVersion::v1_18_30:
 
  668            case ChunkVersion::v1_18_0_25_beta:
 
  669            case ChunkVersion::v1_18_0_24_unused:
 
  670            case ChunkVersion::v1_18_0_24_beta:
 
  671            case ChunkVersion::v1_18_0_22_unused:
 
  672            case ChunkVersion::v1_18_0_22_beta:
 
  673            case ChunkVersion::v1_18_0_20_unused:
 
  674            case ChunkVersion::v1_18_0_20_beta:
 
  675            case ChunkVersion::v1_17_40_unused:
 
  676            case ChunkVersion::v1_17_40_20_beta_experimental_caves_cliffs:
 
  677            case ChunkVersion::v1_17_30_25_unused:
 
  678            case ChunkVersion::v1_17_30_25_beta_experimental_caves_cliffs:
 
  679            case ChunkVersion::v1_17_30_23_unused:
 
  680            case ChunkVersion::v1_17_30_23_beta_experimental_caves_cliffs:
 
  681            case ChunkVersion::v1_16_230_50_unused:
 
  682            case ChunkVersion::v1_16_230_50_beta_experimental_caves_cliffs:
 
  683            case ChunkVersion::v1_16_220_50_unused:
 
  684            case ChunkVersion::v1_16_220_50_beta_experimental_caves_cliffs:
 
  685            case ChunkVersion::v1_16_210:
 
  686            case ChunkVersion::v1_16_100_57_beta:
 
  687            case ChunkVersion::v1_16_100_52_beta:
 
  688            case ChunkVersion::v1_16_0:
 
  689            case ChunkVersion::v1_16_0_51_beta:
 
  691            case ChunkVersion::v1_12_0_unused2:
 
  692            case ChunkVersion::v1_12_0_unused1:
 
  693            case ChunkVersion::v1_12_0_4_beta:
 
  694            case ChunkVersion::v1_11_1:
 
  695            case ChunkVersion::v1_11_0_4_beta:
 
  696            case ChunkVersion::v1_11_0_3_beta:
 
  697            case ChunkVersion::v1_11_0_1_beta:
 
  698            case ChunkVersion::v1_9_0:
 
  699            case ChunkVersion::v1_8_0:
 
  700            case ChunkVersion::v1_2_13:
 
  701            case ChunkVersion::v1_2_0:
 
  702            case ChunkVersion::v1_2_0_2_beta:
 
  703            case ChunkVersion::v1_1_0_converted_from_console:
 
  704            case ChunkVersion::v1_1_0:
 
  706            case ChunkVersion::v1_0_0:
 
  707                $convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion, $logger);
 
  708                $biomeArrays = $this->deserializeBiomeData($index, $chunkVersion, $logger);
 
  709                $subChunks = $this->deserializeAllSubChunkData($index, $chunkVersion, $hasBeenUpgraded, $convertedLegacyExtraData, $biomeArrays, $logger);
 
  711            case ChunkVersion::v0_9_5:
 
  712            case ChunkVersion::v0_9_2:
 
  713            case ChunkVersion::v0_9_0:
 
  714                $subChunks = $this->deserializeLegacyTerrainData($index, $chunkVersion, $logger);
 
  720        $nbt = 
new LittleEndianNbtSerializer();
 
  723        if(($entityData = $this->db->get($index . ChunkDataKey::ENTITIES)) !== 
false && $entityData !== 
""){
 
  725                $entities = array_map(fn(TreeRoot $root) => $root->mustGetCompoundTag(), $nbt->readMultiple($entityData));
 
  726            }
catch(NbtDataException $e){
 
  732        if(($tileData = $this->db->get($index . ChunkDataKey::BLOCK_ENTITIES)) !== 
false && $tileData !== 
""){
 
  734                $tiles = array_map(fn(TreeRoot $root) => $root->mustGetCompoundTag(), $nbt->readMultiple($tileData));
 
  735            }
catch(NbtDataException $e){
 
  736                throw new CorruptedChunkException($e->getMessage(), 0, $e);
 
  740        $finalisationChr = $this->db->get($index . ChunkDataKey::FINALIZATION);
 
  741        if($finalisationChr !== 
false){
 
  742            $finalisation = ord($finalisationChr);
 
  743            $terrainPopulated = $finalisation === self::FINALISATION_DONE;
 
  745            $terrainPopulated = 
true;
 
  750        return new LoadedChunkData(
 
  751            data: 
new ChunkData($subChunks, $terrainPopulated, $entities, $tiles),
 
  752            upgraded: $hasBeenUpgraded,
 
  753            fixerFlags: LoadedChunkData::FIXER_FLAG_ALL 
 
 
  758        $index = 
LevelDB::chunkIndex($chunkX, $chunkZ);
 
  760        $write = new \LevelDBWriteBatch();
 
  762        $write->put($index . ChunkDataKey::NEW_VERSION, chr(self::CURRENT_LEVEL_CHUNK_VERSION));
 
  763        $write->put($index . ChunkDataKey::PM_DATA_VERSION, Binary::writeLLong(VersionInfo::WORLD_DATA_VERSION));
 
  767        if(($dirtyFlags & Chunk::DIRTY_FLAG_BLOCKS) !== 0){
 
  769            foreach($subChunks as $y => $subChunk){
 
  770                $key = $index . ChunkDataKey::SUBCHUNK . chr($y);
 
  771                if($subChunk->isEmptyAuthoritative()){
 
  772                    $write->delete($key);
 
  775                    $subStream->putByte(self::CURRENT_LEVEL_SUBCHUNK_VERSION);
 
  777                    $layers = $subChunk->getBlockLayers();
 
  778                    $subStream->putByte(count($layers));
 
  779                    foreach($layers as $blocks){
 
  780                        $this->serializeBlockPalette($subStream, $blocks);
 
  783                    $write->put($key, $subStream->getBuffer());
 
  788        if(($dirtyFlags & Chunk::DIRTY_FLAG_BIOMES) !== 0){
 
  789            $write->delete($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES);
 
  790            $stream = 
new BinaryStream();
 
  791            $stream->put(str_repeat(
"\x00", 512)); 
 
  792            self::serialize3dBiomes($stream, $subChunks);
 
  793            $write->put($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES, $stream->getBuffer());
 
  797        $write->put($index . ChunkDataKey::FINALIZATION, chr($chunkData->isPopulated() ? self::FINALISATION_DONE : self::FINALISATION_NEEDS_POPULATION));
 
  799        $this->writeTags($chunkData->
getTileNBT(), $index . ChunkDataKey::BLOCK_ENTITIES, $write);
 
  800        $this->writeTags($chunkData->
getEntityNBT(), $index . ChunkDataKey::ENTITIES, $write);
 
  802        $write->delete($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOME_COLORS);
 
  803        $write->delete($index . ChunkDataKey::LEGACY_TERRAIN);
 
  805        $this->db->write($write);
 
 
  811    private function writeTags(array $targets, 
string $index, \LevelDBWriteBatch $write) : void{
 
  812        if(count($targets) > 0){
 
  813            $nbt = 
new LittleEndianNbtSerializer();
 
  814            $write->put($index, $nbt->writeMultiple(array_map(fn(CompoundTag $tag) => 
new TreeRoot($tag), $targets)));
 
  816            $write->delete($index);
 
  820    public function getDatabase() : \LevelDB{
 
  824    public static function chunkIndex(
int $chunkX, 
int $chunkZ) : string{
 
  825        return Binary::writeLInt($chunkX) . Binary::writeLInt($chunkZ);
 
  832    public function close() : void{
 
 
  837        foreach($this->db->getIterator() as $key => $_){
 
  838            if(strlen($key) === 9 && ($key[8] === ChunkDataKey::NEW_VERSION || $key[8] === ChunkDataKey::OLD_VERSION)){
 
  839                $chunkX = Binary::readLInt(substr($key, 0, 4));
 
  840                $chunkZ = Binary::readLInt(substr($key, 4, 4));
 
  842                    if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== 
null){
 
  843                        yield [$chunkX, $chunkZ] => $chunk;
 
  849                    if($logger !== 
null){
 
  850                        $logger->
error(
"Skipped corrupted chunk $chunkX $chunkZ (" . $e->getMessage() . 
")");
 
 
  859        foreach($this->db->getIterator() as $key => $_){
 
  860            if(strlen($key) === 9 && ($key[8] === ChunkDataKey::NEW_VERSION || $key[8] === ChunkDataKey::OLD_VERSION)){