Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <?php
- declare(strict_types = 1);
- namespace test;
- use pocketmine\utils\Binary;
- use pocketmine\utils\MainLogger;
- use pocketmine\level\format\io\ChunkException;
- use pocketmine\level\format\io\ChunkUtils;
- use pocketmine\level\format\SubChunk;
- use pocketmine\level\format\Chunk;
- use pocketmine\nbt\NBT;
- use pocketmine\nbt\tag\{
- ByteArrayTag, ByteTag, CompoundTag, IntArrayTag, IntTag, ListTag, LongTag
- };
- class RegionLoader {
- const VERSION = 1;
- const COMPRESSION_GZIP = 1;
- const COMPRESSION_ZLIB = 2;
- const MAX_SECTOR_LENGTH = 256 << 12; //256 sectors, (1 MiB)
- public static $COMPRESSION_LEVEL = 7;
- protected $x;
- protected $z;
- protected $filePath;
- protected $filePointer;
- protected $lastSector;
- /** @var McRegion */
- protected $levelProvider;
- protected $locationTable = [];
- public $lastUsed;
- public function __construct($folder, int $regionX, int $regionZ, string $fileExtension = "mca"){
- $this->x = $regionX;
- $this->z = $regionZ;
- $this->filePath = "$folder/region/r.$regionX.$regionZ.$fileExtension";
- $exists = file_exists($this->filePath);
- if(!$exists) touch($this->filePath);
- $this->filePointer = fopen($this->filePath, "r+b");
- stream_set_read_buffer($this->filePointer, 1024 * 16); //16KB
- stream_set_write_buffer($this->filePointer, 1024 * 16); //16KB
- if(!$exists) $this->createBlank();
- else $this->loadLocationTable();
- $this->lastUsed = time();
- }
- public function __destruct(){
- if(is_resource($this->filePointer)){
- $this->writeLocationTable();
- fclose($this->filePointer);
- }
- }
- protected function isChunkGenerated(int $index) : bool{
- return !($this->locationTable[$index][0] === 0 or $this->locationTable[$index][1] === 0);
- }
- public function readChunk(int $x, int $z){
- $index = self::getChunkOffset($x, $z);
- if($index < 0 or $index >= 4096){
- return null;
- }
- $this->lastUsed = time();
- if(!$this->isChunkGenerated($index)){
- return null;
- }
- fseek($this->filePointer, $this->locationTable[$index][0] << 12);
- $length = Binary::readInt(fread($this->filePointer, 4));
- $compression = ord(fgetc($this->filePointer));
- if($length <= 0 or $length > self::MAX_SECTOR_LENGTH){ //Not yet generated / corrupted
- if($length >= self::MAX_SECTOR_LENGTH){
- $this->locationTable[$index][0] = ++$this->lastSector;
- $this->locationTable[$index][1] = 1;
- var_dump("Corrupted chunk header detected");
- }
- return null;
- }
- if($length > ($this->locationTable[$index][1] << 12)){ //Invalid chunk, bigger than defined number of sectors
- MainLogger::getLogger()->error("Corrupted bigger chunk detected");
- $this->locationTable[$index][1] = $length >> 12;
- $this->writeLocationIndex($index);
- }elseif($compression !== self::COMPRESSION_ZLIB and $compression !== self::COMPRESSION_GZIP){
- MainLogger::getLogger()->error("Invalid compression type");
- return null;
- }
- $chunk = $this->nbtDeserialize(fread($this->filePointer, $length - 1));
- if($chunk instanceof Chunk){
- return $chunk;
- }else{
- var_dump("Corrupted chunk detected");
- return null;
- }
- }
- public function nbtDeserialize(string $data){
- $nbt = new NBT(NBT::BIG_ENDIAN);
- try{
- $nbt->readCompressed($data, ZLIB_ENCODING_DEFLATE);
- $chunk = $nbt->getData();
- if(!isset($chunk->Level) or !($chunk->Level instanceof CompoundTag)){
- throw new ChunkException("Invalid NBT format");
- }
- $chunk = $chunk->Level;
- $subChunks = [];
- if($chunk->Sections instanceof ListTag){
- foreach($chunk->Sections as $subChunk){
- if($subChunk instanceof CompoundTag){
- $subChunks[$subChunk->Y->getValue()] = new SubChunk(
- ChunkUtils::reorderByteArray($subChunk->Blocks->getValue()),
- ChunkUtils::reorderNibbleArray($subChunk->Data->getValue()),
- ChunkUtils::reorderNibbleArray($subChunk->SkyLight->getValue(), "\xff"),
- ChunkUtils::reorderNibbleArray($subChunk->BlockLight->getValue())
- );
- }
- }
- }
- if(isset($chunk->BiomeColors)){
- $biomeIds = ChunkUtils::convertBiomeColors($chunk->BiomeColors->getValue()); //Convert back to original format
- }elseif(isset($chunk->Biomes)){
- $biomeIds = $chunk->Biomes->getValue();
- }else{
- $biomeIds = "";
- }
- $result = new Chunk(
- $chunk["xPos"],
- $chunk["zPos"],
- $subChunks,
- isset($chunk->Entities) ? $chunk->Entities->getValue() : [],
- isset($chunk->TileEntities) ? $chunk->TileEntities->getValue() : [],
- $biomeIds,
- isset($chunk->HeightMap) ? $chunk->HeightMap->getValue() : []
- );
- $result->setLightPopulated(isset($chunk->LightPopulated) ? ((bool) $chunk->LightPopulated->getValue()) : false);
- $result->setPopulated(isset($chunk->TerrainPopulated) ? ((bool) $chunk->TerrainPopulated->getValue()) : false);
- $result->setGenerated(true);
- return $result;
- }catch(\Throwable $e){
- MainLogger::getLogger()->logException($e);
- return null;
- }
- }
- public function nbtSerialize(Chunk $chunk) : string{
- $nbt = new CompoundTag("Level", []);
- $nbt->xPos = new IntTag("xPos", $chunk->getX());
- $nbt->zPos = new IntTag("zPos", $chunk->getZ());
- $nbt->V = new ByteTag("V", 1);
- $nbt->LastUpdate = new LongTag("LastUpdate", 0); //TODO
- $nbt->InhabitedTime = new LongTag("InhabitedTime", 0); //TODO
- $nbt->TerrainPopulated = new ByteTag("TerrainPopulated", $chunk->isPopulated());
- $nbt->LightPopulated = new ByteTag("LightPopulated", $chunk->isLightPopulated());
- $nbt->Sections = new ListTag("Sections", []);
- $nbt->Sections->setTagType(NBT::TAG_Compound);
- $subChunks = -1;
- foreach($chunk->getSubChunks() as $y => $subChunk){
- if($subChunk->isEmpty()){
- continue;
- }
- $nbt->Sections[++$subChunks] = new CompoundTag(null, [
- "Y" => new ByteTag("Y", $y),
- "Blocks" => new ByteArrayTag("Blocks", ChunkUtils::reorderByteArray($subChunk->getBlockIdArray())), //Generic in-memory chunks are currently always XZY
- "Data" => new ByteArrayTag("Data", ChunkUtils::reorderNibbleArray($subChunk->getBlockDataArray())),
- "SkyLight" => new ByteArrayTag("SkyLight", ChunkUtils::reorderNibbleArray($subChunk->getSkyLightArray(), "\xff")),
- "BlockLight" => new ByteArrayTag("BlockLight", ChunkUtils::reorderNibbleArray($subChunk->getBlockLightArray()))
- ]);
- }
- $nbt->Biomes = new ByteArrayTag("Biomes", $chunk->getBiomeIdArray());
- $nbt->HeightMap = new IntArrayTag("HeightMap", $chunk->getHeightMapArray());
- $entities = [];
- foreach($chunk->getEntities() as $entity){
- if(!($entity instanceof Player) and !$entity->closed){
- $entity->saveNBT();
- $entities[] = $entity->namedtag;
- }
- }
- $nbt->Entities = new ListTag("Entities", $entities);
- $nbt->Entities->setTagType(NBT::TAG_Compound);
- $tiles = [];
- foreach($chunk->getTiles() as $tile){
- $tile->saveNBT();
- $tiles[] = $tile->namedtag;
- }
- $nbt->TileEntities = new ListTag("TileEntities", $tiles);
- $nbt->TileEntities->setTagType(NBT::TAG_Compound);
- //TODO: TileTicks
- $writer = new NBT(NBT::BIG_ENDIAN);
- $nbt->setName("Level");
- $writer->setData(new CompoundTag("", ["Level" => $nbt]));
- return $writer->writeCompressed(ZLIB_ENCODING_DEFLATE, RegionLoader::$COMPRESSION_LEVEL);
- }
- public function chunkExists(int $x, int $z) : bool{
- return $this->isChunkGenerated(self::getChunkOffset($x, $z));
- }
- protected function saveChunk(int $x, int $z, string $chunkData){
- $length = strlen($chunkData) + 1;
- if($length + 4 > self::MAX_SECTOR_LENGTH){
- throw new ChunkException("Chunk is too big! ".($length + 4)." > ".self::MAX_SECTOR_LENGTH);
- }
- $sectors = (int) ceil(($length + 4) / 4096);
- $index = self::getChunkOffset($x, $z);
- $indexChanged = false;
- if($this->locationTable[$index][1] < $sectors){
- $this->locationTable[$index][0] = $this->lastSector + 1;
- $this->lastSector += $sectors; //The GC will clean this shift "later"
- $indexChanged = true;
- }elseif($this->locationTable[$index][1] != $sectors){
- $indexChanged = true;
- }
- $this->locationTable[$index][1] = $sectors;
- $this->locationTable[$index][2] = time();
- fseek($this->filePointer, $this->locationTable[$index][0] << 12);
- fwrite($this->filePointer, str_pad(Binary::writeInt($length) . chr(self::COMPRESSION_ZLIB) . $chunkData, $sectors << 12, "\x00", STR_PAD_RIGHT));
- if($indexChanged){
- $this->writeLocationIndex($index);
- }
- }
- public function removeChunk(int $x, int $z){
- $index = self::getChunkOffset($x, $z);
- $this->locationTable[$index][0] = 0;
- $this->locationTable[$index][1] = 0;
- }
- public function writeChunk(Chunk $chunk){
- $this->lastUsed = time();
- $chunkData = $this->nbtSerialize($chunk);
- if($chunkData !== false){
- $this->saveChunk($chunk->getX() - ($this->getX() * 32), $chunk->getZ() - ($this->getZ() * 32), $chunkData);
- }
- }
- protected static function getChunkOffset(int $x, int $z) : int{
- return $x + ($z << 5);
- }
- public function close(){
- $this->writeLocationTable();
- fclose($this->filePointer);
- $this->levelProvider = null;
- }
- public function doSlowCleanUp() : int{
- for($i = 0; $i < 1024; ++$i){
- if($this->locationTable[$i][0] === 0 or $this->locationTable[$i][1] === 0){
- continue;
- }
- fseek($this->filePointer, $this->locationTable[$i][0] << 12);
- $chunk = fread($this->filePointer, $this->locationTable[$i][1] << 12);
- $length = Binary::readInt(substr($chunk, 0, 4));
- if($length <= 1){
- $this->locationTable[$i] = [0, 0, 0]; //Non-generated chunk, remove it from index
- }
- try{
- $chunk = zlib_decode(substr($chunk, 5));
- }catch(\Throwable $e){
- $this->locationTable[$i] = [0, 0, 0]; //Corrupted chunk, remove it
- continue;
- }
- $chunk = chr(self::COMPRESSION_ZLIB) . zlib_encode($chunk, ZLIB_ENCODING_DEFLATE, 9);
- $chunk = Binary::writeInt(strlen($chunk)) . $chunk;
- $sectors = (int) ceil(strlen($chunk) / 4096);
- if($sectors > $this->locationTable[$i][1]){
- $this->locationTable[$i][0] = $this->lastSector + 1;
- $this->lastSector += $sectors;
- }
- fseek($this->filePointer, $this->locationTable[$i][0] << 12);
- fwrite($this->filePointer, str_pad($chunk, $sectors << 12, "\x00", STR_PAD_RIGHT));
- }
- $this->writeLocationTable();
- $n = $this->cleanGarbage();
- $this->writeLocationTable();
- return $n;
- }
- private function cleanGarbage() : int{
- $sectors = [];
- foreach($this->locationTable as $index => $data){ //Calculate file usage
- if($data[0] === 0 or $data[1] === 0){
- $this->locationTable[$index] = [0, 0, 0];
- continue;
- }
- for($i = 0; $i < $data[1]; ++$i){
- $sectors[$data[0]] = $index;
- }
- }
- if(count($sectors) === ($this->lastSector - 2)){ //No collection needed
- return 0;
- }
- ksort($sectors);
- $shift = 0;
- $lastSector = 1; //First chunk - 1
- fseek($this->filePointer, 8192);
- $sector = 2;
- foreach($sectors as $sector => $index){
- if(($sector - $lastSector) > 1){
- $shift += $sector - $lastSector - 1;
- }
- if($shift > 0){
- fseek($this->filePointer, $sector << 12);
- $old = fread($this->filePointer, 4096);
- fseek($this->filePointer, ($sector - $shift) << 12);
- fwrite($this->filePointer, $old, 4096);
- }
- $this->locationTable[$index][0] -= $shift;
- $lastSector = $sector;
- }
- ftruncate($this->filePointer, ($sector + 1) << 12); //Truncate to the end of file written
- return $shift;
- }
- protected function loadLocationTable(){
- fseek($this->filePointer, 0);
- $this->lastSector = 1;
- $data = unpack("N*", fread($this->filePointer, 4 * 1024 * 2)); //1024 records * 4 bytes * 2 times
- for($i = 0; $i < 1024; ++$i){
- $index = $data[$i + 1];
- $this->locationTable[$i] = [$index >> 8, $index & 0xff, $data[1024 + $i + 1]];
- if(($this->locationTable[$i][0] + $this->locationTable[$i][1] - 1) > $this->lastSector){
- $this->lastSector = $this->locationTable[$i][0] + $this->locationTable[$i][1] - 1;
- }
- }
- }
- private function writeLocationTable(){
- $write = [];
- for($i = 0; $i < 1024; ++$i){
- $write[] = (($this->locationTable[$i][0] << 8) | $this->locationTable[$i][1]);
- }
- for($i = 0; $i < 1024; ++$i){
- $write[] = $this->locationTable[$i][2];
- }
- fseek($this->filePointer, 0);
- fwrite($this->filePointer, pack("N*", ...$write), 4096 * 2);
- }
- protected function writeLocationIndex($index){
- fseek($this->filePointer, $index << 2);
- fwrite($this->filePointer, Binary::writeInt(($this->locationTable[$index][0] << 8) | $this->locationTable[$index][1]), 4);
- fseek($this->filePointer, 4096 + ($index << 2));
- fwrite($this->filePointer, Binary::writeInt($this->locationTable[$index][2]), 4);
- }
- protected function createBlank(){
- fseek($this->filePointer, 0);
- ftruncate($this->filePointer, 0);
- $this->lastSector = 1;
- $table = "";
- for($i = 0; $i < 1024; ++$i){
- $this->locationTable[$i] = [0, 0];
- $table .= Binary::writeInt(0);
- }
- $time = time();
- for($i = 0; $i < 1024; ++$i){
- $this->locationTable[$i][2] = $time;
- $table .= Binary::writeInt($time);
- }
- fwrite($this->filePointer, $table, 4096 * 2);
- }
- public function getX() : int{
- return $this->x;
- }
- public function getZ() : int{
- return $this->z;
- }
- }
Advertisement