Run Flat world generators on the main thread

This commit is contained in:
Dylan K. Taylor 2025-04-05 23:51:54 +01:00
parent d338e7624c
commit 664af7ad7c
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
4 changed files with 128 additions and 10 deletions

View File

@ -0,0 +1,101 @@
<?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;
use pocketmine\event\world\ChunkPopulateEvent;
use pocketmine\promise\Promise;
use pocketmine\promise\PromiseResolver;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\world\format\Chunk;
use pocketmine\world\generator\Generator;
/**
* 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{
public function __construct(
private readonly Generator $generator
){}
public function requestChunkPopulation(World $world, int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
return $this->orderChunkPopulation($world, $chunkX, $chunkZ, $associatedChunkLoader);
}
public function orderChunkPopulation(World $world, int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
$temporaryChunkLoader = new class implements ChunkLoader{};
$world->registerChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
$oldChunk = $world->loadChunk($chunkX, $chunkZ);
//TODO: the following code is basically identical to PopulationTask
//we should probably generalize this
for($xx = $chunkX - 1; $xx <= $chunkX + 1; ++$xx){
for($zz = $chunkZ - 1; $zz <= $chunkZ + 1; ++$zz){
$world->registerChunkLoader($temporaryChunkLoader, $xx, $zz);
$chunk = $world->loadChunk($xx, $zz);
if($chunk === null){
$this->generator->generateChunk($world, $xx, $zz);
}
}
}
$this->generator->populateChunk($world, $chunkX, $chunkZ);
$chunk = $world->getChunk($chunkX, $chunkZ);
if($chunk === null){
throw new AssumptionFailedError("We just generated and populated this, it should not be null");
}
$chunk->setPopulated();
//TODO: this is basically identical to the AsyncChunkGenerator generateChunkCallback side
//we probably ought to generalize this
if(($oldChunk === null || !$oldChunk->isPopulated()) && $chunk->isPopulated()){
if(ChunkPopulateEvent::hasHandlers()){
(new ChunkPopulateEvent($world, $chunkX, $chunkZ, $chunk))->call();
}
foreach($world->getChunkListeners($chunkX, $chunkZ) as $listener){
$listener->onChunkPopulated($chunkX, $chunkZ, $chunk);
}
}
for($xx = $chunkX - 1; $xx <= $chunkX + 1; ++$xx){
for($zz = $chunkZ - 1; $zz <= $chunkZ + 1; ++$zz){
$world->unregisterChunkLoader($temporaryChunkLoader, $xx, $zz);
}
}
/** @phpstan-var PromiseResolver<Chunk> $resolver */
$resolver = new PromiseResolver();
$resolver->resolve($chunk);
return $resolver->getPromise();
}
public function cancelChunkPopulation(World $world, int $chunkX, int $chunkZ) : void{
//NOOP
}
public function shutdown(World $world) : void{
//NOOP
}
}

View File

@ -92,6 +92,7 @@ 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\Generator;
use pocketmine\world\generator\GeneratorManager;
use pocketmine\world\light\BlockLightUpdate;
use pocketmine\world\light\LightPopulationTask;
@ -468,15 +469,25 @@ class World implements ChunkManager{
$this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_preparing($this->displayName)));
$generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator()) ??
throw new AssumptionFailedError("WorldManager should already have checked that the generator exists");
$generator->validateGeneratorOptions($this->provider->getWorldData()->getGeneratorOptions());
$generatorOptions = $this->provider->getWorldData()->getGeneratorOptions();
$generator->validateGeneratorOptions($generatorOptions);
$this->generator = $generator->getGeneratorClass();
$cfg = $this->server->getConfigGroup();
$this->chunkGenerator = new AsyncChunkGenerator(
$this->workerPool,
$this->logger,
$cfg->getPropertyInt(YmlServerProperties::CHUNK_GENERATION_POPULATION_QUEUE_SIZE, 2)
);
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);
}else{
$this->chunkGenerator = new AsyncChunkGenerator(
$this->workerPool,
$this->logger,
$cfg->getPropertyInt(YmlServerProperties::CHUNK_GENERATION_POPULATION_QUEUE_SIZE, 2)
);
$this->logger->debug("Using async task generator system for slow generator " . $this->generator);
}
$this->addOnUnloadCallback(function() : void{
$this->chunkGenerator->shutdown($this);
});

View File

@ -50,7 +50,7 @@ final class GeneratorManager{
}catch(InvalidGeneratorOptionsException $e){
return $e;
}
});
}, fast: true);
$this->addGenerator(Normal::class, "normal", fn() => null);
$this->addAlias("normal", "default");
$this->addGenerator(Nether::class, "nether", fn() => null);
@ -62,6 +62,7 @@ final class GeneratorManager{
* @param string $name Alias for this generator type that can be written in configs
* @param \Closure $presetValidator Callback to validate generator options for new worlds
* @param bool $overwrite Whether to force overwriting any existing registered generator with the same name
* @param bool $fast Whether this generator is fast enough to run without async tasks
*
* @phpstan-param \Closure(string) : ?InvalidGeneratorOptionsException $presetValidator
*
@ -69,7 +70,7 @@ final class GeneratorManager{
*
* @throws \InvalidArgumentException
*/
public function addGenerator(string $class, string $name, \Closure $presetValidator, bool $overwrite = false) : void{
public function addGenerator(string $class, string $name, \Closure $presetValidator, bool $overwrite = false, bool $fast = false) : void{
Utils::testValidInstance($class, Generator::class);
$name = strtolower($name);
@ -77,7 +78,7 @@ final class GeneratorManager{
throw new \InvalidArgumentException("Alias \"$name\" is already assigned");
}
$this->list[$name] = new GeneratorManagerEntry($class, $presetValidator);
$this->list[$name] = new GeneratorManagerEntry($class, $presetValidator, $fast);
}
/**

View File

@ -31,7 +31,8 @@ final class GeneratorManagerEntry{
*/
public function __construct(
private string $generatorClass,
private \Closure $presetValidator
private \Closure $presetValidator,
private bool $fast
){}
/** @phpstan-return class-string<Generator> */
@ -45,4 +46,8 @@ final class GeneratorManagerEntry{
throw $exception;
}
}
public function isFast() : bool{
return $this->fast;
}
}