From e6cecabf3f55a2718f6239d40a6daf021ed1c717 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Fri, 29 Sep 2017 14:09:00 +0100 Subject: [PATCH] New skin API, add support for custom capes & custom geometry (#1416) * Added support for changing skins ingame, custom capes & geometry * Use PlayerSkinPacket for setting Human skin instead of PlayerList hack --- src/pocketmine/Player.php | 54 +++++++---- src/pocketmine/PocketMine.php | 2 +- src/pocketmine/Server.php | 12 +-- src/pocketmine/entity/Human.php | 75 ++++++++++------ src/pocketmine/entity/Skin.php | 90 +++++++++++++++++++ .../event/player/PlayerChangeSkinEvent.php | 78 ++++++++++++++++ .../mcpe/PlayerNetworkSessionAdapter.php | 2 +- .../network/mcpe/protocol/LoginPacket.php | 10 --- .../mcpe/protocol/PlayerListPacket.php | 23 ++--- .../mcpe/protocol/PlayerSkinPacket.php | 41 ++++----- .../mcpe/protocol/types/PlayerListEntry.php | 31 ++----- tests/plugins/PocketMine-DevTools | 2 +- tests/plugins/PocketMine-TesterPlugin | 2 +- 13 files changed, 302 insertions(+), 120 deletions(-) create mode 100644 src/pocketmine/entity/Skin.php create mode 100644 src/pocketmine/event/player/PlayerChangeSkinEvent.php diff --git a/src/pocketmine/Player.php b/src/pocketmine/Player.php index d0e328f48..8bc498dc5 100644 --- a/src/pocketmine/Player.php +++ b/src/pocketmine/Player.php @@ -34,6 +34,7 @@ use pocketmine\entity\Entity; use pocketmine\entity\Human; use pocketmine\entity\Item as DroppedItem; use pocketmine\entity\Living; +use pocketmine\entity\Skin; use pocketmine\event\entity\EntityDamageByBlockEvent; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; @@ -46,6 +47,7 @@ use pocketmine\event\player\PlayerAnimationEvent; use pocketmine\event\player\PlayerBedEnterEvent; use pocketmine\event\player\PlayerBedLeaveEvent; use pocketmine\event\player\PlayerBlockPickEvent; +use pocketmine\event\player\PlayerChangeSkinEvent; use pocketmine\event\player\PlayerChatEvent; use pocketmine\event\player\PlayerCommandPreprocessEvent; use pocketmine\event\player\PlayerDeathEvent; @@ -182,15 +184,6 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ return $lname !== "rcon" and $lname !== "console" and $len >= 1 and $len <= 16 and preg_match("/[^A-Za-z0-9_ ]/", $name) === 0; } - /** - * Checks the length of a supplied skin bitmap and returns whether the length is valid. - * @param string $skin - * - * @return bool - */ - public static function isValidSkin(string $skin) : bool{ - return strlen($skin) === 64 * 64 * 4 or strlen($skin) === 64 * 32 * 4; - } /** @var SourceInterface */ protected $interface; @@ -717,15 +710,36 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ public function setDisplayName(string $name){ $this->displayName = $name; if($this->spawned){ - $this->server->updatePlayerListData($this->getUniqueId(), $this->getId(), $this->getDisplayName(), $this->getSkinId(), $this->getSkinData()); + $this->server->updatePlayerListData($this->getUniqueId(), $this->getId(), $this->getDisplayName(), $this->getSkin()); } } - public function setSkin(string $str, string $skinId){ - parent::setSkin($str, $skinId); - if($this->spawned){ - $this->server->updatePlayerListData($this->getUniqueId(), $this->getId(), $this->getDisplayName(), $skinId, $str); + /** + * Called when a player changes their skin. + * Plugin developers should not use this, use setSkin() and sendSkin() instead. + * + * @param Skin $skin + * @param string $newSkinName + * @param string $oldSkinName + * + * @return bool + */ + public function changeSkin(Skin $skin, string $newSkinName, string $oldSkinName) : bool{ + if(!$skin->isValid()){ + return false; } + + $ev = new PlayerChangeSkinEvent($this, $this->getSkin(), $skin); + $this->server->getPluginManager()->callEvent($ev); + + if($ev->isCancelled()){ + $this->sendSkin([$this]); + return true; + } + + $this->setSkin($ev->getNewSkin()); + $this->sendSkin($this->server->getOnlinePlayers()); + return true; } public function jump(){ @@ -2005,12 +2019,20 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ return true; } - if(!Player::isValidSkin($packet->skin)){ + $skin = new Skin( + $packet->clientData["SkinId"], + base64_decode($packet->clientData["SkinData"] ?? ""), + base64_decode($packet->clientData["CapeData"] ?? ""), + $packet->clientData["SkinGeometryName"], + base64_decode($packet->clientData["SkinGeometry"] ?? "") + ); + + if(!$skin->isValid()){ $this->close("", "disconnectionScreen.invalidSkin"); return true; } - $this->setSkin($packet->skin, $packet->skinId); + $this->setSkin($skin); if(!$this->server->isWhitelisted($this->iusername) and $this->kick("Server is white-listed", false)){ return true; diff --git a/src/pocketmine/PocketMine.php b/src/pocketmine/PocketMine.php index 5b06bcf66..e8bb4cfe2 100644 --- a/src/pocketmine/PocketMine.php +++ b/src/pocketmine/PocketMine.php @@ -81,7 +81,7 @@ namespace pocketmine { const NAME = "PocketMine-MP"; const VERSION = "1.7dev"; - const API_VERSION = "3.0.0-ALPHA8"; + const API_VERSION = "3.0.0-ALPHA9"; const CODENAME = "[REDACTED]"; /* diff --git a/src/pocketmine/Server.php b/src/pocketmine/Server.php index e482da8c2..c354dcb34 100644 --- a/src/pocketmine/Server.php +++ b/src/pocketmine/Server.php @@ -36,6 +36,7 @@ use pocketmine\command\SimpleCommandMap; use pocketmine\entity\Attribute; use pocketmine\entity\Effect; use pocketmine\entity\Entity; +use pocketmine\entity\Skin; use pocketmine\event\HandlerList; use pocketmine\event\level\LevelInitEvent; use pocketmine\event\level\LevelLoadEvent; @@ -2283,7 +2284,7 @@ class Server{ } public function addOnlinePlayer(Player $player){ - $this->updatePlayerListData($player->getUniqueId(), $player->getId(), $player->getDisplayName(), $player->getSkinId(), $player->getSkinData()); + $this->updatePlayerListData($player->getUniqueId(), $player->getId(), $player->getDisplayName(), $player->getSkin()); $this->playerList[$player->getRawUniqueId()] = $player; } @@ -2300,15 +2301,14 @@ class Server{ * @param UUID $uuid * @param int $entityId * @param string $name - * @param string $skinId - * @param string $skinData + * @param Skin $skin * @param Player[]|null $players */ - public function updatePlayerListData(UUID $uuid, int $entityId, string $name, string $skinId, string $skinData, array $players = null){ + public function updatePlayerListData(UUID $uuid, int $entityId, string $name, Skin $skin, array $players = null){ $pk = new PlayerListPacket(); $pk->type = PlayerListPacket::TYPE_ADD; - $pk->entries[] = PlayerListEntry::createAdditionEntry($uuid, $entityId, $name, $skinId, $skinData); + $pk->entries[] = PlayerListEntry::createAdditionEntry($uuid, $entityId, $name, $skin); $this->broadcastPacket($players ?? $this->playerList, $pk); } @@ -2330,7 +2330,7 @@ class Server{ $pk = new PlayerListPacket(); $pk->type = PlayerListPacket::TYPE_ADD; foreach($this->playerList as $player){ - $pk->entries[] = PlayerListEntry::createAdditionEntry($player->getUniqueId(), $player->getId(), $player->getDisplayName(), $player->getSkinId(), $player->getSkinData()); + $pk->entries[] = PlayerListEntry::createAdditionEntry($player->getUniqueId(), $player->getId(), $player->getDisplayName(), $player->getSkin()); } $p->dataPacket($pk); diff --git a/src/pocketmine/entity/Human.php b/src/pocketmine/entity/Human.php index 8dbf99255..528b11344 100644 --- a/src/pocketmine/entity/Human.php +++ b/src/pocketmine/entity/Human.php @@ -37,6 +37,7 @@ use pocketmine\nbt\tag\IntTag; use pocketmine\nbt\tag\ListTag; use pocketmine\nbt\tag\StringTag; use pocketmine\network\mcpe\protocol\AddPlayerPacket; +use pocketmine\network\mcpe\protocol\PlayerSkinPacket; use pocketmine\Player; use pocketmine\utils\UUID; @@ -60,8 +61,8 @@ class Human extends Creature implements ProjectileSource, InventoryHolder{ public $height = 1.8; public $eyeHeight = 1.62; - protected $skinId; - protected $skin = ""; + /** @var Skin */ + protected $skin; protected $foodTickTimer = 0; @@ -71,19 +72,22 @@ class Human extends Creature implements ProjectileSource, InventoryHolder{ protected $baseOffset = 1.62; public function __construct(Level $level, CompoundTag $nbt){ - if($this->skin === "" and (!isset($nbt->Skin) or !isset($nbt->Skin->Data) or !Player::isValidSkin($nbt->Skin->Data->getValue()))){ + if($this->skin === null and (!isset($nbt->Skin) or !isset($nbt->Skin->Data) or !Player::isValidSkin($nbt->Skin->Data->getValue()))){ throw new \InvalidStateException((new \ReflectionClass($this))->getShortName() . " must have a valid skin set"); } parent::__construct($level, $nbt); } - public function getSkinData(){ - return $this->skin; - } - - public function getSkinId(){ - return $this->skinId; + /** + * Checks the length of a supplied skin bitmap and returns whether the length is valid. + * + * @param string $skin + * + * @return bool + */ + public static function isValidSkin(string $skin) : bool{ + return strlen($skin) === 64 * 64 * 4 or strlen($skin) === 64 * 32 * 4; } /** @@ -101,16 +105,35 @@ class Human extends Creature implements ProjectileSource, InventoryHolder{ } /** - * @param string $str - * @param string $skinId + * Returns a Skin object containing information about this human's skin. + * @return Skin */ - public function setSkin(string $str, string $skinId){ - if(!Player::isValidSkin($str)){ + public function getSkin() : Skin{ + return $this->skin; + } + + /** + * Sets the human's skin. This will not send any update to viewers, you need to do that manually using + * {@link sendSkin}. + * + * @param Skin $skin + */ + public function setSkin(Skin $skin) : void{ + if(!$skin->isValid()){ throw new \InvalidStateException("Specified skin is not valid, must be 8KiB or 16KiB"); } - $this->skin = $str; - $this->skinId = $skinId; + $this->skin = $skin; + } + + /** + * @param Player[] $targets + */ + public function sendSkin(array $targets) : void{ + $pk = new PlayerSkinPacket(); + $pk->uuid = $this->getUniqueId(); + $pk->skin = $this->skin; + $this->server->broadcastPacket($targets, $pk); } public function jump(){ @@ -287,10 +310,13 @@ class Human extends Creature implements ProjectileSource, InventoryHolder{ } if(isset($this->namedtag->Skin) and $this->namedtag->Skin instanceof CompoundTag){ - $this->setSkin($this->namedtag->Skin["Data"], $this->namedtag->Skin["Name"]); + $this->setSkin(new Skin( + $this->namedtag->Skin["Name"], + $this->namedtag->Skin["Data"] + )); } - $this->uuid = UUID::fromData((string) $this->getId(), $this->getSkinData(), $this->getNameTag()); + $this->uuid = UUID::fromData((string) $this->getId(), $this->skin->getSkinData(), $this->getNameTag()); } protected function initEntity(){ @@ -467,10 +493,11 @@ class Human extends Creature implements ProjectileSource, InventoryHolder{ $this->namedtag->SelectedInventorySlot = new IntTag("SelectedInventorySlot", $this->inventory->getHeldItemIndex()); } - if(strlen($this->getSkinData()) > 0){ + if($this->skin !== null){ $this->namedtag->Skin = new CompoundTag("Skin", [ - new StringTag("Data", $this->getSkinData()), - new StringTag("Name", $this->getSkinId()) + //TODO: save cape & geometry + new StringTag("Data", $this->skin->getSkinData()), + new StringTag("Name", $this->skin->getSkinId()) ]); } } @@ -479,14 +506,10 @@ class Human extends Creature implements ProjectileSource, InventoryHolder{ if($player !== $this and !isset($this->hasSpawned[$player->getLoaderId()])){ $this->hasSpawned[$player->getLoaderId()] = $player; - if(!Player::isValidSkin($this->skin)){ + if(!$this->skin->isValid()){ throw new \InvalidStateException((new \ReflectionClass($this))->getShortName() . " must have a valid skin set"); } - if(!($this instanceof Player)){ - $this->server->updatePlayerListData($this->getUniqueId(), $this->getId(), $this->getName(), $this->skinId, $this->skin, [$player]); - } - $pk = new AddPlayerPacket(); $pk->uuid = $this->getUniqueId(); $pk->username = $this->getName(); @@ -502,7 +525,7 @@ class Human extends Creature implements ProjectileSource, InventoryHolder{ $this->inventory->sendArmorContents($player); if(!($this instanceof Player)){ - $this->server->removePlayerListData($this->getUniqueId(), [$player]); + $this->sendSkin([$player]); } } } diff --git a/src/pocketmine/entity/Skin.php b/src/pocketmine/entity/Skin.php new file mode 100644 index 000000000..7c5e3b759 --- /dev/null +++ b/src/pocketmine/entity/Skin.php @@ -0,0 +1,90 @@ +skinId = $skinId; + $this->skinData = $skinData; + $this->capeData = $capeData; + $this->geometryName = $geometryName; + $this->geometryData = $geometryData; + } + + public function isValid() : bool{ + return ( + $this->skinId !== "" and + (($s = strlen($this->skinData)) === 16384 or $s === 8192) and + ($this->capeData === "" or strlen($this->capeData) === 8192) + ); + } + + /** + * @return string + */ + public function getSkinId() : string{ + return $this->skinId; + } + + /** + * @return string + */ + public function getSkinData() : string{ + return $this->skinData; + } + + /** + * @return string + */ + public function getCapeData() : string{ + return $this->capeData; + } + + /** + * @return string + */ + public function getGeometryName() : string{ + return $this->geometryName; + } + + /** + * @return string + */ + public function getGeometryData() : string{ + return $this->geometryData; + } + +} \ No newline at end of file diff --git a/src/pocketmine/event/player/PlayerChangeSkinEvent.php b/src/pocketmine/event/player/PlayerChangeSkinEvent.php new file mode 100644 index 000000000..992ee2cf6 --- /dev/null +++ b/src/pocketmine/event/player/PlayerChangeSkinEvent.php @@ -0,0 +1,78 @@ +player = $player; + $this->oldSkin = $oldSkin; + $this->newSkin = $newSkin; + } + + /** + * @return Skin + */ + public function getOldSkin() : Skin{ + return $this->oldSkin; + } + + /** + * @return Skin + */ + public function getNewSkin() : Skin{ + return $this->newSkin; + } + + /** + * @param Skin $skin + * @throws \InvalidArgumentException if the specified skin is not valid + */ + public function setNewSkin(Skin $skin) : void{ + if(!$skin->isValid()){ + throw new \InvalidArgumentException("Skin format is invalid"); + } + + $this->newSkin = $skin; + } + +} \ No newline at end of file diff --git a/src/pocketmine/network/mcpe/PlayerNetworkSessionAdapter.php b/src/pocketmine/network/mcpe/PlayerNetworkSessionAdapter.php index fd6e7dd86..45c35740a 100644 --- a/src/pocketmine/network/mcpe/PlayerNetworkSessionAdapter.php +++ b/src/pocketmine/network/mcpe/PlayerNetworkSessionAdapter.php @@ -228,7 +228,7 @@ class PlayerNetworkSessionAdapter extends NetworkSession{ } public function handlePlayerSkin(PlayerSkinPacket $packet) : bool{ - return false; //TODO + return $this->player->changeSkin($packet->skin, $packet->newSkinName, $packet->oldSkinName); } public function handleModalFormResponse(ModalFormResponsePacket $packet) : bool{ diff --git a/src/pocketmine/network/mcpe/protocol/LoginPacket.php b/src/pocketmine/network/mcpe/protocol/LoginPacket.php index f74f48df1..3e47b7714 100644 --- a/src/pocketmine/network/mcpe/protocol/LoginPacket.php +++ b/src/pocketmine/network/mcpe/protocol/LoginPacket.php @@ -48,11 +48,6 @@ class LoginPacket extends DataPacket{ /** @var string */ public $serverAddress; - /** @var string */ - public $skinId; - /** @var string */ - public $skin = ""; - /** @var array (the "chain" index contains one or more JWTs) */ public $chainData = []; /** @var string */ @@ -102,11 +97,6 @@ class LoginPacket extends DataPacket{ $this->clientId = $this->clientData["ClientRandomId"] ?? null; $this->serverAddress = $this->clientData["ServerAddress"] ?? null; - $this->skinId = $this->clientData["SkinId"] ?? null; - - if(isset($this->clientData["SkinData"])){ - $this->skin = base64_decode($this->clientData["SkinData"]); - } } protected function encodePayload(){ diff --git a/src/pocketmine/network/mcpe/protocol/PlayerListPacket.php b/src/pocketmine/network/mcpe/protocol/PlayerListPacket.php index 3f6760c42..18b95ce02 100644 --- a/src/pocketmine/network/mcpe/protocol/PlayerListPacket.php +++ b/src/pocketmine/network/mcpe/protocol/PlayerListPacket.php @@ -26,6 +26,7 @@ namespace pocketmine\network\mcpe\protocol; #include +use pocketmine\entity\Skin; use pocketmine\network\mcpe\NetworkSession; use pocketmine\network\mcpe\protocol\types\PlayerListEntry; @@ -55,11 +56,13 @@ class PlayerListPacket extends DataPacket{ $entry->uuid = $this->getUUID(); $entry->entityUniqueId = $this->getEntityUniqueId(); $entry->username = $this->getString(); - $entry->skinId = $this->getString(); - $entry->skinData = $this->getString(); - $entry->capeData = $this->getString(); - $entry->geometryModel = $this->getString(); - $entry->geometryData = $this->getString(); + $entry->skin = new Skin( + $this->getString(), //id + $this->getString(), //data + $this->getString(), //cape + $this->getString(), //geometry name + $this->getString() //geometry data + ); $entry->xboxUserId = $this->getString(); }else{ $entry->uuid = $this->getUUID(); @@ -77,11 +80,11 @@ class PlayerListPacket extends DataPacket{ $this->putUUID($entry->uuid); $this->putEntityUniqueId($entry->entityUniqueId); $this->putString($entry->username); - $this->putString($entry->skinId); - $this->putString($entry->skinData); - $this->putString($entry->capeData); - $this->putString($entry->geometryModel); - $this->putString($entry->geometryData); + $this->putString($entry->skin->getSkinId()); + $this->putString($entry->skin->getSkinData()); + $this->putString($entry->skin->getCapeData()); + $this->putString($entry->skin->getGeometryName()); + $this->putString($entry->skin->getGeometryData()); $this->putString($entry->xboxUserId); }else{ $this->putUUID($entry->uuid); diff --git a/src/pocketmine/network/mcpe/protocol/PlayerSkinPacket.php b/src/pocketmine/network/mcpe/protocol/PlayerSkinPacket.php index 8ad835d98..0ed5c6fde 100644 --- a/src/pocketmine/network/mcpe/protocol/PlayerSkinPacket.php +++ b/src/pocketmine/network/mcpe/protocol/PlayerSkinPacket.php @@ -25,6 +25,7 @@ namespace pocketmine\network\mcpe\protocol; #include +use pocketmine\entity\Skin; use pocketmine\network\mcpe\NetworkSession; use pocketmine\utils\UUID; @@ -34,41 +35,37 @@ class PlayerSkinPacket extends DataPacket{ /** @var UUID */ public $uuid; /** @var string */ - public $skinId; + public $oldSkinName = ""; /** @var string */ - public $newSkinName; - /** @var string */ - public $oldSkinName; - /** @var string */ - public $skinData; - /** @var string */ - public $capeData; - /** @var string */ - public $geometryModel; - /** @var string */ - public $geometryData; + public $newSkinName = ""; + /** @var Skin */ + public $skin; protected function decodePayload(){ $this->uuid = $this->getUUID(); - $this->skinId = $this->getString(); + + $skinId = $this->getString(); $this->newSkinName = $this->getString(); $this->oldSkinName = $this->getString(); - $this->skinData = $this->getString(); - $this->capeData = $this->getString(); - $this->geometryModel = $this->getString(); - $this->geometryData = $this->getString(); + $skinData = $this->getString(); + $capeData = $this->getString(); + $geometryModel = $this->getString(); + $geometryData = $this->getString(); + + $this->skin = new Skin($skinId, $skinData, $capeData, $geometryModel, $geometryData); } protected function encodePayload(){ $this->putUUID($this->uuid); - $this->putString($this->skinId); + + $this->putString($this->skin->getSkinId()); $this->putString($this->newSkinName); $this->putString($this->oldSkinName); - $this->putString($this->skinData); - $this->putString($this->capeData); - $this->putString($this->geometryModel); - $this->putString($this->geometryData); + $this->putString($this->skin->getSkinData()); + $this->putString($this->skin->getCapeData()); + $this->putString($this->skin->getGeometryName()); + $this->putString($this->skin->getGeometryData()); } public function handle(NetworkSession $session) : bool{ diff --git a/src/pocketmine/network/mcpe/protocol/types/PlayerListEntry.php b/src/pocketmine/network/mcpe/protocol/types/PlayerListEntry.php index c334368b1..d92de795e 100644 --- a/src/pocketmine/network/mcpe/protocol/types/PlayerListEntry.php +++ b/src/pocketmine/network/mcpe/protocol/types/PlayerListEntry.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\protocol\types; +use pocketmine\entity\Skin; use pocketmine\utils\UUID; class PlayerListEntry{ @@ -33,16 +34,8 @@ class PlayerListEntry{ public $entityUniqueId; /** @var string */ public $username; - /** @var string */ - public $skinId; - /** @var string */ - public $skinData; - /** @var string */ - public $capeData; //TODO - /** @var string */ - public $geometryModel; //TODO - /** @var string */ - public $geometryData; //TODO + /** @var Skin */ + public $skin; /** @var string */ public $xboxUserId; //TODO @@ -53,26 +46,12 @@ class PlayerListEntry{ return $entry; } - public static function createAdditionEntry( - UUID $uuid, - int $entityUniqueId, - string $username, - string $skinId, - string $skinData, - string $capeData = "", - string $geometryModel = "", - string $geometryData = "", - string $xboxUserId = "" - ) : PlayerListEntry{ + public static function createAdditionEntry(UUID $uuid, int $entityUniqueId, string $username, Skin $skin, string $xboxUserId = "") : PlayerListEntry{ $entry = new PlayerListEntry(); $entry->uuid = $uuid; $entry->entityUniqueId = $entityUniqueId; $entry->username = $username; - $entry->skinId = $skinId; - $entry->skinData = $skinData; - $entry->capeData = $capeData; - $entry->geometryModel = $geometryModel; - $entry->geometryData = $geometryData; + $entry->skin = $skin; $entry->xboxUserId = $xboxUserId; return $entry; diff --git a/tests/plugins/PocketMine-DevTools b/tests/plugins/PocketMine-DevTools index eec5da244..857e8afab 160000 --- a/tests/plugins/PocketMine-DevTools +++ b/tests/plugins/PocketMine-DevTools @@ -1 +1 @@ -Subproject commit eec5da2443b84821bd7e7224adea4921c81dc674 +Subproject commit 857e8afab4a7a34c7235ce06e43796bf64581e3d diff --git a/tests/plugins/PocketMine-TesterPlugin b/tests/plugins/PocketMine-TesterPlugin index 924972328..7e084a9ee 160000 --- a/tests/plugins/PocketMine-TesterPlugin +++ b/tests/plugins/PocketMine-TesterPlugin @@ -1 +1 @@ -Subproject commit 9249723281f7ee63e51cd51862682187bbe70ad2 +Subproject commit 7e084a9eea099fadf0eb52c683b367b340a2b570