feat(menu): skip the title screen and load the newest save #1560

Open
nicholasgower wants to merge 1 commit from nicholasgower/feat/skip-title-screen into main
nicholasgower commented 2026-11-10 08:12:11 +00:00 (Migrated from github.com)

Why

On PC every launch starts at the intro and title, even when the player is carrying on from a save. This adds a Gameplay option that presses through to the game's save instead.

What

  • Adds Gameplay ? Skip title screen (Off by default), saved as skip_title_screen, and exposes it to the host as HOS_OPT_SKIP_TITLE.
  • host/oot_autoload.c looks for the newest progress.sav under user/save/<profile>/slot_* and, once the intro/title is up, sends A presses through the title and its file list.
  • The presses stop as soon as Link is running, the Insert menu is open, the option is turned off, or 120 s have passed.
  • With no save yet nothing is pressed, so a first launch still shows the title and can start a new game.
  • Adds a host tap helper in host/nn_hid.c and a player-seen signal in host/hos_main.c.

Tests

  • python -I -B tests/test_skip_title.py --cc gcc
  • python -I -B tests/test_input_background.py --cc gcc
  • Menu checks: test_rumble_menu, test_menu_restart, test_menu_keys, test_menu_hit, test_menu_utf8, test_stick_dead_zone, test_text_speed, test_audio_output_menu, test_output_resolution, test_startup_progress, test_game_font.

Not run in the game here because this machine has no game dump; the title/load transition still needs a local play test. For the same reason there is no screenshot of the new row.

## Why On PC every launch starts at the intro and title, even when the player is carrying on from a save. This adds a Gameplay option that presses through to the game's save instead. ## What - Adds **Gameplay ? Skip title screen** (Off by default), saved as `skip_title_screen`, and exposes it to the host as `HOS_OPT_SKIP_TITLE`. - `host/oot_autoload.c` looks for the newest `progress.sav` under `user/save/<profile>/slot_*` and, once the intro/title is up, sends A presses through the title and its file list. - The presses stop as soon as Link is running, the Insert menu is open, the option is turned off, or 120 s have passed. - With no save yet nothing is pressed, so a first launch still shows the title and can start a new game. - Adds a host tap helper in `host/nn_hid.c` and a player-seen signal in `host/hos_main.c`. ## Tests - `python -I -B tests/test_skip_title.py --cc gcc` - `python -I -B tests/test_input_background.py --cc gcc` - Menu checks: `test_rumble_menu`, `test_menu_restart`, `test_menu_keys`, `test_menu_hit`, `test_menu_utf8`, `test_stick_dead_zone`, `test_text_speed`, `test_audio_output_menu`, `test_output_resolution`, `test_startup_progress`, `test_game_font`. Not run in the game here because this machine has no game dump; the title/load transition still needs a local play test. For the same reason there is no screenshot of the new row.
hckrman101 (Migrated from github.com) approved these changes 2026-11-10 16:39:50 +00:00
hckrman101 (Migrated from github.com) left a comment

Looks good. I read through the new host/oot_autoload.c and checked it against main; I did not run it in game, and the PR says the same about the title-to-load transition, so that still needs a local play test.

Things I checked: option id 44 is free (40-43 are the HDR and Discord ids), the ini key comes out as skip_title_screen from the row name like the other rows, hos_user_dir and the save/<profile>/slot_*/progress.sav layout match nn_fs.c and docs/saves.md, hos_oot_lift_player is patched in unconditionally (not only with the debug tools on), so hos_oot_player_seen() really does stop the presses once Link is running, and the new test is registered in run_all.py. CI is green on Linux and Windows.

Nits, none blocking:

  • The presses go in through the same press table as the input script and live input, so they also reach the game with its window in the background. Probably what you want for an automatic load, just worth knowing.
  • The 20 s first-press delay is fixed. On a slow shader-cache start the first presses land on the intro, which is harmless, but a very fast machine waits longer than it needs to. Fine for now.
Looks good. I read through the new host/oot_autoload.c and checked it against main; I did not run it in game, and the PR says the same about the title-to-load transition, so that still needs a local play test. Things I checked: option id 44 is free (40-43 are the HDR and Discord ids), the ini key comes out as `skip_title_screen` from the row name like the other rows, `hos_user_dir` and the `save/<profile>/slot_*/progress.sav` layout match nn_fs.c and docs/saves.md, `hos_oot_lift_player` is patched in unconditionally (not only with the debug tools on), so `hos_oot_player_seen()` really does stop the presses once Link is running, and the new test is registered in run_all.py. CI is green on Linux and Windows. Nits, none blocking: - The presses go in through the same press table as the input script and live input, so they also reach the game with its window in the background. Probably what you want for an automatic load, just worth knowing. - The 20 s first-press delay is fixed. On a slow shader-cache start the first presses land on the intro, which is harmless, but a very fast machine waits longer than it needs to. Fine for now.
hckrman101 (Migrated from github.com) requested changes 2026-11-10 16:49:43 +00:00
hckrman101 (Migrated from github.com) left a comment

Let's discuss approach for this in Discord, we'll circle back around to this

Let's discuss approach for this in Discord, we'll circle back around to this
hckrman101 commented 2026-11-10 17:49:38 +00:00 (Migrated from github.com)

Heads-up: this conflicts with main now (a lot landed today). Could you merge main into the branch (or rebase) and resolve the conflicts? Thanks!

Heads-up: this conflicts with main now (a lot landed today). Could you merge main into the branch (or rebase) and resolve the conflicts? Thanks!
mooki000 commented 2026-11-11 13:55:23 +00:00 (Migrated from github.com)

I rebased this onto current main on Linux to try it (head 1fc9986f onto 49f75dc8). Two things that may save you time; I have not run it in the game yet and will add what Linux with an RTX 4090 shows when I have.

1. After the conflicts are resolved it still does not compile on main. main got a hos_hid_tap of its own in a93b8694 (2026-11-09, "Input: presses, a stick and a drag from the host", for the game menus' mouse; host/nn_hid.c:443 on main). That commit is not in this branch's base, git reports no conflict there, and the two definitions meet only in the compiler:

host/nn_hid.c:484:6: error: redefinition of 'hos_hid_tap'
  484 | void hos_hid_tap(const char *presses) {
host/nn_hid.c:443:6: note: previous definition is here
  443 | void hos_hid_tap(const char *press) {

main's function takes the same text (presses in the input script's syntax, timed from now; the next controller read takes them), so host/oot_autoload.c can call it as it is. Suggested fix: drop this pull request's copy from host/nn_hid.c and keep only the call to hos_oot_autoload_input() in read_input:

-/* (hookshot) A press the host adds while the game runs, in the input script's syntax, timed from now (the Insert menu's Skip
-   title screen row, host/oot_autoload.c). It joins the script's presses, so it holds for its /<ms> and is read like one. */
-void hos_hid_tap(const char *presses) {
-    if (npresses < 0) parse_script();
-    parse_presses(presses, script_elapsed_ms(hos_now_ns()), 1);
-}
-

With that removed the host and the renderer compile and the game links here (clang 23, Linux; Windows not built), and tests/test_skip_title.py --cc gcc and tests/test_input_background.py --cc gcc pass.

2. The five conflicts, and how I resolved each (in case it helps the rebase):

  • gpu/vk/vk_api.h: main now has HOS_OPT_DISABLE_CAMERA_CENTERING = 45 in an enum of its own, with a comment that 44 is reserved for HOS_OPT_SKIP_TITLE. Your enum line with HOS_OPT_SKIP_TITLE after HOS_OPT_DISCORD still gives 44; the "44 is reserved" words can go.
  • gpu/vk/vk_menu.cpp: the row goes after main's new Text size row. The Audio output row in your hunk's context has moved to the Audio tab on main, so it must not come back on the Gameplay tab.
  • gpu/vk/vk_replay.cpp: the HOS_OPT_SKIP_TITLE answer beside main's new ones (volume, mute in background, camera centering, text size).
  • readme.md: the table row after Text size, the bullet after Menu size (whose text changed on main).
  • tests/run_all.py: since 0876097b (#1822) a test names its own arguments, so the ARGS entry becomes RUN_ALL_ARGS = ["--cc", "gcc"] at the top of tests/test_skip_title.py.

The rebased branch with that fix is pr1560-rebased in my clone; I can push it to my fork if you want to take it from there.

I rebased this onto current `main` on Linux to try it (head `1fc9986f` onto `49f75dc8`). Two things that may save you time; I have not run it in the game yet and will add what Linux with an RTX 4090 shows when I have. **1. After the conflicts are resolved it still does not compile on `main`.** `main` got a `hos_hid_tap` of its own in `a93b8694` (2026-11-09, "Input: presses, a stick and a drag from the host", for the game menus' mouse; `host/nn_hid.c:443` on `main`). That commit is not in this branch's base, git reports no conflict there, and the two definitions meet only in the compiler: ``` host/nn_hid.c:484:6: error: redefinition of 'hos_hid_tap' 484 | void hos_hid_tap(const char *presses) { host/nn_hid.c:443:6: note: previous definition is here 443 | void hos_hid_tap(const char *press) { ``` `main`'s function takes the same text (presses in the input script's syntax, timed from now; the next controller read takes them), so `host/oot_autoload.c` can call it as it is. Suggested fix: drop this pull request's copy from `host/nn_hid.c` and keep only the call to `hos_oot_autoload_input()` in `read_input`: ```diff -/* (hookshot) A press the host adds while the game runs, in the input script's syntax, timed from now (the Insert menu's Skip - title screen row, host/oot_autoload.c). It joins the script's presses, so it holds for its /<ms> and is read like one. */ -void hos_hid_tap(const char *presses) { - if (npresses < 0) parse_script(); - parse_presses(presses, script_elapsed_ms(hos_now_ns()), 1); -} - ``` With that removed the host and the renderer compile and the game links here (clang 23, Linux; Windows not built), and `tests/test_skip_title.py --cc gcc` and `tests/test_input_background.py --cc gcc` pass. **2. The five conflicts, and how I resolved each** (in case it helps the rebase): - `gpu/vk/vk_api.h`: `main` now has `HOS_OPT_DISABLE_CAMERA_CENTERING = 45` in an enum of its own, with a comment that 44 is reserved for `HOS_OPT_SKIP_TITLE`. Your enum line with `HOS_OPT_SKIP_TITLE` after `HOS_OPT_DISCORD` still gives 44; the "44 is reserved" words can go. - `gpu/vk/vk_menu.cpp`: the row goes after `main`'s new Text size row. The `Audio output` row in your hunk's context has moved to the Audio tab on `main`, so it must not come back on the Gameplay tab. - `gpu/vk/vk_replay.cpp`: the `HOS_OPT_SKIP_TITLE` answer beside `main`'s new ones (volume, mute in background, camera centering, text size). - `readme.md`: the table row after Text size, the bullet after Menu size (whose text changed on `main`). - `tests/run_all.py`: since `0876097b` (#1822) a test names its own arguments, so the `ARGS` entry becomes `RUN_ALL_ARGS = ["--cc", "gcc"]` at the top of `tests/test_skip_title.py`. The rebased branch with that fix is `pr1560-rebased` in my clone; I can push it to my fork if you want to take it from there.
hckrman101 commented 2026-11-11 15:51:03 +00:00 (Migrated from github.com)

Heads-up: this conflicts with main now. Could you rebase onto main and resolve the conflicts? Thanks!

Heads-up: this conflicts with main now. Could you rebase onto main and resolve the conflicts? Thanks!
mooki000 commented 2026-11-11 18:24:44 +00:00 (Migrated from github.com)

Here is the Linux game test promised above. Tested build: your 1fc9986f locally rebased onto 49f75dc8, with the duplicate hos_hid_tap removed as described in my earlier comment: fb56bc33 (v2.1.0-rc.3-55-gfb56bc33). This is not a run of current main or Windows. Arch Linux, RTX 4090, NVIDIA 615.78.08, headless, skip_title_screen=1; no manual load presses.

Case Observed result
Existing intact save Reached play. The log says skip title: loaded, the player is running; the picture shows Link in the world with the HUD.
No saves Remained at the title with New Game. The log says skip title: no saved game yet: the title screen stays.
Newest save deliberately cut in half, older slot left intact The game opened the cut slot_01/progress.sav, then crashed with SIGSEGV. It did not open the older slot_00/progress.sav before the crash.

Valid save reaches play; no-save stays at New Game; truncated newest save's last captured loading frame

The negative test altered only a disposable copy, not an original save. The last panel is the final captured loading frame, not a screenshot of the crash. The log then records:

fs: OpenFile(save:/slot_01/progress.sav)
CRASH: exception 0xc0000005 (write 0xfffffffffff, guest 0xffffffffffffffff)
CRASH: thread ModuleSystemWorker4

The process exited 139; coredumpctl confirms SIGSEGV. No device-loss or kernel driver-fault line was reported. The harness's crash=21 counts crash-log lines, not 21 separate crashes.

From the host code: hos_oot_newest_save checks paths/mtimes, not save validity, and the result only enables A presses; the game chooses the slot, not this helper. So file presence is not proof that continuing is safe. This negative result does not establish a regression introduced by this PR: I have not run manual Continue on the same corrupted copy without auto-load, and the underlying load path may fail the same way.

Normal save/no-save behavior is demonstrated on this Linux build. Corrupt-save recovery, current main, Windows, and physical keyboard/controller input remain untested. No save data, game binary or core dump is attached.

Here is the Linux game test promised above. **Tested build:** your `1fc9986f` locally rebased onto `49f75dc8`, with the duplicate `hos_hid_tap` removed as described in my earlier comment: `fb56bc33` (`v2.1.0-rc.3-55-gfb56bc33`). This is not a run of current main or Windows. Arch Linux, RTX 4090, NVIDIA 615.78.08, headless, `skip_title_screen=1`; no manual load presses. | Case | Observed result | |---|---| | Existing intact save | Reached play. The log says `skip title: loaded, the player is running`; the picture shows Link in the world with the HUD. | | No saves | Remained at the title with New Game. The log says `skip title: no saved game yet: the title screen stays`. | | Newest save deliberately cut in half, older slot left intact | The game opened the cut `slot_01/progress.sav`, then crashed with SIGSEGV. It did not open the older `slot_00/progress.sav` before the crash. | ![Valid save reaches play; no-save stays at New Game; truncated newest save's last captured loading frame](https://gist.githubusercontent.com/mooki000/b86bd3f35969803fb032f71f035844f8/raw/4f0130f51658e7037e43212b1020c14440395d22/hookshot-skip-title-three-cases.jpg) The negative test altered only a disposable copy, not an original save. The last panel is the final captured loading frame, **not a screenshot of the crash**. The log then records: ```text fs: OpenFile(save:/slot_01/progress.sav) CRASH: exception 0xc0000005 (write 0xfffffffffff, guest 0xffffffffffffffff) CRASH: thread ModuleSystemWorker4 ``` The process exited 139; coredumpctl confirms SIGSEGV. No device-loss or kernel driver-fault line was reported. The harness's `crash=21` counts crash-log lines, not 21 separate crashes. From the host code: `hos_oot_newest_save` checks paths/mtimes, not save validity, and the result only enables A presses; **the game chooses the slot**, not this helper. So file presence is not proof that continuing is safe. This negative result does **not** establish a regression introduced by this PR: I have not run manual Continue on the same corrupted copy without auto-load, and the underlying load path may fail the same way. Normal save/no-save behavior is demonstrated on this Linux build. Corrupt-save recovery, current main, Windows, and physical keyboard/controller input remain untested. No save data, game binary or core dump is attached.
Joey-Magnus (Migrated from github.com) reviewed 2026-11-11 21:46:00 +00:00
Joey-Magnus (Migrated from github.com) left a comment

hckrman101's change request is about the approach and is still open, so this needs that decision first. Main has already reserved option id 44 for HOS_OPT_SKIP_TITLE (vk_api.h:36), so nothing else has taken it.

For the rebase: it conflicts in vk_api.h, vk_menu.cpp, vk_replay.cpp, nn_hid.c, readme.md and tests/run_all.py. Main now has its own hos_hid_tap at host/nn_hid.c:457, so this PR's copy causes a redefinition error, as mooki000 showed. Drop yours and call main's.

On the approach itself: hos_oot_autoload_input sends 0:A/200 every 2 s, from 20 s to 120 s, without knowing what's on screen. It stops only when the player is seen, the Insert menu opens, or it times out. Any prompt in that window gets A: a damaged-save notice, a confirmation, or a menu the player opened on the title. mooki000's test with a truncated newest save ended in a crash after OpenFile(slot_01/progress.sav). Whatever the design, I'd want the presses to stop as soon as the player touches a control. I'd also want an answer on whether A on any title or file-list prompt can delete or overwrite a save.

hckrman101's change request is about the approach and is still open, so this needs that decision first. Main has already reserved option id 44 for `HOS_OPT_SKIP_TITLE` (`vk_api.h:36`), so nothing else has taken it. For the rebase: it conflicts in `vk_api.h`, `vk_menu.cpp`, `vk_replay.cpp`, `nn_hid.c`, `readme.md` and `tests/run_all.py`. Main now has its own `hos_hid_tap` at `host/nn_hid.c:457`, so this PR's copy causes a redefinition error, as mooki000 showed. Drop yours and call main's. On the approach itself: `hos_oot_autoload_input` sends `0:A/200` every 2 s, from 20 s to 120 s, without knowing what's on screen. It stops only when the player is seen, the Insert menu opens, or it times out. Any prompt in that window gets A: a damaged-save notice, a confirmation, or a menu the player opened on the title. mooki000's test with a truncated newest save ended in a crash after `OpenFile(slot_01/progress.sav)`. Whatever the design, I'd want the presses to stop as soon as the player touches a control. I'd also want an answer on whether A on any title or file-list prompt can delete or overwrite a save.
Commenting is not possible because the repository is archived.
No description provided.