feat(launcher): sync release assets from manifest or attachment list (no fixed exe name)
- default files []: resolve sync list from patch-manifest keys, else discover release attachments (exclude launcher artifacts). - Explicit files[] still overrides; strip deprecated Wow-patched.exe on merge. - listReleaseAttachmentNames + fetchGiteaReleaseRecord helpers. - Version 1.0.7; README config docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,14 +5,16 @@ 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. */
|
||||
const DEPRECATED_FILE_SOURCES = new Set(['patch-Z.MPQ']);
|
||||
const DEPRECATED_FILE_SOURCES = new Set(['patch-Z.MPQ', 'Wow-patched.exe']);
|
||||
|
||||
function mergeFilesList(defaults, user) {
|
||||
const raw =
|
||||
Array.isArray(user.files) && user.files.length ? user.files.map((e) => ({ ...e })) : defaults.files.map((e) => ({ ...e }));
|
||||
const filtered = raw.filter((e) => !DEPRECATED_FILE_SOURCES.has(String(e && e.source ? e.source : '').trim()));
|
||||
if (filtered.length) return filtered;
|
||||
return defaults.files.map((e) => ({ ...e }));
|
||||
const dep = (e) => DEPRECATED_FILE_SOURCES.has(String(e && e.source ? e.source : '').trim());
|
||||
if (Array.isArray(user.files) && user.files.length) {
|
||||
const filtered = user.files.map((e) => ({ ...e })).filter((e) => !dep(e));
|
||||
if (filtered.length) return filtered;
|
||||
}
|
||||
const defList = Array.isArray(defaults.files) ? defaults.files : [];
|
||||
return defList.map((e) => ({ ...e })).filter((e) => !dep(e));
|
||||
}
|
||||
|
||||
function userFilesContainDeprecated(user) {
|
||||
|
||||
@@ -33,7 +33,7 @@ function useGiteaReleases(cfg) {
|
||||
return !!(String(g.base_url || '').trim() && String(g.owner || '').trim() && String(g.repo || '').trim());
|
||||
}
|
||||
|
||||
async function downloadGiteaReleaseAsset(cfg, assetName, destPath) {
|
||||
async function fetchGiteaReleaseRecord(cfg) {
|
||||
const api = giteaApiBase(cfg);
|
||||
const { owner, repo } = cfg.gitea;
|
||||
const tag = (cfg.gitea.release_tag || 'latest').trim() || 'latest';
|
||||
@@ -54,7 +54,18 @@ async function downloadGiteaReleaseAsset(cfg, assetName, destPath) {
|
||||
if (res.status === 401 || res.status === 403) hint = ' (set GITEA_TOKEN or gitea.token_env)';
|
||||
throw new Error(`Gitea release ${res.status}${hint}: ${text.slice(0, 600)}`);
|
||||
}
|
||||
const rel = JSON.parse(text);
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
async function listGiteaReleaseAttachmentNames(cfg) {
|
||||
const rel = await fetchGiteaReleaseRecord(cfg);
|
||||
const list = rel.attachments || rel.assets || [];
|
||||
return list.map((x) => x.name).filter(Boolean);
|
||||
}
|
||||
|
||||
async function downloadGiteaReleaseAsset(cfg, assetName, destPath) {
|
||||
const token = giteaToken(cfg);
|
||||
const rel = await fetchGiteaReleaseRecord(cfg);
|
||||
const list = rel.attachments || rel.assets || [];
|
||||
let downloadUrl = '';
|
||||
for (const a of list) {
|
||||
@@ -101,6 +112,8 @@ async function getGiteaUpdaterFeedBase(cfg) {
|
||||
|
||||
module.exports = {
|
||||
downloadGiteaReleaseAsset,
|
||||
fetchGiteaReleaseRecord,
|
||||
listGiteaReleaseAttachmentNames,
|
||||
giteaToken,
|
||||
useGiteaReleases,
|
||||
getGiteaUpdaterFeedBase,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const { githubToken } = require('./github-token');
|
||||
const { downloadGiteaReleaseAsset, useGiteaReleases } = require('./gitea-release');
|
||||
const { downloadGiteaReleaseAsset, useGiteaReleases, listGiteaReleaseAttachmentNames } = require('./gitea-release');
|
||||
const { fetchToFile, downloadBodyToFile, fetchOrThrow } = require('./http-download');
|
||||
|
||||
function encodeRepoPath(repoPath) {
|
||||
@@ -65,10 +65,7 @@ async function downloadGitHubRepoFile(cfg, repoPath, destPath) {
|
||||
throw new Error(`unexpected GitHub response for ${repoPath}`);
|
||||
}
|
||||
|
||||
async function downloadReleaseAsset(cfg, assetName, destPath) {
|
||||
if (useGiteaReleases(cfg)) {
|
||||
return downloadGiteaReleaseAsset(cfg, assetName, destPath);
|
||||
}
|
||||
async function fetchGitHubReleaseJson(cfg) {
|
||||
const token = githubToken(cfg);
|
||||
const tag = (cfg.github.release_tag || 'latest').trim() || 'latest';
|
||||
const { owner, repo } = cfg.github;
|
||||
@@ -89,7 +86,24 @@ async function downloadReleaseAsset(cfg, assetName, destPath) {
|
||||
if (res.status === 401 || res.status === 403) hint = ' (set GITHUB_TOKEN or token_env PAT)';
|
||||
throw new Error(`releases list ${res.status}${hint}: ${text.slice(0, 600)}`);
|
||||
}
|
||||
const rel = JSON.parse(text);
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
async function listReleaseAttachmentNames(cfg) {
|
||||
if (useGiteaReleases(cfg)) {
|
||||
return listGiteaReleaseAttachmentNames(cfg);
|
||||
}
|
||||
const rel = await fetchGitHubReleaseJson(cfg);
|
||||
const assets = rel.assets || [];
|
||||
return assets.map((a) => a.name).filter(Boolean);
|
||||
}
|
||||
|
||||
async function downloadReleaseAsset(cfg, assetName, destPath) {
|
||||
if (useGiteaReleases(cfg)) {
|
||||
return downloadGiteaReleaseAsset(cfg, assetName, destPath);
|
||||
}
|
||||
const token = githubToken(cfg);
|
||||
const rel = await fetchGitHubReleaseJson(cfg);
|
||||
const assets = rel.assets || [];
|
||||
let assetURL = '';
|
||||
for (const a of assets) {
|
||||
@@ -118,4 +132,10 @@ async function downloadReleaseAsset(cfg, assetName, destPath) {
|
||||
await downloadBodyToFile(dl, destPath);
|
||||
}
|
||||
|
||||
module.exports = { downloadGitHubRepoFile, downloadReleaseAsset, encodeRepoPath };
|
||||
module.exports = {
|
||||
downloadGitHubRepoFile,
|
||||
downloadReleaseAsset,
|
||||
encodeRepoPath,
|
||||
fetchGitHubReleaseJson,
|
||||
listReleaseAttachmentNames,
|
||||
};
|
||||
|
||||
@@ -75,8 +75,10 @@ async function loadManifest(cfg) {
|
||||
*/
|
||||
async function patchesMatchManifest(cfg, manifest, onStatus) {
|
||||
if (!validateManifest(manifest)) return false;
|
||||
const { buildResolvedReleaseFiles } = require('./release-sync');
|
||||
const entries = await buildResolvedReleaseFiles(cfg, manifest);
|
||||
const gameDir = cfg.game_dir;
|
||||
for (const entry of cfg.files || []) {
|
||||
for (const entry of entries) {
|
||||
if (!entry.from_release) continue;
|
||||
const spec = manifest.files[entry.source];
|
||||
if (!spec || !spec.sha256) return false;
|
||||
@@ -98,7 +100,9 @@ async function patchesMatchManifest(cfg, manifest, onStatus) {
|
||||
|
||||
async function verifyInstalledAgainstManifest(cfg, manifest) {
|
||||
if (!validateManifest(manifest)) return;
|
||||
for (const entry of cfg.files || []) {
|
||||
const { buildResolvedReleaseFiles } = require('./release-sync');
|
||||
const entries = await buildResolvedReleaseFiles(cfg, manifest);
|
||||
for (const entry of entries) {
|
||||
if (!entry.from_release) continue;
|
||||
const spec = manifest.files[entry.source];
|
||||
if (!spec || !spec.sha256) {
|
||||
@@ -119,8 +123,10 @@ async function verifyInstalledAgainstManifest(cfg, manifest) {
|
||||
|
||||
async function recordPatchState(cfg, manifest) {
|
||||
if (!validateManifest(manifest)) return;
|
||||
const { buildResolvedReleaseFiles } = require('./release-sync');
|
||||
const entries = await buildResolvedReleaseFiles(cfg, manifest);
|
||||
const shas = {};
|
||||
for (const entry of cfg.files || []) {
|
||||
for (const entry of entries) {
|
||||
if (!entry.from_release) continue;
|
||||
const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean);
|
||||
const destAbs = path.join(cfg.game_dir, ...parts);
|
||||
|
||||
@@ -5,6 +5,8 @@ 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');
|
||||
@@ -94,7 +96,12 @@ async function applyRealmlist(cfg) {
|
||||
}
|
||||
|
||||
async function applyPatches(cfg, onStatus) {
|
||||
for (const f of cfg.files || []) {
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const { listReleaseAttachmentNames } = require('./github');
|
||||
|
||||
/** Legacy launcher.json rows — ignored when merging explicit files. */
|
||||
const DEPRECATED_SOURCES = new Set(['patch-Z.MPQ', 'Wow-patched.exe']);
|
||||
|
||||
function filterExplicitFiles(files) {
|
||||
if (!Array.isArray(files)) return [];
|
||||
return files
|
||||
.filter((e) => e && String(e.source || '').trim())
|
||||
.filter((e) => !DEPRECATED_SOURCES.has(String(e.source).trim()))
|
||||
.map((e) => ({
|
||||
source: String(e.source).trim(),
|
||||
dest: String(e.dest || '').trim(),
|
||||
backup: e.backup !== false,
|
||||
from_release: e.from_release !== false,
|
||||
}))
|
||||
.filter((e) => e.dest);
|
||||
}
|
||||
|
||||
function manifestLooksUsable(m) {
|
||||
return !!(m && m.files && typeof m.files === 'object' && Object.keys(m.files).length > 0);
|
||||
}
|
||||
|
||||
/** Launcher / updater attachments — never copied into the WoW folder. */
|
||||
function isExcludedFromGameSync(fileName) {
|
||||
const n = String(fileName || '');
|
||||
const lower = n.toLowerCase();
|
||||
if (lower === 'patch-manifest.json') return true;
|
||||
if (/^fractured-launcher/i.test(n)) return true;
|
||||
if (/\.blockmap$/i.test(n)) return true;
|
||||
if (/^latest.*\.ya?ml$/i.test(n) || lower === 'latest.yml') return true;
|
||||
if (lower.includes('builder-debug')) return true;
|
||||
if (/\.appimage$/i.test(n)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function destForReleaseSource(source, cfg) {
|
||||
const base = path.basename(String(source || ''));
|
||||
if (/\.mpq$/i.test(base)) return `Data/${base}`;
|
||||
if (/\.exe$/i.test(base)) return (cfg.launch && cfg.launch.exe) || 'Wow.exe';
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicit `files` in config wins. Otherwise use patch-manifest keys if present,
|
||||
* else discover attachments on the release (excluding launcher artifacts).
|
||||
*/
|
||||
async function buildResolvedReleaseFiles(cfg, manifestMaybeNull) {
|
||||
const explicit = filterExplicitFiles(cfg.files);
|
||||
if (explicit.length) return explicit;
|
||||
|
||||
const manifest = manifestMaybeNull;
|
||||
if (manifestLooksUsable(manifest)) {
|
||||
const keys = Object.keys(manifest.files).filter((k) => k && !isExcludedFromGameSync(k));
|
||||
if (!keys.length) {
|
||||
throw new Error('patch-manifest.json has no file entries — add files or attach assets to the release.');
|
||||
}
|
||||
return keys.map((source) => ({
|
||||
source,
|
||||
dest: destForReleaseSource(source, cfg),
|
||||
backup: true,
|
||||
from_release: true,
|
||||
}));
|
||||
}
|
||||
|
||||
const names = await listReleaseAttachmentNames(cfg);
|
||||
const game = names.filter((n) => n && !isExcludedFromGameSync(n));
|
||||
if (!game.length) {
|
||||
throw new Error(
|
||||
'No patch files on this release (after excluding launcher installers). ' +
|
||||
'Attach MPQ/exe assets or ship patch-manifest.json listing filenames.'
|
||||
);
|
||||
}
|
||||
|
||||
const exes = game.filter((n) => /\.exe$/i.test(n));
|
||||
const mpqs = game.filter((n) => /\.mpq$/i.test(n));
|
||||
const rest = game.filter((n) => !/\.(exe|mpq)$/i.test(n));
|
||||
|
||||
if (exes.length > 1) {
|
||||
throw new Error(
|
||||
`Release has multiple .exe files (${exes.join(', ')}). ` +
|
||||
'Remove extras or publish patch-manifest.json with the exact filenames to install.'
|
||||
);
|
||||
}
|
||||
|
||||
const out = [];
|
||||
for (const n of mpqs) {
|
||||
out.push({
|
||||
source: n,
|
||||
dest: path.posix.join('Data', path.basename(n)),
|
||||
backup: true,
|
||||
from_release: true,
|
||||
});
|
||||
}
|
||||
if (exes.length === 1) {
|
||||
out.push({
|
||||
source: exes[0],
|
||||
dest: (cfg.launch && cfg.launch.exe) || 'Wow.exe',
|
||||
backup: true,
|
||||
from_release: true,
|
||||
});
|
||||
}
|
||||
for (const n of rest) {
|
||||
out.push({
|
||||
source: n,
|
||||
dest: path.basename(n),
|
||||
backup: true,
|
||||
from_release: true,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildResolvedReleaseFiles,
|
||||
filterExplicitFiles,
|
||||
isExcludedFromGameSync,
|
||||
DEPRECATED_SOURCES,
|
||||
};
|
||||
Reference in New Issue
Block a user