Input and audio features D469-D472

Gamepad presets and per-seat rebinding (D469, building on
italoarruda's #109), master volume and output
device (D470), controller-family button names and player LEDs (D471), F10
owner-pad input and widescreen pillarbox polish (D472).

Refs: D469 D470 D471 D472

Co-authored-by: italoarruda <3868006+italoarruda@users.noreply.github.com>
This commit is contained in:
James Dansereauanditaloarruda committed 2026-10-06 18:35:41 -04:00
1 parent 9dd98bdb20
commit b735190fd9
24 files changed
+1285 -521

No files matched your search

-34
View File
@@ -2558,40 +2558,6 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
d75d_note_emit(videoGetFrameCount(), v_arr); // survived all rejection gates -> reaches GL
d303_note_emit(videoGetFrameCount(), v_arr); // TEMP D303
zf_note_emit(videoGetFrameCount(), v_arr); // TEMP D306/D308
/* TEMP D306 (M-201): depth-state census of emitted triangles in a frame
* window, GE_D306C="f0 f1". One line per tri: depth flags as sent to GL,
* geometry_mode, other_mode_l, tile-0 texture, NDC centroid, z/w range and
* min w (<=0 = crosses the eye plane). Cap 60000. */
{
static int d306c = -1; static uint32_t d306f0 = 0, d306f1 = 0, d306n = 0; static float d306wmax = 1e30f;
if (d306c < 0) {
const char* e = getenv("GE_D306C");
d306c = (e && sscanf(e, "%u %u %f", &d306f0, &d306f1, &d306wmax) >= 2) ? 1 : 0;
}
const uint32_t fc = videoGetFrameCount();
if (d306c && fc >= d306f0 && fc <= d306f1 && d306n < 60000) {
float cx = 0, cy = 0, zmin = 1e9f, zmax = -1e9f, wmin = 1e9f;
for (int i = 0; i < 3; i++) {
const float w = v_arr[i]->w;
if (w < wmin) wmin = w;
if (w > 0.f) {
const float zn = v_arr[i]->z / w;
cx += v_arr[i]->x / w; cy += v_arr[i]->y / w;
if (zn < zmin) zmin = zn;
if (zn > zmax) zmax = zn;
}
}
cx /= 3.f; cy /= 3.f;
if (wmin > d306wmax) goto d306c_skip; /* optional 3rd arg: near geometry only */
{
const void* t0 = rendering_state.textures[0] ? (const void*)rendering_state.textures[0]->first.texture_addr : nullptr;
d306n++;
fprintf(stderr, "D306C f=%u dm=%02x geom=%08x oml=%08x tex0=%p c=(%.3f,%.3f) z=[%.5f,%.5f] wmin=%.2f\n",
fc, rendering_state.depth_mode, rsp.geometry_mode, rdp.other_mode_l, t0, cx, cy, zmin, zmax, wmin);
}
}
d306c_skip:;
}
d75d_note_emit_z(videoGetFrameCount(), v_arr, (used_textures[0] || used_textures[1]) ? 1 : 0, comb->used_textures[0] ? 1 : 0);
for (int i = 0; i < 3; i++) {
float z = v_arr[i]->z, w = v_arr[i]->w;
+10
View File
@@ -31,6 +31,16 @@ u32 audioGetAiLengthBytes(void);
/* Queue the next block of mixed samples (len is in bytes). */
void audioSetNextBuffer(const s16 *buf, u32 len);
/* D470: output-device selection. audioDeviceRefresh() re-enumerates and
* returns the count; audioDeviceName(i) is valid until the next refresh.
* audioDeviceRequest("") = system default. The switch itself is applied on
* the audio thread. audioNotifyDeviceRemoved() is for SDL_AUDIODEVICEREMOVED. */
int audioDeviceRefresh(void);
const char *audioDeviceName(int i);
const char *audioDeviceCurrentName(void);
void audioDeviceRequest(const char *name);
void audioNotifyDeviceRemoved(u32 which);
#ifdef __cplusplus
}
#endif
+2
View File
@@ -17,6 +17,8 @@ extern "C" {
/* Load the config file (no-op if it doesn't exist yet). */
void configLoad(void);
int configUnknownKeyCount(void); /* D472: unknown ini keys in the last load */
const char *configUnknownKeyFirst(void); /* D472: first such dotted key */
/* Save the current values back to the config file. */
void configSave(void);
+21
View File
@@ -62,6 +62,7 @@ void inputSuspendForOverlay(void);
* navigation). Unaffected by the overlay's pad-swallow, which happens at our
* logic layer, not SDL's. No pad open -> 0. */
int inputPadButton(int idx, SDL_GameControllerButton b);
int inputOverlayOwnerPad(void); /* D472: pad driving the F10 overlay */
short inputPadAxis(int idx, SDL_GameControllerAxis a);
/* Queue a mouse-wheel weapon-cycle input (one short A-button press). Sign is
@@ -80,6 +81,22 @@ const char *inputBindingSlot(const char *key, int slot);
int inputBindingSetSlot(const char *key, int slot, int code);
int inputBindingResetKey(const char *key);
/* D469: gamepad action table, presets and per-seat rebinding.
* Input.PadPreset 0 = Jinx 1.1 (default), 3 = Custom (Input.Pad[N].<Action>).
* Seat = pads[] index. Sources are indices into the SDL-name table
* (inputPadSourceName); -1 = none. */
int inputPadActionCount(void);
const char *inputPadActionLabel(int act);
int inputPadActionForKey(const char *key); /* "Input.Pad.Fire" -> action, else -1 */
int inputPadSourceCount(void);
const char *inputPadSourceName(int src);
int inputPadSeatPresent(int seat);
unsigned inputPadHeldSources(int seat); /* bit i = source i held */
int inputPadPresetStep(int cur, int dir);
void inputPadBindingText(int seat, int act, int slot, char *out, int n);
int inputPadBindingSet(int seat, int act, int slot, int src); /* src < 0 clears */
int inputPadBindingReset(int act); /* all seats; returns seats changed */
/* D345(b): 1 while the 1:1 menu pointer owns cursor_h/v_pos (in a menu,
* abs pointer available, mouse used within the re-assert window). Port
* screens that teleport the crosshair should skip their snap when set. */
@@ -87,6 +104,10 @@ int inputMenuPointerLive(void);
/* Re-enumerate gamepads after a hotplug (SDL_CONTROLLERDEVICEADDED/REMOVED). */
void inputRescanPads(void);
int inputPadFamily(int seat); /* D471: 0 Xbox, 1 PS, 2 Nintendo */
void inputPadHelpFmt(char *out, size_t n, const char *tmpl); /* D471: {A}{B}{X}{Y}{BACK}{START} */
void inputPadHelpFmtFam(char *out, size_t n, const char *tmpl, int fam);
const char *inputPadSourceFamilyName(int seat, const char *src); /* D471: NULL = keep Xbox text */
void inputRequestRescan(void); /* D450: from either SDL event pump; consumed by inputUpdate() */
/* D401: Rumble Pak -> real gamepad haptics. The osMotor* shims in
+1
View File
@@ -72,6 +72,7 @@ const char *optionsRowLabel(int i);
int optionsRowIsSlider(int i);
int optionsRowIsBind(int i);
void optionsRowBeginBind(int i);
int optionsRowIsPadBind(int i); /* D469: gamepad-capture row (pad A may start it) */
int optionsRowIsBondChooser(int i); /* D353: the Bond-file chooser row
(front options screen only) */
/* D356: 1 when this row's value lives in the selected save file (content
+145 -6
View File
@@ -23,6 +23,21 @@
static SDL_AudioDeviceID dev = 0;
/* D470: port-level master volume (0..100, 100 = bit-identical passthrough)
* and output-device selection (empty = SDL's default device, the old
* behaviour). Both are plain config-backed values read where they are used. */
static int masterVolume = 100;
static char deviceName[256] = "";
/* D470: a UI/hot-plug request to re-open the device. Set from any thread,
* consumed (and dev closed/reopened) only on the audio-producer thread at the
* top of audioSetNextBuffer(), the sole thread that queues to / drains `dev`,
* so no queue call can race the close. */
static SDL_atomic_t s_devReopenReq;
/* D470: SDL_AUDIODEVICEREMOVED (video.c's event pump) -> fall back to default. */
static SDL_atomic_t s_devRemovedId;
/* src/audi.c: g_FrameSize + EXTRA_SAMPLES + 0x10, the exact sample count
* info->data is allocated for (audi.c:388). Zero until amCreateAudioManager
* has run. Used by the D204/F4 oversize guard below. */
@@ -72,6 +87,69 @@ static u32 lastBufferBytes = 0;
static u32 dropCount = 0;
static u32 oversizeCount = 0;
/* D470: open the configured device by name (NULL name = system default).
* A named device that cannot be opened logs a warning and falls back to the
* default, so a missing/removed interface never leaves the game silent. */
static SDL_AudioDeviceID audioOpenConfigured(const char *name, SDL_AudioSpec *have)
{
SDL_AudioSpec want = {0};
want.freq = 22050; /* GE's OUTPUT_RATE (src/audi.c) */
want.format = AUDIO_S16SYS;
want.channels = 2;
want.samples = (u16)bufferSize;
SDL_AudioDeviceID d = 0;
if (name && name[0]) {
d = SDL_OpenAudioDevice(name, 0, &want, have, 0);
if (d) {
sysLogPrintf(LOG_INFO, "audioInit: opened audio device by name: \"%s\"", name);
} else {
sysLogPrintf(LOG_WARNING,
"audioInit: audio device \"%s\" unavailable (%s); falling back to the default device",
name, SDL_GetError());
}
}
if (!d) d = SDL_OpenAudioDevice(NULL, 0, &want, have, 0);
return d;
}
/* D470: enumeration for the options UI. Names are copied (SDL's pointers are
* only valid until the next enumeration). */
#define AUDIO_MAXDEV 16
static char s_devNames[AUDIO_MAXDEV][128];
static int s_devCount = 0;
int audioDeviceRefresh(void)
{
s_devCount = 0;
if (!SDL_WasInit(SDL_INIT_AUDIO)) return 0;
int n = SDL_GetNumAudioDevices(0);
for (int i = 0; i < n && s_devCount < AUDIO_MAXDEV; i++) {
const char *nm = SDL_GetAudioDeviceName(i, 0);
if (!nm) continue;
snprintf(s_devNames[s_devCount], sizeof(s_devNames[0]), "%s", nm);
s_devCount++;
}
return s_devCount;
}
const char *audioDeviceName(int i)
{
return (i >= 0 && i < s_devCount) ? s_devNames[i] : NULL;
}
const char *audioDeviceCurrentName(void) { return deviceName; }
void audioDeviceRequest(const char *name)
{
snprintf(deviceName, sizeof(deviceName), "%s", name ? name : "");
SDL_AtomicSet(&s_devReopenReq, 1);
}
void audioNotifyDeviceRemoved(u32 which)
{
SDL_AtomicSet(&s_devRemovedId, (int)which);
}
int audioInit(void)
{
#if defined(_WIN32) && defined(SDL_HINT_AUDIODRIVER)
@@ -90,13 +168,13 @@ int audioInit(void)
sysLogPrintf(LOG_ERROR, "audioInit: SDL_InitSubSystem: %s", SDL_GetError());
return -1;
}
SDL_AudioSpec want = {0};
want.freq = 22050; /* GE's OUTPUT_RATE (src/audi.c) */
want.format = AUDIO_S16SYS;
want.channels = 2;
want.samples = (u16)bufferSize;
{ /* D470: list the outputs so a name can be copied into Audio.Device */
int nd = audioDeviceRefresh();
for (int k = 0; k < nd; k++)
sysLogPrintf(LOG_INFO, "audioInit: output device %d: \"%s\"", k, audioDeviceName(k));
}
SDL_AudioSpec have;
dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
dev = audioOpenConfigured(deviceName, &have);
if (!dev) {
sysLogPrintf(LOG_ERROR, "audioInit: SDL_OpenAudioDevice: %s", SDL_GetError());
return -1;
@@ -176,8 +254,35 @@ u32 audioGetAiLengthBytes(void)
return queued;
}
/* D470: runs on the producer thread only (see s_devReopenReq). The queued
* audio of the old device is dropped (a few tens of ms at most). */
static void audioServiceDeviceRequests(void)
{
int removed = SDL_AtomicSet(&s_devRemovedId, 0);
if (removed && dev && (SDL_AudioDeviceID)removed == dev) {
sysLogPrintf(LOG_WARNING,
"audio: output device removed (id %d); falling back to the default device", removed);
SDL_CloseAudioDevice(dev);
dev = 0;
deviceName[0] = 0; /* the configured device is gone: default until re-picked */
SDL_AtomicSet(&s_devReopenReq, 1);
}
if (SDL_AtomicSet(&s_devReopenReq, 0)) {
if (dev) { SDL_CloseAudioDevice(dev); dev = 0; }
SDL_AudioSpec have;
dev = audioOpenConfigured(deviceName, &have);
if (dev) {
SDL_PauseAudioDevice(dev, 0);
sysLogPrintf(LOG_INFO, "audio: device (re)opened at %d Hz", have.freq);
} else {
sysLogPrintf(LOG_ERROR, "audio: device reopen failed: %s", SDL_GetError());
}
}
}
void audioSetNextBuffer(const s16 *buf, u32 len)
{
audioServiceDeviceRequests();
if (!s_audioDumpChecked) {
s_audioDumpChecked = 1;
if (getenv("GE_AUDIODUMP")) {
@@ -304,6 +409,38 @@ void audioSetNextBuffer(const s16 *buf, u32 len)
}
}
/* D470: master volume. Applied to a scratch copy (the game's buffer is
* never modified) AFTER the game's own music/FX volumes, so it only scales
* the final mix. gain <= 100 means |s*gain/100| <= |s|: no clipping is
* possible. 100 (default) passes `buf` through untouched, byte-identical. */
{
static s16 gainBuf[8192 * 2];
int mv = masterVolume;
if (mv < 0) mv = 0;
if (mv < 100 && buf && len && len <= sizeof(gainBuf)) {
u32 n = len / 2u;
for (u32 i = 0; i < n; i++) gainBuf[i] = (s16)(((s32)buf[i] * mv) / 100);
buf = gainBuf;
}
}
/* D470 diag: GE_AUDIOGAINLOG=1 -> peak |sample| of the final (post-gain)
* buffer, max over each 1 s window. Cached env flag, cheap. */
if (buf && len && GE_ENVFLAG("GE_AUDIOGAINLOG")) {
static u64 winStart = 0; static s32 winPeak = 0;
u64 now = sysGetMicroseconds();
const s16 *sp = buf;
for (u32 i = 0; i < len / 2u; i++) {
s32 v = sp[i]; if (v < 0) v = -v;
if (v > winPeak) winPeak = v;
}
if (!winStart) winStart = now;
if (now - winStart >= 1000000ull) {
sysLogPrintf(LOG_NOTE, "D470 gain: master=%d peak=%d", masterVolume, (int)winPeak);
winStart = now; winPeak = 0;
}
}
if (dev && buf && len) {
if (audioGetSamplesBuffered() < queueLimit) {
SDL_QueueAudio(dev, buf, len);
@@ -322,4 +459,6 @@ PD_CONSTRUCTOR static void audioConfigInit(void)
{
configRegisterInt("Audio.BufferSize", &bufferSize, 0, 1 * 1024 * 1024);
configRegisterInt("Audio.QueueLimit", &queueLimit, 0, 1 * 1024 * 1024);
configRegisterInt("Audio.MasterVolume", &masterVolume, 0, 100); /* D470 */
configRegisterString("Audio.Device", deviceName, sizeof(deviceName)); /* D470 */
}
+13
View File
@@ -222,6 +222,10 @@ static char *trim(char *s)
return s;
}
/* D472: unknown ge007.ini keys seen by the last configLoad. */
static int s_unknownKeys;
static char s_unknownFirst[48];
static void applyKV(const char *dottedKey, const char *val)
{
for (int i = 0; i < numIntOpts; i++) {
@@ -270,8 +274,15 @@ static void applyKV(const char *dottedKey, const char *val)
}
}
sysLogPrintf(LOG_NOTE, "config: unknown key '%s' (ignored)", dottedKey);
if (s_unknownKeys++ == 0) { /* D472: surfaced once in the F10 overlay */
strncpy(s_unknownFirst, dottedKey, sizeof(s_unknownFirst) - 1);
s_unknownFirst[sizeof(s_unknownFirst) - 1] = 0;
}
}
int configUnknownKeyCount(void) { return s_unknownKeys; }
const char *configUnknownKeyFirst(void) { return s_unknownFirst; }
/* Preset plumbing (Original-N64 / port-defaults presets, D440;
* the D283 Deck preset): set / read a registered numeric option by its
* dotted key, with the same clamp a file load applies. Returns 1 if the key
@@ -338,6 +349,8 @@ void configLoad(void)
char line[512];
char section[64] = "";
s_unknownKeys = 0;
s_unknownFirst[0] = 0;
while (fgets(line, sizeof(line), f)) {
char *p = trim(line);
if (!*p || *p == '#' || *p == ';')
-326
View File
@@ -144,7 +144,6 @@ extern s32 chraiGetAIListID(AIRecord *AIList, bool *isGlobalAIList);
* the AI-command header just for one bit constant. */
#define D320_TARGET_AIM_ONLY 0x0020 /* "Aim at target instead of firing" */
#define D318_LIST_AI22 0x0417 /* Ourumov's execution list (ai_22) */
#define D318_OURUMOV_CHR 78
#define D318_TREVELYAN_CHR 67
#define D318_DEADLOCK_TICKS 600 /* 10 s at 60 Hz; escape takes < 2 s */
@@ -331,329 +330,4 @@ void d318WatchdogTick(void)
}
}
/* ------------------------------------------------------------------ */
/* D318T: diagnostic timeline for the "Ourumov shoots Trevelyan the */
/* moment his lines begin" report (2026-09-20). Env-gated, OFF by */
/* default; enable with GE_D318T=1. Port-side read-only: logs state */
/* transitions so a single playtest captures which combat trigger */
/* (gas bit 0x04 vs Bond near-miss flag) derailed ai_22, and how many */
/* ticks after the monologue started it landed. No game state is */
/* touched. */
/* */
/* Background: the ONLY path by which c78 live-fires Trevelyan */
/* (entityid 67) in this scene is section 0x2a -> off=307 */
/* actor_fire_or_aim_at_target_update succeeding while the pre-aim */
/* (ACT_ATTACK + AIM_ONLY|DONTTURN) is active. That requires a combat */
/* trigger inside the monologue loop: gas bit 0x04 (off=75) or */
/* chrIfNearMiss (off=87, CHRFLAG_NEAR_MISS -- set by Bond bullets */
/* passing c78's bounds, cleared on c78's next tick). The freeze */
/* (D318) and the immediate-fire are the two branches of the same */
/* derailed state (random aim-variant endframe vs the hold frame H). */
/* ------------------------------------------------------------------ */
void d318TimelineTick(void)
{
static int s_on = -1; /* -1 uncached, 0 off, 1 on (GE_D318T=1) */
static s32 s_obj = -1; /* last logged objectiveregisters1 */
static s32 s_off = -1; /* last logged c78 aioffset */
static s32 s_act = -1; /* last logged c78 actiontype */
static s32 s_atk = -1; /* last logged c78 act_attack.attacktype */
static s32 s_ent = -1; /* last logged c78 act_attack.entityid */
static s32 s_nm = -1; /* last logged c78 NEAR_MISS flag state */
static f32 s_dmg = -1.0f;
static bool s_dead = FALSE;
static s32 s_hb = 0;
static s32 s_det = 0; /* E1 determinism fingerprint counter */
ChrRecord *c78;
ChrRecord *c67;
if (s_on < 0)
{
const char *e = getenv("GE_D318T");
s_on = (e && e[0] == '1') ? 1 : 0;
}
if (!s_on)
{
return;
}
/* Unified chr view: real guards live in g_ChrSlots[0..g_NumChrSlots) with
* model != NULL; the background script entities (chrnum 0xFE, list IDs
* >= 0x1000) live in g_ActiveChrs. Both are ticked by chrlvAllChrTick,
* so both must be scanned for pins / fingerprints. (D320, 2026-09-22:
* the detector originally walked g_ActiveChrs only and was blind to
* every real guard.) */
static ChrRecord *s_all[96];
static int s_nall = 0;
s32 i;
s_nall = 0;
for (i = 0; i < g_NumChrSlots && s_nall < 96; i++)
{
if (g_ChrSlots[i].model != NULL)
{
s_all[s_nall++] = &g_ChrSlots[i];
}
}
for (i = 0; i < g_ActiveChrsCount && s_nall < 96; i++)
{
s_all[s_nall++] = &g_ActiveChrs[i];
}
c78 = d318wFindChr(D318_OURUMOV_CHR);
c67 = d318wFindChr(D318_TREVELYAN_CHR);
/* Objective bitfield transitions (the derail triggers live here: */
/* 0x04 gas/combat, 0x20 surrender/monologue, 0x40 flee). */
if (s_obj != objectiveregisters1)
{
osSyncPrintf("D318T: t=%d obj bits 0x%08x -> 0x%08x\n",
(int)g_GlobalTimer, (unsigned)s_obj, (unsigned)objectiveregisters1);
s_obj = objectiveregisters1;
}
if (c78)
{
s32 nm = (c78->chrflags & CHRFLAG_NEAR_MISS) != 0;
/* D318 anim-internals line: catches the kneel-attack route */
/* (unk54==0 => sub_GAME_7F0256F0 "chrAttackKneel" re-inits) and the */
/* hold/pin frame values, for the "bending down too far" report. */
{
static s32 s_tom = -1;
static u32 s_u54 = 0xFFFFFFFFu;
static f32 s_endf = -1.0f;
s32 tom = (s32)c78->act_attack.type_of_motion;
u32 u54 = c78->act_attack.unk54;
f32 endf = c78->model ? modelGetAnimEndFrame(c78->model) : -1.0f;
if (s_tom != tom || s_u54 != u54 || s_endf != endf)
{
osSyncPrintf("D318T: t=%d c78 anim mot=%d unk54=%u endframe=%.2f frame1=%.2f (off=%d)\n",
(int)g_GlobalTimer, tom, (unsigned)u54,
(double)endf,
(double)(c78->model ? modelGetAnimFrame(c78->model) : 0.0f),
(int)c78->aioffset);
s_tom = tom;
s_u54 = u54;
s_endf = endf;
}
}
if (s_off != (s32)c78->aioffset || s_act != (s32)c78->actiontype)
{
const char *phase = "?";
if (c78->aioffset >= 38 && c78->aioffset < 44) { phase = "pre-aim(off38)"; }
else if (c78->aioffset >= 44 && c78->aioffset < 69) { phase = "wait-loop(off44)"; }
else if (c78->aioffset >= 69 && c78->aioffset < 272){ phase = "MONOLOGUE(off69+)"; }
else if (c78->aioffset >= 272 && c78->aioffset < 295){ phase = "execution(0x13)"; }
else if (c78->aioffset >= 302 && c78->aioffset < 333){ phase = "COMBAT-0x2a"; }
osSyncPrintf("D318T: t=%d c78 off=%d [%s] act=%d atk=0x%x ent=%d\n",
(int)g_GlobalTimer, (int)c78->aioffset, phase,
(int)c78->actiontype, (unsigned)c78->act_attack.attacktype,
(int)c78->act_attack.entityid);
s_off = (s32)c78->aioffset;
s_act = (s32)c78->actiontype;
}
if (s_atk != (s32)c78->act_attack.attacktype || s_ent != (s32)c78->act_attack.entityid)
{
osSyncPrintf("D318T: t=%d c78 ATTACK CHANGE atk=0x%x ent=%d (off=%d)\n",
(int)g_GlobalTimer, (unsigned)c78->act_attack.attacktype,
(int)c78->act_attack.entityid, (int)c78->aioffset);
s_atk = (s32)c78->act_attack.attacktype;
s_ent = (s32)c78->act_attack.entityid;
}
if (s_nm != nm)
{
osSyncPrintf("D318T: t=%d c78 NEAR_MISS flag %s (Bond bullet passed his bounds)\n",
(int)g_GlobalTimer, nm ? "SET" : "cleared");
s_nm = nm;
}
}
if (c67)
{
bool dead = chrIsDead(c67);
if (s_dmg != c67->damage || s_dead != dead)
{
osSyncPrintf("D318T: t=%d c67(Trevelyan) dmg=%.2f/%.2f %s\n",
(int)g_GlobalTimer, (double)c67->damage, (double)c67->maxdamage,
dead ? "DEAD" : "");
s_dmg = c67->damage;
s_dead = dead;
}
}
/* Heartbeat while c78 is on his execution list, so gaps are visible. */
if (c78 && c78->ailist)
{
bool g = FALSE;
if (chraiGetAIListID(c78->ailist, &g) == D318_LIST_AI22 && (++s_hb >= 60))
{
s_hb = 0;
osSyncPrintf("D318T: t=%d c78 hb off=%d act=%d atk=0x%x ent=%d obj=0x%08x\n",
(int)g_GlobalTimer, (int)c78->aioffset, (int)c78->actiontype,
(unsigned)c78->act_attack.attacktype, (int)c78->act_attack.entityid,
(unsigned)objectiveregisters1);
}
}
/* E1 determinism fingerprint: one line per second over ALL active chrs */
/* + the PRNG seed + objective bits. Two no-input runs that differ here */
/* are non-deterministic in the port (foundational); identical logs */
/* mean run-to-run variation is input-timing-driven. */
if (++s_det >= 60)
{
s32 h = 0x811C9DC5;
s_det = 0;
for (i = 0; i < s_nall; i++)
{
ChrRecord *c = s_all[i];
union { f32 f; u32 u; } xf, yf, zf, df;
xf.f = c->prop ? c->prop->pos.x : 0.0f;
yf.f = c->prop ? c->prop->pos.y : 0.0f;
zf.f = c->prop ? c->prop->pos.z : 0.0f;
df.f = c->damage;
h ^= c->chrnum; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= (s32)c->actiontype; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= (s32)c->aioffset; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= xf.u; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= yf.u; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= zf.u; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= df.u; h = (h * 0x01000193) ^ 0x9E3779B9;
}
osSyncPrintf("D318T: DET t=%d seed=%08x%08x obj=0x%08x h=%08x nchrs=%d\n",
(int)g_GlobalTimer,
(unsigned)(u32)(g_randomSeed >> 32), (unsigned)(u32)g_randomSeed,
(unsigned)objectiveregisters1, (unsigned)h, s_nall);
}
/* D320: generic D318-class pin DETECTION for the sweep's flagged lists
* (Facility ai_19, Control ai_9, Depot ai_12 -- see findings D320).
* Detection only: it logs, it never intervenes; the recovery watchdog
* above stays hardcoded to ai_22 by design until a second list actually
* freezes in play (D320's explicit instruction).
*
* Signature: a chr in ACT_ATTACK whose attacktype is LIVE FIRE (no
* TARGET_AIM_ONLY bit -- it is supposed to be shooting) and whose
* modelGetAnimFrame is byte-stable for 600 consecutive ticks (10 s). A
* guard that should be firing but whose animation never advances is the
* exact D318 frozen shape on any list. The two legitimate long-stable
* states are excluded or self-clearing: monologue pre-aims keep the
* AIM_ONLY bit (excluded), and a working fire cycle re-inits through
* chrlvTickAttackCommon's "endframe <= frame" block, so its frame does
* not stay byte-stable for 10 s. One line at first detection, then one
* per further 600 stable ticks while it persists; a frozen run keeps
* printing, an escaped run stops after the single line (or none). */
{
typedef struct { s16 chrnum; u32 framebits; s32 stable; s32 nextlog; }
D320Pin;
static D320Pin s_pins[8];
static int s_npins = 0;
s32 i, j;
for (i = 0; i < s_nall; i++)
{
ChrRecord *c = s_all[i];
union { f32 f; u32 u; } fr;
D320Pin *p = NULL;
if (c->actiontype != ACT_ATTACK || c->model == NULL)
{
continue;
}
if ((s32)c->act_attack.attacktype & D320_TARGET_AIM_ONLY)
{
continue; /* legitimate hold pose (monologue pre-aim) */
}
fr.f = modelGetAnimFrame(c->model);
for (j = 0; j < s_npins; j++)
{
if (s_pins[j].chrnum == c->chrnum)
{
p = &s_pins[j];
break;
}
}
if (!p)
{
if (s_npins < 8)
{
p = &s_pins[s_npins++];
}
else
{
continue; /* table full: drop the newest, keep tracking */
}
p->chrnum = c->chrnum;
p->framebits = fr.u;
p->stable = 0;
p->nextlog = 600;
}
if (p->framebits == fr.u)
{
p->stable++;
}
else
{
p->framebits = fr.u;
p->stable = 0;
p->nextlog = 600;
}
if (p->stable >= p->nextlog)
{
bool g = FALSE;
s32 aid = c->ailist ? chraiGetAIListID(c->ailist, &g) : -1;
osSyncPrintf("D320T: t=%d PIN? c%d aiid=0x%04x%s off=%d atk=0x%x ent=%d "
"mot=%d unk54=%u frame=%.2f stable=%d ticks (D318-class pin candidate)\n",
(int)g_GlobalTimer, (int)c->chrnum,
(unsigned)aid, g ? "G" : "", (int)c->aioffset,
(unsigned)c->act_attack.attacktype,
(int)c->act_attack.entityid,
(s32)c->act_attack.type_of_motion,
(unsigned)c->act_attack.unk54,
(double)fr.f, p->stable);
p->nextlog = p->stable + 600;
}
}
/* Drop entries for chrs that left the tracked state so a later
* re-entry starts a fresh confirmation window. */
for (i = 0; i < s_npins; i++)
{
bool alive = FALSE;
for (j = 0; j < s_nall; j++)
{
ChrRecord *c = s_all[j];
if (c->chrnum == s_pins[i].chrnum && c->actiontype == ACT_ATTACK
&& c->model != NULL
&& !((s32)c->act_attack.attacktype & D320_TARGET_AIM_ONLY))
{
alive = TRUE;
break;
}
}
if (!alive)
{
s_pins[i] = s_pins[--s_npins];
i--;
}
}
}
}
#endif /* PORT */
+13 -5
View File
@@ -503,7 +503,8 @@ void frontOptionsMenuInterface(void)
* pages but cannot enter a modal that only a key can finish.
* Enter or a mouse click still starts ordinary key capture. */
const Uint8 *ks = SDL_GetKeyboardState(NULL);
if (ks[SDL_SCANCODE_RETURN] || ks[SDL_SCANCODE_KP_ENTER] ||
if (optionsRowIsPadBind(i) || /* D469: pad-driven capture; arming waits for the A release */
ks[SDL_SCANCODE_RETURN] || ks[SDL_SCANCODE_KP_ENTER] ||
(SDL_GetMouseState(NULL, NULL) & SDL_BUTTON(SDL_BUTTON_LEFT)))
optionsRowBeginBind(i);
} else if (optionsRowIsReset(i)) {
@@ -782,10 +783,17 @@ Gfx *frontOptionsMenuDraw(Gfx *DL)
}
if (strcmp(optionsRowLabel(activeHeader()), "MOVEMENT") == 0 ||
strcmp(optionsRowLabel(activeHeader()), "ACTIONS") == 0) {
DL = ink(DL, ROW_X, rowY(s_rowN + 2),
optionsBindingCaptureActive()
? "Press key/mouse B/ESC cancel DEL clear\n"
: "Keys/mouse only Enter: bind B: back\n", INK_DIM);
static char d471buf0[96]; /* D471: pad names for the menu pad's family */
inputPadHelpFmt(d471buf0, sizeof(d471buf0), optionsBindingCaptureActive()
? "Press key/mouse {B}/ESC cancel DEL clear\n"
: "Keys/mouse only Enter: bind {B}: back\n");
DL = ink(DL, ROW_X, rowY(s_rowN + 2), d471buf0, INK_DIM);
} else if (strcmp(optionsRowLabel(activeHeader()), "CONTROLLER") == 0) {
static char d471buf1[96]; /* D471: pad names for the menu pad's family */
inputPadHelpFmt(d471buf1, sizeof(d471buf1), optionsBindingCaptureActive()
? "Press pad button tap {B} cancel hold Back clear\n"
: "{A}: bind Left/Right: slot {Y}: clear {B}: back\n");
DL = ink(DL, ROW_X, rowY(s_rowN + 2), d471buf1, INK_DIM);
} else if (s_rowTotal > ROWS_PER_PAGE) {
/* D406b: bottom hint on every page of a multi-page section (11
* rows per page leaves the last line of the paper for it). */
+634 -76
View File
@@ -382,17 +382,311 @@ static struct player *slotPlayer(void)
* and hung the game in bondinvCycleForward after a respawn). Match play
* routes use/reload through the native B button instead, and reads/writes
* the slot player's fields directly. */
static int padBPrev[MAX_PADS]; /* B/Y edges; track through menus too */
static int padYPrev[MAX_PADS];
static int padAPrev[MAX_PADS]; /* D416: slots 1+ use/reload edges */
static int padXPrev[MAX_PADS];
static int padSelectPrev = 0; /* Select (BACK) edge: overlay toggle */
static int padTriggerPct; /* defined (with its default) further down */
static u32 padActPrev[MAX_PADS]; /* D469: per-slot previous action-held bitset (edges track through menus too) */
static int padSelectPrev[MAX_PADS]; /* Select (BACK) edge per pad (D472: was one shared flag) */
static int s_ovOwner = -1; /* D472: pad that opened the F10 overlay (-1 = keyboard/none) */
/* D472: the pad that drives the F10 overlay: the opener while it is attached,
* else the lowest attached pad (keyboard-opened, or the owner detached), else 0. */
int inputOverlayOwnerPad(void)
{
if (s_ovOwner >= 0 && s_ovOwner < MAX_PADS && pads[s_ovOwner]) return s_ovOwner;
for (int i = 0; i < MAX_PADS; i++)
if (pads[i]) return i;
return 0;
}
/* ------------------------------------------------------------------------
* D469: data-driven gamepad action table + presets + per-seat rebinding.
*
* Sources are SDL controller buttons plus the two analog triggers treated as
* digital (padTriggerPct). Each action is bound to up to two sources, written
* in the ini as SDL names joined by a comma ("a,x", "righttrigger", "none").
* Input.PadPreset: 0 = Jinx 1.1 (the pre-D469 hard-wired mapping; default),
* 1/2 = Jinx 1.2/1.3 (NOT defined by D394: reserved, resolve to 1.1),
* 3 = Custom (the per-seat Input.Pad.* strings). Seat 0 keys are
* Input.Pad.<Action>; seats 1..3 are Input.Pad2/3/4.<Action>. Seat = pads[]
* index (D448), not the controller slot.
*
* Not rebindable (menu lockout guard): menu accept/cancel (A,X -> native A;
* B,Y -> native B outside a playable stage), D-pad -> N64 D-pad, sticks,
* Back (opens the overlay). Start is forced back to `start` if a Custom table
* leaves it unbound. Threading: the poll thread reads an immutable published
* table (atomic pointer) rebuilt into a spare buffer only when the config
* signature changes; no strtok.
* ------------------------------------------------------------------------ */
enum { PS_A, PS_B, PS_X, PS_Y, PS_LB, PS_RB, PS_LS, PS_RS, PS_START,
PS_DUP, PS_DDOWN, PS_DLEFT, PS_DRIGHT, PS_LT, PS_RT, PS_COUNT };
#define PSM(s) (1u << (s))
#define PS_TRIGGERS (PSM(PS_LT) | PSM(PS_RT))
static const struct { const char *name; int btn; } kPadSrc[PS_COUNT] = {
[PS_A] = { "a", SDL_CONTROLLER_BUTTON_A },
[PS_B] = { "b", SDL_CONTROLLER_BUTTON_B },
[PS_X] = { "x", SDL_CONTROLLER_BUTTON_X },
[PS_Y] = { "y", SDL_CONTROLLER_BUTTON_Y },
[PS_LB] = { "leftshoulder", SDL_CONTROLLER_BUTTON_LEFTSHOULDER },
[PS_RB] = { "rightshoulder", SDL_CONTROLLER_BUTTON_RIGHTSHOULDER },
[PS_LS] = { "leftstick", SDL_CONTROLLER_BUTTON_LEFTSTICK },
[PS_RS] = { "rightstick", SDL_CONTROLLER_BUTTON_RIGHTSTICK },
[PS_START] = { "start", SDL_CONTROLLER_BUTTON_START },
[PS_DUP] = { "dpup", SDL_CONTROLLER_BUTTON_DPAD_UP },
[PS_DDOWN] = { "dpdown", SDL_CONTROLLER_BUTTON_DPAD_DOWN },
[PS_DLEFT] = { "dpleft", SDL_CONTROLLER_BUTTON_DPAD_LEFT },
[PS_DRIGHT] = { "dpright", SDL_CONTROLLER_BUTTON_DPAD_RIGHT },
[PS_LT] = { "lefttrigger", -1 },
[PS_RT] = { "righttrigger", -1 },
};
enum { PA_FIRE, PA_AIM, PA_USE, PA_RELOAD, PA_CROUCH, PA_NEXTWPN, PA_PREVWPN,
PA_GADGET, PA_START, PA_COUNT };
/* Jinx 1.1 (D394) = def. Fire/Aim analog triggers act in any state; every
* digital source only acts while a stage is playable (facePlayable). */
static const struct { const char *key; const char *label; const char *def; } kPadAct[PA_COUNT] = {
[PA_FIRE] = { "Fire", "Fire", "righttrigger" },
[PA_AIM] = { "Aim", "Aim", "lefttrigger,leftshoulder" },
[PA_USE] = { "Use", "Use / action", "a" },
[PA_RELOAD] = { "Reload", "Reload", "x" },
[PA_CROUCH] = { "Crouch", "Crouch", "leftstick,rightstick" },
[PA_NEXTWPN] = { "NextWeapon", "Next weapon", "y" },
[PA_PREVWPN] = { "PrevWeapon", "Previous weapon", "none" },
[PA_GADGET] = { "Gadget", "Cycle gadget", "b" },
[PA_START] = { "Start", "Start / pause", "start" },
};
typedef struct { u32 mask[PA_COUNT]; } PadBinds;
typedef struct { PadBinds seat[MAX_PADS]; } PadTable;
static int padPreset = 0; /* Input.PadPreset */
static char padStr[MAX_PADS][PA_COUNT][32]; /* Input.Pad[N].<Action> */
static char padStrKey[MAX_PADS][PA_COUNT][40];
static PadTable padTabBuf[2];
static SDL_atomic_t padTabPub; /* published PadTable* */
static unsigned padTabSig = 0;
static int padTabValid = 0;
/* Parse "a,x" / "none" into a source mask (no strtok). *bad counts unknown names. */
static u32 padParseMask(const char *s, int *bad)
{
u32 m = 0;
while (s && *s) {
while (*s == ' ' || *s == ',') s++;
const char *e = s;
while (*e && *e != ',' && *e != ' ') e++;
size_t n = (size_t)(e - s);
if (n) {
int hit = 0;
for (int i = 0; i < PS_COUNT; i++)
if (strlen(kPadSrc[i].name) == n && !SDL_strncasecmp(s, kPadSrc[i].name, n)) {
m |= PSM(i);
hit = 1;
break;
}
if (!hit && !(n == 4 && !SDL_strncasecmp(s, "none", 4)) && bad) (*bad)++;
}
s = e;
}
return m;
}
static void padBuildBinds(PadBinds *b, int seat, int custom, int *bad)
{
for (int a = 0; a < PA_COUNT; a++)
b->mask[a] = padParseMask(custom ? padStr[seat][a] : kPadAct[a].def, bad);
if (!b->mask[PA_START]) b->mask[PA_START] = PSM(PS_START); /* lockout guard: keep a pause path */
}
static int padPresetCustom(void) { return padPreset >= 3; }
/* Signature of everything the table depends on (preset only unless Custom). */
static unsigned padComputeSig(void)
{
unsigned h = 2166136261u ^ (unsigned)padPreset;
if (padPresetCustom())
for (int s = 0; s < MAX_PADS; s++)
for (int a = 0; a < PA_COUNT; a++)
for (const char *p = padStr[s][a]; *p; p++) h = (h ^ (unsigned char)*p) * 16777619u;
return h;
}
/* Poll thread: rebuild into the spare buffer and publish atomically. */
static const PadTable *padTabGet(void)
{
static SDL_SpinLock rebuildLock; /* callers on several threads (poll, overlay, UI) */
unsigned sig = padComputeSig();
if (!padTabValid || sig != padTabSig) {
SDL_AtomicLock(&rebuildLock);
if (padTabValid && padComputeSig() == padTabSig) { /* another thread just rebuilt */
SDL_AtomicUnlock(&rebuildLock);
return (const PadTable *)SDL_AtomicGetPtr((void **)&padTabPub);
}
PadTable *cur = (PadTable *)SDL_AtomicGetPtr((void **)&padTabPub);
PadTable *spare = (cur == &padTabBuf[0]) ? &padTabBuf[1] : &padTabBuf[0];
int bad = 0;
for (int s = 0; s < MAX_PADS; s++) padBuildBinds(&spare->seat[s], s, padPresetCustom(), &bad);
SDL_AtomicSetPtr((void **)&padTabPub, spare);
padTabSig = sig;
padTabValid = 1;
sysLogPrintf(LOG_INFO, "input: pad table rebuilt (preset %d%s%s, %d unknown name(s))",
padPreset, padPresetCustom() ? " custom" : "",
padPreset == 1 || padPreset == 2 ? " [1.2/1.3 undefined -> 1.1]" : "", bad);
SDL_AtomicUnlock(&rebuildLock);
return spare;
}
return (const PadTable *)SDL_AtomicGetPtr((void **)&padTabPub);
}
/* Current raw source state of a pad (digital buttons + triggers). */
static u32 padReadRaw(SDL_GameController *pad)
{
u32 raw = 0;
if (!pad) return 0;
for (int i = 0; i < PS_LT; i++)
if (SDL_GameControllerGetButton(pad, (SDL_GameControllerButton)kPadSrc[i].btn)) raw |= PSM(i);
int trigPt = padTriggerPct * 327; /* % of the 0..32767 trigger travel */
if (SDL_GameControllerGetAxis(pad, SDL_CONTROLLER_AXIS_TRIGGERLEFT) > trigPt) raw |= PSM(PS_LT);
if (SDL_GameControllerGetAxis(pad, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) > trigPt) raw |= PSM(PS_RT);
return raw;
}
static u32 padActHeld(const PadBinds *b, u32 raw)
{
u32 h = 0;
for (int a = 0; a < PA_COUNT; a++)
if (raw & b->mask[a]) h |= 1u << a;
return h;
}
typedef struct { int facePlayable; int slotNative; int southpaw; } PadMapCtx;
typedef struct { unsigned button; int crouch; int gadgetEdge; u32 actHeld; } PadMapOut;
/* The pure mapping: raw source bits + bindings + previous action state ->
* N64 button mask / crouch / gadget edge. buttonIn is whatever the keyboard
* / stick code already put in the mask (the southpaw swap acts on it exactly
* as the pre-D469 inline code did). No SDL, no game state: GE_PADMAPTEST. */
static void padMapPure(const PadBinds *b, u32 raw, u32 actPrev, unsigned buttonIn,
const PadMapCtx *c, PadMapOut *o)
{
unsigned button = buttonIn;
u32 held = padActHeld(b, raw);
if (raw & b->mask[PA_FIRE] & PS_TRIGGERS) button |= GE_CONT_G;
if (raw & b->mask[PA_AIM] & PS_TRIGGERS) button |= GE_CONT_R;
if (c->southpaw) { /* Wave A: swap fire/grenade after the raw trigger edges */
unsigned t = button;
button = (t & ~(GE_CONT_G | GE_CONT_R))
| ((t & GE_CONT_G) ? GE_CONT_R : 0)
| ((t & GE_CONT_R) ? GE_CONT_G : 0);
}
o->crouch = 0;
o->gadgetEdge = 0;
u32 edge = held & ~actPrev;
if (c->facePlayable) {
if (held & (1u << PA_CROUCH)) o->crouch = 1;
if (c->slotNative && (edge & ((1u << PA_USE) | (1u << PA_RELOAD)))) button |= GE_CONT_B;
if (edge & (1u << PA_GADGET)) o->gadgetEdge = 1;
if (edge & (1u << PA_NEXTWPN)) button |= GE_CONT_A;
if (edge & (1u << PA_PREVWPN)) button |= GE_CONT_A | GE_CONT_G; /* same backward-cycle idiom as the mouse wheel */
if (raw & b->mask[PA_AIM] & ~PS_TRIGGERS) button |= GE_CONT_R; /* digital sources (LB aim): playable only, after the swap */
if (raw & b->mask[PA_FIRE] & ~PS_TRIGGERS) button |= GE_CONT_G;
} else {
if (raw & (PSM(PS_A) | PSM(PS_X))) button |= GE_CONT_A; /* menu lockout guard: fixed native accept/cancel */
if (raw & (PSM(PS_B) | PSM(PS_Y))) button |= GE_CONT_B;
}
if (held & (1u << PA_START)) button |= GE_CONT_START;
if (raw & PSM(PS_DUP)) button |= GE_CONT_UP;
if (raw & PSM(PS_DDOWN)) button |= GE_CONT_DOWN;
if (raw & PSM(PS_DLEFT)) button |= GE_CONT_LEFT;
if (raw & PSM(PS_DRIGHT)) button |= GE_CONT_RIGHT;
o->button = button;
o->actHeld = held;
}
/* Legacy reference: the pre-D469 inline pad block, transcribed verbatim from
* release/v0.4.1 (input.c, trigger/face-button section) onto raw source bits.
* Used ONLY by GE_PADMAPTEST to prove preset 0 is byte-identical. */
static void padMapLegacy(u32 raw, u32 prevA, u32 prevB, u32 prevX, u32 prevY, unsigned buttonIn,
const PadMapCtx *c, PadMapOut *o)
{
unsigned button = buttonIn;
int crouchNow = 0, gadget = 0;
if (raw & PSM(PS_RT)) button |= GE_CONT_G;
if (raw & PSM(PS_LT)) button |= GE_CONT_R;
if (c->southpaw) {
int t = button;
button = (t & ~(GE_CONT_G | GE_CONT_R))
| ((t & GE_CONT_G) ? GE_CONT_R : 0)
| ((t & GE_CONT_R) ? GE_CONT_G : 0);
}
int padA = !!(raw & PSM(PS_A)), padX = !!(raw & PSM(PS_X));
int padB = !!(raw & PSM(PS_B)), padY = !!(raw & PSM(PS_Y));
if (c->facePlayable) {
if ((raw & PSM(PS_LS)) || (raw & PSM(PS_RS))) crouchNow = 1;
if (c->slotNative) {
if ((padA && !prevA) || (padX && !prevX)) button |= GE_CONT_B;
}
if (padB && !prevB) gadget = 1;
if (padY && !prevY) button |= GE_CONT_A;
} else {
if (padA || padX) button |= GE_CONT_A;
if (padB || padY) button |= GE_CONT_B;
}
if (c->facePlayable && (raw & PSM(PS_LB))) button |= GE_CONT_R;
if (raw & PSM(PS_START)) button |= GE_CONT_START;
if (raw & PSM(PS_DUP)) button |= GE_CONT_UP;
if (raw & PSM(PS_DDOWN)) button |= GE_CONT_DOWN;
if (raw & PSM(PS_DLEFT)) button |= GE_CONT_LEFT;
if (raw & PSM(PS_DRIGHT)) button |= GE_CONT_RIGHT;
o->button = button;
o->crouch = crouchNow;
o->gadgetEdge = gadget;
o->actHeld = 0;
}
/* Seat that feeds controller slot idx (pads[] index; D448/D416). */
static int padSeatForSlot(int idx)
{
if (idx < 0) idx = 0;
if (idx >= MAX_PADS) idx = MAX_PADS - 1;
if (inputMpSplit() && inputMpKbmP1() && idx > 0) return idx - 1;
return idx;
}
/* D401: per-pad haptics (N64 Rumble Pak -> SDL_GameControllerRumble).
* padRumbleOn is detected when the pad opens (inputOpenPads); gRumbleScale
* is the single global Input.RumbleScale config value (0 = silent,
* 1 = full), applied to every pad. */
static int padRumbleOn[MAX_PADS];
/* D471: controller family per seat, for button NAMES in the PC menus (no
* glyphs). Detected once when a pad is seated; 0 = Xbox-style (default,
* also Steam Deck / unknown), 1 = PlayStation, 2 = Nintendo. SDL's default
* GAMECONTROLLER_USE_BUTTON_LABELS=1 means BUTTON_A is the button printed
* "A" on a Nintendo pad, so only shoulders/triggers/Back/Start differ there. */
enum { PADFAM_XBOX = 0, PADFAM_PS = 1, PADFAM_NINTENDO = 2 };
static u8 padFamily[MAX_PADS];
static void padDetectFamily(int seat)
{
padFamily[seat] = PADFAM_XBOX;
#if SDL_VERSION_ATLEAST(2, 0, 12)
switch (SDL_GameControllerGetType(pads[seat])) {
case SDL_CONTROLLER_TYPE_PS3:
case SDL_CONTROLLER_TYPE_PS4:
#if SDL_VERSION_ATLEAST(2, 0, 14)
case SDL_CONTROLLER_TYPE_PS5:
#endif
padFamily[seat] = PADFAM_PS; break;
case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO:
#if SDL_VERSION_ATLEAST(2, 24, 0)
case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT:
case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT:
case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR:
#endif
padFamily[seat] = PADFAM_NINTENDO; break;
default: break;
}
#endif
}
static float gRumbleScale = 0.5f;
static int mouseEnabled = 1;
@@ -764,10 +1058,14 @@ static void inputOpenPads(void)
if (pads[s] && !SDL_GameControllerGetAttached(pads[s])) {
sysLogPrintf(LOG_NOTE, "input: seat %d pad detached%s", s,
inMatch ? " (seat kept for the match)" : "");
#if SDL_VERSION_ATLEAST(2, 0, 12)
SDL_GameControllerSetPlayerIndex(pads[s], -1); /* D471: LED off */
#endif
SDL_GameControllerClose(pads[s]);
pads[s] = NULL;
padInst[s] = -1;
padRumbleOn[s] = 0;
padFamily[s] = PADFAM_XBOX;
if (inMatch) s_seatCompactOwed = 1;
}
}
@@ -781,6 +1079,7 @@ static void inputOpenPads(void)
pads[d] = pads[s]; pads[s] = NULL;
padInst[d] = padInst[s]; padInst[s] = -1;
padRumbleOn[d] = padRumbleOn[s]; padRumbleOn[s] = 0;
padFamily[d] = padFamily[s]; padFamily[s] = PADFAM_XBOX; /* D471 */
sysLogPrintf(LOG_NOTE, "input: pad '%s' moved seat %d -> %d",
SDL_GameControllerName(pads[d]), s, d);
}
@@ -809,6 +1108,7 @@ static void inputOpenPads(void)
if (pads[seat]) {
padInst[seat] = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(pads[seat]));
padDetectRumble(seat);
padDetectFamily(seat); /* D471 */
sysLogPrintf(LOG_NOTE, "input: opened gamepad %d '%s' as seat %d (rumble %s)",
i, SDL_GameControllerName(pads[seat]), seat, padRumbleOn[seat] ? "yes" : "no");
}
@@ -831,6 +1131,13 @@ static void inputOpenPads(void)
/* keyboard/mouse is controller 0; seat s is controller s + 1 */
connectedMask = (0x1 | (seatMask << 1)) & ((1 << MAX_PADS) - 1);
}
#if SDL_VERSION_ATLEAST(2, 0, 12)
/* D471 (PD-port parity): the pad's player LED shows its player number;
* seat s is player s + 1 when keyboard/mouse is P1. Re-applied on every
* (re)scan, so compaction and MP-mode changes pick it up. */
for (int s = 0; s < MAX_PADS; ++s)
if (pads[s]) SDL_GameControllerSetPlayerIndex(pads[s], s + (inputMpKbmP1() ? 1 : 0));
#endif
{ /* D416 test harness: GE_MPVIRT=<n> reports n extra idle controllers
* (1..3) so the MP menu unlocks and a 2P+ stage loads with no pads. */
const char *v = getenv("GE_MPVIRT");
@@ -1114,6 +1421,7 @@ static void pcOptionsKeyboardPad(const Uint8 *ks, Uint32 mb, int blocked,
static int bindsVersion = 0; /* D380/D386: versioned migration of effective ini binds */
static int crouchMode = 0; /* 0 = hold, 1 = toggle (latched crouch input) */
static void padMapSelfTest(void);
int inputInit(void)
{
if (!SDL_WasInit(SDL_INIT_GAMECONTROLLER)) {
@@ -1166,6 +1474,8 @@ int inputInit(void)
applyCursorVisibility(); /* hide the OS cursor if we start focused */
inputApplyMouseRequests(); /* inputInit runs on the host thread */
if (GE_ENVFLAG("GE_PADMAPTEST")) padMapSelfTest(); /* D469 */
sysLogPrintf(LOG_INFO, "input: ready (mask=0x%x, %d controller(s), aimSpeed=%d)",
connectedMask, numControllers, mouseAimSpeed);
return connectedMask;
@@ -1741,24 +2051,40 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
* (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()) s_ovOwner = -1; /* D472: ownership lasts one open */
/* D472: a non-zero owner pad is swallowed too (neutral) while the overlay
* is open; pad 0 below still hosts the overlay tick. */
if (idx != 0 && optionsOverlayIsOpen() && idx == inputOverlayOwnerPad()) {
padActPrev[idx] = pads[idx] ? padActHeld(&padTabGet()->seat[padSeatForSlot(idx)], padReadRaw(pads[idx])) : 0;
if (stick_x) *stick_x = 0;
if (stick_y) *stick_y = 0;
return 0;
}
if (idx == 0 && optionsOverlayIsOpen()) {
const Uint8 *overlayKs = SDL_GetKeyboardState(NULL);
s_useHeldPrev = actHeld(overlayKs, IA_CANCEL) ||
(pads[0] && SDL_GameControllerGetButton(pads[0], SDL_CONTROLLER_BUTTON_A));
s_reloadHeldPrev = actHeld(overlayKs, IA_RELOAD) ||
(pads[0] && SDL_GameControllerGetButton(pads[0], SDL_CONTROLLER_BUTTON_X));
/* D469: pad use/reload/edge state follows the bound actions, not fixed A/X/B/Y */
u32 ovHeld = pads[0] ? padActHeld(&padTabGet()->seat[0], padReadRaw(pads[0])) : 0;
s_useHeldPrev = actHeld(overlayKs, IA_CANCEL) || (ovHeld & (1u << PA_USE));
s_reloadHeldPrev = actHeld(overlayKs, IA_RELOAD) || (ovHeld & (1u << PA_RELOAD));
s_crouchLatch = 0;
s_crouchHeldPrev = 0;
inputDropCrouch();
padBPrev[0] = pads[0] ? SDL_GameControllerGetButton(pads[0], SDL_CONTROLLER_BUTTON_B) : 0;
padYPrev[0] = pads[0] ? SDL_GameControllerGetButton(pads[0], SDL_CONTROLLER_BUTTON_Y) : 0;
/* 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;
padActPrev[0] = ovHeld;
/* D472: the OWNER pad's Select closes the overlay; every pad's edge is
* tracked here and on the open path alike, so a button held across
* the transition cannot immediately re-toggle it. */
{
int own = inputOverlayOwnerPad();
int closeNow = 0;
for (int i = 0; i < MAX_PADS; i++) {
int selNow = pads[i] ? SDL_GameControllerGetButton(pads[i], SDL_CONTROLLER_BUTTON_BACK) : 0;
if (i == own && selNow && !padSelectPrev[i]) closeNow = 1;
padSelectPrev[i] = selNow;
}
if (closeNow) optionsOverlayToggle();
}
optionsOverlayHandleInput();
if (stick_x) *stick_x = 0;
if (stick_y) *stick_y = 0;
@@ -1925,8 +2251,9 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
* combined edges prevent a held input from repeatedly using a door.
* In menus/watch/tank the pad keeps native accept/cancel buttons. */
SDL_GameController *slot0Pad = slotPad(0);
int padUse = slot0Pad && SDL_GameControllerGetButton(slot0Pad, SDL_CONTROLLER_BUTTON_A);
int padReload = slot0Pad && SDL_GameControllerGetButton(slot0Pad, SDL_CONTROLLER_BUTTON_X);
u32 slot0Held = slot0Pad ? padActHeld(&padTabGet()->seat[padSeatForSlot(0)], padReadRaw(slot0Pad)) : 0;
int padUse = (slot0Held & (1u << PA_USE)) != 0; /* D469: bound actions (Jinx 1.1: A / X) */
int padReload = (slot0Held & (1u << PA_RELOAD)) != 0;
int useNow = scriptIsActive(0) ? scriptSlots[0].use : (actHeld(ks, IA_CANCEL) || padUse);
int reloadNow = scriptIsActive(0) ? scriptSlots[0].reload : (actHeld(ks, IA_RELOAD) || padReload);
int playable = inputCanUseGameplayActions(menuMode);
@@ -2404,70 +2731,37 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
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;
/* Wave A: southpaw swaps the fire (G) and grenade (R) trigger actions
* (right<->left trigger), after the raw edges above are captured. */
if (padSouthpaw) {
int t = button;
button = (t & ~(GE_CONT_G | GE_CONT_R))
| ((t & GE_CONT_G) ? GE_CONT_R : 0)
| ((t & GE_CONT_R) ? GE_CONT_G : 0);
/* D469: triggers/faces/shoulders/Start/D-pad via the data-driven
* action table (preset 0 == the pre-D469 mapping: RT fire, LT/LB aim,
* A use, X reload, B gadget, Y next weapon, stick clicks crouch,
* Start; menus keep native A/X accept + B/Y cancel). Xbox 1.1
* Jinx-style; RB's remaster HD toggle is unavailable. */
{
PadMapCtx pc;
PadMapOut po;
pc.facePlayable = (idx == 0 || inputMpSplit()) && inputCanUseGameplayActions(padMenuMode);
pc.slotNative = (idx > 0 || inputMpPadsOwnP1());
pc.southpaw = padSouthpaw;
const PadBinds *pb = &padTabGet()->seat[padSeatForSlot(idx)];
padMapPure(pb, padReadRaw(pad), padActPrev[idx], button, &pc, &po);
button = po.button;
if (po.crouch) crouchNow = 1;
if (po.gadgetEdge) inputCycleGadget();
padActPrev[idx] = po.actHeld;
}
/* Xbox 1.1 Jinx-style gameplay: A=use, X=reload (handled by the
* keyboard action gate), B=gadgets, Y=weapons, stick clicks=crouch,
* LB=aim. RB's remaster HD toggle is unavailable in the N64 port.
* Menus/watch/tank retain native accept/cancel mappings. */
int padA = SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_A);
int padX = SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_X);
int padB = SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_B);
int padY = SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_Y);
int facePlayable = (idx == 0 || inputMpSplit()) && inputCanUseGameplayActions(padMenuMode);
if (facePlayable) {
if (SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_LEFTSTICK) ||
SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_RIGHTSTICK))
crouchNow = 1;
if (idx > 0 || inputMpPadsOwnP1()) {
/* Slots 1+ (and a pad-owned P1) have no keyboard block: pad A / X = the native B
* button on the press edge (use, reload when nothing to use). */
if ((padA && !padAPrev[idx]) || (padX && !padXPrev[idx]))
button |= GE_CONT_B;
}
if (padB && !padBPrev[idx]) inputCycleGadget();
if (padY && !padYPrev[idx]) button |= GE_CONT_A;
} else {
if (padA || padX) button |= GE_CONT_A;
if (padB || padY) button |= GE_CONT_B;
}
padBPrev[idx] = padB;
padYPrev[idx] = padY;
padAPrev[idx] = padA;
padXPrev[idx] = padX;
if (facePlayable && SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_LEFTSHOULDER))
button |= GE_CONT_R;
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;
/* D472: only opens here (closing is the owner's, in the overlay path) */
if (selNow && !padSelectPrev[idx] && !frontOptionsBlocksOverlay() && !optionsOverlayIsOpen()) {
s_ovOwner = idx;
optionsOverlayToggle();
}
padSelectPrev[idx] = selNow;
}
}
@@ -2645,6 +2939,73 @@ int inputReleaseCapture(void)
* 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. */
/* D471: family of a seat's pad (GE_PADFAMILY=xbox|ps|nintendo overrides,
* a cached dev knob for headless screenshots). */
int inputPadFamily(int seat)
{
static int ov = -2;
if (ov == -2) {
const char *e = getenv("GE_PADFAMILY");
ov = !e ? -1 : !SDL_strcasecmp(e, "ps") ? PADFAM_PS
: !SDL_strcasecmp(e, "nintendo") ? PADFAM_NINTENDO
: !SDL_strcasecmp(e, "xbox") ? PADFAM_XBOX : -1;
}
if (ov >= 0) return ov;
return (seat >= 0 && seat < MAX_PADS && pads[seat]) ? padFamily[seat] : PADFAM_XBOX;
}
/* D471: expand {A} {B} {X} {Y} {BACK} {START} in a menu help template with
* the names of the menu pad's family (seat 0, which drives the menus). Xbox
* expands to exactly the old text. */
void inputPadHelpFmtFam(char *out, size_t n, const char *tmpl, int fam)
{
static const char *const names[3][6] = {
{ "A", "B", "X", "Y", "BACK", "START" },
{ "CROSS", "CIRCLE", "SQUARE", "TRIANGLE", "SHARE", "OPTIONS" },
{ "A", "B", "X", "Y", "MINUS", "PLUS" },
};
static const char *const keys[6] = { "{A}", "{B}", "{X}", "{Y}", "{BACK}", "{START}" };
if (fam < 0 || fam > 2) fam = PADFAM_XBOX;
size_t o = 0;
if (n == 0) return;
while (*tmpl && o + 1 < n) {
int hit = -1;
if (*tmpl == '{')
for (int k = 0; k < 6; k++)
if (!strncmp(tmpl, keys[k], strlen(keys[k]))) { hit = k; break; }
if (hit >= 0) {
for (const char *p = names[fam][hit]; *p && o + 1 < n; p++) out[o++] = *p;
tmpl += strlen(keys[hit]);
} else {
out[o++] = *tmpl++;
}
}
out[o] = 0;
}
void inputPadHelpFmt(char *out, size_t n, const char *tmpl)
{
inputPadHelpFmtFam(out, n, tmpl, inputPadFamily(optionsOverlayIsOpen() ? inputOverlayOwnerPad() : 0)); /* D472 */
}
/* D471: display name for a binding source token (kPadSrc names) on a seat's
* pad family; NULL = keep the caller's existing Xbox-style text. */
const char *inputPadSourceFamilyName(int seat, const char *src)
{
static const struct { const char *src, *ps, *nin; } t[] = {
{ "a", "Cross", NULL }, { "b", "Circle", NULL }, { "x", "Square", NULL }, { "y", "Triangle", NULL },
{ "start", "Options", "Plus" },
{ "leftshoulder", "L1", "L" }, { "rightshoulder", "R1", "R" },
{ "lefttrigger", "L2", "ZL" }, { "righttrigger", "R2", "ZR" },
{ "leftstick", "L3", NULL }, { "rightstick", "R3", NULL },
};
int fam = inputPadFamily(seat);
if (fam == PADFAM_XBOX) return NULL;
for (size_t k = 0; k < sizeof(t) / sizeof(t[0]); k++)
if (!strcmp(src, t[k].src)) return fam == PADFAM_PS ? t[k].ps : t[k].nin;
return NULL;
}
int inputPadButton(int idx, SDL_GameControllerButton b)
{
if (idx < 0 || idx >= MAX_PADS || !pads[idx]) return 0;
@@ -3120,8 +3481,205 @@ int portMouseAimPdGetTurn(f32 *tx, f32 *ty)
return 1;
}
/* ------------------------------------------------------------------------
* D469: UI-facing pad binding API (F10 overlay + front-end PC Options) and
* the GE_PADMAPTEST self-test.
* ------------------------------------------------------------------------ */
int inputPadActionCount(void) { return PA_COUNT; }
const char *inputPadActionLabel(int act) { return (act >= 0 && act < PA_COUNT) ? kPadAct[act].label : ""; }
int inputPadSourceCount(void) { return PS_COUNT; }
const char *inputPadSourceName(int src) { return (src >= 0 && src < PS_COUNT) ? kPadSrc[src].name : "none"; }
int inputPadActionForKey(const char *key)
{
char want[48];
for (int a = 0; a < PA_COUNT; a++) {
snprintf(want, sizeof(want), "Input.Pad.%s", kPadAct[a].key);
if (key && !strcmp(key, want)) return a;
}
return -1;
}
int inputPadSeatPresent(int seat) { return seat >= 0 && seat < MAX_PADS && pads[seat] != NULL; }
unsigned inputPadHeldSources(int seat)
{
return (seat >= 0 && seat < MAX_PADS) ? padReadRaw(pads[seat]) : 0;
}
int inputPadPresetStep(int cur, int dir)
{
(void)dir; /* only Jinx 1.1 (0) and Custom (3) are defined; 1/2 are reserved (D394 does not define 1.2/1.3) */
return cur == 0 ? 3 : 0;
}
/* "a,x" -> src[0]=A, src[1]=X; empty/none/unknown -> -1 (positional). */
static void padSlotSources(const char *s, int src[INPUT_BIND_SLOTS])
{
src[0] = src[1] = -1;
for (int slot = 0; slot < INPUT_BIND_SLOTS && s; slot++) {
const char *e = strchr(s, ',');
size_t n = e ? (size_t)(e - s) : strlen(s);
for (int i = 0; i < PS_COUNT; i++)
if (strlen(kPadSrc[i].name) == n && !SDL_strncasecmp(s, kPadSrc[i].name, n)) { src[slot] = i; break; }
s = e ? e + 1 : NULL;
}
}
void inputPadBindingText(int seat, int act, int slot, char *out, int n)
{
int src[INPUT_BIND_SLOTS];
if (seat < 0 || seat >= MAX_PADS || act < 0 || act >= PA_COUNT || slot < 0 || slot >= INPUT_BIND_SLOTS) {
snprintf(out, n, "none");
return;
}
padSlotSources(padStr[seat][act], src);
snprintf(out, n, "%s", src[slot] >= 0 ? kPadSrc[src[slot]].name : "none");
}
/* src < 0 clears the slot. Returns 1 on success. Writes the seat's Input.Pad*
* string (the poll thread notices the signature change and republishes). */
int inputPadBindingSet(int seat, int act, int slot, int src)
{
int t[INPUT_BIND_SLOTS];
if (seat < 0 || seat >= MAX_PADS || act < 0 || act >= PA_COUNT ||
slot < 0 || slot >= INPUT_BIND_SLOTS || src >= PS_COUNT) return 0;
padSlotSources(padStr[seat][act], t);
t[slot] = src < 0 ? -1 : src;
char buf[sizeof(padStr[0][0])];
snprintf(buf, sizeof(buf), "%s", t[0] >= 0 ? kPadSrc[t[0]].name : "none");
if (t[1] >= 0) {
size_t l = strlen(buf);
snprintf(buf + l, sizeof(buf) - l, ",%s", kPadSrc[t[1]].name);
}
snprintf(padStr[seat][act], sizeof(padStr[seat][act]), "%s", buf);
return 1;
}
/* Reset one action to the Jinx 1.1 default on every seat. Returns seats changed. */
int inputPadBindingReset(int act)
{
int n = 0;
if (act < 0 || act >= PA_COUNT) return 0;
for (int s = 0; s < MAX_PADS; s++) {
if (strcmp(padStr[s][act], kPadAct[act].def)) n++;
snprintf(padStr[s][act], sizeof(padStr[s][act]), "%s", kPadAct[act].def);
}
return n;
}
/* GE_PADMAPTEST=1: boot-time proof (logs, then the game continues).
* 1. preset 0 == the pre-D469 mapping, exhaustively (every raw source
* combination x prev A/B/X/Y x facePlayable/slotNative/southpaw x 5
* incoming masks);
* 2. a per-source table for preset 0 (menu and playable);
* 3. Custom remap moves the action; seats are independent; Start guard;
* 4. published-table path (padTabGet) honours the preset/strings.
* Config values are restored afterwards so nothing is persisted. */
static void padMapSelfTest(void)
{
int fail = 0;
PadBinds p0;
int bad = 0;
padBuildBinds(&p0, 0, 0, &bad);
static const unsigned kIn[5] = { 0, GE_CONT_G, GE_CONT_R, GE_CONT_G | GE_CONT_R, 0xffffu };
long compared = 0, mism = 0;
for (u32 raw = 0; raw < (1u << PS_COUNT); raw++)
for (u32 pv = 0; pv < 16; pv++) {
u32 prevRaw = (pv & 1 ? PSM(PS_A) : 0) | (pv & 2 ? PSM(PS_B) : 0) |
(pv & 4 ? PSM(PS_X) : 0) | (pv & 8 ? PSM(PS_Y) : 0);
for (int cx = 0; cx < 8; cx++) {
PadMapCtx c = { cx & 1, (cx >> 1) & 1, (cx >> 2) & 1 };
for (int k = 0; k < 5; k++) {
PadMapOut a, b;
padMapPure(&p0, raw, padActHeld(&p0, prevRaw), kIn[k], &c, &a);
padMapLegacy(raw, prevRaw & PSM(PS_A), prevRaw & PSM(PS_B), prevRaw & PSM(PS_X),
prevRaw & PSM(PS_Y), kIn[k], &c, &b);
compared++;
if (a.button != b.button || a.crouch != b.crouch || a.gadgetEdge != b.gadgetEdge) {
if (mism++ < 5)
sysLogPrintf(LOG_ERROR, "GE_PADMAPTEST MISMATCH raw=%04x prev=%x ctx=%d in=%04x new=%04x/%d/%d old=%04x/%d/%d",
raw, pv, cx, kIn[k], a.button, a.crouch, a.gadgetEdge,
b.button, b.crouch, b.gadgetEdge);
}
}
}
}
sysLogPrintf(mism ? LOG_ERROR : LOG_INFO, "GE_PADMAPTEST: preset0 == legacy: %ld cases compared, %ld mismatches", compared, mism);
fail += mism != 0;
for (int s = 0; s < PS_COUNT; s++)
for (int play = 0; play < 2; play++) {
PadMapCtx c = { play, 0, 0 };
PadMapOut a;
padMapPure(&p0, PSM(s), 0, 0, &c, &a);
sysLogPrintf(LOG_INFO, "GE_PADMAPTEST: preset0 %-13s %-8s -> mask=%04x crouch=%d gadget=%d",
kPadSrc[s].name, play ? "playable" : "menu", a.button, a.crouch, a.gadgetEdge);
}
/* Custom: seat 0 Fire := a, seat 1 untouched, seat 2 Use := y. */
int savedPreset = padPreset;
char saved[MAX_PADS][PA_COUNT][32];
memcpy(saved, padStr, sizeof(saved));
padPreset = 3;
snprintf(padStr[0][PA_FIRE], sizeof(padStr[0][0]), "a");
snprintf(padStr[2][PA_USE], sizeof(padStr[0][0]), "y,none");
snprintf(padStr[2][PA_NEXTWPN], sizeof(padStr[0][0]), "none");
snprintf(padStr[3][PA_START], sizeof(padStr[0][0]), "none");
const PadTable *t = padTabGet();
PadMapCtx play = { 1, 0, 0 };
PadMapOut o;
padMapPure(&t->seat[0], PSM(PS_A), 0, 0, &play, &o);
int c1 = (o.button & GE_CONT_G) != 0;
padMapPure(&t->seat[0], PSM(PS_RT), 0, 0, &play, &o);
int c2 = (o.button & GE_CONT_G) == 0;
padMapPure(&t->seat[1], PSM(PS_RT), 0, 0, &play, &o);
int c3 = (o.button & GE_CONT_G) != 0; /* seat 1 still default */
padMapPure(&t->seat[1], PSM(PS_A), 0, 0, &play, &o);
int c4 = (o.button & GE_CONT_G) == 0;
PadMapCtx nat = { 1, 1, 0 };
padMapPure(&t->seat[2], PSM(PS_Y), 0, 0, &nat, &o);
int c5 = (o.button & GE_CONT_B) != 0 && !(o.button & GE_CONT_A); /* Y is Use on seat 2 */
padMapPure(&t->seat[1], PSM(PS_Y), 0, 0, &nat, &o);
int c6 = (o.button & GE_CONT_A) != 0; /* seat 1: Y still next weapon */
padMapPure(&t->seat[3], PSM(PS_START), 0, 0, &play, &o);
int c7 = (o.button & GE_CONT_START) != 0; /* Start guard re-forces start */
sysLogPrintf(LOG_INFO, "GE_PADMAPTEST: custom: seat0 Fire=a moves(%d) RT-off(%d); seat1 independent RT(%d) A-off(%d); seat2 Use=y(%d); seat1 Y=next(%d); start guard(%d)",
c1, c2, c3, c4, c5, c6, c7);
fail += !(c1 && c2 && c3 && c4 && c5 && c6 && c7);
/* Preset != Custom ignores the strings. */
padPreset = 0;
t = padTabGet();
padMapPure(&t->seat[0], PSM(PS_RT), 0, 0, &play, &o);
int c8 = (o.button & GE_CONT_G) != 0;
padMapPure(&t->seat[0], PSM(PS_A), 0, 0, &play, &o);
int c9 = (o.button & GE_CONT_G) == 0;
sysLogPrintf(LOG_INFO, "GE_PADMAPTEST: preset0 ignores Input.Pad.* (RT fire %d, A not fire %d)", c8, c9);
fail += !(c8 && c9);
/* binding API round trip */
inputPadBindingSet(1, PA_FIRE, 1, PS_RB);
char tx[32], ty[32];
inputPadBindingText(1, PA_FIRE, 0, tx, sizeof(tx));
inputPadBindingText(1, PA_FIRE, 1, ty, sizeof(ty));
int c10 = !strcmp(tx, "righttrigger") && !strcmp(ty, "rightshoulder") && !strcmp(padStr[1][PA_FIRE], "righttrigger,rightshoulder");
inputPadBindingSet(1, PA_FIRE, 0, -1);
inputPadBindingText(1, PA_FIRE, 0, tx, sizeof(tx));
int c11 = !strcmp(tx, "none") && !strcmp(padStr[1][PA_FIRE], "none,rightshoulder");
sysLogPrintf(LOG_INFO, "GE_PADMAPTEST: binding API round trip (%d) clear keeps slot (%d) [%s]", c10, c11, padStr[1][PA_FIRE]);
fail += !(c10 && c11);
memcpy(padStr, saved, sizeof(saved));
padPreset = savedPreset;
padTabValid = 0;
sysLogPrintf(fail ? LOG_ERROR : LOG_INFO, "GE_PADMAPTEST: %s", fail ? "FAIL" : "PASS");
}
PD_CONSTRUCTOR static void inputConfigInit(void)
{
configRegisterInt("Input.PadPreset", &padPreset, 0, 3); /* D469: 0 Jinx 1.1, 1/2 reserved (= 1.1), 3 Custom */
for (int seat = 0; seat < MAX_PADS; seat++)
for (int a = 0; a < PA_COUNT; a++) {
if (seat == 0) snprintf(padStrKey[seat][a], sizeof(padStrKey[seat][a]), "Input.Pad.%s", kPadAct[a].key);
else snprintf(padStrKey[seat][a], sizeof(padStrKey[seat][a]), "Input.Pad%d.%s", seat + 1, kPadAct[a].key);
snprintf(padStr[seat][a], sizeof(padStr[seat][a]), "%s", kPadAct[a].def);
configRegisterString(padStrKey[seat][a], padStr[seat][a], sizeof(padStr[seat][a]));
}
configRegisterInt("Input.MPMode", &mpInputMode, 0, 2); /* D416: 0 Auto, 1 PadsOnly, 2 KbmP1 */
configRegisterInt("Input.MouseEnabled", &mouseEnabled, 0, 1);
configRegisterInt("Input.MouseAimSpeed", &mouseAimSpeed, 1, 500);
+320 -27
View File
@@ -50,7 +50,9 @@ extern MENU current_menu;
#include "front.h" /* selected_folder_num (D356 stage probe) */
#include "file.h" /* save_data (D356 reset probe: second-file isolation) */
#include "optionsoverlay.h"
#include "hudaspect.h" /* D335b/D472: PORT_HUD_ASPECT */
#include "watchsettings.h"
#include "audio.h"
#include "../fast3d/gfx_api.h"
/* file2.c; same extern as watchsettings.c (not in a header). */
@@ -80,7 +82,10 @@ extern s16 viGetY(void);
/* ------------------------------------------------------------------------ */
enum { ROW_TOGGLE, ROW_SLIDER, ROW_ENUM, ROW_MSAA, ROW_RES, ROW_ACTION, ROW_FPSCAP,
ROW_HEADER, ROW_BOND_FILE, ROW_BIND /* key / mouse-button capture */ };
ROW_HEADER, ROW_BOND_FILE, ROW_BIND /* key / mouse-button capture */,
ROW_PADBIND /* D469: gamepad button capture (per seat) */,
ROW_PADSEAT /* D469: which controller seat the pad rows edit */,
ROW_AUDIODEV /* D470: audio output device (string, cycles Default + enumerated) */ };
/* D346: wording pass -- Nightdive/Turok + PD-port conventions: title-case
* On/Off, no all-caps value strings. Display-only; config stores 0/1 either way. */
@@ -89,6 +94,8 @@ static const char *const kReverse[] = { "Reverse", "Upright", NULL };
static const char *const kHold[] = { "Hold", "Toggle", NULL };
static const char *const kAspectMode[] = { "Window", "Original", NULL }; /* D447: Original = 4:3 (16:9 with the game Ratio) bars */
static const char *const kTexFilter[] = { "Nearest", "Bilinear", "3-Point", NULL };
static const char *const kPadPreset[] = { "Jinx 1.1", "Jinx 1.2 (n/a)", "Jinx 1.3 (n/a)", "Custom", NULL }; /* D469: 1.2/1.3 undefined by D394 -> behave as 1.1 */
static int s_padSeat = 0; /* D469: seat edited by the Controller page rows */
static const char *const kAimMode[] = { "N64", "Centred (PC)", NULL }; /* D337 */
static const char *const kAimRange[] = { "PC", "N64", NULL }; /* D338 */
static const int kMsaaSeq[] = { 1, 2, 4, 8, 16 }; /* D443: 16x added */
@@ -256,6 +263,7 @@ static struct Row rows[] = {
{ .key="Input.CrouchMode", .label="Crouch mode", .kind=ROW_ENUM, .step=1, .names=kHold },
{ .key="__ResetInput", .label="Reset to defaults", .kind=ROW_ACTION },
{ .key="__OpenBindings", .label="Bindings...", .kind=ROW_ACTION },
{ .key="__OpenController", .label="Controller...", .kind=ROW_ACTION },
/* D388: nested Input -> Bindings -> two short pages, each with a reset.
* Mouse buttons 1-5 share the visible slots with keys; pads stay fixed. */
@@ -283,6 +291,22 @@ static struct Row rows[] = {
{ .key="Input.Bind.Crouch", .label="Crouch", .kind=ROW_BIND },
{ .key="__ResetActionKeys", .label="Reset to defaults", .kind=ROW_ACTION },
/* D469: nested Input -> Controller. Preset selector; Custom unlocks per-seat,
* per-action pad rebinding (two slots per action, pad-driven capture). */
{ .key="__HdrController", .label="CONTROLLER", .kind=ROW_HEADER },
{ .key="Input.PadPreset", .label="Layout preset", .kind=ROW_ENUM, .step=1, .names=kPadPreset },
{ .key="__PadSeat", .label="Controller", .kind=ROW_PADSEAT, .shownWhen="Input.PadPreset", .shownValue=3 },
{ .key="Input.Pad.Fire", .label="Fire", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3 },
{ .key="Input.Pad.Aim", .label="Aim", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3 },
{ .key="Input.Pad.Use", .label="Use / action", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3 },
{ .key="Input.Pad.Reload", .label="Reload", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3 },
{ .key="Input.Pad.Crouch", .label="Crouch", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3 },
{ .key="Input.Pad.NextWeapon", .label="Next weapon", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3 },
{ .key="Input.Pad.PrevWeapon", .label="Previous weapon", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3 },
{ .key="Input.Pad.Gadget", .label="Cycle gadget", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3 },
{ .key="Input.Pad.Start", .label="Pause / start", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3 },
{ .key="__ResetController", .label="Reset to defaults", .kind=ROW_ACTION },
{ .key="__HdrGameplay", .label="GAMEPLAY", .kind=ROW_HEADER },
/* D356 exposure filter (menu surface only, the D181/D216/D304 pattern --
* config keys, the N64 watch paths and ini hand-edits all stay live):
@@ -393,6 +417,10 @@ static struct Row rows[] = {
{ .key="__ResetGraphics", .label="Reset to defaults", .kind=ROW_ACTION },
{ .key="__HdrAudio", .label="AUDIO", .kind=ROW_HEADER },
/* D470: port-level master volume (final gain on the mixed output, whole %)
* and the output device (live switch; Default = system default). */
{ .key="Audio.MasterVolume", .label="Master volume", .kind=ROW_SLIDER, .step=5, .unit="%" },
{ .key="Audio.Device", .label="Output device", .kind=ROW_AUDIODEV },
/* D356: the per-file volume sliders get a real AUDIO section (the D353
* "AUDIO (BOND FILE)" header was retired with the BOND FILE section).
* A port-level master volume (M3) will join here as Turok's
@@ -434,7 +462,8 @@ int optionsRowHeaderParent(int i)
{
if (i < 0 || i >= NUM_ROWS || rows[i].kind != ROW_HEADER) return -1;
const char *key = rows[i].key;
if (!strcmp(key, "__HdrBindings")) return rowIndexByKey("__HdrInput");
if (!strcmp(key, "__HdrBindings") || !strcmp(key, "__HdrController"))
return rowIndexByKey("__HdrInput");
if (!strcmp(key, "__HdrMoveKeys") || !strcmp(key, "__HdrActionKeys"))
return rowIndexByKey("__HdrBindings");
if (!strcmp(key, "__HdrHUD")) return rowIndexByKey("__HdrGameplay");
@@ -446,6 +475,7 @@ int optionsRowChildHeader(int i)
if (i < 0 || i >= NUM_ROWS) return -1;
const char *key = rows[i].key;
if (!strcmp(key, "__OpenBindings")) return rowIndexByKey("__HdrBindings");
if (!strcmp(key, "__OpenController")) return rowIndexByKey("__HdrController");
if (!strcmp(key, "__OpenMoveKeys")) return rowIndexByKey("__HdrMoveKeys");
if (!strcmp(key, "__OpenActionKeys")) return rowIndexByKey("__HdrActionKeys");
if (!strcmp(key, "__OpenHud")) return rowIndexByKey("__HdrHUD");
@@ -475,8 +505,37 @@ static SDL_SpinLock s_bindCaptureLock;
static int s_bindPending = -1, s_bindCaptureRow = -1, s_bindError = 0;
static SDL_atomic_t s_bindHold; /* captured key/mouse button, shared with pad poll */
/* D469: gamepad capture (Controller page). Same modal flag as the key/mouse
* capture (so F10/Back/Start cannot close the page mid-capture), but driven
* by the edited seat's pad. Arming: nothing is accepted until every source
* is released (the A press that opened the modal must not bind itself).
* Cancel: Esc, tap B, or tap Back (either pad 0 or the edited pad), so a
* controller-only user is never trapped (D395). Clear: Delete, or HOLD Back.
* B itself is bindable by HOLDING it (a tap cancels). */
static int s_padCapMode = 0, s_padCapRow = -1, s_padCapSeat = 0, s_padCapArmed = 0, s_padCapSwallow = 0;
static uint64_t s_padCapBackT = 0, s_padCapBT = 0;
#define PADCAP_HOLD_US 600000
static void bindingBegin(struct Row *r)
{
if (r && r->found && r->kind == ROW_PADBIND) {
SDL_AtomicLock(&s_bindCaptureLock);
s_bindPending = -1;
SDL_AtomicUnlock(&s_bindCaptureLock);
s_bindError = 0;
s_padCapMode = 1;
s_padCapRow = (int)(r - rows);
s_padCapSeat = s_padSeat;
s_padCapArmed = 0;
s_padCapSwallow = 0;
s_padCapBackT = s_padCapBT = 0;
s_bindCaptureRow = s_padCapRow;
SDL_AtomicSet(&s_bindPadCancelHold, 0);
SDL_AtomicSet(&s_bindHold, SDL_SCANCODE_UNKNOWN);
SDL_AtomicSet(&s_bindCaptureActive, 1);
return;
}
s_padCapMode = 0;
if (!r || !r->found || r->kind != ROW_BIND) return;
SDL_AtomicLock(&s_bindCaptureLock);
s_bindPending = -1;
@@ -511,6 +570,14 @@ int optionsBindingKeyDown(const SDL_KeyboardEvent *ev)
if ((ev->keysym.sym == SDLK_F4 && (ev->keysym.mod & KMOD_ALT))) return 0;
if (ev->repeat) return 1;
SDL_Scancode sc = ev->keysym.scancode;
if (s_padCapMode) { /* D469: pad capture takes only Esc (cancel) / Delete (clear) from the keyboard */
if (sc == SDL_SCANCODE_ESCAPE || sc == SDL_SCANCODE_DELETE) {
SDL_AtomicLock(&s_bindCaptureLock);
if (s_bindPending < 0) s_bindPending = (int)sc;
SDL_AtomicUnlock(&s_bindCaptureLock);
}
return 1;
}
if (sc == SDL_SCANCODE_F10 || sc == SDL_SCANCODE_F12 ||
sc == SDL_SCANCODE_UNKNOWN) return 1;
SDL_AtomicLock(&s_bindCaptureLock);
@@ -523,6 +590,7 @@ int optionsBindingKeyDown(const SDL_KeyboardEvent *ev)
int optionsBindingMouseDown(const SDL_MouseButtonEvent *ev)
{
if (!optionsBindingCaptureActive()) return 0;
if (s_padCapMode) return 1; /* D469: swallow clicks during a pad capture */
if (ev->button >= 1 && ev->button <= 5) {
SDL_AtomicLock(&s_bindCaptureLock);
if (s_bindPending < 0) s_bindPending = INPUT_BIND_MOUSE(ev->button);
@@ -534,18 +602,102 @@ int optionsBindingMouseDown(const SDL_MouseButtonEvent *ev)
/* The capturing screen calls this once per input tick. Return 1 while the
* modal is active and through the captured key's release, so it cannot
* simultaneously trigger menu navigation or a second action. */
static void padCapFinish(void)
{
SDL_AtomicSet(&s_bindCaptureActive, 0);
s_padCapSwallow = 1; /* swallow until every button is released again */
}
static int padCaptureTick(void)
{
int seat = s_padCapSeat;
unsigned raw = inputPadHeldSources(seat);
int back = inputPadButton(seat, SDL_CONTROLLER_BUTTON_BACK) || inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_BACK);
int bHeld = inputPadButton(seat, SDL_CONTROLLER_BUTTON_B) || inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_B);
int act = inputPadActionForKey(rows[s_padCapRow].key);
uint64_t now = sysGetMicroseconds();
SDL_AtomicLock(&s_bindCaptureLock);
int key = s_bindPending;
s_bindPending = -1;
SDL_AtomicUnlock(&s_bindCaptureLock);
if (key == SDL_SCANCODE_ESCAPE) { padCapFinish(); return 1; }
if (key == SDL_SCANCODE_DELETE) {
inputPadBindingSet(seat, act, rows[s_padCapRow].bindSlot, -1);
configSave();
padCapFinish();
return 1;
}
if (!s_padCapArmed) { /* release-before-capture arming */
if (!raw && !back && !bHeld) s_padCapArmed = 1;
return 1;
}
/* Back: tap = cancel, hold = clear */
if (back) {
if (!s_padCapBackT) s_padCapBackT = now;
if (now - s_padCapBackT >= PADCAP_HOLD_US) {
inputPadBindingSet(seat, act, rows[s_padCapRow].bindSlot, -1);
configSave();
sysLogPrintf(LOG_INFO, "pad bind: %s seat %d slot %d cleared", rows[s_padCapRow].key, seat + 1, rows[s_padCapRow].bindSlot + 1);
padCapFinish();
}
return 1;
} else if (s_padCapBackT) {
padCapFinish(); /* released early: cancel */
return 1;
}
/* B: tap = cancel, hold = bind B (to the edited pad only) */
if (bHeld) {
if (!s_padCapBT) s_padCapBT = now;
if (now - s_padCapBT >= PADCAP_HOLD_US && (raw & 2u)) {
inputPadBindingSet(seat, act, rows[s_padCapRow].bindSlot, 1 /* b */);
configSave();
sysLogPrintf(LOG_INFO, "pad bind: %s seat %d slot %d = b", rows[s_padCapRow].key, seat + 1, rows[s_padCapRow].bindSlot + 1);
padCapFinish();
}
return 1;
} else if (s_padCapBT) {
padCapFinish(); /* tap: cancel */
return 1;
}
for (int src = 0; src < inputPadSourceCount(); src++) {
if (raw & (1u << src)) {
inputPadBindingSet(seat, act, rows[s_padCapRow].bindSlot, src);
configSave();
sysLogPrintf(LOG_INFO, "pad bind: %s seat %d slot %d = %s", rows[s_padCapRow].key, seat + 1,
rows[s_padCapRow].bindSlot + 1, inputPadSourceName(src));
padCapFinish();
return 1;
}
}
return 1;
}
int optionsBindingCaptureTick(void)
{
if (s_padCapMode) { /* D469 */
if (optionsBindingCaptureActive()) return padCaptureTick();
if (s_padCapSwallow) {
unsigned raw = inputPadHeldSources(s_padCapSeat);
if (raw || inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_B) || inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_A) ||
inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_X) || inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_BACK) ||
inputPadButton(s_padCapSeat, SDL_CONTROLLER_BUTTON_BACK))
return 1;
s_padCapSwallow = 0;
}
s_padCapMode = 0;
return 0;
}
/* D395: capture only accepts keyboard/mouse. A controller B must be
* able to cancel a modal entered with a keyboard/mouse; swallow B until
* release so that the same press cannot also back out of the page. */
if (SDL_AtomicGet(&s_bindPadCancelHold)) {
if (!inputPadButton(0, SDL_CONTROLLER_BUTTON_B))
if (!inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_B))
SDL_AtomicSet(&s_bindPadCancelHold, 0);
return 1;
}
if (optionsBindingCaptureActive()) {
if (inputPadButton(0, SDL_CONTROLLER_BUTTON_B)) {
if (inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_B)) {
SDL_AtomicLock(&s_bindCaptureLock);
s_bindPending = -1;
SDL_AtomicUnlock(&s_bindCaptureLock);
@@ -868,6 +1020,7 @@ static void overlayInit(void)
for (int i = 0; i < NUM_ROWS; i++) {
if (rows[i].kind == ROW_RES || rows[i].kind == ROW_ACTION ||
rows[i].kind == ROW_HEADER || rows[i].kind == ROW_BOND_FILE ||
rows[i].kind == ROW_PADSEAT || rows[i].kind == ROW_PADBIND || rows[i].kind == ROW_AUDIODEV ||
watchSettingsFieldForKey(rows[i].key) >= 0) {
rows[i].found = 1; /* not config-backed */
continue;
@@ -926,6 +1079,7 @@ static void overlayInit(void)
{ "__HdrActionKeys", "Input.Bind.Fire", "__ResetActionKeys", 14 },
{ "__HdrGameplay", "__OpenHud", "__ResetGameplay", 14 },
{ "__HdrHUD", "Video.CrosshairColor", "__ResetHUD", 14 },
{ "__HdrController", "Input.PadPreset", "__ResetController", 14 },
{ "__HdrVideo", "Video.FpsCap", "__ResetVideo", 14 },
};
for (size_t k = 0; k < sizeof(pages) / sizeof(pages[0]); k++) {
@@ -1232,6 +1386,10 @@ static void rowAdjust(struct Row *r, int dir)
}
case ROW_ENUM: {
double lo = r->cfgMin, hi = r->cfgMax;
if (!strcmp(r->key, "Input.PadPreset")) { /* D469: only Jinx 1.1 and Custom are defined */
rowSet(r, (double)inputPadPresetStep((int)lround(v), dir));
break;
}
v += dir;
if (v < lo) v = hi;
if (v > hi) v = lo;
@@ -1259,8 +1417,23 @@ static void rowAdjust(struct Row *r, int dir)
break;
}
case ROW_BIND:
case ROW_PADBIND:
r->bindSlot = (r->bindSlot + dir + INPUT_BIND_SLOTS) % INPUT_BIND_SLOTS;
break;
case ROW_PADSEAT:
s_padSeat = (s_padSeat + (dir >= 0 ? 1 : 3)) % 4;
break;
case ROW_AUDIODEV: {
/* D470: cycle Default + enumerated outputs; applied live on the audio thread. */
int n = audioDeviceRefresh();
int idx = 0; /* 0 = Default */
for (int k = 0; k < n; k++)
if (!strcmp(audioDeviceName(k), audioDeviceCurrentName())) idx = k + 1;
idx = (idx + (dir >= 0 ? 1 : n) ) % (n + 1);
audioDeviceRequest(idx == 0 ? "" : audioDeviceName(idx - 1));
sysLogPrintf(LOG_INFO, "optionsoverlay: audio device -> \"%s\"", idx == 0 ? "Default" : audioDeviceName(idx - 1));
break;
}
case ROW_ACTION:
if (optionsRowChildHeader((int)(r - rows)) >= 0) break; /* link, not a Quit or value */
/* D356: reset rows arm/confirm through the shared activation
@@ -1363,6 +1536,7 @@ static const struct { const char *key; double def; } kResetDefaults[] = {
{ "Input.PadTriggerPct", 23 }, /* = 23 */
{ "Input.RumbleScale", 0.5 }, /* = gRumbleScale (0.5f), D401 */
{ "Input.CrouchMode", 0 }, /* = 0 (hold) */
{ "Input.PadPreset", 0 }, /* = 0 (Jinx 1.1), D469 */
/* GRAPHICS (port/src/video.c initializers) */
{ "Video.MSAA", 2 }, /* = 2 */
{ "Video.TextureFilter", 1 }, /* = 1 (bilinear) */
@@ -1391,6 +1565,8 @@ static const struct { const char *key; double def; } kResetDefaults[] = {
{ "Video.VSync", 1 }, /* = 1 (on) */
{ "Video.FpsCap", 60 }, /* = 60 */
{ "Video.DisplayFPS", 0 }, /* registered 0 */
/* AUDIO (port/src/audio.c, D470) */
{ "Audio.MasterVolume", 100 }, /* = 100 (bit-identical passthrough) */
};
static double kResetDefault(const char *key)
@@ -1430,6 +1606,13 @@ static void rowResetSection(int iReset)
r->bindSlot = 0;
continue;
}
if (r->kind == ROW_AUDIODEV) { audioDeviceRequest(""); continue; } /* D470 */
if (r->kind == ROW_PADBIND) { /* D469: reset the action on every seat */
inputPadBindingReset(inputPadActionForKey(r->key));
r->bindSlot = 0;
continue;
}
if (r->kind == ROW_PADSEAT) continue;
int field = watchSettingsFieldForKey(r->key);
if (field >= 0) {
watchSettingsSet((enum WatchSettingField)field,
@@ -1698,27 +1881,27 @@ void optionsOverlayHandleInput(void)
* on an edge, not slider adjustment. Only D-pad/left-stick X (or keyboard
* left/right) adjust sliders, with hold-repeat. The pad is swallowed by
* input.c while the overlay owns it. Start/Select still close it. */
int gUp = inputPadButton(0, SDL_CONTROLLER_BUTTON_DPAD_UP)
|| inputPadAxis(0, SDL_CONTROLLER_AXIS_LEFTY) < -12000;
int gDn = inputPadButton(0, SDL_CONTROLLER_BUTTON_DPAD_DOWN)
|| inputPadAxis(0, SDL_CONTROLLER_AXIS_LEFTY) > 12000;
int gLf = inputPadButton(0, SDL_CONTROLLER_BUTTON_DPAD_LEFT)
|| inputPadAxis(0, SDL_CONTROLLER_AXIS_LEFTX) < -12000;
int gRt = inputPadButton(0, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)
|| inputPadAxis(0, SDL_CONTROLLER_AXIS_LEFTX) > 12000;
int gUp = inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_DPAD_UP)
|| inputPadAxis(inputOverlayOwnerPad(), SDL_CONTROLLER_AXIS_LEFTY) < -12000;
int gDn = inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_DPAD_DOWN)
|| inputPadAxis(inputOverlayOwnerPad(), SDL_CONTROLLER_AXIS_LEFTY) > 12000;
int gLf = inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_DPAD_LEFT)
|| inputPadAxis(inputOverlayOwnerPad(), SDL_CONTROLLER_AXIS_LEFTX) < -12000;
int gRt = inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_DPAD_RIGHT)
|| inputPadAxis(inputOverlayOwnerPad(), SDL_CONTROLLER_AXIS_LEFTX) > 12000;
int up = ks[SDL_SCANCODE_UP] || ks[SDL_SCANCODE_KP_8] || gUp;
int dn = ks[SDL_SCANCODE_DOWN] || ks[SDL_SCANCODE_KP_2] || gDn;
int lf = ks[SDL_SCANCODE_LEFT] || ks[SDL_SCANCODE_KP_4] || gLf;
int rightNav = ks[SDL_SCANCODE_RIGHT] || ks[SDL_SCANCODE_KP_6] || gRt;
int accept = ks[SDL_SCANCODE_RETURN] || ks[SDL_SCANCODE_KP_ENTER] ||
inputPadButton(0, SDL_CONTROLLER_BUTTON_A) ||
inputPadButton(0, SDL_CONTROLLER_BUTTON_X);
inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_A) ||
inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_X);
int rt = rightNav || accept;
int padBack = inputPadButton(0, SDL_CONTROLLER_BUTTON_B);
int padBack = inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_B);
static int prevStart = 0; /* not reset while closed: a Start held across
close must not re-close on the next open */
int startNow = inputPadButton(0, SDL_CONTROLLER_BUTTON_START);
int startNow = inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_START);
if (startNow && !prevStart) {
prevStart = startNow;
optionsOverlayToggle(); /* Start closes */
@@ -1735,6 +1918,20 @@ void optionsOverlayHandleInput(void)
}
prevPadBack = padBack;
/* D469: Y or Delete clears the selected pad-binding slot (one press, no modal). */
{
static int prevYClr = 0, prevDelClr = 0;
int yNow = inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_Y), delNow = ks[SDL_SCANCODE_DELETE];
if (s_section >= 0 && s_sel > 0 && ((yNow && !prevYClr) || (delNow && !prevDelClr))) {
struct Row *cr = &rows[s_visIdx[s_sel]];
if (cr->kind == ROW_PADBIND) {
inputPadBindingSet(s_padSeat, inputPadActionForKey(cr->key), cr->bindSlot, -1);
configSave();
}
}
prevYClr = yNow; prevDelClr = delNow;
}
/* ---- keyboard / D-pad nav (clamped at the ends; scroll follows).
* D347: hold-to-repeat so a controller (and held keys) can traverse the
* list without one-press-per-row. */
@@ -1774,7 +1971,10 @@ void optionsOverlayHandleInput(void)
} else {
struct Row *r = &rows[s_visIdx[s_sel]];
int keyConfirm = ks[SDL_SCANCODE_RETURN] || ks[SDL_SCANCODE_KP_ENTER];
if (r->kind == ROW_BIND && dir > 0 && keyConfirm) {
if (r->kind == ROW_PADBIND && dir > 0 && accept && !rightNav) {
bindingBegin(r); /* D469: A/X/Enter starts pad capture; D-pad right changes the slot */
adjDir = 0;
} else if (r->kind == ROW_BIND && dir > 0 && keyConfirm) {
bindingBegin(r);
adjDir = 0;
} else if (r->kind == ROW_BIND && dir > 0 && accept && !rightNav) {
@@ -1816,6 +2016,14 @@ void optionsOverlayHandleInput(void)
gfx_get_ui_screen_rect(&rx, &ry, &rw, &rh);
if (rw > 0 && rh > 0) {
double ox = (double)(mx - rx) * (double)viGetX() / rw;
{ /* D335b/D472: the card is drawn into a centred 4:3 region under native widescreen */
f32 na = portNativeAspect();
if (na > 1.3334f) {
double vis = (4.0 / 3.0) / (double)na;
double fx = (double)(mx - rx) / rw;
ox = ((fx - (1.0 - vis) * 0.5) / vis) * (double)viGetX();
}
}
double oy = (double)(my - ry) * (double)viGetY() / rh;
int hoverVis = overlayRowAtY(ox, oy);
/* Opt-in diagnostic for D391: distinguish hit-band/scroll errors
@@ -1870,7 +2078,7 @@ void optionsOverlayHandleInput(void)
s_dragRow = i;
int field = watchSettingsFieldForKey(r->key);
SDL_AtomicSet(&s_dragWatchField, field + 1);
} else if (r->kind == ROW_BIND) {
} else if (r->kind == ROW_BIND || r->kind == ROW_PADBIND) {
bindingBegin(r); /* mouse selects slot via arrows; click captures */
} else {
rowAdjust(r, +1); /* toggle / cycle forward (wraps) */
@@ -1969,6 +2177,40 @@ static void valueText(int i, char *out, int n)
}
return;
}
if (r->kind == ROW_AUDIODEV) {
const char *cur = audioDeviceCurrentName();
if (!cur[0]) snprintf(out, n, "Default");
else if (strlen(cur) > 24) snprintf(out, n, "%.22s..", cur);
else snprintf(out, n, "%s", cur);
return;
}
if (r->kind == ROW_PADSEAT) {
snprintf(out, n, "Pad %d%s", s_padSeat + 1, inputPadSeatPresent(s_padSeat) ? "" : " (none)");
return;
}
if (r->kind == ROW_PADBIND) {
if (optionsBindingCaptureActive() && i == s_bindCaptureRow) {
snprintf(out, n, "Press button...");
} else {
static const char *const kSlotName[INPUT_BIND_SLOTS] = { "Primary", "Secondary" };
char nm[32];
inputPadBindingText(s_padSeat, inputPadActionForKey(r->key), r->bindSlot, nm, sizeof(nm));
static const struct { const char *a, *b; } pretty[] = {
{ "leftshoulder", "LB" }, { "rightshoulder", "RB" }, { "lefttrigger", "LT" },
{ "righttrigger", "RT" }, { "leftstick", "L-Click" }, { "rightstick", "R-Click" },
{ "dpup", "D-Up" }, { "dpdown", "D-Down" }, { "dpleft", "D-Left" }, { "dpright", "D-Right" },
};
const char *show = nm;
for (size_t k = 0; k < sizeof(pretty) / sizeof(pretty[0]); k++)
if (!strcmp(nm, pretty[k].a)) show = pretty[k].b;
{ /* D471: PlayStation / Nintendo names for the pad being edited */
const char *fam = inputPadSourceFamilyName(s_padSeat, nm);
if (fam) show = fam;
}
snprintf(out, n, "%s: %s", kSlotName[r->bindSlot], show);
}
return;
}
if (r->kind == ROW_BIND) {
if (optionsBindingCaptureActive() && i == s_bindCaptureRow)
snprintf(out, n, "Key/mouse...");
@@ -2221,6 +2463,10 @@ Gfx *optionsOverlayEmit(void)
* follow GE's watch options (options.c's Bank Gothic/0xA0FFA0F0).
* No dossier paper, gold, 3D watch model or new assets. */
gdl = fillRect(gdl, 0, 0, W, H, 0, 0, 0, 150);
/* D335b/D472: the dim stays full-window; the card + text are drawn under
* the centre aspect mode so native widescreen shows an undistorted,
* centred 4:3 panel (no emission at <= 4:3: byte-identical there). */
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER);
gdl = fillRect(gdl, o.left, o.top, o.right, o.bottom,
5, 17, 13, 225);
gdl = fillRect(gdl, o.left + 8, o.top + 25, o.right - 8, o.top + 26,
@@ -2284,7 +2530,9 @@ Gfx *optionsOverlayEmit(void)
for (int p = s_scroll; p <= pLast; p++) {
const struct Row *r = &rows[s_visIdx[p]];
s32 rowY = o.contentY + (p - s_scroll) * OV_LINE;
int expRow = strstr(r->label, "EXPERIMENTAL") != NULL; /* D472: red ink */
u32 ink = !r->found ? 0x498053ff :
expRow ? (p == s_sel ? 0xff8a80ff : 0xd05a50ff) :
p == s_sel ? 0xa0ffa0ff : 0x66ca77ff;
char val[48];
if (s_section < 0) {
@@ -2316,7 +2564,7 @@ Gfx *optionsOverlayEmit(void)
} else {
u32 valueInk = (strncmp(val, "ON", 2) == 0 ||
(isArmRow(r) && strcmp(val, "Confirm") == 0))
? 0xa0ffa0ff : ink;
? (expRow ? 0xff8a80ff : 0xa0ffa0ff) : ink;
gdl = drawBodyR(gdl, o.valueR, rowY, val, valueInk);
}
}
@@ -2324,19 +2572,25 @@ Gfx *optionsOverlayEmit(void)
* started after CONTROLS: and ran under / beyond the right edge. Reserve
* two lines in overlayLayout so the footer never covers the last row. */
const char *help1, *help2;
if (optionsBindingCaptureActive()) {
if (optionsBindingCaptureActive() && s_padCapMode) {
help1 = "PRESS PAD BUTTON HOLD {B}=BIND {B}";
help2 = "TAP {B}/ESC CANCEL HOLD {BACK} CLEAR";
} else if (optionsBindingCaptureActive()) {
help1 = "PRESS KEY/MOUSE 1-5";
help2 = "B/ESC CANCEL DEL CLEAR";
help2 = "{B}/ESC CANCEL DEL CLEAR";
} else if (s_section < 0) {
help1 = "A/ENTER SELECT";
help2 = "B/F10 CLOSE";
help1 = "{A}/ENTER SELECT";
help2 = "{B}/F10 CLOSE";
} else if (!strcmp(rows[s_section].key, "__HdrController")) {
help1 = "{A} BIND LEFT/RIGHT SLOT {Y} CLEAR";
help2 = "{B}/ESC BACK F10 CLOSE";
} else if (!strcmp(rows[s_section].key, "__HdrMoveKeys") ||
!strcmp(rows[s_section].key, "__HdrActionKeys")) {
help1 = "KEY/MOUSE ONLY LEFT/RIGHT SLOT";
help2 = "ENTER BIND B/ESC BACK";
help2 = "ENTER BIND {B}/ESC BACK";
} else {
help1 = "A SELECT LEFT/RIGHT CHANGE";
help2 = "B/ESC BACK F10 CLOSE";
help1 = "{A} SELECT LEFT/RIGHT CHANGE";
help2 = "{B}/ESC BACK F10 CLOSE";
}
s32 helpX = o.left + 16 + metaWidth("CONTROLS:");
s32 helpRight = o.right - 10;
@@ -2363,6 +2617,32 @@ Gfx *optionsOverlayEmit(void)
}
}
gdl = drawMeta(gdl, o.left + 11, o.footerY, "CONTROLS:", 0xa6baaaff);
{ /* D471: pad button names for the menu pad's family (Xbox = old text) */
static char h1buf[80], h2buf[80];
inputPadHelpFmt(h1buf, sizeof(h1buf), help1);
inputPadHelpFmt(h2buf, sizeof(h2buf), help2);
/* Long PlayStation names must not run past the card: fall back to
* the Xbox letters for a line that would overflow. */
{
/* Too wide: first compact "LEFT/RIGHT" to "L/R", then fall back
* to the Xbox letters, so a line never runs past the card. */
const char *tm[2] = { help1, help2 };
char *bf[2] = { h1buf, h2buf };
s32 av[2] = { helpRight - helpX, helpRight - (o.left + 16) };
for (int L = 0; L < 2; L++) {
if (bodyWidth(bf[L]) <= av[L]) continue;
char compact[80]; size_t o2 = 0;
for (const char *p = tm[L]; *p && o2 + 1 < sizeof(compact); ) {
if (!strncmp(p, "LEFT/RIGHT", 10)) { memcpy(compact + o2, "L/R", 3); o2 += 3; p += 10; }
else compact[o2++] = *p++;
}
compact[o2] = 0;
inputPadHelpFmt(bf[L], 80, compact);
if (bodyWidth(bf[L]) > av[L]) inputPadHelpFmtFam(bf[L], 80, tm[L], 0);
}
}
help1 = h1buf; help2 = h2buf;
}
gdl = drawBody(gdl, helpX, o.footerY, help1, 0x829e91ff);
gdl = drawBody(gdl, o.left + 16, o.footerY + OV_FOOTER_LINE,
help2, 0x829e91ff);
@@ -2371,6 +2651,13 @@ Gfx *optionsOverlayEmit(void)
s_scroll > (s_section >= 0 ? 1 : 0) ? "^ v" : "v",
0x80d58bff);
if (s_section < 0 && configUnknownKeyCount() > 0) { /* D472: non-blocking ini warning */
char w[64];
snprintf(w, sizeof(w), "%d UNKNOWN INI KEY%s - SEE LOG", configUnknownKeyCount(),
configUnknownKeyCount() == 1 ? "" : "S");
gdl = drawBodyR(gdl, o.right - 28, o.top + 8, w, 0xe0b050ff);
}
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE);
gDPPipeSync(gdl++);
gSPEndDisplayList(gdl++);
@@ -2429,7 +2716,13 @@ int optionsRowIsSlider(int i)
int optionsRowIsBind(int i)
{
struct Row *r = rowAt(i);
return r && r->kind == ROW_BIND;
return r && (r->kind == ROW_BIND || r->kind == ROW_PADBIND);
}
int optionsRowIsPadBind(int i)
{
struct Row *r = rowAt(i);
return r && r->kind == ROW_PADBIND;
}
void optionsRowBeginBind(int i)
+5
View File
@@ -31,6 +31,7 @@
#include "input.h"
#include "optionsoverlay.h"
#include "frontoptions.h"
#include "audio.h"
#include "../fast3d/gfx_api.h"
#include "../fast3d/gfx_sdl.h"
@@ -1061,6 +1062,10 @@ void videoPumpEvents(void)
SDL_Event ev;
while (SDL_PollEvent(&ev)) {
switch (ev.type) {
case SDL_AUDIODEVICEREMOVED:
/* D470: an open output device vanished; audio.c falls back to default. */
if (!ev.adevice.iscapture) audioNotifyDeviceRemoved(ev.adevice.which);
break;
case SDL_QUIT:
videoRequestQuit("quit event");
break;