diff --git a/agent/go-service/pkg/control/utils.go b/agent/go-service/pkg/control/utils.go index 585d533a83..b61f1febe7 100644 --- a/agent/go-service/pkg/control/utils.go +++ b/agent/go-service/pkg/control/utils.go @@ -67,6 +67,35 @@ func GetControlType(ctrl *maa.Controller) (string, error) { return "", fmt.Errorf("unsupported controller type: %s", info.Type) } +// GetWin32Hwnd retrieves the native window handle (HWND) of a Win32 controller +// by parsing its info string. It fails for non-Win32 controllers or when the +// info string carries no hwnd. +func GetWin32Hwnd(ctrl *maa.Controller) (uintptr, error) { + if ctrl == nil { + return 0, fmt.Errorf("nil controller") + } + + infoStr, err := ctrl.GetInfo() + if err != nil { + return 0, fmt.Errorf("failed to get controller info: %w", err) + } + if strings.TrimSpace(infoStr) == "" { + return 0, fmt.Errorf("empty controller info") + } + + var info maaControllerInfoDto + if err := json.Unmarshal([]byte(infoStr), &info); err != nil { + return 0, fmt.Errorf("failed to parse controller info: %w", err) + } + if info.Type != "" && !strings.EqualFold(info.Type, CONTROL_TYPE_WIN32) { + return 0, fmt.Errorf("controller type is %q, not win32", info.Type) + } + if info.HWnd == 0 { + return 0, fmt.Errorf("controller info has no hwnd") + } + return uintptr(info.HWnd), nil +} + /* ******** Screen Diagonal Size ******** */ // GetScreenDiagonalSize calculates the diagonal size of the screen based on the controller's raw resolution, diff --git a/agent/go-service/register.go b/agent/go-service/register.go index 38629061a1..43b58e413c 100644 --- a/agent/go-service/register.go +++ b/agent/go-service/register.go @@ -48,6 +48,9 @@ func registerAll() { resource.EnsureResourcePathSink() // Pre-Check Custom + // GFN-App is a Win32 controller: aspectratio enforces the shared 16:9 + + // >=1280x720 contract for it like any other Win32 controller, so no + // GFN-specific forced resize is needed. aspectratio.Register() hdrcheck.Register() processcheck.Register() diff --git a/agent/go-service/taskersink/aspectratio/altenter_windows.go b/agent/go-service/taskersink/aspectratio/altenter_windows.go index 2670cfd1bd..aaa2c2fed6 100644 --- a/agent/go-service/taskersink/aspectratio/altenter_windows.go +++ b/agent/go-service/taskersink/aspectratio/altenter_windows.go @@ -3,9 +3,7 @@ package aspectratio import ( - "encoding/json" "fmt" - "strings" "time" "unsafe" @@ -47,11 +45,6 @@ var ( win32ProcGetCurrentThreadID = win32Kernel32.NewProc("GetCurrentThreadId") ) -type win32ControllerInfo struct { - Type string `json:"type"` - HWnd uint64 `json:"hwnd"` -} - type win32Rect struct { Left int32 Top int32 @@ -64,7 +57,7 @@ func init() { } func sendAltEnterWin32(controller *maa.Controller) (resolutionReader, error) { - hwnd, err := controllerHwndWin32(controller) + hwnd, err := control.GetWin32Hwnd(controller) if err != nil { return nil, err } @@ -260,29 +253,3 @@ func setDPIAwareWin32() func() { } } } - -func controllerHwndWin32(controller *maa.Controller) (uintptr, error) { - if controller == nil { - return 0, fmt.Errorf("nil controller") - } - - infoStr, err := controller.GetInfo() - if err != nil { - return 0, fmt.Errorf("failed to get controller info: %w", err) - } - if strings.TrimSpace(infoStr) == "" { - return 0, fmt.Errorf("empty controller info") - } - - var info win32ControllerInfo - if err := json.Unmarshal([]byte(infoStr), &info); err != nil { - return 0, fmt.Errorf("failed to parse controller info: %w", err) - } - if info.Type != "" && !strings.EqualFold(info.Type, control.CONTROL_TYPE_WIN32) { - return 0, fmt.Errorf("controller type is %q, not win32", info.Type) - } - if info.HWnd == 0 { - return 0, fmt.Errorf("controller info has no hwnd") - } - return uintptr(info.HWnd), nil -} diff --git a/assets/interface.json b/assets/interface.json index 0d8956f9cd..09a51398c9 100644 --- a/assets/interface.json +++ b/assets/interface.json @@ -47,6 +47,23 @@ // }, // "permission_required": true // }, + // GFN-App: GeForce NOW 原生客户端(CEF 流窗口),仅前台模式。 + // 窗口标题已实测确认为 "Arknights: Endfield on GeForce NOW", + // 正则取语言无关片段,见 docs/zh_cn/developers/gfn-app-support-prd.md + // resource_gfn 覆盖串流压缩下失配的精确 ColorMatch 节点(PRD 风险 R3) + { + "name": "GFN-App", + "label": "$controller.GFN-App.label", + "description": "$controller.GFN-App.description", + "type": "Cloud", + "cloud": { + "provider": "geforce_now", + "game_title": "Endfield" + }, + "attach_resource_path": [ + "./resource_gfn" + ] + }, { "name": "ADB", "label": "$controller.ADB.label", diff --git a/assets/locales/interface/en_us.json b/assets/locales/interface/en_us.json index abd63130aa..363ee65440 100644 --- a/assets/locales/interface/en_us.json +++ b/assets/locales/interface/en_us.json @@ -896,6 +896,8 @@ "controller.Win32-Front.description": "Best stability. Requires the game window to remain in the foreground and unobstructed; will fully grab the mouse.", "controller.Win32-Window-Background.label": "PC-Background (Deprecated - incompatible with new version)", "controller.Win32-Window-Background.description": "Due to game updates, it can no longer be adapted. The background controller has been deprecated and will be removed in a future version.", + "controller.GFN-App.label": "Cloud-GeForce NOW", + "controller.GFN-App.description": "Play via the GeForce NOW native client stream. Foreground mode only; will fully grab the mouse. Recommended: lock the streaming resolution to 1280x720 in GFN settings.", "controller.ADB.label": "Android", "controller.ADB.description": "Supports Android emulators and physical Android devices.", "controller.Wlroots.label": "Linux-Wlroots", diff --git a/assets/locales/interface/ja_jp.json b/assets/locales/interface/ja_jp.json index 6f18d19ee4..24f1906da4 100644 --- a/assets/locales/interface/ja_jp.json +++ b/assets/locales/interface/ja_jp.json @@ -896,6 +896,8 @@ "controller.Win32-Front.description": "安定性が最も高い。ゲームウィンドウを最前面に表示し、遮られないようにする必要があります。マウスを完全に占有します。", "controller.Win32-Window-Background.label": "PC-バックグラウンド(非推奨 - 新バージョンに対応不可)", "controller.Win32-Window-Background.description": "ゲームの更新により適応を継続できなくなりました。バックグラウンドコントローラーは非推奨となり、今後のバージョンで削除される予定です。", + "controller.GFN-App.label": "クラウド-GeForce NOW", + "controller.GFN-App.description": "GeForce NOW ネイティブクライアントのストリーミングでプレイします。フォアグラウンドモードのみ対応で、マウスを完全に占有します。GFN の設定でストリーミング解像度を 1280x720 に固定することを推奨します。", "controller.ADB.label": "Android", "controller.ADB.description": "Androidエミュレーターおよび実機Androidデバイスに対応しています。", "controller.Wlroots.label": "Linux-Wlroots", diff --git a/assets/locales/interface/ko_kr.json b/assets/locales/interface/ko_kr.json index 495815326b..82df98dea2 100644 --- a/assets/locales/interface/ko_kr.json +++ b/assets/locales/interface/ko_kr.json @@ -896,6 +896,8 @@ "controller.Win32-Front.description": "안정성이 가장 뛰어납니다. 게임 창이 항상 최앞단에 있고 가려지지 않아야 하며, 마우스를 완전히 점유합니다.", "controller.Win32-Window-Background.label": "PC-백그라운드 (사용 중단 - 새 버전 호환 불가)", "controller.Win32-Window-Background.description": "게임 업데이트로 인해 더 이상 호환할 수 없습니다. 백그라운드 컨트롤러는 사용 중단되었으며 향후 버전에서 제거될 예정입니다.", + "controller.GFN-App.label": "클라우드-GeForce NOW", + "controller.GFN-App.description": "GeForce NOW 네이티브 클라이언트 스트리밍으로 플레이합니다. 포그라운드 모드만 지원하며 마우스를 완전히 점유합니다. GFN 설정에서 스트리밍 해상도를 1280x720으로 고정하는 것을 권장합니다.", "controller.ADB.label": "Android", "controller.ADB.description": "안드로이드 에뮬레이터 및 실물 안드로이드 기기를 지원합니다.", "controller.Wlroots.label": "Linux-Wlroots", diff --git a/assets/locales/interface/zh_cn.json b/assets/locales/interface/zh_cn.json index 676c862ae1..734cb6eabd 100644 --- a/assets/locales/interface/zh_cn.json +++ b/assets/locales/interface/zh_cn.json @@ -896,6 +896,8 @@ "controller.Win32-Front.description": "稳定性最好,需要游戏窗口保持在最前且不被遮挡,会完全抢占鼠标", "controller.Win32-Window-Background.label": "电脑端-后台弃用(无法适配新版本)", "controller.Win32-Window-Background.description": "因游戏更新导致无法继续适配,后台控制器已弃用,并将在后续版本中移除。", + "controller.GFN-App.label": "云游戏-GeForce NOW", + "controller.GFN-App.description": "通过 GeForce NOW 原生客户端串流游玩,仅支持前台模式,会完全抢占鼠标;建议在 GFN 设置中将串流分辨率固定为 1280x720", "controller.ADB.label": "安卓端", "controller.ADB.description": "支持安卓模拟器和实体安卓设备", "controller.Wlroots.label": "Linux端-Wlroots", diff --git a/assets/locales/interface/zh_tw.json b/assets/locales/interface/zh_tw.json index d6244c8112..03f0f79139 100644 --- a/assets/locales/interface/zh_tw.json +++ b/assets/locales/interface/zh_tw.json @@ -688,6 +688,8 @@ "controller.Win32-Front.description": "穩定性最好,需要遊戲視窗保持在最前且不被遮擋,會完全搶占滑鼠", "controller.Win32-Window-Background.label": "電腦端-後台棄用(無法適配新版本)", "controller.Win32-Window-Background.description": "因遊戲更新導致無法繼續適配,後台控制器已棄用,並將在後續版本中移除。", + "controller.GFN-App.label": "雲遊戲-GeForce NOW", + "controller.GFN-App.description": "透過 GeForce NOW 原生客戶端串流遊玩,僅支援前台模式,會完全搶占滑鼠;建議在 GFN 設定中將串流解析度固定為 1280x720", "controller.ADB.label": "安卓端", "controller.ADB.description": "支援安卓模擬器及實體安卓裝置", "controller.Wlroots.label": "Linux端-Wlroots", diff --git a/assets/resource_gfn/pipeline/Common/Button/ButtonColor.json b/assets/resource_gfn/pipeline/Common/Button/ButtonColor.json new file mode 100644 index 0000000000..bc25116bf7 --- /dev/null +++ b/assets/resource_gfn/pipeline/Common/Button/ButtonColor.json @@ -0,0 +1,116 @@ +// GFN 云游戏专用覆盖:按钮颜色基础节点。 +// GeForce NOW 串流帧经过 H.264/HEVC 编码(4:2:0 色度抽样 + 量化), +// 颜色相对本地渲染会偏移数个单位,基础资源中 1~2 个单位宽的精确 +// ColorMatch 区间在串流帧上系统性失配(实测品牌黄 hue 由本地 28-29 +// 扩散为 26-31,见 docs/zh_cn/developers/gfn-app-support-prd.md 风险 R3)。 +// 本文件在保持节点结构不变的前提下放宽颜色容差,仅随 GFN-App 控制器加载。 +{ + "YellowButtonBackground": { + "desc": "黄色按钮背景色(GFN 串流容差版),用作黄色按钮组合识别的背景框", + "recognition": { + "type": "ColorMatch", + "param": { + "method": 40, + "lower": [ + 24, + 90, + 90 + ], + "upper": [ + 33, + 255, + 255 + ], + "connected": true, + "count": 3000 + } + }, + "pre_delay": 0, + "post_delay": 0, + "rate_limit": 0 + }, + "WhiteButtonBackground": { + "desc": "白色按钮背景色(GFN 串流容差版),用作白色按钮组合识别的背景框", + "recognition": { + "type": "ColorMatch", + "param": { + "lower": [ + [ + 246, + 244, + 244 + ], + [ + 230, + 228, + 228 + ], + [ + 145, + 144, + 144 + ] + ], + "upper": [ + [ + 255, + 255, + 255 + ], + [ + 247, + 245, + 245 + ], + [ + 175, + 173, + 173 + ] + ], + "connected": true, + "count": 100 + } + }, + "pre_delay": 0, + "post_delay": 0, + "rate_limit": 0 + }, + "GrayButtonBackground": { + "desc": "灰色按钮背景色(GFN 串流容差版),用作灰色按钮组合识别的背景框", + "recognition": { + "type": "ColorMatch", + "param": { + "lower": [ + [ + 63, + 62, + 62 + ], + [ + 77, + 76, + 76 + ] + ], + "upper": [ + [ + 76, + 75, + 75 + ], + [ + 90, + 89, + 89 + ] + ], + "connected": true, + "count": 100 + } + }, + "pre_delay": 0, + "post_delay": 0, + "rate_limit": 0 + } +} diff --git a/assets/resource_gfn/pipeline/SceneManager/SceneLoading.json b/assets/resource_gfn/pipeline/SceneManager/SceneLoading.json new file mode 100644 index 0000000000..e1c911a63e --- /dev/null +++ b/assets/resource_gfn/pipeline/SceneManager/SceneLoading.json @@ -0,0 +1,92 @@ +// GFN 云游戏专用覆盖:加载界面颜色准确性检查。 +// 基础资源要求校准色块 hue 精确单值且饱和度恒为 255,GeForce NOW 串流帧 +// 经视频编码后饱和度与色相均有数个单位的偏移,精确匹配必然失败并触发 +// "颜色识别失败" 误报(用户并未开启滤镜/HDR)。 +// 本文件仅放宽 Success 判定节点的颜色容差(hue ±4、饱和度下限 200), +// 失败分支与流程结构由基础资源原样提供,仅随 GFN-App 控制器加载。 +{ + "__SceneCheckYellowColorSuccess": { + "desc": "检查颜色准确性:黄色 成功(GFN 串流容差版)", + "recognition": { + "type": "ColorMatch", + "param": { + "roi": [ + 0, + 480, + 65, + 85 + ], + "method": 40, + "lower": [ + 26, + 200, + 1 + ], + "upper": [ + 34, + 255, + 255 + ] + } + }, + "pre_delay": 0, + "post_delay": 0, + "rate_limit": 0 + }, + "__SceneCheckTealColorSuccess": { + "desc": "检查颜色准确性:青色 成功(GFN 串流容差版)", + "recognition": { + "type": "ColorMatch", + "param": { + "roi": [ + 0, + 518, + 65, + 85 + ], + "method": 40, + "lower": [ + 86, + 200, + 1 + ], + "upper": [ + 94, + 255, + 255 + ] + } + }, + "pre_delay": 0, + "post_delay": 0, + "rate_limit": 0 + }, + "__SceneCheckPurpleColorSuccess": { + "desc": "检查颜色准确性:紫色 成功(GFN 串流容差版)", + "recognition": { + "type": "ColorMatch", + "param": { + "roi": [ + 0, + 553, + 65, + 85 + ], + "method": 40, + "lower": [ + 146, + 200, + 1 + ], + "upper": [ + 154, + 255, + 255 + ] + } + }, + "pre_delay": 0, + "post_delay": 0, + "rate_limit": 0 + } +} diff --git a/assets/tasks/AutoCollect.json b/assets/tasks/AutoCollect.json index 89238256e0..6e22667bd8 100644 --- a/assets/tasks/AutoCollect.json +++ b/assets/tasks/AutoCollect.json @@ -9,6 +9,7 @@ "ADB", "PlayCover", "Win32-Front", + "GFN-App", "Wlroots" ], "option": [ diff --git a/assets/tasks/AutoEcoFarm.json b/assets/tasks/AutoEcoFarm.json index 48b1d6337d..c59bae129a 100644 --- a/assets/tasks/AutoEcoFarm.json +++ b/assets/tasks/AutoEcoFarm.json @@ -7,6 +7,7 @@ "description": "$task.AutoEcoFarm.description", "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "option": [ diff --git a/assets/tasks/AutoEssence.json b/assets/tasks/AutoEssence.json index 436703a4f6..f325fd8764 100644 --- a/assets/tasks/AutoEssence.json +++ b/assets/tasks/AutoEssence.json @@ -9,6 +9,7 @@ "ADB", "PlayCover", "Win32-Front", + "GFN-App", "Wlroots" ], "option": [ diff --git a/assets/tasks/AutoSell.json b/assets/tasks/AutoSell.json index 41ed73466c..54d11777aa 100644 --- a/assets/tasks/AutoSell.json +++ b/assets/tasks/AutoSell.json @@ -6,6 +6,7 @@ "entry": "AutoSellMain", "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "option": [ diff --git a/assets/tasks/AutoStockStaple.json b/assets/tasks/AutoStockStaple.json index f2038900d8..0b84c987d4 100644 --- a/assets/tasks/AutoStockStaple.json +++ b/assets/tasks/AutoStockStaple.json @@ -7,6 +7,7 @@ "description": "$task.AutoStockStaple.description", "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" diff --git a/assets/tasks/AutoStockpile.json b/assets/tasks/AutoStockpile.json index 036aa46b35..062e186a30 100644 --- a/assets/tasks/AutoStockpile.json +++ b/assets/tasks/AutoStockpile.json @@ -7,6 +7,7 @@ "description": "$task.AutoStockpile.description", "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "group": [ diff --git a/assets/tasks/AutoUseSpMedication.json b/assets/tasks/AutoUseSpMedication.json index 560ef89d5b..a116782076 100644 --- a/assets/tasks/AutoUseSpMedication.json +++ b/assets/tasks/AutoUseSpMedication.json @@ -7,6 +7,7 @@ "name": "AutoUseSpMedication", "controller": [ "Win32-Front", + "GFN-App", "Wlroots", "ADB" ], diff --git a/assets/tasks/BakerEntry.json b/assets/tasks/BakerEntry.json index 26a7913108..dedc72ffdc 100644 --- a/assets/tasks/BakerEntry.json +++ b/assets/tasks/BakerEntry.json @@ -7,6 +7,7 @@ "description": "$task.BakerEntry.description", "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" diff --git a/assets/tasks/BatchAddFriends.json b/assets/tasks/BatchAddFriends.json index e3ea744bec..c8c6895aaf 100644 --- a/assets/tasks/BatchAddFriends.json +++ b/assets/tasks/BatchAddFriends.json @@ -10,6 +10,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "group": [ diff --git a/assets/tasks/BatchUseDetector.json b/assets/tasks/BatchUseDetector.json index 7037a68a0d..7676110d41 100644 --- a/assets/tasks/BatchUseDetector.json +++ b/assets/tasks/BatchUseDetector.json @@ -12,6 +12,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "group": [ diff --git a/assets/tasks/ClaimSimulationRewards.json b/assets/tasks/ClaimSimulationRewards.json index d67b3a92f0..b9fc11504f 100644 --- a/assets/tasks/ClaimSimulationRewards.json +++ b/assets/tasks/ClaimSimulationRewards.json @@ -7,6 +7,7 @@ "entry": "SimSpaceEntry", "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "option": [ diff --git a/assets/tasks/Crafting.json b/assets/tasks/Crafting.json index 1caf9f8982..4b5aa8b641 100644 --- a/assets/tasks/Crafting.json +++ b/assets/tasks/Crafting.json @@ -7,6 +7,7 @@ "description": "$task.Crafting.description", "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" diff --git a/assets/tasks/CreditShopping.json b/assets/tasks/CreditShopping.json index 28a1f0cd3a..8008f51de7 100644 --- a/assets/tasks/CreditShopping.json +++ b/assets/tasks/CreditShopping.json @@ -17,6 +17,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" diff --git a/assets/tasks/DailyRewards.json b/assets/tasks/DailyRewards.json index 8445ddaadb..053a6691a7 100644 --- a/assets/tasks/DailyRewards.json +++ b/assets/tasks/DailyRewards.json @@ -14,6 +14,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" @@ -47,6 +48,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ] }, @@ -73,6 +75,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ] }, @@ -105,6 +108,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ] }, @@ -153,6 +157,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ] }, diff --git a/assets/tasks/DeliveryJobs.json b/assets/tasks/DeliveryJobs.json index 9d9724e92b..43ff5e3e9d 100644 --- a/assets/tasks/DeliveryJobs.json +++ b/assets/tasks/DeliveryJobs.json @@ -14,6 +14,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" diff --git a/assets/tasks/DijiangRewards.json b/assets/tasks/DijiangRewards.json index ef5dee612f..62234b468c 100644 --- a/assets/tasks/DijiangRewards.json +++ b/assets/tasks/DijiangRewards.json @@ -13,6 +13,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" diff --git a/assets/tasks/EnvironmentMonitoring.json b/assets/tasks/EnvironmentMonitoring.json index d0a311261b..9118de0cc8 100644 --- a/assets/tasks/EnvironmentMonitoring.json +++ b/assets/tasks/EnvironmentMonitoring.json @@ -7,6 +7,7 @@ "description": "$task.EnvironmentMonitoring.description", "controller": [ "Win32-Front", + "GFN-App", "Wlroots", "ADB", "PlayCover" diff --git a/assets/tasks/EssenceFilter.json b/assets/tasks/EssenceFilter.json index c167984c9f..8812a91f53 100644 --- a/assets/tasks/EssenceFilter.json +++ b/assets/tasks/EssenceFilter.json @@ -15,6 +15,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots", "ADB", "PlayCover" diff --git a/assets/tasks/GearAssembly.json b/assets/tasks/GearAssembly.json index 89a3a51417..f811766c70 100644 --- a/assets/tasks/GearAssembly.json +++ b/assets/tasks/GearAssembly.json @@ -11,6 +11,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" diff --git a/assets/tasks/GiftOperator.json b/assets/tasks/GiftOperator.json index ab0b221233..d452353f3e 100644 --- a/assets/tasks/GiftOperator.json +++ b/assets/tasks/GiftOperator.json @@ -14,6 +14,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots", "ADB", "PlayCover" diff --git a/assets/tasks/ImportBluePrints.json b/assets/tasks/ImportBluePrints.json index 47a31561b7..3439eb967a 100644 --- a/assets/tasks/ImportBluePrints.json +++ b/assets/tasks/ImportBluePrints.json @@ -10,6 +10,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "group": [ diff --git a/assets/tasks/ItemTransfer.json b/assets/tasks/ItemTransfer.json index f970be7287..3d0b96c775 100644 --- a/assets/tasks/ItemTransfer.json +++ b/assets/tasks/ItemTransfer.json @@ -16,6 +16,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "group": [ @@ -29,7 +30,8 @@ "label": "$option.ItemTransferAutoTeleport.label", "description": "$option.ItemTransferAutoTeleport.description", "controller": [ - "Win32-Front" + "Win32-Front", + "GFN-App" ], "cases": [ { diff --git a/assets/tasks/ProtocolSpace.json b/assets/tasks/ProtocolSpace.json index c117974fd8..df2fbdec83 100644 --- a/assets/tasks/ProtocolSpace.json +++ b/assets/tasks/ProtocolSpace.json @@ -16,6 +16,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "group": [ diff --git a/assets/tasks/PullCountCalculator.json b/assets/tasks/PullCountCalculator.json index f954407ad7..492b2a254e 100644 --- a/assets/tasks/PullCountCalculator.json +++ b/assets/tasks/PullCountCalculator.json @@ -9,6 +9,7 @@ "ADB", "PlayCover", "Win32-Front", + "GFN-App", "Wlroots" ], "group": [ diff --git a/assets/tasks/PuzzleSolver.json b/assets/tasks/PuzzleSolver.json index 2fdc6182fa..9a69e6297c 100644 --- a/assets/tasks/PuzzleSolver.json +++ b/assets/tasks/PuzzleSolver.json @@ -11,6 +11,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "group": [ diff --git a/assets/tasks/ReadAllWiki.json b/assets/tasks/ReadAllWiki.json index d06f430735..90873e5a96 100644 --- a/assets/tasks/ReadAllWiki.json +++ b/assets/tasks/ReadAllWiki.json @@ -7,6 +7,7 @@ "description": "$task.ReadAllWiki.description", "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" diff --git a/assets/tasks/RealTimeTask.json b/assets/tasks/RealTimeTask.json index 472402f637..e743ad96b3 100644 --- a/assets/tasks/RealTimeTask.json +++ b/assets/tasks/RealTimeTask.json @@ -20,6 +20,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "group": [ diff --git a/assets/tasks/ReceiveProdManual.json b/assets/tasks/ReceiveProdManual.json index 7b0db42708..4b8cfddda9 100644 --- a/assets/tasks/ReceiveProdManual.json +++ b/assets/tasks/ReceiveProdManual.json @@ -7,6 +7,7 @@ "description": "$task.ReceiveProdManual.description", "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" diff --git a/assets/tasks/ResourceRecycleStation.json b/assets/tasks/ResourceRecycleStation.json index a7d64249e6..ae8f0acd11 100644 --- a/assets/tasks/ResourceRecycleStation.json +++ b/assets/tasks/ResourceRecycleStation.json @@ -10,6 +10,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "group": [ diff --git a/assets/tasks/SeizeDeliveryJobs.json b/assets/tasks/SeizeDeliveryJobs.json index b372ca09ae..335960e71f 100644 --- a/assets/tasks/SeizeDeliveryJobs.json +++ b/assets/tasks/SeizeDeliveryJobs.json @@ -11,6 +11,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "group": [ diff --git a/assets/tasks/SellProduct.json b/assets/tasks/SellProduct.json index da945bfa6d..a4aece2562 100644 --- a/assets/tasks/SellProduct.json +++ b/assets/tasks/SellProduct.json @@ -13,6 +13,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" diff --git a/assets/tasks/SimpleProductionBatchStart.json b/assets/tasks/SimpleProductionBatchStart.json index 4cca7d2054..9db3843f61 100644 --- a/assets/tasks/SimpleProductionBatchStart.json +++ b/assets/tasks/SimpleProductionBatchStart.json @@ -7,6 +7,7 @@ "description": "$task.SimpleProductionBatch.description", "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" diff --git a/assets/tasks/StashBackpack.json b/assets/tasks/StashBackpack.json index 45181b8132..bf23298132 100644 --- a/assets/tasks/StashBackpack.json +++ b/assets/tasks/StashBackpack.json @@ -6,6 +6,7 @@ "entry": "StashBackpackMain", "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "group": [ diff --git a/assets/tasks/SwitchTeam.json b/assets/tasks/SwitchTeam.json index cba53f43d0..22fb6cfaea 100644 --- a/assets/tasks/SwitchTeam.json +++ b/assets/tasks/SwitchTeam.json @@ -9,6 +9,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" diff --git a/assets/tasks/TrialOfSwordmancy.json b/assets/tasks/TrialOfSwordmancy.json index 1be786f978..ea2765ce29 100644 --- a/assets/tasks/TrialOfSwordmancy.json +++ b/assets/tasks/TrialOfSwordmancy.json @@ -10,6 +10,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "Wlroots" ], "group": [ diff --git a/assets/tasks/VisitFriends.json b/assets/tasks/VisitFriends.json index 54300595c1..fd1a4305b6 100644 --- a/assets/tasks/VisitFriends.json +++ b/assets/tasks/VisitFriends.json @@ -11,6 +11,7 @@ ], "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" diff --git a/assets/tasks/Weapon.json b/assets/tasks/Weapon.json index 99a6b1d59d..bf0d0eeea8 100644 --- a/assets/tasks/Weapon.json +++ b/assets/tasks/Weapon.json @@ -7,6 +7,7 @@ "description": "$task.WeaponUpgrade.description", "controller": [ "Win32-Front", + "GFN-App", "ADB", "PlayCover", "Wlroots" diff --git a/assets/tasks/WebEvent202605.json b/assets/tasks/WebEvent202605.json index 07aa1c4901..75143224f7 100644 --- a/assets/tasks/WebEvent202605.json +++ b/assets/tasks/WebEvent202605.json @@ -9,7 +9,8 @@ "WebEvent202605MaxShot" ], "controller": [ - "Win32-Front" + "Win32-Front", + "GFN-App" ], "group": [ "other_menu" diff --git a/docs/zh_cn/developers/README.md b/docs/zh_cn/developers/README.md index 74ba74f262..c8e5feee60 100644 --- a/docs/zh_cn/developers/README.md +++ b/docs/zh_cn/developers/README.md @@ -53,6 +53,7 @@ flowchart TD | [节点测试](./node-testing.md) | 如何编写和运行节点测试,验证识别是否稳定命中 | | [Pipeline 协议](https://maafw.com/docs/3.1-PipelineProtocol/) | MaaFramework 官方 Pipeline 协议全文 | | [发版流程](./release-process.md) | 发版周期、分支模型、PR 往哪提、自动化说明 | +| [GFN-App 支持 PRD](./gfn-app-support-prd.md) | GeForce NOW 原生客户端支持的需求与设计约束 | ### Tier 3 — 规范与约束 diff --git a/docs/zh_cn/developers/gfn-app-support-prd.md b/docs/zh_cn/developers/gfn-app-support-prd.md new file mode 100644 index 0000000000..135ef55a10 --- /dev/null +++ b/docs/zh_cn/developers/gfn-app-support-prd.md @@ -0,0 +1,170 @@ +# PRD:GeForce NOW 原生客户端(GFN-App)支持 + +> [!NOTE] +> 本文档为产品需求文档(PRD),描述需求与设计约束,不包含最终实现。 +> 状态:**草案**。涉及"待实测验证"的条目需在实现后于真实 Windows + GeForce NOW 环境确认。 + +> [!IMPORTANT] +> **设计变更(依上游 PR #4178 评审反馈 @ocsin1)**:原设计新增 `gfnwindow` taskersink 主动把 GFN 窗口缩放到精确 1280x720(±2px 容差)。评审指出 MaaEnd 的 Win32 分辨率预检**只要求 16:9 且不主动缩放、不强制 720p**,专用强制缩放与该契约相悖,建议在框架侧解决。 +> 现已改为 **GFN-App 直接复用 Win32 分辨率契约**:作为 `type=Win32` 控制器天然走 `taskersink/aspectratio`(16:9 且 ≥1280x720,MaaFramework 对截图缩放),删除 `gfnwindow` 缩放 sink 及其 ±2px 容差逻辑。 +> 对比佐证:姊妹项目 MaaNTE 的 `PinkPawHeist` **硬性要求精确 1280x720**(`resize_client_area(tolerance=2)`,配置项标注"关闭自动缩放时须自行保证 1280x720,否则识图失败"),因其 pipeline ROI 按固定 720p 编写、不走缩放;MaaEnd pipeline 走 MaaFramework 缩放,任意 16:9 ≥720p 均可,故不继承 MaaNTE 的精确 720p 约束。 + +## 1. 背景与目标 + +### 1.1 背景 + +MaaEnd 目前支持的控制器形态: + +- **本地 Windows 客户端**(`Win32-Front`):Unity 窗口类 `UnityWndClass`,标题匹配 `Endfield`,`ScreenDC` 截图 + `Seize` 输入; +- **安卓端**(`ADB`)、**PlayCover**、**Wlroots**。 + +部分用户通过 **GeForce NOW(GFN)云游戏原生桌面客户端** 游玩终末地:本地进程为 `GeForceNOW.exe`(基于 CEF),游戏本体运行在云端,本机不存在游戏进程与游戏安装目录。GFN 窗口的类名与标题均不匹配 `Win32-Front` 的正则,控制器列表中永远找不到 GFN 会话,MaaEnd 完全不可用。 + +姊妹项目 MaaNTE 已完成同类支持并实测确认关键结论(见其 `docs/zh_cn/develop/geforce-now-support-prd.md`): + +- GFN 原生客户端流窗口类名为 **`CEFCLIENT`**(EnumWindows 运行时取证,pid 级验证;社区旧文档的 `CEF-OSC-WIDGET` 为老版本客户端,不适用); +- 窗口标题模式为 **`<游戏英文名> on GeForce NOW`**; +- `PrintWindow` 截图 + `Seize` 鼠标/键盘组合可用,键盘按键(含 WASD 长按)可透传到云端; +- 流窗口无边框、无页面头部,接受标准 `MoveWindow`/`SetWindowPos` 外部缩放,客户区调至 1280x720 可获得干净 720p 帧; +- **串流渲染分辨率在会话建立时锁定**:本地缩放窗口不会改变云端渲染分辨率,已开始串流的会话仍按原分辨率云端渲染后缩放显示,固定 ROI 识别会系统性失败。 + +### 1.2 目标 + +- 用户使用 GFN 原生客户端游玩终末地时,MaaEnd 能自动找到游戏窗口并正常连接控制器。 +- GFN-App 作为 Win32 控制器复用现有 `aspectratio` 分辨率预检(16:9 且 ≥1280x720),不做 GFN 专用窗口缩放;不达标时按标准 Win32 流程警告并引导用户在 GFN 客户端调整串流分辨率。 +- 对 GFN 场景的能力边界(仅前台、串流分辨率锁定、依赖本地安装的任务不可用)给出明确的用户提示与文档说明。 + +### 1.3 非目标 + +- **不支持 GFN Chrome 网页版**:MaaNTE 实测其窗口模式下页面自绘头部占据客户区顶部约 26px,游戏画面下移且按比例缩放,所有固定 ROI 识别失败;即使 F11 全屏可用,本期不纳入。 +- 不支持 GFN 移动端 / TV 端。 +- 不自动化 GFN 自身的登录、排队、会话续期流程(属流程外状态,见 §6 风险)。 + +## 2. 现状分析 + +### 2.1 主路径:MaaFramework Win32 控制器 + +`assets/interface.json` 中 `Win32-Front` 控制器由 GUI 壳(MXU)传给 MaaFramework 的窗口枚举接口完成匹配: + +```jsonc +"win32": { + "class_regex": "UnityWndClass", + "window_regex": "Endfield", + "screencap": "ScreenDC", + "mouse": "Seize", + "keyboard": "Seize" +} +``` + +GFN 窗口(类 `CEFCLIENT`,标题含 `on GeForce NOW`)两个正则均不命中。 + +### 2.2 辅助路径:Go agent 任务预检链 + +`agent/go-service` 通过 taskersink 在任务启动事件(`EventStatusStarting`)上挂载预检: + +- `taskersink/aspectratio` — 分辨率/宽高比检查,不达标时警告并强停任务;全屏时尝试 Alt+Enter 切窗口化后复检(依赖 `pretask/gamesetting` 读取**本地游戏配置文件**判断全屏状态); +- `taskersink/hdrcheck`、`taskersink/processcheck`、`taskersink/cursormove` — 其他环境预检。 + +对 GFN 的失效点: + +| 失效点 | 说明 | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| 控制器正则不命中 | `class_regex` / `window_regex` 均针对本地 Unity 窗口(由 FR1 修复) | +| 分辨率门禁(非缺陷) | GFN 窗口按 Win32 契约走 `aspectratio`:16:9 且 ≥1280x720 即通过,否则警告强停并引导用户调整 GFN 串流分辨率(不主动缩放) | +| Alt+Enter 复检依赖本地文件 | GFN 场景本机无游戏安装,`gamesetting.GetVideoFullScreen()` 读取失败 → Alt+Enter 路径自然跳过,属预期降级,无需修改 | + +## 3. 目标窗口特征 + +| 目标 | 进程名 | 窗口类 | 标题模式 | 缩放行为 | +| ------------------ | ---------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------- | +| 本地客户端(现状) | 游戏本体进程 | `UnityWndClass` | `Endfield` | 游戏内设置分辨率 | +| GFN 原生客户端 | `GeForceNOW.exe`(本地无游戏进程) | `CEFCLIENT`(MaaNTE 实测;GFN 客户端与游戏无关,同客户端结论可复用) | `Arknights: Endfield on GeForce NOW`(**已实测确认**,正则取语言无关片段) | 接受标准 `SetWindowPos` 外部缩放;流窗口无边框、无页面头部 | + +## 4. 功能需求 + +### FR1 — 新增 GFN-App 控制器条目(`assets/interface.json`) + +```jsonc +{ + "name": "GFN-App", + "label": "$controller.GFN-App.label", + "description": "$controller.GFN-App.description", + "type": "Win32", + "win32": { + "class_regex": "CEFCLIENT", + "window_regex": "Endfield.*on GeForce NOW", + "screencap": "PrintWindow", + "mouse": "Seize", + "keyboard": "Seize", + }, +} +``` + +- 前台模式(`PrintWindow` + `Seize`):`SendMessage`/`PostMessage` 后台注入对 CEF 的 GPU 合成窗口普遍不可靠(MaaNTE 结论);若实测 `PrintWindow` 出现黑屏帧,回退 `ScreenDC`。 +- 不加 `permission_required`:GFN 客户端为普通用户权限进程,无需提权注入。 +- 键盘输入必须能透传到 GFN 串流(客户端把按键转发到云端主机),WASD 等长按键位为验证重点。 +- 配套新增 i18n 键 `controller.GFN-App.label` / `controller.GFN-App.description`,同步 5 个 locale 文件(`assets/locales/interface/{zh_cn,zh_tw,en_us,ja_jp,ko_kr}.json`)。 + +### FR2 — GFN-App 复用 Win32 分辨率契约(不新增专用缩放 sink) + +依设计变更(见文首 IMPORTANT),**不新增** `gfnwindow` taskersink,也不做 GFN 专用窗口缩放。GFN-App 作为 `type=Win32` 控制器天然走既有预检链: + +- **分辨率门禁**:完全交由 `taskersink/aspectratio`。非 ADB(含 GFN-App)路径要求 **16:9 宽高比(±2%)且 ≥1280x720**,由 MaaFramework 对截图缩放至 pipeline 基准,从不主动缩放窗口、不强制精确 720p。GFN 窗口不达标时按标准 Win32 流程警告并强停,用户体验与其他 Win32 控制器一致。 +- **无 GFN 特判**:`registerAll()` 不再注册 `gfnwindow`,不做控制器名判定;不引入 ±2px 容差、右下角贴靠、`PostScreencap` 缓存刷新等 GFN 专用逻辑。 +- **Alt+Enter 降级**:`aspectratio` 的全屏→窗口化复检因 GFN 无本地游戏配置文件(`gamesetting.GetVideoFullScreen()` 读取失败)自然跳过,属预期降级,无需修改。 +- **HWND 公共化保留**:controller info → HWND 解析已提炼为 `pkg/control` 公共函数供 `taskersink/aspectratio`(`altenter_windows.go`)复用;该重构独立于本次决策,予以保留。 +- **用户引导**:GFN 串流分辨率需为 16:9 且 ≥720p 的建议,通过标准 `aspectratio` 警告文案与用户文档说明,不再由专用 sink 弹出 720p 提示。 + +### FR3 — 任务控制器解锁(`assets/tasks/**/*.json`) + +GFN-App 与 `Win32-Front` 模式一致(前台 + Seize),原则上所有支持 `Win32-Front` 的任务均可运行。在所有 `controller` 数组含 `"Win32-Front"` 的任务里追加 `"GFN-App"`,**排除以下依赖本地游戏安装/进程的任务**(GFN 场景游戏运行在云端,原理上不可用): + +| 任务 | 排除原因 | +| --------------------------------------- | --------------------------------- | +| `tasks/pretasks/GameSetting.json` | pretask 读取/修改本地游戏配置文件 | +| `tasks/AccountSwitch.json` | 依赖本地客户端的账号切换流程 | +| `tasks/CloseGame.json` 等 ADB-only 任务 | 本就不含 `Win32-Front`,不受影响 | + +新增任务时按本表原则声明控制器限制。 + +### FR4 — 与现有预检链的交互 + +| 预检 | GFN 场景行为 | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `aspectratio` | GFN-App 走非 ADB 路径:16:9 且 ≥1280x720 即通过,否则警告强停并引导用户调整 GFN 串流分辨率。Alt+Enter 路径因读不到本地配置文件自然跳过,属预期降级,无需修改 | +| `hdrcheck` / `processcheck` | 行为不变(检查的是本机环境,与 GFN 无冲突) | + +## 5. 非功能需求 + +- **零新增 Go 依赖**:Win32 调用基于现有 `golang.org/x/sys/windows` 封装扩展。 +- **向后兼容**:本地客户端 / ADB / PlayCover / Wlroots 用户的行为与性能不受任何影响。 +- **日志规范**:遵循 zerolog 结构化日志约定 —— DEBUG 记探测细节、WARNING 记降级路径;用户可见提示走 maafocus + i18n(5 语言同步),禁止裸文本。 +- **格式规范**:JSON 改动通过 `pnpm format` / `pnpm check`;Go 改动通过 `pnpm format:go` 与 `go vet`。 + +## 6. 风险与开放问题 + +| # | 风险 / 开放问题 | 影响 | 应对 | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| R1 | ~~终末地在 GFN 的实际窗口标题未确认~~ **已关闭**:实测确认标题为 `Arknights: Endfield on GeForce NOW`,正则 `Endfield.*on GeForce NOW` 命中 | — | 保持匹配语言无关片段(游戏英文名 + `GeForce NOW`);如遇多语言客户端标题差异再收集样本 | +| R2 | CEF GPU 合成窗口对后台注入不可靠,GFN 用户只能前台运行 | 无法使用后台任务 | 控制器即声明为前台模式;locale 描述与文档中声明该限制 | +| R3 | ~~压缩伪影可能拉低识别匹配分~~ **已实测定性**:720p 串流下 TemplateMatch 正常(实测 0.95+),但 1~2 单位宽的精确 ColorMatch 全部失配(品牌黄 hue 本地 28-29 → 串流实测 26-31,连通像素 47/3000),触发 SceneManager"颜色识别失败"误报 | ColorMatch 校准门禁在 GFN 下必然中止任务 | 已新增 `resource_gfn` 覆盖资源(随 GFN-App 控制器 `attach_resource_path` 加载),放宽按钮底色与加载校准色块的颜色容差;后续发现同类节点按此模式补充覆盖 | +| R4 | 串流渲染分辨率在会话建立时锁定;本地窗口非 16:9 或低于 720p 时识别失败 | 分辨率不达标则任务被 `aspectratio` 强停 | 不主动缩放(见设计变更);由 `aspectratio` 门禁 + 用户文档引导用户在 GFN 客户端将串流分辨率设为 16:9 且 ≥720p(推荐 1280x720)后重启会话 | +| R5 | `CEFCLIENT` 类名可能随 GFN 客户端版本变化(历史上出现过 `CEF-OSC-WIDGET`) | 客户端更新后控制器失效 | 探测日志保留窗口类字段,便于未来核对;PRD 记录取证方法 | +| R6 | GFN 自身的排队、闲置踢出、会话到期画面 | 任务流程外状态,Pipeline 无法恢复 | 非目标(§1.3);提示用户保持会话活跃,长任务失败时日志可定位 | +| R7 | `PrintWindow` 对 GFN 流窗口截图可用性未在终末地场景复测 | 黑屏帧则识别全部失败 | MaaNTE 同客户端实测可用;若失效回退 `ScreenDC`(要求窗口前台不遮挡,与 Seize 前提一致) | + +## 7. 验收标准 + +- [ ] `GFN-App` 控制器在 MXU 控制器列表中可见、可选、可连接。(待 Windows 实测) +- [ ] 控制器连接成功,串流分辨率为 16:9 且 ≥720p 时截图正常,鼠标/键盘输入在串流中生效(含 WASD 长按)。(待 Windows 实测) +- [ ] GFN 窗口非 16:9 或低于 1280x720 时,`aspectratio` 按标准 Win32 流程警告并强停并输出引导提示(不主动缩放)。(待 Windows 实测) +- [ ] 非 GFN 控制器(Win32-Front / ADB / PlayCover / Wlroots)行为回归一致,无新增日志噪音。 +- [ ] 代表性任务(如 `DailyRewards`、`PuzzleSolver`)在 GFN 下端到端跑通。(待 Windows 实测) +- [ ] 5 个 interface locale 与 5 个 go-service locale 的新增键完整同步,`pnpm format:check` / `pnpm check` / `pnpm test` 通过。 +- [ ] `agent/go-service` 在 `GOOS=windows` 与本机双目标下 `go build ./...` / `go vet ./...` 通过。 + +## 8. 后续工作 + +1. Windows + GFN 实机回填 R5/R7 实测数据(窗口类复核、`PrintWindow` 截图可用性),必要时修正 `class_regex` / `screencap`(R1 窗口标题已确认); +2. 视用户反馈评估 GFN Chrome 网页版(F11 全屏前提)与其他 Chromium 系浏览器支持; +3. 视实测识别命中率评估 GFN 专用模板或阈值调整。 diff --git a/tools/schema/interface.schema.json b/tools/schema/interface.schema.json index 64415c3cf8..40e714c2cf 100644 --- a/tools/schema/interface.schema.json +++ b/tools/schema/interface.schema.json @@ -496,7 +496,8 @@ "MacOS", "PlayCover", "Gamepad", - "WlRoots" + "WlRoots", + "Cloud" ] }, "display_short_side": { @@ -607,6 +608,30 @@ }, "additionalProperties": false }, + "cloud": { + "type": "object", + "title": "Cloud 控制器配置", + "description": "云串流控制器预设(type 为 Cloud 时生效)。框架内置各云服务商的窗口签名(进程 / 窗口类 / 标题模板)及截图与输入方式,只需指定服务商与游戏名", + "properties": { + "provider": { + "type": "string", + "title": "云服务商", + "description": "云串流服务商标识。目前支持 GeForce NOW 原生客户端", + "enum": [ + "geforce_now" + ] + }, + "game_title": { + "type": "string", + "title": "游戏名片段", + "description": "用于匹配窗口标题的游戏名正则片段,会被代入服务商标题模板(如 \"{game}.*on GeForce NOW\")" + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, "macos": { "type": "object", "title": "MacOS 控制器配置",