Conversation
|
All contributors have signed the CLA ✍️ ✅ |
Author
|
I have read the CLA Document and I hereby sign the CLA |
Fuchsoria
marked this pull request as draft
September 27, 2026 12:38
Author
|
Closed during last patches |
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Summary
Movement feels bad for players far from the region. From Switzerland against the
Seoul server (~250 ms RTT) the avatar stood completely motionless for a full
round trip on every input change, and its drawn position trailed the
authoritative one by a whole one-way trip for as long as the walk lasted.
Both defects are client-side. The server already answers input off the movement
tick and ships positions every 200 ms, as doc/SERVER_PLAYER_MOVEMENT.md
specifies — the lag was entirely in how the client presented those answers.
This is deliberately scoped as the groundwork for the optional "phase 2" in
that document, not phase 2 itself.
What was wrong
ServerMovement.direction()calledclear(false), which clearedanchor, sosample()returnednullandPlayerControl.sveltesimply did not move the avatar. Every keypress, turnand sprint toggle cost one full RTT of standing still.
PlayerMovePath/PlayerMoveProgressdescribes where the player was when the server stampedit. Anchoring that on arrival trails the authoritative position by one-way
delay indefinitely.
Heartbeatis akeepalive with no reply and no timestamp, so there was no basis for any
compensation at all.
What changed
ClientMessage::Ping { seq, client_time_ms }andServerMessage::Pong { seq, client_time_ms, server_time_ms }. Both variantsare appended to their enums, since MessagePack encodes variants by integer
index. The server answers the probe ahead of any queued work, so it measures
the link rather than the server's load.
client/src/lib/network/linkLatency.ts— one probe every 2 s, RTT foldedin with EWMA (α = 0.25), jitter reported as the spread of an 8-sample window.
The client schedules the next probe on inbound traffic, so it keeps working
while idle. It deliberately does not track a clock offset:
server_time_msis an epoch stamp while the local clock is monotonic from an arbitrary origin,
so their difference is a large constant rather than a usable offset (confirmed
against a running server).
server_time_msstays on the wire for a futureclock sync.
updates inherit the same offset through their
server_time_msdelta.dropped. This is not prediction: the server still holds that route until the
new input lands, so continuing to show it is simply showing the truth a
moment longer. Stale replies are still rejected, so a pre-change approval
cannot resurrect an old path.
The server change is one match arm that answers the probe. Nothing in the
movement simulation, tick cadence or request throttling is touched.
Measurements
A/B against the shipped movement code (pre-fix and post-fix) on a simulated
link. At 250 ms RTT the drawn position trails by:
0.375 mis exactly the predicted one-way trip at 3 m/s (250 / 2 × 3 m/s), tothe centimetre — which is what makes the measurement trustworthy rather than
merely smaller.
The measurement is capped at 250 ms on purpose. Past that the harness's
geometry re-places the avatar from the origin on a new heading instead of
continuing from where it was, and the two headings diverge enough to swamp the
anchor's own contribution. Input-to-first-motion is still 1 RTT and this PR
does not change that; removing it needs the phase-2 predicted start, which is
not in this change.
Testing
cargo check --workspace --all-targets,cargo test --workspace --locked(1 869 passed, 0 failed),
cargo clippy --workspace --all-targets --locked -D warnings,cargo fmt --all --check— all pass.svelte-checkclean, prettier and eslint clean.for input changes during a move, a stop, and a click; 14 unit tests for
LinkLatency(zero before the first answer, pacing, rejecting a mismatched orretired
seq, spike recovery, reset).ClientInfo→Ping×6 over a real WebSocket with the shipped WASM codecs.Both sides report protocol 104, 6/6
Ponganswered, everyseqand echoedclient_time_msmatched. This covers what unit tests cannot — a real socket,real MessagePack framing, and the server binary agreeing on the new variant
names.
Not addressed
separate display problem from the player's own movement.
validation has real anti-cheat implications and deserves its own discussion.
License
This project is licensed under PolyForm Noncommercial 1.0.0, so the
contribution lands under the same terms. I have read and sign the
Contributor License Agreement — noting for the record
that its §2 grants the Maintainer a perpetual, irrevocable licence to this
contribution including commercial use, while the surrounding project remains
noncommercial.