diff --git a/Unity/Assets/Scripts/TH1_Logic/Editor/NetworkStressEditorWindow.cs b/Unity/Assets/Scripts/TH1_Logic/Editor/NetworkStressEditorWindow.cs index 34d25d877..9541488ac 100644 --- a/Unity/Assets/Scripts/TH1_Logic/Editor/NetworkStressEditorWindow.cs +++ b/Unity/Assets/Scripts/TH1_Logic/Editor/NetworkStressEditorWindow.cs @@ -11,7 +11,9 @@ using System.Linq; using System.Text; using Logic.CrashSight; using MemoryPack; +using RuntimeData; using Steamworks; +using TH1_Logic.Core; using TH1_Logic.Net; using TH1_Logic.Steam; using UnityEditor; @@ -988,4 +990,133 @@ namespace Logic.Editor return copy[index]; } } + + public class MapDataNetworkDebugEditorWindow : EditorWindow + { + private Vector2 _scrollPosition; + private string _lastStatus = "等待发送"; + + [MenuItem("Tools/Steam MapData一致性诊断")] + private static void ShowWindow() + { + var window = GetWindow(); + window.titleContent = new GUIContent("MapData诊断"); + window.minSize = new Vector2(520, 300); + window.Show(); + } + + private void OnGUI() + { + var lobby = LobbyManager.Instance.Lobby; + _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); + DrawStatus(lobby); + EditorGUILayout.Space(10); + DrawButtons(lobby); + EditorGUILayout.EndScrollView(); + } + + private void DrawStatus(ILobby lobby) + { + EditorGUILayout.LabelField("Steam MapData 一致性诊断", EditorStyles.boldLabel); + EditorGUILayout.HelpBox("只发送当前 MapData 做对比,不会触发 ForceUpdate,也不会覆盖接收方地图。接收方在 Console 查看 [MapDataDebug] 日志。", MessageType.Info); + + using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) + { + EditorGUILayout.LabelField($"PlayMode: {Application.isPlaying}"); + if (lobby == null) + { + EditorGUILayout.LabelField("Lobby: null"); + EditorGUILayout.LabelField($"最后操作: {_lastStatus}"); + return; + } + + EditorGUILayout.LabelField($"Lobby 初始化: {lobby.IsInitialized()}"); + EditorGUILayout.LabelField($"房间中: {lobby.IsInLobby()}"); + EditorGUILayout.LabelField($"是否房主: {lobby.IsLobbyOwner()}"); + EditorGUILayout.LabelField($"自己: {lobby.GetSelfMemberId()}"); + EditorGUILayout.LabelField($"房主: {lobby.GetLobbyOwnerId()}"); + EditorGUILayout.LabelField($"成员数: {lobby.GetMemberCount()}/{lobby.GetMemberLimit()}"); + + var mapData = Main.MapData; + if (mapData == null) + { + EditorGUILayout.LabelField("MapData: null"); + } + else + { + EditorGUILayout.LabelField($"MapHash: {SafeHash(mapData)}"); + EditorGUILayout.LabelField($"ActionCount: {mapData.Net?.Actions?.Count ?? 0}"); + EditorGUILayout.LabelField($"NetMode: {mapData.Net?.Mode}"); + } + + EditorGUILayout.LabelField($"最后操作: {_lastStatus}", EditorStyles.wordWrappedLabel); + } + } + + private void DrawButtons(ILobby lobby) + { + var canUse = Application.isPlaying + && lobby != null + && lobby.IsInitialized() + && lobby.IsInLobby() + && Main.MapData != null; + var isOwner = lobby != null && lobby.IsLobbyOwner(); + + using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) + { + using (new EditorGUI.DisabledScope(!canUse || isOwner)) + { + if (GUILayout.Button("成员发送当前 MapData 给房主", GUILayout.Height(44))) + SendToHost(); + } + + using (new EditorGUI.DisabledScope(!canUse || !isOwner)) + { + if (GUILayout.Button("房主广播当前 MapData 给所有成员", GUILayout.Height(44))) + BroadcastFromHost(); + } + } + } + + private void SendToHost() + { + try + { + var ok = GameNetSender.Instance.SendMapDataDebugToHost("manual editor member->host"); + _lastStatus = ok ? $"已发送给房主 {DateTime.Now:HH:mm:ss}" : $"发送给房主失败 {DateTime.Now:HH:mm:ss}"; + } + catch (Exception e) + { + _lastStatus = $"发送给房主异常: {e.Message}"; + LogSystem.LogError($"[MapDataDebug] SendToHost failed: {e}"); + } + } + + private void BroadcastFromHost() + { + try + { + var ok = GameNetSender.Instance.BroadcastMapDataDebug("manual editor host->members"); + _lastStatus = ok ? $"已广播给成员 {DateTime.Now:HH:mm:ss}" : $"广播给成员失败 {DateTime.Now:HH:mm:ss}"; + } + catch (Exception e) + { + _lastStatus = $"广播给成员异常: {e.Message}"; + LogSystem.LogError($"[MapDataDebug] BroadcastFromHost failed: {e}"); + } + } + + private static string SafeHash(MapData mapData) + { + if (mapData?.Net == null) return "null"; + try + { + return NetData.GetMapDataHash(mapData); + } + catch (Exception e) + { + return $"hash_error:{e.Message}"; + } + } + } } diff --git a/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetReceiver.cs b/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetReceiver.cs index 16ba49899..9e803de97 100644 --- a/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetReceiver.cs +++ b/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetReceiver.cs @@ -1,5 +1,10 @@ using Logic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; using Logic.Action; +using Logic.AI; using Logic.CrashSight; using RuntimeData; using TH1_Core.Events; @@ -41,6 +46,7 @@ namespace TH1_Logic.Steam if (message.MessageType == P2PMsgType.ChatMessage) OnReceivedChatMessage((ChatMessage)message); if (message.MessageType == P2PMsgType.InviteMessage) OnReceivedInviteMessage((InviteMessage)message); if (message.MessageType == P2PMsgType.LobbyReport) OnReceivedLobbyReport((LobbyReportMessage)message); + if (message.MessageType == P2PMsgType.MapDataDebug) OnReceivedMapDataDebug((MapDataDebugMessage)message); } catch (System.Exception e) { @@ -374,6 +380,140 @@ namespace TH1_Logic.Steam // Main.MapData = message.MapData; } + private void OnReceivedMapDataDebug(MapDataDebugMessage message) + { + if (message == null) + { + LogSystem.LogError($"消息解析失败: OnReceivedMapDataDebug"); + NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.NetworkMessageParseFailed); + return; + } + + var localMap = Main.MapData; + var remoteMap = message.MapData; + var lobby = LobbyManager.Instance.Lobby; + var selfMemberId = lobby?.GetSelfMemberId() ?? 0; + var selfRole = lobby != null && lobby.IsLobbyOwner() ? "房主" : "成员"; + var localHash = SafeGetMapDataHash(localMap); + var remoteHash = string.IsNullOrEmpty(message.MapHash) ? SafeGetMapDataHash(remoteMap) : message.MapHash; + var localActionCount = localMap?.Net?.Actions?.Count ?? 0; + var remoteActionCount = remoteMap?.Net?.Actions?.Count ?? message.ActionIndex; + + if (localMap == null || remoteMap == null) + { + LogSystem.LogError( + $"[MapDataDebug] 无法比较: localMap null={localMap == null}, remoteMap null={remoteMap == null}, " + + $"fromMember={message.SenderMemberId}, selfMember={selfMemberId}"); + return; + } + + var differences = BuildMapDataDebugDifferences(localMap, remoteMap); + var sb = new StringBuilder(8192); + sb.AppendLine($"[MapDataDebug] {selfRole}收到 MapData 诊断包"); + sb.AppendLine($"fromMember={message.SenderMemberId}, fromPlayer={message.SenderPlayerId}, selfMember={selfMemberId}"); + sb.AppendLine($"note={message.Note}"); + sb.AppendLine($"localHash={localHash}, remoteHash={remoteHash}"); + sb.AppendLine($"localActions={localActionCount}, remoteActions={remoteActionCount}"); + if (!IsValidIncomingMultiMap(remoteMap)) + sb.AppendLine("remoteMap=无效多人地图数据"); + + if (differences.Count == 0) + { + sb.AppendLine("result=一致"); + LogSystem.LogInfo(sb.ToString()); + return; + } + + sb.AppendLine($"result=不一致, diffCount={differences.Count}"); + AppendFirstActionMismatch(sb, localMap, remoteMap); + var logCount = Math.Min(differences.Count, 200); + for (var i = 0; i < logCount; i++) + sb.AppendLine($"diff[{i}] {differences[i]}"); + if (differences.Count > logCount) + sb.AppendLine($"diff 过多,仅打印前 {logCount} 条,剩余 {differences.Count - logCount} 条"); + LogSystem.LogError(sb.ToString()); + } + + private static List BuildMapDataDebugDifferences(MapData localMap, MapData remoteMap) + { + var differences = new List(); + try + { + MapData.CompareComponent(localMap.MapConfig, remoteMap.MapConfig, "MapConfig", differences); + MapData.CompareComponent(localMap.GridMap, remoteMap.GridMap, "GridMap", differences); + MapData.CompareComponent(localMap.PlayerMap, remoteMap.PlayerMap, "PlayerMap", differences); + MapData.CompareComponent(localMap.PlayerMap?.PlayerDataList, remoteMap.PlayerMap?.PlayerDataList, + "PlayerMap.PlayerDataList", differences); + MapData.CompareComponent(localMap.CityMap, remoteMap.CityMap, "CityMap", differences); + MapData.CompareComponent(localMap.UnitMap, remoteMap.UnitMap, "UnitMap", differences); + MapData.CompareComponent(localMap.Net, remoteMap.Net, "Net", differences); + MapData.CompareComponent(localMap.CityToPlayerDict, remoteMap.CityToPlayerDict, "CityToPlayerDict", differences); + MapData.CompareComponent(localMap.UnitToCityDict, remoteMap.UnitToCityDict, "UnitToCityDict", differences); + MapData.CompareComponent(localMap.UnitToGridDict, remoteMap.UnitToGridDict, "UnitToGridDict", differences); + MapData.CompareComponent(localMap.CityToGridDict, remoteMap.CityToGridDict, "CityToGridDict", differences); + } + catch (Exception e) + { + differences.Add($"component comparison failed: {e.Message}"); + } + + try + { + differences.AddRange(MapData.FindDifferences(localMap, remoteMap)); + } + catch (Exception e) + { + differences.Add($"FindDifferences failed: {e.Message}"); + } + + return differences.Where(diff => !string.IsNullOrEmpty(diff)).Distinct().ToList(); + } + + private static void AppendFirstActionMismatch(StringBuilder sb, MapData localMap, MapData remoteMap) + { + var localActions = localMap.Net?.Actions; + var remoteActions = remoteMap.Net?.Actions; + if (localActions == null || remoteActions == null) return; + + var minCount = Math.Min(localActions.Count, remoteActions.Count); + for (var i = 0; i < minCount; i++) + { + var localAction = localActions[i]; + var remoteAction = remoteActions[i]; + if (localAction == null && remoteAction == null) continue; + if (localAction != null && localAction.IsEqual(remoteAction)) continue; + + sb.AppendLine($"firstActionMismatchIndex={i}"); + sb.AppendLine($"localAction={GetActionDebugLine(localAction)}"); + sb.AppendLine($"remoteAction={GetActionDebugLine(remoteAction)}"); + return; + } + + if (localActions.Count == remoteActions.Count) return; + sb.AppendLine($"firstActionMismatchIndex={minCount}"); + sb.AppendLine($"localActionCount={localActions.Count}, remoteActionCount={remoteActions.Count}"); + } + + private static string GetActionDebugLine(ActionNetData action) + { + if (action == null) return "null"; + var actionText = action.ActionId == null ? "null" : action.ActionId.GetStringLog().Replace("\n", " | "); + return $"version={action.Version}, mapHash={action.MapHash}, action={actionText}"; + } + + private static string SafeGetMapDataHash(MapData mapData) + { + if (mapData?.Net == null) return "null"; + try + { + return NetData.GetMapDataHash(mapData); + } + catch (Exception e) + { + return $"hash_error:{e.Message}"; + } + } + private bool IsValidIncomingMultiMap(MapData mapData) { return mapData?.Net != null diff --git a/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetSender.cs b/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetSender.cs index f6d495368..8685628c4 100644 --- a/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetSender.cs +++ b/Unity/Assets/Scripts/TH1_Logic/Steam/GameNetSender.cs @@ -178,6 +178,45 @@ namespace TH1_Logic.Steam BroadcastMessage(data); } + // MapData 一致性诊断 (成员 => 房主) + public bool SendMapDataDebugToHost(string note = null) + { + if (!TryGetValidMultiMapForBroadcast("SendMapDataDebugToHost", out var mapData)) return false; + var data = BuildMapDataDebugMessage(mapData, note); + return SendMessage(data); + } + + // MapData 一致性诊断 (房主 => 所有成员) + public bool BroadcastMapDataDebug(string note = null) + { + if (!TryGetValidMultiMapForBroadcast("BroadcastMapDataDebug", out var mapData)) return false; + var data = BuildMapDataDebugMessage(mapData, note); + return BroadcastMessage(data); + } + + // MapData 一致性诊断 (任意成员 => 指定成员) + public bool SendMapDataDebugToPlayer(ulong memberId, string note = null) + { + if (memberId == 0) return false; + if (!TryGetValidMultiMapForBroadcast("SendMapDataDebugToPlayer", out var mapData)) return false; + var data = BuildMapDataDebugMessage(mapData, note); + return SendMessageToPlayer(memberId, data); + } + + private MapDataDebugMessage BuildMapDataDebugMessage(MapData mapData, string note) + { + return new MapDataDebugMessage + { + MapData = mapData, + SenderMemberId = LobbyManager.Instance.Lobby.GetSelfMemberId(), + SenderPlayerId = mapData.PlayerMap?.SelfPlayerId ?? 0, + ActionIndex = mapData.Net?.Actions?.Count ?? 0, + MapHash = NetData.GetMapDataHash(mapData), + Note = note ?? string.Empty, + CreatedUtcTicks = System.DateTime.UtcNow.Ticks + }; + } + private bool TryGetValidMultiMapForBroadcast(string context, out MapData mapData) { mapData = Main.MapData; diff --git a/Unity/Assets/Scripts/TH1_Logic/Steam/SteamObjectSerializer.cs b/Unity/Assets/Scripts/TH1_Logic/Steam/SteamObjectSerializer.cs index a0a52fee0..49d6f286b 100644 --- a/Unity/Assets/Scripts/TH1_Logic/Steam/SteamObjectSerializer.cs +++ b/Unity/Assets/Scripts/TH1_Logic/Steam/SteamObjectSerializer.cs @@ -61,6 +61,8 @@ namespace TH1_Logic.Steam NetworkStress = 17, // 房间举报 LobbyReport = 18, + // MapData 一致性诊断 + MapDataDebug = 19, } public enum NetworkStressMessageKind : byte @@ -92,6 +94,7 @@ namespace TH1_Logic.Steam [MemoryPackUnion(16, typeof(InviteMessage))] [MemoryPackUnion(17, typeof(NetworkStressMessage))] [MemoryPackUnion(18, typeof(LobbyReportMessage))] + [MemoryPackUnion(19, typeof(MapDataDebugMessage))] public abstract partial class BaseMessage { public abstract P2PMsgType MessageType { get; } @@ -157,6 +160,20 @@ namespace TH1_Logic.Steam public override P2PMsgType MessageType => P2PMsgType.ForceUpdate; public MapData MapData { get; set; } } + + + [MemoryPackable] + public partial class MapDataDebugMessage : BaseMessage + { + public override P2PMsgType MessageType => P2PMsgType.MapDataDebug; + public MapData MapData { get; set; } + public ulong SenderMemberId { get; set; } + public uint SenderPlayerId { get; set; } + public int ActionIndex { get; set; } + public string MapHash { get; set; } + public string Note { get; set; } + public long CreatedUtcTicks { get; set; } + } [MemoryPackable]