-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Fix for "block lag" while placing blocks on higher latency clients #6803
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: stable
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand why this solves the issue. It's still resending blocks on failed actions.
The cause of the flickering is because the right-click action is very noisy and gets sent even when the client can't possibly place the block, which triggers resends of air blocks which then conflict with client-side predictions when it actually does succeed in placing the block. This PR is still resending blocks on failed actions, so I don't see why this would solve anything.
Furthermore, this doesn't seem to account for the possibility that the client may have attempted to place a multi-block object like a door, which is why the original function sends so many blocks to begin with.
src/world/World.php
Outdated
| $newBlocks = []; | ||
| foreach($tx->getBlocks() as [$x, $y, $z, $_]){ | ||
| $blockPos = new Vector3($x, $y, $z); | ||
| if (!in_array($blockPos, $originalBlocks, true)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It always baffles me how experienced developers still use in_array().
This is slow first of all, and it will also always return false because you're testing by object identity for an object which you just created.
It'd likely be faster to just accept the possibility of duplicates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I read the word "experienced" as "inexperienced") - sorry Dylan
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wasn't intended as a personal attack. I just find it surprising.
I don't feel like taking 4-year-old discord messages out of context is constructive to the PR discussion though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The latest commit should resolve this, along with the issue of multi-position block placements needing to be synced properly as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I'm sorry for my condescending comment. It was unnecessary and non-constructive.
Simplified example of what is happening before this PR, where the client has a ~4 tick RTT:
|
|
One more thing I forgot to address:
The PR also has an exemption where it will not synchronize blocks when the player's bounding box is colliding with the block's bounding boxes. This is made with the assumption that the player would already know if they are colliding with a block they placed, and therefore wouldn't need an air update. This is specifically helpful when a player is holding (or spamming) right click while bridging upwards. Before PR: https://streamable.com/shkzda After PR: https://streamable.com/wanchr |
| */ | ||
| 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{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is BC breaking
| $aroundBlocks[] = $otherVector; | ||
| } | ||
| } | ||
| $player->syncBlocks($aroundBlocks); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the block interaction was disabled, then the client would wrongly see a door being openable when it's actually cancelled on the server side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should sync doors properly here, as it is also sending the blocks around the clicked block's position sides, similar to how InGamePacketHandler->syncBlocksNearby does it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a plugin does $ev->setUseBlock(false), the client will predict a door being opened when it isn't. This code is gated inside $ev->useBlock() and wouldn't run in that case.
| } | ||
|
|
||
| if($item->isNull() || !$item->canBePlaced()){ | ||
| $player?->syncBlocks($mainBlocks); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't remember too well how this code works. But I'm pretty sure it's possible for the client & server to disagree about the currently held item. So the client might think it's able to place a door while the server doesn't in network races.
| $item->getPlacementTransaction($blockReplace, $blockClicked, $face, $clickVector, $player); | ||
| if($tx === null){ | ||
| //no placement options available | ||
| $player?->syncBlocks($mainBlocks); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same again here - the client may think the placement will succeed if it doesn't have an up to date copy of the terrain
| if ($player !== null && $player->getBoundingBox()->intersectsWith($collisionBox)) { | ||
| $allowed = false; | ||
| $playerCollision = true; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a solution I didn't want to go for too, because it's possible the server and client may have different ideas about the block's bounding box (or the player's own). I think this is the workaround most people went for, but I'd still be happier if there was a better way to do it.
|
To address something you said earlier: I did try to fix this problem a couple of times in the past, but couldn't figure out a way to do it that covered all bases. I did also find this issue annoying, and the only reason it wasn't fixed is because I couldn't figure out a way to do it that didn't require some sort of hack or expose edge cases, and didn't fancy wasting my time on it. (Had hoped someone else would solve it so I didn't have to...) So, one concern I've had with changes like this is that we don't really know what the client is predicting. It occurred to me today that it might be possible to solve this just by delaying the outbound block syncs instead of having complicated conditions for the syncing. A delay before sending out the block updates would allow newer actions to arrive without spamming conflicting ones. What do you think about that? I will admit I haven't thought about it in depth. |
|
Really the ideal world would be if the client would tell us what predictions it made, or if we could associate a transaction ID with block updates so that the client would know to ignore stale block updates. But unless something has changed in the protocol lately that I'm not aware of, that's probably not possible. |
The client (as of 1.21.20) does send a prediction of whether it predicted it's interaction failed or not. We could use that to determine if
This solution I think would reduce the amount of times the scenario I mentioned previously (the 4 tick RTT one), but it would still happen. This mainly affects jump-bridging, as the blocks being set to air has the potential to make the client predict it should place a block upward instead of the direction the player was building in |
|
So my understanding of that field is that we can interpret it as "did the client make any predictions". If it didn't, we should be able to avoid syncing in those cases (if the action succeeds in spite of this, the world will broadcast updates anyway), which should make towering less buggy without the need for AABB checks. It wouldn't help much when it did predict though, since we'd still have no way to know what it predicted. |
Yeah, I suppose the delay would have to be fairly significant and/or based on RTT to be able to solve the issue fully. But it'd allow for actions that are currently in the pipeline to show up without trashing the client's predictions. The only reason I mention it is because I think it'd probably be less complicated to implement. But I'm not married to any particular solution. |
Ah, I see - yeah, that would probably work well in combination to the stuff in this PR to make sure the client is synced 1:1 with the server's view of the world while also not affecting actions like bridging. This is what I'm thinking right now:
|
…nteract fail if the prediction was a fail, we can assume that the client didn't do anything visual on its end, which avoids the need to resend blocks. This fixes block lag when towering as discussed in #6803. The more general issue in #6803 remains unresolved (that the server's block resyncing may overwrite additional client side predictions if it places a block before the server's resync for the initial placement arrives), but that's more complicated to fix and I'm not convinced on the correct method to resolve it yet. In any case, this change nets a decent improvement for everyone, regardless.
|
I implemented a check based on the predicted result field, and it completely eliminated the lag on my localhost client & server. Basically we can avoid failure syncing if the client predicted that nothing would happen. There still remains a wider issue in higher-latency conditions of server-side success syncing from placing block1 overwriting client-side predictions for placing block2 in high latency conditions. As far as I know, this sort of conflict wasn't an issue until #4683 was implemented. The syncing was changed in that PR to address the possibility of actions succeeding differently to the client predictions. The simplest solution for this would be to have On a last note, I will note that there seems to be a lot of other things that don't work very well under high latency conditions apart from this issue. For example it appears that the server syncs the player's flying state back to it unnecessarily, which causes weird issues when toggling flying in creative with a high ping. Would be worth looking into that sort of issue too if you want to improve the experience for high-latency players. |
Related issues & PRs
#4725
#4924
#3811
Changes
API changes
Player->syncBlocksBehavioural changes
This PR aims to fix a long-known issue where placing two or more blocks consecutively near each other will cause them to "flicker" on the client, and cause issues with bridging mechanics on higher latency clients. Instead of syncing all nearby blocks every time a player clicks a block, we move the logic directly to
World->useItemOn, so that we only synchronize blocks the client needs.Backwards compatibility
N/A
Tests
After changes in this PR:
https://streamable.com/vochsr
Before changes in this PR:
https://streamable.com/id65yg