WIP: feat(cheats): save the game where Link stands, from the Cheats page, the control bridge and test input #461

Draft
HumanBean wants to merge 1 commit from HumanBean/Longshot:feat/cheat-save into main
Owner

Type: feat · Risk: medium · Tested on: nothing yet (not built or run) · Needs: Linux and Windows (llvm-mingw), any GPU

Why

Test runs start from a fixed save and spend about 70 s reaching their scene (an opening event, then a teleport), and making a save at a spot today means scripting blind presses through the pause menu. Save here saves the game where Link stands, from the Debug tab, the control bridge or a live input file, so a test (or a player using the debug tools) can make a save at any spot and start from it.

No issue asks for it: the project owner asked for it. The tracker was searched (save, slot, save state, cheats, debug tools, teleport) and nothing fits; #259 (user profiles and save folders) is a different feature.

Nothing in this PR was built or run: no build, no game session, no test suite (the project owner asked for the code and a WIP PR first: "do the code and open pr, don't test it yet"). It is untested.

What changed

A new Save here row on the Debug tab (and the same action from the control bridge and a live input file) plays the game's own pause-menu save for you: Plus, System, Save, Up, A, then B until the game is back in play. It is a host-side input macro, not a call of the game's save routine, so the save is exactly the game's own, in a new slot; the log and the bridge say which slot was written.

Approach: a host-side input macro, not a call of the game's save routine. The repository has no note or address for the routine the pause menu's Save runs, and the existing tools call only game functions whose arguments and checks are known (oot_teleport, oot_set_time, the pouch edits), so no address is guessed. What the macro plays is the sequence verified by scripted input in the Castle Town Market: Plus (pause menu, Items tab), R (System), A (Save: the slot card, Cancel chosen), Up (Save), A. That wrote save/<user>/slot_03/progress.sav, which then loaded at that spot. The direct approach would still need one profiled run that names the caller of progress.sav's write (nn::fs::WriteFile / CloseFile from the pause menu's Save), then a check of its arguments the way oot_pouch_change and oot_teleport check theirs.

  • host, the save (host/hos_main.c, oot_save_*, inside the debug tools' #if defined(_WIN32) || defined(__linux__)): action 998 (OOT_SAVE_ACTION, next to the age's 999; below the teleport's 1000+, above the pouch's 200-348). It is taken at Link's update (oot_update_player_cheats) like the other actions and is refused, with nothing pressed, while an event plays (oot_event_playing: cutscene, conversation, map change), a map loads (Sequence_IsLoadingBanc), no save is loaded (the pouch count is -1, as the Inventory page's), the free camera is on or Link is flying (they take L, R and B), or a save is already under way. The presses then go one at a time through hos_hid_tap, the queue the live input file's lines go through, from the controller's read (hos_oot_save_tick, called in nn_hid.c scripted_input before host_taps). The waits are counted in the game's presents (hos_present_count, 180 = the verified 3 s), not wall time. Checks along the way:
    • nothing is pressed while the Insert menu is open (the row closes it);
    • if Link is still being updated 3 s after Plus, the pause menu did not open, so it stops there and presses nothing in play;
    • a progress.sav must be written within 10 s of the last A;
    • then B, at most four times, until Link is updated again (the card closed, then the menu).
      One log line at the end: cheats: save: saved in slot_NN, cheats: save refused: <why> or cheats: save not made: <why>, after a cheats: save: through the pause menu ... line when it starts.
  • host, the slot written (host/nn_fs.c, note_save_write, hos_fs_save_writes): a progress.sav in the save folder that is closed after a write (CloseFile) or renamed into place (RenameFile) is counted, with its folder's name. The log and the bridge use it to say which slot was written. It is generic code, so it runs for every game but only does something for a file named progress.sav under the save mount.
  • bridge (host/control.c cmd_save / save_done, host/control.h save, save_result, save_cancel): {"cmd":"save"} answers once it is over with "saved":true,"slot":"03" (and a note if it saved but the menu stayed open), refused or failed with the reason, timeout (not taken in play, so it is withdrawn; or still under way), or cancelled. "wait":false answers at once. The default timeout is 60 s. Client: longshot_ctl.py save [--timeout S] [--no-wait] (scripts/longshot_ctl.py).
  • menu (gpu/vk/vk_menu.cpp): a Save here row (Row::cheat 12, button "Save") at the end of the Debug tab's Travel group, after Teleport and Mark/Return. Confirm stores 998 and closes the menu, as Switch age does. kCheatRows, debug_tools_order and debug_groups change to fit it. There is no translation of the new row's texts yet: the existing texts are unchanged, so no table goes stale.
  • tests: the new tests/test_cheat_save.py cuts the save's code out of hos_main.c and runs it against a fake pause menu and fake game data. It checks the sequence and slot, that nothing is pressed while the Insert menu is open, each refusal, a pause menu that doesn't open (it stops after Plus), no slot written (the menu still gets closed), and the bridge's tickets. It also checks the wiring in the sources. Updated: tests/control_test.c (a fake save, and replies: saved with the slot, refused, timeout with the ask withdrawn, no wait), tests/test_longshot_ctl.py, tests/test_damage_debug_menu.py (the Debug tab's order and groups), tests/test_menu_keys.py, tests/test_input_background.py (a stub for the tick its scripted_input now calls), tests/test_debug_tools_linux.py. None of them was run.
  • docs: docs/control.md (the save command, its errors, a Known limit), docs/development.md (!cheat 998), the README (the Debug row list, a paragraph on Save here).

Ways to trigger it:

  • the Insert menu: Debug tab, Debug tools On, Travel, Save here, then A or Enter;
  • the control bridge: {"cmd":"save"} (or python -I scripts/longshot_ctl.py save);
  • a live input file (HOS_INPUT_LIVE=<file>): a line !cheat 998.

Loading a slot at start: the title's Continue loads the newest slot (docs/control.md, saves: newest marks the one Continue loads), and longshot_ctl.py launch --save FOLDER --slot NN already copies one slot alone so that Continue loads it. This PR adds no slot picker.

Reviewer notes: read oot_save_tick in host/hos_main.c first (the steps and their checks), then oot_save_start (the refusals) and note_save_write in host/nn_fs.c. Left for later on purpose: calling the game's save routine directly (needs one profiled run, see above), a slot picker at start, and translations of the new row's texts.

Measurements

Verdict: not measured yet: nothing was built or run. It is no performance change; the measurement a feat needs (Save here working in the game, and no cost with it unused) is still to do.

Before: main @ 26a2eab0 (2026-11-14, the merge base); after: this PR, feat/cheat-save @ 16976033.
Hardware: none yet (not run)
Scene: none yet (not run); the one to use is the Castle Town Market (child), standing still
Method: none yet: no build, no game session, no test suite
How to reproduce: see Must be tested under Testing

Before: main @ 26a2eab0 After: this PR @ 16976033
Save here in the Market not run not run
not run

Screenshots: none yet, nothing was run. The change shows on screen as one new Debug tab row (Save here, button "Save").

Flags and settings

No new environment switch, ini key or build flag: one new Debug tab row, one new live input action and one new bridge command (with its longshot_ctl.py subcommand). The row stores no setting (no longshot_options.ini key).

Flag What it is Before this PR After this PR Effect Helps / doesn't help / untested Recommendation
Debug tab: Save here (new row) Saves where Link stands through the pause menu (Travel group) - (no such row) Shown only while Debug tools is on (off by default), as the other tools Closes the Insert menu and plays the game's pause-menu save into a new slot Helps: test setup, a save at any spot. Untested: everything, not run yet Keep it under the debug tools
HOS_INPUT_LIVE line !cheat 998 (new action) The same, from a test's live input file - (no action 998) Taken only when a line asks for it Starts the same save Helps: scripted tests. Untested: everything, not run yet For tests only
bridge command save (new), longshot_ctl.py save [--timeout S] [--no-wait] The same, with a reply (the slot, or why not) - (no such command) Bridge off unless HOS_CONTROL is set; default timeout 60 s Starts the same save and answers saved with the slot, refused, failed, timeout or cancelled Helps: agents and tests on the bridge. Untested: everything, not run yet For tests and agents only

Testing

Tested: nothing in the game or the test suite. No build, no game run and no test suite was run on this PC for this PR (as asked). The code was only read: the macro, the bridge's tickets and the fixtures were checked by reading, against the verified menu sequence and the existing teleport and age paths. The Test Farm was asked (@Beanstalk, 2026-11-14 19:12) and started a plan for commit 1697603 on linux-nvidia and linux-amd (the existing pause-menu save in the child Market, main against this PR: crashes, back in play, a picture after saving); at the time of this edit both were still queued, with no report. Its plan says it cannot press Save here or check and reload the written slot, so even when it finishes it does not test the new feature.

Not tested: all of it, nothing having been built or run. Things to watch, where the macro assumes something not yet seen: that Link's update stops while the pause menu is open (the check after Plus relies on it, and docs/control.md says the bridge's samples stop then); that the card's cursor is on Cancel every time (a second save in the same session: the probe's second A saved again, so the cursor may be remembered as Save, and then Up might do nothing or move off it); that two B presses close the card and the menu; that a hold-toggled R (Hold to toggle) doesn't stay latched afterwards.

Must be tested:

  • Linux and Windows (llvm-mingw), any GPU: build, then python -I -B tests/run_all.py, expecting it to build and the new tests/test_cheat_save.py and the updated checks to pass (by hand, or Forgejo's CI: the build log and the test summary)
  • Linux, any GPU: Castle Town Market (child), standing still: Debug tab, Debug tools On, Travel, Save here, expecting the Insert menu to close and the pause menu to open, then System, Save, Up, Save; a new slot_NN beside the others; the log line cheats: save: saved in slot_NN; the game back in play (the pause menu closed by the B presses); then restart and Continue, expecting Link to start at that spot (by hand: the log, the save folder and the spot after Continue)
  • Linux, any GPU: after the save above, longshot_ctl.py slots (or saves), expecting the slot in the log line and the bridge's reply to be the one shown as newest (by hand)
  • Linux, any GPU: longshot_ctl.py save in play, expecting saved in slot NN; during a conversation or a cutscene, expecting refused: ... an event is playing and nothing pressed; on the title or file select, expecting timeout (not taken); two quick asks, expecting the first one cancelled (by hand: the replies and the log)
  • Linux, any GPU: a line !cheat 998 in an HOS_INPUT_LIVE file, expecting the same save (by hand: the log line and the new slot)
  • Linux, any GPU: Save here with the free camera on and while flying, expecting it refused; a second Save here while one is under way, expecting it refused and the first one to finish (by hand: the log)
  • Linux, any GPU: two Save here in the same session, expecting the second also saves into a new slot and the cursor/Up step still lands on Save; with R set to Hold to toggle, expecting R not latched afterwards (by hand: watch the card and the controls after the save)
  • windows (llvm-mingw build), once it has run on Linux: the same Market save and the bridge's reply, expecting the same slot line (Test Farm: @Beanstalk windows, Debug tab Save here in the child Market and the bridge's save reply, the slot written and Continue loading it)

Risks

What could break: presses landing in play if the game isn't where the macro expects it (a pause menu that didn't open, the card's cursor not on Cancel, a menu left open). Against that, it presses nothing until the Insert menu is closed and Link is in play, it stops after Plus if the pause menu didn't open, and it stops pressing B as soon as Link is updated again. Otherwise inert unless asked for: the tick returns at once while no save is under way, and the nn_fs.c hook only counts files named progress.sav in the save folder (generic code, so it runs for every game, but does nothing else).

Turning it off: don't use the row, the save command or !cheat 998 (the row is shown only while Debug tools is on, off by default); reverting the commit removes it.

Compatibility: saves: Save here writes a save, but it is the game's own save into a new slot each time, so it never overwrites an existing slot, and existing saves are read as before; the nn_fs.c hook only notes which progress.sav was written and changes no file. longshot_options.ini: no new key (the row stores nothing). The shader and pipeline caches, the vk ABI and the builder's tools: unchanged. The control bridge gains a command; its other commands are unchanged.

Checklist

Everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description) of the status line's type.
  • Why gives the reason plainly and links the issues that fit (the tracker searched).
  • Measurements has a verdict, or one line saying why there is nothing to measure.
  • Testing names what was tested, what wasn't, and what must still be tested, where and how.
  • Rebased on current main. (Not yet: the branch is on main @ 26a2eab0; main is now at 7ff5256e.)
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named). (Not run, as asked: left to Forgejo's CI and the first test run.)
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

When it applies

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value.
  • Screenshots of the same spot, captioned like the Measurements columns, for a change that shows on screen. (Not yet: nothing was run.)
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Not built yet. Written to be portable: SRW locks and __atomic as the neighbouring code uses, _stricmp as nn_fs.c already does, no new path conversion.)
  • Docs updated.
**Type:** feat · **Risk:** medium · **Tested on:** nothing yet (not built or run) · **Needs:** Linux and Windows (llvm-mingw), any GPU ## Why Test runs start from a fixed save and spend about 70 s reaching their scene (an opening event, then a teleport), and making a save at a spot today means scripting blind presses through the pause menu. **Save here** saves the game where Link stands, from the Debug tab, the control bridge or a live input file, so a test (or a player using the debug tools) can make a save at any spot and start from it. No issue asks for it: the project owner asked for it. The tracker was searched (save, slot, save state, cheats, debug tools, teleport) and nothing fits; #259 (user profiles and save folders) is a different feature. **Nothing in this PR was built or run**: no build, no game session, no test suite (the project owner asked for the code and a WIP PR first: "do the code and open pr, don't test it yet"). It is untested. ## What changed A new **Save here** row on the Debug tab (and the same action from the control bridge and a live input file) plays the game's own pause-menu save for you: Plus, System, Save, Up, A, then B until the game is back in play. It is a host-side input macro, not a call of the game's save routine, so the save is exactly the game's own, in a new slot; the log and the bridge say which slot was written. **Approach: a host-side input macro, not a call of the game's save routine.** The repository has no note or address for the routine the pause menu's Save runs, and the existing tools call only game functions whose arguments and checks are known (`oot_teleport`, `oot_set_time`, the pouch edits), so no address is guessed. What the macro plays is the sequence verified by scripted input in the Castle Town Market: Plus (pause menu, Items tab), R (System), A (Save: the slot card, Cancel chosen), Up (Save), A. That wrote `save/<user>/slot_03/progress.sav`, which then loaded at that spot. **The direct approach would still need** one profiled run that names the caller of `progress.sav`'s write (`nn::fs::WriteFile` / `CloseFile` from the pause menu's Save), then a check of its arguments the way `oot_pouch_change` and `oot_teleport` check theirs. - host, the save (`host/hos_main.c`, `oot_save_*`, inside the debug tools' `#if defined(_WIN32) || defined(__linux__)`): action **998** (`OOT_SAVE_ACTION`, next to the age's 999; below the teleport's 1000+, above the pouch's 200-348). It is taken at Link's update (`oot_update_player_cheats`) like the other actions and is refused, with nothing pressed, while an event plays (`oot_event_playing`: cutscene, conversation, map change), a map loads (`Sequence_IsLoadingBanc`), no save is loaded (the pouch count is -1, as the Inventory page's), the free camera is on or Link is flying (they take L, R and B), or a save is already under way. The presses then go one at a time through `hos_hid_tap`, the queue the live input file's lines go through, from the controller's read (`hos_oot_save_tick`, called in `nn_hid.c scripted_input` before `host_taps`). The waits are counted in the game's presents (`hos_present_count`, 180 = the verified 3 s), not wall time. Checks along the way: - nothing is pressed while the Insert menu is open (the row closes it); - if Link is still being updated 3 s after Plus, the pause menu did not open, so it stops there and presses nothing in play; - a `progress.sav` must be written within 10 s of the last A; - then B, at most four times, until Link is updated again (the card closed, then the menu). One log line at the end: `cheats: save: saved in slot_NN`, `cheats: save refused: <why>` or `cheats: save not made: <why>`, after a `cheats: save: through the pause menu ...` line when it starts. - host, the slot written (`host/nn_fs.c`, `note_save_write`, `hos_fs_save_writes`): a `progress.sav` in the save folder that is closed after a write (`CloseFile`) or renamed into place (`RenameFile`) is counted, with its folder's name. The log and the bridge use it to say which slot was written. It is generic code, so it runs for every game but only does something for a file named `progress.sav` under the save mount. - bridge (`host/control.c` `cmd_save` / `save_done`, `host/control.h` `save`, `save_result`, `save_cancel`): `{"cmd":"save"}` answers once it is over with `"saved":true,"slot":"03"` (and a `note` if it saved but the menu stayed open), `refused` or `failed` with the reason, `timeout` (not taken in play, so it is withdrawn; or still under way), or `cancelled`. `"wait":false` answers at once. The default timeout is 60 s. Client: `longshot_ctl.py save [--timeout S] [--no-wait]` (`scripts/longshot_ctl.py`). - menu (`gpu/vk/vk_menu.cpp`): a **Save here** row (`Row::cheat` 12, button "Save") at the end of the Debug tab's Travel group, after Teleport and Mark/Return. Confirm stores 998 and closes the menu, as Switch age does. `kCheatRows`, `debug_tools_order` and `debug_groups` change to fit it. There is no translation of the new row's texts yet: the existing texts are unchanged, so no table goes stale. - tests: the new `tests/test_cheat_save.py` cuts the save's code out of `hos_main.c` and runs it against a fake pause menu and fake game data. It checks the sequence and slot, that nothing is pressed while the Insert menu is open, each refusal, a pause menu that doesn't open (it stops after Plus), no slot written (the menu still gets closed), and the bridge's tickets. It also checks the wiring in the sources. Updated: `tests/control_test.c` (a fake save, and replies: saved with the slot, refused, timeout with the ask withdrawn, no wait), `tests/test_longshot_ctl.py`, `tests/test_damage_debug_menu.py` (the Debug tab's order and groups), `tests/test_menu_keys.py`, `tests/test_input_background.py` (a stub for the tick its `scripted_input` now calls), `tests/test_debug_tools_linux.py`. **None of them was run.** - docs: `docs/control.md` (the `save` command, its errors, a Known limit), `docs/development.md` (`!cheat 998`), the README (the Debug row list, a paragraph on Save here). **Ways to trigger it:** - the Insert menu: Debug tab, Debug tools On, Travel, **Save here**, then A or Enter; - the control bridge: `{"cmd":"save"}` (or `python -I scripts/longshot_ctl.py save`); - a live input file (`HOS_INPUT_LIVE=<file>`): a line `!cheat 998`. **Loading a slot at start:** the title's Continue loads the newest slot (docs/control.md, `saves`: `newest` marks the one Continue loads), and `longshot_ctl.py launch --save FOLDER --slot NN` already copies one slot alone so that Continue loads it. This PR adds no slot picker. **Reviewer notes:** read `oot_save_tick` in `host/hos_main.c` first (the steps and their checks), then `oot_save_start` (the refusals) and `note_save_write` in `host/nn_fs.c`. Left for later on purpose: calling the game's save routine directly (needs one profiled run, see above), a slot picker at start, and translations of the new row's texts. ## Measurements **Verdict:** not measured yet: nothing was built or run. It is no performance change; the measurement a feat needs (Save here working in the game, and no cost with it unused) is still to do. Before: main @ 26a2eab0 (2026-11-14, the merge base); after: this PR, feat/cheat-save @ 16976033. Hardware: none yet (not run) Scene: none yet (not run); the one to use is the Castle Town Market (child), standing still Method: none yet: no build, no game session, no test suite How to reproduce: see Must be tested under Testing | | Before: main @ 26a2eab0 | After: this PR @ 16976033 | |---|---|---| | Save here in the Market | not run | not run | ``` not run ``` Screenshots: none yet, nothing was run. The change shows on screen as one new Debug tab row (Save here, button "Save"). ### Flags and settings No new environment switch, ini key or build flag: one new Debug tab row, one new live input action and one new bridge command (with its `longshot_ctl.py` subcommand). The row stores no setting (no `longshot_options.ini` key). | Flag | What it is | Before this PR | After this PR | Effect | Helps / doesn't help / untested | Recommendation | |---|---|---|---|---|---|---| | Debug tab: Save here (new row) | Saves where Link stands through the pause menu (Travel group) | - (no such row) | Shown only while Debug tools is on (off by default), as the other tools | Closes the Insert menu and plays the game's pause-menu save into a new slot | Helps: test setup, a save at any spot. Untested: everything, not run yet | Keep it under the debug tools | | `HOS_INPUT_LIVE` line `!cheat 998` (new action) | The same, from a test's live input file | - (no action 998) | Taken only when a line asks for it | Starts the same save | Helps: scripted tests. Untested: everything, not run yet | For tests only | | bridge command `save` (new), `longshot_ctl.py save [--timeout S] [--no-wait]` | The same, with a reply (the slot, or why not) | - (no such command) | Bridge off unless `HOS_CONTROL` is set; default timeout 60 s | Starts the same save and answers `saved` with the slot, `refused`, `failed`, `timeout` or `cancelled` | Helps: agents and tests on the bridge. Untested: everything, not run yet | For tests and agents only | ## Testing **Tested:** nothing in the game or the test suite. No build, no game run and no test suite was run on this PC for this PR (as asked). The code was only read: the macro, the bridge's tickets and the fixtures were checked by reading, against the verified menu sequence and the existing teleport and age paths. The Test Farm was asked (`@Beanstalk`, 2026-11-14 19:12) and started a plan for commit 1697603 on linux-nvidia and linux-amd (the existing pause-menu save in the child Market, main against this PR: crashes, back in play, a picture after saving); at the time of this edit both were still queued, with no report. Its plan says it cannot press Save here or check and reload the written slot, so even when it finishes it does not test the new feature. **Not tested:** all of it, nothing having been built or run. Things to watch, where the macro assumes something not yet seen: that Link's update stops while the pause menu is open (the check after Plus relies on it, and docs/control.md says the bridge's samples stop then); that the card's cursor is on Cancel every time (a second save in the same session: the probe's second A saved again, so the cursor may be remembered as Save, and then Up might do nothing or move off it); that two B presses close the card and the menu; that a hold-toggled R (Hold to toggle) doesn't stay latched afterwards. **Must be tested:** - Linux and Windows (llvm-mingw), any GPU: build, then `python -I -B tests/run_all.py`, expecting it to build and the new `tests/test_cheat_save.py` and the updated checks to pass (by hand, or Forgejo's CI: the build log and the test summary) - Linux, any GPU: Castle Town Market (child), standing still: Debug tab, Debug tools On, Travel, **Save here**, expecting the Insert menu to close and the pause menu to open, then System, Save, Up, Save; a new `slot_NN` beside the others; the log line `cheats: save: saved in slot_NN`; the game back in play (the pause menu closed by the B presses); then restart and Continue, expecting Link to start at that spot (by hand: the log, the save folder and the spot after Continue) - Linux, any GPU: after the save above, `longshot_ctl.py slots` (or `saves`), expecting the slot in the log line and the bridge's reply to be the one shown as newest (by hand) - Linux, any GPU: `longshot_ctl.py save` in play, expecting `saved in slot NN`; during a conversation or a cutscene, expecting `refused: ... an event is playing` and nothing pressed; on the title or file select, expecting `timeout` (not taken); two quick asks, expecting the first one `cancelled` (by hand: the replies and the log) - Linux, any GPU: a line `!cheat 998` in an `HOS_INPUT_LIVE` file, expecting the same save (by hand: the log line and the new slot) - Linux, any GPU: Save here with the free camera on and while flying, expecting it refused; a second Save here while one is under way, expecting it refused and the first one to finish (by hand: the log) - Linux, any GPU: two Save here in the same session, expecting the second also saves into a new slot and the cursor/Up step still lands on Save; with R set to Hold to toggle, expecting R not latched afterwards (by hand: watch the card and the controls after the save) - windows (llvm-mingw build), once it has run on Linux: the same Market save and the bridge's reply, expecting the same slot line (Test Farm: @Beanstalk windows, Debug tab Save here in the child Market and the bridge's `save` reply, the slot written and Continue loading it) ## Risks **What could break:** presses landing in play if the game isn't where the macro expects it (a pause menu that didn't open, the card's cursor not on Cancel, a menu left open). Against that, it presses nothing until the Insert menu is closed and Link is in play, it stops after Plus if the pause menu didn't open, and it stops pressing B as soon as Link is updated again. Otherwise inert unless asked for: the tick returns at once while no save is under way, and the `nn_fs.c` hook only counts files named `progress.sav` in the save folder (generic code, so it runs for every game, but does nothing else). **Turning it off:** don't use the row, the `save` command or `!cheat 998` (the row is shown only while Debug tools is on, off by default); reverting the commit removes it. **Compatibility:** saves: Save here writes a save, but it is the game's own save into a new slot each time, so it never overwrites an existing slot, and existing saves are read as before; the `nn_fs.c` hook only notes which `progress.sav` was written and changes no file. `longshot_options.ini`: no new key (the row stores nothing). The shader and pipeline caches, the vk ABI and the builder's tools: unchanged. The control bridge gains a command; its other commands are unchanged. ## Checklist ### Everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`) of the status line's type. - [x] Why gives the reason plainly and links the issues that fit (the tracker searched). - [x] Measurements has a verdict, or one line saying why there is nothing to measure. - [x] Testing names what was tested, what wasn't, and what must still be tested, where and how. - [ ] Rebased on current `main`. (Not yet: the branch is on main @ 26a2eab0; main is now at 7ff5256e.) - [ ] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). (Not run, as asked: left to Forgejo's CI and the first test run.) - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### When it applies - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value. - [ ] Screenshots of the same spot, captioned like the Measurements columns, for a change that shows on screen. (Not yet: nothing was run.) - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Not built yet. Written to be portable: SRW locks and `__atomic` as the neighbouring code uses, `_stricmp` as `nn_fs.c` already does, no new path conversion.) - [x] Docs updated.
feat(cheats): save the game where Link stands, from the Cheats page, the control bridge and test input
Some checks reported errors
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 16s
CI / Workflow lint (actionlint) (pull_request) Successful in 36s
Third-party parts / As their notes say (pull_request) Successful in 42s
Website / Website on Cloudflare (pull_request_target) Has been skipped
beanstalk/linux-nvidia 0/1 pass, 0 running, 1 queued
beanstalk/linux-amd 0/1 pass, 0 running, 1 queued
PR description / Description follows the template (pull_request_target) Successful in 5s
beanstalk/approval approved by HumanBean (reviewed)
beanstalk/linux+nvidia 0/1 pass, 1 error
beanstalk/linux+amd 0/1 pass, 0 running, 1 queued
169760336d
Test runs start from a fixed save and spend about 70 s reaching their scene; a save made at the scene removes that, and
making one today means scripting blind presses through the pause menu.

Save here (Debug tab, Travel; action 998: the bridge's save command, a live input file's "!cheat 998") plays the game's
own pause menu, the sequence verified in the Market: Plus, R (System), A (Save: the slot card), Up, A. It is a host-side
input macro, not a call of the game's save routine, which is not known from the repository (one profiled run, the caller
of progress.sav's write, would name it). The presses go through hos_hid_tap, the live input's queue, one at a time from
the controller's read, 180 presents apart. Taken at Link's update and refused during an event, a load, with no save
loaded, with the free camera or the flight on, or while a save is under way. Nothing is pressed until the Insert menu is
closed; if Link is still updated 3 s after Plus (the pause menu did not open) it stops there. nn_fs.c counts progress.sav
files written into the save folder with their slot folder, so the log line and the bridge's reply name the slot; B then
closes the menu (at most four times, until Link is updated again).

The bridge's save replies saved and the slot ("03"), refused or failed with the reason, or timeout; longshot_ctl.py save.
Tests: tests/test_cheat_save.py compiles the save's code against a fake pause menu; control_test.c and
test_longshot_ctl.py get a fake save; the Debug tab's order, the menu keys, the input fixture's stubs and the Linux
debug-tools check are updated. Nothing was built or run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017vw8e34avQ5cTCWow16MXn
Author
Owner

Description: the Measurements' main and branch lines in the form the description check reads (main: (), this PR: @ ). Nothing else changed; still nothing built or run.

Description: the Measurements' main and branch lines in the form the description check reads (main: <hash> (<date>), this PR: <branch> @ <hash>). Nothing else changed; still nothing built or run.
Author
Owner

/beanstalk

/beanstalk
Author
Owner

@Beanstalk

@Beanstalk
Member

@HumanBean authors can't approve their own PR; another maintainer has to.

@HumanBean authors can't approve their own PR; another maintainer has to.
Author
Owner

@Beanstalk

@Beanstalk
Member

Beanstalk test farm

Commit 1697603 · tier reviewed · approved by HumanBean · dashboard

What will be tested · AI plan (gpt-6.1-terra)

The plan compares the existing pause-menu save flow in the child Market on main and the pull request, taking about 13 minutes per tester. It checks for crashes, a return to gameplay, and a visual difference after saving. The plan cannot activate Save here or verify the written slot and reload it, so those new behaviors remain untested.

  • Where: linux+nvidia (1 report), linux+amd (1 report).
  • market-manual-save: from a new save, then teleport to Market, press buttons, take the picture market-after-save to compare with main; 1 run on each side.
  • Measured: crashes.
  • The PR fails if: it crashes; or a picture differs from main by more than 10%.
  • Time: about 13 min of game runs per tester (2 game starts), plus the two builds.

Started.

Target Reports Crash Frame p99 (ms) vs main VRAM peak (MB) vs main Result
linux+nvidia · 1: linux+nvidia+nvidia-proprietary+dgpu NVIDIA GeForce RTX 4070, 615.71.9 1/1 1 none — — error
linux+amd 0/1 — — — queued

Measurements · linux+nvidia

Verdict: could not be judged on linux+nvidia (1/1 report).

Hardware: 1: linux+nvidia+nvidia-proprietary+dgpu NVIDIA GeForce RTX 4070, 615.71.9 AMD Ryzen 7 5800X3D 8-Core Processor, 63 GB RAM
Scene: market-manual-save, from a new save; each cell's settings and switches as the table names them
Method: the Beanstalk test farm: main and this PR built and run on the same PC in one session, game starts alternating (ABBA); market-manual-save: 1 game start a side; each number the median of the runs, then of 1 report
How to reproduce: the farm's plan above (@Beanstalk on this PR runs it again)

Nothing was measured: no metric came back.

Flags and settings

None: the plan varied no setting, switch or build option.

Markdown for the PR description
## Measurements

**Verdict:** could not be judged on linux+nvidia (1/1 report).

Hardware: 1: linux+nvidia+nvidia-proprietary+dgpu `NVIDIA GeForce RTX 4070, 615.71.9` `AMD Ryzen 7 5800X3D 8-Core Processor, 63 GB RAM`  
Scene: market-manual-save, from a new save; each cell's settings and switches as the table names them  
Method: the Beanstalk test farm: main and this PR built and run on the same PC in one session, game starts alternating (ABBA); market-manual-save: 1 game start a side; each number the median of the runs, then of 1 report  
How to reproduce: the farm's plan above (`@Beanstalk` on this PR runs it again)

Nothing was measured: no metric came back.

### Flags and settings

None: the plan varied no setting, switch or build option.
<!-- beanstalk:sticky --> ### Beanstalk test farm Commit `1697603` · tier **reviewed** · approved by HumanBean · [dashboard](https://beanstalk.magicbean.dev/prs/461) **What will be tested** · AI plan (`gpt-6.1-terra`) The plan compares the existing pause-menu save flow in the child Market on main and the pull request, taking about 13 minutes per tester. It checks for crashes, a return to gameplay, and a visual difference after saving. The plan cannot activate Save here or verify the written slot and reload it, so those new behaviors remain untested. - **Where:** linux+nvidia (1 report), linux+amd (1 report). - **market-manual-save:** from a new save, then teleport to Market, press buttons, take the picture market-after-save to compare with main; 1 run on each side. - **Measured:** crashes. - **The PR fails if:** it crashes; or a picture differs from main by more than 10%. - **Time:** about 13 min of game runs per tester (2 game starts), plus the two builds. Started. | Target | Reports | Crash | Frame p99 (ms) vs main | VRAM peak (MB) vs main | Result | | --- | --- | --- | --- | --- | --- | | linux+nvidia · 1: linux+nvidia+nvidia-proprietary+dgpu `NVIDIA GeForce RTX 4070, 615.71.9` | 1/1 [1](https://beanstalk.magicbean.dev/reports/rp_dddd96501f6e43e3) | none | — | — | error | | linux+amd | 0/1 | — | — | — | queued | #### Measurements · linux+nvidia **Verdict:** could not be judged on linux+nvidia (1/1 report). Hardware: 1: linux+nvidia+nvidia-proprietary+dgpu `NVIDIA GeForce RTX 4070, 615.71.9` `AMD Ryzen 7 5800X3D 8-Core Processor, 63 GB RAM` Scene: market-manual-save, from a new save; each cell's settings and switches as the table names them Method: the Beanstalk test farm: main and this PR built and run on the same PC in one session, game starts alternating (ABBA); market-manual-save: 1 game start a side; each number the median of the runs, then of 1 report How to reproduce: the farm's plan above (`@Beanstalk` on this PR runs it again) Nothing was measured: no metric came back. ### Flags and settings None: the plan varied no setting, switch or build option. <details><summary>Markdown for the PR description</summary> ```markdown ## Measurements **Verdict:** could not be judged on linux+nvidia (1/1 report). Hardware: 1: linux+nvidia+nvidia-proprietary+dgpu `NVIDIA GeForce RTX 4070, 615.71.9` `AMD Ryzen 7 5800X3D 8-Core Processor, 63 GB RAM` Scene: market-manual-save, from a new save; each cell's settings and switches as the table names them Method: the Beanstalk test farm: main and this PR built and run on the same PC in one session, game starts alternating (ABBA); market-manual-save: 1 game start a side; each number the median of the runs, then of 1 report How to reproduce: the farm's plan above (`@Beanstalk` on this PR runs it again) Nothing was measured: no metric came back. ### Flags and settings None: the plan varied no setting, switch or build option. ``` </details>
Author
Owner

@Beanstalk

@Beanstalk
Member

@HumanBean 1697603 is already running: see the dashboard.

@HumanBean `1697603` is already running: see the [dashboard](https://beanstalk.magicbean.dev/prs/461).
Some checks reported errors
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 16s
CI / Workflow lint (actionlint) (pull_request) Successful in 36s
Third-party parts / As their notes say (pull_request) Successful in 42s
Website / Website on Cloudflare (pull_request_target) Has been skipped
beanstalk/linux-nvidia 0/1 pass, 0 running, 1 queued
beanstalk/linux-amd 0/1 pass, 0 running, 1 queued
PR description / Description follows the template (pull_request_target) Successful in 5s
beanstalk/approval approved by HumanBean (reviewed)
beanstalk/linux+nvidia 0/1 pass, 1 error
beanstalk/linux+amd 0/1 pass, 0 running, 1 queued
This pull request has changes conflicting with the target branch.
  • docs/control.md
  • docs/development.md
  • host/hos_main.c
  • scripts/longshot_ctl.py
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'feat/cheat-save:HumanBean-feat/cheat-save'
git switch 'HumanBean-feat/cheat-save'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!461
No description provided.