feat(launcher): Linux play wrapper, patch UX, Gitea sync cleanup
- Play on Linux: use launch.linux_wrapper (wine) or linux_steam_uri; chmod .exe after install - Windows: retry EBUSY on MPQ replace; download to .new before rename - After successful sync: remove .bak-* backups; realmlist only Data/enUS (ignore enGB) - Gitea/distro merge: skip Fractured-Launcher* from GitHub assets (CI default-branch build wins) - Omit blockmap and builder-debug from staged artifacts and Gitea uploads; upload script validates before clearing attachments - README and launcher version 1.0.12 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -74,8 +74,6 @@ jobs:
|
|||||||
if (Test-Path tools/fractured-launcher-electron/dist/latest.yml) {
|
if (Test-Path tools/fractured-launcher-electron/dist/latest.yml) {
|
||||||
Copy-Item tools/fractured-launcher-electron/dist/latest.yml launcher-publish/
|
Copy-Item tools/fractured-launcher-electron/dist/latest.yml launcher-publish/
|
||||||
}
|
}
|
||||||
Get-ChildItem tools/fractured-launcher-electron/dist/*.blockmap -ErrorAction SilentlyContinue |
|
|
||||||
Copy-Item -Destination launcher-publish/
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -87,6 +85,13 @@ jobs:
|
|||||||
if: github.repository == 'Dawnforger/Fractured'
|
if: github.repository == 'Dawnforger/Fractured'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.repository.default_branch }}
|
||||||
|
sparse-checkout: |
|
||||||
|
tools/fractured-launcher-electron/scripts
|
||||||
|
sparse-checkout-cone-mode: true
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: electron-dist
|
name: electron-dist
|
||||||
@@ -97,6 +102,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
. tools/fractured-launcher-electron/scripts/release-sync-filters.sh
|
||||||
TAG="${{ needs.meta.outputs.tag }}"
|
TAG="${{ needs.meta.outputs.tag }}"
|
||||||
mkdir -p combined
|
mkdir -p combined
|
||||||
mkdir -p /tmp/from-main
|
mkdir -p /tmp/from-main
|
||||||
@@ -104,6 +110,11 @@ jobs:
|
|||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
for f in /tmp/from-main/*; do
|
for f in /tmp/from-main/*; do
|
||||||
if [ -f "$f" ]; then
|
if [ -f "$f" ]; then
|
||||||
|
bn=$(basename "$f")
|
||||||
|
if should_skip_merge_from_github "$bn"; then
|
||||||
|
echo "Skipping GitHub release asset (CI launcher or excluded): $bn"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
cp -f "$f" combined/
|
cp -f "$f" combined/
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
tools/fractured-launcher-electron/dist/*.exe
|
tools/fractured-launcher-electron/dist/*.exe
|
||||||
tools/fractured-launcher-electron/dist/latest.yml
|
tools/fractured-launcher-electron/dist/latest.yml
|
||||||
tools/fractured-launcher-electron/dist/*.blockmap
|
|
||||||
|
|
||||||
electron-launcher-linux:
|
electron-launcher-linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -77,5 +76,5 @@ jobs:
|
|||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
path: |
|
path: |
|
||||||
tools/fractured-launcher-electron/dist/*.AppImage
|
tools/fractured-launcher-electron/dist/*.AppImage
|
||||||
tools/fractured-launcher-electron/dist/*.yml
|
tools/fractured-launcher-electron/dist/latest.yml
|
||||||
tools/fractured-launcher-electron/dist/*.blockmap
|
tools/fractured-launcher-electron/dist/latest-linux.yml
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ jobs:
|
|||||||
working-directory: tools/fractured-launcher-electron
|
working-directory: tools/fractured-launcher-electron
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
|
node -p "'Launcher package.json version: ' + require('./package.json').version"
|
||||||
npm run pack:win
|
npm run pack:win
|
||||||
|
|
||||||
- name: Stage launcher files for upload
|
- name: Stage launcher files for upload
|
||||||
@@ -114,8 +115,6 @@ jobs:
|
|||||||
if (Test-Path tools/fractured-launcher-electron/dist/latest.yml) {
|
if (Test-Path tools/fractured-launcher-electron/dist/latest.yml) {
|
||||||
Copy-Item tools/fractured-launcher-electron/dist/latest.yml launcher-publish/
|
Copy-Item tools/fractured-launcher-electron/dist/latest.yml launcher-publish/
|
||||||
}
|
}
|
||||||
Get-ChildItem tools/fractured-launcher-electron/dist/*.blockmap -ErrorAction SilentlyContinue |
|
|
||||||
Copy-Item -Destination launcher-publish/
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -150,6 +149,7 @@ jobs:
|
|||||||
working-directory: tools/fractured-launcher-electron
|
working-directory: tools/fractured-launcher-electron
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
|
node -p "'Launcher package.json version: ' + require('./package.json').version"
|
||||||
npm run pack:linux
|
npm run pack:linux
|
||||||
|
|
||||||
- name: Stage Linux launcher for upload
|
- name: Stage Linux launcher for upload
|
||||||
@@ -159,8 +159,9 @@ jobs:
|
|||||||
mkdir -p launcher-linux-publish
|
mkdir -p launcher-linux-publish
|
||||||
shopt -s nullglob
|
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/*.AppImage launcher-linux-publish/ 2>/dev/null || true
|
||||||
cp -f tools/fractured-launcher-electron/dist/*.yml launcher-linux-publish/ 2>/dev/null || true
|
for f in tools/fractured-launcher-electron/dist/latest.yml tools/fractured-launcher-electron/dist/latest-linux.yml; do
|
||||||
cp -f tools/fractured-launcher-electron/dist/*.blockmap launcher-linux-publish/ 2>/dev/null || true
|
if [ -f "$f" ]; then cp -f "$f" launcher-linux-publish/; fi
|
||||||
|
done
|
||||||
ls -la launcher-linux-publish/
|
ls -la launcher-linux-publish/
|
||||||
if ! compgen -G "launcher-linux-publish/*.AppImage" > /dev/null; then
|
if ! compgen -G "launcher-linux-publish/*.AppImage" > /dev/null; then
|
||||||
echo "No AppImage under dist/ — electron-builder linux target failed" >&2
|
echo "No AppImage under dist/ — electron-builder linux target failed" >&2
|
||||||
@@ -206,6 +207,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
. tools/fractured-launcher-electron/scripts/release-sync-filters.sh
|
||||||
TAG="${{ needs.meta.outputs.tag }}"
|
TAG="${{ needs.meta.outputs.tag }}"
|
||||||
mkdir -p combined
|
mkdir -p combined
|
||||||
mkdir -p /tmp/from-main
|
mkdir -p /tmp/from-main
|
||||||
@@ -213,6 +215,11 @@ jobs:
|
|||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
for f in /tmp/from-main/*; do
|
for f in /tmp/from-main/*; do
|
||||||
if [ -f "$f" ]; then
|
if [ -f "$f" ]; then
|
||||||
|
bn=$(basename "$f")
|
||||||
|
if should_skip_merge_from_github "$bn"; then
|
||||||
|
echo "Skipping GitHub release asset (CI launcher or excluded): $bn"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
cp -f "$f" combined/
|
cp -f "$f" combined/
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ CI workflow **Sync release to Gitea** (`.github/workflows/gitea-release-sync.yml
|
|||||||
1. Triggers on **release published** on **`Dawnforger/Fractured`** (or **workflow_dispatch** with a tag).
|
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.
|
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.).
|
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).
|
4. Merges with the built launcher artifacts and uploads to a **Gitea release** with the **same tag** (existing attachments on that Gitea release are replaced). **Launcher installers** attached on GitHub (**`Fractured-Launcher*`**, case-insensitive) are **not** merged — the **CI build from the default branch** is the only source of launcher binaries, so an old installer on the GitHub release cannot “stick” on Gitea next to a newer build. **`*.blockmap`** and **`builder-debug.yml`** are omitted from the merge and from Gitea uploads.
|
||||||
|
|
||||||
**GitHub Actions secrets** (repository → Settings → Secrets and variables → Actions):
|
**GitHub Actions secrets** (repository → Settings → Secrets and variables → Actions):
|
||||||
|
|
||||||
@@ -127,6 +127,9 @@ CI workflow **Sync release to Gitea** (`.github/workflows/gitea-release-sync.yml
|
|||||||
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).
|
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).
|
||||||
9. **`sync Wow.exe: fetch failed`** — Often **HTTPS/TLS** to Gitea; use **`http://…`** in **`lib/baked-gitea-channel.js`** if you only serve plain HTTP, or fix certs / **`NODE_EXTRA_CA_CERTS`**. Ensure **`Wow-patched.exe`** exists on the release (**`release_tag`**: `latest` vs pinned). Errors include the failing URL when possible.
|
9. **`sync Wow.exe: fetch failed`** — Often **HTTPS/TLS** to Gitea; use **`http://…`** in **`lib/baked-gitea-channel.js`** if you only serve plain HTTP, or fix certs / **`NODE_EXTRA_CA_CERTS`**. Ensure **`Wow-patched.exe`** exists on the release (**`release_tag`**: `latest` vs pinned). Errors include the failing URL when possible.
|
||||||
10. **Wine + Windows portable** — If the folder picker returns **`/home/...`**, the launcher maps it to **`Z:\home\...`** (Wine’s Unix root). **`Wow.exe`** is matched case-insensitively for Linux-backed folders. Re-save the WoW folder after upgrading if validation still fails.
|
10. **Wine + Windows portable** — If the folder picker returns **`/home/...`**, the launcher maps it to **`Z:\home\...`** (Wine’s Unix root). **`Wow.exe`** is matched case-insensitively for Linux-backed folders. Re-save the WoW folder after upgrading if validation still fails.
|
||||||
|
11. **`EBUSY` / file locked on Windows** — The client (or antivirus) may keep **`.MPQ`** files open. The launcher **retries** for a short window and downloads the new file **before** replacing the old one; if sync still fails, **exit WoW** (and any tool previewing that folder) and run **Download updates** again.
|
||||||
|
12. **`.bak-*` clutter** — When **Download updates** finishes without error, the launcher removes matching **`*.bak-YYYYMMDD-HHmmss`** files from earlier runs (same pattern it uses when replacing files). Failed syncs do not delete backups.
|
||||||
|
13. **Gitea still shows an old launcher version** — The sync workflow overlays **`tools/fractured-launcher-electron` from the default branch**, so **`package.json`** there defines the built version. Ensure launcher changes are **merged to `main`** before publishing the GitHub release (or re-run **Actions → Sync release to Gitea** after merging). Previously, **Fractured-Launcher\*** files **attached on the GitHub release** were merged too, which could leave **two** installer versions on Gitea; those assets are now skipped in favor of CI-only builds.
|
||||||
|
|
||||||
### Private Gitea token for players
|
### Private Gitea token for players
|
||||||
|
|
||||||
@@ -170,4 +173,4 @@ Schema is defined by **`default-launcher.json`** (shipped in the app; first run
|
|||||||
- **`github`**: used for **non-release** repo paths (`from_release`: false) and for **GitHub** **`from_release`** when **`gitea.base_url`** is empty.
|
- **`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).
|
- **`patch_manifest`**: **`enabled`**, **`source`** (default `patch-manifest.json`), **`from_release`** — checksum-based skip + verify (see above).
|
||||||
- **`files`**: default **`[]`**. **Download updates** resolves what to pull in order: (**1**) non-empty **`files`** if you set explicit **`source`** → **`dest`** pairs; (**2**) else each key in **`patch-manifest.json`** on the release (recommended); (**3**) else release attachments except launcher artifacts (`Fractured-Launcher*`, `*.blockmap`, `latest*.yml`, `.AppImage`, `patch-manifest.json`): **`.MPQ`** → **`Data/enUS/<name>.MPQ`** (extension forced to **`.MPQ`** caps for client compatibility), one **`.exe`** → **`launch.exe`**. Multiple `.exe` attachments require a manifest. Legacy **`Wow-patched.exe`** entries are removed when merging **`launcher.json`**.
|
- **`files`**: default **`[]`**. **Download updates** resolves what to pull in order: (**1**) non-empty **`files`** if you set explicit **`source`** → **`dest`** pairs; (**2**) else each key in **`patch-manifest.json`** on the release (recommended); (**3**) else release attachments except launcher artifacts (`Fractured-Launcher*`, `*.blockmap`, `latest*.yml`, `.AppImage`, `patch-manifest.json`): **`.MPQ`** → **`Data/enUS/<name>.MPQ`** (extension forced to **`.MPQ`** caps for client compatibility), one **`.exe`** → **`launch.exe`**. Multiple `.exe` attachments require a manifest. Legacy **`Wow-patched.exe`** entries are removed when merging **`launcher.json`**.
|
||||||
- **`realmlist`**, **`auth`**, **`launch`**.
|
- **`realmlist`**, **`auth`**, **`launch`** (`**exe**`, **`args`**). Only **`Data/enUS/realmlist.wtf`** is written: any **`realmlist.paths`** entry that is not the enUS file is ignored (so **`enGB`** is never created). On **Linux**, **Play** never runs `Wow.exe` as a native process (that yields **EACCES**). Use **`launch.linux_wrapper`** (default **`["wine"]`**) so the launcher runs e.g. **`wine /path/Wow.exe` …`args`**, or set **`launch.linux_steam_uri`** to a Steam URI (e.g. **`steam://rungameid/…`** for a **non-Steam game** shortcut — the number is shown in Steam’s shortcut properties). Optional **`linux_steam_binary`** defaults to **`steam`**; for Flatpak Steam use **`linux_steam_spawn`**: **`["flatpak", "run", "com.valvesoftware.Steam"]`** (the URI is appended as the last argument). After a **successful** **Download updates** run, the launcher deletes prior **`*.bak-YYYYMMDD-HHmmss`** backup files it created under the WoW folder.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"realmlist": {
|
"realmlist": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"line": "set realmlist fracturedwow.ddns.net:47497",
|
"line": "set realmlist fracturedwow.ddns.net:47497",
|
||||||
"paths": ["Data/enUS/realmlist.wtf", "Data/enGB/realmlist.wtf"]
|
"paths": ["Data/enUS/realmlist.wtf"]
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
@@ -37,6 +37,9 @@
|
|||||||
"launch": {
|
"launch": {
|
||||||
"exe": "Wow.exe",
|
"exe": "Wow.exe",
|
||||||
"args": [],
|
"args": [],
|
||||||
"linux_wrapper": ["wine"]
|
"linux_wrapper": ["wine"],
|
||||||
|
"linux_steam_uri": "",
|
||||||
|
"linux_steam_binary": "",
|
||||||
|
"linux_steam_spawn": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,42 @@ function backupSuffix() {
|
|||||||
return `${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}-${pad2(d.getHours())}${pad2(d.getMinutes())}${pad2(d.getSeconds())}`;
|
return `${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}-${pad2(d.getHours())}${pad2(d.getMinutes())}${pad2(d.getSeconds())}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((r) => setTimeout(r, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Windows often returns EBUSY/EPERM when WoW or AV still has an MPQ open. */
|
||||||
|
function isRetryableFsLockError(e) {
|
||||||
|
const c = e && e.code;
|
||||||
|
if (!c) return false;
|
||||||
|
if (c === 'EBUSY' || c === 'EPERM' || c === 'EACCES') return true;
|
||||||
|
if (process.platform === 'win32' && (c === 'UNKNOWN' || c === 'EUNKNOWN')) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function retryFsLock(op, opts) {
|
||||||
|
const attempts = (opts && opts.attempts) || (process.platform === 'win32' ? 30 : 10);
|
||||||
|
const delayMs = (opts && opts.delayMs) || 500;
|
||||||
|
let last;
|
||||||
|
for (let i = 0; i < attempts; i++) {
|
||||||
|
try {
|
||||||
|
return await op();
|
||||||
|
} catch (e) {
|
||||||
|
last = e;
|
||||||
|
if (!isRetryableFsLockError(e)) throw e;
|
||||||
|
if (i === attempts - 1) break;
|
||||||
|
await sleep(delayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hint =
|
||||||
|
process.platform === 'win32'
|
||||||
|
? ' Close World of Warcraft and any launcher using this folder, then try again.'
|
||||||
|
: ' Close programs using this file, then try again.';
|
||||||
|
const err = new Error(String((last && last.message) || last) + hint);
|
||||||
|
err.code = last && last.code;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
function wowExePath(cfg) {
|
function wowExePath(cfg) {
|
||||||
const gd = normalizeWinGameDir(cfg.game_dir || '');
|
const gd = normalizeWinGameDir(cfg.game_dir || '');
|
||||||
const exe = (cfg.launch && cfg.launch.exe) || 'Wow.exe';
|
const exe = (cfg.launch && cfg.launch.exe) || 'Wow.exe';
|
||||||
@@ -51,10 +87,59 @@ function normalizeMpqDestinationPath(absPath) {
|
|||||||
return /\.mpq$/i.test(s) ? s.replace(/\.mpq$/i, '.MPQ') : s;
|
return /\.mpq$/i.test(s) ? s.replace(/\.mpq$/i, '.MPQ') : s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Matches backup names from installFile: `<orig>.bak-YYYYMMDD-HHmmss`. */
|
||||||
|
const LAUNCHER_BACKUP_BASENAME_RE = /\.bak-\d{8}-\d{6}$/;
|
||||||
|
|
||||||
|
async function removeLauncherBackupFiles(gameDir) {
|
||||||
|
const root = normalizeWinGameDir(gameDir || '');
|
||||||
|
if (!root) return;
|
||||||
|
const stack = [root];
|
||||||
|
while (stack.length) {
|
||||||
|
const dir = stack.pop();
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
} catch (_) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const d of entries) {
|
||||||
|
const abs = path.join(dir, d.name);
|
||||||
|
if (d.isDirectory()) {
|
||||||
|
stack.push(abs);
|
||||||
|
} else if (d.isFile() && LAUNCHER_BACKUP_BASENAME_RE.test(d.name)) {
|
||||||
|
try {
|
||||||
|
await fs.unlink(abs);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code !== 'ENOENT') {
|
||||||
|
/* best effort: sync already succeeded */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEnUsRealmlistPath(rel) {
|
||||||
|
const n = String(rel || '')
|
||||||
|
.trim()
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.toLowerCase();
|
||||||
|
return n.endsWith('/enus/realmlist.wtf') || n === 'enus/realmlist.wtf';
|
||||||
|
}
|
||||||
|
|
||||||
async function installFile(cfg, entry) {
|
async function installFile(cfg, entry) {
|
||||||
const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean);
|
const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean);
|
||||||
const root = normalizeWinGameDir(cfg.game_dir || '');
|
const root = normalizeWinGameDir(cfg.game_dir || '');
|
||||||
const destAbs = normalizeMpqDestinationPath(path.join(root, ...parts));
|
const destAbs = normalizeMpqDestinationPath(path.join(root, ...parts));
|
||||||
|
const tmp = destAbs + '.new';
|
||||||
|
|
||||||
|
if (entry.from_release) {
|
||||||
|
await downloadReleaseAsset(cfg, entry.source, tmp);
|
||||||
|
} else {
|
||||||
|
await downloadGitHubRepoFile(cfg, entry.source, tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeOrBackupExisting() {
|
||||||
if (entry.backup) {
|
if (entry.backup) {
|
||||||
try {
|
try {
|
||||||
const st = await fs.stat(destAbs);
|
const st = await fs.stat(destAbs);
|
||||||
@@ -72,13 +157,18 @@ async function installFile(cfg, entry) {
|
|||||||
if (e.code !== 'ENOENT') throw e;
|
if (e.code !== 'ENOENT') throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tmp = destAbs + '.new';
|
|
||||||
if (entry.from_release) {
|
|
||||||
await downloadReleaseAsset(cfg, entry.source, tmp);
|
|
||||||
} else {
|
|
||||||
await downloadGitHubRepoFile(cfg, entry.source, tmp);
|
|
||||||
}
|
}
|
||||||
await fs.rename(tmp, destAbs);
|
|
||||||
|
await retryFsLock(() => removeOrBackupExisting());
|
||||||
|
await retryFsLock(() => fs.rename(tmp, destAbs));
|
||||||
|
|
||||||
|
if (process.platform === 'linux' && /\.exe$/i.test(destAbs)) {
|
||||||
|
try {
|
||||||
|
await fs.chmod(destAbs, 0o755);
|
||||||
|
} catch (_) {
|
||||||
|
/* non-fatal */
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyRealmlist(cfg) {
|
async function applyRealmlist(cfg) {
|
||||||
@@ -91,6 +181,8 @@ async function applyRealmlist(cfg) {
|
|||||||
const content = line + '\n';
|
const content = line + '\n';
|
||||||
let paths = cfg.realmlist.paths;
|
let paths = cfg.realmlist.paths;
|
||||||
if (!paths || !paths.length) paths = ['Data/enUS/realmlist.wtf'];
|
if (!paths || !paths.length) paths = ['Data/enUS/realmlist.wtf'];
|
||||||
|
paths = paths.filter(isEnUsRealmlistPath);
|
||||||
|
if (!paths.length) paths = ['Data/enUS/realmlist.wtf'];
|
||||||
for (const rel of paths) {
|
for (const rel of paths) {
|
||||||
const r = String(rel).trim().replace(/\\/g, '/');
|
const r = String(rel).trim().replace(/\\/g, '/');
|
||||||
if (!r) continue;
|
if (!r) continue;
|
||||||
@@ -119,6 +211,12 @@ async function applyPatches(cfg, onStatus) {
|
|||||||
if (onStatus) onStatus('Applying realmlist …');
|
if (onStatus) onStatus('Applying realmlist …');
|
||||||
await applyRealmlist(cfg);
|
await applyRealmlist(cfg);
|
||||||
}
|
}
|
||||||
|
if (onStatus) onStatus('Removing old backup copies …');
|
||||||
|
try {
|
||||||
|
await removeLauncherBackupFiles(cfg.game_dir);
|
||||||
|
} catch (_) {
|
||||||
|
/* Patches and realmlist already applied; leave .bak files if cleanup cannot run. */
|
||||||
|
}
|
||||||
if (onStatus) onStatus('All patches applied.');
|
if (onStatus) onStatus('All patches applied.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,9 +145,44 @@ ipcMain.handle('launcher:checkUpdates', async () => {
|
|||||||
ipcMain.handle('launcher:play', async () => {
|
ipcMain.handle('launcher:play', async () => {
|
||||||
const { config } = await readMergedConfig();
|
const { config } = await readMergedConfig();
|
||||||
const exe = wowExePath(config);
|
const exe = wowExePath(config);
|
||||||
const args = (config.launch && config.launch.args) || [];
|
const gameArgs = (config.launch && config.launch.args) || [];
|
||||||
const child = spawn(exe, args, {
|
const lc = config.launch || {};
|
||||||
cwd: config.game_dir,
|
const cwd = config.game_dir;
|
||||||
|
|
||||||
|
let cmd;
|
||||||
|
let spawnArgs;
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
const steamUri = String(lc.linux_steam_uri || '').trim();
|
||||||
|
const steamSpawn = Array.isArray(lc.linux_steam_spawn) ? lc.linux_steam_spawn.filter(Boolean) : [];
|
||||||
|
if (steamUri) {
|
||||||
|
if (steamSpawn.length) {
|
||||||
|
cmd = steamSpawn[0];
|
||||||
|
spawnArgs = [...steamSpawn.slice(1), steamUri];
|
||||||
|
} else {
|
||||||
|
const bin = String(lc.linux_steam_binary || 'steam').trim() || 'steam';
|
||||||
|
cmd = bin;
|
||||||
|
spawnArgs = [steamUri];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const wrap = Array.isArray(lc.linux_wrapper) ? lc.linux_wrapper.filter(Boolean) : [];
|
||||||
|
if (wrap.length) {
|
||||||
|
cmd = wrap[0];
|
||||||
|
spawnArgs = [...wrap.slice(1), exe, ...gameArgs];
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'On Linux, Wow.exe is a Windows program and cannot be run directly. ' +
|
||||||
|
'Set launch.linux_steam_uri (e.g. steam://rungameid/… for your Steam shortcut) ' +
|
||||||
|
'or launch.linux_wrapper (e.g. ["wine"]) in launcher.json.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cmd = exe;
|
||||||
|
spawnArgs = gameArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const child = spawn(cmd, spawnArgs, {
|
||||||
|
cwd,
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fractured-launcher-electron",
|
"name": "fractured-launcher-electron",
|
||||||
"version": "1.0.9",
|
"version": "1.0.12",
|
||||||
"description": "Fractured WoW launcher (Electron) — no console window, native folder picker, auto-update",
|
"description": "Fractured WoW launcher (Electron) — no console window, native folder picker, auto-update",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared filters for GitHub → Gitea / distro release merges and Gitea uploads.
|
||||||
|
# shellcheck shell=bash
|
||||||
|
|
||||||
|
# Skip when copying assets from `gh release download` into combined/: CI-built launcher is authoritative.
|
||||||
|
should_skip_merge_from_github() {
|
||||||
|
local l
|
||||||
|
l=$(printf '%s' "${1##*/}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
case "$l" in
|
||||||
|
fractured-launcher*) return 0 ;;
|
||||||
|
*.blockmap) return 0 ;;
|
||||||
|
builder-debug.yml|builder-debug.yaml) return 0 ;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Skip when POSTing attachments to Gitea (belt-and-suspenders if something slips into combined/).
|
||||||
|
should_skip_gitea_upload() {
|
||||||
|
local l
|
||||||
|
l=$(printf '%s' "${1##*/}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
case "$l" in
|
||||||
|
*.blockmap) return 0 ;;
|
||||||
|
builder-debug.yml|builder-debug.yaml) return 0 ;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#
|
#
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
# shellcheck source=release-sync-filters.sh
|
||||||
|
. "$SCRIPT_DIR/release-sync-filters.sh"
|
||||||
|
|
||||||
COMBINED_DIR="${1:?first arg: directory of files to attach}"
|
COMBINED_DIR="${1:?first arg: directory of files to attach}"
|
||||||
TAG="${2:?second arg: release tag (e.g. v1.0.0)}"
|
TAG="${2:?second arg: release tag (e.g. v1.0.0)}"
|
||||||
|
|
||||||
@@ -67,12 +71,6 @@ if [ -z "$rel_id" ] || [ "$rel_id" = "null" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
while read -r aid; do
|
|
||||||
[ -z "$aid" ] || [ "$aid" = "null" ] && continue
|
|
||||||
curl -fsS -X DELETE "${AUTH_H[@]}" \
|
|
||||||
"$API/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/${rel_id}/assets/${aid}" || true
|
|
||||||
done < <(jq -r '(.attachments // .assets // [])[] | .id' "$REL_JSON")
|
|
||||||
|
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
files=("$COMBINED_DIR"/*)
|
files=("$COMBINED_DIR"/*)
|
||||||
if [ "${#files[@]}" -eq 0 ]; then
|
if [ "${#files[@]}" -eq 0 ]; then
|
||||||
@@ -80,12 +78,39 @@ if [ "${#files[@]}" -eq 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
uploadable=0
|
||||||
for f in "${files[@]}"; do
|
for f in "${files[@]}"; do
|
||||||
[ -f "$f" ] || continue
|
[ -f "$f" ] || continue
|
||||||
echo "Uploading $(basename "$f") …"
|
bn=$(basename "$f")
|
||||||
|
if should_skip_gitea_upload "$bn"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
uploadable=$((uploadable + 1))
|
||||||
|
done
|
||||||
|
if [ "$uploadable" -eq 0 ]; then
|
||||||
|
echo "No files to upload after exclusions (check $COMBINED_DIR) — not clearing Gitea attachments." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while read -r aid; do
|
||||||
|
[ -z "$aid" ] || [ "$aid" = "null" ] && continue
|
||||||
|
curl -fsS -X DELETE "${AUTH_H[@]}" \
|
||||||
|
"$API/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/${rel_id}/assets/${aid}" || true
|
||||||
|
done < <(jq -r '(.attachments // .assets // [])[] | .id' "$REL_JSON")
|
||||||
|
|
||||||
|
uploaded=0
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
bn=$(basename "$f")
|
||||||
|
if should_skip_gitea_upload "$bn"; then
|
||||||
|
echo "Skipping upload (excluded): $bn"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "Uploading $bn …"
|
||||||
curl -fsS -X POST "${AUTH_H[@]}" \
|
curl -fsS -X POST "${AUTH_H[@]}" \
|
||||||
-F "attachment=@${f}" \
|
-F "attachment=@${f}" \
|
||||||
"$API/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/${rel_id}/assets"
|
"$API/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/${rel_id}/assets"
|
||||||
|
uploaded=$((uploaded + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Gitea release $TAG (id=$rel_id) updated with ${#files[@]} file(s)."
|
echo "Gitea release $TAG (id=$rel_id) updated with $uploaded file(s)."
|
||||||
|
|||||||
Reference in New Issue
Block a user