feedback5 WX: the 39 summon models no web dump carries come from the local client (extract.py TOKEN_SPINES) and are drawn instead of the avatar diamond when data/local-assets.json lists them
- tools/local-extract/extract.py TOKEN_SPINES (`--only spine/token`): each token's battle prefab in the battle token packs (pkgrps/btl_pfb_tokens_*.ab; the Windows build has all 39, the iOS build lacks btl_pfb_tokens_0), the skeleton of its Front (or only) Spine renderer, written to public/assets/local/spine/token/<id>/ like the enemy models (sized pma atlas, premultiplied pages; the newer RGBA pages keep their own alpha). The enemy writer is shared (write_spine; the two enemy models re-extract byte for byte). The skinpack job of 乌尔比安's block is gone: it extracted nothing, and that token draws no model in the game (nor do 圣聆初雪's, 酒神's, 贝洛内's and 予愿安洁莉娜's hidden tokens: no Spine renderer, no avatar in either install). - tools/assets/local-token-spines.json (committed, `fetch-assets --local-spines`) → data/assets.json tokens[id].spineLocal for 35 自选 summons, 凯瑟琳's 爬行号·防护单元 and 凛御银灰's three 风雪之眼; the manifest is the same with or without the extraction (JSON diff: only the 39 spineLocal entries and the hash). spine.mjs LOCAL_SPINE_ROLES maps clips the resolver cannot read (电弧's 戴乌 C_Skill1_*, 酒神's 本能的召唤 Loop / End, 白铁's platforms End, 战术锚点 on its Default pose) [ASSUMED where named]. - assets.js spineEntry: a token resolves its spineLocal like an enemy (no web model behind it: the diamond stays when it is not listed or fails to load). Scale as the web tokens (UNIT.modelScale). - setup reports an extraction without the summon models; docs: ASSETS.md "Token models from the local client", DEPLOY.md §6, DESIGN §13 (design/client.md), README. - Tests: test/local-token-models.test.js (new), test/local-extract.test.js (job table, prefab walk, alpha merge), test/assets.test.js (the local token models load like the client does), test/package.test.js (the full zip ships them), test/static-local-art.test.js. Golden unchanged.
This commit is contained in:
1 parent
83d76e507d
commit
575333137c
17 files changed
+2738
-111
No files matched your search
+49
-24
@@ -9,8 +9,11 @@
|
||||
// aliases"); the official ones the local client has (tools/local-extract/
|
||||
// extract.py ENEMY_SPINES, optional) are added as `spineLocal` from the
|
||||
// committed tools/assets/local-enemy-spines.json — never from the disk, so the
|
||||
// manifest is the same with or without the extraction. --local-spines rewrites
|
||||
// that file from the extracted models (after a game update).
|
||||
// manifest is the same with or without the extraction. The token (summon)
|
||||
// models no dump carries (extract.py TOKEN_SPINES) likewise, from
|
||||
// tools/assets/local-token-spines.json (ASSETS.md "Token models from the local
|
||||
// client"). --local-spines rewrites both files from the extracted models (after
|
||||
// a game update).
|
||||
//
|
||||
// Idempotent: existing files with the right size are skipped, so re-running is
|
||||
// cheap. Downloads use ~16 parallel connections, 3 retries per direct source,
|
||||
@@ -51,7 +54,10 @@ import { normalizeProxyPrefix } from './assets/sources.mjs';
|
||||
import { loadIndexes } from './assets/cache.mjs';
|
||||
import { indexAudio, VOICE_DIRS } from './assets/audio.mjs';
|
||||
import { buildPlan } from './assets/plan.mjs';
|
||||
import { processModels, findLocalEnemyModels, localEnemySpineMeta, loadLocalEnemySpines, LOCAL_ENEMY_SPINES_FILE } from './assets/spine.mjs';
|
||||
import {
|
||||
processModels, findLocalEnemyModels, findLocalTokenModels, localSpineMeta, loadLocalSpines, LOCAL_ENEMY_SPINES_FILE,
|
||||
LOCAL_TOKEN_SPINES_FILE, LOCAL_ENEMY_SPINE_DIR, LOCAL_TOKEN_SPINE_DIR,
|
||||
} from './assets/spine.mjs';
|
||||
import { collectLeaves, downloadLeaves, resolveTemplate, totalBytes, contentHash, droppedEntries, MANIFEST_VERSION } from './assets/manifest.mjs';
|
||||
import { fontJobs, buildFonts } from './assets/fonts.mjs';
|
||||
import { skelParserAvailable } from './assets/skel.mjs';
|
||||
@@ -62,7 +68,22 @@ const FONTS = join(ROOT, 'public', 'fonts');
|
||||
const CACHE = join(ROOT, '.cache');
|
||||
const MANIFEST = join(ROOT, 'data', 'assets.json');
|
||||
const REPORT = join(CACHE, 'assets-report.json');
|
||||
const LOCAL_SPINES = join(ROOT, LOCAL_ENEMY_SPINES_FILE);
|
||||
/**
|
||||
* The local-client model overlays (spineLocal): the committed metadata file, where extract.py writes the models (under
|
||||
* public/assets), how to find them, and the file's `about` line.
|
||||
*/
|
||||
const LOCAL_SPINE_KINDS = {
|
||||
enemy: {
|
||||
file: LOCAL_ENEMY_SPINES_FILE, dir: LOCAL_ENEMY_SPINE_DIR, find: findLocalEnemyModels, what: 'enemy',
|
||||
about: 'Spine metadata of the enemy models only the local client has (tools/local-extract/extract.py ENEMY_SPINES); '
|
||||
+ 'data/assets.json enemies[id].spineLocal. Written by node tools/fetch-assets.mjs --local-spines (docs/ASSETS.md "Enemy aliases").',
|
||||
},
|
||||
token: {
|
||||
file: LOCAL_TOKEN_SPINES_FILE, dir: LOCAL_TOKEN_SPINE_DIR, find: findLocalTokenModels, what: 'token',
|
||||
about: 'Spine metadata of the token (summon) models only the local client has (tools/local-extract/extract.py TOKEN_SPINES); '
|
||||
+ 'data/assets.json tokens[id].spineLocal. Written by node tools/fetch-assets.mjs --local-spines (docs/ASSETS.md "Token models from the local client").',
|
||||
},
|
||||
};
|
||||
|
||||
const HELP = `Usage: node tools/fetch-assets.mjs [options]
|
||||
--concurrency=N parallel downloads (default 16)
|
||||
@@ -80,8 +101,9 @@ const HELP = `Usage: node tools/fetch-assets.mjs [options]
|
||||
(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/)
|
||||
--local-spines rewrite ${LOCAL_ENEMY_SPINES_FILE} and ${LOCAL_TOKEN_SPINES_FILE} from the
|
||||
enemy and token models extracted by tools/local-extract/extract.py
|
||||
(public/assets/local/spine/enemy/, public/assets/local/spine/token/)
|
||||
--help this text
|
||||
Environment: SP_ASSET_SOURCE sets the default source; SP_GITHUB_PROXY sets the
|
||||
HTTPS mirror prefix (default https://gh-proxy.com/; empty disables the proxy).
|
||||
@@ -247,33 +269,32 @@ function requiredMisses(m, charIds) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata of the local-client enemy models (the committed LOCAL_SPINES). With --local-spines it is rewritten from the
|
||||
* models extracted under public/assets/local/spine/enemy/ (read only); otherwise extracted models whose metadata differs
|
||||
* from the committed one only get a warning — the manifest never depends on what this machine extracted.
|
||||
* Metadata of the local-client models of one kind (LOCAL_SPINE_KINDS: the committed file). With --local-spines it is
|
||||
* rewritten from the models extracted under public/assets/local/spine/<kind>/ (read only); otherwise extracted models
|
||||
* whose metadata differs from the committed one only get a warning — the manifest never depends on what this machine
|
||||
* extracted.
|
||||
* @param {{ localSpines: boolean, dryRun: boolean }} opts
|
||||
* @param {'enemy'|'token'} kind
|
||||
*/
|
||||
async function syncLocalEnemySpines(opts) {
|
||||
const committed = await loadLocalEnemySpines(LOCAL_SPINES);
|
||||
const found = await findLocalEnemyModels(ASSETS);
|
||||
async function syncLocalSpines(opts, kind) {
|
||||
const k = LOCAL_SPINE_KINDS[kind];
|
||||
const committed = await loadLocalSpines(join(ROOT, k.file));
|
||||
const found = await k.find(ASSETS);
|
||||
if (!Object.keys(found).length) {
|
||||
if (opts.localSpines) log(`[local-spines] no extracted enemy model under public/assets/local/spine/enemy/ — ${LOCAL_ENEMY_SPINES_FILE} kept`);
|
||||
if (opts.localSpines) log(`[local-spines] no extracted ${k.what} model under public/assets/${k.dir} — ${k.file} kept`);
|
||||
return committed;
|
||||
}
|
||||
const { meta, problems } = await localEnemySpineMeta(ASSETS, found);
|
||||
const { meta, problems } = await localSpineMeta(ASSETS, found);
|
||||
for (const p of problems) log(`[local-spines] ${p}`);
|
||||
if (opts.localSpines && !opts.dryRun) {
|
||||
const models = { ...committed, ...meta };
|
||||
const sorted = Object.fromEntries(Object.keys(models).sort().map((k) => [k, models[k]]));
|
||||
const doc = {
|
||||
about: 'Spine metadata of the enemy models only the local client has (tools/local-extract/extract.py ENEMY_SPINES); '
|
||||
+ 'data/assets.json enemies[id].spineLocal. Written by node tools/fetch-assets.mjs --local-spines (docs/ASSETS.md "Enemy aliases").',
|
||||
models: sorted,
|
||||
};
|
||||
await writeJsonAtomic(LOCAL_SPINES, doc, 2);
|
||||
log(`[local-spines] ${Object.keys(meta).length} model(s) → ${LOCAL_ENEMY_SPINES_FILE}`);
|
||||
const sorted = Object.fromEntries(Object.keys(models).sort().map((id) => [id, models[id]]));
|
||||
await writeJsonAtomic(join(ROOT, k.file), { about: k.about, models: sorted }, 2);
|
||||
log(`[local-spines] ${Object.keys(meta).length} ${k.what} model(s) → ${k.file}`);
|
||||
return sorted;
|
||||
}
|
||||
for (const [id, m] of Object.entries(meta)) {
|
||||
if (JSON.stringify(m) !== JSON.stringify(committed[id])) log(`[local-spines] ${id}: the extracted model differs from ${LOCAL_ENEMY_SPINES_FILE} (re-run with --local-spines to update it)`);
|
||||
if (JSON.stringify(m) !== JSON.stringify(committed[id])) log(`[local-spines] ${id}: the extracted model differs from ${k.file} (re-run with --local-spines to update it)`);
|
||||
}
|
||||
return committed;
|
||||
}
|
||||
@@ -304,7 +325,8 @@ async function main() {
|
||||
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 localEnemySpines = await syncLocalSpines(opts, 'enemy');
|
||||
const localTokenSpines = await syncLocalSpines(opts, 'token');
|
||||
const plan = buildPlan({
|
||||
assets07, ops03, enemies05, maps05, audio, modelsData, charword, voiceLang: opts.voiceLang,
|
||||
// default: only the slots a battle can play (plan.mjs VOICE_BATTLE_SLOTS); --voice-all takes the whole official set
|
||||
@@ -313,6 +335,7 @@ async function main() {
|
||||
extraTokenIds: [...Object.keys(dataTokens || {}), ...extras.tokenIds],
|
||||
extraHandbook,
|
||||
localEnemySpines,
|
||||
localTokenSpines,
|
||||
extraOperators: extras.extraOperators,
|
||||
moduleTypes: extras.moduleTypes,
|
||||
});
|
||||
@@ -409,6 +432,8 @@ async function main() {
|
||||
log(`on disk (manifest) : ${mb(s.bytes)} in ${s.files} files`);
|
||||
log(`chars ${s.chars} (Back model ${s.charsWithBack}) · enemies ${s.enemies} (Spine ${s.enemiesWithSpine}) · tokens ${s.tokens} (Spine ${s.tokensWithSpine}) · Spine models ${s.spineModels}`);
|
||||
log(`bonds ${s.bonds} · items ${s.items} · bands ${s.bands} · skill icons ${s.skills} · UI ${s.ui} · units with SFX ${s.sfxUnits}`);
|
||||
const overlays = (o) => Object.values(o || {}).filter((e) => e?.spineLocal).length;
|
||||
log(`local-client models (spineLocal, drawn when extracted): enemies ${overlays(manifest.enemies)} · tokens ${overlays(manifest.tokens)}`);
|
||||
log(`operator battle voice: ${s.voiceChars} charIds (--voice-lang=${opts.voiceLang})`);
|
||||
log(`fonts: ${Object.values(fonts.files).map((f) => f.woff2 || f.original).join(', ') || 'none'}`);
|
||||
if (resolved.fallbacks.length) { log(`fallbacks used (${resolved.fallbacks.length}):`); for (const f of resolved.fallbacks.slice(0, 20)) log(` ${f}`); }
|
||||
|
||||
Reference in new issue
Block a user