From dcd5542df07cea8d16bbfe2289437008f77b9810 Mon Sep 17 00:00:00 2001 From: waynebian01 Date: Tue, 1 Sep 2026 13:24:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=89=B9=E6=AE=8A=E5=AE=8F?= =?UTF-8?q?=E7=9A=84=E6=B3=A8=E9=87=8A=E7=AE=A1=E7=90=86=EF=BC=8C=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E6=8A=80=E8=83=BD=E5=90=8D=E7=94=B1=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=89=8B=E5=8A=A8=E5=A1=AB=E5=86=99=E5=B9=B6=E5=9B=BA=E5=AE=9A?= =?UTF-8?q?=E4=B8=BA=E6=97=A0=E7=9B=AE=E6=A0=87=E3=80=81=E6=97=A0=E5=AE=8F?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=EF=BC=9B=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=92=8C=E7=95=8C=E9=9D=A2=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Fuyutsui/core/classmacros.lua | 18 +++---- Infrastructure/ClassMacrosStore.cs | 4 ++ Infrastructure/FuyutsuiKeymapConverter.cs | 20 +++----- Infrastructure/ModuleDependencyService.cs | 4 +- Modules/ModuleDependencies.cs | 2 + .../09-Shigure-Fuyutsui配置宏编辑与同步.md | 3 +- .../10-Shigure-UI功能地图与数据所有权.md | 1 + .../03-Shigure-ClassMacros到keymap与按键契约.md | 4 +- .../CLASSMACROS_AI_Reference_zh-CN.md | 5 +- README.md | 2 +- UI/ClassMacrosEditorControl.cs | 49 ++++++++++++++----- 11 files changed, 68 insertions(+), 44 deletions(-) diff --git a/Fuyutsui/core/classmacros.lua b/Fuyutsui/core/classmacros.lua index 386d4e89..569bb799 100644 --- a/Fuyutsui/core/classmacros.lua +++ b/Fuyutsui/core/classmacros.lua @@ -126,7 +126,7 @@ Fuyutsui.ClassMacros = { }, specialSpells = { - "/stopcasting", + "/stopcasting", -- 停止施法 }, }, @@ -235,8 +235,8 @@ Fuyutsui.ClassMacros = { "[@target]驱散魔法", }, specialSpells = { - "/castsequence reset=0.4 真言术:耀,x", - "/stopcasting", + "/castsequence reset=0.4 真言术:耀,x", -- 真言术:耀 + "/stopcasting", -- 停止施法 }, }, @@ -292,7 +292,7 @@ Fuyutsui.ClassMacros = { "[@cursor]枯萎凋零", }, specialSpells = { - "/castsequence reset=1 死亡之握,x", + "/castsequence reset=1 死亡之握,x", -- 死亡之握 }, }, @@ -346,7 +346,7 @@ Fuyutsui.ClassMacros = { "[@player]治疗之雨", }, specialSpells = { - "/stopcasting", + "/stopcasting", -- 停止施法 }, }, @@ -579,8 +579,8 @@ Fuyutsui.ClassMacros = { "[nostance:1]熊形态", }, specialSpells = { - "/cancelaura [spec:4]猎豹形态\n/cast 万灵之召", - "/castsequence reset=0.5 铁鬃,x", + "/cancelaura [spec:4]猎豹形态\n/cast 万灵之召", -- 万灵之召 + "/castsequence reset=0.5 铁鬃,x", -- 铁鬃 }, }, @@ -634,8 +634,8 @@ Fuyutsui.ClassMacros = { "破裂", }, specialSpells = { - "/castsequence reset=0.5 恶魔变形,x", - "/castsequence reset=0.5 烈火烙印,x", + "/castsequence reset=0.5 恶魔变形,x", -- 恶魔变形 + "/castsequence reset=0.5 烈火烙印,x", -- 烈火烙印 }, }, diff --git a/Infrastructure/ClassMacrosStore.cs b/Infrastructure/ClassMacrosStore.cs index 09c2db8a..da5b3d53 100644 --- a/Infrastructure/ClassMacrosStore.cs +++ b/Infrastructure/ClassMacrosStore.cs @@ -54,6 +54,10 @@ internal static class ClassMacrosStore public sealed class ArrayEntry { public string Text { get; set; } = string.Empty; + + /// + /// Lua 同行注释。静态宏沿用注释/技能名覆盖语义;特殊宏专门用它保存手工技能名。 + /// public string? Comment { get; set; } } diff --git a/Infrastructure/FuyutsuiKeymapConverter.cs b/Infrastructure/FuyutsuiKeymapConverter.cs index e5c61a9a..04f2f8d1 100644 --- a/Infrastructure/FuyutsuiKeymapConverter.cs +++ b/Infrastructure/FuyutsuiKeymapConverter.cs @@ -238,7 +238,8 @@ internal static partial class FuyutsuiKeymapConverter } } - if (entry is { Body.Length: > 0 } + if (isStaticEntry + && entry is { Body.Length: > 0 } && IsWeakSpellName(spell) && TryGetExistingSpellName(existingSpellNames, specId, i, out var preserved) && !string.IsNullOrWhiteSpace(preserved) @@ -328,20 +329,11 @@ internal static partial class FuyutsuiKeymapConverter } /// - /// 解析特殊宏:方括号中以 @ 开头的首个单位作为 unit,技能沿用宏技能推导(castsequence 只取逗号前首项)。 + /// 特殊宏不解析宏正文。技能名必须由编辑器手工填写并保存在同行注释中, + /// keymap 固定使用无目标、无宏条件。 /// - internal static ParsedMacro ParseSpecialMacro(string raw, string? comment = null) - { - // 标准 WoW 条件允许单位出现在其它条件之后,例如 [known:123,@cursor]。 - var target = StaticTargetRegex().Match(raw); - var unit = target.Success - ? ResolveUnitName(target.Groups["unit"].Value) - : ReservedUnit.None; - - var spell = ResolveSpellName(new MacroEntry(raw, comment)); - spell = SplitTopLevel(spell, ',').FirstOrDefault()?.Trim() ?? string.Empty; - return new ParsedMacro(unit, spell, ResolveConditions(raw)); - } + internal static ParsedMacro ParseSpecialMacro(string _, string? comment = null) + => new(ReservedUnit.None, comment?.Trim() ?? string.Empty, string.Empty); /// 方括号中以 @ 开头的是目标,其余逗号分隔项作为只读条件摘要。 private static string ResolveConditions(string raw) diff --git a/Infrastructure/ModuleDependencyService.cs b/Infrastructure/ModuleDependencyService.cs index 0eae1e4f..5e0b5d2a 100644 --- a/Infrastructure/ModuleDependencyService.cs +++ b/Infrastructure/ModuleDependencyService.cs @@ -1094,8 +1094,8 @@ internal sealed class ModuleDependencyService public bool HasConfigChanges => ConfigAdded > 0 || ConfigUpdated > 0; } - // 静态宏和特殊宏可以解析成相同的目标/技能/条件,但仍是两个独立槽位。 - // 例如“恶魔变形”和“/castsequence reset=0.5 恶魔变形,x”必须分别去重。 + // 静态宏按解析出的目标/技能/条件去重;特殊宏只按手工技能名去重, + // 但两类宏仍是两个独立槽位。 private readonly record struct MacroIdentity(bool IsSpecial, int Unit, string Spell, string Condition); } diff --git a/Modules/ModuleDependencies.cs b/Modules/ModuleDependencies.cs index 087dd19e..40e6cf4d 100644 --- a/Modules/ModuleDependencies.cs +++ b/Modules/ModuleDependencies.cs @@ -160,6 +160,8 @@ public sealed class ModuleMacrosSnapshot public sealed class ModuleMacroEntrySnapshot { public string Text { get; set; } = string.Empty; + + // 对特殊宏表示必须手工维护的技能名,并以 Lua 行尾注释持久化。 public string? Comment { get; set; } public ModuleMacroEntrySnapshot Clone() => (ModuleMacroEntrySnapshot)MemberwiseClone(); diff --git a/Obsidian/30-Shigure/09-Shigure-Fuyutsui配置宏编辑与同步.md b/Obsidian/30-Shigure/09-Shigure-Fuyutsui配置宏编辑与同步.md index a94dd864..53d43432 100644 --- a/Obsidian/30-Shigure/09-Shigure-Fuyutsui配置宏编辑与同步.md +++ b/Obsidian/30-Shigure/09-Shigure-Fuyutsui配置宏编辑与同步.md @@ -121,7 +121,8 @@ verified_at: 2026-08-10 - 动态宏项每项预留/消耗 30 个单位位置,然后是静态宏和特殊宏;超过容量会警告并截断。 - 旧 flat 格式或 common+spec 格式都可转换;专精输出是“common + 当前 spec + static + special”的完整映射,以适配运行时不合并顶层的语义。 - party1..4 映射组员槽 2..5;原始 `@player` 动态语义映射组员槽 1;显式中文/player 保留单位 31;raid1..30 映射 1..30。 -- 方括号中非 `@` 条件汇总为 macroCondition;尾注释可以覆盖从宏文本推导的 spell 名。 +- dynamic/static 仍按宏文本生成映射,方括号中非 `@` 条件汇总为 macroCondition,尾注释可以覆盖推导出的 spell 名。 +- special 不再分析宏正文:尾注释必须作为手工技能名,映射固定为 `unit=0`、空 macroCondition;宏编辑器因此只显示可编辑技能名和完整宏,不显示目标与条件。 - 能识别 stopcasting、castsequence、最后一个 cast 和 item 等受支持形式;不等于完整 WoW 宏解释器。 - 输出包含全部 273 项,包括空项,以维持索引位置。 diff --git a/Obsidian/30-Shigure/10-Shigure-UI功能地图与数据所有权.md b/Obsidian/30-Shigure/10-Shigure-UI功能地图与数据所有权.md index 0891d75e..201c2de7 100644 --- a/Obsidian/30-Shigure/10-Shigure-UI功能地图与数据所有权.md +++ b/Obsidian/30-Shigure/10-Shigure-UI功能地图与数据所有权.md @@ -98,6 +98,7 @@ verified_at: 2026-08-10 - ClassBlocks 编辑器恢复前三个固定状态名为锚点/职业/专精;`spellsList` 可编辑索引 1–100 的条目并原位回写 Lua。 - 旧稀疏 ClassBlocks 不是可靠的只读预览:旧专精可显示为空,Store 会拒绝非 modern 文档保存;混合格式需特别谨慎。 - ClassMacros 和 ClassBlocks 保存都会 canonical 重写目标表内部,未知字段/排版不保证保留。 +- ClassMacros 特殊宏的技能名由用户手工填写并保存为 Lua 行尾注释;界面不展示目标和条件,转换器也不从特殊宏正文推导这些字段。 - 条件编辑器只能表达当前简单 AND/OR 语言,不支持括号;动态名称禁止空、`.`、`$`、纯数字和冲突名称。 ## 模块选择的 UI 语义 diff --git a/Obsidian/40-跨项目/03-Shigure-ClassMacros到keymap与按键契约.md b/Obsidian/40-跨项目/03-Shigure-ClassMacros到keymap与按键契约.md index 3b3d8d2d..0378b7c4 100644 --- a/Obsidian/40-跨项目/03-Shigure-ClassMacros到keymap与按键契约.md +++ b/Obsidian/40-跨项目/03-Shigure-ClassMacros到keymap与按键契约.md @@ -80,7 +80,7 @@ dynamicSpells(每项展开 30 个团队槽) | `Fuyutsui.MacroBodies` | 可复用的命名宏体 | 被条目按名称解析,不单独占槽 | | `dynamicSpells` | 对队伍/团队单位展开的技能 | 每个条目连续占 30 个单位槽 | | `staticSpells` | 普通技能或可解析宏条目 | 每项占一个槽,空字符串保留位置 | -| `specialSpells` | 完整特殊宏文本 | 在 static 之后逐项占槽,保留顺序和注释语义 | +| `specialSpells` | 完整特殊宏文本;行尾注释是手工技能名 | 在 static 之后逐项占槽,保留顺序;Shigure 固定映射为无目标、无宏条件 | `LoadPlayerMacros` 先取得当前职业和专精,把 `dynamicSpells.common` 与当前 `[specIndex]` 连接,再交给 `CreateMacro`。 @@ -131,7 +131,7 @@ keymap 是生成物;宏 Lua 与两端一致的展开算法才是来源。 1. `ClassMacrosStore` 与 `LuaLiteParser` 读取同一 `ClassMacros` 表,并保留数组空槽和行尾注释。 2. `FuyutsuiKeymapConverter` 按 dynamic 每项 30 槽,再 static、special 的顺序遍历同一 key pool。 -3. 转换器从直接技能或宏文本中提取技能名、单位与可选宏条件,写入职业 keymap。 +3. 转换器从 dynamic/static 条目提取技能名、单位与可选宏条件;special 不解析正文,只读取行尾注释中的手工技能名,并固定写成无目标、无宏条件。 4. `KeymapService` 根据当前职业/专精选择数据,并以 unit、spell、condition 查找 hotkey。 5. module 可直接提供 hotkey,或提供技能/动态单位后通过 keymap 解析;特殊动作可能先从 `GameState` 解析实际技能。 6. `ShigureRuntime` 应用规则延迟和逻辑延迟后调用 `KeySender`;目标窗口收到的键应命中 Fuyutsui 覆盖绑定或游戏动作条。 diff --git a/Obsidian/50-参考资料/CLASSMACROS_AI_Reference_zh-CN.md b/Obsidian/50-参考资料/CLASSMACROS_AI_Reference_zh-CN.md index d36b8751..170ce96c 100644 --- a/Obsidian/50-参考资料/CLASSMACROS_AI_Reference_zh-CN.md +++ b/Obsidian/50-参考资料/CLASSMACROS_AI_Reference_zh-CN.md @@ -305,13 +305,14 @@ Fuyutsui.MacroBodies = { ```lua specialSpells = { - "/castsequence reset=0.5 死亡之握,x", - "/stopcasting", + "/castsequence reset=0.5 死亡之握,x", -- 死亡之握 + "/stopcasting", -- 停止施法 } ``` - 同样是**数组**,接在该职业**全部** `staticSpells` 之后依次占键。 - 也走 `resolveMacroBody`:可用完整 `/...` 文本,或 MacroBodies 名称,或普通法术名(会加 `/cast`)。 +- Fuyutsui 只使用字符串宏体;Shigure 把同行 `--` 注释作为必须手工维护的技能名,不分析特殊宏正文,并固定生成无目标、无宏条件的 keymap 条目。 - 当前仓库多数职业 `specialSpells = {}`;历史上与 static 同槽的特殊宏(sequence / stopcasting 等)已直接写在 `staticSpells` 对应位置(以 `/` 开头),以保持键位。 ### 8.2 何时用 special 而不是写进 static diff --git a/README.md b/README.md index 037ecaf9..503246fb 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Shigure 是一个 Windows WinForms 桌面程序。它从目标窗口读取 Fuyut - 置顶浮动条:显示程序名、当前职业图标颜色和逻辑状态,提供 `开启/关闭`、`设置`、`✕` 按钮。窗口可拖动和缩放,显示后自动启动运行循环。 - `通用`:设置触发键和发送模式;从项目 Fuyutsui 更新配置并同步游戏插件;按实时环境选择模块,或按职业、专精、英雄天赋和队伍类型指定默认模块;从 GitHub 按需下载或更新技能图标数据包。 - `配置`:直接编辑项目 `Fuyutsui/class/*.lua` 中的 `ClassBlocks`,包括状态、光环、法术和队伍字段;技能列表页可编辑 `Fuyutsui.spellsList` 中索引 1–100 的法术 ID、索引和名称,也可输入 spellId 与名称并自动分配空闲索引。旧版稀疏索引格式只读,需先迁移到 `states/auras/spells/group` 格式。 -- `宏`:编辑项目 `Fuyutsui/core/classmacros.lua` 中各职业的动态宏、静态宏和特殊宏。动态宏每项占用 30 个团队点名槽位。 +- `宏`:编辑项目 `Fuyutsui/core/classmacros.lua` 中各职业的动态宏、静态宏和特殊宏。动态宏每项占用 30 个团队点名槽位;特殊宏的技能名必须手工填写,不从宏正文解析,生成映射时固定为无目标、无宏条件。 - `模块`:新建、编辑、删除本地模块,维护作者、推荐天赋、匹配条件、动态字段和有序规则。规则支持拖拽、上移、下移、复制与插入。 - `状态`:分栏显示基础状态、`auras`、`spells` 和模块计算出的动态单位/数值。 - `队伍`:显示 `group` 中当前队伍成员及扫描字段摘要。 diff --git a/UI/ClassMacrosEditorControl.cs b/UI/ClassMacrosEditorControl.cs index 35d8d09f..603a719c 100644 --- a/UI/ClassMacrosEditorControl.cs +++ b/UI/ClassMacrosEditorControl.cs @@ -261,7 +261,13 @@ public sealed class ClassMacrosEditorControl : UserControl { BuildDynamicPage(), BuildArrayPage(_staticGrid, "class-macros-static", "完整宏", "注释", showParsedMacro: true), - BuildArrayPage(_specialGrid, "class-macros-special", "完整宏", "注释", showParsedMacro: true) + BuildArrayPage( + _specialGrid, + "class-macros-special", + "完整宏", + "注释", + showParsedMacro: false, + editSpecialSpell: true) }; foreach (var page in pages) { @@ -401,7 +407,8 @@ public sealed class ClassMacrosEditorControl : UserControl string cacheKey, string textHeader, string commentHeader, - bool showParsedMacro) + bool showParsedMacro, + bool editSpecialSpell = false) { var panel = new TableLayoutPanel { @@ -445,6 +452,15 @@ public sealed class ClassMacrosEditorControl : UserControl ReadOnly = true }); } + else if (editSpecialSpell) + { + grid.Columns.Add(new DataGridViewTextBoxColumn + { + Name = "Spell", + HeaderText = "技能", + Width = 180 + }); + } grid.Columns.Add(new DataGridViewTextBoxColumn { @@ -452,7 +468,10 @@ public sealed class ClassMacrosEditorControl : UserControl HeaderText = textHeader, AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill }); - grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Comment", HeaderText = commentHeader, Width = 180 }); + if (!editSpecialSpell) + { + grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Comment", HeaderText = commentHeader, Width = 180 }); + } grid.Columns.Add(CreateDeleteColumn()); WireGrid(grid); panel.Controls.Add(grid, 0, 0); @@ -472,7 +491,7 @@ public sealed class ClassMacrosEditorControl : UserControl return; } - if ((grid == _staticGrid || grid == _specialGrid) + if (grid == _staticGrid && e.RowIndex >= 0 && e.ColumnIndex >= 0 && grid.Columns[e.ColumnIndex].Name is "Text" or "Comment") @@ -955,7 +974,8 @@ public sealed class ClassMacrosEditorControl : UserControl } var text = row.Cells["Text"].Value?.ToString() ?? ""; - var comment = row.Cells["Comment"].Value?.ToString()?.Trim(); + var metadataColumn = grid.Columns.Contains("Comment") ? "Comment" : "Spell"; + var comment = row.Cells[metadataColumn].Value?.ToString()?.Trim(); target.Add(new ClassMacrosStore.ArrayEntry { Text = text.Replace("\r\n", "\n", StringComparison.Ordinal), @@ -1137,9 +1157,16 @@ public sealed class ClassMacrosEditorControl : UserControl var row = grid.Rows[rowIndex]; row.Cells["Index"].Value = (i + 1).ToString(CultureInfo.InvariantCulture); row.Cells["Text"].Value = entry.Text; - row.Cells["Comment"].Value = entry.Comment ?? ""; + if (grid == _specialGrid) + { + row.Cells["Spell"].Value = entry.Comment ?? ""; + } + else + { + row.Cells["Comment"].Value = entry.Comment ?? ""; + } row.Cells["Delete"].Value = "×"; - if (grid == _staticGrid || grid == _specialGrid) + if (grid == _staticGrid) { UpdateMacroDisplay(grid, row); } @@ -1158,16 +1185,12 @@ public sealed class ClassMacrosEditorControl : UserControl var text = row.Cells["Text"].Value?.ToString() ?? ""; var comment = row.Cells["Comment"].Value?.ToString(); - var parsed = grid == _specialGrid - ? FuyutsuiKeymapConverter.ParseSpecialMacro(text, comment) - : FuyutsuiKeymapConverter.ParseStaticMacro(text, comment); + var parsed = FuyutsuiKeymapConverter.ParseStaticMacro(text, comment); _updatingDerivedColumns = true; try { - row.Cells["Unit"].Value = grid == _specialGrid - ? ReservedUnit.ToDisplayText(parsed.Unit) - : parsed.Unit.ToString(CultureInfo.InvariantCulture); + row.Cells["Unit"].Value = parsed.Unit.ToString(CultureInfo.InvariantCulture); row.Cells["Spell"].Value = parsed.Spell; row.Cells["Condition"].Value = MacroConditionText.ToDisplayText(parsed.Condition); }