feat(controller): first-class Cloud controller preset (GeForce NOW) - #1
feat(controller): first-class Cloud controller preset (GeForce NOW)#1zeejaytan 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>
PR Summary by Qodofeat(controller): add first-class Cloud controller preset (GeForce NOW)
AI Description
Diagram
High-Level Assessment
Files changed (13)
|
Code Review by Qodo
1. Cloud config not required
|
| case InterfaceData::Controller::Type::Cloud: | ||
| config_.configuration().controller.type = InterfaceData::Controller::Type::Cloud; | ||
| select_cloud_hwnd(controller.cloud); | ||
| break; |
There was a problem hiding this comment.
1. Cloud not windows-gated 🐞 Bug ≡ Correctness
Interactor::select_controller and check_validity allow Controller::Type::Cloud on non-Windows platforms even though Cloud is documented as Windows-only, leading to a failing/unclear configuration flow. Add a Windows-only guard (like Gamepad/PlayCover/WlRoots) before attempting Cloud HWND resolution.
Agent Prompt
### Issue description
`Cloud` is documented as Windows-only, but the CLI allows selecting and validating it on non-Windows builds and immediately runs the Cloud selection path.
### Issue Context
Other platform-specific controllers already have guards and user-facing messages (e.g., Gamepad on non-Windows). Cloud should follow the same pattern in both the initial selection flow and the later `check_validity()` path.
### Fix Focus Areas
- source/MaaPiCli/CLI/interactor.cpp[28-44]
- source/MaaPiCli/CLI/interactor.cpp[590-703]
- source/MaaPiCli/CLI/interactor.cpp[1763-1791]
### Suggested fix
- Introduce `static constexpr bool kCloudSupported = true/false` (based on `_WIN32`).
- When listing controllers, annotate Cloud with "(Windows only)" when unsupported.
- In the `Type::Cloud` switch case, if `!kCloudSupported`, print an explanatory message and re-prompt like the Gamepad/PlayCover/WlRoots flows.
- In `check_validity()`, return a clear error on non-Windows if controller type is Cloud.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| CloudConfig cloud; | ||
|
|
||
| MEO_JSONIZATION( | ||
| name, |
There was a problem hiding this comment.
2. Cloud config not required 🐞 Bug ≡ Correctness
A controller can be declared with type==Cloud while omitting the cloud object (and thus provider), because the schema and InterfaceData JSONization don’t conditionally require it; runtime then fails with "Unknown cloud provider". This should be rejected during schema validation and/or Parser::parse_interface semantic validation.
Agent Prompt
### Issue description
`type: "Cloud"` is allowed without a `cloud` block, producing an empty `controller.cloud.provider` and a later runtime failure.
### Issue Context
- `InterfaceData::Controller` makes `cloud` optional (`MEO_OPT cloud`).
- The JSON schema only requires `name` and `type` for controller items, and does not add an `if/then` to require `cloud` when `type == Cloud`.
- Runtime generation hard-fails when `find_cloud_provider(controller.cloud.provider)` returns null.
### Fix Focus Areas
- source/include/ProjectInterface/Types.h[66-131]
- tools/interface.schema.json[466-753]
- source/MaaPiCli/Impl/Parser.cpp[79-86]
- source/MaaPiCli/Impl/Configurator.cpp[245-257]
### Suggested fix
1) Schema:
- Add an `allOf` with `if: { properties: { type: { const: "Cloud" } } }` and `then: { required: ["cloud"], properties: { cloud: { required: ["provider"] } } }`.
2) Parser semantic validation:
- In `Parser::parse_interface`, extend the controller-type validation loop: when `ctrl.type == Cloud`, enforce `!ctrl.cloud.provider.empty()` and (optionally) `find_cloud_provider(ctrl.cloud.provider) != nullptr` to fail early with a clear error.
This keeps `cloud` optional for non-Cloud controller types while making Cloud configs structurally valid.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Full image path of the owning process, e.g. "C:\...\GeForceNOW.exe". | ||
| // Used by cloud/GFN controller matching to disambiguate a generic window | ||
| // class (CEFCLIENT) by process. Empty if the process cannot be queried. | ||
| std::wstring process_path; | ||
| DWORD process_id = 0; | ||
| GetWindowThreadProcessId(hwnd, &process_id); | ||
| if (process_id != 0) { | ||
| HANDLE process_handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, process_id); | ||
| if (process_handle) { | ||
| std::wstring buffer(1024, L'\0'); | ||
| DWORD buffer_size = static_cast<DWORD>(buffer.size()); | ||
| if (QueryFullProcessImageNameW(process_handle, 0, buffer.data(), &buffer_size)) { | ||
| buffer.resize(buffer_size); | ||
| process_path = std::move(buffer); | ||
| } | ||
| CloseHandle(process_handle); | ||
| } | ||
| } |
There was a problem hiding this comment.
3. Win32 process path truncation 🐞 Bug ☼ Reliability
DesktopWindowWin32Finder uses a fixed 1024-widechar buffer for QueryFullProcessImageNameW and doesn’t retry on insufficient buffer, which can leave process_path empty. Cloud HWND matching explicitly treats empty process_path as “process ok” and bypasses the process filter, reducing the robustness this feature is meant to provide.
Agent Prompt
### Issue description
Process image path capture may silently fail for long paths because the code uses a fixed-size buffer and does not handle `ERROR_INSUFFICIENT_BUFFER` by resizing/retrying.
### Issue Context
Cloud window matching uses process path as an additional disambiguator, but falls back to class+title when `process_path` is empty.
### Fix Focus Areas
- source/MaaToolkit/DesktopWindow/DesktopWindowWin32Finder.cpp[28-45]
- source/MaaPiCli/CLI/interactor.cpp[920-937]
### Suggested fix
- Replace the single-shot `std::wstring buffer(1024, L'\0')` call with a small retry loop:
- Start with a reasonable size (e.g., `MAX_PATH` or 1024).
- Call `QueryFullProcessImageNameW`.
- If it fails with `GetLastError()==ERROR_INSUFFICIENT_BUFFER`, resize using the returned `buffer_size` (or grow exponentially) and retry.
- Keep current behavior for other failures (leave empty), but ensure “buffer too small” isn’t treated as a normal failure case.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
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>
Summary
Adds a first-class
Cloudcontroller type so cloud-streaming clients (GeForce NOW) can be connected with minimal config, instead of every downstream project hand-rolling a brittle generic-Win32 match. Cloud desugars to a Win32 controller — no new ControlUnit, no new create API.Problem
Today a GFN session is connectable only as a generic Win32 window, hand-configured per project:
CEFCLIENTis a generic CEF class (any Electron/CEF app has it); the only GFN signal is a title substring; every project re-derives the same PrintWindow/Seize knowledge. Zero GeForce NOW awareness existed in the framework.Approach
Declarative preset:
{ "name": "GFN", "type": "Cloud", "cloud": { "provider": "geforce_now", "game_title": "Endfield" } }DesktopWindowWin32FinderviaQueryFullProcessImageNameW) +MaaToolkitDesktopWindowGetProcessPath, so a cloud window is disambiguated by process (GeForceNOW.exe), not just the generic CEF class.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 entrygeforce_now:GeForceNOW.exe/CEFCLIENT/{game}.*on GeForce NOW/PrintWindow/Seize.select_cloud_hwndresolves the window by process + class + composed title and stores the HWND in the shared win32 slot;Configuratordesugars Cloud →Win32Param.Cloudtype andgeforce_nowprovider.Reuse (not reinvented)
Signatures copied verbatim from the shipping MaaEnd and MaaNTE GFN configs. The title template reproduces both
Endfield.*on GeForce NOWandNTE.*on GeForce NOW. Adding another provider (Boosteroid, Xbox Cloud) is a single registry entry.Scope
Native GFN app only (Chrome web variant intentionally excluded). Window resizing / resolution enforcement is out of scope — that stays downstream policy.
Testing
🤖 Generated with Claude Code