forked from Ports/goldeneye-pc-port
Input.Layout (0=FPS default, 1=GEPD) + Input.CrouchMode (hold/toggle) rows in F10 INPUT (Turok-standard functional section; the planned MODERN bucket is dropped per D356). The GEPD preset acts as the per-key effective default under [Input.Bind] overrides; new IA_RELOAD/IA_CROUCH actions emit GE_CONT_B (unbound by default, N64-original). Toggle crouch latches B with a menu-boundary latch drop. optionsoverlay rowSetCommit hook re-derives the binds via inputLayoutApply(). inputRebuildBinds no longer warns on intentionally-empty defaults. Boot + ini round-trip + F10 page verified; in-game feel-check owed (D371).
2084 lines
104 KiB
C
2084 lines
104 KiB
C
/*
|
|
* Input: SDL2 keyboard/mouse/gamepad -> N64 controller state (Phase 3, D118).
|
|
*
|
|
* 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.
|
|
*
|
|
* 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.
|
|
*
|
|
* ------------------------------------------------------------------------
|
|
* BINDING SCHEME (documented in docs/internals.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 ....... aim (mode-aware -- see MOUSE-LOOK 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 (mode-aware, no src/ changes)
|
|
* GE's aim model (bondview2.c bondviewProcessInput / MoveData) is
|
|
* mode-dependent, so the mouse->pad mapping is too:
|
|
*
|
|
* - Hipfire (!insightaimmode): yaw = analog stick-X ("natural turn");
|
|
* pitch = DIGITAL C-up/C-down (stick-Y here is move fwd/back and does
|
|
* not pitch). Mouse Y emits a C-button on polls where it moved.
|
|
* - Aim mode (R / RMB held): yaw AND pitch are analog -- the stick pushed
|
|
* past +/-60 gives proportional (stick-60)/10 aim speed. Mouse X/Y push
|
|
* the stick into the 61..80 band. We emit NO C-buttons in this mode
|
|
* because there they mean crouch / lean / zoom (this was D118c:
|
|
* aim + look-down -> crouch).
|
|
*
|
|
* "aim button held" is read from our own RMB/LShift state -- exact for
|
|
* the default hold-to-aim scheme; a toggle-aim scheme would need a read
|
|
* of g_CurrentPlayer->insightaimmode (still no logic change).
|
|
*
|
|
* GE's native pitch is inverted ("flight" style: C-up -> look down). We
|
|
* hide that: mouse-down looks down by default, MouseInvertY flips it.
|
|
*
|
|
* Tunable via ge007.ini [Input]: MouseAimSpeed (aim mode), MouseTurnSpeed
|
|
* (hipfire yaw), MouseInvertY, MouseEnabled. Residual: in hipfire, yaw
|
|
* (analog) and pitch (digital) still feel different (D118a) -- a fully
|
|
* analog hipfire pitch needs an #ifdef PORT hook in bondview.c (TODO).
|
|
* ------------------------------------------------------------------------
|
|
*/
|
|
|
|
#include "port_math.h" /* real system math decls; see header for why */
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <SDL.h>
|
|
|
|
#include "platform.h"
|
|
#include "system.h"
|
|
#include "config.h"
|
|
#include "input.h"
|
|
#include "envflag.h"
|
|
#include "optionsoverlay.h"
|
|
#include "frontoptions.h"
|
|
/* D194 absolute aim: read-only access to the live camera (struct player).
|
|
* Game header pulled in through the same shim path every other compiled game
|
|
* file uses; we only READ vv_theta/vv_verta/speedtheta/speedverta/aspect. */
|
|
#include "player.h"
|
|
|
|
/* D194 spazz diagnosis: game ticks batched into the current poll (lv.h).
|
|
* Read-only; declared locally to avoid pulling lv.h's wider dependency set. */
|
|
extern s32 g_ClockTimer;
|
|
|
|
/* 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
|
|
|
|
/* Front-end pointer mode: GE's menu cursor ("crosshair") is stick-driven
|
|
* (front.c frontUpdateControlStickPosition reads joyGetStickX/Y and
|
|
* integrates a screen position). During a running stage current_menu ==
|
|
* MENU_RUN_STAGE (11, src/bondconstants.h enum MENU); every other value is
|
|
* a front-end / briefing / debrief / cheat screen where the mouse should
|
|
* feel like a pointer, not a look-axis. We read the game global directly
|
|
* (enum MENU is ABI int) -- UI-context only, no logic change.
|
|
* MENU_INVALID (-1) means the front end never ran (bare -level_XX boot):
|
|
* treat that as in-game so direct-launch mouse-look is unaffected. */
|
|
extern int current_menu;
|
|
#define GE_MENU_RUN_STAGE 11
|
|
#define GE_MENU_INVALID (-1)
|
|
/* D169: the front-end cursor lives in the game's virtual-screen field, which
|
|
* is 440x330 in the front end -- not the 320x240 the pointer P-controller
|
|
* assumed. Read the live field so the mouse pointer can reach the whole
|
|
* mission-select grid. These are plain non-static engine accessors
|
|
* (src/game/bondview.c) and globals (src/game/front.c); UI geometry only, no
|
|
* logic change. */
|
|
extern float getPlayer_c_screenwidth(void);
|
|
extern float getPlayer_c_screenheight(void);
|
|
extern float getPlayer_c_screenleft(void);
|
|
extern float getPlayer_c_screentop(void);
|
|
extern float cursor_h_pos, cursor_v_pos;
|
|
|
|
/* D194/D238 -- natural-pitch control scheme.
|
|
*
|
|
* GE's hipfire pitch is structurally digital: bondviewProcessInput's default
|
|
* (1.1/HONEY) scheme reads pitch only from C-up/C-down (see D166), so the
|
|
* port emulates continuous pitch by duty-cycling that button -- an
|
|
* approximation that can never fully match yaw's genuinely continuous,
|
|
* unbounded analog mapping, which is the root of the "vertical feels slower"
|
|
* complaint. GE's own 1.2/SOLITARE scheme (cur_player_get/set_control_type,
|
|
* src/game/options.c; dispatch in bondviewProcessInput, bondview2.c ~5192-
|
|
* 5450) gives BOTH pitch and yaw continuous analog stick control in hipfire
|
|
* (canNaturalTurn/canNaturalPitch) -- at the cost of moving movement
|
|
* (forward/back/strafe) from the analog stick onto digital step buttons
|
|
* (digitalStepForward/Back/Left/Right, fed from C-buttons/D-pad). This is an
|
|
* ORIGINAL, player-selectable GE control style (not new game logic) -- we
|
|
* select it from the port the same way the options menu would
|
|
* (cur_player_set_control_type), and remap WASD/gamepad accordingly so
|
|
* movement keeps working under it. No src/ edits; no behavior invented that
|
|
* GE didn't already support. Escape hatch: Input.NaturalPitch=0 reverts to
|
|
* the 1.1/HONEY default with the D166 digital-pitch-pulse hack. */
|
|
extern int cur_player_get_control_type(void);
|
|
extern void cur_player_set_control_type(int type);
|
|
|
|
/* D194 absolute aim: live camera/projection accessors (src/fr.c, src/game/,
|
|
* port/src/video.c). All are plain reads of state owned by the game thread,
|
|
* sampled from inputComputePad which already runs in that same context
|
|
* (contSnapshotFromKeyboard <- osContStartReadData <- joy.c) -- no new
|
|
* cross-thread access, and nothing here writes. */
|
|
extern f32 viGetFovY(void);
|
|
extern s16 viGetViewWidth(void);
|
|
extern s16 viGetViewHeight(void);
|
|
extern s16 viGetViewLeft(void);
|
|
extern s16 viGetViewTop(void);
|
|
extern s16 getWidth320or440(void); /* CFB width the viewport rect lives in (320 NTSC) */
|
|
extern s16 getHeight330or240(void); /* CFB height (240 NTSC) */
|
|
extern f32 portScaleFovY(f32 fovy, s32 isTitleScreen);
|
|
extern s32 lvlGetCurrentStageToLoad(void);
|
|
#define CONTROLLER_CONFIG_HONEY_ 0
|
|
#define CONTROLLER_CONFIG_SOLITARE_ 1
|
|
#define MENU_POINTER_GAIN 1.5
|
|
#define TRIG_THRESHOLD (30 * 256)
|
|
#define RSTICK_THRESHOLD 0x4000
|
|
/* Mouse-look tuning. GE's aim model is mode-dependent (bondview2.c
|
|
* bondviewProcessInput):
|
|
* - Hipfire (!insightaimmode): yaw = analog stick-X ("natural turn"),
|
|
* pitch = DIGITAL C-up/C-down only (stick-Y is move fwd/back here).
|
|
* - Aim mode (R held): yaw AND pitch = analog stick pushed past
|
|
* +/-60 -> proportional (stick-60)/10. C-up/C-down mean crouch/lean/
|
|
* zoom in this mode, NOT aim -- so we must NOT emit them while aiming
|
|
* (that was D118c: aim + mouse-down -> crouch).
|
|
* GE's native pitch is inverted ("flight" style): C-up -> look down. We
|
|
* hide that so mouse-down looks down by default; MouseInvertY flips it. */
|
|
#define MOUSE_TURN_GAIN 6.0 /* hipfire: raw px this poll -> stick-X counts */
|
|
#define MOUSE_PITCH_THRESH 1.5 /* hipfire: px/poll before a C-button fires */
|
|
#define AIM_MOVE_THRESH 0.3 /* aim mode: px/poll before the stick moves */
|
|
#define HIP_PITCH_FULL 6.0 /* hipfire pitch: |px/poll| for a solid C hold (D166) */
|
|
|
|
/* D194(a) -- aim-mode response curve.
|
|
*
|
|
* bondviewPlayerControlStuff (bondview2.c) turns an aim-mode stick value
|
|
* into turn speed as `(stick_x - 60) / 10.0`, clamped to 1.0 -- i.e. the
|
|
* game's own proportional band is stick in [61,70]; anything from 71 up to
|
|
* whatever AimBand allows (60+AimBand, up to 100) clamps to the exact same
|
|
* max speed as 70. The old code aimed for the port's [61, 60+AimBand] range
|
|
* (up to [61,80]) as if it were all proportional, so a config'd AimBand>10
|
|
* bought nothing, and AIM_GAIN=4.0 reached the (effectively already-maxed)
|
|
* top of that range by ~5 px/poll -- "near bang-bang", matching the D194(a)
|
|
* complaint. Fix: curve-map px/poll onto the game's REAL proportional range
|
|
* [61,70] (independent of AimBand, which still acts as an extra ceiling for
|
|
* anyone who wants to cap below the game's own max), with a configurable
|
|
* gamma so slow motions land near the low end instead of jumping to nearly
|
|
* full speed immediately. */
|
|
#define AIM_STICK_MIN 61
|
|
#define AIM_STICK_GAME_MAX 70 /* bondview2.c: (stick-60)/10 saturates at stick=70 */
|
|
#define AIM_FULL_SPEED_PX 20.0 /* px/poll (at MouseAimSpeed=100) that reaches AIM_STICK_GAME_MAX */
|
|
|
|
/* D194 GEPD-style aim mapping. While RMB is held (cursor grabbed/hidden),
|
|
* mouse MOVEMENT rotates the view by the same screen angle swept: a
|
|
* sweep across the rendered viewport width sweeps the full horizontal FOV,
|
|
* height <-> fovy. The crosshair stays locked at screen centre and stop
|
|
* moving holds the view where it is (the game's own speed decay coasts it to
|
|
* rest). Because the scale is angle-per-pixel of the CURRENT fovy, scope
|
|
* zooms automatically become finer control. No target, no chase, no
|
|
* snap-back: placement is cumulative and path-dependent, exactly like GEPD.
|
|
*/
|
|
/* D194 GEPD-mirror aim constants (MouseInjectorPlugin/games/goldeneye.c): */
|
|
#define AIM_ABS_DEAD_PX 2.0 /* per-poll px below this is jitter, not aim */
|
|
#define GEPD_CROSSHAIR_LIMIT 5.159373283 /* crosshair pos units at the screen edge (GEPD 0x40A51996) */
|
|
#define GEPD_EDGE_THRESHOLD 0.72f /* |pos|/limit beyond which the view scrolls */
|
|
#define GEPD_SCROLL_SPEED 475.0 /* GEPD: (ratio-threshold)*475*timestep per tick */
|
|
#define GEPD_BASE_FOV 90.0f /* unzoomed FOV (our native); GEPD uses its 60 override */
|
|
|
|
/* D194(b) -- mouse sensitivity coupled to frame/poll rate.
|
|
*
|
|
* MOUSE_TURN_GAIN/AIM_GAIN above treat "px accumulated since the last
|
|
* inputComputePad(0) drain" as a fixed per-poll unit, but the real interval
|
|
* between drains drifts with render/scene load (D193 measured a rock-steady
|
|
* 60 sim ticks/s in the idle case, but the drain itself piggybacks on
|
|
* whatever cadence calls contSnapshotFromKeyboard() -- 1-2x per rendered
|
|
* frame per D165's comment -- so a hitch or a variable-length frame changes
|
|
* how much real time one "poll" of accumulated px represents). Net effect:
|
|
* the same physical hand motion emits a different turn rate depending on
|
|
* how ragged the poll cadence is, which reads as "sensitivity changes with
|
|
* movement/frame rate" (the user's own words).
|
|
*
|
|
* Fix: measure real elapsed time since the last drain with a monotonic
|
|
* clock and rescale the accumulated px by (dtRef / dtActual) before it
|
|
* hits MOUSE_TURN_GAIN/AIM_GAIN, where dtRef is the nominal 1/60s tick
|
|
* (matches D193's measured steady-state sim rate) -- so "px this poll"
|
|
* always means "px for a nominal 1/60s tick" regardless of how long that
|
|
* tick actually took in real time. Clamped both directions so one big
|
|
* stall (level load, GC pause) can't fling the view. Menu-cursor paths
|
|
* (D165/D169's P-controller, which has its own poll-count-based settling
|
|
* design) are deliberately NOT touched by this -- gameplay look only. */
|
|
#define MOUSE_DT_REF (1.0 / 60.0)
|
|
#define MOUSE_DT_SCALE_MIN 0.25 /* cap correction for an abnormally long poll gap */
|
|
#define MOUSE_DT_SCALE_MAX 4.0 /* cap correction for an abnormally short poll gap */
|
|
|
|
/* Item 1 (D165) — front-end 1:1 pointer. front.c frontUpdateControlStickPosition
|
|
* INTEGRATES the stick as a velocity into a screen-pixel cursor position
|
|
* (cursor_h_pos += (stickx*0.075 +/- 0.5) * delta, deadzone +/-5, clamp +/-70,
|
|
* cursor clamped into the ~320x240 virtual screen rect minus a 20px margin).
|
|
* Feeding it mouse *velocity* therefore gives velocity^2 feel. Instead we run a
|
|
* P-controller: keep our own estimate of where the game cursor is (menuEst*,
|
|
* integrated with the SAME recurrence as front.c), accumulate a target from
|
|
* mouse motion, and emit stick = clamp(GAIN*(target-est)). The estimate re-syncs
|
|
* to the real cursor whenever the target is held at a screen edge. */
|
|
#define MENU_CURSOR_LO 20.0
|
|
#define MENU_CURSOR_HI_H 300.0
|
|
#define MENU_CURSOR_HI_V 220.0
|
|
/* D345: after a front-end screen change, re-assert the 1:1 pointer for a few
|
|
* frames if the mouse was used within this many ms (see inputComputePad). */
|
|
#define MENU_POINTER_REASSERT_MS 1500.0
|
|
#define MENU_CURSOR_MID_H 160.0
|
|
#define MENU_CURSOR_MID_V 120.0
|
|
|
|
static void applyGrab(int want);
|
|
static void reconcileGrab(int menuMode);
|
|
static void applyCursorVisibility(void);
|
|
|
|
static int numControllers = 1;
|
|
static int connectedMask = 0x1; /* controller 0 always present */
|
|
|
|
static SDL_GameController *pads[MAX_PADS];
|
|
static int padShoulderPrev[MAX_PADS]; /* LB/RB edge state for weapon cycling */
|
|
static int padSelectPrev = 0; /* Select (BACK) edge: overlay toggle */
|
|
|
|
static int mouseEnabled = 1;
|
|
static int mouseGrabbed = 1; /* released while the window is unfocused */
|
|
|
|
/* D287: SDL's mouse-mode and cursor calls belong on the window's thread (the
|
|
* host thread that created it and pumps its events). Controller polls run on
|
|
* the scheduler thread (osContStartReadData -> inputComputePad ->
|
|
* reconcileGrab), so they called SDL_SetRelativeMouseMode / SDL_ShowCursor
|
|
* concurrently with the host thread's own focus/click handling. On Windows
|
|
* relative mode starts/stops SDL's raw-input thread and re-clips the cursor;
|
|
* two threads flipping it at once (clicking back into the window after being
|
|
* unfocused) can wedge. Callers now only record the wanted state; the host
|
|
* thread applies it in inputApplyMouseRequests() from videoPumpEvents(). The
|
|
* logical state (mouseGrabbed, deltas) still updates immediately. */
|
|
static SDL_SpinLock s_mouseReqLock;
|
|
static int s_reqRelative = -1, s_reqCursor = -1; /* -1: nothing pending */
|
|
static int s_appRelative = -1, s_appCursor = -1; /* host thread only */
|
|
|
|
static void mouseRequestRelative(int on)
|
|
{
|
|
SDL_AtomicLock(&s_mouseReqLock);
|
|
s_reqRelative = on ? 1 : 0;
|
|
SDL_AtomicUnlock(&s_mouseReqLock);
|
|
}
|
|
|
|
static void mouseRequestCursor(int show)
|
|
{
|
|
SDL_AtomicLock(&s_mouseReqLock);
|
|
s_reqCursor = show ? 1 : 0;
|
|
SDL_AtomicUnlock(&s_mouseReqLock);
|
|
}
|
|
|
|
/* Host thread only (videoPumpEvents, inputInit). */
|
|
void inputApplyMouseRequests(void)
|
|
{
|
|
SDL_AtomicLock(&s_mouseReqLock);
|
|
const int rel = s_reqRelative, cur = s_reqCursor;
|
|
s_reqRelative = s_reqCursor = -1;
|
|
SDL_AtomicUnlock(&s_mouseReqLock);
|
|
|
|
if (rel >= 0 && rel != s_appRelative) {
|
|
s_appRelative = rel;
|
|
SDL_SetRelativeMouseMode(rel ? SDL_TRUE : SDL_FALSE);
|
|
if (rel) {
|
|
SDL_GetRelativeMouseState(NULL, NULL); /* drain the accumulated jump */
|
|
}
|
|
}
|
|
if (cur >= 0 && cur != s_appCursor) {
|
|
s_appCursor = cur;
|
|
SDL_ShowCursor(cur ? SDL_ENABLE : SDL_DISABLE);
|
|
}
|
|
}
|
|
|
|
/* WI-1: Quake-style click-to-lock cursor capture. 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.
|
|
* D239/D192: the legacy always-grab mode (former Input.MouseCaptureMode=0)
|
|
* is removed -- it felt wrong on the file-select menu and its front-end
|
|
* pointer could not reach the outer grid cells. Click-to-lock is the only
|
|
* mode now; existing ini files setting the old key are ignored.
|
|
* Controller input is entirely independent of all of this. */
|
|
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 gepdSens = 38; /* D194 Input.GepdSens: GEPD SENSITIVITY setting, range 1..500
|
|
(D304: widened from 1..80 to match Input.MouseTurnSpeed)
|
|
(20 -> 25 "a bit slow" -> 30 user-calibrated match point
|
|
-> 38: user asked defaults ~20-35% faster than that) */
|
|
static int aimBand = 20; /* aim mode: usable stick range above the 60 gate */
|
|
/* D194/D238: default 100 -> 40 (M-123 user calibration). The old gain
|
|
* (MOUSE_TURN_GAIN=6 stick/px) saturated the game's quadratic natural-turn
|
|
* curve at ~13 px/poll, i.e. hipfire ran at full 315 deg/s for any normal
|
|
* movement while GEPD aim mode moves proportionally -- "too fast when I
|
|
* leave that mode". At 40% (2.4 stick/px) the mid-speed view rate matches
|
|
* the GEPD reticle's angular pace at GepdSens=30; flicks still reach full
|
|
* turn speed, only later. */
|
|
static int sensLink = 1; /* F10 overlay: keep aim/turn sens at the
|
|
stock 38:50 ratio (default on) */
|
|
static int mouseTurnSpeed = 50; /* hipfire yaw sensitivity, percent
|
|
(40 = M-123 match point at GepdSens=30; 50 = the user's
|
|
"~20-35% faster" default request, same +25% as gepdSens) */
|
|
static int menuPointerSpeed = 100; /* front-end cursor speed, percent */
|
|
static int mouseInvertY = 0; /* 1 = mouse-down looks up */
|
|
static int mouseYScale = 100; /* extra vertical (pitch) sensitivity, % */
|
|
static int mouseSmoothing = 0; /* 0 = raw; 1..90 = low-pass strength (%) */
|
|
static int mouseRawInput = 0; /* 1 = bypass OS pointer accel for aim */
|
|
static int mouseDtDecouple = 1; /* D194(b): normalize look sensitivity by real
|
|
* elapsed poll time instead of raw px/poll.
|
|
* Escape hatch: 0 = prior (coupled) behaviour. */
|
|
static int mouseSensitivity = 100; /* D238: single master sensitivity, percent --
|
|
* multiplies BOTH MouseAimSpeed and MouseTurnSpeed
|
|
* so one number tunes overall feel; the two legacy
|
|
* knobs stay as an independent per-mode trim on
|
|
* top of it (both default 100/16 => unchanged
|
|
* unless the user also touches those). */
|
|
static int aimCurveGamma = 160; /* D194(a): aim-mode response curve exponent x100
|
|
* (100 = linear, >100 = more low-speed control /
|
|
* less bang-bang, <100 = more twitchy). */
|
|
static int aimAbsolute = 1; /* D194: 1 = GEPD-style aim while RMB is held --
|
|
* crosshair locked at centre, mouse MOVEMENT
|
|
* rotates the view 1:1 in screen angle; 0 =
|
|
* legacy velocity stick from mouse delta. */
|
|
static int mouseDirectLook = 1; /* WI-1 (GEPD-INPUT-PLAN.md #89): 1 = hipfire
|
|
* writes vv_theta/vv_verta directly (see
|
|
* hipDirectCompute), same linear-in-px model
|
|
* as aim mode; 0 = legacy stick-curve path
|
|
* (MOUSE_TURN_GAIN), kept as an escape hatch. */
|
|
|
|
/* Input.PdMouseAim -- use the Perfect Dark port's mouse-aim model instead of
|
|
* GEPD's. Mirrors fgsfdsfgs/perfect_dark exactly (bondmove.c/bondgun.c): PD
|
|
* accumulates the mouse delta into a position (`swivelpos`, clamped [-1,1])
|
|
* and then pushes that POSITION into the game's crosshair integrator as its
|
|
* INPUT, with a near-zero crosshair damp (`bgunSwivelWithDamp(x, y, 0.01f)`).
|
|
*
|
|
* The near-zero damp is the whole point. GE's aim path uses the weapon's
|
|
* CrosshairSpeed (~0.8) -- a value chosen for the analog stick, where the
|
|
* integrator does the smoothing. With a mouse that slow integrator is what
|
|
* makes the drawn crosshair step: the port writes once per input poll while
|
|
* the game damps once per sim tick, so the displayed value is write*damp^k
|
|
* with k varying (findings D332). At damp 0.01 the input dominates and k stops
|
|
* mattering -- PD never had this bug because it never gave the mouse a slow
|
|
* damp.
|
|
*
|
|
* In this mode the port only ACCUMULATES; the game's own integrator does the
|
|
* rest, driven by the port-supplied turn through a #ifdef PORT hook in
|
|
* sub_GAME_7F067FBC (gunfire.c). Default off: it changes aim feel, so it is
|
|
* opt-in. */
|
|
static int pdMouseAim = 0;
|
|
|
|
/* D337 -- aim mode, folded (user decision 2026-09-24):
|
|
* Input.AimMode 0 = N64 (default): the N64 aim model -- crosshair travels,
|
|
* camera edge-scrolls -- with the mouse fed through the game's own
|
|
* crosshair integrator at PD's mouse damp (D332 / PR #96). Same range,
|
|
* edge-scroll and bullet mapping as the old GEPD overwrite, without its
|
|
* poll-vs-tick step jitter.
|
|
* Input.AimMode 1 = Centred (PC, opt-in, #104): the mouse turns the camera
|
|
* and the crosshair stays centred. Not an N64 behaviour; kept for players
|
|
* who asked for FPS-style aiming.
|
|
* Input.AimLegacyGepd=1 (hidden, one release) restores the old GEPD overwrite
|
|
* for the N64 mode. GE_PDMOUSEAIM=0/1 still forces the integration at launch.
|
|
* Legacy keys: Input.AimStyle 2 (Centred) migrates to AimMode 1; AimStyle 0/1
|
|
* and Input.PdMouseAim map to the default N64 mode. */
|
|
enum { AIMMODE_N64 = 0, AIMMODE_CENTRED = 1 };
|
|
static int aimMode = AIMMODE_N64;
|
|
static int aimLegacyGepd = 0;
|
|
static int aimStyleLegacy = 0; /* Input.AimStyle (D333, superseded) */
|
|
|
|
static int aimModeGet(void)
|
|
{
|
|
if (aimStyleLegacy == 2 && aimMode == AIMMODE_N64) { /* one-time migration */
|
|
aimMode = AIMMODE_CENTRED;
|
|
}
|
|
aimStyleLegacy = 0;
|
|
pdMouseAim = 0;
|
|
return aimMode;
|
|
}
|
|
|
|
/* D337: which device drove aim last. PD picks its crosshair damp per input
|
|
* device; the mouse integration must not swallow a gamepad's stick aim just
|
|
* because the mouse happens to be grabbed. Set by mouse motion, cleared by a
|
|
* deflected pad stick (inputComputePad). */
|
|
static int s_aimDevMouse = 0;
|
|
|
|
/* D338: Input.AimRange -- how far the N64-mode crosshair can travel.
|
|
* 0 = PC (default, the GEPD/mouse-injector feel): the crosshair reaches the
|
|
* screen edge, camera edge-scroll from 72% of the way there.
|
|
* 1 = N64: the original stick limits. N64 aim turns the crosshair by
|
|
* stick*0.65/80 per tick, so full deflection settles at 65% of the
|
|
* half-width whatever the weapon damp, and the camera starts turning past
|
|
* stick 60/80 -- 75% of that range (~49% of the half-width). */
|
|
static int aimRange = 0;
|
|
static double aimRangeScale(void) { return aimRange ? 0.65 : 1.0; }
|
|
static double aimEdgeThreshold(void) { return aimRange ? 0.75 : (double)GEPD_EDGE_THRESHOLD; }
|
|
|
|
static int pdMouseAimEnabled(void)
|
|
{
|
|
const char *e = GE_ENVSTR("GE_PDMOUSEAIM");
|
|
if (e) return atoi(e) != 0;
|
|
return aimModeGet() == AIMMODE_N64 && !aimLegacyGepd;
|
|
}
|
|
|
|
/* D194 GEPD-aim state. Touched ONLY in inputComputePad (game thread), the
|
|
* same confinement as every other static here. The per-poll grabbed delta
|
|
* drives the view, so there is no free-running angle accumulator for
|
|
* multi-tick catch-up (D193) to double-consume; the frac carry only dithers
|
|
* this poll's stick quantization. */
|
|
/* D194 GEPD-mirror aim state: the crosshair position accumulator in game
|
|
* units (±GEPD_CROSSHAIR_LIMIT at the screen edge). Written straight into
|
|
* g_CurrentPlayer->crosshair_x/y_pos + gun_azimuth_angle/turning each tick;
|
|
* bondview2's damped crosshair update keeps running and is simply
|
|
* overwritten each tick (GEPD model, D194). */
|
|
static double s_gepdCrossX = 0.0, s_gepdCrossY = 0.0;
|
|
static int s_gepdHeldPrev = 0; /* aim held last tick -> adopt on entry */
|
|
static int aimGepdAccumulate(double dxPx, double dyLook);
|
|
static void aimGepdEdgeScroll(void);
|
|
static int aimGepdCompute(double dxPx, double dyLook);
|
|
static int hipDirectCompute(double dxPx, double dyLook);
|
|
/* D194: bondview2's "look-ahead" pitch centreing (docentreupdown) arms during
|
|
* hip-fire walking whenever the pitch strays from the horizon target, and --
|
|
* once armed -- keeps pulling vv_verta back to it even in aim mode, EXCEPT
|
|
* while a manual pitch stick (|stick_y|>60) is present. That reads as "the
|
|
* gun always tries to return to centre" the moment the mouse stops. Port-
|
|
* side fix: on entering aim with the spring armed, emit a minimal 2-tick
|
|
* pitch nudge in the direction of current motion so the game's own rule
|
|
* (manual input clears docentreupdown) disarms it; aiming is then free and
|
|
* holds when the mouse stops. No game logic touched -- this is just what
|
|
* stick we choose to present. */
|
|
static int s_aimHeldPrev = 0;
|
|
static int s_centreClearTicks = 0;
|
|
/* D194: always 0 since the free-cursor experiment was reverted (aim uses
|
|
* grabbed relative deltas). Kept because reconcileGrab(),
|
|
* applyCursorVisibility() and the mouse-button mask still branch on it --
|
|
* with it pinned to 0 they take exactly the pre-D194 paths. */
|
|
static int s_absAimSuspend = 0;
|
|
|
|
static int aimAbsCompute(double dxPx, double dyPx, int *outSx, int *outSy); /* D194 */
|
|
static int naturalPitchMode = 1; /* D194/D238: 1 = force GE's own 1.2/SOLITARE
|
|
* control style for continuous analog pitch;
|
|
* 0 = legacy 1.1/HONEY + D166 digital pulse. */
|
|
static Uint64 s_lastLookPollCounter = 0; /* D194(b): monotonic clock, gameplay-look drain only */
|
|
|
|
/* Gamepad tuning -- defaults reproduce the old hardcoded constants exactly. */
|
|
static int padDeadzone = STICK_DEADZONE; /* left-stick deadzone, raw 0..32767 */
|
|
static int padTriggerPct = 23; /* trigger press point, % of travel (~30*256) */
|
|
static int padLookInvertY = 0; /* 1 = invert right-stick (look) Y */
|
|
|
|
/* Smoothed mouse delta carried between polls when mouseSmoothing > 0. */
|
|
static double mouseSmDX = 0.0, mouseSmDY = 0.0;
|
|
|
|
/* Raw relative-mouse delta accumulated since the last inputComputePad(0).
|
|
* NOT a persistent aim accumulator: mouse-look is a displacement device and
|
|
* GE's aim is a rate device, so we consume the whole delta each poll and
|
|
* reset -- stop moving and the stick/ C-button releases immediately. */
|
|
static double mouseDX = 0.0;
|
|
static double mouseDY = 0.0;
|
|
|
|
/* Mouse-wheel -> weapon cycle (D223): directional, like a normal PC shooter.
|
|
* The port forces CONTROLLER_CONFIG_HONEY (bondview.c:1484), so GE's default
|
|
* scheme applies (bondview2.c:5177-5179): invButtons = A_BUTTON,
|
|
* shootButtons = Z_TRIG, and the cycle signals are (bondview2.c:5337-5346):
|
|
* forward = fresh A edge while Z is NOT held
|
|
* backward = Z fresh edge while A IS held (the N64 "hold A, tap Z" trick)
|
|
* moveData.triggerOn (bondview2.c:5419) requires A to be released, so the
|
|
* synthesized Z edge in the backward sequence can never fire a shot.
|
|
*
|
|
* wheelFwd: remaining polls of the A pulse (clean press+release edge).
|
|
* wheelBack: 2 = present A-only next poll; 1 = present A+Z next poll (the
|
|
* fresh-Z-edge-while-A-held poll). A must already read "held"
|
|
* oldbuttons-wise before Z's edge, hence the two-poll sequence. */
|
|
#define WHEEL_FWD_POLLS 2
|
|
static int wheelFwd = 0;
|
|
static int wheelBack = 0;
|
|
|
|
/* Item 1 (D165) — front-end pointer P-controller state. */
|
|
static int menuPointerMode = 1; /* 0 = legacy velocity, 1 = 1:1 pointer */
|
|
static int hipfirePitchSpeed = 100; /* D166: hipfire pitch pulse rate, percent */
|
|
static int menuPrevActive = 0;
|
|
static double hipPitchPhase = 0.0; /* D166: hipfire pitch pulse phase 0..1 */
|
|
static int lastMenuMouseX = -1, lastMenuMouseY = -1; /* WI-2: last abs cursor seen in a menu */
|
|
/* D345: carry pointer ownership across front-end screen changes. The game
|
|
* teleports cursor_h/v_pos on some transitions (file select -> mode select
|
|
* calls setCursorPOSforMode(0), front.c:2516); the absolute write below only
|
|
* fires while the OS mouse moves, so with an idle mouse the snap persists
|
|
* until the next movement. lastMenuId detects the change; transitionFrames is
|
|
* a short countdown during which a recent-mouse re-assert is allowed;
|
|
* lastAbsWrite is the perf-counter stamp of the last absolute write (mouse
|
|
* recency). */
|
|
static int menuPointerLastMenuId = -1;
|
|
static int menuPointerTransitionFrames = 0;
|
|
static Uint64 menuPointerLastAbsWrite = 0;
|
|
/* D345(b): 1 while the 1:1 pointer can own cursor_h/v_pos this poll
|
|
* (menu, mouse enabled + grabbed). Refreshed every pad-0 poll. */
|
|
static int s_menuPointerLive = 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------
|
|
* Scripted input (test harness, port-only). GE_INPUTSCRIPT lets a headless
|
|
* run walk the front-end / pause menus with no human at the keyboard.
|
|
*
|
|
* GE_INPUTSCRIPT="120:START;180:A;240:A;600:SDOWN;900:SNONE,A"
|
|
*
|
|
* Each entry is `<frame>:<tok>[,<tok>...]`. Buttons (A B Z START L R UP DOWN
|
|
* LEFT RIGHT CUP CDOWN CLEFT CRIGHT) pulse for INPUTSCRIPT_PULSE controller
|
|
* reads from <frame>. Analog-stick tokens (SUP SDOWN SLEFT SRIGHT) are
|
|
* SUSTAINED: the stick stays deflected until a later entry changes it; SNONE
|
|
* re-centres it. "Frame" = count of controller-0 reads since launch (roughly
|
|
* 2 per rendered frame -- watch GE_INPUTLOG to calibrate). Unset env => no
|
|
* effect; when set it is the ONLY controller-0 input source. */
|
|
#define INPUTSCRIPT_MAX 64
|
|
#define INPUTSCRIPT_PULSE 6
|
|
|
|
struct scriptEntry { long frame; unsigned mask; int sx, sy; int hasStick;
|
|
int hasMouse, mdx, mdy; int hasHold, hold; /* D337 mouse/aim tokens */
|
|
int hasZHold, zhold; }; /* D207: sustained fire */
|
|
static struct scriptEntry scriptEntries[INPUTSCRIPT_MAX];
|
|
static int scriptCount = -1; /* -1 = not parsed yet, 0 = parsed empty */
|
|
static long scriptFrame = 0;
|
|
static int scriptCurSX = 0; /* stick set by the last scriptApply() */
|
|
static int scriptCurSY = 0;
|
|
|
|
/* Apply one token to `e`. Buttons: A B Z START L R UP DOWN LEFT RIGHT CUP
|
|
* CDOWN CLEFT CRIGHT (D-pad/C-buttons). Analog stick: SUP SDOWN SLEFT SRIGHT
|
|
* (full +/-80 deflection -- moves menu cursors). */
|
|
static void scriptApplyToken(struct scriptEntry *e, const char *s, int n)
|
|
{
|
|
struct { const char *k; unsigned v; } btn[] = {
|
|
{"A",GE_CONT_A}, {"B",GE_CONT_B}, {"Z",GE_CONT_G}, {"START",GE_CONT_START},
|
|
{"L",GE_CONT_L}, {"R",GE_CONT_R}, {"UP",GE_CONT_UP}, {"DOWN",GE_CONT_DOWN},
|
|
{"LEFT",GE_CONT_LEFT}, {"RIGHT",GE_CONT_RIGHT},
|
|
{"CUP",GE_CONT_E}, {"CDOWN",GE_CONT_D}, {"CLEFT",GE_CONT_C}, {"CRIGHT",GE_CONT_F},
|
|
};
|
|
for (size_t i = 0; i < sizeof(btn)/sizeof(btn[0]); ++i) {
|
|
if ((int)strlen(btn[i].k) == n && SDL_strncasecmp(btn[i].k, s, n) == 0) {
|
|
e->mask |= btn[i].v;
|
|
return;
|
|
}
|
|
}
|
|
/* D337: sustained scripted mouse (px per poll) and aim hold, for headless
|
|
* aim-model measurements: MDX<n> MDY<n> (latest entry wins, 0 stops),
|
|
* RHOLD / RREL (hold / release the game's R aim button). */
|
|
if (n > 3 && (SDL_strncasecmp("MDX", s, 3) == 0 || SDL_strncasecmp("MDY", s, 3) == 0)) {
|
|
char num[16]; int k = n - 3; if (k > 15) k = 15;
|
|
memcpy(num, s + 3, k); num[k] = 0;
|
|
e->hasMouse = 1;
|
|
if (s[2] == 'X' || s[2] == 'x') e->mdx = atoi(num); else e->mdy = atoi(num);
|
|
return;
|
|
}
|
|
if (n == 5 && SDL_strncasecmp("RHOLD", s, 5) == 0) { e->hasHold = 1; e->hold = 1; return; }
|
|
if (n == 5 && SDL_strncasecmp("LHOLD", s, 5) == 0) { e->hasHold = 1; e->hold = 2; return; }
|
|
if (n == 4 && SDL_strncasecmp("RREL", s, 4) == 0) { e->hasHold = 1; e->hold = 0; return; }
|
|
/* D207: ZHOLD / ZREL hold / release fire (Z) for automatic weapons;
|
|
* independent of RHOLD so aim + fire can be held together. */
|
|
if (n == 5 && SDL_strncasecmp("ZHOLD", s, 5) == 0) { e->hasZHold = 1; e->zhold = 1; return; }
|
|
if (n == 4 && SDL_strncasecmp("ZREL", s, 4) == 0) { e->hasZHold = 1; e->zhold = 0; return; }
|
|
e->hasStick = 1;
|
|
if (n == 3 && SDL_strncasecmp("SUP", s, 3) == 0) { e->sy = STICK_MAX; return; }
|
|
if (n == 5 && SDL_strncasecmp("SDOWN", s, 5) == 0) { e->sy = -STICK_MAX; return; }
|
|
if (n == 5 && SDL_strncasecmp("SLEFT", s, 5) == 0) { e->sx = -STICK_MAX; return; }
|
|
if (n == 6 && SDL_strncasecmp("SRIGHT", s, 6) == 0) { e->sx = STICK_MAX; return; }
|
|
if (n == 5 && SDL_strncasecmp("SNONE", s, 5) == 0) { return; } /* recentre */
|
|
e->hasStick = 0;
|
|
sysLogPrintf(LOG_WARNING, "GE_INPUTSCRIPT: unknown token '%.*s'", n, s);
|
|
}
|
|
|
|
static void scriptParse(void)
|
|
{
|
|
scriptCount = 0;
|
|
const char *env = getenv("GE_INPUTSCRIPT");
|
|
if (!env || !*env) {
|
|
return;
|
|
}
|
|
const char *p = env;
|
|
while (*p && scriptCount < INPUTSCRIPT_MAX) {
|
|
char *end = NULL;
|
|
long fr = strtol(p, &end, 10);
|
|
if (end == p || *end != ':') {
|
|
sysLogPrintf(LOG_WARNING, "GE_INPUTSCRIPT: bad entry near '%s'", p);
|
|
break;
|
|
}
|
|
p = end + 1;
|
|
struct scriptEntry *e = &scriptEntries[scriptCount];
|
|
e->frame = fr;
|
|
e->mask = 0;
|
|
e->sx = e->sy = 0;
|
|
e->hasStick = 0;
|
|
e->hasMouse = e->mdx = e->mdy = 0;
|
|
e->hasHold = e->hold = 0;
|
|
e->hasZHold = e->zhold = 0;
|
|
while (*p && *p != ';') {
|
|
const char *tok = p;
|
|
while (*p && *p != ',' && *p != ';') ++p;
|
|
scriptApplyToken(e, tok, (int)(p - tok));
|
|
if (*p == ',') ++p;
|
|
}
|
|
if (*p == ';') ++p;
|
|
scriptCount++;
|
|
}
|
|
sysLogPrintf(LOG_INFO, "GE_INPUTSCRIPT: %d entr%s parsed",
|
|
scriptCount, scriptCount == 1 ? "y" : "ies");
|
|
}
|
|
|
|
static int scriptIsActive(void)
|
|
{
|
|
if (scriptCount < 0) {
|
|
scriptParse();
|
|
}
|
|
return scriptCount > 0;
|
|
}
|
|
|
|
/* When a script is loaded it is the SOLE source of controller-0 input: real
|
|
* keyboard/mouse/pad is ignored so headless menu walks are deterministic
|
|
* (a relative-mouse SDL window with no focus otherwise spews phantom deltas).
|
|
* Returns the scripted button mask for the current frame; advances the frame
|
|
* counter (call exactly once per controller-0 read). */
|
|
/* D337: current sustained scripted mouse delta / aim hold (latest entry). */
|
|
static int s_scriptMouseOn = 0, s_scriptMDX = 0, s_scriptMDY = 0, s_scriptHold = 0, s_scriptZHold = 0;
|
|
static void scriptPreMouse(void)
|
|
{
|
|
long bestM = -1, bestH = -1, bestZ = -1;
|
|
for (int i = 0; i < scriptCount; ++i) {
|
|
long d = scriptFrame - scriptEntries[i].frame;
|
|
if (d < 0) continue;
|
|
if (scriptEntries[i].hasMouse && scriptEntries[i].frame > bestM) {
|
|
bestM = scriptEntries[i].frame;
|
|
s_scriptMouseOn = 1;
|
|
s_scriptMDX = scriptEntries[i].mdx;
|
|
s_scriptMDY = scriptEntries[i].mdy;
|
|
}
|
|
if (scriptEntries[i].hasHold && scriptEntries[i].frame > bestH) {
|
|
bestH = scriptEntries[i].frame;
|
|
s_scriptHold = scriptEntries[i].hold;
|
|
}
|
|
if (scriptEntries[i].hasZHold && scriptEntries[i].frame > bestZ) {
|
|
bestZ = scriptEntries[i].frame;
|
|
s_scriptZHold = scriptEntries[i].zhold;
|
|
}
|
|
}
|
|
}
|
|
|
|
static unsigned scriptApply(unsigned button)
|
|
{
|
|
if (!scriptIsActive()) {
|
|
return button;
|
|
}
|
|
scriptPreMouse(); /* D207: refresh sustained holds even off the mouse path (idempotent) */
|
|
unsigned m = 0;
|
|
long bestStickFrame = -1;
|
|
for (int i = 0; i < scriptCount; ++i) {
|
|
long d = scriptFrame - scriptEntries[i].frame;
|
|
if (d >= 0 && d < INPUTSCRIPT_PULSE) {
|
|
m |= scriptEntries[i].mask;
|
|
}
|
|
/* sustained stick: the latest-starting entry that carried a stick token */
|
|
if (d >= 0 && scriptEntries[i].hasStick && scriptEntries[i].frame > bestStickFrame) {
|
|
bestStickFrame = scriptEntries[i].frame;
|
|
scriptCurSX = scriptEntries[i].sx;
|
|
scriptCurSY = scriptEntries[i].sy;
|
|
}
|
|
}
|
|
if (s_scriptHold == 1) m |= GE_CONT_R; /* D337 RHOLD */
|
|
if (s_scriptHold == 2) m |= GE_CONT_L; /* D337 LHOLD (Q / "LeanLeft") */
|
|
if (s_scriptZHold) m |= GE_CONT_G; /* D207 ZHOLD (fire) */
|
|
scriptFrame++;
|
|
return m;
|
|
}
|
|
|
|
static void inputRebuildBinds(void); /* D214; defined below with keyDown() */
|
|
|
|
int inputInit(void)
|
|
{
|
|
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();
|
|
|
|
inputRebuildBinds(); /* D214: parse [Bind] now that configLoad() has run */
|
|
|
|
/* Relative mouse mode for mouse-look. Click-to-lock: we start released
|
|
* and wait for a click in the window (video.c -> inputNotifyClick). */
|
|
mouseGrabbed = 0;
|
|
if (mouseEnabled) {
|
|
if (mouseRawInput) {
|
|
/* Feed the raw device delta straight through: no OS pointer
|
|
* acceleration, no warp-based emulation. Must be set before
|
|
* relative mode is enabled. */
|
|
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE, "0");
|
|
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "0");
|
|
sysLogPrintf(LOG_INFO, "input: raw mouse input (no OS pointer accel)");
|
|
}
|
|
mouseRequestRelative(mouseGrabbed);
|
|
}
|
|
|
|
applyCursorVisibility(); /* hide the OS cursor if we start focused */
|
|
inputApplyMouseRequests(); /* inputInit runs on the host thread */
|
|
|
|
sysLogPrintf(LOG_INFO, "input: ready (mask=0x%x, %d controller(s), aimSpeed=%d)",
|
|
connectedMask, numControllers, mouseAimSpeed);
|
|
return connectedMask;
|
|
}
|
|
|
|
void inputDestroy(void)
|
|
{
|
|
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)
|
|
{
|
|
SDL_GameControllerUpdate();
|
|
|
|
/* D287: skip until the host thread has actually enabled relative mode
|
|
* (a benign racy read of a host-thread int), so the few ms between the
|
|
* logical grab and its application do not read absolute cursor motion. */
|
|
if (!mouseEnabled || !mouseGrabbed || s_appRelative != 1) {
|
|
return;
|
|
}
|
|
|
|
int dx = 0, dy = 0;
|
|
SDL_GetRelativeMouseState(&dx, &dy);
|
|
|
|
/* Raw px; per-mode sensitivity is applied in inputComputePad(). Accumulate
|
|
* in case inputUpdate() is polled more than once between pad reads. */
|
|
mouseDX += dx;
|
|
mouseDY += dy;
|
|
}
|
|
|
|
static int scaleAxis(int v)
|
|
{
|
|
int dz = padDeadzone;
|
|
if (dz < 0) dz = 0;
|
|
if (dz > 30000) dz = 30000;
|
|
if (v > -dz && v < dz) {
|
|
return 0;
|
|
}
|
|
if (v < 0) v += dz; else v -= dz;
|
|
int out = (int)((long)v * STICK_MAX / (32767 - dz));
|
|
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 && sc != SDL_SCANCODE_UNKNOWN && ks[sc];
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------
|
|
* D214 — keyboard rebinding (PD parity: config-string driven, no UI).
|
|
*
|
|
* Each gameplay action maps to a comma-separated list of SDL scancode names
|
|
* (as printed by SDL_GetScancodeName: "W", "Up", "Left Ctrl", "Space", ...).
|
|
* The defaults reproduce the previously-hardcoded FPS layout exactly, so a
|
|
* fresh or [Bind]-less ini changes nothing. Parsed once in inputInit(), after
|
|
* configLoad(). Mouse buttons (fire = LMB, aim = RMB) stay hardwired.
|
|
* ---------------------------------------------------------------------- */
|
|
enum {
|
|
IA_FORWARD, IA_BACK, IA_STRAFE_L, IA_STRAFE_R, IA_TURN_L, IA_TURN_R,
|
|
IA_FIRE, IA_AIM, IA_ACTION, IA_CANCEL, IA_LEAN_L, IA_START,
|
|
/* v0.4.0 M3 (modern options wave): GEPD-layout actions. Both map to
|
|
* the N64 B button -- the game's own B-context logic
|
|
* (bond_interact_object) picks reload (tap, gun in hand, no
|
|
* interact target) vs crouch (held) exactly as on the N64. */
|
|
IA_RELOAD, IA_CROUCH, IA_COUNT
|
|
};
|
|
|
|
static const struct { const char *key; const char *def; } kBindDefs[IA_COUNT] = {
|
|
[IA_FORWARD] = { "Input.Bind.Forward", "W,Up" },
|
|
[IA_BACK] = { "Input.Bind.Back", "S,Down" },
|
|
[IA_STRAFE_L] = { "Input.Bind.StrafeLeft", "A" },
|
|
[IA_STRAFE_R] = { "Input.Bind.StrafeRight", "D" },
|
|
[IA_TURN_L] = { "Input.Bind.TurnLeft", "Left" },
|
|
[IA_TURN_R] = { "Input.Bind.TurnRight", "Right" },
|
|
[IA_FIRE] = { "Input.Bind.Fire", "Left Ctrl" },
|
|
[IA_AIM] = { "Input.Bind.Aim", "Left Shift" },
|
|
[IA_ACTION] = { "Input.Bind.Action", "Space,Z,E" },
|
|
[IA_CANCEL] = { "Input.Bind.Cancel", "X,R,F,Escape" },
|
|
[IA_LEAN_L] = { "Input.Bind.LeanLeft", "Q" },
|
|
[IA_START] = { "Input.Bind.Start", "Return,Tab" },
|
|
[IA_RELOAD] = { "Input.Bind.Reload", "" },
|
|
[IA_CROUCH] = { "Input.Bind.Crouch", "" },
|
|
};
|
|
|
|
/* v0.4.0 M3: GEPD key-layout preset (docs/dev/notes/GEPORT-REFERENCE-
|
|
* DEEPDIVE.md section 7.3). The preset is the EFFECTIVE DEFAULT for any action
|
|
* the user has not overridden per-key in [Bind]; a per-key ini value
|
|
* layers on top, so switching layouts never clobbers explicit binds.
|
|
* Fire loses its Left Ctrl key in this layout (LMB fires, RMB aims;
|
|
* Ctrl becomes crouch); LeanLeft is unbound (lean is GEPD-removed).
|
|
* NULL entries keep the FPS default. */
|
|
static const char *const kGepdPreset[IA_COUNT] = {
|
|
[IA_FIRE] = "",
|
|
[IA_ACTION] = "Q",
|
|
[IA_CANCEL] = "E",
|
|
[IA_LEAN_L] = "",
|
|
[IA_RELOAD] = "R",
|
|
[IA_CROUCH] = "Left Ctrl",
|
|
};
|
|
static int keyLayout = 0; /* 0 = FPS defaults, 1 = GEPD preset */
|
|
static int crouchMode = 0; /* 0 = hold, 1 = toggle (latched B) */
|
|
static int s_crouchLatch = 0;
|
|
static int s_crouchHeldPrev = 0;
|
|
|
|
#define BIND_MAX_KEYS 4
|
|
static char g_bindStr[IA_COUNT][64];
|
|
static SDL_Scancode g_bind[IA_COUNT][BIND_MAX_KEYS];
|
|
|
|
static void inputRebuildBinds(void)
|
|
{
|
|
for (int a = 0; a < IA_COUNT; a++) {
|
|
for (int k = 0; k < BIND_MAX_KEYS; k++) {
|
|
g_bind[a][k] = SDL_SCANCODE_UNKNOWN;
|
|
}
|
|
/* v0.4.0 M3: the GEPD preset fills the actions the user has not
|
|
* individually overridden (empty g_bindStr); FPS layout = the
|
|
* kBindDefs defaults, byte-identical to the pre-wave binds. */
|
|
const char *def = kBindDefs[a].def;
|
|
if (keyLayout == 1) {
|
|
const char *preset = kGepdPreset[a];
|
|
if (preset) def = preset;
|
|
}
|
|
char buf[64];
|
|
strncpy(buf, g_bindStr[a][0] ? g_bindStr[a] : def, sizeof(buf) - 1);
|
|
buf[sizeof(buf) - 1] = 0;
|
|
int hadInput = (buf[0] != 0); /* before strtok clobbers buf */
|
|
|
|
int n = 0;
|
|
for (char *tok = strtok(buf, ","); tok && n < BIND_MAX_KEYS; tok = strtok(NULL, ",")) {
|
|
while (*tok == ' ' || *tok == '\t') tok++;
|
|
char *end = tok + strlen(tok);
|
|
while (end > tok && (end[-1] == ' ' || end[-1] == '\t')) *--end = 0;
|
|
if (!*tok) continue;
|
|
SDL_Scancode sc = SDL_GetScancodeFromName(tok);
|
|
if (sc == SDL_SCANCODE_UNKNOWN) {
|
|
sysLogPrintf(LOG_WARNING, "input: %s: unknown key name '%s'",
|
|
kBindDefs[a].key, tok);
|
|
continue;
|
|
}
|
|
g_bind[a][n++] = sc;
|
|
}
|
|
/* v0.4.0 M3: an intentionally-empty string (a preset or default
|
|
* that leaves an action unbound, e.g. GEPD Fire) is silent --
|
|
* only a non-empty string that produced no valid keys warns. */
|
|
if (n == 0 && hadInput) {
|
|
sysLogPrintf(LOG_WARNING, "input: %s has no valid keys; action unbound",
|
|
kBindDefs[a].key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* v0.4.0 M3: re-derive the binds after an F10 change to Input.Layout /
|
|
* Input.CrouchMode (optionsoverlay rowSetCommit hook, scheduler thread).
|
|
* g_bind is read per-frame by the game thread's actHeld(): a mid-poll
|
|
* rebuild can at worst drop or add one scancode for one frame (the
|
|
* D214 startup-rebuild is the same thread-pairing, just at boot).
|
|
* Also drops the crouch latch so a latched crouch never carries B into
|
|
* a menu (where B = back/cancel). */
|
|
void inputLayoutApply(void)
|
|
{
|
|
s_crouchLatch = 0;
|
|
s_crouchHeldPrev = 0;
|
|
inputRebuildBinds();
|
|
sysLogPrintf(LOG_INFO, "input: key layout %s, crouch %s applied",
|
|
keyLayout ? "GEPD" : "FPS", crouchMode ? "toggle" : "hold");
|
|
}
|
|
|
|
static int actHeld(const Uint8 *ks, int act)
|
|
{
|
|
for (int k = 0; k < BIND_MAX_KEYS; k++) {
|
|
if (keyDown(ks, g_bind[act][k])) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* 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;
|
|
|
|
/* D194/D238: self-correcting every poll -- cheap (plain field writes,
|
|
* see options.c cur_player_set_control_type), and re-asserts itself if
|
|
* anything else ever calls the setter (menu, save load) in between. */
|
|
if (idx == 0 && g_CurrentPlayer != NULL) {
|
|
int wantSolitare = naturalPitchMode ? CONTROLLER_CONFIG_SOLITARE_ : CONTROLLER_CONFIG_HONEY_;
|
|
if (cur_player_get_control_type() != wantSolitare) {
|
|
cur_player_set_control_type(wantSolitare);
|
|
}
|
|
}
|
|
|
|
if (idx < 0 || idx >= MAX_PADS) {
|
|
if (stick_x) *stick_x = 0;
|
|
if (stick_y) *stick_y = 0;
|
|
return 0;
|
|
}
|
|
|
|
/* F10 options overlay: while it is open, controller 0 is fully swallowed
|
|
* (neutral pad, no stick) and the nav keys / wheel / gamepad drive the
|
|
* overlay instead. Mirrors the WI-1 "cursor free in a stage -> withhold
|
|
* input" pattern. Controllers 1-3 are untouched. */
|
|
if (idx == 0 && optionsOverlayIsOpen()) {
|
|
/* Select closes the overlay. padSelectPrev is tracked on this path
|
|
* and the open path below alike, so a button held across the
|
|
* transition cannot immediately re-toggle it. */
|
|
int selNow = pads[0] ? SDL_GameControllerGetButton(pads[0],
|
|
SDL_CONTROLLER_BUTTON_BACK) : 0;
|
|
if (selNow && !padSelectPrev) optionsOverlayToggle();
|
|
padSelectPrev = selNow;
|
|
optionsOverlayHandleInput();
|
|
if (stick_x) *stick_x = 0;
|
|
if (stick_y) *stick_y = 0;
|
|
s_menuPointerLive = 0; /* D345(b): the overlay owns the mouse */
|
|
return 0;
|
|
}
|
|
|
|
/* ---- keyboard + mouse: controller 0 only ---- */
|
|
if (idx == 0) {
|
|
const Uint8 *ks = SDL_GetKeyboardState(NULL);
|
|
Uint32 mb = mouseEnabled ? SDL_GetMouseState(NULL, NULL) : 0;
|
|
int menuMode = (current_menu != GE_MENU_RUN_STAGE &&
|
|
current_menu != GE_MENU_INVALID);
|
|
|
|
/* D345: detect front-end screen changes (any current_menu flip,
|
|
* including into MENU_PC_OPTIONS / the F10 overlay's parent state)
|
|
* and open a short re-assert window. Runs every poll so the
|
|
* countdown drains even when the pointer block below is skipped
|
|
* (mouse disabled, unfocused window, legacy velocity mode). */
|
|
if ((int)current_menu != menuPointerLastMenuId) {
|
|
menuPointerLastMenuId = (int)current_menu;
|
|
menuPointerTransitionFrames = 3; /* this poll + two more */
|
|
}
|
|
if (menuPointerTransitionFrames > 0) menuPointerTransitionFrames--;
|
|
|
|
/* D194: aim mode drives the view from GRABBED relative deltas (see
|
|
* aimAbsCompute) -- the cursor stays hidden and clipped to the window
|
|
* for the whole hold. The earlier free-cursor experiment (suspend the
|
|
* grab, read absolute position) is reverted: the visible cursor could
|
|
* leave the window and OS micro-jitter read as view jitter.
|
|
* s_absAimSuspend therefore stays 0; the readers that honour it
|
|
* (reconcileGrab/applyCursorVisibility/button mask) behave exactly as
|
|
* pre-D194. */
|
|
s_absAimSuspend = 0;
|
|
reconcileGrab(menuMode);
|
|
/* D337 harness: scripted sustained mouse replaces the real deltas and
|
|
* behaves as a grabbed mouse (headless windows are never focused). */
|
|
if (scriptIsActive()) {
|
|
scriptPreMouse();
|
|
if (s_scriptMouseOn) {
|
|
mouseDX = (double)s_scriptMDX;
|
|
mouseDY = (double)s_scriptMDY;
|
|
mouseGrabbed = 1;
|
|
}
|
|
}
|
|
/* D196: the F10 options overlay forces the OS cursor visible via
|
|
* inputSuspendForOverlay() but nothing re-hides it on close unless
|
|
* reconcileGrab() happens to re-grab (only true if you had already
|
|
* clicked-to-lock in a stage). Re-assert the correct visibility
|
|
* every poll here -- this line is unreachable while the overlay is
|
|
* open (early return above), so it only fires once it has closed. */
|
|
applyCursorVisibility();
|
|
|
|
/* 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. */
|
|
/* ...but not while an aim hold has the cursor out for absolute aim:
|
|
* then the buttons are intentional (RMB is the hold itself, LMB fires). */
|
|
if (!mouseGrabbed && !menuMode && !s_absAimSuspend) {
|
|
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. */
|
|
/* D194/D238 natural-pitch mode (GE's own 1.2/SOLITARE style) reads
|
|
* forward/back from digital C-up/C-down instead of the analog stick
|
|
* (bondview2.c: digitalStepForward/Back <- U/D_JPAD|U/D_CBUTTONS),
|
|
* because the stick's Y axis is what carries continuous analog
|
|
* pitch there instead. Strafe and turn are unchanged -- both
|
|
* schemes read them the same way. */
|
|
/* D345(d): natural pitch is an in-stage aim scheme -- the N64 front
|
|
* end navigates on the analog stick only, so in menus W/S (and the
|
|
* Up/Down arrows) must emit stick even with NaturalPitch=1, or no
|
|
* front-end screen (file select, cheat, MENU_PC_OPTIONS) can be
|
|
* driven vertically from the keyboard. Watch/pause keeps the C-button
|
|
* mapping: it is current_menu==RUN_STAGE (menuMode 0), N64-style. */
|
|
if (naturalPitchMode && !menuMode) {
|
|
if (actHeld(ks, IA_FORWARD)) button |= GE_CONT_E; /* C-up = forward */
|
|
if (actHeld(ks, IA_BACK)) button |= GE_CONT_D; /* C-down = back */
|
|
} else {
|
|
if (actHeld(ks, IA_FORWARD)) sy = STICK_MAX;
|
|
if (actHeld(ks, IA_BACK)) sy = -STICK_MAX;
|
|
}
|
|
if (actHeld(ks, IA_STRAFE_L)) button |= GE_CONT_C; /* strafe left */
|
|
if (actHeld(ks, IA_STRAFE_R)) button |= GE_CONT_F; /* strafe right */
|
|
if (actHeld(ks, IA_TURN_L)) sx = -STICK_MAX; /* keyboard turn */
|
|
if (actHeld(ks, IA_TURN_R)) sx = STICK_MAX;
|
|
if (sx) s_aimDevMouse = 0; /* D337: keyboard stick turn is the active aim device */
|
|
|
|
if ((mb & SDL_BUTTON(SDL_BUTTON_LEFT)) || actHeld(ks, IA_FIRE))
|
|
button |= GE_CONT_G;
|
|
/* aimButton = the physical RMB / Aim bind (emits the game's R);
|
|
* aimHeld = "the game is in aim mode" for routing the mouse. D337: the
|
|
* game's aim buttons in 1.1/1.2 are L|R (bondview2.c aimButtons), so Q
|
|
* ("LeanLeft", sends L) is a second aim button -- it used to leave the
|
|
* mouse on the hipfire camera path, the accidental "centred aim" #104
|
|
* found. Routing also follows the game's own insightaimmode, which
|
|
* covers the Aim Control "Toggle" option and gamepad aim. The R emit
|
|
* stays tied to the physical button so Toggle still sees edges. */
|
|
int aimButton = (mb & SDL_BUTTON(SDL_BUTTON_RIGHT)) != 0 ||
|
|
actHeld(ks, IA_AIM);
|
|
int aimHeld = aimButton || actHeld(ks, IA_LEAN_L) ||
|
|
(g_CurrentPlayer != NULL && g_CurrentPlayer->insightaimmode);
|
|
int aimRisingEdgeAim = aimHeld && !s_aimHeldPrev;
|
|
if (aimRisingEdgeAim && g_CurrentPlayer && g_CurrentPlayer->docentreupdown)
|
|
s_centreClearTicks = 2; /* see D194 centre-spring note above */
|
|
if (!aimHeld) {
|
|
s_centreClearTicks = 0;
|
|
/* GEPD adopts the game's current crosshair pos every non-aim
|
|
* frame so re-entry starts where the game left it. */
|
|
s_gepdHeldPrev = 0;
|
|
if (g_CurrentPlayer) {
|
|
if (pdMouseAimEnabled()) {
|
|
/* D338: the accumulator is in GEPD units (±LIMIT = edge);
|
|
* adopt through the game's screen mapping (offset =
|
|
* pos*(1-damp), gunfire.c) and the aim-range scale, so aim
|
|
* entry starts exactly where the crosshair is drawn. */
|
|
double k = (1.0 - (double) g_CurrentPlayer->guncrossdamp) / 0.99
|
|
* GEPD_CROSSHAIR_LIMIT / aimRangeScale();
|
|
s_gepdCrossX = (double) g_CurrentPlayer->crosshair_x_pos * k;
|
|
s_gepdCrossY = (double) g_CurrentPlayer->crosshair_y_pos * k;
|
|
} else {
|
|
s_gepdCrossX = (double) g_CurrentPlayer->crosshair_x_pos / aimRangeScale();
|
|
s_gepdCrossY = (double) g_CurrentPlayer->crosshair_y_pos / aimRangeScale();
|
|
}
|
|
}
|
|
}
|
|
s_aimHeldPrev = aimHeld;
|
|
if (aimRisingEdgeAim && g_CurrentPlayer && g_CurrentPlayer->docentreupdown
|
|
&& configGetInputLog()) {
|
|
sysLogPrintf(LOG_NOTE,
|
|
"GE_INPUTLOG absaim centre-spring armed at aim entry; nudging to clear");
|
|
}
|
|
if (aimButton)
|
|
button |= GE_CONT_R;
|
|
if (actHeld(ks, IA_ACTION))
|
|
button |= GE_CONT_A;
|
|
if (wheelFwd > 0) { /* wheel up: fresh A edge = cycle forward */
|
|
button |= GE_CONT_A;
|
|
wheelFwd--;
|
|
} else if (wheelBack > 0) { /* wheel down: A+Z together, not staggered.
|
|
* D223 follow-up: staggering (A alone for a poll, THEN adding Z) races
|
|
* the real game-tick rate -- if the two states land in separate ticks,
|
|
* the "A alone" tick is itself a fresh A edge with no Z held, which
|
|
* the game's own weaponForwardOffset formula reads as a genuine
|
|
* cycle-FORWARD request (bondview2.c weaponForwardOffset/
|
|
* weaponBackOffset, both control-scheme sites) *before* the
|
|
* correcting backward tick runs -- so depending on real-time
|
|
* poll/tick alignment (D117-class nondeterminism) a single wheel-down
|
|
* notch could silently do a stray forward step, or forward-then-back
|
|
* (net a skipped slot on wrap). Presenting A and Z together from the
|
|
* very first poll means whichever single tick samples the 0->(A|Z)
|
|
* transition sees them rising simultaneously; weaponForwardOffset
|
|
* requires Z NOT held, so it's unambiguous -- only backward fires,
|
|
* every time, regardless of tick timing. moveData.triggerOn (actual
|
|
* fire) is separately gated off while A/invButtons is held, so this
|
|
* doesn't risk an accidental shot either. */
|
|
button |= GE_CONT_A | GE_CONT_G;
|
|
wheelBack--;
|
|
}
|
|
if (actHeld(ks, IA_CANCEL)) /* D145: Escape is in the default Cancel bind */
|
|
button |= GE_CONT_B;
|
|
/* v0.4.0 M3: reload (B tap, same as any B press) and crouch
|
|
* (hold, or a latched toggle). Both are unbound by default (the
|
|
* N64-original layout), so this is a no-op out of the box; the
|
|
* game's B-context logic decides reload-vs-crouch as on N64. */
|
|
if (actHeld(ks, IA_RELOAD))
|
|
button |= GE_CONT_B;
|
|
int crouchNow = actHeld(ks, IA_CROUCH);
|
|
if (crouchMode == 1) {
|
|
if (crouchNow && !s_crouchHeldPrev) s_crouchLatch ^= 1;
|
|
if (menuMode) s_crouchLatch = 0; /* never carry B into a menu */
|
|
if (s_crouchLatch) button |= GE_CONT_B;
|
|
s_crouchHeldPrev = crouchNow;
|
|
} else if (crouchNow) {
|
|
button |= GE_CONT_B;
|
|
}
|
|
if (actHeld(ks, IA_LEAN_L))
|
|
button |= GE_CONT_L;
|
|
if (actHeld(ks, IA_START))
|
|
button |= GE_CONT_START;
|
|
|
|
/* Mouse-look. Mode-dependent (see the tuning-constants comment):
|
|
* aim mode -> push the analog stick past +/-60 for proportional
|
|
* yaw + pitch; emit NO C-buttons (they mean crouch here).
|
|
* hipfire -> yaw on analog stick-X; pitch on digital C-up/C-down.
|
|
* "look down" convention: mouse-down looks down by default; GE's
|
|
* native pitch is inverted so hipfire down = C-up (GE_CONT_E) and
|
|
* aim-mode down = +stick_y. MouseInvertY flips both. */
|
|
if (mouseEnabled) {
|
|
double invert = mouseInvertY ? -1.0 : 1.0;
|
|
|
|
/* Optional exponential low-pass (mouseSmoothing = blend % of the
|
|
* previous poll). Off (0) => edx/edy are the raw deltas. */
|
|
double edx = mouseDX, edy = mouseDY;
|
|
if (mouseSmoothing > 0) {
|
|
double a = mouseSmoothing / 100.0;
|
|
if (a > 0.90) a = 0.90;
|
|
mouseSmDX = mouseSmDX * a + edx * (1.0 - a);
|
|
mouseSmDY = mouseSmDY * a + edy * (1.0 - a);
|
|
edx = mouseSmDX;
|
|
edy = mouseSmDY;
|
|
}
|
|
edy *= mouseYScale / 100.0;
|
|
|
|
double dyLook = edy * invert; /* >0 => look down */
|
|
if (!menuMode && (fabs(edx) > 0.01 || fabs(dyLook) > 0.01))
|
|
s_aimDevMouse = 1; /* D337: mouse is the active aim device */
|
|
|
|
/* D194(b): gameplay-look-only dt normalization -- see the
|
|
* MOUSE_DT_REF comment above. Computed unconditionally (so the
|
|
* clock stays warm across mode switches) but only ever applied
|
|
* below, inside the aimHeld/hipfire branches. */
|
|
double lookDtScale = 1.0;
|
|
{
|
|
Uint64 now = SDL_GetPerformanceCounter();
|
|
if (s_lastLookPollCounter != 0) {
|
|
double freq = (double)SDL_GetPerformanceFrequency();
|
|
double dtActual = (double)(now - s_lastLookPollCounter) / freq;
|
|
if (dtActual > 0.0005) { /* ignore sub-ms jitter / double polls */
|
|
lookDtScale = MOUSE_DT_REF / dtActual;
|
|
if (lookDtScale < MOUSE_DT_SCALE_MIN) lookDtScale = MOUSE_DT_SCALE_MIN;
|
|
if (lookDtScale > MOUSE_DT_SCALE_MAX) lookDtScale = MOUSE_DT_SCALE_MAX;
|
|
}
|
|
}
|
|
s_lastLookPollCounter = now;
|
|
}
|
|
if (!mouseDtDecouple) lookDtScale = 1.0;
|
|
|
|
if (configGetInputLog() && !menuMode && (aimHeld || fabs(edx) > 0.01 || fabs(dyLook) > 0.01)) {
|
|
sysLogPrintf(LOG_NOTE, "GE_INPUTLOG lookdt scale=%.3f raw=(%.2f,%.2f)",
|
|
lookDtScale, edx, dyLook);
|
|
}
|
|
|
|
if (menuMode && !menuPointerMode) {
|
|
/* Legacy velocity mode (Input.MenuPointerMode = 0): mouse
|
|
* velocity -> stick. Kept as a fallback; integrates as
|
|
* velocity^2 through front.c (D165). */
|
|
double g = MENU_POINTER_GAIN * (menuPointerSpeed / 100.0);
|
|
sx += (int)(edx * g);
|
|
sy -= (int)(dyLook * g); /* front-end cursor: +sy = up */
|
|
} else if (menuMode) {
|
|
/* Front-end menu pointer.
|
|
*
|
|
* front.c frontUpdateControlStickPosition() is the game's cursor
|
|
* integrator: it reads joyGetStickX/Y, applies a +/-5 deadband,
|
|
* clamps the stick to +/-70, then does
|
|
* cursor_h_pos += (stick*0.075 +/- 0.5) * timerDelta
|
|
* and clamps cursor_h/v_pos into [screenleft+20 ..
|
|
* screenleft+screenwidth-20] x [screentop+20 ..
|
|
* screentop+screenheight-20]. That integrator caps the cursor at
|
|
* ~5.75 virtual px per poll, so feeding it a stick derived from
|
|
* an absolute mouse position (D165/D169 P-controller) is
|
|
* unavoidably laggy/floaty and desyncs from the real cursor.
|
|
*
|
|
* With click-to-lock the front end has a
|
|
* real free OS cursor, so we know exactly where the pointer is.
|
|
* Write cursor_h/v_pos DIRECTLY from the absolute mouse position
|
|
* whenever the mouse moved, and emit a zero stick so the game's
|
|
* integrator adds nothing (stick 0 -> deadband). When the mouse
|
|
* is idle we leave the cursor alone and let the keyboard stick
|
|
* (WASD/arrows, added above) drive it through the game as usual.
|
|
* cursor_h/v_pos are the plain front.c UI globals every menu
|
|
* hit-test already reads -- no logic change, just where the
|
|
* pointer is placed. */
|
|
double loH = MENU_CURSOR_LO, hiH = MENU_CURSOR_HI_H;
|
|
double loV = MENU_CURSOR_LO, hiV = MENU_CURSOR_HI_V;
|
|
{
|
|
double sw = getPlayer_c_screenwidth(), sh = getPlayer_c_screenheight();
|
|
double sl = getPlayer_c_screenleft(), st = getPlayer_c_screentop();
|
|
if (sw > 200.0 && sw < 2000.0 && sh > 150.0 && sh < 2000.0) {
|
|
loH = sl + 20.0; hiH = sl + sw - 20.0;
|
|
loV = st + 20.0; hiV = st + sh - 20.0;
|
|
}
|
|
}
|
|
|
|
int haveAbs = 0;
|
|
if (!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) {
|
|
haveAbs = 1;
|
|
if (!menuPrevActive) {
|
|
/* just entered a menu: adopt the current pointer as
|
|
* the baseline, don't yank the cursor this frame */
|
|
lastMenuMouseX = mx;
|
|
lastMenuMouseY = my;
|
|
}
|
|
/* D345: re-assert right after a screen change when the
|
|
* mouse was the recent navigation device (click-to-
|
|
* enter). A stale idle mouse (WASD/arrow navigation)
|
|
* keeps the cede-to-keyboard behaviour: no yank. */
|
|
Uint64 pcNow = SDL_GetPerformanceCounter();
|
|
int mouseRecent = (menuPointerLastAbsWrite != 0) &&
|
|
((double)(pcNow - menuPointerLastAbsWrite) /
|
|
(double)SDL_GetPerformanceFrequency() * 1000.0) <
|
|
MENU_POINTER_REASSERT_MS;
|
|
if ((mx != lastMenuMouseX || my != lastMenuMouseY) ||
|
|
(menuPointerTransitionFrames > 0 && mouseRecent)) {
|
|
double fx = (double)mx / (double)ww;
|
|
/* D335: under native widescreen the front end is
|
|
* pillarboxed to a centred 4:3 region
|
|
* (front.c menu_jump_constructor_handler), so map
|
|
* the pointer across that region, not the window. */
|
|
{
|
|
extern f32 portNativeAspect(void);
|
|
f32 na = portNativeAspect();
|
|
if (na > (4.0f / 3.0f)) {
|
|
double vis = (4.0 / 3.0) / (double)na; /* visible width fraction */
|
|
fx = (fx - (1.0 - vis) * 0.5) / vis;
|
|
}
|
|
}
|
|
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;
|
|
cursor_h_pos = (float)(loH + fx * (hiH - loH));
|
|
cursor_v_pos = (float)(loV + fy * (hiV - loV));
|
|
sx = 0; /* pointer owns the cursor this poll */
|
|
sy = 0;
|
|
menuPointerLastAbsWrite = pcNow;
|
|
}
|
|
lastMenuMouseX = mx;
|
|
lastMenuMouseY = my;
|
|
}
|
|
}
|
|
|
|
if (configGetInputLog()) {
|
|
sysLogPrintf(LOG_NOTE,
|
|
"GE_INPUTLOG menuptr abs=%d cursor=(%.1f,%.1f) stick=(%d,%d)",
|
|
haveAbs, (double)cursor_h_pos, (double)cursor_v_pos, sx, sy);
|
|
}
|
|
} else if (g_CurrentPlayer != NULL &&
|
|
(!g_CurrentPlayer->outside_watch_menu ||
|
|
g_CurrentPlayer->pause_state != 0)) {
|
|
/* D330 (#103): in-stage watch / pause -- the mouse drives
|
|
* nothing. menuMode is 0 here (current_menu is still
|
|
* RUN_STAGE) and hipDirectCompute declines on this same gate,
|
|
* so the legacy stick fallbacks below used to turn mouse
|
|
* motion into stick deflection (and an RMB hold ran the GEPD
|
|
* aim writes, which GEPD itself gates on !watch): a ~15 px flick
|
|
* crossed the watch's +/-0x2E left/right threshold, which on
|
|
* a selected option means "pick OFF/ON" (options.c
|
|
* game_option_toggle_input) and otherwise flips watch pages,
|
|
* while mouse Y scrolled the rows. Auto-aim / look-ahead
|
|
* "wouldn't stay on". Keyboard and pad still drive the watch
|
|
* exactly as before. */
|
|
} else if (aimHeld) {
|
|
/* D194 GEPD-mirror aim: direct crosshair/camera writes, no
|
|
* look stick (see aimGepdCompute). Keyboard turn (sx/sy set
|
|
* above) still works. Otherwise fall through to the legacy
|
|
* velocity stick below. */
|
|
if (aimModeGet() == AIMMODE_CENTRED) {
|
|
/* D333: FPS-style centred aim -- the camera takes the
|
|
* mouse (same path as hipfire, which also declines on
|
|
* the watch/pause/cutscene gates) and no stick is
|
|
* emitted, so the crosshair settles at centre. */
|
|
hipDirectCompute(edx * lookDtScale, dyLook * lookDtScale);
|
|
} else if (pdMouseAimEnabled()) {
|
|
/* PD model: accumulate only. The game's own integrator is
|
|
* driven by the port-supplied turn through the
|
|
* sub_GAME_7F067FBC hook (see Input.PdMouseAim above). */
|
|
aimGepdAccumulate(edx, dyLook);
|
|
/* Edge-scroll belongs to the aim position, not to the
|
|
* crosshair model -- without this the camera stops
|
|
* following the crosshair at the screen edge. */
|
|
aimGepdEdgeScroll();
|
|
} else if (!aimGepdCompute(edx * lookDtScale, dyLook * lookDtScale)) {
|
|
double aimEdx = edx * lookDtScale, aimDyLook = dyLook * lookDtScale;
|
|
double aimSens = (mouseAimSpeed / 100.0) * (mouseSensitivity / 100.0);
|
|
double gamma = aimCurveGamma / 100.0;
|
|
double normX = fabs(aimEdx) * aimSens / AIM_FULL_SPEED_PX;
|
|
double normY = fabs(aimDyLook) * aimSens / AIM_FULL_SPEED_PX;
|
|
if (normX > 1.0) normX = 1.0;
|
|
if (normY > 1.0) normY = 1.0;
|
|
int ceilStick = 60 + aimBand;
|
|
if (ceilStick > AIM_STICK_GAME_MAX) ceilStick = AIM_STICK_GAME_MAX;
|
|
if (fabs(aimEdx) >= AIM_MOVE_THRESH) {
|
|
int m = AIM_STICK_MIN + (int)(pow(normX, gamma) * (AIM_STICK_GAME_MAX - AIM_STICK_MIN));
|
|
if (m > ceilStick) m = ceilStick;
|
|
sx += (aimEdx > 0) ? m : -m;
|
|
}
|
|
if (fabs(aimDyLook) >= AIM_MOVE_THRESH) {
|
|
int m = AIM_STICK_MIN + (int)(pow(normY, gamma) * (AIM_STICK_GAME_MAX - AIM_STICK_MIN));
|
|
if (m > ceilStick) m = ceilStick;
|
|
sy += (aimDyLook > 0) ? m : -m; /* +stick_y = look down */
|
|
}
|
|
} /* D194: end legacy velocity-aim fallback */
|
|
|
|
/* D194 centre-spring clear: a minimal pitch stick for a couple
|
|
* of ticks -- enough for bondview2's manual-input rule to clear
|
|
* docentreupdown, small enough (~0.1-0.3 deg) not to read as a
|
|
* jerk. Only when we are not already pitching this poll. */
|
|
if (s_centreClearTicks > 0 && g_CurrentPlayer &&
|
|
sy >= -60 && sy <= 60) {
|
|
sy = (g_CurrentPlayer->speedverta >= 0.0f) ? -61 : 61;
|
|
s_centreClearTicks--;
|
|
}
|
|
} else {
|
|
double hipEdx = edx * lookDtScale, hipDyLook = dyLook * lookDtScale;
|
|
double hipSens = (mouseTurnSpeed / 100.0) * (mouseSensitivity / 100.0);
|
|
/* WI-1: direct camera write, same linear px->degree model as
|
|
* aim mode (GEPD's hipfire branch) -- bypasses the N64 stick's
|
|
* quadratic natural-turn curve entirely (D238/#89: that curve
|
|
* saturated at ~13 px/poll, making slow motion "almost
|
|
* unrecognised" and fast motion bang-bang). Falls through to
|
|
* the legacy stick path below when it declines (disabled, no
|
|
* player, or a safety gate is closed). */
|
|
if (mouseDirectLook && hipDirectCompute(hipEdx, hipDyLook)) {
|
|
/* Pitch handled inside hipDirectCompute too; nothing left
|
|
* to do for yaw/pitch this poll. Digital pitch-pulse
|
|
* (naturalPitchMode==0) still applies below only in the
|
|
* legacy path, so skip both branches here. */
|
|
} else {
|
|
sx += (int)(hipEdx * hipSens * MOUSE_TURN_GAIN);
|
|
if (naturalPitchMode) {
|
|
/* D194/D238: SOLITARE gives hipfire pitch the same
|
|
* continuous analog stick treatment as yaw -- same
|
|
* formula as the sx line above, so X and Y are, by
|
|
* construction, symmetric. Forward/back moved to
|
|
* digital C-up/C-down above, freeing the stick's Y axis
|
|
* for this. */
|
|
sy += (int)(hipDyLook * hipSens * MOUSE_TURN_GAIN);
|
|
} else {
|
|
/* D166 (legacy): hipfire pitch as C-button pulses whose
|
|
* frequency scales with mouse-Y speed -- fast mouse =
|
|
* solid hold, slow = sparse taps. Kept as the
|
|
* Input.NaturalPitch=0 escape hatch. */
|
|
double sp = fabs(hipDyLook);
|
|
if (sp >= MOUSE_PITCH_THRESH) {
|
|
double duty = sp * (hipfirePitchSpeed / 100.0) * (mouseSensitivity / 100.0) / HIP_PITCH_FULL;
|
|
if (duty > 1.0) duty = 1.0;
|
|
hipPitchPhase += duty;
|
|
if (hipPitchPhase >= 1.0) {
|
|
hipPitchPhase -= 1.0;
|
|
button |= (hipDyLook > 0) ? GE_CONT_E : GE_CONT_D; /* E=C-up=look down */
|
|
}
|
|
} else {
|
|
hipPitchPhase = 0.0;
|
|
}
|
|
}
|
|
} /* end mouseDirectLook fallback (WI-1) */
|
|
}
|
|
}
|
|
|
|
if (menuMode && mouseEnabled) {
|
|
/* Clicks are select / back in the front end, not fire / aim. */
|
|
button &= ~(GE_CONT_G | GE_CONT_R);
|
|
if (mb & SDL_BUTTON(SDL_BUTTON_LEFT)) button |= GE_CONT_A;
|
|
if (mb & SDL_BUTTON(SDL_BUTTON_RIGHT)) button |= GE_CONT_B;
|
|
}
|
|
menuPrevActive = menuMode;
|
|
s_menuPointerLive = (menuMode && mouseEnabled && !mouseGrabbed) ? 1 : 0;
|
|
|
|
mouseDX = 0.0;
|
|
mouseDY = 0.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);
|
|
|
|
/* D282: front-end menus (main menu, file select, mission-select map)
|
|
* share this same stick_x/stick_y channel with in-game look/movement.
|
|
* naturalPitchMode (SOLITARE, default-on) routes that channel to the
|
|
* RIGHT stick for continuous look -- fine in a level, unintuitive in
|
|
* a menu, where the left stick is the expected primary-navigation
|
|
* input on a modern pad/Deck (matches the F10 overlay's own
|
|
* left-stick nav, optionsoverlay.c). Outside a running stage, always
|
|
* source stick_x/stick_y from the left stick, regardless of
|
|
* naturalPitchMode -- a menu-context input remap only, no game-logic
|
|
* change and no effect on in-level control feel. */
|
|
int padMenuMode = (current_menu != GE_MENU_RUN_STAGE &&
|
|
current_menu != GE_MENU_INVALID);
|
|
|
|
if (padMenuMode) {
|
|
int px = scaleAxis(lx);
|
|
int py = -scaleAxis(ly); /* SDL up = negative -> N64 up = positive */
|
|
if (px) sx = px;
|
|
if (py) sy = py;
|
|
} else if (naturalPitchMode) {
|
|
/* D194/D238: SOLITARE swaps stick roles -- left stick becomes
|
|
* digital-step movement (same analog-for-movement tradeoff as
|
|
* the keyboard remap above), right stick becomes continuous
|
|
* natural look (replacing its old digital-C-button emulation),
|
|
* matching how the mouse now drives look continuously too. */
|
|
if (ly < -RSTICK_THRESHOLD) button |= GE_CONT_E; /* stick up = forward */
|
|
if (ly > RSTICK_THRESHOLD) button |= GE_CONT_D; /* stick down = back */
|
|
if (lx < -RSTICK_THRESHOLD) button |= GE_CONT_C; /* strafe left */
|
|
if (lx > RSTICK_THRESHOLD) button |= GE_CONT_F; /* strafe right */
|
|
|
|
int rxs = scaleAxis(rx);
|
|
if (rxs || scaleAxis(ry)) s_aimDevMouse = 0; /* D337: pad aim active */
|
|
int rys = -scaleAxis(ry); /* SDL up = negative -> N64 up = positive */
|
|
if (padLookInvertY) rys = -rys;
|
|
if (rxs) sx = rxs;
|
|
if (rys) sy = rys;
|
|
} else {
|
|
int px = scaleAxis(lx);
|
|
int py = -scaleAxis(ly); /* SDL up = negative -> N64 up = positive */
|
|
if (px) sx = px;
|
|
if (py) sy = py;
|
|
if (px || py) s_aimDevMouse = 0; /* D337: pad aim active */
|
|
|
|
if (padLookInvertY) ry = -ry;
|
|
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;
|
|
}
|
|
|
|
int trigPt = padTriggerPct * 327; /* % of the 0..32767 trigger travel */
|
|
if (SDL_GameControllerGetAxis(pad, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) > trigPt)
|
|
button |= GE_CONT_G;
|
|
if (SDL_GameControllerGetAxis(pad, SDL_CONTROLLER_AXIS_TRIGGERLEFT) > trigPt)
|
|
button |= GE_CONT_R;
|
|
|
|
/* Modern dual-stick layout (Xbox re-release style; the Steam Deck
|
|
* target). A/X = action/use/reload (the game's context-sensitive A
|
|
* line), B/Y = crouch, and LB/RB rising edges cycle weapons.
|
|
* In-game cycling is an A edge (forward) or A+Z held on the same tick
|
|
* (backward -- bondview2.c weaponForwardOffset/weaponBackOffset, the
|
|
* same trick the mouse wheel uses above); emit for exactly one poll
|
|
* so holding RB cannot latch invButtons and block firing. */
|
|
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))
|
|
button |= GE_CONT_B;
|
|
{
|
|
int lbNow = SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_LEFTSHOULDER);
|
|
int rbNow = SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER);
|
|
int *prev = &padShoulderPrev[idx];
|
|
/* Track edge state in menus too: a shoulder held across the
|
|
* menu->game transition must not fire a cycle on entry. */
|
|
if (!padMenuMode) {
|
|
if (rbNow && !(*prev & 1)) button |= GE_CONT_A; /* next weapon */
|
|
if (lbNow && !(*prev & 2)) button |= GE_CONT_A | GE_CONT_G; /* prev weapon */
|
|
}
|
|
*prev = (rbNow ? 1 : 0) | (lbNow ? 2 : 0);
|
|
}
|
|
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;
|
|
|
|
/* Select (BACK) opens the F10 options overlay -- the gamepad
|
|
* equivalent of the F10 key for controller-only machines (Steam
|
|
* Deck). The game never reads BACK, so nothing is withheld. */
|
|
{
|
|
int selNow = SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_BACK);
|
|
/* D343: not on the PC options screen (one options UI at a time). */
|
|
if (selNow && !padSelectPrev && !frontOptionsBlocksOverlay()) optionsOverlayToggle();
|
|
padSelectPrev = selNow;
|
|
}
|
|
}
|
|
|
|
if (idx == 0 && scriptIsActive()) {
|
|
/* D263: apply BEFORE the stick is written out -- scripted stick
|
|
* tokens (SUP/SDOWN/SLEFT/SRIGHT) used to be discarded. */
|
|
button = scriptApply(button);
|
|
sx = scriptCurSX;
|
|
sy = scriptCurSY;
|
|
}
|
|
|
|
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;
|
|
|
|
/* D337: GE_AIMLOG=1 -- per-poll aim-model trace (the game's own crosshair
|
|
* state), for measuring jitter / range / edge-scroll headlessly. */
|
|
if (idx == 0 && g_CurrentPlayer != NULL && GE_ENVFLAG("GE_AIMLOG") &&
|
|
(current_menu == GE_MENU_RUN_STAGE || current_menu == GE_MENU_INVALID)) {
|
|
sysLogPrintf(LOG_NOTE, "AIMLOG f=%ld aim=%d dev=%d drawx=%.3f cx=%.5f cy=%.5f theta=%.4f verta=%.4f acc=%.4f",
|
|
scriptFrame, (int)g_CurrentPlayer->insightaimmode, s_aimDevMouse,
|
|
(double)g_CurrentPlayer->crosshair_angle.f[0],
|
|
(double)g_CurrentPlayer->crosshair_x_pos, (double)g_CurrentPlayer->crosshair_y_pos,
|
|
(double)g_CurrentPlayer->vv_theta, (double)g_CurrentPlayer->vv_verta, s_gepdCrossX);
|
|
}
|
|
if (configGetInputLog() && (button || sx || sy)) {
|
|
sysLogPrintf(LOG_NOTE, "GE_INPUTLOG cont%d: btn=%04x stick=(%d,%d)",
|
|
idx, button, sx, sy);
|
|
}
|
|
|
|
return button;
|
|
}
|
|
|
|
int inputMenuPointerLive(void)
|
|
{
|
|
/* D345(b): true while the 1:1 menu pointer owns cursor_h/v_pos -- in a
|
|
* menu, the abs pointer is available, and the mouse was used recently.
|
|
* Port screens that teleport the crosshair (frontoptions.c cursorToItem)
|
|
* skip their snap when this is set: the cursor is already under the OS
|
|
* pointer. Keyboard/D-pad users (stale mouse) still get the snap. */
|
|
if (!s_menuPointerLive || menuPointerLastAbsWrite == 0) return 0;
|
|
Uint64 now = SDL_GetPerformanceCounter();
|
|
return ((double)(now - menuPointerLastAbsWrite) /
|
|
(double)SDL_GetPerformanceFrequency() * 1000.0) <
|
|
MENU_POINTER_REASSERT_MS;
|
|
}
|
|
|
|
static void applyGrab(int want)
|
|
{
|
|
want = want && mouseEnabled;
|
|
if (want == mouseGrabbed) {
|
|
return;
|
|
}
|
|
mouseGrabbed = want;
|
|
mouseRequestRelative(want); /* D287: applied on the host thread */
|
|
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 = captureArmed && windowFocused && !menuMode; /* click-to-lock */
|
|
if (s_absAimSuspend) {
|
|
want = 0; /* D194: an aim hold wants the free cursor for absolute aim */
|
|
}
|
|
applyGrab(want);
|
|
}
|
|
|
|
/* Hide the OS cursor while the game window is focused (normal PC-game
|
|
* behaviour): in a stage the mouse drives the look axis, and in menus GE draws
|
|
* its own crosshair that now tracks the pointer 1:1 -- a visible OS arrow on
|
|
* top is just clutter. Show it again when focus is lost so the desktop behaves
|
|
* normally. Relative-mouse mode hides the cursor too, but only while grabbed;
|
|
* this covers the free-but-focused states (menus, pre-click stage). */
|
|
static void applyCursorVisibility(void)
|
|
{
|
|
/* D194: while an aim hold has the cursor out for absolute aim, show it --
|
|
* the user is pointing with it and needs to see where. */
|
|
int hide = windowFocused && mouseEnabled && !s_absAimSuspend;
|
|
mouseRequestCursor(!hide); /* D287: applied on the host thread */
|
|
}
|
|
|
|
/* video.c focus events. Records focus and lets reconcileGrab() decide. */
|
|
void inputSetMouseGrab(int on)
|
|
{
|
|
windowFocused = on ? 1 : 0;
|
|
reconcileGrab(0); /* menuMode re-checked on the next poll anyway */
|
|
applyCursorVisibility();
|
|
}
|
|
|
|
/* A mouse click landed in the game window (video.c): arm + grab. */
|
|
void inputNotifyClick(void)
|
|
{
|
|
if (mouseEnabled && windowFocused) {
|
|
captureArmed = 1;
|
|
reconcileGrab(0);
|
|
}
|
|
}
|
|
|
|
/* ESC pressed (video.c): release the cursor and report 1 so the caller can
|
|
* swallow the key; reports 0 when there is nothing to release. */
|
|
int inputReleaseCapture(void)
|
|
{
|
|
if (mouseGrabbed) {
|
|
captureArmed = 0;
|
|
applyGrab(0);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* F10 options overlay: while it owns controller 0 the inputComputePad poll
|
|
* early-returns before reconcileGrab()/applyCursorVisibility(), so whatever
|
|
* grab state was live when F10 was pressed (relative mode + hidden cursor in a
|
|
* stage) would persist and the mouse UI would be unusable. Force the cursor
|
|
* free + visible every poll; the normal reconcile resumes once the overlay
|
|
* closes and the early-return no longer fires. */
|
|
int inputPadButton(int idx, SDL_GameControllerButton b)
|
|
{
|
|
if (idx < 0 || idx >= MAX_PADS || !pads[idx]) return 0;
|
|
return SDL_GameControllerGetButton(pads[idx], b);
|
|
}
|
|
|
|
short inputPadAxis(int idx, SDL_GameControllerAxis a)
|
|
{
|
|
if (idx < 0 || idx >= MAX_PADS || !pads[idx]) return 0;
|
|
return SDL_GameControllerGetAxis(pads[idx], a);
|
|
}
|
|
|
|
void inputSuspendForOverlay(void)
|
|
{
|
|
if (mouseGrabbed) {
|
|
mouseGrabbed = 0;
|
|
mouseRequestRelative(0);
|
|
mouseDX = mouseDY = 0.0;
|
|
}
|
|
mouseRequestCursor(1);
|
|
}
|
|
|
|
void inputPostWheel(int notches)
|
|
{
|
|
/* D223: keep the direction. v0.2.1: swapped per user request -- up now
|
|
* cycles to the PREVIOUS weapon, down to the NEXT one (PC convention
|
|
* "scroll down = advance list"). A burst of same-direction notches just
|
|
* re-arms the same pulse; a direction change mid-sequence restarts it
|
|
* (last wins). */
|
|
if (notches > 0) { wheelBack = 2; wheelFwd = 0; }
|
|
else if (notches < 0) { wheelFwd = WHEEL_FWD_POLLS; wheelBack = 0; }
|
|
}
|
|
|
|
/* Called from the host event pump on SDL_CONTROLLERDEVICEADDED/REMOVED.
|
|
* Closes every open pad and re-opens whatever is present now. `connectedMask`
|
|
* bit 0 (keyboard/mouse) is always kept. Note: the game latches the mask at
|
|
* osContInit (boot), so a pad added later still merges into controller 0 for
|
|
* play -- it just won't appear as a separate controller channel. */
|
|
void inputRescanPads(void)
|
|
{
|
|
for (int i = 0; i < MAX_PADS; ++i) {
|
|
if (pads[i]) {
|
|
SDL_GameControllerClose(pads[i]);
|
|
pads[i] = NULL;
|
|
}
|
|
}
|
|
inputOpenPads();
|
|
sysLogPrintf(LOG_NOTE, "input: rescanned pads (mask=0x%x, %d controller(s))",
|
|
connectedMask, numControllers);
|
|
}
|
|
|
|
int inputConnectedMask(void)
|
|
{
|
|
return connectedMask;
|
|
}
|
|
|
|
int inputGetNumControllers(void)
|
|
{
|
|
return numControllers;
|
|
}
|
|
|
|
/* D194 absolute (cursor-anchored) aim mode -- GEPD "mouse injector" style.
|
|
*
|
|
* Instead of synthesizing a velocity stick from this poll's mouse delta
|
|
* (which can only ever be one of ten discrete (stick-60)/10 speeds, with a
|
|
* hard 10%-speed floor on the smallest nonzero nudge), drive the game's own
|
|
* aim-speed plant as a closed position loop: the cursor's screen position IS
|
|
* the desired aim direction. On RMB press / every mouse move we record the
|
|
* view angles that would put the point under the cursor at screen center;
|
|
* each poll we emit a stick proportional to the remaining angular error, so
|
|
* the crosshair slews to the cursor and HOLDS there -- stationary cursor =
|
|
* stationary aim (even while walking), no floor jump, no release snap.
|
|
*
|
|
* Returns 1 if it handled this poll (callers must use outSx/outSy); 0 means
|
|
* fall back to the legacy velocity stick (grabbed mouse, menus, gamepad,
|
|
* degenerate geometry).
|
|
*/
|
|
/* D194 GEPD-mirror aim -- direct crosshair/camera writes.
|
|
*
|
|
* Mirrors MouseInjectorPlugin (games/goldeneye.c) in model:
|
|
* - mouse motion moves a crosshair POSITION accumulator (not the view),
|
|
* clamped to ±GEPD_CROSSHAIR_LIMIT (the screen edge);
|
|
* - the accumulator is written straight into crosshair_x/y_pos and the
|
|
* gun/arm pose (gun_azimuth_angle/turning) via GEPD's formulas;
|
|
* - the view only scrolls when the crosshair passes 72% toward the edge:
|
|
* cam += (ratio-0.72)*475*timestep*(fov/basefov);
|
|
* - no look stick is emitted while active. bondview2's damped crosshair
|
|
* auto-centre (caclulate_gun_crosshair_position_rotation) keeps RUNNING
|
|
* -- exactly like GEPD, which patches nothing there: the per-frame
|
|
* overwrite simply wins over the damping, and the game derives
|
|
* crosshair_angle (HUD crosshair + aim ray) and field_FFC (arm/gun
|
|
* screen offset in gunUpdateAndFire) from our written values. On aim
|
|
* release we stop writing and the game's damping eases everything back
|
|
* to centre itself.
|
|
*
|
|
* Sensitivity: Input.GepdSens (GEPD SENSITIVITY setting; default 20 ~= 1:1
|
|
* window-px per crosshair-screen-px at full zoom).
|
|
*
|
|
* Returns 1 if it handled this poll (callers must NOT emit a look stick);
|
|
* 0 means fall back to the legacy velocity stick.
|
|
*/
|
|
/* Accumulate this poll's mouse delta into the aim position. Shared by both
|
|
* aim models: GEPD pushes the result into crosshair_x/y_pos itself, PD feeds
|
|
* it to the game's integrator as the turn (see Input.PdMouseAim). Returns 0
|
|
* if aim is not usable this poll. */
|
|
static int aimGepdAccumulate(double dxPx, double dyLook)
|
|
{
|
|
struct player *p = g_CurrentPlayer;
|
|
|
|
/* Needs the grabbed-cursor relative deltas (capture mode, locked in a
|
|
* stage) and a live player. dxPx/dyLook are this poll's px. */
|
|
if (!aimAbsolute || !mouseGrabbed || p == NULL)
|
|
return 0;
|
|
|
|
/* On entry adopt the game's current position (GEPD adopts every
|
|
* non-aim frame; input.c does that in the !aimHeld branch). */
|
|
if (!s_gepdHeldPrev) {
|
|
s_gepdCrossX = (double) p->crosshair_x_pos;
|
|
s_gepdCrossY = (double) p->crosshair_y_pos;
|
|
}
|
|
s_gepdHeldPrev = 1;
|
|
|
|
/* Crosshair position: GEPD crosshairpos += delta/10 * (SENS/292). */
|
|
/* D194/D238: master sensitivity scales aim mode too, so MouseSensitivity
|
|
* moves BOTH hipfire and aim together ("in line"); GepdSens sets the
|
|
* aim-mode offset from that shared baseline. At master=100 this is
|
|
* exactly the M-123-calibrated feel. */
|
|
double sens = (double) gepdSens / 2920.0 * (mouseSensitivity / 100.0);
|
|
s_gepdCrossX += dxPx * sens;
|
|
s_gepdCrossY += dyLook * sens; /* +dyLook = look down = crosshair down */
|
|
if (s_gepdCrossX > GEPD_CROSSHAIR_LIMIT) s_gepdCrossX = GEPD_CROSSHAIR_LIMIT;
|
|
if (s_gepdCrossX < -GEPD_CROSSHAIR_LIMIT) s_gepdCrossX = -GEPD_CROSSHAIR_LIMIT;
|
|
if (s_gepdCrossY > GEPD_CROSSHAIR_LIMIT) s_gepdCrossY = GEPD_CROSSHAIR_LIMIT;
|
|
if (s_gepdCrossY < -GEPD_CROSSHAIR_LIMIT) s_gepdCrossY = -GEPD_CROSSHAIR_LIMIT;
|
|
return 1;
|
|
}
|
|
|
|
/* Camera edge-scroll: past GEPD_EDGE_THRESHOLD (72%) of the way to the
|
|
* crosshair limit, scroll the view proportionally to the overshoot, scaled by
|
|
* zoom like GEPD's (fov/basefov). This belongs to the aim POSITION, not to
|
|
* whichever model owns the crosshair, so both call it -- PD's model needs it
|
|
* too, or dragging the crosshair to the edge no longer moves the camera. */
|
|
static void aimGepdEdgeScroll(void)
|
|
{
|
|
struct player *p = g_CurrentPlayer;
|
|
f32 fov = viGetFovY();
|
|
|
|
double rX = s_gepdCrossX / GEPD_CROSSHAIR_LIMIT;
|
|
double rY = s_gepdCrossY / GEPD_CROSSHAIR_LIMIT;
|
|
double aimx = 0.0, aimy = 0.0;
|
|
const double th = aimEdgeThreshold(); /* D338 */
|
|
if (rX > th) aimx = (rX - th) * GEPD_SCROLL_SPEED / 60.0;
|
|
else if (rX < -th) aimx = (rX + th) * GEPD_SCROLL_SPEED / 60.0;
|
|
if (rY > th) aimy = (rY - th) * GEPD_SCROLL_SPEED / 60.0;
|
|
else if (rY < -th) aimy = (rY + th) * GEPD_SCROLL_SPEED / 60.0;
|
|
|
|
f32 scale = (fov > 0.0f) ? fov / GEPD_BASE_FOV : 1.0f;
|
|
if (aimx != 0.0) {
|
|
/* GEPD does a bare `camx += ...` -- no [0,360) wrap. The game never
|
|
* wraps vv_theta in on-foot play either (bondviewApplyVertaTheta only
|
|
* sin/cos's it; the tank code is the sole wraper), so keep the
|
|
* accumulator unbounded. Wrapping turned a small negative edge-scroll
|
|
* step into a one-tick +360 spike in the raw value (D194 playtest log,
|
|
* line 534: cam 0.5 -> 359.7). */
|
|
p->vv_theta += (f32) aimx * scale;
|
|
}
|
|
if (aimy != 0.0) {
|
|
p->vv_verta -= (f32) aimy * scale; /* crosshair low -> look down */
|
|
if (p->vv_verta > 90.0f) p->vv_verta = 90.0f;
|
|
if (p->vv_verta < -90.0f) p->vv_verta = -90.0f;
|
|
}
|
|
}
|
|
|
|
static int aimGepdCompute(double dxPx, double dyLook)
|
|
{
|
|
struct player *p = g_CurrentPlayer;
|
|
|
|
if (!aimGepdAccumulate(dxPx, dyLook))
|
|
return 0;
|
|
|
|
f32 fov = viGetFovY();
|
|
f32 fovratio = (fov > 0.0f) ? fov / GEPD_BASE_FOV : 1.0f;
|
|
|
|
/* Pre-overwrite residue: what last tick's damped update left behind
|
|
* (crosshair_pos = pos*damp + turn, gunfire.c caclulate_gun_crosshair_...
|
|
* runs AFTER our write each tick). The gap between this and our write
|
|
* below is the game-side turn term (autoaimx/y or speedtheta*0.3) --
|
|
* logged for the D194 spazz diagnosis. */
|
|
double resX = (double) p->crosshair_x_pos;
|
|
double resY = (double) p->crosshair_y_pos;
|
|
|
|
/* Crosshair + gun/arm pose (GEPD formulas, RATIOFACTOR=1 for our 4:3
|
|
* viewport; failsafe weapon offsets 0.15/0 as in goldeneye.c). */
|
|
p->crosshair_x_pos = (f32) (s_gepdCrossX * aimRangeScale()); /* D338 */
|
|
p->crosshair_y_pos = (f32) (s_gepdCrossY * aimRangeScale());
|
|
p->gun_azimuth_angle = (f32) (s_gepdCrossX * (1.11f + 0.15f * 1.5f) + fovratio - 1.0f);
|
|
p->gun_azimuth_turning = (f32) (s_gepdCrossY * 1.11f + fovratio - 1.0f);
|
|
|
|
aimGepdEdgeScroll();
|
|
|
|
if (configGetInputLog()) {
|
|
/* ct = g_ClockTimer: game ticks batched into this poll. If the
|
|
* displayed crosshair shrinks by damp^ct with ct varying tick to
|
|
* tick (frame-pacing catch-up), game= will show fluctuating shrink
|
|
* at d=(0,0) -- the multi-tick spazz hypothesis. */
|
|
sysLogPrintf(LOG_NOTE,
|
|
"GE_INPUTLOG gepdaim d=(%.1f,%.1f) cross=(%.2f,%.2f) game=(%.2f,%.2f) aa=(%.2f,%.2f) st=%.3f sv=%.3f ct=%d cam=(%.1f,%.1f)",
|
|
dxPx, dyLook, s_gepdCrossX, s_gepdCrossY, resX, resY,
|
|
(double)p->autoaimx, (double)p->autoaimy,
|
|
(double)p->speedtheta, (double)p->speedverta,
|
|
g_ClockTimer, (double)p->vv_theta, (double)p->vv_verta);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* WI-1 (GEPD-INPUT-PLAN.md #89): hipfire direct camera write.
|
|
*
|
|
* Mirrors GEPD's hipfire branch (games/goldeneye.c): `camx += XPOS/10 *
|
|
* (SENS/40) * (fov/basefov)` degrees this poll -- linear in px, FOV-scaled,
|
|
* no stick curve in between. We fold GEPD's SENS/40 into our own
|
|
* MouseTurnSpeed*MouseSensitivity product (both already 100% at defaults,
|
|
* same "in line" convention D238 established for aimGepdCompute's GepdSens):
|
|
* at defaults this reduces to exactly GEPD's px/10 baseline.
|
|
*
|
|
* dxPx/dyLook are this poll's dt-scaled px (same convention as
|
|
* aimGepdCompute -- callers pre-multiply by lookDtScale).
|
|
*
|
|
* GEPD's safety gates (`camera==4||0 && menupage==11 && !dead && !watch &&
|
|
* !pause`) matter here in a way they didn't for aim mode: aim mode requires
|
|
* RMB held, so it naturally can't fire during a death/cutscene/pause state a
|
|
* player would be holding RMB through; hipfire looks are always live, so a
|
|
* frozen or scripted-camera state (POSEND/INTRO cutscenes, death cam, pause)
|
|
* must be checked explicitly or this would fight the game's own camera
|
|
* control during those states. `current_menu` already gates menupage==11
|
|
* (checked by the caller's menuMode branch, same as aim mode); the rest are
|
|
* read here directly -- read-only, no logic change.
|
|
*
|
|
* Returns 1 if it handled this poll (caller must not also run the legacy
|
|
* stick path); 0 to fall back (disabled, no player, or a safety gate is
|
|
* closed -- e.g. mid-death or mid-cutscene, matching GEPD's !dead/!watch).
|
|
*/
|
|
static int hipDirectCompute(double dxPx, double dyLook)
|
|
{
|
|
struct player *p = g_CurrentPlayer;
|
|
|
|
if (!mouseDirectLook || !mouseGrabbed || p == NULL)
|
|
return 0;
|
|
|
|
/* GEPD: !dead && !watch && !pause. bonddead/outside_watch_menu/
|
|
* pause_state are the decomp-canonical equivalents (bondview.h). */
|
|
if (p->bonddead || !p->outside_watch_menu || p->pause_state != 0)
|
|
return 0;
|
|
|
|
/* M-190: the "frozen or scripted-camera state (POSEND/INTRO cutscenes...)
|
|
* must be checked explicitly" gate this function's own comment above has
|
|
* always documented, but never actually implemented -- found live: the
|
|
* mouse could still freely move the camera during the Dam intro flyover
|
|
* and the abseil-jump cutscene, both CAMERAMODE_INTRO/POSEND, which are
|
|
* supposed to be camera-locked. bondviewFrozenMoveBond (bondview2.c:7947)
|
|
* already calls bondviewProcessInput(0,0,0,0) to correctly ignore stick
|
|
* input during these modes, but that gate lives entirely inside the
|
|
* stick-based dispatch (MoveBond vs bondviewFrozenMoveBond,
|
|
* bondview2.c:8268) -- this function writes p->vv_theta/vv_verta
|
|
* directly, bypassing that dispatch entirely. Reuses the same
|
|
* scripted-camera test D243's clamps use (bondview2.c, gameScriptedCameraActive,
|
|
* formerly d243mCutsceneActive -- renamed since this isn't D243-specific). */
|
|
extern int gameScriptedCameraActive(void);
|
|
if (gameScriptedCameraActive())
|
|
return 0;
|
|
|
|
f32 fov = viGetFovY();
|
|
f32 scale = (fov > 0.0f) ? fov / GEPD_BASE_FOV : 1.0f;
|
|
double sens = (mouseTurnSpeed / 100.0) * (mouseSensitivity / 100.0);
|
|
|
|
/* dyLook already carries MouseInvertY + MouseYScale (applied by the
|
|
* caller before dt-scaling, same as every other consumer of dyLook) --
|
|
* do not re-apply either here. */
|
|
p->vv_theta += (f32) (dxPx * 0.1 * sens * scale);
|
|
p->vv_verta -= (f32) (dyLook * 0.1 * sens * scale);
|
|
if (p->vv_verta > 90.0f) p->vv_verta = 90.0f;
|
|
if (p->vv_verta < -90.0f) p->vv_verta = -90.0f;
|
|
|
|
if (configGetInputLog()) {
|
|
sysLogPrintf(LOG_NOTE,
|
|
"GE_INPUTLOG hipdirect d=(%.1f,%.1f) cam=(%.1f,%.1f)",
|
|
dxPx, dyLook, (double)p->vv_theta, (double)p->vv_verta);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* --- Input.PdMouseAim: the port side of PD's mouse-aim model ----------------
|
|
* See the flag's comment above for the design. PD's own implementation lives
|
|
* in its GAME files (bondmove.c/bondgun.c) behind `#ifndef PLATFORM_N64`, with
|
|
* the port supplying the input; we mirror that with `#ifdef PORT` hooks in
|
|
* GE's aim path (sub_GAME_7F067FBC, gunfire.c).
|
|
*
|
|
* The port accumulates the mouse delta (aimGepdAccumulate, shared with GEPD's
|
|
* model) and the game's own crosshair integrator does the rest. PD's range is
|
|
* swivelpos in [-1,1] == the screen edge, which is what the game's display
|
|
* formula maps to half a screen width; GEPD's crosshair limit is that same
|
|
* edge in its own units, so dividing by it gives PD's units. */
|
|
int portMouseAimPdActive(void)
|
|
{
|
|
/* D337: only while the mouse is the device driving aim -- otherwise the
|
|
* pad's stick turn (the N64 path, weapon damp) must reach the game. */
|
|
return pdMouseAimEnabled() && mouseGrabbed && s_aimDevMouse;
|
|
}
|
|
|
|
int portMouseAimPdGetTurn(f32 *tx, f32 *ty)
|
|
{
|
|
if (!portMouseAimPdActive() || g_CurrentPlayer == NULL)
|
|
return 0;
|
|
if (tx) *tx = (f32) (s_gepdCrossX / GEPD_CROSSHAIR_LIMIT * aimRangeScale()); /* D338 */
|
|
if (ty) *ty = (f32) (s_gepdCrossY / GEPD_CROSSHAIR_LIMIT * aimRangeScale());
|
|
return 1;
|
|
}
|
|
|
|
PD_CONSTRUCTOR static void inputConfigInit(void)
|
|
{
|
|
configRegisterInt("Input.MouseEnabled", &mouseEnabled, 0, 1);
|
|
configRegisterInt("Input.MouseAimSpeed", &mouseAimSpeed, 1, 500);
|
|
configRegisterInt("Input.PdMouseAim", &pdMouseAim, 0, 1); /* legacy -> AimStyle 1 */
|
|
configRegisterInt("Input.AimStyle", &aimStyleLegacy, 0, 2); /* D333 legacy -> AimMode */
|
|
configRegisterInt("Input.AimMode", &aimMode, 0, 1); /* D337 */
|
|
configRegisterInt("Input.AimLegacyGepd", &aimLegacyGepd, 0, 1); /* D337 hidden fallback */
|
|
configRegisterInt("Input.AimRange", &aimRange, 0, 1); /* D338 PC / N64 */
|
|
configRegisterInt("Input.AimAbsolute", &aimAbsolute, 0, 1); /* D194 */
|
|
configRegisterInt("Input.MouseDirectLook", &mouseDirectLook, 0, 1); /* WI-1 */
|
|
/* D194: renamed Input.GepdSens -> Input.AimModeSens (community name for
|
|
* the RMB aim mode; "GEPD" is internal provenance jargon). The old key
|
|
* stays registered against the same variable as a deprecated alias --
|
|
* if both appear in an ini, the later line wins. */
|
|
/* D304: widened from the old (1,80) clamp to match Input.MouseTurnSpeed's
|
|
* (1,500) range exactly -- both are the same underlying "reference SENS
|
|
* passthrough" unit (see aimGepdCompute/hipDirectCompute), consumed by
|
|
* two different, intentional per-mode divisor constants (2920 vs the
|
|
* turn-speed path's /100*0.1), so a shared range lets the two sliders be
|
|
* directly comparable ("1:1" in the menu) instead of arbitrarily capped
|
|
* at different, mismatched ceilings. Old 80 cap was well below even this
|
|
* value's own reference-model native ceiling (100 raw = 500% in the
|
|
* source GEPD injector's own display convention). */
|
|
configRegisterInt("Input.AimModeSens", &gepdSens, 1, 500);
|
|
configRegisterInt("Input.GepdSens", &gepdSens, 1, 500); /* deprecated alias */
|
|
configRegisterInt("Input.AimBand", &aimBand, 5, 40);
|
|
configRegisterInt("Input.MouseTurnSpeed", &mouseTurnSpeed, 1, 500);
|
|
configRegisterInt("Input.SensLink", &sensLink, 0, 1);
|
|
configRegisterInt("Input.MenuPointerSpeed", &menuPointerSpeed, 10, 500);
|
|
configRegisterInt("Input.MenuPointerMode", &menuPointerMode, 0, 1);
|
|
configRegisterInt("Input.HipfirePitchSpeed", &hipfirePitchSpeed, 10, 500);
|
|
configRegisterInt("Input.MouseInvertY", &mouseInvertY, 0, 1);
|
|
configRegisterInt("Input.MouseYScale", &mouseYScale, 1, 500);
|
|
configRegisterInt("Input.MouseSmoothing", &mouseSmoothing, 0, 90);
|
|
configRegisterInt("Input.MouseRawInput", &mouseRawInput, 0, 1);
|
|
configRegisterInt("Input.MouseDtDecouple", &mouseDtDecouple, 0, 1); /* D194(b) */
|
|
configRegisterInt("Input.MouseSensitivity", &mouseSensitivity, 1, 500); /* D238 */
|
|
configRegisterInt("Input.MouseAimCurve", &aimCurveGamma, 50, 400); /* D194(a), x100 */
|
|
configRegisterInt("Input.NaturalPitch", &naturalPitchMode, 0, 1); /* D194/D238 */
|
|
configRegisterInt("Input.PadDeadzone", &padDeadzone, 0, 30000);
|
|
configRegisterInt("Input.PadTriggerPct", &padTriggerPct, 1, 99);
|
|
configRegisterInt("Input.PadLookInvertY", &padLookInvertY, 0, 1);
|
|
/* v0.4.0 M3 (modern options wave): key-layout preset + crouch bind
|
|
* mode; both default to the N64-original FPS layout / hold. */
|
|
configRegisterInt("Input.Layout", &keyLayout, 0, 1);
|
|
configRegisterInt("Input.CrouchMode", &crouchMode, 0, 1);
|
|
|
|
/* D214: keyboard rebinding. Seed each buffer with its default so the knob
|
|
* is visible/editable in a fresh ge007.ini; configLoad() overwrites any the
|
|
* user set, then inputInit() calls inputRebuildBinds(). */
|
|
for (int a = 0; a < IA_COUNT; a++) {
|
|
strncpy(g_bindStr[a], kBindDefs[a].def, sizeof(g_bindStr[a]) - 1);
|
|
configRegisterString(kBindDefs[a].key, g_bindStr[a], sizeof(g_bindStr[a]));
|
|
}
|
|
}
|