feat(controller): first-class Cloud controller preset (GeForce NOW) - #1400
feat(controller): first-class Cloud controller preset (GeForce NOW)#1400zeejaytan wants to merge 3 commits into
Conversation
Cloud-streaming clients (GeForce NOW) could previously only be connected as a
generic Win32 window, hand-configured per downstream project with a brittle
CEFCLIENT class + title-substring match, duplicating the same PrintWindow/Seize
knowledge everywhere. There was zero GeForce NOW awareness in the framework.
Add a first-class `Cloud` controller type that desugars to Win32:
- MaaToolkit: expose the owning process image path on desktop windows
(DesktopWindowWin32Finder via QueryFullProcessImageNameW) + new
MaaToolkitDesktopWindowGetProcessPath accessor, so cloud windows can be
disambiguated by process (GeForceNOW.exe), not just a generic CEF class.
- ProjectInterface: new Controller::Type::Cloud + CloudConfig{provider, game_title};
a built-in provider registry (CloudProviders.h) carries each provider's
process/class/title-template + screencap/input. First entry: geforce_now
(GeForceNOW.exe, CEFCLIENT, "{game}.*on GeForce NOW", PrintWindow, Seize).
- MaaPiCli: select_cloud_hwnd resolves the window by process + class + composed
title and stores the HWND in the shared win32 slot; Configurator desugars Cloud
to a Win32Param. Downstream declares only {provider, game_title}.
- Schema + docs (en/zh) for the Cloud type and geforce_now provider.
Signatures reused verbatim from the shipping MaaEnd/MaaNTE GFN configs; the title
template reproduces both "Endfield.*on GeForce NOW" and "NTE.*on GeForce NOW".
Adding another provider (Boosteroid, Xbox Cloud) is a single registry entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Demonstrates the new Cloud controller type alongside the existing Android/ Windows/macOS entries, with full metadata (label, description, display, cloud config). Doubles as a ready-to-run controller for testing GFN connection via MaaPiCli. Adds the label translations (zh/en). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Bilibili resource referenced a resource_bilibili folder the sample
never ships, so any non-Android controller (Win32/macOS/Cloud) failed to
load a resource ("path not exists"). Point it at the existing resource
dir so the sample runs for all controllers, including the new GFN/Cloud one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Hey - 我发现了 3 个问题,并给出了一些整体性的反馈:
- 在
Interactor::check_validity中,日志信息 "Contorller not found" 有拼写错误,应更正为 "Controller not found",以便提供更清晰的诊断信息。 - 解析
Cloudprovider 的逻辑(包括find_cloud_provider以及对未知 provider 的错误处理)在Interactor::select_cloud_hwnd和Configurator::generate_runtime中存在重复;建议将其抽取为一个共享的辅助函数,以避免行为分歧并保持一致性。
给 AI Agent 的提示
Please address the comments from this code review:
## Overall Comments
- In `Interactor::check_validity`, the log message "Contorller not found" has a typo and should be corrected to "Controller not found" for clearer diagnostics.
- The logic for resolving a `Cloud` provider (including `find_cloud_provider` and error handling for unknown providers) is duplicated between `Interactor::select_cloud_hwnd` and `Configurator::generate_runtime`; consider centralizing this into a shared helper to avoid divergence and keep behavior consistent.
## Individual Comments
### Comment 1
<location path="source/include/ProjectInterface/Types.h" line_range="72" />
<code_context>
+ struct CloudConfig
+ {
+ std::string provider; // provider key, e.g. "geforce_now"
+ std::string game_title; // regex-safe game fragment substituted into the provider title template
+
+ MEO_JSONIZATION(provider, MEO_OPT game_title);
</code_context>
<issue_to_address>
**issue (bug_risk):** Cloud game_title is treated as a raw regex fragment, which can easily break matching if the title contains regex metacharacters.
This relies on callers passing a regex-safe `game_title`, but many will likely provide a plain title (e.g. `"Arknights: Endfield (Global)"`), causing `regex_valid` to fail and `select_cloud_hwnd` to abort. Either escape `game_title` before inserting it into `title_template`, or explicitly require and validate regex-safe input and surface a clear configuration error when it is not.
</issue_to_address>
### Comment 2
<location path="source/MaaPiCli/CLI/interactor.cpp" line_range="339-340" />
<code_context>
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) {
std::cout << MAA_NS::utf8_to_crt(std::format("\t\t{}\n", format_win32_config(config_.configuration().win32)));
}
</code_context>
<issue_to_address>
**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:
```cpp
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.
</issue_to_address>
### Comment 3
<location path="source/MaaPiCli/CLI/interactor.cpp" line_range="1786" />
<code_context>
+ 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);
+ return false;
+ }
</code_context>
<issue_to_address>
**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.
```suggestion
LogError << "Controller not found" << VAR(name);
```
</issue_to_address>帮我变得更有用!请对每条评论点 👍 或 👎,我会基于这些反馈改进后续的评审。
Original comment in English
Hey - I've found 3 issues, and left some high level feedback:
- In
Interactor::check_validity, the log message "Contorller not found" has a typo and should be corrected to "Controller not found" for clearer diagnostics. - The logic for resolving a
Cloudprovider (includingfind_cloud_providerand error handling for unknown providers) is duplicated betweenInteractor::select_cloud_hwndandConfigurator::generate_runtime; consider centralizing this into a shared helper to avoid divergence and keep behavior consistent.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `Interactor::check_validity`, the log message "Contorller not found" has a typo and should be corrected to "Controller not found" for clearer diagnostics.
- The logic for resolving a `Cloud` provider (including `find_cloud_provider` and error handling for unknown providers) is duplicated between `Interactor::select_cloud_hwnd` and `Configurator::generate_runtime`; consider centralizing this into a shared helper to avoid divergence and keep behavior consistent.
## Individual Comments
### Comment 1
<location path="source/include/ProjectInterface/Types.h" line_range="72" />
<code_context>
+ struct CloudConfig
+ {
+ std::string provider; // provider key, e.g. "geforce_now"
+ std::string game_title; // regex-safe game fragment substituted into the provider title template
+
+ MEO_JSONIZATION(provider, MEO_OPT game_title);
</code_context>
<issue_to_address>
**issue (bug_risk):** Cloud game_title is treated as a raw regex fragment, which can easily break matching if the title contains regex metacharacters.
This relies on callers passing a regex-safe `game_title`, but many will likely provide a plain title (e.g. `"Arknights: Endfield (Global)"`), causing `regex_valid` to fail and `select_cloud_hwnd` to abort. Either escape `game_title` before inserting it into `title_template`, or explicitly require and validate regex-safe input and surface a clear configuration error when it is not.
</issue_to_address>
### Comment 2
<location path="source/MaaPiCli/CLI/interactor.cpp" line_range="339-340" />
<code_context>
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) {
std::cout << MAA_NS::utf8_to_crt(std::format("\t\t{}\n", format_win32_config(config_.configuration().win32)));
}
</code_context>
<issue_to_address>
**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:
```cpp
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.
</issue_to_address>
### Comment 3
<location path="source/MaaPiCli/CLI/interactor.cpp" line_range="1786" />
<code_context>
+ 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);
+ return false;
+ }
</code_context>
<issue_to_address>
**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.
```suggestion
LogError << "Controller not found" << VAR(name);
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| struct CloudConfig | ||
| { | ||
| std::string provider; // provider key, e.g. "geforce_now" | ||
| std::string game_title; // regex-safe game fragment substituted into the provider title template |
There was a problem hiding this comment.
issue (bug_risk): Cloud 的 game_title 被当作原始的正则片段处理,如果标题包含正则元字符,很容易导致匹配失败。
当前做法依赖调用方传入已做正则转义的 game_title,但很多调用方很可能会直接提供普通标题(例如 "Arknights: Endfield (Global)"),从而导致 regex_valid 校验失败,并使 select_cloud_hwnd 中止。建议要么在将 game_title 插入 title_template 前对其进行转义,要么明确要求并校验调用方提供正则安全的输入,并在不满足要求时给出清晰的配置错误提示。
Original comment in English
issue (bug_risk): Cloud game_title is treated as a raw regex fragment, which can easily break matching if the title contains regex metacharacters.
This relies on callers passing a regex-safe game_title, but many will likely provide a plain title (e.g. "Arknights: Endfield (Global)"), causing regex_valid to fail and select_cloud_hwnd to abort. Either escape game_title before inserting it into title_template, or explicitly require and validate regex-safe input and surface a clear configuration error when it is not.
| case InterfaceData::Controller::Type::Cloud: | ||
| if (config_.configuration().win32.hwnd) { |
There was a problem hiding this comment.
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;
}
- 实现一个
format_cloud_config(const decltype(config_.configuration().cloud)& cloud)辅助函数(或等价实现),返回包含 provider 名称、游戏标题以及其他 Cloud 相关元数据的人类可读std::string。将其与format_win32_config放在一起,以保持格式化逻辑一致。 - 如果实际配置类型/结构不同(例如嵌套在其他结构体中或成员名不同),请相应调整在
format_cloud_config中访问字段的方式(config_.configuration().cloud)。 - 如果
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;
}
- Implement a
format_cloud_config(const decltype(config_.configuration().cloud)& cloud)helper (or equivalent) that returns a human-readablestd::stringincluding provider name, game title, and any other relevant Cloud metadata. Place it alongsideformat_win32_configto keep formatting logic consistent. - 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). - If
select_cloud_hwnddepends on or populates additional Cloud-related fields, consider extendingformat_cloud_configto optionally include those fields when they are available.
| 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); |
There was a problem hiding this comment.
nitpick (typo): 缺少控制器时的错误日志字符串中有拼写错误。
日志消息中 "Controller" 拼写不正确。请修正以便日志可被正确检索,并避免调试时造成困扰。
| 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.
| LogError << "Contorller not found" << VAR(name); | |
| LogError << "Controller not found" << VAR(name); |
|
MaaFW can handle cloud-playing well only if:
|
What
Adds a first-class Cloud controller type to the ProjectInterface, so cloud-streaming targets can be declared with just a provider + game title instead of hand-writing the full Win32 window signature.
{ "name": "GFN-App", "type": "Cloud", "cloud": { "provider": "geforce_now", "game_title": "Endfield" } }How
source/include/ProjectInterface/CloudProviders.h(new): built-in provider registry. ACloudProvidercarriesprocess_regex+class_regex+title_template(with a{game}placeholder) +screencap+input. Ships one provider,geforce_now(GeForce NOW native CEF client): processGeForceNOW.exe, classCEFCLIENT, title{game}.*on GeForce NOW,PrintWindow+Seize.MaaPiCliConfigurator / interactor:Clouddesugars to a Win32 controller. Window resolution matches the provider's process + class + title (process filter applied only when the process path is queryable, falling back to class + title), then stores the resolved HWND in the shared win32 slot.MaaToolkitdesktop window finder: now also captures the owning process image path, used to disambiguate the genericCEFCLIENTclass by process.sample/: adds the GeForce NOW (Cloud) controller to the sample project.Adding a new provider is a single entry in the registry.
Testing
Verified end-to-end on Windows against a real GeForce NOW session running Arknights: Endfield — the Cloud controller resolves the GFN client window and connects, driving it via
PrintWindow+Seize. Also exercised through the MXU GUI (companion PR below).Related
🤖 Generated with Claude Code