# Update manifest — format specification The update manifest is a JSON document describing a single release of the Metin2 client. It lives at `https://updates.jakubkadlec.dev/manifest.json` alongside its Ed25519 signature at `manifest.json.sig`. See [update-manager.md](./update-manager.md) for the overall architecture this fits into. ## Top-level schema ```json { "version": "2026.04.14-1", "created_at": "2026-04-14T12:00:00Z", "previous": "2026.04.13-3", "launcher": { "path": "Metin2Launcher.exe", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 15728640 }, "files": [ { "path": "Metin2.exe", "sha256": "a1b2c3...", "size": 27982848, "platform": "windows", "required": true } ] } ``` ### Required top-level fields | Field | Type | Description | |---|---|---| | `version` | string | Release version. Format: `YYYY.MM.DD-N` where `N` is the 1-indexed daily counter. Sortable, human-readable, allows multiple releases per day. | | `created_at` | string | ISO 8601 timestamp in UTC with `Z` suffix. When the release was produced. | | `launcher` | object | The launcher binary. See below. Special because of self-update handling. | | `files` | array | The non-launcher files in the release. May be empty (launcher-only update). | ### Optional top-level fields | Field | Type | Description | |---|---|---| | `previous` | string | `version` of the manifest this release replaces. Omit for the first release ever. Used for changelog display and future delta-patch strategies. | | `notes` | string | Free-form release notes (Markdown). Displayed by the launcher in the changelog panel. | | `min_launcher_version` | string | Refuse to apply this manifest if launcher's own version is older than this. Used when a manifest change requires a newer launcher. | ## File entry schema ```json { "path": "pack/item.pck", "sha256": "def456abc123...", "size": 128000000, "platform": "all", "required": true } ``` ### Required file fields | Field | Type | Description | |---|---|---| | `path` | string | Path relative to the client root, using forward slashes. No `..` segments. | | `sha256` | string | Lowercase hex sha256 of the file contents. | | `size` | integer | File size in bytes. Used for the progress bar and to detect truncated downloads before hashing. | ### Optional file fields | Field | Type | Default | Description | |---|---|---|---| | `platform` | string | `"all"` | One of `"windows"`, `"linux"`, `"all"`. Launcher filters by its own platform. | | `required` | boolean | `true` | If `false`, a failed download for this file does not block the game launch. | | `executable` | boolean | `false` | On Unix-like systems, set the executable bit after applying. Ignored on Windows. | ## Launcher entry The `launcher` top-level object has the same fields as a file entry, but is called out separately because the launcher is a privileged file: - The launcher replaces itself via **rename-before-replace**, not normal atomic move. - The launcher is **always required**; if it fails to update, the launcher does not launch the game, to avoid a broken loop where the player is running a buggy launcher that can't fix itself. - The launcher is **never** listed in the `files` array. ## Signing `manifest.json.sig` is the raw Ed25519 signature over the literal bytes of `manifest.json`, in detached form. The public key is compiled into the launcher binary. Signing and verification use the standard Ed25519 algorithm (RFC 8032), no prehashing. Example verification in Python: ```python import json from nacl.signing import VerifyKey with open("manifest.json", "rb") as f: manifest_bytes = f.read() with open("manifest.json.sig", "rb") as f: sig = f.read() VerifyKey(bytes.fromhex(PUBLIC_KEY_HEX)).verify(manifest_bytes, sig) ``` In C# with `System.Security.Cryptography` (.NET 8+) or BouncyCastle. ## Canonical JSON To keep signatures stable across trivial reformatting: - Top-level keys appear in the order `version, created_at, previous, notes, min_launcher_version, launcher, files`. - Within the `files` array, entries are **sorted by `path`** lexicographically. - Within a file object, keys appear in the order `path, sha256, size, platform, required, executable`. - JSON is pretty-printed with **2-space indentation**, **LF line endings**, final newline. - Strings use the shortest valid JSON escapes (no `\u00XX` for printable ASCII). `scripts/make-manifest.py` produces output in exactly this form. Do not re-serialize a manifest with a different JSON library before signing; the bytes must match. ## Versioning rules - `version` strings are compared **as date + counter** (not as semver), via `(date, counter)` tuples. - A launcher always replaces its own installed version with the one from the latest manifest, regardless of whether the manifest's `version` is newer than the launcher's own version. There is no "downgrade protection" for the launcher itself because the server is the source of truth. - For the **client files** (not launcher), the launcher refuses to apply a manifest whose `version` is older than the locally-recorded `current-manifest.json` version. This prevents rollback attacks where a compromised CDN replays an old manifest to force players back onto an outdated client that had a known vulnerability. ## Example See [examples/manifest-example.json](./examples/manifest-example.json) for a real manifest produced by `scripts/make-manifest.py` over the current dev client.