Skip to content

Commit e69d7be

Browse files
authored
feat(win32): 添加后台托管按键守护,支持后台长按移动 (#1231)
## 概述 为 Win32 控制器添加**后台托管按键守护**(Background Managed Key Guardian)能力,支持在后台持续维持指定按键的按下/释放状态。这是"实现终末地后台移动"计划的一部分,解决后台长按 WASD 移动的需求。 ## 动机 《明日方舟:终末地》等游戏使用 WASD 控制角色移动,需要按键在后台被持续按住。传统的 `PostMessage` 方式无法可靠地维持按键状态,因为系统层面的热键拦截、焦点切换等因素会导致按键"丢失"。本 PR 引入专用守护线程,以 5ms 间隔轮询 + 差量校正的方式确保按键状态一致性。 ## 实现方案 ### 核心组件:`BackgroundManagedKeyInput` - 独立守护线程维护"期望按下键集合"与"受管键域" - 每 5ms 通过 `GetAsyncKeyState` 检测实际按键状态,与期望状态做差量校正 - 使用 `RegisterHotKey` → `SendInput` → `UnregisterHotKey` 的三步法确保按键注入不被系统吃掉 - 每次按下受管键后发送 F13 nudge(终末地特定需求) - generation 计数 + condition_variable 实现同步等待(500ms 超时) ### 透明路由机制 - `ControllerAgent` 的 `handle_click_key` / `handle_key_down` / `handle_key_up` / `handle_long_press_key` 自动检测受管键,命中则走后台守护路径 - Pipeline 层面完全透明,无需修改现有任务定义 ### 生命周期管理 - `MaaControllerPostInactive` 自动释放所有受管键 - `post_stop()` 自动 post inactive action 释放持有的输入状态 - 析构函数确保清理 ## API 变更 新增 C API: ```c MaaCtrlId MaaControllerPostSetBackgroundManagedKeys( MaaController* ctrl, const int32_t* keycodes, MaaSize count); ``` 新增 Python / Node.js 绑定:`post_set_background_managed_keys(keys)` 行为变更: - `MaaControllerPostInactive` 现在会额外释放所有后台受管键 - 任务停止时自动释放持有的输入状态 ## 变更范围 - 新增:`BackgroundManagedKeyInput` 守护类(~500 行核心实现) - 修改:ControllerAgent 路由逻辑、Win32ControlUnitMgr、Tasker 生命周期 - 新增:Agent Client/Server 远程支持 - 新增:Python / Node.js 语言绑定 - 更新:中英文文档 ## 测试计划 - [x] Python binding 测试覆盖 `post_set_background_managed_keys` - [x] Agent 测试覆盖远程托管按键场景 - [x] 终末地实际验证:后台 WASD 长按移动 - [x] 任务停止/inactive 后确认按键正确释放 - [x] 多键同时托管场景验证 --- 此 PR 是"实现终末地后台移动"计划的按键控制部分,与 feat/win32-mouse-lock-follow(鼠标锁定跟随)配合使用。 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by Sourcery 为 Win32 后台托管密钥 guardian 提供支持,并通过 Python、Node.js、agent 客户端/服务端的控制器选项和绑定暴露该功能,同时更新相应的测试和文档。 新特性: - 为 Win32 控制器引入 `BackgroundManagedKeyInput` guardian,用于在托管域中维护指定按键,并通过该 guardian 路由匹配的按键操作。 - 新增 `MaaCtrlOption_BackgroundManagedKeys` 控制器选项,并将其贯通到 `ControllerAgent`、`Win32ControlUnitMgr` 和 `Win32ControlUnitAPI`。 - 在 Python 和 Node.js 控制器绑定中暴露后台托管密钥配置,并通过 agent 客户端/服务端协议传递该选项。 增强: - 扩展 Win32 控制管理器的“非活动”处理逻辑,以释放后台托管密钥,并将 guardian 路由集成到点击和按键操作中。 文档: - 在中英文的集成接口与控制方法指南中,记录 `BackgroundManagedKeys` 控制器选项以及 Win32 后台托管密钥 guardian 的行为。 测试: - 增加 Python 绑定和 agent 测试,用于验证 Win32 与非 Win32 控制器的后台托管密钥选项行为。 <details> <summary>Original summary in English</summary> ## Summary by Sourcery Add Win32 background managed key guardian support and expose it through controller options and bindings for Python, Node.js, agent client/server, with tests and docs updated accordingly. New Features: - Introduce BackgroundManagedKeyInput guardian for Win32 controllers to maintain specified keys in a managed domain and route matching key operations through it. - Add MaaCtrlOption_BackgroundManagedKeys controller option and wire it through ControllerAgent, Win32ControlUnitMgr, and Win32ControlUnitAPI. - Expose background managed key configuration in Python and Node.js controller bindings and propagate the option over agent client/server protocols. Enhancements: - Extend Win32 control manager inactive handling to release background managed keys and integrate guardian routing into click and key operations. Documentation: - Document the BackgroundManagedKeys controller option and Win32 background managed key guardian behavior in English and Chinese integrated interface and control method guides. Tests: - Add Python binding and agent tests to validate background managed keys option behavior for Win32 and non-Win32 controllers. </details> <details> <summary>Original summary in English</summary> ## Summary by Sourcery 为 Win32 后台托管密钥 guardian 提供支持,并通过 Python、Node.js、agent 客户端/服务端的控制器选项和绑定暴露该功能,同时更新相应的测试和文档。 新特性: - 为 Win32 控制器引入 `BackgroundManagedKeyInput` guardian,用于在托管域中维护指定按键,并通过该 guardian 路由匹配的按键操作。 - 新增 `MaaCtrlOption_BackgroundManagedKeys` 控制器选项,并将其贯通到 `ControllerAgent`、`Win32ControlUnitMgr` 和 `Win32ControlUnitAPI`。 - 在 Python 和 Node.js 控制器绑定中暴露后台托管密钥配置,并通过 agent 客户端/服务端协议传递该选项。 增强: - 扩展 Win32 控制管理器的“非活动”处理逻辑,以释放后台托管密钥,并将 guardian 路由集成到点击和按键操作中。 文档: - 在中英文的集成接口与控制方法指南中,记录 `BackgroundManagedKeys` 控制器选项以及 Win32 后台托管密钥 guardian 的行为。 测试: - 增加 Python 绑定和 agent 测试,用于验证 Win32 与非 Win32 控制器的后台托管密钥选项行为。 <details> <summary>Original summary in English</summary> ## Summary by Sourcery Add Win32 background managed key guardian support and expose it through controller options and bindings for Python, Node.js, agent client/server, with tests and docs updated accordingly. New Features: - Introduce BackgroundManagedKeyInput guardian for Win32 controllers to maintain specified keys in a managed domain and route matching key operations through it. - Add MaaCtrlOption_BackgroundManagedKeys controller option and wire it through ControllerAgent, Win32ControlUnitMgr, and Win32ControlUnitAPI. - Expose background managed key configuration in Python and Node.js controller bindings and propagate the option over agent client/server protocols. Enhancements: - Extend Win32 control manager inactive handling to release background managed keys and integrate guardian routing into click and key operations. Documentation: - Document the BackgroundManagedKeys controller option and Win32 background managed key guardian behavior in English and Chinese integrated interface and control method guides. Tests: - Add Python binding and agent tests to validate background managed keys option behavior for Win32 and non-Win32 controllers. </details> </details> --------- Co-authored-by: ZeroAd-06 <190387379+ZeroAd-06@users.noreply.github.com>
1 parent 0c34cef commit e69d7be

21 files changed

Lines changed: 722 additions & 1 deletion

File tree

docs/en_us/2.2-IntegratedInterfaceOverview.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,9 @@ Set controller options. Will be split into specific options in bindings.
359359
- ScreenshotResizeMethod
360360
Set screenshot resize interpolation method (0=INTER_NEAREST, 1=INTER_LINEAR, 2=INTER_CUBIC, 3=INTER_AREA, 4=INTER_LANCZOS4), default is INTER_AREA(3)
361361

362+
- BackgroundManagedKeys
363+
Declare Win32 background managed key domain. After setting, matching `MaaControllerPostClickKey`, `MaaControllerPostKeyDown`, `MaaControllerPostKeyUp` and pipeline `ClickKey`, `LongPressKey`, `KeyDown`, `KeyUp` operations automatically route through the background guardian path. Only supported by Win32 controllers; other controllers will fail.
364+
362365
### MaaControllerPostConnection
363366

364367
Asynchronously connect device. This is an asynchronous operation that immediately returns an operation id. You can query the status via `MaaControllerStatus` and `MaaControllerWait`.

docs/en_us/2.4-ControlMethods.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Different programs on Win32 handle input differently, so there is no universal m
9999
> - The `WithCursorPos` methods briefly move the cursor to the target position, then restore it after sending the message, hence "Brief" cursor seizure, but it does not block user operations.
100100
> - The `WithWindowPos` methods briefly move the window so the target position aligns with the current cursor position, then restore the window position after sending the message. The cursor is not moved, so there is no mouse seizure, but the window may briefly flicker.
101101
> - Win32 also provides a **Mouse Lock Follow** mode: enable with `MaaControllerSetOption(ctrl, MaaCtrlOption_MouseLockFollow, &enabled, sizeof(bool))` (set `enabled` to `true` to enable, `false` to disable). Designed for TPS/FPS games that lock the mouse to the window in the background. When enabled, the window continuously follows the mouse cursor, and RawInput counter-moves prevent the game from sensing hardware mouse movement. Use `MaaControllerPostRelativeMove` to inject intentional camera rotation while this mode is active. **Note:** On Win32, `MaaControllerPostRelativeMove` requires mouse-lock-follow mode to be active; calling it without this mode will fail. Only supported with MessageInput-based input methods (SendMessage / PostMessage variants).
102+
> - Win32 also provides a **Background Managed Keys** guardian: declare the managed key domain through `MaaControllerSetOption(ctrl, MaaCtrlOption_BackgroundManagedKeys, keycodes, sizeof(int32_t) * count)` with an array of virtual key codes to manage. After setting, matching key operations automatically route through the background guardian path and continuously correct key state while the controller is idle.
102103
103104
### Win32 Screencap
104105

docs/zh_cn/2.2-集成接口一览.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,9 @@
359359
- ScreenshotResizeMethod
360360
设置截图缩放插值方法(0=INTER_NEAREST, 1=INTER_LINEAR, 2=INTER_CUBIC, 3=INTER_AREA, 4=INTER_LANCZOS4),默认为 INTER_AREA(3)
361361

362+
- BackgroundManagedKeys
363+
声明 Win32 后台受管键域。声明成功后,现有 `MaaControllerPostClickKey``MaaControllerPostKeyDown``MaaControllerPostKeyUp` 以及 pipeline 中的 `ClickKey``LongPressKey``KeyDown``KeyUp` 会对命中的键自动走后台守护路径。该选项仅支持 Win32 控制器;其他控制器设置会失败。
364+
362365
### MaaControllerPostConnection
363366

364367
异步连接设备。这是一个异步操作,会立即返回一个操作 id,可通过 `MaaControllerStatus``MaaControllerWait` 查询状态。

docs/zh_cn/2.4-控制方式说明.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Win32 下不同程序处理输入的方法不同,不存在一个通用方式
9999
> - `WithCursorPos` 系列方式会短暂移动光标到目标位置,发送完消息后会将光标移回原位置,因此会“短暂”抢占鼠标,但不会阻止用户操作。
100100
> - `WithWindowPos` 系列方式会短暂移动窗口,使目标位置与当前光标位置重合,发送完消息后会将窗口移回原位置。不会移动光标,因此不抢占鼠标,但窗口会短暂闪烁。
101101
> - Win32 还提供了**鼠标锁定跟随模式**(Mouse Lock Follow):通过 `MaaControllerSetOption(ctrl, MaaCtrlOption_MouseLockFollow, &enabled, sizeof(bool))` 开启(`enabled``true` 开启,`false` 关闭),适用于 TPS/FPS 等在后台将鼠标锁定到窗口内的游戏。开启后窗口会始终跟随鼠标移动,同时通过 RawInput 对冲阻止游戏感知硬件鼠标位移。配合 `MaaControllerPostRelativeMove` 可在此模式下注入视角旋转。**注意:** Win32 平台的 `MaaControllerPostRelativeMove` 需要先开启鼠标锁定跟随模式,否则调用将失败。仅支持 MessageInput 系列输入方式(SendMessage / PostMessage 及其变体)。
102+
> - Win32 还提供了**后台受管键守护**(Background Managed Keys):通过 `MaaControllerSetOption(ctrl, MaaCtrlOption_BackgroundManagedKeys, keycodes, sizeof(int32_t) * count)` 声明需要接管的虚拟键码数组。声明成功后,命中的按键操作自动走后台守护路径,并在控制器空闲时持续修正按键状态。
102103
103104
### Win32 Screencap
104105

include/MaaControlUnit/ControlUnitAPI.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class Win32ControlUnitAPI
101101
virtual ~Win32ControlUnitAPI() = default;
102102

103103
virtual bool set_mouse_lock_follow(bool /*enabled*/) { return false; }
104+
virtual bool set_background_managed_keys_option(const int32_t* /*keycodes*/, size_t /*count*/) { return false; }
104105
};
105106

106107
class MacOSControlUnitAPI

include/MaaFramework/MaaDef.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,14 @@ enum MaaCtrlOptionEnum
234234
///
235235
/// value: int, eg: 3; val_size: sizeof(int)
236236
MaaCtrlOption_ScreenshotResizeMethod = 6,
237+
238+
/// Configure background managed key domain for Win32 controllers.
239+
/// Must be set before connection. After setting, matching ClickKey / LongPressKey / KeyDown / KeyUp
240+
/// operations automatically route through the background guardian path.
241+
/// Only supported by Win32 controllers; other controllers will fail.
242+
///
243+
/// value: int32_t array of virtual key codes; val_size: sizeof(int32_t) * count
244+
MaaCtrlOption_BackgroundManagedKeys = 7,
237245
};
238246

239247
typedef MaaOption MaaTaskerOption;

source/MaaAgentClient/Client/AgentClient.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2460,6 +2460,27 @@ bool AgentClient::handle_controller_set_option(const json::value& j)
24602460
ret = controller->set_option(key, &v, sizeof(v));
24612461
break;
24622462
}
2463+
case MaaCtrlOption_BackgroundManagedKeys: {
2464+
if (!req.value.is_array()) {
2465+
LogError << "BackgroundManagedKeys value must be an array" << VAR(req.value.type_name());
2466+
ret = false;
2467+
break;
2468+
}
2469+
std::vector<int32_t> keys;
2470+
for (const auto& item : req.value.as_array()) {
2471+
if (!item.is_number()) {
2472+
LogError << "BackgroundManagedKeys array element must be a number" << VAR(item.type_name());
2473+
ret = false;
2474+
break;
2475+
}
2476+
keys.push_back(static_cast<int32_t>(item.as_integer()));
2477+
}
2478+
if (!ret) {
2479+
break;
2480+
}
2481+
ret = controller->set_option(key, keys.data(), sizeof(int32_t) * keys.size());
2482+
break;
2483+
}
24632484
default:
24642485
LogError << "unknown key" << VAR(req.key);
24652486
break;

source/MaaAgentServer/RemoteInstance/RemoteController.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@ bool RemoteController::set_option(MaaCtrlOption key, MaaOptionValue value, MaaOp
4242
jvalue = *reinterpret_cast<const bool*>(value);
4343
break;
4444

45+
case MaaCtrlOption_BackgroundManagedKeys: {
46+
if (val_size != 0 && val_size % sizeof(int32_t) != 0) {
47+
LogError << "invalid val_size for int32_t[] option" << VAR(key) << VAR(val_size);
48+
return false;
49+
}
50+
size_t count = val_size / sizeof(int32_t);
51+
auto keycodes = reinterpret_cast<const int32_t*>(value);
52+
json::array arr;
53+
for (size_t i = 0; i < count; ++i) {
54+
arr.emplace_back(keycodes[i]);
55+
}
56+
jvalue = std::move(arr);
57+
break;
58+
}
59+
4560
default:
4661
LogError << "unknown key" << VAR(key);
4762
return false;

source/MaaFramework/Controller/ControllerAgent.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ bool ControllerAgent::set_option(MaaCtrlOption key, MaaOptionValue value, MaaOpt
4545
return set_mouse_lock_follow_option(value, val_size);
4646
case MaaCtrlOption_ScreenshotResizeMethod:
4747
return set_screenshot_resize_method(value, val_size);
48+
case MaaCtrlOption_BackgroundManagedKeys:
49+
return set_background_managed_keys_option(value, val_size);
4850

4951
default:
5052
LogError << "Unknown key" << VAR(key) << VAR(value);
@@ -1250,4 +1252,30 @@ bool ControllerAgent::set_screenshot_resize_method(MaaOptionValue value, MaaOpti
12501252
return true;
12511253
}
12521254

1255+
bool ControllerAgent::set_background_managed_keys_option(MaaOptionValue value, MaaOptionValueSize val_size)
1256+
{
1257+
LogDebug;
1258+
1259+
if (val_size != 0 && val_size % sizeof(int32_t) != 0) {
1260+
LogError << "invalid value size: " << val_size;
1261+
return false;
1262+
}
1263+
1264+
if (!control_unit_) {
1265+
LogError << "control_unit_ is nullptr";
1266+
return false;
1267+
}
1268+
1269+
auto win32_unit = std::dynamic_pointer_cast<MAA_CTRL_UNIT_NS::Win32ControlUnitAPI>(control_unit_);
1270+
if (!win32_unit) {
1271+
LogError << "Background managed keys is only supported for Win32 controllers.";
1272+
return false;
1273+
}
1274+
1275+
size_t count = val_size / sizeof(int32_t);
1276+
auto keycodes = reinterpret_cast<const int32_t*>(value);
1277+
1278+
return win32_unit->set_background_managed_keys_option(keycodes, count);
1279+
}
1280+
12531281
MAA_CTRL_NS_END

source/MaaFramework/Controller/ControllerAgent.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ class ControllerAgent : public MaaController
290290
bool set_image_use_raw_size(MaaOptionValue value, MaaOptionValueSize val_size);
291291
bool set_mouse_lock_follow_option(MaaOptionValue value, MaaOptionValueSize val_size);
292292
bool set_screenshot_resize_method(MaaOptionValue value, MaaOptionValueSize val_size);
293+
bool set_background_managed_keys_option(MaaOptionValue value, MaaOptionValueSize val_size);
293294

294295
private:
295296
bool need_to_stop_ = false;

0 commit comments

Comments
 (0)