diff --git a/ProjectNLD/Assets/Editor/Tools/NetDebugWindow.cs b/ProjectNLD/Assets/Editor/Tools/NetDebugWindow.cs new file mode 100644 index 00000000000..5ff7415a0e1 --- /dev/null +++ b/ProjectNLD/Assets/Editor/Tools/NetDebugWindow.cs @@ -0,0 +1,706 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Gameplay.Net; +using Google.Protobuf; +using Google.Protobuf.Reflection; +using NLDMID; +using UnityEditor; +using UnityEngine; + +public class NetDebugWindow : EditorWindow +{ + [MenuItem("Tools/网络协议调试")] + public static void ShowWindow() + { + var window = GetWindow("网络协议调试"); + window.minSize = new Vector2(400f, 500f); + window.Show(); + } + + private List _clientMsgIds; + private List _clientMsgNames; + private List _filteredNames; + private List _filteredIds; + private int _selectedIndex = 0; + private string _searchText = ""; + private Vector2 _fieldScrollPos; + private Vector2 _logScrollPos; + private List _logEntries = new List(); + private Dictionary _foldoutStates = new Dictionary(); + private Dictionary _mPBPropertyCache = new Dictionary(); + private Dictionary _mPBBackingFieldCache = new Dictionary(); + private MSGID _lastSelectedId = MSGID.None; + private bool _initialized; + + private void _Init() + { + if (_initialized) return; + _initialized = true; + + _clientMsgIds = new List(); + _clientMsgNames = new List(); + + foreach (MSGID id in Enum.GetValues(typeof(MSGID))) + { + string name = id.ToString(); + if (name.StartsWith("Clt")) + { + _clientMsgIds.Add(id); + _clientMsgNames.Add($"{name} ({(int)id})"); + } + } + + _filteredIds = new List(_clientMsgIds); + _filteredNames = new List(_clientMsgNames); + } + + private PropertyInfo _GetmPBProperty(Type msgType) + { + if (!_mPBPropertyCache.TryGetValue(msgType, out PropertyInfo prop)) + { + prop = msgType.GetProperty("mPB", BindingFlags.Public | BindingFlags.Instance); + _mPBPropertyCache[msgType] = prop; + } + return prop; + } + + private FieldInfo _GetmPBBackingField(Type msgType) + { + if (!_mPBBackingFieldCache.TryGetValue(msgType, out FieldInfo field)) + { + field = msgType.GetField("m_PB", BindingFlags.NonPublic | BindingFlags.Instance); + _mPBBackingFieldCache[msgType] = field; + } + return field; + } + + private void OnGUI() + { + _Init(); + + bool isPlaying = EditorApplication.isPlaying; + bool netReady = isPlaying && NetManager.Instance != null && NetManager.Instance.mClient != null; + + EditorGUILayout.HelpBox( + netReady ? "连接正常,可以发送协议" : "请先进入 Play 模式并确保网络连接正常", + netReady ? MessageType.Info : MessageType.Warning + ); + + EditorGUI.BeginDisabledGroup(!netReady); + try + { + EditorGUILayout.Space(4); + EditorGUILayout.LabelField("搜索协议", EditorStyles.boldLabel); + string newSearch = EditorGUILayout.TextField(_searchText); + if (newSearch != _searchText) + { + _searchText = newSearch; + _FilterMsgIds(); + } + + EditorGUILayout.Space(4); + EditorGUILayout.LabelField("选择协议ID", EditorStyles.boldLabel); + if (_filteredNames != null && _filteredNames.Count > 0) + { + _selectedIndex = EditorGUILayout.Popup("协议", _selectedIndex, _filteredNames.ToArray()); + } + + EditorGUILayout.Space(4); + EditorGUILayout.LabelField("协议字段", EditorStyles.boldLabel); + + Gameplay.Net.IMessage currentMsg = null; + object pbObj = null; + + if (netReady && _filteredIds != null && _filteredIds.Count > 0 && _selectedIndex >= 0 && _selectedIndex < _filteredIds.Count) + { + try + { + MSGID selectedId = _filteredIds[_selectedIndex]; + if (selectedId != _lastSelectedId) + { + _foldoutStates.Clear(); + _lastSelectedId = selectedId; + } + currentMsg = NetManager.Instance.GetMessage(selectedId); + if (currentMsg != null) + { + PropertyInfo mPBProp = _GetmPBProperty(currentMsg.GetType()); + if (mPBProp != null) + { + pbObj = mPBProp.GetValue(currentMsg); + } + } + } + catch (Exception e) + { + EditorGUILayout.HelpBox($"获取协议消息失败: {e.Message}", MessageType.Error); + } + } + + if (pbObj != null) + { + _fieldScrollPos = EditorGUILayout.BeginScrollView(_fieldScrollPos, GUILayout.ExpandHeight(true)); + try + { + _DrawProtobufMessage(pbObj, ""); + } + catch (Exception e) + { + EditorGUILayout.HelpBox($"绘制协议字段失败: {e.Message}", MessageType.Error); + } + finally + { + EditorGUILayout.EndScrollView(); + } + } + else if (netReady) + { + EditorGUILayout.HelpBox("无法获取协议消息内容", MessageType.Warning); + } + + EditorGUILayout.Space(4); + EditorGUILayout.BeginHorizontal(); + try + { + if (GUILayout.Button("发送协议", GUILayout.Height(30))) + { + _Send(currentMsg); + } + if (GUILayout.Button("重置字段", GUILayout.Height(30))) + { + _Reset(currentMsg); + } + } + finally + { + EditorGUILayout.EndHorizontal(); + } + } + finally + { + EditorGUI.EndDisabledGroup(); + } + + EditorGUILayout.Space(4); + EditorGUILayout.LabelField("发送日志", EditorStyles.boldLabel); + if (GUILayout.Button("清空日志")) + { + _logEntries.Clear(); + } + _logScrollPos = EditorGUILayout.BeginScrollView(_logScrollPos, GUILayout.Height(120)); + for (int i = _logEntries.Count - 1; i >= 0; i--) + { + EditorGUILayout.TextArea(_logEntries[i], GUILayout.ExpandWidth(true)); + } + EditorGUILayout.EndScrollView(); + } + + private void _FilterMsgIds() + { + _filteredIds = new List(); + _filteredNames = new List(); + for (int i = 0; i < _clientMsgIds.Count; i++) + { + if (string.IsNullOrEmpty(_searchText) || _clientMsgNames[i].IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0) + { + _filteredIds.Add(_clientMsgIds[i]); + _filteredNames.Add(_clientMsgNames[i]); + } + } + _selectedIndex = 0; + } + + private void _Send(Gameplay.Net.IMessage msg) + { + if (msg == null) + { + Debug.LogWarning("[NetDebug] 消息为空,无法发送"); + return; + } + if (NetManager.Instance?.mClient == null) + { + Debug.LogWarning("[NetDebug] NetClient 不可用"); + return; + } + try + { + NetManager.Instance.mClient.SendMessage(msg); + string logEntry = $"[{DateTime.Now:HH:mm:ss}] 发送 {msg.ID}"; + _logEntries.Add(logEntry); + Debug.Log($"[NetDebug] {logEntry}\n{msg}"); + } + catch (Exception e) + { + Debug.LogError($"[NetDebug] 发送失败: {e.Message}\n{e.StackTrace}"); + _logEntries.Add($"[{DateTime.Now:HH:mm:ss}] 发送失败: {e.Message}"); + } + } + + private void _Reset(Gameplay.Net.IMessage msg) + { + if (msg == null) return; + + try + { + FieldInfo backField = _GetmPBBackingField(msg.GetType()); + if (backField != null) + { + backField.SetValue(msg, null); + } + _foldoutStates.Clear(); + } + catch (Exception e) + { + Debug.LogWarning($"[NetDebug] 重置失败: {e.Message}"); + } + } + + private void _DrawProtobufMessage(object pbObj, string pathPrefix) + { + if (pbObj == null) return; + + Google.Protobuf.IMessage iMsg = pbObj as Google.Protobuf.IMessage; + if (iMsg == null) return; + + MessageDescriptor descriptor = iMsg.Descriptor; + if (descriptor == null) return; + + Type pbType = pbObj.GetType(); + + foreach (var field in descriptor.Fields.InFieldNumberOrder()) + { + string csharpName = _ProtoNameToCSharpName(field.Name); + string fieldPath = string.IsNullOrEmpty(pathPrefix) ? csharpName : $"{pathPrefix}.{csharpName}"; + PropertyInfo propInfo = pbType.GetProperty(csharpName, BindingFlags.Public | BindingFlags.Instance); + + if (field.IsRepeated) + { + _DrawRepeatedField(field, propInfo, pbObj, fieldPath); + } + else if (_IsMessageField(field)) + { + _DrawNestedMessage(field, propInfo, pbObj, fieldPath); + } + else if (field.FieldType == FieldType.Enum) + { + try + { + if (field.EnumType != null && field.EnumType.Values.Count > 0) + { + _DrawEnumField(field, propInfo, pbObj, fieldPath); + } + } + catch (Exception e) + { + EditorGUILayout.HelpBox("无效的字段:" + field.Name, MessageType.Warning); + } + } + else + { + _DrawScalarField(field, propInfo, pbObj, fieldPath); + } + } + } + + private void _DrawRepeatedField(FieldDescriptor field, PropertyInfo propInfo, object pbObj, string fieldPath) + { + if (propInfo == null) + { + EditorGUILayout.LabelField(field.Name, "(无法访问属性)"); + return; + } + + IList list = propInfo.GetValue(pbObj) as IList; + if (list == null) + { + EditorGUILayout.LabelField(field.Name, "(列表为 null)"); + return; + } + + string csharpName = _ProtoNameToCSharpName(field.Name); + string listLabel = $"{csharpName} [{list.Count}]"; + bool foldout = _GetFoldoutState(fieldPath, true); + foldout = EditorGUILayout.Foldout(foldout, listLabel); + _SetFoldoutState(fieldPath, foldout); + + if (!foldout) return; + + EditorGUI.indentLevel++; + + bool isElementMsg = _IsMessageField(field); + + for (int i = 0; i < list.Count; i++) + { + if (isElementMsg && list[i] != null) + { + bool itemFoldout = _GetFoldoutState($"{fieldPath}[{i}]", false); + EditorGUILayout.BeginHorizontal(); + itemFoldout = EditorGUILayout.Foldout(itemFoldout, $"[{i}]"); + _SetFoldoutState($"{fieldPath}[{i}]", itemFoldout); + if (GUILayout.Button("×", GUILayout.Width(20))) + { + list.RemoveAt(i); + i--; + EditorGUILayout.EndHorizontal(); + continue; + } + EditorGUILayout.EndHorizontal(); + if (itemFoldout) + { + EditorGUI.indentLevel++; + _DrawProtobufMessage(list[i], $"{fieldPath}[{i}]"); + EditorGUI.indentLevel--; + } + } + else + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField($"[{i}]", GUILayout.Width(40)); + object val = list[i]; + object newVal = _DrawValueField(val, field, ""); + if (newVal != null && !newVal.Equals(val)) + { + try { list[i] = newVal; } catch { } + } + if (GUILayout.Button("×", GUILayout.Width(20))) + { + list.RemoveAt(i); + i--; + EditorGUILayout.EndHorizontal(); + continue; + } + EditorGUILayout.EndHorizontal(); + } + } + + if (GUILayout.Button("+ 添加元素", GUILayout.Height(20))) + { + if (isElementMsg) + { + Type elementType = field.MessageType.ClrType; + if (elementType != null) + { + object newElem = Activator.CreateInstance(elementType); + list.Add(newElem); + } + } + else + { + object defaultVal = _GetDefaultForRepeatedElement(field); + if (defaultVal != null) + { + list.Add(defaultVal); + } + } + } + + EditorGUI.indentLevel--; + } + + private void _DrawNestedMessage(FieldDescriptor field, PropertyInfo propInfo, object pbObj, string fieldPath) + { + string label = _ProtoNameToCSharpName(field.Name); + + object subMsg = null; + if (propInfo != null) + { + subMsg = propInfo.GetValue(pbObj); + } + + if (subMsg == null) + { + if (propInfo != null && _IsMessageField(field) && field.MessageType.ClrType != null) + { + try + { + subMsg = Activator.CreateInstance(field.MessageType.ClrType); + propInfo.SetValue(pbObj, subMsg); + } + catch (Exception e) + { + EditorGUILayout.LabelField(label, $"(创建失败: {e.Message})"); + return; + } + } + else + { + EditorGUILayout.LabelField(label, "(无法创建子消息)"); + return; + } + } + + bool foldout = _GetFoldoutState(fieldPath, false); + foldout = EditorGUILayout.Foldout(foldout, label); + _SetFoldoutState(fieldPath, foldout); + + if (foldout) + { + EditorGUI.indentLevel++; + _DrawProtobufMessage(subMsg, fieldPath); + EditorGUI.indentLevel--; + } + } + + private void _DrawEnumField(FieldDescriptor field, PropertyInfo propInfo, object pbObj, string fieldPath) + { + if (propInfo == null) + { + EditorGUILayout.LabelField(field.Name, "(无法访问属性)"); + return; + } + + string label = _ProtoNameToCSharpName(field.Name); + object currentVal = propInfo.GetValue(pbObj); + + int currentInt = Convert.ToInt32(currentVal); + + string[] displayOptions = new string[field.EnumType.Values.Count]; + int[] values = new int[field.EnumType.Values.Count]; + int selectedIdx = 0; + for (int i = 0; i < field.EnumType.Values.Count; i++) + { + displayOptions[i] = $"{field.EnumType.Values[i].Name} = {field.EnumType.Values[i].Number}"; + values[i] = field.EnumType.Values[i].Number; + if (values[i] == currentInt) selectedIdx = i; + } + + int newIdx = EditorGUILayout.Popup(label, selectedIdx, displayOptions); + if (newIdx >= 0 && newIdx < values.Length && values[newIdx] != currentInt) + { + try + { + _SetPropertyValue(propInfo, pbObj, values[newIdx]); + } + catch (Exception e) + { + Debug.LogWarning($"[NetDebug] 设置枚举字段 {label} 失败: {e.Message}"); + } + } + } + + private void _DrawScalarField(FieldDescriptor field, PropertyInfo propInfo, object pbObj, string fieldPath) + { + if (propInfo == null) + { + EditorGUILayout.LabelField(field.Name, "(无法访问属性)"); + return; + } + + string label = _ProtoNameToCSharpName(field.Name); + object currentVal = propInfo.GetValue(pbObj); + if (currentVal == null) currentVal = _GetDefaultValue(field); + + object newVal = _DrawValueField(currentVal, field, label); + if (newVal != null && !newVal.Equals(currentVal)) + { + try + { + _SetPropertyValue(propInfo, pbObj, newVal); + } + catch (Exception e) + { + Debug.LogWarning($"[NetDebug] 设置字段 {label} 失败: {e.Message}"); + } + } + } + + private object _DrawValueField(object currentVal, FieldDescriptor field, string labelOverride) + { + string label = string.IsNullOrEmpty(labelOverride) ? _ProtoNameToCSharpName(field.Name) : labelOverride; + + switch (field.FieldType) + { + case FieldType.UInt32: + case FieldType.Fixed32: + { + long val = Convert.ToInt64(currentVal); + long newVal = EditorGUILayout.LongField(label, val); + return (uint)Math.Max(0, newVal); + } + case FieldType.Int32: + case FieldType.SInt32: + case FieldType.SFixed32: + { + int val = Convert.ToInt32(currentVal); + return EditorGUILayout.IntField(label, val); + } + case FieldType.UInt64: + case FieldType.Fixed64: + { + long val = Convert.ToInt64(currentVal); + long newVal = EditorGUILayout.LongField(label, val); + return (ulong)newVal; + } + case FieldType.Int64: + case FieldType.SInt64: + case FieldType.SFixed64: + { + long val = Convert.ToInt64(currentVal); + return EditorGUILayout.LongField(label, val); + } + case FieldType.Float: + { + float val = Convert.ToSingle(currentVal); + return EditorGUILayout.FloatField(label, val); + } + case FieldType.Double: + { + double val = Convert.ToDouble(currentVal); + return EditorGUILayout.DoubleField(label, val); + } + case FieldType.Bool: + { + bool val = Convert.ToBoolean(currentVal); + return EditorGUILayout.Toggle(label, val); + } + case FieldType.String: + { + string val = currentVal as string ?? ""; + return EditorGUILayout.TextField(label, val); + } + case FieldType.Bytes: + { + string val = currentVal is ByteString bs ? BitConverter.ToString(bs.ToByteArray()) : ""; + EditorGUILayout.LabelField(label, "(bytes) " + val); + return null; + } + case FieldType.Enum: + { + int val = Convert.ToInt32(currentVal); + string[] displayOptions = new string[field.EnumType.Values.Count]; + int[] values = new int[field.EnumType.Values.Count]; + int selectedIdx = 0; + for (int i = 0; i < field.EnumType.Values.Count; i++) + { + displayOptions[i] = $"{field.EnumType.Values[i].Name} = {field.EnumType.Values[i].Number}"; + values[i] = field.EnumType.Values[i].Number; + if (values[i] == val) selectedIdx = i; + } + int newIdx = EditorGUILayout.Popup(label, selectedIdx, displayOptions); + if (newIdx >= 0 && newIdx < values.Length) return values[newIdx]; + return val; + } + default: + EditorGUILayout.LabelField(label, $"(不支持 {field.FieldType})"); + return null; + } + } + + private object _GetDefaultValue(FieldDescriptor field) + { + switch (field.FieldType) + { + case FieldType.UInt32: + case FieldType.Fixed32: return (uint)0; + case FieldType.Int32: + case FieldType.SInt32: + case FieldType.SFixed32: return 0; + case FieldType.UInt64: + case FieldType.Fixed64: return (ulong)0; + case FieldType.Int64: + case FieldType.SInt64: + case FieldType.SFixed64: return (long)0; + case FieldType.Float: return 0f; + case FieldType.Double: return 0.0; + case FieldType.Bool: return false; + case FieldType.String: return ""; + default: return null; + } + } + + private object _GetDefaultForRepeatedElement(FieldDescriptor field) + { + switch (field.FieldType) + { + case FieldType.Int32: + case FieldType.SInt32: + case FieldType.SFixed32: + return 0; + case FieldType.UInt32: + case FieldType.Fixed32: + return (uint)0; + case FieldType.Int64: + case FieldType.SInt64: + case FieldType.SFixed64: + return (long)0; + case FieldType.UInt64: + case FieldType.Fixed64: + return (ulong)0; + case FieldType.Float: + return 0f; + case FieldType.Double: + return 0.0; + case FieldType.Bool: + return false; + case FieldType.String: + return ""; + case FieldType.Enum: + if (field.EnumType != null && field.EnumType.Values.Count > 0) + return field.EnumType.Values[0].Number; + return 0; + default: + return null; + } + } + + private void _SetPropertyValue(PropertyInfo propInfo, object target, object value) + { + Type propType = propInfo.PropertyType; + if (propType.IsEnum && value != null) + { + value = Enum.ToObject(propType, Convert.ToInt32(value)); + } + else if (value != null && !propType.IsAssignableFrom(value.GetType())) + { + try + { + value = Convert.ChangeType(value, propType); + } + catch (InvalidCastException) + { + // Conversion not possible, keep original value + return; + } + } + propInfo.SetValue(target, value); + } + + private bool _IsMessageField(FieldDescriptor field) + { + return field.FieldType == FieldType.Message || field.FieldType == FieldType.Group; + } + + private string _ProtoNameToCSharpName(string protoName) + { + if (string.IsNullOrEmpty(protoName)) return protoName; + string[] parts = protoName.Split('_'); + for (int i = 0; i < parts.Length; i++) + { + if (parts[i].Length > 0) + { + parts[i] = char.ToUpper(parts[i][0]) + parts[i].Substring(1); + } + } + return string.Join("", parts); + } + + private bool _GetFoldoutState(string key, bool defaultValue) + { + if (_foldoutStates.TryGetValue(key, out bool val)) + return val; + return defaultValue; + } + + private void _SetFoldoutState(string key, bool value) + { + _foldoutStates[key] = value; + } + + private void OnInspectorUpdate() + { + Repaint(); + } +} diff --git a/ProjectNLD/Assets/Editor/Tools/NetDebugWindow.cs.meta b/ProjectNLD/Assets/Editor/Tools/NetDebugWindow.cs.meta new file mode 100644 index 00000000000..c63d4644d71 --- /dev/null +++ b/ProjectNLD/Assets/Editor/Tools/NetDebugWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7d36088dc69f41448a1dbb40a70e336e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: