mirror of
https://github.com/waynebian01/Shigure.git
synced 2026-09-03 07:15:24 +08:00
1160 lines
39 KiB
C#
1160 lines
39 KiB
C#
using System.Drawing;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace Shigure;
|
|
|
|
/// <summary>
|
|
/// 单个比较项: 与上一项的连接方式(且/或)、字段、判断符号、值。
|
|
/// </summary>
|
|
public sealed record ConditionTerm(bool OrWithPrevious, string Field, string Op, string Value);
|
|
|
|
/// <summary>
|
|
/// 条件表达式文本与比较项列表之间的双向转换。
|
|
/// 语法与 ModuleConditionEvaluator 保持一致: && 优先于 ||, 不支持括号嵌套。
|
|
/// </summary>
|
|
public static class ConditionExpression
|
|
{
|
|
private static readonly Regex InRegex = new(
|
|
@"^\s*(?<field>.+?)\s+(?<op>not\s+in|in)\s*\((?<value>.*?)\)\s*$",
|
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
|
|
private static readonly Regex ComparisonRegex = new(
|
|
@"^\s*(?<field>.+?)\s*(?<op>==|!=|>=|<=|>|<)\s*(?<value>.+?)\s*$",
|
|
RegexOptions.Compiled);
|
|
|
|
public static List<ConditionTerm> Parse(string? expression)
|
|
{
|
|
var terms = new List<ConditionTerm>();
|
|
if (string.IsNullOrWhiteSpace(expression))
|
|
{
|
|
return terms;
|
|
}
|
|
|
|
foreach (var orPart in Regex.Split(expression, @"\s*\|\|\s*"))
|
|
{
|
|
var firstInGroup = true;
|
|
foreach (var andPart in Regex.Split(orPart, @"\s*&&\s*"))
|
|
{
|
|
var trimmed = andPart.Trim();
|
|
if (trimmed.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
terms.Add(ParseTerm(trimmed, orWithPrevious: firstInGroup && terms.Count > 0));
|
|
firstInGroup = false;
|
|
}
|
|
}
|
|
|
|
return terms;
|
|
}
|
|
|
|
public static string Build(IEnumerable<ConditionTerm> terms)
|
|
{
|
|
var builder = new StringBuilder();
|
|
foreach (var term in terms)
|
|
{
|
|
var op = NormalizeOperator(term.Op);
|
|
var value = IsInOperator(op) ? NormalizeInValue(term.Value) : term.Value.Trim();
|
|
if (string.IsNullOrWhiteSpace(term.Field) || string.IsNullOrWhiteSpace(value))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (builder.Length > 0)
|
|
{
|
|
builder.Append(term.OrWithPrevious ? " || " : " && ");
|
|
}
|
|
|
|
builder.Append(term.Field).Append(' ').Append(op).Append(' ');
|
|
if (IsInOperator(op))
|
|
{
|
|
builder.Append('(').Append(value).Append(')');
|
|
}
|
|
else
|
|
{
|
|
builder.Append(value);
|
|
}
|
|
}
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
private static ConditionTerm ParseTerm(string term, bool orWithPrevious)
|
|
{
|
|
var inMatch = InRegex.Match(term);
|
|
if (inMatch.Success)
|
|
{
|
|
return new ConditionTerm(
|
|
orWithPrevious,
|
|
inMatch.Groups["field"].Value.Trim(),
|
|
NormalizeOperator(inMatch.Groups["op"].Value),
|
|
NormalizeInValue(inMatch.Groups["value"].Value));
|
|
}
|
|
|
|
var comparison = ComparisonRegex.Match(term);
|
|
if (comparison.Success)
|
|
{
|
|
return new ConditionTerm(
|
|
orWithPrevious,
|
|
comparison.Groups["field"].Value.Trim(),
|
|
comparison.Groups["op"].Value,
|
|
comparison.Groups["value"].Value.Trim());
|
|
}
|
|
|
|
// 布尔简写: `字段` 表示为真, `!字段` 表示为假, 归一化为显式比较。
|
|
return term.StartsWith('!')
|
|
? new ConditionTerm(orWithPrevious, term[1..].Trim(), "==", "false")
|
|
: new ConditionTerm(orWithPrevious, term, "==", "true");
|
|
}
|
|
|
|
public static bool IsInOperator(string? op)
|
|
{
|
|
return NormalizeOperator(op) is "in" or "not in";
|
|
}
|
|
|
|
public static string NormalizeOperator(string? op)
|
|
{
|
|
return Regex.Replace(op?.Trim().ToLowerInvariant() ?? string.Empty, @"\s+", " ");
|
|
}
|
|
|
|
public static string NormalizeInValue(string? value)
|
|
{
|
|
var text = value?.Trim() ?? string.Empty;
|
|
if (text.StartsWith('(') && text.EndsWith(')') && text.Length >= 2)
|
|
{
|
|
text = text[1..^1].Trim();
|
|
}
|
|
|
|
return text;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 条件可视化编辑弹窗: 每行一个比较项(连接/类型/字段/判断/值/删除),
|
|
/// 字段下拉按类型过滤, 值控件按字段类型自适应。
|
|
/// </summary>
|
|
public sealed class ConditionEditorForm : Form
|
|
{
|
|
private const int ConnectorWidth = 100;
|
|
private const int CategoryWidth = 140;
|
|
private const int FieldWidth = 230;
|
|
private const int OpWidth = 90;
|
|
private const int ValueWidth = 130;
|
|
private const int DeleteWidth = 36;
|
|
private const int RowTotalWidth = ConnectorWidth + CategoryWidth + FieldWidth + OpWidth + ValueWidth + DeleteWidth;
|
|
|
|
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)
|
|
];
|
|
|
|
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();
|
|
private readonly List<ConditionRow> _rows = new();
|
|
private readonly List<string> _subConditions = new();
|
|
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(默认)时不显示该区,
|
|
// 也用于子条件自身的嵌套编辑弹窗防止无限递归。
|
|
public IReadOnlyList<string> SubConditions => _subConditions;
|
|
|
|
public ConditionEditorForm(
|
|
IReadOnlyList<ConditionField> fields,
|
|
string? condition,
|
|
IEnumerable<string>? subConditions = null,
|
|
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
|
|
.Select(sub => sub?.Trim() ?? string.Empty)
|
|
.Where(sub => sub.Length > 0));
|
|
}
|
|
|
|
InitializeComponent();
|
|
|
|
foreach (var term in ConditionExpression.Parse(condition))
|
|
{
|
|
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)
|
|
{
|
|
AddRow(null);
|
|
}
|
|
|
|
RefreshConnectors();
|
|
UpdatePreview();
|
|
}
|
|
|
|
protected override void OnHandleCreated(EventArgs e)
|
|
{
|
|
base.OnHandleCreated(e);
|
|
UiTheme.ApplyDarkTitleBar(this);
|
|
}
|
|
|
|
private void InitializeComponent()
|
|
{
|
|
Text = "编辑条件";
|
|
Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Regular, GraphicsUnit.Point);
|
|
BackColor = UiTheme.Background;
|
|
ForeColor = UiTheme.Text;
|
|
// 加一个滚动条宽度, 避免行数多时垂直滚动条盖住每行的 ✕ 删除按钮。
|
|
// 子条件区会额外占高度, 允许时把窗口加高, 给主条件行留出空间。
|
|
var initialHeight = _allowSubConditions ? 650 : 460;
|
|
ClientSize = new Size(RowTotalWidth + 50 + SystemInformation.VerticalScrollBarWidth, initialHeight);
|
|
FormBorderStyle = FormBorderStyle.Sizable;
|
|
MaximizeBox = false;
|
|
MinimizeBox = false;
|
|
ShowInTaskbar = false;
|
|
StartPosition = FormStartPosition.CenterParent;
|
|
// 允许纵向放大以一次看到更多条件行; 不小于初始尺寸。
|
|
MinimumSize = Size;
|
|
|
|
var root = new TableLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
BackColor = UiTheme.Background,
|
|
Padding = new Padding(12, 10, 12, 10),
|
|
ColumnCount = 1
|
|
};
|
|
Controls.Add(root);
|
|
|
|
var rowIndex = 0;
|
|
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 26));
|
|
root.Controls.Add(BuildHeaderRow(), 0, rowIndex++);
|
|
|
|
_rowsPanel.Dock = DockStyle.Fill;
|
|
_rowsPanel.BackColor = UiTheme.SurfaceRaised;
|
|
_rowsPanel.FlowDirection = FlowDirection.TopDown;
|
|
_rowsPanel.WrapContents = false;
|
|
_rowsPanel.AutoScroll = true;
|
|
_rowsPanel.Margin = new Padding(0, 4, 0, 6);
|
|
_rowsPanel.Padding = new Padding(8, 6, 8, 6);
|
|
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
|
root.Controls.Add(_rowsPanel, 0, rowIndex++);
|
|
|
|
// 「添加条件」紧贴主条件行下方, 明确它作用于上面的主条件(而非子条件)。
|
|
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 38));
|
|
root.Controls.Add(BuildAddConditionRow(), 0, rowIndex++);
|
|
|
|
if (_allowSubConditions)
|
|
{
|
|
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 172));
|
|
root.Controls.Add(BuildSubConditionsPanel(), 0, rowIndex++);
|
|
}
|
|
|
|
_previewLabel.Dock = DockStyle.Fill;
|
|
_previewLabel.ForeColor = UiTheme.Muted;
|
|
_previewLabel.TextAlign = ContentAlignment.MiddleLeft;
|
|
_previewLabel.AutoEllipsis = true;
|
|
_previewLabel.Margin = new Padding(0);
|
|
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 28));
|
|
root.Controls.Add(_previewLabel, 0, rowIndex++);
|
|
|
|
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
|
|
root.Controls.Add(BuildActionRow(), 0, rowIndex++);
|
|
root.RowCount = rowIndex;
|
|
}
|
|
|
|
// 子条件区: 标题 + 暗色列表 + 添加/编辑/删除。每条子条件本身也是一条完整条件,
|
|
// 通过嵌套的(无子条件区的)条件编辑弹窗来编辑。
|
|
private Control BuildSubConditionsPanel()
|
|
{
|
|
var panel = new TableLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
BackColor = UiTheme.Background,
|
|
Margin = new Padding(0, 4, 0, 4),
|
|
ColumnCount = 2,
|
|
RowCount = 2
|
|
};
|
|
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
|
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 116));
|
|
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 22));
|
|
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
|
|
|
var title = new Label
|
|
{
|
|
Text = "子条件 (满足任一即可, 与主条件为「且」关系)",
|
|
Dock = DockStyle.Fill,
|
|
ForeColor = UiTheme.Muted,
|
|
TextAlign = ContentAlignment.MiddleLeft,
|
|
Margin = new Padding(0)
|
|
};
|
|
panel.Controls.Add(title, 0, 0);
|
|
panel.SetColumnSpan(title, 2);
|
|
|
|
_subList.Dock = DockStyle.Fill;
|
|
_subList.BackColor = UiTheme.Field;
|
|
_subList.ForeColor = UiTheme.Text;
|
|
_subList.BorderStyle = BorderStyle.FixedSingle;
|
|
_subList.IntegralHeight = false;
|
|
_subList.Margin = new Padding(0, 0, 8, 0);
|
|
_subList.DoubleClick += (_, _) => EditSelectedSubCondition();
|
|
panel.Controls.Add(_subList, 0, 1);
|
|
|
|
var buttons = new FlowLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
FlowDirection = FlowDirection.TopDown,
|
|
WrapContents = false,
|
|
BackColor = UiTheme.Background,
|
|
Margin = new Padding(0)
|
|
};
|
|
buttons.Controls.Add(CreateSubButton("添加子条件", UiTheme.Text, AddSubCondition));
|
|
buttons.Controls.Add(CreateSubButton("编辑", UiTheme.Text, EditSelectedSubCondition));
|
|
buttons.Controls.Add(CreateSubButton("删除", UiTheme.Danger, DeleteSelectedSubCondition));
|
|
panel.Controls.Add(buttons, 1, 1);
|
|
|
|
RefreshSubList();
|
|
return panel;
|
|
}
|
|
|
|
private static Button CreateSubButton(string text, Color foreColor, Action onClick)
|
|
{
|
|
var button = UiTheme.CreateButton(text, UiTheme.Field, foreColor);
|
|
button.Width = 108;
|
|
button.Height = 30;
|
|
button.Margin = new Padding(0, 0, 0, 6);
|
|
button.Click += (_, _) => onClick();
|
|
return button;
|
|
}
|
|
|
|
private void AddSubCondition()
|
|
{
|
|
var text = PromptSubCondition(string.Empty);
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_subConditions.Add(text.Trim());
|
|
RefreshSubList();
|
|
_subList.SelectedIndex = _subConditions.Count - 1;
|
|
UpdatePreview();
|
|
}
|
|
|
|
private void EditSelectedSubCondition()
|
|
{
|
|
var index = _subList.SelectedIndex;
|
|
if (index < 0 || index >= _subConditions.Count)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var text = PromptSubCondition(_subConditions[index]);
|
|
if (text is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// 编辑后清空 = 删除该子条件。
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
_subConditions.RemoveAt(index);
|
|
}
|
|
else
|
|
{
|
|
_subConditions[index] = text.Trim();
|
|
}
|
|
|
|
RefreshSubList();
|
|
UpdatePreview();
|
|
}
|
|
|
|
private void DeleteSelectedSubCondition()
|
|
{
|
|
var index = _subList.SelectedIndex;
|
|
if (index < 0 || index >= _subConditions.Count)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_subConditions.RemoveAt(index);
|
|
RefreshSubList();
|
|
UpdatePreview();
|
|
}
|
|
|
|
// 返回 null = 用户取消; 空串 = 用户清空了条件(编辑时表示删除)。
|
|
private string? PromptSubCondition(string current)
|
|
{
|
|
using var editor = new ConditionEditorForm(_fields, current);
|
|
return editor.ShowDialog(this) == DialogResult.OK ? editor.ConditionText : null;
|
|
}
|
|
|
|
private void RefreshSubList()
|
|
{
|
|
_subList.BeginUpdate();
|
|
_subList.Items.Clear();
|
|
foreach (var sub in _subConditions)
|
|
{
|
|
_subList.Items.Add(sub);
|
|
}
|
|
|
|
_subList.EndUpdate();
|
|
}
|
|
|
|
private Control BuildHeaderRow()
|
|
{
|
|
var header = new TableLayoutPanel
|
|
{
|
|
Width = RowTotalWidth,
|
|
Height = 24,
|
|
BackColor = UiTheme.Background,
|
|
Margin = new Padding(0),
|
|
ColumnCount = 6,
|
|
RowCount = 1
|
|
};
|
|
header.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, ConnectorWidth));
|
|
header.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, CategoryWidth));
|
|
header.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, FieldWidth));
|
|
header.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, OpWidth));
|
|
header.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, ValueWidth));
|
|
header.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, DeleteWidth));
|
|
|
|
header.Controls.Add(CreateHeaderLabel("连接"), 0, 0);
|
|
header.Controls.Add(CreateHeaderLabel("类型"), 1, 0);
|
|
header.Controls.Add(CreateHeaderLabel("字段"), 2, 0);
|
|
header.Controls.Add(CreateHeaderLabel("判断"), 3, 0);
|
|
header.Controls.Add(CreateHeaderLabel("值"), 4, 0);
|
|
header.Controls.Add(CreateHeaderLabel(string.Empty), 5, 0);
|
|
return header;
|
|
}
|
|
|
|
// 「添加条件」按钮单独成行, 放在主条件行下方、子条件区上方。
|
|
private Control BuildAddConditionRow()
|
|
{
|
|
var panel = new FlowLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
FlowDirection = FlowDirection.LeftToRight,
|
|
WrapContents = false,
|
|
BackColor = UiTheme.Background,
|
|
Margin = new Padding(0, 2, 0, 2)
|
|
};
|
|
|
|
var addButton = UiTheme.CreateButton("添加条件", UiTheme.Field, UiTheme.Text);
|
|
addButton.Width = 96;
|
|
addButton.Height = 30;
|
|
addButton.Margin = new Padding(0, 2, 0, 0);
|
|
addButton.Click += (_, _) =>
|
|
{
|
|
var row = AddRow(null);
|
|
RefreshConnectors();
|
|
UpdatePreview();
|
|
_rowsPanel.ScrollControlIntoView(row.Panel);
|
|
row.CategoryBox.Focus();
|
|
};
|
|
panel.Controls.Add(addButton);
|
|
return panel;
|
|
}
|
|
|
|
private Control BuildActionRow()
|
|
{
|
|
var row = new TableLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
BackColor = UiTheme.Background,
|
|
Margin = new Padding(0),
|
|
ColumnCount = 2,
|
|
RowCount = 1
|
|
};
|
|
row.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
|
row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 170));
|
|
|
|
var rightButtons = new FlowLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
FlowDirection = FlowDirection.RightToLeft,
|
|
WrapContents = false,
|
|
BackColor = UiTheme.Background,
|
|
Margin = new Padding(0)
|
|
};
|
|
|
|
var okButton = UiTheme.CreateButton("确定", UiTheme.Accent, Color.Black);
|
|
okButton.Width = 72;
|
|
okButton.Height = 30;
|
|
okButton.Margin = new Padding(6, 4, 0, 0);
|
|
okButton.Click += (_, _) => TryConfirm();
|
|
|
|
var cancelButton = UiTheme.CreateButton("取消", UiTheme.Field, UiTheme.Text);
|
|
cancelButton.Width = 72;
|
|
cancelButton.Height = 30;
|
|
cancelButton.Margin = new Padding(6, 4, 0, 0);
|
|
cancelButton.Click += (_, _) => DialogResult = DialogResult.Cancel;
|
|
|
|
rightButtons.Controls.Add(okButton);
|
|
rightButtons.Controls.Add(cancelButton);
|
|
row.Controls.Add(rightButtons, 1, 0);
|
|
|
|
AcceptButton = okButton;
|
|
CancelButton = cancelButton;
|
|
return row;
|
|
}
|
|
|
|
// 提交前对两类静默丢失给出确认: 不完整行被忽略、或结果为空会把条件清成"始终命中"。
|
|
private void TryConfirm()
|
|
{
|
|
var incomplete = _rows.Count(IsRowIncomplete);
|
|
if (incomplete > 0
|
|
&& MessageBox.Show(
|
|
$"有 {incomplete} 行不完整(字段或值为空), 将被忽略。继续?",
|
|
"Shigure",
|
|
MessageBoxButtons.OKCancel,
|
|
MessageBoxIcon.Warning) != DialogResult.OK)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var text = ConditionExpression.Build(CollectTerms());
|
|
if (!TryReadDelay(out var delayMs))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!TryReadLogicDelay(out var logicDelayMs))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// 仅当原本有条件、现在主条件与子条件都为空时才提醒(避免把已有规则误清成"始终命中")。
|
|
if (text.Length == 0
|
|
&& _subConditions.Count == 0
|
|
&& !string.IsNullOrWhiteSpace(_originalCondition)
|
|
&& MessageBox.Show(
|
|
"当前条件为空, 将清除该规则的条件(始终命中)。继续?",
|
|
"Shigure",
|
|
MessageBoxButtons.OKCancel,
|
|
MessageBoxIcon.Warning) != DialogResult.OK)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private ConditionRow AddRow(ConditionTerm? term)
|
|
{
|
|
var panel = new TableLayoutPanel
|
|
{
|
|
Width = RowTotalWidth,
|
|
Height = 34,
|
|
BackColor = UiTheme.SurfaceRaised,
|
|
Margin = new Padding(0, 1, 0, 5),
|
|
ColumnCount = 6,
|
|
RowCount = 1
|
|
};
|
|
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, ConnectorWidth));
|
|
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, CategoryWidth));
|
|
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, FieldWidth));
|
|
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, OpWidth));
|
|
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, ValueWidth));
|
|
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, DeleteWidth));
|
|
|
|
var connectorBox = new ComboBox();
|
|
UiTheme.StyleComboBox(connectorBox);
|
|
connectorBox.Items.AddRange(["且", "或"]);
|
|
connectorBox.SelectedIndex = term?.OrWithPrevious == true ? 1 : 0;
|
|
connectorBox.Dock = DockStyle.Fill;
|
|
connectorBox.Margin = new Padding(0, 2, 8, 2);
|
|
|
|
var categoryBox = new ComboBox();
|
|
UiTheme.StyleComboBox(categoryBox);
|
|
categoryBox.Items.AddRange(CategoryItems
|
|
.Where(item => item.Category != ConditionFieldCategory.Shigure || _allowRuleSettings)
|
|
.ToArray());
|
|
categoryBox.Dock = DockStyle.Fill;
|
|
categoryBox.Margin = new Padding(0, 2, 8, 2);
|
|
|
|
var fieldBox = new ComboBox();
|
|
UiTheme.StyleComboBox(fieldBox);
|
|
fieldBox.Dock = DockStyle.Fill;
|
|
fieldBox.Margin = new Padding(0, 2, 8, 2);
|
|
fieldBox.AutoCompleteSource = AutoCompleteSource.ListItems;
|
|
fieldBox.AutoCompleteMode = AutoCompleteMode.SuggestAppend;
|
|
|
|
var opBox = new ComboBox();
|
|
UiTheme.StyleComboBox(opBox);
|
|
opBox.Dock = DockStyle.Fill;
|
|
opBox.Margin = new Padding(0, 2, 8, 2);
|
|
|
|
var valueHost = new Panel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
BackColor = UiTheme.SurfaceRaised,
|
|
Margin = new Padding(0, 2, 8, 2)
|
|
};
|
|
|
|
var deleteButton = UiTheme.CreateButton("✕", UiTheme.Field, UiTheme.Danger);
|
|
deleteButton.AutoSize = false;
|
|
deleteButton.Width = DeleteWidth - 6;
|
|
deleteButton.Height = 30;
|
|
deleteButton.Margin = new Padding(0, 2, 0, 2);
|
|
deleteButton.Padding = new Padding(0);
|
|
|
|
panel.Controls.Add(connectorBox, 0, 0);
|
|
panel.Controls.Add(categoryBox, 1, 0);
|
|
panel.Controls.Add(fieldBox, 2, 0);
|
|
panel.Controls.Add(opBox, 3, 0);
|
|
panel.Controls.Add(valueHost, 4, 0);
|
|
panel.Controls.Add(deleteButton, 5, 0);
|
|
|
|
var row = new ConditionRow(panel, connectorBox, categoryBox, fieldBox, opBox, valueHost);
|
|
SelectCategory(row, ResolveCategory(term?.Field));
|
|
PopulateFields(row, term?.Field);
|
|
PopulateOps(row, term?.Op);
|
|
CreateValueControl(row, term?.Value, preserveRaw: true);
|
|
|
|
connectorBox.SelectedIndexChanged += (_, _) => UpdatePreview();
|
|
categoryBox.SelectedIndexChanged += (_, _) =>
|
|
{
|
|
PopulateFields(row, null);
|
|
OnFieldChanged(row);
|
|
RefreshConnectors();
|
|
UpdatePreview();
|
|
};
|
|
fieldBox.SelectedIndexChanged += (_, _) =>
|
|
{
|
|
OnFieldChanged(row);
|
|
RefreshConnectors();
|
|
UpdatePreview();
|
|
};
|
|
opBox.SelectedIndexChanged += (_, _) =>
|
|
{
|
|
OnOperatorChanged(row);
|
|
UpdatePreview();
|
|
};
|
|
deleteButton.Click += (_, _) => RemoveRow(row);
|
|
|
|
_rows.Add(row);
|
|
_rowsPanel.Controls.Add(panel);
|
|
return row;
|
|
}
|
|
|
|
private void RemoveRow(ConditionRow row)
|
|
{
|
|
_rows.Remove(row);
|
|
_rowsPanel.Controls.Remove(row.Panel);
|
|
row.Panel.Dispose();
|
|
RefreshConnectors();
|
|
UpdatePreview();
|
|
}
|
|
|
|
private void RefreshConnectors()
|
|
{
|
|
for (var i = 0; i < _rows.Count; i++)
|
|
{
|
|
_rows[i].Connector.Visible = i > 0 && !IsRuleSettingField(_rows[i].SelectedField);
|
|
}
|
|
}
|
|
|
|
private void PopulateFields(ConditionRow row, string? currentField)
|
|
{
|
|
var fieldBox = row.FieldBox;
|
|
var category = row.SelectedCategory?.Category ?? ConditionFieldCategory.State;
|
|
fieldBox.DropDownStyle = ComboBoxStyle.DropDownList;
|
|
fieldBox.Items.Clear();
|
|
foreach (var field in _fields.Where(field => field.Category == category))
|
|
{
|
|
fieldBox.Items.Add(new FieldItem(field.Name, field.DisplayName, field.Type, field.Category, IsCustom: false));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(currentField))
|
|
{
|
|
var index = FindFieldIndex(fieldBox, currentField);
|
|
if (index < 0)
|
|
{
|
|
// 目录里没有的字段(如 group.* 或手写字段)保留为自定义项, 避免丢失原条件。
|
|
fieldBox.Items.Add(new FieldItem(currentField, $"{currentField} (自定义)", ConditionFieldType.Int, category, IsCustom: true));
|
|
|
|
index = fieldBox.Items.Count - 1;
|
|
}
|
|
|
|
fieldBox.SelectedIndex = index;
|
|
return;
|
|
}
|
|
|
|
if (fieldBox.Items.Count > 0)
|
|
{
|
|
fieldBox.SelectedIndex = 0;
|
|
}
|
|
}
|
|
|
|
private ConditionFieldCategory ResolveCategory(string? fieldName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(fieldName))
|
|
{
|
|
return ConditionFieldCategory.State;
|
|
}
|
|
|
|
var field = _fields.FirstOrDefault(field =>
|
|
string.Equals(field.Name, fieldName, StringComparison.OrdinalIgnoreCase));
|
|
if (field is not null)
|
|
{
|
|
return field.Category;
|
|
}
|
|
|
|
if (fieldName.StartsWith("auras.", StringComparison.OrdinalIgnoreCase)
|
|
|| fieldName.StartsWith("aura.", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return ConditionFieldCategory.Aura;
|
|
}
|
|
|
|
if (fieldName.StartsWith("spells.", StringComparison.OrdinalIgnoreCase)
|
|
|| fieldName.StartsWith("spell.", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return ConditionFieldCategory.Spell;
|
|
}
|
|
|
|
return ConditionFieldCategory.State;
|
|
}
|
|
|
|
private static void SelectCategory(ConditionRow row, ConditionFieldCategory category)
|
|
{
|
|
for (var i = 0; i < row.CategoryBox.Items.Count; i++)
|
|
{
|
|
if (row.CategoryBox.Items[i] is CategoryItem item && item.Category == category)
|
|
{
|
|
row.CategoryBox.SelectedIndex = i;
|
|
return;
|
|
}
|
|
}
|
|
|
|
row.CategoryBox.SelectedIndex = 0;
|
|
}
|
|
|
|
private static int FindFieldIndex(ComboBox fieldBox, string fieldName)
|
|
{
|
|
for (var i = 0; i < fieldBox.Items.Count; i++)
|
|
{
|
|
if (fieldBox.Items[i] is FieldItem item
|
|
&& string.Equals(item.Name, fieldName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private static void PopulateOps(ConditionRow row, string? desiredOp)
|
|
{
|
|
var field = row.SelectedField;
|
|
var isRuleSetting = IsRuleSettingField(field);
|
|
var ops = isRuleSetting
|
|
? DelayOperators
|
|
: field is { IsCustom: false, Type: ConditionFieldType.Bool }
|
|
? BoolOperators
|
|
: field is { IsCustom: false, Type: ConditionFieldType.String }
|
|
? TextOperators
|
|
: AllOperators;
|
|
|
|
row.OpBox.Items.Clear();
|
|
row.OpBox.Items.AddRange(ops);
|
|
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)
|
|
{
|
|
foreach (Control old in row.ValueHost.Controls)
|
|
{
|
|
old.Dispose();
|
|
}
|
|
|
|
row.ValueHost.Controls.Clear();
|
|
|
|
var field = row.SelectedField;
|
|
var usesListValue = ConditionExpression.IsInOperator(row.OpBox.SelectedItem?.ToString());
|
|
Control control;
|
|
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);
|
|
combo.Items.AddRange(["是 (true)", "否 (false)"]);
|
|
combo.SelectedIndex = IsFalseText(rawValue) ? 1 : 0;
|
|
combo.SelectedIndexChanged += (_, _) => UpdatePreview();
|
|
control = combo;
|
|
}
|
|
else if (!usesListValue
|
|
&& field is { IsCustom: false, Type: ConditionFieldType.Int }
|
|
&& (TryParseIntegerText(rawValue, out var number) || !preserveRaw))
|
|
{
|
|
// 解析失败时 number 为 0, 仅在 preserveRaw=false 的字段切换场景下走到这里。
|
|
var numeric = new NumericUpDown
|
|
{
|
|
Minimum = -1000000,
|
|
Maximum = 1000000,
|
|
Value = number
|
|
};
|
|
UiTheme.StyleNumericUpDown(numeric);
|
|
numeric.ValueChanged += (_, _) => UpdatePreview();
|
|
numeric.TextChanged += (_, _) => UpdatePreview();
|
|
control = numeric;
|
|
}
|
|
else
|
|
{
|
|
// 字符串字段、自定义字段或无法转成数字的旧值用文本框兜底。
|
|
var box = new TextBox
|
|
{
|
|
Text = usesListValue ? ConditionExpression.NormalizeInValue(rawValue) : rawValue ?? string.Empty
|
|
};
|
|
UiTheme.StyleTextBox(box);
|
|
if (usesListValue)
|
|
{
|
|
box.PlaceholderText = "例如: 12, 13";
|
|
}
|
|
|
|
box.TextChanged += (_, _) => UpdatePreview();
|
|
control = box;
|
|
}
|
|
|
|
control.Dock = DockStyle.Fill;
|
|
row.ValueHost.Controls.Add(control);
|
|
row.ValueControl = control;
|
|
}
|
|
|
|
private void OnFieldChanged(ConditionRow row)
|
|
{
|
|
var currentOp = row.OpBox.SelectedItem?.ToString();
|
|
var currentValue = ReadRowValue(row);
|
|
PopulateOps(row, currentOp);
|
|
CreateValueControl(row, currentValue, preserveRaw: false);
|
|
}
|
|
|
|
private void OnOperatorChanged(ConditionRow row)
|
|
{
|
|
var currentValue = ReadRowValue(row);
|
|
CreateValueControl(row, currentValue, preserveRaw: true);
|
|
}
|
|
|
|
private List<ConditionTerm> CollectTerms()
|
|
{
|
|
var terms = new List<ConditionTerm>();
|
|
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);
|
|
if (field.Length == 0 || value.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
terms.Add(new ConditionTerm(
|
|
OrWithPrevious: i > 0 && row.Connector.SelectedIndex == 1,
|
|
field,
|
|
op,
|
|
value));
|
|
}
|
|
|
|
return terms;
|
|
}
|
|
|
|
private static string ReadRowValue(ConditionRow row)
|
|
{
|
|
return row.ValueControl switch
|
|
{
|
|
NumericUpDown numeric => ReadNumericText(numeric),
|
|
ComboBox combo => combo.SelectedIndex == 1 ? "false" : "true",
|
|
TextBox box => ConditionExpression.IsInOperator(row.OpBox.SelectedItem?.ToString())
|
|
? ConditionExpression.NormalizeInValue(box.Text)
|
|
: box.Text.Trim(),
|
|
_ => string.Empty
|
|
};
|
|
}
|
|
|
|
private static string ReadNumericText(NumericUpDown numeric)
|
|
{
|
|
// 输入中的文本可能尚未提交到 Value, 优先读文本。
|
|
return decimal.TryParse(numeric.Text, NumberStyles.Number, CultureInfo.CurrentCulture, out var typed)
|
|
? decimal.Truncate(typed).ToString("0", CultureInfo.InvariantCulture)
|
|
: numeric.Value.ToString("0", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private void UpdatePreview()
|
|
{
|
|
_ = 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, int? delayMs, int? logicDelayMs)
|
|
{
|
|
var conditionText = mainText;
|
|
if (_subConditions.Count > 0)
|
|
{
|
|
var any = string.Join(" | ", _subConditions);
|
|
conditionText = 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)
|
|
{
|
|
return value?.Trim().ToLowerInvariant() is "false" or "no" or "否" or "0";
|
|
}
|
|
|
|
private static bool TryParseIntegerText(string? text, out decimal value)
|
|
{
|
|
value = 0;
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!decimal.TryParse(text.Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out var parsed)
|
|
|| parsed != decimal.Truncate(parsed)
|
|
|| parsed < -1000000
|
|
|| parsed > 1000000)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
value = parsed;
|
|
return true;
|
|
}
|
|
|
|
private static Label CreateHeaderLabel(string text)
|
|
{
|
|
return new Label
|
|
{
|
|
Text = text,
|
|
Dock = DockStyle.Fill,
|
|
ForeColor = UiTheme.Muted,
|
|
TextAlign = ContentAlignment.MiddleLeft,
|
|
Margin = new Padding(0)
|
|
};
|
|
}
|
|
|
|
private sealed record CategoryItem(string Display, ConditionFieldCategory Category)
|
|
{
|
|
public override string ToString() => Display;
|
|
}
|
|
|
|
private sealed record FieldItem(string Name, string Display, ConditionFieldType Type, ConditionFieldCategory Category, bool IsCustom)
|
|
{
|
|
public override string ToString() => Display;
|
|
}
|
|
|
|
private sealed class ConditionRow(
|
|
TableLayoutPanel panel,
|
|
ComboBox connector,
|
|
ComboBox categoryBox,
|
|
ComboBox fieldBox,
|
|
ComboBox opBox,
|
|
Panel valueHost)
|
|
{
|
|
public TableLayoutPanel Panel { get; } = panel;
|
|
public ComboBox Connector { get; } = connector;
|
|
public ComboBox CategoryBox { get; } = categoryBox;
|
|
public ComboBox FieldBox { get; } = fieldBox;
|
|
public ComboBox OpBox { get; } = opBox;
|
|
public Panel ValueHost { get; } = valueHost;
|
|
public Control? ValueControl { get; set; }
|
|
|
|
public CategoryItem? SelectedCategory => CategoryBox.SelectedItem as CategoryItem;
|
|
public FieldItem? SelectedField => FieldBox.SelectedItem as FieldItem;
|
|
}
|
|
}
|