用主控台撰寫鍵盤腳本
用主控台撰寫鍵盤腳本
前言
最近玩一些動作遊戲發現歲月真是不饒人,
手變殘了反應也變慢了。
為了血壓健康千萬要慎選遊戲。
尤其像是格鬥遊戲這種要尻技能表的玩法,
有沒有方法不用一直操作就能打出好棒棒的連段呢?
沒錯就是按鍵精靈登場的時候囉!
本著沒事找事不務正業的精神,
本次就是要自己來實作簡單的自動放招。
專案目標
本次目標為開發一個 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)