Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ record it again, and make a new tour list. The folder's layout stays the same.

`hookshot_options.ini`, in the game folder, holds the Insert menu's rows as `<key>=<number>`: the row's name in lower
case with `_` for spaces (`frame_generation=2`, `invert_camera_x=1`), except Output resolution, which is `output_res`.
`gpu/vk/vk_menu.cpp` writes the whole file at every change (`save`), with `options_version=4`,
`gpu/vk/vk_menu.cpp` writes the whole file at every change (`save`), with `options_version=5`,
`microphone_device=<name>` when a microphone is chosen by name, and the controls (`pad_<button>` and `key_<button>`:
the README's [Controls](../readme.md#controls)). A value outside a row's range is clamped when it is read. Where the
number is not the obvious one:
Expand Down
14 changes: 10 additions & 4 deletions gpu/vk/vk_menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1426,8 +1426,9 @@ void save() {
// start must not take back to the preset's)
fprintf(f, "memory_budget=%d\n", opts.budget);
// (2: game_speed below 2 was only ever the old default, see menu_init; 3: the mouse's buttons are bindings, see
// host/nn_hid.c binds_load; 4: Descriptor reuse on by default, see menu_init and vk_draw.cpp desc_slotgen_on)
fprintf(f, "options_version=4\n");
// host/nn_hid.c binds_load; 4: Descriptor reuse on by default, see menu_init and vk_draw.cpp desc_slotgen_on; 5: the
// Menu shortcut "Insert, F1 or Esc" by default, see menu_init)
fprintf(f, "options_version=5\n");
fputs(others.c_str(), f);
if (!can_bind()) fputs(kept.c_str(), f);
for (int i = 0; i < HOS_KEY_N && can_bind(); i++) { // (the keys-only controls, HOS_KEY_*, have no pad_ line)
Expand Down Expand Up @@ -3111,6 +3112,10 @@ void menu_init() {
// (Descriptor reuse is on by default from options_version 4: a file from before wrote its 0 whether chosen or not, as
// the off default it then was; fewer stutters and a better 1% low in the field, the flash checks clean, 2026-11-10)
if (version < 4) opts.descriptor_reuse = 1;
// (Esc opens the menu by default from options_version 5, as a PC game's pause menu: a file from before wrote its
// Menu shortcut 0, Insert or F1, whether chosen or not, and moves to Insert, F1 or Esc, where Insert and F1 still
// work. F1 only was chosen: kept)
if (version < 5 && opts.menu_shortcut == 0) opts.menu_shortcut = 2;
}
first_start = !has_options;
// An opt-in profile for 4 GB cards: keep existing options authoritative, but make a first run usable on
Expand Down Expand Up @@ -3576,8 +3581,9 @@ void menu_tick() {
restart_state.choose_later(now);
open_now = false, page = 0, tune_mode = false, changed = true;
};
// (Menu shortcut "Insert, F1 or Esc": Esc on a closed menu opens it, the keyboard's alone (the
// gamepad's back is Esc only while it is open, above). The press that opened it does not go back a page below. No
// (Menu shortcut "Insert, F1 or Esc", the default: Esc on a closed menu opens it, the keyboard's and RX_VK_MENU_SCRIPT's
// ESC alone (the gamepad's back is Esc only while it is open, above). The press that opened it does not go back a page
// below; with the compact menu key held it opens the compact bar, as Insert and F1 do (tune_mode). No
// control can be bound to Esc, so nothing in play changes; the window gives the captured mouse back on the close)
const bool esc_opens = opts.menu_shortcut == 2 && press[5] && !open_now.load();
if (esc_opens) press[5] = false;
Expand Down
3 changes: 2 additions & 1 deletion gpu/vk/vk_menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ struct Options {
// blurred, 2 with black bars (nx2/oot_wide.py, vk_framegen.cpp)
int show_fps = 0; // (hookshot) the FPS counter: 0 Off, 1 FPS, 2 Frame time, 3 Graph, 4 Breakdown (fps_stats.h; saved as fps_counter=, an old On is 1)
int fps_corner = 0; // (hookshot) its corner: 0 top right, 1 top left, 2 bottom right, 3 bottom left
int menu_shortcut = 0; // 0 Insert or F1, 1 F1 only, 2 Insert, F1 or Esc (F1 always opens the menu)
int menu_shortcut = 2; // 0 Insert or F1, 1 F1 only, 2 Insert, F1 or Esc (F1 always opens the menu). 2 by default, as a PC
// game's pause menu; an older file's 0, the default it then was, moves to 2 (options_version 5)
int compact_menu_key = 0; // 0 Right Shift, 1 Right Ctrl (key to switch to or open the compact menu)
int menu_size = 2; // (hookshot) the overlays' size: 1 Small, 2 Medium, 3 Large (gpu/vk/vk_ui.h unit; an older file's 0 Auto: Medium)
int fullscreen = 0; // borderless, at the monitor's size: F11 is the same setting
Expand Down
13 changes: 8 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,13 @@ use and cause stalls when the card runs out of headroom. To undo it, turn the ro

## In-game menu

By default, press <kbd>Insert</kbd> or <kbd>F1</kbd> in game. On a controller, press Home or Select and Start together; on
a DualShock 4 or DualSense, clicking the touchpad does the same as <kbd>F1</kbd>.
The same keys close it, and so does <kbd>Esc</kbd>. <kbd>F1</kbd> always works; **Controls → Menu shortcut** can leave
<kbd>Insert</kbd> free for another overlay. The menu has five tabs, Graphics, Audio, Gameplay, Controls and Debug:
By default, press <kbd>Insert</kbd>, <kbd>F1</kbd> or <kbd>Esc</kbd> in game. On a controller, press Home or Select and
Start together; on a DualShock 4 or DualSense, clicking the touchpad does the same as <kbd>F1</kbd>.
The same keys close it; with every Menu shortcut, <kbd>Esc</kbd> goes back a page or closes the open menu.
<kbd>F1</kbd> always works. **Controls → Menu shortcut** can leave <kbd>Insert</kbd> free for another overlay (F1 only).
With Insert or F1, or with F1 only, <kbd>Esc</kbd> does not open the menu: it only lets the mouse go. Options saved by
an older version on Insert or F1, the default then, move to Insert, F1 or Esc; F1 only stays.
The menu has five tabs, Graphics, Audio, Gameplay, Controls and Debug:
<kbd>Q</kbd> and <kbd>E</kbd> (or <kbd>Tab</kbd> and <kbd>Shift</kbd>+<kbd>Tab</kbd>) switch between them, LB and RB on
a controller (or the triggers). <kbd>Up</kbd> and <kbd>Down</kbd> choose a row, <kbd>Left</kbd> and <kbd>Right</kbd>
change it (<kbd>Enter</kbd> changes it too); beside the rows, the help says what the row is and, on a line of its own,
Expand Down Expand Up @@ -325,7 +328,7 @@ HDR peak brightness show only while HDR is on.
| | Layout preset | Nintendo (Def), Xbox / PC, Swap Face, Custom | Nintendo (Def) |
| | Motion | Opens the Motion page: Motion controls (Off, On; On), Motion sensitivity X (10% to 600%) and Y (10% to 300%; 100%), Invert motion X and Y (Off, On; Off), Motion deadzone (Low, Standard, High; Standard) | |
| | Controls | Opens the Controls page | |
| | Menu shortcut | Insert or F1, F1 only | Insert or F1 |
| | Menu shortcut | Insert or F1; F1 only; Insert, F1 or Esc | Insert, F1 or Esc |
| Debug | Debug tools | Off, On (the tools below it while on) | Off |
| | Developer: Descriptor reuse | Off, On (after a restart) | On |
| | Developer: Sampler cache | Off, On (after a restart) | Off |
Expand Down
156 changes: 148 additions & 8 deletions tests/test_menu_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
link answers to confirm (Enter, A, a click) and back (Esc, B): Motion, Controls, and the debug tools' rows on the Debug tab,
whose Teleport and Inventory open the map list and the inventory, which go back to the tab. The prompts light the same buttons.
The real keyboard polling block is also compiled to exercise both shortcut keys with queued presses and with polling.
With it, menu_tick's Esc block, gpu/vk/vk_menu.h's real Options and the Menu shortcut row's names: out of the box (no
options file, or one without menu_shortcut=) the shortcut is "Insert, F1 or Esc", and Esc opens a closed menu, as a PC
game's pause menu, without that press also going back a page; open, the press is left to go back or close it. And
menu_init's reading of hookshot_options.ini: a file from before options_version 5 wrote Insert or F1, the default then,
whether chosen or not, and loads as Insert, F1 or Esc; F1 only stays, and a file as save() writes it now keeps each choice.
python -I -B tests/test_menu_keys.py [--cxx g++]
"""
Expand All @@ -22,9 +27,14 @@
ROOT = Path(__file__).resolve().parents[1]

FIXTURE = r'''#include "gpu/vk/vk_menu_keys.h"
#include <algorithm>
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cctype>
#include <initializer_list>
#include <string>
#define CHECK(x) do { if (!(x)) { std::fprintf(stderr, "FAIL:%d %s\n", __LINE__, #x); std::exit(1); } } while (0)
using namespace mkwvk::menu_keys;
static void expect(int a, int b, int confirm, int back, int line) {
Expand All @@ -36,8 +46,12 @@
}
}
// KEYBOARD_FIXTURE
int main() {
int main(int argc, char** argv) {
CHECK(argc == 2); // (the options file to write: in the test's own folder)
ini_file = argv[1];
test_keyboard();
test_esc();
test_saved();
expect(kEast, kSouth, kEast, kSouth, __LINE__); // (Nintendo, the default: A right, B bottom)
expect(kSouth, kEast, kSouth, kEast, __LINE__); // (Xbox / PC)
expect(kWest, kNorth, kWest, kNorth, __LINE__); // (bound by hand elsewhere on the face)
Expand Down Expand Up @@ -68,7 +82,8 @@
using DWORD = unsigned;
static int down_key, queued_key;
static bool focused, pad_last;
static struct { int menu_shortcut; } opts;
// OPTIONS
static Options opts;
static HWND GetForegroundWindow() { return 1; }
static DWORD GetCurrentProcessId() { return 1; }
static void GetWindowThreadProcessId(HWND, DWORD* pid) { *pid = focused ? 1 : 2; }
Expand Down Expand Up @@ -110,6 +125,111 @@
CHECK(keyboard_toggle(0, VK_INSERT, false));
std::puts("PASS: menu shortcuts respect the saved mode, focus and key edges");
}
// (menu_tick's Esc block, after the keys are read: what it reaches, stubbed)
static std::atomic<bool> open_now;
static int page, ask_sel, listening;
static bool changed, tune_mode; // (tune_mode: the compact bar, open; it opens when its key is held as the menu opens)
static bool compact_key_held; // (menu_tick's tune_down: Right Shift, or Right Ctrl, held)
static bool restart_ask() { return false; }
static int detect_pad_layout() { return 0; }
static void dlss_probe() {}
static void fsr4_probe() {}
static void refresh_mics() {}
// TX_MENU_SHORTCUT
// One tick with key pressed (0: none) and the options o, the menu open before it or not: whether it is open after. *back:
// the Esc press left for menu_tick's pages below (back a page, or close the menu)
static bool tick_key(const Options& o, int key, bool queued, bool open_before, bool* back) {
opts = o;
focused = true;
host = queued ? &queue_host : nullptr;
down_key = queued ? 0 : key;
queued_key = queued ? key : 0;
// KEY_POLL
open_now = open_before;
page = 0, listening = 0, changed = false, tune_mode = false;
const bool tune_down = compact_key_held;
auto later = [] {};
// ESC_BLOCK
CHECK(changed == (open_now.load() != open_before) && page == 0 && listening == 0);
// (a menu Esc opens is the full menu, or the compact bar with its key held, as with Insert and F1)
CHECK(tune_mode == (open_now.load() && !open_before && compact_key_held));
(void)ask_sel;
*back = press[5];
return open_now.load();
}
// Esc pressed and let go
static bool esc(const Options& o, bool queued, bool open_before, bool* back) {
const bool open = tick_key(o, VK_ESCAPE, queued, open_before, back);
bool none = true;
CHECK(tick_key(o, 0, queued, open, &none) == open && !none);
return open;
}
static void test_esc() {
const Options fresh{}; // (no options file, or none of its lines menu_shortcut=: the struct's defaults, menu_init)
CHECK(!std::strcmp(tx_menu_shortcut(fresh.menu_shortcut), "Insert, F1 or Esc"));
for (bool queued : {false, true}) {
bool back = false;
CHECK(esc(fresh, queued, false, &back) && !back); // (Esc opens a closed menu; that press does not go back too)
CHECK(esc(fresh, queued, true, &back) && back); // (open: the press goes back a page or closes it, further down)
for (int mode = 0; mode <= 2; mode++) { // (each choice as loaded: Esc opens the menu with 2 alone)
Options saved = fresh;
saved.menu_shortcut = mode;
CHECK(esc(saved, queued, false, &back) == (mode == 2) && back == (mode != 2));
CHECK(esc(saved, queued, true, &back) && back);
}
compact_key_held = true; // (the compact menu key held: Esc opens the compact bar, tick_key checks tune_mode)
CHECK(esc(fresh, queued, false, &back) && !back);
compact_key_held = false;
}
std::puts("PASS: out of the box, Esc opens the closed menu (Insert, F1 or Esc); Insert or F1 and F1 only leave it closed");
}
// (menu_init's reading of hookshot_options.ini, stubbed around: the Menu shortcut row as the only row)
static std::string ini_file;
static std::string ini_path() { return ini_file; }
static int parse_output(const char*) { return 0; }
static std::string mic_want;
static std::string mic_key(const char* name) { return name; }
static void mic_tell_host() {}
static int rumble_step(int v) { return v; }
struct Row { int tab; const char* name; int* value; int lo, hi; };
// INI_KEY
static Row rows[] = {{0, "Menu shortcut", &opts.menu_shortcut, 0, 2}};
// The Menu shortcut after menu_init reads a file with menu_shortcut=<shortcut> and options_version=<version> (-1: no line)
static int loaded_as(int shortcut, int version) {
if (FILE* w = fopen(ini_file.c_str(), "w")) {
fputs("output_res=2560x1440\n", w);
if (shortcut >= 0) fprintf(w, "menu_shortcut=%d\n", shortcut);
if (version >= 0) fprintf(w, "options_version=%d\n", version);
fclose(w);
}
opts = Options{};
mic_want.clear();
bool has_options = false, has_vram_budget = false, has_memory_budget = false;
// LOAD_OPTIONS
CHECK(has_options && !has_vram_budget && !has_memory_budget);
return opts.menu_shortcut;
}
static void test_saved() {
// (from before the version save() writes now (-1, no line: before 2): Insert or F1 was the default, written whether
// chosen or not, and moves to Insert, F1 or Esc; F1 only was chosen and stays; no line: the default. Every older
// version: a later bump of save()'s version must move menu_init's threshold with it)
for (int old = -1; old < SAVED_VERSION; old++) {
CHECK(loaded_as(0, old) == 2);
CHECK(loaded_as(1, old) == 1);
CHECK(loaded_as(2, old) == 2);
CHECK(loaded_as(-1, old) == 2);
}
// (a file as save() writes it now: each choice kept, Insert or F1 too, picked after the change)
for (int mode = 0; mode <= 2; mode++) CHECK(loaded_as(mode, SAVED_VERSION) == mode);
CHECK(loaded_as(-1, SAVED_VERSION) == 2);
std::puts("PASS: an older file's Insert or F1 moves to Insert, F1 or Esc; F1 only and a file saved now keep theirs");
}
'''


Expand All @@ -125,12 +245,35 @@ def main():
menu = (ROOT / "gpu/vk/vk_menu.cpp").read_text(encoding="utf-8")
tick = menu[menu.index("void menu_tick() {"):]
key_poll = tick[tick.index(" enum { K_N ="):tick.index(" // (hookshot) the gamepads")]
fixture = FIXTURE.replace("// KEYBOARD_FIXTURE", KEYBOARD.replace(" // KEY_POLL", key_poll))
esc_at = tick.index(" const bool esc_opens")
esc_block = tick[esc_at:tick.index(" if (open_now.load() && page == 5) {", esc_at)]
tx_at = menu.index("const char* tx_menu_shortcut(int v) {")
tx = menu[tx_at:menu.index("\n}\n", tx_at) + 3]
header = (ROOT / "gpu/vk/vk_menu.h").read_text(encoding="utf-8")
options = re.search(r"struct Options \{.*?\n\};", header, re.S)
if not options:
raise SystemExit("FAIL: vk_menu.h: the menu's Options are missing")
# (menu_init's reading of the options file, its moves of old defaults included, and the version save() writes now)
init = menu[menu.index("void menu_init() {"):]
load_at = init.index(' if (FILE* f = fopen(ini_path().c_str(), "r")) {')
load = init[load_at:init.index(" first_start = !has_options;", load_at)]
key_at = menu.index("const char* ini_key(const Row& r) {")
ini_key = menu[key_at:menu.index("\n}\n", key_at) + 3]
saved_version = re.search(r'fprintf\(f, "options_version=(\d+)\\n"\);', menu[menu.index("void save() {"):])
if not saved_version:
raise SystemExit("FAIL: vk_menu.cpp: save() does not write options_version")
keyboard = (KEYBOARD.replace("// OPTIONS", options.group(0)).replace("// TX_MENU_SHORTCUT", tx)
.replace(" // KEY_POLL", key_poll).replace(" // ESC_BLOCK", esc_block)
.replace("// INI_KEY", ini_key).replace(" // LOAD_OPTIONS", load)
.replace("SAVED_VERSION", saved_version.group(1)))
fixture = FIXTURE.replace("// KEYBOARD_FIXTURE", keyboard)
with tempfile.TemporaryDirectory(prefix="menu-keys-") as tmp:
cpp, exe = Path(tmp) / "fixture.cpp", Path(tmp) / "fixture"
cpp.write_text(fixture, encoding="utf-8")
subprocess.run([a.cxx, "-std=c++20", "-Wall", "-Wextra", "-Werror", "-I" + str(ROOT), str(cpp), "-o", str(exe)], check=True)
subprocess.run([str(exe)], check=True)
# (_CRT_SECURE_NO_WARNINGS: the MSVC CRT's fopen deprecation, as the game's own Windows build passes)
subprocess.run([a.cxx, "-std=c++20", "-Wall", "-Wextra", "-Werror", "-D_CRT_SECURE_NO_WARNINGS", "-I" + str(ROOT), str(cpp),
"-o", str(exe)], check=True)
subprocess.run([str(exe), str(Path(tmp) / "hookshot_options.ini")], check=True)

need(menu, r'\{TAB_CONTROLS, "Menu shortcut", &opts\.menu_shortcut, 0, 2, 1, tx_menu_shortcut,[^}]*nullptr, false, true\}',
"the shortcut choice must be saved in Controls and exempt from presets")
Expand All @@ -139,9 +282,6 @@ def main():
r"if \(esc_opens\) press\[5\] = false;", "Esc does not open a closed menu with the Esc shortcut")
need(tick, r"\} else if \(press\[0\] \|\| press\[6\] \|\| esc_opens\) \{\s*open_now = !open_now\.load\(\)",
"the Esc shortcut's press does not open the menu")
header = (ROOT / "gpu/vk/vk_menu.h").read_text(encoding="utf-8")
if "int menu_shortcut = 0;" not in header:
raise SystemExit("FAIL: existing options must keep both shortcuts")
# (the pad: confirm and back from A's and B's bindings, every tick, not fixed positions)
need(tick, r"menu_keys::confirm_back\(host->binding\(HOS_BIND_PAD, HOS_SW_A, 0\), host->binding\(HOS_BIND_PAD, HOS_SW_B, 0\)",
"confirm and back are not taken from A's and B's bindings")
Expand Down
Loading