diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java index 9fad83a2..a7968f46 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java @@ -449,6 +449,19 @@ final class SaaGenNodes { } catch (IOException e) { errs.add("amodel staged 产物读取失败:" + e.getMessage()); } + // ③ 补写 play-spec.json(治九门 E_live/G_input/H_progress):amodel 路由 validate-ok→play 短路了 scaffoldNode + // (唯一写 play-spec.json 处),九门 play.cdp.cjs 读不到 spec → 用空默认 → 不驱动游戏(停 menu→E_live distinctStates:1)、 + // 不跑 assertAfterPlay(H_progress skip)。故在此从 K_PLAY_SPEC(design 节点产 gatespec merge 后的 playSpec)补落盘, + // 与 scaffoldNode 同语义。harness 只 stage bundle+index、不知 gatespec,故由 Java 侧补。 + if (errs.isEmpty()) { + try { + Path spec = gameRuntimeRoot.resolve("games").resolve("_wg1-gen").resolve(gameId).resolve("play-spec.json"); + String playSpec = s.value(SaaStudioNodes.K_PLAY_SPEC, String.class).orElse("{}"); + Files.write(spec, playSpec.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + errs.add("amodel play-spec.json 写入失败:" + e.getMessage()); + } + } return validateOut(s, errs); } diff --git a/game-runtime/games/_template/src/game-logic.js b/game-runtime/games/_template/src/game-logic.js index 1357ccad..798a1c74 100644 --- a/game-runtime/games/_template/src/game-logic.js +++ b/game-runtime/games/_template/src/game-logic.js @@ -79,8 +79,10 @@ export function createGame({ plugins, bundle, viewport }) { roundTimer = timerScheduler.after(ROUND_MS, () => { sessionScore.lose(); sceneFsm.transition(SCENE_OVER); + if (ctx) ctx.log('scene', 'over 结算', { score: sessionScore.getScore(), best: sessionScore.getBest() }); }); sceneFsm.transition(SCENE_PLAY); + if (ctx) ctx.log('scene', 'play 开始', { roundMs: ROUND_MS }); } /** 处理一次"点屏"(play 期命中判定;menu/over 期点按钮)。 */ @@ -131,6 +133,7 @@ export function createGame({ plugins, bundle, viewport }) { defineScenes(); sceneFsm.start(SCENE_MENU); inited = true; + ctx.log('init', 'game-logic 就绪 menu', { vw: VW, vh: VH }); // 运行时日志:读它诊断(boot/scene/score/err 都记) }, /** 逻辑步:驱动插件 onFrame(必调 bundle.tick)+ 场景 update + 倒计时显示递减。 */ diff --git a/game-runtime/src/core/plugin.js b/game-runtime/src/core/plugin.js index 4c491636..f84c207b 100644 --- a/game-runtime/src/core/plugin.js +++ b/game-runtime/src/core/plugin.js @@ -490,6 +490,28 @@ function createHostDevContext(opts) { // lazy 受控引擎能力面(共享单例,T1b-β);host-dev 无工厂返回 null 并告警一次(插件须容错降级)。 return getEngineShared(); }, + /** + * 受控运行时日志(可观测红线:让"读日志诊断运行时问题"成立)。写 window.__gameLog 环形缓冲 + console 镜像。 + * 游戏/插件经 ctx.log(tag, ...args) 记语义事件(boot/scene/score/spawn/err 等);harness 把 __gameLog dump 进 evidence 供离线读。 + * 时间用受控 time.nowMs(不裸 Date.now);node/无 window 时仅 console;失败静默(日志绝不影响玩法)。 + * @param {string} tag 事件标签(如 'init'/'scene'/'score'/'err') + * @param {...any} args 附加信息(对象自动 JSON 化) + */ + log(tag, ...args) { + try { + const msg = args.map((a) => { + if (a == null || typeof a !== 'object') return String(a); + try { return JSON.stringify(a); } catch (_) { return String(a); } + }).join(' '); + const entry = { t: (time && time.nowMs) ? time.nowMs() : 0, scope: scope || 'game', tag: String(tag), msg }; + if (typeof window !== 'undefined') { + const buf = (window.__gameLog || (window.__gameLog = [])); + buf.push(entry); + if (buf.length > 800) buf.shift(); // 环形上限,防无限增长拖垮页面 + } + if (typeof console !== 'undefined' && console.log) console.log('[' + entry.scope + ':' + entry.tag + ']', msg); + } catch (_) { /* 日志失败不影响玩法 */ } + }, time, random, }; diff --git a/game-runtime/src/host/boot-game-host.js b/game-runtime/src/host/boot-game-host.js index 63394f6c..fab7e225 100644 --- a/game-runtime/src/host/boot-game-host.js +++ b/game-runtime/src/host/boot-game-host.js @@ -42,6 +42,45 @@ import { createEngineCaps } from '../../host-dev/engine-caps.js'; // 引擎数学门面构造(纯函数,零引擎 import):real 通道用它包装引擎 lerp/smoothStep/Ease。 import { buildEngineMath } from '../../host-dev/engine-math.js'; +/** + * B2(治九门 F_wiring 对 3 层架构):给插件实例集包一层 recHook 取证视图(Proxy 透明、保 this 绑定)。 + * 每次游戏调某插件的方法 → 记 call-ID `键.方法` 进 recHook(渠道产物 no-op)。新架构游戏经插件 + 2D context 画、 + * 多不直调 boot.ctx.getEngine() → 引擎 recHook 记 0;经此包装后,游戏真用能力库即被记录,F_wiring 据 __engineCalls 判真用非自绘。 + * 非函数属性原样透传;插件对象/this 不变(v.apply(t))。仅在 opts.recHook 提供时调用(见 bootGameHost 内)。 + * @param {Record} plugins 原始插件实例集(host 拥有)。 + * @param {(id:string)=>void} recHook call-ID 取证钩子。 + * @returns {Record} 包装后的插件视图(给游戏工厂;host 自身仍用原始集)。 + */ +function wrapPluginsRecHook(plugins, recHook) { + const out = {}; + for (const key of Object.keys(plugins || {})) { + const p = plugins[key]; + if (!p || typeof p !== 'object') { out[key] = p; continue; } + out[key] = new Proxy(p, { + get(t, prop) { + const v = t[prop]; + if (typeof v === 'function') { + return (...args) => { + const ps = String(prop); + try { recHook(key + '.' + ps); } catch (_) { /* 取证钩子异常不影响玩法 */ } + // 插件自带日志(语义事件→window.__gameLog;跳过高频绘制/查询 draw*/render*/point*/measure*/get* 防刷屏)。 + try { + if (typeof window !== 'undefined' && !/^(draw|render|point|measure|get|is|update|current|tick|step|onFrame|pending)/.test(ps)) { + const b = (window.__gameLog || (window.__gameLog = [])); + b.push({ t: 0, scope: 'p:' + key, tag: ps, msg: '' }); + if (b.length > 800) b.shift(); + } + } catch (_) { /* 日志失败不影响玩法 */ } + return v.apply(t, args); + }; + } + return v; + }, + }); + } + return out; +} + /** 取证固定环境(与 host-dev / browser-evidence FIXED_ENV 对齐;P1 沿用)。 */ const DPR = 2; const FIXED_DT = 1 / 60; @@ -189,7 +228,11 @@ export async function bootGameHost(opts) { mark('t_plugins_ready', { initialized: initRes.initialized.length, ok: initRes.ok }); // 4) 经装载契约建游戏实例(host 经 buildFactoryOpts 注入 wiring:wanglanmei runtime / P3 纯净工厂为 undefined)。 - const factoryOpts = buildFactoryOpts(bundle.context, { bundle, plugins: pluginInstances, getRealAudioCtx: () => realAudioCtx }); + // B2(治九门 F_wiring 对 3 层架构):新架构游戏经插件 + 2D context 画、多不直调 getEngine,故引擎 recHook 记不到 → + // 给「游戏的插件视图」用 Proxy 包一层 recHook(每次游戏调插件方法记 call-ID=键.方法 写 __engineCalls,证真用能力库非自绘伪装)。 + // 仅 opts.recHook 提供时包(测试/取证态);渠道产物 recHook=no-op→传原始实例零开销。host 自身 register/init/onFrame 用原始 pluginInstances,不受影响。 + const pluginsForGame = opts.recHook ? wrapPluginsRecHook(pluginInstances, recHook) : pluginInstances; + const factoryOpts = buildFactoryOpts(bundle.context, { bundle, plugins: pluginsForGame, getRealAudioCtx: () => realAudioCtx }); game = factory(factoryOpts); // 5) game.init:建游戏对象 + 异步装资产 + 订阅受控输入。boot=装载契约脊柱(ctx/mainContext/canvas/seed)。 // HOST TODO(plan 2026-06-19-001 U1 剩余 wiring):U1 已建运行时 sprite 渲染 + boot.assets 契约(按 asset.ref 键), diff --git a/game-runtime/tools/amodel-gen/gen.mjs b/game-runtime/tools/amodel-gen/gen.mjs index 40f356e4..58bcfac3 100644 --- a/game-runtime/tools/amodel-gen/gen.mjs +++ b/game-runtime/tools/amodel-gen/gen.mjs @@ -56,8 +56,8 @@ async function main() { const isSaa = mode === 'saa'; const id = isSaa ? arg('game-id', arg('id', 'run1')) : arg('id', 'run1'); const brief = arg('brief', '一个简单的点击得分小游戏'); - const maxSteps = Number(arg('max-steps', '45')); // saae4 实测需 41 步(含 1 次循环内自恢复)→ 默认 45,别压到 30 误杀正常 run - const maxWallMs = Number(arg('max-wall-ms', '720000')); // 单次 attempt 墙钟上限 12min:bound thrash(saae3 thrash 1646s)而不误杀正常(saae4 446s);超时失败 → 交批 B SAA repair fresh 重试 + const maxSteps = Number(arg('max-steps', '70')); // 默认 70:raw brief 7 步(3层架构 newarch1),但 SAA design 富化 brief(juice/音效/屏震/gatespec)需更多步;留余量 + const maxWallMs = Number(arg('max-wall-ms', '1200000')); // 单次 attempt 墙钟上限 20min:design 富化 brief + M3 大上下文 turn 慢,raw 102s 但富化需更久;bound thrash 而不误杀富化正常 run const port = Number(arg('port', '4320')); const cdpPort = Number(arg('cdp', '9222')); // 模式分派:SAA 用 __GameBundle 信封 + buildSaa + stage + smoke;spike 用 _template 信封 + build + play。 diff --git a/game-runtime/tools/amodel-gen/play.mjs b/game-runtime/tools/amodel-gen/play.mjs index 5c7221c6..b9785bc8 100644 --- a/game-runtime/tools/amodel-gen/play.mjs +++ b/game-runtime/tools/amodel-gen/play.mjs @@ -208,6 +208,17 @@ export async function smokeBoot(gameId, opts = {}) { out.f1 = await cdp.evalJs('window.__genHost ? window.__genHost.frameCount() : -1'); out.framesAdvanced = typeof out.f1 === 'number' && out.f1 > out.f0; } + // 抓运行时日志(window.__gameLog:ctx.log 游戏语义 + 插件自带日志)→ 写 staged evidence + 末段进 out。 + // 无论 boot 成败都抓(boot 失败时日志最有诊断价值)。让"读日志诊断运行时问题"成立。 + try { + const logJson = await cdp.evalJs('JSON.stringify(window.__gameLog||[])').catch(() => '[]'); + const arr = JSON.parse(logJson || '[]'); + out.logCount = arr.length; + out.logTail = arr.slice(-12).map((e) => `[${e.scope}:${e.tag}]${e.msg ? ' ' + e.msg : ''}`); + const evDir = join(GAME_RUNTIME, 'games', '_wg1-gen', gameId, 'evidence'); + mkdirSync(evDir, { recursive: true }); + writeFileSync(join(evDir, 'game-log.json'), JSON.stringify(arr, null, 2)); + } catch (_) { /* 日志捕获失败不影响 smoke 判定 */ } } catch (e) { out.bootErr = out.bootErr || String((e && e.message) || e); } finally {