From d41bdfc31c4875dccec0d572d747acde8bc232de Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Fri, 10 Mar 2017 20:00:31 +0000 Subject: [PATCH] Added resource packs support --- .gitignore | 1 + src/pocketmine/Player.php | 45 +++++- src/pocketmine/PocketMine.php | 5 + src/pocketmine/Server.php | 17 ++- .../protocol/ResourcePackChunkDataPacket.php | 13 +- .../ResourcePackChunkRequestPacket.php | 6 +- .../protocol/ResourcePackDataInfoPacket.php | 24 ++-- .../mcpe/protocol/ResourcePackStackPacket.php | 13 +- .../mcpe/protocol/ResourcePacksInfoPacket.php | 13 +- src/pocketmine/resourcepacks/ResourcePack.php | 39 ++++++ .../resourcepacks/ResourcePackManager.php | 129 ++++++++++++++++++ .../resourcepacks/ZippedResourcePack.php | 105 ++++++++++++++ 12 files changed, 370 insertions(+), 40 deletions(-) create mode 100644 src/pocketmine/resourcepacks/ResourcePack.php create mode 100644 src/pocketmine/resourcepacks/ResourcePackManager.php create mode 100644 src/pocketmine/resourcepacks/ZippedResourcePack.php diff --git a/.gitignore b/.gitignore index b08db6710..17d9ae8d1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ timings/* server.properties /pocketmine.yml memoryDump_*/* +resource_packs/ # Common IDEs .idea/ diff --git a/src/pocketmine/Player.php b/src/pocketmine/Player.php index 75e1ad3c8..2448ff2da 100644 --- a/src/pocketmine/Player.php +++ b/src/pocketmine/Player.php @@ -189,6 +189,7 @@ use pocketmine\network\SourceInterface; use pocketmine\permission\PermissibleBase; use pocketmine\permission\PermissionAttachment; use pocketmine\plugin\Plugin; +use pocketmine\resourcepacks\ResourcePack; use pocketmine\tile\ItemFrame; use pocketmine\tile\Spawnable; use pocketmine\utils\TextFormat; @@ -1934,7 +1935,10 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade $this->sendPlayStatus(PlayStatusPacket::LOGIN_SUCCESS); $pk = new ResourcePacksInfoPacket(); - $this->dataPacket($pk); //TODO: add resource packs stuff + $manager = $this->server->getResourceManager(); + $pk->resourcePackEntries = $manager->getResourceStack(); + $pk->mustAccept = $manager->resourcePacksRequired(); + $this->dataPacket($pk); return true; } @@ -1984,11 +1988,31 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade $this->close("", "must accept resource packs to join", true); break; case ResourcePackClientResponsePacket::STATUS_SEND_PACKS: - //TODO + $manager = $this->server->getResourceManager(); + foreach($packet->packIds as $uuid){ + $pack = $manager->getPackById($uuid); + if(!($pack instanceof ResourcePack)){ + //Client requested a resource pack but we don't have it available on the server + $this->close("", "disconnectionScreen.resourcePack", true); //TODO: add strings to lang files + break; + } + + $pk = new ResourcePackDataInfoPacket(); + $pk->packId = $pack->getPackId(); + $pk->maxChunkSize = 1048576; //1MB + $pk->chunkCount = $pack->getPackSize() / $pk->maxChunkSize; + $pk->compressedPackSize = $pack->getPackSize(); + $pk->sha256 = $pack->getSha256(); + $this->dataPacket($pk); + } + break; case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS: $pk = new ResourcePackStackPacket(); - $this->dataPacket($pk); //TODO: send resource stack + $manager = $this->server->getResourceManager(); + $pk->resourcePackStack = $manager->getResourceStack(); + $pk->mustAccept = $manager->resourcePacksRequired(); + $this->dataPacket($pk); break; case ResourcePackClientResponsePacket::STATUS_COMPLETED: $this->processLogin(); @@ -3292,7 +3316,20 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade } public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{ - return false; + $manager = $this->server->getResourceManager(); + $pack = $manager->getPackById($packet->packId); + if(!($pack instanceof ResourcePack)){ + $this->close("", "disconnectionScreen.resourcePack", true); + return true; + } + + $pk = new ResourcePackChunkDataPacket(); + $pk->packId = $pack->getPackId(); + $pk->chunkIndex = $packet->chunkIndex; + $pk->data = $pack->getPackChunk(1048576 * $packet->chunkIndex, 1048576); + $pk->progress = (1048576 * $packet->chunkIndex); + $this->dataPacket($pk); + return true; } public function handleTransfer(TransferPacket $packet) : bool{ diff --git a/src/pocketmine/PocketMine.php b/src/pocketmine/PocketMine.php index 67a495e21..3e69e6d9d 100644 --- a/src/pocketmine/PocketMine.php +++ b/src/pocketmine/PocketMine.php @@ -446,6 +446,11 @@ namespace pocketmine { ++$errors; } + if(!extension_loaded("openssl")){ + $logger->critical("Unable to find the OpenSSL extension."); + ++$errors; + } + if($errors > 0){ $logger->critical("Please use the installer provided on the homepage, or recompile PHP again."); $logger->shutdown(); diff --git a/src/pocketmine/Server.php b/src/pocketmine/Server.php index 4474cdaef..e5be618de 100644 --- a/src/pocketmine/Server.php +++ b/src/pocketmine/Server.php @@ -90,6 +90,7 @@ use pocketmine\plugin\Plugin; use pocketmine\plugin\PluginLoadOrder; use pocketmine\plugin\PluginManager; use pocketmine\plugin\ScriptPluginLoader; +use pocketmine\resourcepacks\ResourcePackManager; use pocketmine\scheduler\FileWriteTask; use pocketmine\scheduler\SendUsageTask; use pocketmine\scheduler\ServerScheduler; @@ -176,6 +177,9 @@ class Server{ /** @var CraftingManager */ private $craftingManager; + /** @var ResourcePackManager */ + private $resourceManager; + /** @var ConsoleCommandSender */ private $consoleSender; @@ -594,6 +598,13 @@ class Server{ return $this->craftingManager; } + /** + * @return ResourcePackManager + */ + public function getResourceManager() : ResourcePackManager{ + return $this->resourceManager; + } + /** * @return ServerScheduler */ @@ -1510,6 +1521,8 @@ class Server{ Attribute::init(); $this->craftingManager = new CraftingManager(); + $this->resourceManager = new ResourcePackManager($this, \pocketmine\PATH . "resource_packs" . DIRECTORY_SEPARATOR); + $this->pluginManager = new PluginManager($this, $this->commandMap); $this->pluginManager->subscribeToPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE, $this->consoleSender); $this->pluginManager->setUseTimings($this->getProperty("settings.enable-profiling", false)); @@ -2165,9 +2178,7 @@ class Server{ private function checkTickUpdates($currentTick, $tickTime){ foreach($this->players as $p){ - if(!$p->loggedIn and ($tickTime - $p->creationTime) >= 10){ - $p->close("", "Login timeout"); - }elseif($this->alwaysTickPlayers){ + if($this->alwaysTickPlayers){ $p->onUpdate($currentTick); } } diff --git a/src/pocketmine/network/mcpe/protocol/ResourcePackChunkDataPacket.php b/src/pocketmine/network/mcpe/protocol/ResourcePackChunkDataPacket.php index c897ce7fa..9612449db 100644 --- a/src/pocketmine/network/mcpe/protocol/ResourcePackChunkDataPacket.php +++ b/src/pocketmine/network/mcpe/protocol/ResourcePackChunkDataPacket.php @@ -31,21 +31,22 @@ class ResourcePackChunkDataPacket extends DataPacket{ const NETWORK_ID = ProtocolInfo::RESOURCE_PACK_CHUNK_DATA_PACKET; public $packId; - public $unknown1; - public $unknown2; + public $chunkIndex; + public $progress; public $data; public function decode(){ $this->packId = $this->getString(); - $this->unknown1 = $this->getLInt(); - $this->unknown2 = $this->getLLong(); + $this->chunkIndex = $this->getLInt(); + $this->progress = $this->getLLong(); $this->data = $this->get($this->getLInt()); } public function encode(){ + $this->reset(); $this->putString($this->packId); - $this->putLInt($this->unknown1); - $this->putLLong($this->unknown2); + $this->putLInt($this->chunkIndex); + $this->putLLong($this->progress); $this->putLInt(strlen($this->data)); $this->put($this->data); } diff --git a/src/pocketmine/network/mcpe/protocol/ResourcePackChunkRequestPacket.php b/src/pocketmine/network/mcpe/protocol/ResourcePackChunkRequestPacket.php index f572971a4..d9b89bcef 100644 --- a/src/pocketmine/network/mcpe/protocol/ResourcePackChunkRequestPacket.php +++ b/src/pocketmine/network/mcpe/protocol/ResourcePackChunkRequestPacket.php @@ -31,17 +31,17 @@ class ResourcePackChunkRequestPacket extends DataPacket{ const NETWORK_ID = ProtocolInfo::RESOURCE_PACK_CHUNK_REQUEST_PACKET; public $packId; - public $unknown; + public $chunkIndex; public function decode(){ $this->packId = $this->getString(); - $this->unknown = $this->getLInt(); + $this->chunkIndex = $this->getLInt(); } public function encode(){ $this->reset(); $this->putString($this->packId); - $this->putLInt($this->unknown); + $this->putLInt($this->chunkIndex); } public function handle(NetworkSession $session) : bool{ diff --git a/src/pocketmine/network/mcpe/protocol/ResourcePackDataInfoPacket.php b/src/pocketmine/network/mcpe/protocol/ResourcePackDataInfoPacket.php index c371b546e..c8970f55e 100644 --- a/src/pocketmine/network/mcpe/protocol/ResourcePackDataInfoPacket.php +++ b/src/pocketmine/network/mcpe/protocol/ResourcePackDataInfoPacket.php @@ -31,26 +31,26 @@ class ResourcePackDataInfoPacket extends DataPacket{ const NETWORK_ID = ProtocolInfo::RESOURCE_PACK_DATA_INFO_PACKET; public $packId; - public $int1; - public $int2; - public $packSize; - public $unknown; + public $maxChunkSize; + public $chunkCount; + public $compressedPackSize; + public $sha256; public function decode(){ $this->packId = $this->getString(); - $this->int1 = $this->getLInt(); - $this->int2 = $this->getLInt(); - $this->packSize = $this->getLLong(); - $this->unknown = $this->getString(); + $this->maxChunkSize = $this->getLInt(); + $this->chunkCount = $this->getLInt(); + $this->compressedPackSize = $this->getLLong(); + $this->sha256 = $this->getString(); } public function encode(){ $this->reset(); $this->putString($this->packId); - $this->putLInt($this->int1); - $this->putLInt($this->int2); - $this->putLLong($this->packSize); - $this->putString($this->unknown); + $this->putLInt($this->maxChunkSize); + $this->putLInt($this->chunkCount); + $this->putLLong($this->compressedPackSize); + $this->putString($this->sha256); } public function handle(NetworkSession $session) : bool{ diff --git a/src/pocketmine/network/mcpe/protocol/ResourcePackStackPacket.php b/src/pocketmine/network/mcpe/protocol/ResourcePackStackPacket.php index d2ab98c2e..32633a7fb 100644 --- a/src/pocketmine/network/mcpe/protocol/ResourcePackStackPacket.php +++ b/src/pocketmine/network/mcpe/protocol/ResourcePackStackPacket.php @@ -26,6 +26,7 @@ namespace pocketmine\network\mcpe\protocol; use pocketmine\network\mcpe\NetworkSession; +use pocketmine\resourcepacks\ResourcePack; use pocketmine\resourcepacks\ResourcePackInfoEntry; class ResourcePackStackPacket extends DataPacket{ @@ -33,13 +34,13 @@ class ResourcePackStackPacket extends DataPacket{ public $mustAccept = false; - /** @var ResourcePackInfoEntry[] */ + /** @var ResourcePack[] */ public $behaviorPackStack = []; - /** @var ResourcePackInfoEntry[] */ + /** @var ResourcePack[] */ public $resourcePackStack = []; public function decode(){ - $this->mustAccept = $this->getBool(); + /*$this->mustAccept = $this->getBool(); $behaviorPackCount = $this->getLShort(); while($behaviorPackCount-- > 0){ $packId = $this->getString(); @@ -52,7 +53,7 @@ class ResourcePackStackPacket extends DataPacket{ $packId = $this->getString(); $version = $this->getString(); $this->resourcePackStack[] = new ResourcePackInfoEntry($packId, $version); - } + }*/ } public function encode(){ @@ -62,13 +63,13 @@ class ResourcePackStackPacket extends DataPacket{ $this->putLShort(count($this->behaviorPackStack)); foreach($this->behaviorPackStack as $entry){ $this->putString($entry->getPackId()); - $this->putString($entry->getVersion()); + $this->putString($entry->getPackVersion()); } $this->putLShort(count($this->resourcePackStack)); foreach($this->resourcePackStack as $entry){ $this->putString($entry->getPackId()); - $this->putString($entry->getVersion()); + $this->putString($entry->getPackVersion()); } } diff --git a/src/pocketmine/network/mcpe/protocol/ResourcePacksInfoPacket.php b/src/pocketmine/network/mcpe/protocol/ResourcePacksInfoPacket.php index 8f03c1136..3b07fe359 100644 --- a/src/pocketmine/network/mcpe/protocol/ResourcePacksInfoPacket.php +++ b/src/pocketmine/network/mcpe/protocol/ResourcePacksInfoPacket.php @@ -25,19 +25,20 @@ namespace pocketmine\network\mcpe\protocol; use pocketmine\network\mcpe\NetworkSession; +use pocketmine\resourcepacks\ResourcePack; use pocketmine\resourcepacks\ResourcePackInfoEntry; class ResourcePacksInfoPacket extends DataPacket{ const NETWORK_ID = ProtocolInfo::RESOURCE_PACKS_INFO_PACKET; public $mustAccept = false; //if true, forces client to use selected resource packs - /** @var ResourcePackInfoEntry[] */ + /** @var ResourcePack[] */ public $behaviorPackEntries = []; - /** @var ResourcePackInfoEntry[] */ + /** @var ResourcePack[] */ public $resourcePackEntries = []; public function decode(){ - $this->mustAccept = $this->getBool(); + /*$this->mustAccept = $this->getBool(); $behaviorPackCount = $this->getLShort(); while($behaviorPackCount-- > 0){ $id = $this->getString(); @@ -52,7 +53,7 @@ class ResourcePacksInfoPacket extends DataPacket{ $version = $this->getString(); $size = $this->getLLong(); $this->resourcePackEntries[] = new ResourcePackInfoEntry($id, $version, $size); - } + }*/ } public function encode(){ @@ -62,13 +63,13 @@ class ResourcePacksInfoPacket extends DataPacket{ $this->putLShort(count($this->behaviorPackEntries)); foreach($this->behaviorPackEntries as $entry){ $this->putString($entry->getPackId()); - $this->putString($entry->getVersion()); + $this->putString($entry->getPackVersion()); $this->putLLong($entry->getPackSize()); } $this->putLShort(count($this->resourcePackEntries)); foreach($this->resourcePackEntries as $entry){ $this->putString($entry->getPackId()); - $this->putString($entry->getVersion()); + $this->putString($entry->getPackVersion()); $this->putLLong($entry->getPackSize()); } } diff --git a/src/pocketmine/resourcepacks/ResourcePack.php b/src/pocketmine/resourcepacks/ResourcePack.php new file mode 100644 index 000000000..fd7962896 --- /dev/null +++ b/src/pocketmine/resourcepacks/ResourcePack.php @@ -0,0 +1,39 @@ +server = $server; + $this->path = $path; + + if(!file_exists($this->path)){ + $this->server->getLogger()->debug("Resource packs path $path does not exist, creating directory"); + mkdir($this->path); + }elseif(!is_dir($this->path)){ + throw new \InvalidArgumentException("Resource packs path $path exists and is not a directory"); + } + + $this->resourcePacksConfig = new Config($this->path . "resource_packs.yml", Config::YAML, []); + + if(count($this->resourcePacksConfig->getAll()) === 0){ + $this->resourcePacksConfig->set("force_resources", false); + $this->resourcePacksConfig->set("resource_stack", []); + $this->resourcePacksConfig->save(); + } + + $this->serverForceResources = (bool) $this->resourcePacksConfig->get("force_resources", false); + + $this->server->getLogger()->info("Loading resource packs..."); + + foreach($this->resourcePacksConfig->get("resource_stack", []) as $pos => $pack){ + try{ + $packPath = $this->path . DIRECTORY_SEPARATOR . $pack; + if(file_exists($packPath)){ + $newPack = null; + //Detect the type of resource pack. + if(is_dir($packPath)){ + $this->server->getLogger()->warning("Skipped resource entry $pack due to directory resource packs currently unsupported"); + }else{ + $info = new \SplFileInfo($packPath); + switch($info->getExtension()){ + case "zip": + $newPack = new ZippedResourcePack($packPath); + break; + default: + $this->server->getLogger()->warning("Skipped resource entry $pack due to format not recognized"); + break; + } + } + + if($newPack instanceof ResourcePack){ + $this->resourcePacks[] = $newPack; + $this->uuidList[$newPack->getPackId()] = $newPack; + } + }else{ + $this->server->getLogger()->warning("Skipped resource entry $pack due to file or directory not found"); + } + }catch(\Throwable $e){ + $this->server->getLogger()->logException($e); + } + } + + $this->server->getLogger()->debug("Successfully loaded " . count($this->resourcePacks) . " resource packs"); + } + + /** + * @return bool + */ + public function resourcePacksRequired() : bool{ + return $this->serverForceResources; + } + + /** + * @return ResourcePack[] + */ + public function getResourceStack() : array{ + return $this->resourcePacks; + } + + /** + * @param string $id + * + * @return ResourcePack|null + */ + public function getPackById(string $id){ + return $this->uuidList[$id] ?? null; + } +} \ No newline at end of file diff --git a/src/pocketmine/resourcepacks/ZippedResourcePack.php b/src/pocketmine/resourcepacks/ZippedResourcePack.php new file mode 100644 index 000000000..40977d5d2 --- /dev/null +++ b/src/pocketmine/resourcepacks/ZippedResourcePack.php @@ -0,0 +1,105 @@ +format_version) or !isset($manifest->header) or !isset($manifest->modules)){ + return false; + } + + //Right now we don't care about anything else, only the stuff we're sending to clients. + //TODO: add more manifest validation + return + isset($manifest->header->description) and + isset($manifest->header->name) and + isset($manifest->header->uuid) and + isset($manifest->header->version) and + count($manifest->header->version) === 3; + } + + /** @var string */ + protected $path; + + /** @var \stdClass */ + protected $manifest; + + /** @var string */ + protected $sha256 = null; + + + public function __construct(string $zipPath){ + $this->path = $zipPath; + + if(!file_exists($zipPath)){ + throw new \InvalidArgumentException("Could not open resource pack $zipPath: file not found"); + } + + $archive = new \ZipArchive(); + if(($openResult = $archive->open($zipPath)) !== true){ + throw new \InvalidStateException("Encountered ZipArchive error code $openResult while trying to open $zipPath"); + } + + if(($manifestData = $archive->getFromName("manifest.json")) === false){ + throw new \InvalidStateException("Could not load resource pack from $zipPath: manifest.json not found"); + } + + $archive->close(); + + $manifest = json_decode($manifestData); + if(!self::verifyManifest($manifest)){ + throw new \InvalidStateException("Could not load resource pack from $zipPath: manifest.json is invalid or incomplete"); + } + + $this->manifest = $manifest; + } + + public function getPackName() : string{ + return $this->manifest->header->name; + } + + public function getPackVersion() : string{ + return implode(".", $this->manifest->header->version); + } + + public function getPackId() : string{ + return $this->manifest->header->uuid; + } + + public function getPackSize() : int{ + return filesize($this->path); + } + + public function getSha256(bool $cached = true) : string{ + if($this->sha256 === null or !$cached){ + $this->sha256 = openssl_digest(file_get_contents($this->path), "sha256", true); + } + return $this->sha256; + } + + public function getPackChunk(int $start, int $length) : string{ + return substr(file_get_contents($this->path), $start, $length); + } +} \ No newline at end of file