using System.Text; using Simulator = XFEExtension.NetCore.InputSimulator.InputSimulator; namespace XFEExtension.NetCore.InputSimulator.ConsoleApp; internal static class Program { private static async Task Main(string[] args) { Console.OutputEncoding = Encoding.UTF8; Console.InputEncoding = Encoding.UTF8; if (args is ["--help"] or ["-h"]) { Console.WriteLine("InputSimulator.Console.exe [--dry-run | --driver-check | --driver-install]"); Console.WriteLine("无参数:交互式真实输入测试;--dry-run:只打印事件,跳过倒计时,不发送输入。"); Console.WriteLine("真实输入使用内置 UMDF HID 驱动;不会自动安装。--driver-check 只检测设备和协议,不安装、不占用输入会话。"); Console.WriteLine("--driver-install:安装或更新为内嵌驱动包,更换签名证书时使用;需要管理员授权。"); Console.WriteLine("选择测试后切回目标窗口;默认倒计时 5 秒,F8 取消;Ctrl+C 取消当前操作。"); return 0; } if (args is ["--driver-check"] or ["--driver-install"]) { try { if (args[0] == "--driver-install") DriverDeployment.InstallEmbeddedDriver(); return PrintDriverStatus().IsAvailable ? 0 : 1; } catch (Exception error) { Console.Error.WriteLine(error.Message); return 1; } } var preview = args is ["--dry-run"]; if (args.Length != 0 && !preview) { Console.Error.WriteLine("不支持的参数。用 --help 查看帮助。"); return 2; } if (!preview && !OperatingSystem.IsWindows()) { Console.Error.WriteLine("真实输入测试只支持 Windows。"); return 2; } if (!preview && Console.IsInputRedirected) { Console.Error.WriteLine("请在交互式控制台中运行;通过管道检查流程时请使用 --dry-run。"); return 2; } Console.WriteLine("=== InputSimulator 手动测试 / .NET 10 ==="); Console.WriteLine(preview ? "预演模式:只打印事件,不加载驱动或发送真实输入。" : "后端:项目自有 UMDF 虚拟 HID 驱动。启动和测试均不会自动安装;9 检测,I 安装或更新。"); Console.WriteLine($"内嵌驱动:{(DriverDeployment.HasEmbeddedDriver ? "有" : "缺失")};签名:{(DriverDeployment.IsEmbeddedDriverSigned ? "已校验" : "开发版,未签名,不能安装")}"); Console.WriteLine($"签名类型:{DriverDeployment.EmbeddedDriverSigningKind};证书到期:{DriverDeployment.EmbeddedDriverCertificateExpires:yyyy-MM-dd}"); Console.WriteLine("驱动设备仍可被目标识别;发送成功不代表游戏已接受。"); Console.WriteLine("真实测试时请在倒计时内 Alt+Tab 切回目标,并松开切换窗口使用的按键。"); Console.WriteLine(preview ? "Ctrl+C 取消当前预演。0 退出。" : "F8 取消倒计时或执行中的测试;控制台内也可以 Ctrl+C 取消。0 退出。"); var countdown = 5; var exitCode = 0; while (true) { Console.WriteLine(); Console.WriteLine("1 W 短按 100ms 2 W 长按 2 秒"); Console.WriteLine("3 空格 100ms 4 Shift + W 长按 2 秒"); Console.WriteLine("5 鼠标向右相对移动 6 鼠标按键测试(可选左/右/中/侧键)"); Console.WriteLine("7 滚轮向下一格 8 自定义键/组合键、保持时间和次数"); Console.WriteLine("9 检测驱动状态 I 安装或更新驱动(需要管理员授权)"); Console.WriteLine($"T 设置倒计时(当前 {countdown} 秒) K 查看可用键名 0 退出"); Console.Write("请选择 > "); var choice = Console.ReadLine()?.Trim().ToUpperInvariant(); if (choice is null or "0" or "Q") return exitCode; try { if (choice == "T") { countdown = ReadNumber("倒计时秒数", countdown, 1, 30); continue; } if (choice == "K") { Console.WriteLine(string.Join(", ", Enum.GetNames())); Console.WriteLine("F8 保留为取消键。Ctrl / Shift / Alt / Esc 可作为左侧修饰键或 Escape 的简写。"); continue; } if (choice == "9") { if (preview) Console.WriteLine("预演模式:跳过设备检测,不打开驱动。"); else PrintDriverStatus(); Console.WriteLine("系统同时只允许一个控制器连接。最多同时按住 6 个普通键及 8 个修饰键;不支持直接 Unicode 输入和绝对鼠标定位。"); continue; } if (choice == "I") { if (preview) Console.WriteLine("预演模式:跳过驱动安装,不请求管理员授权。"); else { DriverDeployment.InstallEmbeddedDriver(); PrintDriverStatus(); } continue; } var test = CreateTest(choice); if (test is null) { Console.WriteLine("请输入菜单中的编号或字母。"); continue; } await RunTestAsync(test, countdown, preview); } catch (OperationCanceledException) { Console.WriteLine("已取消;本次持有的键已清理。切回控制台可再次选择。"); } catch (Exception error) { exitCode = 1; Console.Error.WriteLine($"操作未完成:{error.Message}"); if (error is AggregateException aggregate) foreach (var inner in aggregate.Flatten().InnerExceptions) Console.Error.WriteLine($" {inner.Message}"); Console.WriteLine("可选择 9 检测驱动,或 I 显式安装或更新。清理报错时请手动按下并松开对应键。"); } } } private static DriverCheckResult PrintDriverStatus() { var result = DriverDeployment.CheckDriver(); Console.WriteLine($"驱动状态:{result.Status};{result.Message}"); Console.WriteLine($"内嵌包签名:{DriverDeployment.EmbeddedDriverSigningKind};时间戳:{DriverDeployment.IsEmbeddedDriverTimestamped};证书到期:{DriverDeployment.EmbeddedDriverCertificateExpires:O}"); if (!result.IsAvailable) Console.WriteLine("需要安装或更新时,选择 I 或运行 --driver-install;检测不会触发安装。"); return result; } private static ManualTest? CreateTest(string choice) => choice switch { "1" => new("W 短按 100ms", new InputSequence().PressKey(ScanCode.W, 100)), "2" => new("W 长按 2 秒", new InputSequence().PressKey(ScanCode.W, 2000)), "3" => new("空格 100ms", new InputSequence().PressKey(ScanCode.Space, 100)), "4" => new("Shift + W 长按 2 秒", new InputSequence().PressCombination([ScanCode.LeftShift, ScanCode.W], 2000)), "5" => new("鼠标向右相对移动 120 单位(12 次,每次 10;实际距离取决于目标)", new InputSequence().MouseMove(10, 0).Wait(20), 12), "6" => ReadMouseTest(), "7" => new("滚轮向下一格", new InputSequence().MouseWheel(-120)), "8" => ReadKeyboardTest(), _ => null }; private static ManualTest ReadMouseTest() { Console.WriteLine("1 左键,2 右键,3 中键,4 侧键 XButton1,5 侧键 XButton2"); var button = (MouseButton)(ReadNumber("鼠标键", 1, 1, 5) - 1); var hold = ReadNumber("保持毫秒", 100, 1, 10000); return new($"鼠标 {button},保持 {hold}ms", new InputSequence().MouseClick(button, hold)); } private static ManualTest ReadKeyboardTest() { ScanCode[] keys; while (true) { Console.Write("键名或组合(例如 W、Space、Ctrl+A;/cancel 返回菜单)[W] > "); var value = ReadValue(); if (value.Length == 0) value = "W"; var parts = value.Split('+', StringSplitOptions.TrimEntries); var parsed = parts.Select(ParseKey).ToArray(); if (parsed.Length is < 1 or > 8 || parsed.Any(key => key is null or ScanCode.F8)) { Console.WriteLine("请输入 1 到 8 个有效键名,用 + 分隔;F8 保留为取消键。主菜单 K 可查看键名。"); continue; } keys = parsed.Select(key => key!.Value).Distinct().ToArray(); break; } var hold = ReadNumber("保持毫秒", 100, 1, 10000); var repeat = ReadNumber("重复次数", 1, 1, 100); var interval = repeat > 1 ? ReadNumber("每次结束后等待毫秒", 500, 0, 10000) : 0; return new($"{string.Join(" + ", keys)},保持 {hold}ms,重复 {repeat} 次,结束后等待 {interval}ms", new InputSequence().PressCombination(keys, hold).Wait(interval), repeat); } private static ScanCode? ParseKey(string value) { value = value.ToUpperInvariant() switch { "CTRL" => nameof(ScanCode.LeftControl), "SHIFT" => nameof(ScanCode.LeftShift), "ALT" => nameof(ScanCode.LeftAlt), "ESC" => nameof(ScanCode.Escape), _ when value.Length == 1 && char.IsAsciiDigit(value[0]) => $"D{value}", _ => value }; // 只接受命名键;Enum.TryParse 的数字输入会被解释为底层枚举值。 var name = Enum.GetNames().FirstOrDefault(name => name.Equals(value, StringComparison.OrdinalIgnoreCase)); return name is null ? null : Enum.Parse(name); } private static int ReadNumber(string prompt, int defaultValue, int minimum, int maximum) { while (true) { Console.Write($"{prompt}({minimum}..{maximum},/cancel 返回菜单)[{defaultValue}] > "); var value = ReadValue(); if (value.Length == 0) return defaultValue; if (int.TryParse(value, out var number) && number >= minimum && number <= maximum) return number; Console.WriteLine($"请输入 {minimum} 到 {maximum} 之间的整数。"); } } private static string ReadValue(bool trim = true) { var value = Console.ReadLine(); if (value is null || value.Trim().Equals("/cancel", StringComparison.OrdinalIgnoreCase)) throw new OperationCanceledException(); return trim ? value.Trim() : value; } private static async Task RunTestAsync(ManualTest test, int countdown, bool preview) { using var cancellation = new CancellationTokenSource(); using var stopWatching = new CancellationTokenSource(); ConsoleCancelEventHandler cancelHandler = (_, e) => { e.Cancel = true; cancellation.Cancel(); }; Console.CancelKeyPress += cancelHandler; var watcher = preview ? Task.CompletedTask : WatchStopKeyAsync(cancellation, stopWatching.Token); try { // Connect to the already installed driver before starting the countdown. using var input = new InputController(preview ? new PreviewBackend() : new DriverBackend()); Console.WriteLine($"\n本次测试:{test.Name}"); if (!preview) { Console.WriteLine("现在切回目标窗口,F8 可取消。"); for (var seconds = countdown; seconds > 0; seconds--) { Console.WriteLine($" {seconds} 秒后执行…"); await Task.Delay(1000, cancellation.Token); } } cancellation.Token.ThrowIfCancellationRequested(); // 每次测试独立拥有控制器;完成、取消和异常退出都会尝试清理。 await test.Sequence.PlayAsync(input, test.Repeat, cancellation.Token); } finally { stopWatching.Cancel(); try { await watcher; } finally { Console.CancelKeyPress -= cancelHandler; } } Console.WriteLine(preview ? "预演完成,没有发送真实输入。" : "输入已提交,按键已释放。是否识别请查看目标程序;切回控制台可再次选择。"); } private static async Task WatchStopKeyAsync(CancellationTokenSource operation, CancellationToken stop) { try { while (true) { stop.ThrowIfCancellationRequested(); if (Simulator.GetKeyDown(ScanCode.F8)) { operation.Cancel(); return; } await Task.Delay(20, stop); } } catch (OperationCanceledException) when (stop.IsCancellationRequested) { } catch { // 无法监测取消键时,停止操作,不让测试在失去取消能力后继续。 operation.Cancel(); throw; } } private sealed record ManualTest(string Name, InputSequence Sequence, int Repeat = 1); }