using System.Drawing; namespace Shigure; public sealed class ModuleEditorControl : UserControl { private readonly ModuleStore _moduleStore; private readonly Action _runtimeRestartRequested; private readonly ListBox _moduleList = new(); private readonly TextBox _nameBox = new(); private readonly CheckBox _enabledBox = new(); private readonly ComboBox _classBox = new(); private readonly ComboBox _specBox = new(); private readonly ComboBox _partyTypeBox = new(); private readonly ComboBox _heroTalentBox = new(); private readonly DataGridView _rulesGrid = new(); private readonly Label _pathLabel = new(); private List _modules = new(); private ModuleDefinition? _selectedModule; private static readonly PartyTypeOption[] PartyTypeOptions = [ new("任意 (*)", null), new("单人 (0)", "0"), new("团队 (1-40)", "1-40"), new("队伍 (46)", "46") ]; private static readonly MatchOption[] ClassOptions = BuildClassOptions(); public ModuleEditorControl(ModuleStore moduleStore, Action runtimeRestartRequested) { _moduleStore = moduleStore; _runtimeRestartRequested = runtimeRestartRequested; InitializeComponent(); LoadModules(); } 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 = 1, Margin = new Padding(0) }; root.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 230)); root.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); Controls.Add(root); root.Controls.Add(BuildSidebar(), 0, 0); root.Controls.Add(BuildEditor(), 1, 0); } private Control BuildSidebar() { var sidebar = new TableLayoutPanel { Dock = DockStyle.Fill, BackColor = UiTheme.Background, Padding = new Padding(0, 0, 10, 0), ColumnCount = 1, RowCount = 2 }; sidebar.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); sidebar.RowStyles.Add(new RowStyle(SizeType.Absolute, 38)); _moduleList.Dock = DockStyle.Fill; _moduleList.BackColor = UiTheme.Background; _moduleList.ForeColor = UiTheme.Text; _moduleList.BorderStyle = BorderStyle.None; _moduleList.IntegralHeight = false; _moduleList.SelectedIndexChanged += (_, _) => SelectModule(_moduleList.SelectedIndex); sidebar.Controls.Add(_moduleList, 0, 0); var buttons = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.LeftToRight, WrapContents = false, BackColor = UiTheme.Background, Margin = new Padding(0) }; var addButton = UiTheme.CreateButton("新建", UiTheme.Field, UiTheme.Text); addButton.Width = 72; addButton.Height = 30; addButton.Click += (_, _) => AddModule(); var reloadButton = UiTheme.CreateButton("刷新", UiTheme.Field, UiTheme.Text); reloadButton.Width = 72; reloadButton.Height = 30; reloadButton.Click += (_, _) => LoadModules(); buttons.Controls.Add(addButton); buttons.Controls.Add(reloadButton); sidebar.Controls.Add(buttons, 0, 1); return sidebar; } private Control BuildEditor() { var editor = new TableLayoutPanel { Dock = DockStyle.Fill, BackColor = UiTheme.Surface, Padding = new Padding(8, 0, 0, 0), ColumnCount = 1, RowCount = 4 }; editor.RowStyles.Add(new RowStyle(SizeType.Absolute, 74)); editor.RowStyles.Add(new RowStyle(SizeType.Absolute, 74)); editor.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); editor.RowStyles.Add(new RowStyle(SizeType.Absolute, 38)); editor.Controls.Add(BuildNameRow(), 0, 0); editor.Controls.Add(BuildMatchRow(), 0, 1); editor.Controls.Add(BuildRulesGrid(), 0, 2); editor.Controls.Add(BuildActionRow(), 0, 3); return editor; } private Control BuildNameRow() { var row = new TableLayoutPanel { Dock = DockStyle.Fill, BackColor = UiTheme.Surface, ColumnCount = 4, RowCount = 2, Margin = new Padding(0) }; row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 58)); row.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 90)); row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 90)); row.RowStyles.Add(new RowStyle(SizeType.Absolute, 34)); row.RowStyles.Add(new RowStyle(SizeType.Absolute, 30)); row.Controls.Add(CreateLabel("名称"), 0, 0); StyleTextBox(_nameBox); _nameBox.Dock = DockStyle.Fill; row.Controls.Add(_nameBox, 1, 0); _enabledBox.Text = "启用"; _enabledBox.Checked = true; _enabledBox.Dock = DockStyle.Fill; _enabledBox.ForeColor = UiTheme.Text; _enabledBox.BackColor = UiTheme.Surface; row.Controls.Add(_enabledBox, 2, 0); _pathLabel.Dock = DockStyle.Fill; _pathLabel.ForeColor = UiTheme.Muted; _pathLabel.TextAlign = ContentAlignment.MiddleLeft; _pathLabel.AutoEllipsis = true; row.Controls.Add(_pathLabel, 0, 1); row.SetColumnSpan(_pathLabel, 4); return row; } private Control BuildMatchRow() { var matchLabels = new[] { "职业", "专精", "英雄天赋", "队伍类型" }; var row = new TableLayoutPanel { Dock = DockStyle.Fill, BackColor = UiTheme.Surface, ColumnCount = 8, RowCount = 2, Margin = new Padding(0) }; foreach (var label in matchLabels) { row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, MeasureLabelColumnWidth(label, Font))); row.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25)); } ResetClassOptions(_classBox); ResetSpecOptions(_specBox, null); ResetHeroTalentOptions(_heroTalentBox, null, null); _classBox.SelectedIndexChanged += (_, _) => { ResetSpecOptions(_specBox, ReadMatchCombo(_classBox)); ResetHeroTalentOptions(_heroTalentBox, ReadMatchCombo(_classBox), ReadMatchCombo(_specBox)); }; _specBox.SelectedIndexChanged += (_, _) => ResetHeroTalentOptions(_heroTalentBox, ReadMatchCombo(_classBox), ReadMatchCombo(_specBox)); AddMatchField(row, "职业:", _classBox, 0); AddMatchField(row, "专精:", _specBox, 2); AddMatchField(row, "英雄天赋:", _heroTalentBox, 4); AddMatchField(row, "队伍类型:", _partyTypeBox, 6); return row; } private Control BuildRulesGrid() { _rulesGrid.Dock = DockStyle.Fill; _rulesGrid.BackgroundColor = UiTheme.Surface; _rulesGrid.BorderStyle = BorderStyle.None; _rulesGrid.GridColor = UiTheme.Field; _rulesGrid.EnableHeadersVisualStyles = false; _rulesGrid.ColumnHeadersDefaultCellStyle.BackColor = UiTheme.Field; _rulesGrid.ColumnHeadersDefaultCellStyle.ForeColor = UiTheme.Muted; _rulesGrid.ColumnHeadersDefaultCellStyle.SelectionBackColor = UiTheme.Field; _rulesGrid.DefaultCellStyle.BackColor = UiTheme.Surface; _rulesGrid.DefaultCellStyle.ForeColor = UiTheme.Text; _rulesGrid.DefaultCellStyle.SelectionBackColor = UiTheme.Hover; _rulesGrid.DefaultCellStyle.SelectionForeColor = UiTheme.Text; _rulesGrid.RowHeadersVisible = false; _rulesGrid.AllowUserToAddRows = true; _rulesGrid.AllowUserToDeleteRows = true; _rulesGrid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; _rulesGrid.Columns.Add(new DataGridViewCheckBoxColumn { Name = "Enabled", HeaderText = "启用", FillWeight = 38 }); _rulesGrid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Spell", HeaderText = "技能", FillWeight = 120 }); _rulesGrid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Unit", HeaderText = "目标", FillWeight = 48 }); _rulesGrid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Condition", HeaderText = "条件", FillWeight = 180 }); return _rulesGrid; } private Control BuildActionRow() { var row = new TableLayoutPanel { Dock = DockStyle.Fill, BackColor = UiTheme.Surface, ColumnCount = 2, RowCount = 1 }; row.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 250)); var hint = new Label { Text = "条件示例: 生命值 < 50 && spells.圣疗术 == 0;目标留空表示默认目标", Dock = DockStyle.Fill, ForeColor = UiTheme.Muted, TextAlign = ContentAlignment.MiddleLeft, AutoEllipsis = true }; row.Controls.Add(hint, 0, 0); var buttons = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.RightToLeft, WrapContents = false, BackColor = UiTheme.Surface }; var saveButton = UiTheme.CreateButton("保存", UiTheme.Accent, Color.Black); saveButton.Width = 72; saveButton.Height = 30; saveButton.Click += (_, _) => SaveSelectedModule(); var deleteButton = UiTheme.CreateButton("删除", UiTheme.Field, UiTheme.Danger); deleteButton.Width = 72; deleteButton.Height = 30; deleteButton.Click += (_, _) => DeleteSelectedModule(); buttons.Controls.Add(saveButton); buttons.Controls.Add(deleteButton); row.Controls.Add(buttons, 1, 0); return row; } private void LoadModules() { _moduleStore.Reload(); _modules = _moduleStore.GetModules().ToList(); _moduleList.Items.Clear(); foreach (var module in _modules) { _moduleList.Items.Add(FormatModuleListItem(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) { _nameBox.Text = module.Name; _enabledBox.Checked = module.Enabled; SelectClass(module.Match.ClassId); SelectSpec(module.Match.SpecId); SelectPartyType(module.Match.PartyType); SelectHeroTalent(module.Match.HeroTalent); _pathLabel.Text = module.FilePath ?? "尚未保存"; _rulesGrid.Rows.Clear(); foreach (var rule in module.Rules) { _rulesGrid.Rows.Add(rule.Enabled, rule.Spell, rule.Unit?.ToString() ?? string.Empty, rule.Condition); } } private void ClearEditor() { _selectedModule = null; _nameBox.Clear(); _enabledBox.Checked = false; SelectClass(null); SelectSpec(null); SelectPartyType(null); SelectHeroTalent(null); _pathLabel.Text = "无模块"; _rulesGrid.Rows.Clear(); } private void AddModule() { var module = ModuleDefinition.CreateDefault(_moduleStore.CreateNextModuleName()); try { _moduleStore.Save(module); } catch (InvalidOperationException ex) { MessageBox.Show(ex.Message, "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } LoadModules(); var index = _modules.FindIndex(existing => string.Equals(existing.Id, module.Id, StringComparison.OrdinalIgnoreCase)); if (index >= 0) { _moduleList.SelectedIndex = index; } _runtimeRestartRequested(); } private void SaveSelectedModule() { if (_selectedModule is null) { return; } if (!TryReadModule(out var module)) { return; } ModuleDefinition saved; try { saved = _moduleStore.Save(module); } catch (InvalidOperationException ex) { MessageBox.Show(ex.Message, "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } LoadModules(); var index = _modules.FindIndex(existing => string.Equals(existing.Id, saved.Id, StringComparison.OrdinalIgnoreCase)); if (index >= 0) { _moduleList.SelectedIndex = index; } _runtimeRestartRequested(); } private void DeleteSelectedModule() { 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(); _runtimeRestartRequested(); } private bool TryReadModule(out ModuleDefinition module) { module = _selectedModule!.Clone(); module.Name = string.IsNullOrWhiteSpace(_nameBox.Text) ? "新模块" : _nameBox.Text.Trim(); module.Enabled = _enabledBox.Checked; module.Match = new ModuleMatch { ClassId = ReadMatchCombo(_classBox), SpecId = ReadMatchCombo(_specBox), PartyType = ReadPartyTypeCombo(), HeroTalent = ReadMatchCombo(_heroTalentBox) }; module.Rules = ReadRules(); return true; } private List ReadRules() { var rules = new List(); foreach (DataGridViewRow row in _rulesGrid.Rows) { if (row.IsNewRow) { continue; } var condition = CellText(row, "Condition"); var spell = CellText(row, "Spell"); if (string.IsNullOrWhiteSpace(condition) && string.IsNullOrWhiteSpace(spell) && string.IsNullOrWhiteSpace(CellText(row, "Unit"))) { continue; } rules.Add(new ModuleRule { Enabled = CellBool(row, "Enabled", defaultValue: true), Condition = condition, Unit = ParseNullableInt(CellText(row, "Unit")), Spell = spell, Hotkey = string.Empty, Step = string.Empty }); } return rules; } private static void AddMatchField(TableLayoutPanel row, string label, TextBox box, int column) { row.Controls.Add(CreateLabel(label), column, 0); StyleTextBox(box); box.Dock = DockStyle.Fill; row.Controls.Add(box, column + 1, 0); } private static void AddMatchField(TableLayoutPanel row, string label, ComboBox box, int column) { row.Controls.Add(CreateLabel(label), column, 0); UiTheme.StyleComboBox(box); box.Dock = DockStyle.Fill; row.Controls.Add(box, column + 1, 0); } private static Label CreateLabel(string text) { return new Label { Text = text, Dock = DockStyle.Fill, ForeColor = UiTheme.Muted, TextAlign = ContentAlignment.MiddleLeft, AutoEllipsis = true }; } private static int MeasureLabelColumnWidth(string text, Font font) { return TextRenderer.MeasureText(text, font).Width + 18; } private static void StyleTextBox(TextBox textBox) { textBox.BackColor = UiTheme.Field; textBox.ForeColor = UiTheme.Text; textBox.BorderStyle = BorderStyle.FixedSingle; } private static string FormatModuleListItem(ModuleDefinition module) { var enabled = module.Enabled ? "●" : "○"; var match = $"{FormatMatchValue(module.Match.ClassId)}/{FormatMatchValue(module.Match.SpecId)}/{FormatPartyTypeValue(module.Match.PartyType)}/{FormatMatchValue(module.Match.HeroTalent)}"; return $"{enabled} {module.Name} [{match}]"; } private static string FormatMatchValue(int? value) { return value?.ToString() ?? "*"; } 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(ComboBox comboBox) { return comboBox.SelectedItem is MatchOption option ? option.Value : null; } private static void ResetClassOptions(ComboBox comboBox) { comboBox.Items.Clear(); comboBox.Items.AddRange(ClassOptions); comboBox.SelectedIndex = 0; } private static void ResetSpecOptions(ComboBox 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(ComboBox 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(ComboBox 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(ComboBox 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 FormatPartyTypeValue(string? value) { return ModuleMatch.NormalizePartyTypeValue(value) switch { null => "*", "0" => "单人", "1-40" => "团队", "46" => "队伍", var other => other }; } 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; } } }