Tidy, allow using closures to setup generators instead of class strings

This commit is contained in:
Dylan K. Taylor 2025-04-06 19:00:33 +01:00
parent 09c074ed22
commit a4ae1991c6
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
6 changed files with 67 additions and 51 deletions

View File

@ -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<covariant \pocketmine\world\generator\Generator>
*/
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<Chunk>
*/
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<Chunk>
*/
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{

View File

@ -50,4 +50,14 @@ final class GeneratorManagerEntry{
public function isFast() : bool{
return $this->fast;
}
/**
* @phpstan-param class-string<covariant Generator> $class
*/
public static function make(string $class, int $seed, string $options) : Generator{
/**
* @see Generator::__construct()
*/
return new $class($seed, $options);
}
}

View File

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

View File

@ -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<ChunkPosHash, bool>
@ -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;
}

View File

@ -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<Chunk>
*/

View File

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