using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; using MongoDB.Bson.Serialization; using OfficeOpenXml; using ProtoBuf; using LicenseContext = OfficeOpenXml.LicenseContext; namespace ET { public enum ConfigType { c = 0, s = 1, cs = 2, } class HeadInfo { public string FieldCS; public string FieldDesc; public string FieldName; public string FieldType; public int FieldIndex; public HeadInfo(string cs, string desc, string name, string type, int index) { this.FieldCS = cs; this.FieldDesc = desc; this.FieldName = name; this.FieldType = type; this.FieldIndex = index; } } // 这里加个标签是为了防止编译时裁剪掉protobuf,因为整个tool工程没有用到protobuf,编译会去掉引用,然后动态编译就会出错 [ProtoContract] class Table { public bool C; public bool S; public int Index; public Dictionary HeadInfos = new Dictionary(); } public static class ExcelExporter { private static string template; private const string ClientClassDir = "../Unity/Assets/Scripts/Codes/Model/Client/Generate/Config"; // 服务端因为机器人的存在必须包含客户端所有配置,所以单独的c字段没有意义,单独的c就表示cs private const string ServerClassDir = "../DotNet/Model/Generate/Config"; private const string CSClassDir = "../Unity/Assets/Scripts/Codes/Model/Generate/ClientServer/Config"; private const string excelDir = "../Config/Excel/"; private const string jsonDir = "../Config/GenJson/{0}/{1}"; private const string clientProtoDir = "../Unity/Assets/Bundles/Config"; private const string serverProtoDir = "../Config/GenFromExcel/{0}/{1}"; private static Assembly[] configAssemblies = new Assembly[3]; private static Dictionary tables = new Dictionary(); private static Dictionary packages = new Dictionary(); private static Table GetTable(string protoName) { if (!tables.TryGetValue(protoName, out var table)) { table = new Table(); tables[protoName] = table; } return table; } public static ExcelPackage GetPackage(string filePath) { if (!packages.TryGetValue(filePath, out var package)) { using Stream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); package = new ExcelPackage(stream); packages[filePath] = package; } return package; } public static void Export() { try { //防止编译时裁剪掉protobuf ProtoBuf.WireType.Fixed64.ToString(); template = File.ReadAllText("Template.txt"); ExcelPackage.LicenseContext = LicenseContext.NonCommercial; if (Directory.Exists(ClientClassDir)) { Directory.Delete(ClientClassDir, true); } if (Directory.Exists(ServerClassDir)) { Directory.Delete(ServerClassDir, true); } List files = FileHelper.GetAllFiles(excelDir); foreach (string path in files) { string fileName = Path.GetFileName(path); if (!fileName.EndsWith(".xlsx") || fileName.StartsWith("~$") || fileName.Contains("#")) { continue; } string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); string fileNameWithoutCS = fileNameWithoutExtension; string cs = "cs"; if (fileNameWithoutExtension.Contains("@")) { string[] ss = fileNameWithoutExtension.Split("@"); fileNameWithoutCS = ss[0]; cs = ss[1]; } if (cs == "") { cs = "cs"; } ExcelPackage p = GetPackage(Path.GetFullPath(path)); string protoName = fileNameWithoutCS; if (fileNameWithoutCS.Contains('_')) { protoName = fileNameWithoutCS.Substring(0, fileNameWithoutCS.LastIndexOf('_')); } Table table = GetTable(protoName); if (cs.Contains("c")) { table.C = true; } if (cs.Contains("s")) { table.S = true; } ExportExcelClass(p, protoName, table); } foreach (var kv in tables) { if (kv.Value.C) { ExportClass(kv.Key, kv.Value.HeadInfos, ConfigType.c); } if (kv.Value.S) { ExportClass(kv.Key, kv.Value.HeadInfos, ConfigType.s); } // ExportClass(kv.Key, kv.Value.HeadInfos, ConfigType.cs); } // 动态编译生成的配置代码 configAssemblies[(int) ConfigType.c] = DynamicBuild(ConfigType.c); configAssemblies[(int) ConfigType.s] = DynamicBuild(ConfigType.s); // configAssemblies[(int) ConfigType.cs] = DynamicBuild(ConfigType.cs); List excels = FileHelper.GetAllFiles(excelDir, "*.xlsx"); foreach (string path in excels) { ExportExcel(path); } if (Directory.Exists(clientProtoDir)) { Directory.Delete(clientProtoDir, true); } FileHelper.CopyDirectory(GetProtoDir(ConfigType.c, ""), clientProtoDir); Log.Console("Export Excel Sucess!"); } catch (Exception e) { Log.Console(e.ToString()); } finally { tables.Clear(); foreach (var kv in packages) { kv.Value.Dispose(); } packages.Clear(); } } private static void ExportExcel(string path) { string dir = Path.GetDirectoryName(path); string relativePath = Path.GetRelativePath(excelDir, dir); string fileName = Path.GetFileName(path); if (!fileName.EndsWith(".xlsx") || fileName.StartsWith("~$") || fileName.Contains("#")) { return; } string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); string fileNameWithoutCS = fileNameWithoutExtension; string cs = "cs"; if (fileNameWithoutExtension.Contains("@")) { string[] ss = fileNameWithoutExtension.Split("@"); fileNameWithoutCS = ss[0]; cs = ss[1]; } if (cs == "") { cs = "cs"; } string protoName = fileNameWithoutCS; if (fileNameWithoutCS.Contains('_')) { protoName = fileNameWithoutCS.Substring(0, fileNameWithoutCS.LastIndexOf('_')); } Table table = GetTable(protoName); ExcelPackage p = GetPackage(Path.GetFullPath(path)); if (cs.Contains("c")) { ExportExcelJson(p, fileNameWithoutCS, table, ConfigType.c, relativePath); ExportExcelProtobuf(ConfigType.c, protoName, relativePath); } if (cs.Contains("s")) { ExportExcelJson(p, fileNameWithoutCS, table, ConfigType.s, relativePath); ExportExcelProtobuf(ConfigType.s, protoName, relativePath); } // ExportExcelJson(p, fileNameWithoutCS, table, ConfigType.cs, relativePath); // ExportExcelProtobuf(ConfigType.cs, protoName, relativePath); } private static string GetProtoDir(ConfigType configType, string relativeDir) { return string.Format(serverProtoDir, configType.ToString(), relativeDir); } private static Assembly GetAssembly(ConfigType configType) { return configAssemblies[(int) configType]; } private static string GetClassDir(ConfigType configType) { return configType switch { ConfigType.c => ClientClassDir, ConfigType.s => ServerClassDir, _ => CSClassDir }; } // 动态编译生成的cs代码 private static Assembly DynamicBuild(ConfigType configType) { string classPath = GetClassDir(configType); if (!Directory.Exists(classPath)) { Directory.CreateDirectory(classPath); } List syntaxTrees = new List(); List protoNames = new List(); foreach (string classFile in Directory.GetFiles(classPath, "*.cs")) { protoNames.Add(Path.GetFileNameWithoutExtension(classFile)); syntaxTrees.Add(CSharpSyntaxTree.ParseText(File.ReadAllText(classFile))); } List references = new List(); Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { if (assembly.IsDynamic) { continue; } if (assembly.Location == "") { continue; } } catch (Exception e) { Console.WriteLine(e); throw; } PortableExecutableReference reference = MetadataReference.CreateFromFile(assembly.Location); references.Add(reference); } CSharpCompilation compilation = CSharpCompilation.Create(null, syntaxTrees.ToArray(), references.ToArray(), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); using MemoryStream memSteam = new MemoryStream(); EmitResult emitResult = compilation.Emit(memSteam); if (!emitResult.Success) { StringBuilder stringBuilder = new StringBuilder(); foreach (Diagnostic t in emitResult.Diagnostics) { stringBuilder.Append($"{t.GetMessage()}\n"); } throw new Exception($"动态编译失败:\n{stringBuilder}"); } memSteam.Seek(0, SeekOrigin.Begin); Assembly ass = Assembly.Load(memSteam.ToArray()); return ass; } #region 导出class static void ExportExcelClass(ExcelPackage p, string name, Table table) { foreach (ExcelWorksheet worksheet in p.Workbook.Worksheets) { ExportSheetClass(worksheet, table); } } static void ExportSheetClass(ExcelWorksheet worksheet, Table table) { const int row = 2; for (int col = 3; col <= worksheet.Dimension.End.Column; ++col) { if (worksheet.Name.StartsWith("#")) { continue; } string fieldName = worksheet.Cells[row + 2, col].Text.Trim(); if (fieldName == "") { continue; } if (table.HeadInfos.ContainsKey(fieldName)) { continue; } string fieldCS = worksheet.Cells[row, col].Text.Trim().ToLower(); if (fieldCS.Contains("#")) { table.HeadInfos[fieldName] = null; continue; } if (fieldCS == "") { fieldCS = "cs"; } if (table.HeadInfos.TryGetValue(fieldName, out var oldClassField)) { if (oldClassField.FieldCS != fieldCS) { Log.Console($"field cs not same: {worksheet.Name} {fieldName} oldcs: {oldClassField.FieldCS} {fieldCS}"); } continue; } string fieldDesc = worksheet.Cells[row + 1, col].Text.Trim(); string fieldType = worksheet.Cells[row + 3, col].Text.Trim(); table.HeadInfos[fieldName] = new HeadInfo(fieldCS, fieldDesc, fieldName, fieldType, ++table.Index); } } static void ExportClass(string protoName, Dictionary classField, ConfigType configType) { string dir = GetClassDir(configType); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } string exportPath = Path.Combine(dir, $"{protoName}.cs"); using FileStream txt = new FileStream(exportPath, FileMode.Create); using StreamWriter sw = new StreamWriter(txt); StringBuilder sb = new StringBuilder(); foreach ((string _, HeadInfo headInfo) in classField) { if (headInfo == null) { continue; } if (configType != ConfigType.cs && !headInfo.FieldCS.Contains(configType.ToString())) { continue; } sb.Append($"\t\t/// {headInfo.FieldDesc}\n"); sb.Append($"\t\t[ProtoMember({headInfo.FieldIndex})]\n"); string fieldType = headInfo.FieldType; sb.Append($"\t\tpublic {fieldType} {headInfo.FieldName} {{ get; set; }}\n"); } string content = template.Replace("(ConfigName)", protoName).Replace(("(Fields)"), sb.ToString()); sw.Write(content); } #endregion #region 导出json static void ExportExcelJson(ExcelPackage p, string name, Table table, ConfigType configType, string relativeDir) { StringBuilder sb = new StringBuilder(); sb.Append("{\"list\":[\n"); foreach (ExcelWorksheet worksheet in p.Workbook.Worksheets) { if (worksheet.Name.StartsWith("#")) { continue; } ExportSheetJson(worksheet, name, table.HeadInfos, configType, sb); } sb.Append("]}\n"); string dir = string.Format(jsonDir, configType.ToString(), relativeDir); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } string jsonPath = Path.Combine(dir, $"{name}.txt"); using FileStream txt = new FileStream(jsonPath, FileMode.Create); using StreamWriter sw = new StreamWriter(txt); sw.Write(sb.ToString()); } static void ExportSheetJson(ExcelWorksheet worksheet, string name, Dictionary classField, ConfigType configType, StringBuilder sb) { string configTypeStr = configType.ToString(); for (int row = 6; row <= worksheet.Dimension.End.Row; ++row) { string prefix = worksheet.Cells[row, 2].Text.Trim(); if (prefix.Contains("#")) { continue; } if (prefix == "") { prefix = "cs"; } if (configType != ConfigType.cs && !prefix.Contains(configTypeStr)) { continue; } if (worksheet.Cells[row, 3].Text.Trim() == "") { continue; } sb.Append("{"); sb.Append($"\"_t\":\"{name}\""); for (int col = 3; col <= worksheet.Dimension.End.Column; ++col) { string fieldName = worksheet.Cells[4, col].Text.Trim(); if (!classField.ContainsKey(fieldName)) { continue; } HeadInfo headInfo = classField[fieldName]; if (headInfo == null) { continue; } if (configType != ConfigType.cs && !headInfo.FieldCS.Contains(configTypeStr)) { continue; } string fieldN = headInfo.FieldName; if (fieldN == "Id") { fieldN = "_id"; } sb.Append($",\"{fieldN}\":{Convert(headInfo.FieldType, worksheet.Cells[row, col].Text.Trim())}"); } sb.Append("},\n"); } } private static string Convert(string type, string value) { switch (type) { case "uint[]": case "int[]": case "int32[]": case "long[]": return $"[{value}]"; case "string[]": case "int[][]": return $"[{value}]"; case "int": case "uint": case "int32": case "int64": case "long": case "float": case "double": if (value == "") { return "0"; } return value; case "string": value = value.Replace("\\", "\\\\"); value = value.Replace("\"", "\\\""); return $"\"{value}\""; default: throw new Exception($"不支持此类型: {type}"); } } #endregion // 根据生成的类,把json转成protobuf private static void ExportExcelProtobuf(ConfigType configType, string protoName, string relativeDir) { string dir = GetProtoDir(configType, relativeDir); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } Assembly ass = GetAssembly(configType); Type type = ass.GetType($"ET.{protoName}Category"); Type subType = ass.GetType($"ET.{protoName}"); Serializer.NonGeneric.PrepareSerializer(type); Serializer.NonGeneric.PrepareSerializer(subType); IMerge final = Activator.CreateInstance(type) as IMerge; string p = Path.Combine(string.Format(jsonDir, configType, relativeDir)); string[] ss = Directory.GetFiles(p, $"{protoName}*.txt"); List jsonPaths = ss.ToList(); jsonPaths.Sort(); jsonPaths.Reverse(); foreach (string jsonPath in jsonPaths) { string json = File.ReadAllText(jsonPath); try { object deserialize = BsonSerializer.Deserialize(json, type); final.Merge(deserialize); } catch { #region 为了定位该文件中具体那一行出现了异常 List list = new List(json.Split('\n')); if (list.Count > 0) list.RemoveAt(0); if (list.Count > 0) list.RemoveAt(list.Count-1); foreach (string s in list) { try { BsonSerializer.Deserialize(s.Substring(0, s.Length-1), subType); } catch (Exception) { Log.Console($"json : {s}"); throw; } } #endregion } } string path = Path.Combine(dir, $"{protoName}Category.bytes"); using FileStream file = File.Create(path); Serializer.Serialize(file, final); } } }