Skip to content

Fix movement stalls and one-round-trip lag on distant clients - #189

Closed
Fuchsoria wants to merge 1 commit into
Julian-adv:masterfrom
Fuchsoria:fix-distant-client-movement-lag
Closed

Fuchsoria wants to merge 1 commit into
Julian-adv:masterfrom
Fuchsoria:fix-distant-client-movement-lag

Conversation

@Fuchsoria

Copy link
Copy Markdown

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

  1. Input changes nulled the anchor. ServerMovement.direction() called
    clear(false), which cleared anchor, so sample() returned null and
    PlayerControl.svelte simply did not move the avatar. Every keypress, turn
    and sprint toggle cost one full RTT of standing still.
  2. The anchor was dated at arrival. The pose in a PlayerMovePath /
    PlayerMoveProgress describes where the player was when the server stamped
    it. Anchoring that on arrival trails the authoritative position by one-way
    delay indefinitely.
  3. The client could not measure its own latency. Heartbeat is a
    keepalive with no reply and no timestamp, so there was no basis for any
    compensation at all.

What changed

  • Protocol 104 — ClientMessage::Ping { seq, client_time_ms } and
    ServerMessage::Pong { seq, client_time_ms, server_time_ms }. Both variants
    are 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 folded
    in 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_ms
    is 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_ms stays on the wire for a future
    clock sync.
  • Anchor dated back by the measured one-way delay (capped at 250 ms). Later
    updates inherit the same offset through their server_time_ms delta.
  • The approved path keeps playing across an input change instead of being
    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:

before after
position trail 0.375 m 0.097 m

0.375 m is exactly the predicted one-way trip at 3 m/s (250 / 2 × 3 m/s), to
the 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.
  • Client: 1 418 tests across 163 files, svelte-check clean, prettier and eslint clean.
  • New client coverage: mocked RTT of 20/150/250/400 ms for the anchor shift and
    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 or
    retired seq, spike recovery, reset).
  • End-to-end against a running server: launched the server locally, drove
    ClientInfo → Ping ×6 over a real WebSocket with the shipped WASM codecs.
    Both sides report protocol 104, 6/6 Pong answered, every seq and echoed
    client_time_ms matched. This covers what unit tests cannot — a real socket,
    real MessagePack framing, and the server binary agreeing on the new variant
    names.

Not addressed

  • Input-to-first-motion is still one RTT (see above).
  • Remote players are still interpolated without reference to latency — a
    separate display problem from the player's own movement.
  • Combat is unchanged. Rewinding targets by the attacker's RTT on hit
    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.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@Fuchsoria

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Sep 27, 2026
@Fuchsoria
Fuchsoria marked this pull request as draft September 27, 2026 12:38
@Fuchsoria

Copy link
Copy Markdown
Author

Closed during last patches

@Fuchsoria Fuchsoria closed this Sep 27, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 27, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant