mirror of
https://github.com/MaaAssistantArknights/MaaAssistantArknights.git
synced 2026-07-16 01:40:46 +08:00
1531 lines
53 KiB
C++
1531 lines
53 KiB
C++
#include "Controller.h"
|
||
#include "Utils/AsstConf.h"
|
||
#include "Utils/Platform/AsstPlatform.h"
|
||
|
||
#ifdef _WIN32
|
||
#include "Utils/Platform/AsstPlatformWin32.h"
|
||
#include <ws2tcpip.h>
|
||
#else
|
||
#include <fcntl.h>
|
||
#include <sys/errno.h>
|
||
#include <sys/wait.h>
|
||
#include <unistd.h>
|
||
#endif
|
||
|
||
#include <algorithm>
|
||
#include <chrono>
|
||
#include <cstdint>
|
||
#include <memory>
|
||
#include <utility>
|
||
#include <vector>
|
||
|
||
#include "Utils/NoWarningCV.h"
|
||
|
||
#ifdef _MSC_VER
|
||
#pragma warning(push)
|
||
#pragma warning(disable : 4068)
|
||
#endif
|
||
#include <zlib/decompress.hpp>
|
||
#ifdef _MSC_VER
|
||
#pragma warning(pop)
|
||
#endif
|
||
|
||
#include "Resource/GeneralConfiger.h"
|
||
#include "Utils/AsstTypes.h"
|
||
#include "Utils/Logger.hpp"
|
||
#include "Utils/StringMisc.hpp"
|
||
|
||
asst::Controller::Controller(AsstCallback callback, void* callback_arg)
|
||
: m_callback(std::move(callback)), m_callback_arg(callback_arg), m_rand_engine(std::random_device {}())
|
||
{
|
||
LogTraceFunction;
|
||
|
||
#ifdef _WIN32
|
||
m_support_socket = WsaHelper::get_instance()();
|
||
#else
|
||
int pipe_in_ret = ::pipe(m_pipe_in);
|
||
int pipe_out_ret = ::pipe(m_pipe_out);
|
||
::fcntl(m_pipe_out[PIPE_READ], F_SETFL, O_NONBLOCK);
|
||
|
||
if (pipe_in_ret < 0 || pipe_out_ret < 0) {
|
||
Log.error(__FUNCTION__, "controller pipe created failed", pipe_in_ret, pipe_out_ret);
|
||
}
|
||
m_support_socket = true;
|
||
#endif
|
||
|
||
if (!m_support_socket) {
|
||
Log.error("sokcet not supports");
|
||
}
|
||
}
|
||
|
||
asst::Controller::~Controller()
|
||
{
|
||
LogTraceFunction;
|
||
|
||
release_minitouch();
|
||
make_instance_inited(false);
|
||
kill_adb_daemon();
|
||
|
||
#ifndef _WIN32
|
||
::close(m_pipe_in[PIPE_READ]);
|
||
::close(m_pipe_in[PIPE_WRITE]);
|
||
::close(m_pipe_out[PIPE_READ]);
|
||
::close(m_pipe_out[PIPE_WRITE]);
|
||
#endif
|
||
}
|
||
|
||
std::pair<int, int> asst::Controller::get_scale_size() const noexcept
|
||
{
|
||
return m_scale_size;
|
||
}
|
||
|
||
bool asst::Controller::need_exit() const
|
||
{
|
||
return m_exit_flag != nullptr && *m_exit_flag;
|
||
}
|
||
|
||
std::optional<std::string> asst::Controller::call_command(const std::string& cmd, int64_t timeout, bool allow_reconnect,
|
||
bool recv_by_socket)
|
||
{
|
||
using namespace std::chrono_literals;
|
||
using namespace std::chrono;
|
||
// LogTraceScope(std::string(__FUNCTION__) + " | `" + cmd + "`");
|
||
|
||
std::string pipe_data;
|
||
std::string sock_data;
|
||
asst::platform::single_page_buffer<char> pipe_buffer;
|
||
std::optional<asst::platform::single_page_buffer<char>> sock_buffer;
|
||
|
||
auto start_time = steady_clock::now();
|
||
std::unique_lock<std::mutex> callcmd_lock(m_callcmd_mutex);
|
||
|
||
#ifdef _WIN32
|
||
|
||
DWORD err = 0;
|
||
HANDLE pipe_parent_read = INVALID_HANDLE_VALUE, pipe_child_write = INVALID_HANDLE_VALUE;
|
||
SECURITY_ATTRIBUTES sa_inherit { .nLength = sizeof(SECURITY_ATTRIBUTES), .bInheritHandle = TRUE };
|
||
if (!asst::win32::CreateOverlappablePipe(&pipe_parent_read, &pipe_child_write, nullptr, &sa_inherit,
|
||
(DWORD)pipe_buffer.size(), true, false)) {
|
||
err = GetLastError();
|
||
Log.error("CreateOverlappablePipe failed, err", err);
|
||
return std::nullopt;
|
||
}
|
||
|
||
STARTUPINFOW si {};
|
||
si.cb = sizeof(STARTUPINFOW);
|
||
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
|
||
si.wShowWindow = SW_HIDE;
|
||
si.hStdOutput = pipe_child_write;
|
||
si.hStdError = pipe_child_write;
|
||
ASST_AUTO_DEDUCED_ZERO_INIT_START
|
||
PROCESS_INFORMATION process_info = { nullptr }; // 进程信息结构体
|
||
ASST_AUTO_DEDUCED_ZERO_INIT_END
|
||
|
||
auto cmdline_osstr = asst::utils::to_osstring(cmd);
|
||
BOOL create_ret =
|
||
CreateProcessW(nullptr, cmdline_osstr.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &process_info);
|
||
if (!create_ret) {
|
||
Log.error("Call `", cmd, "` create process failed, ret", create_ret);
|
||
return std::nullopt;
|
||
}
|
||
|
||
CloseHandle(pipe_child_write);
|
||
pipe_child_write = INVALID_HANDLE_VALUE;
|
||
|
||
std::vector<HANDLE> wait_handles;
|
||
wait_handles.reserve(3);
|
||
bool process_running = true;
|
||
bool pipe_eof = false;
|
||
bool accept_pending = false;
|
||
bool socket_eof = false;
|
||
|
||
OVERLAPPED pipeov { .hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr) };
|
||
(void)ReadFile(pipe_parent_read, pipe_buffer.get(), (DWORD)pipe_buffer.size(), nullptr, &pipeov);
|
||
|
||
OVERLAPPED sockov {};
|
||
SOCKET client_socket = INVALID_SOCKET;
|
||
|
||
if (recv_by_socket) {
|
||
sock_buffer = asst::platform::single_page_buffer<char>();
|
||
sockov.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||
client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||
DWORD dummy;
|
||
if (!m_server_accept_ex(m_server_sock, client_socket, sock_buffer.value().get(),
|
||
(DWORD)sock_buffer.value().size() - ((sizeof(sockaddr_in) + 16) * 2),
|
||
sizeof(sockaddr_in) + 16, sizeof(sockaddr_in) + 16, &dummy, &sockov)) {
|
||
err = WSAGetLastError();
|
||
if (err == ERROR_IO_PENDING) {
|
||
accept_pending = true;
|
||
}
|
||
else {
|
||
Log.trace("AcceptEx failed, err:", err);
|
||
accept_pending = false;
|
||
socket_eof = true;
|
||
::closesocket(client_socket);
|
||
}
|
||
}
|
||
}
|
||
|
||
while (!need_exit()) {
|
||
wait_handles.clear();
|
||
if (process_running) wait_handles.push_back(process_info.hProcess);
|
||
if (!pipe_eof) wait_handles.push_back(pipeov.hEvent);
|
||
if (recv_by_socket && ((accept_pending && process_running) || !socket_eof)) {
|
||
wait_handles.push_back(sockov.hEvent);
|
||
}
|
||
if (wait_handles.empty()) break;
|
||
auto elapsed = steady_clock::now() - start_time;
|
||
// TODO: 这里目前是隔 5000ms 判断一次,应该可以加一个 wait_handle 来判断外部中断(need_exit)
|
||
auto wait_time =
|
||
(std::min)(timeout - duration_cast<milliseconds>(elapsed).count(), process_running ? 5LL * 1000 : 0LL);
|
||
if (wait_time < 0) break;
|
||
auto wait_result =
|
||
WaitForMultipleObjectsEx((DWORD)wait_handles.size(), wait_handles.data(), FALSE, (DWORD)wait_time, TRUE);
|
||
HANDLE signaled_object = INVALID_HANDLE_VALUE;
|
||
if (wait_result >= WAIT_OBJECT_0 && wait_result < WAIT_OBJECT_0 + wait_handles.size()) {
|
||
signaled_object = wait_handles[(size_t)wait_result - WAIT_OBJECT_0];
|
||
}
|
||
else if (wait_result == WAIT_TIMEOUT) {
|
||
if (wait_time == 0) {
|
||
std::vector<std::string> handle_string {};
|
||
for (auto handle : wait_handles) {
|
||
if (handle == process_info.hProcess) {
|
||
handle_string.emplace_back("process_info.hProcess");
|
||
}
|
||
else if (handle == pipeov.hEvent) {
|
||
handle_string.emplace_back("pipeov.hEvent");
|
||
}
|
||
else if (recv_by_socket && handle == sockov.hEvent) {
|
||
handle_string.emplace_back("sockov.hEvent");
|
||
}
|
||
else {
|
||
handle_string.emplace_back("UnknownHandle");
|
||
}
|
||
}
|
||
Log.warn("Wait handles:", handle_string, "timeout.");
|
||
if (process_running) {
|
||
TerminateProcess(process_info.hProcess, 0);
|
||
}
|
||
break;
|
||
}
|
||
continue;
|
||
}
|
||
else {
|
||
// something bad happened
|
||
err = GetLastError();
|
||
// throw std::system_error(std::error_code(err, std::system_category()));
|
||
Log.error(__FUNCTION__, "A fatal error occurred", err);
|
||
break;
|
||
}
|
||
|
||
if (signaled_object == process_info.hProcess) {
|
||
process_running = false;
|
||
}
|
||
else if (signaled_object == pipeov.hEvent) {
|
||
// pipe read
|
||
DWORD len = 0;
|
||
if (GetOverlappedResult(pipe_parent_read, &pipeov, &len, FALSE)) {
|
||
pipe_data.insert(pipe_data.end(), pipe_buffer.get(), pipe_buffer.get() + len);
|
||
(void)ReadFile(pipe_parent_read, pipe_buffer.get(), (DWORD)pipe_buffer.size(), nullptr, &pipeov);
|
||
}
|
||
else {
|
||
err = GetLastError();
|
||
if (err == ERROR_HANDLE_EOF || err == ERROR_BROKEN_PIPE) {
|
||
pipe_eof = true;
|
||
}
|
||
}
|
||
}
|
||
else if (signaled_object == sockov.hEvent) {
|
||
if (accept_pending) {
|
||
// AcceptEx, client_socker is connected and first chunk of data is received
|
||
DWORD len = 0;
|
||
if (GetOverlappedResult(reinterpret_cast<HANDLE>(m_server_sock), &sockov, &len, FALSE)) {
|
||
accept_pending = false;
|
||
if (recv_by_socket)
|
||
sock_data.insert(sock_data.end(), sock_buffer.value().get(), sock_buffer.value().get() + len);
|
||
|
||
if (len == 0) {
|
||
socket_eof = true;
|
||
::closesocket(client_socket);
|
||
}
|
||
else {
|
||
// reset the overlapped since we reuse it for different handle
|
||
auto event = sockov.hEvent;
|
||
sockov = {};
|
||
sockov.hEvent = event;
|
||
|
||
(void)ReadFile(reinterpret_cast<HANDLE>(client_socket), sock_buffer.value().get(),
|
||
(DWORD)sock_buffer.value().size(), nullptr, &sockov);
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
// ReadFile
|
||
DWORD len = 0;
|
||
if (GetOverlappedResult(reinterpret_cast<HANDLE>(client_socket), &sockov, &len, FALSE)) {
|
||
if (recv_by_socket)
|
||
sock_data.insert(sock_data.end(), sock_buffer.value().get(), sock_buffer.value().get() + len);
|
||
if (len == 0) {
|
||
socket_eof = true;
|
||
::closesocket(client_socket);
|
||
}
|
||
else {
|
||
(void)ReadFile(reinterpret_cast<HANDLE>(client_socket), sock_buffer.value().get(),
|
||
(DWORD)sock_buffer.value().size(), nullptr, &sockov);
|
||
}
|
||
}
|
||
else {
|
||
// err = GetLastError();
|
||
socket_eof = true;
|
||
::closesocket(client_socket);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
DWORD exit_ret = 0;
|
||
GetExitCodeProcess(process_info.hProcess, &exit_ret);
|
||
CloseHandle(process_info.hProcess);
|
||
CloseHandle(process_info.hThread);
|
||
CloseHandle(pipe_parent_read);
|
||
CloseHandle(pipeov.hEvent);
|
||
if (recv_by_socket) {
|
||
if (!socket_eof) closesocket(client_socket);
|
||
CloseHandle(sockov.hEvent);
|
||
}
|
||
|
||
#else
|
||
auto check_timeout = [&]() -> bool {
|
||
return timeout && timeout < duration_cast<milliseconds>(steady_clock::now() - start_time).count();
|
||
};
|
||
|
||
int exit_ret = 0;
|
||
m_child = ::fork();
|
||
if (m_child == 0) {
|
||
// child process
|
||
|
||
::dup2(m_pipe_in[PIPE_READ], STDIN_FILENO);
|
||
::dup2(m_pipe_out[PIPE_WRITE], STDOUT_FILENO);
|
||
::dup2(m_pipe_out[PIPE_WRITE], STDERR_FILENO);
|
||
|
||
// all these are for use by parent only
|
||
// close(m_pipe_in[PIPE_READ]);
|
||
// close(m_pipe_in[PIPE_WRITE]);
|
||
// close(m_pipe_out[PIPE_READ]);
|
||
// close(m_pipe_out[PIPE_WRITE]);
|
||
|
||
exit_ret = execlp("sh", "sh", "-c", cmd.c_str(), nullptr);
|
||
::exit(exit_ret);
|
||
}
|
||
else if (m_child > 0) {
|
||
// parent process
|
||
do {
|
||
if (recv_by_socket) {
|
||
sockaddr addr {};
|
||
socklen_t len = sizeof(addr);
|
||
sock_buffer = asst::platform::single_page_buffer<char>();
|
||
|
||
int client_socket = ::accept(m_server_sock, &addr, &len);
|
||
if (client_socket < 0) {
|
||
Log.error("accept failed:", strerror(errno));
|
||
return std::nullopt;
|
||
}
|
||
|
||
ssize_t read_num = ::read(client_socket, sock_buffer.value().get(), sock_buffer.value().size());
|
||
|
||
while (read_num > 0) {
|
||
sock_data.insert(sock_data.end(), sock_buffer.value().get(), sock_buffer.value().get() + read_num);
|
||
read_num = ::read(client_socket, sock_buffer.value().get(), sock_buffer.value().size());
|
||
}
|
||
|
||
::close(client_socket);
|
||
break;
|
||
}
|
||
|
||
ssize_t read_num = ::read(m_pipe_out[PIPE_READ], pipe_buffer.get(), pipe_buffer.size());
|
||
|
||
while (read_num > 0) {
|
||
pipe_data.insert(pipe_data.end(), pipe_buffer.get(), pipe_buffer.get() + read_num);
|
||
read_num = ::read(m_pipe_out[PIPE_READ], pipe_buffer.get(), pipe_buffer.size());
|
||
}
|
||
} while (::waitpid(m_child, &exit_ret, WNOHANG) == 0 && !check_timeout());
|
||
}
|
||
else {
|
||
// failed to create child process
|
||
Log.error("Call `", cmd, "` create process failed, child:", m_child);
|
||
return std::nullopt;
|
||
}
|
||
#endif
|
||
|
||
callcmd_lock.unlock();
|
||
|
||
auto duration = duration_cast<milliseconds>(steady_clock::now() - start_time).count();
|
||
Log.info("Call `", cmd, "` ret", exit_ret, ", cost", duration, "ms , stdout size:", pipe_data.size(),
|
||
", socket size:", sock_data.size());
|
||
if (!pipe_data.empty() && pipe_data.size() < 4096) {
|
||
Log.trace("stdout output:", Logger::separator::newline, pipe_data);
|
||
}
|
||
if (recv_by_socket && !sock_data.empty() && sock_data.size() < 4096) {
|
||
Log.trace("socket output:", Logger::separator::newline, sock_data);
|
||
}
|
||
// 直接 return,避免走到下面的 else if 里的 make_instance_inited(false) 关闭 adb 连接,
|
||
// 导致停止后再开始任务还需要重连一次
|
||
if (need_exit()) {
|
||
return std::nullopt;
|
||
}
|
||
|
||
if (!exit_ret) {
|
||
return recv_by_socket ? sock_data : pipe_data;
|
||
}
|
||
else if (inited() && allow_reconnect) {
|
||
// 之前可以运行,突然运行不了了,这种情况多半是 adb 炸了。所以重新连接一下
|
||
json::value reconnect_info = json::object {
|
||
{ "uuid", m_uuid },
|
||
{ "what", "Reconnecting" },
|
||
{ "why", "" },
|
||
{ "details",
|
||
json::object {
|
||
{ "reconnect", m_adb.connect },
|
||
{ "cmd", cmd },
|
||
} },
|
||
};
|
||
static constexpr int ReconnectTimes = 20;
|
||
for (int i = 0; i < ReconnectTimes; ++i) {
|
||
if (need_exit()) {
|
||
break;
|
||
}
|
||
reconnect_info["details"]["times"] = i;
|
||
callback(AsstMsg::ConnectionInfo, reconnect_info);
|
||
|
||
// TODO: 也许 WIN32 可以用 WaitForSingleObjectEx 做一个允许外部打断的 sleep
|
||
std::this_thread::sleep_for(10s);
|
||
if (need_exit()) {
|
||
break;
|
||
}
|
||
auto reconnect_ret = call_command(m_adb.connect, 60LL * 1000, false /* 禁止重连避免无限递归 */);
|
||
if (need_exit()) {
|
||
break;
|
||
}
|
||
bool is_reconnect_success = false;
|
||
if (reconnect_ret) {
|
||
auto& reconnect_str = reconnect_ret.value();
|
||
is_reconnect_success = reconnect_str.find("error") == std::string::npos;
|
||
}
|
||
if (is_reconnect_success) {
|
||
auto recall_ret = call_command(cmd, timeout, false /* 禁止重连避免无限递归 */, recv_by_socket);
|
||
if (recall_ret) {
|
||
// 重连并成功执行了
|
||
reconnect_info["what"] = "Reconnected";
|
||
callback(AsstMsg::ConnectionInfo, reconnect_info);
|
||
return recall_ret;
|
||
}
|
||
}
|
||
}
|
||
json::value info = json::object {
|
||
{ "uuid", m_uuid },
|
||
{ "what", "Disconnect" },
|
||
{ "why", "Reconnect failed" },
|
||
{ "details",
|
||
json::object {
|
||
{ "cmd", m_adb.connect },
|
||
} },
|
||
};
|
||
make_instance_inited(false); // 重连失败,释放
|
||
callback(AsstMsg::ConnectionInfo, info);
|
||
}
|
||
|
||
return std::nullopt;
|
||
}
|
||
|
||
void asst::Controller::callback(AsstMsg msg, const json::value& details)
|
||
{
|
||
if (m_callback) {
|
||
m_callback(msg, details, m_callback_arg);
|
||
}
|
||
}
|
||
|
||
bool asst::Controller::call_and_hup_minitouch(const std::string& cmd)
|
||
{
|
||
LogTraceFunction;
|
||
Log.info(cmd);
|
||
m_minitouch_avaiable = false;
|
||
|
||
constexpr int PipeReadBuffSize = 4096ULL;
|
||
constexpr int PipeWriteBuffSize = 64 * 1024ULL;
|
||
std::string pipe_str;
|
||
#ifdef _WIN32
|
||
SECURITY_ATTRIBUTES sa_attr_inherit {
|
||
.nLength = sizeof(SECURITY_ATTRIBUTES),
|
||
.lpSecurityDescriptor = nullptr,
|
||
.bInheritHandle = TRUE,
|
||
};
|
||
HANDLE pipe_parent_read = INVALID_HANDLE_VALUE, pipe_child_write = INVALID_HANDLE_VALUE;
|
||
HANDLE pipe_child_read = INVALID_HANDLE_VALUE, pipe_parent_write = INVALID_HANDLE_VALUE;
|
||
if (!asst::win32::CreateOverlappablePipe(&pipe_parent_read, &pipe_child_write, nullptr, &sa_attr_inherit,
|
||
PipeReadBuffSize, true, false) ||
|
||
!asst::win32::CreateOverlappablePipe(&pipe_child_read, &pipe_parent_write, &sa_attr_inherit, nullptr,
|
||
PipeWriteBuffSize, false, false)) {
|
||
DWORD err = GetLastError();
|
||
Log.error("Failed to create pipe for minitouch, err", err);
|
||
return false;
|
||
}
|
||
|
||
STARTUPINFOW si {};
|
||
si.cb = sizeof(STARTUPINFOW);
|
||
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
|
||
si.wShowWindow = SW_HIDE;
|
||
si.hStdInput = pipe_child_read;
|
||
si.hStdOutput = pipe_child_write;
|
||
si.hStdError = pipe_child_write;
|
||
|
||
auto cmd_osstr = utils::to_osstring(cmd);
|
||
BOOL create_ret = CreateProcessW(NULL, cmd_osstr.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si,
|
||
&m_minitouch_process_info);
|
||
CloseHandle(pipe_child_write);
|
||
CloseHandle(pipe_child_read);
|
||
pipe_child_write = INVALID_HANDLE_VALUE;
|
||
pipe_child_read = INVALID_HANDLE_VALUE;
|
||
|
||
if (!create_ret) {
|
||
DWORD err = GetLastError();
|
||
Log.error("Failed to create process for minitouch, err", err);
|
||
return false;
|
||
}
|
||
|
||
auto start_time = std::chrono::steady_clock::now();
|
||
auto check_timeout = [&]() -> bool {
|
||
using namespace std::chrono_literals;
|
||
return std::chrono::steady_clock::now() - start_time < 3s;
|
||
};
|
||
|
||
auto pipe_buffer = std::make_unique<char[]>(PipeReadBuffSize);
|
||
OVERLAPPED pipeov { .hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr) };
|
||
std::ignore = ReadFile(pipe_parent_read, pipe_buffer.get(), PipeReadBuffSize, nullptr, &pipeov);
|
||
|
||
while (!need_exit() && check_timeout()) {
|
||
if (pipe_str.find('$') != std::string::npos) {
|
||
break;
|
||
}
|
||
DWORD len = 0;
|
||
if (!GetOverlappedResult(pipe_parent_read, &pipeov, &len, FALSE)) {
|
||
continue;
|
||
}
|
||
pipe_str.insert(pipe_str.end(), pipe_buffer.get(), pipe_buffer.get() + len);
|
||
std::ignore = ReadFile(pipe_parent_read, pipe_buffer.get(), PipeReadBuffSize, nullptr, &pipeov);
|
||
}
|
||
|
||
CloseHandle(pipe_parent_read);
|
||
pipe_parent_read = INVALID_HANDLE_VALUE;
|
||
|
||
m_minitouch_parent_write = pipe_parent_write;
|
||
#else // !_WIN32
|
||
// TODO
|
||
std::ignore = pipe_str;
|
||
std::ignore = PipeReadBuffSize;
|
||
std::ignore = PipeWriteBuffSize;
|
||
return false;
|
||
#endif // _WIN32
|
||
|
||
Log.info("pipe str", Logger::separator::newline, pipe_str);
|
||
|
||
convert_lf(pipe_str);
|
||
size_t s_pos = pipe_str.find('^');
|
||
size_t e_pos = pipe_str.find('\n', s_pos);
|
||
if (s_pos == std::string::npos || e_pos == std::string::npos) {
|
||
Log.error("Failed to find ^ in minitouch pipe");
|
||
release_minitouch(true);
|
||
return false;
|
||
}
|
||
std::string key_info = pipe_str.substr(s_pos + 1, e_pos - s_pos - 1);
|
||
Log.info("minitouch key props", key_info);
|
||
std::stringstream ss;
|
||
ss << key_info;
|
||
ss >> m_minitouch_props.max_contacts;
|
||
ss >> m_minitouch_props.max_x;
|
||
ss >> m_minitouch_props.max_y;
|
||
ss >> m_minitouch_props.max_pressure;
|
||
|
||
m_minitouch_props.x_scaling = static_cast<double>(m_minitouch_props.max_x) / m_width;
|
||
m_minitouch_props.y_scaling = static_cast<double>(m_minitouch_props.max_y) / m_height;
|
||
|
||
m_minitouch_avaiable = true;
|
||
return true;
|
||
}
|
||
|
||
bool asst::Controller::input_to_minitouch(const std::string& cmd)
|
||
{
|
||
Log.info("Input to minitouch", Logger::separator::newline, cmd);
|
||
|
||
#ifdef _WIN32
|
||
DWORD written = 0;
|
||
if (!WriteFile(m_minitouch_parent_write, cmd.c_str(),
|
||
static_cast<DWORD>(cmd.size() * sizeof(std::string::value_type)), &written, NULL)) {
|
||
auto err = GetLastError();
|
||
Log.error("Failed to write to minitouch, err", err);
|
||
return false;
|
||
}
|
||
|
||
return cmd.size() == written;
|
||
#else
|
||
return false;
|
||
#endif
|
||
}
|
||
|
||
void asst::Controller::release_minitouch(bool force)
|
||
{
|
||
LogTraceFunction;
|
||
|
||
if (!m_minitouch_avaiable && !force) {
|
||
return;
|
||
}
|
||
m_minitouch_avaiable = false;
|
||
|
||
#ifdef _WIN32
|
||
|
||
if (m_minitouch_process_info.hProcess) {
|
||
CloseHandle(m_minitouch_process_info.hProcess);
|
||
m_minitouch_process_info.hProcess = nullptr;
|
||
}
|
||
if (m_minitouch_process_info.hThread) {
|
||
CloseHandle(m_minitouch_process_info.hThread);
|
||
m_minitouch_process_info.hThread = nullptr;
|
||
}
|
||
if (m_minitouch_parent_write) {
|
||
CloseHandle(m_minitouch_parent_write);
|
||
m_minitouch_parent_write = nullptr;
|
||
}
|
||
|
||
#endif // _WIN32
|
||
}
|
||
|
||
// 返回值代表是否找到 "\r\n",函数本身会将所有 "\r\n" 替换为 "\n"
|
||
bool asst::Controller::convert_lf(std::string& data)
|
||
{
|
||
LogTraceFunction;
|
||
|
||
if (data.empty() || data.size() < 2) {
|
||
return false;
|
||
}
|
||
auto pred = [](const std::string::iterator& cur) -> bool { return *cur == '\r' && *(cur + 1) == '\n'; };
|
||
// find the first of "\r\n"
|
||
auto first_iter = data.end();
|
||
for (auto iter = data.begin(); iter != data.end() - 1; ++iter) {
|
||
if (pred(iter)) {
|
||
first_iter = iter;
|
||
break;
|
||
}
|
||
}
|
||
if (first_iter == data.end()) {
|
||
return false;
|
||
}
|
||
// move forward all non-crlf elements
|
||
auto end_r1_iter = data.end() - 1;
|
||
auto next_iter = first_iter;
|
||
while (++first_iter != end_r1_iter) {
|
||
if (!pred(first_iter)) {
|
||
*next_iter = *first_iter;
|
||
++next_iter;
|
||
}
|
||
}
|
||
*next_iter = *end_r1_iter;
|
||
++next_iter;
|
||
data.erase(next_iter, data.end());
|
||
return true;
|
||
}
|
||
|
||
asst::Point asst::Controller::rand_point_in_rect(const Rect& rect)
|
||
{
|
||
int x = 0, y = 0;
|
||
if (rect.width == 0) {
|
||
x = rect.x;
|
||
}
|
||
else {
|
||
int x_rand = std::poisson_distribution<int>(rect.width / 2.)(m_rand_engine);
|
||
|
||
x = x_rand + rect.x;
|
||
}
|
||
|
||
if (rect.height == 0) {
|
||
y = rect.y;
|
||
}
|
||
else {
|
||
int y_rand = std::poisson_distribution<int>(rect.height / 2.)(m_rand_engine);
|
||
y = y_rand + rect.y;
|
||
}
|
||
|
||
return { x, y };
|
||
}
|
||
|
||
void asst::Controller::random_delay() const
|
||
{
|
||
auto& opt = Configer.get_options();
|
||
if (opt.control_delay_upper != 0) {
|
||
LogTraceFunction;
|
||
static std::default_random_engine rand_engine(std::random_device {}());
|
||
static std::uniform_int_distribution<unsigned> rand_uni(opt.control_delay_lower, opt.control_delay_upper);
|
||
|
||
unsigned rand_delay = rand_uni(rand_engine);
|
||
|
||
Log.trace("random_delay |", rand_delay, "ms");
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(rand_delay));
|
||
}
|
||
}
|
||
|
||
void asst::Controller::clear_info() noexcept
|
||
{
|
||
make_instance_inited(false);
|
||
m_adb = decltype(m_adb)();
|
||
m_uuid.clear();
|
||
m_width = 0;
|
||
m_height = 0;
|
||
m_control_scale = 1.0;
|
||
m_minitouch_avaiable = false;
|
||
m_scale_size = { WindowWidthDefault, WindowHeightDefault };
|
||
}
|
||
|
||
void asst::Controller::close_socket() noexcept
|
||
{
|
||
#ifdef _WIN32
|
||
if (m_server_sock != INVALID_SOCKET) {
|
||
::closesocket(m_server_sock);
|
||
m_server_sock = INVALID_SOCKET;
|
||
}
|
||
#else
|
||
if (m_server_sock >= 0) {
|
||
::close(m_server_sock);
|
||
m_server_sock = -1;
|
||
}
|
||
#endif
|
||
m_server_started = false;
|
||
}
|
||
|
||
std::optional<unsigned short> asst::Controller::init_socket(const std::string& local_address)
|
||
{
|
||
LogTraceFunction;
|
||
|
||
#ifdef _WIN32
|
||
if (m_server_sock == INVALID_SOCKET) {
|
||
m_server_sock = ::socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||
if (m_server_sock == INVALID_SOCKET) {
|
||
return std::nullopt;
|
||
}
|
||
}
|
||
|
||
DWORD dummy = 0;
|
||
GUID guid_accept_ex = WSAID_ACCEPTEX;
|
||
int err = WSAIoctl(m_server_sock, SIO_GET_EXTENSION_FUNCTION_POINTER, &guid_accept_ex, sizeof(guid_accept_ex),
|
||
&m_server_accept_ex, sizeof(m_server_accept_ex), &dummy, NULL, NULL);
|
||
if (err == SOCKET_ERROR) {
|
||
err = WSAGetLastError();
|
||
Log.error("failed to resolve AcceptEx, err:", err);
|
||
::closesocket(m_server_sock);
|
||
return std::nullopt;
|
||
}
|
||
m_server_sock_addr.sin_family = PF_INET;
|
||
::inet_pton(AF_INET, local_address.c_str(), &m_server_sock_addr.sin_addr);
|
||
#else
|
||
m_server_sock = ::socket(AF_INET, SOCK_STREAM, 0);
|
||
if (m_server_sock < 0) {
|
||
return std::nullopt;
|
||
}
|
||
|
||
m_server_sock_addr.sin_family = AF_INET;
|
||
m_server_sock_addr.sin_addr.s_addr = INADDR_ANY;
|
||
#endif
|
||
|
||
bool server_start = false;
|
||
uint16_t port_result = 0;
|
||
|
||
#ifdef _WIN32
|
||
m_server_sock_addr.sin_port = ::htons(0);
|
||
int bind_ret = ::bind(m_server_sock, reinterpret_cast<SOCKADDR*>(&m_server_sock_addr), sizeof(SOCKADDR));
|
||
int addrlen = sizeof(m_server_sock_addr);
|
||
int getname_ret = ::getsockname(m_server_sock, reinterpret_cast<sockaddr*>(&m_server_sock_addr), &addrlen);
|
||
int listen_ret = ::listen(m_server_sock, 3);
|
||
server_start = bind_ret == 0 && getname_ret == 0 && listen_ret == 0;
|
||
#else
|
||
m_server_sock_addr.sin_port = htons(0);
|
||
int bind_ret = ::bind(m_server_sock, reinterpret_cast<sockaddr*>(&m_server_sock_addr), sizeof(::sockaddr_in));
|
||
socklen_t addrlen = sizeof(m_server_sock_addr);
|
||
int getname_ret = ::getsockname(m_server_sock, reinterpret_cast<sockaddr*>(&m_server_sock_addr), &addrlen);
|
||
int listen_ret = ::listen(m_server_sock, 3);
|
||
server_start = bind_ret == 0 && getname_ret == 0 && listen_ret == 0;
|
||
#endif
|
||
|
||
if (!server_start) {
|
||
Log.info("not supports socket");
|
||
return std::nullopt;
|
||
}
|
||
|
||
#ifdef _WIN32
|
||
port_result = ::ntohs(m_server_sock_addr.sin_port);
|
||
#else
|
||
port_result = ntohs(m_server_sock_addr.sin_port);
|
||
#endif
|
||
|
||
Log.info("command server start", local_address, port_result);
|
||
return port_result;
|
||
}
|
||
|
||
bool asst::Controller::screencap(bool allow_reconnect)
|
||
{
|
||
DecodeFunc decode_raw = [&](const std::string& data) -> bool {
|
||
if (data.empty()) {
|
||
return false;
|
||
}
|
||
size_t std_size = 4ULL * m_width * m_height;
|
||
if (data.size() < std_size) {
|
||
return false;
|
||
}
|
||
size_t header_size = data.size() - std_size;
|
||
auto img_data_beg = data.cbegin() + header_size;
|
||
if (std::all_of(data.cbegin(), img_data_beg, std::logical_not<bool> {})) {
|
||
return false;
|
||
}
|
||
cv::Mat temp(m_height, m_width, CV_8UC4, const_cast<char*>(&*img_data_beg));
|
||
if (temp.empty()) {
|
||
return false;
|
||
}
|
||
cv::cvtColor(temp, temp, cv::COLOR_RGB2BGR);
|
||
std::unique_lock<std::shared_mutex> image_lock(m_image_mutex);
|
||
m_cache_image = temp;
|
||
return true;
|
||
};
|
||
|
||
DecodeFunc decode_raw_with_gzip = [&](const std::string& data) -> bool {
|
||
const std::string raw_data = gzip::decompress(data.data(), data.size());
|
||
return decode_raw(raw_data);
|
||
};
|
||
|
||
DecodeFunc decode_encode = [&](const std::string& data) -> bool {
|
||
cv::Mat temp = cv::imdecode({ data.data(), int(data.size()) }, cv::IMREAD_COLOR);
|
||
if (temp.empty()) {
|
||
return false;
|
||
}
|
||
std::unique_lock<std::shared_mutex> image_lock(m_image_mutex);
|
||
m_cache_image = temp;
|
||
return true;
|
||
};
|
||
|
||
switch (m_adb.screencap_method) {
|
||
case AdbProperty::ScreencapMethod::UnknownYet: {
|
||
using namespace std::chrono;
|
||
Log.info("Try to find the fastest way to screencap");
|
||
auto min_cost = milliseconds(LLONG_MAX);
|
||
clear_lf_info();
|
||
|
||
auto start_time = high_resolution_clock::now();
|
||
if (m_support_socket && m_server_started &&
|
||
screencap(m_adb.screencap_raw_by_nc, decode_raw, allow_reconnect, true)) {
|
||
// sock 第一次截图比较长(不知道是不是初始化了什么东西耽误时间,减个额外的的时间)
|
||
auto duration = duration_cast<milliseconds>(high_resolution_clock::now() - start_time) - 100ms;
|
||
if (duration < min_cost) {
|
||
m_adb.screencap_method = AdbProperty::ScreencapMethod::RawByNc;
|
||
make_instance_inited(true);
|
||
min_cost = duration;
|
||
}
|
||
Log.info("RawByNc cost", duration.count(), "ms");
|
||
}
|
||
else {
|
||
Log.info("RawByNc is not supported");
|
||
}
|
||
clear_lf_info();
|
||
|
||
start_time = high_resolution_clock::now();
|
||
if (screencap(m_adb.screencap_raw_with_gzip, decode_raw_with_gzip, allow_reconnect)) {
|
||
auto duration = duration_cast<milliseconds>(high_resolution_clock::now() - start_time);
|
||
if (duration < min_cost) {
|
||
m_adb.screencap_method = AdbProperty::ScreencapMethod::RawWithGzip;
|
||
make_instance_inited(true);
|
||
min_cost = duration;
|
||
}
|
||
Log.info("RawWithGzip cost", duration.count(), "ms");
|
||
}
|
||
else {
|
||
Log.info("RawWithGzip is not supported");
|
||
}
|
||
clear_lf_info();
|
||
|
||
start_time = high_resolution_clock::now();
|
||
if (screencap(m_adb.screencap_encode, decode_encode, allow_reconnect)) {
|
||
auto duration = duration_cast<milliseconds>(high_resolution_clock::now() - start_time);
|
||
if (duration < min_cost) {
|
||
m_adb.screencap_method = AdbProperty::ScreencapMethod::Encode;
|
||
make_instance_inited(true);
|
||
min_cost = duration;
|
||
}
|
||
Log.info("Encode cost", duration.count(), "ms");
|
||
}
|
||
else {
|
||
Log.info("Encode is not supported");
|
||
}
|
||
static const std::unordered_map<AdbProperty::ScreencapMethod, std::string> MethodName = {
|
||
{ AdbProperty::ScreencapMethod::UnknownYet, "UnknownYet" },
|
||
{ AdbProperty::ScreencapMethod::RawByNc, "RawByNc" },
|
||
{ AdbProperty::ScreencapMethod::RawWithGzip, "RawWithGzip" },
|
||
{ AdbProperty::ScreencapMethod::Encode, "Encode" },
|
||
};
|
||
Log.info("The fastest way is", MethodName.at(m_adb.screencap_method), ", cost:", min_cost.count(), "ms");
|
||
clear_lf_info();
|
||
return m_adb.screencap_method != AdbProperty::ScreencapMethod::UnknownYet;
|
||
} break;
|
||
case AdbProperty::ScreencapMethod::RawByNc: {
|
||
return screencap(m_adb.screencap_raw_by_nc, decode_raw, allow_reconnect, true);
|
||
} break;
|
||
case AdbProperty::ScreencapMethod::RawWithGzip: {
|
||
return screencap(m_adb.screencap_raw_with_gzip, decode_raw_with_gzip, allow_reconnect);
|
||
} break;
|
||
case AdbProperty::ScreencapMethod::Encode: {
|
||
return screencap(m_adb.screencap_encode, decode_encode, allow_reconnect);
|
||
} break;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
bool asst::Controller::screencap(const std::string& cmd, const DecodeFunc& decode_func, bool allow_reconnect,
|
||
bool by_socket)
|
||
{
|
||
if ((!m_support_socket || !m_server_started) && by_socket) [[unlikely]] {
|
||
return false;
|
||
}
|
||
|
||
auto ret = call_command(cmd, 20000, allow_reconnect, by_socket);
|
||
|
||
if (!ret || ret.value().empty()) [[unlikely]] {
|
||
Log.error("data is empty!");
|
||
return false;
|
||
}
|
||
auto& data = ret.value();
|
||
|
||
bool tried_conversion = false;
|
||
if (m_adb.screencap_end_of_line == AdbProperty::ScreencapEndOfLine::CRLF) {
|
||
tried_conversion = true;
|
||
if (!convert_lf(data)) [[unlikely]] { // 没找到 "\r\n"
|
||
Log.info("screencap_end_of_line is set to CRLF but no `\\r\\n` found, set it to LF");
|
||
m_adb.screencap_end_of_line = AdbProperty::ScreencapEndOfLine::LF;
|
||
}
|
||
}
|
||
|
||
if (decode_func(data)) [[likely]] {
|
||
if (m_adb.screencap_end_of_line == AdbProperty::ScreencapEndOfLine::UnknownYet) [[unlikely]] {
|
||
Log.info("screencap_end_of_line is LF");
|
||
m_adb.screencap_end_of_line = AdbProperty::ScreencapEndOfLine::LF;
|
||
}
|
||
return true;
|
||
}
|
||
else {
|
||
Log.info("data is not empty, but image is empty");
|
||
|
||
if (tried_conversion) { // 已经转换过行尾,再次转换 data 不会变化,不必重试
|
||
Log.error("skip retry decoding and decode failed!");
|
||
return false;
|
||
}
|
||
|
||
Log.info("try to cvt lf");
|
||
if (!convert_lf(data)) { // 没找到 "\r\n",data 没有变化,不必重试
|
||
Log.error("no `\\r\\n` found, skip retry decode");
|
||
return false;
|
||
}
|
||
if (!decode_func(data)) {
|
||
Log.error("convert lf and retry decode failed!");
|
||
return false;
|
||
}
|
||
|
||
if (m_adb.screencap_end_of_line == AdbProperty::ScreencapEndOfLine::UnknownYet) {
|
||
Log.info("screencap_end_of_line is CRLF");
|
||
}
|
||
else {
|
||
Log.info("screencap_end_of_line is changed to CRLF");
|
||
}
|
||
m_adb.screencap_end_of_line = AdbProperty::ScreencapEndOfLine::CRLF;
|
||
return true;
|
||
}
|
||
}
|
||
|
||
void asst::Controller::clear_lf_info()
|
||
{
|
||
m_adb.screencap_end_of_line = AdbProperty::ScreencapEndOfLine::UnknownYet;
|
||
}
|
||
|
||
cv::Mat asst::Controller::get_resized_image_cache() const
|
||
{
|
||
const static cv::Size d_size(m_scale_size.first, m_scale_size.second);
|
||
|
||
std::shared_lock<std::shared_mutex> image_lock(m_image_mutex);
|
||
if (m_cache_image.empty()) {
|
||
Log.error("image is empty");
|
||
return { d_size, CV_8UC3 };
|
||
}
|
||
cv::Mat resized_mat;
|
||
cv::resize(m_cache_image, resized_mat, d_size, 0.0, 0.0, cv::INTER_AREA);
|
||
return resized_mat;
|
||
}
|
||
|
||
bool asst::Controller::start_game(const std::string& client_type)
|
||
{
|
||
if (client_type.empty()) {
|
||
return false;
|
||
}
|
||
auto intent_name = Configer.get_intent_name(client_type);
|
||
if (!intent_name) {
|
||
return false;
|
||
}
|
||
std::string cur_cmd = utils::string_replace_all(m_adb.start, "[Intent]", intent_name.value());
|
||
return call_command(cur_cmd).has_value();
|
||
}
|
||
|
||
bool asst::Controller::stop_game()
|
||
{
|
||
return call_command(m_adb.stop).has_value();
|
||
}
|
||
|
||
bool asst::Controller::click(const Point& p)
|
||
{
|
||
int x = static_cast<int>(p.x * m_control_scale);
|
||
int y = static_cast<int>(p.y * m_control_scale);
|
||
// log.trace("Click, raw:", p.x, p.y, "corr:", x, y);
|
||
|
||
return click_without_scale(Point(x, y));
|
||
}
|
||
|
||
bool asst::Controller::click(const Rect& rect)
|
||
{
|
||
return click(rand_point_in_rect(rect));
|
||
}
|
||
|
||
bool asst::Controller::click_without_scale(const Point& p)
|
||
{
|
||
if (p.x < 0 || p.x >= m_width || p.y < 0 || p.y >= m_height) {
|
||
Log.error("click point out of range");
|
||
}
|
||
|
||
if (m_minitouch_enabled && m_minitouch_avaiable) {
|
||
Minitoucher toucher(std::bind(&Controller::input_to_minitouch, this, std::placeholders::_1), m_minitouch_props);
|
||
return toucher.down(p.x, p.y) && toucher.up();
|
||
}
|
||
else {
|
||
std::string cur_cmd =
|
||
utils::string_replace_all(m_adb.click, { { "[x]", std::to_string(p.x) }, { "[y]", std::to_string(p.y) } });
|
||
return call_command(cur_cmd).has_value();
|
||
}
|
||
}
|
||
|
||
bool asst::Controller::click_without_scale(const Rect& rect)
|
||
{
|
||
return click_without_scale(rand_point_in_rect(rect));
|
||
}
|
||
|
||
bool asst::Controller::swipe(const Point& p1, const Point& p2, int duration, bool extra_swipe, double acceleration_coef)
|
||
{
|
||
int x1 = static_cast<int>(p1.x * m_control_scale);
|
||
int y1 = static_cast<int>(p1.y * m_control_scale);
|
||
int x2 = static_cast<int>(p2.x * m_control_scale);
|
||
int y2 = static_cast<int>(p2.y * m_control_scale);
|
||
// log.trace("Swipe, raw:", p1.x, p1.y, p2.x, p2.y, "corr:", x1, y1, x2, y2);
|
||
|
||
return swipe_without_scale(Point(x1, y1), Point(x2, y2), duration, extra_swipe, acceleration_coef);
|
||
}
|
||
|
||
bool asst::Controller::swipe(const Rect& r1, const Rect& r2, int duration, bool extra_swipe, double acceleration_coef)
|
||
{
|
||
return swipe(rand_point_in_rect(r1), rand_point_in_rect(r2), duration, extra_swipe, acceleration_coef);
|
||
}
|
||
|
||
bool asst::Controller::swipe_without_scale(const Point& p1, const Point& p2, int duration, bool extra_swipe,
|
||
double acceleration_coef)
|
||
{
|
||
int x1 = p1.x, y1 = p1.y;
|
||
int x2 = p2.x, y2 = p2.y;
|
||
|
||
// 起点不能在屏幕外,但是终点可以
|
||
if (x1 < 0 || x1 >= m_width || y1 < 0 || y1 >= m_height) {
|
||
Log.warn("swipe point1 is out of range", x1, y1);
|
||
x1 = std::clamp(x1, 0, m_width - 1);
|
||
y1 = std::clamp(y1, 0, m_height - 1);
|
||
}
|
||
|
||
const auto& opt = Configer.get_options();
|
||
if (m_minitouch_enabled && m_minitouch_avaiable) {
|
||
Minitoucher toucher(std::bind(&Controller::input_to_minitouch, this, std::placeholders::_1), m_minitouch_props);
|
||
toucher.down(x1, y1);
|
||
if (duration == 0) {
|
||
duration = 200;
|
||
}
|
||
auto minitouch_move = [&](int _x1, int _y1, int _x2, int _y2, int _duration) {
|
||
double accelerationx = acceleration_coef * static_cast<double>(_x2 - _x1) / (_duration * _duration);
|
||
double accelerationy = acceleration_coef * static_cast<double>(_y2 - _y1) / (_duration * _duration);
|
||
double v0x = static_cast<double>(_x2 - _x1) / _duration - accelerationx * _duration;
|
||
double v0y = static_cast<double>(_y2 - _y1) / _duration - accelerationy * _duration;
|
||
|
||
constexpr int TimeInterval = Minitoucher::DefaultSwipeDelay;
|
||
for (int cur_time = TimeInterval; cur_time < _duration; cur_time += TimeInterval) {
|
||
int cur_x = _x1 + static_cast<int>(v0x * cur_time + accelerationx * cur_time * cur_time);
|
||
int cur_y = _y1 + static_cast<int>(v0y * cur_time + accelerationy * cur_time * cur_time);
|
||
if (cur_x < 0 || cur_x > m_minitouch_props.max_x || cur_y < 0 || cur_y > m_minitouch_props.max_y) {
|
||
continue;
|
||
}
|
||
toucher.move(cur_x, cur_y);
|
||
}
|
||
if (_x2 >= 0 && _x2 <= m_minitouch_props.max_x && _y2 >= 0 && _y2 <= m_minitouch_props.max_y) {
|
||
toucher.move(_x2, _y2);
|
||
}
|
||
};
|
||
minitouch_move(x1, y1, x2, y2, duration);
|
||
constexpr int ExtraEndDelay = 100; // 停留终点
|
||
toucher.wait(ExtraEndDelay);
|
||
|
||
if (extra_swipe && opt.minitouch_extra_swipe_duration > 0) {
|
||
minitouch_move(x2, y2, x2, y2 - opt.minitouch_extra_swipe_dist, opt.minitouch_extra_swipe_duration);
|
||
duration += opt.minitouch_extra_swipe_duration;
|
||
}
|
||
return toucher.up();
|
||
}
|
||
else {
|
||
std::string cur_cmd =
|
||
utils::string_replace_all(m_adb.swipe, {
|
||
{ "[x1]", std::to_string(x1) },
|
||
{ "[y1]", std::to_string(y1) },
|
||
{ "[x2]", std::to_string(x2) },
|
||
{ "[y2]", std::to_string(y2) },
|
||
{ "[duration]", duration <= 0 ? "" : std::to_string(duration) },
|
||
});
|
||
bool ret = call_command(cur_cmd).has_value();
|
||
|
||
// 额外的滑动:adb有bug,同样的参数,偶尔会划得非常远。额外做一个短程滑动,把之前的停下来
|
||
if (extra_swipe && opt.adb_extra_swipe_duration > 0) {
|
||
std::string extra_cmd = utils::string_replace_all(
|
||
m_adb.swipe, {
|
||
{ "[x1]", std::to_string(x2) },
|
||
{ "[y1]", std::to_string(y2) },
|
||
{ "[x2]", std::to_string(x2) },
|
||
{ "[y2]", std::to_string(y2 - opt.adb_extra_swipe_dist /* * m_control_scale*/) },
|
||
{ "[duration]", std::to_string(opt.adb_extra_swipe_duration) },
|
||
});
|
||
ret &= call_command(extra_cmd).has_value();
|
||
}
|
||
return ret;
|
||
}
|
||
}
|
||
|
||
bool asst::Controller::swipe_without_scale(const Rect& r1, const Rect& r2, int duration, bool extra_swipe,
|
||
double acceleration_coef)
|
||
{
|
||
return swipe_without_scale(rand_point_in_rect(r1), rand_point_in_rect(r2), duration, extra_swipe,
|
||
acceleration_coef);
|
||
}
|
||
|
||
bool asst::Controller::connect(const std::string& adb_path, const std::string& address, const std::string& config)
|
||
{
|
||
LogTraceFunction;
|
||
|
||
release_minitouch();
|
||
clear_info();
|
||
|
||
#ifdef ASST_DEBUG
|
||
if (config == "DEBUG") {
|
||
make_instance_inited(true);
|
||
return true;
|
||
}
|
||
#endif
|
||
|
||
auto get_info_json = [&]() -> json::value {
|
||
return json::object {
|
||
{ "uuid", m_uuid },
|
||
{ "details",
|
||
json::object {
|
||
{ "adb", adb_path },
|
||
{ "address", address },
|
||
{ "config", config },
|
||
} },
|
||
};
|
||
};
|
||
|
||
auto adb_ret = Configer.get_adb_cfg(config);
|
||
if (!adb_ret) {
|
||
json::value info = get_info_json() | json::object {
|
||
{ "what", "ConnectFailed" },
|
||
{ "why", "ConfigNotFound" },
|
||
};
|
||
callback(AsstMsg::ConnectionInfo, info);
|
||
return false;
|
||
}
|
||
|
||
const auto& adb_cfg = adb_ret.value();
|
||
std::string display_id;
|
||
std::string nc_address = "10.0.2.2";
|
||
uint16_t nc_port = 0;
|
||
|
||
// 里面的值每次执行命令后可能更新,所以要用 lambda 拿最新的
|
||
auto cmd_replace = [&](const std::string& cfg_cmd) -> std::string {
|
||
return utils::string_replace_all(cfg_cmd, {
|
||
{ "[Adb]", adb_path },
|
||
{ "[AdbSerial]", address },
|
||
{ "[DisplayId]", display_id },
|
||
{ "[NcPort]", std::to_string(nc_port) },
|
||
{ "[NcAddress]", nc_address },
|
||
});
|
||
};
|
||
|
||
if (need_exit()) {
|
||
return false;
|
||
}
|
||
|
||
/* connect */
|
||
{
|
||
m_adb.connect = cmd_replace(adb_cfg.connect);
|
||
auto connect_ret = call_command(m_adb.connect, 60LL * 1000, false /* adb 连接时不允许重试 */);
|
||
// 端口即使错误,命令仍然会返回0,TODO 对connect_result进行判断
|
||
bool is_connect_success = false;
|
||
if (connect_ret) {
|
||
auto& connect_str = connect_ret.value();
|
||
is_connect_success = connect_str.find("error") == std::string::npos;
|
||
if (connect_str.find("daemon started successfully") != std::string::npos &&
|
||
connect_str.find("daemon still not running") == std::string::npos) {
|
||
m_adb_release = cmd_replace(adb_cfg.release);
|
||
}
|
||
}
|
||
if (!is_connect_success) {
|
||
json::value info = get_info_json() | json::object {
|
||
{ "what", "ConnectFailed" },
|
||
{ "why", "Connection command failed to exec" },
|
||
};
|
||
callback(AsstMsg::ConnectionInfo, info);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (need_exit()) {
|
||
return false;
|
||
}
|
||
|
||
/* get uuid (imei) */
|
||
{
|
||
auto uuid_ret = call_command(cmd_replace(adb_cfg.uuid), 20000, false /* adb 连接时不允许重试 */);
|
||
if (!uuid_ret) {
|
||
json::value info = get_info_json() | json::object {
|
||
{ "what", "ConnectFailed" },
|
||
{ "why", "Uuid command failed to exec" },
|
||
};
|
||
callback(AsstMsg::ConnectionInfo, info);
|
||
return false;
|
||
}
|
||
|
||
auto& uuid_str = uuid_ret.value();
|
||
std::erase_if(uuid_str, [](char c) { return !std::isdigit(c) && !std::isalpha(c); });
|
||
m_uuid = std::move(uuid_str);
|
||
|
||
json::value info = get_info_json() | json::object {
|
||
{ "what", "UuidGot" },
|
||
{ "why", "" },
|
||
};
|
||
info["details"]["uuid"] = m_uuid;
|
||
callback(AsstMsg::ConnectionInfo, info);
|
||
}
|
||
|
||
if (need_exit()) {
|
||
return false;
|
||
}
|
||
|
||
// 按需获取display ID 信息
|
||
if (!adb_cfg.display_id.empty()) {
|
||
auto display_id_ret = call_command(cmd_replace(adb_cfg.display_id));
|
||
if (!display_id_ret) {
|
||
return false;
|
||
}
|
||
|
||
auto& display_id_pipe_str = display_id_ret.value();
|
||
convert_lf(display_id_pipe_str);
|
||
auto last = display_id_pipe_str.rfind(':');
|
||
if (last == std::string::npos) {
|
||
return false;
|
||
}
|
||
|
||
display_id = display_id_pipe_str.substr(last + 1);
|
||
// 去掉换行
|
||
display_id.pop_back();
|
||
}
|
||
|
||
if (need_exit()) {
|
||
return false;
|
||
}
|
||
|
||
/* display */
|
||
{
|
||
auto display_ret = call_command(cmd_replace(adb_cfg.display));
|
||
if (!display_ret) {
|
||
json::value info = get_info_json() | json::object {
|
||
{ "what", "ConnectFailed" },
|
||
{ "why", "Display command failed to exec" },
|
||
};
|
||
callback(AsstMsg::ConnectionInfo, info);
|
||
return false;
|
||
}
|
||
|
||
auto& display_pipe_str = display_ret.value();
|
||
int size_value1 = 0;
|
||
int size_value2 = 0;
|
||
#ifdef _MSC_VER
|
||
sscanf_s(display_pipe_str.c_str(), adb_cfg.display_format.c_str(), &size_value1, &size_value2);
|
||
#else
|
||
sscanf(display_pipe_str.c_str(), adb_cfg.display_format.c_str(), &size_value1, &size_value2);
|
||
#endif
|
||
// 为了防止抓取句柄的时候手机是竖屏的(还没进游戏),这里取大的值为宽,小的为高
|
||
// 总不能有人竖屏玩明日方舟吧(?
|
||
m_width = (std::max)(size_value1, size_value2);
|
||
m_height = (std::min)(size_value1, size_value2);
|
||
|
||
json::value info = get_info_json() | json::object {
|
||
{ "what", "ResolutionGot" },
|
||
{ "why", "" },
|
||
};
|
||
|
||
info["details"] |= json::object {
|
||
{ "width", m_width },
|
||
{ "height", m_height },
|
||
};
|
||
|
||
callback(AsstMsg::ConnectionInfo, info);
|
||
|
||
if (m_width == 0 || m_height == 0) {
|
||
info["what"] = "ResolutionError";
|
||
info["why"] = "Get resolution failed";
|
||
callback(AsstMsg::ConnectionInfo, info);
|
||
return false;
|
||
}
|
||
else if (m_width < WindowWidthDefault || m_height < WindowHeightDefault) {
|
||
info["what"] = "UnsupportedResolution";
|
||
info["why"] = "Low screen resolution";
|
||
callback(AsstMsg::ConnectionInfo, info);
|
||
return false;
|
||
}
|
||
else if (std::fabs(static_cast<double>(WindowWidthDefault) / static_cast<double>(WindowHeightDefault) -
|
||
static_cast<double>(m_width) / static_cast<double>(m_height)) > 1e-7) {
|
||
info["what"] = "UnsupportedResolution";
|
||
info["why"] = "Not 16:9";
|
||
callback(AsstMsg::ConnectionInfo, info);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (need_exit()) {
|
||
return false;
|
||
}
|
||
|
||
/* calc ratio */
|
||
{
|
||
constexpr double DefaultRatio =
|
||
static_cast<double>(WindowWidthDefault) / static_cast<double>(WindowHeightDefault);
|
||
double cur_ratio = static_cast<double>(m_width) / static_cast<double>(m_height);
|
||
|
||
if (cur_ratio >= DefaultRatio // 说明是宽屏或默认16:9,按照高度计算缩放
|
||
|| std::fabs(cur_ratio - DefaultRatio) < DoubleDiff) {
|
||
int scale_width = static_cast<int>(cur_ratio * WindowHeightDefault);
|
||
m_scale_size = std::make_pair(scale_width, WindowHeightDefault);
|
||
m_control_scale = static_cast<double>(m_height) / static_cast<double>(WindowHeightDefault);
|
||
}
|
||
else { // 否则可能是偏正方形的屏幕,按宽度计算
|
||
int scale_height = static_cast<int>(WindowWidthDefault / cur_ratio);
|
||
m_scale_size = std::make_pair(WindowWidthDefault, scale_height);
|
||
m_control_scale = static_cast<double>(m_width) / static_cast<double>(WindowWidthDefault);
|
||
}
|
||
}
|
||
|
||
{
|
||
json::value info = get_info_json() | json::object {
|
||
{ "what", "Connected" },
|
||
{ "why", "" },
|
||
};
|
||
callback(AsstMsg::ConnectionInfo, info);
|
||
}
|
||
|
||
m_adb.click = cmd_replace(adb_cfg.click);
|
||
m_adb.swipe = cmd_replace(adb_cfg.swipe);
|
||
m_adb.screencap_raw_with_gzip = cmd_replace(adb_cfg.screencap_raw_with_gzip);
|
||
m_adb.screencap_encode = cmd_replace(adb_cfg.screencap_encode);
|
||
m_adb_release = m_adb.release = cmd_replace(adb_cfg.release);
|
||
m_adb.start = cmd_replace(adb_cfg.start);
|
||
m_adb.stop = cmd_replace(adb_cfg.stop);
|
||
|
||
if (m_support_socket && !m_server_started) {
|
||
std::string bind_address;
|
||
if (size_t pos = address.rfind(':'); pos != std::string::npos) {
|
||
bind_address = address.substr(0, pos);
|
||
}
|
||
else {
|
||
bind_address = "127.0.0.1";
|
||
}
|
||
|
||
// reference from
|
||
// https://github.com/ArknightsAutoHelper/ArknightsAutoHelper/blob/master/automator/connector/ADBConnector.py#L436
|
||
auto nc_address_ret = call_command(cmd_replace(adb_cfg.nc_address));
|
||
if (nc_address_ret && !m_server_started) {
|
||
auto& nc_result_str = nc_address_ret.value();
|
||
if (auto pos = nc_result_str.find(' '); pos != std::string::npos) {
|
||
nc_address = nc_result_str.substr(0, pos);
|
||
}
|
||
}
|
||
|
||
auto socket_opt = init_socket(bind_address);
|
||
if (socket_opt) {
|
||
nc_port = socket_opt.value();
|
||
m_adb.screencap_raw_by_nc = cmd_replace(adb_cfg.screencap_raw_by_nc);
|
||
m_server_started = true;
|
||
}
|
||
else {
|
||
m_server_started = false;
|
||
}
|
||
}
|
||
|
||
if (need_exit()) {
|
||
return false;
|
||
}
|
||
|
||
auto minitouch_cmd_rep = [&](const std::string& cfg_cmd) -> std::string {
|
||
return utils::string_replace_all(
|
||
cmd_replace(cfg_cmd),
|
||
{
|
||
// TODO: 用 adb shell getprop ro.product.cpu.abilist 来判断该用哪个文件夹里的
|
||
{ "[minitouchLocalPath]", (m_resource_path / "minitouch" / "armeabi-v7a" / "minitouch").string() },
|
||
{ "[minitouchWorkingFile]", m_uuid },
|
||
});
|
||
};
|
||
|
||
call_command(minitouch_cmd_rep(adb_cfg.push_minitouch));
|
||
call_command(minitouch_cmd_rep(adb_cfg.chmod_minitouch));
|
||
|
||
call_and_hup_minitouch(minitouch_cmd_rep(adb_cfg.call_minitouch));
|
||
|
||
// try to find the fastest way
|
||
if (!screencap()) {
|
||
Log.error("Cannot find a proper way to screencap!");
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
bool asst::Controller::make_instance_inited(bool inited)
|
||
{
|
||
Log.trace(__FUNCTION__, "|", inited, ", pre m_inited =", m_inited, ", pre m_instance_count =", m_instance_count);
|
||
|
||
if (inited == m_inited) {
|
||
return true;
|
||
}
|
||
m_inited = inited;
|
||
|
||
if (inited) {
|
||
++m_instance_count;
|
||
}
|
||
else {
|
||
// 所有实例全部释放,执行最终的 release 函数
|
||
if (!--m_instance_count) {
|
||
return release();
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
void asst::Controller::kill_adb_daemon()
|
||
{
|
||
if (m_instance_count) return;
|
||
#ifndef _WIN32
|
||
if (m_child)
|
||
#endif
|
||
{
|
||
if (!m_adb_release.empty()) {
|
||
call_command(m_adb_release, 20000, false);
|
||
m_adb_release.clear();
|
||
}
|
||
}
|
||
}
|
||
|
||
bool asst::Controller::release()
|
||
{
|
||
close_socket();
|
||
|
||
#ifndef _WIN32
|
||
if (m_child)
|
||
#endif
|
||
{
|
||
if (m_adb.release.empty()) {
|
||
return true;
|
||
}
|
||
else {
|
||
m_adb_release.clear();
|
||
return call_command(m_adb.release, 20000, false).has_value();
|
||
}
|
||
}
|
||
}
|
||
|
||
bool asst::Controller::inited() const noexcept
|
||
{
|
||
return m_inited;
|
||
}
|
||
|
||
void asst::Controller::set_exit_flag(bool* flag)
|
||
{
|
||
m_exit_flag = flag;
|
||
}
|
||
|
||
const std::string& asst::Controller::get_uuid() const
|
||
{
|
||
return m_uuid;
|
||
}
|
||
|
||
cv::Mat asst::Controller::get_image(bool raw)
|
||
{
|
||
if (m_scale_size.first == 0 || m_scale_size.second == 0) {
|
||
Log.error("Unknown image size");
|
||
return {};
|
||
}
|
||
|
||
// 有些模拟器adb偶尔会莫名其妙截图失败,多试几次
|
||
static constexpr int MaxTryCount = 20;
|
||
bool success = false;
|
||
for (int i = 0; i < MaxTryCount && inited(); ++i) {
|
||
if (need_exit()) {
|
||
break;
|
||
}
|
||
if (screencap()) {
|
||
success = true;
|
||
break;
|
||
}
|
||
}
|
||
while (!success && !need_exit()) {
|
||
if (screencap(true)) {
|
||
break;
|
||
}
|
||
Log.error(__FUNCTION__, "screencap failed!");
|
||
json::value info = json::object {
|
||
{ "uuid", m_uuid },
|
||
{ "what", "ScreencapFailed" },
|
||
{ "why", "ScreencapFailed" },
|
||
{ "details", json::object {} },
|
||
};
|
||
callback(AsstMsg::ConnectionInfo, info);
|
||
|
||
const static cv::Size d_size(m_scale_size.first, m_scale_size.second);
|
||
m_cache_image = cv::Mat(d_size, CV_8UC3);
|
||
|
||
break;
|
||
}
|
||
|
||
if (raw) {
|
||
std::shared_lock<std::shared_mutex> image_lock(m_image_mutex);
|
||
cv::Mat copy = m_cache_image.clone();
|
||
return copy;
|
||
}
|
||
|
||
return get_resized_image_cache();
|
||
}
|
||
|
||
std::vector<uchar> asst::Controller::get_encoded_image_cache() const
|
||
{
|
||
cv::Mat img = get_resized_image_cache();
|
||
std::vector<uchar> buf;
|
||
cv::imencode(".png", img, buf);
|
||
|
||
return buf;
|
||
}
|