0.2.5-dev: official battlefield scenes on the 3D board — a local fetch / extract step for the season scene bundles, per-field scene rendering with the metal board as fallback, the full bundle can include the extracted scenes and setup can fetch them (--scenes); nothing official is stored in the repository (PR #533 by @andrew05060414, ported; the owner's decision of 2026-10-12); golden: unchanged
This commit is contained in:
1 parent
7fd0478dc0
commit
d482fbb8ba
19 files changed
+1659
-22
No files matched your search
+92
-3
@@ -15,6 +15,11 @@
|
||||
// bottle or PlayCover on macOS, or --game <dir>) with tools/local-extract/extract.py in a project-local Python
|
||||
// venv (.venv-extract), then the board tile crops (tools/crop-board-atlas.mjs → tiles.json). Asked once on a
|
||||
// terminal (the answer is remembered in .cache/setup-state.json; without a terminal it is skipped); never fatal.
|
||||
// 7. Optional: the eleven official battlefield scenes (0.2.5 asset pack). The full release zip already contains
|
||||
// them when the maintainer extracted them. A lite package or a source install may download the in-season bundles
|
||||
// (tools/local-extract/fetch_scenes.py) and extract them (extract.py --only scene). Missing Python or
|
||||
// UnityPy / lz4 / Pillow is a warning: the 3D board keeps the metal tiles. --scenes does it without asking;
|
||||
// --no-scenes skips it. The answer is remembered like step 6.
|
||||
//
|
||||
// Options:
|
||||
// --check report only, change nothing (exit 1 when something essential is missing)
|
||||
@@ -22,8 +27,10 @@
|
||||
// --asset-source=M direct (default) or mirror (opt-in; no public-IP lookup)
|
||||
// SP_ASSET_SOURCE sets the default; SP_GITHUB_PROXY sets the HTTPS prefix (https://gh-proxy.com/)
|
||||
// An empty SP_GITHUB_PROXY disables the proxy, including in mirror mode.
|
||||
// --no-local skip the local-client detection and extraction
|
||||
// --no-local skip the local-client detection and extraction (the scene step still runs unless --no-scenes)
|
||||
// --local extract from the local client without asking (re-extracts when already done)
|
||||
// --scenes download and extract the official battlefield scenes without asking
|
||||
// --no-scenes do not download or extract battlefield scenes (the metal board stays)
|
||||
// --game <dir> AssetBundle root of the local client (…/StreamingAssets/AB/Windows or PlayCover …/Documents/Bundles)
|
||||
// -y, --yes answer "yes" to every question
|
||||
// --quiet fewer lines (used by scripts/launch.mjs)
|
||||
@@ -58,6 +65,7 @@ export const VENDOR_OPTIONAL = ['three.core.js', 'three.module.js'];
|
||||
const STATE_FILE = path.join(ROOT, '.cache', 'setup-state.json');
|
||||
export const VENV_DIR = path.join(ROOT, '.venv-extract');
|
||||
const EXTRACT_PY = path.join(ROOT, 'tools', 'local-extract', 'extract.py');
|
||||
const FETCH_SCENES_PY = path.join(ROOT, 'tools', 'local-extract', 'fetch_scenes.py');
|
||||
const EXTRACT_REQ = path.join(ROOT, 'tools', 'local-extract', 'requirements.txt');
|
||||
const LOCAL_MANIFEST = path.join(ROOT, 'data', 'local-assets.json');
|
||||
const LOCAL_BOARD_ATLAS = path.join(ROOT, 'public', 'assets', 'local', 'map', 'autochess', 'TX_autochessi_D.png');
|
||||
@@ -217,7 +225,7 @@ export function checkLocal() {
|
||||
const has = (prefix) => !!(m && m.groups && Object.keys(m.groups).some((g) => g.startsWith(prefix)));
|
||||
return {
|
||||
manifest: !!m, count, board3d: exists(LOCAL_BOARD_ATLAS), tiles: exists(LOCAL_BOARD_TILES), enemySpines: has('spine/enemy/'),
|
||||
tokenSpines: has('spine/token/'), dirPresent: exists(path.join(ROOT, 'public', 'assets', 'local')),
|
||||
tokenSpines: has('spine/token/'), scenes: has('scene/act'), dirPresent: exists(path.join(ROOT, 'public', 'assets', 'local')),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -226,9 +234,47 @@ export function localGaps(local) {
|
||||
const gaps = [];
|
||||
if (local && !local.enemySpines) gaps.push('灼热/炽焰源石虫模型');
|
||||
if (local && !local.tokenSpines) gaps.push('自选召唤物模型');
|
||||
if (local && local.scenes === false) gaps.push('各战场官方场景');
|
||||
return gaps.length ? `,缺少新版的${gaps.join('和')}` : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether setup should download and extract the official battlefield scenes (0.2.5 asset pack).
|
||||
* The full release zip already contains them when the maintainer extracted them before packaging.
|
||||
* A lite package or a source install may fetch the in-season bundles on this machine and extract them.
|
||||
* Missing Python or UnityPy / lz4 / Pillow is not fatal: the 3D board keeps the metal tiles.
|
||||
* `client` is true (autochess bundles present), 'incomplete' (a client dir without them) or false.
|
||||
* @param {{ hasScenes?: boolean, client?: boolean|string, python?: boolean, deps?: boolean, check?: boolean, declined?: boolean, force?: boolean, yes?: boolean }} o
|
||||
* @returns {{ run: boolean, state: 'ok'|'skip'|'warn'|'ask', detail: string }}
|
||||
*/
|
||||
export function sceneInstallPlan(o = {}) {
|
||||
if (o.hasScenes && !o.force) {
|
||||
return { run: false, state: 'ok', detail: '已提取各战场官方场景(0.2.5 素材;某个战场没有文件时仍用金属地块)' };
|
||||
}
|
||||
if (o.check) {
|
||||
return { run: false, state: 'skip', detail: '未提取各战场官方场景:3D 棋盘用金属地块(node tools/setup.mjs --scenes 可在本机下载并提取)' };
|
||||
}
|
||||
if (o.client === 'incomplete') {
|
||||
return { run: false, state: 'warn', detail: '客户端缺少卫戍协议资源,战场场景未提取。3D 棋盘继续用金属地块' };
|
||||
}
|
||||
if (!o.client) {
|
||||
return { run: false, state: 'skip', detail: '没有本机客户端,不能提取战场场景(贴图仍取自客户端)。3D 棋盘用金属地块;完整包已带场景时可复制 public/assets/local 和 data/local-assets.json' };
|
||||
}
|
||||
if (!o.python) {
|
||||
return { run: false, state: 'warn', detail: '没有 Python 3.8+,战场场景未提取。3D 棋盘继续用金属地块' };
|
||||
}
|
||||
if (o.deps === false) {
|
||||
return { run: false, state: 'warn', detail: '提取依赖(UnityPy / lz4 / Pillow)没装好,战场场景未提取。3D 棋盘继续用金属地块' };
|
||||
}
|
||||
if (o.declined && !o.force && !o.yes) {
|
||||
return { run: false, state: 'skip', detail: '已跳过战场场景(node tools/setup.mjs --scenes)。3D 棋盘用金属地块' };
|
||||
}
|
||||
if (!o.force && !o.yes) {
|
||||
return { run: false, state: 'ask', detail: '可以下载赛季场景包并提取(约 23 MB)。失败或跳过时 3D 棋盘仍用金属地块' };
|
||||
}
|
||||
return { run: true, state: 'ok', detail: '将下载赛季场景包并提取;失败时 3D 棋盘仍用金属地块' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Board tile crops of the extracted atlas (tools/crop-board-atlas.mjs → tiles.json, read by the 2D and 3D board
|
||||
* renderers; without it the client requests a missing file and keeps the procedural tiles). Output only on failure.
|
||||
@@ -347,7 +393,7 @@ function ensureVenv(py, log) {
|
||||
// ---------------------------------------------------------------------------------------------------
|
||||
|
||||
function parseArgs(argv) {
|
||||
const o = { check: false, assets: true, local: 'ask', game: null, yes: false, quiet: false, help: false, source: process.env.SP_ASSET_SOURCE || 'direct' };
|
||||
const o = { check: false, assets: true, local: 'ask', scenes: 'ask', game: null, yes: false, quiet: false, help: false, source: process.env.SP_ASSET_SOURCE || 'direct' };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === '--check') o.check = true;
|
||||
@@ -356,6 +402,8 @@ function parseArgs(argv) {
|
||||
else if (a.startsWith('--asset-source=')) o.source = a.slice('--asset-source='.length);
|
||||
else if (a === '--no-local') o.local = 'no';
|
||||
else if (a === '--local') o.local = 'force';
|
||||
else if (a === '--no-scenes') o.scenes = 'no';
|
||||
else if (a === '--scenes') o.scenes = 'force';
|
||||
else if (a === '--game') { o.game = argv[++i] || null; if (o.local !== 'no') o.local = 'force'; }
|
||||
else if (a.startsWith('--game=')) { o.game = a.slice(7) || null; if (o.local !== 'no') o.local = 'force'; }
|
||||
else if (a === '-y' || a === '--yes') o.yes = true;
|
||||
@@ -497,6 +545,47 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// 7. official battlefield scenes (optional; the full zip ships them, a lite / source install may fetch them)
|
||||
if (opts.scenes !== 'no') {
|
||||
const sceneLocal = checkLocal();
|
||||
const sceneClient = findClient(opts.game);
|
||||
const scenePy = findPython();
|
||||
const clientFlag = sceneClient?.autochess ? true : (sceneClient ? 'incomplete' : false);
|
||||
let decision = sceneInstallPlan({
|
||||
hasScenes: sceneLocal.scenes, client: clientFlag, python: !!scenePy, check: opts.check,
|
||||
declined: !!loadState().sceneDeclined, force: opts.scenes === 'force', yes: opts.yes,
|
||||
});
|
||||
if (decision.state === 'ask') {
|
||||
log(`\n${c.cyan('▶')} 各战场的官方场景还没提取(11 个战场;赛季场景包约 23 MB,贴图仍取自本机客户端)。`);
|
||||
log(' 下载并提取后,3D 棋盘按战场显示官方场景。没有提取依赖、下载失败或跳过时,仍用现在的金属地块。');
|
||||
const answer = await ask('现在下载并提取吗?', true);
|
||||
if (answer === null) decision = { run: false, state: 'skip', detail: '无终端,未询问、未提取战场场景。3D 棋盘用金属地块(需要时 node tools/setup.mjs --scenes)' };
|
||||
else if (!answer) {
|
||||
saveState({ sceneDeclined: true });
|
||||
decision = { run: false, state: 'skip', detail: '已跳过战场场景(node tools/setup.mjs --scenes)。3D 棋盘用金属地块' };
|
||||
} else decision = { run: true, state: 'ok', detail: decision.detail };
|
||||
}
|
||||
if (!decision.run) add(decision.state === 'ok' ? 'ok' : decision.state, '各战场官方场景(可选)', decision.detail);
|
||||
else {
|
||||
const venv = ensureVenv(scenePy, log);
|
||||
if (!venv.ok) add('warn', '各战场官方场景(可选)', `${venv.why}。3D 棋盘继续用金属地块`);
|
||||
else {
|
||||
log(` ${c.cyan('▶')} 下载赛季场景包(python tools/local-extract/fetch_scenes.py,约 23 MB)`);
|
||||
const fr = run(venv.python, [FETCH_SCENES_PY], { env: PY_ENV });
|
||||
if (!fr.ok) add('warn', '各战场官方场景(可选)', `场景包下载未完成(退出码 ${fr.code})。3D 棋盘继续用金属地块;稍后可重试 node tools/setup.mjs --scenes`);
|
||||
else {
|
||||
log(` ${c.cyan('▶')} 提取战场场景(extract.py --only scene)`);
|
||||
const er = run(venv.python, [EXTRACT_PY, '--game', sceneClient.path, '--only', 'scene'], { env: PY_ENV });
|
||||
const afterScenes = checkLocal();
|
||||
if (er.ok && afterScenes.scenes) {
|
||||
add('ok', '各战场官方场景(可选)', '已提取;进入对局时按战场显示,没有的战场仍用金属地块');
|
||||
saveState({ sceneDeclined: false });
|
||||
} else add('warn', '各战场官方场景(可选)', `提取未成功(退出码 ${er.code})。3D 棋盘继续用金属地块`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// summary
|
||||
log(c.bold('\n── 准备情况 ──────────────────────────────'));
|
||||
const width = Math.max(...summary.map((s) => displayWidth(s.label))) + 2;
|
||||
|
||||
Reference in new issue
Block a user