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
This commit is contained in:
2026-04-17 16:05:22 +08:00
parent 6f2cc57eac
commit fb7beedbad
6 changed files with 279 additions and 126 deletions
+3 -2
View File
@@ -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 "";
+263
View File
@@ -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<ParsedSheet> parsedSheets,
List<ParsedEnum> 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<string, uint> cueNameToMinId = BuildNameToMinId(audioSheet, idIndex, cueNameIndex);
sb.AppendLine($"{i1}[SuppressMessage(\"ReSharper\", \"InconsistentNaming\")]");
sb.Append($"{i1}public class Cues\n{i1}{{\n");
foreach (KeyValuePair<string, uint> 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<uint, List<string>> idToNames = new();
Dictionary<string, List<uint>> 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<string> names = new(rawNames.Split(['|'], StringSplitOptions.RemoveEmptyEntries));
idToNames[id] = names;
foreach (string name in names)
{
if (!nameToIds.TryGetValue(name, out List<uint> 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<string, uint> 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<string> 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<string> 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<string> 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<ParsedEnum> 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");
}
// ──────────────────────────────────────────────────────────────────────────
// 工具方法
// ──────────────────────────────────────────────────────────────────────────
/// <summary>遍历 AudioObject 行,构建 name -> minId 映射(处理资源复用)</summary>
private static Dictionary<string, uint> BuildNameToMinId(
ParsedSheet audioSheet, int idIndex, int namesIndex)
{
Dictionary<string, List<uint>> 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<string> names = new(rawNames.Split(['|'], StringSplitOptions.RemoveEmptyEntries));
foreach (string name in names)
{
if (!nameToIds.TryGetValue(name, out List<uint> list))
{
list = [];
nameToIds[name] = list;
}
list.Add(id);
}
}
Dictionary<string, uint> 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;
}
/// <summary>将字符串中不合法的 C# 标识符字符替换为下划线</summary>
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();
}
}
}
-91
View File
@@ -1,91 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace ExcelTool.Parser
{
public static class GenEnums
{
/// <summary>
/// 所有枚举写入同一个文件 AudioEnums.cs
/// </summary>
public static bool GenCSharpEnum(List<ParsedEnum> 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;
}
}
/// <summary>
/// 将所有枚举的 Id 汇总,生成 EnumIds.cs 静态常量类
/// </summary>
public static bool GenEnumIds(List<ParsedEnum> 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;
}
}
}
}
-5
View File
@@ -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;
+6 -23
View File
@@ -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()
{
}
}
}
+7 -5
View File
@@ -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客户端使用范例