Skip to content

Controls: one input source from several devices; checked input capture - #60

Merged
SandroMaglione merged 1 commit into
mainfrom
feat/controls-merge
Oct 22, 2026
Merged

SandroMaglione merged 1 commit into
mainfrom
feat/controls-merge

Conversation

@SandroMaglione

Copy link
Copy Markdown
Member

What

Controls: merge devices into one input source

export const Actions = Controls.actions(["up", "down", "left", "right", "interact", "pause"])

export const makeControls = (devices: { readonly keyboard: KeyboardInput; readonly gamepad: GamepadInput; readonly touch: TouchInput }) =>
  Actions.merge({ keyboard: devices.keyboard, gamepad: devices.gamepad, touch: Actions.lacking(devices.touch, ["pause"]) })

const input = resources.input.get()
input.actions.interact.pressed     // merged: held = any holds, pressed = any pressed, released = released and none holds
input.devices.gamepad.left         // each device's own snapshot, keyed by the merge names
input.lastUsed                     // "keyboard" | "gamepad" | "touch" | null
input.available.pause              // false while the last used device lacks it
services.controls.rebind.gamepad({ ... })
  • Compile-time checks: each of these fails to compile:
    • a device that binds an undeclared action;
    • a device that misses an action without listing it in lacking;
    • a lacking list that names too much or too little (autocomplete offers exactly the missing actions);
    • an action that no device binds;
    • reading a lacking action on that device (input.devices.touch.actions.pause).
  • Lacking actions in describe(): each one is reported as a service-lint.

One snapshot shape (breaking)

  • Keyboard: the snapshot is now { actions, used }.
  • Touch: buttons is renamed to actions and touched to used.
  • Gamepad: gains used.
  • What counts as used: a press or a hold, or a stick or swipe in use. Releases don't count, so a blur or a disconnect doesn't change lastUsed.

Checked capture

  • New access kind: Game.System.consumeService, plus a service-consumed-twice lint that fires when one input is captured in two schedules and each capture sees only part of the presses.
  • InputCapture.system: keeps its real spec. A thisTick reader placed before it is now a compile error that names both systems.
  • Debug.serviceLints: a hook through which service values report their own lints.

Top-down now reads one merged resource instead of two.

Verification

  • pnpm run check passes.
  • pnpm pack:check passes.
  • pnpm bench:check flags query/iterate-write-10k at +26.5% against the committed baseline. Compared with origin/main on the same machine (--compare), there is no regression, and this PR touches no query code. Type-checker metrics are +2.6% against the baseline; main is already at +2.5%.

🤖 Generated with Claude Code

…k input capture

- Controls.actions([...]) declares the game's actions once; its merge(...)
  builds one source from several devices (held/pressed/released as one
  device with every binding), with lacking(device, [...]) naming exactly the
  actions a device cannot report, available/devices/lastUsed in the
  snapshot, rebind per device, idle(...) for the resource, and describe()
  findings for every gap.
- Keyboard snapshots are { actions, used }; Touch renames buttons to actions
  and touched to used; Gamepad adds used. Releases never count as use.
- Game.System.consumeService and the service-consumed-twice lint;
  Debug.serviceLints lets a service report its own findings.
- InputCapture keeps its declared access: thisTick readers placed before it
  no longer compile, and capturing one source twice is linted.
- Top-down reads one merged input resource.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SandroMaglione
SandroMaglione merged commit 3f1efec into main Oct 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant