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