From 9010b2743ce87909b94d5ca2ff6c6c13522832e8 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Fri, 23 Dec 2022 18:58:49 +0000 Subject: [PATCH] Move player data storage handling behind an interface --- src/Server.php | 61 ++++----------- src/player/DatFilePlayerDataProvider.php | 99 ++++++++++++++++++++++++ src/player/PlayerDataLoadException.php | 28 +++++++ src/player/PlayerDataProvider.php | 52 +++++++++++++ src/player/PlayerDataSaveException.php | 28 +++++++ 5 files changed, 224 insertions(+), 44 deletions(-) create mode 100644 src/player/DatFilePlayerDataProvider.php create mode 100644 src/player/PlayerDataLoadException.php create mode 100644 src/player/PlayerDataProvider.php create mode 100644 src/player/PlayerDataSaveException.php diff --git a/src/Server.php b/src/Server.php index ef914afe2..4dfa29207 100644 --- a/src/Server.php +++ b/src/Server.php @@ -49,10 +49,7 @@ use pocketmine\lang\KnownTranslationFactory; use pocketmine\lang\Language; use pocketmine\lang\LanguageNotFoundException; use pocketmine\lang\Translatable; -use pocketmine\nbt\BigEndianNbtSerializer; -use pocketmine\nbt\NbtDataException; use pocketmine\nbt\tag\CompoundTag; -use pocketmine\nbt\TreeRoot; use pocketmine\network\mcpe\compression\CompressBatchPromise; use pocketmine\network\mcpe\compression\CompressBatchTask; use pocketmine\network\mcpe\compression\Compressor; @@ -72,9 +69,13 @@ use pocketmine\network\query\QueryInfo; use pocketmine\network\upnp\UPnPNetworkInterface; use pocketmine\permission\BanList; use pocketmine\permission\DefaultPermissions; +use pocketmine\player\DatFilePlayerDataProvider; use pocketmine\player\GameMode; use pocketmine\player\OfflinePlayer; use pocketmine\player\Player; +use pocketmine\player\PlayerDataLoadException; +use pocketmine\player\PlayerDataProvider; +use pocketmine\player\PlayerDataSaveException; use pocketmine\player\PlayerInfo; use pocketmine\plugin\PharPluginLoader; use pocketmine\plugin\Plugin; @@ -161,12 +162,9 @@ use function time; use function touch; use function trim; use function yaml_parse; -use function zlib_decode; -use function zlib_encode; use const DIRECTORY_SEPARATOR; use const PHP_EOL; use const PHP_INT_MAX; -use const ZLIB_ENCODING_GZIP; /** * The class that manages everything @@ -251,6 +249,8 @@ class Server{ private string $dataPath; private string $pluginPath; + private PlayerDataProvider $playerDataProvider; + /** * @var string[] * @phpstan-var array @@ -484,49 +484,22 @@ class Server{ return $result; } - private function getPlayerDataPath(string $username) : string{ - return Path::join($this->getDataPath(), 'players', strtolower($username) . '.dat'); - } - /** * Returns whether the server has stored any saved data for this player. */ public function hasOfflinePlayerData(string $name) : bool{ - return file_exists($this->getPlayerDataPath($name)); - } - - private function handleCorruptedPlayerData(string $name) : void{ - $path = $this->getPlayerDataPath($name); - rename($path, $path . '.bak'); - $this->logger->error($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_data_playerCorrupted($name))); + return $this->playerDataProvider->hasData($name); } public function getOfflinePlayerData(string $name) : ?CompoundTag{ return Timings::$syncPlayerDataLoad->time(function() use ($name) : ?CompoundTag{ - $name = strtolower($name); - $path = $this->getPlayerDataPath($name); - - if(file_exists($path)){ - $contents = @file_get_contents($path); - if($contents === false){ - throw new \RuntimeException("Failed to read player data file \"$path\" (permission denied?)"); - } - $decompressed = @zlib_decode($contents); - if($decompressed === false){ - $this->logger->debug("Failed to decompress raw player data for \"$name\""); - $this->handleCorruptedPlayerData($name); - return null; - } - - try{ - return (new BigEndianNbtSerializer())->read($decompressed)->mustGetCompoundTag(); - }catch(NbtDataException $e){ //corrupt data - $this->logger->debug("Failed to decode NBT data for \"$name\": " . $e->getMessage()); - $this->handleCorruptedPlayerData($name); - return null; - } + try{ + return $this->playerDataProvider->loadData($name); + }catch(PlayerDataLoadException $e){ + $this->logger->debug("Failed to load player data for $name: " . $e->getMessage()); + $this->logger->error($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_data_playerCorrupted($name))); + return null; } - return null; }); } @@ -540,11 +513,9 @@ class Server{ if(!$ev->isCancelled()){ Timings::$syncPlayerDataSave->time(function() use ($name, $ev) : void{ - $nbt = new BigEndianNbtSerializer(); - $contents = Utils::assumeNotFalse(zlib_encode($nbt->write(new TreeRoot($ev->getSaveData())), ZLIB_ENCODING_GZIP), "zlib_encode() failed unexpectedly"); try{ - Filesystem::safeFilePutContents($this->getPlayerDataPath($name), $contents); - }catch(\RuntimeException $e){ + $this->playerDataProvider->saveData($name, $ev->getSaveData()); + }catch(PlayerDataSaveException $e){ $this->logger->critical($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_data_saveError($name, $e->getMessage()))); $this->logger->logException($e); } @@ -1007,6 +978,8 @@ class Server{ $this->queryInfo = new QueryInfo($this); + $this->playerDataProvider = new DatFilePlayerDataProvider(Path::join($this->dataPath, "players")); + register_shutdown_function([$this, "crashDump"]); $loadErrorCount = 0; diff --git a/src/player/DatFilePlayerDataProvider.php b/src/player/DatFilePlayerDataProvider.php new file mode 100644 index 000000000..cc9cd0531 --- /dev/null +++ b/src/player/DatFilePlayerDataProvider.php @@ -0,0 +1,99 @@ +path, strtolower($username) . '.dat'); + } + + private function handleCorruptedPlayerData(string $name) : void{ + $path = $this->getPlayerDataPath($name); + rename($path, $path . '.bak'); + } + + public function hasData(string $name) : bool{ + return file_exists($this->getPlayerDataPath($name)); + } + + public function loadData(string $name) : ?CompoundTag{ + $name = strtolower($name); + $path = $this->getPlayerDataPath($name); + + if(!file_exists($path)){ + return null; + } + + try{ + $contents = ErrorToExceptionHandler::trapAndRemoveFalse(fn() => file_get_contents($path)); + }catch(\ErrorException $e){ + throw new PlayerDataLoadException("Failed to read player data file \"$path\": " . $e->getMessage(), 0, $e); + } + try{ + $decompressed = ErrorToExceptionHandler::trapAndRemoveFalse(fn() => zlib_decode($contents)); + }catch(\ErrorException $e){ + $this->handleCorruptedPlayerData($name); + throw new PlayerDataLoadException("Failed to decompress raw player data for \"$name\": " . $e->getMessage(), 0, $e); + } + + try{ + return (new BigEndianNbtSerializer())->read($decompressed)->mustGetCompoundTag(); + }catch(NbtDataException $e){ //corrupt data + $this->handleCorruptedPlayerData($name); + throw new PlayerDataLoadException("Failed to decode NBT data for \"$name\": " . $e->getMessage(), 0, $e); + } + } + + public function saveData(string $name, CompoundTag $data) : void{ + $nbt = new BigEndianNbtSerializer(); + $contents = Utils::assumeNotFalse(zlib_encode($nbt->write(new TreeRoot($data)), ZLIB_ENCODING_GZIP), "zlib_encode() failed unexpectedly"); + try{ + Filesystem::safeFilePutContents($this->getPlayerDataPath($name), $contents); + }catch(\RuntimeException $e){ + throw new PlayerDataSaveException("Failed to write player data file: " . $e->getMessage(), 0, $e); + } + } +} diff --git a/src/player/PlayerDataLoadException.php b/src/player/PlayerDataLoadException.php new file mode 100644 index 000000000..5194307f6 --- /dev/null +++ b/src/player/PlayerDataLoadException.php @@ -0,0 +1,28 @@ +