工作原理
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- 键盘、文字和指针位置走游戏自己的窗口过程。 模块子类化游戏窗口。按键变成
WM_KEYDOWN/WM_KEYUP,文字变成WM_CHAR,指针变成WM_MOUSEMOVE加一个虚拟光标(被挂钩的GetCursorPos返回的位置),在游戏线程上按顺序分发。 游戏把它们当作真实输入处理(MainGameCore::WndProc_PC::onKeyboardAction→HIDControllerGameCoreDesktop::onKeyDown), 所以聊天、键位映射和界面悬停都能用。游戏线程处理完输入后才回复,所以脚本的结果是确定的。 - 鼠标按钮、滚轮和视角走虚拟 GameInput 鼠标。 基岩版用 GDK 的 GameInput API(
GameInputRedist.dll,接口 v2) 直接从设备读取鼠标:WM_LBUTTONDOWN之类的窗口消息会被忽略;没有模块的话,你真实的鼠标仍会在后台游戏里点击、转视角。 模块挂钩IGameInputReading::GetMouseState(以及GetKeyCount/GetKeyState):游戏读到的是虚拟按钮、 相对位移(视角)、滚轮和绝对位置。设备报告输入之前,GameInput 没有该鼠标的读数,所以在没人碰过的电脑上, 游戏的GetCurrentReading会从模块拿到该设备的一个虚拟读数。 - 游戏从不抢前台,也不会失去焦点。 失去焦点的消息被吞掉,所以游戏不会暂停或松开按键。
Start-BeDDGame.ps1用SW_SHOWNOACTIVATE启动 exe;被挂钩的ShowWindow/SetWindowPos/SetActiveWindow/BringWindowToTop/SetForegroundWindow从不激活游戏窗口,窗口一直保持在 Z 序最底部。user32 钩子在load()里就装好了:游戏在enable()之前就创建并显示窗口。 模块发送的"获得焦点"消息会跳过默认的WM_ACTIVATE处理(挂钩DefWindowProcW),否则它会设置焦点从而激活窗口: 从用户前台应用(终端、agent)启动的游戏被允许抢前台。 - 你的真实输入永远到不了游戏。 真实的客户区输入消息被吞掉,游戏用来读取或锁定设备的 user32 调用看到的是虚拟状态:
ClipCursor(空操作:你的光标从不被锁住)、SetCursorPos(只移动虚拟光标)、GetCursorPos/GetPhysicalCursorPos、GetAsyncKeyState、GetKeyState、GetKeyboardState、GetForegroundWindow/GetActiveWindow/GetFocus(报告游戏窗口)、GetRawInputData/GetRawInputBuffer(真实的原始输入被丢弃)。 - 截图用
PrintWindow(PW_RENDERFULLCONTENT),窗口被挡住时也能用。 - 管理窗口(
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 设置)告诉模块:游戏启动时若仍激活了自己,
把前台还给哪个窗口。