Skip to content

Commit

Permalink
implement PlayerCreation & PlayerDataSave as async events
Browse files Browse the repository at this point in the history
  • Loading branch information
ShockedPlot7560 committed May 29, 2024
1 parent 7ba0e15 commit c70397d
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 9 deletions.
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.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.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.

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.

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.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.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.

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.

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
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
6 changes: 6 additions & 0 deletions src/event/AsyncEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
89 changes: 89 additions & 0 deletions src/event/player/PlayerCreationAsyncEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace pocketmine\event\player;

use pocketmine\event\AsyncEvent;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\player\Player;
use pocketmine\utils\Utils;

/**
* Allows the use of custom Player classes. This enables overriding built-in Player methods to change behaviour that is
* not possible to alter any other way.
*
* You probably don't need this event, and found your way here because you looked at some code in an old plugin that
* abused it (very common). Instead of using custom player classes, you should consider making session classes instead.
*
* @see https://github.com/pmmp/SessionsDemo
*
* This event is a power-user feature, and multiple plugins using it at the same time will conflict and break unless
* they've been designed to work together. This means that it's only usually useful in private plugins.
*
* WARNING: This should NOT be used for adding extra functions or properties. This is intended for **overriding existing
* core behaviour**, and should only be used if you know EXACTLY what you're doing.
* Custom player classes may break in any update without warning. This event isn't much more than glorified reflection.
*/
class PlayerCreationAsyncEvent extends AsyncEvent{
/** @phpstan-var class-string<Player> */
private string $baseClass = Player::class;
/** @phpstan-var class-string<Player> */
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<Player>
*/
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<Player> $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<Player>
*/
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<Player> $class
*/
public function setPlayerClass(string $class) : void{
Utils::testValidInstance($class, $this->baseClass);
$this->playerClass = $class;
}
}
45 changes: 45 additions & 0 deletions src/event/player/PlayerDataSaveAsyncEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace pocketmine\event\player;

use pocketmine\event\AsyncEvent;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\player\Player;

class PlayerDataSaveAsyncEvent extends AsyncEvent implements Cancellable{
use CancellableTrait;

public function __construct(
protected CompoundTag $data,
protected string $playerName,
private ?Player $player
){}

/**
* Returns the data to be written to disk as a CompoundTag
*/
public function getSaveData() : CompoundTag{
return $this->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;
}
}
8 changes: 8 additions & 0 deletions src/player/Player.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2345,6 +2346,13 @@ public function save() : void{
$this->server->saveOfflinePlayerData($this->username, $this->getSaveData());
}

/**
* @return Promise<null>
*/
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.
Expand Down
38 changes: 33 additions & 5 deletions src/world/WorldManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
}
);
}
}

Expand Down Expand Up @@ -387,14 +397,32 @@ public function setAutoSaveInterval(int $autoSaveTicks) : void{
$this->autoSaveTicks = $autoSaveTicks;
}

private function doAutoSave() : void{
/**
* @return Promise<null>
*/
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();
}
}

0 comments on commit c70397d

Please sign in to comment.