feedback5 WH: 自选 art — fetch-assets plans every data/backups.json unit research 07 lacks (the 72 owned 6★: avatar / portrait with E2, battle Spine Front / Back, every skill icon and skill sound, sub-profession icon, from 07's URL patterns), the 自选 summons as tokens, and a type icon per module of chess.json / backups.json (manifest modules, the loadout tiles' fallback); --add-only downloads only missing files and never rewrites, re-downloads or deletes an existing one (worktrees sharing public/assets)
Downloaded 1,295 files, 127.2 MB (133,426,089 bytes) into the shared public/assets; no existing file changed or removed. data/assets.json only gains entries: 72 chars (all with avatar, portrait, Front and Back Spine), 38 tokens (avatars; no Spine — upstream has skin variants only), 208 skill icons, 10 sub-profession icons, 102 SFX units, 131 module type icons.
This commit is contained in:
1 parent
42920c62c5
commit
9db99aad00
7 files changed
+231
-32
No files matched your search
+61
-15
@@ -24,9 +24,21 @@
|
||||
// the entries it would drop and exits 1; --allow-shrink (or --prune) writes the
|
||||
// smaller manifest.
|
||||
//
|
||||
// Beyond research 07's ids the plan covers what data/*.json adds: spawnable
|
||||
// enemies / tokens (data/enemies.json, data/tokens.json), the 自选 owned-6★ picks
|
||||
// of data/backups.json `units` (art from 07's URL patterns) and their summons
|
||||
// (`tokens`), and the module type icons of every module in data/chess.json /
|
||||
// data/backups.json (manifest `modules`).
|
||||
//
|
||||
// --add-only: for a checkout whose public/assets / public/fonts are shared with
|
||||
// another one (a git worktree): download only the files missing on disk, never
|
||||
// re-download, rewrite or delete an existing file (atlases already on disk are
|
||||
// left as they are; fonts are not rebuilt — the manifest keeps its current
|
||||
// `fonts`).
|
||||
//
|
||||
// Usage: node tools/fetch-assets.mjs [--concurrency=16] [--force] [--offline]
|
||||
// [--dry-run] [--refresh-index] [--prune]
|
||||
// [--allow-shrink] [--local-spines] [--help]
|
||||
// [--allow-shrink] [--add-only] [--local-spines] [--help]
|
||||
|
||||
import { readFile, writeFile, mkdir, rename, readdir, unlink } from 'node:fs/promises';
|
||||
import { existsSync, realpathSync } from 'node:fs';
|
||||
@@ -59,6 +71,8 @@ const HELP = `Usage: node tools/fetch-assets.mjs [options]
|
||||
(public/assets/local/** of tools/local-extract is never deleted); implies --allow-shrink
|
||||
--allow-shrink write data/assets.json even when it loses entries the current one has
|
||||
(without it such a run keeps the current manifest, lists the entries and exits 1)
|
||||
--add-only download only files missing on disk; never re-download, rewrite or delete an existing
|
||||
file, no font rebuild (a worktree sharing public/assets and public/fonts)
|
||||
--local-spines rewrite ${LOCAL_ENEMY_SPINES_FILE} from the enemy models extracted
|
||||
by tools/local-extract/extract.py (public/assets/local/spine/enemy/)
|
||||
--help this text`;
|
||||
@@ -66,10 +80,10 @@ const HELP = `Usage: node tools/fetch-assets.mjs [options]
|
||||
/**
|
||||
* Parse CLI flags.
|
||||
* @param {string[]} argv
|
||||
* @returns {{concurrency:number, force:boolean, offline:boolean, dryRun:boolean, refreshIndex:boolean, prune:boolean, allowShrink:boolean, localSpines:boolean, help:boolean}}
|
||||
* @returns {{concurrency:number, force:boolean, offline:boolean, dryRun:boolean, refreshIndex:boolean, prune:boolean, allowShrink:boolean, addOnly:boolean, localSpines:boolean, help:boolean}}
|
||||
*/
|
||||
export function parseArgs(argv) {
|
||||
const o = { concurrency: 16, force: false, offline: false, dryRun: false, refreshIndex: false, prune: false, allowShrink: false, localSpines: false, help: false };
|
||||
const o = { concurrency: 16, force: false, offline: false, dryRun: false, refreshIndex: false, prune: false, allowShrink: false, addOnly: false, localSpines: false, help: false };
|
||||
for (const a of argv) {
|
||||
const [k, v] = a.split('=');
|
||||
if (k === '--concurrency') o.concurrency = Math.max(1, Math.min(64, parseInt(v, 10) || 16));
|
||||
@@ -79,13 +93,38 @@ export function parseArgs(argv) {
|
||||
else if (k === '--refresh-index') o.refreshIndex = true;
|
||||
else if (k === '--prune') o.prune = true;
|
||||
else if (k === '--allow-shrink') o.allowShrink = true;
|
||||
else if (k === '--add-only') o.addOnly = true;
|
||||
else if (k === '--local-spines') o.localSpines = true;
|
||||
else if (k === '--help' || k === '-h') o.help = true;
|
||||
else throw new Error(`unknown option ${a}\n${HELP}`);
|
||||
}
|
||||
if (o.addOnly && (o.prune || o.force)) throw new Error(`--add-only never deletes or rewrites files: not with --prune / --force\n${HELP}`);
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* What data/backups.json and data/chess.json add to the asset plan: `extraOperators` — every unit of backups.json
|
||||
* (`{ name, subProfessionId, nationId, skills: [{ index, skillId, iconId }] }`; buildPlan plans those research 07 lacks,
|
||||
* the 自选 owned-6★ picks), `tokenIds` — the summons of the 自选 picks (backups.json `tokens`), `moduleTypes` — the type
|
||||
* icon of every module a chess or a unit form offers.
|
||||
* @param {any} backups data/backups.json (or null)
|
||||
* @param {any} chess data/chess.json (or null)
|
||||
*/
|
||||
export function dataExtras(backups, chess) {
|
||||
const extraOperators = {};
|
||||
const moduleTypes = new Set();
|
||||
for (const [id, u] of Object.entries(backups?.units || {})) {
|
||||
const skills = new Map();
|
||||
for (const f of Object.values(u.forms || {})) {
|
||||
for (const sk of f.skills || []) if (!skills.has(sk.index)) skills.set(sk.index, { index: sk.index, skillId: sk.skillId, iconId: sk.iconId || sk.skillId });
|
||||
for (const m of f.modules || []) if (m.typeIcon) moduleTypes.add(m.typeIcon);
|
||||
}
|
||||
extraOperators[id] = { name: u.name, subProfessionId: u.subProfessionId, nationId: u.nationId, skills: [...skills.values()].sort((a, b) => a.index - b.index) };
|
||||
}
|
||||
for (const c of Object.values(chess || {})) for (const m of c?.modules || []) if (m.typeIcon) moduleTypes.add(m.typeIcon);
|
||||
return { extraOperators, tokenIds: Object.keys(backups?.tokens || {}).sort(), moduleTypes: [...moduleTypes].sort() };
|
||||
}
|
||||
|
||||
/**
|
||||
* The shrink guard of data/assets.json (header): the entries of the current manifest `prev` that `next` would drop,
|
||||
* and whether `next` may be written — always when nothing is dropped (or there is no current manifest), otherwise
|
||||
@@ -156,6 +195,7 @@ function countStats(m, bytes, files) {
|
||||
items: Object.keys(m.items || {}).length,
|
||||
bands: Object.keys(m.bands || {}).length,
|
||||
skills: Object.keys(m.skills || {}).length,
|
||||
modules: Object.keys(m.modules || {}).length,
|
||||
ui: Object.keys(m.ui || {}).length,
|
||||
sfxUnits: Object.keys(m.audio?.sfx?.units || {}).length,
|
||||
};
|
||||
@@ -222,17 +262,20 @@ async function main() {
|
||||
const audio = indexAudio(audioData);
|
||||
// The game data built by tools/build-data.mjs (when present) may reference more
|
||||
// spawnable enemies/tokens than research lists (e.g. 机变 enemy swaps): cover them too.
|
||||
const [dataEnemies, dataTokens, dataBosses] = await Promise.all(
|
||||
['data/enemies.json', 'data/tokens.json', 'data/bosses.json'].map((f) => readJson(f).catch(() => null)));
|
||||
const [dataEnemies, dataTokens, dataBosses, dataBackups, dataChess] = await Promise.all(
|
||||
['data/enemies.json', 'data/tokens.json', 'data/bosses.json', 'data/backups.json', 'data/chess.json'].map((f) => readJson(f).catch(() => null)));
|
||||
const extras = dataExtras(dataBackups, dataChess);
|
||||
const extraHandbook = {};
|
||||
for (const b of Object.values(dataBosses || {})) if (b?.enemyKey && typeof b.handbookId === 'string') extraHandbook[b.enemyKey] = b.handbookId;
|
||||
const localEnemySpines = await syncLocalEnemySpines(opts);
|
||||
const plan = buildPlan({
|
||||
assets07, ops03, enemies05, maps05, audio, modelsData,
|
||||
extraEnemyIds: Object.keys(dataEnemies || {}),
|
||||
extraTokenIds: Object.keys(dataTokens || {}),
|
||||
extraTokenIds: [...Object.keys(dataTokens || {}), ...extras.tokenIds],
|
||||
extraHandbook,
|
||||
localEnemySpines,
|
||||
extraOperators: extras.extraOperators,
|
||||
moduleTypes: extras.moduleTypes,
|
||||
});
|
||||
const leaves = collectLeaves(plan.template);
|
||||
log(`[plan] ${leaves.length} files + ${plan.models.size} Spine models ` +
|
||||
@@ -246,26 +289,32 @@ async function main() {
|
||||
|
||||
const dl = new Downloader({
|
||||
root: ASSETS, ledgerPath: join(CACHE, 'assets-ledger.json'),
|
||||
concurrency: opts.concurrency, force: opts.force, log,
|
||||
concurrency: opts.concurrency, force: opts.force, keepExisting: opts.addOnly, log,
|
||||
});
|
||||
await dl.loadLedger();
|
||||
const downloadErrors = opts.offline ? [] : await downloadLeaves(leaves, dl, ASSETS, 'files');
|
||||
|
||||
// Fonts
|
||||
let current = null;
|
||||
if (existsSync(MANIFEST)) {
|
||||
try { current = JSON.parse(await readFile(MANIFEST, 'utf8')); } catch (e) { log(`[manifest] the current ${relative(ROOT, MANIFEST)} is unreadable (${e.message}): replaced`); }
|
||||
}
|
||||
|
||||
// Fonts (--add-only: not rebuilt — public/fonts may be another checkout's; the manifest keeps its current entry)
|
||||
let fontErrors = [];
|
||||
if (!opts.offline) {
|
||||
if (!opts.offline && !opts.addOnly) {
|
||||
const fdl = new Downloader({ root: FONTS, ledgerPath: join(CACHE, 'fonts-ledger.json'), concurrency: 4, force: opts.force, log });
|
||||
await fdl.loadLedger();
|
||||
await fdl.run(fontJobs(), 'fonts');
|
||||
dl.totals.bytesDownloaded += fdl.totals.bytesDownloaded;
|
||||
for (const k of ['ok', 'skip', 'miss', 'error']) dl.totals[k] += fdl.totals[k];
|
||||
}
|
||||
const fonts = await buildFonts(FONTS, log);
|
||||
const fonts = opts.addOnly ? { files: {}, errors: [] } : await buildFonts(FONTS, log);
|
||||
fontErrors = fonts.errors;
|
||||
|
||||
// Spine
|
||||
const spine = await processModels(plan.models, {
|
||||
root: ASSETS, dl, cachePath: join(CACHE, 'spine-info.json'), download: !opts.offline, log,
|
||||
...(opts.addOnly ? { writable: (rel) => dl.written.has(rel) } : null),
|
||||
});
|
||||
|
||||
// Manifest
|
||||
@@ -274,7 +323,8 @@ async function main() {
|
||||
tidyManifest(body);
|
||||
const fontFaces = {};
|
||||
for (const [name, f] of Object.entries(fonts.files)) fontFaces[name] = f;
|
||||
body.fonts = existsSync(join(FONTS, 'fonts.css')) ? { css: '/fonts/fonts.css', faces: fontFaces } : { faces: fontFaces };
|
||||
body.fonts = opts.addOnly && current?.fonts ? current.fonts
|
||||
: existsSync(join(FONTS, 'fonts.css')) ? { css: '/fonts/fonts.css', faces: fontFaces } : { faces: fontFaces };
|
||||
const bytes = totalBytes(ASSETS, resolved.files);
|
||||
const manifest = {
|
||||
version: MANIFEST_VERSION,
|
||||
@@ -283,10 +333,6 @@ async function main() {
|
||||
stats: countStats(body, bytes, resolved.files.size),
|
||||
...body,
|
||||
};
|
||||
let current = null;
|
||||
if (existsSync(MANIFEST)) {
|
||||
try { current = JSON.parse(await readFile(MANIFEST, 'utf8')); } catch (e) { log(`[manifest] the current ${relative(ROOT, MANIFEST)} is unreadable (${e.message}): replaced`); }
|
||||
}
|
||||
const guard = shrinkGuard(current, manifest, opts);
|
||||
if (guard.write) await writeJsonAtomic(MANIFEST, manifest);
|
||||
|
||||
|
||||
Reference in new issue
Block a user