nx_input_abstract.nx source
↩ module page · 205 lines · 8946 B
1// nx_input_abstract.nx -- SOVEREIGN device-agnostic input core (F1102, game-platform lane).
2// ONE semantic action state (up/down/left/right/A/B/menu/pause) driven by FOUR sources -- keyboard,
3// gamepad buttons, gamepad axes, touch -- so a game loop reads ACTIONS, never devices. This moves the
4// key->meaning mapping OUT of the JS shim and INTO the sovereign organ (the shim stays pure transport),
5// and it is the enabler for platform-desktop-native (F1105) and vr-runtime (F1106): a VR controller
6// surfaces as gamepad buttons/axes (WebXR gamepad), so it enters through the SAME ia_pad/ia_axis doors.
7//
8// Base-relative + integer-only + no syscalls CALLED (imports only for consts/compose) => the identical
9// code runs natively (base=mmap) and in wasm (base=0), like nx_wasm_breeders.
10//
11// Per-source held masks are kept SEPARATE and unioned on read, so releasing a key can never eat a
12// gamepad hold (the classic cross-device release bug -- gate tooth T4). Edge detection lives here too:
13// ia_frame() returns the RISING edge, so "interact fires every frame while held" cannot recur (T2).
14// Bindings are DATA rows (code,action) seeded by ia_init and rewritable at runtime via ia_bind (T6).
15// Touch composes the EXISTING nx_touch_zone SSOT (virtual d-pad -> legacy keycode) then routes the
16// code through the SAME binding table, so touch inherits rebinds for free.
17// license_tier: ORIGINAL
18import "nx_syscalls.nx"
19import "_hdl_build/nx_game_touch.nx"
20
21// semantic actions (bitmask). 16-action space since 2026-07-28 (debt seq1136: the first 3D consumer
22// saturated 8 bits) -- the original 8 values are FROZEN; new actions extend upward only.
23const IA_UP: i64 = 1
24const IA_DOWN: i64 = 2
25const IA_LEFT: i64 = 4
26const IA_RIGHT: i64 = 8
27const IA_A: i64 = 16
28const IA_B: i64 = 32
29const IA_MENU: i64 = 64
30const IA_PAUSE: i64 = 128
31const IA_JUMP: i64 = 256
32const IA_LUP: i64 = 512 // look up (keyboard pitch fallback)
33const IA_LDOWN: i64 = 1024 // look down
34const IA_MOD: i64 = 2048 // spare modifier
35const IA_HOME: i64 = 4096 // return-to-spawn (void-fall rescue)
36
37// arena slot layout (i64 words; caller provides a zeroed region of IA_WORDS words)
38const IA_KB: i64 = 0 // keyboard-held action mask
39const IA_PAD: i64 = 1 // gamepad-button-held action mask
40const IA_AX: i64 = 2 // gamepad-axis-derived action mask (hysteresis state)
41const IA_TOUCH: i64 = 3 // touch-held action mask
42const IA_PREV: i64 = 4 // union mask latched by the previous ia_frame (edge detector)
43const IA_AXX: i64 = 5 // last raw x axis, milli (-1000..1000)
44const IA_AXY: i64 = 6 // last raw y axis, milli (+ = down, browser gamepad convention)
45const IA_NBIND: i64 = 7 // live binding rows
46const IA_BROW0: i64 = 8 // rows begin: (code, action) pairs
47const IA_MAXBIND: i64 = 24
48const IA_LOOKX: i64 = 56 // analog LOOK channel: accumulated relative deltas (mouse/touch-drag),
49const IA_LOOKY: i64 = 57 // drained once per frame by ia_look_take -- actions are bits, look is motion
50const IA_LOOK_CLAMP: i64 = 30000
51const IA_WORDS: i64 = 64 // 8 + 2*24 = 56 bind area, +2 look, rounded up for slack
52
53// axis hysteresis (milli): assert above PRESS, keep until below HOLD -- no flicker at one boundary
54const IA_TH_PRESS: i64 = 350
55const IA_TH_HOLD: i64 = 250
56
57func ia_words() -> i64 { return IA_WORDS }
58
59// code -> action bit; 0 = unbound. rows with action 0 are dead (unbind leaves a dead row).
60func ia_lookup(p: *i64, code: i64) -> i64 {
61 var i: i64 = 0
62 var hit: i64 = 0
63 while i < p[IA_NBIND] {
64 if p[IA_BROW0 + i*2] == code { if p[IA_BROW0 + i*2 + 1] != 0 { hit = p[IA_BROW0 + i*2 + 1] } }
65 i = i + 1
66 }
67 return hit
68}
69
70// upsert a binding row; act=0 unbinds. returns 1 on success, 0 when the table is full.
71func ia_bind(p: *i64, code: i64, act: i64) -> i64 {
72 var i: i64 = 0
73 var done: i64 = 0
74 while i < p[IA_NBIND] {
75 if p[IA_BROW0 + i*2] == code { if done == 0 { p[IA_BROW0 + i*2 + 1] = act; done = 1 } }
76 i = i + 1
77 }
78 if done == 1 { return 1 }
79 if p[IA_NBIND] >= IA_MAXBIND { return 0 }
80 p[IA_BROW0 + p[IA_NBIND]*2] = code
81 p[IA_BROW0 + p[IA_NBIND]*2 + 1] = act
82 p[IA_NBIND] = p[IA_NBIND] + 1
83 return 1
84}
85
86// zero the arena and seed the default keyboard map: WASD + arrows + E/space(A) + B + enter/P.
87// the arrow/space codes are nx_game_touch's NX_KEY_* consts, so touch zones land on live rows.
88func ia_init(p: *i64) -> i64 {
89 var k: i64 = 0
90 while k < IA_WORDS { p[k] = 0; k = k + 1 }
91 ia_bind(p, 87, IA_UP) // W
92 ia_bind(p, 65, IA_LEFT) // A
93 ia_bind(p, 83, IA_DOWN) // S
94 ia_bind(p, 68, IA_RIGHT) // D
95 ia_bind(p, NX_KEY_UP, IA_UP) // arrows (38/37/40/39)
96 ia_bind(p, NX_KEY_LEFT, IA_LEFT)
97 ia_bind(p, NX_KEY_DOWN, IA_DOWN)
98 ia_bind(p, NX_KEY_RIGHT, IA_RIGHT)
99 ia_bind(p, 69, IA_A) // E interact
100 ia_bind(p, NX_KEY_SPACE, IA_A) // space = touch center zone
101 ia_bind(p, 66, IA_B) // B secondary
102 ia_bind(p, 13, IA_MENU) // enter
103 ia_bind(p, 80, IA_PAUSE) // P
104 return 0
105}
106
107// keyboard event: code through the binding table into the kb mask. unbound codes are no-ops.
108func ia_key(p: *i64, code: i64, down: i64) -> i64 {
109 let bit: i64 = ia_lookup(p, code)
110 if bit == 0 { return 0 }
111 if down != 0 { p[IA_KB] = p[IA_KB] | bit }
112 if down == 0 { p[IA_KB] = p[IA_KB] - (p[IA_KB] & bit) }
113 return bit
114}
115
116// W3C standard-gamepad button index -> action (12/13/14/15 = dpad, 0=A, 1=B, 9=start, 8=select).
117// this is the published gamepad SPEC, not tunable config, so it is a fixed decode.
118func ia_pad_decode(btn: i64) -> i64 {
119 if btn == 12 { return IA_UP }
120 if btn == 13 { return IA_DOWN }
121 if btn == 14 { return IA_LEFT }
122 if btn == 15 { return IA_RIGHT }
123 if btn == 0 { return IA_A }
124 if btn == 1 { return IA_B }
125 if btn == 9 { return IA_MENU }
126 if btn == 8 { return IA_PAUSE }
127 return 0
128}
129
130func ia_pad(p: *i64, btn: i64, down: i64) -> i64 {
131 let bit: i64 = ia_pad_decode(btn)
132 if bit == 0 { return 0 }
133 if down != 0 { p[IA_PAD] = p[IA_PAD] | bit }
134 if down == 0 { p[IA_PAD] = p[IA_PAD] - (p[IA_PAD] & bit) }
135 return bit
136}
137
138// one direction of one axis with hysteresis: v = signed milli TOWARD this direction.
139func ia_ax1(active: i64, v: i64) -> i64 {
140 if active == 0 { if v >= IA_TH_PRESS { return 1 } return 0 }
141 if v >= IA_TH_HOLD { return 1 }
142 return 0
143}
144
145// gamepad axis sample: idx 0 = x (+right), idx 1 = y (+down), value in milli. recomputes the
146// whole axis mask from the stored raw pair so each direction keeps its own hysteresis state.
147func ia_axis(p: *i64, idx: i64, milli: i64) -> i64 {
148 if idx == 0 { p[IA_AXX] = milli }
149 if idx == 1 { p[IA_AXY] = milli }
150 let m: i64 = p[IA_AX]
151 var nm: i64 = 0
152 if ia_ax1(m & IA_RIGHT, p[IA_AXX]) == 1 { nm = nm | IA_RIGHT }
153 if ia_ax1(m & IA_LEFT, 0 - p[IA_AXX]) == 1 { nm = nm | IA_LEFT }
154 if ia_ax1(m & IA_DOWN, p[IA_AXY]) == 1 { nm = nm | IA_DOWN }
155 if ia_ax1(m & IA_UP, 0 - p[IA_AXY]) == 1 { nm = nm | IA_UP }
156 p[IA_AX] = nm
157 return nm
158}
159
160// touch: virtual-d-pad zone (nx_touch_zone SSOT) -> legacy keycode -> binding table -> action.
161// single-touch rung: the mask IS the current touch's action; up clears it.
162func ia_touch(p: *i64, tx: i64, ty: i64, w: i64, h: i64, down: i64) -> i64 {
163 if down == 0 { p[IA_TOUCH] = 0; return 0 }
164 let code: i64 = nx_touch_zone(tx, ty, w, h)
165 p[IA_TOUCH] = ia_lookup(p, code)
166 return p[IA_TOUCH]
167}
168
169// union of every source -- the mask a game loop reads for HELD movement.
170func ia_held(p: *i64) -> i64 {
171 return p[IA_KB] | p[IA_PAD] | p[IA_AX] | p[IA_TOUCH]
172}
173
174// frame boundary: returns the RISING-edge mask (newly pressed since the last call) and latches.
175// call once per game tick; use the result for one-shot actions (interact/menu), ia_held for movement.
176func ia_frame(p: *i64) -> i64 {
177 let held: i64 = ia_held(p)
178 let pressed: i64 = held - (held & p[IA_PREV])
179 p[IA_PREV] = held
180 return pressed
181}
182
183// analog look: accumulate a relative delta (mouse movement, touch drag). Clamped so a stall
184// between frames can never bank an unbounded spin.
185func ia_look(p: *i64, dx: i64, dy: i64) -> i64 {
186 var nx2: i64 = p[IA_LOOKX] + dx
187 if nx2 > IA_LOOK_CLAMP { nx2 = IA_LOOK_CLAMP }
188 if nx2 < 0 - IA_LOOK_CLAMP { nx2 = 0 - IA_LOOK_CLAMP }
189 p[IA_LOOKX] = nx2
190 var ny2: i64 = p[IA_LOOKY] + dy
191 if ny2 > IA_LOOK_CLAMP { ny2 = IA_LOOK_CLAMP }
192 if ny2 < 0 - IA_LOOK_CLAMP { ny2 = 0 - IA_LOOK_CLAMP }
193 p[IA_LOOKY] = ny2
194 return 0
195}
196
197// drain the accumulated look delta (once per game tick): out[0]=dx out[1]=dy, then zeroed --
198// the same delta can never be applied twice.
199func ia_look_take(p: *i64, out: *i64) -> i64 {
200 out[0] = p[IA_LOOKX]
201 out[1] = p[IA_LOOKY]
202 p[IA_LOOKX] = 0
203 p[IA_LOOKY] = 0
204 return 0
205}