Files
Fractured/tools/fractured-launcher-electron/README.md
T
Docker Build ebd8d81924 fix(launcher): Linux/macOS packaged config in userData (AppImage EROFS)
AppImage mounts read-only at /tmp/.mount_*; writing launcher.json beside
execPath failed. Use app.getPath('userData') for linux/darwin when packaged.
Bump version to 1.0.3.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 21:32:43 -05:00

171 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Fractured Launcher (Electron)
**Windows** and **Linux (AppImage)** launcher with **no extra console window**, **native Browse folder** dialog, **Gitea or GitHub** release assets + GitHub repo file sync, **realmlist**, optional **auth**, **Play**, and **auto-update** (via `electron-updater`). This is the **only** supported client launcher in this repo.
## Requirements
- [Node.js](https://nodejs.org/) 20+ (includes npm)
## Run from source
```bash
cd tools/fractured-launcher-electron
npm install
npm start
```
On first run, **`launcher.json`** is created: **dev** — next to the app in this folder; **Windows packaged** — beside the `.exe`; **Linux AppImage / macOS packaged** — under Electron **`userData`** (e.g. `~/.config/Fractured Launcher/` on Linux), because the app mount is read-only.
### Where patches download from
- **Recommended (self-hosted Gitea):** set **`gitea.base_url`**, **`gitea.owner`**, **`gitea.repo`** in `launcher.json` (see **`default-launcher.json`**). Players need **`GITEA_TOKEN`** (or the env name in **`gitea.token_env`**) if the Gitea repo is **private** — same trade-off as any private host (per-player token, SSO proxy, or a read-only deploy token you accept distributing).
- **Fallback:** if **`gitea.base_url`** is empty, **`from_release`** uses the **GitHub** Releases API against **`github.owner` / `github.repo`** (defaults to this **`Fractured`** repo for non-release paths), with optional **`GITHUB_TOKEN`** for private assets.
## Build Windows installers
```bash
npm install
npm run pack:win
```
Produces under **`dist/`**:
| Artifact | Purpose |
|----------|---------|
| `Fractured-Launcher-${version}-Setup.exe` (NSIS) | **Recommended for players** — supports seamless **auto-update** and restart. |
| `Fractured-Launcher-${version}-Windows-Portable.exe` | No installer; players replace the file manually. Auto-update is **less reliable** than NSIS. |
## Build Linux AppImage
```bash
cd tools/fractured-launcher-electron
npm install
npm run pack:linux
```
Produces **`dist/Fractured-Launcher-${version}-Linux-x86_64.AppImage`**. Same **`lib/baked-gitea-channel.js`** and **`default-launcher.json`** as Windows; run on **Linux** (or use **Fractured launcher CI** / **Sync release to Gitea**, which upload this file to Gitea with the Windows installers).
**Quick local test (avoids tag snapshot / CI):**
- **Linux:** from repo root, **`bash tools/fractured-launcher-electron/scripts/manual-pack-linux.sh`** → **`dist/*.AppImage`**.
- **Windows:** on a Windows machine, **`cd tools/fractured-launcher-electron`**, **`npm ci`**, **`npm run pack:win`** → **`dist/*.exe`**.
### Hardcoded Gitea channel (non-token)
**`lib/baked-gitea-channel.js`** exports **`base_url`**, **`owner`**, **`repo`**, **`release_tag`**. Set those strings once in the repo (same values you use for CI upload — not secret). At runtime **`config-store`** merges them into **`gitea.*`** so **`launcher.json`** does not need those fields; **`GITEA_TOKEN`** (or **`gitea.token_env`**) is still only for **private** Gitea. Leave a field **`''`** in the baked file to fall back to **`default-launcher.json`** / user **`launcher.json`** for that key.
**`npm run pack:win`** is plain **electron-builder** — no inject step, no extra JSON beside the app.
## Auto-update behaviour
- **Packaged** builds only (`npm run pack:win` output). In `npm start` dev mode, update checks are skipped (button still explains that).
- **No implicit GitHub feed:** the app does **not** guess `package.json``repository` anymore. Without configuration you get a clear “skipped” message instead of a **404** on a private repo.
- **Configured feeds** (first match wins): **`update_feed_url` / `LAUNCHER_UPDATE_URL`** (generic `latest.yml`); or **`gitea`** block filled in + **`GITEA_TOKEN`** when the instance is private (resolves `…/releases/download/{tag}/`); or **`GITHUB_TOKEN`** + **`github.owner` / `github.repo`** for **private** GitHub releases only.
- **~5 seconds** after launch, then **every 6 hours**, the app checks when a feed is configured.
- When a download finishes, a dialog offers **Restart now** (calls `quitAndInstall`) or **Later**.
- **Manual check:** button **Check launcher updates** in the UI.
### Where launcher updates are hosted
**`npm run publish:win`** runs **`electron-builder` with `--publish never`** — artifacts stay in **`dist/`**; CI uploads them to Gitea when you **publish a GitHub release**. For ad-hoc uploads, use **`scripts/upload-release-to-gitea.sh`**. For launcher auto-update, prefer:
- Set **`update_feed_url`** (or **`LAUNCHER_UPDATE_URL`**) to a **generic** HTTPS base URL where **`latest.yml`** and the installer files are hosted (often the same Gitea release attachment URLs pattern your reverse proxy exposes), **or**
- Keep publishing to a GitHub release only for **`latest.yml`** + installers if you accept that small metadata/binary channel there.
**Private GitHub** updater: set **`GH_TOKEN`** / **`GITHUB_TOKEN`** / **`github.token_env`** as documented in `lib/auto-update.js` behaviour.
**Generic feed:** optional Bearer token via the same token envs if your static host checks `Authorization`.
### Publishing a new launcher version
1. Bump **`version`** in `package.json` on `main` (or your release branch) and merge.
2. Create a **GitHub release** (tag + attach patches / `Wow.exe` if needed) and click **Publish****Sync release to Gitea** builds **Windows + Linux** launcher artifacts and mirrors everything to Gitea.
3. Local check: **`npm run pack:win`** (on Windows) or **`npm run pack:linux`** / **`scripts/manual-pack-linux.sh`**, then **`scripts/upload-release-to-gitea.sh`** with the same **`GITEA_*`** env vars as CI if you need a manual upload.
## Sync to Gitea (patches + launcher binaries)
CI workflow **Sync release to Gitea** (`.github/workflows/gitea-release-sync.yml`) runs on **every published GitHub release** on this repo:
1. Triggers on **release published** on **`Dawnforger/Fractured`** (or **workflow_dispatch** with a tag).
2. Builds **Windows** (NSIS + portable) and **Linux** (AppImage) in parallel, each using **`tools/fractured-launcher-electron` from the default branch** (overlaid onto the tag checkout), so older release tags never ship a launcher missing new **`lib/*.js`** files.
3. Downloads **all assets** attached to that **GitHub** release (MPQs, patched `Wow.exe`, etc.).
4. Merges with the built launcher artifacts and uploads everything to a **Gitea release** with the **same tag** (existing attachments on that Gitea release are replaced).
**GitHub Actions secrets** (repository → Settings → Secrets and variables → Actions):
| Secret | Example |
|--------|---------|
| **`GITEA_BASE_URL`** | `https://git.yourdomain.com` (no trailing slash) |
| **`GITEA_TOKEN`** | Gitea personal access token with permission to manage releases and attachments on the target repo |
| **`GITEA_OWNER`** | Organization or username on Gitea |
| **`GITEA_REPO`** | Repository name — must already have **at least one commit** (Gitea returns HTTP 422 “repo is empty” for zero-commit repos; push e.g. a README on **`main`** or set **`GITEA_TARGET_REF`** to your default branch) |
**Optional variable** (Settings → Variables): **`GITEA_TARGET_REF`** — default branch/commitish used **only when the workflow must create a new Gitea release** and Gitea needs `target_commitish` (defaults to **`main`** in the upload script if unset).
**Player `launcher.json`:** packaged builds should already include **`gitea.base_url` / `owner` / `repo`** from the bake step above. Players only need to set **`GITEA_TOKEN`** (or your **`token_env`**) if the Gitea repo is **private**. To point at another instance, edit **`gitea`** in **`launcher.json`**:
```json
"gitea": {
"base_url": "https://git.yourdomain.com",
"owner": "myorg",
"repo": "fractured-patches",
"release_tag": "latest",
"token_env": "GITEA_TOKEN"
}
```
**Manual upload:** `bash scripts/upload-release-to-gitea.sh /path/to/files v1.0.0` with the same env vars as CI.
### Sync did not run / Gitea unchanged — checklist
1. **Git tag ≠ GitHub Release** — Only **Releases** (published on the GitHub **Releases** page) trigger this workflow. If your teammate only **`git push --tags`**, create a **Release** from that tag and click **Publish** (or run **Actions → Sync release to Gitea → Run workflow** and enter the tag).
2. **Manual run: tag vs title****Run workflow** must receive the **git tag** (e.g. `v0.7.11-paragon-…`), copied from the release pages tag badge. Pasting the **release title** (long line with spaces/parentheses) breaks `git fetch` with `invalid refspec`.
3. **Draft release** — Must click **Publish release**; drafts do not mirror.
4. **Workflow on default branch** — GitHub runs `release` workflows from the **default branch** (e.g. `main`). Ensure `.github/workflows/gitea-release-sync.yml` is merged there.
5. **Repo name guard** — Jobs use `if: github.repository == 'Dawnforger/Fractured'`. Forks or renames must change that line or runs are skipped.
6. **Secrets****`GITEA_BASE_URL`**, **`GITEA_TOKEN`**, **`GITEA_OWNER`**, **`GITEA_REPO`** must be set under **Settings → Secrets and variables → Actions**. A failed “Upload to Gitea” step usually prints which is missing.
7. **Actions tab** — Open the latest **Sync release to Gitea** run; a red **build-electron** (old tag without `package-lock.json`, etc.) or **Upload to Gitea** step shows the real error.
8. **HTTP 422 `repo is empty`** — The Gitea repo has **no commits** yet. Push any initial commit (e.g. **Add README** in the Gitea web UI, or `git push` to **`main`**). Optionally set **`GITEA_TARGET_REF`** to match your real default branch if it is not **`main`**. From this repo you can run **`scripts/bootstrap-gitea-repo.sh`** (see script header for `GITEA_*` env or pass the HTTPS/SSH clone URL as the first argument).
### Private Gitea token for players
Do **not** embed a shared admin PAT in a shipped `launcher.json`. Prefer read-only tokens scoped to one repo, short-lived tokens, or a small auth service that redirects to signed URLs.
**Release asset names** must match **`files[].source`** when **`from_release`**: true. Use **`release_tag`**: `"latest"` or a pinned tag matching both GitHub and Gitea.
## Patch versions (same filenames, different bytes)
The launcher does **not** read Git commits. For **turn-key** updates when asset names stay fixed (`patch-Z.MPQ`, `Wow-patched.exe`, …):
1. Ship **`patch-manifest.json`** next to those files on **every** release (Gitea/GitHub attachment). It lists a **`version`** label (any string you bump per release, e.g. `v0.9.0-client`) and a **`sha256`** per **`files[].source`** name.
2. With **`patch_manifest.enabled`**: true (default in **`default-launcher.json`**), **Download updates** first fetches the manifest from the same release channel. If the files already on disk match those checksums, the player sees **“already match build … (nothing to download)”** — no redundant downloads.
3. After a real download, the launcher **re-hashes** installed files and compares to the manifest; mismatch → clear error. It also writes **`.fractured/patch-state.json`** under the WoW folder so the UI can show **“Installed client files: …”**.
If **`patch-manifest.json`** is missing on a release, the launcher falls back to **always downloading** all configured files (same as before).
**Generate the manifest** when you cut a release (paths are your local patch binaries):
```bash
cd /path/to/staging
node tools/fractured-launcher-electron/scripts/generate-patch-manifest.js v0.9.0-client patch-Z.MPQ Wow-patched.exe > patch-manifest.json
```
Attach **`patch-manifest.json`** together with the MPQ/exe to the GitHub release (CI sync copies it to Gitea with everything else).
## CI
Workflow **Fractured launcher CI** (`.github/workflows/fractured-launcher-ci.yml`) runs on pushes/PRs under `tools/fractured-launcher-electron/`: **Windows** (`npm run pack:win`) and **Linux** (`npm run pack:linux`) jobs, each **`electron-builder … --publish never`**. **Actions → Fractured launcher CI → Run workflow** runs it manually.
**Sync release to Gitea** (`.github/workflows/gitea-release-sync.yml`) uses the same pack commands. If you see `GH_TOKEN` / `GitHubPublisher` errors in logs, the job is almost certainly an old **Re-run failed jobs** — open **Actions → Sync release to Gitea → Run workflow**, enter the tag, and start a **new** run instead.
## Config
Schema is defined by **`default-launcher.json`** (shipped in the app; first run copies to **`launcher.json`** — beside the **Windows** exe, or under **`userData`** on **Linux/macOS** packaged builds):
- **`game_dir`**: WoW 3.3.5a root (contains `Wow.exe`).
- **`update_feed_url`**: optional generic HTTPS base for launcher auto-update.
- **`launcher_updates_from_github`**: default **`false`**. Only when **`true`** will a **`GITHUB_TOKEN`** (or **`github.token_env`**) enable **electron-updater**s GitHub provider against **`github.owner` / `github.repo`**. Leave **`false`** when launcher binaries and **`latest.yml`** live on **Gitea** (use **`gitea`** + token instead) so a stray GitHub token does not produce “No published versions on GitHub”.
- **`gitea`**: **`base_url`**, **`owner`**, **`repo`**, **`release_tag`**, **`token_env`** — when **`base_url`** is set (and owner/repo set), **`from_release`** downloads and (with token if needed) the **generic** updater feed use **Gitea**. **Required** for players if your CI mirrors patches/launchers to Gitea only.
- **`github`**: used for **non-release** repo paths (`from_release`: false) and for **GitHub** **`from_release`** when **`gitea.base_url`** is empty.
- **`patch_manifest`**: **`enabled`**, **`source`** (default `patch-manifest.json`), **`from_release`** — checksum-based skip + verify (see above).
- **`files`**, **`realmlist`**, **`auth`**, **`launch`**.