用主控台撰寫鍵盤腳本
用主控台撰寫鍵盤腳本
前言
最近玩一些動作遊戲發現歲月真是不饒人,
手變殘了反應也變慢了。
為了血壓健康千萬要慎選遊戲。

尤其像是格鬥遊戲這種要尻技能表的玩法,
有沒有方法不用一直操作就能打出好棒棒的連段呢?
沒錯就是按鍵精靈登場的時候囉!
本著沒事找事不務正業的精神,
本次就是要自己來實作簡單的自動放招。
專案目標
本次目標為開發一個 ASP.NET Framework 4.8 主控台專案,
監聽鍵盤的訊號,當按下對應的按鍵會執行對應的指令。
會使用 ASP.NET Framework 而非 ASP.NET Core,
原因是部分功能會用到 System.Windows.Forms 這個命名空間,
加上遊戲多半都是在 Windows 平台執行,就不用堅持跨平台了。
知識儲備
由於對遊戲搞外掛破解是有風險的,
像是東半球最強法務部,
所以網路上的教學資源比較少。
加上每款遊戲會因為執行平台或遊戲引擎差異,
比較沒有固定的破解手法。
以下是本專案比較重要的知識點
Hook
Hook 白話翻譯就是鉤子,
把預定要發送的消息給「鉤」到別處,
也就是攔截消息。
以遊戲操作這件事情為例:
- 玩家按下前進的按鍵
- 鍵盤發送硬體訊號給作業系統
- 作業系統將訊息轉送到鍵盤驅動
- 鍵盤驅動轉換成作業系統能識別的訊號
- 作業系統判斷這個訊號要送進哪個程式
- 作業系統將消息傳給遊戲程式
- 遊戲程式的人物執行前進的動作
而按鍵精靈的原理就是攔截特定消息後執行預定好的腳本。
根據官方文件 SetWindowsHookExA 函式 (winuser.h) 會看到語法如下
HHOOK SetWindowsHookExA(
[in] int idHook,
[in] HOOKPROC lpfn,
[in] HINSTANCE hmod,
[in] DWORD dwThreadId
);idHook 參數就是決定要攔截的類型
通常比較常用的就是
- WH_KEYBOARD 2
- WH_KEYBOARD_LL 13
- WH_MOUSE 7
- WH_MOUSE_LL 13
LL 是 Low Level 的縮寫
代表能攔截到更底層的事件
模擬訊號輸入
要對遊戲下指令,就要模擬鍵盤的訊號,
由於 keybd_event 比起其他 SendMessage、PostMessage 等函式更為底層,
因為是直接模擬硬體訊號,成功率會比較高。
根據官方文件 keybd_event 函式 (winuser.h) 會看到語法如下
void keybd_event(
[in] BYTE bVk,
[in] BYTE bScan,
[in] DWORD dwFlags,
[in] ULONG_PTR dwExtraInfo
);bVk 是鍵盤虛擬碼
System.Windows.Forms 的 Keys 列舉可以直接用
bScan 是鍵盤掃描碼
這是根據不同的硬體來決定
詳細可以看這篇 淺談鍵盤掃描碼
dwFlags 比較常用的是
- KEYEVENTF_KEYDOWN 0x0000 按下
- KEYEVENTF_KEYUP 0x0002 放開
由於有些遊戲對於按鍵的接收判定各有不同
所以會需要多嘗試,例如可以用 Thread.Sleep() 創造時間差
完整程式範例
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
namespace HookScript
{
internal class Program
{
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private const int KEYEVENTF_KEYDOWN = 0x0000;
private const int KEYEVENTF_KEYUP = 0x0002;
private static IntPtr _hookId = IntPtr.Zero;
private static readonly LowLevelKeyboardProc _proc = HookCallback;
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
#region WinAPI
[DllImport("user32.dll", EntryPoint = "keybd_event", SetLastError = true)]
static extern void keybd_event(Keys bVk, int bScan, uint dwFlags, uint dwExtraInfo);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
#endregion
static void Main(string[] args)
{
// 設定 hook
_hookId = SetHook(_proc);
Console.WriteLine("按下 F7 執行操作,按下 F9 離開應用");
// 保持程式持續執行
Application.Run();
}
private static IntPtr SetHook(LowLevelKeyboardProc proc)
{
using (var curProcess = System.Diagnostics.Process.GetCurrentProcess())
{
using (var curModule = curProcess.MainModule)
{
return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
}
}
}
private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
{
int vkCode = Marshal.ReadInt32(lParam);
if ((Keys)vkCode == Keys.F7)
{
Console.WriteLine("F7已按下。執行操作");
// 下
Console.WriteLine("按下[下]");
keybd_event(Keys.S, 0x9F, KEYEVENTF_KEYDOWN, 0);
Thread.Sleep(100);
Console.WriteLine("放開[下]");
keybd_event(Keys.S, 0x9F, KEYEVENTF_KEYUP, 0);
Thread.Sleep(100);
// 下
Console.WriteLine("按下[下]");
keybd_event(Keys.S, 0x9F, KEYEVENTF_KEYDOWN, 0);
Thread.Sleep(100);
Console.WriteLine("放開[下]");
keybd_event(Keys.S, 0x9F, KEYEVENTF_KEYUP, 0);
Thread.Sleep(100);
// 必殺
Console.WriteLine("按下[必殺技]");
keybd_event(Keys.I, 0x97, KEYEVENTF_KEYDOWN, 0);
Thread.Sleep(100);
Console.WriteLine("放開[必殺技]");
keybd_event(Keys.I, 0x97, KEYEVENTF_KEYUP, 0);
Thread.Sleep(100);
}
else if ((Keys)vkCode == Keys.F9)
{
// 取消 hook 並關閉程式
UnhookWindowsHookEx(_hookId);
Environment.Exit(0);
}
}
return CallNextHookEx(_hookId, nCode, wParam, lParam);
}
}
}成果展示

參考資料
虛擬按鍵代碼
keybd_event 函式 (winuser.h)
SetWindowsHookExA 函式 (winuser.h)