diff --git a/src/event/AsyncEvent.php b/src/event/AsyncEvent.php index 9f1784b90..375774485 100644 --- a/src/event/AsyncEvent.php +++ b/src/event/AsyncEvent.php @@ -24,20 +24,133 @@ declare(strict_types=1); namespace pocketmine\event; use pocketmine\promise\Promise; +use pocketmine\promise\PromiseResolver; +use pocketmine\timings\Timings; +use pocketmine\utils\ObjectSet; +use function array_shift; +use function assert; +use function count; /** - * This interface is implemented by an Event subclass if and only if it can be called asynchronously. + * This class is used to permit asynchronous event handling. * - * Used with {@see AsyncEventTrait} to provide a way to call an event asynchronously. * When an event is called asynchronously, the event handlers are called by priority level. * When all the promises of a priority level have been resolved, the next priority level is called. */ -interface AsyncEvent{ +abstract class AsyncEvent{ + /** @phpstan-var ObjectSet> $promises */ + private ObjectSet $promises; + /** @var array, int> $delegatesCallDepth */ + private static array $delegatesCallDepth = []; + private const MAX_EVENT_CALL_DEPTH = 50; + /** - * Be prudent, calling an event asynchronously can produce unexpected results. - * During the execution of the event, the server, the player and the event context may have changed state. - * * @phpstan-return Promise */ - public function callAsync() : 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(null); + }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(); + + return Promise::all($array); + } } diff --git a/src/event/AsyncEventDelegate.php b/src/event/AsyncEventDelegate.php deleted file mode 100644 index c3054ebe5..000000000 --- a/src/event/AsyncEventDelegate.php +++ /dev/null @@ -1,153 +0,0 @@ -> $promises */ - private ObjectSet $promises; - /** @var array, int> $delegatesCallDepth */ - private static array $delegatesCallDepth = []; - private const MAX_EVENT_CALL_DEPTH = 50; - - public function __construct( - private AsyncEvent&Event $event - ){ - $this->promises = new ObjectSet(); - } - - /** - * @phpstan-return Promise - */ - public function call() : Promise{ - $this->promises->clear(); - if(!isset(self::$delegatesCallDepth[$class = $this->event::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->event); - $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(null); - }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($this->event::class)->getListenersByPriority($priority); - - /** @phpstan-var PromiseResolver $resolver */ - $resolver = new PromiseResolver(); - - $nonConcurrentHandlers = []; - foreach($handlers as $registration){ - if($registration instanceof RegisteredAsyncListener){ - if($registration->canBeCalledConcurrently()){ - $this->promises->add($registration->callAsync($this->event)); - }else{ - $nonConcurrentHandlers[] = $registration; - } - }else{ - $registration->callEvent($this->event); - } - } - - $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); - if($handler instanceof RegisteredAsyncListener){ - $this->promises->add($handler->callAsync($this->event)); - } - $testResolve(); - }, function() use ($resolver) { - $resolver->reject(); - }); - } - }; - - $testResolve(); - - return $resolver->getPromise(); - } - - /** - * @phpstan-return Promise> - */ - private function waitForPromises() : Promise{ - $array = $this->promises->toArray(); - $this->promises->clear(); - - return Promise::all($array); - } -} diff --git a/src/event/AsyncEventTrait.php b/src/event/AsyncEventTrait.php deleted file mode 100644 index 9cea1d36c..000000000 --- a/src/event/AsyncEventTrait.php +++ /dev/null @@ -1,35 +0,0 @@ - - */ - final public function callAsync() : Promise{ - return (new AsyncEventDelegate($this))->call(); - } -} diff --git a/src/event/HandlerListManager.php b/src/event/HandlerListManager.php index 047632f54..92fd3f141 100644 --- a/src/event/HandlerListManager.php +++ b/src/event/HandlerListManager.php @@ -86,7 +86,7 @@ class HandlerListManager{ * * Calling this method also lazily initializes the $classMap inheritance tree of handler lists. * - * @phpstan-template TEvent of Event + * @phpstan-template TEvent of Event|AsyncEvent * @phpstan-param class-string $event * * @throws \ReflectionException diff --git a/src/event/RegisteredAsyncListener.php b/src/event/RegisteredAsyncListener.php index 821b88154..6a0413d99 100644 --- a/src/event/RegisteredAsyncListener.php +++ b/src/event/RegisteredAsyncListener.php @@ -28,26 +28,17 @@ use pocketmine\promise\Promise; use pocketmine\timings\TimingsHandler; class RegisteredAsyncListener extends RegisteredListener{ - /** @phpstan-var Promise $returnPromise */ - private Promise $returnPromise; - /** - * @phpstan-param \Closure(AsyncEvent&Event) : Promise $handler + * @phpstan-param \Closure(AsyncEvent) : Promise $handler */ public function __construct( - \Closure $handler, + protected \Closure $handler, int $priority, Plugin $plugin, bool $handleCancelled, private bool $exclusiveCall, - TimingsHandler $timings + protected TimingsHandler $timings ){ - $handler = function(AsyncEvent&Event $event) use($handler) : void{ - $this->returnPromise = $handler($event); - if(!$this->returnPromise instanceof Promise){ - throw new \TypeError("Async event handler must return a Promise"); - } - }; parent::__construct($handler, $priority, $plugin, $handleCancelled, $timings); } @@ -55,11 +46,22 @@ class RegisteredAsyncListener extends RegisteredListener{ return !$this->exclusiveCall; } + public function callEvent(Event $event) : void{ + throw new \BadMethodCallException("Cannot call async event synchronously, use callAsync() instead"); + } + /** - * @phpstan-return Promise + * @phpstan-return Promise|null */ - public function callAsync(AsyncEvent&Event $event) : Promise{ - $this->callEvent($event); - return $this->returnPromise; + 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 000000000..c520aa5a1 --- /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/player/Player.php b/src/player/Player.php index 0ee9d7dc2..4deab6aa6 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -51,6 +51,7 @@ use pocketmine\event\player\PlayerBedEnterEvent; use pocketmine\event\player\PlayerBedLeaveEvent; use pocketmine\event\player\PlayerBlockPickEvent; use pocketmine\event\player\PlayerChangeSkinEvent; +use pocketmine\event\player\PlayerChatAsyncEvent; use pocketmine\event\player\PlayerChatEvent; use pocketmine\event\player\PlayerDeathEvent; use pocketmine\event\player\PlayerDisplayNameChangeEvent; @@ -158,6 +159,7 @@ use function strlen; use function strtolower; use function substr; use function trim; +use function var_dump; use const M_PI; use const M_SQRT3; use const PHP_INT_MAX; @@ -1517,6 +1519,19 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ if(!$ev->isCancelled()){ $this->server->broadcastMessage($ev->getFormatter()->format($ev->getPlayer()->getDisplayName(), $ev->getMessage()), $ev->getRecipients()); } + + $ev = new PlayerChatAsyncEvent( + $this, $messagePart, + $this->server->getBroadcastChannelSubscribers(Server::BROADCAST_CHANNEL_USERS), + new StandardChatFormatter() + ); + $ev->call()->onCompletion(function () use ($ev) { + if(!$ev->isCancelled()){ + $this->server->broadcastMessage($ev->getFormatter()->format($ev->getPlayer()->getDisplayName(), $ev->getMessage()), $ev->getRecipients()); + } + }, function () { + var_dump("Failed to send chat message"); + }); } } } diff --git a/src/plugin/PluginManager.php b/src/plugin/PluginManager.php index b1050a971..520acae64 100644 --- a/src/plugin/PluginManager.php +++ b/src/plugin/PluginManager.php @@ -689,17 +689,13 @@ class PluginManager{ /** * @param string $event Class name that extends Event and AsyncEvent * - * @phpstan-template TEvent of Event&AsyncEvent + * @phpstan-template TEvent of AsyncEvent * @phpstan-param class-string $event * @phpstan-param \Closure(TEvent) : 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, Event::class)){ - throw new PluginException($event . " is not an Event"); - } - if(!is_subclass_of($event, AsyncEvent::class)){ throw new PluginException($event . " is not an AsyncEvent"); } diff --git a/src/timings/Timings.php b/src/timings/Timings.php index e8d37c898..053f7970c 100644 --- a/src/timings/Timings.php +++ b/src/timings/Timings.php @@ -307,7 +307,7 @@ abstract class Timings{ return self::$events[$eventClass]; } - public static function getAsyncEventTimings(AsyncEvent&Event $event) : TimingsHandler{ + 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"); @@ -317,7 +317,7 @@ abstract class Timings{ } /** - * @phpstan-template TEvent of Event + * @phpstan-template TEvent of Event|AsyncEvent * @phpstan-param class-string $event */ public static function getEventHandlerTimings(string $event, string $handlerName, string $group) : TimingsHandler{