Gistrec

RegionLoader for AsyncLoadChunk

Aug 1st, 2017
246
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 13.09 KB | None | 0 0
  1. <?php
  2.  
  3. declare(strict_types = 1);
  4.  
  5. namespace test;
  6.  
  7. use pocketmine\utils\Binary;
  8. use pocketmine\utils\MainLogger;
  9. use pocketmine\level\format\io\ChunkException;
  10. use pocketmine\level\format\io\ChunkUtils;
  11. use pocketmine\level\format\SubChunk;
  12. use pocketmine\level\format\Chunk;
  13. use pocketmine\nbt\NBT;
  14. use pocketmine\nbt\tag\{
  15.     ByteArrayTag, ByteTag, CompoundTag, IntArrayTag, IntTag, ListTag, LongTag
  16. };
  17.  
  18. class RegionLoader {
  19.  
  20.     const VERSION = 1;
  21.     const COMPRESSION_GZIP = 1;
  22.     const COMPRESSION_ZLIB = 2;
  23.     const MAX_SECTOR_LENGTH = 256 << 12; //256 sectors, (1 MiB)
  24.  
  25.     public static $COMPRESSION_LEVEL = 7;
  26.  
  27.     protected $x;
  28.     protected $z;
  29.     protected $filePath;
  30.     protected $filePointer;
  31.     protected $lastSector;
  32.     /** @var McRegion */
  33.     protected $levelProvider;
  34.     protected $locationTable = [];
  35.  
  36.     public $lastUsed;
  37.  
  38.     public function __construct($folder, int $regionX, int $regionZ, string $fileExtension = "mca"){
  39.         $this->x = $regionX;
  40.         $this->z = $regionZ;
  41.         $this->filePath = "$folder/region/r.$regionX.$regionZ.$fileExtension";
  42.         $exists = file_exists($this->filePath);
  43.         if(!$exists) touch($this->filePath);
  44.         $this->filePointer = fopen($this->filePath, "r+b");
  45.         stream_set_read_buffer($this->filePointer, 1024 * 16); //16KB
  46.         stream_set_write_buffer($this->filePointer, 1024 * 16); //16KB
  47.         if(!$exists) $this->createBlank();
  48.         else $this->loadLocationTable();
  49.  
  50.         $this->lastUsed = time();
  51.     }
  52.  
  53.     public function __destruct(){
  54.         if(is_resource($this->filePointer)){
  55.             $this->writeLocationTable();
  56.             fclose($this->filePointer);
  57.         }
  58.     }
  59.  
  60.     protected function isChunkGenerated(int $index) : bool{
  61.         return !($this->locationTable[$index][0] === 0 or $this->locationTable[$index][1] === 0);
  62.     }
  63.  
  64.     public function readChunk(int $x, int $z){
  65.         $index = self::getChunkOffset($x, $z);
  66.         if($index < 0 or $index >= 4096){
  67.             return null;
  68.         }
  69.  
  70.         $this->lastUsed = time();
  71.  
  72.         if(!$this->isChunkGenerated($index)){
  73.             return null;
  74.         }
  75.  
  76.         fseek($this->filePointer, $this->locationTable[$index][0] << 12);
  77.         $length = Binary::readInt(fread($this->filePointer, 4));
  78.         $compression = ord(fgetc($this->filePointer));
  79.  
  80.         if($length <= 0 or $length > self::MAX_SECTOR_LENGTH){ //Not yet generated / corrupted
  81.             if($length >= self::MAX_SECTOR_LENGTH){
  82.                 $this->locationTable[$index][0] = ++$this->lastSector;
  83.                 $this->locationTable[$index][1] = 1;
  84.                 var_dump("Corrupted chunk header detected");
  85.             }
  86.             return null;
  87.         }
  88.  
  89.         if($length > ($this->locationTable[$index][1] << 12)){ //Invalid chunk, bigger than defined number of sectors
  90.             MainLogger::getLogger()->error("Corrupted bigger chunk detected");
  91.             $this->locationTable[$index][1] = $length >> 12;
  92.             $this->writeLocationIndex($index);
  93.         }elseif($compression !== self::COMPRESSION_ZLIB and $compression !== self::COMPRESSION_GZIP){
  94.             MainLogger::getLogger()->error("Invalid compression type");
  95.             return null;
  96.         }
  97.  
  98.         $chunk = $this->nbtDeserialize(fread($this->filePointer, $length - 1));
  99.         if($chunk instanceof Chunk){
  100.             return $chunk;
  101.         }else{
  102.             var_dump("Corrupted chunk detected");
  103.             return null;
  104.         }
  105.     }
  106.  
  107.     public function nbtDeserialize(string $data){
  108.         $nbt = new NBT(NBT::BIG_ENDIAN);
  109.         try{
  110.             $nbt->readCompressed($data, ZLIB_ENCODING_DEFLATE);
  111.             $chunk = $nbt->getData();
  112.             if(!isset($chunk->Level) or !($chunk->Level instanceof CompoundTag)){
  113.                 throw new ChunkException("Invalid NBT format");
  114.             }
  115.             $chunk = $chunk->Level;
  116.             $subChunks = [];
  117.             if($chunk->Sections instanceof ListTag){
  118.                 foreach($chunk->Sections as $subChunk){
  119.                     if($subChunk instanceof CompoundTag){
  120.                         $subChunks[$subChunk->Y->getValue()] = new SubChunk(
  121.                             ChunkUtils::reorderByteArray($subChunk->Blocks->getValue()),
  122.                             ChunkUtils::reorderNibbleArray($subChunk->Data->getValue()),
  123.                             ChunkUtils::reorderNibbleArray($subChunk->SkyLight->getValue(), "\xff"),
  124.                             ChunkUtils::reorderNibbleArray($subChunk->BlockLight->getValue())
  125.                         );
  126.                     }
  127.                 }
  128.             }
  129.             if(isset($chunk->BiomeColors)){
  130.                 $biomeIds = ChunkUtils::convertBiomeColors($chunk->BiomeColors->getValue()); //Convert back to original format
  131.             }elseif(isset($chunk->Biomes)){
  132.                 $biomeIds = $chunk->Biomes->getValue();
  133.             }else{
  134.                 $biomeIds = "";
  135.             }
  136.             $result = new Chunk(
  137.                 $chunk["xPos"],
  138.                 $chunk["zPos"],
  139.                 $subChunks,
  140.                 isset($chunk->Entities) ? $chunk->Entities->getValue() : [],
  141.                 isset($chunk->TileEntities) ? $chunk->TileEntities->getValue() : [],
  142.                 $biomeIds,
  143.                 isset($chunk->HeightMap) ? $chunk->HeightMap->getValue() : []
  144.             );
  145.             $result->setLightPopulated(isset($chunk->LightPopulated) ? ((bool) $chunk->LightPopulated->getValue()) : false);
  146.             $result->setPopulated(isset($chunk->TerrainPopulated) ? ((bool) $chunk->TerrainPopulated->getValue()) : false);
  147.             $result->setGenerated(true);
  148.             return $result;
  149.         }catch(\Throwable $e){
  150.             MainLogger::getLogger()->logException($e);
  151.             return null;
  152.         }
  153.     }
  154.  
  155.     public function nbtSerialize(Chunk $chunk) : string{
  156.         $nbt = new CompoundTag("Level", []);
  157.         $nbt->xPos = new IntTag("xPos", $chunk->getX());
  158.         $nbt->zPos = new IntTag("zPos", $chunk->getZ());
  159.         $nbt->V = new ByteTag("V", 1);
  160.         $nbt->LastUpdate = new LongTag("LastUpdate", 0); //TODO
  161.         $nbt->InhabitedTime = new LongTag("InhabitedTime", 0); //TODO
  162.         $nbt->TerrainPopulated = new ByteTag("TerrainPopulated", $chunk->isPopulated());
  163.         $nbt->LightPopulated = new ByteTag("LightPopulated", $chunk->isLightPopulated());
  164.         $nbt->Sections = new ListTag("Sections", []);
  165.         $nbt->Sections->setTagType(NBT::TAG_Compound);
  166.         $subChunks = -1;
  167.         foreach($chunk->getSubChunks() as $y => $subChunk){
  168.             if($subChunk->isEmpty()){
  169.                 continue;
  170.             }
  171.             $nbt->Sections[++$subChunks] = new CompoundTag(null, [
  172.                 "Y"          => new ByteTag("Y", $y),
  173.                 "Blocks"     => new ByteArrayTag("Blocks", ChunkUtils::reorderByteArray($subChunk->getBlockIdArray())), //Generic in-memory chunks are currently always XZY
  174.                 "Data"       => new ByteArrayTag("Data", ChunkUtils::reorderNibbleArray($subChunk->getBlockDataArray())),
  175.                 "SkyLight"   => new ByteArrayTag("SkyLight", ChunkUtils::reorderNibbleArray($subChunk->getSkyLightArray(), "\xff")),
  176.                 "BlockLight" => new ByteArrayTag("BlockLight", ChunkUtils::reorderNibbleArray($subChunk->getBlockLightArray()))
  177.             ]);
  178.         }
  179.         $nbt->Biomes = new ByteArrayTag("Biomes", $chunk->getBiomeIdArray());
  180.         $nbt->HeightMap = new IntArrayTag("HeightMap", $chunk->getHeightMapArray());
  181.         $entities = [];
  182.         foreach($chunk->getEntities() as $entity){
  183.             if(!($entity instanceof Player) and !$entity->closed){
  184.                 $entity->saveNBT();
  185.                 $entities[] = $entity->namedtag;
  186.             }
  187.         }
  188.         $nbt->Entities = new ListTag("Entities", $entities);
  189.         $nbt->Entities->setTagType(NBT::TAG_Compound);
  190.         $tiles = [];
  191.         foreach($chunk->getTiles() as $tile){
  192.             $tile->saveNBT();
  193.             $tiles[] = $tile->namedtag;
  194.         }
  195.         $nbt->TileEntities = new ListTag("TileEntities", $tiles);
  196.         $nbt->TileEntities->setTagType(NBT::TAG_Compound);
  197.         //TODO: TileTicks
  198.         $writer = new NBT(NBT::BIG_ENDIAN);
  199.         $nbt->setName("Level");
  200.         $writer->setData(new CompoundTag("", ["Level" => $nbt]));
  201.         return $writer->writeCompressed(ZLIB_ENCODING_DEFLATE, RegionLoader::$COMPRESSION_LEVEL);
  202.     }
  203.  
  204.     public function chunkExists(int $x, int $z) : bool{
  205.         return $this->isChunkGenerated(self::getChunkOffset($x, $z));
  206.     }
  207.  
  208.     protected function saveChunk(int $x, int $z, string $chunkData){
  209.         $length = strlen($chunkData) + 1;
  210.         if($length + 4 > self::MAX_SECTOR_LENGTH){
  211.             throw new ChunkException("Chunk is too big! ".($length + 4)." > ".self::MAX_SECTOR_LENGTH);
  212.         }
  213.         $sectors = (int) ceil(($length + 4) / 4096);
  214.         $index = self::getChunkOffset($x, $z);
  215.         $indexChanged = false;
  216.         if($this->locationTable[$index][1] < $sectors){
  217.             $this->locationTable[$index][0] = $this->lastSector + 1;
  218.             $this->lastSector += $sectors; //The GC will clean this shift "later"
  219.             $indexChanged = true;
  220.         }elseif($this->locationTable[$index][1] != $sectors){
  221.             $indexChanged = true;
  222.         }
  223.  
  224.         $this->locationTable[$index][1] = $sectors;
  225.         $this->locationTable[$index][2] = time();
  226.  
  227.         fseek($this->filePointer, $this->locationTable[$index][0] << 12);
  228.         fwrite($this->filePointer, str_pad(Binary::writeInt($length) . chr(self::COMPRESSION_ZLIB) . $chunkData, $sectors << 12, "\x00", STR_PAD_RIGHT));
  229.  
  230.         if($indexChanged){
  231.             $this->writeLocationIndex($index);
  232.         }
  233.     }
  234.  
  235.     public function removeChunk(int $x, int $z){
  236.         $index = self::getChunkOffset($x, $z);
  237.         $this->locationTable[$index][0] = 0;
  238.         $this->locationTable[$index][1] = 0;
  239.     }
  240.  
  241.     public function writeChunk(Chunk $chunk){
  242.         $this->lastUsed = time();
  243.         $chunkData = $this->nbtSerialize($chunk);
  244.         if($chunkData !== false){
  245.             $this->saveChunk($chunk->getX() - ($this->getX() * 32), $chunk->getZ() - ($this->getZ() * 32), $chunkData);
  246.         }
  247.     }
  248.  
  249.     protected static function getChunkOffset(int $x, int $z) : int{
  250.         return $x + ($z << 5);
  251.     }
  252.  
  253.     public function close(){
  254.         $this->writeLocationTable();
  255.         fclose($this->filePointer);
  256.         $this->levelProvider = null;
  257.     }
  258.  
  259.     public function doSlowCleanUp() : int{
  260.         for($i = 0; $i < 1024; ++$i){
  261.             if($this->locationTable[$i][0] === 0 or $this->locationTable[$i][1] === 0){
  262.                 continue;
  263.             }
  264.             fseek($this->filePointer, $this->locationTable[$i][0] << 12);
  265.             $chunk = fread($this->filePointer, $this->locationTable[$i][1] << 12);
  266.             $length = Binary::readInt(substr($chunk, 0, 4));
  267.             if($length <= 1){
  268.                 $this->locationTable[$i] = [0, 0, 0]; //Non-generated chunk, remove it from index
  269.             }
  270.  
  271.             try{
  272.                 $chunk = zlib_decode(substr($chunk, 5));
  273.             }catch(\Throwable $e){
  274.                 $this->locationTable[$i] = [0, 0, 0]; //Corrupted chunk, remove it
  275.                 continue;
  276.             }
  277.  
  278.             $chunk = chr(self::COMPRESSION_ZLIB) . zlib_encode($chunk, ZLIB_ENCODING_DEFLATE, 9);
  279.             $chunk = Binary::writeInt(strlen($chunk)) . $chunk;
  280.             $sectors = (int) ceil(strlen($chunk) / 4096);
  281.             if($sectors > $this->locationTable[$i][1]){
  282.                 $this->locationTable[$i][0] = $this->lastSector + 1;
  283.                 $this->lastSector += $sectors;
  284.             }
  285.             fseek($this->filePointer, $this->locationTable[$i][0] << 12);
  286.             fwrite($this->filePointer, str_pad($chunk, $sectors << 12, "\x00", STR_PAD_RIGHT));
  287.         }
  288.         $this->writeLocationTable();
  289.         $n = $this->cleanGarbage();
  290.         $this->writeLocationTable();
  291.  
  292.         return $n;
  293.     }
  294.  
  295.     private function cleanGarbage() : int{
  296.         $sectors = [];
  297.         foreach($this->locationTable as $index => $data){ //Calculate file usage
  298.             if($data[0] === 0 or $data[1] === 0){
  299.                 $this->locationTable[$index] = [0, 0, 0];
  300.                 continue;
  301.             }
  302.             for($i = 0; $i < $data[1]; ++$i){
  303.                 $sectors[$data[0]] = $index;
  304.             }
  305.         }
  306.  
  307.         if(count($sectors) === ($this->lastSector - 2)){ //No collection needed
  308.             return 0;
  309.         }
  310.  
  311.         ksort($sectors);
  312.         $shift = 0;
  313.         $lastSector = 1; //First chunk - 1
  314.  
  315.         fseek($this->filePointer, 8192);
  316.         $sector = 2;
  317.         foreach($sectors as $sector => $index){
  318.             if(($sector - $lastSector) > 1){
  319.                 $shift += $sector - $lastSector - 1;
  320.             }
  321.             if($shift > 0){
  322.                 fseek($this->filePointer, $sector << 12);
  323.                 $old = fread($this->filePointer, 4096);
  324.                 fseek($this->filePointer, ($sector - $shift) << 12);
  325.                 fwrite($this->filePointer, $old, 4096);
  326.             }
  327.             $this->locationTable[$index][0] -= $shift;
  328.             $lastSector = $sector;
  329.         }
  330.         ftruncate($this->filePointer, ($sector + 1) << 12); //Truncate to the end of file written
  331.         return $shift;
  332.     }
  333.  
  334.     protected function loadLocationTable(){
  335.         fseek($this->filePointer, 0);
  336.         $this->lastSector = 1;
  337.  
  338.         $data = unpack("N*", fread($this->filePointer, 4 * 1024 * 2)); //1024 records * 4 bytes * 2 times
  339.         for($i = 0; $i < 1024; ++$i){
  340.             $index = $data[$i + 1];
  341.             $this->locationTable[$i] = [$index >> 8, $index & 0xff, $data[1024 + $i + 1]];
  342.             if(($this->locationTable[$i][0] + $this->locationTable[$i][1] - 1) > $this->lastSector){
  343.                 $this->lastSector = $this->locationTable[$i][0] + $this->locationTable[$i][1] - 1;
  344.             }
  345.         }
  346.     }
  347.  
  348.     private function writeLocationTable(){
  349.         $write = [];
  350.  
  351.         for($i = 0; $i < 1024; ++$i){
  352.             $write[] = (($this->locationTable[$i][0] << 8) | $this->locationTable[$i][1]);
  353.         }
  354.         for($i = 0; $i < 1024; ++$i){
  355.             $write[] = $this->locationTable[$i][2];
  356.         }
  357.         fseek($this->filePointer, 0);
  358.         fwrite($this->filePointer, pack("N*", ...$write), 4096 * 2);
  359.     }
  360.  
  361.     protected function writeLocationIndex($index){
  362.         fseek($this->filePointer, $index << 2);
  363.         fwrite($this->filePointer, Binary::writeInt(($this->locationTable[$index][0] << 8) | $this->locationTable[$index][1]), 4);
  364.         fseek($this->filePointer, 4096 + ($index << 2));
  365.         fwrite($this->filePointer, Binary::writeInt($this->locationTable[$index][2]), 4);
  366.     }
  367.  
  368.     protected function createBlank(){
  369.         fseek($this->filePointer, 0);
  370.         ftruncate($this->filePointer, 0);
  371.         $this->lastSector = 1;
  372.         $table = "";
  373.         for($i = 0; $i < 1024; ++$i){
  374.             $this->locationTable[$i] = [0, 0];
  375.             $table .= Binary::writeInt(0);
  376.         }
  377.  
  378.         $time = time();
  379.         for($i = 0; $i < 1024; ++$i){
  380.             $this->locationTable[$i][2] = $time;
  381.             $table .= Binary::writeInt($time);
  382.         }
  383.  
  384.         fwrite($this->filePointer, $table, 4096 * 2);
  385.     }
  386.  
  387.     public function getX() : int{
  388.         return $this->x;
  389.     }
  390.  
  391.     public function getZ() : int{
  392.         return $this->z;
  393.     }
  394.  
  395. }
Advertisement
Comments
  • User was banned
Add Comment
Please, Sign In to add comment