diff --git a/resource/tasks.json b/resource/tasks.json index 0f85162446..398581c4ef 100644 --- a/resource/tasks.json +++ b/resource/tasks.json @@ -2599,7 +2599,6 @@ "algorithm": "JustReturn", "action": "DoNothing", "next": [ - "RecruitFinish", "RecruitFlag", "Recruit", "ReturnToRecruit", @@ -2840,7 +2839,7 @@ 150 ], "next": [ - "RecruitFinish", + "RecruitFlag", "RecruitNowConfirm" ] }, diff --git a/src/MeoAssistant/AutoRecruitTask.cpp b/src/MeoAssistant/AutoRecruitTask.cpp index f408212aa7..9c2ec745af 100644 --- a/src/MeoAssistant/AutoRecruitTask.cpp +++ b/src/MeoAssistant/AutoRecruitTask.cpp @@ -2,6 +2,7 @@ #include "Resource.h" #include "OcrImageAnalyzer.h" +#include "MultiMatchImageAnalyzer.h" #include "Controller.h" #include "ProcessTask.h" #include "RecruitImageAnalyzer.h" @@ -182,19 +183,27 @@ bool asst::AutoRecruitTask::_run() if (!recruit_begin()) return false; + { + const auto image = m_ctrler->get_image(); + // initialize_dirty_slot_info(image); + m_dirty_slots = { 0, 1, 2, 3 }; + if (!hire_all(image)) return false; + } + static constexpr int slot_retry_limit = 3; // m_cur_times means how many times has the confirm button been pressed, NOT expedited plans used while (m_cur_times != m_max_times) { if (m_force_discard_flag) { return false; } - if (m_slot_fail >= slot_retry_limit) { return false; } auto start_rect = try_get_start_button(m_ctrler->get_image()); if (start_rect) { if (need_exit()) return false; - if (recruit_one(start_rect.value())) { + if (recruit_one(start_rect.value())) ++m_cur_times; - } else ++m_slot_fail; + else + ++m_slot_fail; + if (m_slot_fail >= slot_retry_limit) { return false; } } else { if (!check_recruit_home_page()) return false; Log.info("There is no available start button."); @@ -204,7 +213,9 @@ bool asst::AutoRecruitTask::_run() if (m_use_expedited) { if (need_exit()) return false; Log.info("ready to use expedited plan"); - if (!recruit_now()) { + if (recruit_now()) { + hire_all(); + } else { // there is a small chance that confirm button were clicked twice and got stuck into the bottom-right slot // ref: issues/1491 if (check_recruit_home_page()) { m_force_discard_flag = true; } // ran out of expedited plan? @@ -216,19 +227,25 @@ bool asst::AutoRecruitTask::_run() return true; } -std::optional asst::AutoRecruitTask::try_get_start_button(const cv::Mat& image) +std::vector asst::AutoRecruitTask::start_recruit_analyze(const cv::Mat& image) { OcrImageAnalyzer start_analyzer; start_analyzer.set_task_info("StartRecruit"); start_analyzer.set_image(image); - if (!start_analyzer.analyze()) return std::nullopt; - start_analyzer.sort_result_horizontal(); + if (!start_analyzer.analyze()) return {}; + return start_analyzer.get_result(); +} + +std::optional asst::AutoRecruitTask::try_get_start_button(const cv::Mat& image) +{ + const auto result = start_recruit_analyze(image); + if (result.empty()) return std::nullopt; auto iter = - ranges::find_if(std::as_const(start_analyzer.get_result()), + ranges::find_if(result, [&](const TextRect& r) -> bool { return !m_force_skipped.contains(slot_index_from_rect(r.rect)); }); - if (iter == start_analyzer.get_result().cend()) return std::nullopt; + if (iter == result.cend()) return std::nullopt; Log.info("Found slot index", slot_index_from_rect(iter->rect), "."); return iter->rect; } @@ -247,11 +264,11 @@ bool asst::AutoRecruitTask::recruit_one(const Rect& button) m_ctrler->click(button); sleep(delay); - auto calc_result = recruit_calc_task(); + auto calc_result = recruit_calc_task(slot_index_from_rect(button)); sleep(delay); if (!calc_result.success) { - // recognition failed, perhaps open the slot again would not help + // recognition failed, perhaps opening the slot again would not help { json::value info = basic_info(); info["what"] = "RecruitError"; @@ -295,7 +312,7 @@ bool asst::AutoRecruitTask::recruit_one(const Rect& button) } // set recruit timer and tags only -asst::AutoRecruitTask::calc_task_result_type asst::AutoRecruitTask::recruit_calc_task() +asst::AutoRecruitTask::calc_task_result_type asst::AutoRecruitTask::recruit_calc_task(slot_index index) { LogTraceFunction; @@ -342,7 +359,7 @@ asst::AutoRecruitTask::calc_task_result_type asst::AutoRecruitTask::recruit_calc callback(AsstMsg::SubTaskExtraInfo, cb_info); } - // robot tags + // robot tags const std::vector RobotTags = { "支援机械" }; if (auto robot_iter = ranges::find_first_of(RobotTags, tag_names); robot_iter != RobotTags.cend()) [[unlikely]] { @@ -430,7 +447,11 @@ asst::AutoRecruitTask::calc_task_result_type asst::AutoRecruitTask::recruit_calc } if (!is_calc_only_task()) { - async_upload_result(info["details"]); + // report if the slot is clean + if (!m_dirty_slots.contains(index)) { + async_upload_result(info["details"]); + m_dirty_slots.emplace(index); // mark as dirty + } else Log.info("will not report, dirty slots are", m_dirty_slots); } if (need_exit()) return {}; @@ -456,6 +477,9 @@ asst::AutoRecruitTask::calc_task_result_type asst::AutoRecruitTask::recruit_calc ++refresh_count; + // mark the slot clean after refreshed + m_dirty_slots.erase(index); + { json::value cb_info = basic_info(); cb_info["what"] = "RecruitTagsRefreshed"; @@ -602,10 +626,46 @@ bool asst::AutoRecruitTask::refresh() return refresh_task.run(); } +bool asst::AutoRecruitTask::hire_all(const cv::Mat& image) +{ + LogTraceFunction; + // mark slots with *Hire* button clean (regardless of whether hiring will success) + { + MultiMatchImageAnalyzer hire_searcher(image); + hire_searcher.set_task_info("RecruitFinish"); + hire_searcher.analyze(); + for (const MatchRect& r : hire_searcher.get_result()) { + Log.info("Mark", slot_index_from_rect(r.rect), "clean"); + m_dirty_slots.erase(slot_index_from_rect(r.rect)); + } + if (hire_searcher.get_result().empty()) return true; + } + // hire all + return ProcessTask{ *this, { "RecruitFinish" }}.run(); +} + +/// search for blue *Hire* buttons in the recruit home page, mark those slot clean and do hiring +bool asst::AutoRecruitTask::hire_all() +{ + return hire_all(m_ctrler->get_image()); +} + +/// search for *RecruitNow* buttons before recruit and mark them as dirty +[[maybe_unused]] bool asst::AutoRecruitTask::initialize_dirty_slot_info(const cv::Mat& image) +{ + m_dirty_slots.clear(); + const auto result = start_recruit_analyze(image); + for (const TextRect& r: result) { + m_dirty_slots.emplace(slot_index_from_rect(r.rect)); + } + Log.info("Dirty slots are", m_dirty_slots); + return true; +} + void asst::AutoRecruitTask::async_upload_result(const json::value& details) { LogTraceFunction; - + return; if (m_upload_to_penguin) { auto upload_future = std::async( std::launch::async, diff --git a/src/MeoAssistant/AutoRecruitTask.h b/src/MeoAssistant/AutoRecruitTask.h index 4b9bfdbb6a..eb0badfc7d 100644 --- a/src/MeoAssistant/AutoRecruitTask.h +++ b/src/MeoAssistant/AutoRecruitTask.h @@ -41,11 +41,17 @@ namespace asst bool recruit_now(); bool confirm(); bool refresh(); + bool hire_all(const cv::Mat&); + bool hire_all(); + bool initialize_dirty_slot_info(const cv::Mat&); + static std::vector start_recruit_analyze(const cv::Mat& image); void async_upload_result(const json::value& details); void upload_to_penguin(const json::value& details); void upload_to_yituliu(const json::value& details); + using slot_index = size_t; + struct calc_task_result_type { bool success = false; @@ -54,7 +60,7 @@ namespace asst [[maybe_unused]] int tags_selected = 0; }; - calc_task_result_type recruit_calc_task(); + calc_task_result_type recruit_calc_task(slot_index = 0); bool m_force_discard_flag = false; @@ -70,10 +76,14 @@ namespace asst int m_slot_fail = 0; int m_cur_times = 0; - using slot_index = size_t; std::set m_force_skipped; + // Do not report tags from these slot. Already reported, or we can not make sure whether it has been reported. + // e.g. those that were already empty (*Recruit Now*) when we open the recruit page, + // because we can not make sure whether they were already reported yesterday but stayed untouched for some reason + std::set m_dirty_slots = { 0, 1, 2, 3 }; + std::string m_server = "CN"; bool m_upload_to_penguin = false; bool m_upload_to_yituliu = false;