mirror of
https://github.com/waynebian01/Shigure.git
synced 2026-09-02 22:39:01 +08:00
优化界面
This commit is contained in:
@@ -12,11 +12,19 @@ public enum ConditionFieldType
|
||||
public enum ConditionFieldCategory
|
||||
{
|
||||
State,
|
||||
Shigure,
|
||||
Aura,
|
||||
Spell,
|
||||
DynamicUnit
|
||||
}
|
||||
|
||||
public static class ShigureConditionFields
|
||||
{
|
||||
// 仅供条件编辑器承载规则级配置,不会写入条件表达式参与状态求值。
|
||||
public const string Delay = "$shigure.delay";
|
||||
public const string LogicDelay = "$shigure.logicDelay";
|
||||
}
|
||||
|
||||
public sealed record ConditionField(
|
||||
string Name,
|
||||
string DisplayName,
|
||||
@@ -69,6 +77,7 @@ public sealed class ConditionFieldCatalog
|
||||
foreach (var (key, node) in stateConfig)
|
||||
{
|
||||
if (key is "group" or "spells" or "auras"
|
||||
|| key == "锚点"
|
||||
|| ModuleSpecialActions.IsFailedSpell(key))
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -4,7 +4,10 @@ public sealed record LogicDecision(
|
||||
string? Hotkey,
|
||||
string Step,
|
||||
IReadOnlyDictionary<string, object?> UnitInfo,
|
||||
string? ModuleName = null);
|
||||
string? ModuleName = null,
|
||||
int DelayMs = 0,
|
||||
string? RateLimitKey = null,
|
||||
int LogicDelayMs = 0);
|
||||
|
||||
public interface IClassLogic
|
||||
{
|
||||
|
||||
@@ -184,6 +184,10 @@ public sealed class ModuleRule
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public string Condition { get; set; } = string.Empty;
|
||||
// 此规则命中后,两次实际发送之间的最小间隔(毫秒);null/0 表示不限制。
|
||||
public int? DelayMs { get; set; }
|
||||
// 此规则实际发送按键后,暂停整个逻辑循环的时长(毫秒);null/0 表示不暂停。
|
||||
public int? LogicDelayMs { get; set; }
|
||||
public int? Unit { get; set; }
|
||||
public string? UnitName { get; set; }
|
||||
public string Spell { get; set; } = string.Empty;
|
||||
@@ -201,6 +205,8 @@ public sealed class ModuleRule
|
||||
{
|
||||
Enabled = Enabled,
|
||||
Condition = Condition,
|
||||
DelayMs = DelayMs,
|
||||
LogicDelayMs = LogicDelayMs,
|
||||
Unit = Unit,
|
||||
UnitName = UnitName,
|
||||
Spell = Spell,
|
||||
@@ -507,6 +513,8 @@ public sealed class ModuleStore
|
||||
module.Rules ??= new List<ModuleRule>();
|
||||
foreach (var rule in module.Rules)
|
||||
{
|
||||
rule.DelayMs = rule.DelayMs is > 0 ? rule.DelayMs : null;
|
||||
rule.LogicDelayMs = rule.LogicDelayMs is > 0 ? rule.LogicDelayMs : null;
|
||||
if (rule.SubConditions is null)
|
||||
{
|
||||
continue;
|
||||
@@ -594,8 +602,14 @@ public static class ModuleLogic
|
||||
var failedSpells = keymap.GetCurrentFailedSpells();
|
||||
var oneKeySpells = keymap.GetCurrentOneKeySpells();
|
||||
|
||||
foreach (var rule in module.Rules.Where(rule => rule.Enabled))
|
||||
for (var ruleIndex = 0; ruleIndex < module.Rules.Count; ruleIndex++)
|
||||
{
|
||||
var rule = module.Rules[ruleIndex];
|
||||
if (!rule.Enabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ModuleConditionEvaluator.TryEvaluateRule(rule, state, out var conditionMatched, out var error, failedSpells))
|
||||
{
|
||||
info["条件错误"] = error;
|
||||
@@ -660,7 +674,16 @@ public static class ModuleLogic
|
||||
info["动作单位"] = string.IsNullOrWhiteSpace(rule.UnitName)
|
||||
? resolvedUnit.GetValueOrDefault()
|
||||
: $"{rule.UnitName} → {resolvedUnit.GetValueOrDefault()}";
|
||||
return new LogicDecision(hotkey, step, info, module.Name);
|
||||
info["动作延迟"] = rule.DelayMs is > 0 ? $"{rule.DelayMs.Value} ms" : "-";
|
||||
info["逻辑延迟"] = rule.LogicDelayMs is > 0 ? $"{rule.LogicDelayMs.Value} ms" : "-";
|
||||
return new LogicDecision(
|
||||
hotkey,
|
||||
step,
|
||||
info,
|
||||
module.Name,
|
||||
rule.DelayMs.GetValueOrDefault(),
|
||||
$"{module.Id}:{ruleIndex}",
|
||||
rule.LogicDelayMs.GetValueOrDefault());
|
||||
}
|
||||
|
||||
info["命中条件"] = "-";
|
||||
|
||||
@@ -195,7 +195,9 @@ module\ UI 可编辑模块
|
||||
模块页规则表的“条件”列为只读,单击单元格会弹出条件编辑窗口(在新行占位符上点击并保存后会自动追加一条规则)。窗口中每行是一个比较项:
|
||||
|
||||
- 连接:第二行起可选 `且`(`&&`)或 `或`(`||`),决定与上一行的组合方式。
|
||||
- 字段:下拉菜单,内容来自 `config` 目录并按模块当前选中的职业/专精过滤;技能字段以 `技能: 名称` 显示(保存为 `spells.名称`)。原条件中不在目录里的字段(如 `group.1.生命值` 或手写字段)会保留为“自定义”项,不会丢失。
|
||||
- 类型/字段:下拉菜单,状态内容来自 `config` 目录并按模块当前选中的职业/专精过滤(内部定位用的 `锚点` 不显示);技能字段以 `技能: 名称` 显示(保存为 `spells.名称`)。原条件中不在目录里的字段(如 `group.1.生命值` 或手写字段)会保留为“自定义”项,不会丢失。
|
||||
- Shigure 字段:选择 `Shigure → 延迟 (ms)` 可设置当前规则两次实际发送之间的最小间隔。例如填写 `500`,该规则持续命中时最多每 `0.5` 秒发送一次。延迟按规则独立计时,不参与真假条件判断;填 `0` 或删除该行表示不限制。
|
||||
- Shigure 字段:选择 `Shigure → 逻辑延迟 (ms)` 后,规则命中时会先立即发送按键,再暂停整个逻辑循环指定时间;暂停期间不扫描或判断规则,但触发键检测与界面刷新仍然运行。例如填写 `700`,发送后约 `0.7` 秒才恢复下一次逻辑判断。
|
||||
- 判断:数字字段提供 `==`、`!=`、`>`、`>=`、`<`、`<=`;布尔和字符串字段只提供 `==`、`!=`。
|
||||
- 值:按字段类型自适应——数字字段用数字输入框,布尔字段用 `是/否` 下拉,字符串和自定义字段用文本框。
|
||||
- 每行末尾有删除按钮,底部有“添加条件”按钮和生成表达式的实时预览。
|
||||
@@ -214,7 +216,7 @@ module\ UI 可编辑模块
|
||||
- 在规则表“目标”列的下拉里会出现动态单位名,选它即表示“对运行时解析出的那个槽位施法”。运行时若该选择器没选中任何单位(如没人掉血),该规则会被**跳过**,继续判断下一条。
|
||||
- 在“条件”里可引用:裸 `单位名`(解析到槽位即为真,可用于存在性判断)、单位的“值名称”(数值,等于该单位的生命值)、以及数量名(如 `低血量人数 >= 3`)。这些会出现在可视化条件编辑器的字段下拉中;`单位名.字段`(如 `最低血量单位.生命值 < 50`)仍可手写,但不会展开进下拉列表。
|
||||
|
||||
模块 JSON 中以 `Units`、`Counts` 数组保存(单位的值名称存为 `HealthName`,未填则省略),规则用 `UnitName` 字段引用动态单位(与原有数字 `Unit` 互斥)。旧模块没有这些键也能正常加载。名称与值名称不能为纯数字、不能含 `.` 或 `$`,且不能与现有状态/队伍字段或其它单位/数量名重复。
|
||||
模块 JSON 中以 `Units`、`Counts` 数组保存(单位的值名称存为 `HealthName`,未填则省略),规则用 `UnitName` 字段引用动态单位(与原有数字 `Unit` 互斥),可选 `DelayMs` 保存该行的发送间隔,`LogicDelayMs` 保存发送后的逻辑暂停时长。旧模块没有这些键也能正常加载。名称与值名称不能为纯数字、不能含 `.` 或 `$`,且不能与现有状态/队伍字段或其它单位/数量名重复。
|
||||
|
||||
## 默认逻辑
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Shigure;
|
||||
|
||||
public sealed class ShigureRuntime
|
||||
@@ -20,6 +22,8 @@ public sealed class ShigureRuntime
|
||||
private IReadOnlyDictionary<string, object?> _unitInfo = new Dictionary<string, object?>();
|
||||
private bool _enabled;
|
||||
private bool _clickPending;
|
||||
private readonly ConcurrentDictionary<string, DateTimeOffset> _lastRuleSentAt = new(StringComparer.Ordinal);
|
||||
private DateTimeOffset _logicPausedUntil = DateTimeOffset.MinValue;
|
||||
|
||||
public ShigureRuntime(string baseDirectory, AppOptions options, ModuleStore moduleStore)
|
||||
{
|
||||
@@ -40,6 +44,12 @@ public sealed class ShigureRuntime
|
||||
{
|
||||
_enabled = enabled;
|
||||
_clickPending = false;
|
||||
if (!enabled)
|
||||
{
|
||||
_lastRuleSentAt.Clear();
|
||||
_logicPausedUntil = DateTimeOffset.MinValue;
|
||||
}
|
||||
|
||||
_currentStep = enabled ? "手动开启" : "手动关闭";
|
||||
PublishSnapshot();
|
||||
}
|
||||
@@ -81,6 +91,8 @@ public sealed class ShigureRuntime
|
||||
_enabled = pressed;
|
||||
if (falling)
|
||||
{
|
||||
_lastRuleSentAt.Clear();
|
||||
_logicPausedUntil = DateTimeOffset.MinValue;
|
||||
_currentStep = "按住结束";
|
||||
}
|
||||
}
|
||||
@@ -90,7 +102,10 @@ public sealed class ShigureRuntime
|
||||
if (now - lastLogicAt >= _options.LogicInterval)
|
||||
{
|
||||
lastLogicAt = now;
|
||||
TickLogic();
|
||||
if (now >= _logicPausedUntil)
|
||||
{
|
||||
TickLogic();
|
||||
}
|
||||
}
|
||||
|
||||
if (now - lastRenderAt >= _options.RenderInterval)
|
||||
@@ -106,6 +121,7 @@ public sealed class ShigureRuntime
|
||||
{
|
||||
_enabled = false;
|
||||
_clickPending = false;
|
||||
_logicPausedUntil = DateTimeOffset.MinValue;
|
||||
_currentStep = "已停止";
|
||||
PublishSnapshot();
|
||||
}
|
||||
@@ -127,6 +143,12 @@ public sealed class ShigureRuntime
|
||||
default:
|
||||
_enabled = !_enabled;
|
||||
_clickPending = false;
|
||||
if (!_enabled)
|
||||
{
|
||||
_lastRuleSentAt.Clear();
|
||||
_logicPausedUntil = DateTimeOffset.MinValue;
|
||||
}
|
||||
|
||||
_currentStep = _enabled ? "逻辑开启" : "逻辑关闭";
|
||||
break;
|
||||
}
|
||||
@@ -182,9 +204,11 @@ public sealed class ShigureRuntime
|
||||
|
||||
if (_options.Mode == SendMode.Click)
|
||||
{
|
||||
if (_clickPending && !string.IsNullOrWhiteSpace(decision.Hotkey))
|
||||
if (_clickPending
|
||||
&& !string.IsNullOrWhiteSpace(decision.Hotkey)
|
||||
&& CanSend(decision, DateTimeOffset.UtcNow))
|
||||
{
|
||||
_keySender.Send(decision.Hotkey);
|
||||
SendAndPauseLogic(decision);
|
||||
}
|
||||
|
||||
_enabled = false;
|
||||
@@ -192,12 +216,42 @@ public sealed class ShigureRuntime
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(decision.Hotkey))
|
||||
if (!string.IsNullOrWhiteSpace(decision.Hotkey)
|
||||
&& CanSend(decision, DateTimeOffset.UtcNow))
|
||||
{
|
||||
_keySender.Send(decision.Hotkey);
|
||||
SendAndPauseLogic(decision);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendAndPauseLogic(LogicDecision decision)
|
||||
{
|
||||
_keySender.Send(decision.Hotkey!);
|
||||
if (decision.LogicDelayMs > 0)
|
||||
{
|
||||
_logicPausedUntil = DateTimeOffset.UtcNow.AddMilliseconds(decision.LogicDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanSend(LogicDecision decision, DateTimeOffset now)
|
||||
{
|
||||
if (decision.DelayMs <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var key = string.IsNullOrWhiteSpace(decision.RateLimitKey)
|
||||
? decision.Hotkey ?? string.Empty
|
||||
: decision.RateLimitKey;
|
||||
if (_lastRuleSentAt.TryGetValue(key, out var lastSentAt)
|
||||
&& now - lastSentAt < TimeSpan.FromMilliseconds(decision.DelayMs))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_lastRuleSentAt[key] = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void PublishSnapshot()
|
||||
{
|
||||
SnapshotUpdated?.Invoke(new RenderSnapshot(
|
||||
|
||||
@@ -19,7 +19,6 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
private readonly Button _saveButton = null!;
|
||||
|
||||
private readonly DataGridView _statesGrid = new();
|
||||
private readonly ComboBox _stateCategoryBox = new();
|
||||
private readonly DataGridViewComboBoxColumn _stateNameColumn = new();
|
||||
private readonly DataGridView _aurasGrid = new();
|
||||
private readonly ComboBox _auraBucketBox = new();
|
||||
@@ -40,6 +39,7 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
private int? _currentSpecId;
|
||||
private bool _suppressUi;
|
||||
private bool _dirty;
|
||||
private string _selectedStateCategory = ClassStateCatalog.CategoryState;
|
||||
private string _lastStateCategory = ClassStateCatalog.CategoryState;
|
||||
private string _lastAuraBucket = "player";
|
||||
|
||||
@@ -390,32 +390,7 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 44));
|
||||
|
||||
var top = new FlowLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
FlowDirection = FlowDirection.LeftToRight,
|
||||
WrapContents = false,
|
||||
BackColor = UiTheme.SurfaceRaised
|
||||
};
|
||||
top.Controls.Add(CreateMutedLabel("分类"));
|
||||
UiTheme.StyleComboBox(_stateCategoryBox);
|
||||
_stateCategoryBox.Width = 180;
|
||||
_stateCategoryBox.Items.AddRange(ClassStateCatalog.TopCategories);
|
||||
_stateCategoryBox.SelectedIndex = 0;
|
||||
_stateCategoryBox.SelectedIndexChanged += (_, _) =>
|
||||
{
|
||||
if (_suppressUi)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_statesGrid.EndEdit();
|
||||
WriteBackStatesCategory(_lastStateCategory);
|
||||
_lastStateCategory = _stateCategoryBox.SelectedItem as string ?? ClassStateCatalog.CategoryState;
|
||||
ReloadStatesGrid();
|
||||
};
|
||||
top.Controls.Add(_stateCategoryBox);
|
||||
panel.Controls.Add(top, 0, 0);
|
||||
panel.Controls.Add(BuildStateCategoryTabs(), 0, 0);
|
||||
|
||||
ConfigureGrid(_statesGrid);
|
||||
_stateNameColumn.Name = "Name";
|
||||
@@ -437,6 +412,99 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
return panel;
|
||||
}
|
||||
|
||||
private Control BuildStateCategoryTabs()
|
||||
{
|
||||
var categories = ClassStateCatalog.TopCategories;
|
||||
var tabBar = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
BackColor = UiTheme.Border,
|
||||
ColumnCount = categories.Length,
|
||||
RowCount = 1,
|
||||
Margin = new Padding(0),
|
||||
Padding = new Padding(0)
|
||||
};
|
||||
foreach (var _ in categories)
|
||||
{
|
||||
tabBar.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F / categories.Length));
|
||||
}
|
||||
|
||||
var labels = new Label[categories.Length];
|
||||
void ApplySelection()
|
||||
{
|
||||
for (var i = 0; i < labels.Length; i++)
|
||||
{
|
||||
var selected = string.Equals(categories[i], _selectedStateCategory, StringComparison.Ordinal);
|
||||
labels[i].BackColor = selected ? UiTheme.Field : UiTheme.Surface;
|
||||
labels[i].ForeColor = selected ? UiTheme.Text : UiTheme.Muted;
|
||||
labels[i].Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
void SelectCategory(string category)
|
||||
{
|
||||
if (_suppressUi
|
||||
|| string.Equals(category, _selectedStateCategory, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_statesGrid.EndEdit();
|
||||
WriteBackStatesCategory(_lastStateCategory);
|
||||
_selectedStateCategory = category;
|
||||
_lastStateCategory = category;
|
||||
ApplySelection();
|
||||
ReloadStatesGrid();
|
||||
}
|
||||
|
||||
for (var i = 0; i < categories.Length; i++)
|
||||
{
|
||||
var category = categories[i];
|
||||
var label = new Label
|
||||
{
|
||||
Text = category,
|
||||
Dock = DockStyle.Fill,
|
||||
TextAlign = ContentAlignment.MiddleCenter,
|
||||
AutoSize = false,
|
||||
BackColor = UiTheme.Surface,
|
||||
ForeColor = UiTheme.Muted,
|
||||
Cursor = Cursors.Hand,
|
||||
Margin = new Padding(i == 0 ? 0 : 1, 0, 0, 0)
|
||||
};
|
||||
label.Click += (_, _) => SelectCategory(category);
|
||||
label.MouseEnter += (_, _) =>
|
||||
{
|
||||
if (!string.Equals(category, _selectedStateCategory, StringComparison.Ordinal))
|
||||
{
|
||||
label.BackColor = UiTheme.Hover;
|
||||
}
|
||||
};
|
||||
label.MouseLeave += (_, _) =>
|
||||
{
|
||||
if (!string.Equals(category, _selectedStateCategory, StringComparison.Ordinal))
|
||||
{
|
||||
label.BackColor = UiTheme.Surface;
|
||||
}
|
||||
};
|
||||
label.Paint += (_, e) =>
|
||||
{
|
||||
if (!string.Equals(category, _selectedStateCategory, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var accent = new SolidBrush(UiTheme.Accent);
|
||||
e.Graphics.FillRectangle(accent, 8, label.Height - 3, Math.Max(0, label.Width - 16), 2);
|
||||
};
|
||||
label.SizeChanged += (_, _) => label.Invalidate();
|
||||
labels[i] = label;
|
||||
tabBar.Controls.Add(label, i, 0);
|
||||
}
|
||||
|
||||
ApplySelection();
|
||||
return tabBar;
|
||||
}
|
||||
|
||||
private Control BuildAurasPage()
|
||||
{
|
||||
var panel = new TableLayoutPanel
|
||||
@@ -663,6 +731,7 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
grid.RowHeadersVisible = false;
|
||||
grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
|
||||
grid.MultiSelect = false;
|
||||
grid.EditMode = DataGridViewEditMode.EditOnEnter;
|
||||
grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None;
|
||||
}
|
||||
|
||||
@@ -973,7 +1042,7 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
_suppressUi = true;
|
||||
try
|
||||
{
|
||||
_lastStateCategory = _stateCategoryBox.SelectedItem as string ?? ClassStateCatalog.CategoryState;
|
||||
_lastStateCategory = _selectedStateCategory;
|
||||
_lastAuraBucket = (_auraBucketBox.SelectedItem as BucketOption)?.Key ?? "player";
|
||||
FillStatesGrid();
|
||||
FillAurasGrid();
|
||||
@@ -994,7 +1063,7 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
return;
|
||||
}
|
||||
|
||||
var category = _stateCategoryBox.SelectedItem as string ?? ClassStateCatalog.CategoryState;
|
||||
var category = _selectedStateCategory;
|
||||
BindStateNameColumn(ClassStateCatalog.GetAllOptions(category));
|
||||
var storageCategory = ClassStateCatalog.GetStorageCategory(category);
|
||||
IEnumerable<string> names = _currentSpec.NestedStates
|
||||
@@ -1067,7 +1136,7 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
return;
|
||||
}
|
||||
|
||||
var category = _stateCategoryBox.SelectedItem as string ?? ClassStateCatalog.CategoryState;
|
||||
var category = _selectedStateCategory;
|
||||
var current = _statesGrid.CurrentCell.Value?.ToString()?.Trim();
|
||||
var currentRowIndex = _statesGrid.CurrentCell.RowIndex;
|
||||
var usedNames = GetUsedStateNames(category, currentRowIndex);
|
||||
|
||||
@@ -431,6 +431,7 @@ public sealed class ClassMacrosEditorControl : UserControl
|
||||
grid.RowHeadersVisible = false;
|
||||
grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
|
||||
grid.MultiSelect = false;
|
||||
grid.EditMode = DataGridViewEditMode.EditOnEnter;
|
||||
grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None;
|
||||
}
|
||||
|
||||
|
||||
@@ -149,9 +149,11 @@ public sealed class ConditionEditorForm : Form
|
||||
private static readonly string[] AllOperators = ["==", "!=", ">", ">=", "<", "<=", "in", "not in"];
|
||||
private static readonly string[] TextOperators = ["==", "!=", "in", "not in"];
|
||||
private static readonly string[] BoolOperators = ["==", "!="];
|
||||
private static readonly string[] DelayOperators = ["=="];
|
||||
private static readonly CategoryItem[] CategoryItems =
|
||||
[
|
||||
new("状态", ConditionFieldCategory.State),
|
||||
new("Shigure", ConditionFieldCategory.Shigure),
|
||||
new("光环", ConditionFieldCategory.Aura),
|
||||
new("技能", ConditionFieldCategory.Spell),
|
||||
new("动态单位", ConditionFieldCategory.DynamicUnit)
|
||||
@@ -160,6 +162,7 @@ public sealed class ConditionEditorForm : Form
|
||||
private readonly IReadOnlyList<ConditionField> _fields;
|
||||
private readonly string _originalCondition;
|
||||
private readonly bool _allowSubConditions;
|
||||
private readonly bool _allowRuleSettings;
|
||||
private readonly FlowLayoutPanel _rowsPanel = new();
|
||||
private readonly Label _previewLabel = new();
|
||||
private readonly ToolTip _previewToolTip = new();
|
||||
@@ -168,6 +171,8 @@ public sealed class ConditionEditorForm : Form
|
||||
private readonly ListBox _subList = new();
|
||||
|
||||
public string ConditionText { get; private set; } = string.Empty;
|
||||
public int? DelayMs { get; private set; }
|
||||
public int? LogicDelayMs { get; private set; }
|
||||
|
||||
// 子条件: 与主条件是「且」、子条件彼此是「或」。allowSubConditions=false(默认)时不显示该区,
|
||||
// 也用于子条件自身的嵌套编辑弹窗防止无限递归。
|
||||
@@ -177,11 +182,15 @@ public sealed class ConditionEditorForm : Form
|
||||
IReadOnlyList<ConditionField> fields,
|
||||
string? condition,
|
||||
IEnumerable<string>? subConditions = null,
|
||||
bool allowSubConditions = false)
|
||||
bool allowSubConditions = false,
|
||||
int? delayMs = null,
|
||||
int? logicDelayMs = null,
|
||||
bool allowRuleSettings = false)
|
||||
{
|
||||
_fields = fields;
|
||||
_originalCondition = condition ?? string.Empty;
|
||||
_allowSubConditions = allowSubConditions;
|
||||
_allowRuleSettings = allowRuleSettings;
|
||||
if (subConditions is not null)
|
||||
{
|
||||
_subConditions.AddRange(subConditions
|
||||
@@ -196,6 +205,24 @@ public sealed class ConditionEditorForm : Form
|
||||
AddRow(term);
|
||||
}
|
||||
|
||||
if (_allowRuleSettings && delayMs is > 0)
|
||||
{
|
||||
AddRow(new ConditionTerm(
|
||||
OrWithPrevious: false,
|
||||
ShigureConditionFields.Delay,
|
||||
"==",
|
||||
delayMs.Value.ToString(CultureInfo.InvariantCulture)));
|
||||
}
|
||||
|
||||
if (_allowRuleSettings && logicDelayMs is > 0)
|
||||
{
|
||||
AddRow(new ConditionTerm(
|
||||
OrWithPrevious: false,
|
||||
ShigureConditionFields.LogicDelay,
|
||||
"==",
|
||||
logicDelayMs.Value.ToString(CultureInfo.InvariantCulture)));
|
||||
}
|
||||
|
||||
// 空条件直接落在一条可填行上, 无需先去找"添加条件"。
|
||||
if (_rows.Count == 0)
|
||||
{
|
||||
@@ -527,6 +554,16 @@ public sealed class ConditionEditorForm : Form
|
||||
}
|
||||
|
||||
var text = ConditionExpression.Build(CollectTerms());
|
||||
if (!TryReadDelay(out var delayMs))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryReadLogicDelay(out var logicDelayMs))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅当原本有条件、现在主条件与子条件都为空时才提醒(避免把已有规则误清成"始终命中")。
|
||||
if (text.Length == 0
|
||||
&& _subConditions.Count == 0
|
||||
@@ -541,12 +578,19 @@ public sealed class ConditionEditorForm : Form
|
||||
}
|
||||
|
||||
ConditionText = text;
|
||||
DelayMs = delayMs;
|
||||
LogicDelayMs = logicDelayMs;
|
||||
DialogResult = DialogResult.OK;
|
||||
}
|
||||
|
||||
// 恰好字段与值一空一非空 → 不完整; 两者都空只是空行(静默忽略, 不算不完整)。
|
||||
private static bool IsRowIncomplete(ConditionRow row)
|
||||
{
|
||||
if (IsRuleSettingField(row.SelectedField))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var field = row.SelectedField?.Name.Trim() ?? string.Empty;
|
||||
var value = ReadRowValue(row);
|
||||
return (field.Length == 0) ^ (value.Length == 0);
|
||||
@@ -579,7 +623,9 @@ public sealed class ConditionEditorForm : Form
|
||||
|
||||
var categoryBox = new ComboBox();
|
||||
UiTheme.StyleComboBox(categoryBox);
|
||||
categoryBox.Items.AddRange(CategoryItems);
|
||||
categoryBox.Items.AddRange(CategoryItems
|
||||
.Where(item => item.Category != ConditionFieldCategory.Shigure || _allowRuleSettings)
|
||||
.ToArray());
|
||||
categoryBox.Dock = DockStyle.Fill;
|
||||
categoryBox.Margin = new Padding(0, 2, 8, 2);
|
||||
|
||||
@@ -627,11 +673,13 @@ public sealed class ConditionEditorForm : Form
|
||||
{
|
||||
PopulateFields(row, null);
|
||||
OnFieldChanged(row);
|
||||
RefreshConnectors();
|
||||
UpdatePreview();
|
||||
};
|
||||
fieldBox.SelectedIndexChanged += (_, _) =>
|
||||
{
|
||||
OnFieldChanged(row);
|
||||
RefreshConnectors();
|
||||
UpdatePreview();
|
||||
};
|
||||
opBox.SelectedIndexChanged += (_, _) =>
|
||||
@@ -659,7 +707,7 @@ public sealed class ConditionEditorForm : Form
|
||||
{
|
||||
for (var i = 0; i < _rows.Count; i++)
|
||||
{
|
||||
_rows[i].Connector.Visible = i > 0;
|
||||
_rows[i].Connector.Visible = i > 0 && !IsRuleSettingField(_rows[i].SelectedField);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -755,7 +803,10 @@ public sealed class ConditionEditorForm : Form
|
||||
private static void PopulateOps(ConditionRow row, string? desiredOp)
|
||||
{
|
||||
var field = row.SelectedField;
|
||||
var ops = field is { IsCustom: false, Type: ConditionFieldType.Bool }
|
||||
var isRuleSetting = IsRuleSettingField(field);
|
||||
var ops = isRuleSetting
|
||||
? DelayOperators
|
||||
: field is { IsCustom: false, Type: ConditionFieldType.Bool }
|
||||
? BoolOperators
|
||||
: field is { IsCustom: false, Type: ConditionFieldType.String }
|
||||
? TextOperators
|
||||
@@ -763,9 +814,10 @@ public sealed class ConditionEditorForm : Form
|
||||
|
||||
row.OpBox.Items.Clear();
|
||||
row.OpBox.Items.AddRange(ops);
|
||||
var normalizedOp = ConditionExpression.NormalizeOperator(desiredOp);
|
||||
var normalizedOp = isRuleSetting ? "==" : ConditionExpression.NormalizeOperator(desiredOp);
|
||||
var index = normalizedOp.Length == 0 ? -1 : Array.IndexOf(ops, normalizedOp);
|
||||
row.OpBox.SelectedIndex = index >= 0 ? index : 0;
|
||||
row.OpBox.Enabled = !isRuleSetting;
|
||||
}
|
||||
|
||||
private void CreateValueControl(ConditionRow row, string? rawValue, bool preserveRaw)
|
||||
@@ -780,7 +832,21 @@ public sealed class ConditionEditorForm : Form
|
||||
var field = row.SelectedField;
|
||||
var usesListValue = ConditionExpression.IsInOperator(row.OpBox.SelectedItem?.ToString());
|
||||
Control control;
|
||||
if (field is { IsCustom: false, Type: ConditionFieldType.Bool })
|
||||
if (IsRuleSettingField(field))
|
||||
{
|
||||
var numeric = new NumericUpDown
|
||||
{
|
||||
Minimum = 0,
|
||||
Maximum = int.MaxValue,
|
||||
Value = TryParseDelayText(rawValue, out var delay) ? delay : 0,
|
||||
ThousandsSeparator = true
|
||||
};
|
||||
UiTheme.StyleNumericUpDown(numeric);
|
||||
numeric.ValueChanged += (_, _) => UpdatePreview();
|
||||
numeric.TextChanged += (_, _) => UpdatePreview();
|
||||
control = numeric;
|
||||
}
|
||||
else if (field is { IsCustom: false, Type: ConditionFieldType.Bool })
|
||||
{
|
||||
var combo = new ComboBox();
|
||||
UiTheme.StyleComboBox(combo);
|
||||
@@ -847,6 +913,11 @@ public sealed class ConditionEditorForm : Form
|
||||
for (var i = 0; i < _rows.Count; i++)
|
||||
{
|
||||
var row = _rows[i];
|
||||
if (IsRuleSettingField(row.SelectedField))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var field = row.SelectedField?.Name.Trim() ?? string.Empty;
|
||||
var op = row.OpBox.SelectedItem?.ToString() ?? "==";
|
||||
var value = ReadRowValue(row);
|
||||
@@ -888,22 +959,135 @@ public sealed class ConditionEditorForm : Form
|
||||
|
||||
private void UpdatePreview()
|
||||
{
|
||||
var full = ComposePreview(ConditionExpression.Build(CollectTerms()));
|
||||
_ = TryReadDelay(out var delayMs, showWarning: false);
|
||||
_ = TryReadLogicDelay(out var logicDelayMs, showWarning: false);
|
||||
var full = ComposePreview(ConditionExpression.Build(CollectTerms()), delayMs, logicDelayMs);
|
||||
_previewLabel.Text = full.Length == 0 ? "预览: (无条件, 始终命中)" : $"预览: {full}";
|
||||
// 单行预览会被省略号截断, 悬停看完整表达式。
|
||||
_previewToolTip.SetToolTip(_previewLabel, full.Length == 0 ? string.Empty : full);
|
||||
}
|
||||
|
||||
// 把主条件文本与子条件合成为可读的整体表达式(与 ModuleRule.DescribeCondition 同形)。
|
||||
private string ComposePreview(string mainText)
|
||||
private string ComposePreview(string mainText, int? delayMs, int? logicDelayMs)
|
||||
{
|
||||
if (_subConditions.Count == 0)
|
||||
var conditionText = mainText;
|
||||
if (_subConditions.Count > 0)
|
||||
{
|
||||
return mainText;
|
||||
var any = string.Join(" | ", _subConditions);
|
||||
conditionText = mainText.Length == 0 ? $"任一({any})" : $"{mainText} 且任一({any})";
|
||||
}
|
||||
|
||||
var any = string.Join(" | ", _subConditions);
|
||||
return mainText.Length == 0 ? $"任一({any})" : $"{mainText} 且任一({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 bool TryReadDelay(out int? delayMs, bool showWarning = true)
|
||||
{
|
||||
return TryReadRuleSettingDelay(
|
||||
ShigureConditionFields.Delay,
|
||||
"延迟",
|
||||
out delayMs,
|
||||
showWarning);
|
||||
}
|
||||
|
||||
private bool TryReadLogicDelay(out int? delayMs, bool showWarning = true)
|
||||
{
|
||||
return TryReadRuleSettingDelay(
|
||||
ShigureConditionFields.LogicDelay,
|
||||
"逻辑延迟",
|
||||
out delayMs,
|
||||
showWarning);
|
||||
}
|
||||
|
||||
private bool TryReadRuleSettingDelay(
|
||||
string fieldName,
|
||||
string displayName,
|
||||
out int? delayMs,
|
||||
bool showWarning)
|
||||
{
|
||||
delayMs = null;
|
||||
var delayRows = _rows.Where(row => IsRuleSettingField(row.SelectedField, fieldName)).ToList();
|
||||
if (delayRows.Count > 1)
|
||||
{
|
||||
if (showWarning)
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"每条规则只能设置一个“{displayName}”。请删除多余的 Shigure {displayName}行。",
|
||||
"Shigure",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (delayRows.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var value = ReadRowValue(delayRows[0]);
|
||||
if (!TryParseDelayText(value, out var parsed))
|
||||
{
|
||||
if (showWarning)
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"{displayName}必须是 0 到 2147483647 之间的整数,单位为 ms。",
|
||||
"Shigure",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
delayMs = parsed > 0 ? parsed : null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsDelayField(FieldItem? field)
|
||||
{
|
||||
return IsRuleSettingField(field, ShigureConditionFields.Delay);
|
||||
}
|
||||
|
||||
private static bool IsLogicDelayField(FieldItem? field)
|
||||
{
|
||||
return IsRuleSettingField(field, ShigureConditionFields.LogicDelay);
|
||||
}
|
||||
|
||||
private static bool IsRuleSettingField(FieldItem? field)
|
||||
{
|
||||
return IsDelayField(field) || IsLogicDelayField(field);
|
||||
}
|
||||
|
||||
private static bool IsRuleSettingField(FieldItem? field, string fieldName)
|
||||
{
|
||||
return field is not null
|
||||
&& field.Category == ConditionFieldCategory.Shigure
|
||||
&& string.Equals(field.Name, fieldName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool TryParseDelayText(string? text, out int value)
|
||||
{
|
||||
return int.TryParse(
|
||||
text?.Trim(),
|
||||
NumberStyles.Integer | NumberStyles.AllowThousands,
|
||||
CultureInfo.InvariantCulture,
|
||||
out value)
|
||||
&& value >= 0;
|
||||
}
|
||||
|
||||
private static bool IsFalseText(string? value)
|
||||
|
||||
@@ -1449,7 +1449,24 @@ public sealed class ModuleEditorControl : UserControl
|
||||
Count
|
||||
}
|
||||
|
||||
private sealed record RuleRowValues(bool Enabled, string Spell, string UnitText, string Condition, IReadOnlyList<string> SubConditions);
|
||||
private sealed record RuleRowValues(
|
||||
bool Enabled,
|
||||
string Spell,
|
||||
string UnitText,
|
||||
string Condition,
|
||||
IReadOnlyList<string> SubConditions,
|
||||
int? DelayMs,
|
||||
int? LogicDelayMs);
|
||||
|
||||
private sealed class RuleRowMetadata(
|
||||
IEnumerable<string>? subConditions = null,
|
||||
int? delayMs = null,
|
||||
int? logicDelayMs = null)
|
||||
{
|
||||
public List<string> SubConditions { get; } = subConditions?.ToList() ?? new List<string>();
|
||||
public int? DelayMs { get; set; } = delayMs;
|
||||
public int? LogicDelayMs { get; set; } = logicDelayMs;
|
||||
}
|
||||
|
||||
private void ApplyColumnWidths(Dictionary<string, int>? widths)
|
||||
{
|
||||
@@ -1562,21 +1579,45 @@ public sealed class ModuleEditorControl : UserControl
|
||||
// 「条件」列在有子条件时显示成 "主条件 且任一(子1 | 子2)"; 仅改显示, 底层值仍是主条件, 不影响 ReadRules 存盘。
|
||||
if (columnName == "Condition" && !row.IsNewRow)
|
||||
{
|
||||
e.Value = DecorateCondition(e.Value?.ToString() ?? string.Empty, row.Tag as List<string>);
|
||||
var metadata = GetRuleMetadata(row);
|
||||
e.Value = DecorateCondition(
|
||||
e.Value?.ToString() ?? string.Empty,
|
||||
metadata.SubConditions,
|
||||
metadata.DelayMs,
|
||||
metadata.LogicDelayMs);
|
||||
e.FormattingApplied = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 把主条件与子条件合成可读文本(与 ModuleRule.DescribeCondition / 弹窗预览同形)。无子条件时原样返回。
|
||||
private static string DecorateCondition(string main, List<string>? subs)
|
||||
// 把主条件、子条件和规则延迟合成可读文本;仅改显示,不改变底层条件表达式。
|
||||
private static string DecorateCondition(
|
||||
string main,
|
||||
IReadOnlyList<string>? subs,
|
||||
int? delayMs,
|
||||
int? logicDelayMs)
|
||||
{
|
||||
if (subs is not { Count: > 0 })
|
||||
var conditionText = main;
|
||||
if (subs is { Count: > 0 })
|
||||
{
|
||||
return main;
|
||||
var any = string.Join(" | ", subs);
|
||||
conditionText = main.Length == 0 ? $"任一({any})" : $"{main} 且任一({any})";
|
||||
}
|
||||
|
||||
var any = string.Join(" | ", subs);
|
||||
return main.Length == 0 ? $"任一({any})" : $"{main} 且任一({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 void OnRulesGridCellPainting(object? sender, DataGridViewCellPaintingEventArgs e)
|
||||
@@ -1678,8 +1719,13 @@ public sealed class ModuleEditorControl : UserControl
|
||||
var text = CellText(row, columnName);
|
||||
if (columnName == "Condition")
|
||||
{
|
||||
// 提示与裁剪检测都用合成后的完整文本(含子条件), 与单元格显示一致。
|
||||
text = DecorateCondition(text, row.Tag as List<string>);
|
||||
// 提示与裁剪检测都用合成后的完整文本(含子条件和延迟), 与单元格显示一致。
|
||||
var metadata = GetRuleMetadata(row);
|
||||
text = DecorateCondition(
|
||||
text,
|
||||
metadata.SubConditions,
|
||||
metadata.DelayMs,
|
||||
metadata.LogicDelayMs);
|
||||
if (text.Length == 0)
|
||||
{
|
||||
return "点击编辑条件 (当前: 始终命中)";
|
||||
@@ -2040,7 +2086,14 @@ public sealed class ModuleEditorControl : UserControl
|
||||
return;
|
||||
}
|
||||
|
||||
InsertRuleAfter(rowIndex, new RuleRowValues(true, string.Empty, string.Empty, string.Empty, Array.Empty<string>()));
|
||||
InsertRuleAfter(rowIndex, new RuleRowValues(
|
||||
true,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
Array.Empty<string>(),
|
||||
null,
|
||||
null));
|
||||
}
|
||||
|
||||
private void InsertRuleAfter(int rowIndex, RuleRowValues values)
|
||||
@@ -2250,8 +2303,10 @@ public sealed class ModuleEditorControl : UserControl
|
||||
CellText(row, "Spell"),
|
||||
CellText(row, "Unit"),
|
||||
CellText(row, "Condition"),
|
||||
// 子条件挂在 row.Tag, 随行一起被移动/拖拽/复制搬运。
|
||||
row.Tag as List<string> ?? new List<string>());
|
||||
// 子条件和延迟挂在 row.Tag, 随行一起被移动/拖拽/复制搬运。
|
||||
GetRuleMetadata(row).SubConditions,
|
||||
GetRuleMetadata(row).DelayMs,
|
||||
GetRuleMetadata(row).LogicDelayMs);
|
||||
}
|
||||
|
||||
private void WriteRuleRow(DataGridViewRow row, RuleRowValues values)
|
||||
@@ -2260,7 +2315,7 @@ public sealed class ModuleEditorControl : UserControl
|
||||
EnsureComboItem(_spellColumn, values.Spell);
|
||||
row.Cells["Spell"].Value = values.Spell;
|
||||
row.Cells["Condition"].Value = values.Condition;
|
||||
row.Tag = new List<string>(values.SubConditions);
|
||||
row.Tag = new RuleRowMetadata(values.SubConditions, values.DelayMs, values.LogicDelayMs);
|
||||
RebuildUnitCell(row, values.UnitText);
|
||||
}
|
||||
|
||||
@@ -2268,10 +2323,17 @@ public sealed class ModuleEditorControl : UserControl
|
||||
{
|
||||
var row = _rulesGrid.Rows[rowIndex];
|
||||
var current = row.IsNewRow ? string.Empty : CellText(row, "Condition");
|
||||
var currentSubs = row.IsNewRow ? null : row.Tag as List<string>;
|
||||
var fields = BuildConditionFields();
|
||||
var currentMetadata = row.IsNewRow ? new RuleRowMetadata() : GetRuleMetadata(row);
|
||||
var fields = BuildConditionFields(includeRuleSettings: true);
|
||||
|
||||
using var editor = new ConditionEditorForm(fields, current, currentSubs, allowSubConditions: true);
|
||||
using var editor = new ConditionEditorForm(
|
||||
fields,
|
||||
current,
|
||||
currentMetadata.SubConditions,
|
||||
allowSubConditions: true,
|
||||
delayMs: currentMetadata.DelayMs,
|
||||
logicDelayMs: currentMetadata.LogicDelayMs,
|
||||
allowRuleSettings: true);
|
||||
if (editor.ShowDialog(FindForm()) != DialogResult.OK)
|
||||
{
|
||||
return;
|
||||
@@ -2281,29 +2343,53 @@ public sealed class ModuleEditorControl : UserControl
|
||||
if (row.IsNewRow)
|
||||
{
|
||||
// 新行占位符不能直接赋值, 改为追加一行(主条件或子条件任一非空即可)。
|
||||
if (!string.IsNullOrWhiteSpace(editor.ConditionText) || subs.Count > 0)
|
||||
if (!string.IsNullOrWhiteSpace(editor.ConditionText)
|
||||
|| subs.Count > 0
|
||||
|| editor.DelayMs is > 0
|
||||
|| editor.LogicDelayMs is > 0)
|
||||
{
|
||||
var index = _rulesGrid.Rows.Add(true, string.Empty, string.Empty, editor.ConditionText);
|
||||
_rulesGrid.Rows[index].Tag = subs;
|
||||
_rulesGrid.Rows[index].Tag = new RuleRowMetadata(
|
||||
subs,
|
||||
editor.DelayMs,
|
||||
editor.LogicDelayMs);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
row.Cells["Condition"].Value = editor.ConditionText;
|
||||
row.Tag = subs;
|
||||
row.Tag = new RuleRowMetadata(subs, editor.DelayMs, editor.LogicDelayMs);
|
||||
// 让「条件」列的装饰显示(主条件 且任一(…))立即刷新。
|
||||
_rulesGrid.InvalidateRow(rowIndex);
|
||||
}
|
||||
|
||||
// 条件字段 = 状态/技能字段 + 每个动态单位的裸名(存在)和值名称 + 每个数量名。
|
||||
private IReadOnlyList<ConditionField> BuildConditionFields()
|
||||
private IReadOnlyList<ConditionField> BuildConditionFields(bool includeRuleSettings = false)
|
||||
{
|
||||
var classId = ReadMatchCombo(_classBox);
|
||||
var specId = ReadMatchCombo(_specBox);
|
||||
var fields = new List<ConditionField>(_fieldCatalog.GetFields(classId, specId));
|
||||
var seen = new HashSet<string>(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))
|
||||
@@ -2470,9 +2556,10 @@ public sealed class ModuleEditorControl : UserControl
|
||||
EnsureComboItem(_spellColumn, rule.Spell);
|
||||
// 先加行(目标先留空), 再按技能重建目标选项并写回目标值, 避免值不在选项内被吞掉。
|
||||
var index = _rulesGrid.Rows.Add(rule.Enabled, rule.Spell, string.Empty, rule.Condition);
|
||||
_rulesGrid.Rows[index].Tag = rule.SubConditions is null
|
||||
? new List<string>()
|
||||
: new List<string>(rule.SubConditions);
|
||||
_rulesGrid.Rows[index].Tag = new RuleRowMetadata(
|
||||
rule.SubConditions,
|
||||
rule.DelayMs,
|
||||
rule.LogicDelayMs);
|
||||
RebuildUnitCell(_rulesGrid.Rows[index], unitText);
|
||||
}
|
||||
}
|
||||
@@ -2719,16 +2806,20 @@ public sealed class ModuleEditorControl : UserControl
|
||||
var condition = CellText(row, "Condition");
|
||||
var spell = CellText(row, "Spell");
|
||||
var unitText = CellText(row, "Unit");
|
||||
var metadata = GetRuleMetadata(row);
|
||||
if (string.IsNullOrWhiteSpace(condition)
|
||||
&& string.IsNullOrWhiteSpace(spell)
|
||||
&& string.IsNullOrWhiteSpace(unitText))
|
||||
&& string.IsNullOrWhiteSpace(unitText)
|
||||
&& metadata.SubConditions.Count == 0
|
||||
&& metadata.DelayMs is not > 0
|
||||
&& metadata.LogicDelayMs is not > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 目标文本命中已定义动态单位名 → UnitName; 否则按数字 → Unit; 都不是则留空。
|
||||
var isDynamic = unitNames.Contains(unitText);
|
||||
var subs = (row.Tag as List<string>)?
|
||||
var subs = metadata.SubConditions
|
||||
.Select(sub => sub?.Trim() ?? string.Empty)
|
||||
.Where(sub => sub.Length > 0)
|
||||
.ToList();
|
||||
@@ -2741,13 +2832,26 @@ public sealed class ModuleEditorControl : UserControl
|
||||
Spell = spell,
|
||||
Hotkey = string.Empty,
|
||||
Step = string.Empty,
|
||||
SubConditions = subs is { Count: > 0 } ? subs : null
|
||||
SubConditions = subs is { Count: > 0 } ? subs : null,
|
||||
DelayMs = metadata.DelayMs,
|
||||
LogicDelayMs = metadata.LogicDelayMs
|
||||
});
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
private static RuleRowMetadata GetRuleMetadata(DataGridViewRow row)
|
||||
{
|
||||
return row.Tag switch
|
||||
{
|
||||
RuleRowMetadata metadata => metadata,
|
||||
// 兼容本次升级前已经加载到控件中的旧 Tag 结构。
|
||||
List<string> subConditions => new RuleRowMetadata(subConditions),
|
||||
_ => new RuleRowMetadata()
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddMatchField(TableLayoutPanel row, string label, ComboBox box, int column)
|
||||
{
|
||||
row.Controls.Add(CreateLabel(label), column, 0);
|
||||
|
||||
Reference in New Issue
Block a user