feat(input): action maps + deterministic record/replay (#7)
The two ideas the input revamp leads with, built in Ludic over the single-key poll every target already provides: - Action maps: gameplay reads named actions, not physical keys, so keys are rebindable and a scheme is data. Input.bind(action, key), Input.down/pressed(action), Input.rebind(action, from, to). - Deterministic record/replay: Input.poll() is the one per-frame input read; Input.record() captures the key each frame and Input.replay() feeds the tape back, so a run reproduces exactly — the seed of lockstep netcode. "Read input" and "read a recorded snapshot" are the same call. runtime/native/input.ludic (spliced when the new Input.* methods are used; pulls in core.ludic for rt_poll). emit_ns_call routes the methods to the @fn_input_* runtime; parse.ludic gates the splice. Seven docs/language pages; worked example + regression examples/library/input_actions.ludic (1 0 1 1 0 1 0). Full suite 75 passed, self-host fixpoint intact, no golden drift. The device layer (multi-key held, gamepads, touch, analog) needs a platform key-state backend and is tracked separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3679ce1797
commit
377b6d1186
15 changed files with 19159 additions and 18515 deletions
12
LANGUAGE.md
12
LANGUAGE.md
|
|
@ -288,6 +288,18 @@ fields — so "Position + Light2D" and a self-positioned light both work. With
|
|||
`Light2D` present the engine owns the frame flip: a draw handler renders the
|
||||
scene and does **not** call `Screen.show`.
|
||||
|
||||
### Input actions & deterministic replay
|
||||
|
||||
Beyond the raw `Input.key()` (this frame's key code), gameplay can read **named
|
||||
actions** instead of physical keys, so a key is rebindable and a control scheme
|
||||
is data. `Input.bind(action, key)` binds a key; `Input.down(action)` /
|
||||
`Input.pressed(action)` read it (held vs one-shot edge); `Input.rebind(action,
|
||||
from, to)` remaps it at runtime. `Input.poll()` is the single per-frame input
|
||||
read the actions sit on — which is what makes **deterministic replay** fall out:
|
||||
`Input.record()` captures the polled key each frame and `Input.replay()` feeds
|
||||
the tape back, so a run reproduces exactly (the seed of lockstep netcode). All
|
||||
integer and deterministic. See `examples/library/input_actions.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`.
|
||||
|
|
|
|||
3
changes/input-actions.md
Normal file
3
changes/input-actions.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
Input action maps + deterministic record/replay (#7) — gameplay now reads named actions instead of physical keys, so keys are rebindable and a control scheme is data. `Input.bind(action, key)` binds a key to an action, `Input.down`/`Input.pressed` read it (held vs one-shot edge), and `Input.rebind(action, from, to)` remaps it at runtime for an options screen. `Input.poll` is the single per-frame input read, which makes deterministic replay fall out for free: `Input.record` captures the polled key each frame and `Input.replay` feeds the tape back so a run reproduces exactly — the seed of lockstep netcode. All integer and deterministic over the single-key poll every target provides; the multi-key/gamepad/touch/analog device layer is tracked as a follow-up.
|
||||
25
docs/language/input/input-bind.md
Normal file
25
docs/language/input/input-bind.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
id: input-bind
|
||||
name: Input.bind
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.bind
|
||||
sig: Input.bind(action: str, key: int) -> void
|
||||
tip: Bind a physical key to a named action, so gameplay reads the action, not the key.
|
||||
order: 1
|
||||
ns: Input
|
||||
member: bind
|
||||
---
|
||||
|
||||
Binds a physical key (a character code such as <code>'w'</code> or <code>' '</code>) to a named <em>action</em>, creating the action the first time it is named. Gameplay then reads the action with <a href="input-down.html"><code>Input.down</code></a> / <a href="input-pressed.html"><code>Input.pressed</code></a> instead of a raw key, which is what makes rebinding and alternate control schemes clean. Call it more than once with the same action to bind several keys to it; binding a key already on the action is a no-op. Actions are advanced once per frame by <a href="input-poll.html"><code>Input.poll</code></a>.
|
||||
|
||||
```ludic
|
||||
program Bindings {
|
||||
entry {
|
||||
Input.bind("jump", ' ')
|
||||
Input.bind("up", 'w')
|
||||
Input.poll()
|
||||
if Input.down("jump") { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/input/input-down.md
Normal file
24
docs/language/input/input-down.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-down
|
||||
name: Input.down
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.down
|
||||
sig: Input.down(action: str) -> bool
|
||||
tip: Is a named action held on the frame last polled?
|
||||
order: 4
|
||||
ns: Input
|
||||
member: down
|
||||
---
|
||||
|
||||
Returns whether a named action is <em>held</em> on the frame last read by <a href="input-poll.html"><code>Input.poll</code></a> — true when the polled key is one of the keys bound to the action. Use it for continuous input (move while held); for a one-shot press use <a href="input-pressed.html"><code>Input.pressed</code></a>. Reading an action rather than a key is what lets the same handler serve any binding.
|
||||
|
||||
```ludic
|
||||
program Held {
|
||||
entry {
|
||||
Input.bind("up", 'w')
|
||||
Input.poll()
|
||||
if Input.down("up") { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/input/input-poll.md
Normal file
24
docs/language/input/input-poll.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-poll
|
||||
name: Input.poll
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.poll
|
||||
sig: Input.poll() -> int
|
||||
tip: Advance one frame of input; the single per-frame read behind actions and replay.
|
||||
order: 3
|
||||
ns: Input
|
||||
member: poll
|
||||
---
|
||||
|
||||
Advances the input by one frame and returns the frame's key. Call it once at the top of a frame; the action reads (<a href="input-down.html"><code>Input.down</code></a>, <a href="input-pressed.html"><code>Input.pressed</code></a>) then report on the key it captured. It is the single place input crosses into the frame, which is what makes recording and replay possible: in record mode it saves the live key, and in replay mode it takes the next key from the tape instead of the device — so "read input" and "read a recorded snapshot" are the same call.
|
||||
|
||||
```ludic
|
||||
program Poll {
|
||||
entry {
|
||||
Input.bind("up", 'w')
|
||||
Input.poll()
|
||||
if Input.down("up") { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/input/input-pressed.md
Normal file
24
docs/language/input/input-pressed.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-pressed
|
||||
name: Input.pressed
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.pressed
|
||||
sig: Input.pressed(action: str) -> bool
|
||||
tip: Did a named action go down this frame (a one-shot edge)?
|
||||
order: 5
|
||||
ns: Input
|
||||
member: pressed
|
||||
---
|
||||
|
||||
Returns whether a named action went down <em>this</em> frame — held on the frame last polled, but not on the one before — the edge you want for "press to jump / confirm / fire", where <a href="input-down.html"><code>Input.down</code></a> would retrigger every frame the key is held. Depends on the two most recent <a href="input-poll.html"><code>Input.poll</code></a> calls, so poll once per frame.
|
||||
|
||||
```ludic
|
||||
program Edge {
|
||||
entry {
|
||||
Input.bind("jump", ' ')
|
||||
Input.poll()
|
||||
if Input.pressed("jump") { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
25
docs/language/input/input-rebind.md
Normal file
25
docs/language/input/input-rebind.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
id: input-rebind
|
||||
name: Input.rebind
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.rebind
|
||||
sig: Input.rebind(action: str, from: int, to: int) -> void
|
||||
tip: Remap an action from one key to another at runtime (rebinding menus).
|
||||
order: 2
|
||||
ns: Input
|
||||
member: rebind
|
||||
---
|
||||
|
||||
Replaces the key <code>from</code> with <code>to</code> on a named action, at runtime — the primitive a "press a key to rebind" options screen is built on. A no-op if the action does not exist or is not bound to <code>from</code>. Because gameplay reads actions, not keys, a rebind takes effect immediately with no change to the game logic.
|
||||
|
||||
```ludic
|
||||
program Rebinding {
|
||||
entry {
|
||||
Input.bind("jump", ' ')
|
||||
Input.rebind("jump", ' ', 'x')
|
||||
Input.poll()
|
||||
if Input.down("jump") { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
25
docs/language/input/input-record.md
Normal file
25
docs/language/input/input-record.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
id: input-record
|
||||
name: Input.record
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.record
|
||||
sig: Input.record() -> void
|
||||
tip: Start recording polled input each frame (for deterministic replay).
|
||||
order: 6
|
||||
ns: Input
|
||||
member: record
|
||||
---
|
||||
|
||||
Puts input into <em>record</em> mode and resets the tape: from now on each <a href="input-poll.html"><code>Input.poll</code></a> reads the live key and appends it to a recording. Because the simulation is deterministic in its input stream, that tape is all you need to reproduce a run — play it back with <a href="input-replay.html"><code>Input.replay</code></a> for a free exact replay, the seed of lockstep netcode. Recording captures input only; it does not change what the game sees this frame.
|
||||
|
||||
```ludic
|
||||
program Recording {
|
||||
entry {
|
||||
Input.bind("up", 'w')
|
||||
Input.record()
|
||||
Input.poll()
|
||||
if Input.down("up") { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
27
docs/language/input/input-replay.md
Normal file
27
docs/language/input/input-replay.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
id: input-replay
|
||||
name: Input.replay
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.replay
|
||||
sig: Input.replay() -> void
|
||||
tip: Replay recorded input; poll then reads the tape, not the device.
|
||||
order: 7
|
||||
ns: Input
|
||||
member: replay
|
||||
---
|
||||
|
||||
Rewinds the recording made by <a href="input-record.html"><code>Input.record</code></a> and switches to <em>replay</em> mode: each subsequent <a href="input-poll.html"><code>Input.poll</code></a> returns the next key from the tape instead of the live device, so the recorded session runs again exactly — deterministic replays, demo playback, and the basis of rollback netcode. Past the end of the tape, poll reports no input.
|
||||
|
||||
```ludic
|
||||
program Replaying {
|
||||
entry {
|
||||
Input.bind("up", 'w')
|
||||
Input.record()
|
||||
Input.poll()
|
||||
Input.replay()
|
||||
Input.poll()
|
||||
if Input.down("up") { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
37
examples/library/input_actions.ludic
Normal file
37
examples/library/input_actions.ludic
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# input_actions.ludic — action maps, runtime rebinding, and deterministic input
|
||||
# record/replay (#7). Gameplay reads named actions, not physical keys, so a key
|
||||
# is rebindable; and because the sim is deterministic in its input stream, a
|
||||
# recorded run replays exactly. Driven headless off stdin (one key per poll):
|
||||
#
|
||||
# printf ' xwa' | bin/ludic examples/library/input_actions.ludic -> 1 0 1 1 0 1 0
|
||||
program InputActions {
|
||||
function bi(b: bool) -> int { if b { return 1 }; return 0 }
|
||||
|
||||
entry {
|
||||
Input.bind("jump", ' ') # space triggers "jump"
|
||||
Input.bind("up", 'w') # w triggers "up"
|
||||
|
||||
# --- action map + runtime rebinding (live) ---
|
||||
Input.poll() # stdin: ' '
|
||||
print(bi(Input.down("jump"))) # 1 — space is bound to jump
|
||||
|
||||
Input.rebind("jump", ' ', 'x') # remap jump from space to x at runtime
|
||||
Input.poll() # stdin: ' '
|
||||
print(bi(Input.down("jump"))) # 0 — space no longer fires jump
|
||||
Input.poll() # stdin: 'x'
|
||||
print(bi(Input.down("jump"))) # 1 — x now fires jump
|
||||
|
||||
# --- deterministic record / replay ---
|
||||
Input.record() # capture the polled keys from here
|
||||
Input.poll() # stdin: 'w'
|
||||
print(bi(Input.down("up"))) # 1
|
||||
Input.poll() # stdin: 'a'
|
||||
print(bi(Input.down("up"))) # 0
|
||||
|
||||
Input.replay() # rewind; poll now reads the tape, not stdin
|
||||
Input.poll()
|
||||
print(bi(Input.down("up"))) # 1 — reproduces the recorded frame exactly
|
||||
Input.poll()
|
||||
print(bi(Input.down("up"))) # 0
|
||||
}
|
||||
}
|
||||
155
runtime/native/input.ludic
Normal file
155
runtime/native/input.ludic
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# ============================================================================
|
||||
# input.ludic — action maps + deterministic input recording/replay (#7).
|
||||
#
|
||||
# The raw platform gives one key per frame (Input.key / rt_poll). This layer
|
||||
# adds the two ideas the input proposal leads with:
|
||||
#
|
||||
# * Action maps — gameplay reads *named actions*, not physical keys, so a key
|
||||
# is rebindable at runtime and a scheme is data. Bind with Input.bind, read
|
||||
# with Input.down / Input.pressed, remap with Input.rebind.
|
||||
# * Deterministic record/replay — because the sim is deterministic in its input
|
||||
# stream, snapshotting the per-frame key and feeding it back reproduces a run
|
||||
# exactly (free replays, the seed of lockstep netcode). Input.poll is the one
|
||||
# call that advances a frame of input; it reads the live key, records it, or
|
||||
# replays a recorded one depending on the mode — "read input" and "read a
|
||||
# recorded snapshot" are the same call, as the proposal asks.
|
||||
#
|
||||
# All integer and deterministic. The device layer the proposal also sketches —
|
||||
# multiple simultaneous keys, gamepads, touch, analog axes/vectors — needs a
|
||||
# platform key-state backend and is tracked separately; this layer stands on the
|
||||
# single-key poll every target already provides.
|
||||
# ============================================================================
|
||||
|
||||
const INPUT_MAX_ACT: int = 32 # named actions
|
||||
const INPUT_MAX_KEYS: int = 4 # physical keys bound per action
|
||||
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_nact: int = 0
|
||||
|
||||
var input_frame: int = 0 # the key polled this frame
|
||||
var input_last: int = 0 # the key polled last frame (for edges)
|
||||
|
||||
var input_mode: int = 0 # 0 = live, 1 = record, 2 = replay
|
||||
var input_rec: words = null # recorded key per frame
|
||||
var input_recn: int = 0 # frames recorded
|
||||
var input_pos: int = 0 # replay / record cursor
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
# slot of the action `name`, or -1. Names compare by byte-string equality.
|
||||
function input_find(name: pointer) -> int {
|
||||
input_init()
|
||||
var i = 0
|
||||
while i < input_nact {
|
||||
if input_names[i] == name { return i }
|
||||
i = i + 1
|
||||
}
|
||||
return 0 - 1
|
||||
}
|
||||
|
||||
# get-or-create the slot for `name`.
|
||||
function input_slot(name: pointer) -> int {
|
||||
let f = input_find(name)
|
||||
if f >= 0 { return f }
|
||||
if input_nact >= INPUT_MAX_ACT { return INPUT_MAX_ACT - 1 } # silently reuse the last slot when full
|
||||
let s = input_nact
|
||||
input_names[s] = name
|
||||
input_nact = input_nact + 1
|
||||
return s
|
||||
}
|
||||
|
||||
# bind physical `key` to the named action, creating the action if new. A key
|
||||
# already bound to the action is left as-is (idempotent).
|
||||
function input_bind(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] == key { return } # already bound
|
||||
i = i + 1
|
||||
}
|
||||
i = 0
|
||||
while i < INPUT_MAX_KEYS {
|
||||
if input_keys[base + i] == 0 { input_keys[base + i] = key; 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 {
|
||||
let s = input_find(name)
|
||||
if s < 0 { return }
|
||||
let base = s * INPUT_MAX_KEYS
|
||||
var i = 0
|
||||
while i < INPUT_MAX_KEYS {
|
||||
if input_keys[base + i] == oldkey { input_keys[base + i] = newkey; return }
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
# does key `k` (0 = none) fire the action in slot `s`?
|
||||
function input_slot_has(s: int, k: int) -> bool {
|
||||
if s < 0 { return false }
|
||||
if k == 0 { return false }
|
||||
let base = s * INPUT_MAX_KEYS
|
||||
var i = 0
|
||||
while i < INPUT_MAX_KEYS {
|
||||
if input_keys[base + i] == k { return true }
|
||||
i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
# Advance one frame of input and return the frame's key. This is the single
|
||||
# per-frame input read: call it once at the top of a frame.
|
||||
# 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).
|
||||
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
|
||||
}
|
||||
let k = rt_poll()
|
||||
if input_mode == 1 { # record
|
||||
if input_rec == null { input_rec = words(INPUT_REC_CAP) }
|
||||
if input_recn < INPUT_REC_CAP { input_rec[input_recn] = k; input_recn = input_recn + 1 }
|
||||
}
|
||||
input_frame = k
|
||||
return k
|
||||
}
|
||||
|
||||
# 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)
|
||||
}
|
||||
|
||||
# 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)
|
||||
return input_slot_has(s, input_frame) and (not input_slot_has(s, input_last))
|
||||
}
|
||||
|
||||
# begin recording polled input from the next frame (resets the tape).
|
||||
function input_record() -> void {
|
||||
if input_rec == null { input_rec = words(INPUT_REC_CAP) }
|
||||
input_recn = 0
|
||||
input_pos = 0
|
||||
input_mode = 1
|
||||
}
|
||||
|
||||
# replay the recording from its start; subsequent Input.poll calls read the tape.
|
||||
function input_replay() -> void {
|
||||
input_pos = 0
|
||||
input_mode = 2
|
||||
}
|
||||
|
|
@ -177,6 +177,15 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
}
|
||||
if (ns == "Input") {
|
||||
if (meth == "key") { bare = "key" }
|
||||
# action maps + deterministic record/replay (#7) — spliced runtime in
|
||||
# runtime/native/input.ludic, reached as ordinary @fn_input_* calls.
|
||||
if (meth == "bind") { bare = "input_bind" }
|
||||
if (meth == "rebind") { bare = "input_rebind" }
|
||||
if (meth == "poll") { bare = "input_poll" }
|
||||
if (meth == "down") { bare = "input_down" }
|
||||
if (meth == "pressed") { bare = "input_pressed" }
|
||||
if (meth == "record") { bare = "input_record" }
|
||||
if (meth == "replay") { bare = "input_replay" }
|
||||
}
|
||||
# Phase 3: the bare reflection / networking / process builtins, namespaced.
|
||||
# Each is a pure alias — the callee is rewritten to the bare name below.
|
||||
|
|
|
|||
|
|
@ -180,6 +180,9 @@ function p_postfix() -> Node {
|
|||
if e.a.kind == E_ID and e.a.s == "Light" { g_uses_light = true } # splice the 2D light-accumulation pass on demand
|
||||
if e.a.kind == E_ID and (e.a.s == "Value" or e.a.s == "Json") { g_uses_value = true } # splice the value tree + JSON on demand (#44)
|
||||
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 }
|
||||
}
|
||||
else { if is_op("[") { pi = pi + 1; let lo = expr()
|
||||
if is_op("..") { pi = pi + 1; let sl = node(E_SLICE); sl.a = e; sl.b = lo; sl.c = expr(); eat_op("]"); e = sl } # s[a..b] substring
|
||||
|
|
@ -408,6 +411,7 @@ var g_uses_light: bool = false # a program mentioned Light.* -> splice the 2D
|
|||
var g_uses_value: bool = false # Value.*/Json.*/Reflect.serialize -> splice the value tree + JSON (#44)
|
||||
var g_uses_reflect_io: bool = false # Reflect.serialize/apply -> splice the reflection serializer
|
||||
var g_uses_esys: bool = false # an engine-owned system component (SpriteAnim/Motion/Light2D) is declared -> splice systems.ludic + force the reflection ABI
|
||||
var g_uses_input: bool = false # a program used Input.bind/down/poll/… (action maps + record/replay) -> splice input.ludic
|
||||
|
||||
function already_loaded(full: pointer) -> bool {
|
||||
var i = 0
|
||||
|
|
@ -610,6 +614,15 @@ function maybe_splice_runtime() -> void {
|
|||
# (emit_engine_systems_for_phase). The systems read/write components through the
|
||||
# reflection ABI, so g_uses_esys also force-emits the world table (emit_decl).
|
||||
# Light2D/Occluder additionally consume the 2D light pass, so pull it in too.
|
||||
# Input.* action maps + record/replay (#7): splice input.ludic. It reads the
|
||||
# live key through rt_poll (core.ludic), so pull the runtime in even for a
|
||||
# program with no ECS (do_import dedupes when a game already linked core).
|
||||
if g_uses_input {
|
||||
cur_dir = ""
|
||||
do_import("runtime/native/core.ludic")
|
||||
do_import("runtime/native/input.ludic")
|
||||
cur_dir = saved
|
||||
}
|
||||
if uses_engine_systems() {
|
||||
g_uses_esys = true
|
||||
cur_dir = ""
|
||||
|
|
@ -648,6 +661,7 @@ function parse_program() -> void {
|
|||
g_uses_query = false
|
||||
g_uses_reflect = false
|
||||
g_uses_esys = false
|
||||
g_uses_input = false
|
||||
g_uses_light = false
|
||||
g_uses_value = false
|
||||
g_uses_reflect_io = false
|
||||
|
|
|
|||
37267
selfhost/ludicc.seed.ll
37267
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -250,6 +250,9 @@ function cmd_test() -> int {
|
|||
# the end of the Render phase — ambient tint, additive glow, hard shadow — with
|
||||
# no Light.* calls wired. Sampled back off the framebuffer with Screen.pixel.
|
||||
net_case("library/light_ecs", "32 1 32 1")
|
||||
# #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("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