feat(input): #83 Input Manager + auto-commit the device layer in the frame loop
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m18s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 28s

Fixes the papercut: the generated frame loop called rt_poll() (feeding only
Input.key) but never input_poll(), so Input.active/key_down/mouse/pad read
empty unless the game called Input.poll() by hand. The loop now calls
input_poll() when the game uses any Input runtime method — committing the
held-key/mouse/gamepad state, and record/replay — and stores its return as the
frame key so Input.key still works. A game using no Input runtime keeps the
plain rt_poll path, byte-identical.

Adds the Input-Manager API: Input.action(name,key) ships a default binding
(kept if already bound, so a rebind/loaded map isn't clobbered),
Input.bind_pad(name,button) makes an action device-agnostic (keyboard OR pad),
and Input.active/just_pressed/just_released read the multi-key device layer
with clean on-press/on-release edges (deterministic, dispatch-free — a handler
polls the edge; a replay fires identically).

Examples input_manager + input_auto, 5 docs pages. Full suite 107/0, goldens
byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-02 07:04:10 +03:00
parent 6a83c28e05
commit bccd26fb29
14 changed files with 31773 additions and 31127 deletions

View file

@ -209,6 +209,13 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if (meth == "pressed") { bare = "input_pressed" }
if (meth == "record") { bare = "input_record" }
if (meth == "replay") { bare = "input_replay" }
# Input Manager (#83): default bindings + device-agnostic actions that read the
# multi-key device layer (the frame loop now commits it automatically).
if (meth == "action") { bare = "input_default"; push(labels, "action"); push(labels, "key") }
if (meth == "bind_pad") { bare = "input_bind_pad"; push(labels, "action"); push(labels, "button") }
if (meth == "active") { bare = "input_active"; push(labels, "action") }
if (meth == "just_pressed") { bare = "input_just_pressed"; push(labels, "action") }
if (meth == "just_released") { bare = "input_just_released"; push(labels, "action") }
# device layer (#50): multi-key held state, analog axes/vectors, mouse,
# gamepads, touch — reached as ordinary @fn_input_* calls into input.ludic.
if (meth == "key_down") { bare = "input_key_down"; push(labels, "key") }

View file

@ -467,9 +467,20 @@ function emit_game_main() -> void {
emit(" br i1 "); emit(rc); emit(", label %body, label %done\n")
}
emit("body:\n")
if (find_fn("rt_poll") != null) {
let k = emit_bind("call i32 @fn_rt_poll()")
# #83: when the game uses the input action-map / device layer, the frame loop
# commits the device layer automatically each frame by calling input_poll (which
# reads the live key via rt_poll, records/replays, and rebuilds the held-key set,
# mouse and gamepad state). Its return is the frame key, so Input.key still works
# and the game no longer has to call Input.poll by hand. A game that uses no Input
# runtime keeps the plain rt_poll path, byte-identical.
if g_uses_input and (find_fn("input_poll") != null) {
let k = emit_bind("call i32 @fn_input_poll()")
emit(" store i32 "); emit(k); emit(", ptr @L_key\n")
} else {
if (find_fn("rt_poll") != null) {
let k = emit_bind("call i32 @fn_rt_poll()")
emit(" store i32 "); emit(k); emit(", ptr @L_key\n")
}
}
emit_calls_for_phase("Input")
emit_calls_for_phase("FixedUpdate")

View file

@ -189,7 +189,7 @@ function p_postfix() -> Node {
if e.a.kind == E_ID and e.a.s == "Reflect" and (e.s == "serialize" or e.s == "apply") { g_uses_value = true; g_uses_reflect_io = true } # Reflect.serialize/apply -> value tree + world table
# Input.* action-map / record-replay methods (#7) -> splice input.ludic.
# Input.key stays bare (no runtime), so gate on the new methods only.
if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "bind" or e.s == "rebind" or e.s == "poll" or e.s == "down" or e.s == "pressed" or e.s == "record" or e.s == "replay") { g_uses_input = true }
if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "bind" or e.s == "rebind" or e.s == "poll" or e.s == "down" or e.s == "pressed" or e.s == "record" or e.s == "replay" or e.s == "action" or e.s == "bind_pad" or e.s == "active" or e.s == "just_pressed" or e.s == "just_released") { g_uses_input = true }
# #50 device layer — any of the multi-key / analog / mouse / gamepad / touch
# methods also splices input.ludic (Input.key stays bare, no runtime).
if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "key_down" or e.s == "key_pressed" or e.s == "key_released" or e.s == "press" or e.s == "release" or e.s == "axis" or e.s == "vector" or e.s == "strength" or e.s == "mouse_x" or e.s == "mouse_y" or e.s == "mouse_dx" or e.s == "mouse_dy" or e.s == "mouse_down" or e.s == "wheel" or e.s == "set_mouse" or e.s == "pad_connected" or e.s == "pad_button" or e.s == "pad_axis" or e.s == "set_pad" or e.s == "touch_count" or e.s == "touch_x" or e.s == "touch_y" or e.s == "set_touch") { g_uses_input = true }

File diff suppressed because it is too large Load diff