fix(prompts): a failed rebuild of the button pictures keeps the last good file #486

Open
jarynclouatre wants to merge 1 commit from jarynclouatre/Longshot:fix/prompt-cache-keep-good into main
Contributor

Why

When a start writes the button prompts' cache again (after the Button prompts face or a binding changed) and the write fails part way, for example on a full disk, cache/prompts/Common.bntx.zs (or Common.blarc.zs) is left cut short while its key file still holds the old setting's key. Back on the old setting the key matches, the file exists, and the cut file is given to the game at every start.

write_patched in host/prompts.c opens out with fopen(out, "wb"), which empties the old file before the new one is known good. On a failure the old key stays, and the reuse check is only "key equal and the file exists".

What changed

  • prompts (host/prompts.c, write_patched): writes to out.tmp beside the file, with every write and the fclose checked. Then it removes the key file, moves the temporary file over out (MoveFileExA with MOVEFILE_REPLACE_EXISTING) and writes the new key. On any failure the temporary file is deleted and the old file and its key stay as they were, which is still a valid pair. A stop between the steps leaves no key, so the next start writes again. Both callers (textures and layouts) go through this function, and each has its own key file.
  • tests: new tests/test_prompt_cache_errors.py compiles write_patched and its zstd helpers from host/prompts.c, with the three Windows file calls as stand-ins on POSIX calls. Setting A is written; setting B's write fails at a file size limit (RLIMIT_FSIZE, SIGXFSZ ignored); A's file bytes and key are unchanged and no .tmp is left; A again serves the same bytes; B with room replaces file and key, and the file holds the game's bytes with B's run. Skipped on Windows (no file size limit there). tests/test_mods_size_table.py's routing fixture builds prompts.c over pieces of host/posix/win32_posix.c and now takes its DeleteFileA and MoveFileExA too.

Flags and settings

None.

Measurements

main: 9c9d69ed (2026-11-14)
this PR: fix/prompt-cache-keep-good @ d690b06b
Hardware: none used (game-free checks on a Linux x86-64 PC, gcc 15.2, Python 3.14)
Scene: none, the game was not run
Method: python3 -I -B tests/run_all.py --jobs 4, two runs each (main twice, then the branches of this batch in turn, twice). The new test also ran against main's host/prompts.c.
How to reproduce: the commands above in a checkout of each commit

main @ 9c9d69ed this PR @ d690b06b
tests/run_all.py (run 1, run 2) 485 passed, 2 failed, 83 skipped (both) 486 passed, 2 failed, 83 skipped (both)
failing checks test_bake_portability.py, test_walk_key.py the same two
tests/test_prompt_cache_errors.py FAIL: a failed write of setting B changed setting A's file (200000 bytes, was 300019) pass

test_bake_portability.py fails here for a missing Python module (zstandard). test_walk_key.py's fixture doesn't link on main (undefined reference to hos_oot_autoload_input).

Testing

Tested: the game-free suite on a Linux PC, as in Measurements. host/prompts.c compiles (-fsyntax-only -Wall -Wextra) with llvm-mingw 20261104's x86_64-w64-mingw32-clang with no warnings, as main's, and whole with gcc in tests/test_mods_size_table.py's fixture.

Not tested: the game, and a full disk on Windows.

Needs other testers: None.

Screenshots

Nothing on screen.

Risk and rollback

A rewrite now needs room for a second copy of the file for a moment (a few MB). Reverting the commit puts the write in place back. #218 changes the fopen calls in the same function to hos_fopen_*; whichever lands second needs a small rebase.

Issues

None filed. Found reading write_patched.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • 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.

If applicable

  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths.
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
## Why When a start writes the button prompts' cache again (after the Button prompts face or a binding changed) and the write fails part way, for example on a full disk, `cache/prompts/Common.bntx.zs` (or `Common.blarc.zs`) is left cut short while its key file still holds the old setting's key. Back on the old setting the key matches, the file exists, and the cut file is given to the game at every start. `write_patched` in `host/prompts.c` opens `out` with `fopen(out, "wb")`, which empties the old file before the new one is known good. On a failure the old key stays, and the reuse check is only "key equal and the file exists". ## What changed - prompts (`host/prompts.c`, `write_patched`): writes to `out.tmp` beside the file, with every write and the `fclose` checked. Then it removes the key file, moves the temporary file over `out` (`MoveFileExA` with `MOVEFILE_REPLACE_EXISTING`) and writes the new key. On any failure the temporary file is deleted and the old file and its key stay as they were, which is still a valid pair. A stop between the steps leaves no key, so the next start writes again. Both callers (textures and layouts) go through this function, and each has its own key file. - tests: new `tests/test_prompt_cache_errors.py` compiles `write_patched` and its zstd helpers from `host/prompts.c`, with the three Windows file calls as stand-ins on POSIX calls. Setting A is written; setting B's write fails at a file size limit (`RLIMIT_FSIZE`, SIGXFSZ ignored); A's file bytes and key are unchanged and no `.tmp` is left; A again serves the same bytes; B with room replaces file and key, and the file holds the game's bytes with B's run. Skipped on Windows (no file size limit there). `tests/test_mods_size_table.py`'s routing fixture builds `prompts.c` over pieces of `host/posix/win32_posix.c` and now takes its `DeleteFileA` and `MoveFileExA` too. ## Flags and settings None. ## Measurements main: 9c9d69ed (2026-11-14) this PR: fix/prompt-cache-keep-good @ d690b06b Hardware: none used (game-free checks on a Linux x86-64 PC, gcc 15.2, Python 3.14) Scene: none, the game was not run Method: `python3 -I -B tests/run_all.py --jobs 4`, two runs each (main twice, then the branches of this batch in turn, twice). The new test also ran against main's `host/prompts.c`. How to reproduce: the commands above in a checkout of each commit | | main @ 9c9d69ed | this PR @ d690b06b | |---|---|---| | `tests/run_all.py` (run 1, run 2) | 485 passed, 2 failed, 83 skipped (both) | 486 passed, 2 failed, 83 skipped (both) | | failing checks | `test_bake_portability.py`, `test_walk_key.py` | the same two | | `tests/test_prompt_cache_errors.py` | `FAIL: a failed write of setting B changed setting A's file (200000 bytes, was 300019)` | pass | `test_bake_portability.py` fails here for a missing Python module (zstandard). `test_walk_key.py`'s fixture doesn't link on main (`undefined reference to hos_oot_autoload_input`). ## Testing **Tested:** the game-free suite on a Linux PC, as in Measurements. `host/prompts.c` compiles (`-fsyntax-only -Wall -Wextra`) with llvm-mingw 20261104's x86_64-w64-mingw32-clang with no warnings, as main's, and whole with gcc in `tests/test_mods_size_table.py`'s fixture. **Not tested:** the game, and a full disk on Windows. **Needs other testers:** None. ## Screenshots Nothing on screen. ## Risk and rollback A rewrite now needs room for a second copy of the file for a moment (a few MB). Reverting the commit puts the write in place back. #218 changes the `fopen` calls in the same function to `hos_fopen_*`; whichever lands second needs a small rebase. ## Issues None filed. Found reading `write_patched`. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [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. ### If applicable - [x] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened.
fix(prompts): a failed rebuild of the button pictures keeps the last good file
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
d690b06b18
write_patched opened the cached file (Common.bntx.zs or Common.blarc.zs) for
writing before the new one was known good. A write that failed part way, a
full disk for one, left the file cut short with the old setting's key still
beside it. Back on the old setting the key matched and the cut file was given
to the game at every start.

The file is now written beside the old one (.tmp), the key is removed, the
new file is moved over the old and the new key is written. A failed write
deletes the temporary file and leaves the old file and its key as they were.
A stop between the steps leaves no key, so the next start writes again.
jarynclouatre changed title from WIP: fix(prompts): a failed rebuild of the button pictures keeps the last good file to fix(prompts): a failed rebuild of the button pictures keeps the last good file 2026-11-14 16:31:22 +00:00
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/prompt-cache-keep-good:jarynclouatre-fix/prompt-cache-keep-good'
git switch 'jarynclouatre-fix/prompt-cache-keep-good'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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!486
No description provided.