Compare commits

...

12 Commits

Author SHA1 Message Date
a0f5ad1669 Fix remaining usage of deprecated getBlockLayers() in World.php
- Replace getBlockLayers() with getBlockLayer() and getLiquidLayer()
- Ensures all block layers are validated when setting chunks
- Completes the migration away from the deprecated method
2025-06-27 16:30:40 +00:00
0f932f7cc0 Merge branch 'major-next' of https://github.com/pmmp/PocketMine-MP into block-layers-cleanup 2025-06-27 16:28:18 +00:00
f01d87474d Final cleanup: fix imports and indentation
- Remove unused imports (array_map, count) from SubChunk.php
- Fix indentation in ChunkSerializer.php serializeSubChunk method
- Fix indentation in FastChunkSerializer.php serialize/deserialize methods
- Ensure consistent tab-based indentation throughout all modified files
- Clean up whitespace and formatting issues
2025-06-27 16:25:17 +00:00
7472c5b73d Fix indentation in LevelDB saveChunk method
- Correct indentation for putByte and foreach loop calls
- Ensure consistent tab-based indentation throughout the method
2025-06-27 16:16:56 +00:00
941632bed9 Fix formatting issue in LevelDB.php
- Restore missing newline on line 782
- Ensure proper indentation for liquid layer check
2025-06-27 16:15:00 +00:00
e5a38f270c Clean up BlockLightUpdate::recalculateChunk
- Remove unnecessary null checks for blockLayer and liquidLayer (they are now non-nullable)
- Deduplicate code by scanning both layers in a single loop
- Simplify logic flow by checking palettes first before scanning blocks
2025-06-27 16:08:08 +00:00
7c651928ea Simplify LevelDB saveChunk using layers array pattern
- Use the same pattern as ChunkSerializer by building an array of non-empty layers
- Cleaner and more maintainable code with fewer changes from original
- Remove inline comment for cleaner appearance
2025-06-27 16:01:14 +00:00
48cc87d066 Remove deprecated and unused methods from SubChunk
- Remove deprecated getBlockLayers() method that was replaced by getBlockLayer() and getLiquidLayer()
- Remove setBlockLayer() and setLiquidLayer() setter methods as layers are now managed internally
- Simplifies the API and prevents external mutation of internal layer state
2025-06-27 15:58:10 +00:00
72781f4042 Add helper methods for layer emptiness checks and update serializers
- Add isBlockLayerEmpty() and isLiquidLayerEmpty() methods to SubChunk to avoid code duplication
- Update ChunkSerializer to use emptiness checks instead of null checks for layers
- Update LevelDB to use the new helper methods for cleaner code
- Ensures consistent emptiness checking logic across all serializers
2025-06-27 15:54:42 +00:00
30d13508b4 Update LevelDB to check for layer emptiness instead of null
- Check if layers are empty (bitsPerBlock == 0 and value == emptyBlockId) instead of null when saving subchunks
- This aligns with the refactored SubChunk class that now uses non-nullable blockLayer and liquidLayer fields
2025-06-27 15:49:43 +00:00
ba2a7cceaf Refactor SubChunk to use non-nullable block and liquid layers
- Changed SubChunk constructor to accept null for layers and initialize empty PalettedBlockArray internally
- Updated all SubChunk getters to return non-nullable PalettedBlockArray
- Modified serialization/deserialization logic to check for layer emptiness instead of null
- Updated all SubChunk constructor calls across codebase to pass null for empty layers
- Simplified code by removing unnecessary null checks throughout the codebase
- Updated tests to match new SubChunk API
2025-06-27 15:47:58 +00:00
8005c74681 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.
2025-06-26 00:00:45 +00:00
12 changed files with 121 additions and 92 deletions

View File

@ -112,7 +112,14 @@ final class ChunkSerializer{
}
public static function serializeSubChunk(SubChunk $subChunk, BlockTranslator $blockTranslator, PacketSerializer $stream, bool $persistentBlockStates) : void{
$layers = $subChunk->getBlockLayers();
$layers = [];
if(!$subChunk->isBlockLayerEmpty()){
$layers[] = $subChunk->getBlockLayer();
}
if(!$subChunk->isLiquidLayerEmpty()){
$layers[] = $subChunk->getLiquidLayer();
}
$stream->putByte(8); //version
$stream->putByte(count($layers));

View File

@ -2607,7 +2607,7 @@ class World implements ChunkManager{
public function setChunk(int $chunkX, int $chunkZ, Chunk $chunk) : void{
foreach($chunk->getSubChunks() as $subChunk){
foreach($subChunk->getBlockLayers() as $blockLayer){
foreach([$subChunk->getBlockLayer(), $subChunk->getLiquidLayer()] as $blockLayer){
foreach($blockLayer->getPalette() as $blockStateId){
if(!$this->blockStateRegistry->hasStateId($blockStateId)){
throw new \InvalidArgumentException("Provided chunk contains unknown/unregistered blocks (found unknown state ID $blockStateId)");

View File

@ -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;
}

View File

@ -23,27 +23,36 @@ declare(strict_types=1);
namespace pocketmine\world\format;
use function array_map;
use function count;
class SubChunk{
public const COORD_BIT_SIZE = 4;
public const COORD_MASK = ~(~0 << self::COORD_BIT_SIZE);
public const EDGE_LENGTH = 1 << self::COORD_BIT_SIZE;
private int $emptyBlockId;
private PalettedBlockArray $blockLayer;
private PalettedBlockArray $liquidLayer;
private PalettedBlockArray $biomes;
private ?LightArray $skyLight;
private ?LightArray $blockLight;
/**
* SubChunk constructor.
*
* @param PalettedBlockArray[] $blockLayers
* @phpstan-param list<PalettedBlockArray> $blockLayers
*/
public function __construct(
private int $emptyBlockId,
private array $blockLayers,
private PalettedBlockArray $biomes,
private ?LightArray $skyLight = null,
private ?LightArray $blockLight = null
){}
int $emptyBlockId,
?PalettedBlockArray $blockLayer,
?PalettedBlockArray $liquidLayer,
PalettedBlockArray $biomes,
?LightArray $skyLight = null,
?LightArray $blockLight = null
){
$this->emptyBlockId = $emptyBlockId;
$this->blockLayer = $blockLayer ?? new PalettedBlockArray($emptyBlockId);
$this->liquidLayer = $liquidLayer ?? new PalettedBlockArray($emptyBlockId);
$this->biomes = $biomes;
$this->skyLight = $skyLight;
$this->blockLight = $blockLight;
}
/**
* Returns whether this subchunk contains any non-air blocks.
@ -60,7 +69,22 @@ 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->getBitsPerBlock() === 0 && $this->blockLayer->get(0, 0, 0) === $this->emptyBlockId &&
$this->liquidLayer->getBitsPerBlock() === 0 && $this->liquidLayer->get(0, 0, 0) === $this->emptyBlockId;
}
/**
* Returns whether the block layer is empty (contains only empty blocks).
*/
public function isBlockLayerEmpty() : bool{
return $this->blockLayer->getBitsPerBlock() === 0 && $this->blockLayer->get(0, 0, 0) === $this->emptyBlockId;
}
/**
* Returns whether the liquid layer is empty (contains only empty blocks).
*/
public function isLiquidLayerEmpty() : bool{
return $this->liquidLayer->getBitsPerBlock() === 0 && $this->liquidLayer->get(0, 0, 0) === $this->emptyBlockId;
}
/**
@ -70,33 +94,24 @@ class SubChunk{
public function getEmptyBlockId() : int{ return $this->emptyBlockId; }
public function getBlockStateId(int $x, int $y, int $z) : int{
if(count($this->blockLayers) === 0){
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);
}
$this->blockLayers[0]->set($x, $y, $z, $block);
$this->blockLayer->set($x, $y, $z, $block);
}
/**
* @return PalettedBlockArray[]
* @phpstan-return list<PalettedBlockArray>
*/
public function getBlockLayers() : array{
return $this->blockLayers;
public function getBlockLayer() : PalettedBlockArray{
return $this->blockLayer;
}
public function getLiquidLayer() : PalettedBlockArray{
return $this->liquidLayer;
}
public function getHighestBlockAt(int $x, int $z) : ?int{
if(count($this->blockLayers) === 0){
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 +145,8 @@ 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;
}
}
$this->blockLayers = $cleanedLayers;
$this->blockLayer->collectGarbage();
$this->liquidLayer->collectGarbage();
$this->biomes->collectGarbage();
if($this->skyLight !== null && $this->skyLight->isUniform(0)){
@ -150,9 +158,8 @@ class SubChunk{
}
public function __clone(){
$this->blockLayers = array_map(function(PalettedBlockArray $array) : PalettedBlockArray{
return clone $array;
}, $this->blockLayers);
$this->blockLayer = clone $this->blockLayer;
$this->liquidLayer = clone $this->liquidLayer;
$this->biomes = clone $this->biomes;
if($this->skyLight !== null){

View File

@ -74,13 +74,11 @@ 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);
}
self::serializePalettedArray($stream, $subChunk->getBiomeArray());
// Write block and liquid layers (always present)
self::serializePalettedArray($stream, $subChunk->getBlockLayer());
self::serializePalettedArray($stream, $subChunk->getLiquidLayer());
self::serializePalettedArray($stream, $subChunk->getBiomeArray());
}
return $stream->getBuffer();
@ -112,12 +110,12 @@ 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 and liquid layers (always present)
$blockLayer = self::deserializePalettedArray($stream);
$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);

View File

@ -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,18 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$subStream = new BinaryStream();
$subStream->putByte(self::CURRENT_LEVEL_SUBCHUNK_VERSION);
$layers = $subChunk->getBlockLayers();
$layers = [];
if(!$subChunk->isBlockLayerEmpty()){
$layers[] = $subChunk->getBlockLayer();
}
if(!$subChunk->isLiquidLayerEmpty()){
$layers[] = $subChunk->getLiquidLayer();
}
$subStream->putByte(count($layers));
foreach($layers as $blocks){
$this->serializeBlockPalette($subStream, $blocks);
foreach($layers as $layer){
$this->serializeBlockPalette($subStream, $layer);
}
$write->put($key, $subStream->getBuffer());

View File

@ -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
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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{

View File

@ -65,14 +65,19 @@ class BlockLightUpdate extends LightUpdate{
foreach($chunk->getSubChunks() as $subChunkY => $subChunk){
$subChunk->setBlockLightArray(LightArray::fill(0));
foreach($subChunk->getBlockLayers() as $layer){
$hasLightEmitter = false;
foreach([$subChunk->getBlockLayer(), $subChunk->getLiquidLayer()] as $layer){
foreach($layer->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);
$hasLightEmitter = true;
break 2;
}
}
}
if($hasLightEmitter){
$lightSources += $this->scanForLightEmittingBlocks($subChunk, $chunkX << SubChunk::COORD_BIT_SIZE, $subChunkY << SubChunk::COORD_BIT_SIZE, $chunkZ << SubChunk::COORD_BIT_SIZE);
}
}
return $lightSources;

View File

@ -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);