131    private const MAX_FORM_RESPONSE_DEPTH = 2; 
 
  133    protected float $lastRightClickTime = 0.0;
 
  136    protected ?
Vector3 $lastPlayerAuthInputPosition = 
null;
 
  137    protected ?
float $lastPlayerAuthInputYaw = 
null;
 
  138    protected ?
float $lastPlayerAuthInputPitch = 
null;
 
  139    protected ?
BitSet $lastPlayerAuthInputFlags = 
null;
 
  143    public bool $forceMoveSync = 
false;
 
  145    protected ?
string $lastRequestedFullSkinId = 
null;
 
  147    public function __construct(
 
  153    public function handleText(
TextPacket $packet) : 
bool{
 
  154        if($packet->type === TextPacket::TYPE_CHAT){
 
  155            return $this->player->chat($packet->message);
 
  167    private function resolveOnOffInputFlags(
BitSet $inputFlags, 
int $startFlag, 
int $stopFlag) : ?
bool{
 
  168        $enabled = $inputFlags->get($startFlag);
 
  169        $disabled = $inputFlags->get($stopFlag);
 
  170        if($enabled !== $disabled){
 
  178        $rawPos = $packet->getPosition();
 
  179        $rawYaw = $packet->getYaw();
 
  180        $rawPitch = $packet->getPitch();
 
  181        foreach([$rawPos->x, $rawPos->y, $rawPos->z, $rawYaw, $packet->getHeadYaw(), $rawPitch] as $float){
 
  182            if(is_infinite($float) || is_nan($float)){
 
  183                $this->session->getLogger()->debug(
"Invalid movement received, contains NAN/INF components");
 
  188        if($rawYaw !== $this->lastPlayerAuthInputYaw || $rawPitch !== $this->lastPlayerAuthInputPitch){
 
  189            $this->lastPlayerAuthInputYaw = $rawYaw;
 
  190            $this->lastPlayerAuthInputPitch = $rawPitch;
 
  192            $yaw = fmod($rawYaw, 360);
 
  193            $pitch = fmod($rawPitch, 360);
 
  198            $this->player->setRotation($yaw, $pitch);
 
  201        $hasMoved = $this->lastPlayerAuthInputPosition === 
null || !$this->lastPlayerAuthInputPosition->equals($rawPos);
 
  202        $newPos = $rawPos->subtract(0, 1.62, 0)->round(4);
 
  204        if($this->forceMoveSync && $hasMoved){
 
  205            $curPos = $this->player->getLocation();
 
  207            if($newPos->distanceSquared($curPos) > 1){  
 
  208                $this->session->getLogger()->debug(
"Got outdated pre-teleport movement, received " . $newPos . 
", expected " . $curPos);
 
  214            $this->forceMoveSync = 
false;
 
  217        $inputFlags = $packet->getInputFlags();
 
  218        if($this->lastPlayerAuthInputFlags === 
null || !$inputFlags->equals($this->lastPlayerAuthInputFlags)){
 
  219            $this->lastPlayerAuthInputFlags = $inputFlags;
 
  222            if($this->player->isSneaking() === $sneaking){
 
  225            $sprinting = $this->resolveOnOffInputFlags($inputFlags, PlayerAuthInputFlags::START_SPRINTING, PlayerAuthInputFlags::STOP_SPRINTING);
 
  226            $swimming = $this->resolveOnOffInputFlags($inputFlags, PlayerAuthInputFlags::START_SWIMMING, PlayerAuthInputFlags::STOP_SWIMMING);
 
  227            $gliding = $this->resolveOnOffInputFlags($inputFlags, PlayerAuthInputFlags::START_GLIDING, PlayerAuthInputFlags::STOP_GLIDING);
 
  228            $flying = $this->resolveOnOffInputFlags($inputFlags, PlayerAuthInputFlags::START_FLYING, PlayerAuthInputFlags::STOP_FLYING);
 
  230                ($sneaking !== 
null && !$this->player->toggleSneak($sneaking)) |
 
  231                ($sprinting !== 
null && !$this->player->toggleSprint($sprinting)) |
 
  232                ($swimming !== 
null && !$this->player->toggleSwim($swimming)) |
 
  233                ($gliding !== 
null && !$this->player->toggleGlide($gliding)) |
 
  234                ($flying !== 
null && !$this->player->toggleFlight($flying));
 
  235            if((
bool) $mismatch){
 
  236                $this->player->sendData([$this->player]);
 
  240                $this->player->jump();
 
  243                $this->player->missSwing();
 
  247        if(!$this->forceMoveSync && $hasMoved){
 
  248            $this->lastPlayerAuthInputPosition = $rawPos;
 
  250            $this->player->handleMovement($newPos);
 
  253        $packetHandled = 
true;
 
  255        $useItemTransaction = $packet->getItemInteractionData();
 
  256        if($useItemTransaction !== 
null){
 
  257            if(count($useItemTransaction->getTransactionData()->getActions()) > 100){
 
  261            $this->inventoryManager->setCurrentItemStackRequestId($useItemTransaction->getRequestId());
 
  262            $this->inventoryManager->addRawPredictedSlotChanges($useItemTransaction->getTransactionData()->getActions());
 
  263            if(!$this->handleUseItemTransaction($useItemTransaction->getTransactionData())){
 
  264                $packetHandled = 
false;
 
  265                $this->session->getLogger()->debug(
"Unhandled transaction in PlayerAuthInputPacket (type " . $useItemTransaction->getTransactionData()->getActionType() . 
")");
 
  267                $this->inventoryManager->syncMismatchedPredictedSlotChanges();
 
  269            $this->inventoryManager->setCurrentItemStackRequestId(
null);
 
  272        $itemStackRequest = $packet->getItemStackRequest();
 
  273        $itemStackResponseBuilder = $itemStackRequest !== 
null ? $this->handleSingleItemStackRequest($itemStackRequest) : 
null;
 
  277        $blockActions = $packet->getBlockActions();
 
  278        if($blockActions !== 
null){
 
  279            if(count($blockActions) > 100){
 
  282            foreach(Utils::promoteKeys($blockActions) as $k => $blockAction){
 
  283                $actionHandled = 
false;
 
  285                    $actionHandled = $this->handlePlayerActionFromData($blockAction->getActionType(), 
new BlockPosition(0, 0, 0), Facing::DOWN);
 
  287                    $actionHandled = $this->handlePlayerActionFromData($blockAction->getActionType(), $blockAction->getBlockPosition(), $blockAction->getFace());
 
  291                    $packetHandled = 
false;
 
  292                    $this->session->getLogger()->debug(
"Unhandled player block action at offset $k in PlayerAuthInputPacket");
 
  297        if($itemStackRequest !== 
null){
 
  298            $itemStackResponse = $itemStackResponseBuilder?->build() ?? 
new ItemStackResponse(ItemStackResponse::RESULT_ERROR, $itemStackRequest->getRequestId());
 
  302        return $packetHandled;
 
  306        if($packet->actorRuntimeId !== $this->player->getId()){
 
  308            return $packet->actorRuntimeId === ActorEvent::EATING_ITEM;
 
  311        switch($packet->eventId){
 
  312            case ActorEvent::EATING_ITEM: 
 
  313                $item = $this->player->getInventory()->getItemInHand();
 
  317                $this->player->broadcastAnimation(
new ConsumingItemAnimation($this->player, $this->player->getInventory()->getItemInHand()));
 
  329        if(count($packet->trData->getActions()) > 50){
 
  332        if(count($packet->requestChangedSlots) > 10){
 
  336        $this->inventoryManager->setCurrentItemStackRequestId($packet->requestId);
 
  337        $this->inventoryManager->addRawPredictedSlotChanges($packet->trData->getActions());
 
  340            $result = $this->handleNormalTransaction($packet->trData, $packet->requestId);
 
  342            $this->session->getLogger()->debug(
"Mismatch transaction received");
 
  343            $this->inventoryManager->requestSyncAll();
 
  346            $result = $this->handleUseItemTransaction($packet->trData);
 
  348            $result = $this->handleUseItemOnEntityTransaction($packet->trData);
 
  350            $result = $this->handleReleaseItemTransaction($packet->trData);
 
  353        $this->inventoryManager->syncMismatchedPredictedSlotChanges();
 
  359        foreach($packet->requestChangedSlots as $containerInfo){
 
  360            foreach($containerInfo->getChangedSlotIndexes() as $netSlot){
 
  362                $inventoryAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slot);
 
  363                if($inventoryAndSlot !== 
null){ 
 
  364                    $this->inventoryManager->onSlotChange($inventoryAndSlot[0], $inventoryAndSlot[1]);
 
  369        $this->inventoryManager->setCurrentItemStackRequestId(
null);
 
  373    private function executeInventoryTransaction(
InventoryTransaction $transaction, 
int $requestId) : 
bool{
 
  374        $this->player->setUsingItem(
false);
 
  376        $this->inventoryManager->setCurrentItemStackRequestId($requestId);
 
  377        $this->inventoryManager->addTransactionPredictedSlotChanges($transaction);
 
  381            $this->inventoryManager->requestSyncAll();
 
  382            $logger = $this->session->getLogger();
 
  383            $logger->debug(
"Invalid inventory transaction $requestId: " . $e->getMessage());
 
  387            $this->session->getLogger()->debug(
"Inventory transaction $requestId cancelled by a plugin");
 
  391            $this->inventoryManager->syncMismatchedPredictedSlotChanges();
 
  392            $this->inventoryManager->setCurrentItemStackRequestId(
null);
 
  398    private function handleNormalTransaction(
NormalTransactionData $data, 
int $itemStackRequestId) : 
bool{
 
  405        if($actionCount > 2){
 
  406            if($actionCount > 5){
 
  412            $this->session->getLogger()->debug(
"Ignoring normal inventory transaction with $actionCount actions (drop-item should have exactly 2 actions)");
 
  417        $clientItemStack = 
null;
 
  418        $droppedCount = 
null;
 
  420        foreach($data->
getActions() as $networkInventoryAction){
 
  421            if($networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_WORLD && $networkInventoryAction->inventorySlot === NetworkInventoryAction::ACTION_MAGIC_SLOT_DROP_ITEM){
 
  422                $droppedCount = $networkInventoryAction->newItem->getItemStack()->getCount();
 
  423                if($droppedCount <= 0){
 
  426            }elseif($networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_CONTAINER && $networkInventoryAction->windowId === ContainerIds::INVENTORY){
 
  428                $sourceSlot = $networkInventoryAction->inventorySlot;
 
  429                $clientItemStack = $networkInventoryAction->oldItem->getItemStack();
 
  431                $this->session->getLogger()->debug(
"Unexpected inventory action type $networkInventoryAction->sourceType in drop item transaction");
 
  435        if($sourceSlot === 
null || $clientItemStack === 
null || $droppedCount === 
null){
 
  436            $this->session->getLogger()->debug(
"Missing information in drop item transaction, need source slot, client item stack and dropped count");
 
  440        $inventory = $this->player->getInventory();
 
  442        if(!$inventory->slotExists($sourceSlot)){
 
  446        $sourceSlotItem = $inventory->getItem($sourceSlot);
 
  447        if($sourceSlotItem->getCount() < $droppedCount){
 
  450        $serverItemStack = $this->session->getTypeConverter()->coreItemStackToNet($sourceSlotItem);
 
  454            $serverItemStack->getId() !== $clientItemStack->getId() ||
 
  455            $serverItemStack->getMeta() !== $clientItemStack->getMeta() ||
 
  456            $serverItemStack->getCount() !== $clientItemStack->getCount() ||
 
  457            $serverItemStack->getBlockRuntimeId() !== $clientItemStack->getBlockRuntimeId()
 
  467        $droppedItem = $sourceSlotItem->pop($droppedCount);
 
  470        $builder->getInventory($inventory)->setItem($sourceSlot, $sourceSlotItem);
 
  474        return $this->executeInventoryTransaction($transaction, $itemStackRequestId);
 
  478        $this->player->selectHotbarSlot($data->getHotbarSlot());
 
  480        switch($data->getActionType()){
 
  481            case UseItemTransactionData::ACTION_CLICK_BLOCK:
 
  483                $clickPos = $data->getClickPosition();
 
  484                $spamBug = ($this->lastRightClickData !== 
null &&
 
  485                    microtime(
true) - $this->lastRightClickTime < 0.1 && 
 
  486                    $this->lastRightClickData->getPlayerPosition()->distanceSquared($data->getPlayerPosition()) < 0.00001 &&
 
  487                    $this->lastRightClickData->getBlockPosition()->equals($data->getBlockPosition()) &&
 
  488                    $this->lastRightClickData->getClickPosition()->distanceSquared($clickPos) < 0.00001 
 
  491                $this->lastRightClickData = $data;
 
  492                $this->lastRightClickTime = microtime(
true);
 
  498                self::validateFacing($data->getFace());
 
  500                $blockPos = $data->getBlockPosition();
 
  501                $vBlockPos = 
new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ());
 
  502                $this->player->interactBlock($vBlockPos, $data->getFace(), $clickPos);
 
  503                if($data->getClientInteractPrediction() === PredictedResult::SUCCESS){
 
  508                    $this->syncBlocksNearby($vBlockPos, $data->getFace());
 
  511            case UseItemTransactionData::ACTION_CLICK_AIR:
 
  512                if($this->player->isUsingItem()){
 
  513                    if(!$this->player->consumeHeldItem()){
 
  514                        $hungerAttr = $this->player->getAttributeMap()->get(Attribute::HUNGER) ?? 
throw new AssumptionFailedError();
 
  515                        $hungerAttr->markSynchronized(
false);
 
  519                $this->player->useHeldItem();
 
  529    private static function validateFacing(
int $facing) : 
void{
 
  530        if(!in_array($facing, Facing::ALL, 
true)){
 
  538    private function syncBlocksNearby(
Vector3 $blockPos, ?
int $face) : 
void{
 
  539        if($blockPos->distanceSquared($this->player->getLocation()) < 10000){
 
  542                $sidePos = $blockPos->
getSide($face);
 
  545                array_push($blocks, ...$sidePos->sidesArray()); 
 
  547                $blocks[] = $blockPos;
 
  549            foreach($this->player->getWorld()->createBlockUpdatePackets($blocks) as $packet){
 
  550                $this->session->sendDataPacket($packet);
 
  556        $target = $this->player->getWorld()->getEntity($data->getActorRuntimeId());
 
  557        if($target === 
null){
 
  561        $this->player->selectHotbarSlot($data->getHotbarSlot());
 
  563        switch($data->getActionType()){
 
  564            case UseItemOnEntityTransactionData::ACTION_INTERACT:
 
  565                $this->player->interactEntity($target, $data->getClickPosition());
 
  567            case UseItemOnEntityTransactionData::ACTION_ATTACK:
 
  568                $this->player->attackEntity($target);
 
  576        $this->player->selectHotbarSlot($data->getHotbarSlot());
 
  578        if($data->getActionType() === ReleaseItemTransactionData::ACTION_RELEASE){
 
  579            $this->player->releaseHeldItem();
 
  602            $transaction = $executor->generateInventoryTransaction();
 
  603            if($transaction !== 
null){
 
  604                $result = $this->executeInventoryTransaction($transaction, $request->getRequestId());
 
  610            $this->session->getLogger()->debug(
"ItemStackRequest #" . $request->getRequestId() . 
" failed: " . $e->getMessage());
 
  611            $this->session->getLogger()->debug(implode(
"\n", Utils::printableExceptionInfo($e)));
 
  612            $this->inventoryManager->requestSyncAll();
 
  615        return $result ? $executor->getItemStackResponseBuilder() : 
null;
 
  620        if(count($packet->getRequests()) > 80){
 
  624        foreach($packet->getRequests() as $request){
 
  625            $responses[] = $this->handleSingleItemStackRequest($request)?->build() ?? 
new ItemStackResponse(ItemStackResponse::RESULT_ERROR, $request->getRequestId());
 
  634        if($packet->windowId === ContainerIds::OFFHAND){
 
  637        if($packet->windowId === ContainerIds::INVENTORY){
 
  638            $this->inventoryManager->onClientSelectHotbarSlot($packet->hotbarSlot);
 
  639            if(!$this->player->selectHotbarSlot($packet->hotbarSlot)){
 
  640                $this->inventoryManager->syncSelectedHotbarSlot();
 
  652        if($packet->action === InteractPacket::ACTION_MOUSEOVER){
 
  660        $target = $this->player->getWorld()->getEntity($packet->targetActorRuntimeId);
 
  661        if($target === 
null){
 
  664        if($packet->action === InteractPacket::ACTION_OPEN_INVENTORY && $target === $this->player){
 
  665            $this->inventoryManager->onClientOpenMainInventory();
 
  672        return $this->player->pickBlock(
new Vector3($packet->blockPosition->getX(), $packet->blockPosition->getY(), $packet->blockPosition->getZ()), $packet->addUserData);
 
  676        return $this->player->pickEntity($packet->actorUniqueId);
 
  680        return $this->handlePlayerActionFromData($packet->action, $packet->blockPosition, $packet->face);
 
  683    private function handlePlayerActionFromData(
int $action, 
BlockPosition $blockPosition, 
int $face) : 
bool{
 
  684        $pos = 
new Vector3($blockPosition->getX(), $blockPosition->getY(), $blockPosition->getZ());
 
  687            case PlayerAction::START_BREAK:
 
  688            case PlayerAction::CONTINUE_DESTROY_BLOCK: 
 
  689                self::validateFacing($face);
 
  690                if($this->lastBlockAttacked !== 
null && $blockPosition->equals($this->lastBlockAttacked)){
 
  695                    $this->session->getLogger()->debug(
"Ignoring PlayerAction $action on $pos because we were already destroying this block");
 
  698                if(!$this->player->attackBlock($pos, $face)){
 
  699                    $this->syncBlocksNearby($pos, $face);
 
  701                $this->lastBlockAttacked = $blockPosition;
 
  705            case PlayerAction::ABORT_BREAK:
 
  706            case PlayerAction::STOP_BREAK:
 
  707                $this->player->stopBreakBlock($pos);
 
  708                $this->lastBlockAttacked = 
null;
 
  710            case PlayerAction::START_SLEEPING:
 
  713            case PlayerAction::STOP_SLEEPING:
 
  714                $this->player->stopSleep();
 
  716            case PlayerAction::CRACK_BREAK:
 
  717                self::validateFacing($face);
 
  718                $this->player->continueBreakBlock($pos, $face);
 
  719                $this->lastBlockAttacked = $blockPosition;
 
  721            case PlayerAction::INTERACT_BLOCK: 
 
  723            case PlayerAction::CREATIVE_PLAYER_DESTROY_BLOCK:
 
  726            case PlayerAction::PREDICT_DESTROY_BLOCK:
 
  727                self::validateFacing($face);
 
  728                if(!$this->player->breakBlock($pos)){
 
  729                    $this->syncBlocksNearby($pos, $face);
 
  731                $this->lastBlockAttacked = 
null;
 
  733            case PlayerAction::START_ITEM_USE_ON:
 
  734            case PlayerAction::STOP_ITEM_USE_ON:
 
  738                $this->session->getLogger()->debug(
"Unhandled/unknown player action type " . $action);
 
  742        $this->player->setUsingItem(
false);
 
  758        $this->inventoryManager->onClientRemoveWindow($packet->windowId);
 
  770        $textTag = $nbt->
getTag($tagName);
 
  772            throw new PacketHandlingException(
"Invalid tag type " . get_debug_type($textTag) . 
" for tag \"$tagName\" in sign update data");
 
  774        $textBlobTag = $textTag->getTag(Sign::TAG_TEXT_BLOB);
 
  776            throw new PacketHandlingException(
"Invalid tag type " . get_debug_type($textBlobTag) . 
" for tag \"" . Sign::TAG_TEXT_BLOB . 
"\" in sign update data");
 
  780            $text = SignText::fromBlob($textBlobTag->getValue());
 
  781        }
catch(\InvalidArgumentException $e){
 
  782            throw PacketHandlingException::wrap($e, 
"Invalid sign text update");
 
  785        $oldText = $block->getFaceText($frontFace);
 
  786        if($text->getLines() === $oldText->getLines()){
 
  792                foreach($this->player->getWorld()->createBlockUpdatePackets([$pos]) as $updatePacket){
 
  793                    $this->session->sendDataPacket($updatePacket);
 
  798        }
catch(\UnexpectedValueException $e){
 
  799            throw PacketHandlingException::wrap($e);
 
  804        $pos = 
new Vector3($packet->blockPosition->getX(), $packet->blockPosition->getY(), $packet->blockPosition->getZ());
 
  805        if($pos->distanceSquared($this->player->getLocation()) > 10000){
 
  809        $block = $this->player->getLocation()->getWorld()->getBlock($pos);
 
  810        $nbt = $packet->nbt->getRoot();
 
  814            if(!$this->updateSignText($nbt, Sign::TAG_FRONT_TEXT, 
true, $block, $pos)){
 
  816                $this->updateSignText($nbt, Sign::TAG_BACK_TEXT, 
false, $block, $pos);
 
  826        $gameMode = $this->session->getTypeConverter()->protocolGameModeToCore($packet->gamemode);
 
  827        if($gameMode !== $this->player->getGamemode()){
 
  829            $this->session->syncGameMode($this->player->getGamemode(), 
true);
 
  843        $this->player->setViewDistance($packet->radius);
 
  857        if(str_starts_with($packet->command, 
'/')){
 
  858            $this->player->chat($packet->command);
 
  869        if($packet->skin->getFullSkinId() === $this->lastRequestedFullSkinId){
 
  872            $this->session->getLogger()->debug(
"Refused duplicate skin change request");
 
  875        $this->lastRequestedFullSkinId = $packet->skin->getFullSkinId();
 
  877        $this->session->getLogger()->debug(
"Processing skin change request");
 
  879            $skin = $this->session->getTypeConverter()->getSkinAdapter()->fromSkinData($packet->skin);
 
  881            throw PacketHandlingException::wrap($e, 
"Invalid skin in PlayerSkinPacket");
 
  883        return $this->player->changeSkin($skin, $packet->newSkinName, $packet->oldSkinName);
 
  893    private function checkBookText(
string $string, 
string $fieldName, 
int $softLimit, 
int $hardLimit, 
bool &$cancel) : 
string{
 
  894        if(strlen($string) > $hardLimit){
 
  895            throw new PacketHandlingException(sprintf(
"Book %s must be at most %d bytes, but have %d bytes", $fieldName, $hardLimit, strlen($string)));
 
  898        $result = TextFormat::clean($string, 
false);
 
  900        if(strlen($result) > $softLimit * 4 || mb_strlen($result, 
'UTF-8') > $softLimit){
 
  902            $this->session->getLogger()->debug(
"Cancelled book edit due to $fieldName exceeded soft limit of $softLimit chars");
 
  909        $inventory = $this->player->getInventory();
 
  910        if(!$inventory->slotExists($packet->inventorySlot)){
 
  914        $oldBook = $inventory->getItem($packet->inventorySlot);
 
  919        $newBook = clone $oldBook;
 
  922        switch($packet->type){
 
  923            case BookEditPacket::TYPE_REPLACE_PAGE:
 
  924                $text = self::checkBookText($packet->text, 
"page text", 256, WritableBookPage::PAGE_LENGTH_HARD_LIMIT_BYTES, $cancel);
 
  925                $newBook->setPageText($packet->pageNumber, $text);
 
  926                $modifiedPages[] = $packet->pageNumber;
 
  928            case BookEditPacket::TYPE_ADD_PAGE:
 
  929                if(!$newBook->pageExists($packet->pageNumber)){
 
  934                $text = self::checkBookText($packet->text, 
"page text", 256, WritableBookPage::PAGE_LENGTH_HARD_LIMIT_BYTES, $cancel);
 
  935                $newBook->insertPage($packet->pageNumber, $text);
 
  936                $modifiedPages[] = $packet->pageNumber;
 
  938            case BookEditPacket::TYPE_DELETE_PAGE:
 
  939                if(!$newBook->pageExists($packet->pageNumber)){
 
  942                $newBook->deletePage($packet->pageNumber);
 
  943                $modifiedPages[] = $packet->pageNumber;
 
  945            case BookEditPacket::TYPE_SWAP_PAGES:
 
  946                if(!$newBook->pageExists($packet->pageNumber) || !$newBook->pageExists($packet->secondaryPageNumber)){
 
  948                    $newBook->addPage(max($packet->pageNumber, $packet->secondaryPageNumber));
 
  950                $newBook->swapPages($packet->pageNumber, $packet->secondaryPageNumber);
 
  951                $modifiedPages = [$packet->pageNumber, $packet->secondaryPageNumber];
 
  953            case BookEditPacket::TYPE_SIGN_BOOK:
 
  954                $title = self::checkBookText($packet->title, 
"title", 16, Limits::INT16_MAX, $cancel);
 
  956                $author = self::checkBookText($packet->author, 
"author", 256, Limits::INT16_MAX, $cancel);
 
  958                $newBook = VanillaItems::WRITTEN_BOOK()
 
  959                    ->setPages($oldBook->getPages())
 
  962                    ->setGeneration(WrittenBook::GENERATION_ORIGINAL);
 
  969        $action = match($packet->type){
 
  970            BookEditPacket::TYPE_REPLACE_PAGE => PlayerEditBookEvent::ACTION_REPLACE_PAGE,
 
  971            BookEditPacket::TYPE_ADD_PAGE => PlayerEditBookEvent::ACTION_ADD_PAGE,
 
  972            BookEditPacket::TYPE_DELETE_PAGE => PlayerEditBookEvent::ACTION_DELETE_PAGE,
 
  973            BookEditPacket::TYPE_SWAP_PAGES => PlayerEditBookEvent::ACTION_SWAP_PAGES,
 
  974            BookEditPacket::TYPE_SIGN_BOOK => PlayerEditBookEvent::ACTION_SIGN_BOOK,
 
  982        $oldPageCount = count($oldBook->getPages());
 
  983        $newPageCount = count($newBook->getPages());
 
  984        if(($newPageCount > $oldPageCount && $newPageCount > 50)){
 
  985            $this->session->getLogger()->debug(
"Cancelled book edit due to adding too many pages (new page count would be $newPageCount)");
 
  989        $event = 
new PlayerEditBookEvent($this->player, $oldBook, $newBook, $action, $modifiedPages);
 
  995        if($event->isCancelled()){
 
  999        $this->player->getInventory()->setItem($packet->inventorySlot, $event->getNewBook());
 
 1005        if($packet->cancelReason !== 
null){
 
 1007            return $this->player->onFormSubmit($packet->formId, 
null);
 
 1008        }elseif($packet->formData !== 
null){
 
 1010                $responseData = json_decode($packet->formData, 
true, self::MAX_FORM_RESPONSE_DEPTH, JSON_THROW_ON_ERROR);
 
 1011            }
catch(\JsonException $e){
 
 1012                throw PacketHandlingException::wrap($e, 
"Failed to decode form response data");
 
 1014            return $this->player->onFormSubmit($packet->formId, $responseData);
 
 1016            throw new PacketHandlingException(
"Expected either formData or cancelReason to be set in ModalFormResponsePacket");
 
 1029        $pos = $packet->blockPosition;
 
 1030        $chunkX = $pos->getX() >> Chunk::COORD_BIT_SIZE;
 
 1031        $chunkZ = $pos->getZ() >> Chunk::COORD_BIT_SIZE;
 
 1032        $world = $this->player->getWorld();
 
 1033        if(!$world->isChunkLoaded($chunkX, $chunkZ) || $world->isChunkLocked($chunkX, $chunkZ)){
 
 1037        $lectern = $world->getBlockAt($pos->getX(), $pos->getY(), $pos->getZ());
 
 1038        if($lectern instanceof 
Lectern && $this->player->canInteract($lectern->getPosition(), 15)){
 
 1039            if(!$lectern->onPageTurn($packet->page)){
 
 1040                $this->syncBlocksNearby($lectern->getPosition(), 
null);
 
 1062    public function handleEmote(
EmotePacket $packet) : 
bool{
 
 1063        $this->player->emote($packet->getEmoteId());