Files
MaaAssistantArknights/MeoAssistance/src/Assistance.cpp

554 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Assistance.h"
#include "WinMacro.h"
#include "Configer.h"
#include "Identify.h"
#include "Logger.hpp"
#include "AsstAux.h"
#include <opencv2/opencv.hpp>
#include <time.h>
#include <filesystem>
using namespace asst;
Assistance::Assistance()
{
DebugTraceFunction;
m_configer.load(GetResourceDir() + "config.json");
m_recruit_configer.load(GetResourceDir() + "operInfo.json");
m_pIder = std::make_shared<Identify>();
for (const auto& [name, info] : m_configer.m_tasks)
{
m_pIder->add_image(name, GetResourceDir() + "template\\" + info.template_filename);
}
m_pIder->set_use_cache(m_configer.m_options.identify_cache);
m_pIder->set_ocr_param(m_configer.m_options.ocr_gpu_index, m_configer.m_options.ocr_thread_number);
m_pIder->ocr_init_models(GetResourceDir() + "OcrLiteNcnn\\models\\");
m_working_thread = std::thread(working_proc, this);
}
Assistance::~Assistance()
{
DebugTraceFunction;
//if (m_pWindow != nullptr) {
// m_pWindow->showWindow();
//}
m_thread_exit = true;
m_thread_running = false;
m_condvar.notify_all();
if (m_working_thread.joinable()) {
m_working_thread.join();
}
}
std::optional<std::string> Assistance::catch_emulator(const std::string& emulator_name)
{
DebugTraceFunction;
stop();
auto create_handles = [&](const EmulatorInfo& info) -> bool {
m_pWindow = std::make_shared<WinMacro>(info, HandleType::Window);
m_pView = std::make_shared<WinMacro>(info, HandleType::View);
m_pCtrl = std::make_shared<WinMacro>(info, HandleType::Control);
return m_pWindow->captured() && m_pView->captured() && m_pCtrl->captured();
};
bool ret = false;
std::string cor_name = emulator_name;
std::unique_lock<std::mutex> lock(m_mutex);
// 自动匹配模拟器,逐个找
if (emulator_name.empty()) {
for (const auto& [name, info] : m_configer.m_handles)
{
ret = create_handles(info);
if (ret) {
cor_name = name;
break;
}
}
}
else { // 指定的模拟器
ret = create_handles(m_configer.m_handles[emulator_name]);
}
if (ret && m_pWindow->showWindow() && m_pWindow->resizeWindow()) {
m_inited = true;
return cor_name;
}
else {
m_inited = false;
return std::nullopt;
}
}
void Assistance::start(const std::string& task)
{
DebugTraceFunction;
DebugTrace("Start |", task);
if (m_thread_running || !m_inited) {
return;
}
std::unique_lock<std::mutex> lock(m_mutex);
m_configer.clear_exec_times();
m_pIder->clear_cache();
m_next_tasks.clear();
m_next_tasks.emplace_back(task);
m_thread_running = true;
m_condvar.notify_one();
}
void Assistance::stop(bool block)
{
DebugTraceFunction;
DebugTrace("Stop |", block ? "block" : "non block");
std::unique_lock<std::mutex> lock;
if (block) { // 外部调用
lock = std::unique_lock<std::mutex>(m_mutex);
m_configer.clear_exec_times();
}
m_thread_running = false;
m_next_tasks.clear();
m_pIder->clear_cache();
}
bool Assistance::set_param(const std::string& type, const std::string& param, const std::string& value)
{
DebugTraceFunction;
DebugTrace("SetParam |", type, param, value);
std::unique_lock<std::mutex> lock(m_mutex);
return m_configer.set_param(type, param, value);
}
std::optional<std::string> Assistance::get_param(const std::string& type, const std::string& param)
{
// DebugTraceFunction;
if (type == "status") {
if (param == "running") {
return std::to_string(m_thread_running);
}
else {
return std::nullopt;
}
}
std::unique_lock<std::mutex> lock(m_mutex);
return m_configer.get_param(type, param);
}
bool asst::Assistance::print_window(const std::string& filename, bool block)
{
DebugTraceFunction;
DebugTrace("print_window |", block ? "block" : "non block");
std::unique_lock<std::mutex> lock;
if (block) { // 外部调用
lock = std::unique_lock<std::mutex>(m_mutex);
}
const cv::Mat& image = get_format_image();
// 保存的截图额外再裁剪掉一圈,不然企鹅物流识别不出来
int offset = m_configer.m_options.print_window_crop_offset;
cv::Rect rect(offset, offset, image.cols - offset * 2, image.rows - offset * 2);
bool ret = cv::imwrite(filename.c_str(), image(rect));
if (ret) {
DebugTraceInfo("PrintWindow to", filename);
}
else {
DebugTraceError("PrintWindow error", filename);
}
return ret;
}
bool asst::Assistance::find_text_and_click(const std::string& text, bool block)
{
DebugTraceFunction;
DebugTrace("find_text_and_click |", Utf8ToGbk(text), block ? "block" : "non block");
std::unique_lock<std::mutex> lock;
if (block) { // 外部调用
lock = std::unique_lock<std::mutex>(m_mutex);
}
const cv::Mat& image = get_format_image();
std::optional<Rect>&& result = m_pIder->find_text(image, text);
if (!result) {
DebugTrace("Cannot found", Utf8ToGbk(text));
return false;
}
set_control_scale(image.cols, image.rows);
return m_pCtrl->click(result.value());
}
void asst::Assistance::find_and_clac_tags(bool need_click)
{
DebugTraceFunction;
std::unique_lock<std::mutex> lock(m_mutex);
const cv::Mat& image = get_format_image();
set_control_scale(image.cols, image.rows);
lock.unlock();
if (need_click) {
start("RecruitTime");
}
std::vector<TextArea> ider_result = m_pIder->ocr_detect(image);
std::vector<TextArea> filt_result;
std::string ider_str;
for (TextArea& res : ider_result) {
// 替换一些常见的文字识别错误
// TODO: 这块时间复杂度有点高,待优化
for (const auto& [src, cor] : m_configer.m_ocr_replace) {
res.text = StringReplaceAll(res.text, src, cor);
}
ider_str += res.text + " ,";
for (const std::string& t : m_recruit_configer.m_all_tags) {
if (res.text == t) {
filt_result.emplace_back(std::move(res));
}
}
}
if (ider_str.back() == ',') {
ider_str.pop_back();
}
DebugTrace("All ocr text: ", Utf8ToGbk(ider_str));
std::vector<std::string> tags;
std::string tags_str;
for (const TextArea& t_a : filt_result) {
tags.emplace_back(t_a.text);
tags_str += t_a.text + " ,";
}
if (tags_str.back() == ',') {
tags_str.pop_back();
}
DebugTraceInfo("All Tags", Utf8ToGbk(tags_str));
// Tags全组合
std::vector<std::vector<std::string>> all_combs;
int len = tags.size();
int count = std::pow(2, len);
for (int i = 0; i < count; ++i) {
std::vector<std::string> temp;
for (int j = 0, mask = 1; j < len; ++j) {
if ((i & mask) != 0) { // What the fuck???
temp.emplace_back(tags.at(j));
}
mask = mask * 2;
}
// 游戏里最多选择3个tag
if (!temp.empty() && temp.size() <= 3) {
all_combs.emplace_back(std::move(temp));
}
}
std::map<std::vector<std::string>, OperCombs> result_map;
for (const std::vector<std::string>& comb : all_combs) {
for (const OperInfo& cur_oper : m_recruit_configer.m_opers) {
int matched_count = 0;
// 组合中每一个tag是否在干员tags中
for (const std::string& tag : comb) {
if (cur_oper.tags.find(tag) != cur_oper.tags.cend()) {
++matched_count;
}
else {
break;
}
}
if (matched_count == comb.size()) {
static const std::string SeniorOper = GbkToUtf8("高级资深干员");
// 高资tag和六星强绑定如果没有高资tag即使其他tag匹配上了也不可能出六星
if (cur_oper.level == 6
&& std::find(comb.cbegin(), comb.cend(), SeniorOper) == comb.cend()) {
continue;
}
if (result_map.find(comb) == result_map.cend()) {
result_map.emplace(comb, OperCombs());
}
OperCombs& oper_combs = result_map[comb];
oper_combs.opers.emplace_back(cur_oper);
// 一星小车不计入最低等级
if (cur_oper.level != 1 && (
oper_combs.min_level == 0 || oper_combs.min_level > cur_oper.level)) {
oper_combs.min_level = cur_oper.level;
}
if (oper_combs.max_level == 0 || oper_combs.max_level < cur_oper.level) {
oper_combs.max_level = cur_oper.level;
}
}
}
}
// map没法按值排序转个vector再排
std::vector<std::pair<std::vector<std::string>, OperCombs>> result_vector;
for (auto&& pair : result_map) {
result_vector.emplace_back(std::move(pair));
}
std::sort(result_vector.begin(), result_vector.end(), []( const auto& lhs, const auto& rhs)
->bool {
// 1、最小等级小的排最后
// 2、最小等级相同最大等级小的排后面
// 3、1 2都相同干员数量越多的排后面
if (lhs.second.min_level != rhs.second.min_level) {
return lhs.second.min_level > rhs.second.min_level;
}
else if (lhs.second.max_level != rhs.second.max_level) {
return lhs.second.max_level > rhs.second.max_level;
}
else {
return lhs.second.opers.size() < rhs.second.opers.size();
}
});
#ifdef LOG_TRACE
for (const auto& [combs, oper_combs] : result_vector) {
std::string tag_str;
for (const std::string& tag : combs) {
tag_str += tag + " ,";
}
if (tags_str.back() == ',') {
tag_str.pop_back();
}
std::string opers_str;
for (const OperInfo& oper : oper_combs.opers) {
opers_str += std::to_string(oper.level) + "-" + oper.name + " ,";
}
if (opers_str.back() == ',') {
opers_str.pop_back();
}
DebugTraceInfo("Tags:", Utf8ToGbk(tag_str), "May be recruited: ", Utf8ToGbk(opers_str));
}
#endif
if (need_click && !result_vector.empty()) {
const std::vector<std::string>& final_tags = result_vector[0].first;
std::vector<TextArea> final_text_areas;
lock.lock();
for (const TextArea& text_area : filt_result) {
if (std::find(final_tags.cbegin(), final_tags.cend(), text_area.text) != final_tags.cend()) {
final_text_areas.emplace_back(text_area);
m_pCtrl->click(text_area.rect);
Sleep(300);
}
}
lock.unlock();
}
}
void Assistance::working_proc(Assistance* pThis)
{
DebugTraceFunction;
while (!pThis->m_thread_exit) {
std::unique_lock<std::mutex> lock(pThis->m_mutex);
if (pThis->m_thread_running) {
const cv::Mat& cur_image = pThis->get_format_image();
pThis->set_control_scale(cur_image.cols, cur_image.rows);
if (cur_image.empty()) {
DebugTraceError("Unable to capture window image!!!");
pThis->stop(false);
continue;
}
if (cur_image.rows < 100) {
DebugTraceInfo("Window Could not be minimized!!!");
pThis->m_pWindow->showWindow();
pThis->m_condvar.wait_for(lock,
std::chrono::milliseconds(pThis->m_configer.m_options.identify_delay),
[&]() -> bool { return !pThis->m_thread_running; });
continue;
}
std::string matched_task;
Rect matched_rect;
// 逐个匹配当前可能的图像
for (const std::string& task_name : pThis->m_next_tasks) {
double threshold = pThis->m_configer.m_tasks[task_name].threshold;
double cache_threshold = pThis->m_configer.m_tasks[task_name].cache_threshold;
auto&& [algorithm, value, rect] = pThis->m_pIder->find_image(cur_image, task_name, threshold);
DebugTrace(task_name, "Type:", algorithm, "Value:", value);
if (algorithm == AlgorithmType::JustReturn ||
(algorithm == AlgorithmType::MatchTemplate && value >= threshold)
|| (algorithm == AlgorithmType::CompareHist && value >= cache_threshold)) {
matched_task = std::move(task_name);
matched_rect = std::move(rect);
break;
}
}
// 执行任务
if (!matched_task.empty()) {
TaskInfo& task = pThis->m_configer.m_tasks[matched_task];
DebugTraceInfo("***Matched***", matched_task, "Type:", task.type);
// 前置固定延时
if (task.pre_delay > 0) {
DebugTrace("PreDelay", task.pre_delay);
// std::this_thread::sleep_for(std::chrono::milliseconds(task.pre_delay));
bool cv_ret = pThis->m_condvar.wait_for(lock, std::chrono::milliseconds(task.pre_delay),
[&]() -> bool { return !pThis->m_thread_running; });
if (cv_ret) { continue; }
}
if (task.max_times != INT_MAX) {
DebugTrace("CurTimes:", task.exec_times, "MaxTimes:", task.max_times);
}
if (task.exec_times < task.max_times) {
// 随机延时功能
if ((task.type & TaskType::BasicClick)
&& pThis->m_configer.m_options.control_delay_upper != 0) {
static std::default_random_engine rand_engine(std::chrono::system_clock::now().time_since_epoch().count());
static std::uniform_int_distribution<unsigned> rand_uni(pThis->m_configer.m_options.control_delay_lower, pThis->m_configer.m_options.control_delay_upper);
int delay = rand_uni(rand_engine);
DebugTraceInfo("Random Delay", delay, "ms");
bool cv_ret = pThis->m_condvar.wait_for(lock, std::chrono::milliseconds(delay),
[&]() -> bool { return !pThis->m_thread_running; });
if (cv_ret) { continue; }
}
if (!pThis->m_thread_running) {
continue;
}
switch (task.type) {
case TaskType::ClickRect:
matched_rect = task.specific_area;
[[fallthrough]];
case TaskType::ClickSelf:
pThis->m_pCtrl->click(matched_rect);
break;
case TaskType::ClickRand:
pThis->m_pCtrl->click(pThis->m_pCtrl->getWindowRect());
break;
case TaskType::DoNothing:
break;
case TaskType::Stop:
DebugTrace("TaskType is Stop");
pThis->stop(false);
continue;
break;
case TaskType::PrintWindow:
if (pThis->m_configer.m_options.print_window) {
// 每次到结算界面,掉落物品不是一次性出来的,有个动画,所以需要等一会再截图
int print_delay = pThis->m_configer.m_options.print_window_delay;
DebugTraceInfo("Ready to print window, delay", print_delay);
pThis->m_condvar.wait_for(lock,
std::chrono::milliseconds(print_delay),
[&]() -> bool { return !pThis->m_thread_running; });
const std::string dirname = GetCurrentDir() + "screenshot\\";
std::filesystem::create_directory(dirname);
const std::string time_str = StringReplaceAll(StringReplaceAll(GetFormatTimeString(), " ", "_"), ":", "-");
const std::string filename = dirname + time_str + ".png";
pThis->print_window(filename, false);
}
break;
default:
DebugTraceError("Unknown option type:", task.type);
break;
}
++task.exec_times;
// 减少其他任务的执行次数
// 例如,进入吃理智药的界面了,相当于上一次点蓝色开始行动没生效
// 所以要给蓝色开始行动的次数减一
for (const std::string& reduce : task.reduce_other_times) {
--pThis->m_configer.m_tasks[reduce].exec_times;
DebugTrace("Reduce exec times", reduce, pThis->m_configer.m_tasks[reduce].exec_times);
}
// 后置固定延时
if (task.rear_delay > 0) {
DebugTrace("RearDelay", task.rear_delay);
// std::this_thread::sleep_for(std::chrono::milliseconds(task.rear_delay));
bool cv_ret = pThis->m_condvar.wait_for(lock, std::chrono::milliseconds(task.rear_delay),
[&]() -> bool { return !pThis->m_thread_running; });
if (cv_ret) { continue; }
}
pThis->m_next_tasks = pThis->m_configer.m_tasks[matched_task].next;
}
else {
DebugTraceInfo("Reached limit");
pThis->m_next_tasks = pThis->m_configer.m_tasks[matched_task].exceeded_next;
}
// 单纯为了打印日志。。感觉可以优化下
std::string nexts_str;
for (const std::string& name : pThis->m_next_tasks) {
nexts_str += name + " ,";
}
if (nexts_str.back() == ',') {
nexts_str.pop_back();
}
DebugTrace("Next:", nexts_str);
}
pThis->m_condvar.wait_for(lock,
std::chrono::milliseconds(pThis->m_configer.m_options.identify_delay),
[&]() -> bool { return !pThis->m_thread_running; });
}
else {
pThis->m_condvar.wait(lock);
}
}
}
cv::Mat asst::Assistance::get_format_image()
{
const cv::Mat& raw_image = m_pView->getImage(m_pView->getWindowRect());
if (raw_image.empty() || raw_image.rows < 100) {
DebugTraceError("Window image error");
return raw_image;
}
// 把模拟器边框的一圈裁剪掉
const EmulatorInfo& window_info = m_pView->getEmulatorInfo();
int x_offset = window_info.x_offset;
int y_offset = window_info.y_offset;
int width = raw_image.cols - x_offset - window_info.right_offset;
int height = raw_image.rows - y_offset - window_info.bottom_offset;
cv::Mat cropped(raw_image, cv::Rect(x_offset, y_offset, width, height));
//// 调整尺寸,与资源中截图的标准尺寸一致
//cv::Mat dst;
//cv::resize(cropped, dst, cv::Size(m_configer.DefaultWindowWidth, m_configer.DefaultWindowHeight));
return cropped;
}
void asst::Assistance::set_control_scale(int cur_width, int cur_height)
{
double scale_width = static_cast<double>(cur_width) / m_configer.DefaultWindowWidth;
double scale_height = static_cast<double>(cur_height) / m_configer.DefaultWindowHeight;
// 有些模拟器有可收缩的侧边,会增加宽度。
// config.json中设置的是侧边展开后的offset
// 如果用户把侧边收起来了,则有侧边的那头会额外裁剪掉一些,长度偏小
// 所以按这里面长、宽里大的那个算,大的那边没侧边
double scale = std::max(scale_width, scale_height);
m_pCtrl->setControlScale(scale);
}