import { type InputEncoderOptions, type InputMode } from '@colyseus/schema/input'; /** * Options accepted by `Room.input()`. Extends {@link InputEncoderOptions} * (mode / historySize) with a `type` field for the schema constructor. * Inputs are ALWAYS delta-encoded (the codec has no full-snapshot mode): * each send carries only changed fields, or a body-less frame on a no-change * tick. Every `send()` transmits one input; to skip a tick, just don't call * `send()`. * * Recommended for rollback netcode: `{ mode: "unreliable", historySize: 4 }` * — small redundant deltas, idempotent across drops via absolute-value wire ops. * Each packet re-sends the last `historySize` inputs, so a dropped one is * recovered from its successors and the server dedupes by wire seq; only a run * of losses longer than `historySize` actually loses input. * * Lag compensation works on either channel: a room that rewinds * (`allowRewindState` + a rewind group) gets a per-input `renderTime`/ * `reckonTime` stamp here too, and an input recovered redundantly from a later * packet still carries the instant it was sampled at. `mode` is purely a * delivery choice. * * **`mode:"unreliable"` is only actually unreliable on `@colyseus/h3-transport` * (WebTransport), which is experimental.** Every WebSocket transport lacks a * datagram channel and sends this traffic on the reliable one instead — * correct, and every input still arrives exactly once, but the redundancy ring * is then pure overhead (`historySize` duplicate slots the ordered channel * didn't need). On WebSocket, prefer `mode:"reliable"`. * * `I` is intentionally unconstrained: pinning it to `Schema` from this * SDK's copy of `@colyseus/schema` would reject user-side schemas coming * from a different copy of the package (npm hoisting, multi-version * installs). Runtime calls duck-type via the encoder, so a structural * match is enough. */ export interface InputOptions extends InputEncoderOptions { /** * Schema constructor for the input. Optional when the server room called * `defineInput()` — the schema then arrives via the JOIN handshake's input * reflection and is used automatically (the synthesized class mirrors the * server's fields, but `instanceof YourInput` won't pass on it). An explicit * ctor always wins over reflection — pass one to use your own class. * `room.input()` throws when neither source yields a constructor. */ type?: new () => I; /** * Your interpolation buffer in ms — how far in the PAST you render remote * entities (e.g. a `Predict` lerp `delay`). It feeds the stamped * `renderDelta = renderDelay + smoothedRtt()/2`, from which the server * derives `renderTime = reckonTime − renderDelta`: this term covers the * interp buffer, and the SDK adds the one-way downstream latency itself. So * pass ONLY your interp buffer, never the latency. * * **Usually omit this.** When you wire the handle through * `predict.reconciler(self, { input })` or `predict.sim({ input })`, the SDK * binds this to the Predict's lerp `delay` automatically, so the interp buffer * the remotes render at and the server's rewind instant are derived from ONE * number and can't drift out of sync. Set it explicitly only to override that * (e.g. you smooth remotes some other way) — an explicit value always wins. * * Default `0` — correct when you dead-reckon remote entities to current server * time (no interp lag). Has no effect unless the Room rewinds a * `mode:"snapshot"` group (which auto-enables the renderTime stamp). */ renderDelay?: number; /** * Predicate selecting which of this client's inputs the server may REWIND to — * i.e. which inputs carry their lag-comp timestamp on the wire. The client-side * mirror of the server room's `allowRewindState`: there the room records history * to rewind into; here you say which inputs are worth rewinding to. Omitted (the * default) stamps EVERY reliable input. * * Return `true` only for inputs the server lag-compensates — e.g. a firing input * that triggers a rewound hit-test — to drop the ~1-byte timestamp on the rest * (typically the majority of frames: moving/aiming without shooting). Evaluated * against the staged {@link InputHandle.data} on each {@link InputHandle.send}. * * A pure client-side bandwidth trim: the server already tolerates mixed * stamped/unstamped reliable inputs (an unstamped one reads `renderTime` 0 and * falls back to live), and the delta-coded stamp baseline self-syncs across the * gaps — no server change. Per-input ONLY: it gates the timestamp, not rewind * itself (that's the room's `allowRewindState`), so `() => false` just keeps the * server live for this client — it never disables rewind. * * **`mode:"reliable"` only.** An unreliable packet carries a whole ring of * inputs under one stamp block, which is all-or-nothing: mixing stamped and * unstamped slots would blow up the intra-packet deltas for no saving, since * the block ships either way. The predicate is not evaluated on that channel, * and setting it there warns once. * * ⚠ ONLY safe when the timestamp is consumed SERVER-side (a `mode:"snapshot"` * renderTime rewind for hit registration). If the CLIENT reads the stamp for its * OWN prediction — a `mode:"reckon"` room whose reconciler hit-tests at * `ctx.reckonTime` every step — every input needs it; do NOT set this there. */ allowRewind?: (data: I) => boolean; } /** * Per-room input handle returned by `Room.input()`. Mutate {@link data} * to stage the next input, then call {@link send} to encode and transmit on * the channel chosen at construction (reliable or unreliable). * * @example * ```typescript * const input = room.input({ type: MoveInput, mode: "unreliable" }); * input.data.vx = 10; * input.data.vy = 20; * input.send(); * ``` */ export interface InputHandle { /** Mutable schema instance — mutate, then call {@link send}. */ readonly data: I; /** Wire mode this handle was constructed with. */ readonly mode: InputMode; /** * Server-advertised fixed simulation/input step rate in Hz, from * `defineInput({ tickRate })` cascaded through the join handshake. Predict at * this exact rate (dt = 1/tickRate) so client rollback-replay stays * deterministic with the server — the single source of truth for the * timestep. `undefined` when the server didn't advertise one (fall back to * your own constant). */ readonly tickRate?: number; /** * The fixed step as **seconds** (`1/tickRate`) — the exact dt to predict and * rollback-replay each input with, matching the server's per-input dt. Prefer * this over hand-computing `1/tickRate`. `undefined` when no rate advertised. */ readonly stepSeconds?: number; /** * The fixed step as **milliseconds** (`1000/tickRate`), e.g. to drive a * fixed-timestep accumulator. `undefined` when no rate advertised. */ readonly stepMs?: number; /** * Server-advertised state-patch interval (ms) from the join handshake = the * reconcile/correction cadence (acks + authoritative state arrive this often). * A reconciler can tune its correction-smoothing window to it. `undefined` * when not advertised. */ readonly patchRate?: number; /** * Server-advertised physics sub-steps per input tick, from * `setFixedTimestep(..., { subSteps })` cascaded through the join handshake. * One input still drives ONE predicted/replayed step, but inside it the * simulation integrates this many engine steps of {@link subStepSeconds} — * physics at `tickRate * subSteps` Hz on a `tickRate` input rate. `1` when * the server didn't sub-step. The reconcilers default their step context's * `subSteps`/`subDt` from this. */ readonly subSteps: number; /** * The physics sub-step as **seconds** (`stepSeconds / subSteps`) — the exact * engine dt for each sub-step, bit-identical to the server's `ctx.subDt`. * Equals {@link stepSeconds} when `subSteps` is 1; `undefined` when no rate * advertised. */ readonly subStepSeconds?: number; /** The physics sub-step as **milliseconds** (`stepMs / subSteps`). `undefined` * when no rate advertised. */ readonly subStepMs?: number; /** * Encode the staged input and send it. Routes to the reliable or * unreliable channel based on {@link mode}. * * A no-op ONLY when the connection isn't open. Otherwise it always * transmits one input — a **body-less** frame when nothing changed since the * last send (the server decodes it as a no-op, holding the last values), so * the server receives exactly one input per `send()` and its consumed/ack * count tracks yours 1:1. To skip a tick entirely, simply don't call `send()`. * * Returns the seq assigned to this input — the same value the reconciler steps, * that {@link at}/{@link reckonTimeAt} key on, and that {@link sentCount} now * reads. It is `0` when nothing was sent (connection closed); seqs are 1-based, * so `const seq = input.send(); if (seq) …` doubles as a "did it transmit?" check. */ send(): number; /** * Subscribe to sends: `listener(seq)` fires synchronously at the END of each * {@link send} (after the input is buffered for replay and its reckon instant * stamped), with the just-sent seq. Returns an unsubscribe fn. * * This is how the prediction layer OBSERVES your input stream without owning the * send: `predict.reconciler(...)` / `predict.sim(...)` subscribe here and step * their predicted simulation for the sent input — so you mutate + send through * the handle (`input.data.x = …; input.send()`) and prediction stays current * with zero extra calls on your side. `at(seq)` / `reckonTimeAt(seq)` are valid * inside the listener. Empty (no allocation, no dispatch) when nothing subscribes. */ onSend(listener: (seq: number) => void): () => void; /** * Reset encoder state. Drops the unreliable ring buffer; re-marks every * populated field as dirty so the next send emits a full snapshot. Useful * on scene transitions; the SDK calls it itself on the reconnect path. * Observing rollback controllers follow a reset automatically (they poll * {@link epoch}) — no manual `Reconciler.reset()` wiring needed. */ reset(): void; /** * Monotonic reset counter: increments on every {@link reset}, whatever * triggered it (the SDK's reconnect path or an app call). Rollback * controllers poll it each tick and self-reset when it moves. Compare with * `!==`, never `+1` — multiple resets can land between polls. */ readonly epoch: number; /** * Last input the server has acknowledged PROCESSING into its authoritative * state (the server input-buffer's consumed count, echoed via the TIMED * prefix). The canonical server-reconciled-rollback ack — prune your pending * inputs against it (`seq <= lastProcessed`). `0` until the first ack. * * Lives here (not on `room.clock`) because it's an INPUT concern: this is the * channel you send through, so it's the channel that knows what's been acked. */ readonly lastProcessed: number; /** * Count of reliable inputs this handle has actually transmitted — equals the * seq the server will ack via {@link lastProcessed}. Key a client-side * prediction/replay buffer by this (read it right AFTER {@link send}). */ readonly sentCount: number; /** * Reliable inputs sent but not yet acked (`sentCount − lastProcessed`) — the * in-flight set a reconciler replays on rollback. */ readonly pendingCount: number; /** * Capacity (in seqs) of the replay ring backing {@link at} — the in-flight * window this handle can serve. A reconciler sizes its own per-seq state to * this, so both rings cover the same window and age out together. */ readonly replayBufferSize: number; /** * The buffered snapshot of the reliable input sent as `seq`, for * reconciliation replay — the client-side mirror of the server's input * buffer. Returns `undefined` if `seq` is already acked, was never sent, or * has aged out of the bounded ring. The returned instance is REUSED — read it * synchronously during replay, don't retain it. */ at(seq: number): I | undefined; /** * The reckon instant (server-clock ms) stamped onto reliable input `seq` — the * client's `serverNow()` estimate at send, the SAME value the server reads as * `channel.reckonTime` (and `rewind.lastSeenBy(sid)`). The reconciler surfaces * it as `ctx.reckonTime` so a prediction step hit-tests remote entities at the * exact instant the server rewinds to (live AND replay). Returns the RAW * stamp: `0` when reckon lag-comp isn't enabled (the room never rewinds to * it), or if `seq` is unsent, acked, aged out, or pre-clock-sync — the * reconciler resolves that `0` to the clock's live `serverNow()` before * surfacing it as `ctx.reckonTime` (see `StepContext.reckonTime` / * `lagCompActive`). */ reckonTimeAt(seq: number): number; }