First look at 3D biome support

This commit is contained in:
Dylan K. Taylor 2023-01-17 21:41:30 +00:00
parent 3a13f5cf5f
commit 7abfc46567
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
19 changed files with 319 additions and 243 deletions

View File

@ -36,9 +36,7 @@ use pocketmine\utils\BinaryStream;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk;
use function chr;
use function count;
use function str_repeat;
final class ChunkSerializer{
private function __construct(){
@ -64,13 +62,16 @@ final class ChunkSerializer{
$stream = PacketSerializer::encoder($encoderContext);
$subChunkCount = self::getSubChunkCount($chunk);
for($y = Chunk::MIN_SUBCHUNK_INDEX, $writtenCount = 0; $writtenCount < $subChunkCount; ++$y, ++$writtenCount){
$writtenCount = 0;
for($y = Chunk::MIN_SUBCHUNK_INDEX; $writtenCount < $subChunkCount; ++$y, ++$writtenCount){
self::serializeSubChunk($chunk->getSubChunk($y), $blockMapper, $stream, false);
}
//TODO: right now we don't support 3D natively, so we just 3Dify our 2D biomes so they fill the column
$encodedBiomePalette = self::serializeBiomesAsPalette($chunk);
$stream->put(str_repeat($encodedBiomePalette, 24));
$biomeIdMap = LegacyBiomeIdToStringIdMap::getInstance();
//all biomes must always be written :(
for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){
self::serializeBiomePalette($chunk->getSubChunk($y)->getBiomeArray(), $biomeIdMap, $stream);
}
$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.
@ -123,6 +124,28 @@ final class ChunkSerializer{
}
}
private static function serializeBiomePalette(PalettedBlockArray $biomePalette, LegacyBiomeIdToStringIdMap $biomeIdMap, PacketSerializer $stream) : void{
$biomePaletteBitsPerBlock = $biomePalette->getBitsPerBlock();
$stream->putByte(($biomePaletteBitsPerBlock << 1) | 1); //the last bit is non-persistence (like for blocks), though it has no effect on biomes since they always use integer IDs
$stream->put($biomePalette->getWordArray());
//these LSHIFT by 1 uvarints are optimizations: the client expects zigzag varints here
//but since we know they are always unsigned, we can avoid the extra fcall overhead of
//zigzag and just shift directly.
$biomePaletteArray = $biomePalette->getPalette();
if($biomePaletteBitsPerBlock !== 0){
$stream->putUnsignedVarInt(count($biomePaletteArray) << 1);
}
foreach($biomePaletteArray as $p){
if($biomeIdMap->legacyToString($p) === null){
//make sure we aren't sending bogus biomes - the 1.18.0 client crashes if we do this
$p = BiomeIds::OCEAN;
}
$stream->put(Binary::writeUnsignedVarInt($p << 1));
}
}
public static function serializeTiles(Chunk $chunk) : string{
$stream = new BinaryStream();
foreach($chunk->getTiles() as $tile){
@ -133,39 +156,4 @@ final class ChunkSerializer{
return $stream->getBuffer();
}
private static function serializeBiomesAsPalette(Chunk $chunk) : string{
$biomeIdMap = LegacyBiomeIdToStringIdMap::getInstance();
$biomePalette = new PalettedBlockArray($chunk->getBiomeId(0, 0));
for($x = 0; $x < 16; ++$x){
for($z = 0; $z < 16; ++$z){
$biomeId = $chunk->getBiomeId($x, $z);
if($biomeIdMap->legacyToString($biomeId) === null){
//make sure we aren't sending bogus biomes - the 1.18.0 client crashes if we do this
$biomeId = BiomeIds::OCEAN;
}
for($y = 0; $y < 16; ++$y){
$biomePalette->set($x, $y, $z, $biomeId);
}
}
}
$biomePaletteBitsPerBlock = $biomePalette->getBitsPerBlock();
$encodedBiomePalette =
chr(($biomePaletteBitsPerBlock << 1) | 1) . //the last bit is non-persistence (like for blocks), though it has no effect on biomes since they always use integer IDs
$biomePalette->getWordArray();
//these LSHIFT by 1 uvarints are optimizations: the client expects zigzag varints here
//but since we know they are always unsigned, we can avoid the extra fcall overhead of
//zigzag and just shift directly.
$biomePaletteArray = $biomePalette->getPalette();
if($biomePaletteBitsPerBlock !== 0){
$encodedBiomePalette .= Binary::writeUnsignedVarInt(count($biomePaletteArray) << 1);
}
foreach($biomePaletteArray as $p){
$encodedBiomePalette .= Binary::writeUnsignedVarInt($p << 1);
}
return $encodedBiomePalette;
}
}

View File

@ -2241,23 +2241,23 @@ class World implements ChunkManager{
return ($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null ? $chunk->getTile($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK) : null;
}
public function getBiomeId(int $x, int $z) : int{
public function getBiomeId(int $x, int $y, int $z) : int{
if(($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){
return $chunk->getBiomeId($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
return $chunk->getBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
}
return BiomeIds::OCEAN; //TODO: this should probably throw instead (terrain not generated yet)
}
public function getBiome(int $x, int $z) : Biome{
return BiomeRegistry::getInstance()->getBiome($this->getBiomeId($x, $z));
public function getBiome(int $x, int $y, int $z) : Biome{
return BiomeRegistry::getInstance()->getBiome($this->getBiomeId($x, $y, $z));
}
public function setBiomeId(int $x, int $z, int $biomeId) : void{
public function setBiomeId(int $x, int $y, int $z, int $biomeId) : void{
$chunkX = $x >> Chunk::COORD_BIT_SIZE;
$chunkZ = $z >> Chunk::COORD_BIT_SIZE;
$this->unlockChunk($chunkX, $chunkZ, null);
if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== null){
$chunk->setBiomeId($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK, $biomeId);
$chunk->setBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK, $biomeId);
}else{
//if we allowed this, the modifications would be lost when the chunk is created
throw new WorldException("Cannot set biome in a non-generated chunk");

View File

@ -1,72 +0,0 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\world\format;
use function chr;
use function ord;
use function str_repeat;
use function strlen;
final class BiomeArray{
private string $payload;
/**
* @param string $payload ZZZZXXXX key bits
*/
public function __construct(string $payload){
if(strlen($payload) !== 256){
throw new \InvalidArgumentException("Biome array is expected to be exactly 256 bytes");
}
$this->payload = $payload;
}
public static function fill(int $biomeId) : self{
return new BiomeArray(str_repeat(chr($biomeId), 256));
}
private static function idx(int $x, int $z) : int{
if($x < 0 || $x >= 16 || $z < 0 || $z >= 16){
throw new \InvalidArgumentException("x and z must be in the range 0-15");
}
return ($z << 4) | $x;
}
public function get(int $x, int $z) : int{
return ord($this->payload[self::idx($x, $z)]);
}
public function set(int $x, int $z, int $biomeId) : void{
if($biomeId < 0 || $biomeId >= 256){
throw new \InvalidArgumentException("Biome ID must be in the range 0-255");
}
$this->payload[self::idx($x, $z)] = chr($biomeId);
}
/**
* @return string ZZZZXXXX key bits
*/
public function getData() : string{
return $this->payload;
}
}

View File

@ -29,6 +29,7 @@ namespace pocketmine\world\format;
use pocketmine\block\Block;
use pocketmine\block\BlockTypeIds;
use pocketmine\block\tile\Tile;
use pocketmine\data\bedrock\BiomeIds;
use function array_map;
class Chunk{
@ -59,21 +60,19 @@ class Chunk{
protected HeightArray $heightMap;
protected BiomeArray $biomeIds;
/**
* @param SubChunk[] $subChunks
*/
public function __construct(array $subChunks, BiomeArray $biomeIds, bool $terrainPopulated){
public function __construct(array $subChunks, bool $terrainPopulated){
$this->subChunks = new \SplFixedArray(Chunk::MAX_SUBCHUNKS);
foreach($this->subChunks as $y => $null){
$this->subChunks[$y] = $subChunks[$y + self::MIN_SUBCHUNK_INDEX] ?? new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, []);
//TODO: we should probably require all subchunks to be provided here
$this->subChunks[$y] = $subChunks[$y + self::MIN_SUBCHUNK_INDEX] ?? new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, [], new PalettedBlockArray(BiomeIds::OCEAN));
}
$val = (self::MAX_SUBCHUNK_INDEX + 1) * SubChunk::EDGE_LENGTH;
$this->heightMap = HeightArray::fill($val); //TODO: what about lazily initializing this?
$this->biomeIds = $biomeIds;
$this->terrainPopulated = $terrainPopulated;
}
@ -153,8 +152,8 @@ class Chunk{
*
* @return int 0-255
*/
public function getBiomeId(int $x, int $z) : int{
return $this->biomeIds->get($x, $z);
public function getBiomeId(int $x, int $y, int $z) : int{
return $this->getSubChunk($y >> SubChunk::COORD_BIT_SIZE)->getBiomeArray()->get($x, $y, $z);
}
/**
@ -164,8 +163,8 @@ class Chunk{
* @param int $z 0-15
* @param int $biomeId 0-255
*/
public function setBiomeId(int $x, int $z, int $biomeId) : void{
$this->biomeIds->set($x, $z, $biomeId);
public function setBiomeId(int $x, int $y, int $z, int $biomeId) : void{
$this->getSubChunk($y >> SubChunk::COORD_BIT_SIZE)->getBiomeArray()->set($x, $y, $z, $biomeId);
$this->terrainDirtyFlags |= self::DIRTY_FLAG_BIOMES;
}
@ -230,10 +229,6 @@ class Chunk{
}
}
public function getBiomeIdArray() : string{
return $this->biomeIds->getData();
}
/**
* @return int[]
*/
@ -291,7 +286,7 @@ class Chunk{
throw new \InvalidArgumentException("Invalid subchunk Y coordinate $y");
}
$this->subChunks[$y - self::MIN_SUBCHUNK_INDEX] = $subChunk ?? new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, []);
$this->subChunks[$y - self::MIN_SUBCHUNK_INDEX] = $subChunk ?? new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, [], new PalettedBlockArray(BiomeIds::OCEAN));
$this->setTerrainDirtyFlag(self::DIRTY_FLAG_BLOCKS, true);
}
@ -322,7 +317,6 @@ class Chunk{
return clone $subChunk;
}, $this->subChunks->toArray()));
$this->heightMap = clone $this->heightMap;
$this->biomeIds = clone $this->biomeIds;
}
/**

View File

@ -40,6 +40,7 @@ class SubChunk{
public function __construct(
private int $emptyBlockId,
private array $blockLayers,
private PalettedBlockArray $biomes,
private ?LightArray $skyLight = null,
private ?LightArray $blockLight = null
){}
@ -102,6 +103,8 @@ class SubChunk{
return null; //highest block not in this subchunk
}
public function getBiomeArray() : PalettedBlockArray{ return $this->biomes; }
public function getBlockSkyLightArray() : LightArray{
return $this->skyLight ??= LightArray::fill(0);
}
@ -137,6 +140,7 @@ class SubChunk{
unset($this->blockLayers[$k]);
}
$this->blockLayers = array_values($this->blockLayers);
$this->biomes->collectGarbage();
if($this->skyLight !== null && $this->skyLight->isUniform(0)){
$this->skyLight = null;
@ -150,6 +154,7 @@ class SubChunk{
$this->blockLayers = array_map(function(PalettedBlockArray $array) : PalettedBlockArray{
return clone $array;
}, $this->blockLayers);
$this->biomes = clone $this->biomes;
if($this->skyLight !== null){
$this->skyLight = clone $this->skyLight;

View File

@ -23,8 +23,11 @@ declare(strict_types=1);
namespace pocketmine\world\format\io;
use pocketmine\world\format\PalettedBlockArray;
use function chr;
use function ord;
use function str_repeat;
use function strlen;
class ChunkUtils{
@ -42,4 +45,23 @@ class ChunkUtils{
return $result;
}
/**
* Converts 2D biomes into a 3D biome palette. This palette can then be cloned for every subchunk.
*/
public static function extrapolate3DBiomes(string $biomes2d) : PalettedBlockArray{
if(strlen($biomes2d) !== 256){
throw new \InvalidArgumentException("Biome array is expected to be exactly 256 bytes");
}
$biomePalette = new PalettedBlockArray(ord($biomes2d[0]));
for($x = 0; $x < 16; ++$x){
for($z = 0; $z < 16; ++$z){
$biomeId = ord($biomes2d[($z << 4) | $x]);
for($y = 0; $y < 16; ++$y){
$biomePalette->set($x, $y, $z, $biomeId);
}
}
}
return $biomePalette;
}
}

View File

@ -25,7 +25,6 @@ namespace pocketmine\world\format\io;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryStream;
use pocketmine\world\format\BiomeArray;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk;
@ -46,6 +45,17 @@ final class FastChunkSerializer{
//NOOP
}
private static function serializePalettedArray(BinaryStream $stream, PalettedBlockArray $array) : void{
$wordArray = $array->getWordArray();
$palette = $array->getPalette();
$stream->putByte($array->getBitsPerBlock());
$stream->put($wordArray);
$serialPalette = pack("L*", ...$palette);
$stream->putInt(strlen($serialPalette));
$stream->put($serialPalette);
}
/**
* Fast-serializes the chunk for passing between threads
* TODO: tiles and entities
@ -67,23 +77,25 @@ final class FastChunkSerializer{
$layers = $subChunk->getBlockLayers();
$stream->putByte(count($layers));
foreach($layers as $blocks){
$wordArray = $blocks->getWordArray();
$palette = $blocks->getPalette();
$stream->putByte($blocks->getBitsPerBlock());
$stream->put($wordArray);
$serialPalette = pack("L*", ...$palette);
$stream->putInt(strlen($serialPalette));
$stream->put($serialPalette);
self::serializePalettedArray($stream, $blocks);
}
self::serializePalettedArray($stream, $subChunk->getBiomeArray());
}
//biomes
$stream->put($chunk->getBiomeIdArray());
return $stream->getBuffer();
}
private static function deserializePalettedArray(BinaryStream $stream) : PalettedBlockArray{
$bitsPerBlock = $stream->getByte();
$words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
/** @var int[] $unpackedPalette */
$unpackedPalette = unpack("L*", $stream->get($stream->getInt())); //unpack() will never fail here
$palette = array_values($unpackedPalette);
return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
}
/**
* Deserializes a fast-serialized chunk
*/
@ -103,19 +115,12 @@ final class FastChunkSerializer{
/** @var PalettedBlockArray[] $layers */
$layers = [];
for($i = 0, $layerCount = $stream->getByte(); $i < $layerCount; ++$i){
$bitsPerBlock = $stream->getByte();
$words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
/** @var int[] $unpackedPalette */
$unpackedPalette = unpack("L*", $stream->get($stream->getInt())); //unpack() will never fail here
$palette = array_values($unpackedPalette);
$layers[] = PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
$layers[] = self::deserializePalettedArray($stream);
}
$subChunks[$y] = new SubChunk($airBlockId, $layers);
$biomeArray = self::deserializePalettedArray($stream);
$subChunks[$y] = new SubChunk($airBlockId, $layers, $biomeArray);
}
$biomeIds = new BiomeArray($stream->get(256));
return new Chunk($subChunks, $biomeIds, $terrainPopulated);
return new Chunk($subChunks, $terrainPopulated);
}
}

View File

@ -35,7 +35,6 @@ use pocketmine\nbt\TreeRoot;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
use pocketmine\world\format\BiomeArray;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\io\BaseWorldProvider;
use pocketmine\world\format\io\ChunkData;
@ -145,7 +144,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
/**
* @throws CorruptedChunkException
*/
protected function deserializePaletted(BinaryStream $stream) : PalettedBlockArray{
protected function deserializeBlockPalette(BinaryStream $stream) : PalettedBlockArray{
$bitsPerBlock = $stream->getByte() >> 1;
try{
@ -188,6 +187,99 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
}
/**
* @throws CorruptedChunkException
*/
private static function getExpected3dBiomesCount(int $chunkVersion) : int{
return match(true){
$chunkVersion >= ChunkVersion::v1_18_30 => 24,
$chunkVersion >= ChunkVersion::v1_18_0_25_beta => 25,
$chunkVersion >= ChunkVersion::v1_18_0_24_beta => 32,
$chunkVersion >= ChunkVersion::v1_18_0_22_beta => 65,
$chunkVersion >= ChunkVersion::v1_17_40_20_beta_experimental_caves_cliffs => 32,
default => throw new CorruptedChunkException("Chunk version $chunkVersion should not have 3D biomes")
};
}
/**
* @throws CorruptedChunkException
*/
private static function deserializeBiomePalette(BinaryStream $stream, int $bitsPerBlock) : PalettedBlockArray{
try{
$words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
}catch(\InvalidArgumentException $e){
throw new CorruptedChunkException("Failed to deserialize paletted biomes: " . $e->getMessage(), 0, $e);
}
$palette = [];
$paletteSize = $bitsPerBlock === 0 ? 1 : $stream->getLInt();
for($i = 0; $i < $paletteSize; ++$i){
$palette[] = $stream->getLInt();
}
//TODO: exceptions
return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
}
private static function serializeBiomePalette(BinaryStream $stream, PalettedBlockArray $biomes) : void{
$stream->putByte($biomes->getBitsPerBlock() << 1);
$stream->put($biomes->getWordArray());
$palette = $biomes->getPalette();
if($biomes->getBitsPerBlock() !== 0){
$stream->putLInt(count($palette));
}
foreach($palette as $p){
$stream->putLInt($p);
}
}
/**
* @throws CorruptedChunkException
* @return PalettedBlockArray[]
* @phpstan-return array<int, PalettedBlockArray>
*/
private static function deserialize3dBiomes(BinaryStream $stream, int $chunkVersion) : array{
$previous = null;
$result = [];
$nextIndex = Chunk::MIN_SUBCHUNK_INDEX;
$expectedCount = self::getExpected3dBiomesCount($chunkVersion);
for($i = 0; $i < $expectedCount; ++$i){
try{
$bitsPerBlock = $stream->getByte() >> 1;
if($bitsPerBlock === 127){
if($previous === null){
throw new CorruptedChunkException("Serialized biome palette $i has no previous palette to copy from");
}
$decoded = clone $previous;
}else{
$decoded = self::deserializeBiomePalette($stream, $bitsPerBlock);
}
$previous = $decoded;
if($nextIndex <= Chunk::MAX_SUBCHUNK_INDEX){ //older versions wrote additional superfluous biome palettes
$result[$nextIndex++] = $decoded;
}
}catch(BinaryDataException $e){
throw new CorruptedChunkException("Failed to deserialize biome palette $i: " . $e->getMessage(), 0, $e);
}
}
if(!$stream->feof()){
throw new CorruptedChunkException("3D biomes data contains extra unread data");
}
return $result;
}
private static function serialize3dBiomes(BinaryStream $stream, Chunk $chunk) : void{
//TODO: the server-side min/max may not coincide with the world storage min/max - we may need additional logic to handle this
for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; $y++){
//TODO: is it worth trying to use the previous palette if it's the same as the current one? vanilla supports
//this, but it's not clear if it's worth the effort to implement.
self::serializeBiomePalette($stream, $chunk->getSubChunk($y)->getBiomeArray());
}
}
/**
* @phpstan-param-out int $x
* @phpstan-param-out int $y
@ -280,9 +372,6 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
/** @var SubChunk[] $subChunks */
$subChunks = [];
/** @var BiomeArray|null $biomeArray */
$biomeArray = null;
$hasBeenUpgraded = $chunkVersion < self::CURRENT_LEVEL_CHUNK_VERSION;
$subChunkKeyOffset = self::hasOffsetCavesAndCliffsSubChunks($chunkVersion) ? self::CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET : 0;
@ -330,8 +419,37 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
case ChunkVersion::v1_0_0:
$convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion);
$biomeArrays = [];
if(($maps2d = $this->db->get($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES)) !== false){
$binaryStream = new BinaryStream($maps2d);
try{
$binaryStream->get(512); //heightmap, discard it
$biomes3d = ChunkUtils::extrapolate3DBiomes($binaryStream->get(256)); //never throws
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
for($i = Chunk::MIN_SUBCHUNK_INDEX; $i <= Chunk::MAX_SUBCHUNK_INDEX; ++$i){
$biomeArrays[$i] = clone $biomes3d;
}
}elseif(($maps3d = $this->db->get($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES)) !== false){
$binaryStream = new BinaryStream($maps3d);
try{
$binaryStream->get(512);
$biomeArrays = self::deserialize3dBiomes($binaryStream, $chunkVersion);
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
}else{
for($i = Chunk::MIN_SUBCHUNK_INDEX; $i <= Chunk::MAX_SUBCHUNK_INDEX; ++$i){
$biomeArrays[$i] = new PalettedBlockArray(BiomeIds::OCEAN); //polyfill
}
}
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(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, [], $biomeArrays[$y]);
continue;
}
@ -369,14 +487,14 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$storages[] = $convertedLegacyExtraData[$y];
}
$subChunks[$y] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, $storages);
$subChunks[$y] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, $storages, $biomeArrays[$y]);
break;
case SubChunkVersion::PALETTED_SINGLE:
$storages = [$this->deserializePaletted($binaryStream)];
$storages = [$this->deserializeBlockPalette($binaryStream)];
if(isset($convertedLegacyExtraData[$y])){
$storages[] = $convertedLegacyExtraData[$y];
}
$subChunks[$y] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, $storages);
$subChunks[$y] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, $storages, $biomeArrays[$y]);
break;
case SubChunkVersion::PALETTED_MULTI:
case SubChunkVersion::PALETTED_MULTI_WITH_OFFSET:
@ -390,9 +508,9 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$storages = [];
for($k = 0; $k < $storageCount; ++$k){
$storages[] = $this->deserializePaletted($binaryStream);
$storages[] = $this->deserializeBlockPalette($binaryStream);
}
$subChunks[$y] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, $storages);
$subChunks[$y] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, $storages, $biomeArrays[$y]);
}
break;
default:
@ -401,16 +519,6 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
}
}
if(($maps2d = $this->db->get($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES)) !== false){
$binaryStream = new BinaryStream($maps2d);
try{
$binaryStream->get(512); //heightmap, discard it
$biomeArray = new BiomeArray($binaryStream->get(256)); //never throws
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
}
break;
case ChunkVersion::v0_9_5:
case ChunkVersion::v0_9_2:
@ -430,22 +538,30 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
try{
$binaryStream->get(256); //heightmap, discard it
/** @var int[] $unpackedBiomeArray */
$unpackedBiomeArray = unpack("N*", $binaryStream->get(1024)); //unpack() will never fail here
$biomes3d = ChunkUtils::extrapolate3DBiomes(ChunkUtils::convertBiomeColors(array_values($unpackedBiomeArray))); //never throws
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
for($yy = 0; $yy < 8; ++$yy){
$storages = [$this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $yy)];
if(isset($convertedLegacyExtraData[$yy])){
$storages[] = $convertedLegacyExtraData[$yy];
}
$subChunks[$yy] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, $storages);
$subChunks[$yy] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, $storages, clone $biomes3d);
}
try{
$binaryStream->get(256); //heightmap, discard it
/** @var int[] $unpackedBiomeArray */
$unpackedBiomeArray = unpack("N*", $binaryStream->get(1024)); //unpack() will never fail here
$biomeArray = new BiomeArray(ChunkUtils::convertBiomeColors(array_values($unpackedBiomeArray))); //never throws
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
//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(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, [], clone $biomes3d);
}
}
break;
default:
//TODO: set chunks read-only so the version on disk doesn't get overwritten
@ -485,8 +601,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
//TODO: tile ticks, biome states (?)
$chunk = new Chunk(
$subChunks,
$biomeArray ?? BiomeArray::fill(BiomeIds::OCEAN), //TODO: maybe missing biomes should be an error?
$subChunks, //TODO: maybe missing biomes should be an error?
$terrainPopulated
);
@ -545,7 +660,11 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
}
if($chunk->getTerrainDirtyFlag(Chunk::DIRTY_FLAG_BIOMES)){
$write->put($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES, str_repeat("\x00", 512) . $chunk->getBiomeIdArray());
$write->delete($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES);
$stream = new BinaryStream();
$stream->put(str_repeat("\x00", 512)); //fake heightmap
self::serialize3dBiomes($stream, $chunk);
$write->put($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES, $stream->getBuffer());
}
//TODO: use this properly

View File

@ -26,16 +26,17 @@ namespace pocketmine\world\format\io\region;
use pocketmine\block\Block;
use pocketmine\block\BlockTypeIds;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk;
class Anvil extends RegionWorldProvider{
use LegacyAnvilChunkTrait;
protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk{
protected function deserializeSubChunk(CompoundTag $subChunk, PalettedBlockArray $biomes3d) : SubChunk{
return new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, [$this->palettizeLegacySubChunkYZX(
self::readFixedSizeByteArray($subChunk, "Blocks", 4096),
self::readFixedSizeByteArray($subChunk, "Data", 2048)
)]);
)], $biomes3d);
//ignore legacy light information
}

View File

@ -23,6 +23,8 @@ declare(strict_types=1);
namespace pocketmine\world\format\io\region;
use pocketmine\block\Block;
use pocketmine\block\BlockTypeIds;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\nbt\BigEndianNbtSerializer;
use pocketmine\nbt\NbtDataException;
@ -30,12 +32,13 @@ use pocketmine\nbt\tag\ByteArrayTag;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\IntArrayTag;
use pocketmine\nbt\tag\ListTag;
use pocketmine\world\format\BiomeArray;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\io\ChunkData;
use pocketmine\world\format\io\ChunkUtils;
use pocketmine\world\format\io\exception\CorruptedChunkException;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk;
use function strlen;
use function zlib_decode;
/**
@ -67,34 +70,38 @@ trait LegacyAnvilChunkTrait{
throw new CorruptedChunkException("'Level' key is missing from chunk NBT");
}
$makeBiomeArray = function(string $biomeIds) : PalettedBlockArray{
if(strlen($biomeIds) !== 256){
throw new CorruptedChunkException("Expected biome array to be exactly 256 bytes, got " . strlen($biomeIds));
}
//TODO: we may need to convert legacy biome IDs
return ChunkUtils::extrapolate3DBiomes($biomeIds);
};
if(($biomeColorsTag = $chunk->getTag("BiomeColors")) instanceof IntArrayTag){
$biomes3d = $makeBiomeArray(ChunkUtils::convertBiomeColors($biomeColorsTag->getValue())); //Convert back to original format
}elseif(($biomesTag = $chunk->getTag("Biomes")) instanceof ByteArrayTag){
$biomes3d = $makeBiomeArray($biomesTag->getValue());
}else{
$biomes3d = new PalettedBlockArray(BiomeIds::OCEAN);
}
$subChunks = [];
$subChunksTag = $chunk->getListTag("Sections") ?? [];
foreach($subChunksTag as $subChunk){
if($subChunk instanceof CompoundTag){
$subChunks[$subChunk->getByte("Y")] = $this->deserializeSubChunk($subChunk);
$subChunks[$subChunk->getByte("Y")] = $this->deserializeSubChunk($subChunk, clone $biomes3d);
}
}
$makeBiomeArray = function(string $biomeIds) : BiomeArray{
try{
return new BiomeArray($biomeIds);
}catch(\InvalidArgumentException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){
if(!isset($subChunks[$y])){
$subChunks[$y] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, [], clone $biomes3d);
}
};
$biomeArray = null;
if(($biomeColorsTag = $chunk->getTag("BiomeColors")) instanceof IntArrayTag){
$biomeArray = $makeBiomeArray(ChunkUtils::convertBiomeColors($biomeColorsTag->getValue())); //Convert back to original format
}elseif(($biomesTag = $chunk->getTag("Biomes")) instanceof ByteArrayTag){
$biomeArray = $makeBiomeArray($biomesTag->getValue());
}else{
$biomeArray = BiomeArray::fill(BiomeIds::OCEAN);
}
return new ChunkData(
new Chunk(
$subChunks,
$biomeArray,
$chunk->getByte("TerrainPopulated", 0) !== 0
),
($entitiesTag = $chunk->getTag("Entities")) instanceof ListTag ? self::getCompoundList("Entities", $entitiesTag) : [],
@ -102,6 +109,6 @@ trait LegacyAnvilChunkTrait{
);
}
abstract protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk;
abstract protected function deserializeSubChunk(CompoundTag $subChunk, PalettedBlockArray $biomes3d) : SubChunk;
}

View File

@ -33,12 +33,13 @@ use pocketmine\nbt\tag\ByteTag;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\IntArrayTag;
use pocketmine\nbt\tag\ListTag;
use pocketmine\world\format\BiomeArray;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\io\ChunkData;
use pocketmine\world\format\io\ChunkUtils;
use pocketmine\world\format\io\exception\CorruptedChunkException;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk;
use function strlen;
use function zlib_decode;
class McRegion extends RegionWorldProvider{
@ -69,34 +70,37 @@ class McRegion extends RegionWorldProvider{
//trying to read it.
return null;
}
$makeBiomeArray = function(string $biomeIds) : PalettedBlockArray{
if(strlen($biomeIds) !== 256){
throw new CorruptedChunkException("Expected biome array to be exactly 256 bytes, got " . strlen($biomeIds));
}
return ChunkUtils::extrapolate3DBiomes($biomeIds);
};
if(($biomeColorsTag = $chunk->getTag("BiomeColors")) instanceof IntArrayTag){
$biomes3d = $makeBiomeArray(ChunkUtils::convertBiomeColors($biomeColorsTag->getValue())); //Convert back to original format
}elseif(($biomesTag = $chunk->getTag("Biomes")) instanceof ByteArrayTag){
$biomes3d = $makeBiomeArray($biomesTag->getValue());
}else{
$biomes3d = new PalettedBlockArray(BiomeIds::OCEAN);
}
$subChunks = [];
$fullIds = self::readFixedSizeByteArray($chunk, "Blocks", 32768);
$fullData = self::readFixedSizeByteArray($chunk, "Data", 16384);
for($y = 0; $y < 8; ++$y){
$subChunks[$y] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, [$this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $y)]);
$subChunks[$y] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, [$this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $y)], clone $biomes3d);
}
$makeBiomeArray = function(string $biomeIds) : BiomeArray{
try{
return new BiomeArray($biomeIds);
}catch(\InvalidArgumentException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){
if(!isset($subChunks[$y])){
$subChunks[$y] = new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, [], clone $biomes3d);
}
};
$biomeIds = null;
if(($biomeColorsTag = $chunk->getTag("BiomeColors")) instanceof IntArrayTag){
$biomeIds = $makeBiomeArray(ChunkUtils::convertBiomeColors($biomeColorsTag->getValue())); //Convert back to original format
}elseif(($biomesTag = $chunk->getTag("Biomes")) instanceof ByteArrayTag){
$biomeIds = $makeBiomeArray($biomesTag->getValue());
}else{
$biomeIds = BiomeArray::fill(BiomeIds::OCEAN);
}
return new ChunkData(
new Chunk(
$subChunks,
$biomeIds,
$chunk->getByte("TerrainPopulated", 0) !== 0
),
($entitiesTag = $chunk->getTag("Entities")) instanceof ListTag ? self::getCompoundList("Entities", $entitiesTag) : [],

View File

@ -26,6 +26,7 @@ namespace pocketmine\world\format\io\region;
use pocketmine\block\Block;
use pocketmine\block\BlockTypeIds;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk;
/**
@ -35,11 +36,11 @@ use pocketmine\world\format\SubChunk;
class PMAnvil extends RegionWorldProvider{
use LegacyAnvilChunkTrait;
protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk{
protected function deserializeSubChunk(CompoundTag $subChunk, PalettedBlockArray $biomes3d) : SubChunk{
return new SubChunk(BlockTypeIds::AIR << Block::INTERNAL_STATE_DATA_BITS, [$this->palettizeLegacySubChunkXZY(
self::readFixedSizeByteArray($subChunk, "Blocks", 4096),
self::readFixedSizeByteArray($subChunk, "Data", 2048)
)]);
)], $biomes3d);
}
protected static function getRegionFileExtension() : string{

View File

@ -25,7 +25,6 @@ namespace pocketmine\world\generator;
use pocketmine\block\VanillaBlocks;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\BiomeArray;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\SubChunk;
use pocketmine\world\generator\object\OreType;
@ -67,7 +66,7 @@ class Flat extends Generator{
}
protected function generateBaseChunk() : void{
$this->chunk = new Chunk([], BiomeArray::fill($this->options->getBiomeId()), false);
$this->chunk = new Chunk([], false);
$structure = $this->options->getStructure();
$count = count($structure);

View File

@ -23,10 +23,8 @@ declare(strict_types=1);
namespace pocketmine\world\generator;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\scheduler\AsyncTask;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\world\format\BiomeArray;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\io\FastChunkSerializer;
use pocketmine\world\SimpleChunkManager;
@ -112,7 +110,7 @@ class PopulationTask extends AsyncTask{
}
private static function setOrGenerateChunk(SimpleChunkManager $manager, Generator $generator, int $chunkX, int $chunkZ, ?Chunk $chunk) : Chunk{
$manager->setChunk($chunkX, $chunkZ, $chunk ?? new Chunk([], BiomeArray::fill(BiomeIds::OCEAN), false));
$manager->setChunk($chunkX, $chunkZ, $chunk ?? new Chunk([], false));
if($chunk === null){
$generator->generateChunk($manager, $chunkX, $chunkZ);
$chunk = $manager->getChunk($chunkX, $chunkZ);

View File

@ -34,6 +34,7 @@ use pocketmine\world\generator\noise\Simplex;
use pocketmine\world\generator\object\OreType;
use pocketmine\world\generator\populator\Ore;
use pocketmine\world\generator\populator\Populator;
use pocketmine\world\World;
use function abs;
class Nether extends Generator{
@ -78,7 +79,9 @@ class Nether extends Generator{
for($x = 0; $x < Chunk::EDGE_LENGTH; ++$x){
for($z = 0; $z < Chunk::EDGE_LENGTH; ++$z){
$chunk->setBiomeId($x, $z, BiomeIds::HELL);
for($y = World::Y_MIN; $y < World::Y_MAX; $y++){
$chunk->setBiomeId($x, $y, $z, BiomeIds::HELL);
}
for($y = 0; $y < 128; ++$y){
if($y === 0 || $y === 127){
@ -109,7 +112,7 @@ class Nether extends Generator{
}
$chunk = $world->getChunk($chunkX, $chunkZ);
$biome = BiomeRegistry::getInstance()->getBiome($chunk->getBiomeId(7, 7));
$biome = BiomeRegistry::getInstance()->getBiome($chunk->getBiomeId(7, 7, 7));
$biome->populateChunk($world, $chunkX, $chunkZ, $this->random);
}
}

View File

@ -160,7 +160,9 @@ class Normal extends Generator{
$weightSum = 0;
$biome = $this->pickBiome($absoluteX, $absoluteZ);
$chunk->setBiomeId($x, $z, $biome->getId());
for($y = World::Y_MIN; $y < World::Y_MAX; $y++){
$chunk->setBiomeId($x, $y, $z, $biome->getId());
}
for($sx = -$this->gaussian->smoothSize; $sx <= $this->gaussian->smoothSize; ++$sx){
for($sz = -$this->gaussian->smoothSize; $sz <= $this->gaussian->smoothSize; ++$sz){
@ -218,7 +220,7 @@ class Normal extends Generator{
}
$chunk = $world->getChunk($chunkX, $chunkZ);
$biome = BiomeRegistry::getInstance()->getBiome($chunk->getBiomeId(7, 7));
$biome = BiomeRegistry::getInstance()->getBiome($chunk->getBiomeId(7, 7, 7));
$biome->populateChunk($world, $chunkX, $chunkZ, $this->random);
}
}

View File

@ -41,7 +41,7 @@ class GroundCover implements Populator{
$biomeRegistry = BiomeRegistry::getInstance();
for($x = 0; $x < Chunk::EDGE_LENGTH; ++$x){
for($z = 0; $z < Chunk::EDGE_LENGTH; ++$z){
$biome = $biomeRegistry->getBiome($chunk->getBiomeId($x, $z));
$biome = $biomeRegistry->getBiome($chunk->getBiomeId($x, 0, $z));
$cover = $biome->getGroundCover();
if(count($cover) > 0){
$diffY = 0;

View File

@ -24,23 +24,22 @@ declare(strict_types=1);
namespace pocketmine\world\format;
use PHPUnit\Framework\TestCase;
use pocketmine\data\bedrock\BiomeIds;
class ChunkTest extends TestCase{
public function testClone() : void{
$chunk = new Chunk([], BiomeArray::fill(BiomeIds::OCEAN), false);
$chunk = new Chunk([], false);
$chunk->setFullBlock(0, 0, 0, 1);
$chunk->setBiomeId(0, 0, 1);
$chunk->setBiomeId(0, 0, 0, 1);
$chunk->setHeightMap(0, 0, 1);
$chunk2 = clone $chunk;
$chunk2->setFullBlock(0, 0, 0, 2);
$chunk2->setBiomeId(0, 0, 2);
$chunk2->setBiomeId(0, 0, 0, 2);
$chunk2->setHeightMap(0, 0, 2);
self::assertNotSame($chunk->getFullBlock(0, 0, 0), $chunk2->getFullBlock(0, 0, 0));
self::assertNotSame($chunk->getBiomeId(0, 0), $chunk2->getBiomeId(0, 0));
self::assertNotSame($chunk->getBiomeId(0, 0, 0), $chunk2->getBiomeId(0, 0, 0));
self::assertNotSame($chunk->getHeightMap(0, 0), $chunk2->getHeightMap(0, 0));
}
}

View File

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\world\format;
use PHPUnit\Framework\TestCase;
use pocketmine\data\bedrock\BiomeIds;
class SubChunkTest extends TestCase{
@ -31,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, []);
$sub1 = new SubChunk(0, [], new PalettedBlockArray(BiomeIds::OCEAN));
$sub1->setFullBlock(0, 0, 0, 1);
$sub1->getBlockLightArray()->set(0, 0, 0, 1);