host_keymap: a present-but-empty / "None" [KeyMap] line is an explicit unbind

host_keymap_load zeroed an action's binds when its line was present and then
apply_defaults() put the built-in key straight back, so "Turbo = None" (or
an empty value) silently re-armed Tab. Track explicit_unbound per action:
set when the line is present but yields no binds, and honoured by
apply_defaults through want_default(). A missing line still receives the
default, so existing config.ini files are unaffected.

Pairs with the launcher's Backspace-to-clear (recomp-ui
feat/additional-ui-functionality), which writes "None" for that reason.
README documents the rule; test_rewind_toggle_combo.py guards it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin
2026-09-04 07:01:14 -04:00
parent 15822694c6
commit 975f6646ef
3 changed files with 33 additions and 11 deletions
+4
View File
@@ -746,6 +746,10 @@ acceleration is **opt-in**, per game, so the accurate path is never compromised:
speed until pressed again. Controller host shortcuts bound to a single
button or trigger are chords with Select — the launcher shows them as
`select + …`; only a two-button capture replaces the implicit Select.
Backspace while the launcher is listening clears a shortcut; in the files
that is `0` for a `[hotkeys]` pad value and `None` for a `[KeyMap]` key (a
present-but-empty or `None` line is an explicit unbind, a missing line
keeps the built-in default).
- **The "Fast Loading (host pacing)" and "CD Speed" mods** — automatic
acceleration during load waits, shipped with every title and **off by
default**, with `turbo_audio_sink` keeping the SPU timeline coherent through
+23 -11
View File
@@ -25,10 +25,19 @@ typedef struct HostKeyBind {
typedef struct HostKeyAction {
HostKeyBind binds[HOST_KEYMAP_MAX_BINDS];
int count;
/* 1 when [KeyMap] names this action with an empty / "None" / "(unbound)"
* value: the user cleared it, so apply_defaults must NOT put the built-in
* key back. A MISSING line still gets the default. */
int explicit_unbound;
} HostKeyAction;
static HostKeyAction s_actions[HOST_KEYMAP_ACTION_COUNT];
static int want_default(HostKeymapAction action) {
return s_actions[action].count == 0 && !s_actions[action].explicit_unbound;
}
static int ieq(const char *a, const char *b) {
if (!a || !b) return 0;
while (*a && *b) {
@@ -79,27 +88,27 @@ static void add_bind(HostKeymapAction action, int keycode, int scancode, int mod
}
static void apply_defaults(void) {
if (s_actions[HOST_KEYMAP_FULLSCREEN].count == 0) {
if (want_default(HOST_KEYMAP_FULLSCREEN)) {
add_bind(HOST_KEYMAP_FULLSCREEN, (int)SDLK_RETURN, (int)SDL_SCANCODE_RETURN, KMOD_ALT);
add_bind(HOST_KEYMAP_FULLSCREEN, (int)SDLK_f, (int)SDL_SCANCODE_F, KMOD_CTRL);
}
if (s_actions[HOST_KEYMAP_TURBO].count == 0)
if (want_default(HOST_KEYMAP_TURBO))
add_bind(HOST_KEYMAP_TURBO, (int)SDLK_TAB, (int)SDL_SCANCODE_TAB, 0);
if (s_actions[HOST_KEYMAP_VOLUME_UP].count == 0)
if (want_default(HOST_KEYMAP_VOLUME_UP))
add_bind(HOST_KEYMAP_VOLUME_UP, (int)SDLK_KP_PLUS, (int)SDL_SCANCODE_KP_PLUS, 0);
if (s_actions[HOST_KEYMAP_VOLUME_DOWN].count == 0)
if (want_default(HOST_KEYMAP_VOLUME_DOWN))
add_bind(HOST_KEYMAP_VOLUME_DOWN, (int)SDLK_KP_MINUS, (int)SDL_SCANCODE_KP_MINUS, 0);
if (s_actions[HOST_KEYMAP_DISPLAY_PERF].count == 0)
if (want_default(HOST_KEYMAP_DISPLAY_PERF))
add_bind(HOST_KEYMAP_DISPLAY_PERF, (int)SDLK_f, (int)SDL_SCANCODE_F, 0);
#if defined(PSX_HAS_RBENGINE_SNAP)
if (s_actions[HOST_KEYMAP_REWIND].count == 0)
if (want_default(HOST_KEYMAP_REWIND))
add_bind(HOST_KEYMAP_REWIND, (int)SDLK_F8, (int)SDL_SCANCODE_F8, 0);
#endif
if (s_actions[HOST_KEYMAP_SAVE_STATE_MENU].count == 0)
if (want_default(HOST_KEYMAP_SAVE_STATE_MENU))
add_bind(HOST_KEYMAP_SAVE_STATE_MENU, (int)SDLK_F7, (int)SDL_SCANCODE_F7, 0);
if (s_actions[HOST_KEYMAP_SCANLINES].count == 0)
if (want_default(HOST_KEYMAP_SCANLINES))
add_bind(HOST_KEYMAP_SCANLINES, (int)SDLK_F6, (int)SDL_SCANCODE_F6, 0);
if (s_actions[HOST_KEYMAP_TURBO_TOGGLE].count == 0)
if (want_default(HOST_KEYMAP_TURBO_TOGGLE))
add_bind(HOST_KEYMAP_TURBO_TOGGLE, (int)SDLK_F9, (int)SDL_SCANCODE_F9, 0);
}
@@ -202,10 +211,13 @@ void host_keymap_load(const char *config_ini_path) {
trim_inplace(val);
act = action_for_key(key);
if (act == HOST_KEYMAP_ACTION_COUNT) continue;
/* Explicit empty unbinds (no keypad fallback for that action until
* apply_defaults — empty means user cleared it; still fall back). */
/* Explicit empty / "None" unbinds: the user cleared it, so no
* default fallback for that action (explicit_unbound). */
s_actions[act].count = 0;
s_actions[act].explicit_unbound = 0;
parse_value(act, val);
if (s_actions[act].count == 0)
s_actions[act].explicit_unbound = 1;
}
fclose(f);
apply_defaults();
@@ -75,5 +75,11 @@ KEYMAP_C = (ROOT / "runtime" / "src" / "host_keymap.c").read_text(encoding="utf-
assert "HOST_KEYMAP_TURBO_TOGGLE," in KEYMAP_H
assert 'ieq(name, "TurboToggle")' in KEYMAP_C
assert "add_bind(HOST_KEYMAP_TURBO_TOGGLE, (int)SDLK_F9" in KEYMAP_C
# A present-but-empty / "None" [KeyMap] line is an explicit unbind: no
# built-in default is re-applied for that action (launcher Backspace-to-clear).
assert "int explicit_unbound;" in KEYMAP_C
assert "static int want_default(HostKeymapAction action)" in KEYMAP_C
assert "s_actions[HOST_KEYMAP_TURBO].count == 0" not in KEYMAP_C
assert "if (want_default(HOST_KEYMAP_TURBO))" in KEYMAP_C
print("host shortcut guard passed")