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<WorldProvider>. 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.
This commit is contained in:
Dylan K. Taylor 2021-07-13 16:53:17 +01:00
parent 654fc9a2a6
commit 676bacbee1
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
11 changed files with 158 additions and 184 deletions

View File

@ -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 !== ""){

View File

@ -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);

View File

@ -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<TNewProvider> $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{

View File

@ -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); }
}

View File

@ -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.
*

View File

@ -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<string, class-string<WorldProvider>>
* @var WorldProviderManagerEntry[]
* @phpstan-var array<string, WorldProviderManagerEntry>
*/
protected $providers = [];
/**
* @var string
* @phpstan-var class-string<WritableWorldProvider>
*/
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<WritableWorldProvider>
*/
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<WritableWorldProvider> $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<WorldProvider> $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<string, class-string<WorldProvider>>
* @return WorldProviderManagerEntry[]
* @phpstan-return array<string, WorldProviderManagerEntry>
*/
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<string, class-string<WorldProvider>>
* @return WorldProviderManagerEntry[]
* @phpstan-return array<string, WorldProviderManagerEntry>
*/
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<WorldProvider>|null
*/
public function getProviderByName(string $name) : ?string{
public function getProviderByName(string $name) : ?WorldProviderManagerEntry{
return $this->providers[trim(strtolower($name))] ?? null;
}
}

View 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;
use pocketmine\world\format\io\exception\CorruptedWorldException;
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
/**
* @phpstan-type IsValid \Closure(string $path) : bool
*/
abstract class WorldProviderManagerEntry{
/** @phpstan-var IsValid */
protected \Closure $isValid;
/** @phpstan-param IsValid $isValid */
protected function __construct(\Closure $isValid){
$this->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;
}

View File

@ -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).
*/

View File

@ -0,0 +1,58 @@
<?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\WorldCreationOptions;
/**
* @phpstan-type FromPath \Closure(string $path) : WritableWorldProvider
* @phpstan-type Generate \Closure(string $path, string $name, WorldCreationOptions $options) : void
*/
final class WritableWorldProviderManagerEntry extends WorldProviderManagerEntry{
/** @phpstan-var FromPath */
private \Closure $fromPath;
/** @phpstan-var Generate */
private \Closure $generate;
/**
* @phpstan-param FromPath $fromPath
* @phpstan-param Generate $generate
*/
public function __construct(\Closure $isValid, \Closure $fromPath, \Closure $generate){
parent::__construct($isValid);
$this->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);
}
}

View File

@ -1,28 +0,0 @@
<?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;
interface InterfaceWorldProvider extends WorldProvider{
}

View File

@ -1,60 +0,0 @@
<?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 PHPUnit\Framework\TestCase;
class LevelProviderManagerTest extends TestCase{
/** @var WorldProviderManager */
private $providerManager;
protected function setUp() : void{
$this->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");
}
}