forked from Ports/goldeneye-pc-port
The two user-facing config keys were registered but only reachable by hand-editing ge007.ini. Add ROW_TOGGLE rows (below Screen shake): - No hit flash (D232, community "no damage flash"; suppresses the non-black fade paths in bondview2) - Skip intro (D216; read at title-stage load, so it takes effect the next time the title screen is reached) Video.FixMipTextures / Video.WrapFix deliberately left out: opt-in fidelity bug-fixes, not preferences. Verified live (isolated CWD): both rows render in the F10 panel and toggle; clean exit persists Game.NoHitFlash=1 + Game.SkipIntro=1 to ini; relaunch with SkipIntro=1 logs "Game.SkipIntro: booting to file-select" and skips straight to mission select. NoHitFlash's in-game suppression rests on its prior D232 verification (needs combat, not automatable).
860 lines
31 KiB
C
860 lines
31 KiB
C
/*
|
|
* F10 in-game options overlay -- approach (C) from docs/dev/OPTIONS-MENU-PLAN.md.
|
|
*
|
|
* Port-layer only. No src/ menu code is touched: the overlay draws its own
|
|
* fast3d 2D display list (appended after the game DL in gfx_run) and edits the
|
|
* port-owned config.c variables directly. Live knobs apply immediately; the
|
|
* two that need an FBO/window rebuild (MSAA, Fullscreen) are tagged "(restart)".
|
|
*
|
|
* The panel adapts to whatever 2D space it is drawn in (320x240 in-game vs
|
|
* 440x330 on front-end screens -- viSetXY differs) and scrolls when the row
|
|
* list outgrows the viewport (wheel / arrows at the edges). Cyclic rows
|
|
* (MSAA, texture filter, resolution, toggles) wrap in both directions; the
|
|
* manual % rows (draw/LOD distance) are hidden while their "auto" toggle is on.
|
|
*
|
|
* Text + fill helpers are the game's own (textRender / microcode_constructor /
|
|
* gDPFillRectangle) reached by extern -- same pattern input.c uses to read
|
|
* current_menu / cursor_h_pos. This is a rendering/UI view, not a logic change.
|
|
*
|
|
* Diagnostic: set GE_OPTIONSOVERLAY=1 to auto-open at boot (headless layout
|
|
* check). Env-gated, harmless when unset.
|
|
*/
|
|
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <SDL.h>
|
|
|
|
#include <PR/ultratypes.h>
|
|
/* gbi.h's gDP* DL macros use _SHIFTL/_SHIFTR but do not define them -- game TUs
|
|
* get them from <ultra64.h>/<PR/mbi.h>, which also drags in N64 OS headers that
|
|
* shadow libc here. Define the two pure macros locally (verbatim from mbi.h) so
|
|
* this stays a plain port TU. Without them GCC/ld fails "undefined reference to
|
|
* _SHIFTL" (MinGW's chain happens to provide it). */
|
|
#ifndef _SHIFTL
|
|
#define _SHIFTL(v, s, w) ((u32)(((u32)(v) & ((0x01 << (w)) - 1)) << (s)))
|
|
#define _SHIFTR(v, s, w) ((u32)(((u32)(v) >> (s)) & ((0x01 << (w)) - 1)))
|
|
#endif
|
|
#include <PR/gbi.h>
|
|
|
|
#include "platform.h"
|
|
#include "system.h"
|
|
#include "config.h"
|
|
#include "video.h"
|
|
#include "input.h"
|
|
#include "optionsoverlay.h"
|
|
|
|
/* ---- game symbols (rendering/UI only; see input.c for the same pattern) ---- */
|
|
struct font;
|
|
struct fontchar;
|
|
extern struct font *ptrFontBankGothic;
|
|
extern struct fontchar *ptrFontBankGothicChars;
|
|
extern Gfx *microcode_constructor(Gfx *gdl);
|
|
extern Gfx *textRender(Gfx *gdl, s32 *x, s32 *y, char *text, struct fontchar *chars,
|
|
struct font *font, u32 colour, s32 width, s32 height,
|
|
u32 yOffset, s32 lineheight);
|
|
extern void textMeasure(s32 *textheight, s32 *textwidth, char *text,
|
|
struct fontchar *chars, struct font *font, s32 lineheight);
|
|
extern s16 viGetX(void);
|
|
extern s16 viGetY(void);
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
enum { ROW_TOGGLE, ROW_SLIDER, ROW_ENUM, ROW_MSAA, ROW_RES };
|
|
|
|
static const char *const kOnOff[] = { "OFF", "ON", NULL };
|
|
static const char *const kTexFilter[] = { "NEAREST", "BILINEAR", "3-POINT", NULL };
|
|
static const int kMsaaSeq[] = { 1, 2, 4, 8 };
|
|
|
|
/* Windowed-mode resolution presets. Filtered at init to those that fit the
|
|
* desktop; the Resolution row cycles the surviving list. */
|
|
static const int kResList[][2] = {
|
|
{ 640, 480 }, { 800, 600 }, { 960, 720 }, { 1024, 768 },
|
|
{ 1152, 864 }, { 1280, 720 }, { 1280, 800 }, { 1280, 960 },
|
|
{ 1366, 768 }, { 1440, 900 }, { 1600, 900 }, { 1600, 1200 },
|
|
{ 1680, 1050 }, { 1920, 1080 }, { 1920, 1200 }, { 2560, 1440 },
|
|
{ 3200, 1800 }, { 3840, 2160 },
|
|
};
|
|
#define NUM_RES ((int)(sizeof(kResList) / sizeof(kResList[0])))
|
|
static int s_resFit[NUM_RES]; /* indices into kResList that fit the desktop */
|
|
static int s_resFitN = 0;
|
|
static int s_resSel = 0; /* index into s_resFit */
|
|
|
|
struct Row {
|
|
const char *key;
|
|
const char *label;
|
|
int kind;
|
|
double step;
|
|
const char *const *names; /* ROW_TOGGLE / ROW_ENUM value names */
|
|
int restart; /* value change needs a restart */
|
|
double uiMin, uiMax; /* 0,0 -> use the registered clamp */
|
|
|
|
/* resolved from config.c at init */
|
|
int found;
|
|
int type; /* CONFIG_OPT_* */
|
|
void *ptr;
|
|
double cfgMin, cfgMax;
|
|
|
|
/* Hidden while the option named here is nonzero (a manual % row
|
|
* disappears while its "auto" toggle is on). Resolved to hidePtr at init. */
|
|
const char *hiddenIfOn;
|
|
int *hidePtr;
|
|
};
|
|
|
|
static struct Row rows[] = {
|
|
{ "Video.Fullscreen", "Fullscreen", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
|
{ "__Resolution", "Resolution", ROW_RES, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
|
|
{ "Video.VSync", "VSync", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
|
{ "Video.FpsCap", "Frame cap", ROW_SLIDER, 10, NULL, 0, 0, 360, 0,0,0,0,0 },
|
|
{ "Video.MSAA", "MSAA", ROW_MSAA, 0, NULL, 1, 0, 0, 0,0,0,0,0 },
|
|
{ "Video.TextureFilter", "Texture filter", ROW_ENUM, 1, kTexFilter, 0, 0, 0, 0,0,0,0,0 },
|
|
{ "Video.Anisotropy", "Anisotropic", ROW_SLIDER, 1, NULL, 0, 0, 0, 0,0,0,0,0 },
|
|
{ "Video.FovScale", "FOV scale %", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 },
|
|
{ "Video.DrawDistance", "Draw distance %", ROW_SLIDER, 25, NULL, 0, 0, 0, 0,0,0,0,0, "Video.DrawDistanceAutoFov" },
|
|
{ "Video.DrawDistanceAutoFov","Draw dist. auto", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
|
{ "Video.LodDistance", "LOD distance %", ROW_SLIDER, 25, NULL, 0, 0, 0, 0,0,0,0,0, "Video.LodDistanceAutoFov" },
|
|
{ "Video.LodDistanceAutoFov", "LOD dist. auto", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
|
/* Aim row edits Input.AimModeSens -- the knob the default GEPD aim path
|
|
* actually uses (Input.MouseAimSpeed only feeds the legacy velocity-stick
|
|
* fallback, so it was inert here). */
|
|
{ "Input.AimModeSens", "Mouse aim speed", ROW_SLIDER, 1, NULL, 0, 1, 80, 0,0,0,0,0 },
|
|
{ "Input.MouseTurnSpeed", "Mouse turn speed", ROW_SLIDER, 1, NULL, 0, 0, 100, 0,0,0,0,0 },
|
|
{ "Input.SensLink", "Link aim/turn sens",ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
|
{ "Input.MouseInvertY", "Mouse invert Y", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
|
{ "Game.ScreenShakeIntensity","Screen shake", ROW_SLIDER, 0.25, NULL, 0, 0, 3, 0,0,0,0,0 },
|
|
/* D232: the community "no damage flash" toggle (suppresses the red/green
|
|
* hit-flash overlay in bondview2). */
|
|
{ "Game.NoHitFlash", "No hit flash", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
|
/* D216: read once at title-stage load (lv.c) -- toggling it takes effect
|
|
* the next time the title screen is reached. */
|
|
{ "Game.SkipIntro", "Skip intro", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
|
};
|
|
#define NUM_ROWS ((int)(sizeof(rows) / sizeof(rows[0])))
|
|
|
|
static int s_inited = 0;
|
|
static volatile int s_open = 0;
|
|
static int s_sel = 0; /* selection, index into s_visIdx (visible list) */
|
|
|
|
/* Visible-row list: rows whose hiddenIfOn option is nonzero are omitted
|
|
* (manual % rows hide while their auto toggle is on). Rebuilt every frame in
|
|
* overlayUpdateVisible(); s_scroll is the first visible-list entry drawn. */
|
|
static int s_visIdx[NUM_ROWS];
|
|
static int s_visN = 0;
|
|
static int s_scroll = 0;
|
|
|
|
/* D213: optional on-screen FPS readout (PD parity: Video.DisplayFPS). Drawn
|
|
* top-right whenever enabled, independent of the F10 panel. Config-only knob
|
|
* (kept off the 13-row panel, which is at its layout limit) -- matches PD,
|
|
* whose DisplayFPS is also file-only. Default 0 => emit path unchanged =>
|
|
* golden dumps byte-identical. */
|
|
static int s_showFps = 0;
|
|
static char s_fpsText[16] = "";
|
|
|
|
PD_CONSTRUCTOR static void overlayConfigInit(void)
|
|
{
|
|
configRegisterInt("Video.DisplayFPS", &s_showFps, 0, 1);
|
|
}
|
|
|
|
/* Sampled once per emitted frame; recomputes the string every ~0.5 s. */
|
|
static void fpsTick(void)
|
|
{
|
|
static uint64_t winStartUs = 0;
|
|
static int frames = 0;
|
|
|
|
uint64_t nowUs = sysGetMicroseconds();
|
|
if (winStartUs == 0) {
|
|
winStartUs = nowUs;
|
|
return;
|
|
}
|
|
frames++;
|
|
uint64_t dtUs = nowUs - winStartUs;
|
|
if (dtUs >= 500000) {
|
|
int fps = (int)((double)frames * 1e6 / (double)dtUs + 0.5);
|
|
snprintf(s_fpsText, sizeof(s_fpsText), "%d FPS", fps);
|
|
winStartUs = nowUs;
|
|
frames = 0;
|
|
}
|
|
}
|
|
|
|
/* Layout (game 2D pixel space = viGetX() x viGetY(), ~320x240). Shared by the
|
|
* emit path and the mouse hit-testing in optionsOverlayHandleInput().
|
|
* BankGothic caps are ~9 units tall here, so rows need ~16 units of pitch and
|
|
* values are right-aligned to the panel edge to survive the wide font. */
|
|
#define OV_X0 20
|
|
#define OV_LABEL_X 28
|
|
#define OV_TOP 12
|
|
#define OV_LINE 15 /* row pitch (13 rows must fit ~240) */
|
|
#define OV_HDR 2 /* header rows above row 0 (title+hint) */
|
|
/* i is a VISIBLE-POSITION (already scroll-adjusted by the caller). */
|
|
#define OV_ROW_Y(i) (OV_TOP + ((i) + OV_HDR) * OV_LINE)
|
|
|
|
/* How many rows fit between the header and the bottom edge of whatever 2D
|
|
* space we are in right now (320x240 in-game, 440x330 on front-end screens --
|
|
* viSetXY differs, see src/game/front.c). More rows than this => scroll. */
|
|
static int maxVisibleRows(void)
|
|
{
|
|
int n = (viGetY() - 6 - OV_TOP - OV_HDR * OV_LINE) / OV_LINE;
|
|
if (n < 4) {
|
|
n = 4;
|
|
}
|
|
return n;
|
|
}
|
|
#define OV_RIGHT (viGetX() - OV_X0) /* right edge for right-aligned text */
|
|
#define OV_NUM_W 36 /* reserved width for a slider's number */
|
|
#define OV_BAR_X 150
|
|
|
|
/* Slider fill bar span in overlay space (shared by emit + hit-testing). */
|
|
static void sliderBarSpan(s32 *x0, s32 *x1)
|
|
{
|
|
*x0 = OV_BAR_X;
|
|
*x1 = OV_RIGHT - OV_NUM_W;
|
|
if (*x1 < *x0 + 16) {
|
|
*x1 = *x0 + 16;
|
|
}
|
|
}
|
|
|
|
/* Visible position (0..s_visN-1) the given overlay-space y falls in, or -1. */
|
|
static int overlayRowAtY(double oy)
|
|
{
|
|
for (int p = 0; p < s_visN; p++) {
|
|
double top = OV_ROW_Y(p - s_scroll) - 3;
|
|
if (oy >= top && oy < top + OV_LINE) {
|
|
return p;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* Rebuild the visible-row list and keep the selection in range. */
|
|
static void overlayUpdateVisible(void)
|
|
{
|
|
s_visN = 0;
|
|
for (int i = 0; i < NUM_ROWS; i++) {
|
|
if (!(rows[i].hidePtr && *rows[i].hidePtr != 0)) {
|
|
s_visIdx[s_visN++] = i;
|
|
}
|
|
}
|
|
if (s_visN == 0) { /* cannot happen (toggles have no hide source) */
|
|
s_visIdx[s_visN++] = 0;
|
|
}
|
|
if (s_sel < 0) {
|
|
s_sel = 0;
|
|
}
|
|
if (s_sel >= s_visN) {
|
|
s_sel = s_visN - 1;
|
|
}
|
|
}
|
|
|
|
/* Keep the selected row on screen: shift the window when it nears an edge. */
|
|
static void overlayUpdateScroll(void)
|
|
{
|
|
int maxV = maxVisibleRows();
|
|
if (s_visN <= maxV) {
|
|
s_scroll = 0;
|
|
return;
|
|
}
|
|
s_scroll = s_sel - (maxV - 1);
|
|
if (s_scroll < 0) {
|
|
s_scroll = 0;
|
|
}
|
|
if (s_scroll > s_visN - maxV) {
|
|
s_scroll = s_visN - maxV;
|
|
}
|
|
}
|
|
|
|
/* The close box brackets the title row at the panel's right edge. */
|
|
#define OV_CB_X0 (OV_RIGHT - 14)
|
|
#define OV_CB_X1 (OV_RIGHT + 7)
|
|
#define OV_CB_Y0 (OV_TOP - 3)
|
|
#define OV_CB_Y1 (OV_TOP + 12)
|
|
static int overlayInCloseBox(double ox, double oy)
|
|
{
|
|
return ox >= OV_CB_X0 && ox <= OV_CB_X1 &&
|
|
oy >= OV_CB_Y0 && oy <= OV_CB_Y1;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
static void resolveCb(const char *key, int type, void *ptr, double min, double max,
|
|
double step, const char *label, const char *const *names,
|
|
void *ctx)
|
|
{
|
|
(void)step; (void)label; (void)names; (void)ctx;
|
|
for (int i = 0; i < NUM_ROWS; i++) {
|
|
if (strcmp(rows[i].key, key) == 0) {
|
|
rows[i].found = 1;
|
|
rows[i].type = type;
|
|
rows[i].ptr = ptr;
|
|
rows[i].cfgMin = min;
|
|
rows[i].cfgMax = max;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void overlayInit(void)
|
|
{
|
|
if (s_inited) {
|
|
return;
|
|
}
|
|
s_inited = 1;
|
|
|
|
/* Publish display metadata so config.c / future consumers can see it,
|
|
* without config.c knowing any specific key. */
|
|
for (int i = 0; i < NUM_ROWS; i++) {
|
|
configSetOptionMeta(rows[i].key, rows[i].label, rows[i].step, rows[i].names);
|
|
}
|
|
configForEachOption(resolveCb, NULL);
|
|
|
|
for (int i = 0; i < NUM_ROWS; i++) {
|
|
if (rows[i].kind == ROW_RES) {
|
|
rows[i].found = 1; /* not config-backed; driven via video.c */
|
|
continue;
|
|
}
|
|
if (!rows[i].found) {
|
|
sysLogPrintf(LOG_WARNING, "optionsoverlay: option '%s' not registered",
|
|
rows[i].key);
|
|
}
|
|
}
|
|
|
|
/* Resolve the hidden-while-on sources (manual % rows vs their auto
|
|
* toggles), then build the initial visible list. */
|
|
for (int i = 0; i < NUM_ROWS; i++) {
|
|
if (!rows[i].hiddenIfOn) {
|
|
continue;
|
|
}
|
|
for (int j = 0; j < NUM_ROWS; j++) {
|
|
if (strcmp(rows[j].key, rows[i].hiddenIfOn) == 0 && rows[j].found) {
|
|
rows[i].hidePtr = rows[j].ptr;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
overlayUpdateVisible();
|
|
|
|
/* Build the windowed-resolution preset list: presets that fit the desktop,
|
|
* plus the current window size snapped to the nearest surviving entry. */
|
|
{
|
|
int dw = 1920, dh = 1080;
|
|
videoGetDesktopSize(&dw, &dh);
|
|
s_resFitN = 0;
|
|
for (int i = 0; i < NUM_RES; i++) {
|
|
if (kResList[i][0] <= dw && kResList[i][1] <= dh) {
|
|
s_resFit[s_resFitN++] = i;
|
|
}
|
|
}
|
|
if (s_resFitN == 0) {
|
|
s_resFit[s_resFitN++] = 0;
|
|
}
|
|
int cw = 0, ch = 0;
|
|
videoGetWindowSize(&cw, &ch);
|
|
long best = -1;
|
|
for (int k = 0; k < s_resFitN; k++) {
|
|
int i = s_resFit[k];
|
|
long d = labs((long)kResList[i][0] - cw) +
|
|
labs((long)kResList[i][1] - ch);
|
|
if (best < 0 || d < best) { best = d; s_resSel = k; }
|
|
}
|
|
}
|
|
|
|
const char *e = getenv("GE_OPTIONSOVERLAY");
|
|
if (e && atoi(e) != 0) {
|
|
s_open = 1;
|
|
sysLogPrintf(LOG_INFO, "optionsoverlay: auto-opened (GE_OPTIONSOVERLAY)");
|
|
}
|
|
}
|
|
|
|
static double rowGet(const struct Row *r)
|
|
{
|
|
if (!r->found || !r->ptr) {
|
|
return 0.0;
|
|
}
|
|
switch (r->type) {
|
|
case CONFIG_OPT_INT: return (double)*(int *)r->ptr;
|
|
case CONFIG_OPT_UINT: return (double)*(unsigned int *)r->ptr;
|
|
case CONFIG_OPT_FLOAT: return (double)*(float *)r->ptr;
|
|
default: return 0.0;
|
|
}
|
|
}
|
|
|
|
static double rowLo(const struct Row *r)
|
|
{
|
|
return (r->uiMin != r->uiMax) ? r->uiMin : r->cfgMin;
|
|
}
|
|
static double rowHi(const struct Row *r)
|
|
{
|
|
return (r->uiMin != r->uiMax) ? r->uiMax : r->cfgMax;
|
|
}
|
|
|
|
static struct Row *rowByKey(const char *key)
|
|
{
|
|
for (int i = 0; i < NUM_ROWS; i++) {
|
|
if (strcmp(rows[i].key, key) == 0) {
|
|
return &rows[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int s_linkDepth = 0; /* re-entrancy guard for the sens link below */
|
|
|
|
static void rowSet(struct Row *r, double v)
|
|
{
|
|
double lo = rowLo(r), hi = rowHi(r);
|
|
if (lo != hi) {
|
|
if (v < lo) v = lo;
|
|
if (v > hi) v = hi;
|
|
}
|
|
switch (r->type) {
|
|
case CONFIG_OPT_INT: *(int *)r->ptr = (int)lround(v); break;
|
|
case CONFIG_OPT_UINT: *(unsigned int *)r->ptr = (unsigned int)(v < 0 ? 0 : lround(v)); break;
|
|
case CONFIG_OPT_FLOAT: *(float *)r->ptr = (float)v; break;
|
|
default: return;
|
|
}
|
|
|
|
/* Live-apply the video knobs that need a fast3d/SDL call. Everything else
|
|
* is read straight off the pointer by its owner every frame/poll. */
|
|
if (strcmp(r->key, "Video.Fullscreen") == 0) {
|
|
videoRequestFullscreen((int)lround(v));
|
|
} else if (strncmp(r->key, "Video.", 6) == 0 && !r->restart) {
|
|
videoRequestLiveConfig();
|
|
}
|
|
|
|
/* Linked aim/turn sensitivity (Input.SensLink, default on): moving either
|
|
* knob scales the other to hold the stock default ratio -- AimModeSens 38
|
|
* : MouseTurnSpeed 50 (the D194/D238 calibrated defaults). */
|
|
if (!s_linkDepth && strcmp(r->key, "Input.SensLink") != 0) {
|
|
struct Row *lk = rowByKey("Input.SensLink");
|
|
if (lk && lk->found && *(int *)lk->ptr != 0) {
|
|
struct Row *o = NULL;
|
|
double nv = 0.0;
|
|
if (strcmp(r->key, "Input.MouseTurnSpeed") == 0) {
|
|
o = rowByKey("Input.AimModeSens");
|
|
nv = v * 38.0 / 50.0;
|
|
} else if (strcmp(r->key, "Input.AimModeSens") == 0) {
|
|
o = rowByKey("Input.MouseTurnSpeed");
|
|
nv = v * 50.0 / 38.0;
|
|
}
|
|
if (o && o->found) {
|
|
s_linkDepth = 1;
|
|
rowSet(o, nv);
|
|
s_linkDepth = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void rowAdjust(struct Row *r, int dir)
|
|
{
|
|
if (!r->found) {
|
|
return;
|
|
}
|
|
double v = rowGet(r);
|
|
switch (r->kind) {
|
|
case ROW_TOGGLE:
|
|
rowSet(r, (v != 0.0) ? 0.0 : 1.0);
|
|
break;
|
|
case ROW_MSAA: {
|
|
int idx = 0;
|
|
for (int i = 0; i < 4; i++) {
|
|
if (kMsaaSeq[i] == (int)lround(v)) idx = i;
|
|
}
|
|
/* Wrap like a normal settings-menu cycle: OFF->2x->4x->8x->OFF, in
|
|
* both directions (left/right click and arrows all roll). */
|
|
idx = (idx + dir + 4) % 4;
|
|
rowSet(r, (double)kMsaaSeq[idx]);
|
|
break;
|
|
}
|
|
case ROW_ENUM: {
|
|
double lo = r->cfgMin, hi = r->cfgMax;
|
|
v += dir;
|
|
if (v < lo) v = hi;
|
|
if (v > hi) v = lo;
|
|
rowSet(r, v);
|
|
break;
|
|
}
|
|
case ROW_RES: {
|
|
if (s_resFitN <= 0 || videoIsFullscreen()) {
|
|
break; /* resolution is windowed-only */
|
|
}
|
|
s_resSel += (dir >= 0) ? 1 : -1;
|
|
if (s_resSel < 0) s_resSel = s_resFitN - 1;
|
|
if (s_resSel >= s_resFitN) s_resSel = 0;
|
|
int i = s_resFit[s_resSel];
|
|
videoRequestWindowSize(kResList[i][0], kResList[i][1]);
|
|
break;
|
|
}
|
|
default: /* ROW_SLIDER */
|
|
rowSet(r, v + dir * r->step);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
void optionsOverlayToggle(void)
|
|
{
|
|
overlayInit();
|
|
s_open = !s_open;
|
|
sysLogPrintf(LOG_INFO, "optionsoverlay: %s", s_open ? "opened" : "closed");
|
|
if (!s_open) {
|
|
configSave();
|
|
}
|
|
}
|
|
|
|
int optionsOverlayIsOpen(void)
|
|
{
|
|
if (!s_inited) {
|
|
overlayInit();
|
|
}
|
|
return s_open;
|
|
}
|
|
|
|
void optionsOverlayScroll(int dir)
|
|
{
|
|
if (!s_inited) {
|
|
overlayInit();
|
|
}
|
|
if (!s_open || dir == 0) {
|
|
return;
|
|
}
|
|
overlayUpdateVisible();
|
|
s_sel += (dir > 0) ? -1 : 1; /* wheel-up -> move up the list */
|
|
/* Clamp at both ends like a normal PC settings list -- wrapping made the
|
|
* selection "repeat" from the far edge, which read as a duplicate. */
|
|
if (s_sel < 0) s_sel = 0;
|
|
if (s_sel >= s_visN) s_sel = s_visN - 1;
|
|
}
|
|
|
|
/* Set a slider row from an overlay-space x inside its value bar, snapped to
|
|
* the row's step. */
|
|
static void sliderSetFromX(struct Row *r, double ox)
|
|
{
|
|
double lo = rowLo(r), hi = rowHi(r);
|
|
if (hi <= lo) {
|
|
return;
|
|
}
|
|
s32 bx0, bx1;
|
|
sliderBarSpan(&bx0, &bx1);
|
|
double f = (ox - bx0) / (double)(bx1 - bx0);
|
|
if (f < 0) f = 0;
|
|
if (f > 1) f = 1;
|
|
double v = lo + f * (hi - lo);
|
|
double step = (r->step > 0.0) ? r->step : 1.0;
|
|
v = lround(v / step) * step;
|
|
rowSet(r, v);
|
|
}
|
|
|
|
void optionsOverlayHandleInput(void)
|
|
{
|
|
static int prevUp, prevDn, prevLf, prevRt, prevLmb, prevRmb;
|
|
static int dragRow = -1;
|
|
|
|
if (!s_open) {
|
|
prevUp = prevDn = prevLf = prevRt = prevLmb = prevRmb = 0;
|
|
dragRow = -1;
|
|
return;
|
|
}
|
|
|
|
/* The overlay owns the mouse while it is open: force the OS cursor free +
|
|
* visible (a stage poll would otherwise leave it locked/hidden). */
|
|
inputSuspendForOverlay();
|
|
|
|
overlayUpdateVisible(); /* % rows may have appeared/vanished (auto toggles) */
|
|
overlayUpdateScroll();
|
|
|
|
const Uint8 *ks = SDL_GetKeyboardState(NULL);
|
|
int mx = 0, my = 0;
|
|
Uint32 mb = SDL_GetMouseState(&mx, &my);
|
|
int lmb = (mb & SDL_BUTTON(SDL_BUTTON_LEFT)) ? 1 : 0;
|
|
int rmb = (mb & SDL_BUTTON(SDL_BUTTON_RIGHT)) ? 1 : 0;
|
|
|
|
/* Gamepad navigation (controller-only machines, e.g. Steam Deck): the
|
|
* D-pad or left stick moves the selection; A/X step forward (the Enter
|
|
* equivalent), B/Y step back; Start closes. OR-ed into the same edge
|
|
* logic as the keyboard, so repeat/clamp/scroll behaviour is identical.
|
|
* input.c swallows the pad while we are open, so none of this reaches
|
|
* the game. (Select also closes -- handled in input.c's toggle, which
|
|
* runs before this one.) */
|
|
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 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]
|
|
|| inputPadButton(0, SDL_CONTROLLER_BUTTON_B)
|
|
|| inputPadButton(0, SDL_CONTROLLER_BUTTON_Y);
|
|
int rt = ks[SDL_SCANCODE_RIGHT] || ks[SDL_SCANCODE_KP_6]
|
|
|| ks[SDL_SCANCODE_RETURN] || ks[SDL_SCANCODE_KP_ENTER]
|
|
|| inputPadButton(0, SDL_CONTROLLER_BUTTON_A)
|
|
|| inputPadButton(0, SDL_CONTROLLER_BUTTON_X);
|
|
|
|
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);
|
|
if (startNow && !prevStart) optionsOverlayToggle(); /* Start closes */
|
|
prevStart = startNow;
|
|
|
|
/* ---- keyboard / D-pad nav (clamped at the ends; scroll follows) ---- */
|
|
if (up && !prevUp && s_sel > 0) s_sel--;
|
|
if (dn && !prevDn && s_sel < s_visN - 1) s_sel++;
|
|
overlayUpdateScroll();
|
|
if (lf && !prevLf) rowAdjust(&rows[s_visIdx[s_sel]], -1);
|
|
if (rt && !prevRt) rowAdjust(&rows[s_visIdx[s_sel]], +1);
|
|
|
|
/* ---- mouse ---- */
|
|
int ww = 0, wh = 0;
|
|
videoGetWindowSize(&ww, &wh);
|
|
if (ww > 0 && wh > 0) {
|
|
double ox = (double)mx * (double)viGetX() / ww;
|
|
double oy = (double)my * (double)viGetY() / wh;
|
|
int hoverVis = overlayRowAtY(oy);
|
|
int onClose = overlayInCloseBox(ox, oy);
|
|
|
|
/* No hover-to-highlight: merely moving the mouse must not move the
|
|
* selection or scroll the window (hovering near a list edge fed the
|
|
* new row back into the cursor and made the bottom twitch/echo).
|
|
* Selection moves only by click, wheel, or arrows. */
|
|
|
|
/* left press. A click in the label column only focuses the row; a
|
|
* click in the value/control column (>= the bar-span start) changes
|
|
* it -- so clicking to select a toggle doesn't also flip it. */
|
|
s32 bx0, bx1;
|
|
sliderBarSpan(&bx0, &bx1);
|
|
if (lmb && !prevLmb) {
|
|
if (onClose) {
|
|
optionsOverlayToggle(); /* close + configSave */
|
|
return;
|
|
}
|
|
if (hoverVis >= 0) {
|
|
s_sel = hoverVis; /* explicit click -> select */
|
|
overlayUpdateScroll();
|
|
if (ox >= bx0) {
|
|
struct Row *r = &rows[s_visIdx[hoverVis]];
|
|
if (r->kind == ROW_SLIDER && r->found) {
|
|
sliderSetFromX(r, ox);
|
|
dragRow = s_visIdx[hoverVis];
|
|
} else {
|
|
rowAdjust(r, +1); /* toggle / cycle forward (wraps) */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* drag a slider */
|
|
if (lmb && dragRow >= 0 && rows[dragRow].kind == ROW_SLIDER) {
|
|
sliderSetFromX(&rows[dragRow], ox);
|
|
}
|
|
if (!lmb) {
|
|
dragRow = -1;
|
|
}
|
|
/* right press in the value column: cycle back / decrement */
|
|
if (rmb && !prevRmb && hoverVis >= 0 && !onClose && ox >= bx0) {
|
|
s_sel = hoverVis;
|
|
overlayUpdateScroll();
|
|
rowAdjust(&rows[s_visIdx[hoverVis]], -1);
|
|
}
|
|
}
|
|
|
|
prevUp = up; prevDn = dn; prevLf = lf; prevRt = rt;
|
|
prevLmb = lmb; prevRmb = rmb;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
#define OV_BUF_CMDS 8192
|
|
static Gfx s_buf[OV_BUF_CMDS];
|
|
|
|
static Gfx *fillRect(Gfx *gdl, s32 x0, s32 y0, s32 x1, s32 y1,
|
|
u8 r, u8 g, u8 b, u8 a)
|
|
{
|
|
gDPSetRenderMode(gdl++, G_RM_XLU_SURF, G_RM_XLU_SURF2);
|
|
gDPSetCombineMode(gdl++, G_CC_PRIMITIVE, G_CC_PRIMITIVE);
|
|
gDPSetPrimColor(gdl++, 0, 0, r, g, b, a);
|
|
gDPFillRectangle(gdl++, x0, y0, x1, y1);
|
|
return gdl;
|
|
}
|
|
|
|
static void valueText(const struct Row *r, char *out, int n)
|
|
{
|
|
double v = rowGet(r);
|
|
if (r->kind == ROW_RES) {
|
|
if (videoIsFullscreen()) {
|
|
snprintf(out, n, "(fullscreen)");
|
|
} else if (s_resFitN <= 0) {
|
|
snprintf(out, n, "n/a");
|
|
} else {
|
|
int i = s_resFit[s_resSel];
|
|
snprintf(out, n, "%d x %d", kResList[i][0], kResList[i][1]);
|
|
}
|
|
return;
|
|
}
|
|
if ((r->kind == ROW_TOGGLE || r->kind == ROW_ENUM) && r->names) {
|
|
int idx = (int)lround(v);
|
|
int cnt = 0;
|
|
while (r->names[cnt]) cnt++;
|
|
if (idx >= 0 && idx < cnt) {
|
|
snprintf(out, n, "%s", r->names[idx]);
|
|
return;
|
|
}
|
|
}
|
|
if (r->kind == ROW_MSAA) {
|
|
if ((int)lround(v) <= 1) snprintf(out, n, "OFF");
|
|
else snprintf(out, n, "%dx", (int)lround(v));
|
|
return;
|
|
}
|
|
if (r->kind == ROW_SLIDER && r->type == CONFIG_OPT_FLOAT) {
|
|
snprintf(out, n, "%.2f", v);
|
|
return;
|
|
}
|
|
if (r->kind == ROW_SLIDER && (int)lround(v) == 0 &&
|
|
strcmp(r->key, "Video.FpsCap") == 0) {
|
|
snprintf(out, n, "OFF");
|
|
return;
|
|
}
|
|
snprintf(out, n, "%d", (int)lround(v));
|
|
}
|
|
|
|
static Gfx *drawText(Gfx *gdl, s32 x, s32 y, const char *str, u32 colour)
|
|
{
|
|
s32 px = x, py = y;
|
|
/* width/height are the on-screen CLIP rect textRenderGlyph tests against
|
|
* (clipX=start x, clipY=start y, +clipWidth/+clipHeight), NOT the text's
|
|
* own measured size -- passing the measured w/h clipped every glyph out
|
|
* (baseline+height > measured h => nothing drawn). Match the game: pass
|
|
* the full 2D viewport, like bondview2.c's debug-text path. */
|
|
return textRender(gdl, &px, &py, (char *)str, ptrFontBankGothicChars,
|
|
ptrFontBankGothic, colour, viGetX(), viGetY(), 0, 0);
|
|
}
|
|
|
|
static s32 measureText(const char *str)
|
|
{
|
|
s32 h = 0, w = 0;
|
|
textMeasure(&h, &w, (char *)str, ptrFontBankGothicChars, ptrFontBankGothic, 0);
|
|
return w;
|
|
}
|
|
|
|
/* Right-aligned: the string ends at xr. */
|
|
static Gfx *drawTextR(Gfx *gdl, s32 xr, s32 y, const char *str, u32 colour)
|
|
{
|
|
return drawText(gdl, xr - measureText(str), y, str, colour);
|
|
}
|
|
|
|
Gfx *optionsOverlayEmit(void)
|
|
{
|
|
if (!s_inited) {
|
|
overlayInit();
|
|
}
|
|
|
|
fpsTick();
|
|
|
|
if (!s_open) {
|
|
if (!s_showFps || !s_fpsText[0]) {
|
|
return NULL; /* nothing appended -> golden dumps byte-identical */
|
|
}
|
|
/* D213: FPS-only mini DL (top-right), panel closed. */
|
|
const s32 fw = viGetX();
|
|
const s32 fh = viGetY();
|
|
Gfx *fgdl = s_buf;
|
|
gDPPipeSync(fgdl++);
|
|
gDPSetCycleType(fgdl++, G_CYC_1CYCLE);
|
|
gDPSetTexturePersp(fgdl++, G_TP_NONE);
|
|
gDPSetScissor(fgdl++, G_SC_NON_INTERLACE, 0, 0, fw, fh);
|
|
fgdl = microcode_constructor(fgdl);
|
|
fgdl = drawTextR(fgdl, fw - 6, 6, s_fpsText, 0x40ff60ff);
|
|
gDPPipeSync(fgdl++);
|
|
gSPEndDisplayList(fgdl++);
|
|
return s_buf;
|
|
}
|
|
|
|
overlayUpdateVisible();
|
|
overlayUpdateScroll();
|
|
|
|
const s32 W = viGetX();
|
|
const s32 H = viGetY();
|
|
const s32 right = OV_RIGHT;
|
|
const s32 panelTop = OV_TOP - 9;
|
|
/* Last visible position actually drawn (window may be shorter than the
|
|
* list at small 2D viewports -- the rest is reached by scrolling). */
|
|
const int maxV = maxVisibleRows();
|
|
const int pLast = (s_visN - s_scroll < maxV) ? s_visN - s_scroll
|
|
: s_scroll + maxV - 1;
|
|
const s32 panelBottom = OV_ROW_Y(pLast) + OV_LINE / 2 + 3;
|
|
s32 bx0, bx1;
|
|
sliderBarSpan(&bx0, &bx1);
|
|
Gfx *gdl = s_buf;
|
|
|
|
gDPPipeSync(gdl++);
|
|
gDPSetCycleType(gdl++, G_CYC_1CYCLE);
|
|
gDPSetTexturePersp(gdl++, G_TP_NONE);
|
|
gDPSetScissor(gdl++, G_SC_NON_INTERLACE, 0, 0, W, H);
|
|
|
|
/* ---- pass 1: all fills (G_CC_PRIMITIVE) ---- */
|
|
gdl = fillRect(gdl, 0, 0, W, H, 0, 0, 0, 150); /* dim */
|
|
gdl = fillRect(gdl, OV_X0 - 8, panelTop, W - (OV_X0 - 8), panelBottom,
|
|
8, 10, 24, 210); /* panel */
|
|
gdl = fillRect(gdl, OV_CB_X0, OV_CB_Y0, OV_CB_X1, OV_CB_Y1,
|
|
150, 40, 40, 235); /* close */
|
|
|
|
for (int p = s_scroll; p <= pLast; p++) {
|
|
const struct Row *r = &rows[s_visIdx[p]];
|
|
s32 rowY = OV_ROW_Y(p - s_scroll);
|
|
if (p == s_sel) {
|
|
gdl = fillRect(gdl, OV_X0 - 4, rowY - 3, W - (OV_X0 - 4),
|
|
rowY + OV_LINE - 4, 40, 46, 96, 220);
|
|
}
|
|
if (r->kind == ROW_SLIDER && r->found) {
|
|
double lo = rowLo(r), hi = rowHi(r);
|
|
double f = (hi > lo) ? (rowGet(r) - lo) / (hi - lo) : 0.0;
|
|
if (f < 0) f = 0; if (f > 1) f = 1;
|
|
s32 by = rowY + 3;
|
|
gdl = fillRect(gdl, bx0, by, bx1, by + 5, 60, 60, 70, 220);
|
|
gdl = fillRect(gdl, bx0, by, bx0 + (s32)((bx1 - bx0) * f), by + 5,
|
|
210, 200, 90, 255);
|
|
}
|
|
}
|
|
|
|
/* ---- pass 2: text ---- */
|
|
gdl = microcode_constructor(gdl);
|
|
|
|
gdl = drawText(gdl, OV_X0, OV_TOP, "PC OPTIONS", 0xffe040ff);
|
|
gdl = drawText(gdl, OV_X0, OV_TOP + OV_LINE,
|
|
"select: scroll/click, change: mouse/arrows",
|
|
0x8890a0ff); /* hint line */
|
|
gdl = drawText(gdl, (OV_CB_X0 + OV_CB_X1) / 2 - measureText("X") / 2,
|
|
OV_TOP, "X", 0xffffffff); /* close glyph */
|
|
|
|
for (int p = s_scroll; p <= pLast; p++) {
|
|
const struct Row *r = &rows[s_visIdx[p]];
|
|
s32 rowY = OV_ROW_Y(p - s_scroll);
|
|
u32 col = (p == s_sel) ? 0xffffffff : 0xc0c0c8ff;
|
|
char val[48];
|
|
|
|
gdl = drawText(gdl, OV_LABEL_X, rowY, (char *)r->label,
|
|
r->found ? col : 0x808080ff);
|
|
if (!r->found) {
|
|
gdl = drawTextR(gdl, right, rowY, "(n/a)", 0x808080ff);
|
|
continue;
|
|
}
|
|
|
|
valueText(r, val, sizeof(val));
|
|
if (r->restart) {
|
|
/* value left of the bar span, "(restart)" pinned to the edge */
|
|
gdl = drawText(gdl, bx0, rowY, val, col);
|
|
gdl = drawTextR(gdl, right, rowY, "(restart)", 0x909090ff);
|
|
} else {
|
|
gdl = drawTextR(gdl, right, rowY, val, col);
|
|
}
|
|
}
|
|
|
|
gDPPipeSync(gdl++);
|
|
gSPEndDisplayList(gdl++);
|
|
|
|
if ((gdl - s_buf) > OV_BUF_CMDS) {
|
|
sysLogPrintf(LOG_ERROR, "optionsoverlay: DL overflow (%d)", (int)(gdl - s_buf));
|
|
}
|
|
return s_buf;
|
|
}
|