diff --git a/src/pocketmine/level/format/io/region/Anvil.php b/src/pocketmine/level/format/io/region/Anvil.php index 5864d30f7..a00455c05 100644 --- a/src/pocketmine/level/format/io/region/Anvil.php +++ b/src/pocketmine/level/format/io/region/Anvil.php @@ -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{ diff --git a/src/pocketmine/level/format/io/region/LegacyAnvilChunkTrait.php b/src/pocketmine/level/format/io/region/LegacyAnvilChunkTrait.php new file mode 100644 index 000000000..b467df506 --- /dev/null +++ b/src/pocketmine/level/format/io/region/LegacyAnvilChunkTrait.php @@ -0,0 +1,138 @@ +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; + +} diff --git a/src/pocketmine/level/format/io/region/McRegion.php b/src/pocketmine/level/format/io/region/McRegion.php index 4cb8df9ca..890de55c1 100644 --- a/src/pocketmine/level/format/io/region/McRegion.php +++ b/src/pocketmine/level/format/io/region/McRegion.php @@ -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; - } - } - } - } - } } diff --git a/src/pocketmine/level/format/io/region/PMAnvil.php b/src/pocketmine/level/format/io/region/PMAnvil.php index 5ef0d665c..472fa9397 100644 --- a/src/pocketmine/level/format/io/region/PMAnvil.php +++ b/src/pocketmine/level/format/io/region/PMAnvil.php @@ -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; + } } diff --git a/src/pocketmine/level/format/io/region/RegionLevelProvider.php b/src/pocketmine/level/format/io/region/RegionLevelProvider.php new file mode 100644 index 000000000..e5c5a82b8 --- /dev/null +++ b/src/pocketmine/level/format/io/region/RegionLevelProvider.php @@ -0,0 +1,303 @@ +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; + } + } + } + } + } +}