PocketMine-MP 5.15.1 git-ed158f8a1b0cfe334ac5f45febc0f633602014f2
LevelDB.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\world\format\io\leveldb;
25
51use pocketmine\world\format\PalettedBlockArray;
54use Symfony\Component\Filesystem\Path;
55use function array_map;
56use function array_values;
57use function chr;
58use function count;
59use function defined;
60use function extension_loaded;
61use function file_exists;
62use function implode;
63use function is_dir;
64use function mkdir;
65use function ord;
66use function str_repeat;
67use function strlen;
68use function substr;
69use function trim;
70use function unpack;
71use const LEVELDB_ZLIB_RAW_COMPRESSION;
72
74
75 protected const FINALISATION_NEEDS_INSTATICKING = 0;
76 protected const FINALISATION_NEEDS_POPULATION = 1;
77 protected const FINALISATION_DONE = 2;
78
79 protected const ENTRY_FLAT_WORLD_LAYERS = "game_flatworldlayers";
80
81 protected const CURRENT_LEVEL_CHUNK_VERSION = ChunkVersion::v1_18_30;
82 protected const CURRENT_LEVEL_SUBCHUNK_VERSION = SubChunkVersion::PALETTED_MULTI;
83
84 private const CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET = 4;
85
86 protected \LevelDB $db;
87
88 private static function checkForLevelDBExtension() : void{
89 if(!extension_loaded('leveldb')){
90 throw new UnsupportedWorldFormatException("The leveldb PHP extension is required to use this world format");
91 }
92
93 if(!defined('LEVELDB_ZLIB_RAW_COMPRESSION')){
94 throw new UnsupportedWorldFormatException("Given version of php-leveldb doesn't support zlib raw compression");
95 }
96 }
97
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 //64KB, big enough for most chunks
105 ]);
106 }
107
108 public function __construct(string $path, \Logger $logger){
109 self::checkForLevelDBExtension();
110 parent::__construct($path, $logger);
111
112 try{
113 $this->db = self::createDB($path);
114 }catch(\LevelDBException $e){
115 //we can't tell the difference between errors caused by bad permissions and actual corruption :(
116 throw new CorruptedWorldException(trim($e->getMessage()), 0, $e);
117 }
118 }
119
120 protected function loadLevelData() : WorldData{
121 return new BedrockWorldData(Path::join($this->getPath(), "level.dat"));
122 }
123
124 public function getWorldMinY() : int{
125 return -64;
126 }
127
128 public function getWorldMaxY() : int{
129 return 320;
130 }
131
132 public static function isValid(string $path) : bool{
133 return file_exists(Path::join($path, "level.dat")) && is_dir(Path::join($path, "db"));
134 }
135
136 public static function generate(string $path, string $name, WorldCreationOptions $options) : void{
137 self::checkForLevelDBExtension();
138
139 $dbPath = Path::join($path, "db");
140 if(!file_exists($dbPath)){
141 mkdir($dbPath, 0777, true);
142 }
143
144 BedrockWorldData::generate($path, $name, $options);
145 }
146
150 protected function deserializeBlockPalette(BinaryStream $stream, \Logger $logger) : PalettedBlockArray{
151 $bitsPerBlock = $stream->getByte() >> 1;
152
153 try{
154 $words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
155 }catch(\InvalidArgumentException $e){
156 throw new CorruptedChunkException("Failed to deserialize paletted storage: " . $e->getMessage(), 0, $e);
157 }
158 $nbt = new LittleEndianNbtSerializer();
159 $palette = [];
160
161 if($bitsPerBlock === 0){
162 $paletteSize = 1;
163 /*
164 * Due to code copy-paste in a public plugin, some PM4 worlds have 0 bpb palettes with a length prefix.
165 * This is invalid and does not happen in vanilla.
166 * These palettes were accepted by PM4 despite being invalid, but PM5 considered them corrupt, causing loss
167 * of data. Since many users were affected by this, a workaround is therefore necessary to allow PM5 to read
168 * these worlds without data loss.
169 *
170 * References:
171 * - https://github.com/Refaltor77/CustomItemAPI/issues/68
172 * - https://github.com/pmmp/PocketMine-MP/issues/5911
173 */
174 $offset = $stream->getOffset();
175 $byte1 = $stream->getByte();
176 $stream->setOffset($offset); //reset offset
177
178 if($byte1 !== NBT::TAG_Compound){ //normally the first byte would be the NBT of the blockstate
179 $susLength = $stream->getLInt();
180 if($susLength !== 1){ //make sure the data isn't complete garbage
181 throw new CorruptedChunkException("CustomItemAPI borked 0 bpb palette should always have a length of 1");
182 }
183 $logger->error("Unexpected palette size for 0 bpb palette");
184 }
185 }else{
186 $paletteSize = $stream->getLInt();
187 }
188
189 $blockDecodeErrors = [];
190
191 for($i = 0; $i < $paletteSize; ++$i){
192 try{
193 $offset = $stream->getOffset();
194 $blockStateNbt = $nbt->read($stream->getBuffer(), $offset)->mustGetCompoundTag();
195 $stream->setOffset($offset);
196 }catch(NbtDataException $e){
197 //NBT borked, unrecoverable
198 throw new CorruptedChunkException("Invalid blockstate NBT at offset $i in paletted storage: " . $e->getMessage(), 0, $e);
199 }
200
201 //TODO: remember data for unknown states so we can implement them later
202 try{
203 $blockStateData = $this->blockDataUpgrader->upgradeBlockStateNbt($blockStateNbt);
204 }catch(BlockStateDeserializeException $e){
205 //while not ideal, this is not a fatal error
206 $blockDecodeErrors[] = "Palette offset $i / Upgrade error: " . $e->getMessage() . ", NBT: " . $blockStateNbt->toString();
207 $palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
208 continue;
209 }
210 try{
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());
218 }
219 }
220
221 if(count($blockDecodeErrors) > 0){
222 $logger->error("Errors decoding blocks:\n - " . implode("\n - ", $blockDecodeErrors));
223 }
224
225 //TODO: exceptions
226 return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
227 }
228
229 private function serializeBlockPalette(BinaryStream $stream, PalettedBlockArray $blocks) : void{
230 $stream->putByte($blocks->getBitsPerBlock() << 1);
231 $stream->put($blocks->getWordArray());
232
233 $palette = $blocks->getPalette();
234 if($blocks->getBitsPerBlock() !== 0){
235 $stream->putLInt(count($palette));
236 }
237 $tags = [];
238 foreach($palette as $p){
239 $tags[] = new TreeRoot($this->blockStateSerializer->serialize($p)->toNbt());
240 }
241
242 $stream->put((new LittleEndianNbtSerializer())->writeMultiple($tags));
243 }
244
248 private static function getExpected3dBiomesCount(int $chunkVersion) : int{
249 return match(true){
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")
256 };
257 }
258
262 private static function deserializeBiomePalette(BinaryStream $stream, int $bitsPerBlock) : PalettedBlockArray{
263 try{
264 $words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
265 }catch(\InvalidArgumentException $e){
266 throw new CorruptedChunkException("Failed to deserialize paletted biomes: " . $e->getMessage(), 0, $e);
267 }
268 $palette = [];
269 $paletteSize = $bitsPerBlock === 0 ? 1 : $stream->getLInt();
270
271 for($i = 0; $i < $paletteSize; ++$i){
272 $palette[] = $stream->getLInt();
273 }
274
275 //TODO: exceptions
276 return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
277 }
278
279 private static function serializeBiomePalette(BinaryStream $stream, PalettedBlockArray $biomes) : void{
280 $stream->putByte($biomes->getBitsPerBlock() << 1);
281 $stream->put($biomes->getWordArray());
282
283 $palette = $biomes->getPalette();
284 if($biomes->getBitsPerBlock() !== 0){
285 $stream->putLInt(count($palette));
286 }
287 foreach($palette as $p){
288 $stream->putLInt($p);
289 }
290 }
291
297 private static function deserialize3dBiomes(BinaryStream $stream, int $chunkVersion, \Logger $logger) : array{
298 $previous = null;
299 $result = [];
300 $nextIndex = Chunk::MIN_SUBCHUNK_INDEX;
301
302 $expectedCount = self::getExpected3dBiomesCount($chunkVersion);
303 for($i = 0; $i < $expectedCount; ++$i){
304 try{
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");
309 }
310 $decoded = clone $previous;
311 }else{
312 $decoded = self::deserializeBiomePalette($stream, $bitsPerBlock);
313 }
314 $previous = $decoded;
315 if($nextIndex <= Chunk::MAX_SUBCHUNK_INDEX){ //older versions wrote additional superfluous biome palettes
316 $result[$nextIndex++] = $decoded;
317 }elseif($stream->feof()){
318 //not enough padding biome arrays for the given version - this is non-critical since we discard the excess anyway, but this should be logged
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");
320 break;
321 }
322 }catch(BinaryDataException $e){
323 throw new CorruptedChunkException("Failed to deserialize biome palette $i: " . $e->getMessage(), 0, $e);
324 }
325 }
326 if(!$stream->feof()){
327 //maybe bad output produced by a third-party conversion tool like Chunker
328 $logger->error("Unexpected trailing data after 3D biomes data");
329 }
330
331 return $result;
332 }
333
337 private static function serialize3dBiomes(BinaryStream $stream, array $subChunks) : void{
338 //TODO: the server-side min/max may not coincide with the world storage min/max - we may need additional logic to handle this
339 for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; $y++){
340 //TODO: is it worth trying to use the previous palette if it's the same as the current one? vanilla supports
341 //this, but it's not clear if it's worth the effort to implement.
342 self::serializeBiomePalette($stream, $subChunks[$y]->getBiomeArray());
343 }
344 }
345
351 protected static function deserializeExtraDataKey(int $chunkVersion, int $key, ?int &$x, ?int &$y, ?int &$z) : void{
352 if($chunkVersion >= ChunkVersion::v1_0_0){
353 $x = ($key >> 12) & 0xf;
354 $z = ($key >> 8) & 0xf;
355 $y = $key & 0xff;
356 }else{ //pre-1.0, 7 bits were used because the build height limit was lower
357 $x = ($key >> 11) & 0xf;
358 $z = ($key >> 7) & 0xf;
359 $y = $key & 0x7f;
360 }
361 }
362
366 protected function deserializeLegacyExtraData(string $index, int $chunkVersion, \Logger $logger) : array{
367 if(($extraRawData = $this->db->get($index . ChunkDataKey::LEGACY_BLOCK_EXTRA_DATA)) === false || $extraRawData === ""){
368 return [];
369 }
370
372 $extraDataLayers = [];
373 $binaryStream = new BinaryStream($extraRawData);
374 $count = $binaryStream->getLInt();
375
376 for($i = 0; $i < $count; ++$i){
377 $key = $binaryStream->getLInt();
378 $value = $binaryStream->getLShort();
379
380 self::deserializeExtraDataKey($chunkVersion, $key, $x, $fullY, $z);
381
382 $ySub = ($fullY >> SubChunk::COORD_BIT_SIZE);
383 $y = $key & SubChunk::COORD_MASK;
384
385 $blockId = $value & 0xff;
386 $blockData = ($value >> 8) & 0xf;
387 try{
388 $blockStateData = $this->blockDataUpgrader->upgradeIntIdMeta($blockId, $blockData);
390 //TODO: we could preserve this in case it's supported in the future, but this was historically only
391 //used for grass anyway, so we probably don't need to care
392 $logger->error("Failed to upgrade legacy extra block: " . $e->getMessage() . " ($blockId:$blockData)");
393 continue;
394 }
395 //assume this won't throw
396 $blockStateId = $this->blockStateDeserializer->deserialize($blockStateData);
397
398 if(!isset($extraDataLayers[$ySub])){
399 $extraDataLayers[$ySub] = new PalettedBlockArray(Block::EMPTY_STATE_ID);
400 }
401 $extraDataLayers[$ySub]->set($x, $y, $z, $blockStateId);
402 }
403
404 return $extraDataLayers;
405 }
406
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){
413 return null;
414 }
415 }
416
417 return ord($chunkVersionRaw);
418 }
419
427 private function deserializeLegacyTerrainData(string $index, int $chunkVersion, \Logger $logger) : array{
428 $convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion, $logger);
429
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");
433 }
434 $binaryStream = new BinaryStream($legacyTerrain);
435 try{
436 $fullIds = $binaryStream->get(32768);
437 $fullData = $binaryStream->get(16384);
438 $binaryStream->get(32768); //legacy light info, discard it
439 }catch(BinaryDataException $e){
440 throw new CorruptedChunkException($e->getMessage(), 0, $e);
441 }
442
443 try{
444 $binaryStream->get(256); //heightmap, discard it
446 $unpackedBiomeArray = unpack("N*", $binaryStream->get(1024)); //unpack() will never fail here
447 $biomes3d = ChunkUtils::extrapolate3DBiomes(ChunkUtils::convertBiomeColors(array_values($unpackedBiomeArray))); //never throws
448 }catch(BinaryDataException $e){
449 throw new CorruptedChunkException($e->getMessage(), 0, $e);
450 }
451 if(!$binaryStream->feof()){
452 $logger->error("Unexpected trailing data in legacy terrain data");
453 }
454
455 $subChunks = [];
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];
460 }
461 $subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, $storages, clone $biomes3d);
462 }
463
464 //make sure extrapolated biomes get filled in correctly
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);
468 }
469 }
470
471 return $subChunks;
472 }
473
477 private function deserializeNonPalettedSubChunkData(BinaryStream $binaryStream, int $chunkVersion, ?PalettedBlockArray $convertedLegacyExtraData, PalettedBlockArray $biomePalette, \Logger $logger) : SubChunk{
478 try{
479 $blocks = $binaryStream->get(4096);
480 $blockData = $binaryStream->get(2048);
481 }catch(BinaryDataException $e){
482 throw new CorruptedChunkException($e->getMessage(), 0, $e);
483 }
484
485 if($chunkVersion < ChunkVersion::v1_1_0){
486 try{
487 $binaryStream->get(4096); //legacy light info, discard it
488 if(!$binaryStream->feof()){
489 $logger->error("Unexpected trailing data in legacy subchunk data");
490 }
491 }catch(BinaryDataException $e){
492 $logger->error("Failed to read legacy subchunk light info: " . $e->getMessage());
493 }
494 }
495
496 $storages = [$this->palettizeLegacySubChunkXZY($blocks, $blockData, $logger)];
497 if($convertedLegacyExtraData !== null){
498 $storages[] = $convertedLegacyExtraData;
499 }
500
501 return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
502 }
503
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: //these are all identical to version 0, but vanilla respects these so we should also
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;
524 }
525 return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
526 case SubChunkVersion::PALETTED_MULTI:
527 case SubChunkVersion::PALETTED_MULTI_WITH_OFFSET:
528 //legacy extradata layers intentionally ignored because they aren't supposed to exist in v8
529
530 $storageCount = $binaryStream->getByte();
531 if($subChunkVersion >= SubChunkVersion::PALETTED_MULTI_WITH_OFFSET){
532 //height ignored; this seems pointless since this is already in the key anyway
533 $binaryStream->getByte();
534 }
535
536 $storages = [];
537 for($k = 0; $k < $storageCount; ++$k){
538 $storages[] = $this->deserializeBlockPalette($binaryStream, $logger);
539 }
540 return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
541 default:
542 //this should never happen - an unsupported chunk appearing in a supported world is a sign of corruption
543 throw new CorruptedChunkException("don't know how to decode LevelDB subchunk format version $subChunkVersion");
544 }
545 }
546
547 private static function hasOffsetCavesAndCliffsSubChunks(int $chunkVersion) : bool{
548 return $chunkVersion >= ChunkVersion::v1_16_220_50_unused && $chunkVersion <= ChunkVersion::v1_16_230_50_unused;
549 }
550
564 private function deserializeAllSubChunkData(string $index, int $chunkVersion, bool &$hasBeenUpgraded, array $convertedLegacyExtraData, array $biomeArrays, \Logger $logger) : array{
565 $subChunks = [];
566
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]);
571 continue;
572 }
573
574 $binaryStream = new BinaryStream($data);
575 if($binaryStream->feof()){
576 throw new CorruptedChunkException("Unexpected empty data for subchunk $y");
577 }
578 $subChunkVersion = $binaryStream->getByte();
579 if($subChunkVersion < self::CURRENT_LEVEL_SUBCHUNK_VERSION){
580 $hasBeenUpgraded = true;
581 }
582
583 $subChunks[$y] = $this->deserializeSubChunkData(
584 $binaryStream,
585 $chunkVersion,
586 $subChunkVersion,
587 $convertedLegacyExtraData[$y] ?? null,
588 $biomeArrays[$y],
589 new \PrefixedLogger($logger, "Subchunk y=$y v$subChunkVersion")
590 );
591 }
592
593 return $subChunks;
594 }
595
602 private function deserializeBiomeData(string $index, int $chunkVersion, \Logger $logger) : array{
603 $biomeArrays = [];
604 if(($maps2d = $this->db->get($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES)) !== false){
605 $binaryStream = new BinaryStream($maps2d);
606
607 try{
608 $binaryStream->get(512); //heightmap, discard it
609 $biomes3d = ChunkUtils::extrapolate3DBiomes($binaryStream->get(256)); //never throws
610 if(!$binaryStream->feof()){
611 $logger->error("Unexpected trailing data after 2D biome data");
612 }
613 }catch(BinaryDataException $e){
614 throw new CorruptedChunkException($e->getMessage(), 0, $e);
615 }
616 for($i = Chunk::MIN_SUBCHUNK_INDEX; $i <= Chunk::MAX_SUBCHUNK_INDEX; ++$i){
617 $biomeArrays[$i] = clone $biomes3d;
618 }
619 }elseif(($maps3d = $this->db->get($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES)) !== false){
620 $binaryStream = new BinaryStream($maps3d);
621
622 try{
623 $binaryStream->get(512);
624 $biomeArrays = self::deserialize3dBiomes($binaryStream, $chunkVersion, $logger);
625 }catch(BinaryDataException $e){
626 throw new CorruptedChunkException($e->getMessage(), 0, $e);
627 }
628 }else{
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); //polyfill
632 }
633 }
634
635 return $biomeArrays;
636 }
637
641 public function loadChunk(int $chunkX, int $chunkZ) : ?LoadedChunkData{
642 $index = LevelDB::chunkIndex($chunkX, $chunkZ);
643
644 $chunkVersion = $this->readVersion($chunkX, $chunkZ);
645 if($chunkVersion === null){
646 //TODO: this might be a slightly-corrupted chunk with a missing version field
647 return null;
648 }
649
650 //TODO: read PM_DATA_VERSION - we'll need it to fix up old chunks
651
652 $logger = new \PrefixedLogger($this->logger, "Loading chunk x=$chunkX z=$chunkZ v$chunkVersion");
653
654 $hasBeenUpgraded = $chunkVersion < self::CURRENT_LEVEL_CHUNK_VERSION;
655
656 switch($chunkVersion){
657 case ChunkVersion::v1_18_30:
658 case ChunkVersion::v1_18_0_25_beta:
659 case ChunkVersion::v1_18_0_24_unused:
660 case ChunkVersion::v1_18_0_24_beta:
661 case ChunkVersion::v1_18_0_22_unused:
662 case ChunkVersion::v1_18_0_22_beta:
663 case ChunkVersion::v1_18_0_20_unused:
664 case ChunkVersion::v1_18_0_20_beta:
665 case ChunkVersion::v1_17_40_unused:
666 case ChunkVersion::v1_17_40_20_beta_experimental_caves_cliffs:
667 case ChunkVersion::v1_17_30_25_unused:
668 case ChunkVersion::v1_17_30_25_beta_experimental_caves_cliffs:
669 case ChunkVersion::v1_17_30_23_unused:
670 case ChunkVersion::v1_17_30_23_beta_experimental_caves_cliffs:
671 case ChunkVersion::v1_16_230_50_unused:
672 case ChunkVersion::v1_16_230_50_beta_experimental_caves_cliffs:
673 case ChunkVersion::v1_16_220_50_unused:
674 case ChunkVersion::v1_16_220_50_beta_experimental_caves_cliffs:
675 case ChunkVersion::v1_16_210:
676 case ChunkVersion::v1_16_100_57_beta:
677 case ChunkVersion::v1_16_100_52_beta:
678 case ChunkVersion::v1_16_0:
679 case ChunkVersion::v1_16_0_51_beta:
680 //TODO: check walls
681 case ChunkVersion::v1_12_0_unused2:
682 case ChunkVersion::v1_12_0_unused1:
683 case ChunkVersion::v1_12_0_4_beta:
684 case ChunkVersion::v1_11_1:
685 case ChunkVersion::v1_11_0_4_beta:
686 case ChunkVersion::v1_11_0_3_beta:
687 case ChunkVersion::v1_11_0_1_beta:
688 case ChunkVersion::v1_9_0:
689 case ChunkVersion::v1_8_0:
690 case ChunkVersion::v1_2_13:
691 case ChunkVersion::v1_2_0:
692 case ChunkVersion::v1_2_0_2_beta:
693 case ChunkVersion::v1_1_0_converted_from_console:
694 case ChunkVersion::v1_1_0:
695 //TODO: check beds
696 case ChunkVersion::v1_0_0:
697 $convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion, $logger);
698 $biomeArrays = $this->deserializeBiomeData($index, $chunkVersion, $logger);
699 $subChunks = $this->deserializeAllSubChunkData($index, $chunkVersion, $hasBeenUpgraded, $convertedLegacyExtraData, $biomeArrays, $logger);
700 break;
701 case ChunkVersion::v0_9_5:
702 case ChunkVersion::v0_9_2:
703 case ChunkVersion::v0_9_0:
704 $subChunks = $this->deserializeLegacyTerrainData($index, $chunkVersion, $logger);
705 break;
706 default:
707 throw new CorruptedChunkException("don't know how to decode chunk format version $chunkVersion");
708 }
709
710 $nbt = new LittleEndianNbtSerializer();
711
713 $entities = [];
714 if(($entityData = $this->db->get($index . ChunkDataKey::ENTITIES)) !== false && $entityData !== ""){
715 try{
716 $entities = array_map(fn(TreeRoot $root) => $root->mustGetCompoundTag(), $nbt->readMultiple($entityData));
717 }catch(NbtDataException $e){
718 throw new CorruptedChunkException($e->getMessage(), 0, $e);
719 }
720 }
721
723 $tiles = [];
724 if(($tileData = $this->db->get($index . ChunkDataKey::BLOCK_ENTITIES)) !== false && $tileData !== ""){
725 try{
726 $tiles = array_map(fn(TreeRoot $root) => $root->mustGetCompoundTag(), $nbt->readMultiple($tileData));
727 }catch(NbtDataException $e){
728 throw new CorruptedChunkException($e->getMessage(), 0, $e);
729 }
730 }
731
732 $finalisationChr = $this->db->get($index . ChunkDataKey::FINALIZATION);
733 if($finalisationChr !== false){
734 $finalisation = ord($finalisationChr);
735 $terrainPopulated = $finalisation === self::FINALISATION_DONE;
736 }else{ //older versions didn't have this tag
737 $terrainPopulated = true;
738 }
739
740 //TODO: tile ticks, biome states (?)
741
742 return new LoadedChunkData(
743 data: new ChunkData($subChunks, $terrainPopulated, $entities, $tiles),
744 upgraded: $hasBeenUpgraded,
745 fixerFlags: LoadedChunkData::FIXER_FLAG_ALL //TODO: fill this by version rather than just setting all flags
746 );
747 }
748
749 public function saveChunk(int $chunkX, int $chunkZ, ChunkData $chunkData, int $dirtyFlags) : void{
750 $index = LevelDB::chunkIndex($chunkX, $chunkZ);
751
752 $write = new \LevelDBWriteBatch();
753
754 $write->put($index . ChunkDataKey::NEW_VERSION, chr(self::CURRENT_LEVEL_CHUNK_VERSION));
755 $write->put($index . ChunkDataKey::PM_DATA_VERSION, Binary::writeLLong(VersionInfo::WORLD_DATA_VERSION));
756
757 $subChunks = $chunkData->getSubChunks();
758
759 if(($dirtyFlags & Chunk::DIRTY_FLAG_BLOCKS) !== 0){
760
761 foreach($subChunks as $y => $subChunk){
762 $key = $index . ChunkDataKey::SUBCHUNK . chr($y);
763 if($subChunk->isEmptyAuthoritative()){
764 $write->delete($key);
765 }else{
766 $subStream = new BinaryStream();
767 $subStream->putByte(self::CURRENT_LEVEL_SUBCHUNK_VERSION);
768
769 $layers = $subChunk->getBlockLayers();
770 $subStream->putByte(count($layers));
771 foreach($layers as $blocks){
772 $this->serializeBlockPalette($subStream, $blocks);
773 }
774
775 $write->put($key, $subStream->getBuffer());
776 }
777 }
778 }
779
780 if(($dirtyFlags & Chunk::DIRTY_FLAG_BIOMES) !== 0){
781 $write->delete($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES);
782 $stream = new BinaryStream();
783 $stream->put(str_repeat("\x00", 512)); //fake heightmap
784 self::serialize3dBiomes($stream, $subChunks);
785 $write->put($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES, $stream->getBuffer());
786 }
787
788 //TODO: use this properly
789 $write->put($index . ChunkDataKey::FINALIZATION, chr($chunkData->isPopulated() ? self::FINALISATION_DONE : self::FINALISATION_NEEDS_POPULATION));
790
791 $this->writeTags($chunkData->getTileNBT(), $index . ChunkDataKey::BLOCK_ENTITIES, $write);
792 $this->writeTags($chunkData->getEntityNBT(), $index . ChunkDataKey::ENTITIES, $write);
793
794 $write->delete($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOME_COLORS);
795 $write->delete($index . ChunkDataKey::LEGACY_TERRAIN);
796
797 $this->db->write($write);
798 }
799
803 private function writeTags(array $targets, string $index, \LevelDBWriteBatch $write) : void{
804 if(count($targets) > 0){
805 $nbt = new LittleEndianNbtSerializer();
806 $write->put($index, $nbt->writeMultiple(array_map(fn(CompoundTag $tag) => new TreeRoot($tag), $targets)));
807 }else{
808 $write->delete($index);
809 }
810 }
811
812 public function getDatabase() : \LevelDB{
813 return $this->db;
814 }
815
816 public static function chunkIndex(int $chunkX, int $chunkZ) : string{
817 return Binary::writeLInt($chunkX) . Binary::writeLInt($chunkZ);
818 }
819
820 public function doGarbageCollection() : void{
821
822 }
823
824 public function close() : void{
825 unset($this->db);
826 }
827
828 public function getAllChunks(bool $skipCorrupted = false, ?\Logger $logger = null) : \Generator{
829 foreach($this->db->getIterator() as $key => $_){
830 if(strlen($key) === 9 && ($key[8] === ChunkDataKey::NEW_VERSION || $key[8] === ChunkDataKey::OLD_VERSION)){
831 $chunkX = Binary::readLInt(substr($key, 0, 4));
832 $chunkZ = Binary::readLInt(substr($key, 4, 4));
833 try{
834 if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== null){
835 yield [$chunkX, $chunkZ] => $chunk;
836 }
837 }catch(CorruptedChunkException $e){
838 if(!$skipCorrupted){
839 throw $e;
840 }
841 if($logger !== null){
842 $logger->error("Skipped corrupted chunk $chunkX $chunkZ (" . $e->getMessage() . ")");
843 }
844 }
845 }
846 }
847 }
848
849 public function calculateChunkCount() : int{
850 $count = 0;
851 foreach($this->db->getIterator() as $key => $_){
852 if(strlen($key) === 9 && ($key[8] === ChunkDataKey::NEW_VERSION || $key[8] === ChunkDataKey::OLD_VERSION)){
853 $count++;
854 }
855 }
856 return $count;
857 }
858}
getAllChunks(bool $skipCorrupted=false, ?\Logger $logger=null)
Definition: LevelDB.php:828
loadChunk(int $chunkX, int $chunkZ)
Definition: LevelDB.php:641
deserializeBlockPalette(BinaryStream $stream, \Logger $logger)
Definition: LevelDB.php:150
static deserializeExtraDataKey(int $chunkVersion, int $key, ?int &$x, ?int &$y, ?int &$z)
Definition: LevelDB.php:351
deserializeLegacyExtraData(string $index, int $chunkVersion, \Logger $logger)
Definition: LevelDB.php:366
saveChunk(int $chunkX, int $chunkZ, ChunkData $chunkData, int $dirtyFlags)
Definition: LevelDB.php:749
error($message)