From a858103e6b861cc889328ee73dbae2a5215309ac Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Mon, 4 Mar 2019 20:02:06 +0000 Subject: [PATCH] Support paletted subchunks, drop all formats except leveldb --- composer.json | 2 + composer.lock | 4 +- resources/legacy_id_map.json | 87 ++++++++++++- src/pocketmine/PocketMine.php | 2 + src/pocketmine/level/Level.php | 5 +- src/pocketmine/level/format/Chunk.php | 116 ++++++++++-------- src/pocketmine/level/format/EmptySubChunk.php | 24 +--- .../level/format/PalettedBlockArray.php | 53 ++++++++ src/pocketmine/level/format/SubChunk.php | 108 ++++++++-------- .../level/format/SubChunkInterface.php | 38 +----- .../level/format/io/LevelProviderManager.php | 2 +- .../level/format/io/SubChunkConverter.php | 37 ++++++ .../level/format/io/data/BedrockLevelData.php | 2 +- .../level/format/io/leveldb/LevelDB.php | 109 ++++++++++++---- .../level/format/io/region/Anvil.php | 21 +--- .../io/region/LegacyAnvilChunkTrait.php | 37 +----- .../level/format/io/region/McRegion.php | 16 +-- .../level/format/io/region/PMAnvil.php | 20 +-- .../level/generator/populator/GroundCover.php | 9 +- 19 files changed, 421 insertions(+), 271 deletions(-) create mode 100644 src/pocketmine/level/format/PalettedBlockArray.php create mode 100644 src/pocketmine/level/format/io/SubChunkConverter.php diff --git a/composer.json b/composer.json index 013babad3..a1ad3c122 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "php": ">=7.2.0", "php-64bit": "*", "ext-bcmath": "*", + "ext-chunkutils2": "^0.1.0", "ext-curl": "*", "ext-crypto": "^0.3.1", "ext-ctype": "*", @@ -16,6 +17,7 @@ "ext-gmp": "*", "ext-hash": "*", "ext-json": "*", + "ext-leveldb": "^0.2.1", "ext-mbstring": "*", "ext-openssl": "*", "ext-pcre": "*", diff --git a/composer.lock b/composer.lock index 7d512129d..f46ba97f7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d865f72c482e34c96a7e46bc535d0c15", + "content-hash": "7d9bec9f6226ca3ec19b06f6ed406718", "packages": [ { "name": "adhocore/json-comment", @@ -532,6 +532,7 @@ "php": ">=7.2.0", "php-64bit": "*", "ext-bcmath": "*", + "ext-chunkutils2": "^0.1.0", "ext-curl": "*", "ext-crypto": "^0.3.1", "ext-ctype": "*", @@ -540,6 +541,7 @@ "ext-gmp": "*", "ext-hash": "*", "ext-json": "*", + "ext-leveldb": "^0.2.1", "ext-mbstring": "*", "ext-openssl": "*", "ext-pcre": "*", diff --git a/resources/legacy_id_map.json b/resources/legacy_id_map.json index 3f83bf124..9795ee314 100644 --- a/resources/legacy_id_map.json +++ b/resources/legacy_id_map.json @@ -247,5 +247,88 @@ "minecraft:structure_block": 252, "minecraft:hard_glass": 253, "minecraft:hard_stained_glass": 254, - "minecraft:reserved6": 255 -} \ No newline at end of file + "minecraft:reserved6": 255, + "minecraft:prismarine_stairs": 257, + "minecraft:dark_prismarine_stairs": 258, + "minecraft:prismarine_bricks_stairs": 259, + "minecraft:stripped_spruce_log": 260, + "minecraft:stripped_birch_log": 261, + "minecraft:stripped_jungle_log": 262, + "minecraft:stripped_acacia_log": 263, + "minecraft:stripped_dark_oak_log": 264, + "minecraft:stripped_oak_log": 265, + "minecraft:blue_ice": 266, + "minecraft:seagrass": 385, + "minecraft:coral": 386, + "minecraft:coral_block": 387, + "minecraft:coral_fan": 388, + "minecraft:coral_fan_dead": 389, + "minecraft:coral_fan_hang": 390, + "minecraft:coral_fan_hang2": 391, + "minecraft:coral_fan_hang3": 392, + "minecraft:kelp": 393, + "minecraft:dried_kelp_block": 394, + "minecraft:acacia_button": 395, + "minecraft:birch_button": 396, + "minecraft:dark_oak_button": 397, + "minecraft:jungle_button": 398, + "minecraft:spruce_button": 399, + "minecraft:acacia_trapdoor": 400, + "minecraft:birch_trapdoor": 401, + "minecraft:dark_oak_trapdoor": 402, + "minecraft:jungle_trapdoor": 403, + "minecraft:spruce_trapdoor": 404, + "minecraft:acacia_pressure_plate": 405, + "minecraft:birch_pressure_plate": 406, + "minecraft:dark_oak_pressure_plate": 407, + "minecraft:jungle_pressure_plate": 408, + "minecraft:spruce_pressure_plate": 409, + "minecraft:carved_pumpkin": 410, + "minecraft:sea_pickle": 411, + "minecraft:conduit": 412, + "minecraft:turtle_egg": 414, + "minecraft:bubble_column": 415, + "minecraft:barrier": 416, + "minecraft:stone_slab3": 417, + "minecraft:bamboo": 418, + "minecraft:bamboo_sapling": 419, + "minecraft:scaffolding": 420, + "minecraft:stone_slab4": 421, + "minecraft:double_stone_slab3": 422, + "minecraft:double_stone_slab4": 423, + "minecraft:granite_stairs": 424, + "minecraft:diorite_stairs": 425, + "minecraft:andesite_stairs": 426, + "minecraft:polished_granite_stairs": 427, + "minecraft:polished_diorite_stairs": 428, + "minecraft:polished_andesite_stairs": 429, + "minecraft:mossy_stone_brick_stairs": 430, + "minecraft:smooth_red_sandstone_stairs": 431, + "minecraft:smooth_sandstone_stairs": 432, + "minecraft:end_brick_stairs": 433, + "minecraft:mossy_cobblestone_stairs": 434, + "minecraft:normal_stone_stairs": 435, + "minecraft:spruce_standing_sign": 436, + "minecraft:spruce_wall_sign": 437, + "minecraft:smooth_stone": 438, + "minecraft:red_nether_brick_stairs": 439, + "minecraft:smooth_quartz_stairs": 440, + "minecraft:birch_standing_sign": 441, + "minecraft:birch_wall_sign": 442, + "minecraft:jungle_standing_sign": 443, + "minecraft:jungle_wall_sign": 444, + "minecraft:acacia_standing_sign": 445, + "minecraft:acacia_wall_sign": 446, + "minecraft:darkoak_standing_sign": 447, + "minecraft:darkoak_wall_sign": 448, + "minecraft:grindstone": 450, + "minecraft:blast_furnace": 451, + "minecraft:smoker": 453, + "minecraft:cartography_table": 455, + "minecraft:fletching_table": 456, + "minecraft:smithing_table": 457, + "minecraft:barrel": 458, + "minecraft:bell": 461, + "minecraft:lantern": 463, + "minecraft:lava_cauldron": 465 +} diff --git a/src/pocketmine/PocketMine.php b/src/pocketmine/PocketMine.php index daf84e6cb..f98081703 100644 --- a/src/pocketmine/PocketMine.php +++ b/src/pocketmine/PocketMine.php @@ -76,6 +76,7 @@ namespace pocketmine { $extensions = [ "bcmath" => "BC Math", + "chunkutils2" => "PocketMine ChunkUtils v2", "curl" => "cURL", "crypto" => "php-crypto", "ctype" => "ctype", @@ -84,6 +85,7 @@ namespace pocketmine { "gmp" => "GMP", "hash" => "Hash", "json" => "JSON", + "leveldb" => "LevelDB", "mbstring" => "Multibyte String", "openssl" => "OpenSSL", "pcre" => "PCRE", diff --git a/src/pocketmine/level/Level.php b/src/pocketmine/level/Level.php index 1c57ff5ca..f8c3ffa84 100644 --- a/src/pocketmine/level/Level.php +++ b/src/pocketmine/level/Level.php @@ -28,6 +28,7 @@ namespace pocketmine\level; use pocketmine\block\Block; use pocketmine\block\BlockFactory; +use pocketmine\block\BlockIds; use pocketmine\block\UnknownBlock; use pocketmine\entity\Entity; use pocketmine\entity\EntityFactory; @@ -380,7 +381,7 @@ class Level implements ChunkManager, Metadatable{ $dontTickBlocks = array_fill_keys($this->server->getProperty("chunk-ticking.disable-block-ticking", []), true); - $this->randomTickBlocks = new \SplFixedArray(4096); + $this->randomTickBlocks = new \SplFixedArray(16384); foreach($this->randomTickBlocks as $i => $null){ $id = $i >> 4; $meta = $i & 0xf; @@ -2783,7 +2784,7 @@ class Level implements ChunkManager, Metadatable{ $z = (int) $v->z; if($chunk !== null and $chunk->isGenerated()){ $y = (int) min($max - 2, $v->y); - $wasAir = ($chunk->getBlockId($x & 0x0f, $y - 1, $z & 0x0f) === 0); + $wasAir = $this->getBlockAt($x, $y - 1, $z)->getId() === BlockIds::AIR; //TODO: bad hack, clean up for(; $y > 0; --$y){ if($this->isFullBlock($this->getBlockAt($x, $y, $z))){ if($wasAir){ diff --git a/src/pocketmine/level/format/Chunk.php b/src/pocketmine/level/format/Chunk.php index 094205db7..476e69048 100644 --- a/src/pocketmine/level/format/Chunk.php +++ b/src/pocketmine/level/format/Chunk.php @@ -27,10 +27,12 @@ declare(strict_types=1); namespace pocketmine\level\format; use pocketmine\block\BlockFactory; +use pocketmine\block\BlockIds; use pocketmine\entity\Entity; use pocketmine\entity\EntityFactory; use pocketmine\level\Level; use pocketmine\nbt\tag\CompoundTag; +use pocketmine\network\mcpe\NetworkBinaryStream; use pocketmine\Player; use pocketmine\tile\Spawnable; use pocketmine\tile\Tile; @@ -205,32 +207,6 @@ class Chunk{ return false; } - /** - * Returns the block ID at the specified chunk block coordinates - * - * @param int $x 0-15 - * @param int $y - * @param int $z 0-15 - * - * @return int 0-255 - */ - public function getBlockId(int $x, int $y, int $z) : int{ - return $this->getSubChunk($y >> 4)->getBlockId($x, $y & 0x0f, $z); - } - - /** - * Returns the block meta value at the specified chunk block coordinates - * - * @param int $x 0-15 - * @param int $y - * @param int $z 0-15 - * - * @return int 0-15 - */ - public function getBlockData(int $x, int $y, int $z) : int{ - return $this->getSubChunk($y >> 4)->getBlockData($x, $y & 0x0f, $z); - } - /** * Returns the sky light level at the specified chunk block coordinates * @@ -698,7 +674,7 @@ class Chunk{ if($y < 0 or $y >= $this->height){ return $this->emptySubChunk; }elseif($generateNew and $this->subChunks[$y] instanceof EmptySubChunk){ - $this->subChunks[$y] = new SubChunk(); + $this->subChunks[$y] = new SubChunk([new PalettedBlockArray(BlockIds::AIR << 4)]); } return $this->subChunks[$y]; @@ -765,10 +741,9 @@ class Chunk{ public function collectGarbage() : void{ foreach($this->subChunks as $y => $subChunk){ if($subChunk instanceof SubChunk){ + $subChunk->collectGarbage(); if($subChunk->isEmpty()){ $this->subChunks[$y] = $this->emptySubChunk; - }else{ - $subChunk->collectGarbage(); } } } @@ -780,24 +755,42 @@ class Chunk{ * @return string */ public function networkSerialize() : string{ - $result = ""; + $stream = new NetworkBinaryStream(); $subChunkCount = $this->getSubChunkSendCount(); - $result .= chr($subChunkCount); - for($y = 0; $y < $subChunkCount; ++$y){ - $result .= $this->subChunks[$y]->networkSerialize(); + $stream->putByte($subChunkCount); + + if(empty(BlockFactory::$staticRuntimeIdMap)){ + BlockFactory::registerStaticRuntimeIdMappings(); } - $result .= pack("v*", ...$this->heightMap) - . $this->biomeIds - . chr(0); //border block array count + + for($y = 0; $y < $subChunkCount; ++$y){ + $layers = $this->subChunks[$y]->getBlockLayers(); + $stream->putByte(8); //version + + $stream->putByte(count($layers)); + + foreach($layers as $blocks){ + $stream->putByte(($blocks->getBitsPerBlock() << 1) | 1); //last 1-bit means "network format", but seems pointless + $stream->put($blocks->getWordArray()); + $palette = $blocks->getPalette(); + $stream->putVarInt(count($palette)); //yes, this is intentionally zigzag + foreach($palette as $p){ + $stream->putVarInt(BlockFactory::toStaticRuntimeId($p >> 4, $p & 0xf)); + } + } + } + $stream->put(pack("v*", ...$this->heightMap)); + $stream->put($this->biomeIds); + $stream->putByte(0); //border block array count //Border block entry format: 1 byte (4 bits X, 4 bits Z). These are however useless since they crash the regular client. foreach($this->tiles as $tile){ if($tile instanceof Spawnable){ - $result .= $tile->getSerializedSpawnCompound(); + $stream->put($tile->getSerializedSpawnCompound()); } } - return $result; + return $stream->getBuffer(); } /** @@ -814,19 +807,35 @@ class Chunk{ if($this->terrainGenerated){ //subchunks $count = 0; - $subChunks = ""; + $subStream = new BinaryStream(); foreach($this->subChunks as $y => $subChunk){ if($subChunk instanceof EmptySubChunk){ continue; } ++$count; - $subChunks .= chr($y) . $subChunk->getBlockIdArray() . $subChunk->getBlockDataArray(); + + $subStream->putByte($y); + $layers = $subChunk->getBlockLayers(); + $subStream->putByte(count($subChunk->getBlockLayers())); + foreach($layers as $blocks){ + $wordArray = $blocks->getWordArray(); + $palette = $blocks->getPalette(); + + $subStream->putByte($blocks->getBitsPerBlock()); + $subStream->put($wordArray); + $subStream->putInt(count($palette)); + foreach($palette as $p){ + $subStream->putInt($p); + } + } + if($this->lightPopulated){ - $subChunks .= $subChunk->getBlockSkyLightArray() . $subChunk->getBlockLightArray(); + $subStream->put($subChunk->getBlockSkyLightArray()); + $subStream->put($subChunk->getBlockLightArray()); } } $stream->putByte($count); - $stream->put($subChunks); + $stream->put($subStream->getBuffer()); //biomes $stream->put($this->biomeIds); @@ -860,12 +869,23 @@ class Chunk{ $heightMap = []; if($terrainGenerated){ $count = $stream->getByte(); - for($y = 0; $y < $count; ++$y){ - $subChunks[$stream->getByte()] = new SubChunk( - $stream->get(4096), //blockids - $stream->get(2048), //blockdata - $lightPopulated ? $stream->get(2048) : "", //skylight - $lightPopulated ? $stream->get(2048) : "" //blocklight + for($subCount = 0; $subCount < $count; ++$subCount){ + $y = $stream->getByte(); + + /** @var PalettedBlockArray[] $layers */ + $layers = []; + for($i = 0, $layerCount = $stream->getByte(); $i < $layerCount; ++$i){ + $bitsPerBlock = $stream->getByte(); + $words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock)); + $palette = []; + for($k = 0, $paletteSize = $stream->getInt(); $k < $paletteSize; ++$k){ + $palette[] = $stream->getInt(); + } + + $layers[] = PalettedBlockArray::fromData($bitsPerBlock, $words, $palette); + } + $subChunks[$y] = new SubChunk( + $layers, $lightPopulated ? $stream->get(2048) : "", $lightPopulated ? $stream->get(2048) : "" //blocklight ); } diff --git a/src/pocketmine/level/format/EmptySubChunk.php b/src/pocketmine/level/format/EmptySubChunk.php index a26341c6a..f45a8b071 100644 --- a/src/pocketmine/level/format/EmptySubChunk.php +++ b/src/pocketmine/level/format/EmptySubChunk.php @@ -41,14 +41,6 @@ class EmptySubChunk implements SubChunkInterface{ return true; } - public function getBlockId(int $x, int $y, int $z) : int{ - return 0; - } - - public function getBlockData(int $x, int $y, int $z) : int{ - return 0; - } - public function getFullBlock(int $x, int $y, int $z) : int{ return 0; } @@ -57,6 +49,10 @@ class EmptySubChunk implements SubChunkInterface{ return false; } + public function getBlockLayers() : array{ + return []; + } + public function getBlockLight(int $x, int $y, int $z) : int{ return 0; } @@ -77,14 +73,6 @@ class EmptySubChunk implements SubChunkInterface{ return -1; } - public function getBlockIdArray() : string{ - return str_repeat("\x00", 4096); - } - - public function getBlockDataArray() : string{ - return str_repeat("\x00", 2048); - } - public function getBlockLightArray() : string{ return str_repeat("\x00", 2048); } @@ -100,8 +88,4 @@ class EmptySubChunk implements SubChunkInterface{ public function setBlockSkyLightArray(string $data) : void{ } - - public function networkSerialize() : string{ - return "\x00" . str_repeat("\x00", 6144); - } } diff --git a/src/pocketmine/level/format/PalettedBlockArray.php b/src/pocketmine/level/format/PalettedBlockArray.php new file mode 100644 index 000000000..90a380873 --- /dev/null +++ b/src/pocketmine/level/format/PalettedBlockArray.php @@ -0,0 +1,53 @@ +ids, $ids, 4096); - self::assignData($this->data, $data, 2048); + /** + * SubChunk constructor. + * + * @param PalettedBlockArray[] $blocks + * @param string $skyLight + * @param string $blockLight + */ + public function __construct(array $blocks, string $skyLight = "", string $blockLight = ""){ + $this->blockLayers = $blocks; + self::assignData($this->skyLight, $skyLight, 2048, "\xff"); self::assignData($this->blockLight, $blockLight, 2048); - $this->collectGarbage(); } public function isEmpty(bool $checkLight = true) : bool{ - return ( - substr_count($this->ids, "\x00") === 4096 and + foreach($this->blockLayers as $layer){ + $palette = $layer->getPalette(); + foreach($palette as $p){ + if(($p >> 4) !== BlockIds::AIR){ + return false; + } + } + } + return (!$checkLight or ( substr_count($this->skyLight, "\xff") === 2048 and $this->blockLight === ZERO_NIBBLE_ARRAY - )) + ) ); } - public function getBlockId(int $x, int $y, int $z) : int{ - return ord($this->ids{($x << 8) | ($z << 4) | $y}); - } - - public function getBlockData(int $x, int $y, int $z) : int{ - return (ord($this->data{($x << 7) | ($z << 3) | ($y >> 1)}) >> (($y & 1) << 2)) & 0xf; - } - public function getFullBlock(int $x, int $y, int $z) : int{ - $i = ($x << 8) | ($z << 4) | $y; - return (ord($this->ids{$i}) << 4) | ((ord($this->data{$i >> 1}) >> (($y & 1) << 2)) & 0xf); + if(empty($this->blockLayers)){ + return BlockIds::AIR << 4; + } + return $this->blockLayers[0]->get($x, $y, $z); } public function setBlock(int $x, int $y, int $z, int $id, int $data) : bool{ - $i = ($x << 8) | ($z << 4) | $y; - $changed = false; - - $block = chr($id); - if($this->ids{$i} !== $block){ - $this->ids{$i} = $block; - $changed = true; + if(empty($this->blockLayers)){ + $this->blockLayers[] = new PalettedBlockArray(BlockIds::AIR << 4); } + $this->blockLayers[0]->set($x, $y, $z, ($id << 4) | $data); + return true; + } - $i >>= 1; - $shift = ($y & 1) << 2; - $oldPair = ord($this->data{$i}); - $newPair = ($oldPair & ~(0xf << $shift)) | (($data & 0xf) << $shift); - if($newPair !== $oldPair){ - $this->data{$i} = chr($newPair); - $changed = true; - } - - return $changed; + /** + * @return PalettedBlockArray[] + */ + public function getBlockLayers() : array{ + return $this->blockLayers; } public function getBlockLight(int $x, int $y, int $z) : int{ @@ -136,7 +141,7 @@ class SubChunk implements SubChunkInterface{ $low = ($x << 8) | ($z << 4); $i = $low | 0x0f; for(; $i >= $low; --$i){ - if($this->ids{$i} !== "\x00"){ + if(($this->blockLayers[0]->get($x, $i, $z) >> 4) !== BlockIds::AIR){ return $i & 0x0f; } } @@ -144,16 +149,6 @@ class SubChunk implements SubChunkInterface{ return -1; //highest block not in this subchunk } - public function getBlockIdArray() : string{ - assert(strlen($this->ids) === 4096, "Wrong length of ID array, expecting 4096 bytes, got " . strlen($this->ids)); - return $this->ids; - } - - public function getBlockDataArray() : string{ - assert(strlen($this->data) === 2048, "Wrong length of data array, expecting 2048 bytes, got " . strlen($this->data)); - return $this->data; - } - public function getBlockSkyLightArray() : string{ assert(strlen($this->skyLight) === 2048, "Wrong length of skylight array, expecting 2048 bytes, got " . strlen($this->skyLight)); return $this->skyLight; @@ -174,23 +169,28 @@ class SubChunk implements SubChunkInterface{ $this->blockLight = $data; } - public function networkSerialize() : string{ - return "\x00" . $this->ids . $this->data; - } - public function __debugInfo(){ return []; } public function collectGarbage() : void{ + foreach($this->blockLayers as $k => $layer){ + $layer->collectGarbage(); + + foreach($layer->getPalette() as $p){ + if(($p >> 4) !== BlockIds::AIR){ + continue 2; + } + } + unset($this->blockLayers[$k]); + } + $this->blockLayers = array_values($this->blockLayers); + /* * This strange looking code is designed to exploit PHP's copy-on-write behaviour. Assigning will copy a * reference to the const instead of duplicating the whole string. The string will only be duplicated when * modified, which is perfect for this purpose. */ - if($this->data === ZERO_NIBBLE_ARRAY){ - $this->data = ZERO_NIBBLE_ARRAY; - } if($this->skyLight === ZERO_NIBBLE_ARRAY){ $this->skyLight = ZERO_NIBBLE_ARRAY; } diff --git a/src/pocketmine/level/format/SubChunkInterface.php b/src/pocketmine/level/format/SubChunkInterface.php index d058efd55..6b57b6bec 100644 --- a/src/pocketmine/level/format/SubChunkInterface.php +++ b/src/pocketmine/level/format/SubChunkInterface.php @@ -32,24 +32,6 @@ interface SubChunkInterface{ */ public function isEmpty(bool $checkLight = true) : bool; - /** - * @param int $x - * @param int $y - * @param int $z - * - * @return int - */ - public function getBlockId(int $x, int $y, int $z) : int; - - /** - * @param int $x - * @param int $y - * @param int $z - * - * @return int - */ - public function getBlockData(int $x, int $y, int $z) : int; - /** * @param int $x * @param int $y @@ -70,6 +52,11 @@ interface SubChunkInterface{ */ public function setBlock(int $x, int $y, int $z, int $id, int $data) : bool; + /** + * @return PalettedBlockArray[] + */ + public function getBlockLayers() : array; + /** * @param int $x * @param int $y @@ -116,16 +103,6 @@ interface SubChunkInterface{ */ public function getHighestBlockAt(int $x, int $z) : int; - /** - * @return string - */ - public function getBlockIdArray() : string; - - /** - * @return string - */ - public function getBlockDataArray() : string; - /** * @return string */ @@ -145,9 +122,4 @@ interface SubChunkInterface{ * @param string $data */ public function setBlockLightArray(string $data) : void; - - /** - * @return string - */ - public function networkSerialize() : string; } diff --git a/src/pocketmine/level/format/io/LevelProviderManager.php b/src/pocketmine/level/format/io/LevelProviderManager.php index 7585f077e..75268fd88 100644 --- a/src/pocketmine/level/format/io/LevelProviderManager.php +++ b/src/pocketmine/level/format/io/LevelProviderManager.php @@ -35,7 +35,7 @@ abstract class LevelProviderManager{ protected static $providers = []; /** @var string|LevelProvider */ - private static $default = PMAnvil::class; + private static $default = LevelDB::class; public static function init() : void{ self::addProvider(Anvil::class, "anvil"); diff --git a/src/pocketmine/level/format/io/SubChunkConverter.php b/src/pocketmine/level/format/io/SubChunkConverter.php new file mode 100644 index 000000000..8aaab68ce --- /dev/null +++ b/src/pocketmine/level/format/io/SubChunkConverter.php @@ -0,0 +1,37 @@ +getByte() >> 1; + + try{ + $words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock)); + }catch(\InvalidArgumentException $e){ + throw new CorruptedChunkException("Failed to deserialize paletted storage: " . $e->getMessage(), 0, $e); + } + $nbt = new LittleEndianNbtSerializer(); + $palette = []; + for($i = 0, $paletteSize = $stream->getLInt(); $i < $paletteSize; ++$i){ + $offset = $stream->getOffset(); + $tag = $nbt->read($stream->getBuffer(), $offset); + $stream->setOffset($offset); + + $id = $stringToLegacyId[$tag->getString("name")] ?? BlockIds::INFO_UPDATE; + $data = $tag->getShort("val"); + $palette[] = ($id << 4) | $data; + } + + //TODO: exceptions + return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette); + } + /** * @param int $chunkX * @param int $chunkZ @@ -159,6 +198,8 @@ class LevelDB extends BaseLevelProvider implements WritableLevelProvider{ $binaryStream = new BinaryStream(); switch($chunkVersion){ + case 10: //MCPE 1.9 (???) + case 9: //MCPE 1.8 (???) case 7: //MCPE 1.2 (???) case 4: //MCPE 1.1 //TODO: check beds @@ -179,6 +220,12 @@ class LevelDB extends BaseLevelProvider implements WritableLevelProvider{ switch($subChunkVersion){ case 0: + case 2: //these are all identical to version 0, but vanilla respects these so we should also + case 3: + case 4: + case 5: + case 6: + case 7: try{ $blocks = $binaryStream->get(4096); $blockData = $binaryStream->get(2048); @@ -191,7 +238,17 @@ class LevelDB extends BaseLevelProvider implements WritableLevelProvider{ throw new CorruptedChunkException($e->getMessage(), 0, $e); } - $subChunks[$y] = new SubChunk($blocks, $blockData); + $subChunks[$y] = new SubChunk([SubChunkConverter::convertSubChunkXZY($blocks, $blockData)]); + break; + case 1: //paletted v1, has a single blockstorage + $subChunks[$y] = new SubChunk([$this->deserializePaletted($binaryStream)]); + break; + case 8: + $storages = []; + for($k = 0, $storageCount = $binaryStream->getByte(); $k < $storageCount; ++$k){ + $storages[] = $this->deserializePaletted($binaryStream); + } + $subChunks[$y] = new SubChunk($storages); break; default: //TODO: set chunks read-only so the version on disk doesn't get overwritten @@ -225,20 +282,7 @@ class LevelDB extends BaseLevelProvider implements WritableLevelProvider{ } 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; - } - - $subChunks[$yy] = new SubChunk($ids, $data); + $subChunks[$yy] = new SubChunk([SubChunkConverter::convertSubChunkFromLegacyColumn($fullIds, $fullData, $yy)]); } try{ @@ -307,6 +351,10 @@ class LevelDB extends BaseLevelProvider implements WritableLevelProvider{ } protected function writeChunk(Chunk $chunk) : void{ + static $idMap = null; + if($idMap === null){ + $idMap = array_flip(json_decode(file_get_contents(\pocketmine\RESOURCE_PATH . '/legacy_id_map.json'), true)); + } $index = LevelDB::chunkIndex($chunk->getX(), $chunk->getZ()); $this->db->put($index . self::TAG_VERSION, chr(self::CURRENT_LEVEL_CHUNK_VERSION)); @@ -316,11 +364,30 @@ class LevelDB extends BaseLevelProvider implements WritableLevelProvider{ 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) . - $subChunk->getBlockIdArray() . - $subChunk->getBlockDataArray() - ); + $subStream = new BinaryStream(); + $subStream->putByte(self::CURRENT_LEVEL_SUBCHUNK_VERSION); + + $layers = $subChunk->getBlockLayers(); + $subStream->putByte(count($layers)); + foreach($layers as $blocks){ + $subStream->putByte($blocks->getBitsPerBlock() << 1); + $subStream->put($blocks->getWordArray()); + + $palette = $blocks->getPalette(); + $subStream->putLInt(count($palette)); + $tags = []; + foreach($palette as $p){ + $tags[] = new CompoundTag("", [ + new StringTag("name", $idMap[$p >> 4] ?? "minecraft:info_update"), + new IntTag("oldid", $p >> 4), //PM only (debugging), vanilla doesn't have this + new ShortTag("val", $p & 0xf) + ]); + } + + $subStream->put((new LittleEndianNbtSerializer())->writeMultiple($tags)); + } + + $this->db->put($key, $subStream->getBuffer()); } } diff --git a/src/pocketmine/level/format/io/region/Anvil.php b/src/pocketmine/level/format/io/region/Anvil.php index abeef950d..6d81486b3 100644 --- a/src/pocketmine/level/format/io/region/Anvil.php +++ b/src/pocketmine/level/format/io/region/Anvil.php @@ -23,31 +23,20 @@ declare(strict_types=1); namespace pocketmine\level\format\io\region; -use pocketmine\level\format\io\ChunkUtils; -use pocketmine\level\format\io\WritableLevelProvider; +use pocketmine\level\format\io\SubChunkConverter; use pocketmine\level\format\SubChunk; -use pocketmine\nbt\tag\ByteArrayTag; use pocketmine\nbt\tag\CompoundTag; -use function str_repeat; -class Anvil extends RegionLevelProvider implements WritableLevelProvider{ +class Anvil extends RegionLevelProvider{ use LegacyAnvilChunkTrait; protected function serializeSubChunk(SubChunk $subChunk) : CompoundTag{ - return new CompoundTag("", [ - new ByteArrayTag("Blocks", ChunkUtils::reorderByteArray($subChunk->getBlockIdArray())), //Generic in-memory chunks are currently always XZY - new ByteArrayTag("Data", ChunkUtils::reorderNibbleArray($subChunk->getBlockDataArray())), - new ByteArrayTag("SkyLight", str_repeat("\x00", 2048)), - new ByteArrayTag("BlockLight", str_repeat("\x00", 2048)) - ]); + throw new \RuntimeException("Unsupported"); } protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk{ - return new SubChunk( - ChunkUtils::reorderByteArray($subChunk->getByteArray("Blocks")), - ChunkUtils::reorderNibbleArray($subChunk->getByteArray("Data")) - //ignore legacy light information - ); + return new SubChunk([SubChunkConverter::convertSubChunkYZX($subChunk->getByteArray("Blocks"), $subChunk->getByteArray("Data"))]); + //ignore legacy light information } protected static function getRegionFileExtension() : string{ diff --git a/src/pocketmine/level/format/io/region/LegacyAnvilChunkTrait.php b/src/pocketmine/level/format/io/region/LegacyAnvilChunkTrait.php index fc2ac45d0..a60c914e3 100644 --- a/src/pocketmine/level/format/io/region/LegacyAnvilChunkTrait.php +++ b/src/pocketmine/level/format/io/region/LegacyAnvilChunkTrait.php @@ -28,12 +28,10 @@ use pocketmine\level\format\io\ChunkUtils; use pocketmine\level\format\io\exception\CorruptedChunkException; use pocketmine\level\format\SubChunk; use pocketmine\nbt\BigEndianNbtSerializer; -use pocketmine\nbt\NBT; use pocketmine\nbt\NbtDataException; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\IntArrayTag; use pocketmine\nbt\tag\ListTag; -use function array_fill; /** * Trait containing I/O methods for handling legacy Anvil-style chunks. @@ -49,42 +47,9 @@ use function array_fill; trait LegacyAnvilChunkTrait{ protected function serializeChunk(Chunk $chunk) : string{ - $nbt = new CompoundTag("Level", []); - $nbt->setInt("xPos", $chunk->getX()); - $nbt->setInt("zPos", $chunk->getZ()); - - $nbt->setByte("V", 1); - $nbt->setLong("LastUpdate", 0); //TODO - $nbt->setLong("InhabitedTime", 0); //TODO - $nbt->setByte("TerrainPopulated", $chunk->isPopulated() ? 1 : 0); - $nbt->setByte("LightPopulated", 0); - - $subChunks = []; - foreach($chunk->getSubChunks() as $y => $subChunk){ - if($subChunk->isEmpty()){ - continue; - } - - $tag = $this->serializeSubChunk($subChunk); - $tag->setByte("Y", $y); - $subChunks[] = $tag; - } - $nbt->setTag(new ListTag("Sections", $subChunks, NBT::TAG_Compound)); - - $nbt->setByteArray("Biomes", $chunk->getBiomeIdArray()); - $nbt->setIntArray("HeightMap", array_fill(0, 256, 0)); - - $nbt->setTag(new ListTag("Entities", $chunk->getNBTentities(), NBT::TAG_Compound)); - $nbt->setTag(new ListTag("TileEntities", $chunk->getNBTtiles(), NBT::TAG_Compound)); - - //TODO: TileTicks - - $writer = new BigEndianNbtSerializer(); - return $writer->writeCompressed(new CompoundTag("", [$nbt]), ZLIB_ENCODING_DEFLATE, RegionLoader::$COMPRESSION_LEVEL); + throw new \RuntimeException("Unsupported"); } - abstract protected function serializeSubChunk(SubChunk $subChunk) : CompoundTag; - /** * @param string $data * diff --git a/src/pocketmine/level/format/io/region/McRegion.php b/src/pocketmine/level/format/io/region/McRegion.php index 4059a4895..c4a81aa14 100644 --- a/src/pocketmine/level/format/io/region/McRegion.php +++ b/src/pocketmine/level/format/io/region/McRegion.php @@ -26,6 +26,7 @@ namespace pocketmine\level\format\io\region; use pocketmine\level\format\Chunk; use pocketmine\level\format\io\ChunkUtils; use pocketmine\level\format\io\exception\CorruptedChunkException; +use pocketmine\level\format\io\SubChunkConverter; use pocketmine\level\format\SubChunk; use pocketmine\nbt\BigEndianNbtSerializer; use pocketmine\nbt\NbtDataException; @@ -33,7 +34,6 @@ use pocketmine\nbt\tag\ByteArrayTag; use pocketmine\nbt\tag\IntArrayTag; use pocketmine\nbt\tag\ListTag; use function str_repeat; -use function substr; class McRegion extends RegionLevelProvider{ @@ -71,19 +71,7 @@ class McRegion extends RegionLevelProvider{ $fullData = $chunk->hasTag("Data", ByteArrayTag::class) ? $chunk->getByteArray("Data") : str_repeat("\x00", 16384); for($y = 0; $y < 8; ++$y){ - $offset = ($y << 4); - $ids = ""; - for($i = 0; $i < 256; ++$i){ - $ids .= substr($fullIds, $offset, 16); - $offset += 128; - } - $data = ""; - $offset = ($y << 3); - for($i = 0; $i < 256; ++$i){ - $data .= substr($fullData, $offset, 8); - $offset += 64; - } - $subChunks[$y] = new SubChunk($ids, $data); + $subChunks[$y] = new SubChunk([SubChunkConverter::convertSubChunkFromLegacyColumn($fullIds, $fullData, $y)]); } if($chunk->hasTag("BiomeColors", IntArrayTag::class)){ diff --git a/src/pocketmine/level/format/io/region/PMAnvil.php b/src/pocketmine/level/format/io/region/PMAnvil.php index a1b1f46e4..c726ea6d4 100644 --- a/src/pocketmine/level/format/io/region/PMAnvil.php +++ b/src/pocketmine/level/format/io/region/PMAnvil.php @@ -23,33 +23,19 @@ declare(strict_types=1); namespace pocketmine\level\format\io\region; -use pocketmine\level\format\io\WritableLevelProvider; +use pocketmine\level\format\io\SubChunkConverter; use pocketmine\level\format\SubChunk; -use pocketmine\nbt\tag\ByteArrayTag; use pocketmine\nbt\tag\CompoundTag; -use function str_repeat; /** * This format is exactly the same as the PC Anvil format, with the only difference being that the stored data order * is XZY instead of YZX for more performance loading and saving worlds. */ -class PMAnvil extends RegionLevelProvider implements WritableLevelProvider{ +class PMAnvil extends RegionLevelProvider{ use LegacyAnvilChunkTrait; - protected function serializeSubChunk(SubChunk $subChunk) : CompoundTag{ - return new CompoundTag("", [ - new ByteArrayTag("Blocks", $subChunk->getBlockIdArray()), - new ByteArrayTag("Data", $subChunk->getBlockDataArray()), - new ByteArrayTag("SkyLight", str_repeat("\x00", 2048)), - new ByteArrayTag("BlockLight", str_repeat("\x00", 2048)) - ]); - } - protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk{ - return new SubChunk( - $subChunk->getByteArray("Blocks"), - $subChunk->getByteArray("Data") - ); + return new SubChunk([SubChunkConverter::convertSubChunkXZY($subChunk->getByteArray("Blocks"), $subChunk->getByteArray("Data"))]); } protected static function getRegionFileExtension() : string{ diff --git a/src/pocketmine/level/generator/populator/GroundCover.php b/src/pocketmine/level/generator/populator/GroundCover.php index 6127d66a0..84e0405c8 100644 --- a/src/pocketmine/level/generator/populator/GroundCover.php +++ b/src/pocketmine/level/generator/populator/GroundCover.php @@ -47,8 +47,7 @@ class GroundCover extends Populator{ } for($y = 127; $y > 0; --$y){ - $id = $chunk->getBlockId($x, $y, $z); - if($id !== Block::AIR and !BlockFactory::get($id)->isTransparent()){ + if(!BlockFactory::fromFullBlock($chunk->getFullBlock($x, $y, $z))->isTransparent()){ break; } } @@ -56,11 +55,11 @@ class GroundCover extends Populator{ $endY = $startY - count($cover); for($y = $startY; $y > $endY and $y >= 0; --$y){ $b = $cover[$startY - $y]; - $id = $chunk->getBlockId($x, $y, $z); - if($id === Block::AIR and $b->isSolid()){ + $id = BlockFactory::fromFullBlock($chunk->getFullBlock($x, $y, $z)); + if($id->getId() === Block::AIR and $b->isSolid()){ break; } - if($b->canBeFlowedInto() and BlockFactory::get($id) instanceof Liquid){ + if($b->canBeFlowedInto() and $id instanceof Liquid){ continue; }