From 8005c746813a3e5dda8287fda2cc1c99ed7ad535 Mon Sep 17 00:00:00 2001 From: "Dylan T." Date: Thu, 26 Jun 2025 00:00:45 +0000 Subject: [PATCH] Refactor SubChunk to use separate block and liquid layer fields Replace blockLayers array with individual blockLayer and liquidLayer fields. Update constructor and all usages throughout the codebase. Deprecate getBlockLayers() method for backward compatibility. --- .../mcpe/serializer/ChunkSerializer.php | 10 ++- src/world/format/Chunk.php | 4 +- src/world/format/SubChunk.php | 74 +++++++++++++------ src/world/format/io/FastChunkSerializer.php | 34 +++++++-- src/world/format/io/leveldb/LevelDB.php | 59 +++++++++------ src/world/format/io/region/Anvil.php | 5 +- .../io/region/LegacyAnvilChunkTrait.php | 2 +- src/world/format/io/region/McRegion.php | 7 +- src/world/format/io/region/PMAnvil.php | 5 +- src/world/light/BlockLightUpdate.php | 24 +++++- tests/phpunit/world/format/SubChunkTest.php | 2 +- 11 files changed, 156 insertions(+), 70 deletions(-) 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);