Files
MaaAssistantArknights/src/MaaCore/Task/Fight/MedicineCounterTaskPlugin.cpp
2025-08-11 14:40:33 +08:00

311 lines
12 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 "MedicineCounterTaskPlugin.h"
#include <ranges>
#include "Config/GeneralConfig.h"
#include "Config/TaskData.h"
#include "Controller/Controller.h"
#include "InstHelper.h"
#include "Task/Fight/DrGrandetTaskPlugin.h"
#include "Task/Fight/FightTimesTaskPlugin.h"
#include "Task/ProcessTask.h"
#include "Utils/Logger.hpp"
#include "Utils/NoWarningCV.h"
#include "Vision/Matcher.h"
#include "Vision/MultiMatcher.h"
#include "Vision/RegionOCRer.h"
bool asst::MedicineCounterTaskPlugin::verify(AsstMsg msg, const json::value& details) const
{
if (msg != AsstMsg::SubTaskStart || details.get("subtask", std::string()) != "ProcessTask") {
return false;
}
const std::string task = details.get("details", "task", "");
if (/*task.ends_with("MedicineConfirm") 走插件 */ task.ends_with("StoneConfirm")) {
m_has_used_medicine = true;
}
else if (task.ends_with("StartButton2")) {
m_has_used_medicine = false;
}
return task.ends_with("UseMedicine");
}
bool asst::MedicineCounterTaskPlugin::_run()
{
LogTraceFunction;
if (m_used_count >= m_max_count && !m_use_expiring) {
LogTrace << __FUNCTION__ << "Needn't to use medicines"
<< ",used:" << m_used_count << ",max:" << m_max_count << "use_expiring:" << m_use_expiring;
return true;
}
// 现在葛朗台插件在前,暂时不需要延迟
// sleep(Task.get("MedicineReduceIcon")->pre_delay);
auto image = ctrler()->get_image();
// 预计最大使用药品情况不会超过2种不考虑滑动
// 10+10+N
auto using_medicine = init_count(image);
if (!using_medicine) [[unlikely]] {
return false;
}
LogTrace << __FUNCTION__ << "Using medicines init finished,"
<< " using:" << using_medicine->using_count << ", used:" << m_used_count << ", max:" << m_max_count;
// 移除超量使用的理智药后,再次获取理智药数量
// 如果移除后没有使用任何理智药,则单独返回数据;进入插件时应当有使用至少一瓶药
auto refresh_medicine_count = [&]() {
image = ctrler()->get_image();
auto count = init_count(image);
if (!count) {
// 判断是不是还在使用理智药的页面
Matcher matcher(image);
matcher.set_task_info("UseMedicine");
if (!matcher.analyze()) [[unlikely]] {
Log.error(__FUNCTION__, "unable to analyze UseMedicine");
return false;
}
using_medicine = MedicineResult { .using_count = 0, .medicines = {} };
}
else {
using_medicine = count;
}
return true;
};
if (m_used_count < m_max_count && using_medicine->using_count + m_used_count > m_max_count) {
reduce_excess(*using_medicine, m_used_count + using_medicine->using_count - m_max_count);
if (!refresh_medicine_count()) {
return false;
}
}
else if (m_used_count >= m_max_count && m_use_expiring) {
bool changed = false;
for (const auto& [use, inventory, rect, is_expiring] : using_medicine->medicines | views::reverse) {
if (use > 0 && is_expiring != ExpiringStatus::Expiring) {
ctrler()->click(rect);
sleep(Config.get_options().task_delay);
changed = true;
}
}
if (changed && !refresh_medicine_count()) {
return false;
}
}
if (using_medicine->using_count == 0) {
return true;
}
std::optional<int> sanity_target, sanity_max;
auto analyze_sanity = [&]() {
sanity_target = get_target_of_sanity(image);
sanity_max = get_maximun_of_sanity(image);
return sanity_target && sanity_max;
};
if (!analyze_sanity()) [[unlikely]] {
Log.error(__FUNCTION__, "unable to analyze sanity");
}
else if (*sanity_target >= *sanity_max) [[unlikely]] {
Log.info(__FUNCTION__, "sanity target >= sanity max, reduce count");
if (m_dr_grandet) { // 博朗台: 如果溢出则等待
auto waitTime = DrGrandetTaskPlugin::analyze_time_left(image);
if (waitTime > 0) {
sleep(waitTime);
}
}
else if (!m_has_used_medicine && m_reduce_when_exceed) { // 直接减少药品使用
while (*sanity_target >= *sanity_max) {
reduce_excess(*using_medicine, 1);
if (!refresh_medicine_count() || !analyze_sanity() || using_medicine->using_count <= 0) {
break;
}
}
if (using_medicine->using_count <= 0) { // 糊个屎, 假装吃了药, FightTimes后续选择不吃药的次数
if (auto ptr = m_task_ptr->find_plugin<FightTimesTaskPlugin>()) {
ptr->set_has_used_medicine();
}
m_has_used_medicine = true;
ProcessTask(*this, { "CloseStonePage" })
.set_times_limit("CloseStonePage", 1, asst::ProcessTask::TimesLimitType::Post)
.set_times_limit("CloseStonePageExceeded", 0)
.run();
return true;
}
}
}
if (!ProcessTask(*this, { "MedicineConfirm" }).set_retry_times(5).run()) {
Log.error(__FUNCTION__, "unable to run medicine confirm");
return false;
}
m_used_count += using_medicine->using_count;
if (auto ptr = m_task_ptr->find_plugin<FightTimesTaskPlugin>()) {
ptr->set_has_used_medicine();
}
m_has_used_medicine = true;
auto info = basic_info_with_what("UseMedicine");
info["details"]["is_expiring"] = m_used_count > m_max_count;
info["details"]["count"] = using_medicine->using_count;
callback(AsstMsg::SubTaskExtraInfo, info);
return true;
}
std::optional<asst::MedicineCounterTaskPlugin::MedicineResult>
asst::MedicineCounterTaskPlugin::init_count(const cv::Mat& image) const
{
int use = 0;
MultiMatcher multi_matcher(image);
multi_matcher.set_task_info("MedicineReduceIcon");
if (!multi_matcher.analyze()) {
Log.error(__FUNCTION__, "medicine reduce icon analyze failed");
return std::nullopt;
}
std::vector<Medicine> medicines;
static const auto& using_count_task = Task.get("UsingMedicineCount");
static const auto& inventory_task = Task.get("MedicineInventory");
static const auto& expiring_task = Task.get("MedicineExpiringTime");
auto match_result = multi_matcher.get_result();
sort_by_horizontal_(match_result); // 排序以保证结果为从左到右
for (const auto& result : match_result) {
auto using_rect = result.rect.move(using_count_task->rect_move);
auto inventory_rect = result.rect.move(inventory_task->rect_move);
auto expiring_rect = result.rect.move(expiring_task->rect_move);
RegionOCRer using_count_ocr(image);
using_count_ocr.set_task_info(using_count_task);
using_count_ocr.set_bin_threshold(using_count_task->special_params[0], using_count_task->special_params[1]);
using_count_ocr.set_roi(using_rect);
using_count_ocr.set_use_raw(false);
if (!using_count_ocr.analyze()) {
Log.error(__FUNCTION__, "medicine using count analyze failed");
return std::nullopt;
}
RegionOCRer inventory_ocr(image);
inventory_ocr.set_task_info(inventory_task);
inventory_ocr.set_bin_threshold(inventory_task->special_params[0], inventory_task->special_params[1]);
inventory_ocr.set_roi(inventory_rect);
if (!inventory_ocr.analyze()) {
Log.error(__FUNCTION__, "medicine using count analyze failed");
return std::nullopt;
}
// 仅在已使用>=上限时才进行过期判断,否则下次再检查,理智不够会进第二次的
auto is_expiring = ExpiringStatus::UnSure;
if (m_used_count >= m_max_count) {
RegionOCRer expiring_ocr(image);
expiring_ocr.set_task_info(expiring_task);
expiring_ocr.set_roi(expiring_rect);
if (expiring_ocr.analyze()) {
is_expiring = ExpiringStatus::Expiring;
}
else {
is_expiring = ExpiringStatus::NotExpiring;
}
}
int using_count = 0, inventory_count = 0;
if (!utils::chars_to_number(using_count_ocr.get_result().text, using_count) ||
!utils::chars_to_number(inventory_ocr.get_result().text, inventory_count)) {
LogError << __FUNCTION__ << "unable to convert ocr result to int,"
<< "use:" << using_count_ocr.get_result().text << ","
<< "inventory:" << inventory_ocr.get_result().text;
return std::nullopt;
}
use += using_count;
medicines.emplace_back(
Medicine { .use = using_count,
.inventory = inventory_count,
.reduce_button_position = result.rect,
.is_expiring = is_expiring });
LogTrace << __FUNCTION__ << "medicine using count:" << using_count << ","
<< "inventory count:" << inventory_count << ","
<< "is expiring:" << expiring_status_to_string(is_expiring);
}
return MedicineResult { .using_count = use, .medicines = medicines };
}
void asst::MedicineCounterTaskPlugin::reduce_excess(const MedicineResult& using_medicine, int reduce)
{
Log.info(__FUNCTION__, "reduce excess medicine count, current:", using_medicine.using_count, ", reduce:", reduce);
for (const auto& [use, inventory, rect, is_expiring] : using_medicine.medicines | views::reverse) {
ctrler()->click(rect);
sleep(Config.get_options().task_delay);
reduce -= use;
auto add_rect = rect.move(Task.get("UsingMedicineCount")->rect_move);
while (reduce < 0) {
ctrler()->click(add_rect);
sleep(Config.get_options().task_delay);
reduce++;
}
if (reduce == 0) {
break;
}
else if (reduce < 0) {
Log.error(__FUNCTION__, "reduce count is less than 0");
break;
}
}
}
std::optional<int> asst::MedicineCounterTaskPlugin::get_target_of_sanity(const cv::Mat& image)
{
const auto& number_replace = Task.get<OcrTaskInfo>("NumberOcrReplace")->replace_map;
const auto& ocr_task = Task.get<OcrTaskInfo>("UsingMedicine-Target");
const auto& ocr_replace = ocr_task->replace_map;
std::decay_t<decltype(ocr_replace)> merged_replace {};
merged_replace.reserve(number_replace.size() + ocr_replace.size());
ranges::copy(number_replace, std::back_inserter(merged_replace));
ranges::copy(ocr_replace, std::back_inserter(merged_replace));
RegionOCRer ocr(image);
ocr.set_bin_threshold(100, 255);
ocr.set_task_info(ocr_task);
ocr.set_replace(merged_replace);
if (!ocr.analyze()) [[unlikely]] {
Log.error(__FUNCTION__, "unable to ocr");
return std::nullopt;
}
int num = 0;
if (!utils::chars_to_number(ocr.get_result().text, num)) {
Log.error(__FUNCTION__, "unable to change [", ocr.get_result().text, "] into int");
return false;
}
return num;
}
std::optional<int> asst::MedicineCounterTaskPlugin::get_maximun_of_sanity(const cv::Mat& image)
{
const auto& ocr_task = Task.get<OcrTaskInfo>("UsingMedicine-SanityMax");
const auto& number_replace = Task.get<OcrTaskInfo>("NumberOcrReplace")->replace_map;
const auto& task_replace = ocr_task->replace_map;
auto merge_map = std::vector(number_replace);
ranges::copy(task_replace, std::back_inserter(merge_map));
RegionOCRer ocr(image);
ocr.set_bin_threshold(100, 255);
ocr.set_task_info(ocr_task);
ocr.set_replace(merge_map);
if (!ocr.analyze()) [[unlikely]] {
Log.error(__FUNCTION__, "unable to ocr");
return std::nullopt;
}
int num = 0;
if (!utils::chars_to_number(ocr.get_result().text, num)) {
Log.error(__FUNCTION__, "unable to change [", ocr.get_result().text, "] into int");
return false;
}
return num;
}