Hit block legacy metadata with the biggest nuke you've ever seen

This commit completely revamps the way that blocks are represented in memory at runtime.

Instead of being represented by legacy Mojang block IDs and metadata, which are dated, limited and unchangeable, we now use custom PM block IDs, which are generated from VanillaBlocks.
This means we have full control of how they are assigned, which opens the doors to finally addressing inconsistencies like glazed terracotta, stripped logs handling, etc.

To represent state, BlockDataReader and BlockDataWriter have been introduced, and are used by blocks with state information to pack said information into a binary form that can be stored on a chunk at runtime.
Conceptually it's pretty similar to legacy metadata, but the actual format shares no resemblance whatsoever to legacy metadata, and is fully controlled by PM.
This means that the 'state data' may change in serialization format at any time, so it should **NOT** be stored on disk or in a config.

In the future, this will be improved using more auto-generated code and attributes, instead of hand-baked decodeState() and encodeState(). For now, this opens the gateway to a significant expansion of features.
It's not ideal, but it's a big step forwards.
This commit is contained in:
Dylan K. Taylor
2022-06-24 23:19:37 +01:00
parent be2fe160b3
commit f24f2d9ca9
149 changed files with 2234 additions and 2650 deletions

View File

@@ -48,7 +48,7 @@ class SimpleChunkManager implements ChunkManager{
public function setBlockAt(int $x, int $y, int $z, Block $block) : void{
if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){
$chunk->setFullBlock($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK, $block->getFullId());
$chunk->setFullBlock($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK, $block->getStateId());
}else{
throw new \InvalidArgumentException("Cannot set block at coordinates x=$x,y=$y,z=$z, terrain is not loaded or out of bounds");
}

View File

@@ -81,6 +81,7 @@ use pocketmine\world\biome\BiomeRegistry;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\io\ChunkData;
use pocketmine\world\format\io\exception\CorruptedChunkException;
use pocketmine\world\format\io\GlobalBlockStateHandlers;
use pocketmine\world\format\io\WritableWorldProvider;
use pocketmine\world\format\LightArray;
use pocketmine\world\format\SubChunk;
@@ -438,7 +439,12 @@ class World implements ChunkManager{
if($item !== null){
$block = $item->getBlock();
}elseif(preg_match("/^-?\d+$/", $name) === 1){
$block = BlockFactory::getInstance()->get((int) $name, 0);
//TODO: this may throw if the ID/meta was invalid
$blockStateData = GlobalBlockStateHandlers::getUpgrader()->upgradeIntIdMeta((int) $name, 0);
if($blockStateData === null){
continue;
}
$block = BlockFactory::getInstance()->fromFullBlock(GlobalBlockStateHandlers::getDeserializer()->deserialize($blockStateData));
}else{
//TODO: we probably ought to log an error here
continue;
@@ -452,7 +458,7 @@ class World implements ChunkManager{
foreach(BlockFactory::getInstance()->getAllKnownStates() as $state){
$dontTickName = $dontTickBlocks[$state->getTypeId()] ?? null;
if($dontTickName === null && $state->ticksRandomly()){
$this->randomTickBlocks[$state->getFullId()] = true;
$this->randomTickBlocks[$state->getStateId()] = true;
}
}
}
@@ -940,7 +946,7 @@ class World implements ChunkManager{
$blockPosition = BlockPosition::fromVector3($b);
$packets[] = UpdateBlockPacket::create(
$blockPosition,
$blockMapping->toRuntimeId($fullBlock->getFullId()),
$blockMapping->toRuntimeId($fullBlock->getStateId()),
UpdateBlockPacket::FLAG_NETWORK,
UpdateBlockPacket::DATA_LAYER_NORMAL
);
@@ -980,11 +986,11 @@ class World implements ChunkManager{
if($block instanceof UnknownBlock){
throw new \InvalidArgumentException("Cannot do random-tick on unknown block");
}
$this->randomTickBlocks[$block->getFullId()] = true;
$this->randomTickBlocks[$block->getStateId()] = true;
}
public function removeRandomTickedBlock(Block $block) : void{
unset($this->randomTickBlocks[$block->getFullId()]);
unset($this->randomTickBlocks[$block->getStateId()]);
}
private function tickChunks() : void{
@@ -2455,26 +2461,6 @@ class World implements ChunkManager{
private function initChunk(int $chunkX, int $chunkZ, ChunkData $chunkData) : void{
$logger = new \PrefixedLogger($this->logger, "Loading chunk $chunkX $chunkZ");
$this->timings->syncChunkLoadFixInvalidBlocks->startTiming();
$blockFactory = BlockFactory::getInstance();
$invalidBlocks = 0;
foreach($chunkData->getChunk()->getSubChunks() as $subChunk){
foreach($subChunk->getBlockLayers() as $blockLayer){
foreach($blockLayer->getPalette() as $blockStateId){
$mappedStateId = $blockFactory->getMappedStateId($blockStateId);
if($mappedStateId !== $blockStateId){
$blockLayer->replaceAll($blockStateId, $mappedStateId);
$invalidBlocks++;
}
}
}
}
if($invalidBlocks > 0){
$logger->debug("Fixed $invalidBlocks invalid blockstates");
$chunkData->getChunk()->setTerrainDirtyFlag(Chunk::DIRTY_FLAG_BLOCKS, true);
}
$this->timings->syncChunkLoadFixInvalidBlocks->stopTiming();
if(count($chunkData->getEntityNBT()) !== 0){
$this->timings->syncChunkLoadEntities->startTiming();
$entityFactory = EntityFactory::getInstance();

View File

@@ -27,7 +27,7 @@ declare(strict_types=1);
namespace pocketmine\world\format;
use pocketmine\block\Block;
use pocketmine\block\BlockLegacyIds;
use pocketmine\block\BlockTypeIds;
use pocketmine\block\tile\Tile;
use function array_map;
@@ -68,7 +68,7 @@ class Chunk{
$this->subChunks = new \SplFixedArray(Chunk::MAX_SUBCHUNKS);
foreach($this->subChunks as $y => $null){
$this->subChunks[$y] = $subChunks[$y + self::MIN_SUBCHUNK_INDEX] ?? new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, []);
$this->subChunks[$y] = $subChunks[$y + self::MIN_SUBCHUNK_INDEX] ?? new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, []);
}
$val = (self::MAX_SUBCHUNK_INDEX + 1) * SubChunk::EDGE_LENGTH;
@@ -291,7 +291,7 @@ class Chunk{
throw new \InvalidArgumentException("Invalid subchunk Y coordinate $y");
}
$this->subChunks[$y - self::MIN_SUBCHUNK_INDEX] = $subChunk ?? new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, []);
$this->subChunks[$y - self::MIN_SUBCHUNK_INDEX] = $subChunk ?? new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, []);
$this->setTerrainDirtyFlag(self::DIRTY_FLAG_BLOCKS, true);
}

View File

@@ -48,11 +48,11 @@ use const pocketmine\BEDROCK_BLOCK_UPGRADE_SCHEMA_PATH;
*/
final class GlobalBlockStateHandlers{
private static ?BlockStateSerializer $blockStateSerializer;
private static ?BlockStateSerializer $blockStateSerializer = null;
private static ?BlockStateDeserializer $blockStateDeserializer;
private static ?BlockStateDeserializer $blockStateDeserializer = null;
private static ?BlockDataUpgrader $blockDataUpgrader;
private static ?BlockDataUpgrader $blockDataUpgrader = null;
public static function getDeserializer() : BlockStateDeserializer{
return self::$blockStateDeserializer ??= new CachingBlockStateDeserializer(new BlockStateToBlockObjectDeserializer());
@@ -63,18 +63,24 @@ final class GlobalBlockStateHandlers{
}
public static function getUpgrader() : BlockDataUpgrader{
return self::$blockDataUpgrader ??= new BlockDataUpgrader(
LegacyBlockStateMapper::loadFromString(
ErrorToExceptionHandler::trapAndRemoveFalse(fn() => file_get_contents(Path::join(
BEDROCK_BLOCK_UPGRADE_SCHEMA_PATH,
'1.12.0_to_1.18.10_blockstate_map.bin'
))),
LegacyBlockIdToStringIdMap::getInstance()
),
new BlockStateUpgrader(BlockStateUpgradeSchemaUtils::loadSchemas(
if(self::$blockDataUpgrader === null){
$blockStateUpgrader = new BlockStateUpgrader(BlockStateUpgradeSchemaUtils::loadSchemas(
Path::join(BEDROCK_BLOCK_UPGRADE_SCHEMA_PATH, 'nbt_upgrade_schema'),
BlockStateData::CURRENT_VERSION
))
);
));
self::$blockDataUpgrader = new BlockDataUpgrader(
LegacyBlockStateMapper::loadFromString(
ErrorToExceptionHandler::trapAndRemoveFalse(fn() => file_get_contents(Path::join(
BEDROCK_BLOCK_UPGRADE_SCHEMA_PATH,
'1.12.0_to_1.18.10_blockstate_map.bin'
))),
LegacyBlockIdToStringIdMap::getInstance(),
$blockStateUpgrader
),
$blockStateUpgrader
);
}
return self::$blockDataUpgrader;
}
}

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\world\format\io\leveldb;
use pocketmine\block\Block;
use pocketmine\block\BlockLegacyIds;
use pocketmine\block\BlockTypeIds;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\data\bedrock\block\BlockStateDeserializeException;
@@ -238,7 +238,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$blockStateId = $blockStateDeserializer->deserialize($blockStateData);
if(!isset($extraDataLayers[$ySub])){
$extraDataLayers[$ySub] = new PalettedBlockArray(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS);
$extraDataLayers[$ySub] = new PalettedBlockArray(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS);
}
$extraDataLayers[$ySub]->set($x, $y, $z, $blockStateId);
}
@@ -367,14 +367,14 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$storages[] = $convertedLegacyExtraData[$y];
}
$subChunks[$y] = new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, $storages);
$subChunks[$y] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, $storages);
break;
case SubChunkVersion::PALETTED_SINGLE:
$storages = [$this->deserializePaletted($binaryStream)];
if(isset($convertedLegacyExtraData[$y])){
$storages[] = $convertedLegacyExtraData[$y];
}
$subChunks[$y] = new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, $storages);
$subChunks[$y] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, $storages);
break;
case SubChunkVersion::PALETTED_MULTI:
case SubChunkVersion::PALETTED_MULTI_WITH_OFFSET:
@@ -390,7 +390,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
for($k = 0; $k < $storageCount; ++$k){
$storages[] = $this->deserializePaletted($binaryStream);
}
$subChunks[$y] = new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, $storages);
$subChunks[$y] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, $storages);
}
break;
default:
@@ -433,7 +433,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
if(isset($convertedLegacyExtraData[$yy])){
$storages[] = $convertedLegacyExtraData[$yy];
}
$subChunks[$yy] = new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, $storages);
$subChunks[$yy] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, $storages);
}
try{

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\world\format\io\region;
use pocketmine\block\Block;
use pocketmine\block\BlockLegacyIds;
use pocketmine\block\BlockTypeIds;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\world\format\SubChunk;
@@ -32,7 +32,7 @@ class Anvil extends RegionWorldProvider{
use LegacyAnvilChunkTrait;
protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk{
return new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, [$this->palettizeLegacySubChunkYZX(
return new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, [$this->palettizeLegacySubChunkYZX(
self::readFixedSizeByteArray($subChunk, "Blocks", 4096),
self::readFixedSizeByteArray($subChunk, "Data", 2048)
)]);

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\world\format\io\region;
use pocketmine\block\Block;
use pocketmine\block\BlockLegacyIds;
use pocketmine\block\BlockTypeIds;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\nbt\BigEndianNbtSerializer;
use pocketmine\nbt\NbtDataException;
@@ -74,7 +74,7 @@ class McRegion extends RegionWorldProvider{
$fullData = self::readFixedSizeByteArray($chunk, "Data", 16384);
for($y = 0; $y < 8; ++$y){
$subChunks[$y] = new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, [$this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $y)]);
$subChunks[$y] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, [$this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $y)]);
}
$makeBiomeArray = function(string $biomeIds) : BiomeArray{

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\world\format\io\region;
use pocketmine\block\Block;
use pocketmine\block\BlockLegacyIds;
use pocketmine\block\BlockTypeIds;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\world\format\SubChunk;
@@ -36,7 +36,7 @@ class PMAnvil extends RegionWorldProvider{
use LegacyAnvilChunkTrait;
protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk{
return new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, [$this->palettizeLegacySubChunkXZY(
return new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, [$this->palettizeLegacySubChunkXZY(
self::readFixedSizeByteArray($subChunk, "Blocks", 4096),
self::readFixedSizeByteArray($subChunk, "Data", 2048)
)]);

View File

@@ -87,7 +87,7 @@ final class FlatGeneratorOptions{
throw new InvalidGeneratorOptionsException("Invalid preset layer \"$line\": " . $e->getMessage(), 0, $e);
}
for($cY = $y, $y += $cnt; $cY < $y; ++$cY){
$result[$cY] = $b->getFullId();
$result[$cY] = $b->getStateId();
}
}

View File

@@ -72,9 +72,9 @@ class Nether extends Generator{
$chunk = $world->getChunk($chunkX, $chunkZ);
$bedrock = VanillaBlocks::BEDROCK()->getFullId();
$netherrack = VanillaBlocks::NETHERRACK()->getFullId();
$stillLava = VanillaBlocks::LAVA()->getFullId();
$bedrock = VanillaBlocks::BEDROCK()->getStateId();
$netherrack = VanillaBlocks::NETHERRACK()->getStateId();
$stillLava = VanillaBlocks::LAVA()->getStateId();
for($x = 0; $x < Chunk::EDGE_LENGTH; ++$x){
for($z = 0; $z < Chunk::EDGE_LENGTH; ++$z){

View File

@@ -144,9 +144,9 @@ class Normal extends Generator{
$biomeCache = [];
$bedrock = VanillaBlocks::BEDROCK()->getFullId();
$stillWater = VanillaBlocks::WATER()->getFullId();
$stone = VanillaBlocks::STONE()->getFullId();
$bedrock = VanillaBlocks::BEDROCK()->getStateId();
$stillWater = VanillaBlocks::WATER()->getStateId();
$stone = VanillaBlocks::STONE()->getStateId();
$baseX = $chunkX * Chunk::EDGE_LENGTH;
$baseZ = $chunkZ * Chunk::EDGE_LENGTH;

View File

@@ -67,7 +67,7 @@ class GroundCover implements Populator{
continue;
}
$chunk->setFullBlock($x, $y, $z, $b->getFullId());
$chunk->setFullBlock($x, $y, $z, $b->getStateId());
}
}
}

View File

@@ -34,6 +34,6 @@ class BlockBreakParticle implements Particle{
public function __construct(private Block $b){}
public function encode(Vector3 $pos) : array{
return [LevelEventPacket::create(LevelEvent::PARTICLE_DESTROY, RuntimeBlockMapping::getInstance()->toRuntimeId($this->b->getFullId()), $pos)];
return [LevelEventPacket::create(LevelEvent::PARTICLE_DESTROY, RuntimeBlockMapping::getInstance()->toRuntimeId($this->b->getStateId()), $pos)];
}
}

View File

@@ -39,6 +39,6 @@ class BlockPunchParticle implements Particle{
){}
public function encode(Vector3 $pos) : array{
return [LevelEventPacket::create(LevelEvent::PARTICLE_PUNCH_BLOCK, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()) | ($this->face << 24), $pos)];
return [LevelEventPacket::create(LevelEvent::PARTICLE_PUNCH_BLOCK, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getStateId()) | ($this->face << 24), $pos)];
}
}

View File

@@ -33,6 +33,6 @@ class TerrainParticle implements Particle{
public function __construct(private Block $b){}
public function encode(Vector3 $pos) : array{
return [LevelEventPacket::standardParticle(ParticleIds::TERRAIN, RuntimeBlockMapping::getInstance()->toRuntimeId($this->b->getFullId()), $pos)];
return [LevelEventPacket::standardParticle(ParticleIds::TERRAIN, RuntimeBlockMapping::getInstance()->toRuntimeId($this->b->getStateId()), $pos)];
}
}

View File

@@ -33,6 +33,6 @@ class BlockBreakSound implements Sound{
public function __construct(private Block $block){}
public function encode(Vector3 $pos) : array{
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BREAK, $pos, false, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()))];
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BREAK, $pos, false, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getStateId()))];
}
}

View File

@@ -33,6 +33,6 @@ class BlockPlaceSound implements Sound{
public function __construct(private Block $block){}
public function encode(Vector3 $pos) : array{
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::PLACE, $pos, false, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()))];
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::PLACE, $pos, false, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getStateId()))];
}
}

View File

@@ -40,7 +40,7 @@ class BlockPunchSound implements Sound{
LevelSoundEvent::HIT,
$pos,
false,
RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId())
RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getStateId())
)];
}
}

View File

@@ -43,7 +43,7 @@ class EntityLandSound implements Sound{
return [LevelSoundEventPacket::create(
LevelSoundEvent::LAND,
$pos,
RuntimeBlockMapping::getInstance()->toRuntimeId($this->blockLandedOn->getFullId()),
RuntimeBlockMapping::getInstance()->toRuntimeId($this->blockLandedOn->getStateId()),
$this->entity::getNetworkTypeId(),
false, //TODO: does isBaby have any relevance here?
false

View File

@@ -42,7 +42,7 @@ final class ItemUseOnBlockSound implements Sound{
LevelSoundEvent::ITEM_USE_ON,
$pos,
false,
RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId())
RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getStateId())
)];
}
}