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