NLDClient-yudde/ProjectNLD/Assets/Editor/Tools/NetDebugWindow.cs

707 lines
24 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<NetDebugWindow>("网络协议调试");
window.minSize = new Vector2(400f, 500f);
window.Show();
}
private List<MSGID> _clientMsgIds;
private List<string> _clientMsgNames;
private List<string> _filteredNames;
private List<MSGID> _filteredIds;
private int _selectedIndex = 0;
private string _searchText = "";
private Vector2 _fieldScrollPos;
private Vector2 _logScrollPos;
private List<string> _logEntries = new List<string>();
private Dictionary<string, bool> _foldoutStates = new Dictionary<string, bool>();
private Dictionary<Type, PropertyInfo> _mPBPropertyCache = new Dictionary<Type, PropertyInfo>();
private Dictionary<Type, FieldInfo> _mPBBackingFieldCache = new Dictionary<Type, FieldInfo>();
private MSGID _lastSelectedId = MSGID.None;
private bool _initialized;
private void _Init()
{
if (_initialized) return;
_initialized = true;
_clientMsgIds = new List<MSGID>();
_clientMsgNames = new List<string>();
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<MSGID>(_clientMsgIds);
_filteredNames = new List<string>(_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<MSGID>();
_filteredNames = new List<string>();
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();
}
}