跳至主要內容

用主控台撰寫鍵盤腳本

Pamis Wang大约 4 分鐘外掛開發外掛ASP.NET FrameworkC#

用主控台撰寫鍵盤腳本

前言

最近玩一些動作遊戲發現歲月真是不饒人,
手變殘了反應也變慢了。

為了血壓健康千萬要慎選遊戲。

宮崎英高的疝液
宮崎英高的疝液

尤其像是格鬥遊戲這種要尻技能表的玩法,
有沒有方法不用一直操作就能打出好棒棒的連段呢?

沒錯就是按鍵精靈登場的時候囉!
本著沒事找事不務正業的精神,
本次就是要自己來實作簡單的自動放招。

專案目標

本次目標為開發一個 ASP.NET Framework 4.8 主控台專案,
監聽鍵盤的訊號,當按下對應的按鍵會執行對應的指令。

會使用 ASP.NET Framework 而非 ASP.NET Core
原因是部分功能會用到 System.Windows.Forms 這個命名空間,
加上遊戲多半都是在 Windows 平台執行,就不用堅持跨平台了。

知識儲備

由於對遊戲搞外掛破解是有風險的,
像是東半球最強法務部,
所以網路上的教學資源比較少。

加上每款遊戲會因為執行平台或遊戲引擎差異,
比較沒有固定的破解手法。

以下是本專案比較重要的知識點

Hook

Hook 白話翻譯就是鉤子,
把預定要發送的消息給「鉤」到別處,
也就是攔截消息。

以遊戲操作這件事情為例:

  1. 玩家按下前進的按鍵
  2. 鍵盤發送硬體訊號給作業系統
  3. 作業系統將訊息轉送到鍵盤驅動
  4. 鍵盤驅動轉換成作業系統能識別的訊號
  5. 作業系統判斷這個訊號要送進哪個程式
  6. 作業系統將消息傳給遊戲程式
  7. 遊戲程式的人物執行前進的動作

而按鍵精靈的原理就是攔截特定消息後執行預定好的腳本。

根據官方文件 SetWindowsHookExA 函式 (winuser.h)open in new window 會看到語法如下

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 比起其他 SendMessagePostMessage 等函式更為底層,
因為是直接模擬硬體訊號,成功率會比較高。

根據官方文件 keybd_event 函式 (winuser.h)open in new window 會看到語法如下

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);
        }
    }
}

成果展示

心情就跟佩可拉一樣開心
心情就跟佩可拉一樣開心

參考資料

虛擬按鍵代碼open in new window
keybd_event 函式 (winuser.h)open in new window
SetWindowsHookExA 函式 (winuser.h)open in new window

上次編輯於:
貢獻者: Pamis Wang