Files
MaaAssistantArknights/src/MaaCore/Task/BattleHelper.cpp
2023-01-12 23:08:06 +08:00

607 lines
18 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 "BattleHelper.h"
#include <future>
#include <thread>
#include "Config/Miscellaneous/AvatarCacheManager.h"
#include "Config/Miscellaneous/BattleDataConfig.h"
#include "Config/TaskData.h"
#include "Controller.h"
#include "Task/ProcessTask.h"
#include "Utils/ImageIo.hpp"
#include "Utils/Logger.hpp"
#include "Utils/NoWarningCV.h"
#include "Vision/MatchImageAnalyzer.h"
#include "Vision/Miscellaneous/BattleImageAnalyzer.h"
#include "Vision/Miscellaneous/BattleSkillReadyImageAnalyzer.h"
#include "Vision/OcrWithPreprocessImageAnalyzer.h"
using namespace asst::battle;
asst::BattleHelper::BattleHelper(Assistant* inst) : m_inst_helper(inst) {}
bool asst::BattleHelper::set_stage_name(const std::string& name)
{
LogTraceFunction;
if (!Tile.contains(name)) {
return false;
}
m_stage_name = name;
return true;
}
void asst::BattleHelper::clear()
{
m_side_tile_info.clear();
m_normal_tile_info.clear();
m_skill_usage.clear();
m_camera_count = 0;
m_in_battle = false;
m_kills = 0;
m_total_kills = 0;
m_cur_deployment_opers.clear();
m_battlefield_opers.clear();
m_used_tiles.clear();
}
bool asst::BattleHelper::calc_tiles_info(const std::string& stage_name, double shift_x, double shift_y)
{
LogTraceFunction;
if (!Tile.contains(stage_name)) {
return false;
}
m_normal_tile_info = Tile.calc(stage_name, false, shift_x, shift_y);
m_side_tile_info = Tile.calc(stage_name, true, shift_x, shift_y);
return true;
}
bool asst::BattleHelper::pause()
{
LogTraceFunction;
return ProcessTask(this_task(), { "BattlePause" }).run();
}
bool asst::BattleHelper::speed_up()
{
LogTraceFunction;
return ProcessTask(this_task(), { "BattleSpeedUp" }).run();
}
bool asst::BattleHelper::abandon()
{
return ProcessTask(this_task(), { "RoguelikeBattleExitBegin" }).run();
}
bool asst::BattleHelper::update_deployment(bool init, const cv::Mat& reusable)
{
LogTraceFunction;
if (init) {
wait_until_start();
}
cv::Mat image = init || reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable;
if (init) {
auto draw_future = std::async(std::launch::async, [&]() { save_map(image); });
update_kills(image);
}
BattleImageAnalyzer oper_analyzer(image);
oper_analyzer.set_target(BattleImageAnalyzer::Target::Oper);
if (!oper_analyzer.analyze()) {
return false;
}
m_cur_deployment_opers.clear();
auto remove_cooling_from_battlefield = [&](const DeploymentOper& oper) {
if (!oper.cooling) {
return;
}
auto iter = m_battlefield_opers.find(oper.name);
if (iter == m_battlefield_opers.end()) {
return;
}
auto loc = iter->second;
m_used_tiles.erase(loc);
m_battlefield_opers.erase(iter);
};
auto cur_opers = oper_analyzer.get_opers();
std::vector<DeploymentOper> unknown_opers;
for (auto& oper : cur_opers) {
MatchImageAnalyzer avatar_analyzer(oper.avatar);
if (oper.cooling) {
Log.trace("start matching cooling", oper.index);
static const double cooling_threshold = Task.get<MatchTaskInfo>("BattleAvatarCoolingData")->templ_threshold;
static const auto cooling_mask_range = Task.get<MatchTaskInfo>("BattleAvatarCoolingData")->mask_range;
avatar_analyzer.set_threshold(cooling_threshold);
avatar_analyzer.set_mask_range(cooling_mask_range, true);
}
else {
static const double threshold = Task.get<MatchTaskInfo>("BattleAvatarData")->templ_threshold;
avatar_analyzer.set_threshold(threshold);
}
double max_socre = 0;
auto& avatar_cache = AvatarCache.get_avatars(oper.role);
for (const auto& [name, avatar] : avatar_cache) {
avatar_analyzer.set_templ(avatar);
if (!avatar_analyzer.analyze()) {
continue;
}
const auto& cur_matched = avatar_analyzer.get_result();
if (max_socre < cur_matched.score) {
max_socre = cur_matched.score;
oper.name = name;
}
}
if (max_socre) {
m_cur_deployment_opers.insert_or_assign(oper.name, oper);
remove_cooling_from_battlefield(oper);
}
else {
Log.info("unknown oper", oper.index);
unknown_opers.emplace_back(oper);
}
if (oper.cooling) {
Log.trace("stop matching cooling", oper.index);
}
}
if (!unknown_opers.empty() || init) {
// 一个都没匹配上的,挨个点开来看一下
LogTraceScope("rec unknown opers");
// 暂停游戏准备识别干员
do {
pause();
// 在刚进入游戏的时候(画面刚刚完全亮起来的时候),点暂停是没反应的
// 所以这里一直点,直到真的点上了为止
if (!init || !check_pause_button()) {
break;
}
std::this_thread::yield();
} while (!m_inst_helper.need_exit());
for (auto& oper : unknown_opers) {
LogTraceScope("rec unknown oper: " + std::to_string(oper.index));
click_oper_on_deployment(oper.rect);
cv::Mat name_image = m_inst_helper.ctrler()->get_image();
auto analyze = [&](OcrImageAnalyzer& name_analyzer) {
name_analyzer.set_image(name_image);
name_analyzer.set_task_info(oper_name_ocr_task_name());
name_analyzer.set_replace(Task.get<OcrTaskInfo>("CharsNameOcrReplace")->replace_map);
if (!name_analyzer.analyze()) {
return std::string();
}
name_analyzer.sort_result_by_score();
return name_analyzer.get_result().front().text;
};
OcrWithPreprocessImageAnalyzer preproc_analyzer;
std::string name = analyze(preproc_analyzer);
if (BattleData.is_name_invalid(name)) {
Log.warn("ocr with preprocess got a invalid name, try to use detect model", name);
OcrImageAnalyzer det_analyzer;
std::string det_name = analyze(det_analyzer);
if (det_name.empty()) {
Log.warn("ocr with det model failed");
}
else if (BattleData.is_name_invalid(det_name)) {
Log.info("use ocr with det", det_name);
name = det_name;
}
}
// 这时候即使名字不合法也只能凑合用了,但是为空还是不行的
if (name.empty()) {
Log.error("name is empty");
continue;
}
oper.name = name;
remove_cooling_from_battlefield(oper);
if (oper.cooling) {
// cd 中的干员如果识别错一次这时候保存的是cd中的图像后面就会一直错
// 且一般来说cd 的干员都是一开始上过的m_all_deployment_avatars 中应该有他的头像
// 而且由于 cd 干员头像阈值设置的非常低,为了防止把正确的干员覆盖掉了
// 所以不进行覆盖
m_cur_deployment_opers.emplace(name, oper);
AvatarCache.set_avatar(name, oper.role, oper.avatar, false);
}
else {
m_cur_deployment_opers.insert_or_assign(name, oper);
AvatarCache.set_avatar(name, oper.role, oper.avatar);
}
}
pause();
if (!unknown_opers.empty()) {
cancel_oper_selection();
}
}
check_pause_button(image);
return true;
}
bool asst::BattleHelper::update_kills(const cv::Mat& reusable)
{
cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable;
BattleImageAnalyzer analyzer(image);
if (m_total_kills) {
analyzer.set_pre_total_kills(m_total_kills);
}
analyzer.set_target(BattleImageAnalyzer::Target::Kills);
if (!analyzer.analyze()) {
return false;
}
m_kills = analyzer.get_kills();
m_total_kills = analyzer.get_total_kills();
return true;
}
bool asst::BattleHelper::update_cost(const cv::Mat& reusable)
{
cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable;
BattleImageAnalyzer analyzer(image);
analyzer.set_target(BattleImageAnalyzer::Target::Cost);
if (!analyzer.analyze()) {
return false;
}
m_cost = analyzer.get_cost();
return true;
}
bool asst::BattleHelper::deploy_oper(const std::string& name, const Point& loc, DeployDirection direction)
{
LogTraceFunction;
const auto swipe_oper_task_ptr = Task.get("BattleSwipeOper");
const auto use_oper_task_ptr = Task.get("BattleUseOper");
auto rect_opt = get_oper_rect_on_deployment(name);
if (!rect_opt) {
return false;
}
const Rect& oper_rect = *rect_opt;
auto target_iter = m_side_tile_info.find(loc);
if (target_iter == m_side_tile_info.cend()) {
Log.error("No loc", loc);
return false;
}
const Point& target_point = target_iter->second.pos;
int dist = static_cast<int>(
Point::distance(target_point, { oper_rect.x + oper_rect.width / 2, oper_rect.y + oper_rect.height / 2 }));
// 1000 是随便取的一个系数,把整数的 pre_delay 转成小数用的
int duration = static_cast<int>(dist / 1000.0 * swipe_oper_task_ptr->pre_delay);
bool deploy_with_pause = m_inst_helper.ctrler()->support_swipe_with_pause();
m_inst_helper.ctrler()->swipe(oper_rect, Rect(target_point.x, target_point.y, 1, 1), duration, false,
swipe_oper_task_ptr->special_params.at(1), swipe_oper_task_ptr->special_params.at(2),
deploy_with_pause);
// 拖动干员朝向
if (direction != DeployDirection::None) {
static const std::unordered_map<DeployDirection, Point> DirectionMap = {
{ DeployDirection::Right, Point(1, 0) }, { DeployDirection::Down, Point(0, 1) },
{ DeployDirection::Left, Point(-1, 0) }, { DeployDirection::Up, Point(0, -1) },
{ DeployDirection::None, Point(0, 0) },
};
// 计算往哪边拖动
const Point& direction_target = DirectionMap.at(direction);
// 将方向转换为实际的 swipe end 坐标点
static const int coeff = swipe_oper_task_ptr->special_params.at(0);
Point end_point = target_point + (direction_target * coeff);
m_inst_helper.sleep(use_oper_task_ptr->post_delay);
m_inst_helper.ctrler()->swipe(target_point, end_point, swipe_oper_task_ptr->post_delay);
}
if (deploy_with_pause) {
m_inst_helper.ctrler()->press_esc();
}
m_battlefield_opers.emplace(name, loc);
m_used_tiles.emplace(loc, name);
return true;
}
bool asst::BattleHelper::retreat_oper(const std::string& name)
{
LogTraceFunction;
auto oper_iter = m_battlefield_opers.find(name);
if (oper_iter == m_battlefield_opers.cend()) {
Log.error("No oper", name);
return false;
}
if (!retreat_oper(oper_iter->second, false)) {
return false;
}
m_battlefield_opers.erase(name);
return true;
}
bool asst::BattleHelper::retreat_oper(const Point& loc, bool manually)
{
LogTraceFunction;
if (!click_oper_on_battlefield(loc) || !click_retreat()) {
return false;
}
m_used_tiles.erase(loc);
if (manually) {
std::erase_if(m_battlefield_opers, [&loc](const auto& pair) -> bool { return pair.second == loc; });
}
return true;
}
bool asst::BattleHelper::use_skill(const std::string& name, bool keep_waiting)
{
LogTraceFunction;
auto oper_iter = m_battlefield_opers.find(name);
if (oper_iter == m_battlefield_opers.cend()) {
Log.error("No oper", name);
return false;
}
return use_skill(oper_iter->second, keep_waiting);
}
bool asst::BattleHelper::use_skill(const Point& loc, bool keep_waiting)
{
LogTraceFunction;
return click_oper_on_battlefield(loc) && click_skill(keep_waiting);
}
bool asst::BattleHelper::check_pause_button(const cv::Mat& reusable)
{
cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable;
MatchImageAnalyzer battle_flag_analyzer(image);
battle_flag_analyzer.set_task_info("BattleOfficiallyBegin");
bool ret = battle_flag_analyzer.analyze();
m_in_battle = ret;
return ret;
}
bool asst::BattleHelper::wait_until_start()
{
LogTraceFunction;
while (!m_inst_helper.need_exit() && !check_pause_button()) {
std::this_thread::yield();
}
return true;
}
bool asst::BattleHelper::wait_until_end()
{
LogTraceFunction;
while (!m_inst_helper.need_exit() && check_pause_button()) {
do_strategic_action();
std::this_thread::yield();
}
return true;
}
bool asst::BattleHelper::do_strategic_action(const cv::Mat& reusable)
{
check_pause_button(reusable);
return use_all_ready_skill(reusable);
}
bool asst::BattleHelper::use_all_ready_skill(const cv::Mat& reusable)
{
bool used = false;
cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable;
for (const auto& [name, loc] : m_battlefield_opers) {
auto& usage = m_skill_usage[name];
if (usage != SkillUsage::Possibly && usage != SkillUsage::Once) {
continue;
}
if (!check_and_use_skill(loc, image)) {
continue;
}
used = true;
if (usage == SkillUsage::Once) {
usage = SkillUsage::OnceUsed;
}
image = m_inst_helper.ctrler()->get_image();
}
return used;
}
bool asst::BattleHelper::check_and_use_skill(const std::string& name, const cv::Mat& reusable)
{
auto oper_iter = m_battlefield_opers.find(name);
if (oper_iter == m_battlefield_opers.cend()) {
Log.error("No oper", name);
return false;
}
return check_and_use_skill(oper_iter->second, reusable);
}
bool asst::BattleHelper::check_and_use_skill(const Point& loc, const cv::Mat& reusable)
{
cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable;
BattleSkillReadyImageAnalyzer skill_analyzer(image);
auto target_iter = m_normal_tile_info.find(loc);
if (target_iter == m_normal_tile_info.end()) {
Log.error("No loc", loc);
return false;
}
const Point& battlefield_point = target_iter->second.pos;
skill_analyzer.set_base_point(battlefield_point);
if (!skill_analyzer.analyze()) {
return false;
}
return use_skill(loc, false);
}
void asst::BattleHelper::save_map(const cv::Mat& image)
{
LogTraceFunction;
using namespace asst::utils::path_literals;
const auto& MapDir = "map"_p;
std::filesystem::create_directories(MapDir);
auto draw = image.clone();
for (const auto& [loc, info] : m_normal_tile_info) {
std::string text = "( " + std::to_string(loc.x) + ", " + std::to_string(loc.y) + " )";
cv::putText(draw, text, cv::Point(info.pos.x - 30, info.pos.y), 1, 1.2, cv::Scalar(0, 0, 255), 2);
}
std::string suffix;
if (++m_camera_count > 1) {
suffix = "-" + std::to_string(m_camera_count);
}
asst::imwrite(MapDir / asst::utils::path(m_stage_name + suffix + ".png"), draw);
}
bool asst::BattleHelper::click_oper_on_deployment(const std::string& name)
{
LogTraceFunction;
auto rect_opt = get_oper_rect_on_deployment(name);
if (!rect_opt) {
return false;
}
return click_oper_on_deployment(*rect_opt);
}
bool asst::BattleHelper::click_oper_on_deployment(const Rect& rect)
{
LogTraceFunction;
const auto use_oper_task_ptr = Task.get("BattleUseOper");
m_inst_helper.ctrler()->click(rect);
m_inst_helper.sleep(use_oper_task_ptr->pre_delay);
return true;
}
bool asst::BattleHelper::click_oper_on_battlefield(const std::string& name)
{
LogTraceFunction;
auto oper_iter = m_battlefield_opers.find(name);
if (oper_iter == m_battlefield_opers.cend()) {
Log.error("No oper", name);
return false;
}
return click_oper_on_battlefield(oper_iter->second);
}
bool asst::BattleHelper::click_oper_on_battlefield(const Point& loc)
{
LogTraceFunction;
const auto use_oper_task_ptr = Task.get("BattleUseOper");
auto target_iter = m_normal_tile_info.find(loc);
if (target_iter == m_normal_tile_info.end()) {
Log.error("No loc", loc);
return false;
}
const Point& target_point = target_iter->second.pos;
m_inst_helper.ctrler()->click(target_point);
m_inst_helper.sleep(use_oper_task_ptr->pre_delay);
return true;
}
bool asst::BattleHelper::click_retreat()
{
LogTraceFunction;
return ProcessTask(this_task(), { "BattleOperRetreatJustClick" }).run();
}
bool asst::BattleHelper::click_skill(bool keep_waiting)
{
LogTraceFunction;
ProcessTask skill_task(this_task(), { "BattleSkillReadyOnClick", "BattleSkillStopOnClick" });
skill_task.set_task_delay(0);
if (keep_waiting) {
return skill_task.set_retry_times(1000).run();
}
else {
bool ret = skill_task.set_retry_times(5).run();
if (!ret) {
cancel_oper_selection();
}
return ret;
}
}
bool asst::BattleHelper::cancel_oper_selection()
{
return ProcessTask(this_task(), { "BattleCancelSelection" }).run();
}
bool asst::BattleHelper::move_camera(const std::pair<double, double>& move_loc)
{
LogTraceFunction;
Log.info("move", move_loc.first, move_loc.second);
update_kills();
// 还没转场的时候
if (m_kills != 0) {
wait_until_end();
}
m_kills = 0;
m_total_kills = 0;
calc_tiles_info(m_stage_name, -move_loc.first, move_loc.second);
update_deployment(true);
return true;
}
std::optional<asst::Rect> asst::BattleHelper::get_oper_rect_on_deployment(const std::string& name) const
{
LogTraceFunction;
auto oper_iter = m_cur_deployment_opers.find(name);
if (oper_iter == m_cur_deployment_opers.cend()) {
Log.error("No oper", name);
return std::nullopt;
}
return oper_iter->second.rect;
}