setInt("xPos", $chunk->getX()); $nbt->setInt("zPos", $chunk->getZ()); $nbt->setLong("LastUpdate", 0); //TODO $nbt->setByte("TerrainPopulated", $chunk->isPopulated() ? 1 : 0); $nbt->setByte("LightPopulated", $chunk->isLightPopulated() ? 1 : 0); $ids = ""; $data = ""; $skyLight = ""; $blockLight = ""; $subChunks = $chunk->getSubChunks(); for($x = 0; $x < 16; ++$x){ for($z = 0; $z < 16; ++$z){ for($y = 0; $y < 8; ++$y){ $subChunk = $subChunks[$y]; $ids .= $subChunk->getBlockIdColumn($x, $z); $data .= $subChunk->getBlockDataColumn($x, $z); $skyLight .= $subChunk->getBlockSkyLightColumn($x, $z); $blockLight .= $subChunk->getBlockLightColumn($x, $z); } } } $nbt->setByteArray("Blocks", $ids); $nbt->setByteArray("Data", $data); $nbt->setByteArray("SkyLight", $skyLight); $nbt->setByteArray("BlockLight", $blockLight); $nbt->setByteArray("Biomes", $chunk->getBiomeIdArray()); //doesn't exist in regular McRegion, this is here for PocketMine-MP only $nbt->setByteArray("HeightMap", pack("C*", ...$chunk->getHeightMapArray())); //this is ByteArray in McRegion, but IntArray in Anvil (due to raised build height) $entities = []; foreach($chunk->getEntities() as $entity){ if($entity->canSaveWithChunk() and !$entity->isClosed()){ $entity->saveNBT(); $entities[] = $entity->namedtag; } } $nbt->setTag(new ListTag("Entities", $entities, NBT::TAG_Compound)); $tiles = []; foreach($chunk->getTiles() as $tile){ $tile->saveNBT(); $tiles[] = $tile->namedtag; } $nbt->setTag(new ListTag("TileEntities", $tiles, NBT::TAG_Compound)); $writer = new NBT(NBT::BIG_ENDIAN); $nbt->setName("Level"); $writer->setData(new CompoundTag("", [$nbt])); return $writer->writeCompressed(ZLIB_ENCODING_DEFLATE, RegionLoader::$COMPRESSION_LEVEL); } /** * @param string $data * * @return Chunk|null */ public function nbtDeserialize(string $data){ $nbt = new NBT(NBT::BIG_ENDIAN); try{ $nbt->readCompressed($data); $chunk = $nbt->getData()->getCompoundTag("Level"); if($chunk === null){ throw new ChunkException("Invalid NBT format, 'Level' key not found"); } $subChunks = []; $fullIds = $chunk->hasTag("Blocks", ByteArrayTag::class) ? $chunk->getByteArray("Blocks") : str_repeat("\x00", 32768); $fullData = $chunk->hasTag("Data", ByteArrayTag::class) ? $chunk->getByteArray("Data") : str_repeat("\x00", 16384); $fullSkyLight = $chunk->hasTag("SkyLight", ByteArrayTag::class) ? $chunk->getByteArray("SkyLight") : str_repeat("\xff", 16384); $fullBlockLight = $chunk->hasTag("BlockLight", ByteArrayTag::class) ? $chunk->getByteArray("BlockLight") : str_repeat("\x00", 16384); for($y = 0; $y < 8; ++$y){ $offset = ($y << 4); $ids = ""; for($i = 0; $i < 256; ++$i){ $ids .= substr($fullIds, $offset, 16); $offset += 128; } $data = ""; $offset = ($y << 3); for($i = 0; $i < 256; ++$i){ $data .= substr($fullData, $offset, 8); $offset += 64; } $skyLight = ""; $offset = ($y << 3); for($i = 0; $i < 256; ++$i){ $skyLight .= substr($fullSkyLight, $offset, 8); $offset += 64; } $blockLight = ""; $offset = ($y << 3); for($i = 0; $i < 256; ++$i){ $blockLight .= substr($fullBlockLight, $offset, 8); $offset += 64; } $subChunks[$y] = new SubChunk($ids, $data, $skyLight, $blockLight); } if($chunk->hasTag("BiomeColors", IntArrayTag::class)){ $biomeIds = ChunkUtils::convertBiomeColors($chunk->getIntArray("BiomeColors")); //Convert back to original format }elseif($chunk->hasTag("Biomes", ByteArrayTag::class)){ $biomeIds = $chunk->getByteArray("Biomes"); }else{ $biomeIds = ""; } $heightMap = []; if($chunk->hasTag("HeightMap", ByteArrayTag::class)){ $heightMap = array_values(unpack("C*", $chunk->getByteArray("HeightMap"))); }elseif($chunk->hasTag("HeightMap", IntArrayTag::class)){ $heightMap = $chunk->getIntArray("HeightMap"); #blameshoghicp } $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, $heightMap ); $result->setLightPopulated($chunk->getByte("LightPopulated", 0) !== 0); $result->setPopulated($chunk->getByte("TerrainPopulated", 0) !== 0); $result->setGenerated(true); return $result; }catch(\Throwable $e){ MainLogger::getLogger()->logException($e); return null; } } public static function getProviderName() : string{ return "mcregion"; } /** * Returns the storage version as per Minecraft PC world formats. * @return int */ public static function getPcWorldFormatVersion() : int{ return 19132; //mcregion } 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", Generator::getGeneratorName($generator)), new StringTag("generatorOptions", $options["preset"] ?? ""), new StringTag("LevelName", $name), new CompoundTag("GameRules", []) ]); $nbt = new NBT(NBT::BIG_ENDIAN); $nbt->setData(new CompoundTag("", [ $levelData ])); $buffer = $nbt->writeCompressed(); file_put_contents($path . "level.dat", $buffer); } public function getGenerator() : string{ return (string) $this->levelData["generatorName"]; } public function getGeneratorOptions() : array{ return ["preset" => $this->levelData["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 getChunk(int $chunkX, int $chunkZ, bool $create = false){ $index = Level::chunkHash($chunkX, $chunkZ); if(isset($this->chunks[$index])){ return $this->chunks[$index]; }else{ $this->loadChunk($chunkX, $chunkZ, $create); return $this->chunks[$index] ?? null; } } public function setChunk(int $chunkX, int $chunkZ, Chunk $chunk){ self::getRegionIndex($chunkX, $chunkZ, $regionX, $regionZ); $this->loadRegion($regionX, $regionZ); $chunk->setX($chunkX); $chunk->setZ($chunkZ); if(isset($this->chunks[$index = Level::chunkHash($chunkX, $chunkZ)]) and $this->chunks[$index] !== $chunk){ $this->unloadChunk($chunkX, $chunkZ, false); } $this->chunks[$index] = $chunk; } public function saveChunk(int $chunkX, int $chunkZ) : bool{ if($this->isChunkLoaded($chunkX, $chunkZ)){ $chunk = $this->getChunk($chunkX, $chunkZ); if(!$chunk->isGenerated()){ throw new \InvalidStateException("Cannot save un-generated chunk"); } $this->getRegion($chunkX >> 5, $chunkZ >> 5)->writeChunk($chunk); return true; } return false; } public function saveChunks(){ foreach($this->chunks as $chunk){ $this->saveChunk($chunk->getX(), $chunk->getZ()); } } public function loadChunk(int $chunkX, int $chunkZ, bool $create = false) : bool{ $index = Level::chunkHash($chunkX, $chunkZ); if(isset($this->chunks[$index])){ return true; } $regionX = $regionZ = null; self::getRegionIndex($chunkX, $chunkZ, $regionX, $regionZ); /** @noinspection PhpStrictTypeCheckingInspection */ $this->loadRegion($regionX, $regionZ); $this->level->timings->syncChunkLoadDataTimer->startTiming(); /** @noinspection PhpStrictTypeCheckingInspection */ $chunk = $this->getRegion($regionX, $regionZ)->readChunk($chunkX - $regionX * 32, $chunkZ - $regionZ * 32); if($chunk === null and $create){ $chunk = $this->getEmptyChunk($chunkX, $chunkZ); } $this->level->timings->syncChunkLoadDataTimer->stopTiming(); if($chunk !== null){ $this->chunks[$index] = $chunk; return true; }else{ return false; } } public function unloadChunk(int $chunkX, int $chunkZ, bool $safe = true) : bool{ $chunk = $this->chunks[$index = Level::chunkHash($chunkX, $chunkZ)] ?? null; if($chunk instanceof Chunk and $chunk->unload($safe)){ unset($this->chunks[$index]); return true; } return false; } public function unloadChunks(){ foreach($this->chunks as $chunk){ $this->unloadChunk($chunk->getX(), $chunk->getZ(), false); } $this->chunks = []; } public function isChunkLoaded(int $chunkX, int $chunkZ) : bool{ return isset($this->chunks[Level::chunkHash($chunkX, $chunkZ)]); } public function isChunkGenerated(int $chunkX, int $chunkZ) : bool{ if(($region = $this->getRegion($chunkX >> 5, $chunkZ >> 5)) !== null){ return $region->chunkExists($chunkX - $region->getX() * 32, $chunkZ - $region->getZ() * 32) and $this->getChunk($chunkX - $region->getX() * 32, $chunkZ - $region->getZ() * 32, true)->isGenerated(); } return false; } public function isChunkPopulated(int $chunkX, int $chunkZ) : bool{ $chunk = $this->getChunk($chunkX, $chunkZ); if($chunk !== null){ return $chunk->isPopulated(); }else{ return false; } } public function getLoadedChunks() : array{ return $this->chunks; } 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 &$x * @param int &$z */ public static function getRegionIndex(int $chunkX, int $chunkZ, &$x, &$z){ $x = $chunkX >> 5; $z = $chunkZ >> 5; } /** * @param int $chunkX * @param int $chunkZ * * @return Chunk */ public function getEmptyChunk(int $chunkX, int $chunkZ) : Chunk{ return Chunk::getEmptyChunk($chunkX, $chunkZ); } /** * @param int $x * @param int $z * * @return RegionLoader|null */ protected function getRegion(int $x, int $z){ return $this->regions[Level::chunkHash($x, $z)] ?? null; } /** * @param int $x * @param int $z */ protected function loadRegion(int $x, int $z){ if(!isset($this->regions[$index = Level::chunkHash($x, $z)])){ $this->regions[$index] = new RegionLoader($this, $x, $z, static::REGION_FILE_EXTENSION); try{ $this->regions[$index]->open(); }catch(CorruptedRegionException $e){ $logger = $this->level->getServer()->getLogger(); $logger->error("Corrupted region file detected: " . $e->getMessage()); $this->regions[$index]->close(false); //Do not write anything to the file $path = $this->regions[$index]->getFilePath(); $backupPath = $path . ".bak." . time(); rename($path, $backupPath); $logger->error("Corrupted region file has been backed up to " . $backupPath); $this->regions[$index] = new RegionLoader($this, $x, $z, static::REGION_FILE_EXTENSION); $this->regions[$index]->open(); //this will create a new empty region to replace the corrupted one } } } public function close(){ $this->unloadChunks(); foreach($this->regions as $index => $region){ $region->close(); unset($this->regions[$index]); } $this->level = null; } }