#include "BattleHelper.h" #include #include #include "Config/Miscellaneous/AvatarCacheManager.h" #include "Config/Miscellaneous/BattleDataConfig.h" #include "Config/TaskData.h" #include "Controller/Controller.h" #include "Task/ProcessTask.h" #include "Utils/ImageIo.hpp" #include "Utils/Logger.hpp" #include "Utils/NoWarningCV.h" #include "Vision/Battle/BattlefieldClassifier.h" #include "Vision/Battle/BattlefieldMatcher.h" #include "Vision/BestMatcher.h" #include "Vision/Matcher.h" #include "Vision/RegionOCRer.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 (auto result = Tile.find(name); !result || !json::open(result->second)) { 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_skill_times.clear(); m_skill_error_count.clear(); m_last_use_skill_time.clear(); m_camera_count = 0; m_camera_shift = { 0., 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 (auto result = Tile.find(stage_name); !result || !json::open(result->second)) { return false; } m_map_data = TilePack::find_level(stage_name).value_or(Map::Level {}); auto calc_result = TilePack::calc(stage_name, shift_x, shift_y); m_normal_tile_info = std::move(calc_result.normal_tile_info); m_side_tile_info = std::move(calc_result.side_tile_info); m_retreat_button_pos = calc_result.retreat_button; m_skill_button_pos = calc_result.skill_button; 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) { AvatarCache.remove_avatars(Role::Drone);// 移除小龙等不同技能很像的召唤物,防止错误识别 wait_until_start(false); } 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); }); } BattlefieldMatcher oper_analyzer(image); // 保全要识别开局费用,先用init判断了,之后别的地方要用的话再做cache if (init) { oper_analyzer.set_object_of_interest({ .deployment = true, .oper_cost = true }); } else { oper_analyzer.set_object_of_interest({ .deployment = true }); } auto oper_result_opt = oper_analyzer.analyze(); if (!oper_result_opt) { check_in_battle(image); return false; } const auto old_deployment_opers = std::move(m_cur_deployment_opers); m_cur_deployment_opers = std::vector(); m_cur_deployment_opers.reserve(oper_result_opt->deployment.size()); // 从场上干员和已占用格子中移除冷却中的干员 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 set_oper_name = [&](DeploymentOper& oper, const std::string& name) { oper.name = name; oper.location_type = BattleData.get_location_type(name); oper.is_unusual_location = battle::get_role_usual_location(oper.role) == oper.location_type; }; auto& cur_opers = oper_result_opt->deployment; std::vector unknown_opers; for (auto& oper : cur_opers) { BestMatcher avatar_analyzer(oper.avatar); if (oper.cooling) { Log.trace("start matching cooling", oper.index); static const auto cooling_threshold = Task.get("BattleAvatarCoolingData")->templ_thresholds.front(); static const auto cooling_mask_range = Task.get("BattleAvatarCoolingData")->mask_range; avatar_analyzer.set_threshold(cooling_threshold); avatar_analyzer .set_mask_range(cooling_mask_range.first, cooling_mask_range.second, true, true); } else { static const auto threshold = Task.get("BattleAvatarData")->templ_thresholds.front(); static const auto drone_threshold = Task.get("BattleDroneAvatarData")->templ_thresholds.front(); avatar_analyzer.set_threshold(oper.role == Role::Drone ? drone_threshold : threshold); } bool is_analyzed = false; if (!init) { for (const auto& old_oper : old_deployment_opers | views::filter([&](const battle::DeploymentOper& temp_oper) { return temp_oper.role == oper.role; })) { avatar_analyzer.append_templ(old_oper.name, old_oper.avatar); } if (avatar_analyzer.analyze()) { set_oper_name(oper, avatar_analyzer.get_result().templ_info.name); remove_cooling_from_battlefield(oper); is_analyzed = true; } } if (!is_analyzed) { // 之前的干员都没匹配上,那就把所有的干员都加进去 // 可能的优化: 移除之前添加的模板 auto& avatar_cache = AvatarCache.get_avatars(oper.role); for (const auto& [name, avatar] : avatar_cache) { avatar_analyzer.append_templ(name, avatar); } if (avatar_analyzer.analyze()) { set_oper_name(oper, avatar_analyzer.get_result().templ_info.name); remove_cooling_from_battlefield(oper); } else { Log.info("unknown oper", oper.index); unknown_opers.emplace_back(oper); } } m_cur_deployment_opers.emplace_back(oper); if (oper.cooling) { Log.trace("stop matching cooling", oper.index); } } if (ranges::count_if(unknown_opers, [](const DeploymentOper& it) { return !it.cooling; }) > 0 || init) { // 一个都没匹配上的,挨个点开来看一下 LogTraceScope("rec unknown opers"); // 暂停游戏准备识别干员 do { pause(); // 在刚进入游戏的时候(画面刚刚完全亮起来的时候),点暂停是没反应的 // 所以这里一直点,直到真的点上了为止 if (!init || !check_pause_button()) { break; } std::this_thread::yield(); } while (!m_inst_helper.need_exit()); if (!check_in_battle(image)) { return false; } cv::Mat name_image; for (auto& oper : unknown_opers) { LogTraceScope("rec unknown oper: " + std::to_string(oper.index)); if (oper.cooling) { Log.info("cooling oper, skip"); oper.name = "UnknownCooling_" + std::to_string(oper.index); continue; } Rect oper_rect = oper.rect; // 点完部署区的一个干员之后,他的头像会放大;其他干员的位置都被挤开了,不在原来的位置了 // 所以只有第一个干员可以直接点,后面干员都要重新识别一下位置 if (!name_image.empty()) { Matcher re_matcher; re_matcher.set_task_info("BattleAvatarReMatch"); re_matcher.set_image(name_image); re_matcher.set_templ(oper.avatar); auto rematched = re_matcher.analyze(); if (rematched) { oper_rect = rematched->rect; } } click_oper_on_deployment(oper_rect); name_image = m_inst_helper.ctrler()->get_image(); if (!check_in_battle(name_image)) { return false; } std::string name = analyze_detail_page_oper_name(name_image); // 这时候即使名字不合法也只能凑合用了,但是为空还是不行的 if (name.empty()) { Log.error("name is empty"); continue; } set_oper_name(oper, name); remove_cooling_from_battlefield(oper); m_cur_deployment_opers[oper.index] = oper; AvatarCache.set_avatar(name, oper.role, oper.avatar); } pause(); if (!unknown_opers.empty()) { cancel_oper_selection(); } image = m_inst_helper.ctrler()->get_image(); } if (init) { update_kills(image); } return check_in_battle(image); } bool asst::BattleHelper::update_kills(const cv::Mat& reusable) { cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable; BattlefieldMatcher analyzer(image); analyzer.set_object_of_interest({ .kills = true }); if (m_total_kills) { analyzer.set_total_kills_prompt(m_total_kills); } auto result_opt = analyzer.analyze(); if (!result_opt || !result_opt->kills) { return false; } std::tie(m_kills, m_total_kills) = result_opt->kills.value(); return true; } bool asst::BattleHelper::update_cost(const cv::Mat& reusable) { cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable; BattlefieldMatcher analyzer(image); analyzer.set_object_of_interest({ .costs = true }); auto result_opt = analyzer.analyze(); if (!result_opt || !result_opt->costs) { return false; } m_cost = result_opt->costs.value(); 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; } Point target_point = target_iter->second.pos; int dist = static_cast(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(dist / 1000.0 * swipe_oper_task_ptr->pre_delay); // 时间太短了的压根放不上去,故意加长一点 if (int min_duration = swipe_oper_task_ptr->special_params.at(4); duration < min_duration) { duration = min_duration; } bool deploy_with_pause = ControlFeat::support( m_inst_helper.ctrler()->support_features(), ControlFeat::SWIPE_WITH_PAUSE); Point oper_point(oper_rect.x + oper_rect.width / 2, oper_rect.y + oper_rect.height / 2); m_inst_helper.ctrler()->swipe( oper_point, target_point, duration, false, swipe_oper_task_ptr->special_params.at(2), swipe_oper_task_ptr->special_params.at(3), deploy_with_pause); // 拖动干员朝向 if (direction != DeployDirection::None) { static const std::unordered_map DirectionMap = { { DeployDirection::Right, Point::right() }, { DeployDirection::Down, Point::down() }, { DeployDirection::Left, Point::left() }, { DeployDirection::Up, Point::up() }, { DeployDirection::None, Point::zero() }, }; // 计算往哪边拖动 const Point& direction_target = DirectionMap.at(direction); // 将方向转换为实际的 swipe end 坐标点,并对滑动距离进行缩放 const auto scale_size = m_inst_helper.ctrler()->get_scale_size(); static const int coeff = static_cast(swipe_oper_task_ptr->special_params.at(0) * scale_size.second / 720.0); Point end_point = target_point + (direction_target * coeff); fix_swipe_out_of_limit( target_point, end_point, scale_size.first, scale_size.second, swipe_oper_task_ptr->special_params.at(1)); 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); // 仅简单复用,该延迟含义与此处逻辑无关 by MistEO m_inst_helper.sleep(use_oper_task_ptr->pre_delay); } if (deploy_with_pause) { // m_inst_helper.ctrler()->press_esc(); ProcessTask(this_task(), { "BattlePauseCancel" }).run(); } // for SSS, multiple operator may be deployed at the same location. if (m_used_tiles.contains(loc)) { std::string pre_name = m_used_tiles.at(loc); Log.info("remove previous oper", pre_name, loc); m_used_tiles.erase(loc); m_battlefield_opers.erase(pre_name); } // 肉鸽中,一个干员可能被部署多次 if (m_battlefield_opers.contains(name) && BattleData.get_role(name) != battle::Role::Drone) { Point pre_loc = m_battlefield_opers.at(name); Log.info("remove deploy failed oper", name, pre_loc); m_battlefield_opers.erase(name); // 擦除已使用干员,但是不擦除已使用格子 // m_used_tiles.erase(pre_loc); } m_used_tiles.emplace(loc, name); m_battlefield_opers.emplace(name, loc); m_last_use_skill_time.emplace(loc, std::chrono::steady_clock::time_point()); 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; Matcher battle_flag_analyzer(image); battle_flag_analyzer.set_task_info("BattleOfficiallyBegin"); bool ret = battle_flag_analyzer.analyze().has_value(); BattlefieldMatcher battle_flag_analyzer_2(image); auto battle_result_opt = battle_flag_analyzer_2.analyze(); ret &= battle_result_opt && battle_result_opt->pause_button; return ret; } bool asst::BattleHelper::check_skip_plot_button(const cv::Mat& reusable) { cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable; Matcher battle_plot_analyzer(image); battle_plot_analyzer.set_task_info("SkipThePreBattlePlot"); bool ret = battle_plot_analyzer.analyze().has_value(); if (ret) { ProcessTask(this_task(), { "SkipThePreBattlePlot" }).run(); } return ret; } bool asst::BattleHelper::check_in_speed_up(const cv::Mat& reusable) { cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable; Matcher analyzer(image); analyzer.set_task_info("BattleSpeedUpCheck"); return analyzer.analyze().has_value(); } bool asst::BattleHelper::check_in_battle(const cv::Mat& reusable, bool weak) { cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable; if (weak) { BattlefieldMatcher analyzer(image); m_in_battle = analyzer.analyze().has_value(); } else { check_skip_plot_button(image); m_in_battle = check_pause_button(image); } return m_in_battle; } bool asst::BattleHelper::wait_until_start(bool weak) { LogTraceFunction; cv::Mat image = m_inst_helper.ctrler()->get_image(); while (!m_inst_helper.need_exit() && !check_in_battle(image, weak)) { do_strategic_action(image); std::this_thread::yield(); image = m_inst_helper.ctrler()->get_image(); } return true; } bool asst::BattleHelper::wait_until_end(bool weak) { LogTraceFunction; cv::Mat image = m_inst_helper.ctrler()->get_image(); while (!m_inst_helper.need_exit() && check_in_battle(image, weak)) { do_strategic_action(image); std::this_thread::yield(); image = m_inst_helper.ctrler()->get_image(); } return true; } bool asst::BattleHelper::do_strategic_action(const cv::Mat& reusable) { cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable; return use_all_ready_skill(image); } bool asst::BattleHelper::use_all_ready_skill(const cv::Mat& reusable) { // TODO: 可配置延迟时间 static constexpr auto min_frame_interval = std::chrono::milliseconds(1000); 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]; auto& retry = m_skill_error_count[name]; auto& times = m_skill_times[name]; auto& last_use_time = m_last_use_skill_time[name]; if (usage != SkillUsage::Possibly && usage != SkillUsage::Times) { continue; } const auto now = std::chrono::steady_clock::now(); if (auto interval = now - last_use_time; min_frame_interval > interval) { Log.debug( name, "use skill too fast, interval time:", std::chrono::duration_cast(interval)); continue; } bool has_error = false; if (!check_and_use_skill(loc, has_error, image)) { continue; } // 识别到了,但点进去发现没有。一般来说是识别错了 if (has_error) { Log.warn("Skill", name, "is not ready"); constexpr int MaxRetry = 3; if (++retry >= MaxRetry) { Log.warn("Do not use skill anymore", name); usage = SkillUsage::NotUse; } continue; } used = true; retry = 0; m_last_use_skill_time[name] = now; if (usage == SkillUsage::Times) { times--; if (times == 0) { usage = SkillUsage::TimesUsed; } } // image = m_inst_helper.ctrler()->get_image(); } return used; } bool asst::BattleHelper::check_and_use_skill( const std::string& name, bool& has_error, 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, has_error, reusable); } bool asst::BattleHelper::check_and_use_skill( const Point& loc, bool& has_error, const cv::Mat& reusable) { cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable; BattlefieldClassifier skill_analyzer(image); skill_analyzer.set_object_of_interest({ .skill_ready = true }); 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()->skill_ready.ready) { return false; } has_error = !use_skill(loc, false); return true; } void asst::BattleHelper::save_map(const cv::Mat& image) { LogTraceFunction; using namespace asst::utils::path_literals; const auto& MapRelativeDir = "debug"_p / "map"_p; auto draw = image.clone(); for (const auto& [loc, info] : m_normal_tile_info) { cv::circle(draw, cv::Point(info.pos.x, info.pos.y), 5, cv::Scalar(0, 255, 0), -1); cv::putText( draw, loc.to_string(), 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(MapRelativeDir / 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; bool deploy_with_pause = ControlFeat::support( m_inst_helper.ctrler()->support_features(), ControlFeat::SWIPE_WITH_PAUSE); if (deploy_with_pause) { ProcessTask(this_task(), { "BattlePause" }).run(); } // return ProcessTask(this_task(), { "BattleOperRetreatJustClick" }).run(); bool ret = m_inst_helper.ctrler()->click(m_retreat_button_pos); if (deploy_with_pause) { ProcessTask(this_task(), { "BattlePauseCancel" }).run(); } return ret; } // TODO: use m_skill_button_pos bool asst::BattleHelper::click_skill(bool keep_waiting) { LogTraceFunction; bool deploy_with_pause = ControlFeat::support( m_inst_helper.ctrler()->support_features(), ControlFeat::SWIPE_WITH_PAUSE); if (!keep_waiting && deploy_with_pause) { ProcessTask(this_task(), { "BattlePause" }).run(); } ProcessTask skill_task( this_task(), { "BattleSkillReadyOnClick", "BattleSkillReadyOnClick-SquareMap", "BattleSkillStopOnClick", "BattleSkillStopOnClick-MiddleMap", "BattleSkillStopOnClick-SquareMap" }); 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(); } if (deploy_with_pause) { ProcessTask(this_task(), { "BattlePauseCancel" }).run(); } return ret; } } bool asst::BattleHelper::cancel_oper_selection() { return ProcessTask(this_task(), { "BattleCancelSelection" }).run(); } void asst::BattleHelper::fix_swipe_out_of_limit( Point& p1, Point& p2, int width, int height, int max_distance, double radian) { Point direct = Point::zero(); int distance = 0; if (p2.y > height) { // 下边界超限 direct = Point::up(); distance = p2.y - height; } else if (p2.x > width) { // 右边界超限 direct = Point::left(); distance = p2.x - width; } else if (p2.y < 0) { // 上边界超限 direct = Point::down(); distance = -p2.y; } else if (p2.x < 0) { // 左边界超限 direct = Point::right(); distance = -p2.x; } else { return; } std::tuple adjust_scale = { direct.x * std::cos(radian) - direct.y * std::sin(radian), direct.y * std::cos(radian) + direct.x * std::sin(radian), }; // 旋转后偏移值会不够,计算补偿比例 double adjust_more = std::get<0>(adjust_scale) * direct.x + std::get<1>(adjust_scale) * direct.y; Point adjust = { static_cast(std::get<0>(adjust_scale) / adjust_more * distance), static_cast(std::get<1>(adjust_scale) / adjust_more * distance), }; if (auto point_distance = Point::distance(adjust, { 0, 0 }); point_distance > max_distance) { adjust = { static_cast(adjust.x * max_distance / point_distance), static_cast(adjust.y * max_distance / point_distance), }; } Log.info( __FUNCTION__, "swipe end_point out of limit, start:", p1, ", end:", p2, ", adjust:", adjust); p1 += adjust; p2 += adjust; } bool asst::BattleHelper::move_camera(const std::pair& delta) { LogTraceFunction; Log.info("move", delta.first, delta.second); update_kills(); // 还没转场的时候 if (m_kills != 0) { wait_until_end(false); } m_kills = 0; m_total_kills = 0; m_camera_shift.first += delta.first; m_camera_shift.second += delta.second; calc_tiles_info(m_stage_name, -m_camera_shift.first, m_camera_shift.second); return update_deployment(true); } std::string asst::BattleHelper::analyze_detail_page_oper_name(const cv::Mat& image) { const auto& replace_task = Task.get("CharsNameOcrReplace"); const auto& task = Task.get(oper_name_ocr_task_name()); RegionOCRer preproc_analyzer(image); preproc_analyzer.set_task_info(task); preproc_analyzer.set_replace(replace_task->replace_map, replace_task->replace_full); auto preproc_result_opt = preproc_analyzer.analyze(); if (preproc_result_opt && !BattleData.is_name_invalid(preproc_result_opt->text)) { return preproc_result_opt->text; } Log.warn("ocr with preprocess got a invalid name, try to use detect model"); OCRer det_analyzer(image); det_analyzer.set_task_info(task); det_analyzer.set_replace(replace_task->replace_map, replace_task->replace_full); auto det_result_opt = det_analyzer.analyze(); if (!det_result_opt) { return {}; } sort_by_score_(*det_result_opt); const auto& det_name = det_result_opt->front().text; return BattleData.is_name_invalid(det_name) ? std::string() : det_name; } std::optional asst::BattleHelper::get_oper_rect_on_deployment(const std::string& name) const { LogTraceFunction; auto oper_iter = ranges::find_if(m_cur_deployment_opers, [&](const auto& oper) { return oper.name == name; }); if (oper_iter == m_cur_deployment_opers.end()) { Log.error("No oper", name); return std::nullopt; } return oper_iter->rect; }