mirror of
https://github.com/waynebian01/Shigure.git
synced 2026-09-03 07:15:24 +08:00
feat: 添加宏条件支持到 keymap,更新 priest.json 中的技能条目
This commit is contained in:
@@ -188,6 +188,7 @@ internal static partial class FuyutsuiKeymapConverter
|
||||
var hotkey = MacroKind[i - 1];
|
||||
var unit = 0;
|
||||
var spell = string.Empty;
|
||||
var macroCondition = string.Empty;
|
||||
|
||||
if (i <= dynamicSlots)
|
||||
{
|
||||
@@ -226,12 +227,14 @@ internal static partial class FuyutsuiKeymapConverter
|
||||
var parsed = ParseStaticMacro(macroEntry.Body, macroEntry.Comment);
|
||||
unit = parsed.Unit;
|
||||
spell = parsed.Spell;
|
||||
macroCondition = parsed.Condition;
|
||||
}
|
||||
else
|
||||
{
|
||||
var parsed = ParseSpecialMacro(macroEntry.Body, macroEntry.Comment);
|
||||
unit = parsed.Unit;
|
||||
spell = parsed.Spell;
|
||||
macroCondition = parsed.Condition;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +252,7 @@ internal static partial class FuyutsuiKeymapConverter
|
||||
root[i.ToString()] = new JsonObject
|
||||
{
|
||||
["unit"] = unit,
|
||||
["宏条件"] = macroCondition,
|
||||
["技能"] = spell,
|
||||
["热键"] = hotkey
|
||||
};
|
||||
@@ -305,10 +309,10 @@ internal static partial class FuyutsuiKeymapConverter
|
||||
return spell.StartsWith("item:", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal readonly record struct ParsedMacro(int Unit, string Spell);
|
||||
internal readonly record struct ParsedMacro(int Unit, string Spell, string Condition);
|
||||
|
||||
/// <summary>
|
||||
/// 解析静态宏供 keymap 与宏列表共用。目标类单位使用 31-35,引导/非引导使用 36-37。
|
||||
/// 解析静态宏供 keymap 与宏列表共用。只有方括号内以 @ 开头的项属于目标。
|
||||
/// </summary>
|
||||
internal static ParsedMacro ParseStaticMacro(string raw, string? comment = null)
|
||||
{
|
||||
@@ -317,16 +321,14 @@ internal static partial class FuyutsuiKeymapConverter
|
||||
? ResolveUnitName(target.Groups["unit"].Value)
|
||||
: ReservedUnit.None;
|
||||
|
||||
if (unit == ReservedUnit.None && SpecialUnitRegex().Match(raw) is { Success: true } directUnit)
|
||||
{
|
||||
unit = ResolveUnitName(directUnit.Groups["unit"].Value);
|
||||
}
|
||||
|
||||
return new ParsedMacro(unit, ResolveSpellName(new MacroEntry(raw, comment)));
|
||||
return new ParsedMacro(
|
||||
unit,
|
||||
ResolveSpellName(new MacroEntry(raw, comment)),
|
||||
ResolveConditions(raw));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析特殊宏:方括号中的首个单位作为 unit,技能沿用宏技能推导(castsequence 只取逗号前首项)。
|
||||
/// 解析特殊宏:方括号中以 @ 开头的首个单位作为 unit,技能沿用宏技能推导(castsequence 只取逗号前首项)。
|
||||
/// </summary>
|
||||
internal static ParsedMacro ParseSpecialMacro(string raw, string? comment = null)
|
||||
{
|
||||
@@ -336,15 +338,21 @@ internal static partial class FuyutsuiKeymapConverter
|
||||
? ResolveUnitName(target.Groups["unit"].Value)
|
||||
: ReservedUnit.None;
|
||||
|
||||
// 特殊宏也接受 [player]/[玩家]/[31] 这种“方括号内直接写单位”的简写。
|
||||
if (unit == ReservedUnit.None && SpecialUnitRegex().Match(raw) is { Success: true } specialUnit)
|
||||
{
|
||||
unit = ResolveUnitName(specialUnit.Groups["unit"].Value);
|
||||
}
|
||||
|
||||
var spell = ResolveSpellName(new MacroEntry(raw, comment));
|
||||
spell = spell.Split(',', 2, StringSplitOptions.TrimEntries)[0];
|
||||
return new ParsedMacro(unit, spell);
|
||||
spell = SplitTopLevel(spell, ',').FirstOrDefault()?.Trim() ?? string.Empty;
|
||||
return new ParsedMacro(unit, spell, ResolveConditions(raw));
|
||||
}
|
||||
|
||||
/// <summary>方括号中以 @ 开头的是目标,其余逗号分隔项作为只读条件摘要。</summary>
|
||||
private static string ResolveConditions(string raw)
|
||||
{
|
||||
var conditions = ConditionRegex().Matches(raw)
|
||||
.SelectMany(bracket => bracket.Value.Length < 2
|
||||
? []
|
||||
: bracket.Value[1..^1]
|
||||
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
||||
.Where(item => !item.StartsWith('@'));
|
||||
return MacroConditionText.Normalize(string.Join(", ", conditions));
|
||||
}
|
||||
|
||||
private static int ResolveUnitName(string raw)
|
||||
@@ -356,8 +364,6 @@ internal static partial class FuyutsuiKeymapConverter
|
||||
"focus" or "焦点" or "33" => ReservedUnit.Focus,
|
||||
"cursor" or "地面" or "34" => ReservedUnit.Cursor,
|
||||
"mouseover" or "鼠标" or "35" => ReservedUnit.Mouseover,
|
||||
"channeling" or "引导中" or "36" => ReservedUnit.Channeling,
|
||||
"nochanneling" or "非引导" or "37" => ReservedUnit.NoChanneling,
|
||||
_ => ReservedUnit.None
|
||||
};
|
||||
}
|
||||
@@ -393,8 +399,14 @@ internal static partial class FuyutsuiKeymapConverter
|
||||
{
|
||||
var sequenceBody = castSequence.Groups[1].Value.Trim();
|
||||
sequenceBody = ResetOptionRegex().Replace(sequenceBody, string.Empty).Trim();
|
||||
foreach (var part in sequenceBody.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
||||
foreach (var rawPart in SplitTopLevel(sequenceBody, ','))
|
||||
{
|
||||
var part = rawPart.Trim();
|
||||
if (part.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.Equals("x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
@@ -445,6 +457,35 @@ internal static partial class FuyutsuiKeymapConverter
|
||||
return spell;
|
||||
}
|
||||
|
||||
/// <summary>按顶层分隔符切分,方括号内的逗号不作为技能分隔符。</summary>
|
||||
private static IEnumerable<string> SplitTopLevel(string text, char separator)
|
||||
{
|
||||
var start = 0;
|
||||
var bracketDepth = 0;
|
||||
for (var i = 0; i < text.Length; i++)
|
||||
{
|
||||
switch (text[i])
|
||||
{
|
||||
case '[':
|
||||
bracketDepth++;
|
||||
break;
|
||||
case ']' when bracketDepth > 0:
|
||||
bracketDepth--;
|
||||
break;
|
||||
default:
|
||||
if (text[i] == separator && bracketDepth == 0)
|
||||
{
|
||||
yield return text[start..i];
|
||||
start = i + 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
yield return text[start..];
|
||||
}
|
||||
|
||||
private static string StripConditions(string text)
|
||||
{
|
||||
var stripped = ConditionRegex().Replace(text, string.Empty).Trim();
|
||||
@@ -614,6 +655,4 @@ internal static partial class FuyutsuiKeymapConverter
|
||||
[GeneratedRegex(@"\[[^\]]*@(?<unit>cursor|target|focus|player|mouseover)\b[^\]]*\]", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex StaticTargetRegex();
|
||||
|
||||
[GeneratedRegex(@"\[\s*@?(?<unit>player|target|focus|cursor|mouseover|channeling|nochanneling|玩家|目标|焦点|地面|鼠标|引导中|非引导|无目标|0|31|32|33|34|35|36|37)\s*(?:,|\])", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex SpecialUnitRegex();
|
||||
}
|
||||
|
||||
@@ -79,6 +79,20 @@ public sealed class KeymapCatalog
|
||||
return units.ToList();
|
||||
}
|
||||
|
||||
/// <summary>指定技能和 unit 在 keymap 中配置过的宏条件,按文件内首次出现顺序返回。</summary>
|
||||
public IReadOnlyList<string> 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<string> GetFailedSpellNames(int? classId)
|
||||
{
|
||||
return _config?.GetFailedSpells(classId).Values.ToList() ?? [];
|
||||
@@ -135,6 +149,7 @@ public sealed class KeymapCatalog
|
||||
var spells = new List<string>();
|
||||
var units = new List<int>();
|
||||
var unitsBySpell = new Dictionary<string, List<int>>(StringComparer.Ordinal);
|
||||
var macroConditions = new Dictionary<(string Spell, int Unit), List<string>>();
|
||||
try
|
||||
{
|
||||
var root = JsonNode.Parse(File.ReadAllText(path), documentOptions: new JsonDocumentOptions
|
||||
@@ -188,7 +203,12 @@ public sealed class KeymapCatalog
|
||||
continue;
|
||||
}
|
||||
|
||||
var unit = JsonHelpers.GetInt(JsonHelpers.Get(entry, "unit")) ?? 0;
|
||||
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);
|
||||
@@ -209,6 +229,18 @@ public sealed class KeymapCatalog
|
||||
{
|
||||
spellUnits.Add(unit);
|
||||
}
|
||||
|
||||
var conditionKey = (spell, unit);
|
||||
if (!macroConditions.TryGetValue(conditionKey, out var conditions))
|
||||
{
|
||||
conditions = new List<string>();
|
||||
macroConditions[conditionKey] = conditions;
|
||||
}
|
||||
|
||||
if (!conditions.Contains(macroCondition, StringComparer.Ordinal))
|
||||
{
|
||||
conditions.Add(macroCondition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,17 +253,20 @@ public sealed class KeymapCatalog
|
||||
return new KeymapEntries(
|
||||
spells,
|
||||
units,
|
||||
unitsBySpell.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<int>)kvp.Value, StringComparer.Ordinal));
|
||||
unitsBySpell.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<int>)kvp.Value, StringComparer.Ordinal),
|
||||
macroConditions.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<string>)kvp.Value));
|
||||
}
|
||||
|
||||
private sealed record KeymapEntries(
|
||||
IReadOnlyList<string> Spells,
|
||||
IReadOnlyList<int> Units,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<int>> UnitsBySpell)
|
||||
IReadOnlyDictionary<string, IReadOnlyList<int>> UnitsBySpell,
|
||||
IReadOnlyDictionary<(string Spell, int Unit), IReadOnlyList<string>> MacroConditions)
|
||||
{
|
||||
public static readonly KeymapEntries Empty = new(
|
||||
[],
|
||||
[],
|
||||
new Dictionary<string, IReadOnlyList<int>>(StringComparer.Ordinal));
|
||||
new Dictionary<string, IReadOnlyList<int>>(StringComparer.Ordinal),
|
||||
new Dictionary<(string Spell, int Unit), IReadOnlyList<string>>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ public sealed class KeymapService : IKeymapResolver
|
||||
{
|
||||
private readonly string _baseDirectory;
|
||||
private readonly ConfigService _config;
|
||||
private readonly Dictionary<(int Unit, string Spell), string> _hotkeys = new();
|
||||
private readonly Dictionary<(int Unit, string Spell, string MacroCondition), string> _hotkeys = new();
|
||||
private readonly Dictionary<(int Unit, string Spell), string> _fallbackHotkeys = new();
|
||||
private int? _currentClassId;
|
||||
private int? _currentSpecId;
|
||||
|
||||
@@ -32,6 +33,7 @@ public sealed class KeymapService : IKeymapResolver
|
||||
_currentClassId = classId;
|
||||
_currentSpecId = specId;
|
||||
_hotkeys.Clear();
|
||||
_fallbackHotkeys.Clear();
|
||||
|
||||
var path = KeymapCatalog.ResolveKeymapFilePath(_baseDirectory, _config.GetKeymapName(classId));
|
||||
if (!File.Exists(path))
|
||||
@@ -65,23 +67,44 @@ public sealed class KeymapService : IKeymapResolver
|
||||
continue;
|
||||
}
|
||||
|
||||
var unit = JsonHelpers.GetInt(JsonHelpers.Get(entry, "unit")) ?? 0;
|
||||
var rawUnit = JsonHelpers.GetInt(JsonHelpers.Get(entry, "unit")) ?? 0;
|
||||
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, "热键"));
|
||||
var normalizedMacro = MacroConditionText.NormalizeLegacyUnit(
|
||||
rawUnit,
|
||||
JsonHelpers.GetString(JsonHelpers.Get(entry, "宏条件")));
|
||||
var unit = normalizedMacro.Unit;
|
||||
var macroCondition = normalizedMacro.Condition;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(spell) && !string.IsNullOrWhiteSpace(hotkey))
|
||||
{
|
||||
_hotkeys[(unit, spell)] = hotkey;
|
||||
_hotkeys[(unit, spell, macroCondition)] = hotkey;
|
||||
// 兼容未保存“宏条件”的旧模块:保留旧版按单位+技能查询时的最后一项行为。
|
||||
_fallbackHotkeys[(unit, spell)] = hotkey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string? GetHotkey(int? unit, string spell)
|
||||
public string? GetHotkey(int? unit, string spell, string? macroCondition = null)
|
||||
{
|
||||
var normalizedUnit = unit.GetValueOrDefault();
|
||||
return _hotkeys.TryGetValue((normalizedUnit, spell), out var hotkey) ? hotkey : null;
|
||||
// null 表示旧模块根本没有该字段,严格沿用升级前“单位+技能”的最后一项匹配。
|
||||
if (macroCondition is null)
|
||||
{
|
||||
return _fallbackHotkeys.TryGetValue((normalizedUnit, spell), out var legacyHotkey)
|
||||
? legacyHotkey
|
||||
: null;
|
||||
}
|
||||
|
||||
var normalizedCondition = MacroConditionText.Normalize(macroCondition);
|
||||
if (_hotkeys.TryGetValue((normalizedUnit, spell, normalizedCondition), out var exactHotkey))
|
||||
{
|
||||
return exactHotkey;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<int, string> GetCurrentFailedSpells()
|
||||
|
||||
@@ -4,7 +4,7 @@ public interface IKeymapResolver
|
||||
{
|
||||
void SelectForClass(int? classId, int? specId);
|
||||
|
||||
string? GetHotkey(int? unit, string spell);
|
||||
string? GetHotkey(int? unit, string spell, string? macroCondition = null);
|
||||
|
||||
IReadOnlyDictionary<int, string> GetCurrentFailedSpells();
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Shigure;
|
||||
|
||||
public sealed class ModuleDefinition
|
||||
{
|
||||
internal const int CurrentUnitMappingVersion = 2;
|
||||
internal const int CurrentUnitMappingVersion = 3;
|
||||
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = "新模块";
|
||||
@@ -16,7 +16,7 @@ public sealed class ModuleDefinition
|
||||
public string RecommendedTalent { get; set; } = string.Empty;
|
||||
// 保存时写入当时的 Shigure 版本(AppInfo.Version)。
|
||||
public string Version { get; set; } = string.Empty;
|
||||
// v2: 31=玩家、32=目标、33=焦点、34=地面、35=鼠标、36=引导中、37=非引导;旧模块 v1 的 31/34 含义相反。
|
||||
// v2: 31=玩家、32=目标、33=焦点、34=地面、35=鼠标;v3: 36/37 从目标迁移为引导/非引导宏条件。
|
||||
public int? UnitMappingVersion { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
public ModuleMatch Match { get; set; } = new();
|
||||
@@ -197,6 +197,8 @@ public sealed class ModuleRule
|
||||
public int? Unit { get; set; }
|
||||
public string? UnitName { get; set; }
|
||||
public string Spell { get; set; } = string.Empty;
|
||||
// null 表示升级前的旧模块(运行时沿用原二元匹配);空字符串表示明确选择无宏条件。
|
||||
public string? MacroCondition { get; set; }
|
||||
public string Hotkey { get; set; } = string.Empty;
|
||||
public string Step { get; set; } = string.Empty;
|
||||
|
||||
@@ -216,6 +218,7 @@ public sealed class ModuleRule
|
||||
Unit = Unit,
|
||||
UnitName = UnitName,
|
||||
Spell = Spell,
|
||||
MacroCondition = MacroCondition,
|
||||
Hotkey = Hotkey,
|
||||
Step = Step,
|
||||
SubConditions = SubConditions is null ? null : new List<string>(SubConditions)
|
||||
@@ -572,7 +575,8 @@ public sealed class ModuleStore
|
||||
}
|
||||
|
||||
module.Rules ??= new List<ModuleRule>();
|
||||
if (module.UnitMappingVersion.GetValueOrDefault() < ModuleDefinition.CurrentUnitMappingVersion)
|
||||
var unitMappingVersion = module.UnitMappingVersion.GetValueOrDefault();
|
||||
if (unitMappingVersion < 2)
|
||||
{
|
||||
foreach (var rule in module.Rules)
|
||||
{
|
||||
@@ -584,12 +588,30 @@ public sealed class ModuleStore
|
||||
};
|
||||
}
|
||||
|
||||
module.UnitMappingVersion = ModuleDefinition.CurrentUnitMappingVersion;
|
||||
}
|
||||
|
||||
if (unitMappingVersion < 3)
|
||||
{
|
||||
foreach (var rule in module.Rules)
|
||||
{
|
||||
if (rule.Unit is 36 or 37)
|
||||
{
|
||||
var normalizedMacro = MacroConditionText.NormalizeLegacyUnit(rule.Unit.Value, rule.MacroCondition);
|
||||
rule.Unit = normalizedMacro.Unit;
|
||||
rule.MacroCondition = normalizedMacro.Condition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.UnitMappingVersion = ModuleDefinition.CurrentUnitMappingVersion;
|
||||
|
||||
foreach (var rule in module.Rules)
|
||||
{
|
||||
rule.Spell = ModuleSpecialActions.NormalizeSpellAction(rule.Spell);
|
||||
if (rule.MacroCondition is not null)
|
||||
{
|
||||
rule.MacroCondition = MacroConditionText.Normalize(rule.MacroCondition);
|
||||
}
|
||||
rule.DelayMs = rule.DelayMs is > 0 ? rule.DelayMs : null;
|
||||
rule.LogicDelayMs = rule.LogicDelayMs is > 0 ? rule.LogicDelayMs : null;
|
||||
if (rule.SubConditions is null)
|
||||
@@ -746,24 +768,29 @@ public static class ModuleLogic
|
||||
resolvedUnit = 0;
|
||||
}
|
||||
|
||||
var resolvedMacroCondition = rule.MacroCondition;
|
||||
var hotkey = string.IsNullOrWhiteSpace(rule.Hotkey)
|
||||
? string.IsNullOrWhiteSpace(actionSpell) ? null : keymap.GetHotkey(resolvedUnit, actionSpell)
|
||||
? string.IsNullOrWhiteSpace(actionSpell) ? null : keymap.GetHotkey(resolvedUnit, actionSpell, resolvedMacroCondition)
|
||||
: rule.Hotkey.Trim();
|
||||
if (isOneKeySpell
|
||||
&& string.IsNullOrWhiteSpace(rule.Hotkey)
|
||||
&& string.IsNullOrWhiteSpace(hotkey)
|
||||
&& !string.IsNullOrWhiteSpace(actionSpell))
|
||||
{
|
||||
hotkey = keymap.GetHotkey(ReservedUnit.NoChanneling, actionSpell);
|
||||
hotkey = keymap.GetHotkey(ReservedUnit.None, actionSpell, MacroConditionText.NoChanneling);
|
||||
if (!string.IsNullOrWhiteSpace(hotkey))
|
||||
{
|
||||
resolvedUnit = ReservedUnit.NoChanneling;
|
||||
resolvedUnit = ReservedUnit.None;
|
||||
resolvedMacroCondition = MacroConditionText.NoChanneling;
|
||||
}
|
||||
}
|
||||
|
||||
var step = BuildStep(module, rule, hotkey, actionSpell);
|
||||
info["命中条件"] = string.IsNullOrWhiteSpace(rule.Condition) ? "始终" : rule.Condition;
|
||||
info["动作技能"] = string.IsNullOrWhiteSpace(actionSpell) ? "-" : actionSpell;
|
||||
info["宏条件"] = string.IsNullOrWhiteSpace(resolvedMacroCondition)
|
||||
? "-"
|
||||
: MacroConditionText.ToDisplayText(resolvedMacroCondition);
|
||||
info["动作按键"] = string.IsNullOrWhiteSpace(hotkey) ? "-" : hotkey;
|
||||
info["动作单位"] = string.IsNullOrWhiteSpace(rule.UnitName)
|
||||
? resolvedUnit.GetValueOrDefault()
|
||||
|
||||
@@ -13,8 +13,6 @@ internal static class ReservedUnit
|
||||
public const int Focus = 33;
|
||||
public const int Cursor = 34;
|
||||
public const int Mouseover = 35;
|
||||
public const int Channeling = 36;
|
||||
public const int NoChanneling = 37;
|
||||
|
||||
public static string ToDisplayText(int unit)
|
||||
{
|
||||
@@ -26,8 +24,6 @@ internal static class ReservedUnit
|
||||
Focus => "焦点",
|
||||
Cursor => "地面",
|
||||
Mouseover => "鼠标",
|
||||
Channeling => "引导中",
|
||||
NoChanneling => "非引导",
|
||||
_ => unit.ToString(CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
@@ -43,11 +39,60 @@ internal static class ReservedUnit
|
||||
"焦点" => Focus,
|
||||
"地面" => Cursor,
|
||||
"鼠标" => Mouseover,
|
||||
"引导中" => Channeling,
|
||||
"非引导" => NoChanneling,
|
||||
_ => int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var unit)
|
||||
? unit
|
||||
: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>宏条件在 keymap/模块内保存原始标识,在界面使用中文名称。</summary>
|
||||
internal static class MacroConditionText
|
||||
{
|
||||
private const int LegacyChannelingUnit = 36;
|
||||
private const int LegacyNoChannelingUnit = 37;
|
||||
public const string Channeling = "channeling";
|
||||
public const string NoChanneling = "nochanneling";
|
||||
|
||||
/// <summary>兼容旧版误把引导条件写入 unit=36/37 的 keymap 与模块。</summary>
|
||||
public static (int Unit, string Condition) NormalizeLegacyUnit(int unit, string? condition)
|
||||
{
|
||||
var normalizedCondition = Normalize(condition);
|
||||
return unit switch
|
||||
{
|
||||
LegacyChannelingUnit => (ReservedUnit.None,
|
||||
normalizedCondition.Length == 0 ? Channeling : normalizedCondition),
|
||||
LegacyNoChannelingUnit => (ReservedUnit.None,
|
||||
normalizedCondition.Length == 0 ? NoChanneling : normalizedCondition),
|
||||
_ => (unit, normalizedCondition)
|
||||
};
|
||||
}
|
||||
|
||||
public static string Normalize(string? text)
|
||||
{
|
||||
return Transform(text, toDisplay: false);
|
||||
}
|
||||
|
||||
public static string ToDisplayText(string? text)
|
||||
{
|
||||
return Transform(text, toDisplay: true);
|
||||
}
|
||||
|
||||
public static string ParseDisplayText(string? text)
|
||||
{
|
||||
return Normalize(text);
|
||||
}
|
||||
|
||||
private static string Transform(string? text, bool toDisplay)
|
||||
{
|
||||
var parts = (text ?? string.Empty)
|
||||
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(part => part.ToLowerInvariant() switch
|
||||
{
|
||||
"channeling" or "引导中" => toDisplay ? "引导中" : Channeling,
|
||||
"nochanneling" or "非引导" => toDisplay ? "非引导" : NoChanneling,
|
||||
_ => part
|
||||
});
|
||||
return string.Join(", ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,8 +235,8 @@ public sealed class ClassMacrosEditorControl : UserControl
|
||||
var pages = new Control[]
|
||||
{
|
||||
BuildDynamicPage(),
|
||||
BuildArrayPage(_staticGrid, "法术/条件或完整宏(空字符串 = 占位)", "注释", showParsedMacro: true),
|
||||
BuildArrayPage(_specialGrid, "追加宏内容(空字符串 = 占位)", "注释", showParsedMacro: true)
|
||||
BuildArrayPage(_staticGrid, "完整宏", "注释", showParsedMacro: true),
|
||||
BuildArrayPage(_specialGrid, "完整宏", "注释", showParsedMacro: true)
|
||||
};
|
||||
foreach (var page in pages)
|
||||
{
|
||||
@@ -439,6 +439,13 @@ public sealed class ClassMacrosEditorControl : UserControl
|
||||
ReadOnly = true
|
||||
});
|
||||
grid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
Name = "Condition",
|
||||
HeaderText = "条件",
|
||||
Width = 180,
|
||||
ReadOnly = true
|
||||
});
|
||||
grid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
Name = "Spell",
|
||||
HeaderText = "技能",
|
||||
@@ -1074,6 +1081,7 @@ public sealed class ClassMacrosEditorControl : UserControl
|
||||
{
|
||||
if (!grid.Columns.Contains("Unit")
|
||||
|| !grid.Columns.Contains("Spell")
|
||||
|| !grid.Columns.Contains("Condition")
|
||||
|| row.IsNewRow)
|
||||
{
|
||||
return;
|
||||
@@ -1092,6 +1100,7 @@ public sealed class ClassMacrosEditorControl : UserControl
|
||||
? ReservedUnit.ToDisplayText(parsed.Unit)
|
||||
: parsed.Unit.ToString(CultureInfo.InvariantCulture);
|
||||
row.Cells["Spell"].Value = parsed.Spell;
|
||||
row.Cells["Condition"].Value = MacroConditionText.ToDisplayText(parsed.Condition);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -26,6 +26,7 @@ public sealed class ModuleEditorControl : UserControl
|
||||
private readonly DataGridView _formulaAdjustmentsGrid = new();
|
||||
private readonly DataGridViewComboBoxColumn _spellColumn = new();
|
||||
private readonly DataGridViewComboBoxColumn _unitColumn = new();
|
||||
private readonly DataGridViewComboBoxColumn _macroConditionColumn = new();
|
||||
private readonly DataGridViewComboBoxColumn _adjustmentFieldColumn = new();
|
||||
private readonly DataGridViewComboBoxColumn _adjustmentTypeColumn = new();
|
||||
private readonly ListView _unitsList = new();
|
||||
@@ -66,8 +67,8 @@ public sealed class ModuleEditorControl : UserControl
|
||||
new("队伍 (46)", "46")
|
||||
];
|
||||
private static readonly MatchOption[] ClassOptions = BuildClassOptions();
|
||||
// 这三列固定宽度并缓存; "条件"列为 Fill, 图标列固定且不缓存。
|
||||
private static readonly string[] FixedWidthColumns = ["Enabled", "Spell", "Unit"];
|
||||
// 这些列固定宽度并缓存; "条件"列为 Fill, 图标列固定且不缓存。
|
||||
private static readonly string[] FixedWidthColumns = ["Enabled", "Spell", "Unit", "MacroCondition"];
|
||||
private static readonly HashSet<string> NonAuraGroupFields = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"生命值",
|
||||
@@ -99,6 +100,11 @@ public sealed class ModuleEditorControl : UserControl
|
||||
{
|
||||
_fieldCatalog = ConditionFieldCatalog.Load(_baseDirectory);
|
||||
_keymapCatalog = KeymapCatalog.Load(_baseDirectory);
|
||||
// “更新配置”可能刚重建了 keymap;立即刷新当前规则的技能/目标/宏条件下拉,
|
||||
// 避免必须切换职业或重启应用后才能看到新解析出的宏条件。
|
||||
RefreshKeymapColumns();
|
||||
RefreshAdjustmentFieldColumn();
|
||||
_rulesGrid.Invalidate();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
@@ -795,7 +801,7 @@ public sealed class ModuleEditorControl : UserControl
|
||||
_rulesGrid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None;
|
||||
_rulesGrid.ShowCellToolTips = false;
|
||||
|
||||
// 启用/技能/目标 三列宽度固定可调并缓存; 条件列用 Fill 自动充满剩余窗口。
|
||||
// 启用/技能/目标/宏条件列宽度固定可调并缓存; 条件列用 Fill 自动充满剩余窗口。
|
||||
_rulesGrid.Columns.Add(new DataGridViewCheckBoxColumn
|
||||
{
|
||||
Name = "Enabled",
|
||||
@@ -815,6 +821,12 @@ public sealed class ModuleEditorControl : UserControl
|
||||
_unitColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.None;
|
||||
_unitColumn.FlatStyle = FlatStyle.Flat;
|
||||
_rulesGrid.Columns.Add(_unitColumn);
|
||||
_macroConditionColumn.Name = "MacroCondition";
|
||||
_macroConditionColumn.HeaderText = "宏条件";
|
||||
_macroConditionColumn.Width = 150;
|
||||
_macroConditionColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.None;
|
||||
_macroConditionColumn.FlatStyle = FlatStyle.Flat;
|
||||
_rulesGrid.Columns.Add(_macroConditionColumn);
|
||||
_rulesGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
Name = "Condition",
|
||||
@@ -830,7 +842,7 @@ public sealed class ModuleEditorControl : UserControl
|
||||
AddRuleIconColumn("InsertBlank", "+", "在下一行添加空白条件");
|
||||
AddRuleIconColumn("Delete", "×", "删除", UiTheme.Danger);
|
||||
|
||||
// 拖拽手柄列: 加在集合末尾(保持 Rows.Add 的位置参数仍对应 启用/技能/目标/条件),
|
||||
// 拖拽手柄列: 加在集合末尾(保持 Rows.Add 的位置参数仍对应 启用/技能/目标/宏条件/条件),
|
||||
// 用 DisplayIndex=0 显示到"启用"前面。自绘六点抓手, 按住拖动可调整该条逻辑顺序。
|
||||
_rulesGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
@@ -930,7 +942,7 @@ public sealed class ModuleEditorControl : UserControl
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按当前选中职业的 keymap 重建“技能/目标”下拉选项。
|
||||
/// 按当前选中职业的 keymap 重建“技能/目标/宏条件”下拉选项。
|
||||
/// 技能去重(同名技能只出现一次), unit 去重升序; 首项留空表示不填。
|
||||
/// 已有行里不在 keymap 中的旧值会补录为额外选项, 避免数据丢失。
|
||||
/// </summary>
|
||||
@@ -959,6 +971,9 @@ public sealed class ModuleEditorControl : UserControl
|
||||
_unitColumn.Items.Add(ReservedUnit.ToDisplayText(unit));
|
||||
}
|
||||
|
||||
_macroConditionColumn.Items.Clear();
|
||||
_macroConditionColumn.Items.Add(string.Empty);
|
||||
|
||||
foreach (DataGridViewRow row in _rulesGrid.Rows)
|
||||
{
|
||||
if (row.IsNewRow)
|
||||
@@ -968,6 +983,7 @@ public sealed class ModuleEditorControl : UserControl
|
||||
|
||||
EnsureComboItem(_spellColumn, row.Cells["Spell"].Value);
|
||||
UpdateUnitCellItems(row);
|
||||
UpdateMacroConditionCellItems(row);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1055,6 +1071,67 @@ public sealed class ModuleEditorControl : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMacroConditionCellItems(DataGridViewRow row)
|
||||
{
|
||||
if (row.IsNewRow || row.Cells["MacroCondition"] is not DataGridViewComboBoxCell cell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RebuildMacroConditionCell(row, cell.Value?.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按当前技能与目标重建“宏条件”选项。只有一个非空条件时自动选中;
|
||||
/// 自定义技能或动态单位没有 keymap 条目时保留已有值。
|
||||
/// </summary>
|
||||
private void RebuildMacroConditionCell(DataGridViewRow row, string? desiredValue)
|
||||
{
|
||||
if (row.IsNewRow || row.Cells["MacroCondition"] is not DataGridViewComboBoxCell cell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var desired = MacroConditionText.ToDisplayText(desiredValue);
|
||||
var spell = row.Cells["Spell"].Value?.ToString();
|
||||
var unitText = row.Cells["Unit"].Value?.ToString();
|
||||
var unit = ReservedUnit.ParseDisplayText(unitText);
|
||||
var allowed = unit is null || string.IsNullOrWhiteSpace(spell)
|
||||
? (IReadOnlyList<string>)[]
|
||||
: _keymapCatalog.GetMacroConditions(ReadMatchCombo(_classBox), spell, unit);
|
||||
|
||||
cell.Items.Clear();
|
||||
cell.Items.Add(string.Empty);
|
||||
foreach (var condition in allowed)
|
||||
{
|
||||
var displayCondition = MacroConditionText.ToDisplayText(condition);
|
||||
if (!cell.Items.Contains(displayCondition))
|
||||
{
|
||||
cell.Items.Add(displayCondition);
|
||||
}
|
||||
}
|
||||
|
||||
if (desired.Length > 0 && cell.Items.Contains(desired))
|
||||
{
|
||||
cell.Value = desired;
|
||||
}
|
||||
else if (desired.Length > 0 && allowed.Count == 0)
|
||||
{
|
||||
cell.Items.Add(desired);
|
||||
cell.Value = desired;
|
||||
}
|
||||
else
|
||||
{
|
||||
var nonEmptyConditions = allowed
|
||||
.Select(MacroConditionText.ToDisplayText)
|
||||
.Where(condition => !string.IsNullOrWhiteSpace(condition))
|
||||
.ToList();
|
||||
cell.Value = nonEmptyConditions.Count == 1 && allowed.Count == 1
|
||||
? nonEmptyConditions[0]
|
||||
: string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRulesGridCellValueChanged(object? sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
if (e.RowIndex < 0 || e.ColumnIndex < 0)
|
||||
@@ -1062,10 +1139,16 @@ public sealed class ModuleEditorControl : UserControl
|
||||
return;
|
||||
}
|
||||
|
||||
// 技能改变时联动刷新该行"目标"的可选值。
|
||||
if (_rulesGrid.Columns[e.ColumnIndex].Name == "Spell")
|
||||
var columnName = _rulesGrid.Columns[e.ColumnIndex].Name;
|
||||
// 技能改变时联动刷新该行"目标",技能或目标改变时再刷新"宏条件"。
|
||||
if (columnName == "Spell")
|
||||
{
|
||||
UpdateUnitCellItems(_rulesGrid.Rows[e.RowIndex]);
|
||||
UpdateMacroConditionCellItems(_rulesGrid.Rows[e.RowIndex]);
|
||||
}
|
||||
else if (columnName == "Unit")
|
||||
{
|
||||
UpdateMacroConditionCellItems(_rulesGrid.Rows[e.RowIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1557,6 +1640,7 @@ public sealed class ModuleEditorControl : UserControl
|
||||
bool Enabled,
|
||||
string Spell,
|
||||
string UnitText,
|
||||
string MacroCondition,
|
||||
string Condition,
|
||||
IReadOnlyList<string> SubConditions,
|
||||
int? DelayMs,
|
||||
@@ -1796,7 +1880,7 @@ public sealed class ModuleEditorControl : UserControl
|
||||
_rulesGridToolTip.Hide(_rulesGrid);
|
||||
}
|
||||
|
||||
// 图标列沿用原提示; 条件/技能/目标三列在文本被列宽截断或可点击时给出悬停提示。
|
||||
// 图标列沿用原提示; 条件/技能/目标/宏条件列在文本被列宽截断或可点击时给出悬停提示。
|
||||
private string GetRuleCellToolTip(string columnName, int rowIndex, int columnIndex)
|
||||
{
|
||||
if (columnName is "MoveUp" or "MoveDown" or "Copy" or "InsertBlank" or "Delete")
|
||||
@@ -1814,7 +1898,7 @@ public sealed class ModuleEditorControl : UserControl
|
||||
return "拖动调整顺序";
|
||||
}
|
||||
|
||||
if (columnName is not ("Condition" or "Spell" or "Unit"))
|
||||
if (columnName is not ("Condition" or "Spell" or "Unit" or "MacroCondition"))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
@@ -2195,6 +2279,7 @@ public sealed class ModuleEditorControl : UserControl
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
Array.Empty<string>(),
|
||||
null,
|
||||
null));
|
||||
@@ -2406,6 +2491,7 @@ public sealed class ModuleEditorControl : UserControl
|
||||
CellBool(row, "Enabled", defaultValue: true),
|
||||
CellText(row, "Spell"),
|
||||
CellText(row, "Unit"),
|
||||
CellText(row, "MacroCondition"),
|
||||
CellText(row, "Condition"),
|
||||
// 子条件和延迟挂在 row.Tag, 随行一起被移动/拖拽/复制搬运。
|
||||
GetRuleMetadata(row).SubConditions,
|
||||
@@ -2418,9 +2504,11 @@ public sealed class ModuleEditorControl : UserControl
|
||||
row.Cells["Enabled"].Value = values.Enabled;
|
||||
EnsureComboItem(_spellColumn, values.Spell);
|
||||
row.Cells["Spell"].Value = values.Spell;
|
||||
row.Cells["MacroCondition"].Value = string.Empty;
|
||||
row.Cells["Condition"].Value = values.Condition;
|
||||
row.Tag = new RuleRowMetadata(values.SubConditions, values.DelayMs, values.LogicDelayMs);
|
||||
RebuildUnitCell(row, values.UnitText);
|
||||
RebuildMacroConditionCell(row, values.MacroCondition);
|
||||
}
|
||||
|
||||
private void OpenConditionEditor(int rowIndex)
|
||||
@@ -2452,7 +2540,7 @@ public sealed class ModuleEditorControl : UserControl
|
||||
|| editor.DelayMs is > 0
|
||||
|| editor.LogicDelayMs is > 0)
|
||||
{
|
||||
var index = _rulesGrid.Rows.Add(true, string.Empty, string.Empty, editor.ConditionText);
|
||||
var index = _rulesGrid.Rows.Add(true, string.Empty, string.Empty, string.Empty, editor.ConditionText);
|
||||
_rulesGrid.Rows[index].Tag = new RuleRowMetadata(
|
||||
subs,
|
||||
editor.DelayMs,
|
||||
@@ -2537,7 +2625,7 @@ public sealed class ModuleEditorControl : UserControl
|
||||
|
||||
var hint = new Label
|
||||
{
|
||||
Text = "目标可选技能支持的单位或上方定义的动态单位;点击“条件”列打开可视化编辑器",
|
||||
Text = "目标可选技能支持的单位或上方定义的动态单位;宏条件来自对应宏的方括号;点击“条件”列打开可视化编辑器",
|
||||
Dock = DockStyle.Fill,
|
||||
ForeColor = UiTheme.Muted,
|
||||
TextAlign = ContentAlignment.MiddleLeft,
|
||||
@@ -2664,12 +2752,13 @@ public sealed class ModuleEditorControl : UserControl
|
||||
: rule.Unit is { } unit ? ReservedUnit.ToDisplayText(unit) : string.Empty;
|
||||
EnsureComboItem(_spellColumn, rule.Spell);
|
||||
// 先加行(目标先留空), 再按技能重建目标选项并写回目标值, 避免值不在选项内被吞掉。
|
||||
var index = _rulesGrid.Rows.Add(rule.Enabled, rule.Spell, string.Empty, rule.Condition);
|
||||
var index = _rulesGrid.Rows.Add(rule.Enabled, rule.Spell, string.Empty, string.Empty, rule.Condition);
|
||||
_rulesGrid.Rows[index].Tag = new RuleRowMetadata(
|
||||
rule.SubConditions,
|
||||
rule.DelayMs,
|
||||
rule.LogicDelayMs);
|
||||
RebuildUnitCell(_rulesGrid.Rows[index], unitText);
|
||||
RebuildMacroConditionCell(_rulesGrid.Rows[index], rule.MacroCondition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2946,10 +3035,12 @@ public sealed class ModuleEditorControl : UserControl
|
||||
var condition = CellText(row, "Condition");
|
||||
var spell = CellText(row, "Spell");
|
||||
var unitText = CellText(row, "Unit");
|
||||
var macroCondition = CellText(row, "MacroCondition");
|
||||
var metadata = GetRuleMetadata(row);
|
||||
if (string.IsNullOrWhiteSpace(condition)
|
||||
&& string.IsNullOrWhiteSpace(spell)
|
||||
&& string.IsNullOrWhiteSpace(unitText)
|
||||
&& string.IsNullOrWhiteSpace(macroCondition)
|
||||
&& metadata.SubConditions.Count == 0
|
||||
&& metadata.DelayMs is not > 0
|
||||
&& metadata.LogicDelayMs is not > 0)
|
||||
@@ -2970,6 +3061,7 @@ public sealed class ModuleEditorControl : UserControl
|
||||
Unit = isDynamic ? null : ReservedUnit.ParseDisplayText(unitText),
|
||||
UnitName = isDynamic ? unitText : null,
|
||||
Spell = spell,
|
||||
MacroCondition = MacroConditionText.ParseDisplayText(macroCondition),
|
||||
Hotkey = string.Empty,
|
||||
Step = string.Empty,
|
||||
SubConditions = subs is { Count: > 0 } ? subs : null,
|
||||
|
||||
@@ -961,6 +961,7 @@
|
||||
},
|
||||
"193": {
|
||||
"unit": 0,
|
||||
"宏条件": "nostance:1",
|
||||
"技能": "暗影形态",
|
||||
"热键": "ALT-SHIFT-9"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user