code wiki / (root) / nx_input_abstract.nx

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}