[P0][CR-B3] 模拟非确定:Math.random、墙钟、DOM 事件直改状态、美术加载决定掉落时序 #523
Loading…
Reference in New Issue
No description provided.
Delete Branch "%!s(<nil>)"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
问题描述
回放、锁步联机,以及"同种子同结果"的测试,都以确定性模拟为前提。但单机模拟里有多处非确定来源:
同样的输入跑两遍,结果可能不同。
证据
随机数:
Math.random(skill-caster.ts:342、skill-caster.ts:347)。Math.random在 arena-only 路径或兜底默认参数里(如 drop-pipeline.ts:1012、drop-pipeline.ts:1465),同样应该清理。DOM 事件直接改模拟:
onPointerDown(mouse-controller.ts:570)直接调用this.castSkill(...)(mouse-controller.ts:599)。既不经过onTick,也不记录为输入帧。渲染决定模拟时序:
stepPlayerAndMonsterAnimations在onTick里运行(act-scene.ts:571)。deathGated,取决于美术是否加载了死亡动画(world-renderer.ts:236)。结果:掉落时机和 RNG 的调用顺序取决于资源加载进度。慢网络、缺图时的结果与正常情况不同。
墙钟进入 tick:
updateBounces(performance.now(), …)(world-renderer.ts:293);Date.now()拼接(ground-items.ts:1372)。状态不在快照里:
let hydraGroupSeqCounter = 0(skill-caster.ts:42)跨引擎实例存活,也不进存档。castRng不在快照里:engine.ts:1178-1182 只保存了loot。跨浏览器:规范只要求
Math.hypot、Math.atan2等函数"实现近似",Chrome 和 Firefox 的结果可能在最低位不同。锁步对局会因此逐渐漂移。根因
模拟没有边界。模拟代码可以直接读墙钟、调随机数、被 DOM 事件调用、被渲染器的回调驱动。这是 #526 的分层问题在时间维度上的表现。
修复指南
{kind:'cast', skillId, target}、{kind:'pickup', id}。onTick开头统一出队并应用。队列里的内容,就是回放和锁步要传输的输入帧。D2Rng实例,按用途分流(loot、castRng等),全部纳入快照。castRng,数值来自 Skills.txt(见 #524)。tick,或tick × 40ms)。performance.now()只允许出现在渲染插值里。hydraGroupSeqCounter这类模块级可变状态移到引擎上,并纳入快照。Math.hypot/Math.atan2的结果直接写进权威状态。no-restricted-properties/no-restricted-globals,在src/game/**、src/scene/backend/**、src/net/**中禁用Math.random、Date.now和performance.now(#537)。castRng,可参考 #519 的哈希改进。验收标准
src/game/**、src/scene/backend/**、src/net/**中没有Math.random、Date.now、performance.now;对应的 ESLint 规则已开启并通过。onTick中生效;回放一段录制好的输入序列,能得到相同的结果。castRng和所有计数器。"存档 → 读档 → 继续运行"的结果,与不读档直接运行的结果一致。npm run typecheck0 error;npx vitest run全部通过。相关
已在提交
112c128(fix(sim): enforce deterministic seeded RNG and tick-aligned simulation (#523))中完成修复,并在5de27f8中补充端到端与 136 关全量验证套件。修复与验证摘要
Math.random()、Date.now()与performance.now(),统一使用实例级种子D2Rng/Rng与 tick 时钟(随GameSnapshot完整序列化),将 DOM 指针输入入队到onTick边界执行,并将怪物死亡掉落 RNG 与异步美术加载解耦,确保相同种子与输入序列逐 tickcomputeStateHash()完全一致。tests/p0-523-determinism.test.tsnpm run build0 错误、14 个 P0/E2E 测试套件(190/190 用例)100% 通过、全仓 Vitest 6,460/6,460 通过、136/136 关无头浏览器巡检通过。