writeCompressed(new CompoundTag("", [ $levelData ])); file_put_contents($path . "level.dat", $buffer); } /** @var RegionLoader[] */ protected $regions = []; protected function loadLevelData() : void{ $levelDatPath = $this->getPath() . "level.dat"; if(!file_exists($levelDatPath)){ throw new LevelException("level.dat not found"); } $nbt = new BigEndianNBTStream(); $levelData = $nbt->readCompressed(file_get_contents($levelDatPath)); if(!($levelData instanceof CompoundTag) or !$levelData->hasTag("Data", CompoundTag::class)){ throw new LevelException("Invalid level.dat"); } $this->levelData = $levelData->getCompoundTag("Data"); } protected function fixLevelData() : void{ if(!$this->levelData->hasTag("generatorName", StringTag::class)){ $this->levelData->setString("generatorName", "default", true); }elseif(($generatorName = self::hackyFixForGeneratorClasspathInLevelDat($this->levelData->getString("generatorName"))) !== null){ $this->levelData->setString("generatorName", $generatorName); } if(!$this->levelData->hasTag("generatorOptions", StringTag::class)){ $this->levelData->setString("generatorOptions", ""); } } public function saveLevelData(){ $nbt = new BigEndianNBTStream(); $buffer = $nbt->writeCompressed(new CompoundTag("", [ $this->levelData ])); file_put_contents($this->getPath() . "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::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; } } } } } }