MeowmentArt/Assets/Editor/CustomTools/ThriftPipeline/ThriftIntegratedPipelineEditor.cs
2026-02-01 13:52:10 +08:00

2068 lines
87 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 System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using OfficeOpenXml;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Thrift.Protocol;
using Thrift.Transport.Client;
using Debug = UnityEngine.Debug;
namespace ThriftPipelineTools
{
/// <summary>
/// 导入 Docs 配置表工具
/// 完全复刻 integrated_pipeline.py 的功能
/// 从 Excel 到 Unity 的一站式处理
/// </summary>
public class ThriftIntegratedPipelineEditor : EditorWindow
{
// ========== 版本信息 ==========
private const string VERSION = "v1.0.0 (C# Edition)";
// ========== 路径配置 ==========
private string docsProjectPath = ""; // Docs 项目路径
private string thriftProjectPath = ""; // Thrift 项目路径
// ========== UI 状态 ==========
private Vector2 scrollPosition;
private Vector2 logScrollPosition;
private StringBuilder logBuilder = new StringBuilder();
private bool isExecuting = false;
// ========== 进度控制 ==========
private float progress = 0f;
private string currentStep = "";
private int totalSteps = 7;
private int completedSteps = 0;
// ========== 报告数据 ==========
private List<StepReport> reportSteps = new List<StepReport>();
private DateTime startTime;
private DateTime endTime;
// ========== 配置Key使用EditorPrefs ==========
private const string PREF_KEY_DOCS_PATH = "ThriftPipeline_DocsPath";
private const string PREF_KEY_THRIFT_PATH = "ThriftPipeline_ThriftPath";
// ========== 临时数据 ==========
private List<string> configStructNames = new List<string>();
[MenuItem("蹊径/Thrift/导入 Docs 配置表工具")]
public static void ShowWindow()
{
var window = GetWindow<ThriftIntegratedPipelineEditor>("导入 Docs 配置表工具");
window.minSize = new Vector2(1000, 800);
window.Show();
}
private void OnEnable()
{
LoadConfig();
}
private void OnGUI()
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
EditorGUILayout.Space(10);
EditorGUILayout.LabelField($"导入 Docs 配置表工具 - {VERSION}", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"此工具完全复刻 integrated_pipeline.py 的功能\n" +
"从 Excel 配置文件生成 Thrift 定义、编译为 C# 代码、生成二进制数据、创建 DataRow 类\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(
"配置两个基础路径,其他路径将自动推导\n" +
"- Docs 项目路径: 包含 Excel 配置和 cfg_txt.json\n" +
"- Thrift 项目路径: 包含 thrift.exe 编译器和 thrift 文件目录",
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();
// Thrift 项目路径
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Thrift 项目路径:", GUILayout.Width(120));
thriftProjectPath = EditorGUILayout.TextField(thriftProjectPath);
if (GUILayout.Button("选择", GUILayout.Width(60)))
{
string selected = EditorUtility.OpenFolderPanel("选择 Thrift 项目路径", thriftProjectPath, "");
if (!string.IsNullOrEmpty(selected))
{
thriftProjectPath = selected;
SaveConfig();
}
}
EditorGUILayout.EndHorizontal();
// 显示派生路径预览
if (!string.IsNullOrEmpty(docsProjectPath) && !string.IsNullOrEmpty(thriftProjectPath))
{
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($" Thrift 编译器: {paths["compiler"]}", EditorStyles.miniLabel);
EditorGUILayout.LabelField($" 输出到 Unity C#: {paths["unity_csharp_dir"]}", EditorStyles.miniLabel);
EditorGUILayout.LabelField($" 输出到 Unity Bytes: {paths["unity_bytes_dir"]}", EditorStyles.miniLabel);
EditorGUILayout.LabelField($" 输出到 Unity DR: {paths["dr_output_dir"]}", EditorStyles.miniLabel);
}
EditorGUILayout.EndVertical();
}
/// <summary>
/// 绘制执行区域
/// </summary>
private void DrawExecutionSection()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("执行控制", EditorStyles.boldLabel);
GUI.enabled = !isExecuting && ValidatePaths();
if (GUILayout.Button("🚀 开始执行完整流程", GUILayout.Height(40)))
{
ExecutePipeline();
}
GUI.enabled = true;
if (isExecuting)
{
EditorGUILayout.Space(5);
EditorGUILayout.LabelField($"当前步骤: {currentStep}");
EditorGUI.ProgressBar(EditorGUILayout.GetControlRect(GUILayout.Height(20)), progress, $"{completedSteps}/{totalSteps}");
}
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
{
// 从 EditorPrefs 读取路径(每台电脑独立保存)
docsProjectPath = EditorPrefs.GetString(PREF_KEY_DOCS_PATH, "");
thriftProjectPath = EditorPrefs.GetString(PREF_KEY_THRIFT_PATH, "");
}
catch (Exception ex)
{
Debug.LogError($"加载配置失败: {ex.Message}");
}
}
/// <summary>
/// 保存配置(使用 EditorPrefs本地保存不会被git提交
/// </summary>
private void SaveConfig()
{
try
{
// 保存到 EditorPrefs每台电脑独立保存不会被git追踪
EditorPrefs.SetString(PREF_KEY_DOCS_PATH, docsProjectPath);
EditorPrefs.SetString(PREF_KEY_THRIFT_PATH, thriftProjectPath);
}
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");
}
// Thrift 项目相关路径(包含中间输出目录)
if (!string.IsNullOrEmpty(thriftProjectPath))
{
paths["thrift_project_path"] = thriftProjectPath; // 添加项目根路径供Python脚本使用
paths["compiler"] = Path.Combine(thriftProjectPath, "compiler", "exe", "thrift.exe");
paths["thrift_dir"] = Path.Combine(thriftProjectPath, "thrift-files", "meowment");
paths["csharp_output_dir"] = Path.Combine(thriftProjectPath, "compiled_output", "csharp");
paths["bytes_output_dir"] = Path.Combine(thriftProjectPath, "binary_output");
}
// Unity 项目相关路径(当前项目)
string assetsPath = Application.dataPath;
paths["unity_csharp_dir"] = Path.Combine(assetsPath, "Design_SubModule", "Scripts", "thrift", "gen-netstd");
paths["unity_bytes_dir"] = Path.Combine(assetsPath, "Design_SubModule", "ConfigData");
paths["dr_output_dir"] = Path.Combine(assetsPath, "Design_SubModule", "Scripts", "DR_Generated");
return paths;
}
/// <summary>
/// 验证路径
/// </summary>
private bool ValidatePaths()
{
if (string.IsNullOrEmpty(docsProjectPath) || string.IsNullOrEmpty(thriftProjectPath))
return false;
var paths = GetDerivedPaths();
if (!File.Exists(paths["cfg_json"]))
return false;
if (!Directory.Exists(paths["config_dir"]))
return false;
if (!File.Exists(paths["compiler"]))
return false;
return true;
}
/// <summary>
/// 日志输出
/// </summary>
private void Log(string message)
{
logBuilder.AppendLine(message);
Repaint();
}
/// <summary>
/// 添加步骤报告
/// </summary>
private void AddStepReport(string stepName, bool success, Dictionary<string, object> details = null)
{
reportSteps.Add(new StepReport
{
Name = stepName,
Success = success,
Time = DateTime.Now,
Details = details ?? new Dictionary<string, object>()
});
}
/// <summary>
/// 更新进度
/// </summary>
private void UpdateProgress(int step, string stepName)
{
completedSteps = step;
currentStep = stepName;
progress = (float)completedSteps / totalSteps;
Repaint();
}
/// <summary>
/// 执行完整流程
/// </summary>
private async void ExecutePipeline()
{
isExecuting = true;
logBuilder.Clear();
reportSteps.Clear();
completedSteps = 0;
startTime = DateTime.Now;
Log("================================================================================");
Log($"开始执行完整流程 ({VERSION})");
Log("================================================================================");
Log("");
try
{
var paths = GetDerivedPaths();
// 步骤1: 生成 Thrift 文件
UpdateProgress(0, "步骤1: 生成 Thrift 文件");
if (!await Step1_GenerateThriftFiles(paths))
{
Log("[FAIL] 步骤1失败终止执行");
return;
}
// 步骤2: 编译 Thrift 到 C#
UpdateProgress(1, "步骤2: 编译 Thrift 到 C#");
if (!await Step2_CompileToCSharp(paths))
{
Log("[FAIL] 步骤2失败终止执行");
return;
}
// 步骤3: 生成 Bytes 文件
UpdateProgress(2, "步骤3: 生成 Bytes 文件");
if (!await Step3_GenerateBytes(paths))
{
Log("[FAIL] 步骤3失败终止执行");
return;
}
// 步骤4: 清理冲突文件
UpdateProgress(3, "步骤4: 清理冲突文件");
if (!Step4_CleanupExtensions(paths))
{
Log("[FAIL] 步骤4失败终止执行");
return;
}
// 步骤5: 复制 C# 到 Unity
UpdateProgress(4, "步骤5: 复制 C# 到 Unity");
if (!Step5_CopyCSharp(paths))
{
Log("[FAIL] 步骤5失败终止执行");
return;
}
// 步骤6: 复制 Bytes 到 Unity
UpdateProgress(5, "步骤6: 复制 Bytes 到 Unity");
if (!Step6_CopyBytes(paths))
{
Log("[FAIL] 步骤6失败终止执行");
return;
}
// 步骤7: 生成 DR 文件
UpdateProgress(6, "步骤7: 生成 DR 文件");
if (!Step7_GenerateDR(paths))
{
Log("[FAIL] 步骤7失败终止执行");
return;
}
UpdateProgress(7, "完成");
endTime = DateTime.Now;
// 生成报告
string report = GenerateReport();
Log("\n" + report);
// 保存完整日志到 ThriftPipeline/Log 目录
string logDir = Path.Combine(
Application.dataPath,
"Editor",
"CustomTools",
"ThriftPipeline",
"Log"
);
// 确保Log目录存在
if (!Directory.Exists(logDir))
{
Directory.CreateDirectory(logDir);
}
// 保存完整日志(详细日志 + 摘要报告)
string logFileName = $"ThriftPipeline_{DateTime.Now:yyyyMMdd_HHmmss}.txt";
string logFilePath = Path.Combine(logDir, logFileName);
StringBuilder fullLog = new StringBuilder();
fullLog.AppendLine("================================================================================");
fullLog.AppendLine(" 详细执行日志");
fullLog.AppendLine("================================================================================");
fullLog.AppendLine();
fullLog.Append(logBuilder.ToString());
fullLog.AppendLine();
fullLog.AppendLine();
fullLog.Append(report);
File.WriteAllText(logFilePath, fullLog.ToString());
Log($"\n日志已保存: Log/{logFileName}");
EditorUtility.DisplayDialog("成功", "流程执行完成!", "确定");
}
catch (Exception ex)
{
Log($"\n[FAIL] 执行失败: {ex.Message}");
Log(ex.StackTrace);
EditorUtility.DisplayDialog("错误", $"执行失败:\n{ex.Message}", "确定");
}
finally
{
isExecuting = false;
AssetDatabase.Refresh();
Repaint();
}
}
// ========== 步骤1: 生成 Thrift 文件 ==========
private async Task<bool> Step1_GenerateThriftFiles(Dictionary<string, string> paths)
{
Log("\n【步骤1】生成 Thrift 文件");
Log("--------------------------------------------------------------------------------");
try
{
string cfgJsonPath = paths["cfg_json"];
string configDir = paths["config_dir"];
string thriftDir = paths["thrift_dir"];
// 读取 cfg_txt.json
string cfgText = File.ReadAllText(cfgJsonPath);
var cfg = JsonConvert.DeserializeObject<JObject>(cfgText);
var fileList = cfg["file_list"] as JArray;
if (fileList == null || fileList.Count == 0)
{
Log("[FAIL] cfg_txt.json 中没有文件配置");
AddStepReport("生成Thrift文件", false);
return false;
}
// 创建 thrift 目录
if (!Directory.Exists(thriftDir))
Directory.CreateDirectory(thriftDir);
int generatedCount = 0;
List<string> structNames = new List<string>();
foreach (JObject configItem in fileList)
{
string inFile = configItem["in_file"]?.ToString();
string outFile = configItem["out_file"]?.ToString(); // 用这个生成 thrift 名称
string sheetName = configItem["sheet_name"]?.ToString();
var columnTypes = configItem["coloum_type"] as JArray;
if (string.IsNullOrEmpty(inFile) || string.IsNullOrEmpty(outFile))
continue;
// 从 out_file 获取结构名(与 Python 版本一致)
string structName = outFile.Replace(".txt", "");
string excelPath = Path.Combine(configDir, inFile);
if (!File.Exists(excelPath))
{
Log($" [SKIP] Excel 文件不存在: {inFile}");
continue;
}
Log($" 处理: {inFile} -> {structName}");
// 读取 Excel如果有 sheet_name 就用,否则用第一个 sheet
var (headers, types, comments) = ReadExcelHeaders(excelPath, sheetName, columnTypes);
if (headers == null || headers.Count == 0)
{
Log($" [SKIP] 无法读取表头: {structName}");
continue;
}
// 生成 thrift 内容
string thriftContent = GenerateThriftContent(structName, headers, types, comments);
// 写入文件
string thriftFilePath = Path.Combine(thriftDir, $"{structName}.thrift");
File.WriteAllText(thriftFilePath, thriftContent);
structNames.Add(structName);
generatedCount++;
Log($" [OK] 生成: {structName}.thrift");
}
// 生成 AllConfigs.thrift
if (structNames.Count > 0)
{
string allConfigsContent = GenerateAllConfigsContent(structNames);
string allConfigsPath = Path.Combine(thriftDir, "AllConfigs.thrift");
File.WriteAllText(allConfigsPath, allConfigsContent);
Log($" [OK] 生成: AllConfigs.thrift (包含 {structNames.Count} 个配置)");
}
// 保存结构名列表供后续步骤使用
configStructNames = structNames;
Log($"\n生成完成: {generatedCount} 个 thrift 文件");
AddStepReport("生成Thrift文件", true, new Dictionary<string, object>
{
{ "total", generatedCount },
{ "files", structNames }
});
return true;
}
catch (Exception ex)
{
Log($"[FAIL] 步骤1失败: {ex.Message}");
Log(ex.StackTrace);
AddStepReport("生成Thrift文件", false, new Dictionary<string, object>
{
{ "error", ex.Message }
});
return false;
}
}
/// <summary>
/// 读取 Excel 表头、类型和注释
/// 完全复刻 Python 的读取逻辑:跳过空表头列,而不是停止
/// </summary>
private (List<string> headers, List<string> types, List<string> comments) ReadExcelHeaders(
string excelPath, string sheetName, JArray columnTypes)
{
try
{
FileInfo fileInfo = new FileInfo(excelPath);
using (ExcelPackage package = new ExcelPackage(fileInfo))
{
// 如果指定了 sheet_name 就用,否则用第一个 sheet与 Python 版本一致)
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($" [WARN] 工作表 '{sheetName}' 不存在或为空");
return (null, null, null);
}
int maxCols = worksheet.Dimension.End.Column;
List<string> headers = new List<string>();
List<string> types = new List<string>();
List<string> comments = new List<string>();
// 只读取 coloum_types 定义的数量(与 Python 版本完全一致)
int maxColumns = columnTypes != null && columnTypes.Count > 0 ? columnTypes.Count : maxCols;
// Excel 结构(与 Python 版本完全一致):
// 第1行字段名表头
// 第2行注释说明
// 第3行开始数据
// EPPlus 索引从 1 开始
for (int col = 1; col <= maxCols; col++)
{
// 读取字段名第1行
string header = worksheet.Cells[1, col].Value?.ToString() ?? "";
// 重要:如果字段名为空,跳过这一列,而不是停止(与 Python 版本一致)
if (string.IsNullOrWhiteSpace(header))
{
continue;
}
headers.Add(header);
// 读取注释第2行
string comment = worksheet.Cells[2, col].Value?.ToString() ?? "";
comments.Add(comment);
// 读取类型(从 cfg_txt.json 的 coloum_type
int typeIndex = headers.Count - 1;
if (columnTypes != null && typeIndex < columnTypes.Count)
{
types.Add(columnTypes[typeIndex].ToString());
}
else
{
types.Add("string"); // 默认类型
}
// 达到 coloum_types 定义的数量后停止(关键逻辑)
if (headers.Count >= maxColumns)
{
break;
}
}
Log($" 读取到 {headers.Count} 个有效列");
return (headers, types, comments);
}
}
catch (Exception ex)
{
Log($" [ERROR] 读取 Excel 失败: {ex.Message}");
return (null, null, null);
}
}
/// <summary>
/// 生成 Thrift 文件内容
/// </summary>
private string GenerateThriftContent(string structName, List<string> headers, List<string> types, List<string> comments)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("namespace netstd Byway.Thrift.Data");
sb.AppendLine();
sb.AppendLine($"struct {structName}Item");
sb.AppendLine("{");
// 类型映射
Dictionary<string, string> typeMap = new Dictionary<string, string>
{
{ "int", "i32" },
{ "int32", "i32" },
{ "i32", "i32" },
{ "integer", "i32" },
{ "long", "i64" },
{ "int64", "i64" },
{ "i64", "i64" },
{ "float", "double" },
{ "double", "double" },
{ "string", "string" },
{ "str", "string" },
{ "text", "string" },
{ "bool", "bool" },
{ "boolean", "bool" },
{ "i8", "i8" },
{ "i16", "i16" }
};
for (int i = 0; i < headers.Count; i++)
{
string header = SanitizeFieldName(headers[i]); // 清理字段名
string fieldType = "string";
if (i < types.Count && !string.IsNullOrEmpty(types[i]))
{
string typeKey = types[i].ToLower().Trim();
if (typeMap.ContainsKey(typeKey))
{
fieldType = typeMap[typeKey];
}
}
string comment = i < comments.Count ? comments[i] : "";
if (!string.IsNullOrEmpty(comment))
{
sb.AppendLine($"\t{i + 1}:{fieldType} {header}, // {comment}");
}
else
{
sb.AppendLine($"\t{i + 1}:{fieldType} {header},");
}
}
sb.AppendLine("}");
sb.AppendLine();
sb.AppendLine($"struct {structName}");
sb.AppendLine("{");
sb.AppendLine($"\t1:map<i32,{structName}Item> {structName.ToLower()}s,");
sb.AppendLine("}");
return sb.ToString();
}
/// <summary>
/// 清理并转换字段名为PascalCase与Thrift生成的C#属性匹配)
/// 规则:只将第一个字母大写,其余保持不变
/// 示例: key -> Key, en_US -> En_US, merge_id_list -> Merge_id_list
/// </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>
/// 生成 AllConfigs.thrift 内容
/// </summary>
private string GenerateAllConfigsContent(List<string> structNames)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("namespace netstd Byway.Thrift.Data");
sb.AppendLine();
// 添加所有 include
foreach (string structName in structNames)
{
sb.AppendLine($"include \"{structName}.thrift\"");
}
sb.AppendLine();
sb.AppendLine("struct AllConfigs");
sb.AppendLine("{");
// 添加所有字段PascalCase命名
for (int i = 0; i < structNames.Count; i++)
{
string structName = structNames[i];
sb.AppendLine($"\t{i + 1}:{structName}.{structName} {structName},");
}
sb.AppendLine("}");
return sb.ToString();
}
// ========== 步骤2: 编译 Thrift 到 C# ==========
private async Task<bool> Step2_CompileToCSharp(Dictionary<string, string> paths)
{
Log("\n【步骤2】编译 Thrift 到 C#");
Log("--------------------------------------------------------------------------------");
try
{
string compiler = paths["compiler"];
string thriftDir = paths["thrift_dir"];
string outputDir = paths["csharp_output_dir"]; // 先生成到Thrift项目的中间目录
// 清空输出目录
if (Directory.Exists(outputDir))
{
Directory.Delete(outputDir, true);
}
Directory.CreateDirectory(outputDir);
// 获取所有 thrift 文件
string[] thriftFiles = Directory.GetFiles(thriftDir, "*.thrift");
// 过滤:只编译 cfg_txt.json 中定义的配置 + AllConfigs
var filesToCompile = thriftFiles.Where(f =>
{
string fileName = Path.GetFileNameWithoutExtension(f);
return fileName == "AllConfigs" || configStructNames.Contains(fileName);
}).ToArray();
Log($" 找到 {thriftFiles.Length} 个 thrift 文件");
Log($" 将编译 {filesToCompile.Length} 个配置文件(过滤了 {thriftFiles.Length - filesToCompile.Length} 个无关文件)");
Log($" 输出目录: {outputDir}");
Log("");
int successCount = 0;
List<string> failedList = new List<string>();
foreach (string thriftFile in filesToCompile)
{
string fileName = Path.GetFileName(thriftFile);
Log($" 编译: {fileName}");
// 调用 thrift.exe参数与 Python 版本完全一致)
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = compiler,
Arguments = $"-strict -v -r --gen netstd:unity,serial,async_postfix -out \"{outputDir}\" \"{thriftFile}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
WorkingDirectory = thriftDir
};
using (Process process = Process.Start(psi))
{
string output = await process.StandardOutput.ReadToEndAsync();
string error = await process.StandardError.ReadToEndAsync();
await Task.Run(() => process.WaitForExit());
if (process.ExitCode == 0)
{
Log($" [OK] 成功");
successCount++;
}
else
{
Log($" [FAIL] 失败");
if (!string.IsNullOrEmpty(error))
Log($" 错误: {error}");
failedList.Add(fileName);
}
}
}
// 检查生成的 C# 文件
string csharpActualDir = Path.Combine(outputDir, "Byway", "Thrift", "Data");
int csFilesCount = 0;
if (Directory.Exists(csharpActualDir))
{
csFilesCount = Directory.GetFiles(csharpActualDir, "*.cs").Length;
Log($"\n 生成了 {csFilesCount} 个 C# 文件");
}
Log($"\n编译完成: {successCount}/{filesToCompile.Length}");
AddStepReport("编译到C#", true, new Dictionary<string, object>
{
{ "success", successCount },
{ "failed", failedList.Count },
{ "total", filesToCompile.Length },
{ "cs_files_count", csFilesCount }
});
return successCount > 0;
}
catch (Exception ex)
{
Log($"[FAIL] 步骤2失败: {ex.Message}");
Log(ex.StackTrace);
AddStepReport("编译到C#", false, new Dictionary<string, object>
{
{ "error", ex.Message }
});
return false;
}
}
// ========== 步骤3: 生成 Bytes 文件 ==========
private async Task<bool> Step3_GenerateBytes(Dictionary<string, string> paths)
{
Log("\n【步骤3】生成 Bytes 文件");
Log("--------------------------------------------------------------------------------");
try
{
string cfgJsonPath = paths["cfg_json"];
string configDir = paths["config_dir"];
string bytesOutputDir = paths["bytes_output_dir"];
string csharpOutputDir = paths["csharp_output_dir"];
string unityCSharpDir = paths["unity_csharp_dir"];
// 创建输出目录
if (!Directory.Exists(bytesOutputDir))
Directory.CreateDirectory(bytesOutputDir);
// 临时复制C#文件到Unity以便编译和加载
Log(" [1/4] 临时复制C#文件到Unity");
string sourceDir = Path.Combine(csharpOutputDir, "Byway", "Thrift", "Data");
string targetDir = Path.Combine(unityCSharpDir, "Byway", "Thrift", "Data");
if (!Directory.Exists(sourceDir))
{
Log($"[FAIL] 中间C#目录不存在: {sourceDir}");
return false;
}
if (!Directory.Exists(targetDir))
Directory.CreateDirectory(targetDir);
// 复制所有.cs文件除了Extensions
string[] csFiles = Directory.GetFiles(sourceDir, "*.cs", SearchOption.TopDirectoryOnly);
int copiedCount = 0;
foreach (string csFile in csFiles)
{
string fileName = Path.GetFileName(csFile);
if (fileName.Contains("Extensions.cs"))
{
Log($" [SKIP] 跳过: {fileName}");
continue;
}
string destFile = Path.Combine(targetDir, fileName);
File.Copy(csFile, destFile, true);
copiedCount++;
}
Log($" [OK] 已复制 {copiedCount} 个C#文件");
// 等待Unity编译
Log(" [2/4] 等待Unity编译");
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
await Task.Delay(2000);
// 加载程序集
Log(" [3/4] 加载程序集并生成bytes");
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] 找不到程序集");
return false;
}
// 读取配置
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关键使用ToArray而不是GetBuffer
Log($"\n 序列化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;
// 关键使用ToArray()获取实际写入的数据对应Python的getvalue()
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),可能大部分配置未序列化!");
}
}
}
}
}
// 清理Unity中的临时文件
Log(" [4/4] 清理临时C#文件");
if (Directory.Exists(targetDir))
{
try
{
Directory.Delete(targetDir, true);
// 清理空目录
string thriftFolder = Path.GetDirectoryName(targetDir);
if (Directory.Exists(thriftFolder) && Directory.GetFileSystemEntries(thriftFolder).Length == 0)
Directory.Delete(thriftFolder);
string bywayFolder = Path.GetDirectoryName(thriftFolder);
if (Directory.Exists(bywayFolder) && Directory.GetFileSystemEntries(bywayFolder).Length == 0)
Directory.Delete(bywayFolder);
AssetDatabase.Refresh();
Log($" [OK] 已清理临时文件");
}
catch (Exception ex)
{
Log($" [WARN] 清理失败: {ex.Message}");
}
}
Log($"\n生成完成: 成功 {successCount} 个配置");
AddStepReport("生成Bytes文件", true, new Dictionary<string, object>
{
{ "success", successCount }
});
return true;
}
catch (Exception ex)
{
Log($"[FAIL] 步骤3失败: {ex.Message}");
Log(ex.StackTrace);
AddStepReport("生成Bytes文件", false, new Dictionary<string, object>
{
{ "error", ex.Message }
});
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)
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)
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;
int fieldSetCount = 0;
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;
// 处理null值
if (cellValue == null)
{
if (property.PropertyType == typeof(string))
{
convertedValue = "";
}
else if (property.PropertyType.IsValueType)
{
// 值类型int, bool, double等设置默认值0, false, 0.0等)
convertedValue = Activator.CreateInstance(property.PropertyType);
}
else
{
// 引用类型跳过
continue;
}
}
else
{
convertedValue = ConvertCellValue(cellValue, property.PropertyType);
}
property.SetValue(itemInstance, convertedValue);
fieldSetCount++;
if (fieldName.ToLower() == "id" && convertedValue is int)
idValue = (int)convertedValue;
}
catch (Exception ex)
{
Log($" [WARN] 行{row} 字段{fieldName}转换失败: {ex.Message}");
}
}
else
{
// 尝试查找所有属性来调试
if (filledItemCount == 0) // 只在第一项时输出
{
Log($" [DEBUG] 未找到属性: {fieldName} (原始: {originalHeader})");
Log($" [DEBUG] {itemType.Name} 可用属性: {string.Join(", ", itemType.GetProperties().Select(p => p.Name))}");
}
}
}
if (filledItemCount == 0)
{
if (fieldSetCount < headers.Count)
{
// 找出未设置的字段
var unsetFields = new List<string>();
for (int i = 0; i < headers.Count; i++)
{
var h = headers[i];
var fieldName = SanitizeFieldName(h);
var property = itemType.GetProperty(fieldName);
if (property == null)
{
unsetFields.Add(h);
continue;
}
var colIndex = validColumns[i];
var cellValue = worksheet.Cells[row, colIndex].Value;
// 如果是null且不是string类型则算作未设置
if (cellValue == null && property.PropertyType != typeof(string))
{
unsetFields.Add(h);
}
}
Log($" 第一行数据: ID={idValue}, 设置了 {fieldSetCount}/{headers.Count} 个字段" +
(unsetFields.Any() ? $" (未设置: {string.Join(", ", unsetFields)})" : ""));
}
else
{
Log($" 第一行数据: ID={idValue}, 设置了 {fieldSetCount}/{headers.Count} 个字段");
}
}
if (idValue.HasValue)
{
var addMethod = dictType.GetMethod("Add");
addMethod.Invoke(dictInstance, new object[] { idValue.Value, itemInstance });
filledItemCount++;
}
}
}
// 获取字典中的条目数量
var countProperty = dictType.GetProperty("Count");
int itemCount = countProperty != null ? (int)countProperty.GetValue(dictInstance) : 0;
// 设置字典属性(格式:首字母大写 + 其余小写 + s如 Adgiftdatas
string dictPropertyName = char.ToUpper(structName[0]) + structName.Substring(1).ToLower() + "s";
var dictProperty = configType.GetProperty(dictPropertyName);
if (dictProperty != null)
{
dictProperty.SetValue(configInstance, dictInstance);
Log($" 字典属性: {dictPropertyName} = {itemCount} 条");
}
else
{
Log($" [ERROR] 未找到字典属性: {dictPropertyName} (类: {configType.Name})");
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;
}
// 验证__isset标志
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();
var issetProperty = issetType.GetProperty(structName);
if (issetProperty != null)
{
bool issetValue = (bool)issetProperty.GetValue(issetObj);
Log($" AllConfigs.{structName} = OK (__isset={issetValue})");
if (!issetValue)
{
Log($" [ERROR] __isset.{structName} = false!");
return false;
}
}
}
}
else
{
Log($" [ERROR] AllConfigs中未找到属性: {structName}");
return false;
}
return true;
}
catch (Exception ex)
{
Log($" [ERROR] {structName}: {ex.Message}");
return false;
}
}
/// <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;
}
}
// ========== 步骤4: 清理冲突文件 ==========
private bool Step4_CleanupExtensions(Dictionary<string, string> paths)
{
Log("\n【步骤4】清理冲突的 Extensions 文件");
Log("--------------------------------------------------------------------------------");
try
{
string csharpOutputDir = paths["csharp_output_dir"];
string actualDir = Path.Combine(csharpOutputDir, "Byway", "Thrift", "Data");
if (!Directory.Exists(actualDir))
{
Log(" [WARN] C# 目录不存在");
return true;
}
// 查找 AllConfigs.Extensions.cs
string[] extensionFiles = Directory.GetFiles(actualDir, "*AllConfigs.Extensions.cs", SearchOption.TopDirectoryOnly);
int deletedCount = 0;
foreach (string file in extensionFiles)
{
string fileName = Path.GetFileName(file);
Log($" 删除: {fileName}");
try
{
File.Delete(file);
deletedCount++;
Log($" [OK] 已删除");
}
catch (Exception ex)
{
Log($" [WARN] 删除失败: {ex.Message}");
}
}
if (deletedCount == 0)
{
Log(" 未发现需要删除的 Extensions 文件");
}
Log($"\n清理完成: 删除 {deletedCount} 个文件");
AddStepReport("清理冲突文件", true, new Dictionary<string, object>
{
{ "deleted", deletedCount }
});
return true;
}
catch (Exception ex)
{
Log($"[FAIL] 步骤4失败: {ex.Message}");
AddStepReport("清理冲突文件", false, new Dictionary<string, object>
{
{ "error", ex.Message }
});
return false;
}
}
// ========== 步骤5: 复制 C# 到 Unity ==========
private bool Step5_CopyCSharp(Dictionary<string, string> paths)
{
Log("\n【步骤5】复制 C# 文件到 Unity");
Log("--------------------------------------------------------------------------------");
try
{
string csharpOutputDir = paths["csharp_output_dir"];
string unityCSharpDir = paths["unity_csharp_dir"];
string sourceDir = Path.Combine(csharpOutputDir, "Byway", "Thrift", "Data");
string targetDir = Path.Combine(unityCSharpDir, "Byway", "Thrift", "Data");
if (!Directory.Exists(sourceDir))
{
Log(" [FAIL] 源目录不存在");
return false;
}
// 创建目标目录
if (Directory.Exists(targetDir))
{
Directory.Delete(targetDir, true);
}
Directory.CreateDirectory(targetDir);
// 复制所有C#文件
int copiedCount = 0;
foreach (string file in Directory.GetFiles(sourceDir, "*.cs"))
{
string fileName = Path.GetFileName(file);
string destFile = Path.Combine(targetDir, fileName);
File.Copy(file, destFile, true);
copiedCount++;
}
Log($"\n复制完成: {copiedCount} 个 C# 文件");
AddStepReport("复制C#到Unity", true, new Dictionary<string, object>
{
{ "count", copiedCount }
});
return true;
}
catch (Exception ex)
{
Log($"[FAIL] 步骤5失败: {ex.Message}");
Log(ex.StackTrace);
AddStepReport("复制C#到Unity", false, new Dictionary<string, object>
{
{ "error", ex.Message }
});
return false;
}
}
// ========== 步骤6: 复制 Bytes 到 Unity ==========
private bool Step6_CopyBytes(Dictionary<string, string> paths)
{
Log("\n【步骤6】复制 Bytes 文件到 Unity");
Log("--------------------------------------------------------------------------------");
try
{
string bytesOutputDir = paths["bytes_output_dir"];
string unityBytesDir = paths["unity_bytes_dir"];
if (!Directory.Exists(unityBytesDir))
{
Directory.CreateDirectory(unityBytesDir);
}
// 只复制 AllConfigs.bytes
string sourceFile = Path.Combine(bytesOutputDir, "AllConfigs.bytes");
string targetFile = Path.Combine(unityBytesDir, "AllConfigs.bytes");
if (File.Exists(sourceFile))
{
File.Copy(sourceFile, targetFile, true);
Log(" [OK] 复制 AllConfigs.bytes");
AddStepReport("复制Bytes到Unity", true, new Dictionary<string, object>
{
{ "count", 1 }
});
return true;
}
else
{
Log(" [FAIL] 未找到 AllConfigs.bytes");
return false;
}
}
catch (Exception ex)
{
Log($"[FAIL] 步骤6失败: {ex.Message}");
AddStepReport("复制Bytes到Unity", false, new Dictionary<string, object>
{
{ "error", ex.Message }
});
return false;
}
}
// ========== 步骤7: 生成 DR 文件 ==========
private bool Step7_GenerateDR(Dictionary<string, string> paths)
{
Log("\n【步骤5】生成 DR 文件");
Log("--------------------------------------------------------------------------------");
try
{
// 从Unity项目中读取C#文件(已经复制过来的)
string unityCSharpDir = paths["unity_csharp_dir"];
string drOutputDir = paths["dr_output_dir"];
string actualDir = Path.Combine(unityCSharpDir, "Byway", "Thrift", "Data");
// 创建 DR 输出目录
if (!Directory.Exists(drOutputDir))
Directory.CreateDirectory(drOutputDir);
// 查找所有配置类(不包括 AllConfigs 和 Item 类)
string[] csFiles = Directory.GetFiles(actualDir, "*.cs");
int generatedCount = 0;
foreach (string csFile in csFiles)
{
string fileName = Path.GetFileName(csFile);
// 跳过 ItemItem数据类、AllConfigs、Extensions 文件
// 注意PetAirItem.cs 是配置类应该生成DRPetAirItemItem.cs 是数据类不生成DR
if (fileName.EndsWith("ItemItem.cs") ||
fileName.Contains("AllConfigs") ||
fileName.Contains("Extensions"))
continue;
// 解析配置类
var configInfo = ParseConfigClass(csFile);
if (configInfo == null)
continue;
string className = configInfo["class_name"];
Log($" 生成: DR{className}");
// 查找对应的 Item 类
string itemFileName = configInfo["item_type"] + ".cs";
string itemFilePath = Path.Combine(actualDir, itemFileName);
if (!File.Exists(itemFilePath))
{
Log($" [WARN] 找不到 Item 文件: {itemFileName}");
continue;
}
// 解析 Item 类属性
var properties = ParseItemClass(itemFilePath);
// 生成 DR 代码
string drCode = GenerateDRCode(configInfo, properties);
// 写入文件
string drFilePath = Path.Combine(drOutputDir, $"DR{configInfo["class_name"]}.cs");
File.WriteAllText(drFilePath, drCode);
Log($" [OK] 生成完成");
generatedCount++;
}
Log($"\n生成完成: {generatedCount} 个 DR 文件");
AddStepReport("生成DR文件", true, new Dictionary<string, object>
{
{ "total", generatedCount }
});
return true;
}
catch (Exception ex)
{
Log($"[FAIL] 步骤5失败: {ex.Message}");
Log(ex.StackTrace);
AddStepReport("生成DR文件", false, new Dictionary<string, object>
{
{ "error", ex.Message }
});
return false;
}
}
/// <summary>
/// 解析配置类文件
/// </summary>
private Dictionary<string, string> ParseConfigClass(string filePath)
{
try
{
string content = File.ReadAllText(filePath);
// 提取类名
var classMatch = Regex.Match(content, @"public partial class (\w+) : TBase");
if (!classMatch.Success)
return null;
string className = classMatch.Groups[1].Value;
// 提取 Dictionary 属性(支持 global:: 命名空间)
// 注意:必须匹配公开属性(public Dictionary),而不是私有字段(_xxx)
// 使用多行匹配,确保找到完整的属性定义
var propPattern = @"public\s+Dictionary<int,\s*(?:global::[\w\.]+\.)?(\w+)>\s+(\w+)\s*\{";
var propMatch = Regex.Match(content, propPattern, RegexOptions.Multiline);
if (!propMatch.Success)
{
// 如果没找到,记录详细信息用于调试
return null;
}
string itemType = propMatch.Groups[1].Value;
string dictProperty = propMatch.Groups[2].Value;
return new Dictionary<string, string>
{
{ "class_name", className },
{ "item_type", itemType },
{ "dict_property", dictProperty }
};
}
catch
{
return null;
}
}
/// <summary>
/// 解析 Item 类
/// </summary>
private List<Dictionary<string, string>> ParseItemClass(string filePath)
{
List<Dictionary<string, string>> properties = new List<Dictionary<string, string>>();
try
{
string content = File.ReadAllText(filePath);
// 匹配所有公共属性
var matches = Regex.Matches(content, @"public\s+(\w+(?:<[^>]+>)?)\s+(\w+)\s*\{");
foreach (Match match in matches)
{
string type = match.Groups[1].Value;
string name = match.Groups[2].Value;
// 跳过 __isset 和 Isset
if (name == "__isset" || name == "Isset")
continue;
var (finalType, defaultValue) = GetTypeInfo(type);
properties.Add(new Dictionary<string, string>
{
{ "type", finalType },
{ "name", name },
{ "original_type", type }, // 保存原始类型用于判断
{ "default", defaultValue }
});
}
}
catch
{
// 忽略错误
}
return properties;
}
/// <summary>
/// 生成 DR 类代码
/// </summary>
private string GenerateDRCode(Dictionary<string, string> configInfo, List<Dictionary<string, string>> properties)
{
string className = configInfo["class_name"];
string itemType = configInfo["item_type"];
string dictProperty = configInfo["dict_property"];
StringBuilder sb = new StringBuilder();
sb.AppendLine("// 此文件由 ThriftIntegratedPipeline 自动生成,请勿手动修改");
sb.AppendLine($"// 配置类: {className}");
sb.AppendLine($"// 数据类: {itemType}");
sb.AppendLine();
sb.AppendLine("using UnityEngine;");
sb.AppendLine("using Byway.Config;");
sb.AppendLine("using Byway.Thrift.Data;");
sb.AppendLine("using UnityGameFramework.Runtime;");
sb.AppendLine();
sb.AppendLine("namespace CrazyMaple");
sb.AppendLine("{");
sb.AppendLine(" /// <summary>");
sb.AppendLine($" /// {className} 数据行");
sb.AppendLine(" /// </summary>");
sb.AppendLine($" public class DR{className} : DataRowBase");
sb.AppendLine(" {");
sb.AppendLine($" private {itemType} _configData;");
sb.AppendLine();
// Id 属性
var idProp = properties.FirstOrDefault(p => p["name"].ToLower() == "id");
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 唯一标识");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public override int Id");
sb.AppendLine(" {");
sb.AppendLine(" get");
sb.AppendLine(" {");
if (idProp != null)
{
sb.AppendLine($" return _configData?.{idProp["name"]} ?? 0;");
}
else
{
sb.AppendLine(" return 0;");
}
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
// 其他属性
foreach (var prop in properties)
{
string propName = prop["name"];
// 跳过ID字段已经作为override属性生成
if (propName.ToLower() == "id")
continue;
string propType = prop["type"];
string originalType = prop.ContainsKey("original_type") ? prop["original_type"] : propType;
string defaultValue = prop.ContainsKey("default") ? prop["default"] : "";
if (string.IsNullOrEmpty(defaultValue))
{
var (_, defVal) = GetTypeInfo(propType);
defaultValue = defVal;
}
sb.AppendLine(" /// <summary>");
sb.AppendLine($" /// {propName}");
sb.AppendLine(" /// </summary>");
// 特殊处理double类型在DR中返回float与Python版本一致
if (originalType.ToLower().Contains("double"))
{
sb.AppendLine($" public float {propName}");
sb.AppendLine(" {");
sb.AppendLine(" get");
sb.AppendLine(" {");
sb.AppendLine($" return System.Convert.ToSingle(_configData?.{propName} ?? 0.0);");
sb.AppendLine(" }");
sb.AppendLine(" }");
}
else
{
sb.AppendLine($" public {propType} {propName}");
sb.AppendLine(" {");
sb.AppendLine(" get");
sb.AppendLine(" {");
sb.AppendLine($" return _configData?.{propName} ?? {defaultValue};");
sb.AppendLine(" }");
sb.AppendLine(" }");
}
sb.AppendLine();
}
// UIForm 特殊处理:使用完整命名空间类型名
string fullClassName = className == "UIForm" ? "Byway.Thrift.Data.UIForm" : className;
// LoadFromConfig 方法(优化:支持可选的配置实例参数和直接传入 Item
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 从配置加载数据(优先使用传入的配置实例)");
sb.AppendLine(" /// </summary>");
sb.AppendLine($" public void LoadFromConfig(int id, {fullClassName} config = null)");
sb.AppendLine(" {");
sb.AppendLine(" if (config == null)");
sb.AppendLine(" {");
sb.AppendLine($" config = ConfigManager.Instance.GetConfig<{fullClassName}>();");
sb.AppendLine(" }");
sb.AppendLine(" ");
sb.AppendLine($" if (config?.{dictProperty} != null)");
sb.AppendLine(" {");
sb.AppendLine($" config.{dictProperty}.TryGetValue(id, out _configData);");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
// 添加直接设置 Item 的方法(性能优化)
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 直接设置配置数据(性能优化:跳过字典查询)");
sb.AppendLine(" /// </summary>");
sb.AppendLine($" public void SetConfigData({itemType} configData)");
sb.AppendLine(" {");
sb.AppendLine(" _configData = configData;");
sb.AppendLine(" }");
sb.AppendLine();
// ParseDataRow 方法(优化:使用 userData 传入的配置实例,或直接传入 Item
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 解析数据行(优化:使用 userData 传入的配置实例,避免重复调用 GetConfig");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public override bool ParseDataRow(string dataRowString, object userData)");
sb.AppendLine(" {");
sb.AppendLine(" int id = 0;");
sb.AppendLine(" if (!int.TryParse(dataRowString, out id))");
sb.AppendLine(" {");
sb.AppendLine(" return false;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" // 性能优化:尝试从 userData 获取配置字典,直接获取 Item");
sb.AppendLine(" if (userData is System.Collections.Generic.Dictionary<string, object> userDataDict)");
sb.AppendLine(" {");
sb.AppendLine(" // 优先尝试从缓存的字典直接获取 Item最快");
sb.AppendLine(" if (userDataDict.TryGetValue(\"ConfigDict\", out object dictObj))");
sb.AppendLine(" {");
sb.AppendLine(" var dict = dictObj as System.Collections.Generic.Dictionary<int, " + itemType + ">;");
sb.AppendLine(" if (dict != null && dict.TryGetValue(id, out var item))");
sb.AppendLine(" {");
sb.AppendLine(" _configData = item;");
sb.AppendLine(" return true;");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" // 备选方案:从配置实例获取");
sb.AppendLine(" if (userDataDict.TryGetValue(\"ConfigInstance\", out object configObj))");
sb.AppendLine(" {");
sb.AppendLine($" var config = configObj as {fullClassName};");
sb.AppendLine(" if (config != null)");
sb.AppendLine(" {");
sb.AppendLine(" LoadFromConfig(id, config);");
sb.AppendLine(" return _configData != null;");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" // 兜底方案:直接查询(最慢)");
sb.AppendLine(" LoadFromConfig(id);");
sb.AppendLine(" return _configData != null;");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
/// <summary>
/// 获取类型信息和默认值
/// </summary>
private (string type, string defaultValue) GetTypeInfo(string csType)
{
if (csType.Contains("Dictionary") || csType.Contains("List"))
{
return (csType, "null");
}
switch (csType.ToLower())
{
case "int":
case "i32":
return ("int", "0");
case "long":
case "i64":
return ("long", "0");
case "double":
case "float":
return ("double", "0.0");
case "bool":
case "boolean":
return ("bool", "false");
case "string":
return ("string", "\"\"");
default:
return ("string", "\"\"");
}
}
// ========== 报告生成 ==========
private string GenerateReport()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("================================================================================");
sb.AppendLine(" Thrift 流程执行报告");
sb.AppendLine("================================================================================");
sb.AppendLine($"执行时间: {startTime:yyyy-MM-dd HH:mm:ss} -> {endTime:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine($"总耗时: {(endTime - startTime).TotalSeconds:F2} 秒");
sb.AppendLine();
// 步骤摘要
int totalSteps = reportSteps.Count;
int successSteps = reportSteps.Count(s => s.Success);
sb.AppendLine("步骤摘要:");
for (int i = 0; i < reportSteps.Count; i++)
{
var step = reportSteps[i];
string status = step.Success ? "[OK]" : "[FAIL]";
sb.AppendLine($" {i + 1}. {status} {step.Name}");
}
sb.AppendLine();
if (successSteps == totalSteps)
{
sb.AppendLine("【总体状态】[OK] 所有步骤成功完成,无错误");
}
else
{
sb.AppendLine($"【总体状态】[WARNING] {totalSteps - successSteps} 个步骤失败");
}
sb.AppendLine();
sb.AppendLine("================================================================================");
sb.AppendLine(" 详细执行报告");
sb.AppendLine("================================================================================");
sb.AppendLine();
// 详细步骤信息
for (int i = 0; i < reportSteps.Count; i++)
{
var step = reportSteps[i];
string status = step.Success ? "[OK]" : "[FAIL]";
sb.AppendLine($"【步骤 {i + 1}】{step.Name}");
sb.AppendLine($"状态: {status}");
sb.AppendLine($"时间: {step.Time:HH:mm:ss}");
sb.AppendLine("--------------------------------------------------------------------------------");
if (step.Details != null && step.Details.Count > 0)
{
foreach (var detail in step.Details)
{
sb.AppendLine($" {detail.Key}: {detail.Value}");
}
}
sb.AppendLine();
}
// 配置路径信息
sb.AppendLine("================================================================================");
sb.AppendLine(" 配置路径信息");
sb.AppendLine("================================================================================");
var paths = GetDerivedPaths();
sb.AppendLine("[基础路径]");
sb.AppendLine($" - Docs 项目: {docsProjectPath}");
sb.AppendLine($" - Thrift 项目: {thriftProjectPath}");
sb.AppendLine();
sb.AppendLine("[派生路径]");
sb.AppendLine($" - cfg_txt.json: {paths["cfg_json"]}");
sb.AppendLine($" - Config 目录: {paths["config_dir"]}");
sb.AppendLine($" - Thrift 编译器: {paths["compiler"]}");
sb.AppendLine($" - Unity C# 目录: {paths["unity_csharp_dir"]}");
sb.AppendLine($" - Unity Bytes 目录: {paths["unity_bytes_dir"]}");
sb.AppendLine($" - DR 生成目录: {paths["dr_output_dir"]}");
sb.AppendLine("================================================================================");
return sb.ToString();
}
private class StepReport
{
public string Name { get; set; }
public bool Success { get; set; }
public DateTime Time { get; set; }
public Dictionary<string, object> Details { get; set; }
}
}
}