diff --git a/docs/update-manager.md b/docs/update-manager.md index b5684f36..b901a071 100644 --- a/docs/update-manager.md +++ b/docs/update-manager.md @@ -119,10 +119,19 @@ Launcher does, in order: ### Self-update details -The Windows filesystem does not allow replacing a currently-running executable. Two common patterns: +We do not implement self-update from scratch. The launcher embeds **[Velopack](https://github.com/velopack/velopack)** (MIT, Rust+.NET, actively maintained), which handles: -- **Rename-before-replace**: on Windows you can rename `Metin2Launcher.exe` while it's running, then write the new file at the original path. The running process keeps its file handle open via the renamed copy. Next start picks up the new launcher. This works without a trampoline and is what we use for the launcher's own self-update. -- **Trampoline** (only if rename-before-replace fails): a ~50 KB `launcher-update.exe` that waits for our PID to exit, replaces the main launcher, then exits. Kept as fallback. +- Atomic replacement of the running launcher binary (stable install path, unlike legacy Squirrel) +- Delta patches between launcher versions +- Authenticode signature verification +- Antivirus / firewall friendliness (no UAC prompt, no path churn) +- ~2s update + relaunch + +Velopack is used **only for the launcher binary itself**, which is small (~15 MB). The 4 GB game assets are handled by our own patcher code — Velopack is explicitly not designed for payloads that large. + +Practical shape: at launcher startup we call `VelopackApp.Build().Run()`, then later `UpdateManager.CheckForUpdatesAsync()` against a separate Velopack release feed that lives alongside our asset manifest (e.g. `updates.jakubkadlec.dev/launcher/`). If a new launcher version is available, Velopack downloads it in the background and applies it on next restart. The asset update (sha256 manifest walk) runs unconditionally regardless of whether the launcher itself is updating. + +The fallback path if Velopack ever fails — rename-before-replace plus a small `launcher-update.exe` trampoline — is documented but not implemented in the MVP. Velopack has been stable enough in production for us to start without it. ### Offline fallback @@ -191,6 +200,27 @@ Trade-off: automation speed vs. the attack surface of a CI-held signing key. Whe | Player has antivirus quarantining files | Error message naming the file that disappeared | Document, whitelist in launcher installer | | Someone ships a manifest with missing blobs | Launcher reports which files it can't fetch | Broken release, re-run publish | +## Prior art survey + +Before writing code, a scan of the ecosystem for things to fork or copy. Bottom line: nothing in the Metin2 community is worth forking, but three external projects inform this design. + +**Metin2-specific launchers**: the community reference is [Karbust/Metin2-Patcher-Electron](https://github.com/Karbust/Metin2-Patcher-Electron) (TypeScript/Electron, MIT, last push 2021). SHA256 per-file manifest, parallel HTTP downloads, two-zip deploy model. No signing, no delta, no self-update, dead deps. Worth skimming to understand what Metin2 server admins UX-expect. Everything else in the space (`VoidEngineCC/Metin2-Simple-C-Patcher`, `CeerdaN/metin2-patcher-electron`, `Cankira-BK/metin2-pvp-launcher`, ...) is either unlicensed, a toy, or abandoned. The ceiling of published prior art is "file list + sha256 + HTTP GET." We are already above it on paper. + +**d1str4ught upstream**: no launcher, no patcher. The upstream distribution model is "clone the repo." Greenfield for us. + +**General-purpose auto-updaters**: + +- **[Velopack](https://github.com/velopack/velopack)** — the modern successor to Squirrel.Windows, by the same primary author. MIT, Rust+.NET, released regularly in 2025. Handles atomic binary replacement, delta patches, Authenticode, stable install paths. Used for the launcher self-update layer. Not used for game assets — not designed for 4 GB payloads. +- Squirrel.Windows (legacy, unmaintained, known path-churn bugs), WiX Burn (wrong shape — chain installer, not update loop), NSIS (you reimplement everything), Rust `self_update` crate (single-file, no multi-artifact) — all rejected. + +**Architectural reference**: **[runelite/launcher](https://github.com/runelite/launcher)** (Java, BSD-2). A tiny native launcher for a non-Steam game, with exactly the shape we want: bootstrap JSON → signed → list of artifacts with hashes → download missing → verify → launch. X.509 instead of Ed25519, same threat model. Before writing launcher code we read this end-to-end as the reference implementation; we do not copy code (wrong language), we copy structure. + +**Ed25519 prior art in private-server game launchers**: [wowemulation-dev/wow-patcher](https://github.com/wowemulation-dev/wow-patcher) (Rust) replaces the WoW client's Ed25519 public key to redirect auth. Direct precedent for using Ed25519 in this role. Sparkle 2 on macOS has shipped Ed25519 appcast since 2021; same primitive, coarser per-release granularity. + +**Manifest format**: the shape we have is loosely TUF-lite — one signed top-level JSON pointing at content-addressed blobs, without TUF's role separation. Full [TUF](https://theupdateframework.io/) is overkill for a 4-dev private-server project, but worth naming as the professional vocabulary. [OSTree](https://ostreedev.github.io/ostree/) implements exactly the content-addressed part at the filesystem level — a good read, too Linux-specific to reuse. + +**Net take**: this design converges on the intersection of OSTree (content addressing), Sparkle (Ed25519 signing) and RuneLite launcher (bootstrap-signed-JSON → artifact list → verify → launch), with Velopack handling the self-update plumbing. Nothing novel, which is the point. + ## Implementation plan Effort is real-days of Claude + review time from the team. @@ -202,9 +232,9 @@ Effort is real-days of Claude + review time from the team. | 3 | `scripts/make-manifest.py` — walk dir, produce unsigned manifest | 1 d | Python script + docs | | 4 | Sign/verify script (Ed25519) | 0.5 d | Python + keygen docs | | 5 | Caddy config for `updates.jakubkadlec.dev` | 0.5 d | Caddyfile fragment + DNS note | -| 6 | Launcher (C# .NET 8 self-contained, single-file) — skeleton + HTTP fetch + manifest parse + verify | 2 d | `launcher/` project | +| 6 | Launcher (C# .NET 8 self-contained, single-file) — skeleton + HTTP fetch + manifest parse + Ed25519 verify | 2 d | `launcher/` project | | 7 | Launcher — file diff + download + hash verify + atomic apply | 2 d | | -| 8 | Launcher — self-update with rename-before-replace + `.old` rollback | 1 d | | +| 8 | Launcher — Velopack integration for self-update | 0.5 d | | | 9 | End-to-end test (publish → client updates → launch) | 1 d | | | 10 | `scripts/make-release.sh` wiring it all together | 1 d | | | 11 | Docs: publisher runbook, player troubleshooting, threat model | 1 d | |