TH1/MD/NetMapDataConsistencyAudit_2026-05-29.md
2026-05-29 11:30:09 +08:00

12 KiB
Raw Permalink Blame History

联机 MapData 一致性排查报告2026-05-22 至 2026-05-28

范围

  • 基准提交:575b8a2882026-05-21 18:41:43 +0800增加单位删除新增的格子保护
  • 当前 HEADb2a8019a02026-05-28 16:29:21 +0800merge commit
  • 扫描提交数40
  • Unity/Assets/Scripts 净变更90 files changed, 7242 insertions, 893 deletions
  • 重点扫描:MapData/NetData/PlayerData/UnitData、MemoryPack 类型、Steam 联机收发、action 流、结算与技能生命周期

总结

最像“进局后基本立刻不一致”的嫌疑,不是某个普通 action 的执行体,而是 GameStart/ForceUpdate 收到完整 MapData 后,本机反序列化和 NetStartGame 校验阶段会按本机 lobby/UI 状态重写 MapConfig。这会让客户端在执行第一个 TurnStart action 前,NetData.GetMapDataHash(Main.MapData) 已经和房主不同。

优先看 F-001 和 F-002。F-004 会放大 F-001开局队友首都视野是 action 内数据改动,但它依赖 MapConfig.MultiCivs[*].PlayerId/TeamId 一致。

F-001 P0MapConfig.EnsurePlayerSlots 在反序列化路径读取本机 lobby 并改写 PlayerCount/MultiCivs

相关提交:03c33d6372026-05-23bug审查优化槽位代码

关键位置:

  • Unity/Assets/Scripts/TH1_Data/MapData.cs:115MapConfig.OnAfterMemoryPackDeserialize
  • Unity/Assets/Scripts/TH1_Data/MapData.cs:125:反序列化后直接 EnsurePlayerSlots(NetMode.Multi)
  • Unity/Assets/Scripts/TH1_Data/MapData.cs:129EnsurePlayerSlots
  • Unity/Assets/Scripts/TH1_Data/MapData.cs:174GetRequiredPlayerSlotCount
  • Unity/Assets/Scripts/TH1_Data/MapData.cs:177:读取 LobbyManager.Instance?.Lobby
  • Unity/Assets/Scripts/TH1_Data/MapData.cs:180:读取本机 lobby.GetAllMemberIds()
  • Unity/Assets/Scripts/TH1_Data/MapData.cs:132-133:用本机结果写回 targetCountPlayerCount
  • Unity/Assets/Scripts/TH1_Data/MapData.cs:1989MapData.OnAfterMemoryPackDeserialize 又调用 MapConfig.ApplyTeamDiplomacy(this)
  • Unity/Assets/Scripts/TH1_Data/MapData.cs:492-521ApplyTeamDiplomacy 再次 EnsurePlayerSlots 并写 MultiCivs[i].PlayerId

风险说明:

MemoryPack 的 OnAfterMemoryPackDeserialize 应该只做纯数据补默认值、重建缓存、确定性迁移。这里读取了本机 Steam lobby 成员列表,并据此改写 MapConfig.PlayerCountMultiCivs。房主和客户端在进局瞬间的 lobby 可见成员列表、顺序、连接状态可能不完全同步;一旦任何客户端反序列化出的 MultiCivs 比房主多/少一个槽,或 PlayerId/TeamId 绑定不同,第一次 ActionExecute 的 map hash 就会不一致。

这条路径也会影响 ForceUpdate,因为客户端收到房主完整 MapData 后仍会本地反序列化并再次走这些后处理。

建议修复:

  • EnsurePlayerSlots 拆成两套:
    • EnsureSerializedPlayerSlots:纯数据、只按已序列化的 PlayerCount/MultiCivs 修 null、修 index不读取 lobby、不改变 PlayerCount
    • EnsureLobbyPlayerSlots:只允许在房间阶段的 Main.Instance.MapConfig 上、由 host 权威调用。
  • MapConfig.OnAfterMemoryPackDeserialize 不要读取 LobbyManager,也不要按本机 lobby 扩容/裁剪槽位。
  • MapData.OnAfterMemoryPackDeserialize 中的 ApplyTeamDiplomacy 不应再次触发 lobby 相关槽位修复。
  • 对收到的 GameStart/ForceUpdate,如果槽位和 Net.Players 不一致,应校验失败并提示,而不是本地修。

F-002 P0/P1IsResumeArchiveSelected 是 UI/房间状态,但新增为 MapConfig 可序列化字段

相关提交:880e66fcd2026-05-28联机新功能

关键位置:

  • Unity/Assets/Scripts/TH1_Data/MapData.cs:87public bool IsResumeArchiveSelected;
  • Unity/Assets/Scripts/TH1_Data/MapData.cs:733SetResumeArchiveSelected
  • Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMultiplayView.cs:673-700UI toggle 直接写 Main.Instance.MapConfig
  • Unity/Assets/Scripts/TH1_Data/MapData.cs:1872MapData(MapConfig mapCfg, ...) 直接保存 MapConfig = mapCfg,不是拷贝

风险说明:

IsResumeArchiveSelected 看语义是“房主当前是否选择继续存档”的 UI/房间状态,不是局内权威 gameplay 状态。但它没有 [MemoryPackIgnore],所以会进入 MapData 的 MemoryPack 序列化和 hash。

更危险的是,房主 new MapData(MapConfig, NetMode.Multi) 时直接复用 Main.Instance.MapConfig 对象引用。进局后如果外部房间 UI、lobby 回调或残留 timer 再写 Main.Instance.MapConfig.SetResumeArchiveSelected(...),房主的 Main.MapData.MapConfig 也会被 action 外修改,而客户端不会同步同样修改。

建议修复:

  • IsResumeArchiveSelected[MemoryPackIgnore],或移出 MapConfig,放到独立房间 UI state。
  • 如果它必须参与开始/继续流程,用 host-authoritative lobby 消息同步,不要放进局内 MapData hash。
  • MapData(MapConfig mapCfg, ...) 最好深拷贝一份只读局内配置,避免进局后 Main.Instance.MapConfig 被 UI 改动时污染 Main.MapData.MapConfig

F-003 P1MomentData.OnAfterMemoryPackDeserialize 会修改已反序列化的 Items

相关提交:a5a0bf51d2026-05-27联机功能开发

关键位置:

  • Unity/Assets/Scripts/TH1_Logic/Moment/MomentData.cs:46-50:反序列化后调用 EnsureMomentItems
  • Unity/Assets/Scripts/TH1_Logic/Moment/MomentData.cs:56-87:删除旧 ExploitWonderMomentItem,再通过 assembly.GetTypes() 补齐缺失 item
  • Unity/Assets/Scripts/TH1_Logic/Moment/MomentData.cs:166-172:新增 MemoryPackUnion 59-65

风险说明:

MomentDataPlayerData 下的 MemoryPack 数据,会进入 MapData hash。现在每次反序列化都会改 Items 列表:移除旧奇观 moment并用反射扫描补新类型。只要两端程序集类型枚举顺序、编译裁剪、类型列表存在差异就可能得到不同 Items 顺序或内容。

即使当前 Windows 同版本通常稳定,这仍然是反序列化时改权威数据的隐患。它更适合做成显式版本迁移,并且只在 host 生成/加载地图后、广播前执行一次。

建议修复:

  • 用固定顺序的静态类型表代替 assembly.GetTypes()
  • MomentData 增加 schema/version 字段,迁移逻辑只按版本执行。
  • 客户端收到 GameStart/ForceUpdate 时不做会改变列表内容的“自动补全”,只做 null 防御。

F-004 P1开局队友首都视野会在 TurnStart action 中写 PlayerData.Sight

相关提交:7a501d15e2026-05-28增加开局开队友首都视野

关键位置:

  • Unity/Assets/Scripts/TH1_Data/PlayerData.cs:499-502OnTurnStart 中调用 UpdateAllTeammateCapitalSight
  • Unity/Assets/Scripts/TH1_Logic/Player/PlayerLogic.cs:1618-1639:遍历所有玩家,为队友首都补视野
  • Unity/Assets/Scripts/TH1_Logic/Player/PlayerLogic.cs:1642-1650:队友判断依赖 MapConfig.ArePlayersInSameTeam 和外交 IsTeammate

风险说明:

这条数据修改本身是在 TurnStart action 里,按 action 流程执行,不是直接绕过 action。但它强依赖 MapConfig.MultiCivs[*].PlayerId/TeamId 与外交标记一致。若 F-001 导致任意客户端槽位绑定不同,第一回合开始时玩家视野会立刻不同。

建议修复:

  • 先修 F-001保证 MultiCivs 反序列化不依赖本机 lobby。
  • 给首个 TurnStart 前后加临时日志:每个玩家的 SightGidSet.Count,以及所有 MultiCivsIndex/MemberId/PlayerId/TeamId
  • 若这条逻辑只需要开局执行,考虑放在 host 地图生成完成、GameStart 广播前,避免每次 TurnStart 反复补。

F-005 P1/P2SanaeDivineSkill.BigLucky 仍存在基于本机视野的真实数据改动

相关文件本周被修改:Unity/Assets/Scripts/TH1_Logic/Skill/AllSkill/SanaeDivineSkill.cs

关键位置:

  • Unity/Assets/Scripts/TH1_Logic/Skill/AllSkill/SanaeDivineSkill.cs:386:同盟单位先执行一次 RecoverHealth_Legacy
  • Unity/Assets/Scripts/TH1_Logic/Skill/AllSkill/SanaeDivineSkill.cs:388:判断 unit.Grid(map)?.InMainSight()
  • Unity/Assets/Scripts/TH1_Logic/Skill/AllSkill/SanaeDivineSkill.cs:399-400:不在本机视野时又执行一次 RecoverHealth_Legacy
  • Unity/Assets/Scripts/TH1_Data/GridData.cs:519-522InMainSight() 依赖 Main.MapData.PlayerMap.SelfPlayerData.Sight

风险说明:

这不是“进局即不一致”的首要嫌疑,但它是明确的联机 desync 风险:同一个 action 在不同客户端的“本机视野”可能不同,而这里的真实回血次数会随本机视野分支变化。可见时回血一次,不可见时回血两次。

建议修复:

  • 逻辑层固定只回血一次。
  • InMainSight() 只控制 VFX/Renderer/timer不控制任何 MapData 数据变更。
  • 搜索所有 InMainSight()mapData == Main.MapDataTimer 分支,确认分支里没有真实 HP、技能、单位、城市、视野、资源变化。

F-006 P2SakuyaGuardSkill 改为 OnDamaged 中嵌套 DamageSettlement

相关文件本周被修改:Unity/Assets/Scripts/TH1_Logic/Skill/AllSkill/SakuyaGuardSkill.cs

关键位置:

  • Unity/Assets/Scripts/TH1_Logic/Skill/AllSkill/SakuyaGuardSkill.cs:49-55:新增“双结算模型”
  • Unity/Assets/Scripts/TH1_Logic/Skill/AllSkill/SakuyaGuardSkill.cs:54:在受击生命周期内调用 Main.UnitLogic.DamageSettlement(...)
  • Unity/Assets/Scripts/TH1_Logic/Skill/AllSkill/SakuyaGuardSkill.cs:55:原目标 DamageValue = 0

风险说明:

这条目前看仍由 action 触发,且没有看到本机随机/时间直接影响数据。但嵌套 DamageSettlement 会触发更多技能、Moment、HeroTask、外交、死亡流程复杂度高。若其中任一生命周期有本机视野/renderer/timer 条件控制真实数据,就会被放大。

建议修复:

  • 保证嵌套结算只在逻辑阶段同步执行timer 里只做表现。
  • 为 SakuyaGuard 场景加一次 host/client 双端 action hash 对照测试。

MemoryPack/序列化变更清单

类型 变更 风险
MapConfig 新增 IsResumeArchiveSelected UI 状态进入 MapData hash建议忽略或移出
MapConfig EnsurePlayerSlots 变为可读取本机 lobby 反序列化后可产生不同 PlayerCount/MultiCivs
MatchSettlementType Domination/Perfect/Creative 合并为 Normal,新增 Normalize 同版本通常确定;旧值迁移需保证只按纯数据执行
MomentItemBase 新增 union 59-65 类型表扩展本身可行,但反序列化补项方式有风险
MomentData OnAfterMemoryPackDeserialize 自动删/补 items 会在客户端收到完整 MapData 后改权威数据
LobbyReportMessage 新增 Steam 消息 union 18 不属于 MapData非本次 hash 首要嫌疑

建议排查日志

为了最快确认 F-001/F-002建议在三处临时打印同一组字段

  • 房主 GameNetSender.GameStart() 序列化前
  • 客户端 OnReceivedGameStart 刚收到后、NetStartGame
  • 客户端 ValidateNetworkStartMap/RefreshPlayerNet 后、第一条 ActionExecute

字段:

  • NetData.GetMapDataHash(map)
  • MapConfig.PlayerCount
  • MapConfig.IsResumeArchiveSelected
  • MultiCivs.Count
  • 每个 slot 的 Index/MemberId/PlayerId/CivId/ForceId/TeamId/IsAI/IsReady/IsCivFixed
  • Net.Players
  • 每个玩家 Id/PlayerCivId/PlayerForceId/Sight.Count
  • 每个玩家 MomentData.Items.Select(type/name/isExecute/executeCount/playerCultureValue)

最快验证手段:

  1. 临时让 GetRequiredPlayerSlotCount 只返回传入的 targetCount,不读 lobby。
  2. 临时给 IsResumeArchiveSelected[MemoryPackIgnore]
  3. 若进局不再立刻 hash mismatch优先按 F-001/F-002 正式修。