三国卡牌客户端基础资源仓库
lcy
2026-08-07 ea7847f8786d5695f10a56fe531285fbf18b8e70
651 封包查看工具查看和导出详细日志支持显示嵌套的所有条目
2个文件已修改
1214 ■■■■ 已修改文件
Assets/Editor/Web/NetPkgTool.cs 264 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/Web/PackageDetailsWindow.cs 950 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/Web/NetPkgTool.cs
@@ -110,7 +110,7 @@
    private void OnThreading()
    {
        while (threading)
        for (; threading;)
        {
            var packages = NetPkgCtl.GetPackages();
            if (packages != null)
@@ -146,162 +146,168 @@
        GUI.skin.button.normal.textColor = professionalSkin ? Color.white : Color.black;
        GUILayout.BeginVertical();
        GUILayout.BeginHorizontal();
        professionalSkin = !GUILayout.Toggle(!professionalSkin, "普通皮肤");
        professionalSkin = GUILayout.Toggle(professionalSkin, "高级皮肤");
        GUILayout.EndHorizontal();
        GUILayout.BeginHorizontal();
        maxShowPackageCount = EditorGUILayout.IntField("最大显示封包数:", maxShowPackageCount);
        showHeartPackage = GUILayout.Toggle(showHeartPackage, "心跳包");
        m_CurrentType = GUILayout.Toggle(m_CurrentType == NetPackagetType.All, "全部") ? NetPackagetType.All : m_CurrentType;
        m_CurrentType = GUILayout.Toggle(m_CurrentType == NetPackagetType.Server, "收包") ? NetPackagetType.Server : m_CurrentType;
        m_CurrentType = GUILayout.Toggle(m_CurrentType == NetPackagetType.Client, "发包") ? NetPackagetType.Client : m_CurrentType;
        GUILayout.EndHorizontal();
        GUILayout.BeginHorizontal();
        filterNetPkgStr = EditorGUILayout.TextField("关键字", filterNetPkgStr);
        reveralFilter = GUILayout.Toggle(reveralFilter, "反向过滤");
        GUILayout.EndHorizontal();
        SimSendPkg();
        scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, false);
        var packages = NetPkgCtl.GetPackages();
        if (packages != null)
        using (new GUILayout.VerticalScope())
        {
            var count = 0;
            for (int i = packages.Count - 1; i >= 0; i--)
            using (new GUILayout.HorizontalScope())
            {
                var package = packages[i];
                if (package != null)
                {
                    count++;
                }
                if (count >= maxShowPackageCount)
                {
                    break;
                }
                if (package.show)
                {
                    try
                    {
                        EditorGUILayout.BeginHorizontal();
                        if (professionalSkin)
                        {
                            GUI.skin.button.normal.textColor = package.type == NetPackagetType.Client ? Color.yellow : Color.white;
                        }
                        else
                        {
                            GUI.skin.button.normal.textColor = package.type == NetPackagetType.Client ? Color.blue : Color.black;
                        }
                        if (GUILayout.Button(package.type == NetPackagetType.Client ? "CS" : "SC", GUILayout.Width(30)))
                        {
                            PackageDetailsWindow.packageTime = package.time;
                            PackageDetailsWindow.packageName = package.name;
                            PackageDetailsWindow.packageBytes = package.content;
                            PackageDetailsWindow.packageDetails = package.fieldDetails;
                        }
                        GUI.skin.button.normal.textColor = professionalSkin ? Color.white : Color.black;
                        var showName = StringUtility.Concat(package.time, "  ", package.name);
                        EditorGUILayout.LabelField(package.socketType == ServerType.B430 ? new GUIContent($"<color=#119911>{showName}</color>") :
                        (package.socketType == ServerType.MainFight ? new GUIContent($"<color=#9C5E13>{showName}</color>") : new GUIContent(showName)),
                         new GUIStyle(EditorStyles.label) { richText = true }, GUILayout.Width(260));
                        EditorGUILayout.TextField(package.fields);
                        EditorGUILayout.EndHorizontal();
                    }
                    catch (Exception ex)
                    {
                    }
                }
                professionalSkin = !GUILayout.Toggle(!professionalSkin, "普通皮肤");
                professionalSkin = GUILayout.Toggle(professionalSkin, "高级皮肤");
            }
        }
        GUILayout.EndScrollView();
        GUILayout.BeginHorizontal();
        if (GUILayout.Button(stopRecBtnDetail))
        {
            NetPkgCtl.stopRecieve = !NetPkgCtl.stopRecieve;
            if (NetPkgCtl.stopRecieve)
            using (new GUILayout.HorizontalScope())
            {
                stopRecBtnDetail = "开始接收包";
                maxShowPackageCount = EditorGUILayout.IntField("最大显示封包数:", maxShowPackageCount);
                showHeartPackage = GUILayout.Toggle(showHeartPackage, "心跳包");
                m_CurrentType = GUILayout.Toggle(m_CurrentType == NetPackagetType.All, "全部") ? NetPackagetType.All : m_CurrentType;
                m_CurrentType = GUILayout.Toggle(m_CurrentType == NetPackagetType.Server, "收包") ? NetPackagetType.Server : m_CurrentType;
                m_CurrentType = GUILayout.Toggle(m_CurrentType == NetPackagetType.Client, "发包") ? NetPackagetType.Client : m_CurrentType;
            }
            else
            using (new GUILayout.HorizontalScope())
            {
                List<NetPackage> tempNetPkgLst = NetPkgCtl.GetTempPackages();
                filterNetPkgStr = EditorGUILayout.TextField("关键字", filterNetPkgStr);
                reveralFilter = GUILayout.Toggle(reveralFilter, "反向过滤");
            }
            SimSendPkg();
            var packages = NetPkgCtl.GetPackages();
            using (var scrollView = new GUILayout.ScrollViewScope(scrollPosition, false, false))
            {
                scrollPosition = scrollView.scrollPosition;
                if (packages != null)
                {
                    packages.AddRange(tempNetPkgLst);
                    while (packages.Count > MAX_PACKAGE_COUNT)
                    var count = 0;
                    for (int i = packages.Count - 1; i >= 0; i--)
                    {
                        packages.RemoveAt(0);
                        var package = packages[i];
                        if (package != null)
                        {
                            count++;
                        }
                        if (count >= maxShowPackageCount)
                        {
                            break;
                        }
                        if (package.show)
                        {
                            try
                            {
                                using (new GUILayout.HorizontalScope())
                                {
                                    if (professionalSkin)
                                    {
                                        GUI.skin.button.normal.textColor = package.type == NetPackagetType.Client ? Color.yellow : Color.white;
                                    }
                                    else
                                    {
                                        GUI.skin.button.normal.textColor = package.type == NetPackagetType.Client ? Color.blue : Color.black;
                                    }
                                    if (GUILayout.Button(package.type == NetPackagetType.Client ? "CS" : "SC", GUILayout.Width(30)))
                                    {
                                        PackageDetailsWindow.SetPackage(package);
                                    }
                                    GUI.skin.button.normal.textColor = professionalSkin ? Color.white : Color.black;
                                    var showName = StringUtility.Concat(package.time, "  ", package.name);
                                    EditorGUILayout.LabelField(package.socketType == ServerType.B430 ? new GUIContent($"<color=#119911>{showName}</color>") :
                                    (package.socketType == ServerType.MainFight ? new GUIContent($"<color=#9C5E13>{showName}</color>") : new GUIContent(showName)),
                                     new GUIStyle(EditorStyles.label) { richText = true }, GUILayout.Width(260));
                                    EditorGUILayout.TextField(package.fields);
                                }
                            }
                            catch (Exception ex)
                            {
                            }
                        }
                    }
                }
                tempNetPkgLst.Clear();
                stopRecBtnDetail = "暂停接收包";
            }
        }
        if (GUILayout.Button("打印摘要"))
        {
            NetPkgCtl.WriteAllNetLog(false);
        }
        if (GUILayout.Button("打印详情"))
        {
            NetPkgCtl.WriteAllNetLog(true);
        }
        if (GUILayout.Button("清除"))
        {
            if (NetPkgCtl.GetPackages() != null)
            using (new GUILayout.HorizontalScope())
            {
                NetPkgCtl.GetPackages().Clear();
                if (GUILayout.Button(stopRecBtnDetail))
                {
                    NetPkgCtl.stopRecieve = !NetPkgCtl.stopRecieve;
                    if (NetPkgCtl.stopRecieve)
                    {
                        stopRecBtnDetail = "开始接收包";
                    }
                    else
                    {
                        List<NetPackage> tempNetPkgLst = NetPkgCtl.GetTempPackages();
                        if (packages != null)
                        {
                            packages.AddRange(tempNetPkgLst);
                            for (; packages.Count > MAX_PACKAGE_COUNT;)
                            {
                                packages.RemoveAt(0);
                            }
                        }
                        tempNetPkgLst.Clear();
                        stopRecBtnDetail = "暂停接收包";
                    }
                }
                if (GUILayout.Button("打印摘要"))
                {
                    NetPkgCtl.WriteAllNetLog(false);
                }
                if (GUILayout.Button("打印详情"))
                {
                    PackageDetailsWindow.WriteAllPackageDetails();
                }
                if (GUILayout.Button("清除"))
                {
                    if (NetPkgCtl.GetPackages() != null)
                    {
                        NetPkgCtl.GetPackages().Clear();
                    }
                }
            }
        }
        GUILayout.EndHorizontal();
        GUILayout.EndVertical();
    }
    private void SimSendPkg()
    {
        GUILayout.BeginHorizontal();
        simSendPkgDetail = EditorGUILayout.TextField("模拟发包内容:", simSendPkgDetail);
        if (GUILayout.Button("模拟发包"))
        using (new GUILayout.HorizontalScope())
        {
            string sendPkgDetail = simSendPkgDetail.Replace(" ", "");
            if (sendPkgDetail.Length % 2 != 0)
            simSendPkgDetail = EditorGUILayout.TextField("模拟发包内容:", simSendPkgDetail);
            if (GUILayout.Button("模拟发包"))
            {
                sendPkgDetail += " ";
                string sendPkgDetail = simSendPkgDetail.Replace(" ", "");
                if (sendPkgDetail.Length % 2 != 0)
                {
                    sendPkgDetail += " ";
                }
                byte[] sendBytes = new byte[sendPkgDetail.Length / 2];
                for (int i = 0; i < sendBytes.Length; i++)
                {
                    sendBytes[i] = Convert.ToByte(sendPkgDetail.Substring(i * 2, 2), 16);
                }
                GameNetSystem.Instance.SendInfo(sendBytes);
            }
            byte[] sendBytes = new byte[sendPkgDetail.Length / 2];
            for (int i = 0; i < sendBytes.Length; i++)
            {
                sendBytes[i] = Convert.ToByte(sendPkgDetail.Substring(i * 2, 2), 16);
            }
            GameNetSystem.Instance.SendInfo(sendBytes);
        }
        GUILayout.EndHorizontal();
    }
    private string NetPkgTypeString()
Assets/Editor/Web/PackageDetailsWindow.cs
@@ -1,21 +1,75 @@
using System.Collections;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using UnityEngine;
using UnityEditor;
public class PackageDetailsWindow : ScriptableWizard
{
    private class PrintNode
    {
        public object value;
        public bool isArray;
        public string[] childNames;
        public int[] childIndexes;
        public string content;
    }
    private struct DetailSearchMatch
    {
        public int detailIndex;
        public int characterIndex;
        public DetailSearchMatch(int detailIndex, int characterIndex)
        {
            this.detailIndex = detailIndex;
            this.characterIndex = characterIndex;
        }
    }
    static PackageDetailsWindow window;
    public static string packageTime;
    public static string packageName;
    public static string packageBytes;
    public static List<string> packageDetails;
    private static string[] formattedPackageDetails;
    private static bool[] detailFormattedStates;
    private static bool clearDetailSelectionOnNextGUI;
    private static string detailSearchText = string.Empty;
    private static string activeDetailSearchText = string.Empty;
    private static readonly List<DetailSearchMatch> detailSearchMatches =
        new List<DetailSearchMatch>();
    private static int currentDetailSearchMatch = -1;
    private static int pendingScrollSearchMatch = -1;
    private static float[] detailRowPositions;
    private static float[] detailSearchMatchPositions;
    private static float detailScrollViewHeight;
    private Vector2 scrollPosition;
    public static void SetPackage(NetPackage package)
    {
        packageTime = package.time;
        packageName = package.name;
        packageBytes = package.content;
        packageDetails = BuildPackageDetails(package);
        ResetDetailFormatState();
        ResetDetailSearchState();
        RequestDetailDisplayRefresh();
    }
    private void OnGUI()
    {
        window = this;
        ClearDetailSelectionIfNeeded();
        var fieldDetailStyle = new GUIStyle(EditorStyles.textArea);
        fieldDetailStyle.wordWrap = true;
        EditorGUILayout.BeginVertical();
        EditorGUILayout.Space();
        EditorGUILayout.LabelField("名称:", !string.IsNullOrEmpty(packageName) ? packageName : "");
@@ -29,21 +83,915 @@
        EditorGUILayout.Space();
        EditorGUILayout.LabelField("字段详情:");
        ApplyPendingSearchScroll();
        scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true);
        if (packageDetails != null)
        {
            for (int i = 0; i < packageDetails.Count; i++)
            {
                EditorGUILayout.TextField(packageDetails[i]);
                using (new GUILayout.HorizontalScope())
                {
                    var detail = GetDisplayDetail(i);
                    var detailWidth = Mathf.Max(100f, position.width - 145f);
                    var detailHeight = Mathf.Max(EditorGUIUtility.singleLineHeight + 4f,
                        fieldDetailStyle.CalcHeight(new GUIContent(detail), detailWidth));
                    var backgroundColor = GUI.backgroundColor;
                    if (IsCurrentSearchMatch(i))
                    {
                        GUI.backgroundColor = new Color(0.45f, 0.75f, 1f, 1f);
                    }
                    var detailRect = EditorGUILayout.GetControlRect(false, detailHeight,
                        GUILayout.ExpandWidth(true));
                    EditorGUI.SelectableLabel(detailRect, detail, fieldDetailStyle);
                    GUI.backgroundColor = backgroundColor;
                    DrawDetailSearchHighlights(i, detailRect, detail, fieldDetailStyle);
                    if (GUILayout.Button("格式化", GUILayout.Width(55f),
                        GUILayout.Height(EditorGUIUtility.singleLineHeight + 4f)))
                    {
                        SetDetailFormatted(i, true);
                    }
                    if (GUILayout.Button("还原", GUILayout.Width(45f),
                        GUILayout.Height(EditorGUIUtility.singleLineHeight + 4f)))
                    {
                        SetDetailFormatted(i, false);
                    }
                }
                if (Event.current.type == EventType.Repaint
                    && detailRowPositions != null
                    && i < detailRowPositions.Length)
                {
                    detailRowPositions[i] = GUILayoutUtility.GetLastRect().y;
                }
            }
        }
        GUILayout.EndScrollView();
        if (Event.current.type == EventType.Repaint)
        {
            detailScrollViewHeight = GUILayoutUtility.GetLastRect().height;
            if (pendingScrollSearchMatch >= 0)
            {
                Repaint();
            }
        }
        using (new GUILayout.HorizontalScope())
        {
            detailSearchText = EditorGUILayout.TextField(detailSearchText);
            if (GUILayout.Button("搜索", GUILayout.Width(50f)))
            {
                SearchDetails();
            }
            if (GUILayout.Button("清除", GUILayout.Width(50f)))
            {
                ClearDetailSearch();
            }
            using (new EditorGUI.DisabledScope(detailSearchMatches.Count == 0))
            {
                if (GUILayout.Button("上一条", GUILayout.Width(55f)))
                {
                    MoveToDetailSearchMatch(-1);
                }
                if (GUILayout.Button("下一条", GUILayout.Width(55f)))
                {
                    MoveToDetailSearchMatch(1);
                }
            }
            GUILayout.Label(GetDetailSearchProgress(), GUILayout.Width(55f));
        }
        using (new GUILayout.HorizontalScope())
        {
            if (GUILayout.Button("格式化全部"))
            {
                SetAllDetailsFormatted(true);
            }
            if (GUILayout.Button("还原全部"))
            {
                SetAllDetailsFormatted(false);
            }
        }
        using (new GUILayout.HorizontalScope())
        {
            if (GUILayout.Button("导出 TXT"))
            {
                ExportToTxt();
            }
            if (GUILayout.Button("复制"))
            {
                CopyToClipboard();
            }
        }
        EditorGUILayout.EndVertical();
    }
    private static void ExportToTxt()
    {
        var fileName = GetExportFileName() + ".txt";
        var path = Path.Combine(Application.dataPath, fileName);
        File.WriteAllText(path, BuildTextContent(), new UTF8Encoding(true));
    }
    public static void WriteAllPackageDetails()
    {
#if !UNITY_WEBGL
        var packages = NetPkgCtl.GetPackages();
        if (packages == null)
        {
            return;
        }
        var count = 0;
        var lines = new List<string>();
        for (int i = packages.Count - 1; i >= 0; i--)
        {
            if (count > 20000)
            {
                break;
            }
            var package = packages[i];
            var line = StringUtility.Concat(
                package.type == NetPackagetType.Client ? "【发送】\t" : "【接收】\t",
                package.time, ":", package.name, "\r\n");
            var details = BuildPackageDetails(package);
            if (details != null)
            {
                for (int j = 0; j < details.Count; j++)
                {
                    line = StringUtility.Concat(line, "\t\t\t", details[j], "\r\n");
                }
            }
            lines.Add(line);
            count++;
        }
        var path = Application.dataPath + "/PackageLogs_details_"
            + DateTime.Now.ToString("HH_mm_ss") + ".txt";
        File.WriteAllLines(path, lines.ToArray());
#endif
    }
    private static void CopyToClipboard()
    {
        EditorGUIUtility.systemCopyBuffer = BuildTextContent();
    }
    private static string BuildTextContent()
    {
        var content = new StringBuilder();
        content.Append("名称:").AppendLine(string.IsNullOrEmpty(packageName) ? string.Empty : packageName);
        content.Append("时间:").AppendLine(string.IsNullOrEmpty(packageTime) ? string.Empty : packageTime);
        content.AppendLine("字节流:");
        content.AppendLine(string.IsNullOrEmpty(packageBytes) ? string.Empty : packageBytes);
        content.AppendLine("字段详情:");
        if (packageDetails != null)
        {
            for (int i = 0; i < packageDetails.Count; i++)
            {
                content.AppendLine(GetDisplayDetail(i));
            }
        }
        return content.ToString();
    }
    private static string GetExportFileName()
    {
        var safePackageName = string.IsNullOrEmpty(packageName) ? "PackageDetails" : packageName;
        var invalidFileNameChars = Path.GetInvalidFileNameChars();
        for (int i = 0; i < invalidFileNameChars.Length; i++)
        {
            safePackageName = safePackageName.Replace(invalidFileNameChars[i], '_');
        }
        var safePackageTime = string.IsNullOrEmpty(packageTime) ? string.Empty : "_" + packageTime.Replace(':', '_');
        return safePackageName + safePackageTime;
    }
    private static void ResetDetailFormatState()
    {
        if (packageDetails == null)
        {
            formattedPackageDetails = null;
            detailFormattedStates = null;
            return;
        }
        formattedPackageDetails = new string[packageDetails.Count];
        detailFormattedStates = new bool[packageDetails.Count];
        for (int i = 0; i < packageDetails.Count; i++)
        {
            formattedPackageDetails[i] = FormatDetail(packageDetails[i]);
        }
    }
    private static void ResetDetailSearchState()
    {
        detailSearchText = string.Empty;
        activeDetailSearchText = string.Empty;
        detailSearchMatches.Clear();
        currentDetailSearchMatch = -1;
        pendingScrollSearchMatch = -1;
        detailRowPositions = packageDetails == null ? null : new float[packageDetails.Count];
        detailSearchMatchPositions = null;
        detailScrollViewHeight = 0f;
    }
    private static void ClearDetailSearch()
    {
        ResetDetailSearchState();
        if (window != null)
        {
            window.scrollPosition = Vector2.zero;
        }
        RequestDetailDisplayRefresh();
    }
    private void ApplyPendingSearchScroll()
    {
        if (pendingScrollSearchMatch < 0
            || pendingScrollSearchMatch >= detailSearchMatches.Count)
        {
            return;
        }
        var match = detailSearchMatches[pendingScrollSearchMatch];
        var targetY = -1f;
        if (detailSearchMatchPositions != null
            && pendingScrollSearchMatch < detailSearchMatchPositions.Length)
        {
            targetY = detailSearchMatchPositions[pendingScrollSearchMatch];
        }
        if (targetY >= 0f)
        {
            scrollPosition.y = Mathf.Max(0f, targetY - detailScrollViewHeight * 0.45f);
            pendingScrollSearchMatch = -1;
            return;
        }
        if (detailRowPositions != null
            && match.detailIndex >= 0
            && match.detailIndex < detailRowPositions.Length)
        {
            scrollPosition.y = Mathf.Max(0f, detailRowPositions[match.detailIndex] - 4f);
        }
    }
    private static bool IsCurrentSearchMatch(int detailIndex)
    {
        return currentDetailSearchMatch >= 0
            && currentDetailSearchMatch < detailSearchMatches.Count
            && detailSearchMatches[currentDetailSearchMatch].detailIndex == detailIndex;
    }
    private static void DrawDetailSearchHighlights(int detailIndex, Rect detailRect, string detail,
        GUIStyle style)
    {
        if (Event.current.type != EventType.Repaint
            || string.IsNullOrEmpty(detail)
            || string.IsNullOrEmpty(activeDetailSearchText))
        {
            return;
        }
        var content = new GUIContent(detail);
        var searchLength = activeDetailSearchText.Length;
        for (int matchListIndex = 0; matchListIndex < detailSearchMatches.Count; matchListIndex++)
        {
            var match = detailSearchMatches[matchListIndex];
            if (match.detailIndex != detailIndex
                || match.characterIndex < 0
                || match.characterIndex + searchLength > detail.Length)
            {
                continue;
            }
            if (detailSearchMatchPositions != null
                && matchListIndex < detailSearchMatchPositions.Length)
            {
                detailSearchMatchPositions[matchListIndex] =
                    style.GetCursorPixelPosition(detailRect, content, match.characterIndex).y;
            }
            var highlightColor = matchListIndex == currentDetailSearchMatch
                ? new Color(1f, 0.55f, 0.02f, 0.68f)
                : new Color(1f, 0.72f, 0.05f, 0.42f);
            DrawDetailSearchHighlightRange(detailRect, content, style, match.characterIndex,
                match.characterIndex + searchLength, highlightColor);
        }
    }
    private static void DrawDetailSearchHighlightRange(Rect detailRect, GUIContent content,
        GUIStyle style, int startIndex, int endIndex, Color highlightColor)
    {
        for (int cursorIndex = startIndex; cursorIndex < endIndex; cursorIndex++)
        {
            var startCursor = style.GetCursorPixelPosition(detailRect, content, cursorIndex);
            var endCursor = style.GetCursorPixelPosition(detailRect, content, cursorIndex + 1);
            var endX = endCursor.x;
            if (Mathf.Abs(endCursor.y - startCursor.y) > 0.1f || endX <= startCursor.x)
            {
                var characterWidth = style.CalcSize(
                    new GUIContent(content.text[cursorIndex].ToString())).x;
                endX = Mathf.Min(detailRect.xMax - style.padding.right,
                    startCursor.x + characterWidth);
            }
            DrawDetailSearchHighlightRect(startCursor.x, startCursor.y, endX, style,
                highlightColor);
        }
    }
    private static void DrawDetailSearchHighlightRect(float startX, float y, float endX,
        GUIStyle style, Color highlightColor)
    {
        var width = endX - startX;
        if (width <= 0f)
        {
            return;
        }
        var lineHeight = Mathf.Max(style.lineHeight, EditorGUIUtility.singleLineHeight);
        var highlightRect = new Rect(startX, y + 1f, width, Mathf.Max(1f, lineHeight - 2f));
        EditorGUI.DrawRect(highlightRect, highlightColor);
    }
    private static void SearchDetails()
    {
        RebuildDetailSearchMatches();
        if (detailSearchMatches.Count == 0)
        {
            return;
        }
        currentDetailSearchMatch = 0;
        JumpToCurrentDetailSearchMatch();
    }
    private static void MoveToDetailSearchMatch(int direction)
    {
        if (!string.Equals(activeDetailSearchText, detailSearchText, StringComparison.Ordinal))
        {
            RebuildDetailSearchMatches();
            if (detailSearchMatches.Count == 0)
            {
                return;
            }
            currentDetailSearchMatch = direction < 0 ? detailSearchMatches.Count - 1 : 0;
            JumpToCurrentDetailSearchMatch();
            return;
        }
        if (detailSearchMatches.Count == 0)
        {
            return;
        }
        currentDetailSearchMatch += direction;
        if (currentDetailSearchMatch < 0)
        {
            currentDetailSearchMatch = detailSearchMatches.Count - 1;
        }
        else if (currentDetailSearchMatch >= detailSearchMatches.Count)
        {
            currentDetailSearchMatch = 0;
        }
        JumpToCurrentDetailSearchMatch();
    }
    private static void RebuildDetailSearchMatches()
    {
        activeDetailSearchText = detailSearchText ?? string.Empty;
        detailSearchMatches.Clear();
        currentDetailSearchMatch = -1;
        pendingScrollSearchMatch = -1;
        detailSearchMatchPositions = null;
        if (packageDetails == null || string.IsNullOrWhiteSpace(activeDetailSearchText))
        {
            RequestDetailDisplayRefresh();
            return;
        }
        for (int i = 0; i < packageDetails.Count; i++)
        {
            var detail = GetDisplayDetail(i);
            if (string.IsNullOrEmpty(detail))
            {
                continue;
            }
            var searchLength = activeDetailSearchText.Length;
            for (int searchStart = 0; searchStart <= detail.Length - searchLength;)
            {
                var matchIndex = detail.IndexOf(activeDetailSearchText, searchStart,
                    StringComparison.OrdinalIgnoreCase);
                if (matchIndex < 0)
                {
                    break;
                }
                detailSearchMatches.Add(new DetailSearchMatch(i, matchIndex));
                searchStart = matchIndex + searchLength;
            }
        }
        detailSearchMatchPositions = new float[detailSearchMatches.Count];
        for (int i = 0; i < detailSearchMatchPositions.Length; i++)
        {
            detailSearchMatchPositions[i] = -1f;
        }
        RequestDetailDisplayRefresh();
    }
    private static void JumpToCurrentDetailSearchMatch()
    {
        if (currentDetailSearchMatch < 0 || currentDetailSearchMatch >= detailSearchMatches.Count)
        {
            return;
        }
        pendingScrollSearchMatch = currentDetailSearchMatch;
        RequestDetailDisplayRefresh();
    }
    private static string GetDetailSearchProgress()
    {
        if (detailSearchMatches.Count == 0 || currentDetailSearchMatch < 0)
        {
            return "0/0";
        }
        return (currentDetailSearchMatch + 1) + "/" + detailSearchMatches.Count;
    }
    private static string GetDisplayDetail(int index)
    {
        if (packageDetails == null || index < 0 || index >= packageDetails.Count)
        {
            return string.Empty;
        }
        if (detailFormattedStates != null
            && formattedPackageDetails != null
            && index < detailFormattedStates.Length
            && index < formattedPackageDetails.Length
            && detailFormattedStates[index])
        {
            return formattedPackageDetails[index];
        }
        return packageDetails[index] ?? string.Empty;
    }
    private static void SetDetailFormatted(int index, bool formatted)
    {
        var states = detailFormattedStates;
        if (states == null || index < 0 || index >= states.Length)
        {
            return;
        }
        EditorApplication.delayCall += () =>
        {
            if (detailFormattedStates != states || index < 0 || index >= states.Length)
            {
                return;
            }
            states[index] = formatted;
            RefreshDetailSearchAfterFormatChange();
        };
    }
    private static void SetAllDetailsFormatted(bool formatted)
    {
        var states = detailFormattedStates;
        if (states == null)
        {
            return;
        }
        EditorApplication.delayCall += () =>
        {
            if (detailFormattedStates != states)
            {
                return;
            }
            for (int i = 0; i < states.Length; i++)
            {
                states[i] = formatted;
            }
            RefreshDetailSearchAfterFormatChange();
        };
    }
    private static void RefreshDetailSearchAfterFormatChange()
    {
        if (detailSearchMatches.Count == 0 || string.IsNullOrEmpty(activeDetailSearchText))
        {
            RequestDetailDisplayRefresh();
            return;
        }
        var previousMatch = currentDetailSearchMatch;
        RebuildDetailSearchMatches();
        if (detailSearchMatches.Count == 0)
        {
            return;
        }
        currentDetailSearchMatch = Mathf.Clamp(previousMatch, 0, detailSearchMatches.Count - 1);
        JumpToCurrentDetailSearchMatch();
    }
    private static void RequestDetailDisplayRefresh()
    {
        clearDetailSelectionOnNextGUI = true;
        if (window != null)
        {
            window.Repaint();
        }
    }
    private static void ClearDetailSelectionIfNeeded()
    {
        if (!clearDetailSelectionOnNextGUI)
        {
            return;
        }
        GUI.FocusControl(null);
        GUIUtility.keyboardControl = 0;
        GUIUtility.hotControl = 0;
        EditorGUIUtility.editingTextField = false;
        clearDetailSelectionOnNextGUI = false;
    }
    private static string FormatDetail(string detail)
    {
        if (string.IsNullOrEmpty(detail))
        {
            return string.Empty;
        }
        var valueStart = detail.IndexOf("-->", StringComparison.Ordinal);
        if (valueStart < 0)
        {
            return detail;
        }
        valueStart += 3;
        var hasContainer = false;
        for (int i = valueStart; i < detail.Length; i++)
        {
            if (detail[i] == '{' || detail[i] == '[')
            {
                hasContainer = true;
                break;
            }
        }
        if (!hasContainer)
        {
            return detail;
        }
        var content = new StringBuilder(detail.Length * 2);
        content.Append(detail, 0, valueStart);
        var indent = 0;
        var inQuotes = false;
        var escaped = false;
        var skipWhitespace = false;
        for (int i = valueStart; i < detail.Length; i++)
        {
            var character = detail[i];
            if (inQuotes)
            {
                content.Append(character);
                if (escaped)
                {
                    escaped = false;
                }
                else if (character == '\\')
                {
                    escaped = true;
                }
                else if (character == '"')
                {
                    inQuotes = false;
                }
                continue;
            }
            if (skipWhitespace && char.IsWhiteSpace(character))
            {
                continue;
            }
            skipWhitespace = false;
            if (character == '"')
            {
                inQuotes = true;
                content.Append(character);
                continue;
            }
            if (character == '{' || character == '[')
            {
                content.Append(character);
                indent++;
                if (!IsEmptyContainer(detail, i, character == '{' ? '}' : ']'))
                {
                    AppendNewLineAndIndent(content, indent);
                }
                skipWhitespace = true;
                continue;
            }
            if (character == '}' || character == ']')
            {
                indent = Mathf.Max(0, indent - 1);
                var openingCharacter = character == '}' ? '{' : '[';
                if (content.Length == 0 || content[content.Length - 1] != openingCharacter)
                {
                    AppendNewLineAndIndent(content, indent);
                }
                content.Append(character);
                continue;
            }
            if (character == ';' || character == ',')
            {
                content.Append(character);
                AppendNewLineAndIndent(content, indent);
                skipWhitespace = true;
                continue;
            }
            content.Append(character);
        }
        return content.ToString();
    }
    private static bool IsEmptyContainer(string content, int openingIndex, char closingCharacter)
    {
        for (int i = openingIndex + 1; i < content.Length; i++)
        {
            if (char.IsWhiteSpace(content[i]))
            {
                continue;
            }
            return content[i] == closingCharacter;
        }
        return false;
    }
    private static void AppendNewLineAndIndent(StringBuilder content, int indent)
    {
        content.AppendLine();
        content.Append(' ', indent * 4);
    }
    public static List<string> BuildPackageDetails(NetPackage package)
    {
        if (package.type != NetPackagetType.Server
            || string.IsNullOrEmpty(package.name)
            || string.IsNullOrEmpty(package.content))
        {
            return package.fieldDetails;
        }
        var packageType = typeof(GameNetPackBasic).Assembly.GetType(package.name);
        if (packageType == null || !typeof(GameNetPackBasic).IsAssignableFrom(packageType))
        {
            return package.fieldDetails;
        }
        var readMethod = packageType.GetMethod("ReadFromBytes", BindingFlags.Instance | BindingFlags.Public);
        if (readMethod == null || readMethod.DeclaringType == typeof(GameNetPackBasic))
        {
            return package.fieldDetails;
        }
        try
        {
            var packageObject = FormatterServices.GetUninitializedObject(packageType) as GameNetPackBasic;
            var readIndexField = typeof(GameNetPackBasic).GetField("_readIndex",
                BindingFlags.Instance | BindingFlags.NonPublic);
            readIndexField.SetValue(packageObject, 2);
            packageObject.ReadFromBytes(ParsePackageBytes(package.content));
            return BuildExpandedFieldDetails(packageObject, package.fieldDetails);
        }
        catch (Exception ex)
        {
            Debug.LogError(string.Format("[PackageDetailsWindow] 完整展开封包 {0} 失败:{1}",
                package.name, ex));
            return package.fieldDetails;
        }
    }
    private static byte[] ParsePackageBytes(string content)
    {
        var bytesText = content.Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
        var bytes = new byte[bytesText.Length];
        for (int i = 0; i < bytesText.Length; i++)
        {
            bytes[i] = Convert.ToByte(bytesText[i], 16);
        }
        return bytes;
    }
    private static List<string> BuildExpandedFieldDetails(object packageObject, List<string> recordedDetails)
    {
        var fields = packageObject.GetType().GetFields();
        var contents = new List<string>();
        for (int i = 0; i < fields.Length; i++)
        {
            var fieldContent = fields[i].GetValue(packageObject);
            var array = fieldContent as Array;
            if (array != null)
            {
                contents.AddRange(FormatArray(fields[i].Name, array));
            }
            else
            {
                var recordedDetail = FindRecordedFieldDetail(recordedDetails, fields[i].Name);
                contents.Add(recordedDetail ?? string.Format("\"{0}\":{1}", fields[i].Name, fieldContent));
            }
        }
        return contents;
    }
    private static string FindRecordedFieldDetail(List<string> recordedDetails, string fieldName)
    {
        if (recordedDetails == null)
        {
            return null;
        }
        var prefix = string.Format("\"{0}\":", fieldName);
        for (int i = 0; i < recordedDetails.Count; i++)
        {
            if (recordedDetails[i].StartsWith(prefix, StringComparison.Ordinal))
            {
                return recordedDetails[i];
            }
        }
        return null;
    }
    private static string[] FormatArray(string fieldName, Array array)
    {
        var contents = new string[array.Length];
        for (int i = 0; i < array.Length; i++)
        {
            var value = array.GetValue(i);
            if (value == null)
            {
                contents[i] = string.Format("\"{0}\"[{1}]-->null", fieldName, i);
                continue;
            }
            var type = value.GetType();
            contents[i] = IsSimpleType(type)
                ? string.Format("\"{0}\"[{1}]-->({2}){3}", fieldName, i, type.Name, FormatSimpleValue(value))
                : string.Format("\"{0}\"[{1}]-->{2}", fieldName, i, FormatValue(value));
        }
        return contents;
    }
    private static string FormatValue(object value)
    {
        var nodes = new List<PrintNode>();
        nodes.Add(new PrintNode { value = value });
        for (int nodeIndex = 0; nodeIndex < nodes.Count; nodeIndex++)
        {
            var node = nodes[nodeIndex];
            if (node.value == null)
            {
                continue;
            }
            var type = node.value.GetType();
            if (IsSimpleType(type))
            {
                continue;
            }
            var array = node.value as Array;
            if (array != null)
            {
                node.isArray = true;
                node.childIndexes = new int[array.Length];
                for (int i = 0; i < array.Length; i++)
                {
                    node.childIndexes[i] = nodes.Count;
                    nodes.Add(new PrintNode { value = array.GetValue(i) });
                }
                continue;
            }
            var fields = type.GetFields();
            node.childNames = new string[fields.Length];
            node.childIndexes = new int[fields.Length];
            for (int i = 0; i < fields.Length; i++)
            {
                node.childNames[i] = fields[i].Name;
                node.childIndexes[i] = nodes.Count;
                nodes.Add(new PrintNode { value = fields[i].GetValue(node.value) });
            }
        }
        for (int nodeIndex = nodes.Count - 1; nodeIndex >= 0; nodeIndex--)
        {
            var node = nodes[nodeIndex];
            if (node.value == null)
            {
                node.content = "null";
                continue;
            }
            var type = node.value.GetType();
            if (IsSimpleType(type))
            {
                node.content = FormatSimpleValue(node.value);
                continue;
            }
            var content = new StringBuilder();
            if (node.isArray)
            {
                content.Append("[");
                for (int i = 0; i < node.childIndexes.Length; i++)
                {
                    if (i > 0)
                    {
                        content.Append(",  ");
                    }
                    content.Append(nodes[node.childIndexes[i]].content);
                }
                content.Append("]");
            }
            else
            {
                content.Append("{");
                for (int i = 0; i < node.childIndexes.Length; i++)
                {
                    if (i > 0)
                    {
                        content.Append(";  ");
                    }
                    content.Append("\"").Append(node.childNames[i]).Append("\":")
                        .Append(nodes[node.childIndexes[i]].content);
                }
                content.Append("}");
            }
            node.content = content.ToString();
        }
        return nodes[0].content;
    }
    private static bool IsSimpleType(Type type)
    {
        return type.IsPrimitive
            || type.IsEnum
            || type == typeof(string)
            || type == typeof(decimal)
            || type == typeof(DateTime);
    }
    private static string FormatSimpleValue(object value)
    {
        return value == null ? "null" : value.ToString();
    }
    [UnityEditor.MenuItem("程序/封包详情 &2")]
    public static void CreatePackageDetailsWindow()
    {