diff --git a/game-runtime/evidence/integration/evidence.json b/game-runtime/evidence/integration/evidence.json index 588770ac..356ada2c 100644 --- a/game-runtime/evidence/integration/evidence.json +++ b/game-runtime/evidence/integration/evidence.json @@ -1,6 +1,6 @@ { "meta": { - "ts": "2026-06-12T12:29:25.919Z", + "ts": "2026-06-13T05:14:05.317Z", "base": "http://localhost:4321", "cdpHttp": "http://localhost:9222", "seed": 305441741, @@ -11,233 +11,16 @@ "dpr": 2, "note": "core-protocol-v0 host-dev 集成证据(α 不接 littlejsengine)" }, - "smoke": { - "protocol": "core-protocol-v0", - "anchors": [ - "t_boot", - "t_plugins_ready", - "t_game_init", - "t_first_paint", - "t_input_bound", - "t_game_start" - ], - "recordCount": 6, - "verify": { - "ok": true - }, - "uncaught": [], - "registeredPlugins": [ - "collision", - "physics-lite", - "gamefeel", - "particles-juice", - "palette-post", - "save-progress", - "audio-music", - "example", - "runtime-probe" - ], - "initOk": true, - "initErrors": [] - }, - "firstInput": { - "beforeRecords": 6, - "afterRecords": 7, - "detail": { - "total": 7, - "firstInputCount": 1, - "sample": { - "seq": 6, - "anchor": "t_input_bound", - "tMono": 150, - "buildSha": "", - "pluginSet": [ - "audio-music", - "collision", - "example", - "gamefeel", - "palette-post", - "particles-juice", - "physics-lite", - "runtime-probe", - "save-progress" - ], - "prevHash": "77ab8a73", - "hash": "7986e506", - "extra": { - "firstInput": true, - "type": "pointerdown" - }, - "warning": "duplicate_anchor;out_of_order" - } - } - }, - "vfx": { - "burst": { - "seed": 1001, - "frames": 12, - "hashes": [ - "c53f293e", - "c53f293e", - "c53f293e" - ], - "consistent": true, - "nonEmpty": true, - "hasContent": true, - "histogramTop": [ - { - "color": "r0g0b0", - "count": 1313579 - }, - { - "color": "r11g19b23", - "count": 449 - }, - { - "color": "r10g17b21", - "count": 420 - }, - { - "color": "r11g18b22", - "count": 353 - }, - { - "color": "r11g18b23", - "count": 301 - } - ], - "geometry": [ - { - "region": { - "x": 120, - "y": 580, - "w": 540, - "h": 540, - "expect": "non-empty" - }, - "pass": true, - "opaquePixels": 291600 - } - ] - }, - "trail": { - "seed": 2002, - "frames": 20, - "hashes": [ - "7c62d75d", - "7c62d75d", - "7c62d75d" - ], - "consistent": true, - "nonEmpty": true, - "hasContent": true, - "histogramTop": [ - { - "color": "r0g0b0", - "count": 1316295 - }, - { - "color": "r15g25b31", - "count": 153 - }, - { - "color": "r3g5b6", - "count": 30 - }, - { - "color": "r14g24b29", - "count": 16 - }, - { - "color": "r0g1b1", - "count": 14 - } - ], - "geometry": [ - { - "region": { - "x": 120, - "y": 580, - "w": 540, - "h": 540, - "expect": "non-empty" - }, - "pass": true, - "opaquePixels": 291600 - } - ] - }, - "drift": { - "seed": 3003, - "frames": 40, - "hashes": [ - "b9265b7a", - "b9265b7a", - "b9265b7a" - ], - "consistent": true, - "nonEmpty": true, - "hasContent": true, - "histogramTop": [ - { - "color": "r0g0b0", - "count": 1316171 - }, - { - "color": "r13g21b26", - "count": 51 - }, - { - "color": "r9g16b19", - "count": 45 - }, - { - "color": "r15g24b30", - "count": 43 - }, - { - "color": "r14g23b28", - "count": 38 - } - ], - "geometry": [ - { - "region": { - "x": 60, - "y": 580, - "w": 660, - "h": 720, - "expect": "non-empty" - }, - "pass": true, - "opaquePixels": 475200 - } - ] - } - }, - "palette": { - "vignette": { - "seed": 5005, - "frames": 30, - "hashOff": "ce26c98e", - "hashOn": "b48da7ef", - "differ": true, - "bothNonEmpty": true - }, - "dither": { - "seed": 6006, - "frames": 30, - "hashOff": "2c23d9b8", - "hashOn": "c62e4397", - "differ": true, - "bothNonEmpty": true - } - }, + "smoke": null, + "firstInput": null, + "vfx": {}, + "palette": {}, "gates": { - "smoke": true, - "vfx": true, - "firstInput": true + "smoke": false, + "vfx": false, + "firstInput": false }, + "fatal": "AggregateError", "failures": [], - "pass": true + "pass": false } \ No newline at end of file diff --git a/game-runtime/src/core/plugin.js b/game-runtime/src/core/plugin.js index a1ac00b1..4c491636 100644 --- a/game-runtime/src/core/plugin.js +++ b/game-runtime/src/core/plugin.js @@ -393,6 +393,12 @@ function createHostDevContext(opts) { let audioCtx = null; // 无工厂告警只发一次的闸(防每帧刷屏)。 let audioWarned = false; + // 引擎能力背书工厂(可空,T1b-β):lazy 创建,全 host 共享同一引擎能力面。host-dev 桩缺省无工厂→getEngine 返 null。 + const engineFactory = typeof o.engineFactory === 'function' ? o.engineFactory : null; + /** @type {EngineCapabilities|null} lazy 创建后的引擎能力面缓存(null=尚未创建或无工厂)。 */ + let engineCaps = null; + // 无工厂告警只发一次的闸(防每帧刷屏)。 + let engineWarned = false; // 帧回调表:用 Map 以便精确按句柄注销(key=自增 id) /** @type {Mapvoid>} */ @@ -424,6 +430,31 @@ function createHostDevContext(opts) { return audioCtx; } + /** + * lazy 获取受控引擎能力面(全 host 共享,T1b-β)。无工厂 → 返回 null 并告警一次(不抛错)。 + * host-dev/node 桩无引擎,缺省返 null;集成段注入真实工厂(封装 littlejsengine:ParticleEmitter/zzfxG/zzfxM/lerp/smoothStep)。 + * @returns {EngineCapabilities|null} + */ + function getEngineShared() { + if (engineCaps) return engineCaps; // 已创建:返回同一实例(lazy 单例)。 + if (!engineFactory) { + // 无工厂:插件容错降级——返回 null,仅首次告警一次(绝不抛错)。 + if (!engineWarned) { + logWarn('[host-dev engine] 未注入 engineFactory,getEngine 返回 null(插件须容错降级:粒子退受控面自管/合成核退 vendored/数学退内置;集成段注入真实背书)'); + engineWarned = true; + } + return null; + } + try { + engineCaps = engineFactory(); + } catch (e) { + // 工厂抛错也降级为 null(不连坐插件),记 stderr。 + logError('[host-dev engine] engineFactory 抛错,getEngine 降级返回 null', e); + engineCaps = null; + } + return engineCaps; + } + /** * 构造一个受控上下文(共享 canvas/帧/时间/输入桥/音频,random 按入参独立)。 * @param {any} scope 输入订阅作用域(null=直接用;插件对象=注册器派生,用于 dispose 定向回收)。 @@ -455,6 +486,10 @@ function createHostDevContext(opts) { // lazy 受控音频上下文(共享单例);无工厂返回 null 并告警一次(不抛错)。 return getAudioContextShared(); }, + getEngine() { + // lazy 受控引擎能力面(共享单例,T1b-β);host-dev 无工厂返回 null 并告警一次(插件须容错降级)。 + return getEngineShared(); + }, time, random, };