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>
167 lines
7.1 KiB
Markdown
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`.
|