TH1/Unity/Assets/Scripts/TH1_Logic/Editor/MapDataCompressionEditorWindow.cs
2026-05-23 21:03:20 +08:00

414 lines
16 KiB
C#

/*
* @Author: Codex
* @Description: Runtime MapData compression measurement tool.
* @Date: 2026年05月23日 星期六
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Text;
using MemoryPack;
using RuntimeData;
using TH1_Logic.Core;
using TH1_Logic.Net;
using TH1_Logic.Steam;
using UnityEditor;
using UnityEngine;
namespace Logic.Editor
{
public class MapDataCompressionEditorWindow : EditorWindow
{
private enum PayloadKind
{
MainMapData,
GameStartMessage,
ForceUpdateMessage,
}
private enum CompressionKind
{
RuntimeDeflateCodec,
GZipFastest,
GZipOptimal,
DeflateFastest,
DeflateOptimal,
}
private class CompressionResult
{
public string Name;
public int RawBytes;
public int CompressedBytes;
public int DecompressedBytes;
public double CompressMs;
public double DecompressMs;
public double DeserializeMs;
public bool BytesEqual;
public bool DeserializeOk;
public string Error;
}
private PayloadKind _payloadKind = PayloadKind.MainMapData;
private int _targetReceiverCount = 3;
private bool _verifyDeserialize = true;
private Vector2 _reportScrollPosition;
private GUIStyle _reportStyle;
private readonly List<CompressionResult> _results = new List<CompressionResult>();
private string _lastReport = "尚未测试";
[MenuItem("Tools/Steam/MapData 压缩率测试")]
private static void ShowWindow()
{
var window = GetWindow<MapDataCompressionEditorWindow>();
window.titleContent = new GUIContent("MapData 压缩率");
window.minSize = new Vector2(620, 500);
window.Show();
}
private void OnGUI()
{
DrawOptions();
EditorGUILayout.Space(8);
DrawRunButtons();
EditorGUILayout.Space(8);
DrawResults();
}
private void DrawOptions()
{
EditorGUILayout.LabelField("MapData 压缩率测试", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("运行时默认测试 Main.MapData。Runtime Deflate Codec 是正式联机发送链路当前使用的压缩格式。", MessageType.Info);
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
_payloadKind = (PayloadKind)EditorGUILayout.EnumPopup("测试内容", _payloadKind);
_targetReceiverCount = Mathf.Max(1, EditorGUILayout.IntField("接收端人数", _targetReceiverCount));
_verifyDeserialize = EditorGUILayout.Toggle("解压后反序列化验证", _verifyDeserialize);
var mapData = Main.MapData;
EditorGUILayout.LabelField($"Main.MapData: {(mapData == null ? "null" : "")}");
if (mapData?.Net != null)
{
EditorGUILayout.LabelField($"NetMode: {mapData.Net.Mode}");
EditorGUILayout.LabelField($"ActionCount: {mapData.Net.Actions?.Count ?? 0}");
}
var lobby = LobbyManager.Instance?.Lobby;
if (lobby != null && lobby.IsInLobby())
{
var liveTargets = Mathf.Max(1, lobby.GetMemberCount() - 1);
if (GUILayout.Button($"使用当前房间接收人数: {liveTargets}"))
{
_targetReceiverCount = liveTargets;
}
}
}
}
private void DrawRunButtons()
{
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
GUI.enabled = Main.MapData != null;
if (GUILayout.Button("测试 Main.MapData 压缩率", GUILayout.Height(42)))
{
RunAllCases();
}
GUI.enabled = true;
if (GUILayout.Button("复制上次报告"))
{
EditorGUIUtility.systemCopyBuffer = _lastReport;
}
}
}
private void DrawResults()
{
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
EditorGUILayout.LabelField("结果", EditorStyles.boldLabel);
var style = GetReportStyle();
_reportScrollPosition = EditorGUILayout.BeginScrollView(_reportScrollPosition, GUILayout.MinHeight(220), GUILayout.ExpandHeight(true));
EditorGUILayout.SelectableLabel(_lastReport, style, GUILayout.Height(GetReportHeight(style)));
EditorGUILayout.EndScrollView();
}
}
private void RunAllCases()
{
_results.Clear();
try
{
var rawBytes = BuildPayloadBytes();
if (rawBytes == null || rawBytes.Length == 0)
{
_lastReport = "测试失败: 原始数据为空";
return;
}
RunCase(rawBytes, CompressionKind.RuntimeDeflateCodec);
RunCase(rawBytes, CompressionKind.GZipFastest);
RunCase(rawBytes, CompressionKind.GZipOptimal);
RunCase(rawBytes, CompressionKind.DeflateFastest);
RunCase(rawBytes, CompressionKind.DeflateOptimal);
_lastReport = BuildReport();
UnityEngine.Debug.Log(_lastReport);
}
catch (Exception e)
{
_lastReport = $"测试失败: {e}";
UnityEngine.Debug.LogError(_lastReport);
}
}
private byte[] BuildPayloadBytes()
{
var mapData = Main.MapData;
if (mapData == null) return null;
switch (_payloadKind)
{
case PayloadKind.GameStartMessage:
return MemoryPackSerializer.Serialize<BaseMessage>(new GameStartMessage { MapData = mapData });
case PayloadKind.ForceUpdateMessage:
return MemoryPackSerializer.Serialize<BaseMessage>(new ForceUpdateMessage { MapData = mapData });
default:
return MemoryPackSerializer.Serialize(mapData);
}
}
private void RunCase(byte[] rawBytes, CompressionKind kind)
{
var result = new CompressionResult
{
Name = GetCaseName(kind),
RawBytes = rawBytes.Length,
};
try
{
var stopwatch = Stopwatch.StartNew();
var compressed = Compress(rawBytes, kind);
stopwatch.Stop();
result.CompressMs = stopwatch.Elapsed.TotalMilliseconds;
result.CompressedBytes = compressed.Length;
stopwatch.Restart();
var decompressed = Decompress(compressed, kind);
stopwatch.Stop();
result.DecompressMs = stopwatch.Elapsed.TotalMilliseconds;
result.DecompressedBytes = decompressed.Length;
result.BytesEqual = BytesEqual(rawBytes, decompressed);
if (_verifyDeserialize && result.BytesEqual)
{
stopwatch.Restart();
result.DeserializeOk = VerifyDeserialize(decompressed);
stopwatch.Stop();
result.DeserializeMs = stopwatch.Elapsed.TotalMilliseconds;
}
else
{
result.DeserializeOk = !_verifyDeserialize;
}
}
catch (Exception e)
{
result.Error = e.Message;
}
_results.Add(result);
}
private static byte[] Compress(byte[] rawBytes, CompressionKind kind)
{
if (kind == CompressionKind.RuntimeDeflateCodec)
{
return NetworkPayloadCodec.EncodeForDiagnostics(rawBytes, true);
}
using (var output = new MemoryStream())
{
using (var stream = CreateCompressionStream(output, kind))
{
stream.Write(rawBytes, 0, rawBytes.Length);
}
return output.ToArray();
}
}
private static byte[] Decompress(byte[] compressedBytes, CompressionKind kind)
{
if (kind == CompressionKind.RuntimeDeflateCodec)
{
return NetworkPayloadCodec.DecodeIfNeeded(compressedBytes);
}
using (var input = new MemoryStream(compressedBytes))
using (var stream = CreateDecompressionStream(input, kind))
using (var output = new MemoryStream())
{
stream.CopyTo(output);
return output.ToArray();
}
}
private bool VerifyDeserialize(byte[] bytes)
{
switch (_payloadKind)
{
case PayloadKind.GameStartMessage:
case PayloadKind.ForceUpdateMessage:
return MemoryPackSerializer.Deserialize<BaseMessage>(bytes) != null;
default:
return MemoryPackSerializer.Deserialize<MapData>(bytes) != null;
}
}
private static Stream CreateCompressionStream(Stream output, CompressionKind kind)
{
switch (kind)
{
case CompressionKind.RuntimeDeflateCodec:
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
case CompressionKind.GZipFastest:
return new GZipStream(output, System.IO.Compression.CompressionLevel.Fastest);
case CompressionKind.GZipOptimal:
return new GZipStream(output, System.IO.Compression.CompressionLevel.Optimal);
case CompressionKind.DeflateFastest:
return new DeflateStream(output, System.IO.Compression.CompressionLevel.Fastest);
case CompressionKind.DeflateOptimal:
return new DeflateStream(output, System.IO.Compression.CompressionLevel.Optimal);
default:
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
}
}
private static Stream CreateDecompressionStream(Stream input, CompressionKind kind)
{
switch (kind)
{
case CompressionKind.GZipFastest:
case CompressionKind.GZipOptimal:
return new GZipStream(input, CompressionMode.Decompress);
case CompressionKind.RuntimeDeflateCodec:
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
case CompressionKind.DeflateFastest:
case CompressionKind.DeflateOptimal:
return new DeflateStream(input, CompressionMode.Decompress);
default:
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
}
}
private string BuildReport()
{
var builder = new StringBuilder();
builder.AppendLine($"MapData 压缩率测试 - {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
builder.AppendLine($"测试内容: {_payloadKind}");
builder.AppendLine($"接收端人数: {_targetReceiverCount}");
builder.AppendLine();
foreach (var result in _results)
{
builder.AppendLine($"[{result.Name}]");
if (!string.IsNullOrEmpty(result.Error))
{
builder.AppendLine($"失败: {result.Error}");
builder.AppendLine();
continue;
}
var savedBytes = result.RawBytes - result.CompressedBytes;
var savedPercent = result.RawBytes > 0 ? savedBytes * 100.0 / result.RawBytes : 0;
var ratio = result.RawBytes > 0 ? result.CompressedBytes * 100.0 / result.RawBytes : 0;
var rawTotal = (long)result.RawBytes * _targetReceiverCount;
var compressedTotal = (long)result.CompressedBytes * _targetReceiverCount;
var totalSaved = rawTotal - compressedTotal;
builder.AppendLine($"原始: {FormatBytes(result.RawBytes)} ({result.RawBytes:N0} bytes)");
builder.AppendLine($"压缩后: {FormatBytes(result.CompressedBytes)} ({result.CompressedBytes:N0} bytes)");
builder.AppendLine($"单份降低: {FormatBytes(savedBytes)} / {savedPercent:F2}%");
builder.AppendLine($"压缩后占原始: {ratio:F2}%");
builder.AppendLine($"房主发送 {_targetReceiverCount} 份: {FormatBytes(rawTotal)} -> {FormatBytes(compressedTotal)}, 少发 {FormatBytes(totalSaved)}");
builder.AppendLine($"耗时: 压缩 {result.CompressMs:F2} ms, 解压 {result.DecompressMs:F2} ms, 反序列化验证 {result.DeserializeMs:F2} ms");
builder.AppendLine($"校验: 解压字节一致={result.BytesEqual}, 反序列化={result.DeserializeOk}");
builder.AppendLine();
}
return builder.ToString();
}
private static string GetCaseName(CompressionKind kind)
{
switch (kind)
{
case CompressionKind.RuntimeDeflateCodec:
return "Runtime Deflate Codec";
case CompressionKind.GZipFastest:
return "GZip Fastest";
case CompressionKind.GZipOptimal:
return "GZip Optimal";
case CompressionKind.DeflateFastest:
return "Deflate Fastest";
case CompressionKind.DeflateOptimal:
return "Deflate Optimal";
default:
return kind.ToString();
}
}
private static bool BytesEqual(byte[] left, byte[] right)
{
if (left == null || right == null || left.Length != right.Length) return false;
for (var i = 0; i < left.Length; i++)
{
if (left[i] != right[i]) return false;
}
return true;
}
private GUIStyle GetReportStyle()
{
if (_reportStyle != null) return _reportStyle;
_reportStyle = new GUIStyle(EditorStyles.textArea)
{
alignment = TextAnchor.UpperLeft,
richText = false,
stretchHeight = false,
wordWrap = true,
};
return _reportStyle;
}
private float GetReportHeight(GUIStyle style)
{
var report = _lastReport ?? string.Empty;
var width = Mathf.Max(100f, position.width - 52f);
return Mathf.Max(220f, style.CalcHeight(new GUIContent(report), width) + 12f);
}
private static string FormatBytes(long bytes)
{
string[] units = { "B", "KB", "MB", "GB" };
var value = (double)bytes;
var unitIndex = 0;
while (value >= 1024 && unitIndex < units.Length - 1)
{
value /= 1024;
unitIndex++;
}
return $"{value:F2} {units[unitIndex]}";
}
}
}