mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-09-06 01:46:04 +00:00
Removed pocketmine subdirectory, map PSR-4 style
This commit is contained in:
94
src/world/format/io/BaseWorldProvider.php
Normal file
94
src/world/format/io/BaseWorldProvider.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?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\world\format\io;
|
||||
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\format\io\exception\CorruptedChunkException;
|
||||
use pocketmine\world\format\io\exception\CorruptedWorldException;
|
||||
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
|
||||
use pocketmine\world\WorldException;
|
||||
use function file_exists;
|
||||
|
||||
abstract class BaseWorldProvider implements WorldProvider{
|
||||
/** @var string */
|
||||
protected $path;
|
||||
/** @var WorldData */
|
||||
protected $worldData;
|
||||
|
||||
public function __construct(string $path){
|
||||
if(!file_exists($path)){
|
||||
throw new WorldException("World does not exist");
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
$this->worldData = $this->loadLevelData();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WorldData
|
||||
* @throws CorruptedWorldException
|
||||
* @throws UnsupportedWorldFormatException
|
||||
*/
|
||||
abstract protected function loadLevelData() : WorldData;
|
||||
|
||||
public function getPath() : string{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WorldData
|
||||
*/
|
||||
public function getWorldData() : WorldData{
|
||||
return $this->worldData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $chunkX
|
||||
* @param int $chunkZ
|
||||
*
|
||||
* @return Chunk|null
|
||||
* @throws CorruptedChunkException
|
||||
*/
|
||||
public function loadChunk(int $chunkX, int $chunkZ) : ?Chunk{
|
||||
return $this->readChunk($chunkX, $chunkZ);
|
||||
}
|
||||
|
||||
public function saveChunk(Chunk $chunk) : void{
|
||||
if(!$chunk->isGenerated()){
|
||||
throw new \InvalidStateException("Cannot save un-generated chunk");
|
||||
}
|
||||
$this->writeChunk($chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $chunkX
|
||||
* @param int $chunkZ
|
||||
*
|
||||
* @return Chunk|null
|
||||
* @throws CorruptedChunkException
|
||||
*/
|
||||
abstract protected function readChunk(int $chunkX, int $chunkZ) : ?Chunk;
|
||||
|
||||
abstract protected function writeChunk(Chunk $chunk) : void;
|
||||
}
|
46
src/world/format/io/ChunkUtils.php
Normal file
46
src/world/format/io/ChunkUtils.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?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\world\format\io;
|
||||
|
||||
use function chr;
|
||||
use function str_repeat;
|
||||
|
||||
class ChunkUtils{
|
||||
|
||||
/**
|
||||
* Converts pre-MCPE-1.0 biome color array to biome ID array.
|
||||
*
|
||||
* @param int[] $array of biome color values
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function convertBiomeColors(array $array) : string{
|
||||
$result = str_repeat("\x00", 256);
|
||||
foreach($array as $i => $color){
|
||||
$result[$i] = chr(($color >> 24) & 0xff);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
156
src/world/format/io/FastChunkSerializer.php
Normal file
156
src/world/format/io/FastChunkSerializer.php
Normal file
@ -0,0 +1,156 @@
|
||||
<?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\world\format\io;
|
||||
|
||||
use pocketmine\block\BlockLegacyIds;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\format\EmptySubChunk;
|
||||
use pocketmine\world\format\LightArray;
|
||||
use pocketmine\world\format\PalettedBlockArray;
|
||||
use pocketmine\world\format\SubChunk;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function pack;
|
||||
use function strlen;
|
||||
use function unpack;
|
||||
|
||||
/**
|
||||
* This class provides a serializer used for transmitting chunks between threads.
|
||||
* The serialization format **is not intended for permanent storage** and may change without warning.
|
||||
*/
|
||||
final class FastChunkSerializer{
|
||||
|
||||
private function __construct(){
|
||||
//NOOP
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast-serializes the chunk for passing between threads
|
||||
* TODO: tiles and entities
|
||||
*
|
||||
* @param Chunk $chunk
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function serialize(Chunk $chunk) : string{
|
||||
$stream = new BinaryStream();
|
||||
$stream->putInt($chunk->getX());
|
||||
$stream->putInt($chunk->getZ());
|
||||
$stream->putByte(($chunk->isLightPopulated() ? 4 : 0) | ($chunk->isPopulated() ? 2 : 0) | ($chunk->isGenerated() ? 1 : 0));
|
||||
if($chunk->isGenerated()){
|
||||
//subchunks
|
||||
$count = 0;
|
||||
$subStream = new BinaryStream();
|
||||
foreach($chunk->getSubChunks() as $y => $subChunk){
|
||||
if($subChunk instanceof EmptySubChunk){
|
||||
continue;
|
||||
}
|
||||
++$count;
|
||||
|
||||
$subStream->putByte($y);
|
||||
$layers = $subChunk->getBlockLayers();
|
||||
$subStream->putByte(count($subChunk->getBlockLayers()));
|
||||
foreach($layers as $blocks){
|
||||
$wordArray = $blocks->getWordArray();
|
||||
$palette = $blocks->getPalette();
|
||||
|
||||
$subStream->putByte($blocks->getBitsPerBlock());
|
||||
$subStream->put($wordArray);
|
||||
$serialPalette = pack("N*", ...$palette);
|
||||
$subStream->putInt(strlen($serialPalette));
|
||||
$subStream->put($serialPalette);
|
||||
}
|
||||
|
||||
if($chunk->isLightPopulated()){
|
||||
$subStream->put($subChunk->getBlockSkyLightArray()->getData());
|
||||
$subStream->put($subChunk->getBlockLightArray()->getData());
|
||||
}
|
||||
}
|
||||
$stream->putByte($count);
|
||||
$stream->put($subStream->getBuffer());
|
||||
|
||||
//biomes
|
||||
$stream->put($chunk->getBiomeIdArray());
|
||||
if($chunk->isLightPopulated()){
|
||||
$stream->put(pack("v*", ...$chunk->getHeightMapArray()));
|
||||
}
|
||||
}
|
||||
|
||||
return $stream->getBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a fast-serialized chunk
|
||||
*
|
||||
* @param string $data
|
||||
*
|
||||
* @return Chunk
|
||||
*/
|
||||
public static function deserialize(string $data) : Chunk{
|
||||
$stream = new BinaryStream($data);
|
||||
|
||||
$x = $stream->getInt();
|
||||
$z = $stream->getInt();
|
||||
$flags = $stream->getByte();
|
||||
$lightPopulated = (bool) ($flags & 4);
|
||||
$terrainPopulated = (bool) ($flags & 2);
|
||||
$terrainGenerated = (bool) ($flags & 1);
|
||||
|
||||
$subChunks = [];
|
||||
$biomeIds = "";
|
||||
$heightMap = [];
|
||||
if($terrainGenerated){
|
||||
$count = $stream->getByte();
|
||||
for($subCount = 0; $subCount < $count; ++$subCount){
|
||||
$y = $stream->getByte();
|
||||
|
||||
/** @var PalettedBlockArray[] $layers */
|
||||
$layers = [];
|
||||
for($i = 0, $layerCount = $stream->getByte(); $i < $layerCount; ++$i){
|
||||
$bitsPerBlock = $stream->getByte();
|
||||
$words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
|
||||
$palette = array_values(unpack("N*", $stream->get($stream->getInt())));
|
||||
|
||||
$layers[] = PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
|
||||
}
|
||||
$subChunks[$y] = new SubChunk(
|
||||
BlockLegacyIds::AIR << 4, $layers, $lightPopulated ? new LightArray($stream->get(2048)) : null, $lightPopulated ? new LightArray($stream->get(2048)) : null
|
||||
);
|
||||
}
|
||||
|
||||
$biomeIds = $stream->get(256);
|
||||
if($lightPopulated){
|
||||
$heightMap = array_values(unpack("v*", $stream->get(512)));
|
||||
}
|
||||
}
|
||||
|
||||
$chunk = new Chunk($x, $z, $subChunks, null, null, $biomeIds, $heightMap);
|
||||
$chunk->setGenerated($terrainGenerated);
|
||||
$chunk->setPopulated($terrainPopulated);
|
||||
$chunk->setLightPopulated($lightPopulated);
|
||||
|
||||
return $chunk;
|
||||
}
|
||||
}
|
150
src/world/format/io/FormatConverter.php
Normal file
150
src/world/format/io/FormatConverter.php
Normal file
@ -0,0 +1,150 @@
|
||||
<?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\world\format\io;
|
||||
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\generator\GeneratorManager;
|
||||
use function basename;
|
||||
use function crc32;
|
||||
use function file_exists;
|
||||
use function floor;
|
||||
use function microtime;
|
||||
use function mkdir;
|
||||
use function random_bytes;
|
||||
use function rename;
|
||||
use function round;
|
||||
use function rtrim;
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
class FormatConverter{
|
||||
|
||||
/** @var WorldProvider */
|
||||
private $oldProvider;
|
||||
/** @var WritableWorldProvider|string */
|
||||
private $newProvider;
|
||||
|
||||
/** @var string */
|
||||
private $backupPath;
|
||||
|
||||
/** @var \Logger */
|
||||
private $logger;
|
||||
|
||||
public function __construct(WorldProvider $oldProvider, string $newProvider, string $backupPath, \Logger $logger){
|
||||
$this->oldProvider = $oldProvider;
|
||||
Utils::testValidInstance($newProvider, WritableWorldProvider::class);
|
||||
$this->newProvider = $newProvider;
|
||||
$this->logger = new \PrefixedLogger($logger, "World Converter: " . $this->oldProvider->getWorldData()->getName());
|
||||
|
||||
if(!file_exists($backupPath)){
|
||||
@mkdir($backupPath, 0777, true);
|
||||
}
|
||||
$nextSuffix = "";
|
||||
do{
|
||||
$this->backupPath = $backupPath . DIRECTORY_SEPARATOR . basename($this->oldProvider->getPath()) . $nextSuffix;
|
||||
$nextSuffix = "_" . crc32(random_bytes(4));
|
||||
}while(file_exists($this->backupPath));
|
||||
}
|
||||
|
||||
public function getBackupPath() : string{
|
||||
return $this->backupPath;
|
||||
}
|
||||
|
||||
public function execute() : WritableWorldProvider{
|
||||
$new = $this->generateNew();
|
||||
|
||||
$this->populateLevelData($new->getWorldData());
|
||||
$this->convertTerrain($new);
|
||||
|
||||
$path = $this->oldProvider->getPath();
|
||||
$this->oldProvider->close();
|
||||
$new->close();
|
||||
|
||||
$this->logger->info("Backing up pre-conversion world to " . $this->backupPath);
|
||||
rename($path, $this->backupPath);
|
||||
rename($new->getPath(), $path);
|
||||
|
||||
$this->logger->info("Conversion completed");
|
||||
/**
|
||||
* @see WritableWorldProvider::__construct()
|
||||
*/
|
||||
return new $this->newProvider($path);
|
||||
}
|
||||
|
||||
private function generateNew() : WritableWorldProvider{
|
||||
$this->logger->info("Generating new world");
|
||||
$data = $this->oldProvider->getWorldData();
|
||||
|
||||
$convertedOutput = rtrim($this->oldProvider->getPath(), "/\\") . "_converted" . DIRECTORY_SEPARATOR;
|
||||
if(file_exists($convertedOutput)){
|
||||
$this->logger->info("Found previous conversion attempt, deleting...");
|
||||
Utils::recursiveUnlink($convertedOutput);
|
||||
}
|
||||
$this->newProvider::generate($convertedOutput, $data->getName(), $data->getSeed(), GeneratorManager::getGenerator($data->getGenerator()), $data->getGeneratorOptions());
|
||||
|
||||
/**
|
||||
* @see WritableWorldProvider::__construct()
|
||||
*/
|
||||
return new $this->newProvider($convertedOutput);
|
||||
}
|
||||
|
||||
private function populateLevelData(WorldData $data) : void{
|
||||
$this->logger->info("Converting world manifest");
|
||||
$oldData = $this->oldProvider->getWorldData();
|
||||
$data->setDifficulty($oldData->getDifficulty());
|
||||
$data->setLightningLevel($oldData->getLightningLevel());
|
||||
$data->setLightningTime($oldData->getLightningTime());
|
||||
$data->setRainLevel($oldData->getRainLevel());
|
||||
$data->setRainTime($oldData->getRainTime());
|
||||
$data->setSpawn($oldData->getSpawn());
|
||||
$data->setTime($oldData->getTime());
|
||||
|
||||
$data->save();
|
||||
$this->logger->info("Finished converting manifest");
|
||||
//TODO: add more properties as-needed
|
||||
}
|
||||
|
||||
private function convertTerrain(WritableWorldProvider $new) : void{
|
||||
$this->logger->info("Calculating chunk count");
|
||||
$count = $this->oldProvider->calculateChunkCount();
|
||||
$this->logger->info("Discovered $count chunks");
|
||||
|
||||
$counter = 0;
|
||||
|
||||
$start = microtime(true);
|
||||
$thisRound = $start;
|
||||
static $reportInterval = 256;
|
||||
foreach($this->oldProvider->getAllChunks(true, $this->logger) as $chunk){
|
||||
$new->saveChunk($chunk);
|
||||
$counter++;
|
||||
if(($counter % $reportInterval) === 0){
|
||||
$time = microtime(true);
|
||||
$diff = $time - $thisRound;
|
||||
$thisRound = $time;
|
||||
$this->logger->info("Converted $counter / $count chunks (" . floor($reportInterval / $diff) . " chunks/sec)");
|
||||
}
|
||||
}
|
||||
$total = microtime(true) - $start;
|
||||
$this->logger->info("Converted $counter / $counter chunks in " . round($total, 3) . " seconds (" . floor($counter / $total) . " chunks/sec)");
|
||||
}
|
||||
}
|
136
src/world/format/io/WorldData.php
Normal file
136
src/world/format/io/WorldData.php
Normal file
@ -0,0 +1,136 @@
|
||||
<?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\world\format\io;
|
||||
|
||||
use pocketmine\math\Vector3;
|
||||
|
||||
interface WorldData{
|
||||
|
||||
/**
|
||||
* Saves information about the world state, such as weather, time, etc.
|
||||
*/
|
||||
public function save() : void;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName() : string;
|
||||
|
||||
/**
|
||||
* Returns the generator name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getGenerator() : string;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getGeneratorOptions() : array;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getSeed() : int;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getTime() : int;
|
||||
|
||||
/**
|
||||
* @param int $value
|
||||
*/
|
||||
public function setTime(int $value) : void;
|
||||
|
||||
|
||||
/**
|
||||
* @return Vector3
|
||||
*/
|
||||
public function getSpawn() : Vector3;
|
||||
|
||||
/**
|
||||
* @param Vector3 $pos
|
||||
*/
|
||||
public function setSpawn(Vector3 $pos) : void;
|
||||
|
||||
/**
|
||||
* Returns the world difficulty. This will be one of the World constants.
|
||||
* @return int
|
||||
*/
|
||||
public function getDifficulty() : int;
|
||||
|
||||
/**
|
||||
* Sets the world difficulty.
|
||||
*
|
||||
* @param int $difficulty
|
||||
*/
|
||||
public function setDifficulty(int $difficulty) : void;
|
||||
|
||||
/**
|
||||
* Returns the time in ticks to the next rain level change.
|
||||
* @return int
|
||||
*/
|
||||
public function getRainTime() : int;
|
||||
|
||||
/**
|
||||
* Sets the time in ticks to the next rain level change.
|
||||
* @param int $ticks
|
||||
*/
|
||||
public function setRainTime(int $ticks) : void;
|
||||
|
||||
/**
|
||||
* @return float 0.0 - 1.0
|
||||
*/
|
||||
public function getRainLevel() : float;
|
||||
|
||||
/**
|
||||
* @param float $level 0.0 - 1.0
|
||||
*/
|
||||
public function setRainLevel(float $level) : void;
|
||||
|
||||
/**
|
||||
* Returns the time in ticks to the next lightning level change.
|
||||
* @return int
|
||||
*/
|
||||
public function getLightningTime() : int;
|
||||
|
||||
/**
|
||||
* Sets the time in ticks to the next lightning level change.
|
||||
* @param int $ticks
|
||||
*/
|
||||
public function setLightningTime(int $ticks) : void;
|
||||
|
||||
/**
|
||||
* @return float 0.0 - 1.0
|
||||
*/
|
||||
public function getLightningLevel() : float;
|
||||
|
||||
/**
|
||||
* @param float $level 0.0 - 1.0
|
||||
*/
|
||||
public function setLightningLevel(float $level) : void;
|
||||
}
|
109
src/world/format/io/WorldProvider.php
Normal file
109
src/world/format/io/WorldProvider.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?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\world\format\io;
|
||||
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\format\io\exception\CorruptedChunkException;
|
||||
use pocketmine\world\format\io\exception\CorruptedWorldException;
|
||||
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
|
||||
|
||||
interface WorldProvider{
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @throws CorruptedWorldException
|
||||
* @throws UnsupportedWorldFormatException
|
||||
*/
|
||||
public function __construct(string $path);
|
||||
|
||||
/**
|
||||
* Gets the build height limit of this world
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getWorldHeight() : int;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPath() : string;
|
||||
|
||||
/**
|
||||
* Tells if the path is a valid world.
|
||||
* This must tell if the current format supports opening the files in the directory
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isValid(string $path) : bool;
|
||||
|
||||
/**
|
||||
* Loads a chunk (usually from disk storage) and returns it. If the chunk does not exist, null is returned.
|
||||
*
|
||||
* @param int $chunkX
|
||||
* @param int $chunkZ
|
||||
*
|
||||
* @return null|Chunk
|
||||
*
|
||||
* @throws CorruptedChunkException
|
||||
*/
|
||||
public function loadChunk(int $chunkX, int $chunkZ) : ?Chunk;
|
||||
|
||||
/**
|
||||
* Performs garbage collection in the world provider, such as cleaning up regions in Region-based worlds.
|
||||
*/
|
||||
public function doGarbageCollection() : void;
|
||||
|
||||
/**
|
||||
* Returns information about the world
|
||||
*
|
||||
* @return WorldData
|
||||
*/
|
||||
public function getWorldData() : WorldData;
|
||||
|
||||
/**
|
||||
* Performs cleanups necessary when the world provider is closed and no longer needed.
|
||||
*/
|
||||
public function close() : void;
|
||||
|
||||
/**
|
||||
* Returns a generator which yields all the chunks in this world.
|
||||
*
|
||||
* @param bool $skipCorrupted
|
||||
*
|
||||
* @param \Logger|null $logger
|
||||
*
|
||||
* @return \Generator|Chunk[]
|
||||
* @throws CorruptedChunkException
|
||||
*/
|
||||
public function getAllChunks(bool $skipCorrupted = false, ?\Logger $logger = null) : \Generator;
|
||||
|
||||
/**
|
||||
* Returns the number of chunks in the provider. Used for world conversion time estimations.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function calculateChunkCount() : int;
|
||||
}
|
115
src/world/format/io/WorldProviderManager.php
Normal file
115
src/world/format/io/WorldProviderManager.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?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\world\format\io;
|
||||
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\format\io\leveldb\LevelDB;
|
||||
use pocketmine\world\format\io\region\Anvil;
|
||||
use pocketmine\world\format\io\region\McRegion;
|
||||
use pocketmine\world\format\io\region\PMAnvil;
|
||||
use function strtolower;
|
||||
use function trim;
|
||||
|
||||
abstract class WorldProviderManager{
|
||||
protected static $providers = [];
|
||||
|
||||
/** @var string|WorldProvider */
|
||||
private static $default = LevelDB::class;
|
||||
|
||||
public static function init() : void{
|
||||
self::addProvider(Anvil::class, "anvil");
|
||||
self::addProvider(McRegion::class, "mcregion");
|
||||
self::addProvider(PMAnvil::class, "pmanvil");
|
||||
self::addProvider(LevelDB::class, "leveldb");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default format used to generate new worlds.
|
||||
*
|
||||
* @return string|WritableWorldProvider
|
||||
*/
|
||||
public static function getDefault() : string{
|
||||
return self::$default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default format.
|
||||
*
|
||||
* @param string $class Class implementing WritableWorldProvider
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public static function setDefault(string $class) : void{
|
||||
Utils::testValidInstance($class, WritableWorldProvider::class);
|
||||
|
||||
self::$default = $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
*
|
||||
* @param string $name
|
||||
* @param bool $overwrite
|
||||
*/
|
||||
public static function addProvider(string $class, string $name, bool $overwrite = false) : void{
|
||||
Utils::testValidInstance($class, WorldProvider::class);
|
||||
|
||||
$name = strtolower($name);
|
||||
if(!$overwrite and isset(self::$providers[$name])){
|
||||
throw new \InvalidArgumentException("Alias \"$name\" is already assigned");
|
||||
}
|
||||
|
||||
/** @var WorldProvider $class */
|
||||
self::$providers[$name] = $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a WorldProvider class for this path, or null
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return string[]|WorldProvider[]
|
||||
*/
|
||||
public static function getMatchingProviders(string $path) : array{
|
||||
$result = [];
|
||||
foreach(self::$providers as $alias => $provider){
|
||||
/** @var WorldProvider|string $provider */
|
||||
if($provider::isValid($path)){
|
||||
$result[$alias] = $provider;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a WorldProvider by name, or null if not found
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public static function getProviderByName(string $name) : ?string{
|
||||
return self::$providers[trim(strtolower($name))] ?? null;
|
||||
}
|
||||
}
|
46
src/world/format/io/WritableWorldProvider.php
Normal file
46
src/world/format/io/WritableWorldProvider.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?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\world\format\io;
|
||||
|
||||
use pocketmine\world\format\Chunk;
|
||||
|
||||
interface WritableWorldProvider extends WorldProvider{
|
||||
/**
|
||||
* Generate the needed files in the path given
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $name
|
||||
* @param int $seed
|
||||
* @param string $generator
|
||||
* @param array[] $options
|
||||
*/
|
||||
public static function generate(string $path, string $name, int $seed, string $generator, array $options = []) : void;
|
||||
|
||||
/**
|
||||
* Saves a chunk (usually to disk).
|
||||
*
|
||||
* @param Chunk $chunk
|
||||
*/
|
||||
public function saveChunk(Chunk $chunk) : void;
|
||||
}
|
148
src/world/format/io/data/BaseNbtWorldData.php
Normal file
148
src/world/format/io/data/BaseNbtWorldData.php
Normal file
@ -0,0 +1,148 @@
|
||||
<?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\world\format\io\data;
|
||||
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\world\format\io\exception\CorruptedWorldException;
|
||||
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
|
||||
use pocketmine\world\format\io\WorldData;
|
||||
use function file_exists;
|
||||
|
||||
abstract class BaseNbtWorldData implements WorldData{
|
||||
|
||||
/** @var string */
|
||||
protected $dataPath;
|
||||
|
||||
/** @var CompoundTag */
|
||||
protected $compoundTag;
|
||||
|
||||
/**
|
||||
* @param string $dataPath
|
||||
*
|
||||
* @throws CorruptedWorldException
|
||||
* @throws UnsupportedWorldFormatException
|
||||
*/
|
||||
public function __construct(string $dataPath){
|
||||
$this->dataPath = $dataPath;
|
||||
|
||||
if(!file_exists($this->dataPath)){
|
||||
throw new CorruptedWorldException("World data not found at $dataPath");
|
||||
}
|
||||
|
||||
try{
|
||||
$this->compoundTag = $this->load();
|
||||
}catch(CorruptedWorldException $e){
|
||||
throw new CorruptedWorldException("Corrupted world data: " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
$this->fix();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CompoundTag
|
||||
* @throws CorruptedWorldException
|
||||
* @throws UnsupportedWorldFormatException
|
||||
*/
|
||||
abstract protected function load() : CompoundTag;
|
||||
|
||||
/**
|
||||
* @throws CorruptedWorldException
|
||||
* @throws UnsupportedWorldFormatException
|
||||
*/
|
||||
abstract protected function fix() : void;
|
||||
|
||||
/**
|
||||
* Hack to fix worlds broken previously by older versions of PocketMine-MP which incorrectly saved classpaths of
|
||||
* generators into level.dat on imported (not generated) worlds.
|
||||
*
|
||||
* This should only have affected leveldb worlds as far as I know, because PC format worlds include the
|
||||
* generatorName tag by default. However, MCPE leveldb ones didn't, and so they would get filled in with something
|
||||
* broken.
|
||||
*
|
||||
* This bug took a long time to get found because previously the generator manager would just return the default
|
||||
* generator silently on failure to identify the correct generator, which caused lots of unexpected bugs.
|
||||
*
|
||||
* Only classnames which were written into the level.dat from "fixing" the level data are included here. These are
|
||||
* hardcoded to avoid problems fixing broken worlds in the future if these classes get moved, renamed or removed.
|
||||
*
|
||||
* @param string $className Classname saved in level.dat
|
||||
*
|
||||
* @return null|string Name of the correct generator to replace the broken value
|
||||
*/
|
||||
protected static function hackyFixForGeneratorClasspathInLevelDat(string $className) : ?string{
|
||||
//THESE ARE DELIBERATELY HARDCODED, DO NOT CHANGE!
|
||||
switch($className){
|
||||
/** @noinspection ClassConstantCanBeUsedInspection */
|
||||
case 'pocketmine\level\generator\normal\Normal':
|
||||
return "normal";
|
||||
/** @noinspection ClassConstantCanBeUsedInspection */
|
||||
case 'pocketmine\level\generator\Flat':
|
||||
return "flat";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getCompoundTag() : CompoundTag{
|
||||
return $this->compoundTag;
|
||||
}
|
||||
|
||||
|
||||
/* The below are common between PC and PE */
|
||||
|
||||
public function getName() : string{
|
||||
return $this->compoundTag->getString("LevelName");
|
||||
}
|
||||
|
||||
public function getGenerator() : string{
|
||||
return $this->compoundTag->getString("generatorName", "DEFAULT");
|
||||
}
|
||||
|
||||
public function getGeneratorOptions() : array{
|
||||
return ["preset" => $this->compoundTag->getString("generatorOptions", "")];
|
||||
}
|
||||
|
||||
public function getSeed() : int{
|
||||
return $this->compoundTag->getLong("RandomSeed");
|
||||
}
|
||||
|
||||
public function getTime() : int{
|
||||
return $this->compoundTag->getLong("Time", 0, true);
|
||||
}
|
||||
|
||||
public function setTime(int $value) : void{
|
||||
$this->compoundTag->setLong("Time", $value, true); //some older PM worlds had this in the wrong format
|
||||
}
|
||||
|
||||
public function getSpawn() : Vector3{
|
||||
return new Vector3($this->compoundTag->getInt("SpawnX"), $this->compoundTag->getInt("SpawnY"), $this->compoundTag->getInt("SpawnZ"));
|
||||
}
|
||||
|
||||
public function setSpawn(Vector3 $pos) : void{
|
||||
$this->compoundTag->setInt("SpawnX", $pos->getFloorX());
|
||||
$this->compoundTag->setInt("SpawnY", $pos->getFloorY());
|
||||
$this->compoundTag->setInt("SpawnZ", $pos->getFloorZ());
|
||||
}
|
||||
|
||||
}
|
201
src/world/format/io/data/BedrockWorldData.php
Normal file
201
src/world/format/io/data/BedrockWorldData.php
Normal file
@ -0,0 +1,201 @@
|
||||
<?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\world\format\io\data;
|
||||
|
||||
use pocketmine\nbt\LittleEndianNbtSerializer;
|
||||
use pocketmine\nbt\NbtDataException;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\IntTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\nbt\TreeRoot;
|
||||
use pocketmine\network\mcpe\protocol\ProtocolInfo;
|
||||
use pocketmine\utils\Binary;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\format\io\exception\CorruptedWorldException;
|
||||
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
|
||||
use pocketmine\world\generator\Flat;
|
||||
use pocketmine\world\generator\Generator;
|
||||
use pocketmine\world\generator\GeneratorManager;
|
||||
use pocketmine\world\World;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use function time;
|
||||
|
||||
class BedrockWorldData extends BaseNbtWorldData{
|
||||
|
||||
public const CURRENT_STORAGE_VERSION = 8;
|
||||
|
||||
public const GENERATOR_LIMITED = 0;
|
||||
public const GENERATOR_INFINITE = 1;
|
||||
public const GENERATOR_FLAT = 2;
|
||||
|
||||
public static function generate(string $path, string $name, int $seed, string $generator, array $options = []) : void{
|
||||
Utils::testValidInstance($generator, Generator::class);
|
||||
switch($generator){
|
||||
case Flat::class:
|
||||
$generatorType = self::GENERATOR_FLAT;
|
||||
break;
|
||||
default:
|
||||
$generatorType = self::GENERATOR_INFINITE;
|
||||
//TODO: add support for limited worlds
|
||||
}
|
||||
|
||||
$worldData = CompoundTag::create()
|
||||
//Vanilla fields
|
||||
->setInt("DayCycleStopTime", -1)
|
||||
->setInt("Difficulty", World::getDifficultyFromString((string) ($options["difficulty"] ?? "normal")))
|
||||
->setByte("ForceGameType", 0)
|
||||
->setInt("GameType", 0)
|
||||
->setInt("Generator", $generatorType)
|
||||
->setLong("LastPlayed", time())
|
||||
->setString("LevelName", $name)
|
||||
->setInt("NetworkVersion", ProtocolInfo::CURRENT_PROTOCOL)
|
||||
//->setInt("Platform", 2) //TODO: find out what the possible values are for
|
||||
->setLong("RandomSeed", $seed)
|
||||
->setInt("SpawnX", 0)
|
||||
->setInt("SpawnY", 32767)
|
||||
->setInt("SpawnZ", 0)
|
||||
->setInt("StorageVersion", self::CURRENT_STORAGE_VERSION)
|
||||
->setLong("Time", 0)
|
||||
->setByte("eduLevel", 0)
|
||||
->setByte("falldamage", 1)
|
||||
->setByte("firedamage", 1)
|
||||
->setByte("hasBeenLoadedInCreative", 1) //badly named, this actually determines whether achievements can be earned in this world...
|
||||
->setByte("immutableWorld", 0)
|
||||
->setFloat("lightningLevel", 0.0)
|
||||
->setInt("lightningTime", 0)
|
||||
->setByte("pvp", 1)
|
||||
->setFloat("rainLevel", 0.0)
|
||||
->setInt("rainTime", 0)
|
||||
->setByte("spawnMobs", 1)
|
||||
->setByte("texturePacksRequired", 0) //TODO
|
||||
|
||||
//Additional PocketMine-MP fields
|
||||
->setTag("GameRules", new CompoundTag())
|
||||
->setByte("hardcore", ($options["hardcore"] ?? false) === true ? 1 : 0)
|
||||
->setString("generatorName", GeneratorManager::getGeneratorName($generator))
|
||||
->setString("generatorOptions", $options["preset"] ?? "");
|
||||
|
||||
$nbt = new LittleEndianNbtSerializer();
|
||||
$buffer = $nbt->write(new TreeRoot($worldData));
|
||||
file_put_contents($path . "level.dat", Binary::writeLInt(self::CURRENT_STORAGE_VERSION) . Binary::writeLInt(strlen($buffer)) . $buffer);
|
||||
}
|
||||
|
||||
protected function load() : CompoundTag{
|
||||
$nbt = new LittleEndianNbtSerializer();
|
||||
try{
|
||||
$worldData = $nbt->read(substr(file_get_contents($this->dataPath), 8))->getTag();
|
||||
}catch(NbtDataException $e){
|
||||
throw new CorruptedWorldException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
$version = $worldData->getInt("StorageVersion", INT32_MAX, true);
|
||||
if($version > self::CURRENT_STORAGE_VERSION){
|
||||
throw new UnsupportedWorldFormatException("LevelDB world format version $version is currently unsupported");
|
||||
}
|
||||
|
||||
return $worldData;
|
||||
}
|
||||
|
||||
protected function fix() : void{
|
||||
if(!$this->compoundTag->hasTag("generatorName", StringTag::class)){
|
||||
if($this->compoundTag->hasTag("Generator", IntTag::class)){
|
||||
switch($this->compoundTag->getInt("Generator")){ //Detect correct generator from MCPE data
|
||||
case self::GENERATOR_FLAT:
|
||||
$this->compoundTag->setString("generatorName", "flat");
|
||||
$this->compoundTag->setString("generatorOptions", "2;7,3,3,2;1");
|
||||
break;
|
||||
case self::GENERATOR_INFINITE:
|
||||
//TODO: add a null generator which does not generate missing chunks (to allow importing back to MCPE and generating more normal terrain without PocketMine messing things up)
|
||||
$this->compoundTag->setString("generatorName", "default");
|
||||
$this->compoundTag->setString("generatorOptions", "");
|
||||
break;
|
||||
case self::GENERATOR_LIMITED:
|
||||
throw new UnsupportedWorldFormatException("Limited worlds are not currently supported");
|
||||
default:
|
||||
throw new UnsupportedWorldFormatException("Unknown LevelDB generator type");
|
||||
}
|
||||
}else{
|
||||
$this->compoundTag->setString("generatorName", "default");
|
||||
}
|
||||
}elseif(($generatorName = self::hackyFixForGeneratorClasspathInLevelDat($this->compoundTag->getString("generatorName"))) !== null){
|
||||
$this->compoundTag->setString("generatorName", $generatorName);
|
||||
}
|
||||
|
||||
if(!$this->compoundTag->hasTag("generatorOptions", StringTag::class)){
|
||||
$this->compoundTag->setString("generatorOptions", "");
|
||||
}
|
||||
}
|
||||
|
||||
public function save() : void{
|
||||
$this->compoundTag->setInt("NetworkVersion", ProtocolInfo::CURRENT_PROTOCOL);
|
||||
$this->compoundTag->setInt("StorageVersion", self::CURRENT_STORAGE_VERSION);
|
||||
|
||||
$nbt = new LittleEndianNbtSerializer();
|
||||
$buffer = $nbt->write(new TreeRoot($this->compoundTag));
|
||||
file_put_contents($this->dataPath, Binary::writeLInt(self::CURRENT_STORAGE_VERSION) . Binary::writeLInt(strlen($buffer)) . $buffer);
|
||||
}
|
||||
|
||||
public function getDifficulty() : int{
|
||||
return $this->compoundTag->getInt("Difficulty", World::DIFFICULTY_NORMAL);
|
||||
}
|
||||
|
||||
public function setDifficulty(int $difficulty) : void{
|
||||
$this->compoundTag->setInt("Difficulty", $difficulty); //yes, this is intended! (in PE: int, PC: byte)
|
||||
}
|
||||
|
||||
public function getRainTime() : int{
|
||||
return $this->compoundTag->getInt("rainTime", 0);
|
||||
}
|
||||
|
||||
public function setRainTime(int $ticks) : void{
|
||||
$this->compoundTag->setInt("rainTime", $ticks);
|
||||
}
|
||||
|
||||
public function getRainLevel() : float{
|
||||
return $this->compoundTag->getFloat("rainLevel", 0.0);
|
||||
}
|
||||
|
||||
public function setRainLevel(float $level) : void{
|
||||
$this->compoundTag->setFloat("rainLevel", $level);
|
||||
}
|
||||
|
||||
public function getLightningTime() : int{
|
||||
return $this->compoundTag->getInt("lightningTime", 0);
|
||||
}
|
||||
|
||||
public function setLightningTime(int $ticks) : void{
|
||||
$this->compoundTag->setInt("lightningTime", $ticks);
|
||||
}
|
||||
|
||||
public function getLightningLevel() : float{
|
||||
return $this->compoundTag->getFloat("lightningLevel", 0.0);
|
||||
}
|
||||
|
||||
public function setLightningLevel(float $level) : void{
|
||||
$this->compoundTag->setFloat("lightningLevel", $level);
|
||||
}
|
||||
}
|
154
src/world/format/io/data/JavaWorldData.php
Normal file
154
src/world/format/io/data/JavaWorldData.php
Normal 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\world\format\io\data;
|
||||
|
||||
use pocketmine\nbt\BigEndianNbtSerializer;
|
||||
use pocketmine\nbt\NbtDataException;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\FloatTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\nbt\TreeRoot;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\format\io\exception\CorruptedWorldException;
|
||||
use pocketmine\world\generator\Generator;
|
||||
use pocketmine\world\generator\GeneratorManager;
|
||||
use pocketmine\world\World;
|
||||
use function ceil;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function microtime;
|
||||
|
||||
class JavaWorldData extends BaseNbtWorldData{
|
||||
|
||||
public static function generate(string $path, string $name, int $seed, string $generator, array $options = [], int $version = 19133) : void{
|
||||
Utils::testValidInstance($generator, Generator::class);
|
||||
//TODO, add extra details
|
||||
$worldData = CompoundTag::create()
|
||||
->setByte("hardcore", ($options["hardcore"] ?? false) === true ? 1 : 0)
|
||||
->setByte("Difficulty", World::getDifficultyFromString((string) ($options["difficulty"] ?? "normal")))
|
||||
->setByte("initialized", 1)
|
||||
->setInt("GameType", 0)
|
||||
->setInt("generatorVersion", 1) //2 in MCPE
|
||||
->setInt("SpawnX", 256)
|
||||
->setInt("SpawnY", 70)
|
||||
->setInt("SpawnZ", 256)
|
||||
->setInt("version", $version)
|
||||
->setInt("DayTime", 0)
|
||||
->setLong("LastPlayed", (int) (microtime(true) * 1000))
|
||||
->setLong("RandomSeed", $seed)
|
||||
->setLong("SizeOnDisk", 0)
|
||||
->setLong("Time", 0)
|
||||
->setString("generatorName", GeneratorManager::getGeneratorName($generator))
|
||||
->setString("generatorOptions", $options["preset"] ?? "")
|
||||
->setString("LevelName", $name)
|
||||
->setTag("GameRules", new CompoundTag());
|
||||
|
||||
$nbt = new BigEndianNbtSerializer();
|
||||
$buffer = $nbt->writeCompressed(new TreeRoot(CompoundTag::create()->setTag("Data", $worldData)));
|
||||
file_put_contents($path . "level.dat", $buffer);
|
||||
}
|
||||
|
||||
protected function load() : CompoundTag{
|
||||
$nbt = new BigEndianNbtSerializer();
|
||||
try{
|
||||
$worldData = $nbt->readCompressed(file_get_contents($this->dataPath))->getTag();
|
||||
}catch(NbtDataException $e){
|
||||
throw new CorruptedWorldException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
if(!$worldData->hasTag("Data", CompoundTag::class)){
|
||||
throw new CorruptedWorldException("Missing 'Data' key or wrong type");
|
||||
}
|
||||
return $worldData->getCompoundTag("Data");
|
||||
}
|
||||
|
||||
protected function fix() : void{
|
||||
if(!$this->compoundTag->hasTag("generatorName", StringTag::class)){
|
||||
$this->compoundTag->setString("generatorName", "default", true);
|
||||
}elseif(($generatorName = self::hackyFixForGeneratorClasspathInLevelDat($this->compoundTag->getString("generatorName"))) !== null){
|
||||
$this->compoundTag->setString("generatorName", $generatorName);
|
||||
}
|
||||
|
||||
if(!$this->compoundTag->hasTag("generatorOptions", StringTag::class)){
|
||||
$this->compoundTag->setString("generatorOptions", "");
|
||||
}
|
||||
}
|
||||
|
||||
public function save() : void{
|
||||
$nbt = new BigEndianNbtSerializer();
|
||||
$buffer = $nbt->writeCompressed(new TreeRoot(CompoundTag::create()->setTag("Data", $this->compoundTag)));
|
||||
file_put_contents($this->dataPath, $buffer);
|
||||
}
|
||||
|
||||
|
||||
public function getDifficulty() : int{
|
||||
return $this->compoundTag->getByte("Difficulty", World::DIFFICULTY_NORMAL);
|
||||
}
|
||||
|
||||
public function setDifficulty(int $difficulty) : void{
|
||||
$this->compoundTag->setByte("Difficulty", $difficulty);
|
||||
}
|
||||
|
||||
public function getRainTime() : int{
|
||||
return $this->compoundTag->getInt("rainTime", 0);
|
||||
}
|
||||
|
||||
public function setRainTime(int $ticks) : void{
|
||||
$this->compoundTag->setInt("rainTime", $ticks);
|
||||
}
|
||||
|
||||
public function getRainLevel() : float{
|
||||
if($this->compoundTag->hasTag("rainLevel", FloatTag::class)){ //PocketMine/MCPE
|
||||
return $this->compoundTag->getFloat("rainLevel");
|
||||
}
|
||||
|
||||
return (float) $this->compoundTag->getByte("raining", 0); //PC vanilla
|
||||
}
|
||||
|
||||
public function setRainLevel(float $level) : void{
|
||||
$this->compoundTag->setFloat("rainLevel", $level); //PocketMine/MCPE
|
||||
$this->compoundTag->setByte("raining", (int) ceil($level)); //PC vanilla
|
||||
}
|
||||
|
||||
public function getLightningTime() : int{
|
||||
return $this->compoundTag->getInt("thunderTime", 0);
|
||||
}
|
||||
|
||||
public function setLightningTime(int $ticks) : void{
|
||||
$this->compoundTag->setInt("thunderTime", $ticks);
|
||||
}
|
||||
|
||||
public function getLightningLevel() : float{
|
||||
if($this->compoundTag->hasTag("lightningLevel", FloatTag::class)){ //PocketMine/MCPE
|
||||
return $this->compoundTag->getFloat("lightningLevel");
|
||||
}
|
||||
|
||||
return (float) $this->compoundTag->getByte("thundering", 0); //PC vanilla
|
||||
}
|
||||
|
||||
public function setLightningLevel(float $level) : void{
|
||||
$this->compoundTag->setFloat("lightningLevel", $level); //PocketMine/MCPE
|
||||
$this->compoundTag->setByte("thundering", (int) ceil($level)); //PC vanilla
|
||||
}
|
||||
}
|
30
src/world/format/io/exception/CorruptedChunkException.php
Normal file
30
src/world/format/io/exception/CorruptedChunkException.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?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\world\format\io\exception;
|
||||
|
||||
use pocketmine\world\format\ChunkException;
|
||||
|
||||
class CorruptedChunkException extends ChunkException{
|
||||
|
||||
}
|
30
src/world/format/io/exception/CorruptedWorldException.php
Normal file
30
src/world/format/io/exception/CorruptedWorldException.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?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\world\format\io\exception;
|
||||
|
||||
use pocketmine\world\WorldException;
|
||||
|
||||
class CorruptedWorldException extends WorldException{
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?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\world\format\io\exception;
|
||||
|
||||
use pocketmine\world\WorldException;
|
||||
|
||||
class UnsupportedWorldFormatException extends WorldException{
|
||||
|
||||
}
|
547
src/world/format/io/leveldb/LevelDB.php
Normal file
547
src/world/format/io/leveldb/LevelDB.php
Normal file
@ -0,0 +1,547 @@
|
||||
<?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\world\format\io\leveldb;
|
||||
|
||||
use pocketmine\block\BlockLegacyIds;
|
||||
use pocketmine\nbt\LittleEndianNbtSerializer;
|
||||
use pocketmine\nbt\NbtDataException;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\TreeRoot;
|
||||
use pocketmine\utils\Binary;
|
||||
use pocketmine\utils\BinaryDataException;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\format\io\BaseWorldProvider;
|
||||
use pocketmine\world\format\io\ChunkUtils;
|
||||
use pocketmine\world\format\io\data\BedrockWorldData;
|
||||
use pocketmine\world\format\io\exception\CorruptedChunkException;
|
||||
use pocketmine\world\format\io\exception\CorruptedWorldException;
|
||||
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
|
||||
use pocketmine\world\format\io\SubChunkConverter;
|
||||
use pocketmine\world\format\io\WorldData;
|
||||
use pocketmine\world\format\io\WritableWorldProvider;
|
||||
use pocketmine\world\format\PalettedBlockArray;
|
||||
use pocketmine\world\format\SubChunk;
|
||||
use pocketmine\world\generator\Generator;
|
||||
use function array_flip;
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
use function chr;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function extension_loaded;
|
||||
use function file_exists;
|
||||
use function file_get_contents;
|
||||
use function is_dir;
|
||||
use function json_decode;
|
||||
use function mkdir;
|
||||
use function ord;
|
||||
use function str_repeat;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use function trim;
|
||||
use function unpack;
|
||||
use const LEVELDB_ZLIB_RAW_COMPRESSION;
|
||||
|
||||
class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
|
||||
|
||||
//According to Tomasso, these aren't supposed to be readable anymore. Thankfully he didn't change the readable ones...
|
||||
protected const TAG_DATA_2D = "\x2d";
|
||||
protected const TAG_DATA_2D_LEGACY = "\x2e";
|
||||
protected const TAG_SUBCHUNK_PREFIX = "\x2f";
|
||||
protected const TAG_LEGACY_TERRAIN = "0";
|
||||
protected const TAG_BLOCK_ENTITY = "1";
|
||||
protected const TAG_ENTITY = "2";
|
||||
protected const TAG_PENDING_TICK = "3";
|
||||
protected const TAG_BLOCK_EXTRA_DATA = "4";
|
||||
protected const TAG_BIOME_STATE = "5";
|
||||
protected const TAG_STATE_FINALISATION = "6";
|
||||
|
||||
protected const TAG_BORDER_BLOCKS = "8";
|
||||
protected const TAG_HARDCODED_SPAWNERS = "9";
|
||||
|
||||
protected const FINALISATION_NEEDS_INSTATICKING = 0;
|
||||
protected const FINALISATION_NEEDS_POPULATION = 1;
|
||||
protected const FINALISATION_DONE = 2;
|
||||
|
||||
protected const TAG_VERSION = "v";
|
||||
|
||||
protected const ENTRY_FLAT_WORLD_LAYERS = "game_flatworldlayers";
|
||||
|
||||
protected const CURRENT_LEVEL_CHUNK_VERSION = 7;
|
||||
protected const CURRENT_LEVEL_SUBCHUNK_VERSION = 8;
|
||||
|
||||
/** @var \LevelDB */
|
||||
protected $db;
|
||||
|
||||
private static function checkForLevelDBExtension() : void{
|
||||
if(!extension_loaded('leveldb')){
|
||||
throw new UnsupportedWorldFormatException("The leveldb PHP extension is required to use this world format");
|
||||
}
|
||||
|
||||
if(!defined('LEVELDB_ZLIB_RAW_COMPRESSION')){
|
||||
throw new UnsupportedWorldFormatException("Given version of php-leveldb doesn't support zlib raw compression");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return \LevelDB
|
||||
* @throws \LevelDBException
|
||||
*/
|
||||
private static function createDB(string $path) : \LevelDB{
|
||||
return new \LevelDB($path . "/db", [
|
||||
"compression" => LEVELDB_ZLIB_RAW_COMPRESSION,
|
||||
"block_size" => 64 * 1024 //64KB, big enough for most chunks
|
||||
]);
|
||||
}
|
||||
|
||||
public function __construct(string $path){
|
||||
self::checkForLevelDBExtension();
|
||||
parent::__construct($path);
|
||||
|
||||
try{
|
||||
$this->db = self::createDB($path);
|
||||
}catch(\LevelDBException $e){
|
||||
//we can't tell the difference between errors caused by bad permissions and actual corruption :(
|
||||
throw new CorruptedWorldException(trim($e->getMessage()), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
protected function loadLevelData() : WorldData{
|
||||
return new BedrockWorldData($this->getPath() . "level.dat");
|
||||
}
|
||||
|
||||
public function getWorldHeight() : int{
|
||||
return 256;
|
||||
}
|
||||
|
||||
public static function isValid(string $path) : bool{
|
||||
return file_exists($path . "/level.dat") and is_dir($path . "/db/");
|
||||
}
|
||||
|
||||
public static function generate(string $path, string $name, int $seed, string $generator, array $options = []) : void{
|
||||
Utils::testValidInstance($generator, Generator::class);
|
||||
self::checkForLevelDBExtension();
|
||||
|
||||
if(!file_exists($path . "/db")){
|
||||
mkdir($path . "/db", 0777, true);
|
||||
}
|
||||
|
||||
BedrockWorldData::generate($path, $name, $seed, $generator, $options);
|
||||
}
|
||||
|
||||
protected function deserializePaletted(BinaryStream $stream) : PalettedBlockArray{
|
||||
static $stringToLegacyId = null;
|
||||
if($stringToLegacyId === null){
|
||||
$stringToLegacyId = json_decode(file_get_contents(\pocketmine\RESOURCE_PATH . 'vanilla/block_id_map.json'), true);
|
||||
}
|
||||
|
||||
$bitsPerBlock = $stream->getByte() >> 1;
|
||||
|
||||
try{
|
||||
$words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
|
||||
}catch(\InvalidArgumentException $e){
|
||||
throw new CorruptedChunkException("Failed to deserialize paletted storage: " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
$nbt = new LittleEndianNbtSerializer();
|
||||
$palette = [];
|
||||
for($i = 0, $paletteSize = $stream->getLInt(); $i < $paletteSize; ++$i){
|
||||
$offset = $stream->getOffset();
|
||||
$tag = $nbt->read($stream->getBuffer(), $offset)->getTag();
|
||||
$stream->setOffset($offset);
|
||||
|
||||
$id = $stringToLegacyId[$tag->getString("name")] ?? BlockLegacyIds::INFO_UPDATE;
|
||||
$data = $tag->getShort("val");
|
||||
$palette[] = ($id << 4) | $data;
|
||||
}
|
||||
|
||||
//TODO: exceptions
|
||||
return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
|
||||
}
|
||||
|
||||
protected static function deserializeExtraDataKey(int $chunkVersion, int $key, ?int &$x, ?int &$y, ?int &$z) : void{
|
||||
if($chunkVersion >= 3){
|
||||
$x = ($key >> 12) & 0xf;
|
||||
$z = ($key >> 8) & 0xf;
|
||||
$y = $key & 0xff;
|
||||
}else{ //pre-1.0, 7 bits were used because the build height limit was lower
|
||||
$x = ($key >> 11) & 0xf;
|
||||
$z = ($key >> 7) & 0xf;
|
||||
$y = $key & 0x7f;
|
||||
}
|
||||
}
|
||||
|
||||
protected function deserializeLegacyExtraData(string $index, int $chunkVersion) : array{
|
||||
if(($extraRawData = $this->db->get($index . self::TAG_BLOCK_EXTRA_DATA)) === false or $extraRawData === ""){
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var PalettedBlockArray[] $extraDataLayers */
|
||||
$extraDataLayers = [];
|
||||
$binaryStream = new BinaryStream($extraRawData);
|
||||
$count = $binaryStream->getLInt();
|
||||
for($i = 0; $i < $count; ++$i){
|
||||
$key = $binaryStream->getLInt();
|
||||
$value = $binaryStream->getLShort();
|
||||
|
||||
self::deserializeExtraDataKey($chunkVersion, $key, $x, $fullY, $z);
|
||||
|
||||
$ySub = ($fullY >> 4) & 0xf;
|
||||
$y = $key & 0xf;
|
||||
|
||||
$blockId = $value & 0xff;
|
||||
$blockData = ($value >> 8) & 0xf;
|
||||
if(!isset($extraDataLayers[$ySub])){
|
||||
$extraDataLayers[$ySub] = new PalettedBlockArray(BlockLegacyIds::AIR << 4);
|
||||
}
|
||||
$extraDataLayers[$ySub]->set($x, $y, $z, ($blockId << 4) | $blockData);
|
||||
}
|
||||
|
||||
return $extraDataLayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $chunkX
|
||||
* @param int $chunkZ
|
||||
*
|
||||
* @return Chunk|null
|
||||
* @throws CorruptedChunkException
|
||||
*/
|
||||
protected function readChunk(int $chunkX, int $chunkZ) : ?Chunk{
|
||||
$index = LevelDB::chunkIndex($chunkX, $chunkZ);
|
||||
|
||||
if(!$this->chunkExists($chunkX, $chunkZ)){
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var SubChunk[] $subChunks */
|
||||
$subChunks = [];
|
||||
|
||||
/** @var string $biomeIds */
|
||||
$biomeIds = "";
|
||||
|
||||
$chunkVersion = ord($this->db->get($index . self::TAG_VERSION));
|
||||
$hasBeenUpgraded = $chunkVersion < self::CURRENT_LEVEL_CHUNK_VERSION;
|
||||
|
||||
$binaryStream = new BinaryStream();
|
||||
|
||||
switch($chunkVersion){
|
||||
case 15: //MCPE 1.12.0.4 beta (???)
|
||||
case 14: //MCPE 1.11.1.2 (???)
|
||||
case 13: //MCPE 1.11.0.4 beta (???)
|
||||
case 12: //MCPE 1.11.0.3 beta (???)
|
||||
case 11: //MCPE 1.11.0.1 beta (???)
|
||||
case 10: //MCPE 1.9 (???)
|
||||
case 9: //MCPE 1.8 (???)
|
||||
case 7: //MCPE 1.2 (???)
|
||||
case 6: //MCPE 1.2.0.2 beta (???)
|
||||
case 4: //MCPE 1.1
|
||||
//TODO: check beds
|
||||
case 3: //MCPE 1.0
|
||||
/** @var PalettedBlockArray[] $convertedLegacyExtraData */
|
||||
$convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion);
|
||||
|
||||
for($y = 0; $y < Chunk::MAX_SUBCHUNKS; ++$y){
|
||||
if(($data = $this->db->get($index . self::TAG_SUBCHUNK_PREFIX . chr($y))) === false){
|
||||
continue;
|
||||
}
|
||||
|
||||
$binaryStream->setBuffer($data);
|
||||
if($binaryStream->feof()){
|
||||
throw new CorruptedChunkException("Unexpected empty data for subchunk $y");
|
||||
}
|
||||
$subChunkVersion = $binaryStream->getByte();
|
||||
if($subChunkVersion < self::CURRENT_LEVEL_SUBCHUNK_VERSION){
|
||||
$hasBeenUpgraded = true;
|
||||
}
|
||||
|
||||
switch($subChunkVersion){
|
||||
case 0:
|
||||
case 2: //these are all identical to version 0, but vanilla respects these so we should also
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 6:
|
||||
case 7:
|
||||
try{
|
||||
$blocks = $binaryStream->get(4096);
|
||||
$blockData = $binaryStream->get(2048);
|
||||
|
||||
if($chunkVersion < 4){
|
||||
$binaryStream->get(4096); //legacy light info, discard it
|
||||
$hasBeenUpgraded = true;
|
||||
}
|
||||
}catch(BinaryDataException $e){
|
||||
throw new CorruptedChunkException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
$storages = [SubChunkConverter::convertSubChunkXZY($blocks, $blockData)];
|
||||
if(isset($convertedLegacyExtraData[$y])){
|
||||
$storages[] = $convertedLegacyExtraData[$y];
|
||||
}
|
||||
|
||||
$subChunks[$y] = new SubChunk(BlockLegacyIds::AIR << 4, $storages);
|
||||
break;
|
||||
case 1: //paletted v1, has a single blockstorage
|
||||
$storages = [$this->deserializePaletted($binaryStream)];
|
||||
if(isset($convertedLegacyExtraData[$y])){
|
||||
$storages[] = $convertedLegacyExtraData[$y];
|
||||
}
|
||||
$subChunks[$y] = new SubChunk(BlockLegacyIds::AIR << 4, $storages);
|
||||
break;
|
||||
case 8:
|
||||
//legacy extradata layers intentionally ignored because they aren't supposed to exist in v8
|
||||
$storageCount = $binaryStream->getByte();
|
||||
if($storageCount > 0){
|
||||
$storages = [];
|
||||
|
||||
for($k = 0; $k < $storageCount; ++$k){
|
||||
$storages[] = $this->deserializePaletted($binaryStream);
|
||||
}
|
||||
$subChunks[$y] = new SubChunk(BlockLegacyIds::AIR << 4, $storages);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
//TODO: set chunks read-only so the version on disk doesn't get overwritten
|
||||
throw new CorruptedChunkException("don't know how to decode LevelDB subchunk format version $subChunkVersion");
|
||||
}
|
||||
}
|
||||
|
||||
if(($maps2d = $this->db->get($index . self::TAG_DATA_2D)) !== false){
|
||||
$binaryStream->setBuffer($maps2d);
|
||||
|
||||
try{
|
||||
$binaryStream->get(512); //heightmap, discard it
|
||||
$biomeIds = $binaryStream->get(256);
|
||||
}catch(BinaryDataException $e){
|
||||
throw new CorruptedChunkException($e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 2: // < MCPE 1.0
|
||||
case 1:
|
||||
case 0: //MCPE 0.9.0.1 beta (first version)
|
||||
/** @var PalettedBlockArray[] $extraDataLayers */
|
||||
$convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion);
|
||||
|
||||
$legacyTerrain = $this->db->get($index . self::TAG_LEGACY_TERRAIN);
|
||||
if($legacyTerrain === false){
|
||||
throw new CorruptedChunkException("Missing expected LEGACY_TERRAIN tag for format version $chunkVersion");
|
||||
}
|
||||
$binaryStream->setBuffer($legacyTerrain);
|
||||
try{
|
||||
$fullIds = $binaryStream->get(32768);
|
||||
$fullData = $binaryStream->get(16384);
|
||||
$binaryStream->get(32768); //legacy light info, discard it
|
||||
}catch(BinaryDataException $e){
|
||||
throw new CorruptedChunkException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
for($yy = 0; $yy < 8; ++$yy){
|
||||
$storages = [SubChunkConverter::convertSubChunkFromLegacyColumn($fullIds, $fullData, $yy)];
|
||||
if(isset($convertedLegacyExtraData[$yy])){
|
||||
$storages[] = $convertedLegacyExtraData[$yy];
|
||||
}
|
||||
$subChunks[$yy] = new SubChunk(BlockLegacyIds::AIR << 4, $storages);
|
||||
}
|
||||
|
||||
try{
|
||||
$binaryStream->get(256); //heightmap, discard it
|
||||
$biomeIds = ChunkUtils::convertBiomeColors(array_values(unpack("N*", $binaryStream->get(1024))));
|
||||
}catch(BinaryDataException $e){
|
||||
throw new CorruptedChunkException($e->getMessage(), 0, $e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
//TODO: set chunks read-only so the version on disk doesn't get overwritten
|
||||
throw new CorruptedChunkException("don't know how to decode chunk format version $chunkVersion");
|
||||
}
|
||||
|
||||
$nbt = new LittleEndianNbtSerializer();
|
||||
|
||||
/** @var CompoundTag[] $entities */
|
||||
$entities = [];
|
||||
if(($entityData = $this->db->get($index . self::TAG_ENTITY)) !== false and $entityData !== ""){
|
||||
try{
|
||||
$entities = array_map(function(TreeRoot $root) : CompoundTag{ return $root->getTag(); }, $nbt->readMultiple($entityData));
|
||||
}catch(NbtDataException $e){
|
||||
throw new CorruptedChunkException($e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var CompoundTag[] $tiles */
|
||||
$tiles = [];
|
||||
if(($tileData = $this->db->get($index . self::TAG_BLOCK_ENTITY)) !== false and $tileData !== ""){
|
||||
try{
|
||||
$tiles = array_map(function(TreeRoot $root) : CompoundTag{ return $root->getTag(); }, $nbt->readMultiple($tileData));
|
||||
}catch(NbtDataException $e){
|
||||
throw new CorruptedChunkException($e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
$chunk = new Chunk(
|
||||
$chunkX,
|
||||
$chunkZ,
|
||||
$subChunks,
|
||||
$entities,
|
||||
$tiles,
|
||||
$biomeIds
|
||||
);
|
||||
|
||||
//TODO: tile ticks, biome states (?)
|
||||
|
||||
$chunk->setGenerated();
|
||||
$chunk->setPopulated();
|
||||
$chunk->setChanged($hasBeenUpgraded); //trigger rewriting chunk to disk if it was converted from an older format
|
||||
|
||||
return $chunk;
|
||||
}
|
||||
|
||||
protected function writeChunk(Chunk $chunk) : void{
|
||||
static $idMap = null;
|
||||
if($idMap === null){
|
||||
$idMap = array_flip(json_decode(file_get_contents(\pocketmine\RESOURCE_PATH . 'vanilla/block_id_map.json'), true));
|
||||
}
|
||||
$index = LevelDB::chunkIndex($chunk->getX(), $chunk->getZ());
|
||||
|
||||
$write = new \LevelDBWriteBatch();
|
||||
$write->put($index . self::TAG_VERSION, chr(self::CURRENT_LEVEL_CHUNK_VERSION));
|
||||
|
||||
$subChunks = $chunk->getSubChunks();
|
||||
foreach($subChunks as $y => $subChunk){
|
||||
$key = $index . self::TAG_SUBCHUNK_PREFIX . chr($y);
|
||||
if($subChunk->isEmpty(false)){ //MCPE doesn't save light anymore as of 1.1
|
||||
$write->delete($key);
|
||||
}else{
|
||||
$subStream = new BinaryStream();
|
||||
$subStream->putByte(self::CURRENT_LEVEL_SUBCHUNK_VERSION);
|
||||
|
||||
$layers = $subChunk->getBlockLayers();
|
||||
$subStream->putByte(count($layers));
|
||||
foreach($layers as $blocks){
|
||||
$subStream->putByte($blocks->getBitsPerBlock() << 1);
|
||||
$subStream->put($blocks->getWordArray());
|
||||
|
||||
$palette = $blocks->getPalette();
|
||||
$subStream->putLInt(count($palette));
|
||||
$tags = [];
|
||||
foreach($palette as $p){
|
||||
$tags[] = new TreeRoot(CompoundTag::create()
|
||||
->setString("name", $idMap[$p >> 4] ?? "minecraft:info_update")
|
||||
->setInt("oldid", $p >> 4) //PM only (debugging), vanilla doesn't have this
|
||||
->setShort("val", $p & 0xf));
|
||||
}
|
||||
|
||||
$subStream->put((new LittleEndianNbtSerializer())->writeMultiple($tags));
|
||||
}
|
||||
|
||||
$write->put($key, $subStream->getBuffer());
|
||||
}
|
||||
}
|
||||
|
||||
$write->put($index . self::TAG_DATA_2D, str_repeat("\x00", 512) . $chunk->getBiomeIdArray());
|
||||
|
||||
//TODO: use this properly
|
||||
$write->put($index . self::TAG_STATE_FINALISATION, chr(self::FINALISATION_DONE));
|
||||
|
||||
$this->writeTags($chunk->getNBTtiles(), $index . self::TAG_BLOCK_ENTITY, $write);
|
||||
$this->writeTags($chunk->getNBTentities(), $index . self::TAG_ENTITY, $write);
|
||||
|
||||
$write->delete($index . self::TAG_DATA_2D_LEGACY);
|
||||
$write->delete($index . self::TAG_LEGACY_TERRAIN);
|
||||
|
||||
$this->db->write($write);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CompoundTag[] $targets
|
||||
* @param string $index
|
||||
* @param \LevelDBWriteBatch $write
|
||||
*/
|
||||
private function writeTags(array $targets, string $index, \LevelDBWriteBatch $write) : void{
|
||||
if(!empty($targets)){
|
||||
$nbt = new LittleEndianNbtSerializer();
|
||||
$write->put($index, $nbt->writeMultiple(array_map(function(CompoundTag $tag) : TreeRoot{ return new TreeRoot($tag); }, $targets)));
|
||||
}else{
|
||||
$write->delete($index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \LevelDB
|
||||
*/
|
||||
public function getDatabase() : \LevelDB{
|
||||
return $this->db;
|
||||
}
|
||||
|
||||
public static function chunkIndex(int $chunkX, int $chunkZ) : string{
|
||||
return Binary::writeLInt($chunkX) . Binary::writeLInt($chunkZ);
|
||||
}
|
||||
|
||||
private function chunkExists(int $chunkX, int $chunkZ) : bool{
|
||||
return $this->db->get(LevelDB::chunkIndex($chunkX, $chunkZ) . self::TAG_VERSION) !== false;
|
||||
}
|
||||
|
||||
public function doGarbageCollection() : void{
|
||||
|
||||
}
|
||||
|
||||
public function close() : void{
|
||||
$this->db->close();
|
||||
}
|
||||
|
||||
public function getAllChunks(bool $skipCorrupted = false, ?\Logger $logger = null) : \Generator{
|
||||
foreach($this->db->getIterator() as $key => $_){
|
||||
if(strlen($key) === 9 and substr($key, -1) === self::TAG_VERSION){
|
||||
$chunkX = Binary::readLInt(substr($key, 0, 4));
|
||||
$chunkZ = Binary::readLInt(substr($key, 4, 4));
|
||||
try{
|
||||
if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== null){
|
||||
yield $chunk;
|
||||
}
|
||||
}catch(CorruptedChunkException $e){
|
||||
if(!$skipCorrupted){
|
||||
throw $e;
|
||||
}
|
||||
if($logger !== null){
|
||||
$logger->error("Skipped corrupted chunk $chunkX $chunkZ (" . $e->getMessage() . ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function calculateChunkCount() : int{
|
||||
$count = 0;
|
||||
foreach($this->db->getIterator() as $key => $_){
|
||||
if(strlen($key) === 9 and substr($key, -1) === self::TAG_VERSION){
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
}
|
55
src/world/format/io/region/Anvil.php
Normal file
55
src/world/format/io/region/Anvil.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?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\world\format\io\region;
|
||||
|
||||
use pocketmine\block\BlockLegacyIds;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\world\format\io\SubChunkConverter;
|
||||
use pocketmine\world\format\SubChunk;
|
||||
|
||||
class Anvil extends RegionWorldProvider{
|
||||
use LegacyAnvilChunkTrait;
|
||||
|
||||
protected function serializeSubChunk(SubChunk $subChunk) : CompoundTag{
|
||||
throw new \RuntimeException("Unsupported");
|
||||
}
|
||||
|
||||
protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk{
|
||||
return new SubChunk(BlockLegacyIds::AIR << 4, [SubChunkConverter::convertSubChunkYZX($subChunk->getByteArray("Blocks"), $subChunk->getByteArray("Data"))]);
|
||||
//ignore legacy light information
|
||||
}
|
||||
|
||||
protected static function getRegionFileExtension() : string{
|
||||
return "mca";
|
||||
}
|
||||
|
||||
protected static function getPcWorldFormatVersion() : int{
|
||||
return 19133;
|
||||
}
|
||||
|
||||
public function getWorldHeight() : int{
|
||||
//TODO: add world height options
|
||||
return 256;
|
||||
}
|
||||
}
|
30
src/world/format/io/region/CorruptedRegionException.php
Normal file
30
src/world/format/io/region/CorruptedRegionException.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?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\world\format\io\region;
|
||||
|
||||
|
||||
class CorruptedRegionException extends RegionException{
|
||||
|
||||
}
|
99
src/world/format/io/region/LegacyAnvilChunkTrait.php
Normal file
99
src/world/format/io/region/LegacyAnvilChunkTrait.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?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\world\format\io\region;
|
||||
|
||||
use pocketmine\nbt\BigEndianNbtSerializer;
|
||||
use pocketmine\nbt\NbtDataException;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\IntArrayTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\format\io\ChunkUtils;
|
||||
use pocketmine\world\format\io\exception\CorruptedChunkException;
|
||||
use pocketmine\world\format\SubChunk;
|
||||
|
||||
/**
|
||||
* Trait containing I/O methods for handling legacy Anvil-style chunks.
|
||||
*
|
||||
* Motivation: In the future PMAnvil will become a legacy read-only format, but Anvil will continue to exist for the sake
|
||||
* of handling worlds in the PC 1.13 format. Thus, we don't want PMAnvil getting accidentally influenced by changes
|
||||
* happening to the underlying Anvil, because it only uses the legacy part.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait LegacyAnvilChunkTrait{
|
||||
|
||||
protected function serializeChunk(Chunk $chunk) : string{
|
||||
throw new \RuntimeException("Unsupported");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
*
|
||||
* @return Chunk
|
||||
* @throws CorruptedChunkException
|
||||
*/
|
||||
protected function deserializeChunk(string $data) : Chunk{
|
||||
$nbt = new BigEndianNbtSerializer();
|
||||
try{
|
||||
$chunk = $nbt->readCompressed($data)->getTag();
|
||||
}catch(NbtDataException $e){
|
||||
throw new CorruptedChunkException($e->getMessage(), 0, $e);
|
||||
}
|
||||
if(!$chunk->hasTag("Level")){
|
||||
throw new CorruptedChunkException("'Level' key is missing from chunk NBT");
|
||||
}
|
||||
|
||||
$chunk = $chunk->getCompoundTag("Level");
|
||||
|
||||
$subChunks = [];
|
||||
$subChunksTag = $chunk->getListTag("Sections") ?? [];
|
||||
foreach($subChunksTag as $subChunk){
|
||||
if($subChunk instanceof CompoundTag){
|
||||
$subChunks[$subChunk->getByte("Y")] = $this->deserializeSubChunk($subChunk);
|
||||
}
|
||||
}
|
||||
|
||||
if($chunk->hasTag("BiomeColors", IntArrayTag::class)){
|
||||
$biomeIds = ChunkUtils::convertBiomeColors($chunk->getIntArray("BiomeColors")); //Convert back to original format
|
||||
}else{
|
||||
$biomeIds = $chunk->getByteArray("Biomes", "", true);
|
||||
}
|
||||
|
||||
$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
|
||||
);
|
||||
$result->setPopulated($chunk->getByte("TerrainPopulated", 0) !== 0);
|
||||
$result->setGenerated();
|
||||
return $result;
|
||||
}
|
||||
|
||||
abstract protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk;
|
||||
|
||||
}
|
111
src/world/format/io/region/McRegion.php
Normal file
111
src/world/format/io/region/McRegion.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?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\world\format\io\region;
|
||||
|
||||
use pocketmine\block\BlockLegacyIds;
|
||||
use pocketmine\nbt\BigEndianNbtSerializer;
|
||||
use pocketmine\nbt\NbtDataException;
|
||||
use pocketmine\nbt\tag\ByteArrayTag;
|
||||
use pocketmine\nbt\tag\IntArrayTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\format\io\ChunkUtils;
|
||||
use pocketmine\world\format\io\exception\CorruptedChunkException;
|
||||
use pocketmine\world\format\io\SubChunkConverter;
|
||||
use pocketmine\world\format\SubChunk;
|
||||
use function str_repeat;
|
||||
|
||||
class McRegion extends RegionWorldProvider{
|
||||
|
||||
/**
|
||||
* @param Chunk $chunk
|
||||
*
|
||||
* @return string
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function serializeChunk(Chunk $chunk) : string{
|
||||
throw new \RuntimeException("Unsupported");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
*
|
||||
* @return Chunk
|
||||
* @throws CorruptedChunkException
|
||||
*/
|
||||
protected function deserializeChunk(string $data) : Chunk{
|
||||
$nbt = new BigEndianNbtSerializer();
|
||||
try{
|
||||
$chunk = $nbt->readCompressed($data)->getTag();
|
||||
}catch(NbtDataException $e){
|
||||
throw new CorruptedChunkException($e->getMessage(), 0, $e);
|
||||
}
|
||||
if(!$chunk->hasTag("Level")){
|
||||
throw new CorruptedChunkException("'Level' key is missing from chunk NBT");
|
||||
}
|
||||
|
||||
$chunk = $chunk->getCompoundTag("Level");
|
||||
|
||||
$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);
|
||||
|
||||
for($y = 0; $y < 8; ++$y){
|
||||
$subChunks[$y] = new SubChunk(BlockLegacyIds::AIR << 4, [SubChunkConverter::convertSubChunkFromLegacyColumn($fullIds, $fullData, $y)]);
|
||||
}
|
||||
|
||||
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 = "";
|
||||
}
|
||||
|
||||
$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
|
||||
);
|
||||
$result->setPopulated($chunk->getByte("TerrainPopulated", 0) !== 0);
|
||||
$result->setGenerated(true);
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected static function getRegionFileExtension() : string{
|
||||
return "mcr";
|
||||
}
|
||||
|
||||
protected static function getPcWorldFormatVersion() : int{
|
||||
return 19132;
|
||||
}
|
||||
|
||||
public function getWorldHeight() : int{
|
||||
//TODO: add world height options
|
||||
return 128;
|
||||
}
|
||||
}
|
53
src/world/format/io/region/PMAnvil.php
Normal file
53
src/world/format/io/region/PMAnvil.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?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\world\format\io\region;
|
||||
|
||||
use pocketmine\block\BlockLegacyIds;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\world\format\io\SubChunkConverter;
|
||||
use pocketmine\world\format\SubChunk;
|
||||
|
||||
/**
|
||||
* 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 RegionWorldProvider{
|
||||
use LegacyAnvilChunkTrait;
|
||||
|
||||
protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk{
|
||||
return new SubChunk(BlockLegacyIds::AIR << 4, [SubChunkConverter::convertSubChunkXZY($subChunk->getByteArray("Blocks"), $subChunk->getByteArray("Data"))]);
|
||||
}
|
||||
|
||||
protected static function getRegionFileExtension() : string{
|
||||
return "mcapm";
|
||||
}
|
||||
|
||||
protected static function getPcWorldFormatVersion() : int{
|
||||
return -1; //Not a PC format, only PocketMine-MP
|
||||
}
|
||||
|
||||
public function getWorldHeight() : int{
|
||||
return 256;
|
||||
}
|
||||
}
|
30
src/world/format/io/region/RegionException.php
Normal file
30
src/world/format/io/region/RegionException.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?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\world\format\io\region;
|
||||
|
||||
|
||||
class RegionException extends \RuntimeException{
|
||||
|
||||
}
|
347
src/world/format/io/region/RegionLoader.php
Normal file
347
src/world/format/io/region/RegionLoader.php
Normal file
@ -0,0 +1,347 @@
|
||||
<?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\world\format\io\region;
|
||||
|
||||
use pocketmine\utils\Binary;
|
||||
use pocketmine\world\format\ChunkException;
|
||||
use pocketmine\world\format\io\exception\CorruptedChunkException;
|
||||
use function ceil;
|
||||
use function chr;
|
||||
use function fclose;
|
||||
use function feof;
|
||||
use function file_exists;
|
||||
use function filesize;
|
||||
use function fopen;
|
||||
use function fread;
|
||||
use function fseek;
|
||||
use function ftruncate;
|
||||
use function fwrite;
|
||||
use function is_resource;
|
||||
use function max;
|
||||
use function ord;
|
||||
use function str_pad;
|
||||
use function stream_set_read_buffer;
|
||||
use function stream_set_write_buffer;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use function time;
|
||||
use function touch;
|
||||
use function unpack;
|
||||
use const STR_PAD_RIGHT;
|
||||
|
||||
class RegionLoader{
|
||||
public const COMPRESSION_GZIP = 1;
|
||||
public const COMPRESSION_ZLIB = 2;
|
||||
|
||||
private const MAX_SECTOR_LENGTH = 255 << 12; //255 sectors (~0.996 MiB)
|
||||
private const REGION_HEADER_LENGTH = 8192; //4096 location table + 4096 timestamps
|
||||
|
||||
private const FIRST_SECTOR = 2; //location table occupies 0 and 1
|
||||
|
||||
public static $COMPRESSION_LEVEL = 7;
|
||||
|
||||
/** @var string */
|
||||
protected $filePath;
|
||||
/** @var resource */
|
||||
protected $filePointer;
|
||||
/** @var int */
|
||||
protected $nextSector = self::FIRST_SECTOR;
|
||||
/** @var RegionLocationTableEntry[] */
|
||||
protected $locationTable = [];
|
||||
/** @var int */
|
||||
public $lastUsed = 0;
|
||||
|
||||
public function __construct(string $filePath){
|
||||
$this->filePath = $filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CorruptedRegionException
|
||||
*/
|
||||
public function open() : void{
|
||||
$exists = file_exists($this->filePath);
|
||||
if(!$exists){
|
||||
touch($this->filePath);
|
||||
}elseif(filesize($this->filePath) % 4096 !== 0){
|
||||
throw new CorruptedRegionException("Region file should be padded to a multiple of 4KiB");
|
||||
}
|
||||
|
||||
$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)){
|
||||
fclose($this->filePointer);
|
||||
}
|
||||
}
|
||||
|
||||
protected function isChunkGenerated(int $index) : bool{
|
||||
return !$this->locationTable[$index]->isNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $x
|
||||
* @param int $z
|
||||
*
|
||||
* @return null|string
|
||||
* @throws \InvalidArgumentException if invalid coordinates are given
|
||||
* @throws CorruptedChunkException if chunk corruption is detected
|
||||
*/
|
||||
public function readChunk(int $x, int $z) : ?string{
|
||||
$index = self::getChunkOffset($x, $z);
|
||||
|
||||
$this->lastUsed = time();
|
||||
|
||||
if(!$this->isChunkGenerated($index)){
|
||||
return null;
|
||||
}
|
||||
|
||||
fseek($this->filePointer, $this->locationTable[$index]->getFirstSector() << 12);
|
||||
|
||||
$prefix = fread($this->filePointer, 4);
|
||||
if($prefix === false or strlen($prefix) !== 4){
|
||||
throw new CorruptedChunkException("Corrupted chunk header detected (unexpected end of file reading length prefix)");
|
||||
}
|
||||
$length = Binary::readInt($prefix);
|
||||
|
||||
if($length <= 0){ //TODO: if we reached here, the locationTable probably needs updating
|
||||
return null;
|
||||
}
|
||||
if($length > self::MAX_SECTOR_LENGTH){ //corrupted
|
||||
throw new CorruptedChunkException("Length for chunk x=$x,z=$z ($length) is larger than maximum " . self::MAX_SECTOR_LENGTH);
|
||||
}
|
||||
|
||||
if($length > ($this->locationTable[$index]->getSectorCount() << 12)){ //Invalid chunk, bigger than defined number of sectors
|
||||
\GlobalLogger::get()->error("Chunk x=$x,z=$z length mismatch (expected " . ($this->locationTable[$index]->getSectorCount() << 12) . " sectors, got $length sectors)");
|
||||
$old = $this->locationTable[$index];
|
||||
$this->locationTable[$index] = new RegionLocationTableEntry($old->getFirstSector(), $length >> 12, time());
|
||||
$this->writeLocationIndex($index);
|
||||
}
|
||||
|
||||
$chunkData = fread($this->filePointer, $length);
|
||||
if($chunkData === false or strlen($chunkData) !== $length){
|
||||
throw new CorruptedChunkException("Corrupted chunk detected (unexpected end of file reading chunk data)");
|
||||
}
|
||||
|
||||
$compression = ord($chunkData[0]);
|
||||
if($compression !== self::COMPRESSION_ZLIB and $compression !== self::COMPRESSION_GZIP){
|
||||
throw new CorruptedChunkException("Invalid compression type (got $compression, expected " . self::COMPRESSION_ZLIB . " or " . self::COMPRESSION_GZIP . ")");
|
||||
}
|
||||
|
||||
return substr($chunkData, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $x
|
||||
* @param int $z
|
||||
*
|
||||
* @return bool
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function chunkExists(int $x, int $z) : bool{
|
||||
return $this->isChunkGenerated(self::getChunkOffset($x, $z));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $x
|
||||
* @param int $z
|
||||
* @param string $chunkData
|
||||
*
|
||||
* @throws ChunkException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function writeChunk(int $x, int $z, string $chunkData) : void{
|
||||
$this->lastUsed = time();
|
||||
|
||||
$length = strlen($chunkData) + 1;
|
||||
if($length + 4 > self::MAX_SECTOR_LENGTH){
|
||||
throw new ChunkException("Chunk is too big! " . ($length + 4) . " > " . self::MAX_SECTOR_LENGTH);
|
||||
}
|
||||
|
||||
$newSize = (int) ceil(($length + 4) / 4096);
|
||||
$index = self::getChunkOffset($x, $z);
|
||||
$offset = $this->locationTable[$index]->getFirstSector();
|
||||
|
||||
if($this->locationTable[$index]->getSectorCount() < $newSize){
|
||||
$offset = $this->nextSector;
|
||||
}
|
||||
|
||||
$this->locationTable[$index] = new RegionLocationTableEntry($offset, $newSize, time());
|
||||
$this->bumpNextFreeSector($this->locationTable[$index]);
|
||||
|
||||
fseek($this->filePointer, $offset << 12);
|
||||
fwrite($this->filePointer, str_pad(Binary::writeInt($length) . chr(self::COMPRESSION_ZLIB) . $chunkData, $newSize << 12, "\x00", STR_PAD_RIGHT));
|
||||
|
||||
$this->writeLocationIndex($index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $x
|
||||
* @param int $z
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function removeChunk(int $x, int $z) : void{
|
||||
$index = self::getChunkOffset($x, $z);
|
||||
$this->locationTable[$index] = new RegionLocationTableEntry(0, 0, 0);
|
||||
$this->writeLocationIndex($index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $x
|
||||
* @param int $z
|
||||
*
|
||||
* @return int
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected static function getChunkOffset(int $x, int $z) : int{
|
||||
if($x < 0 or $x > 31 or $z < 0 or $z > 31){
|
||||
throw new \InvalidArgumentException("Invalid chunk position in region, expected x/z in range 0-31, got x=$x, z=$z");
|
||||
}
|
||||
return $x | ($z << 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $offset
|
||||
* @param int &$x
|
||||
* @param int &$z
|
||||
*/
|
||||
protected static function getChunkCoords(int $offset, ?int &$x, ?int &$z) : void{
|
||||
$x = $offset & 0x1f;
|
||||
$z = ($offset >> 5) & 0x1f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the file
|
||||
*/
|
||||
public function close() : void{
|
||||
if(is_resource($this->filePointer)){
|
||||
fclose($this->filePointer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CorruptedRegionException
|
||||
*/
|
||||
protected function loadLocationTable() : void{
|
||||
fseek($this->filePointer, 0);
|
||||
|
||||
$headerRaw = fread($this->filePointer, self::REGION_HEADER_LENGTH);
|
||||
if(($len = strlen($headerRaw)) !== self::REGION_HEADER_LENGTH){
|
||||
throw new CorruptedRegionException("Invalid region file header, expected " . self::REGION_HEADER_LENGTH . " bytes, got " . $len . " bytes");
|
||||
}
|
||||
|
||||
$data = unpack("N*", $headerRaw);
|
||||
|
||||
for($i = 0; $i < 1024; ++$i){
|
||||
$index = $data[$i + 1];
|
||||
$offset = $index >> 8;
|
||||
$timestamp = $data[$i + 1025];
|
||||
|
||||
if($offset === 0){
|
||||
$this->locationTable[$i] = new RegionLocationTableEntry(0, 0, 0);
|
||||
}else{
|
||||
$this->locationTable[$i] = new RegionLocationTableEntry($offset, $index & 0xff, $timestamp);
|
||||
$this->bumpNextFreeSector($this->locationTable[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkLocationTableValidity();
|
||||
|
||||
fseek($this->filePointer, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CorruptedRegionException
|
||||
*/
|
||||
private function checkLocationTableValidity() : void{
|
||||
/** @var int[] $usedOffsets */
|
||||
$usedOffsets = [];
|
||||
|
||||
for($i = 0; $i < 1024; ++$i){
|
||||
$entry = $this->locationTable[$i];
|
||||
if($entry->isNull()){
|
||||
continue;
|
||||
}
|
||||
|
||||
self::getChunkCoords($i, $x, $z);
|
||||
$offset = $entry->getFirstSector();
|
||||
$fileOffset = $offset << 12;
|
||||
|
||||
//TODO: more validity checks
|
||||
|
||||
fseek($this->filePointer, $fileOffset);
|
||||
if(feof($this->filePointer)){
|
||||
throw new CorruptedRegionException("Region file location offset x=$x,z=$z points to invalid file location $fileOffset");
|
||||
}
|
||||
if(isset($usedOffsets[$offset])){
|
||||
self::getChunkCoords($usedOffsets[$offset], $existingX, $existingZ);
|
||||
throw new CorruptedRegionException("Found two chunk offsets (chunk1: x=$existingX,z=$existingZ, chunk2: x=$x,z=$z) pointing to the file location $fileOffset");
|
||||
}
|
||||
$usedOffsets[$offset] = $i;
|
||||
}
|
||||
}
|
||||
|
||||
protected function writeLocationIndex(int $index) : void{
|
||||
fseek($this->filePointer, $index << 2);
|
||||
fwrite($this->filePointer, Binary::writeInt(($this->locationTable[$index]->getFirstSector() << 8) | $this->locationTable[$index]->getSectorCount()), 4);
|
||||
fseek($this->filePointer, 4096 + ($index << 2));
|
||||
fwrite($this->filePointer, Binary::writeInt($this->locationTable[$index]->getTimestamp()), 4);
|
||||
}
|
||||
|
||||
protected function createBlank() : void{
|
||||
fseek($this->filePointer, 0);
|
||||
ftruncate($this->filePointer, 8192); // this fills the file with the null byte
|
||||
for($i = 0; $i < 1024; ++$i){
|
||||
$this->locationTable[$i] = new RegionLocationTableEntry(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private function bumpNextFreeSector(RegionLocationTableEntry $entry) : void{
|
||||
$this->nextSector = max($this->nextSector, $entry->getLastSector()) + 1;
|
||||
}
|
||||
|
||||
public function getFilePath() : string{
|
||||
return $this->filePath;
|
||||
}
|
||||
|
||||
public function calculateChunkCount() : int{
|
||||
$count = 0;
|
||||
for($i = 0; $i < 1024; ++$i){
|
||||
if($this->isChunkGenerated($i)){
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
}
|
98
src/world/format/io/region/RegionLocationTableEntry.php
Normal file
98
src/world/format/io/region/RegionLocationTableEntry.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?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\world\format\io\region;
|
||||
|
||||
use function range;
|
||||
|
||||
class RegionLocationTableEntry{
|
||||
|
||||
/** @var int */
|
||||
private $firstSector;
|
||||
/** @var int */
|
||||
private $sectorCount;
|
||||
/** @var int */
|
||||
private $timestamp;
|
||||
|
||||
/**
|
||||
* @param int $firstSector
|
||||
* @param int $sectorCount
|
||||
* @param int $timestamp
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(int $firstSector, int $sectorCount, int $timestamp){
|
||||
if($firstSector < 0){
|
||||
throw new \InvalidArgumentException("Start sector must be positive, got $firstSector");
|
||||
}
|
||||
$this->firstSector = $firstSector;
|
||||
if($sectorCount < 0 or $sectorCount > 255){
|
||||
throw new \InvalidArgumentException("Sector count must be in range 0...255, got $sectorCount");
|
||||
}
|
||||
$this->sectorCount = $sectorCount;
|
||||
$this->timestamp = $timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getFirstSector() : int{
|
||||
return $this->firstSector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLastSector() : int{
|
||||
return $this->firstSector + $this->sectorCount - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of sector offsets reserved by this chunk.
|
||||
* @return int[]
|
||||
*/
|
||||
public function getUsedSectors() : array{
|
||||
return range($this->getFirstSector(), $this->getLastSector());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getSectorCount() : int{
|
||||
return $this->sectorCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getTimestamp() : int{
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isNull() : bool{
|
||||
return $this->firstSector === 0 or $this->sectorCount === 0;
|
||||
}
|
||||
}
|
274
src/world/format/io/region/RegionWorldProvider.php
Normal file
274
src/world/format/io/region/RegionWorldProvider.php
Normal file
@ -0,0 +1,274 @@
|
||||
<?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\world\format\io\region;
|
||||
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\format\io\BaseWorldProvider;
|
||||
use pocketmine\world\format\io\data\JavaWorldData;
|
||||
use pocketmine\world\format\io\exception\CorruptedChunkException;
|
||||
use pocketmine\world\format\io\WorldData;
|
||||
use pocketmine\world\generator\Generator;
|
||||
use pocketmine\world\World;
|
||||
use function assert;
|
||||
use function file_exists;
|
||||
use function is_dir;
|
||||
use function is_int;
|
||||
use function mkdir;
|
||||
use function rename;
|
||||
use function scandir;
|
||||
use function strrpos;
|
||||
use function substr;
|
||||
use function time;
|
||||
use const SCANDIR_SORT_NONE;
|
||||
|
||||
abstract class RegionWorldProvider extends BaseWorldProvider{
|
||||
|
||||
/**
|
||||
* Returns the file extension used for regions in this region-based format.
|
||||
* @return string
|
||||
*/
|
||||
abstract protected static function getRegionFileExtension() : string;
|
||||
|
||||
/**
|
||||
* Returns the storage version as per Minecraft PC world formats.
|
||||
* @return int
|
||||
*/
|
||||
abstract protected static function getPcWorldFormatVersion() : int;
|
||||
|
||||
public static function isValid(string $path) : bool{
|
||||
if(file_exists($path . "/level.dat") and is_dir($path . "/region/")){
|
||||
foreach(scandir($path . "/region/", SCANDIR_SORT_NONE) as $file){
|
||||
if(substr($file, strrpos($file, ".") + 1) === static::getRegionFileExtension()){
|
||||
//we don't care if other region types exist, we only care if this format is possible
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function generate(string $path, string $name, int $seed, string $generator, array $options = []) : void{
|
||||
Utils::testValidInstance($generator, Generator::class);
|
||||
if(!file_exists($path)){
|
||||
mkdir($path, 0777, true);
|
||||
}
|
||||
|
||||
if(!file_exists($path . "/region")){
|
||||
mkdir($path . "/region", 0777);
|
||||
}
|
||||
|
||||
JavaWorldData::generate($path, $name, $seed, $generator, $options, static::getPcWorldFormatVersion());
|
||||
}
|
||||
|
||||
/** @var RegionLoader[] */
|
||||
protected $regions = [];
|
||||
|
||||
protected function loadLevelData() : WorldData{
|
||||
return new JavaWorldData($this->getPath() . "level.dat");
|
||||
}
|
||||
|
||||
public function doGarbageCollection() : void{
|
||||
$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) : void{
|
||||
$regionX = $chunkX >> 5;
|
||||
$regionZ = $chunkZ >> 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $regionX
|
||||
* @param int $regionZ
|
||||
*
|
||||
* @return RegionLoader|null
|
||||
*/
|
||||
protected function getRegion(int $regionX, int $regionZ) : ?RegionLoader{
|
||||
return $this->regions[World::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) : void{
|
||||
if(!isset($this->regions[$index = World::chunkHash($regionX, $regionZ)])){
|
||||
$path = $this->pathToRegion($regionX, $regionZ);
|
||||
|
||||
$region = new RegionLoader($path);
|
||||
try{
|
||||
$region->open();
|
||||
}catch(CorruptedRegionException $e){
|
||||
$logger = \GlobalLogger::get();
|
||||
$logger->error("Corrupted region file detected: " . $e->getMessage());
|
||||
|
||||
$region->close(); //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);
|
||||
$region->open(); //this will create a new empty region to replace the corrupted one
|
||||
}
|
||||
|
||||
$this->regions[$index] = $region;
|
||||
}
|
||||
}
|
||||
|
||||
protected function unloadRegion(int $regionX, int $regionZ) : void{
|
||||
if(isset($this->regions[$hash = World::chunkHash($regionX, $regionZ)])){
|
||||
$this->regions[$hash]->close();
|
||||
unset($this->regions[$hash]);
|
||||
}
|
||||
}
|
||||
|
||||
public function close() : void{
|
||||
foreach($this->regions as $index => $region){
|
||||
$region->close();
|
||||
unset($this->regions[$index]);
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected function serializeChunk(Chunk $chunk) : string;
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
*
|
||||
* @return Chunk
|
||||
* @throws CorruptedChunkException
|
||||
*/
|
||||
abstract protected function deserializeChunk(string $data) : Chunk;
|
||||
|
||||
/**
|
||||
* @param int $chunkX
|
||||
* @param int $chunkZ
|
||||
*
|
||||
* @return Chunk|null
|
||||
* @throws CorruptedChunkException
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
private function createRegionIterator() : \RegexIterator{
|
||||
return 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
|
||||
);
|
||||
}
|
||||
|
||||
public function getAllChunks(bool $skipCorrupted = false, ?\Logger $logger = null) : \Generator{
|
||||
$iterator = $this->createRegionIterator();
|
||||
|
||||
foreach($iterator as $region){
|
||||
$regionX = ((int) $region[1]);
|
||||
$regionZ = ((int) $region[2]);
|
||||
$rX = $regionX << 5;
|
||||
$rZ = $regionZ << 5;
|
||||
|
||||
for($chunkX = $rX; $chunkX < $rX + 32; ++$chunkX){
|
||||
for($chunkZ = $rZ; $chunkZ < $rZ + 32; ++$chunkZ){
|
||||
try{
|
||||
$chunk = $this->loadChunk($chunkX, $chunkZ);
|
||||
if($chunk !== null){
|
||||
yield $chunk;
|
||||
}
|
||||
}catch(CorruptedChunkException $e){
|
||||
if(!$skipCorrupted){
|
||||
throw $e;
|
||||
}
|
||||
if($logger !== null){
|
||||
$logger->error("Skipped corrupted chunk $chunkX $chunkZ (" . $e->getMessage() . ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->unloadRegion($regionX, $regionZ);
|
||||
}
|
||||
}
|
||||
|
||||
public function calculateChunkCount() : int{
|
||||
$count = 0;
|
||||
foreach($this->createRegionIterator() as $region){
|
||||
$regionX = ((int) $region[1]);
|
||||
$regionZ = ((int) $region[2]);
|
||||
$this->loadRegion($regionX, $regionZ);
|
||||
$count += $this->getRegion($regionX, $regionZ)->calculateChunkCount();
|
||||
$this->unloadRegion($regionX, $regionZ);
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user