【工具】增加协议模拟工具

main
刘涛 2026-05-12 18:17:34 +08:00
parent 7f19b4a4d6
commit 3e3a44b66b
2 changed files with 717 additions and 0 deletions

View File

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

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7d36088dc69f41448a1dbb40a70e336e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: