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/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 index 12337602473..c3d2ef9d73f 100644 --- a/src/event/AsyncEvent.php +++ b/src/event/AsyncEvent.php @@ -150,6 +150,12 @@ private function callPriority(int $priority) : 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/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/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/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(); } }