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.
This commit is contained in:
James Dansereau committed 2026-08-29 11:11:08 -04:00
1 parent 06c601f9b3
commit 8337a87fb2
3 files changed
+345 -67

No files matched your search

+10
View File
@@ -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
+314 -17
View File
@@ -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 <math.h>
#include <stdlib.h>
#include <SDL.h>
#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 <errno.h>'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);
}
+21 -50
View File
@@ -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)