Skip to content

v0.1.1

Choose a tag to compare

@mococa mococa released this 27 May 08:04
· 42 commits to main since this release

Changelog

Breaking

  • Intent wire format changed. Frames now carry a per-send sequence in a 4-byte LE prefix: [CMSG_INTENT][sequence:u32][encoded intent]. Client and server must deploy together; mixing versions silently drops intents.
  • sendIntent now returns boolean and emits 'error' on unknown intent. It also emits 'error' and returns false until the server has assigned an entity to the peer.
  • RPC event payload field renamed from args to payload. Event payloads are now strongly typed via ClientEventPayloads / ServerEventPayloads.
  • GameClient.interpBuffer is now public — callers tune delay and staleWindow at runtime via setDelay / setStaleWindow.
  • Removed GameClient.interpolationDelayMs. Use client.interpolationDelay.
  • InterpolationBuffer.forget() removed (was a no-op).
  • shared/constants.ts no longer exports PORT — the server reads process.env.PORT (defaults to 3010).

Added

  • Per-send intent sequence numbers, separate from game tick. Contiguous on the client even when game ticks are skipped (intermittent senders), so the server's ack signal survives reorder and skip.
  • Server contiguous-ack with pending buffer. Out-of-order intents wait in pendingBySequence until the gap fills; only then are they applied in order. Bounded stall-skip after 16 missing sequences.
  • Play-out clock in InterpolationBuffer.apply(). Smoothed renderTick advances ~1 tick per call with ±10% rate-warp toward a wall-clock target, instead of recomputing position from absolute time every frame.
  • Stale-snapshot drop in reconcile. Snapshots with tick <= lastReconciledServerTick still update the interp buffer but skip reconcile (server position is monotonic; an older snapshot would rewind the local entity).
  • Cross-snapshot peer interpolation. When a peer is missing from a or b (server only sends dirty entities), the lerp walks outward to the nearest snapshots that contain it instead of freezing the peer.
  • Reset-tracking in reconcile. Predictions only replay for entities the snapshot actually included; previously, missing entities double-counted motion.
  • GameClientOptions.now — injectable wall-clock provider (defaults to performance.now), used by snapshot timestamps and the interp buffer. Lets tests drive a virtual clock.
  • Ping / pong RPC and RTT tracking. client.ping(), client.rttMs, and a 'pong' event with measured round-trip time. Server echoes ping via internal PONG RPC.
  • packages/netcode/src/packets/ test harness. VirtualNetwork, VirtualPeerTransport, makeHarness, plus bootstrap, drain, captureServerCounts, hashPositions helpers.
  • Scenario tests: convergence, reordering, pathological, peer-interpolation, intermittent-intents.
  • Insertion-sort in record() so reordered snapshots end up in serverTick order in the buffer.
  • latestReceivedAt tracking so staleWindow and tickRateMs use a true wall-clock high-water mark rather than the most-recent-by-tick entry.
  • Smoothed tick-rate estimate (EMA, alpha = 0.1) to absorb per-snapshot delivery jitter.
  • Example multiplayer-cube-arena mobile controls. On-screen forward / back / left / right buttons (touch pointer events), HUD ping display, safetyTicks-derived interpolation delay.
  • Deployable bundle script for the example (scripts/build.ts) producing dist/server.js + client assets + minimal package.json + README.

Fixed

  • Peer entity rubberband when multiple clients send simultaneously. Three causes addressed:
    1. record() appended in arrival order, so reordered packets produced a non-monotonic buffer and pair-finding fell into overrun with the wrong endpoint. Fixed by inserting sorted by serverTick.
    2. apply() used wall-clock receivedAt as the lerp parameter, so per-packet jitter warped peer motion speed directly. Fixed by interpolating in tick-space with a smoothed rate.
    3. staleWindow prune compared incoming receivedAt against the buffer's highest-serverTick entry, but reordering meant that entry's receivedAt could lag mid-buffer entries — causing false buffer wipes. Fixed by tracking latestReceivedAt.
  • Local cube snap-forward on every reconcile. Server's ack advanced to the highest seen tick, so when an out-of-order intent applied after a newer one already advanced the ack, the client dropped a still-in-flight prediction. Corrections accumulated up to ~8 ticks of motion under jitter bursts. Fixed by per-send sequence + contiguous-ack on the server.
  • Local-entity double-counting in reconcile. When the local entity wasn't in a snapshot (no dirty components that server tick), reconcile still replayed every unacked prediction on top of current world state. Now replay only runs for entities the snapshot reset.
  • renderTick blowing past newest.serverTick indefinitely. wallSpan = newest.receivedAt - oldest.receivedAt could be negative when reordering put oldest later in wall-clock than middle entries; tickRateMs collapsed to 0 and the formula produced unbounded values. Fixed by using latestReceivedAt - oldest.receivedAt for the wall span.
  • Per-frame peer speed oscillation under steady delivery. Even with sorted, in-order snapshots, recomputing renderTick from absolute time every frame re-anchored on every snapshot arrival, jittering the lerp parameter t. Fixed by the play-out clock.

Docs

  • Netcode README simplified: trimmed minimal example (no Health/shooting/AOI/lag-comp), clarified snapshot delta behavior, added "Not wired up yet" section listing unimplemented networked() options and their fallback behaviors.
  • Netcode README example now uses buyItem RPC pattern.

Full Changelog: v0.1.0...v0.1.1