75 protected const FINALISATION_NEEDS_INSTATICKING = 0;
76 protected const FINALISATION_NEEDS_POPULATION = 1;
77 protected const FINALISATION_DONE = 2;
79 protected const ENTRY_FLAT_WORLD_LAYERS =
"game_flatworldlayers";
81 protected const CURRENT_LEVEL_CHUNK_VERSION = ChunkVersion::v1_21_40;
84 private const CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET = 4;
86 protected \LevelDB $db;
88 private static function checkForLevelDBExtension() :
void{
89 if(!extension_loaded(
'leveldb')){
93 if(!defined(
'LEVELDB_ZLIB_RAW_COMPRESSION')){
101 private static function createDB(
string $path) : \
LevelDB{
102 return new \LevelDB(Path::join($path,
"db"), [
103 "compression" => LEVELDB_ZLIB_RAW_COMPRESSION,
104 "block_size" => 64 * 1024
108 public function __construct(
string $path, \
Logger $logger){
109 self::checkForLevelDBExtension();
110 parent::__construct($path, $logger);
113 $this->db = self::createDB($path);
114 }
catch(\LevelDBException $e){
132 public static function isValid(
string $path) : bool{
133 return file_exists(Path::join($path,
"level.dat")) && is_dir(Path::join($path,
"db"));
136 public static function generate(
string $path,
string $name,
WorldCreationOptions $options) : void{
137 self::checkForLevelDBExtension();
139 $dbPath = Path::join($path,
"db");
140 if(!file_exists($dbPath)){
141 mkdir($dbPath, 0777,
true);
144 BedrockWorldData::generate($path, $name, $options);
151 $bitsPerBlock = $stream->getByte() >> 1;
154 $words = $stream->
get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
155 }
catch(\InvalidArgumentException $e){
158 $nbt =
new LittleEndianNbtSerializer();
161 if($bitsPerBlock === 0){
174 $offset = $stream->getOffset();
176 $stream->setOffset($offset);
178 if($byte1 !== NBT::TAG_Compound){
179 $susLength = $stream->
getLInt();
180 if($susLength !== 1){
183 $logger->
error(
"Unexpected palette size for 0 bpb palette");
186 $paletteSize = $stream->
getLInt();
189 $blockDecodeErrors = [];
191 for($i = 0; $i < $paletteSize; ++$i){
193 $offset = $stream->getOffset();
194 $blockStateNbt = $nbt->read($stream->getBuffer(), $offset)->mustGetCompoundTag();
195 $stream->setOffset($offset);
196 }
catch(NbtDataException $e){
198 throw new CorruptedChunkException(
"Invalid blockstate NBT at offset $i in paletted storage: " . $e->getMessage(), 0, $e);
203 $blockStateData = $this->blockDataUpgrader->upgradeBlockStateNbt($blockStateNbt);
204 }
catch(BlockStateDeserializeException $e){
206 $blockDecodeErrors[] =
"Palette offset $i / Upgrade error: " . $e->getMessage() .
", NBT: " . $blockStateNbt->toString();
207 $palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
211 $palette[] = $this->blockStateDeserializer->deserialize($blockStateData);
212 }
catch(UnsupportedBlockStateException $e){
213 $blockDecodeErrors[] =
"Palette offset $i / " . $e->getMessage();
214 $palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
215 }
catch(BlockStateDeserializeException $e){
216 $blockDecodeErrors[] =
"Palette offset $i / Deserialize error: " . $e->getMessage() .
", NBT: " . $blockStateNbt->toString();
217 $palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
221 if(count($blockDecodeErrors) > 0){
222 $logger->
error(
"Errors decoding blocks:\n - " . implode(
"\n - ", $blockDecodeErrors));
226 return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
229 private function serializeBlockPalette(BinaryStream $stream, PalettedBlockArray $blocks) : void{
230 $stream->putByte($blocks->getBitsPerBlock() << 1);
231 $stream->put($blocks->getWordArray());
233 $palette = $blocks->getPalette();
234 if($blocks->getBitsPerBlock() !== 0){
235 $stream->putLInt(count($palette));
238 foreach($palette as $p){
239 $tags[] = new TreeRoot($this->blockStateSerializer->serialize($p)->toNbt());
242 $stream->put((
new LittleEndianNbtSerializer())->writeMultiple($tags));
248 private static function getExpected3dBiomesCount(
int $chunkVersion) : int{
250 $chunkVersion >= ChunkVersion::v1_18_30 => 24,
251 $chunkVersion >= ChunkVersion::v1_18_0_25_beta => 25,
252 $chunkVersion >= ChunkVersion::v1_18_0_24_beta => 32,
253 $chunkVersion >= ChunkVersion::v1_18_0_22_beta => 65,
254 $chunkVersion >= ChunkVersion::v1_17_40_20_beta_experimental_caves_cliffs => 32,
255 default =>
throw new CorruptedChunkException(
"Chunk version $chunkVersion should not have 3D biomes")
262 private static function deserializeBiomePalette(BinaryStream $stream,
int $bitsPerBlock) : PalettedBlockArray{
264 $words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
265 }
catch(\InvalidArgumentException $e){
266 throw new CorruptedChunkException(
"Failed to deserialize paletted biomes: " . $e->getMessage(), 0, $e);
269 $paletteSize = $bitsPerBlock === 0 ? 1 : $stream->getLInt();
271 for($i = 0; $i < $paletteSize; ++$i){
272 $palette[] = $stream->getLInt();
276 return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
279 private static function serializeBiomePalette(BinaryStream $stream, PalettedBlockArray $biomes) : void{
280 $stream->putByte($biomes->getBitsPerBlock() << 1);
281 $stream->put($biomes->getWordArray());
283 $palette = $biomes->getPalette();
284 if($biomes->getBitsPerBlock() !== 0){
285 $stream->putLInt(count($palette));
287 foreach($palette as $p){
288 $stream->putLInt($p);
297 private static function deserialize3dBiomes(BinaryStream $stream,
int $chunkVersion, \
Logger $logger) : array{
300 $nextIndex = Chunk::MIN_SUBCHUNK_INDEX;
302 $expectedCount = self::getExpected3dBiomesCount($chunkVersion);
303 for($i = 0; $i < $expectedCount; ++$i){
305 $bitsPerBlock = $stream->getByte() >> 1;
306 if($bitsPerBlock === 127){
307 if($previous ===
null){
308 throw new CorruptedChunkException(
"Serialized biome palette $i has no previous palette to copy from");
310 $decoded = clone $previous;
312 $decoded = self::deserializeBiomePalette($stream, $bitsPerBlock);
314 $previous = $decoded;
315 if($nextIndex <= Chunk::MAX_SUBCHUNK_INDEX){
316 $result[$nextIndex++] = $decoded;
317 }elseif($stream->feof()){
319 $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");
322 }
catch(BinaryDataException $e){
323 throw new CorruptedChunkException(
"Failed to deserialize biome palette $i: " . $e->getMessage(), 0, $e);
326 if(!$stream->feof()){
328 $logger->
error(
"Unexpected trailing data after 3D biomes data");
337 private static function serialize3dBiomes(BinaryStream $stream, array $subChunks) : void{
339 for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; $y++){
342 self::serializeBiomePalette($stream, $subChunks[$y]->getBiomeArray());
353 $x = ($key >> 12) & 0xf;
354 $z = ($key >> 8) & 0xf;
357 $x = ($key >> 11) & 0xf;
358 $z = ($key >> 7) & 0xf;
367 if(($extraRawData = $this->db->get($index .
ChunkDataKey::LEGACY_BLOCK_EXTRA_DATA)) === false || $extraRawData ===
""){
372 $extraDataLayers = [];
374 $count = $binaryStream->getLInt();
376 for($i = 0; $i < $count; ++$i){
377 $key = $binaryStream->getLInt();
378 $value = $binaryStream->getLShort();
380 self::deserializeExtraDataKey($chunkVersion, $key, $x, $fullY, $z);
382 $ySub = ($fullY >> SubChunk::COORD_BIT_SIZE);
383 $y = $key & SubChunk::COORD_MASK;
385 $blockId = $value & 0xff;
386 $blockData = ($value >> 8) & 0xf;
388 $blockStateData = $this->blockDataUpgrader->upgradeIntIdMeta($blockId, $blockData);
392 $logger->
error(
"Failed to upgrade legacy extra block: " . $e->getMessage() .
" ($blockId:$blockData)");
396 $blockStateId = $this->blockStateDeserializer->deserialize($blockStateData);
398 if(!isset($extraDataLayers[$ySub])){
399 $extraDataLayers[$ySub] =
new PalettedBlockArray(Block::EMPTY_STATE_ID);
401 $extraDataLayers[$ySub]->set($x, $y, $z, $blockStateId);
404 return $extraDataLayers;
407 private function readVersion(
int $chunkX,
int $chunkZ) : ?int{
408 $index = self::chunkIndex($chunkX, $chunkZ);
409 $chunkVersionRaw = $this->db->get($index . ChunkDataKey::NEW_VERSION);
410 if($chunkVersionRaw ===
false){
411 $chunkVersionRaw = $this->db->get($index . ChunkDataKey::OLD_VERSION);
412 if($chunkVersionRaw ===
false){
417 return ord($chunkVersionRaw);
427 private function deserializeLegacyTerrainData(
string $index,
int $chunkVersion, \
Logger $logger) : array{
428 $convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion, $logger);
430 $legacyTerrain = $this->db->get($index . ChunkDataKey::LEGACY_TERRAIN);
431 if($legacyTerrain ===
false){
432 throw new CorruptedChunkException(
"Missing expected LEGACY_TERRAIN tag for format version $chunkVersion");
434 $binaryStream =
new BinaryStream($legacyTerrain);
436 $fullIds = $binaryStream->get(32768);
437 $fullData = $binaryStream->get(16384);
438 $binaryStream->get(32768);
439 }
catch(BinaryDataException $e){
440 throw new CorruptedChunkException($e->getMessage(), 0, $e);
444 $binaryStream->get(256);
446 $unpackedBiomeArray = unpack(
"N*", $binaryStream->get(1024));
447 $biomes3d = ChunkUtils::extrapolate3DBiomes(ChunkUtils::convertBiomeColors(array_values($unpackedBiomeArray)));
448 }
catch(BinaryDataException $e){
449 throw new CorruptedChunkException($e->getMessage(), 0, $e);
451 if(!$binaryStream->feof()){
452 $logger->
error(
"Unexpected trailing data in legacy terrain data");
456 for($yy = 0; $yy < 8; ++$yy){
457 $storages = [$this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $yy,
new \
PrefixedLogger($logger,
"Subchunk y=$yy"))];
458 if(isset($convertedLegacyExtraData[$yy])){
459 $storages[] = $convertedLegacyExtraData[$yy];
461 $subChunks[$yy] =
new SubChunk(Block::EMPTY_STATE_ID, $storages, clone $biomes3d);
465 for($yy = Chunk::MIN_SUBCHUNK_INDEX; $yy <= Chunk::MAX_SUBCHUNK_INDEX; ++$yy){
466 if(!isset($subChunks[$yy])){
467 $subChunks[$yy] =
new SubChunk(Block::EMPTY_STATE_ID, [], clone $biomes3d);
477 private function deserializeNonPalettedSubChunkData(BinaryStream $binaryStream,
int $chunkVersion, ?PalettedBlockArray $convertedLegacyExtraData, PalettedBlockArray $biomePalette, \
Logger $logger) : SubChunk{
479 $blocks = $binaryStream->get(4096);
480 $blockData = $binaryStream->get(2048);
481 }
catch(BinaryDataException $e){
482 throw new CorruptedChunkException($e->getMessage(), 0, $e);
485 if($chunkVersion < ChunkVersion::v1_1_0){
487 $binaryStream->get(4096);
488 if(!$binaryStream->feof()){
489 $logger->
error(
"Unexpected trailing data in legacy subchunk data");
491 }
catch(BinaryDataException $e){
492 $logger->
error(
"Failed to read legacy subchunk light info: " . $e->getMessage());
496 $storages = [$this->palettizeLegacySubChunkXZY($blocks, $blockData, $logger)];
497 if($convertedLegacyExtraData !==
null){
498 $storages[] = $convertedLegacyExtraData;
501 return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
510 private function deserializeSubChunkData(BinaryStream $binaryStream,
int $chunkVersion,
int $subChunkVersion, ?PalettedBlockArray $convertedLegacyExtraData, PalettedBlockArray $biomePalette, \
Logger $logger) : SubChunk{
511 switch($subChunkVersion){
512 case SubChunkVersion::CLASSIC:
513 case SubChunkVersion::CLASSIC_BUG_2:
514 case SubChunkVersion::CLASSIC_BUG_3:
515 case SubChunkVersion::CLASSIC_BUG_4:
516 case SubChunkVersion::CLASSIC_BUG_5:
517 case SubChunkVersion::CLASSIC_BUG_6:
518 case SubChunkVersion::CLASSIC_BUG_7:
519 return $this->deserializeNonPalettedSubChunkData($binaryStream, $chunkVersion, $convertedLegacyExtraData, $biomePalette, $logger);
520 case SubChunkVersion::PALETTED_SINGLE:
521 $storages = [$this->deserializeBlockPalette($binaryStream, $logger)];
522 if($convertedLegacyExtraData !==
null){
523 $storages[] = $convertedLegacyExtraData;
525 return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
526 case SubChunkVersion::PALETTED_MULTI:
527 case SubChunkVersion::PALETTED_MULTI_WITH_OFFSET:
530 $storageCount = $binaryStream->getByte();
531 if($subChunkVersion >= SubChunkVersion::PALETTED_MULTI_WITH_OFFSET){
533 $binaryStream->getByte();
537 for($k = 0; $k < $storageCount; ++$k){
538 $storages[] = $this->deserializeBlockPalette($binaryStream, $logger);
540 return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
543 throw new CorruptedChunkException(
"don't know how to decode LevelDB subchunk format version $subChunkVersion");
547 private static function hasOffsetCavesAndCliffsSubChunks(
int $chunkVersion) : bool{
548 return $chunkVersion >= ChunkVersion::v1_16_220_50_unused && $chunkVersion <= ChunkVersion::v1_16_230_50_unused;
564 private function deserializeAllSubChunkData(
string $index,
int $chunkVersion,
bool &$hasBeenUpgraded, array $convertedLegacyExtraData, array $biomeArrays, \
Logger $logger) : array{
567 $subChunkKeyOffset = self::hasOffsetCavesAndCliffsSubChunks($chunkVersion) ? self::CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET : 0;
568 for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){
569 if(($data = $this->db->get($index . ChunkDataKey::SUBCHUNK . chr($y + $subChunkKeyOffset))) ===
false){
570 $subChunks[$y] =
new SubChunk(Block::EMPTY_STATE_ID, [], $biomeArrays[$y]);
574 $binaryStream =
new BinaryStream($data);
575 if($binaryStream->feof()){
576 throw new CorruptedChunkException(
"Unexpected empty data for subchunk $y");
578 $subChunkVersion = $binaryStream->getByte();
579 if($subChunkVersion < self::CURRENT_LEVEL_SUBCHUNK_VERSION){
580 $hasBeenUpgraded =
true;
583 $subChunks[$y] = $this->deserializeSubChunkData(
587 $convertedLegacyExtraData[$y] ??
null,
602 private function deserializeBiomeData(
string $index,
int $chunkVersion, \
Logger $logger) : array{
604 if(($maps2d = $this->db->get($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES)) !==
false){
605 $binaryStream =
new BinaryStream($maps2d);
608 $binaryStream->get(512);
609 $biomes3d = ChunkUtils::extrapolate3DBiomes($binaryStream->get(256));
610 if(!$binaryStream->feof()){
611 $logger->
error(
"Unexpected trailing data after 2D biome data");
613 }
catch(BinaryDataException $e){
614 throw new CorruptedChunkException($e->getMessage(), 0, $e);
616 for($i = Chunk::MIN_SUBCHUNK_INDEX; $i <= Chunk::MAX_SUBCHUNK_INDEX; ++$i){
617 $biomeArrays[$i] = clone $biomes3d;
619 }elseif(($maps3d = $this->db->get($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES)) !==
false){
620 $binaryStream =
new BinaryStream($maps3d);
623 $binaryStream->get(512);
624 $biomeArrays = self::deserialize3dBiomes($binaryStream, $chunkVersion, $logger);
625 }
catch(BinaryDataException $e){
626 throw new CorruptedChunkException($e->getMessage(), 0, $e);
629 $logger->
error(
"Missing biome data, using default ocean biome");
630 for($i = Chunk::MIN_SUBCHUNK_INDEX; $i <= Chunk::MAX_SUBCHUNK_INDEX; ++$i){
631 $biomeArrays[$i] =
new PalettedBlockArray(BiomeIds::OCEAN);
642 $index =
LevelDB::chunkIndex($chunkX, $chunkZ);
644 $chunkVersion = $this->readVersion($chunkX, $chunkZ);
645 if($chunkVersion ===
null){
652 $logger = new \PrefixedLogger($this->logger,
"Loading chunk x=$chunkX z=$chunkZ v$chunkVersion");
654 $hasBeenUpgraded = $chunkVersion < self::CURRENT_LEVEL_CHUNK_VERSION;
656 switch($chunkVersion){
657 case ChunkVersion::v1_21_40:
659 case ChunkVersion::v1_18_30:
660 case ChunkVersion::v1_18_0_25_beta:
661 case ChunkVersion::v1_18_0_24_unused:
662 case ChunkVersion::v1_18_0_24_beta:
663 case ChunkVersion::v1_18_0_22_unused:
664 case ChunkVersion::v1_18_0_22_beta:
665 case ChunkVersion::v1_18_0_20_unused:
666 case ChunkVersion::v1_18_0_20_beta:
667 case ChunkVersion::v1_17_40_unused:
668 case ChunkVersion::v1_17_40_20_beta_experimental_caves_cliffs:
669 case ChunkVersion::v1_17_30_25_unused:
670 case ChunkVersion::v1_17_30_25_beta_experimental_caves_cliffs:
671 case ChunkVersion::v1_17_30_23_unused:
672 case ChunkVersion::v1_17_30_23_beta_experimental_caves_cliffs:
673 case ChunkVersion::v1_16_230_50_unused:
674 case ChunkVersion::v1_16_230_50_beta_experimental_caves_cliffs:
675 case ChunkVersion::v1_16_220_50_unused:
676 case ChunkVersion::v1_16_220_50_beta_experimental_caves_cliffs:
677 case ChunkVersion::v1_16_210:
678 case ChunkVersion::v1_16_100_57_beta:
679 case ChunkVersion::v1_16_100_52_beta:
680 case ChunkVersion::v1_16_0:
681 case ChunkVersion::v1_16_0_51_beta:
683 case ChunkVersion::v1_12_0_unused2:
684 case ChunkVersion::v1_12_0_unused1:
685 case ChunkVersion::v1_12_0_4_beta:
686 case ChunkVersion::v1_11_1:
687 case ChunkVersion::v1_11_0_4_beta:
688 case ChunkVersion::v1_11_0_3_beta:
689 case ChunkVersion::v1_11_0_1_beta:
690 case ChunkVersion::v1_9_0:
691 case ChunkVersion::v1_8_0:
692 case ChunkVersion::v1_2_13:
693 case ChunkVersion::v1_2_0:
694 case ChunkVersion::v1_2_0_2_beta:
695 case ChunkVersion::v1_1_0_converted_from_console:
696 case ChunkVersion::v1_1_0:
698 case ChunkVersion::v1_0_0:
699 $convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion, $logger);
700 $biomeArrays = $this->deserializeBiomeData($index, $chunkVersion, $logger);
701 $subChunks = $this->deserializeAllSubChunkData($index, $chunkVersion, $hasBeenUpgraded, $convertedLegacyExtraData, $biomeArrays, $logger);
703 case ChunkVersion::v0_9_5:
704 case ChunkVersion::v0_9_2:
705 case ChunkVersion::v0_9_0:
706 $subChunks = $this->deserializeLegacyTerrainData($index, $chunkVersion, $logger);
712 $nbt =
new LittleEndianNbtSerializer();
716 if(($entityData = $this->db->get($index . ChunkDataKey::ENTITIES)) !==
false && $entityData !==
""){
718 $entities = array_map(fn(TreeRoot $root) => $root->mustGetCompoundTag(), $nbt->readMultiple($entityData));
719 }
catch(NbtDataException $e){
726 if(($tileData = $this->db->get($index . ChunkDataKey::BLOCK_ENTITIES)) !==
false && $tileData !==
""){
728 $tiles = array_map(fn(TreeRoot $root) => $root->mustGetCompoundTag(), $nbt->readMultiple($tileData));
729 }
catch(NbtDataException $e){
730 throw new CorruptedChunkException($e->getMessage(), 0, $e);
734 $finalisationChr = $this->db->get($index . ChunkDataKey::FINALIZATION);
735 if($finalisationChr !==
false){
736 $finalisation = ord($finalisationChr);
737 $terrainPopulated = $finalisation === self::FINALISATION_DONE;
739 $terrainPopulated =
true;
744 return new LoadedChunkData(
745 data:
new ChunkData($subChunks, $terrainPopulated, $entities, $tiles),
746 upgraded: $hasBeenUpgraded,
747 fixerFlags: LoadedChunkData::FIXER_FLAG_ALL
752 $index =
LevelDB::chunkIndex($chunkX, $chunkZ);
754 $write = new \LevelDBWriteBatch();
756 $write->put($index . ChunkDataKey::NEW_VERSION, chr(self::CURRENT_LEVEL_CHUNK_VERSION));
757 $write->put($index . ChunkDataKey::PM_DATA_VERSION, Binary::writeLLong(VersionInfo::WORLD_DATA_VERSION));
761 if(($dirtyFlags & Chunk::DIRTY_FLAG_BLOCKS) !== 0){
763 foreach($subChunks as $y => $subChunk){
764 $key = $index . ChunkDataKey::SUBCHUNK . chr($y);
765 if($subChunk->isEmptyAuthoritative()){
766 $write->delete($key);
769 $subStream->putByte(self::CURRENT_LEVEL_SUBCHUNK_VERSION);
771 $layers = $subChunk->getBlockLayers();
772 $subStream->putByte(count($layers));
773 foreach($layers as $blocks){
774 $this->serializeBlockPalette($subStream, $blocks);
777 $write->put($key, $subStream->getBuffer());
782 if(($dirtyFlags & Chunk::DIRTY_FLAG_BIOMES) !== 0){
783 $write->delete($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES);
784 $stream =
new BinaryStream();
785 $stream->put(str_repeat(
"\x00", 512));
786 self::serialize3dBiomes($stream, $subChunks);
787 $write->put($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES, $stream->getBuffer());
791 $write->put($index . ChunkDataKey::FINALIZATION, chr($chunkData->isPopulated() ? self::FINALISATION_DONE : self::FINALISATION_NEEDS_POPULATION));
793 $this->writeTags($chunkData->
getTileNBT(), $index . ChunkDataKey::BLOCK_ENTITIES, $write);
794 $this->writeTags($chunkData->
getEntityNBT(), $index . ChunkDataKey::ENTITIES, $write);
796 $write->delete($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOME_COLORS);
797 $write->delete($index . ChunkDataKey::LEGACY_TERRAIN);
799 $this->db->write($write);
805 private function writeTags(array $targets,
string $index, \LevelDBWriteBatch $write) : void{
806 if(count($targets) > 0){
807 $nbt =
new LittleEndianNbtSerializer();
808 $write->put($index, $nbt->writeMultiple(array_map(fn(CompoundTag $tag) =>
new TreeRoot($tag), $targets)));
810 $write->delete($index);
814 public function getDatabase() : \LevelDB{
818 public static function chunkIndex(
int $chunkX,
int $chunkZ) : string{
819 return Binary::writeLInt($chunkX) . Binary::writeLInt($chunkZ);
826 public function close() : void{
831 foreach($this->db->getIterator() as $key => $_){
832 if(strlen($key) === 9 && ($key[8] === ChunkDataKey::NEW_VERSION || $key[8] === ChunkDataKey::OLD_VERSION)){
833 $chunkX = Binary::readLInt(substr($key, 0, 4));
834 $chunkZ = Binary::readLInt(substr($key, 4, 4));
836 if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !==
null){
837 yield [$chunkX, $chunkZ] => $chunk;
843 if($logger !==
null){
844 $logger->
error(
"Skipped corrupted chunk $chunkX $chunkZ (" . $e->getMessage() .
")");
853 foreach($this->db->getIterator() as $key => $_){
854 if(strlen($key) === 9 && ($key[8] === ChunkDataKey::NEW_VERSION || $key[8] === ChunkDataKey::OLD_VERSION)){