Files
MaaAssistantArknights/src/MeoAssistance/InfrastDormTask.cpp

366 lines
14 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 "InfrastDormTask.h"
#include <future>
#include <thread>
#include <opencv2/opencv.hpp>
#include "WinMacro.h"
#include "Identify.h"
#include "Configer.h"
using namespace asst;
bool asst::InfrastDormTask::run()
{
if (m_controller_ptr == nullptr
|| m_identify_ptr == nullptr)
{
m_callback(AsstMsg::PtrIsNull, json::value(), m_callback_arg);
return false;
}
json::value task_start_json = json::object{
{ "task_type", "InfrastDormTask" },
{ "task_chain", m_task_chain}
};
m_callback(AsstMsg::TaskStart, task_start_json, m_callback_arg);
bool ret = enter_station({ "Dorm", "DormMini" }, m_dorm_begin, 0.8);
if (!ret) {
return false;
}
int dorm_index = m_dorm_begin;
for (; dorm_index < DormNum; ++dorm_index) {
if (need_exit()) {
return false;
}
if (dorm_index != m_dorm_begin) {
ret = enter_next_dorm();
if (!ret) {
return false;
}
}
json::value enter_json;
enter_json["station"] = "Dorm";
enter_json["index"] = dorm_index;
m_callback(AsstMsg::EnterStation, enter_json, m_callback_arg);
enter_operator_selection();
int selected = select_operators(true);
if (selected == -1) {
ret = false;
break;
}
else if (selected < MaxOperNumInDorm) { // 如果选不满5个人说明没有更多需要休息的了直接结束宿舍任务
break;
}
}
m_dorm_begin = dorm_index;
m_callback(AsstMsg::TaskCompleted, task_start_json, m_callback_arg);
return ret;
}
bool asst::InfrastDormTask::click_confirm_button()
{
InfrastAbstractTask::click_confirm_button();
if (!sleep(300)) {
return false;
}
// 点完确定后,如果把工作中的干员撤下来了,会再弹出来一个确认的界面,如果没撤下来则不会弹出。先识别一下再决定要不要点击
auto&& [algorithm, score, second_confirm_rect] =
m_identify_ptr->find_image(m_controller_ptr->get_image(), "DormConfirm");
if (score >= Configer::TemplThresholdDefault) {
m_controller_ptr->click(second_confirm_rect);
}
return sleep(2000);
}
bool asst::InfrastDormTask::enter_next_dorm()
{
static const Rect swipe_down_begin( // 向下滑动起点
Configer::WindowWidthDefault * 0.3,
Configer::WindowHeightDefault * 0.8,
Configer::WindowWidthDefault * 0.2,
Configer::WindowWidthDefault * 0.1);
static const Rect swipe_down_end( // 向下滑动终点
Configer::WindowWidthDefault * 0.3,
Configer::WindowHeightDefault * 0.2,
Configer::WindowWidthDefault * 0.2,
Configer::WindowWidthDefault * 0.1);
static const int swipe_dwon_duration = 1000; // 向下滑动持续时间
// 游戏bug宿舍中如果“进驻信息”已被选中直接进行滑动会被滑的很远
// 所以这里先检查一下,如果进驻信息被选中了,就先把它关了,再进行滑动
auto find_result = m_identify_ptr->find_image(m_controller_ptr->get_image(), "StationInfoSelected");
if (find_result.score >= 0.75) {
m_controller_ptr->click(find_result.rect);
}
m_controller_ptr->swipe(swipe_down_begin, swipe_down_end, swipe_dwon_duration);
static const Rect double_click_rect(
Configer::WindowWidthDefault * 0.4,
Configer::WindowHeightDefault * 0.4,
Configer::WindowWidthDefault * 0.2,
Configer::WindowWidthDefault * 0.2
);
// 游戏中的宿舍里,双击可以让当前设施回到正确的位置
m_controller_ptr->click(double_click_rect);
m_controller_ptr->click(double_click_rect);
return true;
}
bool asst::InfrastDormTask::enter_operator_selection()
{
// 有这些文字之一就说明“进驻信息”这个按钮已经点开了
constexpr static std::array<std::string_view, 3> info_opened_flags = {
"当前房间入住信息","进驻人数","清空"
};
std::vector<TextArea> ocr_result = ocr_detect();
bool is_info_opened =
std::find_first_of(
ocr_result.cbegin(), ocr_result.cend(),
info_opened_flags.cbegin(), info_opened_flags.cend(),
[](const TextArea& lhs, const std::string_view& rhs)
-> bool { return lhs.text == rhs; })
!= ocr_result.cend();
constexpr static std::string_view station_info = "进驻信息";
// 如果“进驻信息”窗口没点开,那就点开
if (!is_info_opened) {
auto station_info_iter = std::find_if(ocr_result.cbegin(), ocr_result.cend(),
[](const TextArea& textarea) -> bool {
return textarea.text == station_info;
});
m_controller_ptr->click(station_info_iter->rect);
if (!sleep(1000)) {
return false;
}
ocr_result = ocr_detect();
}
// 点击这里面任意一个,都可以进入干员选择页面
constexpr static std::array<std::string_view, 4> enter_operator_page_buttons = {
"进驻","休息中","空闲中","心情"
};
auto button_iter = std::find_first_of(
ocr_result.cbegin(), ocr_result.cend(),
enter_operator_page_buttons.cbegin(), enter_operator_page_buttons.cend(),
[](const TextArea& lhs, const std::string_view& rhs)
-> bool { return lhs.text == rhs; });
if (button_iter == ocr_result.cend()) {
// TODO 报错
return false;
}
m_controller_ptr->click(button_iter->rect);
return sleep(2000);
}
int asst::InfrastDormTask::select_operators(bool need_to_the_left)
{
if (need_to_the_left) {
if (!swipe_to_the_left()) {
return false;
}
}
cv::Mat image = m_controller_ptr->get_image();
json::value shift_json;
shift_json["station"] = "Dorm";
// 识别“休息中”的干员
auto resting_result = m_identify_ptr->find_all_images(image, "Resting", 0.8);
if (resting_result.size() == MaxOperNumInDorm) { // 如果所有人都在休息,那这个宿舍不用换班,直接关了
click_return_button();
m_callback(AsstMsg::NoNeedToShift, shift_json, m_callback_arg);
return resting_result.size();
}
// 识别“注意力涣散”的干员
// TODO这个阈值太低了不正常有时间再调整一下模板图片
//auto listless_result = m_identify_ptr->find_all_images(image, "Listless", 0.6);
const static std::array<std::string, 1> ListlessKeyword = { "注意力" };
auto listless_result = text_search(m_identify_ptr->ocr_detect(image), ListlessKeyword);
// 识别正在工作中的干员的心情状态
double mood_threshold = Configer::get_instance().m_infrast_options.dorm_threshold;
auto work_mood_result = detect_mood_status_at_work(image, mood_threshold);
m_callback(AsstMsg::ReadyToShift, shift_json, m_callback_arg);
click_clear_button();
// 把“休息中”的干员,都再次选上
for (const auto& resting : resting_result) {
m_controller_ptr->click(resting.rect);
}
int count = resting_result.size();
// 选择“注意力涣散”的干员
for (const auto& listless : listless_result) {
if (count >= MaxOperNumInDorm) {
break;
}
m_controller_ptr->click(listless.rect);
++count;
}
// 选择心情较低的干员
for (const auto& mood_status : work_mood_result) {
if (count >= MaxOperNumInDorm) {
break;
}
m_controller_ptr->click(mood_status.rect);
++count;
}
if (count >= MaxOperNumInDorm) {
click_confirm_button();
m_callback(AsstMsg::ShiftCompleted, shift_json, m_callback_arg);
return count;
}
while (count < MaxOperNumInDorm) {
if (need_exit()) {
return false;
}
swipe();
image = m_controller_ptr->get_image();
work_mood_result = detect_mood_status_at_work(image, mood_threshold);
// 选择心情较低的干员
for (const auto& mood_status : work_mood_result) {
if (count >= MaxOperNumInDorm) {
break;
}
m_controller_ptr->click(mood_status.rect);
++count;
}
if (count >= MaxOperNumInDorm) {
break;
}
// 识别到满心情的笑脸,就说明后面的都是满心情的(心情排序的),没必要继续滑动识别后面的了
auto rest_result = m_identify_ptr->find_image(image, "SmileyAtRest");
if (rest_result.score > Configer::TemplThresholdDefault) {
break;
}
}
click_confirm_button();
m_callback(AsstMsg::ShiftCompleted, shift_json, m_callback_arg);
return count;
}
std::vector<InfrastDormTask::MoodStatus> InfrastDormTask::detect_mood_status_at_work(const cv::Mat& image, double process_threshold) const
{
constexpr static int MoodWidth = Configer::WindowWidthDefault * 0.0664; // 心情进度条长度(满心情的时候)
constexpr static int MoodHeight = Configer::WindowHeightDefault * 0.00416 + 0.5; // 心情进度条高度
#ifdef LOG_TRACE
cv::Mat draw_image = image.clone();
#endif
// 把工作中的那个黄色笑脸全抓出来
auto smiley_result = m_identify_ptr->find_all_images(image, "SmileyAtWork", 0.88, false);
std::vector<MoodStatus> moods_vec;
for (const auto& smiley : smiley_result) {
// 检查进度条是否超出了图片范围
if (smiley.rect.x + smiley.rect.width + MoodWidth >= image.cols) {
continue;
}
// 根据黄色笑脸往右推心情进度条的区域
cv::Rect process_rect(
smiley.rect.x + smiley.rect.width,
smiley.rect.y,
MoodWidth,
smiley.rect.height);
cv::Mat process_mat = image(process_rect);
cv::Mat process_gray;
cv::cvtColor(process_mat, process_gray, cv::COLOR_BGR2GRAY);
int max_white_length = 0; // 最长的横向连续的白色长条的长度
for (int i = 0; i != process_gray.rows; ++i) {
int cur_white_length = 0;
cv::uint8_t left_value = 250; // 当前点左侧的点的值
for (int j = 0; j != process_gray.cols; ++j) {
constexpr static cv::uint8_t LowerLimit = 180;
constexpr static cv::uint8_t DiffThreshold = 20;
auto value = process_gray.at<cv::uint8_t>(i, j);
if (value >= LowerLimit && left_value - value < DiffThreshold) { // 右边的颜色相比左边变化在阈值以内,都认为是连续的
left_value = value;
++cur_white_length;
if (max_white_length < cur_white_length) {
max_white_length = cur_white_length;
}
}
else {
if (max_white_length < cur_white_length) {
max_white_length = cur_white_length;
}
left_value = 250;
cur_white_length = 0;
}
}
}
MoodStatus mood_status;
mood_status.actual_length = max_white_length;
mood_status.process = static_cast<double>(max_white_length) / MoodWidth;
mood_status.rect = asst::Rect(smiley.rect.x, smiley.rect.y,
smiley.rect.width + process_rect.width, smiley.rect.height + process_rect.height);
mood_status.actual_rect = asst::Rect(process_rect.x, process_rect.y,
max_white_length, process_rect.height);
#ifdef LOG_TRACE
cv::Rect cv_actual_rect(mood_status.actual_rect.x, mood_status.actual_rect.y,
mood_status.actual_rect.width, mood_status.actual_rect.height);
cv::rectangle(draw_image, cv_actual_rect, cv::Scalar(0, 0, 255), 1);
cv::putText(draw_image, std::to_string(mood_status.process), cv::Point(cv_actual_rect.x, cv_actual_rect.y), cv::FONT_HERSHEY_PLAIN, 1, cv::Scalar(0, 0, 255));
#endif
if (mood_status.process == 0 || mood_status.process >= 1) {
// 值为0说明是“注意力涣散”红色哭脸被错误的识别成黄色笑脸了这里直接忽略这个值
// 值为1说明是“空闲中”绿色笑脸被误识别了也直接忽略了
}
else if (mood_status.process <= process_threshold) { // 心情小于阈值的直接加入结果
moods_vec.emplace_back(std::move(mood_status));
}
else {
// 既不在工作中,也不在休息中,但是心情值还大于阈值的,也需要加入结果中
// 思路根据心情进度条的位置往上裁剪出“工作中”or“休息中”的那一块区域
// 进行模板匹配,如果没匹配上,则认为这个干员既不在工作,也不在休息
constexpr static int HeightDiff = Configer::WindowHeightDefault * 0.13;
constexpr static int OnShiftHeight = Configer::WindowHeightDefault * 0.08;
cv::Rect on_shift_rect(mood_status.rect.x, mood_status.rect.y - HeightDiff, mood_status.rect.width, OnShiftHeight);
// “休息中”的笑脸前面的模板匹配是对不上的,走不到这里,所以这里只匹配“工作中”即可
auto find_result = m_identify_ptr->find_image(image(on_shift_rect), "OnShift");
if (find_result.score < 0.45) {
moods_vec.emplace_back(std::move(mood_status));
}
}
}
std::sort(moods_vec.begin(), moods_vec.end(),
[](const auto& lhs, const auto& rhs) -> bool {
// 按剩余心情进度排个序,少的在前面
if (lhs.actual_length != rhs.actual_length) {
return lhs.actual_length < rhs.actual_length;
}
// 如果剩余心情相等,优先选靠左侧、靠上侧的,更符合用户直觉
else if (lhs.rect.x != rhs.rect.x) {
return lhs.rect.x < rhs.rect.x;
}
else {
return lhs.rect.y < rhs.rect.y;
}
});
return moods_vec;
}