Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/en_us/2.4-ControlMethods.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,46 @@ Different programs on Win32 handle rendering differently, so there is no univers
> FramePool and PrintWindow have built-in pseudo-minimize support: when the target window is minimized, they make it transparent and click-through, then restore it without activation, allowing screencap to continue without disturbing the user.
> Other screencap methods will fail when the target window is minimized. Please avoid minimizing the window when using those methods.

## Cloud (Windows)

The Cloud controller is a first-class preset, built on top of the Win32 controller, for cloud-streaming game clients (e.g. GeForce NOW). It lets a project enable a cloud-streaming client with minimal configuration — just a provider and the game title — instead of hand-writing the window class/title, screencap method, and input method. Internally, it desugars into a Win32 controller.

### Cloud Configuration

Configure via the `cloud` field in `interface.json`:

```jsonc
{
"name": "GFN",
"type": "Cloud",
"cloud": {
"provider": "geforce_now",
"game_title": "Endfield"
}
}
```

- `provider` (required): the cloud provider key. Currently the only supported value is `geforce_now` (GeForce NOW native desktop client).
- `game_title` (optional): a regex-safe fragment of the game name, substituted into the provider's window-title template. For `geforce_now` the template is `{game}.*on GeForce NOW`, so `"Endfield"` becomes the title regex `Endfield.*on GeForce NOW` (matches a window titled "Arknights: Endfield on GeForce NOW"); `"NTE"` becomes `NTE.*on GeForce NOW`.

### geforce_now Provider

The `geforce_now` provider bakes in the following Win32 settings, so users don't need to specify them manually:

| Item | Value |
| --- | --- |
| Owning process | `GeForceNOW.exe` |
| Window class | `CEFCLIENT` |
| Screencap method | `PrintWindow` |
| Input method | `Seize` |

The target window is located by matching process + class + title together.

> [!NOTE]
>
> - The Cloud controller is foreground-only and Windows-only (the GeForce NOW native client runs on Windows only), subject to the same runtime constraints as the Win32 foreground methods.
> - Additional providers can be added to the framework's built-in provider registry to support other cloud-streaming clients.

## MacOS

The MacOS controller is used to control native macOS applications on macOS.
Expand Down
40 changes: 40 additions & 0 deletions docs/zh_cn/2.4-控制方式说明.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,46 @@ Win32 下不同程序处理绘制的方法不同,不存在一个通用方式
> FramePool 和 PrintWindow 内置了伪最小化支持:当目标窗口被最小化时,会将窗口设为透明并开启点击穿透,以不激活的方式恢复窗口,从而在不打扰用户的情况下继续截图。
> 其他截图方式在窗口最小化后无法获取有效内容,请避免窗口最小化。

## Cloud (Windows)

Cloud 控制器是基于 Win32 控制器封装的预设,专为云串流游戏客户端(如 GeForce NOW)设计。项目只需配置一个 provider 和游戏名称,即可启用云串流客户端,无需手动编写窗口类名/标题、截图方式和输入方式。Cloud 控制器内部会脱糖为一个 Win32 控制器。

### Cloud 配置

通过 `interface.json` 中的 `cloud` 字段配置:

```jsonc
{
"name": "GFN",
"type": "Cloud",
"cloud": {
"provider": "geforce_now",
"game_title": "Endfield"
}
}
```

- `provider`(必填):云平台标识。目前仅支持 `geforce_now`(GeForce NOW 原生桌面客户端)。
- `game_title`(可选):游戏名称中可安全用于正则的片段,会被代入该 provider 的窗口标题模板。对于 `geforce_now`,模板为 `{game}.*on GeForce NOW`,因此 `"Endfield"` 会生成标题正则 `Endfield.*on GeForce NOW`(匹配标题为 "Arknights: Endfield on GeForce NOW" 的窗口);`"NTE"` 则生成 `NTE.*on GeForce NOW`。

### geforce_now Provider

`geforce_now` provider 内置了以下 Win32 配置,用户无需手动指定:

| 项目 | 值 |
| --- | --- |
| 所属进程 | `GeForceNOW.exe` |
| 窗口类名 | `CEFCLIENT` |
| 截图方式 | `PrintWindow` |
| 输入方式 | `Seize` |

目标窗口通过同时匹配进程、类名与标题来定位。

> [!NOTE]
>
> - Cloud 控制器仅支持前台运行,且仅支持 Windows(GeForce NOW 原生客户端仅运行于 Windows),与 Win32 前台方式具有相同的运行时限制。
> - 框架内置的 provider 注册表可以扩展,以支持更多云串流客户端。

## MacOS

MacOS 控制器用于在 macOS 上控制原生 macOS 应用程序。
Expand Down
4 changes: 4 additions & 0 deletions include/MaaToolkit/DesktopWindow/MaaToolkitDesktopWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ extern "C"
// macOS: window title (SCWindow.title)
MAA_TOOLKIT_API const char* MaaToolkitDesktopWindowGetWindowName(const MaaToolkitDesktopWindow* window);

// Win32: full image path of the owning process, e.g. "C:\\...\\GeForceNOW.exe"
// Other platforms: empty string
MAA_TOOLKIT_API const char* MaaToolkitDesktopWindowGetProcessPath(const MaaToolkitDesktopWindow* window);

#ifdef __cplusplus
}
#endif
17 changes: 15 additions & 2 deletions sample/interface.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@
"screencap": "ScreenCaptureKit", // 截图方式
"input": "GlobalEvent" // 输入方式
}
},
{
"name": "GFN",
"label": "$GeForceNOW端", // 显示名称,支持国际化
"description": "通过 GeForce NOW 云串流原生客户端连接(前台模式,需先在 GFN 中启动对应游戏)",
"type": "Cloud",
"display_short_side": 720, // GFN 串流以 720p 为基准
"cloud": {
// provider 决定框架内置的窗口签名与截图/输入方式:
// geforce_now => 进程 GeForceNOW.exe、窗口类 CEFCLIENT、标题 "{game}.*on GeForce NOW"、PrintWindow + Seize
"provider": "geforce_now",
// game_title 为游戏名的正则片段,代入标题模板匹配窗口
"game_title": "Endfield"
}
}
],
"resource": [
Expand All @@ -85,8 +99,7 @@
"name": "Bilibili",
"label": "$B站资源", // 显示名称,支持国际化。优先显示label,未设置时显示name
"path": [
"resource",
"resource_bilibili" // 资源路径,相对于interface.json所在目录
"resource" // 资源路径,相对于interface.json所在目录(resource_bilibili 为多资源包演示占位,示例未随附,故此处仅用 resource)
]
}
],
Expand Down
1 change: 1 addition & 0 deletions sample/interface_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"demo3-png": "./assets/demo-en.png",
"安卓端": "Android Device",
"电脑端": "Windows Desktop App",
"GeForceNOW端": "GeForce NOW (Cloud)",
"官服资源": "Official Server Resource Pack",
"B站资源": "Bilibili Server Resource Pack",
"收取荒原": "Collect Wilderness Resources",
Expand Down
1 change: 1 addition & 0 deletions sample/interface_zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"demo3-png": "./assets/demo-zh.png",
"安卓端": "安卓设备",
"电脑端": "Windows 桌面应用",
"GeForceNOW端": "GeForce NOW 云串流",
"官服资源": "官方服务器资源包",
"B站资源": "Bilibili 服务器资源包",
"收取荒原": "收取荒原资源",
Expand Down
95 changes: 95 additions & 0 deletions source/MaaPiCli/CLI/interactor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "MaaUtils/Encoding.h"
#include "MaaUtils/Logger.h"
#include "MaaUtils/Platform.h"
#include "ProjectInterface/CloudProviders.h"
#include "ProjectInterface/Runner.h"

static bool s_eof = false;
Expand Down Expand Up @@ -335,6 +336,7 @@ void Interactor::print_config() const
std::format("\t\t{}\n\t\t{}\n", config_.configuration().adb.adb_path, config_.configuration().adb.address));
break;
case InterfaceData::Controller::Type::Win32:
case InterfaceData::Controller::Type::Cloud:
if (config_.configuration().win32.hwnd) {
Comment on lines +339 to 340

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Cloud 控制器目前只打印由 Win32 hwnd 推导出的信息,可能遗漏有用的 provider/游戏上下文。

由于 Cloud 分支直接复用 Win32 的逻辑,它只会在 win32.hwnd 已设置时输出,并且忽略 Cloud 特有的元数据(例如 provider 名称、游戏标题)。请扩展 Cloud 分支的打印逻辑,加入 Cloud 配置的相关信息,并确保即使 Win32 句柄尚未解析成功时也能有输出。

Suggested implementation:

    case InterfaceData::Controller::Type::Win32:
        if (config_.configuration().win32.hwnd) {
            std::cout << MAA_NS::utf8_to_crt(std::format("\t\t{}\n", format_win32_config(config_.configuration().win32)));
        }
        config_.configuration().controller.type = InterfaceData::Controller::Type::Win32;
        select_win32_hwnd(controller.win32);
        break;
    case InterfaceData::Controller::Type::Cloud: {
        config_.configuration().controller.type = InterfaceData::Controller::Type::Cloud;

        // Always print Cloud-specific configuration, even if no native window handle
        // is available/resolved yet.
        std::cout << MAA_NS::utf8_to_crt(std::format("\t\t{}\n", format_cloud_config(config_.configuration().cloud)));

        select_cloud_hwnd(controller.cloud);
        break;
    }
  1. 实现一个 format_cloud_config(const decltype(config_.configuration().cloud)& cloud) 辅助函数(或等价实现),返回包含 provider 名称、游戏标题以及其他 Cloud 相关元数据的人类可读 std::string。将其与 format_win32_config 放在一起,以保持格式化逻辑一致。
  2. 如果实际配置类型/结构不同(例如嵌套在其他结构体中或成员名不同),请相应调整在 format_cloud_config 中访问字段的方式(config_.configuration().cloud)。
  3. 如果 select_cloud_hwnd 依赖或填充了额外的 Cloud 相关字段,可考虑扩展 format_cloud_config,在这些字段可用时选择性地输出它们。
Original comment in English

suggestion: Cloud controller prints only Win32 hwnd-derived info, which may omit useful provider/game context.

Since the Cloud case just reuses the Win32 path, it only prints when win32.hwnd is set and omits Cloud-specific metadata (e.g., provider name, game title). Please extend the Cloud printing logic to include Cloud configuration details, and ensure something is printed even when the Win32 handle hasn’t been resolved yet.

Suggested implementation:

    case InterfaceData::Controller::Type::Win32:
        if (config_.configuration().win32.hwnd) {
            std::cout << MAA_NS::utf8_to_crt(std::format("\t\t{}\n", format_win32_config(config_.configuration().win32)));
        }
        config_.configuration().controller.type = InterfaceData::Controller::Type::Win32;
        select_win32_hwnd(controller.win32);
        break;
    case InterfaceData::Controller::Type::Cloud: {
        config_.configuration().controller.type = InterfaceData::Controller::Type::Cloud;

        // Always print Cloud-specific configuration, even if no native window handle
        // is available/resolved yet.
        std::cout << MAA_NS::utf8_to_crt(std::format("\t\t{}\n", format_cloud_config(config_.configuration().cloud)));

        select_cloud_hwnd(controller.cloud);
        break;
    }
  1. Implement a format_cloud_config(const decltype(config_.configuration().cloud)& cloud) helper (or equivalent) that returns a human-readable std::string including provider name, game title, and any other relevant Cloud metadata. Place it alongside format_win32_config to keep formatting logic consistent.
  2. Adjust the field access in format_cloud_config (config_.configuration().cloud) if the actual configuration type/layout differs (e.g., nested under another struct or different member names).
  3. If select_cloud_hwnd depends on or populates additional Cloud-related fields, consider extending format_cloud_config to optionally include those fields when they are available.

std::cout << MAA_NS::utf8_to_crt(std::format("\t\t{}\n", format_win32_config(config_.configuration().win32)));
}
Expand Down Expand Up @@ -624,6 +626,10 @@ void Interactor::select_controller()
config_.configuration().controller.type = InterfaceData::Controller::Type::Win32;
select_win32_hwnd(controller.win32);
break;
case InterfaceData::Controller::Type::Cloud:
config_.configuration().controller.type = InterfaceData::Controller::Type::Cloud;
select_cloud_hwnd(controller.cloud);
break;
case InterfaceData::Controller::Type::MacOS:
config_.configuration().controller.type = InterfaceData::Controller::Type::MacOS;
select_macos(controller.macos);
Expand Down Expand Up @@ -881,6 +887,82 @@ bool Interactor::select_win32_hwnd(const MAA_PROJECT_INTERFACE_NS::InterfaceData
return true;
}

bool Interactor::select_cloud_hwnd(const MAA_PROJECT_INTERFACE_NS::InterfaceData::Controller::CloudConfig& cloud_config)
{
using namespace MAA_PROJECT_INTERFACE_NS;

const CloudProvider* provider = find_cloud_provider(cloud_config.provider);
if (!provider) {
LogError << "Unknown cloud provider" << VAR(cloud_config.provider);
mpause();
return false;
}

auto list_handle = MaaToolkitDesktopWindowListCreate();
OnScopeLeave([&]() { MaaToolkitDesktopWindowListDestroy(list_handle); });

MaaToolkitDesktopWindowFindAll(list_handle);

size_t list_size = MaaToolkitDesktopWindowListSize(list_handle);

// Cloud desugars to Win32: match the provider's window class + composed title,
// additionally filtered by the owning process for robustness. The resolved HWND
// is stored in the shared win32 config slot.
std::string window_regex_str = cloud_window_regex(*provider, cloud_config.game_title);
auto class_regex = MAA_NS::regex_valid(MAA_NS::to_u16(provider->class_regex));
auto window_regex = MAA_NS::regex_valid(MAA_NS::to_u16(window_regex_str));
auto process_regex = MAA_NS::regex_valid(MAA_NS::to_u16(provider->process_regex));
if (!class_regex || !window_regex || !process_regex) {
LogError << "regex is invalid" << VAR(provider->class_regex) << VAR(window_regex_str) << VAR(provider->process_regex);
return false;
}

std::vector<Configuration::Win32Config> matched_config;
for (size_t i = 0; i < list_size; ++i) {
Configuration::Win32Config rt_config;

auto window_handle = MaaToolkitDesktopWindowListAt(list_handle, i);
rt_config.hwnd = MaaToolkitDesktopWindowGetHandle(window_handle);
rt_config.class_name = MAA_NS::to_u16(MaaToolkitDesktopWindowGetClassName(window_handle));
rt_config.window_name = MAA_NS::to_u16(MaaToolkitDesktopWindowGetWindowName(window_handle));
std::wstring process_path = MAA_NS::to_u16(MaaToolkitDesktopWindowGetProcessPath(window_handle));

// Process filter is applied only when the process path is available; if it
// cannot be queried, fall back to class + title (the baseline that MaaEnd
// and MaaNTE both ship).
bool process_ok = process_path.empty() || boost::regex_search(process_path, *process_regex);

if (process_ok && boost::regex_search(rt_config.class_name, *class_regex)
&& boost::regex_search(rt_config.window_name, *window_regex)) {
matched_config.emplace_back(std::move(rt_config));
}
}

if (matched_config.empty()) {
LogError << "Cloud window not found" << VAR(cloud_config.provider) << VAR(provider->class_regex) << VAR(window_regex_str)
<< VAR(provider->process_regex);
mpause();
return false;
}
size_t matched_size = matched_config.size();
if (matched_size == 1) {
config_.configuration().win32 = matched_config.front();
return true;
}

std::cout << "### Select HWND ###\n\n";

for (size_t i = 0; i < matched_size; ++i) {
std::cout << MAA_NS::utf8_to_crt(std::format("\t{}. {}\n", i + 1, format_win32_config(matched_config.at(i))));
}
std::cout << "\n";

int index = input(matched_size) - 1;
config_.configuration().win32 = matched_config.at(index);

return true;
}

void Interactor::select_gamepad(const MAA_PROJECT_INTERFACE_NS::InterfaceData::Controller::GamepadConfig& gamepad_config)
{
using namespace MAA_PROJECT_INTERFACE_NS;
Expand Down Expand Up @@ -1695,6 +1777,19 @@ bool Interactor::check_validity()
return select_win32_hwnd(controller_iter->win32);
}

if (config_.configuration().controller.type == InterfaceData::Controller::Type::Cloud
&& config_.configuration().win32.hwnd == nullptr) {
auto& name = config_.configuration().controller.name;
auto controller_iter = std::ranges::find(config_.interface_data().controller, name, std::mem_fn(&InterfaceData::Controller::name));

if (controller_iter == config_.interface_data().controller.end()) {
LogError << "Contorller not found" << VAR(name);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (typo): 缺少控制器时的错误日志字符串中有拼写错误。

日志消息中 "Controller" 拼写不正确。请修正以便日志可被正确检索,并避免调试时造成困扰。

Suggested change
LogError << "Contorller not found" << VAR(name);
LogError << "Controller not found" << VAR(name);
Original comment in English

nitpick (typo): Typo in error message string for missing controller.

The log message spells "Controller" incorrectly. Please correct it to keep logs searchable and avoid confusion during debugging.

Suggested change
LogError << "Contorller not found" << VAR(name);
LogError << "Controller not found" << VAR(name);

return false;
}

return select_cloud_hwnd(controller_iter->cloud);
}

if (config_.configuration().controller.type == InterfaceData::Controller::Type::MacOS) {
auto& mac = config_.configuration().macos;
if (mac.window_id == 0 || mac.title.empty()) {
Expand Down
1 change: 1 addition & 0 deletions source/MaaPiCli/CLI/interactor.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Interactor
void select_adb_manual_input();

bool select_win32_hwnd(const MAA_PROJECT_INTERFACE_NS::InterfaceData::Controller::Win32Config& win32_config);
bool select_cloud_hwnd(const MAA_PROJECT_INTERFACE_NS::InterfaceData::Controller::CloudConfig& cloud_config);
void select_macos(const MAA_PROJECT_INTERFACE_NS::InterfaceData::Controller::MacOSConfig& macos_config);
void select_playcover(const MAA_PROJECT_INTERFACE_NS::InterfaceData::Controller::PlayCoverConfig& playcover_config);
void select_gamepad(const MAA_PROJECT_INTERFACE_NS::InterfaceData::Controller::GamepadConfig& gamepad_config);
Expand Down
29 changes: 29 additions & 0 deletions source/MaaPiCli/Impl/Configurator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "MaaUtils/Logger.h"
#include "MaaUtils/Platform.h"
#include "MaaUtils/StringMisc.hpp"
#include "ProjectInterface/CloudProviders.h"
#include "ProjectInterface/Parser.h"

MAA_PROJECT_INTERFACE_NS_BEGIN
Expand Down Expand Up @@ -241,6 +242,34 @@ std::optional<RuntimeParam> Configurator::generate_runtime() const
runtime.controller_param = std::move(win32);
} break;

case InterfaceData::Controller::Type::Cloud: {
// Cloud desugars to a Win32 controller: the resolved HWND is stored in the
// shared win32 config slot; the screencap + input methods come from the
// provider registry (default PrintWindow + Seize for GeForce NOW).
RuntimeParam::Win32Param win32;

win32.hwnd = config_.win32.hwnd;

const CloudProvider* provider = find_cloud_provider(controller.cloud.provider);
if (!provider) {
LogError << "Unknown cloud provider" << VAR(controller.cloud.provider);
return std::nullopt;
}

win32.screencap = parse_win32_screencap_method(provider->screencap);
if (win32.screencap == MaaWin32ScreencapMethod_None) {
win32.screencap = MaaWin32ScreencapMethod_PrintWindow;
}

win32.mouse = parse_win32_input_method(provider->input);
if (win32.mouse == MaaWin32InputMethod_None) {
win32.mouse = MaaWin32InputMethod_Seize;
}
win32.keyboard = win32.mouse;

runtime.controller_param = std::move(win32);
} break;

case InterfaceData::Controller::Type::MacOS: {
RuntimeParam::MacOSParam macos;

Expand Down
1 change: 1 addition & 0 deletions source/MaaToolkit/API/MaaToolkitBufferTypes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ struct MaaToolkitDesktopWindow
virtual void* handle() const = 0;
virtual const std::string& class_name() const = 0;
virtual const std::string& window_name() const = 0;
virtual const std::string& process_path() const = 0;
};
10 changes: 10 additions & 0 deletions source/MaaToolkit/API/MaaToolkitDesktopWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,13 @@ const char* MaaToolkitDesktopWindowGetWindowName(const MaaToolkitDesktopWindow*

return window->window_name().c_str();
}

const char* MaaToolkitDesktopWindowGetProcessPath(const MaaToolkitDesktopWindow* window)
{
if (!window) {
LogError << "window is null";
return "";
}

return window->process_path().c_str();
}
7 changes: 6 additions & 1 deletion source/MaaToolkit/DesktopWindow/DesktopWindowBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ struct DesktopWindow
void* hwnd = nullptr;
std::wstring class_name;
std::wstring window_name;
std::wstring process_path;

MEO_TOJSON(hwnd, class_name, window_name);
MEO_TOJSON(hwnd, class_name, window_name, process_path);
};

class DesktopWindowBuffer : public MaaToolkitDesktopWindow
Expand All @@ -26,6 +27,7 @@ class DesktopWindowBuffer : public MaaToolkitDesktopWindow
: hwnd_(window.hwnd)
, class_name_(from_u16(window.class_name))
, window_name_(from_u16(window.window_name))
, process_path_(from_u16(window.process_path))
{
}

Expand All @@ -37,10 +39,13 @@ class DesktopWindowBuffer : public MaaToolkitDesktopWindow

virtual const std::string& window_name() const override { return window_name_; }

virtual const std::string& process_path() const override { return process_path_; }

private:
void* hwnd_ = nullptr;
std::string class_name_;
std::string window_name_;
std::string process_path_;
};

MAA_TOOLKIT_NS_END
Expand Down
Loading
Loading