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