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
This commit is contained in:
Dylan K. Taylor 2017-09-29 14:09:00 +01:00 committed by GitHub
parent c273a46537
commit e6cecabf3f
13 changed files with 302 additions and 120 deletions

View File

@ -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;

View File

@ -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]";
/*

View File

@ -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);

View File

@ -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]);
}
}
}

View File

@ -0,0 +1,90 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\entity;
class Skin{
/** @var string */
private $skinId;
/** @var string */
private $skinData;
/** @var string */
private $capeData;
/** @var string */
private $geometryName;
/** @var string */
private $geometryData;
public function __construct(string $skinId, string $skinData, string $capeData = "", string $geometryName = "", string $geometryData = ""){
$this->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;
}
}

View File

@ -0,0 +1,78 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\event\player;
use pocketmine\entity\Skin;
use pocketmine\event\Cancellable;
use pocketmine\Player;
/**
* Called when a player changes their skin in-game.
*/
class PlayerChangeSkinEvent extends PlayerEvent implements Cancellable{
public static $handlerList = null;
/** @var Skin */
private $oldSkin;
/** @var Skin */
private $newSkin;
/**
* @param Player $player
* @param Skin $oldSkin
* @param Skin $newSkin
*/
public function __construct(Player $player, Skin $oldSkin, Skin $newSkin){
$this->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;
}
}

View File

@ -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{

View File

@ -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(){

View File

@ -26,6 +26,7 @@ namespace pocketmine\network\mcpe\protocol;
#include <rules/DataPacket.h>
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);

View File

@ -25,6 +25,7 @@ namespace pocketmine\network\mcpe\protocol;
#include <rules/DataPacket.h>
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{

View File

@ -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;

@ -1 +1 @@
Subproject commit eec5da2443b84821bd7e7224adea4921c81dc674
Subproject commit 857e8afab4a7a34c7235ce06e43796bf64581e3d

@ -1 +1 @@
Subproject commit 9249723281f7ee63e51cd51862682187bbe70ad2
Subproject commit 7e084a9eea099fadf0eb52c683b367b340a2b570