diff --git a/App/AppOptions.cs b/App/AppOptions.cs index c820b280..2f416a2b 100644 --- a/App/AppOptions.cs +++ b/App/AppOptions.cs @@ -8,7 +8,6 @@ public enum SendMode } public sealed record AppOptions( - string WindowTitle, string ToggleKey, SendMode Mode, string? ModuleId, @@ -17,7 +16,6 @@ public sealed record AppOptions( { public static AppOptions FromArgs(string[] args) { - var windowTitle = "魔兽世界"; var toggleKey = "XBUTTON2"; var mode = SendMode.Switch; string? moduleId = null; @@ -30,10 +28,6 @@ public sealed record AppOptions( var value = i + 1 < args.Length ? args[i + 1] : null; switch (arg) { - case "--window" when value is not null: - windowTitle = value; - i++; - break; case "--toggle" when value is not null: toggleKey = value; i++; @@ -57,7 +51,7 @@ public sealed record AppOptions( } } - return new AppOptions(windowTitle, toggleKey, mode, moduleId, logicInterval, renderInterval); + return new AppOptions(toggleKey, mode, moduleId, logicInterval, renderInterval); } private static SendMode ParseMode(string value) diff --git a/App/Program.cs b/App/Program.cs index 9a590cb1..e7fa5350 100644 --- a/App/Program.cs +++ b/App/Program.cs @@ -32,7 +32,8 @@ internal static class Program var baseDirectory = AppPaths.BaseDirectory; var moduleStore = new ModuleStore(ModuleStore.ResolveModuleDirectory(baseDirectory)); var triggerKeyState = new WindowsTriggerKeyState(); - var runtimeFactory = new ShigureRuntimeFactory(baseDirectory, moduleStore, triggerKeyState); + var processLocator = new WowProcessLocator(baseDirectory); + var runtimeFactory = new ShigureRuntimeFactory(baseDirectory, moduleStore, triggerKeyState, processLocator); var runtimeSession = new RuntimeSessionCoordinator(runtimeFactory); Application.Run(new MainForm( @@ -40,6 +41,7 @@ internal static class Program baseDirectory, moduleStore, triggerKeyState, + processLocator, runtimeSession)); } } diff --git a/App/ShigureRuntimeFactory.cs b/App/ShigureRuntimeFactory.cs index c24d5a11..20a513da 100644 --- a/App/ShigureRuntimeFactory.cs +++ b/App/ShigureRuntimeFactory.cs @@ -10,17 +10,20 @@ internal sealed class ShigureRuntimeFactory : IShigureRuntimeFactory private readonly string _baseDirectory; private readonly ModuleStore _moduleStore; private readonly ITriggerKeyState _triggerKeyState; + private readonly WowProcessLocator _processLocator; private readonly TimeProvider _timeProvider; public ShigureRuntimeFactory( string baseDirectory, ModuleStore moduleStore, ITriggerKeyState triggerKeyState, + WowProcessLocator processLocator, TimeProvider? timeProvider = null) { _baseDirectory = baseDirectory; _moduleStore = moduleStore; _triggerKeyState = triggerKeyState; + _processLocator = processLocator; _timeProvider = timeProvider ?? TimeProvider.System; } @@ -32,9 +35,9 @@ internal sealed class ShigureRuntimeFactory : IShigureRuntimeFactory return new ShigureRuntime( options, - new PixelScanner(options.WindowTitle), + new PixelScanner(_processLocator), new StateBuilder(config), - new KeySender(options.WindowTitle), + new KeySender(_processLocator), _triggerKeyState, new LogicRegistry(keymap, _moduleStore, options.ModuleId), _timeProvider); diff --git a/CLAUDE.md b/CLAUDE.md index 649a1fec..ad71d189 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,12 +13,12 @@ Shigure 是一个 **Windows WinForms(.NET 10)** 桌面程序:扫描目标 ```powershell dotnet build .\Shigure.csproj dotnet run --project .\Shigure.csproj -dotnet run --project .\Shigure.csproj -- --window 魔兽世界 --toggle XBUTTON2 --mode switch --logic-ms 100 --render-ms 100 +dotnet run --project .\Shigure.csproj -- --toggle XBUTTON2 --mode switch --logic-ms 100 --render-ms 100 ``` - 目标框架 `net10.0-windows`,`WinExe`,`Nullable`/`ImplicitUsings` 均 enable。 - **没有测试项目**:验证 = 能编译 + 实际运行点开「设置」走查。`dotnet build` 干净通过(0 警告 0 错误)是基线要求。 -- 启动参数见 [README.md](README.md#运行)(`--window/--toggle/--mode/--logic-ms/--render-ms`),解析在 [App/AppOptions.cs](App/AppOptions.cs)。 +- 启动参数见 [README.md](README.md#运行)(`--toggle/--mode/--logic-ms/--render-ms`),解析在 [App/AppOptions.cs](App/AppOptions.cs)。目标进程名来自 `wow_process.txt`。 ### ⚠️ 随机重启会拦截普通运行 @@ -74,7 +74,7 @@ config/ keymap/ module/ 运行时 JSON 数据(构建时复制到输出, 见 .c ### 定位 -[Infrastructure/WowAddonLocator.cs](Infrastructure/WowAddonLocator.cs) 通过 `FindWindow` → 进程路径 → 向上查找 `Interface\AddOns\Fuyutsui`。提供三个入口:`FindAddonRoot`、`FindClassDirectory`(`class/` 子目录)、`FindClassMacrosPath`(`core/classmacros.lua`)。 +[Infrastructure/WowProcessLocator.cs](Infrastructure/WowProcessLocator.cs) 读取 `wow_process.txt`,按 Windows Z 顺序选择最靠前的候选进程可见顶层窗口;[Infrastructure/WowAddonLocator.cs](Infrastructure/WowAddonLocator.cs) 再由该进程路径向上查找 `Interface\AddOns\Fuyutsui`。提供三个入口:`FindAddonRoot`、`FindClassDirectory`(`class/` 子目录)、`FindClassMacrosPath`(`core/classmacros.lua`)。 ### Lua 解析 diff --git a/Infrastructure/WowAddonLocator.cs b/Infrastructure/WowAddonLocator.cs index 8979b0e4..d3eadb56 100644 --- a/Infrastructure/WowAddonLocator.cs +++ b/Infrastructure/WowAddonLocator.cs @@ -1,5 +1,3 @@ -using System.Text; - namespace Shigure; /// @@ -11,9 +9,9 @@ internal static class WowAddonLocator private const string ClassRelativePath = @"Interface\AddOns\Fuyutsui\class"; private const string ClassMacrosRelativePath = @"Interface\AddOns\Fuyutsui\core\classmacros.lua"; - public static string? FindClassDirectory(string windowTitle) + public static string? FindClassDirectory(WowProcessLocator processLocator) { - var addonRoot = FindAddonRoot(windowTitle); + var addonRoot = FindAddonRoot(processLocator); if (addonRoot is null) { return null; @@ -24,9 +22,9 @@ internal static class WowAddonLocator } /// 定位 Fuyutsui 插件根目录(含 class/、core/)。 - public static string? FindAddonRoot(string windowTitle) + public static string? FindAddonRoot(WowProcessLocator processLocator) { - var exePath = TryGetProcessPathByWindowTitle(windowTitle); + var exePath = processLocator.FindFrontmostProcessPath(); if (string.IsNullOrWhiteSpace(exePath)) { return null; @@ -54,9 +52,9 @@ internal static class WowAddonLocator return null; } - public static string? FindClassMacrosPath(string windowTitle) + public static string? FindClassMacrosPath(WowProcessLocator processLocator) { - var addonRoot = FindAddonRoot(windowTitle); + var addonRoot = FindAddonRoot(processLocator); if (addonRoot is not null) { var fromRoot = Path.Combine(addonRoot, "core", "classmacros.lua"); @@ -66,7 +64,7 @@ internal static class WowAddonLocator } } - var exePath = TryGetProcessPathByWindowTitle(windowTitle); + var exePath = processLocator.FindFrontmostProcessPath(); if (string.IsNullOrWhiteSpace(exePath)) { return null; @@ -86,46 +84,4 @@ internal static class WowAddonLocator return null; } - - private static string? TryGetProcessPathByWindowTitle(string windowTitle) - { - if (string.IsNullOrWhiteSpace(windowTitle)) - { - return null; - } - - var hwnd = NativeMethods.FindWindow(null, windowTitle.Trim()); - if (hwnd == 0) - { - return null; - } - - _ = NativeMethods.GetWindowThreadProcessId(hwnd, out var processId); - if (processId == 0) - { - return null; - } - - var handle = NativeMethods.OpenProcess(NativeMethods.ProcessQueryLimitedInformation, false, processId); - if (handle == 0) - { - return null; - } - - try - { - var buffer = new StringBuilder(1024); - var size = buffer.Capacity; - if (!NativeMethods.QueryFullProcessImageName(handle, 0, buffer, ref size) || size <= 0) - { - return null; - } - - return buffer.ToString(); - } - finally - { - _ = NativeMethods.CloseHandle(handle); - } - } } diff --git a/Infrastructure/WowProcessLocator.cs b/Infrastructure/WowProcessLocator.cs new file mode 100644 index 00000000..2c1fc2c2 --- /dev/null +++ b/Infrastructure/WowProcessLocator.cs @@ -0,0 +1,158 @@ +using System.Diagnostics; +using System.Text; + +namespace Shigure; + +/// +/// 按 wow_process.txt 中的进程名,从 Windows Z 顺序顶部查找第一个可见顶层窗口。 +/// 每次查询都会重新读取配置与窗口顺序,以便运行期间直接切换游戏窗口或修改进程名。 +/// +internal sealed class WowProcessLocator +{ + private const string ProcessFileName = "wow_process.txt"; + private readonly string _processFilePath; + + public WowProcessLocator(string baseDirectory) + { + _processFilePath = Path.Combine(baseDirectory, ProcessFileName); + } + + public string ProcessFilePath => _processFilePath; + + public nint FindFrontmostWindow() + { + var processIds = GetCandidateProcessIds(); + if (processIds.Count == 0) + { + return 0; + } + + nint foundWindow = 0; + _ = NativeMethods.EnumWindows((hwnd, lParam) => + { + if (!NativeMethods.IsWindowVisible(hwnd)) + { + return true; + } + + _ = NativeMethods.GetWindowThreadProcessId(hwnd, out var processId); + if (!processIds.Contains(processId)) + { + return true; + } + + foundWindow = hwnd; + return false; + }, 0); + + return foundWindow; + } + + public string? FindFrontmostProcessPath() + { + var hwnd = FindFrontmostWindow(); + if (hwnd == 0) + { + return null; + } + + _ = NativeMethods.GetWindowThreadProcessId(hwnd, out var processId); + return processId == 0 ? null : TryGetProcessPath(processId); + } + + public string DescribeConfiguredProcesses() + { + var names = ReadProcessNames(); + return names.Count == 0 ? "未配置" : string.Join("、", names); + } + + private HashSet GetCandidateProcessIds() + { + var result = new HashSet(); + foreach (var processName in ReadProcessNames()) + { + Process[] processes; + try + { + processes = Process.GetProcessesByName(processName); + } + catch (InvalidOperationException) + { + continue; + } + + foreach (var process in processes) + { + using (process) + { + try + { + result.Add(unchecked((uint)process.Id)); + } + catch (InvalidOperationException) + { + // 进程可能在枚举期间退出。 + } + } + } + } + + return result; + } + + private IReadOnlyList ReadProcessNames() + { + try + { + return File.ReadLines(_processFilePath) + .Select(NormalizeProcessName) + .Where(name => name is not null) + .Select(name => name!) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + catch (IOException) + { + return []; + } + catch (UnauthorizedAccessException) + { + return []; + } + } + + private static string? NormalizeProcessName(string line) + { + var name = line.Trim(); + if (name.Length == 0 || name.StartsWith('#') || name.StartsWith(';')) + { + return null; + } + + return name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) + ? name[..^4].Trim() + : name; + } + + private static string? TryGetProcessPath(uint processId) + { + var handle = NativeMethods.OpenProcess(NativeMethods.ProcessQueryLimitedInformation, false, processId); + if (handle == 0) + { + return null; + } + + try + { + var buffer = new StringBuilder(1024); + var size = buffer.Capacity; + return NativeMethods.QueryFullProcessImageName(handle, 0, buffer, ref size) && size > 0 + ? buffer.ToString() + : null; + } + finally + { + _ = NativeMethods.CloseHandle(handle); + } + } +} diff --git a/Input/KeySender.cs b/Input/KeySender.cs index 944d9e77..9eb1b4cb 100644 --- a/Input/KeySender.cs +++ b/Input/KeySender.cs @@ -2,27 +2,28 @@ public sealed class KeySender : IRuntimeKeyOutput { - private readonly string _windowTitle; + private readonly WowProcessLocator _processLocator; - public KeySender(string windowTitle) + internal KeySender(WowProcessLocator processLocator) { - _windowTitle = windowTitle; + _processLocator = processLocator; } public string? LastFailureReason { get; private set; } public bool Send(string hotkey) { - var result = SendCore(hotkey); + var result = SendCore(hotkey, expectedWindow: 0); LastFailureReason = result.FailureReason; return result.Succeeded; } public static int? GetVk(string keyName) => WindowsVirtualKeyMap.Resolve(keyName); - KeySendResult IRuntimeKeyOutput.Send(string hotkey) => SendCore(hotkey); + KeySendResult IRuntimeKeyOutput.Send(string hotkey, nint expectedWindow) + => SendCore(hotkey, expectedWindow); - private KeySendResult SendCore(string hotkey) + private KeySendResult SendCore(string hotkey, nint expectedWindow) { var (mods, mainKey) = ParseHotkey(hotkey); if (mainKey is null) @@ -36,10 +37,15 @@ public sealed class KeySender : IRuntimeKeyOutput return Fail($"无法识别主键“{mainKey}”"); } - var hwnd = NativeMethods.FindWindow(null, _windowTitle); + var hwnd = _processLocator.FindFrontmostWindow(); if (hwnd == 0) { - return Fail($"未找到目标窗口“{_windowTitle}”"); + return Fail($"未找到目标进程的可见窗口(wow_process.txt: {_processLocator.DescribeConfiguredProcesses()})"); + } + + if (expectedWindow != 0 && hwnd != expectedWindow) + { + return Fail("目标窗口已切换,等待重新扫描后再发送按键"); } // ParseHotkey 只产出去重后的 CTRL/ALT/SHIFT, 三者都在虚拟键表里且映射到互异 VK, diff --git a/Input/NativeMethods.cs b/Input/NativeMethods.cs index 67e47ce2..2a65ddaf 100644 --- a/Input/NativeMethods.cs +++ b/Input/NativeMethods.cs @@ -5,6 +5,8 @@ namespace Shigure; internal static class NativeMethods { + public delegate bool EnumWindowsProc(nint hWnd, nint lParam); + public const uint ProcessQueryLimitedInformation = 0x1000; public const uint WmKeyDown = 0x0100; @@ -47,8 +49,13 @@ internal static class NativeMethods public int Bottom; } - [DllImport("user32.dll", EntryPoint = "FindWindowW", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern nint FindWindow(string? lpClassName, string lpWindowName); + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, nint lParam); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool IsWindowVisible(nint hWnd); [DllImport("user32.dll", SetLastError = true)] public static extern uint GetWindowThreadProcessId(nint hWnd, out uint lpdwProcessId); diff --git a/README.md b/README.md index 361f1bc1..3cd043c8 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,10 @@ dotnet run --project .\Shigure.csproj 可选启动参数: ```powershell -dotnet run --project .\Shigure.csproj -- --window 魔兽世界 --toggle XBUTTON2 --mode switch --logic-ms 100 --render-ms 100 +dotnet run --project .\Shigure.csproj -- --toggle XBUTTON2 --mode switch --logic-ms 100 --render-ms 100 ``` -- `--window`:目标窗口标题,默认 `魔兽世界`。 +- `wow_process.txt`:每行一个目标进程名(可带或不带 `.exe`)。程序使用 Windows Z 顺序中最靠前的候选进程可见顶层窗口,切换窗口后会自动跟随。 - `--toggle`:触发键,默认 `XBUTTON2`。 - `--mode`:发送模式,支持 `switch`、`click`、`hold`。 - `--logic-ms`:逻辑循环间隔,默认 `100` ms,最小 `50` ms。 diff --git a/Runtime/PixelScanner.cs b/Runtime/PixelScanner.cs index 541b4cb2..6d679b4b 100644 --- a/Runtime/PixelScanner.cs +++ b/Runtime/PixelScanner.cs @@ -8,7 +8,10 @@ public sealed record ScreenScanResult( IReadOnlyDictionary? RowData, IReadOnlyDictionary BarData, IReadOnlyDictionary HealAbsorbData, - string? FailureReason); + string? FailureReason) +{ + internal nint TargetWindowHandle { get; init; } +} public sealed class PixelScanner : IRuntimeScreenScanner { @@ -16,11 +19,11 @@ public sealed class PixelScanner : IRuntimeScreenScanner private const int TopRowFirstSchemeMax = 255; private const int HealAbsorbMaxRows = 6; private const int HealAbsorbMaxUnits = 30; - private readonly string _windowTitle; + private readonly WowProcessLocator _processLocator; - public PixelScanner(string windowTitle) + internal PixelScanner(WowProcessLocator processLocator) { - _windowTitle = windowTitle; + _processLocator = processLocator; try { NativeMethods.SetProcessDPIAware(); @@ -35,15 +38,19 @@ public sealed class PixelScanner : IRuntimeScreenScanner { var emptyBars = new Dictionary(); var emptyAbsorb = new Dictionary(); - var hwnd = NativeMethods.FindWindow(null, _windowTitle); + var hwnd = _processLocator.FindFrontmostWindow(); if (hwnd == 0) { - return new ScreenScanResult(null, emptyBars, emptyAbsorb, $"未找到目标窗口“{_windowTitle}”"); + return new ScreenScanResult( + null, + emptyBars, + emptyAbsorb, + $"未找到目标进程的可见窗口(wow_process.txt: {_processLocator.DescribeConfiguredProcesses()})"); } if (NativeMethods.IsIconic(hwnd)) { - return new ScreenScanResult(null, emptyBars, emptyAbsorb, $"目标窗口“{_windowTitle}”已最小化"); + return new ScreenScanResult(null, emptyBars, emptyAbsorb, "最靠前的目标进程窗口已最小化"); } var point = new NativeMethods.Point(0, 0); @@ -82,13 +89,14 @@ public sealed class PixelScanner : IRuntimeScreenScanner var healAbsorbData = markerY is null ? emptyAbsorb : ScanHealAbsorbGrid(point.X, point.Y, width, height, markerY.Value); - return rowData.Count == 0 + var result = rowData.Count == 0 ? new ScreenScanResult(null, barData, healAbsorbData, "未找到有效的状态像素起始标记") : new ScreenScanResult( rowData, barData, healAbsorbData, markerY is null ? "未找到 CountBars 标记,层数条和治疗吸收数据未采集" : null); + return result with { TargetWindowHandle = hwnd }; } catch (Exception ex) { diff --git a/Runtime/RuntimeDependencies.cs b/Runtime/RuntimeDependencies.cs index 2133163a..b11d2b0b 100644 --- a/Runtime/RuntimeDependencies.cs +++ b/Runtime/RuntimeDependencies.cs @@ -36,7 +36,7 @@ public sealed record LogicEvaluation(string? ModuleName, LogicDecision? Decision internal interface IRuntimeKeyOutput { - KeySendResult Send(string hotkey); + KeySendResult Send(string hotkey, nint expectedWindow); } public readonly record struct KeySendResult(bool Succeeded, string? FailureReason) diff --git a/Runtime/ShigureRuntime.cs b/Runtime/ShigureRuntime.cs index ba756945..c96b8b24 100644 --- a/Runtime/ShigureRuntime.cs +++ b/Runtime/ShigureRuntime.cs @@ -253,7 +253,7 @@ public sealed class ShigureRuntime var sendAttemptAt = _timeProvider.GetUtcNow(); if (CanSend(decision, sendAttemptAt)) { - SendAndPauseLogic(decision); + SendAndPauseLogic(decision, scan.TargetWindowHandle); } } @@ -267,14 +267,14 @@ public sealed class ShigureRuntime var sendAttemptAt = _timeProvider.GetUtcNow(); if (CanSend(decision, sendAttemptAt)) { - SendAndPauseLogic(decision); + SendAndPauseLogic(decision, scan.TargetWindowHandle); } } } - private void SendAndPauseLogic(LogicDecision decision) + private void SendAndPauseLogic(LogicDecision decision, nint targetWindowHandle) { - var sendResult = _keySender.Send(decision.Hotkey!); + var sendResult = _keySender.Send(decision.Hotkey!, targetWindowHandle); if (!sendResult.Succeeded) { var info = _unitInfo.ToDictionary( diff --git a/Shigure.csproj b/Shigure.csproj index 8392b9bd..45684cea 100644 --- a/Shigure.csproj +++ b/Shigure.csproj @@ -24,6 +24,7 @@ + diff --git a/UI/MainForm.cs b/UI/MainForm.cs index 6e4fadda..c4b31ebc 100644 --- a/UI/MainForm.cs +++ b/UI/MainForm.cs @@ -51,6 +51,7 @@ public sealed class MainForm : Form, IMessageFilter 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; @@ -76,12 +77,14 @@ public sealed class MainForm : Form, IMessageFilter 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(); @@ -103,11 +106,11 @@ public sealed class MainForm : Form, IMessageFilter _moduleEditor = new ModuleEditorControl(_moduleStore, RestartRuntimeFromEditorAsync, _baseDirectory); _statusForm.AttachModuleEditor(_moduleEditor); _classConfigEditor = new ClassConfigEditorControl( - () => WowAddonLocator.FindClassDirectory(_initialOptions.WindowTitle), + () => WowAddonLocator.FindClassDirectory(_processLocator), UpdateConfigFromAddonAsync); _statusForm.AttachConfigEditor(_classConfigEditor); _classMacrosEditor = new ClassMacrosEditorControl( - () => WowAddonLocator.FindClassMacrosPath(_initialOptions.WindowTitle), + () => WowAddonLocator.FindClassMacrosPath(_processLocator), UpdateConfigFromAddonAsync); _statusForm.AttachMacrosEditor(_classMacrosEditor); _statusForm.FormClosing += (_, _) => @@ -672,14 +675,14 @@ public sealed class MainForm : Form, IMessageFilter return; } - var windowTitle = _initialOptions.WindowTitle; - var classDirectory = WowAddonLocator.FindClassDirectory(windowTitle); - var classMacrosPath = WowAddonLocator.FindClassMacrosPath(windowTitle); + var processNames = _processLocator.DescribeConfiguredProcesses(); + var classDirectory = WowAddonLocator.FindClassDirectory(_processLocator); + var classMacrosPath = WowAddonLocator.FindClassMacrosPath(_processLocator); if (string.IsNullOrWhiteSpace(classDirectory)) { - _configSourceLabel.Text = $"Fuyutsui class: 未找到(请先打开「{windowTitle}」窗口)"; + _configSourceLabel.Text = $"Fuyutsui class: 未找到(目标进程: {processNames})"; MessageBox.Show( - $"未找到「{windowTitle}」窗口下的 Interface\\AddOns\\Fuyutsui\\class 目录。\n请确认游戏已启动且已安装 Fuyutsui。", + $"未找到目标进程({processNames})对应的 Interface\\AddOns\\Fuyutsui\\class 目录。\n请确认游戏已启动且已安装 Fuyutsui。", "更新配置", MessageBoxButtons.OK, MessageBoxIcon.Warning); @@ -884,7 +887,7 @@ public sealed class MainForm : Form, IMessageFilter ResetRuntimeLogState(); SetRuntimeControls(running: true); - AppendLog($"运行已{(restart ? "重启" : "启动")}: {options.WindowTitle} / {options.ToggleKey} / {ModeLabel(options.Mode)}"); + AppendLog($"运行已{(restart ? "重启" : "启动")}: {_processLocator.DescribeConfiguredProcesses()} / {options.ToggleKey} / {ModeLabel(options.Mode)}"); return true; } diff --git a/wow_process.txt b/wow_process.txt new file mode 100644 index 00000000..84c3ea4e --- /dev/null +++ b/wow_process.txt @@ -0,0 +1,2 @@ +Wow +WowT \ No newline at end of file