22declare(strict_types=1);
24namespace pocketmine\network\mcpe;
99use pocketmine\player\GameMode;
102use pocketmine\player\UsedChunkStatus;
115use
function array_map;
116use
function array_values;
117use
function base64_encode;
120use
function get_class;
122use
function in_array;
123use
function is_string;
124use
function json_encode;
126use
function random_bytes;
127use
function str_split;
128use
function strcasecmp;
130use
function strtolower;
134use
const JSON_THROW_ON_ERROR;
137 private const INCOMING_PACKET_BATCH_PER_TICK = 2;
138 private const INCOMING_PACKET_BATCH_BUFFER_TICKS = 100;
140 private const INCOMING_GAME_PACKETS_PER_TICK = 2;
141 private const INCOMING_GAME_PACKETS_BUFFER_TICKS = 100;
146 private \PrefixedLogger $logger;
147 private ?
Player $player =
null;
149 private ?
int $ping =
null;
153 private bool $connected =
true;
154 private bool $disconnectGuard =
false;
155 private bool $loggedIn =
false;
156 private bool $authenticated =
false;
157 private int $connectTime;
158 private ?
CompoundTag $cachedOfflinePlayerData =
null;
163 private array $sendBuffer = [];
168 private array $sendBufferAckPromises = [];
171 private \SplQueue $compressedQueue;
172 private bool $forceAsyncCompression =
true;
173 private bool $enableCompression =
false;
175 private int $nextAckReceiptId = 0;
180 private array $ackPromisesByReceiptId = [];
190 public function __construct(
202 $this->logger = new \PrefixedLogger($this->
server->getLogger(), $this->getLogPrefix());
204 $this->compressedQueue = new \SplQueue();
208 $this->connectTime = time();
209 $this->packetBatchLimiter =
new PacketRateLimiter(
"Packet Batches", self::INCOMING_PACKET_BATCH_PER_TICK, self::INCOMING_PACKET_BATCH_BUFFER_TICKS);
210 $this->gamePacketLimiter =
new PacketRateLimiter(
"Game Packets", self::INCOMING_GAME_PACKETS_PER_TICK, self::INCOMING_GAME_PACKETS_BUFFER_TICKS);
214 $this->onSessionStartSuccess(...)
217 $this->manager->add($this);
218 $this->logger->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_open()));
221 private function getLogPrefix() :
string{
222 return "NetworkSession: " . $this->getDisplayName();
225 public function getLogger() :
\Logger{
226 return $this->logger;
229 private function onSessionStartSuccess() :
void{
230 $this->logger->debug(
"Session start handshake completed, awaiting login packet");
231 $this->flushSendBuffer(
true);
232 $this->enableCompression =
true;
238 $this->logger->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_playerName(TextFormat::AQUA . $info->getUsername() . TextFormat::RESET)));
239 $this->logger->setPrefix($this->getLogPrefix());
240 $this->manager->markLoginReceived($this);
242 $this->setAuthenticationStatus(...)
246 protected function createPlayer() :
void{
247 $this->
server->createPlayer($this, $this->info, $this->authenticated, $this->cachedOfflinePlayerData)->onCompletion(
248 $this->onPlayerCreated(...),
251 $this->disconnectWithError(
252 reason:
"Failed to create player",
253 disconnectScreenMessage: KnownTranslationFactory::pocketmine_disconnect_error_internal()
259 private function onPlayerCreated(
Player $player) :
void{
260 if(!$this->isConnected()){
264 $this->player = $player;
265 if(!$this->
server->addOnlinePlayer($player)){
271 $effectManager = $this->player->getEffects();
272 $effectManager->getEffectAddHooks()->add($effectAddHook =
function(
EffectInstance $effect,
bool $replacesOldEffect) :
void{
273 $this->entityEventBroadcaster->onEntityEffectAdded([$this], $this->player, $effect, $replacesOldEffect);
275 $effectManager->getEffectRemoveHooks()->add($effectRemoveHook =
function(
EffectInstance $effect) :
void{
276 $this->entityEventBroadcaster->onEntityEffectRemoved([$this], $this->player, $effect);
278 $this->disposeHooks->add(
static function() use ($effectManager, $effectAddHook, $effectRemoveHook) :
void{
279 $effectManager->getEffectAddHooks()->
remove($effectAddHook);
280 $effectManager->getEffectRemoveHooks()->
remove($effectRemoveHook);
283 $permissionHooks = $this->player->getPermissionRecalculationCallbacks();
284 $permissionHooks->add($permHook =
function() :
void{
285 $this->logger->debug(
"Syncing available commands and abilities/permissions due to permission recalculation");
286 $this->syncAbilities($this->player);
287 $this->syncAvailableCommands();
289 $this->disposeHooks->add(
static function() use ($permissionHooks, $permHook) :
void{
290 $permissionHooks->remove($permHook);
292 $this->beginSpawnSequence();
295 public function getPlayer() : ?
Player{
296 return $this->player;
299 public function getPlayerInfo() : ?
PlayerInfo{
303 public function isConnected() :
bool{
304 return $this->connected && !$this->disconnectGuard;
307 public function getIp() :
string{
311 public function getPort() :
int{
315 public function getDisplayName() :
string{
316 return $this->info !==
null ? $this->info->getUsername() : $this->ip .
" " . $this->port;
329 public function updatePing(
int $ping) : void{
333 public function getHandler() : ?PacketHandler{
334 return $this->handler;
337 public function setHandler(?PacketHandler $handler) : void{
338 if($this->connected){
339 $this->handler = $handler;
340 if($this->handler !==
null){
341 $this->handler->setUp();
350 if(!$this->connected){
354 Timings::$playerNetworkReceive->startTiming();
356 $this->packetBatchLimiter->decrement();
358 if($this->cipher !==
null){
359 Timings::$playerNetworkReceiveDecrypt->startTiming();
361 $payload = $this->cipher->decrypt($payload);
362 }
catch(DecryptionException $e){
363 $this->logger->debug(
"Encrypted packet: " . base64_encode($payload));
364 throw PacketHandlingException::wrap($e,
"Packet decryption error");
366 Timings::$playerNetworkReceiveDecrypt->stopTiming();
370 if(strlen($payload) < 1){
371 throw new PacketHandlingException(
"No bytes in payload");
374 if($this->enableCompression){
375 $compressionType = ord($payload[0]);
376 $compressed = substr($payload, 1);
377 if($compressionType === CompressionAlgorithm::NONE){
378 $decompressed = $compressed;
379 }elseif($compressionType === $this->compressor->getNetworkId()){
380 Timings::$playerNetworkReceiveDecompress->startTiming();
382 $decompressed = $this->compressor->decompress($compressed);
383 }
catch(DecompressionException $e){
384 $this->logger->debug(
"Failed to decompress packet: " . base64_encode($compressed));
385 throw PacketHandlingException::wrap($e,
"Compressed packet batch decode error");
387 Timings::$playerNetworkReceiveDecompress->stopTiming();
390 throw new PacketHandlingException(
"Packet compressed with unexpected compression type $compressionType");
393 $decompressed = $payload;
397 $stream =
new BinaryStream($decompressed);
398 foreach(PacketBatch::decodeRaw($stream) as $buffer){
399 $this->gamePacketLimiter->decrement();
400 $packet = $this->packetPool->getPacket($buffer);
401 if($packet ===
null){
402 $this->logger->debug(
"Unknown packet: " . base64_encode($buffer));
403 throw new PacketHandlingException(
"Unknown packet received");
406 $this->handleDataPacket($packet, $buffer);
407 }
catch(PacketHandlingException $e){
408 $this->logger->debug($packet->getName() .
": " . base64_encode($buffer));
409 throw PacketHandlingException::wrap($e,
"Error processing " . $packet->getName());
412 }
catch(PacketDecodeException|BinaryDataException $e){
413 $this->logger->logException($e);
414 throw PacketHandlingException::wrap($e,
"Packet batch decode error");
417 Timings::$playerNetworkReceive->stopTiming();
429 $timings = Timings::getReceiveDataPacketTimings($packet);
430 $timings->startTiming();
433 if(DataPacketDecodeEvent::hasHandlers()){
436 if($ev->isCancelled()){
441 $decodeTimings = Timings::getDecodeDataPacketTimings($packet);
442 $decodeTimings->startTiming();
444 $stream = PacketSerializer::decoder($buffer, 0);
446 $packet->decode($stream);
447 }
catch(PacketDecodeException $e){
448 throw PacketHandlingException::wrap($e);
450 if(!$stream->feof()){
451 $remains = substr($stream->getBuffer(), $stream->getOffset());
452 $this->logger->debug(
"Still " . strlen($remains) .
" bytes unread in " . $packet->getName() .
": " . bin2hex($remains));
455 $decodeTimings->stopTiming();
458 if(DataPacketReceiveEvent::hasHandlers()){
459 $ev =
new DataPacketReceiveEvent($this, $packet);
461 if($ev->isCancelled()){
465 $handlerTimings = Timings::getHandleDataPacketTimings($packet);
466 $handlerTimings->startTiming();
468 if($this->handler ===
null || !$packet->handle($this->handler)){
469 $this->logger->debug(
"Unhandled " . $packet->getName() .
": " . base64_encode($stream->getBuffer()));
472 $handlerTimings->stopTiming();
475 $timings->stopTiming();
479 public function handleAckReceipt(
int $receiptId) : void{
480 if(!$this->connected){
483 if(isset($this->ackPromisesByReceiptId[$receiptId])){
484 $promises = $this->ackPromisesByReceiptId[$receiptId];
485 unset($this->ackPromisesByReceiptId[$receiptId]);
486 foreach($promises as $promise){
487 $promise->resolve(
true);
495 private function sendDataPacketInternal(ClientboundPacket $packet,
bool $immediate, ?PromiseResolver $ackReceiptResolver) : bool{
496 if(!$this->connected){
500 if(!$this->loggedIn && !$packet->canBeSentBeforeLogin()){
501 throw new \InvalidArgumentException(
"Attempted to send " . get_class($packet) .
" to " . $this->getDisplayName() .
" too early");
504 $timings = Timings::getSendDataPacketTimings($packet);
505 $timings->startTiming();
507 if(DataPacketSendEvent::hasHandlers()){
508 $ev =
new DataPacketSendEvent([$this], [$packet]);
510 if($ev->isCancelled()){
513 $packets = $ev->getPackets();
515 $packets = [$packet];
518 if($ackReceiptResolver !==
null){
519 $this->sendBufferAckPromises[] = $ackReceiptResolver;
521 foreach($packets as $evPacket){
522 $this->addToSendBuffer(self::encodePacketTimed(PacketSerializer::encoder(), $evPacket));
525 $this->flushSendBuffer(
true);
530 $timings->stopTiming();
534 public function sendDataPacket(ClientboundPacket $packet,
bool $immediate =
false) : bool{
535 return $this->sendDataPacketInternal($packet, $immediate, null);
544 if(!$this->sendDataPacketInternal($packet, $immediate, $resolver)){
548 return $resolver->getPromise();
554 public static function encodePacketTimed(PacketSerializer $serializer, ClientboundPacket $packet) : string{
555 $timings =
Timings::getEncodeDataPacketTimings($packet);
556 $timings->startTiming();
558 $packet->encode($serializer);
559 return $serializer->getBuffer();
561 $timings->stopTiming();
568 public function addToSendBuffer(
string $buffer) : void{
569 $this->sendBuffer[] = $buffer;
572 private function flushSendBuffer(
bool $immediate =
false) : void{
573 if(count($this->sendBuffer) > 0){
574 Timings::$playerNetworkSend->startTiming();
579 }elseif($this->forceAsyncCompression){
583 $stream =
new BinaryStream();
584 PacketBatch::encodeRaw($stream, $this->sendBuffer);
586 if($this->enableCompression){
587 $batch = $this->
server->prepareBatch($stream->getBuffer(), $this->compressor, $syncMode, Timings::$playerNetworkSendCompressSessionBuffer);
589 $batch = $stream->getBuffer();
591 $this->sendBuffer = [];
592 $ackPromises = $this->sendBufferAckPromises;
593 $this->sendBufferAckPromises = [];
594 $this->queueCompressedNoBufferFlush($batch, $immediate, $ackPromises);
596 Timings::$playerNetworkSend->stopTiming();
601 public function getBroadcaster() : PacketBroadcaster{ return $this->broadcaster; }
603 public function getEntityEventBroadcaster() : EntityEventBroadcaster{ return $this->entityEventBroadcaster; }
605 public function getCompressor() : Compressor{
606 return $this->compressor;
609 public function getTypeConverter() : TypeConverter{ return $this->typeConverter; }
611 public function queueCompressed(CompressBatchPromise|
string $payload,
bool $immediate =
false) : void{
612 Timings::$playerNetworkSend->startTiming();
614 $this->flushSendBuffer($immediate);
615 $this->queueCompressedNoBufferFlush($payload, $immediate);
617 Timings::$playerNetworkSend->stopTiming();
626 private function queueCompressedNoBufferFlush(CompressBatchPromise|
string $batch,
bool $immediate =
false, array $ackPromises = []) : void{
627 Timings::$playerNetworkSend->startTiming();
629 if(is_string($batch)){
632 $this->sendEncoded($batch,
true, $ackPromises);
634 $this->compressedQueue->enqueue([$batch, $ackPromises]);
635 $this->flushCompressedQueue();
639 $this->sendEncoded($batch->getResult(),
true, $ackPromises);
641 $this->compressedQueue->enqueue([$batch, $ackPromises]);
642 $batch->onResolve(
function() :
void{
643 if($this->connected){
644 $this->flushCompressedQueue();
649 Timings::$playerNetworkSend->stopTiming();
653 private function flushCompressedQueue() : void{
654 Timings::$playerNetworkSend->startTiming();
656 while(!$this->compressedQueue->isEmpty()){
658 [$current, $ackPromises] = $this->compressedQueue->bottom();
659 if(is_string($current)){
660 $this->compressedQueue->dequeue();
661 $this->sendEncoded($current,
false, $ackPromises);
663 }elseif($current->hasResult()){
664 $this->compressedQueue->dequeue();
665 $this->sendEncoded($current->getResult(),
false, $ackPromises);
673 Timings::$playerNetworkSend->stopTiming();
681 private function sendEncoded(
string $payload,
bool $immediate, array $ackPromises) : void{
682 if($this->cipher !== null){
683 Timings::$playerNetworkSendEncrypt->startTiming();
684 $payload = $this->cipher->encrypt($payload);
685 Timings::$playerNetworkSendEncrypt->stopTiming();
688 if(count($ackPromises) > 0){
689 $ackReceiptId = $this->nextAckReceiptId++;
690 $this->ackPromisesByReceiptId[$ackReceiptId] = $ackPromises;
692 $ackReceiptId =
null;
694 $this->sender->send($payload, $immediate, $ackReceiptId);
700 private function tryDisconnect(\Closure $func, Translatable|
string $reason) : void{
701 if($this->connected && !$this->disconnectGuard){
702 $this->disconnectGuard =
true;
704 $this->disconnectGuard =
false;
705 $this->flushSendBuffer(
true);
706 $this->sender->close(
"");
707 foreach($this->disposeHooks as $callback){
710 $this->disposeHooks->clear();
711 $this->setHandler(
null);
712 $this->connected =
false;
714 $ackPromisesByReceiptId = $this->ackPromisesByReceiptId;
715 $this->ackPromisesByReceiptId = [];
716 foreach($ackPromisesByReceiptId as $resolvers){
717 foreach($resolvers as $resolver){
721 $sendBufferAckPromises = $this->sendBufferAckPromises;
722 $this->sendBufferAckPromises = [];
723 foreach($sendBufferAckPromises as $resolver){
727 $this->logger->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_close($reason)));
735 private function dispose() : void{
736 $this->invManager = null;
739 private function sendDisconnectPacket(Translatable|
string $message) : void{
740 if($message instanceof Translatable){
741 $translated = $this->
server->getLanguage()->translate($message);
743 $translated = $message;
745 $this->sendDataPacket(DisconnectPacket::create(0, $translated));
755 $this->tryDisconnect(function() use ($reason, $disconnectScreenMessage, $notify) : void{
757 $this->sendDisconnectPacket($disconnectScreenMessage ?? $reason);
759 if($this->player !==
null){
760 $this->player->onPostDisconnect($reason,
null);
765 public function disconnectWithError(Translatable|
string $reason, Translatable|
string|
null $disconnectScreenMessage =
null) : void{
766 $errorId = implode(
"-", str_split(bin2hex(random_bytes(6)), 4));
769 reason: KnownTranslationFactory::pocketmine_disconnect_error($reason, $errorId)->prefix(TextFormat::RED),
770 disconnectScreenMessage: KnownTranslationFactory::pocketmine_disconnect_error($disconnectScreenMessage ?? $reason, $errorId),
774 public function disconnectIncompatibleProtocol(
int $protocolVersion) : void{
775 $this->tryDisconnect(
776 function() use ($protocolVersion) : void{
777 $this->sendDataPacket(PlayStatusPacket::create($protocolVersion < ProtocolInfo::CURRENT_PROTOCOL ? PlayStatusPacket::LOGIN_FAILED_CLIENT : PlayStatusPacket::LOGIN_FAILED_SERVER), true);
779 KnownTranslationFactory::pocketmine_disconnect_incompatibleProtocol((
string) $protocolVersion)
788 $this->tryDisconnect(
function() use ($ip, $port, $reason) :
void{
789 $this->sendDataPacket(TransferPacket::create($ip, $port),
true);
790 if($this->player !==
null){
791 $this->player->onPostDisconnect($reason,
null);
800 $this->tryDisconnect(function() use ($disconnectScreenMessage) : void{
801 $this->sendDisconnectPacket($disconnectScreenMessage);
810 $this->tryDisconnect(function() use ($reason) : void{
811 if($this->player !== null){
812 $this->player->onPostDisconnect($reason,
null);
817 private function setAuthenticationStatus(
bool $authenticated,
bool $authRequired,
Translatable|
string|
null $error, ?
string $clientPubKey) : void{
818 if(!$this->connected){
822 if($authenticated && !($this->info instanceof XboxLivePlayerInfo)){
823 $error =
"Expected XUID but none found";
824 }elseif($clientPubKey ===
null){
825 $error =
"Missing client public key";
830 $this->disconnectWithError(
831 reason: KnownTranslationFactory::pocketmine_disconnect_invalidSession($error),
832 disconnectScreenMessage: KnownTranslationFactory::pocketmine_disconnect_error_authentication()
838 $this->authenticated = $authenticated;
840 if(!$this->authenticated){
842 $this->disconnect(
"Not authenticated", KnownTranslationFactory::disconnectionScreen_notAuthenticated());
845 if($this->info instanceof XboxLivePlayerInfo){
846 $this->logger->warning(
"Discarding unexpected XUID for non-authenticated player");
847 $this->info = $this->info->withoutXboxData();
850 $this->logger->debug(
"Xbox Live authenticated: " . ($this->authenticated ?
"YES" :
"NO"));
852 $checkXUID = $this->
server->getConfigGroup()->getPropertyBool(YmlServerProperties::PLAYER_VERIFY_XUID,
true);
853 $myXUID = $this->info instanceof XboxLivePlayerInfo ? $this->info->getXuid() :
"";
854 $kickForXUIDMismatch =
function(
string $xuid) use ($checkXUID, $myXUID) :
bool{
855 if($checkXUID && $myXUID !== $xuid){
856 $this->logger->debug(
"XUID mismatch: expected '$xuid', but got '$myXUID'");
861 $this->disconnect(
"XUID does not match (possible impersonation attempt)");
867 foreach($this->manager->getSessions() as $existingSession){
868 if($existingSession === $this){
871 $info = $existingSession->getPlayerInfo();
872 if($info !==
null && (strcasecmp($info->getUsername(), $this->info->getUsername()) === 0 || $info->getUuid()->equals($this->info->getUuid()))){
873 if($kickForXUIDMismatch($info instanceof XboxLivePlayerInfo ? $info->getXuid() :
"")){
876 $ev =
new PlayerDuplicateLoginEvent($this, $existingSession, KnownTranslationFactory::disconnectionScreen_loggedinOtherLocation(),
null);
878 if($ev->isCancelled()){
879 $this->disconnect($ev->getDisconnectReason(), $ev->getDisconnectScreenMessage());
883 $existingSession->disconnect($ev->getDisconnectReason(), $ev->getDisconnectScreenMessage());
889 $this->cachedOfflinePlayerData = $this->
server->getOfflinePlayerData($this->info->getUsername());
891 $recordedXUID = $this->cachedOfflinePlayerData !==
null ? $this->cachedOfflinePlayerData->getTag(Player::TAG_LAST_KNOWN_XUID) :
null;
892 if(!($recordedXUID instanceof StringTag)){
893 $this->logger->debug(
"No previous XUID recorded, no choice but to trust this player");
894 }elseif(!$kickForXUIDMismatch($recordedXUID->getValue())){
895 $this->logger->debug(
"XUID match");
899 if(EncryptionContext::$ENABLED){
900 $this->
server->getAsyncPool()->submitTask(
new PrepareEncryptionTask($clientPubKey,
function(
string $encryptionKey,
string $handshakeJwt) :
void{
901 if(!$this->connected){
904 $this->sendDataPacket(ServerToClientHandshakePacket::create($handshakeJwt),
true);
906 $this->cipher = EncryptionContext::fakeGCM($encryptionKey);
908 $this->setHandler(
new HandshakePacketHandler($this->onServerLoginSuccess(...)));
909 $this->logger->debug(
"Enabled encryption");
912 $this->onServerLoginSuccess();
916 private function onServerLoginSuccess() : void{
917 $this->loggedIn = true;
919 $this->sendDataPacket(PlayStatusPacket::create(PlayStatusPacket::LOGIN_SUCCESS));
921 $this->logger->debug(
"Initiating resource packs phase");
923 $packManager = $this->
server->getResourcePackManager();
924 $resourcePacks = $packManager->getResourceStack();
926 foreach($resourcePacks as $resourcePack){
927 $key = $packManager->getPackEncryptionKey($resourcePack->getPackId());
929 $keys[$resourcePack->getPackId()] = $key;
932 $event =
new PlayerResourcePackOfferEvent($this->info, $resourcePacks, $keys, $packManager->resourcePacksRequired());
934 $this->setHandler(
new ResourcePacksPacketHandler($this, $event->getResourcePacks(), $event->getEncryptionKeys(), $event->mustAccept(),
function() :
void{
935 $this->createPlayer();
939 private function beginSpawnSequence() : void{
940 $this->setHandler(new PreSpawnPacketHandler($this->
server, $this->player, $this, $this->invManager));
941 $this->player->setNoClientPredictions();
943 $this->logger->debug(
"Waiting for chunk radius request");
946 public function notifyTerrainReady() : void{
947 $this->logger->debug(
"Sending spawn notification, waiting for spawn response");
948 $this->sendDataPacket(PlayStatusPacket::create(PlayStatusPacket::PLAYER_SPAWN));
949 $this->setHandler(
new SpawnResponsePacketHandler($this->onClientSpawnResponse(...)));
952 private function onClientSpawnResponse() : void{
953 $this->logger->debug(
"Received spawn response, entering in-game phase");
954 $this->player->setNoClientPredictions(
false);
955 $this->player->doFirstSpawn();
956 $this->forceAsyncCompression =
false;
957 $this->setHandler(
new InGamePacketHandler($this->player, $this, $this->invManager));
960 public function onServerDeath(Translatable|
string $deathMessage) : void{
961 if($this->handler instanceof InGamePacketHandler){
962 $this->setHandler(
new DeathPacketHandler($this->player, $this, $this->invManager ??
throw new AssumptionFailedError(), $deathMessage));
966 public function onServerRespawn() : void{
967 $this->entityEventBroadcaster->syncAttributes([$this], $this->player, $this->player->getAttributeMap()->getAll());
968 $this->player->sendData(
null);
970 $this->syncAbilities($this->player);
971 $this->invManager->syncAll();
972 $this->setHandler(
new InGamePacketHandler($this->player, $this, $this->invManager));
975 public function syncMovement(Vector3 $pos, ?
float $yaw =
null, ?
float $pitch =
null,
int $mode = MovePlayerPacket::MODE_NORMAL) : void{
976 if($this->player !== null){
977 $location = $this->player->getLocation();
978 $yaw = $yaw ?? $location->getYaw();
979 $pitch = $pitch ?? $location->getPitch();
981 $this->sendDataPacket(MovePlayerPacket::simple(
982 $this->player->getId(),
983 $this->player->getOffsetPosition($pos),
988 $this->player->onGround,
993 if($this->handler instanceof InGamePacketHandler){
994 $this->handler->forceMoveSync =
true;
999 public function syncViewAreaRadius(
int $distance) : void{
1000 $this->sendDataPacket(ChunkRadiusUpdatedPacket::create($distance));
1003 public function syncViewAreaCenterPoint(Vector3 $newPos,
int $viewDistance) : void{
1004 $this->sendDataPacket(NetworkChunkPublisherUpdatePacket::create(BlockPosition::fromVector3($newPos), $viewDistance * 16, []));
1007 public function syncPlayerSpawnPoint(Position $newSpawn) : void{
1008 $newSpawnBlockPosition = BlockPosition::fromVector3($newSpawn);
1010 $this->sendDataPacket(SetSpawnPositionPacket::playerSpawn($newSpawnBlockPosition, DimensionIds::OVERWORLD, $newSpawnBlockPosition));
1013 public function syncWorldSpawnPoint(Position $newSpawn) : void{
1014 $this->sendDataPacket(SetSpawnPositionPacket::worldSpawn(BlockPosition::fromVector3($newSpawn), DimensionIds::OVERWORLD));
1017 public function syncGameMode(GameMode $mode,
bool $isRollback =
false) : void{
1018 $this->sendDataPacket(SetPlayerGameTypePacket::create($this->typeConverter->coreGameModeToProtocol($mode)));
1019 if($this->player !==
null){
1020 $this->syncAbilities($this->player);
1021 $this->syncAdventureSettings();
1023 if(!$isRollback && $this->invManager !==
null){
1024 $this->invManager->syncCreative();
1028 public function syncAbilities(Player $for) : void{
1029 $isOp = $for->hasPermission(DefaultPermissions::ROOT_OPERATOR);
1033 AbilitiesLayer::ABILITY_ALLOW_FLIGHT => $for->getAllowFlight(),
1034 AbilitiesLayer::ABILITY_FLYING => $for->isFlying(),
1035 AbilitiesLayer::ABILITY_NO_CLIP => !$for->hasBlockCollision(),
1036 AbilitiesLayer::ABILITY_OPERATOR => $isOp,
1037 AbilitiesLayer::ABILITY_TELEPORT => $for->hasPermission(DefaultPermissionNames::COMMAND_TELEPORT_SELF),
1038 AbilitiesLayer::ABILITY_INVULNERABLE => $for->isCreative(),
1039 AbilitiesLayer::ABILITY_MUTED =>
false,
1040 AbilitiesLayer::ABILITY_WORLD_BUILDER =>
false,
1041 AbilitiesLayer::ABILITY_INFINITE_RESOURCES => !$for->hasFiniteResources(),
1042 AbilitiesLayer::ABILITY_LIGHTNING =>
false,
1043 AbilitiesLayer::ABILITY_BUILD => !$for->isSpectator(),
1044 AbilitiesLayer::ABILITY_MINE => !$for->isSpectator(),
1045 AbilitiesLayer::ABILITY_DOORS_AND_SWITCHES => !$for->isSpectator(),
1046 AbilitiesLayer::ABILITY_OPEN_CONTAINERS => !$for->isSpectator(),
1047 AbilitiesLayer::ABILITY_ATTACK_PLAYERS => !$for->isSpectator(),
1048 AbilitiesLayer::ABILITY_ATTACK_MOBS => !$for->isSpectator(),
1049 AbilitiesLayer::ABILITY_PRIVILEGED_BUILDER =>
false,
1054 new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, 0.05, 0.1),
1056 if(!$for->hasBlockCollision()){
1062 $layers[] =
new AbilitiesLayer(AbilitiesLayer::LAYER_SPECTATOR, [
1063 AbilitiesLayer::ABILITY_FLYING =>
true,
1067 $this->sendDataPacket(UpdateAbilitiesPacket::create(
new AbilitiesData(
1068 $isOp ? CommandPermissions::OPERATOR : CommandPermissions::NORMAL,
1069 $isOp ? PlayerPermissions::OPERATOR : PlayerPermissions::MEMBER,
1075 public function syncAdventureSettings() : void{
1076 if($this->player === null){
1077 throw new \LogicException(
"Cannot sync adventure settings for a player that is not yet created");
1080 $this->sendDataPacket(UpdateAdventureSettingsPacket::create(
1081 noAttackingMobs:
false,
1082 noAttackingPlayers:
false,
1083 worldImmutable:
false,
1085 autoJump: $this->player->hasAutoJump()
1089 public function syncAvailableCommands() : void{
1091 foreach($this->
server->getCommandMap()->getCommands() as $name => $command){
1092 if(isset($commandData[$command->getLabel()]) || $command->getLabel() ===
"help" || !$command->testPermissionSilent($this->player)){
1096 $lname = strtolower($command->getLabel());
1097 $aliases = $command->getAliases();
1099 if(count($aliases) > 0){
1100 if(!in_array($lname, $aliases,
true)){
1102 $aliases[] = $lname;
1104 $aliasObj =
new CommandEnum(ucfirst($command->getLabel()) .
"Aliases", array_values($aliases));
1107 $description = $command->getDescription();
1108 $data =
new CommandData(
1110 $description instanceof Translatable ? $this->player->getLanguage()->translate($description) : $description,
1115 new CommandOverload(chaining:
false, parameters: [CommandParameter::standard(
"args", AvailableCommandsPacket::ARG_TYPE_RAWTEXT, 0,
true)])
1117 chainedSubCommandData: []
1120 $commandData[$command->getLabel()] = $data;
1123 $this->sendDataPacket(AvailableCommandsPacket::create($commandData, [], [], []));
1132 $language = $this->player->getLanguage();
1134 return [$language->translateString($message->getText(), $parameters,
"pocketmine."), $parameters];
1137 public function onChatMessage(
Translatable|
string $message) : void{
1139 if(!$this->
server->isLanguageForced()){
1140 $this->sendDataPacket(TextPacket::translation(...$this->prepareClientTranslatableMessage($message)));
1142 $this->sendDataPacket(TextPacket::raw($this->player->getLanguage()->translate($message)));
1145 $this->sendDataPacket(TextPacket::raw($message));
1149 public function onJukeboxPopup(Translatable|
string $message) : void{
1151 if($message instanceof Translatable){
1152 if(!$this->
server->isLanguageForced()){
1153 [$message, $parameters] = $this->prepareClientTranslatableMessage($message);
1155 $message = $this->player->getLanguage()->translate($message);
1158 $this->sendDataPacket(TextPacket::jukeboxPopup($message, $parameters));
1161 public function onPopup(
string $message) : void{
1162 $this->sendDataPacket(TextPacket::popup($message));
1165 public function onTip(
string $message) : void{
1166 $this->sendDataPacket(TextPacket::tip($message));
1169 public function onFormSent(
int $id, Form $form) : bool{
1170 return $this->sendDataPacket(ModalFormRequestPacket::create($id, json_encode($form, JSON_THROW_ON_ERROR)));
1179 $world = $this->player->getLocation()->getWorld();
1180 ChunkCache::getInstance($world, $this->compressor)->request($chunkX, $chunkZ)->onResolve(
1184 if(!$this->isConnected()){
1187 $currentWorld = $this->player->getLocation()->getWorld();
1188 if($world !== $currentWorld || ($status = $this->player->getUsedChunkStatus($chunkX, $chunkZ)) ===
null){
1189 $this->logger->debug(
"Tried to send no-longer-active chunk $chunkX $chunkZ in world " . $world->getFolderName());
1192 if($status !== UsedChunkStatus::REQUESTED_SENDING){
1199 $world->timings->syncChunkSend->startTiming();
1201 $this->queueCompressed($promise);
1204 $world->timings->syncChunkSend->stopTiming();
1210 public function stopUsingChunk(
int $chunkX,
int $chunkZ) : void{
1214 public function onEnterWorld() : void{
1215 if($this->player !== null){
1216 $world = $this->player->getWorld();
1217 $this->syncWorldTime($world->getTime());
1218 $this->syncWorldDifficulty($world->getDifficulty());
1219 $this->syncWorldSpawnPoint($world->getSpawnLocation());
1224 public function syncWorldTime(
int $worldTime) : void{
1225 $this->sendDataPacket(SetTimePacket::create($worldTime));
1228 public function syncWorldDifficulty(
int $worldDifficulty) : void{
1229 $this->sendDataPacket(SetDifficultyPacket::create($worldDifficulty));
1232 public function getInvManager() : ?InventoryManager{
1233 return $this->invManager;
1241 return
PlayerListEntry::createAdditionEntry($player->getUniqueId(), $player->getId(), $player->getDisplayName(), $this->typeConverter->getSkinAdapter()->toSkinData($player->getSkin()), $player->getXuid());
1245 public function onPlayerAdded(
Player $p) : void{
1246 $this->sendDataPacket(PlayerListPacket::add([PlayerListEntry::createAdditionEntry($p->getUniqueId(), $p->getId(), $p->getDisplayName(), $this->typeConverter->getSkinAdapter()->toSkinData($p->getSkin()), $p->getXuid())]));
1249 public function onPlayerRemoved(
Player $p) : void{
1250 if($p !== $this->player){
1251 $this->sendDataPacket(PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($p->
getUniqueId())]));
1255 public function onTitle(
string $title) : void{
1256 $this->sendDataPacket(SetTitlePacket::title($title));
1259 public function onSubTitle(
string $subtitle) : void{
1260 $this->sendDataPacket(SetTitlePacket::subtitle($subtitle));
1263 public function onActionBar(
string $actionBar) : void{
1264 $this->sendDataPacket(SetTitlePacket::actionBarMessage($actionBar));
1267 public function onClearTitle() : void{
1268 $this->sendDataPacket(SetTitlePacket::clearTitle());
1271 public function onResetTitleOptions() : void{
1272 $this->sendDataPacket(SetTitlePacket::resetTitleOptions());
1275 public function onTitleDuration(
int $fadeIn,
int $stay,
int $fadeOut) : void{
1276 $this->sendDataPacket(SetTitlePacket::setAnimationTimes($fadeIn, $stay, $fadeOut));
1279 public function onToastNotification(
string $title,
string $body) : void{
1280 $this->sendDataPacket(ToastRequestPacket::create($title, $body));
1283 public function onOpenSignEditor(Vector3 $signPosition,
bool $frontSide) : void{
1284 $this->sendDataPacket(OpenSignPacket::create(BlockPosition::fromVector3($signPosition), $frontSide));
1287 public function tick() : void{
1288 if(!$this->isConnected()){
1293 if($this->info ===
null){
1294 if(time() >= $this->connectTime + 10){
1295 $this->disconnectWithError(KnownTranslationFactory::pocketmine_disconnect_error_loginTimeout());
1301 if($this->player !==
null){
1302 $this->player->doChunkRequests();
1304 $dirtyAttributes = $this->player->getAttributeMap()->needSend();
1305 $this->entityEventBroadcaster->syncAttributes([$this], $this->player, $dirtyAttributes);
1306 foreach($dirtyAttributes as $attribute){
1309 $attribute->markSynchronized();
1312 Timings::$playerNetworkSendInventorySync->startTiming();
1314 $this->invManager?->flushPendingUpdates();
1316 Timings::$playerNetworkSendInventorySync->stopTiming();
1319 $this->flushSendBuffer();
handleDataPacket(Packet $packet, string $buffer)
prepareClientTranslatableMessage(Translatable $message)
onPlayerDestroyed(Translatable|string $reason, Translatable|string $disconnectScreenMessage)
syncPlayerList(array $players)
sendDataPacketWithReceipt(ClientboundPacket $packet, bool $immediate=false)
onClientDisconnect(Translatable|string $reason)
handleEncoded(string $payload)
transfer(string $ip, int $port, Translatable|string|null $reason=null)
disconnect(Translatable|string $reason, Translatable|string|null $disconnectScreenMessage=null, bool $notify=true)
startUsingChunk(int $chunkX, int $chunkZ, \Closure $onCompletion)