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