From 8707575669e60650f6a11e4cd2c16ffa3c51cc82 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Wed, 10 Sep 2025 20:45:49 -0400 Subject: [PATCH 1/4] fix block stuttering while placing blocks --- .../mcpe/handler/InGamePacketHandler.php | 5 --- src/player/Player.php | 13 +++++++ src/world/World.php | 39 ++++++++++++++++++- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/network/mcpe/handler/InGamePacketHandler.php b/src/network/mcpe/handler/InGamePacketHandler.php index 6aae60745d2..e9cc3408c09 100644 --- a/src/network/mcpe/handler/InGamePacketHandler.php +++ b/src/network/mcpe/handler/InGamePacketHandler.php @@ -498,11 +498,6 @@ private function handleUseItemTransaction(UseItemTransactionData $data) : bool{ $blockPos = $data->getBlockPosition(); $vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ()); $this->player->interactBlock($vBlockPos, $data->getFace(), $clickPos); - //always sync this in case plugins caused a different result than the client expected - //we *could* try to enhance detection of plugin-altered behaviour, but this would require propagating - //more information up the stack. For now I think this is good enough. - //if only the client would tell us what blocks it thinks changed... - $this->syncBlocksNearby($vBlockPos, $data->getFace()); return true; case UseItemTransactionData::ACTION_CLICK_AIR: if($this->player->isUsingItem()){ diff --git a/src/player/Player.php b/src/player/Player.php index aa2d2af8818..c222be9e499 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -1938,12 +1938,25 @@ public function interactBlock(Vector3 $pos, int $face, Vector3 $clickOffset) : b return true; } }else{ + $this->syncBlocks([$pos, $pos->getSide($face)]); $this->logger->debug("Cancelled interaction of block at $pos due to not currently being interactable"); } return false; } + /** + * Sync blocks sends block updates to the player at the block positions. + * + * @param Vector3[] $blocks + */ + public function syncBlocks(array $blocks) : void { + $blocks = array_filter($blocks, fn(Vector3 $block) => $block->distanceSquared($this->getLocation()) < 10000); + foreach ($this->getWorld()->createBlockUpdatePackets($blocks) as $packet) { + $this->getNetworkSession()->sendDataPacket($packet); + } + } + /** * Attacks the given entity with the currently-held item. * TODO: move this up the class hierarchy diff --git a/src/world/World.php b/src/world/World.php index 236fd6e5601..783c5e9b8ec 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -122,6 +122,7 @@ use function floor; use function get_class; use function gettype; +use function in_array; use function is_a; use function is_object; use function max; @@ -2241,6 +2242,7 @@ private function destroyBlockInternal(Block $target, Item $item, ?Player $player public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $clickVector = null, ?Player $player = null, bool $playSound = false, array &$returnedItems = []) : bool{ $blockClicked = $this->getBlock($vector); $blockReplace = $blockClicked->getSide($face); + $mainBlocks = [$vector, ($sideVector = $vector->getSide($face))]; if($clickVector === null){ $clickVector = new Vector3(0.0, 0.0, 0.0); @@ -2254,15 +2256,18 @@ public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $cli if(!$this->isInWorld($blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z)){ //TODO: build height limit messages for custom world heights and mcregion cap + $player?->syncBlocks($mainBlocks); return false; } $chunkX = $blockReplace->getPosition()->getFloorX() >> Chunk::COORD_BIT_SIZE; $chunkZ = $blockReplace->getPosition()->getFloorZ() >> Chunk::COORD_BIT_SIZE; if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){ + $player?->syncBlocks($mainBlocks); return false; } if($blockClicked->getTypeId() === BlockTypeIds::AIR){ + $player?->syncBlocks($mainBlocks); return false; } @@ -2279,6 +2284,18 @@ public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $cli $ev->call(); if(!$ev->isCancelled()){ if($ev->useBlock() && $blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){ + $aroundBlocks = []; + foreach ($vector->sidesArray() as $otherVector) { + if ($sideVector !== $otherVector) { + $aroundBlocks[] = $otherVector; + } + } + foreach ($sideVector->sidesArray() as $otherVector) { + if ($vector !== $otherVector) { + $aroundBlocks[] = $otherVector; + } + } + $player->syncBlocks($aroundBlocks); return true; } @@ -2289,6 +2306,7 @@ public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $cli } } }else{ + $player->syncBlocks($mainBlocks); return false; } }elseif($blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){ @@ -2296,6 +2314,7 @@ public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $cli } if($item->isNull() || !$item->canBePlaced()){ + $player?->syncBlocks($mainBlocks); return false; } @@ -2307,14 +2326,22 @@ public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $cli $item->getPlacementTransaction($blockReplace, $blockClicked, $face, $clickVector, $player); if($tx === null){ //no placement options available + $player?->syncBlocks($mainBlocks); return false; } + /** @var Vector3[] $originalBlocks */ + $originalBlocks = []; foreach($tx->getBlocks() as [$x, $y, $z, $block]){ + $originalBlocks[] = new Vector3($x, $y, $z); $block->position($this, $x, $y, $z); foreach($block->getCollisionBoxes() as $collisionBox){ + if ($player !== null && $player->getBoundingBox()->intersectsWith($collisionBox)) { + return false; + } if(count($this->getCollidingEntities($collisionBox)) > 0){ - return false; //Entity in block + $player?->syncBlocks($mainBlocks); + return false; } } } @@ -2343,15 +2370,22 @@ public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $cli $ev->call(); if($ev->isCancelled()){ + $player->syncBlocks($mainBlocks); return false; } } if(!$tx->apply()){ + $player?->syncBlocks($mainBlocks); return false; } $first = true; + $newBlocks = []; foreach($tx->getBlocks() as [$x, $y, $z, $_]){ + $blockPos = new Vector3($x, $y, $z); + if (!in_array($blockPos, $originalBlocks, true)) { + $newBlocks[] = $blockPos; + } $tile = $this->getTileAt($x, $y, $z); if($tile !== null){ //TODO: seal this up inside block placement @@ -2367,6 +2401,9 @@ public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $cli } $item->pop(); + if ($player !== null && count($newBlocks) > 0) { + $player->syncBlocks($newBlocks); + } return true; } From 7f51c4c00b6c9bb9f37779ccad1821c791c0ceb1 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Fri, 12 Sep 2025 13:10:59 -0400 Subject: [PATCH 2/4] World.php: account for multi-block placements being cancelled --- src/world/World.php | 60 ++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/src/world/World.php b/src/world/World.php index 783c5e9b8ec..6e9bc662592 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -122,7 +122,6 @@ use function floor; use function get_class; use function gettype; -use function in_array; use function is_a; use function is_object; use function max; @@ -2239,10 +2238,10 @@ private function destroyBlockInternal(Block $target, Item $item, ?Player $player * @param bool $playSound Whether to play a block-place sound if the block was placed successfully. * @param Item[] &$returnedItems Items to be added to the target's inventory (or dropped if the inventory is full) */ - public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $clickVector = null, ?Player $player = null, bool $playSound = false, array &$returnedItems = []) : bool{ - $blockClicked = $this->getBlock($vector); + public function useItemOn(Vector3 $clickedBlockPos, Item &$item, int $face, ?Vector3 $clickVector = null, ?Player $player = null, bool $playSound = false, array &$returnedItems = []) : bool{ + $blockClicked = $this->getBlock($clickedBlockPos); $blockReplace = $blockClicked->getSide($face); - $mainBlocks = [$vector, ($sideVector = $vector->getSide($face))]; + $mainBlocks = [$clickedBlockPos, ($sideClickPos = $clickedBlockPos->getSide($face))]; if($clickVector === null){ $clickVector = new Vector3(0.0, 0.0, 0.0); @@ -2285,13 +2284,13 @@ public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $cli if(!$ev->isCancelled()){ if($ev->useBlock() && $blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){ $aroundBlocks = []; - foreach ($vector->sidesArray() as $otherVector) { - if ($sideVector !== $otherVector) { + foreach ($clickedBlockPos->sidesArray() as $otherVector) { + if ($sideClickPos !== $otherVector) { $aroundBlocks[] = $otherVector; } } - foreach ($sideVector->sidesArray() as $otherVector) { - if ($vector !== $otherVector) { + foreach ($sideClickPos->sidesArray() as $otherVector) { + if ($clickedBlockPos !== $otherVector) { $aroundBlocks[] = $otherVector; } } @@ -2331,27 +2330,39 @@ public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $cli } /** @var Vector3[] $originalBlocks */ - $originalBlocks = []; - foreach($tx->getBlocks() as [$x, $y, $z, $block]){ - $originalBlocks[] = new Vector3($x, $y, $z); + $placementSyncBlocks = []; + $allowed = true; + $needsSync = false; + foreach($tx->getBlocks() as [$x, $y, $z, $block]) { + $blockPos = new Vector3($x, $y, $z); + $placementSyncBlocks[] = $blockPos; $block->position($this, $x, $y, $z); - foreach($block->getCollisionBoxes() as $collisionBox){ + foreach($block->getCollisionBoxes() as $collisionBox) { + $playerCollision = false; if ($player !== null && $player->getBoundingBox()->intersectsWith($collisionBox)) { - return false; + $allowed = false; + $needsSync = !$blockPos->equals($clickedBlockPos) && !$blockPos->equals($sideClickPos); + $playerCollision = true; } - if(count($this->getCollidingEntities($collisionBox)) > 0){ - $player?->syncBlocks($mainBlocks); - return false; + if (count($this->getCollidingEntities($collisionBox)) > 0) { + $allowed = false; + $needsSync = $needsSync || !$playerCollision; } } } + if (!$allowed) { + if ($needsSync) { + $player?->syncBlocks($placementSyncBlocks); + } + return false; + } + if($player !== null){ $ev = new BlockPlaceEvent($player, $tx, $blockClicked, $item); if($player->isSpectator()){ $ev->cancel(); } - if($player->isAdventure(true) && !$ev->isCancelled()){ $canPlace = false; $itemParser = LegacyStringToItemParser::getInstance(); @@ -2370,22 +2381,17 @@ public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $cli $ev->call(); if($ev->isCancelled()){ - $player->syncBlocks($mainBlocks); + $player->syncBlocks($placementSyncBlocks); return false; } } if(!$tx->apply()){ - $player?->syncBlocks($mainBlocks); + $player?->syncBlocks($placementSyncBlocks); return false; } $first = true; - $newBlocks = []; foreach($tx->getBlocks() as [$x, $y, $z, $_]){ - $blockPos = new Vector3($x, $y, $z); - if (!in_array($blockPos, $originalBlocks, true)) { - $newBlocks[] = $blockPos; - } $tile = $this->getTileAt($x, $y, $z); if($tile !== null){ //TODO: seal this up inside block placement @@ -2401,10 +2407,8 @@ public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $cli } $item->pop(); - if ($player !== null && count($newBlocks) > 0) { - $player->syncBlocks($newBlocks); - } - + // As long as we are only syncing the relevant blocks to the actual placement, this should be fine. + $player?->syncBlocks($placementSyncBlocks); return true; } From 83bb7bd1325e36a5d346b65df15c17cb4958fecc Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Fri, 12 Sep 2025 13:14:31 -0400 Subject: [PATCH 3/4] fix mismatching phpdoc tag --- src/world/World.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/world/World.php b/src/world/World.php index 6e9bc662592..b7fad9eadda 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -2329,7 +2329,7 @@ public function useItemOn(Vector3 $clickedBlockPos, Item &$item, int $face, ?Vec return false; } - /** @var Vector3[] $originalBlocks */ + /** @var Vector3[] $placementSyncBlocks */ $placementSyncBlocks = []; $allowed = true; $needsSync = false; From 1805bcdc228730432bb4202d816b3638be8052db Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Fri, 12 Sep 2025 13:24:04 -0400 Subject: [PATCH 4/4] World.php: fix faulty logic --- src/world/World.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/world/World.php b/src/world/World.php index b7fad9eadda..f85871056d5 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -2341,7 +2341,6 @@ public function useItemOn(Vector3 $clickedBlockPos, Item &$item, int $face, ?Vec $playerCollision = false; if ($player !== null && $player->getBoundingBox()->intersectsWith($collisionBox)) { $allowed = false; - $needsSync = !$blockPos->equals($clickedBlockPos) && !$blockPos->equals($sideClickPos); $playerCollision = true; } if (count($this->getCollidingEntities($collisionBox)) > 0) {