def _tactical_books_choose(self): """ Choose tactical book according to config. """ books = BookGroup([ Book(self.device.image, button) for button in BOOKS_GRID.buttons() ]).select(valid=True) logger.attr('Book_count', len(books)) for index in range(1, 4): logger.info(f'Book_T{index}: {books.select(tier=index)}') if not books: logger.warning('No book found.') raise ScriptError('No book found.') # if not time_range_active(self.config.TACTICAL_NIGHT_RANGE): # tier = self.config.TACTICAL_BOOK_TIER # exp = self.config.TACTICAL_EXP_FIRST # else: # tier = self.config.TACTICAL_BOOK_TIER_NIGHT # exp = self.config.TACTICAL_EXP_FIRST_NIGHT # book = books.choose(tier=tier, exp=exp) book = books.choose(tier_max=self.config.TACTICAL_BOOK_TIER_MAX, tier_min=self.config.TACTICAL_BOOK_TIER_MIN, exp=self.config.TACTICAL_EXP_FIRST) self.device.click(book.button) self.device.sleep((0.3, 0.5))
def name_to_zone(self, name): """ Args: name (str, int, Zone): Name in CN/EN/JP, zone id, or Zone instance. Returns: Zone: """ if isinstance(name, Zone): return name elif isinstance(name, int): return self.zones.select(zone_id=name)[0] elif isinstance(name, str) and name.isdigit(): return self.zones.select(zone_id=int(name))[0] else: def parse_name(n): n = str(n).replace(' ', '').lower() return n name = parse_name(name) for zone in self.zones: if name == parse_name(zone.cn) or name == parse_name( zone.en) or name == parse_name(zone.jp): return zone logger.warning(f'Unable to find OS globe zone: {name}') raise ScriptError(f'Unable to find OS globe zone: {name}')
def name_to_zone(self, name): """ Convert a name from various format to zone instance. Args: name (str, int, Zone): Name in CN/EN/JP/TW, zone id, or Zone instance. Returns: Zone: Raises: ScriptError: If Unable to find such zone. """ if isinstance(name, Zone): return name elif isinstance(name, int): return self.zones.select(zone_id=name)[0] elif isinstance(name, str) and name.isdigit(): return self.zones.select(zone_id=int(name))[0] else: def parse_name(n): n = str(n).replace(' ', '').lower() return n name = parse_name(name) for zone in self.zones: if name == parse_name(zone.cn): return zone if name == parse_name(zone.en): return zone if name == parse_name(zone.jp): return zone if name == parse_name(zone.tw): return zone raise ScriptError(f'Unable to find OS globe zone: {name}')
def predict_enemy_genre(self): image_dic = {} scaling_dic = self.config.MAP_ENEMY_GENRE_DETECTION_SCALING for name, template in self.template_enemy_genre.items(): if template is None: logger.warning(f'Enemy detection template not found: {name}') logger.warning( 'Please create it with dev_tools/relative_record.py or dev_tools/relative_crop.py, ' 'then place it under ./assets/<server>/template') raise ScriptError( f'Enemy detection template not found: {name}') short_name = name[6:] if name.startswith('Siren_') else name scaling = scaling_dic.get(short_name, 1) scaling = ( scaling, ) if not isinstance(scaling, tuple) else scaling for scale in scaling: if scale not in image_dic: shape = tuple( np.round(np.array((60, 60)) * scale).astype(int)) image_dic[scale] = rgb2gray( self.relative_crop((-0.5, -1, 0.5, 0), shape=shape)) if template.match( image_dic[scale], similarity=self.config.MAP_ENEMY_GENRE_SIMILARITY): return name return None
def _tactical_books_get(self): """ Get books. Handle loadings, wait 10 times at max. When TACTICAL_CLASS_START appears, game may stuck in loading, wait and retry detection. If loading still exists, raise ScriptError. Returns: BookGroup: Pages: in: TACTICAL_CLASS_START out: TACTICAL_CLASS_START """ for n in range(10): self.device.screenshot() self.handle_info_bar() # info_bar appears when get ship in Launch Ceremony commissions books = BookGroup([Book(self.device.image, button) for button in BOOKS_GRID.buttons()]).select(valid=True) logger.attr('Book_count', len(books)) for index in range(1, 4): logger.info(f'Book_T{index}: {books.select(tier=index)}') # End if books: return books else: self.device.sleep(3) continue logger.warning('No book found.') raise ScriptError('No book found, after 10 attempts.')
def execute_a_battle(self): logger.hr(f'{self.FUNCTION_NAME_BASE}{self.battle_count}', level=2) prev = self.battle_count result = False for _ in range(10): try: result = self.battle_function() break except MapEnemyMoved: if self.battle_count > prev: result = True break else: continue if not result: logger.warning('ScriptError, No combat executed.') if self.config.ENABLE_EXCEPTION: raise ScriptError('No combat executed.') else: logger.warning( 'ScriptError, Withdrawing because enable_exception = no') self.withdraw() return result
def __load_screenshot(self, screenshot, method): if method == 0: pass elif method == 1: screenshot = screenshot.replace(b'\r\n', b'\n') elif method == 2: screenshot = screenshot.replace(b'\r\r\n', b'\n') else: raise ScriptError(f'Unknown method to load screenshots: {method}') raw_compressed_data = self._ascreencap_reposition_byte_pointer(screenshot) # See headers in: # https://github.com/ClnViewer/Android-fast-screen-capture#streamimage-compressed---header-format-using compressed_data_header = np.frombuffer(raw_compressed_data[0:20], dtype=np.uint32) if compressed_data_header[0] != 828001602: compressed_data_header = compressed_data_header.byteswap() if compressed_data_header[0] != 828001602: text = f'aScreenCap header verification failure, corrupted image received. ' \ f'HEADER IN HEX = {compressed_data_header.tobytes().hex()}' logger.warning(text) raise AscreencapError(text) _, uncompressed_size, _, width, height = compressed_data_header channel = 3 data = lz4.block.decompress(raw_compressed_data[20:], uncompressed_size=uncompressed_size) image = np.frombuffer(data, dtype=np.uint8) # Equivalent to cv2.imdecode() shape = image.shape[0] image = image[shape - width * height * channel:].reshape(height, width, channel) image = cv2.flip(image, 0) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) return image
def screenshot_interval_set(self, interval=None): """ Args: interval (int, float, str): Minimum interval between 2 screenshots in seconds. Or None for Optimization_ScreenshotInterval, 'combat' for Optimization_CombatScreenshotInterval """ if interval is None: origin = self.config.Optimization_ScreenshotInterval interval = limit_in(origin, 0.1, 0.3) if interval != origin: logger.warning( f'Optimization.ScreenshotInterval {origin} is revised to {interval}' ) self.config.Optimization_ScreenshotInterval = interval elif interval == 'combat': origin = self.config.Optimization_CombatScreenshotInterval interval = limit_in(origin, 0.3, 1.0) if interval != origin: logger.warning( f'Optimization.CombatScreenshotInterval {origin} is revised to {interval}' ) self.config.Optimization_CombatScreenshotInterval = interval elif isinstance(interval, (int, float)): # No limitation for manual set in code pass else: logger.warning(f'Unknown screenshot interval: {interval}') raise ScriptError(f'Unknown screenshot interval: {interval}') if interval != self._screenshot_interval.limit: logger.info(f'Screenshot interval set to {interval}s') self._screenshot_interval.limit = interval
def execute_a_battle(self): func = self.FUNCTION_NAME_BASE + 'default' for extra_battle in range(10): if hasattr( self, self.FUNCTION_NAME_BASE + str(self.battle_count - extra_battle)): func = self.FUNCTION_NAME_BASE + str(self.battle_count - extra_battle) break logger.hr(f'{self.FUNCTION_NAME_BASE}{self.battle_count}', level=2) logger.info(f'Using function: {func}') func = self.__getattribute__(func) result = func() if not result: logger.warning('ScriptError, No combat executed.') if self.config.ENABLE_EXCEPTION: raise ScriptError('No combat executed.') else: logger.warning( 'ScriptError, Withdrawing because enable_exception = no') self.withdraw() return result
def _handle_orientated_image(self, image): """ Args: image (np.ndarray): Returns: np.ndarray: """ width, height = image_size(self.image) if width == 1280 and height == 720: return image # Rotate screenshots only when they're not 1280x720 if self.orientation == 0: pass elif self.orientation == 1: image = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE) elif self.orientation == 2: image = cv2.rotate(image, cv2.ROTATE_180) elif self.orientation == 3: image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) else: raise ScriptError( f'Invalid device orientation: {self.orientation}') return image
def task_call(self, task, force_call=True): """ Call another task to run. That task will run when current task finished. But it might not be run because: - Other tasks should run first according to SCHEDULER_PRIORITY - Task is disabled by user Args: task (str): Task name to call, such as `Restart` force_call (bool): Returns: bool: If called. """ if deep_get(self.data, keys=f"{task}.Scheduler.NextRun", default=None) is None: raise ScriptError( f"Task to call: `{task}` does not exist in user config") if force_call or deep_get( self.data, keys=f"{task}.Scheduler.Enable", default=False): logger.info(f"Task call: {task}") self.modified[f"{task}.Scheduler.NextRun"] = datetime.now( ).replace(microsecond=0) self.modified[f"{task}.Scheduler.Enable"] = True self.update() return True else: logger.info( f"Task call: {task} (skipped because disabled by user)") return False
def run(self, name='', mode='', total=0): """ Args: name (str): Raid name, such as 'raid_20200624' mode (str): Raid mode, such as 'hard', 'normal', 'easy' total (int): Total run count """ name = name if name else self.config.Campaign_Event mode = mode if mode else self.config.Raid_Mode if not name or not mode: raise ScriptError( f'RaidRun arguments unfilled. name={name}, mode={mode}') self.run_count = 0 self.run_limit = self.config.StopCondition_RunCount while 1: # End if total and self.run_count == total: break if self.event_time_limit_triggered(): self.config.task_stop() # Log logger.hr(f'{name}_{mode}', level=2) if self.config.StopCondition_RunCount > 0: logger.info( f'Count remain: {self.config.StopCondition_RunCount}') else: logger.info(f'Count: {self.run_count}') # End if self.triggered_stop_condition(): break # UI ensure self.ui_ensure(page_raid) # Run try: self.raid_execute_once(mode=mode, raid=name) except OilExhausted: logger.hr('Triggered stop condition: Oil limit') self.config.task_delay(minute=(120, 240)) break except ScriptEnd as e: logger.hr('Script end') logger.info(str(e)) break # After run self.run_count += 1 if self.config.StopCondition_RunCount: self.config.StopCondition_RunCount -= 1 # End if self.triggered_stop_condition(): break # Scheduler if self.config.task_switched(): self.config.task_stop()
def find_tesseract_tool(self): """ Returns: pyocr tesseract wrapper module """ ocr_tool = pyocr.get_available_tools() if len(ocr_tool) == 0: raise ScriptError( 'No ocr-tool found, please install tesseract by yourself and make sure to set correct env vars.' ) ocr_tool = ocr_tool[0] ocr_langs = ocr_tool.get_available_languages() if 'jpn' not in ocr_langs: raise ScriptError( 'No jpn found in tesseract langs, please install japanese data files.' ) return ocr_tool
def dock_filter_set(self, category, type, enable): key = f'filter_{category}_{type}' try: obj = globals()[key] obj.set('on' if enable else 'off', main=self) except KeyError: raise ScriptError(f'{key} filter switch object does not exist in module/retire/dock.py')
def globe_goto(self, zone, types=('SAFE', 'DANGEROUS'), refresh=False, stop_if_safe=False): """ Goto another zone in OS. Args: zone (str, int, Zone): Name in CN/EN/JP/TW, zone id, or Zone instance. types (tuple[str], list[str], str): Zone types, or a list of them. Available types: DANGEROUS, SAFE, OBSCURE, ABYSSAL, STRONGHOLD. Try the the first selection in type list, if not available, try the next one. refresh (bool): If already at target zone, set false to skip zone switching, set true to re-enter current zone to refresh. stop_if_safe (bool): Return false if zone is SAFE. Returns: bool: If zone switched. Pages: in: IN_MAP or IN_GLOBE out: IN_MAP """ zone = self.name_to_zone(zone) logger.hr(f'Globe goto: {zone}') if self.zone == zone: if refresh: logger.info('Goto another zone to refresh current zone') return self.globe_goto(self.zone_nearest_azur_port(self.zone), types=('SAFE', 'DANGEROUS'), refresh=False) else: logger.info('Already at target zone') return False # MAP_EXIT if self.is_in_special_zone(): self.map_exit() # IN_MAP if self.is_in_map(): self.os_map_goto_globe() # IN_GLOBE if not self.is_in_globe(): logger.warning('Trying to move in globe, but not in os globe map') raise ScriptError('Trying to move in globe, but not in os globe map') # self.ensure_no_zone_pinned() self.globe_update() self.globe_focus_to(zone) if stop_if_safe: if self.zone_has_safe(): logger.info('Zone is safe, stopped') self.ensure_no_zone_pinned() return False self.zone_type_select(types=types) self.globe_enter(zone) # IN_MAP if hasattr(self, 'zone'): del self.zone self.zone_init() # self.map_init() return True
def _load_screenshot(self, screenshot, method): if method == 0: return Image.open(BytesIO(screenshot)).convert('RGB') elif method == 1: return Image.open(BytesIO(screenshot.replace(b'\r\n', b'\n'))).convert('RGB') elif method == 2: return Image.open(BytesIO(screenshot.replace(b'\r\r\n', b'\n'))).convert('RGB') else: raise ScriptError(f'Unknown method to load screenshots: {method}')
def __load_screenshot(self, screenshot, method): if method == 0: return screenshot elif method == 1: return screenshot.replace(b'\r\n', b'\n') elif method == 2: return screenshot.replace(b'\r\r\n', b'\n') else: raise ScriptError(f'Unknown method to load screenshots: {method}')
def task_delay(self, success=None, server_update=None, target=None, minute=None): """ Set Scheduler.NextRun Should set at least one arguments. If multiple arguments are set, use the nearest. Args: success (bool): If True, delay Scheduler.SuccessInterval If False, delay Scheduler.FailureInterval server_update (bool, list, str): If True, delay to nearest Scheduler.ServerUpdate If type is list or str, delay to such server update target (datetime.datetime, str, list): Delay to such time. minute (int, float, tuple): Delay several minutes. """ def ensure_delta(delay): return timedelta(seconds=int(ensure_time(delay, precision=3) * 60)) run = [] if success is not None: interval = (self.Scheduler_SuccessInterval if success else self.Scheduler_FailureInterval) run.append(datetime.now() + ensure_delta(interval)) if server_update is not None: if server_update is True: server_update = self.Scheduler_ServerUpdate run.append(get_server_next_update(server_update)) if target is not None: target = [target] if not isinstance(target, list) else target target = nearest_future(target) run.append(target) if minute is not None: run.append(datetime.now() + ensure_delta(minute)) if len(run): run = min(run).replace(microsecond=0) kv = dict_to_kv( { "success": success, "server_update": server_update, "target": target, "minute": minute, }, allow_none=False, ) logger.info(f"Delay task `{self.task.command}` to {run} ({kv})") self.Scheduler_NextRun = run else: raise ScriptError( "Missing argument in delay_next_run, should set at least one")
def _api(self): method = self.config.DropRecord_API if method == 'default': return 'https://azurstats.lyoko.io/api/upload/' elif method == 'cn_gz_reverse_proxy': return 'https://service-rjfzwz8i-1301182309.gz.apigw.tencentcs.com/api/upload' elif method == 'cn_sh_reverse_proxy': return 'https://service-nlvjetab-1301182309.sh.apigw.tencentcs.com/api/upload' else: logger.critical('Invalid upload API, please check your settings') raise ScriptError('Invalid upload API')
def _storage_item_to_template(item): """ Args: item (str): 'OBSCURE' or 'ABYSSAL'. Returns: Template: """ if item == 'OBSCURE': return TEMPLATE_STORAGE_OBSCURE elif item == 'ABYSSAL': return TEMPLATE_STORAGE_ABYSSAL else: raise ScriptError(f'Unknown storage item: {item}')
def raid_name_shorten(name): """ Args: name (str): Raid name, such as raid_20200624, raid_20210708. Returns: str: Prefix of button name, such as ESSEX, SURUGA. """ if name == 'raid_20200624': return 'ESSEX' elif name == 'raid_20210708': return 'SURUGA' else: raise ScriptError(f'Unknown raid name: {name}')
def raid_entrance(raid, mode): """ Args: raid (str): Raid name, such as raid_20200624, raid_20210708. mode (str): easy, normal, hard Returns: Button: """ key = f'{raid_name_shorten(raid)}_RAID_{mode.upper()}' try: return globals()[key] except KeyError: raise ScriptError(f'Raid entrance asset not exists: {key}')
def get_current_zone(self): """ Returns: Zone: """ if not self.is_in_map(): logger.warning('Trying to get zone name, but not in OS map') raise ScriptError('Trying to get zone name, but not in OS map') name = self.get_zone_name() logger.info(f'Map name processed: {name}') self.zone = self.name_to_zone(name) logger.attr('Zone', self.zone) return self.zone
def zone_select(self, hazard_level): """ Similar to `self.zone.select(**kwargs)`, but delete zones in region 5. Args: hazard_level: 1-6, or 10 for center zones. Returns: SelectedGrids: SelectedGrids containing zone objects. """ if 1 <= hazard_level <= 6: return self.zones.select(hazard_level=hazard_level).delete(self.zones.select(region=5)) elif hazard_level == 10: return self.zones.select(region=5) else: raise ScriptError(f'Invalid hazard_level of zones: {hazard_level}')
def click_record_check(self, button): """ Args: button (button.Button): AzurLane Button instance. Returns: bool: """ if sum([1 if str(prev) == str(button) else 0 for prev in self.click_record]) >= 12: logger.warning(f'Too many click for a button: {button}') logger.info(f'History click: {[str(prev) for prev in self.click_record]}') raise ScriptError(f'Too many click for a button: {button}') else: self.click_record.append(str(button)) return False
def _retire_handler(self, mode=None): """ Args: mode (str): `one_click_retire` or `old_retire` Returns: int: Amount of retired ships Pages: in: IN_RETIREMENT_CHECK out: the page before retirement popup """ if mode is None: mode = self.config.Retirement_RetireMode if mode == 'one_click_retire': total = self.retire_ships_one_click() if not total: logger.warning( 'No ship retired, trying to reset dock filter and disable favourite, then retire again' ) self.dock_filter_set() self.dock_favourite_set(False) total = self.retire_ships_one_click() if not total: logger.critical('No ship retired') logger.critical( 'Please configure your one-click-retire in game, ' 'make sure it can select ships to retire') raise RequestHumanTakeover elif mode == 'old_retire': self.handle_dock_cards_loading() total = self.retire_ships_old() if not total: logger.critical('No ship retired') logger.critical( 'Please configure your retirement settings in Alas, ' 'make sure it can select ships to retire') raise RequestHumanTakeover else: raise ScriptError( f'Unknown retire mode: {self.config.Retirement_RetireMode}') self._retirement_quit() self.config.DOCK_FULL_TRIGGERED = True return total
def get_data(self, status): """ Args: status (str): Returns: dict: Dictionary in add_status Raises: ScriptError: If status invalid """ for row in self.status_list: if row['status'] == status: return row logger.warning(f'Switch {self.name} received an invalid status {status}') raise ScriptError(f'Switch {self.name} received an invalid status {status}')
def _tactical_books_get(self, skip_first_screenshot=True): """ Get books. Handle loadings, wait 10 times at max. When TACTICAL_CLASS_START appears, game may stuck in loading, wait and retry detection. If loading still exists, raise ScriptError. Returns: BookGroup: Pages: in: TACTICAL_CLASS_START out: TACTICAL_CLASS_START """ prev = SelectedGrids([]) for n in range(1, 16): if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() self.handle_info_bar( ) # info_bar appears when get ship in Launch Ceremony commissions if not self.appear(TACTICAL_CLASS_START, offset=(30, 30)): logger.info('Not in TACTICAL_CLASS_START anymore, exit') return False books = SelectedGrids([ Book(self.device.image, button) for button in BOOKS_GRID.buttons ]).select(valid=True) self.books = books logger.attr('Book_count', books.count) logger.attr('Books', str(books)) # End if books and books.count == prev.count: return books else: prev = books if n % 3 == 0: self.device.sleep(3) continue logger.warning('No book found.') raise ScriptError('No book found, after 15 attempts.')
def run(self): logger.hr(self.ENTRANCE, level=2) self.handle_spare_fleet() self.ENTRANCE.area = self.ENTRANCE.button self.enter_map(self.ENTRANCE, mode=self.config.CAMPAIGN_MODE) self.handle_map_fleet_lock() self.handle_fleet_reverse() self.map_init(self.MAP) for _ in range(20): try: self.execute_a_battle() except CampaignEnd: logger.hr('Campaign end') return True logger.warning('Battle function exhausted.') raise ScriptError('Battle function exhausted.')
def raid_ocr(raid, mode): """ Args: raid (str): Raid name, such as raid_20200624, raid_20210708. mode (str): easy, normal, hard Returns: RaidCounter: """ raid = raid_name_shorten(raid) key = f'{raid}_OCR_REMAIN_{mode.upper()}' try: button = globals()[key] if raid == 'ESSEX': return RaidCounter(button, letter=(57, 52, 255), threshold=128) elif raid == 'SURUGA': return RaidCounter(button, letter=(49, 48, 49), threshold=128) except KeyError: raise ScriptError(f'Raid entrance asset not exists: {key}')