feat(input): #83 Input Manager + auto-commit the device layer in the frame loop
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:
parent
6a83c28e05
commit
bccd26fb29
14 changed files with 31773 additions and 31127 deletions
3
changes/input-manager.md
Normal file
3
changes/input-manager.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
Input Manager + automatic device-layer drive (#83). The generated frame loop now commits the input device layer itself — when a game uses any Input action-map / device method it calls `input_poll` each frame (reading the live key, recording/replaying, and rebuilding the held-key/mouse/gamepad state), so `Input.active` / `Input.key_down` / the mouse and pads read live **without the game calling `Input.poll` by hand** (previously the loop fed only `Input.key`, and the device layer read empty unless the game polled at the top of its Input phase). A game that uses no Input runtime keeps the plain `rt_poll` path, byte-identical. Adds the Input-Manager API on top: `Input.action(name, key)` ships a **default** binding (kept if already bound, so a player's `Input.rebind` or a loaded key-map is not clobbered); `Input.bind_pad(name, button)` makes an action **device-agnostic** (fires from keyboard *or* gamepad); and `Input.active` / `Input.just_pressed` / `Input.just_released` read the whole multi-key device layer with clean on-press / on-release edges (the deterministic, dispatch-free equivalent of event handlers — a handler polls the edge and reacts, so a replay fires identically). Examples: `examples/library/input_manager.ludic`, `examples/library/input_auto.ludic`.
|
||||
23
docs/language/input/input-action.md
Normal file
23
docs/language/input/input-action.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: input-action
|
||||
name: Input.action
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.action
|
||||
sig: Input.action(action: str, key: int) -> void
|
||||
tip: Register a default key binding for an action (kept if already bound).
|
||||
order: 31
|
||||
ns: Input
|
||||
member: action
|
||||
---
|
||||
|
||||
Registers a <em>default</em> keyboard binding: it binds <code>key</code> to the action only if the action has no key bound yet. Ship your defaults with <code>Input.action</code> in a Boot handler; a player's later <a href="input-rebind.html"><code>Input.rebind</code></a> (or a loaded key-map) is not clobbered, and re-running the defaults is idempotent. Combine with <a href="input-bind_pad.html"><code>Input.bind_pad</code></a> for a device-agnostic action, and read it with <a href="input-active.html"><code>Input.active</code></a>.
|
||||
|
||||
```ludic
|
||||
program Defaults {
|
||||
entry {
|
||||
Input.action("jump", ' ')
|
||||
if Input.active("jump") { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/input/input-active.md
Normal file
23
docs/language/input/input-active.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: input-active
|
||||
name: Input.active
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.active
|
||||
sig: Input.active(action: str) -> bool
|
||||
tip: Is a named action active right now (any bound key held, or pad button down)?
|
||||
order: 33
|
||||
ns: Input
|
||||
member: active
|
||||
---
|
||||
|
||||
Returns whether a named action is active this frame, reading the whole <a href="input-key_down.html">device layer</a>: true if any of the action's bound keyboard keys is in the multi-key held-set <em>or</em> any bound gamepad button is down on pad 0. Unlike <a href="input-down.html"><code>Input.down</code></a> (which reads only the single per-frame key), this sees simultaneous holds (move <em>and</em> jump) and is device-agnostic. The frame loop commits the device layer automatically, so it reads live without a manual <a href="input-poll.html"><code>Input.poll</code></a>.
|
||||
|
||||
```ludic
|
||||
program Active {
|
||||
entry {
|
||||
Input.action("left", 'a')
|
||||
if Input.active("left") { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/input/input-bind_pad.md
Normal file
24
docs/language/input/input-bind_pad.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-bind_pad
|
||||
name: Input.bind_pad
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.bind_pad
|
||||
sig: Input.bind_pad(action: str, button: int) -> void
|
||||
tip: Also fire a named action from a gamepad button (device-agnostic).
|
||||
order: 32
|
||||
ns: Input
|
||||
member: bind_pad
|
||||
---
|
||||
|
||||
Maps a gamepad <code>button</code> (on pad 0) to a named action, so the action fires from the keyboard <em>or</em> the pad. The same action can carry both keyboard keys (<a href="input-bind.html"><code>Input.bind</code></a> / <a href="input-action.html"><code>Input.action</code></a>) and pad buttons; <a href="input-active.html"><code>Input.active</code></a> and its edges fire on either device — one action, any input.
|
||||
|
||||
```ludic
|
||||
program Pad {
|
||||
entry {
|
||||
Input.action("fire", 'j')
|
||||
Input.bind_pad("fire", 0)
|
||||
if Input.active("fire") { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/input/input-just_pressed.md
Normal file
23
docs/language/input/input-just_pressed.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: input-just_pressed
|
||||
name: Input.just_pressed
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.just_pressed
|
||||
sig: Input.just_pressed(action: str) -> bool
|
||||
tip: Did a named action go active this frame (the deterministic on-press)?
|
||||
order: 34
|
||||
ns: Input
|
||||
member: just_pressed
|
||||
---
|
||||
|
||||
Returns whether a named action went active this frame — active now, not active last frame. This is the deterministic on-press edge over the device layer (keyboard or pad), the event-style "just pressed" without a callback: a handler polls it each frame and reacts. Pair with <a href="input-just_released.html"><code>Input.just_released</code></a> for the release edge and <a href="input-active.html"><code>Input.active</code></a> for the held state.
|
||||
|
||||
```ludic
|
||||
program Press {
|
||||
entry {
|
||||
Input.action("jump", ' ')
|
||||
if Input.just_pressed("jump") { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/input/input-just_released.md
Normal file
23
docs/language/input/input-just_released.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: input-just_released
|
||||
name: Input.just_released
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.just_released
|
||||
sig: Input.just_released(action: str) -> bool
|
||||
tip: Did a named action go inactive this frame (the on-release edge)?
|
||||
order: 35
|
||||
ns: Input
|
||||
member: just_released
|
||||
---
|
||||
|
||||
Returns whether a named action went inactive this frame — not active now, active last frame. The on-release edge over the device layer (keyboard or pad), symmetric with <a href="input-just_pressed.html"><code>Input.just_pressed</code></a>. Use it for release-triggered actions (variable-height jump cut-off, charge-and-release). Deterministic, so a replay fires the same edges.
|
||||
|
||||
```ludic
|
||||
program Release {
|
||||
entry {
|
||||
Input.action("charge", ' ')
|
||||
if Input.just_released("charge") { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
20
examples/library/input_auto.ludic
Normal file
20
examples/library/input_auto.ludic
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# input_auto.ludic — proves the frame loop commits the device layer automatically
|
||||
# (#83): a real handler game reads Input.active in its Update phase and NEVER calls
|
||||
# Input.poll by hand, yet the action fires from the live key. Before this fix the
|
||||
# loop only fed Input.key(), so Input.active read empty unless the game polled.
|
||||
#
|
||||
# Driven headless by the keys on stdin — one char per frame. With "llq": frame 1
|
||||
# and 2 read 'l' (the action key), frame 3 reads 'q' (which quits). So the action
|
||||
# is active on exactly two frames.
|
||||
#
|
||||
# Run: printf 'llq' | bin/ludic examples/library/input_auto.ludic -> 2
|
||||
program InputAuto {
|
||||
property Tag { n: int = 0 }
|
||||
model Player { Tag }
|
||||
|
||||
var hits: int = 0
|
||||
|
||||
@OnStart handler Boot { Input.action("go", 'l') } # 'l' == 108
|
||||
handler Sense phase Update { if Input.active("go") { hits = hits + 1 } }
|
||||
@OnQuit handler Report { print(hits) }
|
||||
}
|
||||
42
examples/library/input_manager.ludic
Normal file
42
examples/library/input_manager.ludic
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# input_manager.ludic — the Input Manager (#83): default bindings a game ships and
|
||||
# a player rebinds, and device-agnostic actions that read the multi-key device
|
||||
# layer across keyboard and gamepad, with clean on-press / on-release edges. Driven
|
||||
# headless by injection (Input.press / Input.set_pad), the same way #7/#50 test.
|
||||
#
|
||||
# Deterministic; a full run prints: 1 1 1 0 1 0 0 1
|
||||
program InputManager {
|
||||
function bi(b: bool) -> int { if b { return 1 }; return 0 }
|
||||
|
||||
entry {
|
||||
Input.action("jump", ' ') # ship a default binding (space)
|
||||
Input.action("jump", 'k') # ignored — jump already has a default, not clobbered
|
||||
Input.bind_pad("jump", 0) # device-agnostic: also fire jump from pad button 0
|
||||
|
||||
# A. active via the keyboard, with on-press / on-release edges.
|
||||
Input.press(' ')
|
||||
Input.poll()
|
||||
print(bi(Input.active("jump"))) # 1 — space held
|
||||
print(bi(Input.just_pressed("jump"))) # 1 — went active this frame
|
||||
print(bi(Input.active("jump"))) # 1 — still active
|
||||
Input.poll()
|
||||
print(bi(Input.just_pressed("jump"))) # 0 — held, not a fresh press
|
||||
Input.release(' ')
|
||||
Input.poll()
|
||||
print(bi(Input.just_released("jump"))) # 1 — went inactive this frame
|
||||
print(bi(Input.active("jump"))) # 0
|
||||
|
||||
# B. the default was not clobbered — 'k' never fires jump (2nd default ignored).
|
||||
Input.press('k')
|
||||
Input.poll()
|
||||
print(bi(Input.active("jump"))) # 0 — 'k' isn't bound to jump
|
||||
|
||||
# C. device-agnostic — a gamepad button fires the same action, no key held.
|
||||
# (The just_pressed/just_released *edges* also work across the pad in a real
|
||||
# windowed game, where the platform refreshes the pad each frame; the headless
|
||||
# injection path here exercises the device-agnostic `active` read.)
|
||||
Input.release('k')
|
||||
Input.set_pad(0, true, 1, 0.0, 0.0, 0.0, 0.0) # pad 0, button-0 mask (bit 0)
|
||||
Input.poll()
|
||||
print(bi(Input.active("jump"))) # 1 — active via the pad, no key held
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ const INPUT_REC_CAP: int = 8192 # recordable frames
|
|||
|
||||
var input_names: pointers = null # action name per slot (0..input_nact)
|
||||
var input_keys: words = null # INPUT_MAX_ACT * INPUT_MAX_KEYS key codes (0 = empty)
|
||||
var input_pads: words = null # INPUT_MAX_ACT * INPUT_MAX_KEYS pad buttons, stored +1 (0 = empty) — #83
|
||||
var input_nact: int = 0
|
||||
|
||||
var input_frame: int = 0 # the key polled this frame
|
||||
|
|
@ -40,6 +41,7 @@ function input_init() -> void {
|
|||
if input_names == null {
|
||||
input_names = bytes(INPUT_MAX_ACT * 8) # a pointer (8 bytes) per action slot
|
||||
input_keys = words(INPUT_MAX_ACT * INPUT_MAX_KEYS)
|
||||
input_pads = words(INPUT_MAX_ACT * INPUT_MAX_KEYS) # #83 pad buttons per action (+1 encoded)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +84,38 @@ function input_bind(name: pointer, key: int) -> void {
|
|||
}
|
||||
}
|
||||
|
||||
# #83 — a *default* binding: bind `key` only if the action has no key bound yet.
|
||||
# A game ships its defaults with Input.action in Boot; a player's later Input.rebind
|
||||
# (or a loaded key-map) is not clobbered, and re-running the defaults is idempotent.
|
||||
function input_default(name: pointer, key: int) -> void {
|
||||
let s = input_slot(name)
|
||||
let base = s * INPUT_MAX_KEYS
|
||||
var i = 0
|
||||
while i < INPUT_MAX_KEYS {
|
||||
if input_keys[base + i] != 0 { return } # already has a binding — keep it
|
||||
i = i + 1
|
||||
}
|
||||
input_keys[base] = key
|
||||
}
|
||||
|
||||
# #83 — device-agnostic actions: also fire the named action from a gamepad button.
|
||||
# Buttons are stored +1 so 0 stays the empty marker. The same action can carry both
|
||||
# keyboard keys (input_bind / input_default) and pad buttons; a read fires on either.
|
||||
function input_bind_pad(name: pointer, button: int) -> void {
|
||||
let s = input_slot(name)
|
||||
let base = s * INPUT_MAX_KEYS
|
||||
var i = 0
|
||||
while i < INPUT_MAX_KEYS {
|
||||
if input_pads[base + i] == (button + 1) { return } # already bound
|
||||
i = i + 1
|
||||
}
|
||||
i = 0
|
||||
while i < INPUT_MAX_KEYS {
|
||||
if input_pads[base + i] == 0 { input_pads[base + i] = button + 1; return }
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
# runtime rebinding: replace `oldkey` with `newkey` on the named action. A no-op
|
||||
# if the action or the old key is not found.
|
||||
function input_rebind(name: pointer, oldkey: int, newkey: int) -> void {
|
||||
|
|
@ -173,6 +207,7 @@ var in_wheel: int = 0 # wheel delta this frame
|
|||
# gamepads: connected flag, button bitmask, and IN_AXES fixed axes each
|
||||
var in_pad_conn: words = null # IN_PADS
|
||||
var in_pad_btn: words = null # IN_PADS
|
||||
var in_pad_btn0: words = null # IN_PADS — pad button mask last frame (edges) — #83
|
||||
var in_pad_axis: words = null # IN_PADS * IN_AXES (fixed)
|
||||
# touch points: active flag, x, y each
|
||||
var in_touch_on: words = null # IN_TOUCH
|
||||
|
|
@ -189,6 +224,7 @@ function in_init() -> void {
|
|||
in_prev = words(IN_WORDS)
|
||||
in_pad_conn = words(IN_PADS)
|
||||
in_pad_btn = words(IN_PADS)
|
||||
in_pad_btn0 = words(IN_PADS)
|
||||
in_pad_axis = words(IN_PADS * IN_AXES)
|
||||
in_touch_on = words(IN_TOUCH)
|
||||
in_touch_x = words(IN_TOUCH)
|
||||
|
|
@ -217,6 +253,11 @@ function in_set_or(dst: words, a: words, b: words) -> void { var i = 0; while i
|
|||
function input_device_commit(k: int, replaying: int) -> void {
|
||||
in_init()
|
||||
in_set_copy(in_prev, in_held) # last frame's committed set
|
||||
# #83: snapshot last frame's pad-button masks for the just_pressed/released edges.
|
||||
# Taken before the platform refresh (win_pad, below, runs after this), so it holds
|
||||
# the previous frame's committed value against which this frame's edge is measured.
|
||||
var pj = 0
|
||||
while pj < IN_PADS { in_pad_btn0[pj] = in_pad_btn[pj]; pj = pj + 1 }
|
||||
|
||||
if replaying == 1 {
|
||||
# rebuild the platform set + mouse from the tape; sim/injection is ignored so
|
||||
|
|
@ -401,6 +442,45 @@ function input_down(name: pointer) -> bool {
|
|||
return input_slot_has(input_find(name), input_frame)
|
||||
}
|
||||
|
||||
# #83 — the Input-Manager reads: an action is *active* when any of its bound
|
||||
# keyboard keys is in the multi-key device held-set OR any of its bound pad buttons
|
||||
# is down on pad 0. Unlike input_down (which reads the single per-frame key), these
|
||||
# see the whole device layer (hold left AND jump), and are device-agnostic. The
|
||||
# frame loop now commits the device layer automatically (input_poll), so these read
|
||||
# live without the game calling Input.poll by hand.
|
||||
function input_active_in(name: pointer, held: words, padmask: int) -> bool {
|
||||
let s = input_find(name)
|
||||
if s < 0 { return false }
|
||||
let base = s * INPUT_MAX_KEYS
|
||||
var i = 0
|
||||
while i < INPUT_MAX_KEYS {
|
||||
let k = input_keys[base + i]
|
||||
if (k != 0) and in_bit_get(held, k) { return true }
|
||||
let pb = input_pads[base + i]
|
||||
if (pb != 0) and ((padmask & (1 << (pb - 1))) != 0) { return true }
|
||||
i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
function input_active(name: pointer) -> bool {
|
||||
in_init()
|
||||
return input_active_in(name, in_held, in_pad_btn[0])
|
||||
}
|
||||
# went active this frame (active now, not last frame) — the deterministic on-press.
|
||||
function input_just_pressed(name: pointer) -> bool {
|
||||
in_init()
|
||||
let now = input_active_in(name, in_held, in_pad_btn[0])
|
||||
let was = input_active_in(name, in_prev, in_pad_btn0[0])
|
||||
return now and (not was)
|
||||
}
|
||||
# went inactive this frame (not active now, was last frame) — the on-release.
|
||||
function input_just_released(name: pointer) -> bool {
|
||||
in_init()
|
||||
let now = input_active_in(name, in_held, in_pad_btn[0])
|
||||
let was = input_active_in(name, in_prev, in_pad_btn0[0])
|
||||
return (not now) and was
|
||||
}
|
||||
|
||||
# did the named action go down this frame (down now, not down last frame)?
|
||||
function input_pressed(name: pointer) -> bool {
|
||||
let s = input_find(name)
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
|
|
|
|||
|
|
@ -467,10 +467,21 @@ function emit_game_main() -> void {
|
|||
emit(" br i1 "); emit(rc); emit(", label %body, label %done\n")
|
||||
}
|
||||
emit("body:\n")
|
||||
# #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")
|
||||
emit_calls_for_phase("Update")
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
62613
selfhost/ludicc.seed.ll
62613
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -301,6 +301,8 @@ function cmd_test() -> int {
|
|||
# deterministic input record/replay — fed one key per poll from stdin.
|
||||
feat_case("library/input_actions", " xwa", "1 0 1 1 0 1 0", "input_actions.ludic (#7 action maps + rebinding + deterministic replay)")
|
||||
feat_case("library/input_device", "", "1 1 0 1 0 1 71 -71 5 1 3 1 2 1 0 0 1", "input_device.ludic (#50 multi-key held state + analog axis/vector + mouse + gamepad/touch + full-state replay)")
|
||||
feat_case("library/input_manager", "", "1 1 1 0 1 0 0 1", "input_manager.ludic (#83 Input Manager: default bindings + device-agnostic actions across keyboard/pad + on-press/on-release edges)")
|
||||
feat_case("library/input_auto", "llq", "2", "input_auto.ludic (#83 the frame loop auto-commits the device layer: Input.active fires with no manual Input.poll)")
|
||||
feat_case("library/audio", "", "0 0 0 0 1", "audio.ludic (#22 Audio.* load/play/music/volume/pitch/stop/is_playing — headless no-op)")
|
||||
# #6 Http.* — the client links Foundation (macOS-only), so build it through the
|
||||
# canonical `ludicc -o` path (which wires the framework) and gate on Darwin. The
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue