First look at loading 1.13+ worlds

This commit is contained in:
Dylan K. Taylor 2022-02-07 03:04:29 +00:00
parent dd3b79b142
commit e58b3ba46c
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
12 changed files with 321 additions and 68 deletions

View File

@ -34,7 +34,7 @@
"adhocore/json-comment": "^1.1",
"fgrosse/phpasn1": "^2.3",
"netresearch/jsonmapper": "^4.0",
"pocketmine/bedrock-data": "~1.5.0+bedrock-1.18.0",
"pocketmine/bedrock-data": "dev-experimental/upgrade-tables",
"pocketmine/bedrock-protocol": "~7.3.0+bedrock-1.18.0",
"pocketmine/binaryutils": "^0.2.1",
"pocketmine/callback-validator": "^1.0.2",

38
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9aa2f11ba68d00423732973554fafb20",
"content-hash": "6723715c08c1582240f1c219df330f96",
"packages": [
{
"name": "adhocore/json-comment",
@ -249,16 +249,16 @@
},
{
"name": "pocketmine/bedrock-data",
"version": "1.5.0+bedrock-1.18.0",
"version": "dev-experimental/upgrade-tables",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockData.git",
"reference": "482c679aa5ed0b81c088c2b1ff0b8110a94c8a6c"
"reference": "747359be18b433659556c5adb527e47b0b150fdd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/482c679aa5ed0b81c088c2b1ff0b8110a94c8a6c",
"reference": "482c679aa5ed0b81c088c2b1ff0b8110a94c8a6c",
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/747359be18b433659556c5adb527e47b0b150fdd",
"reference": "747359be18b433659556c5adb527e47b0b150fdd",
"shasum": ""
},
"type": "library",
@ -269,9 +269,9 @@
"description": "Blobs of data generated from Minecraft: Bedrock Edition, used by PocketMine-MP",
"support": {
"issues": "https://github.com/pmmp/BedrockData/issues",
"source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.18.0"
"source": "https://github.com/pmmp/BedrockData/tree/experimental/upgrade-tables"
},
"time": "2021-11-30T18:30:46+00:00"
"time": "2022-02-05T16:34:56+00:00"
},
{
"name": "pocketmine/bedrock-protocol",
@ -1136,12 +1136,12 @@
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
@ -1219,12 +1219,12 @@
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php81\\": ""
},
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php81\\": ""
},
"classmap": [
"Resources/stubs"
]
@ -1474,12 +1474,12 @@
},
"type": "library",
"autoload": {
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
},
"files": [
"src/DeepCopy/deep_copy.php"
]
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@ -3507,7 +3507,9 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {
"pocketmine/bedrock-data": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {

View File

@ -180,13 +180,13 @@ final class BlockStateUpgradeSchemaUtils{
*
* @return BlockStateUpgradeSchema[]
*/
public static function loadSchemas(string $path) : array{
public static function loadSchemas(string $path, int $currentVersion) : array{
$iterator = new \RegexIterator(
new \FilesystemIterator(
$path,
\FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS
),
'/\/mapping_schema_(\d{4}).*\.json$/',
'/\/(\d{4}).*\.json$/',
\RegexIterator::GET_MATCH
);
@ -209,7 +209,14 @@ final class BlockStateUpgradeSchemaUtils{
}
$model = $jsonMapper->map($json, new BlockStateUpgradeSchemaModel());
$result[$priority] = self::fromJsonModel($model);
$schema = self::fromJsonModel($model);
if($schema->getVersionId() > $currentVersion){
//this might be a beta schema which shouldn't be applicable
//TODO: why do we load the whole schema just to throw it away if it's too new? ...
continue;
}
$result[$priority] = $schema;
}
ksort($result, SORT_NUMERIC);

View File

@ -34,6 +34,16 @@ final class BlockStateUpgrader{
/** @var BlockStateUpgradeSchema[][] */
private array $upgradeSchemas = [];
/**
* @param BlockStateUpgradeSchema[] $upgradeSchemas
* @phpstan-param array<int, BlockStateUpgradeSchema> $upgradeSchemas
*/
public function __construct(array $upgradeSchemas){
foreach($upgradeSchemas as $priority => $schema){
$this->addSchema($schema, $priority);
}
}
public function addSchema(BlockStateUpgradeSchema $schema, int $priority) : void{
if(isset($this->upgradeSchemas[$schema->getVersionId()][$priority])){
throw new \InvalidArgumentException("Another schema already has this priority");

View File

@ -27,12 +27,11 @@ use pocketmine\data\bedrock\blockstate\BlockStateData;
use pocketmine\data\bedrock\blockstate\BlockStateSerializeException;
use pocketmine\data\bedrock\blockstate\BlockStateSerializer;
use pocketmine\data\bedrock\blockstate\BlockTypeNames;
use pocketmine\data\bedrock\blockstate\CachingBlockStateSerializer;
use pocketmine\data\bedrock\blockstate\convert\BlockObjectToBlockStateSerializer;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use pocketmine\world\format\io\GlobalBlockStateHandlers;
use Webmozart\PathUtil\Path;
use function file_get_contents;
@ -56,7 +55,7 @@ final class RuntimeBlockMapping{
private function __construct(){
$contents = Utils::assumeNotFalse(file_get_contents(Path::join(\pocketmine\BEDROCK_DATA_PATH, "canonical_block_states.nbt")), "Missing required resource file");
$this->blockStateDictionary = BlockStateDictionary::loadFromString($contents);
$this->blockStateSerializer = new CachingBlockStateSerializer(new BlockObjectToBlockStateSerializer());
$this->blockStateSerializer = GlobalBlockStateHandlers::getSerializer();
$this->fallbackStateData = new BlockStateData(BlockTypeNames::INFO_UPDATE, CompoundTag::create(), BlockStateData::CURRENT_VERSION);
}

View File

@ -23,8 +23,12 @@ declare(strict_types=1);
namespace pocketmine\world\format\io;
use pocketmine\data\bedrock\blockstate\BlockStateData;
use pocketmine\data\bedrock\blockstate\BlockTypeNames;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\world\format\io\exception\CorruptedWorldException;
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\WorldException;
use function file_exists;
@ -49,6 +53,44 @@ abstract class BaseWorldProvider implements WorldProvider{
*/
abstract protected function loadLevelData() : WorldData;
private function translatePalette(PalettedBlockArray $blockArray) : PalettedBlockArray{
$palette = $blockArray->getPalette();
//TODO: this should be dependency-injected so it can be replaced, but that would break BC
//also, we want it to be lazy-loaded ...
$legacyBlockStateMapper = GlobalBlockStateHandlers::getLegacyBlockStateMapper();
$blockStateDeserializer = GlobalBlockStateHandlers::getDeserializer();
$newPalette = [];
foreach($palette as $k => $legacyIdMeta){
$newStateData = $legacyBlockStateMapper->fromIntIdMeta($legacyIdMeta >> 4, $legacyIdMeta & 0xf);
if($newStateData === null){
//TODO: remember data for unknown states so we can implement them later
$newStateData = new BlockStateData(BlockTypeNames::INFO_UPDATE, CompoundTag::create(), BlockStateData::CURRENT_VERSION);
}
$newPalette[$k] = $blockStateDeserializer->deserialize($newStateData);
}
//TODO: this is sub-optimal since it reallocates the offset table multiple times
return PalettedBlockArray::fromData(
$blockArray->getBitsPerBlock(),
$blockArray->getWordArray(),
$newPalette
);
}
protected function palettizeLegacySubChunkXZY(string $idArray, string $metaArray) : PalettedBlockArray{
return $this->translatePalette(SubChunkConverter::convertSubChunkXZY($idArray, $metaArray));
}
protected function palettizeLegacySubChunkYZX(string $idArray, string $metaArray) : PalettedBlockArray{
return $this->translatePalette(SubChunkConverter::convertSubChunkYZX($idArray, $metaArray));
}
protected function palettizeLegacySubChunkFromColumn(string $idArray, string $metaArray, int $yOffset) : PalettedBlockArray{
return $this->translatePalette(SubChunkConverter::convertSubChunkFromLegacyColumn($idArray, $metaArray, $yOffset));
}
public function getPath() : string{
return $this->path;
}

View File

@ -0,0 +1,79 @@
<?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\io;
use pocketmine\data\bedrock\blockstate\BlockStateData;
use pocketmine\data\bedrock\blockstate\BlockStateDeserializer;
use pocketmine\data\bedrock\blockstate\BlockStateSerializer;
use pocketmine\data\bedrock\blockstate\CachingBlockStateDeserializer;
use pocketmine\data\bedrock\blockstate\CachingBlockStateSerializer;
use pocketmine\data\bedrock\blockstate\convert\BlockObjectToBlockStateSerializer;
use pocketmine\data\bedrock\blockstate\convert\BlockStateToBlockObjectDeserializer;
use pocketmine\data\bedrock\blockstate\upgrade\BlockStateUpgrader;
use pocketmine\data\bedrock\blockstate\upgrade\BlockStateUpgradeSchemaUtils;
use pocketmine\data\bedrock\blockstate\upgrade\LegacyBlockStateMapper;
use pocketmine\data\bedrock\blockstate\UpgradingBlockStateDeserializer;
use pocketmine\data\bedrock\LegacyBlockIdToStringIdMap;
use pocketmine\errorhandler\ErrorToExceptionHandler;
use Webmozart\PathUtil\Path;
use function file_get_contents;
use const pocketmine\BEDROCK_DATA_PATH;
/**
* Provides global access to blockstate serializers for all world providers.
* TODO: Get rid of this. This is necessary to enable plugins to register custom serialize/deserialize handlers, and
* also because we can't break BC of WorldProvider before PM5. While this is a sucky hack, it provides meaningful
* benefits for now.
*/
final class GlobalBlockStateHandlers{
private static ?BlockStateSerializer $blockStateSerializer;
private static ?BlockStateDeserializer $blockStateDeserializer;
private static ?LegacyBlockStateMapper $legacyBlockStateMapper;
public static function getDeserializer() : BlockStateDeserializer{
return self::$blockStateDeserializer ??= new CachingBlockStateDeserializer(
new UpgradingBlockStateDeserializer(
new BlockStateUpgrader(BlockStateUpgradeSchemaUtils::loadSchemas(
Path::join(BEDROCK_DATA_PATH, 'upgrade_schema'),
BlockStateData::CURRENT_VERSION
)),
new BlockStateToBlockObjectDeserializer()
)
);
}
public static function getSerializer() : BlockStateSerializer{
return self::$blockStateSerializer ??= new CachingBlockStateSerializer(new BlockObjectToBlockStateSerializer());
}
public static function getLegacyBlockStateMapper() : LegacyBlockStateMapper{
return self::$legacyBlockStateMapper ??= LegacyBlockStateMapper::loadFromString(
ErrorToExceptionHandler::trapAndRemoveFalse(fn() => file_get_contents(Path::join(BEDROCK_DATA_PATH, 'r12_to_current_block_map.bin'))),
LegacyBlockIdToStringIdMap::getInstance()
);
}
}

View File

@ -26,7 +26,9 @@ namespace pocketmine\world\format\io\leveldb;
use pocketmine\block\Block;
use pocketmine\block\BlockLegacyIds;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\data\bedrock\LegacyBlockIdToStringIdMap;
use pocketmine\data\bedrock\blockstate\BlockStateData;
use pocketmine\data\bedrock\blockstate\BlockStateDeserializeException;
use pocketmine\data\bedrock\blockstate\BlockTypeNames;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\NbtDataException;
use pocketmine\nbt\NbtException;
@ -44,7 +46,7 @@ use pocketmine\world\format\io\data\BedrockWorldData;
use pocketmine\world\format\io\exception\CorruptedChunkException;
use pocketmine\world\format\io\exception\CorruptedWorldException;
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
use pocketmine\world\format\io\SubChunkConverter;
use pocketmine\world\format\io\GlobalBlockStateHandlers;
use pocketmine\world\format\io\WorldData;
use pocketmine\world\format\io\WritableWorldProvider;
use pocketmine\world\format\PalettedBlockArray;
@ -70,32 +72,46 @@ use const LEVELDB_ZLIB_RAW_COMPRESSION;
class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
//According to Tomasso, these aren't supposed to be readable anymore. Thankfully he didn't change the readable ones...
/** @deprecated */
protected const TAG_DATA_2D = ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES;
/** @deprecated */
protected const TAG_DATA_2D_LEGACY = ChunkDataKey::HEIGHTMAP_AND_2D_BIOME_COLORS;
/** @deprecated */
protected const TAG_SUBCHUNK_PREFIX = ChunkDataKey::SUBCHUNK;
/** @deprecated */
protected const TAG_LEGACY_TERRAIN = ChunkDataKey::LEGACY_TERRAIN;
/** @deprecated */
protected const TAG_BLOCK_ENTITY = ChunkDataKey::BLOCK_ENTITIES;
/** @deprecated */
protected const TAG_ENTITY = ChunkDataKey::ENTITIES;
/** @deprecated */
protected const TAG_PENDING_TICK = ChunkDataKey::PENDING_SCHEDULED_TICKS;
/** @deprecated */
protected const TAG_BLOCK_EXTRA_DATA = ChunkDataKey::LEGACY_BLOCK_EXTRA_DATA;
/** @deprecated */
protected const TAG_BIOME_STATE = ChunkDataKey::BIOME_STATES;
/** @deprecated */
protected const TAG_STATE_FINALISATION = ChunkDataKey::FINALIZATION;
/** @deprecated */
protected const TAG_BORDER_BLOCKS = ChunkDataKey::BORDER_BLOCKS;
/** @deprecated */
protected const TAG_HARDCODED_SPAWNERS = ChunkDataKey::HARDCODED_SPAWNERS;
protected const FINALISATION_NEEDS_INSTATICKING = 0;
protected const FINALISATION_NEEDS_POPULATION = 1;
protected const FINALISATION_DONE = 2;
protected const TAG_VERSION = ChunkDataKey::OLD_VERSION;
/** @deprecated */
protected const TAG_VERSION = ChunkDataKey::NEW_VERSION;
protected const ENTRY_FLAT_WORLD_LAYERS = "game_flatworldlayers";
protected const CURRENT_LEVEL_CHUNK_VERSION = ChunkVersion::v1_2_0; //yes, I know this is wrong, but it ensures vanilla auto-fixes stuff we currently don't
protected const CURRENT_LEVEL_CHUNK_VERSION = ChunkVersion::v1_18_0_25_beta;
protected const CURRENT_LEVEL_SUBCHUNK_VERSION = SubChunkVersion::PALETTED_MULTI;
private const CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET = 4;
/** @var \LevelDB */
protected $db;
@ -171,18 +187,41 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
}
$nbt = new LittleEndianNbtSerializer();
$palette = [];
$idMap = LegacyBlockIdToStringIdMap::getInstance();
for($i = 0, $paletteSize = $stream->getLInt(); $i < $paletteSize; ++$i){
$paletteSize = $bitsPerBlock === 0 ? 1 : $stream->getLInt();
$blockStateDeserializer = GlobalBlockStateHandlers::getDeserializer();
for($i = 0; $i < $paletteSize; ++$i){
try{
$offset = $stream->getOffset();
$tag = $nbt->read($stream->getBuffer(), $offset)->mustGetCompoundTag();
$stream->setOffset($offset);
$id = $idMap->stringToLegacy($tag->getString("name")) ?? BlockLegacyIds::INFO_UPDATE;
$data = $tag->getShort("val");
$palette[] = ($id << Block::INTERNAL_METADATA_BITS) | $data;
}catch(NbtException $e){
if($tag->getTag("name") !== null && $tag->getTag("val") !== null){
//Legacy (pre-1.13) blockstate - upgrade it to a version we understand
$id = $tag->getString("name");
$data = $tag->getShort("val");
$blockStateData = GlobalBlockStateHandlers::getLegacyBlockStateMapper()->fromStringIdMeta($id, $data);
if($blockStateData === null){
//TODO: this might be a slightly-invalid state that isn't in the mapping table
$blockStateData = new BlockStateData(BlockTypeNames::INFO_UPDATE, CompoundTag::create(), BlockStateData::CURRENT_VERSION);
}
}else{
//Modern (post-1.13) blockstate
$blockStateData = BlockStateData::fromNbt($tag);
}
try{
$palette[] = $blockStateDeserializer->deserialize($blockStateData);
}catch(BlockStateDeserializeException){
//TODO: remember data for unknown states so we can implement them later
//TODO: this is slow; we need to cache this
//TODO: log this
$palette[] = $blockStateDeserializer->deserialize(new BlockStateData(BlockTypeNames::INFO_UPDATE, CompoundTag::create(), BlockStateData::CURRENT_VERSION));
}
}catch(NbtException | BlockStateDeserializeException $e){
throw new CorruptedChunkException("Invalid blockstate NBT at offset $i in paletted storage: " . $e->getMessage(), 0, $e);
}
}
@ -215,6 +254,9 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$extraDataLayers = [];
$binaryStream = new BinaryStream($extraRawData);
$count = $binaryStream->getLInt();
$legacyMapper = GlobalBlockStateHandlers::getLegacyBlockStateMapper();
$blockStateDeserializer = GlobalBlockStateHandlers::getDeserializer();
for($i = 0; $i < $count; ++$i){
$key = $binaryStream->getLInt();
$value = $binaryStream->getLShort();
@ -226,23 +268,49 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$blockId = $value & 0xff;
$blockData = ($value >> 8) & 0xf;
$blockStateData = $legacyMapper->fromIntIdMeta($blockId, $blockData);
if($blockStateData === null){
//TODO: we could preserve this in case it's supported in the future, but this was historically only
//used for grass anyway, so we probably don't need to care
continue;
}
$blockStateId = $blockStateDeserializer->deserialize($blockStateData);
if(!isset($extraDataLayers[$ySub])){
$extraDataLayers[$ySub] = new PalettedBlockArray(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS);
}
$extraDataLayers[$ySub]->set($x, $y, $z, ($blockId << Block::INTERNAL_METADATA_BITS) | $blockData);
$extraDataLayers[$ySub]->set($x, $y, $z, $blockStateId);
}
return $extraDataLayers;
}
private function readVersion(int $chunkX, int $chunkZ) : ?int{
$index = self::chunkIndex($chunkX, $chunkZ);
$chunkVersionRaw = $this->db->get($index . ChunkDataKey::NEW_VERSION);
if($chunkVersionRaw === false){
$chunkVersionRaw = $this->db->get($index . ChunkDataKey::OLD_VERSION);
if($chunkVersionRaw === false){
return null;
}
}
return ord($chunkVersionRaw);
}
private static function hasOffsetCavesAndCliffsSubChunks(int $chunkVersion) : bool{
return $chunkVersion >= ChunkVersion::v1_16_220_50_unused && $chunkVersion <= ChunkVersion::v1_16_230_50_unused;
}
/**
* @throws CorruptedChunkException
*/
public function loadChunk(int $chunkX, int $chunkZ) : ?ChunkData{
$index = LevelDB::chunkIndex($chunkX, $chunkZ);
$chunkVersionRaw = $this->db->get($index . ChunkDataKey::OLD_VERSION);
if($chunkVersionRaw === false){
$chunkVersion = $this->readVersion($chunkX, $chunkZ);
if($chunkVersion === null){
//TODO: this might be a slightly-corrupted chunk with a missing version field
return null;
}
@ -252,10 +320,36 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
/** @var BiomeArray|null $biomeArray */
$biomeArray = null;
$chunkVersion = ord($chunkVersionRaw);
$hasBeenUpgraded = $chunkVersion < self::CURRENT_LEVEL_CHUNK_VERSION;
$subChunkKeyOffset = self::hasOffsetCavesAndCliffsSubChunks($chunkVersion) ? self::CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET : 0;
switch($chunkVersion){
case ChunkVersion::v1_18_0_25_beta:
case ChunkVersion::v1_18_0_24_unused:
case ChunkVersion::v1_18_0_24_beta:
case ChunkVersion::v1_18_0_22_unused:
case ChunkVersion::v1_18_0_22_beta:
case ChunkVersion::v1_18_0_20_unused:
case ChunkVersion::v1_18_0_20_beta:
case ChunkVersion::v1_17_40_unused:
case ChunkVersion::v1_17_40_20_beta_experimental_caves_cliffs:
case ChunkVersion::v1_17_30_25_unused:
case ChunkVersion::v1_17_30_25_beta_experimental_caves_cliffs:
case ChunkVersion::v1_17_30_23_unused:
case ChunkVersion::v1_17_30_23_beta_experimental_caves_cliffs:
case ChunkVersion::v1_16_230_50_unused:
case ChunkVersion::v1_16_230_50_beta_experimental_caves_cliffs:
case ChunkVersion::v1_16_220_50_unused:
case ChunkVersion::v1_16_220_50_beta_experimental_caves_cliffs:
case ChunkVersion::v1_16_210:
case ChunkVersion::v1_16_100_57_beta:
case ChunkVersion::v1_16_100_52_beta:
case ChunkVersion::v1_16_0:
case ChunkVersion::v1_16_0_51_beta:
//TODO: check walls
case ChunkVersion::v1_12_0_unused2:
case ChunkVersion::v1_12_0_unused1:
case ChunkVersion::v1_12_0_4_beta:
case ChunkVersion::v1_11_1:
case ChunkVersion::v1_11_0_4_beta:
@ -266,13 +360,14 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
case ChunkVersion::v1_2_13:
case ChunkVersion::v1_2_0:
case ChunkVersion::v1_2_0_2_beta:
case ChunkVersion::v1_1_0_converted_from_console:
case ChunkVersion::v1_1_0:
//TODO: check beds
case ChunkVersion::v1_0_0:
$convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion);
for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){
if(($data = $this->db->get($index . ChunkDataKey::SUBCHUNK . chr($y))) === false){
if(($data = $this->db->get($index . ChunkDataKey::SUBCHUNK . chr($y + $subChunkKeyOffset))) === false){
continue;
}
@ -305,7 +400,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
$storages = [SubChunkConverter::convertSubChunkXZY($blocks, $blockData)];
$storages = [$this->palettizeLegacySubChunkXZY($blocks, $blockData)];
if(isset($convertedLegacyExtraData[$y])){
$storages[] = $convertedLegacyExtraData[$y];
}
@ -320,8 +415,13 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$subChunks[$y] = new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, $storages);
break;
case SubChunkVersion::PALETTED_MULTI:
case SubChunkVersion::PALETTED_MULTI_WITH_OFFSET:
//legacy extradata layers intentionally ignored because they aren't supposed to exist in v8
$storageCount = $binaryStream->getByte();
if($subChunkVersion >= SubChunkVersion::PALETTED_MULTI_WITH_OFFSET){
//height ignored; this seems pointless since this is already in the key anyway
$binaryStream->getByte();
}
if($storageCount > 0){
$storages = [];
@ -367,7 +467,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
}
for($yy = 0; $yy < 8; ++$yy){
$storages = [SubChunkConverter::convertSubChunkFromLegacyColumn($fullIds, $fullData, $yy)];
$storages = [$this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $yy)];
if(isset($convertedLegacyExtraData[$yy])){
$storages[] = $convertedLegacyExtraData[$yy];
}
@ -434,15 +534,40 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
}
public function saveChunk(int $chunkX, int $chunkZ, ChunkData $chunkData) : void{
$idMap = LegacyBlockIdToStringIdMap::getInstance();
$index = LevelDB::chunkIndex($chunkX, $chunkZ);
$write = new \LevelDBWriteBatch();
$write->put($index . ChunkDataKey::OLD_VERSION, chr(self::CURRENT_LEVEL_CHUNK_VERSION));
$previousVersion = $this->readVersion($chunkX, $chunkZ);
$write->put($index . ChunkDataKey::NEW_VERSION, chr(self::CURRENT_LEVEL_CHUNK_VERSION));
$chunk = $chunkData->getChunk();
//TODO: This ensures that negative subchunks don't get destroyed in newer worlds for as long as we don't yet
//support negative height. Since we don't save with a shift, if the old save had the subchunks shifted, we need
//to shift them to their correct positions to avoid destroying data.
//This can be removed once we support the full height.
if($previousVersion !== null && self::hasOffsetCavesAndCliffsSubChunks($previousVersion)){
$subChunks = $chunk->getSubChunks();
for($y = -4; $y <= 20; $y++){
$key = $index . ChunkDataKey::SUBCHUNK . chr($y + self::CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET);
if(
(!isset($subChunks[$y]) || !$chunk->getTerrainDirtyFlag(Chunk::DIRTY_FLAG_BLOCKS)) &&
($subChunkData = $this->db->get($key)) !== false
){
$write->delete($key);
$write->put($index . ChunkDataKey::SUBCHUNK . chr($y & 0xff), $subChunkData);
}
}
}
if($chunk->getTerrainDirtyFlag(Chunk::DIRTY_FLAG_BLOCKS)){
$subChunks = $chunk->getSubChunks();
//TODO: this should not rely on globals, but in PM4 we have no other option, and it's not worse than what we
//were doing before anyway ...
$blockStateSerializer = GlobalBlockStateHandlers::getSerializer();
foreach($subChunks as $y => $subChunk){
$key = $index . ChunkDataKey::SUBCHUNK . chr($y);
if($subChunk->isEmptyAuthoritative()){
@ -454,24 +579,16 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$layers = $subChunk->getBlockLayers();
$subStream->putByte(count($layers));
foreach($layers as $blocks){
if($blocks->getBitsPerBlock() !== 0){
$subStream->putByte($blocks->getBitsPerBlock() << 1);
$subStream->put($blocks->getWordArray());
}else{
//TODO: we use these in-memory, but they aren't supported on disk by the game yet
//polyfill them with a zero'd 1-bpb instead
$subStream->putByte(1 << 1);
$subStream->put(str_repeat("\x00", PalettedBlockArray::getExpectedWordArraySize(1)));
}
$subStream->putByte($blocks->getBitsPerBlock() << 1);
$subStream->put($blocks->getWordArray());
$palette = $blocks->getPalette();
$subStream->putLInt(count($palette));
if($blocks->getBitsPerBlock() !== 0){
$subStream->putLInt(count($palette));
}
$tags = [];
foreach($palette as $p){
$tags[] = new TreeRoot(CompoundTag::create()
->setString("name", $idMap->legacyToString($p >> Block::INTERNAL_METADATA_BITS) ?? "minecraft:info_update")
->setInt("oldid", $p >> Block::INTERNAL_METADATA_BITS) //PM only (debugging), vanilla doesn't have this
->setShort("val", $p & Block::INTERNAL_METADATA_MASK));
$tags[] = new TreeRoot($blockStateSerializer->serialize($p)->toNbt());
}
$subStream->put((new LittleEndianNbtSerializer())->writeMultiple($tags));
@ -528,7 +645,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
public function getAllChunks(bool $skipCorrupted = false, ?\Logger $logger = null) : \Generator{
foreach($this->db->getIterator() as $key => $_){
if(strlen($key) === 9 && substr($key, -1) === ChunkDataKey::OLD_VERSION){
if(strlen($key) === 9 && ($key[8] === ChunkDataKey::NEW_VERSION || $key[8] === ChunkDataKey::OLD_VERSION)){
$chunkX = Binary::readLInt(substr($key, 0, 4));
$chunkZ = Binary::readLInt(substr($key, 4, 4));
try{
@ -550,7 +667,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
public function calculateChunkCount() : int{
$count = 0;
foreach($this->db->getIterator() as $key => $_){
if(strlen($key) === 9 && substr($key, -1) === ChunkDataKey::OLD_VERSION){
if(strlen($key) === 9 && ($key[8] === ChunkDataKey::NEW_VERSION || $key[8] === ChunkDataKey::OLD_VERSION)){
$count++;
}
}

View File

@ -26,14 +26,13 @@ namespace pocketmine\world\format\io\region;
use pocketmine\block\Block;
use pocketmine\block\BlockLegacyIds;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\world\format\io\SubChunkConverter;
use pocketmine\world\format\SubChunk;
class Anvil extends RegionWorldProvider{
use LegacyAnvilChunkTrait;
protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk{
return new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, [SubChunkConverter::convertSubChunkYZX(
return new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, [$this->palettizeLegacySubChunkYZX(
self::readFixedSizeByteArray($subChunk, "Blocks", 4096),
self::readFixedSizeByteArray($subChunk, "Data", 2048)
)]);

View File

@ -38,7 +38,6 @@ 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\io\SubChunkConverter;
use pocketmine\world\format\SubChunk;
use function zlib_decode;
@ -75,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, [SubChunkConverter::convertSubChunkFromLegacyColumn($fullIds, $fullData, $y)]);
$subChunks[$y] = new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, [$this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $y)]);
}
$makeBiomeArray = function(string $biomeIds) : BiomeArray{

View File

@ -26,7 +26,6 @@ namespace pocketmine\world\format\io\region;
use pocketmine\block\Block;
use pocketmine\block\BlockLegacyIds;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\world\format\io\SubChunkConverter;
use pocketmine\world\format\SubChunk;
/**
@ -37,7 +36,7 @@ class PMAnvil extends RegionWorldProvider{
use LegacyAnvilChunkTrait;
protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk{
return new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, [SubChunkConverter::convertSubChunkXZY(
return new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, [$this->palettizeLegacySubChunkXZY(
self::readFixedSizeByteArray($subChunk, "Blocks", 4096),
self::readFixedSizeByteArray($subChunk, "Data", 2048)
)]);

View File

@ -44,7 +44,7 @@ class BlockStateUpgraderTest extends TestCase{
private BlockStateUpgrader $upgrader;
public function setUp() : void{
$this->upgrader = new BlockStateUpgrader();
$this->upgrader = new BlockStateUpgrader([]);
}
private function getNewSchema() : BlockStateUpgradeSchema{