feat(linux-launcher): add profiles, each with saves of its own #1958
Open
timstr97 wants to merge 10 commits from
timstr97/linux-profiles into main
pull from: timstr97/linux-profiles
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!1958
Loading…
Reference in a new issue
No description provided.
Delete branch "timstr97/linux-profiles"
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?
On Linux everyone who plays on one PC shares one set of saves, and moving a save over from an emulator means running
scripts/oot_saves.pyby hand. This PR adds profiles to the Linux launcher, so each player gets their own saves, and lets the launcher import a save from an emulator in a few clicks.Issues
scripts/oot_saves.pyby hand).What changed
Launcher (
builder/linux/hookshot_builder.py, newbuilder/linux/profiles.py)profile=inbuilder.ini).save-backups/<id>-<time>: nothing is deleted. The default profile cannot be removed, because the game plays it when it is started without the launcher.profiles.dat). Other folder… takes any other folder. When a folder holds saves of several emulator users, the launcher asks whose. The copy runs on a worker thread behind a "Copying the save…" screen, and a failure is shown in an error box.Hookshot save - <name> - <date and time>. It never writes into an emulator and replaces nothing.hookshot_version.txthasprofiles=1, whichnx2/make_game.pynow writes on every build. An older folder plays the default profile's saves, as before.Game (
host/nn_applet.c,host/hos_main.c)HOOKSHOT_PROFILE=<32 hex digits>picks the user, and so the save foldersave/<id>. The game reads it once at start-up (hos_account_init), after the user folder is known.profiles.txtin the user folder (a profile removed since it was chosen, or one from another user folder) falls back to the default profile, as does a missing or malformed value. The default profile's uid is unchanged, so existing saves stay where they were.hookshot-launchpasses the ini'sprofile=on asHOOKSHOT_PROFILE. Whether it is still on the list is the game's to check. It also starts withHOMEunset.scripts/oot_saves.pysavescommand: the saves of a folder, or of every emulator's usual folder, as JSON.listshows users' names.--hookshotcan name one profile's save folder, which an import creates if it isn't there yet.load/<title id>) is no longer taken for a user. User folders copied into a folder of any name are still found by their ids.Docs: readme ("Profiles, and saves from emulators"),
docs/saves.md("Copy from Hookshot's launcher"), and theHOOKSHOT_PROFILErow indocs/development.md.How it was tested
python -I -B tests/run_all.pyon CachyOS: 331 passed, 2 failed, 13 skipped. Both failures also happen onmain:test_wayland_libdecor_hint.py(-Wunused-but-set-globalin its harness with this clang) andnx2/test_nup_read_name.py(zstandardis not installed).test_launcher_profiles.py: add, rename, remove, ids never reused, a list in another encoding, import and export, a failed import shown as an error, an id starting with-.test_account_profile.py: builds the realhos_account_init, covering listed and unlisted ids, case, comments and malformed values.test_hookshot_launch.py: the profile passed on, and a start withoutHOME.test_oot_saves.py:saves, a new profile's folder, empty files, a failed copy restored, Eden's mods folder, copied user folders.v2.0.8-604-g829b783(this branch before its review fixes): Play starts the game with the profile chosen in the launcher, and the game plays that profile's saves. Four starts, two profiles (account: profile …0002,…0003inhookshot_log.txt). A row is added todocs/tested-configurations.md.Not tested
hos_account_init, read once at start-up, and its fall-back to the default profile for an id that is not inprofiles.txt. The game run above predates them;test_account_profile.pybuilds and checks that code.host/change. The Windows launcher has no profiles yet and plays the default profile.Screenshots
The launcher, with a profile chosen:
The profile list, and the Manage menu for the chosen profile:
New profile:
Import a save, from Eden with two users:
After the import:
Read through all of it (game side, launcher,
profiles.py,oot_saves.py, tests). The design holds up, and I found no bug in the code I could check by reading. It can't merge yet, though.Blocking:
main(GitHub says dirty, 45 commits behind). Files touched on both sides:builder/linux/hookshot_builder.py,host/hos_main.c,host/hos_update.c,nx2/make_game.py,readme.md,docs/development.md. Please rebase and reruntests/run_all.py.action_required(first-time fork approval), so "331 passed" rests on your local run only. The PR needs a maintainer to approve the workflows once it is rebased.What I checked and found fine:
hos_account_initmatchesnn_fs.c(second word, then first), and the default uid stays5a3c...0001, so existing saves stay put.hos_account_initruns afterhos_user_diris set (thesaves_dirresult) and before the game runs.profile_listedandprofiles.loadagree: id in any case, a space (not a tab) before the name,#comments skipped, all-zero id refused, long lines handled.profiles=1first inhookshot_version.txtis safe. Every reader (hos_update.c,linux_update.py,core.py,MainWindow.xaml.cs) looks forversion=by key. A source build now gets a file with no version line, so it still never offers an update.profiles.user_dirfollowssaves_dirinhos_main.c, including the beside-folder case.RUN_ALL_values correctly.test_account_profile.pyneeds gcc and is skipped without it.Questions and nits, none blocking:
empty_files()makescopy_savesrefuse a save with any zero-byte file under a slot. Guarding against a truncated copy is a good idea. I couldn't confirm that real emulator saves never hold a legitimately empty file (a marker, say), which would make the import refuse a good save. Does Eden or NX2navi write any? If you know they don't, ignore this.hookshot-launchwithHOMEunset and noXDG_CONFIG_HOMEnow reads/.config/hookshot/builder.ini. It is harmless, but a plain "no ini" would be cleaner.host/change (it is plain C, nowindows.hnames, andhos_account_initonly reads an env var), and the fix-up code in the game after review. Both stay unchecked for me too.Thanks for reading all of it. Point by point:
Conflicts. I merged
main(e19d969) into the branch instead of rebasing, because the branch already had merges frommainand a rebase would rewrite commits you have reviewed. The PR is mergeable now. Three files conflicted:builder/linux/hookshot_builder.py:play()keepsHOOKSHOT_PROFILEand starts the game through main's newplay-diagnostics.sh. The profile methods and main's track methods (track_id,switching,on_track_changed,older_ok) are both kept.readme.mdanddocs/tested-configurations.md: both sides kept.tests/test_linux_play_diagnostics.py, which came withmain, takesplay()out of the file by its text and failed with aNameErroronchosen_profile. It now stubschosen_profileand checks that the game getsHOOKSHOT_PROFILE.tests/run_all.pyafter the merge: 359 passed, 3 failed, 13 skipped. The three failures also happen onmainhere:test_wayland_libdecor_hint.py(-Wunused-but-set-globalwith this clang), andnx2/test_nup_read_name.pyandtest_bake_portability.py(nozstandard).CI. Understood. It needs a maintainer to approve the workflows.
Profiles as a feature. Agreed, that is NotRexed's decision. I'd rather hear "no" or "not like this" now than polish it further.
Empty files. Neither Eden nor NX2navi writes the files under
slot_*; the game does, throughnn::fs, which they map to plain files. The game writes onlyprogress.savandcaption.savthere. I checked the real saves on my PC (two Eden users with 12 slots, and one Hookshot profile with 6): every slot has just those two files, none of them empty, andaccount.savandoption.savare not empty either. Eden's own bookkeeping files would be in the title folder, whichempty_files()does not look at. NX2navi isn't installed here, so I can't show it. Its pending saves go toprofile/save-tx, outside the slots, and its slot files come from the same game code, so I kept the check./.confignit. Fixed.hookshot-launchnow reads no ini when neitherXDG_CONFIG_HOMEnorHOMEis set.test_hookshot_launch.pyruns the script undersh -xwithout them and checks the trace for no/.config. That check fails on the old script.Windows build. Still untested: I have no llvm-mingw here. I've said so in the description.
I've updated the description to match.
Tried this on Linux (Arch, kernel 7.2.15, Python 3.12), head
8b6f26bd, without the Tk window: the tests, thenprofiles.pyandoot_saves.pycalled the wayimport_profile_saveandexport_profile_savecall them.1. One of the new tests fails on a PC that has an NX2navi save in its usual place.
tests/test_launcher_profiles.py,test_import_offers_the_emulators_usual_folders_with_their_users_names:The test points
XDG_DATA_HOMEat its temporary folder, butscripts/oot_saves.pyalso looks for NX2navi in{home}/Games/Ocarina(theProgram("nx2navi", ...)entry), and this PC has a save there, so the launcher offers itbeside the fixture's Eden save. That is the launcher doing the right thing; only the test sees the real home folder.
With
HOMEset to an empty folder the file passes (15 tests). Adding"HOME": tmpto that test'smock.patch.dict(os.environ, ...)should be all it needs. The other four (test_account_profile,test_hookshot_launch,test_oot_saves,test_linux_play_diagnostics) pass here.2. The profile logic does what the description says. On a new user folder: the list starts as
Player; twoprofiles added get
...0002and...0003; a secondzeldais refused. An import from a folder holding a Hookshotsave (
saves_infinds the one save) copies 6 slots,account.savandoption.savbyte for byte intosave/...0002and leaves the still empty
...0003without a folder; a second import backs the first up tosave-backups/import-folder-<time>. Export makesHookshot save - Zelda - 2026-11-11 07-08, a second one in thesame minute
(2), equal to the profile's save, andsaves_infinds it again; exporting the empty profile is refused.One thing a player can run into: the import refuses while any Hookshot runs on the PC (
close Hookshot first ...),also one that plays another profile or another user folder. Here other test runs of the game were up, and the import
only went through with
running_gamesstubbed as the tests stub it. Safe, and probably right; it may be worth sayingin the error which game it saw.
Not done yet: the game side (
HOOKSHOT_PROFILEin headless runs on an RTX 4090: a listed profile, an unlisted id, a malformed value, a listed profile without a save). Those runs are queued here and I'll add what they show.Follow-up to my comment above: the game side, which the description lists as not tested (this head built from source, RTX 4090, NVIDIA 615.78.08, no window, scripted input; a user folder
made with the launcher's own functions, as in my comment above: the default profile with a child save in Kokiri Forest,
...0002with an adult save,...0003with none).HOOKSHOT_PROFILEhookshot_log.txt...0002(listed)account: profile 5a3c9e1f0b7d24680000000000000002save:mounted onsave/...0002; Continue, Load: six slots with 6 hearts, then adult Link in play...0009(not inprofiles.txt)account: profile ...0009 is not in .../profiles.txt: the default profilesave:mounted onsave/...0001; the Load list says Kokiri Forest, 3 hearts, then child Link by his housezeldaaccount: HOOKSHOT_PROFILE=zelda is not 32 hex digits: the default profileaccount:line...0003(listed, no save yet)account: profile 5a3c9e1f0b7d24680000000000000003save/...0003/account.sav(320 bytes); the other profiles' folders were not touchedSo
hos_account_initand its fall-back do in the game whattest_account_profile.pychecks. No crash or device-lostline in any of the five runs.
The game side is well covered now: mooki000's runs show
HOOKSHOT_PROFILEand its fallback doing whattest_account_profile.pychecks. I also readprofiles.py's remove and export paths. Ids are checked with_ID.fullmatch, removing moves tosave-backups/rather than deleting, and export only everrmtrees the folder it just created. I don't see a data-loss risk there.Three things before it can merge:
test_import_offers_the_emulators_usual_folders_with_their_users_names(tests/test_launcher_profiles.py:245) only patchesXDG_DATA_HOME. On a PC with an NX2navi save in~/Games/Ocarinait fails. Adding"HOME": tmpto that dict fixes it.The Windows build of the host/ change is still untested.
Is this only for Linux? Shouldn't we also make this for Windows?
Currently it's only for Linux. But sure, but i haven't done it yet for windows. I thought splitting it up for the different launchers would be easier to review.
Maybe someone else wants to implement it for windows?
This now conflicts with main in
builder/linux/hookshot_builder.py,docs/tested-configurations.md,nx2/make_game.py,tests/test_linux_play_diagnostics.py. 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.My two concrete points are fixed. The branch merges cleanly with current main again, and test_import_offers_the_emulators_usual_folders_with_their_users_names now patches HOME as well as XDG_DATA_HOME (tests/test_launcher_profiles.py:246). I merged 61d19af into current main and ran the whole tests/run_all.py: 428 passed, and the only failure is test_lift_cache, which fails on main here too. That run covers test_launcher_profiles, test_account_profile, test_hookshot_launch, test_oot_saves and test_linux_play_diagnostics. CI's builds still wait for approval, so the Windows build of the host/ change (nn_applet.c, hos_main.c) hasn't been run anywhere.
What's left is the product decision. NotRexed closed #1594, #1820 and #1868 into #1007, and nobody has said yet whether profiles are wanted in this form. This is also Linux only. The Windows launcher would play the default profile, so the two launchers would differ until someone ports it, which hckrman101 asked about.