12 KiB
联机 MapData 一致性排查报告(2026-05-22 至 2026-05-28)
范围
- 基准提交:
575b8a288,2026-05-21 18:41:43 +0800,增加单位删除新增的格子保护 - 当前 HEAD:
b2a8019a0,2026-05-28 16:29:21 +0800,merge 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 P0:MapConfig.EnsurePlayerSlots 在反序列化路径读取本机 lobby 并改写 PlayerCount/MultiCivs
相关提交:03c33d637(2026-05-23,bug审查,优化槽位代码)
关键位置:
Unity/Assets/Scripts/TH1_Data/MapData.cs:115:MapConfig.OnAfterMemoryPackDeserializeUnity/Assets/Scripts/TH1_Data/MapData.cs:125:反序列化后直接EnsurePlayerSlots(NetMode.Multi)Unity/Assets/Scripts/TH1_Data/MapData.cs:129:EnsurePlayerSlotsUnity/Assets/Scripts/TH1_Data/MapData.cs:174:GetRequiredPlayerSlotCountUnity/Assets/Scripts/TH1_Data/MapData.cs:177:读取LobbyManager.Instance?.LobbyUnity/Assets/Scripts/TH1_Data/MapData.cs:180:读取本机lobby.GetAllMemberIds()Unity/Assets/Scripts/TH1_Data/MapData.cs:132-133:用本机结果写回targetCount和PlayerCountUnity/Assets/Scripts/TH1_Data/MapData.cs:1989:MapData.OnAfterMemoryPackDeserialize又调用MapConfig.ApplyTeamDiplomacy(this)Unity/Assets/Scripts/TH1_Data/MapData.cs:492-521:ApplyTeamDiplomacy再次EnsurePlayerSlots并写MultiCivs[i].PlayerId
风险说明:
MemoryPack 的 OnAfterMemoryPackDeserialize 应该只做纯数据补默认值、重建缓存、确定性迁移。这里读取了本机 Steam lobby 成员列表,并据此改写 MapConfig.PlayerCount 与 MultiCivs。房主和客户端在进局瞬间的 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/P1:IsResumeArchiveSelected 是 UI/房间状态,但新增为 MapConfig 可序列化字段
相关提交:880e66fcd(2026-05-28,联机新功能)
关键位置:
Unity/Assets/Scripts/TH1_Data/MapData.cs:87:public bool IsResumeArchiveSelected;Unity/Assets/Scripts/TH1_Data/MapData.cs:733:SetResumeArchiveSelectedUnity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMultiplayView.cs:673-700:UI toggle 直接写Main.Instance.MapConfigUnity/Assets/Scripts/TH1_Data/MapData.cs:1872:MapData(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 消息同步,不要放进局内
MapDatahash。 MapData(MapConfig mapCfg, ...)最好深拷贝一份只读局内配置,避免进局后Main.Instance.MapConfig被 UI 改动时污染Main.MapData.MapConfig。
F-003 P1:MomentData.OnAfterMemoryPackDeserialize 会修改已反序列化的 Items
相关提交:a5a0bf51d(2026-05-27,联机功能开发)
关键位置:
Unity/Assets/Scripts/TH1_Logic/Moment/MomentData.cs:46-50:反序列化后调用EnsureMomentItemsUnity/Assets/Scripts/TH1_Logic/Moment/MomentData.cs:56-87:删除旧ExploitWonderMomentItem,再通过assembly.GetTypes()补齐缺失 itemUnity/Assets/Scripts/TH1_Logic/Moment/MomentData.cs:166-172:新增 MemoryPackUnion 59-65
风险说明:
MomentData 是 PlayerData 下的 MemoryPack 数据,会进入 MapData hash。现在每次反序列化都会改 Items 列表:移除旧奇观 moment,并用反射扫描补新类型。只要两端程序集类型枚举顺序、编译裁剪、类型列表存在差异,就可能得到不同 Items 顺序或内容。
即使当前 Windows 同版本通常稳定,这仍然是反序列化时改权威数据的隐患。它更适合做成显式版本迁移,并且只在 host 生成/加载地图后、广播前执行一次。
建议修复:
- 用固定顺序的静态类型表代替
assembly.GetTypes()。 - 给
MomentData增加 schema/version 字段,迁移逻辑只按版本执行。 - 客户端收到
GameStart/ForceUpdate时不做会改变列表内容的“自动补全”,只做 null 防御。
F-004 P1:开局队友首都视野会在 TurnStart action 中写 PlayerData.Sight
相关提交:7a501d15e(2026-05-28,增加开局开队友首都视野)
关键位置:
Unity/Assets/Scripts/TH1_Data/PlayerData.cs:499-502:OnTurnStart中调用UpdateAllTeammateCapitalSightUnity/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,以及所有MultiCivs的Index/MemberId/PlayerId/TeamId。 - 若这条逻辑只需要开局执行,考虑放在 host 地图生成完成、
GameStart广播前,避免每次TurnStart反复补。
F-005 P1/P2:SanaeDivineSkill.BigLucky 仍存在基于本机视野的真实数据改动
相关文件本周被修改:Unity/Assets/Scripts/TH1_Logic/Skill/AllSkill/SanaeDivineSkill.cs
关键位置:
Unity/Assets/Scripts/TH1_Logic/Skill/AllSkill/SanaeDivineSkill.cs:386:同盟单位先执行一次RecoverHealth_LegacyUnity/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_LegacyUnity/Assets/Scripts/TH1_Data/GridData.cs:519-522:InMainSight()依赖Main.MapData.PlayerMap.SelfPlayerData.Sight
风险说明:
这不是“进局即不一致”的首要嫌疑,但它是明确的联机 desync 风险:同一个 action 在不同客户端的“本机视野”可能不同,而这里的真实回血次数会随本机视野分支变化。可见时回血一次,不可见时回血两次。
建议修复:
- 逻辑层固定只回血一次。
InMainSight()只控制 VFX/Renderer/timer,不控制任何MapData数据变更。- 搜索所有
InMainSight()、mapData == Main.MapData、Timer分支,确认分支里没有真实 HP、技能、单位、城市、视野、资源变化。
F-006 P2:SakuyaGuardSkill 改为 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.PlayerCountMapConfig.IsResumeArchiveSelectedMultiCivs.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)
最快验证手段:
- 临时让
GetRequiredPlayerSlotCount只返回传入的targetCount,不读 lobby。 - 临时给
IsResumeArchiveSelected加[MemoryPackIgnore]。 - 若进局不再立刻 hash mismatch,优先按 F-001/F-002 正式修。