feat(playable): 首个 Mac 联网内测版 STB-01 soak 工具与发布状态文档
- 新增 script/playable_soak.sh:先做 soak 配置/资源校验,无 --allow-gameplay 只校验不启动客户端;N(>=10) 次独立正常退出运行 + 墙钟 soak,失败即停止后续 运行并记 BLOCKED;写本批次 release-manifest.json 后聚合 - run_client_gate.sh:确认的故障代理对所有 suite 启动(共享场景的退出运行也经代理); soak 超时下限只约束 soak 客户端(validate_soak 增加 soak_client 参数) - 新增本地 127.0.0.1 故障代理、RSS 采样/内存判定、窗口/指标/流程模块及其测试 - forest_mob_render_test 输出 PASS/FAIL 标记,供 rendering_batch_test.sh 识别 - 新增 docs/FIRST-MAC-PLAYABLE-STATUS.md:如实记录 PASS/BLOCKED、已知问题与环境需求 - .gitignore 排除本地场景配置、凭据文件与运行输出 离线回归:rendering_batch_test.sh failures=0,playable_gate_test.sh PASS, node 夹具测试 PASS。未联网运行,未重建候选包。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env node
|
||||
/* MAP-01 audit regression on a throw-away asset tree. Proves that unresolved
|
||||
* property CRCs, missing model/texture/motion files and an empty config map_key
|
||||
* are reported (exit 1 / 2) instead of passing, and that a resource-level
|
||||
* RUN/WALK gap is only "explained" by the reference keep-current-motion rule. */
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const script = path.join(path.dirname(new URL(import.meta.url).pathname), 'audit_playable_maps.mjs');
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mt-map-audit.'));
|
||||
let failures = 0;
|
||||
const check = (ok, label) => { if (ok) console.log(`ok - ${label}`); else { failures += 1; console.error(`not ok - ${label}`); } };
|
||||
|
||||
const write = (rel, text) => { const file = path.join(root, rel); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, text); };
|
||||
const object = (i, crc) => `Start Object${String(i).padStart(3, '0')}\n 100.0 -100.0 0.0\n ${crc}\n 0.0#0.0#0.0\n 0\nEnd Object\n`;
|
||||
const prop = (crc, type, key, value) => `YPRT\n${crc}\n${key}\t\t"${value}"\npropertyname\t\t"p${crc}"\npropertytype\t\t"${type}"\n`;
|
||||
const motlist = names => names.map(([name, file]) => `GENERAL ${name} ${file} 50`).join('\n') + '\n';
|
||||
|
||||
function buildFixture({ broken }) {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
write('mapa/setting.txt', 'ScriptType\tMapSetting\nCellScale\t200\nMapSize\t1\t1\nBasePosition\t0\t0\nTextureSet\ttextureset\\mapa.txt\nEnvironment\ta.msenv\n');
|
||||
write('mapa/000000/height.raw', 'x');
|
||||
write('mapa/000000/tile.raw', 'x');
|
||||
write('mapa/000000/attr.atr', 'x');
|
||||
const objects = [object(0, 11), object(1, 22), object(2, 33)];
|
||||
if (broken) objects.push(object(3, 44), object(4, 55));
|
||||
write('mapa/000000/areadata.txt', `AreaDataFile\n\nObjectCount ${objects.length}\n${objects.join('')}`);
|
||||
write('textureset/textureset/mapa.txt', 'TextureSet\n\nTextureCount 1\n\nStart Texture001\n "d:\\ymir work\\terrainmaps\\a\\field.dds"\n 5.0\nEnd Texture001\n');
|
||||
write('Terrain/ymir work/terrainmaps/a/field.dds', 'x');
|
||||
write('ETC/ymir work/environment/a.msenv', 'x');
|
||||
write('Property/property/t/tree.prt', prop(11, 'Tree', 'treefile', 'd:/ymir work/tree/a.spt'));
|
||||
write('Tree/ymir work/tree/a.spt', 'spt');
|
||||
write('TreeGeometry/manifest.json', JSON.stringify({ trees: { h: { source: 'Tree/ymir work/tree/a.spt', glb: 'h.glb', source_sha256: 'h' } } }));
|
||||
write('TreeGeometry/h.glb', 'glb');
|
||||
write('Property/property/b/house.prb', prop(22, 'Building', 'buildingfile', 'd:/ymir work/zone/house.gr2'));
|
||||
if (!broken) write('Zone/ymir work/zone/house.gr2', 'gr2');
|
||||
write('Property/property/e/fx.pre', prop(33, 'Effect', 'effectfile', 'd:/ymir work/effect/fx.mse'));
|
||||
write('Effect/ymir work/effect/fx.mse', 'mse');
|
||||
// 44 only exists outside Property/ (the runtime registry never sees it); 55 exists nowhere.
|
||||
write('Zone/property/stray.prb', prop(44, 'Building', 'buildingfile', 'd:/ymir work/zone/house.gr2'));
|
||||
write('root/npclist.txt', '2301\tent_a\n2302\tent_b\n');
|
||||
const mob = (code, names) => {
|
||||
const dir = `monster2/ymir work/monster2/${code}`;
|
||||
write(`${dir}/${code}.gr2`, 'gr2');
|
||||
write(`${dir}/${code}.dds`, 'dds');
|
||||
write(`${dir}/${code}.msm`, `BaseModelFileName "d:\\ymir work\\monster2\\${code}\\${code}.gr2"\n`);
|
||||
write(`${dir}/motlist.txt`, motlist(names));
|
||||
for (const [, file] of names) write(`${dir}/${file}`, 'msa');
|
||||
};
|
||||
mob('ent_a', [['WAIT', '00.msa'], ['NORMAL_ATTACK', '20.msa'], ['FRONT_DAMAGE', '30.msa'], ['FRONT_DEAD', '31.msa']]);
|
||||
const b = [['WAIT', '00.msa'], ['RUN', '10.msa'], ['NORMAL_ATTACK', '20.msa'], ['FRONT_DAMAGE', '30.msa']];
|
||||
if (!broken) b.push(['FRONT_DEAD', '31.msa']);
|
||||
mob('ent_b', b);
|
||||
}
|
||||
|
||||
function run(args) {
|
||||
const output = path.join(root, 'out', 'map-assets.json');
|
||||
const result = spawnSync(process.execPath, [script, '--assets', root, '--output', output, ...args], { encoding: 'utf8' });
|
||||
const report = fs.existsSync(output) ? JSON.parse(fs.readFileSync(output, 'utf8')) : null;
|
||||
return { code: result.status, report, stderr: result.stderr };
|
||||
}
|
||||
|
||||
try {
|
||||
buildFixture({ broken: false });
|
||||
let r = run(['--maps', 'mapa']);
|
||||
check(r.code === 0 && r.report?.status === 'PASS', `complete fixture passes (exit ${r.code}) ${r.stderr}`);
|
||||
check(r.report?.explained.some(e => e.scope === 'race:2301' && e.ref === 'run'), 'missing RUN/WALK is explained by the reference rule');
|
||||
const map = r.report?.maps[0];
|
||||
check(map?.static_spt[0]?.native_glb === 'TreeGeometry/h.glb' && map?.static_gr2[0]?.gr2 === 'Zone/ymir work/zone/house.gr2', 'tree SPT and building GR2 resolve');
|
||||
check(map?.texture_set?.textures[0]?.resolved === 'Terrain/ymir work/terrainmaps/a/field.dds', 'TextureSet entries resolve through ymir work');
|
||||
check(map?.effects[0]?.mse === 'Effect/ymir work/effect/fx.mse', 'effect property resolves');
|
||||
check(r.report?.tree_monster_candidates.every(m => m.candidate_only && m.msm?.matches_runtime_gr2), 'MSM base model matches runtime GR2 and races stay candidates');
|
||||
|
||||
buildFixture({ broken: true });
|
||||
r = run(['--maps', 'mapa']);
|
||||
const kinds = new Set((r.report?.missing || []).map(m => `${m.kind}:${m.ref}`));
|
||||
check(r.code === 1 && r.report?.status === 'FAIL', `broken fixture fails (exit ${r.code})`);
|
||||
check(kinds.has('building_gr2:d:/ymir work/zone/house.gr2'), 'missing building GR2 is listed');
|
||||
check(kinds.has('property:44') && r.report?.maps[0].unresolved_properties.some(p => p.property_id === 44 && p.found_outside_runtime_scope === 'Zone/property/stray.prb'), 'CRC outside Property/ is listed with its location');
|
||||
check(kinds.has('property:55'), 'unknown CRC is listed');
|
||||
check(kinds.has('motion:ent_b:dead'), 'missing death motion is not explained away');
|
||||
check(!kinds.has('motion:ent_a:run'), 'RUN/WALK gap is explained, not missing');
|
||||
|
||||
fs.rmSync(path.join(root, 'mapa', '000000', 'attr.atr'));
|
||||
r = run(['--maps', 'mapa']);
|
||||
check((r.report?.missing || []).some(m => m.kind === 'tile_file'), 'missing tile file is listed');
|
||||
|
||||
write('scenario.local.json', JSON.stringify({ scenario_id: 'x', map_key: '' }));
|
||||
r = run(['--config', path.join(root, 'scenario.local.json')]);
|
||||
check(r.code === 2 && /map_key is empty/.test(r.stderr), `empty config map_key is BLOCKED (exit ${r.code})`);
|
||||
write('scenario.local.json', JSON.stringify({ scenario_id: 'x', map_key: '../mapa' }));
|
||||
r = run(['--config', path.join(root, 'scenario.local.json')]);
|
||||
check(r.code === 2, 'map_key escaping the asset root is BLOCKED');
|
||||
write('scenario.local.json', JSON.stringify({ scenario_id: 'x', map_key: 'mapa' }));
|
||||
r = run(['--config', path.join(root, 'scenario.local.json')]);
|
||||
check(r.report?.map_source === 'config' && r.report?.maps[0].key === 'mapa', 'config map_key is the audited map');
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
console.log(`audit_playable_maps_test: failures=${failures}`);
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
Reference in New Issue
Block a user