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