MassBattleDesyncAutoDebugger.h
Source/MassBattle/Public/Debug/MassBattleDesyncAutoDebugger.h
概述 Overview
锁步失步自动取证工具(2026-09-12 引入),替代低效的「插桩 → 运行 → 缩小范围 → 再插桩」循环,改为「全量转储再比对」流程:① 首个发散 tick 当帧,模拟先暂停(Net->bSimulationPaused),赶在 resync 机制覆盖现场之前冻结证据;② 双端各自把完整世界状态转储到 Saved/MassBattleDesync/dump_<role>_t<tick>.mbdmp;③ 用 Tools/desync_dump_diff.py 离线逐字段比对两份转储,打印所有差异值;④ [实验性] 种子化输入模糊器从 INI 种子复现一段确定性的「人类」会话(MoveTo / ChaseAttack / 编队移动随机批次),使发现的任何发散都可逐位复现。实现为全静态成员的普通 C++ struct——无实例、无反射类型,配置经 UMassBattleSubsystem 的 Config 属性一次性载入。
Lockstep desync auto-forensics tool (introduced 2026-09-12). It replaces the slow probe → run → narrow → probe instrumentation loop with a dump-everything-then-diff flow: ① on the FIRST divergent tick the sim pauses first (Net->bSimulationPaused), freezing the evidence before resync machinery can overwrite it; ② both peers dump FULL world state to Saved/MassBattleDesync/dump_<role>_t<tick>.mbdmp; ③ Tools/desync_dump_diff.py diffs the two dumps offline and prints every differing value; ④ [experimental] a seeded input fuzzer replays a deterministic "human" session (MoveTo / ChaseAttack / formation-move batches at random intervals) from an INI seed so any found divergence is reproducible bit-for-bit. Implemented as a plain C++ struct with all-static members — no instances, no reflected type; config is loaded once from UMassBattleSubsystem Config properties.
公开方法 Public Methods
发散回调,由双端各自的失步检测点调用(客户端的延迟校验 / 服务端的回执哈希失配,见管线角色)。流程:① LoadConfigOnce 首次调用时从 UMassBattleSubsystem 读取 Config 属性并打日志;② 按世界名去重(函数内 static TSet<FName>)——PIE 双 World 共进程时,进程级静态闸门会挡掉另一端的转储,按世界名键控既保留「每世界每会话只转储一次」的防刷闸,又兼容 PIE(PIE 世界名按实例唯一);③ 置 Net->bSimulationPaused = true 冻结现场;④ 调 DumpWorldState 全量转储;⑤ 打 Error 日志(含转储路径)。任一指针无效或该世界已转储过则直接返回,不暂停。
Divergence callback, invoked from both peers' divergence-detection sites (the client-side deferred check / the server-side acked-hash mismatch — see Pipeline Role). Flow: ① LoadConfigOnce reads the Config properties from UMassBattleSubsystem on first call and logs them; ② deduplicates per world name (a function-local static TSet<FName>) — with two PIE worlds in one editor process a process-static gate would block the other peer's dump; keying by world name keeps the "one dump per session per world" spam guard while remaining PIE-safe (PIE world names are unique per instance); ③ sets Net->bSimulationPaused = true to freeze the scene; ④ dumps full state via DumpWorldState; ⑤ logs an Error with the dump path. Returns immediately (no pause) if either pointer is invalid or this world was already dumped.
Input
| 参数 Param | 类型 Type | 说明 Description |
|---|---|---|
MB | UMassBattleSubsystem* | MassBattle 子系统,提供实体枚举与 Mass API 访问;无效时本函数直接返回。MassBattle subsystem providing entity enumeration and Mass API access; the function returns early when invalid. |
Net | UMassBattleNetworkSubsystem* | 网络子系统,其 bSimulationPaused 被置 true 以冻结模拟。Network subsystem whose bSimulationPaused is set true to freeze the simulation. |
Tick | int32 | 检测到发散的逻辑 tick 号(写入转储文件名与日志)。Logic tick at which divergence was detected (written into the dump filename and logs). |
Role | const FString& | 端角色字符串("client" / "server"),用于双端转储文件命名区分。Peer role string ("client" / "server"), distinguishing the two dump files. |
Output
无返回值。副作用:向世界名集合登记、置 bSimulationPaused = true(模拟从此暂停)、写出一份 .mbdmp 全量转储文件、打一条 Error 日志。开关 bEnabled 为 false 时无任何副作用。
No return value. Side effects: registers the world name, sets bSimulationPaused = true (the sim stays paused), writes one full .mbdmp dump file, logs one Error. With bEnabled false there are no side effects at all.
完整世界转储。文件头 MBDMP v1 tick=… role=…;实体来自 MB->DiagUIDEnumerateForDump() 并按 UID 升序排序;每个实体先输出 FEntityFlagFragment 旗标(低/高 32 位十六进制),再对 14 个热 fragment 做反射全量转储:FMoving、FNavigating、FLocating、FRotating、FTracing、FBehaving、FAttacking、FDeterminism、FMove、FChase、FCollider、FAttack、FAvoiding。属性序列化规则:对象/弱引用/软引用/委托/接口/Class 属性一律剥离;浮点以位模式十六进制(%016llx)输出以暴露最低位差异;整数/枚举输出十进制;名称/字符串原样;数组先记 LEN(真实长度)再最多转储 32 个元素;嵌套结构递归(深度上限 6);实体句柄类(EntityHandle / MassEntityHandle / FlowFieldLayerHandle)跳过。最后 TASKS 段反射转储每个活跃的 MoveTo 任务对象(MB->GetActiveMoveToTasks())的全部属性。整串以 UTF-8(无 BOM)写盘。
Full world dump. Header MBDMP v1 tick=… role=…; entities come from MB->DiagUIDEnumerateForDump() sorted ascending by UID; each entity first emits its FEntityFlagFragment flags (low/high 32-bit hex), then a full reflection dump of 14 hot fragments: FMoving, FNavigating, FLocating, FRotating, FTracing, FBehaving, FAttacking, FDeterminism, FMove, FChase, FCollider, FAttack, FAvoiding. Property serialization rules: object/weak/soft/delegate/interface/class properties are stripped; floats print as bit-pattern hex (%016llx) to expose last-bit differences; ints/enums print decimal; names/strings verbatim; arrays record LEN (true length) then dump up to 32 elements; nested structs recurse (depth cap 6); handle-typed structs (EntityHandle / MassEntityHandle / FlowFieldLayerHandle) are skipped. A final TASKS section reflects every active MoveTo task object (MB->GetActiveMoveToTasks()). The whole string is written UTF-8 without BOM.
Input
| 参数 Param | 类型 Type | 说明 Description |
|---|---|---|
MB | UMassBattleSubsystem* | MassBattle 子系统,提供实体枚举、Mass API(GetMassAPISubsystem)与活跃任务列表。MassBattle subsystem providing entity enumeration, the Mass API (GetMassAPISubsystem) and the active-task list. |
Tick | int32 | 转储对应的逻辑 tick 号(写入文件名与文件头)。Logic tick the dump belongs to (filename and header). |
Role | const FString& | 端角色字符串,拼入文件名 dump_<role>_t<tick>.mbdmp。Peer role string used in the filename dump_<role>_t<tick>.mbdmp. |
Output
返回输出文件路径(Saved/MassBattleDesync/ 下,目录按需递归创建)。副作用:写盘一份转储文件;无效句柄实体以 <INVALID-HANDLE> 标记;不修改任何模拟状态。
Returns the output file path (under Saved/MassBattleDesync/, directory created recursively as needed). Side effect: one dump file written to disk; entities with invalid handles are marked <INVALID-HANDLE>; no simulation state is modified.
模糊器的确定性 LCG(线性同余发生器):FuzzerRngState = FuzzerRngState * 1664525u + 1013904223u,返回状态右移 8 位。同种子必得同序列——这是「任何发现的发散可逐位复现」的基础。
The fuzzer's deterministic LCG (linear congruential generator): FuzzerRngState = FuzzerRngState * 1664525u + 1013904223u, returning the state shifted right by 8. Same seed ⇒ same sequence — the foundation of "any found divergence is reproducible bit-for-bit".
Input
无参数。
No parameters.
Output
返回一个伪随机 32 位值(高 24 位有效)。副作用:推进 FuzzerRngState。
Returns one pseudo-random 32-bit value (top 24 bits). Side effect: advances FuzzerRngState.
模糊器逐 tick 推进,仅服务器端调用(命令经正常锁步通道复制到客户端)。首次调用为初始化:置 bFuzzerInitialized、以 FuzzerSeed | 1 播种 LCG、下次触发定在当前 tick + 60、打布防日志。到达触发 tick 时:① 在 [Min, Max] 区间抽下次触发间隔;② 经 DiagUIDEnumerateForDump 枚举实体(不足 4 个则本 tick 放弃);③ 在 [MinBatch, MaxBatch] 抽批量、以 FuzzerRand 无放回抽取有效实体;④ 抽动作(0=MoveTo / 1=Chase / 2=Formation)与是否可打断;⑤ 构造 FMassBattleNetCommand(CommandTag = "MB.FuzzTask",IssuedTick = T,ExecuteTick = T + 2,IntPayload = [action, canInterrupt, targetUid, n, uid1..uidN],FloatPayload = [X, Y]——MoveTo/Formation 为 [-10000, 10000) 随机坐标、Chase 为 0),经 AMassBattleNetworkGameModeBase::EnqueueAuthorityCommandForFuzz 入队。走锁步命令通道的原因:任务工厂仅可在 ExecuteNetCommand 内合法激活。
Per-tick fuzzer advance, called on the SERVER only (commands replicate to clients through the normal lockstep path). First call initializes: sets bFuzzerInitialized, seeds the LCG with FuzzerSeed | 1, schedules the first fire at current tick + 60, and logs the arming. On a fire tick: ① draws the next interval within [Min, Max]; ② enumerates entities via DiagUIDEnumerateForDump (fewer than 4 ⇒ skip this tick); ③ draws a batch size within [MinBatch, MaxBatch] and picks valid entities without replacement via FuzzerRand; ④ draws the action (0=MoveTo / 1=Chase / 2=Formation) and the interrupt flag; ⑤ builds an FMassBattleNetCommand (CommandTag = "MB.FuzzTask", IssuedTick = T, ExecuteTick = T + 2, IntPayload = [action, canInterrupt, targetUid, n, uid1..uidN], FloatPayload = [X, Y] — random coordinates in [-10000, 10000) for MoveTo/Formation, 0 for Chase) and enqueues it via AMassBattleNetworkGameModeBase::EnqueueAuthorityCommandForFuzz. The lockstep-command routing exists because task factories may only activate legally inside ExecuteNetCommand.
Input
| 参数 Param | 类型 Type | 说明 Description |
|---|---|---|
MB | UMassBattleSubsystem* | MassBattle 子系统,提供 tick 计数(GetTickCount)、实体枚举与 UID 反查;无效时直接返回。MassBattle subsystem providing the tick count (GetTickCount), entity enumeration and UID lookup; returns early when invalid. |
Output
无返回值。副作用:bFuzzerEnabled 关闭时无操作;开启时推进模糊器状态,可能向权威命令队列压入一条 MB.FuzzTask 锁步命令并打一条 Warning 日志。多次 LCG 消耗(状态推进)。
No return value. Side effects: no-op when bFuzzerEnabled is off; otherwise advances the fuzzer state and may push one MB.FuzzTask lockstep command into the authority queue plus a Warning log. Consumes several LCG draws (state advances).
在锁步门内解码并激活一条 "MB.FuzzTask" 命令——由 ExecuteNetCommand 在双端调用,保证双端同构执行(任务工厂因此可合法激活)。先做载荷长度校验(IntPayload.Num() >= 4 + N,不足直接返回);经 UMassBattleSubsystem::GetPtr 取子系统,按 UID 反查并过滤有效实体(空批次返回)。分发:action 1 → UMassBattleBPTaskAgentsChaseAttack::AgentsChaseAttack(目标为 TargetUid 实体);action 0 → 单点 AgentsMoveTo(目标 (X, Y, 0));action 2 → 编队网格 AgentsMoveTo(列数 = √N,间距 150cm,自 (X, Y) 起排布)。
Decodes and activates one "MB.FuzzTask" command inside the lockstep gate — called from ExecuteNetCommand on both peers so both execute identically (task factories thereby activate legally). Validates payload length first (IntPayload.Num() >= 4 + N, early return otherwise); resolves the subsystem via UMassBattleSubsystem::GetPtr, maps UIDs back to valid entities (empty pick returns). Dispatch: action 1 → UMassBattleBPTaskAgentsChaseAttack::AgentsChaseAttack (target = the TargetUid entity); action 0 → single-point AgentsMoveTo (goal (X, Y, 0)); action 2 → formation-grid AgentsMoveTo (columns = √N, 150 cm spacing from (X, Y)).
Input
| 参数 Param | 类型 Type | 说明 Description |
|---|---|---|
WorldContext | UObject* | 世界上下文,用于解析 UMassBattleSubsystem 并传给任务工厂。World context used to resolve UMassBattleSubsystem and passed to the task factories. |
IntPayload | const TArray<int32>& | [action(0=MoveTo,1=Chase,2=Formation), canInterrupt, targetUid, n, uid1..uidN]。[action (0=MoveTo, 1=Chase, 2=Formation), canInterrupt, targetUid, n, uid1..uidN]. |
FloatPayload | const TArray<float>& | [X, Y] 目标坐标(厘米);Chase 动作不使用。[X, Y] goal coordinates (cm); unused by the Chase action. |
Output
无返回值。副作用:激活一个 BP 任务(MoveTo 或 ChaseAttack),改写被选中实体的行为状态;载荷非法或批次为空时无副作用。
No return value. Side effect: activates one BP task (MoveTo or ChaseAttack), redirecting the selected entities' behavior; no side effects on malformed payload or empty pick.
静态成员变量 Members
| 名称 Name | 类型 Type | 默认值 | 说明 Description |
|---|---|---|---|
bEnabled | bool | false | 总开关(发散时转储+暂停)。由 Config 属性 bDesyncAutoDump 首次载入。Master switch (dump + pause on divergence). Loaded once from the Config property bDesyncAutoDump. |
bDumpedThisSession | bool | false | [已被取代 2026-09-12] 转储闸门改为按世界键控(OnDivergence 内的 TSet)——进程级静态闸门曾在 PIE 中挡掉第二个 peer 的转储。成员保留但不再作为闸门。[superseded 2026-09-12] the dump gate is now per-WORLD (the TSet inside OnDivergence) — a process-static gate blocked the 2nd peer's dump in PIE. The member remains but no longer gates anything. |
bFuzzerEnabled | bool | false | 模糊器开关,来自 Config 属性 bDesyncAutoFuzzer。Fuzzer switch, from the Config property bDesyncAutoFuzzer. |
FuzzerSeed | int32 | 20260912 | 模糊器种子(DesyncFuzzerSeed);同种子同会话,发散可复现。Fuzzer seed (DesyncFuzzerSeed); same seed ⇒ same session, divergences reproducible. |
FuzzerMinInterval / FuzzerMaxInterval | int32 | 40 / 160 | 两次任务批次之间的随机 tick 间隔上下界(DesyncFuzzerMinInterval/MaxInterval)。Random tick-interval bounds between task batches (DesyncFuzzerMinInterval/MaxInterval). |
FuzzerMinBatch / FuzzerMaxBatch | int32 | 8 / 120 | 每批次随机实体数上下界(DesyncFuzzerMinBatch/MaxBatch)。Random per-batch entity-count bounds (DesyncFuzzerMinBatch/MaxBatch). |
FuzzerRngState | uint32 | 1 | LCG 当前状态;初始化时被覆写为 Seed | 1。Current LCG state; overwritten with Seed | 1 at initialization. |
FuzzerNextFireTick | int32 | -1 | 下次触发模糊命令的 tick(初始化时 = 当前 tick + 60)。Next tick to fire a fuzz command (= current tick + 60 at initialization). |
bFuzzerInitialized | bool | false | 模糊器是否已初始化(首次 TickFuzzer 完成播种)。Whether the fuzzer has initialized (first TickFuzzer seeds it). |
管线角色 Pipeline Role
本类型不在模拟管线中运行,而是挂在锁步失步检测点与服务器 tick 收尾上(以下调用点均在发布树 grep 验证):AMassBattleNetworkGameStateBase 的延迟失步校验触发 [DIVERGE_TICK] 时以角色 "client" 调用 OnDivergence;AMassBattleNetworkGameModeBase 的回执哈希失配在确认窗开始时(streak 起点,服务端早触发——客户端暂停后不再回执,确认窗永不会闭合)与确认满时各以 "server" 调用一次 OnDivergence。UMassBattleNetworkProcessor 的 Net_Fold(逐 tick 网络收尾)在非客户端(NetMode ≠ NM_Client)每 tick 调用 TickFuzzer。模糊命令经权威命令队列复制,最终由 AMassBattleNetworkGameModeBase::ExecuteNetCommand 在锁步门内按 CommandTag == "MB.FuzzTask" 分发给 ExecuteFuzzCommand(双端各执行一次,同构)。配置项位于 DefaultMassBattle.ini 的 [/Script/MassBattle.MassBattleSubsystem] 段(bDesyncAutoDump / bDesyncAutoFuzzer / DesyncFuzzerSeed / 间隔与批量),由 LoadConfigOnce 一次性读入。
This type does not run inside the sim pipeline; it hooks the lockstep divergence-detection sites and the server tick fold (all call sites below grep-verified in the release tree): the client-side deferred desync check in AMassBattleNetworkGameStateBase invokes OnDivergence with role "client" when [DIVERGE_TICK] fires; the acked-hash mismatch in AMassBattleNetworkGameModeBase invokes OnDivergence with "server" both at the start of the confirm window (streak start — the early server dump; a paused client stops acking so the window would never close) and when the streak confirms. The Net_Fold of UMassBattleNetworkProcessor (per-tick network close-out) calls TickFuzzer every tick on non-clients (NetMode ≠ NM_Client). Fuzz commands replicate through the authority command queue and are finally dispatched by AMassBattleNetworkGameModeBase::ExecuteNetCommand inside the lockstep gate on CommandTag == "MB.FuzzTask" into ExecuteFuzzCommand (executed once per peer, identically). Config lives in DefaultMassBattle.ini under [/Script/MassBattle.MassBattleSubsystem] (bDesyncAutoDump / bDesyncAutoFuzzer / DesyncFuzzerSeed / interval & batch bounds), read once by LoadConfigOnce.
补充说明 Notes
离线比对脚本 Tools/desync_dump_diff.py 随发布树提供。头文件注释中的配置段名写的是 [/Script/MassBattle.MassBattleDesyncAutoDebugger],实际生效的是 [/Script/MassBattle.MassBattleSubsystem](属性宿主是子系统,见 Subsystems/MassBattleSubsystem.h)。转储的浮点十六进制位模式专为暴露末位差异而设;数组元素上限 32、真实长度单列,保证大数组差异可见又不失控。模糊器为实验性功能。
The offline diff script Tools/desync_dump_diff.py ships with the release tree. The header comment names the config section [/Script/MassBattle.MassBattleDesyncAutoDebugger]; the section that actually takes effect is [/Script/MassBattle.MassBattleSubsystem] (the properties live on the subsystem — see Subsystems/MassBattleSubsystem.h). Floats dump as bit-pattern hex specifically to expose last-bit differences; arrays cap at 32 elements with the true length recorded separately, keeping large-array diffs visible yet bounded. The fuzzer is experimental.