Files
Shigure/Infrastructure/ClassBlocksStore.cs
waynebian01 3c206fc86b 配置与宏
2026-07-28 20:31:34 +08:00

571 lines
19 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.Globalization;
using System.Text;
using static Shigure.LuaLiteParser;
namespace Shigure;
/// <summary>
/// 读写 Fuyutsui class/*.lua 中的 ClassBlocksstates/auras/spells/group
/// 保存时只替换 ClassBlocks 表字面量,保留文件其余内容。
/// </summary>
internal static class ClassBlocksStore
{
public const string AssignmentName = "Fuyutsui.ClassBlocks";
public sealed class ClassFileDocument
{
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<int, SpecBlocks> Specs { get; set; } = new();
public bool IsModernFormat { get; set; }
}
public sealed class SpecBlocks
{
public bool NestedStates { get; set; } = true;
public List<string> FlatStates { get; } = new();
public Dictionary<string, List<string>> CategorizedStates { get; } = new(StringComparer.Ordinal)
{
["状态"] = new List<string>(),
["目标"] = new List<string>(),
["焦点"] = new List<string>()
};
public List<AuraEntry> PlayerAuras { get; } = new();
public List<AuraEntry> TargetHarmfulAuras { get; } = new();
public List<AuraEntry> TargetHelpfulAuras { get; } = new();
public List<AuraEntry> FocusHarmfulAuras { get; } = new();
public List<AuraEntry> FocusHelpfulAuras { get; } = new();
public List<SpellEntry> Spells { get; } = new();
public GroupBlocks? Group { get; set; }
}
public sealed class AuraEntry
{
public string Name { get; set; } = string.Empty;
public long? SpellId { get; set; }
public List<long> SpellIds { get; } = new();
public int? MaxApps { get; set; }
}
public sealed class SpellEntry
{
public string Name { get; set; } = string.Empty;
public long SpellId { get; set; }
public bool Charge { get; set; }
public int? MaxCharge { get; set; }
public int? CastCount { get; set; }
public bool ForcedKnown { get; set; }
public bool InSpellBook { get; set; }
}
public sealed class GroupBlocks
{
public int Num { get; set; } = 5;
public int? HealthPercent { get; set; } = 1;
public int? Role { get; set; } = 2;
public int? Dispel { get; set; }
public List<GroupAuraEntry> Auras { get; } = new();
}
public sealed class GroupAuraEntry
{
public int Offset { get; set; }
public string Name { get; set; } = string.Empty;
public long? SpellId { get; set; }
public List<long> SpellIds { get; } = new();
}
public static ClassFileDocument 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 specs = new Dictionary<int, SpecBlocks>();
var modern = false;
foreach (var (key, value) in table.Entries)
{
if (key is not long specId || value is not TableValue specTable)
{
continue;
}
var spec = ParseSpec(specTable, out var specModern);
modern |= specModern;
specs[(int)specId] = spec;
}
return new ClassFileDocument
{
FilePath = filePath,
SourceText = source,
TableStart = start,
TableEndExclusive = end,
Specs = specs,
IsModernFormat = modern
};
}
public static void Save(ClassFileDocument document)
{
if (!document.IsModernFormat)
{
throw new InvalidOperationException("当前文件仍是旧版稀疏索引 ClassBlocks无法用图形编辑器保存。");
}
var serialized = SerializeClassBlocks(document.Specs);
var updated = document.SourceText[..document.TableStart]
+ serialized
+ document.SourceText[document.TableEndExclusive..];
File.WriteAllText(document.FilePath, updated, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
if (!TryExtractAssignedTable(updated, AssignmentName, out _, out var start, out var end))
{
throw new InvalidOperationException("保存后无法重新定位 ClassBlocks 表。");
}
document.SourceText = updated;
document.TableStart = start;
document.TableEndExclusive = end;
}
private static SpecBlocks ParseSpec(TableValue spec, out bool isModern)
{
var result = new SpecBlocks();
isModern = spec.GetTable("states") is not null
|| spec.GetTable("auras") is not null
|| spec.GetTable("spells") is not null
|| spec.GetTable("group") is not null;
if (!isModern)
{
return result;
}
if (spec.GetTable("states") is { } states)
{
var nested = states.GetTable("状态") is not null
|| states.GetTable("目标") is not null
|| states.GetTable("焦点") is not null;
result.NestedStates = nested;
if (nested)
{
foreach (var category in new[] { "状态", "目标", "焦点" })
{
if (states.GetTable(category) is not { } list)
{
continue;
}
var target = result.CategorizedStates[category];
foreach (var item in list.IPairs())
{
if (item is StringValue name && !string.IsNullOrWhiteSpace(name.Value))
{
target.Add(name.Value);
}
}
}
}
else
{
foreach (var item in states.IPairs())
{
if (item is StringValue name && !string.IsNullOrWhiteSpace(name.Value))
{
result.FlatStates.Add(name.Value);
}
}
}
}
if (spec.GetTable("auras") is { } auras)
{
var nested = auras.GetTable("player") is not null
|| auras.GetTable("target") is not null
|| auras.GetTable("focus") is not null;
if (nested)
{
AppendAuraList(auras.GetTable("player"), result.PlayerAuras);
if (auras.GetTable("target") is { } target)
{
AppendAuraList(target.GetTable("harmful"), result.TargetHarmfulAuras);
AppendAuraList(target.GetTable("helpful"), result.TargetHelpfulAuras);
}
if (auras.GetTable("focus") is { } focus)
{
AppendAuraList(focus.GetTable("harmful"), result.FocusHarmfulAuras);
AppendAuraList(focus.GetTable("helpful"), result.FocusHelpfulAuras);
}
}
else
{
AppendAuraList(auras, result.PlayerAuras);
}
}
if (spec.GetTable("spells") is { } spells)
{
foreach (var item in spells.IPairs())
{
if (item is not TableValue spell)
{
continue;
}
var spellId = spell.GetNumber("spellId");
if (spellId is null)
{
continue;
}
result.Spells.Add(new SpellEntry
{
SpellId = (long)spellId.Value,
Name = spell.GetString("name")?.Trim() ?? string.Empty,
Charge = spell.GetBool("charge") == true,
MaxCharge = spell.GetNumber("maxCharge") is { } maxCharge ? (int)maxCharge : null,
CastCount = spell.GetNumber("castCount") is { } castCount ? (int)castCount : null,
ForcedKnown = spell.GetBool("forcedKnown") == true,
InSpellBook = spell.GetBool("inSpellBook") == true
});
}
}
if (spec.GetTable("group") is { } group)
{
var groupBlocks = new GroupBlocks
{
Num = (int)(group.GetNumber("num") ?? 5),
HealthPercent = group.GetNumber("healthPercent") is { } hp ? (int)hp : null,
Role = group.GetNumber("role") is { } role ? (int)role : null,
Dispel = group.GetNumber("dispel") is { } dispel ? (int)dispel : null
};
if (group.GetTable("aura") is { } auraOffsets)
{
foreach (var (key, value) in auraOffsets.Entries)
{
if (key is not long offset || value is not TableValue auraInfo)
{
continue;
}
var entry = new GroupAuraEntry
{
Offset = (int)offset,
Name = auraInfo.GetString("name")?.Trim() ?? string.Empty
};
if (auraInfo.GetNumber("spellId") is { } sid)
{
entry.SpellId = (long)sid;
}
if (auraInfo.Get("spellIds") is TableValue ids)
{
foreach (var idItem in ids.IPairs())
{
if (idItem is NumberValue n)
{
entry.SpellIds.Add(n.AsInt());
}
}
}
groupBlocks.Auras.Add(entry);
}
groupBlocks.Auras.Sort((a, b) => a.Offset.CompareTo(b.Offset));
}
result.Group = groupBlocks;
}
return result;
}
private static void AppendAuraList(TableValue? list, List<AuraEntry> target)
{
if (list is null)
{
return;
}
foreach (var item in list.IPairs())
{
if (item is not TableValue aura)
{
continue;
}
var entry = new AuraEntry
{
Name = aura.GetString("name")?.Trim() ?? string.Empty,
MaxApps = aura.GetNumber("maxApps") is { } maxApps ? (int)maxApps : null
};
if (aura.GetNumber("spellId") is { } sid)
{
entry.SpellId = (long)sid;
}
if (aura.Get("spellIds") is TableValue ids)
{
foreach (var idItem in ids.IPairs())
{
if (idItem is NumberValue n)
{
entry.SpellIds.Add(n.AsInt());
}
}
}
target.Add(entry);
}
}
public static string SerializeClassBlocks(IReadOnlyDictionary<int, SpecBlocks> specs)
{
var sb = new StringBuilder();
sb.AppendLine("{");
foreach (var specId in specs.Keys.OrderBy(x => x))
{
sb.Append(" [").Append(specId).AppendLine("] = {");
WriteSpec(sb, specs[specId], " ");
sb.AppendLine(" },");
}
sb.Append('}');
return sb.ToString();
}
private static void WriteSpec(StringBuilder sb, SpecBlocks spec, string indent)
{
// states
sb.Append(indent).AppendLine("states = {");
if (spec.NestedStates)
{
foreach (var category in new[] { "状态", "目标", "焦点" })
{
var list = spec.CategorizedStates.GetValueOrDefault(category);
if (list is null || list.Count == 0)
{
continue;
}
sb.Append(indent).Append(" [\"").Append(Escape(category)).AppendLine("\"] = {");
foreach (var name in list)
{
sb.Append(indent).Append(" \"").Append(Escape(name)).AppendLine("\",");
}
sb.Append(indent).AppendLine(" },");
}
}
else
{
foreach (var name in spec.FlatStates)
{
sb.Append(indent).Append(" \"").Append(Escape(name)).AppendLine("\",");
}
}
sb.Append(indent).AppendLine("},");
// auras
var hasAuras = spec.PlayerAuras.Count > 0
|| spec.TargetHarmfulAuras.Count > 0
|| spec.TargetHelpfulAuras.Count > 0
|| spec.FocusHarmfulAuras.Count > 0
|| spec.FocusHelpfulAuras.Count > 0;
if (hasAuras)
{
sb.Append(indent).AppendLine("auras = {");
WriteAuraUnit(sb, "player", null, spec.PlayerAuras, indent + " ");
WriteAuraSplitUnit(sb, "target", spec.TargetHarmfulAuras, spec.TargetHelpfulAuras, indent + " ");
WriteAuraSplitUnit(sb, "focus", spec.FocusHarmfulAuras, spec.FocusHelpfulAuras, indent + " ");
sb.Append(indent).AppendLine("},");
}
// spells
if (spec.Spells.Count > 0)
{
sb.Append(indent).AppendLine("spells = {");
foreach (var spell in spec.Spells)
{
sb.Append(indent).Append(" { spellId = ").Append(spell.SpellId);
if (!string.IsNullOrWhiteSpace(spell.Name))
{
sb.Append(", name = \"").Append(Escape(spell.Name)).Append('"');
}
if (spell.Charge)
{
sb.Append(", charge = true");
}
if (spell.MaxCharge is { } maxCharge)
{
sb.Append(", maxCharge = ").Append(maxCharge);
}
if (spell.CastCount is { } castCount)
{
sb.Append(", castCount = ").Append(castCount);
}
if (spell.ForcedKnown)
{
sb.Append(", forcedKnown = true");
}
if (spell.InSpellBook)
{
sb.Append(", inSpellBook = true");
}
sb.AppendLine(" },");
}
sb.Append(indent).AppendLine("},");
}
// group
if (spec.Group is { } group)
{
sb.Append(indent).AppendLine("group = {");
sb.Append(indent).Append(" num = ").Append(group.Num).AppendLine(",");
if (group.HealthPercent is { } hp)
{
sb.Append(indent).Append(" healthPercent = ").Append(hp).AppendLine(",");
}
if (group.Role is { } role)
{
sb.Append(indent).Append(" role = ").Append(role).AppendLine(",");
}
if (group.Dispel is { } dispel)
{
sb.Append(indent).Append(" dispel = ").Append(dispel).AppendLine(",");
}
if (group.Auras.Count > 0)
{
sb.Append(indent).AppendLine(" aura = {");
foreach (var aura in group.Auras.OrderBy(a => a.Offset))
{
sb.Append(indent).Append(" [").Append(aura.Offset).Append("] = {");
if (!string.IsNullOrWhiteSpace(aura.Name))
{
sb.Append(" name = \"").Append(Escape(aura.Name)).Append("\",");
}
WriteSpellIdFields(sb, aura.SpellId, aura.SpellIds);
sb.AppendLine(" },");
}
sb.Append(indent).AppendLine(" },");
}
sb.Append(indent).AppendLine("},");
}
}
private static void WriteAuraUnit(StringBuilder sb, string unit, string? filter, List<AuraEntry> list, string indent)
{
if (list.Count == 0)
{
return;
}
if (filter is null)
{
sb.Append(indent).Append(unit).AppendLine(" = {");
foreach (var aura in list)
{
WriteAuraEntry(sb, aura, indent + " ");
}
sb.Append(indent).AppendLine("},");
}
}
private static void WriteAuraSplitUnit(
StringBuilder sb,
string unit,
List<AuraEntry> harmful,
List<AuraEntry> helpful,
string indent)
{
if (harmful.Count == 0 && helpful.Count == 0)
{
return;
}
sb.Append(indent).Append(unit).AppendLine(" = {");
if (harmful.Count > 0)
{
sb.Append(indent).AppendLine(" harmful = {");
foreach (var aura in harmful)
{
WriteAuraEntry(sb, aura, indent + " ");
}
sb.Append(indent).AppendLine(" },");
}
if (helpful.Count > 0)
{
sb.Append(indent).AppendLine(" helpful = {");
foreach (var aura in helpful)
{
WriteAuraEntry(sb, aura, indent + " ");
}
sb.Append(indent).AppendLine(" },");
}
sb.Append(indent).AppendLine("},");
}
private static void WriteAuraEntry(StringBuilder sb, AuraEntry aura, string indent)
{
sb.Append(indent).Append("{");
if (!string.IsNullOrWhiteSpace(aura.Name))
{
sb.Append(" name = \"").Append(Escape(aura.Name)).Append("\",");
}
WriteSpellIdFields(sb, aura.SpellId, aura.SpellIds);
if (aura.MaxApps is { } maxApps)
{
sb.Append(" maxApps = ").Append(maxApps).Append(',');
}
sb.AppendLine(" },");
}
private static void WriteSpellIdFields(StringBuilder sb, long? spellId, List<long> spellIds)
{
if (spellIds.Count > 0)
{
sb.Append(" spellIds = { ");
sb.Append(string.Join(", ", spellIds.Select(id => id.ToString(CultureInfo.InvariantCulture))));
sb.Append(" },");
}
else if (spellId is { } id)
{
sb.Append(" spellId = ").Append(id).Append(',');
}
}
private static string Escape(string value)
=> value.Replace("\\", "\\\\", StringComparison.Ordinal).Replace("\"", "\\\"", StringComparison.Ordinal);
}