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