From fb7beedbad1678b0f4064161805f2c6068a1708a Mon Sep 17 00:00:00 2001 From: Oliver Wong Date: Fri, 17 Apr 2026 16:05:22 +0800 Subject: [PATCH] feat: refactor audio generation and improve Excel handling - Add GenAudioConsts.cs to generate AudioConsts.cs with Cues, NameDictionaries, and Parameters - Remove GenEnums.cs and its enum generation functions - Update GenModels.cs to remove AudioObjectDefinitions generation - Modify ExcelHelper.cs to improve numeric and formula cell handling - Update Program.cs to use GenAudioConsts for audio constants generation - Update README.md with improved validation checks and CueSheet generation status --- ExcelTool/ExcelHelper.cs | 5 +- ExcelTool/Parser/GenAudioConsts.cs | 263 +++++++++++++++++++++++++++++ ExcelTool/Parser/GenEnums.cs | 91 ---------- ExcelTool/Parser/GenModels.cs | 5 - ExcelTool/Program.cs | 29 +--- README.md | 12 +- 6 files changed, 279 insertions(+), 126 deletions(-) create mode 100644 ExcelTool/Parser/GenAudioConsts.cs delete mode 100644 ExcelTool/Parser/GenEnums.cs diff --git a/ExcelTool/ExcelHelper.cs b/ExcelTool/ExcelHelper.cs index 6896c9a..224ae67 100644 --- a/ExcelTool/ExcelHelper.cs +++ b/ExcelTool/ExcelHelper.cs @@ -2,6 +2,7 @@ using NPOI.XSSF.UserModel; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using ExcelTool.Parser; @@ -158,13 +159,13 @@ namespace ExcelTool return cell.StringCellValue; case CellType.Numeric: - return DateUtil.IsCellDateFormatted(cell) ? cell.DateCellValue.ToString() : cell.NumericCellValue.ToString(); + return DateUtil.IsCellDateFormatted(cell) ? cell.DateCellValue.ToString() : cell.NumericCellValue.ToString(CultureInfo.InvariantCulture); case CellType.Boolean: return cell.BooleanCellValue ? "1" : "0"; case CellType.Formula: - return cell.ToString(); + return cell.StringCellValue; default: return ""; diff --git a/ExcelTool/Parser/GenAudioConsts.cs b/ExcelTool/Parser/GenAudioConsts.cs new file mode 100644 index 0000000..0bdb5ee --- /dev/null +++ b/ExcelTool/Parser/GenAudioConsts.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace ExcelTool.Parser +{ + public static class GenAudioConsts + { + public static bool Gen( + List parsedSheets, + List enumSheets, + string outputDir, + string nameSpace = "") + { + try + { + ParsedSheet audioSheet = parsedSheets?.Find(s => s.SheetName == "AudioObject"); + bool hasAudioObject = audioSheet != null; + bool hasEnums = enumSheets is { Count: > 0 }; + + if (!hasAudioObject && !hasEnums) return true; + + // 根据是否有 namespace 决定顶层缩进 + string i1 = string.IsNullOrEmpty(nameSpace) ? "" : "\t"; // class 级 + string i2 = i1 + "\t"; // class 成员级 + string i3 = i2 + "\t"; // 嵌套 class 成员级 + + StringBuilder sb = new(); + sb.Append("/*\n * auto generated by tools(注意:千万不要手动修改本文件)\n * AudioConsts\n */\n\n"); + sb.Append("using System.Collections.Generic;\nusing System.Diagnostics.CodeAnalysis;\n\n"); + + if (!string.IsNullOrEmpty(nameSpace)) + sb.Append($"namespace {nameSpace}\n{{\n"); + + // ── CUES ────────────────────────────────────────────────────────── + if (hasAudioObject) + { + AppendCues(sb, audioSheet, i1, i2); + sb.Append('\n'); + } + + // ── NAME_DICTIONARIES ───────────────────────────────────────────── + if (hasAudioObject) + { + AppendNameDictionaries(sb, audioSheet, i1, i2, i3); + sb.Append('\n'); + } + + // ── PARAMETERS ──────────────────────────────────────────────────── + if (hasEnums) + { + AppendParameters(sb, enumSheets, i1, i2, i3); + } + + if (!string.IsNullOrEmpty(nameSpace)) + sb.Append("}\n"); + + FileManager.WriteToFile(Path.Combine(outputDir, "AudioConsts.cs"), sb.ToString()); + return true; + } + catch (Exception ex) + { + ex.ToString().WriteErrorLine(); + return false; + } + } + + // ────────────────────────────────────────────────────────────────────────── + // CUES + // ────────────────────────────────────────────────────────────────────────── + + private static void AppendCues(StringBuilder sb, ParsedSheet audioSheet, string i1, string i2) + { + int idIndex = audioSheet.Headers.FindIndex(h => h.FieldName == "Id"); + int cueNameIndex = audioSheet.Headers.FindIndex(h => h.FieldName == "CueName"); + + // 如果没有 CueName 字段,回退到 Name 字段(向后兼容) + if (cueNameIndex < 0) + { + cueNameIndex = audioSheet.Headers.FindIndex(h => h.FieldName == "Name"); + if (cueNameIndex < 0) + { + // 两个字段都没有,跳过 CUES 生成 + sb.Append($"{i1}public class Cues\n{i1}{{\n"); + sb.Append($"{i1}}} //public class Cues\n"); + return; + } + } + + Dictionary cueNameToMinId = BuildNameToMinId(audioSheet, idIndex, cueNameIndex); + + sb.AppendLine($"{i1}[SuppressMessage(\"ReSharper\", \"InconsistentNaming\")]"); + sb.Append($"{i1}public class Cues\n{i1}{{\n"); + foreach (KeyValuePair kv in cueNameToMinId) + { + string fieldName = SanitizeIdentifier(kv.Key); + sb.Append($"{i2}public static uint {fieldName} = {kv.Value};\n"); + } + sb.Append($"{i1}}} //public class Cues\n"); + } + + // ────────────────────────────────────────────────────────────────────────── + // NAME_DICTIONARIES + // ────────────────────────────────────────────────────────────────────────── + + private static void AppendNameDictionaries( + StringBuilder sb, ParsedSheet audioSheet, + string i1, string i2, string i3) + { + int idIndex = audioSheet.Headers.FindIndex(h => h.FieldName == "Id"); + int namesIndex = audioSheet.Headers.FindIndex(h => h.FieldName == "Name"); + + Dictionary> idToNames = new(); + Dictionary> nameToIds = new(); + + foreach (TableExcelRow row in audioSheet.Data.Rows) + { + if (idIndex < 0 || namesIndex < 0) continue; + if (string.IsNullOrEmpty(row.StrList[idIndex])) continue; + + uint id = Convert.ToUInt32(row.StrList[idIndex]); + string rawNames = row.StrList[namesIndex]; + List names = new(rawNames.Split(['|'], StringSplitOptions.RemoveEmptyEntries)); + + idToNames[id] = names; + foreach (string name in names) + { + if (!nameToIds.TryGetValue(name, out List list)) + { + list = []; + nameToIds[name] = list; + } + list.Add(id); + } + } + + sb.AppendLine($"{i1}[SuppressMessage(\"ReSharper\", \"ClassNeverInstantiated.Global\")]"); + sb.Append($"{i1}public class NameDictionaries\n{i1}{{\n"); + + // NameToId + sb.Append($"{i2}public static readonly Dictionary NameToId = new()\n{i2}{{\n"); + foreach (var kv in nameToIds) + { + uint minId = uint.MaxValue; + foreach (uint id in kv.Value) + if (id < minId) minId = id; + sb.Append($"{i3}{{ \"{kv.Key}\", {minId} }},\n"); + } + sb.Append($"{i2}}};\n\n"); + + // AmbiguousNames + sb.Append($"{i2}public static readonly HashSet AmbiguousNames = new()\n{i2}{{\n"); + foreach (var kv in nameToIds) + if (kv.Value.Count > 1) + sb.Append($"{i3}\"{kv.Key}\",\n"); + sb.Append($"{i2}}};\n\n"); + + // SharedIdNames + HashSet sharedNames = []; + foreach (var kv in idToNames) + if (kv.Value.Count > 1) + foreach (string name in kv.Value) + sharedNames.Add(name); + + sb.Append($"{i2}public static readonly HashSet SharedIdNames = new()\n{i2}{{\n"); + foreach (string name in sharedNames) + sb.Append($"{i3}\"{name}\",\n"); + sb.Append($"{i2}}};\n"); + + sb.Append($"{i1}}} //public class NameDictionaries\n"); + } + + // ────────────────────────────────────────────────────────────────────────── + // PARAMETERS + // ────────────────────────────────────────────────────────────────────────── + + private static void AppendParameters( + StringBuilder sb, List enumSheets, + string i1, string i2, string i3) + { + sb.AppendLine($"{i1}[SuppressMessage(\"ReSharper\", \"ClassNeverInstantiated.Global\")]"); + sb.Append($"{i1}public class Parameters\n{i1}{{\n"); + + // EnumIds 嵌套静态类 + sb.Append($"{i2}public static class EnumIds\n{i2}{{\n"); + foreach (ParsedEnum parsedEnum in enumSheets) + sb.Append($"{i3}public const uint {parsedEnum.EnumName}Id = {parsedEnum.Id};\n\n"); + + sb.Append($"\n{i3}public static void RegisterAllGameState()\n{i3}{{\n"); + foreach (ParsedEnum parsedEnum in enumSheets) + sb.Append($"{i3}\tStateGroupRegistry.Register<{parsedEnum.EnumName}>({parsedEnum.Id});\n"); + sb.Append($"{i3}}}\n"); + sb.Append($"{i2}}}\n\n"); + + // enum 定义 + foreach (ParsedEnum parsedEnum in enumSheets) + { + sb.Append($"{i2}public enum {parsedEnum.EnumName}\n{i2}{{\n"); + foreach (ParsedEnumMember member in parsedEnum.Members) + { + string valuePart = member.Value.HasValue ? $" = {member.Value.Value}" : ""; + string descPart = string.IsNullOrEmpty(member.Desc) ? "" : $" // {member.Desc}"; + sb.Append($"{i3}{member.Key}{valuePart},{descPart}\n"); + } + sb.Append($"{i2}}}\n\n"); + } + + sb.Append($"{i1}}} //public class Parameters\n"); + } + + // ────────────────────────────────────────────────────────────────────────── + // 工具方法 + // ────────────────────────────────────────────────────────────────────────── + + /// 遍历 AudioObject 行,构建 name -> minId 映射(处理资源复用) + private static Dictionary BuildNameToMinId( + ParsedSheet audioSheet, int idIndex, int namesIndex) + { + Dictionary> nameToIds = new(); + foreach (TableExcelRow row in audioSheet.Data.Rows) + { + if (idIndex < 0 || namesIndex < 0) continue; + if (string.IsNullOrEmpty(row.StrList[idIndex])) continue; + + uint id = Convert.ToUInt32(row.StrList[idIndex]); + string rawNames = row.StrList[namesIndex]; + List names = new(rawNames.Split(['|'], StringSplitOptions.RemoveEmptyEntries)); + + foreach (string name in names) + { + if (!nameToIds.TryGetValue(name, out List list)) + { + list = []; + nameToIds[name] = list; + } + list.Add(id); + } + } + + Dictionary result = new(); + foreach (var kv in nameToIds) + { + uint minId = uint.MaxValue; + foreach (uint id in kv.Value) + if (id < minId) minId = id; + result[kv.Key] = minId; + } + return result; + } + + /// 将字符串中不合法的 C# 标识符字符替换为下划线 + private static string SanitizeIdentifier(string name) + { + StringBuilder sb = new(); + foreach (char c in name) + sb.Append(char.IsLetterOrDigit(c) || c == '_' ? c : '_'); + if (sb.Length > 0 && char.IsDigit(sb[0])) + sb.Insert(0, '_'); + return sb.ToString(); + } + } +} diff --git a/ExcelTool/Parser/GenEnums.cs b/ExcelTool/Parser/GenEnums.cs deleted file mode 100644 index 54c8859..0000000 --- a/ExcelTool/Parser/GenEnums.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace ExcelTool.Parser -{ - public static class GenEnums - { - /// - /// 所有枚举写入同一个文件 AudioEnums.cs - /// - public static bool GenCSharpEnum(List enumSheet, string outputDir, string nameSpace = "") - { - try - { - StringBuilder sb = new(); - sb.Append("/*\n * auto generated by tools(注意:千万不要手动修改本文件)\n * AudioEnums\n */\n\n"); - - if (!string.IsNullOrEmpty(nameSpace)) - sb.Append($"namespace {nameSpace}\n{{\n"); - - foreach (ParsedEnum parsedEnum in enumSheet) - { - sb.Append($"\tpublic enum {parsedEnum.EnumName}\n\t{{\n"); - - foreach (ParsedEnumMember member in parsedEnum.Members) - { - string valuePart = member.Value.HasValue ? $" = {member.Value.Value}" : ""; - string descPart = string.IsNullOrEmpty(member.Desc) ? "" : $" // {member.Desc}"; - sb.Append($"\t\t{member.Key}{valuePart},{descPart}\n"); - } - - sb.Append("\t}\n\n"); - } - - if (!string.IsNullOrEmpty(nameSpace)) - sb.Append("}\n"); - - FileManager.WriteToFile(Path.Combine(outputDir, "AudioEnums.cs"), sb.ToString()); - return true; - } - catch (Exception ex) - { - ex.ToString().WriteErrorLine(); - return false; - } - } - - /// - /// 将所有枚举的 Id 汇总,生成 EnumIds.cs 静态常量类 - /// - public static bool GenEnumIds(List enumSheet, string outputDir, string nameSpace = "") - { - try - { - StringBuilder sb = new(); - sb.Append("/*\n * auto generated by tools(注意:千万不要手动修改本文件)\n * EnumIds\n */\n\n"); - - if (!string.IsNullOrEmpty(nameSpace)) - sb.Append($"namespace {nameSpace}\n{{\n"); - - sb.Append("\tpublic static class EnumIds\n\t{\n"); - - foreach (ParsedEnum parsedEnum in enumSheet) - sb.AppendLine($"\t\tpublic const uint {parsedEnum.EnumName} = {parsedEnum.Id};\n"); - - sb.AppendLine("\t\tpublic static void RegisterAllGameState()\n\t\t{"); - foreach (ParsedEnum parsedEnum in enumSheet) - { - sb.AppendLine($"\t\t\tStateGroupRegistry.Register<{parsedEnum.EnumName}>({parsedEnum.Id});"); - } - sb.AppendLine("\t\t}"); - - sb.Append("\t}\n"); - - - if (!string.IsNullOrEmpty(nameSpace)) - sb.Append("}\n"); - - FileManager.WriteToFile(Path.Combine(outputDir, "AudioEnumIds.cs"), sb.ToString()); - return true; - } - catch (Exception ex) - { - ex.ToString().WriteErrorLine(); - return false; - } - } - } -} diff --git a/ExcelTool/Parser/GenModels.cs b/ExcelTool/Parser/GenModels.cs index 8f6899f..4df0e23 100644 --- a/ExcelTool/Parser/GenModels.cs +++ b/ExcelTool/Parser/GenModels.cs @@ -44,11 +44,6 @@ namespace ExcelTool.Parser FileManager.WriteToFile(Path.Combine(outputDir, $"{sheet.SheetName}.cs"), sb.ToString()); - // ── AudioObjectDefinitions 生成(仅针对 AudioObject 表) ───────────────────── - if (sheet.SheetName == "AudioObject") - { - GenerateAudioObjectDefinitions(parsedSheets, outputDir, nameSpace); - } } return true; diff --git a/ExcelTool/Program.cs b/ExcelTool/Program.cs index 2c29c24..5cd06f5 100644 --- a/ExcelTool/Program.cs +++ b/ExcelTool/Program.cs @@ -22,7 +22,6 @@ namespace ExcelTool rootCommand.AddOption(tablesOption); rootCommand.SetHandler(ExcelProcess.Run, tablesOption); - // TODO 单元测试 return await rootCommand.InvokeAsync(args); } @@ -53,8 +52,6 @@ namespace ExcelTool "== 说明:将exe放在xlsx目录中或者exe或者传入根目录 ==".WriteSuccessLine(); "==========================================================".WriteSuccessLine(); - // excels = dirInfo.GetFiles("*.xlsx", SearchOption.AllDirectories); - //读取 foreach (TableEntry tableEntry in tableEntries) { @@ -89,21 +86,12 @@ namespace ExcelTool $"{fileName}CS模板生成失败".WriteErrorLine(); } - // 生成CS枚举文件 - if (enumSheets.Count > 0) - { - bool enumRes = GenEnums.GenCSharpEnum(enumSheets, tableEntry.OutputCodeDir, tableEntry.Namespace); - if (enumRes) - $"{fileName} 枚举代码生成成功".WriteSuccessLine(); - else - $"{fileName} 枚举代码生成失败".WriteErrorLine(); - - bool enumIdsRes = GenEnums.GenEnumIds(enumSheets, tableEntry.OutputCodeDir, tableEntry.Namespace); - if (enumIdsRes) - $"{fileName} EnumIds 生成成功".WriteSuccessLine(); - else - $"{fileName} EnumIds 生成失败".WriteErrorLine(); - } + // 生成 AudioConsts.cs(合并枚举、枚举ID、AudioObject定义和CUE映射) + bool audioConstsRes = GenAudioConsts.Gen(sheets, enumSheets, tableEntry.OutputCodeDir, tableEntry.Namespace); + if (audioConstsRes) + $"{fileName} AudioConsts 生成成功".WriteSuccessLine(); + else + $"{fileName} AudioConsts 生成失败".WriteErrorLine(); //生成二进制文件,如果list或者vector数据为空则写入0,要根据类型来读取csv的字段数据强转成对应的数据类型然后写入 res = TableExcelExportBytes.ExportToFile(sheets, tableEntry.OutputDataDir); @@ -118,11 +106,6 @@ namespace ExcelTool } } } - - public static void Test() - { - - } } } diff --git a/README.md b/README.md index 40b4957..7bd23a4 100644 --- a/README.md +++ b/README.md @@ -285,11 +285,13 @@ pause * [x] 支持枚举类型 * [x] 支持不同表不同namespace * [ ] 支持Excel数据配置规范性检测,例如手误配置不符合规范导致加载异常,例如大小写逗号(肉眼容易忽略),或者空格等等 - * [ ] ID不能重复 - * [ ] Music、Audio Container不能自引用 - * [ ] 同时播放的音乐BPM必须一致 - * [ ] Blend容器不能配置Haptic ID -* [ ] 生成CueSheet避免magic number + * [ ] ID不能重复 给出错误 + * [ ] Music、Audio Container不能自引用 给出错误 + * [ ] 同时播放的音乐BPM必须一致 给出错误 + * [ ] CueName不能包含空格` `,和`-`,不能以数字开头。给出警告 + * [ ] Blend容器不能配置Haptic ID 给出警告 + * [ ] SyncPoint功能性需要两个文件BPM、采样率皆一致。给出警告 +* [x] 生成CueSheet避免magic number * [ ] AudioObject支持Container嵌套 #### Unity客户端使用范例