diff --git a/src/pocketmine/level/format/Chunk.php b/src/pocketmine/level/format/Chunk.php index 0865b28f2..e7e5c90c2 100644 --- a/src/pocketmine/level/format/Chunk.php +++ b/src/pocketmine/level/format/Chunk.php @@ -28,7 +28,7 @@ namespace pocketmine\level\format; use pocketmine\block\Block; use pocketmine\entity\Entity; -use pocketmine\level\format\io\ChunkException; +use pocketmine\level\format\ChunkException; use pocketmine\level\Level; use pocketmine\nbt\NBT; use pocketmine\nbt\tag\CompoundTag; diff --git a/src/pocketmine/level/format/io/ChunkException.php b/src/pocketmine/level/format/ChunkException.php similarity index 95% rename from src/pocketmine/level/format/io/ChunkException.php rename to src/pocketmine/level/format/ChunkException.php index 61c3c9d32..570720df3 100644 --- a/src/pocketmine/level/format/io/ChunkException.php +++ b/src/pocketmine/level/format/ChunkException.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\level\format\io; +namespace pocketmine\level\format; class ChunkException extends \RuntimeException{ diff --git a/src/pocketmine/level/format/EmptySubChunk.php b/src/pocketmine/level/format/EmptySubChunk.php index bc5b52b8a..f946d4083 100644 --- a/src/pocketmine/level/format/EmptySubChunk.php +++ b/src/pocketmine/level/format/EmptySubChunk.php @@ -25,7 +25,7 @@ namespace pocketmine\level\format; class EmptySubChunk implements SubChunkInterface{ - public function isEmpty() : bool{ + public function isEmpty(bool $checkLight = true) : bool{ return true; } diff --git a/src/pocketmine/level/format/SubChunk.php b/src/pocketmine/level/format/SubChunk.php index a3f4931d6..56db712f1 100644 --- a/src/pocketmine/level/format/SubChunk.php +++ b/src/pocketmine/level/format/SubChunk.php @@ -46,11 +46,13 @@ class SubChunk implements SubChunkInterface{ self::assignData($this->blockLight, $blockLight, 2048); } - public function isEmpty() : bool{ + public function isEmpty(bool $checkLight = true) : bool{ return ( substr_count($this->ids, "\x00") === 4096 and - substr_count($this->skyLight, "\xff") === 2048 and - substr_count($this->blockLight, "\x00") === 2048 + (!$checkLight or ( + substr_count($this->skyLight, "\xff") === 2048 and + substr_count($this->blockLight, "\x00") === 2048 + )) ); } diff --git a/src/pocketmine/level/format/SubChunkInterface.php b/src/pocketmine/level/format/SubChunkInterface.php index 2690cfc17..a0001b12f 100644 --- a/src/pocketmine/level/format/SubChunkInterface.php +++ b/src/pocketmine/level/format/SubChunkInterface.php @@ -26,9 +26,10 @@ namespace pocketmine\level\format; interface SubChunkInterface{ /** + * @param bool $checkLight * @return bool */ - public function isEmpty() : bool; + public function isEmpty(bool $checkLight = true) : bool; /** * @param int $x diff --git a/src/pocketmine/level/format/io/BaseLevelProvider.php b/src/pocketmine/level/format/io/BaseLevelProvider.php index fd5c46738..247e7a6ad 100644 --- a/src/pocketmine/level/format/io/BaseLevelProvider.php +++ b/src/pocketmine/level/format/io/BaseLevelProvider.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\level\format\io; use pocketmine\level\format\Chunk; +use pocketmine\level\format\ChunkException; use pocketmine\level\generator\Generator; use pocketmine\level\Level; use pocketmine\level\LevelException; diff --git a/src/pocketmine/level/format/io/exception/UnsupportedChunkFormatException.php b/src/pocketmine/level/format/io/exception/UnsupportedChunkFormatException.php new file mode 100644 index 000000000..eec42c0a2 --- /dev/null +++ b/src/pocketmine/level/format/io/exception/UnsupportedChunkFormatException.php @@ -0,0 +1,30 @@ +levelData->NetworkVersion = new IntTag("NetworkVersion", ProtocolInfo::CURRENT_PROTOCOL); + $this->levelData->StorageVersion = new IntTag("StorageVersion", self::CURRENT_STORAGE_VERSION); + $nbt = new NBT(NBT::LITTLE_ENDIAN); $nbt->setData($this->levelData); $buffer = $nbt->write(); @@ -287,78 +302,94 @@ class LevelDB extends BaseLevelProvider{ } try{ + /** @var SubChunk[] $subChunks */ $subChunks = []; - $heightMap = []; - $biomeIds = ""; - for($y = Chunk::MAX_SUBCHUNKS - 1; $y >= 0; --$y){ - if($this->db->get($index . self::TAG_SUBCHUNK_PREFIX . chr($y)) !== false){ //Found subchunk data! + /** @var bool $lightPopulated */ + $lightPopulated = true; + + $chunkVersion = ord($this->db->get($index . self::TAG_VERSION)); + + $binaryStream = new BinaryStream(); + + switch($chunkVersion){ + case 4: //MCPE 1.1 + //TODO: check beds + case 3: //MCPE 1.0 + for($y = 0; $y < Chunk::MAX_SUBCHUNKS; ++$y){ + if(($data = $this->db->get($index . self::TAG_SUBCHUNK_PREFIX . chr($y))) === false){ + continue; + } + + $binaryStream->setBuffer($data, 0); + $subChunkVersion = $binaryStream->getByte(); + + switch($subChunkVersion){ + case 0: + $blocks = $binaryStream->get(4096); + $blockData = $binaryStream->get(2048); + if($chunkVersion < 4){ + $blockSkyLight = $binaryStream->get(2048); + $blockLight = $binaryStream->get(2048); + }else{ + //Mojang didn't bother changing the subchunk version when they stopped saving sky light -_- + $blockSkyLight = ""; + $blockLight = ""; + $lightPopulated = false; + } + + $subChunks[$y] = new SubChunk($blocks, $blockData, $blockSkyLight, $blockLight); + break; + default: + throw new UnsupportedChunkFormatException("don't know how to decode LevelDB subchunk format version $subChunkVersion"); + } + } + + $binaryStream->setBuffer($this->db->get($index . self::TAG_DATA_2D), 0); + + $heightMap = array_values(unpack("v*", $binaryStream->get(512))); + $biomeIds = $binaryStream->get(256); break; - } - } + case 2: // < MCPE 1.0 + $binaryStream->setBuffer($this->db->get($index . self::TAG_LEGACY_TERRAIN)); + $fullIds = $binaryStream->get(32768); + $fullData = $binaryStream->get(16384); + $fullSkyLight = $binaryStream->get(16384); + $fullBlockLight = $binaryStream->get(16384); - if($y <= 0 and ($legacyTerrain = $this->db->get($index . self::TAG_LEGACY_TERRAIN)) !== false){ //didn't find any subchunk data but found old (pre-1.0) data - $offset = 0; - $fullIds = substr($legacyTerrain, $offset, 32768); - $offset += 32768; - $fullData = substr($legacyTerrain, $offset, 16384); - $offset += 16384; - $fullSkyLight = substr($legacyTerrain, $offset, 16384); - $offset += 16384; - $fullBlockLight = substr($legacyTerrain, $offset, 16384); - $offset += 16384; + for($yy = 0; $yy < 8; ++$yy){ + $subOffset = ($yy << 4); + $ids = ""; + for($i = 0; $i < 256; ++$i){ + $ids .= substr($fullIds, $subOffset, 16); + $subOffset += 128; + } + $data = ""; + $subOffset = ($yy << 3); + for($i = 0; $i < 256; ++$i){ + $data .= substr($fullData, $subOffset, 8); + $subOffset += 64; + } + $skyLight = ""; + $subOffset = ($yy << 3); + for($i = 0; $i < 256; ++$i){ + $skyLight .= substr($fullSkyLight, $subOffset, 8); + $subOffset += 64; + } + $blockLight = ""; + $subOffset = ($yy << 3); + for($i = 0; $i < 256; ++$i){ + $blockLight .= substr($fullBlockLight, $subOffset, 8); + $subOffset += 64; + } + $subChunks[$yy] = new SubChunk($ids, $data, $skyLight, $blockLight); + } - for($yy = 0; $yy < 8; ++$yy){ - $subOffset = ($yy << 4); - $ids = ""; - for($i = 0; $i < 256; ++$i){ - $ids .= substr($fullIds, $subOffset, 16); - $subOffset += 128; - } - $data = ""; - $subOffset = ($yy << 3); - for($i = 0; $i < 256; ++$i){ - $data .= substr($fullData, $subOffset, 8); - $subOffset += 64; - } - $skyLight = ""; - $subOffset = ($yy << 3); - for($i = 0; $i < 256; ++$i){ - $skyLight .= substr($fullSkyLight, $subOffset, 8); - $subOffset += 64; - } - $blockLight = ""; - $subOffset = ($yy << 3); - for($i = 0; $i < 256; ++$i){ - $blockLight .= substr($fullBlockLight, $subOffset, 8); - $subOffset += 64; - } - $subChunks[$yy] = new SubChunk($ids, $data, $skyLight, $blockLight); - } - - $heightMap = array_values(unpack("C*", substr($legacyTerrain, $offset, 256))); - $offset += 256; - $biomeIds = ChunkUtils::convertBiomeColors(array_values(unpack("N*", substr($legacyTerrain, $offset, 1024)))); - $offset += 1024; - }else{ - for(; $y >= 0; --$y){ //If one subchunk exists, all subchunks below it are also guaranteed to exist. - $offset = 1; //Skip subchunk version byte - $subChunkData = $this->db->get($index . self::TAG_SUBCHUNK_PREFIX . chr($y)); - $subChunks[$y] = new SubChunk( - substr($subChunkData, $offset, 4096), //block ids - substr($subChunkData, $offset += 4096, 2048), //block meta - substr($subChunkData, $offset += 2048, 2048), //sky light - substr($subChunkData, $offset += 2048, 2048) //block light - ); - } - - if(($data2dLegacy = $this->db->get($index . self::TAG_DATA_2D_LEGACY)) !== false){ //Found old data, convert it to new format - $heightMap = array_values(unpack("C*", substr($data2dLegacy, 0, 256))); - $biomeIds = ChunkUtils::convertBiomeColors(array_values(unpack("N*", substr($data2dLegacy, 256, 1024)))); - }elseif(($data2d = $this->db->get($index . self::TAG_DATA_2D)) !== false){ - $heightMap = array_values(unpack("v*", substr($data2d, 0, 512))); - $biomeIds = substr($data2d, 512, 256); - } + $heightMap = array_values(unpack("C*", $binaryStream->get(256))); + $biomeIds = ChunkUtils::convertBiomeColors(array_values(unpack("N*", $binaryStream->get(1024)))); + break; + default: + throw new UnsupportedChunkFormatException("don't know how to decode chunk format version $chunkVersion"); } $nbt = new NBT(NBT::LITTLE_ENDIAN); @@ -381,17 +412,16 @@ class LevelDB extends BaseLevelProvider{ } } - /* $extraData = []; - if(($extraRawData = $this->db->get($index . self::TAG_EXTRA_DATA)) !== false){ - $stream = new BinaryStream($extraRawData); - $count = $stream->getLInt(); //TODO: check if the extra data is BE or LE + if(($extraRawData = $this->db->get($index . self::TAG_BLOCK_EXTRA_DATA)) !== false and strlen($extraRawData) > 0){ + $binaryStream->setBuffer($extraRawData, 0); + $count = $binaryStream->getLInt(); for($i = 0; $i < $count; ++$i){ - $key = $stream->getInt(); - $value = $stream->getShort(false); + $key = $binaryStream->getLInt(); + $value = $binaryStream->getLShort(); $extraData[$key] = $value; } - }*/ //TODO + } $chunk = new Chunk( $chunkX, @@ -405,17 +435,18 @@ class LevelDB extends BaseLevelProvider{ //TODO: tile ticks, biome states (?) - /* - $flags = $this->db->get($index . self::ENTRY_FLAGS); - if($flags === false){ - $flags = "\x03"; - }*/ - // TODO: check this, add flags (?) $chunk->setGenerated(true); $chunk->setPopulated(true); - $chunk->setLightPopulated(true); + $chunk->setLightPopulated($lightPopulated); return $chunk; + }catch(UnsupportedChunkFormatException $e){ + //TODO: set chunks read-only so the version on disk doesn't get overwritten + + $logger = MainLogger::getLogger(); + $logger->error("Failed to decode LevelDB chunk: " . $e->getMessage()); + + return null; }catch(\Throwable $t){ $logger = MainLogger::getLogger(); $logger->error("LevelDB chunk decode error"); @@ -428,28 +459,52 @@ class LevelDB extends BaseLevelProvider{ private function writeChunk(Chunk $chunk){ $index = LevelDB::chunkIndex($chunk->getX(), $chunk->getZ()); - $this->db->put($index . self::TAG_VERSION, chr(self::CURRENT_STORAGE_VERSION)); - $highestIndex = $chunk->getHighestSubChunkIndex(); - $subChunks = $chunk->getSubChunks(); - for($y = $highestIndex; $y >= 0; --$y){ //Subchunks behave like a stack + $this->db->put($index . self::TAG_VERSION, chr(self::CURRENT_LEVEL_CHUNK_VERSION)); - $this->db->put($index . self::TAG_SUBCHUNK_PREFIX . chr($y), - "\x00" . //Subchunk version byte - $subChunks[$y]->getBlockIdArray() . - $subChunks[$y]->getBlockDataArray() . - $subChunks[$y]->getBlockSkyLightArray() . - $subChunks[$y]->getBlockLightArray() - ); + $subChunks = $chunk->getSubChunks(); + foreach($subChunks as $y => $subChunk){ + $key = $index . self::TAG_SUBCHUNK_PREFIX . chr($y); + if($subChunk->isEmpty(false)){ //MCPE doesn't save light anymore as of 1.1 + $this->db->delete($key); + }else{ + $this->db->put($key, + chr(self::CURRENT_LEVEL_SUBCHUNK_VERSION) . + $subChunks[$y]->getBlockIdArray() . + $subChunks[$y]->getBlockDataArray() + ); + } } $this->db->put($index . self::TAG_DATA_2D, pack("v*", ...$chunk->getHeightMapArray()) . $chunk->getBiomeIdArray()); + $extraData = $chunk->getBlockExtraDataArray(); + if(count($extraData) > 0){ + $stream = new BinaryStream(); + $stream->putLInt(count($extraData)); + foreach($extraData as $key => $value){ + $stream->putLInt($key); + $stream->putLShort($value); + } + + $this->db->put($index . self::TAG_BLOCK_EXTRA_DATA, $stream->getBuffer()); + }else{ + $this->db->delete($index . self::TAG_BLOCK_EXTRA_DATA); + } + + //TODO: use this properly + $this->db->put($index . self::TAG_STATE_FINALISATION, chr(self::FINALISATION_DONE)); + $this->writeTags($chunk->getTiles(), $index . self::TAG_BLOCK_ENTITY); $this->writeTags($chunk->getEntities(), $index . self::TAG_ENTITY); - //TODO: clean up old data + $this->db->delete($index . self::TAG_DATA_2D_LEGACY); + $this->db->delete($index . self::TAG_LEGACY_TERRAIN); } + /** + * @param Entity[]|Tile[] $targets + * @param string $index + */ private function writeTags(array $targets, string $index){ $nbt = new NBT(NBT::LITTLE_ENDIAN); $out = []; diff --git a/src/pocketmine/level/format/io/region/Anvil.php b/src/pocketmine/level/format/io/region/Anvil.php index 774b5e0f1..9a87d40f6 100644 --- a/src/pocketmine/level/format/io/region/Anvil.php +++ b/src/pocketmine/level/format/io/region/Anvil.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\level\format\io\region; use pocketmine\level\format\Chunk; -use pocketmine\level\format\io\ChunkException; +use pocketmine\level\format\ChunkException; use pocketmine\level\format\io\ChunkUtils; use pocketmine\level\format\SubChunk; use pocketmine\nbt\NBT; diff --git a/src/pocketmine/level/format/io/region/McRegion.php b/src/pocketmine/level/format/io/region/McRegion.php index c8778a2a0..8ffc81c2b 100644 --- a/src/pocketmine/level/format/io/region/McRegion.php +++ b/src/pocketmine/level/format/io/region/McRegion.php @@ -25,7 +25,7 @@ namespace pocketmine\level\format\io\region; use pocketmine\level\format\Chunk; use pocketmine\level\format\io\BaseLevelProvider; -use pocketmine\level\format\io\ChunkException; +use pocketmine\level\format\ChunkException; use pocketmine\level\format\io\ChunkUtils; use pocketmine\level\format\SubChunk; use pocketmine\level\generator\Generator; diff --git a/src/pocketmine/level/format/io/region/PMAnvil.php b/src/pocketmine/level/format/io/region/PMAnvil.php index e36b9bccf..7e1440484 100644 --- a/src/pocketmine/level/format/io/region/PMAnvil.php +++ b/src/pocketmine/level/format/io/region/PMAnvil.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\level\format\io\region; use pocketmine\level\format\Chunk; -use pocketmine\level\format\io\ChunkException; +use pocketmine\level\format\ChunkException; use pocketmine\level\format\SubChunk; use pocketmine\nbt\NBT; use pocketmine\nbt\tag\{ diff --git a/src/pocketmine/level/format/io/region/RegionLoader.php b/src/pocketmine/level/format/io/region/RegionLoader.php index 05b6c7b6e..95ff67e35 100644 --- a/src/pocketmine/level/format/io/region/RegionLoader.php +++ b/src/pocketmine/level/format/io/region/RegionLoader.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\level\format\io\region; use pocketmine\level\format\Chunk; -use pocketmine\level\format\io\ChunkException; +use pocketmine\level\format\ChunkException; use pocketmine\utils\Binary; use pocketmine\utils\MainLogger;