PocketMine-MP 5.37.2 git-aa47b7cd412ddb171ec53c035c2bbe84199be285
Loading...
Searching...
No Matches
Player.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\player;
25
26use DateTimeImmutable;
104use pocketmine\item\ItemUseResult;
127use pocketmine\permission\PermissibleDelegateTrait;
135use pocketmine\world\ChunkListenerNoOpTrait;
148use Ramsey\Uuid\UuidInterface;
149use function abs;
150use function array_filter;
151use function array_shift;
152use function assert;
153use function count;
154use function explode;
155use function floor;
156use function get_class;
157use function max;
158use function mb_strlen;
159use function microtime;
160use function min;
161use function preg_match;
162use function spl_object_id;
163use function sqrt;
164use function str_starts_with;
165use function strlen;
166use function strtolower;
167use function substr;
168use function trim;
169use const M_PI;
170use const M_SQRT3;
171use const PHP_INT_MAX;
172
177 use PermissibleDelegateTrait;
178
179 private const MOVES_PER_TICK = 2;
180 private const MOVE_BACKLOG_SIZE = 100 * self::MOVES_PER_TICK; //100 ticks backlog (5 seconds)
181
183 private const MAX_CHAT_CHAR_LENGTH = 512;
189 private const MAX_CHAT_BYTE_LENGTH = self::MAX_CHAT_CHAR_LENGTH * 4;
190 private const MAX_REACH_DISTANCE_CREATIVE = 13;
191 private const MAX_REACH_DISTANCE_SURVIVAL = 7;
192 private const MAX_REACH_DISTANCE_ENTITY_INTERACTION = 8;
193
194 public const DEFAULT_FLIGHT_SPEED_MULTIPLIER = 0.05;
195
196 public const TAG_FIRST_PLAYED = "firstPlayed"; //TAG_Long
197 public const TAG_LAST_PLAYED = "lastPlayed"; //TAG_Long
198 private const TAG_GAME_MODE = "playerGameType"; //TAG_Int
199 private const TAG_SPAWN_WORLD = "SpawnLevel"; //TAG_String
200 private const TAG_SPAWN_X = "SpawnX"; //TAG_Int
201 private const TAG_SPAWN_Y = "SpawnY"; //TAG_Int
202 private const TAG_SPAWN_Z = "SpawnZ"; //TAG_Int
203 private const TAG_DEATH_WORLD = "DeathLevel"; //TAG_String
204 private const TAG_DEATH_X = "DeathPositionX"; //TAG_Int
205 private const TAG_DEATH_Y = "DeathPositionY"; //TAG_Int
206 private const TAG_DEATH_Z = "DeathPositionZ"; //TAG_Int
207 public const TAG_LEVEL = "Level"; //TAG_String
208 public const TAG_LAST_KNOWN_XUID = "LastKnownXUID"; //TAG_String
209
213 public static function isValidUserName(?string $name) : bool{
214 if($name === null){
215 return false;
216 }
217
218 $lname = strtolower($name);
219 $len = strlen($name);
220 return $lname !== "rcon" && $lname !== "console" && $len >= 1 && $len <= 16 && preg_match("/[^A-Za-z0-9_ ]/", $name) === 0;
221 }
222
223 protected ?NetworkSession $networkSession;
224
225 public bool $spawned = false;
226
227 protected string $username;
228 protected string $displayName;
229 protected string $xuid = "";
230 protected bool $authenticated;
231 protected PlayerInfo $playerInfo;
232
233 protected ?InventoryWindow $currentWindow = null;
235 protected array $permanentWindows = [];
236 protected Inventory $cursorInventory;
237 protected CraftingGrid $craftingGrid;
238 protected CreativeInventory $creativeInventory;
239
240 protected int $messageCounter = 2;
241
242 protected DateTimeImmutable $firstPlayed;
243 protected DateTimeImmutable $lastPlayed;
244 protected GameMode $gamemode;
245
250 protected array $usedChunks = [];
255 private array $activeChunkGenerationRequests = [];
260 protected array $loadQueue = [];
261 protected int $nextChunkOrderRun = 5;
262
264 private array $tickingChunks = [];
265
266 protected int $viewDistance = -1;
267 protected int $spawnThreshold;
268 protected int $spawnChunkLoadCount = 0;
269 protected int $chunksPerTick;
270 protected ChunkSelector $chunkSelector;
271 protected ChunkLoader $chunkLoader;
272 protected ChunkTicker $chunkTicker;
273
275 protected array $hiddenPlayers = [];
276
277 protected float $moveRateLimit = 10 * self::MOVES_PER_TICK;
278 protected ?float $lastMovementProcess = null;
279
280 protected int $inAirTicks = 0;
281
282 protected float $stepHeight = 0.6;
283
284 protected ?Vector3 $sleeping = null;
285 private ?Position $spawnPosition = null;
286
287 private bool $respawnLocked = false;
288
289 private ?Position $deathPosition = null;
290
291 //TODO: Abilities
292 protected bool $autoJump = true;
293 protected bool $allowFlight = false;
294 protected bool $blockCollision = true;
295 protected bool $flying = false;
296 protected bool $sneakPressed = false;
297
298 protected float $flightSpeedMultiplier = self::DEFAULT_FLIGHT_SPEED_MULTIPLIER;
299
301 protected ?int $lineHeight = null;
302 private CommandAliasMap $commandAliasMap;
303
304 protected string $locale = "en_US";
305
306 protected int $startAction = -1;
307
312 protected array $usedItemsCooldown = [];
313
314 private int $lastEmoteTick = 0;
315
316 protected int $formIdCounter = 0;
318 protected array $forms = [];
319
320 protected \Logger $logger;
321
322 protected ?SurvivalBlockBreakHandler $blockBreakHandler = null;
323
324 public function __construct(Server $server, NetworkSession $session, PlayerInfo $playerInfo, bool $authenticated, Location $spawnLocation, ?CompoundTag $namedtag){
325 $username = TextFormat::clean($playerInfo->getUsername());
326 $this->logger = new \PrefixedLogger($server->getLogger(), "Player: $username");
327
328 $this->server = $server;
329 $this->networkSession = $session;
330 $this->playerInfo = $playerInfo;
331 $this->authenticated = $authenticated;
332
333 $this->username = $username;
334 $this->displayName = $this->username;
335 $this->locale = $this->playerInfo->getLocale();
336
337 $this->uuid = $this->playerInfo->getUuid();
338 $this->xuid = $this->playerInfo instanceof XboxLivePlayerInfo ? $this->playerInfo->getXuid() : "";
339
340 $this->creativeInventory = CreativeInventory::getInstance();
341
342 $rootPermissions = [DefaultPermissions::ROOT_USER => true];
343 if($this->server->isOp($this->username)){
344 $rootPermissions[DefaultPermissions::ROOT_OPERATOR] = true;
345 }
346 $this->perm = new PermissibleBase($rootPermissions);
347 $this->commandAliasMap = new CommandAliasMap();
348
349 $this->chunksPerTick = $this->server->getConfigGroup()->getPropertyInt(YmlServerProperties::CHUNK_SENDING_PER_TICK, 4);
350 $this->spawnThreshold = (int) (($this->server->getConfigGroup()->getPropertyInt(YmlServerProperties::CHUNK_SENDING_SPAWN_RADIUS, 4) ** 2) * M_PI);
351 $this->chunkSelector = new ChunkSelector();
352
353 $this->chunkLoader = new ChunkLoader();
354 $this->chunkTicker = new ChunkTicker();
355 $world = $spawnLocation->getWorld();
356 //load the spawn chunk so we can see the terrain
357 $xSpawnChunk = $spawnLocation->getFloorX() >> Chunk::COORD_BIT_SIZE;
358 $zSpawnChunk = $spawnLocation->getFloorZ() >> Chunk::COORD_BIT_SIZE;
359 $world->registerChunkLoader($this->chunkLoader, $xSpawnChunk, $zSpawnChunk, true);
360 $world->registerChunkListener($this, $xSpawnChunk, $zSpawnChunk);
361 $this->usedChunks[World::chunkHash($xSpawnChunk, $zSpawnChunk)] = UsedChunkStatus::NEEDED;
362
363 parent::__construct($spawnLocation, $this->playerInfo->getSkin(), $namedtag);
364 }
365
366 protected function initHumanData(CompoundTag $nbt) : void{
367 $this->setNameTag($this->username);
368 }
369
370 private function callDummyItemHeldEvent() : void{
371 $slot = $this->hotbar->getSelectedIndex();
372
373 $event = new PlayerItemHeldEvent($this, $this->inventory->getItem($slot), $slot);
374 $event->call();
375 //TODO: this event is actually cancellable, but cancelling it here has no meaningful result, so we
376 //just ignore it. We fire this only because the content of the held slot changed, not because the
377 //held slot index changed. We can't prevent that from here, and nor would it be sensible to.
378 }
379
380 protected function initEntity(CompoundTag $nbt) : void{
381 parent::initEntity($nbt);
382 $this->addDefaultWindows();
383
384 $this->inventory->getListeners()->add(new CallbackInventoryListener(
385 function(Inventory $unused, int $slot) : void{
386 if($slot === $this->hotbar->getSelectedIndex()){
387 $this->setUsingItem(false);
388
389 $this->callDummyItemHeldEvent();
390 }
391 },
392 function() : void{
393 $this->setUsingItem(false);
394 $this->callDummyItemHeldEvent();
395 }
396 ));
397
398 $now = (int) (microtime(true) * 1000);
399 $createDateTimeImmutable = static function(string $tag) use ($nbt, $now) : DateTimeImmutable{
400 return new DateTimeImmutable('@' . $nbt->getLong($tag, $now) / 1000);
401 };
402 $this->firstPlayed = $createDateTimeImmutable(self::TAG_FIRST_PLAYED);
403 $this->lastPlayed = $createDateTimeImmutable(self::TAG_LAST_PLAYED);
404
405 if(!$this->server->getForceGamemode() && ($gameModeTag = $nbt->getTag(self::TAG_GAME_MODE)) instanceof IntTag){
406 $this->internalSetGameMode(GameModeIdMap::getInstance()->fromId($gameModeTag->getValue()) ?? GameMode::SURVIVAL); //TODO: bad hack here to avoid crashes on corrupted data
407 }else{
408 $this->internalSetGameMode($this->server->getGamemode());
409 }
410
411 $this->keepMovement = true;
412
413 $this->setNameTagVisible();
414 $this->setNameTagAlwaysVisible();
415 $this->setCanClimb();
416
417 if(($world = $this->server->getWorldManager()->getWorldByName($nbt->getString(self::TAG_SPAWN_WORLD, ""))) instanceof World){
418 $this->spawnPosition = new Position($nbt->getInt(self::TAG_SPAWN_X), $nbt->getInt(self::TAG_SPAWN_Y), $nbt->getInt(self::TAG_SPAWN_Z), $world);
419 }
420 if(($world = $this->server->getWorldManager()->getWorldByName($nbt->getString(self::TAG_DEATH_WORLD, ""))) instanceof World){
421 $this->deathPosition = new Position($nbt->getInt(self::TAG_DEATH_X), $nbt->getInt(self::TAG_DEATH_Y), $nbt->getInt(self::TAG_DEATH_Z), $world);
422 }
423 }
424
425 public function getLeaveMessage() : Translatable|string{
426 if($this->spawned){
427 return KnownTranslationFactory::multiplayer_player_left($this->getDisplayName())->prefix(TextFormat::YELLOW);
428 }
429
430 return "";
431 }
432
433 public function isAuthenticated() : bool{
434 return $this->authenticated;
435 }
436
441 public function getPlayerInfo() : PlayerInfo{ return $this->playerInfo; }
442
447 public function getXuid() : string{
448 return $this->xuid;
449 }
450
458 public function getUniqueId() : UuidInterface{
459 return parent::getUniqueId();
460 }
461
465 public function getFirstPlayed() : ?DateTimeImmutable{
466 return $this->firstPlayed;
467 }
468
472 public function getLastPlayed() : ?DateTimeImmutable{
473 return $this->lastPlayed;
474 }
475
476 public function hasPlayedBefore() : bool{
477 return ((int) $this->firstPlayed->diff($this->lastPlayed)->format('%s')) > 1;
478 }
479
489 public function setAllowFlight(bool $value) : void{
490 if($this->allowFlight !== $value){
491 $this->allowFlight = $value;
492 $this->getNetworkSession()->syncAbilities($this);
493 }
494 }
495
502 public function getAllowFlight() : bool{
503 return $this->allowFlight;
504 }
505
514 public function setHasBlockCollision(bool $value) : void{
515 if($this->blockCollision !== $value){
516 $this->blockCollision = $value;
517 $this->getNetworkSession()->syncAbilities($this);
518 }
519 }
520
525 public function hasBlockCollision() : bool{
526 return $this->blockCollision;
527 }
528
529 public function setFlying(bool $value) : void{
530 if($this->flying !== $value){
531 $this->flying = $value;
532 $this->resetFallDistance();
533 $this->getNetworkSession()->syncAbilities($this);
534 }
535 }
536
537 public function isFlying() : bool{
538 return $this->flying;
539 }
540
554 public function setFlightSpeedMultiplier(float $flightSpeedMultiplier) : void{
555 if($this->flightSpeedMultiplier !== $flightSpeedMultiplier){
556 $this->flightSpeedMultiplier = $flightSpeedMultiplier;
557 $this->getNetworkSession()->syncAbilities($this);
558 }
559 }
560
572 public function getFlightSpeedMultiplier() : float{
573 return $this->flightSpeedMultiplier;
574 }
575
576 public function setAutoJump(bool $value) : void{
577 if($this->autoJump !== $value){
578 $this->autoJump = $value;
579 $this->getNetworkSession()->syncAdventureSettings();
580 }
581 }
582
583 public function hasAutoJump() : bool{
584 return $this->autoJump;
585 }
586
587 public function spawnTo(Player $player) : void{
588 if($this->isAlive() && $player->isAlive() && $player->canSee($this) && !$this->isSpectator()){
589 parent::spawnTo($player);
590 }
591 }
592
593 public function getServer() : Server{
594 return $this->server;
595 }
596
597 public function getScreenLineHeight() : int{
598 return $this->lineHeight ?? 7;
599 }
600
601 public function setScreenLineHeight(?int $height) : void{
602 if($height !== null && $height < 1){
603 throw new \InvalidArgumentException("Line height must be at least 1");
604 }
605 $this->lineHeight = $height;
606 }
607
608 public function getCommandAliasMap() : CommandAliasMap{ return $this->commandAliasMap; }
609
610 public function canSee(Player $player) : bool{
611 return !isset($this->hiddenPlayers[$player->getUniqueId()->getBytes()]);
612 }
613
614 public function hidePlayer(Player $player) : void{
615 if($player === $this){
616 return;
617 }
618 $this->hiddenPlayers[$player->getUniqueId()->getBytes()] = true;
619 $player->despawnFrom($this);
620 }
621
622 public function showPlayer(Player $player) : void{
623 if($player === $this){
624 return;
625 }
626 unset($this->hiddenPlayers[$player->getUniqueId()->getBytes()]);
627 if($player->isOnline()){
628 $player->spawnTo($this);
629 }
630 }
631
632 public function canCollideWith(Entity $entity) : bool{
633 return false;
634 }
635
636 public function canBeCollidedWith() : bool{
637 return !$this->isSpectator() && parent::canBeCollidedWith();
638 }
639
640 public function resetFallDistance() : void{
641 parent::resetFallDistance();
642 $this->inAirTicks = 0;
643 }
644
645 public function getViewDistance() : int{
646 return $this->viewDistance;
647 }
648
649 public function setViewDistance(int $distance) : void{
650 $newViewDistance = $this->server->getAllowedViewDistance($distance);
651
652 if($newViewDistance !== $this->viewDistance){
653 $ev = new PlayerViewDistanceChangeEvent($this, $this->viewDistance, $newViewDistance);
654 $ev->call();
655 }
656
657 $this->viewDistance = $newViewDistance;
658
659 $this->spawnThreshold = (int) (min($this->viewDistance, $this->server->getConfigGroup()->getPropertyInt(YmlServerProperties::CHUNK_SENDING_SPAWN_RADIUS, 4)) ** 2 * M_PI);
660
661 $this->nextChunkOrderRun = 0;
662
663 $this->getNetworkSession()->syncViewAreaRadius($this->viewDistance);
664
665 $this->logger->debug("Setting view distance to " . $this->viewDistance . " (requested " . $distance . ")");
666 }
667
668 public function isOnline() : bool{
669 return $this->isConnected();
670 }
671
672 public function isConnected() : bool{
673 return $this->networkSession !== null && $this->networkSession->isConnected();
674 }
675
676 public function getNetworkSession() : NetworkSession{
677 if($this->networkSession === null){
678 throw new \LogicException("Player is not connected");
679 }
680 return $this->networkSession;
681 }
682
686 public function getName() : string{
687 return $this->username;
688 }
689
693 public function getDisplayName() : string{
694 return $this->displayName;
695 }
696
697 public function setDisplayName(string $name) : void{
698 $ev = new PlayerDisplayNameChangeEvent($this, $this->displayName, $name);
699 $ev->call();
700
701 $this->displayName = $ev->getNewName();
702 }
703
704 public function canBeRenamed() : bool{
705 return false;
706 }
707
711 public function getLocale() : string{
712 return $this->locale;
713 }
714
715 public function getLanguage() : Language{
716 return $this->server->getLanguage();
717 }
718
723 public function changeSkin(Skin $skin, string $newSkinName, string $oldSkinName) : bool{
724 $ev = new PlayerChangeSkinEvent($this, $this->getSkin(), $skin);
725 $ev->call();
726
727 if($ev->isCancelled()){
728 $this->sendSkin([$this]);
729 return true;
730 }
731
732 $this->setSkin($ev->getNewSkin());
733 $this->sendSkin($this->server->getOnlinePlayers());
734 return true;
735 }
736
742 public function sendSkin(?array $targets = null) : void{
743 parent::sendSkin($targets ?? $this->server->getOnlinePlayers());
744 }
745
749 public function isUsingItem() : bool{
750 return $this->startAction > -1;
751 }
752
753 public function setUsingItem(bool $value) : void{
754 $this->startAction = $value ? $this->server->getTick() : -1;
755 $this->networkPropertiesDirty = true;
756 }
757
762 public function getItemUseDuration() : int{
763 return $this->startAction === -1 ? -1 : ($this->server->getTick() - $this->startAction);
764 }
765
769 public function getItemCooldownExpiry(Item $item) : int{
770 $this->checkItemCooldowns();
771 return $this->usedItemsCooldown[$item->getCooldownTag() ?? $item->getStateId()] ?? 0;
772 }
773
777 public function hasItemCooldown(Item $item) : bool{
778 $this->checkItemCooldowns();
779 return isset($this->usedItemsCooldown[$item->getCooldownTag() ?? $item->getStateId()]);
780 }
781
785 public function resetItemCooldown(Item $item, ?int $ticks = null) : void{
786 $ticks = $ticks ?? $item->getCooldownTicks();
787 if($ticks > 0){
788 $this->usedItemsCooldown[$item->getCooldownTag() ?? $item->getStateId()] = $this->server->getTick() + $ticks;
789 $this->getNetworkSession()->onItemCooldownChanged($item, $ticks);
790 }
791 }
792
793 protected function checkItemCooldowns() : void{
794 $serverTick = $this->server->getTick();
795 foreach($this->usedItemsCooldown as $itemId => $cooldownUntil){
796 if($cooldownUntil <= $serverTick){
797 unset($this->usedItemsCooldown[$itemId]);
798 }
799 }
800 }
801
802 protected function setPosition(Vector3 $pos) : bool{
803 $oldWorld = $this->location->isValid() ? $this->location->getWorld() : null;
804 if(parent::setPosition($pos)){
805 $newWorld = $this->getWorld();
806 if($oldWorld !== $newWorld){
807 if($oldWorld !== null){
808 foreach($this->usedChunks as $index => $status){
809 World::getXZ($index, $X, $Z);
810 $this->unloadChunk($X, $Z, $oldWorld);
811 }
812 }
813
814 $this->usedChunks = [];
815 $this->loadQueue = [];
816 $this->getNetworkSession()->onEnterWorld();
817 }
818
819 return true;
820 }
821
822 return false;
823 }
824
825 protected function unloadChunk(int $x, int $z, ?World $world = null) : void{
826 $world = $world ?? $this->getWorld();
827 $index = World::chunkHash($x, $z);
828 if(isset($this->usedChunks[$index])){
829 foreach($world->getChunkEntities($x, $z) as $entity){
830 if($entity !== $this){
831 $entity->despawnFrom($this);
832 }
833 }
834 $this->getNetworkSession()->stopUsingChunk($x, $z);
835 unset($this->usedChunks[$index]);
836 unset($this->activeChunkGenerationRequests[$index]);
837 }
838 $world->unregisterChunkLoader($this->chunkLoader, $x, $z);
839 $world->unregisterChunkListener($this, $x, $z);
840 unset($this->loadQueue[$index]);
841 $world->unregisterTickingChunk($this->chunkTicker, $x, $z);
842 unset($this->tickingChunks[$index]);
843 }
844
845 protected function spawnEntitiesOnAllChunks() : void{
846 foreach($this->usedChunks as $chunkHash => $status){
847 if($status === UsedChunkStatus::SENT){
848 World::getXZ($chunkHash, $chunkX, $chunkZ);
849 $this->spawnEntitiesOnChunk($chunkX, $chunkZ);
850 }
851 }
852 }
853
854 protected function spawnEntitiesOnChunk(int $chunkX, int $chunkZ) : void{
855 foreach($this->getWorld()->getChunkEntities($chunkX, $chunkZ) as $entity){
856 if($entity !== $this && !$entity->isFlaggedForDespawn()){
857 $entity->spawnTo($this);
858 }
859 }
860 }
861
866 protected function requestChunks() : void{
867 if(!$this->isConnected()){
868 return;
869 }
870
871 Timings::$playerChunkSend->startTiming();
872
873 $count = 0;
874 $world = $this->getWorld();
875
876 $limit = $this->chunksPerTick - count($this->activeChunkGenerationRequests);
877 foreach($this->loadQueue as $index => $distance){
878 if($count >= $limit){
879 break;
880 }
881
882 $X = null;
883 $Z = null;
884 World::getXZ($index, $X, $Z);
885
886 ++$count;
887
888 $this->usedChunks[$index] = UsedChunkStatus::REQUESTED_GENERATION;
889 $this->activeChunkGenerationRequests[$index] = true;
890 unset($this->loadQueue[$index]);
891 $world->registerChunkLoader($this->chunkLoader, $X, $Z, true);
892 $world->registerChunkListener($this, $X, $Z);
893 if(isset($this->tickingChunks[$index])){
894 $world->registerTickingChunk($this->chunkTicker, $X, $Z);
895 }
896
897 $world->requestChunkPopulation($X, $Z, $this->chunkLoader)->onCompletion(
898 function() use ($X, $Z, $index, $world) : void{
899 if(!$this->isConnected() || !isset($this->usedChunks[$index]) || $world !== $this->getWorld()){
900 return;
901 }
902 if($this->usedChunks[$index] !== UsedChunkStatus::REQUESTED_GENERATION){
903 //We may have previously requested this, decided we didn't want it, and then decided we did want
904 //it again, all before the generation request got executed. In that case, the promise would have
905 //multiple callbacks for this player. In that case, only the first one matters.
906 return;
907 }
908 unset($this->activeChunkGenerationRequests[$index]);
909 $this->usedChunks[$index] = UsedChunkStatus::REQUESTED_SENDING;
910
911 $this->getNetworkSession()->startUsingChunk($X, $Z, function() use ($X, $Z, $index) : void{
912 $this->usedChunks[$index] = UsedChunkStatus::SENT;
913 if($this->spawnChunkLoadCount === -1){
914 $this->spawnEntitiesOnChunk($X, $Z);
915 }elseif($this->spawnChunkLoadCount++ === $this->spawnThreshold){
916 $this->spawnChunkLoadCount = -1;
917
918 $this->spawnEntitiesOnAllChunks();
919
920 $this->getNetworkSession()->notifyTerrainReady();
921 }
922 (new PlayerPostChunkSendEvent($this, $X, $Z))->call();
923 });
924 },
925 static function() : void{
926 //NOOP: we'll re-request this if it fails anyway
927 }
928 );
929 }
930
931 Timings::$playerChunkSend->stopTiming();
932 }
933
934 private function recheckBroadcastPermissions() : void{
935 foreach([
936 DefaultPermissionNames::BROADCAST_ADMIN => Server::BROADCAST_CHANNEL_ADMINISTRATIVE,
937 DefaultPermissionNames::BROADCAST_USER => Server::BROADCAST_CHANNEL_USERS
938 ] as $permission => $channel){
939 if($this->hasPermission($permission)){
940 $this->server->subscribeToBroadcastChannel($channel, $this);
941 }else{
942 $this->server->unsubscribeFromBroadcastChannel($channel, $this);
943 }
944 }
945 }
946
951 public function doFirstSpawn() : void{
952 if($this->spawned){
953 return;
954 }
955 $this->spawned = true;
956 $this->recheckBroadcastPermissions();
957 $this->getPermissionRecalculationCallbacks()->add(function(array $changedPermissionsOldValues) : void{
958 if(isset($changedPermissionsOldValues[Server::BROADCAST_CHANNEL_ADMINISTRATIVE]) || isset($changedPermissionsOldValues[Server::BROADCAST_CHANNEL_USERS])){
959 $this->recheckBroadcastPermissions();
960 }
961 });
962
963 $ev = new PlayerJoinEvent($this,
964 KnownTranslationFactory::multiplayer_player_joined($this->getDisplayName())->prefix(TextFormat::YELLOW)
965 );
966 $ev->call();
967 if($ev->getJoinMessage() !== ""){
968 $this->server->broadcastMessage($ev->getJoinMessage());
969 }
970
971 $this->noDamageTicks = 60;
972
973 $this->spawnToAll();
974
975 if($this->getHealth() <= 0){
976 $this->logger->debug("Quit while dead, forcing respawn");
977 $this->actuallyRespawn();
978 }
979 }
980
988 private function updateTickingChunkRegistrations(array $oldTickingChunks, array $newTickingChunks) : void{
989 $world = $this->getWorld();
990 foreach($oldTickingChunks as $hash => $_){
991 if(!isset($newTickingChunks[$hash]) && !isset($this->loadQueue[$hash])){
992 //we are (probably) still using this chunk, but it's no longer within ticking range
993 World::getXZ($hash, $tickingChunkX, $tickingChunkZ);
994 $world->unregisterTickingChunk($this->chunkTicker, $tickingChunkX, $tickingChunkZ);
995 }
996 }
997 foreach($newTickingChunks as $hash => $_){
998 if(!isset($oldTickingChunks[$hash]) && !isset($this->loadQueue[$hash])){
999 //we were already using this chunk, but it is now within ticking range
1000 World::getXZ($hash, $tickingChunkX, $tickingChunkZ);
1001 $world->registerTickingChunk($this->chunkTicker, $tickingChunkX, $tickingChunkZ);
1002 }
1003 }
1004 }
1005
1010 protected function orderChunks() : void{
1011 if(!$this->isConnected() || $this->viewDistance === -1){
1012 return;
1013 }
1014
1015 Timings::$playerChunkOrder->startTiming();
1016
1017 $newOrder = [];
1018 $tickingChunks = [];
1019 $unloadChunks = $this->usedChunks;
1020
1021 $world = $this->getWorld();
1022 $tickingChunkRadius = $world->getChunkTickRadius();
1023
1024 foreach($this->chunkSelector->selectChunks(
1025 $this->server->getAllowedViewDistance($this->viewDistance),
1026 $this->location->getFloorX() >> Chunk::COORD_BIT_SIZE,
1027 $this->location->getFloorZ() >> Chunk::COORD_BIT_SIZE
1028 ) as $radius => $hash){
1029 if(!isset($this->usedChunks[$hash]) || $this->usedChunks[$hash] === UsedChunkStatus::NEEDED){
1030 $newOrder[$hash] = true;
1031 }
1032 if($radius < $tickingChunkRadius){
1033 $tickingChunks[$hash] = true;
1034 }
1035 unset($unloadChunks[$hash]);
1036 }
1037
1038 foreach($unloadChunks as $index => $status){
1039 World::getXZ($index, $X, $Z);
1040 $this->unloadChunk($X, $Z);
1041 }
1042
1043 $this->loadQueue = $newOrder;
1044
1045 $this->updateTickingChunkRegistrations($this->tickingChunks, $tickingChunks);
1046 $this->tickingChunks = $tickingChunks;
1047
1048 if(count($this->loadQueue) > 0 || count($unloadChunks) > 0){
1049 $this->getNetworkSession()->syncViewAreaCenterPoint($this->location, $this->viewDistance);
1050 }
1051
1052 Timings::$playerChunkOrder->stopTiming();
1053 }
1054
1059 public function isUsingChunk(int $chunkX, int $chunkZ) : bool{
1060 return isset($this->usedChunks[World::chunkHash($chunkX, $chunkZ)]);
1061 }
1062
1067 public function getUsedChunks() : array{
1068 return $this->usedChunks;
1069 }
1070
1074 public function getUsedChunkStatus(int $chunkX, int $chunkZ) : ?UsedChunkStatus{
1075 return $this->usedChunks[World::chunkHash($chunkX, $chunkZ)] ?? null;
1076 }
1077
1081 public function hasReceivedChunk(int $chunkX, int $chunkZ) : bool{
1082 $status = $this->usedChunks[World::chunkHash($chunkX, $chunkZ)] ?? null;
1083 return $status === UsedChunkStatus::SENT;
1084 }
1085
1089 public function doChunkRequests() : void{
1090 if($this->nextChunkOrderRun !== PHP_INT_MAX && $this->nextChunkOrderRun-- <= 0){
1091 $this->nextChunkOrderRun = PHP_INT_MAX;
1092 $this->orderChunks();
1093 }
1094
1095 if(count($this->loadQueue) > 0){
1096 $this->requestChunks();
1097 }
1098 }
1099
1100 public function getDeathPosition() : ?Position{
1101 if($this->deathPosition !== null && !$this->deathPosition->isValid()){
1102 $this->deathPosition = null;
1103 }
1104 return $this->deathPosition;
1105 }
1106
1110 public function setDeathPosition(?Vector3 $pos) : void{
1111 if($pos !== null){
1112 if($pos instanceof Position && $pos->world !== null){
1113 $world = $pos->world;
1114 }else{
1115 $world = $this->getWorld();
1116 }
1117 $this->deathPosition = new Position($pos->x, $pos->y, $pos->z, $world);
1118 }else{
1119 $this->deathPosition = null;
1120 }
1121 $this->networkPropertiesDirty = true;
1122 }
1123
1127 public function getSpawn(){
1128 if($this->hasValidCustomSpawn()){
1129 return $this->spawnPosition;
1130 }else{
1131 $world = $this->server->getWorldManager()->getDefaultWorld();
1132
1133 return $world->getSpawnLocation();
1134 }
1135 }
1136
1137 public function hasValidCustomSpawn() : bool{
1138 return $this->spawnPosition !== null && $this->spawnPosition->isValid();
1139 }
1140
1147 public function setSpawn(?Vector3 $pos) : void{
1148 if($pos !== null){
1149 if(!($pos instanceof Position)){
1150 $world = $this->getWorld();
1151 }else{
1152 $world = $pos->getWorld();
1153 }
1154 $this->spawnPosition = new Position($pos->x, $pos->y, $pos->z, $world);
1155 }else{
1156 $this->spawnPosition = null;
1157 }
1158 $this->getNetworkSession()->syncPlayerSpawnPoint($this->getSpawn());
1159 }
1160
1161 public function isSleeping() : bool{
1162 return $this->sleeping !== null;
1163 }
1164
1165 public function sleepOn(Vector3 $pos) : bool{
1166 $pos = $pos->floor();
1167 $b = $this->getWorld()->getBlock($pos);
1168
1169 $ev = new PlayerBedEnterEvent($this, $b);
1170 $ev->call();
1171 if($ev->isCancelled()){
1172 return false;
1173 }
1174
1175 if($b instanceof Bed){
1176 $b->setOccupied();
1177 $this->getWorld()->setBlock($pos, $b);
1178 }
1179
1180 $this->sleeping = $pos;
1181 $this->networkPropertiesDirty = true;
1182
1183 $this->setSpawn($pos);
1184
1185 $this->getWorld()->setSleepTicks(60);
1186
1187 return true;
1188 }
1189
1190 public function stopSleep() : void{
1191 if($this->sleeping instanceof Vector3){
1192 $b = $this->getWorld()->getBlock($this->sleeping);
1193 if($b instanceof Bed){
1194 $b->setOccupied(false);
1195 $this->getWorld()->setBlock($this->sleeping, $b);
1196 }
1197 (new PlayerBedLeaveEvent($this, $b))->call();
1198
1199 $this->sleeping = null;
1200 $this->networkPropertiesDirty = true;
1201
1202 $this->getWorld()->setSleepTicks(0);
1203
1204 $this->getNetworkSession()->sendDataPacket(AnimatePacket::create($this->getId(), AnimatePacket::ACTION_STOP_SLEEP));
1205 }
1206 }
1207
1208 public function getGamemode() : GameMode{
1209 return $this->gamemode;
1210 }
1211
1212 protected function internalSetGameMode(GameMode $gameMode) : void{
1213 $this->gamemode = $gameMode;
1214
1215 $this->allowFlight = $this->gamemode === GameMode::CREATIVE;
1216 $this->hungerManager->setEnabled($this->isSurvival());
1217
1218 if($this->isSpectator()){
1219 $this->setFlying(true);
1220 $this->setHasBlockCollision(false);
1221 $this->setSilent();
1222 $this->onGround = false;
1223
1224 //TODO: HACK! this syncs the onground flag with the client so that flying works properly
1225 //this is a yucky hack but we don't have any other options :(
1226 $this->sendPosition($this->location, null, null, MovePlayerPacket::MODE_TELEPORT);
1227 }else{
1228 if($this->isSurvival()){
1229 $this->setFlying(false);
1230 }
1231 $this->setHasBlockCollision(true);
1232 $this->setSilent(false);
1233 $this->checkGroundState(0, 0, 0, 0, 0, 0);
1234 }
1235 }
1236
1240 public function setGamemode(GameMode $gm) : bool{
1241 if($this->gamemode === $gm){
1242 return false;
1243 }
1244
1245 $ev = new PlayerGameModeChangeEvent($this, $gm);
1246 $ev->call();
1247 if($ev->isCancelled()){
1248 return false;
1249 }
1250
1251 $this->internalSetGameMode($gm);
1252
1253 if($this->isSpectator()){
1254 $this->despawnFromAll();
1255 }else{
1256 $this->spawnToAll();
1257 }
1258
1259 $this->getNetworkSession()->syncGameMode($this->gamemode);
1260 return true;
1261 }
1262
1269 public function isSurvival(bool $literal = false) : bool{
1270 return $this->gamemode === GameMode::SURVIVAL || (!$literal && $this->gamemode === GameMode::ADVENTURE);
1271 }
1272
1279 public function isCreative(bool $literal = false) : bool{
1280 return $this->gamemode === GameMode::CREATIVE || (!$literal && $this->gamemode === GameMode::SPECTATOR);
1281 }
1282
1289 public function isAdventure(bool $literal = false) : bool{
1290 return $this->gamemode === GameMode::ADVENTURE || (!$literal && $this->gamemode === GameMode::SPECTATOR);
1291 }
1292
1293 public function isSpectator() : bool{
1294 return $this->gamemode === GameMode::SPECTATOR;
1295 }
1296
1297 public function setSneakPressed(bool $sneakPressed) : void{
1298 $this->sneakPressed = $sneakPressed;
1299 }
1300
1305 public function isSneakPressed() : bool{
1306 return $this->sneakPressed;
1307 }
1308
1312 public function hasFiniteResources() : bool{
1313 return $this->gamemode !== GameMode::CREATIVE;
1314 }
1315
1316 public function getDrops() : array{
1317 if($this->hasFiniteResources()){
1318 return parent::getDrops();
1319 }
1320
1321 return [];
1322 }
1323
1324 public function getXpDropAmount() : int{
1325 if($this->hasFiniteResources()){
1326 return parent::getXpDropAmount();
1327 }
1328
1329 return 0;
1330 }
1331
1332 protected function checkGroundState(float $wantedX, float $wantedY, float $wantedZ, float $dx, float $dy, float $dz) : void{
1333 if(!$this->blockCollision){
1334 $this->onGround = false;
1335 }else{
1336 //TODO: AxisAlignedBB::withComponents() would be nice here
1337 $bb = new AxisAlignedBB(
1338 $this->boundingBox->minX,
1339 $this->location->y - 0.2,
1340 $this->boundingBox->minZ,
1341 $this->boundingBox->maxX,
1342 $this->location->y + 0.2,
1343 $this->boundingBox->maxZ
1344 );
1345
1346 //we're already at the new position at this point; check if there are blocks we might have landed on between
1347 //the old and new positions (running down stairs necessitates this)
1348 $bb = $bb->addCoord(-$dx, -$dy, -$dz);
1349
1350 $this->onGround = $this->isCollided = count($this->getWorld()->getCollisionBlocks($bb, true)) > 0;
1351 }
1352 }
1353
1354 public function canBeMovedByCurrents() : bool{
1355 return false; //currently has no server-side movement
1356 }
1357
1358 protected function checkNearEntities() : void{
1359 foreach($this->getWorld()->getNearbyEntities($this->boundingBox->expandedCopy(1, 0.5, 1), $this) as $entity){
1360 $entity->scheduleUpdate();
1361
1362 if(!$entity->isAlive() || $entity->isFlaggedForDespawn()){
1363 continue;
1364 }
1365
1366 $entity->onCollideWithPlayer($this);
1367 }
1368 }
1369
1370 public function getInAirTicks() : int{
1371 return $this->inAirTicks;
1372 }
1373
1382 public function handleMovement(Vector3 $newPos) : void{
1383 Timings::$playerMove->startTiming();
1384 try{
1385 $this->actuallyHandleMovement($newPos);
1386 }finally{
1387 Timings::$playerMove->stopTiming();
1388 }
1389 }
1390
1391 private function actuallyHandleMovement(Vector3 $newPos) : void{
1392 $this->moveRateLimit--;
1393 if($this->moveRateLimit < 0){
1394 return;
1395 }
1396
1397 $oldPos = $this->location;
1398 $distanceSquared = $newPos->distanceSquared($oldPos);
1399
1400 $revert = false;
1401
1402 if($distanceSquared > 225){ //15 blocks
1403 //TODO: this is probably too big if we process every movement
1404 /* !!! BEWARE YE WHO ENTER HERE !!!
1405 *
1406 * This is NOT an anti-cheat check. It is a safety check.
1407 * Without it hackers can teleport with freedom on their own and cause lots of undesirable behaviour, like
1408 * freezes, lag spikes and memory exhaustion due to sync chunk loading and collision checks across large distances.
1409 * Not only that, but high-latency players can trigger such behaviour innocently.
1410 *
1411 * If you must tamper with this code, be aware that this can cause very nasty results. Do not waste our time
1412 * asking for help if you suffer the consequences of messing with this.
1413 */
1414 $this->logger->debug("Moved too fast (" . sqrt($distanceSquared) . " blocks in 1 movement), reverting movement");
1415 $this->logger->debug("Old position: " . $oldPos->asVector3() . ", new position: " . $newPos);
1416 $revert = true;
1417 }elseif(!$this->getWorld()->isInLoadedTerrain($newPos)){
1418 $revert = true;
1419 $this->nextChunkOrderRun = 0;
1420 }
1421
1422 if(!$revert && $distanceSquared !== 0.0){
1423 $dx = $newPos->x - $oldPos->x;
1424 $dy = $newPos->y - $oldPos->y;
1425 $dz = $newPos->z - $oldPos->z;
1426
1427 $this->move($dx, $dy, $dz);
1428 }
1429
1430 if($revert){
1431 $this->revertMovement($oldPos);
1432 }
1433 }
1434
1438 protected function processMostRecentMovements() : void{
1439 $now = microtime(true);
1440 $multiplier = $this->lastMovementProcess !== null ? ($now - $this->lastMovementProcess) * 20 : 1;
1441 $exceededRateLimit = $this->moveRateLimit < 0;
1442 $this->moveRateLimit = min(self::MOVE_BACKLOG_SIZE, max(0, $this->moveRateLimit) + self::MOVES_PER_TICK * $multiplier);
1443 $this->lastMovementProcess = $now;
1444
1445 $from = clone $this->lastLocation;
1446 $to = clone $this->location;
1447
1448 $delta = $to->distanceSquared($from);
1449 $deltaAngle = abs($this->lastLocation->yaw - $to->yaw) + abs($this->lastLocation->pitch - $to->pitch);
1450
1451 if($delta > 0.0001 || $deltaAngle > 1.0){
1452 if(PlayerMoveEvent::hasHandlers()){
1453 $ev = new PlayerMoveEvent($this, $from, $to);
1454
1455 $ev->call();
1456
1457 if($ev->isCancelled()){
1458 $this->revertMovement($from);
1459 return;
1460 }
1461
1462 if($to->distanceSquared($ev->getTo()) > 0.01){ //If plugins modify the destination
1463 $this->teleport($ev->getTo());
1464 return;
1465 }
1466 }
1467
1468 $this->lastLocation = $to;
1469 $this->broadcastMovement();
1470
1471 $horizontalDistanceTravelled = sqrt((($from->x - $to->x) ** 2) + (($from->z - $to->z) ** 2));
1472 if($horizontalDistanceTravelled > 0){
1473 //TODO: check for swimming
1474 if($this->isSprinting()){
1475 $this->hungerManager->exhaust(0.01 * $horizontalDistanceTravelled, EntityExhaustEvent::CAUSE_SPRINTING);
1476 }else{
1477 $this->hungerManager->exhaust(0.0, EntityExhaustEvent::CAUSE_WALKING);
1478 }
1479
1480 if($this->nextChunkOrderRun > 20){
1481 $this->nextChunkOrderRun = 20;
1482 }
1483 }
1484 }
1485
1486 if($exceededRateLimit){ //client and server positions will be out of sync if this happens
1487 $this->logger->debug("Exceeded movement rate limit, forcing to last accepted position");
1488 $this->sendPosition($this->location, $this->location->getYaw(), $this->location->getPitch(), MovePlayerPacket::MODE_RESET);
1489 }
1490 }
1491
1492 protected function revertMovement(Location $from) : void{
1493 $this->setPosition($from);
1494 $this->sendPosition($from, $from->yaw, $from->pitch, MovePlayerPacket::MODE_RESET);
1495 }
1496
1497 protected function calculateFallDamage(float $fallDistance) : float{
1498 return $this->flying ? 0 : parent::calculateFallDamage($fallDistance);
1499 }
1500
1501 public function jump() : void{
1502 (new PlayerJumpEvent($this))->call();
1503 parent::jump();
1504 }
1505
1506 public function setMotion(Vector3 $motion) : bool{
1507 if(parent::setMotion($motion)){
1508 $this->broadcastMotion();
1509 $this->getNetworkSession()->sendDataPacket(SetActorMotionPacket::create($this->id, $motion, tick: 0));
1510
1511 return true;
1512 }
1513 return false;
1514 }
1515
1516 protected function updateMovement(bool $teleport = false) : void{
1517
1518 }
1519
1520 protected function tryChangeMovement() : void{
1521
1522 }
1523
1524 public function onUpdate(int $currentTick) : bool{
1525 $tickDiff = $currentTick - $this->lastUpdate;
1526
1527 if($tickDiff <= 0){
1528 return true;
1529 }
1530
1531 $this->messageCounter = 2;
1532
1533 $this->lastUpdate = $currentTick;
1534
1535 if($this->justCreated){
1536 $this->onFirstUpdate($currentTick);
1537 }
1538
1539 if(!$this->isAlive() && $this->spawned){
1540 $this->onDeathUpdate($tickDiff);
1541 return true;
1542 }
1543
1544 $this->timings->startTiming();
1545
1546 if($this->spawned){
1547 Timings::$playerMove->startTiming();
1548 $this->processMostRecentMovements();
1549 $this->motion = Vector3::zero(); //TODO: HACK! (Fixes player knockback being messed up)
1550 if($this->onGround){
1551 $this->inAirTicks = 0;
1552 }else{
1553 $this->inAirTicks += $tickDiff;
1554 }
1555 Timings::$playerMove->stopTiming();
1556
1557 Timings::$entityBaseTick->startTiming();
1558 $this->entityBaseTick($tickDiff);
1559 Timings::$entityBaseTick->stopTiming();
1560
1561 if($this->isCreative() && $this->fireTicks > 1){
1562 $this->fireTicks = 1;
1563 }
1564
1565 if(!$this->isSpectator() && $this->isAlive()){
1566 Timings::$playerCheckNearEntities->startTiming();
1567 $this->checkNearEntities();
1568 Timings::$playerCheckNearEntities->stopTiming();
1569 }
1570
1571 if($this->blockBreakHandler !== null && !$this->blockBreakHandler->update()){
1572 $this->blockBreakHandler = null;
1573 }
1574 }
1575
1576 $this->timings->stopTiming();
1577
1578 return true;
1579 }
1580
1581 public function canEat() : bool{
1582 return $this->isCreative() || parent::canEat();
1583 }
1584
1585 public function canBreathe() : bool{
1586 return $this->isCreative() || parent::canBreathe();
1587 }
1588
1594 public function canInteract(Vector3 $pos, float $maxDistance, float $maxDiff = M_SQRT3 / 2) : bool{
1595 $eyePos = $this->getEyePos();
1596 if($eyePos->distanceSquared($pos) > $maxDistance ** 2){
1597 return false;
1598 }
1599
1600 $dV = $this->getDirectionVector();
1601 $eyeDot = $dV->dot($eyePos);
1602 $targetDot = $dV->dot($pos);
1603 return ($targetDot - $eyeDot) >= -$maxDiff;
1604 }
1605
1610 public function chat(string $message) : bool{
1611 $this->removeCurrentWindow();
1612
1613 if($this->messageCounter <= 0){
1614 //the check below would take care of this (0 * (maxlen + 1) = 0), but it's better be explicit
1615 return false;
1616 }
1617
1618 //Fast length check, to make sure we don't get hung trying to explode MBs of string ...
1619 $maxTotalLength = $this->messageCounter * (self::MAX_CHAT_BYTE_LENGTH + 1);
1620 if(strlen($message) > $maxTotalLength){
1621 return false;
1622 }
1623
1624 $message = TextFormat::clean($message, false);
1625 foreach(explode("\n", $message, $this->messageCounter + 1) as $messagePart){
1626 if(trim($messagePart) !== "" && strlen($messagePart) <= self::MAX_CHAT_BYTE_LENGTH && mb_strlen($messagePart, 'UTF-8') <= self::MAX_CHAT_CHAR_LENGTH && $this->messageCounter-- > 0){
1627 if(str_starts_with($messagePart, './')){
1628 $messagePart = substr($messagePart, 1);
1629 }
1630
1631 if(str_starts_with($messagePart, "/")){
1632 Timings::$playerCommand->startTiming();
1633 $this->server->dispatchCommand($this, substr($messagePart, 1));
1634 Timings::$playerCommand->stopTiming();
1635 }else{
1636 $ev = new PlayerChatEvent($this, $messagePart, $this->server->getBroadcastChannelSubscribers(Server::BROADCAST_CHANNEL_USERS), new StandardChatFormatter());
1637 $ev->call();
1638 if(!$ev->isCancelled()){
1639 $this->server->broadcastMessage($ev->getFormatter()->format($ev->getPlayer()->getDisplayName(), $ev->getMessage()), $ev->getRecipients());
1640 }
1641 }
1642 }
1643 }
1644
1645 return true;
1646 }
1647
1648 public function selectHotbarSlot(int $hotbarSlot) : bool{
1649 if(!$this->hotbar->isHotbarSlot($hotbarSlot)){ //TODO: exception here?
1650 return false;
1651 }
1652 if($hotbarSlot === $this->hotbar->getSelectedIndex()){
1653 return true;
1654 }
1655
1656 $ev = new PlayerItemHeldEvent($this, $this->inventory->getItem($hotbarSlot), $hotbarSlot);
1657 $ev->call();
1658 if($ev->isCancelled()){
1659 return false;
1660 }
1661
1662 $this->hotbar->setSelectedIndex($hotbarSlot);
1663 $this->setUsingItem(false);
1664
1665 return true;
1666 }
1667
1671 private function returnItemsFromAction(Item $oldHeldItem, Item $newHeldItem, array $extraReturnedItems) : void{
1672 $heldItemChanged = false;
1673
1674 if(!$newHeldItem->equalsExact($oldHeldItem) && $oldHeldItem->equalsExact($this->getMainHandItem())){
1675 //determine if the item was changed in some meaningful way, or just damaged/changed count
1676 //if it was really changed we always need to set it, whether we have finite resources or not
1677 $newReplica = clone $oldHeldItem;
1678 $newReplica->setCount($newHeldItem->getCount());
1679 if($newReplica instanceof Durable && $newHeldItem instanceof Durable){
1680 $newDamage = $newHeldItem->getDamage();
1681 if($newDamage >= 0 && $newDamage <= $newReplica->getMaxDurability()){
1682 $newReplica->setDamage($newDamage);
1683 }
1684 }
1685 $damagedOrDeducted = $newReplica->equalsExact($newHeldItem);
1686
1687 if(!$damagedOrDeducted || $this->hasFiniteResources()){
1688 if($newHeldItem instanceof Durable && $newHeldItem->isBroken()){
1689 $this->broadcastSound(new ItemBreakSound());
1690 }
1691 $this->setMainHandItem($newHeldItem);
1692 $heldItemChanged = true;
1693 }
1694 }
1695
1696 if(!$heldItemChanged){
1697 $newHeldItem = $oldHeldItem;
1698 }
1699
1700 if($heldItemChanged && count($extraReturnedItems) > 0 && $newHeldItem->isNull()){
1701 $this->setMainHandItem(array_shift($extraReturnedItems));
1702 }
1703 foreach($this->inventory->addItem(...$extraReturnedItems) as $drop){
1704 //TODO: we can't generate a transaction for this since the items aren't coming from an inventory :(
1705 $ev = new PlayerDropItemEvent($this, $drop);
1706 if($this->isSpectator()){
1707 $ev->cancel();
1708 }
1709 $ev->call();
1710 if(!$ev->isCancelled()){
1711 $this->dropItem($drop);
1712 }
1713 }
1714 }
1715
1721 public function useHeldItem() : bool{
1722 $directionVector = $this->getDirectionVector();
1723 $item = $this->getMainHandItem();
1724 $oldItem = clone $item;
1725
1726 $ev = new PlayerItemUseEvent($this, $item, $directionVector);
1727 if($this->hasItemCooldown($item) || $this->isSpectator()){
1728 $ev->cancel();
1729 }
1730
1731 $ev->call();
1732
1733 if($ev->isCancelled()){
1734 return false;
1735 }
1736
1737 $returnedItems = [];
1738 $result = $item->onClickAir($this, $directionVector, $returnedItems);
1739 if($result === ItemUseResult::FAIL){
1740 return false;
1741 }
1742
1743 $this->resetItemCooldown($oldItem);
1744 $this->returnItemsFromAction($oldItem, $item, $returnedItems);
1745
1746 $this->setUsingItem($item instanceof Releasable && $item->canStartUsingItem($this));
1747
1748 return true;
1749 }
1750
1756 public function consumeHeldItem() : bool{
1757 $slot = $this->getMainHandItem();
1758 if($slot instanceof ConsumableItem){
1759 $oldItem = clone $slot;
1760
1761 $ev = new PlayerItemConsumeEvent($this, $slot);
1762 if($this->hasItemCooldown($slot)){
1763 $ev->cancel();
1764 }
1765 $ev->call();
1766
1767 if($ev->isCancelled() || !$this->consumeObject($slot)){
1768 return false;
1769 }
1770
1771 $this->setUsingItem(false);
1772 $this->resetItemCooldown($oldItem);
1773
1774 $slot->pop();
1775 $this->returnItemsFromAction($oldItem, $slot, [$slot->getResidue()]);
1776
1777 return true;
1778 }
1779
1780 return false;
1781 }
1782
1788 public function releaseHeldItem() : bool{
1789 try{
1790 $item = $this->getMainHandItem();
1791 if(!$this->isUsingItem() || $this->hasItemCooldown($item)){
1792 return false;
1793 }
1794
1795 $oldItem = clone $item;
1796
1797 $returnedItems = [];
1798 $result = $item->onReleaseUsing($this, $returnedItems);
1799 if($result === ItemUseResult::SUCCESS){
1800 $this->resetItemCooldown($oldItem);
1801 $this->returnItemsFromAction($oldItem, $item, $returnedItems);
1802 return true;
1803 }
1804
1805 return false;
1806 }finally{
1807 $this->setUsingItem(false);
1808 }
1809 }
1810
1811 public function pickBlock(Vector3 $pos, bool $addTileNBT) : bool{
1812 $block = $this->getWorld()->getBlock($pos);
1813 if($block instanceof UnknownBlock){
1814 return true;
1815 }
1816
1817 $item = $block->getPickedItem($addTileNBT);
1818
1819 $ev = new PlayerBlockPickEvent($this, $block, $item);
1820 $existingSlot = $this->inventory->first($item);
1821 if($existingSlot === -1 && $this->hasFiniteResources()){
1822 $ev->cancel();
1823 }
1824 $ev->call();
1825
1826 if(!$ev->isCancelled()){
1827 $this->equipOrAddPickedItem($existingSlot, $item);
1828 }
1829
1830 return true;
1831 }
1832
1833 public function pickEntity(int $entityId) : bool{
1834 $entity = $this->getWorld()->getEntity($entityId);
1835 if($entity === null){
1836 return true;
1837 }
1838
1839 $item = $entity->getPickedItem();
1840 if($item === null){
1841 return true;
1842 }
1843
1844 $ev = new PlayerEntityPickEvent($this, $entity, $item);
1845 $existingSlot = $this->inventory->first($item);
1846 if($existingSlot === -1 && ($this->hasFiniteResources() || $this->isSpectator())){
1847 $ev->cancel();
1848 }
1849 $ev->call();
1850
1851 if(!$ev->isCancelled()){
1852 $this->equipOrAddPickedItem($existingSlot, $item);
1853 }
1854
1855 return true;
1856 }
1857
1858 private function equipOrAddPickedItem(int $existingSlot, Item $item) : void{
1859 if($existingSlot !== -1){
1860 if($existingSlot < $this->hotbar->getSize()){
1861 $this->hotbar->setSelectedIndex($existingSlot);
1862 }else{
1863 $this->inventory->swap($this->hotbar->getSelectedIndex(), $existingSlot);
1864 }
1865 }else{
1866 $firstEmpty = $this->inventory->firstEmpty();
1867 if($firstEmpty === -1){ //full inventory
1868 $this->setMainHandItem($item);
1869 }elseif($firstEmpty < $this->hotbar->getSize()){
1870 $this->inventory->setItem($firstEmpty, $item);
1871 $this->hotbar->setSelectedIndex($firstEmpty);
1872 }else{
1873 $this->inventory->swap($this->hotbar->getSelectedIndex(), $firstEmpty);
1874 $this->setMainHandItem($item);
1875 }
1876 }
1877 }
1878
1884 public function attackBlock(Vector3 $pos, Facing $face) : bool{
1885 if($pos->distanceSquared($this->location) > 10000){
1886 return false; //TODO: maybe this should throw an exception instead?
1887 }
1888
1889 $target = $this->getWorld()->getBlock($pos);
1890
1891 $ev = new PlayerInteractEvent($this, $this->getMainHandItem(), $target, null, $face, PlayerInteractEvent::LEFT_CLICK_BLOCK);
1892 if($this->isSpectator()){
1893 $ev->cancel();
1894 }
1895 $ev->call();
1896 if($ev->isCancelled()){
1897 return false;
1898 }
1899 $this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
1900 if($target->onAttack($this->getMainHandItem(), $face, $this)){
1901 return true;
1902 }
1903
1904 $block = $target->getSide($face);
1905 if($block->hasTypeTag(BlockTypeTags::FIRE)){
1906 $this->getWorld()->setBlock($block->getPosition(), VanillaBlocks::AIR());
1907 $this->getWorld()->addSound($block->getPosition()->add(0.5, 0.5, 0.5), new FireExtinguishSound());
1908 return true;
1909 }
1910
1911 if(!$this->isCreative() && !$target->getBreakInfo()->breaksInstantly()){
1912 $this->blockBreakHandler = new SurvivalBlockBreakHandler($this, $pos, $target, $face, 16);
1913 }
1914
1915 return true;
1916 }
1917
1918 public function continueBreakBlock(Vector3 $pos, Facing $face) : void{
1919 if($this->blockBreakHandler !== null && $this->blockBreakHandler->getBlockPos()->distanceSquared($pos) < 0.0001){
1920 $this->blockBreakHandler->setTargetedFace($face);
1921 }
1922 }
1923
1924 public function stopBreakBlock(Vector3 $pos) : void{
1925 if($this->blockBreakHandler !== null && $this->blockBreakHandler->getBlockPos()->distanceSquared($pos) < 0.0001){
1926 $this->blockBreakHandler = null;
1927 }
1928 }
1929
1935 public function breakBlock(Vector3 $pos) : bool{
1936 $this->removeCurrentWindow();
1937
1938 if($this->canInteract($pos->add(0.5, 0.5, 0.5), $this->isCreative() ? self::MAX_REACH_DISTANCE_CREATIVE : self::MAX_REACH_DISTANCE_SURVIVAL)){
1939 $this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
1940 $this->stopBreakBlock($pos);
1941 $item = $this->getMainHandItem();
1942 $oldItem = clone $item;
1943 $returnedItems = [];
1944 if($this->getWorld()->useBreakOn($pos, $item, $this, true, $returnedItems)){
1945 $this->returnItemsFromAction($oldItem, $item, $returnedItems);
1946 $this->hungerManager->exhaust(0.005, EntityExhaustEvent::CAUSE_MINING);
1947 return true;
1948 }
1949 }else{
1950 $this->logger->debug("Cancelled block break at $pos due to not currently being interactable");
1951 }
1952
1953 return false;
1954 }
1955
1961 public function interactBlock(Vector3 $pos, Facing $face, Vector3 $clickOffset) : bool{
1962 $this->setUsingItem(false);
1963
1964 if($this->canInteract($pos->add(0.5, 0.5, 0.5), $this->isCreative() ? self::MAX_REACH_DISTANCE_CREATIVE : self::MAX_REACH_DISTANCE_SURVIVAL)){
1965 $this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
1966 $item = $this->getMainHandItem(); //this is a copy of the real item
1967 $oldItem = clone $item;
1968 $returnedItems = [];
1969 if($this->getWorld()->useItemOn($pos, $item, $face, $clickOffset, $this, true, $returnedItems)){
1970 $this->returnItemsFromAction($oldItem, $item, $returnedItems);
1971 return true;
1972 }
1973 }else{
1974 $this->logger->debug("Cancelled interaction of block at $pos due to not currently being interactable");
1975 }
1976
1977 return false;
1978 }
1979
1986 public function attackEntity(Entity $entity) : bool{
1987 if(!$entity->isAlive()){
1988 return false;
1989 }
1990 if($entity instanceof ItemEntity || $entity instanceof Arrow){
1991 $this->logger->debug("Attempted to attack non-attackable entity " . get_class($entity));
1992 return false;
1993 }
1994
1995 $heldItem = $this->getMainHandItem();
1996 $oldItem = clone $heldItem;
1997
1998 $ev = new EntityDamageByEntityEvent($this, $entity, EntityDamageEvent::CAUSE_ENTITY_ATTACK, $heldItem->getAttackPoints());
1999 if(!$this->canInteract($entity->getLocation(), self::MAX_REACH_DISTANCE_ENTITY_INTERACTION)){
2000 $this->logger->debug("Cancelled attack of entity " . $entity->getId() . " due to not currently being interactable");
2001 $ev->cancel();
2002 }elseif($this->isSpectator() || ($entity instanceof Player && !$this->server->getConfigGroup()->getConfigBool(ServerProperties::PVP))){
2003 $ev->cancel();
2004 }
2005
2006 $meleeEnchantmentDamage = 0;
2008 $meleeEnchantments = [];
2009 foreach($heldItem->getEnchantments() as $enchantment){
2010 $type = $enchantment->getType();
2011 if($type instanceof MeleeWeaponEnchantment && $type->isApplicableTo($entity)){
2012 $meleeEnchantmentDamage += $type->getDamageBonus($enchantment->getLevel());
2013 $meleeEnchantments[] = $enchantment;
2014 }
2015 }
2016 $ev->setModifier($meleeEnchantmentDamage, EntityDamageEvent::MODIFIER_WEAPON_ENCHANTMENTS);
2017
2018 if(!$this->isSprinting() && !$this->isFlying() && $this->fallDistance > 0 && !$this->effectManager->has(VanillaEffects::BLINDNESS()) && !$this->isUnderwater()){
2019 $ev->setModifier($ev->getFinalDamage() / 2, EntityDamageEvent::MODIFIER_CRITICAL);
2020 }
2021
2022 $entity->attack($ev);
2023 $this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
2024
2025 $soundPos = $entity->getPosition()->add(0, $entity->size->getHeight() / 2, 0);
2026 if($ev->isCancelled()){
2027 $this->getWorld()->addSound($soundPos, new EntityAttackNoDamageSound());
2028 return false;
2029 }
2030 $this->getWorld()->addSound($soundPos, new EntityAttackSound());
2031
2032 if($ev->getModifier(EntityDamageEvent::MODIFIER_CRITICAL) > 0 && $entity instanceof Living){
2033 $entity->broadcastAnimation(new CriticalHitAnimation($entity));
2034 }
2035 if($ev->getModifier(EntityDamageEvent::MODIFIER_WEAPON_ENCHANTMENTS) > 0 && $entity instanceof Living){
2036 $entity->broadcastAnimation(new MagicHitAnimation($entity));
2037 }
2038
2039 foreach($meleeEnchantments as $enchantment){
2040 $type = $enchantment->getType();
2041 assert($type instanceof MeleeWeaponEnchantment);
2042 $type->onPostAttack($this, $entity, $enchantment->getLevel());
2043 }
2044
2045 if($this->isAlive()){
2046 //reactive damage like thorns might cause us to be killed by attacking another mob, which
2047 //would mean we'd already have dropped the inventory by the time we reached here
2048 $returnedItems = [];
2049 $heldItem->onAttackEntity($entity, $returnedItems);
2050 $this->returnItemsFromAction($oldItem, $heldItem, $returnedItems);
2051
2052 $this->hungerManager->exhaust(0.1, EntityExhaustEvent::CAUSE_ATTACK);
2053 }
2054
2055 return true;
2056 }
2057
2062 public function missSwing() : void{
2063 $ev = new PlayerMissSwingEvent($this);
2064 $ev->call();
2065 if(!$ev->isCancelled()){
2066 $this->broadcastSound(new EntityAttackNoDamageSound());
2067 $this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
2068 }
2069 }
2070
2074 public function interactEntity(Entity $entity, Vector3 $clickPos) : bool{
2075 $ev = new PlayerEntityInteractEvent($this, $entity, $clickPos);
2076
2077 if(!$this->canInteract($entity->getLocation(), self::MAX_REACH_DISTANCE_ENTITY_INTERACTION)){
2078 $this->logger->debug("Cancelled interaction with entity " . $entity->getId() . " due to not currently being interactable");
2079 $ev->cancel();
2080 }
2081
2082 $ev->call();
2083
2084 $item = $this->getMainHandItem();
2085 $oldItem = clone $item;
2086 if(!$ev->isCancelled()){
2087 if($item->onInteractEntity($this, $entity, $clickPos)){
2088 if($this->hasFiniteResources() && !$item->equalsExact($oldItem) && $oldItem->equalsExact($this->getMainHandItem())){
2089 if($item instanceof Durable && $item->isBroken()){
2090 $this->broadcastSound(new ItemBreakSound());
2091 }
2092 $this->setMainHandItem($item);
2093 }
2094 }
2095 return $entity->onInteract($this, $clickPos);
2096 }
2097 return false;
2098 }
2099
2100 public function toggleSprint(bool $sprint) : bool{
2101 if($sprint === $this->sprinting){
2102 return true;
2103 }
2104 $ev = new PlayerToggleSprintEvent($this, $sprint);
2105 $ev->call();
2106 if($ev->isCancelled()){
2107 return false;
2108 }
2109 $this->setSprinting($sprint);
2110 return true;
2111 }
2112
2113 public function toggleSneak(bool $sneak, bool $sneakPressed = true) : bool{
2114 if($sneak === $this->sneaking && $sneakPressed === $this->sneakPressed){
2115 return true;
2116 }
2117 $this->setSneakPressed($sneakPressed);
2118
2119 $ev = new PlayerToggleSneakEvent($this, $sneak, $sneakPressed);
2120 if($sneak === $this->sneaking){
2121 $ev->cancel();
2122 }
2123 $ev->call();
2124
2125 if($ev->isCancelled()){
2126 return false;
2127 }
2128 $this->setSneaking($sneak);
2129 return true;
2130 }
2131
2132 public function toggleFlight(bool $fly) : bool{
2133 if($fly === $this->flying){
2134 return true;
2135 }
2136 $ev = new PlayerToggleFlightEvent($this, $fly);
2137 if(!$this->allowFlight){
2138 $ev->cancel();
2139 }
2140 $ev->call();
2141 if($ev->isCancelled()){
2142 return false;
2143 }
2144 $this->setFlying($fly);
2145 return true;
2146 }
2147
2148 public function toggleGlide(bool $glide) : bool{
2149 if($glide === $this->gliding){
2150 return true;
2151 }
2152 $ev = new PlayerToggleGlideEvent($this, $glide);
2153 $ev->call();
2154 if($ev->isCancelled()){
2155 return false;
2156 }
2157 $this->setGliding($glide);
2158 return true;
2159 }
2160
2161 public function toggleSwim(bool $swim) : bool{
2162 if($swim === $this->swimming){
2163 return true;
2164 }
2165 $ev = new PlayerToggleSwimEvent($this, $swim);
2166 $ev->call();
2167 if($ev->isCancelled()){
2168 return false;
2169 }
2170 $this->setSwimming($swim);
2171 return true;
2172 }
2173
2174 public function emote(string $emoteId) : void{
2175 $currentTick = $this->server->getTick();
2176 if($currentTick - $this->lastEmoteTick > 5){
2177 $this->lastEmoteTick = $currentTick;
2178 $event = new PlayerEmoteEvent($this, $emoteId);
2179 $event->call();
2180 if(!$event->isCancelled()){
2181 $emoteId = $event->getEmoteId();
2182 parent::emote($emoteId);
2183 }
2184 }
2185 }
2186
2190 public function dropItem(Item $item) : void{
2191 $this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
2192 $this->getWorld()->dropItem($this->location->add(0, 1.3, 0), $item, $this->getDirectionVector()->multiply(0.4), 40);
2193 }
2194
2202 public function sendTitle(string $title, string $subtitle = "", int $fadeIn = -1, int $stay = -1, int $fadeOut = -1) : void{
2203 $this->setTitleDuration($fadeIn, $stay, $fadeOut);
2204 if($subtitle !== ""){
2205 $this->sendSubTitle($subtitle);
2206 }
2207 $this->getNetworkSession()->onTitle($title);
2208 }
2209
2213 public function sendSubTitle(string $subtitle) : void{
2214 $this->getNetworkSession()->onSubTitle($subtitle);
2215 }
2216
2220 public function sendActionBarMessage(string $message) : void{
2221 $this->getNetworkSession()->onActionBar($message);
2222 }
2223
2227 public function removeTitles() : void{
2228 $this->getNetworkSession()->onClearTitle();
2229 }
2230
2234 public function resetTitles() : void{
2235 $this->getNetworkSession()->onResetTitleOptions();
2236 }
2237
2245 public function setTitleDuration(int $fadeIn, int $stay, int $fadeOut) : void{
2246 if($fadeIn >= 0 && $stay >= 0 && $fadeOut >= 0){
2247 $this->getNetworkSession()->onTitleDuration($fadeIn, $stay, $fadeOut);
2248 }
2249 }
2250
2254 public function sendMessage(Translatable|string $message) : void{
2255 $this->getNetworkSession()->onChatMessage($message);
2256 }
2257
2258 public function sendJukeboxPopup(Translatable|string $message) : void{
2259 $this->getNetworkSession()->onJukeboxPopup($message);
2260 }
2261
2267 public function sendPopup(string $message) : void{
2268 $this->getNetworkSession()->onPopup($message);
2269 }
2270
2271 public function sendTip(string $message) : void{
2272 $this->getNetworkSession()->onTip($message);
2273 }
2274
2278 public function sendToastNotification(string $title, string $body) : void{
2279 $this->getNetworkSession()->onToastNotification($title, $body);
2280 }
2281
2287 public function sendForm(Form $form) : void{
2288 $id = $this->formIdCounter++;
2289 if($this->getNetworkSession()->onFormSent($id, $form)){
2290 $this->forms[$id] = $form;
2291 }
2292 }
2293
2294 public function onFormSubmit(int $formId, mixed $responseData) : bool{
2295 if(!isset($this->forms[$formId])){
2296 $this->logger->debug("Got unexpected response for form $formId");
2297 return false;
2298 }
2299
2300 try{
2301 $this->forms[$formId]->handleResponse($this, $responseData);
2302 }catch(FormValidationException $e){
2303 $this->logger->critical("Failed to validate form " . get_class($this->forms[$formId]) . ": " . $e->getMessage());
2304 $this->logger->logException($e);
2305 }finally{
2306 unset($this->forms[$formId]);
2307 }
2308
2309 return true;
2310 }
2311
2315 public function closeAllForms() : void{
2316 $this->getNetworkSession()->onCloseAllForms();
2317 }
2318
2328 public function transfer(string $address, int $port = 19132, Translatable|string|null $message = null) : bool{
2329 $ev = new PlayerTransferEvent($this, $address, $port, $message ?? KnownTranslationFactory::pocketmine_disconnect_transfer());
2330 $ev->call();
2331 if(!$ev->isCancelled()){
2332 $this->getNetworkSession()->transfer($ev->getAddress(), $ev->getPort(), $ev->getMessage());
2333 return true;
2334 }
2335
2336 return false;
2337 }
2338
2346 public function kick(Translatable|string $reason = "", Translatable|string|null $quitMessage = null, Translatable|string|null $disconnectScreenMessage = null) : bool{
2347 $ev = new PlayerKickEvent($this, $reason, $quitMessage ?? $this->getLeaveMessage(), $disconnectScreenMessage);
2348 $ev->call();
2349 if(!$ev->isCancelled()){
2350 $reason = $ev->getDisconnectReason();
2351 if($reason === ""){
2352 $reason = KnownTranslationFactory::disconnectionScreen_noReason();
2353 }
2354 $disconnectScreenMessage = $ev->getDisconnectScreenMessage() ?? $reason;
2355 if($disconnectScreenMessage === ""){
2356 $disconnectScreenMessage = KnownTranslationFactory::disconnectionScreen_noReason();
2357 }
2358 $this->disconnect($reason, $ev->getQuitMessage(), $disconnectScreenMessage);
2359
2360 return true;
2361 }
2362
2363 return false;
2364 }
2365
2379 public function disconnect(Translatable|string $reason, Translatable|string|null $quitMessage = null, Translatable|string|null $disconnectScreenMessage = null) : void{
2380 if(!$this->isConnected()){
2381 return;
2382 }
2383
2384 $this->getNetworkSession()->onPlayerDestroyed($reason, $disconnectScreenMessage ?? $reason);
2385 $this->onPostDisconnect($reason, $quitMessage);
2386 }
2387
2395 public function onPostDisconnect(Translatable|string $reason, Translatable|string|null $quitMessage) : void{
2396 if($this->isConnected()){
2397 throw new \LogicException("Player is still connected");
2398 }
2399
2400 //prevent the player receiving their own disconnect message
2401 $this->server->unsubscribeFromAllBroadcastChannels($this);
2402
2403 $this->removeCurrentWindow();
2404
2405 $ev = new PlayerQuitEvent($this, $quitMessage ?? $this->getLeaveMessage(), $reason);
2406 $ev->call();
2407 if(($quitMessage = $ev->getQuitMessage()) !== ""){
2408 $this->server->broadcastMessage($quitMessage);
2409 }
2410 $this->save();
2411
2412 $this->spawned = false;
2413
2414 $this->stopSleep();
2415 $this->blockBreakHandler = null;
2416 $this->despawnFromAll();
2417
2418 $this->server->removeOnlinePlayer($this);
2419
2420 foreach($this->server->getOnlinePlayers() as $player){
2421 if(!$player->canSee($this)){
2422 $player->showPlayer($this);
2423 }
2424 }
2425 $this->hiddenPlayers = [];
2426
2427 if($this->location->isValid()){
2428 foreach($this->usedChunks as $index => $status){
2429 World::getXZ($index, $chunkX, $chunkZ);
2430 $this->unloadChunk($chunkX, $chunkZ);
2431 }
2432 }
2433 if(count($this->usedChunks) !== 0){
2434 throw new AssumptionFailedError("Previous loop should have cleared this array");
2435 }
2436 $this->loadQueue = [];
2437
2438 $this->removeCurrentWindow();
2439 $this->removePermanentWindows();
2440
2441 $this->perm->getPermissionRecalculationCallbacks()->clear();
2442
2443 $this->flagForDespawn();
2444 }
2445
2446 protected function onDispose() : void{
2447 $this->disconnect("Player destroyed");
2448 $this->cursorInventory->removeAllWindows();
2449 $this->craftingGrid->removeAllWindows();
2450 parent::onDispose();
2451 }
2452
2453 protected function destroyCycles() : void{
2454 $this->networkSession = null;
2455 $this->spawnPosition = null;
2456 $this->deathPosition = null;
2457 $this->blockBreakHandler = null;
2458 parent::destroyCycles();
2459 }
2460
2464 public function __debugInfo() : array{
2465 return [];
2466 }
2467
2468 public function __destruct(){
2469 parent::__destruct();
2470 $this->logger->debug("Destroyed by garbage collector");
2471 }
2472
2473 public function canSaveWithChunk() : bool{
2474 return false;
2475 }
2476
2477 public function setCanSaveWithChunk(bool $value) : void{
2478 throw new \BadMethodCallException("Players can't be saved with chunks");
2479 }
2480
2481 public function getSaveData() : CompoundTag{
2482 $nbt = $this->saveNBT();
2483
2484 $nbt->setString(self::TAG_LAST_KNOWN_XUID, $this->xuid);
2485
2486 if($this->location->isValid()){
2487 $nbt->setString(self::TAG_LEVEL, $this->getWorld()->getFolderName());
2488 }
2489
2490 if($this->hasValidCustomSpawn()){
2491 $spawn = $this->getSpawn();
2492 $nbt->setString(self::TAG_SPAWN_WORLD, $spawn->getWorld()->getFolderName());
2493 $nbt->setInt(self::TAG_SPAWN_X, $spawn->getFloorX());
2494 $nbt->setInt(self::TAG_SPAWN_Y, $spawn->getFloorY());
2495 $nbt->setInt(self::TAG_SPAWN_Z, $spawn->getFloorZ());
2496 }
2497
2498 if($this->deathPosition !== null && $this->deathPosition->isValid()){
2499 $nbt->setString(self::TAG_DEATH_WORLD, $this->deathPosition->getWorld()->getFolderName());
2500 $nbt->setInt(self::TAG_DEATH_X, $this->deathPosition->getFloorX());
2501 $nbt->setInt(self::TAG_DEATH_Y, $this->deathPosition->getFloorY());
2502 $nbt->setInt(self::TAG_DEATH_Z, $this->deathPosition->getFloorZ());
2503 }
2504
2505 $nbt->setInt(self::TAG_GAME_MODE, GameModeIdMap::getInstance()->toId($this->gamemode));
2506 $nbt->setLong(self::TAG_FIRST_PLAYED, (int) $this->firstPlayed->format('Uv'));
2507 $nbt->setLong(self::TAG_LAST_PLAYED, (int) floor(microtime(true) * 1000));
2508
2509 return $nbt;
2510 }
2511
2515 public function save() : void{
2516 $this->server->saveOfflinePlayerData($this->username, $this->getSaveData());
2517 }
2518
2519 protected function onDeath() : void{
2520 //Crafting grid must always be evacuated even if keep-inventory is true. This dumps the contents into the
2521 //main inventory and drops the rest on the ground.
2522 $this->removeCurrentWindow();
2523
2524 $this->setDeathPosition($this->getPosition());
2525
2526 $ev = new PlayerDeathEvent($this, $this->getDrops(), $this->getXpDropAmount(), null);
2527 $ev->call();
2528
2529 if(!$ev->getKeepInventory()){
2530 foreach($ev->getDrops() as $item){
2531 $this->getWorld()->dropItem($this->location, $item);
2532 }
2533
2534 $this->hotbar->setSelectedIndex(0);
2535 $clearInventory = fn(Inventory $inventory) => $inventory->setContents(array_filter($inventory->getContents(), fn(Item $item) => $item->keepOnDeath()));
2536 $clearInventory($this->inventory);
2537 $clearInventory($this->armorInventory);
2538 $clearInventory($this->offHandInventory);
2539 }
2540
2541 if(!$ev->getKeepXp()){
2542 $this->getWorld()->dropExperience($this->location, $ev->getXpDropAmount());
2543 $this->xpManager->setXpAndProgress(0, 0.0);
2544 }
2545
2546 if($ev->getDeathMessage() !== ""){
2547 $this->server->broadcastMessage($ev->getDeathMessage());
2548 }
2549
2550 $this->startDeathAnimation();
2551
2552 $this->getNetworkSession()->onServerDeath($ev->getDeathScreenMessage());
2553 }
2554
2555 protected function onDeathUpdate(int $tickDiff) : bool{
2556 parent::onDeathUpdate($tickDiff);
2557 return false; //never flag players for despawn
2558 }
2559
2560 public function respawn() : void{
2561 if($this->server->isHardcore()){
2562 if($this->kick(KnownTranslationFactory::pocketmine_disconnect_ban(KnownTranslationFactory::pocketmine_disconnect_ban_hardcore()))){ //this allows plugins to prevent the ban by cancelling PlayerKickEvent
2563 $this->server->getNameBans()->addBan($this->getName(), "Died in hardcore mode");
2564 }
2565 return;
2566 }
2567
2568 $this->actuallyRespawn();
2569 }
2570
2571 protected function actuallyRespawn() : void{
2572 if($this->respawnLocked){
2573 return;
2574 }
2575 $this->respawnLocked = true;
2576
2577 $this->logger->debug("Waiting for safe respawn position to be located");
2578 $spawn = $this->getSpawn();
2579 $spawn->getWorld()->requestSafeSpawn($spawn)->onCompletion(
2580 function(Position $safeSpawn) : void{
2581 if(!$this->isConnected()){
2582 return;
2583 }
2584 $this->logger->debug("Respawn position located, completing respawn");
2585 $ev = new PlayerRespawnEvent($this, $safeSpawn);
2586 $spawnPosition = $ev->getRespawnPosition();
2587 $spawnBlock = $spawnPosition->getWorld()->getBlock($spawnPosition);
2588 if($spawnBlock instanceof RespawnAnchor){
2589 if($spawnBlock->getCharges() > 0){
2590 $spawnPosition->getWorld()->setBlock($spawnPosition, $spawnBlock->setCharges($spawnBlock->getCharges() - 1));
2591 $spawnPosition->getWorld()->addSound($spawnPosition, new RespawnAnchorDepleteSound());
2592 }else{
2593 $defaultSpawn = $this->server->getWorldManager()->getDefaultWorld()?->getSpawnLocation();
2594 if($defaultSpawn !== null){
2595 $this->setSpawn($defaultSpawn);
2596 $ev->setRespawnPosition($defaultSpawn);
2597 $this->sendMessage(KnownTranslationFactory::tile_respawn_anchor_notValid()->prefix(TextFormat::GRAY));
2598 }
2599 }
2600 }
2601 $ev->call();
2602
2603 $realSpawn = Position::fromObject($ev->getRespawnPosition()->add(0.5, 0, 0.5), $ev->getRespawnPosition()->getWorld());
2604 $this->teleport($realSpawn);
2605
2606 $this->setSprinting(false);
2607 $this->setSneaking(false);
2608 $this->setFlying(false);
2609
2610 $this->extinguish(EntityExtinguishEvent::CAUSE_RESPAWN);
2611 $this->setAirSupplyTicks($this->getMaxAirSupplyTicks());
2612 $this->deadTicks = 0;
2613 $this->noDamageTicks = 60;
2614
2615 $this->effectManager->clear();
2616 $this->setHealth($this->getMaxHealth());
2617
2618 foreach($this->attributeMap->getAll() as $attr){
2619 if($attr->getId() === Attribute::EXPERIENCE || $attr->getId() === Attribute::EXPERIENCE_LEVEL){ //we have already reset both of those if needed when the player died
2620 continue;
2621 }
2622 $attr->resetToDefault();
2623 }
2624
2625 $this->spawnToAll();
2626 $this->scheduleUpdate();
2627
2628 $this->getNetworkSession()->onServerRespawn();
2629 $this->respawnLocked = false;
2630 },
2631 function() : void{
2632 if($this->isConnected()){
2633 $this->getNetworkSession()->disconnectWithError(KnownTranslationFactory::pocketmine_disconnect_error_respawn());
2634 }
2635 }
2636 );
2637 }
2638
2639 protected function applyPostDamageEffects(EntityDamageEvent $source) : void{
2640 parent::applyPostDamageEffects($source);
2641
2642 $this->hungerManager->exhaust(0.1, EntityExhaustEvent::CAUSE_DAMAGE);
2643 }
2644
2645 public function attack(EntityDamageEvent $source) : void{
2646 if(!$this->isAlive()){
2647 return;
2648 }
2649
2650 if($this->isCreative()
2651 && $source->getCause() !== EntityDamageEvent::CAUSE_SUICIDE
2652 ){
2653 $source->cancel();
2654 }elseif($this->allowFlight && $source->getCause() === EntityDamageEvent::CAUSE_FALL){
2655 $source->cancel();
2656 }
2657
2658 parent::attack($source);
2659 }
2660
2661 protected function syncNetworkData(EntityMetadataCollection $properties) : void{
2662 parent::syncNetworkData($properties);
2663
2664 $properties->setGenericFlag(EntityMetadataFlags::ACTION, $this->startAction > -1);
2665 $properties->setGenericFlag(EntityMetadataFlags::HAS_COLLISION, $this->hasBlockCollision());
2666
2667 $properties->setPlayerFlag(PlayerMetadataFlags::SLEEP, $this->sleeping !== null);
2668 $properties->setBlockPos(EntityMetadataProperties::PLAYER_BED_POSITION, $this->sleeping !== null ? BlockPosition::fromVector3($this->sleeping) : new BlockPosition(0, 0, 0));
2669
2670 if($this->deathPosition !== null && $this->deathPosition->world === $this->location->world){
2671 $properties->setBlockPos(EntityMetadataProperties::PLAYER_DEATH_POSITION, BlockPosition::fromVector3($this->deathPosition));
2672 //TODO: this should be updated when dimensions are implemented
2673 $properties->setInt(EntityMetadataProperties::PLAYER_DEATH_DIMENSION, DimensionIds::OVERWORLD);
2674 $properties->setByte(EntityMetadataProperties::PLAYER_HAS_DIED, 1);
2675 }else{
2676 $properties->setBlockPos(EntityMetadataProperties::PLAYER_DEATH_POSITION, new BlockPosition(0, 0, 0));
2677 $properties->setInt(EntityMetadataProperties::PLAYER_DEATH_DIMENSION, DimensionIds::OVERWORLD);
2678 $properties->setByte(EntityMetadataProperties::PLAYER_HAS_DIED, 0);
2679 }
2680 }
2681
2682 public function sendData(?array $targets, ?array $data = null) : void{
2683 if($targets === null){
2684 $targets = $this->getViewers();
2685 $targets[] = $this;
2686 }
2687 parent::sendData($targets, $data);
2688 }
2689
2690 public function broadcastAnimation(Animation $animation, ?array $targets = null) : void{
2691 if($this->spawned && $targets === null){
2692 $targets = $this->getViewers();
2693 $targets[] = $this;
2694 }
2695 parent::broadcastAnimation($animation, $targets);
2696 }
2697
2698 public function broadcastSound(Sound $sound, ?array $targets = null) : void{
2699 if($this->spawned && $targets === null){
2700 $targets = $this->getViewers();
2701 $targets[] = $this;
2702 }
2703 parent::broadcastSound($sound, $targets);
2704 }
2705
2709 protected function sendPosition(Vector3 $pos, ?float $yaw = null, ?float $pitch = null, int $mode = MovePlayerPacket::MODE_NORMAL) : void{
2710 $this->getNetworkSession()->syncMovement($pos, $yaw, $pitch, $mode);
2711
2712 $this->ySize = 0;
2713 }
2714
2715 public function teleport(Vector3 $pos, ?float $yaw = null, ?float $pitch = null) : bool{
2716 if(parent::teleport($pos, $yaw, $pitch)){
2717
2718 $this->removeCurrentWindow();
2719 $this->stopSleep();
2720
2721 $this->sendPosition($this->location, $this->location->yaw, $this->location->pitch, MovePlayerPacket::MODE_TELEPORT);
2722 $this->broadcastMovement(true);
2723
2724 $this->spawnToAll();
2725
2726 $this->resetFallDistance();
2727 $this->nextChunkOrderRun = 0;
2728 if($this->spawnChunkLoadCount !== -1){
2729 $this->spawnChunkLoadCount = 0;
2730 }
2731 $this->blockBreakHandler = null;
2732
2733 //TODO: workaround for player last pos not getting updated
2734 //Entity::updateMovement() normally handles this, but it's overridden with an empty function in Player
2735 $this->resetLastMovements();
2736
2737 return true;
2738 }
2739
2740 return false;
2741 }
2742
2743 protected function addDefaultWindows() : void{
2744 $this->cursorInventory = new SimpleInventory(1);
2745 $this->craftingGrid = new CraftingGrid(CraftingGrid::SIZE_SMALL);
2746
2747 $this->addPermanentWindows([
2748 new PlayerInventoryWindow($this, $this->inventory, PlayerInventoryWindow::TYPE_INVENTORY),
2749 new PlayerInventoryWindow($this, $this->armorInventory, PlayerInventoryWindow::TYPE_ARMOR),
2750 new PlayerInventoryWindow($this, $this->cursorInventory, PlayerInventoryWindow::TYPE_CURSOR),
2751 new PlayerInventoryWindow($this, $this->offHandInventory, PlayerInventoryWindow::TYPE_OFFHAND),
2752 new PlayerInventoryWindow($this, $this->craftingGrid, PlayerInventoryWindow::TYPE_CRAFTING),
2753 ]);
2754 }
2755
2756 public function getCursorInventory() : Inventory{
2757 return $this->cursorInventory;
2758 }
2759
2760 public function getCraftingGrid() : CraftingGrid{
2761 return $this->craftingGrid;
2762 }
2763
2769 return $this->creativeInventory;
2770 }
2771
2775 public function setCreativeInventory(CreativeInventory $inventory) : void{
2776 $this->creativeInventory = $inventory;
2777 if($this->spawned && $this->isConnected()){
2778 $this->getNetworkSession()->getInvManager()?->syncCreative();
2779 }
2780 }
2781
2786 private function doCloseInventory() : void{
2787 $windowsToClear = [];
2788 $mainInventoryWindow = null;
2789 foreach($this->permanentWindows as $window){
2790 if($window->getType() === PlayerInventoryWindow::TYPE_CRAFTING || $window->getType() === PlayerInventoryWindow::TYPE_CURSOR){
2791 $windowsToClear[] = $window;
2792 }elseif($window->getType() === PlayerInventoryWindow::TYPE_INVENTORY){
2793 $mainInventoryWindow = $window;
2794 }
2795 }
2796 if($mainInventoryWindow === null){
2797 //TODO: in the future this might not be the case, if we implement support for the player closing their
2798 //inventory window outside the protocol layer
2799 //in that case we'd have to create a new ephemeral window here
2800 throw new AssumptionFailedError("This should never be null");
2801 }
2802
2803 if($this->currentWindow instanceof TemporaryInventoryWindow){
2804 $windowsToClear[] = $this->currentWindow;
2805 }
2806
2807 $builder = new TransactionBuilder();
2808 foreach($windowsToClear as $window){
2809 $contents = $window->getInventory()->getContents();
2810
2811 if(count($contents) > 0){
2812 $drops = $builder->getActionBuilder($mainInventoryWindow)->addItem(...$contents);
2813 foreach($drops as $drop){
2814 $builder->addAction(new DropItemAction($drop));
2815 }
2816
2817 $builder->getActionBuilder($window)->clearAll();
2818 }
2819 }
2820
2821 $actions = $builder->generateActions();
2822 if(count($actions) !== 0){
2823 $transaction = new InventoryTransaction($this, $actions);
2824 try{
2825 $transaction->execute();
2826 $this->logger->debug("Successfully evacuated items from temporary inventories");
2827 }catch(TransactionCancelledException){
2828 $this->logger->debug("Plugin cancelled transaction evacuating items from temporary inventories; items will be destroyed");
2829 foreach($windowsToClear as $window){
2830 $window->getInventory()->clearAll();
2831 }
2832 }catch(TransactionValidationException $e){
2833 throw new AssumptionFailedError("This server-generated transaction should never be invalid", 0, $e);
2834 }
2835 }
2836 }
2837
2841 public function getCurrentWindow() : ?InventoryWindow{
2842 return $this->currentWindow;
2843 }
2844
2848 public function setCurrentWindow(InventoryWindow $window) : bool{
2849 if($window === $this->currentWindow){
2850 return true;
2851 }
2852 if($window->getViewer() !== $this){
2853 throw new \InvalidArgumentException("Cannot reuse InventoryWindow instances, please create a new one for each player");
2854 }
2855 $ev = new InventoryOpenEvent($window, $this);
2856 $ev->call();
2857 if($ev->isCancelled()){
2858 return false;
2859 }
2860
2861 $this->removeCurrentWindow();
2862
2863 if(($inventoryManager = $this->getNetworkSession()->getInvManager()) === null){
2864 throw new \InvalidArgumentException("Player cannot open inventories in this state");
2865 }
2866 $this->logger->debug("Opening inventory window " . get_class($window) . "#" . spl_object_id($window));
2867 $inventoryManager->onCurrentWindowChange($window);
2868 $window->onOpen();
2869 $this->currentWindow = $window;
2870 return true;
2871 }
2872
2873 public function removeCurrentWindow() : void{
2874 $this->doCloseInventory();
2875 if($this->currentWindow !== null){
2876 $currentWindow = $this->currentWindow;
2877 $this->logger->debug("Closing inventory window " . get_class($this->currentWindow) . "#" . spl_object_id($this->currentWindow));
2878 $this->currentWindow->onClose();
2879 if(($inventoryManager = $this->getNetworkSession()->getInvManager()) !== null){
2880 $inventoryManager->onCurrentWindowRemove();
2881 }
2882 $this->currentWindow = null;
2883 (new InventoryCloseEvent($currentWindow, $this))->call();
2884 }
2885 }
2886
2890 protected function addPermanentWindows(array $windows) : void{
2891 foreach($windows as $window){
2892 $window->onOpen();
2893 $this->permanentWindows[spl_object_id($window)] = $window;
2894 }
2895 }
2896
2897 protected function removePermanentWindows() : void{
2898 foreach($this->permanentWindows as $window){
2899 $window->onClose();
2900 }
2901 $this->permanentWindows = [];
2902 }
2903
2908 public function getPermanentWindows() : array{
2909 return $this->permanentWindows;
2910 }
2911
2915 public function openSignEditor(Vector3 $position, bool $frontFace = true) : void{
2916 $block = $this->getWorld()->getBlock($position);
2917 if($block instanceof BaseSign){
2918 $this->getWorld()->setBlock($position, $block->setEditorEntityRuntimeId($this->getId()));
2919 $this->getNetworkSession()->onOpenSignEditor($position, $frontFace);
2920 }else{
2921 throw new \InvalidArgumentException("Block at this position is not a sign");
2922 }
2923 }
2924
2925 use ChunkListenerNoOpTrait {
2926 onChunkChanged as private;
2927 onChunkUnloaded as private;
2928 }
2929
2930 public function onChunkChanged(int $chunkX, int $chunkZ, Chunk $chunk) : void{
2931 $status = $this->usedChunks[$hash = World::chunkHash($chunkX, $chunkZ)] ?? null;
2932 if($status === UsedChunkStatus::SENT){
2933 $this->usedChunks[$hash] = UsedChunkStatus::NEEDED;
2934 $this->nextChunkOrderRun = 0;
2935 }
2936 }
2937
2938 public function onChunkUnloaded(int $chunkX, int $chunkZ, Chunk $chunk) : void{
2939 if($this->isUsingChunk($chunkX, $chunkZ)){
2940 $this->logger->debug("Detected forced unload of chunk " . $chunkX . " " . $chunkZ);
2941 $this->unloadChunk($chunkX, $chunkZ);
2942 }
2943 }
2944}
onInteract(Player $player, Vector3 $clickPos)
Definition Entity.php:1132
setString(string $name, string $value)
setInt(string $name, int $value)
setLong(string $name, int $value)
attackBlock(Vector3 $pos, Facing $face)
Definition Player.php:1884
setCreativeInventory(CreativeInventory $inventory)
Definition Player.php:2775
setCurrentWindow(InventoryWindow $window)
Definition Player.php:2848
hasItemCooldown(Item $item)
Definition Player.php:777
isUsingChunk(int $chunkX, int $chunkZ)
Definition Player.php:1059
setDeathPosition(?Vector3 $pos)
Definition Player.php:1110
setScreenLineHeight(?int $height)
Definition Player.php:601
setCanSaveWithChunk(bool $value)
Definition Player.php:2477
kick(Translatable|string $reason="", Translatable|string|null $quitMessage=null, Translatable|string|null $disconnectScreenMessage=null)
Definition Player.php:2346
openSignEditor(Vector3 $position, bool $frontFace=true)
Definition Player.php:2915
teleport(Vector3 $pos, ?float $yaw=null, ?float $pitch=null)
Definition Player.php:2715
applyPostDamageEffects(EntityDamageEvent $source)
Definition Player.php:2639
setAllowFlight(bool $value)
Definition Player.php:489
initHumanData(CompoundTag $nbt)
Definition Player.php:366
getItemCooldownExpiry(Item $item)
Definition Player.php:769
sendTitle(string $title, string $subtitle="", int $fadeIn=-1, int $stay=-1, int $fadeOut=-1)
Definition Player.php:2202
transfer(string $address, int $port=19132, Translatable|string|null $message=null)
Definition Player.php:2328
addPermanentWindows(array $windows)
Definition Player.php:2890
setFlightSpeedMultiplier(float $flightSpeedMultiplier)
Definition Player.php:554
changeSkin(Skin $skin, string $newSkinName, string $oldSkinName)
Definition Player.php:723
broadcastAnimation(Animation $animation, ?array $targets=null)
Definition Player.php:2690
sendMessage(Translatable|string $message)
Definition Player.php:2254
hasReceivedChunk(int $chunkX, int $chunkZ)
Definition Player.php:1081
static isValidUserName(?string $name)
Definition Player.php:213
attackEntity(Entity $entity)
Definition Player.php:1986
breakBlock(Vector3 $pos)
Definition Player.php:1935
onDeathUpdate(int $tickDiff)
Definition Player.php:2555
sendToastNotification(string $title, string $body)
Definition Player.php:2278
resetItemCooldown(Item $item, ?int $ticks=null)
Definition Player.php:785
setTitleDuration(int $fadeIn, int $stay, int $fadeOut)
Definition Player.php:2245
sendPosition(Vector3 $pos, ?float $yaw=null, ?float $pitch=null, int $mode=MovePlayerPacket::MODE_NORMAL)
Definition Player.php:2709
setSpawn(?Vector3 $pos)
Definition Player.php:1147
onChunkUnloaded as onChunkChanged(int $chunkX, int $chunkZ, Chunk $chunk)
Definition Player.php:2930
interactBlock(Vector3 $pos, Facing $face, Vector3 $clickOffset)
Definition Player.php:1961
sendData(?array $targets, ?array $data=null)
Definition Player.php:2682
isCreative(bool $literal=false)
Definition Player.php:1279
onChunkUnloaded(int $chunkX, int $chunkZ, Chunk $chunk)
Definition Player.php:2938
chat(string $message)
Definition Player.php:1610
sendActionBarMessage(string $message)
Definition Player.php:2220
sendPopup(string $message)
Definition Player.php:2267
isAdventure(bool $literal=false)
Definition Player.php:1289
canInteract(Vector3 $pos, float $maxDistance, float $maxDiff=M_SQRT3/2)
Definition Player.php:1594
broadcastSound(Sound $sound, ?array $targets=null)
Definition Player.php:2698
getUsedChunkStatus(int $chunkX, int $chunkZ)
Definition Player.php:1074
interactEntity(Entity $entity, Vector3 $clickPos)
Definition Player.php:2074
sendSkin(?array $targets=null)
Definition Player.php:742
isSurvival(bool $literal=false)
Definition Player.php:1269
disconnect(Translatable|string $reason, Translatable|string|null $quitMessage=null, Translatable|string|null $disconnectScreenMessage=null)
Definition Player.php:2379
handleMovement(Vector3 $newPos)
Definition Player.php:1382
setHasBlockCollision(bool $value)
Definition Player.php:514
setGamemode(GameMode $gm)
Definition Player.php:1240
sendSubTitle(string $subtitle)
Definition Player.php:2213