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