fix(launcher): Gitea http URL; Wine Z: path + Wow.exe case check

- baked-gitea-channel: http:// for brassnet mirror.
- win-game-dir: map Unix /home/... to Z:\ under win32 (Wine folder picker).
- resolveGameDir + saveGameDir + patch paths use it; Wow.exe resolved case-insensitively.
- Version 1.0.6; README checklist for Wine.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Docker Build
2026-05-10 21:54:04 -05:00
parent c1f7eaa153
commit f409ffad12
7 changed files with 59 additions and 11 deletions
+2 -1
View File
@@ -125,7 +125,8 @@ CI workflow **Sync release to Gitea** (`.github/workflows/gitea-release-sync.yml
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. 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. 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). 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`** (Linux/AppImage especially) — Usually **TLS** (self-signed Gitea cert) or **cannot reach** the host. Use a **trusted certificate** on Gitea, or put your CA in the system trust store, or set **`NODE_EXTRA_CA_CERTS=/path/to/ca.pem`** when launching the AppImage if you must use self-signed HTTPS. If the mirror is LAN-only HTTP, set **`base_url`** in **`lib/baked-gitea-channel.js`** to **`http://…`** (only if acceptable). Ensure **`Wow-patched.exe`** exists on the **same Gitea release** you configured (**`release_tag`**: `latest` vs pinned tag). Newer builds show the failing URL and TLS hints in the error text. 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\...`** (Wines Unix root). **`Wow.exe`** is matched case-insensitively for Linux-backed folders. Re-save the WoW folder after upgrading if validation still fails.
### Private Gitea token for players ### Private Gitea token for players
@@ -6,8 +6,8 @@
* Token stays in env: GITEA_TOKEN or launcher.json → gitea.token_env. * Token stays in env: GITEA_TOKEN or launcher.json → gitea.token_env.
*/ */
module.exports = { module.exports = {
// Scheme optional — gitea-release normalizes to https:// if missing. // http:// kept as-is; bare host gets https in gitea-release.js
base_url: 'https://brassnet.ddns.net:33983', base_url: 'http://brassnet.ddns.net:33983',
owner: 'Dawnsorrow', owner: 'Dawnsorrow',
repo: 'Fractured-Distro', repo: 'Fractured-Distro',
release_tag: 'latest', release_tag: 'latest',
@@ -2,6 +2,7 @@
const path = require('path'); const path = require('path');
const fs = require('fs').promises; const fs = require('fs').promises;
const { normalizeWinGameDir } = require('./win-game-dir');
/** Sources no longer shipped; drop from merged files so old launcher.json does not keep fetching them. */ /** Sources no longer shipped; drop from merged files so old launcher.json does not keep fetching them. */
const DEPRECATED_FILE_SOURCES = new Set(['patch-Z.MPQ']); const DEPRECATED_FILE_SOURCES = new Set(['patch-Z.MPQ']);
@@ -105,8 +106,9 @@ async function saveGameDir(configPath, gameDir) {
function resolveGameDir(cfg, configPath) { function resolveGameDir(cfg, configPath) {
const gd = cfg.game_dir; const gd = cfg.game_dir;
if (!gd) return ''; if (!gd) return '';
if (path.isAbsolute(gd)) return path.normalize(gd); const abs = path.isAbsolute(gd) ? path.normalize(gd) : path.normalize(path.join(path.dirname(configPath), gd));
return path.normalize(path.join(path.dirname(configPath), gd)); if (process.platform === 'win32') return normalizeWinGameDir(abs);
return abs;
} }
module.exports = { getConfigPath, loadConfig, saveGameDir, resolveGameDir, mergeConfig, applyBakedGitea }; module.exports = { getConfigPath, loadConfig, saveGameDir, resolveGameDir, mergeConfig, applyBakedGitea };
+25 -4
View File
@@ -2,7 +2,9 @@
const path = require('path'); const path = require('path');
const fs = require('fs').promises; const fs = require('fs').promises;
const fsSync = require('fs');
const { downloadGitHubRepoFile, downloadReleaseAsset } = require('./github'); const { downloadGitHubRepoFile, downloadReleaseAsset } = require('./github');
const { normalizeWinGameDir } = require('./win-game-dir');
function pad2(n) { function pad2(n) {
return String(n).padStart(2, '0'); return String(n).padStart(2, '0');
@@ -13,19 +15,38 @@ function backupSuffix() {
} }
function wowExePath(cfg) { function wowExePath(cfg) {
const gd = normalizeWinGameDir(cfg.game_dir || '');
const exe = (cfg.launch && cfg.launch.exe) || 'Wow.exe'; const exe = (cfg.launch && cfg.launch.exe) || 'Wow.exe';
const parts = exe.replace(/\\/g, '/').split('/').filter(Boolean); const parts = exe.replace(/\\/g, '/').split('/').filter(Boolean);
return path.join(cfg.game_dir, ...parts); const primary = path.join(gd, ...parts);
if (process.platform === 'win32' && gd && fsSync.existsSync(primary)) return primary;
if (process.platform === 'win32' && gd) {
try {
const base = path.basename(primary);
const dir = path.dirname(primary);
const names = fsSync.readdirSync(dir);
const hit = names.find((n) => n.toLowerCase() === base.toLowerCase());
if (hit) {
const alt = path.join(dir, hit);
if (fsSync.statSync(alt).isFile()) return alt;
}
} catch (_) {
/* ignore */
}
}
return primary;
} }
function wowInstallValid(cfg) { function wowInstallValid(cfg) {
if (!cfg.game_dir) return false; if (!cfg.game_dir) return false;
return require('fs').existsSync(wowExePath(cfg)); const p = wowExePath(cfg);
return fsSync.existsSync(p) && fsSync.statSync(p).isFile();
} }
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 destAbs = path.join(cfg.game_dir, ...parts); const root = normalizeWinGameDir(cfg.game_dir || '');
const destAbs = path.join(root, ...parts);
if (entry.backup) { if (entry.backup) {
try { try {
const st = await fs.stat(destAbs); const st = await fs.stat(destAbs);
@@ -66,7 +87,7 @@ async function applyRealmlist(cfg) {
const r = String(rel).trim().replace(/\\/g, '/'); const r = String(rel).trim().replace(/\\/g, '/');
if (!r) continue; if (!r) continue;
const segs = r.split('/').filter(Boolean); const segs = r.split('/').filter(Boolean);
const abs = path.join(cfg.game_dir, ...segs); const abs = path.join(normalizeWinGameDir(cfg.game_dir || ''), ...segs);
await fs.mkdir(path.dirname(abs), { recursive: true }); await fs.mkdir(path.dirname(abs), { recursive: true });
await fs.writeFile(abs, content, 'utf8'); await fs.writeFile(abs, content, 'utf8');
} }
@@ -0,0 +1,21 @@
'use strict';
const path = require('path');
/**
* Under Wine, the folder picker often returns a Unix absolute path (/home/...).
* Windows Node does not resolve that to the WoW install; map to Wine's Z: drive
* (Z: == / on typical Wine prefixes).
*/
function normalizeWinGameDir(gameDir) {
if (process.platform !== 'win32') return String(gameDir || '').trim();
let s = String(gameDir || '').trim();
if (!s) return s;
s = s.replace(/\//g, path.win32.sep);
if (s.startsWith('\\\\')) return path.normalize(s);
if (/^[A-Za-z]:/.test(s)) return path.normalize(s);
if (s.startsWith(path.win32.sep)) return path.win32.normalize(`Z:${s}`);
return path.normalize(s);
}
module.exports = { normalizeWinGameDir };
+3 -1
View File
@@ -4,6 +4,7 @@ const { app, BrowserWindow, ipcMain, dialog, Menu } = require('electron');
const path = require('path'); const path = require('path');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const { loadConfig, saveGameDir, resolveGameDir } = require('./lib/config-store'); const { loadConfig, saveGameDir, resolveGameDir } = require('./lib/config-store');
const { normalizeWinGameDir } = require('./lib/win-game-dir');
const { applyPatches, wowExePath, wowInstallValid, doAuth } = require('./lib/patch'); const { applyPatches, wowExePath, wowInstallValid, doAuth } = require('./lib/patch');
const { readPatchState } = require('./lib/patch-manifest'); const { readPatchState } = require('./lib/patch-manifest');
const { setupAutoUpdater } = require('./lib/auto-update'); const { setupAutoUpdater } = require('./lib/auto-update');
@@ -95,7 +96,8 @@ ipcMain.handle('launcher:saveGameDir', async (_e, dir) => {
const trimmed = String(dir || '').trim(); const trimmed = String(dir || '').trim();
if (!trimmed) throw new Error('folder path is empty'); if (!trimmed) throw new Error('folder path is empty');
const { configPath } = await loadConfig(app); const { configPath } = await loadConfig(app);
const norm = path.normalize(trimmed); const norm =
process.platform === 'win32' ? normalizeWinGameDir(path.normalize(trimmed)) : path.normalize(trimmed);
const probe = { ...(await readMergedConfig()).config, game_dir: norm }; const probe = { ...(await readMergedConfig()).config, game_dir: norm };
if (!wowInstallValid(probe)) { if (!wowInstallValid(probe)) {
throw new Error(`That folder does not contain ${(probe.launch && probe.launch.exe) || 'Wow.exe'}`); throw new Error(`That folder does not contain ${(probe.launch && probe.launch.exe) || 'Wow.exe'}`);
@@ -1,6 +1,6 @@
{ {
"name": "fractured-launcher-electron", "name": "fractured-launcher-electron",
"version": "1.0.5", "version": "1.0.6",
"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": {
@@ -36,6 +36,7 @@
"renderer.js", "renderer.js",
"styles.css", "styles.css",
"default-launcher.json", "default-launcher.json",
"lib/win-game-dir.js",
"lib/baked-gitea-channel.js", "lib/baked-gitea-channel.js",
"lib/gitea-release.js", "lib/gitea-release.js",
"lib/patch-manifest.js", "lib/patch-manifest.js",