Skip to content

feat(launcher): the Hookshot Launcher, one app to build, update and play every version - #2739

Draft
hckrman101 wants to merge 7 commits into
NotRexed:mainfrom
hckrman101:feat/hookshot-launcher
Draft

hckrman101 wants to merge 7 commits into
NotRexed:mainfrom
hckrman101:feat/hookshot-launcher

Conversation

@hckrman101

Copy link
Copy Markdown
Collaborator

Windows and Linux each have their own Builder today, and a PC keeps only one built version of Hookshot at a time. Hookshot 3.0 replaces both with one launcher that builds, updates and plays several versions side by side (release, release candidate and nightly), and copies saves between them and the emulators.

Depends on: #2733 (nightly naming) and #2722 (the 2.x hand-over contract and upgrader). This branch carries their commits until they merge. Please review only the four launcher commits on top of them:

  1. feat(launcher): the launcher's core library, with its tests
  2. feat(launcher): the Avalonia app shell
  3. feat(launcher): what the launcher's screens need from the core: game files found, checked, built and timed
  4. feat(launcher): the launcher's screens, from the design mockup

Related:

What changed

Core (launcher/Core, no UI and no packages):

  • Versions and channels: the rules of Updater.cs and linux_update.py, checked against tests/test_update_tracks.py's own vectors. Nightlies use feat(release): name a nightly by its commit count on main and its commit #2733's nightly-<count>-<commit> names, and nightlies and releases are never ordered against each other. Each build follows its own channel, and an update is offered only when it is newer on that channel.
  • Build tools per version: each version gets its own tools in <home>/toolchains/<tag>, installed from that release's parts list (hookshot-setup.txt or hookshot-linux.txt). Every part is checked for size and SHA-256 before it is unpacked. Zip entries that would land outside the folder are refused, and so are links that escape it. A part another version already has is hard-linked rather than downloaded again. Downloads are https only, also after redirects.
  • builds.json: the list of builds, with exactly one default.
  • The 2.x hand-over (docs/launcher-upgrade.md): reads the import files and, on a first start, the Builder's own settings. An import file is deleted only once what it brought has been saved.
  • Running make_game.py: its command line and environment as Runner.Info and AppRun build them. Its @@ lines become one progress bar that never goes back. Cancel kills make_game's own process tree and nothing else.
  • Saves: a rule-for-rule port of scripts/oot_saves.py. It finds saves in Hookshot and the emulators, takes a backup before every copy, and refuses while a game or emulator runs.
  • Also: game-file discovery (the Builder's Finder), disk space, shortcuts, the GPU makers on Linux.

App (launcher/App, Avalonia 12):

  • The window draws its own title bar, in the Builder's Slate palette with its Inter font.
  • View models are plain INotifyPropertyChanged, with one compiled-binding view per page.
  • The 2.x command line still works: --launch, --update [--tag] [--fetched] and --play, so the game's own update prompt and the shortcuts keep working.

Screens (from the design mockup):

  • Home: empty, one or several versions, update offers, the hand-over notice.
  • The channel picker.
  • A three-step wizard:
    1. The version's tools download while the game files on this PC are found and checked.
    2. Settings: saves, DLSS and FSR 4 behind their licences, the game folder with the space it needs, language, shaders, and memory and workers under Advanced.
    3. The build, with one bar and a row for each stage.
  • Ready, and a problem screen with Copy for a bug report.
  • The launcher's settings.
  • Saves: copy from another build, import from an emulator, export to one. Each shows a confirm screen with what gets replaced and where the backup goes.
  • Design notes: focus rings for keyboard and controller, quick transitions that stop when the desktop asks for less motion, tabular digits where numbers change, paths on one line (middle-elided, with the full path in a tooltip) and the home folder shown as ~ on Linux.

Safety calls worth checking:

  • Delete removes a folder only if make_game.py made it. It refuses:
    • a link;
    • the home folder;
    • a folder that holds another build or the launcher's home (a 2.x install at ~/Games/Hookshot holds the new builds for now);
    • any folder while Hookshot runs.
  • Free up space deletes only what make_game made in the build-files folder, never the folder itself.
  • DLSS and FSR 4: NVIDIA's and AMD's SDKs are fetched only after their licence is accepted. A test checks that the licence texts name the commits scripts/ngx_sdk.py and scripts/ffx_sdk.py fetch.
  • No game data leaves the PC. Nothing in the launcher uploads anything. The only network calls are GitHub's release API and file downloads. The bug-report copy holds the launcher's log, the settings and the PC's memory and GPU makers.
  • Closing the window during a build asks first, as Cancel does, so make_game is never left running without a window.

Why Avalonia, and the first NuGet dependency

One UI codebase has to run natively on Windows and Linux, and WPF and Tk each cover only one. Avalonia is MIT-licensed, mature and draws the same on both, with native Wayland. This is the repository's first NuGet dependency, so it is kept tight:

  • The four Avalonia packages are pinned exactly in Directory.Packages.props, with packages.lock.json for everything they bring in. Those are SkiaSharp, HarfBuzzSharp, Tmds.DBus, NWayland and MicroCom, all MIT.
  • NuGet.Config allows nuget.org only.
  • Avalonia.BuildServices' telemetry target is replaced by an empty one. The build log shows the override, so builds report nothing.
  • launcher/Core has no packages at all: the tests compile its sources directly.
  • UseWaylandWithFallback() uses native Wayland where there is a compositor and falls back to X11 otherwise. On Windows it does nothing (Avalonia's docs).

How it was tested

  • python -I -B tests/run_all.py on Linux: 425 passed, 67 skipped. The only failures are the two known local ones, test_bake_portability (no zstandard here) and test_builder_log_read_only (no Tk here).

  • tests/test_launcher_core.py: 453 checks over temporary folders, with a UTF-8 scratch path (ゲーム Zélda 🎮). They cover:

    • the version vectors and the hand-over fixtures;
    • the saves cases from test_oot_saves.py;
    • the tool installs: size and SHA-256, zip-slip, escaping links, reuse;
    • the progress, the delete guards and Free up space;
    • the shortcuts' quoting.

    It also passes at each of the four commits.

  • dotnet build launcher/HookshotLauncher.slnx: 0 warnings, with warnings as errors.

  • An end-to-end run on Linux, using a fake parts list (file://) and a stand-in make_game: download, check, build, Ready, and the row in builds.json.

  • CI: source-regressions.yml now installs .NET 10 next to the Builder's 8 on Linux and Windows, so the core test runs in "Every test". Part 1 of each system compiles the solution with RestoreLockedMode.

Not tested:

  • Nothing on Windows: the hard links (CreateHardLinkW), the PowerShell shortcuts, the reduced-motion check, process detection, and %LOCALAPPDATA% paths. CI's Windows compile with SDK 10 has not run yet.
  • No real toolchain download from a release, and no real game build.
  • Play on Linux starts bin/hookshot directly. It does not go through play-diagnostics.sh or its own systemd scope as the 2.x Linux Builder does.
  • The X11 fallback was not run here.
  • The screenshots are headless renders with fake data.

Planned next (not in this PR)

  • Profiles and a clean folder layout: builds in ~/Games/Hookshot/versions/<version> and saves and settings in ~/Games/Hookshot/profiles, with an importer that moves a 2.x install into it.
  • CI and release packaging:
    • building the launcher (the Windows zip and the Linux AppImage);
    • publishing hookshot-launcher.txt and the per-platform tools lists;
    • the 2.x bridge payload (docs/launcher-upgrade.md, sections 1 and 5).

Open design decisions

The calls this PR makes are listed in launcher/README.md, "Open design decisions". Each one can be changed:

  • saves are copied, never moved;
  • an update takes its build's row and leaves the old folder on disk;
  • building a version you already have rebuilds it;
  • Delete's guards;
  • shortcuts are made on Ready, ticked by default;
  • the 2.x Builders' time and space figures;
  • the slots are shown as "Slot N";
  • "Your GPU" is shown on Linux only for now.

Screenshots

These are headless renders (Avalonia.Headless and Skia, 960x600 at 1x) with fake builds, saves and game files, and a fake home folder shown as ~. Headless rendering draws blurred shadows poorly: the glows under Play and the main buttons show as dark corners here, though they are soft glows in the app.

Home with several versions: a release, a release candidate and a nightly
Updates offered for the default version and the nightly
The 2.x Builder's hand-over notice
The channel picker
Step 1: the tools download while the game files are checked
Step 2: build settings
Step 2: NVIDIA's DLSS licence, shown before the SDK can be fetched
Step 3: building, with one bar and a row per stage
The build stopped: out of memory
Ready, with saves moved from the previous version
Saves
Import: confirm what gets replaced and where the backup goes

Before and after the styling pass

Before After
Home before the styling pass Home after: the path under ~, the nightly as "Nightly 2043 · 9f3c2ab4"
Step 2 before Step 2 after: the game folder on one line
Delete before Delete after: the folder on its own line
Every other screen

Home, nothing built yet
Home, nothing built yet, on Linux with round inset window buttons
Home with one version
Home with several versions on Linux, where saves are shared
Play with the keyboard and controller focus ring
Delete, with the Windows saves warning
Delete on Linux, where saves stay
Step 1: a supported dump picked
Step 2 on Linux: shared saves, no FSR
Step 2 with Advanced open
Step 2 with too little disk space
Step 2 with too little disk space, on Linux
An update's step 2, moving saves from the build it replaces
An update's step 2 on Linux
Step 3 with the log shown
Ready after a first build
Ready on Linux, after an end-to-end build by the stand-in make_game
The launcher's settings
Saves on Linux: one shared set
Import: pick an emulator
Import: pick a profile
Import: confirm, on Linux
Refused: the game is running
Refused: NX2navi has a save waiting
Saves imported

Before After
Step 1 before Step 1 after: the found files' folders as short paths under ~
Step 3 before Step 3 after: tabular digits
Saves before Saves after: roomier rows, a plain "Slots" label
Settings before Settings after: paths under ~

docs/launcher-upgrade.md is the contract between a 2.2/2.3 Builder that hands an
install over to the Hookshot Launcher (3.0) and the launcher that takes it: how a
launcher release is told from a Builder release (hookshot-launcher.txt, never the
version name), where the launcher is installed and how it is swapped in whole, the
import file dropped for the game folder, what changes in the folder's stamp, and the
bridge that brings Builders older than 2.2 to the same screen in one update.

tests/fixtures/launcher/ holds sample files that both sides test against, so the
upgrader and the launcher read and write the same thing.
A nightly was named by git describe (v2.1.0-rc.3-251-gee29dd55), which reads as a
release candidate and says nothing a player can compare. CI now names every build
that is not a release nightly-<commit count>-<commit> (scripts/nightly_name.sh), a
checkout the same way (nx2/version.py), and players see it as "Nightly 2029 ·
ee29dd5" in the menu, the start-up screen, Discord, both builders and the update
question.

Nightlies are ordered by their count, and every new name is newer than the old
git describe names. A nightly and a release or release candidate are not ordered:
a builder does not build one over the other unless that track is chosen in the
Track menu, and it asks first. Updater.cs is now checked against the shared track
vectors too (tests/test_update_tracks_cs.py).
launcher/Core holds the Hookshot Launcher's logic with no UI and no package: versions and
channels (the Updater.cs/linux_update.py rules, nightlies named by their commit count as
docs/versioning.md now says), the newest version per channel, both parts lists,
per-version build tools with checked and reused parts, game folders, builds.json, the
2.x hand-over (docs/launcher-upgrade.md; an import file is deleted only once what it
brought is saved), remembered settings, make_game.py's command line and @@ lines as one
overall progress, update offers per channel, the 2.x command line, and saves (a port of
scripts/oot_saves.py: discovery, summary, copy with a backup). tests/launcher_core checks
it with the shared version vectors, the hand-over fixtures and test_oot_saves.py's cases.
launcher/App: the window (960x600, the Builder's Slate palette and Inter), home with
Build/Play, the list of builds (play, set default, delete, update offer), the channel
for new builds, placeholder wizard steps, the settings page and a saves page. The 2.x
command line is handled (--play starts the game with no window). Avalonia 12.1.6, pinned,
with lock files and nuget.org as the only source; Avalonia's build telemetry target is
switched off. CI installs .NET 10 beside the Builder's 8 on Linux and Windows, so the
core's test runs there, and compiles the solution with warnings as errors.
…files found, checked, built and timed

The core's side of the screens: the game files on this PC (a port of the Builder's
Finder) and make_game.py --check's answer, make_game's run with its whole process tree
stopped on cancel, the build's clock and time left, the builds folder and an update
taking its build's row, disk space and what Free up space may let go (only what
make_game made in the build files folder), the saves' refusals and summaries, the Linux
shortcuts and the GPU makers. Deleting a build refuses a link, the home folder and a
folder that holds another build or the launcher's home. ScreenChecks.cs checks each.
The mockup's screens on the app shell: home (empty, one or several versions, updates,
the hand-over's notice), the channel picker, the build wizard (the tools download while
the game files are found and checked; settings with saves, DLSS and FSR 4 behind their
licences and the disk space; the build with one bar), Ready and the problem screen, the
launcher's settings and the Saves page. Nightlies show as "Nightly 2029 · c544828"
(docs/versioning.md). A styling pass: focus rings for the keyboard and a controller,
quick transitions that stop when the desktop asks for less motion, tabular digits, and
paths on one line with the home folder shown as ~. Closing the window during a build
asks first, a delete runs off the window's thread and not while the game runs, and
--update with no --tag offers the build's newest. launcher/README.md lists the calls
made and what comes next.
@Joey-Magnus

Copy link
Copy Markdown
Contributor

A PR for this branch: hckrman101#20 (one commit on top of fabb12c). It adds plain-language problem screens with the fix, finds a game crash in the last run's log at the next start, and a Send report button with consent asked once. Sending needs a report inbox; docs/launcher-reports.md there has the API, and until one exists the button copies the report and opens the issue form. Could magicbean.dev host the inbox? (_trash.panda's agent)

…art, and Send report (#20)

A build that stops shows make_game.py's message as it is, and a crash leaves only a log a player has to
find and attach to a GitHub issue. Now:

- launcher/Core/Problems.cs: make_game.py's and the game's own messages mapped to a plain headline,
  what to do and a fix (antivirus removed the tools, another game version, an incomplete dump, a full
  drive, the build out of memory, a lost GPU, no mesh shaders); a crash's signature (exception code and
  module+offset, no folder) for grouping. A message it doesn't know stays "The build stopped".
- ProblemReport.cs: the report under the issue form's headings, as the game's Bug report writes them,
  the log's end with the home folder and user name taken out by the game's own rule; CrashCheck finds a
  crash in a build's last run (or the run before) at the launcher's next start, once per log.
- ReportSender.cs: one https POST to a report inbox (docs/launcher-reports.md); none is set up yet, so
  Send copies the report and opens the issue form. HOOKSHOT_REPORT_URL for trying an inbox.
- The report page: what happened, the fix (the driver page for the card's maker), consent asked once
  (Always send, Just this once, Never send) with "What gets sent"; the build's problem screen uses the
  plain words and has Send report; the settings have Problem reports.

tests/launcher_core/ProblemChecks.cs covers the mapping, the redaction (the game's cases), the crash
found once, the report, the settings and the inbox rules. Built with warnings as errors; 480 checks
pass. The crash page checked on Linux with a crashed build's log.

Co-authored-by: Joey Magnus <joey.magnus@cxalloy.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants