mirror of
https://github.com/waynebian01/Shigure.git
synced 2026-09-03 07:15:24 +08:00
feat: 引入 WowProcessLocator 以支持动态进程窗口查找,移除窗口标题参数,更新相关逻辑
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 解析
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Shigure;
|
||||
|
||||
/// <summary>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>定位 Fuyutsui 插件根目录(含 class/、core/)。</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
158
Infrastructure/WowProcessLocator.cs
Normal file
158
Infrastructure/WowProcessLocator.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace Shigure;
|
||||
|
||||
/// <summary>
|
||||
/// 按 wow_process.txt 中的进程名,从 Windows Z 顺序顶部查找第一个可见顶层窗口。
|
||||
/// 每次查询都会重新读取配置与窗口顺序,以便运行期间直接切换游戏窗口或修改进程名。
|
||||
/// </summary>
|
||||
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<uint> GetCandidateProcessIds()
|
||||
{
|
||||
var result = new HashSet<uint>();
|
||||
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<string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -8,7 +8,10 @@ public sealed record ScreenScanResult(
|
||||
IReadOnlyDictionary<int, int>? RowData,
|
||||
IReadOnlyDictionary<int, int> BarData,
|
||||
IReadOnlyDictionary<int, int> 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<int, int>();
|
||||
var emptyAbsorb = new Dictionary<int, int>();
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<None Include="config\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="keymap\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="module\**\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="wow_process.txt" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
2
wow_process.txt
Normal file
2
wow_process.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Wow
|
||||
WowT
|
||||
Reference in New Issue
Block a user