using System.Text.Json;
using System.Text.Json.Nodes;
namespace Shigure;
///
/// 从职业 keymap 文件构建模块编辑器可选择的技能与目标(unit)目录。
/// 同名技能只保留一个;unit 去重后升序排列。
/// 专精格式会聚合顶层回退与所有专精映射;有效条目规则与 KeymapService 保持一致。
///
public sealed class KeymapCatalog
{
private readonly string _baseDirectory;
private readonly ConfigService? _config;
private readonly Dictionary _cache = new(StringComparer.OrdinalIgnoreCase);
private KeymapCatalog(string baseDirectory, ConfigService? config)
{
_baseDirectory = baseDirectory;
_config = config;
}
public static KeymapCatalog Load(string baseDirectory)
{
try
{
return new KeymapCatalog(baseDirectory, ConfigService.LoadFromBaseDirectory(baseDirectory));
}
catch
{
// config 缺失或损坏时仍可回退到 keymap/keymap.json。
return new KeymapCatalog(baseDirectory, null);
}
}
/// 该职业 keymap 中出现过的技能名, 去重后按文件内首次出现的顺序排列。
public IReadOnlyList GetSpells(int? classId)
{
return GetEntries(classId).Spells;
}
/// 该职业 keymap 中出现过的 unit 编号, 去重后升序排列。
public IReadOnlyList GetUnits(int? classId)
{
return GetEntries(classId).Units;
}
///
/// 指定技能在该职业 keymap 中实际配置过的 unit 编号(去重升序)。
/// 技能为空时返回该职业全部 unit; 技能不在 keymap 中时返回空列表。
///
public IReadOnlyList GetUnitsForSpell(int? classId, string? spell)
{
var entries = GetEntries(classId);
if (string.IsNullOrEmpty(spell))
{
return entries.Units;
}
return entries.UnitsBySpell.TryGetValue(spell, out var units) ? units : [];
}
public IReadOnlyList GetUnitsForSpells(int? classId, IEnumerable spells)
{
var entries = GetEntries(classId);
var units = new SortedSet();
foreach (var spell in spells)
{
if (!string.IsNullOrWhiteSpace(spell)
&& entries.UnitsBySpell.TryGetValue(spell, out var spellUnits))
{
foreach (var unit in spellUnits)
{
units.Add(unit);
}
}
}
return units.ToList();
}
/// 指定技能和 unit 在 keymap 中配置过的宏条件,按文件内首次出现顺序返回。
public IReadOnlyList GetMacroConditions(int? classId, string? spell, int? unit)
{
if (string.IsNullOrWhiteSpace(spell))
{
return [];
}
var entries = GetEntries(classId);
return entries.MacroConditions.TryGetValue((spell, unit.GetValueOrDefault()), out var conditions)
? conditions
: [];
}
public IReadOnlyCollection GetFailedSpellNames(int? classId)
{
return _config?.GetFailedSpells(classId).Values.ToList() ?? [];
}
private KeymapEntries GetEntries(int? classId)
{
var path = ResolveKeymapPath(classId);
if (path is null)
{
return KeymapEntries.Empty;
}
if (_cache.TryGetValue(path, out var cached))
{
return cached;
}
var entries = ParseKeymap(path);
_cache[path] = entries;
return entries;
}
private string? ResolveKeymapPath(int? classId)
{
var path = ResolveKeymapFilePath(_baseDirectory, _config?.GetKeymapName(classId));
return File.Exists(path) ? path : null;
}
///
/// keymap 文件路径解析, 由 KeymapCatalog 与 KeymapService 共用以保证行为一致:
/// .yml 改写为 .json; 绝对路径直接使用, 否则相对 baseDirectory/keymap; 找不到回退 keymap/keymap.json。
/// 返回的是候选路径(可能仍不存在), 由调用方自行判断 File.Exists。
///
internal static string ResolveKeymapFilePath(string baseDirectory, string? keymapName)
{
var name = keymapName ?? "keymap.json";
if (name.EndsWith(".yml", StringComparison.OrdinalIgnoreCase))
{
name = Path.ChangeExtension(name, ".json");
}
var path = Path.IsPathRooted(name)
? name
: Path.Combine(baseDirectory, "keymap", name);
return File.Exists(path)
? path
: Path.Combine(baseDirectory, "keymap", "keymap.json");
}
private static KeymapEntries ParseKeymap(string path)
{
var spells = new List();
var units = new List();
var unitsBySpell = new Dictionary>(StringComparer.Ordinal);
var macroConditions = new Dictionary<(string Spell, int Unit), List>();
try
{
var root = JsonNode.Parse(File.ReadAllText(path), documentOptions: new JsonDocumentOptions
{
CommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true
}) as JsonObject;
if (root is null)
{
return KeymapEntries.Empty;
}
var seenSpells = new HashSet(StringComparer.Ordinal);
var seenUnits = new HashSet();
AddMap(root);
if (JsonHelpers.Get(root, "专精") is JsonObject specRoot)
{
foreach (var (_, node) in specRoot)
{
if (node is JsonObject specMap)
{
AddMap(specMap);
}
}
}
units.Sort();
foreach (var spellUnits in unitsBySpell.Values)
{
spellUnits.Sort();
}
void AddMap(JsonObject map)
{
foreach (var (_, node) in map)
{
if (node is not JsonObject entry)
{
continue;
}
var spell = JsonHelpers.GetString(JsonHelpers.Get(entry, "spell"))
?? JsonHelpers.GetString(JsonHelpers.Get(entry, "技能"));
var hotkey = JsonHelpers.GetString(JsonHelpers.Get(entry, "hotkey"))
?? JsonHelpers.GetString(JsonHelpers.Get(entry, "热键"));
// 与运行时一致: 只有技能和热键都非空的条目才能被查到并发送。
if (string.IsNullOrWhiteSpace(spell) || string.IsNullOrWhiteSpace(hotkey))
{
continue;
}
var rawUnit = JsonHelpers.GetInt(JsonHelpers.Get(entry, "unit")) ?? 0;
var normalizedMacro = MacroConditionText.NormalizeLegacyUnit(
rawUnit,
JsonHelpers.GetString(JsonHelpers.Get(entry, "宏条件")));
var unit = normalizedMacro.Unit;
var macroCondition = normalizedMacro.Condition;
if (seenSpells.Add(spell))
{
spells.Add(spell);
}
if (seenUnits.Add(unit))
{
units.Add(unit);
}
if (!unitsBySpell.TryGetValue(spell, out var spellUnits))
{
spellUnits = new List();
unitsBySpell[spell] = spellUnits;
}
if (!spellUnits.Contains(unit))
{
spellUnits.Add(unit);
}
var conditionKey = (spell, unit);
if (!macroConditions.TryGetValue(conditionKey, out var conditions))
{
conditions = new List();
macroConditions[conditionKey] = conditions;
}
if (!conditions.Contains(macroCondition, StringComparer.Ordinal))
{
conditions.Add(macroCondition);
}
}
}
}
catch
{
// keymap 损坏时返回空目录, 下拉降级为只有留空项。
return KeymapEntries.Empty;
}
return new KeymapEntries(
spells,
units,
unitsBySpell.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value, StringComparer.Ordinal),
macroConditions.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value));
}
private sealed record KeymapEntries(
IReadOnlyList Spells,
IReadOnlyList Units,
IReadOnlyDictionary> UnitsBySpell,
IReadOnlyDictionary<(string Spell, int Unit), IReadOnlyList> MacroConditions)
{
public static readonly KeymapEntries Empty = new(
[],
[],
new Dictionary>(StringComparer.Ordinal),
new Dictionary<(string Spell, int Unit), IReadOnlyList>());
}
}