mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-09-06 09:56:06 +00:00
Initial mass migration to session handlers
This introduces several new session handlers, splitting up session handling into several new states: - Login: Only allows handling the LoginPacket. This is the only time LoginPacket can be sent, and it'll be discarded when sent at any other time. - Resource packs: Handles only the resource packs sequence (downloading packs and such). This is the only time ResourcePackClientResponse and ResourcePackChunkRequest will be handled. - Pre-spawn: Only chunk radius requests are accepted during this state. SimpleNetworkHandler handles all the "rest" of the logic that hasn't yet been separated out into their own dedicated handlers. There's also a NullNetworkHandler which discards all packets while it's active. This solves a large number of issues with the security of the login sequence. It solves a range of possible DoS attacks and crashes, while also allowing great code simplification and cleanup.
This commit is contained in:
@ -25,11 +25,15 @@ namespace pocketmine\network\mcpe;
|
||||
|
||||
use pocketmine\event\server\DataPacketReceiveEvent;
|
||||
use pocketmine\event\server\DataPacketSendEvent;
|
||||
use pocketmine\network\mcpe\handler\LoginSessionHandler;
|
||||
use pocketmine\network\mcpe\handler\PreSpawnSessionHandler;
|
||||
use pocketmine\network\mcpe\handler\ResourcePacksSessionHandler;
|
||||
use pocketmine\network\mcpe\handler\SessionHandler;
|
||||
use pocketmine\network\mcpe\handler\SimpleSessionHandler;
|
||||
use pocketmine\network\mcpe\protocol\DataPacket;
|
||||
use pocketmine\network\mcpe\protocol\DisconnectPacket;
|
||||
use pocketmine\network\mcpe\protocol\PacketPool;
|
||||
use pocketmine\network\mcpe\protocol\PlayStatusPacket;
|
||||
use pocketmine\network\NetworkInterface;
|
||||
use pocketmine\Player;
|
||||
use pocketmine\Server;
|
||||
@ -60,7 +64,7 @@ class NetworkSession{
|
||||
$this->ip = $ip;
|
||||
$this->port = $port;
|
||||
|
||||
$this->setHandler(new SimpleSessionHandler($player));
|
||||
$this->setHandler(new LoginSessionHandler($player, $this));
|
||||
}
|
||||
|
||||
public function getInterface() : NetworkInterface{
|
||||
@ -144,4 +148,29 @@ class NetworkSession{
|
||||
}
|
||||
$this->interface->close($this->player, $notify ? $reason : "");
|
||||
}
|
||||
|
||||
//TODO: onEnableEncryption() step
|
||||
|
||||
public function onLoginSuccess() : void{
|
||||
$pk = new PlayStatusPacket();
|
||||
$pk->status = PlayStatusPacket::LOGIN_SUCCESS;
|
||||
$this->sendDataPacket($pk);
|
||||
|
||||
$this->setHandler(new ResourcePacksSessionHandler($this->server, $this->player, $this));
|
||||
}
|
||||
|
||||
public function onResourcePacksDone() : void{
|
||||
$this->player->_actuallyConstruct();
|
||||
|
||||
$this->setHandler(new PreSpawnSessionHandler($this->server, $this->player, $this));
|
||||
}
|
||||
|
||||
public function onSpawn() : void{
|
||||
$pk = new PlayStatusPacket();
|
||||
$pk->status = PlayStatusPacket::PLAYER_SPAWN;
|
||||
$this->sendDataPacket($pk);
|
||||
|
||||
//TODO: split this up even further
|
||||
$this->setHandler(new SimpleSessionHandler($this->player));
|
||||
}
|
||||
}
|
||||
|
89
src/pocketmine/network/mcpe/handler/LoginSessionHandler.php
Normal file
89
src/pocketmine/network/mcpe/handler/LoginSessionHandler.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?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\network\mcpe\handler;
|
||||
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\network\mcpe\protocol\LoginPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayStatusPacket;
|
||||
use pocketmine\network\mcpe\protocol\ProtocolInfo;
|
||||
use pocketmine\Player;
|
||||
|
||||
/**
|
||||
* Handles the initial login phase of the session. This handler is used as the initial state.
|
||||
*/
|
||||
class LoginSessionHandler extends SessionHandler{
|
||||
|
||||
/** @var Player */
|
||||
private $player;
|
||||
/** @var NetworkSession */
|
||||
private $session;
|
||||
|
||||
public function __construct(Player $player, NetworkSession $session){
|
||||
$this->player = $player;
|
||||
$this->session = $session;
|
||||
}
|
||||
|
||||
public function handleLogin(LoginPacket $packet) : bool{
|
||||
if(!$this->isCompatibleProtocol($packet->protocol)){
|
||||
$pk = new PlayStatusPacket();
|
||||
$pk->status = $packet->protocol < ProtocolInfo::CURRENT_PROTOCOL ?
|
||||
PlayStatusPacket::LOGIN_FAILED_CLIENT : PlayStatusPacket::LOGIN_FAILED_SERVER;
|
||||
$pk->protocol = $packet->protocol;
|
||||
$this->session->sendDataPacket($pk, true);
|
||||
|
||||
//This pocketmine disconnect message will only be seen by the console (PlayStatusPacket causes the messages to be shown for the client)
|
||||
$this->player->close(
|
||||
"",
|
||||
$this->player->getServer()->getLanguage()->translateString("pocketmine.disconnect.incompatibleProtocol", [$packet->protocol]),
|
||||
false
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!Player::isValidUserName($packet->username)){
|
||||
$this->player->close("", "disconnectionScreen.invalidName");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if($packet->skin === null or !$packet->skin->isValid()){
|
||||
$this->player->close("", "disconnectionScreen.invalidSkin");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if($this->player->handleLogin($packet)){
|
||||
if($this->session->getHandler() === $this){ //when login verification is disabled, the handler will already have been replaced
|
||||
$this->session->setHandler(new NullSessionHandler()); //drop packets received during login verification
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function isCompatibleProtocol(int $protocolVersion) : bool{
|
||||
return $protocolVersion === ProtocolInfo::CURRENT_PROTOCOL;
|
||||
}
|
||||
}
|
31
src/pocketmine/network/mcpe/handler/NullSessionHandler.php
Normal file
31
src/pocketmine/network/mcpe/handler/NullSessionHandler.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?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\network\mcpe\handler;
|
||||
|
||||
/**
|
||||
* Handler which simply ignores all packets received.
|
||||
*/
|
||||
class NullSessionHandler extends SessionHandler{
|
||||
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
<?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\network\mcpe\handler;
|
||||
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\network\mcpe\protocol\RequestChunkRadiusPacket;
|
||||
use pocketmine\network\mcpe\protocol\StartGamePacket;
|
||||
use pocketmine\network\mcpe\protocol\types\DimensionIds;
|
||||
use pocketmine\Player;
|
||||
use pocketmine\Server;
|
||||
|
||||
/**
|
||||
* Handler used for the pre-spawn phase of the session.
|
||||
*/
|
||||
class PreSpawnSessionHandler extends SessionHandler{
|
||||
|
||||
/** @var Server */
|
||||
private $server;
|
||||
/** @var Player */
|
||||
private $player;
|
||||
/** @var NetworkSession */
|
||||
private $session;
|
||||
|
||||
public function __construct(Server $server, Player $player, NetworkSession $session){
|
||||
$this->player = $player;
|
||||
$this->server = $server;
|
||||
$this->session = $session;
|
||||
}
|
||||
|
||||
public function setUp() : void{
|
||||
$spawnPosition = $this->player->getSpawn();
|
||||
|
||||
$pk = new StartGamePacket();
|
||||
$pk->entityUniqueId = $this->player->getId();
|
||||
$pk->entityRuntimeId = $this->player->getId();
|
||||
$pk->playerGamemode = Player::getClientFriendlyGamemode($this->player->getGamemode());
|
||||
$pk->playerPosition = $this->player->getOffsetPosition($this->player);
|
||||
$pk->pitch = $this->player->pitch;
|
||||
$pk->yaw = $this->player->yaw;
|
||||
$pk->seed = -1;
|
||||
$pk->dimension = DimensionIds::OVERWORLD; //TODO: implement this properly
|
||||
$pk->worldGamemode = Player::getClientFriendlyGamemode($this->server->getGamemode());
|
||||
$pk->difficulty = $this->player->getLevel()->getDifficulty();
|
||||
$pk->spawnX = $spawnPosition->getFloorX();
|
||||
$pk->spawnY = $spawnPosition->getFloorY();
|
||||
$pk->spawnZ = $spawnPosition->getFloorZ();
|
||||
$pk->hasAchievementsDisabled = true;
|
||||
$pk->time = $this->player->getLevel()->getTime();
|
||||
$pk->eduMode = false;
|
||||
$pk->rainLevel = 0; //TODO: implement these properly
|
||||
$pk->lightningLevel = 0;
|
||||
$pk->commandsEnabled = true;
|
||||
$pk->levelId = "";
|
||||
$pk->worldName = $this->server->getMotd();
|
||||
$this->session->sendDataPacket($pk);
|
||||
|
||||
$this->player->getLevel()->sendTime($this->player);
|
||||
|
||||
$this->player->sendAttributes(true);
|
||||
$this->player->sendCommandData();
|
||||
$this->player->sendSettings();
|
||||
$this->player->sendPotionEffects($this->player);
|
||||
$this->player->sendData($this->player);
|
||||
|
||||
$this->player->sendAllInventories();
|
||||
$this->player->getInventory()->sendCreativeContents();
|
||||
$this->player->getInventory()->sendHeldItem($this->player);
|
||||
$this->session->getInterface()->putPacket($this->player, $this->server->getCraftingManager()->getCraftingDataPacket());
|
||||
|
||||
$this->server->sendFullPlayerListData($this->player);
|
||||
}
|
||||
|
||||
public function handleRequestChunkRadius(RequestChunkRadiusPacket $packet) : bool{
|
||||
$this->player->setViewDistance($packet->radius);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
<?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\network\mcpe\handler;
|
||||
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\network\mcpe\protocol\ResourcePackChunkDataPacket;
|
||||
use pocketmine\network\mcpe\protocol\ResourcePackChunkRequestPacket;
|
||||
use pocketmine\network\mcpe\protocol\ResourcePackClientResponsePacket;
|
||||
use pocketmine\network\mcpe\protocol\ResourcePackDataInfoPacket;
|
||||
use pocketmine\network\mcpe\protocol\ResourcePacksInfoPacket;
|
||||
use pocketmine\network\mcpe\protocol\ResourcePackStackPacket;
|
||||
use pocketmine\Player;
|
||||
use pocketmine\resourcepacks\ResourcePack;
|
||||
use pocketmine\Server;
|
||||
|
||||
/**
|
||||
* Handler used for the resource packs sequence phase of the session. This handler takes care of downloading resource
|
||||
* packs to the client.
|
||||
*/
|
||||
class ResourcePacksSessionHandler extends SessionHandler{
|
||||
|
||||
/** @var Server */
|
||||
private $server;
|
||||
/** @var Player */
|
||||
private $player;
|
||||
/** @var NetworkSession */
|
||||
private $session;
|
||||
|
||||
public function __construct(Server $server, Player $player, NetworkSession $session){
|
||||
$this->server = $server;
|
||||
$this->player = $player;
|
||||
$this->session = $session;
|
||||
}
|
||||
|
||||
public function setUp() : void{
|
||||
$pk = new ResourcePacksInfoPacket();
|
||||
$manager = $this->server->getResourcePackManager();
|
||||
$pk->resourcePackEntries = $manager->getResourceStack();
|
||||
$pk->mustAccept = $manager->resourcePacksRequired();
|
||||
$this->session->sendDataPacket($pk);
|
||||
}
|
||||
|
||||
public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{
|
||||
switch($packet->status){
|
||||
case ResourcePackClientResponsePacket::STATUS_REFUSED:
|
||||
//TODO: add lang strings for this
|
||||
$this->player->close("", "You must accept resource packs to join this server.", true);
|
||||
break;
|
||||
case ResourcePackClientResponsePacket::STATUS_SEND_PACKS:
|
||||
$manager = $this->server->getResourcePackManager();
|
||||
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->player->close("", "disconnectionScreen.resourcePack", true);
|
||||
$this->server->getLogger()->debug("Got a resource pack request for unknown pack with UUID " . $uuid . ", available packs: " . implode(", ", $manager->getPackIdList()));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$pk = new ResourcePackDataInfoPacket();
|
||||
$pk->packId = $pack->getPackId();
|
||||
$pk->maxChunkSize = 1048576; //1MB
|
||||
$pk->chunkCount = (int) ceil($pack->getPackSize() / $pk->maxChunkSize);
|
||||
$pk->compressedPackSize = $pack->getPackSize();
|
||||
$pk->sha256 = $pack->getSha256();
|
||||
$this->session->sendDataPacket($pk);
|
||||
}
|
||||
|
||||
break;
|
||||
case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS:
|
||||
$pk = new ResourcePackStackPacket();
|
||||
$manager = $this->server->getResourcePackManager();
|
||||
$pk->resourcePackStack = $manager->getResourceStack();
|
||||
$pk->mustAccept = $manager->resourcePacksRequired();
|
||||
$this->session->sendDataPacket($pk);
|
||||
break;
|
||||
case ResourcePackClientResponsePacket::STATUS_COMPLETED:
|
||||
$this->session->onResourcePacksDone();
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{
|
||||
$manager = $this->server->getResourcePackManager();
|
||||
$pack = $manager->getPackById($packet->packId);
|
||||
if(!($pack instanceof ResourcePack)){
|
||||
$this->player->close("", "disconnectionScreen.resourcePack", true);
|
||||
$this->server->getLogger()->debug("Got a resource pack chunk request for unknown pack with UUID " . $packet->packId . ", available packs: " . implode(", ", $manager->getPackIdList()));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$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->session->sendDataPacket($pk);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -42,7 +42,6 @@ use pocketmine\network\mcpe\protocol\InventoryTransactionPacket;
|
||||
use pocketmine\network\mcpe\protocol\ItemFrameDropItemPacket;
|
||||
use pocketmine\network\mcpe\protocol\LabTablePacket;
|
||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\LoginPacket;
|
||||
use pocketmine\network\mcpe\protocol\MapInfoRequestPacket;
|
||||
use pocketmine\network\mcpe\protocol\MobArmorEquipmentPacket;
|
||||
use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
|
||||
@ -53,8 +52,6 @@ use pocketmine\network\mcpe\protocol\PlayerHotbarPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayerInputPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayerSkinPacket;
|
||||
use pocketmine\network\mcpe\protocol\RequestChunkRadiusPacket;
|
||||
use pocketmine\network\mcpe\protocol\ResourcePackChunkRequestPacket;
|
||||
use pocketmine\network\mcpe\protocol\ResourcePackClientResponsePacket;
|
||||
use pocketmine\network\mcpe\protocol\ServerSettingsRequestPacket;
|
||||
use pocketmine\network\mcpe\protocol\SetLocalPlayerAsInitializedPacket;
|
||||
use pocketmine\network\mcpe\protocol\SetPlayerGameTypePacket;
|
||||
@ -77,18 +74,10 @@ class SimpleSessionHandler extends SessionHandler{
|
||||
$this->player = $player;
|
||||
}
|
||||
|
||||
public function handleLogin(LoginPacket $packet) : bool{
|
||||
return $this->player->handleLogin($packet);
|
||||
}
|
||||
|
||||
public function handleClientToServerHandshake(ClientToServerHandshakePacket $packet) : bool{
|
||||
return false; //TODO
|
||||
}
|
||||
|
||||
public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{
|
||||
return $this->player->handleResourcePackClientResponse($packet);
|
||||
}
|
||||
|
||||
public function handleText(TextPacket $packet) : bool{
|
||||
if($packet->type === TextPacket::TYPE_CHAT){
|
||||
return $this->player->chat($packet->message);
|
||||
@ -207,10 +196,6 @@ class SimpleSessionHandler extends SessionHandler{
|
||||
return false; //TODO
|
||||
}
|
||||
|
||||
public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{
|
||||
return $this->player->handleResourcePackChunkRequest($packet);
|
||||
}
|
||||
|
||||
public function handlePlayerSkin(PlayerSkinPacket $packet) : bool{
|
||||
return $this->player->changeSkin($packet->skin, $packet->newSkinName, $packet->oldSkinName);
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ namespace pocketmine\network\mcpe\protocol;
|
||||
#include <rules/DataPacket.h>
|
||||
|
||||
|
||||
use pocketmine\entity\Skin;
|
||||
use pocketmine\network\mcpe\handler\SessionHandler;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use pocketmine\utils\MainLogger;
|
||||
@ -52,6 +53,8 @@ class LoginPacket extends DataPacket{
|
||||
public $serverAddress;
|
||||
/** @var string */
|
||||
public $locale;
|
||||
/** @var Skin|null */
|
||||
public $skin;
|
||||
|
||||
/** @var array (the "chain" index contains one or more JWTs) */
|
||||
public $chainData = [];
|
||||
@ -136,7 +139,15 @@ class LoginPacket extends DataPacket{
|
||||
$this->clientId = $this->clientData["ClientRandomId"] ?? null;
|
||||
$this->serverAddress = $this->clientData["ServerAddress"] ?? null;
|
||||
|
||||
$this->locale = $this->clientData["LanguageCode"] ?? null;
|
||||
$this->locale = $this->clientData["LanguageCode"] ?? "en_US";
|
||||
|
||||
$this->skin = new Skin(
|
||||
$this->clientData["SkinId"] ?? "",
|
||||
base64_decode($this->clientData["SkinData"] ?? ""),
|
||||
base64_decode($this->clientData["CapeData"] ?? ""),
|
||||
$this->clientData["SkinGeometryName"] ?? "",
|
||||
base64_decode($this->clientData["SkinGeometry"] ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
protected function encodePayload() : void{
|
||||
|
Reference in New Issue
Block a user