Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 656cf2d07d | |||
| bfe51f6ad4 | |||
| 2a3107a78d | |||
| 48826e21d6 | |||
| 15c476c12d | |||
| 6c4d7244c3 |
@@ -22,7 +22,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
electron-launcher:
|
||||
electron-launcher-windows:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 45
|
||||
defaults:
|
||||
@@ -38,10 +38,6 @@ jobs:
|
||||
cache-dependency-path: tools/fractured-launcher-electron/package-lock.json
|
||||
|
||||
- name: Install and pack (NSIS + portable)
|
||||
env:
|
||||
GITEA_BASE_URL: ${{ secrets.GITEA_BASE_URL }}
|
||||
GITEA_OWNER: ${{ secrets.GITEA_OWNER }}
|
||||
GITEA_REPO: ${{ secrets.GITEA_REPO }}
|
||||
run: |
|
||||
npm ci
|
||||
npm run pack:win
|
||||
@@ -54,3 +50,32 @@ jobs:
|
||||
tools/fractured-launcher-electron/dist/*.exe
|
||||
tools/fractured-launcher-electron/dist/latest.yml
|
||||
tools/fractured-launcher-electron/dist/*.blockmap
|
||||
|
||||
electron-launcher-linux:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
defaults:
|
||||
run:
|
||||
working-directory: tools/fractured-launcher-electron
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: npm
|
||||
cache-dependency-path: tools/fractured-launcher-electron/package-lock.json
|
||||
|
||||
- name: Install and pack (AppImage)
|
||||
run: |
|
||||
npm ci
|
||||
npm run pack:linux
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fractured-launcher-electron-linux-${{ github.run_id }}
|
||||
if-no-files-found: warn
|
||||
path: |
|
||||
tools/fractured-launcher-electron/dist/*.AppImage
|
||||
tools/fractured-launcher-electron/dist/*.yml
|
||||
tools/fractured-launcher-electron/dist/*.blockmap
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
# Release on github.com (Releases → Draft/new release → Publish). The workflow
|
||||
# definition must exist on the repo DEFAULT branch (GitHub runs it from there).
|
||||
#
|
||||
# Steps: build Electron from tag → download this repo’s release attachments → upload all to Gitea.
|
||||
# Steps: Windows (NSIS+portable) + Linux (AppImage) in parallel, launcher from DEFAULT BRANCH
|
||||
# overlay on tag checkout → merge with GitHub release assets → upload all to Gitea.
|
||||
#
|
||||
# Secrets: GITEA_BASE_URL, GITEA_TOKEN, GITEA_OWNER, GITEA_REPO
|
||||
# Optional variable: GITEA_TARGET_REF (see tools/fractured-launcher-electron/README.md)
|
||||
@@ -66,6 +67,16 @@ jobs:
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.tag }}
|
||||
|
||||
# Release tags often point at server/game commits that predate launcher lib fixes.
|
||||
# Always pack the launcher from default branch so app.asar includes the full tree.
|
||||
- name: Overlay launcher from default branch
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DB="${{ github.event.repository.default_branch }}"
|
||||
git fetch --no-tags --depth=1 origin "+refs/heads/${DB}:refs/remotes/origin/${DB}"
|
||||
git checkout "origin/${DB}" -- tools/fractured-launcher-electron
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
@@ -74,14 +85,8 @@ jobs:
|
||||
|
||||
- name: Install and pack (NSIS + portable)
|
||||
working-directory: tools/fractured-launcher-electron
|
||||
env:
|
||||
# Same values as upload step — baked into default-launcher.json (no token).
|
||||
GITEA_BASE_URL: ${{ secrets.GITEA_BASE_URL }}
|
||||
GITEA_OWNER: ${{ secrets.GITEA_OWNER }}
|
||||
GITEA_REPO: ${{ secrets.GITEA_REPO }}
|
||||
run: |
|
||||
npm ci
|
||||
# pack:win runs inject-release-channel.js then electron-builder --publish never
|
||||
npm run pack:win
|
||||
|
||||
- name: Stage launcher files for upload
|
||||
@@ -97,11 +102,61 @@ jobs:
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: electron-dist
|
||||
name: electron-dist-windows
|
||||
path: launcher-publish/
|
||||
|
||||
build-electron-linux:
|
||||
needs: meta
|
||||
if: github.repository == 'Dawnforger/Fractured'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.tag }}
|
||||
|
||||
- name: Overlay launcher from default branch
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DB="${{ github.event.repository.default_branch }}"
|
||||
git fetch --no-tags --depth=1 origin "+refs/heads/${DB}:refs/remotes/origin/${DB}"
|
||||
git checkout "origin/${DB}" -- tools/fractured-launcher-electron
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: npm
|
||||
cache-dependency-path: tools/fractured-launcher-electron/package-lock.json
|
||||
|
||||
- name: Install and pack (AppImage)
|
||||
working-directory: tools/fractured-launcher-electron
|
||||
run: |
|
||||
npm ci
|
||||
npm run pack:linux
|
||||
|
||||
- name: Stage Linux launcher for upload
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p launcher-linux-publish
|
||||
shopt -s nullglob
|
||||
cp -f tools/fractured-launcher-electron/dist/*.AppImage launcher-linux-publish/ 2>/dev/null || true
|
||||
cp -f tools/fractured-launcher-electron/dist/*.yml launcher-linux-publish/ 2>/dev/null || true
|
||||
cp -f tools/fractured-launcher-electron/dist/*.blockmap launcher-linux-publish/ 2>/dev/null || true
|
||||
ls -la launcher-linux-publish/
|
||||
if ! compgen -G "launcher-linux-publish/*.AppImage" > /dev/null; then
|
||||
echo "No AppImage under dist/ — electron-builder linux target failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: electron-dist-linux
|
||||
path: launcher-linux-publish/
|
||||
|
||||
sync-gitea:
|
||||
needs: [meta, build-electron]
|
||||
needs: [meta, build-electron, build-electron-linux]
|
||||
if: github.repository == 'Dawnforger/Fractured'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
@@ -121,8 +176,13 @@ jobs:
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: electron-dist
|
||||
path: /tmp/electron
|
||||
name: electron-dist-windows
|
||||
path: /tmp/electron-win
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: electron-dist-linux
|
||||
path: /tmp/electron-linux
|
||||
|
||||
- name: Merge GitHub release assets + Electron build
|
||||
env:
|
||||
@@ -145,7 +205,7 @@ jobs:
|
||||
cat /tmp/dl.err || true
|
||||
fi
|
||||
shopt -s nullglob
|
||||
for f in /tmp/electron/*; do
|
||||
for f in /tmp/electron-win/* /tmp/electron-linux/*; do
|
||||
if [ -f "$f" ]; then
|
||||
cp -f "$f" combined/
|
||||
fi
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5368,6 +5368,18 @@ void SpellMgr::LoadSpellInfoCorrections()
|
||||
LockEntry* key = const_cast<LockEntry*>(sLockStore.LookupEntry(36)); // 3366 Opening, allows to open without proper key
|
||||
key->Type[2] = LOCK_KEY_NONE;
|
||||
|
||||
// Fractured / Paragon: DK weapon-line "passives" Forceful Deflection and
|
||||
// Runic Focus ship in 3.3.5a Spell.dbc without SPELL_ATTR0_PASSIVE set.
|
||||
// SpellInfo::IsPassive() is therefore false, and mod-paragon's panel-learn
|
||||
// diff treats them as castable actives and revokes them — while true
|
||||
// actives (Blood Presence, Death Coil, Death Grip, ...) must stay
|
||||
// stripped. Mark these two passive in-memory so the panel policy matches
|
||||
// the spellbook UX for every class (stock DK benefits too).
|
||||
ApplySpellFix({ 49410, 61455 }, [](SpellInfo* spellInfo)
|
||||
{
|
||||
spellInfo->Attributes |= SPELL_ATTR0_PASSIVE;
|
||||
});
|
||||
|
||||
// Fractured: strip reagent requirements from every player-class spell at
|
||||
// load time. Filtered by SpellFamilyName != 0 so that profession spells
|
||||
// (cooking, alchemy, enchanting, blacksmithing, jewelcrafting, leatherworking,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Fractured Launcher (Electron)
|
||||
|
||||
Windows 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.
|
||||
**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
|
||||
|
||||
@@ -35,14 +35,25 @@ Produces under **`dist/`**:
|
||||
| `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. |
|
||||
|
||||
### Baked Gitea channel (non-token)
|
||||
## Build Linux AppImage
|
||||
|
||||
**`npm run pack:win`** runs **`scripts/inject-release-channel.js`** first. It merges **`gitea.base_url`**, **`owner`**, **`repo`**, and optional **`release_tag`** into **`default-launcher.json`** for that build only (then **electron-builder** packs that file).
|
||||
```bash
|
||||
cd tools/fractured-launcher-electron
|
||||
npm install
|
||||
npm run pack:linux
|
||||
```
|
||||
|
||||
- **GitHub Actions** — **Sync release to Gitea** and **Fractured launcher CI** export **`GITEA_BASE_URL`**, **`GITEA_OWNER`**, **`GITEA_REPO`** (same names as your upload secrets) for the pack step, so installers match the repo you sync to. Nothing embeds **`GITEA_TOKEN`**.
|
||||
- **Local packs** — put the same values in **`fractured-release-channel.json`** (committed or personal copy) **or** export those env vars before **`npm run pack:win`**.
|
||||
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).
|
||||
|
||||
First launch still copies **`default-launcher.json`** → **`launcher.json`** beside the exe, so players get the baked **`gitea.*`** without editing unless they override.
|
||||
**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
|
||||
|
||||
@@ -67,15 +78,15 @@ First launch still copies **`default-launcher.json`** → **`launcher.json`** be
|
||||
### 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 installers and mirrors everything to Gitea.
|
||||
3. Local check: `npm run pack:win` then **`scripts/upload-release-to-gitea.sh`** with the same **`GITEA_*`** env vars as CI if you need a manual upload.
|
||||
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 the **Electron** app from that tag on Windows.
|
||||
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).
|
||||
|
||||
@@ -141,9 +152,9 @@ Attach **`patch-manifest.json`** together with the MPQ/exe to the GitHub release
|
||||
|
||||
## CI
|
||||
|
||||
Workflow **Fractured launcher CI** (`.github/workflows/fractured-launcher-ci.yml`) runs on pushes/PRs under `tools/fractured-launcher-electron/`: Windows pack uses **`electron-builder … --publish never`** (not `npm run pack:win`, so tagged checkouts never require `GH_TOKEN`). **Actions → Fractured launcher CI → Run workflow** runs it manually.
|
||||
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 command. 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.
|
||||
**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
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"gitea": {
|
||||
"base_url": "",
|
||||
"owner": "",
|
||||
"repo": "",
|
||||
"release_tag": "latest"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Production Gitea mirror (non-secret). Edit here and ship — no inject script,
|
||||
* no fractured-release-channel.json, no CI env needed for these fields.
|
||||
* Token stays in env: GITEA_TOKEN or launcher.json → gitea.token_env.
|
||||
*/
|
||||
module.exports = {
|
||||
// Scheme optional — gitea-release normalizes to https:// if missing.
|
||||
base_url: 'https://brassnet.ddns.net:33983',
|
||||
owner: 'Dawnsorrow',
|
||||
repo: 'Fractured-Distro',
|
||||
release_tag: 'latest',
|
||||
};
|
||||
@@ -11,7 +11,13 @@ function mergeConfig(defaults, user) {
|
||||
user.update_feed_url != null && user.update_feed_url !== ''
|
||||
? user.update_feed_url
|
||||
: defaults.update_feed_url,
|
||||
launcher_updates_from_github:
|
||||
user.launcher_updates_from_github != null
|
||||
? user.launcher_updates_from_github
|
||||
: defaults.launcher_updates_from_github,
|
||||
github: { ...defaults.github, ...(user.github || {}) },
|
||||
gitea: { ...defaults.gitea, ...(user.gitea || {}) },
|
||||
patch_manifest: { ...defaults.patch_manifest, ...(user.patch_manifest || {}) },
|
||||
launch: { ...defaults.launch, ...(user.launch || {}) },
|
||||
auth: user.auth != null ? { ...defaults.auth, ...user.auth } : defaults.auth,
|
||||
realmlist: user.realmlist != null ? { ...defaults.realmlist, ...user.realmlist } : defaults.realmlist,
|
||||
@@ -19,6 +25,23 @@ function mergeConfig(defaults, user) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Hardcoded Gitea host/repo (see lib/baked-gitea-channel.js). Non-empty baked values win. */
|
||||
function applyBakedGitea(cfg) {
|
||||
let baked;
|
||||
try {
|
||||
baked = require('./baked-gitea-channel');
|
||||
} catch {
|
||||
return cfg;
|
||||
}
|
||||
if (!baked || typeof baked !== 'object') return cfg;
|
||||
cfg.gitea = { ...(cfg.gitea || {}) };
|
||||
for (const k of ['base_url', 'owner', 'repo', 'release_tag']) {
|
||||
const v = baked[k];
|
||||
if (v != null && String(v).trim() !== '') cfg.gitea[k] = String(v).trim();
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
function getConfigPath(app) {
|
||||
if (process.env.FRACTURED_LAUNCHER_CONFIG) return process.env.FRACTURED_LAUNCHER_CONFIG;
|
||||
if (app && app.isPackaged) {
|
||||
@@ -33,11 +56,12 @@ async function loadConfig(app) {
|
||||
const defaults = JSON.parse(await fs.readFile(defPath, 'utf8'));
|
||||
try {
|
||||
const user = JSON.parse(await fs.readFile(p, 'utf8'));
|
||||
return { configPath: p, config: mergeConfig(defaults, user) };
|
||||
return { configPath: p, config: applyBakedGitea(mergeConfig(defaults, user)) };
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
await fs.writeFile(p, JSON.stringify(defaults, null, 2), 'utf8');
|
||||
return { configPath: p, config: JSON.parse(JSON.stringify(defaults)) };
|
||||
const initial = applyBakedGitea(mergeConfig(defaults, {}));
|
||||
await fs.writeFile(p, JSON.stringify(initial, null, 2), 'utf8');
|
||||
return { configPath: p, config: JSON.parse(JSON.stringify(initial)) };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
@@ -48,7 +72,7 @@ async function saveGameDir(configPath, gameDir) {
|
||||
const defaults = JSON.parse(await fs.readFile(defPath, 'utf8'));
|
||||
const user = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
||||
user.game_dir = gameDir;
|
||||
const merged = mergeConfig(defaults, user);
|
||||
const merged = applyBakedGitea(mergeConfig(defaults, user));
|
||||
await fs.writeFile(configPath, JSON.stringify(merged, null, 2), 'utf8');
|
||||
return merged;
|
||||
}
|
||||
@@ -60,4 +84,4 @@ function resolveGameDir(cfg, configPath) {
|
||||
return path.normalize(path.join(path.dirname(configPath), gd));
|
||||
}
|
||||
|
||||
module.exports = { getConfigPath, loadConfig, saveGameDir, resolveGameDir, mergeConfig };
|
||||
module.exports = { getConfigPath, loadConfig, saveGameDir, resolveGameDir, mergeConfig, applyBakedGitea };
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
'use strict';
|
||||
|
||||
const { downloadBodyToFile } = require('./http-download');
|
||||
|
||||
function normalizeGiteaBaseUrl(raw) {
|
||||
let b = String(raw || '').trim().replace(/\/+$/, '');
|
||||
if (!b) return '';
|
||||
if (!/^https?:\/\//i.test(b)) b = `https://${b}`;
|
||||
return b;
|
||||
}
|
||||
|
||||
function giteaApiBase(cfg) {
|
||||
const base = normalizeGiteaBaseUrl(cfg.gitea.base_url);
|
||||
return `${base}/api/v1`;
|
||||
}
|
||||
|
||||
function giteaToken(cfg) {
|
||||
const name = cfg.gitea && cfg.gitea.token_env;
|
||||
if (name && process.env[name]) return String(process.env[name]).trim();
|
||||
return String(process.env.GITEA_TOKEN || '').trim();
|
||||
}
|
||||
|
||||
function giteaHeaders(token, json = false) {
|
||||
const h = { 'User-Agent': 'Fractured-Launcher-Electron' };
|
||||
if (json) h.Accept = 'application/json';
|
||||
if (token) h.Authorization = `token ${token}`;
|
||||
return h;
|
||||
}
|
||||
|
||||
function useGiteaReleases(cfg) {
|
||||
const g = cfg.gitea;
|
||||
if (!g) return false;
|
||||
return !!(String(g.base_url || '').trim() && String(g.owner || '').trim() && String(g.repo || '').trim());
|
||||
}
|
||||
|
||||
async function downloadGiteaReleaseAsset(cfg, assetName, destPath) {
|
||||
const api = giteaApiBase(cfg);
|
||||
const { owner, repo } = cfg.gitea;
|
||||
const tag = (cfg.gitea.release_tag || 'latest').trim() || 'latest';
|
||||
const token = giteaToken(cfg);
|
||||
|
||||
let listUrl;
|
||||
if (tag.toLowerCase() === 'latest') {
|
||||
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/latest`;
|
||||
} else {
|
||||
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`;
|
||||
}
|
||||
|
||||
const res = await fetch(listUrl, { headers: giteaHeaders(token, true) });
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
let hint = '';
|
||||
if (res.status === 404) hint = ' (wrong tag / no release / check base_url owner repo)';
|
||||
if (res.status === 401 || res.status === 403) hint = ' (set GITEA_TOKEN or gitea.token_env)';
|
||||
throw new Error(`Gitea release ${res.status}${hint}: ${text.slice(0, 600)}`);
|
||||
}
|
||||
const rel = JSON.parse(text);
|
||||
const list = rel.attachments || rel.assets || [];
|
||||
let downloadUrl = '';
|
||||
for (const a of list) {
|
||||
if (a.name !== assetName) continue;
|
||||
downloadUrl = a.browser_download_url || a.download_url || '';
|
||||
break;
|
||||
}
|
||||
if (!downloadUrl) {
|
||||
const names = list.map((x) => x.name).filter(Boolean);
|
||||
throw new Error(`Gitea release asset "${assetName}" not found; attachments: ${names.join(', ') || '(none)'}`);
|
||||
}
|
||||
|
||||
const h = { Accept: 'application/octet-stream' };
|
||||
if (token) h.Authorization = `token ${token}`;
|
||||
const dl = await fetch(downloadUrl, { headers: h, redirect: 'follow' });
|
||||
await downloadBodyToFile(dl, destPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base URL for electron-updater generic provider (expects latest.yml under this path).
|
||||
* Matches Gitea’s pattern: …/owner/repo/releases/download/{tag}/latest.yml
|
||||
*/
|
||||
async function getGiteaUpdaterFeedBase(cfg) {
|
||||
if (!useGiteaReleases(cfg)) return null;
|
||||
const api = giteaApiBase(cfg);
|
||||
const { owner, repo } = cfg.gitea;
|
||||
const tag = (cfg.gitea.release_tag || 'latest').trim() || 'latest';
|
||||
const token = giteaToken(cfg);
|
||||
let listUrl;
|
||||
if (tag.toLowerCase() === 'latest') {
|
||||
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/latest`;
|
||||
} else {
|
||||
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`;
|
||||
}
|
||||
const res = await fetch(listUrl, { headers: giteaHeaders(token, true) });
|
||||
if (!res.ok) return null;
|
||||
const rel = await res.json();
|
||||
const tagName = rel.tag_name;
|
||||
if (!tagName || typeof tagName !== 'string') return null;
|
||||
const root = normalizeGiteaBaseUrl(cfg.gitea.base_url);
|
||||
const url = `${root}/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/download/${encodeURIComponent(tagName)}/`;
|
||||
return { url, token };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
downloadGiteaReleaseAsset,
|
||||
giteaToken,
|
||||
useGiteaReleases,
|
||||
getGiteaUpdaterFeedBase,
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { createHash } = require('node:crypto');
|
||||
const { downloadReleaseAsset, downloadGitHubRepoFile } = require('./github');
|
||||
|
||||
async function sha256File(absPath) {
|
||||
const buf = await fs.readFile(absPath);
|
||||
return createHash('sha256').update(buf).digest('hex');
|
||||
}
|
||||
|
||||
function stateDir(gameDir) {
|
||||
return path.join(gameDir, '.fractured');
|
||||
}
|
||||
|
||||
function statePath(gameDir) {
|
||||
return path.join(stateDir(gameDir), 'patch-state.json');
|
||||
}
|
||||
|
||||
async function readPatchState(gameDir) {
|
||||
if (!gameDir) return null;
|
||||
try {
|
||||
const t = await fs.readFile(statePath(gameDir), 'utf8');
|
||||
return JSON.parse(t);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function writePatchState(gameDir, manifestVersion, fileShas) {
|
||||
const p = statePath(gameDir);
|
||||
await fs.mkdir(path.dirname(p), { recursive: true });
|
||||
const body = {
|
||||
client_build: manifestVersion,
|
||||
updated_at: new Date().toISOString(),
|
||||
files: fileShas,
|
||||
};
|
||||
const tmp = p + '.tmp';
|
||||
await fs.writeFile(tmp, JSON.stringify(body, null, 2), 'utf8');
|
||||
await fs.rename(tmp, p);
|
||||
}
|
||||
|
||||
function validateManifest(m) {
|
||||
if (!m || m.version == null || String(m.version).trim() === '') return false;
|
||||
if (!m.files || typeof m.files !== 'object') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and parse patch-manifest.json (or custom name). Returns null on any failure.
|
||||
*/
|
||||
async function loadManifest(cfg) {
|
||||
const pm = cfg.patch_manifest;
|
||||
if (!pm || !pm.enabled || !String(pm.source || '').trim()) return null;
|
||||
const tmp = path.join(os.tmpdir(), `fr-patch-manifest-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
|
||||
try {
|
||||
if (pm.from_release) {
|
||||
await downloadReleaseAsset(cfg, String(pm.source).trim(), tmp);
|
||||
} else {
|
||||
await downloadGitHubRepoFile(cfg, String(pm.source).trim(), tmp);
|
||||
}
|
||||
const raw = await fs.readFile(tmp, 'utf8');
|
||||
await fs.unlink(tmp).catch(() => {});
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
await fs.unlink(tmp).catch(() => {});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True if every from_release file on disk matches manifest sha256.
|
||||
*/
|
||||
async function patchesMatchManifest(cfg, manifest, onStatus) {
|
||||
if (!validateManifest(manifest)) return false;
|
||||
const gameDir = cfg.game_dir;
|
||||
for (const entry of cfg.files || []) {
|
||||
if (!entry.from_release) continue;
|
||||
const spec = manifest.files[entry.source];
|
||||
if (!spec || !spec.sha256) return false;
|
||||
const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean);
|
||||
const destAbs = path.join(gameDir, ...parts);
|
||||
let disk;
|
||||
try {
|
||||
disk = await sha256File(destAbs);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (disk.toLowerCase() !== String(spec.sha256).trim().toLowerCase()) return false;
|
||||
}
|
||||
if (onStatus) {
|
||||
onStatus(`Client files already match build ${manifest.version} (nothing to download).`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function verifyInstalledAgainstManifest(cfg, manifest) {
|
||||
if (!validateManifest(manifest)) return;
|
||||
for (const entry of cfg.files || []) {
|
||||
if (!entry.from_release) continue;
|
||||
const spec = manifest.files[entry.source];
|
||||
if (!spec || !spec.sha256) {
|
||||
throw new Error(
|
||||
`patch-manifest.json is missing a sha256 for "${entry.source}" — regenerate the manifest for this release.`
|
||||
);
|
||||
}
|
||||
const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean);
|
||||
const destAbs = path.join(cfg.game_dir, ...parts);
|
||||
const disk = await sha256File(destAbs);
|
||||
if (disk.toLowerCase() !== String(spec.sha256).trim().toLowerCase()) {
|
||||
throw new Error(
|
||||
`${entry.source}: checksum mismatch after install (expected ${spec.sha256.slice(0, 12)}…, got ${disk.slice(0, 12)}…). Try syncing again.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function recordPatchState(cfg, manifest) {
|
||||
if (!validateManifest(manifest)) return;
|
||||
const shas = {};
|
||||
for (const entry of cfg.files || []) {
|
||||
if (!entry.from_release) continue;
|
||||
const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean);
|
||||
const destAbs = path.join(cfg.game_dir, ...parts);
|
||||
try {
|
||||
shas[entry.source] = await sha256File(destAbs);
|
||||
} catch {
|
||||
/* skip */
|
||||
}
|
||||
}
|
||||
await writePatchState(cfg.game_dir, String(manifest.version), shas);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadManifest,
|
||||
validateManifest,
|
||||
patchesMatchManifest,
|
||||
verifyInstalledAgainstManifest,
|
||||
recordPatchState,
|
||||
readPatchState,
|
||||
statePath,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fractured-launcher-electron",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"description": "Fractured WoW launcher (Electron) — no console window, native folder picker, auto-update",
|
||||
"main": "main.js",
|
||||
"repository": {
|
||||
@@ -9,8 +9,9 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"pack:win": "node scripts/inject-release-channel.js && electron-builder --win nsis portable --x64 --publish never",
|
||||
"publish:win": "node scripts/inject-release-channel.js && electron-builder --win nsis portable --x64 --publish never"
|
||||
"pack:win": "electron-builder --win nsis portable --x64 --publish never",
|
||||
"pack:linux": "electron-builder --linux AppImage --x64 --publish never",
|
||||
"publish:win": "electron-builder --win nsis portable --x64 --publish never"
|
||||
},
|
||||
"author": "",
|
||||
"license": "GPL-3.0",
|
||||
@@ -35,6 +36,9 @@
|
||||
"renderer.js",
|
||||
"styles.css",
|
||||
"default-launcher.json",
|
||||
"lib/baked-gitea-channel.js",
|
||||
"lib/gitea-release.js",
|
||||
"lib/patch-manifest.js",
|
||||
"lib/**/*"
|
||||
],
|
||||
"win": {
|
||||
@@ -56,6 +60,18 @@
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "Fractured-Launcher-${version}-Windows-Portable.${ext}"
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": ["x64"]
|
||||
}
|
||||
],
|
||||
"category": "Game"
|
||||
},
|
||||
"appImage": {
|
||||
"artifactName": "Fractured-Launcher-${version}-Linux-x86_64.${ext}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build patch-manifest.json for a release (same names as files[].source in launcher.json).
|
||||
*
|
||||
* Usage (from a folder containing the patch binaries):
|
||||
* node generate-patch-manifest.js v0.9.0-client patch-Z.MPQ Wow-patched.exe
|
||||
*
|
||||
* Prints JSON to stdout — redirect to file:
|
||||
* node generate-patch-manifest.js v0.9.0-client patch-Z.MPQ Wow-patched.exe > patch-manifest.json
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const version = process.argv[2];
|
||||
const names = process.argv.slice(3);
|
||||
if (!version || names.length === 0) {
|
||||
console.error('Usage: generate-patch-manifest.js <version-label> <file1> [file2 ...]');
|
||||
console.error(' Example: generate-patch-manifest.js v0.9.0-client patch-Z.MPQ Wow-patched.exe');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const out = { version, files: {} };
|
||||
for (const f of names) {
|
||||
const base = path.basename(f);
|
||||
const buf = fs.readFileSync(f);
|
||||
const sha256 = crypto.createHash('sha256').update(buf).digest('hex');
|
||||
out.files[base] = { sha256 };
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Merge Gitea release channel (non-token) into default-launcher.json before pack.
|
||||
* Precedence: env → fractured-release-channel.json → leave existing default-launcher values.
|
||||
*
|
||||
* Env (any of these names):
|
||||
* FRACTURED_LAUNCHER_GITEA_BASE_URL | GITEA_BASE_URL
|
||||
* FRACTURED_LAUNCHER_GITEA_OWNER | GITEA_OWNER
|
||||
* FRACTURED_LAUNCHER_GITEA_REPO | GITEA_REPO
|
||||
* FRACTURED_LAUNCHER_GITEA_RELEASE_TAG | GITEA_RELEASE_TAG
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const defPath = path.join(root, 'default-launcher.json');
|
||||
const channelPath = path.join(root, 'fractured-release-channel.json');
|
||||
|
||||
function pickEnv() {
|
||||
return {
|
||||
base_url: String(
|
||||
process.env.FRACTURED_LAUNCHER_GITEA_BASE_URL || process.env.GITEA_BASE_URL || ''
|
||||
).trim(),
|
||||
owner: String(
|
||||
process.env.FRACTURED_LAUNCHER_GITEA_OWNER || process.env.GITEA_OWNER || ''
|
||||
).trim(),
|
||||
repo: String(
|
||||
process.env.FRACTURED_LAUNCHER_GITEA_REPO || process.env.GITEA_REPO || ''
|
||||
).trim(),
|
||||
release_tag: String(
|
||||
process.env.FRACTURED_LAUNCHER_GITEA_RELEASE_TAG || process.env.GITEA_RELEASE_TAG || ''
|
||||
).trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const cfg = JSON.parse(fs.readFileSync(defPath, 'utf8'));
|
||||
cfg.gitea = cfg.gitea && typeof cfg.gitea === 'object' ? cfg.gitea : {};
|
||||
|
||||
let fileGitea = {};
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(channelPath, 'utf8'));
|
||||
if (raw && raw.gitea && typeof raw.gitea === 'object') fileGitea = raw.gitea;
|
||||
} catch (e) {
|
||||
if (e.code !== 'ENOENT') throw e;
|
||||
}
|
||||
|
||||
const env = pickEnv();
|
||||
const keys = ['base_url', 'owner', 'repo', 'release_tag'];
|
||||
let changed = false;
|
||||
|
||||
for (const k of keys) {
|
||||
const fromEnv = env[k];
|
||||
const fromFile =
|
||||
fileGitea[k] != null && String(fileGitea[k]).trim() !== '' ? String(fileGitea[k]).trim() : '';
|
||||
const val = (fromEnv && String(fromEnv).trim()) || fromFile;
|
||||
if (!val) continue;
|
||||
if (cfg.gitea[k] !== val) {
|
||||
cfg.gitea[k] = val;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
console.log(
|
||||
'inject-release-channel: no overrides (set GITEA_* env and/or fill fractured-release-channel.json)'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(defPath, `${JSON.stringify(cfg, null, 2)}\n`, 'utf8');
|
||||
console.log('inject-release-channel: wrote gitea.* into default-launcher.json for this build');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Local Linux AppImage build (uses current tree — no tag snapshot). Run from repo root or this dir.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
echo "==> npm ci"
|
||||
npm ci
|
||||
echo "==> npm run pack:linux (AppImage x64)"
|
||||
npm run pack:linux
|
||||
echo "==> dist/:"
|
||||
ls -la dist/
|
||||
Reference in New Issue
Block a user