diff --git a/README.md b/README.md index 4ae0ea50..b5f7be78 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/runtime/src/host_keymap.c b/runtime/src/host_keymap.c index 36997456..7202dd4b 100644 --- a/runtime/src/host_keymap.c +++ b/runtime/src/host_keymap.c @@ -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(); diff --git a/runtime/tests/test_rewind_toggle_combo.py b/runtime/tests/test_rewind_toggle_combo.py index ac937f0b..2ea4b422 100644 --- a/runtime/tests/test_rewind_toggle_combo.py +++ b/runtime/tests/test_rewind_toggle_combo.py @@ -75,5 +75,11 @@ 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")