diff --git a/assets/cn/assignment/ui/EVENT_FINISHED.png b/assets/cn/assignment/ui/EVENT_FINISHED.png new file mode 100644 index 000000000..b0dedb535 Binary files /dev/null and b/assets/cn/assignment/ui/EVENT_FINISHED.png differ diff --git a/module/alas.py b/module/alas.py index 34d1d7b53..5c185d170 100644 --- a/module/alas.py +++ b/module/alas.py @@ -1,5 +1,3 @@ -import os -import re import threading import time from datetime import datetime, timedelta @@ -11,7 +9,7 @@ from module.base.decorator import del_cached_property from module.config.config import AzurLaneConfig, TaskEnd from module.config.utils import deep_get, deep_set from module.exception import * -from module.logger import logger +from module.logger import logger, save_error_log from module.notify import handle_notify @@ -117,6 +115,9 @@ class AzurLaneAutoScript: else: self.checker.wait_until_available() return False + except HandledError as e: + logger.error(e) + return False except ScriptError as e: logger.critical(e) logger.critical('This is likely to be a mistake of developers, but sometimes just random issues') @@ -149,33 +150,7 @@ class AzurLaneAutoScript: Save last 60 screenshots in ./log/error/ Save logs to ./log/error//log.txt """ - from module.base.utils import save_image - from module.handler.sensitive_info import (handle_sensitive_image, handle_sensitive_logs) - if self.config.Error_SaveError: - folder = f'./log/error/{int(time.time() * 1000)}' - logger.warning(f'Saving error: {folder}') - os.makedirs(folder, exist_ok=True) - for data in self.device.screenshot_deque: - image_time = datetime.strftime(data['time'], '%Y-%m-%d_%H-%M-%S-%f') - image = handle_sensitive_image(data['image']) - save_image(image, f'{folder}/{image_time}.png') - if self.device.screenshot_tracking: - os.makedirs(f'{folder}/tracking', exist_ok=True) - for data in self.device.screenshot_tracking: - image_time = datetime.strftime(data['time'], '%Y-%m-%d_%H-%M-%S-%f') - with open(f'{folder}/tracking/{image_time}.png', 'wb') as f: - f.write(data['image'].getvalue()) - with open(logger.log_file, 'r', encoding='utf-8') as f: - lines = f.readlines() - start = 0 - for index, line in enumerate(lines): - line = line.strip(' \r\t\n') - if re.match('^═{15,}$', line): - start = index - lines = lines[start - 2:] - lines = handle_sensitive_logs(lines) - with open(f'{folder}/log.txt', 'w', encoding='utf-8') as f: - f.writelines(lines) + save_error_log(config=self.config, device=self.device) def wait_until(self, future): """ diff --git a/module/exception.py b/module/exception.py index 2d027bb99..a0e96e460 100644 --- a/module/exception.py +++ b/module/exception.py @@ -17,6 +17,12 @@ class GameTooManyClickError(Exception): pass +class HandledError(Exception): + # Error handled before raising + # No extra handling required, just retry + pass + + class EmulatorNotRunningError(Exception): pass diff --git a/module/logger/__init__.py b/module/logger/__init__.py index b584b3c1f..a4d045ddc 100644 --- a/module/logger/__init__.py +++ b/module/logger/__init__.py @@ -1,3 +1,2 @@ -from .logger import logger -from .logger import set_file_logger, set_func_logger -from .logger import WEB_THEME, Highlighter, HTMLConsole +from .error import save_error_log +from .logger import HTMLConsole, Highlighter, WEB_THEME, logger, set_file_logger, set_func_logger diff --git a/module/logger/error.py b/module/logger/error.py new file mode 100644 index 000000000..b2236d5ad --- /dev/null +++ b/module/logger/error.py @@ -0,0 +1,44 @@ +import os +import re +import time +from datetime import datetime + +from module.logger.logger import logger + + +def save_error_log(config, device): + """ + Save last 60 screenshots in ./log/error/ + Save logs to ./log/error//log.txt + + Args: + config: AzurLaneConfig object + device: Device object + """ + from module.base.utils import save_image + from module.handler.sensitive_info import (handle_sensitive_image, handle_sensitive_logs) + if config.Error_SaveError: + folder = f'./log/error/{int(time.time() * 1000)}' + logger.warning(f'Saving error: {folder}') + os.makedirs(folder, exist_ok=True) + for data in device.screenshot_deque: + image_time = datetime.strftime(data['time'], '%Y-%m-%d_%H-%M-%S-%f') + image = handle_sensitive_image(data['image']) + save_image(image, f'{folder}/{image_time}.png') + if device.screenshot_tracking: + os.makedirs(f'{folder}/tracking', exist_ok=True) + for data in device.screenshot_tracking: + image_time = datetime.strftime(data['time'], '%Y-%m-%d_%H-%M-%S-%f') + with open(f'{folder}/tracking/{image_time}.png', 'wb') as f: + f.write(data['image'].getvalue()) + with open(logger.log_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + start = 0 + for index, line in enumerate(lines): + line = line.strip(' \r\t\n') + if re.match('^═{15,}$', line): + start = index + lines = lines[start - 2:] + lines = handle_sensitive_logs(lines) + with open(f'{folder}/log.txt', 'w', encoding='utf-8') as f: + f.writelines(lines) diff --git a/tasks/assignment/assets/assets_assignment_ui.py b/tasks/assignment/assets/assets_assignment_ui.py index 1ef0c0780..26379e15b 100644 --- a/tasks/assignment/assets/assets_assignment_ui.py +++ b/tasks/assignment/assets/assets_assignment_ui.py @@ -116,6 +116,17 @@ ENTRY_LOADED = ButtonWrapper( button=(474, 161, 491, 615), ), ) +EVENT_FINISHED = ButtonWrapper( + name='EVENT_FINISHED', + cn=Button( + file='./assets/cn/assignment/ui/EVENT_FINISHED.png', + area=(700, 300, 752, 331), + search=(680, 280, 772, 351), + color=(223, 215, 195), + button=(700, 300, 752, 331), + ), + en=None, +) EXP_MATERIALS_CREDITS_CHECK = ButtonWrapper( name='EXP_MATERIALS_CREDITS_CHECK', cn=Button( diff --git a/tasks/assignment/assignment.py b/tasks/assignment/assignment.py index 015247cfd..3019181dd 100644 --- a/tasks/assignment/assignment.py +++ b/tasks/assignment/assignment.py @@ -35,7 +35,6 @@ class Assignment(AssignmentClaim, SynthesizeUI): self.has_new_dispatch = False self.ensure_scroll_top(page_menu) self.ui_ensure(page_assignment) - self._wait_until_group_loaded() event_ongoing = next(( g for g in self._iter_groups() if isinstance(g, AssignmentEventGroup) @@ -91,12 +90,14 @@ class Assignment(AssignmentClaim, SynthesizeUI): logger.info( f'User specified assignments: {", ".join([x.name for x in assignments])}') remain = None + insight = False for assignment in assignments: if assignment in self.dispatched: continue logger.hr('Assignment inlist', level=2) logger.info(f'Check assignment inlist: {assignment}') - self.goto_entry(assignment) + self.goto_entry(assignment, insight=insight) + insight = True if remain is None: _, remain, _ = self._limit_status status = self._check_assignment_status() @@ -106,6 +107,7 @@ class Assignment(AssignmentClaim, SynthesizeUI): if status == AssignmentStatus.DISPATCHED: self.dispatched[assignment] = datetime.now() + \ self._get_assignment_time() + insight = False continue # General assignments must be dispatchable here if remain <= 0: @@ -205,8 +207,8 @@ class Assignment(AssignmentClaim, SynthesizeUI): continue logger.hr('Assignment event', level=2) logger.info(f'Check assignment event: {assignment}') - # Order of entries does not change during iteration - self.goto_entry(assignment, insight=False) + # Order of entries changes if claimed + self.goto_entry(assignment, insight=claimed) status = self._check_assignment_status() if status == AssignmentStatus.LOCKED: continue diff --git a/tasks/assignment/claim.py b/tasks/assignment/claim.py index 7b4260d00..4118fe3fd 100644 --- a/tasks/assignment/claim.py +++ b/tasks/assignment/claim.py @@ -4,6 +4,7 @@ from module.config.stored.classes import now from module.logger import logger from module.ocr.ocr import Duration from tasks.assignment.assets.assets_assignment_claim import * +from tasks.assignment.assets.assets_assignment_ui import EVENT_FINISHED from tasks.assignment.dispatch import AssignmentDispatch from tasks.assignment.keywords import AssignmentEntry from tasks.base.page import page_assignment @@ -79,6 +80,9 @@ class AssignmentClaim(AssignmentDispatch): if self.appear(page_assignment.check_button): logger.info('Assignment report is closed') break + if self.appear(EVENT_FINISHED): + logger.info('Event finished') + return # Close report if self.appear(REPORT, interval=1): self.device.click(click_button) diff --git a/tasks/assignment/dispatch.py b/tasks/assignment/dispatch.py index 37acd7e12..fb818c569 100644 --- a/tasks/assignment/dispatch.py +++ b/tasks/assignment/dispatch.py @@ -105,11 +105,11 @@ class AssignmentDispatch(AssignmentUI): continue # Select if self.interval_is_reached(CHARACTER_1_SELECTED, interval=2): - if not self.image_color_count(CHARACTER_1_SELECTED, (240, 240, 240)): + if not self.image_color_count(CHARACTER_1_SELECTED, (240, 240, 240), threshold=221, count=160): self.device.click(CHARACTER_1) self.interval_reset(CHARACTER_1_SELECTED, interval=2) if self.interval_is_reached(CHARACTER_2_SELECTED, interval=2): - if not self.image_color_count(CHARACTER_2_SELECTED, (240, 240, 240)): + if not self.image_color_count(CHARACTER_2_SELECTED, (240, 240, 240), threshold=221, count=160): self.device.click(CHARACTER_2) self.interval_reset(CHARACTER_2_SELECTED, interval=2) @@ -126,7 +126,8 @@ class AssignmentDispatch(AssignmentUI): # End if self.appear(CONFIRM_ASSIGNMENT): if self.image_color_count(CONFIRM_ASSIGNMENT.button, color=(227, 227, 227), count=1000): - logger.info('Characters are all selected (light button)') + logger.info( + 'Characters are all selected (light button)') break if self.appear(CHARACTER_LIST, interval=2): # EMPTY_SLOT appeared above diff --git a/tasks/assignment/ui.py b/tasks/assignment/ui.py index b083b6897..60e8cde5e 100644 --- a/tasks/assignment/ui.py +++ b/tasks/assignment/ui.py @@ -40,7 +40,8 @@ class AssignmentOcr(Ocr): (KEYWORDS_ASSIGNMENT_ENTRY.Legend_of_the_Puppet_Master, '^师传说'), (KEYWORDS_ASSIGNMENT_ENTRY.The_Wages_of_Humanity, '[赠]养人类'), (KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Car_Thief, '.*的偷车贼.*'), - (KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Synesthesia_Beacon_Function_Iteration, '联觉信标功能[送]代'), + (KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Synesthesia_Beacon_Function_Iteration, + '联觉信标功能[送]代'), ], 'en': [ # (KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Food_Improvement_Plan.name, @@ -156,10 +157,13 @@ class AssignmentUI(UI): self.goto_group(KEYWORDS_ASSIGNMENT_GROUP.Character_Materials) """ if ASSIGNMENT_GROUP_SWITCH.get(self) == group: + if not ASSIGNMENT_ENTRY_LIST.cur_buttons: + ASSIGNMENT_ENTRY_LIST.load_rows(self) return logger.hr('Assignment group goto', level=3) if ASSIGNMENT_GROUP_SWITCH.set(group, self): self._wait_until_entry_loaded() + self._wait_until_correct_entry_loaded(group) def goto_entry(self, entry: AssignmentEntry, insight: bool = True): """ @@ -213,11 +217,38 @@ class AssignmentUI(UI): if timeout.reached(): logger.warning('Wait entry loaded timeout') break + if self.appear(EVENT_FINISHED): + logger.info('Event finished') + break if self.appear(ASSIGNMENT_CHECK) and \ self.image_color_count(ENTRY_LOADED, (35, 35, 35), count=800): logger.info('Entry loaded') break + def _wait_until_correct_entry_loaded(self, group: AssignmentGroup): + skip_first_screenshot = True + timeout = Timer(3, count=3).start() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if timeout.reached(): + logger.warning('Wait correct entry loaded timeout') + break + if self.appear(EVENT_FINISHED): + logger.info('Event finished') + break + + ASSIGNMENT_ENTRY_LIST.load_rows(self) + if all( + x.matched_keyword.group == group + for x in ASSIGNMENT_ENTRY_LIST.cur_buttons + ): + logger.info('Correct entry loaded') + break + @property def _limit_status(self) -> tuple[int, int, int]: self.device.screenshot() @@ -279,7 +310,7 @@ class AssignmentUI(UI): """ Iterate entries from top to bottom """ - ASSIGNMENT_ENTRY_LIST.load_rows(main=self) + # load_rows is done in goto_group already # Freeze ocr results here yield from [ button.matched_keyword diff --git a/tasks/character/keywords/__init__.py b/tasks/character/keywords/__init__.py index 346ed08ad..812f10b3e 100644 --- a/tasks/character/keywords/__init__.py +++ b/tasks/character/keywords/__init__.py @@ -11,6 +11,8 @@ DICT_SORTED_RANGES = { Welt, Aventurine, FuXuan, + # Slow bullet + Robin, # Longer precast BlackSwan, ], diff --git a/tasks/combat/combat.py b/tasks/combat/combat.py index bffed1a96..ee5918ccf 100644 --- a/tasks/combat/combat.py +++ b/tasks/combat/combat.py @@ -91,6 +91,8 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo support_set = False else: support_set = True + # Reset combat_wave_cost, so handle_combat_interact() won't activate before handle_combat_prepare() + self.combat_wave_cost = 10 logger.info([support_character, support_set]) trial = 0 while 1: diff --git a/tasks/map/control/control.py b/tasks/map/control/control.py index bf99c8302..844bc8788 100644 --- a/tasks/map/control/control.py +++ b/tasks/map/control/control.py @@ -135,6 +135,7 @@ class MapControl(Combat, AimDetectorMixin): logger.info(f'Goto {waypoint}') self.screenshot_tracking_add() self.waypoint = waypoint + waypoint.unexpected_confirm.reset() self.device.stuck_record_clear() self.device.click_record_clear() @@ -176,6 +177,12 @@ class MapControl(Combat, AimDetectorMixin): if waypoint.early_stop: return result if self.walk_additional(): + # Clearing items may trigger additional popups + if attacked_item.started() and attacked_item.reached(): + logger.info('Walk result add: item') + result.append('item') + if 'item' in waypoint.expected_end and waypoint.early_stop: + return result attacked_enemy.clear() attacked_item.clear() continue @@ -227,7 +234,7 @@ class MapControl(Combat, AimDetectorMixin): if attacked_item.started() and attacked_item.reached(): logger.info('Walk result add: item') result.append('item') - if waypoint.early_stop: + if 'item' in waypoint.expected_end and waypoint.early_stop: return result if waypoint.interact_radius > 0: if diff < waypoint.interact_radius: diff --git a/tasks/rogue/entry/entry.py b/tasks/rogue/entry/entry.py index 448e33888..acba8caf1 100644 --- a/tasks/rogue/entry/entry.py +++ b/tasks/rogue/entry/entry.py @@ -164,6 +164,11 @@ class RogueEntry(RouteBase, RogueRewardHandler, RoguePathHandler, DungeonUI): if interval.reached() and self.is_page_rogue_main(): self.device.click(THEME_SWITCH) interval.reset() + # Weekly refresh popup + if self.appear_then_click(REWARD_CLOSE, interval=2): + continue + if self.handle_reward(): + continue def _rogue_world_set(self, world: int | DungeonList, skip_first_screenshot=True): """ diff --git a/tasks/rogue/route/base.py b/tasks/rogue/route/base.py index 859c6e7d8..548d1deec 100644 --- a/tasks/rogue/route/base.py +++ b/tasks/rogue/route/base.py @@ -20,6 +20,10 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent, RogueReward): enroute_add_item = True def combat_expected_end(self): + # Curio effect, that drops curio after combat + if self.handle_blessing_popup(): + return False + # Blessings after combat if self.is_page_choose_blessing(): logger.info('Combat ended at is_page_choose_blessing()') return True diff --git a/tasks/rogue/route/loader.py b/tasks/rogue/route/loader.py index ecf78455c..927000031 100644 --- a/tasks/rogue/route/loader.py +++ b/tasks/rogue/route/loader.py @@ -4,7 +4,10 @@ import numpy as np from module.base.decorator import cached_property from module.base.timer import Timer -from module.logger import logger +from module.exception import GameStuckError, HandledError +from module.logger import logger, save_error_log +from tasks.base.assets.assets_base_main_page import ROGUE_LEAVE_FOR_NOW +from tasks.base.assets.assets_base_page import MAP_EXIT from tasks.character.switch import CharacterSwitch from tasks.map.keywords import MapPlane from tasks.map.keywords.plane import ( @@ -17,6 +20,8 @@ from tasks.map.keywords.plane import ( from tasks.map.minimap.minimap import Minimap from tasks.map.resource.resource import SPECIAL_PLANES from tasks.map.route.loader import RouteLoader as RouteLoader_ +from tasks.rogue.assets.assets_rogue_ui import BLESSING_CONFIRM +from tasks.rogue.assets.assets_rogue_weekly import ROGUE_REPORT from tasks.rogue.blessing.ui import RogueUI from tasks.rogue.route.base import RouteBase from tasks.rogue.route.model import RogueRouteListModel, RogueRouteModel @@ -75,7 +80,7 @@ class MinimapWrapper: return self.all_minimap[route.plane_floor] -class RouteLoader(RogueUI, MinimapWrapper, RouteLoader_, CharacterSwitch): +class RouteLoader(RouteBase, MinimapWrapper, RouteLoader_, CharacterSwitch): def position_find_known(self, image, force_return=False) -> Optional[RogueRouteModel]: """ Try to find from known route spawn point @@ -271,6 +276,43 @@ class RouteLoader(RogueUI, MinimapWrapper, RouteLoader_, CharacterSwitch): if route is not None: return route + def rogue_leave(self, skip_first_screenshot=True): + logger.hr('Rogue leave', level=1) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if self.is_page_rogue_main(): + logger.info('Rogue left') + break + + # Re-enter + if self.handle_combat_interact(): + continue + # From ui_leave_special + if self.is_in_map_exit(interval=2): + self.device.click(MAP_EXIT) + continue + if self.handle_popup_confirm(): + continue + if self.appear_then_click(ROGUE_LEAVE_FOR_NOW, interval=2): + continue + # Blessing + if self.handle_blessing(): + continue + # _domain_exit_wait_next() + if self.match_template_color(ROGUE_REPORT, interval=2): + logger.info(f'{ROGUE_REPORT} -> {BLESSING_CONFIRM}') + self.device.click(BLESSING_CONFIRM) + continue + if self.handle_reward(): + continue + if self.handle_get_character(): + continue + def route_run(self, route=None): """ Run a rogue domain @@ -286,7 +328,15 @@ class RouteLoader(RogueUI, MinimapWrapper, RouteLoader_, CharacterSwitch): # To have a newer image, since previous loadings took some time route = self.position_find(skip_first_screenshot=False) self.screenshot_tracking_add() - super().route_run(route) + + try: + super().route_run(route) + return True + except GameStuckError as e: + logger.error(e) + save_error_log(config=self.config, device=self.device) + self.rogue_leave() + raise HandledError('Rogue run failed') def rogue_run(self, skip_first_screenshot=True): """