Skip to content

feat(controller): first-class Cloud controller preset (GeForce NOW) - #1400

Open
zeejaytan wants to merge 3 commits into
MaaXYZ:mainfrom
zeejaytan:feat/cloud-controller-gfn
Open

feat(controller): first-class Cloud controller preset (GeForce NOW)#1400
zeejaytan wants to merge 3 commits into
MaaXYZ:mainfrom
zeejaytan:feat/cloud-controller-gfn

Conversation

@zeejaytan

@zeejaytan zeejaytan commented Jul 15, 2026

Copy link
Copy Markdown

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. A CloudProvider carries process_regex + class_regex + title_template (with a {game} placeholder) + screencap + input. Ships one provider, geforce_now (GeForce NOW native CEF client): process GeForceNOW.exe, class CEFCLIENT, title {game}.*on GeForce NOW, PrintWindow + Seize.
  • MaaPiCli Configurator / interactor: Cloud desugars 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.
  • MaaToolkit desktop window finder: now also captures the owning process image path, used to disambiguate the generic CEFCLIENT class 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

Zeejay Tan and others added 3 commits July 14, 2026 15:04
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>

@sourcery-ai sourcery-ai Bot left a comment

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.

Hey - 我发现了 3 个问题,并给出了一些整体性的反馈:

  • Interactor::check_validity 中,日志信息 "Contorller not found" 有拼写错误,应更正为 "Controller not found",以便提供更清晰的诊断信息。
  • 解析 Cloud provider 的逻辑(包括 find_cloud_provider 以及对未知 provider 的错误处理)在 Interactor::select_cloud_hwndConfigurator::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>

Sourcery 对开源项目是免费的——如果你觉得我们的评审有帮助,欢迎分享 ✨
帮我变得更有用!请对每条评论点 👍 或 👎,我会基于这些反馈改进后续的评审。
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 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
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

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.

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.

Comment on lines +339 to 340
case InterfaceData::Controller::Type::Cloud:
if (config_.configuration().win32.hwnd) {

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.

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);

@isHarryh

Copy link
Copy Markdown

MaaFW can handle cloud-playing well only if:

  • The streaming uses 4:4:4 true-color space.
  • The streaming bit-rate is high enough (at least 3000kbps as estimated by practices)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants