feedback2 WE: setup downloads the 36 battle emotes and the 19 玩法说明 pages from the public mirror (ArknightsAssets2 cn); the client shows the local-client art first, then the mirror copy, then its fallback (#42)

This commit is contained in:
sganggs committed 2026-10-03 23:05:42 +08:00
1 parent 380af1ba80
commit db7b2b1404
11 files changed
+280 -44

No files matched your search

+62 -1
View File
@@ -18,9 +18,10 @@ import { pngSize, isCompletePng, isMp3, validate } from '../tools/assets/formats
import { encodeWoff2, decodeWoff2Tables, readSfnt, uintBase128 } from '../tools/assets/woff2.mjs';
import { assetToPath, pickUnitSfx, indexAudio } from '../tools/assets/audio.mjs';
import { mirrorUrl, safeName, encodePath } from '../tools/assets/sources.mjs';
import { collectEnemyIds, skillIndicesByChar } from '../tools/assets/plan.mjs';
import { collectEnemyIds, skillIndicesByChar, buildPlan, GUIDE_PAGES, UI_EXTRAS } from '../tools/assets/plan.mjs';
import { resolveTemplate, collectLeaves } from '../tools/assets/manifest.mjs';
import { spineEntry } from '../public/js/assets.js';
import { EMOTE_CATALOG, emoteArtGroup } from '../shared/constants.js';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const PUBLIC = join(ROOT, 'public');
@@ -468,6 +469,66 @@ describe('downloader (fake network)', () => {
});
});
// ---------------------------------------------------------------------------
// GitHub issue #42: a source deploy on a server without the Arknights client had no public/assets/local, so the battle
// emotes showed default icons and 玩法说明 showed text tips. The public mirror has both (checked 2026-10-03: all 55
// URLs answer 200 on raw.githubusercontent.com and on jsDelivr, same pictures and sizes as the local extraction), so the
// plan downloads them like any other UI sprite, keyed by the data/local-assets.json group and name.
describe('emotes and 玩法说明 pages from the public mirror (GitHub issue #42)', () => {
const AA2 = 'https://raw.githubusercontent.com/ArknightsAssets/ArknightsAssets2/cn/assets/dyn/';
const plan = () => buildPlan({ assets07: {}, ops03: {}, enemies05: {}, maps05: {}, audio: indexAudio({}), modelsData: {} }).template;
test('the plan fetches the 36 battle emotes and the 19 guide pages from ArknightsAssets2 cn, keyed like the local art', () => {
const ui = plan().ui;
assert.equal(EMOTE_CATALOG.length, 36);
for (const e of EMOTE_CATALOG) {
const key = `${emoteArtGroup(e.id)}/${e.picId}`; // the local manifest's group + name (emoticon/<dir>/<picId>)
assert.deepEqual(ui[key]?.alts, [{ rel: `ui/${key}.png`, urls: [`${AA2}ui/emoticon/theme/%5Buc%5D${e.themeId}/icon/${e.picId}.png`], kind: 'png' }], key);
}
assert.ok(ui['emoticon/fooldoctor/pic_fooldoctor_08_battle'], 'keyed by picId, not by the emote id (fooldoctor_06 → pic 08)');
assert.notEqual(ui['emoticon/slug/pic_thanks_battle'].alts[0].rel, ui['emoticon/basic/pic_thanks_battle'].alts[0].rel, 'same picId, two themes');
assert.deepEqual(GUIDE_PAGES.slice(0, 2), ['autochess_home_1', 'autochess_home_2']);
assert.equal(GUIDE_PAGES.length, 19);
for (const k of GUIDE_PAGES) {
assert.deepEqual(ui[`guide/${k}`]?.alts, [{ rel: `ui/guide/${k}.png`, urls: [`${AA2}arts/guidebookpages/%5Bpack%5Dautochess/${k}.png`], kind: 'png' }], k);
}
const rels = Object.values(ui).map((l) => l.alts[0].rel);
assert.equal(new Set(rels).size, rels.length, 'no two UI entries share a file');
assert.ok(rels.every((r) => !r.startsWith('local/')), 'public/assets/local belongs to tools/local-extract');
assert.equal(mirrorUrl(ui['emoticon/fooldoctor/pic_fooldoctor_08_battle'].alts[0].urls[0]),
'https://cdn.jsdelivr.net/gh/ArknightsAssets/ArknightsAssets2@cn/assets/dyn/ui/emoticon/theme/%5Buc%5Demoticon_foolsday_doctor/icon/pic_fooldoctor_08_battle.png',
'the jsDelivr fallback (downloader) serves the same path');
assert.ok(Object.isFrozen(UI_EXTRAS) && UI_EXTRAS.every(Object.isFrozen));
});
test('a rebuilt manifest lists the copies that are on disk under /assets/ui/…; a missing one is left out', async () => {
const { mkdtemp, mkdir, writeFile } = await import('node:fs/promises');
const { tmpdir } = await import('node:os');
const root = await mkdtemp(join(tmpdir(), 'sp-mirror-art-'));
const ui = plan().ui;
const keys = ['emoticon/basic/pic_happy_battle', 'guide/autochess_home_1', 'emoticon/slug/pic_bye_battle'];
for (const k of keys.slice(0, 2)) {
await mkdir(join(root, dirname(ui[k].alts[0].rel)), { recursive: true });
await writeFile(join(root, ui[k].alts[0].rel), 'x');
}
const r = resolveTemplate({ ui: Object.fromEntries(keys.map((k) => [k, ui[k]])) }, { root, spine: new Map() });
assert.deepEqual(r.value.ui, {
'emoticon/basic/pic_happy_battle': '/assets/ui/emoticon/basic/pic_happy_battle.png',
'guide/autochess_home_1': '/assets/ui/guide/autochess_home_1.png',
});
assert.deepEqual(r.misses, ['ui.emoticon/slug/pic_bye_battle']);
});
test('the committed data/assets.json lists all 55, so setup on an install made before them downloads them', () => {
// tools/setup.mjs runs fetch-assets when data/assets.json lists a file that is not on disk (checkAssets)
const m = readJson('data/assets.json');
for (const e of EMOTE_CATALOG) assert.equal(m.ui[`emoticon/${e.dir}/${e.picId}`], `/assets/ui/emoticon/${e.dir}/${e.picId}.png`, e.id);
for (const k of GUIDE_PAGES) assert.equal(m.ui[`guide/${k}`], `/assets/ui/guide/${k}.png`, k);
assert.equal(m.stats.ui, Object.keys(m.ui).length);
assert.ok(!JSON.stringify(m).includes('/assets/local/'), 'never a local-client path');
});
});
// ---------------------------------------------------------------------------
describe('generated manifest data/assets.json', () => {
const haveManifest = existsSync(MANIFEST);