def switch_fleet(self, context): preset_id = self._get_next_preset_id(context) Log.log_msg(f"Switching to Fleet Preset {preset_id}.") if preset_id not in self.presets: Log.log_error( f"Fleet Preset {preset_id} is not specified in-game. Please " f"check your config.") exit(1) list_idx = (preset_id if preset_id < 5 else 5) - 1 idx_offset = preset_id - 5 if idx_offset > 0: kca_u.kca.sleep() self._scroll_preset_list(idx_offset) kca_u.kca.r['top'].hover() kca_u.kca.sleep() preset_idx_region = Region(kca_u.kca.game_x + 410, kca_u.kca.game_y + 275 + (list_idx * 76), 70, 45) kca_u.kca.click_existing(preset_idx_region, 'fleetswitcher|fleetswitch_button.png') if kca_u.kca.exists('left', 'fleetswitcher|fleetswitch_fail_check.png'): Log.log_error( f"Could not switch in fleet preset {preset_id}. Please check " f"your config and fleet presets.") exit(1) Log.log_msg(f"Fleet Preset {preset_id} loaded.") if context == 'combat': self._set_next_combat_preset()
def navigate_to(self, target, api_update=True): """Method that interacts with the game to transition from the current node to the destination via the pre-defined connections. Args: target (str): name of destination node Returns: NavNode: NavNode instance of the destination """ if target in self.connections: c = self.connections[target] kca_u.kca.hover('top') if c['click_target'] == 'QUEST_MENU': kca_u.kca.click('top_menu_quest') else: kca_u.kca.wait_and_click( kca_u.kca.r[c['click_target_region']], c['click_target']) if api_update: api.api.update_from_api({c['api_target']}) kca_u.kca.hover('top') kca_u.kca.sleep() kca_u.kca.wait( kca_u.kca.r[c['wait_target_region']], c['wait_target'], 20, NEAR_EXACT) return c['target'] else: Log.log_error( f"Not possible to navigate to '{target}' from {self.name} " "screen.")
def update_ship_library_from_json(self): try: ship_data = JsonData.load_json('data|temp|get_data_ship.json') shp.ships.update_ship_library(ship_data) except FileNotFoundError as e: Log.log_error( "get_data_ship.json not found. Please run kcauto from the " "Kancolle splash screen to download data.") Log.log_error(e) sys.exit(1)
def __init__(self): Log.log_success("Initializing kcauto.") if arg.args.parsed_args.cfg_path: self.cfg_path = arg.args.parsed_args.cfg_path else: cfg_file = arg.args.parsed_args.cfg self.cfg_path = JsonData.create_path(f'configs|{cfg_file}.json') self.initialize_config() if not self.general: Log.log_error("Error loading config.") exit(1)
def recovery_from_catbomb(cls, screen=Region(), catbomb_201=False): """Method that contains steps for recovery attempt from catbombs. If the catbomb_201 flag is set to True the recovery is attempt is far less aggressive to not aggravate the fairies any further. Includes incremental fallback for retry attempts. Args: screen (Region, optional): screen region. Defaults to Region(). catbomb_201 (bool, optional): whether or not this is a 201 catbomb. Defaults to False. Returns: bool: True if recovery was successful; False otherwise. """ catbomb_count = 0 if not catbomb_201 else 2 if catbomb_201: if sts.stats.recovery.catbomb_201_encountered > 0: Log.log_error( "Multiple 201 catbombs encountered. Shutting down kcauto.") return False else: kca_u.kca.sleep(300) while catbomb_count < 3: cls._refresh_screen(screen) if kca_u.kca.start_kancolle(): Log.log_success("Catbomb Recovery successful.") kca_u.kca.hook_chrome(port=cfg.config.general.chrome_dev_port) sts.stats.recovery.recoveries_done += 1 if catbomb_201: sts.stats.recovery.catbomb_201_encountered += 1 sts.stats.recovery.catbomb_201_recoveries_done += 1 else: sts.stats.recovery.catbomb_recoveries_done += 1 return True elif kca_u.kca.exists(screen, 'global|catbomb.png'): if catbomb_201: Log.log_error( "Persistent 201 catbomb. Shutting down kcauto.") return False catbomb_count += 1 # incremental backoff; 16, 64, then 256 seconds sleep_len = pow(4, catbomb_count + 1) Log.log_warn( f"Catbomb Recovery attempt {catbomb_count}. Sleeping for " f"{sleep_len} seconds before next recovery attempt.") sleep(sleep_len) else: return False return False
def hook_chrome(self, host="localhost", port=9222): """Method that initializes the necessary hooks to Chrome using PyChromeDevTools. The visual hook connects to the tab that actually contains the Kancolle HTML5 canvas, while the api hook connects to the tab that interacts with the Kancolle backend. The former is used for detecting refreshes and when using the Chrome driver interaction mode, while the latter is used for reading all API interactions. Args: host (str, optional): Chrome dev protocol server address. Defaults to "localhost". port (int, optional): Chrome dev protocol server port. Defaults to 9222. Raises: Exception: could not find Kancolle tabs in Chrome. """ Log.log_msg("Hooking into Chrome.") self.visual_hook = PyChromeDevTools.ChromeInterface(host=host, port=port) self.api_hook = PyChromeDevTools.ChromeInterface(host=host, port=port) visual_tab = None visual_tab_id = None api_tab = None api_tab_id = None for n, tab in enumerate(self.visual_hook.tabs): if tab['url'] == VISUAL_URL: visual_tab = n visual_tab_id = tab['id'] if API_URL in tab['url']: api_tab = n api_tab_id = tab['id'] if visual_tab_id is None or api_tab_id is None: Log.log_error( "No Kantai Collection tab found in Chrome. Shutting down " "kcauto.") raise Exception( "No running Kantai Collection tab found in Chrome.") self.visual_hook.connect_targetID(visual_tab_id) Log.log_debug( f"Connected to visual tab ({visual_tab}:{visual_tab_id})") self.visual_hook.Page.enable() self.api_hook.connect_targetID(api_tab_id) self.api_hook.Network.enable() Log.log_debug(f"Connected to API tab ({api_tab}:{api_tab_id})") Log.log_success("Connected to Chrome")
def attempt_recovery(cls): """Primary method that runs through all the various recovery options. Runs through basic recovery, result screen recovery, catbomb recovery, then Chrome tab crash recoveries. Typically run when there has been a generic Exception caused. Returns: bool: True if recovery was successful; False otherwise. """ Log.log_warn("Attempting recovery.") screen = Region() if cls.basic_recovery(): Log.log_success("Basic Recovery successful.") sts.stats.recovery.recoveries_done += 1 sts.stats.recovery.basic_recoveries_done += 1 return True if ( kca_u.kca.exists(screen, 'global|next.png') or kca_u.kca.exists(screen, 'global|next_alt.png')): Log.log_warn("Results screen detected.") if cls.recovery_from_results(screen): Log.log_success("Results Recovery successful.") sts.stats.recovery.recoveries_done += 1 sts.stats.recovery.results_recoveries_done += 1 return True return False if kca_u.kca.exists(screen, 'global|catbomb.png'): Log.log_warn("Catbomb detected.") if cls.recovery_from_catbomb(screen=screen): return True else: Log.log_error("Catbomb Recovery failed.") return False if kca_u.kca.exists(screen, 'global|chrome_crash.png'): Log.log_warn("Chrome Crash (Type 1) detected.") if cls.recovery_from_chrome_crash(screen, crash_type=1): return True visual_events = kca_u.kca.visual_hook.pop_messages() for event in visual_events: if event['method'] == 'Inspector.targetCrashed': Log.log_warn("Chrome Crash (Type 2) detected.") if cls.recovery_from_chrome_crash(screen, crash_type=2): return True
def initialize_config(self): update = False config_json = self._load_cfg_json() initial_load = True new_update_time = os.path.getmtime(self.cfg_path) if self.general: initial_load = False if config_json != self.general._config: Log.log_msg("Changes detected from previous config load.") else: Log.log_debug("No change from previous config load.") self.last_cfg_update_time = new_update_time return False try: new_general = ConfigGeneral(config_json) new_expedition = ConfigExpedition(config_json) new_pvp = ConfigPvP(config_json) new_combat = ConfigCombat(config_json) new_event_reset = ConfigEventReset(config_json) new_ship_switcher = ConfigShipSwitcher(config_json) new_passive_repair = ConfigPassiveRepair(config_json) new_quest = ConfigQuest(config_json) new_scheduler = ConfigScheduler(config_json) update = True except Exception as e: Log.log_error(e) if update: Log.log_success("Config successfully loaded.") if not initial_load: Log.log_success("Hot-reloading config in 3...") sleep(1) Log.log_success("2...") sleep(1) Log.log_success("1...") sleep(1) self.general = new_general self.expedition = new_expedition self.pvp = new_pvp self.combat = new_combat self.event_reset = new_event_reset self.ship_switcher = new_ship_switcher self.passive_repair = new_passive_repair self.quest = new_quest self.scheduler = new_scheduler self.last_cfg_update_time = new_update_time return True
def recovery_from_chrome_crash(cls, screen=Region(), crash_type=1): """Method that contains steps for recovery from Chrome tab crashes. Args: screen (Region, optional): screen region. Defaults to Region(). crash_type (int, optional): type of Chrome tab crash encountered. Defaults to 1. Returns: bool: True if recovery was successful; False otherwise. """ cls._refresh_screen(screen) if kca_u.kca.start_kancolle(): Log.log_success("Chrome Crash Recovery successful.") sts.stats.recovery.recoveries_done += 1 if crash_type == 1: sts.stats.recovery.chrome_crash_t1_recoveries_done += 1 elif crash_type == 2: sts.stats.recovery.chrome_crash_t2_recoveries_done += 1 return True Log.log_error("Chrome Crash Recovery failed.") return False
def find_kancolle(self): """Method that finds the Kancolle game on-screen and determine the UI being used as well as the position of the game. On first startup the method will look for all UIs until one is found; on subsequent runs it will first look for the last found UI. Generates or modifies pre-defined regions accordingly. Raises: FindFailed: could not find the game on-screen. """ Log.log_msg("Finding kancolle.") ref_r = None attempt = 0 screen = Region() # look for last-seen UI, if set if self.last_ui: try: ref_r = self.find(screen, f'global|kc_ref_point_{self.last_ui}.png', EXACT) except FindFailed: self.last_ui = None Log.log_debug("Last known UI not found.") # if last-seen UI was not found, or if kcauto is in first start while not ref_r: try: ref_r = self.find(screen, 'global|kc_ref_point_1.png', EXACT) self.last_ui = 1 Log.log_debug("Using UI 1 or 2") break except FindFailed: Log.log_debug("Not using UI 1 or 2") try: ref_r = self.find(screen, 'global|kc_ref_point_2.png', EXACT) self.last_ui = 2 Log.log_debug("Using UI 3") break except FindFailed: Log.log_debug("Not using UI 3") try: ref_r = self.find(screen, 'global|kc_ref_point_3.png', EXACT) self.last_ui = 3 Log.log_debug("Using UI 4 or 5") break except FindFailed: Log.log_debug("Not using UI 4 or 5") attempt += 1 sleep(1) if attempt > 3: Log.log_error("Could not find Kancolle reference point.") raise FindFailed() new_game_x = ref_r.x - 144 new_game_y = ref_r.y Log.log_debug(f"Game X:{new_game_x}, Y:{new_game_y}") # define click callback as needed if not arg.args.parsed_args.no_click_track: ImageMatch.click_callback = clt.click_tracker.track_click # define click and hover method overrides as needed if (cfg.config.general.interaction_mode is InteractionModeEnum.CHROME_DRIVER): ImageMatch.override_click_method = self._override_click_method ImageMatch.override_hover_method = self._override_hover_method if new_game_x != self.game_x or new_game_y != self.game_y: if not self.game_x or self.game_y: Log.log_success("Game found. Initializing regions.") else: Log.log_msg("Game has moved. Shifting regions.") self.game_x = new_game_x self.game_y = new_game_y self._update_regions() return True
def _load_api_data(self, request_data, data): request_type = request_data['type'] if data['api_result'] != 1: Log.log_debug("Encountered non-1 API result.") Log.log_debug(data) if data['api_result'] == 201: Log.log_error("Encountered catbomb.") raise Catbomb201Exception else: raise ApiException if request_type is KCSAPIEnum.GET_DATA: return self._process_get_data(data) elif request_type is KCSAPIEnum.REQUIRE_INFO: return self._process_require_info(data) elif request_type is KCSAPIEnum.PORT: return self._process_port(data) elif request_type is KCSAPIEnum.SORTIE_MAPS: return self._process_sortie_maps(data) elif request_type is KCSAPIEnum.SORTIE_START: return self._process_sortie_start(data) elif request_type is KCSAPIEnum.SORTIE_NEXT: return self._process_sortie_next(data) elif request_type is KCSAPIEnum.SORTIE_BATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_NIGHTBATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_AIRBATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_LD_AIRBATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_LD_SHOOTING: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_N2D: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_NIGHT_ONLY: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_ECF_BATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_ECF_NIGHTBATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_CF_BATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_CF_NIGHTBATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_CF_AIRBATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_CF_WATERBATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_CF_LD_AIRBATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_CF_LD_SHOOTING: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_CF_N2D: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_CF_NIGHT_ONLY: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_CF_EACH_NIGHT_ONLY: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_CF_ECF_BATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_CF_ECF_AIRBATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_CF_ECF_WATERBATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_CF_ECF_LD_AIRBATTLE: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_CF_ECF_LD_SHOOTING: return self._process_battle(data) elif request_type is KCSAPIEnum.SORTIE_RESULT: return self._process_battle_result(data) elif request_type is KCSAPIEnum.SORTIE_CF_RESULT: return self._process_battle_result(data) elif request_type is KCSAPIEnum.SORTIE_SHIPDECK: return self._process_battle_deck(data) elif request_type is KCSAPIEnum.EXPEDITION_LIST: return self._process_expedition_list(data) elif request_type is KCSAPIEnum.EXPEDITION_START: return self._process_expedition_start(data) elif request_type is KCSAPIEnum.PVP_LIST: return self._process_pvp_list(data) elif request_type is KCSAPIEnum.PVP_ENEMY_INFO: return self._process_pvp_enemy_info(data) elif request_type is KCSAPIEnum.FLEETCOMP_PRESETS: return self._process_fleetcomp_presets(data) elif request_type is KCSAPIEnum.REPAIR_DOCKS: return self._process_repair_dock_data(data) elif request_type is KCSAPIEnum.QUEST_LIST: return self._process_quest_data(data) return None
def to(cls, destination, max_sidestep=1): """Method to call to detect the current location and move to the specified destination, with or without sidesteps. Args: regions (dict): dict of pre-defined kcauto regions destination (str): name of the destination max_sidestep (int, optional): the max number of sidesteps to take; in the current implementation the name and type is a misnomer: if it is a non-zero number the code will sidestep once, otherwise never sidestep (should be renamed to 'sidestep' with bool type) Returns: bool: True if navigation was successful, False if no actions were taken """ if (kca_u.kca.exists('home_menu', 'nav|home_menu_sortie.png') and destination == 'home'): # if visibly at home menu and destination is home, not refresh # home, short circuit return False sidestep = bool(randint(0, max_sidestep)) kca_u.kca.hover('top') kca_u.kca.sleep() # Figure out where we are current_location = None if kca_u.kca.exists('home_menu', 'nav|home_menu_sortie.png'): Log.log_msg("At home") current_location = nodes.nav_nodes.home elif kca_u.kca.exists('side_menu', 'nav|side_menu_home.png'): Log.log_msg("At side menu") current_location = nodes.nav_nodes.side_menu elif kca_u.kca.exists('lower_left', 'nav|top_menu_home.png'): Log.log_msg("At top menu") current_location = nodes.nav_nodes.top_menu if not current_location: Log.log_error("Nav module could not figure out current location.") raise FindFailed() if current_location.name == 'home': # Starting from home screen if destination == 'home': # Already at home # Util.log_msg('Already at home.') return False elif destination == 'refresh_home': # Refresh home Log.log_msg("Refreshing home.") destination = 'home' current_location = current_location.navigate_to( cls._choose_sidestep(destination), False) else: # Go to and side menu sub screen Log.log_msg( "Navigating to {} screen.".format(destination)) if destination in ('combat', 'pvp', 'expedition'): current_location = current_location.navigate_to( 'sortie', False) kca_u.kca.sleep(1) else: if sidestep: current_location = current_location.navigate_to( cls._choose_sidestep(destination), False) elif current_location.name == 'side_menu': # Starting from a main menu item screen if destination in ('home', 'refresh_home'): # Go or refresh home Log.log_msg('Going home.') destination = 'home' elif current_location.name == 'top_menu': # Starting from top menu item. Theoretically, the script should # never attempt to go anywhere but home from here if destination in ('home', 'refresh_home'): Log.log_msg('Going home.') destination = 'home' current_location.navigate_to(destination) return True