Files
metin-launcher/README.md
Jan Nedbal 1790502b58 docs: document m2pack launcher integration
adds docs/m2pack-integration.md covering the signature boundary,
runtime key env-var delivery, telemetry opt-in, backward compatibility
and expected on-disk layout. README gains a short "Release formats"
section pointing at the new doc, and CHANGELOG tracks the [Unreleased]
entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:12:08 +02:00

167 lines
7.1 KiB
Markdown

# metin-launcher
Self-updating launcher for the Metin2 client.
This is the single entry point the player runs. It fetches a signed manifest from
`updates.jakubkadlec.dev`, verifies an Ed25519 signature against a compiled-in
public key, reconciles the local client against the manifest by sha256, downloads
anything that differs from a content-addressed blob store (with HTTP Range resume),
applies updates atomically, hands launcher self-update off to Velopack, and then
starts `Metin2.exe`. If the update server is unreachable the launcher falls back
to launching whatever the player already has.
Architecture and failure modes live in the client repo under
`docs/update-manager.md` and `docs/update-manifest.md`. This project implements
those specs in C# .NET 8; it does not own the spec.
## Build
```
dotnet build Launcher.sln -c Release
dotnet test
```
Tests are xUnit and run on Linux. A local `HttpListener` stands in for the blob
server in `BlobDownloaderTests`.
## Publish (Windows single-file)
```
dotnet publish src/Metin2Launcher/Metin2Launcher.csproj \
-c Release -r win-x64 --self-contained \
-p:PublishSingleFile=true
```
The CI hasn't been wired yet; the MVP only requires the build to succeed.
## Publishing a release
`scripts/publish-launcher.sh` is the single entry point for cutting a Velopack
release of the launcher and pushing it to the
`https://updates.jakubkadlec.dev/launcher/` feed that
`LauncherConfig.VelopackFeedUrl` points at. It runs `dotnet publish` for
`win-x64` self-contained single-file, then drives `vpk pack` (Velopack CLI
global tool, auto-installed on first run) with `--packId Metin2Launcher
--channel win-x64 --mainExe Metin2Launcher.exe`, and finally rsyncs the
resulting `RELEASES-win-x64` + `*.nupkg` (full + delta) to the VPS.
```
scripts/publish-launcher.sh --version 0.1.0 --dry-run
scripts/publish-launcher.sh --version 0.1.0 --yes
```
Flags: `--version` (required), `--dry-run` (build + pack but skip rsync),
`--yes` (skip the interactive rsync confirmation), `--rsync-target` (override
the default VPS path).
**Known limitation (2026-04-14):** Velopack's `vpk pack` is platform-routed.
On a Linux host it only builds Linux `.AppImage` bundles and refuses to pack
the win-x64 publish output (it tries to ELF-parse `Metin2Launcher.exe` and
crashes with `Given stream is not a proper ELF file`). Building the win-x64
Velopack release therefore needs a Windows host. The script is correct —
it's the toolchain that's gated. Once we have a Windows CI runner this can
be wired into Gitea Actions; until then run the script on a Windows dev box.
The `dotnet publish` step on Linux still works and is a useful sanity check.
## Run
By default the launcher opens an Avalonia GUI window (900x560, fixed size)
showing a banner, status / progress on the left and a news panel on the right.
Pass `--nogui` to run the legacy headless flow that just logs progress and
launches the game when done — useful for CI or unattended boxes.
```
Metin2Launcher.exe # GUI (default)
Metin2Launcher.exe --nogui # headless, log + launch
```
User settings (locale, dev-mode override) live at
`<client>/.updates/launcher-settings.json`. The manifest URL override is only
honoured when `DevMode` is true so a tampered local file alone cannot redirect
updates.
Drop `Metin2Launcher.exe` next to `Metin2.exe` in the client install directory
and run it. On first launch it will:
1. fetch `https://updates.jakubkadlec.dev/manifest.json` + `.sig`
2. verify the signature against the compiled-in public key
3. hash every file listed in the manifest, download anything that doesn't match
into `.updates/staging/`, then atomically move each one into place
4. check for a Velopack launcher update against the feed at
`https://updates.jakubkadlec.dev/launcher` (skipped cleanly on non-installed
dev builds)
5. start `Metin2.exe`
A log is written to both stdout and `<client>/.updates/launcher.log`. On a plain
`dotnet run` (no client root yet), the log falls back to
`%LOCALAPPDATA%/Metin2Launcher/launcher.log` on Windows or
`$XDG_DATA_HOME/Metin2Launcher/launcher.log` on Linux.
## Project layout
```
src/Metin2Launcher/
Program.cs entry: Velopack bootstrap, --nogui switch, Avalonia host
Config/LauncherConfig.cs hardcoded URLs, timeouts, public key
Config/LauncherSettings.cs user settings (locale, dev-mode override) JSON
Orchestration/ update flow extracted from CLI, IProgress driven
Manifest/ DTOs, HTTP loader, Ed25519 verifier
Transfer/ streaming sha256, Range-resume blob downloader
Apply/ atomic staging -> final move
GameLaunch/ CreateProcess wrapper
Logging/ hand-rolled file + stdout logger
Localization/ cs.json + en.json embedded resources, Loc loader
UI/ Avalonia App, MainWindow, SettingsDialog
UI/ViewModels/ MVVM view models
tests/Metin2Launcher.Tests/
```
## Dependencies
- **Avalonia 11** — cross-platform GUI (Linux + Windows from one csproj).
- **CommunityToolkit.Mvvm** — `ObservableObject` + `[RelayCommand]` source generators.
- **NSec.Cryptography** — libsodium-backed Ed25519 (.NET 8 BCL does not have it).
- **Velopack** — launcher self-update. Only used for the launcher binary itself;
game assets (~4 GB) are handled by our own patcher code.
- **xunit / xunit.runner.visualstudio** — tests.
## Release formats
The launcher dispatches on a top-level `format` field in the signed manifest.
Two strategies exist today:
- `legacy-json-blob` (default when `format` is absent) — individual files
listed in `files[]`, downloaded by sha256 and atomically replaced inside
the client root.
- `m2pack` — the `files[]` entries are `.m2p` pack archives plus a
`runtime-key.json` sidecar. The launcher **never** opens or decrypts
`.m2p` archives; it just places them next to the client root and forwards
the runtime key to `Metin2.exe` via environment variables
(`M2PACK_MASTER_KEY_HEX`, `M2PACK_SIGN_PUBKEY_HEX`, `M2PACK_KEY_ID`).
A manifest whose `format` value is signed but unknown is refused outright —
the orchestrator falls back to offline mode rather than silently downgrading.
See `docs/m2pack-integration.md` for the full threat model and file layout.
An opt-in "client applied this release" telemetry ping is available via
`LauncherConfig.TelemetryUrlTemplate`. It is empty by default, which short-
circuits the reporter so no network call is made. When set, the reporter
fires a single bounded (5s) best-effort POST after a successful apply and
swallows any failure.
## Signing key
`LauncherConfig.PublicKeyHex` is the Ed25519 public key that every manifest
must verify against:
```
1d2b63751ea0e0354d28e7eb4ec175919a01518b0bcf5878f0a3aa8e7c6ce2bc
```
The matching private key lives only on the release machine at
`~/.config/metin/launcher-signing-key` with `chmod 600`, is never committed
to any repo, and is used by `scripts/sign-manifest.py` in the `m2dev-client`
repo to sign `manifest.json` before upload. Rotation is done by shipping a
new launcher binary that embeds the new public key; the rotation flow is
documented in `m2dev-client/docs/update-manager.md`.