mirror of
https://github.com/MaaAssistantArknights/MaaAssistantArknights.git
synced 2026-07-01 01:10:34 +08:00
feat: support native android (#16179)
* feat: add android controller support
* feat: add android compile options and add core static options
* fix: add __ANDROID__ macro for Android platform
* fix: move switch break into macro conditional block
* ci: add Android CMake presets (arm64, x64)
* fix: add missing Android TouchMode handling
* fix: remove unnecessary pre-check in android
* fix: restore unnecessary modifications
* fix: unified use __ANDROID__
* refactor: use interpolate_swipe_with_pause in AndroidController
* feat: add Android crash logcat output and native backtrace
* feat: only copy once from external lib screencap
* feat: add Android JNI AttachThread/DetachThread to working_proc
* fix: break control in switch
* fix: update click interval & swipe interval
* ci: restore Android build job with MaaFramework download
* fix: remove thread attach
* ci: temporarily remove MaaAndroidNativeControlUnit.so
* feat(android-ctrl): SWIPE_WITH_PAUSE 由 InstanceOption DeploymentWithPause 控制
* feat: SWIPE_WITH_PAUSE 默认启用
* feat: add android compile options and add core static options
* feat: use maafw controll unit
* fix: minor adjustments
* refactor: adapt maafw android lib
* fix: remove screencap when connect
* chore: change maafw lib use
* feat: adapt maafw control unit click & click_key
* fix: address critical issues in Android native controller
- Restore InstanceOptionKey::ClientType case in set_instance_option,
which was accidentally removed and broke client type setting on all platforms
- Enable libMaaAndroidNativeControlUnit.so copy step in CI (was commented out),
so Android artifacts actually include the required control unit library
- Fail connect() early when screen_resolution is missing or invalid in config,
preventing silent {0,0} resolution that would break swipe and screencap
- Check touch_down() return value in swipe() and abort on failure
* fix: correct bounds_check off-by-one and extract KEYCODE_ESCAPE constant
- bounds_check used <= which allowed coordinates equal to screen width/height
(off-screen); use < to match the valid range [0, dimension-1]
- Extract magic number 111 into KEYCODE_ESCAPE constexpr in press_esc()
* fix: avoid heap allocation in noexcept terminate/signal handlers
format_signal_reason was returning std::string and using std::format in
its default branch. Called from noexcept custom_terminate_handler before
the outer try block, any allocation failure would throw std::bad_alloc,
triggering recursive std::terminate and aborting crash reporting.
- Change return type to const char* noexcept, returning string literals
- Replace std::format("Signal {}", sig) with "Unknown Signal" (only the
four registered signals are ever passed, so this branch is unreachable)
- Change signal_info in custom_terminate_handler from std::string to
const char*, eliminating all allocations on the pre-try path
* fix: suppress unused-variable warning for signal_reason on non-Android
This commit is contained in:
74
.github/workflows/ci.yml
vendored
74
.github/workflows/ci.yml
vendored
@@ -349,6 +349,78 @@ jobs:
|
|||||||
release/*.AppImage
|
release/*.AppImage
|
||||||
release/*.tar.gz
|
release/*.tar.gz
|
||||||
|
|
||||||
|
android:
|
||||||
|
name: Build for Android
|
||||||
|
needs: meta
|
||||||
|
runs-on: macos-26
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: [arm64, x64]
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Fetch submodules
|
||||||
|
run: |
|
||||||
|
git submodule update --init --depth 1 src/MaaUtils
|
||||||
|
|
||||||
|
- name: Cache MaaDeps
|
||||||
|
id: cache-maadeps
|
||||||
|
uses: actions/cache@v5
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
path: ./src/MaaUtils/MaaDeps
|
||||||
|
key: ${{ runner.os }}-${{ matrix.arch }}-android-maadeps-${{ hashFiles('tools/maadeps-download.py') }}
|
||||||
|
|
||||||
|
- name: Bootstrap MaaDeps
|
||||||
|
if: steps.cache-maadeps.outputs.cache-hit != 'true'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
python3 tools/maadeps-download.py ${{ matrix.arch }}-android
|
||||||
|
|
||||||
|
- name: Setup Android NDK
|
||||||
|
id: setup-ndk
|
||||||
|
uses: nttld/setup-ndk@v1
|
||||||
|
with:
|
||||||
|
ndk-version: r29
|
||||||
|
|
||||||
|
- name: Configure, build and install
|
||||||
|
run: |
|
||||||
|
cmake -B build --preset 'android-publish-${{ matrix.arch }}' \
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE=${{ steps.setup-ndk.outputs.ndk-path }}/build/cmake/android.toolchain.cmake \
|
||||||
|
-DMAA_HASH_VERSION='${{ needs.meta.outputs.tag }}'
|
||||||
|
cmake --build build --parallel $(sysctl -n hw.logicalcpu)
|
||||||
|
cmake --install build --prefix install
|
||||||
|
|
||||||
|
- name: Download MaaFramework
|
||||||
|
uses: robinraju/release-downloader@v1
|
||||||
|
with:
|
||||||
|
repository: MaaXYZ/MaaFramework
|
||||||
|
latest: true
|
||||||
|
fileName: "*android-${{ matrix.arch == 'arm64' && 'aarch64' || 'x86_64' }}*.zip"
|
||||||
|
extract: true
|
||||||
|
out-file-path: MaaFramework-temp
|
||||||
|
|
||||||
|
- name: Copy MaaAndroidNativeControlUnit
|
||||||
|
run: |
|
||||||
|
cp MaaFramework-temp/bin/libMaaAndroidNativeControlUnit.so install/
|
||||||
|
|
||||||
|
- name: Tar files
|
||||||
|
run: |
|
||||||
|
cd install
|
||||||
|
tar czvf $GITHUB_WORKSPACE/MAAComponent-${{ needs.meta.outputs.tag }}-android-${{ matrix.arch }}.tar.gz .
|
||||||
|
|
||||||
|
- name: Upload MAA to GitHub
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: MAAComponent-android-${{ matrix.arch }}
|
||||||
|
path: MAAComponent-*.tar.gz
|
||||||
|
|
||||||
macOS-Core:
|
macOS-Core:
|
||||||
name: Build Core for macOS
|
name: Build Core for macOS
|
||||||
needs: meta
|
needs: meta
|
||||||
@@ -574,7 +646,7 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
name: Publish Release
|
name: Publish Release
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
needs: [meta, windows, ubuntu, macOS-Core, macOS-GUI]
|
needs: [meta, windows, ubuntu, android, macOS-Core, macOS-GUI]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Download MAA from GitHub
|
- name: Download MAA from GitHub
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -486,3 +486,4 @@ install-*
|
|||||||
|
|
||||||
# CMake user presets
|
# CMake user presets
|
||||||
CMakeUserPresets.json
|
CMakeUserPresets.json
|
||||||
|
.ace-tool/
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ if(BUILD_WPF_GUI)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (ANDROID)
|
||||||
|
add_library(stdc++fs INTERFACE)
|
||||||
|
add_compile_options(-Wno-unused-parameter)
|
||||||
|
add_compile_options(-ffunction-sections -fdata-sections)
|
||||||
|
add_link_options(-Wl,--gc-sections)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(INSTALL_PYTHON)
|
if(INSTALL_PYTHON)
|
||||||
install(DIRECTORY src/Python DESTINATION .)
|
install(DIRECTORY src/Python DESTINATION .)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -158,6 +158,39 @@
|
|||||||
"CMAKE_OSX_ARCHITECTURES": "x86_64"
|
"CMAKE_OSX_ARCHITECTURES": "x86_64"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "android-base",
|
||||||
|
"hidden": true,
|
||||||
|
"generator": "Ninja",
|
||||||
|
"binaryDir": "${sourceDir}/build",
|
||||||
|
"$comment": [
|
||||||
|
"Base for Android presets; cross-compilation via NDK toolchain",
|
||||||
|
"CMAKE_TOOLCHAIN_FILE must be passed externally pointing to NDK's android.toolchain.cmake"
|
||||||
|
],
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_SYSTEM_NAME": "Android",
|
||||||
|
"ANDROID_PLATFORM": "android-28",
|
||||||
|
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "android-arm64",
|
||||||
|
"inherits": "android-base",
|
||||||
|
"displayName": "Android arm64",
|
||||||
|
"cacheVariables": {
|
||||||
|
"ANDROID_ABI": "arm64-v8a",
|
||||||
|
"MAADEPS_TRIPLET": "maa-arm64-android"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "android-x64",
|
||||||
|
"inherits": "android-base",
|
||||||
|
"displayName": "Android x64",
|
||||||
|
"cacheVariables": {
|
||||||
|
"ANDROID_ABI": "x86_64",
|
||||||
|
"MAADEPS_TRIPLET": "maa-x64-android"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "publish-base",
|
"name": "publish-base",
|
||||||
"$comment": [
|
"$comment": [
|
||||||
@@ -239,6 +272,22 @@
|
|||||||
],
|
],
|
||||||
"displayName": "macOS arm64 Publish"
|
"displayName": "macOS arm64 Publish"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "android-publish-arm64",
|
||||||
|
"inherits": ["publish-base", "android-arm64"],
|
||||||
|
"displayName": "Android arm64 Publish",
|
||||||
|
"cacheVariables": {
|
||||||
|
"INSTALL_PYTHON": "OFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "android-publish-x64",
|
||||||
|
"inherits": ["publish-base", "android-x64"],
|
||||||
|
"displayName": "Android x64 Publish",
|
||||||
|
"cacheVariables": {
|
||||||
|
"INSTALL_PYTHON": "OFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "smoke-test",
|
"name": "smoke-test",
|
||||||
"$comment": [
|
"$comment": [
|
||||||
@@ -400,6 +449,16 @@
|
|||||||
"configurePreset": "macos-publish-arm64",
|
"configurePreset": "macos-publish-arm64",
|
||||||
"configuration": "RelWithDebInfo"
|
"configuration": "RelWithDebInfo"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "android-publish-arm64",
|
||||||
|
"displayName": "Build Android arm64 Publish",
|
||||||
|
"configurePreset": "android-publish-arm64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "android-publish-x64",
|
||||||
|
"displayName": "Build Android x64 Publish",
|
||||||
|
"configurePreset": "android-publish-x64"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "smoke-test",
|
"name": "smoke-test",
|
||||||
"displayName": "Build macOS arm64 Smoke Test",
|
"displayName": "Build macOS arm64 Smoke Test",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
#include "Task/Interface/DebugTask.h"
|
#include "Task/Interface/DebugTask.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
using namespace asst;
|
using namespace asst;
|
||||||
|
|
||||||
bool ::AsstExtAPI::set_static_option(StaticOptionKey key, const std::string& value)
|
bool ::AsstExtAPI::set_static_option(StaticOptionKey key, const std::string& value)
|
||||||
@@ -137,6 +138,12 @@ bool asst::Assistant::set_instance_option(InstanceOptionKey key, const std::stri
|
|||||||
m_ctrler->set_touch_mode(TouchMode::MaaFwAdb);
|
m_ctrler->set_touch_mode(TouchMode::MaaFwAdb);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
else if (constexpr std::string_view Android = "Android"; value == Android) {
|
||||||
|
m_ctrler->set_touch_mode(TouchMode::Android);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
break;
|
break;
|
||||||
case InstanceOptionKey::DeploymentWithPause:
|
case InstanceOptionKey::DeploymentWithPause:
|
||||||
if (constexpr std::string_view Enable = "1"; value == Enable) {
|
if (constexpr std::string_view Enable = "1"; value == Enable) {
|
||||||
@@ -624,7 +631,7 @@ void asst::Assistant::call_proc()
|
|||||||
while (true) {
|
while (true) {
|
||||||
std::unique_lock<std::mutex> lock(m_call_mutex);
|
std::unique_lock<std::mutex> lock(m_call_mutex);
|
||||||
if (m_thread_exit) {
|
if (m_thread_exit) {
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_call_queue.empty()) {
|
if (m_call_queue.empty()) {
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ if(MSVC)
|
|||||||
set_target_properties(MaaCore PROPERTIES VS_DEBUGGER_VISUALIZER "${CMAKE_CURRENT_SOURCE_DIR}/meojson.natvis")
|
set_target_properties(MaaCore PROPERTIES VS_DEBUGGER_VISUALIZER "${CMAKE_CURRENT_SOURCE_DIR}/meojson.natvis")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(ANDROID)
|
||||||
|
target_link_libraries(MaaCore log dl)
|
||||||
|
endif()
|
||||||
|
|
||||||
file(GLOB_RECURSE MaaCore_PUBLIC_HEADERS ${PROJECT_SOURCE_DIR}/include/*.h)
|
file(GLOB_RECURSE MaaCore_PUBLIC_HEADERS ${PROJECT_SOURCE_DIR}/include/*.h)
|
||||||
target_sources(MaaCore PUBLIC ${MaaCore_PUBLIC_HEADERS})
|
target_sources(MaaCore PUBLIC ${MaaCore_PUBLIC_HEADERS})
|
||||||
set_target_properties(MaaCore PROPERTIES PUBLIC_HEADER "${MaaCore_PUBLIC_HEADERS}")
|
set_target_properties(MaaCore PROPERTIES PUBLIC_HEADER "${MaaCore_PUBLIC_HEADERS}")
|
||||||
@@ -120,3 +124,15 @@ if(WIN32)
|
|||||||
$<TARGET_FILE_DIR:MaaCore>
|
$<TARGET_FILE_DIR:MaaCore>
|
||||||
COMMAND_EXPAND_LISTS)
|
COMMAND_EXPAND_LISTS)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(ANDROID)
|
||||||
|
if(CMAKE_STRIP)
|
||||||
|
add_custom_command(
|
||||||
|
TARGET MaaCore
|
||||||
|
POST_BUILD
|
||||||
|
COMMAND ${CMAKE_STRIP} --strip-all $<TARGET_FILE:MaaCore>
|
||||||
|
COMMENT "Stripping MaaCore for Android")
|
||||||
|
else()
|
||||||
|
message(WARNING "CMAKE_STRIP not found, Android library will not be stripped")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ enum class TouchMode
|
|||||||
Maatouch = 2,
|
Maatouch = 2,
|
||||||
MacPlayTools = 3,
|
MacPlayTools = 3,
|
||||||
MaaFwAdb = 4,
|
MaaFwAdb = 4,
|
||||||
|
Android = 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
@@ -155,7 +156,7 @@ struct Point
|
|||||||
{ \
|
{ \
|
||||||
return { lhs.x Op rhs.x, lhs.y Op rhs.y }; \
|
return { lhs.x Op rhs.x, lhs.y Op rhs.y }; \
|
||||||
} \
|
} \
|
||||||
friend Point& operator Op##=(Point& val, const Point& opd) noexcept \
|
friend Point& operator Op## =(Point& val, const Point& opd) noexcept \
|
||||||
{ \
|
{ \
|
||||||
val.x Op## = opd.x; \
|
val.x Op## = opd.x; \
|
||||||
val.y Op## = opd.y; \
|
val.y Op## = opd.y; \
|
||||||
@@ -260,7 +261,7 @@ struct Rect
|
|||||||
static Rect bounding_box(const std::vector<Rect>& rects)
|
static Rect bounding_box(const std::vector<Rect>& rects)
|
||||||
{
|
{
|
||||||
if (rects.empty()) {
|
if (rects.empty()) {
|
||||||
return {};
|
return { };
|
||||||
}
|
}
|
||||||
|
|
||||||
int min_x = INT_MAX;
|
int min_x = INT_MAX;
|
||||||
@@ -303,11 +304,11 @@ struct AnalyzerResult
|
|||||||
{
|
{
|
||||||
virtual ~AnalyzerResult() = default;
|
virtual ~AnalyzerResult() = default;
|
||||||
|
|
||||||
virtual std::string to_string() const { return {}; };
|
virtual std::string to_string() const { return { }; };
|
||||||
|
|
||||||
explicit operator std::string() const { return to_string(); }
|
explicit operator std::string() const { return to_string(); }
|
||||||
|
|
||||||
virtual json::object to_json() const { return {}; };
|
virtual json::object to_json() const { return { }; };
|
||||||
|
|
||||||
explicit operator json::object() const { return to_json(); }
|
explicit operator json::object() const { return to_json(); }
|
||||||
};
|
};
|
||||||
@@ -418,8 +419,8 @@ struct pair_hash
|
|||||||
{
|
{
|
||||||
size_t operator()(const std::pair<T1, T2>& p) const noexcept
|
size_t operator()(const std::pair<T1, T2>& p) const noexcept
|
||||||
{
|
{
|
||||||
std::size_t hash1 = std::hash<T1> {}(p.first);
|
std::size_t hash1 = std::hash<T1> { }(p.first);
|
||||||
std::size_t hash2 = std::hash<T2> {}(p.second);
|
std::size_t hash2 = std::hash<T2> { }(p.second);
|
||||||
hash1 ^= hash2 + 0x9e3779b9 + (hash1 << 6) + (hash1 >> 2);
|
hash1 ^= hash2 + 0x9e3779b9 + (hash1 << 6) + (hash1 >> 2);
|
||||||
|
|
||||||
hash1 ^= hash1 >> 32;
|
hash1 ^= hash1 >> 32;
|
||||||
|
|||||||
@@ -28,6 +28,10 @@
|
|||||||
#include "Win32Controller.h"
|
#include "Win32Controller.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
#include "MaaFwAndroidNativeController.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "Common/AsstTypes.h"
|
#include "Common/AsstTypes.h"
|
||||||
#include "Utils/Logger.hpp"
|
#include "Utils/Logger.hpp"
|
||||||
|
|
||||||
@@ -58,6 +62,11 @@ std::shared_ptr<asst::ControllerAPI>
|
|||||||
return std::make_shared<PlayToolsController>(m_callback, m_inst, platform_type);
|
return std::make_shared<PlayToolsController>(m_callback, m_inst, platform_type);
|
||||||
case ControllerType::MaaFwAdb:
|
case ControllerType::MaaFwAdb:
|
||||||
return std::make_shared<MaaFwAdbController>(m_callback, m_inst, platform_type);
|
return std::make_shared<MaaFwAdbController>(m_callback, m_inst, platform_type);
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
case ControllerType::MaaFwAndroidNative:
|
||||||
|
Log.debug("Use Android");
|
||||||
|
return std::make_shared<MaaFwAndroidNativeController>(m_callback, m_inst);
|
||||||
|
#endif
|
||||||
default:
|
default:
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -247,11 +256,14 @@ bool asst::Controller::connect(const std::string& adb_path, const std::string& a
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Android uses lazy loading; no need to check in advance
|
||||||
|
#ifndef __ANDROID__
|
||||||
// try to find the fastest way
|
// try to find the fastest way
|
||||||
if (!screencap()) {
|
if (!screencap()) {
|
||||||
Log.error("Cannot find a proper way to screencap!");
|
Log.error("Cannot find a proper way to screencap!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
auto proxy_callback = [&](const json::object& details) {
|
auto proxy_callback = [&](const json::object& details) {
|
||||||
json::value connection_info = json::object {
|
json::value connection_info = json::object {
|
||||||
@@ -368,6 +380,11 @@ void asst::Controller::set_touch_mode(const TouchMode& mode) noexcept
|
|||||||
case TouchMode::MaaFwAdb:
|
case TouchMode::MaaFwAdb:
|
||||||
m_controller_type = ControllerType::MaaFwAdb;
|
m_controller_type = ControllerType::MaaFwAdb;
|
||||||
break;
|
break;
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
case TouchMode::Android:
|
||||||
|
m_controller_type = ControllerType::MaaFwAndroidNative;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
default:
|
default:
|
||||||
m_controller_type = ControllerType::Minitouch;
|
m_controller_type = ControllerType::Minitouch;
|
||||||
}
|
}
|
||||||
@@ -409,7 +426,7 @@ cv::Mat asst::Controller::get_image(bool raw)
|
|||||||
{
|
{
|
||||||
if (get_scale_size() == std::pair(0, 0)) {
|
if (get_scale_size() == std::pair(0, 0)) {
|
||||||
Log.error("Unknown image size");
|
Log.error("Unknown image size");
|
||||||
return {};
|
return { };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 有些模拟器adb偶尔会莫名其妙截图失败,多试几次
|
// 有些模拟器adb偶尔会莫名其妙截图失败,多试几次
|
||||||
@@ -433,7 +450,7 @@ cv::Mat asst::Controller::get_image(bool raw)
|
|||||||
{ "uuid", m_uuid },
|
{ "uuid", m_uuid },
|
||||||
{ "what", "ScreencapFailed" },
|
{ "what", "ScreencapFailed" },
|
||||||
{ "why", "ScreencapFailed" },
|
{ "why", "ScreencapFailed" },
|
||||||
{ "details", json::object {} },
|
{ "details", json::object { } },
|
||||||
};
|
};
|
||||||
callback(AsstMsg::ConnectionInfo, info);
|
callback(AsstMsg::ConnectionInfo, info);
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ public:
|
|||||||
|
|
||||||
ControllerType get_controller_type() const noexcept;
|
ControllerType get_controller_type() const noexcept;
|
||||||
|
|
||||||
|
ControllerAPI* get_underlying() const noexcept { return m_controller.get(); }
|
||||||
|
|
||||||
cv::Mat get_image(bool raw = false);
|
cv::Mat get_image(bool raw = false);
|
||||||
cv::Mat get_image_cache() const;
|
cv::Mat get_image_cache() const;
|
||||||
bool screencap(bool allow_reconnect = false);
|
bool screencap(bool allow_reconnect = false);
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ enum class ControllerType
|
|||||||
Win32,
|
Win32,
|
||||||
#endif
|
#endif
|
||||||
MaaFwAdb,
|
MaaFwAdb,
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
MaaFwAndroidNative,
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
class ControllerAPI
|
class ControllerAPI
|
||||||
|
|||||||
425
src/MaaCore/Controller/MaaFwAndroidNativeController.cpp
Normal file
425
src/MaaCore/Controller/MaaFwAndroidNativeController.cpp
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
#ifdef __ANDROID__
|
||||||
|
|
||||||
|
#include "MaaFwAndroidNativeController.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "Common/AsstMsg.h"
|
||||||
|
#include "Config/GeneralConfig.h"
|
||||||
|
#include "Controller/MaaFwControlUnitInterface.h"
|
||||||
|
#include "Controller/SwipeHelper.hpp"
|
||||||
|
#include "Utils/Logger.hpp"
|
||||||
|
|
||||||
|
namespace asst
|
||||||
|
{
|
||||||
|
|
||||||
|
MaaFwAndroidNativeController::MaaFwAndroidNativeController(const AsstCallback& callback, Assistant* inst) :
|
||||||
|
InstHelper(inst),
|
||||||
|
m_callback(callback)
|
||||||
|
{
|
||||||
|
LogTraceFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
MaaFwAndroidNativeController::~MaaFwAndroidNativeController()
|
||||||
|
{
|
||||||
|
LogTraceFunction;
|
||||||
|
|
||||||
|
if (m_unit_handle && m_destroy_func) {
|
||||||
|
LogInfo << "Cleaning up MaaAndroidNativeControlUnit";
|
||||||
|
m_destroy_func(m_unit_handle);
|
||||||
|
m_unit_handle = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaaFwAndroidNativeController::connect(
|
||||||
|
const std::string& adb_path [[maybe_unused]],
|
||||||
|
const std::string& address [[maybe_unused]],
|
||||||
|
const std::string& config)
|
||||||
|
{
|
||||||
|
LogTraceFunction;
|
||||||
|
|
||||||
|
m_inited = false;
|
||||||
|
m_uuid.clear();
|
||||||
|
auto get_info_json = [&]() -> json::object {
|
||||||
|
return json::object {
|
||||||
|
{ "uuid", m_uuid },
|
||||||
|
{ "details",
|
||||||
|
json::object {
|
||||||
|
{ "config", config },
|
||||||
|
} },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!init_library()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_unit_handle && m_destroy_func) {
|
||||||
|
LogInfo << "Cleaning up the old connection and reconnecting";
|
||||||
|
m_destroy_func(m_unit_handle);
|
||||||
|
m_unit_handle = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.empty()) {
|
||||||
|
if (auto config_opt = json::parse(config); config_opt.has_value()) {
|
||||||
|
auto& config_json = config_opt.value();
|
||||||
|
if (config_json.contains("screen_resolution")) {
|
||||||
|
if (const auto& res = config_json["screen_resolution"];
|
||||||
|
res.contains("width") && res.contains("height")) {
|
||||||
|
int width = res.get("width", 1280);
|
||||||
|
int height = res.get("height", 720);
|
||||||
|
m_screen_resolution = { width, height };
|
||||||
|
LogInfo << "Parsed screen resolution from config:" << width << "x" << height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LogError << "Failed to parse config as JSON";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_screen_resolution.first <= 0 || m_screen_resolution.second <= 0) {
|
||||||
|
LogError << "screen_resolution not provided or invalid in config, cannot connect";
|
||||||
|
callback(
|
||||||
|
AsstMsg::ConnectionInfo,
|
||||||
|
json::object {
|
||||||
|
{ "what", "ConnectFailed" },
|
||||||
|
{ "why", "screen_resolution missing in config" },
|
||||||
|
} | get_info_json());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_unit_handle = m_create_func(config.c_str());
|
||||||
|
|
||||||
|
if (!m_unit_handle) {
|
||||||
|
LogError << "Failed to create MaaAndroidNativeControlUnit";
|
||||||
|
callback(
|
||||||
|
AsstMsg::ConnectionInfo,
|
||||||
|
json::object {
|
||||||
|
{ "what", "ConnectFailed" },
|
||||||
|
{ "why", "MaaAndroidNativeControlUnit creation failed" },
|
||||||
|
} | get_info_json());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_unit_handle->connect()) {
|
||||||
|
LogError << "MaaAndroidNativeControlUnit failed to connect";
|
||||||
|
m_destroy_func(m_unit_handle);
|
||||||
|
m_unit_handle = nullptr;
|
||||||
|
callback(
|
||||||
|
AsstMsg::ConnectionInfo,
|
||||||
|
json::object {
|
||||||
|
{ "what", "ConnectFailed" },
|
||||||
|
{ "why", "MaaAndroidNativeControlUnit failed to connect" },
|
||||||
|
} | get_info_json());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_unit_handle->request_uuid(m_uuid)) {
|
||||||
|
LogWarn << "Failed to get UUID from MaaAndroidNativeControlUnit";
|
||||||
|
m_destroy_func(m_unit_handle);
|
||||||
|
m_unit_handle = nullptr;
|
||||||
|
callback(
|
||||||
|
AsstMsg::ConnectionInfo,
|
||||||
|
json::object {
|
||||||
|
{ "what", "ConnectFailed" },
|
||||||
|
{ "why", "MaaAndroidNativeControlUnit failed to get UUID" },
|
||||||
|
} | get_info_json());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_inited = true;
|
||||||
|
callback(
|
||||||
|
AsstMsg::ConnectionInfo,
|
||||||
|
json::object {
|
||||||
|
{ "what", "Connected" },
|
||||||
|
{ "why", "NativeAndroid" },
|
||||||
|
} | get_info_json());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaaFwAndroidNativeController::inited() const noexcept
|
||||||
|
{
|
||||||
|
return m_inited && m_unit_handle && m_unit_handle->connected();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& MaaFwAndroidNativeController::get_uuid() const
|
||||||
|
{
|
||||||
|
return m_uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaaFwAndroidNativeController::screencap(cv::Mat& image_payload, bool allow_reconnect [[maybe_unused]])
|
||||||
|
{
|
||||||
|
LogTraceFunction;
|
||||||
|
if (!m_unit_handle) {
|
||||||
|
LogWarn << "MaaAndroidNativeControlUnit is not initialized";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_unit_handle->screencap(image_payload)) {
|
||||||
|
LogWarn << "MaaAndroidNativeControlUnit screencap failed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaaFwAndroidNativeController::start_game(const std::string& client_type)
|
||||||
|
{
|
||||||
|
LogTraceFunction;
|
||||||
|
if (!m_unit_handle) {
|
||||||
|
LogWarn << "MaaAndroidNativeControlUnit is not initialized";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto package_name = Config.get_package_name(client_type);
|
||||||
|
if (!package_name) {
|
||||||
|
LogWarn << "Invalid client_type" << VAR(client_type);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_unit_handle->start_app(*package_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaaFwAndroidNativeController::stop_game(const std::string& client_type)
|
||||||
|
{
|
||||||
|
LogTraceFunction;
|
||||||
|
if (!m_unit_handle) {
|
||||||
|
LogWarn << "MaaAndroidNativeControlUnit is not initialized";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto package_name = Config.get_package_name(client_type);
|
||||||
|
if (!package_name) {
|
||||||
|
LogWarn << "Invalid client_type" << VAR(client_type);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_unit_handle->stop_app(*package_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaaFwAndroidNativeController::click(const Point& p)
|
||||||
|
{
|
||||||
|
LogTraceFunction;
|
||||||
|
if (!m_unit_handle) {
|
||||||
|
LogWarn << "MaaAndroidNativeControlUnit is not initialized";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_unit_handle->touch_down(0, p.x, p.y, 1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
return m_unit_handle->touch_up(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaaFwAndroidNativeController::input(const std::string& text)
|
||||||
|
{
|
||||||
|
LogTraceFunction;
|
||||||
|
if (!m_unit_handle) {
|
||||||
|
LogWarn << "MaaAndroidNativeControlUnit is not initialized";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_unit_handle->input_text(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaaFwAndroidNativeController::swipe(
|
||||||
|
const Point& p1,
|
||||||
|
const Point& p2,
|
||||||
|
const int duration,
|
||||||
|
const bool extra_swipe,
|
||||||
|
const double slope_in,
|
||||||
|
const double slope_out,
|
||||||
|
const bool with_pause)
|
||||||
|
{
|
||||||
|
LogTraceFunction;
|
||||||
|
if (!m_unit_handle) {
|
||||||
|
LogWarn << "MaaAndroidNativeControlUnit is not initialized";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x1 = p1.x, y1 = p1.y;
|
||||||
|
int x2 = p2.x, y2 = p2.y;
|
||||||
|
|
||||||
|
// 起点不能在屏幕外,但是终点可以
|
||||||
|
if (x1 < 0 || x1 >= m_screen_resolution.first || y1 < 0 || y1 >= m_screen_resolution.second) {
|
||||||
|
LogWarn << "swipe point1 is out of range" << x1 << y1;
|
||||||
|
x1 = std::clamp(x1, 0, m_screen_resolution.first - 1);
|
||||||
|
y1 = std::clamp(y1, 0, m_screen_resolution.second - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触摸按下起点
|
||||||
|
if (!m_unit_handle->touch_down(0, x1, y1, 1)) {
|
||||||
|
LogError << "touch_down failed at swipe start point";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int TimeInterval = 5; // 类似 Minitoucher::DefaultSwipeDelay
|
||||||
|
|
||||||
|
bool need_pause = with_pause;
|
||||||
|
const auto& opt = Config.get_options();
|
||||||
|
|
||||||
|
auto bounds_check = [this](int x, int y) {
|
||||||
|
return x >= 0 && x < m_screen_resolution.first && y >= 0 && y < m_screen_resolution.second;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto move_func = [&](int x, int y) -> bool {
|
||||||
|
if (!m_unit_handle->touch_move(0, x, y, 1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(TimeInterval));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto do_swipe = [&](const int _x1, const int _y1, const int _x2, const int _y2, const int _duration) -> bool {
|
||||||
|
if (need_pause) {
|
||||||
|
auto pause_check = [&opt](const int cur_x, const int cur_y, const int start_x, const 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
return interpolate_swipe_with_pause(
|
||||||
|
_x1,
|
||||||
|
_y1,
|
||||||
|
_x2,
|
||||||
|
_y2,
|
||||||
|
_duration,
|
||||||
|
TimeInterval,
|
||||||
|
slope_in,
|
||||||
|
slope_out,
|
||||||
|
move_func,
|
||||||
|
bounds_check,
|
||||||
|
pause_check,
|
||||||
|
[&]() {
|
||||||
|
need_pause = false;
|
||||||
|
press_esc();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return interpolate_swipe(
|
||||||
|
_x1,
|
||||||
|
_y1,
|
||||||
|
_x2,
|
||||||
|
_y2,
|
||||||
|
_duration,
|
||||||
|
TimeInterval,
|
||||||
|
slope_in,
|
||||||
|
slope_out,
|
||||||
|
move_func,
|
||||||
|
bounds_check);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!do_swipe(x1, y1, x2, y2, duration ? duration : opt.minitouch_swipe_default_duration)) {
|
||||||
|
LogError << "Failed during main swipe movement";
|
||||||
|
m_unit_handle->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));
|
||||||
|
|
||||||
|
if (!do_swipe(x2, y2, x2, y2 - opt.minitouch_extra_swipe_dist, opt.minitouch_extra_swipe_duration)) {
|
||||||
|
LogWarn << "Failed during extra swipe movement";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_unit_handle->touch_up(0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaaFwAndroidNativeController::inject_input_event(const InputEvent& event)
|
||||||
|
{
|
||||||
|
LogTraceFunction;
|
||||||
|
if (!m_unit_handle) {
|
||||||
|
LogWarn << "MaaAndroidNativeControlUnit is not initialized";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case InputEvent::Type::TOUCH_DOWN:
|
||||||
|
return m_unit_handle->touch_down(event.pointerId, event.point.x, event.point.y, 0);
|
||||||
|
case InputEvent::Type::TOUCH_MOVE:
|
||||||
|
return m_unit_handle->touch_move(event.pointerId, event.point.x, event.point.y, 0);
|
||||||
|
case InputEvent::Type::TOUCH_UP:
|
||||||
|
return m_unit_handle->touch_up(event.pointerId);
|
||||||
|
case InputEvent::Type::TOUCH_RESET:
|
||||||
|
return true;
|
||||||
|
case InputEvent::Type::KEY_DOWN:
|
||||||
|
return m_unit_handle->key_down(event.keycode);
|
||||||
|
case InputEvent::Type::KEY_UP:
|
||||||
|
return m_unit_handle->key_up(event.keycode);
|
||||||
|
case InputEvent::Type::WAIT_MS:
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(event.milisec));
|
||||||
|
return true;
|
||||||
|
case InputEvent::Type::COMMIT:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
LogError << "unknown input event type" << VAR(static_cast<int>(event.type));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaaFwAndroidNativeController::press_esc()
|
||||||
|
{
|
||||||
|
LogTraceFunction;
|
||||||
|
if (!m_unit_handle) {
|
||||||
|
LogWarn << "MaaAndroidNativeControlUnit is not initialized";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int KEYCODE_ESCAPE = 111;
|
||||||
|
if (!m_unit_handle->key_down(KEYCODE_ESCAPE)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
return m_unit_handle->key_up(KEYCODE_ESCAPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlFeat::Feat MaaFwAndroidNativeController::support_features() const noexcept
|
||||||
|
{
|
||||||
|
// MaaFwAndroidNativeController 支持精确滑动和暂停滑动功能
|
||||||
|
auto feat = ControlFeat::PRECISE_SWIPE;
|
||||||
|
feat |= ControlFeat::SWIPE_WITH_PAUSE;
|
||||||
|
return feat;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<int, int> MaaFwAndroidNativeController::get_screen_res() const noexcept
|
||||||
|
{
|
||||||
|
return m_screen_resolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaaFwAndroidNativeController::init_library()
|
||||||
|
{
|
||||||
|
if (m_get_version_func && m_create_func && m_destroy_func) {
|
||||||
|
LogInfo << "MaaAndroidNativeControlUnit library already loaded";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!load_library("MaaAndroidNativeControlUnit")) {
|
||||||
|
LogError << "Failed to load MaaAndroidNativeControlUnit library";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_get_version_func = get_function<GetVersionFunc>("MaaAndroidNativeControlUnitGetVersion");
|
||||||
|
m_create_func = get_function<CreateFunc>("MaaAndroidNativeControlUnitCreate");
|
||||||
|
m_destroy_func = get_function<DestroyFunc>("MaaAndroidNativeControlUnitDestroy");
|
||||||
|
|
||||||
|
if (!m_get_version_func || !m_create_func || !m_destroy_func) {
|
||||||
|
LogError << "Failed to get function pointers from MaaAndroidNativeControlUnit library";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogInfo << "MaaAndroidNativeControlUnit library version:" << m_get_version_func();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MaaFwAndroidNativeController::callback(const AsstMsg msg, const json::value& details) const
|
||||||
|
{
|
||||||
|
if (m_callback) {
|
||||||
|
m_callback(msg, details, m_inst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace asst
|
||||||
|
|
||||||
|
#endif // __ANDROID__
|
||||||
85
src/MaaCore/Controller/MaaFwAndroidNativeController.h
Normal file
85
src/MaaCore/Controller/MaaFwAndroidNativeController.h
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "Common/AsstMsg.h"
|
||||||
|
#include "ControllerAPI.h"
|
||||||
|
#include "InstHelper.h"
|
||||||
|
#include "MaaFwControlUnitInterface.h"
|
||||||
|
#include "Utils/LibraryHolder.hpp"
|
||||||
|
|
||||||
|
namespace asst
|
||||||
|
{
|
||||||
|
class Assistant;
|
||||||
|
|
||||||
|
class MaaFwAndroidNativeController : public ControllerAPI, private InstHelper,
|
||||||
|
public LibraryHolder<MaaFwAndroidNativeController>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MaaFwAndroidNativeController(const AsstCallback& callback, Assistant* inst);
|
||||||
|
virtual ~MaaFwAndroidNativeController() override;
|
||||||
|
|
||||||
|
MaaFwAndroidNativeController(const MaaFwAndroidNativeController&) = delete;
|
||||||
|
MaaFwAndroidNativeController& operator=(const MaaFwAndroidNativeController&) = delete;
|
||||||
|
MaaFwAndroidNativeController(MaaFwAndroidNativeController&&) = delete;
|
||||||
|
MaaFwAndroidNativeController& operator=(MaaFwAndroidNativeController&&) = delete;
|
||||||
|
|
||||||
|
public:
|
||||||
|
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 1; }
|
||||||
|
|
||||||
|
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<int, int> get_screen_res() const noexcept override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool m_inited = false;
|
||||||
|
std::string m_uuid;
|
||||||
|
std::pair<int, int> m_screen_resolution = { 0, 0 };
|
||||||
|
|
||||||
|
MaaFwAndroidNativeControlUnitAPI* m_unit_handle = nullptr;
|
||||||
|
bool init_library();
|
||||||
|
|
||||||
|
AsstCallback m_callback = nullptr;
|
||||||
|
void callback(AsstMsg msg, const json::value& details) const;
|
||||||
|
|
||||||
|
// MaaFramework/source/include/MaaControlUnit/AndroidNativeControlUnitAPI.h
|
||||||
|
using GetVersionFunc = const char*();
|
||||||
|
using CreateFunc = MaaFwAndroidNativeControlUnitAPI*(const char*);
|
||||||
|
using DestroyFunc = void(MaaFwAndroidNativeControlUnitAPI*);
|
||||||
|
|
||||||
|
std::function<GetVersionFunc> m_get_version_func;
|
||||||
|
std::function<CreateFunc> m_create_func;
|
||||||
|
std::function<DestroyFunc> m_destroy_func;
|
||||||
|
};
|
||||||
|
} // namespace asst
|
||||||
|
|
||||||
|
#endif // __ANDROID__
|
||||||
@@ -39,7 +39,10 @@ public:
|
|||||||
virtual bool key_down(int key) = 0;
|
virtual bool key_down(int key) = 0;
|
||||||
virtual bool key_up(int key) = 0;
|
virtual bool key_up(int key) = 0;
|
||||||
|
|
||||||
virtual bool scroll(int dx, int dy) = 0;
|
virtual bool inactive() = 0;
|
||||||
|
|
||||||
|
// json::object get_info() const - ABI slot occupied, not called from MAA side
|
||||||
|
virtual void* get_info() const = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class MaaFwAdbControlUnitAPI : public MaaFwControlUnitAPI
|
class MaaFwAdbControlUnitAPI : public MaaFwControlUnitAPI
|
||||||
@@ -54,6 +57,12 @@ public:
|
|||||||
std::chrono::milliseconds timeout = std::chrono::milliseconds(20000)) = 0;
|
std::chrono::milliseconds timeout = std::chrono::milliseconds(20000)) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class MaaFwAndroidNativeControlUnitAPI : public MaaFwControlUnitAPI
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
~MaaFwAndroidNativeControlUnitAPI() override = default;
|
||||||
|
};
|
||||||
|
|
||||||
// 与 MaaFramework 的 MaaControllerFeature 兼容的常量
|
// 与 MaaFramework 的 MaaControllerFeature 兼容的常量
|
||||||
namespace MaaFeature
|
namespace MaaFeature
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,8 +5,13 @@
|
|||||||
|
|
||||||
using namespace asst;
|
using namespace asst;
|
||||||
|
|
||||||
bool StartGameTaskPlugin::start_game_with_retries(size_t pipe_data_size_limit, bool newer_android) const
|
bool StartGameTaskPlugin::start_game_with_retries([[maybe_unused]] size_t pipe_data_size_limit,
|
||||||
|
[[maybe_unused]] bool newer_android) const
|
||||||
{
|
{
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
// On Android, start_game returns true only after the game is actually started
|
||||||
|
return !need_exit() && ctrler()->start_game(m_client_type);
|
||||||
|
#else
|
||||||
int extra_runs = 0;
|
int extra_runs = 0;
|
||||||
for (int i = 0; i < 30; ++i) {
|
for (int i = 0; i < 30; ++i) {
|
||||||
if (need_exit() || !ctrler()->start_game(m_client_type)) {
|
if (need_exit() || !ctrler()->start_game(m_client_type)) {
|
||||||
@@ -23,6 +28,7 @@ bool StartGameTaskPlugin::start_game_with_retries(size_t pipe_data_size_limit, b
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StartGameTaskPlugin::_run()
|
bool StartGameTaskPlugin::_run()
|
||||||
|
|||||||
@@ -4,7 +4,12 @@
|
|||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <io.h>
|
#include <io.h>
|
||||||
#endif
|
#endif
|
||||||
|
#include <atomic>
|
||||||
|
#include <array>
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <exception>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
@@ -15,6 +20,7 @@
|
|||||||
#include <streambuf>
|
#include <streambuf>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
#include <typeinfo>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "Common/AsstTypes.h"
|
#include "Common/AsstTypes.h"
|
||||||
@@ -31,6 +37,14 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
#include <android/log.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <unwind.h>
|
||||||
|
|
||||||
|
#include "Demangle.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace asst
|
namespace asst
|
||||||
{
|
{
|
||||||
template <typename Stream, typename T>
|
template <typename Stream, typename T>
|
||||||
@@ -251,7 +265,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<id> m_state {};
|
std::vector<id> m_state { };
|
||||||
};
|
};
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
@@ -320,7 +334,7 @@ public:
|
|||||||
requires has_stream_insertion_operator<std::ostream, T>
|
requires has_stream_insertion_operator<std::ostream, T>
|
||||||
ostreams& operator<<(T&& x)
|
ostreams& operator<<(T&& x)
|
||||||
{
|
{
|
||||||
streams_put(m_ofss, x, std::index_sequence_for<Args...> {});
|
streams_put(m_ofss, x, std::index_sequence_for<Args...> { });
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +346,7 @@ public:
|
|||||||
|
|
||||||
ostreams& operator<<(std::ostream& (*pf)(std::ostream&))
|
ostreams& operator<<(std::ostream& (*pf)(std::ostream&))
|
||||||
{
|
{
|
||||||
streams_put(m_ofss, pf, std::index_sequence_for<Args...> {});
|
streams_put(m_ofss, pf, std::index_sequence_for<Args...> { });
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,7 +490,7 @@ public:
|
|||||||
#else
|
#else
|
||||||
int pid = ::getpid();
|
int pid = ::getpid();
|
||||||
#endif
|
#endif
|
||||||
auto tid = static_cast<uint16_t>(std::hash<std::thread::id> {}(std::this_thread::get_id()));
|
auto tid = static_cast<uint16_t>(std::hash<std::thread::id> { }(std::this_thread::get_id()));
|
||||||
|
|
||||||
s << std::format("[{}][{}][Px{}][Tx{}]", MAA_NS::format_now(), v.str, pid, tid);
|
s << std::format("[{}][{}][Px{}][Tx{}]", MAA_NS::format_now(), v.str, pid, tid);
|
||||||
}
|
}
|
||||||
@@ -491,7 +505,7 @@ public:
|
|||||||
}
|
}
|
||||||
else if constexpr (std::ranges::input_range<T>) {
|
else if constexpr (std::ranges::input_range<T>) {
|
||||||
s << "[";
|
s << "[";
|
||||||
std::string_view comma_space {};
|
std::string_view comma_space { };
|
||||||
for (const auto& elem : std::forward<T>(v)) {
|
for (const auto& elem : std::forward<T>(v)) {
|
||||||
s << comma_space;
|
s << comma_space;
|
||||||
stream_put(s, elem);
|
stream_put(s, elem);
|
||||||
@@ -842,7 +856,23 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline static std::atomic<const char*> g_last_signal_reason { nullptr };
|
inline static std::atomic<int> g_last_signal { 0 };
|
||||||
|
|
||||||
|
static const char* format_signal_reason(int sig) noexcept
|
||||||
|
{
|
||||||
|
switch (sig) {
|
||||||
|
case SIGSEGV:
|
||||||
|
return "SIGSEGV (Segmentation Fault)";
|
||||||
|
case SIGABRT:
|
||||||
|
return "SIGABRT (Abort)";
|
||||||
|
case SIGFPE:
|
||||||
|
return "SIGFPE (Floating Point Error)";
|
||||||
|
case SIGILL:
|
||||||
|
return "SIGILL (Illegal Instruction)";
|
||||||
|
default:
|
||||||
|
return "Unknown Signal";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void write_crash_file(const char* reason, const char* detail = nullptr) noexcept
|
static void write_crash_file(const char* reason, const char* detail = nullptr) noexcept
|
||||||
{
|
{
|
||||||
@@ -900,6 +930,103 @@ private:
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
static constexpr const char* AndroidCrashLogTag = "MaaCoreCrash";
|
||||||
|
|
||||||
|
struct AndroidBacktraceState
|
||||||
|
{
|
||||||
|
void** current = nullptr;
|
||||||
|
void** end = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
static _Unwind_Reason_Code android_unwind_callback(_Unwind_Context* context, void* arg) noexcept
|
||||||
|
{
|
||||||
|
auto* state = static_cast<AndroidBacktraceState*>(arg);
|
||||||
|
if (state == nullptr || state->current == state->end) {
|
||||||
|
return _URC_END_OF_STACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto pc = reinterpret_cast<void*>(_Unwind_GetIP(context));
|
||||||
|
if (pc == nullptr) {
|
||||||
|
return _URC_NO_REASON;
|
||||||
|
}
|
||||||
|
|
||||||
|
*state->current++ = pc;
|
||||||
|
return _URC_NO_REASON;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::size_t capture_android_backtrace(void** frames, std::size_t max_frames) noexcept
|
||||||
|
{
|
||||||
|
AndroidBacktraceState state {
|
||||||
|
.current = frames,
|
||||||
|
.end = frames + max_frames,
|
||||||
|
};
|
||||||
|
|
||||||
|
_Unwind_Backtrace(android_unwind_callback, &state);
|
||||||
|
return static_cast<std::size_t>(state.current - frames);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_android_crash_signal(const char* signal_reason) noexcept
|
||||||
|
{
|
||||||
|
__android_log_write(ANDROID_LOG_FATAL, AndroidCrashLogTag, "=== FATAL ERROR ===");
|
||||||
|
if (signal_reason != nullptr) {
|
||||||
|
__android_log_print(ANDROID_LOG_FATAL, AndroidCrashLogTag, "Fatal Signal: %s", signal_reason);
|
||||||
|
}
|
||||||
|
__android_log_write(ANDROID_LOG_FATAL, AndroidCrashLogTag, "===================");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_android_crash_terminate(const char* exception_info) noexcept
|
||||||
|
{
|
||||||
|
__android_log_write(ANDROID_LOG_FATAL, AndroidCrashLogTag, "=== FATAL ERROR ===");
|
||||||
|
if (exception_info != nullptr) {
|
||||||
|
__android_log_print(ANDROID_LOG_FATAL, AndroidCrashLogTag, "Unhandled exception: %s", exception_info);
|
||||||
|
}
|
||||||
|
__android_log_write(ANDROID_LOG_FATAL, AndroidCrashLogTag, "===================");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dump_android_stacktrace(Logger& logger) noexcept
|
||||||
|
{
|
||||||
|
std::array<void*, 32> frames { };
|
||||||
|
const auto frame_count = capture_android_backtrace(frames.data(), frames.size());
|
||||||
|
|
||||||
|
std::size_t frame_start = 0;
|
||||||
|
while (frame_start < frame_count && frames[frame_start] == nullptr) {
|
||||||
|
++frame_start;
|
||||||
|
}
|
||||||
|
if (frame_start < frame_count) {
|
||||||
|
++frame_start; // Skip the helper frame itself.
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto dump_count = frame_count > frame_start ? frame_count - frame_start : 0;
|
||||||
|
__android_log_print(ANDROID_LOG_FATAL, AndroidCrashLogTag, "Native backtrace (%zu frames):", dump_count);
|
||||||
|
logger.error("Native backtrace", dump_count, "frames");
|
||||||
|
|
||||||
|
for (std::size_t i = frame_start; i < frame_count; ++i) {
|
||||||
|
Dl_info info { };
|
||||||
|
std::string frame_message;
|
||||||
|
if (dladdr(frames[i], &info) != 0 && info.dli_sname != nullptr) {
|
||||||
|
const auto symbol_name = utils::demangle(info.dli_sname);
|
||||||
|
const auto base = reinterpret_cast<std::uintptr_t>(info.dli_saddr);
|
||||||
|
const auto pc = reinterpret_cast<std::uintptr_t>(frames[i]);
|
||||||
|
const auto offset = pc >= base ? pc - base : 0;
|
||||||
|
frame_message = std::format(
|
||||||
|
"#{:02} pc {:p} {} ({}+0x{:x})",
|
||||||
|
i - frame_start,
|
||||||
|
frames[i],
|
||||||
|
info.dli_fname != nullptr ? info.dli_fname : "<unknown>",
|
||||||
|
symbol_name,
|
||||||
|
offset);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
frame_message = std::format("#{:02} pc {:p}", i - frame_start, frames[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
__android_log_write(ANDROID_LOG_FATAL, AndroidCrashLogTag, frame_message.c_str());
|
||||||
|
logger.error(frame_message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
static void custom_terminate_handler() noexcept
|
static void custom_terminate_handler() noexcept
|
||||||
{
|
{
|
||||||
static bool in_handler = false;
|
static bool in_handler = false;
|
||||||
@@ -908,18 +1035,10 @@ private:
|
|||||||
}
|
}
|
||||||
in_handler = true;
|
in_handler = true;
|
||||||
|
|
||||||
|
const int last_signal = g_last_signal.exchange(0);
|
||||||
|
const char* signal_info = last_signal != 0 ? format_signal_reason(last_signal) : nullptr;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
auto& logger = Logger::get_instance();
|
|
||||||
|
|
||||||
// 先写信号信息
|
|
||||||
if (auto sig_reason = g_last_signal_reason.load()) {
|
|
||||||
logger.error("=== FATAL ERROR ===");
|
|
||||||
logger.error("Signal caught:", sig_reason);
|
|
||||||
logger.flush();
|
|
||||||
write_crash_file("Fatal Signal", sig_reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 再处理 C++ 异常
|
|
||||||
std::string exception_info = "Unknown exception";
|
std::string exception_info = "Unknown exception";
|
||||||
if (auto eptr = std::current_exception()) {
|
if (auto eptr = std::current_exception()) {
|
||||||
try {
|
try {
|
||||||
@@ -933,11 +1052,29 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
if (last_signal == 0) {
|
||||||
|
log_android_crash_terminate(exception_info.c_str());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
auto& logger = Logger::get_instance();
|
||||||
|
|
||||||
|
if (signal_info != nullptr) {
|
||||||
|
logger.error("=== FATAL ERROR ===");
|
||||||
|
logger.error("Signal caught:", signal_info);
|
||||||
|
logger.flush();
|
||||||
|
write_crash_file("Fatal Signal", signal_info);
|
||||||
|
}
|
||||||
|
|
||||||
logger.error("=== FATAL ERROR ===");
|
logger.error("=== FATAL ERROR ===");
|
||||||
logger.error("Version", MAA_VERSION);
|
logger.error("Version", MAA_VERSION);
|
||||||
logger.error("Built at", __DATE__, __TIME__);
|
logger.error("Built at", __DATE__, __TIME__);
|
||||||
logger.error("User Dir", UserDir.get());
|
logger.error("User Dir", UserDir.get());
|
||||||
logger.error("Unhandled exception caught:", exception_info);
|
logger.error("Unhandled exception caught:", exception_info);
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
dump_android_stacktrace(logger);
|
||||||
|
#endif
|
||||||
logger.error("Program terminating...");
|
logger.error("Program terminating...");
|
||||||
logger.error("===================");
|
logger.error("===================");
|
||||||
logger.flush();
|
logger.flush();
|
||||||
@@ -953,40 +1090,36 @@ private:
|
|||||||
|
|
||||||
static void signal_handler(int sig)
|
static void signal_handler(int sig)
|
||||||
{
|
{
|
||||||
std::string sig_name;
|
#ifdef __ANDROID__
|
||||||
switch (sig) {
|
log_android_crash_signal(format_signal_reason(sig));
|
||||||
case SIGSEGV:
|
#endif
|
||||||
sig_name = "SIGSEGV (Segmentation Fault)";
|
g_last_signal.store(sig);
|
||||||
break;
|
|
||||||
case SIGABRT:
|
|
||||||
sig_name = "SIGABRT (Abort)";
|
|
||||||
break;
|
|
||||||
case SIGFPE:
|
|
||||||
sig_name = "SIGFPE (Floating Point Error)";
|
|
||||||
break;
|
|
||||||
case SIGILL:
|
|
||||||
sig_name = "SIGILL (Illegal Instruction)";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
sig_name = "Signal " + std::to_string(sig);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
g_last_signal_reason.store(sig_name.c_str());
|
|
||||||
custom_terminate_handler();
|
custom_terminate_handler();
|
||||||
std::_Exit(EXIT_FAILURE);
|
std::_Exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
[[noreturn]] static void android_terminate_handler() noexcept
|
||||||
|
{
|
||||||
|
custom_terminate_handler();
|
||||||
|
std::_Exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
static void initialize_exception_handlers()
|
static void initialize_exception_handlers()
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
// Windows: 设置未处理异常过滤器
|
// Windows: 设置未处理异常过滤器
|
||||||
SetUnhandledExceptionFilter(unhandled_exception_filter);
|
SetUnhandledExceptionFilter(unhandled_exception_filter);
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
std::set_terminate(android_terminate_handler);
|
||||||
|
#endif
|
||||||
std::signal(SIGSEGV, signal_handler);
|
std::signal(SIGSEGV, signal_handler);
|
||||||
std::signal(SIGABRT, signal_handler);
|
std::signal(SIGABRT, signal_handler);
|
||||||
std::signal(SIGFPE, signal_handler);
|
std::signal(SIGFPE, signal_handler);
|
||||||
std::signal(SIGILL, signal_handler);
|
std::signal(SIGILL, signal_handler);
|
||||||
|
|
||||||
#ifdef ASST_DEBUG
|
#ifdef ASST_DEBUG
|
||||||
const auto& path = UserDir.get() / "debug" / "crash.log";
|
const auto& path = UserDir.get() / "debug" / "crash.log";
|
||||||
if (std::filesystem::exists(path)) {
|
if (std::filesystem::exists(path)) {
|
||||||
@@ -1047,7 +1180,7 @@ private:
|
|||||||
std::ostream m_of;
|
std::ostream m_of;
|
||||||
std::size_t m_file_size = 0;
|
std::size_t m_file_size = 0;
|
||||||
|
|
||||||
static inline utils::NullStreambuf null_buf {};
|
static inline utils::NullStreambuf null_buf { };
|
||||||
static inline std::ostream null_stream { &null_buf };
|
static inline std::ostream null_stream { &null_buf };
|
||||||
const std::size_t MaxLogSize = 64LL * 1024 * 1024;
|
const std::size_t MaxLogSize = 64LL * 1024 * 1024;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user