Skip to content

Colyseus 0.18: Predict netcode hooks, schema v5 compatibility, v0.18.0 - #11

Open
endel wants to merge 13 commits into
mainfrom
0.18
Open

Colyseus 0.18: Predict netcode hooks, schema v5 compatibility, v0.18.0#11
endel wants to merge 13 commits into
mainfrom
0.18

Conversation

@endel

@endel endel commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary

Targets Colyseus 0.18 / @colyseus/schema v5, adds React bindings for the new Predict netcode, and moves the package to 0.18.0 — from this release on, @colyseus/react versioning tracks colyseus itself.

Netcode hooks (new src/predict module)

React bindings for the 0.18 Predict tools (client prediction, reconciliation, remote smoothing). Every hook exists standalone (useX(room, ...)) and room-bound via createRoomContext({ predict }):

  • usePredict — one shared, ref-counted Predict per room (StrictMode-safe deferred dispose)
  • useInput — the room's InputHandle
  • usePredictLoop — the frame driver; owns rAF, or { external: true } for an existing loop (e.g. R3F useFrame)
  • useReconciler — active prediction with lifecycle handled (late spawn, instance replacement, dispose)
  • useAttachAll — passive smoothing as an effect
  • useEventChannel — optimistic events with teardown plus reactivity
  • useEntityInstance / useSessionEntity — decoded-instance selection, identity-change re-renders only
  • useInputBuffer — the buffer-then-consume tap recipe (renamed from useLatch during review)
  • getSchemaInstance(snapshot) — bridges snapshots back to their decoded instance for predict.value() reads (renamed from getSource during review — self-describing, matches the "decoded schema instance" vocabulary)

Schema v5 compatibility

  • refId read via v5's Symbol.for("$refId") (v4 ~refId fallback kept)
  • tolerate v5's mixed Metadata shape when deriving field names
  • seed a no-op decoder.triggerChanges so decode() returns DataChange[] without an external subscriber
  • Snapshot<T> strips v5's publicly-declared Schema internals and accepts plain IArray/IMap shapes so frontends can type props without importing @colyseus/schema

Release

Test plan

  • npx vitest run — 154 passed, 3 skipped (includes new predictHooks.test.tsx and snapshotType.test.ts suites)
  • Test harnesses fixed for the v5 encoder lifecycle (discardChanges() after each encode())

🤖 Generated with Claude Code

FTWinston and others added 13 commits June 1, 2026 09:55
- refId now read via the v5 `Symbol.for("$refId")` tag (v4 "~refId" kept
  as fallback)
- tolerate v5's mixed Metadata shape (index→field objects + name→index
  reverse entries) when deriving field names
- seed a no-op `decoder.triggerChanges` — v5 only collects (and returns)
  DataChange[] when a subscriber exists, and the subscription wrapper
  relies on the returned changes
- Snapshot<T> strips Schema base internals that v5 declares publicly
  (`isTrackingPaused`, symbol-keyed $refId/$values), and accepts plain
  Array/Map-shaped interfaces (IArray/IMap) so frontends can type props
  without importing @colyseus/schema; compile-time assertions in
  snapshotType.test.ts

Assisted-by: Claude Opus 4.8
encode() does not clear the encoder's dirty state — the caller must run
discardChanges() after each patch (colyseus core does). Without it, the
next encode() re-emits stale ops against a frozen index space, which
schema v5 punishes with corrupted sequential array-splice patches
(client decoded [C, C] where the server had [C]). Fixes 4 array tests.

Also: RoomAvailable lost `publicAddress` in 0.18, and v5's Schema<any>
generic needs an explicit cast in the bench comparison walker.

Assisted-by: Claude Opus 4.8
Peer + dev deps: @colyseus/sdk ^0.18.1, @colyseus/schema ^5.0.8,
@colyseus/shared-types ^0.18.1.

Assisted-by: Claude Opus 4.8
New src/predict module, exported standalone and as room-bound variants
from createRoomContext({ predict }):

- usePredict — one shared Predict per room (Predict.get() constructs a
  fresh instance per call), ref-counted with StrictMode-safe deferred
  dispose
- useInput — the room's InputHandle (room.input() is idempotent)
- usePredictLoop — the frame driver: predict.tick(now) once per frame,
  then the caller's send loop; owns rAF, or `{ external: true }` returns
  drive(now) for an existing loop (e.g. R3F useFrame at priority -1)
- useReconciler — active prediction with the lifecycle handled: waits
  for late spawn, recreates on instance replacement, disposes on unmount
- useAttachAll — passive smoothing as an effect (detach on unmount)
- useEventChannel — defineEvent with teardown plus reactivity: injected
  callback wrappers re-render the caller, so has(key) derives work in
  render code
- useEntityInstance / useSessionEntity — select decoded instances,
  re-rendering only on identity change
- useLatch — the latch-then-consume tap recipe
- getSource(snapshot) — bridges useRoomState snapshots back to their
  decoded instance for predict.value() reads (recorded for free during
  snapshot creation via WeakMap)

Assisted-by: Claude Opus 4.8
Documents the structure-vs-motion split (snapshots at patch rate,
predict reads per frame outside the render cycle) and each hook.

Assisted-by: Claude Opus 4.8
"source" said where the object came from, not what you get back. The
new name matches the "decoded schema instance" vocabulary used across
the docs and the entity hooks. Pre-release rename — no alias kept.

Assisted-by: Claude Fable 5
CHANGELOG for the 0.18 branch (predict hooks, schema v5 compat, peer
dep bumps) and version bump 0.2.0 → 0.18.0 so @colyseus/react pairs
with colyseus's own version numbers from here on.

Assisted-by: Claude Fable 5
The IArray/IMap types and structural Snapshot branches were already
absorbed into this branch (49bb64a) in extended form — concrete
ArraySchema/MapSchema branches kept first for element inference, v5
internals stripped — so conflicts resolve to the branch's version.
New from the PR: prepublishOnly → prepare, so git revisions
(npm i colyseus/react-tools#0.18) build dist on install.

Assisted-by: Claude Fable 5
Assisted-by: Claude Fable 5
"Latch" is electronics jargon; input buffering is the gamedev term for
the same mechanism. Latch → InputBuffer, latch() → press(); consume()
and peek() unchanged. Docs recipe wording moves to "buffer, then
consume" alongside.

Assisted-by: Claude Fable 5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants