f409ffad12
- 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>
139 lines
4.2 KiB
JavaScript
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,
|
|
};
|