'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'); const { loadManifest } = require('./patch-manifest'); const { buildResolvedReleaseFiles } = require('./release-sync'); 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(); } /** WoW expects patch MPQ names with a literal .MPQ extension (case-sensitive clients). */ function normalizeMpqDestinationPath(absPath) { const s = String(absPath || ''); return /\.mpq$/i.test(s) ? s.replace(/\.mpq$/i, '.MPQ') : s; } async function installFile(cfg, entry) { const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean); const root = normalizeWinGameDir(cfg.game_dir || ''); const destAbs = normalizeMpqDestinationPath(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) { let manifest = null; if (cfg.patch_manifest && cfg.patch_manifest.enabled) { manifest = await loadManifest(cfg); } const entries = await buildResolvedReleaseFiles(cfg, manifest); for (const f of entries) { 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, };