From a4ae1991c631617051e2fce3cd7b40f0e261b256 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sun, 6 Apr 2025 19:00:33 +0100 Subject: [PATCH] Tidy, allow using closures to setup generators instead of class strings --- src/world/World.php | 41 ++++++++----------- src/world/generator/GeneratorManagerEntry.php | 10 +++++ src/world/generator/GeneratorRegisterTask.php | 13 ++---- .../executor/AsyncGeneratorExecutor.php} | 22 +++++++--- .../executor/GeneratorExecutor.php} | 26 +++++++----- .../executor/SyncGeneratorExecutor.php} | 6 ++- 6 files changed, 67 insertions(+), 51 deletions(-) rename src/world/{AsyncChunkGenerator.php => generator/executor/AsyncGeneratorExecutor.php} (95%) rename src/world/{ChunkGenerator.php => generator/executor/GeneratorExecutor.php} (66%) rename src/world/{SyncChunkGenerator.php => generator/executor/SyncGeneratorExecutor.php} (95%) diff --git a/src/world/World.php b/src/world/World.php index ebced35be..c4d9c7a48 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -92,8 +92,12 @@ use pocketmine\world\format\io\GlobalBlockStateHandlers; use pocketmine\world\format\io\WritableWorldProvider; use pocketmine\world\format\LightArray; use pocketmine\world\format\SubChunk; +use pocketmine\world\generator\executor\AsyncGeneratorExecutor; +use pocketmine\world\generator\executor\GeneratorExecutor; +use pocketmine\world\generator\executor\SyncGeneratorExecutor; use pocketmine\world\generator\Generator; use pocketmine\world\generator\GeneratorManager; +use pocketmine\world\generator\GeneratorManagerEntry; use pocketmine\world\light\BlockLightUpdate; use pocketmine\world\light\LightPopulationTask; use pocketmine\world\light\SkyLightUpdate; @@ -306,7 +310,7 @@ class World implements ChunkManager{ */ private array $neighbourBlockUpdateQueueIndex = []; - private readonly ChunkGenerator $chunkGenerator; + private readonly GeneratorExecutor $generatorExecutor; /** * @var ChunkLockId[] @@ -332,9 +336,6 @@ class World implements ChunkManager{ private bool $doingTick = false; - /** @phpstan-var class-string<\pocketmine\world\generator\Generator> */ - private string $generator; - private bool $unloaded = false; /** * @var \Closure[] @@ -471,25 +472,24 @@ class World implements ChunkManager{ throw new AssumptionFailedError("WorldManager should already have checked that the generator exists"); $generatorOptions = $this->provider->getWorldData()->getGeneratorOptions(); $generator->validateGeneratorOptions($generatorOptions); - $this->generator = $generator->getGeneratorClass(); + $generatorClass = $generator->getGeneratorClass(); $cfg = $this->server->getConfigGroup(); + $seed = $this->getSeed(); if($generator->isFast()){ - /** - * @see Generator::__construct() - */ - $this->chunkGenerator = new SyncChunkGenerator(new $this->generator($this->getSeed(), $generatorOptions)); - $this->logger->debug("Using main thread generator system for fast generator " . $this->generator); + $this->generatorExecutor = new SyncGeneratorExecutor(GeneratorManagerEntry::make($generatorClass, $seed, $generatorOptions)); + $this->logger->debug("Using main thread generator system for fast generator " . $generatorClass); }else{ - $this->chunkGenerator = new AsyncChunkGenerator( + $this->generatorExecutor = new AsyncGeneratorExecutor( $this->workerPool, $this->logger, + static fn() => GeneratorManagerEntry::make($generatorClass, $seed, $generatorOptions), $cfg->getPropertyInt(YmlServerProperties::CHUNK_GENERATION_POPULATION_QUEUE_SIZE, 2) ); - $this->logger->debug("Using async task generator system for slow generator " . $this->generator); + $this->logger->debug("Using async task generator system for slow generator " . $generatorClass); } $this->addOnUnloadCallback(function() : void{ - $this->chunkGenerator->shutdown($this); + $this->generatorExecutor->shutdown($this); }); $this->scheduledBlockUpdateQueue = new ReversePriorityQueue(); @@ -549,13 +549,6 @@ class World implements ChunkManager{ return $this->tickRateTime; } - /** - * @phpstan-return class-string - */ - public function getGeneratorClass() : string{ - return $this->generator; - } - public function getServer() : Server{ return $this->server; } @@ -791,7 +784,7 @@ class World implements ChunkManager{ if(count($this->chunkLoaders[$chunkHash]) === 1){ unset($this->chunkLoaders[$chunkHash]); $this->unloadChunkRequest($chunkX, $chunkZ, true); - $this->chunkGenerator->cancelChunkPopulation($this, $chunkX, $chunkZ); + $this->generatorExecutor->cancelChunkPopulation($this, $chunkX, $chunkZ); }else{ unset($this->chunkLoaders[$chunkHash][$loaderId]); } @@ -3068,7 +3061,7 @@ class World implements ChunkManager{ unset($this->registeredTickingChunks[$chunkHash]); $this->markTickingChunkForRecheck($x, $z); - $this->chunkGenerator->cancelChunkPopulation($this, $x, $z); + $this->generatorExecutor->cancelChunkPopulation($this, $x, $z); $this->timings->doChunkUnload->stopTiming(); @@ -3265,7 +3258,7 @@ class World implements ChunkManager{ * @phpstan-return Promise */ public function requestChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{ - return $this->chunkGenerator->requestChunkPopulation($this, $chunkX, $chunkZ, $associatedChunkLoader); + return $this->generatorExecutor->requestChunkPopulation($this, $chunkX, $chunkZ, $associatedChunkLoader); } /** @@ -3279,7 +3272,7 @@ class World implements ChunkManager{ * @phpstan-return Promise */ public function orderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{ - return $this->chunkGenerator->orderChunkPopulation($this, $chunkX, $chunkZ, $associatedChunkLoader); + return $this->generatorExecutor->orderChunkPopulation($this, $chunkX, $chunkZ, $associatedChunkLoader); } public function doChunkGarbageCollection() : void{ diff --git a/src/world/generator/GeneratorManagerEntry.php b/src/world/generator/GeneratorManagerEntry.php index c514f2fbe..692dc47c4 100644 --- a/src/world/generator/GeneratorManagerEntry.php +++ b/src/world/generator/GeneratorManagerEntry.php @@ -50,4 +50,14 @@ final class GeneratorManagerEntry{ public function isFast() : bool{ return $this->fast; } + + /** + * @phpstan-param class-string $class + */ + public static function make(string $class, int $seed, string $options) : Generator{ + /** + * @see Generator::__construct() + */ + return new $class($seed, $options); + } } diff --git a/src/world/generator/GeneratorRegisterTask.php b/src/world/generator/GeneratorRegisterTask.php index e2e773a35..7962b7a99 100644 --- a/src/world/generator/GeneratorRegisterTask.php +++ b/src/world/generator/GeneratorRegisterTask.php @@ -27,31 +27,24 @@ use pocketmine\scheduler\AsyncTask; use pocketmine\world\World; class GeneratorRegisterTask extends AsyncTask{ - public int $seed; public int $worldId; public int $worldMinY; public int $worldMaxY; /** - * @phpstan-param class-string $generatorClass + * @phpstan-param \Closure() : Generator $generatorFactory */ public function __construct( World $world, - public string $generatorClass, - public string $generatorSettings + private readonly \Closure $generatorFactory ){ - $this->seed = $world->getSeed(); $this->worldId = $world->getId(); $this->worldMinY = $world->getMinY(); $this->worldMaxY = $world->getMaxY(); } public function onRun() : void{ - /** - * @var Generator $generator - * @see Generator::__construct() - */ - $generator = new $this->generatorClass($this->seed, $this->generatorSettings); + $generator = ($this->generatorFactory)(); ThreadLocalGeneratorContext::register(new ThreadLocalGeneratorContext($generator, $this->worldMinY, $this->worldMaxY), $this->worldId); } } diff --git a/src/world/AsyncChunkGenerator.php b/src/world/generator/executor/AsyncGeneratorExecutor.php similarity index 95% rename from src/world/AsyncChunkGenerator.php rename to src/world/generator/executor/AsyncGeneratorExecutor.php index f4e5e28ed..210983fe4 100644 --- a/src/world/AsyncChunkGenerator.php +++ b/src/world/generator/executor/AsyncGeneratorExecutor.php @@ -21,16 +21,21 @@ declare(strict_types=1); -namespace pocketmine\world; +namespace pocketmine\world\generator\executor; +use pmmp\thread\ThreadSafeArray; use pocketmine\event\world\ChunkPopulateEvent; use pocketmine\promise\Promise; use pocketmine\promise\PromiseResolver; use pocketmine\scheduler\AsyncPool; use pocketmine\utils\AssumptionFailedError; +use pocketmine\world\ChunkLoader; +use pocketmine\world\ChunkLockId; use pocketmine\world\format\Chunk; +use pocketmine\world\generator\Generator; use pocketmine\world\generator\GeneratorRegisterTask; use pocketmine\world\generator\PopulationTask; +use pocketmine\world\World; use function array_key_exists; use function assert; use function count; @@ -38,7 +43,7 @@ use function count; /** * @phpstan-import-type ChunkPosHash from World */ -final class AsyncChunkGenerator implements ChunkGenerator{ +final class AsyncGeneratorExecutor implements GeneratorExecutor{ /** * @var bool[] chunkHash => isValid * @phpstan-var array @@ -71,11 +76,19 @@ final class AsyncChunkGenerator implements ChunkGenerator{ /** @phpstan-var \Closure(int) : void */ private \Closure $workerStartHook; + /** + * @phpstan-param \Closure() : Generator $generatorFactory Must be a thread-safe closure + */ public function __construct( private readonly AsyncPool $workerPool, private readonly \Logger $logger, - private readonly int $maxConcurrentChunkPopulationTasks = 2, + private readonly \Closure $generatorFactory, + private readonly int $maxConcurrentChunkPopulationTasks = 2 ){ + //TODO: we really need a better way to check if a closure is thread-safe :( + $temp = new ThreadSafeArray(); + $temp["dummy"] = $this->generatorFactory; + $this->chunkPopulationRequestQueue = new \SplQueue(); //TODO: don't love the circular reference here, but we need to make sure this gets cleaned up on shutdown $this->workerStartHook = function(int $workerId) : void{ @@ -91,8 +104,7 @@ final class AsyncChunkGenerator implements ChunkGenerator{ $world->getLogger()->debug("Registering generator on worker $worker"); $this->workerPool->submitTaskToWorker(new GeneratorRegisterTask( $world, - $world->getGeneratorClass(), - $world->getProvider()->getWorldData()->getGeneratorOptions() + $this->generatorFactory, ), $worker); $this->generatorRegisteredWorkers[$worker] = true; } diff --git a/src/world/ChunkGenerator.php b/src/world/generator/executor/GeneratorExecutor.php similarity index 66% rename from src/world/ChunkGenerator.php rename to src/world/generator/executor/GeneratorExecutor.php index d4850451a..586b5f8dd 100644 --- a/src/world/ChunkGenerator.php +++ b/src/world/generator/executor/GeneratorExecutor.php @@ -21,19 +21,25 @@ declare(strict_types=1); -namespace pocketmine\world; +namespace pocketmine\world\generator\executor; use pocketmine\promise\Promise; +use pocketmine\world\ChunkLoader; use pocketmine\world\format\Chunk; +use pocketmine\world\World; /** + * Decides how and when to invoke the world generator. + * * @phpstan-import-type ChunkPosHash from World */ -interface ChunkGenerator{ +interface GeneratorExecutor{ /** - * Attempts to initiate asynchronous generation/population of the target chunk, if it's currently reasonable to do - * so (and if it isn't already generated/populated). - * If the generator is busy, the request will be put into a queue and delayed until a better time. + * Requests generation/population of the target chunk, if it's currently reasonable to do so (and if it isn't + * already generated/populated). + * The executor may decide not to process this request immediately based on internal conditions (e.g. the number of + * concurrently active generation tasks may have reached a limit). If this happens, it will be queued and processed + * as soon as possible. * * A ChunkLoader can be associated with the generation request to ensure that the generation request is cancelled if * no loaders are attached to the target chunk. If no loader is provided, one will be assigned (and automatically @@ -44,12 +50,12 @@ interface ChunkGenerator{ public function requestChunkPopulation(World $world, int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise; /** - * Initiates asynchronous generation/population of the target chunk, if it's not already generated/populated. - * If generation has already been requested for the target chunk, the promise for the already active request will be - * returned directly. + * Initiates generation/population of the target chunk, if it's not already generated/populated. * - * If the chunk is currently locked (for example due to another chunk using it for async generation), the request - * will be queued and executed at the earliest opportunity. + * This function will begin processing the request immediately, unless any of the adjacent chunks are currently in + * use for other generation tasks. + * + * @see World::isChunkLocked() * * @phpstan-return Promise */ diff --git a/src/world/SyncChunkGenerator.php b/src/world/generator/executor/SyncGeneratorExecutor.php similarity index 95% rename from src/world/SyncChunkGenerator.php rename to src/world/generator/executor/SyncGeneratorExecutor.php index 9c58b97ec..9e8f50cc6 100644 --- a/src/world/SyncChunkGenerator.php +++ b/src/world/generator/executor/SyncGeneratorExecutor.php @@ -21,20 +21,22 @@ declare(strict_types=1); -namespace pocketmine\world; +namespace pocketmine\world\generator\executor; use pocketmine\event\world\ChunkPopulateEvent; use pocketmine\promise\Promise; use pocketmine\promise\PromiseResolver; use pocketmine\utils\AssumptionFailedError; +use pocketmine\world\ChunkLoader; use pocketmine\world\format\Chunk; use pocketmine\world\generator\Generator; +use pocketmine\world\World; /** * Very simple chunk generator which runs everything immediately on the main thread. * Useful if your generator is very fast and doesn't benefit from async tasks or threading. */ -final class SyncChunkGenerator implements ChunkGenerator{ +final class SyncGeneratorExecutor implements GeneratorExecutor{ public function __construct( private readonly Generator $generator ){}