Skip to content

Commit

Permalink
Merge branch 'feat/async-events' into stable
Browse files Browse the repository at this point in the history
  • Loading branch information
ShockedPlot7560 committed May 29, 2024
2 parents fb9a74e + 15a7ec6 commit 9932a4a
Show file tree
Hide file tree
Showing 18 changed files with 702 additions and 27 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
50 changes: 50 additions & 0 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -530,6 +533,31 @@ public function getOfflinePlayerData(string $name) : ?CompoundTag{
});
}

/**
* @return Promise<null>
*/
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{

Check failure on line 547 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerDataSaveAsyncEvent): void given.

Check failure on line 547 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerDataSaveAsyncEvent): void given.

Check failure on line 547 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerDataSaveAsyncEvent): void given.
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()){
Expand All @@ -554,7 +582,29 @@ public function saveOfflinePlayerData(string $name, CompoundTag $nbtTag) : void{
* @phpstan-return Promise<Player>
*/
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) =>

Check failure on line 589 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerCreationAsyncEvent): void given.

Check failure on line 589 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerCreationAsyncEvent): void given.

Check failure on line 589 in src/Server.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 / PHPStan analysis

Parameter #1 $onSuccess of method pocketmine\promise\Promise<pocketmine\event\AsyncEvent>::onCompletion() expects Closure(pocketmine\event\AsyncEvent): void, Closure(pocketmine\event\player\PlayerCreationAsyncEvent): void given.
$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<Player>
*/
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();

Expand Down
4 changes: 2 additions & 2 deletions src/VersionInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down
27 changes: 23 additions & 4 deletions src/command/defaults/SaveCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}
Expand Down
162 changes: 162 additions & 0 deletions src/event/AsyncEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?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\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 class is used to permit asynchronous event handling.
*
* 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.
*/
abstract class AsyncEvent{
/** @phpstan-var ObjectSet<Promise<null>> $promises */
private ObjectSet $promises;
/** @var array<class-string<AsyncEvent>, int> $delegatesCallDepth */
private static array $delegatesCallDepth = [];
private const MAX_EVENT_CALL_DEPTH = 50;

/**
* @phpstan-return Promise<self>
*/
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<self>
*/
private function callAsyncDepth() : Promise{
/** @phpstan-var PromiseResolver<self> $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<null>
*/
private function callPriority(int $priority) : Promise{
$handlers = HandlerListManager::global()->getListFor(static::class)->getListenersByPriority($priority);

/** @phpstan-var PromiseResolver<null> $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<array<int, null>>
*/
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);
}
}
20 changes: 16 additions & 4 deletions src/event/HandlerList.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,7 +38,7 @@ class HandlerList{
private array $affectedHandlerCaches = [];

/**
* @phpstan-param class-string<covariant Event> $class
* @phpstan-param class-string<Event|AsyncEvent> $class
*/
public function __construct(
private string $class,
Expand Down Expand Up @@ -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);
Expand Down
13 changes: 7 additions & 6 deletions src/event/HandlerListManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static function global() : self{
private array $allLists = [];
/**
* @var RegisteredListenerCache[] event class name => cache
* @phpstan-var array<class-string<Event>, RegisteredListenerCache>
* @phpstan-var array<class-string<Event|AsyncEvent>, RegisteredListenerCache>
*/
private array $handlerCaches = [];

Expand All @@ -59,17 +59,17 @@ public function unregisterAll(RegisteredListener|Plugin|Listener|null $object =
}

/**
* @phpstan-param \ReflectionClass<Event> $class
* @phpstan-param \ReflectionClass<Event|AsyncEvent> $class
*/
private static function isValidClass(\ReflectionClass $class) : bool{
$tags = Utils::parseDocComment((string) $class->getDocComment());
return !$class->isAbstract() || isset($tags["allowHandle"]);
}

/**
* @phpstan-param \ReflectionClass<Event> $class
* @phpstan-param \ReflectionClass<Event|AsyncEvent> $class
*
* @phpstan-return \ReflectionClass<Event>|null
* @phpstan-return \ReflectionClass<Event|AsyncEvent>|null
*/
private static function resolveNearestHandleableParent(\ReflectionClass $class) : ?\ReflectionClass{
for($parent = $class->getParentClass(); $parent !== false; $parent = $parent->getParentClass()){
Expand All @@ -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<covariant Event> $event
* @phpstan-template TEvent of Event|AsyncEvent
* @phpstan-param class-string<TEvent> $event
*
* @throws \ReflectionException
* @throws \InvalidArgumentException
Expand All @@ -112,7 +113,7 @@ public function getListFor(string $event) : HandlerList{
}

/**
* @phpstan-param class-string<covariant Event> $event
* @phpstan-param class-string<Event|AsyncEvent> $event
*
* @return RegisteredListener[]
*/
Expand Down
1 change: 1 addition & 0 deletions src/event/ListenerMethodTags.php
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Loading

0 comments on commit 9932a4a

Please sign in to comment.