@colyseus/core 0.18.24 → 0.18.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/input/types.ts"],
4
- "sourcesContent": ["/**\n * Types for the room input subsystem: the option shapes accepted by\n * `Room.defineInput()`, the per-client {@link InputAccessor} / room-wide\n * {@link InputAPI} surfaces, and the internal normalized options. Runtime\n * lives in `InputBuffer.ts`.\n */\n\n/**\n * Names of fields on `I` whose values are `number` \u2014 used by\n * `Room.defineInput()` to constrain `seqField` to actually-numeric fields\n * on the input schema. Filters out booleans, strings, methods, etc.\n */\nexport type NumericFieldsOf<I> = {\n [K in keyof I]: I[K] extends number ? (K extends string ? K : never) : never;\n}[keyof I];\n\n/**\n * Context handed to the `idle` callback form \u2014 see {@link IdleInput}. A reused\n * per-client scratch: read it inside the callback, don't store it. It carries\n * MECHANISM only \u2014 derived judgments (e.g. liveness) stay in userland; look the\n * client up via `this.clients.get(ctx.sessionId)` when your policy needs it.\n */\nexport interface IdleContext<I> {\n /** Last decoded input (`undefined` before the client's first). */\n latest: I | undefined;\n sessionId: string;\n}\n\n/**\n * What to synthesize when the buffer is empty (declared room-wide at\n * `defineInput({ idle })`, or per-call via {@link ConsumeOptions.idle}):\n * - `true` \u2014 pure schema defaults.\n * - `Partial<I>` \u2014 defaults overlaid with these fields. A full schema instance\n * (e.g. {@link InputAccessor.latest}) also works \u2014 fields are copied BY NAME,\n * so its prototype accessors are read correctly.\n * - callback \u2014 invoked LAZILY (only on an actually-empty tick) with an\n * {@link IdleContext}, returning either of the above. The right form when the\n * overrides take work to compute (entity lookups, held-button carry-over).\n *\n * \u26A0 Do NOT build overrides by spreading a schema instance (`{ ...latest, x }`):\n * schema fields live on the prototype, so the spread copies NOTHING of them.\n * Return `latest` itself, or name the fields (`{ x, plant: !!latest?.plant }`).\n */\nexport type IdleInput<I> = true | Partial<I> | ((ctx: IdleContext<I>) => true | Partial<I>);\n\n/**\n * Options for {@link InputAccessor.drain} / {@link InputAccessor.next}.\n * `idle` overrides the room-level `defineInput({ idle })` policy for this call\n * (see {@link IdleInput}); pass `false` to suppress it (force skip behavior).\n */\nexport interface ConsumeOptions<I> {\n idle?: IdleInput<I> | false;\n}\n\n/**\n * Input sanitization, declared at `defineInput({ sanitize })` \u2014 never trust the\n * wire. Applied to each decoded frame IN PLACE, before anything reads it\n * (`latest`, the buffer, the `idle` callback's ctx):\n * - **Map form** \u2014 per-field `[min, max]` range clamps with NaN-safe semantics:\n * `NaN` (both comparisons false) lands on `min`, closing the classic\n * `Math.min(NaN, \u2026)` poisoning hole.\n * - **Callback form** \u2014 arbitrary in-place fix-up (wrap an angle, enforce a\n * cross-field rule) for anything beyond ranges.\n *\n * Sanitizers MODIFY, they never reject \u2014 a malformed value becomes a legal one\n * instead of dropping the frame. Must not touch the `seqField` (it runs before\n * dedupe). Semantic validation (\"slot must name an owned weapon\") stays in\n * your sim \u2014 this handles value domains, not game rules.\n */\nexport type SanitizeInput<I> =\n | Partial<Record<NumericFieldsOf<I>, readonly [number, number]>>\n | ((input: I) => void);\n\n/** Options for `Room.defineInput()`. The user-facing twin of the internal\n * normalized {@link NormalizedInputOptions}. */\nexport interface DefineInputOptions<I = any> {\n seqField?: NumericFieldsOf<I>;\n bufferMaxSize?: number;\n /**\n * Input sanitization \u2014 never trust the wire. Applied IN PLACE to every\n * decoded frame before anything reads it (`latest`, the buffer, the `idle`\n * ctx). Map form = per-field `[min, max]` clamps with NaN-safe semantics\n * (NaN \u2192 min \u2014 closes the `Math.min(NaN, \u2026)` poisoning hole); callback form\n * = arbitrary in-place fix-up. Sanitizers MODIFY, never reject. See\n * {@link SanitizeInput}.\n *\n * ```ts\n * sanitize: { moveF: [-1, 1], pitch: [-PITCH_LIMIT, PITCH_LIMIT], dt: [0, MAX_DT] },\n * // or: sanitize: (f) => { f.angle = wrapAngle(f.angle); },\n * ```\n */\n sanitize?: SanitizeInput<I>;\n /**\n * Room-level absence policy: when a tick has no buffered input, bare\n * `drain()` / `next()` synthesize ONE \"idle\" frame from it \u2014 the schema's\n * defaults overlaid with the policy's overrides \u2014 so the sim loop needs no\n * empty-branch. Prefer the callback form, invoked lazily (only on\n * actually-empty ticks) with an {@link IdleContext} (`latest` + `sessionId`):\n *\n * ```ts\n * idle: ({ latest, sessionId }) => {\n * const p = this.state.players.get(sessionId); // closes over the room\n * return p ? { yaw: p.yaw, plant: !!latest?.plant } : true;\n * }\n * ```\n *\n * Not declaring it keeps the skip behavior (`drain()` \u2192 `[]`). Per-call\n * `{ idle }` overrides this default; `{ idle: false }` suppresses it.\n * Synthesized frames never advance the reconcile ack or `renderTime`.\n */\n idle?: IdleInput<I>;\n /**\n * Fixed step rate in **Hz** cascaded to the client via the join handshake;\n * it predicts at dt = 1/tickRate for deterministic rollback. Defaults to\n * the `setTimestep` rate \u2014 pass this only when the prediction\n * step differs from it. NOTE: a *rate*, not an interval \u2014 `1000/30` is the\n * step in ms, a classic mistake; use {@link stepMs} for that. Superseded by\n * {@link stepMs} / {@link stepSeconds} when those are given.\n */\n tickRate?: number;\n /**\n * Fixed step as a duration in **milliseconds** \u2014 the unit-safe alternative\n * to {@link tickRate} (`stepMs: 1000/30` is unambiguous where\n * `tickRate: 1000/30` is a bug). Normalized to the canonical Hz value\n * (`1000/stepMs`). Takes precedence over `tickRate`.\n */\n stepMs?: number;\n /**\n * Fixed step as a duration in **seconds** (e.g. `1/30`). Normalized to the\n * canonical Hz value (`1/stepSeconds`). Highest precedence.\n */\n stepSeconds?: number;\n /**\n * Physics sub-steps per input tick (integer \u2265 1, default 1) \u2014 decouples the\n * PHYSICS rate from the input/network rate. One input still drives exactly\n * one fixed step (the replay invariant), but the simulation integrates\n * `subSteps` engine steps of `stepSeconds/subSteps` inside it, identically\n * on client and server \u2014 physics at `tickRate * subSteps` Hz while sending\n * `tickRate` inputs/sec. Cascaded to clients via the join handshake;\n * both sides read the derived numbers off their step context\n * (`ctx.subSteps` / `ctx.subDt`) so N and dt can't drift apart.\n * Usually declared via {@link Room.setFixedTimestep}'s `subSteps` option\n * instead \u2014 pass it here only when the room runs its own loop.\n */\n subSteps?: number;\n}\n\n/** `true` when the defineInput opts declared an `idle` policy \u2014 narrows the\n * returned {@link InputAPI} so bare `next()` types non-optional `I`.\n * @internal \u2014 exported for {@link RoomInput.define}. */\nexport type IdleDeclared<O, I> = O extends { idle: IdleInput<I> } ? true : false;\n\n/**\n * Internal: input configuration captured by `Room.defineInput()` \u2014 the\n * normalized form of {@link DefineInputOptions} (step declarations resolved to\n * Hz, sanitizer compiled). The schema constructor is stored here so the\n * runtime doesn't need to know it through the public `room.inputs` (the\n * `.get(sessionId)` accessor).\n *\n * @internal\n */\nexport interface NormalizedInputOptions {\n /**\n * Schema constructor used to allocate per-client input instances on join.\n * Captured by `defineInput()` from its `type` argument.\n *\n * Typed loosely (`new () => any`) to sidestep type-identity issues across\n * duplicate `@colyseus/schema` installs; the runtime calls\n * `instance.clone()` and friends, which match by shape.\n */\n ctor: new () => any;\n\n /**\n * Name of a monotonically-increasing numeric field on the input schema used\n * to order and dedupe incoming frames. Unset by default (opt-in) \u2014 dedupe and\n * `.at()` lookup are off unless you name a field here. When set, the framework:\n * - Drops redundant frames (`input[seqField]` \u2264 the last-seen value are\n * discarded before they enter the buffer) \u2014 the unreliable-mode\n * ring-redundancy pattern.\n * - Powers `room.inputs.get(sessionId).at(value)` lookups.\n *\n * Despite the name, \"seq\" here is broader than an integer counter \u2014 any\n * monotonic numeric field works:\n * - **Sequence counter** (`\"seq\"`, `\"tick\"`, `\"frame\"`) \u2014 typical for\n * lockstep / rollback netcode.\n * - **Timestamp** (`\"timestamp\"`, milliseconds or seconds) \u2014 useful for\n * variable-rate clients, lag compensation, hit registration (a float-seconds\n * timestamp stamped on each saved move is the common shape).\n *\n * Whichever you use, the field must increase monotonically across frames\n * for dedupe to work.\n */\n seqField?: string;\n\n /**\n * > 0 enables per-client buffering of cloned snapshots \u2014 required for\n * `room.inputs.get(sessionId).drain() / .peek() / .at()` to return populated\n * data. Oldest drops on overflow. Set to `0` to disable (`.latest` still\n * works).\n */\n bufferMaxSize: number;\n\n /**\n * Fixed step rate (Hz) advertised to clients via the join handshake; they\n * predict at dt = 1/tickRate. Set explicitly via `defineInput`, or derived\n * from `setTimestep`. Unset = not advertised.\n */\n tickRate?: number;\n\n /**\n * Physics sub-steps per input tick (integer \u2265 1) advertised to clients via\n * the join handshake \u2014 the simulation integrates `subSteps` engine steps of\n * `(1/tickRate)/subSteps` per input on BOTH sides, so physics runs at\n * `tickRate * subSteps` Hz on a `tickRate` input/network rate. Set via\n * `setFixedTimestep(..., { subSteps })` (or `defineInput`). Unset/1 = not\n * advertised (input rate == physics rate).\n */\n subSteps?: number;\n\n /**\n * Room-level absence policy: bare `drain()` / `next()` synthesize one idle\n * frame from it when a tick has no input. See {@link IdleInput}.\n */\n idle?: IdleInput<any>;\n\n /**\n * COMPILED sanitizer (see {@link SanitizeInput} / `compileSanitizer`) \u2014\n * applied in place to each decoded frame before it becomes visible to\n * `latest` / the buffer / the idle ctx.\n */\n sanitize?: (instance: any) => void;\n}\n\n/**\n * Per-client input accessor returned by `room.inputs.get(sessionId)`. Combines the\n * latest decoded instance with the (optional) snapshot ring buffer.\n *\n * - {@link latest} \u2014 the bound Schema instance, mutated in place by the\n * decoder. Cheapest read; use when only the most recent state matters.\n * - {@link consume} / `for (const inp of accessor)` / {@link drain} /\n * {@link next} / {@link take} / {@link peek} / {@link at} \u2014 populated when\n * `defineInput()` was called with `bufferMaxSize > 0` (default 32). Use for\n * rollback netcode / lockstep where every frame matters.\n *\n * **Per-entity vs shared world** \u2014 pick the consume primitive by who integrates:\n * - **Per-entity** (each body integrates itself): iterate the accessor \u2014\n * `for (const inp of this.inputs.get(sid))` (sugar for {@link consume}) \u2014 and\n * sub-integrate one per input. N inputs = N steps for that player, ack lands\n * on the newest applied. The clean default. Iterating consumes ONE AT A TIME,\n * so {@link renderTime} tracks each input (lag comp stays exact per step) and\n * `break` leaves the rest buffered. {@link drain} returns the same set as an\n * array (retainable), but reports only the newest input's {@link renderTime}.\n * - **Shared world** (one solver step advances every body together): you can't\n * replay N inputs as N world steps without over-stepping everyone else, so\n * {@link next} exactly one input per entity per tick (or {@link take} a bounded\n * few and sub-step the solver per input). The ack then === inputs actually\n * simulated; `drain()`-then-apply-latest would silently jump the ack past\n * inputs the server never simulated, snapping the client's reconciler.\n *\n * Returned for unknown sessionIds and rooms without `defineInput()` is a\n * frozen no-op accessor (latest=undefined, drain/next/take/peek=[]/undefined,\n * at=undefined, size=0, clear=no-op).\n *\n * `Idle` is `true` when the room declared `defineInput({ idle })` \u2014 it narrows\n * {@link next} to non-optional `I` (bare calls always yield a frame).\n */\nexport interface InputAccessor<I = any, Idle extends boolean = false> {\n /** Latest decoded input. `undefined` when unknown sessionId or no input declared. */\n readonly latest: I | undefined;\n\n /**\n * Find the buffered snapshot whose `[seqField]` equals `value`. The field\n * name is the Room's `defineInput()` `seqField`. Linear scan \u2014 cheap for\n * typical buffer sizes; not intended for very large rings. Returns\n * `undefined` when no match is buffered (or `seqField` isn't configured).\n *\n * Useful for tick-aligned retrieval (lockstep, rollback).\n */\n at(value: number): I | undefined;\n\n /**\n * The blessed **per-entity** loop: consume each pending input oldest \u2192 newest,\n * ONE AT A TIME. Unlike {@link drain} (which reports only the newest input's\n * stamps up front), iterating updates {@link renderTime}/{@link reckonTime} and\n * advances {@link consumedCount} per yielded input \u2014 so per-step lag comp\n * rewinds to the exact instant of the input being applied:\n *\n * ```ts\n * for (const inp of this.inputs.get(sid)) { // sugar: accessor is iterable\n * applyInput(p, inp, dt);\n * this.collide(sid, p); // renderTime === THIS input's stamp\n * if (!p.alive) break; // break leaves the rest buffered\n * }\n * ```\n *\n * When the buffer is empty and an `idle` policy is in effect (room-level, or\n * per-call `opts.idle`), yields EXACTLY ONE idle frame (not consumed: no ack\n * bump, no {@link renderTime} change, {@link wasIdle} === true) then stops. No\n * policy + empty \u2192 zero iterations. Unknown sessionId / `bufferMaxSize: 0` \u2192\n * empty. `for (const inp of accessor)` is `consume()` with no opts.\n *\n * The returned iterator is POOLED (reused per call, allocation-free). For\n * `for..of` / spread / `Array.from` this is invisible. If you drive it by hand,\n * iterate it once to completion (or call `.return()` / `break`) before the next\n * `consume()`, and read each `.next()` result before the next \u2014 don't retain or\n * compare result objects. (Nesting two `consume()` loops over the SAME channel\n * is handled safely but is meaningless \u2014 both share one cursor.)\n */\n consume(opts?: ConsumeOptions<I>): IterableIterator<I>;\n\n /** Iterate pending (and/or one idle) inputs \u2014 sugar for {@link consume}(). */\n [Symbol.iterator](): IterableIterator<I>;\n\n /**\n * Take everything buffered (oldest \u2192 newest) and clear. Snapshots are safe to retain.\n *\n * With a room-level `defineInput({ idle })` policy the result is TOTAL: an\n * empty tick yields one synthesized \"idle\" frame instead of `[]`, so the sim\n * loop needs no empty-branch (gravity still integrates, action guards\n * naturally no-op on default values). The synthesized frame is the schema's\n * DEFAULTS overlaid with the policy's overrides (see {@link IdleInput}):\n *\n * ```ts\n * // declared once at defineInput({ idle: (ctx) => ({ ... }) }); then simply:\n * for (const f of this.inputs.get(sid).drain()) {\n * stepPlayer(p, f, world); // \u22651 frame, always\n * if (f.fire) tryFire(...); // idle frame: fire=false \u2192 no-op\n * }\n * ```\n *\n * Pass `{ idle }` to override the room policy for this call, or\n * `{ idle: false }` to suppress it.\n *\n * The idle frame is NOT a consumed input: it advances neither the reconcile\n * ack (`consumedCount`) nor {@link renderTime}. It is ONE reused instance per\n * client, refilled on each synthesis \u2014 read it within the tick, don't store\n * it. No synthesis for unknown sessionIds or `bufferMaxSize: 0`.\n */\n drain(opts?: ConsumeOptions<I>): I[];\n\n /**\n * Consume the single oldest buffered input and advance the reconcile ack by\n * exactly one \u2014 the complement to {@link peek}. Returns `undefined`\n * when the buffer is empty.\n *\n * Use this in a **shared-world** loop where one solver step advances every\n * entity together: you can't apply N-inputs-per-player as N world steps without\n * over-stepping the others, so consume one input per entity per fixed step\n * instead. {@link drain} (consume all, ack jumps to newest) is correct only\n * when each entity integrates ITSELF \u2014 see the {@link InputAccessor} docs.\n *\n * `consumedCount`/{@link renderTime} stay exact: the ack reflects only the\n * inputs you actually simulated, and `renderTime` is the stamp of THIS input.\n *\n * With a room-level `defineInput({ idle })` policy (or a per-call `{ idle }`\n * override) an empty tick returns a synthesized frame instead of `undefined`\n * \u2014 same contract as {@link drain}'s. A \"held key keeps moving through a\n * packet gap\" loop:\n *\n * @example\n * ```ts\n * // declared once:\n * inputs = this.defineInput(InputSchema, {\n * idle: ({ latest }) => latest ?? true, // empty tick \u2192 last input verbatim\n * });\n * // per tick:\n * this.setFixedTimestep(() => {\n * for (const [sid, body] of bodies) {\n * applyInputToBody(body, this.inputs.get(sid).next()); // typed I \u2014 never undefined\n * }\n * world.step(); // one step for everyone\n * }, 30);\n * ```\n */\n next(): Idle extends true ? I : I | undefined;\n next(opts: { idle: IdleInput<I> }): I;\n next(opts?: ConsumeOptions<I>): I | undefined;\n\n /**\n * Consume up to `n` oldest buffered inputs (oldest \u2192 newest) and advance the\n * reconcile ack by the count actually taken. Returns fewer than `n` (or `[]`)\n * when the buffer holds fewer. Use for shared-world **sub-stepping** \u2014 apply\n * each taken input as its own solver sub-step within one tick, bounding the\n * sub-steps per tick. {@link next} is the `take(1)` shorthand.\n */\n take(n: number): I[];\n\n /** Read everything buffered without consuming. */\n peek(): I[];\n\n /** Number of snapshots currently buffered. */\n readonly size: number;\n\n /**\n * Cumulative count of this client's inputs CONSUMED so far \u2014 the reconcile ack\n * echoed back to the client (its `lastProcessed`). Advances by one per\n * {@link next}, by the count taken per {@link take}/{@link drain}, by one per\n * yield while iterating {@link consume}, and on overflow-drop; a synthesized\n * idle frame NEVER advances it. Monotonic, so it doubles as a server-side input\n * seq \u2014 gate per-input actions off it (e.g. a fire cooldown counted in inputs)\n * instead of hand-rolling a counter. `0` before the first consume / for the\n * no-op accessor.\n */\n readonly consumedCount: number;\n\n /**\n * `true` when the most recently produced frame was a synthesized idle (vs a\n * real consumed input) \u2014 the \"skip work on idle\" guard. Set per consume\n * call/yield: `true` after a {@link next}/{@link consume}/{@link drain} that\n * fell back to the `idle` policy on an empty buffer, `false` after a real\n * input. Always `false` for {@link take} (never synthesizes) and the no-op\n * accessor. Reads meaningfully right after the consume that produced the frame.\n */\n readonly wasIdle: boolean;\n\n /** Drop all buffered snapshots (also resets the dedupe tracker). */\n clear(): void;\n\n /**\n * Server-clock render timestamp (ms since room start) of the most recently\n * consumed input \u2014 the time the client was rendering the world at when it\n * issued that input. Tracks the consume primitive: {@link drain}/{@link take}\n * report the NEWEST input they consumed; {@link next} and each\n * {@link consume}/`for..of` yield report THAT one input, so per-input loops\n * rewind to the exact instant of the input simulated.\n * `0` until the first render-time-stamped input is consumed. Populated only\n * when the room rewinds a `mode:\"snapshot\"` group (which auto-enables the\n * renderTime stamp) and `bufferMaxSize > 0`. Usually you don't read this\n * directly \u2014 pass the\n * sessionId to `rewind.lastSeenBy(sessionId)` (which resolves this value, clamps\n * it, and falls back to live) for lag-compensated \"what you see is what you\n * hit\" hit registration; use this getter only for a custom rewind time.\n */\n readonly renderTime: number;\n\n /**\n * Reckon-display stamp (server-clock ms) of the most recently consumed input\n * \u2014 the client's serverNow ESTIMATE when it sampled that input, i.e. the\n * exact instant its forward-reckoned (`mode:\"reckon\"`) entities were\n * displayed at. Stamping the display instant DIRECTLY makes the rewind read\n * immune to the client's RTT-estimation error: the client displayed\n * `f(serverNow_est)` and the server reads `f(serverNow_est)` \u2014 the same\n * index into the same recorded timeline, so estimation error cancels (the\n * `maxRewindMs` clamp still bounds spoofing). Same consume semantics as\n * {@link renderTime}; consumed automatically by `rewind.lastSeenBy()` \u2014\n * rarely read directly.\n *\n * Always a usable instant: when no reckon-stamped input has been consumed\n * yet (lag-comp off, client clock unsynced, nothing consumed), resolves to\n * the room clock's current `elapsedTime` \u2014 the fallback rooms previously\n * hand-wrote as `reckonTime > 0 ? reckonTime : now`. `rewind.lastSeenBy()`\n * internally reads the raw stamp, so its midpoint reconstruction still\n * engages for unstamped clients. The no-op accessor (unknown session / no\n * `defineInput()`) returns `0`.\n */\n readonly reckonTime: number;\n}\n\n/**\n * Returned by `Room.defineInput()`. Assign it to `this.inputs`, then call\n * `room.inputs.get(sessionId)` per tick to read each client's input stream \u2014\n * the same per-session lookup verb as `room.clients.get(sessionId)`.\n *\n * The fixed-step metadata lives HERE (one per room), not on the per-client\n * {@link InputAccessor} \u2014 these values are identical for every client, so they'd\n * be pure duplication per connection. `.get()` keeps the accessor and this\n * metadata cleanly separated.\n */\nexport type InputAPI<I = any, Idle extends boolean = false> = {\n /**\n * The input accessor for `sessionId` \u2014 the per-client stream you iterate /\n * `.next()` / `.drain()`. Returns a frozen no-op accessor for unknown sessions.\n * Mirrors `room.clients.get(sessionId)`.\n */\n get(sessionId: string): InputAccessor<I, Idle>;\n /**\n * Server-advertised fixed step rate in **Hz** \u2014 from `defineInput`'s\n * `tickRate`/`stepMs`/`stepSeconds`, or derived from `setTimestep` \u2014\n * the same value cascaded to predicting clients. `undefined` when no fixed\n * step is advertised.\n */\n readonly tickRate?: number;\n /**\n * The fixed step as **seconds** (`1/tickRate`): the dt to integrate one input\n * with, bit-identical to the client's prediction dt. Pass it to your physics\n * step (`applyInput(p, cmd, level, room.inputs.stepSeconds)`) so server and\n * client share one timestep instead of each keeping a constant that can drift.\n * `undefined` when no rate is advertised.\n */\n readonly stepSeconds?: number;\n /** The fixed step as **milliseconds** (`1000/tickRate`). `undefined` when no rate. */\n readonly stepMs?: number;\n /** Physics sub-steps per input tick (\u2265 1; `1` unless declared via\n * `setFixedTimestep(..., { subSteps })` / `defineInput`). */\n readonly subSteps: number;\n /** The physics sub-step as **seconds** (`stepSeconds / subSteps`) \u2014 the engine\n * dt when sub-stepping; equals {@link stepSeconds} when `subSteps` is 1.\n * `undefined` when no rate is advertised. */\n readonly subStepSeconds?: number;\n /** The physics sub-step as **milliseconds** (`stepMs / subSteps`). `undefined`\n * when no rate is advertised. */\n readonly subStepMs?: number;\n};\n"],
4
+ "sourcesContent": ["/**\n * Types for the room input subsystem: the option shapes accepted by\n * `Room.defineInput()`, the per-client {@link InputAccessor} / room-wide\n * {@link InputAPI} surfaces, and the internal normalized options. Runtime\n * lives in `InputBuffer.ts`.\n */\n\n/**\n * Names of fields on `I` whose values are `number` \u2014 used by\n * `Room.defineInput()` to constrain `seqField` to actually-numeric fields\n * on the input schema. Filters out booleans, strings, methods, etc.\n */\nexport type NumericFieldsOf<I> = {\n [K in keyof I]: I[K] extends number ? (K extends string ? K : never) : never;\n}[keyof I];\n\n/**\n * Context handed to the `idle` callback form \u2014 see {@link IdleInput}. A reused\n * per-client scratch: read it inside the callback, don't store it. It carries\n * MECHANISM only \u2014 derived judgments (e.g. liveness) stay in userland; look the\n * client up via `this.clients.get(ctx.sessionId)` when your policy needs it.\n */\nexport interface IdleContext<I> {\n /** Last decoded input (`undefined` before the client's first). */\n latest: I | undefined;\n sessionId: string;\n}\n\n/**\n * What to synthesize when the buffer is empty (declared room-wide at\n * `defineInput({ idle })`, or per-call via {@link ConsumeOptions.idle}):\n * - `true` \u2014 pure schema defaults.\n * - `Partial<I>` \u2014 defaults overlaid with these fields. A full schema instance\n * (e.g. {@link InputAccessor.latest}) also works \u2014 fields are copied BY NAME,\n * so its prototype accessors are read correctly.\n * - callback \u2014 invoked LAZILY (only on an actually-empty tick) with an\n * {@link IdleContext}, returning either of the above. The right form when the\n * overrides take work to compute (entity lookups, held-button carry-over).\n *\n * \u26A0 Do NOT build overrides by spreading a schema instance (`{ ...latest, x }`):\n * schema fields live on the prototype, so the spread copies NOTHING of them.\n * Return `latest` itself, or name the fields (`{ x, plant: !!latest?.plant }`).\n */\nexport type IdleInput<I> = true | Partial<I> | ((ctx: IdleContext<I>) => true | Partial<I>);\n\n/**\n * Options for {@link InputAccessor.drain} / {@link InputAccessor.next}.\n * `idle` overrides the room-level `defineInput({ idle })` policy for this call\n * (see {@link IdleInput}); pass `false` to suppress it (force skip behavior).\n */\nexport interface ConsumeOptions<I> {\n idle?: IdleInput<I> | false;\n}\n\n/**\n * Input sanitization, declared at `defineInput({ sanitize })` \u2014 never trust the\n * wire. Applied to each decoded frame IN PLACE, before anything reads it\n * (`latest`, the buffer, the `idle` callback's ctx):\n * - **Map form** \u2014 per-field `[min, max]` range clamps with NaN-safe semantics:\n * `NaN` (both comparisons false) lands on `min`, closing the classic\n * `Math.min(NaN, \u2026)` poisoning hole.\n * - **Callback form** \u2014 arbitrary in-place fix-up (wrap an angle, enforce a\n * cross-field rule) for anything beyond ranges.\n *\n * Sanitizers MODIFY, they never reject \u2014 a malformed value becomes a legal one\n * instead of dropping the frame. Must not touch the `seqField` (it runs before\n * dedupe). Semantic validation (\"slot must name an owned weapon\") stays in\n * your sim \u2014 this handles value domains, not game rules.\n */\nexport type SanitizeInput<I> =\n | Partial<Record<NumericFieldsOf<I>, readonly [number, number]>>\n | ((input: I) => void);\n\n/** Options for `Room.defineInput()`. The user-facing twin of the internal\n * normalized {@link NormalizedInputOptions}. */\nexport interface DefineInputOptions<I = any> {\n seqField?: NumericFieldsOf<I>;\n bufferMaxSize?: number;\n /**\n * Input sanitization \u2014 never trust the wire. Applied IN PLACE to every\n * decoded frame before anything reads it (`latest`, the buffer, the `idle`\n * ctx). Map form = per-field `[min, max]` clamps with NaN-safe semantics\n * (NaN \u2192 min \u2014 closes the `Math.min(NaN, \u2026)` poisoning hole); callback form\n * = arbitrary in-place fix-up. Sanitizers MODIFY, never reject. See\n * {@link SanitizeInput}.\n *\n * ```ts\n * sanitize: { moveF: [-1, 1], pitch: [-PITCH_LIMIT, PITCH_LIMIT], dt: [0, MAX_DT] },\n * // or: sanitize: (f) => { f.angle = wrapAngle(f.angle); },\n * ```\n */\n sanitize?: SanitizeInput<I>;\n /**\n * Room-level absence policy: when a tick has no buffered input, bare\n * `drain()` / `next()` synthesize ONE \"idle\" frame from it \u2014 the schema's\n * defaults overlaid with the policy's overrides \u2014 so the sim loop needs no\n * empty-branch. Prefer the callback form, invoked lazily (only on\n * actually-empty ticks) with an {@link IdleContext} (`latest` + `sessionId`):\n *\n * ```ts\n * idle: ({ latest, sessionId }) => {\n * const p = this.state.players.get(sessionId); // closes over the room\n * return p ? { yaw: p.yaw, plant: !!latest?.plant } : true;\n * }\n * ```\n *\n * Not declaring it keeps the skip behavior (`drain()` \u2192 `[]`). Per-call\n * `{ idle }` overrides this default; `{ idle: false }` suppresses it.\n * Synthesized frames never advance the reconcile ack or `renderTime`.\n */\n idle?: IdleInput<I>;\n /**\n * Fixed step rate in **Hz** cascaded to the client via the join handshake;\n * it predicts at dt = 1/tickRate for deterministic rollback. Defaults to\n * the `setTimestep` rate \u2014 pass this only when the prediction\n * step differs from it. NOTE: a *rate*, not an interval \u2014 `1000/30` is the\n * step in ms, a classic mistake; use {@link stepMs} for that. Superseded by\n * {@link stepMs} / {@link stepSeconds} when those are given.\n */\n tickRate?: number;\n /**\n * Fixed step as a duration in **milliseconds** \u2014 the unit-safe alternative\n * to {@link tickRate} (`stepMs: 1000/30` is unambiguous where\n * `tickRate: 1000/30` is a bug). Normalized to the canonical Hz value\n * (`1000/stepMs`). Takes precedence over `tickRate`.\n */\n stepMs?: number;\n /**\n * Fixed step as a duration in **seconds** (e.g. `1/30`). Normalized to the\n * canonical Hz value (`1/stepSeconds`). Highest precedence.\n */\n stepSeconds?: number;\n /**\n * Physics sub-steps per input tick (integer \u2265 1, default 1) \u2014 decouples the\n * PHYSICS rate from the input/network rate. One input still drives exactly\n * one fixed step (the replay invariant), but the simulation integrates\n * `subSteps` engine steps of `stepSeconds/subSteps` inside it, identically\n * on client and server \u2014 physics at `tickRate * subSteps` Hz while sending\n * `tickRate` inputs/sec. Cascaded to clients via the join handshake;\n * both sides read the derived numbers off their step context\n * (`ctx.subSteps` / `ctx.subDt`) so N and dt can't drift apart.\n * Usually declared via {@link Room.setFixedTimestep}'s `subSteps` option\n * instead \u2014 pass it here only when the room runs its own loop.\n */\n subSteps?: number;\n}\n\n/** `true` when the defineInput opts declared an `idle` policy \u2014 narrows the\n * returned {@link InputAPI} so bare `next()` types non-optional `I`.\n * @internal \u2014 exported for {@link RoomInput.define}. */\nexport type IdleDeclared<O, I> = O extends { idle: IdleInput<I> } ? true : false;\n\n/**\n * Internal: input configuration captured by `Room.defineInput()` \u2014 the\n * normalized form of {@link DefineInputOptions} (step declarations resolved to\n * Hz, sanitizer compiled). The schema constructor is stored here so the\n * runtime doesn't need to know it through the public `room.inputs` (the\n * `.get(sessionId)` accessor).\n *\n * @internal\n */\nexport interface NormalizedInputOptions {\n /**\n * Schema constructor used to allocate per-client input instances on join.\n * Captured by `defineInput()` from its `type` argument.\n *\n * Typed loosely (`new () => any`) to sidestep type-identity issues across\n * duplicate `@colyseus/schema` installs; the runtime calls\n * `instance.clone()` and friends, which match by shape.\n */\n ctor: new () => any;\n\n /**\n * Name of a monotonically-increasing numeric field on the input schema used\n * to order and dedupe incoming frames. Unset by default (opt-in) \u2014 dedupe and\n * `.at()` lookup are off unless you name a field here. When set, the framework:\n * - Drops redundant frames (`input[seqField]` \u2264 the last-seen value are\n * discarded before they enter the buffer) \u2014 the unreliable-mode\n * ring-redundancy pattern.\n * - Powers `room.inputs.get(sessionId).at(value)` lookups.\n *\n * Despite the name, \"seq\" here is broader than an integer counter \u2014 any\n * monotonic numeric field works:\n * - **Sequence counter** (`\"seq\"`, `\"tick\"`, `\"frame\"`) \u2014 typical for\n * lockstep / rollback netcode.\n * - **Timestamp** (`\"timestamp\"`, milliseconds or seconds) \u2014 useful for\n * variable-rate clients, lag compensation, hit registration (a float-seconds\n * timestamp stamped on each saved move is the common shape).\n *\n * Whichever you use, the field must increase monotonically across frames\n * for dedupe to work.\n */\n seqField?: string;\n\n /**\n * > 0 enables per-client buffering of input snapshots \u2014 required for\n * `room.inputs.get(sessionId).drain() / .peek() / .at()` to return populated\n * data. Oldest drops on overflow. Set to `0` to disable (`.latest` still\n * works).\n */\n bufferMaxSize: number;\n\n /**\n * Fixed step rate (Hz) advertised to clients via the join handshake; they\n * predict at dt = 1/tickRate. Set explicitly via `defineInput`, or derived\n * from `setTimestep`. Unset = not advertised.\n */\n tickRate?: number;\n\n /**\n * Physics sub-steps per input tick (integer \u2265 1) advertised to clients via\n * the join handshake \u2014 the simulation integrates `subSteps` engine steps of\n * `(1/tickRate)/subSteps` per input on BOTH sides, so physics runs at\n * `tickRate * subSteps` Hz on a `tickRate` input/network rate. Set via\n * `setFixedTimestep(..., { subSteps })` (or `defineInput`). Unset/1 = not\n * advertised (input rate == physics rate).\n */\n subSteps?: number;\n\n /**\n * Room-level absence policy: bare `drain()` / `next()` synthesize one idle\n * frame from it when a tick has no input. See {@link IdleInput}.\n */\n idle?: IdleInput<any>;\n\n /**\n * COMPILED sanitizer (see {@link SanitizeInput} / `compileSanitizer`) \u2014\n * applied in place to each decoded frame before it becomes visible to\n * `latest` / the buffer / the idle ctx.\n */\n sanitize?: (instance: any) => void;\n\n /**\n * COMPILED copy of a decoded frame for the buffer (see `compileSnapshot`).\n * Reads and writes like the schema, without its change tracking.\n */\n snapshot: (instance: any) => any;\n}\n\n/**\n * Per-client input accessor returned by `room.inputs.get(sessionId)`. Combines the\n * latest decoded instance with the (optional) snapshot ring buffer.\n *\n * - {@link latest} \u2014 the bound Schema instance, mutated in place by the\n * decoder. Cheapest read; use when only the most recent state matters.\n * - {@link consume} / `for (const inp of accessor)` / {@link drain} /\n * {@link next} / {@link take} / {@link peek} / {@link at} \u2014 populated when\n * `defineInput()` was called with `bufferMaxSize > 0` (default 32). Use for\n * rollback netcode / lockstep where every frame matters.\n *\n * **Per-entity vs shared world** \u2014 pick the consume primitive by who integrates:\n * - **Per-entity** (each body integrates itself): iterate the accessor \u2014\n * `for (const inp of this.inputs.get(sid))` (sugar for {@link consume}) \u2014 and\n * sub-integrate one per input. N inputs = N steps for that player, ack lands\n * on the newest applied. The clean default. Iterating consumes ONE AT A TIME,\n * so {@link renderTime} tracks each input (lag comp stays exact per step) and\n * `break` leaves the rest buffered. {@link drain} returns the same set as an\n * array (retainable), but reports only the newest input's {@link renderTime}.\n * - **Shared world** (one solver step advances every body together): you can't\n * replay N inputs as N world steps without over-stepping everyone else, so\n * {@link next} exactly one input per entity per tick (or {@link take} a bounded\n * few and sub-step the solver per input). The ack then === inputs actually\n * simulated; `drain()`-then-apply-latest would silently jump the ack past\n * inputs the server never simulated, snapping the client's reconciler.\n *\n * Returned for unknown sessionIds and rooms without `defineInput()` is a\n * frozen no-op accessor (latest=undefined, drain/next/take/peek=[]/undefined,\n * at=undefined, size=0, clear=no-op).\n *\n * `Idle` is `true` when the room declared `defineInput({ idle })` \u2014 it narrows\n * {@link next} to non-optional `I` (bare calls always yield a frame).\n */\nexport interface InputAccessor<I = any, Idle extends boolean = false> {\n /** Latest decoded input. `undefined` when unknown sessionId or no input declared. */\n readonly latest: I | undefined;\n\n /**\n * Find the buffered snapshot whose `[seqField]` equals `value`. The field\n * name is the Room's `defineInput()` `seqField`. Linear scan \u2014 cheap for\n * typical buffer sizes; not intended for very large rings. Returns\n * `undefined` when no match is buffered (or `seqField` isn't configured).\n *\n * Useful for tick-aligned retrieval (lockstep, rollback).\n */\n at(value: number): I | undefined;\n\n /**\n * The blessed **per-entity** loop: consume each pending input oldest \u2192 newest,\n * ONE AT A TIME. Unlike {@link drain} (which reports only the newest input's\n * stamps up front), iterating updates {@link renderTime}/{@link reckonTime} and\n * advances {@link consumedCount} per yielded input \u2014 so per-step lag comp\n * rewinds to the exact instant of the input being applied:\n *\n * ```ts\n * for (const inp of this.inputs.get(sid)) { // sugar: accessor is iterable\n * applyInput(p, inp, dt);\n * this.collide(sid, p); // renderTime === THIS input's stamp\n * if (!p.alive) break; // break leaves the rest buffered\n * }\n * ```\n *\n * When the buffer is empty and an `idle` policy is in effect (room-level, or\n * per-call `opts.idle`), yields EXACTLY ONE idle frame (not consumed: no ack\n * bump, no {@link renderTime} change, {@link wasIdle} === true) then stops. No\n * policy + empty \u2192 zero iterations. Unknown sessionId / `bufferMaxSize: 0` \u2192\n * empty. `for (const inp of accessor)` is `consume()` with no opts.\n *\n * The returned iterator is POOLED (reused per call, allocation-free). For\n * `for..of` / spread / `Array.from` this is invisible. If you drive it by hand,\n * iterate it once to completion (or call `.return()` / `break`) before the next\n * `consume()`, and read each `.next()` result before the next \u2014 don't retain or\n * compare result objects. (Nesting two `consume()` loops over the SAME channel\n * is handled safely but is meaningless \u2014 both share one cursor.)\n */\n consume(opts?: ConsumeOptions<I>): IterableIterator<I>;\n\n /** Iterate pending (and/or one idle) inputs \u2014 sugar for {@link consume}(). */\n [Symbol.iterator](): IterableIterator<I>;\n\n /**\n * Take everything buffered (oldest \u2192 newest) and clear. Snapshots are safe to retain.\n *\n * With a room-level `defineInput({ idle })` policy the result is TOTAL: an\n * empty tick yields one synthesized \"idle\" frame instead of `[]`, so the sim\n * loop needs no empty-branch (gravity still integrates, action guards\n * naturally no-op on default values). The synthesized frame is the schema's\n * DEFAULTS overlaid with the policy's overrides (see {@link IdleInput}):\n *\n * ```ts\n * // declared once at defineInput({ idle: (ctx) => ({ ... }) }); then simply:\n * for (const f of this.inputs.get(sid).drain()) {\n * stepPlayer(p, f, world); // \u22651 frame, always\n * if (f.fire) tryFire(...); // idle frame: fire=false \u2192 no-op\n * }\n * ```\n *\n * Pass `{ idle }` to override the room policy for this call, or\n * `{ idle: false }` to suppress it.\n *\n * The idle frame is NOT a consumed input: it advances neither the reconcile\n * ack (`consumedCount`) nor {@link renderTime}. It is ONE reused instance per\n * client, refilled on each synthesis \u2014 read it within the tick, don't store\n * it. No synthesis for unknown sessionIds or `bufferMaxSize: 0`.\n */\n drain(opts?: ConsumeOptions<I>): I[];\n\n /**\n * Consume the single oldest buffered input and advance the reconcile ack by\n * exactly one \u2014 the complement to {@link peek}. Returns `undefined`\n * when the buffer is empty.\n *\n * Use this in a **shared-world** loop where one solver step advances every\n * entity together: you can't apply N-inputs-per-player as N world steps without\n * over-stepping the others, so consume one input per entity per fixed step\n * instead. {@link drain} (consume all, ack jumps to newest) is correct only\n * when each entity integrates ITSELF \u2014 see the {@link InputAccessor} docs.\n *\n * `consumedCount`/{@link renderTime} stay exact: the ack reflects only the\n * inputs you actually simulated, and `renderTime` is the stamp of THIS input.\n *\n * With a room-level `defineInput({ idle })` policy (or a per-call `{ idle }`\n * override) an empty tick returns a synthesized frame instead of `undefined`\n * \u2014 same contract as {@link drain}'s. A \"held key keeps moving through a\n * packet gap\" loop:\n *\n * @example\n * ```ts\n * // declared once:\n * inputs = this.defineInput(InputSchema, {\n * idle: ({ latest }) => latest ?? true, // empty tick \u2192 last input verbatim\n * });\n * // per tick:\n * this.setFixedTimestep(() => {\n * for (const [sid, body] of bodies) {\n * applyInputToBody(body, this.inputs.get(sid).next()); // typed I \u2014 never undefined\n * }\n * world.step(); // one step for everyone\n * }, 30);\n * ```\n */\n next(): Idle extends true ? I : I | undefined;\n next(opts: { idle: IdleInput<I> }): I;\n next(opts?: ConsumeOptions<I>): I | undefined;\n\n /**\n * Consume up to `n` oldest buffered inputs (oldest \u2192 newest) and advance the\n * reconcile ack by the count actually taken. Returns fewer than `n` (or `[]`)\n * when the buffer holds fewer. Use for shared-world **sub-stepping** \u2014 apply\n * each taken input as its own solver sub-step within one tick, bounding the\n * sub-steps per tick. {@link next} is the `take(1)` shorthand.\n */\n take(n: number): I[];\n\n /** Read everything buffered without consuming. */\n peek(): I[];\n\n /** Number of snapshots currently buffered. */\n readonly size: number;\n\n /**\n * Cumulative count of this client's inputs CONSUMED so far \u2014 the reconcile ack\n * echoed back to the client (its `lastProcessed`). Advances by one per\n * {@link next}, by the count taken per {@link take}/{@link drain}, by one per\n * yield while iterating {@link consume}, and on overflow-drop; a synthesized\n * idle frame NEVER advances it. Monotonic, so it doubles as a server-side input\n * seq \u2014 gate per-input actions off it (e.g. a fire cooldown counted in inputs)\n * instead of hand-rolling a counter. `0` before the first consume / for the\n * no-op accessor.\n */\n readonly consumedCount: number;\n\n /**\n * `true` when the most recently produced frame was a synthesized idle (vs a\n * real consumed input) \u2014 the \"skip work on idle\" guard. Set per consume\n * call/yield: `true` after a {@link next}/{@link consume}/{@link drain} that\n * fell back to the `idle` policy on an empty buffer, `false` after a real\n * input. Always `false` for {@link take} (never synthesizes) and the no-op\n * accessor. Reads meaningfully right after the consume that produced the frame.\n */\n readonly wasIdle: boolean;\n\n /** Drop all buffered snapshots (also resets the dedupe tracker). */\n clear(): void;\n\n /**\n * Server-clock render timestamp (ms since room start) of the most recently\n * consumed input \u2014 the time the client was rendering the world at when it\n * issued that input. Tracks the consume primitive: {@link drain}/{@link take}\n * report the NEWEST input they consumed; {@link next} and each\n * {@link consume}/`for..of` yield report THAT one input, so per-input loops\n * rewind to the exact instant of the input simulated.\n * `0` until the first render-time-stamped input is consumed. Populated only\n * when the room rewinds a `mode:\"snapshot\"` group (which auto-enables the\n * renderTime stamp) and `bufferMaxSize > 0`. Usually you don't read this\n * directly \u2014 pass the\n * sessionId to `rewind.lastSeenBy(sessionId)` (which resolves this value, clamps\n * it, and falls back to live) for lag-compensated \"what you see is what you\n * hit\" hit registration; use this getter only for a custom rewind time.\n */\n readonly renderTime: number;\n\n /**\n * Reckon-display stamp (server-clock ms) of the most recently consumed input\n * \u2014 the client's serverNow ESTIMATE when it sampled that input, i.e. the\n * exact instant its forward-reckoned (`mode:\"reckon\"`) entities were\n * displayed at. Stamping the display instant DIRECTLY makes the rewind read\n * immune to the client's RTT-estimation error: the client displayed\n * `f(serverNow_est)` and the server reads `f(serverNow_est)` \u2014 the same\n * index into the same recorded timeline, so estimation error cancels (the\n * `maxRewindMs` clamp still bounds spoofing). Same consume semantics as\n * {@link renderTime}; consumed automatically by `rewind.lastSeenBy()` \u2014\n * rarely read directly.\n *\n * Always a usable instant: when no reckon-stamped input has been consumed\n * yet (lag-comp off, client clock unsynced, nothing consumed), resolves to\n * the room clock's current `elapsedTime` \u2014 the fallback rooms previously\n * hand-wrote as `reckonTime > 0 ? reckonTime : now`. `rewind.lastSeenBy()`\n * internally reads the raw stamp, so its midpoint reconstruction still\n * engages for unstamped clients. The no-op accessor (unknown session / no\n * `defineInput()`) returns `0`.\n */\n readonly reckonTime: number;\n}\n\n/**\n * Returned by `Room.defineInput()`. Assign it to `this.inputs`, then call\n * `room.inputs.get(sessionId)` per tick to read each client's input stream \u2014\n * the same per-session lookup verb as `room.clients.get(sessionId)`.\n *\n * The fixed-step metadata lives HERE (one per room), not on the per-client\n * {@link InputAccessor} \u2014 these values are identical for every client, so they'd\n * be pure duplication per connection. `.get()` keeps the accessor and this\n * metadata cleanly separated.\n */\nexport type InputAPI<I = any, Idle extends boolean = false> = {\n /**\n * The input accessor for `sessionId` \u2014 the per-client stream you iterate /\n * `.next()` / `.drain()`. Returns a frozen no-op accessor for unknown sessions.\n * Mirrors `room.clients.get(sessionId)`.\n */\n get(sessionId: string): InputAccessor<I, Idle>;\n /**\n * Server-advertised fixed step rate in **Hz** \u2014 from `defineInput`'s\n * `tickRate`/`stepMs`/`stepSeconds`, or derived from `setTimestep` \u2014\n * the same value cascaded to predicting clients. `undefined` when no fixed\n * step is advertised.\n */\n readonly tickRate?: number;\n /**\n * The fixed step as **seconds** (`1/tickRate`): the dt to integrate one input\n * with, bit-identical to the client's prediction dt. Pass it to your physics\n * step (`applyInput(p, cmd, level, room.inputs.stepSeconds)`) so server and\n * client share one timestep instead of each keeping a constant that can drift.\n * `undefined` when no rate is advertised.\n */\n readonly stepSeconds?: number;\n /** The fixed step as **milliseconds** (`1000/tickRate`). `undefined` when no rate. */\n readonly stepMs?: number;\n /** Physics sub-steps per input tick (\u2265 1; `1` unless declared via\n * `setFixedTimestep(..., { subSteps })` / `defineInput`). */\n readonly subSteps: number;\n /** The physics sub-step as **seconds** (`stepSeconds / subSteps`) \u2014 the engine\n * dt when sub-stepping; equals {@link stepSeconds} when `subSteps` is 1.\n * `undefined` when no rate is advertised. */\n readonly subStepSeconds?: number;\n /** The physics sub-step as **milliseconds** (`stepMs / subSteps`). `undefined`\n * when no rate is advertised. */\n readonly subStepMs?: number;\n};\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
@@ -183,7 +183,7 @@ export interface NormalizedInputOptions {
183
183
  */
184
184
  seqField?: string;
185
185
  /**
186
- * > 0 enables per-client buffering of cloned snapshots — required for
186
+ * > 0 enables per-client buffering of input snapshots — required for
187
187
  * `room.inputs.get(sessionId).drain() / .peek() / .at()` to return populated
188
188
  * data. Oldest drops on overflow. Set to `0` to disable (`.latest` still
189
189
  * works).
@@ -215,6 +215,11 @@ export interface NormalizedInputOptions {
215
215
  * `latest` / the buffer / the idle ctx.
216
216
  */
217
217
  sanitize?: (instance: any) => void;
218
+ /**
219
+ * COMPILED copy of a decoded frame for the buffer (see `compileSnapshot`).
220
+ * Reads and writes like the schema, without its change tracking.
221
+ */
222
+ snapshot: (instance: any) => any;
218
223
  }
219
224
  /**
220
225
  * Per-client input accessor returned by `room.inputs.get(sessionId)`. Combines the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/core",
3
- "version": "0.18.24",
3
+ "version": "0.18.25",
4
4
  "description": "Multiplayer Framework for Node.js.",
5
5
  "type": "module",
6
6
  "input": "./src/index.ts",
package/src/index.ts CHANGED
@@ -20,7 +20,7 @@ export {
20
20
  // Core classes
21
21
  export { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, applySimulatedLatency, parseLatencyEnv, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';
22
22
  export { Room, RoomInternalState, validate, type RoomOptions, type DefineInputOptions, type SimulationCallback, type FixedTimestepCallback, type StepContext, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';
23
- export { InputBufferImpl, compileSanitizer } from './input/InputBuffer.ts';
23
+ export { InputBufferImpl, compileSanitizer, compileSnapshot } from './input/InputBuffer.ts';
24
24
  export { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf } from './input/types.ts';
25
25
  export { Rewind, RewindView, type RewindOptions, type RewindMode } from './Rewind.ts';
26
26
  export {
@@ -1,3 +1,5 @@
1
+ import { $values } from '@colyseus/schema';
2
+
1
3
  import type { ClientPrivate } from '../Transport.ts';
2
4
  import { $METADATA } from '../utils/Utils.ts';
3
5
 
@@ -43,7 +45,9 @@ export function compileSanitizer<I>(spec: SanitizeInput<I>): (input: I) => void
43
45
  // never assigned; the range floor turned "no input" into "held stick").
44
46
  if (v === undefined) { continue; }
45
47
  // NaN-safe: NaN fails both comparisons → clamp floor.
46
- inst[names[i]] = v >= mins[i] ? (v <= maxs[i] ? v : maxs[i]) : mins[i];
48
+ const clamped = v >= mins[i] ? (v <= maxs[i] ? v : maxs[i]) : mins[i];
49
+ // Honest frames clamp nothing: a keyed write through the setter costs more than the walk.
50
+ if (clamped !== v) { inst[names[i]] = clamped; }
47
51
  }
48
52
  };
49
53
  }
@@ -63,6 +67,59 @@ function fieldNamesOf(ctor: new () => any): string[] {
63
67
  return names;
64
68
  }
65
69
 
70
+ /** One per input schema, so every room's frames share a hidden class. */
71
+ const _snapshots = new WeakMap<new () => any, (input: any) => any>();
72
+
73
+ /**
74
+ * @internal Compile how a decoded input is copied into the buffer. `clone()`
75
+ * builds a Schema and its ChangeTree per input, for a frame that's never
76
+ * encoded, so a schema of plain values gets a lighter frame: its own array of
77
+ * values behind the ctor's prototype. Methods, getters, `instanceof`, `clone()`
78
+ * and `toJSON()` all hold, and a field write lands on the copy, untracked. A
79
+ * nested schema or collection keeps `clone()`, whose copy is deep.
80
+ *
81
+ * Fields are read by name, as `clone()` reads them, never by copying `$values`:
82
+ * native class fields (`useDefineForClassFields`) shadow the schema's accessors,
83
+ * so the decoder's values sit on the instance and `$values` stays empty.
84
+ */
85
+ export function compileSnapshot<I>(ctor: new () => I): (input: I) => I {
86
+ let snapshot = _snapshots.get(ctor);
87
+ if (snapshot !== undefined) { return snapshot; }
88
+
89
+ const md = (ctor as any)[$METADATA] as Record<number, { type?: unknown }> | undefined;
90
+ const names = fieldNamesOf(ctor);
91
+ const plain = md !== undefined && names.every((_, i) => {
92
+ const type = md[i].type;
93
+ return typeof type === "string" || (type !== null && typeof type === "object" && "quantized" in type);
94
+ });
95
+
96
+ if (plain) {
97
+ const fields: PropertyDescriptorMap = {};
98
+ names.forEach((name, i) => {
99
+ fields[name] = {
100
+ get(this: any) { return this[$values][i]; },
101
+ set(this: any, value: unknown) { this[$values][i] = value; },
102
+ enumerable: true,
103
+ configurable: true,
104
+ };
105
+ });
106
+ // A function, not a class: `constructor` must stay the schema's (clone/toJSON read its metadata).
107
+ function Frame(this: any, values: unknown[]) { this[$values] = values; }
108
+ Frame.prototype = Object.create(ctor.prototype, fields);
109
+ const count = names.length;
110
+ snapshot = (input) => {
111
+ const values = new Array(count);
112
+ for (let i = 0; i < count; i++) { values[i] = input[names[i]]; }
113
+ return new (Frame as any)(values);
114
+ };
115
+ } else {
116
+ snapshot = (input) => input.clone();
117
+ }
118
+
119
+ _snapshots.set(ctor, snapshot!);
120
+ return snapshot!;
121
+ }
122
+
66
123
  /**
67
124
  * @internal Seed every declared field that has no construction default with
68
125
  * its wire-neutral zero value (0 / false / ""; quantized: 0 clamped into the
@@ -4,7 +4,7 @@ import { HandshakeSection, InputFlags, ProtocolModifier } from '@colyseus/shared
4
4
 
5
5
  import {
6
6
  InputAccessorImpl, InputBufferImpl, NO_OP_INPUT_ACCESSOR,
7
- compileSanitizer, seedInputZeroValues, validateSubSteps,
7
+ compileSanitizer, compileSnapshot, seedInputZeroValues, validateSubSteps,
8
8
  } from './InputBuffer.ts';
9
9
  import type {
10
10
  InputAccessor, InputAPI, NormalizedInputOptions,
@@ -148,6 +148,7 @@ export class RoomInput {
148
148
  idle: opts?.idle,
149
149
  // Compiled once (map → dense min/max walk); applied per decoded frame.
150
150
  sanitize: opts?.sanitize !== undefined ? compileSanitizer(opts.sanitize) : undefined,
151
+ snapshot: compileSnapshot(type),
151
152
  tickRate,
152
153
  subSteps: validateSubSteps(opts?.subSteps, 'defineInput'),
153
154
  };
@@ -220,15 +221,15 @@ export class RoomInput {
220
221
 
221
222
  /**
222
223
  * Sanitize the freshly-decoded `client._input`, then (when buffering is on)
223
- * push a clone into the per-client buffer. Honors the framework seq
224
+ * push a snapshot into the per-client buffer. Honors the framework seq
224
225
  * (unreliable) / a user `inputOptions.seqField` (reliable) to dedupe the
225
226
  * redundancy ring.
226
227
  */
227
228
  capture(client: ClientPrivate, renderTime: number = 0, reckonTime: number = 0, seq?: number): void {
228
- // Sanitize before anything reads it (latest, the clone below, the idle ctx).
229
+ // Sanitize before anything reads it (latest, the snapshot below, the idle ctx).
229
230
  this.options.sanitize?.(client._input);
230
231
  const buf = client._inputBuffer;
231
- if (!buf) { return; } // no consumer registered — skip the clone allocation
232
+ if (!buf) { return; } // no consumer registered — skip the snapshot allocation
232
233
  const inst = client._input!;
233
234
  if (seq !== undefined) {
234
235
  // Unreliable: dedup the ring by the framework wire seq (user seqField is for .at() only).
@@ -241,7 +242,7 @@ export class RoomInput {
241
242
  if (typeof value === 'number' && !buf.accept(value)) { return; }
242
243
  }
243
244
  }
244
- buf.push(inst.clone() as any, renderTime, reckonTime, seq);
245
+ buf.push(this.options.snapshot(inst), renderTime, reckonTime, seq);
245
246
  }
246
247
 
247
248
  /** Decode a `ROOM_INPUT_RELIABLE` frame: an optional TIMED stamp prefix (shape
@@ -193,7 +193,7 @@ export interface NormalizedInputOptions {
193
193
  seqField?: string;
194
194
 
195
195
  /**
196
- * > 0 enables per-client buffering of cloned snapshots — required for
196
+ * > 0 enables per-client buffering of input snapshots — required for
197
197
  * `room.inputs.get(sessionId).drain() / .peek() / .at()` to return populated
198
198
  * data. Oldest drops on overflow. Set to `0` to disable (`.latest` still
199
199
  * works).
@@ -229,6 +229,12 @@ export interface NormalizedInputOptions {
229
229
  * `latest` / the buffer / the idle ctx.
230
230
  */
231
231
  sanitize?: (instance: any) => void;
232
+
233
+ /**
234
+ * COMPILED copy of a decoded frame for the buffer (see `compileSnapshot`).
235
+ * Reads and writes like the schema, without its change tracking.
236
+ */
237
+ snapshot: (instance: any) => any;
232
238
  }
233
239
 
234
240
  /**