fix(linux): a saves move that fails part way brings back what it moved #557

Open
zeldaootr wants to merge 3 commits from zeldaootr/Longshot:saves-move-partial-failure into main
Contributor

Type: fix · Risk: low · Tested on: Linux x86-64 (game-free); Steam Deck OLED, SteamOS 3.9.5 (a tester's run) · Needs: none

Why

On the first start of a new version, the game moves its saves out of the old user/ folder. If one entry could not be moved (a full disk, say), the entries moved before it stayed in the new place, and the next start kept using the old folder without them: saves and settings could seem to vanish. The tracker has no issue for it (searched).

How to reproduce

Seen on: Steam Deck OLED, SteamOS 3.9.5, main 7ff5256e (a tester's run; Linux only: Windows keeps user/ beside the game)
Settings and switches: XDG_DATA_HOME on another file system than the game folder (here tmpfs against ext4), so the move has to copy
Steps: in a copy of a test game folder, put a test save in user/ with an entry after it that cannot be read (chmod 000), then start the game
Expected: the log says the saves stay in user/, and user/ keeps every entry
Actual: on main the log says the saves stay in user/, but the move had already taken the save: user/ was left with only locked/, temp/ and an empty save/ account folder, and 10 save files were in the new data folder, where the game does not look

What changed

A saves move that fails part way now puts back what it had already moved, so the old user/ is whole again and the next start uses it as before. A copied entry's old place is deleted only once every entry has arrived.

  • host: saves_dir() in host/hos_main.c remembers each entry it moves (renamed or copied). When one fails, the renamed ones are moved back and the copies in the new place deleted; a rename back that fails is logged (saves: <to> could not come back to <from>).
  • tests: tests/test_linux_saves_move.py gets a case with an entry that cannot be copied (an unreadable file stands in for a full disk), with renamed and copied entries listed before it, on one drive and across two: at both starts the old place must keep every entry and the new place stay empty, and once the entry can go, the next start moves them all.

Measurements

Verdict: pass, the bug is gone. On a tester's Steam Deck OLED, with an unreadable entry in user/ and the data folder on another file system, main lost 10 save files out of user/ into a folder the game does not read; with this PR user/ was unchanged in both starts and the new place held no files, and once the entry was readable the next start moved everything. The game-free test fails on main and passes here.

Hardware: Steam Deck OLED, AMD Custom APU 0932 (RADV VANGOGH), 16 GB, Mesa 26.2.0-devel git-5c65d4fc31, SteamOS 3.9.5 (build 20261023.100), kernel 7.2.7-valve2 (dukenheimer's run, test request T-WYKE); game-free: Linux x86-64 server, no GPU or game
Scene: the first start in a copy of a test game folder on ext4 (/home), XDG_DATA_HOME an empty test folder on tmpfs (/tmp); user/ held a test save (one account, four slots), an empty temp/ and locked/unreadable.bin with no read permission, in that order (save, temp, locked), so the save went first; started headless (HOS_HEADLESS=1) with stderr on the journal, closed through the control bridge
Method: the saves: lines of each start's logs/longshot_log.txt, and the entries, modes and sizes of user/ and the data folder after it. This PR twice with the entry unreadable and once with it readable again (chmod 644), main once with it unreadable; one more start of this PR with the data folder on ext4 (a rename). A behaviour check, not a timing: the outcome of each start is all or nothing. Game-free: python3 -I -B tests/test_linux_saves_move.py at 692a7948 and at 6464bfde (rebased onto main aa95d35f), with gcc 13.3 and with clang 23.1.5; before that, at 3e562a68 and 4f15e996 on main 53f96d78, and before the first rebase at ccccaa8b and f7f5578a, twice each, alternating
How to reproduce: build main 7ff5256e and the combined build (main 7ff5256e with #550 to #558 merged in order), both x86-64-v3 with the release play profile and LLVM 23.1.5; then the steps under How to reproduce, never in a real game folder; for the game-free check, run the test command at each commit

Before: main @ 7ff5256e After: this PR @ f7f5578a (in the combined build)
starts with the unreadable entry 1 2
user/ after them 10 of its files gone unchanged both times (the same 20 entries, modes and sizes)
the new data folder after them those 10 files no files (only the empty longshot/ folder the start made)
the next start with the entry readable not run moved all of user/ (the 10 save files, temp/, locked/); saves-moved.txt beside the game
this PR, starts 1 and 2 (unreadable): saves: 1 of <game>/user's entries could not go to <data>/longshot: the saves stay in <game>/user
this PR, start 3 (readable again):    saves: moved from <game>/user to <data>/longshot
main, start 1 (unreadable):           saves: 1 of <game>/user's entries could not go to <data>/longshot: the saves stay in <game>/user
this PR, data folder on ext4 too:     saves: moved from <game>/user to <data>/longshot

Each start ran: in start 3 the frame count went from 190 to 503 in 5 s; starts 1 and 2 were closed while still preparing shaders, after the saves step, which comes first. Within one file system the move is a rename, which never reads the file, so it does not fail there. The tester's report: #557 (comment)

Before: main @ aa95d35f (with the test, 692a7948) After: this PR @ 6464bfde
old user/ whole and new place empty after a failed move (gcc and clang the same) no: entries lost yes
692a7948: test_linux_saves_move: an entry that cannot go: start 1: user/ lost or changed entries (exit 1, gcc and clang)
6464bfde: PASS: the saves' move makes a link again in the new place and leaves its folder alone, from another drive too; a linked user/ and a new place with entries keep the old place whole (exit 0, gcc and clang)

Flags and settings

None.

Testing

Tested: Linux x86-64, game-free. The new test fails on the test-only commit 692a7948 and passes at 6464bfde, this branch rebased onto main aa95d35f, with gcc 13.3 and with clang 23.1.5 (before, it did the same at 3e562a68 and 4f15e996 on main 53f96d78, and at ccccaa8b and f7f5578a, twice each, alternating) (python3 -I -B tests/test_linux_saves_move.py). On land/integration 27553c83 (main 7ff5256e with this PR and twelve other fixes merged), the thirteen fixes' own tests and the checks their fix commits edit pass, 16 of 16; the full python3 -I -B tests/run_all.py ran on that branch too (see the checklist).

In the game: dukenheimer ran test request T-WYKE on a Steam Deck OLED (SteamOS 3.9.5) and reported a pass on 2026-11-15 at 03:06 UTC (#557 (comment)). The builds were the combined build of main 7ff5256e with #550 to #558 merged in order, this PR at its head then, f7f5578a (6464bfde after the rebases, the same change by git patch-id), and main 7ff5256e for the before; each in a copy of a test game folder, started headless with stderr on the journal, as a launcher gives it. With an unreadable entry in user/ and the data folder on tmpfs, this PR left user/ unchanged in both starts and the new place empty, and the third start, with the entry readable again, moved everything; main, once, logged the same "the saves stay" line while 10 save files had already gone to the new place. With the data folder on ext4 the move is a rename and succeeded (this PR, one start).

The branch has been rebased since the tester's run, onto main aa95d35f, with its commits re-authored under the forge's noreply address; each commit's change is identical to before by git patch-id, so the commits named above are the ones tested and the result stands for this head (5c1b3834).

Not tested: A real full disk (an unreadable file stands in for it, in the test and on the Deck). An exFAT SD card as the data folder. A start through Steam in Game Mode with a window (the Deck's starts were headless). A rename back that fails, and a crash during the deferred deletes (see Risks). Windows is not affected: saves_dir is built on Linux only (#ifndef _WIN32; Windows keeps user/ beside the game).

Must be tested: None

Risks

What could break: The rollback has limits. Copied entries' old places are deleted after all entries arrived: a crash or power loss during those deferred deletes leaves some entries in both places, and the next start then sees entries in the new place and keeps the old one, which still has everything (no save lost, the move just does not happen). A rename back that fails (rare: same drive, the name was just free) leaves that entry in the new place and logs it. If memory runs out while listing, the move is called off as if an entry failed. The Host code builds on Windows box is left out: the change is inside #ifndef _WIN32.

Turning it off: revert the commit

Compatibility: Nothing changes for a move that succeeds; one that fails now leaves the saves in user/, where the game keeps using them

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. Onto main aa95d35f with no conflict, the commits re-authored under the forge's noreply address: each of the three commits' changes (the test, the fix, the docs/tested/ file) is identical to before by git patch-id. Since 53f96d78, main changed host/hos_main.c in #729 (9e6f231d), away from this PR's lines.
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named). Run on this branch at ab8fcc68 on main 53f96d78, before this rebase (5c1b3834 now, the same patches by git patch-id; the Vulkan and SPIR-V headers from the CPM cache, 8 at once, on a loaded Linux x86-64 server): 530 passed, 15 failed, 48 skipped. The 15 are the ones that fail on main 53f96d78 in the same run: test_appimage_player_build, test_bake_portability, test_cpm_discovery, test_crash_dialog, test_env_on, test_lift_cache, test_lift_cleanup_error, test_make_game_cache, test_pipeline_tour, test_readback_late, test_vk_pgo, test_vram_budget_row, test_walk_key, nx2/test_mesh_first_types.py and nx2/test_texture_init_barrier.py. Main's run also timed out test_fpx_sticky (it passes alone there). After the rebase onto main aa95d35f, at 5c1b3834: 35 checks, this PR's test and those that read its changed files, with g++ 13.3 and with clang 23.1.5: 32 pass; the 3 that fail fail on main aa95d35f too (run alone there, both compilers): tests/test_crash_dialog.py (#626), tests/test_lift_cache.py (#628), tests/test_pipeline_tour.py (#627).
  • 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.
  • Numbers from the same PC, scene and settings, at least 2 interleaved runs each, the median reported. The game-free test twice each, alternating. The Steam Deck check is a behaviour check with an all-or-nothing outcome: this PR twice and main once with the entry unreadable.
  • A file in docs/tested/ for each new measured run (format in docs/tested-configurations.md). docs/tested/2026-11-15-saves-move-dukenheimer-t-wyke.md, the tester's run; scripts/tested_configurations.py and tests/test_tested_configurations.py pass with it.
  • A game-free test added for the fix, failing on main and passing here.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. The change is Linux-only (saves_dir() is inside #ifndef _WIN32). Linux: the game linked in the tester's combined build (x86-64-v3, LLVM 23.1.5) and ran. After the rebase onto main aa95d35f, host/hos_main.c at 5c1b3834 checked whole (-fsyntax-only -Wall -Wextra with the host's include paths) for Linux (clang 23.1.5) and Windows (llvm-mingw 20261104): the same warnings as main aa95d35f's, none new.
**Type:** fix · **Risk:** low · **Tested on:** Linux x86-64 (game-free); Steam Deck OLED, SteamOS 3.9.5 (a tester's run) · **Needs:** none ## Why On the first start of a new version, the game moves its saves out of the old `user/` folder. If one entry could not be moved (a full disk, say), the entries moved before it stayed in the new place, and the next start kept using the old folder without them: saves and settings could seem to vanish. The tracker has no issue for it (searched). ## How to reproduce **Seen on:** Steam Deck OLED, SteamOS 3.9.5, main 7ff5256e (a tester's run; Linux only: Windows keeps `user/` beside the game) **Settings and switches:** `XDG_DATA_HOME` on another file system than the game folder (here tmpfs against ext4), so the move has to copy **Steps:** in a copy of a test game folder, put a test save in `user/` with an entry after it that cannot be read (`chmod 000`), then start the game **Expected:** the log says the saves stay in `user/`, and `user/` keeps every entry **Actual:** on main the log says the saves stay in `user/`, but the move had already taken the save: `user/` was left with only `locked/`, `temp/` and an empty `save/` account folder, and 10 save files were in the new data folder, where the game does not look ## What changed A saves move that fails part way now puts back what it had already moved, so the old `user/` is whole again and the next start uses it as before. A copied entry's old place is deleted only once every entry has arrived. - host: `saves_dir()` in `host/hos_main.c` remembers each entry it moves (renamed or copied). When one fails, the renamed ones are moved back and the copies in the new place deleted; a rename back that fails is logged (`saves: <to> could not come back to <from>`). - tests: `tests/test_linux_saves_move.py` gets a case with an entry that cannot be copied (an unreadable file stands in for a full disk), with renamed and copied entries listed before it, on one drive and across two: at both starts the old place must keep every entry and the new place stay empty, and once the entry can go, the next start moves them all. ## Measurements **Verdict:** pass, the bug is gone. On a tester's Steam Deck OLED, with an unreadable entry in `user/` and the data folder on another file system, main lost 10 save files out of `user/` into a folder the game does not read; with this PR `user/` was unchanged in both starts and the new place held no files, and once the entry was readable the next start moved everything. The game-free test fails on main and passes here. Hardware: Steam Deck OLED, AMD Custom APU 0932 (RADV VANGOGH), 16 GB, Mesa 26.2.0-devel git-5c65d4fc31, SteamOS 3.9.5 (build 20261023.100), kernel 7.2.7-valve2 (dukenheimer's run, test request T-WYKE); game-free: Linux x86-64 server, no GPU or game Scene: the first start in a copy of a test game folder on ext4 (/home), `XDG_DATA_HOME` an empty test folder on tmpfs (/tmp); `user/` held a test save (one account, four slots), an empty `temp/` and `locked/unreadable.bin` with no read permission, in that order (`save`, `temp`, `locked`), so the save went first; started headless (`HOS_HEADLESS=1`) with stderr on the journal, closed through the control bridge Method: the `saves:` lines of each start's `logs/longshot_log.txt`, and the entries, modes and sizes of `user/` and the data folder after it. This PR twice with the entry unreadable and once with it readable again (`chmod 644`), main once with it unreadable; one more start of this PR with the data folder on ext4 (a rename). A behaviour check, not a timing: the outcome of each start is all or nothing. Game-free: `python3 -I -B tests/test_linux_saves_move.py` at 692a7948 and at 6464bfde (rebased onto main aa95d35f), with gcc 13.3 and with clang 23.1.5; before that, at 3e562a68 and 4f15e996 on main 53f96d78, and before the first rebase at ccccaa8b and f7f5578a, twice each, alternating How to reproduce: build main 7ff5256e and the combined build (main 7ff5256e with https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/550 to https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/558 merged in order), both x86-64-v3 with the release play profile and LLVM 23.1.5; then the steps under How to reproduce, never in a real game folder; for the game-free check, run the test command at each commit | | Before: main @ 7ff5256e | After: this PR @ f7f5578a (in the combined build) | |---|---|---| | starts with the unreadable entry | 1 | 2 | | `user/` after them | 10 of its files gone | unchanged both times (the same 20 entries, modes and sizes) | | the new data folder after them | those 10 files | no files (only the empty `longshot/` folder the start made) | | the next start with the entry readable | not run | moved all of `user/` (the 10 save files, `temp/`, `locked/`); `saves-moved.txt` beside the game | ``` this PR, starts 1 and 2 (unreadable): saves: 1 of <game>/user's entries could not go to <data>/longshot: the saves stay in <game>/user this PR, start 3 (readable again): saves: moved from <game>/user to <data>/longshot main, start 1 (unreadable): saves: 1 of <game>/user's entries could not go to <data>/longshot: the saves stay in <game>/user this PR, data folder on ext4 too: saves: moved from <game>/user to <data>/longshot ``` Each start ran: in start 3 the frame count went from 190 to 503 in 5 s; starts 1 and 2 were closed while still preparing shaders, after the saves step, which comes first. Within one file system the move is a rename, which never reads the file, so it does not fail there. The tester's report: https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/557#issuecomment-22265 | | Before: main @ aa95d35f (with the test, 692a7948) | After: this PR @ 6464bfde | |---|---|---| | old `user/` whole and new place empty after a failed move (gcc and clang the same) | no: entries lost | yes | ``` 692a7948: test_linux_saves_move: an entry that cannot go: start 1: user/ lost or changed entries (exit 1, gcc and clang) 6464bfde: PASS: the saves' move makes a link again in the new place and leaves its folder alone, from another drive too; a linked user/ and a new place with entries keep the old place whole (exit 0, gcc and clang) ``` ### Flags and settings None. ## Testing **Tested:** Linux x86-64, game-free. The new test fails on the test-only commit 692a7948 and passes at 6464bfde, this branch rebased onto main aa95d35f, with gcc 13.3 and with clang 23.1.5 (before, it did the same at 3e562a68 and 4f15e996 on main 53f96d78, and at ccccaa8b and f7f5578a, twice each, alternating) (`python3 -I -B tests/test_linux_saves_move.py`). On `land/integration` 27553c83 (main 7ff5256e with this PR and twelve other fixes merged), the thirteen fixes' own tests and the checks their fix commits edit pass, 16 of 16; the full `python3 -I -B tests/run_all.py` ran on that branch too (see the checklist). In the game: dukenheimer ran test request T-WYKE on a Steam Deck OLED (SteamOS 3.9.5) and reported a pass on 2026-11-15 at 03:06 UTC (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/557#issuecomment-22265). The builds were the combined build of main 7ff5256e with https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/550 to https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/558 merged in order, this PR at its head then, f7f5578a (6464bfde after the rebases, the same change by `git patch-id`), and main 7ff5256e for the before; each in a copy of a test game folder, started headless with stderr on the journal, as a launcher gives it. With an unreadable entry in `user/` and the data folder on tmpfs, this PR left `user/` unchanged in both starts and the new place empty, and the third start, with the entry readable again, moved everything; main, once, logged the same "the saves stay" line while 10 save files had already gone to the new place. With the data folder on ext4 the move is a rename and succeeded (this PR, one start). The branch has been rebased since the tester's run, onto main aa95d35f, with its commits re-authored under the forge's noreply address; each commit's change is identical to before by `git patch-id`, so the commits named above are the ones tested and the result stands for this head (5c1b3834). **Not tested:** A real full disk (an unreadable file stands in for it, in the test and on the Deck). An exFAT SD card as the data folder. A start through Steam in Game Mode with a window (the Deck's starts were headless). A rename back that fails, and a crash during the deferred deletes (see Risks). Windows is not affected: `saves_dir` is built on Linux only (`#ifndef _WIN32`; Windows keeps `user/` beside the game). **Must be tested:** None ## Risks **What could break:** The rollback has limits. Copied entries' old places are deleted after all entries arrived: a crash or power loss during those deferred deletes leaves some entries in both places, and the next start then sees entries in the new place and keeps the old one, which still has everything (no save lost, the move just does not happen). A rename back that fails (rare: same drive, the name was just free) leaves that entry in the new place and logs it. If memory runs out while listing, the move is called off as if an entry failed. The *Host code builds on Windows* box is left out: the change is inside `#ifndef _WIN32`. **Turning it off:** revert the commit **Compatibility:** Nothing changes for a move that succeeds; one that fails now leaves the saves in `user/`, where the game keeps using them ## 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. - [x] Rebased on current `main`. Onto main aa95d35f with no conflict, the commits re-authored under the forge's noreply address: each of the three commits' changes (the test, the fix, the `docs/tested/` file) is identical to before by `git patch-id`. Since 53f96d78, main changed `host/hos_main.c` in https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/729 (9e6f231d), away from this PR's lines. - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). Run on this branch at ab8fcc68 on main 53f96d78, before this rebase (5c1b3834 now, the same patches by `git patch-id`; the Vulkan and SPIR-V headers from the CPM cache, 8 at once, on a loaded Linux x86-64 server): 530 passed, 15 failed, 48 skipped. The 15 are the ones that fail on main 53f96d78 in the same run: `test_appimage_player_build`, `test_bake_portability`, `test_cpm_discovery`, `test_crash_dialog`, `test_env_on`, `test_lift_cache`, `test_lift_cleanup_error`, `test_make_game_cache`, `test_pipeline_tour`, `test_readback_late`, `test_vk_pgo`, `test_vram_budget_row`, `test_walk_key`, `nx2/test_mesh_first_types.py` and `nx2/test_texture_init_barrier.py`. Main's run also timed out `test_fpx_sticky` (it passes alone there). After the rebase onto main aa95d35f, at 5c1b3834: 35 checks, this PR's test and those that read its changed files, with g++ 13.3 and with clang 23.1.5: 32 pass; the 3 that fail fail on main aa95d35f too (run alone there, both compilers): `tests/test_crash_dialog.py` (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/626), `tests/test_lift_cache.py` (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/628), `tests/test_pipeline_tour.py` (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/627). - [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] Numbers from the same PC, scene and settings, at least 2 interleaved runs each, the median reported. The game-free test twice each, alternating. The Steam Deck check is a behaviour check with an all-or-nothing outcome: this PR twice and main once with the entry unreadable. - [x] A file in `docs/tested/` for each new measured run (format in `docs/tested-configurations.md`). `docs/tested/2026-11-15-saves-move-dukenheimer-t-wyke.md`, the tester's run; `scripts/tested_configurations.py` and `tests/test_tested_configurations.py` pass with it. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. The change is Linux-only (`saves_dir()` is inside `#ifndef _WIN32`). Linux: the game linked in the tester's combined build (x86-64-v3, LLVM 23.1.5) and ran. After the rebase onto main aa95d35f, `host/hos_main.c` at 5c1b3834 checked whole (`-fsyntax-only -Wall -Wextra` with the host's include paths) for Linux (clang 23.1.5) and Windows (llvm-mingw 20261104): the same warnings as main aa95d35f's, none new.
Assisted-by: Claude
saves move: an entry that cannot go brings back the ones that went (saves_dir)
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 5s
beanstalk/approval Test farm not requested
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
f7f5578ad4
Assisted-by: Claude
Author
Contributor

Filled in: the full game-free suite on the combined branch, the Windows and Linux builds, and the test request T-WYKE. Still work in progress until a Steam Deck run is in.

Filled in: the full game-free suite on the combined branch, the Windows and Linux builds, and the test request T-WYKE. Still work in progress until a Steam Deck run is in.
Contributor

Test request T-WYKE: pass

Built in a combined build of main 7ff5256e with #550 to #558 merged in order, this PR at its head f7f5578a; main 7ff5256e for the
before. Setup below.

Result

start build the file in user/ its saves: line user/ after the test data folder after
1 this PR no read permission (chmod 000) saves: 1 of <game>/user's entries could not go to <data>/longshot: the saves stay in <game>/user unchanged only an empty longshot/ folder, no files
2 this PR no read permission the same line unchanged only the empty longshot/ folder
3 this PR readable again (chmod 644) saves: moved from <game>/user to <data>/longshot gone; saves-moved.txt beside the game all of user/: the 10 save files, temp/ and locked/
1 main no read permission the same line as starts 1 and 2 10 of its files gone those 10 files
  • This PR: both starts with the unreadable file said the saves stay in user/. user/ was unchanged both times: the same 20
    entries, with the same modes and sizes. The test data folder held no files, only the empty longshot/ folder the start made. With
    the file readable again, the third start moved the saves and user/ went.
  • Main, for the before: it logs the same "the saves stay in user/" line, but the move had already taken the save. user/ was left
    with only locked/, temp/ and an empty save/<account>/. account.sav, option.sav and all four slots (progress.sav and
    caption.sav) were in the test data folder, where the line says the game does not look.
  • The same failure within one file system: with the data folder on ext4, like the game folder, the move is a rename. It never
    reads the file, so it does not fail there: saves: moved from <game>/user to <data>/longshot, user/ gone (this PR, one start).
  • Each start ran: in start 3 the frame count went from 190 to 503 in 5 s. Starts 1 and 2 were closed while still preparing
    shaders; the saves step comes first, before that.

File systems: game folder ext4 (/home); test data folder tmpfs (/tmp), so the move had to copy and could fail on the file.

Setup

  • Steam Deck OLED: AMD Custom APU 0932 (RADV VANGOGH), 16 GB; Mesa 26.2.0-devel git-5c65d4fc31; SteamOS 3.9.5 (build 20261023.100),
    kernel 7.2.7-valve2.
  • Builds: main 7ff5256e, and the combined build: main + #550 to #558, merged in order, all clean. Both for x86-64-v3 with the
    release play profile and LLVM 23.1.5.
  • Each arm in a copy of a test game folder, never a real one. user/ beside it held a test save (one account, four slots), an
    empty temp/, and locked/unreadable.bin with no read permission. The game takes user/'s entries in the folder's own order: here
    save, temp, locked, so the save went first.
  • XDG_DATA_HOME pointed at an empty test folder each time.
  • Started headless (HOS_HEADLESS=1, no window) with stderr on the journal, as a launcher gives it, so the game wrote its own
    logs/longshot_log.txt. The saves: lines above are from it. Each start was closed through the control bridge.
Test request T-WYKE: pass Built in a combined build of main 7ff5256e with #550 to #558 merged in order, this PR at its head f7f5578a; main 7ff5256e for the before. Setup below. **Result** | start | build | the file in user/ | its `saves:` line | user/ after | the test data folder after | |---|---|---|---|---|---| | 1 | this PR | no read permission (chmod 000) | `saves: 1 of <game>/user's entries could not go to <data>/longshot: the saves stay in <game>/user` | unchanged | only an empty `longshot/` folder, no files | | 2 | this PR | no read permission | the same line | unchanged | only the empty `longshot/` folder | | 3 | this PR | readable again (chmod 644) | `saves: moved from <game>/user to <data>/longshot` | gone; `saves-moved.txt` beside the game | all of user/: the 10 save files, `temp/` and `locked/` | | 1 | main | no read permission | the same line as starts 1 and 2 | **10 of its files gone** | **those 10 files** | - **This PR:** both starts with the unreadable file said the saves stay in user/. user/ was unchanged both times: the same 20 entries, with the same modes and sizes. The test data folder held no files, only the empty `longshot/` folder the start made. With the file readable again, the third start moved the saves and user/ went. - **Main, for the before:** it logs the same "the saves stay in user/" line, but the move had already taken the save. user/ was left with only `locked/`, `temp/` and an empty `save/<account>/`. `account.sav`, `option.sav` and all four slots (`progress.sav` and `caption.sav`) were in the test data folder, where the line says the game does not look. - **The same failure within one file system:** with the data folder on ext4, like the game folder, the move is a rename. It never reads the file, so it does not fail there: `saves: moved from <game>/user to <data>/longshot`, user/ gone (this PR, one start). - Each start ran: in start 3 the frame count went from 190 to 503 in 5 s. Starts 1 and 2 were closed while still preparing shaders; the saves step comes first, before that. **File systems:** game folder ext4 (/home); test data folder tmpfs (/tmp), so the move had to copy and could fail on the file. **Setup** - Steam Deck OLED: AMD Custom APU 0932 (RADV VANGOGH), 16 GB; Mesa 26.2.0-devel git-5c65d4fc31; SteamOS 3.9.5 (build 20261023.100), kernel 7.2.7-valve2. - **Builds:** main 7ff5256e, and the combined build: main + #550 to #558, merged in order, all clean. Both for x86-64-v3 with the release play profile and LLVM 23.1.5. - **Each arm in a copy of a test game folder**, never a real one. user/ beside it held a test save (one account, four slots), an empty `temp/`, and `locked/unreadable.bin` with no read permission. The game takes user/'s entries in the folder's own order: here `save`, `temp`, `locked`, so the save went first. - **XDG_DATA_HOME** pointed at an empty test folder each time. - **Started headless** (`HOS_HEADLESS=1`, no window) with stderr on the journal, as a launcher gives it, so the game wrote its own `logs/longshot_log.txt`. The `saves:` lines above are from it. Each start was closed through the control bridge.
zeldaootr force-pushed saves-move-partial-failure from f7f5578ad4
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 5s
beanstalk/approval Test farm not requested
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
to ab8fcc6823
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 15s
Website / Website on Cloudflare (pull_request_target) Has been skipped
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Guide screenshots / Guide images and local UI source review (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
2026-11-15 05:35:29 +00:00
Compare
Author
Contributor

Thank you, dukenheimer, for T-WYKE on the Steam Deck (#557 (comment)), main's lost saves included.

Rebased onto main 53f96d78 with no conflict: the test and fix commits are the same as before (git range-diff: ccccaa8b is now 3e562a68, f7f5578a is now 4f15e996), and the run is added as docs/tested/2026-11-15-saves-move-dukenheimer-t-wyke.md in a third commit, ab8fcc68. The description now ticks the rebase and docs/tested/ boxes, and has the host code box (the change is Linux-only, inside #ifndef _WIN32).

Tests run again after the rebase: tests/test_linux_saves_move.py fails at 3e562a68 (an entry that cannot go: user/ lost entries at start 1) and passes at the tip, with gcc 13.3 and with clang 23.1.5. The full python3 -I -B tests/run_all.py on this branch: 530 passed, 15 failed, 48 skipped, the same 15 that fail on main 53f96d78 in the same run (named in the checklist). host/hos_main.c checked whole for Linux and Windows with the build's host flags: the same warnings as main's, none new.

Why the Deck result still holds: main changed nothing in saves_dir() or the lines that call it. Since the tested base 7ff5256e, main changed host/hos_main.c in three commits, all elsewhere in the file: the crash window's helper and watcher (a0dac82e; the watcher is off headless, as the tester's starts were), HOS_HLE_STATS (1c9958c1, off unless set) and the removal of the Windows write watch and LONGSHOT_OCCLUSION_OFF (414f56e2). None of them reads or moves user/ or the data folder.

Thank you, dukenheimer, for T-WYKE on the Steam Deck (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/557#issuecomment-22265), main's lost saves included. Rebased onto main 53f96d78 with no conflict: the test and fix commits are the same as before (`git range-diff`: ccccaa8b is now 3e562a68, f7f5578a is now 4f15e996), and the run is added as `docs/tested/2026-11-15-saves-move-dukenheimer-t-wyke.md` in a third commit, ab8fcc68. The description now ticks the rebase and `docs/tested/` boxes, and has the host code box (the change is Linux-only, inside `#ifndef _WIN32`). Tests run again after the rebase: `tests/test_linux_saves_move.py` fails at 3e562a68 (an entry that cannot go: `user/` lost entries at start 1) and passes at the tip, with gcc 13.3 and with clang 23.1.5. The full `python3 -I -B tests/run_all.py` on this branch: 530 passed, 15 failed, 48 skipped, the same 15 that fail on main 53f96d78 in the same run (named in the checklist). `host/hos_main.c` checked whole for Linux and Windows with the build's host flags: the same warnings as main's, none new. Why the Deck result still holds: main changed nothing in `saves_dir()` or the lines that call it. Since the tested base 7ff5256e, main changed `host/hos_main.c` in three commits, all elsewhere in the file: the crash window's helper and watcher (a0dac82e; the watcher is off headless, as the tester's starts were), `HOS_HLE_STATS` (1c9958c1, off unless set) and the removal of the Windows write watch and `LONGSHOT_OCCLUSION_OFF` (414f56e2). None of them reads or moves `user/` or the data folder.
zeldaootr changed title from WIP: fix(linux): a saves move that fails part way brings back what it moved to fix(linux): a saves move that fails part way brings back what it moved 2026-11-15 05:35:30 +00:00
zeldaootr force-pushed saves-move-partial-failure from ab8fcc6823
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 15s
Website / Website on Cloudflare (pull_request_target) Has been skipped
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Guide screenshots / Guide images and local UI source review (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
to 5c1b3834be
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
Guide screenshots / Guide images and local UI source review (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 4s
2026-11-15 17:11:51 +00:00
Compare
Author
Contributor

Rebased onto main aa95d35f (head 5c1b3834); the commits are re-authored with the forge's noreply address: no code change, the same patches by git patch-id.
Tests re-run on the new base, gcc 13.3 and clang 23.1.5: test_linux_saves_move fails at the test-only commit 692a7948 and passes at 6464bfde; of the 35 checks that are this PR's or read its files, 32 pass and the 3 that fail fail on main too. The Deck run (T-WYKE) stands; the description names the new commits.

Rebased onto `main` aa95d35f (head 5c1b3834); the commits are re-authored with the forge's noreply address: no code change, the same patches by `git patch-id`. Tests re-run on the new base, gcc 13.3 and clang 23.1.5: `test_linux_saves_move` fails at the test-only commit 692a7948 and passes at 6464bfde; of the 35 checks that are this PR's or read its files, 32 pass and the 3 that fail fail on main too. The Deck run (T-WYKE) stands; the description names the new commits.
HumanBean approved these changes 2026-11-15 19:03:53 +00:00
HumanBean left a comment

Code review only (no tests run). Read the saves_dir() change in host/hos_main.c.

  • Order is right: every entry is looked at, then moved and recorded; the old place of a copied entry is deleted only after all arrived, and on a failure renamed entries go back and copies in the new place are deleted.
  • Allocation failure is handled (went is updated whenever realloc succeeds, a failed strdup counts as kept), no leak or use after free on the paths I followed. The (move && kept) skip stops further moves after the first failure.
  • Linux-only (inside #ifndef _WIN32), so no Windows concern. The tester's Steam Deck run and the test fit the description.

Approving; nothing needs to change.

Code review only (no tests run). Read the `saves_dir()` change in `host/hos_main.c`. - Order is right: every entry is looked at, then moved and recorded; the old place of a copied entry is deleted only after all arrived, and on a failure renamed entries go back and copies in the new place are deleted. - Allocation failure is handled (`went` is updated whenever realloc succeeds, a failed `strdup` counts as kept), no leak or use after free on the paths I followed. The `(move && kept)` skip stops further moves after the first failure. - Linux-only (inside `#ifndef _WIN32`), so no Windows concern. The tester's Steam Deck run and the test fit the description. Approving; nothing needs to change.
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
Guide screenshots / Guide images and local UI source review (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 4s
This pull request can be merged automatically.
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 'saves-move-partial-failure:zeldaootr-saves-move-partial-failure'
git switch 'zeldaootr-saves-move-partial-failure'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 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!557
No description provided.