From 676bacbee18d5eafeddf7b89ed7b70a2ce911f70 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Tue, 13 Jul 2021 16:53:17 +0100 Subject: [PATCH] Improve the flexibility of WorldProvider registration WorldProviders now have the following requirements removed: - __construct() is no longer required to have a specific signature - static isValid() no longer needs to be implemented (you will still need it for registering, but it can be declared anywhere now) - static generate() no longer needs to be implemented This paves the way for more interesting types of world providers that use something other than local disk to store chunks (e.g. a mysql database). WorldProviderManager no longer accepts class-string. Instead, WorldProviderManagerEntry is required, with 2 or 3 callbacks: - ReadOnlyWorldProviderManager must provide a callback for isValid, and a callback for fromPath - WritableWorldProviderManagerEntry must provide the same, and also a generate() callback In practice, this requires zero changes to the WorldProviders themselves, since a WorldProviderManagerEntry can be created like this: `new WritableWorldProviderManagerEntry(\Closure::fromCallable([LevelDB::class, 'isValid']), fn(string ) => new LevelDB(), \Closure::fromCallable([LevelDB::class, 'generate']))` This provides identical functionality to before for the provider itself; only registration is changed. --- src/Server.php | 5 +- src/world/WorldManager.php | 15 ++--- src/world/format/io/FormatConverter.php | 22 ++----- .../io/ReadOnlyWorldProviderManagerEntry.php | 15 ++++- src/world/format/io/WorldProvider.php | 15 ----- src/world/format/io/WorldProviderManager.php | 65 +++++++------------ .../format/io/WorldProviderManagerEntry.php | 53 +++++++++++++++ src/world/format/io/WritableWorldProvider.php | 6 -- .../io/WritableWorldProviderManagerEntry.php | 58 +++++++++++++++++ .../format/io/InterfaceWorldProvider.php | 28 -------- .../format/io/LevelProviderManagerTest.php | 60 ----------------- 11 files changed, 158 insertions(+), 184 deletions(-) rename tests/phpunit/world/format/io/AbstractWorldProvider.php => src/world/format/io/ReadOnlyWorldProviderManagerEntry.php (61%) create mode 100644 src/world/format/io/WorldProviderManagerEntry.php create mode 100644 src/world/format/io/WritableWorldProviderManagerEntry.php delete mode 100644 tests/phpunit/world/format/io/InterfaceWorldProvider.php delete mode 100644 tests/phpunit/world/format/io/LevelProviderManagerTest.php diff --git a/src/Server.php b/src/Server.php index 8d2e83c18..2b34f1cb7 100644 --- a/src/Server.php +++ b/src/Server.php @@ -99,7 +99,7 @@ use pocketmine\utils\Terminal; use pocketmine\utils\TextFormat; use pocketmine\utils\Utils; use pocketmine\world\format\io\WorldProviderManager; -use pocketmine\world\format\io\WritableWorldProvider; +use pocketmine\world\format\io\WritableWorldProviderManagerEntry; use pocketmine\world\generator\Generator; use pocketmine\world\generator\GeneratorManager; use pocketmine\world\World; @@ -121,7 +121,6 @@ use function filemtime; use function get_class; use function implode; use function ini_set; -use function is_a; use function is_array; use function is_string; use function json_decode; @@ -1014,7 +1013,7 @@ class Server{ $providerManager = new WorldProviderManager(); if( ($format = $providerManager->getProviderByName($formatName = $this->configGroup->getPropertyString("level-settings.default-format", ""))) !== null and - is_a($format, WritableWorldProvider::class, true) + $format instanceof WritableWorldProviderManagerEntry ){ $providerManager->setDefault($format); }elseif($formatName !== ""){ diff --git a/src/world/WorldManager.php b/src/world/WorldManager.php index 99e07b566..0c3a166b8 100644 --- a/src/world/WorldManager.php +++ b/src/world/WorldManager.php @@ -34,7 +34,6 @@ use pocketmine\timings\Timings; use pocketmine\world\format\io\exception\CorruptedWorldException; use pocketmine\world\format\io\exception\UnsupportedWorldFormatException; use pocketmine\world\format\io\FormatConverter; -use pocketmine\world\format\io\WorldProvider; use pocketmine\world\format\io\WorldProviderManager; use pocketmine\world\format\io\WritableWorldProvider; use pocketmine\world\generator\GeneratorManager; @@ -205,11 +204,7 @@ class WorldManager{ $providerClass = array_shift($providers); try{ - /** - * @var WorldProvider $provider - * @see WorldProvider::__construct() - */ - $provider = new $providerClass($path); + $provider = $providerClass->fromPath($path); }catch(CorruptedWorldException $e){ $this->server->getLogger()->error($this->server->getLanguage()->translateString(KnownTranslationKeys::POCKETMINE_LEVEL_LOADERROR, [$name, "Corruption detected: " . $e->getMessage()])); return false; @@ -255,14 +250,12 @@ class WorldManager{ return false; } - $providerClass = $this->providerManager->getDefault(); + $providerEntry = $this->providerManager->getDefault(); $path = $this->getWorldPath($name); - /** @var WritableWorldProvider $providerClass */ - $providerClass::generate($path, $name, $options); + $providerEntry->generate($path, $name, $options); - /** @see WritableWorldProvider::__construct() */ - $world = new World($this->server, $name, new $providerClass($path), $this->server->getAsyncPool()); + $world = new World($this->server, $name, $providerEntry->fromPath($path), $this->server->getAsyncPool()); $this->worlds[$world->getId()] = $world; $world->setAutoSave($this->autoSave); diff --git a/src/world/format/io/FormatConverter.php b/src/world/format/io/FormatConverter.php index 053e26c37..24ca24db3 100644 --- a/src/world/format/io/FormatConverter.php +++ b/src/world/format/io/FormatConverter.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace pocketmine\world\format\io; use pocketmine\utils\Filesystem; -use pocketmine\utils\Utils; use pocketmine\world\generator\GeneratorManager; use pocketmine\world\WorldCreationOptions; use Webmozart\PathUtil\Path; @@ -44,7 +43,7 @@ class FormatConverter{ /** @var WorldProvider */ private $oldProvider; - /** @var WritableWorldProvider|string */ + /** @var WritableWorldProviderManagerEntry */ private $newProvider; /** @var string */ @@ -56,13 +55,8 @@ class FormatConverter{ /** @var int */ private $chunksPerProgressUpdate; - /** - * @phpstan-template TNewProvider of WritableWorldProvider - * @phpstan-param class-string $newProvider - */ - public function __construct(WorldProvider $oldProvider, string $newProvider, string $backupPath, \Logger $logger, int $chunksPerProgressUpdate = 256){ + public function __construct(WorldProvider $oldProvider, WritableWorldProviderManagerEntry $newProvider, string $backupPath, \Logger $logger, int $chunksPerProgressUpdate = 256){ $this->oldProvider = $oldProvider; - Utils::testValidInstance($newProvider, WritableWorldProvider::class); $this->newProvider = $newProvider; $this->logger = new \PrefixedLogger($logger, "World Converter: " . $this->oldProvider->getWorldData()->getName()); $this->chunksPerProgressUpdate = $chunksPerProgressUpdate; @@ -105,10 +99,7 @@ class FormatConverter{ } $this->logger->info("Conversion completed"); - /** - * @see WritableWorldProvider::__construct() - */ - return new $this->newProvider($path); + return $this->newProvider->fromPath($path); } private function generateNew() : WritableWorldProvider{ @@ -120,7 +111,7 @@ class FormatConverter{ $this->logger->info("Found previous conversion attempt, deleting..."); Filesystem::recursiveUnlink($convertedOutput); } - $this->newProvider::generate($convertedOutput, $data->getName(), WorldCreationOptions::create() + $this->newProvider->generate($convertedOutput, $data->getName(), WorldCreationOptions::create() ->setGeneratorClass(GeneratorManager::getInstance()->getGenerator($data->getGenerator())) ->setGeneratorOptions($data->getGeneratorOptions()) ->setSeed($data->getSeed()) @@ -128,10 +119,7 @@ class FormatConverter{ ->setDifficulty($data->getDifficulty()) ); - /** - * @see WritableWorldProvider::__construct() - */ - return new $this->newProvider($convertedOutput); + return $this->newProvider->fromPath($convertedOutput); } private function populateLevelData(WorldData $data) : void{ diff --git a/tests/phpunit/world/format/io/AbstractWorldProvider.php b/src/world/format/io/ReadOnlyWorldProviderManagerEntry.php similarity index 61% rename from tests/phpunit/world/format/io/AbstractWorldProvider.php rename to src/world/format/io/ReadOnlyWorldProviderManagerEntry.php index 29e78451e..beb409882 100644 --- a/tests/phpunit/world/format/io/AbstractWorldProvider.php +++ b/src/world/format/io/ReadOnlyWorldProviderManagerEntry.php @@ -23,6 +23,19 @@ declare(strict_types=1); namespace pocketmine\world\format\io; -abstract class AbstractWorldProvider implements WorldProvider{ +/** + * @phpstan-type FromPath \Closure(string $path) : WorldProvider + */ +class ReadOnlyWorldProviderManagerEntry extends WorldProviderManagerEntry{ + /** @phpstan-var FromPath */ + private \Closure $fromPath; + + /** @phpstan-param FromPath $fromPath */ + public function __construct(\Closure $isValid, \Closure $fromPath){ + parent::__construct($isValid); + $this->fromPath = $fromPath; + } + + public function fromPath(string $path) : WorldProvider{ return ($this->fromPath)($path); } } diff --git a/src/world/format/io/WorldProvider.php b/src/world/format/io/WorldProvider.php index 77f0fa199..5089d8e5b 100644 --- a/src/world/format/io/WorldProvider.php +++ b/src/world/format/io/WorldProvider.php @@ -25,17 +25,8 @@ 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{ - - /** - * @throws CorruptedWorldException - * @throws UnsupportedWorldFormatException - */ - public function __construct(string $path); - /** * Returns the lowest buildable Y coordinate of this world */ @@ -48,12 +39,6 @@ interface WorldProvider{ 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 - */ - 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. * diff --git a/src/world/format/io/WorldProviderManager.php b/src/world/format/io/WorldProviderManager.php index ba611eba1..001f92913 100644 --- a/src/world/format/io/WorldProviderManager.php +++ b/src/world/format/io/WorldProviderManager.php @@ -23,7 +23,6 @@ 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; @@ -33,80 +32,62 @@ use function trim; final class WorldProviderManager{ /** - * @var string[] - * @phpstan-var array> + * @var WorldProviderManagerEntry[] + * @phpstan-var array */ protected $providers = []; - /** - * @var string - * @phpstan-var class-string - */ - private $default = LevelDB::class; + private WritableWorldProviderManagerEntry $default; public function __construct(){ - $this->addProvider(Anvil::class, "anvil"); - $this->addProvider(McRegion::class, "mcregion"); - $this->addProvider(PMAnvil::class, "pmanvil"); - $this->addProvider(LevelDB::class, "leveldb"); + $leveldb = new WritableWorldProviderManagerEntry(\Closure::fromCallable([LevelDB::class, 'isValid']), fn(string $path) => new LevelDB($path), \Closure::fromCallable([LevelDB::class, 'generate'])); + $this->default = $leveldb; + $this->addProvider($leveldb, "leveldb"); + + $this->addProvider(new ReadOnlyWorldProviderManagerEntry(\Closure::fromCallable([Anvil::class, 'isValid']), fn(string $path) => new Anvil($path)), "anvil"); + $this->addProvider(new ReadOnlyWorldProviderManagerEntry(\Closure::fromCallable([McRegion::class, 'isValid']), fn(string $path) => new McRegion($path)), "mcregion"); + $this->addProvider(new ReadOnlyWorldProviderManagerEntry(\Closure::fromCallable([PMAnvil::class, 'isValid']), fn(string $path) => new PMAnvil($path)), "pmanvil"); } /** * Returns the default format used to generate new worlds. - * - * @phpstan-return class-string */ - public function getDefault() : string{ + public function getDefault() : WritableWorldProviderManagerEntry{ return $this->default; } - /** - * Sets the default format. - * - * @param string $class Class implementing WritableWorldProvider - * @phpstan-param class-string $class - * - * @throws \InvalidArgumentException - */ - public function setDefault(string $class) : void{ - Utils::testValidInstance($class, WritableWorldProvider::class); - + public function setDefault(WritableWorldProviderManagerEntry $class) : void{ $this->default = $class; } - /** - * @phpstan-param class-string $class - */ - public function addProvider(string $class, string $name, bool $overwrite = false) : void{ - Utils::testValidInstance($class, WorldProvider::class); - + public function addProvider(WorldProviderManagerEntry $providerEntry, string $name, bool $overwrite = false) : void{ $name = strtolower($name); if(!$overwrite and isset($this->providers[$name])){ throw new \InvalidArgumentException("Alias \"$name\" is already assigned"); } - $this->providers[$name] = $class; + $this->providers[$name] = $providerEntry; } /** * Returns a WorldProvider class for this path, or null * - * @return string[] - * @phpstan-return array> + * @return WorldProviderManagerEntry[] + * @phpstan-return array */ public function getMatchingProviders(string $path) : array{ $result = []; - foreach($this->providers as $alias => $provider){ - if($provider::isValid($path)){ - $result[$alias] = $provider; + foreach($this->providers as $alias => $providerEntry){ + if($providerEntry->isValid($path)){ + $result[$alias] = $providerEntry; } } return $result; } /** - * @return string[] - * @phpstan-return array> + * @return WorldProviderManagerEntry[] + * @phpstan-return array */ public function getAvailableProviders() : array{ return $this->providers; @@ -114,10 +95,8 @@ final class WorldProviderManager{ /** * Returns a WorldProvider by name, or null if not found - * - * @phpstan-return class-string|null */ - public function getProviderByName(string $name) : ?string{ + public function getProviderByName(string $name) : ?WorldProviderManagerEntry{ return $this->providers[trim(strtolower($name))] ?? null; } } diff --git a/src/world/format/io/WorldProviderManagerEntry.php b/src/world/format/io/WorldProviderManagerEntry.php new file mode 100644 index 000000000..5c35ac09e --- /dev/null +++ b/src/world/format/io/WorldProviderManagerEntry.php @@ -0,0 +1,53 @@ +isValid = $isValid; + } + + /** + * Tells if the path is a valid world. + * This must tell if the current format supports opening the files in the directory + */ + public function isValid(string $path) : bool{ return ($this->isValid)($path); } + + /** + * @throws CorruptedWorldException + * @throws UnsupportedWorldFormatException + */ + abstract public function fromPath(string $path) : WorldProvider; +} diff --git a/src/world/format/io/WritableWorldProvider.php b/src/world/format/io/WritableWorldProvider.php index 3f1652417..61fe03f0b 100644 --- a/src/world/format/io/WritableWorldProvider.php +++ b/src/world/format/io/WritableWorldProvider.php @@ -24,14 +24,8 @@ declare(strict_types=1); namespace pocketmine\world\format\io; use pocketmine\world\format\Chunk; -use pocketmine\world\WorldCreationOptions; interface WritableWorldProvider extends WorldProvider{ - /** - * Generate the needed files in the path given - */ - public static function generate(string $path, string $name, WorldCreationOptions $options) : void; - /** * Saves a chunk (usually to disk). */ diff --git a/src/world/format/io/WritableWorldProviderManagerEntry.php b/src/world/format/io/WritableWorldProviderManagerEntry.php new file mode 100644 index 000000000..296e4ca42 --- /dev/null +++ b/src/world/format/io/WritableWorldProviderManagerEntry.php @@ -0,0 +1,58 @@ +fromPath = $fromPath; + $this->generate = $generate; + } + + public function fromPath(string $path) : WritableWorldProvider{ + return ($this->fromPath)($path); + } + + /** + * Generates world manifest files and any other things needed to initialize a new world on disk + */ + public function generate(string $path, string $name, WorldCreationOptions $options) : void{ + ($this->generate)($path, $name, $options); + } +} diff --git a/tests/phpunit/world/format/io/InterfaceWorldProvider.php b/tests/phpunit/world/format/io/InterfaceWorldProvider.php deleted file mode 100644 index baaa3337d..000000000 --- a/tests/phpunit/world/format/io/InterfaceWorldProvider.php +++ /dev/null @@ -1,28 +0,0 @@ -providerManager = new WorldProviderManager(); - } - - public function testAddNonClassProvider() : void{ - $this->expectException(\InvalidArgumentException::class); - - $this->providerManager->addProvider("lol", "nope"); - } - - public function testAddAbstractClassProvider() : void{ - $this->expectException(\InvalidArgumentException::class); - - $this->providerManager->addProvider(AbstractWorldProvider::class, "abstract"); - } - - public function testAddInterfaceProvider() : void{ - $this->expectException(\InvalidArgumentException::class); - - $this->providerManager->addProvider(InterfaceWorldProvider::class, "interface"); - } - - public function testAddWrongClassProvider() : void{ - $this->expectException(\InvalidArgumentException::class); - - $this->providerManager->addProvider(LevelProviderManagerTest::class, "bad_class"); - } -}