code wiki / (root) / nx_input_abstract.nx

nx_input_abstract.nx source

↩ module page · 235 lines · 9983 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_KEYROWS: i64 = 58 // physical keyboard holds by binding row; uses reserved arena slack 51const IA_LOOK_CLAMP: i64 = 30000 52const IA_WORDS: i64 = 64 // 8 + 2*24 = 56 bind area, +2 look, rounded up for slack 53 54// axis hysteresis (milli): assert above PRESS, keep until below HOLD -- no flicker at one boundary 55const IA_TH_PRESS: i64 = 350 56const IA_TH_HOLD: i64 = 250 57 58func ia_words() -> i64 { return IA_WORDS } 59 60// code -> action bit; 0 = unbound. rows with action 0 are dead (unbind leaves a dead row). 61func ia_lookup(p: *i64, code: i64) -> i64 { 62 var i: i64 = 0 63 var hit: i64 = 0 64 while i < p[IA_NBIND] { 65 if p[IA_BROW0 + i*2] == code { if p[IA_BROW0 + i*2 + 1] != 0 { hit = p[IA_BROW0 + i*2 + 1] } } 66 i = i + 1 67 } 68 return hit 69} 70 71// Recompute from physical holds: releasing one alias must preserve the other. 72func ia_keyboard_union(p: *i64) -> i64 { 73 var mask: i64 = 0 74 var i: i64 = 0 75 while i < p[IA_NBIND] { 76 if (p[IA_KEYROWS] & (1 << i)) != 0 { mask = mask | p[IA_BROW0 + i*2 + 1] } 77 i = i + 1 78 } 79 p[IA_KB] = mask 80 return mask 81} 82 83// upsert a binding row; act=0 unbinds. returns 1 on success, 0 when the table is full. 84func ia_bind(p: *i64, code: i64, act: i64) -> i64 { 85 var i: i64 = 0 86 var done: i64 = 0 87 while i < p[IA_NBIND] { 88 if p[IA_BROW0 + i*2] == code { if done == 0 { 89 if p[IA_BROW0 + i*2 + 1] != act { 90 // A remap clears this physical hold, avoiding a synthetic action under the new binding. 91 let rowbit: i64 = 1 << i 92 p[IA_KEYROWS] = p[IA_KEYROWS] - (p[IA_KEYROWS] & rowbit) 93 p[IA_BROW0 + i*2 + 1] = act 94 ia_keyboard_union(p) 95 } 96 done = 1 97 } } 98 i = i + 1 99 } 100 if done == 1 { return 1 } 101 if p[IA_NBIND] >= IA_MAXBIND { return 0 } 102 p[IA_BROW0 + p[IA_NBIND]*2] = code 103 p[IA_BROW0 + p[IA_NBIND]*2 + 1] = act 104 p[IA_NBIND] = p[IA_NBIND] + 1 105 return 1 106} 107 108// zero the arena and seed the default keyboard map: WASD + arrows + E/space(A) + B + enter/P. 109// the arrow/space codes are nx_game_touch's NX_KEY_* consts, so touch zones land on live rows. 110func ia_init(p: *i64) -> i64 { 111 var k: i64 = 0 112 while k < IA_WORDS { p[k] = 0; k = k + 1 } 113 ia_bind(p, 87, IA_UP) // W 114 ia_bind(p, 65, IA_LEFT) // A 115 ia_bind(p, 83, IA_DOWN) // S 116 ia_bind(p, 68, IA_RIGHT) // D 117 ia_bind(p, NX_KEY_UP, IA_UP) // arrows (38/37/40/39) 118 ia_bind(p, NX_KEY_LEFT, IA_LEFT) 119 ia_bind(p, NX_KEY_DOWN, IA_DOWN) 120 ia_bind(p, NX_KEY_RIGHT, IA_RIGHT) 121 ia_bind(p, 69, IA_A) // E interact 122 ia_bind(p, NX_KEY_SPACE, IA_A) // space = touch center zone 123 ia_bind(p, 66, IA_B) // B secondary 124 ia_bind(p, 13, IA_MENU) // enter 125 ia_bind(p, 80, IA_PAUSE) // P 126 return 0 127} 128 129// keyboard event: code through the binding table into the kb mask. unbound codes are no-ops. 130func ia_key(p: *i64, code: i64, down: i64) -> i64 { 131 var i: i64 = 0 132 while i < p[IA_NBIND] { 133 if p[IA_BROW0 + i*2] == code { 134 let action: i64 = p[IA_BROW0 + i*2 + 1] 135 let rowbit: i64 = 1 << i 136 if down != 0 && action != 0 { p[IA_KEYROWS] = p[IA_KEYROWS] | rowbit } 137 else { p[IA_KEYROWS] = p[IA_KEYROWS] - (p[IA_KEYROWS] & rowbit) } 138 ia_keyboard_union(p) 139 return action 140 } 141 i = i + 1 142 } 143 return 0 144} 145 146// W3C standard-gamepad button index -> action (12/13/14/15 = dpad, 0=A, 1=B, 9=start, 8=select). 147// this is the published gamepad SPEC, not tunable config, so it is a fixed decode. 148func ia_pad_decode(btn: i64) -> i64 { 149 if btn == 12 { return IA_UP } 150 if btn == 13 { return IA_DOWN } 151 if btn == 14 { return IA_LEFT } 152 if btn == 15 { return IA_RIGHT } 153 if btn == 0 { return IA_A } 154 if btn == 1 { return IA_B } 155 if btn == 9 { return IA_MENU } 156 if btn == 8 { return IA_PAUSE } 157 return 0 158} 159 160func ia_pad(p: *i64, btn: i64, down: i64) -> i64 { 161 let bit: i64 = ia_pad_decode(btn) 162 if bit == 0 { return 0 } 163 if down != 0 { p[IA_PAD] = p[IA_PAD] | bit } 164 if down == 0 { p[IA_PAD] = p[IA_PAD] - (p[IA_PAD] & bit) } 165 return bit 166} 167 168// one direction of one axis with hysteresis: v = signed milli TOWARD this direction. 169func ia_ax1(active: i64, v: i64) -> i64 { 170 if active == 0 { if v >= IA_TH_PRESS { return 1 } return 0 } 171 if v >= IA_TH_HOLD { return 1 } 172 return 0 173} 174 175// gamepad axis sample: idx 0 = x (+right), idx 1 = y (+down), value in milli. recomputes the 176// whole axis mask from the stored raw pair so each direction keeps its own hysteresis state. 177func ia_axis(p: *i64, idx: i64, milli: i64) -> i64 { 178 if idx == 0 { p[IA_AXX] = milli } 179 if idx == 1 { p[IA_AXY] = milli } 180 let m: i64 = p[IA_AX] 181 var nm: i64 = 0 182 if ia_ax1(m & IA_RIGHT, p[IA_AXX]) == 1 { nm = nm | IA_RIGHT } 183 if ia_ax1(m & IA_LEFT, 0 - p[IA_AXX]) == 1 { nm = nm | IA_LEFT } 184 if ia_ax1(m & IA_DOWN, p[IA_AXY]) == 1 { nm = nm | IA_DOWN } 185 if ia_ax1(m & IA_UP, 0 - p[IA_AXY]) == 1 { nm = nm | IA_UP } 186 p[IA_AX] = nm 187 return nm 188} 189 190// touch: virtual-d-pad zone (nx_touch_zone SSOT) -> legacy keycode -> binding table -> action. 191// single-touch rung: the mask IS the current touch's action; up clears it. 192func ia_touch(p: *i64, tx: i64, ty: i64, w: i64, h: i64, down: i64) -> i64 { 193 if down == 0 { p[IA_TOUCH] = 0; return 0 } 194 let code: i64 = nx_touch_zone(tx, ty, w, h) 195 p[IA_TOUCH] = ia_lookup(p, code) 196 return p[IA_TOUCH] 197} 198 199// union of every source -- the mask a game loop reads for HELD movement. 200func ia_held(p: *i64) -> i64 { 201 return p[IA_KB] | p[IA_PAD] | p[IA_AX] | p[IA_TOUCH] 202} 203 204// frame boundary: returns the RISING-edge mask (newly pressed since the last call) and latches. 205// call once per game tick; use the result for one-shot actions (interact/menu), ia_held for movement. 206func ia_frame(p: *i64) -> i64 { 207 let held: i64 = ia_held(p) 208 let pressed: i64 = held - (held & p[IA_PREV]) 209 p[IA_PREV] = held 210 return pressed 211} 212 213// analog look: accumulate a relative delta (mouse movement, touch drag). Clamped so a stall 214// between frames can never bank an unbounded spin. 215func ia_look(p: *i64, dx: i64, dy: i64) -> i64 { 216 var nx2: i64 = p[IA_LOOKX] + dx 217 if nx2 > IA_LOOK_CLAMP { nx2 = IA_LOOK_CLAMP } 218 if nx2 < 0 - IA_LOOK_CLAMP { nx2 = 0 - IA_LOOK_CLAMP } 219 p[IA_LOOKX] = nx2 220 var ny2: i64 = p[IA_LOOKY] + dy 221 if ny2 > IA_LOOK_CLAMP { ny2 = IA_LOOK_CLAMP } 222 if ny2 < 0 - IA_LOOK_CLAMP { ny2 = 0 - IA_LOOK_CLAMP } 223 p[IA_LOOKY] = ny2 224 return 0 225} 226 227// drain the accumulated look delta (once per game tick): out[0]=dx out[1]=dy, then zeroed -- 228// the same delta can never be applied twice. 229func ia_look_take(p: *i64, out: *i64) -> i64 { 230 out[0] = p[IA_LOOKX] 231 out[1] = p[IA_LOOKY] 232 p[IA_LOOKX] = 0 233 p[IA_LOOKY] = 0 234 return 0 235}