From 312cc6f617967e7fe3b7e3e950b4830ae988e46d Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Thu, 5 Oct 2023 01:58:14 +0800 Subject: [PATCH] Add: Detect domain exit and go --- assets/share/rogue/exit/OCR_DOMAIN_EXIT.png | Bin 0 -> 5927 bytes tasks/base/main_page.py | 12 ++ tasks/map/control/control.py | 18 ++- tasks/map/control/joystick.py | 4 +- tasks/map/control/waypoint.py | 7 + tasks/rogue/assets/assets_rogue_exit.py | 15 ++ tasks/rogue/route/base.py | 25 ++- tasks/rogue/route/exit.py | 171 ++++++++++++++++++++ 8 files changed, 238 insertions(+), 14 deletions(-) create mode 100644 assets/share/rogue/exit/OCR_DOMAIN_EXIT.png create mode 100644 tasks/rogue/assets/assets_rogue_exit.py diff --git a/assets/share/rogue/exit/OCR_DOMAIN_EXIT.png b/assets/share/rogue/exit/OCR_DOMAIN_EXIT.png new file mode 100644 index 0000000000000000000000000000000000000000..f8c4e9b9ce467bfa19e247312c66c9f5530b95f1 GIT binary patch literal 5927 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 literal 0 HcmV?d00001 diff --git a/tasks/base/main_page.py b/tasks/base/main_page.py index abdb3496a..4027098ee 100644 --- a/tasks/base/main_page.py +++ b/tasks/base/main_page.py @@ -28,7 +28,19 @@ class OcrPlaneName(Ocr): result = result.replace('omaini', 'omain') # Domain=Combat result = result.replace('=', '') + # Domain--Occunrence + # Domain'--Occurence + # Domain-Qccurrence + result = result.replace('cunr', 'cur').replace('uren', 'urren').replace('Qcc', 'Occ') + # 区域-战 + result = re.sub(r'区域.*战$', '区域战斗', result) + # 区域-事伴, 区域-事祥 + result = result.replace('事伴', '事件').replace('事祥', '事件') + # 医域-战斗 + result = result.replace('医域', '区域') + # 区域-战半 + result = result.replace('战半', '战斗') # 累塔的办公室 result = result.replace('累塔', '黑塔') if '星港' in result: diff --git a/tasks/map/control/control.py b/tasks/map/control/control.py index 9f9ee6210..238ca3745 100644 --- a/tasks/map/control/control.py +++ b/tasks/map/control/control.py @@ -179,6 +179,11 @@ class MapControl(Combat, AimDetectorMixin): rotation_diff = self.minimap.rotation_diff(direction) logger.info(f'Pdiff: {diff}, Ddiff: {direction}, Rdiff: {rotation_diff}') + def contact_direction(): + if waypoint.lock_direction is not None: + return waypoint.lock_direction + return diff_to_180_180(direction - last_rotation) + # Interact if self.aim.aimed_enemy: if 'enemy' in waypoint.expected_end: @@ -204,6 +209,9 @@ class MapControl(Combat, AimDetectorMixin): result.append('item') if waypoint.early_stop: return result + if waypoint.interact_radius > 0: + if diff < waypoint.interact_radius: + self.handle_combat_interact() # Arrive if near := self.minimap.is_position_near(waypoint.position, threshold=waypoint.get_threshold(end_opt)): @@ -231,7 +239,7 @@ class MapControl(Combat, AimDetectorMixin): aim_interval = Timer(0.1) self.map_run_2x_timer.reset() allow_straight_run = False - if allow_run and diff < 7: + if allow_run and diff < 7 and waypoint.min_speed == 'walk': logger.info(f'Approaching target, diff={round(diff, 1)}, disallow run') direction_interval = Timer(0.2) aim_interval = Timer(0.2) @@ -255,7 +263,7 @@ class MapControl(Combat, AimDetectorMixin): rotation_interval.reset() direction_interval.reset() if direction_interval.reached(): - contact.set(direction=diff_to_180_180(direction - last_rotation), run=True) + contact.set(direction=contact_direction(), run=True) direction_interval.reset() self.handle_map_run_2x(run=True) elif allow_straight_run: @@ -275,7 +283,7 @@ class MapControl(Combat, AimDetectorMixin): rotation_interval.reset() direction_interval.reset() if direction_interval.reached(): - contact.set(direction=diff_to_180_180(direction - last_rotation), run=True) + contact.set(direction=contact_direction(), run=True) direction_interval.reset() self.handle_map_run_2x(run=False) elif allow_run: @@ -287,7 +295,7 @@ class MapControl(Combat, AimDetectorMixin): last_rotation = self.minimap.rotation allow_rotation_set = False if direction_interval.reached(): - contact.set(direction=diff_to_180_180(direction - last_rotation), run=True) + contact.set(direction=contact_direction(), run=True) direction_interval.reset() self.handle_map_run_2x(run=False) elif allow_walk: @@ -298,7 +306,7 @@ class MapControl(Combat, AimDetectorMixin): last_rotation = self.minimap.rotation allow_rotation_set = False if direction_interval.reached(): - contact.set(direction=diff_to_180_180(direction - last_rotation), run=False) + contact.set(direction=contact_direction(), run=False) direction_interval.reset() self.handle_map_run_2x(run=False) else: diff --git a/tasks/map/control/joystick.py b/tasks/map/control/joystick.py index f90dd1b10..30e9f4be4 100644 --- a/tasks/map/control/joystick.py +++ b/tasks/map/control/joystick.py @@ -81,7 +81,7 @@ class JoystickContact: def direction2screen(cls, direction, run=True): """ Args: - direction (int, float): Direction to goto (0~360) + direction (int, float): Direction to goto (-180~180) run: True for character running, False for walking Returns: @@ -111,7 +111,7 @@ class JoystickContact: Set joystick to given position Args: - direction (int, float): Direction to goto (0~360) + direction (int, float): Direction to goto (-180~180) run: True for character running, False for walking """ logger.info(f'JoystickContact set to {direction}, run={run}') diff --git a/tasks/map/control/waypoint.py b/tasks/map/control/waypoint.py index 0c61ef277..a6ca4ceec 100644 --- a/tasks/map/control/waypoint.py +++ b/tasks/map/control/waypoint.py @@ -16,6 +16,13 @@ class Waypoint: # Max move speed, 'run_2x', 'straight_run', 'run', 'walk' # See MapControl._goto() for details of each speed level speed: str = 'run' + # Min move speed, 'run' or 'walk' + min_speed: str = 'walk' + # Lock joystick direction on the way to this waypoint, -180~180 + lock_direction: int = None + # Trigger handle_combat_interact() if position diff < radius + # Usually to use 7 if interact is required, 0 for no interact + interact_radius: int = 0 """ The following attributes are only be used if this waypoint is the end point of goto() diff --git a/tasks/rogue/assets/assets_rogue_exit.py b/tasks/rogue/assets/assets_rogue_exit.py new file mode 100644 index 000000000..c2c1118ca --- /dev/null +++ b/tasks/rogue/assets/assets_rogue_exit.py @@ -0,0 +1,15 @@ +from module.base.button import Button, ButtonWrapper + +# This file was auto-generated, do not modify it manually. To generate: +# ``` python -m dev_tools.button_extract ``` + +OCR_DOMAIN_EXIT = ButtonWrapper( + name='OCR_DOMAIN_EXIT', + share=Button( + file='./assets/share/rogue/exit/OCR_DOMAIN_EXIT.png', + area=(0, 0, 1280, 320), + search=(0, 0, 1280, 340), + color=(255, 255, 255), + button=(0, 0, 1280, 320), + ), +) diff --git a/tasks/rogue/route/base.py b/tasks/rogue/route/base.py index 2327e3be0..a64fbbc37 100644 --- a/tasks/rogue/route/base.py +++ b/tasks/rogue/route/base.py @@ -135,8 +135,6 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent): """ if self.ui_page_appear(page_rogue): return True - if self.handle_combat_interact(): - return False return False def clear_event(self, *waypoints): @@ -147,6 +145,7 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent): waypoints = ensure_waypoints(waypoints) end_point = waypoints[-1] end_point.endpoint_threshold = 1.5 + end_point.interact_radius = 7 end_point.expected_end.append(self._domain_event_expected_end) result = self.goto(*waypoints) @@ -194,9 +193,6 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent): if self.handle_popup_confirm(): return False - if self.minimap.position_diff(self.waypoint.position) < 7: - if self.handle_combat_interact(): - return False return False @@ -236,6 +232,7 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent): logger.hr('Domain single exit', level=1) waypoints = ensure_waypoints(waypoints) end_point = waypoints[-1] + end_point.interact_radius = 7 end_point.expected_end.append(self._domain_exit_expected_end) result = self.goto(*waypoints) @@ -251,8 +248,22 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent): end_point.end_rotation_threshold = 10 result = self.goto(*waypoints) - # TODO: Domain exit detection - pass + logger.hr('Find domain exit', level=2) + direction = self.predict_door_by_name(self.device.image) + direction_limit = 55 + if direction is not None: + logger.warning(f'Unexpected direction to go: {direction}, limited in {direction_limit}') + if abs(direction) > direction_limit: + if direction > 0: + direction = direction_limit + elif direction < 0: + direction = -direction_limit + end_point.end_rotation = None + end_point.min_speed = 'run' + end_point.interact_radius = 50 + end_point.expected_end.append(self._domain_exit_expected_end) + end_point.lock_direction = direction + self.goto(end_point) return result diff --git a/tasks/rogue/route/exit.py b/tasks/rogue/route/exit.py index fbca65bb6..06d55b52e 100644 --- a/tasks/rogue/route/exit.py +++ b/tasks/rogue/route/exit.py @@ -1,11 +1,82 @@ +import re +from typing import Optional + +import cv2 +import numpy as np + from module.base.timer import Timer +from module.base.utils import Points, extract_white_letters from module.logger import logger +from tasks.base.assets.assets_base_main_page import OCR_MAP_NAME +from tasks.base.main_page import OcrPlaneName from tasks.base.page import page_rogue from tasks.combat.interact import CombatInteract +from tasks.map.keywords import KEYWORDS_MAP_PLANE, MapPlane +from tasks.rogue.assets.assets_rogue_exit import OCR_DOMAIN_EXIT from tasks.rogue.assets.assets_rogue_reward import ROGUE_REPORT from tasks.rogue.assets.assets_rogue_ui import BLESSING_CONFIRM +def area_center(area): + """ + Get the center of an area + + Args: + area: (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y) + + Returns: + tuple: (x, y) + """ + x1, y1, x2, y2 = area + return (x1 + x2) / 2, (y1 + y2) / 2 + + +class OcrDomainExit(OcrPlaneName): + merge_thres_x = 50 + + def pre_process(self, image): + image = extract_white_letters(image, threshold=255) + image = cv2.merge([image, image, image]) + return image + + def detect_and_ocr(self, *args, **kwargs): + # Try hard to lower TextSystem.box_thresh + backup = self.model.text_detector.box_thresh + self.model.text_detector.box_thresh = 0.2 + + result = super().detect_and_ocr(*args, **kwargs) + + self.model.text_detector.box_thresh = backup + return result + + def _match_result( + self, + result: str, + keyword_classes, + lang: str = None, + ignore_punctuation=True, + ignore_digit=True): + matched = super()._match_result(result, keyword_classes, lang, ignore_punctuation, ignore_digit) + + # Name may be covered by minimap, "Domain - " is missing, + # check keywords like "Combat" + if matched is None: + for domain in MapPlane.instances.values(): + domain: MapPlane = domain + if not domain.rogue_domain: + continue + + name = domain._keywords_to_find(ignore_punctuation=False)[0] + try: + name = re.split('[ \-—]', name)[-1] + except IndexError: + pass + if name in result: + return domain + + return matched + + class RogueExit(CombatInteract): def domain_exit_interact(self, skip_first_screenshot=True): """ @@ -57,3 +128,103 @@ class RogueExit(CombatInteract): if self.handle_popup_confirm(): confirm.reset() continue + + @staticmethod + def screen2direction(point): + """ + Args: + point: Coordinate on screenshot + + Returns: + float: Direction to move, -180~180 + """ + screen_middle = (640.0, 360.0) + vanish_point = np.array((640.0, 247.34)) + distant_point = np.array((1509.46, 247.34)) + name_y = 77.60 + foot_y = 621.82 + + door_projection_bottom = ( + Points([point]).link(vanish_point).get_x(name_y)[0], + foot_y, + ) + door_bottom = ( + point[0], + Points([door_projection_bottom]).link(vanish_point).get_y(point[0])[0], + ) + door_distant = ( + Points([door_bottom]).link(distant_point).get_x(foot_y)[0], + foot_y, + ) + planar_door = ( + door_projection_bottom[0] - screen_middle[0], + door_projection_bottom[0] - door_distant[0], + ) + if abs(planar_door[0]) < 5: + direction = 0 + else: + direction = np.rad2deg(np.arctan(planar_door[0] / planar_door[1])) + + planar_door = (round(planar_door[0], 1), round(planar_door[1], 1)) + direction = round(direction, 1) + logger.info(f'PlanarDoor: {planar_door}, direction: {direction}') + return direction + + def predict_door_by_name(self, image) -> Optional[float]: + # Paint current name black + x1, y1, x2, y2 = OCR_MAP_NAME.area + image[y1:y2, x1:x2] = (0, 0, 0) + + ocr = OcrDomainExit(OCR_DOMAIN_EXIT) + results = ocr.matched_ocr(image, keyword_classes=MapPlane) + centers = [area_center(result.area) for result in results] + logger.info(f'DomainDoor: {centers}') + directions = [self.screen2direction(center) for center in centers] + + count = len(centers) + if count == 0: + logger.warning('No domain exit found') + return None + if count == 1: + logger.info(f'Goto next domain: {results[0]}') + return directions[0] + + # Doors >= 2 + for expect in [ + KEYWORDS_MAP_PLANE.Rogue_DomainBoss, + KEYWORDS_MAP_PLANE.Rogue_DomainElite, + KEYWORDS_MAP_PLANE.Rogue_DomainRespite, + ]: + for domain, direction in zip(results, directions): + if domain == expect: + logger.warning('Found multiple doors but has unique domain in it') + logger.info(f'Goto next domain: {domain}') + return direction + if self.config.RoguePath_DomainStrategy == 'leave': + for expect in [ + KEYWORDS_MAP_PLANE.Rogue_DomainTransaction, + KEYWORDS_MAP_PLANE.Rogue_DomainOccurrence, + KEYWORDS_MAP_PLANE.Rogue_DomainEncounter, + KEYWORDS_MAP_PLANE.Rogue_DomainCombat, + ]: + for domain, direction in zip(results, directions): + if domain == expect: + logger.info(f'Goto next domain: {domain}') + return direction + elif self.config.RoguePath_DomainStrategy == 'fight': + for expect in [ + KEYWORDS_MAP_PLANE.Rogue_DomainCombat, + KEYWORDS_MAP_PLANE.Rogue_DomainEncounter, + KEYWORDS_MAP_PLANE.Rogue_DomainOccurrence, + KEYWORDS_MAP_PLANE.Rogue_DomainTransaction, + ]: + for domain, direction in zip(results, directions): + if domain == expect: + logger.info(f'Goto next domain: {domain}') + return direction + else: + logger.error(f'Unknown domain strategy: {self.config.RoguePath_DomainStrategy}') + + logger.error('No domain was selected, return the first instead') + logger.info(f'Goto next domain: {results[0]}') + return directions[0]