Files
Fractured/tools/fractured-launcher-electron/lib/patch.js
T
Docker Build f409ffad12 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>
2026-05-10 21:54:04 -05:00

139 lines
4.2 KiB
JavaScript

'use strict';
const path = require('path');
const fs = require('fs').promises;
const fsSync = require('fs');
const { downloadGitHubRepoFile, downloadReleaseAsset } = require('./github');
const { normalizeWinGameDir } = require('./win-game-dir');
function pad2(n) {
return String(n).padStart(2, '0');
}
function backupSuffix() {
const d = new Date();
return `${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}-${pad2(d.getHours())}${pad2(d.getMinutes())}${pad2(d.getSeconds())}`;
}
function wowExePath(cfg) {
const gd = normalizeWinGameDir(cfg.game_dir || '');
const exe = (cfg.launch && cfg.launch.exe) || 'Wow.exe';
const parts = exe.replace(/\\/g, '/').split('/').filter(Boolean);
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) {
if (!cfg.game_dir) return false;
const p = wowExePath(cfg);
return fsSync.existsSync(p) && fsSync.statSync(p).isFile();
}
async function installFile(cfg, entry) {
const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean);
const root = normalizeWinGameDir(cfg.game_dir || '');
const destAbs = path.join(root, ...parts);
if (entry.backup) {
try {
const st = await fs.stat(destAbs);
if (st.isFile()) {
const bak = `${destAbs}.bak-${backupSuffix()}`;
await fs.rename(destAbs, bak);
}
} catch (e) {
if (e.code !== 'ENOENT') throw e;
}
} else {
try {
await fs.unlink(destAbs);
} catch (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);
}
async function applyRealmlist(cfg) {
if (!cfg.realmlist || !cfg.realmlist.enabled) return;
let line = String(cfg.realmlist.line || '').trim();
if (!line) throw new Error('realmlist.line empty');
if (!line.toLowerCase().startsWith('set realmlist ')) {
line = `set realmlist ${line}`;
}
const content = line + '\n';
let paths = cfg.realmlist.paths;
if (!paths || !paths.length) paths = ['Data/enUS/realmlist.wtf'];
for (const rel of paths) {
const r = String(rel).trim().replace(/\\/g, '/');
if (!r) continue;
const segs = r.split('/').filter(Boolean);
const abs = path.join(normalizeWinGameDir(cfg.game_dir || ''), ...segs);
await fs.mkdir(path.dirname(abs), { recursive: true });
await fs.writeFile(abs, content, 'utf8');
}
}
async function applyPatches(cfg, onStatus) {
for (const f of cfg.files || []) {
if (onStatus) onStatus(`Updating ${f.dest}`);
try {
await installFile(cfg, f);
} catch (e) {
throw new Error(`sync ${f.dest}: ${e.message || e}`);
}
}
if (cfg.realmlist && cfg.realmlist.enabled) {
if (onStatus) onStatus('Applying realmlist …');
await applyRealmlist(cfg);
}
if (onStatus) onStatus('All patches applied.');
}
async function doAuth(cfg, username, password) {
if (!cfg.auth || !cfg.auth.enabled) return;
const u = String(username || '').trim();
const p = String(password || '');
if (!u || !p) throw new Error('username and password required');
const body = {
[cfg.auth.username_field || 'username']: u,
[cfg.auth.password_field || 'password']: p,
};
const res = await fetch(cfg.auth.url, {
method: cfg.auth.method || 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(body),
});
const t = await res.text();
if (res.status < 200 || res.status >= 300) {
throw new Error(`login failed ${res.status}: ${t.slice(0, 400)}`);
}
}
module.exports = {
applyPatches,
applyRealmlist,
wowExePath,
wowInstallValid,
doAuth,
};