From a11660cbac98113737c052d22c2e219c51d418ac Mon Sep 17 00:00:00 2001 From: MistEO Date: Sun, 18 Jan 2026 21:17:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(PC):=20=E6=94=AF=E6=8C=81=20PC=20=E7=AB=AF?= =?UTF-8?q?=E6=98=8E=E6=97=A5=E6=96=B9=E8=88=9F=20(#15407)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(controller): 集成 MaaWin32ControlUnit 支持 Win32 窗口截图与控制 - 提取公共滑动插值逻辑到 SwipeHelper.hpp,重构 Minitouch/PlayTools 控制器 - 新增 Win32ControlUnitLoader 动态加载 MaaWin32ControlUnit.dll - 新增 Win32Controller 实现 ControllerAPI,适配 touch_down/move/up 接口 - 添加 AsstAsyncAttachWindow 公开 API 用于绑定 Win32 窗口 - 添加 Win32 截图/输入方式的类型定义和枚举常量 * feat(api): 增加同步版本的 AsstAttachWindow 接口 模仿 AsstConnect 实现同步绑定 Win32 窗口的接口,方便简单场景下直接调用。 该接口标记为 deprecated,建议使用异步版本 AsstAsyncAttachWindow。 * fix: can build MaaCore * feat: 完成整体适配 * feat(ci): 添加 MaaFramework Win32ControlUnit 下载步骤 使用 robinraju/release-downloader 从 MaaFramework 最新 release 下载 win-x64 版本并提取 MaaWin32ControlUnit 到构建产物中 * docs: 添加 Win32Controller 调试所需 MaaWin32ControlUnit 的说明 在开发文档的环境配置步骤中说明调试 Win32Controller 需要手动下载 MaaWin32ControlUnit.dll,并欢迎社区贡献自动下载脚本 * fix(i18n): 修改 UseAttachWindowWarning 措辞为"仅供尝鲜" * fix: 小细节修复 * ci: fix maafw filename * feat: PC 移至连接配置 * feat: 调整描述 * chore: 调整 PC 在连接配置中的顺序 * feat: 设置指引和开始唤醒界面 PC 选项绑定 --------- Co-authored-by: uye <99072975+ABA2396@users.noreply.github.com> --- .github/workflows/ci.yml | 15 + docs/en-us/develop/development.md | 4 + docs/ja-jp/develop/development.md | 4 + docs/ko-kr/develop/development.md | 4 + docs/zh-cn/develop/development.md | 4 + docs/zh-tw/develop/development.md | 4 + include/AsstCaller.h | 55 +++ src/MaaCore/Assistant.cpp | 68 ++++ src/MaaCore/Assistant.h | 35 ++ src/MaaCore/AsstCaller.cpp | 38 ++ src/MaaCore/Common/AsstTypes.h | 31 ++ src/MaaCore/Controller/Controller.cpp | 63 +++ src/MaaCore/Controller/Controller.h | 3 + src/MaaCore/Controller/ControllerAPI.h | 3 + .../Controller/MinitouchController.cpp | 84 ++-- .../Controller/PlayToolsController.cpp | 30 +- src/MaaCore/Controller/SwipeHelper.hpp | 137 +++++++ .../Controller/Win32ControlUnitLoader.cpp | 119 ++++++ .../Controller/Win32ControlUnitLoader.h | 107 +++++ src/MaaCore/Controller/Win32Controller.cpp | 383 ++++++++++++++++++ src/MaaCore/Controller/Win32Controller.h | 102 +++++ src/MaaWpfGui/Constants/ConfigurationKeys.cs | 6 + src/MaaWpfGui/Main/AsstProxy.cs | 144 ++++++- src/MaaWpfGui/Res/Localizations/en-us.xaml | 26 ++ src/MaaWpfGui/Res/Localizations/ja-jp.xaml | 26 ++ src/MaaWpfGui/Res/Localizations/ko-kr.xaml | 26 ++ src/MaaWpfGui/Res/Localizations/zh-cn.xaml | 26 ++ src/MaaWpfGui/Res/Localizations/zh-tw.xaml | 26 ++ src/MaaWpfGui/Services/MaaService.cs | 4 + .../ViewModels/UI/TaskQueueViewModel.cs | 8 + .../ConnectSettingsUserControlModel.cs | 83 ++++ .../Views/UserControl/GuideUserControl.xaml | 219 ++++++---- .../Settings/ConnectSettingsUserControl.xaml | 238 +++++++---- .../TaskQueue/StartUpTaskUserControl.xaml | 270 ++++++------ 34 files changed, 2042 insertions(+), 353 deletions(-) create mode 100644 src/MaaCore/Controller/SwipeHelper.hpp create mode 100644 src/MaaCore/Controller/Win32ControlUnitLoader.cpp create mode 100644 src/MaaCore/Controller/Win32ControlUnitLoader.h create mode 100644 src/MaaCore/Controller/Win32Controller.cpp create mode 100644 src/MaaCore/Controller/Win32Controller.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8398185f09..c25e4a4807 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,6 +144,21 @@ jobs: mkdir -p install cmake --install build --prefix install --config RelWithDebInfo + - name: Download MaaFramework + if: matrix.arch == 'x64' + uses: robinraju/release-downloader@v1 + with: + repository: MaaXYZ/MaaFramework + latest: true + fileName: '*win-x86_64*.zip' + extract: true + out-file-path: MaaFramework-temp + + - name: Copy MaaWin32ControlUnit + if: matrix.arch == 'x64' + run: | + cp MaaFramework-temp/bin/*Win32ControlUnit* install/ + - name: Cache .nuke/temp, ~/.nuget/packages id: cache-nuget uses: actions/cache@v5 diff --git a/docs/en-us/develop/development.md b/docs/en-us/develop/development.md index eacc04d9a7..32bcefd6b5 100644 --- a/docs/en-us/develop/development.md +++ b/docs/en-us/develop/development.md @@ -74,6 +74,10 @@ We've preset several different development environments for you to choose from: - Right-click `MaaWpfGui` - Set as Startup Project - Press F5 to run + ::: tip + To debug Win32Controller (Windows window control) features, you need to manually download the corresponding platform package from [MaaFramework Releases](https://github.com/MaaXYZ/MaaFramework/releases), and place `MaaWin32ControlUnit.dll` from the `bin` directory into MAA's DLL directory (e.g. `build/bin/Debug`). PRs for an auto-download script are welcome! + ::: + 9. Now you're ready to happily ~~mess around~~ start developing! 10. Commit regularly with meaningful messages during development If you're not familiar with git usage, you might want to create a new branch for changes instead of committing directly to `dev`: diff --git a/docs/ja-jp/develop/development.md b/docs/ja-jp/develop/development.md index 477c120da3..936bcfb1b1 100644 --- a/docs/ja-jp/develop/development.md +++ b/docs/ja-jp/develop/development.md @@ -80,6 +80,10 @@ icon: iconoir:developer - `MaaWpfGui` 右クリック → スタートアップ プロジェクトに設定 - F5 キーを押して実行 + ::: tip + Win32Controller(Windows ウィンドウ制御)関連機能をデバッグする場合は、[MaaFramework Releases](https://github.com/MaaXYZ/MaaFramework/releases) から対応プラットフォームのアーカイブをダウンロードし、`bin` ディレクトリ内の `MaaWin32ControlUnit.dll` を MAA の DLL と同じディレクトリ(例:`build/bin/Debug`)に配置してください。自動ダウンロードスクリプトの PR 歓迎! + ::: + 9. これで自由に ~~改造~~ 開発を始められます 10. 一定量の変更ごにコミット(メッセージ記入必須) Git 未経験者は dev ブランチ直接変更ではなく新規ブランチ作成推奨: diff --git a/docs/ko-kr/develop/development.md b/docs/ko-kr/develop/development.md index e56200b39d..413085c395 100644 --- a/docs/ko-kr/develop/development.md +++ b/docs/ko-kr/develop/development.md @@ -79,6 +79,10 @@ icon: iconoir:developer - `MaaWpfGui` 우클릭 → 시작 프로젝트로 설정 - F5 키를 눌러 실행 + ::: tip + Win32Controller(Windows 창 제어) 관련 기능을 디버깅하려면 [MaaFramework Releases](https://github.com/MaaXYZ/MaaFramework/releases)에서 해당 플랫폼 압축 파일을 다운로드하고, `bin` 디렉토리의 `MaaWin32ControlUnit.dll`을 MAA DLL과 같은 디렉토리(예: `build/bin/Debug`)에 배치해야 합니다. 자동 다운로드 스크립트 PR 환영! + ::: + 9. 이제 자유롭게 ~~개조~~ 개발 시작! 10. 주기적 커밋 (메시지 필수 작성) Git 초보자는 dev 브랜치 대신 새 브랜치 생성 권장: diff --git a/docs/zh-cn/develop/development.md b/docs/zh-cn/develop/development.md index 06bfb98c23..7025600343 100644 --- a/docs/zh-cn/develop/development.md +++ b/docs/zh-cn/develop/development.md @@ -74,6 +74,10 @@ icon: iconoir:developer - 右键 `MaaWpfGui` - 设为启动项目 - 按 F5 运行 + ::: tip + 若需调试 Win32Controller(Windows 窗口控制)相关功能,需要自行从 [MaaFramework Releases](https://github.com/MaaXYZ/MaaFramework/releases) 下载对应平台的压缩包,将 `bin` 目录中的 `MaaWin32ControlUnit.dll` 放到 MAA 的 DLL 同目录下(如 `build/bin/Debug`)。欢迎 PR 一个自动下载脚本! + ::: + 9. 到这里,你就可以愉快地 ~~瞎 JB 改~~ 发电了 10. 开发过程中,每一定数量,记得提交一个 Commit, 别忘了写上 Message 假如你不熟悉 git 的使用,你可能想要新建一个分支进行更改,而不是直接提交在 `dev` 上 diff --git a/docs/zh-tw/develop/development.md b/docs/zh-tw/develop/development.md index 8dc070de3c..53bb2e5717 100644 --- a/docs/zh-tw/develop/development.md +++ b/docs/zh-tw/develop/development.md @@ -74,6 +74,10 @@ icon: iconoir:developer - 右鍵 `MaaWpfGui` - 設為啟動項目 - 按 F5 運行 + ::: tip + 若需調試 Win32Controller(Windows 視窗控制)相關功能,需要自行從 [MaaFramework Releases](https://github.com/MaaXYZ/MaaFramework/releases) 下載對應平台的壓縮包,將 `bin` 目錄中的 `MaaWin32ControlUnit.dll` 放到 MAA 的 DLL 同目錄下(如 `build/bin/Debug`)。歡迎 PR 一個自動下載腳本! + ::: + 9. 到這裡,你就可以愉快地 ~~瞎 JB 改~~ 發電了 10. 開發過程中,每一定數量,記得提交一個 Commit, 別忘了寫上 Message 假如你不熟悉 git 的使用,你可能想要新建一個分支進行更改,而不是直接提交在 `dev` 上 diff --git a/include/AsstCaller.h b/include/AsstCaller.h index d82647b6f5..5d04578820 100644 --- a/include/AsstCaller.h +++ b/include/AsstCaller.h @@ -6,6 +6,33 @@ struct AsstExtAPI; typedef struct AsstExtAPI* AsstHandle; +#ifdef _WIN32 +// Win32 截图方式 +typedef enum AsstWin32ScreencapMethodEnum +{ + AsstWin32ScreencapMethod_None = 0, + AsstWin32ScreencapMethod_GDI = 1, + AsstWin32ScreencapMethod_FramePool = 1 << 1, + AsstWin32ScreencapMethod_DXGI_DesktopDup = 1 << 2, + AsstWin32ScreencapMethod_DXGI_DesktopDup_Window = 1 << 3, + AsstWin32ScreencapMethod_PrintWindow = 1 << 4, + AsstWin32ScreencapMethod_ScreenDC = 1 << 5, +} AsstWin32ScreencapMethodEnum; + +// Win32 输入方式 +typedef enum AsstWin32InputMethodEnum +{ + AsstWin32InputMethod_None = 0, + AsstWin32InputMethod_Seize = 1, + AsstWin32InputMethod_SendMessage = 1 << 1, + AsstWin32InputMethod_PostMessage = 1 << 2, + AsstWin32InputMethod_LegacyEvent = 1 << 3, + AsstWin32InputMethod_PostThreadMessage = 1 << 4, + AsstWin32InputMethod_SendMessageWithCursorPos = 1 << 5, + AsstWin32InputMethod_PostMessageWithCursorPos = 1 << 6, +} AsstWin32InputMethodEnum; +#endif + typedef uint8_t AsstBool; typedef uint64_t AsstSize; @@ -61,6 +88,34 @@ extern "C" AsstBool block); void ASSTAPI AsstSetConnectionExtras(const char* name, const char* extras); +#ifdef _WIN32 + // 同步绑定到 Win32 窗口,功能已完全被异步绑定取代 + // FIXME: 5.0 版本将废弃此接口 + // hwnd: 目标窗口句柄 + // screencap_method: 截图方式,参见 Win32ScreencapMethod + // mouse_method: 鼠标输入方式,参见 Win32InputMethod + // keyboard_method: 键盘输入方式,参见 Win32InputMethod + /* deprecated */ AsstBool ASSTAPI AsstAttachWindow( + AsstHandle handle, + void* hwnd, + uint64_t screencap_method, + uint64_t mouse_method, + uint64_t keyboard_method); + + // 异步绑定到 Win32 窗口(仅 Windows 平台) + // hwnd: 目标窗口句柄 + // screencap_method: 截图方式,参见 Win32ScreencapMethod + // mouse_method: 鼠标输入方式,参见 Win32InputMethod + // keyboard_method: 键盘输入方式,参见 Win32InputMethod + AsstAsyncCallId ASSTAPI AsstAsyncAttachWindow( + AsstHandle handle, + void* hwnd, + uint64_t screencap_method, + uint64_t mouse_method, + uint64_t keyboard_method, + AsstBool block); +#endif + AsstAsyncCallId ASSTAPI AsstAsyncClick(AsstHandle handle, int32_t x, int32_t y, AsstBool block); AsstAsyncCallId ASSTAPI AsstAsyncScreencap(AsstHandle handle, AsstBool block); diff --git a/src/MaaCore/Assistant.cpp b/src/MaaCore/Assistant.cpp index e66b7c2e75..b900edcfb8 100644 --- a/src/MaaCore/Assistant.cpp +++ b/src/MaaCore/Assistant.cpp @@ -193,6 +193,34 @@ bool asst::Assistant::ctrl_connect(const std::string& adb_path, const std::strin return ret; } +#ifdef _WIN32 +bool asst::Assistant::ctrl_attach_window( + void* hwnd, + Win32ScreencapMethod screencap_method, + Win32InputMethod mouse_method, + Win32InputMethod keyboard_method) +{ + LogTraceFunction; + + std::unique_lock lock(m_mutex); + + // 仍有任务进行,attach 前需要 stop + if (!m_thread_idle) { + return false; + } + + m_thread_idle = false; + + bool ret = m_ctrler->attach_window(hwnd, screencap_method, mouse_method, keyboard_method); + if (ret) { + m_uuid = m_ctrler->get_uuid(); + } + + m_thread_idle = true; + return ret; +} +#endif + bool asst::Assistant::ctrl_click(int x, int y) { return m_ctrler->click(Point(x, y)); @@ -346,6 +374,38 @@ asst::Assistant::AsyncCallId asst::Assistant::async_connect( block); } +#ifdef _WIN32 +bool asst::Assistant::attach_window( + void* hwnd, + Win32ScreencapMethod screencap_method, + Win32InputMethod mouse_method, + Win32InputMethod keyboard_method) +{ + LogTraceFunction; + + return ctrl_attach_window(hwnd, screencap_method, mouse_method, keyboard_method); +} + +asst::Assistant::AsyncCallId asst::Assistant::async_attach_window( + void* hwnd, + Win32ScreencapMethod screencap_method, + Win32InputMethod mouse_method, + Win32InputMethod keyboard_method, + bool block) +{ + LogTraceFunction; + + return append_async_call( + AsyncCallItem::Type::AttachWindow, + AsyncCallItem::AttachWindowParams { + .hwnd = hwnd, + .screencap_method = screencap_method, + .mouse_method = mouse_method, + .keyboard_method = keyboard_method }, + block); +} +#endif + asst::Assistant::AsyncCallId asst::Assistant::async_click(int x, int y, bool block) { LogTraceFunction; @@ -580,6 +640,14 @@ void asst::Assistant::call_proc() const auto& [adb_path, address, config] = std::get(call_item.params); ret = ctrl_connect(adb_path, address, config); } break; +#ifdef _WIN32 + case AsyncCallItem::Type::AttachWindow: { + what = "AttachWindow"; + const auto& [hwnd, screencap_method, mouse_method, keyboard_method] = + std::get(call_item.params); + ret = ctrl_attach_window(hwnd, screencap_method, mouse_method, keyboard_method); + } break; +#endif case AsyncCallItem::Type::Click: { what = "Click"; const auto& [x, y] = std::get(call_item.params); diff --git a/src/MaaCore/Assistant.h b/src/MaaCore/Assistant.h index d3fe922e6b..b5aee143df 100644 --- a/src/MaaCore/Assistant.h +++ b/src/MaaCore/Assistant.h @@ -85,6 +85,21 @@ public: const std::string& address, const std::string& config, bool block = false) override; +#ifdef _WIN32 + // 同步绑定到 Win32 窗口,功能已完全被异步绑定取代 + // FIXME: 5.0 版本将废弃此接口 + bool attach_window( + void* hwnd, + Win32ScreencapMethod screencap_method, + Win32InputMethod mouse_method, + Win32InputMethod keyboard_method); + AsyncCallId async_attach_window( + void* hwnd, + Win32ScreencapMethod screencap_method, + Win32InputMethod mouse_method, + Win32InputMethod keyboard_method, + bool block = false); +#endif virtual AsyncCallId async_click(int x, int y, bool block = false) override; virtual AsyncCallId async_screencap(bool block = false) override; @@ -121,6 +136,9 @@ private: enum class Type { Connect, +#ifdef _WIN32 + AttachWindow, +#endif Click, Screencap, }; @@ -132,6 +150,16 @@ private: std::string config; }; +#ifdef _WIN32 + struct AttachWindowParams + { + void* hwnd = nullptr; + Win32ScreencapMethod screencap_method = Win32Screencap::None; + Win32InputMethod mouse_method = Win32Input::None; + Win32InputMethod keyboard_method = Win32Input::None; + }; +#endif + struct ClickParams { int x = 0; @@ -142,7 +170,11 @@ private: { }; +#ifdef _WIN32 + using Parmas = std::variant; +#else using Parmas = std::variant; +#endif AsyncCallId id; Type type; @@ -162,6 +194,9 @@ private: bool inited() const noexcept; bool ctrl_connect(const std::string& adb_path, const std::string& address, const std::string& config); +#ifdef _WIN32 + bool ctrl_attach_window(void* hwnd, Win32ScreencapMethod screencap_method, Win32InputMethod mouse_method, Win32InputMethod keyboard_method); +#endif bool ctrl_click(int x, int y); bool ctrl_screencap(); diff --git a/src/MaaCore/AsstCaller.cpp b/src/MaaCore/AsstCaller.cpp index cbc5921f00..fa9cd7c6e4 100644 --- a/src/MaaCore/AsstCaller.cpp +++ b/src/MaaCore/AsstCaller.cpp @@ -182,6 +182,44 @@ void AsstSetConnectionExtras(const char* name, const char* extras) asst::ResourceLoader::get_instance().set_connection_extras(name, jopt->as_object()); } +#ifdef _WIN32 +AsstBool AsstAttachWindow( + AsstHandle handle, + void* hwnd, + uint64_t screencap_method, + uint64_t mouse_method, + uint64_t keyboard_method) +{ + if (!inited() || handle == nullptr) { + Log.error(__FUNCTION__, "Cannot attach to window, asst not inited or handle is null", inited(), handle); + return AsstFalse; + } + auto* assistant = dynamic_cast(handle); + if (!assistant) { + return AsstFalse; + } + return assistant->attach_window(hwnd, screencap_method, mouse_method, keyboard_method) ? AsstTrue : AsstFalse; +} + +AsstAsyncCallId AsstAsyncAttachWindow( + AsstHandle handle, + void* hwnd, + uint64_t screencap_method, + uint64_t mouse_method, + uint64_t keyboard_method, + AsstBool block) +{ + if (!inited() || handle == nullptr) { + return InvalidId; + } + auto* assistant = dynamic_cast(handle); + if (!assistant) { + return InvalidId; + } + return assistant->async_attach_window(hwnd, screencap_method, mouse_method, keyboard_method, block); +} +#endif + AsstTaskId AsstAppendTask(AsstHandle handle, const char* type, const char* params) { if (!inited() || handle == nullptr) { diff --git a/src/MaaCore/Common/AsstTypes.h b/src/MaaCore/Common/AsstTypes.h index 610d02c29a..95b8e8a6ae 100644 --- a/src/MaaCore/Common/AsstTypes.h +++ b/src/MaaCore/Common/AsstTypes.h @@ -56,6 +56,37 @@ enum class TouchMode MacPlayTools = 3, }; +#ifdef _WIN32 + +// Win32 截图方式,与 MaaFramework 的 MaaWin32ScreencapMethod 保持一致 +using Win32ScreencapMethod = uint64_t; +namespace Win32Screencap +{ +constexpr Win32ScreencapMethod None = 0ULL; +constexpr Win32ScreencapMethod GDI = 1ULL; +constexpr Win32ScreencapMethod FramePool = 1ULL << 1; +constexpr Win32ScreencapMethod DXGI_DesktopDup = 1ULL << 2; +constexpr Win32ScreencapMethod DXGI_DesktopDup_Window = 1ULL << 3; +constexpr Win32ScreencapMethod PrintWindow = 1ULL << 4; +constexpr Win32ScreencapMethod ScreenDC = 1ULL << 5; +} // namespace Win32Screencap + +// Win32 输入方式,与 MaaFramework 的 MaaWin32InputMethod 保持一致 +using Win32InputMethod = uint64_t; +namespace Win32Input +{ +constexpr Win32InputMethod None = 0ULL; +constexpr Win32InputMethod Seize = 1ULL; +constexpr Win32InputMethod SendMessage = 1ULL << 1; +constexpr Win32InputMethod PostMessage = 1ULL << 2; +constexpr Win32InputMethod LegacyEvent = 1ULL << 3; +constexpr Win32InputMethod PostThreadMessage = 1ULL << 4; +constexpr Win32InputMethod SendMessageWithCursorPos = 1ULL << 5; +constexpr Win32InputMethod PostMessageWithCursorPos = 1ULL << 6; +} // namespace Win32Input + +#endif // _WIN32 + namespace ControlFeat { using Feat = int64_t; diff --git a/src/MaaCore/Controller/Controller.cpp b/src/MaaCore/Controller/Controller.cpp index fd4fafa715..e613101304 100644 --- a/src/MaaCore/Controller/Controller.cpp +++ b/src/MaaCore/Controller/Controller.cpp @@ -23,6 +23,9 @@ #include "MaatouchController.h" #include "MinitouchController.h" #include "PlayToolsController.h" +#ifdef _WIN32 +#include "Win32Controller.h" +#endif #include "Common/AsstTypes.h" #include "Utils/Logger.hpp" @@ -284,6 +287,66 @@ bool asst::Controller::connect(const std::string& adb_path, const std::string& a return true; } +#ifdef _WIN32 +bool asst::Controller::attach_window( + void* hwnd, + Win32ScreencapMethod screencap_method, + Win32InputMethod mouse_method, + Win32InputMethod keyboard_method) +{ + LogTraceFunction; + + clear_info(); + + auto win32_controller = std::make_shared(m_callback, m_inst); + if (!win32_controller->attach(hwnd, screencap_method, mouse_method, keyboard_method)) { + Log.error("attach_window failed"); + return false; + } + + m_controller = win32_controller; + m_controller_type = ControllerType::Win32; + m_uuid = m_controller->get_uuid(); + + // 尝试截图 + if (!screencap()) { + Log.error("Cannot screencap!"); + return false; + } + + auto proxy_callback = [&](const json::object& details) { + json::value connection_info = json::object { + { "uuid", m_uuid }, + { "details", + json::object { + { "hwnd", reinterpret_cast(hwnd) }, + { "screencap_method", screencap_method }, + { "mouse_method", mouse_method }, + { "keyboard_method", keyboard_method }, + } }, + } | details; + callback(AsstMsg::ConnectionInfo, connection_info); + }; + + try { + m_scale_proxy = std::make_shared(m_controller, m_controller_type, proxy_callback); + } + catch (const std::exception& e) { + Log.error("Cannot create controller proxy: {}", e.what()); + return false; + } + + if (!m_scale_proxy) { + Log.error("Cannot create controller proxy!"); + return false; + } + + m_scale_size = m_scale_proxy->get_scale_size(); + + return true; +} +#endif + bool asst::Controller::inited() noexcept { CHECK_EXIST(m_controller, false); diff --git a/src/MaaCore/Controller/Controller.h b/src/MaaCore/Controller/Controller.h index 75a9d4568f..313b2ae398 100644 --- a/src/MaaCore/Controller/Controller.h +++ b/src/MaaCore/Controller/Controller.h @@ -45,6 +45,9 @@ public: PlatformType platform_type) const; bool connect(const std::string& adb_path, const std::string& address, const std::string& config); +#ifdef _WIN32 + bool attach_window(void* hwnd, Win32ScreencapMethod screencap_method, Win32InputMethod mouse_method, Win32InputMethod keyboard_method); +#endif bool inited() noexcept; void set_touch_mode(const TouchMode& mode) noexcept; void set_swipe_with_pause(bool enable) noexcept; diff --git a/src/MaaCore/Controller/ControllerAPI.h b/src/MaaCore/Controller/ControllerAPI.h index e76014b7f9..ec7033f54f 100644 --- a/src/MaaCore/Controller/ControllerAPI.h +++ b/src/MaaCore/Controller/ControllerAPI.h @@ -15,6 +15,9 @@ enum class ControllerType Minitouch, Maatouch, MacPlayTools, +#ifdef _WIN32 + Win32, +#endif }; class ControllerAPI diff --git a/src/MaaCore/Controller/MinitouchController.cpp b/src/MaaCore/Controller/MinitouchController.cpp index 461a874aba..8167b8c8f0 100644 --- a/src/MaaCore/Controller/MinitouchController.cpp +++ b/src/MaaCore/Controller/MinitouchController.cpp @@ -4,6 +4,7 @@ #include "Common/AsstTypes.h" #include "Config/GeneralConfig.h" +#include "SwipeHelper.hpp" #include "Utils/Logger.hpp" #include "Utils/StringMisc.hpp" @@ -183,50 +184,55 @@ bool asst::MinitouchController::swipe( constexpr int TimeInterval = Minitoucher::DefaultSwipeDelay; - auto cubic_spline = [](double slope_0, double slope_1, double t) { - const double a = slope_0; - const double b = -(2 * slope_0 + slope_1 - 3); - const double c = -(-slope_0 - slope_1 + 2); - return a * t + b * std::pow(t, 2) + c * std::pow(t, 3); - }; // TODO: move this to math.hpp - bool need_pause = with_pause && use_swipe_with_pause(); const auto& opt = Config.get_options(); std::future pause_future; + + auto bounds_check = [this](int x, int y) { + return x >= 0 && x <= m_minitouch_props.max_x && y >= 0 && y <= m_minitouch_props.max_y; + }; + + auto move_func = [this](int x, int y) { return m_minitoucher->move(x, y); }; + + auto pause_check = [&opt](int cur_x, int cur_y, int start_x, int start_y) { + return std::sqrt(std::pow(cur_x - start_x, 2) + std::pow(cur_y - start_y, 2)) > + opt.swipe_with_pause_required_distance; + }; + + auto pause_action = [this, &pause_future]() { + if (m_use_maa_touch) { + constexpr int EscKeyCode = 111; + (void)m_minitoucher->key_down(EscKeyCode); + (void)m_minitoucher->key_up(EscKeyCode, 0); + } + else { + pause_future = std::async(std::launch::async, [this]() { press_esc(); }); + } + }; + auto minitouch_move = [&](int _x1, int _y1, int _x2, int _y2, int _duration) -> bool { - for (int cur_time = TimeInterval; cur_time < _duration; cur_time += TimeInterval) { - double progress = cubic_spline(slope_in, slope_out, static_cast(cur_time) / duration); - int cur_x = static_cast(std::lerp(_x1, _x2, progress)); - int cur_y = static_cast(std::lerp(_y1, _y2, progress)); - if (need_pause && std::sqrt(std::pow(cur_x - _x1, 2) + std::pow(cur_y - _y1, 2)) > - opt.swipe_with_pause_required_distance) { - need_pause = false; - if (m_use_maa_touch) { - constexpr int EscKeyCode = 111; - if (!m_minitoucher->key_down(EscKeyCode)) { - return false; - } - if (!m_minitoucher->key_up(EscKeyCode, 0)) { - return false; - } - } - else { - pause_future = std::async(std::launch::async, [&]() { press_esc(); }); - } - } - if (cur_x < 0 || cur_x > m_minitouch_props.max_x || cur_y < 0 || cur_y > m_minitouch_props.max_y) { - continue; - } - if (!m_minitoucher->move(cur_x, cur_y)) { - return false; - } + if (need_pause) { + bool result = interpolate_swipe_with_pause( + _x1, + _y1, + _x2, + _y2, + _duration, + TimeInterval, + slope_in, + slope_out, + move_func, + bounds_check, + pause_check, + [&]() { + need_pause = false; + pause_action(); + }); + return result; } - if (_x2 >= 0 && _x2 <= m_minitouch_props.max_x && _y2 >= 0 && _y2 <= m_minitouch_props.max_y) { - if (!m_minitoucher->move(_x2, _y2)) { - return false; - } + else { + return interpolate_swipe(_x1, _y1, _x2, _y2, _duration, TimeInterval, slope_in, slope_out, move_func, bounds_check); } - return true; }; if (!minitouch_move(x1, y1, x2, y2, duration ? duration : opt.minitouch_swipe_default_duration)) { @@ -235,7 +241,7 @@ bool asst::MinitouchController::swipe( if (extra_swipe && opt.minitouch_extra_swipe_duration > 0) { if (!m_minitoucher->wait(opt.minitouch_swipe_extra_end_delay)) { - return false; // 停留终点 + return false; } if (!minitouch_move(x2, y2, x2, y2 - opt.minitouch_extra_swipe_dist, opt.minitouch_extra_swipe_duration)) { return false; diff --git a/src/MaaCore/Controller/PlayToolsController.cpp b/src/MaaCore/Controller/PlayToolsController.cpp index 9cfd9f8505..254f0530ae 100644 --- a/src/MaaCore/Controller/PlayToolsController.cpp +++ b/src/MaaCore/Controller/PlayToolsController.cpp @@ -4,6 +4,7 @@ #include "Config/GeneralConfig.h" #include "MaaUtils/NoWarningCV.hpp" +#include "SwipeHelper.hpp" using boost::asio::ip::tcp; namespace socket_ops = boost::asio::detail::socket_ops; @@ -153,26 +154,15 @@ bool asst::PlayToolsController::swipe( toucher_down(p1); - auto cubic_spline = [](double slope_0, double slope_1, double t) { - const double a = slope_0; - const double b = -(2 * slope_0 + slope_1 - 3); - const double c = -(-slope_0 - slope_1 + 2); - return a * t + b * std::pow(t, 2) + c * std::pow(t, 3); - }; // TODO: move this to math.hpp + auto bounds_check = [width, height](int x, int y) { return x >= 0 && x <= width && y >= 0 && y <= height; }; - const auto progressive_move = [&](int _x1, int _y1, int _x2, int _y2, int _duration) { - for (int cur_time = DefaultSwipeDelay; cur_time < _duration; cur_time += DefaultSwipeDelay) { - double progress = cubic_spline(slope_in, slope_out, static_cast(cur_time) / duration); - int cur_x = static_cast(std::lerp(_x1, _x2, progress)); - int cur_y = static_cast(std::lerp(_y1, _y2, progress)); - if (cur_x < 0 || cur_x > width || cur_y < 0 || cur_y > height) { - continue; - } - toucher_move({ cur_x, cur_y }); - } - if (_x2 >= 0 && _x2 <= width && _y2 >= 0 && _y2 <= height) { - toucher_move({ _x2, _y2 }); - } + auto move_func = [this](int x, int y) { + toucher_move({ x, y }); + return true; + }; + + auto progressive_move = [&](int _x1, int _y1, int _x2, int _y2, int _duration) { + interpolate_swipe(_x1, _y1, _x2, _y2, _duration, DefaultSwipeDelay, slope_in, slope_out, move_func, bounds_check); }; const auto& opt = Config.get_options(); @@ -180,7 +170,7 @@ bool asst::PlayToolsController::swipe( progressive_move(x1, y1, x2, y2, duration ? duration : opt.minitouch_swipe_default_duration); if (extra_swipe && opt.minitouch_extra_swipe_duration > 0) { - toucher_wait(opt.minitouch_swipe_extra_end_delay); // 停留终点 + toucher_wait(opt.minitouch_swipe_extra_end_delay); progressive_move(x2, y2, x2, y2 - opt.minitouch_extra_swipe_dist, opt.minitouch_extra_swipe_duration); } diff --git a/src/MaaCore/Controller/SwipeHelper.hpp b/src/MaaCore/Controller/SwipeHelper.hpp new file mode 100644 index 0000000000..f4fc8e4136 --- /dev/null +++ b/src/MaaCore/Controller/SwipeHelper.hpp @@ -0,0 +1,137 @@ +#pragma once + +#include +#include + +#include "Common/AsstTypes.h" + +namespace asst +{ + +// 三次样条插值函数,用于生成平滑的滑动曲线 +// slope_0: 起点斜率,slope_1: 终点斜率,t: 插值进度 [0, 1] +inline double cubic_spline(double slope_0, double slope_1, double t) +{ + const double a = slope_0; + const double b = -(2 * slope_0 + slope_1 - 3); + const double c = -(-slope_0 - slope_1 + 2); + return a * t + b * std::pow(t, 2) + c * std::pow(t, 3); +} + +// 滑动插值执行器 +// MoveFunc: bool(int x, int y) - 移动到指定位置的回调,返回 false 表示失败 +// BoundsCheckFunc: bool(int x, int y) - 边界检查回调,返回 true 表示在边界内 +template +bool interpolate_swipe( + int x1, + int y1, + int x2, + int y2, + int duration, + int interval, + double slope_in, + double slope_out, + MoveFunc&& move_func, + BoundsCheckFunc&& bounds_check) +{ + for (int cur_time = interval; cur_time < duration; cur_time += interval) { + double progress = cubic_spline(slope_in, slope_out, static_cast(cur_time) / duration); + int cur_x = static_cast(std::lerp(static_cast(x1), static_cast(x2), progress)); + int cur_y = static_cast(std::lerp(static_cast(y1), static_cast(y2), progress)); + + if (!bounds_check(cur_x, cur_y)) { + continue; + } + + if (!move_func(cur_x, cur_y)) { + return false; + } + } + + // 确保到达终点(如果在边界内) + if (bounds_check(x2, y2)) { + if (!move_func(x2, y2)) { + return false; + } + } + + return true; +} + +// 简化版本:不需要边界检查 +template +bool interpolate_swipe( + int x1, + int y1, + int x2, + int y2, + int duration, + int interval, + double slope_in, + double slope_out, + MoveFunc&& move_func) +{ + return interpolate_swipe( + x1, + y1, + x2, + y2, + duration, + interval, + slope_in, + slope_out, + std::forward(move_func), + [](int, int) { return true; }); +} + +// 带暂停检测的滑动插值执行器(用于 MinitouchController 的 swipe_with_pause 功能) +// PauseCheckFunc: bool(int cur_x, int cur_y, int start_x, int start_y) - 检查是否需要暂停 +// PauseFunc: void() - 执行暂停操作 +template +bool interpolate_swipe_with_pause( + int x1, + int y1, + int x2, + int y2, + int duration, + int interval, + double slope_in, + double slope_out, + MoveFunc&& move_func, + BoundsCheckFunc&& bounds_check, + PauseCheckFunc&& pause_check, + PauseFunc&& pause_func) +{ + bool pause_triggered = false; + + for (int cur_time = interval; cur_time < duration; cur_time += interval) { + double progress = cubic_spline(slope_in, slope_out, static_cast(cur_time) / duration); + int cur_x = static_cast(std::lerp(static_cast(x1), static_cast(x2), progress)); + int cur_y = static_cast(std::lerp(static_cast(y1), static_cast(y2), progress)); + + // 检查是否需要触发暂停 + if (!pause_triggered && pause_check(cur_x, cur_y, x1, y1)) { + pause_triggered = true; + pause_func(); + } + + if (!bounds_check(cur_x, cur_y)) { + continue; + } + + if (!move_func(cur_x, cur_y)) { + return false; + } + } + + // 确保到达终点(如果在边界内) + if (bounds_check(x2, y2)) { + if (!move_func(x2, y2)) { + return false; + } + } + + return true; +} + +} // namespace asst diff --git a/src/MaaCore/Controller/Win32ControlUnitLoader.cpp b/src/MaaCore/Controller/Win32ControlUnitLoader.cpp new file mode 100644 index 0000000000..42629411e7 --- /dev/null +++ b/src/MaaCore/Controller/Win32ControlUnitLoader.cpp @@ -0,0 +1,119 @@ +#ifdef _WIN32 + +#include "Win32ControlUnitLoader.h" + +#include + +#include "Utils/Logger.hpp" + +namespace asst +{ + +Win32ControlUnitLoader::Win32ControlUnitLoader() = default; + +Win32ControlUnitLoader::~Win32ControlUnitLoader() +{ + unload(); +} + +bool Win32ControlUnitLoader::load(const std::filesystem::path& dll_path) +{ + LogTraceFunction; + + if (m_module) { + Log.warn("DLL already loaded"); + return true; + } + + std::filesystem::path full_path = dll_path; + if (!full_path.has_extension()) { + full_path += ".dll"; + } + + Log.info("Loading", full_path); + + m_module = LoadLibraryW(full_path.wstring().c_str()); + if (!m_module) { + DWORD error = GetLastError(); + Log.error("Failed to load DLL, error code:", error); + return false; + } + + m_get_version = reinterpret_cast(GetProcAddress(static_cast(m_module), "MaaWin32ControlUnitGetVersion")); + m_create = reinterpret_cast(GetProcAddress(static_cast(m_module), "MaaWin32ControlUnitCreate")); + m_destroy = reinterpret_cast(GetProcAddress(static_cast(m_module), "MaaWin32ControlUnitDestroy")); + + if (!m_create || !m_destroy) { + Log.error("Failed to get function pointers from DLL"); + unload(); + return false; + } + + if (m_get_version) { + Log.info("MaaWin32ControlUnit version:", m_get_version()); + } + + return true; +} + +void Win32ControlUnitLoader::unload() +{ + if (m_module) { + FreeLibrary(static_cast(m_module)); + m_module = nullptr; + } + + m_get_version = nullptr; + m_create = nullptr; + m_destroy = nullptr; +} + +const char* Win32ControlUnitLoader::get_version() const +{ + if (m_get_version) { + return m_get_version(); + } + return nullptr; +} + +void* Win32ControlUnitLoader::create( + void* hwnd, + Win32ScreencapMethod screencap_method, + Win32InputMethod mouse_method, + Win32InputMethod keyboard_method) +{ + LogTraceFunction; + + if (!m_create) { + Log.error("DLL not loaded or create function not available"); + return nullptr; + } + + void* handle = m_create(hwnd, screencap_method, mouse_method, keyboard_method); + if (!handle) { + Log.error("Failed to create Win32ControlUnit"); + return nullptr; + } + + Log.info("Created Win32ControlUnit:", reinterpret_cast(handle)); + return handle; +} + +void Win32ControlUnitLoader::destroy(void* handle) +{ + LogTraceFunction; + + if (!m_destroy) { + Log.error("DLL not loaded or destroy function not available"); + return; + } + + if (handle) { + m_destroy(handle); + Log.info("Destroyed Win32ControlUnit:", reinterpret_cast(handle)); + } +} + +} // namespace asst + +#endif // _WIN32 diff --git a/src/MaaCore/Controller/Win32ControlUnitLoader.h b/src/MaaCore/Controller/Win32ControlUnitLoader.h new file mode 100644 index 0000000000..25d6b1e828 --- /dev/null +++ b/src/MaaCore/Controller/Win32ControlUnitLoader.h @@ -0,0 +1,107 @@ +#pragma once + +#ifdef _WIN32 + +#include +#include +#include +#include + +#include "MaaUtils/SafeWindows.hpp" + +#include "Common/AsstTypes.h" +#include "MaaUtils/NoWarningCVMat.hpp" + +namespace asst +{ +// 与 MaaFramework 的 ControlUnitAPI 接口保持 ABI 兼容 +// 虚函数表布局必须与 MaaFramework 完全一致 +class MaaControlUnitInterface +{ +public: + virtual ~MaaControlUnitInterface() = default; + + virtual bool connect() = 0; + virtual bool connected() const = 0; + + virtual bool request_uuid(std::string& uuid) = 0; + virtual uint64_t get_features() const = 0; + + virtual bool start_app(const std::string& intent) = 0; + virtual bool stop_app(const std::string& intent) = 0; + + virtual bool screencap(cv::Mat& image) = 0; + + virtual bool click(int x, int y) = 0; + virtual bool swipe(int x1, int y1, int x2, int y2, int duration) = 0; + + virtual bool touch_down(int contact, int x, int y, int pressure) = 0; + virtual bool touch_move(int contact, int x, int y, int pressure) = 0; + virtual bool touch_up(int contact) = 0; + + virtual bool click_key(int key) = 0; + virtual bool input_text(const std::string& text) = 0; + + virtual bool key_down(int key) = 0; + virtual bool key_up(int key) = 0; + + virtual bool scroll(int dx, int dy) = 0; +}; + +// 与 MaaFramework 的 MaaControllerFeature 兼容的常量 +namespace MaaFeature +{ +constexpr uint64_t None = 0; +constexpr uint64_t UseMouseDownAndUpInsteadOfClick = 1ULL << 1; +constexpr uint64_t UseKeyboardDownAndUpInsteadOfClick = 1ULL << 2; +} // namespace MaaFeature + +// 动态加载 MaaWin32ControlUnit.dll +class Win32ControlUnitLoader +{ +public: + Win32ControlUnitLoader(); + ~Win32ControlUnitLoader(); + + Win32ControlUnitLoader(const Win32ControlUnitLoader&) = delete; + Win32ControlUnitLoader& operator=(const Win32ControlUnitLoader&) = delete; + Win32ControlUnitLoader(Win32ControlUnitLoader&&) = delete; + Win32ControlUnitLoader& operator=(Win32ControlUnitLoader&&) = delete; + + // 加载 DLL + bool load(const std::filesystem::path& dll_path); + + // 卸载 DLL + void unload(); + + // 是否已加载 + bool loaded() const noexcept { return m_module != nullptr; } + + // 获取版本 + const char* get_version() const; + + // 创建控制单元 + void* create( + void* hwnd, + Win32ScreencapMethod screencap_method, + Win32InputMethod mouse_method, + Win32InputMethod keyboard_method); + + // 销毁控制单元 + void destroy(void* handle); + +private: + void* m_module = nullptr; + + // 函数指针类型 + using GetVersionFunc = const char* (*)(); + using CreateFunc = void* (*)(void*, uint64_t, uint64_t, uint64_t); + using DestroyFunc = void (*)(void*); + + GetVersionFunc m_get_version = nullptr; + CreateFunc m_create = nullptr; + DestroyFunc m_destroy = nullptr; +}; +} // namespace asst + +#endif // _WIN32 diff --git a/src/MaaCore/Controller/Win32Controller.cpp b/src/MaaCore/Controller/Win32Controller.cpp new file mode 100644 index 0000000000..d10e4972d5 --- /dev/null +++ b/src/MaaCore/Controller/Win32Controller.cpp @@ -0,0 +1,383 @@ +#ifdef _WIN32 + +#include "Win32Controller.h" + +#include +#include + +#include "Config/GeneralConfig.h" +#include "SwipeHelper.hpp" +#include "Utils/Logger.hpp" +#include "Utils/WorkingDir.hpp" + +namespace asst +{ +Win32Controller::Win32Controller(const AsstCallback& callback, Assistant* inst) : + InstHelper(inst), + m_callback(callback), + m_loader(std::make_unique()) +{ + LogTraceFunction; +} + +Win32Controller::~Win32Controller() +{ + LogTraceFunction; + + if (m_unit_handle && m_loader) { + m_loader->destroy(m_unit_handle); + m_unit_handle = nullptr; + } +} + +bool Win32Controller::attach( + void* hwnd, + Win32ScreencapMethod screencap_method, + Win32InputMethod mouse_method, + Win32InputMethod keyboard_method) +{ + LogTraceFunction; + + m_inited = false; + m_hwnd = hwnd; + m_screencap_method = screencap_method; + m_mouse_method = mouse_method; + m_keyboard_method = keyboard_method; + + // 销毁旧的控制单元 + if (m_unit_handle && m_loader) { + m_loader->destroy(m_unit_handle); + m_unit_handle = nullptr; + } + + // 加载 DLL + if (!m_loader->loaded()) { + auto dll_path = "MaaWin32ControlUnit"; + if (!m_loader->load(dll_path)) { + Log.error("Failed to load MaaWin32ControlUnit.dll"); + return false; + } + } + + // 创建控制单元 + m_unit_handle = m_loader->create(hwnd, screencap_method, mouse_method, keyboard_method); + if (!m_unit_handle) { + Log.error("Failed to create Win32ControlUnit"); + return false; + } + + // 连接 + if (!unit_connect()) { + Log.error("Failed to connect Win32ControlUnit"); + m_loader->destroy(m_unit_handle); + m_unit_handle = nullptr; + return false; + } + + // 获取 UUID + auto* unit = static_cast(m_unit_handle); + if (!unit->request_uuid(m_uuid)) { + std::stringstream ss; + ss << hwnd; + m_uuid = ss.str(); + } + + // 尝试截图获取屏幕分辨率 + cv::Mat image; + if (unit_screencap(image)) { + m_screen_size = { image.cols, image.rows }; + Log.info("Screen size:", m_screen_size.first, "x", m_screen_size.second); + } + + m_inited = true; + return true; +} + +bool Win32Controller::connect( + const std::string& adb_path [[maybe_unused]], + const std::string& address [[maybe_unused]], + const std::string& config [[maybe_unused]]) +{ + Log.error("Win32Controller does not support connect(), use attach() instead"); + return false; +} + +bool Win32Controller::inited() const noexcept +{ + return m_inited && m_unit_handle; +} + +const std::string& Win32Controller::get_uuid() const +{ + return m_uuid; +} + +bool Win32Controller::screencap(cv::Mat& image_payload, bool allow_reconnect [[maybe_unused]]) +{ + LogTraceFunction; + + if (!unit_screencap(image_payload)) { + return false; + } + + if (m_screen_size.first == 0) { + m_screen_size = { image_payload.cols, image_payload.rows }; + } + + return true; +} + +bool Win32Controller::start_game(const std::string& client_type [[maybe_unused]]) +{ + Log.warn("start_game is not supported on Win32Controller"); + return false; +} + +bool Win32Controller::stop_game(const std::string& client_type [[maybe_unused]]) +{ + Log.warn("stop_game is not supported on Win32Controller"); + return false; +} + +bool Win32Controller::click(const Point& p) +{ + LogTraceFunction; + Log.trace("Win32Controller click:", p); + + // MaaWin32ControlUnit 返回 MaaControllerFeature_UseMouseDownAndUpInsteadOfClick + // 需要使用 touch_down/touch_up 替代 click + if (!unit_touch_down(0, p.x, p.y, 0)) { + return false; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + return unit_touch_up(0); +} + +bool Win32Controller::input(const std::string& text) +{ + LogTraceFunction; + return unit_input_text(text); +} + +bool Win32Controller::swipe( + const Point& p1, + const Point& p2, + int duration, + bool extra_swipe, + double slope_in, + double slope_out, + bool with_pause [[maybe_unused]]) +{ + LogTraceFunction; + + int x1 = p1.x, y1 = p1.y; + int x2 = p2.x, y2 = p2.y; + + const auto width = m_screen_size.first; + const auto height = m_screen_size.second; + + // 起点不能在屏幕外,但是终点可以 + if (width > 0 && height > 0) { + if (x1 < 0 || x1 >= width || y1 < 0 || y1 >= height) { + Log.warn("swipe point1 is out of range", x1, y1); + x1 = std::clamp(x1, 0, width - 1); + y1 = std::clamp(y1, 0, height - 1); + } + } + + Log.trace("Win32Controller swipe", p1, p2, duration, extra_swipe, slope_in, slope_out); + + // MaaWin32ControlUnit 返回 MaaControllerFeature_UseMouseDownAndUpInsteadOfClick + // 需要使用 touch_down/touch_move/touch_up 实现滑动 + if (!unit_touch_down(0, x1, y1, 0)) { + return false; + } + + const auto& opt = Config.get_options(); + int actual_duration = duration > 0 ? duration : opt.minitouch_swipe_default_duration; + + auto bounds_check = [width, height](int x, int y) { + if (width <= 0 || height <= 0) { + return true; + } + return x >= 0 && x <= width && y >= 0 && y <= height; + }; + + auto move_func = [this](int x, int y) { + return unit_touch_move(0, x, y, 0); + }; + + auto do_swipe = [&](int _x1, int _y1, int _x2, int _y2, int _duration) { + return interpolate_swipe( + _x1, + _y1, + _x2, + _y2, + _duration, + DefaultSwipeDelay, + slope_in, + slope_out, + move_func, + bounds_check); + }; + + if (!do_swipe(x1, y1, x2, y2, actual_duration)) { + unit_touch_up(0); + return false; + } + + if (extra_swipe && opt.minitouch_extra_swipe_duration > 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(opt.minitouch_swipe_extra_end_delay)); + do_swipe(x2, y2, x2, y2 - opt.minitouch_extra_swipe_dist, opt.minitouch_extra_swipe_duration); + } + + return unit_touch_up(0); +} + +bool Win32Controller::inject_input_event(const InputEvent& event) +{ + LogTraceFunction; + + switch (event.type) { + case InputEvent::Type::TOUCH_DOWN: + return unit_touch_down(event.pointerId, event.point.x, event.point.y, 0); + case InputEvent::Type::TOUCH_UP: + return unit_touch_up(event.pointerId); + case InputEvent::Type::TOUCH_MOVE: + return unit_touch_move(event.pointerId, event.point.x, event.point.y, 0); + case InputEvent::Type::KEY_DOWN: { + auto* unit = static_cast(m_unit_handle); + return unit ? unit->key_down(event.keycode) : false; + } + case InputEvent::Type::KEY_UP: { + auto* unit = static_cast(m_unit_handle); + return unit ? unit->key_up(event.keycode) : false; + } + case InputEvent::Type::WAIT_MS: + std::this_thread::sleep_for(std::chrono::milliseconds(event.milisec)); + return true; + case InputEvent::Type::TOUCH_RESET: + case InputEvent::Type::COMMIT: + return true; + case InputEvent::Type::UNKNOWN: + default: + Log.error("unknown input event type"); + return false; + } +} + +bool Win32Controller::press_esc() +{ + LogTraceFunction; + return unit_click_key(VK_ESCAPE); // VK_ESCAPE = 0x1B, defined in WinUser.h +} + +ControlFeat::Feat Win32Controller::support_features() const noexcept +{ + return ControlFeat::PRECISE_SWIPE; +} + +std::pair Win32Controller::get_screen_res() const noexcept +{ + return m_screen_size; +} + +void Win32Controller::callback(AsstMsg msg, const json::value& details) +{ + if (m_callback) { + m_callback(msg, details, m_inst); + } +} + +bool Win32Controller::unit_connect() +{ + auto* unit = static_cast(m_unit_handle); + if (!unit) { + return false; + } + return unit->connect(); +} + +bool Win32Controller::unit_screencap(cv::Mat& image) +{ + auto* unit = static_cast(m_unit_handle); + if (!unit) { + return false; + } + return unit->screencap(image); +} + +bool Win32Controller::unit_click(int x, int y) +{ + auto* unit = static_cast(m_unit_handle); + if (!unit) { + return false; + } + return unit->click(x, y); +} + +bool Win32Controller::unit_swipe(int x1, int y1, int x2, int y2, int duration) +{ + auto* unit = static_cast(m_unit_handle); + if (!unit) { + return false; + } + return unit->swipe(x1, y1, x2, y2, duration); +} + +bool Win32Controller::unit_touch_down(int contact, int x, int y, int pressure) +{ + auto* unit = static_cast(m_unit_handle); + if (!unit) { + return false; + } + return unit->touch_down(contact, x, y, pressure); +} + +bool Win32Controller::unit_touch_move(int contact, int x, int y, int pressure) +{ + auto* unit = static_cast(m_unit_handle); + if (!unit) { + return false; + } + return unit->touch_move(contact, x, y, pressure); +} + +bool Win32Controller::unit_touch_up(int contact) +{ + auto* unit = static_cast(m_unit_handle); + if (!unit) { + return false; + } + return unit->touch_up(contact); +} + +bool Win32Controller::unit_input_text(const std::string& text) +{ + auto* unit = static_cast(m_unit_handle); + if (!unit) { + return false; + } + return unit->input_text(text); +} + +bool Win32Controller::unit_click_key(int key) +{ + auto* unit = static_cast(m_unit_handle); + if (!unit) { + return false; + } + + // MaaWin32ControlUnit 返回 MaaControllerFeature_UseKeyboardDownAndUpInsteadOfClick + // 需要使用 key_down/key_up 替代 click_key + if (!unit->key_down(key)) { + return false; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + return unit->key_up(key); +} +} // namespace asst + +#endif // _WIN32 diff --git a/src/MaaCore/Controller/Win32Controller.h b/src/MaaCore/Controller/Win32Controller.h new file mode 100644 index 0000000000..c248d02b29 --- /dev/null +++ b/src/MaaCore/Controller/Win32Controller.h @@ -0,0 +1,102 @@ +#pragma once + +#ifdef _WIN32 + +#include +#include + +#include "MaaUtils/SafeWindows.hpp" + +#include "Common/AsstMsg.h" +#include "ControllerAPI.h" +#include "InstHelper.h" +#include "Win32ControlUnitLoader.h" + +namespace asst +{ +class Assistant; + +class Win32Controller : public ControllerAPI, private InstHelper +{ +public: + Win32Controller(const AsstCallback& callback, Assistant* inst); + virtual ~Win32Controller() override; + + Win32Controller(const Win32Controller&) = delete; + Win32Controller& operator=(const Win32Controller&) = delete; + Win32Controller(Win32Controller&&) = delete; + Win32Controller& operator=(Win32Controller&&) = delete; + + // 绑定到窗口(替代 connect) + bool attach( + void* hwnd, + Win32ScreencapMethod screencap_method, + Win32InputMethod mouse_method, + Win32InputMethod keyboard_method); + +public: // ControllerAPI 接口 + virtual bool connect(const std::string& adb_path, const std::string& address, const std::string& config) override; + virtual bool inited() const noexcept override; + + virtual const std::string& get_uuid() const override; + + virtual size_t get_pipe_data_size() const noexcept override { return 0; } + + virtual size_t get_version() const noexcept override { return 0; } + + virtual bool screencap(cv::Mat& image_payload, bool allow_reconnect = false) override; + + virtual bool start_game(const std::string& client_type) override; + virtual bool stop_game(const std::string& client_type) override; + + virtual bool click(const Point& p) override; + virtual bool input(const std::string& text) override; + virtual bool swipe( + const Point& p1, + const Point& p2, + int duration = 0, + bool extra_swipe = false, + double slope_in = 1, + double slope_out = 1, + bool with_pause = false) override; + + virtual bool inject_input_event(const InputEvent& event) override; + + virtual bool press_esc() override; + virtual ControlFeat::Feat support_features() const noexcept override; + + virtual std::pair get_screen_res() const noexcept override; + +private: + void callback(AsstMsg msg, const json::value& details); + + // 封装 MaaWin32ControlUnit 的调用 + bool unit_connect(); + bool unit_screencap(cv::Mat& image); + bool unit_click(int x, int y); + bool unit_swipe(int x1, int y1, int x2, int y2, int duration); + bool unit_touch_down(int contact, int x, int y, int pressure); + bool unit_touch_move(int contact, int x, int y, int pressure); + bool unit_touch_up(int contact); + bool unit_input_text(const std::string& text); + bool unit_click_key(int key); + +private: + static constexpr int DefaultSwipeDelay = 10; // ms + + AsstCallback m_callback = nullptr; + std::unique_ptr m_loader; + void* m_unit_handle = nullptr; + void* m_hwnd = nullptr; + + bool m_inited = false; + std::string m_uuid; + std::pair m_screen_size = { 0, 0 }; + + Win32ScreencapMethod m_screencap_method = Win32Screencap::None; + Win32InputMethod m_mouse_method = Win32Input::None; + Win32InputMethod m_keyboard_method = Win32Input::None; +}; +} // namespace asst + +#endif // _WIN32 diff --git a/src/MaaWpfGui/Constants/ConfigurationKeys.cs b/src/MaaWpfGui/Constants/ConfigurationKeys.cs index 76b20edef9..ed5c0d8979 100644 --- a/src/MaaWpfGui/Constants/ConfigurationKeys.cs +++ b/src/MaaWpfGui/Constants/ConfigurationKeys.cs @@ -87,6 +87,12 @@ public static class ConfigurationKeys public const string TouchMode = "Connect.TouchMode"; public const string AdbReplaced = "Connect.AdbReplaced"; + // AttachWindow (Win32窗口绑定) 配置 + public const string UseAttachWindow = "Connect.UseAttachWindow"; + public const string AttachWindowScreencapMethod = "Connect.AttachWindow.ScreencapMethod"; + public const string AttachWindowMouseMethod = "Connect.AttachWindow.MouseMethod"; + public const string AttachWindowKeyboardMethod = "Connect.AttachWindow.KeyboardMethod"; + public const string StartGame = "Start.StartGame"; public const string ClientType = "Start.ClientType"; public const string AccountName = "Start.AccountName"; diff --git a/src/MaaWpfGui/Main/AsstProxy.cs b/src/MaaWpfGui/Main/AsstProxy.cs index 11efc99a5c..e942fd91a6 100644 --- a/src/MaaWpfGui/Main/AsstProxy.cs +++ b/src/MaaWpfGui/Main/AsstProxy.cs @@ -48,6 +48,7 @@ using Newtonsoft.Json.Linq; using ObservableCollections; using Serilog; using Stylet; +using Windows.Win32; using static MaaWpfGui.Helper.Instances.Data; using AsstHandle = nint; using AsstInstanceOptionKey = System.Int32; @@ -140,6 +141,16 @@ public class AsstProxy } } + private static bool AsstAttachWindow(AsstHandle handle, IntPtr hwnd, ulong screencapMethod, ulong mouseMethod, ulong keyboardMethod) + { + _logger.Information("handle: {Handle}, hwnd: {Hwnd}, screencapMethod: {ScreencapMethod}, mouseMethod: {MouseMethod}, keyboardMethod: {KeyboardMethod}", + (long)handle, hwnd, screencapMethod, mouseMethod, keyboardMethod); + + bool ret = MaaService.AsstAttachWindow(handle, hwnd, screencapMethod, mouseMethod, keyboardMethod); + _logger.Information("handle: {Handle}, hwnd: {Hwnd}, return: {Ret}", (long)handle, hwnd, ret); + return ret; + } + private static void AsstSetConnectionExtrasMuMu12(string extras) { AsstSetConnectionExtras("MuMuEmulator12", extras); @@ -475,6 +486,13 @@ public class AsstProxy loaded &= LoadResIfExists(globalRes) && LoadResIfExists(globalCacheRes); } + // 使用窗口绑定模式时,额外加载 PC 平台差异资源 + if (SettingsViewModel.ConnectSettings.UseAttachWindow) + { + string pcPlatformRes = Path.Combine(mainRes, "platform_diff", "PC", "resource"); + loaded &= LoadResIfExists(pcPlatformRes); + } + return loaded; static bool LoadResIfExists(string path) @@ -1656,6 +1674,7 @@ public class AsstProxy AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.ClueUseGroup); AchievementTrackerHelper.Instance.ClueObsessionAdd(); break; + case "SendClues": AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.ClueSendGroup); break; @@ -1709,11 +1728,11 @@ public class AsstProxy ProcRecruitCalcMsg(details); break; - /* - case "VideoRecognition": - ProcVideoRecMsg(details); - break; - */ + /* + case "VideoRecognition": + ProcVideoRecMsg(details); + break; + */ } var subTaskDetails = details["details"]; @@ -2337,6 +2356,121 @@ public class AsstProxy /// 具体的连接错误。 /// 是否成功。 public bool AsstConnect(ref string error) + { + // 如果启用了 AttachWindow 模式,则使用窗口绑定而非 ADB 连接 + if (SettingsViewModel.ConnectSettings.UseAttachWindow) + { + return AsstAttachWindowConnect(ref error); + } + + return AsstAdbConnect(ref error); + } + + /// + /// 搜索指定窗口标题的窗口句柄。 + /// + /// 窗口标题(完全匹配)。 + /// 找到的窗口句柄列表。 + private static List FindWindowsByName(string windowName) + { + var results = new List(); + + PInvoke.EnumWindows((hWnd, lParam) => { + if (!PInvoke.IsWindowVisible(hWnd)) + { + return true; + } + + var len = PInvoke.GetWindowTextLength(hWnd); + if (len <= 0) + { + return true; + } + + Span buffer = len <= 1024 ? stackalloc char[len + 1] : new char[len + 1]; + int written = PInvoke.GetWindowText(hWnd, buffer); + var title = new string(buffer[..Math.Max(0, Math.Min(written, buffer.Length))]); + + if (title == windowName) + { + results.Add((IntPtr)hWnd); + } + + return true; + }, IntPtr.Zero); + + return results; + } + + /// + /// 通过 AttachWindow 绑定 Win32 窗口。 + /// 自动搜索 "明日方舟" 窗口。 + /// + /// 具体的连接错误。 + /// 是否成功。 + private bool AsstAttachWindowConnect(ref string error) + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("UseAttachWindowWarning"), UiLogColor.Warning); + + const string TargetWindowName = "明日方舟"; + var foundWindows = FindWindowsByName(TargetWindowName); + + if (foundWindows.Count == 0) + { + error = string.Format(LocalizationHelper.GetString("AttachWindowNotFound"), TargetWindowName); + Instances.TaskQueueViewModel.AddLog(error, UiLogColor.Error); + _logger.Warning("AttachWindow: No window found with name {WindowName}", TargetWindowName); + return false; + } + + var hwnd = foundWindows[0]; + + if (foundWindows.Count > 1) + { + // 找到多个窗口,使用第一个并记录日志 + var multipleMsg = string.Format(LocalizationHelper.GetString("AttachWindowMultipleFound"), foundWindows.Count, TargetWindowName); + Instances.TaskQueueViewModel.AddLog(multipleMsg, UiLogColor.Info); + _logger.Warning("AttachWindow: Multiple windows found with name {WindowName}, count: {Count}, using first one: {Hwnd}", TargetWindowName, foundWindows.Count, hwnd); + } + else + { + var foundMsg = string.Format(LocalizationHelper.GetString("AttachWindowFound"), TargetWindowName); + Instances.TaskQueueViewModel.AddLog(foundMsg, UiLogColor.Info); + _logger.Information("AttachWindow: Found window \"{WindowName}\" with HWND: {Hwnd}", TargetWindowName, hwnd); + } + + if (!ulong.TryParse(SettingsViewModel.ConnectSettings.AttachWindowScreencapMethod, out var screencapMethod)) + { + screencapMethod = 2; // 默认 FramePool + } + + if (!ulong.TryParse(SettingsViewModel.ConnectSettings.AttachWindowMouseMethod, out var mouseMethod)) + { + mouseMethod = 64; // 默认 PostMessageWithCursorPos + } + + if (!ulong.TryParse(SettingsViewModel.ConnectSettings.AttachWindowKeyboardMethod, out var keyboardMethod)) + { + keyboardMethod = 64; // 默认 PostMessageWithCursorPos + } + + bool ret = AsstAttachWindow(_handle, hwnd, screencapMethod, mouseMethod, keyboardMethod); + + if (!ret) + { + error = LocalizationHelper.GetString("AttachWindowFailed"); + Instances.TaskQueueViewModel.AddLog(error, UiLogColor.Error); + } + + return ret; + } + + /// + /// 通过 ADB 连接模拟器。 + /// + /// 具体的连接错误。 + /// 是否成功。 + private bool AsstAdbConnect(ref string error) { switch (SettingsViewModel.ConnectSettings.ConnectConfig) { diff --git a/src/MaaWpfGui/Res/Localizations/en-us.xaml b/src/MaaWpfGui/Res/Localizations/en-us.xaml index 0897faf444..d15375c61a 100644 --- a/src/MaaWpfGui/Res/Localizations/en-us.xaml +++ b/src/MaaWpfGui/Res/Localizations/en-us.xaml @@ -174,6 +174,7 @@ To customize rotation schedules, please use 「{key=InfrastModeCustom}」.Nox MEmu Old version of WSA + PC Client MaaTouch Compatible Mode 2nd Resolution @@ -1213,6 +1214,31 @@ Right-click to clear inactive jobs Trying to restart the ADB Server Trying to kill and restart the ADB process Please 「Check connection settings」 → 「Try restarting the emulator and ADB」 → 「Restart the computer」 + Window "{0}" not found, please ensure the game is running + Found {0} "{1}" windows, using the first one + Found window "{0}" + AttachWindow failed, please check screencap/input method settings + Use Win32 window binding for Arknights PC client instead of ADB connection for emulators or phones + PC connection (experimental feature, stability not guaranteed) +MAA does not provide maintenance or compatibility support for PC usage: +● If the feature works properly, you may choose to continue using it +● If it fails to run or has issues, PC-related issues will not be accepted +● Community adaptations and Pull Requests are welcome + +Before use, please ensure: +1. MAA is launched with administrator privileges +2. The game window is not minimized +3. Arknights Settings → Game → UI Scaling is set to 100% + Screencap Method + Mouse Input Method + Keyboard Input Method + FramePool (Default, Background) + PrintWindow (Background, Backup 1) + ScreenDC (Background, Backup 2) + DesktopWindow (Foreground, More Stable) + Seize (Foreground, More Stable) + PostMessageWithCursor (Semi-background) + SendMessageWithCursor (Semi-background, Backup) Force display of MAA diff --git a/src/MaaWpfGui/Res/Localizations/ja-jp.xaml b/src/MaaWpfGui/Res/Localizations/ja-jp.xaml index 6ae77be66b..35c1a6ccde 100644 --- a/src/MaaWpfGui/Res/Localizations/ja-jp.xaml +++ b/src/MaaWpfGui/Res/Localizations/ja-jp.xaml @@ -174,6 +174,7 @@ NoxPlayer MEmu 古いバージョンのWSA + PC クライアント MaaTouch 互換モード 2番目の決議 @@ -1214,6 +1215,31 @@ C:\\leidian\\LDPlayer9 ADBサーバーの再起動を試みています ADBプロセスの再起動を試みています 「接続設定を確認」→「エミュレーターとADBを再起動してみる」→「コンピュータを再起動」してください + ウィンドウ "{0}" が見つかりません。ゲームが起動していることを確認してください + {0} 個の "{1}" ウィンドウが見つかりました。最初のものを使用します + ウィンドウ "{0}" が見つかりました + AttachWindow の接続に失敗しました。スクリーンキャプチャ/入力方法の設定を確認してください + エミュレータや携帯電話への ADB 接続ではなく、アークナイツ PC クライアントに Win32 ウィンドウバインディングを使用します + PC 接続(実験的機能・動作の安定性は保証されません) +MAA は PC 環境での動作について、保守や互換対応を行いません: +● 正常に動作する場合は、自己判断でご利用ください +● 起動不可または問題が発生した場合、PC 関連の Issue は受け付けません +● コミュニティによる対応および Pull Request は歓迎します + +使用前にご確認ください: +1. MAA を管理者権限で起動してください +2. ゲームウィンドウを最小化しないでください +3. アークナイツ設定 → ゲーム → UI スケーリングを 100% に設定してください + スクリーンキャプチャ方式 + マウス入力方式 + キーボード入力方式 + FramePool(デフォルト、バックグラウンド) + PrintWindow(バックグラウンド、予備1) + ScreenDC(バックグラウンド、予備2) + DesktopWindow(フォアグラウンド、より安定) + Seize(フォアグラウンド、より安定) + PostMessageWithCursor(半バックグラウンド) + SendMessageWithCursor(半バックグラウンド、予備) MAAの強制表示 diff --git a/src/MaaWpfGui/Res/Localizations/ko-kr.xaml b/src/MaaWpfGui/Res/Localizations/ko-kr.xaml index c934ee7c53..c0429caf38 100644 --- a/src/MaaWpfGui/Res/Localizations/ko-kr.xaml +++ b/src/MaaWpfGui/Res/Localizations/ko-kr.xaml @@ -174,6 +174,7 @@ Nox MEmu WSA 레거시 버전 + PC 클라이언트 MaaTouch 호환 모드 분할 화면 @@ -1215,6 +1216,31 @@ C:\\leidian\\LDPlayer9 ADB 서버 재시작을 시도 중입니다 ADB 프로세스 재시작을 시도 중입니다 「연결 설정 확인」 → 「에뮬레이터와 ADB 재시작 시도」 → 「컴퓨터 재부팅」 해 주세요 + 창 "{0}"을(를) 찾을 수 없습니다. 게임이 실행 중인지 확인하세요 + {0}개의 "{1}" 창을 찾았습니다. 첫 번째 창을 사용합니다 + 창 "{0}"을(를) 찾았습니다 + AttachWindow 연결에 실패했습니다. 스크린샷/입력 방법 설정을 확인하세요 + 에뮬레이터나 휴대폰용 ADB 연결 대신 명일방주 PC 클라이언트에 Win32 창 바인딩을 사용합니다 + PC 연결 (실험적 기능이며 안정성은 보장되지 않습니다) +MAA는 PC 환경에서의 동작에 대해 유지보수 또는 호환 지원을 제공하지 않습니다: +● 정상적으로 동작하는 경우에 한해 사용자 판단하에 사용할 수 있습니다 +● 실행되지 않거나 문제가 발생할 경우 PC 관련 Issue는 접수하지 않습니다 +● 커뮤니티의 자체 대응 및 Pull Request 제출은 환영합니다 + +사용 전 확인 사항: +1. MAA를 관리자 권한으로 실행해야 합니다 +2. 게임 창을 최소화하면 안 됩니다 +3. 명일방주 설정 → 게임 → UI 배율을 100%로 설정해야 합니다 + 스크린샷 방식 + 마우스 입력 방식 + 키보드 입력 방식 + FramePool (기본값, 백그라운드) + PrintWindow (백그라운드, 백업 1) + ScreenDC (백그라운드, 백업 2) + DesktopWindow (포그라운드, 더 안정적) + Seize (포그라운드, 더 안정적) + PostMessageWithCursor (반 백그라운드) + SendMessageWithCursor (반 백그라운드, 백업) MAA 강제 표시 diff --git a/src/MaaWpfGui/Res/Localizations/zh-cn.xaml b/src/MaaWpfGui/Res/Localizations/zh-cn.xaml index 9b254db13f..911fb59df8 100644 --- a/src/MaaWpfGui/Res/Localizations/zh-cn.xaml +++ b/src/MaaWpfGui/Res/Localizations/zh-cn.xaml @@ -174,6 +174,7 @@ 夜神模拟器 逍遥模拟器 WSA 旧版本 + PC 端 MaaTouch 兼容模式 第二分辨率 @@ -1214,6 +1215,31 @@ C:\\leidian\\LDPlayer9。\n 正在尝试重启 ADB Server 正在尝试关闭并重启 ADB 进程 请 「检查连接设置」 → 「尝试重启模拟器与 ADB」 → 「重启电脑」 + 未找到窗口 "{0}",请确保游戏已启动 + 找到 {0} 个 "{1}" 窗口,将使用第一个 + 找到窗口 "{0}" + AttachWindow 绑定窗口失败,请检查截图/输入方式配置 + 使用 Win32 窗口绑定明日方舟 PC 端,而非 ADB 连接模拟器或手机 + 连接 PC 端(实验性功能,稳定性无法保证) +MAA 不对 PC 端运行情况提供维护或适配支持: +● 功能可正常使用的情况下,可自行选择继续使用 +● 若无法运行或存在问题,不受理 PC 端相关的 Issue +● 欢迎社区自行适配并提交 Pull Request + +使用前请确认: +1. 需要以管理员身份启动 MAA +2. 游戏窗口不能最小化 +3. 明日方舟设置 → 游戏 → UI 缩放需设为 100% + 截图方式 + 鼠标输入 + 键盘输入 + FramePool(默认,后台) + PrintWindow(后台,备用1) + ScreenDC(后台,备用2) + DesktopWindow(前台,更稳定) + Seize(前台,更稳定) + PostMessageWithCursor(半后台) + SendMessageWithCursor(半后台,备用) 强制显示MAA diff --git a/src/MaaWpfGui/Res/Localizations/zh-tw.xaml b/src/MaaWpfGui/Res/Localizations/zh-tw.xaml index 0086f3b5c4..f225bbb7ac 100644 --- a/src/MaaWpfGui/Res/Localizations/zh-tw.xaml +++ b/src/MaaWpfGui/Res/Localizations/zh-tw.xaml @@ -174,6 +174,7 @@ 夜神模擬器 逍遙模擬器 WSA 舊版本 + PC 端 MaaTouch 相容模式 第二解析度 @@ -1214,6 +1215,31 @@ C:\\leidian\\LDPlayer9。\n 正在嘗試重開 ADB 伺服器 正在嘗試重開 ADB 程式 請 「檢查連接設定」 → 「嘗試重開模擬器與 ADB」 → 「重開電腦」 + 未找到視窗 "{0}",請確保遊戲已啟動 + 找到 {0} 個 "{1}" 視窗,將使用第一個 + 找到視窗 "{0}" + AttachWindow 綁定視窗失敗,請檢查截圖/輸入方式配置 + 使用 Win32 視窗綁定明日方舟 PC 端,而非 ADB 連接模擬器或手機 + 連接 PC 端(實驗性功能,穩定性無法保證) +MAA 不對 PC 端的運行情況提供維護或適配支援: +● 在功能可正常使用的情況下,可自行選擇繼續使用 +● 若無法運行或出現問題,不受理 PC 端相關的 Issue +● 歡迎社群自行適配並提交 Pull Request + +使用前請確認: +1. 需以系統管理員身分啟動 MAA +2. 遊戲視窗不可最小化 +3. 明日方舟設定 → 遊戲 → UI 縮放需設為 100% + 截圖方式 + 滑鼠輸入 + 鍵盤輸入 + FramePool(預設,背景) + PrintWindow(背景,備用1) + ScreenDC(背景,備用2) + DesktopWindow(前台,更穩定) + Seize(前台,更穩定) + PostMessageWithCursor(半背景) + SendMessageWithCursor(半背景,備用) 強制顯示 MAA diff --git a/src/MaaWpfGui/Services/MaaService.cs b/src/MaaWpfGui/Services/MaaService.cs index f746dd5607..4608962a94 100644 --- a/src/MaaWpfGui/Services/MaaService.cs +++ b/src/MaaWpfGui/Services/MaaService.cs @@ -54,6 +54,10 @@ internal static partial class MaaService [return: MarshalAs(UnmanagedType.Bool)] internal static unsafe partial bool AsstConnect(AsstHandle handle, byte* adbPath, byte* address, byte* config); + [LibraryImport("MaaCore.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool AsstAttachWindow(AsstHandle handle, IntPtr hwnd, ulong screencapMethod, ulong mouseMethod, ulong keyboardMethod); + [LibraryImport("MaaCore.dll")] internal static unsafe partial AsstTaskId AsstAppendTask(AsstHandle handle, byte* type, byte* taskParams); diff --git a/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs b/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs index 66f7e6083f..64bfa649a2 100644 --- a/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs +++ b/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs @@ -1368,6 +1368,14 @@ public class TaskQueueViewModel : Screen string errMsg = string.Empty; bool connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + if (!connected && SettingsViewModel.ConnectSettings.UseAttachWindow) + { + AddLog(errMsg, UiLogColor.Error); + _runningState.SetIdle(true); + SetStopped(); + return false; + } + // 尝试启动模拟器 if (!connected && SettingsViewModel.ConnectSettings.RetryOnDisconnected) { diff --git a/src/MaaWpfGui/ViewModels/UserControl/Settings/ConnectSettingsUserControlModel.cs b/src/MaaWpfGui/ViewModels/UserControl/Settings/ConnectSettingsUserControlModel.cs index daf3ecbe32..6198fd6db4 100644 --- a/src/MaaWpfGui/ViewModels/UserControl/Settings/ConnectSettingsUserControlModel.cs +++ b/src/MaaWpfGui/ViewModels/UserControl/Settings/ConnectSettingsUserControlModel.cs @@ -29,6 +29,7 @@ using MaaWpfGui.Constants; using MaaWpfGui.Helper; using MaaWpfGui.Main; using MaaWpfGui.States; +using MaaWpfGui.Utilities; using MaaWpfGui.Utilities.ValueType; using MaaWpfGui.ViewModels.UI; using MaaWpfGui.WineCompat; @@ -52,6 +53,11 @@ public class ConnectSettingsUserControlModel : PropertyChangedBase Instance = new(); } + private ConnectSettingsUserControlModel() + { + PropertyDependsOnUtility.InitializePropertyDependencies(this); + } + public static ConnectSettingsUserControlModel Instance { get; } private static readonly ILogger _logger = Log.ForContext(); @@ -69,6 +75,7 @@ public class ConnectSettingsUserControlModel : PropertyChangedBase new() { Display = LocalizationHelper.GetString("LDPlayer"), Value = "LDPlayer" }, new() { Display = LocalizationHelper.GetString("Nox"), Value = "Nox" }, new() { Display = LocalizationHelper.GetString("XYAZ"), Value = "XYAZ" }, + new() { Display = LocalizationHelper.GetString("PC"), Value = "PC" }, new() { Display = LocalizationHelper.GetString("WSA"), Value = "WSA" }, new() { Display = LocalizationHelper.GetString("Compatible"), Value = "Compatible" }, new() { Display = LocalizationHelper.GetString("SecondResolution"), Value = "SecondResolution" }, @@ -1193,4 +1200,80 @@ public class ConnectSettingsUserControlModel : PropertyChangedBase } public bool AdbReplaced { get; set; } = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.AdbReplaced, bool.FalseString)); + + #region AttachWindow (Win32窗口绑定) 配置 + + /// + /// Gets a value indicating whether to use AttachWindow mode instead of ADB connection. + /// + [PropertyDependsOn(nameof(ConnectConfig))] + public bool UseAttachWindow => ConnectConfig == "PC"; + + /// + /// Gets win32 截图方式枚举(与 AsstCaller.h 中 AsstWin32ScreencapMethodEnum 对应) + /// + public List AttachWindowScreencapMethodList { get; } = + [ + new() { Display = LocalizationHelper.GetString("AttachWindowScreencapFramePool"), Value = "2" }, + new() { Display = LocalizationHelper.GetString("AttachWindowScreencapPrintWindow"), Value = "16" }, + new() { Display = LocalizationHelper.GetString("AttachWindowScreencapScreenDC"), Value = "32" }, + new() { Display = LocalizationHelper.GetString("AttachWindowScreencapDesktopDupWindow"), Value = "8" }, + ]; + + private string _attachWindowScreencapMethod = ConfigurationHelper.GetValue(ConfigurationKeys.AttachWindowScreencapMethod, "2"); // 默认 FramePool + + /// + /// Gets or sets the screencap method for AttachWindow mode. + /// + public string AttachWindowScreencapMethod + { + get => _attachWindowScreencapMethod; + set { + Instances.AsstProxy.Connected = false; + SetAndNotify(ref _attachWindowScreencapMethod, value); + ConfigurationHelper.SetValue(ConfigurationKeys.AttachWindowScreencapMethod, value); + } + } + + /// + /// Win32 输入方式枚举(与 AsstCaller.h 中 AsstWin32InputMethodEnum 对应) + /// + public List AttachWindowInputMethodList { get; } = + [ + new() { Display = LocalizationHelper.GetString("AttachWindowInputSeize"), Value = "1" }, + new() { Display = LocalizationHelper.GetString("AttachWindowInputPostWithCursor"), Value = "64" }, + new() { Display = LocalizationHelper.GetString("AttachWindowInputSendWithCursor"), Value = "32" }, + ]; + + private string _attachWindowMouseMethod = ConfigurationHelper.GetValue(ConfigurationKeys.AttachWindowMouseMethod, "64"); // 默认 PostMessageWithCursorPos + + /// + /// Gets or sets the mouse input method for AttachWindow mode. + /// + public string AttachWindowMouseMethod + { + get => _attachWindowMouseMethod; + set { + Instances.AsstProxy.Connected = false; + SetAndNotify(ref _attachWindowMouseMethod, value); + ConfigurationHelper.SetValue(ConfigurationKeys.AttachWindowMouseMethod, value); + } + } + + private string _attachWindowKeyboardMethod = ConfigurationHelper.GetValue(ConfigurationKeys.AttachWindowKeyboardMethod, "64"); // 默认 PostMessageWithCursorPos + + /// + /// Gets or sets the keyboard input method for AttachWindow mode. + /// + public string AttachWindowKeyboardMethod + { + get => _attachWindowKeyboardMethod; + set { + Instances.AsstProxy.Connected = false; + SetAndNotify(ref _attachWindowKeyboardMethod, value); + ConfigurationHelper.SetValue(ConfigurationKeys.AttachWindowKeyboardMethod, value); + } + } + + #endregion } diff --git a/src/MaaWpfGui/Views/UserControl/GuideUserControl.xaml b/src/MaaWpfGui/Views/UserControl/GuideUserControl.xaml index a8cdd05516..5a8adb671b 100644 --- a/src/MaaWpfGui/Views/UserControl/GuideUserControl.xaml +++ b/src/MaaWpfGui/Views/UserControl/GuideUserControl.xaml @@ -151,8 +151,10 @@ Block.TextAlignment="Left" Foreground="{DynamicResource InfoBrush}" Text="{DynamicResource GuideStepClarification}" - TextWrapping="Wrap" /> - + TextWrapping="Wrap" + Visibility="{c:Binding !UseAttachWindow}" /> + + - - - - - - - - - - - -