using UnityEngine; using UnityEditor; using System; using System.IO; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OfficeOpenXml; using Thrift.Protocol; using Thrift.Transport.Client; using System.Threading; namespace ThriftPipelineTools { /// /// 快速生成 Bytes 工具(简化版) /// 只生成 AllConfigs.bytes,不生成 C# 和 DR /// 适用于表结构未变化,只需要更新数据的场景 /// public class ThriftBytesGeneratorEditor : EditorWindow { // ========== 版本信息 ========== private const string VERSION = "v1.0.0 (Bytes Only)"; // ========== 配置Key(使用EditorPrefs) ========== private const string PREF_KEY_DOCS_PATH = "ThriftPipeline_DocsPath"; // ========== 路径配置 ========== private string docsProjectPath = ""; // Docs 项目路径 // ========== UI 状态 ========== private Vector2 scrollPosition; private Vector2 logScrollPosition; private StringBuilder logBuilder = new StringBuilder(); private bool isExecuting = false; // ========== 进度控制 ========== private float progress = 0f; private string currentStep = ""; [MenuItem("配置表pipeline/Thrift/快速生成 Bytes(简化版)")] public static void ShowWindow() { var window = GetWindow("快速生成 Bytes"); window.minSize = new Vector2(800, 600); window.Show(); } private void OnEnable() { LoadConfig(); } private void OnGUI() { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); EditorGUILayout.Space(10); EditorGUILayout.LabelField($"快速生成 Bytes 工具 - {VERSION}", EditorStyles.boldLabel); EditorGUILayout.HelpBox( "此工具只生成 AllConfigs.bytes 文件,不生成 C# 和 DR\n" + "适用于:表结构未变化,只需要更新配置数据的场景\n" + "⚡ 执行速度更快,跳过编译和代码生成步骤", MessageType.Info ); EditorGUILayout.Space(10); DrawPathConfiguration(); EditorGUILayout.Space(10); DrawExecutionSection(); EditorGUILayout.Space(10); DrawLogSection(); EditorGUILayout.EndScrollView(); } /// /// 绘制路径配置区域 /// private void DrawPathConfiguration() { EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("路径配置", EditorStyles.boldLabel); EditorGUILayout.HelpBox( "只需配置 Docs 项目路径(包含 Excel 配置和 cfg_txt.json)", MessageType.None ); // Docs 项目路径 EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Docs 项目路径:", GUILayout.Width(120)); docsProjectPath = EditorGUILayout.TextField(docsProjectPath); if (GUILayout.Button("选择", GUILayout.Width(60))) { string selected = EditorUtility.OpenFolderPanel("选择 Docs 项目路径", docsProjectPath, ""); if (!string.IsNullOrEmpty(selected)) { docsProjectPath = selected; SaveConfig(); } } EditorGUILayout.EndHorizontal(); // 显示派生路径预览 if (!string.IsNullOrEmpty(docsProjectPath)) { EditorGUILayout.Space(5); EditorGUILayout.LabelField("派生路径预览:", EditorStyles.miniLabel); var paths = GetDerivedPaths(); EditorGUILayout.LabelField($" cfg_txt.json: {paths["cfg_json"]}", EditorStyles.miniLabel); EditorGUILayout.LabelField($" Config 目录: {paths["config_dir"]}", EditorStyles.miniLabel); EditorGUILayout.LabelField($" 输出到 Unity: {paths["unity_bytes_dir"]}", EditorStyles.miniLabel); } EditorGUILayout.EndVertical(); } /// /// 绘制执行区域 /// private void DrawExecutionSection() { EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("执行控制", EditorStyles.boldLabel); GUI.enabled = !isExecuting && ValidatePaths(); if (GUILayout.Button("⚡ 快速生成 AllConfigs.bytes", GUILayout.Height(40))) { GenerateBytes(); } GUI.enabled = true; if (isExecuting) { EditorGUILayout.Space(5); EditorGUILayout.LabelField($"当前步骤: {currentStep}"); EditorGUI.ProgressBar(EditorGUILayout.GetControlRect(GUILayout.Height(20)), progress, "执行中..."); } EditorGUILayout.EndVertical(); } /// /// 绘制日志区域 /// private void DrawLogSection() { EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("执行日志", EditorStyles.boldLabel); logScrollPosition = EditorGUILayout.BeginScrollView( logScrollPosition, GUILayout.Height(300) ); EditorGUILayout.TextArea(logBuilder.ToString(), GUILayout.ExpandHeight(true)); EditorGUILayout.EndScrollView(); if (GUILayout.Button("清空日志")) { logBuilder.Clear(); } EditorGUILayout.EndVertical(); } /// /// 加载配置(使用 EditorPrefs,本地保存,不会被git提交) /// private void LoadConfig() { try { docsProjectPath = EditorPrefs.GetString(PREF_KEY_DOCS_PATH, ""); } catch (Exception ex) { Debug.LogError($"加载配置失败: {ex.Message}"); } } /// /// 保存配置(使用 EditorPrefs,本地保存,不会被git提交) /// private void SaveConfig() { try { EditorPrefs.SetString(PREF_KEY_DOCS_PATH, docsProjectPath); } catch (Exception ex) { Debug.LogError($"保存配置失败: {ex.Message}"); } } /// /// 获取所有派生路径 /// private Dictionary GetDerivedPaths() { var paths = new Dictionary(); // Docs 相关路径 if (!string.IsNullOrEmpty(docsProjectPath)) { paths["cfg_json"] = Path.Combine(docsProjectPath, "tool", "cfg", "cfg_txt.json"); paths["config_dir"] = Path.Combine(docsProjectPath, "config"); } // Unity 项目相关路径(当前项目) string assetsPath = Application.dataPath; paths["unity_bytes_dir"] = Path.Combine(assetsPath, "Design_SubModule", "ConfigData"); return paths; } /// /// 验证路径 /// private bool ValidatePaths() { if (string.IsNullOrEmpty(docsProjectPath)) return false; var paths = GetDerivedPaths(); if (!File.Exists(paths["cfg_json"])) return false; if (!Directory.Exists(paths["config_dir"])) return false; return true; } /// /// 日志输出 /// private void Log(string message) { logBuilder.AppendLine(message); Repaint(); } /// /// 生成 Bytes 文件 /// private async void GenerateBytes() { isExecuting = true; logBuilder.Clear(); progress = 0f; DateTime startTime = DateTime.Now; Log("================================================================================"); Log($"开始生成 AllConfigs.bytes ({VERSION})"); Log("================================================================================"); Log(""); try { var paths = GetDerivedPaths(); currentStep = "读取配置并生成 bytes"; progress = 0.3f; Repaint(); if (!await GenerateBytesFile(paths)) { Log("[FAIL] 生成失败"); EditorUtility.DisplayDialog("失败", "生成 AllConfigs.bytes 失败,请查看日志", "确定"); return; } progress = 1f; currentStep = "完成"; Repaint(); DateTime endTime = DateTime.Now; TimeSpan duration = endTime - startTime; Log(""); Log("================================================================================"); Log($"✅ 生成完成!"); Log($" 耗时: {duration.TotalSeconds:F2} 秒"); Log($" 输出: {paths["unity_bytes_dir"]}/AllConfigs.bytes"); Log("================================================================================"); EditorUtility.DisplayDialog("成功", $"AllConfigs.bytes 生成完成!\n耗时: {duration.TotalSeconds:F2} 秒", "确定"); } catch (Exception ex) { Log($"\n[FAIL] 执行失败: {ex.Message}"); Log(ex.StackTrace); EditorUtility.DisplayDialog("错误", $"执行失败:\n{ex.Message}", "确定"); } finally { isExecuting = false; AssetDatabase.Refresh(); Repaint(); } } /// /// 生成 Bytes 文件 /// private async Task GenerateBytesFile(Dictionary paths) { Log("【生成 AllConfigs.bytes】"); Log("--------------------------------------------------------------------------------"); try { string cfgJsonPath = paths["cfg_json"]; string configDir = paths["config_dir"]; string bytesOutputDir = paths["unity_bytes_dir"]; // 创建输出目录 if (!Directory.Exists(bytesOutputDir)) Directory.CreateDirectory(bytesOutputDir); // 加载程序集 Log(" [1/3] 加载程序集"); System.Reflection.Assembly assembly = null; foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) { if (asm.GetType("Byway.Thrift.Data.AllConfigs") != null) { assembly = asm; break; } } if (assembly == null) { Log("[FAIL] 找不到 Byway.Thrift.Data.AllConfigs 程序集"); Log(" 请确保已经执行过完整流程,生成了 C# 类"); return false; } Log(" [OK] 程序集加载成功"); // 读取配置 Log(" [2/3] 读取配置并填充数据"); string cfgText = File.ReadAllText(cfgJsonPath); var cfg = JsonConvert.DeserializeObject(cfgText); var fileList = cfg["file_list"] as JArray; // 创建AllConfigs实例 var allConfigsType = assembly.GetType("Byway.Thrift.Data.AllConfigs"); object allConfigsInstance = Activator.CreateInstance(allConfigsType); int successCount = 0; int processedCount = 0; // 填充每个配置 foreach (JObject configItem in fileList) { string inFile = configItem["in_file"]?.ToString(); string outFile = configItem["out_file"]?.ToString(); string sheetName = configItem["sheet_name"]?.ToString(); var columnTypes = configItem["coloum_type"] as JArray; if (string.IsNullOrEmpty(inFile) || string.IsNullOrEmpty(outFile)) continue; string structName = outFile.Replace(".txt", ""); string excelPath = Path.Combine(configDir, inFile); if (!File.Exists(excelPath)) { Log($" [{++processedCount}] [SKIP] {structName} - Excel文件不存在"); continue; } Log($" [{++processedCount}] 处理: {structName}"); if (FillThriftObject(allConfigsInstance, allConfigsType, structName, excelPath, sheetName, columnTypes, assembly)) { successCount++; } } // 序列化为bytes Log($"\n [3/3] 序列化 AllConfigs.bytes (已设置 {successCount} 个配置)"); // 检查有多少配置实际被设置 int setCount = 0; var issetField = allConfigsType.GetField("__isset", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); if (issetField != null) { var issetObj = issetField.GetValue(allConfigsInstance); var issetType = issetObj.GetType(); foreach (var prop in allConfigsType.GetProperties()) { if (prop.Name != "Isset" && prop.PropertyType.Namespace == "Byway.Thrift.Data") { var issetProp = issetType.GetProperty(prop.Name); if (issetProp != null && (bool)issetProp.GetValue(issetObj)) setCount++; } } } Log($" __isset 标志统计: {setCount} 个配置被标记为已设置"); using (var memoryStream = new MemoryStream()) { using (var transport = new TStreamTransport(memoryStream, memoryStream, new Thrift.TConfiguration())) { using (var protocol = new TCompactProtocol(transport)) { var writeMethod = allConfigsType.GetMethod("WriteAsync"); if (writeMethod != null) { var task = (Task)writeMethod.Invoke(allConfigsInstance, new object[] { protocol, CancellationToken.None }); await task; byte[] bytes = memoryStream.ToArray(); string allConfigsBytesPath = Path.Combine(bytesOutputDir, "AllConfigs.bytes"); File.WriteAllBytes(allConfigsBytesPath, bytes); Log($" [OK] 生成成功 ({bytes.Length} bytes / {bytes.Length / 1024.0:F2} KB)"); if (bytes.Length < 1000) { Log($" [WARNING] 文件太小 ({bytes.Length} bytes),可能大部分配置未序列化!"); } } } } } Log($"\n生成完成: 成功 {successCount} 个配置"); return true; } catch (Exception ex) { Log($"[FAIL] 生成失败: {ex.Message}"); Log(ex.StackTrace); return false; } } /// /// 填充Thrift对象数据 /// private bool FillThriftObject(object allConfigsInstance, Type allConfigsType, string structName, string excelPath, string sheetName, JArray columnTypes, System.Reflection.Assembly assembly) { try { var configType = assembly.GetType($"Byway.Thrift.Data.{structName}"); var itemType = assembly.GetType($"Byway.Thrift.Data.{structName}Item"); if (configType == null || itemType == null) { Log($" [ERROR] 找不到类型: {structName} 或 {structName}Item"); return false; } object configInstance = Activator.CreateInstance(configType); var dictType = typeof(Dictionary<,>).MakeGenericType(typeof(int), itemType); var dictInstance = Activator.CreateInstance(dictType); // 读取Excel FileInfo fileInfo = new FileInfo(excelPath); using (ExcelPackage package = new ExcelPackage(fileInfo)) { ExcelWorksheet worksheet = null; if (!string.IsNullOrEmpty(sheetName) && package.Workbook.Worksheets[sheetName] != null) worksheet = package.Workbook.Worksheets[sheetName]; else worksheet = package.Workbook.Worksheets[0]; if (worksheet == null || worksheet.Dimension == null) { Log($" [ERROR] 工作表为空"); return false; } int maxRows = worksheet.Dimension.End.Row; int maxCols = worksheet.Dimension.End.Column; // 读取表头(第1行) List headers = new List(); List validColumns = new List(); int maxColumns = columnTypes?.Count ?? maxCols; for (int col = 1; col <= maxCols && headers.Count < maxColumns; col++) { string header = worksheet.Cells[1, col].Value?.ToString() ?? ""; if (!string.IsNullOrWhiteSpace(header)) { headers.Add(header); validColumns.Add(col); } } // 读取数据(从第3行开始) int filledItemCount = 0; for (int row = 3; row <= maxRows; row++) { bool isEmptyRow = true; for (int i = 0; i < validColumns.Count; i++) { if (worksheet.Cells[row, validColumns[i]].Value != null) { isEmptyRow = false; break; } } if (isEmptyRow) continue; object itemInstance = Activator.CreateInstance(itemType); int? idValue = null; for (int i = 0; i < headers.Count; i++) { string originalHeader = headers[i]; string fieldName = SanitizeFieldName(originalHeader); int colIndex = validColumns[i]; var cell = worksheet.Cells[row, colIndex]; object cellValue = cell.Value; var property = itemType.GetProperty(fieldName); if (property != null) { try { object convertedValue; if (cellValue == null) { if (property.PropertyType == typeof(string)) { convertedValue = ""; } else if (property.PropertyType.IsValueType) { convertedValue = Activator.CreateInstance(property.PropertyType); } else { continue; } } else { convertedValue = ConvertCellValue(cellValue, property.PropertyType); } property.SetValue(itemInstance, convertedValue); if (fieldName.ToLower() == "id" && convertedValue is int) idValue = (int)convertedValue; } catch (Exception ex) { Log($" [WARN] 行{row} 字段{fieldName}转换失败: {ex.Message}"); } } } if (idValue.HasValue) { var addMethod = dictType.GetMethod("Add"); addMethod.Invoke(dictInstance, new object[] { idValue.Value, itemInstance }); filledItemCount++; } } Log($" 读取了 {filledItemCount} 条数据"); } // 设置字典属性 string dictPropertyName = char.ToUpper(structName[0]) + structName.Substring(1).ToLower() + "s"; var dictProperty = configType.GetProperty(dictPropertyName); if (dictProperty != null) { dictProperty.SetValue(configInstance, dictInstance); } else { Log($" [ERROR] 未找到字典属性: {dictPropertyName}"); return false; } // 设置到AllConfigs var allConfigProperty = allConfigsType.GetProperty(structName); if (allConfigProperty != null) { allConfigProperty.SetValue(allConfigsInstance, configInstance); var verifyValue = allConfigProperty.GetValue(allConfigsInstance); if (verifyValue == null) { Log($" [ERROR] 设置到AllConfigs后为null!"); return false; } Log($" [OK] 成功"); } else { Log($" [ERROR] AllConfigs中未找到属性: {structName}"); return false; } return true; } catch (Exception ex) { Log($" [ERROR] {structName}: {ex.Message}"); return false; } } /// /// 清理并转换字段名为PascalCase /// private string SanitizeFieldName(string fieldName) { if (string.IsNullOrWhiteSpace(fieldName)) return "Field_unknown"; StringBuilder sb = new StringBuilder(); foreach (char c in fieldName) { if (char.IsLetterOrDigit(c) || c == '_') sb.Append(c); else if (c == ' ' || c == '-') sb.Append('_'); } string cleaned = sb.ToString(); if (string.IsNullOrEmpty(cleaned)) return "Field_unknown"; if (cleaned.Length > 0) { return char.ToUpper(cleaned[0]) + cleaned.Substring(1); } return cleaned; } /// /// 转换单元格值 /// private object ConvertCellValue(object cellValue, Type targetType) { try { if (targetType == typeof(int)) { if (cellValue is double d) return (int)d; return Convert.ToInt32(cellValue); } else if (targetType == typeof(long)) { if (cellValue is double d) return (long)d; return Convert.ToInt64(cellValue); } else if (targetType == typeof(double)) { return Convert.ToDouble(cellValue); } else if (targetType == typeof(bool)) { return Convert.ToBoolean(cellValue); } else if (targetType == typeof(string)) { return cellValue.ToString(); } return cellValue; } catch { if (targetType == typeof(int)) return 0; if (targetType == typeof(long)) return 0L; if (targetType == typeof(double)) return 0.0; if (targetType == typeof(bool)) return false; if (targetType == typeof(string)) return ""; return null; } } } }