launcher: wire m2pack key delivery, wine prefix, platform filter, and prune #1

Merged
jann merged 5 commits from avalonia-gui into main 2026-04-15 14:17:33 +02:00
Owner

What changed since round 1

Rebased on top of main (which now has the full m2pack infra: Runtime/ key model, Formats/ dispatch, EnvVarKeyDelivery, tests). The previous round duplicated that infrastructure by hand; this round drops those duplicated bits and keeps only the four surgical fixes that are still needed.

Commits

  • orchestration: target windows platform + prune stale files — addresses both the platform filter bug (dropping platform=windows files on Linux host) and the missing prune. Prune keep set now includes manifest.launcher.path per Jakub's review — a correctly-authored manifest with a top-level launcher entry will not have it deleted.
  • game: auto-pick metin wine prefix when WINEPREFIX is unset — surgical addition to BuildStartInfo Linux branch, falls back to ~/metin/wine-metin only if env not set and dir exists. No-op on deployments without setup-wine-prefix.sh run.
  • gui: thread m2pack runtime key from orchestrator result into play command — GUI path Play now passes _lastRuntimeKey to GameProcess.Launch, matching the headless path in Program.cs. Caches the key on the view model when StartUpdateAsync completes.
  • gitignore: exclude runtime client data dropped into source tree — unchanged from round 1.

Dropped from round 1

  • Manual M2PACK_MASTER_KEY_HEX env var wiring in GameProcess — superseded by Runtime/EnvVarKeyDelivery and the existing GameProcess.BuildStartInfo(..., RuntimeKey?) parameter in main.
  • runtime-key.json parsing in GameProcess — superseded by M2PackFormat.OnApplied + RuntimeKey.Load in main.

Test plan

  • dotnet build -c Release green
  • dotnet test -c Release — 92/92 pass
  • End-to-end GUI run against staging m2pack release once the release tree is fixed (case collisions + reframed launcher field — tracked outside this PR)

Outstanding from round 1 review

Addressed here:

  • PruneStaleFiles respects top-level launcher entry
  • No longer duplicating runtime key infrastructure that's already in main

Tracked elsewhere (not in this PR):

  • 19 case-collision pairs in release-v2/client/pack — fixed in the release tree rebuild, not a launcher concern
  • make-manifest.py issue #9 — needs to be reframed; the upstream script works as spec-intended, the problem is my post-process that was using Metin2.exe as the manifest launcher field (which is reserved for Metin2Launcher.exe per spec). Will open a separate issue correctly framed.
## What changed since round 1 Rebased on top of `main` (which now has the full m2pack infra: `Runtime/` key model, `Formats/` dispatch, `EnvVarKeyDelivery`, tests). The previous round duplicated that infrastructure by hand; this round drops those duplicated bits and keeps only the four surgical fixes that are still needed. ## Commits - **`orchestration: target windows platform + prune stale files`** — addresses both the platform filter bug (dropping `platform=windows` files on Linux host) and the missing prune. Prune keep set now includes `manifest.launcher.path` per Jakub's review — a correctly-authored manifest with a top-level `launcher` entry will not have it deleted. - **`game: auto-pick metin wine prefix when WINEPREFIX is unset`** — surgical addition to `BuildStartInfo` Linux branch, falls back to `~/metin/wine-metin` only if env not set and dir exists. No-op on deployments without `setup-wine-prefix.sh` run. - **`gui: thread m2pack runtime key from orchestrator result into play command`** — GUI path `Play` now passes `_lastRuntimeKey` to `GameProcess.Launch`, matching the headless path in `Program.cs`. Caches the key on the view model when `StartUpdateAsync` completes. - **`gitignore: exclude runtime client data dropped into source tree`** — unchanged from round 1. ## Dropped from round 1 - Manual `M2PACK_MASTER_KEY_HEX` env var wiring in `GameProcess` — superseded by `Runtime/EnvVarKeyDelivery` and the existing `GameProcess.BuildStartInfo(..., RuntimeKey?)` parameter in main. - `runtime-key.json` parsing in `GameProcess` — superseded by `M2PackFormat.OnApplied` + `RuntimeKey.Load` in main. ## Test plan - [x] `dotnet build -c Release` green - [x] `dotnet test -c Release` — 92/92 pass - [ ] End-to-end GUI run against staging m2pack release once the release tree is fixed (case collisions + reframed launcher field — tracked outside this PR) ## Outstanding from round 1 review Addressed here: - ✅ `PruneStaleFiles` respects top-level `launcher` entry - ✅ No longer duplicating runtime key infrastructure that's already in main Tracked elsewhere (not in this PR): - 19 case-collision pairs in `release-v2/client/pack` — fixed in the release tree rebuild, not a launcher concern - `make-manifest.py` issue #9 — needs to be reframed; the upstream script works as spec-intended, the problem is my post-process that was using `Metin2.exe` as the manifest `launcher` field (which is reserved for `Metin2Launcher.exe` per spec). Will open a separate issue correctly framed.
jann added 4 commits 2026-04-15 12:22:42 +02:00
Two fixes addressing review feedback on the previous avalonia-gui PR round.

1. Platform filter was using host OS (`IsWindows() ? "windows" : "linux"`),
   which dropped all `platform=windows` manifest entries whenever the
   launcher ran on Linux. Since the client is always a Windows PE binary
   launched through Wine on Linux hosts, the target platform we apply is
   always `"windows"` regardless of host. Before this, the orchestrator
   silently skipped Metin2.exe and the python314 / openssl / mingw runtime
   DLLs on Linux, leaving an unbootable install dir.

2. Added `PruneStaleFiles` that walks clientRoot after every successful
   update (both the normal apply path and the already-up-to-date
   short-circuit) and deletes any file not in the manifest. Without it,
   switching from a dirty release to a leaner one — e.g. dropping legacy
   .pck after a .m2p migration — left orphaned files on disk and inflated
   the install dir.

   The keep set is `manifest.files ∪ {manifest.launcher.path}`. Per the
   manifest spec in `m2dev-client/docs/update-manifest.md`, the top-level
   launcher entry is privileged and never listed in files; it must
   survive prune so a correctly-authored manifest does not delete the
   updater itself (caught in Jakub's review of the previous round).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The default ~/.wine prefix lacks tahoma + d3dx9 + vcrun, which makes the
client run with invisible fonts and missing renderer DLLs — exactly what
Jan hit when running the launcher against a fresh install dir on Fedora.

If the parent shell already set WINEPREFIX, honor it. Otherwise fall
back to ~/metin/wine-metin, which is what
m2dev-client/scripts/setup-wine-prefix.sh prepares with the right
runtime deps. The fallback is guarded on the dir existing, so
deployments without that setup are a no-op rather than a broken prefix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Headless Program.cs already passes `result.RuntimeKey` to
GameProcess.Launch, but the GUI Play command dropped it and spawned
Metin2.exe without any env vars. Any m2p-capable client then hit
"Invalid M2PACK_MASTER_KEY_HEX" and refused to load .m2p archives.

Cache the runtime key on the view model when StartUpdateAsync completes
and pass it through on Play. Matches the headless path and the
EnvVarKeyDelivery wiring already in main.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the launcher is run from its bin/ dir during local dev, CWD
becomes the source tree and the orchestrator stages the client release
into src/Metin2Launcher/{pack,bgm,mark,config,.updates,...}. None of
that should ever land in git.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jann force-pushed avalonia-gui from e23f4e1e6e to ac0034fc51 2026-04-15 12:22:42 +02:00 Compare
jann added 1 commit 2026-04-15 13:03:50 +02:00
The canonical launcher runtime-key.json shape is
  { key_id: string, master_key_hex, sign_pubkey_hex }
but `m2pack export-runtime-key --format json` emits
  { version, mapping_name, key_id: int, master_key_hex, sign_public_key_hex }
because its JSON is really a dump of the Windows shared-memory struct.

Parsing the CLI output with the old strict deserializer throws
JsonException on key_id (int != string) and silently drops the public
key field (name mismatch), after which Validate() rejects the key as
not 64 hex chars and the m2pack release fails to boot with
"runtime master key with key_id=1 required for 'pack/root.m2p'".

Hit this tonight during the 2026.04.15-m2pack-v2 release and worked
around it by hand-writing runtime-key.json. Fix: parse into a
JsonElement and extract fields tolerantly — key_id accepts either a
JSON string or a JSON number (stringified), and the pubkey field is
looked up under both "sign_pubkey_hex" and "sign_public_key_hex".

Added a test covering the m2pack CLI shape end to end. Also kept the
malformed-input path on JsonSerializer.Deserialize so it still throws
JsonException (JsonDocument.Parse throws its internal subtype which
breaks Assert.Throws<JsonException>).

Tracked separately as metin-server/m2pack-secure#3 — the m2pack side
should also align its JSON to the canonical shape; this commit is the
client-side belt to the server-side suspenders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jann merged commit db59f4963c into main 2026-04-15 14:17:33 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jann/metin-launcher#1