feat: 添加宏条件支持到 keymap,更新 priest.json 中的技能条目

This commit is contained in:
waynebian01
2026-08-06 14:54:51 +08:00
parent afc970b2d4
commit a5f84f3fe2
9 changed files with 330 additions and 59 deletions

View File

@@ -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();
}

View File

@@ -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>>());
}
}

View File

@@ -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()

View File

@@ -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();

View File

@@ -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()

View File

@@ -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);
}
}

View File

@@ -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
{

View File

@@ -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,

View File

@@ -961,6 +961,7 @@
},
"193": {
"unit": 0,
"宏条件": "nostance:1",
"技能": "暗影形态",
"热键": "ALT-SHIFT-9"
},