D257: Game.AllUnlocked option — all levels, 007 mode, full cheat menu (#79)

Port-layer only (no game-code edits):
- main.c seeds debug_enable_all_levels_flag + debug_007_unlock_flag
  after configLoad() when Game.AllUnlocked is set (same class as the
  existing GE_UNLOCK_ALL getenv hook).
- libultra.c eep shim patches the save block at read time (the exact
  fileValidateSaves pattern: block 4, 5x96 bytes): sets all 20
  progression-gated cheat-unlock bits per slot and fills every empty
  stage/difficulty completion time with the max (0x3FF), preserving
  existing player times, then recomputes each slot's CRC with the
  game's own fileGenerateCRC so validation passes. The completion-time
  half is required: with any SP cheat active, g_AppendCheatSinglePlayer
  makes mission select accept only STAGESTATUS_COMPLETED levels, so an
  unlock-only save locks every level once a cheat is turned on.
- optionsoverlay.c: 'All unlocked' toggle row (default ON for this cut).

Verified: fresh-eep GE_SAVELOG shows all 5 slots stored==calc; user
playtest confirms all levels at each difficulty, 007 mode, populated
cheat menu, cheats working, mission select intact with all cheats on.
This commit is contained in:
James Dansereau authored and GitHub committed 2026-09-13 21:48:09 -04:00
1 parent c51f01ad14
commit c62803f890
5 files changed
+166

No files matched your search

+1
View File
@@ -534,6 +534,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D254 | **Linux crash handler destroyed its own logs: `ge007.crash.log` only ever contained the SIGABRT block, never the original SEGV.** The POSIX `crashHandler` (`port/src/crash.c`) finished by calling `sysFatalError()` → `abort()` → SIGABRT, which re-entered the *same* handler (SIGABRT was one of the installed signals); the second pass reopened the log with `"wb"` and overwrote the first pass's SEGV block — faulting PC, signal, everything. The Deck firefight crash (D255) came back as a bare `SIGNAL: 6` abort record for exactly this reason, forcing offline disassembly of the CI binary to recover what the log should have said. **Fix (port-only):** re-entrancy guard (`inCrashHandler`, second pass `_exit(128+sig)` immediately) + terminate directly with `_exit()` instead of `sysFatalError()` (the Windows path already terminated directly for the same reason) + the Linux block now dumps the faulting context: `FAULT ADDR` (`si_addr`) and the full general register set (Rax/Rcx/Rdx/Rsi/Rdi/R8–R11, Rsp/Rbp) from the `ucontext`, so a field crash now names the bad pointer on its face. | FIXED — needs one Deck crash to prove the new format in the field. | | D254 | **Linux crash handler destroyed its own logs: `ge007.crash.log` only ever contained the SIGABRT block, never the original SEGV.** The POSIX `crashHandler` (`port/src/crash.c`) finished by calling `sysFatalError()` → `abort()` → SIGABRT, which re-entered the *same* handler (SIGABRT was one of the installed signals); the second pass reopened the log with `"wb"` and overwrote the first pass's SEGV block — faulting PC, signal, everything. The Deck firefight crash (D255) came back as a bare `SIGNAL: 6` abort record for exactly this reason, forcing offline disassembly of the CI binary to recover what the log should have said. **Fix (port-only):** re-entrancy guard (`inCrashHandler`, second pass `_exit(128+sig)` immediately) + terminate directly with `_exit()` instead of `sysFatalError()` (the Windows path already terminated directly for the same reason) + the Linux block now dumps the faulting context: `FAULT ADDR` (`si_addr`) and the full general register set (Rax/Rcx/Rdx/Rsi/Rdi/R8–R11, Rsp/Rbp) from the `ucontext`, so a field crash now names the bad pointer on its face. | FIXED — needs one Deck crash to prove the new format in the field. |
| D255 | **Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle, D253 fix confirmed working — user got past the opening crouch tile).** Crash during gunplay; abort-unwind backtrace points at `sub_GAME_7F09BAC4` (`vtxstore.c:158`) ← `sub_GAME_7F09BBBC` (`:220`) ← `lvlManageMpGame` (`lv.c:1302`) ← `bossMainloop` (`boss.c:567`). Disassembling the exact CI binary at the fault PC (`0x200db135`) gives `mov 0x14(%rax),%edi; mov 0x10(%rdi),%rax`: the function walks the active PropRecord list keeping `type == 1` — which is **PROP_TYPE_OBJ, not CHR** (the decompiler cast to the union's first member and misnamed the field `ChrRecord.chrflags`; it is actually `ObjectRecord.model` at `0x14`) — then dereferences `model->obj`. Two port-relevant wrinkles: (a) the load is **32-bit zero-extended** because the decomp types the field through the `CHRFLAG` enum, so a model pointer ≥ 4 GB would truncate to garbage; (b) the loop skips records whose `model->obj` is NULL but *dereferences* `model` unconditionally, so an active object with a NULL/freed `model` faults — plausible in a firefight where destructibles (crates etc.) die. Not reproduced on WSL after equivalent play. **Next:** the D254 handler now captures registers at fault time (`rax` = the ObjectRecord, `rdi` = its model value) — one more Deck crash identifies which case: tiny/zero `rdi` → destroy-path ordering; plausible-but-wrong `rdi` → stale/reused record or truncation. | OPEN — awaiting register capture from the Deck. | | D255 | **Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle, D253 fix confirmed working — user got past the opening crouch tile).** Crash during gunplay; abort-unwind backtrace points at `sub_GAME_7F09BAC4` (`vtxstore.c:158`) ← `sub_GAME_7F09BBBC` (`:220`) ← `lvlManageMpGame` (`lv.c:1302`) ← `bossMainloop` (`boss.c:567`). Disassembling the exact CI binary at the fault PC (`0x200db135`) gives `mov 0x14(%rax),%edi; mov 0x10(%rdi),%rax`: the function walks the active PropRecord list keeping `type == 1` — which is **PROP_TYPE_OBJ, not CHR** (the decompiler cast to the union's first member and misnamed the field `ChrRecord.chrflags`; it is actually `ObjectRecord.model` at `0x14`) — then dereferences `model->obj`. Two port-relevant wrinkles: (a) the load is **32-bit zero-extended** because the decomp types the field through the `CHRFLAG` enum, so a model pointer ≥ 4 GB would truncate to garbage; (b) the loop skips records whose `model->obj` is NULL but *dereferences* `model` unconditionally, so an active object with a NULL/freed `model` faults — plausible in a firefight where destructibles (crates etc.) die. Not reproduced on WSL after equivalent play. **Next:** the D254 handler now captures registers at fault time (`rax` = the ObjectRecord, `rdi` = its model value) — one more Deck crash identifies which case: tiny/zero `rdi` → destroy-path ordering; plausible-but-wrong `rdi` → stale/reused record or truncation. | OPEN — awaiting register capture from the Deck. |
| D256 | **Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing `data/` — reported as "saves do not work at all" on Steam Deck.** `romdataInit` finds the ROM in `$S/` (CWD `data/`) *or* `$E/` (exe dir) *or* `./`, but `sysResolvePath`'s POSIX `$S` branch (used by `ge007.eep` and `ge007.ini`) only ever produced a CWD-relative path — its "fallback" was `./data/`, identical to the primary. Launch from any other CWD (Steam non-steam-game shortcut, terminal in `~`): ROM loads via the exe-dir candidate, game runs, but every EEPROM/config write hits `fopen(..., "wb")` on a nonexistent `./data/` and fails with a stderr-only "cannot write" — saves never persist, and **every launch also starts with default settings** (F10 changes don't stick), which presents as "draw distance/LOD is low" on levels like Dam where the user had raised `Video.DrawDistance` on another machine. The Windows branch already did CWD-else-exedir; the POSIX branch now mirrors it (`readlink("/proc/self/exe")`) and creates the directory best-effort so first-run writes don't fail. **Verified on WSL:** with exe+`data/` in `/tmp/fb2` and CWD=`/`, config/eeprom/ROM all resolve into `/tmp/fb2/data/` and a 25 s run completes clean; the pre-fix binary in the same layout fails to find the ROM at all. **Caveat:** if the user's Deck launch *was* from the bundle dir (CWD had `data/`), this fix does not explain their save failure — need the save-flow detail (does the memory-card screen appear? slot confirm? "saved" message?) before closing the report. Side note found while testing: a `data/` without the generated sidecars (`pccg-*/`, `pcmodels-*/` from `tools_pc/d69_emit.py`/`d43_emit.py`) starts fine but faults in `load_bg_file` (`bg.c:865`) on level load — the reserve functions warn "stage loads will fault" and return 0; expected degraded mode, not a new bug. | FIXED (path half) — save-flow detail still owed from the user. | | D256 | **Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing `data/` — reported as "saves do not work at all" on Steam Deck.** `romdataInit` finds the ROM in `$S/` (CWD `data/`) *or* `$E/` (exe dir) *or* `./`, but `sysResolvePath`'s POSIX `$S` branch (used by `ge007.eep` and `ge007.ini`) only ever produced a CWD-relative path — its "fallback" was `./data/`, identical to the primary. Launch from any other CWD (Steam non-steam-game shortcut, terminal in `~`): ROM loads via the exe-dir candidate, game runs, but every EEPROM/config write hits `fopen(..., "wb")` on a nonexistent `./data/` and fails with a stderr-only "cannot write" — saves never persist, and **every launch also starts with default settings** (F10 changes don't stick), which presents as "draw distance/LOD is low" on levels like Dam where the user had raised `Video.DrawDistance` on another machine. The Windows branch already did CWD-else-exedir; the POSIX branch now mirrors it (`readlink("/proc/self/exe")`) and creates the directory best-effort so first-run writes don't fail. **Verified on WSL:** with exe+`data/` in `/tmp/fb2` and CWD=`/`, config/eeprom/ROM all resolve into `/tmp/fb2/data/` and a 25 s run completes clean; the pre-fix binary in the same layout fails to find the ROM at all. **Caveat:** if the user's Deck launch *was* from the bundle dir (CWD had `data/`), this fix does not explain their save failure — need the save-flow detail (does the memory-card screen appear? slot confirm? "saved" message?) before closing the report. Side note found while testing: a `data/` without the generated sidecars (`pccg-*/`, `pcmodels-*/` from `tools_pc/d69_emit.py`/`d43_emit.py`) starts fine but faults in `load_bg_file` (`bg.c:865`) on level load — the reserve functions warn "stage loads will fault" and return 0; expected degraded mode, not a new bug. | FIXED (path half) — save-flow detail still owed from the user. |
| D257 | **New `Game.AllUnlocked` option (F10, default ON for this cut): skip N64 progression entirely — all 21 solo levels selectable at every difficulty, 007 mode available, and the cheat menu fully populated. Port-layer only; no game-code edits.** Three mechanisms, all in `port/`: (1) `main.c` seeds the two live `LEFTOVERDEBUG` RAM flags after `configLoad()` — `debug_enable_all_levels_flag` (all levels) and `debug_007_unlock_flag` (007 mode; both in `src/game/debugmenu_handler.c`, same class as the existing `GE_UNLOCK_ALL` getenv hook in the getter); (2) the eep shim (`libultra.c geEepromPatchAllCheats`) patches the save block at read time — special-casing `fileValidateSaves`'s exact pattern (block 4, 5×96 bytes) — setting all 20 progression-gated cheat-unlock bits per slot (`unlocked_cheats_1/2/3`; the cheat menu row on mode-select only renders when ≥1 is set, `front.c interface_menu06_modesel`) **and** filling every *empty* stage/difficulty completion-time field with the max time 0x3FF (existing player times preserved); both are then CRC'd per slot with the game's own `fileGenerateCRC` or `fileValidateSaves` would wipe the slots. (3) `optionsoverlay.c` row so users can toggle OFF for faithful progression. **Why the completion-time half is required (found by user repro):** when ANY single-player cheat is active, `g_AppendCheatSinglePlayer` makes mission-select (`get_highest_unlocked_difficulty_for_level`, front.c) accept only levels with status `STAGESTATUS_COMPLETED (3)`, not merely `UNLOCKED (1)` — AllUnlocked's debug-flag path returns 1, so enabling a cheat locked every level ("could only do Silo"). Reading the save as a fully-completed campaign satisfies both branches. Time bit-math mirrors `fileGet/SetSaveStageDifficultyTime` verbatim (10-bit fields, difficulties 0–2; 007 times are virtual). **Pitfall hit during implementation:** the first mirror of `save_data` used `times[80]` instead of `(SP_LEVEL_MAX-1)*4 = 76`, making `sizeof` 100≠96 so the block-read size check silently never matched — now enforced by a compile-time `ge_save_slot_size_check`. **Semantics:** unlocked cheats/completions persist to `ge007.eep` on any subsequent save write (self-consistent; mirrors N64 where completing a level permanently unlocks its cheat). No *active* cheats are enabled (invincibility/all-guns stay OFF); weapons remain per-mission pickups. **Verified:** fresh eep — all 5 slots validate `stored==calc` under `GE_SAVELOG=1`; user playtest: all levels selectable at each difficulty, 007 mode present, cheat menu populated, cheats toggle and work (time-modifying cheats untested by user), mission select intact with all cheats ON. | FIXED — verified by user playtest; PR open. |
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software
+125
View File
@@ -1188,6 +1188,126 @@ static void geEepromStore(void)
} }
} }
/* D257: Game.AllUnlocked -- mirror of src/game/file.h save_data (96 bytes;
* keep in sync). Two things are patched in at read time:
* - the progression-gated cheat-unlock bits (front.c
* frontCheckIfCheatIsUnlocked) test per-LEVEL bits in
* unlocked_cheats_1/2/3 that normal play sets when each level is
* completed, so a fresh save has an empty cheat menu;
* - every stage/difficulty completion time. When ANY single-player cheat
* is active, g_AppendCheatSinglePlayer makes the mission-select screen
* (front.c get_highest_unlocked_difficulty_for_level) only accept levels
* whose status is STAGESTATUS_COMPLETED (3), not merely UNLOCKED (1) --
* so AllUnlocked must read as a fully-completed campaign or enabling a
* cheat locks every level. Existing (nonzero) player times are kept;
* only empty slots get the max time (0x3FF). */
typedef struct ge_save_slot {
s32 chksum1;
s32 chksum2;
u8 completion_bitflags;
u8 flag_007;
u8 music_vol;
u8 sfx_vol;
u16 options;
u8 unlocked_cheats_1;
u8 unlocked_cheats_2;
u8 unlocked_cheats_3;
u8 padding;
u8 times[76]; /* (SP_LEVEL_MAX-1)*4: 19 levels x 4 difficulties */
} ge_save_slot; /* sizeof == 96 == save_data (file.h) -- enforced below */
/* If this ever fires, the mirror drifted from save_data and the AllUnlocked
* cheat patch silently no-ops (the block-read size check stops matching). */
typedef char ge_save_slot_size_check[(sizeof(ge_save_slot) == 96) ? 1 : -1];
/* Game-side CRC (src/game/crc.c) over [completion_bitflags, next slot),
* stored in the slot's own chksum1/2 -- exactly what fileValidateSaves
* re-checks after the block read. */
extern void fileGenerateCRC(u8 *addressA, u8 *addressB, void *retval);
extern s32 portAllUnlocked; /* port/src/video.c */
/* Bit math mirrors fileGetSaveStageDifficultyTime / fileSetDifficultyStageTime
* (src/game/file2.c): 10-bit time fields, offset = (difficulty*20+level)*10,
* difficulties 0..2 (agent/secret/00) only -- 007 times are virtual. */
static u32 geSaveGetTime(const u8 *t, s32 difficulty, s32 level)
{
s32 offset = (difficulty * 20 + level) * 10;
s32 index = offset >> 3;
switch (7 - (offset & 7)) {
case 7: return ((u32)(t[index] & 0xFF) << 2) | ((u32)(t[index + 1] & 0xc0) >> 6);
case 5: return ((u32)(t[index] & 0x3f) << 4) | ((u32)(t[index + 1] & 0xf0) >> 4);
case 3: return ((u32)(t[index] & 0x0f) << 6) | ((u32)(t[index + 1] & 0xfc) >> 2);
case 1: return ((u32)(t[index] & 0x03) << 8) | (u32)(t[index + 1] & 0xFF);
default: return 0;
}
}
static void geSaveSetTime(u8 *t, s32 difficulty, s32 level, u32 newtime)
{
s32 offset = (difficulty * 20 + level) * 10;
s32 index = offset >> 3;
switch (7 - (offset & 7)) {
case 7:
t[index] = (u8)((t[index] & 0x00) | ((newtime >> 2) & 0xFF));
t[index + 1] = (u8)((t[index + 1] & 0x3F) | ((newtime << 6) & 0xC0));
break;
case 5:
t[index] = (u8)((t[index] & 0xC0) | ((newtime >> 4) & 0x3F));
t[index + 1] = (u8)((t[index + 1] & 0x0F) | ((newtime << 4) & 0xF0));
break;
case 3:
t[index] = (u8)((t[index] & 0xF0) | ((newtime >> 6) & 0x0F));
t[index + 1] = (u8)((t[index + 1] & 0x03) | ((newtime << 2) & 0xFC));
break;
case 1:
t[index] = (u8)((t[index] & 0xFC) | ((newtime >> 8) & 0x03));
t[index + 1] = (u8)(newtime & 0xFF);
break;
}
}
static void geEepromPatchAllCheats(u8 *buf)
{
/* buf holds the five save slots read from block 4. The sixth local slot
* is only a CRC end-boundary for the last one (fileGenerateCRC reads
* [A, B), so its contents are irrelevant). */
static ge_save_slot slots[6];
memcpy(slots, buf, sizeof(ge_save_slot) * 5);
memset(&slots[5], 0, sizeof(ge_save_slot));
int changed = 0;
for (int i = 0; i < 5; i++) {
/* Cheat ids are level ids 0..19 (CHEAT_INPUT_BUFFER_SIZE == 20):
* bits 0-7 in _1, 8-15 in _2, 16-19 in the low nibble of _3. */
if (slots[i].unlocked_cheats_1 != 0xFF ||
slots[i].unlocked_cheats_2 != 0xFF ||
(slots[i].unlocked_cheats_3 & 0x0F) != 0x0F) {
slots[i].unlocked_cheats_1 = 0xFF;
slots[i].unlocked_cheats_2 = 0xFF;
slots[i].unlocked_cheats_3 |= 0x0F;
changed = 1;
}
/* Fully-completed campaign: fill empty completion times with the
* max (0x3FF). Needed so mission select still works while any cheat
* is active (see header comment); player records are preserved. */
for (s32 diff = 0; diff < 3; diff++) {
for (s32 lvl = 0; lvl < 20; lvl++) {
if (geSaveGetTime(slots[i].times, diff, lvl) == 0) {
geSaveSetTime(slots[i].times, diff, lvl, 0x3FF);
changed = 1;
}
}
}
}
if (!changed) return;
for (int i = 0; i < 5; i++)
fileGenerateCRC(&slots[i].completion_bitflags, (u8 *)&slots[i + 1],
&slots[i]);
memcpy(buf, slots, sizeof(ge_save_slot) * 5);
}
static s32 geEepromRW(u8 block, u8 *buf, int nbytes, int write) static s32 geEepromRW(u8 block, u8 *buf, int nbytes, int write)
{ {
u32 off = (u32)block * 8; u32 off = (u32)block * 8;
@@ -1198,6 +1318,11 @@ static s32 geEepromRW(u8 block, u8 *buf, int nbytes, int write)
geEepromStore(); geEepromStore();
} else { } else {
memcpy(buf, s_eeprom + off, nbytes); memcpy(buf, s_eeprom + off, nbytes);
/* fileValidateSaves' block read: five save slots from block 4. */
if (portAllUnlocked && block == 4 &&
nbytes == (int)(sizeof(ge_save_slot) * 5)) {
geEepromPatchAllCheats(buf);
}
} }
return 0; return 0;
} }
+27
View File
@@ -126,6 +126,33 @@ int main(int argc, char **argv)
configLoad(); configLoad();
atexit(portAtExit); /* persist config + window geometry on clean exit */ atexit(portAtExit); /* persist config + window geometry on clean exit */
/* 1a. D257: Game.AllUnlocked (default ON) -- seed the game's own RAM
* unlock flags so mission select offers every solo level at every
* difficulty plus 007 mode, with no save data required. Both are
* plain s32 globals in src/game/debugmenu_handler.c (compiled because
* the PC build defines LEFTOVERDEBUG); file2.c's
* fileIsStageUnlockedAtDifficulty() and front.c's 007-mode gate OR
* them in ahead of the EEPROM completion bits. Port-layer memory
* writes only -- no game-logic edits (AGENTS rule 2), same class as
* the existing GE_UNLOCK_ALL getenv hook in the getter. No active
* cheats (invincibility / all guns) are enabled; weapons remain
* per-mission pickups as on the N64. Separately, the eep shim
* (libultra.c geEepromPatchAllCheats) sets every progression-gated
* cheat-unlock bit in the save block at read time (per-slot CRC
* recomputed via the game's own fileGenerateCRC), so the cheat
* menu is fully populated without completed levels. */
{
extern s32 portAllUnlocked; /* port/src/video.c */
extern s32 debug_enable_all_levels_flag; /* src/game/debugmenu_handler.c */
extern s32 debug_007_unlock_flag; /* ditto */
if (portAllUnlocked) {
debug_enable_all_levels_flag = 1;
debug_007_unlock_flag = 1;
sysLogPrintf(LOG_INFO, "all-unlocked: RAM unlock flags seeded "
"(Game.AllUnlocked=1)");
}
}
/* 2. Load the ROM and map segments. */ /* 2. Load the ROM and map segments. */
if (romdataInit() != 0) { if (romdataInit() != 0) {
sysLogPrintf(LOG_ERROR, "Failed to load ROM (expected a .z64 in the " sysLogPrintf(LOG_ERROR, "Failed to load ROM (expected a .z64 in the "
+3
View File
@@ -130,6 +130,9 @@ static struct Row rows[] = {
/* D216: read once at title-stage load (lv.c) -- toggling it takes effect /* D216: read once at title-stage load (lv.c) -- toggling it takes effect
* the next time the title screen is reached. */ * the next time the title screen is reached. */
{ "Game.SkipIntro", "Skip intro", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, { "Game.SkipIntro", "Skip intro", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
/* D257: everything-unlocked goodie (default ON). Consumed at startup by
* main.c -- applies from the next launch. */
{ "Game.AllUnlocked", "All unlocked", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
}; };
#define NUM_ROWS ((int)(sizeof(rows) / sizeof(rows[0]))) #define NUM_ROWS ((int)(sizeof(rows) / sizeof(rows[0])))
+10
View File
@@ -95,6 +95,15 @@ s32 portSkipIntro = 0;
* original damage flash. */ * original damage flash. */
s32 portNoHitFlash = 0; s32 portNoHitFlash = 0;
/* D257: Game.AllUnlocked — ship-with-everything-unlocked goodie, ON by
* default for this cut. Consumed once at startup by main.c, which sets the
* game's own RAM unlock flags (debug_enable_all_levels_flag /
* debug_007_unlock_flag in src/game/debugmenu_handler.c, live because the PC
* build defines LEFTOVERDEBUG) — port-layer memory writes only, no game-code
* edits. Takes effect at launch; toggling it mid-session applies next run.
* 0 = faithful N64 progression (levels unlock as you complete them). */
s32 portAllUnlocked = 1;
/* D211: Video.FovScale as a multiplier on the render FOV. Applied game-side /* D211: Video.FovScale as a multiplier on the render FOV. Applied game-side
* at the guPerspectiveF chokepoint (src/fr.c) so it lands BEFORE the CPU * at the guPerspectiveF chokepoint (src/fr.c) so it lands BEFORE the CPU
* pre-multiplies projection x view into the combined world matrix — the * pre-multiplies projection x view into the combined world matrix — the
@@ -200,6 +209,7 @@ PD_CONSTRUCTOR static void videoConfigInit(void)
configRegisterFloat("Game.ScreenShakeIntensity", &portScreenShakeScale, 0.0f, 10.0f); configRegisterFloat("Game.ScreenShakeIntensity", &portScreenShakeScale, 0.0f, 10.0f);
configRegisterInt("Game.SkipIntro", &portSkipIntro, 0, 1); configRegisterInt("Game.SkipIntro", &portSkipIntro, 0, 1);
configRegisterInt("Game.NoHitFlash", &portNoHitFlash, 0, 1); configRegisterInt("Game.NoHitFlash", &portNoHitFlash, 0, 1);
configRegisterInt("Game.AllUnlocked", &portAllUnlocked, 0, 1);
configRegisterInt("Video.VSync", &cfgVSync, 0, 1); configRegisterInt("Video.VSync", &cfgVSync, 0, 1);
configRegisterInt("Video.FpsCap", &cfgFpsCap, 0, 1000); configRegisterInt("Video.FpsCap", &cfgFpsCap, 0, 1000);
configRegisterInt("Video.MSAA", &cfgMSAA, 1, 8); configRegisterInt("Video.MSAA", &cfgMSAA, 1, 8);