mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-06-06 20:07:09 +00:00
Refactor hierarchy of Region-based world formats
In the future, McRegion and PMAnvil will become deprecated legacy formats which we won't support for writing anymore. However, this brings complications because Anvil (which would not be deprecated) would be sandwiched between its deprecated base (McRegion) and its deprecated child (PMAnvil), complicating the implementation (particularly for PMAnvil). This abstracts away the generic Region-based world functionality into a separate class, making McRegion, Anvil and PMAnvil all inherit from it directly. Since only the chunk formats are different, they are more accurately siblings rather than parent-child (although Anvil and PMAnvil are cousins, ish).
This commit is contained in:
parent
eaf6b316c7
commit
65b49dec71
@ -23,67 +23,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\level\format\io\region;
|
||||
|
||||
use pocketmine\level\format\Chunk;
|
||||
use pocketmine\level\format\ChunkException;
|
||||
use pocketmine\level\format\io\ChunkUtils;
|
||||
use pocketmine\level\format\SubChunk;
|
||||
use pocketmine\nbt\BigEndianNBTStream;
|
||||
use pocketmine\nbt\NBT;
|
||||
use pocketmine\nbt\tag\ByteArrayTag;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\IntArrayTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
|
||||
class Anvil extends McRegion{
|
||||
|
||||
public const REGION_FILE_EXTENSION = "mca";
|
||||
|
||||
protected function nbtSerialize(Chunk $chunk) : string{
|
||||
$nbt = new CompoundTag("Level", []);
|
||||
$nbt->setInt("xPos", $chunk->getX());
|
||||
$nbt->setInt("zPos", $chunk->getZ());
|
||||
|
||||
$nbt->setByte("V", 1);
|
||||
$nbt->setLong("LastUpdate", 0); //TODO
|
||||
$nbt->setLong("InhabitedTime", 0); //TODO
|
||||
$nbt->setByte("TerrainPopulated", $chunk->isPopulated() ? 1 : 0);
|
||||
$nbt->setByte("LightPopulated", $chunk->isLightPopulated() ? 1 : 0);
|
||||
|
||||
$subChunks = [];
|
||||
foreach($chunk->getSubChunks() as $y => $subChunk){
|
||||
if($subChunk->isEmpty()){
|
||||
continue;
|
||||
}
|
||||
|
||||
$tag = $this->serializeSubChunk($subChunk);
|
||||
$tag->setByte("Y", $y);
|
||||
$subChunks[] = $tag;
|
||||
}
|
||||
$nbt->setTag(new ListTag("Sections", $subChunks, NBT::TAG_Compound));
|
||||
|
||||
$nbt->setByteArray("Biomes", $chunk->getBiomeIdArray());
|
||||
$nbt->setIntArray("HeightMap", $chunk->getHeightMapArray());
|
||||
|
||||
$entities = [];
|
||||
|
||||
foreach($chunk->getSavableEntities() as $entity){
|
||||
$entities[] = $entity->saveNBT();
|
||||
}
|
||||
|
||||
$nbt->setTag(new ListTag("Entities", $entities, NBT::TAG_Compound));
|
||||
|
||||
$tiles = [];
|
||||
foreach($chunk->getTiles() as $tile){
|
||||
$tiles[] = $tile->saveNBT();
|
||||
}
|
||||
|
||||
$nbt->setTag(new ListTag("TileEntities", $tiles, NBT::TAG_Compound));
|
||||
|
||||
//TODO: TileTicks
|
||||
|
||||
$writer = new BigEndianNBTStream();
|
||||
return $writer->writeCompressed(new CompoundTag("", [$nbt]), ZLIB_ENCODING_DEFLATE, RegionLoader::$COMPRESSION_LEVEL);
|
||||
}
|
||||
class Anvil extends RegionLevelProvider{
|
||||
use LegacyAnvilChunkTrait;
|
||||
|
||||
protected function serializeSubChunk(SubChunk $subChunk) : CompoundTag{
|
||||
return new CompoundTag("", [
|
||||
@ -94,44 +40,6 @@ class Anvil extends McRegion{
|
||||
]);
|
||||
}
|
||||
|
||||
protected function nbtDeserialize(string $data) : Chunk{
|
||||
$nbt = new BigEndianNBTStream();
|
||||
$chunk = $nbt->readCompressed($data);
|
||||
if(!($chunk instanceof CompoundTag) or !$chunk->hasTag("Level")){
|
||||
throw new ChunkException("Invalid NBT format");
|
||||
}
|
||||
|
||||
$chunk = $chunk->getCompoundTag("Level");
|
||||
|
||||
$subChunks = [];
|
||||
$subChunksTag = $chunk->getListTag("Sections") ?? [];
|
||||
foreach($subChunksTag as $subChunk){
|
||||
if($subChunk instanceof CompoundTag){
|
||||
$subChunks[$subChunk->getByte("Y")] = $this->deserializeSubChunk($subChunk);
|
||||
}
|
||||
}
|
||||
|
||||
if($chunk->hasTag("BiomeColors", IntArrayTag::class)){
|
||||
$biomeIds = ChunkUtils::convertBiomeColors($chunk->getIntArray("BiomeColors")); //Convert back to original format
|
||||
}else{
|
||||
$biomeIds = $chunk->getByteArray("Biomes", "", true);
|
||||
}
|
||||
|
||||
$result = new Chunk(
|
||||
$chunk->getInt("xPos"),
|
||||
$chunk->getInt("zPos"),
|
||||
$subChunks,
|
||||
$chunk->hasTag("Entities", ListTag::class) ? $chunk->getListTag("Entities")->getValue() : [],
|
||||
$chunk->hasTag("TileEntities", ListTag::class) ? $chunk->getListTag("TileEntities")->getValue() : [],
|
||||
$biomeIds,
|
||||
$chunk->getIntArray("HeightMap", [])
|
||||
);
|
||||
$result->setLightPopulated($chunk->getByte("LightPopulated", 0) !== 0);
|
||||
$result->setPopulated($chunk->getByte("TerrainPopulated", 0) !== 0);
|
||||
$result->setGenerated();
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk{
|
||||
return new SubChunk(
|
||||
ChunkUtils::reorderByteArray($subChunk->getByteArray("Blocks")),
|
||||
@ -145,8 +53,12 @@ class Anvil extends McRegion{
|
||||
return "anvil";
|
||||
}
|
||||
|
||||
public static function getPcWorldFormatVersion() : int{
|
||||
return 19133; //anvil
|
||||
protected static function getRegionFileExtension() : string{
|
||||
return "mca";
|
||||
}
|
||||
|
||||
protected static function getPcWorldFormatVersion() : int{
|
||||
return 19133;
|
||||
}
|
||||
|
||||
public function getWorldHeight() : int{
|
||||
|
138
src/pocketmine/level/format/io/region/LegacyAnvilChunkTrait.php
Normal file
138
src/pocketmine/level/format/io/region/LegacyAnvilChunkTrait.php
Normal file
@ -0,0 +1,138 @@
|
||||
<?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\level\format\io\region;
|
||||
|
||||
use pocketmine\level\format\Chunk;
|
||||
use pocketmine\level\format\ChunkException;
|
||||
use pocketmine\level\format\io\ChunkUtils;
|
||||
use pocketmine\level\format\SubChunk;
|
||||
use pocketmine\nbt\BigEndianNBTStream;
|
||||
use pocketmine\nbt\NBT;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\IntArrayTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
|
||||
/**
|
||||
* Trait containing I/O methods for handling legacy Anvil-style chunks.
|
||||
*
|
||||
* Motivation: In the future PMAnvil will become a legacy read-only format, but Anvil will continue to exist for the sake
|
||||
* of handling worlds in the PC 1.13 format. Thus, we don't want PMAnvil getting accidentally influenced by changes
|
||||
* happening to the underlying Anvil, because it only uses the legacy part.
|
||||
*
|
||||
* TODO: When the formats are deprecated, the write parts of this trait can be eliminated.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait LegacyAnvilChunkTrait{
|
||||
|
||||
protected function serializeChunk(Chunk $chunk) : string{
|
||||
$nbt = new CompoundTag("Level", []);
|
||||
$nbt->setInt("xPos", $chunk->getX());
|
||||
$nbt->setInt("zPos", $chunk->getZ());
|
||||
|
||||
$nbt->setByte("V", 1);
|
||||
$nbt->setLong("LastUpdate", 0); //TODO
|
||||
$nbt->setLong("InhabitedTime", 0); //TODO
|
||||
$nbt->setByte("TerrainPopulated", $chunk->isPopulated() ? 1 : 0);
|
||||
$nbt->setByte("LightPopulated", $chunk->isLightPopulated() ? 1 : 0);
|
||||
|
||||
$subChunks = [];
|
||||
foreach($chunk->getSubChunks() as $y => $subChunk){
|
||||
if($subChunk->isEmpty()){
|
||||
continue;
|
||||
}
|
||||
|
||||
$tag = $this->serializeSubChunk($subChunk);
|
||||
$tag->setByte("Y", $y);
|
||||
$subChunks[] = $tag;
|
||||
}
|
||||
$nbt->setTag(new ListTag("Sections", $subChunks, NBT::TAG_Compound));
|
||||
|
||||
$nbt->setByteArray("Biomes", $chunk->getBiomeIdArray());
|
||||
$nbt->setIntArray("HeightMap", $chunk->getHeightMapArray());
|
||||
|
||||
$entities = [];
|
||||
|
||||
foreach($chunk->getSavableEntities() as $entity){
|
||||
$entities[] = $entity->saveNBT();
|
||||
}
|
||||
|
||||
$nbt->setTag(new ListTag("Entities", $entities, NBT::TAG_Compound));
|
||||
|
||||
$tiles = [];
|
||||
foreach($chunk->getTiles() as $tile){
|
||||
$tiles[] = $tile->saveNBT();
|
||||
}
|
||||
|
||||
$nbt->setTag(new ListTag("TileEntities", $tiles, NBT::TAG_Compound));
|
||||
|
||||
//TODO: TileTicks
|
||||
|
||||
$writer = new BigEndianNBTStream();
|
||||
return $writer->writeCompressed(new CompoundTag("", [$nbt]), ZLIB_ENCODING_DEFLATE, RegionLoader::$COMPRESSION_LEVEL);
|
||||
}
|
||||
|
||||
abstract protected function serializeSubChunk(SubChunk $subChunk) : CompoundTag;
|
||||
|
||||
protected function deserializeChunk(string $data) : Chunk{
|
||||
$nbt = new BigEndianNBTStream();
|
||||
$chunk = $nbt->readCompressed($data);
|
||||
if(!($chunk instanceof CompoundTag) or !$chunk->hasTag("Level")){
|
||||
throw new ChunkException("Invalid NBT format");
|
||||
}
|
||||
|
||||
$chunk = $chunk->getCompoundTag("Level");
|
||||
|
||||
$subChunks = [];
|
||||
$subChunksTag = $chunk->getListTag("Sections") ?? [];
|
||||
foreach($subChunksTag as $subChunk){
|
||||
if($subChunk instanceof CompoundTag){
|
||||
$subChunks[$subChunk->getByte("Y")] = $this->deserializeSubChunk($subChunk);
|
||||
}
|
||||
}
|
||||
|
||||
if($chunk->hasTag("BiomeColors", IntArrayTag::class)){
|
||||
$biomeIds = ChunkUtils::convertBiomeColors($chunk->getIntArray("BiomeColors")); //Convert back to original format
|
||||
}else{
|
||||
$biomeIds = $chunk->getByteArray("Biomes", "", true);
|
||||
}
|
||||
|
||||
$result = new Chunk(
|
||||
$chunk->getInt("xPos"),
|
||||
$chunk->getInt("zPos"),
|
||||
$subChunks,
|
||||
$chunk->hasTag("Entities", ListTag::class) ? $chunk->getListTag("Entities")->getValue() : [],
|
||||
$chunk->hasTag("TileEntities", ListTag::class) ? $chunk->getListTag("TileEntities")->getValue() : [],
|
||||
$biomeIds,
|
||||
$chunk->getIntArray("HeightMap", [])
|
||||
);
|
||||
$result->setLightPopulated($chunk->getByte("LightPopulated", 0) !== 0);
|
||||
$result->setPopulated($chunk->getByte("TerrainPopulated", 0) !== 0);
|
||||
$result->setGenerated();
|
||||
return $result;
|
||||
}
|
||||
|
||||
abstract protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk;
|
||||
|
||||
}
|
@ -25,31 +25,23 @@ namespace pocketmine\level\format\io\region;
|
||||
|
||||
use pocketmine\level\format\Chunk;
|
||||
use pocketmine\level\format\ChunkException;
|
||||
use pocketmine\level\format\io\BaseLevelProvider;
|
||||
use pocketmine\level\format\io\ChunkUtils;
|
||||
use pocketmine\level\format\SubChunk;
|
||||
use pocketmine\level\generator\GeneratorManager;
|
||||
use pocketmine\level\Level;
|
||||
use pocketmine\nbt\BigEndianNBTStream;
|
||||
use pocketmine\nbt\NBT;
|
||||
use pocketmine\nbt\tag\{
|
||||
ByteArrayTag, ByteTag, CompoundTag, FloatTag, IntArrayTag, IntTag, ListTag, LongTag, StringTag
|
||||
};
|
||||
use pocketmine\utils\MainLogger;
|
||||
use pocketmine\nbt\tag\ByteArrayTag;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\IntArrayTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
|
||||
class McRegion extends BaseLevelProvider{
|
||||
|
||||
public const REGION_FILE_EXTENSION = "mcr";
|
||||
|
||||
/** @var RegionLoader[] */
|
||||
protected $regions = [];
|
||||
class McRegion extends RegionLevelProvider{
|
||||
|
||||
/**
|
||||
* @param Chunk $chunk
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function nbtSerialize(Chunk $chunk) : string{
|
||||
protected function serializeChunk(Chunk $chunk) : string{
|
||||
$nbt = new CompoundTag("Level", []);
|
||||
$nbt->setInt("xPos", $chunk->getX());
|
||||
$nbt->setInt("zPos", $chunk->getZ());
|
||||
@ -107,7 +99,7 @@ class McRegion extends BaseLevelProvider{
|
||||
*
|
||||
* @return Chunk
|
||||
*/
|
||||
protected function nbtDeserialize(string $data) : Chunk{
|
||||
protected function deserializeChunk(string $data) : Chunk{
|
||||
$nbt = new BigEndianNBTStream();
|
||||
$chunk = $nbt->readCompressed($data);
|
||||
if(!($chunk instanceof CompoundTag) or !$chunk->hasTag("Level")){
|
||||
@ -184,258 +176,16 @@ class McRegion extends BaseLevelProvider{
|
||||
return "mcregion";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage version as per Minecraft PC world formats.
|
||||
* @return int
|
||||
*/
|
||||
public static function getPcWorldFormatVersion() : int{
|
||||
return 19132; //mcregion
|
||||
protected static function getRegionFileExtension() : string{
|
||||
return "mcr";
|
||||
}
|
||||
|
||||
protected static function getPcWorldFormatVersion() : int{
|
||||
return 19132;
|
||||
}
|
||||
|
||||
public function getWorldHeight() : int{
|
||||
//TODO: add world height options
|
||||
return 128;
|
||||
}
|
||||
|
||||
public static function isValid(string $path) : bool{
|
||||
$isValid = (file_exists($path . "/level.dat") and is_dir($path . "/region/"));
|
||||
|
||||
if($isValid){
|
||||
$files = array_filter(scandir($path . "/region/", SCANDIR_SORT_NONE), function($file){
|
||||
return substr($file, strrpos($file, ".") + 1, 2) === "mc"; //region file
|
||||
});
|
||||
|
||||
foreach($files as $f){
|
||||
if(substr($f, strrpos($f, ".") + 1) !== static::REGION_FILE_EXTENSION){
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
public static function generate(string $path, string $name, int $seed, string $generator, array $options = []){
|
||||
if(!file_exists($path)){
|
||||
mkdir($path, 0777, true);
|
||||
}
|
||||
|
||||
if(!file_exists($path . "/region")){
|
||||
mkdir($path . "/region", 0777);
|
||||
}
|
||||
//TODO, add extra details
|
||||
$levelData = new CompoundTag("Data", [
|
||||
new ByteTag("hardcore", ($options["hardcore"] ?? false) === true ? 1 : 0),
|
||||
new ByteTag("Difficulty", Level::getDifficultyFromString((string) ($options["difficulty"] ?? "normal"))),
|
||||
new ByteTag("initialized", 1),
|
||||
new IntTag("GameType", 0),
|
||||
new IntTag("generatorVersion", 1), //2 in MCPE
|
||||
new IntTag("SpawnX", 256),
|
||||
new IntTag("SpawnY", 70),
|
||||
new IntTag("SpawnZ", 256),
|
||||
new IntTag("version", static::getPcWorldFormatVersion()),
|
||||
new IntTag("DayTime", 0),
|
||||
new LongTag("LastPlayed", (int) (microtime(true) * 1000)),
|
||||
new LongTag("RandomSeed", $seed),
|
||||
new LongTag("SizeOnDisk", 0),
|
||||
new LongTag("Time", 0),
|
||||
new StringTag("generatorName", GeneratorManager::getGeneratorName($generator)),
|
||||
new StringTag("generatorOptions", $options["preset"] ?? ""),
|
||||
new StringTag("LevelName", $name),
|
||||
new CompoundTag("GameRules", [])
|
||||
]);
|
||||
$nbt = new BigEndianNBTStream();
|
||||
$buffer = $nbt->writeCompressed(new CompoundTag("", [
|
||||
$levelData
|
||||
]));
|
||||
file_put_contents($path . "level.dat", $buffer);
|
||||
}
|
||||
|
||||
public function getGenerator() : string{
|
||||
return $this->levelData->getString("generatorName", "DEFAULT");
|
||||
}
|
||||
|
||||
public function getGeneratorOptions() : array{
|
||||
return ["preset" => $this->levelData->getString("generatorOptions", "")];
|
||||
}
|
||||
|
||||
public function getDifficulty() : int{
|
||||
return $this->levelData->getByte("Difficulty", Level::DIFFICULTY_NORMAL);
|
||||
}
|
||||
|
||||
public function setDifficulty(int $difficulty){
|
||||
$this->levelData->setByte("Difficulty", $difficulty);
|
||||
}
|
||||
|
||||
public function getRainTime() : int{
|
||||
return $this->levelData->getInt("rainTime", 0);
|
||||
}
|
||||
|
||||
public function setRainTime(int $ticks) : void{
|
||||
$this->levelData->setInt("rainTime", $ticks);
|
||||
}
|
||||
|
||||
public function getRainLevel() : float{
|
||||
if($this->levelData->hasTag("rainLevel", FloatTag::class)){ //PocketMine/MCPE
|
||||
return $this->levelData->getFloat("rainLevel");
|
||||
}
|
||||
|
||||
return (float) $this->levelData->getByte("raining", 0); //PC vanilla
|
||||
}
|
||||
|
||||
public function setRainLevel(float $level) : void{
|
||||
$this->levelData->setFloat("rainLevel", $level); //PocketMine/MCPE
|
||||
$this->levelData->setByte("raining", (int) ceil($level)); //PC vanilla
|
||||
}
|
||||
|
||||
public function getLightningTime() : int{
|
||||
return $this->levelData->getInt("thunderTime", 0);
|
||||
}
|
||||
|
||||
public function setLightningTime(int $ticks) : void{
|
||||
$this->levelData->setInt("thunderTime", $ticks);
|
||||
}
|
||||
|
||||
public function getLightningLevel() : float{
|
||||
if($this->levelData->hasTag("lightningLevel", FloatTag::class)){ //PocketMine/MCPE
|
||||
return $this->levelData->getFloat("lightningLevel");
|
||||
}
|
||||
|
||||
return (float) $this->levelData->getByte("thundering", 0); //PC vanilla
|
||||
}
|
||||
|
||||
public function setLightningLevel(float $level) : void{
|
||||
$this->levelData->setFloat("lightningLevel", $level); //PocketMine/MCPE
|
||||
$this->levelData->setByte("thundering", (int) ceil($level)); //PC vanilla
|
||||
}
|
||||
|
||||
public function doGarbageCollection(){
|
||||
$limit = time() - 300;
|
||||
foreach($this->regions as $index => $region){
|
||||
if($region->lastUsed <= $limit){
|
||||
$region->close();
|
||||
unset($this->regions[$index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $chunkX
|
||||
* @param int $chunkZ
|
||||
* @param int &$regionX
|
||||
* @param int &$regionZ
|
||||
*/
|
||||
public static function getRegionIndex(int $chunkX, int $chunkZ, &$regionX, &$regionZ){
|
||||
$regionX = $chunkX >> 5;
|
||||
$regionZ = $chunkZ >> 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $regionX
|
||||
* @param int $regionZ
|
||||
*
|
||||
* @return RegionLoader|null
|
||||
*/
|
||||
protected function getRegion(int $regionX, int $regionZ){
|
||||
return $this->regions[Level::chunkHash($regionX, $regionZ)] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to a specific region file based on its X/Z coordinates
|
||||
*
|
||||
* @param int $regionX
|
||||
* @param int $regionZ
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function pathToRegion(int $regionX, int $regionZ) : string{
|
||||
return $this->path . "region/r.$regionX.$regionZ." . static::REGION_FILE_EXTENSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $regionX
|
||||
* @param int $regionZ
|
||||
*/
|
||||
protected function loadRegion(int $regionX, int $regionZ){
|
||||
if(!isset($this->regions[$index = Level::chunkHash($regionX, $regionZ)])){
|
||||
$path = $this->pathToRegion($regionX, $regionZ);
|
||||
|
||||
$region = new RegionLoader($path, $regionX, $regionZ);
|
||||
try{
|
||||
$region->open();
|
||||
}catch(CorruptedRegionException $e){
|
||||
$logger = MainLogger::getLogger();
|
||||
$logger->error("Corrupted region file detected: " . $e->getMessage());
|
||||
|
||||
$region->close(false); //Do not write anything to the file
|
||||
|
||||
$backupPath = $path . ".bak." . time();
|
||||
rename($path, $backupPath);
|
||||
$logger->error("Corrupted region file has been backed up to " . $backupPath);
|
||||
|
||||
$region = new RegionLoader($path, $regionX, $regionZ);
|
||||
$region->open(); //this will create a new empty region to replace the corrupted one
|
||||
}
|
||||
|
||||
$this->regions[$index] = $region;
|
||||
}
|
||||
}
|
||||
|
||||
public function close(){
|
||||
foreach($this->regions as $index => $region){
|
||||
$region->close();
|
||||
unset($this->regions[$index]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function readChunk(int $chunkX, int $chunkZ) : ?Chunk{
|
||||
$regionX = $regionZ = null;
|
||||
self::getRegionIndex($chunkX, $chunkZ, $regionX, $regionZ);
|
||||
assert(is_int($regionX) and is_int($regionZ));
|
||||
|
||||
$this->loadRegion($regionX, $regionZ);
|
||||
|
||||
$chunkData = $this->getRegion($regionX, $regionZ)->readChunk($chunkX & 0x1f, $chunkZ & 0x1f);
|
||||
if($chunkData !== null){
|
||||
return $this->nbtDeserialize($chunkData);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function writeChunk(Chunk $chunk) : void{
|
||||
$chunkX = $chunk->getX();
|
||||
$chunkZ = $chunk->getZ();
|
||||
|
||||
self::getRegionIndex($chunkX, $chunkZ, $regionX, $regionZ);
|
||||
$this->loadRegion($regionX, $regionZ);
|
||||
|
||||
$this->getRegion($regionX, $regionZ)->writeChunk($chunkX & 0x1f, $chunkZ & 0x1f, $this->nbtSerialize($chunk));
|
||||
}
|
||||
|
||||
public function getAllChunks() : \Generator{
|
||||
$iterator = new \RegexIterator(
|
||||
new \FilesystemIterator(
|
||||
$this->path . '/region/',
|
||||
\FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS
|
||||
),
|
||||
'/\/r\.(-?\d+)\.(-?\d+)\.' . static::REGION_FILE_EXTENSION . '$/',
|
||||
\RegexIterator::GET_MATCH
|
||||
);
|
||||
|
||||
foreach($iterator as $region){
|
||||
$rX = ((int) $region[1]) << 5;
|
||||
$rZ = ((int) $region[2]) << 5;
|
||||
|
||||
for($chunkX = $rX; $chunkX < $rX + 32; ++$chunkX){
|
||||
for($chunkZ = $rZ; $chunkZ < $rZ + 32; ++$chunkZ){
|
||||
$chunk = $this->loadChunk($chunkX, $chunkZ);
|
||||
if($chunk !== null){
|
||||
yield $chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,9 +31,8 @@ use pocketmine\nbt\tag\CompoundTag;
|
||||
* This format is exactly the same as the PC Anvil format, with the only difference being that the stored data order
|
||||
* is XZY instead of YZX for more performance loading and saving worlds.
|
||||
*/
|
||||
class PMAnvil extends Anvil{
|
||||
|
||||
public const REGION_FILE_EXTENSION = "mcapm";
|
||||
class PMAnvil extends RegionLevelProvider{
|
||||
use LegacyAnvilChunkTrait;
|
||||
|
||||
protected function serializeSubChunk(SubChunk $subChunk) : CompoundTag{
|
||||
return new CompoundTag("", [
|
||||
@ -57,7 +56,15 @@ class PMAnvil extends Anvil{
|
||||
return "pmanvil";
|
||||
}
|
||||
|
||||
public static function getPcWorldFormatVersion() : int{
|
||||
protected static function getRegionFileExtension() : string{
|
||||
return "mcapm";
|
||||
}
|
||||
|
||||
protected static function getPcWorldFormatVersion() : int{
|
||||
return -1; //Not a PC format, only PocketMine-MP
|
||||
}
|
||||
|
||||
public function getWorldHeight() : int{
|
||||
return 256;
|
||||
}
|
||||
}
|
||||
|
303
src/pocketmine/level/format/io/region/RegionLevelProvider.php
Normal file
303
src/pocketmine/level/format/io/region/RegionLevelProvider.php
Normal file
@ -0,0 +1,303 @@
|
||||
<?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\level\format\io\region;
|
||||
|
||||
use pocketmine\level\format\Chunk;
|
||||
use pocketmine\level\format\io\BaseLevelProvider;
|
||||
use pocketmine\level\generator\GeneratorManager;
|
||||
use pocketmine\level\Level;
|
||||
use pocketmine\nbt\BigEndianNBTStream;
|
||||
use pocketmine\nbt\tag\ByteTag;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\FloatTag;
|
||||
use pocketmine\nbt\tag\IntTag;
|
||||
use pocketmine\nbt\tag\LongTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\utils\MainLogger;
|
||||
|
||||
abstract class RegionLevelProvider extends BaseLevelProvider{
|
||||
|
||||
/**
|
||||
* Returns the file extension used for regions in this region-based format.
|
||||
* @return string
|
||||
*/
|
||||
abstract protected static function getRegionFileExtension() : string;
|
||||
|
||||
/**
|
||||
* Returns the storage version as per Minecraft PC world formats.
|
||||
* @return int
|
||||
*/
|
||||
abstract protected static function getPcWorldFormatVersion() : int;
|
||||
|
||||
public static function isValid(string $path) : bool{
|
||||
$isValid = (file_exists($path . "/level.dat") and is_dir($path . "/region/"));
|
||||
|
||||
if($isValid){
|
||||
$files = array_filter(scandir($path . "/region/", SCANDIR_SORT_NONE), function($file){
|
||||
return substr($file, strrpos($file, ".") + 1, 2) === "mc"; //region file
|
||||
});
|
||||
|
||||
$ext = static::getRegionFileExtension();
|
||||
foreach($files as $f){
|
||||
if(substr($f, strrpos($f, ".") + 1) !== $ext){
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
public static function generate(string $path, string $name, int $seed, string $generator, array $options = []){
|
||||
if(!file_exists($path)){
|
||||
mkdir($path, 0777, true);
|
||||
}
|
||||
|
||||
if(!file_exists($path . "/region")){
|
||||
mkdir($path . "/region", 0777);
|
||||
}
|
||||
//TODO, add extra details
|
||||
$levelData = new CompoundTag("Data", [
|
||||
new ByteTag("hardcore", ($options["hardcore"] ?? false) === true ? 1 : 0),
|
||||
new ByteTag("Difficulty", Level::getDifficultyFromString((string) ($options["difficulty"] ?? "normal"))),
|
||||
new ByteTag("initialized", 1),
|
||||
new IntTag("GameType", 0),
|
||||
new IntTag("generatorVersion", 1), //2 in MCPE
|
||||
new IntTag("SpawnX", 256),
|
||||
new IntTag("SpawnY", 70),
|
||||
new IntTag("SpawnZ", 256),
|
||||
new IntTag("version", static::getPcWorldFormatVersion()),
|
||||
new IntTag("DayTime", 0),
|
||||
new LongTag("LastPlayed", (int) (microtime(true) * 1000)),
|
||||
new LongTag("RandomSeed", $seed),
|
||||
new LongTag("SizeOnDisk", 0),
|
||||
new LongTag("Time", 0),
|
||||
new StringTag("generatorName", GeneratorManager::getGeneratorName($generator)),
|
||||
new StringTag("generatorOptions", $options["preset"] ?? ""),
|
||||
new StringTag("LevelName", $name),
|
||||
new CompoundTag("GameRules", [])
|
||||
]);
|
||||
$nbt = new BigEndianNBTStream();
|
||||
$buffer = $nbt->writeCompressed(new CompoundTag("", [
|
||||
$levelData
|
||||
]));
|
||||
file_put_contents($path . "level.dat", $buffer);
|
||||
}
|
||||
|
||||
/** @var RegionLoader[] */
|
||||
protected $regions = [];
|
||||
|
||||
|
||||
public function getGenerator() : string{
|
||||
return $this->levelData->getString("generatorName", "DEFAULT");
|
||||
}
|
||||
|
||||
public function getGeneratorOptions() : array{
|
||||
return ["preset" => $this->levelData->getString("generatorOptions", "")];
|
||||
}
|
||||
|
||||
public function getDifficulty() : int{
|
||||
return $this->levelData->getByte("Difficulty", Level::DIFFICULTY_NORMAL);
|
||||
}
|
||||
|
||||
public function setDifficulty(int $difficulty){
|
||||
$this->levelData->setByte("Difficulty", $difficulty);
|
||||
}
|
||||
|
||||
public function getRainTime() : int{
|
||||
return $this->levelData->getInt("rainTime", 0);
|
||||
}
|
||||
|
||||
public function setRainTime(int $ticks) : void{
|
||||
$this->levelData->setInt("rainTime", $ticks);
|
||||
}
|
||||
|
||||
public function getRainLevel() : float{
|
||||
if($this->levelData->hasTag("rainLevel", FloatTag::class)){ //PocketMine/MCPE
|
||||
return $this->levelData->getFloat("rainLevel");
|
||||
}
|
||||
|
||||
return (float) $this->levelData->getByte("raining", 0); //PC vanilla
|
||||
}
|
||||
|
||||
public function setRainLevel(float $level) : void{
|
||||
$this->levelData->setFloat("rainLevel", $level); //PocketMine/MCPE
|
||||
$this->levelData->setByte("raining", (int) ceil($level)); //PC vanilla
|
||||
}
|
||||
|
||||
public function getLightningTime() : int{
|
||||
return $this->levelData->getInt("thunderTime", 0);
|
||||
}
|
||||
|
||||
public function setLightningTime(int $ticks) : void{
|
||||
$this->levelData->setInt("thunderTime", $ticks);
|
||||
}
|
||||
|
||||
public function getLightningLevel() : float{
|
||||
if($this->levelData->hasTag("lightningLevel", FloatTag::class)){ //PocketMine/MCPE
|
||||
return $this->levelData->getFloat("lightningLevel");
|
||||
}
|
||||
|
||||
return (float) $this->levelData->getByte("thundering", 0); //PC vanilla
|
||||
}
|
||||
|
||||
public function setLightningLevel(float $level) : void{
|
||||
$this->levelData->setFloat("lightningLevel", $level); //PocketMine/MCPE
|
||||
$this->levelData->setByte("thundering", (int) ceil($level)); //PC vanilla
|
||||
}
|
||||
|
||||
public function doGarbageCollection(){
|
||||
$limit = time() - 300;
|
||||
foreach($this->regions as $index => $region){
|
||||
if($region->lastUsed <= $limit){
|
||||
$region->close();
|
||||
unset($this->regions[$index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $chunkX
|
||||
* @param int $chunkZ
|
||||
* @param int &$regionX
|
||||
* @param int &$regionZ
|
||||
*/
|
||||
public static function getRegionIndex(int $chunkX, int $chunkZ, &$regionX, &$regionZ){
|
||||
$regionX = $chunkX >> 5;
|
||||
$regionZ = $chunkZ >> 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $regionX
|
||||
* @param int $regionZ
|
||||
*
|
||||
* @return RegionLoader|null
|
||||
*/
|
||||
protected function getRegion(int $regionX, int $regionZ){
|
||||
return $this->regions[Level::chunkHash($regionX, $regionZ)] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to a specific region file based on its X/Z coordinates
|
||||
*
|
||||
* @param int $regionX
|
||||
* @param int $regionZ
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function pathToRegion(int $regionX, int $regionZ) : string{
|
||||
return $this->path . "region/r.$regionX.$regionZ." . static::getRegionFileExtension();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $regionX
|
||||
* @param int $regionZ
|
||||
*/
|
||||
protected function loadRegion(int $regionX, int $regionZ){
|
||||
if(!isset($this->regions[$index = Level::chunkHash($regionX, $regionZ)])){
|
||||
$path = $this->pathToRegion($regionX, $regionZ);
|
||||
|
||||
$region = new RegionLoader($path, $regionX, $regionZ);
|
||||
try{
|
||||
$region->open();
|
||||
}catch(CorruptedRegionException $e){
|
||||
$logger = MainLogger::getLogger();
|
||||
$logger->error("Corrupted region file detected: " . $e->getMessage());
|
||||
|
||||
$region->close(false); //Do not write anything to the file
|
||||
|
||||
$backupPath = $path . ".bak." . time();
|
||||
rename($path, $backupPath);
|
||||
$logger->error("Corrupted region file has been backed up to " . $backupPath);
|
||||
|
||||
$region = new RegionLoader($path, $regionX, $regionZ);
|
||||
$region->open(); //this will create a new empty region to replace the corrupted one
|
||||
}
|
||||
|
||||
$this->regions[$index] = $region;
|
||||
}
|
||||
}
|
||||
|
||||
public function close(){
|
||||
foreach($this->regions as $index => $region){
|
||||
$region->close();
|
||||
unset($this->regions[$index]);
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected function serializeChunk(Chunk $chunk) : string;
|
||||
|
||||
abstract protected function deserializeChunk(string $data) : Chunk;
|
||||
|
||||
protected function readChunk(int $chunkX, int $chunkZ) : ?Chunk{
|
||||
$regionX = $regionZ = null;
|
||||
self::getRegionIndex($chunkX, $chunkZ, $regionX, $regionZ);
|
||||
assert(is_int($regionX) and is_int($regionZ));
|
||||
|
||||
$this->loadRegion($regionX, $regionZ);
|
||||
|
||||
$chunkData = $this->getRegion($regionX, $regionZ)->readChunk($chunkX & 0x1f, $chunkZ & 0x1f);
|
||||
if($chunkData !== null){
|
||||
return $this->deserializeChunk($chunkData);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function writeChunk(Chunk $chunk) : void{
|
||||
$chunkX = $chunk->getX();
|
||||
$chunkZ = $chunk->getZ();
|
||||
|
||||
self::getRegionIndex($chunkX, $chunkZ, $regionX, $regionZ);
|
||||
$this->loadRegion($regionX, $regionZ);
|
||||
|
||||
$this->getRegion($regionX, $regionZ)->writeChunk($chunkX & 0x1f, $chunkZ & 0x1f, $this->serializeChunk($chunk));
|
||||
}
|
||||
|
||||
public function getAllChunks() : \Generator{
|
||||
$iterator = new \RegexIterator(
|
||||
new \FilesystemIterator(
|
||||
$this->path . '/region/',
|
||||
\FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS
|
||||
),
|
||||
'/\/r\.(-?\d+)\.(-?\d+)\.' . static::getRegionFileExtension() . '$/',
|
||||
\RegexIterator::GET_MATCH
|
||||
);
|
||||
|
||||
foreach($iterator as $region){
|
||||
$rX = ((int) $region[1]) << 5;
|
||||
$rZ = ((int) $region[2]) << 5;
|
||||
|
||||
for($chunkX = $rX; $chunkX < $rX + 32; ++$chunkX){
|
||||
for($chunkZ = $rZ; $chunkZ < $rZ + 32; ++$chunkZ){
|
||||
$chunk = $this->loadChunk($chunkX, $chunkZ);
|
||||
if($chunk !== null){
|
||||
yield $chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user