# Input: the action map, remapping, and where a player's keys are kept

Three documents meet here. The **action map** is the scene's, in `meta.input`, and belongs to the
author. The **overrides file** is the player's, and belongs to whoever is holding the keyboard. The
**API** is [`ctx.input`](API.md), which reads the first, writes the second, and never confuses them.

## The action map

Contexts → actions → the devices that drive them. A scene that declares no `meta.input` runs
`DEFAULT_INPUT_MAP` from `@argus/contract`, so a scene with no input block plays:

| Context | Actions |
|---|---|
| `gameplay` | `move` (axis2, WASD / left stick), `look` (axis2, mouse delta / right stick), `jump` (Space / south), `crouch` (LControl / east), `sprint` (LShift / left-thumb), `interact` (E / west) |
| `menu` | `cancel` (Escape / east), `confirm` (Return / south), `navigate` (axis2, arrows / dpad) |

One action names up to four sources — `keys`, `mouse`, `gamepad`, `touch` — and they are **additive, with
the strongest winning**: an action bound to both the keyboard and a stick reads whichever is further
from neutral this tick, so a player may swap devices mid-frame and the engine tracks no "active
device". Face buttons are named by *position*, not by letter, so one map fits an Xbox pad, a
DualSense and a Switch pad: `south` is A / Cross / B.

How many keys an action takes follows from its kind: `axis2` takes four, in WASD order
(+y, -x, -y, +x); `axis1` takes two, negative then positive (`["A", "D"]` reads -1 on A and +1 on
D); `button` takes any number and ORs them together. An axis also takes **alternate sets** back to
back, any of which drives it: `"keys": ["A", "D", "Left", "Right"]` steers on A/D and on the arrows,
and `["W", "A", "S", "D", "Up", "Left", "Down", "Right"]` moves on both. Two keys for the same
direction held together are one full push, not two. A list that is not a whole number of sets binds
nothing (reported, not refused: the critic's `input-unbound`).

**A mouse is an angle, a stick is a rate.** A stick held at full push keeps turning the view, so a
camera turns by it times a speed and `dt` (the rigs' `yawSpeedDeg`/`pitchSpeedDeg`, 180°/s and 120°/s
at full push). The mouse's `delta` is how far the hand moved this tick, so a camera turns by exactly
that angle, once: pixels × degrees per pixel, with no `dt` and no top speed. An action's
`sensitivity` is the stick's multiplier (default 1) and the mouse's **degrees per pixel** (default
0.1°/px when the action gives none; the built-in map's `look` sets 0.1). `ctx.input.axisParts(name)`
reads the two apart — `{ rate, delta }`, `delta` in degrees — and the built-in camera rigs and the
character controller turn by `rate × speed × dt + delta`. `ctx.input.axis(name)` still folds the
mouse in as a rate (pixels ÷ 8 × sensitivity), as it always has, so a script that turns a view from
`axis("look")` caps a flick at its own top speed; turn from `axisParts` instead. The critic's
`feel/look-capped` probe sweeps a synthetic mouse (`POST /input` `mouse`) and fails a view that has
a top speed.

**Key names** are the engine's (`W`, `5`, `Space`, `Return`, `LShift`, `LControl`, `Up`, `F1`), in any
case, and every key also answers to its DOM `KeyboardEvent.code` (`KeyW`, `Digit5`, `Enter`,
`ShiftLeft`, `ArrowUp`) and to the key names other engines use (`Alpha5`, `LeftShift`, `LeftControl`, `UpArrow`).
They are the same key, so a map written from either habit binds what it says; docs and recipes use
the engine's spelling.

**In the editor**, Window ▸ Input edits this map as a table (contexts, actions, keys by pressing
them, gamepad from a list, shared keys warned about, unknown keys refused with
`input-key-unknown`'s sentence), each change one undo step: [`docs/EDITOR.md`](EDITOR.md), "The
Input table". An agent edits the same map with `scene.patch` on `/meta/input`.

**A behaviour reads actions, never keys.** That is what makes remapping a data change rather than a
code change, and it is why hot reload can rebind a running scene.

## Remapping

```ts
const input = ctx.input
if (!input) return
input.rebind("jump", "K")
```

`rebind(action, binding)` changes one action of the **live context** — the one the player is looking
at when they press the key. The binding is written in whichever of three ways fits:

| Written as | Means |
|---|---|
| `"K"` | one token. Resolved as a key name first, then a gamepad source, then a mouse one. |
| `"key:Left"`, `"pad:south"`, `"mouse:left"` | the same, with the device said outright. |
| `["W", "A", "S", "D"]` | the whole key list. An `axis2` takes four of them, an `axis1` two, or alternate sets of them back to back. |
| `{ keys, mouse, gamepad }` | the halves by name. A half not mentioned is left alone; `null` clears one. |

The prefixes exist for the three names two devices share: `left`, `right` and `middle` are arrow
keys **and** mouse buttons, and a bare token resolves to the key. Everything else is unambiguous —
no key is called `south` and no pad source is called `Space`.

`bindings(action)` reads the map back as `{ kind, keys, mouse, gamepad }`, or `null` when the live
context binds no such action. It reports the map **as played**, overrides included, so a rebinding
screen and the game can never disagree about which key jump is on. `actions()` lists what to show.

`resetBindings()` drops every override and empties the file: a "restore defaults" button.

### Refusals

A rebind that bound nothing would leave the player with a dead action and no way to find out why, so
an unresolvable name throws instead. The `Error` has `name === "InputBindingError"` and a message
that opens with a code:

| Code | When |
|---|---|
| `input-unknown-action` | the live context binds no action by that name |
| `input-unknown-key` | a `keys` entry, or a `key:`-prefixed token, is not a key name |
| `input-unknown-binding` | a bare token matched no device, or a `pad:`/`mouse:` name is not one |
| `input-binding-arity` | a number of keys that is not a whole number of the kind's sets (four for `axis2`, two for `axis1`); an empty list is always allowed: unbinding the keyboard half is a thing a player does |

**Nothing changes when a rebind throws** — not the map, not the file. A behaviour may catch it and
show a prompt, which is what a remapping screen does with a key it cannot accept.

### Conflicts

A key may drive two actions. `rebind("jump", "W")` in a scene whose `move` is still `W A S D`
leaves W doing both, and the engine **allows it and says so**, naming both actions and the shared
key on stderr:

```
argus: warning "jump" and "move" now share ["W"] in the "gameplay" context; one key drives both
```

Reported rather than refused, because two actions on one key is a legitimate thing to author — a
confirm that is also a jump — and nothing in the engine knows which of the two the player meant to
give up. A remapping screen that offers "press a key" is the usual way to produce one by accident,
and it is the screen, not the engine, that is in a position to ask. Mouse and gamepad sources are
checked the same way.

Note the deliberate asymmetry with the rest of `ctx.input`: *reading* an action the map does not
bind is neutral rather than an error, because a behaviour must not need a guard around
`pressed("sprint")` for a scene that has no sprint. *Writing* one is a mistake, and mistakes throw.

## Where the overrides live

A rebind is written to a JSON file the runtime is given at launch. It is small, readable, and holds
only what was actually rebound:

```json
{
  "version": 1,
  "contexts": {
    "gameplay": {
      "jump": { "keys": ["K"] }
    }
  }
}
```

Each entry names an action the scene already has and overrides the halves it mentions, in exactly
the spelling `meta.input` uses for the same thing. An override cannot invent an action, change a
`kind`, or add a context: a player remaps controls, they do not author them. An entry naming
something the scene has since dropped is ignored rather than refused — a scene may legitimately lose
an action between two runs, and losing the whole file over it would throw away the rest of the
player's bindings.

The file is read once at load, after the scene's own map is compiled and before the first tick, so
the very first frame of a run is already remapped. It is rewritten immediately on every `rebind`, so
a player who alt-F4s straight after remapping keeps the remap.

### A file the engine cannot use is never a launch it cannot make

A truncated, hand-mangled or future-versioned overrides file **warns and is ignored**. The scene's
own bindings stand, the warning names the file, and the launch goes ahead:

```
argus: warning ignoring input overrides at /…/.argus/input.json and playing on the scene's own
bindings: … EOF while parsing a string at line 4 column 10; delete the file, or rebind once to
replace it
```

The path is still adopted, so the next `rebind` writes a clean file over the broken one and the
player is not stuck with it. This used to propagate out of `World::load_file` instead: in the editor
that was a Play button that did nothing and no interface that could clear it, and in an exported
build it was a game that would not start. A player's own bindings are the least important thing in a
launch, so they are what gives way.

### Two sessions on one project

The write is **atomic**: a temporary file in the same directory, renamed over the target. A reader
never sees half a document, and a process killed mid-rebind leaves either the old file or the new
one — never the truncated JSON of the paragraph above.

It does **not** merge, and the rule is **last writer wins over the whole file**. Editor Play and an
`argus run` on the same project are the ordinary way to have two: the second session to save carries
the bindings it loaded plus its own, and the first session's later rebinds are gone. Merging per
save was the alternative and is worse — it would make "I unbound that" unfixable for as long as
another session still held the binding, because the other session's copy would keep putting it
back.

### Who passes the path

| Launch | File |
|---|---|
| `argus run <scene> --input-overrides <file>` | whatever the flag names; the folder is created on the first rebind |
| editor Play | `<project>/.argus/input.json` — beside the project, not under `.argus/editor`, because the bindings belong to whoever is playing |
| `argus run <scene>` with no flag | none. The scene plays on its own map and a rebind lasts until the process exits, with one line on stderr saying so |
| the exported launcher | `<unpack dir>/input.json` |

The editor's path and `argus run --input-overrides <project>/.argus/input.json` are the same file on
purpose: a remap made in Play is the remap that a launch of the same project starts with.

**The exported launcher passes one.** `packages/export/launcher` spawns
`argus run <target> --serve … --input-overrides <unpack dir>/input.json`, so a rebind in an exported
build is there the next time the player starts it — which is what acceptance bullet 1's "remap an
action, restart, verify the mapping persists" asks for on the one path an end user takes. The unpack
directory is per-user (`XDG_CACHE_HOME`/`LOCALAPPDATA`/`~/.cache`, under `argus-export/<payload
id>`) and already writable; the file is created by the first rebind. An engine build that predates
the flag ignores it rather than failing.

The payload id changes when the build does, so a new version of a game starts a new overrides file.
That is a deliberate limit of the simplest thing that works, not an oversight: carrying bindings
across versions needs a per-title identity the payload does not yet have.

## Touch

A phone, a tablet and a touch laptop have no keyboard to bind, so `touch` is the fourth source an
action can name: a **region of the screen** and what a finger in it does. It is data like the other
three, and a script still reads `move` and `jump`, never a finger.

```json
"move":  { "kind": "axis2", "keys": ["W", "A", "S", "D"],
           "touch": { "mode": "stick",  "region": { "x": 0,   "y": 0.4, "w": 0.4, "h": 0.6 } } },
"look":  { "kind": "axis2", "mouse": "delta",
           "touch": { "mode": "drag",   "region": { "x": 0.4, "y": 0,   "w": 0.6, "h": 1 } } },
"jump":  { "kind": "button", "keys": ["Space"],
           "touch": { "mode": "button", "region": { "x": 0.8, "y": 0.7, "w": 0.2, "h": 0.3 } } },
"interact": { "kind": "button",
           "touch": { "mode": "tap",    "region": { "x": 0.4, "y": 0,   "w": 0.6, "h": 0.5 } } }
```

**Regions are normalised**: `x`, `y`, `w`, `h` are fractions of the screen, origin at the top left, so
one region fits every resolution, density and orientation. A region is live while its context is, so
a menu context with no touch regions gives the screen back to the UI. `safeArea` (default `true`)
says the region is relative to the platform's safe area, clear of a notch or a home indicator; no
platform reports insets yet, so today the safe area is the whole screen and the field is carried,
not read.

| `mode` | kind | reads |
|---|---|---|
| `stick` | `axis2` (`axis1` reads x) | the first finger that landed in the region. Deflection is its offset from where it landed, or from the region's centre with `"fixed": true`, full at half the region's size, +y up, clamped to the unit circle, through the action's `deadzone` |
| `drag` | `axis2` (`axis1` reads x) | the finger's travel, **as an angle, like the mouse's `delta`**: the screen counts as 1800 reference pixels across, so a full-screen swipe is 180° at the default 0.1°/px, and `sensitivity` is degrees per reference pixel exactly as for the mouse. Read it from `axisParts(name).delta` |
| `button` | `button` | held while a finger that landed in the region is down, even if it slides out; a press and lift between two ticks is still one tick held. `toggle` works |
| `tap` | `button` | held for one tick when a finger that landed in the region is lifted within 20 ticks (a third of a second) having moved less than 0.03 of the screen; a long press, a swipe or a system-cancelled touch is not a tap |

Ownership is by where a finger **landed**, not where it is: a finger that lands on the stick and
wanders across the jump button is still the stick, and one that lands elsewhere and slides onto a
button does not press it. Ten fingers are followed at once. Sources stay additive, strongest wins,
so an action with `keys` and `touch` plays on either. A source that cannot work (a `stick` on a
`button` action, a region with no size or entirely off the screen) binds nothing and is reported in
`ActionMap::problems`, like a misspelt key. Regions should not overlap; two that do both take a
finger that lands in the overlap.

**The UI treats touch as a pointer.** Every finger reaches the action map, but a finger that lands
in no live region of the live context is the UI's pointer for as long as it is down: it presses,
drags and releases the primary button of the same pointer the mouse drives, so buttons, sliders
and scroll lists work with no touch code. Only the first such finger points; a second one outside
every region does nothing, which is what the extra fingers are for on a screen with virtual
controls. A finger that lands on a control is the control's, and a region wins over a UI element
under it.

**On desktop** a touch screen (real on Windows) sends winit's `WindowEvent::Touch`, which the window
normalises by its size into `DeviceEvent::Touch { id, phase, pos }`, the same event the mapper takes
from a test or a hold. Focus loss lifts every finger, as it releases keys.

**Headless and in a self-test**, `Holds::place_touch` puts a synthetic finger down at a position,
walks it in a line to another over N ticks and lifts it, through the same `Holds` that `POST /input`
and `--self-test` place holds with:

```json
{ "touch": [0.2, 0.8], "to": [0.2, 0.5], "frames": 60 }
```

a self-test step ([`docs/SELF-TEST.md`](SELF-TEST.md)) with positions normalised to the screen,
`to` defaulting to where it landed and `id` (default 0) naming the finger. A scenario's steps run
one after another, so two fingers at once are not expressible there; `place_touch` places as many as
it is given. The step fails, as an action does, when nothing reads touch within `bindTimeout`.
`fixtures/selftest/touch-stick-and-button.json` walks a stick and presses a button.

## Devices

**Gamepads** are gilrs behind the `gamepad` feature, on by default, in editor Play and in the
export. Note that `argus: gamepads open` means the backend opened, not that a pad is present.

**The mouse** drives `look` through `DeviceEvent::MouseMotion`: the window loop accumulates winit's
motion, `ActionMapper::tick` folds it into the `look` action once a tick as an angle
(`AxisParts::delta`, degrees), and the character controller and the rigs turn by that angle. A
mouse that stops moving stops turning the character in the same tick. `POST /input`'s `mouse` event
feeds the same path headlessly. `crates/app/tests/d4_review_probes.rs` drives both axes and a mouse
button through that path with no feature flag, so the suite as CI runs it has mouse evidence.

**Focus loss releases everything held** in both the editor and the standalone window: a synthetic up
for every held key and mouse button, and every stick and pad button released, so a player who
alt-tabs mid-sprint does not come back still sprinting. Covered by `crates/editor/tests/focus.rs`
and `crates/app/tests/window.rs` — and, because that second file is behind `--features window`,
which the ordinary suite does not set, by
`d4_review_probes.rs::losing_focus_releases_held_keys_and_mouse_travel` as well.

**A restart keeps what is held.** It is not a key-up; see [`docs/API.md`](API.md).

**Synthetic input** (`POST /input`) and device input **merge** rather than overwrite, keeping
whichever is further from neutral, so a human at the window and an agent posting holds can both be
driving and neither cancels the other. Input has to be re-posted every tick to stay held, exactly as
a device poll behaves — which is why a test that wants to read `ctx.input` after driving a key calls
`Sim::input_tick` rather than `World::step`, whose last act is to roll the tick's input over.

