PocketMine-MP 5.32.2 git-237b304ef9858756b018e44e8f298093f66f823b
Loading...
Searching...
No Matches
NetworkSession.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\network\mcpe;
25
102use pocketmine\player\GameMode;
105use pocketmine\player\UsedChunkStatus;
120use function array_map;
121use function base64_encode;
122use function bin2hex;
123use function count;
124use function get_class;
125use function implode;
126use function in_array;
127use function is_string;
128use function json_encode;
129use function ord;
130use function random_bytes;
131use function str_split;
132use function strcasecmp;
133use function strlen;
134use function strtolower;
135use function substr;
136use function time;
137use function ucfirst;
138use const JSON_THROW_ON_ERROR;
139
141 private const INCOMING_PACKET_BATCH_PER_TICK = 2; //usually max 1 per tick, but transactions arrive separately
142 private const INCOMING_PACKET_BATCH_BUFFER_TICKS = 100; //enough to account for a 5-second lag spike
143
144 private const INCOMING_GAME_PACKETS_PER_TICK = 2;
145 private const INCOMING_GAME_PACKETS_BUFFER_TICKS = 100;
146
147 private PacketRateLimiter $packetBatchLimiter;
148 private PacketRateLimiter $gamePacketLimiter;
149
150 private \PrefixedLogger $logger;
151 private ?Player $player = null;
152 private ?PlayerInfo $info = null;
153 private ?int $ping = null;
154
155 private ?PacketHandler $handler = null;
156
157 private bool $connected = true;
158 private bool $disconnectGuard = false;
159 private bool $loggedIn = false;
160 private bool $authenticated = false;
161 private int $connectTime;
162 private ?CompoundTag $cachedOfflinePlayerData = null;
163
164 private ?EncryptionContext $cipher = null;
165
170 private array $sendBuffer = [];
175 private array $sendBufferAckPromises = [];
176
178 private \SplQueue $compressedQueue;
179 private bool $forceAsyncCompression = true;
180 private bool $enableCompression = false; //disabled until handshake completed
181
182 private int $nextAckReceiptId = 0;
187 private array $ackPromisesByReceiptId = [];
188
189 private ?InventoryManager $invManager = null;
190
195 private ObjectSet $disposeHooks;
196
197 public function __construct(
198 private Server $server,
199 private NetworkSessionManager $manager,
200 private PacketPool $packetPool,
201 private PacketSender $sender,
202 private PacketBroadcaster $broadcaster,
203 private EntityEventBroadcaster $entityEventBroadcaster,
204 private Compressor $compressor,
205 private TypeConverter $typeConverter,
206 private string $ip,
207 private int $port
208 ){
209 $this->logger = new \PrefixedLogger($this->server->getLogger(), $this->getLogPrefix());
210
211 $this->compressedQueue = new \SplQueue();
212
213 $this->disposeHooks = new ObjectSet();
214
215 $this->connectTime = time();
216 $this->packetBatchLimiter = new PacketRateLimiter("Packet Batches", self::INCOMING_PACKET_BATCH_PER_TICK, self::INCOMING_PACKET_BATCH_BUFFER_TICKS);
217 $this->gamePacketLimiter = new PacketRateLimiter("Game Packets", self::INCOMING_GAME_PACKETS_PER_TICK, self::INCOMING_GAME_PACKETS_BUFFER_TICKS);
218
219 $this->setHandler(new SessionStartPacketHandler(
220 $this,
221 $this->onSessionStartSuccess(...)
222 ));
223
224 $this->manager->add($this);
225 $this->logger->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_open()));
226 }
227
228 private function getLogPrefix() : string{
229 return "NetworkSession: " . $this->getDisplayName();
230 }
231
232 public function getLogger() : \Logger{
233 return $this->logger;
234 }
235
236 private function onSessionStartSuccess() : void{
237 $this->logger->debug("Session start handshake completed, awaiting login packet");
238 $this->flushGamePacketQueue();
239 $this->enableCompression = true;
240 $this->setHandler(new LoginPacketHandler(
241 $this->server,
242 $this,
243 function(PlayerInfo $info) : void{
244 $this->info = $info;
245 $this->logger->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_playerName(TextFormat::AQUA . $info->getUsername() . TextFormat::RESET)));
246 $this->logger->setPrefix($this->getLogPrefix());
247 $this->manager->markLoginReceived($this);
248 },
249 $this->setAuthenticationStatus(...)
250 ));
251 }
252
253 protected function createPlayer() : void{
254 $this->server->createPlayer($this, $this->info, $this->authenticated, $this->cachedOfflinePlayerData)->onCompletion(
255 $this->onPlayerCreated(...),
256 function() : void{
257 //TODO: this should never actually occur... right?
258 $this->disconnectWithError(
259 reason: "Failed to create player",
260 disconnectScreenMessage: KnownTranslationFactory::pocketmine_disconnect_error_internal()
261 );
262 }
263 );
264 }
265
266 private function onPlayerCreated(Player $player) : void{
267 if(!$this->isConnected()){
268 //the remote player might have disconnected before spawn terrain generation was finished
269 return;
270 }
271 $this->player = $player;
272 if(!$this->server->addOnlinePlayer($player)){
273 return;
274 }
275
276 $this->invManager = new InventoryManager($this->player, $this);
277
278 $effectManager = $this->player->getEffects();
279 $effectManager->getEffectAddHooks()->add($effectAddHook = function(EffectInstance $effect, bool $replacesOldEffect) : void{
280 $this->entityEventBroadcaster->onEntityEffectAdded([$this], $this->player, $effect, $replacesOldEffect);
281 });
282 $effectManager->getEffectRemoveHooks()->add($effectRemoveHook = function(EffectInstance $effect) : void{
283 $this->entityEventBroadcaster->onEntityEffectRemoved([$this], $this->player, $effect);
284 });
285 $this->disposeHooks->add(static function() use ($effectManager, $effectAddHook, $effectRemoveHook) : void{
286 $effectManager->getEffectAddHooks()->remove($effectAddHook);
287 $effectManager->getEffectRemoveHooks()->remove($effectRemoveHook);
288 });
289
290 $permissionHooks = $this->player->getPermissionRecalculationCallbacks();
291 $permissionHooks->add($permHook = function() : void{
292 $this->logger->debug("Syncing available commands and abilities/permissions due to permission recalculation");
293 $this->syncAbilities($this->player);
294 $this->syncAvailableCommands();
295 });
296 $this->disposeHooks->add(static function() use ($permissionHooks, $permHook) : void{
297 $permissionHooks->remove($permHook);
298 });
299 $this->beginSpawnSequence();
300 }
301
302 public function getPlayer() : ?Player{
303 return $this->player;
304 }
305
306 public function getPlayerInfo() : ?PlayerInfo{
307 return $this->info;
308 }
309
310 public function isConnected() : bool{
311 return $this->connected && !$this->disconnectGuard;
312 }
313
314 public function getIp() : string{
315 return $this->ip;
316 }
317
318 public function getPort() : int{
319 return $this->port;
320 }
321
322 public function getDisplayName() : string{
323 return $this->info !== null ? $this->info->getUsername() : $this->ip . " " . $this->port;
324 }
325
329 public function getPing() : ?int{
330 return $this->ping;
331 }
332
336 public function updatePing(int $ping) : void{
337 $this->ping = $ping;
338 }
339
340 public function getHandler() : ?PacketHandler{
341 return $this->handler;
342 }
343
344 public function setHandler(?PacketHandler $handler) : void{
345 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
346 $this->handler = $handler;
347 if($this->handler !== null){
348 $this->handler->setUp();
349 }
350 }
351 }
352
356 public function handleEncoded(string $payload) : void{
357 if(!$this->connected){
358 return;
359 }
360
361 Timings::$playerNetworkReceive->startTiming();
362 try{
363 $this->packetBatchLimiter->decrement();
364
365 if($this->cipher !== null){
366 Timings::$playerNetworkReceiveDecrypt->startTiming();
367 try{
368 $payload = $this->cipher->decrypt($payload);
369 }catch(DecryptionException $e){
370 $this->logger->debug("Encrypted packet: " . base64_encode($payload));
371 throw PacketHandlingException::wrap($e, "Packet decryption error");
372 }finally{
373 Timings::$playerNetworkReceiveDecrypt->stopTiming();
374 }
375 }
376
377 if(strlen($payload) < 1){
378 throw new PacketHandlingException("No bytes in payload");
379 }
380
381 if($this->enableCompression){
382 $compressionType = ord($payload[0]);
383 $compressed = substr($payload, 1);
384 if($compressionType === CompressionAlgorithm::NONE){
385 $decompressed = $compressed;
386 }elseif($compressionType === $this->compressor->getNetworkId()){
387 Timings::$playerNetworkReceiveDecompress->startTiming();
388 try{
389 $decompressed = $this->compressor->decompress($compressed);
390 }catch(DecompressionException $e){
391 $this->logger->debug("Failed to decompress packet: " . base64_encode($compressed));
392 throw PacketHandlingException::wrap($e, "Compressed packet batch decode error");
393 }finally{
394 Timings::$playerNetworkReceiveDecompress->stopTiming();
395 }
396 }else{
397 throw new PacketHandlingException("Packet compressed with unexpected compression type $compressionType");
398 }
399 }else{
400 $decompressed = $payload;
401 }
402
403 try{
404 $stream = new BinaryStream($decompressed);
405 foreach(PacketBatch::decodeRaw($stream) as $buffer){
406 $this->gamePacketLimiter->decrement();
407 $packet = $this->packetPool->getPacket($buffer);
408 if($packet === null){
409 $this->logger->debug("Unknown packet: " . base64_encode($buffer));
410 throw new PacketHandlingException("Unknown packet received");
411 }
412 try{
413 $this->handleDataPacket($packet, $buffer);
414 }catch(PacketHandlingException $e){
415 $this->logger->debug($packet->getName() . ": " . base64_encode($buffer));
416 throw PacketHandlingException::wrap($e, "Error processing " . $packet->getName());
417 }
418 if(!$this->isConnected()){
419 //handling this packet may have caused a disconnection
420 $this->logger->debug("Aborting batch processing due to server-side disconnection");
421 break;
422 }
423 }
424 }catch(PacketDecodeException|BinaryDataException $e){
425 $this->logger->logException($e);
426 throw PacketHandlingException::wrap($e, "Packet batch decode error");
427 }
428 }finally{
429 Timings::$playerNetworkReceive->stopTiming();
430 }
431 }
432
436 public function handleDataPacket(Packet $packet, string $buffer) : void{
437 if(!($packet instanceof ServerboundPacket)){
438 throw new PacketHandlingException("Unexpected non-serverbound packet");
439 }
440
441 $timings = Timings::getReceiveDataPacketTimings($packet);
442 $timings->startTiming();
443
444 try{
445 if(DataPacketDecodeEvent::hasHandlers()){
446 $ev = new DataPacketDecodeEvent($this, $packet->pid(), $buffer);
447 $ev->call();
448 if($ev->isCancelled()){
449 return;
450 }
451 }
452
453 $decodeTimings = Timings::getDecodeDataPacketTimings($packet);
454 $decodeTimings->startTiming();
455 try{
456 $stream = PacketSerializer::decoder($buffer, 0);
457 try{
458 $packet->decode($stream);
459 }catch(PacketDecodeException $e){
460 throw PacketHandlingException::wrap($e);
461 }
462 if(!$stream->feof()){
463 $remains = substr($stream->getBuffer(), $stream->getOffset());
464 $this->logger->debug("Still " . strlen($remains) . " bytes unread in " . $packet->getName() . ": " . bin2hex($remains));
465 }
466 }finally{
467 $decodeTimings->stopTiming();
468 }
469
470 if(DataPacketReceiveEvent::hasHandlers()){
471 $ev = new DataPacketReceiveEvent($this, $packet);
472 $ev->call();
473 if($ev->isCancelled()){
474 return;
475 }
476 }
477 $handlerTimings = Timings::getHandleDataPacketTimings($packet);
478 $handlerTimings->startTiming();
479 try{
480 if($this->handler === null || !$packet->handle($this->handler)){
481 $this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getBuffer()));
482 }
483 }finally{
484 $handlerTimings->stopTiming();
485 }
486 }finally{
487 $timings->stopTiming();
488 }
489 }
490
491 public function handleAckReceipt(int $receiptId) : void{
492 if(!$this->connected){
493 return;
494 }
495 if(isset($this->ackPromisesByReceiptId[$receiptId])){
496 $promises = $this->ackPromisesByReceiptId[$receiptId];
497 unset($this->ackPromisesByReceiptId[$receiptId]);
498 foreach($promises as $promise){
499 $promise->resolve(true);
500 }
501 }
502 }
503
507 private function sendDataPacketInternal(ClientboundPacket $packet, bool $immediate, ?PromiseResolver $ackReceiptResolver) : bool{
508 if(!$this->connected){
509 return false;
510 }
511 //Basic safety restriction. TODO: improve this
512 if(!$this->loggedIn && !$packet->canBeSentBeforeLogin()){
513 throw new \InvalidArgumentException("Attempted to send " . get_class($packet) . " to " . $this->getDisplayName() . " too early");
514 }
515
516 $timings = Timings::getSendDataPacketTimings($packet);
517 $timings->startTiming();
518 try{
519 if(DataPacketSendEvent::hasHandlers()){
520 $ev = new DataPacketSendEvent([$this], [$packet]);
521 $ev->call();
522 if($ev->isCancelled()){
523 return false;
524 }
525 $packets = $ev->getPackets();
526 }else{
527 $packets = [$packet];
528 }
529
530 if($ackReceiptResolver !== null){
531 $this->sendBufferAckPromises[] = $ackReceiptResolver;
532 }
533 foreach($packets as $evPacket){
534 $this->addToSendBuffer(self::encodePacketTimed(PacketSerializer::encoder(), $evPacket));
535 }
536 if($immediate){
537 $this->flushGamePacketQueue();
538 }
539
540 return true;
541 }finally{
542 $timings->stopTiming();
543 }
544 }
545
546 public function sendDataPacket(ClientboundPacket $packet, bool $immediate = false) : bool{
547 return $this->sendDataPacketInternal($packet, $immediate, null);
548 }
549
553 public function sendDataPacketWithReceipt(ClientboundPacket $packet, bool $immediate = false) : Promise{
555 $resolver = new PromiseResolver();
556
557 if(!$this->sendDataPacketInternal($packet, $immediate, $resolver)){
558 $resolver->reject();
559 }
560
561 return $resolver->getPromise();
562 }
563
567 public static function encodePacketTimed(PacketSerializer $serializer, ClientboundPacket $packet) : string{
568 $timings = Timings::getEncodeDataPacketTimings($packet);
569 $timings->startTiming();
570 try{
571 $packet->encode($serializer);
572 return $serializer->getBuffer();
573 }finally{
574 $timings->stopTiming();
575 }
576 }
577
581 public function addToSendBuffer(string $buffer) : void{
582 $this->sendBuffer[] = $buffer;
583 }
584
585 private function flushGamePacketQueue() : void{
586 if(count($this->sendBuffer) > 0){
587 Timings::$playerNetworkSend->startTiming();
588 try{
589 $syncMode = null; //automatic
590 if($this->forceAsyncCompression){
591 $syncMode = false;
592 }
593
594 $stream = new BinaryStream();
595 PacketBatch::encodeRaw($stream, $this->sendBuffer);
596
597 if($this->enableCompression){
598 $batch = $this->server->prepareBatch($stream->getBuffer(), $this->compressor, $syncMode, Timings::$playerNetworkSendCompressSessionBuffer);
599 }else{
600 $batch = $stream->getBuffer();
601 }
602 $this->sendBuffer = [];
603 $ackPromises = $this->sendBufferAckPromises;
604 $this->sendBufferAckPromises = [];
605 //these packets were already potentially buffered for up to 50ms - make sure the transport layer doesn't
606 //delay them any longer
607 $this->queueCompressedNoGamePacketFlush($batch, networkFlush: true, ackPromises: $ackPromises);
608 }finally{
609 Timings::$playerNetworkSend->stopTiming();
610 }
611 }
612 }
613
614 public function getBroadcaster() : PacketBroadcaster{ return $this->broadcaster; }
615
616 public function getEntityEventBroadcaster() : EntityEventBroadcaster{ return $this->entityEventBroadcaster; }
617
618 public function getCompressor() : Compressor{
619 return $this->compressor;
620 }
621
622 public function getTypeConverter() : TypeConverter{ return $this->typeConverter; }
623
624 public function queueCompressed(CompressBatchPromise|string $payload, bool $immediate = false) : void{
625 Timings::$playerNetworkSend->startTiming();
626 try{
627 //if the next packet causes a flush, avoid unnecessarily flushing twice
628 //however, if the next packet does *not* cause a flush, game packets should be flushed to avoid delays
629 $this->flushGamePacketQueue();
630 $this->queueCompressedNoGamePacketFlush($payload, $immediate);
631 }finally{
632 Timings::$playerNetworkSend->stopTiming();
633 }
634 }
635
641 private function queueCompressedNoGamePacketFlush(CompressBatchPromise|string $batch, bool $networkFlush = false, array $ackPromises = []) : void{
642 Timings::$playerNetworkSend->startTiming();
643 try{
644 $this->compressedQueue->enqueue([$batch, $ackPromises, $networkFlush]);
645 if(is_string($batch)){
646 $this->flushCompressedQueue();
647 }else{
648 $batch->onResolve(function() : void{
649 if($this->connected){
650 $this->flushCompressedQueue();
651 }
652 });
653 }
654 }finally{
655 Timings::$playerNetworkSend->stopTiming();
656 }
657 }
658
659 private function flushCompressedQueue() : void{
660 Timings::$playerNetworkSend->startTiming();
661 try{
662 while(!$this->compressedQueue->isEmpty()){
664 [$current, $ackPromises, $networkFlush] = $this->compressedQueue->bottom();
665 if(is_string($current)){
666 $this->compressedQueue->dequeue();
667 $this->sendEncoded($current, $networkFlush, $ackPromises);
668
669 }elseif($current->hasResult()){
670 $this->compressedQueue->dequeue();
671 $this->sendEncoded($current->getResult(), $networkFlush, $ackPromises);
672
673 }else{
674 //can't send any more queued until this one is ready
675 break;
676 }
677 }
678 }finally{
679 Timings::$playerNetworkSend->stopTiming();
680 }
681 }
682
687 private function sendEncoded(string $payload, bool $immediate, array $ackPromises) : void{
688 if($this->cipher !== null){
689 Timings::$playerNetworkSendEncrypt->startTiming();
690 $payload = $this->cipher->encrypt($payload);
691 Timings::$playerNetworkSendEncrypt->stopTiming();
692 }
693
694 if(count($ackPromises) > 0){
695 $ackReceiptId = $this->nextAckReceiptId++;
696 $this->ackPromisesByReceiptId[$ackReceiptId] = $ackPromises;
697 }else{
698 $ackReceiptId = null;
699 }
700 $this->sender->send($payload, $immediate, $ackReceiptId);
701 }
702
706 private function tryDisconnect(\Closure $func, Translatable|string $reason) : void{
707 if($this->connected && !$this->disconnectGuard){
708 $this->disconnectGuard = true;
709 $func();
710 $this->disconnectGuard = false;
711 $this->flushGamePacketQueue();
712 $this->sender->close("");
713 foreach($this->disposeHooks as $callback){
714 $callback();
715 }
716 $this->disposeHooks->clear();
717 $this->setHandler(null);
718 $this->connected = false;
719
720 $ackPromisesByReceiptId = $this->ackPromisesByReceiptId;
721 $this->ackPromisesByReceiptId = [];
722 foreach($ackPromisesByReceiptId as $resolvers){
723 foreach($resolvers as $resolver){
724 $resolver->reject();
725 }
726 }
727 $sendBufferAckPromises = $this->sendBufferAckPromises;
728 $this->sendBufferAckPromises = [];
729 foreach($sendBufferAckPromises as $resolver){
730 $resolver->reject();
731 }
732
733 $this->logger->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_close($reason)));
734 }
735 }
736
741 private function dispose() : void{
742 $this->invManager = null;
743 }
744
745 private function sendDisconnectPacket(Translatable|string $message) : void{
746 if($message instanceof Translatable){
747 $translated = $this->server->getLanguage()->translate($message);
748 }else{
749 $translated = $message;
750 }
751 $this->sendDataPacket(DisconnectPacket::create(0, $translated, ""));
752 }
753
760 public function disconnect(Translatable|string $reason, Translatable|string|null $disconnectScreenMessage = null, bool $notify = true) : void{
761 $this->tryDisconnect(function() use ($reason, $disconnectScreenMessage, $notify) : void{
762 if($notify){
763 $this->sendDisconnectPacket($disconnectScreenMessage ?? $reason);
764 }
765 if($this->player !== null){
766 $this->player->onPostDisconnect($reason, null);
767 }
768 }, $reason);
769 }
770
771 public function disconnectWithError(Translatable|string $reason, Translatable|string|null $disconnectScreenMessage = null) : void{
772 $errorId = implode("-", str_split(bin2hex(random_bytes(6)), 4));
773
774 $this->disconnect(
775 reason: KnownTranslationFactory::pocketmine_disconnect_error($reason, $errorId)->prefix(TextFormat::RED),
776 disconnectScreenMessage: KnownTranslationFactory::pocketmine_disconnect_error($disconnectScreenMessage ?? $reason, $errorId),
777 );
778 }
779
780 public function disconnectIncompatibleProtocol(int $protocolVersion) : void{
781 $this->tryDisconnect(
782 function() use ($protocolVersion) : void{
783 $this->sendDataPacket(PlayStatusPacket::create($protocolVersion < ProtocolInfo::CURRENT_PROTOCOL ? PlayStatusPacket::LOGIN_FAILED_CLIENT : PlayStatusPacket::LOGIN_FAILED_SERVER), true);
784 },
785 KnownTranslationFactory::pocketmine_disconnect_incompatibleProtocol((string) $protocolVersion)
786 );
787 }
788
792 public function transfer(string $ip, int $port, Translatable|string|null $reason = null) : void{
793 $reason ??= KnownTranslationFactory::pocketmine_disconnect_transfer();
794 $this->tryDisconnect(function() use ($ip, $port, $reason) : void{
795 $this->sendDataPacket(TransferPacket::create($ip, $port, false), true);
796 if($this->player !== null){
797 $this->player->onPostDisconnect($reason, null);
798 }
799 }, $reason);
800 }
801
805 public function onPlayerDestroyed(Translatable|string $reason, Translatable|string $disconnectScreenMessage) : void{
806 $this->tryDisconnect(function() use ($disconnectScreenMessage) : void{
807 $this->sendDisconnectPacket($disconnectScreenMessage);
808 }, $reason);
809 }
810
815 public function onClientDisconnect(Translatable|string $reason) : void{
816 $this->tryDisconnect(function() use ($reason) : void{
817 if($this->player !== null){
818 $this->player->onPostDisconnect($reason, null);
819 }
820 }, $reason);
821 }
822
823 private function setAuthenticationStatus(bool $authenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPubKey) : void{
824 if(!$this->connected){
825 return;
826 }
827 if($error === null){
828 if($authenticated && !($this->info instanceof XboxLivePlayerInfo)){
829 $error = "Expected XUID but none found";
830 }elseif($clientPubKey === null){
831 $error = "Missing client public key"; //failsafe
832 }
833 }
834
835 if($error !== null){
836 $this->disconnectWithError(
837 reason: KnownTranslationFactory::pocketmine_disconnect_invalidSession($error),
838 disconnectScreenMessage: KnownTranslationFactory::pocketmine_disconnect_error_authentication()
839 );
840
841 return;
842 }
843
844 $this->authenticated = $authenticated;
845
846 if(!$this->authenticated){
847 if($authRequired){
848 $this->disconnect("Not authenticated", KnownTranslationFactory::disconnectionScreen_notAuthenticated());
849 return;
850 }
851 if($this->info instanceof XboxLivePlayerInfo){
852 $this->logger->warning("Discarding unexpected XUID for non-authenticated player");
853 $this->info = $this->info->withoutXboxData();
854 }
855 }
856 $this->logger->debug("Xbox Live authenticated: " . ($this->authenticated ? "YES" : "NO"));
857
858 $checkXUID = $this->server->getConfigGroup()->getPropertyBool(YmlServerProperties::PLAYER_VERIFY_XUID, true);
859 $myXUID = $this->info instanceof XboxLivePlayerInfo ? $this->info->getXuid() : "";
860 $kickForXUIDMismatch = function(string $xuid) use ($checkXUID, $myXUID) : bool{
861 if($checkXUID && $myXUID !== $xuid){
862 $this->logger->debug("XUID mismatch: expected '$xuid', but got '$myXUID'");
863 //TODO: Longer term, we should be identifying playerdata using something more reliable, like XUID or UUID.
864 //However, that would be a very disruptive change, so this will serve as a stopgap for now.
865 //Side note: this will also prevent offline players hijacking XBL playerdata on online servers, since their
866 //XUID will always be empty.
867 $this->disconnect("XUID does not match (possible impersonation attempt)");
868 return true;
869 }
870 return false;
871 };
872
873 foreach($this->manager->getSessions() as $existingSession){
874 if($existingSession === $this){
875 continue;
876 }
877 $info = $existingSession->getPlayerInfo();
878 if($info !== null && (strcasecmp($info->getUsername(), $this->info->getUsername()) === 0 || $info->getUuid()->equals($this->info->getUuid()))){
879 if($kickForXUIDMismatch($info instanceof XboxLivePlayerInfo ? $info->getXuid() : "")){
880 return;
881 }
882 $ev = new PlayerDuplicateLoginEvent($this, $existingSession, KnownTranslationFactory::disconnectionScreen_loggedinOtherLocation(), null);
883 $ev->call();
884 if($ev->isCancelled()){
885 $this->disconnect($ev->getDisconnectReason(), $ev->getDisconnectScreenMessage());
886 return;
887 }
888
889 $existingSession->disconnect($ev->getDisconnectReason(), $ev->getDisconnectScreenMessage());
890 }
891 }
892
893 //TODO: make player data loading async
894 //TODO: we shouldn't be loading player data here at all, but right now we don't have any choice :(
895 $this->cachedOfflinePlayerData = $this->server->getOfflinePlayerData($this->info->getUsername());
896 if($checkXUID){
897 $recordedXUID = $this->cachedOfflinePlayerData !== null ? $this->cachedOfflinePlayerData->getTag(Player::TAG_LAST_KNOWN_XUID) : null;
898 if(!($recordedXUID instanceof StringTag)){
899 $this->logger->debug("No previous XUID recorded, no choice but to trust this player");
900 }elseif(!$kickForXUIDMismatch($recordedXUID->getValue())){
901 $this->logger->debug("XUID match");
902 }
903 }
904
905 if(EncryptionContext::$ENABLED){
906 $this->server->getAsyncPool()->submitTask(new PrepareEncryptionTask($clientPubKey, function(string $encryptionKey, string $handshakeJwt) : void{
907 if(!$this->connected){
908 return;
909 }
910 $this->sendDataPacket(ServerToClientHandshakePacket::create($handshakeJwt), true); //make sure this gets sent before encryption is enabled
911
912 $this->cipher = EncryptionContext::fakeGCM($encryptionKey);
913
914 $this->setHandler(new HandshakePacketHandler($this->onServerLoginSuccess(...)));
915 $this->logger->debug("Enabled encryption");
916 }));
917 }else{
918 $this->onServerLoginSuccess();
919 }
920 }
921
922 private function onServerLoginSuccess() : void{
923 $this->loggedIn = true;
924
925 $this->sendDataPacket(PlayStatusPacket::create(PlayStatusPacket::LOGIN_SUCCESS));
926
927 $this->logger->debug("Initiating resource packs phase");
928
929 $packManager = $this->server->getResourcePackManager();
930 $resourcePacks = $packManager->getResourceStack();
931 $keys = [];
932 foreach($resourcePacks as $resourcePack){
933 $key = $packManager->getPackEncryptionKey($resourcePack->getPackId());
934 if($key !== null){
935 $keys[$resourcePack->getPackId()] = $key;
936 }
937 }
938 $event = new PlayerResourcePackOfferEvent($this->info, $resourcePacks, $keys, $packManager->resourcePacksRequired());
939 $event->call();
940 $this->setHandler(new ResourcePacksPacketHandler($this, $event->getResourcePacks(), $event->getEncryptionKeys(), $event->mustAccept(), function() : void{
941 $this->createPlayer();
942 }));
943 }
944
945 private function beginSpawnSequence() : void{
946 $this->setHandler(new PreSpawnPacketHandler($this->server, $this->player, $this, $this->invManager));
947 $this->player->setNoClientPredictions(); //TODO: HACK: fix client-side falling pre-spawn
948
949 $this->logger->debug("Waiting for chunk radius request");
950 }
951
952 public function notifyTerrainReady() : void{
953 $this->logger->debug("Sending spawn notification, waiting for spawn response");
954 $this->sendDataPacket(PlayStatusPacket::create(PlayStatusPacket::PLAYER_SPAWN));
955 $this->setHandler(new SpawnResponsePacketHandler($this->onClientSpawnResponse(...)));
956 }
957
958 private function onClientSpawnResponse() : void{
959 $this->logger->debug("Received spawn response, entering in-game phase");
960 $this->player->setNoClientPredictions(false); //TODO: HACK: we set this during the spawn sequence to prevent the client sending junk movements
961 $this->player->doFirstSpawn();
962 $this->forceAsyncCompression = false;
963 $this->setHandler(new InGamePacketHandler($this->player, $this, $this->invManager));
964 }
965
966 public function onServerDeath(Translatable|string $deathMessage) : void{
967 if($this->handler instanceof InGamePacketHandler){ //TODO: this is a bad fix for pre-spawn death, this shouldn't be reachable at all at this stage :(
968 $this->setHandler(new DeathPacketHandler($this->player, $this, $this->invManager ?? throw new AssumptionFailedError(), $deathMessage));
969 }
970 }
971
972 public function onServerRespawn() : void{
973 $this->entityEventBroadcaster->syncAttributes([$this], $this->player, $this->player->getAttributeMap()->getAll());
974 $this->player->sendData(null);
975
976 $this->syncAbilities($this->player);
977 $this->invManager->syncAll();
978 $this->setHandler(new InGamePacketHandler($this->player, $this, $this->invManager));
979 }
980
981 public function syncMovement(Vector3 $pos, ?float $yaw = null, ?float $pitch = null, int $mode = MovePlayerPacket::MODE_NORMAL) : void{
982 if($this->player !== null){
983 $location = $this->player->getLocation();
984 $yaw = $yaw ?? $location->getYaw();
985 $pitch = $pitch ?? $location->getPitch();
986
987 $this->sendDataPacket(MovePlayerPacket::simple(
988 $this->player->getId(),
989 $this->player->getOffsetPosition($pos),
990 $pitch,
991 $yaw,
992 $yaw, //TODO: head yaw
993 $mode,
994 $this->player->onGround,
995 0, //TODO: riding entity ID
996 0 //TODO: tick
997 ));
998
999 if($this->handler instanceof InGamePacketHandler){
1000 $this->handler->forceMoveSync = true;
1001 }
1002 }
1003 }
1004
1005 public function syncViewAreaRadius(int $distance) : void{
1006 $this->sendDataPacket(ChunkRadiusUpdatedPacket::create($distance));
1007 }
1008
1009 public function syncViewAreaCenterPoint(Vector3 $newPos, int $viewDistance) : void{
1010 $this->sendDataPacket(NetworkChunkPublisherUpdatePacket::create(BlockPosition::fromVector3($newPos), $viewDistance * 16, [])); //blocks, not chunks >.>
1011 }
1012
1013 public function syncPlayerSpawnPoint(Position $newSpawn) : void{
1014 $newSpawnBlockPosition = BlockPosition::fromVector3($newSpawn);
1015 //TODO: respawn causing block position (bed, respawn anchor)
1016 $this->sendDataPacket(SetSpawnPositionPacket::playerSpawn($newSpawnBlockPosition, DimensionIds::OVERWORLD, $newSpawnBlockPosition));
1017 }
1018
1019 public function syncWorldSpawnPoint(Position $newSpawn) : void{
1020 $this->sendDataPacket(SetSpawnPositionPacket::worldSpawn(BlockPosition::fromVector3($newSpawn), DimensionIds::OVERWORLD));
1021 }
1022
1023 public function syncGameMode(GameMode $mode, bool $isRollback = false) : void{
1024 $this->sendDataPacket(SetPlayerGameTypePacket::create($this->typeConverter->coreGameModeToProtocol($mode)));
1025 if($this->player !== null){
1026 $this->syncAbilities($this->player);
1027 $this->syncAdventureSettings(); //TODO: we might be able to do this with the abilities packet alone
1028 }
1029 if(!$isRollback && $this->invManager !== null){
1030 $this->invManager->syncCreative();
1031 }
1032 }
1033
1034 public function syncAbilities(Player $for) : void{
1035 $isOp = $for->hasPermission(DefaultPermissions::ROOT_OPERATOR);
1036
1037 //ALL of these need to be set for the base layer, otherwise the client will cry
1038 $boolAbilities = [
1039 AbilitiesLayer::ABILITY_ALLOW_FLIGHT => $for->getAllowFlight(),
1040 AbilitiesLayer::ABILITY_FLYING => $for->isFlying(),
1041 AbilitiesLayer::ABILITY_NO_CLIP => !$for->hasBlockCollision(),
1042 AbilitiesLayer::ABILITY_OPERATOR => $isOp,
1043 AbilitiesLayer::ABILITY_TELEPORT => $for->hasPermission(DefaultPermissionNames::COMMAND_TELEPORT_SELF),
1044 AbilitiesLayer::ABILITY_INVULNERABLE => $for->isCreative(),
1045 AbilitiesLayer::ABILITY_MUTED => false,
1046 AbilitiesLayer::ABILITY_WORLD_BUILDER => false,
1047 AbilitiesLayer::ABILITY_INFINITE_RESOURCES => !$for->hasFiniteResources(),
1048 AbilitiesLayer::ABILITY_LIGHTNING => false,
1049 AbilitiesLayer::ABILITY_BUILD => !$for->isSpectator(),
1050 AbilitiesLayer::ABILITY_MINE => !$for->isSpectator(),
1051 AbilitiesLayer::ABILITY_DOORS_AND_SWITCHES => !$for->isSpectator(),
1052 AbilitiesLayer::ABILITY_OPEN_CONTAINERS => !$for->isSpectator(),
1053 AbilitiesLayer::ABILITY_ATTACK_PLAYERS => !$for->isSpectator(),
1054 AbilitiesLayer::ABILITY_ATTACK_MOBS => !$for->isSpectator(),
1055 AbilitiesLayer::ABILITY_PRIVILEGED_BUILDER => false,
1056 ];
1057
1058 $layers = [
1059 new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, $for->getFlightSpeedMultiplier(), 1, 0.1),
1060 ];
1061 if(!$for->hasBlockCollision()){
1062 //TODO: HACK! In 1.19.80, the client starts falling in our faux spectator mode when it clips into a
1063 //block. We can't seem to prevent this short of forcing the player to always fly when block collision is
1064 //disabled. Also, for some reason the client always reads flight state from this layer if present, even
1065 //though the player isn't in spectator mode.
1066
1067 $layers[] = new AbilitiesLayer(AbilitiesLayer::LAYER_SPECTATOR, [
1068 AbilitiesLayer::ABILITY_FLYING => true,
1069 ], null, null, null);
1070 }
1071
1072 $this->sendDataPacket(UpdateAbilitiesPacket::create(new AbilitiesData(
1073 $isOp ? CommandPermissions::OPERATOR : CommandPermissions::NORMAL,
1074 $isOp ? PlayerPermissions::OPERATOR : PlayerPermissions::MEMBER,
1075 $for->getId(),
1076 $layers
1077 )));
1078 }
1079
1080 public function syncAdventureSettings() : void{
1081 if($this->player === null){
1082 throw new \LogicException("Cannot sync adventure settings for a player that is not yet created");
1083 }
1084 //everything except auto jump is handled via UpdateAbilitiesPacket
1085 $this->sendDataPacket(UpdateAdventureSettingsPacket::create(
1086 noAttackingMobs: false,
1087 noAttackingPlayers: false,
1088 worldImmutable: false,
1089 showNameTags: true,
1090 autoJump: $this->player->hasAutoJump()
1091 ));
1092 }
1093
1094 public function syncAvailableCommands() : void{
1095 $commandData = [];
1096 foreach($this->server->getCommandMap()->getCommands() as $command){
1097 if(isset($commandData[$command->getLabel()]) || $command->getLabel() === "help" || !$command->testPermissionSilent($this->player)){
1098 continue;
1099 }
1100
1101 $lname = strtolower($command->getLabel());
1102 $aliases = $command->getAliases();
1103 $aliasObj = null;
1104 if(count($aliases) > 0){
1105 if(!in_array($lname, $aliases, true)){
1106 //work around a client bug which makes the original name not show when aliases are used
1107 $aliases[] = $lname;
1108 }
1109 $aliasObj = new CommandEnum(ucfirst($command->getLabel()) . "Aliases", $aliases);
1110 }
1111
1112 $description = $command->getDescription();
1113 $data = new CommandData(
1114 $lname, //TODO: commands containing uppercase letters in the name crash 1.9.0 client
1115 $description instanceof Translatable ? $this->player->getLanguage()->translate($description) : $description,
1116 0,
1117 0,
1118 $aliasObj,
1119 [
1120 new CommandOverload(chaining: false, parameters: [CommandParameter::standard("args", AvailableCommandsPacket::ARG_TYPE_RAWTEXT, 0, true)])
1121 ],
1122 chainedSubCommandData: []
1123 );
1124
1125 $commandData[$command->getLabel()] = $data;
1126 }
1127
1128 $this->sendDataPacket(AvailableCommandsPacket::create($commandData, [], [], []));
1129 }
1130
1135 public function prepareClientTranslatableMessage(Translatable $message) : array{
1136 //we can't send nested translations to the client, so make sure they are always pre-translated by the server
1137 $language = $this->player->getLanguage();
1138 $parameters = array_map(fn(string|Translatable $p) => $p instanceof Translatable ? $language->translate($p) : $p, $message->getParameters());
1139 return [$language->translateString($message->getText(), $parameters, "pocketmine."), $parameters];
1140 }
1141
1142 public function onChatMessage(Translatable|string $message) : void{
1143 if($message instanceof Translatable){
1144 if(!$this->server->isLanguageForced()){
1145 $this->sendDataPacket(TextPacket::translation(...$this->prepareClientTranslatableMessage($message)));
1146 }else{
1147 $this->sendDataPacket(TextPacket::raw($this->player->getLanguage()->translate($message)));
1148 }
1149 }else{
1150 $this->sendDataPacket(TextPacket::raw($message));
1151 }
1152 }
1153
1154 public function onJukeboxPopup(Translatable|string $message) : void{
1155 $parameters = [];
1156 if($message instanceof Translatable){
1157 if(!$this->server->isLanguageForced()){
1158 [$message, $parameters] = $this->prepareClientTranslatableMessage($message);
1159 }else{
1160 $message = $this->player->getLanguage()->translate($message);
1161 }
1162 }
1163 $this->sendDataPacket(TextPacket::jukeboxPopup($message, $parameters));
1164 }
1165
1166 public function onPopup(string $message) : void{
1167 $this->sendDataPacket(TextPacket::popup($message));
1168 }
1169
1170 public function onTip(string $message) : void{
1171 $this->sendDataPacket(TextPacket::tip($message));
1172 }
1173
1174 public function onFormSent(int $id, Form $form) : bool{
1175 return $this->sendDataPacket(ModalFormRequestPacket::create($id, json_encode($form, JSON_THROW_ON_ERROR)));
1176 }
1177
1178 public function onCloseAllForms() : void{
1179 $this->sendDataPacket(ClientboundCloseFormPacket::create());
1180 }
1181
1185 private function sendChunkPacket(string $chunkPacket, \Closure $onCompletion, World $world) : void{
1186 $world->timings->syncChunkSend->startTiming();
1187 try{
1188 $this->queueCompressed($chunkPacket);
1189 $onCompletion();
1190 }finally{
1191 $world->timings->syncChunkSend->stopTiming();
1192 }
1193 }
1194
1200 public function startUsingChunk(int $chunkX, int $chunkZ, \Closure $onCompletion) : void{
1201 $world = $this->player->getLocation()->getWorld();
1202 $promiseOrPacket = ChunkCache::getInstance($world, $this->compressor)->request($chunkX, $chunkZ);
1203 if(is_string($promiseOrPacket)){
1204 $this->sendChunkPacket($promiseOrPacket, $onCompletion, $world);
1205 return;
1206 }
1207 $promiseOrPacket->onResolve(
1208 //this callback may be called synchronously or asynchronously, depending on whether the promise is resolved yet
1209 function(CompressBatchPromise $promise) use ($world, $onCompletion, $chunkX, $chunkZ) : void{
1210 if(!$this->isConnected()){
1211 return;
1212 }
1213 $currentWorld = $this->player->getLocation()->getWorld();
1214 if($world !== $currentWorld || ($status = $this->player->getUsedChunkStatus($chunkX, $chunkZ)) === null){
1215 $this->logger->debug("Tried to send no-longer-active chunk $chunkX $chunkZ in world " . $world->getFolderName());
1216 return;
1217 }
1218 if($status !== UsedChunkStatus::REQUESTED_SENDING){
1219 //TODO: make this an error
1220 //this could be triggered due to the shitty way that chunk resends are handled
1221 //right now - not because of the spammy re-requesting, but because the chunk status reverts
1222 //to NEEDED if they want to be resent.
1223 return;
1224 }
1225 $this->sendChunkPacket($promise->getResult(), $onCompletion, $world);
1226 }
1227 );
1228 }
1229
1230 public function stopUsingChunk(int $chunkX, int $chunkZ) : void{
1231
1232 }
1233
1234 public function onEnterWorld() : void{
1235 if($this->player !== null){
1236 $world = $this->player->getWorld();
1237 $this->syncWorldTime($world->getTime());
1238 $this->syncWorldDifficulty($world->getDifficulty());
1239 $this->syncWorldSpawnPoint($world->getSpawnLocation());
1240 //TODO: weather needs to be synced here (when implemented)
1241 }
1242 }
1243
1244 public function syncWorldTime(int $worldTime) : void{
1245 $this->sendDataPacket(SetTimePacket::create($worldTime));
1246 }
1247
1248 public function syncWorldDifficulty(int $worldDifficulty) : void{
1249 $this->sendDataPacket(SetDifficultyPacket::create($worldDifficulty));
1250 }
1251
1252 public function getInvManager() : ?InventoryManager{
1253 return $this->invManager;
1254 }
1255
1259 public function syncPlayerList(array $players) : void{
1260 $this->sendDataPacket(PlayerListPacket::add(array_map(function(Player $player) : PlayerListEntry{
1261 return PlayerListEntry::createAdditionEntry($player->getUniqueId(), $player->getId(), $player->getDisplayName(), $this->typeConverter->getSkinAdapter()->toSkinData($player->getSkin()), $player->getXuid());
1262 }, $players)));
1263 }
1264
1265 public function onPlayerAdded(Player $p) : void{
1266 $this->sendDataPacket(PlayerListPacket::add([PlayerListEntry::createAdditionEntry($p->getUniqueId(), $p->getId(), $p->getDisplayName(), $this->typeConverter->getSkinAdapter()->toSkinData($p->getSkin()), $p->getXuid())]));
1267 }
1268
1269 public function onPlayerRemoved(Player $p) : void{
1270 if($p !== $this->player){
1271 $this->sendDataPacket(PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($p->getUniqueId())]));
1272 }
1273 }
1274
1275 public function onTitle(string $title) : void{
1276 $this->sendDataPacket(SetTitlePacket::title($title));
1277 }
1278
1279 public function onSubTitle(string $subtitle) : void{
1280 $this->sendDataPacket(SetTitlePacket::subtitle($subtitle));
1281 }
1282
1283 public function onActionBar(string $actionBar) : void{
1284 $this->sendDataPacket(SetTitlePacket::actionBarMessage($actionBar));
1285 }
1286
1287 public function onClearTitle() : void{
1288 $this->sendDataPacket(SetTitlePacket::clearTitle());
1289 }
1290
1291 public function onResetTitleOptions() : void{
1292 $this->sendDataPacket(SetTitlePacket::resetTitleOptions());
1293 }
1294
1295 public function onTitleDuration(int $fadeIn, int $stay, int $fadeOut) : void{
1296 $this->sendDataPacket(SetTitlePacket::setAnimationTimes($fadeIn, $stay, $fadeOut));
1297 }
1298
1299 public function onToastNotification(string $title, string $body) : void{
1300 $this->sendDataPacket(ToastRequestPacket::create($title, $body));
1301 }
1302
1303 public function onOpenSignEditor(Vector3 $signPosition, bool $frontSide) : void{
1304 $this->sendDataPacket(OpenSignPacket::create(BlockPosition::fromVector3($signPosition), $frontSide));
1305 }
1306
1307 public function onItemCooldownChanged(Item $item, int $ticks) : void{
1308 $this->sendDataPacket(PlayerStartItemCooldownPacket::create(
1309 GlobalItemDataHandlers::getSerializer()->serializeType($item)->getName(),
1310 $ticks
1311 ));
1312 }
1313
1314 public function tick() : void{
1315 if(!$this->isConnected()){
1316 $this->dispose();
1317 return;
1318 }
1319
1320 if($this->info === null){
1321 if(time() >= $this->connectTime + 10){
1322 $this->disconnectWithError(KnownTranslationFactory::pocketmine_disconnect_error_loginTimeout());
1323 }
1324
1325 return;
1326 }
1327
1328 if($this->player !== null){
1329 $this->player->doChunkRequests();
1330
1331 $dirtyAttributes = $this->player->getAttributeMap()->needSend();
1332 $this->entityEventBroadcaster->syncAttributes([$this], $this->player, $dirtyAttributes);
1333 foreach($dirtyAttributes as $attribute){
1334 //TODO: we might need to send these to other players in the future
1335 //if that happens, this will need to become more complex than a flag on the attribute itself
1336 $attribute->markSynchronized();
1337 }
1338 }
1339 Timings::$playerNetworkSendInventorySync->startTiming();
1340 try{
1341 $this->invManager?->flushPendingUpdates();
1342 }finally{
1343 Timings::$playerNetworkSendInventorySync->stopTiming();
1344 }
1345
1346 $this->flushGamePacketQueue();
1347 }
1348}
handleDataPacket(Packet $packet, string $buffer)
prepareClientTranslatableMessage(Translatable $message)
onPlayerDestroyed(Translatable|string $reason, Translatable|string $disconnectScreenMessage)
sendDataPacketWithReceipt(ClientboundPacket $packet, bool $immediate=false)
onClientDisconnect(Translatable|string $reason)
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)