/* * @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 _results = new List(); private string _lastReport = "尚未测试"; [MenuItem("Tools/Steam/MapData 压缩率测试")] private static void ShowWindow() { var window = GetWindow(); 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(new GameStartMessage { MapData = mapData }); case PayloadKind.ForceUpdateMessage: return MemoryPackSerializer.Serialize(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(bytes) != null; default: return MemoryPackSerializer.Deserialize(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]}"; } } }