59    public const COMPRESSION_GZIP = 1;
 
   60    public const COMPRESSION_ZLIB = 2;
 
   62    private const MAX_SECTOR_LENGTH = 255 << 12; 
 
   63    private const REGION_HEADER_LENGTH = 8192; 
 
   65    public const FIRST_SECTOR = 2; 
 
   68    protected $filePointer;
 
   69    protected int $nextSector = self::FIRST_SECTOR;
 
   74    protected array $locationTable = [];
 
   81    private function __construct(
 
   82        protected string $filePath
 
   85        $this->lastUsed = time();
 
   87        $filePointer = fopen($this->filePath, 
"r+b");
 
   89        $this->filePointer = $filePointer;
 
   90        stream_set_read_buffer($this->filePointer, 1024 * 16); 
 
   91        stream_set_write_buffer($this->filePointer, 1024 * 16); 
 
   98        clearstatcache(false, $filePath);
 
   99        if(!file_exists($filePath)){
 
  100            throw new \RuntimeException(
"File $filePath does not exist");
 
  102        if(filesize($filePath) % 4096 !== 0){
 
  106        $result = 
new self($filePath);
 
  107        $result->loadLocationTable();
 
 
  111    public static function createNew(
string $filePath) : self{
 
  112        clearstatcache(false, $filePath);
 
  113        if(file_exists($filePath)){
 
  114            throw new \RuntimeException(
"Region file $filePath already exists");
 
  118        $result = 
new self($filePath);
 
  119        $result->createBlank();
 
  123    public function __destruct(){
 
  124        if(is_resource($this->filePointer)){
 
  125            fclose($this->filePointer);
 
  129    protected function isChunkGenerated(
int $index) : bool{
 
  130        return $this->locationTable[$index] !== null;
 
  138        $index = self::getChunkOffset($x, $z);
 
  140        $this->lastUsed = time();
 
  142        if($this->locationTable[$index] === 
null){
 
  146        fseek($this->filePointer, $this->locationTable[$index]->getFirstSector() << 12);
 
  153        $bytesToRead = $this->locationTable[$index]->getSectorCount() << 12;
 
  154        $payload = fread($this->filePointer, $bytesToRead);
 
  156        if($payload === 
false || strlen($payload) !== $bytesToRead){
 
  157            throw new CorruptedChunkException(
"Corrupted chunk detected (unexpected EOF, truncated or non-padded chunk found)");
 
  159        $stream = 
new BinaryStream($payload);
 
  162            $length = $stream->getInt();
 
  167            $compression = $stream->getByte();
 
  168            if($compression !== self::COMPRESSION_ZLIB && $compression !== self::COMPRESSION_GZIP){
 
  169                throw new CorruptedChunkException(
"Invalid compression type (got $compression, expected " . self::COMPRESSION_ZLIB . 
" or " . self::COMPRESSION_GZIP . 
")");
 
  172            return $stream->get($length - 1); 
 
  173        }
catch(BinaryDataException $e){
 
  174            throw new CorruptedChunkException(
"Corrupted chunk detected: " . $e->getMessage(), 0, $e);
 
 
  182        return $this->isChunkGenerated(self::getChunkOffset($x, $z));
 
 
  187        $this->garbageTable->add($oldLocation);
 
  189        $endGarbage = $this->garbageTable->end();
 
  190        $nextSector = $this->nextSector;
 
  191        for(; $endGarbage !== 
null && $endGarbage->getLastSector() + 1 === $nextSector; $endGarbage = $this->garbageTable->end()){
 
  192            $nextSector = $endGarbage->getFirstSector();
 
  193            $this->garbageTable->remove($endGarbage);
 
  196        if($nextSector !== $this->nextSector){
 
  197            $this->nextSector = $nextSector;
 
  198            ftruncate($this->filePointer, $this->nextSector << 12);
 
  206    public function writeChunk(
int $x, 
int $z, 
string $chunkData) : void{
 
  207        $this->lastUsed = time();
 
  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);
 
  214        $newSize = (int) ceil(($length + 4) / 4096);
 
  215        $index = self::getChunkOffset($x, $z);
 
  223        $newLocation = $this->garbageTable->allocate($newSize);
 
  226        if($newLocation === 
null){
 
  228            $this->bumpNextFreeSector($newLocation);
 
  232        fseek($this->filePointer, $newLocation->getFirstSector() << 12);
 
  233        fwrite($this->filePointer, str_pad(Binary::writeInt($length) . chr(self::COMPRESSION_ZLIB) . $chunkData, $newSize << 12, 
"\x00", STR_PAD_RIGHT));
 
  240        $oldLocation = $this->locationTable[$index];
 
  241        $this->locationTable[$index] = $newLocation;
 
  242        $this->writeLocationIndex($index);
 
  244        if($oldLocation !== 
null){
 
  245            $this->disposeGarbageArea($oldLocation);
 
 
  253        $index = self::getChunkOffset($x, $z);
 
  254        $oldLocation = $this->locationTable[$index];
 
  255        $this->locationTable[$index] = 
null;
 
  256        $this->writeLocationIndex($index);
 
  257        if($oldLocation !== 
null){
 
  258            $this->disposeGarbageArea($oldLocation);
 
 
  266        if($x < 0 || $x > 31 || $z < 0 || $z > 31){
 
  267            throw new \InvalidArgumentException(
"Invalid chunk position in region, expected x/z in range 0-31, got x=$x, z=$z");
 
  269        return $x | ($z << 5);
 
 
  278    protected static function getChunkCoords(
int $offset, ?
int &$x, ?
int &$z) : void{
 
  280        $z = ($offset >> 5) & 0x1f;
 
 
  287        if(is_resource($this->filePointer)){
 
  288            fclose($this->filePointer);
 
 
  296        fseek($this->filePointer, 0);
 
  298        $headerRaw = fread($this->filePointer, self::REGION_HEADER_LENGTH);
 
  299        if($headerRaw === 
false || strlen($headerRaw) !== self::REGION_HEADER_LENGTH){
 
  304        $data = unpack(
"N*", $headerRaw);
 
  306        for($i = 0; $i < 1024; ++$i){
 
  307            $index = $data[$i + 1];
 
  308            $offset = $index >> 8;
 
  309            $sectorCount = $index & 0xff;
 
  310            $timestamp = $data[$i + 1025];
 
  312            if($offset === 0 || $sectorCount === 0){
 
  313                $this->locationTable[$i] = 
null;
 
  314            }elseif($offset >= self::FIRST_SECTOR){
 
  315                $this->bumpNextFreeSector($this->locationTable[$i] = 
new RegionLocationTableEntry($offset, $sectorCount, $timestamp));
 
  317                self::getChunkCoords($i, $chunkXX, $chunkZZ);
 
  318                throw new CorruptedRegionException(
"Invalid region header entry for x=$chunkXX z=$chunkZZ, offset overlaps with header");
 
  322        $this->checkLocationTableValidity();
 
  324        $this->garbageTable = RegionGarbageMap::buildFromLocationTable($this->locationTable);
 
  326        fseek($this->filePointer, 0);
 
 
  332    private function checkLocationTableValidity() : void{
 
  335        $fileSize = filesize($this->filePath);
 
  336        if($fileSize === 
false) 
throw new AssumptionFailedError(
"filesize() should not return false here");
 
  337        for($i = 0; $i < 1024; ++$i){
 
  338            $entry = $this->locationTable[$i];
 
  343            self::getChunkCoords($i, $x, $z);
 
  344            $offset = $entry->getFirstSector();
 
  345            $fileOffset = $offset << 12;
 
  349            if($fileOffset >= $fileSize){
 
  350                throw new CorruptedRegionException(
"Region file location offset x=$x,z=$z points to invalid file location $fileOffset");
 
  352            if(isset($usedOffsets[$offset])){
 
  353                self::getChunkCoords($usedOffsets[$offset], $existingX, $existingZ);
 
  354                throw new CorruptedRegionException(
"Found two chunk offsets (chunk1: x=$existingX,z=$existingZ, chunk2: x=$x,z=$z) pointing to the file location $fileOffset");
 
  356            $usedOffsets[$offset] = $i;
 
  358        ksort($usedOffsets, SORT_NUMERIC);
 
  359        $prevLocationIndex = 
null;
 
  360        foreach($usedOffsets as $locationTableIndex){
 
  361            if($this->locationTable[$locationTableIndex] === 
null){
 
  364            if($prevLocationIndex !== 
null){
 
  365                assert($this->locationTable[$prevLocationIndex] !== 
null);
 
  366                if($this->locationTable[$locationTableIndex]->overlaps($this->locationTable[$prevLocationIndex])){
 
  367                    self::getChunkCoords($locationTableIndex, $chunkXX, $chunkZZ);
 
  368                    self::getChunkCoords($prevLocationIndex, $prevChunkXX, $prevChunkZZ);
 
  369                    throw new CorruptedRegionException(
"Overlapping chunks detected in region header (chunk1: x=$chunkXX,z=$chunkZZ, chunk2: x=$prevChunkXX,z=$prevChunkZZ)");
 
  372            $prevLocationIndex = $locationTableIndex;
 
  376    protected function writeLocationIndex(
int $index) : void{
 
  377        $entry = $this->locationTable[$index];
 
  378        fseek($this->filePointer, $index << 2);
 
  379        fwrite($this->filePointer, Binary::writeInt($entry !== 
null ? ($entry->getFirstSector() << 8) | $entry->getSectorCount() : 0), 4);
 
  380        fseek($this->filePointer, 4096 + ($index << 2));
 
  381        fwrite($this->filePointer, Binary::writeInt($entry !== 
null ? $entry->getTimestamp() : 0), 4);
 
  382        clearstatcache(
false, $this->filePath);
 
  385    protected function createBlank() : void{
 
  386        fseek($this->filePointer, 0);
 
  387        ftruncate($this->filePointer, 8192); 
 
  388        for($i = 0; $i < 1024; ++$i){
 
  389            $this->locationTable[$i] = 
null;
 
  393    private function bumpNextFreeSector(RegionLocationTableEntry $entry) : void{
 
  394        $this->nextSector = max($this->nextSector, $entry->getLastSector() + 1);
 
  397    public function generateSectorMap(
string $usedChar, 
string $freeChar) : string{
 
  398        $result = str_repeat($freeChar, $this->nextSector);
 
  399        for($i = 0; $i < self::FIRST_SECTOR; ++$i){
 
  400            $result[$i] = $usedChar;
 
  402        foreach($this->locationTable as $locationTableEntry){
 
  403            if($locationTableEntry === 
null){
 
  406            foreach($locationTableEntry->getUsedSectors() as $sectorIndex){
 
  407                if($sectorIndex >= strlen($result)){
 
  408                    throw new AssumptionFailedError(
"This should never happen...");
 
  410                if($result[$sectorIndex] === $usedChar){
 
  411                    throw new AssumptionFailedError(
"Overlap detected");
 
  413                $result[$sectorIndex] = $usedChar;
 
  423        $size = $this->nextSector;
 
  424        $used = self::FIRST_SECTOR; 
 
  425        foreach($this->locationTable as $entry){
 
  427                $used += $entry->getSectorCount();
 
  430        return 1 - ($used / $size);
 
 
  433    public function getFilePath() : string{
 
  434        return $this->filePath;
 
  437    public function calculateChunkCount() : int{
 
  439        for($i = 0; $i < 1024; ++$i){
 
  440            if($this->isChunkGenerated($i)){