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