Show storage terminal interactions before the server confirms them - #217
Open
rubensworks wants to merge 1 commit into
Open
Show storage terminal interactions before the server confirms them#217rubensworks wants to merge 1 commit into
rubensworks wants to merge 1 commit into
Conversation
Every terminal interaction was a custom packet with no client-side prediction, so nothing happened until the server answered: the grabbed item appeared after a round trip, and the shown quantity only after the ingredient network observer had picked up the change, a few ticks later. The client now simulates a click as soon as it sends it: * The container change is simulated by running the same movement logic that the server runs, against a storage that holds what the client believes is available. So what is predicted is what the server does. * The shown quantities get the predicted change on top of the server-sent state, never merged into it, as the server sends diffs that would otherwise be applied twice. The server stays the only source of truth. A prediction is dropped as soon as the server sends the change it expected, and expires by itself when the server never does. Slots that the client changed but the server did not are corrected by sending the full container state after a click, which the regular per-slot sync can not do. Clicks that can not start a drag are also handled when the mouse button goes down instead of when it is released, like vanilla containers do when the cursor is empty. This drops the button hold time, which was part of every interaction. Also parse the search query once per view rebuild instead of once per shown ingredient, as predictions rebuild the view more often. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N51XZVzjfA7j3EUKbBaCZW
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Player report: "just annoying grabbing from storage isnt instant".
Why it happens
Every terminal interaction is a custom packet with no client-side prediction, so nothing at all happens until the server answers. Three delays stack up on a single grab:
ContainerScreenTerminalStorage.mouseClickedonly set a flag;handleClickran inmouseReleased. That is the whole button hold, typically 50-150ms, before the packet is even sent. Shift-clicking into storage already went through vanilla'squickMoveStackon press, so the two directions were not even consistent.SetSlotcorrect them.What this changes
The client now simulates a click as soon as it sends it.
The container change is simulated by running the server's own movement logic.
predictInsertIntoContainer,predictInsertMaxIntoContainerandpredictExtractMaxFromContainerSlotcall the realinsertIntoContainer/insertMaxIntoContainer/extractMaxFromContainerSlotagainst the client container and a storage holding what the client believes is available (IngredientComponentStorageCollectionWrapper). No movement rules are duplicated, so what is predicted is what the server will do, for every ingredient type. The prediction passes no player, so it never picks a slot's contents up into the cursor; where the server would do that, the prediction simply moves nothing and the player waits as before.The shown quantities get the predicted change layered on top of the server-sent state, in
TerminalStorageIngredientPredictions, and never merged into it. This matters: the server sends diffs, so a prediction merged into the client's mirror would be applied twice and the mirror would drift for as long as the GUI stays open.The server stays the only source of truth. A prediction is consumed when the server sends the change it expected (by quantity, so a second click stays shown while the first is confirmed), and expires on its own when the server never sends it. Slots the client changed but the server did not are the one case per-slot sync cannot fix — the server sees no change, so it sends nothing — so
handleStorageSlotClicknow ends withbroadcastFullState().Clicks that cannot start a drag are handled on press, gated by a new
ITerminalStorageTabClient#isClickHandledOnPress(default false, so third-party tabs are unaffected). This is exactly vanilla's rule: act on press while the cursor is empty, defer to release while it is not, since that press may be the start of a drag.Prediction can be turned off with the new client-side
guiStoragePredictInteractionsconfig.Also: the search query was parsed and its regex recompiled once per shown ingredient on every view rebuild. It is now compiled once per rebuild (
IngredientQueryMatchers), which matters more now that predictions rebuild the view more often.Deliberately not included: forcing a synchronous network observation on each click. It would shorten the window a prediction is shown, but it walks every position of every channel on the click's tick, which is a server-side cost on exactly the large networks that have had performance trouble before.
Testing
./gradlew buildand./gradlew runGameTestServerpass (45 game tests, 20 of them new):GameTestTerminalStorageClickPredictions— the simulated movements: one stack per quick move, limited by what the storage holds, partial stacks filled, occupied slots left alone, extraction limits.GameTestTerminalStorageIngredientPredictions— the view overlay: subtraction, an emptied instance disappearing, additions, the wildcard channel, confirmation by a server change (full, partial and larger-than-predicted).GameTestIngredientQueryMatchers— query matching, including invalid regexes matching nothing rather than throwing per ingredient.Verified in a dev client (clientdevbridge-cli) on a real network — a cable with an item interface on a chest of 192 diamonds, 128 iron and 32 logs, and a storage terminal part:
🤖 Generated with Claude Code
https://claude.ai/code/session_01N51XZVzjfA7j3EUKbBaCZW
Generated by Claude Code