147 private const INCOMING_PACKET_BATCH_PER_TICK = 2;
148 private const INCOMING_PACKET_BATCH_BUFFER_TICKS = 100;
150 private const INCOMING_GAME_PACKETS_PER_TICK = 2;
151 private const INCOMING_GAME_PACKETS_BUFFER_TICKS = 100;
153 private const INCOMING_PACKET_BATCH_HARD_LIMIT = 300;
158 private \PrefixedLogger $logger;
159 private ?
Player $player =
null;
161 private ?
int $ping =
null;
168 private ?array $handlerActions =
null;
170 private bool $connected =
true;
171 private bool $disconnectGuard =
false;
172 private bool $loggedIn =
false;
173 private bool $authenticated =
false;
174 private int $connectTime;
175 private ?
CompoundTag $cachedOfflinePlayerData =
null;
183 private array $sendBuffer = [];
188 private array $sendBufferAckPromises = [];
191 private \SplQueue $compressedQueue;
192 private bool $forceAsyncCompression =
true;
193 private bool $enableCompression =
false;
195 private int $nextAckReceiptId = 0;
200 private array $ackPromisesByReceiptId = [];
210 private string $noisyPacketBuffer =
"";
211 private int $noisyPacketsDropped = 0;
213 public function __construct(
225 $this->logger = new \PrefixedLogger($this->
server->getLogger(), $this->getLogPrefix());
227 $this->compressedQueue = new \SplQueue();
231 $this->connectTime = time();
232 $this->packetBatchLimiter =
new PacketRateLimiter(
"Packet Batches", self::INCOMING_PACKET_BATCH_PER_TICK, self::INCOMING_PACKET_BATCH_BUFFER_TICKS);
233 $this->gamePacketLimiter =
new PacketRateLimiter(
"Game Packets", self::INCOMING_GAME_PACKETS_PER_TICK, self::INCOMING_GAME_PACKETS_BUFFER_TICKS);
237 $this->onSessionStartSuccess(...)
240 $this->manager->add($this);
241 $this->logger->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_open()));
244 private function getLogPrefix() :
string{
245 return "NetworkSession: " . $this->getDisplayName();
248 public function getLogger() : \
Logger{
249 return $this->logger;
252 private function onSessionStartSuccess() :
void{
253 $this->logger->debug(
"Session start handshake completed, awaiting login packet");
254 $this->flushGamePacketQueue();
255 $this->enableCompression =
true;
261 $this->logger->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_playerName(TextFormat::AQUA . $info->getUsername() . TextFormat::RESET)));
262 $this->logger->setPrefix($this->getLogPrefix());
263 $this->manager->markLoginReceived($this);
265 $this->setAuthenticationStatus(...)
269 protected function createPlayer() :
void{
270 $this->
server->createPlayer($this, $this->info, $this->authenticated, $this->cachedOfflinePlayerData)->onCompletion(
271 $this->onPlayerCreated(...),
274 $this->disconnectWithError(
275 reason:
"Failed to create player",
276 disconnectScreenMessage: KnownTranslationFactory::pocketmine_disconnect_error_internal()
282 private function onPlayerCreated(
Player $player) :
void{
283 if(!$this->isConnected()){
287 $this->player = $player;
288 if(!$this->
server->addOnlinePlayer($player)){
294 $effectManager = $this->player->getEffects();
295 $effectManager->getEffectAddHooks()->add($effectAddHook =
function(
EffectInstance $effect,
bool $replacesOldEffect) :
void{
296 $this->entityEventBroadcaster->onEntityEffectAdded([$this], $this->player, $effect, $replacesOldEffect);
298 $effectManager->getEffectRemoveHooks()->add($effectRemoveHook =
function(
EffectInstance $effect) :
void{
299 $this->entityEventBroadcaster->onEntityEffectRemoved([$this], $this->player, $effect);
301 $this->disposeHooks->add(
static function() use ($effectManager, $effectAddHook, $effectRemoveHook) :
void{
302 $effectManager->getEffectAddHooks()->remove($effectAddHook);
303 $effectManager->getEffectRemoveHooks()->remove($effectRemoveHook);
306 $permissionHooks = $this->player->getPermissionRecalculationCallbacks();
307 $permissionHooks->add($permHook =
function() :
void{
308 $this->logger->debug(
"Syncing available commands and abilities/permissions due to permission recalculation");
309 $this->syncAbilities($this->player);
310 $this->syncAvailableCommands();
312 $this->disposeHooks->add(
static function() use ($permissionHooks, $permHook) :
void{
313 $permissionHooks->remove($permHook);
315 $this->beginSpawnSequence();
318 public function getPlayer() : ?
Player{
319 return $this->player;
322 public function getPlayerInfo() : ?
PlayerInfo{
326 public function isConnected() :
bool{
327 return $this->connected && !$this->disconnectGuard;
330 public function getIp() :
string{
334 public function getPort() :
int{
338 public function getDisplayName() :
string{
339 return $this->info !==
null ? $this->info->getUsername() : $this->ip .
" " . $this->port;
352 public function updatePing(
int $ping) : void{
356 public function getHandler() : ?PacketHandler{
357 return $this->handler;
360 public function setHandler(?PacketHandler $handler) : void{
361 if($this->connected){
362 $this->handler = $handler;
363 if($this->handler !==
null){
364 $this->handlerActions = PacketHandlerInspector::getHandlerActions($this->handler);
365 $this->handler->setUp();
367 $this->handlerActions =
null;
372 private function checkRepeatedPacketFilter(
string $buffer) : bool{
373 if($buffer === $this->noisyPacketBuffer){
374 $this->noisyPacketsDropped++;
380 $this->noisyPacketBuffer =
"";
381 $this->noisyPacketsDropped = 0;
390 if(!$this->connected){
394 Timings::$playerNetworkReceive->startTiming();
396 $this->packetBatchLimiter->decrement();
398 if($this->cipher !==
null){
399 Timings::$playerNetworkReceiveDecrypt->startTiming();
401 $payload = $this->cipher->decrypt($payload);
402 }
catch(DecryptionException $e){
403 $this->logger->debug(
"Encrypted packet: " . base64_encode($payload));
404 throw PacketHandlingException::wrap($e,
"Packet decryption error");
406 Timings::$playerNetworkReceiveDecrypt->stopTiming();
410 if(strlen($payload) < 1){
411 throw new PacketHandlingException(
"No bytes in payload");
414 if($this->enableCompression){
415 $compressionType = ord($payload[0]);
416 $compressed = substr($payload, 1);
417 if($compressionType === CompressionAlgorithm::NONE){
418 $decompressed = $compressed;
419 }elseif($compressionType === $this->compressor->getNetworkId()){
420 Timings::$playerNetworkReceiveDecompress->startTiming();
422 $decompressed = $this->compressor->decompress($compressed);
423 }
catch(DecompressionException $e){
424 $this->logger->debug(
"Failed to decompress packet: " . base64_encode($compressed));
425 throw PacketHandlingException::wrap($e,
"Compressed packet batch decode error");
427 Timings::$playerNetworkReceiveDecompress->stopTiming();
430 throw new PacketHandlingException(
"Packet compressed with unexpected compression type $compressionType");
433 $decompressed = $payload;
438 $stream =
new ByteBufferReader($decompressed);
439 foreach(PacketBatch::decodeRaw($stream) as $buffer){
440 if(++$count >= self::INCOMING_PACKET_BATCH_HARD_LIMIT){
444 throw new PacketHandlingException(
"Reached hard limit of " . self::INCOMING_PACKET_BATCH_HARD_LIMIT .
" per batch packet");
447 if($this->checkRepeatedPacketFilter($buffer)){
451 $this->gamePacketLimiter->decrement();
452 $packet = $this->packetPool->getPacket($buffer);
453 if($packet ===
null){
454 $this->logger->debug(
"Unknown packet: " . base64_encode($buffer));
455 throw new PacketHandlingException(
"Unknown packet received");
458 $this->handleDataPacket($packet, $buffer);
459 }
catch(PacketHandlingException $e){
460 $this->logger->debug($packet->getName() .
": " . base64_encode($buffer));
461 throw PacketHandlingException::wrap($e,
"Error processing " . $packet->getName());
462 }
catch(FilterNoisyPacketException){
463 $this->noisyPacketBuffer = $buffer;
465 if(!$this->isConnected()){
467 $this->logger->debug(
"Aborting batch processing due to server-side disconnection");
471 }
catch(PacketDecodeException|DataDecodeException $e){
472 $this->logger->logException($e);
473 throw PacketHandlingException::wrap($e,
"Packet batch decode error");
476 Timings::$playerNetworkReceive->stopTiming();
480 private function unhandledPacketDebug(Packet $packet,
string $buffer,
string $label) : void{
481 $this->logger->debug($label .
": " . $packet->getName() .
": " . base64_encode($buffer));
493 $timings = Timings::getReceiveDataPacketTimings($packet);
494 $timings->startTiming();
497 $handlerAction = PacketHandlerAction::DISCARD_WITH_DEBUG;
500 if($this->handlerActions !==
null && isset($this->handlerActions[$packet::class])){
501 $handlerAction = $this->handlerActions[$packet::class];
503 if(DataPacketDecodeEvent::hasHandlers()){
504 $ev =
new DataPacketDecodeEvent($this, $packet->pid(), $buffer);
505 $cancel = $handlerAction !== PacketHandlerAction::HANDLED;
510 if($cancel && !$ev->isCancelled()){
512 $handlerAction = PacketHandlerAction::HANDLED;
513 }elseif(!$cancel && $ev->isCancelled()){
515 $handlerAction = PacketHandlerAction::DISCARD_SILENT;
519 if($handlerAction !== PacketHandlerAction::HANDLED){
520 if($handlerAction === PacketHandlerAction::DISCARD_WITH_DEBUG){
521 $this->unhandledPacketDebug($packet, $buffer,
"Discarded without decoding");
526 $decodeTimings = Timings::getDecodeDataPacketTimings($packet);
527 $decodeTimings->startTiming();
529 $stream =
new ByteBufferReader($buffer);
531 $packet->decode($stream);
532 }
catch(PacketDecodeException $e){
533 throw PacketHandlingException::wrap($e);
535 if($stream->getUnreadLength() > 0){
536 $remains = substr($stream->getData(), $stream->getOffset());
537 $this->logger->debug(
"Still " . strlen($remains) .
" bytes unread in " . $packet->getName() .
": " . bin2hex($remains));
540 $decodeTimings->stopTiming();
543 if(DataPacketReceiveEvent::hasHandlers()){
544 $ev =
new DataPacketReceiveEvent($this, $packet);
546 if($ev->isCancelled()){
550 $handlerTimings = Timings::getHandleDataPacketTimings($packet);
551 $handlerTimings->startTiming();
553 if($this->handler ===
null || !$packet->handle($this->handler)){
554 $this->unhandledPacketDebug($packet, $buffer,
"Handler rejected");
557 $handlerTimings->stopTiming();
560 $timings->stopTiming();
564 public function handleAckReceipt(
int $receiptId) : void{
565 if(!$this->connected){
568 if(isset($this->ackPromisesByReceiptId[$receiptId])){
569 $promises = $this->ackPromisesByReceiptId[$receiptId];
570 unset($this->ackPromisesByReceiptId[$receiptId]);
571 foreach($promises as $promise){
572 $promise->resolve(
true);
580 private function sendDataPacketInternal(ClientboundPacket $packet,
bool $immediate, ?PromiseResolver $ackReceiptResolver) : bool{
581 if(!$this->connected){
585 if(!$this->loggedIn && !$packet->canBeSentBeforeLogin()){
586 throw new \InvalidArgumentException(
"Attempted to send " . get_class($packet) .
" to " . $this->getDisplayName() .
" too early");
589 $timings = Timings::getSendDataPacketTimings($packet);
590 $timings->startTiming();
592 if(DataPacketSendEvent::hasHandlers()){
593 $ev = new DataPacketSendEvent([$this], [$packet]);
595 if($ev->isCancelled()){
598 $packets = $ev->getPackets();
600 $packets = [$packet];
603 if($ackReceiptResolver !==
null){
604 $this->sendBufferAckPromises[] = $ackReceiptResolver;
606 $writer =
new ByteBufferWriter();
607 foreach($packets as $evPacket){
609 $this->addToSendBuffer(self::encodePacketTimed($writer, $evPacket));
612 $this->flushGamePacketQueue();
617 $timings->stopTiming();
621 public function sendDataPacket(ClientboundPacket $packet,
bool $immediate =
false) : bool{
622 return $this->sendDataPacketInternal($packet, $immediate, null);
632 if(!$this->sendDataPacketInternal($packet, $immediate, $resolver)){
642 public static function encodePacketTimed(ByteBufferWriter $serializer, ClientboundPacket $packet) : string{
643 $timings =
Timings::getEncodeDataPacketTimings($packet);
644 $timings->startTiming();
646 $packet->encode($serializer);
647 return $serializer->getData();
649 $timings->stopTiming();
656 public function addToSendBuffer(
string $buffer) : void{
657 $this->sendBuffer[] = $buffer;
660 private function flushGamePacketQueue() : void{
661 if(count($this->sendBuffer) > 0){
662 Timings::$playerNetworkSend->startTiming();
665 if($this->forceAsyncCompression){
669 $stream =
new ByteBufferWriter();
670 PacketBatch::encodeRaw($stream, $this->sendBuffer);
672 if($this->enableCompression){
673 $batch = $this->
server->prepareBatch($stream->getData(), $this->compressor, $syncMode, Timings::$playerNetworkSendCompressSessionBuffer);
675 $batch = $stream->getData();
677 $this->sendBuffer = [];
678 $ackPromises = $this->sendBufferAckPromises;
679 $this->sendBufferAckPromises = [];
682 $this->queueCompressedNoGamePacketFlush($batch, networkFlush:
true, ackPromises: $ackPromises);
684 Timings::$playerNetworkSend->stopTiming();
689 public function getBroadcaster() : PacketBroadcaster{ return $this->broadcaster; }
691 public function getEntityEventBroadcaster() : EntityEventBroadcaster{ return $this->entityEventBroadcaster; }
693 public function getCompressor() : Compressor{
694 return $this->compressor;
697 public function getTypeConverter() : TypeConverter{ return $this->typeConverter; }
699 public function queueCompressed(CompressBatchPromise|
string $payload,
bool $immediate =
false) : void{
700 Timings::$playerNetworkSend->startTiming();
704 $this->flushGamePacketQueue();
705 $this->queueCompressedNoGamePacketFlush($payload, $immediate);
707 Timings::$playerNetworkSend->stopTiming();
716 private function queueCompressedNoGamePacketFlush(CompressBatchPromise|
string $batch,
bool $networkFlush =
false, array $ackPromises = []) : void{
717 Timings::$playerNetworkSend->startTiming();
719 $this->compressedQueue->enqueue([$batch, $ackPromises, $networkFlush]);
720 if(is_string($batch)){
721 $this->flushCompressedQueue();
723 $batch->onResolve(
function() :
void{
724 if($this->connected){
725 $this->flushCompressedQueue();
730 Timings::$playerNetworkSend->stopTiming();
734 private function flushCompressedQueue() : void{
735 Timings::$playerNetworkSend->startTiming();
737 while(!$this->compressedQueue->isEmpty()){
739 [$current, $ackPromises, $networkFlush] = $this->compressedQueue->bottom();
740 if(is_string($current)){
741 $this->compressedQueue->dequeue();
742 $this->sendEncoded($current, $networkFlush, $ackPromises);
744 }elseif($current->hasResult()){
745 $this->compressedQueue->dequeue();
746 $this->sendEncoded($current->getResult(), $networkFlush, $ackPromises);
754 Timings::$playerNetworkSend->stopTiming();
762 private function sendEncoded(
string $payload,
bool $immediate, array $ackPromises) : void{
763 if($this->cipher !== null){
764 Timings::$playerNetworkSendEncrypt->startTiming();
765 $payload = $this->cipher->encrypt($payload);
766 Timings::$playerNetworkSendEncrypt->stopTiming();
769 if(count($ackPromises) > 0){
770 $ackReceiptId = $this->nextAckReceiptId++;
771 $this->ackPromisesByReceiptId[$ackReceiptId] = $ackPromises;
773 $ackReceiptId =
null;
775 $this->sender->send($payload, $immediate, $ackReceiptId);
781 private function tryDisconnect(\Closure $func, Translatable|
string $reason) : void{
782 if($this->connected && !$this->disconnectGuard){
783 $this->disconnectGuard =
true;
785 $this->disconnectGuard =
false;
786 $this->flushGamePacketQueue();
787 $this->sender->close(
"");
788 foreach($this->disposeHooks as $callback){
791 $this->disposeHooks->clear();
792 $this->setHandler(
null);
793 $this->connected =
false;
795 $ackPromisesByReceiptId = $this->ackPromisesByReceiptId;
796 $this->ackPromisesByReceiptId = [];
797 foreach($ackPromisesByReceiptId as $resolvers){
798 foreach($resolvers as $resolver){
802 $sendBufferAckPromises = $this->sendBufferAckPromises;
803 $this->sendBufferAckPromises = [];
804 foreach($sendBufferAckPromises as $resolver){
808 $this->logger->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_close($reason)));
816 private function dispose() : void{
817 $this->invManager = null;
820 private function sendDisconnectPacket(Translatable|
string $message) : void{
821 if($message instanceof Translatable){
822 $translated = $this->
server->getLanguage()->translate($message);
824 $translated = $message;
826 $this->sendDataPacket(DisconnectPacket::create(0, $translated,
""));
836 $this->tryDisconnect(function() use ($reason, $disconnectScreenMessage, $notify) : void{
838 $this->sendDisconnectPacket($disconnectScreenMessage ?? $reason);
840 if($this->player !==
null){
841 $this->player->onPostDisconnect($reason,
null);
846 public function disconnectWithError(Translatable|
string $reason, Translatable|
string|
null $disconnectScreenMessage =
null) : void{
847 $errorId = implode(
"-", str_split(bin2hex(random_bytes(6)), 4));
850 reason: KnownTranslationFactory::pocketmine_disconnect_error($reason, $errorId)->prefix(TextFormat::RED),
851 disconnectScreenMessage: KnownTranslationFactory::pocketmine_disconnect_error($disconnectScreenMessage ?? $reason, $errorId),
855 public function disconnectIncompatibleProtocol(
int $protocolVersion) : void{
856 $this->tryDisconnect(
857 function() use ($protocolVersion) : void{
858 $this->sendDataPacket(PlayStatusPacket::create($protocolVersion < ProtocolInfo::CURRENT_PROTOCOL ? PlayStatusPacket::LOGIN_FAILED_CLIENT : PlayStatusPacket::LOGIN_FAILED_SERVER), true);
860 KnownTranslationFactory::pocketmine_disconnect_incompatibleProtocol((
string) $protocolVersion)
869 $this->tryDisconnect(
function() use ($ip, $port, $reason) :
void{
870 $this->sendDataPacket(TransferPacket::create($ip, $port,
false),
true);
871 if($this->player !==
null){
872 $this->player->onPostDisconnect($reason,
null);
881 $this->tryDisconnect(function() use ($disconnectScreenMessage) : void{
882 $this->sendDisconnectPacket($disconnectScreenMessage);
891 $this->tryDisconnect(function() use ($reason) : void{
892 if($this->player !== null){
893 $this->player->onPostDisconnect($reason,
null);
898 private function setAuthenticationStatus(
bool $authenticated,
bool $authRequired,
Translatable|
string|
null $error, ?
string $clientPubKey) : void{
899 if(!$this->connected){
903 if($authenticated && !($this->info instanceof XboxLivePlayerInfo)){
904 $error =
"Expected XUID but none found";
905 }elseif($clientPubKey ===
null){
906 $error =
"Missing client public key";
911 $this->disconnectWithError(
912 reason: KnownTranslationFactory::pocketmine_disconnect_invalidSession($error),
913 disconnectScreenMessage: KnownTranslationFactory::pocketmine_disconnect_error_authentication()
919 $this->authenticated = $authenticated;
921 if(!$this->authenticated){
923 $this->disconnect(
"Not authenticated", KnownTranslationFactory::disconnectionScreen_notAuthenticated());
926 if($this->info instanceof XboxLivePlayerInfo){
927 $this->logger->warning(
"Discarding unexpected XUID for non-authenticated player");
928 $this->info = $this->info->withoutXboxData();
931 $this->logger->debug(
"Xbox Live authenticated: " . ($this->authenticated ?
"YES" :
"NO"));
933 $checkXUID = $this->
server->getConfigGroup()->getPropertyBool(YmlServerProperties::PLAYER_VERIFY_XUID,
true);
934 $myXUID = $this->info instanceof XboxLivePlayerInfo ? $this->info->getXuid() :
"";
935 $kickForXUIDMismatch =
function(
string $xuid) use ($checkXUID, $myXUID) : bool{
936 if($checkXUID && $myXUID !== $xuid){
937 $this->logger->debug(
"XUID mismatch: expected '$xuid', but got '$myXUID'");
942 $this->disconnect(
"XUID does not match (possible impersonation attempt)");
948 foreach($this->manager->getSessions() as $existingSession){
949 if($existingSession === $this){
952 $info = $existingSession->getPlayerInfo();
953 if($info !==
null && (strcasecmp($info->getUsername(), $this->info->getUsername()) === 0 || $info->getUuid()->equals($this->info->getUuid()))){
954 if($kickForXUIDMismatch($info instanceof XboxLivePlayerInfo ? $info->getXuid() :
"")){
957 $ev =
new PlayerDuplicateLoginEvent($this, $existingSession, KnownTranslationFactory::disconnectionScreen_loggedinOtherLocation(),
null);
959 if($ev->isCancelled()){
960 $this->disconnect($ev->getDisconnectReason(), $ev->getDisconnectScreenMessage());
964 $existingSession->disconnect($ev->getDisconnectReason(), $ev->getDisconnectScreenMessage());
970 $this->cachedOfflinePlayerData = $this->
server->getOfflinePlayerData($this->info->getUsername());
972 $recordedXUID = $this->cachedOfflinePlayerData !==
null ? $this->cachedOfflinePlayerData->getTag(Player::TAG_LAST_KNOWN_XUID) :
null;
973 if(!($recordedXUID instanceof StringTag)){
974 $this->logger->debug(
"No previous XUID recorded, no choice but to trust this player");
975 }elseif(!$kickForXUIDMismatch($recordedXUID->getValue())){
976 $this->logger->debug(
"XUID match");
980 if(EncryptionContext::$ENABLED){
981 $this->
server->getAsyncPool()->submitTask(
new PrepareEncryptionTask($clientPubKey,
function(
string $encryptionKey,
string $handshakeJwt) :
void{
982 if(!$this->connected){
985 $this->sendDataPacket(ServerToClientHandshakePacket::create($handshakeJwt),
true);
987 $this->cipher = EncryptionContext::fakeGCM($encryptionKey);
989 $this->setHandler(
new HandshakePacketHandler($this->onServerLoginSuccess(...)));
990 $this->logger->debug(
"Enabled encryption");
993 $this->onServerLoginSuccess();
997 private function onServerLoginSuccess() : void{
998 $this->loggedIn = true;
1000 $this->sendDataPacket(PlayStatusPacket::create(PlayStatusPacket::LOGIN_SUCCESS));
1002 $this->logger->debug(
"Initiating resource packs phase");
1004 $packManager = $this->
server->getResourcePackManager();
1005 $resourcePacks = $packManager->getResourceStack();
1007 foreach($resourcePacks as $resourcePack){
1008 $key = $packManager->getPackEncryptionKey($resourcePack->getPackId());
1010 $keys[$resourcePack->getPackId()->toString()] = $key;
1013 $event =
new PlayerResourcePackOfferEvent($this->info, $resourcePacks, $keys, $packManager->resourcePacksRequired());
1015 $this->setHandler(
new ResourcePacksPacketHandler($this, $event->getResourcePacks(), $event->getEncryptionKeys(), $event->mustAccept(),
function() :
void{
1016 $this->createPlayer();
1020 private function beginSpawnSequence() : void{
1021 $this->setHandler(new PreSpawnPacketHandler($this->
server, $this->player, $this, $this->invManager));
1022 $this->player->setNoClientPredictions();
1024 $this->logger->debug(
"Waiting for chunk radius request");
1027 public function notifyTerrainReady() : void{
1028 $this->logger->debug(
"Sending spawn notification, waiting for spawn response");
1029 $this->sendDataPacket(PlayStatusPacket::create(PlayStatusPacket::PLAYER_SPAWN));
1030 $this->setHandler(
new SpawnResponsePacketHandler($this->onClientSpawnResponse(...)));
1033 private function onClientSpawnResponse() : void{
1034 $this->logger->debug(
"Received spawn response, entering in-game phase");
1035 $this->player->setNoClientPredictions(
false);
1036 $this->player->doFirstSpawn();
1037 $this->forceAsyncCompression =
false;
1038 $this->setHandler(
new InGamePacketHandler($this->player, $this, $this->invManager));
1041 public function onServerDeath(Translatable|
string $deathMessage) : void{
1042 if($this->handler instanceof InGamePacketHandler){
1043 $this->setHandler(
new DeathPacketHandler($this->player, $this, $this->invManager ??
throw new AssumptionFailedError(), $deathMessage));
1047 public function onServerRespawn() : void{
1048 $this->entityEventBroadcaster->syncAttributes([$this], $this->player, $this->player->getAttributeMap()->getAll());
1049 $this->player->sendData(
null);
1051 $this->syncAbilities($this->player);
1052 $this->invManager->syncAll();
1053 $this->setHandler(
new InGamePacketHandler($this->player, $this, $this->invManager));
1056 public function syncMovement(Vector3 $pos, ?
float $yaw =
null, ?
float $pitch =
null,
int $mode = MovePlayerPacket::MODE_NORMAL) : void{
1057 if($this->player !== null){
1058 $location = $this->player->getLocation();
1059 $yaw = $yaw ?? $location->getYaw();
1060 $pitch = $pitch ?? $location->getPitch();
1062 $this->sendDataPacket(MovePlayerPacket::simple(
1063 $this->player->getId(),
1064 $this->player->getOffsetPosition($pos),
1069 $this->player->onGround,
1074 if($this->handler instanceof InGamePacketHandler){
1075 $this->handler->forceMoveSync =
true;
1080 public function syncViewAreaRadius(
int $distance) : void{
1081 $this->sendDataPacket(ChunkRadiusUpdatedPacket::create($distance));
1084 public function syncViewAreaCenterPoint(Vector3 $newPos,
int $viewDistance) : void{
1085 $this->sendDataPacket(NetworkChunkPublisherUpdatePacket::create(BlockPosition::fromVector3($newPos), $viewDistance * 16, []));
1088 public function syncPlayerSpawnPoint(Position $newSpawn) : void{
1089 $newSpawnBlockPosition = BlockPosition::fromVector3($newSpawn);
1091 $this->sendDataPacket(SetSpawnPositionPacket::playerSpawn($newSpawnBlockPosition, DimensionIds::OVERWORLD, $newSpawnBlockPosition));
1094 public function syncWorldSpawnPoint(Position $newSpawn) : void{
1095 $this->sendDataPacket(SetSpawnPositionPacket::worldSpawn(BlockPosition::fromVector3($newSpawn), DimensionIds::OVERWORLD));
1098 public function syncGameMode(GameMode $mode,
bool $isRollback =
false) : void{
1099 $this->sendDataPacket(SetPlayerGameTypePacket::create($this->typeConverter->coreGameModeToProtocol($mode)));
1100 if($this->player !==
null){
1101 $this->syncAbilities($this->player);
1102 $this->syncAdventureSettings();
1104 if(!$isRollback && $this->invManager !==
null){
1105 $this->invManager->syncCreative();
1109 public function syncAbilities(Player $for) : void{
1110 $isOp = $for->hasPermission(DefaultPermissions::ROOT_OPERATOR);
1114 AbilitiesLayer::ABILITY_ALLOW_FLIGHT => $for->getAllowFlight(),
1115 AbilitiesLayer::ABILITY_FLYING => $for->isFlying(),
1116 AbilitiesLayer::ABILITY_NO_CLIP => !$for->hasBlockCollision(),
1117 AbilitiesLayer::ABILITY_OPERATOR => $isOp,
1118 AbilitiesLayer::ABILITY_TELEPORT => $for->hasPermission(DefaultPermissionNames::COMMAND_TELEPORT_SELF),
1119 AbilitiesLayer::ABILITY_INVULNERABLE => $for->isCreative(),
1120 AbilitiesLayer::ABILITY_MUTED =>
false,
1121 AbilitiesLayer::ABILITY_WORLD_BUILDER =>
false,
1122 AbilitiesLayer::ABILITY_INFINITE_RESOURCES => !$for->hasFiniteResources(),
1123 AbilitiesLayer::ABILITY_LIGHTNING =>
false,
1124 AbilitiesLayer::ABILITY_BUILD => !$for->isSpectator(),
1125 AbilitiesLayer::ABILITY_MINE => !$for->isSpectator(),
1126 AbilitiesLayer::ABILITY_DOORS_AND_SWITCHES => !$for->isSpectator(),
1127 AbilitiesLayer::ABILITY_OPEN_CONTAINERS => !$for->isSpectator(),
1128 AbilitiesLayer::ABILITY_ATTACK_PLAYERS => !$for->isSpectator(),
1129 AbilitiesLayer::ABILITY_ATTACK_MOBS => !$for->isSpectator(),
1130 AbilitiesLayer::ABILITY_PRIVILEGED_BUILDER =>
false,
1134 new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, $for->getFlightSpeedMultiplier(), 1, 0.1),
1136 if(!$for->hasBlockCollision()){
1142 $layers[] = new AbilitiesLayer(AbilitiesLayer::LAYER_SPECTATOR, [
1143 AbilitiesLayer::ABILITY_FLYING => true,
1144 ], null, null, null);
1147 $this->sendDataPacket(UpdateAbilitiesPacket::create(
new AbilitiesData(
1148 $isOp ? CommandPermissions::OPERATOR : CommandPermissions::NORMAL,
1149 $isOp ? PlayerPermissions::OPERATOR : PlayerPermissions::MEMBER,
1155 public function syncAdventureSettings() : void{
1156 if($this->player === null){
1157 throw new \LogicException(
"Cannot sync adventure settings for a player that is not yet created");
1160 $this->sendDataPacket(UpdateAdventureSettingsPacket::create(
1161 noAttackingMobs:
false,
1162 noAttackingPlayers:
false,
1163 worldImmutable:
false,
1165 autoJump: $this->player->hasAutoJump()
1169 public function syncAvailableCommands() : void{
1171 $globalAliasMap = $this->
server->getCommandMap()->getAliasMap();
1172 $userAliasMap = $this->player->getCommandAliasMap();
1173 foreach($this->
server->getCommandMap()->getUniqueCommands() as $command){
1174 if(!$command->testPermissionSilent($this->player)){
1178 $userAliases = $userAliasMap->getMergedAliases($command->getId(), $globalAliasMap);
1180 $aliases = array_values(array_filter($userAliases, fn(
string $alias) => $alias !==
"help" && $alias !==
"?"));
1181 if(count($aliases) === 0){
1184 $firstNetworkAlias = $aliases[0];
1187 $lname = strtolower($firstNetworkAlias);
1188 $aliasObj =
new CommandHardEnum(ucfirst($firstNetworkAlias) .
"Aliases", $aliases);
1190 $description = $command->getDescription();
1191 $data =
new CommandData(
1193 $description instanceof Translatable ? $this->player->getLanguage()->translate($description) : $description,
1195 CommandPermissions::NORMAL,
1198 new CommandOverload(chaining:
false, parameters: [CommandParameter::standard(
"args", AvailableCommandsPacket::ARG_TYPE_RAWTEXT, 0,
true)])
1200 chainedSubCommandData: []
1203 $commandData[] = $data;
1206 $this->sendDataPacket(AvailableCommandsPacketAssembler::assemble($commandData, [], []));
1215 $language = $this->player->getLanguage();
1217 $untranslatedParameterCount = 0;
1218 $translated = $language->translateString($message->getText(), $parameters,
"pocketmine.", $untranslatedParameterCount);
1219 return [$translated, array_slice($parameters, 0, $untranslatedParameterCount)];
1222 public function onChatMessage(
Translatable|
string $message) : void{
1224 if(!$this->
server->isLanguageForced()){
1225 $this->sendDataPacket(TextPacket::translation(...$this->prepareClientTranslatableMessage($message)));
1227 $this->sendDataPacket(TextPacket::raw($this->player->getLanguage()->translate($message)));
1230 $this->sendDataPacket(TextPacket::raw($message));
1234 public function onJukeboxPopup(Translatable|
string $message) : void{
1236 if($message instanceof Translatable){
1237 if(!$this->server->isLanguageForced()){
1238 [$message, $parameters] = $this->prepareClientTranslatableMessage($message);
1240 $message = $this->player->getLanguage()->translate($message);
1243 $this->sendDataPacket(TextPacket::jukeboxPopup($message, $parameters));
1246 public function onPopup(
string $message) : void{
1247 $this->sendDataPacket(TextPacket::popup($message));
1250 public function onTip(
string $message) : void{
1251 $this->sendDataPacket(TextPacket::tip($message));
1254 public function onFormSent(
int $id, Form $form) : bool{
1255 return $this->sendDataPacket(ModalFormRequestPacket::create($id, json_encode($form, JSON_THROW_ON_ERROR)));
1258 public function onCloseAllForms() : void{
1259 $this->sendDataPacket(ClientboundCloseFormPacket::create());
1265 private function sendChunkPacket(
string $chunkPacket, \Closure $onCompletion, World $world) : void{
1266 $world->timings->syncChunkSend->startTiming();
1268 $this->queueCompressed($chunkPacket);
1271 $world->timings->syncChunkSend->stopTiming();
1281 $world = $this->player->getLocation()->getWorld();
1282 $promiseOrPacket = ChunkCache::getInstance($world, $this->compressor)->request($chunkX, $chunkZ);
1283 if(is_string($promiseOrPacket)){
1284 $this->sendChunkPacket($promiseOrPacket, $onCompletion, $world);
1287 $promiseOrPacket->onResolve(
1290 if(!$this->isConnected()){
1293 $currentWorld = $this->player->getLocation()->getWorld();
1294 if($world !== $currentWorld || ($status = $this->player->getUsedChunkStatus($chunkX, $chunkZ)) ===
null){
1295 $this->logger->debug(
"Tried to send no-longer-active chunk $chunkX $chunkZ in world " . $world->getFolderName());
1298 if($status !== UsedChunkStatus::REQUESTED_SENDING){
1305 $this->sendChunkPacket($promise->getResult(), $onCompletion, $world);
1310 public function stopUsingChunk(
int $chunkX,
int $chunkZ) : void{
1314 public function onEnterWorld() : void{
1315 if($this->player !== null){
1316 $world = $this->player->getWorld();
1317 $this->syncWorldTime($world->getTime());
1318 $this->syncWorldDifficulty($world->getDifficulty());
1319 $this->syncWorldSpawnPoint($world->getSpawnLocation());
1324 public function syncWorldTime(
int $worldTime) : void{
1325 $this->sendDataPacket(SetTimePacket::create($worldTime));
1328 public function syncWorldDifficulty(
int $worldDifficulty) : void{
1329 $this->sendDataPacket(SetDifficultyPacket::create($worldDifficulty));
1332 public function getInvManager() : ?InventoryManager{
1333 return $this->invManager;
1341 return
PlayerListEntry::createAdditionEntry($player->getUniqueId(), $player->getId(), $player->getDisplayName(), $this->typeConverter->getSkinAdapter()->toSkinData($player->getSkin()), $player->getXuid());
1345 public function onPlayerAdded(
Player $p) : void{
1346 $this->sendDataPacket(PlayerListPacket::add([PlayerListEntry::createAdditionEntry($p->getUniqueId(), $p->getId(), $p->getDisplayName(), $this->typeConverter->getSkinAdapter()->toSkinData($p->getSkin()), $p->getXuid())]));
1349 public function onPlayerRemoved(
Player $p) : void{
1350 if($p !== $this->player){
1351 $this->sendDataPacket(PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($p->
getUniqueId())]));
1355 public function onTitle(
string $title) : void{
1356 $this->sendDataPacket(SetTitlePacket::title($title));
1359 public function onSubTitle(
string $subtitle) : void{
1360 $this->sendDataPacket(SetTitlePacket::subtitle($subtitle));
1363 public function onActionBar(
string $actionBar) : void{
1364 $this->sendDataPacket(SetTitlePacket::actionBarMessage($actionBar));
1367 public function onClearTitle() : void{
1368 $this->sendDataPacket(SetTitlePacket::clearTitle());
1371 public function onResetTitleOptions() : void{
1372 $this->sendDataPacket(SetTitlePacket::resetTitleOptions());
1375 public function onTitleDuration(
int $fadeIn,
int $stay,
int $fadeOut) : void{
1376 $this->sendDataPacket(SetTitlePacket::setAnimationTimes($fadeIn, $stay, $fadeOut));
1379 public function onToastNotification(
string $title,
string $body) : void{
1380 $this->sendDataPacket(ToastRequestPacket::create($title, $body));
1383 public function onOpenSignEditor(Vector3 $signPosition,
bool $frontSide) : void{
1384 $this->sendDataPacket(OpenSignPacket::create(BlockPosition::fromVector3($signPosition), $frontSide));
1387 public function onItemCooldownChanged(Item $item,
int $ticks) : void{
1388 $this->sendDataPacket(PlayerStartItemCooldownPacket::create(
1389 GlobalItemDataHandlers::getSerializer()->serializeType($item)->getName(),
1394 public function tick() : void{
1395 if(!$this->isConnected()){
1400 if($this->info ===
null){
1401 if(time() >= $this->connectTime + 10){
1402 $this->disconnectWithError(KnownTranslationFactory::pocketmine_disconnect_error_loginTimeout());
1408 if($this->player !==
null){
1409 $this->player->doChunkRequests();
1411 $dirtyAttributes = $this->player->getAttributeMap()->needSend();
1412 $this->entityEventBroadcaster->syncAttributes([$this], $this->player, $dirtyAttributes);
1413 foreach($dirtyAttributes as $attribute){
1416 $attribute->markSynchronized();
1419 Timings::$playerNetworkSendInventorySync->startTiming();
1421 $this->invManager?->flushPendingUpdates();
1423 Timings::$playerNetworkSendInventorySync->stopTiming();
1426 $this->flushGamePacketQueue();