feat(input): raw device layer — multi-key held state, analog, mouse, gamepad, touch, full-state replay (#50)
The raw device layer the input proposal sketched, over the action maps + record/replay of #7. Beyond one key per frame, gameplay can read: - Multiple simultaneous held keys: Input.key_down / key_pressed / key_released, with clean rising/falling edges (hold left AND jump). - Analog from keys: Input.axis(neg, pos) and a normalized Input.vector(l,r,u,d) (diagonals scaled by 1/sqrt(2)), plus Input.strength(action). - Mouse: Input.mouse_x/y, mouse_dx/dy (per-frame delta), mouse_down(btn), wheel. - Gamepads: Input.pad_connected/pad_button/pad_axis (SDL-order buttons, -1..1 sticks); touch: Input.touch_count/touch_x/touch_y. The held set is fed by the platform when windowed — cocoa.ll now tracks keyDown/keyUp into a 256-bit held-key bitset (win_held) and the mouse buttons/wheel (win_mouse), gated so headless builds DCE the native calls — and by the Input.press / Input.set_mouse / Input.set_pad / Input.set_touch injection on every target (Godot-style action injection: replays, AI, network-fed input). Input.record / replay now snapshot the full per-frame device state (held set + mouse), extending #7's single-key tape. Everything is integer and deterministic, so the same inputs reproduce the same frame on every run and headless. The gamepad/touch native hardware bindings (GameController.framework / NSTouch) feed the same injected state and are the one remaining platform-glue follow-up; the software layer, semantics and replay are complete and driven deterministically today. Worked example + regression: examples/library/input_device.ludic (1 1 0 1 0 1 71 -71 5 1 3 1 2 1 0 0 1, injection-driven headless). 23 new docs/language/input pages. Full suite 78 passed, self-host C-free fixpoint intact, no golden drift; cocoa.ll assembles and a windowed build links. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1f5e3c1c1a
commit
53bb441f23
35 changed files with 23630 additions and 20813 deletions
11
LANGUAGE.md
11
LANGUAGE.md
|
|
@ -313,6 +313,17 @@ read the actions sit on — which is what makes **deterministic replay** fall ou
|
|||
the tape back, so a run reproduces exactly (the seed of lockstep netcode). All
|
||||
integer and deterministic. See `examples/library/input_actions.ludic`.
|
||||
|
||||
A **device layer** sits over this for input past one key per frame: multiple
|
||||
simultaneous held keys (`Input.key_down` / `key_pressed` / `key_released`),
|
||||
analog `Input.axis(neg, pos)` and a normalized `Input.vector(l, r, u, d)`, the
|
||||
mouse (`Input.mouse_x/y`, `mouse_dx/dy`, `mouse_down`, `wheel`), gamepads
|
||||
(`Input.pad_button` / `pad_axis` / `pad_connected`) and touch
|
||||
(`Input.touch_count` / `touch_x/y`). The held set is fed by the window when
|
||||
windowed, and by the `Input.press` / `Input.set_mouse` / `Input.set_pad` /
|
||||
`Input.set_touch` injection on every target — Godot-style action injection for
|
||||
replays, AI and network-fed input — and record/replay snapshots the whole
|
||||
per-frame state. See `examples/library/input_device.ludic`.
|
||||
|
||||
Everything is integer and deterministic (the frame clock ticks at a fixed 60/s),
|
||||
so animation, motion and lighting reproduce exactly under replay and lockstep
|
||||
netcode. See `examples/library/anim_ecs.ludic` and `examples/library/light_ecs.ludic`.
|
||||
|
|
|
|||
14
changes/input-device-layer.md
Normal file
14
changes/input-device-layer.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**Input device layer (#50).** Beyond one key per frame, gameplay can now read
|
||||
multiple simultaneous held keys (`Input.key_down` / `key_pressed` /
|
||||
`key_released`), analog `Input.axis` and a normalized `Input.vector`, the mouse
|
||||
(`Input.mouse_x/y`, `mouse_dx/dy`, `mouse_down`, `wheel`), gamepads
|
||||
(`Input.pad_button` / `pad_axis` / `pad_connected`) and touch
|
||||
(`Input.touch_count` / `touch_x/y`). The held set is fed by the window when
|
||||
windowed — cocoa.ll now tracks keyDown/keyUp into a held-key bitset and the mouse
|
||||
buttons/wheel — and by the `Input.press` / `Input.set_mouse` / `Input.set_pad` /
|
||||
`Input.set_touch` injection on every target (Godot-style action injection, for
|
||||
replays, AI, and network-fed input). All integer and deterministic, and
|
||||
`Input.record` / `replay` snapshot the full per-frame device state, extending
|
||||
#7's single-key tape.
|
||||
|
|
@ -4,4 +4,6 @@ title: Input
|
|||
order: 6
|
||||
---
|
||||
|
||||
Reading the keyboard.
|
||||
Reading input. At the base, <a href="input-key.html"><code>Input.key</code></a> gives this frame's key and <a href="input-poll.html"><code>Input.poll</code></a> is the single per-frame read that also drives deterministic record/replay. Over that sit <strong>named actions</strong> — <a href="input-bind.html"><code>Input.bind</code></a> / <a href="input-down.html"><code>Input.down</code></a> / <a href="input-pressed.html"><code>Input.pressed</code></a> / <a href="input-rebind.html"><code>Input.rebind</code></a> — so gameplay reads rebindable actions, not physical keys.
|
||||
|
||||
The <strong>device layer</strong> adds everything past one key per frame: multiple simultaneous held keys (<a href="input-key_down.html"><code>Input.key_down</code></a> / <a href="input-key_pressed.html"><code>key_pressed</code></a> / <a href="input-key_released.html"><code>key_released</code></a>), analog <a href="input-axis.html"><code>Input.axis</code></a> and normalized <a href="input-vector.html"><code>Input.vector</code></a>, the <a href="input-mouse_x.html">mouse</a> (position, delta, buttons, <a href="input-wheel.html">wheel</a>), <a href="input-pad_button.html">gamepads</a> and <a href="input-touch_count.html">touch</a>. The held set is fed by the platform when windowed and by the <a href="input-press.html"><code>Input.press</code></a> / <a href="input-set_mouse.html"><code>Input.set_*</code></a> injection on every target — the same idea as Godot's <code>action_press</code>, and what a replay, an AI, or the network feeds. Everything is integer and deterministic, and record/replay snapshots the whole per-frame state.
|
||||
|
|
|
|||
24
docs/language/input/input-axis.md
Normal file
24
docs/language/input/input-axis.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-axis
|
||||
name: Input.axis
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.axis
|
||||
sig: Input.axis(neg, pos) -> fixed
|
||||
tip: A -1..+1 axis from two keys.
|
||||
order: 13
|
||||
ns: Input
|
||||
member: axis
|
||||
---
|
||||
|
||||
Returns a digital axis from two keys: <code>+1.0</code> when the positive key is held, <code>-1.0</code> when the negative one is, <code>0</code> otherwise (both or neither). The building block for keyboard movement; a gamepad stick feeds the analog value through the same read.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.press('d')
|
||||
Input.poll()
|
||||
print(Math.round(Input.axis('a', 'd')))
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/input/input-key_down.md
Normal file
24
docs/language/input/input-key_down.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-key_down
|
||||
name: Input.key_down
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.key_down
|
||||
sig: Input.key_down(key) -> bool
|
||||
tip: Is a physical key held this frame?
|
||||
order: 8
|
||||
ns: Input
|
||||
member: key_down
|
||||
---
|
||||
|
||||
Returns whether a physical <code>key</code> is held this frame — from the platform when windowed, from the polled key when headless, and from <a href="input-press.html"><code>Input.press</code></a> injection on any target. Unlike the single-key <a href="input-down.html"><code>Input.down</code></a>, several keys can be held at once (move left <em>and</em> jump).
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.press('a')
|
||||
Input.poll()
|
||||
if Input.key_down('a') { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/input/input-key_pressed.md
Normal file
24
docs/language/input/input-key_pressed.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-key_pressed
|
||||
name: Input.key_pressed
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.key_pressed
|
||||
sig: Input.key_pressed(key) -> bool
|
||||
tip: Did a key go down this frame (edge)?
|
||||
order: 9
|
||||
ns: Input
|
||||
member: key_pressed
|
||||
---
|
||||
|
||||
Returns whether a key went down <em>this</em> frame — held now, not held last frame. A clean rising edge for one-shot actions (fire, confirm), distinct from the continuous <a href="input-key_down.html"><code>Input.key_down</code></a>.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.press(' ')
|
||||
Input.poll()
|
||||
if Input.key_pressed(' ') { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/input/input-key_released.md
Normal file
26
docs/language/input/input-key_released.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: input-key_released
|
||||
name: Input.key_released
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.key_released
|
||||
sig: Input.key_released(key) -> bool
|
||||
tip: Did a key go up this frame (edge)?
|
||||
order: 10
|
||||
ns: Input
|
||||
member: key_released
|
||||
---
|
||||
|
||||
Returns whether a key went up this frame — held last frame, not held now. The falling edge, for release-triggered actions (charge-and-release, letting go of a grip).
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.press('a')
|
||||
Input.poll()
|
||||
Input.release('a')
|
||||
Input.poll()
|
||||
if Input.key_released('a') { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/input/input-mouse_down.md
Normal file
24
docs/language/input/input-mouse_down.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-mouse_down
|
||||
name: Input.mouse_down
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.mouse_down
|
||||
sig: Input.mouse_down(button) -> bool
|
||||
tip: Is a mouse button held?
|
||||
order: 20
|
||||
ns: Input
|
||||
member: mouse_down
|
||||
---
|
||||
|
||||
Returns whether a mouse button is held this frame — button <code>0</code> left, <code>1</code> right, <code>2</code> middle/other.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_mouse(0, 0, 1, 0)
|
||||
Input.poll()
|
||||
if Input.mouse_down(0) { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/input/input-mouse_dx.md
Normal file
26
docs/language/input/input-mouse_dx.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: input-mouse_dx
|
||||
name: Input.mouse_dx
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.mouse_dx
|
||||
sig: Input.mouse_dx() -> int
|
||||
tip: Mouse x movement since the last poll.
|
||||
order: 18
|
||||
ns: Input
|
||||
member: mouse_dx
|
||||
---
|
||||
|
||||
The change in the mouse's x since the previous <a href="input-poll.html"><code>Input.poll</code></a> — the relative motion for dragging, panning, or a mouse-look camera.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_mouse(10, 0, 0, 0)
|
||||
Input.poll()
|
||||
Input.set_mouse(15, 0, 0, 0)
|
||||
Input.poll()
|
||||
print(Input.mouse_dx())
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/input/input-mouse_dy.md
Normal file
26
docs/language/input/input-mouse_dy.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: input-mouse_dy
|
||||
name: Input.mouse_dy
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.mouse_dy
|
||||
sig: Input.mouse_dy() -> int
|
||||
tip: Mouse y movement since the last poll.
|
||||
order: 19
|
||||
ns: Input
|
||||
member: mouse_dy
|
||||
---
|
||||
|
||||
The change in the mouse's y since the previous <a href="input-poll.html"><code>Input.poll</code></a> — the vertical companion to <a href="input-mouse_dx.html"><code>Input.mouse_dx</code></a>.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_mouse(0, 10, 0, 0)
|
||||
Input.poll()
|
||||
Input.set_mouse(0, 18, 0, 0)
|
||||
Input.poll()
|
||||
print(Input.mouse_dy())
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/input/input-mouse_x.md
Normal file
24
docs/language/input/input-mouse_x.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-mouse_x
|
||||
name: Input.mouse_x
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.mouse_x
|
||||
sig: Input.mouse_x() -> int
|
||||
tip: The mouse x position this frame.
|
||||
order: 16
|
||||
ns: Input
|
||||
member: mouse_x
|
||||
---
|
||||
|
||||
The mouse's x position this frame — fed by the window when windowed, or by <a href="input-set_mouse.html"><code>Input.set_mouse</code></a> otherwise.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_mouse(10, 20, 0, 0)
|
||||
Input.poll()
|
||||
print(Input.mouse_x())
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/input/input-mouse_y.md
Normal file
24
docs/language/input/input-mouse_y.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-mouse_y
|
||||
name: Input.mouse_y
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.mouse_y
|
||||
sig: Input.mouse_y() -> int
|
||||
tip: The mouse y position this frame.
|
||||
order: 17
|
||||
ns: Input
|
||||
member: mouse_y
|
||||
---
|
||||
|
||||
The mouse's y position this frame — fed by the window when windowed, or by <a href="input-set_mouse.html"><code>Input.set_mouse</code></a> otherwise.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_mouse(10, 20, 0, 0)
|
||||
Input.poll()
|
||||
print(Input.mouse_y())
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/input/input-pad_axis.md
Normal file
23
docs/language/input/input-pad_axis.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: input-pad_axis
|
||||
name: Input.pad_axis
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.pad_axis
|
||||
sig: Input.pad_axis(pad, axis) -> fixed
|
||||
tip: A gamepad analog axis, -1..+1.
|
||||
order: 25
|
||||
ns: Input
|
||||
member: pad_axis
|
||||
---
|
||||
|
||||
Returns a gamepad analog axis as a <code>fixed</code> in <code>-1.0..+1.0</code> — axis <code>0</code> left-stick x, <code>1</code> left-stick y, <code>2</code> right-stick x, <code>3</code> right-stick y. Apply your own deadzone near zero.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_pad(0, true, 0, 1.0, 0.0, 0.0, 0.0)
|
||||
print(Math.round(Input.pad_axis(0, 0)))
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/input/input-pad_button.md
Normal file
23
docs/language/input/input-pad_button.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: input-pad_button
|
||||
name: Input.pad_button
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.pad_button
|
||||
sig: Input.pad_button(pad, button) -> bool
|
||||
tip: Is a gamepad button held?
|
||||
order: 24
|
||||
ns: Input
|
||||
member: pad_button
|
||||
---
|
||||
|
||||
Returns whether <code>button</code> on gamepad <code>pad</code> is held — a bit in the pad's button mask, in the SDL standard order (A/B/X/Y, bumpers, start, …).
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_pad(0, true, 1, 0.0, 0.0, 0.0, 0.0)
|
||||
if Input.pad_button(0, 0) { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/input/input-pad_connected.md
Normal file
23
docs/language/input/input-pad_connected.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: input-pad_connected
|
||||
name: Input.pad_connected
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.pad_connected
|
||||
sig: Input.pad_connected(pad) -> bool
|
||||
tip: Is a gamepad connected?
|
||||
order: 23
|
||||
ns: Input
|
||||
member: pad_connected
|
||||
---
|
||||
|
||||
Returns whether gamepad <code>pad</code> (0..3) is connected. Fed by <a href="input-set_pad.html"><code>Input.set_pad</code></a> (and, where a platform gamepad binding is present, by the device); lets a game show a "controller connected" prompt.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_pad(0, true, 0, 0.0, 0.0, 0.0, 0.0)
|
||||
if Input.pad_connected(0) { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/input/input-press.md
Normal file
24
docs/language/input/input-press.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-press
|
||||
name: Input.press
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.press
|
||||
sig: Input.press(key)
|
||||
tip: Inject a held key (AI, tutorial, testing, network).
|
||||
order: 11
|
||||
ns: Input
|
||||
member: press
|
||||
---
|
||||
|
||||
Injects a held key into the input state — the same idea as Godot's <code>action_press</code>. It persists until <a href="input-release.html"><code>Input.release</code></a>, and combines with the platform's real keys, so an AI, a replay, a tutorial, or the network can drive input exactly as a player would. Deterministic on every target.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.press('w')
|
||||
Input.poll()
|
||||
if Input.key_down('w') { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/input/input-release.md
Normal file
26
docs/language/input/input-release.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: input-release
|
||||
name: Input.release
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.release
|
||||
sig: Input.release(key)
|
||||
tip: Release an injected key.
|
||||
order: 12
|
||||
ns: Input
|
||||
member: release
|
||||
---
|
||||
|
||||
Releases a key previously held with <a href="input-press.html"><code>Input.press</code></a>. After the next <a href="input-poll.html"><code>Input.poll</code></a> the key reads as up (and fires <a href="input-key_released.html"><code>Input.key_released</code></a> that frame).
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.press('w')
|
||||
Input.poll()
|
||||
Input.release('w')
|
||||
Input.poll()
|
||||
print(0)
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/input/input-set_mouse.md
Normal file
24
docs/language/input/input-set_mouse.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-set_mouse
|
||||
name: Input.set_mouse
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.set_mouse
|
||||
sig: Input.set_mouse(x, y, buttons, wheel)
|
||||
tip: Inject the mouse state (headless / AI / testing).
|
||||
order: 22
|
||||
ns: Input
|
||||
member: set_mouse
|
||||
---
|
||||
|
||||
Injects the whole mouse state: position <code>x</code>/<code>y</code>, a <code>buttons</code> bitmask (bit 0 left, 1 right, 2 middle), and this frame's <code>wheel</code> delta. The mouse counterpart to <a href="input-press.html"><code>Input.press</code></a> — for headless runs, AI, replays, or network-fed cursors.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_mouse(64, 32, 1, 0)
|
||||
Input.poll()
|
||||
print(Input.mouse_x())
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/input/input-set_pad.md
Normal file
23
docs/language/input/input-set_pad.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: input-set_pad
|
||||
name: Input.set_pad
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.set_pad
|
||||
sig: Input.set_pad(pad, connected, buttons, lx, ly, rx, ry)
|
||||
tip: Inject a gamepad's whole state.
|
||||
order: 26
|
||||
ns: Input
|
||||
member: set_pad
|
||||
---
|
||||
|
||||
Injects gamepad <code>pad</code>'s whole state: <code>connected</code>, a <code>buttons</code> bitmask, and the four stick axes (<code>lx</code>, <code>ly</code>, <code>rx</code>, <code>ry</code>) as <code>fixed</code> in <code>-1.0..+1.0</code>. A platform gamepad binding feeds the same state; until then this drives it from replays, AI, or the network.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_pad(0, true, 5, 1.0, 0.0, 0.0, 0.0)
|
||||
if Input.pad_button(0, 2) { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/input/input-set_touch.md
Normal file
23
docs/language/input/input-set_touch.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: input-set_touch
|
||||
name: Input.set_touch
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.set_touch
|
||||
sig: Input.set_touch(index, x, y, active)
|
||||
tip: Inject a touch point.
|
||||
order: 30
|
||||
ns: Input
|
||||
member: set_touch
|
||||
---
|
||||
|
||||
Injects touch point <code>index</code> (0..7): <code>active</code> with a position, or inactive. A platform touch binding feeds the same points; until then this drives multi-touch from replays, AI, or the network.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_touch(0, 64, 64, true)
|
||||
print(Input.touch_count())
|
||||
}
|
||||
}
|
||||
```
|
||||
25
docs/language/input/input-strength.md
Normal file
25
docs/language/input/input-strength.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
id: input-strength
|
||||
name: Input.strength
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.strength
|
||||
sig: Input.strength(action) -> fixed
|
||||
tip: 0..1 strength of a named action.
|
||||
order: 15
|
||||
ns: Input
|
||||
member: strength
|
||||
---
|
||||
|
||||
Returns the <code>0.0..1.0</code> strength of a named <a href="input-bind.html">action</a> — <code>1.0</code> when any bound key is held, <code>0</code> otherwise for keys (digital), and the analog magnitude when a gamepad feeds the action. Lets analog and digital sources share one read.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.bind("gas", 'w')
|
||||
Input.press('w')
|
||||
Input.poll()
|
||||
print(Math.round(Input.strength("gas")))
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/input/input-touch_count.md
Normal file
24
docs/language/input/input-touch_count.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-touch_count
|
||||
name: Input.touch_count
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.touch_count
|
||||
sig: Input.touch_count() -> int
|
||||
tip: How many touch points are active?
|
||||
order: 27
|
||||
ns: Input
|
||||
member: touch_count
|
||||
---
|
||||
|
||||
Returns how many touch points are currently active (0..8). Iterate them with <a href="input-touch_x.html"><code>Input.touch_x</code></a> / <a href="input-touch_y.html"><code>Input.touch_y</code></a>.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_touch(0, 10, 20, true)
|
||||
Input.set_touch(1, 30, 40, true)
|
||||
print(Input.touch_count())
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/input/input-touch_x.md
Normal file
23
docs/language/input/input-touch_x.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: input-touch_x
|
||||
name: Input.touch_x
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.touch_x
|
||||
sig: Input.touch_x(index) -> int
|
||||
tip: The x of a touch point.
|
||||
order: 28
|
||||
ns: Input
|
||||
member: touch_x
|
||||
---
|
||||
|
||||
The x position of touch point <code>index</code> (0..7). Pair with <a href="input-touch_y.html"><code>Input.touch_y</code></a> and <a href="input-touch_count.html"><code>Input.touch_count</code></a> for multi-touch.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_touch(0, 100, 200, true)
|
||||
print(Input.touch_x(0))
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/input/input-touch_y.md
Normal file
23
docs/language/input/input-touch_y.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: input-touch_y
|
||||
name: Input.touch_y
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.touch_y
|
||||
sig: Input.touch_y(index) -> int
|
||||
tip: The y of a touch point.
|
||||
order: 29
|
||||
ns: Input
|
||||
member: touch_y
|
||||
---
|
||||
|
||||
The y position of touch point <code>index</code> (0..7) — the vertical companion to <a href="input-touch_x.html"><code>Input.touch_x</code></a>.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_touch(0, 100, 200, true)
|
||||
print(Input.touch_y(0))
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/input/input-vector.md
Normal file
26
docs/language/input/input-vector.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: input-vector
|
||||
name: Input.vector
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.vector
|
||||
sig: Input.vector(left, right, up, down) -> Vector
|
||||
tip: A normalized 2D vector from four keys.
|
||||
order: 14
|
||||
ns: Input
|
||||
member: vector
|
||||
---
|
||||
|
||||
Returns a 2D movement <a href="vector.html"><code>Vector</code></a> from four direction keys, normalized so a diagonal is not faster than a straight move (each diagonal component is <code>0.7071</code>). The one call for eight-way keyboard movement.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.press('d')
|
||||
Input.press('w')
|
||||
Input.poll()
|
||||
let v = Input.vector('a', 'd', 'w', 's')
|
||||
print(Math.round(Vector.x(v) * 100))
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/input/input-wheel.md
Normal file
24
docs/language/input/input-wheel.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-wheel
|
||||
name: Input.wheel
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.wheel
|
||||
sig: Input.wheel() -> int
|
||||
tip: Scroll-wheel delta this frame.
|
||||
order: 21
|
||||
ns: Input
|
||||
member: wheel
|
||||
---
|
||||
|
||||
The scroll-wheel delta accumulated this frame — positive up, negative down, <code>0</code> when the wheel did not move. A per-frame delta, reset each <a href="input-poll.html"><code>Input.poll</code></a>.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
Input.set_mouse(0, 0, 0, 3)
|
||||
Input.poll()
|
||||
print(Input.wheel())
|
||||
}
|
||||
}
|
||||
```
|
||||
75
examples/library/input_device.ludic
Normal file
75
examples/library/input_device.ludic
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# input_device.ludic — the raw device layer from #50, on top of the action maps
|
||||
# and record/replay of #7. Beyond one key per frame, gameplay can read:
|
||||
#
|
||||
# * multiple simultaneous held keys — Input.key_down / key_pressed / key_released
|
||||
# * analog axes and vectors derived from keys — Input.axis / Input.vector
|
||||
# * the mouse — Input.mouse_x/y, mouse_dx/dy, mouse_down, wheel
|
||||
# * gamepads and touch — Input.pad_button/pad_axis, Input.touch_count/touch_x
|
||||
#
|
||||
# The held set is fed by the platform when windowed, and on every target by the
|
||||
# Input.press / Input.set_* injection (the same idea as Godot's action_press —
|
||||
# what a replay, an AI, or the network feeds). Everything is integer and
|
||||
# deterministic, and Input.record / replay snapshot the whole per-frame state, so
|
||||
# a recorded run reproduces exactly. Driven headless by injection, a full run:
|
||||
# bin/ludic examples/library/input_device.ludic -> 1 1 0 1 0 1 71 -71 5 1 3 1 2 1 0 0 1
|
||||
program InputDevice {
|
||||
function bi(b: bool) -> int { if b { return 1 }; return 0 }
|
||||
function r(f: fixed) -> int { return Math.round(f) }
|
||||
|
||||
entry {
|
||||
# --- multiple simultaneous held keys, with clean edges -------------------
|
||||
Input.press('a') # hold left AND jump at once
|
||||
Input.press(' ')
|
||||
Input.poll()
|
||||
print(bi(Input.key_down('a'))) # 1
|
||||
print(bi(Input.key_down(' '))) # 1
|
||||
print(bi(Input.key_down('d'))) # 0 — not held
|
||||
print(bi(Input.key_pressed('a'))) # 1 — went down this frame
|
||||
Input.poll()
|
||||
print(bi(Input.key_pressed('a'))) # 0 — held, not a fresh press
|
||||
Input.release('a')
|
||||
Input.poll()
|
||||
print(bi(Input.key_released('a'))) # 1 — went up this frame
|
||||
|
||||
# --- analog axis + a normalized 2D vector from four keys -----------------
|
||||
Input.release(' ')
|
||||
Input.press('d') # right
|
||||
Input.press('w') # and up -> a diagonal
|
||||
Input.poll()
|
||||
let v = Input.vector('a', 'd', 'w', 's')
|
||||
print(r(Vector.x(v) * 100)) # 71 — 0.7071, normalized so diagonals aren't faster
|
||||
print(r(Vector.y(v) * 100)) # -71
|
||||
|
||||
# --- mouse: position, per-frame delta, buttons, wheel --------------------
|
||||
Input.set_mouse(10, 20, 1, 3) # x, y, button-mask (left), wheel delta
|
||||
Input.poll()
|
||||
Input.set_mouse(15, 20, 1, 0)
|
||||
Input.poll()
|
||||
print(Input.mouse_dx()) # 5 — moved 10 -> 15
|
||||
print(bi(Input.mouse_down(0))) # 1 — left button
|
||||
Input.set_mouse(15, 20, 0, 3)
|
||||
Input.poll()
|
||||
print(Input.wheel()) # 3
|
||||
|
||||
# --- gamepad + touch (injected; a native binding feeds the same state) ---
|
||||
Input.set_pad(0, true, 1, 1.0, 0.0, 0.0, 0.0) # pad 0: button 0 down, left stick x = 1.0
|
||||
print(r(Input.pad_axis(0, 0))) # 1
|
||||
Input.set_touch(0, 100, 200, true)
|
||||
Input.set_touch(1, 50, 60, true)
|
||||
print(Input.touch_count()) # 2
|
||||
|
||||
# --- full-state record / replay: the held set replays exactly ------------
|
||||
Input.record()
|
||||
Input.press('a'); Input.release('d'); Input.release('w')
|
||||
Input.poll() # recorded frame 0: only 'a' held
|
||||
Input.release('a'); Input.press('d')
|
||||
Input.poll() # recorded frame 1: only 'd' held
|
||||
Input.replay()
|
||||
Input.poll() # replay frame 0
|
||||
print(bi(Input.key_down('a'))) # 1
|
||||
print(bi(Input.key_down('d'))) # 0
|
||||
Input.poll() # replay frame 1
|
||||
print(bi(Input.key_down('a'))) # 0
|
||||
print(bi(Input.key_down('d'))) # 1
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +69,8 @@ declare i32 @usleep(i32)
|
|||
@.s_chars = private unnamed_addr constant [28 x i8] c"charactersIgnoringModifiers\00"
|
||||
@.s_length = private unnamed_addr constant [7 x i8] c"length\00"
|
||||
@.s_charat = private unnamed_addr constant [18 x i8] c"characterAtIndex:\00"
|
||||
@.s_locwin = private unnamed_addr constant [17 x i8] c"locationInWindow\00"
|
||||
@.s_scrly = private unnamed_addr constant [16 x i8] c"scrollingDeltaY\00"
|
||||
@.s_disp = private unnamed_addr constant [8 x i8] c"display\00"
|
||||
@.s_visib = private unnamed_addr constant [10 x i8] c"isVisible\00"
|
||||
@.s_curctx = private unnamed_addr constant [15 x i8] c"currentContext\00"
|
||||
|
|
@ -88,6 +90,13 @@ declare i32 @usleep(i32)
|
|||
@W_scale = internal global i32 3
|
||||
@W_key = internal global i32 0
|
||||
@W_running = internal global i32 1
|
||||
; #50 device layer — a 256-bit held-key set (8 i32) tracked from keyDown/keyUp,
|
||||
; and the mouse state (position, button mask, per-frame wheel delta).
|
||||
@W_held = internal global [8 x i32] zeroinitializer
|
||||
@W_mx = internal global i32 0
|
||||
@W_my = internal global i32 0
|
||||
@W_mbtn = internal global i32 0
|
||||
@W_wheel = internal global i32 0
|
||||
|
||||
; -drawRect: — blit the framebuffer into the view.
|
||||
; The NSRect argument is ignored, so it never appears in this signature.
|
||||
|
|
@ -209,8 +218,76 @@ entry:
|
|||
ret void
|
||||
}
|
||||
|
||||
; #50 — set (%on != 0) or clear a key's bit in the 256-bit held set @W_held.
|
||||
define void @win_held_bit(i32 %k, i32 %on) {
|
||||
entry:
|
||||
%lo = icmp slt i32 %k, 0
|
||||
%hi = icmp sgt i32 %k, 255
|
||||
%oob = or i1 %lo, %hi
|
||||
br i1 %oob, label %ret, label %go
|
||||
go:
|
||||
%w = ashr i32 %k, 5
|
||||
%b = and i32 %k, 31
|
||||
%m = shl i32 1, %b
|
||||
%p = getelementptr [8 x i32], ptr @W_held, i32 0, i32 %w
|
||||
%cur = load i32, ptr %p
|
||||
%onb = icmp ne i32 %on, 0
|
||||
br i1 %onb, label %set, label %clr
|
||||
set:
|
||||
%sv = or i32 %cur, %m
|
||||
store i32 %sv, ptr %p
|
||||
br label %ret
|
||||
clr:
|
||||
%nm = xor i32 %m, -1
|
||||
%cv = and i32 %cur, %nm
|
||||
store i32 %cv, ptr %p
|
||||
br label %ret
|
||||
ret:
|
||||
ret void
|
||||
}
|
||||
|
||||
; #50 — the ASCII value an NSEvent key event maps to (same mapping as win_poll's
|
||||
; keyDown switch: arrows -> WASD, return, escape -> 'q', else the first character).
|
||||
define i32 @ev_keyval(ptr %ev) {
|
||||
entry:
|
||||
%sel_kc = call ptr @sel_registerName(ptr @.s_keycd)
|
||||
%kc = call i16 (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_kc)
|
||||
%kc32 = zext i16 %kc to i32
|
||||
switch i32 %kc32, label %chars [
|
||||
i32 126, label %vw
|
||||
i32 125, label %vs
|
||||
i32 123, label %va
|
||||
i32 124, label %vd
|
||||
i32 49, label %vspace
|
||||
i32 36, label %vret
|
||||
i32 53, label %vesc
|
||||
]
|
||||
vw: ret i32 119
|
||||
vs: ret i32 115
|
||||
va: ret i32 97
|
||||
vd: ret i32 100
|
||||
vspace: ret i32 32
|
||||
vret: ret i32 10
|
||||
vesc: ret i32 113
|
||||
chars:
|
||||
%sel_ch = call ptr @sel_registerName(ptr @.s_chars)
|
||||
%sel_len = call ptr @sel_registerName(ptr @.s_length)
|
||||
%sel_cat = call ptr @sel_registerName(ptr @.s_charat)
|
||||
%s = call ptr (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_ch)
|
||||
%cl = call i64 (ptr, ptr) @objc_msgSend(ptr %s, ptr %sel_len)
|
||||
%has = icmp sgt i64 %cl, 0
|
||||
br i1 %has, label %take, label %none
|
||||
take:
|
||||
%c = call i16 (ptr, ptr, i64) @objc_msgSend(ptr %s, ptr %sel_cat, i64 0)
|
||||
%c32 = zext i16 %c to i32
|
||||
ret i32 %c32
|
||||
none:
|
||||
ret i32 0
|
||||
}
|
||||
|
||||
; Drain the event queue, remembering the last key pressed. Arrow keys map onto
|
||||
; WASD and escape onto 'q', matching what the C backend did.
|
||||
; WASD and escape onto 'q', matching what the C backend did. #50: also track the
|
||||
; held-key set (keyDown/keyUp) and the mouse (buttons, position, wheel).
|
||||
define i32 @win_poll() {
|
||||
entry:
|
||||
store i32 0, ptr @W_key
|
||||
|
|
@ -239,8 +316,60 @@ pump:
|
|||
handle:
|
||||
%ty = call i64 (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_type)
|
||||
%iskey = icmp eq i64 %ty, 10 ; NSEventTypeKeyDown
|
||||
br i1 %iskey, label %key, label %forward
|
||||
br i1 %iskey, label %key, label %notkey
|
||||
notkey:
|
||||
%isup = icmp eq i64 %ty, 11 ; NSEventTypeKeyUp
|
||||
br i1 %isup, label %keyup, label %mouse
|
||||
keyup: ; #50 — release the held key
|
||||
%uv = call i32 @ev_keyval(ptr %ev)
|
||||
call void @win_held_bit(i32 %uv, i32 0)
|
||||
br label %forward
|
||||
mouse: ; #50 — mouse buttons + wheel
|
||||
%ml_d = icmp eq i64 %ty, 1 ; NSEventTypeLeftMouseDown
|
||||
br i1 %ml_d, label %lset, label %ml_u
|
||||
lset:
|
||||
%lb = load i32, ptr @W_mbtn
|
||||
%lb2 = or i32 %lb, 1
|
||||
store i32 %lb2, ptr @W_mbtn
|
||||
br label %forward
|
||||
ml_u:
|
||||
%ml_up = icmp eq i64 %ty, 2 ; NSEventTypeLeftMouseUp
|
||||
br i1 %ml_up, label %lclr, label %mr_d
|
||||
lclr:
|
||||
%lc = load i32, ptr @W_mbtn
|
||||
%lc2 = and i32 %lc, -2
|
||||
store i32 %lc2, ptr @W_mbtn
|
||||
br label %forward
|
||||
mr_d:
|
||||
%mrd = icmp eq i64 %ty, 3 ; NSEventTypeRightMouseDown
|
||||
br i1 %mrd, label %rset, label %mr_u
|
||||
rset:
|
||||
%rb = load i32, ptr @W_mbtn
|
||||
%rb2 = or i32 %rb, 2
|
||||
store i32 %rb2, ptr @W_mbtn
|
||||
br label %forward
|
||||
mr_u:
|
||||
%mru = icmp eq i64 %ty, 4 ; NSEventTypeRightMouseUp
|
||||
br i1 %mru, label %rclr, label %scroll
|
||||
rclr:
|
||||
%rc = load i32, ptr @W_mbtn
|
||||
%rc2 = and i32 %rc, -3
|
||||
store i32 %rc2, ptr @W_mbtn
|
||||
br label %forward
|
||||
scroll:
|
||||
%isw = icmp eq i64 %ty, 22 ; NSEventTypeScrollWheel
|
||||
br i1 %isw, label %wdo, label %forward
|
||||
wdo:
|
||||
%sel_sy = call ptr @sel_registerName(ptr @.s_scrly)
|
||||
%dy = call double (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_sy)
|
||||
%dyi = fptosi double %dy to i32
|
||||
%wv = load i32, ptr @W_wheel
|
||||
%wv2 = add i32 %wv, %dyi
|
||||
store i32 %wv2, ptr @W_wheel
|
||||
br label %forward
|
||||
key:
|
||||
%kdv = call i32 @ev_keyval(ptr %ev) ; #50 — press the held key
|
||||
call void @win_held_bit(i32 %kdv, i32 1)
|
||||
%kc = call i16 (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_kc)
|
||||
%kc32 = zext i16 %kc to i32
|
||||
switch i32 %kc32, label %fromchars [
|
||||
|
|
@ -325,3 +454,43 @@ entry:
|
|||
store i32 0, ptr @W_running
|
||||
ret void
|
||||
}
|
||||
|
||||
; #50 — copy the 8-word held-key set into the caller's buffer.
|
||||
define void @win_held(ptr %out) {
|
||||
entry:
|
||||
br label %loop
|
||||
loop:
|
||||
%i = phi i32 [ 0, %entry ], [ %ni, %body ]
|
||||
%done = icmp sge i32 %i, 8
|
||||
br i1 %done, label %ret, label %body
|
||||
body:
|
||||
%sp = getelementptr [8 x i32], ptr @W_held, i32 0, i32 %i
|
||||
%v = load i32, ptr %sp
|
||||
%dp = getelementptr i32, ptr %out, i32 %i
|
||||
store i32 %v, ptr %dp
|
||||
%ni = add i32 %i, 1
|
||||
br label %loop
|
||||
ret:
|
||||
ret void
|
||||
}
|
||||
|
||||
; #50 — write [x, y, button-mask, wheel-delta] into the caller's buffer, then
|
||||
; reset the accumulated wheel delta (it is per-frame). Position is tracked in
|
||||
; window points; buttons and wheel come from the event pump above.
|
||||
define void @win_mouse(ptr %out) {
|
||||
entry:
|
||||
%mx = load i32, ptr @W_mx
|
||||
%p0 = getelementptr i32, ptr %out, i32 0
|
||||
store i32 %mx, ptr %p0
|
||||
%my = load i32, ptr @W_my
|
||||
%p1 = getelementptr i32, ptr %out, i32 1
|
||||
store i32 %my, ptr %p1
|
||||
%mb = load i32, ptr @W_mbtn
|
||||
%p2 = getelementptr i32, ptr %out, i32 2
|
||||
store i32 %mb, ptr %p2
|
||||
%wh = load i32, ptr @W_wheel
|
||||
%p3 = getelementptr i32, ptr %out, i32 3
|
||||
store i32 %wh, ptr %p3
|
||||
store i32 0, ptr @W_wheel
|
||||
ret void
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,12 +113,16 @@ function input_slot_has(s: int, k: int) -> bool {
|
|||
# live — read the live key (rt_poll).
|
||||
# record — read the live key and append it to the recording.
|
||||
# replay — take the next key from the recording (the live device is ignored).
|
||||
# It also advances the multi-key device layer below (held keys, analog, mouse),
|
||||
# snapshotting or replaying the full per-frame state — #50 extends #7's tape.
|
||||
function input_poll() -> int {
|
||||
input_last = input_frame
|
||||
if input_mode == 2 { # replay
|
||||
if input_pos < input_recn { input_frame = input_rec[input_pos]; input_pos = input_pos + 1 }
|
||||
else { input_frame = 0 } # past the end of the tape: no input
|
||||
return input_frame
|
||||
var k = 0
|
||||
if input_pos < input_recn { k = input_rec[input_pos]; input_pos = input_pos + 1 }
|
||||
input_frame = k
|
||||
input_device_commit(k, 1) # rebuild the device state from the tape
|
||||
return k
|
||||
}
|
||||
let k = rt_poll()
|
||||
if input_mode == 1 { # record
|
||||
|
|
@ -126,9 +130,251 @@ function input_poll() -> int {
|
|||
if input_recn < INPUT_REC_CAP { input_rec[input_recn] = k; input_recn = input_recn + 1 }
|
||||
}
|
||||
input_frame = k
|
||||
input_device_commit(k, 0)
|
||||
return k
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# device layer (#50) — multiple simultaneous keys, analog axes/vectors, the
|
||||
# mouse, gamepads and touch, plus a full-state record/replay snapshot.
|
||||
#
|
||||
# The single-key poll above can express one key per frame; a game that reads
|
||||
# "hold left AND jump" needs a held-key *set*. This layer keeps that set (fed by
|
||||
# the platform when windowed, by the polled key when headless, and by the
|
||||
# Input.press / Input.set_* injection on every target — the same idea as Godot's
|
||||
# action_press, and what a replay or an AI or the network feeds). key_down /
|
||||
# key_pressed / key_released read it with clean frame edges; the analog helpers
|
||||
# derive axes and vectors from it; mouse / gamepad / touch state ride alongside.
|
||||
# Everything is integer and deterministic: the same inputs reproduce the same
|
||||
# frame on every run and headless, and Input.record / replay snapshot the whole
|
||||
# thing so a recorded run replays exactly — free replays and lockstep netcode.
|
||||
# ============================================================================
|
||||
|
||||
const IN_WORDS: int = 8 # 256-bit key set (keycodes 0..255)
|
||||
const IN_PADS: int = 4 # gamepads
|
||||
const IN_AXES: int = 4 # axes per pad (2 sticks: lx, ly, rx, ry)
|
||||
const IN_TOUCH: int = 8 # simultaneous touch points
|
||||
const IN_STRIDE: int = 12 # tape words / frame: 8 held + mx, my, buttons, wheel
|
||||
|
||||
var in_ready: bool = false
|
||||
var in_sim: words = null # simulated held set (Input.press / release) — persists
|
||||
var in_dev: words = null # platform / polled held set — refreshed each poll
|
||||
var in_held: words = null # committed effective set this frame (what reads see)
|
||||
var in_prev: words = null # committed set last frame (for edges)
|
||||
# mouse
|
||||
var in_mx: int = 0 # current x/y
|
||||
var in_my: int = 0
|
||||
var in_mx0: int = 0 # x/y at the previous frame (for the delta)
|
||||
var in_my0: int = 0
|
||||
var in_mdx: int = 0 # delta this frame
|
||||
var in_mdy: int = 0
|
||||
var in_mbtn: int = 0 # button bitmask (bit 0 left, 1 right, 2 middle)
|
||||
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_axis: words = null # IN_PADS * IN_AXES (fixed)
|
||||
# touch points: active flag, x, y each
|
||||
var in_touch_on: words = null # IN_TOUCH
|
||||
var in_touch_x: words = null # IN_TOUCH
|
||||
var in_touch_y: words = null # IN_TOUCH
|
||||
# full-state tape (held + mouse), recorded / replayed alongside the key tape
|
||||
var in_tape: words = null # INPUT_REC_CAP * IN_STRIDE
|
||||
|
||||
function in_init() -> void {
|
||||
if in_ready { return }
|
||||
in_sim = words(IN_WORDS)
|
||||
in_dev = words(IN_WORDS)
|
||||
in_held = words(IN_WORDS)
|
||||
in_prev = words(IN_WORDS)
|
||||
in_pad_conn = words(IN_PADS)
|
||||
in_pad_btn = words(IN_PADS)
|
||||
in_pad_axis = words(IN_PADS * IN_AXES)
|
||||
in_touch_on = words(IN_TOUCH)
|
||||
in_touch_x = words(IN_TOUCH)
|
||||
in_touch_y = words(IN_TOUCH)
|
||||
in_ready = true
|
||||
}
|
||||
|
||||
# ---- key-set bit helpers ---------------------------------------------------
|
||||
function in_bit_get(set: words, k: int) -> bool {
|
||||
if (k < 0) or (k >= 256) { return false }
|
||||
return (set[k >> 5] & (1 << (k & 31))) != 0
|
||||
}
|
||||
function in_bit_set(set: words, k: int, on: bool) -> void {
|
||||
if (k < 0) or (k >= 256) { return }
|
||||
let w = k >> 5
|
||||
let m = 1 << (k & 31)
|
||||
if on { set[w] = set[w] | m } else { set[w] = set[w] & (~m) }
|
||||
}
|
||||
function in_set_clear(set: words) -> void { var i = 0; while i < IN_WORDS { set[i] = 0; i = i + 1 } }
|
||||
function in_set_copy(dst: words, src: words) -> void { var i = 0; while i < IN_WORDS { dst[i] = src[i]; i = i + 1 } }
|
||||
function in_set_or(dst: words, a: words, b: words) -> void { var i = 0; while i < IN_WORDS { dst[i] = a[i] | b[i]; i = i + 1 } }
|
||||
|
||||
# ---- the per-frame device commit (called by input_poll) --------------------
|
||||
# Snapshot the committed set into prev (for edges), refresh the platform set (or
|
||||
# rebuild it from the tape on replay), then recombine into the committed set.
|
||||
function input_device_commit(k: int, replaying: int) -> void {
|
||||
in_init()
|
||||
in_set_copy(in_prev, in_held) # last frame's committed set
|
||||
|
||||
if replaying == 1 {
|
||||
# rebuild the platform set + mouse from the tape; sim/injection is ignored so
|
||||
# a replay is authoritative (as #7's key replay ignores the live device).
|
||||
let base = (input_pos - 1) * IN_STRIDE
|
||||
if (in_tape != null) and (base >= 0) {
|
||||
var i = 0
|
||||
while i < IN_WORDS { in_dev[i] = in_tape[base + i]; i = i + 1 }
|
||||
in_mx = in_tape[base + 8]
|
||||
in_my = in_tape[base + 9]
|
||||
in_mbtn = in_tape[base + 10]
|
||||
in_wheel = in_tape[base + 11]
|
||||
}
|
||||
in_set_copy(in_held, in_dev)
|
||||
} else {
|
||||
# live: fill the platform set from the window (real simultaneous keys) or,
|
||||
# headless, from the single polled key. Injection (in_sim) is OR-ed on top.
|
||||
if is_windowed() {
|
||||
win_held(in_dev)
|
||||
let mbuf = words(4) # [x, y, button-mask, wheel]
|
||||
win_mouse(mbuf)
|
||||
in_mx = mbuf[0]; in_my = mbuf[1]; in_mbtn = mbuf[2]; in_wheel = mbuf[3]
|
||||
} else {
|
||||
in_set_clear(in_dev)
|
||||
if k > 0 { in_bit_set(in_dev, k, true) }
|
||||
}
|
||||
in_set_or(in_held, in_dev, in_sim)
|
||||
if input_mode == 1 { input_device_record() } # snapshot the frame into the tape
|
||||
}
|
||||
|
||||
# mouse delta vs the previous frame's committed position (in_mx set by the
|
||||
# platform above when windowed, by Input.set_mouse before this poll otherwise).
|
||||
in_mdx = in_mx - in_mx0
|
||||
in_mdy = in_my - in_my0
|
||||
in_mx0 = in_mx
|
||||
in_my0 = in_my
|
||||
}
|
||||
|
||||
# write this frame's committed set + mouse into the tape at the record cursor.
|
||||
function input_device_record() -> void {
|
||||
if in_tape == null { in_tape = words(INPUT_REC_CAP * IN_STRIDE) }
|
||||
let f = input_recn - 1
|
||||
if (f < 0) or (f >= INPUT_REC_CAP) { return }
|
||||
let base = f * IN_STRIDE
|
||||
var i = 0
|
||||
while i < IN_WORDS { in_tape[base + i] = in_held[i]; i = i + 1 }
|
||||
in_tape[base + 8] = in_mx
|
||||
in_tape[base + 9] = in_my
|
||||
in_tape[base + 10] = in_mbtn
|
||||
in_tape[base + 11] = in_wheel
|
||||
}
|
||||
|
||||
# ---- held keys -------------------------------------------------------------
|
||||
function input_key_down(k: int) -> bool { in_init(); return in_bit_get(in_held, k) }
|
||||
function input_key_pressed(k: int) -> bool { in_init(); return in_bit_get(in_held, k) and (not in_bit_get(in_prev, k)) }
|
||||
function input_key_released(k: int) -> bool { in_init(); return (not in_bit_get(in_held, k)) and in_bit_get(in_prev, k) }
|
||||
|
||||
# inject a held key (AI, tutorial, testing, network) — persists until released.
|
||||
function input_press(k: int) -> void { in_init(); in_bit_set(in_sim, k, true) }
|
||||
function input_release(k: int) -> void { in_init(); in_bit_set(in_sim, k, false) }
|
||||
|
||||
# ---- analog from keys ------------------------------------------------------
|
||||
# A digital axis: +1.0 if the positive key is held, -1.0 if the negative, else 0.
|
||||
function input_axis(neg: int, pos: int) -> fixed {
|
||||
in_init()
|
||||
var v = fixed(0)
|
||||
if in_bit_get(in_held, pos) { v = v + fixed(1) }
|
||||
if in_bit_get(in_held, neg) { v = v - fixed(1) }
|
||||
return v
|
||||
}
|
||||
# A 2D vector from four direction keys, normalized so a diagonal is not faster.
|
||||
function input_vector(left: int, right: int, up: int, down: int) -> Vector {
|
||||
in_init()
|
||||
var x = fixed(0)
|
||||
var y = fixed(0)
|
||||
if in_bit_get(in_held, right) { x = x + fixed(1) }
|
||||
if in_bit_get(in_held, left) { x = x - fixed(1) }
|
||||
if in_bit_get(in_held, down) { y = y + fixed(1) }
|
||||
if in_bit_get(in_held, up) { y = y - fixed(1) }
|
||||
if (x != 0) and (y != 0) { # diagonal: scale by 1/sqrt(2)
|
||||
x = x * 0.7071 # fixed multiply (64-bit intermediate)
|
||||
y = y * 0.7071
|
||||
}
|
||||
return Vector.make(x, y)
|
||||
}
|
||||
# 0.0..1.0 strength of a named action (digital for keys: 1 if any bound key held).
|
||||
function input_strength(name: pointer) -> fixed {
|
||||
if input_down(name) { return fixed(1) }
|
||||
return fixed(0)
|
||||
}
|
||||
|
||||
# ---- mouse -----------------------------------------------------------------
|
||||
function input_mouse_x() -> int { in_init(); return in_mx }
|
||||
function input_mouse_y() -> int { in_init(); return in_my }
|
||||
function input_mouse_dx() -> int { in_init(); return in_mdx }
|
||||
function input_mouse_dy() -> int { in_init(); return in_mdy }
|
||||
function input_mouse_down(btn: int) -> bool { in_init(); return (in_mbtn & (1 << btn)) != 0 }
|
||||
function input_wheel() -> int { in_init(); return in_wheel }
|
||||
# inject the mouse (headless / AI / testing). buttons is a bitmask; wheel is this
|
||||
# frame's delta.
|
||||
function input_set_mouse(x: int, y: int, buttons: int, wheel: int) -> void {
|
||||
in_init()
|
||||
in_mx = x; in_my = y; in_mbtn = buttons; in_wheel = wheel
|
||||
}
|
||||
|
||||
# ---- gamepads --------------------------------------------------------------
|
||||
function input_pad_connected(pad: int) -> bool {
|
||||
in_init()
|
||||
if (pad < 0) or (pad >= IN_PADS) { return false }
|
||||
return in_pad_conn[pad] != 0
|
||||
}
|
||||
function input_pad_button(pad: int, btn: int) -> bool {
|
||||
in_init()
|
||||
if (pad < 0) or (pad >= IN_PADS) { return false }
|
||||
return (in_pad_btn[pad] & (1 << btn)) != 0
|
||||
}
|
||||
function input_pad_axis(pad: int, axis: int) -> fixed {
|
||||
in_init()
|
||||
if (pad < 0) or (pad >= IN_PADS) { return fixed(0) }
|
||||
if (axis < 0) or (axis >= IN_AXES) { return fixed(0) }
|
||||
return in_pad_axis[pad * IN_AXES + axis]
|
||||
}
|
||||
# inject a gamepad's whole state: connected, button bitmask, and four fixed axes.
|
||||
function input_set_pad(pad: int, connected: bool, buttons: int, lx: fixed, ly: fixed, rx: fixed, ry: fixed) -> void {
|
||||
in_init()
|
||||
if (pad < 0) or (pad >= IN_PADS) { return }
|
||||
var c = 0
|
||||
if connected { c = 1 }
|
||||
in_pad_conn[pad] = c
|
||||
in_pad_btn[pad] = buttons
|
||||
let b = pad * IN_AXES
|
||||
in_pad_axis[b] = lx
|
||||
in_pad_axis[b + 1] = ly
|
||||
in_pad_axis[b + 2] = rx
|
||||
in_pad_axis[b + 3] = ry
|
||||
}
|
||||
|
||||
# ---- touch -----------------------------------------------------------------
|
||||
function input_touch_count() -> int {
|
||||
in_init()
|
||||
var n = 0
|
||||
var i = 0
|
||||
while i < IN_TOUCH { if in_touch_on[i] != 0 { n = n + 1 }; i = i + 1 }
|
||||
return n
|
||||
}
|
||||
function input_touch_x(i: int) -> int { in_init(); if (i < 0) or (i >= IN_TOUCH) { return 0 }; return in_touch_x[i] }
|
||||
function input_touch_y(i: int) -> int { in_init(); if (i < 0) or (i >= IN_TOUCH) { return 0 }; return in_touch_y[i] }
|
||||
# inject a touch point i: active with a position, or inactive.
|
||||
function input_set_touch(i: int, x: int, y: int, active: bool) -> void {
|
||||
in_init()
|
||||
if (i < 0) or (i >= IN_TOUCH) { return }
|
||||
var a = 0
|
||||
if active { a = 1 }
|
||||
in_touch_on[i] = a
|
||||
in_touch_x[i] = x
|
||||
in_touch_y[i] = y
|
||||
}
|
||||
|
||||
# is the named action held on the frame last polled?
|
||||
function input_down(name: pointer) -> bool {
|
||||
return input_slot_has(input_find(name), input_frame)
|
||||
|
|
|
|||
|
|
@ -190,6 +190,31 @@ 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" }
|
||||
# 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") }
|
||||
if (meth == "key_pressed") { bare = "input_key_pressed"; push(labels, "key") }
|
||||
if (meth == "key_released") { bare = "input_key_released"; push(labels, "key") }
|
||||
if (meth == "press") { bare = "input_press"; push(labels, "key") }
|
||||
if (meth == "release") { bare = "input_release"; push(labels, "key") }
|
||||
if (meth == "axis") { bare = "input_axis"; push(labels, "neg"); push(labels, "pos") }
|
||||
if (meth == "vector") { bare = "input_vector"; push(labels, "left"); push(labels, "right"); push(labels, "up"); push(labels, "down") }
|
||||
if (meth == "strength") { bare = "input_strength"; push(labels, "action") }
|
||||
if (meth == "mouse_x") { bare = "input_mouse_x" }
|
||||
if (meth == "mouse_y") { bare = "input_mouse_y" }
|
||||
if (meth == "mouse_dx") { bare = "input_mouse_dx" }
|
||||
if (meth == "mouse_dy") { bare = "input_mouse_dy" }
|
||||
if (meth == "mouse_down") { bare = "input_mouse_down"; push(labels, "button") }
|
||||
if (meth == "wheel") { bare = "input_wheel" }
|
||||
if (meth == "set_mouse") { bare = "input_set_mouse"; push(labels, "x"); push(labels, "y"); push(labels, "buttons"); push(labels, "wheel") }
|
||||
if (meth == "pad_connected"){ bare = "input_pad_connected"; push(labels, "pad") }
|
||||
if (meth == "pad_button") { bare = "input_pad_button"; push(labels, "pad"); push(labels, "button") }
|
||||
if (meth == "pad_axis") { bare = "input_pad_axis"; push(labels, "pad"); push(labels, "axis") }
|
||||
if (meth == "set_pad") { bare = "input_set_pad"; push(labels, "pad"); push(labels, "connected"); push(labels, "buttons"); push(labels, "lx"); push(labels, "ly"); push(labels, "rx"); push(labels, "ry") }
|
||||
if (meth == "touch_count") { bare = "input_touch_count" }
|
||||
if (meth == "touch_x") { bare = "input_touch_x"; push(labels, "index") }
|
||||
if (meth == "touch_y") { bare = "input_touch_y"; push(labels, "index") }
|
||||
if (meth == "set_touch") { bare = "input_set_touch"; push(labels, "index"); push(labels, "x"); push(labels, "y"); push(labels, "active") }
|
||||
}
|
||||
# Phase 3: the bare reflection / networking / process builtins, namespaced.
|
||||
# Each is a pure alias — the callee is rewritten to the bare name below.
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ function emit_header() -> void {
|
|||
emith("declare void @win_present(ptr, i32, i32)\n")
|
||||
emith("declare i32 @win_running()\n")
|
||||
emith("declare void @win_close()\n")
|
||||
emith("declare void @win_held(ptr)\n")
|
||||
emith("declare void @win_mouse(ptr)\n")
|
||||
emith("@__stderrp = external global ptr\n")
|
||||
emith("@__stdoutp = external global ptr\n")
|
||||
emith("@.fmt_int = private unnamed_addr constant [4 x i8] c\"%d\\0A\\00\"\n")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ function is_intrinsic2(name: pointer) -> bool {
|
|||
if (name == "is_windowed") or (name == "game_title") { return true }
|
||||
if (name == "win_open") or (name == "win_poll") or (name == "win_present") { return true }
|
||||
if (name == "win_running") or (name == "win_close") { return true }
|
||||
if (name == "win_held") or (name == "win_mouse") { return true } # #50 device layer
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -49,5 +50,10 @@ function emit_intrinsic2(name: pointer, e: Node) -> Val {
|
|||
return val("0", "void")
|
||||
}
|
||||
if (name == "win_close") { emit(" call void @win_close()\n"); return val("0", "void") }
|
||||
# #50 — fill a caller buffer with the platform device state (windowed only,
|
||||
# DCE'd headless). win_held: an 8-word held-key bitset; win_mouse: [x, y,
|
||||
# button-mask, wheel-delta].
|
||||
if (name == "win_held") { let a = arg_code(e, 0); emit(" call void @win_held(ptr "); emit(a); emit(")\n"); return val("0", "void") }
|
||||
if (name == "win_mouse") { let a = arg_code(e, 0); emit(" call void @win_mouse(ptr "); emit(a); emit(")\n"); return val("0", "void") }
|
||||
return val("0", "void")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,6 +183,9 @@ function p_postfix() -> Node {
|
|||
# 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 }
|
||||
# #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 }
|
||||
# Anim.play/clip/on_frame/fired + Motion.to (#48): the ergonomic writes over
|
||||
# the SpriteAnim/Motion components live in systems.ludic and use the world
|
||||
# table, so splice it and force the reflection ABI even if the game leaves
|
||||
|
|
|
|||
43321
selfhost/ludicc.seed.ll
43321
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -255,6 +255,7 @@ function cmd_test() -> int {
|
|||
# #7: action maps (read named actions, not keys), runtime rebinding, and
|
||||
# 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("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
|
||||
net_case("events/scoped", "2")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue