跳到正文

工作原理

text
 test script (pwsh)                      Minecraft.Windows.exe + BeDD.dll
 ──────────────────                      ─────────────────────────────────────────────
 Send-BeDDKey E ──JSON line──▶ \\.\pipe\BeDD
                                          │ queue Event{WM_KEYDOWN 'E'}; PostMessage(hwnd, WM_LHI)
                                          ▼
                                   subclassed window procedure (game thread)
                                     WM_LHI → apply virtual key/cursor state
                                            → CallWindowProc(game's WndProc, WM_KEYDOWN, ...)
                                     real WM_KEY*/WM_MOUSE*/WM_INPUT/WM_POINTER* → swallowed
                                     WM_ACTIVATEAPP(0)/WM_KILLFOCUS/WM_ACTIVATE(inactive) → swallowed
 ◀──{"ok":true}── after the game thread dispatched it
  1. 鍵盤、文字和指針位置走遊戲自己的窗口過程。 模塊子類化遊戲窗口。按鍵變成 WM_KEYDOWN/WM_KEYUP,文字變成 WM_CHAR,指針變成 WM_MOUSEMOVE 加一個虛擬光標(被掛鉤的 GetCursorPos 返回的位置),在遊戲線程上按順序分發。 遊戲把它們當作真實輸入處理(MainGameCore::WndProc_PC::onKeyboardAction → HIDControllerGameCoreDesktop::onKeyDown), 所以聊天、鍵位映射和界面懸停都能用。遊戲線程處理完輸入後才回復,所以腳本的結果是確定的。
  2. 鼠標按鈕、滾輪和視角走虛擬 GameInput 鼠標。 基岩版用 GDK 的 GameInput API(GameInputRedist.dll,接口 v2) 直接從設備讀取鼠標:WM_LBUTTONDOWN 之類的窗口消息會被忽略;沒有模塊的話,你真實的鼠標仍會在後臺遊戲裡點擊、轉視角。 模塊掛鉤 IGameInputReading::GetMouseState(以及 GetKeyCount/GetKeyState):遊戲讀到的是虛擬按鈕、 相對位移(視角)、滾輪和絕對位置。設備報告輸入之前,GameInput 沒有該鼠標的讀數,所以在沒人碰過的電腦上, 遊戲的 GetCurrentReading 會從模塊拿到該設備的一個虛擬讀數。
  3. 遊戲從不搶前臺,也不會失去焦點。 失去焦點的消息被吞掉,所以遊戲不會暫停或鬆開按鍵。Start-BeDDGame.ps1 用 SW_SHOWNOACTIVATE 啟動 exe;被掛鉤的 ShowWindow/SetWindowPos/SetActiveWindow/BringWindowToTop/SetForegroundWindow 從不激活遊戲窗口,窗口一直保持在 Z 序最底部。user32 鉤子在 load() 裡就裝好了:遊戲在 enable() 之前就創建並顯示窗口。 模塊發送的"獲得焦點"消息會跳過默認的 WM_ACTIVATE 處理(掛鉤 DefWindowProcW),否則它會設置焦點從而激活窗口: 從用戶前臺應用(終端、agent)啟動的遊戲被允許搶前臺。
  4. 你的真實輸入永遠到不了遊戲。 真實的客戶區輸入消息被吞掉,遊戲用來讀取或鎖定設備的 user32 調用看到的是虛擬狀態: ClipCursor(空操作:你的光標從不被鎖住)、SetCursorPos(只移動虛擬光標)、GetCursorPos/GetPhysicalCursorPos、 GetAsyncKeyState、GetKeyState、GetKeyboardState、GetForegroundWindow/GetActiveWindow/GetFocus(報告遊戲窗口)、 GetRawInputData/GetRawInputBuffer(真實的原始輸入被丟棄)。
  5. 截圖用 PrintWindow(PW_RENDERFULLCONTENT),窗口被擋住時也能用。
  6. 管理窗口(client/BeDDManager.ps1,用 Add-Type 編譯的 C# WinForms,每個登錄會話一個)通過 DWM 縮略圖顯示每個遊戲, 遊戲窗口在屏幕外時縮略圖仍是實時的;遊戲只顯示在這裡。Start-BeDDGame.ps1 通過 WMI(在 conhost --headless 裡調用 Win32_Process.Create) 啟動它,所以它不屬於調用者的進程樹或作業,啟動它的 agent 或測試框架退出後它仍然存在。模塊在 load() 裡連接它, 沒有管理窗口時就啟動一個(配置 managerScript:pwsh -File <它> -Ensure);90 秒內沒有管理窗口連上、或者徹底失去連接的遊戲會自行關閉。 遊戲總是把窗口創建為屏幕外的工具窗口(掛鉤 CreateWindowExW:遊戲在 enable() 之前不處理消息,所以在那之前無法從它的線程外修改窗口), 並一直留在那裡(SetWindowPos 的移動被丟棄,一個監視器會把它放回去)。管理窗口把每個已連接的遊戲放進它自己的 kill-on-close 作業(連接之後才放,啟動過程不受影響;一分鐘內沒連接的遊戲之後也會加入):關閉或結束管理窗口就會關閉這些遊戲。

環境變量 BEDD_RETURN_FOCUS(一個窗口句柄,由 Start-BeDDGame.ps1 和墊片的 Start-Game 設置)告訴模塊:遊戲啟動時若仍激活了自己, 把前臺還給哪個窗口。