feat(paths): saves and settings in a per-user data folder, caches and logs in the game folder's cache/ and logs/ #2317
Open
LNDF wants to merge 9 commits from
LNDF/structure into main
pull from: LNDF/structure
merge into: MagicBeanDev:main
MagicBeanDev:main
MagicBeanDev:github/pr/2893
MagicBeanDev:github/pr/2890
MagicBeanDev:github/pr/2887
MagicBeanDev:github/pr/2878
MagicBeanDev:github/pr/2875
MagicBeanDev:github/pr/2874
MagicBeanDev:github/pr/2873
MagicBeanDev:github/pr/2871
MagicBeanDev:github/pr/2756
MagicBeanDev:github/pr/2897
MagicBeanDev:github/pr/2896
MagicBeanDev:github/pr/2718
MagicBeanDev:github/pr/2842
MagicBeanDev:github/pr/2691
MagicBeanDev:github/pr/2895
MagicBeanDev:github/pr/2743
MagicBeanDev:github/pr/2765
MagicBeanDev:github/pr/2708
MagicBeanDev:github/pr/2732
MagicBeanDev:github/pr/2755
MagicBeanDev:github/pr/2817
MagicBeanDev:github/pr/2876
MagicBeanDev:release-please--branches--main
MagicBeanDev:release-please--branches--main--release-notes
MagicBeanDev:github/pr/2778
MagicBeanDev:github/pr/2888
MagicBeanDev:github/pr/2866
MagicBeanDev:github/pr/2877
MagicBeanDev:github/pr/2800
MagicBeanDev:github/pr/2829
MagicBeanDev:github/pr/2850
MagicBeanDev:github/pr/2889
MagicBeanDev:github/pr/2882
MagicBeanDev:github/pr/2860
MagicBeanDev:github/pr/2849
MagicBeanDev:github/pr/2886
MagicBeanDev:github/pr/2794
MagicBeanDev:github/pr/2840
MagicBeanDev:github/pr/2830
MagicBeanDev:github/pr/2834
MagicBeanDev:github/pr/2719
MagicBeanDev:github/pr/2742
MagicBeanDev:github/pr/2741
MagicBeanDev:github/pr/2820
MagicBeanDev:github/pr/2771
MagicBeanDev:github/pr/2859
MagicBeanDev:github/pr/2858
MagicBeanDev:github/pr/2823
MagicBeanDev:github/pr/2757
MagicBeanDev:github/pr/2769
MagicBeanDev:github/pr/2844
MagicBeanDev:github/pr/2843
MagicBeanDev:github/pr/2673
MagicBeanDev:github/pr/2855
MagicBeanDev:github/pr/2739
MagicBeanDev:github/pr/2807
MagicBeanDev:github/pr/2851
MagicBeanDev:github/pr/2789
MagicBeanDev:github/pr/2814
MagicBeanDev:github/pr/2818
MagicBeanDev:github/pr/2774
MagicBeanDev:github/pr/2772
MagicBeanDev:github/pr/2750
MagicBeanDev:github/pr/2786
MagicBeanDev:github/pr/2785
MagicBeanDev:github/pr/2782
MagicBeanDev:github/pr/2779
MagicBeanDev:github/pr/2775
MagicBeanDev:github/pr/2706
MagicBeanDev:github/pr/2766
MagicBeanDev:github/pr/2747
MagicBeanDev:github/pr/2759
MagicBeanDev:github/pr/2745
MagicBeanDev:github/pr/2740
MagicBeanDev:github/pr/2733
MagicBeanDev:github/pr/2725
MagicBeanDev:github/pr/2727
MagicBeanDev:github/pr/2728
MagicBeanDev:github/pr/2729
MagicBeanDev:github/pr/2721
MagicBeanDev:github/pr/2722
MagicBeanDev:github/pr/2715
MagicBeanDev:github/pr/2714
MagicBeanDev:github/pr/2707
MagicBeanDev:github/pr/2703
MagicBeanDev:github/pr/2705
MagicBeanDev:github/pr/2697
MagicBeanDev:github/pr/2679
MagicBeanDev:github/pr/2676
MagicBeanDev:github/pr/2669
MagicBeanDev:docs/what-hookshot-connects-to
MagicBeanDev:fix/hdr-encode-test
MagicBeanDev:fix/main-tests
MagicBeanDev:fix/play-cache-files
MagicBeanDev:perf/game-barrier-order
MagicBeanDev:feat/heap-lazy
MagicBeanDev:feat/texture-detail
MagicBeanDev:perf/astc-opaque-bc1
MagicBeanDev:fix/fs-dir-lock
MagicBeanDev:fix/framegen-native-kept
MagicBeanDev:fix/update-no-downgrade
MagicBeanDev:port/k10-gcn-f16-mirrors
MagicBeanDev:fix/framegen-mult-cap
MagicBeanDev:chore/release-as-2.1.1
MagicBeanDev:perf/pcache-load-switch
MagicBeanDev:fix/prepare-progress-log
MagicBeanDev:fix/update-closes-stale-game
MagicBeanDev:fix/hdr-scaled-sampled
MagicBeanDev:perf/first-start-preset
MagicBeanDev:perf/zero-upload
MagicBeanDev:fix/vcore-escape-2s
MagicBeanDev:test/fake-gh-atomic
MagicBeanDev:perf/batch-idle-flush
MagicBeanDev:test/finish-stub-a
MagicBeanDev:chore/record-2.1.0
MagicBeanDev:fix/main-red-tests
MagicBeanDev:fix/test-display-startup-driver-cache
MagicBeanDev:feat/mesh-shader-mode
MagicBeanDev:fix/menu-probes-at-startup
MagicBeanDev:fix/fps-counter-classic
MagicBeanDev:fix/test-log-batch-flake
MagicBeanDev:fix/narrow-screen-hud
MagicBeanDev:ci/prereleases
MagicBeanDev:release/v2.0.15
MagicBeanDev:hotfix/v2.0.15
MagicBeanDev:fix/present-ahead-replay
MagicBeanDev:release/v2.0.13
MagicBeanDev:fix/bracket-paths-2013
MagicBeanDev:fix/hdr-overlays-2013
MagicBeanDev:fix/fps-counter-scale-2013
MagicBeanDev:fix/stick-seen-at-rest
MagicBeanDev:hotfix/menu-fits-screen
MagicBeanDev:fix/f1-always-opens-menu
MagicBeanDev:fix/ini-keep-lines
MagicBeanDev:claude/cranky-pike-7cf247
MagicBeanDev:perf/texture-answer-flat
MagicBeanDev:fix/stamp-test-modules
MagicBeanDev:fix/appimage-fonts
MagicBeanDev:fix/display-awake
MagicBeanDev:release/v2.0.12
MagicBeanDev:fix/discord-windows
MagicBeanDev:rc/v2.0.12-tests
MagicBeanDev:fix/dlss-keep-feature
MagicBeanDev:rc/v2.0.12
MagicBeanDev:button-icons
No reviewers
Labels
Clear labels
accessibility
Barrier affecting people with disabilities
autorelease: pending
autorelease: tagged
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
github_actions
Pull requests that update GitHub Actions code
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
testing wanted
wontfix
This will not be worked on
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
No assignees
2 participants
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/Hookshot!2317
Loading…
Reference in a new issue
No description provided.
Delete branch "LNDF/structure"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Saves, settings, logs and several GB of shader caches were all written loose into the game folder (on Windows even the saves), mixed in with what the build made. This sorts them: your saves and settings go to a per-user data folder that survives rebuilding or deleting the game folder, and the caches and logs go into two clearly named folders of the game folder.
What changed
Where files go
save/,mouse_sens.txt, …)<game>\user(Windows),~/.local/share/hookshot(Linux)%LOCALAPPDATA%\Hookshot/$XDG_DATA_HOME/hookshothookshot_options.ini,hookshot_env.txt,hookshot_update_skip.txthookshot_env.txtbeside the game is still read when the data folder has none)hookshot_driver_faults.txt<game>/cache/(safe to delete; one per game folder, so a release and a nightly don't invalidate each other)<game>/prompts/<game>/cache/prompts/hookshot_log*.txt,hookshot_startup.log,hookshot_frames.csv<game>/logs/language.txt,hookshot_shaders.bin, shipped listsExceptions:
--user <folder>sets the data folder; a run given an explicitimage.binuses./user..hookshot_builder_homein the game folder, #2367): the data folder is<game>\user, so saves go with the drive. Only the settings move into it; nothing goes to%LOCALAPPDATA%.<game>\user.Code
host/hos_data.c/.h: picks the data folder once at start (beforehookshot_env.txtand the log), makescache/andlogs/, and moves an older build's files there on the first start. Replaces Linux'ssaves_dir()inhos_main.c.userthat is a link itself is used as it is. On Windows a link or junction keeps the saves where they are.saves-in-two-places.txtsays where both are and how to switch.userthat can't be fully deleted is renameduser.moved.files-moved.txtsays what moved; the log also says when a settings file beside the game is not read.nn_applet,nn_hid,nn_audio,discord_rpc,prompts,hos_main,hos_update) usehos_data_path().MkwVkHost.data_dir(MKWVK_API_VERSION19);vk_paths.hgetsdata_path(),cache_path(),log_path(), used for the options file, the program cache, pipeline cache, notes, state list, prepared record, driver faults and frame trace.mkw_vk_pipelines*.bin→hookshot_vk_pipelines*.bin; existing ones are renamed as they move, so nothing is rebuilt.make_game.pyseeds the recorded pipeline list intocache/hookshot_pipelines.bin, asmainseeds the game folder's.scripts/oot_saves.pyfinds Windows saves in%LOCALAPPDATA%\Hookshotwithout--hookshot.Split out at review: the Linux builder's DLSS SDK move is #2485 (merged); its Windows counterpart is #2528.
Testing
python -I -B tests/run_all.pyon Linux: same results asmain. #2282'stests/test_linux_saves_move.pyruns all its cases onhos_data.c. Newtests/test_data_dir.pybuildshos_data.cand covers folder choice, the migration, thebin/layout, a second game folder, a relative link on the same drive, the portable rule, the "not read" log, and copies across file systems (rollback, links, read-only originals).hookshot_vk.socompiled and linked in an Ubuntu 24.04 container; Linux AppImage built.main). The Windows save move across drives is the main thing to check.🤖 Generated with Claude Code
Once we update to a major versioon, we can remove the migration code
Heads-up on an integration point with #2367 (portable Windows Builder): it keeps the Builder home, settings/work, updater and game-to-Builder association relocatable. This PR changes the game's save/settings location, so we should agree the behavior when a portable-built game starts: shared per-user data by default versus an explicit game-local
--userprofile. I don't want to change the default save policy implicitly or migrate existing saves twice. Could we coordinate that contract and the migration tests before either branch absorbs the other's path changes? The launcher proposals #2373 and #2403 also need to consume the same decision.<@324633150510727178> about the comment on #2317 and the portable/launcher PRs. here's what I'm thinking:
if that sounds ok I'll add the portable part to my PR so you can build on it
Separating the player's files from the build's is the right direction, and the save move is careful in the important places: the first pass checks before anything moves, a failure rolls back,
delete_treenever enters a link, anduser.movedstops a later start from reading leftovers as saves.MkwVkHost.data_diris added at the end withMKWVK_API_VERSIONbumped, and paths stay UTF-8 (GetEnvironmentVariableW+CP_UTF8, under the exe's UTF-8 manifest). I merged the branch with current main (no conflicts) and ran the checks it touches:test_data_dir,test_env_file,test_linux_update_skip,test_oot_saves,test_pipeline_play_cache,test_pipeline_notes_open,test_menu_env_on,test_mods_size_table,test_make_game_ngx,test_utf8_paths,test_prompts_live, the driver-fault and precompile checks and a few more. All pass. I focused on the save and settings migration, since that's where a mistake costs players data. Findings:blocking: the Windows save move hasn't been run. This PR moves every Windows player's saves for the first time, from
<game>\userto%LOCALAPPDATA%\Hookshot, usually across drives, so throughcopy_tree+delete_tree. Windows is most of the player base, and you list it yourself as not yet done. Please run at least: game folder on a second drive; a save file held open or read-only (to exercise rollback anduser.moved); a non-ASCII user name; and a second start afterwards. Also, only the title check has run on0981ace7. The full game-free suite and the Windows Vulkan build still have to go green on this head.blocking
host/hos_data.c:163-164:move_entrytriesMoveFileExAbeforetree_has_link. On the same drive, an entry that is a symbolic link is therefore renamed as it stands, and a relative one (user/save -> ../../sync/save) names nothing from~/.local/share/hookshot: the player sees no saves and can't write any. This is the bug #2282 fixes in the code this PR replaces. Checkingtree_has_link(from)before the rename (keep the saves where they are, as you already do for the copy) or carrying over #2282's re-linking would close it. Whichever of the two PRs lands second needs to carry the other's handling and its test (test_linux_saves_move.pycutssaves_dirout ofhos_main.c, which this PR deletes).should fix
host/hos_data.c:294-305: when a game folder's saves can't move (the usual case for a second game folder, such as a nightly beside a release, whoseuser/savecollides with the first one's),hos_user_dirstays<game>/user, butdata_diris still the shared folder. That folder'shookshot_options.inialso collides, somove_fileleaves it loose in the game folder, where nothing reads it any more. That game then runs with its own saves but the other folder's settings, and its own settings are silently dropped. Please either keep saves and settings together (when the saves stay,data_dirstays too) or log clearly that the game folder'shookshot_options.iniis ignored. The same applies tohookshot_env.txt: once the data folder has one, a file put beside the game, which is what issues and Discord have told players to do until now, is ignored without a word (hos_main.cenv reading). A log line when both exist would save support time.should fix: the scope is large for one PR (CONTRIBUTING, "one topic"). The saves/settings move, the cache/ and logs/ split with the pipeline-cache rename, the shipped-notes semantics (
seed_pipelinesno longer overwrites the notes,notes_adopt_shippedplus.taken), and the Linux builder's NGX SDK relocation can each break separately and would be much easier to review, test and revert apart. At minimum I'd take the shipped-notes change and the NGX SDK move out into their own PRs.question: the portable-builder contract with #2367/#2373/#2403 (Arkex-ai's comment, your reply) is still open. Merging this first makes "per-user data folder" the default for portable builds too, until the follow-up lands. Is that acceptable to them, or should the portable check (
.hookshot_builder_home) be in this PR?minor
host/hos_data.c:168,211: if an entry's copy succeeded but its original couldn't be fully deleted (left), and a later entry fails, the rollback finds the partial original still there (exists(to)) and keeps both. The game then runs from<game>/userwith that entry incomplete, while the complete copy sits in the data folder. Nothing is lost, but the player sees missing saves. The rollback could delete the partial original before moving the complete copy back.minor: the PR description says
MKWVK_API_VERSION16, but the code is 18, and #2261 also takes 18 forMkwVkHost.wheel, so whichever merges second has to go to 19. Also,nx2/make_game.py:1021says the notes are in "the data folder's shaders/hookshot_pipelines.bin", butpipe_notes_path()usescache/hookshot_pipelines.bin.Thanks for the review! Pushed
469aff4a:user/save -> ../../synced-saveson the same drive. Whichever of this and #2282 lands second carries the other's test over.<game>\user, so the folder keeps its own saves and settings. The log now also says when ahookshot_options.iniorhookshot_env.txtbeside the game isn't read..hookshot_builder_homein the game folder, the data folder is<game>\user, only the settings move into it, and nothing goes to%LOCALAPPDATA%. Same proposal I sent Arkex-ai.make_game.pyseedscache/hookshot_pipelines.binthe same waymainseeds the game folder.make_game.pydocstring fixed.Still open: the Windows run (second drive, held/read-only save, non-ASCII user name, second start). I'll post results when I have them.
The earlier points are addressed in
469aff4a. Two things remain:host/hos_data.cmove_saves/migrate: a player who goes back to an older build in the same folder gets a fresh<game>\user\save. On the next upgrade that collides, so all saves stay in<game>\userand the game shows the new ones, while the migrated saves sit unseen in%LOCALAPPDATA%\Hookshot. Nothing is deleted, butfiles-moved.txtisn't written and only the log mentions it. This needs handling or a release note.Coordination:
docs/saves.md.MKWVK_API_VERSION18 is also claimed by #2261, #2459, #2478 and #2479.hos_data.c.hookshot_mirror_skip.txtis written loose in the game folder, and belongs undercache/once this lands.Thanks! Pushed
2b32b1c3(merged with main):<game>\userand the data folder have saves, the game folder's are still used and nothing is merged or picked, but it now writessaves-in-two-places.txtin the game folder (both paths, and how to switch: renameusertouser.oldand start again) and logs it. Mentioned in the readme too. Tested, including switching back to the moved saves.hos_data.c. On Linux a link in the saves is made again in the data folder to the same folder (never renamed or copied through), links to entries that move follow them, and auserthat is a link itself is used as it is. On Windows a link or junction still keeps the saves where they are.test_linux_saves_move.pynow buildshos_data.cand all its cases pass, the other-drive ones included.docs/saves.mdfixed in the merge.hookshot_mirror_skip.txttocache_path()and add it to the files the first start moves. If this lands first, it should usecache_path()there.builder/py).About the Windows run: that one will have to wait a bit. I don't have a good Windows machine or a second drive to test with right now. I'll post the results (second drive, held/read-only save, non-ASCII user name, second start) once I can, or if someone with that setup can try it before, even better.
This now conflicts with main in
docs/development.md. Could you merge main into the branch (or rebase) when you get a chance? I'll take another look once it's up to date.Merged main (
26793b60);docs/development.mdwas the only conflict. The merge also brought #2447's Save my pipeline list:nx2/pipeline_share.pynow reads the list fromcache/, the log fromlogs/and the options from the data folder (old places as fallback), with a test.One new integration point: #2600 appends
romfs_readtoMkwVkHostand advances main's API18 to19. This PR appendsdata_diras19; #2594 appendstake_textas20. Whichever lands second needs the complete callback layout and a version bump from the landed head, including matching host initializers. I'll adapt #2600 if either lands first and rerun its startup/font/filesystem checks. The packed backend uses a separate base-asset callback; your data-location policy remains yours.github-sync referenced this pull request2026-11-12 06:22:55 +00:00
Most of what I raised is fixed: links are never moved (#2282's handling and test carried in), a second game folder keeps saves and settings together, the rollback clears a partial original first, the portable rule is in, and
saves-in-two-places.txt(host/hos_data.c:390) covers the downgrade case. API 20 is right against main's 19.Two things still block it:
tests/test_pipeline_share.py(:413). The test setsXDG_DATA_HOME(:409), but on Windowsdata_dirs()innx2/pipeline_share.pyreads%LOCALAPPDATA%\Hookshot, so the options aren't found (2048x1152). Set and restoreLOCALAPPDATAthere too.I merged it into current main (clean) and ran the 20 checks it touches on Linux (
test_data_dir,test_linux_saves_move,test_pipeline_share,test_env_file,test_utf8_paths,test_prompt_skipamong them): all pass.Native Windows migration evidence at
47786aaf7d4dca7e1a54434b187a38775e47e363I exercised the unchanged
host/hos_data.cfrom this exact head in a small guarded standalone executable using real Win32/CRT calls and the production UTF-8 manifest (GetACP() == 65001). Game-free source qualification only: no game launch, real player saves or installation changes. The PR head still matches at this pre-publication check.The extended set has 8 migration cases: 5 conforming and 3 functional failures, plus a separate positive notification witness. All payloads are authored synthetic data, at most 88 bytes per case, under isolated fake executable/CWD/LOCALAPPDATA roots on verified distinct volumes. A native exit0 did not establish a complete active data directory.
1. Cross-volume rollback selects a partial source — two cases.
The actual enumeration was
a-tree, thenz-tree, each holding two files. The first entry copies completely to C:, but deleting its original removes the free file and leaves the other file: one variant uses READONLY, the other a realCreateFileW(GENERIC_READ, FILE_SHARE_READ)handle without DELETE sharing. A separate share-zero handle makes the later entry's copy fail.Rollback cannot remove the refractory original, so the complete first-entry copy remains on C:. Nevertheless, both
data=anduser=select D:game/user, now 3/4 files. C: has only the first entry. Neither root alone is complete; their union retains all original sizes/hashes. Recoverability of that union is not a complete active directory or successful all-or-nothing restoration. Relevant paths:hos_data.c:218-221,278-287,381-405.2. A residual source becomes active on the next startup — one case.
Without the later copy failure, the share-READ/no-DELETE case copies 4/4 files to C: and initially selects that complete directory. D: retains one held file and putting
useraside fails. After the handle is closed, a fresh second process returns0 but selects D:game/userwith 1/4 files, while the full C: copy remains intact. File hashes are unchanged between starts; the selected directory is not. Relevant paths:289-301,258-262,381-405.The raw harness had called this last case
PASS_MIGRATIONbecause it checked first-start completeness and unchanged snapshots. A separate adjudication records the functional FAIL; raw receipts remain unchanged.Positive controls and verification: share-zero refusal preserves the source payload; same-volume rename can succeed despite a READONLY file; ordinary cross-volume migration and the simple READONLY-copy case remain complete across two processes. A simple same-volume rollback was causally witnessed with real
ReadDirectoryChangesW: source REMOVED→ADDED and destination ADDED→REMOVED, alongside an independent add/rename/delete witness. Final hashes alone were not used to infer rollback.An independent read-only Windows capture matched all 72 native receipts by name, size and SHA256. The parent then replayed the semantic verifier offline and directly cross-compared all72 exports against that capture: the three failures above remain confirmed. Synthetic fixtures/recovery copies are retained; no repair was applied.
Limits: SSH/non-interactive account, not an actually non-ASCII Windows account or Interactive/Limited token; no full Windows CI/
test_pipeline_share.py, real save serialization, GPU/gameplay, directory ACL, disk-full/crash or hostile-reparse qualification. Linux/Windows wall clocks were not combined. No binaries or game-derived data were exported. This does not close the outstanding Windows request or the separate LOCALAPPDATA fixture issue.Could migration keep a complete active directory when rollback or setting aside a residual source fails, rather than returning initialization success with a partial source selected?
AI-assisted evidence analysis and review: Hermes Agent (OpenAI GPT-6.1-sol).
@Nitnek @Joey-Magnus thanks for the reports. All three cases are fixed in d2b65510:
<game>\user. The originals are only deleted after everything got across..hookshot-movedrecord in the data folder. On later starts, a<game>\userwith no file newer than that record is treated as a leftover. It gets set aside asuser.moved(or left alone if it's still held) and the data folder stays in use.test_pipeline_sharenow setsLOCALAPPDATAtoo, so it no longer reads the runner's real folder on Windows.tests/test_data_dir.pyhas two new cases that reproduce them: a copy that fails after other entries moved, and a leftover that can't be deleted on the first start. The old code fails both.Could you rerun your Windows harness on the new head, Nitnek? And Joey, could you take another look when you have a moment? I still can't do a native run myself (no good Windows PC or second drive), so your results are what I'm going on here.