using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Globalization; using System.Text.RegularExpressions; namespace Shigure; public sealed class ModuleEditorControl : UserControl { private const string ModuleWebsiteUrl = "https://www.shigure.club"; // 使用独立列名避开旧版“注释”文字列缓存的较大宽度;新图标列从紧凑宽度重新开始缓存。 private const string RuleCommentColumnName = "RuleComment"; private readonly ModuleStore _moduleStore; private readonly Func _runtimeRestartRequested; private readonly Func _captureDependencies; private readonly Func _modulesReloadRequested; private readonly string _baseDirectory; private ConditionFieldCatalog _fieldCatalog; private KeymapCatalog _keymapCatalog; private readonly ListBox _moduleList = new(); private readonly TextBox _nameBox = new(); private readonly TextBox _authorBox = new(); private readonly TextBox _recommendedTalentBox = new(); private readonly UiDropDown _classBox = new(); private readonly UiDropDown _specBox = new(); private readonly UiDropDown _partyTypeBox = new(); private readonly UiDropDown _heroTalentBox = new(); private readonly DataGridView _rulesGrid = new(); private readonly DataGridView _adjustmentsGrid = new(); private readonly DataGridView _formulaAdjustmentsGrid = new(); private readonly DataGridViewComboBoxColumn _spellColumn = new(); private readonly DataGridViewComboBoxColumn _unitColumn = new(); private readonly DataGridViewComboBoxColumn _macroConditionColumn = new(); private ToolStripDropDown? _rulesComboDropDown; private ToolStripDropDown? _adjustmentComboDropDown; private readonly DataGridViewTextBoxColumn _adjustmentFieldColumn = new(); private readonly DataGridViewComboBoxColumn _adjustmentTypeColumn = new(); private readonly ListView _unitsList = new(); private readonly Label _pathLabel = new(); private readonly Label _versionLabel = new(); private readonly Label _unitsEmptyHint = new(); private readonly Label _editorEmptyHint = new(); private readonly ToolTip _pathToolTip = new(); private Button _saveButton = null!; private Button _deleteButton = null!; private Button _addButton = null!; private readonly ToolTip _rulesGridToolTip = new() { InitialDelay = 300, ReshowDelay = 100, AutoPopDelay = 4000, ShowAlways = true }; private List _modules = new(); private ModuleDefinition? _selectedModule; // 当前编辑中模块的动态单位/数量字段(含未保存的新增), 供目标下拉与条件字段使用。 private readonly List _units = new(); private readonly List _counts = new(); private readonly List _valueAdjustments = new(); private readonly Dictionary> _currentClassSpellIdsByName = new(StringComparer.Ordinal); private readonly Dictionary> _currentSpecItemIdsByName = new(StringComparer.Ordinal); private readonly List _currentClassConditionSpells = new(); private readonly List _currentClassConditionItems = new(); private HashSet? _availableConditionFields; private HashSet? _availableGroupConditionFields; private Dictionary? _conditionFieldDisplayNames; // 载入时程序化写入"类型"单元格会触发 CellValueChanged; 置真以跳过"按类型清空数值"的联动。 private bool _suppressAdjustmentTypeChange; private bool _moduleCommandInProgress; // 规则行拖拽重排: 拖动起始行, 以及拖动中的插入指示位置(显示一条强调线)。 private int _dragSourceRow = -1; private int _dragIndicatorRow = -1; private static readonly PartyTypeOption[] PartyTypeOptions = [ new("任意 (*)", null), new("单人 (0)", "0"), new("团队 (1-40)", "1-40"), new("队伍 (46)", "46") ]; private static readonly MatchOption[] ClassOptions = BuildClassOptions(); private static readonly HashSet NonAuraGroupFields = new(StringComparer.OrdinalIgnoreCase) { "生命值", "职责", "驱散", "治疗吸收" }; // 条件动态数值"类型"下拉: 决定"数值"可选项的过滤类别, 顺序与界面一致。 private static readonly (string Text, ConditionFieldCategory Category)[] AdjustmentTypeOptions = [ ("状态", ConditionFieldCategory.State), ("技能", ConditionFieldCategory.Spell), ("光环", ConditionFieldCategory.Aura), ("动态单位", ConditionFieldCategory.DynamicUnit), ("动态数值", ConditionFieldCategory.DynamicValue) ]; public ModuleEditorControl( ModuleStore moduleStore, Func runtimeRestartRequested, Func captureDependencies, Func modulesReloadRequested, string baseDirectory) { _moduleStore = moduleStore; _runtimeRestartRequested = runtimeRestartRequested; _captureDependencies = captureDependencies; _modulesReloadRequested = modulesReloadRequested; _baseDirectory = baseDirectory; _fieldCatalog = ConditionFieldCatalog.Load(baseDirectory); _keymapCatalog = KeymapCatalog.Load(baseDirectory); InitializeComponent(); SpellIconCatalog.CatalogChanged += OnSpellIconCatalogChanged; LoadModules(); } protected override void Dispose(bool disposing) { if (disposing) { CloseRulesComboDropDown(); CloseAdjustmentComboDropDown(); SpellIconCatalog.CatalogChanged -= OnSpellIconCatalogChanged; } base.Dispose(disposing); } public void ReloadCatalogs() { _fieldCatalog = ConditionFieldCatalog.Load(_baseDirectory); _keymapCatalog = KeymapCatalog.Load(_baseDirectory); ReloadCurrentClassSpellIds(); // “更新配置”可能刚重建了 keymap;立即刷新当前规则的技能/目标/宏条件下拉, // 避免必须切换职业或重启应用后才能看到新解析出的宏条件。 RefreshKeymapColumns(); RefreshAdjustmentFieldColumn(); RefreshRuleSpellIcons(); _rulesGrid.Invalidate(); } private const int ModuleFooterBarHeight = 56; private const int ModuleFooterButtonHeight = 36; private void InitializeComponent() { Dock = DockStyle.Fill; BackColor = UiTheme.Surface; ForeColor = UiTheme.Text; Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Regular, GraphicsUnit.Point); var root = new TableLayoutPanel { Dock = DockStyle.Fill, BackColor = UiTheme.Surface, ColumnCount = 2, RowCount = 2, Margin = new Padding(0) }; root.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 280)); root.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); root.RowStyles.Add(new RowStyle(SizeType.Absolute, ModuleFooterBarHeight)); Controls.Add(root); root.Controls.Add(BuildSidebar(), 0, 0); root.Controls.Add(BuildEditor(), 1, 0); root.Controls.Add(BuildSidebarFooter(), 0, 1); root.Controls.Add(BuildActionRow(), 1, 1); } private Control BuildSidebar() { var sidebar = new UiCardPanel { Dock = DockStyle.Fill, Padding = new Padding(UiTheme.CardPadding), Margin = new Padding(0, 0, UiTheme.PageGap, UiTheme.PageGap), ColumnCount = 1, RowCount = 1 }; sidebar.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); _moduleList.Dock = DockStyle.Fill; UiTheme.StyleListBox( _moduleList, Font, index => index >= 0 && index < _modules.Count ? (_modules[index].Match.ClassId, _modules[index].Match.SpecId) : (null, null), itemForeColorSelector: index => index >= 0 && index < _modules.Count && _moduleStore.HasImportIssue(_modules[index].Id) ? UiTheme.Danger : null); _moduleList.BackColor = UiTheme.SurfaceRaised; _moduleList.SelectedIndexChanged += (_, _) => SelectModule(_moduleList.SelectedIndex); sidebar.Controls.Add(_moduleList, 0, 0); return sidebar; } private Control BuildSidebarFooter() { var footer = new UiCardPanel { Dock = DockStyle.Fill, Padding = new Padding(UiTheme.CardPadding, 10, UiTheme.CardPadding, 10), Margin = new Padding(0, 0, UiTheme.PageGap, 0), ColumnCount = 3, RowCount = 1 }; footer.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); footer.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 8)); footer.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); footer.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); var reloadButton = UiTheme.CreateButton("刷新", UiTheme.ButtonKind.Secondary); StyleModuleFooterButton(reloadButton); reloadButton.Dock = DockStyle.Fill; reloadButton.Click += async (_, _) => await RunModuleCommandAsync(_modulesReloadRequested); var getModulesButton = UiTheme.CreateButton( "获取模块", Color.FromArgb(252, 238, 10), Color.Black); StyleModuleFooterButton(getModulesButton); getModulesButton.Dock = DockStyle.Fill; getModulesButton.Padding = new Padding(0, 2, 24, 2); getModulesButton.FlatAppearance.BorderColor = Color.FromArgb(252, 238, 10); getModulesButton.FlatAppearance.MouseOverBackColor = Color.FromArgb(255, 244, 64); getModulesButton.FlatAppearance.MouseDownBackColor = Color.FromArgb(220, 207, 8); getModulesButton.Paint += (_, e) => UiTheme.DrawExternalLinkIcon( e.Graphics, getModulesButton.ClientRectangle, getModulesButton.Text, getModulesButton.Font, getModulesButton.ForeColor, getModulesButton.DeviceDpi / 96F); getModulesButton.Click += (_, _) => OpenModuleWebsite(); footer.Controls.Add(reloadButton, 0, 0); footer.Controls.Add(getModulesButton, 2, 0); return footer; } private static void OpenModuleWebsite() { try { Process.Start(new ProcessStartInfo(ModuleWebsiteUrl) { UseShellExecute = true }); } catch (Exception ex) { MessageBox.Show( $"无法打开模块网站: {ex.Message}", "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } private Control BuildEditor() { var editor = new TableLayoutPanel { Dock = DockStyle.Fill, BackColor = UiTheme.Surface, Padding = new Padding(0), Margin = new Padding(0, 0, 0, UiTheme.PageGap), ColumnCount = 1, RowCount = 3 }; editor.RowStyles.Add(new RowStyle(SizeType.Absolute, 86)); editor.RowStyles.Add(new RowStyle(SizeType.Absolute, 120)); editor.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); editor.Controls.Add(BuildNameRow(), 0, 0); editor.Controls.Add(BuildMatchRow(), 0, 1); editor.Controls.Add(BuildEditorTabs(), 0, 2); return editor; } private Control BuildEditorTabs() { var root = new TableLayoutPanel { Dock = DockStyle.Fill, Margin = new Padding(0, UiTheme.PageGap, 0, 0), Padding = new Padding(0), BackColor = UiTheme.Surface, ColumnCount = 1, RowCount = 2 }; root.RowStyles.Add(new RowStyle(SizeType.Absolute, UiTheme.TabBarHeight)); root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); var tabBar = new TableLayoutPanel { Dock = DockStyle.Fill, BackColor = UiTheme.Surface, ColumnCount = 3, RowCount = 1, Margin = new Padding(0, 0, 0, 8), Padding = new Padding(0) }; tabBar.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); for (var i = 0; i < 3; i++) { tabBar.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F / 3F)); } var contentCard = new UiCardPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 1, Margin = new Padding(0), Padding = new Padding(0) }; contentCard.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); contentCard.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); var contentHost = new Panel { Dock = DockStyle.Fill, BackColor = UiTheme.SurfaceRaised, Margin = new Padding(0) }; contentCard.Controls.Add(contentHost, 0, 0); var pages = new[] { BuildRulesPanel(), BuildUnitsPanel(), BuildAdjustmentsPanel(), }; foreach (var page in pages) { page.Dock = DockStyle.Fill; page.Visible = false; page.BackColor = UiTheme.SurfaceRaised; contentHost.Controls.Add(page); } _editorEmptyHint.Text = "请在左侧选择模块, 或点击「新建」创建"; _editorEmptyHint.Dock = DockStyle.Fill; _editorEmptyHint.TextAlign = ContentAlignment.MiddleCenter; _editorEmptyHint.ForeColor = UiTheme.Muted; _editorEmptyHint.BackColor = UiTheme.SurfaceRaised; _editorEmptyHint.Visible = false; contentHost.Controls.Add(_editorEmptyHint); _editorEmptyHint.BringToFront(); var tabs = new UiPillTab[3]; var selectedIndex = -1; void SelectTab(int index) { if (selectedIndex == index) { return; } selectedIndex = index; for (var i = 0; i < tabs.Length; i++) { var selected = i == index; tabs[i].Selected = selected; pages[i].Visible = selected; if (selected) { pages[i].BringToFront(); } } } var titles = new[] { "逻辑编辑", "动态单位", "动态数值" }; for (var i = 0; i < titles.Length; i++) { var index = i; var tab = new UiPillTab(titles[i]); tab.Click += (_, _) => SelectTab(index); tabs[i] = tab; tabBar.Controls.Add(tab, i, 0); } root.Controls.Add(tabBar, 0, 0); root.Controls.Add(contentCard, 0, 1); SelectTab(0); return root; } private Control BuildAdjustmentsPanel() { var panel = new TableLayoutPanel { Dock = DockStyle.Fill, BackColor = UiTheme.SurfaceRaised, ColumnCount = 1, RowCount = 4, Padding = new Padding(UiTheme.CardPadding), Margin = new Padding(0) }; panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 28)); panel.RowStyles.Add(new RowStyle(SizeType.Percent, 50)); panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 30)); panel.RowStyles.Add(new RowStyle(SizeType.Percent, 50)); panel.Controls.Add(CreateSectionLabel("条件动态数值"), 0, 0); panel.Controls.Add(BuildAdjustmentsGrid(), 0, 1); panel.Controls.Add(CreateSectionLabel("公式动态数值"), 0, 2); panel.Controls.Add(BuildFormulaAdjustmentsGrid(), 0, 3); return panel; } private Control BuildRulesPanel() { var panel = new TableLayoutPanel { Dock = DockStyle.Fill, BackColor = UiTheme.SurfaceRaised, ColumnCount = 1, RowCount = 1, Padding = new Padding(UiTheme.CardPadding), Margin = new Padding(0) }; panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); panel.Controls.Add(BuildRulesGrid(), 0, 0); return panel; } private Control BuildUnitsPanel() { var panel = new TableLayoutPanel { Dock = DockStyle.Fill, BackColor = UiTheme.SurfaceRaised, ColumnCount = 2, RowCount = 1, Padding = new Padding(UiTheme.CardPadding), Margin = new Padding(0) }; panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 92)); panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); UiTheme.ConfigureListViewColumns( _unitsList, Font, "module-units", new UiTheme.ListColumn("名称", 210, 420), new UiTheme.ListColumn("类型", 80, 240), new UiTheme.ListColumn("摘要", 160, 2000, FillRemaining: true)); _unitsList.MultiSelect = false; _unitsList.DoubleClick += (_, _) => EditSelectedUnit(); _unitsList.KeyDown += OnUnitsListKeyDown; _unitsEmptyHint.Text = "暂无动态单位 / 数量\n点击右侧「添加」创建"; _unitsEmptyHint.Dock = DockStyle.Fill; _unitsEmptyHint.TextAlign = ContentAlignment.MiddleCenter; _unitsEmptyHint.ForeColor = UiTheme.Muted; _unitsEmptyHint.BackColor = UiTheme.Surface; _unitsEmptyHint.Visible = false; // 列表与空状态提示叠放在同一宿主里, 列表为空时显示提示。 var listHost = new Panel { Dock = DockStyle.Fill, BackColor = UiTheme.Surface, Margin = new Padding(0) }; listHost.Controls.Add(_unitsEmptyHint); listHost.Controls.Add(_unitsList); _unitsEmptyHint.BringToFront(); panel.Controls.Add(listHost, 0, 0); var buttons = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.TopDown, WrapContents = false, BackColor = UiTheme.SurfaceRaised, Margin = new Padding(8, 0, 0, 0), Padding = new Padding(0) }; buttons.Resize += (_, _) => LayoutUnitActionButtons(buttons); var addButton = CreateUnitActionButton("添加", UiTheme.Field, UiTheme.Text, bottomGap: true); addButton.Click += (_, _) => AddUnit(); var editButton = CreateUnitActionButton("编辑", UiTheme.Field, UiTheme.Text, bottomGap: true); editButton.Click += (_, _) => EditSelectedUnit(); var deleteButton = CreateUnitActionButton("删除", UiTheme.Field, UiTheme.Danger, bottomGap: false); deleteButton.Click += (_, _) => DeleteSelectedUnit(); buttons.Controls.Add(addButton); buttons.Controls.Add(editButton); buttons.Controls.Add(deleteButton); panel.Controls.Add(buttons, 1, 0); return panel; } private Control BuildNameRow() { var row = new UiCardPanel { Dock = DockStyle.Fill, ColumnCount = 4, RowCount = 2, Padding = new Padding(UiTheme.CardPadding, 10, UiTheme.CardPadding, 8), Margin = new Padding(0, 0, 0, UiTheme.PageGap) }; // 名称/作者各占剩余宽度的一半, 两个输入框等宽并铺满窗口。 row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 58)); row.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 58)); row.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); row.RowStyles.Add(new RowStyle(SizeType.Absolute, 32)); row.RowStyles.Add(new RowStyle(SizeType.Absolute, 26)); row.Controls.Add(CreateLabel("名称"), 0, 0); UiTheme.StyleTextBox(_nameBox); _nameBox.Dock = DockStyle.Fill; row.Controls.Add(_nameBox, 1, 0); var authorLabel = CreateLabel("作者"); authorLabel.Margin = new Padding(10, 0, 0, 0); row.Controls.Add(authorLabel, 2, 0); UiTheme.StyleTextBox(_authorBox); _authorBox.Dock = DockStyle.Fill; row.Controls.Add(_authorBox, 3, 0); _pathLabel.Dock = DockStyle.Fill; _pathLabel.ForeColor = UiTheme.Muted; _pathLabel.BackColor = Color.Transparent; _pathLabel.TextAlign = ContentAlignment.MiddleLeft; _pathLabel.AutoEllipsis = true; _pathLabel.TextChanged += (_, _) => _pathToolTip.SetToolTip(_pathLabel, _pathLabel.Text); row.Controls.Add(_pathLabel, 0, 1); row.SetColumnSpan(_pathLabel, 3); // 版本号紧贴窗口右侧, 右对齐显示在"路径"同一行。 _versionLabel.Dock = DockStyle.Fill; _versionLabel.ForeColor = UiTheme.Muted; _versionLabel.BackColor = Color.Transparent; _versionLabel.TextAlign = ContentAlignment.MiddleRight; _versionLabel.AutoEllipsis = true; row.Controls.Add(_versionLabel, 3, 1); return row; } private Control BuildMatchRow() { var matchLabels = new[] { "职业", "专精", "英雄天赋", "队伍类型" }; var row = new UiCardPanel { Dock = DockStyle.Fill, ColumnCount = 12, RowCount = 2, Padding = new Padding(UiTheme.CardPadding), Margin = new Padding(0) }; foreach (var label in matchLabels) { row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, MeasureLabelColumnWidth(label, Font))); // 下拉框由原来的 25% 缩短到 20%,余下 5% 作为与下一项标签之间的弹性间隔。 row.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20)); row.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 5)); } // RowCount 会预置 Percent 样式, 必须 Clear 后再设 Absolute, 否则 Add 只追加到末尾不生效。 row.RowStyles.Clear(); row.RowStyles.Add(new RowStyle(SizeType.Absolute, 36)); row.RowStyles.Add(new RowStyle(SizeType.Absolute, 52)); ResetClassOptions(_classBox); ResetSpecOptions(_specBox, null); ResetHeroTalentOptions(_heroTalentBox, null, null); _classBox.SelectedIndexChanged += (_, _) => { ResetSpecOptions(_specBox, ReadMatchCombo(_classBox)); ResetHeroTalentOptions(_heroTalentBox, ReadMatchCombo(_classBox), ReadMatchCombo(_specBox)); ReloadCurrentClassSpellIds(); RefreshKeymapColumns(); RefreshAdjustmentFieldColumn(); RefreshRuleSpellIcons(); InvalidateConditionFieldValidation(); _rulesGrid.Invalidate(); }; _specBox.SelectedIndexChanged += (_, _) => { ResetHeroTalentOptions(_heroTalentBox, ReadMatchCombo(_classBox), ReadMatchCombo(_specBox)); ReloadCurrentClassSpellIds(); RefreshRuleSpellIcons(); RefreshAdjustmentFieldColumn(); InvalidateConditionFieldValidation(); _rulesGrid.Invalidate(); }; AddMatchField(row, "职业:", _classBox, 0); AddMatchField(row, "专精:", _specBox, 3); AddMatchField(row, "英雄天赋:", _heroTalentBox, 6); AddMatchField(row, "队伍类型:", _partyTypeBox, 9); var recommendedTalentRow = new TableLayoutPanel { Dock = DockStyle.Fill, BackColor = Color.Transparent, ColumnCount = 2, RowCount = 1, // 推荐天赋整行相对原位置下移 4px,并与上方匹配项保持清晰间距。 Margin = new Padding(0, 12, 0, 0) }; recommendedTalentRow.RowStyles.Clear(); recommendedTalentRow.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); recommendedTalentRow.ColumnStyles.Add(new ColumnStyle( SizeType.Absolute, MeasureLabelColumnWidth("推荐天赋", Font))); recommendedTalentRow.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); var recommendedTalentLabel = CreateLabel("推荐天赋:"); recommendedTalentLabel.AutoSize = false; recommendedTalentLabel.Margin = Padding.Empty; recommendedTalentRow.Controls.Add(recommendedTalentLabel, 0, 0); UiTheme.StyleTextBox(_recommendedTalentBox); _recommendedTalentBox.Dock = DockStyle.None; _recommendedTalentBox.Anchor = AnchorStyles.Left | AnchorStyles.Right; _recommendedTalentBox.Margin = Padding.Empty; recommendedTalentRow.Controls.Add(_recommendedTalentBox, 1, 0); row.Controls.Add(recommendedTalentRow, 0, 1); row.SetColumnSpan(recommendedTalentRow, 12); return row; } private Control BuildAdjustmentsGrid() { UiTheme.StyleDataGridView(_adjustmentsGrid); _adjustmentsGrid.AllowUserToAddRows = true; _adjustmentsGrid.AllowUserToDeleteRows = false; _adjustmentsGrid.AllowUserToResizeColumns = true; _adjustmentsGrid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None; _adjustmentsGrid.Columns.Add(new DataGridViewCheckBoxColumn { Name = "Enabled", HeaderText = "启用", Width = 68, AutoSizeMode = DataGridViewAutoSizeColumnMode.None }); _adjustmentFieldColumn.Name = "Field"; _adjustmentFieldColumn.HeaderText = "数值"; _adjustmentFieldColumn.Width = 260; _adjustmentFieldColumn.MinimumWidth = 200; _adjustmentFieldColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.None; _adjustmentsGrid.Columns.Add(_adjustmentFieldColumn); _adjustmentsGrid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Delta", HeaderText = "调整", Width = 70, AutoSizeMode = DataGridViewAutoSizeColumnMode.None }); _adjustmentsGrid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Condition", HeaderText = "条件 (点击编辑)", AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, ReadOnly = true }); AddDeleteColumn(_adjustmentsGrid); // "类型"列加在集合末尾以保留 Rows.Add 的位置参数(启用/数值/调整/条件), 再用 DisplayIndex 显示到"数值"前。 _adjustmentTypeColumn.Name = "Type"; _adjustmentTypeColumn.HeaderText = "类型"; _adjustmentTypeColumn.Width = 140; _adjustmentTypeColumn.MinimumWidth = 100; _adjustmentTypeColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.None; _adjustmentTypeColumn.FlatStyle = FlatStyle.Flat; _adjustmentTypeColumn.DisplayStyle = DataGridViewComboBoxDisplayStyle.DropDownButton; _adjustmentTypeColumn.ReadOnly = true; foreach (var option in AdjustmentTypeOptions) { _adjustmentTypeColumn.Items.Add(option.Text); } _adjustmentsGrid.Columns.Add(_adjustmentTypeColumn); _adjustmentTypeColumn.DisplayIndex = 1; _adjustmentsGrid.CellClick += OnAdjustmentsGridCellClick; _adjustmentsGrid.CellFormatting += OnAdjustmentsGridCellFormatting; _adjustmentsGrid.CellPainting += OnAdjustmentsGridCellPainting; _adjustmentsGrid.CellValueChanged += OnAdjustmentsGridCellValueChanged; _adjustmentsGrid.DataError += (_, e) => e.ThrowException = false; _adjustmentsGrid.EditingControlShowing += OnAdjustmentsGridEditingControlShowing; _adjustmentsGrid.KeyDown += OnAdjustmentsGridKeyDown; _adjustmentsGrid.CellBeginEdit += (_, _) => CloseAdjustmentComboDropDown(); _adjustmentsGrid.Disposed += (_, _) => CloseAdjustmentComboDropDown(); _adjustmentsGrid.CurrentCellDirtyStateChanged += (_, _) => { if (_adjustmentsGrid.IsCurrentCellDirty && _adjustmentsGrid.CurrentCell is DataGridViewCheckBoxCell) { _adjustmentsGrid.CommitEdit(DataGridViewDataErrorContexts.Commit); } }; RefreshAdjustmentFieldColumn(); UiTheme.CacheDataGridViewColumnWidths(_adjustmentsGrid, "module-adjustments"); return _adjustmentsGrid; } private Control BuildFormulaAdjustmentsGrid() { UiTheme.StyleDataGridView(_formulaAdjustmentsGrid); _formulaAdjustmentsGrid.AllowUserToAddRows = true; _formulaAdjustmentsGrid.AllowUserToDeleteRows = false; _formulaAdjustmentsGrid.AllowUserToResizeColumns = true; _formulaAdjustmentsGrid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None; _formulaAdjustmentsGrid.Columns.Add(new DataGridViewCheckBoxColumn { Name = "Enabled", HeaderText = "启用", Width = 68, AutoSizeMode = DataGridViewAutoSizeColumnMode.None }); _formulaAdjustmentsGrid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Field", HeaderText = "数值名称", Width = 180, AutoSizeMode = DataGridViewAutoSizeColumnMode.None }); _formulaAdjustmentsGrid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Formula", HeaderText = "公式 (点击编辑)", AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, ReadOnly = true }); AddDeleteColumn(_formulaAdjustmentsGrid); _formulaAdjustmentsGrid.CellClick += OnFormulaAdjustmentsGridCellClick; _formulaAdjustmentsGrid.CellPainting += OnFormulaAdjustmentsGridCellPainting; _formulaAdjustmentsGrid.CellEndEdit += OnFormulaAdjustmentsGridCellEndEdit; _formulaAdjustmentsGrid.DataError += (_, e) => e.ThrowException = false; _formulaAdjustmentsGrid.UserDeletedRow += (_, _) => RefreshAdjustmentFieldColumn(); _formulaAdjustmentsGrid.CurrentCellDirtyStateChanged += (_, _) => { if (_formulaAdjustmentsGrid.IsCurrentCellDirty && _formulaAdjustmentsGrid.CurrentCell is DataGridViewCheckBoxCell) { _formulaAdjustmentsGrid.CommitEdit(DataGridViewDataErrorContexts.Commit); } }; RefreshAdjustmentFieldColumn(); UiTheme.CacheDataGridViewColumnWidths(_formulaAdjustmentsGrid, "module-formula-adjustments"); return _formulaAdjustmentsGrid; } private Control BuildRulesGrid() { UiTheme.StyleDataGridView(_rulesGrid); _rulesGrid.AllowUserToAddRows = true; _rulesGrid.AllowUserToDeleteRows = false; _rulesGrid.AllowUserToResizeColumns = true; _rulesGrid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None; _rulesGrid.ShowCellToolTips = false; // 启用/技能/目标/宏条件列宽度固定可调并缓存; 条件列用 Fill 自动充满剩余窗口。 _rulesGrid.Columns.Add(new DataGridViewCheckBoxColumn { Name = "Enabled", HeaderText = "启用", Width = 68, AutoSizeMode = DataGridViewAutoSizeColumnMode.None }); _rulesGrid.Columns.Add(CreateSpellIconColumn()); _spellColumn.Name = "Spell"; _spellColumn.HeaderText = "技能"; _spellColumn.Width = 150; _spellColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.None; _spellColumn.FlatStyle = FlatStyle.Flat; _spellColumn.DisplayStyle = DataGridViewComboBoxDisplayStyle.DropDownButton; _spellColumn.ReadOnly = true; _rulesGrid.Columns.Add(_spellColumn); _unitColumn.Name = "Unit"; _unitColumn.HeaderText = "目标"; _unitColumn.Width = 150; _unitColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.None; _unitColumn.FlatStyle = FlatStyle.Flat; _unitColumn.DisplayStyle = DataGridViewComboBoxDisplayStyle.DropDownButton; _unitColumn.ReadOnly = true; _rulesGrid.Columns.Add(_unitColumn); _macroConditionColumn.Name = "MacroCondition"; _macroConditionColumn.HeaderText = "宏条件"; _macroConditionColumn.Width = 150; _macroConditionColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.None; _macroConditionColumn.FlatStyle = FlatStyle.Flat; _macroConditionColumn.DisplayStyle = DataGridViewComboBoxDisplayStyle.DropDownButton; _macroConditionColumn.ReadOnly = true; _rulesGrid.Columns.Add(_macroConditionColumn); _rulesGrid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Condition", HeaderText = "条件 (点击编辑)", AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, MinimumWidth = 220, ReadOnly = true, SortMode = DataGridViewColumnSortMode.NotSortable }); _rulesGrid.Columns.Add(new DataGridViewTextBoxColumn { Name = RuleCommentColumnName, HeaderText = "注释", Width = 64, MinimumWidth = 40, AutoSizeMode = DataGridViewAutoSizeColumnMode.None, ReadOnly = true, SortMode = DataGridViewColumnSortMode.NotSortable, HeaderCell = new DataGridViewColumnHeaderCell { Value = "注释", Style = new DataGridViewCellStyle { Alignment = DataGridViewContentAlignment.MiddleCenter } }, DefaultCellStyle = new DataGridViewCellStyle { Alignment = DataGridViewContentAlignment.MiddleCenter } }); AddRuleIconColumn("MoveUp", "▲", "上移"); AddRuleIconColumn("MoveDown", "▼", "下移"); AddRuleIconColumn("Copy", "⧉", "复制到下一行"); AddRuleIconColumn("InsertBlank", "+", "在下一行添加空白条件"); AddRuleIconColumn("Delete", "×", "删除", UiTheme.Danger); // 拖拽手柄列: 加在集合末尾(保持 Rows.Add 的位置参数仍对应 启用/技能/目标/宏条件/条件), // 用 DisplayIndex=0 显示到"启用"前面。自绘六点抓手, 按住拖动可调整该条逻辑顺序。 _rulesGrid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Drag", HeaderText = string.Empty, Width = 30, MinimumWidth = 30, AutoSizeMode = DataGridViewAutoSizeColumnMode.None, Resizable = DataGridViewTriState.False, ReadOnly = true }); _rulesGrid.Columns.Add(new DataGridViewTextBoxColumn { Name = "RuleNumber", HeaderText = "#", Width = 48, MinimumWidth = 48, AutoSizeMode = DataGridViewAutoSizeColumnMode.None, Resizable = DataGridViewTriState.False, ReadOnly = true, SortMode = DataGridViewColumnSortMode.NotSortable }); _rulesGrid.Columns["RuleNumber"]!.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter; _rulesGrid.Columns["RuleNumber"]!.DefaultCellStyle.ForeColor = UiTheme.Muted; _rulesGrid.Columns["Drag"]!.DisplayIndex = 0; _rulesGrid.Columns["RuleNumber"]!.DisplayIndex = 1; _rulesGrid.AllowDrop = true; _rulesGrid.CellClick += OnRulesGridCellClick; _rulesGrid.CellFormatting += OnRulesGridCellFormatting; _rulesGrid.CellPainting += OnRulesGridCellPainting; _rulesGrid.CellMouseEnter += OnRulesGridCellMouseEnter; _rulesGrid.CellMouseLeave += OnRulesGridCellMouseLeave; _rulesGrid.MouseLeave += (_, _) => _rulesGridToolTip.Hide(_rulesGrid); _rulesGrid.MouseDown += OnRulesGridMouseDown; _rulesGrid.MouseMove += OnRulesGridMouseMove; _rulesGrid.DragOver += OnRulesGridDragOver; _rulesGrid.DragDrop += OnRulesGridDragDrop; _rulesGrid.DragLeave += (_, _) => ClearDragIndicator(); _rulesGrid.Paint += OnRulesGridPaint; _rulesGrid.DataError += (_, e) => e.ThrowException = false; _rulesGrid.CellValueChanged += OnRulesGridCellValueChanged; _rulesGrid.HandleCreated += (_, _) => SetCompactRulePrefixColumns(); SetCompactRulePrefixColumns(); RefreshKeymapColumns(); UiTheme.CacheDataGridViewColumnWidths(_rulesGrid, "module-rules"); return _rulesGrid; } // 规则表格最左侧的拖拽手柄和编号列只承载结构信息,宽度各缩短为原可读性保护宽度的一半。 private void SetCompactRulePrefixColumns() { const int compactWidth = 36; foreach (var name in new[] { "Drag", "RuleNumber" }) { var column = _rulesGrid.Columns[name]; if (column is null) { continue; } var width = UiTheme.Scale(_rulesGrid, compactWidth); column.MinimumWidth = width; column.Width = width; } } private void AddRuleIconColumn(string name, string icon, string tooltip, Color? foreColor = null) { var column = new DataGridViewButtonColumn { Name = name, HeaderText = string.Empty, Text = icon, UseColumnTextForButtonValue = true, Width = 32, MinimumWidth = 32, AutoSizeMode = DataGridViewAutoSizeColumnMode.None, Resizable = DataGridViewTriState.False, FlatStyle = FlatStyle.Flat }; column.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter; column.DefaultCellStyle.ForeColor = foreColor ?? UiTheme.Muted; column.DefaultCellStyle.SelectionForeColor = foreColor ?? UiTheme.Text; _rulesGrid.Columns.Add(column); } private static DataGridViewImageColumn CreateSpellIconColumn() => new() { Name = "SpellIcon", HeaderText = "图标", Width = 54, MinimumWidth = 54, AutoSizeMode = DataGridViewAutoSizeColumnMode.None, ImageLayout = DataGridViewImageCellLayout.Zoom, ReadOnly = true, SortMode = DataGridViewColumnSortMode.NotSortable, DefaultCellStyle = new DataGridViewCellStyle { Alignment = DataGridViewContentAlignment.MiddleCenter, NullValue = null, BackColor = UiTheme.Surface } }; // 两个动态数值表共用的红色 "×" 删除列。 private static void AddDeleteColumn(DataGridView grid) { grid.Columns.Add(new DataGridViewButtonColumn { Name = "Delete", HeaderText = string.Empty, Text = "×", ToolTipText = "删除", UseColumnTextForButtonValue = true, Width = 32, MinimumWidth = 32, AutoSizeMode = DataGridViewAutoSizeColumnMode.None, Resizable = DataGridViewTriState.False, FlatStyle = FlatStyle.Flat }); grid.Columns["Delete"]!.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter; grid.Columns["Delete"]!.DefaultCellStyle.ForeColor = UiTheme.Danger; } /// /// 按当前选中职业的 keymap 重建“技能/目标/宏条件”下拉选项。 /// 技能去重(同名技能只出现一次), unit 去重升序; 首项留空表示不填。 /// 已有行里不在 keymap 中的旧值会补录为额外选项, 避免数据丢失。 /// private void RefreshKeymapColumns() { var classId = ReadMatchCombo(_classBox); _spellColumn.Items.Clear(); _spellColumn.Items.Add(string.Empty); _spellColumn.Items.Add(ModuleSpecialActions.PauseSpell); _spellColumn.Items.Add(ModuleSpecialActions.FailedSpell); _spellColumn.Items.Add(ModuleSpecialActions.FailedItem); _spellColumn.Items.Add(ModuleSpecialActions.OneKeySpell); foreach (var spell in _keymapCatalog.GetSpells(classId)) { if (!_spellColumn.Items.Contains(spell)) { _spellColumn.Items.Add(spell); } } // 列级 unit 选项作为新行(尚未选技能)的默认全集; 已有行用单元格级选项按技能联动。 _unitColumn.Items.Clear(); _unitColumn.Items.Add(string.Empty); foreach (var unit in _keymapCatalog.GetUnits(classId)) { _unitColumn.Items.Add(ReservedUnit.ToDisplayText(unit)); } _macroConditionColumn.Items.Clear(); _macroConditionColumn.Items.Add(string.Empty); foreach (DataGridViewRow row in _rulesGrid.Rows) { if (row.IsNewRow) { continue; } EnsureComboItem(_spellColumn, row.Cells["Spell"].Value); UpdateUnitCellItems(row); UpdateMacroConditionCellItems(row); } } /// /// 按该行当前选中的技能, 把"目标"单元格的可选 unit 重建为该技能在 keymap 中实际配置过的值。 /// 旧值若不在新选项内则补录保留; 若是技能切换导致的非法值则清空。 /// private void UpdateUnitCellItems(DataGridViewRow row) { if (row.IsNewRow || row.Cells["Unit"] is not DataGridViewComboBoxCell cell) { return; } RebuildUnitCell(row, cell.Value?.ToString()); } /// /// 重建"目标"单元格选项并写入目标值。选项 = 当前技能在 keymap 中的 unit 集合。 /// desiredValue 合法则保留; 自定义技能(keymap 无该技能)保留旧值; 否则清空。 /// private void RebuildUnitCell(DataGridViewRow row, string? desiredValue) { if (row.IsNewRow || row.Cells["Unit"] is not DataGridViewComboBoxCell cell) { return; } var spell = row.Cells["Spell"].Value?.ToString(); cell.Items.Clear(); cell.Items.Add(string.Empty); if (ModuleSpecialActions.IsPauseSpell(spell)) { cell.Value = string.Empty; return; } if (ModuleSpecialActions.IsOneKeySpell(spell)) { var noTarget = ReservedUnit.ToDisplayText(ReservedUnit.None); cell.Items.Add(noTarget); cell.Value = noTarget; return; } var classId = ReadMatchCombo(_classBox); var allowed = ModuleSpecialActions.IsFailedSpell(spell) ? _keymapCatalog.GetUnitsForSpells(classId, _keymapCatalog.GetFailedSpellNames(classId)) : ModuleSpecialActions.IsFailedItem(spell) ? _keymapCatalog.GetUnitsForSpells(classId, _keymapCatalog.GetFailedItemNames(classId)) : _keymapCatalog.GetUnitsForSpell(classId, spell); foreach (var unit in allowed) { cell.Items.Add(ReservedUnit.ToDisplayText(unit)); } // 动态单位与技能无关, 始终可选; 放在 keymap 编号之后。 foreach (var unit in _units) { if (!string.IsNullOrWhiteSpace(unit.Name) && !cell.Items.Contains(unit.Name)) { cell.Items.Add(unit.Name); } } if (string.IsNullOrEmpty(desiredValue)) { cell.Value = string.Empty; } else if (cell.Items.Contains(desiredValue)) { // keymap 编号或动态单位名(已在上面加入), 直接保留。 cell.Value = desiredValue; } else if (allowed.Count == 0) { // 该技能不在 keymap(自定义技能), 保留旧值不强制清空。 cell.Items.Add(desiredValue); cell.Value = desiredValue; } else { // 技能切换导致旧目标非法, 清空。 cell.Value = string.Empty; } } private void UpdateMacroConditionCellItems(DataGridViewRow row) { if (row.IsNewRow || row.Cells["MacroCondition"] is not DataGridViewComboBoxCell cell) { return; } RebuildMacroConditionCell(row, cell.Value?.ToString()); } /// /// 按当前技能与目标重建“宏条件”选项。只有一个非空条件时自动选中; /// 自定义技能或动态单位没有 keymap 条目时保留已有值。 /// 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)[] : _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) { return; } var columnName = _rulesGrid.Columns[e.ColumnIndex].Name; // 技能改变时联动刷新该行"目标",技能或目标改变时再刷新"宏条件"。 if (columnName == "Spell") { _rulesGrid.Rows[e.RowIndex].Cells["SpellIcon"].Value = GetRuleSpellIcon(CellText(_rulesGrid.Rows[e.RowIndex], "Spell")); UpdateUnitCellItems(_rulesGrid.Rows[e.RowIndex]); UpdateMacroConditionCellItems(_rulesGrid.Rows[e.RowIndex]); } else if (columnName == "Unit") { UpdateMacroConditionCellItems(_rulesGrid.Rows[e.RowIndex]); } } private void OnSpellIconCatalogChanged() { if (IsDisposed || Disposing || !IsHandleCreated) { return; } if (InvokeRequired) { BeginInvoke(OnSpellIconCatalogChanged); return; } RefreshRuleSpellIcons(); _rulesGrid.Invalidate(); } private void ReloadCurrentClassSpellIds() { _currentClassSpellIdsByName.Clear(); _currentSpecItemIdsByName.Clear(); _currentClassConditionSpells.Clear(); _currentClassConditionItems.Clear(); var classId = ReadMatchCombo(_classBox); if (classId is null) { return; } var classPath = Path.Combine( _baseDirectory, "Fuyutsui", "class", $"{ClassNames.GetConfigFileName(classId.Value)}.lua"); try { var document = ClassBlocksStore.Load(classPath); foreach (var spell in document.SpellsList .Where(spell => spell.SpellId > 0 && !string.IsNullOrWhiteSpace(spell.Name)) .OrderBy(spell => spell.Index) .ThenBy(spell => spell.SpellId)) { var name = spell.Name.Trim(); if (_currentClassConditionSpells.All(item => item.SpellId != spell.SpellId)) { _currentClassConditionSpells.Add(new ConditionSpell(spell.SpellId, spell.Index, name)); } if (!_currentClassSpellIdsByName.TryGetValue(name, out var spellIds)) { spellIds = []; _currentClassSpellIdsByName[name] = spellIds; } if (!spellIds.Contains(spell.SpellId)) { spellIds.Add(spell.SpellId); } } foreach (var item in document.ItemsList .Where(item => item.ItemId > 0 && !string.IsNullOrWhiteSpace(item.Name)) .OrderBy(item => item.Index) .ThenBy(item => item.ItemId)) { var name = item.Name.Trim(); SpellIconCatalog.RegisterItem(item.ItemId, name); if (_currentClassConditionItems.All(entry => entry.ItemId != item.ItemId)) { _currentClassConditionItems.Add(new ConditionItem(item.ItemId, item.Index, name)); } if (!_currentSpecItemIdsByName.TryGetValue(name, out var classItemIds)) { classItemIds = []; _currentSpecItemIdsByName[name] = classItemIds; } if (!classItemIds.Contains(item.ItemId)) { classItemIds.Add(item.ItemId); } } var specId = ReadMatchCombo(_specBox); if (specId is not null && document.Specs.TryGetValue(specId.Value, out var spec)) { foreach (var item in spec.Items) { if (item.ItemId is not { } itemId || itemId <= 0 || string.IsNullOrWhiteSpace(item.Name)) { continue; } var name = item.Name.Trim(); SpellIconCatalog.RegisterItem(itemId, name); if (!_currentSpecItemIdsByName.TryGetValue(name, out var itemIds)) { itemIds = []; _currentSpecItemIdsByName[name] = itemIds; } if (!itemIds.Contains(itemId)) { itemIds.Add(itemId); } } } } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException or ArgumentException) { // 当前职业文件缺失或暂时不可读时,继续使用全局名称匹配。 } } private Image? GetRuleSpellIcon(string? spellName) { var normalized = spellName?.Trim(); if (string.IsNullOrWhiteSpace(normalized)) { return null; } if (_currentSpecItemIdsByName.TryGetValue(normalized, out var itemIds)) { foreach (var itemId in itemIds) { var icon = SpellIconCatalog.GetItem(itemId); if (icon is not null) { return icon; } } } if (SpellIconCatalog.TryParseItemReference(normalized, out var explicitItemId)) { var icon = SpellIconCatalog.GetItem(explicitItemId); if (icon is not null) { return icon; } } var officialItemIcon = SpellIconCatalog.GetItem(normalized); if (officialItemIcon is not null) { return officialItemIcon; } if (_currentClassSpellIdsByName.TryGetValue(normalized, out var spellIds)) { foreach (var spellId in spellIds) { var icon = SpellIconCatalog.Get(spellId); if (icon is not null) { return icon; } } } return SpellIconCatalog.Get(normalized); } private void RefreshRuleSpellIcons() { foreach (DataGridViewRow row in _rulesGrid.Rows) { if (!row.IsNewRow) { row.Cells["SpellIcon"].Value = GetRuleSpellIcon(CellText(row, "Spell")); } } } private static void EnsureComboItem(DataGridViewComboBoxColumn column, object? value) { var text = value?.ToString(); if (!string.IsNullOrEmpty(text) && !column.Items.Contains(text)) { column.Items.Add(text); } } private void RefreshAdjustmentFieldColumn() { foreach (DataGridViewRow row in _adjustmentsGrid.Rows) { if (!row.IsNewRow) { // 字段集合可能因职业/专精/动态单位变化, 按该行"类型"重新校验现值。 RebuildAdjustmentFieldCell(row, row.Cells["Field"].Value?.ToString(), keepCustom: true); } } // 动态数值也是可用于条件的字段;新增、改名或删除后立即刷新规则行的缺失字段提示。 InvalidateConditionFieldValidation(); RefreshAdjustmentValidation(); } private void RefreshAdjustmentValidation() { var structuredFields = BuildStructuredFieldSet(); foreach (DataGridViewRow row in _adjustmentsGrid.Rows) { if (row.IsNewRow) { continue; } var messages = new List(); AddMissingStructuredField(CellText(row, "Field"), "调整目标", structuredFields, messages); AddMissingConditionReferences(CellText(row, "Condition"), structuredFields, messages); ApplyAdjustmentValidationStyle(row, messages); } foreach (DataGridViewRow row in _formulaAdjustmentsGrid.Rows) { if (row.IsNewRow) { continue; } var messages = new List(); foreach (Match match in Regex.Matches( FormulaEvaluator.NormalizeExpression(CellText(row, "Formula")), @"\b(?:auras|aura|spells|spell)\.[_$\p{L}\p{N}.]+", RegexOptions.IgnoreCase)) { AddMissingStructuredField(match.Value, "公式字段", structuredFields, messages); } ApplyAdjustmentValidationStyle(row, messages); } } private HashSet BuildStructuredFieldSet() { var classId = ReadMatchCombo(_classBox); var specId = ReadMatchCombo(_specBox); var fields = new HashSet( _fieldCatalog.GetFields(classId, specId) .Where(field => field.Category is ConditionFieldCategory.Aura or ConditionFieldCategory.Spell) .Select(field => NormalizeConditionFieldName(field.Name)), StringComparer.Ordinal); fields.UnionWith(_fieldCatalog.GetAuraAliasFieldNames(classId, specId, groupOnly: false)); var groupFields = _fieldCatalog.GetGroupFields(classId, specId) .Where(field => SpellFieldKey.TryParseAuraMember(field.Name, out _, out _)) .Select(field => field.Name) .ToList(); groupFields.AddRange(_fieldCatalog.GetAuraAliasFieldNames(classId, specId, groupOnly: true)); fields.UnionWith(groupFields); foreach (var unit in _units.Where(unit => !string.IsNullOrWhiteSpace(unit.Name))) { fields.UnionWith(groupFields.Select(field => $"{unit.Name}.{field}")); } return fields; } private void AddMissingConditionReferences( string expression, IReadOnlySet structuredFields, ICollection messages) { var availableSpellIds = _currentClassConditionSpells.Select(spell => spell.SpellId).ToHashSet(); foreach (var term in ConditionExpression.Parse(expression)) { if (SpellIdConditionFields.Contains(term.Field)) { if (!long.TryParse(term.Value.Trim(), out var spellId) || spellId <= 0 || !availableSpellIds.Contains(spellId)) { AddUnique(messages, $"{term.Field}不存在 spellId 为 {term.Value.Trim()} 的法术"); } continue; } if (ItemIdConditionFields.Contains(term.Field)) { if (!long.TryParse(term.Value.Trim(), out var itemId) || itemId <= 0 || !_currentClassConditionItems.Any(item => item.ItemId == itemId)) { AddUnique(messages, $"{term.Field}不存在 itemId 为 {term.Value.Trim()} 的物品"); } continue; } AddMissingStructuredField(term.Field, "条件字段", structuredFields, messages); } } private static void AddMissingStructuredField( string field, string source, IReadOnlySet available, ICollection messages) { var normalized = NormalizeConditionFieldName(field); var isStructured = normalized.StartsWith("auras.", StringComparison.Ordinal) || normalized.StartsWith("spells.", StringComparison.Ordinal) || normalized.Contains(".auras.", StringComparison.Ordinal); if (!isStructured || available.Contains(normalized)) { return; } var spellId = normalized.Split('.', StringSplitOptions.RemoveEmptyEntries) .FirstOrDefault(part => long.TryParse(part, out var id) && id > 0); AddUnique(messages, spellId is null ? $"{source}不存在:{field}" : $"{source}不存在 spellId 为 {spellId} 的字段:{field}"); } private static void AddUnique(ICollection values, string value) { if (!values.Contains(value)) { values.Add(value); } } private static void ApplyAdjustmentValidationStyle(DataGridViewRow row, IReadOnlyCollection messages) { var text = string.Join('\n', messages); row.ErrorText = text; row.DefaultCellStyle.BackColor = messages.Count == 0 ? Color.Empty : UiTheme.DangerSoft; row.DefaultCellStyle.ForeColor = messages.Count == 0 ? Color.Empty : UiTheme.Danger; row.DefaultCellStyle.SelectionBackColor = messages.Count == 0 ? Color.Empty : UiTheme.Danger; row.DefaultCellStyle.SelectionForeColor = messages.Count == 0 ? Color.Empty : UiTheme.Background; foreach (DataGridViewCell cell in row.Cells) { cell.ToolTipText = text; } } // 按该行选中的"类型"重建"数值"单元格的可选项 = 该类别下的字段。 // desiredValue 为 null 时取单元格现值; 命中过滤后选项则保留, 否则: keepCustom 时补录为自定义项(载入旧数据), 反之清空(用户切换类型)。 private void RebuildAdjustmentFieldCell(DataGridViewRow row, string? desiredValue, bool keepCustom) { if (row.IsNewRow) { return; } var cell = row.Cells["Field"]; desiredValue ??= cell.Value?.ToString(); var category = ReadAdjustmentType(row); var isAvailable = !string.IsNullOrEmpty(desiredValue) && BuildAdjustmentFields().Any(field => (category is null || field.Category == category) && string.Equals(field.Name, desiredValue, StringComparison.Ordinal)); if (string.IsNullOrEmpty(desiredValue) || isAvailable || keepCustom) { cell.Value = desiredValue ?? string.Empty; } else { cell.Value = string.Empty; } } // 载入旧数据时: 由字段名推断类别, 写入"类型"单元格并重建"数值"选项(保留原值, 含自定义)。 private void ApplyAdjustmentRowType(DataGridViewRow row, string field) { _suppressAdjustmentTypeChange = true; try { row.Cells["Type"].Value = AdjustmentTypeText(ResolveAdjustmentCategory(field)); } finally { _suppressAdjustmentTypeChange = false; } RebuildAdjustmentFieldCell(row, field, keepCustom: true); } private static ConditionFieldCategory? ReadAdjustmentType(DataGridViewRow row) { var text = CellText(row, "Type"); foreach (var option in AdjustmentTypeOptions) { if (string.Equals(option.Text, text, StringComparison.Ordinal)) { return option.Category; } } // 未选类型 = 不过滤, 显示全部字段。 return null; } private static string AdjustmentTypeText(ConditionFieldCategory category) { foreach (var option in AdjustmentTypeOptions) { if (option.Category == category) { return option.Text; } } return AdjustmentTypeOptions[0].Text; } // 优先按目录里的字段类别判定; 目录外的自定义字段按 auras./spells. 前缀兜底, 其余归为动态数值。 private ConditionFieldCategory ResolveAdjustmentCategory(string field) { var name = field?.Trim() ?? string.Empty; if (name.Length == 0) { return ConditionFieldCategory.State; } var match = BuildAdjustmentFields() .FirstOrDefault(candidate => string.Equals(candidate.Name, name, StringComparison.Ordinal)); if (match is not null) { return match.Category; } if (name.StartsWith("auras.", StringComparison.OrdinalIgnoreCase) || name.StartsWith("aura.", StringComparison.OrdinalIgnoreCase)) { return ConditionFieldCategory.Aura; } if (name.StartsWith("spells.", StringComparison.OrdinalIgnoreCase) || name.StartsWith("spell.", StringComparison.OrdinalIgnoreCase)) { return ConditionFieldCategory.Spell; } return ConditionFieldCategory.DynamicValue; } private void OnAdjustmentsGridCellValueChanged(object? sender, DataGridViewCellEventArgs e) { if (_suppressAdjustmentTypeChange || e.RowIndex < 0 || e.ColumnIndex < 0) { return; } // 用户切换"类型": 重建"数值"选项, 仅保留仍属于该类型的现值, 否则清空让其重选。 if (_adjustmentsGrid.Columns[e.ColumnIndex].Name == "Type") { RebuildAdjustmentFieldCell(_adjustmentsGrid.Rows[e.RowIndex], null, keepCustom: false); } if (_adjustmentsGrid.Columns[e.ColumnIndex].Name == "Field") { InvalidateConditionFieldValidation(); } RefreshAdjustmentValidation(); } private void OnAdjustmentsGridCellFormatting(object? sender, DataGridViewCellFormattingEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0 || _adjustmentsGrid.Rows[e.RowIndex].IsNewRow) { return; } var columnName = _adjustmentsGrid.Columns[e.ColumnIndex].Name; if (columnName == "Field") { // 调整目标只显示字段名称;单元格底层继续保存结构化 spellId 键。 e.Value = FormatAdjustmentFieldForDisplay(e.Value?.ToString()); e.FormattingApplied = true; } else if (columnName == "Condition") { // 动态数值条件沿用规则表的名称化显示;单元格底层仍保留 spellId 表达式供保存和运行。 e.Value = FormatConditionExpressionForDisplay(e.Value?.ToString()); e.FormattingApplied = true; } } private string FormatAdjustmentFieldForDisplay(string? field) { var source = field?.Trim() ?? string.Empty; var normalized = NormalizeConditionFieldName(source); var isSpellReference = SpellFieldKey.TryParseSpell(normalized, out var spellId, out _); var isAuraReference = !isSpellReference && (SpellFieldKey.TryParseAura(normalized, out _, out spellId, out _) || SpellFieldKey.TryParseAuraMember(normalized, out spellId, out _)); if (!isSpellReference && !isAuraReference) { return source; } return ResolveConditionFieldDisplayName(normalized, spellId) ?? ResolveConditionSpellName(spellId, normalized) ?? source; } private IReadOnlyList BuildAdjustmentFields() { var fields = new List(); var seen = new HashSet(StringComparer.Ordinal); foreach (var field in _fieldCatalog.GetFields(ReadMatchCombo(_classBox), ReadMatchCombo(_specBox))) { // 状态仅取可加减的整数字段; 技能/光环原样收录, 供"类型"筛选。 if (field.Category == ConditionFieldCategory.State && field.Type == ConditionFieldType.Int) { AddAdjustmentField(fields, seen, field.Name, field.DisplayName, ConditionFieldCategory.State); } else if (field.Category == ConditionFieldCategory.Spell && string.Equals( field.Classification, CooldownConditionClassifications.Item, StringComparison.Ordinal) && field.Type == ConditionFieldType.Int) { AddAdjustmentField(fields, seen, field.Name, field.DisplayName, ConditionFieldCategory.State); } else if (field.Category is ConditionFieldCategory.Spell or ConditionFieldCategory.Aura) { AddAdjustmentField(fields, seen, field.Name, field.DisplayName, field.Category); } } // 已定义的动态单位生命值和数量拥有明确类别,必须先于动态数值目标加入; // 否则同名目标会被 seen 抢先登记成错误类别,载入时无法正确回填“类型”。 foreach (var unit in _units) { if (!string.IsNullOrWhiteSpace(unit.HealthName)) { AddAdjustmentField(fields, seen, unit.HealthName, $"{unit.HealthName} (生命值)", ConditionFieldCategory.DynamicUnit); } } foreach (var count in _counts) { if (!string.IsNullOrWhiteSpace(count.Name)) { AddAdjustmentField(fields, seen, count.Name, $"人数: {count.Name}", ConditionFieldCategory.DynamicValue); } } // 公式结果和其它不属于状态/技能/光环/动态单位的命名目标都是动态数值。 foreach (var fieldName in GetAdjustmentTargetFields()) { AddAdjustmentField(fields, seen, fieldName, $"{fieldName} (动态数值)", ConditionFieldCategory.DynamicValue); } return fields; } private IEnumerable GetAdjustmentTargetFields() { var seen = new HashSet(StringComparer.Ordinal); foreach (DataGridViewRow row in _adjustmentsGrid.Rows) { if (row.IsNewRow) { continue; } if (TryGetAdjustmentField(CellText(row, "Field"), null, out var field) && seen.Add(field)) { yield return field; } } foreach (DataGridViewRow row in _formulaAdjustmentsGrid.Rows) { if (row.IsNewRow) { continue; } if (TryGetAdjustmentField(CellText(row, "Field"), CellText(row, "Formula"), out var field) && seen.Add(field)) { yield return field; } } } private static bool TryGetAdjustmentField(string? fieldText, string? formulaText, out string field) { field = fieldText?.Trim() ?? string.Empty; if (field.Length > 0) { return true; } return FormulaEvaluator.TrySplitAssignment(formulaText, out field, out _); } private static void AddAdjustmentField( List fields, HashSet seen, string name, string displayName, ConditionFieldCategory category) { if (string.IsNullOrWhiteSpace(name) || !seen.Add(name)) { return; } fields.Add(new ConditionField(name, displayName, ConditionFieldType.Int, category)); } private void AddUnit() { using var editor = new UnitEditorForm(GetAuraFields(), GetThresholdFields(), CollectTakenNames(), null, null); if (editor.ShowDialog(FindForm()) != DialogResult.OK) { return; } if (editor.ResultUnit is { } unit) { _units.Add(unit); } else if (editor.ResultCount is { } count) { _counts.Add(count); } RefreshUnitsList(); RefreshUnitDependentUi(); RefreshAdjustmentFieldColumn(); } private void EditSelectedUnit() { var (kind, index) = GetSelectedUnitRef(); if (kind == UnitRowKind.None) { return; } var existingUnit = kind == UnitRowKind.Unit ? _units[index] : null; var existingCount = kind == UnitRowKind.Count ? _counts[index] : null; var ownName = existingUnit?.Name ?? existingCount?.Name; var ownHealthName = existingUnit?.HealthName; using var editor = new UnitEditorForm(GetAuraFields(), GetThresholdFields(), CollectTakenNames(ownName, ownHealthName), existingUnit, existingCount); if (editor.ShowDialog(FindForm()) != DialogResult.OK) { return; } // 类别可能在编辑中改变(单位↔数量), 先移除原项再按结果加入。 if (kind == UnitRowKind.Unit) { _units.RemoveAt(index); } else { _counts.RemoveAt(index); } if (editor.ResultUnit is { } unit) { _units.Add(unit); } else if (editor.ResultCount is { } count) { _counts.Add(count); } RefreshUnitsList(); RefreshUnitDependentUi(); RefreshAdjustmentFieldColumn(); } private void DeleteSelectedUnit() { var (kind, index) = GetSelectedUnitRef(); if (kind == UnitRowKind.None) { return; } if (kind == UnitRowKind.Unit) { _units.RemoveAt(index); } else { _counts.RemoveAt(index); } RefreshUnitsList(); RefreshUnitDependentUi(); RefreshAdjustmentFieldColumn(); } // ListView 行顺序: 先全部单位, 再全部数量。把选中行映射回对应列表索引。 private (UnitRowKind Kind, int Index) GetSelectedUnitRef() { if (_unitsList.SelectedIndices.Count == 0) { return (UnitRowKind.None, -1); } var row = _unitsList.SelectedIndices[0]; if (row < _units.Count) { return (UnitRowKind.Unit, row); } var countIndex = row - _units.Count; return countIndex < _counts.Count ? (UnitRowKind.Count, countIndex) : (UnitRowKind.None, -1); } private void RefreshUnitsList() { var availableAuraIds = GetAuraFields() .SelectMany(field => field.Name.Split('.', StringSplitOptions.RemoveEmptyEntries)) .Where(part => long.TryParse(part, out _)) .Select(long.Parse) .ToHashSet(); _unitsList.BeginUpdate(); _unitsList.Items.Clear(); foreach (var unit in _units) { var name = string.IsNullOrWhiteSpace(unit.HealthName) ? unit.Name : $"{unit.Name} / {unit.HealthName}"; var summary = UnitSummary.Describe(unit, ResolveGroupAuraName); var item = new ListViewItem([name, "单位", summary]) { ToolTipText = $"{name}\n{summary}" }; var missing = (unit.AuraSpellIds ?? []).Where(id => !availableAuraIds.Contains(id)).ToArray(); if (unit.AuraNames is { Count: > 0 }) { item.BackColor = UiTheme.DangerSoft; item.ForeColor = UiTheme.Danger; item.ToolTipText += $"\n旧名称光环引用尚未转换:{string.Join("、", unit.AuraNames)}"; } else if (missing.Length > 0) { item.BackColor = UiTheme.DangerSoft; item.ForeColor = UiTheme.Danger; item.ToolTipText += $"\n队伍不存在 spellId 为 {string.Join("、", missing)} 的光环"; } _unitsList.Items.Add(item); } foreach (var count in _counts) { var summary = UnitSummary.Describe(count, ResolveGroupAuraName); var item = new ListViewItem([count.Name, "数量", summary]) { ToolTipText = $"{count.Name}\n{summary}" }; if (!string.IsNullOrWhiteSpace(count.AuraName)) { item.BackColor = UiTheme.DangerSoft; item.ForeColor = UiTheme.Danger; item.ToolTipText += $"\n旧名称光环引用尚未转换:{count.AuraName}"; } else if (count.AuraSpellId is { } id && !availableAuraIds.Contains(id)) { item.BackColor = UiTheme.DangerSoft; item.ForeColor = UiTheme.Danger; item.ToolTipText += $"\n队伍不存在 spellId 为 {id} 的光环"; } _unitsList.Items.Add(item); } _unitsList.EndUpdate(); _unitsEmptyHint.Visible = _unitsList.Items.Count == 0; } // 单位/数量增删改后, 刷新各规则行"目标"下拉以反映最新的动态单位名。 private void RefreshUnitDependentUi() { foreach (DataGridViewRow row in _rulesGrid.Rows) { if (!row.IsNewRow) { UpdateUnitCellItems(row); } } // 动态单位名、生命值名和数量名都会参与条件字段校验。 InvalidateConditionFieldValidation(); } private IReadOnlyList GetAuraFields() { return _fieldCatalog .GetGroupFields(ReadMatchCombo(_classBox), ReadMatchCombo(_specBox)) .Where(field => !NonAuraGroupFields.Contains(field.Name)) .ToList(); } private string? ResolveGroupAuraName(long spellId) { foreach (var field in GetAuraFields()) { if (field.Name.Split('.', StringSplitOptions.RemoveEmptyEntries) .Any(part => long.TryParse(part, out var id) && id == spellId)) { return field.DisplayName.Split(" / ", 2, StringSplitOptions.TrimEntries)[0]; } } return null; } private IReadOnlyList GetThresholdFields() { // 阈值字段仅取状态/动态单位/动态数值(可加减数值), 排除技能/光环字段。 return BuildAdjustmentFields() .Where(field => field.Category is ConditionFieldCategory.State or ConditionFieldCategory.DynamicUnit or ConditionFieldCategory.DynamicValue) .Select(field => field.Name) .Where(name => !string.IsNullOrWhiteSpace(name)) .ToList(); } // 名称查重集合: 其它单位/数量(含生命值名) + 当前职业/专精的状态字段与 group 字段; 排除正在编辑项自身的名称。 private IReadOnlyCollection CollectTakenNames(params string?[] ownNames) { var classId = ReadMatchCombo(_classBox); var specId = ReadMatchCombo(_specBox); var taken = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var unit in _units) { taken.Add(unit.Name); if (!string.IsNullOrWhiteSpace(unit.HealthName)) { taken.Add(unit.HealthName); } } foreach (var count in _counts) { taken.Add(count.Name); } foreach (var field in _fieldCatalog.GetFields(classId, specId)) { taken.Add(field.Name); } foreach (var field in _fieldCatalog.GetGroupFields(classId, specId)) { taken.Add(field.Name); } foreach (var ownName in ownNames) { if (!string.IsNullOrEmpty(ownName)) { taken.Remove(ownName); } } return taken; } private void OnUnitsListKeyDown(object? sender, KeyEventArgs e) { if (e.KeyCode == Keys.Enter) { EditSelectedUnit(); e.Handled = true; e.SuppressKeyPress = true; } else if (e.KeyCode == Keys.Delete) { DeleteSelectedUnit(); e.Handled = true; } } private enum UnitRowKind { None, Unit, Count } private sealed record RuleRowValues( bool Enabled, string Spell, string UnitText, string MacroCondition, string Condition, string Comment, IReadOnlyList SubConditions, int? DelayMs, int? LogicDelayMs); private sealed class RuleRowMetadata( IEnumerable? subConditions = null, int? delayMs = null, int? logicDelayMs = null) { public List SubConditions { get; } = subConditions?.ToList() ?? new List(); public int? DelayMs { get; set; } = delayMs; public int? LogicDelayMs { get; set; } = logicDelayMs; } private void OnRulesGridCellClick(object? sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) { return; } var columnName = _rulesGrid.Columns[e.ColumnIndex].Name; if (_rulesGrid.Rows[e.RowIndex].Cells[e.ColumnIndex] is DataGridViewComboBoxCell) { ShowRulesComboDropDown(e.RowIndex, e.ColumnIndex); return; } if (columnName == "MoveUp") { MoveRule(e.RowIndex, -1); return; } if (columnName == "MoveDown") { MoveRule(e.RowIndex, 1); return; } if (columnName == "Copy") { CopyRule(e.RowIndex); return; } if (columnName == "InsertBlank") { InsertBlankRule(e.RowIndex); return; } if (columnName == "Delete") { DeleteRule(e.RowIndex); return; } if (columnName == "Condition") { OpenConditionEditor(e.RowIndex); return; } if (columnName == RuleCommentColumnName && !_rulesGrid.Rows[e.RowIndex].IsNewRow) { OpenRuleTextEditor(e.RowIndex); } } // 不进入 WinForms 原生 ComboBox 编辑态,直接显示受控的深色列表,避免白边、尺寸跳变和按钮错位。 private void ShowRulesComboDropDown(int rowIndex, int columnIndex) { CloseRulesComboDropDown(); var row = _rulesGrid.Rows[rowIndex]; if (row.IsNewRow) { rowIndex = _rulesGrid.Rows.Add(true, null!, string.Empty, string.Empty, string.Empty, string.Empty); row = _rulesGrid.Rows[rowIndex]; } if (row.Cells[columnIndex] is not DataGridViewComboBoxCell cell) { return; } _rulesGrid.CurrentCell = cell; var values = cell.Items.Cast() .Select(item => item?.ToString() ?? string.Empty) .Distinct(StringComparer.Ordinal) .ToList(); if (values.Count == 0 && cell.OwningColumn is DataGridViewComboBoxColumn column) { values.AddRange(column.Items.Cast() .Select(item => item?.ToString() ?? string.Empty) .Distinct(StringComparer.Ordinal)); } var currentValue = cell.Value?.ToString() ?? string.Empty; if (!values.Contains(currentValue, StringComparer.Ordinal)) { values.Insert(0, currentValue); } var cellBounds = _rulesGrid.GetCellDisplayRectangle(columnIndex, rowIndex, cutOverflow: true); var options = values .Select(value => new UiDropDownOption(value, value)) .ToList(); ToolStripDropDown? dropDown = null; dropDown = UiDropDownPopup.Show( _rulesGrid, cellBounds, options, currentValue, selected => { cell.Value = selected.Value?.ToString() ?? string.Empty; _rulesGrid.InvalidateCell(cell); }, minimumWidth: 150, closed: () => { if (ReferenceEquals(_rulesComboDropDown, dropDown)) { _rulesComboDropDown = null; } }); _rulesComboDropDown = dropDown; } private void CloseRulesComboDropDown() { _rulesComboDropDown?.Close(ToolStripDropDownCloseReason.AppClicked); _rulesComboDropDown = null; } private void OnRulesGridCellFormatting(object? sender, DataGridViewCellFormattingEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) { return; } var columnName = _rulesGrid.Columns[e.ColumnIndex].Name; var row = _rulesGrid.Rows[e.RowIndex]; if (columnName == "SpellIcon" && row.IsNewRow) { e.Value = SpellIconCatalog.GetLastRuleRowIcon(); e.FormattingApplied = true; return; } if (!row.IsNewRow && (GetMissingConditionFields(row).Count > 0 || GetMissingConditionSpells(row).Count > 0 || GetMissingConditionItems(row).Count > 0)) { // 缺失字段或 spellId 会让条件不命中;用整行红色状态提醒用户修复配置。 e.CellStyle.BackColor = UiTheme.DangerSoft; e.CellStyle.ForeColor = UiTheme.Danger; e.CellStyle.SelectionBackColor = UiTheme.Danger; e.CellStyle.SelectionForeColor = UiTheme.Background; } if (columnName == "RuleNumber") { e.Value = row.IsNewRow ? string.Empty : (e.RowIndex + 1).ToString(); e.FormattingApplied = true; return; } // 「条件」列在有子条件时显示成 "主条件 且任一(子1 | 子2)"; 仅改显示, 底层值仍是主条件, 不影响 ReadRules 存盘。 if (columnName == "Condition" && !row.IsNewRow) { var metadata = GetRuleMetadata(row); e.Value = DecorateCondition( e.Value?.ToString() ?? string.Empty, metadata.SubConditions, metadata.DelayMs, metadata.LogicDelayMs); e.FormattingApplied = true; } } // 返回主条件和所有子条件中不属于当前职业/专精、动态单位或动态数值目录的字段。 private IReadOnlyList GetMissingConditionFields(DataGridViewRow row) { EnsureConditionFieldValidationCatalog(); var available = _availableConditionFields!; var groupFields = _availableGroupConditionFields!; var missing = new List(); var seen = new HashSet(StringComparer.Ordinal); var metadata = GetRuleMetadata(row); foreach (var expression in new[] { CellText(row, "Condition") }.Concat(metadata.SubConditions)) { foreach (var term in ConditionExpression.Parse(expression)) { var original = term.Field.Trim(); var normalized = NormalizeConditionFieldName(original); if (normalized.Length == 0 || available.Contains(normalized)) { continue; } // group.<槽位>.<字段> 是运行时支持的直接队伍引用;只需确认末段字段存在。 var groupParts = normalized.Split('.', 3); if (groupParts.Length == 3 && string.Equals(groupParts[0], "group", StringComparison.OrdinalIgnoreCase) && groupFields.Contains(groupParts[2])) { continue; } if (seen.Add(original)) { missing.Add(original); } } } return missing; } private IReadOnlyList GetMissingConditionSpells(DataGridViewRow row) { var availableSpellIds = _currentClassConditionSpells .Select(spell => spell.SpellId) .ToHashSet(); var missing = new List(); var seen = new HashSet(StringComparer.Ordinal); var metadata = GetRuleMetadata(row); foreach (var expression in new[] { CellText(row, "Condition") }.Concat(metadata.SubConditions)) { foreach (var term in ConditionExpression.Parse(expression)) { if (!SpellIdConditionFields.Contains(term.Field)) { continue; } var value = term.Value.Trim(); if (long.TryParse(value, out var spellId) && spellId > 0 && availableSpellIds.Contains(spellId)) { continue; } var message = $"{term.Field}不存在 spellId 为 {value} 的法术"; if (seen.Add(message)) { missing.Add(message); } } } return missing; } private IReadOnlyList GetMissingConditionItems(DataGridViewRow row) { var availableItemIds = _currentClassConditionItems .Select(item => item.ItemId) .ToHashSet(); var missing = new List(); var seen = new HashSet(StringComparer.Ordinal); var metadata = GetRuleMetadata(row); foreach (var expression in new[] { CellText(row, "Condition") }.Concat(metadata.SubConditions)) { foreach (var term in ConditionExpression.Parse(expression)) { if (!ItemIdConditionFields.Contains(term.Field)) { continue; } var value = term.Value.Trim(); if (long.TryParse(value, out var itemId) && itemId > 0 && availableItemIds.Contains(itemId)) { continue; } var message = $"{term.Field}不存在 itemId 为 {value} 的物品"; if (seen.Add(message)) { missing.Add(message); } } } return missing; } // 字段目录的构造会读取职业配置和动态数值表;缓存后避免每个可见单元格重复做同一份工作。 private void EnsureConditionFieldValidationCatalog() { if (_availableConditionFields is not null && _availableGroupConditionFields is not null && _conditionFieldDisplayNames is not null) { return; } var conditionFields = BuildConditionFields(includeRuleSettings: true); _availableConditionFields = new HashSet( conditionFields .Select(field => NormalizeConditionFieldName(field.Name)), StringComparer.Ordinal); _conditionFieldDisplayNames = conditionFields .GroupBy(field => NormalizeConditionFieldName(field.Name), StringComparer.Ordinal) .ToDictionary(group => group.Key, group => group.First().DisplayName, StringComparer.Ordinal); var classId = ReadMatchCombo(_classBox); var specId = ReadMatchCombo(_specBox); _availableGroupConditionFields = new HashSet( _fieldCatalog.GetGroupFields(classId, specId).Select(field => field.Name), StringComparer.Ordinal); _availableConditionFields.UnionWith( _fieldCatalog.GetAuraAliasFieldNames(classId, specId, groupOnly: false)); _availableGroupConditionFields.UnionWith( _fieldCatalog.GetAuraAliasFieldNames(classId, specId, groupOnly: true)); // 运行时也支持“动态单位名.队伍字段”;编辑器目录未展开这些组合,这里仍应判定为有效。 foreach (var unit in _units.Where(unit => !string.IsNullOrWhiteSpace(unit.Name))) { foreach (var groupField in _availableGroupConditionFields) { _availableConditionFields.Add($"{unit.Name}.{groupField}"); } } } private void InvalidateConditionFieldValidation() { _availableConditionFields = null; _availableGroupConditionFields = null; _conditionFieldDisplayNames = null; _rulesGrid.Invalidate(); } // 运行时允许 state./spell./aura. 别名且前缀不区分大小写;校验时归一化为目录使用的名称。 private static string NormalizeConditionFieldName(string? fieldName) { var name = fieldName?.Trim() ?? string.Empty; if (name.StartsWith("state.", StringComparison.OrdinalIgnoreCase)) { return name["state.".Length..]; } if (name.StartsWith("spells.", StringComparison.OrdinalIgnoreCase)) { return $"spells.{name["spells.".Length..]}"; } if (name.StartsWith("spell.", StringComparison.OrdinalIgnoreCase)) { return $"spells.{name["spell.".Length..]}"; } if (name.StartsWith("auras.", StringComparison.OrdinalIgnoreCase)) { return $"auras.{name["auras.".Length..]}"; } if (name.StartsWith("aura.", StringComparison.OrdinalIgnoreCase)) { return $"auras.{name["aura.".Length..]}"; } return name; } // 把主条件、子条件和规则延迟合成可读文本;仅改显示,不改变底层条件表达式。 private string DecorateCondition( string main, IReadOnlyList? subs, int? delayMs, int? logicDelayMs) { var conditionText = FormatConditionExpressionForDisplay(main); if (subs is { Count: > 0 }) { var any = string.Join(" | ", subs.Select(FormatConditionExpressionForDisplay)); conditionText = conditionText.Length == 0 ? $"任一({any})" : $"{conditionText} 且任一({any})"; } if (delayMs is > 0) { conditionText = conditionText.Length == 0 ? $"延迟 {delayMs.Value} ms" : $"{conditionText};延迟 {delayMs.Value} ms"; } if (logicDelayMs is > 0) { conditionText = conditionText.Length == 0 ? $"逻辑延迟 {logicDelayMs.Value} ms" : $"{conditionText};逻辑延迟 {logicDelayMs.Value} ms"; } return conditionText; } private string FormatConditionExpressionForDisplay(string? expression) { var source = expression?.Trim() ?? string.Empty; if (source.Length == 0) { return string.Empty; } var terms = ConditionExpression.Parse(source); if (terms.Count == 0) { return source; } return ConditionExpression.Build(terms.Select(term => { var field = FormatConditionFieldForDisplay(term.Field); var value = SpellIdConditionFields.Contains(term.Field) ? FormatConditionSpellValueForDisplay(term.Value) : ItemIdConditionFields.Contains(term.Field) ? FormatConditionItemValueForDisplay(term.Value) : string.Equals(NormalizeConditionFieldName(term.Field), "首领战", StringComparison.Ordinal) ? FormatBossValueForDisplay(term.Value) : term.Value; return term with { Field = field, Value = value }; })); } private string FormatConditionFieldForDisplay(string field) { var normalized = NormalizeConditionFieldName(field); var isSpellReference = SpellFieldKey.TryParseSpell(normalized, out var spellId, out _); var isAuraReference = !isSpellReference && (SpellFieldKey.TryParseAura(normalized, out _, out spellId, out _) || SpellFieldKey.TryParseAuraMember(normalized, out spellId, out _)); if (!isSpellReference && !isAuraReference) { return field; } var fieldDisplayName = ResolveConditionFieldDisplayName(normalized, spellId); if (!string.IsNullOrWhiteSpace(fieldDisplayName)) { return $"{(isSpellReference ? "cd:" : "aura:")}{fieldDisplayName}"; } var name = ResolveConditionSpellName(spellId, normalized); if (string.IsNullOrWhiteSpace(name)) { return field; } return $"{(isSpellReference ? "cd:" : "aura:")}{name}"; } private string FormatConditionSpellValueForDisplay(string value) { var normalized = value.Trim(); if (!long.TryParse(normalized, NumberStyles.Integer, CultureInfo.InvariantCulture, out var spellId) || spellId <= 0) { return value; } var name = ResolveConditionSpellName(spellId, null); return string.IsNullOrWhiteSpace(name) ? value : name; } private string FormatConditionItemValueForDisplay(string value) { var normalized = value.Trim(); if (!long.TryParse(normalized, NumberStyles.Integer, CultureInfo.InvariantCulture, out var itemId) || itemId <= 0) { return value; } var name = _currentClassConditionItems.FirstOrDefault(item => item.ItemId == itemId)?.Name; return string.IsNullOrWhiteSpace(name) ? value : name; } private static string FormatBossValueForDisplay(string value) { var normalized = value.Trim(); if (!int.TryParse(normalized, NumberStyles.Integer, CultureInfo.InvariantCulture, out var bossNumber)) { return value; } if (bossNumber == 0) { return "非首领战"; } return StatusForm.GetBossNumberOptions() .FirstOrDefault(option => option.Number == bossNumber)?.Name ?? value; } private string? ResolveConditionSpellName(long spellId, string? normalizedField) { var localName = _currentClassConditionSpells .FirstOrDefault(spell => spell.SpellId == spellId)?.Name; if (!string.IsNullOrWhiteSpace(localName)) { return localName; } var catalogName = SpellIconCatalog.ResolveSuggestionName(spellId, null); if (!string.IsNullOrWhiteSpace(catalogName)) { return catalogName; } if (!string.IsNullOrWhiteSpace(normalizedField)) { return ResolveConditionFieldDisplayName(normalizedField, spellId); } return null; } private string? ResolveConditionFieldDisplayName(string normalizedField, long spellId) { EnsureConditionFieldValidationCatalog(); if (!_conditionFieldDisplayNames!.TryGetValue(normalizedField, out var displayName)) { return null; } var value = displayName; if (value.StartsWith("技能: ", StringComparison.Ordinal)) { value = value["技能: ".Length..]; } var idSuffix = $" / {spellId.ToString(CultureInfo.InvariantCulture)}"; if (value.EndsWith(idSuffix, StringComparison.Ordinal)) { value = value[..^idSuffix.Length]; } return value.Trim(); } private void OnRulesGridCellPainting(object? sender, DataGridViewCellPaintingEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) { return; } var columnName = _rulesGrid.Columns[e.ColumnIndex].Name; if (columnName is "Spell" or "Unit" or "MacroCondition") { UiTheme.PaintDataGridViewComboBoxCell(_rulesGrid, e); return; } if (columnName == "Drag") { PaintRuleDragHandle(e); return; } if (columnName == RuleCommentColumnName) { if (!_rulesGrid.Rows[e.RowIndex].IsNewRow) { PaintRuleCommentCell(e); } return; } if (columnName is not ("MoveUp" or "MoveDown" or "Copy" or "InsertBlank" or "Delete")) { return; } var icon = columnName switch { "MoveUp" => "▲", "MoveDown" => "▼", "Copy" => "⧉", "InsertBlank" => "+", _ => "×" }; var enabled = IsRuleIconEnabled(columnName, e.RowIndex); var color = columnName == "Delete" ? UiTheme.Danger : UiTheme.Muted; if (!enabled) { color = Color.FromArgb(70, color); } PaintGridIconCell(_rulesGrid, e, icon, color); } private void PaintRuleCommentCell(DataGridViewCellPaintingEventArgs e) { e.Paint(e.CellBounds, DataGridViewPaintParts.Background | DataGridViewPaintParts.Border); if (e.Graphics is null) { e.Handled = true; return; } var hasComment = !string.IsNullOrWhiteSpace(CellText(_rulesGrid.Rows[e.RowIndex], RuleCommentColumnName)); var color = hasComment ? UiTheme.Accent : UiTheme.Muted; var left = e.CellBounds.Left + (e.CellBounds.Width - 19f) / 2f; var top = e.CellBounds.Top + (e.CellBounds.Height - 18f) / 2f; var oldSmoothingMode = e.Graphics.SmoothingMode; e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; using var outlinePen = new Pen(color, 1.5f); using var detailPen = new Pen(color, 1.2f); e.Graphics.DrawRectangle(outlinePen, left + 4.5f, top + 1.5f, 13f, 15f); e.Graphics.DrawLine(detailPen, left + 7.5f, top + 2.5f, left + 7.5f, top + 15.5f); e.Graphics.DrawLine(outlinePen, left + 1.5f, top + 5f, left + 6f, top + 5f); e.Graphics.DrawLine(outlinePen, left + 1.5f, top + 9f, left + 6f, top + 9f); e.Graphics.DrawLine(outlinePen, left + 1.5f, top + 13f, left + 6f, top + 13f); e.Graphics.DrawLine(detailPen, left + 10f, top + 6f, left + 15f, top + 6f); e.Graphics.DrawLine(detailPen, left + 10f, top + 9.5f, left + 15f, top + 9.5f); e.Graphics.DrawLine(detailPen, left + 10f, top + 13f, left + 14f, top + 13f); e.Graphics.SmoothingMode = oldSmoothingMode; e.Handled = true; } private void OnRulesGridCellMouseEnter(object? sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) { return; } var columnName = _rulesGrid.Columns[e.ColumnIndex].Name; // 条件/注释列 → 手型; 拖拽手柄列 → 移动光标; 其它 → 默认。 var isExisting = !_rulesGrid.Rows[e.RowIndex].IsNewRow; _rulesGrid.Cursor = columnName switch { "Condition" or RuleCommentColumnName when isExisting => Cursors.Hand, "Drag" when isExisting => Cursors.SizeAll, _ => Cursors.Default }; var text = GetRuleCellToolTip(columnName, e.RowIndex, e.ColumnIndex); if (string.IsNullOrEmpty(text)) { _rulesGridToolTip.Hide(_rulesGrid); return; } var cellBounds = _rulesGrid.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, cutOverflow: true); _rulesGridToolTip.Show(text, _rulesGrid, cellBounds.Left + cellBounds.Width / 2, cellBounds.Bottom + 4); } private void OnRulesGridCellMouseLeave(object? sender, DataGridViewCellEventArgs e) { _rulesGrid.Cursor = Cursors.Default; _rulesGridToolTip.Hide(_rulesGrid); } // 图标列沿用原提示; 条件/技能/目标/宏条件列在文本被列宽截断或可点击时给出悬停提示。 private string GetRuleCellToolTip(string columnName, int rowIndex, int columnIndex) { if (rowIndex >= _rulesGrid.Rows.Count || _rulesGrid.Rows[rowIndex].IsNewRow) { return string.Empty; } if (columnName == RuleCommentColumnName) { var comment = CellText(_rulesGrid.Rows[rowIndex], RuleCommentColumnName); return comment.Length == 0 ? "点击编辑注释" : comment; } var missingFields = GetMissingConditionFields(_rulesGrid.Rows[rowIndex]); var missingSpells = GetMissingConditionSpells(_rulesGrid.Rows[rowIndex]); var missingItems = GetMissingConditionItems(_rulesGrid.Rows[rowIndex]); if (missingFields.Count > 0 || missingSpells.Count > 0 || missingItems.Count > 0) { var messages = new List(); if (missingFields.Count > 0) { messages.Add($"条件字段不存在:{string.Join("、", missingFields)}"); messages.Add("请先添加对应字段。"); } messages.AddRange(missingSpells); messages.AddRange(missingItems); return string.Join('\n', messages); } if (columnName is "MoveUp" or "MoveDown" or "Copy" or "InsertBlank" or "Delete") { return GetRuleIconToolTip(columnName, rowIndex); } if (columnName == "Drag") { return "拖动调整顺序"; } if (columnName is not ("Condition" or "Spell" or "Unit" or "MacroCondition")) { return string.Empty; } var row = _rulesGrid.Rows[rowIndex]; var text = CellText(row, columnName); if (columnName == "Condition") { // 提示与裁剪检测都用合成后的完整文本(含子条件和延迟), 与单元格显示一致。 var metadata = GetRuleMetadata(row); text = DecorateCondition( text, metadata.SubConditions, metadata.DelayMs, metadata.LogicDelayMs); if (text.Length == 0) { return "点击编辑条件 (当前: 始终命中)"; } } return IsCellTextClipped(text, columnIndex) ? text : string.Empty; } private bool IsCellTextClipped(string text, int columnIndex) { if (string.IsNullOrEmpty(text)) { return false; } var available = _rulesGrid.Columns[columnIndex].Width - 12; return TextRenderer.MeasureText(text, _rulesGrid.Font).Width > available; } private string GetRuleIconToolTip(string columnName, int rowIndex) { if (!IsRuleIconEnabled(columnName, rowIndex)) { return string.Empty; } return columnName switch { "MoveUp" => "上移", "MoveDown" => "下移", "Copy" => "复制到下一行", "InsertBlank" => "在下一行添加空白条件", "Delete" => "删除", _ => string.Empty }; } private void OnAdjustmentsGridCellPainting(object? sender, DataGridViewCellPaintingEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) { return; } var columnName = _adjustmentsGrid.Columns[e.ColumnIndex].Name; if (columnName is "Type" or "Field") { UiTheme.PaintDataGridViewComboBoxCell(_adjustmentsGrid, e); return; } if (columnName != "Delete") { return; } PaintGridIconCell(_adjustmentsGrid, e, "×", UiTheme.Danger); } private void OnFormulaAdjustmentsGridCellPainting(object? sender, DataGridViewCellPaintingEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) { return; } if (_formulaAdjustmentsGrid.Columns[e.ColumnIndex].Name != "Delete") { return; } PaintGridIconCell(_formulaAdjustmentsGrid, e, "×", UiTheme.Danger); } private static void PaintGridIconCell(DataGridView grid, DataGridViewCellPaintingEventArgs e, string icon, Color color) { e.Paint(e.CellBounds, DataGridViewPaintParts.Background | DataGridViewPaintParts.Border); if (e.Graphics is null) { e.Handled = true; return; } TextRenderer.DrawText( e.Graphics, icon, grid.Font, e.CellBounds, color, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); e.Handled = true; } // 自绘 2×3 六点抓手, 不依赖字体里是否有 grip 字形; 新行不画。 private void PaintRuleDragHandle(DataGridViewCellPaintingEventArgs e) { e.Paint(e.CellBounds, DataGridViewPaintParts.Background | DataGridViewPaintParts.Border); if (e.Graphics is null || e.RowIndex < 0 || _rulesGrid.Rows[e.RowIndex].IsNewRow) { e.Handled = true; return; } var cx = e.CellBounds.Left + e.CellBounds.Width / 2; var cy = e.CellBounds.Top + e.CellBounds.Height / 2; var color = _rulesGrid.Rows[e.RowIndex].Selected ? UiTheme.Text : UiTheme.Muted; using var brush = new SolidBrush(color); foreach (var x in new[] { cx - 4, cx }) { foreach (var y in new[] { cy - 7, cy - 1, cy + 5 }) { e.Graphics.FillEllipse(brush, x, y, 2, 2); } } e.Handled = true; } private bool IsRuleIconEnabled(string columnName, int rowIndex) { if (rowIndex < 0 || rowIndex >= _rulesGrid.Rows.Count || _rulesGrid.Rows[rowIndex].IsNewRow) { return false; } return columnName switch { "MoveUp" => rowIndex > 0, "MoveDown" => rowIndex < LastRuleRowIndex(), "Copy" => true, "InsertBlank" => true, "Delete" => true, _ => false }; } private void OnAdjustmentsGridCellClick(object? sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) { return; } var columnName = _adjustmentsGrid.Columns[e.ColumnIndex].Name; if (columnName is "Type" or "Field") { ShowAdjustmentComboDropDown(e.RowIndex, e.ColumnIndex); return; } CloseAdjustmentComboDropDown(); if (columnName == "Delete") { var row = _adjustmentsGrid.Rows[e.RowIndex]; if (!row.IsNewRow) { _adjustmentsGrid.Rows.RemoveAt(e.RowIndex); RefreshAdjustmentFieldColumn(); } return; } if (columnName == "Condition") { OpenAdjustmentConditionEditor(e.RowIndex); } } private void OnAdjustmentsGridKeyDown(object? sender, KeyEventArgs e) { var cell = _adjustmentsGrid.CurrentCell; if (cell is null || cell.OwningColumn?.Name is not ("Type" or "Field") || e.KeyCode is not (Keys.Enter or Keys.Space or Keys.F4 or Keys.Down)) { return; } if (e.KeyCode == Keys.Down && !e.Alt) { return; } e.Handled = true; e.SuppressKeyPress = true; ShowAdjustmentComboDropDown(cell.RowIndex, cell.ColumnIndex); } private void ShowAdjustmentComboDropDown(int rowIndex, int columnIndex) { CloseAdjustmentComboDropDown(); if (rowIndex < 0 || rowIndex >= _adjustmentsGrid.Rows.Count) { return; } var row = _adjustmentsGrid.Rows[rowIndex]; if (row.IsNewRow) { rowIndex = _adjustmentsGrid.Rows.Add(true, string.Empty, 0, string.Empty); row = _adjustmentsGrid.Rows[rowIndex]; } var cell = row.Cells[columnIndex]; var columnName = cell.OwningColumn?.Name; if (columnName is not ("Type" or "Field")) { return; } _adjustmentsGrid.CurrentCell = cell; var currentValue = cell.Value?.ToString() ?? string.Empty; List options; if (columnName == "Type") { options = AdjustmentTypeOptions .Select(option => new UiDropDownOption(option.Text, option.Text)) .ToList(); } else { var category = ReadAdjustmentType(row); var fields = BuildAdjustmentFields() .Where(field => category is null || field.Category == category) .GroupBy(field => field.Name, StringComparer.Ordinal) .Select(group => group.First()) .ToList(); options = fields .Select(field => new UiDropDownOption( field.Name, FormatAdjustmentFieldForDisplay(field.Name))) .Prepend(new UiDropDownOption(string.Empty, string.Empty)) .ToList(); if (!string.IsNullOrEmpty(currentValue) && !fields.Any(field => string.Equals(field.Name, currentValue, StringComparison.Ordinal))) { options.Insert(0, new UiDropDownOption( currentValue, FormatAdjustmentFieldForDisplay(currentValue))); } } var cellBounds = _adjustmentsGrid.GetCellDisplayRectangle(columnIndex, rowIndex, cutOverflow: true); ToolStripDropDown? dropDown = null; dropDown = UiDropDownPopup.Show( _adjustmentsGrid, cellBounds, options, currentValue, selected => { cell.Value = selected.Value?.ToString() ?? string.Empty; _adjustmentsGrid.InvalidateCell(cell); }, minimumWidth: columnName == "Type" ? 120 : 180, closed: () => { if (ReferenceEquals(_adjustmentComboDropDown, dropDown)) { _adjustmentComboDropDown = null; } }); _adjustmentComboDropDown = dropDown; } private void CloseAdjustmentComboDropDown() { var dropDown = _adjustmentComboDropDown; _adjustmentComboDropDown = null; dropDown?.Close(ToolStripDropDownCloseReason.AppClicked); } private void OnFormulaAdjustmentsGridCellClick(object? sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) { return; } var columnName = _formulaAdjustmentsGrid.Columns[e.ColumnIndex].Name; if (columnName == "Delete") { var row = _formulaAdjustmentsGrid.Rows[e.RowIndex]; if (!row.IsNewRow) { _formulaAdjustmentsGrid.Rows.RemoveAt(e.RowIndex); RefreshAdjustmentFieldColumn(); } return; } if (columnName == "Formula") { OpenFormulaEditor(e.RowIndex); } } private void OnFormulaAdjustmentsGridCellEndEdit(object? sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) { return; } if (_formulaAdjustmentsGrid.Columns[e.ColumnIndex].Name == "Field") { RefreshAdjustmentFieldColumn(); } } private void OnAdjustmentsGridEditingControlShowing(object? sender, DataGridViewEditingControlShowingEventArgs e) { if (_adjustmentsGrid.CurrentCell?.OwningColumn?.Name != "Field" || e.Control is not TextBox textBox) { return; } textBox.BackColor = UiTheme.Field; textBox.ForeColor = UiTheme.Text; textBox.BorderStyle = BorderStyle.None; } private void OpenAdjustmentConditionEditor(int rowIndex) { var row = _adjustmentsGrid.Rows[rowIndex]; var current = row.IsNewRow ? string.Empty : CellText(row, "Condition"); using var editor = new ConditionEditorForm( RefreshAndBuildConditionFields(), current, conditionFieldsProvider: () => RefreshAndBuildConditionFields(), spells: RefreshAndBuildConditionSpells(), conditionSpellsProvider: () => RefreshAndBuildConditionSpells(), items: RefreshAndBuildConditionItems(), conditionItemsProvider: () => RefreshAndBuildConditionItems()); if (editor.ShowDialog(FindForm()) != DialogResult.OK) { return; } if (row.IsNewRow) { if (!string.IsNullOrWhiteSpace(editor.ConditionText)) { _adjustmentsGrid.Rows.Add(true, string.Empty, 0, editor.ConditionText); RefreshAdjustmentFieldColumn(); } return; } row.Cells["Condition"].Value = editor.ConditionText; RefreshAdjustmentValidation(); } private void OpenFormulaEditor(int rowIndex) { var row = _formulaAdjustmentsGrid.Rows[rowIndex]; var current = row.IsNewRow ? string.Empty : CellText(row, "Formula"); using var editor = new FormulaEditorForm(current); if (editor.ShowDialog(FindForm()) != DialogResult.OK) { return; } var field = row.IsNewRow ? string.Empty : CellText(row, "Field"); var formula = editor.FormulaText; if (FormulaEvaluator.TrySplitAssignment(formula, out var formulaField, out var normalizedFormula)) { if (string.IsNullOrWhiteSpace(field)) { field = formulaField; } formula = normalizedFormula; } else { formula = FormulaEvaluator.NormalizeExpression(formula); } if (string.IsNullOrWhiteSpace(field) && !string.IsNullOrWhiteSpace(formula)) { MessageBox.Show( "请先填写公式动态数值的“数值名称”,或在公式中写成“名称 = 表达式”。", "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning); } if (row.IsNewRow) { if (!string.IsNullOrWhiteSpace(field) || !string.IsNullOrWhiteSpace(formula)) { _formulaAdjustmentsGrid.Rows.Add(true, field, formula); RefreshAdjustmentFieldColumn(); } return; } if (!string.IsNullOrWhiteSpace(field)) { row.Cells["Field"].Value = field; } row.Cells["Formula"].Value = formula; RefreshAdjustmentFieldColumn(); } private void DeleteRule(int rowIndex) { _rulesGrid.EndEdit(); var row = _rulesGrid.Rows[rowIndex]; // 新行占位符无需删除。 if (!row.IsNewRow) { _rulesGrid.Rows.RemoveAt(rowIndex); } } private void CopyRule(int rowIndex) { _rulesGrid.EndEdit(); if (!IsExistingRuleRow(rowIndex)) { return; } InsertRuleAfter(rowIndex, ReadRuleRow(_rulesGrid.Rows[rowIndex])); } private void InsertBlankRule(int rowIndex) { _rulesGrid.EndEdit(); if (!IsExistingRuleRow(rowIndex)) { return; } InsertRuleAfter(rowIndex, new RuleRowValues( true, string.Empty, string.Empty, string.Empty, string.Empty, string.Empty, Array.Empty(), null, null)); } private void InsertRuleAfter(int rowIndex, RuleRowValues values) { var insertIndex = rowIndex + 1; _rulesGrid.Rows.Insert(insertIndex, 1); var inserted = _rulesGrid.Rows[insertIndex]; WriteRuleRow(inserted, values); _rulesGrid.CurrentCell = inserted.Cells["Spell"]; inserted.Selected = true; _rulesGrid.Invalidate(); } private void MoveRule(int rowIndex, int direction) { _rulesGrid.EndEdit(); if (!IsExistingRuleRow(rowIndex)) { return; } var targetIndex = rowIndex + direction; if (targetIndex < 0 || targetIndex > LastRuleRowIndex()) { return; } var current = ReadRuleRow(_rulesGrid.Rows[rowIndex]); var target = ReadRuleRow(_rulesGrid.Rows[targetIndex]); WriteRuleRow(_rulesGrid.Rows[rowIndex], target); WriteRuleRow(_rulesGrid.Rows[targetIndex], current); _rulesGrid.CurrentCell = _rulesGrid.Rows[targetIndex].Cells["Spell"]; _rulesGrid.Rows[targetIndex].Selected = true; _rulesGrid.Invalidate(); } // 拖拽手柄按下: 记录起始行(仅限抓手列上的已有规则行)。 private void OnRulesGridMouseDown(object? sender, MouseEventArgs e) { _dragSourceRow = -1; var hit = _rulesGrid.HitTest(e.X, e.Y); if (hit.RowIndex >= 0 && hit.ColumnIndex >= 0 && _rulesGrid.Columns[hit.ColumnIndex].Name == "Drag" && IsExistingRuleRow(hit.RowIndex)) { _dragSourceRow = hit.RowIndex; } } // 在抓手上按住左键移动即开始拖拽(DoDragDrop 自带模态循环, 结束后复位)。 private void OnRulesGridMouseMove(object? sender, MouseEventArgs e) { if (_dragSourceRow < 0 || (e.Button & MouseButtons.Left) == 0) { return; } var source = _dragSourceRow; _rulesGrid.DoDragDrop(source, DragDropEffects.Move); _dragSourceRow = -1; ClearDragIndicator(); } private void OnRulesGridDragOver(object? sender, DragEventArgs e) { if (e.Data?.GetDataPresent(typeof(int)) != true) { e.Effect = DragDropEffects.None; return; } e.Effect = DragDropEffects.Move; SetDragIndicator(ResolveDropSlot(e)); } private void OnRulesGridDragDrop(object? sender, DragEventArgs e) { ClearDragIndicator(); if (e.Data?.GetData(typeof(int)) is int source) { MoveRuleByDrag(source, ResolveDropSlot(e)); } } private void OnRulesGridPaint(object? sender, PaintEventArgs e) { if (_dragIndicatorRow < 0) { return; } var last = LastRuleRowIndex(); // 指示位置可能等于"末尾"(= last+1): 画在最后一行的下边缘, 否则画在该行上边缘。 var atEnd = _dragIndicatorRow > last; var rect = _rulesGrid.GetRowDisplayRectangle(atEnd ? last : _dragIndicatorRow, false); if (rect.Height == 0) { return; } var y = atEnd ? rect.Bottom - 1 : rect.Top; using var pen = new Pen(UiTheme.Accent, 2); e.Graphics.DrawLine(pen, rect.Left, y, rect.Right, y); } // 把拖放点解析为"插入到第几行之前"的槽位(0..last+1), 行下半区视为插入到其后。 private int ResolveDropSlot(DragEventArgs e) { var pt = _rulesGrid.PointToClient(new Point(e.X, e.Y)); var hit = _rulesGrid.HitTest(pt.X, pt.Y); var last = LastRuleRowIndex(); if (hit.RowIndex < 0 || hit.RowIndex > last) { return last + 1; } var rect = _rulesGrid.GetRowDisplayRectangle(hit.RowIndex, false); var lowerHalf = pt.Y > rect.Top + rect.Height / 2; return lowerHalf ? hit.RowIndex + 1 : hit.RowIndex; } private void SetDragIndicator(int slot) { if (_dragIndicatorRow == slot) { return; } _dragIndicatorRow = slot; _rulesGrid.Invalidate(); } private void ClearDragIndicator() { if (_dragIndicatorRow < 0) { return; } _dragIndicatorRow = -1; _rulesGrid.Invalidate(); } // 把第 source 行移动到插入槽位 slot 之前, 通过读出全部规则行 → 重排 → 写回(行数不变)。 private void MoveRuleByDrag(int source, int slot) { _rulesGrid.EndEdit(); if (!IsExistingRuleRow(source)) { return; } var count = LastRuleRowIndex() + 1; if (count <= 1) { return; } slot = Math.Clamp(slot, 0, count); // 移除 source 后, 其后的插入位置整体前移一位。 var insertAt = Math.Clamp(source < slot ? slot - 1 : slot, 0, count - 1); if (insertAt == source) { return; } var rows = new List(count); for (var i = 0; i < count; i++) { rows.Add(ReadRuleRow(_rulesGrid.Rows[i])); } var moved = rows[source]; rows.RemoveAt(source); rows.Insert(insertAt, moved); for (var i = 0; i < count; i++) { WriteRuleRow(_rulesGrid.Rows[i], rows[i]); } _rulesGrid.CurrentCell = _rulesGrid.Rows[insertAt].Cells["Spell"]; _rulesGrid.Rows[insertAt].Selected = true; _rulesGrid.Invalidate(); } private int LastRuleRowIndex() { var last = _rulesGrid.Rows.Count - 1; if (_rulesGrid.AllowUserToAddRows) { last--; } return last; } private bool IsExistingRuleRow(int rowIndex) { return rowIndex >= 0 && rowIndex <= LastRuleRowIndex() && !_rulesGrid.Rows[rowIndex].IsNewRow; } private RuleRowValues ReadRuleRow(DataGridViewRow row) { return new RuleRowValues( CellBool(row, "Enabled", defaultValue: true), CellText(row, "Spell"), CellText(row, "Unit"), CellText(row, "MacroCondition"), CellText(row, "Condition"), CellText(row, RuleCommentColumnName), // 子条件和延迟挂在 row.Tag, 随行一起被移动/拖拽/复制搬运。 GetRuleMetadata(row).SubConditions, GetRuleMetadata(row).DelayMs, GetRuleMetadata(row).LogicDelayMs); } private void WriteRuleRow(DataGridViewRow row, RuleRowValues values) { 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.Cells[RuleCommentColumnName].Value = values.Comment; row.Tag = new RuleRowMetadata(values.SubConditions, values.DelayMs, values.LogicDelayMs); RebuildUnitCell(row, values.UnitText); RebuildMacroConditionCell(row, values.MacroCondition); } private void OpenConditionEditor(int rowIndex) { var row = _rulesGrid.Rows[rowIndex]; var current = row.IsNewRow ? string.Empty : CellText(row, "Condition"); var currentMetadata = row.IsNewRow ? new RuleRowMetadata() : GetRuleMetadata(row); var fields = RefreshAndBuildConditionFields(includeRuleSettings: true); var spells = RefreshAndBuildConditionSpells(); var items = RefreshAndBuildConditionItems(); using var editor = new ConditionEditorForm( fields, current, currentMetadata.SubConditions, allowSubConditions: true, delayMs: currentMetadata.DelayMs, logicDelayMs: currentMetadata.LogicDelayMs, allowRuleSettings: true, conditionFieldsProvider: () => RefreshAndBuildConditionFields(includeRuleSettings: true), spells: spells, conditionSpellsProvider: () => RefreshAndBuildConditionSpells(), items: items, conditionItemsProvider: () => RefreshAndBuildConditionItems()); if (editor.ShowDialog(FindForm()) != DialogResult.OK) { return; } var subs = new List(editor.SubConditions); if (row.IsNewRow) { // 新行占位符不能直接赋值, 改为追加一行(主条件或子条件任一非空即可)。 if (!string.IsNullOrWhiteSpace(editor.ConditionText) || subs.Count > 0 || editor.DelayMs is > 0 || editor.LogicDelayMs is > 0) { var index = _rulesGrid.Rows.Add(true, null!, string.Empty, string.Empty, string.Empty, editor.ConditionText); _rulesGrid.Rows[index].Tag = new RuleRowMetadata( subs, editor.DelayMs, editor.LogicDelayMs); } return; } row.Cells["Condition"].Value = editor.ConditionText; row.Tag = new RuleRowMetadata(subs, editor.DelayMs, editor.LogicDelayMs); // 让「条件」列的装饰显示(主条件 且任一(…))立即刷新。 _rulesGrid.InvalidateRow(rowIndex); } private void OpenRuleTextEditor(int rowIndex) { if (!IsExistingRuleRow(rowIndex)) { return; } var row = _rulesGrid.Rows[rowIndex]; using var editor = new RuleTextEditorForm(CellText(row, RuleCommentColumnName)); if (editor.ShowDialog(FindForm()) != DialogResult.OK) { return; } row.Cells[RuleCommentColumnName].Value = editor.CommentText; _rulesGrid.InvalidateRow(rowIndex); } // 条件字段 = 状态/技能字段 + 每个动态单位的裸名(存在)和值名称 + 动态数值。 private IReadOnlyList RefreshAndBuildConditionFields(bool includeRuleSettings = false) { // 配置可能由“更新配置”或外部文件同步在当前编辑会话中被重建;每次打开条件弹窗都读取最新目录。 _fieldCatalog = ConditionFieldCatalog.Load(_baseDirectory); InvalidateConditionFieldValidation(); return BuildConditionFields(includeRuleSettings); } private IReadOnlyList RefreshAndBuildConditionSpells() { ReloadCurrentClassSpellIds(); return _currentClassConditionSpells.ToArray(); } private IReadOnlyList RefreshAndBuildConditionItems() { ReloadCurrentClassSpellIds(); return _currentClassConditionItems.ToArray(); } private IReadOnlyList BuildConditionFields(bool includeRuleSettings = false) { var classId = ReadMatchCombo(_classBox); var specId = ReadMatchCombo(_specBox); var fields = new List(_fieldCatalog.GetFields(classId, specId)); var seen = new HashSet(fields.Select(field => field.Name), StringComparer.Ordinal); if (includeRuleSettings && seen.Add(ShigureConditionFields.Delay)) { fields.Add(new ConditionField( ShigureConditionFields.Delay, "延迟 (ms)", ConditionFieldType.Int, ConditionFieldCategory.Shigure)); } if (includeRuleSettings && seen.Add(ShigureConditionFields.LogicDelay)) { fields.Add(new ConditionField( ShigureConditionFields.LogicDelay, "逻辑延迟 (ms)", ConditionFieldType.Int, ConditionFieldCategory.Shigure)); } foreach (var unit in _units) { if (string.IsNullOrWhiteSpace(unit.Name)) { continue; } // 裸单位名作为存在性布尔。 if (seen.Add(unit.Name)) { fields.Add(new ConditionField(unit.Name, $"{unit.Name} (存在)", ConditionFieldType.Bool, ConditionFieldCategory.DynamicUnit)); } // 值名称: 该单位 生命值 的直接命名数值字段。 if (!string.IsNullOrWhiteSpace(unit.HealthName) && seen.Add(unit.HealthName)) { fields.Add(new ConditionField(unit.HealthName, $"{unit.HealthName} (生命值)", ConditionFieldType.Int, ConditionFieldCategory.DynamicUnit)); } } foreach (var count in _counts) { if (!string.IsNullOrWhiteSpace(count.Name) && seen.Add(count.Name)) { fields.Add(new ConditionField(count.Name, $"人数: {count.Name}", ConditionFieldType.Int, ConditionFieldCategory.DynamicValue)); } } foreach (var fieldName in GetAdjustmentTargetFields()) { if (seen.Add(fieldName)) { fields.Add(new ConditionField( fieldName, $"{fieldName} (动态数值)", ConditionFieldType.Int, ConditionFieldCategory.DynamicValue)); } } return fields; } private Control BuildActionRow() { var row = new UiCardPanel { Dock = DockStyle.Fill, ColumnCount = 3, RowCount = 1, Margin = new Padding(0), Padding = new Padding(UiTheme.CardPadding, 10, UiTheme.CardPadding, 10) }; row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 148)); row.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 360)); row.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); var openFolderButton = UiTheme.CreateButton("打开目录", UiTheme.ButtonKind.Secondary); StyleModuleFooterButton(openFolderButton); openFolderButton.Dock = DockStyle.Fill; openFolderButton.Margin = new Padding(0, 0, 8, 0); openFolderButton.Click += (_, _) => OpenModuleFolder(); _pathToolTip.SetToolTip( openFolderButton, "在资源管理器中打开模块目录;若已选中已保存模块则定位到对应文件"); var spacer = new Panel { Dock = DockStyle.Fill, BackColor = Color.Transparent, Margin = new Padding(0) }; var buttons = new TableLayoutPanel { Dock = DockStyle.Fill, BackColor = Color.Transparent, ColumnCount = 3, RowCount = 1, Margin = new Padding(0), Padding = new Padding(0) }; buttons.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33.33f)); buttons.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33.33f)); buttons.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33.34f)); buttons.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); _addButton = UiTheme.CreateButton("新建", UiTheme.ButtonKind.Secondary); StyleModuleFooterButton(_addButton); _addButton.Dock = DockStyle.Fill; _addButton.Margin = new Padding(0, 0, 8, 0); _addButton.Click += async (_, _) => await RunModuleCommandAsync(AddModuleAsync); _deleteButton = UiTheme.CreateButton("删除", UiTheme.ButtonKind.Danger); StyleModuleFooterButton(_deleteButton); _deleteButton.Dock = DockStyle.Fill; _deleteButton.Margin = new Padding(0, 0, 8, 0); _deleteButton.Click += async (_, _) => await RunModuleCommandAsync(DeleteSelectedModuleAsync); _saveButton = UiTheme.CreateButton("保存", UiTheme.ButtonKind.Primary); StyleModuleFooterButton(_saveButton); _saveButton.Dock = DockStyle.Fill; _saveButton.Margin = new Padding(0); _saveButton.Click += async (_, _) => await RunModuleCommandAsync(SaveSelectedModuleAsync); buttons.Controls.Add(_addButton, 0, 0); buttons.Controls.Add(_deleteButton, 1, 0); buttons.Controls.Add(_saveButton, 2, 0); row.Controls.Add(openFolderButton, 0, 0); row.Controls.Add(spacer, 1, 0); row.Controls.Add(buttons, 2, 0); return row; } private void OpenModuleFolder() { var moduleDirectory = ModuleStore.ResolveModuleDirectory(); var filePath = _selectedModule?.FilePath; try { if (!string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath)) { Process.Start(new ProcessStartInfo { FileName = "explorer.exe", Arguments = $"/select,\"{filePath}\"", UseShellExecute = true }); return; } if (!Directory.Exists(moduleDirectory)) { Directory.CreateDirectory(moduleDirectory); } Process.Start(new ProcessStartInfo { FileName = "explorer.exe", Arguments = $"\"{moduleDirectory}\"", UseShellExecute = true }); } catch (Exception ex) { MessageBox.Show( $"无法打开模块文件夹:{ex.Message}", "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } private static void StyleModuleFooterButton(Button button) { button.AutoSize = false; button.Height = ModuleFooterButtonHeight; button.Margin = new Padding(0); button.Padding = new Padding(10, 0, 10, 0); button.TextAlign = ContentAlignment.MiddleCenter; } private static void StyleModuleActionButton(Button button) => StyleModuleFooterButton(button); public void ReloadModulesFromStore(bool reloadStore = true) => LoadModules(reloadStore); private void LoadModules(bool reloadStore = true) { if (reloadStore) { _moduleStore.Reload(); } _modules = _moduleStore.GetModulesForDisplay().ToList(); _moduleList.Items.Clear(); foreach (var module in _modules) { _moduleList.Items.Add(ModuleDisplay.FormatListItem(module)); } if (_modules.Count > 0) { _moduleList.SelectedIndex = 0; } else { ClearEditor(); } } private void SelectModule(int index) { if (index < 0 || index >= _modules.Count) { ClearEditor(); return; } _selectedModule = _modules[index].Clone(); FillEditor(_selectedModule); } private void FillEditor(ModuleDefinition module) { CloseRulesComboDropDown(); CloseAdjustmentComboDropDown(); _nameBox.Text = module.Name; _authorBox.Text = module.Author; _recommendedTalentBox.Text = module.RecommendedTalent; SetEditorEnabled(hasModule: true); // 先填充动态单位/数量, 后续目标下拉与条件字段都依赖它们。 _units.Clear(); _units.AddRange(module.Units.Select(unit => unit.Clone())); _counts.Clear(); _counts.AddRange(module.Counts.Select(count => count.Clone())); _valueAdjustments.Clear(); _valueAdjustments.AddRange(module.ValueAdjustments.Select(adjustment => adjustment.Clone())); SelectClass(module.Match.ClassId); SelectSpec(module.Match.SpecId); SelectPartyType(module.Match.PartyType); SelectHeroTalent(module.Match.HeroTalent); RefreshUnitsList(); _pathLabel.Text = module.FilePath ?? "尚未保存"; _versionLabel.Text = string.IsNullOrWhiteSpace(module.Version) ? "版本 未知" : $"版本 {module.Version}"; _adjustmentsGrid.Rows.Clear(); _formulaAdjustmentsGrid.Rows.Clear(); RefreshAdjustmentFieldColumn(); foreach (var adjustment in _valueAdjustments) { if (string.IsNullOrWhiteSpace(adjustment.Formula)) { var index = _adjustmentsGrid.Rows.Add(adjustment.Enabled, adjustment.Field, adjustment.Delta, adjustment.Condition); // 由字段名回填"类型", 并按类型重建该行"数值"的可选项。 ApplyAdjustmentRowType(_adjustmentsGrid.Rows[index], adjustment.Field); } else { _formulaAdjustmentsGrid.Rows.Add( adjustment.Enabled, adjustment.Field, FormulaEvaluator.NormalizeExpression(adjustment.Formula)); } } RefreshAdjustmentFieldColumn(); _rulesGrid.Rows.Clear(); RefreshKeymapColumns(); foreach (var rule in module.Rules) { var ruleSpellDisplay = DisplayRuleSpell(rule.Spell); // 动态目标优先显示单位名;保留单位显示中文,其余团队槽位显示数字。 var unitText = !string.IsNullOrWhiteSpace(rule.UnitName) ? rule.UnitName! : rule.Unit is { } unit ? ReservedUnit.ToDisplayText(unit) : string.Empty; EnsureComboItem(_spellColumn, ruleSpellDisplay); // 先加行(目标先留空), 再按技能重建目标选项并写回目标值, 避免值不在选项内被吞掉。 var index = _rulesGrid.Rows.Add( rule.Enabled, GetRuleSpellIcon(ruleSpellDisplay)!, ruleSpellDisplay, string.Empty, string.Empty, rule.Condition, rule.Comment); _rulesGrid.Rows[index].Tag = new RuleRowMetadata( rule.SubConditions, rule.DelayMs, rule.LogicDelayMs); RebuildUnitCell(_rulesGrid.Rows[index], unitText); RebuildMacroConditionCell(_rulesGrid.Rows[index], rule.MacroCondition); } } private void ClearEditor() { CloseRulesComboDropDown(); CloseAdjustmentComboDropDown(); _selectedModule = null; _nameBox.Clear(); _authorBox.Clear(); _recommendedTalentBox.Clear(); _units.Clear(); _counts.Clear(); _valueAdjustments.Clear(); RefreshUnitsList(); SelectClass(null); SelectSpec(null); SelectPartyType(null); SelectHeroTalent(null); _pathLabel.Text = "无模块"; _versionLabel.Text = string.Empty; _adjustmentsGrid.Rows.Clear(); _formulaAdjustmentsGrid.Rows.Clear(); RefreshAdjustmentFieldColumn(); _rulesGrid.Rows.Clear(); SetEditorEnabled(hasModule: false); } // 无选中模块时禁用保存/删除(否则点了静默无反应), 并在编辑区显示引导提示。 private void SetEditorEnabled(bool hasModule) { _saveButton.Enabled = hasModule && !_moduleCommandInProgress; _deleteButton.Enabled = hasModule && !_moduleCommandInProgress; _addButton.Enabled = !_moduleCommandInProgress; _editorEmptyHint.Visible = !hasModule; if (!hasModule) { _editorEmptyHint.BringToFront(); } } private async Task RunModuleCommandAsync(Func command) { if (_moduleCommandInProgress) { return; } _moduleCommandInProgress = true; SetEditorEnabled(_selectedModule is not null); try { await command(); } catch (Exception ex) { if (!IsDisposed) { MessageBox.Show(ex.Message, "模块操作失败", MessageBoxButtons.OK, MessageBoxIcon.Error); } } finally { _moduleCommandInProgress = false; if (!IsDisposed) { SetEditorEnabled(_selectedModule is not null); } } } private async Task AddModuleAsync() { var module = ModuleDefinition.CreateDefault(_moduleStore.CreateNextModuleName()); try { _moduleStore.Save(module); } catch (InvalidOperationException ex) { MessageBox.Show(ex.Message, "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } LoadModules(reloadStore: false); var index = _modules.FindIndex(existing => string.Equals(existing.Id, module.Id, StringComparison.OrdinalIgnoreCase)); if (index >= 0) { _moduleList.SelectedIndex = index; } await _runtimeRestartRequested(); } private async Task SaveSelectedModuleAsync() { if (_selectedModule is null) { return; } if (!TryReadModule(out var module)) { return; } ModuleDefinition saved; string? dependencyWarning; try { dependencyWarning = _captureDependencies(module); saved = _moduleStore.Save(module); } catch (InvalidOperationException ex) { MessageBox.Show(ex.Message, "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } LoadModules(reloadStore: false); var index = _modules.FindIndex(existing => string.Equals(existing.Id, saved.Id, StringComparison.OrdinalIgnoreCase)); if (index >= 0) { _moduleList.SelectedIndex = index; } if (!string.IsNullOrWhiteSpace(dependencyWarning)) { MessageBox.Show(dependencyWarning, "模块已保存", MessageBoxButtons.OK, MessageBoxIcon.Information); } await _runtimeRestartRequested(); } private async Task DeleteSelectedModuleAsync() { if (_selectedModule is null) { return; } var result = MessageBox.Show( $"删除模块“{_selectedModule.Name}”?", "Shigure", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (result != DialogResult.Yes) { return; } _moduleStore.Delete(_selectedModule); LoadModules(reloadStore: false); await _runtimeRestartRequested(); } private bool TryReadModule(out ModuleDefinition module) { module = _selectedModule!.Clone(); module.Name = string.IsNullOrWhiteSpace(_nameBox.Text) ? "新模块" : _nameBox.Text.Trim(); module.Author = _authorBox.Text.Trim(); module.RecommendedTalent = _recommendedTalentBox.Text.Trim(); // 保存时记录当前 Shigure 版本。 module.Version = AppInfo.Version; module.Match = new ModuleMatch { ClassId = ReadMatchCombo(_classBox), SpecId = ReadMatchCombo(_specBox), PartyType = ReadPartyTypeCombo(), HeroTalent = ReadMatchCombo(_heroTalentBox) }; module.Units = _units.Select(unit => unit.Clone()).ToList(); module.Counts = _counts.Select(count => count.Clone()).ToList(); if (!TryReadValueAdjustments(out var valueAdjustments, out var adjustmentError)) { MessageBox.Show(adjustmentError, "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; } module.ValueAdjustments = valueAdjustments; if (!TryReadRules(out var rules, out var rulesError)) { MessageBox.Show(rulesError, "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; } module.Rules = rules; if (!TryUpgradeLegacySpellReferences(module, out var upgradeError)) { MessageBox.Show(upgradeError, "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; } return true; } private bool TryUpgradeLegacySpellReferences(ModuleDefinition module, out string error) { error = string.Empty; var failure = string.Empty; var candidates = new Dictionary>(StringComparer.Ordinal); void Add(string legacy, string current) { if (string.IsNullOrWhiteSpace(legacy) || string.IsNullOrWhiteSpace(current)) { return; } if (!candidates.TryGetValue(legacy, out var values)) { values = new HashSet(StringComparer.Ordinal); candidates[legacy] = values; } values.Add(current); } var spec = module.Dependencies?.Config?.Spec; if (spec is not null) { AddAuraEntries(spec.PlayerAuras, "player", string.Empty); AddAuraEntries(spec.TargetHarmfulAuras, "target.harmful", "目标"); AddAuraEntries(spec.TargetHelpfulAuras, "target.helpful", "目标"); AddAuraEntries(spec.FocusHarmfulAuras, "focus.harmful", "焦点"); AddAuraEntries(spec.FocusHelpfulAuras, "focus.helpful", "焦点"); foreach (var spell in spec.Spells ?? []) { if (spell.SpellId <= 0 || string.IsNullOrWhiteSpace(spell.Name)) { continue; } Add($"spells.{spell.Name}", SpellFieldKey.Spell(spell.SpellId)); if (spell.Charge) { Add($"spells.{spell.Name}充能", SpellFieldKey.Spell(spell.SpellId, SpellFieldKey.SpellChargeCooldown)); } if (spell.MaxCharge is not null || spell.CastCount is not null) { Add($"spells.{spell.Name}层数", SpellFieldKey.Spell(spell.SpellId, SpellFieldKey.SpellCount)); } } } foreach (var field in _fieldCatalog.GetGroupFields(module.Match.ClassId, module.Match.SpecId) .Where(field => field.Name.StartsWith("auras.", StringComparison.Ordinal))) { var display = field.DisplayName.Split(" / ", 2, StringSplitOptions.TrimEntries)[0]; Add(display, field.Name); } var ambiguous = candidates.Where(pair => pair.Value.Count != 1).Select(pair => pair.Key).ToHashSet(StringComparer.Ordinal); var replacements = candidates .Where(pair => pair.Value.Count == 1) .ToDictionary(pair => pair.Key, pair => pair.Value.Single(), StringComparer.Ordinal); var dynamicFieldNames = module.Units .SelectMany(unit => new[] { unit.Name, unit.HealthName }) .Concat(module.Counts.Select(count => count.Name)) .Where(name => !string.IsNullOrWhiteSpace(name)) .Select(name => name!.Trim()) .Distinct(StringComparer.Ordinal) .ToArray(); for (var ruleIndex = 0; ruleIndex < module.Rules.Count; ruleIndex++) { var rule = module.Rules[ruleIndex]; var rowLabel = $"规则第 {ruleIndex + 1} 行"; if (!TryReplace(rule.Condition, $"{rowLabel}主条件", out var condition) || rule.SubConditions is not null && !TryReplaceList(rule.SubConditions, index => $"{rowLabel}子条件第 {index + 1} 行")) { error = failure; return false; } rule.Condition = condition; } for (var adjustmentIndex = 0; adjustmentIndex < module.ValueAdjustments.Count; adjustmentIndex++) { var adjustment = module.ValueAdjustments[adjustmentIndex]; var rowLabel = $"动态数值第 {adjustmentIndex + 1} 行"; if (!TryReplace(adjustment.Condition, $"{rowLabel}条件", out var condition) || !TryReplace(adjustment.Field, $"{rowLabel}调整目标", out var field) || !TryReplace(adjustment.Formula, $"{rowLabel}公式", out var formula)) { error = failure; return false; } adjustment.Condition = condition; adjustment.Field = field; adjustment.Formula = formula; } for (var unitIndex = 0; unitIndex < module.Units.Count; unitIndex++) { var unit = module.Units[unitIndex]; if (unit.AuraNames is not { Count: > 0 }) { continue; } var ids = new List(); foreach (var name in unit.AuraNames) { if (!replacements.TryGetValue(name, out var key) || !TryExtractId(key, out var id)) { error = $"动态单位第 {unitIndex + 1} 行“{unit.Name}”引用的队伍光环“{name}”无法唯一转换为本地 spellId。"; return false; } ids.Add(id); } unit.AuraSpellIds = ids.Distinct().ToList(); unit.AuraNames = null; } for (var countIndex = 0; countIndex < module.Counts.Count; countIndex++) { var count = module.Counts[countIndex]; if (string.IsNullOrWhiteSpace(count.AuraName)) { continue; } if (!replacements.TryGetValue(count.AuraName, out var key) || !TryExtractId(key, out var id)) { error = $"动态数量第 {countIndex + 1} 行“{count.Name}”引用的队伍光环“{count.AuraName}”无法唯一转换为本地 spellId。"; return false; } count.AuraSpellId = id; count.AuraName = null; } return true; void AddAuraEntries(IEnumerable? entries, string scope, string prefix) { foreach (var aura in entries ?? []) { var id = SpellFieldKey.CanonicalAuraId(aura.SpellId, aura.SpellIds); if (id is null || string.IsNullOrWhiteSpace(aura.Name)) { continue; } Add($"auras.{prefix}{aura.Name}", SpellFieldKey.Aura(scope, id.Value)); if (aura.MaxApps is not null) { Add($"auras.{prefix}{aura.Name}层数", SpellFieldKey.Aura(scope, id.Value, SpellFieldKey.AuraApplications)); } } } bool TryReplace(string source, string location, out string result) { result = source ?? string.Empty; // 兼容修复旧迁移逻辑曾经造成的子串污染,例如: // “无救赎最低”被错误写成“无auras.194384.value最低”。 foreach (var dynamicName in dynamicFieldNames) { foreach (var pair in replacements) { if (!dynamicName.Contains(pair.Key, StringComparison.Ordinal)) { continue; } var corrupted = dynamicName.Replace(pair.Key, pair.Value, StringComparison.Ordinal); if (!string.Equals(corrupted, dynamicName, StringComparison.Ordinal)) { result = ReplaceFieldReference(result, corrupted, dynamicName); } } } // 动态单位/生命值/数量名称优先于旧光环显示名。先暂存完整引用, // 防止名称恰好等于或包含旧光环名时被迁移逻辑改写。 var protectedReferences = new List<(string Placeholder, string Name)>(); for (var index = 0; index < dynamicFieldNames.Length; index++) { var name = dynamicFieldNames[index]; if (!ContainsFieldReference(result, name)) { continue; } var placeholder = $"__SHIGURE_DYNAMIC_FIELD_{index}__"; result = ReplaceFieldReference(result, name, placeholder); protectedReferences.Add((placeholder, name)); } foreach (var key in ambiguous) { if (ContainsFieldReference(result, key)) { failure = $"{location}:旧模块字段“{key}”对应多个 spellId,请重新选择后再保存。"; return false; } } foreach (var pair in replacements.OrderByDescending(pair => pair.Key.Length)) { result = ReplaceFieldReference(result, pair.Key, pair.Value); } if (Regex.IsMatch(result, @"\b(?:auras|spells)\.[^\s&|<>=!+\-*/()]+")) { var unresolved = Regex.Match(result, @"\b(?:auras|spells)\.[^\s&|<>=!+\-*/()]+").Value; if (!SpellFieldKey.TryParseAura(unresolved, out _, out _, out _) && !SpellFieldKey.TryParseAuraMember(unresolved, out _, out _) && !SpellFieldKey.TryParseSpell(unresolved, out _, out _)) { failure = $"{location}:旧模块字段“{unresolved}”无法转换为 spellId,请重新选择后再保存。"; return false; } } foreach (var (placeholder, name) in protectedReferences) { result = result.Replace(placeholder, name, StringComparison.Ordinal); } return true; } static bool ContainsFieldReference(string source, string field) => Regex.IsMatch(source, FieldReferencePattern(field)); static string ReplaceFieldReference(string source, string field, string replacement) => Regex.Replace(source, FieldReferencePattern(field), _ => replacement); static string FieldReferencePattern(string field) => $@"(? values, Func location) { for (var i = 0; i < values.Count; i++) { if (!TryReplace(values[i], location(i), out var replaced)) { return false; } values[i] = replaced; } return true; } static bool TryExtractId(string key, out long id) { foreach (var part in key.Split('.', StringSplitOptions.RemoveEmptyEntries)) { if (long.TryParse(part, out id) && id > 0) { return true; } } id = 0; return false; } } private bool TryReadValueAdjustments(out List adjustments, out string error) { adjustments = new List(); error = string.Empty; foreach (DataGridViewRow row in _adjustmentsGrid.Rows) { if (row.IsNewRow) { continue; } var field = CellText(row, "Field"); var condition = CellText(row, "Condition"); var delta = ParseNullableInt(CellText(row, "Delta")) ?? 0; if (string.IsNullOrWhiteSpace(field) && string.IsNullOrWhiteSpace(condition) && delta == 0) { continue; } if (string.IsNullOrWhiteSpace(field)) { continue; } adjustments.Add(new ModuleValueAdjustment { Enabled = CellBool(row, "Enabled", defaultValue: true), Field = field, Delta = delta, Formula = string.Empty, Condition = condition }); } foreach (DataGridViewRow row in _formulaAdjustmentsGrid.Rows) { if (row.IsNewRow) { continue; } var field = CellText(row, "Field"); var formula = CellText(row, "Formula"); if (string.IsNullOrWhiteSpace(field) && FormulaEvaluator.TrySplitAssignment(formula, out var formulaField, out var normalizedFormula)) { field = formulaField; formula = normalizedFormula; } else { formula = FormulaEvaluator.NormalizeExpression(formula); } if (string.IsNullOrWhiteSpace(field) && string.IsNullOrWhiteSpace(formula)) { continue; } if (string.IsNullOrWhiteSpace(field) || string.IsNullOrWhiteSpace(formula)) { var rowNumber = row.Index + 1; error = string.IsNullOrWhiteSpace(field) ? $"公式动态数值第 {rowNumber} 行缺少数值名称。请在“数值名称”里输入名称,或把公式写成“名称 = 表达式”。" : $"公式动态数值第 {rowNumber} 行缺少公式。"; return false; } adjustments.Add(new ModuleValueAdjustment { Enabled = CellBool(row, "Enabled", defaultValue: true), Field = field, Delta = 0, Formula = formula, Condition = string.Empty }); } return true; } private bool TryReadRules(out List rules, out string error) { error = string.Empty; var unitNames = new HashSet( _units.Where(unit => !string.IsNullOrWhiteSpace(unit.Name)).Select(unit => unit.Name), StringComparer.Ordinal); rules = new List(); foreach (DataGridViewRow row in _rulesGrid.Rows) { if (row.IsNewRow) { continue; } var condition = CellText(row, "Condition"); var comment = CellText(row, RuleCommentColumnName); 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(comment) && string.IsNullOrWhiteSpace(spell) && string.IsNullOrWhiteSpace(unitText) && string.IsNullOrWhiteSpace(macroCondition) && metadata.SubConditions.Count == 0 && metadata.DelayMs is not > 0 && metadata.LogicDelayMs is not > 0) { continue; } // 目标文本命中已定义动态单位名 → UnitName;否则把中文保留单位或数字槽位还原为 Unit。 var isDynamic = unitNames.Contains(unitText); var subs = metadata.SubConditions .Select(sub => sub?.Trim() ?? string.Empty) .Where(sub => sub.Length > 0) .ToList(); rules.Add(new ModuleRule { Enabled = CellBool(row, "Enabled", defaultValue: true), Condition = condition, Comment = comment, 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, DelayMs = metadata.DelayMs, LogicDelayMs = metadata.LogicDelayMs }); } return true; } private string DisplayRuleSpell(string? persisted) { var value = persisted?.Trim() ?? string.Empty; if (value.Length == 0 || ModuleSpecialActions.IsPauseSpell(value) || ModuleSpecialActions.IsFailedSpell(value) || ModuleSpecialActions.IsFailedItem(value) || ModuleSpecialActions.IsOneKeySpell(value)) { return value; } if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var spellId)) { return _currentClassConditionSpells.FirstOrDefault(spell => spell.SpellId == spellId)?.Name ?? value; } return value; } private static RuleRowMetadata GetRuleMetadata(DataGridViewRow row) { return row.Tag switch { RuleRowMetadata metadata => metadata, // 兼容本次升级前已经加载到控件中的旧 Tag 结构。 List subConditions => new RuleRowMetadata(subConditions), _ => new RuleRowMetadata() }; } private static void AddMatchField(TableLayoutPanel row, string label, UiDropDown box, int column) { var fieldLabel = CreateLabel(label); fieldLabel.Margin = Padding.Empty; row.Controls.Add(fieldLabel, column, 0); UiTheme.StyleComboBox(box); // 仅横向拉伸,让固定高度的下拉控件在行内垂直居中,与标签共用一条水平中心线。 box.Dock = DockStyle.None; box.Anchor = AnchorStyles.Left | AnchorStyles.Right; box.Margin = Padding.Empty; row.Controls.Add(box, column + 1, 0); } private static Label CreateLabel(string text) { return new Label { Text = text, Dock = DockStyle.Fill, ForeColor = UiTheme.Muted, BackColor = Color.Transparent, TextAlign = ContentAlignment.MiddleLeft, AutoEllipsis = true }; } private static Label CreateSectionLabel(string text) { return new Label { Text = text, Dock = DockStyle.Fill, ForeColor = UiTheme.Muted, TextAlign = ContentAlignment.MiddleLeft, AutoEllipsis = true, Padding = new Padding(0, 2, 0, 0) }; } private static int MeasureLabelColumnWidth(string text, Font font) { return TextRenderer.MeasureText(text, font).Width + 18; } private static Button CreateUnitActionButton(string text, Color backColor, Color foreColor, bool bottomGap) { var button = UiTheme.CreateButton(text, backColor, foreColor); button.AutoSize = false; button.AutoEllipsis = true; button.Height = 36; button.Margin = new Padding(0, 0, 0, bottomGap ? 8 : 0); button.Padding = new Padding(0); button.TextAlign = ContentAlignment.MiddleCenter; return button; } private static void LayoutUnitActionButtons(FlowLayoutPanel panel) { var width = Math.Max(0, panel.ClientSize.Width); foreach (Control control in panel.Controls) { if (control is Button button) { button.Width = width; } } } private void SelectClass(int? value) { var index = FindMatchOption(_classBox, value); if (index < 0 && value is not null) { _classBox.Items.Add(new MatchOption($"职业{value} ({value})", value)); index = _classBox.Items.Count - 1; } _classBox.SelectedIndex = index >= 0 ? index : 0; ResetSpecOptions(_specBox, ReadMatchCombo(_classBox)); } private void SelectSpec(int? value) { var index = FindMatchOption(_specBox, value); if (index < 0 && value is not null) { _specBox.Items.Add(new MatchOption($"专精{value} ({value})", value)); index = _specBox.Items.Count - 1; } _specBox.SelectedIndex = index >= 0 ? index : 0; ResetHeroTalentOptions(_heroTalentBox, ReadMatchCombo(_classBox), ReadMatchCombo(_specBox)); } private void SelectHeroTalent(int? value) { var index = FindMatchOption(_heroTalentBox, value); if (index < 0 && value is not null) { _heroTalentBox.Items.Add(new MatchOption($"英雄天赋{value} ({value})", value)); index = _heroTalentBox.Items.Count - 1; } _heroTalentBox.SelectedIndex = index >= 0 ? index : 0; } private static int? ReadMatchCombo(UiDropDown comboBox) { return comboBox.SelectedItem is MatchOption option ? option.Value : null; } private static void ResetClassOptions(UiDropDown comboBox) { comboBox.Items.Clear(); comboBox.Items.AddRange(ClassOptions); comboBox.SelectedIndex = 0; } private static void ResetSpecOptions(UiDropDown comboBox, int? classId) { comboBox.Items.Clear(); comboBox.Items.Add(new MatchOption("任意 (*)", null)); if (classId is not null) { foreach (var spec in ClassNames.GetSpecs(classId.Value)) { comboBox.Items.Add(new MatchOption($"{spec.Name} ({spec.Id})", spec.Id)); } } comboBox.SelectedIndex = 0; } private static void ResetHeroTalentOptions(UiDropDown comboBox, int? classId, int? specId) { comboBox.Items.Clear(); comboBox.Items.Add(new MatchOption("任意 (*)", null)); if (classId is not null && specId is not null) { foreach (var heroTalent in ClassNames.GetHeroTalents(classId.Value, specId.Value)) { comboBox.Items.Add(new MatchOption($"{heroTalent.Name} ({heroTalent.Id})", heroTalent.Id)); } } comboBox.SelectedIndex = 0; } private static int FindMatchOption(UiDropDown comboBox, int? value) { for (var i = 0; i < comboBox.Items.Count; i++) { if (comboBox.Items[i] is MatchOption option && option.Value == value) { return i; } } return -1; } private static MatchOption[] BuildClassOptions() { return ClassNames.GetClasses() .Select(item => new MatchOption($"{item.Name} ({item.Id})", item.Id)) .Prepend(new MatchOption("任意 (*)", null)) .ToArray(); } private void SelectPartyType(string? value) { ResetPartyTypeOptions(_partyTypeBox); var normalized = ModuleMatch.NormalizePartyTypeValue(value); var index = FindPartyTypeOption(normalized); if (index < 0 && !string.IsNullOrWhiteSpace(normalized)) { _partyTypeBox.Items.Add(new PartyTypeOption($"自定义 ({normalized})", normalized)); index = _partyTypeBox.Items.Count - 1; } _partyTypeBox.SelectedIndex = index >= 0 ? index : 0; } private string? ReadPartyTypeCombo() { return _partyTypeBox.SelectedItem is PartyTypeOption option ? option.Value : null; } private static void ResetPartyTypeOptions(UiDropDown comboBox) { comboBox.Items.Clear(); comboBox.Items.AddRange(PartyTypeOptions); comboBox.SelectedIndex = 0; } private static int FindPartyTypeOption(string? value) { for (var i = 0; i < PartyTypeOptions.Length; i++) { if (string.Equals(PartyTypeOptions[i].Value, value, StringComparison.OrdinalIgnoreCase)) { return i; } } return -1; } private static string CellText(DataGridViewRow row, string columnName) { return row.Cells[columnName].Value?.ToString()?.Trim() ?? string.Empty; } private static bool CellBool(DataGridViewRow row, string columnName, bool defaultValue) { var value = row.Cells[columnName].Value; return value switch { bool b => b, string s when bool.TryParse(s, out var parsed) => parsed, null => defaultValue, _ => defaultValue }; } private static int? ParseNullableInt(string text) { return int.TryParse(text, out var value) ? value : null; } private sealed record PartyTypeOption(string Text, string? Value) { public override string ToString() { return Text; } } private sealed record MatchOption(string Text, int? Value) { public override string ToString() { return Text; } } }