mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-09-06 09:56:06 +00:00
this was an old hack to prevent debug spam being emitted when we halted decoding of logins over breaking protocol changes. Since then, we've gone back to trying to decode the packet regardless, so this property is useless.
827 lines
26 KiB
PHP
827 lines
26 KiB
PHP
<?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;
|
|
|
|
use pocketmine\entity\effect\EffectInstance;
|
|
use pocketmine\entity\Living;
|
|
use pocketmine\event\player\PlayerCreationEvent;
|
|
use pocketmine\event\server\DataPacketReceiveEvent;
|
|
use pocketmine\event\server\DataPacketSendEvent;
|
|
use pocketmine\form\Form;
|
|
use pocketmine\GameMode;
|
|
use pocketmine\inventory\Inventory;
|
|
use pocketmine\math\Vector3;
|
|
use pocketmine\network\BadPacketException;
|
|
use pocketmine\network\mcpe\handler\DeathSessionHandler;
|
|
use pocketmine\network\mcpe\handler\HandshakeSessionHandler;
|
|
use pocketmine\network\mcpe\handler\InGameSessionHandler;
|
|
use pocketmine\network\mcpe\handler\LoginSessionHandler;
|
|
use pocketmine\network\mcpe\handler\NullSessionHandler;
|
|
use pocketmine\network\mcpe\handler\PreSpawnSessionHandler;
|
|
use pocketmine\network\mcpe\handler\ResourcePacksSessionHandler;
|
|
use pocketmine\network\mcpe\handler\SessionHandler;
|
|
use pocketmine\network\mcpe\protocol\AdventureSettingsPacket;
|
|
use pocketmine\network\mcpe\protocol\AvailableCommandsPacket;
|
|
use pocketmine\network\mcpe\protocol\ChunkRadiusUpdatedPacket;
|
|
use pocketmine\network\mcpe\protocol\ClientboundPacket;
|
|
use pocketmine\network\mcpe\protocol\ContainerSetDataPacket;
|
|
use pocketmine\network\mcpe\protocol\DisconnectPacket;
|
|
use pocketmine\network\mcpe\protocol\InventoryContentPacket;
|
|
use pocketmine\network\mcpe\protocol\InventorySlotPacket;
|
|
use pocketmine\network\mcpe\protocol\MobArmorEquipmentPacket;
|
|
use pocketmine\network\mcpe\protocol\MobEffectPacket;
|
|
use pocketmine\network\mcpe\protocol\ModalFormRequestPacket;
|
|
use pocketmine\network\mcpe\protocol\MovePlayerPacket;
|
|
use pocketmine\network\mcpe\protocol\NetworkChunkPublisherUpdatePacket;
|
|
use pocketmine\network\mcpe\protocol\Packet;
|
|
use pocketmine\network\mcpe\protocol\PlayerListPacket;
|
|
use pocketmine\network\mcpe\protocol\PlayStatusPacket;
|
|
use pocketmine\network\mcpe\protocol\ServerboundPacket;
|
|
use pocketmine\network\mcpe\protocol\ServerToClientHandshakePacket;
|
|
use pocketmine\network\mcpe\protocol\SetPlayerGameTypePacket;
|
|
use pocketmine\network\mcpe\protocol\SetSpawnPositionPacket;
|
|
use pocketmine\network\mcpe\protocol\TextPacket;
|
|
use pocketmine\network\mcpe\protocol\TransferPacket;
|
|
use pocketmine\network\mcpe\protocol\types\CommandData;
|
|
use pocketmine\network\mcpe\protocol\types\CommandEnum;
|
|
use pocketmine\network\mcpe\protocol\types\CommandParameter;
|
|
use pocketmine\network\mcpe\protocol\types\ContainerIds;
|
|
use pocketmine\network\mcpe\protocol\types\PlayerListEntry;
|
|
use pocketmine\network\mcpe\protocol\types\PlayerPermissions;
|
|
use pocketmine\network\mcpe\protocol\UpdateAttributesPacket;
|
|
use pocketmine\network\NetworkInterface;
|
|
use pocketmine\network\NetworkSessionManager;
|
|
use pocketmine\Player;
|
|
use pocketmine\PlayerInfo;
|
|
use pocketmine\Server;
|
|
use pocketmine\timings\Timings;
|
|
use pocketmine\utils\BinaryDataException;
|
|
use pocketmine\utils\Utils;
|
|
use pocketmine\world\Position;
|
|
use function array_map;
|
|
use function bin2hex;
|
|
use function count;
|
|
use function get_class;
|
|
use function in_array;
|
|
use function json_encode;
|
|
use function json_last_error_msg;
|
|
use function strlen;
|
|
use function strtolower;
|
|
use function substr;
|
|
use function time;
|
|
use function ucfirst;
|
|
|
|
class NetworkSession{
|
|
/** @var \PrefixedLogger */
|
|
private $logger;
|
|
/** @var Server */
|
|
private $server;
|
|
/** @var Player|null */
|
|
private $player = null;
|
|
/** @var NetworkSessionManager */
|
|
private $manager;
|
|
/** @var NetworkInterface */
|
|
private $interface;
|
|
/** @var string */
|
|
private $ip;
|
|
/** @var int */
|
|
private $port;
|
|
/** @var PlayerInfo */
|
|
private $info;
|
|
/** @var int */
|
|
private $ping;
|
|
|
|
/** @var SessionHandler */
|
|
private $handler;
|
|
|
|
/** @var bool */
|
|
private $connected = true;
|
|
/** @var bool */
|
|
private $disconnectGuard = false;
|
|
/** @var bool */
|
|
private $loggedIn = false;
|
|
/** @var bool */
|
|
private $authenticated = false;
|
|
/** @var int */
|
|
private $connectTime;
|
|
|
|
/** @var NetworkCipher */
|
|
private $cipher;
|
|
|
|
/** @var PacketBatch|null */
|
|
private $sendBuffer;
|
|
|
|
/** @var \SplQueue|CompressBatchPromise[] */
|
|
private $compressedQueue;
|
|
|
|
public function __construct(Server $server, NetworkSessionManager $manager, NetworkInterface $interface, string $ip, int $port){
|
|
$this->server = $server;
|
|
$this->manager = $manager;
|
|
$this->interface = $interface;
|
|
$this->ip = $ip;
|
|
$this->port = $port;
|
|
|
|
$this->logger = new \PrefixedLogger($this->server->getLogger(), $this->getLogPrefix());
|
|
|
|
$this->compressedQueue = new \SplQueue();
|
|
|
|
$this->connectTime = time();
|
|
|
|
$this->setHandler(new LoginSessionHandler($this->server, $this));
|
|
|
|
$this->manager->add($this);
|
|
}
|
|
|
|
private function getLogPrefix() : string{
|
|
return "NetworkSession: " . $this->getDisplayName();
|
|
}
|
|
|
|
public function getLogger() : \Logger{
|
|
return $this->logger;
|
|
}
|
|
|
|
protected function createPlayer() : void{
|
|
$ev = new PlayerCreationEvent($this);
|
|
$ev->call();
|
|
$class = $ev->getPlayerClass();
|
|
|
|
/**
|
|
* @var Player $player
|
|
* @see Player::__construct()
|
|
*/
|
|
$this->player = new $class($this->server, $this, $this->info, $this->authenticated);
|
|
}
|
|
|
|
public function getPlayer() : ?Player{
|
|
return $this->player;
|
|
}
|
|
|
|
public function getPlayerInfo() : ?PlayerInfo{
|
|
return $this->info;
|
|
}
|
|
|
|
/**
|
|
* TODO: this shouldn't be accessible after the initial login phase
|
|
*
|
|
* @param PlayerInfo $info
|
|
* @throws \InvalidStateException
|
|
*/
|
|
public function setPlayerInfo(PlayerInfo $info) : void{
|
|
if($this->info !== null){
|
|
throw new \InvalidStateException("Player info has already been set");
|
|
}
|
|
$this->info = $info;
|
|
$this->logger->setPrefix($this->getLogPrefix());
|
|
}
|
|
|
|
public function isConnected() : bool{
|
|
return $this->connected;
|
|
}
|
|
|
|
public function getInterface() : NetworkInterface{
|
|
return $this->interface;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getIp() : string{
|
|
return $this->ip;
|
|
}
|
|
|
|
/**
|
|
* @return int
|
|
*/
|
|
public function getPort() : int{
|
|
return $this->port;
|
|
}
|
|
|
|
public function getDisplayName() : string{
|
|
return $this->info !== null ? $this->info->getUsername() : $this->ip . " " . $this->port;
|
|
}
|
|
|
|
/**
|
|
* Returns the last recorded ping measurement for this session, in milliseconds.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getPing() : int{
|
|
return $this->ping;
|
|
}
|
|
|
|
/**
|
|
* @internal Called by the network interface to update last recorded ping measurements.
|
|
*
|
|
* @param int $ping
|
|
*/
|
|
public function updatePing(int $ping) : void{
|
|
$this->ping = $ping;
|
|
}
|
|
|
|
public function getHandler() : SessionHandler{
|
|
return $this->handler;
|
|
}
|
|
|
|
public function setHandler(SessionHandler $handler) : void{
|
|
if($this->connected){ //TODO: this is fine since we can't handle anything from a disconnected session, but it might produce surprises in some cases
|
|
$this->handler = $handler;
|
|
$this->handler->setUp();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $payload
|
|
*
|
|
* @throws BadPacketException
|
|
*/
|
|
public function handleEncoded(string $payload) : void{
|
|
if(!$this->connected){
|
|
return;
|
|
}
|
|
|
|
if($this->cipher !== null){
|
|
Timings::$playerNetworkReceiveDecryptTimer->startTiming();
|
|
try{
|
|
$payload = $this->cipher->decrypt($payload);
|
|
}catch(\UnexpectedValueException $e){
|
|
$this->logger->debug("Encrypted packet: " . bin2hex($payload));
|
|
throw new BadPacketException("Packet decryption error: " . $e->getMessage(), 0, $e);
|
|
}finally{
|
|
Timings::$playerNetworkReceiveDecryptTimer->stopTiming();
|
|
}
|
|
}
|
|
|
|
Timings::$playerNetworkReceiveDecompressTimer->startTiming();
|
|
try{
|
|
$stream = new PacketBatch(NetworkCompression::decompress($payload));
|
|
}catch(\ErrorException $e){
|
|
$this->logger->debug("Failed to decompress packet: " . bin2hex($payload));
|
|
//TODO: this isn't incompatible game version if we already established protocol version
|
|
throw new BadPacketException("Compressed packet batch decode error (incompatible game version?)", 0, $e);
|
|
}finally{
|
|
Timings::$playerNetworkReceiveDecompressTimer->stopTiming();
|
|
}
|
|
|
|
$count = 0;
|
|
while(!$stream->feof() and $this->connected){
|
|
if($count++ >= 500){
|
|
throw new BadPacketException("Too many packets in a single batch");
|
|
}
|
|
try{
|
|
$pk = $stream->getPacket();
|
|
}catch(BinaryDataException $e){
|
|
$this->logger->debug("Packet batch: " . bin2hex($stream->getBuffer()));
|
|
throw new BadPacketException("Packet batch decode error: " . $e->getMessage(), 0, $e);
|
|
}
|
|
|
|
try{
|
|
$this->handleDataPacket($pk);
|
|
}catch(BadPacketException $e){
|
|
$this->logger->debug($pk->getName() . ": " . bin2hex($pk->getBuffer()));
|
|
throw new BadPacketException("Error processing " . $pk->getName() . ": " . $e->getMessage(), 0, $e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Packet $packet
|
|
*
|
|
* @throws BadPacketException
|
|
*/
|
|
public function handleDataPacket(Packet $packet) : void{
|
|
if(!($packet instanceof ServerboundPacket)){
|
|
throw new BadPacketException("Unexpected non-serverbound packet");
|
|
}
|
|
|
|
$timings = Timings::getReceiveDataPacketTimings($packet);
|
|
$timings->startTiming();
|
|
|
|
try{
|
|
$packet->decode();
|
|
if(!$packet->feof()){
|
|
$remains = substr($packet->getBuffer(), $packet->getOffset());
|
|
$this->logger->debug("Still " . strlen($remains) . " bytes unread in " . $packet->getName() . ": " . bin2hex($remains));
|
|
}
|
|
|
|
$ev = new DataPacketReceiveEvent($this, $packet);
|
|
$ev->call();
|
|
if(!$ev->isCancelled() and !$packet->handle($this->handler)){
|
|
$this->logger->debug("Unhandled " . $packet->getName() . ": " . bin2hex($packet->getBuffer()));
|
|
}
|
|
}finally{
|
|
$timings->stopTiming();
|
|
}
|
|
}
|
|
|
|
public function sendDataPacket(ClientboundPacket $packet, bool $immediate = false) : bool{
|
|
//Basic safety restriction. TODO: improve this
|
|
if(!$this->loggedIn and !$packet->canBeSentBeforeLogin()){
|
|
throw new \InvalidArgumentException("Attempted to send " . get_class($packet) . " to " . $this->getDisplayName() . " too early");
|
|
}
|
|
|
|
$timings = Timings::getSendDataPacketTimings($packet);
|
|
$timings->startTiming();
|
|
try{
|
|
$ev = new DataPacketSendEvent($this, $packet);
|
|
$ev->call();
|
|
if($ev->isCancelled()){
|
|
return false;
|
|
}
|
|
|
|
$this->addToSendBuffer($packet);
|
|
if($immediate){
|
|
$this->flushSendBuffer(true);
|
|
}
|
|
|
|
return true;
|
|
}finally{
|
|
$timings->stopTiming();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
* @param ClientboundPacket $packet
|
|
*/
|
|
public function addToSendBuffer(ClientboundPacket $packet) : void{
|
|
$timings = Timings::getSendDataPacketTimings($packet);
|
|
$timings->startTiming();
|
|
try{
|
|
if($this->sendBuffer === null){
|
|
$this->sendBuffer = new PacketBatch();
|
|
}
|
|
$this->sendBuffer->putPacket($packet);
|
|
$this->manager->scheduleUpdate($this); //schedule flush at end of tick
|
|
}finally{
|
|
$timings->stopTiming();
|
|
}
|
|
}
|
|
|
|
private function flushSendBuffer(bool $immediate = false) : void{
|
|
if($this->sendBuffer !== null){
|
|
$promise = $this->server->prepareBatch($this->sendBuffer, $immediate);
|
|
$this->sendBuffer = null;
|
|
$this->queueCompressed($promise, $immediate);
|
|
}
|
|
}
|
|
|
|
public function queueCompressed(CompressBatchPromise $payload, bool $immediate = false) : void{
|
|
$this->flushSendBuffer($immediate); //Maintain ordering if possible
|
|
if($immediate){
|
|
//Skips all queues
|
|
$this->sendEncoded($payload->getResult(), true);
|
|
}else{
|
|
$this->compressedQueue->enqueue($payload);
|
|
$payload->onResolve(function(CompressBatchPromise $payload) : void{
|
|
if($this->connected and $this->compressedQueue->bottom() === $payload){
|
|
$this->compressedQueue->dequeue(); //result unused
|
|
$this->sendEncoded($payload->getResult());
|
|
|
|
while(!$this->compressedQueue->isEmpty()){
|
|
/** @var CompressBatchPromise $current */
|
|
$current = $this->compressedQueue->bottom();
|
|
if($current->hasResult()){
|
|
$this->compressedQueue->dequeue();
|
|
|
|
$this->sendEncoded($current->getResult());
|
|
}else{
|
|
//can't send any more queued until this one is ready
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private function sendEncoded(string $payload, bool $immediate = false) : void{
|
|
if($this->cipher !== null){
|
|
Timings::$playerNetworkSendEncryptTimer->startTiming();
|
|
$payload = $this->cipher->encrypt($payload);
|
|
Timings::$playerNetworkSendEncryptTimer->stopTiming();
|
|
}
|
|
$this->interface->putPacket($this, $payload, $immediate);
|
|
}
|
|
|
|
private function tryDisconnect(\Closure $func) : void{
|
|
if($this->connected and !$this->disconnectGuard){
|
|
$this->disconnectGuard = true;
|
|
$func();
|
|
$this->disconnectGuard = false;
|
|
$this->setHandler(NullSessionHandler::getInstance());
|
|
$this->connected = false;
|
|
$this->manager->remove($this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disconnects the session, destroying the associated player (if it exists).
|
|
*
|
|
* @param string $reason
|
|
* @param bool $notify
|
|
*/
|
|
public function disconnect(string $reason, bool $notify = true) : void{
|
|
$this->tryDisconnect(function() use($reason, $notify){
|
|
if($this->player !== null){
|
|
$this->player->disconnect($reason, null, $notify);
|
|
}
|
|
$this->doServerDisconnect($reason, $notify);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Instructs the remote client to connect to a different server.
|
|
*
|
|
* @param string $ip
|
|
* @param int $port
|
|
* @param string $reason
|
|
*
|
|
* @throws \UnsupportedOperationException
|
|
*/
|
|
public function transfer(string $ip, int $port, string $reason = "transfer") : void{
|
|
$this->tryDisconnect(function() use($ip, $port, $reason){
|
|
$this->sendDataPacket(TransferPacket::create($ip, $port), true);
|
|
$this->disconnect($reason, false);
|
|
if($this->player !== null){
|
|
$this->player->disconnect($reason, null, false);
|
|
}
|
|
$this->doServerDisconnect($reason, false);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Called by the Player when it is closed (for example due to getting kicked).
|
|
*
|
|
* @param string $reason
|
|
* @param bool $notify
|
|
*/
|
|
public function onPlayerDestroyed(string $reason, bool $notify = true) : void{
|
|
$this->tryDisconnect(function() use($reason, $notify){
|
|
$this->doServerDisconnect($reason, $notify);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Internal helper function used to handle server disconnections.
|
|
*
|
|
* @param string $reason
|
|
* @param bool $notify
|
|
*/
|
|
private function doServerDisconnect(string $reason, bool $notify = true) : void{
|
|
if($notify){
|
|
$this->sendDataPacket($reason === "" ? DisconnectPacket::silent() : DisconnectPacket::message($reason), true);
|
|
}
|
|
|
|
$this->interface->close($this, $notify ? $reason : "");
|
|
}
|
|
|
|
/**
|
|
* Called by the network interface to close the session when the client disconnects without server input, for
|
|
* example in a timeout condition or voluntary client disconnect.
|
|
*
|
|
* @param string $reason
|
|
*/
|
|
public function onClientDisconnect(string $reason) : void{
|
|
$this->tryDisconnect(function() use($reason){
|
|
if($this->player !== null){
|
|
$this->player->disconnect($reason, null, false);
|
|
}
|
|
});
|
|
}
|
|
|
|
public function setAuthenticationStatus(bool $authenticated, bool $authRequired, ?string $error) : bool{
|
|
if($authenticated and $this->info->getXuid() === ""){
|
|
$error = "Expected XUID but none found";
|
|
}
|
|
|
|
if($error !== null){
|
|
$this->disconnect($this->server->getLanguage()->translateString("pocketmine.disconnect.invalidSession", [$error]));
|
|
|
|
return false;
|
|
}
|
|
|
|
$this->authenticated = $authenticated;
|
|
|
|
if(!$this->authenticated){
|
|
if($authRequired){
|
|
$this->disconnect("disconnectionScreen.notAuthenticated");
|
|
return false;
|
|
}
|
|
|
|
if($this->info->getXuid() !== ""){
|
|
$this->logger->warning("Found XUID, but login keychain is not signed by Mojang");
|
|
}
|
|
}
|
|
$this->logger->debug("Xbox Live authenticated: " . ($this->authenticated ? "YES" : "NO"));
|
|
|
|
return $this->manager->kickDuplicates($this);
|
|
}
|
|
|
|
public function enableEncryption(string $encryptionKey, string $handshakeJwt) : void{
|
|
$this->sendDataPacket(ServerToClientHandshakePacket::create($handshakeJwt), true); //make sure this gets sent before encryption is enabled
|
|
|
|
$this->cipher = new NetworkCipher($encryptionKey);
|
|
|
|
$this->setHandler(new HandshakeSessionHandler($this));
|
|
$this->logger->debug("Enabled encryption");
|
|
}
|
|
|
|
public function onLoginSuccess() : void{
|
|
$this->loggedIn = true;
|
|
|
|
$this->sendDataPacket(PlayStatusPacket::create(PlayStatusPacket::LOGIN_SUCCESS));
|
|
|
|
$this->setHandler(new ResourcePacksSessionHandler($this, $this->server->getResourcePackManager()));
|
|
}
|
|
|
|
public function onResourcePacksDone() : void{
|
|
$this->createPlayer();
|
|
|
|
$this->setHandler(new PreSpawnSessionHandler($this->server, $this->player, $this));
|
|
}
|
|
|
|
public function onTerrainReady() : void{
|
|
$this->sendDataPacket(PlayStatusPacket::create(PlayStatusPacket::PLAYER_SPAWN));
|
|
}
|
|
|
|
public function onSpawn() : void{
|
|
$this->setHandler(new InGameSessionHandler($this->player, $this));
|
|
}
|
|
|
|
public function onDeath() : void{
|
|
$this->setHandler(new DeathSessionHandler($this->player, $this));
|
|
}
|
|
|
|
public function onRespawn() : void{
|
|
$this->setHandler(new InGameSessionHandler($this->player, $this));
|
|
}
|
|
|
|
public function syncMovement(Vector3 $pos, ?float $yaw = null, ?float $pitch = null, int $mode = MovePlayerPacket::MODE_NORMAL) : void{
|
|
$yaw = $yaw ?? $this->player->getYaw();
|
|
$pitch = $pitch ?? $this->player->getPitch();
|
|
|
|
$pk = new MovePlayerPacket();
|
|
$pk->entityRuntimeId = $this->player->getId();
|
|
$pk->position = $this->player->getOffsetPosition($pos);
|
|
$pk->pitch = $pitch;
|
|
$pk->headYaw = $yaw;
|
|
$pk->yaw = $yaw;
|
|
$pk->mode = $mode;
|
|
|
|
$this->sendDataPacket($pk);
|
|
}
|
|
|
|
public function syncViewAreaRadius(int $distance) : void{
|
|
$this->sendDataPacket(ChunkRadiusUpdatedPacket::create($distance));
|
|
}
|
|
|
|
public function syncViewAreaCenterPoint(Vector3 $newPos, int $viewDistance) : void{
|
|
$this->sendDataPacket(NetworkChunkPublisherUpdatePacket::create($newPos->getFloorX(), $newPos->getFloorY(), $newPos->getFloorZ(), $viewDistance * 16)); //blocks, not chunks >.>
|
|
}
|
|
|
|
public function syncPlayerSpawnPoint(Position $newSpawn) : void{
|
|
$this->sendDataPacket(SetSpawnPositionPacket::playerSpawn($newSpawn->getFloorX(), $newSpawn->getFloorY(), $newSpawn->getFloorZ(), false)); //TODO: spawn forced
|
|
}
|
|
|
|
public function syncGameMode(GameMode $mode) : void{
|
|
$this->sendDataPacket(SetPlayerGameTypePacket::create(self::getClientFriendlyGamemode($mode)));
|
|
}
|
|
|
|
/**
|
|
* TODO: make this less specialized
|
|
*
|
|
* @param Player $for
|
|
*/
|
|
public function syncAdventureSettings(Player $for) : void{
|
|
$pk = new AdventureSettingsPacket();
|
|
|
|
$pk->setFlag(AdventureSettingsPacket::WORLD_IMMUTABLE, $for->isSpectator());
|
|
$pk->setFlag(AdventureSettingsPacket::NO_PVP, $for->isSpectator());
|
|
$pk->setFlag(AdventureSettingsPacket::AUTO_JUMP, $for->hasAutoJump());
|
|
$pk->setFlag(AdventureSettingsPacket::ALLOW_FLIGHT, $for->getAllowFlight());
|
|
$pk->setFlag(AdventureSettingsPacket::NO_CLIP, $for->isSpectator());
|
|
$pk->setFlag(AdventureSettingsPacket::FLYING, $for->isFlying());
|
|
|
|
//TODO: permission flags
|
|
|
|
$pk->commandPermission = ($for->isOp() ? AdventureSettingsPacket::PERMISSION_OPERATOR : AdventureSettingsPacket::PERMISSION_NORMAL);
|
|
$pk->playerPermission = ($for->isOp() ? PlayerPermissions::OPERATOR : PlayerPermissions::MEMBER);
|
|
$pk->entityUniqueId = $for->getId();
|
|
|
|
$this->sendDataPacket($pk);
|
|
}
|
|
|
|
public function syncAttributes(Living $entity, bool $sendAll = false){
|
|
$entries = $sendAll ? $entity->getAttributeMap()->getAll() : $entity->getAttributeMap()->needSend();
|
|
if(count($entries) > 0){
|
|
$this->sendDataPacket(UpdateAttributesPacket::create($entity->getId(), $entries));
|
|
foreach($entries as $entry){
|
|
$entry->markSynchronized();
|
|
}
|
|
}
|
|
}
|
|
|
|
public function onEntityEffectAdded(Living $entity, EffectInstance $effect, bool $replacesOldEffect) : void{
|
|
$this->sendDataPacket(MobEffectPacket::add($entity->getId(), $replacesOldEffect, $effect->getId(), $effect->getAmplifier(), $effect->isVisible(), $effect->getDuration()));
|
|
}
|
|
|
|
public function onEntityEffectRemoved(Living $entity, EffectInstance $effect) : void{
|
|
$this->sendDataPacket(MobEffectPacket::remove($entity->getId(), $effect->getId()));
|
|
}
|
|
|
|
public function syncAvailableCommands() : void{
|
|
$pk = new AvailableCommandsPacket();
|
|
foreach($this->server->getCommandMap()->getCommands() as $name => $command){
|
|
if(isset($pk->commandData[$command->getName()]) or $command->getName() === "help" or !$command->testPermissionSilent($this->player)){
|
|
continue;
|
|
}
|
|
|
|
$data = new CommandData();
|
|
//TODO: commands containing uppercase letters in the name crash 1.9.0 client
|
|
$data->commandName = strtolower($command->getName());
|
|
$data->commandDescription = $this->server->getLanguage()->translateString($command->getDescription());
|
|
$data->flags = 0;
|
|
$data->permission = 0;
|
|
|
|
$parameter = new CommandParameter();
|
|
$parameter->paramName = "args";
|
|
$parameter->paramType = AvailableCommandsPacket::ARG_FLAG_VALID | AvailableCommandsPacket::ARG_TYPE_RAWTEXT;
|
|
$parameter->isOptional = true;
|
|
$data->overloads[0][0] = $parameter;
|
|
|
|
$aliases = $command->getAliases();
|
|
if(!empty($aliases)){
|
|
if(!in_array($data->commandName, $aliases, true)){
|
|
//work around a client bug which makes the original name not show when aliases are used
|
|
$aliases[] = $data->commandName;
|
|
}
|
|
$data->aliases = new CommandEnum();
|
|
$data->aliases->enumName = ucfirst($command->getName()) . "Aliases";
|
|
$data->aliases->enumValues = $aliases;
|
|
}
|
|
|
|
$pk->commandData[$command->getName()] = $data;
|
|
}
|
|
|
|
$this->sendDataPacket($pk);
|
|
}
|
|
|
|
public function onRawChatMessage(string $message) : void{
|
|
$this->sendDataPacket(TextPacket::raw($message));
|
|
}
|
|
|
|
public function onTranslatedChatMessage(string $key, array $parameters) : void{
|
|
$this->sendDataPacket(TextPacket::translation($key, $parameters));
|
|
}
|
|
|
|
public function onPopup(string $message) : void{
|
|
$this->sendDataPacket(TextPacket::popup($message));
|
|
}
|
|
|
|
public function onTip(string $message) : void{
|
|
$this->sendDataPacket(TextPacket::tip($message));
|
|
}
|
|
|
|
public function onFormSent(int $id, Form $form) : bool{
|
|
$formData = json_encode($form);
|
|
if($formData === false){
|
|
throw new \InvalidArgumentException("Failed to encode form JSON: " . json_last_error_msg());
|
|
}
|
|
return $this->sendDataPacket(ModalFormRequestPacket::create($id, $formData));
|
|
}
|
|
|
|
/**
|
|
* Instructs the networksession to start using the chunk at the given coordinates. This may occur asynchronously.
|
|
* @param int $chunkX
|
|
* @param int $chunkZ
|
|
* @param \Closure $onCompletion To be called when chunk sending has completed.
|
|
*/
|
|
public function startUsingChunk(int $chunkX, int $chunkZ, \Closure $onCompletion) : void{
|
|
Utils::validateCallableSignature(function(int $chunkX, int $chunkZ){}, $onCompletion);
|
|
|
|
ChunkCache::getInstance($this->player->getWorld())->request($chunkX, $chunkZ)->onResolve(
|
|
|
|
//this callback may be called synchronously or asynchronously, depending on whether the promise is resolved yet
|
|
function(CompressBatchPromise $promise) use($chunkX, $chunkZ, $onCompletion){
|
|
if(!$this->isConnected()){
|
|
return;
|
|
}
|
|
$this->player->world->timings->syncChunkSendTimer->startTiming();
|
|
try{
|
|
$this->queueCompressed($promise);
|
|
$onCompletion($chunkX, $chunkZ);
|
|
}finally{
|
|
$this->player->world->timings->syncChunkSendTimer->stopTiming();
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
public function stopUsingChunk(int $chunkX, int $chunkZ) : void{
|
|
|
|
}
|
|
|
|
public function syncInventorySlot(Inventory $inventory, int $slot) : void{
|
|
$windowId = $this->player->getWindowId($inventory);
|
|
if($windowId !== ContainerIds::NONE){
|
|
$this->sendDataPacket(InventorySlotPacket::create($windowId, $slot, $inventory->getItem($slot)));
|
|
}
|
|
}
|
|
|
|
public function syncInventoryContents(Inventory $inventory) : void{
|
|
$windowId = $this->player->getWindowId($inventory);
|
|
if($windowId !== ContainerIds::NONE){
|
|
$this->sendDataPacket(InventoryContentPacket::create($windowId, $inventory->getContents(true)));
|
|
}
|
|
}
|
|
|
|
public function syncAllInventoryContents() : void{
|
|
foreach($this->player->getAllWindows() as $inventory){
|
|
$this->syncInventoryContents($inventory);
|
|
}
|
|
}
|
|
|
|
public function syncInventoryData(Inventory $inventory, int $propertyId, int $value) : void{
|
|
$windowId = $this->player->getWindowId($inventory);
|
|
if($windowId !== ContainerIds::NONE){
|
|
$this->sendDataPacket(ContainerSetDataPacket::create($windowId, $propertyId, $value));
|
|
}
|
|
}
|
|
|
|
public function onMobArmorChange(Living $mob) : void{
|
|
$inv = $mob->getArmorInventory();
|
|
$this->sendDataPacket(MobArmorEquipmentPacket::create($mob->getId(), $inv->getHelmet(), $inv->getChestplate(), $inv->getLeggings(), $inv->getBoots()));
|
|
}
|
|
|
|
public function syncPlayerList() : void{
|
|
$this->sendDataPacket(PlayerListPacket::add(array_map(function(Player $player){
|
|
return PlayerListEntry::createAdditionEntry($player->getUniqueId(), $player->getId(), $player->getDisplayName(), $player->getSkin(), $player->getXuid());
|
|
}, $this->server->getOnlinePlayers())));
|
|
}
|
|
|
|
public function onPlayerAdded(Player $p) : void{
|
|
$this->sendDataPacket(PlayerListPacket::add([PlayerListEntry::createAdditionEntry($p->getUniqueId(), $p->getId(), $p->getName(), $p->getSkin(), $p->getXuid())]));
|
|
}
|
|
|
|
public function onPlayerRemoved(Player $p) : void{
|
|
if($p !== $this->player){
|
|
$this->sendDataPacket(PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($p->getUniqueId())]));
|
|
}
|
|
}
|
|
|
|
public function tick() : bool{
|
|
if($this->handler instanceof LoginSessionHandler){
|
|
if(time() >= $this->connectTime + 10){
|
|
$this->disconnect("Login timeout");
|
|
return false;
|
|
}
|
|
|
|
return true; //keep ticking until timeout
|
|
}
|
|
|
|
if($this->sendBuffer !== null){
|
|
$this->flushSendBuffer();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns a client-friendly gamemode of the specified real gamemode
|
|
* This function takes care of handling gamemodes known to MCPE (as of 1.1.0.3, that includes Survival, Creative and Adventure)
|
|
*
|
|
* @internal
|
|
* @param GameMode $gamemode
|
|
*
|
|
* @return int
|
|
*/
|
|
public static function getClientFriendlyGamemode(GameMode $gamemode) : int{
|
|
if($gamemode->equals(GameMode::SPECTATOR())){
|
|
return GameMode::CREATIVE()->getMagicNumber();
|
|
}
|
|
|
|
return $gamemode->getMagicNumber();
|
|
}
|
|
}
|