mirror of
https://github.com/MaaAssistantArknights/MaaAssistantArknights.git
synced 2026-07-15 17:30:27 +08:00
607 lines
18 KiB
C++
607 lines
18 KiB
C++
#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;
|
||
}
|