MeowmentDesign/Assets/Editor/CustomTools/ThriftPipeline/ThriftBytesGeneratorEditor.cs
2026-02-01 15:37:46 +08:00

716 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
{
/// <summary>
/// 快速生成 Bytes 工具(简化版)
/// 只生成 AllConfigs.bytes不生成 C# 和 DR
/// 适用于表结构未变化,只需要更新数据的场景
/// </summary>
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("蹊径/Thrift/快速生成 Bytes简化版")]
public static void ShowWindow()
{
var window = GetWindow<ThriftBytesGeneratorEditor>("快速生成 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();
}
/// <summary>
/// 绘制路径配置区域
/// </summary>
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();
}
/// <summary>
/// 绘制执行区域
/// </summary>
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();
}
/// <summary>
/// 绘制日志区域
/// </summary>
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();
}
/// <summary>
/// 加载配置(使用 EditorPrefs本地保存不会被git提交
/// </summary>
private void LoadConfig()
{
try
{
docsProjectPath = EditorPrefs.GetString(PREF_KEY_DOCS_PATH, "");
}
catch (Exception ex)
{
Debug.LogError($"加载配置失败: {ex.Message}");
}
}
/// <summary>
/// 保存配置(使用 EditorPrefs本地保存不会被git提交
/// </summary>
private void SaveConfig()
{
try
{
EditorPrefs.SetString(PREF_KEY_DOCS_PATH, docsProjectPath);
}
catch (Exception ex)
{
Debug.LogError($"保存配置失败: {ex.Message}");
}
}
/// <summary>
/// 获取所有派生路径
/// </summary>
private Dictionary<string, string> GetDerivedPaths()
{
var paths = new Dictionary<string, string>();
// 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;
}
/// <summary>
/// 验证路径
/// </summary>
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;
}
/// <summary>
/// 日志输出
/// </summary>
private void Log(string message)
{
logBuilder.AppendLine(message);
Repaint();
}
/// <summary>
/// 生成 Bytes 文件
/// </summary>
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();
}
}
/// <summary>
/// 生成 Bytes 文件
/// </summary>
private async Task<bool> GenerateBytesFile(Dictionary<string, string> 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<JObject>(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 TBinaryProtocol(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;
}
}
/// <summary>
/// 填充Thrift对象数据
/// </summary>
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<string> headers = new List<string>();
List<int> validColumns = new List<int>();
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;
}
}
/// <summary>
/// 清理并转换字段名为PascalCase
/// </summary>
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;
}
/// <summary>
/// 转换单元格值
/// </summary>
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;
}
}
}
}