Files
Shigure/UI/MainForm.cs

1642 lines
51 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Drawing;
namespace Shigure;
public sealed class MainForm : Form, IMessageFilter
{
private const int ResizeGripSize = 8;
private const int RoundedCornerResizeDebounceMs = 80;
private const string HeaderIconResourcePath = "Assets.arasaka-icon-transparent.png";
private static readonly Color DefaultHeaderIconColor = Color.White;
private static readonly IReadOnlyDictionary<int, Color> ClassIconColors = new Dictionary<int, Color>
{
[1] = ColorTranslator.FromHtml("#C79C6E"),
[2] = ColorTranslator.FromHtml("#F58CBA"),
[3] = ColorTranslator.FromHtml("#ABD473"),
[4] = ColorTranslator.FromHtml("#FFF569"),
[5] = ColorTranslator.FromHtml("#FFFFFF"),
[6] = ColorTranslator.FromHtml("#C41F3B"),
[7] = ColorTranslator.FromHtml("#0070DE"),
[8] = ColorTranslator.FromHtml("#69CCF0"),
[9] = ColorTranslator.FromHtml("#9482C9"),
[10] = ColorTranslator.FromHtml("#00FF96"),
[11] = ColorTranslator.FromHtml("#FF7D0A"),
[12] = ColorTranslator.FromHtml("#A330C9"),
[13] = ColorTranslator.FromHtml("#33937F")
};
private Button _toggleKeyButton = null!;
private ComboBox _modeComboBox = null!;
private ComboBox _moduleComboBox = null!;
private Label _moduleFilterLabel = null!;
private Label _moduleCountLabel = null!;
private Label _configSourceLabel = null!;
private Button _settingsButton = null!;
private string _toggleKeyName = "XBUTTON2";
private string? _selectedModuleId;
private bool _isCapturingToggleKey;
private bool _suppressModuleSelectionChanged;
private string? _lastModuleSelectorSignature;
private bool _usesDwmRoundedCorners = true;
private Button _enableButton = null!;
private PictureBox _headerIcon = null!;
private Label _titleLabel = null!;
private Label _runtimeStatusLabel = null!;
private Bitmap? _headerIconMask;
private Color? _currentHeaderIconColor;
private readonly StatusForm _statusForm;
private readonly string _baseDirectory;
private readonly ModuleStore _moduleStore;
private readonly ITriggerKeyState _triggerKeyState;
private readonly WowProcessLocator _processLocator;
private readonly RuntimeSessionCoordinator _runtimeSession;
private readonly ModuleEditorControl _moduleEditor;
private readonly ClassConfigEditorControl _classConfigEditor;
private readonly ClassMacrosEditorControl _classMacrosEditor;
private readonly AppOptions _initialOptions;
private readonly UiCacheState _uiCache;
private readonly System.Windows.Forms.Timer _roundedCornerResizeTimer;
private RenderSnapshot? _lastSnapshot;
private string? _lastLoggedStep;
private string? _lastLoggedStepDetails;
private string? _lastLoggedScanFailureReason;
private string? _lastLoggedClass;
private string? _lastLoggedModule;
private bool? _lastLoggedEnabled;
private readonly object _configUpdateSync = new();
private Task _configUpdateTail = Task.CompletedTask;
private long _runtimeRequestVersion;
private bool _shutdownStarted;
private bool _shutdownCompleted;
internal MainForm(
AppOptions initialOptions,
string baseDirectory,
ModuleStore moduleStore,
ITriggerKeyState triggerKeyState,
WowProcessLocator processLocator,
RuntimeSessionCoordinator runtimeSession)
{
_initialOptions = initialOptions;
_baseDirectory = baseDirectory;
_moduleStore = moduleStore;
_triggerKeyState = triggerKeyState;
_processLocator = processLocator;
_runtimeSession = runtimeSession;
_uiCache = UiCacheStore.Load();
_statusForm = new StatusForm();
_roundedCornerResizeTimer = new System.Windows.Forms.Timer
{
Interval = RoundedCornerResizeDebounceMs
};
_roundedCornerResizeTimer.Tick += (_, _) =>
{
_roundedCornerResizeTimer.Stop();
if (IsHandleCreated && !_usesDwmRoundedCorners)
{
UiTheme.ApplyFallbackRoundedCorners(this);
}
};
Application.AddMessageFilter(this);
InitializeComponent();
_statusForm.AttachSettingsPanel(BuildSettingsPanel());
_moduleEditor = new ModuleEditorControl(_moduleStore, RestartRuntimeFromEditorAsync, _baseDirectory);
_statusForm.AttachModuleEditor(_moduleEditor);
_classConfigEditor = new ClassConfigEditorControl(
() => WowAddonLocator.FindClassDirectory(_processLocator),
UpdateConfigFromAddonAsync);
_statusForm.AttachConfigEditor(_classConfigEditor);
_classMacrosEditor = new ClassMacrosEditorControl(
() => WowAddonLocator.FindClassMacrosPath(_processLocator),
UpdateConfigFromAddonAsync);
_statusForm.AttachMacrosEditor(_classMacrosEditor);
_statusForm.FormClosing += (_, _) =>
{
CancelToggleKeyCapture();
SaveUiCache();
};
TryApplyApplicationIcon();
ApplyCachedWindowState();
ApplyInitialOptions();
WireSettingEvents();
_runtimeSession.SnapshotUpdated += HandleSnapshotUpdated;
_runtimeSession.RuntimeFailed += HandleRuntimeFailed;
_runtimeSession.RuntimeStopped += HandleRuntimeStopped;
SetRuntimeControls(running: false);
AppendLog("界面已就绪");
}
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
UiTheme.ApplyDarkTitleBar(this);
UiTheme.ApplyTranslucentBackground(this);
_usesDwmRoundedCorners = UiTheme.ApplyRoundedCorners(this);
}
protected override async void OnShown(EventArgs e)
{
base.OnShown(e);
await StartRuntimeAsync();
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (!_shutdownCompleted)
{
e.Cancel = true;
if (!_shutdownStarted)
{
_shutdownStarted = true;
SaveUiCache();
_roundedCornerResizeTimer.Stop();
Application.RemoveMessageFilter(this);
_ = CompleteShutdownAsync();
}
base.OnFormClosing(e);
return;
}
base.OnFormClosing(e);
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_roundedCornerResizeTimer.Dispose();
base.OnFormClosed(e);
}
private async Task CompleteShutdownAsync()
{
_runtimeSession.SnapshotUpdated -= HandleSnapshotUpdated;
_runtimeSession.RuntimeFailed -= HandleRuntimeFailed;
_runtimeSession.RuntimeStopped -= HandleRuntimeStopped;
try
{
var runtimeShutdown = _runtimeSession.DisposeAsync().AsTask();
await Task.WhenAll(runtimeShutdown, GetPendingConfigUpdateTask());
}
catch (Exception ex)
{
AppendLog($"停止运行失败: {ex.Message}");
}
finally
{
_statusForm.Dispose();
_shutdownCompleted = true;
if (!IsDisposed)
{
Close();
}
}
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
if (IsHandleCreated && !_usesDwmRoundedCorners)
{
ScheduleFallbackRoundedCornerUpdate();
}
}
protected override void OnResizeEnd(EventArgs e)
{
base.OnResizeEnd(e);
if (IsHandleCreated && !_usesDwmRoundedCorners)
{
_roundedCornerResizeTimer.Stop();
UiTheme.ApplyFallbackRoundedCorners(this);
}
}
private void ScheduleFallbackRoundedCornerUpdate()
{
_roundedCornerResizeTimer.Stop();
_roundedCornerResizeTimer.Start();
}
protected override void WndProc(ref Message m)
{
const int WmNcHitTest = 0x0084;
if (m.Msg == WmNcHitTest)
{
base.WndProc(ref m);
if (m.Result == NativeMethods.HtClient)
{
m.Result = HitTestResizeGrip(PointToClient(Cursor.Position));
}
return;
}
base.WndProc(ref m);
}
private void InitializeComponent()
{
SuspendLayout();
Text = GetWindowTitle();
StartPosition = FormStartPosition.CenterScreen;
FormBorderStyle = FormBorderStyle.None;
TopMost = true;
ClientSize = new Size(680, 64);
MinimumSize = new Size(420, 56);
BackColor = Color.FromArgb(18, 21, 26);
ForeColor = UiTheme.Text;
Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Regular, GraphicsUnit.Point);
var root = new TableLayoutPanel
{
Dock = DockStyle.Fill,
BackColor = Color.Transparent,
Padding = new Padding(12),
RowCount = 1,
ColumnCount = 1
};
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
Controls.Add(root);
root.Controls.Add(BuildTopBar(), 0, 0);
ResumeLayout(false);
}
private Control BuildTopBar()
{
var bar = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 3,
RowCount = 1,
BackColor = Color.Transparent,
Margin = new Padding(0)
};
bar.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
bar.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
bar.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
var brand = new FlowLayoutPanel
{
AutoSize = true,
Anchor = AnchorStyles.Left,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false,
BackColor = Color.Transparent,
Margin = new Padding(0, 0, 12, 0),
Padding = new Padding(0)
};
_headerIcon = CreateHeaderIcon();
UpdateHeaderIconColor(null);
_titleLabel = new Label
{
Text = "Shigure",
AutoSize = true,
Anchor = AnchorStyles.Left,
Font = new Font(Font.FontFamily, 13F, FontStyle.Bold),
ForeColor = UiTheme.Text,
Margin = new Padding(8, 0, 0, 0)
};
brand.Controls.Add(_headerIcon);
brand.Controls.Add(_titleLabel);
_runtimeStatusLabel = new Label
{
Text = string.Empty,
AutoSize = false,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft,
ForeColor = UiTheme.Muted
};
EnableDrag(bar);
EnableDrag(brand);
EnableDrag(_headerIcon);
EnableDrag(_titleLabel);
EnableDrag(_runtimeStatusLabel);
var buttons = new FlowLayoutPanel
{
AutoSize = true,
Anchor = AnchorStyles.Right,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false,
BackColor = Color.Transparent,
Margin = new Padding(0)
};
_enableButton = UiTheme.CreateButton("开关", UiTheme.Field, UiTheme.Text);
ConfigureTopBarButton(_enableButton);
_enableButton.Click += (_, _) => ToggleEnabled();
_settingsButton = UiTheme.CreateButton("设置", UiTheme.Field, UiTheme.Text);
ConfigureTopBarButton(_settingsButton);
_settingsButton.Click += (_, _) => ShowSettingsView();
var closeButton = UiTheme.CreateButton("✕", UiTheme.Field, UiTheme.Muted);
ConfigureTopBarButton(closeButton);
closeButton.FlatAppearance.MouseOverBackColor = UiTheme.Danger;
closeButton.Click += (_, _) => Close();
buttons.Controls.AddRange(new Control[] { _enableButton, _settingsButton, closeButton });
bar.Controls.Add(brand, 0, 0);
bar.Controls.Add(_runtimeStatusLabel, 1, 0);
bar.Controls.Add(buttons, 2, 0);
return bar;
}
private static PictureBox CreateHeaderIcon()
{
var box = new PictureBox
{
Size = new Size(32, 32),
MinimumSize = new Size(32, 32),
MaximumSize = new Size(32, 32),
SizeMode = PictureBoxSizeMode.Zoom,
BackColor = Color.Transparent,
Margin = new Padding(0),
Anchor = AnchorStyles.Left
};
return box;
}
private void UpdateHeaderIconColor(int? classId)
{
var color = ResolveClassIconColor(classId);
if (_currentHeaderIconColor == color)
{
return;
}
_currentHeaderIconColor = color;
_headerIconMask ??= LoadHeaderIconMask();
if (_headerIconMask is null)
{
return;
}
var previous = _headerIcon.Image;
_headerIcon.Image = TintHeaderIcon(_headerIconMask, color);
previous?.Dispose();
}
private static Color ResolveClassIconColor(int? classId)
=> classId is not null && ClassIconColors.TryGetValue(classId.Value, out var color)
? color
: DefaultHeaderIconColor;
private static Bitmap? LoadHeaderIconMask()
{
using var stream = typeof(MainForm).Assembly.GetManifestResourceStream(GetHeaderIconResourceName());
if (stream is null)
{
return null;
}
using var image = Image.FromStream(stream);
return new Bitmap(image);
}
private static string GetHeaderIconResourceName() =>
$"{typeof(MainForm).Namespace}.{HeaderIconResourcePath}";
private static Bitmap TintHeaderIcon(Bitmap mask, Color color)
{
var bitmap = new Bitmap(mask.Width, mask.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
bitmap.SetResolution(mask.HorizontalResolution, mask.VerticalResolution);
for (var y = 0; y < mask.Height; y++)
{
for (var x = 0; x < mask.Width; x++)
{
var pixel = mask.GetPixel(x, y);
if (pixel.A == 0)
{
continue;
}
bitmap.SetPixel(x, y, Color.FromArgb(pixel.A, color));
}
}
return bitmap;
}
private Control BuildSettingsPanel()
{
var panel = new TableLayoutPanel
{
Dock = DockStyle.Fill,
BackColor = UiTheme.Surface,
Padding = new Padding(0),
ColumnCount = 1,
Margin = new Padding(0),
RowCount = 3
};
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
var settingsGrid = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
BackColor = UiTheme.SurfaceRaised,
ColumnCount = 2,
RowCount = 2,
Padding = new Padding(16, 16, 16, 10),
Margin = new Padding(0, 0, 0, 14)
};
settingsGrid.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 96));
settingsGrid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
settingsGrid.RowStyles.Add(new RowStyle(SizeType.Absolute, 54));
settingsGrid.RowStyles.Add(new RowStyle(SizeType.Absolute, 54));
Label CreateSettingLabel(string text) => new()
{
Text = text,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft,
ForeColor = UiTheme.Muted,
Margin = new Padding(0, 0, 12, 14)
};
const int settingControlWidth = 190;
_toggleKeyButton = UiTheme.CreateButton("XBUTTON2", UiTheme.Field, UiTheme.Text);
_toggleKeyButton.AutoSize = false;
_toggleKeyButton.Width = settingControlWidth;
_toggleKeyButton.Height = 36;
_toggleKeyButton.Padding = new Padding(6, 0, 6, 0);
_toggleKeyButton.TextAlign = ContentAlignment.MiddleCenter;
_toggleKeyButton.Anchor = AnchorStyles.Left;
_toggleKeyButton.Margin = new Padding(0, 0, 0, 12);
_toggleKeyButton.Click += (_, _) => BeginCaptureToggleKey();
settingsGrid.Controls.Add(CreateSettingLabel("触发键"), 0, 0);
settingsGrid.Controls.Add(_toggleKeyButton, 1, 0);
_modeComboBox = new ComboBox();
UiTheme.StyleComboBox(_modeComboBox);
_modeComboBox.Items.AddRange(new object[] { "开关", "单击", "按住" });
_modeComboBox.SelectedIndex = 0;
_modeComboBox.Width = settingControlWidth;
_modeComboBox.Anchor = AnchorStyles.Left;
_modeComboBox.Margin = new Padding(0, 0, 0, 12);
settingsGrid.Controls.Add(CreateSettingLabel("发送模式"), 0, 1);
settingsGrid.Controls.Add(_modeComboBox, 1, 1);
var configPanel = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
BackColor = UiTheme.SurfaceRaised,
ColumnCount = 2,
RowCount = 3,
Padding = new Padding(16, 16, 16, 14),
Margin = new Padding(0, 0, 0, 14)
};
configPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 96));
configPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
configPanel.RowStyles.Add(new RowStyle(SizeType.Absolute, 28));
configPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
configPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var configTitle = new Label
{
Text = "配置同步",
Dock = DockStyle.Fill,
ForeColor = UiTheme.Text,
Font = new Font(Font.FontFamily, 10F, FontStyle.Bold),
TextAlign = ContentAlignment.MiddleLeft,
Margin = new Padding(0)
};
configPanel.Controls.Add(configTitle, 0, 0);
configPanel.SetColumnSpan(configTitle, 2);
_configSourceLabel = CreateInfoLabel("Fuyutsui: 点击「更新配置」时同步 class 与 classmacros → keymap");
_configSourceLabel.Margin = new Padding(0, 4, 0, 10);
configPanel.Controls.Add(_configSourceLabel, 0, 1);
configPanel.SetColumnSpan(_configSourceLabel, 2);
var updateConfigButton = UiTheme.CreateButton("更新配置", UiTheme.Field, UiTheme.Text);
updateConfigButton.AutoSize = false;
updateConfigButton.Size = new Size(116, 38);
updateConfigButton.Padding = new Padding(8, 0, 8, 0);
updateConfigButton.TextAlign = ContentAlignment.MiddleCenter;
updateConfigButton.Anchor = AnchorStyles.Top | AnchorStyles.Left;
updateConfigButton.Margin = new Padding(0);
updateConfigButton.Click += async (_, _) => await UpdateConfigFromAddonAsync();
configPanel.Controls.Add(updateConfigButton, 0, 2);
configPanel.SetColumnSpan(updateConfigButton, 2);
var moduleInfo = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
BackColor = UiTheme.SurfaceRaised,
ColumnCount = 2,
RowCount = 2,
Padding = new Padding(16, 16, 16, 14),
Margin = new Padding(0)
};
moduleInfo.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 96));
moduleInfo.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
moduleInfo.RowStyles.Add(new RowStyle(SizeType.Absolute, 54));
moduleInfo.RowStyles.Add(new RowStyle(SizeType.AutoSize));
_moduleComboBox = new ComboBox();
UiTheme.StyleComboBox(_moduleComboBox);
_moduleComboBox.Width = 520;
_moduleComboBox.Anchor = AnchorStyles.Left;
_moduleComboBox.Margin = new Padding(0, 0, 0, 12);
moduleInfo.Controls.Add(CreateSettingLabel("模块选择"), 0, 0);
moduleInfo.Controls.Add(_moduleComboBox, 1, 0);
var moduleStatus = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
ColumnCount = 1,
RowCount = 2,
BackColor = UiTheme.SurfaceRaised,
Margin = new Padding(0)
};
moduleStatus.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
moduleStatus.RowStyles.Add(new RowStyle(SizeType.AutoSize));
moduleStatus.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var moduleInfoText = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
AutoSize = true,
FlowDirection = FlowDirection.TopDown,
WrapContents = false,
BackColor = UiTheme.SurfaceRaised,
Margin = new Padding(0)
};
_moduleFilterLabel = CreateInfoLabel("筛选: 等待游戏状态");
_moduleCountLabel = CreateInfoLabel("可选模块: 0");
moduleInfoText.Controls.Add(_moduleFilterLabel);
moduleInfoText.Controls.Add(_moduleCountLabel);
var refreshModulesButton = UiTheme.CreateButton("刷新模块", UiTheme.Field, UiTheme.Text);
refreshModulesButton.AutoSize = false;
refreshModulesButton.Size = new Size(116, 38);
refreshModulesButton.Padding = new Padding(8, 0, 8, 0);
refreshModulesButton.TextAlign = ContentAlignment.MiddleCenter;
refreshModulesButton.Anchor = AnchorStyles.Top | AnchorStyles.Left;
refreshModulesButton.Margin = new Padding(0, 0, 0, 8);
refreshModulesButton.Click += (_, _) => RefreshModuleSelector(_lastSnapshot, reloadModules: true);
moduleStatus.Controls.Add(refreshModulesButton, 0, 0);
moduleStatus.Controls.Add(moduleInfoText, 0, 1);
moduleInfo.Controls.Add(moduleStatus, 1, 1);
panel.Controls.Add(settingsGrid, 0, 0);
panel.Controls.Add(configPanel, 0, 1);
panel.Controls.Add(moduleInfo, 0, 2);
return panel;
}
private Task UpdateConfigFromAddonAsync()
{
lock (_configUpdateSync)
{
if (_shutdownStarted)
{
return Task.CompletedTask;
}
_configUpdateTail = RunQueuedConfigUpdateAsync(_configUpdateTail);
return _configUpdateTail;
}
}
private async Task RunQueuedConfigUpdateAsync(Task previousUpdate)
{
await Task.Yield();
try
{
await previousUpdate;
}
catch
{
// 前一个调用方会收到自己的异常;队列仍继续处理后续更新。
}
if (!_shutdownStarted)
{
await UpdateConfigFromAddonCoreAsync();
}
}
private Task GetPendingConfigUpdateTask()
{
lock (_configUpdateSync)
{
return _configUpdateTail;
}
}
private async Task WaitForPendingConfigUpdatesAsync()
{
while (true)
{
var pending = GetPendingConfigUpdateTask();
await pending;
lock (_configUpdateSync)
{
if (ReferenceEquals(pending, _configUpdateTail))
{
return;
}
}
}
}
private async Task UpdateConfigFromAddonCoreAsync()
{
if (_shutdownStarted)
{
return;
}
var processNames = _processLocator.DescribeConfiguredProcesses();
var classDirectory = WowAddonLocator.FindClassDirectory(_processLocator);
var classMacrosPath = WowAddonLocator.FindClassMacrosPath(_processLocator);
if (string.IsNullOrWhiteSpace(classDirectory))
{
_configSourceLabel.Text = $"Fuyutsui class: 未找到(目标进程: {processNames}";
MessageBox.Show(
$"未找到目标进程({processNames})对应的 Interface\\AddOns\\Fuyutsui\\class 目录。\n请确认游戏已启动且已安装 Fuyutsui。",
"更新配置",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return;
}
_configSourceLabel.Text = string.IsNullOrWhiteSpace(classMacrosPath)
? $"Fuyutsui class: {classDirectory}"
: $"Fuyutsui: {classDirectory} + classmacros.lua";
var configDirectory = ConfigService.ResolveConfigPath(_baseDirectory);
if (!Directory.Exists(configDirectory))
{
MessageBox.Show($"配置目录不存在: {configDirectory}", "更新配置", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
var keymapDirectory = Path.Combine(_baseDirectory, "keymap");
try
{
UseWaitCursor = true;
var result = await Task.Run(() =>
{
var configResult = FuyutsuiConfigConverter.UpdateFromClassDirectory(classDirectory, configDirectory);
FuyutsuiKeymapConverter.UpdateResult? keymapResult = null;
if (!string.IsNullOrWhiteSpace(classMacrosPath))
{
keymapResult = FuyutsuiKeymapConverter.UpdateFromClassMacros(classMacrosPath, keymapDirectory);
}
return (Config: configResult, Keymap: keymapResult);
});
if (_shutdownStarted)
{
return;
}
_moduleEditor.ReloadCatalogs();
AppendLog($"已从 Fuyutsui 更新配置: {result.Config.UpdatedFiles.Count} 个文件 ← {result.Config.ClassDirectory}");
foreach (var warning in result.Config.Warnings.Take(20))
{
AppendLog($"配置警告: {warning}");
}
if (result.Keymap is { } keymap)
{
AppendLog($"已从 classmacros 更新 keymap: {keymap.UpdatedFiles.Count} 个文件 ← {keymap.ClassMacrosPath}");
foreach (var warning in keymap.Warnings.Take(20))
{
AppendLog($"keymap 警告: {warning}");
}
}
else
{
AppendLog("未找到 core\\classmacros.lua已跳过 keymap 更新");
}
if (_runtimeSession.HasSession)
{
AppendLog("配置已更新, 重新启动运行");
await StartOrRestartRuntimeAsync(restart: true, waitForConfigUpdates: false);
}
if (_shutdownStarted)
{
return;
}
var warningCount = result.Config.Warnings.Count + (result.Keymap?.Warnings.Count ?? 0);
var warningText = warningCount == 0
? string.Empty
: $"\n警告 {warningCount} 条(详见日志)。";
var keymapText = result.Keymap is { } km
? $"\nkeymap: {km.UpdatedFiles.Count} 个文件\n{km.ClassMacrosPath}"
: "\nkeymap: 未更新(缺少 classmacros.lua";
MessageBox.Show(
$"已更新 {result.Config.UpdatedFiles.Count} 个职业配置。\n来源:\n{result.Config.ClassDirectory}{keymapText}{warningText}",
"更新配置",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
catch (Exception ex)
{
if (_shutdownStarted)
{
return;
}
AppendLog($"更新配置失败: {ex.Message}");
MessageBox.Show(ex.Message, "更新配置失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
UseWaitCursor = false;
}
}
private void ApplyInitialOptions()
{
var cachedToggleKey = _uiCache.ToggleKey?.Trim();
var initialToggleKey = !string.IsNullOrWhiteSpace(cachedToggleKey)
? cachedToggleKey
: _initialOptions.ToggleKey.Trim();
initialToggleKey = string.IsNullOrWhiteSpace(initialToggleKey) ? "XBUTTON2" : initialToggleKey;
_toggleKeyName = IsUnsupportedToggleKey(initialToggleKey) ? "XBUTTON2" : initialToggleKey;
_selectedModuleId = string.IsNullOrWhiteSpace(_uiCache.SelectedModuleId)
? null
: _uiCache.SelectedModuleId.Trim();
SetToggleKeyButtonText();
_modeComboBox.SelectedIndex = _initialOptions.Mode switch
{
SendMode.Click => 1,
SendMode.Hold => 2,
_ => 0
};
RefreshModuleSelector(_lastSnapshot, reloadModules: false);
}
private void WireSettingEvents()
{
_modeComboBox.SelectedIndexChanged += HandleSettingCommitted;
_moduleComboBox.SelectedIndexChanged += HandleModuleSelectionChanged;
}
private async void HandleSettingCommitted(object? sender, EventArgs e)
{
await RestartRuntimeAfterSettingChangeAsync();
}
private async Task StartRuntimeAsync()
{
if (_runtimeSession.IsRunning)
{
return;
}
await StartOrRestartRuntimeAsync(restart: false);
}
private async Task<bool> StartOrRestartRuntimeAsync(
bool restart,
bool waitForConfigUpdates = true)
{
if (_shutdownStarted)
{
return false;
}
var options = BuildOptions();
if (!ValidateRuntimeOptions(options))
{
return false;
}
var requestVersion = Interlocked.Increment(ref _runtimeRequestVersion);
try
{
if (waitForConfigUpdates)
{
await WaitForPendingConfigUpdatesAsync();
if (_shutdownStarted || requestVersion != Volatile.Read(ref _runtimeRequestVersion))
{
return false;
}
}
if (restart)
{
await _runtimeSession.RestartAsync(options, requestVersion);
}
else
{
await _runtimeSession.StartAsync(options, requestVersion);
}
}
catch (Exception ex)
{
if (_shutdownStarted || requestVersion != Volatile.Read(ref _runtimeRequestVersion))
{
return false;
}
var operation = restart ? "重启" : "启动";
MessageBox.Show(ex.Message, $"{operation}失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
AppendLog($"{operation}失败: {ex.Message}");
SetRuntimeControls(running: _runtimeSession.IsRunning);
return false;
}
if (_shutdownStarted || requestVersion != Volatile.Read(ref _runtimeRequestVersion))
{
return false;
}
if (!_runtimeSession.IsRunning)
{
SetRuntimeControls(running: false);
return false;
}
ResetRuntimeLogState();
SetRuntimeControls(running: true);
AppendLog($"运行已{(restart ? "" : "")}: {_processLocator.DescribeConfiguredProcesses()} / {options.ToggleKey} / {ModeLabel(options.Mode)}");
return true;
}
private bool ValidateRuntimeOptions(AppOptions options)
{
if (IsUnsupportedToggleKey(options.ToggleKey))
{
MessageBox.Show("触发键不支持 ALT请选择其他按键。", "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
if (_triggerKeyState.ResolveVirtualKey(options.ToggleKey) is null)
{
MessageBox.Show($"无法识别触发键: {options.ToggleKey}", "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
return true;
}
private void ResetRuntimeLogState()
{
_lastLoggedStep = null;
_lastLoggedStepDetails = null;
_lastLoggedScanFailureReason = null;
_lastLoggedClass = null;
_lastLoggedModule = null;
_lastLoggedEnabled = null;
}
private async Task RestartRuntimeFromEditorAsync()
{
RefreshModuleSelector(_lastSnapshot, reloadModules: true);
if (!_runtimeSession.HasSession)
{
_moduleStore.Reload();
return;
}
AppendLog("模块已变更, 重新启动运行");
await StartOrRestartRuntimeAsync(restart: true);
}
private void ToggleEnabled()
{
if (!_runtimeSession.IsRunning)
{
return;
}
_runtimeSession.ToggleEnabled();
}
private AppOptions BuildOptions()
{
var toggleKey = string.IsNullOrWhiteSpace(_toggleKeyName)
? "XBUTTON2"
: _toggleKeyName.Trim();
return _initialOptions with { ToggleKey = toggleKey, Mode = ReadMode(), ModuleId = _selectedModuleId };
}
private SendMode ReadMode()
{
return _modeComboBox.SelectedIndex switch
{
1 => SendMode.Click,
2 => SendMode.Hold,
_ => SendMode.Switch
};
}
private void HandleSnapshotUpdated(long sessionId, RenderSnapshot snapshot)
{
PostToUi(() =>
{
if (_runtimeSession.CurrentSessionId == sessionId)
{
ApplySnapshot(snapshot);
}
});
}
private void HandleRuntimeFailed(long sessionId, Exception exception)
{
PostToUi(() =>
{
if (_runtimeSession.CurrentSessionId != sessionId)
{
return;
}
AppendLog($"运行异常: {exception.Message}");
_titleLabel.ForeColor = UiTheme.Danger;
SetRuntimeControls(running: false);
});
}
private void HandleRuntimeStopped(long sessionId)
{
PostToUi(() =>
{
if (_runtimeSession.CurrentSessionId == sessionId)
{
SetRuntimeControls(running: false);
}
});
}
private void ApplySnapshot(RenderSnapshot snapshot)
{
_lastSnapshot = snapshot;
UpdateHeaderIconColor(snapshot.ClassId);
UpdateLogicStatusLabel(snapshot.Enabled);
_enableButton.Text = snapshot.Enabled ? "关闭" : "开启";
RefreshModuleSelector(snapshot, reloadModules: false);
_statusForm.ApplySnapshot(snapshot);
WriteSnapshotLog(snapshot);
}
private void RefreshModuleSelector(RenderSnapshot? snapshot, bool reloadModules)
{
if (_moduleComboBox is null)
{
return;
}
if (reloadModules)
{
_moduleStore.Reload();
}
var hasValidState = snapshot?.State?.GetBool("有效性") == true;
var (classId, specId, partyType, heroTalent, filterText) = GetModuleFilter(snapshot, hasValidState);
var modules = !hasValidState
? _moduleStore.GetModules()
: _moduleStore.FindMatches(classId, specId, partyType, heroTalent);
var signature = BuildModuleSelectorSignature(
hasValidState,
classId,
specId,
partyType,
heroTalent,
modules);
if (!reloadModules && signature == _lastModuleSelectorSignature)
{
return;
}
_lastModuleSelectorSignature = signature;
_suppressModuleSelectionChanged = true;
try
{
_moduleComboBox.BeginUpdate();
try
{
_moduleComboBox.Items.Clear();
_moduleComboBox.Items.Add(ModuleSelectionOption.Auto);
foreach (var module in modules)
{
_moduleComboBox.Items.Add(new ModuleSelectionOption(module.Id, ModuleDisplay.FormatListItem(module)));
}
var selectedIndex = 0;
var selectedModuleVisible = string.IsNullOrWhiteSpace(_selectedModuleId);
if (!string.IsNullOrWhiteSpace(_selectedModuleId))
{
for (var i = 1; i < _moduleComboBox.Items.Count; i++)
{
if (_moduleComboBox.Items[i] is ModuleSelectionOption option
&& string.Equals(option.ModuleId, _selectedModuleId, StringComparison.OrdinalIgnoreCase))
{
selectedIndex = i;
selectedModuleVisible = true;
break;
}
}
}
_moduleComboBox.SelectedIndex = selectedIndex;
_moduleCountLabel.Text = selectedModuleVisible
? $"可选模块: {modules.Count}"
: $"可选模块: {modules.Count},已选模块不符合当前筛选";
}
finally
{
_moduleComboBox.EndUpdate();
}
}
finally
{
_suppressModuleSelectionChanged = false;
}
_moduleFilterLabel.Text = filterText;
}
private string BuildModuleSelectorSignature(
bool hasValidState,
int? classId,
int? specId,
int? partyType,
int? heroTalent,
IReadOnlyList<ModuleDefinition> modules)
{
var moduleText = string.Join("|", modules.Select(module => $"{module.Id}:{module.Name}:{ModuleDisplay.FormatMatch(module.Match)}"));
return $"{hasValidState}:{classId}:{specId}:{partyType}:{heroTalent}:{_selectedModuleId}:{moduleText}";
}
private static (int? ClassId, int? SpecId, int? PartyType, int? HeroTalent, string Text) GetModuleFilter(
RenderSnapshot? snapshot,
bool hasValidState)
{
if (!hasValidState || snapshot?.State is null)
{
return (null, null, null, null, "筛选: 等待游戏状态,暂时显示全部模块");
}
var partyType = snapshot.State.GetInt("队伍类型");
var heroTalent = snapshot.State.GetInt("英雄天赋");
return (
snapshot.ClassId,
snapshot.SpecId,
partyType,
heroTalent,
$"筛选: {ModuleDisplay.FormatState(snapshot)}");
}
private async void HandleModuleSelectionChanged(object? sender, EventArgs e)
{
if (_suppressModuleSelectionChanged)
{
return;
}
_selectedModuleId = _moduleComboBox.SelectedItem is ModuleSelectionOption option
? option.ModuleId
: null;
SaveUiCache();
AppendLog($"模块选择: {(_selectedModuleId is null ? "" : _moduleComboBox.Text)}");
await RestartRuntimeAfterSettingChangeAsync();
}
private async Task RestartRuntimeAfterSettingChangeAsync()
{
var options = BuildOptions();
if (_runtimeSession.IsRunning && options == _runtimeSession.CurrentOptions)
{
return;
}
AppendLog("设置已变更, 重新启动运行");
await StartOrRestartRuntimeAsync(restart: _runtimeSession.HasSession);
}
private void WriteSnapshotLog(RenderSnapshot snapshot)
{
if (!string.Equals(snapshot.ScanFailureReason, _lastLoggedScanFailureReason, StringComparison.Ordinal))
{
if (string.IsNullOrWhiteSpace(snapshot.ScanFailureReason))
{
if (!string.IsNullOrWhiteSpace(_lastLoggedScanFailureReason))
{
AppendLog("扫描已恢复");
}
}
else
{
AppendLog($"扫描失败: {snapshot.ScanFailureReason}");
}
_lastLoggedScanFailureReason = snapshot.ScanFailureReason;
}
var classSpec = snapshot.ClassName is null ? null : $"{snapshot.ClassName} / {snapshot.SpecName ?? "-"}";
if (!string.IsNullOrWhiteSpace(classSpec) && classSpec != _lastLoggedClass)
{
_lastLoggedClass = classSpec;
AppendLog($"识别职业: {classSpec}");
}
if (_lastLoggedEnabled != snapshot.Enabled)
{
_lastLoggedEnabled = snapshot.Enabled;
AppendLog(snapshot.Enabled ? "逻辑已开启" : "逻辑已关闭");
}
if (snapshot.ModuleName != _lastLoggedModule)
{
_lastLoggedModule = snapshot.ModuleName;
if (!string.IsNullOrWhiteSpace(snapshot.ModuleName))
{
AppendLog($"匹配模块: {snapshot.ModuleName}");
}
}
if (!string.IsNullOrWhiteSpace(snapshot.CurrentStep))
{
var details = BuildStepLogDetails(snapshot);
if (snapshot.CurrentStep != _lastLoggedStep || details != _lastLoggedStepDetails)
{
_lastLoggedStep = snapshot.CurrentStep;
_lastLoggedStepDetails = details;
AppendLog($"步骤: {snapshot.CurrentStep}{details}");
}
}
}
private static string BuildStepLogDetails(RenderSnapshot snapshot)
{
var fields = new (string Key, string Label)[]
{
("动作单位", "目标"),
("动作按键", "按键"),
("动作延迟", "动作延迟"),
("逻辑延迟", "逻辑延迟"),
("规则编号", "规则编号"),
("限流键", "限流键"),
("发送失败", "发送失败")
};
var details = new List<string>();
foreach (var (key, label) in fields)
{
if (!snapshot.UnitInfo.TryGetValue(key, out var value))
{
continue;
}
var text = UiTheme.FormatValue(value);
if (!string.IsNullOrWhiteSpace(text))
{
details.Add($"{label}: {text}");
}
}
return details.Count == 0 ? string.Empty : $"{string.Join("", details)}";
}
private void SetRuntimeControls(bool running)
{
if (!running)
{
UpdateHeaderIconColor(null);
UpdateLogicStatusLabel(enabled: false);
}
_enableButton.Enabled = running;
}
private void UpdateLogicStatusLabel(bool enabled)
{
_runtimeStatusLabel.Text = string.Empty;
_titleLabel.ForeColor = enabled ? UiTheme.Accent : UiTheme.Text;
}
private void PostToUi(Action action)
{
if (IsDisposed)
{
return;
}
if (InvokeRequired)
{
try
{
BeginInvoke(action);
}
catch (InvalidOperationException)
{
// Form is closing.
}
return;
}
action();
}
private void AppendLog(string message)
{
_statusForm.AppendLog(message);
}
private void BeginCaptureToggleKey()
{
ShowSettingsView();
if (_isCapturingToggleKey)
{
return;
}
_isCapturingToggleKey = true;
_toggleKeyButton.Text = "请按任意键...";
ActiveControl = null;
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (_isCapturingToggleKey)
{
return TryHandleCapturedKey(keyData & Keys.KeyCode);
}
return base.ProcessCmdKey(ref msg, keyData);
}
private bool TryHandleCapturedKey(Keys key)
{
if (key is Keys.Escape)
{
_isCapturingToggleKey = false;
SetToggleKeyButtonText();
AppendLog("已取消按键录入");
return true;
}
if (IsUnsupportedToggleKey(key.ToString()))
{
_toggleKeyButton.Text = "ALT 不支持";
AppendLog("触发键不支持 ALT, 请重试");
_ = ResetCaptureButtonTextAsync();
_isCapturingToggleKey = false;
return true;
}
var keyName = TryMapKeyToHotkey(key);
if (keyName is null)
{
_toggleKeyButton.Text = "不支持";
AppendLog("该按键暂不支持, 请重试");
_ = ResetCaptureButtonTextAsync();
_isCapturingToggleKey = false;
return true;
}
_isCapturingToggleKey = false;
_toggleKeyName = keyName;
SetToggleKeyButtonText();
SaveUiCache();
AppendLog($"已录入触发键: {_toggleKeyName}");
HandleSettingCommitted(this, EventArgs.Empty);
return true;
}
public bool PreFilterMessage(ref Message m)
{
if (!_isCapturingToggleKey)
{
return false;
}
const int WmXButtonDown = 0x020B;
const int WmKeyDown = 0x0100;
const int WmSysKeyDown = 0x0104;
if (m.Msg is WmKeyDown or WmSysKeyDown)
{
return TryHandleCapturedKey((Keys)(int)m.WParam);
}
if (m.Msg != WmXButtonDown)
{
return false;
}
var xButton = (((int)m.WParam) >> 16) & 0xFFFF;
var keyName = xButton switch
{
1 => "XBUTTON1",
2 => "XBUTTON2",
_ => null
};
if (keyName is null)
{
return false;
}
_isCapturingToggleKey = false;
_toggleKeyName = keyName;
SetToggleKeyButtonText();
SaveUiCache();
AppendLog($"已录入触发键: {_toggleKeyName}");
HandleSettingCommitted(this, EventArgs.Empty);
return true;
}
private void ApplyCachedWindowState()
{
if (_uiCache.MainWindowBounds is { } mainBounds)
{
var restoredBounds = new Rectangle(
mainBounds.X,
mainBounds.Y,
Math.Max(MinimumSize.Width, mainBounds.Width),
Math.Max(MinimumSize.Height, mainBounds.Height));
if (UiCacheStore.IsBoundsVisible(restoredBounds))
{
StartPosition = FormStartPosition.Manual;
Bounds = restoredBounds;
}
}
else if (_uiCache.MainWindowLocation is { } mainLocation)
{
var restoredBounds = new Rectangle(mainLocation.X, mainLocation.Y, Width, Height);
if (UiCacheStore.IsBoundsVisible(restoredBounds))
{
StartPosition = FormStartPosition.Manual;
Location = new Point(mainLocation.X, mainLocation.Y);
}
}
_statusForm.ApplyCachedBounds(_uiCache.SettingsWindowBounds);
}
private void SaveUiCache()
{
var latestCache = UiCacheStore.Load();
_uiCache.ModuleRulesGridColumns = latestCache.ModuleRulesGridColumns;
_uiCache.MainWindowBounds = new WindowBounds
{
X = Left,
Y = Top,
Width = Width,
Height = Height
};
_uiCache.MainWindowLocation = new WindowLocation
{
X = Left,
Y = Top
};
if (_statusForm.HasKnownBounds)
{
_uiCache.SettingsWindowBounds = _statusForm.GetCachedBounds();
}
_uiCache.ToggleKey = _toggleKeyName;
_uiCache.SelectedModuleId = _selectedModuleId;
UiCacheStore.Save(_uiCache);
}
private void ShowSettingsView()
{
RefreshModuleSelector(_lastSnapshot, reloadModules: true);
_statusForm.ShowSettings(_lastSnapshot);
}
private async Task ResetCaptureButtonTextAsync()
{
await Task.Delay(1000);
if (!IsDisposed)
{
PostToUi(SetToggleKeyButtonText);
}
}
private void CancelToggleKeyCapture()
{
if (!_isCapturingToggleKey)
{
return;
}
_isCapturingToggleKey = false;
SetToggleKeyButtonText();
}
private void SetToggleKeyButtonText()
{
_toggleKeyButton.Text = _toggleKeyName;
}
private nint HitTestResizeGrip(Point clientPoint)
{
var left = clientPoint.X <= ResizeGripSize;
var right = clientPoint.X >= ClientSize.Width - ResizeGripSize;
var top = clientPoint.Y <= ResizeGripSize;
var bottom = clientPoint.Y >= ClientSize.Height - ResizeGripSize;
if (top && left)
{
return NativeMethods.HtTopLeft;
}
if (top && right)
{
return NativeMethods.HtTopRight;
}
if (bottom && left)
{
return NativeMethods.HtBottomLeft;
}
if (bottom && right)
{
return NativeMethods.HtBottomRight;
}
if (left)
{
return NativeMethods.HtLeft;
}
if (right)
{
return NativeMethods.HtRight;
}
if (top)
{
return NativeMethods.HtTop;
}
if (bottom)
{
return NativeMethods.HtBottom;
}
return NativeMethods.HtClient;
}
private string? TryMapKeyToHotkey(Keys key)
{
var keyName = key.ToString().ToUpperInvariant();
if (IsUnsupportedToggleKey(keyName))
{
return null;
}
if (key is >= Keys.D0 and <= Keys.D9)
{
return ((char)('0' + (key - Keys.D0))).ToString();
}
if (key is >= Keys.NumPad0 and <= Keys.NumPad9)
{
return $"NUMPAD{key - Keys.NumPad0}";
}
return keyName switch
{
"OEMCOMMA" => ",",
"OEMPERIOD" => ".",
"OEMQUESTION" => "/",
"OEMSEMICOLON" => ";",
"OEMQUOTES" => "'",
"OEMOPENBRACKETS" => "[",
"OEMCLOSEBRACKETS" => "]",
"OEMPLUS" => "=",
"OEMMINUS" => "-",
"OEMTILDE" => "`",
"OEMBACKSLASH" => "\\",
"DECIMAL" => "NUMPADDECIMAL",
"ADD" => "NUMPADPLUS",
"SUBTRACT" => "NUMPADMINUS",
"MULTIPLY" => "NUMPADMULTIPLY",
"DIVIDE" => "NUMPADDIVIDE",
_ => _triggerKeyState.ResolveVirtualKey(keyName) is not null ? keyName : null
};
}
private static bool IsUnsupportedToggleKey(string keyName)
{
var key = keyName.Trim().ToUpperInvariant();
return key is "ALT" or "MENU" or "LMENU" or "RMENU";
}
private static Label CreateInfoLabel(string text)
{
return new Label
{
Text = text,
AutoSize = true,
ForeColor = UiTheme.Muted,
Margin = new Padding(0, 0, 0, 4)
};
}
private static string GetWindowTitle()
{
var randomizedName = Environment.GetEnvironmentVariable(AppPaths.RandomizedDisplayNameEnvironmentKey);
return string.IsNullOrWhiteSpace(randomizedName) ? "Shigure" : randomizedName;
}
private void EnableDrag(Control control)
{
control.MouseDown += (_, e) =>
{
if (e.Button == MouseButtons.Left)
{
NativeMethods.ReleaseCapture();
NativeMethods.SendMessageW(Handle, NativeMethods.WmNcLButtonDown, NativeMethods.HtCaption, 0);
}
};
}
private static void ConfigureTopBarButton(Button button)
{
button.AutoSize = false;
button.Size = new Size(88, 36);
button.Padding = new Padding(4, 1, 4, 1);
}
private sealed record ModuleSelectionOption(string? ModuleId, string Text)
{
public static readonly ModuleSelectionOption Auto = new(null, "自动选择(最匹配)");
public override string ToString()
{
return Text;
}
}
private static Icon? LoadApplicationIcon()
{
try
{
return Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
catch
{
return null;
}
}
private void TryApplyApplicationIcon()
{
var icon = LoadApplicationIcon();
if (icon != null)
{
Icon = icon;
}
}
private static string ModeLabel(SendMode mode)
{
return mode switch
{
SendMode.Click => "单击",
SendMode.Hold => "按住",
_ => "开关"
};
}
}