diff --git a/src/network/mcpe/serializer/ChunkSerializer.php b/src/network/mcpe/serializer/ChunkSerializer.php index 9120f34a7..855bcd24e 100644 --- a/src/network/mcpe/serializer/ChunkSerializer.php +++ b/src/network/mcpe/serializer/ChunkSerializer.php @@ -112,7 +112,15 @@ final class ChunkSerializer{ } public static function serializeSubChunk(SubChunk $subChunk, BlockTranslator $blockTranslator, PacketSerializer $stream, bool $persistentBlockStates) : void{ - $layers = $subChunk->getBlockLayers(); + // Create array from the new methods to minimize code changes + $layers = []; + if(($blockLayer = $subChunk->getBlockLayer()) !== null){ + $layers[] = $blockLayer; + } + if(($liquidLayer = $subChunk->getLiquidLayer()) !== null){ + $layers[] = $liquidLayer; + } + $stream->putByte(8); //version $stream->putByte(count($layers)); diff --git a/src/world/format/Chunk.php b/src/world/format/Chunk.php index 9ea5d3f8e..b72d89492 100644 --- a/src/world/format/Chunk.php +++ b/src/world/format/Chunk.php @@ -73,7 +73,7 @@ class Chunk{ foreach($this->subChunks as $y => $null){ //TODO: we should probably require all subchunks to be provided here - $this->subChunks[$y] = $subChunks[$y + self::MIN_SUBCHUNK_INDEX] ?? new SubChunk(Block::EMPTY_STATE_ID, [], new PalettedBlockArray(BiomeIds::OCEAN)); + $this->subChunks[$y] = $subChunks[$y + self::MIN_SUBCHUNK_INDEX] ?? new SubChunk(Block::EMPTY_STATE_ID, null, null, new PalettedBlockArray(BiomeIds::OCEAN)); } $val = (self::MAX_SUBCHUNK_INDEX + 1) * SubChunk::EDGE_LENGTH; @@ -298,7 +298,7 @@ class Chunk{ throw new \InvalidArgumentException("Invalid subchunk Y coordinate $y"); } - $this->subChunks[$y - self::MIN_SUBCHUNK_INDEX] = $subChunk ?? new SubChunk(Block::EMPTY_STATE_ID, [], new PalettedBlockArray(BiomeIds::OCEAN)); + $this->subChunks[$y - self::MIN_SUBCHUNK_INDEX] = $subChunk ?? new SubChunk(Block::EMPTY_STATE_ID, null, null, new PalettedBlockArray(BiomeIds::OCEAN)); $this->terrainDirtyFlags |= self::DIRTY_FLAG_BLOCKS; } diff --git a/src/world/format/SubChunk.php b/src/world/format/SubChunk.php index cc6673430..e4653ccdb 100644 --- a/src/world/format/SubChunk.php +++ b/src/world/format/SubChunk.php @@ -33,13 +33,11 @@ class SubChunk{ /** * SubChunk constructor. - * - * @param PalettedBlockArray[] $blockLayers - * @phpstan-param list $blockLayers */ public function __construct( private int $emptyBlockId, - private array $blockLayers, + private ?PalettedBlockArray $blockLayer, + private ?PalettedBlockArray $liquidLayer, private PalettedBlockArray $biomes, private ?LightArray $skyLight = null, private ?LightArray $blockLight = null @@ -60,7 +58,7 @@ class SubChunk{ * This may report non-empty erroneously if the chunk has been modified and not garbage-collected. */ public function isEmptyFast() : bool{ - return count($this->blockLayers) === 0; + return $this->blockLayer === null && $this->liquidLayer === null; } /** @@ -70,33 +68,57 @@ class SubChunk{ public function getEmptyBlockId() : int{ return $this->emptyBlockId; } public function getBlockStateId(int $x, int $y, int $z) : int{ - if(count($this->blockLayers) === 0){ + if($this->blockLayer === null){ return $this->emptyBlockId; } - return $this->blockLayers[0]->get($x, $y, $z); + return $this->blockLayer->get($x, $y, $z); } public function setBlockStateId(int $x, int $y, int $z, int $block) : void{ - if(count($this->blockLayers) === 0){ - $this->blockLayers[] = new PalettedBlockArray($this->emptyBlockId); + if($this->blockLayer === null){ + $this->blockLayer = new PalettedBlockArray($this->emptyBlockId); } - $this->blockLayers[0]->set($x, $y, $z, $block); + $this->blockLayer->set($x, $y, $z, $block); } /** + * @deprecated Use getBlockLayer() and getLiquidLayer() instead * @return PalettedBlockArray[] * @phpstan-return list */ public function getBlockLayers() : array{ - return $this->blockLayers; + $layers = []; + if($this->blockLayer !== null){ + $layers[] = $this->blockLayer; + } + if($this->liquidLayer !== null){ + $layers[] = $this->liquidLayer; + } + return $layers; + } + + public function getBlockLayer() : ?PalettedBlockArray{ + return $this->blockLayer; + } + + public function setBlockLayer(?PalettedBlockArray $blockLayer) : void{ + $this->blockLayer = $blockLayer; + } + + public function getLiquidLayer() : ?PalettedBlockArray{ + return $this->liquidLayer; + } + + public function setLiquidLayer(?PalettedBlockArray $liquidLayer) : void{ + $this->liquidLayer = $liquidLayer; } public function getHighestBlockAt(int $x, int $z) : ?int{ - if(count($this->blockLayers) === 0){ + if($this->blockLayer === null){ return null; } for($y = self::EDGE_LENGTH - 1; $y >= 0; --$y){ - if($this->blockLayers[0]->get($x, $y, $z) !== $this->emptyBlockId){ + if($this->blockLayer->get($x, $y, $z) !== $this->emptyBlockId){ return $y; } } @@ -130,15 +152,18 @@ class SubChunk{ } public function collectGarbage() : void{ - $cleanedLayers = []; - foreach($this->blockLayers as $layer){ - $layer->collectGarbage(); - - if($layer->getBitsPerBlock() !== 0 || $layer->get(0, 0, 0) !== $this->emptyBlockId){ - $cleanedLayers[] = $layer; + if($this->blockLayer !== null){ + $this->blockLayer->collectGarbage(); + if($this->blockLayer->getBitsPerBlock() === 0 && $this->blockLayer->get(0, 0, 0) === $this->emptyBlockId){ + $this->blockLayer = null; + } + } + if($this->liquidLayer !== null){ + $this->liquidLayer->collectGarbage(); + if($this->liquidLayer->getBitsPerBlock() === 0 && $this->liquidLayer->get(0, 0, 0) === $this->emptyBlockId){ + $this->liquidLayer = null; } } - $this->blockLayers = $cleanedLayers; $this->biomes->collectGarbage(); if($this->skyLight !== null && $this->skyLight->isUniform(0)){ @@ -150,9 +175,12 @@ class SubChunk{ } public function __clone(){ - $this->blockLayers = array_map(function(PalettedBlockArray $array) : PalettedBlockArray{ - return clone $array; - }, $this->blockLayers); + if($this->blockLayer !== null){ + $this->blockLayer = clone $this->blockLayer; + } + if($this->liquidLayer !== null){ + $this->liquidLayer = clone $this->liquidLayer; + } $this->biomes = clone $this->biomes; if($this->skyLight !== null){ diff --git a/src/world/format/io/FastChunkSerializer.php b/src/world/format/io/FastChunkSerializer.php index 35a8ff42f..10a316d36 100644 --- a/src/world/format/io/FastChunkSerializer.php +++ b/src/world/format/io/FastChunkSerializer.php @@ -74,11 +74,21 @@ final class FastChunkSerializer{ foreach($subChunks as $y => $subChunk){ $stream->putByte($y); $stream->putInt($subChunk->getEmptyBlockId()); - $layers = $subChunk->getBlockLayers(); - $stream->putByte(count($layers)); - foreach($layers as $blocks){ - self::serializePalettedArray($stream, $blocks); + + // Write block layer presence and data + $blockLayer = $subChunk->getBlockLayer(); + $stream->putByte($blockLayer !== null ? 1 : 0); + if($blockLayer !== null){ + self::serializePalettedArray($stream, $blockLayer); } + + // Write liquid layer presence and data + $liquidLayer = $subChunk->getLiquidLayer(); + $stream->putByte($liquidLayer !== null ? 1 : 0); + if($liquidLayer !== null){ + self::serializePalettedArray($stream, $liquidLayer); + } + self::serializePalettedArray($stream, $subChunk->getBiomeArray()); } @@ -112,12 +122,20 @@ final class FastChunkSerializer{ $y = Binary::signByte($stream->getByte()); $airBlockId = $stream->getInt(); - $layers = []; - for($i = 0, $layerCount = $stream->getByte(); $i < $layerCount; ++$i){ - $layers[] = self::deserializePalettedArray($stream); + // Read block layer + $blockLayer = null; + if($stream->getByte() !== 0){ + $blockLayer = self::deserializePalettedArray($stream); } + + // Read liquid layer + $liquidLayer = null; + if($stream->getByte() !== 0){ + $liquidLayer = self::deserializePalettedArray($stream); + } + $biomeArray = self::deserializePalettedArray($stream); - $subChunks[$y] = new SubChunk($airBlockId, $layers, $biomeArray); + $subChunks[$y] = new SubChunk($airBlockId, $blockLayer, $liquidLayer, $biomeArray); } return new Chunk($subChunks, $terrainPopulated); diff --git a/src/world/format/io/leveldb/LevelDB.php b/src/world/format/io/leveldb/LevelDB.php index 3a64f93f6..8f28c3231 100644 --- a/src/world/format/io/leveldb/LevelDB.php +++ b/src/world/format/io/leveldb/LevelDB.php @@ -462,17 +462,15 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ $subChunks = []; for($yy = 0; $yy < 8; ++$yy){ - $storages = [$this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $yy, new \PrefixedLogger($logger, "Subchunk y=$yy"))]; - if(isset($convertedLegacyExtraData[$yy])){ - $storages[] = $convertedLegacyExtraData[$yy]; - } - $subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, $storages, clone $biomes3d); + $blockLayer = $this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $yy, new \PrefixedLogger($logger, "Subchunk y=$yy")); + $liquidLayer = $convertedLegacyExtraData[$yy] ?? null; + $subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, $blockLayer, $liquidLayer, clone $biomes3d); } //make sure extrapolated biomes get filled in correctly for($yy = Chunk::MIN_SUBCHUNK_INDEX; $yy <= Chunk::MAX_SUBCHUNK_INDEX; ++$yy){ if(!isset($subChunks[$yy])){ - $subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, [], clone $biomes3d); + $subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, null, null, clone $biomes3d); } } @@ -501,12 +499,10 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ } } - $storages = [$this->palettizeLegacySubChunkXZY($blocks, $blockData, $logger)]; - if($convertedLegacyExtraData !== null){ - $storages[] = $convertedLegacyExtraData; - } + $blockLayer = $this->palettizeLegacySubChunkXZY($blocks, $blockData, $logger); + $liquidLayer = $convertedLegacyExtraData; - return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette); + return new SubChunk(Block::EMPTY_STATE_ID, $blockLayer, $liquidLayer, $biomePalette); } /** @@ -526,11 +522,9 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ case SubChunkVersion::CLASSIC_BUG_7: return $this->deserializeNonPalettedSubChunkData($binaryStream, $chunkVersion, $convertedLegacyExtraData, $biomePalette, $logger); case SubChunkVersion::PALETTED_SINGLE: - $storages = [$this->deserializeBlockPalette($binaryStream, $logger)]; - if($convertedLegacyExtraData !== null){ - $storages[] = $convertedLegacyExtraData; - } - return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette); + $blockLayer = $this->deserializeBlockPalette($binaryStream, $logger); + $liquidLayer = $convertedLegacyExtraData; + return new SubChunk(Block::EMPTY_STATE_ID, $blockLayer, $liquidLayer, $biomePalette); case SubChunkVersion::PALETTED_MULTI: case SubChunkVersion::PALETTED_MULTI_WITH_OFFSET: //legacy extradata layers intentionally ignored because they aren't supposed to exist in v8 @@ -541,11 +535,18 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ $binaryStream->getByte(); } - $storages = []; + $blockLayer = null; + $liquidLayer = null; for($k = 0; $k < $storageCount; ++$k){ - $storages[] = $this->deserializeBlockPalette($binaryStream, $logger); + $layer = $this->deserializeBlockPalette($binaryStream, $logger); + if($k === 0){ + $blockLayer = $layer; + }elseif($k === 1){ + $liquidLayer = $layer; + } + // Ignore additional layers beyond the first two } - return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette); + return new SubChunk(Block::EMPTY_STATE_ID, $blockLayer, $liquidLayer, $biomePalette); default: //this should never happen - an unsupported chunk appearing in a supported world is a sign of corruption throw new CorruptedChunkException("don't know how to decode LevelDB subchunk format version $subChunkVersion"); @@ -575,7 +576,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ $subChunkKeyOffset = self::hasOffsetCavesAndCliffsSubChunks($chunkVersion) ? self::CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET : 0; for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){ if(($data = $this->db->get($index . ChunkDataKey::SUBCHUNK . chr($y + $subChunkKeyOffset))) === false){ - $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, [], $biomeArrays[$y]); + $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, null, null, $biomeArrays[$y]); continue; } @@ -774,10 +775,20 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ $subStream = new BinaryStream(); $subStream->putByte(self::CURRENT_LEVEL_SUBCHUNK_VERSION); - $layers = $subChunk->getBlockLayers(); - $subStream->putByte(count($layers)); - foreach($layers as $blocks){ - $this->serializeBlockPalette($subStream, $blocks); + $layerCount = 0; + $blockLayer = $subChunk->getBlockLayer(); + $liquidLayer = $subChunk->getLiquidLayer(); + + if($blockLayer !== null) $layerCount++; + if($liquidLayer !== null) $layerCount++; + + $subStream->putByte($layerCount); + + if($blockLayer !== null){ + $this->serializeBlockPalette($subStream, $blockLayer); + } + if($liquidLayer !== null){ + $this->serializeBlockPalette($subStream, $liquidLayer); } $write->put($key, $subStream->getBuffer()); diff --git a/src/world/format/io/region/Anvil.php b/src/world/format/io/region/Anvil.php index 2c14b54e8..0e790975c 100644 --- a/src/world/format/io/region/Anvil.php +++ b/src/world/format/io/region/Anvil.php @@ -32,11 +32,12 @@ class Anvil extends RegionWorldProvider{ use LegacyAnvilChunkTrait; protected function deserializeSubChunk(CompoundTag $subChunk, PalettedBlockArray $biomes3d, \Logger $logger) : SubChunk{ - return new SubChunk(Block::EMPTY_STATE_ID, [$this->palettizeLegacySubChunkYZX( + $blockLayer = $this->palettizeLegacySubChunkYZX( self::readFixedSizeByteArray($subChunk, "Blocks", 4096), self::readFixedSizeByteArray($subChunk, "Data", 2048), $logger - )], $biomes3d); + ); + return new SubChunk(Block::EMPTY_STATE_ID, $blockLayer, null, $biomes3d); //ignore legacy light information } diff --git a/src/world/format/io/region/LegacyAnvilChunkTrait.php b/src/world/format/io/region/LegacyAnvilChunkTrait.php index 6e2f4c8f8..cfd3e496d 100644 --- a/src/world/format/io/region/LegacyAnvilChunkTrait.php +++ b/src/world/format/io/region/LegacyAnvilChunkTrait.php @@ -96,7 +96,7 @@ trait LegacyAnvilChunkTrait{ } for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){ if(!isset($subChunks[$y])){ - $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, [], clone $biomes3d); + $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, null, null, clone $biomes3d); } } diff --git a/src/world/format/io/region/McRegion.php b/src/world/format/io/region/McRegion.php index ad6a2d7f2..55c64f1a8 100644 --- a/src/world/format/io/region/McRegion.php +++ b/src/world/format/io/region/McRegion.php @@ -90,16 +90,17 @@ class McRegion extends RegionWorldProvider{ $fullData = self::readFixedSizeByteArray($chunk, "Data", 16384); for($y = 0; $y < 8; ++$y){ - $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, [$this->palettizeLegacySubChunkFromColumn( + $blockLayer = $this->palettizeLegacySubChunkFromColumn( $fullIds, $fullData, $y, new \PrefixedLogger($logger, "Subchunk y=$y"), - )], clone $biomes3d); + ); + $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, $blockLayer, null, clone $biomes3d); } for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){ if(!isset($subChunks[$y])){ - $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, [], clone $biomes3d); + $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, null, null, clone $biomes3d); } } diff --git a/src/world/format/io/region/PMAnvil.php b/src/world/format/io/region/PMAnvil.php index 4fb463124..bb5154306 100644 --- a/src/world/format/io/region/PMAnvil.php +++ b/src/world/format/io/region/PMAnvil.php @@ -36,11 +36,12 @@ class PMAnvil extends RegionWorldProvider{ use LegacyAnvilChunkTrait; protected function deserializeSubChunk(CompoundTag $subChunk, PalettedBlockArray $biomes3d, \Logger $logger) : SubChunk{ - return new SubChunk(Block::EMPTY_STATE_ID, [$this->palettizeLegacySubChunkXZY( + $blockLayer = $this->palettizeLegacySubChunkXZY( self::readFixedSizeByteArray($subChunk, "Blocks", 4096), self::readFixedSizeByteArray($subChunk, "Data", 2048), $logger - )], $biomes3d); + ); + return new SubChunk(Block::EMPTY_STATE_ID, $blockLayer, null, $biomes3d); } protected static function getRegionFileExtension() : string{ diff --git a/src/world/light/BlockLightUpdate.php b/src/world/light/BlockLightUpdate.php index b7e0aa9e3..06efa6abf 100644 --- a/src/world/light/BlockLightUpdate.php +++ b/src/world/light/BlockLightUpdate.php @@ -65,11 +65,29 @@ class BlockLightUpdate extends LightUpdate{ foreach($chunk->getSubChunks() as $subChunkY => $subChunk){ $subChunk->setBlockLightArray(LightArray::fill(0)); - foreach($subChunk->getBlockLayers() as $layer){ - foreach($layer->getPalette() as $state){ + $foundLightEmitter = false; + + // Check block layer for light emitters + $blockLayer = $subChunk->getBlockLayer(); + if($blockLayer !== null){ + foreach($blockLayer->getPalette() as $state){ if(($this->lightEmitters[$state] ?? 0) > 0){ $lightSources += $this->scanForLightEmittingBlocks($subChunk, $chunkX << SubChunk::COORD_BIT_SIZE, $subChunkY << SubChunk::COORD_BIT_SIZE, $chunkZ << SubChunk::COORD_BIT_SIZE); - break 2; + $foundLightEmitter = true; + break; + } + } + } + + // Check liquid layer for light emitters if not found in block layer + if(!$foundLightEmitter){ + $liquidLayer = $subChunk->getLiquidLayer(); + if($liquidLayer !== null){ + foreach($liquidLayer->getPalette() as $state){ + if(($this->lightEmitters[$state] ?? 0) > 0){ + $lightSources += $this->scanForLightEmittingBlocks($subChunk, $chunkX << SubChunk::COORD_BIT_SIZE, $subChunkY << SubChunk::COORD_BIT_SIZE, $chunkZ << SubChunk::COORD_BIT_SIZE); + break; + } } } } diff --git a/tests/phpunit/world/format/SubChunkTest.php b/tests/phpunit/world/format/SubChunkTest.php index cdb440147..9d8625ff7 100644 --- a/tests/phpunit/world/format/SubChunkTest.php +++ b/tests/phpunit/world/format/SubChunkTest.php @@ -32,7 +32,7 @@ class SubChunkTest extends TestCase{ * Test that a cloned SubChunk instance doesn't influence the original */ public function testClone() : void{ - $sub1 = new SubChunk(0, [], new PalettedBlockArray(BiomeIds::OCEAN)); + $sub1 = new SubChunk(0, null, null, new PalettedBlockArray(BiomeIds::OCEAN)); $sub1->setBlockStateId(0, 0, 0, 1); $sub1->getBlockLightArray()->set(0, 0, 0, 1);