From 059f4ee7bfb2b892dadce97fa3c54a0f680f60f9 Mon Sep 17 00:00:00 2001 From: "Dylan T." Date: Tue, 27 May 2025 21:51:10 +0100 Subject: [PATCH 1/2] Extract GeneratorExecutor system from World, v2 (#6682) - `AsyncGeneratorExecutor` class added that encapsulates the logic of generating chunks using async tasks as previously - `GeneratorExecutor` interface added that can be implemented to provide chunks in other ways - `SyncGeneratorExecutor` which invokes the generator directly on the main thread, useful for simple generators like `Flat` where async tasks are not needed - Some redundant APIs were removed from `World` (these will probably come back as deprecated stubs for the remainder of 5.x, but I was having too much fun deleting code) - Removed internal `World->registerGeneratorToWorker()` (no longer useful) - `World` now invokes generator executor instead of posting AsyncTasks directly - Some internal classes moved to `pocketmine\world\generator\executor` (PopulationTask excluded because plugins use it in lieu of being able to regenerate chunks - Generators can opt into main-thread execution by setting the `$fast` parameter to `true` in `GeneratorManager::register()` --- src/world/World.php | 74 +++++------- src/world/generator/GeneratorManager.php | 7 +- src/world/generator/GeneratorManagerEntry.php | 5 +- src/world/generator/PopulationTask.php | 7 ++ .../executor/AsyncGeneratorExecutor.php | 106 ++++++++++++++++++ .../AsyncGeneratorRegisterTask.php} | 33 ++---- .../AsyncGeneratorUnregisterTask.php} | 15 +-- .../generator/executor/GeneratorExecutor.php | 38 +++++++ .../GeneratorExecutorSetupParameters.php | 50 +++++++++ .../executor/SyncGeneratorExecutor.php | 61 ++++++++++ .../ThreadLocalGeneratorContext.php | 4 +- tests/phpstan/configs/actual-problems.neon | 12 +- 12 files changed, 318 insertions(+), 94 deletions(-) create mode 100644 src/world/generator/executor/AsyncGeneratorExecutor.php rename src/world/generator/{GeneratorRegisterTask.php => executor/AsyncGeneratorRegisterTask.php} (54%) rename src/world/generator/{GeneratorUnregisterTask.php => executor/AsyncGeneratorUnregisterTask.php} (74%) create mode 100644 src/world/generator/executor/GeneratorExecutor.php create mode 100644 src/world/generator/executor/GeneratorExecutorSetupParameters.php create mode 100644 src/world/generator/executor/SyncGeneratorExecutor.php rename src/world/generator/{ => executor}/ThreadLocalGeneratorContext.php (94%) diff --git a/src/world/World.php b/src/world/World.php index c4d8f8671..792681a89 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -93,9 +93,11 @@ 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\GeneratorExecutorSetupParameters; +use pocketmine\world\generator\executor\SyncGeneratorExecutor; use pocketmine\world\generator\GeneratorManager; -use pocketmine\world\generator\GeneratorRegisterTask; -use pocketmine\world\generator\GeneratorUnregisterTask; use pocketmine\world\generator\PopulationTask; use pocketmine\world\light\BlockLightUpdate; use pocketmine\world\light\LightPopulationTask; @@ -336,11 +338,7 @@ class World implements ChunkManager{ */ private array $chunkPopulationRequestQueueIndex = []; - /** - * @var true[] - * @phpstan-var array - */ - private array $generatorRegisteredWorkers = []; + private readonly GeneratorExecutor $generatorExecutor; private bool $autoSave = true; @@ -360,9 +358,6 @@ class World implements ChunkManager{ private bool $doingTick = false; - /** @phpstan-var class-string */ - private string $generator; - private bool $unloaded = false; /** * @var \Closure[] @@ -498,7 +493,23 @@ class World implements ChunkManager{ $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()); - $this->generator = $generator->getGeneratorClass(); + + $executorSetupParameters = new GeneratorExecutorSetupParameters( + worldMinY: $this->minY, + worldMaxY: $this->maxY, + generatorSeed: $this->getSeed(), + generatorClass: $generator->getGeneratorClass(), + generatorSettings: $this->provider->getWorldData()->getGeneratorOptions() + ); + $this->generatorExecutor = $generator->isFast() ? + new SyncGeneratorExecutor($executorSetupParameters) : + new AsyncGeneratorExecutor( + $this->logger, + $this->workerPool, + $executorSetupParameters, + $this->worldId + ); + $this->chunkPopulationRequestQueue = new \SplQueue(); $this->addOnUnloadCallback(function() : void{ $this->logger->debug("Cancelling unfulfilled generation requests"); @@ -534,17 +545,6 @@ class World implements ChunkManager{ $this->initRandomTickBlocksFromConfig($cfg); $this->timings = new WorldTimings($this); - - $this->workerPool->addWorkerStartHook($workerStartHook = function(int $workerId) : void{ - if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){ - $this->logger->debug("Worker $workerId with previously registered generator restarted, flagging as unregistered"); - unset($this->generatorRegisteredWorkers[$workerId]); - } - }); - $workerPool = $this->workerPool; - $this->addOnUnloadCallback(static function() use ($workerPool, $workerStartHook) : void{ - $workerPool->removeWorkerStartHook($workerStartHook); - }); } private function initRandomTickBlocksFromConfig(ServerConfigGroup $cfg) : void{ @@ -585,21 +585,6 @@ class World implements ChunkManager{ return $this->tickRateTime; } - public function registerGeneratorToWorker(int $worker) : void{ - $this->logger->debug("Registering generator on worker $worker"); - $this->workerPool->submitTaskToWorker(new GeneratorRegisterTask($this, $this->generator, $this->provider->getWorldData()->getGeneratorOptions()), $worker); - $this->generatorRegisteredWorkers[$worker] = true; - } - - public function unregisterGenerator() : void{ - foreach($this->workerPool->getRunningWorkers() as $i){ - if(isset($this->generatorRegisteredWorkers[$i])){ - $this->workerPool->submitTaskToWorker(new GeneratorUnregisterTask($this), $i); - } - } - $this->generatorRegisteredWorkers = []; - } - public function getServer() : Server{ return $this->server; } @@ -657,7 +642,7 @@ class World implements ChunkManager{ $this->save(); - $this->unregisterGenerator(); + $this->generatorExecutor->shutdown(); $this->provider->close(); $this->blockCache = []; @@ -3486,8 +3471,8 @@ class World implements ChunkManager{ $centerChunk = $this->loadChunk($chunkX, $chunkZ); $adjacentChunks = $this->getAdjacentChunks($chunkX, $chunkZ); - $task = new PopulationTask( - $this->worldId, + + $this->generatorExecutor->populate( $chunkX, $chunkZ, $centerChunk, @@ -3500,15 +3485,6 @@ class World implements ChunkManager{ $this->generateChunkCallback($chunkPopulationLockId, $chunkX, $chunkZ, $centerChunk, $adjacentChunks, $temporaryChunkLoader); } ); - $workerId = $this->workerPool->selectWorker(); - if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){ - $this->logger->debug("Selected worker $workerId previously had generator registered, but is now offline"); - unset($this->generatorRegisteredWorkers[$workerId]); - } - if(!isset($this->generatorRegisteredWorkers[$workerId])){ - $this->registerGeneratorToWorker($workerId); - } - $this->workerPool->submitTaskToWorker($task, $workerId); return $resolver->getPromise(); }finally{ diff --git a/src/world/generator/GeneratorManager.php b/src/world/generator/GeneratorManager.php index 291ea91de..a1b00480e 100644 --- a/src/world/generator/GeneratorManager.php +++ b/src/world/generator/GeneratorManager.php @@ -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); } /** diff --git a/src/world/generator/GeneratorManagerEntry.php b/src/world/generator/GeneratorManagerEntry.php index 256ed27d5..942f6ee79 100644 --- a/src/world/generator/GeneratorManagerEntry.php +++ b/src/world/generator/GeneratorManagerEntry.php @@ -31,12 +31,15 @@ final class GeneratorManagerEntry{ */ public function __construct( private string $generatorClass, - private \Closure $presetValidator + private \Closure $presetValidator, + private readonly bool $fast ){} /** @phpstan-return class-string */ public function getGeneratorClass() : string{ return $this->generatorClass; } + public function isFast() : bool{ return $this->fast; } + /** * @throws InvalidGeneratorOptionsException */ diff --git a/src/world/generator/PopulationTask.php b/src/world/generator/PopulationTask.php index a8366a306..971349a5b 100644 --- a/src/world/generator/PopulationTask.php +++ b/src/world/generator/PopulationTask.php @@ -27,11 +27,18 @@ use pocketmine\scheduler\AsyncTask; use pocketmine\utils\AssumptionFailedError; use pocketmine\world\format\Chunk; use pocketmine\world\format\io\FastChunkSerializer; +use pocketmine\world\generator\executor\ThreadLocalGeneratorContext; use function array_map; use function igbinary_serialize; use function igbinary_unserialize; /** + * @internal + * + * TODO: this should be moved to the executor namespace, but plugins have unfortunately used it directly due to the + * difficulty of regenerating chunks. This should be addressed in the future. + * For the remainder of PM5, we can't relocate this class. + * * @phpstan-type OnCompletion \Closure(Chunk $centerChunk, array $adjacentChunks) : void */ class PopulationTask extends AsyncTask{ diff --git a/src/world/generator/executor/AsyncGeneratorExecutor.php b/src/world/generator/executor/AsyncGeneratorExecutor.php new file mode 100644 index 000000000..d19b6e661 --- /dev/null +++ b/src/world/generator/executor/AsyncGeneratorExecutor.php @@ -0,0 +1,106 @@ + + */ + private array $generatorRegisteredWorkers = []; + + public function __construct( + \Logger $logger, + private readonly AsyncPool $workerPool, + private readonly GeneratorExecutorSetupParameters $setupParameters, + int $asyncContextId = null + ){ + $this->logger = new \PrefixedLogger($logger, "AsyncGeneratorExecutor"); + + //TODO: we only allow setting this for PM5 because of PopulationTask uses in plugins + $this->asyncContextId = $asyncContextId ?? self::$nextAsyncContextId++; + + $this->workerStartHook = function(int $workerId) : void{ + if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){ + $this->logger->debug("Worker $workerId with previously registered generator restarted, flagging as unregistered"); + unset($this->generatorRegisteredWorkers[$workerId]); + } + }; + $this->workerPool->addWorkerStartHook($this->workerStartHook); + } + + private function registerGeneratorToWorker(int $worker) : void{ + $this->logger->debug("Registering generator on worker $worker"); + $this->workerPool->submitTaskToWorker(new AsyncGeneratorRegisterTask($this->setupParameters, $this->asyncContextId), $worker); + $this->generatorRegisteredWorkers[$worker] = true; + } + + private function unregisterGenerator() : void{ + foreach($this->workerPool->getRunningWorkers() as $i){ + if(isset($this->generatorRegisteredWorkers[$i])){ + $this->workerPool->submitTaskToWorker(new AsyncGeneratorUnregisterTask($this->asyncContextId), $i); + } + } + $this->generatorRegisteredWorkers = []; + } + + public function populate(int $chunkX, int $chunkZ, ?Chunk $centerChunk, array $adjacentChunks, \Closure $onCompletion) : void{ + $task = new PopulationTask( + $this->asyncContextId, + $chunkX, + $chunkZ, + $centerChunk, + $adjacentChunks, + $onCompletion + ); + $workerId = $this->workerPool->selectWorker(); + if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){ + $this->logger->debug("Selected worker $workerId previously had generator registered, but is now offline"); + unset($this->generatorRegisteredWorkers[$workerId]); + } + if(!isset($this->generatorRegisteredWorkers[$workerId])){ + $this->registerGeneratorToWorker($workerId); + } + $this->workerPool->submitTaskToWorker($task, $workerId); + } + + public function shutdown() : void{ + $this->unregisterGenerator(); + $this->workerPool->removeWorkerStartHook($this->workerStartHook); + } +} diff --git a/src/world/generator/GeneratorRegisterTask.php b/src/world/generator/executor/AsyncGeneratorRegisterTask.php similarity index 54% rename from src/world/generator/GeneratorRegisterTask.php rename to src/world/generator/executor/AsyncGeneratorRegisterTask.php index e2e773a35..5bc67834d 100644 --- a/src/world/generator/GeneratorRegisterTask.php +++ b/src/world/generator/executor/AsyncGeneratorRegisterTask.php @@ -21,37 +21,20 @@ declare(strict_types=1); -namespace pocketmine\world\generator; +namespace pocketmine\world\generator\executor; use pocketmine\scheduler\AsyncTask; -use pocketmine\world\World; -class GeneratorRegisterTask extends AsyncTask{ - public int $seed; - public int $worldId; - public int $worldMinY; - public int $worldMaxY; +class AsyncGeneratorRegisterTask extends AsyncTask{ - /** - * @phpstan-param class-string $generatorClass - */ public function __construct( - World $world, - public string $generatorClass, - public string $generatorSettings - ){ - $this->seed = $world->getSeed(); - $this->worldId = $world->getId(); - $this->worldMinY = $world->getMinY(); - $this->worldMaxY = $world->getMaxY(); - } + private readonly GeneratorExecutorSetupParameters $setupParameters, + private readonly int $contextId + ){} public function onRun() : void{ - /** - * @var Generator $generator - * @see Generator::__construct() - */ - $generator = new $this->generatorClass($this->seed, $this->generatorSettings); - ThreadLocalGeneratorContext::register(new ThreadLocalGeneratorContext($generator, $this->worldMinY, $this->worldMaxY), $this->worldId); + $setupParameters = $this->setupParameters; + $generator = $setupParameters->createGenerator(); + ThreadLocalGeneratorContext::register(new ThreadLocalGeneratorContext($generator, $setupParameters->worldMinY, $setupParameters->worldMaxY), $this->contextId); } } diff --git a/src/world/generator/GeneratorUnregisterTask.php b/src/world/generator/executor/AsyncGeneratorUnregisterTask.php similarity index 74% rename from src/world/generator/GeneratorUnregisterTask.php rename to src/world/generator/executor/AsyncGeneratorUnregisterTask.php index 41b4cd808..c771903f5 100644 --- a/src/world/generator/GeneratorUnregisterTask.php +++ b/src/world/generator/executor/AsyncGeneratorUnregisterTask.php @@ -21,19 +21,16 @@ declare(strict_types=1); -namespace pocketmine\world\generator; +namespace pocketmine\world\generator\executor; use pocketmine\scheduler\AsyncTask; -use pocketmine\world\World; -class GeneratorUnregisterTask extends AsyncTask{ - public int $worldId; - - public function __construct(World $world){ - $this->worldId = $world->getId(); - } +class AsyncGeneratorUnregisterTask extends AsyncTask{ + public function __construct( + private readonly int $contextId + ){} public function onRun() : void{ - ThreadLocalGeneratorContext::unregister($this->worldId); + ThreadLocalGeneratorContext::unregister($this->contextId); } } diff --git a/src/world/generator/executor/GeneratorExecutor.php b/src/world/generator/executor/GeneratorExecutor.php new file mode 100644 index 000000000..d3f62d410 --- /dev/null +++ b/src/world/generator/executor/GeneratorExecutor.php @@ -0,0 +1,38 @@ + $adjacentChunks + * @phpstan-param \Closure(Chunk $centerChunk, array $adjacentChunks) : void $onCompletion + */ + public function populate(int $chunkX, int $chunkZ, ?Chunk $centerChunk, array $adjacentChunks, \Closure $onCompletion) : void; + + public function shutdown() : void; + +} diff --git a/src/world/generator/executor/GeneratorExecutorSetupParameters.php b/src/world/generator/executor/GeneratorExecutorSetupParameters.php new file mode 100644 index 000000000..b5fdb7bf9 --- /dev/null +++ b/src/world/generator/executor/GeneratorExecutorSetupParameters.php @@ -0,0 +1,50 @@ + $generatorClass + */ + public function __construct( + public readonly int $worldMinY, + public readonly int $worldMaxY, + public readonly int $generatorSeed, + public readonly string $generatorClass, + public readonly string $generatorSettings, + ){} + + public function createGenerator() : Generator{ + /** + * @var Generator $generator + * @see Generator::__construct() + */ + $generator = new $this->generatorClass($this->generatorSeed, $this->generatorSettings); + return $generator; + } +} diff --git a/src/world/generator/executor/SyncGeneratorExecutor.php b/src/world/generator/executor/SyncGeneratorExecutor.php new file mode 100644 index 000000000..79b5fdd00 --- /dev/null +++ b/src/world/generator/executor/SyncGeneratorExecutor.php @@ -0,0 +1,61 @@ +generator = $setupParameters->createGenerator(); + $this->worldMinY = $setupParameters->worldMinY; + $this->worldMaxY = $setupParameters->worldMaxY; + } + + public function populate(int $chunkX, int $chunkZ, ?Chunk $centerChunk, array $adjacentChunks, \Closure $onCompletion) : void{ + [$centerChunk, $adjacentChunks] = PopulationUtils::populateChunkWithAdjacents( + $this->worldMinY, + $this->worldMaxY, + $this->generator, + $chunkX, + $chunkZ, + $centerChunk, + $adjacentChunks + ); + + $onCompletion($centerChunk, $adjacentChunks); + } + + public function shutdown() : void{ + //NOOP + } +} diff --git a/src/world/generator/ThreadLocalGeneratorContext.php b/src/world/generator/executor/ThreadLocalGeneratorContext.php similarity index 94% rename from src/world/generator/ThreadLocalGeneratorContext.php rename to src/world/generator/executor/ThreadLocalGeneratorContext.php index bcf99882b..bea8bb032 100644 --- a/src/world/generator/ThreadLocalGeneratorContext.php +++ b/src/world/generator/executor/ThreadLocalGeneratorContext.php @@ -21,7 +21,9 @@ declare(strict_types=1); -namespace pocketmine\world\generator; +namespace pocketmine\world\generator\executor; + +use pocketmine\world\generator\Generator; /** * Manages thread-local caches for generators and the things needed to support them diff --git a/tests/phpstan/configs/actual-problems.neon b/tests/phpstan/configs/actual-problems.neon index d3adde422..2030a0dad 100644 --- a/tests/phpstan/configs/actual-problems.neon +++ b/tests/phpstan/configs/actual-problems.neon @@ -1272,18 +1272,18 @@ parameters: count: 1 path: ../../../src/world/format/io/region/RegionLoader.php - - - message: '#^Dynamic new is not allowed\.$#' - identifier: pocketmine.new.dynamic - count: 1 - path: ../../../src/world/generator/GeneratorRegisterTask.php - - message: '#^Method pocketmine\\world\\generator\\biome\\BiomeSelector\:\:pickBiome\(\) should return pocketmine\\world\\biome\\Biome but returns pocketmine\\world\\biome\\Biome\|null\.$#' identifier: return.type count: 1 path: ../../../src/world/generator/biome/BiomeSelector.php + - + message: '#^Dynamic new is not allowed\.$#' + identifier: pocketmine.new.dynamic + count: 1 + path: ../../../src/world/generator/executor/GeneratorExecutorSetupParameters.php + - message: '#^Cannot call method getBiomeId\(\) on pocketmine\\world\\format\\Chunk\|null\.$#' identifier: method.nonObject From bf33a625c93469b4f158e292c5868a161603daf0 Mon Sep 17 00:00:00 2001 From: Adam <116978956+b1zeyofficial@users.noreply.github.com> Date: Wed, 28 May 2025 02:57:28 +0600 Subject: [PATCH 2/2] Implemented Respawn Anchor (#6646) PlayerRespawnAnchorUseEvent is also added with options SET_SPAWN and EXPLODE, which allows plugins to customise the outcome of using the anchor in PM, which currently doesn't support dimensions. The event is also cancellable. --- src/block/BlockTypeIds.php | 3 +- src/block/RespawnAnchor.php | 123 +++++++++++++++++ src/block/VanillaBlocks.php | 3 + .../convert/BlockObjectToStateSerializer.php | 5 + .../BlockStateToObjectDeserializer.php | 4 + src/entity/object/EndCrystal.php | 2 +- src/entity/object/PrimedTNT.php | 2 +- src/event/block/BlockExplodeEvent.php | 122 +++++++++++++++++ src/event/block/BlockPreExplodeEvent.php | 129 ++++++++++++++++++ src/event/entity/EntityExplodeEvent.php | 25 +++- src/event/entity/EntityPreExplodeEvent.php | 50 ++++++- .../player/PlayerRespawnAnchorUseEvent.php | 56 ++++++++ src/item/StringToItemParser.php | 1 + src/player/Player.php | 17 +++ src/world/Explosion.php | 77 +++++++++-- src/world/sound/RespawnAnchorChargeSound.php | 35 +++++ src/world/sound/RespawnAnchorDepleteSound.php | 35 +++++ .../sound/RespawnAnchorSetSpawnSound.php | 35 +++++ .../block_factory_consistency_check.json | 1 + 19 files changed, 711 insertions(+), 14 deletions(-) create mode 100644 src/block/RespawnAnchor.php create mode 100644 src/event/block/BlockExplodeEvent.php create mode 100644 src/event/block/BlockPreExplodeEvent.php create mode 100644 src/event/player/PlayerRespawnAnchorUseEvent.php create mode 100644 src/world/sound/RespawnAnchorChargeSound.php create mode 100644 src/world/sound/RespawnAnchorDepleteSound.php create mode 100644 src/world/sound/RespawnAnchorSetSpawnSound.php diff --git a/src/block/BlockTypeIds.php b/src/block/BlockTypeIds.php index c440cefdc..4af1894bd 100644 --- a/src/block/BlockTypeIds.php +++ b/src/block/BlockTypeIds.php @@ -786,8 +786,9 @@ final class BlockTypeIds{ public const RESIN_BRICKS = 10756; public const RESIN_CLUMP = 10757; public const CHISELED_RESIN_BRICKS = 10758; + public const RESPAWN_ANCHOR = 10759; - public const FIRST_UNUSED_BLOCK_ID = 10759; + public const FIRST_UNUSED_BLOCK_ID = 10760; private static int $nextDynamicId = self::FIRST_UNUSED_BLOCK_ID; diff --git a/src/block/RespawnAnchor.php b/src/block/RespawnAnchor.php new file mode 100644 index 000000000..f702d240d --- /dev/null +++ b/src/block/RespawnAnchor.php @@ -0,0 +1,123 @@ +boundedIntAuto(self::MIN_CHARGES, self::MAX_CHARGES, $this->charges); + } + + public function getCharges() : int{ + return $this->charges; + } + + /** @return $this */ + public function setCharges(int $charges) : self{ + if($charges < self::MIN_CHARGES || $charges > self::MAX_CHARGES){ + throw new \InvalidArgumentException("Charges must be between " . self::MIN_CHARGES . " and " . self::MAX_CHARGES . ", given: $charges"); + } + $this->charges = $charges; + return $this; + } + + public function getLightLevel() : int{ + return $this->charges > 0 ? ($this->charges * 4) - 1 : 0; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ + if($item->getTypeId() === ItemTypeIds::fromBlockTypeId(BlockTypeIds::GLOWSTONE) && $this->charges < self::MAX_CHARGES){ + $this->position->getWorld()->setBlock($this->position, $this->setCharges($this->charges + 1)); + $this->position->getWorld()->addSound($this->position, new RespawnAnchorChargeSound()); + return true; + } + + if($this->charges > self::MIN_CHARGES){ + if($player === null){ + return false; + } + + $ev = new PlayerRespawnAnchorUseEvent($player, $this, PlayerRespawnAnchorUseEvent::ACTION_EXPLODE); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + + switch($ev->getAction()){ + case PlayerRespawnAnchorUseEvent::ACTION_EXPLODE: + $this->explode($player); + return false; + + case PlayerRespawnAnchorUseEvent::ACTION_SET_SPAWN: + if($player->getSpawn() !== null && $player->getSpawn()->equals($this->position)){ + return true; + } + + $player->setSpawn($this->position); + $this->position->getWorld()->addSound($this->position, new RespawnAnchorSetSpawnSound()); + $player->sendMessage(KnownTranslationFactory::tile_respawn_anchor_respawnSet()->prefix(TextFormat::GRAY)); + return true; + } + } + return false; + } + + private function explode(?Player $player) : void{ + $ev = new BlockPreExplodeEvent($this, 5, $player); + $ev->setIncendiary(true); + + $ev->call(); + if($ev->isCancelled()){ + return; + } + + $this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR()); + + $explosion = new Explosion(Position::fromObject($this->position->add(0.5, 0.5, 0.5), $this->position->getWorld()), $ev->getRadius(), $this); + $explosion->setFireChance($ev->getFireChance()); + + if($ev->isBlockBreaking()){ + $explosion->explodeA(); + } + $explosion->explodeB(); + } +} diff --git a/src/block/VanillaBlocks.php b/src/block/VanillaBlocks.php index 0a6d4b31c..54ec27fc2 100644 --- a/src/block/VanillaBlocks.php +++ b/src/block/VanillaBlocks.php @@ -694,6 +694,7 @@ use function strtolower; * @method static Stair RESIN_BRICK_STAIRS() * @method static Wall RESIN_BRICK_WALL() * @method static ResinClump RESIN_CLUMP() + * @method static RespawnAnchor RESPAWN_ANCHOR() * @method static DoublePlant ROSE_BUSH() * @method static Sand SAND() * @method static Opaque SANDSTONE() @@ -1647,6 +1648,8 @@ final class VanillaBlocks{ self::register("warped_roots", fn(BID $id) => new NetherRoots($id, "Warped Roots", $netherRootsInfo)); self::register("chain", fn(BID $id) => new Chain($id, "Chain", new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD, 30.0)))); + + self::register("respawn_anchor", fn(BID $id) => new RespawnAnchor($id, "Respawn Anchor", new Info(BreakInfo::pickaxe(50.0, ToolTier::DIAMOND, 6000.0)))); } private static function registerBlocksR17() : void{ diff --git a/src/data/bedrock/block/convert/BlockObjectToStateSerializer.php b/src/data/bedrock/block/convert/BlockObjectToStateSerializer.php index 45784d409..27d550f13 100644 --- a/src/data/bedrock/block/convert/BlockObjectToStateSerializer.php +++ b/src/data/bedrock/block/convert/BlockObjectToStateSerializer.php @@ -122,6 +122,7 @@ use pocketmine\block\RedstoneRepeater; use pocketmine\block\RedstoneTorch; use pocketmine\block\RedstoneWire; use pocketmine\block\ResinClump; +use pocketmine\block\RespawnAnchor; use pocketmine\block\RuntimeBlockStateRegistry; use pocketmine\block\Sapling; use pocketmine\block\SeaPickle; @@ -1754,6 +1755,10 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{ return Writer::create(Ids::RESIN_CLUMP) ->writeFacingFlags($block->getFaces()); }); + $this->map(Blocks::RESPAWN_ANCHOR(), function(RespawnAnchor $block) : Writer{ + return Writer::create(Ids::RESPAWN_ANCHOR) + ->writeInt(StateNames::RESPAWN_ANCHOR_CHARGE, $block->getCharges()); + }); $this->map(Blocks::ROSE_BUSH(), fn(DoublePlant $block) => Helper::encodeDoublePlant($block, Writer::create(Ids::ROSE_BUSH))); $this->mapSlab(Blocks::SANDSTONE_SLAB(), Ids::SANDSTONE_SLAB, Ids::SANDSTONE_DOUBLE_SLAB); $this->mapStairs(Blocks::SANDSTONE_STAIRS(), Ids::SANDSTONE_STAIRS); diff --git a/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php b/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php index ed45a47d3..c55fde77a 100644 --- a/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php +++ b/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php @@ -1717,6 +1717,10 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{ $this->mapStairs(Ids::RESIN_BRICK_STAIRS, fn() => Blocks::RESIN_BRICK_STAIRS()); $this->map(Ids::RESIN_BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::RESIN_BRICK_WALL(), $in)); $this->map(Ids::RESIN_CLUMP, fn(Reader $in) => Blocks::RESIN_CLUMP()->setFaces($in->readFacingFlags())); + $this->map(Ids::RESPAWN_ANCHOR, function(Reader $in) : Block{ + return Blocks::RESPAWN_ANCHOR() + ->setCharges($in->readBoundedInt(StateNames::RESPAWN_ANCHOR_CHARGE, 0, 4)); + }); $this->mapSlab(Ids::SANDSTONE_SLAB, Ids::SANDSTONE_DOUBLE_SLAB, fn() => Blocks::SANDSTONE_SLAB()); $this->mapStairs(Ids::SANDSTONE_STAIRS, fn() => Blocks::SANDSTONE_STAIRS()); $this->map(Ids::SANDSTONE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::SANDSTONE_WALL(), $in)); diff --git a/src/entity/object/EndCrystal.php b/src/entity/object/EndCrystal.php index afaeb6769..74c7664bf 100644 --- a/src/entity/object/EndCrystal.php +++ b/src/entity/object/EndCrystal.php @@ -129,7 +129,7 @@ class EndCrystal extends Entity implements Explosive{ $ev = new EntityPreExplodeEvent($this, 6); $ev->call(); if(!$ev->isCancelled()){ - $explosion = new Explosion($this->getPosition(), $ev->getRadius(), $this); + $explosion = new Explosion($this->getPosition(), $ev->getRadius(), $this, $ev->getFireChance()); if($ev->isBlockBreaking()){ $explosion->explodeA(); } diff --git a/src/entity/object/PrimedTNT.php b/src/entity/object/PrimedTNT.php index af3c97922..f3f299d70 100644 --- a/src/entity/object/PrimedTNT.php +++ b/src/entity/object/PrimedTNT.php @@ -121,7 +121,7 @@ class PrimedTNT extends Entity implements Explosive{ $ev->call(); if(!$ev->isCancelled()){ //TODO: deal with underwater TNT (underwater TNT treats water as if it has a blast resistance of 0) - $explosion = new Explosion(Position::fromObject($this->location->add(0, $this->size->getHeight() / 2, 0), $this->getWorld()), $ev->getRadius(), $this); + $explosion = new Explosion(Position::fromObject($this->location->add(0, $this->size->getHeight() / 2, 0), $this->getWorld()), $ev->getRadius(), $this, $ev->getFireChance()); if($ev->isBlockBreaking()){ $explosion->explodeA(); } diff --git a/src/event/block/BlockExplodeEvent.php b/src/event/block/BlockExplodeEvent.php new file mode 100644 index 000000000..a8501d475 --- /dev/null +++ b/src/event/block/BlockExplodeEvent.php @@ -0,0 +1,122 @@ + 100.0){ + throw new \InvalidArgumentException("Yield must be in range 0.0 - 100.0"); + } + } + + public function getPosition() : Position{ + return $this->position; + } + + /** + * Returns the percentage chance of drops from each block destroyed by the explosion. + * + * @return float 0-100 + */ + public function getYield() : float{ + return $this->yield; + } + + /** + * Sets the percentage chance of drops from each block destroyed by the explosion. + * + * @param float $yield 0-100 + */ + public function setYield(float $yield) : void{ + Utils::checkFloatNotInfOrNaN("yield", $yield); + if($yield < 0.0 || $yield > 100.0){ + throw new \InvalidArgumentException("Yield must be in range 0.0 - 100.0"); + } + $this->yield = $yield; + } + + /** + * Returns a list of blocks destroyed by the explosion. + * + * @return Block[] + */ + public function getAffectedBlocks() : array{ + return $this->blocks; + } + + /** + * Sets the blocks destroyed by the explosion. + * + * @param Block[] $blocks + */ + public function setAffectedBlocks(array $blocks) : void{ + Utils::validateArrayValueType($blocks, fn(Block $block) => null); + $this->blocks = $blocks; + } + + /** + * Returns a list of affected blocks that will be replaced by fire. + * + * @return Block[] + */ + public function getIgnitions() : array{ + return $this->ignitions; + } + + /** + * Set the list of blocks that will be replaced by fire. + * + * @param Block[] $ignitions + */ + public function setIgnitions(array $ignitions) : void{ + Utils::validateArrayValueType($ignitions, fn(Block $block) => null); + $this->ignitions = $ignitions; + } +} diff --git a/src/event/block/BlockPreExplodeEvent.php b/src/event/block/BlockPreExplodeEvent.php new file mode 100644 index 000000000..f41cb8a63 --- /dev/null +++ b/src/event/block/BlockPreExplodeEvent.php @@ -0,0 +1,129 @@ + 1.0){ + throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1."); + } + parent::__construct($block); + } + + public function getRadius() : float{ + return $this->radius; + } + + public function setRadius(float $radius) : void{ + Utils::checkFloatNotInfOrNaN("radius", $radius); + if($radius <= 0){ + throw new \InvalidArgumentException("Explosion radius must be positive"); + } + $this->radius = $radius; + } + + public function isBlockBreaking() : bool{ + return $this->blockBreaking; + } + + public function setBlockBreaking(bool $affectsBlocks) : void{ + $this->blockBreaking = $affectsBlocks; + } + + /** + * Returns whether the explosion will create a fire. + */ + public function isIncendiary() : bool{ + return $this->fireChance > 0; + } + + /** + * Sets whether the explosion will create a fire by filling fireChance with default values. + * + * If $incendiary is true, the fire chance will be filled only if explosion isn't currently creating a fire (if fire chance is 0). + */ + public function setIncendiary(bool $incendiary) : void{ + if(!$incendiary){ + $this->fireChance = 0; + }elseif($this->fireChance <= 0){ + $this->fireChance = Explosion::DEFAULT_FIRE_CHANCE; + } + } + + /** + * Returns a chance between 0 and 1 of creating a fire. + */ + public function getFireChance() : float{ + return $this->fireChance; + } + + /** + * Sets a chance between 0 and 1 of creating a fire. + * For example, if the chance is 1/3, then that amount of affected blocks will be ignited. + * + * @param float $fireChance 0 ... 1 + */ + public function setFireChance(float $fireChance) : void{ + Utils::checkFloatNotInfOrNaN("fireChance", $fireChance); + if($fireChance < 0.0 || $fireChance > 1.0){ + throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1."); + } + $this->fireChance = $fireChance; + } + + /** + * Returns the player who triggered the block explosion. + * Returns null if the block was exploded by other means. + */ + public function getPlayer() : ?Player{ + return $this->player; + } +} diff --git a/src/event/entity/EntityExplodeEvent.php b/src/event/entity/EntityExplodeEvent.php index 0a0e4f696..c1750ccb3 100644 --- a/src/event/entity/EntityExplodeEvent.php +++ b/src/event/entity/EntityExplodeEvent.php @@ -43,13 +43,15 @@ class EntityExplodeEvent extends EntityEvent implements Cancellable{ /** * @param Block[] $blocks - * @param float $yield 0-100 + * @param float $yield 0-100 + * @param Block[] $ignitions */ public function __construct( Entity $entity, protected Position $position, protected array $blocks, - protected float $yield + protected float $yield, + private array $ignitions ){ $this->entity = $entity; if($yield < 0.0 || $yield > 100.0){ @@ -98,4 +100,23 @@ class EntityExplodeEvent extends EntityEvent implements Cancellable{ } $this->yield = $yield; } + + /** + * Set the list of blocks that will be replaced by fire. + * + * @param Block[] $ignitions + */ + public function setIgnitions(array $ignitions) : void{ + Utils::validateArrayValueType($ignitions, fn(Block $block) => null); + $this->ignitions = $ignitions; + } + + /** + * Returns a list of affected blocks that will be replaced by fire. + * + * @return Block[] + */ + public function getIgnitions() : array{ + return $this->ignitions; + } } diff --git a/src/event/entity/EntityPreExplodeEvent.php b/src/event/entity/EntityPreExplodeEvent.php index f02a85ecd..c88e83304 100644 --- a/src/event/entity/EntityPreExplodeEvent.php +++ b/src/event/entity/EntityPreExplodeEvent.php @@ -26,6 +26,8 @@ namespace pocketmine\event\entity; use pocketmine\entity\Entity; use pocketmine\event\Cancellable; use pocketmine\event\CancellableTrait; +use pocketmine\utils\Utils; +use pocketmine\world\Explosion; /** * Called when an entity decides to explode, before the explosion's impact is calculated. @@ -42,11 +44,16 @@ class EntityPreExplodeEvent extends EntityEvent implements Cancellable{ public function __construct( Entity $entity, - protected float $radius + protected float $radius, + private float $fireChance = 0.0, ){ if($radius <= 0){ throw new \InvalidArgumentException("Explosion radius must be positive"); } + Utils::checkFloatNotInfOrNaN("fireChance", $fireChance); + if($fireChance < 0.0 || $fireChance > 1.0){ + throw new \InvalidArgumentException("Fire chance must be between 0 and 1."); + } $this->entity = $entity; } @@ -61,6 +68,47 @@ class EntityPreExplodeEvent extends EntityEvent implements Cancellable{ $this->radius = $radius; } + /** + * Returns whether the explosion will create a fire. + */ + public function isIncendiary() : bool{ + return $this->fireChance > 0; + } + + /** + * Sets whether the explosion will create a fire by filling fireChance with default values. + * + * If $incendiary is true, the fire chance will be filled only if explosion isn't currently creating a fire (if fire chance is 0). + */ + public function setIncendiary(bool $incendiary) : void{ + if(!$incendiary){ + $this->fireChance = 0; + }elseif($this->fireChance <= 0){ + $this->fireChance = Explosion::DEFAULT_FIRE_CHANCE; + } + } + + /** + * Returns a chance between 0 and 1 of creating a fire. + */ + public function getFireChance() : float{ + return $this->fireChance; + } + + /** + * Sets a chance between 0 and 1 of creating a fire. + * For example, if the chance is 1/3, then that amount of affected blocks will be ignited. + * + * @param float $fireChance 0 ... 1 + */ + public function setFireChance(float $fireChance) : void{ + Utils::checkFloatNotInfOrNaN("fireChance", $fireChance); + if($fireChance < 0.0 || $fireChance > 1.0){ + throw new \InvalidArgumentException("Fire chance must be between 0 and 1."); + } + $this->fireChance = $fireChance; + } + public function isBlockBreaking() : bool{ return $this->blockBreaking; } diff --git a/src/event/player/PlayerRespawnAnchorUseEvent.php b/src/event/player/PlayerRespawnAnchorUseEvent.php new file mode 100644 index 000000000..be7697f11 --- /dev/null +++ b/src/event/player/PlayerRespawnAnchorUseEvent.php @@ -0,0 +1,56 @@ +player = $player; + } + + public function getBlock() : Block{ + return $this->block; + } + + public function getAction() : int{ + return $this->action; + } + + public function setAction(int $action) : void{ + $this->action = $action; + } +} diff --git a/src/item/StringToItemParser.php b/src/item/StringToItemParser.php index a3bd7b872..7a90babed 100644 --- a/src/item/StringToItemParser.php +++ b/src/item/StringToItemParser.php @@ -993,6 +993,7 @@ final class StringToItemParser extends StringToTParser{ $result->registerBlock("resin_brick_wall", fn() => Blocks::RESIN_BRICK_WALL()); $result->registerBlock("resin_bricks", fn() => Blocks::RESIN_BRICKS()); $result->registerBlock("resin_clump", fn() => Blocks::RESIN_CLUMP()); + $result->registerBlock("respawn_anchor", fn() => Blocks::RESPAWN_ANCHOR()); $result->registerBlock("rooted_dirt", fn() => Blocks::DIRT()->setDirtType(DirtType::ROOTED)); $result->registerBlock("rose", fn() => Blocks::POPPY()); $result->registerBlock("rose_bush", fn() => Blocks::ROSE_BUSH()); diff --git a/src/player/Player.php b/src/player/Player.php index 3c494b980..6429d1281 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -26,6 +26,7 @@ namespace pocketmine\player; use pocketmine\block\BaseSign; use pocketmine\block\Bed; use pocketmine\block\BlockTypeTags; +use pocketmine\block\RespawnAnchor; use pocketmine\block\UnknownBlock; use pocketmine\block\VanillaBlocks; use pocketmine\command\CommandSender; @@ -136,6 +137,7 @@ use pocketmine\world\sound\EntityAttackNoDamageSound; use pocketmine\world\sound\EntityAttackSound; use pocketmine\world\sound\FireExtinguishSound; use pocketmine\world\sound\ItemBreakSound; +use pocketmine\world\sound\RespawnAnchorDepleteSound; use pocketmine\world\sound\Sound; use pocketmine\world\World; use pocketmine\YmlServerProperties; @@ -2538,6 +2540,21 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ } $this->logger->debug("Respawn position located, completing respawn"); $ev = new PlayerRespawnEvent($this, $safeSpawn); + $spawnPosition = $ev->getRespawnPosition(); + $spawnBlock = $spawnPosition->getWorld()->getBlock($spawnPosition); + if($spawnBlock instanceof RespawnAnchor){ + if($spawnBlock->getCharges() > 0){ + $spawnPosition->getWorld()->setBlock($spawnPosition, $spawnBlock->setCharges($spawnBlock->getCharges() - 1)); + $spawnPosition->getWorld()->addSound($spawnPosition, new RespawnAnchorDepleteSound()); + }else{ + $defaultSpawn = $this->server->getWorldManager()->getDefaultWorld()?->getSpawnLocation(); + if($defaultSpawn !== null){ + $this->setSpawn($defaultSpawn); + $ev->setRespawnPosition($defaultSpawn); + $this->sendMessage(KnownTranslationFactory::tile_respawn_anchor_notValid()->prefix(TextFormat::GRAY)); + } + } + } $ev->call(); $realSpawn = Position::fromObject($ev->getRespawnPosition()->add(0.5, 0, 0.5), $ev->getRespawnPosition()->getWorld()); diff --git a/src/world/Explosion.php b/src/world/Explosion.php index 601f9109e..9e83d06be 100644 --- a/src/world/Explosion.php +++ b/src/world/Explosion.php @@ -26,16 +26,20 @@ namespace pocketmine\world; use pocketmine\block\Block; use pocketmine\block\RuntimeBlockStateRegistry; use pocketmine\block\TNT; +use pocketmine\block\utils\SupportType; use pocketmine\block\VanillaBlocks; use pocketmine\entity\Entity; +use pocketmine\event\block\BlockExplodeEvent; use pocketmine\event\entity\EntityDamageByBlockEvent; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; use pocketmine\event\entity\EntityExplodeEvent; use pocketmine\item\VanillaItems; use pocketmine\math\AxisAlignedBB; +use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\utils\AssumptionFailedError; +use pocketmine\utils\Utils; use pocketmine\world\format\SubChunk; use pocketmine\world\particle\HugeExplodeSeedParticle; use pocketmine\world\sound\ExplodeSound; @@ -48,25 +52,36 @@ use function mt_rand; use function sqrt; class Explosion{ + public const DEFAULT_FIRE_CHANCE = 1.0 / 3.0; + private int $rays = 16; public World $world; - /** @var Block[] */ + /** + * @var Block[] + * @phpstan-var array + */ public array $affectedBlocks = []; public float $stepLen = 0.3; + /** @var Block[] */ + private array $fireIgnitions = []; private SubChunkExplorer $subChunkExplorer; public function __construct( public Position $source, public float $radius, - private Entity|Block|null $what = null + private Entity|Block|null $what = null, + private float $fireChance = 0.0 ){ if(!$this->source->isValid()){ throw new \InvalidArgumentException("Position does not have a valid world"); } $this->world = $this->source->getWorld(); - + Utils::checkFloatNotInfOrNaN("fireChance", $fireChance); + if($fireChance < 0.0 || $fireChance > 1.0){ + throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1."); + } if($radius <= 0){ throw new \InvalidArgumentException("Explosion radius must be greater than 0, got $radius"); } @@ -85,6 +100,7 @@ class Explosion{ $blockFactory = RuntimeBlockStateRegistry::getInstance(); $mRays = $this->rays - 1; + $incendiary = $this->fireChance > 0; for($i = 0; $i < $this->rays; ++$i){ for($j = 0; $j < $this->rays; ++$j){ for($k = 0; $k < $this->rays; ++$k){ @@ -127,7 +143,12 @@ class Explosion{ $_block = $this->world->getBlockAt($vBlockX, $vBlockY, $vBlockZ, true, false); foreach($_block->getAffectedBlocks() as $_affectedBlock){ $_affectedBlockPos = $_affectedBlock->getPosition(); - $this->affectedBlocks[World::blockHash($_affectedBlockPos->x, $_affectedBlockPos->y, $_affectedBlockPos->z)] = $_affectedBlock; + $posHash = World::blockHash($_affectedBlockPos->x, $_affectedBlockPos->y, $_affectedBlockPos->z); + $this->affectedBlocks[$posHash] = $_affectedBlock; + + if($incendiary && Utils::getRandomFloat() <= $this->fireChance){ + $this->fireIgnitions[$posHash] = $_affectedBlock; + } } } } @@ -150,13 +171,32 @@ class Explosion{ $yield = min(100, (1 / $this->radius) * 100); if($this->what instanceof Entity){ - $ev = new EntityExplodeEvent($this->what, $this->source, $this->affectedBlocks, $yield); + $ev = new EntityExplodeEvent($this->what, $this->source, $this->affectedBlocks, $yield, $this->fireIgnitions); + + $ev->call(); + if($ev->isCancelled()){ + return false; + } + + $yield = $ev->getYield(); + $this->affectedBlocks = $ev->getBlockList(); + $this->fireIgnitions = $ev->getIgnitions(); + }elseif($this->what instanceof Block){ + $ev = new BlockExplodeEvent( + $this->what, + $this->source, + $this->affectedBlocks, + $yield, + $this->fireIgnitions, + ); + $ev->call(); if($ev->isCancelled()){ return false; }else{ $yield = $ev->getYield(); - $this->affectedBlocks = $ev->getBlockList(); + $this->affectedBlocks = $ev->getAffectedBlocks(); + $this->fireIgnitions = $ev->getIgnitions(); } } @@ -198,8 +238,9 @@ class Explosion{ $air = VanillaItems::AIR(); $airBlock = VanillaBlocks::AIR(); + $fireBlock = VanillaBlocks::FIRE(); - foreach($this->affectedBlocks as $block){ + foreach($this->affectedBlocks as $hash => $block){ $pos = $block->getPosition(); if($block instanceof TNT){ $block->ignite(mt_rand(10, 30)); @@ -212,7 +253,13 @@ class Explosion{ if(($t = $this->world->getTileAt($pos->x, $pos->y, $pos->z)) !== null){ $t->onBlockDestroyed(); //needed to create drops for inventories } - $this->world->setBlockAt($pos->x, $pos->y, $pos->z, $airBlock); + $targetBlock = + isset($this->fireIgnitions[$hash]) && + $block->getSide(Facing::DOWN)->getSupportType(Facing::UP) === SupportType::FULL ? + $fireBlock : + $airBlock; + + $this->world->setBlockAt($pos->x, $pos->y, $pos->z, $targetBlock); } } @@ -221,4 +268,18 @@ class Explosion{ return true; } + + /** + * Sets a chance between 0 and 1 of creating a fire. + * For example, if the chance is 1/3, then that amount of affected blocks will be ignited. + * + * @param float $fireChance 0 ... 1 + */ + public function setFireChance(float $fireChance) : void{ + Utils::checkFloatNotInfOrNaN("fireChance", $fireChance); + if($fireChance < 0.0 || $fireChance > 1.0){ + throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1."); + } + $this->fireChance = $fireChance; + } } diff --git a/src/world/sound/RespawnAnchorChargeSound.php b/src/world/sound/RespawnAnchorChargeSound.php new file mode 100644 index 000000000..5a5731262 --- /dev/null +++ b/src/world/sound/RespawnAnchorChargeSound.php @@ -0,0 +1,35 @@ +