feedback5 WU: module type icons get one lower-case file per type (dec-X and dec-x were two names for one file)

The official data spells one DEC X module 'dec-X' (uniequip_003_aglina) and the others 'dec-x' (WAH-Y is the other
mixed-case type id), and the asset plan stored each under its own case: module/dec-X.png and module/dec-x.png. On
Windows / macOS those are one file, so a release zip built there could hold only one of them and a case-sensitive
server (a Linux VPS) would miss the other — the release packager found it ("planned but not staged"). The local file
is now module/{typeIcon in lower case}.png; the upstream names keep their order (the id as spelled, then lower case).

data/assets.json regenerated offline (node tools/fetch-assets.mjs --offline --add-only). JSON diff: modules.dec-X
/assets/module/dec-X.png -> /assets/module/dec-x.png, modules.WAH-Y /assets/module/WAH-Y.png -> /assets/module/wah-y.png,
hash, stats.files 6979 -> 6978, stats.bytes 458334505 -> 458333444; nothing else.

fetch-assets compares its orphans (what --prune deletes) without case: on Windows / macOS module/WAH-Y.png on disk is
the listed module/wah-y.png and must not be deleted (orphanFiles, exported).

Tests: test/assets-diy.test.js (one lower-case file per type, no two manifest URLs differing only in case, orphans).
docs/ASSETS.md: the module icon file name.
This commit is contained in:
sganggs committed 2026-10-06 17:59:13 +08:00
1 parent 9bfac54c8f
commit b8a86b88f1
5 files changed
+42 -8

No files matched your search

+20 -1
View File
@@ -10,7 +10,7 @@ import assert from 'node:assert/strict';
import { existsSync, readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { dataExtras, parseArgs } from '../tools/fetch-assets.mjs';
import { dataExtras, orphanFiles, parseArgs } from '../tools/fetch-assets.mjs';
import { patternOperator, buildPlan } from '../tools/assets/plan.mjs';
import { indexAudio } from '../tools/assets/audio.mjs';
import { Downloader } from '../tools/assets/downloader.mjs';
@@ -65,6 +65,25 @@ test('patternOperator / buildPlan: an operator research 07 lacks gets the 07 URL
assert.equal(t.template.prof.sub.pioneer.alts[0].rel, 'prof/sub/pioneer.png');
});
test('module type icons: one lower-case file per type, so the official \'dec-X\' (uniequip_003_aglina) and \'dec-x\' share one; --prune keeps a file whose name differs only in case', () => {
const t = buildPlan({ assets07: {}, ops03: {}, enemies05: {}, maps05: {}, audio: indexAudio({}), modelsData: {}, moduleTypes: ['dec-X', 'dec-x', 'WAH-Y'] });
const rel = (k) => t.template.modules[k].alts[0].rel;
assert.deepEqual([rel('dec-X'), rel('dec-x'), rel('WAH-Y')], ['module/dec-x.png', 'module/dec-x.png', 'module/wah-y.png']);
assert.match(t.template.modules['dec-X'].alts[0].urls[0], /uniequiptype\/dec-X\.png$/, 'the upstream name keeps its case first');
const m = load('assets');
assert.equal(m.modules['dec-X'], m.modules['dec-x']);
const byCase = new Map();
const walk = (v) => {
if (typeof v === 'string' && v.startsWith('/assets/')) {
const k = v.toLowerCase();
assert.ok(!byCase.has(k) || byCase.get(k) === v, `${v} and ${byCase.get(k)} differ only in case: one file on Windows / macOS`);
byCase.set(k, v);
} else if (v && typeof v === 'object') for (const x of Object.values(v)) walk(x);
};
walk(m);
assert.deepEqual(orphanFiles(['module/WAH-Y.png', 'module/old.png', 'local/x.png', 'char/a.png'], new Set(['module/wah-y.png', 'char/a.png'])), ['module/old.png']);
});
test('--add-only: parsed, refused with --prune / --force; its downloader keeps every existing file and records what it wrote', async () => {
assert.equal(parseArgs(['--add-only']).addOnly, true);
assert.equal(parseArgs([]).addOnly, false);