增加一致性判断编辑器

This commit is contained in:
wuwenbo 2026-05-29 14:36:05 +08:00
parent 934493a32a
commit a9e469c2bc
4 changed files with 327 additions and 0 deletions

View File

@ -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<MapDataNetworkDebugEditorWindow>();
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}";
}
}
}
}

View File

@ -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<string> BuildMapDataDebugDifferences(MapData localMap, MapData remoteMap)
{
var differences = new List<string>();
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

View File

@ -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;

View File

@ -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]