两个基于C#的自动按键控制台程序

C#写的几个控制台程序,用来执行电脑的自动按键,另外还使用到了Windows API解决了使用DirectX键盘接口的游戏的问题

  从2024年初开始说学C#编程以来已经过了一年多了,虽最早只是想当成一个业余爱好来玩的,不过还是切切实实解决了一些实际的问题。也稍微学了下Winform,做出了几个非常小的窗体应用程序。不过我因为需求都比较简单,总的来说还是比较爱写控制台应用程序,也就是“黑框”那种。目前写的比较完善的是几个自动按键程序,用来在电脑上自动执行按键任务。

1. 自动按键发生器(AutoKeyGenerator)

  第一个程序AutoKeyGenerator,是基于SendKeys.SendWait方法,以及Timer定时器的委托事件,从而重复周期性地向Windows窗口发送某些特定的键盘按键的控制台应用程序。开发这个应用的初衷是为了作为一个工控软件的辅助程序使用。之前工作中遇到了一种设备附带有一个工控软件,因为这个工控软件本身在流程控制上的功能非常简陋,除了工艺相关的参数外,就几乎只有启动和停止了,而在实际使用中,该设备需要周期性反复启动10-20次,每次间隔时间往往在5-10min不等。也就是说必须有人专门看守这个工控软件将近3个小时,每隔几分钟就必须对它进行启动操作。当然,在那时这个人就是我,而且除了这个任务之外我还有别的主要任务要做,造成我必须每隔几分钟就去那个烦人的设备面前用鼠标点一下那个工控软件的启动按键。于是我写了个这样的程序来自动完成这个工作,这个程序经过我后续不断改进,现在已经能支持更多的功能,也更加稳定。程序完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Forms;

namespace AutoKeyGenerator
{
class Program
{
static int generatorMode; // Record the mode of the program, will be selected by user.
static string keyName = ""; // The key which was finally executed in each loops.
static string keyName1 = ""; // In mode 1, this variable is the single key, and in mode 2 means the first key (Mode 2A).
static string keyName2 = ""; // The second key in mode 2 (Mode 2B).
static bool isKey_2A = true; // In mode 2, it distinguishes the current execution is the first key or the second key.
static string realLocalTime; // Show the localtime of each executions.
static double timerIntervalInSecond; // Transform milliseconds into seconds.

static void Main(string[] args)
{
// Title of this console .
Console.Title = "AutoKeyGenerator V0.5";
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("\n*******************【自动按键发生器 V0.5】*******************\n");
// An ASCII art for AUTOKEY with colors
Console.ForegroundColor = ConsoleColor.DarkCyan;
Console.WriteLine(" █████╗ ██╗ ██╗████████╗ ██████╗ ██╗ ██╗███████╗██╗ ██╗\n ██╔══██╗██║ ██║╚══██╔══╝██╔═══██╗██║██╔═╝██╔════╝██║ ██║\n ███████║██║ ██║ ██║ ██║ ██║████╔╝ █████╗ ╚█████╔╝\n ██╔══██║██║ ██║ ██║ ██║ ██║██║██═╗ ██╔══╝ ╚██╔═╝\n ██║ ██║╚█████╔╝ ██║ ╚██████╔╝██║ ╚██╗███████╗ ██║\n ╚═╝ ╚═╝ ╚════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝\n");
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("*************************************************************\n");
Console.ResetColor();

// Auto task by a timer triggered event.
System.Timers.Timer aTimer = new System.Timers.Timer();
aTimer.Elapsed += new ElapsedEventHandler(ModeOption);

UserInputing_Mode: // Target of the program reset Goto command.
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("请选择模式:\n\n【1】单一指令\n\n【2】双指令交替\n\n【3】显示规则说明\n");
Console.ResetColor();

// User must input a key for mode selection in 1,2 and 3
// 1 is the single key mode(mode 1), 2 is a 2-commands alternating mode(mode 2), 3 will just show the guide information.
ConsoleKeyInfo keyInfo1;
do
{
keyInfo1 = Console.ReadKey(true);

} while (keyInfo1.Key != ConsoleKey.D1 && keyInfo1.Key != ConsoleKey.NumPad1 && keyInfo1.Key != ConsoleKey.D2 && keyInfo1.Key != ConsoleKey.NumPad2 && keyInfo1.Key != ConsoleKey.D3 && keyInfo1.Key != ConsoleKey.NumPad3);

// User selected Mode 1:
if (keyInfo1.Key == ConsoleKey.D1 || keyInfo1.Key == ConsoleKey.NumPad1)
{
generatorMode = 1; // Marked the mode, here 1 refer to mode 1

// If user input a key in one of the +, ^, %, {, }, (, ), it will ask user to input with correct syntax.
// If user's input is a real valid key command, then program will go on.

bool isKeyValid;
string key_Mode1;

Console.WriteLine("\n-------- 单一指令模式:将循环输出同一个或同一组按键 ---------\n");
do
{
Console.WriteLine("输入要循环的按键或指令:\n");
key_Mode1 = Convert.ToString(Console.ReadLine());

isKeyValid = (key_Mode1 == "+" || key_Mode1 == "^" || key_Mode1 == "%" || key_Mode1 == "(" || key_Mode1 == ")" || key_Mode1 == "{" || key_Mode1 == "}");

if (isKeyValid)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine("键入的指令为 +, ^, %, { }, ()时,必须用{}括起,请重新输入:\n\n");
Console.ResetColor();
}
else
{
keyName1 = key_Mode1;
}
} while (isKeyValid);
}

// User selected Mode 2:
else if (keyInfo1.Key == ConsoleKey.D2 || keyInfo1.Key == ConsoleKey.NumPad2)
{
generatorMode = 2; // Marked mode was 2



string key_Mode2_A; // the first key argument for user inputing
string key_Mode2_B; // the second key argument for user inputing

bool isKeyInvalid_A; // For check the validity of command.
bool isKeyInvalid_B; // -
bool isKeyInvalid_A_B; // -

Console.WriteLine("\n----- 双指令交替模式:两个指令将以A-B-A-B-的形式交替输出 -----\n");
do
{
Console.WriteLine("输入第一个指令:\n");
key_Mode2_A = Convert.ToString(Console.ReadLine());
isKeyInvalid_A = (key_Mode2_A == "+" || key_Mode2_A == "^" || key_Mode2_A == "%" || key_Mode2_A == "(" || key_Mode2_A == ")" || key_Mode2_A == "{" || key_Mode2_A == "}");

Console.WriteLine("\n输入第二个指令:\n");
key_Mode2_B = Convert.ToString(Console.ReadLine());
isKeyInvalid_B = (key_Mode2_B == "+" || key_Mode2_B == "^" || key_Mode2_B == "%" || key_Mode2_B == "(" || key_Mode2_B == ")" || key_Mode2_B == "{" || key_Mode2_B == "}");

isKeyInvalid_A_B = (isKeyInvalid_A || isKeyInvalid_B); // if one of the 2 keys is invalid the command will be regarded as invalid

if (isKeyInvalid_A_B)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine("键入的指令为 +, ^, %, { }, ()时,必须用{}括起,请重新输入:\n\n");
Console.ResetColor();
}
else
{
keyName1 = key_Mode2_A;
keyName2 = key_Mode2_B;
}

} while (isKeyInvalid_A_B);

}
else if (keyInfo1.Key == ConsoleKey.D3 || keyInfo1.Key == ConsoleKey.NumPad3)
{
Console.WriteLine("\n--------------------【按键命令规则如下】--------------------\n");
Console.WriteLine("数字、英文字母、汉字直接输入即可\n\n多个按键直接组合即可,例如abcdef\n\n除 +, ^, %, {}, ()以外的符号直接输入即可\n\n空格键(Space)键入空格即可\n\n以下是一些特殊按键的命令:\n\nEnter {ENTER} 或 ~\nESC {ESC}\nTab {TAB}\nF1 - F12 {F1} - {F12}\n向上键 {UP}\n向下键 {DOWN}\n向左键 {LEFT}\n向右键 {RIGHT}\nPage Down {PGDN}\nPage Up {PGUP}\nBackspace {BACKSPACE}、{BS} 或 {BKSP}\nDelete {DELETE} 或 {DEL}\nBreak {BREAK}\nCaps Lock {CAPSLOCK} (受键盘物理按键状态影响)\nEnd {END}\nHome {HOME}\nInsert {INSERT} 或 {INS}\nScroll Lock {SCROLLLOCK} (受键盘物理按键状态影响)\nNum Lock {NUMLOCK} (受键盘物理按键状态影响)\n小键盘加 {ADD}\n小键盘减 {SUBTRACT}\n小键盘乘 {MULTIPLY}\n小键盘除 {DIVIDE}\n截屏键 {PRTSC}\n\n大小写控制规则:\n\n命令{CAPSLOCK}为在命令中模拟按下一次CapsLock键,只能用于自动命令的输出,无法控制键盘物理按键的状态,并受其影响。\n例如键入命令{CAPSLOCK}abc{CAPSLOCK}def,在CapsLock键为关闭状态时执行,得到的输出结果为ABCdef\n\n+, ^, %, {}, ()或其组合均须用括号{}括起,否则会造成程序代码解析错误。例如当自动按键为(时,则命令应为{(}\n\n若要指定按键与 SHIFT、Ctrl 和 ALT 键的组合,请在键代码前面加上以下一个或多个代码:\n\nSHIFT +\nCtrl ^\nALT %\n\n执行自动循环前请切换至英文输入法,否则可能会造成指令输出不正常\n\n自动循环开始后,可通过按【Ctrl+Alt+空格】组合键重置程序,或关闭控制台窗口结束程序\n");
Console.WriteLine("------------------------------------------------------------\n\n");

goto UserInputing_Mode;
}


// Here user will be allowed to input a time interval.
// The try-catch block will prevent the exceptions when user send non-number characters to timer interval. And then back to beginning of time inputing through a Goto command.
UserInputing_Time:
try
{
Console.WriteLine("\n输入每次执行的时间间隔(秒,可以是小数):\n");
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("(输入数字后请切换至英文输入法,按Enter键执行程序)\n");
Console.ResetColor();
Program.timerIntervalInSecond = Convert.ToDouble(Console.ReadLine());
aTimer.Interval = Program.timerIntervalInSecond * 1000; // Transform milliseconds into seconds.
aTimer.Enabled = true; // Timer event started
}

catch
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine("请重新输入一个正确的秒数!\n");
Console.ResetColor();

goto UserInputing_Time;
}

Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("\n************ !自动按键任务已经开始! ************\n");
Console.WriteLine("****** 请切换至需要执行按键的窗口或位置上 ******\n");
Console.WriteLine($"设定的间隔时间为 {Program.timerIntervalInSecond} 秒...");
Console.ResetColor();
Console.Title = "* -Task Started- *";

// This would prevent the auto close of the console when generated autokey or followed user's typing was send to console itself.
// If user put combination key of [Ctrl+Alt+Spacebar], the event will stop and program will be reset.
do
{
ConsoleKeyInfo keyInfo2 = Console.ReadKey();
if ((keyInfo2.Modifiers & ConsoleModifiers.Control) != 0 && (keyInfo2.Modifiers & ConsoleModifiers.Alt) != 0 && keyInfo2.Key == ConsoleKey.Spacebar)
{
aTimer.Enabled = false; // Timer event stop.
isKey_2A = true; // Reset the key distinguish argument for mode 2 while program was reseted.

Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("\n\n********* 程序已重置,自动按键任务停止 *********\n\n");
Console.ResetColor();
Console.Title = "AutoKeyGenerator V0.5";

goto UserInputing_Mode;
}
else
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine("\n检测到对此窗口的按键输入,请切换至目标窗口上\n");
Console.ResetColor();
}
} while (true);
}

// Event executor function
// If is mode 1, will only execute a single key(keyName1).
// If is mode 2, the first key(keyName1) and the second key(keyName2) will be executed alternately.
public static void ModeOption(object source, ElapsedEventArgs e)
{
// mode 1 method:
if (generatorMode == 1)
{
keyName = keyName1;
}

// mode 2 method (Will always execute keyName1 first):
else
{
if (isKey_2A)
{
keyName = keyName1;
}
else
{
keyName = keyName2;
}
isKey_2A = !isKey_2A; // Reverse the value(a boolean) of distinguish argument after each executions.
}

try
{
SendKeys.SendWait($"{keyName}"); // Executing the autokey command
realLocalTime = Convert.ToString(DateTime.Now); // Show the localtime of each executions
Console.WriteLine($"{realLocalTime}" + " - 执行完成");
}

// If the key command still cannot be parsed whatever, this will ask user to reset the program by a key combination.
catch (ArgumentException)
{
Console.ForegroundColor = ConsoleColor.DarkRed;
Console.WriteLine("程序错误,请按【Ctrl+Alt+空格】重置程序...");
}
}
}
}

  这个程序的使用说明基本上已经在代码里写出,也较为详细地列出了SendKeys类的按键表,可在使用时通过按键直接调出,注释也写的比较详细。相比于最早的只能执行单一指令的程序,我还加上了一个执行双指令交替的功能,即两个(组)指令将以A-B-A-B-的形式交替输出,但目前还未得到实际应用,现在两组指令之间的时间间隔也是完全相同的,而在我能想到的实际使用中,很可能会遇到A-B的时间要远小于B-A的时间的情况,即较为迅速地执行A-B后需要等待较长时间,再迅速执行下一次A-B,如此循环。鉴于我目前还没有遇到类似的需求,这个功能就暂且这样。

  在判断用户的输入有效性上,这个程序还要面临比平常更复杂的情况。因为程序本身可能必须去执行一些特殊的按键,如+, ^, %, {}, ()或其组合,这些按键对应的字符串因为本身就可能构成一些运算操作符或者代码结构,一旦将其注入进代码后,就会破坏正常的代码结构,从而造成错误。我的方法是加了一层筛选用户的输入内容的块,用来专门对付这些特殊的按键,虽然并不是所有的情况都能被完美应对,但至少在大部分情况下防止了程序出现错误。

  程序还可以通过向控制台本身发送一个【Ctrl+Alt+空格】的组合键来重置程序事件以及所有关键变量,这也增加了程序的可操作性。

  像是在上文提到的那个工控软件上,因为“启动”按键的快捷键是F2,就可以将指令输入为{F2},便可实现自动循环启动。这个程序还被我用来玩一个叫《Zombidle》的放置类小游戏,这个游戏的特点是敲键盘越快伤害越高,显然我们并不愿意一直去物理敲击键盘,于是可以将这个程序里的间隔时间设定到很短(如10 ms),便可实现超高伤害输出。此外,还被用来挂一种网络课程,这个课程的页面会记录观看者的观看情况,如果观看者长时间没有操作则会自动中断,我当时是设置了{PGDN}和{PGUP},让网页每隔几分钟就自动来回翻一次页面,从而打断网页中断的计时。

2. 暖暖的自动种植工具(NikkiHarvester)

  这个程序就是纯粹的游戏辅助了,因为《无限暖暖》这个游戏最近出了一个家园系统,玩法和传统的家园经营类游戏差不多,需要不断地执行播种、浇水、收获等等操作,当然很容易想到去用程序自动执行这一过程。首先我们必须明确的一个重要的前提环境是,该游戏不能自带有任何的反作弊机制。像《无限暖暖》的游戏内容几乎可以算是单机游戏,是没有这些东西的。如果想要在拥有强大反作弊机制的游戏里实现类似的自动按键功能,那就完全是另一个话题了。

  首先我遇到最大的问题是,像是《无限暖暖》这样的游戏,和一般的网页或Windows程序不同,大多数这种具有一定体量的游戏,其键盘都使用DirectX的DirectInput接口,而不是标准windows消息会话机制,所以通常的方法——如上文提到的SendKeys类等等的方法,都不能起作用,这一度难住了我。后来发现,Windows API一定程度上可以解决这个问题,即利用Keybd_event()方法。我们这里先放完整的程序代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using static System.Runtime.CompilerServices.RuntimeHelpers;

namespace NikkiHarvester
{
class Program
{
public static int TurnsCounter = 0; // 运行轮数计时器,每运行一轮加1,用于记录和终止程序
public static int TotalTurns; // 用户指定的运行轮数

public static int NikkiPlantingTime = 46000; // 暖暖种植运动的时间
public static int NikkiHarvestingTime = 5200; // 暖暖收获运动的时间
public static int WaitingTime = 603000; // 生长等待时间 (10min = 600000),多加了3秒钟考虑容错
public static int WaitingTimeClock = 600000; // 生长等待时间的报时器时间
public static int ClickIntervalTime = 100; // 点击间隔


static void Main(string[] args)
{

//Console.Title = "NikkiHarvester V0.1";
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("\n **************************************\n * *\n * *\n * 暖暖的自动种植工具 V0.1 *\n * *\n * *\n **************************************\n");

Console.ForegroundColor = ConsoleColor.White;


UserInputing_Turns:
try
{
Console.WriteLine("请设定需要种植的轮数,并按Enter键执行程序(输入0,程序将无限次运行):\n");
TotalTurns = Convert.ToInt32(Console.ReadLine());
}

catch
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine("请重新输入一个正确的整数数字!\n");
Console.ResetColor();

goto UserInputing_Turns;
}


Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("\n************ !自动工作任务已经开始! ************\n");
Console.WriteLine("* 请在 5 秒内切换至需要执行按键的窗口或位置上 *\n");
Console.ResetColor();

Keyboard.Delay(5000);


// 如果用户输入一个轮数,程序运行运行到指定次数后将自动停止
// 如果用户输入的轮数是0,则程序将无限次运行
if (TotalTurns == 0)
{
while (true)
{
Keyboard.Thread_Keyboard_W();
}
}
else
{
while (TurnsCounter != TotalTurns)
{
Keyboard.Thread_Keyboard_W();
}
}

Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("\n************ 程序已经结束,关闭程序窗体退出 ************\n");
Console.ReadKey(true);

}
}

public static class Keyboard
{

[DllImport("user32.dll", SetLastError = true)]
static extern void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo);

// 进程睡眠方法
public static void Delay(int delay)
{
System.Threading.Thread.Sleep(delay);
}

// 模拟按键按下的方法
public static void KeyDown(KEYCODE keycode)
{
keybd_event((byte)keycode, 0x0, 0, 0);// 按下
}

// 模拟一次按下和松开的方法
public static void KeyPress(KEYCODE keycode, SCANCODE scancode, int delay = 0)
{
keybd_event((byte)keycode, (byte)scancode, 0, 0);// 按下
System.Threading.Thread.Sleep(delay);
keybd_event((byte)keycode, (byte)scancode, 2, 0); // 释放
}

// 模拟按键松开的方法
public static void KeyUp(KEYCODE keycode)
{
keybd_event((byte)keycode, 0, 2, 0); // 释放
}

// SendKeys方法
public static void Type(string message)
{
System.Windows.Forms.SendKeys.SendWait(message);
}


// 一次完整的种植方法
public static void Thread_Keyboard_W()
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("\n正在进行种植和浇水...\n");

Thread tr_1 = new Thread(Keyboard.Thread_RightClick_0); // 右键种植-浇水线程
tr_1.Start();

Keyboard.KeyPress(KEYCODE.VK_W, SCANCODE.SK_W, Program.NikkiPlantingTime); // W键前进线程

tr_1.Abort(); // 前进到底后,停止右键线程

Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine($"\n等待植物生长中...设定的等待时间为 {(Program.WaitingTimeClock) / 1000} 秒\n");
Console.WriteLine($"等待期间请不要移动人物的镜头视角!\n");

Thread tr_2 = new Thread(Keyboard.Thread_WaitingClock); // 开始等待报时器
tr_2.Start();

Thread.Sleep(Program.WaitingTime); // 主线程睡眠,等待植物生长

tr_2.Abort(); // 结束等待报时器

Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine("\n!! 开始收割植物 !!\n");

Thread tr_3 = new Thread(Keyboard.Thread_E); // E键收割线程
tr_3.Start();

Keyboard.KeyPress(KEYCODE.VK_S, SCANCODE.SK_S, Program.NikkiHarvestingTime); // S键倒退线程

tr_3.Abort(); // 收割完毕后,停止E键线程

Program.TurnsCounter++; // 程序轮数自加

Program.WaitingTimeClock = 600000; // 重置报时器时间

Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n********** 已完成了 {Program.TurnsCounter} 轮种植 **********\n");
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine(" 5 秒后将进行下一轮种植...\n");

Thread.Sleep(5000); // 等待5秒,准备下一轮种植
}


// 右键不断点击的方法
public static void Thread_RightClick_0()
{
while (true)
{
Keyboard.KeyPress(KEYCODE.VK_RBUTTON, SCANCODE.SK_RBUTTON, 25);

Thread.Sleep(Program.ClickIntervalTime);
}
}

// E键不断点击的方法
public static void Thread_E()
{
while (true)
{
Keyboard.KeyPress(KEYCODE.VK_E, SCANCODE.SK_E, 25);

Thread.Sleep(Program.ClickIntervalTime);
}
}

// 报时器时钟
public static void Thread_WaitingClock()
{
while (true)
{
Thread.Sleep(30000);
Program.WaitingTimeClock = Program.WaitingTimeClock - 30000;

Console.WriteLine($"距植物成熟还剩余 {Program.WaitingTimeClock / 1000} 秒...");
}
}
}


// 虚拟按键码表
public enum KEYCODE
{
VK_A = 0x41, VK_B = 0x42, VK_C = 0x43, VK_D = 0x44, VK_E = 0x45, VK_F = 0x46, VK_G = 0x47,
VK_H = 0x48, VK_I = 0x49, VK_J = 0x4A, VK_K = 0x4B, VK_L = 0x4C, VK_M = 0x4D, VK_N = 0x4E, VK_O = 0x4F,
VK_P = 0x50, VK_Q = 0x51, VK_R = 0x52, VK_S = 0x53, VK_T = 0x54, VK_U = 0x55, VK_V = 0x56, VK_W = 0x57,
VK_X = 0x58, VK_Y = 0x59, VK_Z = 0x5A, VK_LSHIFT = 0xA0, VK_RSHIFT = 0xA1, VK_LCONTROL = 0xA2, VK_RCONTROL = 0xA3,
VK_ENTER = 0x0D, VK_LBUTTON = 0x01, VK_RBUTTON = 0x02
}

// 扫描按键码表
public enum SCANCODE
{
SK_A = 0x1E, SK_B = 0x30, SK_C = 0x2e, SK_D = 0x20, SK_E = 0x12, SK_F = 0x21, SK_G = 0x22,
SK_H = 0x23, SK_I = 0x14, SK_J = 0x24, SK_K = 0x25, SK_L = 0x26, SK_M = 0x32, SK_N = 0x31, SK_O = 0x18,
SK_P = 0x19, SK_Q = 0x10, SK_R = 0x13, SK_S = 0x1F, SK_T = 0x14, SK_U = 0x16, SK_V = 0x2F, SK_W = 0x11,
SK_X = 0x2D, SK_Y = 0x15, SK_Z = 0x2C, SK_LSHIFT = 0x2A, SK_RSHIFT = 0x36, SK_LCONTROL = 0x1D, SK_RCONTROL = 0x1D,
SK_ENTER = 0X1C, SK_LBUTTON = 0x100, SK_RBUTTON = 0x101
}
}

  的如果在互联网上搜索Keybd_event()方法的话,会发现很多人写的Keybd_event照样无法在DirectX输入的游戏中生效,这是因为很多人的程序并没有写全该方法中的关键内容。这个方法的结构如下文,当然,想要调用这个方法,首先得先声明Windows用户界面的API,即user32.dll:

1
[DllImport("user32.dll", SetLastError = true)]

  然后我们再逐字分析Keybd_event()这个方法:

1
void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo);

  这其中,bVK指的是虚拟按键码,即Virtual-Key,代码必须是1-254范围内的值,通常用16进制表示。我在程序代码里写了常用按键的虚拟按键码码表,完整的码表见微软官方文档:https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes

  bScan指的是键盘的硬件驱动扫描码,即Scan code,简称“扫描码”。通常也用16进制表示。我的程序代码里也写了常用按键的扫描码,但要注意的是扫描码拥有多个版本的码表,目前最常用键盘的码表即Set2扫描码表,可自行查询,也可参考文末附表给出的扫描码键值。若是将bScan的值写成0,即表示忽略这个扫描码,同理bVK的值也可以为0,表示忽略。

  dwFlags控制函数的动作状态。其值只能是整数:值为0时表示忽略,即按键按下;而将其值设为2时,则表示按键弹起。

  dwExtraInfo是附加的辅助信息,这个是没有用的,至少我现在没有用到这个,设置为0即可。

  这其中最关键的就是bScan,也就是键盘驱动扫描码,因为它是硬件驱动级别的,所以即便是DirectInput也无法绕开它。网上很多人写的程序里都完全忽略了bScan,而将其写成了0,故而无法生效,我们这里只需要将虚拟按键码和扫描码写成同一个按键,即可让自动按键程序在应用DirectX键盘接口的游戏里工作。

  虽然到这里我们已经解决了键盘自动按键不生效的问题,但要想在《无限暖暖》游戏里实现自动种植,还需要攻克另一个关键的问题,即如何实现鼠标的自动点击。《无限暖暖》这个游戏因为是所谓的“三端互通”游戏,而我们都知道,这种游戏本质上更接近手游,其PC端游更多意义上只是手游的一个“官方模拟器”,故游戏里很多按键都是靠鼠标去点击,从而模拟移动端点击屏幕的效果。这款游戏里的鼠标左右键是不能被更改的,占据了大量的操作,如在种地、浇水这两个任务上完全依赖鼠标右键的点击,并且还没有快捷键。所以必须找到一个能够自动执行鼠标按键的途径,否则整套流程还是无法顺利进行。

  我第一个尝试的是Windows API的mouse_event方法,但很遗憾,这个方法虽然能够在Windows会话中起作用,但仍然无法在DirectInput下工作。不断研究尝试不同方法的时候,我发现了一篇扫描码的码表,其中里居然包含LEFT_BUTTON和RIGHT_BUTTON的字样,难道说扫描码还包含有鼠标按键?经过我的验证,确实是这样,Keybd_event方法在使用特定的Scan code后,是能够模拟鼠标按键的。从代码中可以看到,例如鼠标左键,其扫描码为0x100,而鼠标右键为0x101,有点奇怪的是,这些值都超过了255,看来扫描码并不一定局限在255之内。更多的鼠标按键详见文末的附表,并不是每个按键我都验证过。

  最终,在配合上多线程后,这个问题终于得到了解决。现在游戏里的人物可以反复进行种植、浇水、等待以及最后的收获动作。多次调试后再配合上游戏里的一些机制,仅仅用一个十几KB的程序就完美实现了全自动无人值守的挂机种地。但更多的收获在于对Keybd_event方法的不断探索上,现在看来,这个函数其实还是相当强力的,只不过目前互联网上确实很难找到把这个函数的使用和操作都讲很透彻的信息,就连谷歌上搜一圈,同样也是几乎没几个人把这些搞的很明白的,不得不说,还是有点成就感的啦!

3. Visual Studio以管理员权限调试、运行代码程序的方法

  最后还有一个小问题,就是以上的这些程序都只有在管理员权限下运行才能正常工作。然而这些程序在VS里调试的时候,默认情况是没有管理员权限的,这很是麻烦。于是这是以管理员权限调试代码的方法(测试过VS 2019和VS 2022):

  右键【解决方案资源管理器】里的解决方案 - 安全性 - 将启用ClickOnce安全设置的勾打上再取消,我们就可以看到在Properties里出现了一个新的文件,即app.manifest。打开这个文件,找到“UAC 清单选项”的代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- UAC 清单选项
如果想要更改 Windows 用户帐户控制级别,请使用
以下节点之一替换 requestedExecutionLevel 节点。n
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />

指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
如果你的应用程序需要此虚拟化来实现向后兼容性,则删除此
元素。
-->
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>

  通常在默认的情况下,代码就像上面这样,requestedExecutionLevel的级别是”asInvoker”。这里我们需要将”asInvoker”改为”requireAdministrator”或者”highestAvailable”,我一般都是改成”highestAvailable”,嫌麻烦的可以直接复制我修改后的块:

1
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />

  保存后,对我们的程序点击【启动】,会弹出一个“此任务要求应用程序具有提升的权限”的框,这里选择【使用其他凭据重新启动】,将再次重启VS,重启后,再次点【启动】,此时启动的程序已经具有管理员权限。

附表:扫描码(Scan code)代码表

Hex Dec Button
0x01 1 Escape
0x02 2 1
0x03 3 2
0x04 4 3
0x05 5 4
0x06 6 5
0x07 7 6
0x08 8 7
0x09 9 8
0x0A 10 9
0x0B 11 0
0x0C 12 Minus
0x0D 13 Equals
0x0E 14 Backspace
0x0F 15 Tab
0x10 16 Q
0x11 17 W
0x12 18 E
0x13 19 R
0x14 20 T
0x15 21 Y
0x16 22 U
0x17 23 I
0x18 24 O
0x19 25 P
0x1A 26 Left Bracket
0x1B 27 Right Bracket
0x1C 28 Enter
0x1D 29 Left Control
0x1E 30 A
0x1F 31 S
0x20 32 D
0x21 33 F
0x22 34 G
0x23 35 H
0x24 36 J
0x25 37 K
0x26 38 L
0x27 39 Semicolon
0x28 40 Apostrophe
0x29 41 ~ (Console)
0x2A 42 Left Shift
0x2B 43 Back Slash
0x2C 44 Z
0x2D 45 X
0x2E 46 C
0x2F 47 V
0x30 48 B
0x31 49 N
0x32 50 M
0x33 51 Comma
0x34 52 Period
0x35 53 Forward Slash
0x36 54 Right Shift
0x37 55 NUM*
0x38 56 Left Alt
0x39 57 Spacebar
0x3A 58 Caps Lock
0x3B 59 F1
0x3C 60 F2
0x3D 61 F3
0x3E 62 F4
0x3F 63 F5
0x40 64 F6
0x41 65 F7
0x42 66 F8
0x43 67 F9
0x44 68 F10
0x45 69 Num Lock
0x46 70 Scroll Lock
0x47 71 NUM7
0x48 72 NUM8
0x49 73 NUM9
0x4A 74 NUM-
0x4B 75 NUM4
0x4C 76 NUM5
0x4D 77 NUM6
0x4E 78 NUM+
0x4F 79 NUM1
0x50 80 NUM2
0x51 81 NUM3
0x52 82 NUM0
0x53 83 NUM.
0x57 87 F11
0x58 88 F12
0x9C 156 NUM Enter
0x9D 157 Right Control
0xB5 181 NUM/
0xB8 184 Right Alt
0xC7 199 Home
0xC8 200 Up Arrow
0xC9 201 PgUp
0xCB 203 Left Arrow
0xCD 205 Right Arrow
0xCF 207 End
0xD0 208 Down Arrow
0xD1 209 PgDown
0xD2 210 Insert
0xD3 211 Delete
0x100 256 Left Mouse Button
0x101 257 Right Mouse Button
0x102 258 Middle/Wheel Mouse Button
0x103 259 Mouse Button 3
0x104 260 Mouse Button 4
0x105 261 Mouse Button 5
0x106 262 Mouse Button 6
0x107 263 Mouse Button 7
0x108 264 Mouse Wheel Up
0x109 265 Mouse Wheel Down

  • 版权声明: 本博客所有文章著作权归作者所有,禁止任何形式的转载。
  • Copyrights © 2019-2025 Caelica

请我喝杯咖啡吧~