diff --git a/composer.json b/composer.json index 14be6f0d2e8..72e75e7be56 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "pocketmine/pocketmine-mp", + "name": "plutonium/pocketmine-mp", "description": "A server software for Minecraft: Bedrock Edition written in PHP", "type": "project", "homepage": "https://pmmp.io", diff --git a/src/Server.php b/src/Server.php index a34349bb5cd..8e4a812b924 100644 --- a/src/Server.php +++ b/src/Server.php @@ -38,8 +38,11 @@ use pocketmine\crash\CrashDumpRenderer; use pocketmine\entity\EntityDataHelper; use pocketmine\entity\Location; +use pocketmine\event\AsyncEvent; use pocketmine\event\HandlerListManager; +use pocketmine\event\player\PlayerCreationAsyncEvent; use pocketmine\event\player\PlayerCreationEvent; +use pocketmine\event\player\PlayerDataSaveAsyncEvent; use pocketmine\event\player\PlayerDataSaveEvent; use pocketmine\event\player\PlayerLoginEvent; use pocketmine\event\server\CommandEvent; @@ -530,6 +533,31 @@ public function getOfflinePlayerData(string $name) : ?CompoundTag{ }); } + /** + * @return Promise + */ + public function saveOfflinePlayerDataAsync(string $name, CompoundTag $nbtTag) : Promise{ + $ev = new PlayerDataSaveAsyncEvent($nbtTag, $name, $this->getPlayerExact($name)); + if(!$this->shouldSavePlayerData()){ + $ev->cancel(); + } + $resolver = new PromiseResolver(); + + $ev->call()->onCompletion( + function (PlayerDataSaveAsyncEvent $event) use ($name, $resolver) : void{ + if($event->isCancelled()){ + $resolver->reject(); + return; + } + $this->saveOfflinePlayerData($name, $event->getSaveData()); + $resolver->resolve(null); + }, + fn() => $this->logger->debug("Cancelled saving player data for $name") + ); + + return $resolver->getPromise(); + } + public function saveOfflinePlayerData(string $name, CompoundTag $nbtTag) : void{ $ev = new PlayerDataSaveEvent($nbtTag, $name, $this->getPlayerExact($name)); if(!$this->shouldSavePlayerData()){ @@ -554,7 +582,29 @@ public function saveOfflinePlayerData(string $name, CompoundTag $nbtTag) : void{ * @phpstan-return Promise */ public function createPlayer(NetworkSession $session, PlayerInfo $playerInfo, bool $authenticated, ?CompoundTag $offlinePlayerData) : Promise{ + $globalResolver = new PromiseResolver(); + + $evAsync = new PlayerCreationAsyncEvent($session); + $evAsync->call()->onCompletion( + fn(PlayerCreationAsyncEvent $event) => + $this->onCreatePlayer($event, $playerInfo, $authenticated, $offlinePlayerData)->onCompletion( + fn(Player $player) => $globalResolver->resolve($player), + fn() => $globalResolver->reject() + ), + fn() => $globalResolver->reject() + ); + + return $globalResolver->getPromise(); + } + + /** + * @phpstan-return Promise + */ + private function onCreatePlayer(PlayerCreationAsyncEvent $event, PlayerInfo $playerInfo, bool $authenticated, ?CompoundTag $offlinePlayerData) : Promise{ + $session = $event->getNetworkSession(); $ev = new PlayerCreationEvent($session); + $ev->setBaseClass($event->getBaseClass()); + $ev->setPlayerClass($event->getPlayerClass()); $ev->call(); $class = $ev->getPlayerClass(); diff --git a/src/VersionInfo.php b/src/VersionInfo.php index a7d4111afda..21680dd5d34 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -31,8 +31,8 @@ final class VersionInfo{ public const NAME = "PocketMine-MP"; - public const BASE_VERSION = "5.15.1"; - public const IS_DEVELOPMENT_BUILD = true; + public const BASE_VERSION = "5.100.1"; + public const IS_DEVELOPMENT_BUILD = false; public const BUILD_CHANNEL = "stable"; /** diff --git a/src/command/defaults/SaveCommand.php b/src/command/defaults/SaveCommand.php index 4e406e6a3ce..0b7b76dc8a2 100644 --- a/src/command/defaults/SaveCommand.php +++ b/src/command/defaults/SaveCommand.php @@ -27,6 +27,8 @@ use pocketmine\command\CommandSender; use pocketmine\lang\KnownTranslationFactory; use pocketmine\permission\DefaultPermissionNames; +use pocketmine\promise\Promise; +use pocketmine\promise\PromiseResolver; use function microtime; use function round; @@ -44,15 +46,32 @@ public function execute(CommandSender $sender, string $commandLabel, array $args Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_save_start()); $start = microtime(true); + $promises = []; foreach($sender->getServer()->getOnlinePlayers() as $player){ - $player->save(); + $promises[] = $player->saveAsync(); } - foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){ - $world->save(true); + $resolver = new PromiseResolver(); + + if(count($promises) === 0){ + $resolver->resolve(null); + } else { + Promise::all($promises)->onCompletion( + fn () => $resolver->resolve(null), + fn () => $resolver->reject() + ); } - Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_save_success((string) round(microtime(true) - $start, 3))); + $resolver->getPromise()->onCompletion( + function () use ($sender, $start) : void { + foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){ + $world->save(true); + } + + Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_save_success((string) round(microtime(true) - $start, 3))); + }, + fn() => Command::broadcastCommandMessage($sender, "§cUnable to save the server") + ); return true; } diff --git a/src/event/AsyncEvent.php b/src/event/AsyncEvent.php new file mode 100644 index 00000000000..c3d2ef9d73f --- /dev/null +++ b/src/event/AsyncEvent.php @@ -0,0 +1,162 @@ +> $promises */ + private ObjectSet $promises; + /** @var array, int> $delegatesCallDepth */ + private static array $delegatesCallDepth = []; + private const MAX_EVENT_CALL_DEPTH = 50; + + /** + * @phpstan-return Promise + */ + final public function call() : Promise{ + $this->promises = new ObjectSet(); + if(!isset(self::$delegatesCallDepth[$class = static::class])){ + self::$delegatesCallDepth[$class] = 0; + } + + if(self::$delegatesCallDepth[$class] >= self::MAX_EVENT_CALL_DEPTH){ + //this exception will be caught by the parent event call if all else fails + throw new \RuntimeException("Recursive event call detected (reached max depth of " . self::MAX_EVENT_CALL_DEPTH . " calls)"); + } + + $timings = Timings::getAsyncEventTimings($this); + $timings->startTiming(); + + ++self::$delegatesCallDepth[$class]; + try{ + return $this->callAsyncDepth(); + }finally{ + --self::$delegatesCallDepth[$class]; + $timings->stopTiming(); + } + } + + /** + * @phpstan-return Promise + */ + private function callAsyncDepth() : Promise{ + /** @phpstan-var PromiseResolver $globalResolver */ + $globalResolver = new PromiseResolver(); + + $priorities = EventPriority::ALL; + $testResolve = function () use (&$testResolve, &$priorities, $globalResolver){ + if(count($priorities) === 0){ + $globalResolver->resolve($this); + }else{ + $this->callPriority(array_shift($priorities))->onCompletion(function() use ($testResolve) : void{ + $testResolve(); + }, function () use ($globalResolver) { + $globalResolver->reject(); + }); + } + }; + + $testResolve(); + + return $globalResolver->getPromise(); + } + + /** + * @phpstan-return Promise + */ + private function callPriority(int $priority) : Promise{ + $handlers = HandlerListManager::global()->getListFor(static::class)->getListenersByPriority($priority); + + /** @phpstan-var PromiseResolver $resolver */ + $resolver = new PromiseResolver(); + + $nonConcurrentHandlers = []; + foreach($handlers as $registration){ + assert($registration instanceof RegisteredAsyncListener); + if($registration->canBeCalledConcurrently()){ + $result = $registration->callAsync($this); + if($result !== null) { + $this->promises->add($result); + } + }else{ + $nonConcurrentHandlers[] = $registration; + } + } + + $testResolve = function() use (&$nonConcurrentHandlers, &$testResolve, $resolver){ + if(count($nonConcurrentHandlers) === 0){ + $this->waitForPromises()->onCompletion(function() use ($resolver){ + $resolver->resolve(null); + }, function() use ($resolver){ + $resolver->reject(); + }); + }else{ + $this->waitForPromises()->onCompletion(function() use (&$nonConcurrentHandlers, $testResolve){ + $handler = array_shift($nonConcurrentHandlers); + assert($handler instanceof RegisteredAsyncListener); + $result = $handler->callAsync($this); + if($result !== null) { + $this->promises->add($result); + } + $testResolve(); + }, function() use ($resolver) { + $resolver->reject(); + }); + } + }; + + $testResolve(); + + return $resolver->getPromise(); + } + + /** + * @phpstan-return Promise> + */ + private function waitForPromises() : Promise{ + $array = $this->promises->toArray(); + $this->promises->clear(); + if(count($array) === 0){ + $resolver = new PromiseResolver(); + $resolver->resolve([]); + + return $resolver->getPromise(); + } + + return Promise::all($array); + } +} diff --git a/src/event/HandlerList.php b/src/event/HandlerList.php index 2072cd5226f..89629e140e3 100644 --- a/src/event/HandlerList.php +++ b/src/event/HandlerList.php @@ -25,6 +25,7 @@ use pocketmine\plugin\Plugin; use function array_merge; +use function array_merge_recursive; use function krsort; use function spl_object_id; use const SORT_NUMERIC; @@ -37,7 +38,7 @@ class HandlerList{ private array $affectedHandlerCaches = []; /** - * @phpstan-param class-string $class + * @phpstan-param class-string $class */ public function __construct( private string $class, @@ -126,12 +127,23 @@ public function getListenerList() : array{ $handlerLists[] = $currentList; } - $listenersByPriority = []; + $listeners = []; + $asyncListeners = []; + $exclusiveAsyncListeners = []; foreach($handlerLists as $currentList){ - foreach($currentList->handlerSlots as $priority => $listeners){ - $listenersByPriority[$priority] = array_merge($listenersByPriority[$priority] ?? [], $listeners); + foreach($currentList->handlerSlots as $priority => $listenersToSort){ + foreach($listenersToSort as $listener){ + if(!$listener instanceof RegisteredAsyncListener){ + $listeners[$priority][] = $listener; + }elseif(!$listener->canBeCalledConcurrently()){ + $asyncListeners[$priority][] = $listener; + }else{ + $exclusiveAsyncListeners[$priority][] = $listener; + } + } } } + $listenersByPriority = array_merge_recursive($listeners, $asyncListeners, $exclusiveAsyncListeners); //TODO: why on earth do the priorities have higher values for lower priority? krsort($listenersByPriority, SORT_NUMERIC); diff --git a/src/event/HandlerListManager.php b/src/event/HandlerListManager.php index 605a3874789..4fc9ac3052d 100644 --- a/src/event/HandlerListManager.php +++ b/src/event/HandlerListManager.php @@ -38,7 +38,7 @@ public static function global() : self{ private array $allLists = []; /** * @var RegisteredListenerCache[] event class name => cache - * @phpstan-var array, RegisteredListenerCache> + * @phpstan-var array, RegisteredListenerCache> */ private array $handlerCaches = []; @@ -59,7 +59,7 @@ public function unregisterAll(RegisteredListener|Plugin|Listener|null $object = } /** - * @phpstan-param \ReflectionClass $class + * @phpstan-param \ReflectionClass $class */ private static function isValidClass(\ReflectionClass $class) : bool{ $tags = Utils::parseDocComment((string) $class->getDocComment()); @@ -67,9 +67,9 @@ private static function isValidClass(\ReflectionClass $class) : bool{ } /** - * @phpstan-param \ReflectionClass $class + * @phpstan-param \ReflectionClass $class * - * @phpstan-return \ReflectionClass|null + * @phpstan-return \ReflectionClass|null */ private static function resolveNearestHandleableParent(\ReflectionClass $class) : ?\ReflectionClass{ for($parent = $class->getParentClass(); $parent !== false; $parent = $parent->getParentClass()){ @@ -86,7 +86,8 @@ private static function resolveNearestHandleableParent(\ReflectionClass $class) * * Calling this method also lazily initializes the $classMap inheritance tree of handler lists. * - * @phpstan-param class-string $event + * @phpstan-template TEvent of Event|AsyncEvent + * @phpstan-param class-string $event * * @throws \ReflectionException * @throws \InvalidArgumentException @@ -112,7 +113,7 @@ public function getListFor(string $event) : HandlerList{ } /** - * @phpstan-param class-string $event + * @phpstan-param class-string $event * * @return RegisteredListener[] */ diff --git a/src/event/ListenerMethodTags.php b/src/event/ListenerMethodTags.php index cb932ce27ac..e65f25f80ba 100644 --- a/src/event/ListenerMethodTags.php +++ b/src/event/ListenerMethodTags.php @@ -31,4 +31,5 @@ final class ListenerMethodTags{ public const HANDLE_CANCELLED = "handleCancelled"; public const NOT_HANDLER = "notHandler"; public const PRIORITY = "priority"; + public const EXCLUSIVE_CALL = "exclusiveCall"; } diff --git a/src/event/RegisteredAsyncListener.php b/src/event/RegisteredAsyncListener.php new file mode 100644 index 00000000000..6a0413d9930 --- /dev/null +++ b/src/event/RegisteredAsyncListener.php @@ -0,0 +1,67 @@ + $handler + */ + public function __construct( + protected \Closure $handler, + int $priority, + Plugin $plugin, + bool $handleCancelled, + private bool $exclusiveCall, + protected TimingsHandler $timings + ){ + parent::__construct($handler, $priority, $plugin, $handleCancelled, $timings); + } + + public function canBeCalledConcurrently() : bool{ + return !$this->exclusiveCall; + } + + public function callEvent(Event $event) : void{ + throw new \BadMethodCallException("Cannot call async event synchronously, use callAsync() instead"); + } + + /** + * @phpstan-return Promise|null + */ + public function callAsync(AsyncEvent $event) : ?Promise{ + if($event instanceof Cancellable && $event->isCancelled() && !$this->isHandlingCancelled()){ + return null; + } + $this->timings->startTiming(); + try{ + return ($this->handler)($event); + }finally{ + $this->timings->stopTiming(); + } + } +} diff --git a/src/event/player/PlayerChatAsyncEvent.php b/src/event/player/PlayerChatAsyncEvent.php new file mode 100644 index 00000000000..c520aa5a117 --- /dev/null +++ b/src/event/player/PlayerChatAsyncEvent.php @@ -0,0 +1,92 @@ +message; + } + + public function setMessage(string $message) : void{ + $this->message = $message; + } + + /** + * Changes the player that is sending the message + */ + public function setPlayer(Player $player) : void{ + $this->player = $player; + } + + public function getPlayer() : Player{ + return $this->player; + } + + public function getFormatter() : ChatFormatter{ + return $this->formatter; + } + + public function setFormatter(ChatFormatter $formatter) : void{ + $this->formatter = $formatter; + } + + /** + * @return CommandSender[] + */ + public function getRecipients() : array{ + return $this->recipients; + } + + /** + * @param CommandSender[] $recipients + */ + public function setRecipients(array $recipients) : void{ + Utils::validateArrayValueType($recipients, function(CommandSender $_) : void{}); + $this->recipients = $recipients; + } +} diff --git a/src/event/player/PlayerCreationAsyncEvent.php b/src/event/player/PlayerCreationAsyncEvent.php new file mode 100644 index 00000000000..37b1bdf7fa9 --- /dev/null +++ b/src/event/player/PlayerCreationAsyncEvent.php @@ -0,0 +1,89 @@ + */ + private string $baseClass = Player::class; + /** @phpstan-var class-string */ + private string $playerClass = Player::class; + + public function __construct(private NetworkSession $session){} + + public function getNetworkSession() : NetworkSession{ + return $this->session; + } + + public function getAddress() : string{ + return $this->session->getIp(); + } + + public function getPort() : int{ + return $this->session->getPort(); + } + + /** + * Returns the base class that the final player class must extend. + * + * @phpstan-return class-string + */ + public function getBaseClass() : string{ + return $this->baseClass; + } + + /** + * Sets the class that the final player class must extend. + * The new base class must be a subclass of the current base class. + * This can (perhaps) be used to limit the options for custom player classes provided by other plugins. + * + * @phpstan-param class-string $class + */ + public function setBaseClass(string $class) : void{ + if(!is_a($class, $this->baseClass, true)){ + throw new \RuntimeException("Base class $class must extend " . $this->baseClass); + } + + $this->baseClass = $class; + } + + /** + * Returns the class that will be instantiated to create the player after the event. + * + * @phpstan-return class-string + */ + public function getPlayerClass() : string{ + return $this->playerClass; + } + + /** + * Sets the class that will be instantiated to create the player after the event. The class must not be abstract, + * and must be an instance of the base class. + * + * @phpstan-param class-string $class + */ + public function setPlayerClass(string $class) : void{ + Utils::testValidInstance($class, $this->baseClass); + $this->playerClass = $class; + } +} \ No newline at end of file diff --git a/src/event/player/PlayerDataSaveAsyncEvent.php b/src/event/player/PlayerDataSaveAsyncEvent.php new file mode 100644 index 00000000000..9977bb48925 --- /dev/null +++ b/src/event/player/PlayerDataSaveAsyncEvent.php @@ -0,0 +1,45 @@ +data; + } + + public function setSaveData(CompoundTag $data) : void{ + $this->data = $data; + } + + /** + * Returns the username of the player whose data is being saved. This is not necessarily an online player. + */ + public function getPlayerName() : string{ + return $this->playerName; + } + + /** + * Returns the player whose data is being saved, if online. + * If null, this data is for an offline player (possibly just disconnected). + */ + public function getPlayer() : ?Player{ + return $this->player; + } +} \ No newline at end of file diff --git a/src/item/VanillaItems.php b/src/item/VanillaItems.php index bbd0dfc01b3..5115ee48a8b 100644 --- a/src/item/VanillaItems.php +++ b/src/item/VanillaItems.php @@ -591,12 +591,12 @@ protected function createEntity(World $world, Vector3 $pos, float $yaw, float $p } }); self::register("squid_spawn_egg", new class(new IID(Ids::SQUID_SPAWN_EGG), "Squid Spawn Egg") extends SpawnEgg{ - public function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity{ + protected function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity{ return new Squid(Location::fromObject($pos, $world, $yaw, $pitch)); } }); self::register("villager_spawn_egg", new class(new IID(Ids::VILLAGER_SPAWN_EGG), "Villager Spawn Egg") extends SpawnEgg{ - public function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity{ + protected function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity{ return new Villager(Location::fromObject($pos, $world, $yaw, $pitch)); } }); diff --git a/src/player/Player.php b/src/player/Player.php index 0cefbe71f22..d7135457cef 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -118,6 +118,7 @@ use pocketmine\permission\PermissibleBase; use pocketmine\permission\PermissibleDelegateTrait; use pocketmine\player\chat\StandardChatFormatter; +use pocketmine\promise\Promise; use pocketmine\Server; use pocketmine\ServerProperties; use pocketmine\timings\Timings; @@ -2345,6 +2346,13 @@ public function save() : void{ $this->server->saveOfflinePlayerData($this->username, $this->getSaveData()); } + /** + * @return Promise + */ + public function saveAsync() : Promise { + return $this->server->saveOfflinePlayerDataAsync($this->username, $this->getSaveData()); + } + protected function onDeath() : void{ //Crafting grid must always be evacuated even if keep-inventory is true. This dumps the contents into the //main inventory and drops the rest on the ground. diff --git a/src/plugin/PluginManager.php b/src/plugin/PluginManager.php index 198e4e893bf..ca126ab4cfd 100644 --- a/src/plugin/PluginManager.php +++ b/src/plugin/PluginManager.php @@ -23,6 +23,7 @@ namespace pocketmine\plugin; +use pocketmine\event\AsyncEvent; use pocketmine\event\Cancellable; use pocketmine\event\Event; use pocketmine\event\EventPriority; @@ -31,11 +32,13 @@ use pocketmine\event\ListenerMethodTags; use pocketmine\event\plugin\PluginDisableEvent; use pocketmine\event\plugin\PluginEnableEvent; +use pocketmine\event\RegisteredAsyncListener; use pocketmine\event\RegisteredListener; use pocketmine\lang\KnownTranslationFactory; use pocketmine\permission\DefaultPermissions; use pocketmine\permission\PermissionManager; use pocketmine\permission\PermissionParser; +use pocketmine\promise\Promise; use pocketmine\Server; use pocketmine\timings\Timings; use pocketmine\utils\AssumptionFailedError; @@ -575,7 +578,7 @@ private function getEventsHandledBy(\ReflectionMethod $method) : ?string{ /** @phpstan-var class-string $paramClass */ $paramClass = $paramType->getName(); $eventClass = new \ReflectionClass($paramClass); - if(!$eventClass->isSubclassOf(Event::class)){ + if(!$eventClass->isSubclassOf(Event::class) && !$eventClass->isSubclassOf(AsyncEvent::class)){ return null; } @@ -629,8 +632,33 @@ public function registerEvents(Listener $listener, Plugin $plugin) : void{ throw new PluginException("Event handler " . Utils::getNiceClosureName($handlerClosure) . "() declares invalid @" . ListenerMethodTags::HANDLE_CANCELLED . " value \"" . $tags[ListenerMethodTags::HANDLE_CANCELLED] . "\""); } } + $exclusiveCall = false; + if(isset($tags[ListenerMethodTags::EXCLUSIVE_CALL])){ + if(!is_a($eventClass, AsyncEvent::class, true)){ + throw new PluginException(sprintf( + "Event handler %s() declares @%s for non-async event of type %s", + Utils::getNiceClosureName($handlerClosure), + ListenerMethodTags::EXCLUSIVE_CALL, + $eventClass + )); + } + switch(strtolower($tags[ListenerMethodTags::EXCLUSIVE_CALL])){ + case "true": + case "": + $exclusiveCall = true; + break; + case "false": + break; + default: + throw new PluginException("Event handler " . Utils::getNiceClosureName($handlerClosure) . "() declares invalid @" . ListenerMethodTags::EXCLUSIVE_CALL . " value \"" . $tags[ListenerMethodTags::EXCLUSIVE_CALL] . "\""); + } + } - $this->registerEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled); + if(is_subclass_of($eventClass, AsyncEvent::class) && $this->canHandleAsyncEvent($handlerClosure)){ + $this->registerAsyncEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled, $exclusiveCall); + }else{ + $this->registerEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled); + } } } @@ -665,4 +693,44 @@ public function registerEvent(string $event, \Closure $handler, int $priority, P HandlerListManager::global()->getListFor($event)->register($registeredListener); return $registeredListener; } + + /** + * @param string $event Class name that extends Event and AsyncEvent + * + * @phpstan-param class-string $event + * @phpstan-param \Closure(AsyncEvent) : Promise $handler + * + * @throws \ReflectionException + */ + public function registerAsyncEvent(string $event, \Closure $handler, int $priority, Plugin $plugin, bool $handleCancelled = false, bool $exclusiveCall = false) : RegisteredAsyncListener{ + if(!is_subclass_of($event, AsyncEvent::class)){ + throw new PluginException($event . " is not an AsyncEvent"); + } + + $handlerName = Utils::getNiceClosureName($handler); + + if(!$plugin->isEnabled()){ + throw new PluginException("Plugin attempted to register event handler " . $handlerName . "() to event " . $event . " while not enabled"); + } + + $timings = Timings::getEventHandlerTimings($event, $handlerName, $plugin->getDescription()->getFullName()); + + $registeredListener = new RegisteredAsyncListener($handler, $priority, $plugin, $handleCancelled, $exclusiveCall, $timings); + HandlerListManager::global()->getListFor($event)->register($registeredListener); + return $registeredListener; + } + + /** + * Check if the given handler return type is async-compatible (equal to Promise) + * + * @phpstan-param \Closure(AsyncEvent) : Promise $handler + * + * @throws \ReflectionException + */ + private function canHandleAsyncEvent(\Closure $handler) : bool{ + $reflection = new \ReflectionFunction($handler); + $return = $reflection->getReturnType(); + + return $return instanceof \ReflectionNamedType && $return->getName() === Promise::class; + } } diff --git a/src/timings/Timings.php b/src/timings/Timings.php index 563af69bff6..19e8db145a8 100644 --- a/src/timings/Timings.php +++ b/src/timings/Timings.php @@ -25,6 +25,7 @@ use pocketmine\block\tile\Tile; use pocketmine\entity\Entity; +use pocketmine\event\AsyncEvent; use pocketmine\event\Event; use pocketmine\network\mcpe\protocol\ClientboundPacket; use pocketmine\network\mcpe\protocol\ServerboundPacket; @@ -114,6 +115,8 @@ abstract class Timings{ /** @var TimingsHandler[] */ private static array $events = []; + /** @var TimingsHandler[] */ + private static array $asyncEvents = []; /** @var TimingsHandler[][] */ private static array $eventHandlers = []; @@ -300,8 +303,18 @@ public static function getEventTimings(Event $event) : TimingsHandler{ return self::$events[$eventClass]; } + public static function getAsyncEventTimings(AsyncEvent $event) : TimingsHandler{ + $eventClass = get_class($event); + if(!isset(self::$asyncEvents[$eventClass])){ + self::$asyncEvents[$eventClass] = new TimingsHandler(self::shortenCoreClassName($eventClass, "pocketmine\\event\\"), group: "Events"); + } + + return self::$asyncEvents[$eventClass]; + } + /** - * @phpstan-param class-string $event + * @phpstan-template TEvent of Event|AsyncEvent + * @phpstan-param class-string $event */ public static function getEventHandlerTimings(string $event, string $handlerName, string $group) : TimingsHandler{ if(!isset(self::$eventHandlers[$event][$handlerName])){ diff --git a/src/world/WorldManager.php b/src/world/WorldManager.php index ff603a2dfcb..0b02dcc6a33 100644 --- a/src/world/WorldManager.php +++ b/src/world/WorldManager.php @@ -29,6 +29,8 @@ use pocketmine\event\world\WorldUnloadEvent; use pocketmine\lang\KnownTranslationFactory; use pocketmine\player\ChunkSelector; +use pocketmine\promise\Promise; +use pocketmine\promise\PromiseResolver; use pocketmine\Server; use pocketmine\world\format\Chunk; use pocketmine\world\format\io\exception\CorruptedWorldException; @@ -356,9 +358,17 @@ public function tick(int $currentTick) : void{ $this->autoSaveTicker = 0; $this->server->getLogger()->debug("[Auto Save] Saving worlds..."); $start = microtime(true); - $this->doAutoSave(); - $time = microtime(true) - $start; - $this->server->getLogger()->debug("[Auto Save] Save completed in " . ($time >= 1 ? round($time, 3) . "s" : round($time * 1000) . "ms")); + $this->doAutoSave()->onCompletion( + function () use ($start) : void{ + $time = microtime(true) - $start; + $this->server->getLogger()->debug("[Auto Save] Save completed in " . ($time >= 1 ? round($time, 3) . "s" : round($time * 1000) . "ms")); + }, + function () use ($start) : void{ + $this->server->getLogger()->error("[Auto Save] Save failed" ); + $time = microtime(true) - $start; + $this->server->getLogger()->debug("[Auto Save] Save completed in " . ($time >= 1 ? round($time, 3) . "s" : round($time * 1000) . "ms")); + } + ); } } @@ -387,14 +397,32 @@ public function setAutoSaveInterval(int $autoSaveTicks) : void{ $this->autoSaveTicks = $autoSaveTicks; } - private function doAutoSave() : void{ + /** + * @return Promise + */ + private function doAutoSave() : Promise{ + $promises = []; foreach($this->worlds as $world){ foreach($world->getPlayers() as $player){ if($player->spawned){ - $player->save(); + $promises[] = $player->saveAsync(); } } $world->save(false); } + + $resolver = new PromiseResolver(); + + if(count($promises) === 0){ + $resolver->resolve(null); + } else { + Promise::all($promises) + ->onCompletion( + fn() => $resolver->resolve(null), + fn() => $resolver->reject() + ); + } + + return $resolver->getPromise(); } } diff --git a/tests/phpstan/configs/actual-problems.neon b/tests/phpstan/configs/actual-problems.neon index cc647da8050..d7fe2d87447 100644 --- a/tests/phpstan/configs/actual-problems.neon +++ b/tests/phpstan/configs/actual-problems.neon @@ -1205,3 +1205,23 @@ parameters: count: 1 path: ../../phpunit/scheduler/AsyncPoolTest.php + - + message: "#^Right side of && is always true\\.$#" + count: 1 + path: ../../../src/promise/Promise.php + + - + message: "#^Parameter \\#1 \\$value of method pocketmine\\\\promise\\\\PromiseResolver\\\\:\\:resolve\\(\\) expects null, string given\\.$#" + count: 2 + path: ../../../src/event/AsyncEventDelegate.php + + - + message: "#^Parameter \\#1 \\$handler of class pocketmine\\\\event\\\\RegisteredAsyncListener constructor expects Closure\\(pocketmine\\\\event\\\\AsyncEvent&pocketmine\\\\event\\\\Event\\)\\: pocketmine\\\\promise\\\\Promise\\, \\(Closure\\(TEvent\\)\\: void\\)\\|\\(Closure\\(pocketmine\\\\event\\\\AsyncEvent&TEvent\\)\\: pocketmine\\\\promise\\\\Promise\\\\) given\\.$#" + count: 1 + path: ../../../src/plugin/PluginManager.php + + - + message: "#^Parameter \\#1 \\$handler of class pocketmine\\\\event\\\\RegisteredAsyncListener constructor expects Closure\\(pocketmine\\\\event\\\\AsyncEvent&pocketmine\\\\event\\\\Event\\)\\: pocketmine\\\\promise\\\\Promise\\, Closure\\(TEvent\\)\\: pocketmine\\\\promise\\\\Promise\\ given\\.$#" + count: 1 + path: ../../../src/plugin/PluginManager.php +