From 28a57b8e00929fc40602483cb33042358f70abb7 Mon Sep 17 00:00:00 2001 From: zizi Date: Sat, 13 Jun 2026 05:12:35 +0000 Subject: [PATCH] =?UTF-8?q?feat(contracts):=20T1b-=CE=B2=20P2=20=E4=B8=89?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=E5=86=BB=E7=BB=93(grounded=20=E6=8D=AE?= =?UTF-8?q?=E7=9C=9F=E7=AD=BE=E5=90=8D+=E6=8F=92=E4=BB=B6=E7=9C=9F?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E9=9D=A2)=E2=80=94=E2=80=94=E5=BE=85?= =?UTF-8?q?=E5=AF=B9=E6=8A=97=E5=A4=8D=E5=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ①受控面引擎能力透传通道 PluginContext.getEngine()→EngineCapabilities(api.d.ts):白名单3项particles.spawnEmitter/audio.synth/math.lerp+smoothStep(YAGNI核6件impl真调用点);铁律=封装禁裸Vector2/引擎import只活集成段host背书engineFactory不散进插件impl(保引擎可换+满足门1真接线);random/time集成段换引擎RandomGenerator/timeReal背书(契约形状不变) ②GamePackage manifest加packageUrl+immutable(additive,复用后端packageUrl镜像走DB-manifest降级,checksum复用旧字段供门4防demo假阳性) ③SDK StoragePlugin根契约上提(sdk-interface.d.ts):前端contract.ts定稿对象形态上提+含R4-P1 array排除契约语义声明 grounded工位核impl逮关键澄清:hitStop/屏震非通道项(impl.js:582-615=受控面输出+游戏层render.js:915应用闭环,插件不调引擎;屏震引擎化=render.js切相机属agent生成域)→通道收敛6件→3项,§7/§6 A2同步澄清;audio.synth留契约A2据SIZES定用否 JSON校验过+.d.ts括号平衡 Co-Authored-By: Claude Fable 5 --- contracts/game-package.schema.json | 11 +- contracts/sdk-interface.d.ts | 48 ++++++++ ...13-T1b-β引擎真接线与Runner-v2-execution.md | 8 +- game-runtime/src/core/api.d.ts | 112 ++++++++++++++++++ 4 files changed, 174 insertions(+), 5 deletions(-) diff --git a/contracts/game-package.schema.json b/contracts/game-package.schema.json index 22f9b6b6..6df910ed 100644 --- a/contracts/game-package.schema.json +++ b/contracts/game-package.schema.json @@ -62,7 +62,16 @@ "enum": ["eager", "lazy"] }, "bundleSize": { "description": "包总字节(编译期门禁校验上限)", "type": "integer", "minimum": 0 }, - "checksum": { "description": "整包 sha256(hex),Manifest 完整性校验", "type": "string", "pattern": "^[a-f0-9]{64}$" } + "checksum": { "description": "整包 sha256(hex),Manifest 完整性校验。T1b-β 门4 防 demo 兜底假阳性=Network 捕获本 checksum 比对发布版本", "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "packageUrl": { + "description": "运行包可访问 URL(T1b-β Runner v2 交付锚;additive 可选,存量消费者无此字段时回退后端 resolveManifestUrl 原链路)。MVP 默认降级=复用后端 game_version.package_url 走 DB-manifest 端点 /app-api/runtime/package/{versionId}/manifest(天然含 versionId 版本化,不接 OSS、不改写链);切 OSS 后=OSS 版本化路径。= 后端 RuntimePackageDO.packageUrl 的 GamePackage 契约镜像(同源同物,后端写权)。", + "type": "string", + "format": "uri" + }, + "immutable": { + "description": "本 URL 是否携带 immutable 强缓存语义(Cache-Control: public,max-age=31536000,immutable)。true=URL 版本化+内容寻址、命中即永久缓存(编译缓存靠 versionId 兜,门4 用 CDP Network timing 验缓存命中 fromCache/304);false/缺省=普通缓存。additive 可选。", + "type": "boolean" + } } }, "meta": { diff --git a/contracts/sdk-interface.d.ts b/contracts/sdk-interface.d.ts index b117a932..5245c898 100644 --- a/contracts/sdk-interface.d.ts +++ b/contracts/sdk-interface.d.ts @@ -40,6 +40,8 @@ export interface WanxiangGameSDK { /** 插件(首次调用懒加载) */ ad: AdPlugin; pay: PayPlugin; + /** 存储插件(首次调用懒加载;idle 离线存档;T1b-β 由前端 host/contract.ts 定稿形态上提至根契约) */ + storage: StoragePlugin; } /** 广告插件(MVP 桩 → 真实穿山甲/优量汇切换;每调用 5s 超时降级) */ @@ -56,6 +58,22 @@ export interface PayPlugin { pay(order: { orderId: string; amount: number }, cb: (result: { paid: boolean }) => void): void; } +/** 存储插件(idle 离线产出存档;HJ-MC-TPL-EXEC-002 §6.1.1 定稿对象形态,T1b-β 上提至根契约,semver 只增不改) */ +export interface StoragePlugin { + /** + * 写入存档(fire-and-forget,不返回 Promise)。宿主侧白名单前缀校验 + 大小上限校验后落 localStorage。 + * @param key 存档键(runtime 侧按 'idle:'+gameId+':'+versionId 约定)。 + * @param value 存档值(JSON 字符串;宿主侧落库前校验大小)。 + */ + save(key: string, value: string): void; + /** + * 读取存档(宿主受信边界已解析为对象后回调;runtime 侧零 JSON.parse,守 runtime §5.5 零解析红线)。 + * @param key 存档键。 + * @param cb 回调;命中=已解析对象,未命中/校验失败/**非纯对象(含数组)**=null(见 StorageResultPayload.value 的 array 排除)。 + */ + load(key: string, cb: (value: Record | null) => void): void; +} + /* ============================================================================ * SDK ↔ 宿主 postMessage 协议 * 游戏在 iframe 沙箱内,通过 postMessage 与 game-studio 宿主通信。 @@ -87,3 +105,33 @@ export interface PostMessageEnvelope { traceId: string; payload: T; } + +/* ----- storage 消息 payload(idle 离线产出存档;HJ-MC-TPL-EXEC-002 §6.1.1,T1b-β 上提,semver 只增不改) ----- */ + +/** storage 写入请求 payload(游戏→宿主:保存键值) */ +export interface StorageSetPayload { + /** 存档键(runtime 侧按 'idle:'+gameId+':'+versionId 约定,宿主侧白名单前缀校验) */ + key: string; + /** 存档值(JSON 字符串,宿主侧落 localStorage 前校验大小上限) */ + value: string; +} + +/** storage 读取请求 payload(游戏→宿主:按 key 取值;带 requestId,宿主读分支按同 requestId 回包) */ +export interface StorageGetPayload { + key: string; +} + +/** storage 读取结果 payload(宿主→游戏:回包 value,无则 null) */ +export interface StorageResultPayload { + key: string; + /** + * 命中的存档值 = 宿主受信边界已解析好的对象(非原始 JSON 字符串);未命中/校验失败为 null。 + * 解析在宿主侧做:runtime 侧零 JSON.parse(守 runtime §5.5「runtime 内不引可抛解析路径」), + * loadState resolve 出对象后 runtime 直接读字段。写入 payload(StorageSetPayload.value)仍是 JSON 字符串。 + * **对象语义不含数组(T1b-β R4-P1)**:宿主回包端(game-studio inject.ts 已落 !Array.isArray 运行时排除) + * 与本类型层须一致——value 虽 TS 标注 Record|null(array 在 TS 是 object 子型不被拒), + * 但**契约语义明令排除数组**:数组/字符串/标量一律按未命中处理(resolve null)。 + * storage 三套形状(本根契约 / game-studio host/contract.ts / inject.ts 应答端)收敛校验须覆盖此「array→null」运行时语义,不止类型签名对齐。 + */ + value: Record | null; +} diff --git a/docs/agent-specs/2026-06-13-T1b-β引擎真接线与Runner-v2-execution.md b/docs/agent-specs/2026-06-13-T1b-β引擎真接线与Runner-v2-execution.md index 824fc30a..bcde0020 100644 --- a/docs/agent-specs/2026-06-13-T1b-β引擎真接线与Runner-v2-execution.md +++ b/docs/agent-specs/2026-06-13-T1b-β引擎真接线与Runner-v2-execution.md @@ -71,7 +71,7 @@ A0 引擎主循环接管(host-dev 桩→**`engineInit` 五回调**〔gameInit/ **Phase A**: - **A0(地基)引擎主循环接管**:host-dev `host.js`(文件头§注明的「RAF+bundle.tick+普通 2D canvas 桩」)改 **`engineInit(gameInit,gameUpdate,gameUpdatePost,gameRender,gameRenderPost,...)` 五回调接管**帧驱动+渲染循环(引擎掌帧、回调里接桩;**无 `engineUpdate` 导出可手摇**)——**这是 particles/physics/audio/time 一切真接线的前提**,不是可选项。 - A1 受控面:random 透传引擎 `RandomGenerator`;time 透传引擎 `time`(依赖 A0);input 改「对引擎轮询态(`mouseWasPressed`/`keyIsDown`)的归一化包装」(订阅形状留给 P4/P10 消费者,底层读引擎态,不另造事件系统);**probe 墙钟独立保留**。 -- A2 particles-juice:粒子→`ParticleEmitter`(包装);hitStop→`timeScale`(包装);**屏震→引擎 `cameraPos`,须同步改 render.js 渲染基座到引擎相机**(否则全 Canvas2D 的 render 不响应 cameraPos;不改则屏震保留受控面偏移量补层)。 +- A2 particles-juice:粒子→经 `ctx.getEngine().particles.spawnEmitter` 调引擎 `ParticleEmitter`(通道包装)。**hitStop/屏震不进透传通道**(P2 grounded 核 impl.js:582-615 定):二者在 impl 里是「受控面输出偏移/timeScale + 游戏层自己应用」闭环,插件不调引擎——hitStop 保插件自管(游戏层读 `getTimeScale()`);**屏震引擎化=render.js〔agent 生成域,非插件〕切引擎 `cameraPos` 相机**(render.js:915 现 `g.translate(shake)`,切相机后由引擎驱动;不切则保受控面偏移补层)。 - A3 audio-music:合成核 zzfxG/zzfxM→引擎 `zzfx`/`zzfxM`(包装);音乐用 **`ZzFXMusic extends Sound`/`Sound.playMusic`**(**引擎无独立 `Music` 类**);**播放/混音层=补层**(受控面 PCM playChannels 与引擎世界音频 `Sound.play` 不兼容,§1.5)。 - A4 collision/gamefeel/physics-lite:按 §7 三分逐函数落地。 - A5 SIZES 重建(引擎首次入 bundle,单口径「引擎+插件」,**非「消双份记账」**——双份在 SIZES 从未成立,R3-P1-2)。 @@ -90,8 +90,8 @@ A0 引擎主循环接管(host-dev 桩→**`engineInit` 五回调**〔gameInit/ | audio-music | zzfxG/zzfxM 合成核 | `zzfxG→Array:2724`/`zzfxM→Array:3730` | ✅ | n/a | ✅ | **合成包装** | | audio-music | playChannels 播放/混音 | `Sound.play(pos,...):2551` 世界定位+range 衰减 | ✗ | ✗ | ✗ | **缺件补层** | | particles-juice | 粒子发射/池 | `ParticleEmitter(pos,...) extends EngineObject:3232` | ✅ | ✗(世界 vs 像素) | ~ | **包装+世界↔像素阻抗层** | -| particles-juice | hitStop | `timeScale:352`/`setTimeScale:638` | ✅ | n/a | ✅ | **包装** | -| particles-juice | 屏震 | `cameraPos:334`/`setCameraPos:626` | ✅ | ~ | ⚠跨层 | **包装(须 render 切引擎相机)/否则补层** | +| particles-juice | hitStop | `timeScale:352`/`setTimeScale:638` | ✅ | n/a | ✅ | **包装(但不进透传通道)**——插件自管 timeScale,游戏层读 `getTimeScale()` 应用,插件不调引擎(P2 核 impl.js:582-593) | +| particles-juice | 屏震 | `cameraPos:334`/`setCameraPos:626` | ✅ | ~ | ⚠跨层 | **render.js 迁移(非插件通道)**——插件出偏移、render.js:915 `g.translate` 应用;引擎化=render.js〔agent 生成域〕切相机,不切则保偏移补层 | | palette-post | postProcess | `PostProcessPlugin(shaderCode):3578` GLSL 可注入 | — | — | ✗(CPU 像素缓冲 vs GPU GLSL) | **整体缺件补层**(引擎仅提供 GLSL 注入位) | | palette-post | RGB↔HSL/Bayer | `Color.setHSLA/HSLA:1431-40`(单色) | ~ | n/a | ✗(单色 vs 缓冲批) | **混合:基元参照,缓冲/索引/抖动补层** | | physics-lite | 抛体/clampSpeed/friction | `EngineObject.updatePhysics:2832`(帧步刚体)/`objectMaxSpeed:469` | ✗ | ✗ | ✗(秒级无状态纯函数 vs 帧步 OOP) | **缺件补层** | @@ -160,4 +160,4 @@ A0 引擎主循环接管(host-dev 桩→**`engineInit` 五回调**〔gameInit/ --- -> R3 复审 5P0/5P1/3P2 全采纳整改 + R4 二轮复审「可冻结」(1P1+3P2 已就地补净) + 待确认 A 默认采纳降级——**✅ 已冻结开工**。**Phase 0 ✅ 完成(`204eaed`:装 littlejsengine@1.18.19+ENGINE-CAPABILITIES.md 50+ 能力真签名+§7 验签填定);§1.5 延迟裁定获验证(4 条初判包装翻缺件补层)。Phase A 解锁**(地基=A0 engineInit 五回调接管);P2 三契约冻结待主会话据签名定(透传白名单/Runner URL 降级/storage 根)。 +> R3 复审 5P0/5P1/3P2 全采纳整改 + R4 二轮复审「可冻结」(1P1+3P2 已就地补净) + 待确认 A 默认采纳降级——**✅ 已冻结开工**。**Phase 0 ✅ 完成(`204eaed`:装 littlejsengine@1.18.19+ENGINE-CAPABILITIES.md 50+ 能力真签名+§7 验签填定);§1.5 延迟裁定获验证(4 条初判包装翻缺件补层)。Phase A 解锁**(地基=A0 engineInit 五回调接管);**P2 三契约 ✅ 冻结(grounded 据真签名+插件真调用面,待对抗复审)**:①受控面引擎能力透传通道 `PluginContext.getEngine()`(白名单 3 项 particles/audio/math,YAGNI 核 impl;引擎 import 只活集成段 host)②GamePackage `manifest.packageUrl`+`immutable`(复用后端 packageUrl 降级 DB-manifest)③SDK `StoragePlugin` 根契约上提(含 R4-P1 array 排除)。 diff --git a/game-runtime/src/core/api.d.ts b/game-runtime/src/core/api.d.ts index 063a8fd2..8bc3e43e 100644 --- a/game-runtime/src/core/api.d.ts +++ b/game-runtime/src/core/api.d.ts @@ -46,6 +46,103 @@ export interface FrameHandle { cancel(): void; } +/* ────────────────────────────────────────────────────────────────────────── + * 受控引擎能力透传面(T1b-β 新增;core-protocol-v0.1) + * 背景:PluginContext 的 6 项抽象(canvas/帧/输入/音频/时间/随机)是「引擎可换」边界, + * 不含粒子发射器 / 合成核 / 引擎数学库。β 真接线需让**特定包装插件**经受控面够到这些 + * 引擎内建能力,故在 PluginContext 上新开一条「引擎能力透传通道」getEngine()。 + * + * 铁律(与 6 项抽象同源): + * 1) 仍封装——绝不暴露 littlejsengine 裸对象 / 裸类型 / 裸 Vector2;几何一律归一化 {x,y}。 + * 2) 引擎 import 只活在集成段 host(背书本通道),不散进各插件 impl.js; + * 插件经 ctx.getEngine().* 够引擎,host-dev 桩与集成段 host 同形(换引擎只换 host)。 + * 3) host-dev 无引擎时整条通道为 null(getEngine() 返 null),插件须容错降级—— + * 对齐 getAudioContext() 的「返 null 不抛、告警一次」范式(见下 PluginContext)。 + * 4) 白名单最小完整(YAGNI,每项指到插件 impl 真有引擎调用点):仅收 particles-juice 发射器 / + * audio-music 合成核 / gamefeel 数学库 3 项。**裁决②-3 的 hitStop/屏震不进通道**——它们在 + * impl 里是「受控面输出偏移+游戏层(render.js)自己应用」的闭环,插件不调引擎;屏震引擎化= + * render.js〔agent 生成域〕切引擎相机,非插件经通道(2026-06-13 P2 grounded 核 impl.js:582-615 定)。 + * ⚠ 通道 = P2 定形状 / Phase A2 接调用点 / 门1② probe 在 A2 后验;audio.synth 是否真用由 + * audio-music lane 在 A2 据 SIZES 实测定(去重 vendored zzfx 收益 vs 降级回退复杂度),不用则清。 + * ────────────────────────────────────────────────────────────────────────── */ + +/** 归一化二维矢量(POJO,非引擎 Vector2 实例)——通道内一切坐标用它,禁裸 Vector2 泄漏。仅本通道用,不回改 InputEvent。 */ +export interface Vec2 { + x: number; + y: number; +} + +/** 受控粒子发射器句柄(封装引擎 ParticleEmitter;插件只见本面,不见引擎对象)。 */ +export interface EngineEmitterHandle { + /** 是否仍在发射(封装引擎 isActive)。 */ + isActive(): boolean; + /** 停止并从引擎对象列表移除(幂等;封装引擎对象销毁)。 */ + stop(): void; +} + +/** 归一化发射器参数(受控面像素 + 中性形态;host 映射到引擎 ParticleEmitter,禁引擎构造签名泄漏)。 */ +export interface EngineEmitterSpec { + /** 发射原点(受控面像素坐标 {x,y})。 */ + pos: Vec2; + /** 发射锥中心方向(弧度)。 */ + angle?: number; + /** 发射尺寸(像素半径,host 换算引擎 emitSize)。 */ + emitSize?: number; + /** 持续时长(秒,0=一次性 burst)。 */ + emitTime?: number; + /** 每秒发射率(continuous 模式)。 */ + emitRate?: number; + /** 单粒子生命(秒)。 */ + particleTime?: number; +} + +/** 受控粒子能力面(封装引擎 ParticleEmitter;坐标=受控面像素,host 内部做像素↔世界阻抗换算)。 */ +export interface EngineParticles { + /** + * 创建一个引擎背书的粒子发射器。坐标/尺寸均为**受控面像素口径**(host 换算到引擎世界坐标)。 + * @param spec 归一化发射参数(中性运动学,无玩法语义;host 映射到引擎 ParticleEmitter 构造)。 + * @returns 发射器句柄。 + */ + spawnEmitter(spec: EngineEmitterSpec): EngineEmitterHandle; +} + +/** 受控合成核能力面(封装引擎 zzfxG/zzfxM;返回 PCM 样本数组,与 vendored 同口径,不播放)。 */ +export interface EngineAudioSynth { + /** + * ZzFX 单音效合成核:参数包 → PCM 单声道样本数组(不播放)。与插件 vendored zzfxG 同口径。 + * @returns PCM 样本数组;无引擎或合成失败返 null(插件回退 vendored)。 + */ + synthSfx(params: number[]): number[] | null; + /** + * ZzFXM 曲谱合成核:乐器/模式/序列/BPM → [L,R] 双声道样本(不播放)。 + * @returns 双声道样本数组;无引擎/失败返 null(插件回退 vendored zzfxM)。 + */ + synthSong(instruments: number[][], patterns: number[][][], sequence: number[], bpm?: number): number[][] | null; +} + +/** 受控引擎数学库能力面(封装引擎 lerp/smoothStep;口径对齐引擎,纯函数恒可用)。 */ +export interface EngineMath { + /** 线性插值(封装引擎 lerp,口径一致)。 */ + lerp(valueA: number, valueB: number, percent: number): number; + /** smoothstep 缓动(封装引擎 smoothStep,口径一致)。 */ + smoothStep(percent: number): number; +} + +/** + * 受控引擎能力透传面(β 新增)。挂在 PluginContext.getEngine() 上。 + * **封装边界**:所有方法/类型均为归一化封装,零 littlejsengine 裸对象/裸类型泄漏。 + * **降级语义**:host-dev/node 无引擎时 getEngine() 返 null,插件须整体容错(粒子退受控面自管、 + * 合成核退 vendored、数学退插件内置)。集成段 host 注入真实引擎背书实现。 + */ +export interface EngineCapabilities { + /** 受控粒子能力(封装 ParticleEmitter)。 */ + particles: EngineParticles; + /** 受控合成核(封装 zzfxG/zzfxM)。A2 据 SIZES 定是否真用,不用则清。 */ + audio: { synth: EngineAudioSynth }; + /** 受控引擎数学库(封装 lerp/smoothStep)。 */ + math: EngineMath; +} + /* ────────────────────────────────────────────────────────────────────────── * 受控输入(Gate0.1)—— 具名消费者 P4 输入缓冲 / P10 t_input_bound * ────────────────────────────────────────────────────────────────────────── */ @@ -126,8 +223,17 @@ export interface PluginContext { * 受控随机源(可注种子)。 * **Gate0.1 起为 per-plugin 派生流**:注册器 init 时给每插件派生独立子随机源 * (主 seed + 插件名 FNV-1a 子种子),插件 A 取随机不再推进插件 B 的序列。 + * **β 集成段实现换引擎背书**:底层由引擎 `RandomGenerator`(种子 xorshift)背书, + * 非 host-dev 的 mulberry32 SeededRandom(裁决②-2;契约形状不变,仅换实现)。 */ random: RandomSource; + /** + * 受控引擎能力透传面(β 新增;具名消费者:particles-juice 发射器 / audio-music 合成核 / gamefeel 数学库)。 + * **host-dev/node 无引擎时返回 null 并 console 告警一次(不抛错)**——对齐 getAudioContext 范式, + * 插件须整体容错降级(粒子退受控面自管 / 合成核退 vendored / 数学退插件内置纯函数)。 + * 集成段=LittleJS 真实句柄背书;**引擎 import 只在集成段 host,不进插件 impl(保引擎可换 + 满足门1 真接线)**。 + */ + getEngine(): EngineCapabilities | null; } /* ────────────────────────────────────────────────────────────────────────── @@ -224,6 +330,12 @@ export interface HostDevContextOptions { * 集成段注入真实工厂(如 () => new AudioContext())。 */ audioFactory?: (() => AudioContext) | null; + /** + * 引擎能力背书工厂(β 新增)。缺省 null → getEngine() 返 null 并告警一次(插件容错降级)。 + * 集成段注入真实工厂(封装 littlejsengine:ParticleEmitter/zzfxG/zzfxM/lerp/smoothStep)。 + * **本工厂是「引擎 import 只活在集成段 host」的唯一落点**——host-dev 桩传 null,集成段 host 传真实背书。 + */ + engineFactory?: (() => EngineCapabilities) | null; } /** createHostDevContext 返回:受控上下文 + host 私有 tick 驱动器 + 帧数读取 + 输入桥 + 每插件派生器。 */