From ad748654044b4888cb4fed69477349411f8f0cfb Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Wed, 2 Sep 2026 01:26:33 -0400 Subject: [PATCH] port: click-to-lock mouse capture + absolute menu cursor + aim retune (D180) WI-1: Input.MouseCaptureMode (0 = legacy always-grab, default; 1 = Quake/Nightdive-style click-to-lock). In mode 1 the OS cursor is free until a click lands in the game window; ESC, focus loss, or entering any front-end menu frees it again, and re-entering a stage while armed re-locks so unpausing needs no click. Mouse buttons are withheld from the game while the cursor is free in-stage (no phantom fire). Controller input paths are untouched. reconcileGrab() runs once per controller-0 poll off the game's current_menu global (UI context only). WI-2: when capture mode is on and the cursor is free in a menu, the D165/D169 pointer P-controller takes its target from the real cursor's absolute window position mapped onto the live virtual front-end rect (getPlayer_c_screen*), for true 1:1 tracking. The relative-delta path (legacy / grabbed) is unchanged. B3: Input.MouseAimSpeed default 25 -> 16 (aim mode still overshot at 25). Build 241/241; -level_09 framediff 3/3 within threshold (unregressed); GE_STARTMENU menu boot crash-free. Feel-checks owed (headless can't drive the mouse). Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT --- port/include/input.h | 10 ++++ port/src/input.c | 113 +++++++++++++++++++++++++++++++++++++++---- port/src/video.c | 18 +++++++ 3 files changed, 132 insertions(+), 9 deletions(-) diff --git a/port/include/input.h b/port/include/input.h index e478670a..ea57e382 100644 --- a/port/include/input.h +++ b/port/include/input.h @@ -44,6 +44,16 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y); * disabled in config. */ void inputSetMouseGrab(int on); +/* WI-1 click-to-lock cursor capture (Input.MouseCaptureMode = 1). The host + * event pump calls inputNotifyClick() when a mouse button goes down inside the + * game window (arms + locks the cursor), and inputReleaseCapture() when ESC is + * pressed (frees it; returns 1 if it consumed the key). inputMouseCaptureActive() + * is true when capture mode is on and the cursor is currently free -- the pump + * uses it to decide whether a click should be swallowed rather than passed on. */ +void inputNotifyClick(void); +int inputReleaseCapture(void); +int inputMouseCaptureActive(void); + /* Queue a mouse-wheel weapon-cycle input (one short A-button press). Sign is * ignored -- GE only cycles forward on a bare A edge. */ void inputPostWheel(int notches); diff --git a/port/src/input.c b/port/src/input.c index c8426a6a..85806347 100644 --- a/port/src/input.c +++ b/port/src/input.c @@ -161,14 +161,29 @@ extern float cursor_h_pos, cursor_v_pos; #define MENU_EST_DELTA 1.0 /* deltaEst per poll (front.c integrates ~1 tick/frame) */ #define MENU_MOUSE_TO_PX 1.0 /* device px -> virtual cursor px (x MenuPointerSpeed/100) */ +static void applyGrab(int want); +static void reconcileGrab(int menuMode); + static int numControllers = 1; static int connectedMask = 0x1; /* controller 0 always present */ static SDL_GameController *pads[MAX_PADS]; static int mouseEnabled = 1; -static int mouseGrabbed = 1; /* released while the window is unfocused */ -static int mouseAimSpeed = 25; /* aim-mode sensitivity, percent (B3: was 50, overshot) */ +static int mouseGrabbed = 1; /* released while the window is unfocused */ + +/* WI-1: Quake-style click-to-lock cursor capture. + * MouseCaptureMode 0 (default) = legacy: cursor is grabbed whenever the + * window has focus and MouseEnabled is set (prior behaviour, byte-exact). + * MouseCaptureMode 1 = native-PC: the cursor is free until you click in the + * game window; ESC (or focus loss, or opening a menu) frees it again. + * Re-entering a stage while still "armed" re-locks automatically so + * unpausing / starting a level does not need a click. + * Controller input is entirely independent of all of this. */ +static int mouseCaptureMode = 0; +static int captureArmed = 0; /* user has clicked to lock (capture mode) */ +static int windowFocused = 1; +static int mouseAimSpeed = 16; /* aim-mode sensitivity, percent (B3: 50 -> 25 M-29 -> 16; still overshot at 25) */ static int aimBand = 20; /* aim mode: usable stick range above the 60 gate */ static int mouseTurnSpeed = 100; /* hipfire yaw sensitivity, percent */ static int menuPointerSpeed = 100; /* front-end cursor speed, percent */ @@ -395,8 +410,9 @@ int inputInit(void) } inputOpenPads(); - /* Relative mouse mode for mouse-look. ESC still quits (video.c). */ - mouseGrabbed = mouseEnabled; + /* Relative mouse mode for mouse-look. In click-to-lock mode we start + * released and wait for a click in the window (video.c -> inputNotifyClick). */ + mouseGrabbed = mouseEnabled && !mouseCaptureMode; if (mouseEnabled) { if (mouseRawInput) { /* Feed the raw device delta straight through: no OS pointer @@ -406,7 +422,7 @@ int inputInit(void) SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "0"); sysLogPrintf(LOG_INFO, "input: raw mouse input (no OS pointer accel)"); } - SDL_SetRelativeMouseMode(SDL_TRUE); + SDL_SetRelativeMouseMode(mouseGrabbed ? SDL_TRUE : SDL_FALSE); /* Drain the initial jump. */ SDL_GetRelativeMouseState(NULL, NULL); } @@ -487,6 +503,15 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y) int menuMode = (current_menu != GE_MENU_RUN_STAGE && current_menu != GE_MENU_INVALID); + reconcileGrab(menuMode); + + /* Click-to-lock, in a stage, cursor free: the mouse buttons must not + * reach the game (no phantom fire) -- the first click only re-locks + * (handled in video.c -> inputNotifyClick). Menus keep their buttons. */ + if (mouseCaptureMode && !mouseGrabbed && !menuMode) { + mb = 0; + } + /* GE default control (1.1): stick Y = move fwd/back, stick X = turn, * C-left/right = sidestep, C-up/down = look. FPS layout: W/S move, * A/D strafe (C-buttons), mouse X turns (stick X), mouse Y looks. */ @@ -575,9 +600,33 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y) menuEstV = menuTgtV = cursor_v_pos; hipPitchPhase = 0.0; } + /* WI-2: with click-to-lock the front end has a real free OS + * cursor. Map its absolute window position straight onto the + * virtual front-end rect so the game crosshair tracks it 1:1 + * (no drift -- the P-controller below just chases this target). + * The relative-delta path (legacy / grabbed) is unchanged. */ + int haveAbs = 0; + if (mouseCaptureMode && !mouseGrabbed) { + int mx = 0, my = 0; + SDL_GetMouseState(&mx, &my); + SDL_Window *w = SDL_GetMouseFocus(); + int ww = 0, wh = 0; + if (w) SDL_GetWindowSize(w, &ww, &wh); + if (ww > 0 && wh > 0) { + double fx = (double)mx / (double)ww; + double fy = (double)my / (double)wh; + if (fx < 0.0) fx = 0.0; else if (fx > 1.0) fx = 1.0; + if (fy < 0.0) fy = 0.0; else if (fy > 1.0) fy = 1.0; + menuTgtH = loH + fx * (hiH - loH); + menuTgtV = loV + fy * (hiV - loV); + haveAbs = 1; + } + } double s = (menuPointerSpeed / 100.0) * MENU_MOUSE_TO_PX; - menuTgtH += edx * s; - menuTgtV += dyLook * s; /* dyLook > 0 => cursor moves down (+V) */ + if (!haveAbs) { + menuTgtH += edx * s; + menuTgtV += dyLook * s; /* dyLook > 0 => cursor moves down (+V) */ + } if (menuTgtH < loH) menuTgtH = loH; if (menuTgtH > hiH) menuTgtH = hiH; if (menuTgtV < loV) menuTgtV = loV; @@ -711,9 +760,9 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y) return button; } -void inputSetMouseGrab(int on) +static void applyGrab(int want) { - int want = on && mouseEnabled; + want = want && mouseEnabled; if (want == mouseGrabbed) { return; } @@ -725,6 +774,51 @@ void inputSetMouseGrab(int on) mouseDX = mouseDY = 0.0; } +/* Reconcile the SDL grab state with what the current mode wants. Called once + * per controller-0 poll (menuMode known there) and from the notify hooks. */ +static void reconcileGrab(int menuMode) +{ + int want; + if (!mouseCaptureMode) { + want = windowFocused; /* legacy */ + } else { + want = captureArmed && windowFocused && !menuMode; /* click-to-lock */ + } + applyGrab(want); +} + +/* video.c focus events. In legacy mode this directly grabs/releases; in + * capture mode it just records focus and lets reconcileGrab() decide. */ +void inputSetMouseGrab(int on) +{ + windowFocused = on ? 1 : 0; + reconcileGrab(0); /* menuMode re-checked on the next poll anyway */ +} + +/* A mouse click landed in the game window (video.c). In click-to-lock mode + * this arms + grabs; otherwise it is ignored (the game sees the click). */ +void inputNotifyClick(void) +{ + if (mouseCaptureMode && mouseEnabled && windowFocused) { + captureArmed = 1; + reconcileGrab(0); + } +} + +/* ESC pressed (video.c). In click-to-lock mode, releases the cursor and + * reports 1 so the caller can swallow the key; otherwise reports 0. */ +int inputReleaseCapture(void) +{ + if (mouseCaptureMode && mouseGrabbed) { + captureArmed = 0; + applyGrab(0); + return 1; + } + return 0; +} + +int inputMouseCaptureActive(void) { return mouseCaptureMode && !mouseGrabbed; } + void inputPostWheel(int notches) { if (notches < 0) notches = -notches; /* both directions cycle forward */ @@ -762,6 +856,7 @@ int inputGetNumControllers(void) PD_CONSTRUCTOR static void inputConfigInit(void) { configRegisterInt("Input.MouseEnabled", &mouseEnabled, 0, 1); + configRegisterInt("Input.MouseCaptureMode", &mouseCaptureMode, 0, 1); configRegisterInt("Input.MouseAimSpeed", &mouseAimSpeed, 1, 500); configRegisterInt("Input.AimBand", &aimBand, 5, 40); configRegisterInt("Input.MouseTurnSpeed", &mouseTurnSpeed, 1, 500); diff --git a/port/src/video.c b/port/src/video.c index 28a3b01e..08d5f256 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -71,8 +71,16 @@ static int cfgWinX = -1; static int cfgWinY = -1; static int cfgWinMax = 0; +/* + * [Game] gameplay-cosmetic knobs (route-(b) hooks in src/, findings D181). + * portScreenShakeScale multiplies every viShake() amplitude (src/fr.c). + * 1.0f = original behaviour (headless golden dumps unaffected). + */ +f32 portScreenShakeScale = 1.0f; + PD_CONSTRUCTOR static void videoConfigInit(void) { + configRegisterFloat("Game.ScreenShakeIntensity", &portScreenShakeScale, 0.0f, 10.0f); configRegisterInt("Video.VSync", &cfgVSync, 0, 1); configRegisterInt("Video.FpsCap", &cfgFpsCap, 0, 1000); configRegisterInt("Video.MSAA", &cfgMSAA, 1, 8); @@ -223,8 +231,18 @@ void videoPumpEvents(void) exit(0); } else if (ev.key.keysym.sym == SDLK_F12 && !ev.key.repeat) { screenshotReq = 1; + } else if (ev.key.keysym.sym == SDLK_ESCAPE && !ev.key.repeat) { + /* WI-1: in click-to-lock mode ESC frees the captured cursor + * (and is swallowed); otherwise it falls through to input.c + * where it feeds the N64 B button (D145). */ + inputReleaseCapture(); } break; + case SDL_MOUSEBUTTONDOWN: + /* WI-1: a click in the window (re)locks the cursor in + * click-to-lock mode; a no-op otherwise. */ + inputNotifyClick(); + break; case SDL_MOUSEWHEEL: inputPostWheel(ev.wheel.y); /* weapon cycle */ break;