Bump to API 3.0.0-ALPHA2 - READ DESCRIPTION!

Refactored level\format\generic\GenericChunk -> level\format\Chunk.
Re-added support for async chunk sending
Refactored most Level IO into new namespaces for more organisation
Removed LevelDB loader completely (will be re-added at a later date)
This commit is contained in:
Dylan K. Taylor
2017-01-06 17:13:45 +00:00
parent d8908676ac
commit ad0553fbf8
23 changed files with 795 additions and 1952 deletions

View File

@ -0,0 +1,164 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types = 1);
namespace pocketmine\level\format\io\region;
use pocketmine\level\format\Chunk;
use pocketmine\level\format\SubChunk;
use pocketmine\nbt\NBT;
use pocketmine\nbt\tag\{
ByteArrayTag, ByteTag, CompoundTag, IntArrayTag, IntTag, ListTag, LongTag
};
use pocketmine\Player;
use pocketmine\utils\ChunkException;
use pocketmine\utils\MainLogger;
class Anvil extends McRegion{
const REGION_FILE_EXTENSION = "mca";
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", Chunk::reorderByteArray($subChunk->getBlockIdArray())), //Generic in-memory chunks are currently always XZY
"Data" => new ByteArrayTag("Data", Chunk::reorderNibbleArray($subChunk->getBlockDataArray())),
"SkyLight" => new ByteArrayTag("SkyLight", Chunk::reorderNibbleArray($subChunk->getSkyLightArray())),
"BlockLight" => new ByteArrayTag("BlockLight", Chunk::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 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(
Chunk::reorderByteArray($subChunk->Blocks->getValue()),
Chunk::reorderNibbleArray($subChunk->Data->getValue()),
Chunk::reorderNibbleArray($subChunk->SkyLight->getValue()),
Chunk::reorderNibbleArray($subChunk->BlockLight->getValue())
);
}
}
}
if(isset($chunk->BiomeColors)){
$biomeIds = Chunk::convertBiomeColours($chunk->BiomeColors->getValue()); //Convert back to PC format (RIP colours D:)
}elseif(isset($chunk->Biomes)){
$biomeIds = $chunk->Biomes->getValue();
}else{
$biomeIds = "";
}
$result = new Chunk(
$this,
$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 static function getProviderName() : string{
return "anvil";
}
public function getWorldHeight() : int{
//TODO: add world height options
return 256;
}
}

View File

@ -0,0 +1,443 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types = 1);
namespace pocketmine\level\format\io\region;
use pocketmine\level\format\Chunk;
use pocketmine\level\format\io\BaseLevelProvider;
use pocketmine\level\format\SubChunk;
use pocketmine\level\generator\Generator;
use pocketmine\level\Level;
use pocketmine\nbt\NBT;
use pocketmine\nbt\tag\{
ByteArrayTag, ByteTag, CompoundTag, IntArrayTag, IntTag, ListTag, LongTag, StringTag
};
use pocketmine\Player;
use pocketmine\utils\MainLogger;
class McRegion extends BaseLevelProvider{
const REGION_FILE_EXTENSION = "mcr";
/** @var RegionLoader[] */
protected $regions = [];
/** @var Chunk[] */
protected $chunks = [];
/**
* @param Chunk $chunk
*
* @return string
*/
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", 0); //guess
$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());
$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->getSkyLightColumn($x, $z);
$blockLight .= $subChunk->getBlockLightColumn($x, $z);
}
}
}
$nbt->Blocks = new ByteArrayTag("Blocks", $ids);
$nbt->Data = new ByteArrayTag("Data", $data);
$nbt->SkyLight = new ByteArrayTag("SkyLight", $skyLight);
$nbt->BlockLight = new ByteArrayTag("BlockLight", $blockLight);
$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);
}
/**
* @param string $data
*
* @return Chunk|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)){
return null;
}
$chunk = $chunk->Level;
$subChunks = [];
$fullIds = isset($chunk->Blocks) ? $chunk->Blocks->getValue() : str_repeat("\x00", 32768);
$fullData = isset($chunk->Data) ? $chunk->Data->getValue() : (str_repeat("\x00", 16384));
$fullSkyLight = isset($chunk->SkyLight) ? $chunk->SkyLight->getValue() : str_repeat("\xff", 16384);
$fullBlockLight = isset($chunk->BlockLight) ? $chunk->BlockLight->getValue() : (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(isset($chunk->BiomeColors)){
$biomeIds = Chunk::convertBiomeColours($chunk->BiomeColors->getValue()); //Convert back to PC format (RIP colours D:)
}elseif(isset($chunk->Biomes)){
$biomeIds = $chunk->Biomes->getValue();
}else{
$biomeIds = "";
}
$result = new Chunk(
$this,
$chunk["xPos"],
$chunk["zPos"],
$subChunks,
isset($chunk->Entities) ? $chunk->Entities->getValue() : [],
isset($chunk->TileEntities) ? $chunk->TileEntities->getValue() : [],
$biomeIds,
isset($chunk->HeightMap) ? $chunk->HeightMap->getValue() : [] //this shouldn't exist in normal mcregion worlds anyway...
);
$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 static function getProviderName() : string{
return "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 = glob($path . "/region/*.mc*");
if(empty($files)){ //possible glob() issue on some systems
$files = array_filter(scandir($path . "/region/"), 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, $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", [
"hardcore" => new ByteTag("hardcore", 0),
"initialized" => new ByteTag("initialized", 1),
"GameType" => new IntTag("GameType", 0),
"generatorVersion" => new IntTag("generatorVersion", 1), //2 in MCPE
"SpawnX" => new IntTag("SpawnX", 256),
"SpawnY" => new IntTag("SpawnY", 70),
"SpawnZ" => new IntTag("SpawnZ", 256),
"version" => new IntTag("version", 19133),
"DayTime" => new IntTag("DayTime", 0),
"LastPlayed" => new LongTag("LastPlayed", microtime(true) * 1000),
"RandomSeed" => new LongTag("RandomSeed", $seed),
"SizeOnDisk" => new LongTag("SizeOnDisk", 0),
"Time" => new LongTag("Time", 0),
"generatorName" => new StringTag("generatorName", Generator::getGeneratorName($generator)),
"generatorOptions" => new StringTag("generatorOptions", isset($options["preset"]) ? $options["preset"] : ""),
"LevelName" => new StringTag("LevelName", $name),
"GameRules" => new CompoundTag("GameRules", [])
]);
$nbt = new NBT(NBT::BIG_ENDIAN);
$nbt->setData(new CompoundTag("", [
"Data" => $levelData
]));
$buffer = $nbt->writeCompressed();
file_put_contents($path . "level.dat", $buffer);
}
public function getGenerator() : string{
return $this->levelData["generatorName"];
}
public function getGeneratorOptions() : array{
return ["preset" => $this->levelData["generatorOptions"]];
}
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){
$chunk->setProvider($this);
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)){
$this->getRegion($chunkX >> 5, $chunkZ >> 5)->writeChunk($this->getChunk($chunkX, $chunkZ));
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);
$this->loadRegion($regionX, $regionZ);
$this->level->timings->syncChunkLoadDataTimer->startTiming();
$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(false, $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){
return Chunk::getEmptyChunk($chunkX, $chunkZ, $this);
}
/**
* @param int $x
* @param int $z
*
* @return RegionLoader
*/
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);
}
}
public function close(){
$this->unloadChunks();
foreach($this->regions as $index => $region){
$region->close();
unset($this->regions[$index]);
}
$this->level = null;
}
}

View File

@ -0,0 +1,154 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types = 1);
namespace pocketmine\level\format\io\region;
use pocketmine\level\format\Chunk;
use pocketmine\level\format\SubChunk;
use pocketmine\nbt\NBT;
use pocketmine\nbt\tag\{
ByteArrayTag, ByteTag, CompoundTag, IntArrayTag, IntTag, ListTag, LongTag
};
use pocketmine\Player;
use pocketmine\utils\ChunkException;
use pocketmine\utils\MainLogger;
/**
* This format is exactly the same as the PC Anvil format, with the only difference being that the stored data order
* is XZY instead of YZX for more performance loading and saving worlds.
*/
class PMAnvil extends Anvil{
const REGION_FILE_EXTENSION = "mcapm";
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", $subChunk->getBlockIdArray()),
"Data" => new ByteArrayTag("Data", $subChunk->getBlockDataArray()),
"SkyLight" => new ByteArrayTag("SkyLight", $subChunk->getSkyLightArray()),
"BlockLight" => new ByteArrayTag("BlockLight", $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 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(
$subChunk->Blocks->getValue(),
$subChunk->Data->getValue(),
$subChunk->SkyLight->getValue(),
$subChunk->BlockLight->getValue()
);
}
}
}
$result = new Chunk(
$this,
$chunk["xPos"],
$chunk["zPos"],
$subChunks,
isset($chunk->Entities) ? $chunk->Entities->getValue() : [],
isset($chunk->TileEntities) ? $chunk->TileEntities->getValue() : [],
isset($chunk->Biomes) ? $chunk->Biomes->getValue() : "",
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 static function getProviderName() : string{
return "pmanvil";
}
}

View File

@ -0,0 +1,304 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types = 1);
namespace pocketmine\level\format\io\region;
use pocketmine\level\format\Chunk;
use pocketmine\level\format\io\LevelProvider;
use pocketmine\utils\Binary;
use pocketmine\utils\ChunkException;
use pocketmine\utils\MainLogger;
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 LevelProvider */
protected $levelProvider;
protected $locationTable = [];
public $lastUsed;
public function __construct(LevelProvider $level, int $regionX, int $regionZ, string $fileExtension = McRegion::REGION_FILE_EXTENSION){
$this->x = $regionX;
$this->z = $regionZ;
$this->levelProvider = $level;
$this->filePath = $this->levelProvider->getPath() . "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;
MainLogger::getLogger()->error("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->levelProvider->nbtDeserialize(fread($this->filePointer, $length - 1));
if($chunk instanceof Chunk){
return $chunk;
}else{
MainLogger::getLogger()->error("Corrupted chunk detected");
return null;
}
}
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->levelProvider->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, 8192); // this fills the file with the null byte
$this->lastSector = 1;
$this->locationTable = array_fill(0, 1024, [0, 0, 0]);
}
public function getX() : int{
return $this->x;
}
public function getZ() : int{
return $this->z;
}
}