From 8337a87fb2ecfe9fd0ed4cd172fc98ec588b9d7f Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:11:08 -0400 Subject: [PATCH] D118: implement SDL input layer (kbd/mouse/gamepad -> N64 pad) port/src/input.c was a stub; input only worked via libultra.c's minimal contSnapshotFromKeyboard (no C-buttons, mouse, or gamepad). - input.c: keyboard+mouse + SDL_GameController -> OSContPad. Analog stick = move, C-buttons = aim. Mouse-look bridged to digital C-buttons via a clamped per-axis accumulator (proportional press dwell). RMB/LT = aim mode, LMB/RT = fire. Config: ge007.ini [Input] MouseEnabled / MouseAimSpeed / MouseInvertY. - input.h: add inputConnectedMask() + inputComputePad(). - libultra.c SI section: contSnapshotFromKeyboard() now delegates to the input module (single source of controller state); osContGetReadData() fills all MAXCONTROLLERS channels, not just controller 0. - GE_INPUTLOG=1 gates a per-poll OSContPad trace (zero-cost when unset). Build green (ntsc-final); boots -level_09 crash-free 35s. Live input untested from headless agent - human playtest owed. --- port/include/input.h | 10 ++ port/src/input.c | 331 ++++++++++++++++++++++++++++++++++++++++--- port/src/libultra.c | 71 +++------- 3 files changed, 345 insertions(+), 67 deletions(-) diff --git a/port/include/input.h b/port/include/input.h index 1e74180d..b37c5caf 100644 --- a/port/include/input.h +++ b/port/include/input.h @@ -28,6 +28,16 @@ void inputUpdate(void); /* Number of controllers currently "connected" (1..4). */ int inputGetNumControllers(void); +/* Bitmask of connected controllers (bit N = controller N). Bit 0 is always + * set (keyboard/mouse). Consumed by libultra.c's osContInit. */ +int inputConnectedMask(void); + +/* Compute the N64 button mask + analog stick for controller `idx`. + * Returns the 16-bit CONT_* button mask; writes the stick (-80..80) through + * the out params. Reads current SDL keyboard/mouse/gamepad state plus the + * mouse-aim accumulator maintained by inputUpdate(). */ +unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y); + #ifdef __cplusplus } #endif diff --git a/port/src/input.c b/port/src/input.c index bd378e0f..d8365c18 100644 --- a/port/src/input.c +++ b/port/src/input.c @@ -1,15 +1,70 @@ /* - * Input: SDL2 keyboard/mouse/gamepad -> N64 controller structs. + * Input: SDL2 keyboard/mouse/gamepad -> N64 controller state (Phase 3, D118). * - * The game reads controllers via osContInit / osContStartReadData (src/joy.c). - * This module populates the N64 controller state from SDL events. + * The game reads controllers via osContStartReadData / osContGetReadData + * (src/joy.c). libultra.c's SI section calls into this module once per + * controller poll: inputUpdate() refreshes SDL + accumulates the mouse-aim + * delta, then inputComputePad() produces the 16-bit button mask + stick for + * one controller. This is the single source of controller state on PC -- + * the old contSnapshotFromKeyboard() now delegates here. * - * Modelled on the PD port's port/src/input.c (~1550 lines). Default bindings - * follow the 1964GEPD / Xbox schemes (see the PD port README table). + * Deliberately NOT a full port of pd_port/port/src/input.c (1551 lines): GE's + * menu/config code never calls that module's VK/bind-string API, so this is a + * focused implementation matching port/include/input.h plus the two helpers + * libultra.c needs. * - * STATUS: scaffolding stub — implement during Phase 3. + * ------------------------------------------------------------------------ + * BINDING SCHEME (documented in docs/PCPortResearch.md sec F "D118") + * + * GE's default "1.1" control style: analog stick = move/strafe, the four + * C-buttons = aim/turn/look (DIGITAL on N64), R = aim mode, Z = fire. + * + * Keyboard + mouse (controller 0): + * W/S/A/D or arrows .. analog stick (move / strafe) + * mouse motion ....... C-buttons (proportional pulse -- see below) + * left mouse / LCtrl . Z trigger (fire) + * right mouse / LShift R trigger (aim mode) + * Space / Z / E ...... A button (action / use) + * X / R / F ......... B button (reload / cancel) + * Q ................. L trigger + * Enter / Tab ....... Start + * + * Xbox / SDL_GameController (controller 0 merges pad 0 with kbd/mouse; + * pads 1-3 -> controllers 1-3): + * left stick ........ analog stick (move / strafe) + * right stick ....... C-buttons (digital, 50% threshold -- aim) + * right trigger ..... Z trigger (fire) + * left trigger ...... R trigger (aim mode) + * A / X ............. A button + * B / Y ............. B button + * LB ............... L trigger + * RB ............... B button (reload) + * D-pad ............ N64 D-pad + * Start ............ Start + * + * MOUSE-LOOK -> C-BUTTON BRIDGE + * GE has no analog-aim hook we can reach without touching src/ (game + * logic). The only lever is OSContPad, so mouse motion is converted to + * digital C-button presses. inputUpdate() integrates the relative mouse + * delta (scaled by Input.MouseAimSpeed/100) into a signed accumulator per + * axis, clamped to +/-AIM_ACCUM_CLAMP. Each poll inputComputePad() emits + * the matching C-button while |accum| >= 0.5 and drains one unit. A large + * flick therefore holds the C-button down for several frames + * (proportional dwell) rather than a single blip. + * + * Limitations: (a) it is still digital -- GE applies its own accel curve + * to a held C-button, so aim speed is not linear in mouse speed; + * (b) very fast flicks saturate at the clamp (~8 frames of turn); + * (c) diagonal precision is limited to the 8 C-button combinations. + * Tunable via ge007.ini [Input] MouseAimSpeed / MouseInvertY. A true + * analog aim path would need an #ifdef PORT hook in bondview.c and is + * left as a TODO. + * ------------------------------------------------------------------------ */ +#include +#include + #include #include "platform.h" @@ -17,32 +72,274 @@ #include "config.h" #include "input.h" +/* N64 button bits (from PR/os.h -- duplicated here to avoid pulling os.h, + * whose `u8 errno;` field collides with 's macro). */ +#define GE_CONT_A 0x8000 +#define GE_CONT_B 0x4000 +#define GE_CONT_G 0x2000 /* Z trigger */ +#define GE_CONT_START 0x1000 +#define GE_CONT_UP 0x0800 +#define GE_CONT_DOWN 0x0400 +#define GE_CONT_LEFT 0x0200 +#define GE_CONT_RIGHT 0x0100 +#define GE_CONT_L 0x0020 +#define GE_CONT_R 0x0010 +#define GE_CONT_E 0x0008 /* C-up */ +#define GE_CONT_D 0x0004 /* C-down */ +#define GE_CONT_C 0x0002 /* C-left */ +#define GE_CONT_F 0x0001 /* C-right */ + +#define MAX_PADS 4 +#define STICK_DEADZONE 7000 +#define STICK_MAX 80 +#define TRIG_THRESHOLD (30 * 256) +#define RSTICK_THRESHOLD 0x4000 +#define AIM_ACCUM_CLAMP 8.0 +#define AIM_EMIT_THRESHOLD 0.5 + static int numControllers = 1; +static int connectedMask = 0x1; /* controller 0 always present */ + +static SDL_GameController *pads[MAX_PADS]; + +static int mouseEnabled = 1; +static int mouseAimSpeed = 50; /* /100 -> mouse-px to accumulator units */ +static int mouseInvertY = 0; + +static double aimAccumX = 0.0; +static double aimAccumY = 0.0; + +/* ------------------------------------------------------------------------ */ + +static void inputOpenPads(void) +{ + connectedMask = 0x1; + int n = SDL_NumJoysticks(); + for (int i = 0; i < n && i < MAX_PADS; ++i) { + if (!SDL_IsGameController(i)) { + continue; + } + if (pads[i]) { + continue; + } + pads[i] = SDL_GameControllerOpen(i); + if (pads[i]) { + connectedMask |= (1 << i); + sysLogPrintf(LOG_NOTE, "input: opened gamepad %d '%s' as controller %d", + i, SDL_GameControllerName(pads[i]), i); + } + } + for (int i = 0; i < MAX_PADS; ++i) { + if (pads[i]) { + connectedMask |= (1 << i); + } + } + numControllers = 1; + for (int i = 1; i < MAX_PADS; ++i) { + if (connectedMask & (1 << i)) { + numControllers = i + 1; + } + } +} int inputInit(void) { - /* TODO(Phase 3): - * - SDL_InitSubSystem(INPUT) - * - open gamepad(s) - * - set up relative mouse mode for mouse-look - * - load bindings from config - */ - sysLogPrintf(LOG_INFO, "inputInit: TODO (Phase 3)"); - return 0; + if (!SDL_WasInit(SDL_INIT_GAMECONTROLLER)) { + if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) { + sysLogPrintf(LOG_WARNING, "input: SDL_INIT_GAMECONTROLLER failed: %s", + SDL_GetError()); + } + } + + for (int i = 0; i < MAX_PADS; ++i) { + pads[i] = NULL; + } + inputOpenPads(); + + /* Relative mouse mode for mouse-look. ESC still quits (video.c). */ + if (mouseEnabled) { + SDL_SetRelativeMouseMode(SDL_TRUE); + /* Drain the initial jump. */ + SDL_GetRelativeMouseState(NULL, NULL); + } + + sysLogPrintf(LOG_INFO, "input: ready (mask=0x%x, %d controller(s), aimSpeed=%d)", + connectedMask, numControllers, mouseAimSpeed); + return connectedMask; } void inputDestroy(void) { - /* TODO(Phase 3) */ + for (int i = 0; i < MAX_PADS; ++i) { + if (pads[i]) { + SDL_GameControllerClose(pads[i]); + pads[i] = NULL; + } + } + if (SDL_WasInit(SDL_INIT_GAMECONTROLLER)) { + SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); + } } +/* Poll once per controller-read: refresh SDL device state and integrate the + * mouse-aim delta into the accumulator. */ void inputUpdate(void) { - /* TODO(Phase 3): poll SDL events, write into the controller state that - * osContStartReadData / osContRead will return. */ + SDL_GameControllerUpdate(); + + if (!mouseEnabled) { + return; + } + + int dx = 0, dy = 0; + SDL_GetRelativeMouseState(&dx, &dy); + + const double scale = (double)mouseAimSpeed / 100.0; + aimAccumX += dx * scale; + aimAccumY += dy * scale * (mouseInvertY ? -1.0 : 1.0); + + if (aimAccumX > AIM_ACCUM_CLAMP) aimAccumX = AIM_ACCUM_CLAMP; + if (aimAccumX < -AIM_ACCUM_CLAMP) aimAccumX = -AIM_ACCUM_CLAMP; + if (aimAccumY > AIM_ACCUM_CLAMP) aimAccumY = AIM_ACCUM_CLAMP; + if (aimAccumY < -AIM_ACCUM_CLAMP) aimAccumY = -AIM_ACCUM_CLAMP; +} + +static int scaleAxis(int v) +{ + if (v > -STICK_DEADZONE && v < STICK_DEADZONE) { + return 0; + } + if (v < 0) v += STICK_DEADZONE; else v -= STICK_DEADZONE; + int out = (int)((long)v * STICK_MAX / (32767 - STICK_DEADZONE)); + if (out > STICK_MAX) out = STICK_MAX; + if (out < -STICK_MAX) out = -STICK_MAX; + return out; +} + +static int keyDown(const Uint8 *ks, SDL_Scancode sc) +{ + return ks && ks[sc]; +} + +/* Fill button mask + stick for controller idx. Returns the 16-bit mask. */ +unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y) +{ + unsigned button = 0; + int sx = 0, sy = 0; + + if (idx < 0 || idx >= MAX_PADS) { + if (stick_x) *stick_x = 0; + if (stick_y) *stick_y = 0; + return 0; + } + + /* ---- keyboard + mouse: controller 0 only ---- */ + if (idx == 0) { + const Uint8 *ks = SDL_GetKeyboardState(NULL); + Uint32 mb = mouseEnabled ? SDL_GetMouseState(NULL, NULL) : 0; + + if (keyDown(ks, SDL_SCANCODE_W) || keyDown(ks, SDL_SCANCODE_UP)) sy = STICK_MAX; + if (keyDown(ks, SDL_SCANCODE_S) || keyDown(ks, SDL_SCANCODE_DOWN)) sy = -STICK_MAX; + if (keyDown(ks, SDL_SCANCODE_A) || keyDown(ks, SDL_SCANCODE_LEFT)) sx = -STICK_MAX; + if (keyDown(ks, SDL_SCANCODE_D) || keyDown(ks, SDL_SCANCODE_RIGHT)) sx = STICK_MAX; + + if ((mb & SDL_BUTTON(SDL_BUTTON_LEFT)) || keyDown(ks, SDL_SCANCODE_LCTRL)) + button |= GE_CONT_G; + if ((mb & SDL_BUTTON(SDL_BUTTON_RIGHT)) || keyDown(ks, SDL_SCANCODE_LSHIFT)) + button |= GE_CONT_R; + if (keyDown(ks, SDL_SCANCODE_SPACE) || keyDown(ks, SDL_SCANCODE_Z) || + keyDown(ks, SDL_SCANCODE_E)) + button |= GE_CONT_A; + if (keyDown(ks, SDL_SCANCODE_X) || keyDown(ks, SDL_SCANCODE_R) || + keyDown(ks, SDL_SCANCODE_F)) + button |= GE_CONT_B; + if (keyDown(ks, SDL_SCANCODE_Q)) + button |= GE_CONT_L; + if (keyDown(ks, SDL_SCANCODE_RETURN) || keyDown(ks, SDL_SCANCODE_TAB)) + button |= GE_CONT_START; + + /* mouse-aim accumulator -> C-buttons (drained here) */ + if (mouseEnabled) { + if (aimAccumX >= AIM_EMIT_THRESHOLD) { button |= GE_CONT_F; aimAccumX -= 1.0; } + else if (aimAccumX <= -AIM_EMIT_THRESHOLD) { button |= GE_CONT_C; aimAccumX += 1.0; } + if (aimAccumY >= AIM_EMIT_THRESHOLD) { button |= GE_CONT_D; aimAccumY -= 1.0; } + else if (aimAccumY <= -AIM_EMIT_THRESHOLD) { button |= GE_CONT_E; aimAccumY += 1.0; } + } + } + + /* ---- gamepad ---- */ + SDL_GameController *pad = pads[idx]; + if (pad) { + int lx = SDL_GameControllerGetAxis(pad, SDL_CONTROLLER_AXIS_LEFTX); + int ly = SDL_GameControllerGetAxis(pad, SDL_CONTROLLER_AXIS_LEFTY); + int rx = SDL_GameControllerGetAxis(pad, SDL_CONTROLLER_AXIS_RIGHTX); + int ry = SDL_GameControllerGetAxis(pad, SDL_CONTROLLER_AXIS_RIGHTY); + + int px = scaleAxis(lx); + int py = -scaleAxis(ly); /* SDL up = negative -> N64 up = positive */ + if (px) sx = px; + if (py) sy = py; + + if (rx > RSTICK_THRESHOLD) button |= GE_CONT_F; + if (rx < -RSTICK_THRESHOLD) button |= GE_CONT_C; + if (ry > RSTICK_THRESHOLD) button |= GE_CONT_D; + if (ry < -RSTICK_THRESHOLD) button |= GE_CONT_E; + + if (SDL_GameControllerGetAxis(pad, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) > TRIG_THRESHOLD) + button |= GE_CONT_G; + if (SDL_GameControllerGetAxis(pad, SDL_CONTROLLER_AXIS_TRIGGERLEFT) > TRIG_THRESHOLD) + button |= GE_CONT_R; + + if (SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_A) || + SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_X)) + button |= GE_CONT_A; + if (SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_B) || + SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_Y) || + SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)) + button |= GE_CONT_B; + if (SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_LEFTSHOULDER)) + button |= GE_CONT_L; + if (SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_START)) + button |= GE_CONT_START; + if (SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_DPAD_UP)) + button |= GE_CONT_UP; + if (SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_DPAD_DOWN)) + button |= GE_CONT_DOWN; + if (SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_DPAD_LEFT)) + button |= GE_CONT_LEFT; + if (SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) + button |= GE_CONT_RIGHT; + } + + if (sx > STICK_MAX) sx = STICK_MAX; + if (sx < -STICK_MAX) sx = -STICK_MAX; + if (sy > STICK_MAX) sy = STICK_MAX; + if (sy < -STICK_MAX) sy = -STICK_MAX; + + if (stick_x) *stick_x = (signed char)sx; + if (stick_y) *stick_y = (signed char)sy; + + if (getenv("GE_INPUTLOG") && (button || sx || sy)) { + sysLogPrintf(LOG_NOTE, "GE_INPUTLOG cont%d: btn=%04x stick=(%d,%d)", + idx, button, sx, sy); + } + + return button; +} + +int inputConnectedMask(void) +{ + return connectedMask; } int inputGetNumControllers(void) { return numControllers; } + +PD_CONSTRUCTOR static void inputConfigInit(void) +{ + configRegisterInt("Input.MouseEnabled", &mouseEnabled, 0, 1); + configRegisterInt("Input.MouseAimSpeed", &mouseAimSpeed, 1, 500); + configRegisterInt("Input.MouseInvertY", &mouseInvertY, 0, 1); +} diff --git a/port/src/libultra.c b/port/src/libultra.c index 5283d68a..59c8a733 100644 --- a/port/src/libultra.c +++ b/port/src/libultra.c @@ -1005,61 +1005,30 @@ static OSContStatus g_contStatus[MAXCONTROLLERS]; static OSContPad g_contPad[MAXCONTROLLERS]; static u8 g_contConnected = 0x1; /* controller 0 connected */ -/* Keyboard -> N64 pad for controller 0: - * A: Z / Space B: X Start: Enter - * Z trigger: LCtrl C-stick: arrows D-pad: WASD - */ +/* Snapshot all controllers from the SDL input module (port/src/input.c). + * That module owns the keyboard/mouse/gamepad -> N64 mapping and the + * mouse-look -> C-button bridge; this just marshals its output into the + * OSContPad / OSContStatus arrays joy.c reads via osContGetReadData(). */ static void contSnapshotFromKeyboard(void) { - /* SDL2: the keyboard state array is indexed by SCANCODE (512 entries). - * Non-ASCII SDLK_* constants carry a 0x40000000 flag (SDLK_UP == - * 0x40000052), so they must be masked to the scancode value before use - * as an index — indexing with the raw constant reads ~1 GB past the - * array and faults. */ -#define K(key) keys[(key) & 0xFFFF] - const uint8_t *keys = SDL_GetKeyboardState(NULL); - u16 button = 0; - s8 sx = 0, sy = 0; + inputUpdate(); - if (!keys) return; /* SDL not ready yet (first frames before videoInit) - * — leave the previous frame's pad state in place. */ + const s32 mask = inputConnectedMask(); + g_contConnected = (u8)mask; - if (K(SDLK_z) || K(SDLK_SPACE)) button |= CONT_A; - if (K(SDLK_x)) button |= CONT_B; - if (K(SDLK_LCTRL)) button |= CONT_G; /* Z trigger */ - if (K(SDLK_RETURN)) button |= CONT_START; + for (int i = 0; i < MAXCONTROLLERS; ++i) { + const int connected = (mask & (1 << i)) != 0; + s8 sx = 0, sy = 0; + u16 button = connected ? (u16)inputComputePad(i, &sx, &sy) : 0; - /* C-stick (arrows) — GE's movement stick. */ - if (K(SDLK_UP)) sy = -80; - if (K(SDLK_DOWN)) sy = 80; - if (K(SDLK_LEFT)) sx = -80; - if (K(SDLK_RIGHT)) sx = 80; - - /* D-pad (WASD) — separate from the C-stick. */ - if (K(SDLK_w)) button |= CONT_UP; - if (K(SDLK_s)) button |= CONT_DOWN; - if (K(SDLK_a)) button |= CONT_LEFT; - if (K(SDLK_d)) button |= CONT_RIGHT; -#undef K - - g_contStatus[0].type = CONT_TYPE_NORMAL; - g_contStatus[0].status = 0; - g_contStatus[0].errno = 0; - /* The pad is kept in a parallel array; joy.c copies it via - * osContGetReadData(). */ - g_contPad[0].button = button; - g_contPad[0].stick_x = sx; - g_contPad[0].stick_y = sy; - g_contPad[0].errno = 0; - - for (int i = 1; i < MAXCONTROLLERS; ++i) { - g_contStatus[i].type = 0; + g_contStatus[i].type = connected ? CONT_TYPE_NORMAL : 0; g_contStatus[i].status = 0; - g_contStatus[i].errno = CONT_NO_RESPONSE_ERROR; - g_contPad[i].button = 0; - g_contPad[i].stick_x = 0; - g_contPad[i].stick_y = 0; - g_contPad[i].errno = CONT_NO_RESPONSE_ERROR; + g_contStatus[i].errno = connected ? 0 : CONT_NO_RESPONSE_ERROR; + + g_contPad[i].button = button; + g_contPad[i].stick_x = connected ? sx : 0; + g_contPad[i].stick_y = connected ? sy : 0; + g_contPad[i].errno = connected ? 0 : CONT_NO_RESPONSE_ERROR; } } @@ -1095,7 +1064,9 @@ void osContGetQuery(OSContStatus *status) void osContGetReadData(OSContPad *pad) { - *pad = g_contPad[0]; + /* N64 semantics: fill one OSContPad per controller channel. joy.c passes + * g_ContData[0].samples[i].pads (a MAXCONTROLLERS-long array). */ + memcpy(pad, g_contPad, sizeof(g_contPad)); } s32 osContReset(OSMesgQueue *mq, OSContStatus *status)