v0.1.1
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. sendIntentnow returnsbooleanand emits'error'on unknown intent. It also emits'error'and returnsfalseuntil the server has assigned an entity to the peer.- RPC event payload field renamed from
argstopayload. Event payloads are now strongly typed viaClientEventPayloads/ServerEventPayloads. GameClient.interpBufferis now public — callers tunedelayandstaleWindowat runtime viasetDelay/setStaleWindow.- Removed
GameClient.interpolationDelayMs. Useclient.interpolationDelay. InterpolationBuffer.forget()removed (was a no-op).shared/constants.tsno longer exportsPORT— the server readsprocess.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
pendingBySequenceuntil the gap fills; only then are they applied in order. Bounded stall-skip after 16 missing sequences. - Play-out clock in
InterpolationBuffer.apply(). SmoothedrenderTickadvances ~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 <= lastReconciledServerTickstill 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
aorb(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 toperformance.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 internalPONGRPC. packages/netcode/src/packets/test harness.VirtualNetwork,VirtualPeerTransport,makeHarness, plusbootstrap,drain,captureServerCounts,hashPositionshelpers.- Scenario tests:
convergence,reordering,pathological,peer-interpolation,intermittent-intents. - Insertion-sort in
record()so reordered snapshots end up in serverTick order in the buffer. latestReceivedAttracking sostaleWindowandtickRateMsuse 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-arenamobile 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) producingdist/server.js+ client assets + minimalpackage.json+ README.
Fixed
- Peer entity rubberband when multiple clients send simultaneously. Three causes addressed:
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 byserverTick.apply()used wall-clockreceivedAtas the lerp parameter, so per-packet jitter warped peer motion speed directly. Fixed by interpolating in tick-space with a smoothed rate.staleWindowprune compared incomingreceivedAtagainst the buffer's highest-serverTickentry, but reordering meant that entry'sreceivedAtcould lag mid-buffer entries — causing false buffer wipes. Fixed by trackinglatestReceivedAt.
- 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.
renderTickblowing pastnewest.serverTickindefinitely.wallSpan = newest.receivedAt - oldest.receivedAtcould be negative when reordering putoldestlater in wall-clock than middle entries;tickRateMscollapsed to 0 and the formula produced unbounded values. Fixed by usinglatestReceivedAt - oldest.receivedAtfor the wall span.- Per-frame peer speed oscillation under steady delivery. Even with sorted, in-order snapshots, recomputing
renderTickfrom absolute time every frame re-anchored on every snapshot arrival, jittering the lerp parametert. 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
buyItemRPC pattern.
Full Changelog: v0.1.0...v0.1.1