using System.Text; using static Shigure.LuaLiteParser; namespace Shigure; /// /// 读写 Fuyutsui core/classmacros.lua 中的 ClassMacros, /// 保存时只替换 ClassMacros 表字面量,保留文件其余内容。 /// internal static class ClassMacrosStore { public const string AssignmentName = "Fuyutsui.ClassMacros"; public const string MacroBodiesAssignmentName = "Fuyutsui.MacroBodies"; private static readonly string[] ClassFileOrder = [ "WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST", "DEATHKNIGHT", "SHAMAN", "MAGE", "WARLOCK", "MONK", "DRUID", "DEMONHUNTER", "EVOKER" ]; public sealed class MacrosDocument { public string FilePath { get; set; } = string.Empty; public string SourceText { get; set; } = string.Empty; public int TableStart { get; set; } public int TableEndExclusive { get; set; } public Dictionary Classes { get; set; } = new(StringComparer.OrdinalIgnoreCase); public List ClassOrder { get; set; } = new(); } public sealed class ClassMacros { /// /// false 表示旧式纯数组;true 表示 common + [specIndex] 分组格式。 /// 两种格式的通用项都保存在 DynamicCommon 中,以便旧文件无损回写。 /// public bool UsesSpecDynamicSpells { get; set; } public List DynamicCommon { get; } = new(); public Dictionary> DynamicBySpec { get; } = new(); public List StaticSpells { get; } = new(); public List SpecialSpells { get; } = new(); public IReadOnlyList ResolveDynamicSpells(int? specIndex) { if (!UsesSpecDynamicSpells || specIndex is null || !DynamicBySpec.TryGetValue(specIndex.Value, out var specSpells)) { return DynamicCommon; } return [.. DynamicCommon, .. specSpells]; } } public sealed class ArrayEntry { public string Text { get; set; } = string.Empty; /// /// Lua 同行注释。静态宏沿用注释/技能名覆盖语义;特殊宏专门用它保存手工技能名。 /// public string? Comment { get; set; } } public static string ToClassFileKey(int classId) => ClassNames.GetConfigFileName(classId).ToUpperInvariant(); public static MacrosDocument Load(string filePath) { var source = File.ReadAllText(filePath, Encoding.UTF8); if (!TryExtractAssignedTable(source, AssignmentName, out var table, out var start, out var end)) { throw new InvalidDataException($"{Path.GetFileName(filePath)} 中未找到 {AssignmentName}"); } var doc = new MacrosDocument { FilePath = filePath, SourceText = source, TableStart = start, TableEndExclusive = end }; foreach (var (key, value) in table.Entries) { if (key is not string classFile || value is not TableValue classTable) { continue; } var macros = ParseClass(classTable); doc.Classes[classFile] = macros; doc.ClassOrder.Add(classFile); } return doc; } public static IReadOnlyDictionary LoadMacroBodies(string filePath) { var result = new Dictionary(StringComparer.Ordinal); if (!File.Exists(filePath)) { return result; } var source = File.ReadAllText(filePath, Encoding.UTF8); if (!TryExtractAssignedTable(source, MacroBodiesAssignmentName, out var table, out _, out _)) { return result; } foreach (var (key, value) in table.Entries) { if (key is string name && !string.IsNullOrWhiteSpace(name) && value is StringValue text && !string.IsNullOrWhiteSpace(text.Value)) { result.TryAdd(name.Trim(), text.Value); } } return result; } public static void Save(MacrosDocument document) { var serialized = SerializeClassMacros(document); var updated = document.SourceText[..document.TableStart] + serialized + document.SourceText[document.TableEndExclusive..]; AtomicFile.WriteAllText(document.FilePath, updated, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); if (!TryExtractAssignedTable(updated, AssignmentName, out _, out var start, out var end)) { throw new InvalidOperationException("保存后无法重新定位 ClassMacros 表。"); } document.SourceText = updated; document.TableStart = start; document.TableEndExclusive = end; } private static ClassMacros ParseClass(TableValue classTable) { var macros = new ClassMacros(); if (classTable.GetTable("dynamicSpells") is { } dynamic) { var specIndexes = dynamic.Entries .Select(entry => TryGetPositiveIndex(entry.Key)) .Where(index => index is not null) .Select(index => index!.Value) .Distinct() .Where(index => dynamic.GetTable((long)index) is not null) .OrderBy(index => index) .ToList(); macros.UsesSpecDynamicSpells = dynamic.GetTable("common") is not null || specIndexes.Count > 0; if (macros.UsesSpecDynamicSpells) { ReadStringArray(dynamic.GetTable("common"), macros.DynamicCommon); foreach (var specIndex in specIndexes) { var spells = new List(); ReadStringArray(dynamic.GetTable((long)specIndex), spells); macros.DynamicBySpec[specIndex] = spells; } } else { ReadStringArray(dynamic, macros.DynamicCommon); } } ReadArray(classTable.GetTable("staticSpells"), macros.StaticSpells); ReadArray(classTable.GetTable("specialSpells"), macros.SpecialSpells); return macros; } private static void ReadArray(TableValue? table, List target) { if (table is null) { return; } var index = 1; foreach (var value in table.IPairs()) { if (value is not StringValue text) { break; } target.Add(new ArrayEntry { Text = text.Value, Comment = table.GetTrailingComment((long)index) }); index++; } } private static void ReadStringArray(TableValue? table, List target) { if (table is null) { return; } foreach (var value in table.IPairs()) { if (value is not StringValue text) { break; } target.Add(text.Value); } } private static int? TryGetPositiveIndex(object? key) { var value = key switch { long number when number is > 0 and <= int.MaxValue => (int)number, int number when number > 0 => number, _ => (int?)null }; return value; } public static string SerializeClassMacros(MacrosDocument document) { var sb = new StringBuilder(); sb.AppendLine("{"); var written = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var classFile in EnumerateClassOrder(document)) { if (!document.Classes.TryGetValue(classFile, out var macros)) { continue; } written.Add(classFile); WriteClass(sb, classFile, macros); } foreach (var (classFile, macros) in document.Classes) { if (written.Contains(classFile)) { continue; } WriteClass(sb, classFile, macros); } sb.Append('}'); return sb.ToString(); } private static IEnumerable EnumerateClassOrder(MacrosDocument document) { if (document.ClassOrder.Count > 0) { foreach (var key in document.ClassOrder) { yield return key; } yield break; } foreach (var key in ClassFileOrder) { yield return key; } } private static void WriteClass(StringBuilder sb, string classFile, ClassMacros macros) { sb.Append(" ").Append(classFile).AppendLine(" = {"); WriteDynamicSpells(sb, macros); // staticSpells WriteArrayTable(sb, "staticSpells", macros.StaticSpells); // specialSpells WriteArrayTable(sb, "specialSpells", macros.SpecialSpells); sb.AppendLine(" },"); sb.AppendLine(); } private static void WriteDynamicSpells(StringBuilder sb, ClassMacros macros) { if (!macros.UsesSpecDynamicSpells) { WriteInlineStringArray(sb, " dynamicSpells = ", macros.DynamicCommon); return; } sb.AppendLine(" dynamicSpells = {"); WriteInlineStringArray(sb, " common = ", macros.DynamicCommon); foreach (var (specIndex, spells) in macros.DynamicBySpec.OrderBy(item => item.Key)) { WriteInlineStringArray(sb, $" [{specIndex}] = ", spells); } sb.AppendLine(" },"); } private static void WriteInlineStringArray(StringBuilder sb, string prefix, IReadOnlyList values) { sb.Append(prefix); if (values.Count == 0) { sb.AppendLine("{},"); return; } sb.Append("{ "); sb.Append(string.Join(", ", values.Select(value => $"\"{Escape(value)}\""))); sb.AppendLine(" },"); } private static void WriteArrayTable(StringBuilder sb, string name, List entries) { if (entries.Count == 0) { sb.Append(" ").Append(name).AppendLine(" = {},"); return; } sb.Append(" ").Append(name).AppendLine(" = {"); foreach (var entry in entries) { sb.Append(" \"").Append(Escape(entry.Text)).Append('"'); if (!string.IsNullOrWhiteSpace(entry.Comment)) { sb.Append(", -- ").Append(entry.Comment.Trim()); } else { sb.Append(','); } sb.AppendLine(); } sb.AppendLine(" },"); } private static string Escape(string value) { var sb = new StringBuilder(value.Length + 8); foreach (var ch in value) { switch (ch) { case '\\': sb.Append("\\\\"); break; case '"': sb.Append("\\\""); break; case '\n': sb.Append("\\n"); break; case '\r': sb.Append("\\r"); break; case '\t': sb.Append("\\t"); break; default: sb.Append(ch); break; } } return sb.ToString(); } }