/** * Reconciler — server-reconciled rollback for a locally-controlled entity whose * authoritative truth is a flat list of scalar `fields` on ONE schema instance. * * The active counterpart to {@link Predict}: where Predict passively smooths the * server stream for entities you DON'T control (lerp/reckon), the Reconciler * owns the predicted simulation of the entity you DO control. It applies your * inputs immediately (zero-latency local feel), buffers the unacknowledged ones, * and when the server's authoritative state arrives rewinds to that truth and * replays the still-unacked inputs — the standard rewind-and-replay rollback loop * (shared with `SimReconciler` via {@link RollbackController}), with the *server* * as the authority that provides the restore point. * * This is the flat-`fields` face of the shared engine: it mirrors a declared * `fields` list off one authoritative schema instance (adopt = copy those fields; * pose = read them back). For composite state across several instances, or an * opaque physics-engine handle, reach for `SimReconciler` (`predict.sim`). * * `fields` is OPTIONAL and defaults to EVERY scalar field the instance's schema * declares (numbers / booleans / strings, declaration order — the same metadata * walk as `predict.sim`'s auto-bind; child schemas, collections and `.noSync()` * fields are excluded). Pass an explicit list only to subset deliberately: hot * server-driven scalars (hp regen, timers) defeat the wire-precision reconcile * skip and surface as drift-telemetry corrections when they move; a string field * disables the history ring; adopt-only fields become mirrored into `.state` at * ack cadence (read them off the raw instance for decode cadence). With an * explicit subset, a dev-only diagnostic (debug overlay loaded) warns once per * schema field `step` touches that the list doesn't mirror. * * The acknowledgement lives on the INPUT HANDLE, not a clock: you send inputs * through `room.input(...)`, so that handle knows the server ack * (`input.lastProcessed`) and the seq you've sent (`input.sentCount`). * * Smooth error correction: a misprediction is absorbed into a per-field visual * offset that decays to 0 over a few frames, so corrections never pop. * * Input is FIXED-TIMESTEP and the reconciler is a pure OBSERVER: you mutate + send * through the input handle directly, and `predict.tick(now)` returns how many fixed * steps are due this frame. The reconciler watches the handle's `sentCount` and * predicts each new send (see {@link RollbackController}); render reads * ({@link value}) interpolate between the two latest steps so motion stays smooth * above the step rate. * * // Every send() transmits one input (body-less when unchanged), so each * // predicted step is delivered — predicted set == server-applied set. * const input = room.input({ type: MoveInput, mode: "reliable" }); * const me = predict.reconciler(player, { * input, * step: (ctx, state, command) => applyInput(state, command, LEVEL, ctx.dt), // ctx.dt shared w/ server * smoothMs: 65, * // fields: defaults to every scalar field of `player`'s schema. * // stepMs / stepSeconds default from `input`'s server-advertised rate. * }); * * // per frame: predict.tick returns how many fixed input steps are due; mutate * // + send each through the handle — the reconciler observes and predicts them: * const n = predict.tick(now); * for (let i = 0; i < n; i++) { * input.data.moveX = moveX; input.data.jump = jump; // stage the wire input * input.send(); // transmit + buffer for replay * } * draw(predict.value(player, "x"), predict.value(player, "y")); // one read idiom * * The reconciler registers its numeric `fields` into the owning Predict's * `predict.value(instance, field)` — the SAME render read as every passively- * smoothed entity (raw fallback before spawn / after dispose). `me.value(f)` * remains for direct handle reads. * * Hot path (`value`) is plain-number only; reconcile (cold, ~server-tick rate) * reads the authoritative instance. */ import { RollbackController, type RollbackOptions, type StepContext } from "./rollback.ts"; export type { StepContext, PredictSink } from "./rollback.ts"; export interface ReconcilerOptions extends RollbackOptions { /** * Deterministic input-application step, SHARED with the server. Mutates * `state` in place by applying `command` over `ctx.dt`. `ctx` leads (the * step-context-first argument convention): it carries the fixed `dt` * (matches the server's, so replay reproduces the server's result), * `ctx.memo` (freeze a value replay can't re-derive) and `ctx.predict` * (declare an optimistic event — live steps only, replay-safe). * * `command` is the buffered wire input the handle recorded at `send()` * (`input.at(seq)`) — the exact value the server decodes, read the same way on * the live catch-up step and on rollback replay, so lossy wire fields (a * `t.quantized` angle) reconcile by construction. Keep everything the step * reads on the input schema; anything you don't stage on `input.data` before * `send()` won't reach `step`. */ step: (ctx: StepContext, state: S, command: I) => void; /** * Fields to mirror from the server on reconcile (everything the step reads * or writes). Numeric fields additionally get smooth error correction; * non-numeric fields (e.g. `grounded`) are copied verbatim. * * OPTIONAL — when omitted, defaults to EVERY scalar field the instance's * schema declares (numbers / booleans / strings, declaration order; child * schemas, collections and `.noSync()` fields excluded — the same metadata * walk as `predict.sim`'s auto-bind). * * List explicitly only to SUBSET deliberately: hot server-driven scalars * (hp regen, timers) defeat the wire-precision reconcile skip and register * as drift-telemetry corrections when they move; string fields disable the * history ring; adopt-only fields become mirrored at ack cadence (read them * off the raw instance for decode cadence). With an explicit subset, a * dev-only diagnostic (debug overlay active) warns once per schema field * `step` touches that the list doesn't mirror. */ fields?: readonly (keyof S & string)[]; } export declare class Reconciler extends RollbackController { /** The current predicted step state (the "true" state, advanced one fixed * step per input). Read raw for logic; rendered via interpolation. */ private local; private readonly fields; private readonly numericFields; private readonly step; private readonly instance; /** Per-field wire quantizer, parallel to `fields`: maps a predicted float64 * to the exact value the wire would deliver (fround for `float32`, the * codec's dynamic rule for auto `number`, identity for exact types). */ private readonly wireRound; /** Per-seq predicted-state ring (values via {@link asScalar}): slot * `seq % size` holds the post-step field values for that seq; `historySeq` * validates the slot (-1 = empty). Written on every applied step — live AND * replay, so after a rollback the ring tracks the post-rollback trajectory. * Sized to the input handle's replay ring: same seq window, ages out together. */ private readonly history; private readonly historySeq; private readonly historySize; /** False when a declared field isn't a number/boolean at construction (the * ring can't represent it) — the short-circuit is then disabled and every * reconcile adopts, exactly the pre-history behavior. */ private readonly historyOn; /** Schema scalars absent from an EXPLICIT `fields` list — the dev * diagnostic's watch set. null ⇒ diagnostic off (derived list, complete * list, or no metadata): applyStep's check is then one null compare. */ private undeclared; /** Cached dev proxy over `local` handed to `step` while the debug overlay * is active (see {@link stepView}). */ private devView; constructor(instance: object, opts: ReconcilerOptions); /** * The TRUE predicted state — read it for game logic, and mutate it directly * to apply a per-step side-effect (e.g. a collision bounce). It carries no * interpolation / smooth-correction offset (that's what {@link value} adds * for rendering). To survive a reconcile, a mutation must be reproducible by * `step` during replay — record it per-seq and re-apply it inside `step`. * * Always current — inputs are stepped eagerly as you `send()` them (the * reconciler observes the handle). Plain object access — no string-keyed * get/set, no mutator closure. The reconciler tracks ONE entity, so its state * is naturally a single struct; this ports to `state.vy = …` in C# / Lua and * `state->vy = …` in C. */ get state(): S; /** * Rendered value: the predicted state interpolated between the previous and * current fixed step by {@link renderAlpha}, plus the decaying correction * offset. Smooth above the step rate (at ~one step of render latency). Numeric * fields only; non-numeric fields return the current value verbatim. */ value(field: keyof S & string): number; protected smoothedFields(): readonly string[]; protected readCurrent(field: string): number; protected applyStep(input: I): void; /** Dev-only view of `local` for `step`: warns ONCE per schema scalar missing * from the explicit `fields` list on first read/write — the "step touched a * field the reconciler doesn't mirror" hazard (prediction integrates on a * stale value; rubber-bands only under latency). String keys only: symbols * (runtime probes) and non-schema scratch keys forward untouched. Built * lazily on the first overlay-active step, then cached — identity is stable * across live and replay steps. Non-semantic by construction (forwards * everything unchanged) — ports substitute a native check or skip it, see * PORTING.md. */ private stepView; /** * Wire-precision compare: the prediction at `acked` (from the history ring) * against the decoded truth on the instance, each predicted value passed * through its field's wire quantizer. All fields indistinguishable ⇒ the * wire could not have told the server anything different ⇒ skip adopt+replay * (see {@link RollbackController.reconcile}). Any miss — ring slot aged out * or pre-reset, NaN, a genuine mismatch — falls back to a full adopt. */ protected truthMatchesAt(acked: number): boolean; protected snapshotPrev(): void; protected adoptTruth(): void; protected reseedState(): void; }