# Client prediction

> Make movement feel instant with a stepper shared by the room and the client.

The room is the authority, so a client that waits for the room to move
its player feels a round trip of lag. Prediction runs the room's own
simulation step on the client for the local player, sends the input it
used, and reconciles when the room's state arrives. Everything else on
screen is [interpolated](/docs/guides/state-sync#on-the-client).

## The stepper

A **stepper** is a simulation step the room and its clients share, in a
module both import:

```ts
function defineStepper<I>(stepper: {
  step(world: World, inputs: ReadonlyMap<string, I>, dt: number, tick: number): void;
  parseInput?(raw: unknown): I;
  save?(world: World): unknown;
  load?(saved: unknown, world: World): void;
  release?(world: World): void;
}): Stepper<I>;

interface World {
  all<K extends Kind>(kind: K): Iterable<Entity<K>>;
  get(id: number): Entity | undefined;
  readonly predicted?: Entity;   // on a client's prediction world only
  readonly stateTick?: number;   // on a client's prediction world only
}
```

- `world` is the room's entities, read and written in place; on the
  client it's the client's view. `world.get(id)` is a map lookup;
  `world.all(kind)` visits every entity of the kind on each call, so
  reach static data, such as voxel chunks, by computed IDs; see
  [Entity IDs and lookups](/docs/guides/state-sync#entity-ids-and-lookups).
  The shared state is in it on both sides, as `world.get(SHARED_ID)`
  (0) and in `world.all(schema.state)`, so a stepper can read a race's
  start tick from it. Private state isn't: the room has one per player.
- `inputs` maps player IDs to this tick's input. On the room it holds
  every player that sent one for this tick; on the client only the local
  player's.
- `dt` is the fixed timestep in seconds, `1 / tickRate`.
- `tick` is the room tick being stepped: `room.tick` on the room, and
  on the client the tick of the input being stepped, which is the tick
  the room runs that input at. A rule that depends on time, such as
  holding cars on the grid until `tick` reaches the start, predicts
  exactly.
- `world.predicted` is the predicted entity on a client and undefined on
  the room, for a stepper that simulates only that entity on the client.
- `world.stateTick` is the room tick of the state the client holds, and
  undefined on the room.
- `parseInput` runs on the room only. Inputs arrive as JSON from the
  client, or as the schema's input kind's fields (see
  [Compact inputs](#compact-inputs)); without `parseInput` the room
  passes the decoded input as is, so validate it here and throw to drop
  a bad one.
- `save` and `load` are for caches of the stepper's own, such as a
  physics engine's world. They run on the room only, on hibernate, at
  checkpoints and on wake, never per tick. One stepper serves every room
  in a room host, so they get the room's `world`, the object `step` gets;
  key caches by it. See [Steppers](/docs/room-state#steppers).
- `release` frees what a stepper keeps for a `world` that's gone: on the
  room when the room leaves its room host, and on the client when the
  prediction stops. Only caches the garbage collector can't free
  promptly need it, such as a physics engine's WebAssembly memory.
- For rigid bodies and characters, [`@irt/physics`](/docs/guides/physics)
  is a stepper built on Rapier.

```ts
// shared.ts
import { defineSchema, defineStepper, entity } from "@irt/room";

export const Runner = entity("runner", { owner: "string", x: "f32", y: "f32" });
export const schema = defineSchema({ entities: [Runner] });

export interface Input {
  dx: number;
  dy: number;
}

const SPEED = 6;

export const stepper = defineStepper<Input>({
  parseInput(raw) {
    const i = raw as Partial<Input>;
    if (typeof i.dx !== "number" || typeof i.dy !== "number" || Math.abs(i.dx) > 1 || Math.abs(i.dy) > 1) {
      throw new TypeError("input is { dx, dy } with each from -1 to 1");
    }
    return { dx: i.dx, dy: i.dy };
  },
  step(world, inputs, dt) {
    for (const r of world.all(Runner)) {
      const input = inputs.get(r.owner);
      if (!input) continue;
      r.x += input.dx * SPEED * dt;
      r.y += input.dy * SPEED * dt;
    }
  },
});
```

## The room

```ts
// room.ts
import { defineRoom } from "@irt/room";
import { Runner, schema, stepper } from "./shared.js";

export default defineRoom({
  schema,
  tickRate: 30,
  stepper,
  onJoin(room, conn) {
    // After a crash or a wake the player's runner is still here, and onJoin runs again.
    const existing = [...room.entities.all(Runner)].find((r) => r.owner === conn.player);
    conn.data.runner = existing ?? room.entities.create(Runner, { owner: conn.player });
  },
  onTick(room) {
    // Rules only the room knows; the client reconciles with them.
    for (const r of room.entities.all(Runner)) r.x = Math.min(r.x, 50);
  },
  onPlayerLeave(room, player) {
    for (const r of room.entities.all(Runner)) if (r.owner === player) room.entities.delete(r);
  },
});
```

`onJoin` reuses the player's runner rather than creating one each time:
after a room host crash it runs again with no `onLeave` or
`onPlayerLeave` first. The runner goes in `onPlayerLeave`, which runs
10 seconds after the player's last connection closed with no new one,
so a dropped network and a reconnect keep it, and before every
hibernation; see [State sync](/docs/guides/state-sync).

Each tick the room runs the stepper with each player's input for that
tick, then `onTick`, then sends the state update, then acknowledges the
inputs it applied.

## The client

```ts
// client.ts
import { connect } from "@irt/client";
import { type Input, Runner, schema, stepper } from "./shared.js";

const room = await connect({ router: "wss://eu.irt.io", project: "runners", room: "track-1", schema });

const keys = new Set<string>();
addEventListener("keydown", (e) => keys.add(e.key));
addEventListener("keyup", (e) => keys.delete(e.key));

const me = room.predict(stepper, {
  find: (r) => [...r.entities.all(Runner)].find((e) => e.owner === r.player),
  sample: (): Input => ({
    dx: (keys.has("ArrowRight") ? 1 : 0) - (keys.has("ArrowLeft") ? 1 : 0),
    dy: (keys.has("ArrowDown") ? 1 : 0) - (keys.has("ArrowUp") ? 1 : 0),
  }),
});
const others = room.interpolate(Runner, ["x", "y"]);

function frame() {
  const local = me.render(["x", "y"]);
  for (const r of room.entities.all(Runner)) {
    const at = r.owner === room.player ? local : others.get(r);
    console.log(r.owner, at.x, at.y);
  }
  requestAnimationFrame(frame);
}
frame();
```

`room.predict(stepper, { find, sample })` returns a `Prediction`, typed
by the kind `find` returns:

| Member | Meaning |
|---|---|
| `entity` | The predicted entity: the room's last state plus the inputs it hasn't acknowledged, or `undefined` until `find` finds it. Use it for game logic. |
| `render(fields, opts?)` | The predicted numeric fields plus a decaying share of recent mispredictions, as `Record<field, number>`. Before the first input it's the room's state of the entity, and `NaN` for each field while `find` finds nothing. Use it for drawing. With `{ snapDistance }`, a misprediction of these fields longer than that is dropped instead of smoothed; see [Respawns and teleports](#respawns-and-teleports). |
| `snap()` | Drops every misprediction `render` is smoothing, so it draws `entity` as it is |
| `pending` | Ticks stepped ahead of the room's state: inputs sent and not yet acknowledged, and [idle](#idle-inputs) ticks not sent. With `sample` this stays at about 2 plus half the round trip in ticks, never 0, because inputs are sent ahead of the room's clock |
| `settled` | `true` when the prediction agrees with the room: `entity` equals the room's last state, so the pending inputs move nothing. `render` may still be smoothing away an earlier misprediction |
| `mispredictions` | How many replays found the room's state different from the prediction in a field the stepper writes |
| `onMispredict(fn)` | Calls `fn({ tick, fields, delta })` after each such replay: the room tick it replayed from, the fields that differed, and for numeric ones the prediction's value less the replayed one, which `render` smooths away. Returns an unsubscribe |
| `push(input)` | Sends an input for the next tick and steps the prediction. Only without `sample`. |
| `stop()` | Stops sampling |

How it works:

1. Once per room tick, by the client's estimate of the room's clock, the
   prediction calls `sample()`, steps the predicted entity with the
   input and sends it tagged with the room tick it is for. `sample`
   receives the `Prediction` it belongs to, so it can steer by
   `entity`, which is set before each call whenever `find` finds the
   entity. It's undefined only while `find` finds nothing: before the
   entity enters the client's view, while the room has deleted it, and
   after a reconnect until the full state arrives, so read it as
   `p.entity?.x ?? 0`. The first call is at the next room tick after
   `predict` returns, never during it. After the
   page was paused it calls `sample` up to 4 times at once to catch up
   and skips the rest. It aims 2 ticks ahead of the room, plus half the
   round trip, so the input arrives in time. Each message repeats the
   last 4 inputs, so a lost one costs nothing; see
   [Compact inputs](#compact-inputs) for what that costs.
2. The room holds each input until the tick it names and applies it
   then. An input for a tick that already ran is dropped, and so is one
   more than 32 ticks ahead (`INPUT_WINDOW`). One input per player per
   tick: a later one for the same tick replaces the earlier.
3. After each tick's state update the room sends an ack with the highest
   input it applied or dropped. The client resets the predicted entity
   to the room's state and replays the inputs after the ack. A field
   that a step has written and that came out different is a
   misprediction, and the difference goes into what `render` smooths
   away, 20% per tick. A field no step writes, such as a lap counter
   only `onTick` sets, is the room's: its changes are taken as they
   come, never counted or smoothed.
4. On a reconnect or a new room generation, prediction state is dropped
   and starts again from the room's state.

### Compact inputs

Inputs are JSON unless the schema declares their fields. Each message
repeats the last 4 inputs, so JSON inputs can cost more upstream than
the state a client receives: a shooter's
`{"mx":1,"my":0,"aim":0.1234567,"fire":true,"rocket":false}` is 200
to 300 bytes a message, 6 to 9 KB/s at 30 ticks a second. Declare the input as
a kind, with `entity`, and pass it to `defineSchema` as `input`; inputs
then go as binary, each field encoded as in a state update, with the
repeats' ticks as differences:

```ts
// compact.ts
import { defineSchema, defineStepper, entity, type ValuesOf } from "@irt/room";

export const Ship = entity("ship", { owner: "string", x: "f32", aim: "f32" });
export const ShipInput = entity("input", { mx: "i8", aim: "f32", fire: "bool" });
export type ShipInput = ValuesOf<typeof ShipInput>;
export const compactSchema = defineSchema({ entities: [Ship], input: ShipInput });

export const shipStepper = defineStepper<ShipInput>({
  parseInput(raw) {
    const i = raw as ShipInput;
    if (Math.abs(i.mx) > 1) throw new RangeError("mx is -1, 0 or 1");
    return i;
  },
  step(world, inputs, dt) {
    for (const s of world.all(Ship)) {
      const input = inputs.get(s.owner);
      if (!input) continue;
      s.x += input.mx * 100 * dt;
      s.aim = input.aim;
    }
  },
});
```

That shooter's input is then 48 bytes a message, about 1.4 KB/s. `sample`
returns the same object as before. Fields it returns that the kind
doesn't declare are dropped, on the client as on the room. An input
that isn't an object, or with a field missing or of the wrong type,
throws a `TypeError` naming the field and stops the prediction, as
`stop()` does; call `predict` again once `sample` is fixed. `f32` fields are
rounded with `Math.fround` before the client steps with them, as the
room will see them, so the prediction stays exact. `parseInput` gets an
object of the kind's fields with their types, so check ranges and
leave types to the encoding. The input kind is part of the schema hash,
so a site built with a different input layout fails to connect with
4004, as for any other schema change. `room.stats.sent` shows what
inputs cost, under `typedInput` for these and `input` for JSON ones.

### Idle inputs

A predicting client sends an input message every tick, even standing
still. Pass `idle` to say which inputs mean the same as none:

```ts
const me = room.predict(stepper, { find, sample, idle: (i) => i.dx === 0 && i.dy === 0 });
```

Once the room has acknowledged every input that wasn't idle, an idle
input isn't sent, and both sides step that tick with no input: the room
because none arrived, the client because it skipped it. That holds only
if the stepper treats a missing input as the idle one, as the runner's
`if (!input) continue` does. While an earlier input awaits its ack,
idle inputs are still sent, so the repeats in each message carry the
inputs from just before the player stopped past a lost message. A
standing player then sends only pings.

### Respawns and teleports

A respawn or a teleport that room code does in `onTick` is a rule the
stepper can't predict, so it arrives as a misprediction, and `render`
would slide the player from the old spot to the new one over half a
second. Pass a `snapDistance` to `render`, longer than any misprediction
of ordinary play, and one longer than that is drawn at once:

```ts
const local = me.render(["x", "y"], { snapDistance: 64 });
```

Where the entity itself says it jumped, call `me.snap()` instead; it
drops whatever `render` is smoothing. The misprediction lands when the
room's ack arrives, after the tick's state update and any message the
room sent in that tick, so watch a field the room sets with the jump,
such as a `spawns` counter, and snap in the frame where `me.entity`
shows the new value:

```ts
if (me.entity && me.entity.spawns !== lastSpawns) {
  lastSpawns = me.entity.spawns;
  me.snap();
}
```

### The prediction world

On the client the stepper runs on a prediction world. The predicted
entity is writable. Every other entity is a copy whose writes last
until the prediction next replays from the room's state, at the next
ack, so a step's effect on another entity holds exactly as long as the
input that caused it is unacknowledged; once the room acks it, the
room's state has the effect, or doesn't if the room rejected it. Don't
mutate a `bytes` value in place in a stepper; on the client it's the
client's own copy of room state. Use `entity.patch(field, offset,
bytes)`, which works on both sides.

The prediction world is one object for the life of the `Prediction`,
so a cache a stepper keys by `world` lasts across its steps and replays.
A replay starts from the room's state, but such a cache doesn't rewind
with it, so keep only data derived from static entities in it, such as
a voxel chunk's collision shapes, never state a step advances.
`@irt/physics` keeps its Rapier world this way, and makes a body's
colliders again when its kind's
[`shapeKey`](/docs/guides/physics#shapes-that-change) changes.

### Predicting edits

An edit the player makes next to its own entity, such as digging the
block under its feet, mispredicts if it goes as a message: the room
applies it when it arrives, while the prediction has already stepped
past with the old blocks. Send it as fields of the input instead, and
apply it in the stepper:

```ts
const DigInput = entity("input", { dx: "i8", edit: "u8", ex: "i16", ey: "u8", ez: "i16", eb: "u8" });

step(world, inputs) {
  for (const p of world.all(Player)) {
    const i = inputs.get(p.owner);
    if (i?.edit && inReach(p, i) && p.editsLeft > 0) {
      chunkAt(world, i.ex, i.ez)?.patch("blocks", blockIndex(i.ex, i.ey, i.ez), new Uint8Array([i.eb]));
      p.editsLeft--;
    }
    // ...movement, which now sees the edited block.
  }
}
```

The room applies the edit at the input's tick and the client predicts
it at the same tick, so an edit the room accepts mispredicts nothing.
Checks the room makes, such as reach and a rate limit, go in the
stepper too, with the rate limit's state in an entity field, as
`editsLeft` is, so the client predicts it.

Inputs are best-effort, unlike messages. The room drops an input that
arrives after its tick has run or more than 32 ticks ahead, and
acknowledges it as handled; each message repeats only the last 4
inputs; and the client keeps at most 64 unacknowledged ones. Under lag
or loss an edit sent as an input can be lost without an error, and the
player sees the block come back when the prediction replays. A message
arrives in order or the connection drops. Use inputs for edits a player
makes while moving next to them, where a lost edit costs a retry, and
messages or calls for edits that must land: rare or costly ones, ones
not tied to the player's movement, and anything the game records, such
as a build placed from an inventory.

## Shooting and lag compensation

A player aims at remote players drawn by
[`interpolate`](/docs/guides/state-sync#on-the-client), which shows them
a little in the past, while the room resolves the shot at its own
current tick. Checking the shot against where targets are now misses
anything that moves. Rewind the targets to what the shooter saw, as
[Lag compensation](/docs/guides/lag-compensation) shows with
`defineHistory` and `room.rewind`. The ticks involved:

- **Values at tick T** are an entity's fields at the end of tick T,
  after the stepper and `onTick`: what the state update for tick T
  carries, and what `room.rewind(history, T)` returns.
- **`interpolate`** draws remote entities at render time
  `serverTick() - max(2 ticks, 100 ms)`, between the updates for the
  tick before that time and the tick after it.
- **`conn.viewTick`** is that render time, rounded down to a tick and
  never past the last update the client has, when the client sampled
  the input the room applies this tick. During the stepper and
  `onTick` it's the view tick of this tick's input, or of the last one
  applied, and at most `MAX_REWIND_MS` (250 ms) behind the room. The
  shooter saw targets between their values at `viewTick` and
  `viewTick + 1`, so allow a tick of their movement in the hit test.
- Don't rewind the shooter. Its input moved it in this tick's step, as
  its prediction did on its screen.
- On a link slower one way than the other, the client's estimate of
  the room's tick is off by half the difference, so `viewTick` is too.
  See [how accurate `serverTick()` is](/docs/reference/client-sdk#roomclient).

## Testing it

`TestClient.predict` samples once per room tick as the test moves the
clock. Since `pending` never reaches 0 while sampling, wait for `settled`
to know the prediction has caught up with the room:

```ts
// prediction.test.ts
import { testRoom } from "@irt/testing";
import { expect, test } from "vitest";
import room from "./room.js";
import { Runner, stepper } from "./shared.js";

test("the client's prediction converges on the room's wall", async () => {
  const t = testRoom(room);
  const alice = await t.connect({ player: "alice" });
  let dx = 1;
  const me = alice.predict(stepper, {
    find: (r) => [...r.entities.all(Runner)].find((e) => e.owner === r.player),
    sample: () => ({ dx, dy: 0 }),
  });
  await t.host.tick(300);
  expect(me.settled).toBe(false);
  dx = 0;
  await t.host.tick(30);
  expect(me.mispredictions).toBeGreaterThan(0);
  expect(me.settled).toBe(true);
  expect(me.entity!.x).toBe(50);
  // render is still smoothing the last misprediction away, by 20% a tick.
  expect(me.render(["x"]).x).toBeCloseTo(50, 1);
});

test("sample can steer by the prediction it belongs to", async () => {
  const t = testRoom(room);
  const alice = await t.connect({ player: "alice" });
  const me = alice.predict(stepper, {
    find: (r) => [...r.entities.all(Runner)].find((e) => e.owner === r.player),
    sample: (p) => ({ dx: (p.entity?.x ?? 0) < 10 ? 1 : 0, dy: 0 }),
  });
  await t.host.tick(120);
  expect(me.settled).toBe(true);
  expect(me.entity!.x).toBeGreaterThanOrEqual(10);
});
```

## Common mistakes

- **Steppers that depend on anything but `world`, `inputs`, `dt` and
  `tick`.** `Math.random()`, `Date.now()` or module state make the
  client's replay differ from the room, and every tick becomes a
  misprediction.
- **Moving the predicted entity outside the stepper.** Room code in
  `onTick` that moves it produces a misprediction each tick; put movement in
  the stepper and keep `onTick` for rules the client can't predict.
- **Calling `push` and passing `sample`.** With `sample` the prediction
  pushes for you; pushing as well sends two inputs per tick.
- **Waiting for `pending` to reach 0 in a test.** With `sample` it
  doesn't; assert on `settled`.
- **Drawing `entity` instead of `render(...)`.** `entity` jumps on a
  misprediction; `render` smooths it.
- **Trusting inputs in the room.** Without `parseInput`, `step` receives
  whatever the client sent: any JSON, or with an input kind any value
  its field types hold.
- **Predicting other players.** Interpolate them; only the local player's
  entity is predicted. Others in the prediction world are where the
  client last saw them, or with [`@irt/physics`](/docs/guides/physics),
  dynamic bodies are extrapolated to the tick being predicted.
