using System.Text;
using Simulator = XFEExtension.NetCore.InputSimulator.InputSimulator;
namespace XFEExtension.NetCore.InputSimulator.ConsoleApp;
internal static class Program
{
private static async Task<int> 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<ScanCode>()));
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<ScanCode>().FirstOrDefault(name => name.Equals(value, StringComparison.OrdinalIgnoreCase));
return name is null ? null : Enum.Parse<ScanCode>(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);
}
using System.Text;
using Simulator = XFEExtension.NetCore.InputSimulator.InputSimulator;
namespace XFEExtension.NetCore.InputSimulator.ConsoleApp;
internal static class Program
{
private static async Task<int> 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<ScanCode>()));
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<ScanCode>().FirstOrDefault(name => name.Equals(value, StringComparison.OrdinalIgnoreCase));
return name is null ? null : Enum.Parse<ScanCode>(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);
}