diff --git a/alas.py b/alas.py index dd97241d8..3696d5451 100644 --- a/alas.py +++ b/alas.py @@ -587,6 +587,7 @@ class AzurLaneAutoScript: from module.luahook.crack import * +from module.scheduler_watcher import * class AzurLaneAutoScript(AzurLaneAutoScript): @@ -602,6 +603,8 @@ class AzurLaneAutoScript(AzurLaneAutoScript): elif self.class_name == "ArknightsAutoScript": self.is_ark = True + self.scheduler_watcher = None + full_config = self.config.full_config self.is_fatal_error_restart = full_config.Restart_GameRestart_FatalErrorRestart self.max_retry_times_for_same_task_failed = full_config.Restart_GameRestart_MaxRetryTimesForSameTaskFailed @@ -823,10 +826,73 @@ class AzurLaneAutoScript(AzurLaneAutoScript): def coalition_sp(self): super().coalition_sp() + def get_next_task(self): + while 1: + task = self.config.get_next() + self.config.task = task + self.config.bind(task) + + from module.base.resource import release_resources + if self.config.task.command != 'Alas': + release_resources(next_task=task.command) + + if task.next_run > datetime.now(): + self.scheduler_watcher.no_task() + logger.info(f'Wait until {task.next_run} for task `{task.command}`') + self.is_first_task = False + method = self.config.Optimization_WhenTaskQueueEmpty + if method == 'close_game': + logger.info('Close game during wait') + self.device.app_stop() + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + del_cached_property(self, 'config') + continue + if task.command != 'Restart': + self.config.task_call('Restart') + del_cached_property(self, 'config') + continue + elif method == 'goto_main': + logger.info('Goto main page during wait') + self.run('goto_main') + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + del_cached_property(self, 'config') + continue + elif method == 'stay_there': + logger.info('Stay there during wait') + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + del_cached_property(self, 'config') + continue + else: + logger.warning(f'Invalid Optimization_WhenTaskQueueEmpty: {method}, fallback to stay_there') + release_resources() + self.device.release_during_wait() + if not self.wait_until(task.next_run): + del_cached_property(self, 'config') + continue + break + + AzurLaneConfig.is_hoarding_task = False + return task.command + def loop(self): if self.is_azur: CrackResource(self.config, self.device).ensure() + if self.is_azur and self.config.full_config.Restart_SchedulerWatcher_Enable: + watcher = SchedulerWatcher.get_instance() + watcher.set_alas_obj(self) + watcher.start_watch( + min_timedelta=self.config.full_config.Restart_SchedulerWatcher_TimeDelta, + max_warning_count=self.config.full_config.Restart_SchedulerWatcher_WarningCount + ) + self.scheduler_watcher = watcher + logger.set_file_logger(self.config_name) logger.info(f'Start scheduler loop: {self.config_name}') @@ -861,6 +927,7 @@ class AzurLaneAutoScript(AzurLaneAutoScript): # Run logger.info(f'Scheduler: Start task `{task}`') + self.scheduler_watcher.switch_task(task) self.device.stuck_record_clear() self.device.click_record_clear() logger.hr(task, level=0) diff --git a/config/template.json b/config/template.json index db5688af8..f28fb0a2f 100644 --- a/config/template.json +++ b/config/template.json @@ -73,6 +73,11 @@ "FailureInterval": 0, "ServerUpdate": "00:00" }, + "SchedulerWatcher": { + "Enable": false, + "TimeDelta": 10, + "WarningCount": 3 + }, "GameRestart": { "FatalErrorRestart": false, "MaxRetryTimesForSameTaskFailed": 5 diff --git a/module/campaign/campaign_base.py b/module/campaign/campaign_base.py index 15ebf4783..65a92dab1 100644 --- a/module/campaign/campaign_base.py +++ b/module/campaign/campaign_base.py @@ -177,3 +177,49 @@ class CampaignBase(CampaignUI, Map, AutoSearchCombat): self.auto_search_moving() self.auto_search_combat(fleet_index=self.fleet_show_index) self.battle_count += 1 + + +from module.scheduler_watcher import request_extend_task_deadline + + +class CampaignBase(CampaignBase): + def run(self): + logger.hr(self.ENTRANCE, level=2) + + # Enter map + self.emotion.check_reduce(self._map_battle) + self.ENTRANCE.area = self.ENTRANCE.button + self.enter_map(self.ENTRANCE, mode=self.config.Campaign_Mode) + + # Map init + if not self.map_is_auto_search: + self.handle_map_fleet_lock() + self.map_init(self.MAP) + else: + self.map = self.MAP + self.battle_count = 0 + self.lv_reset() + self.lv_get() + + # Run + request_extend_task_deadline() + for _ in range(20): + try: + if not self.map_is_auto_search: + self.execute_a_battle() + else: + self.auto_search_execute_a_battle() + except CampaignEnd: + logger.hr('Campaign end') + return True + + # Exception + logger.warning('Battle function exhausted.') + if self.config.Error_HandleError: + logger.warning('ScriptError, Battle function exhausted, Withdrawing') + try: + self.withdraw() + except CampaignEnd: + pass + else: + raise ScriptError('Battle function exhausted.') diff --git a/module/config/argument/args.json b/module/config/argument/args.json index a0432280a..7821e7481 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -413,6 +413,20 @@ "display": "hide" } }, + "SchedulerWatcher": { + "Enable": { + "type": "checkbox", + "value": false + }, + "TimeDelta": { + "type": "input", + "value": 10 + }, + "WarningCount": { + "type": "input", + "value": 3 + } + }, "GameRestart": { "FatalErrorRestart": { "type": "checkbox", diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 59374412c..a73203cde 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -843,6 +843,10 @@ PowerLimit: Exercise: 25000 Raid: 25000 Ash: 25000 +SchedulerWatcher: + Enable: false + TimeDelta: 10 + WarningCount: 3 GameRestart: FatalErrorRestart: false MaxRetryTimesForSameTaskFailed: 5 diff --git a/module/config/argument/task.yaml b/module/config/argument/task.yaml index 03c90f2b9..2e6d9742b 100644 --- a/module/config/argument/task.yaml +++ b/module/config/argument/task.yaml @@ -21,6 +21,7 @@ Alas: - OldRetire Restart: - Scheduler + - SchedulerWatcher - GameRestart - InstanceRestart diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 5c908d21a..ee1a5764f 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -521,6 +521,11 @@ class GeneratedConfig: PowerLimit_Raid = 25000 PowerLimit_Ash = 25000 + # Group `SchedulerWatcher` + SchedulerWatcher_Enable = False + SchedulerWatcher_TimeDelta = 10 + SchedulerWatcher_WarningCount = 3 + # Group `GameRestart` GameRestart_FatalErrorRestart = False GameRestart_MaxRetryTimesForSameTaskFailed = 5 diff --git a/module/config/full_config_generated.py b/module/config/full_config_generated.py index 95fc7e412..6dddc9383 100644 --- a/module/config/full_config_generated.py +++ b/module/config/full_config_generated.py @@ -57,6 +57,9 @@ class FullGeneratedConfig: Restart_Scheduler_SuccessInterval = None Restart_Scheduler_FailureInterval = None Restart_Scheduler_ServerUpdate = None + Restart_SchedulerWatcher_Enable = None + Restart_SchedulerWatcher_TimeDelta = None + Restart_SchedulerWatcher_WarningCount = None Restart_GameRestart_FatalErrorRestart = None Restart_GameRestart_MaxRetryTimesForSameTaskFailed = None Restart_InstanceRestart_Enable = None diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 1c8348e28..800ef0844 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -2917,6 +2917,24 @@ "help": "PowerLimit.Ash.help" } }, + "SchedulerWatcher": { + "_info": { + "name": "SchedulerWatcher._info.name", + "help": "SchedulerWatcher._info.help" + }, + "Enable": { + "name": "SchedulerWatcher.Enable.name", + "help": "SchedulerWatcher.Enable.help" + }, + "TimeDelta": { + "name": "SchedulerWatcher.TimeDelta.name", + "help": "SchedulerWatcher.TimeDelta.help" + }, + "WarningCount": { + "name": "SchedulerWatcher.WarningCount.name", + "help": "SchedulerWatcher.WarningCount.help" + } + }, "GameRestart": { "_info": { "name": "GameRestart._info.name", diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 9ec58ed84..b48e455ed 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -2917,6 +2917,24 @@ "help": "PowerLimit.Ash.help" } }, + "SchedulerWatcher": { + "_info": { + "name": "SchedulerWatcher._info.name", + "help": "SchedulerWatcher._info.help" + }, + "Enable": { + "name": "SchedulerWatcher.Enable.name", + "help": "SchedulerWatcher.Enable.help" + }, + "TimeDelta": { + "name": "SchedulerWatcher.TimeDelta.name", + "help": "SchedulerWatcher.TimeDelta.help" + }, + "WarningCount": { + "name": "SchedulerWatcher.WarningCount.name", + "help": "SchedulerWatcher.WarningCount.help" + } + }, "GameRestart": { "_info": { "name": "GameRestart._info.name", diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index e2619600b..667b8e156 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -2917,6 +2917,24 @@ "help": "" } }, + "SchedulerWatcher": { + "_info": { + "name": "调度器监视器", + "help": "如果要开启此功能则建议开启自动实例重启\n注意: 修改以下的配置后要重新启动Alas实例" + }, + "Enable": { + "name": "启用", + "help": "" + }, + "TimeDelta": { + "name": "单次时限(分钟)", + "help": "" + }, + "WarningCount": { + "name": "最大警告次数", + "help": "" + } + }, "GameRestart": { "_info": { "name": "游戏重启", diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 816977b6b..38b562b7e 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -2917,6 +2917,24 @@ "help": "PowerLimit.Ash.help" } }, + "SchedulerWatcher": { + "_info": { + "name": "SchedulerWatcher._info.name", + "help": "SchedulerWatcher._info.help" + }, + "Enable": { + "name": "SchedulerWatcher.Enable.name", + "help": "SchedulerWatcher.Enable.help" + }, + "TimeDelta": { + "name": "SchedulerWatcher.TimeDelta.name", + "help": "SchedulerWatcher.TimeDelta.help" + }, + "WarningCount": { + "name": "SchedulerWatcher.WarningCount.name", + "help": "SchedulerWatcher.WarningCount.help" + } + }, "GameRestart": { "_info": { "name": "GameRestart._info.name", diff --git a/module/exercise/exercise.py b/module/exercise/exercise.py index 6b78f76e4..e6fab7d5a 100644 --- a/module/exercise/exercise.py +++ b/module/exercise/exercise.py @@ -248,3 +248,61 @@ class Exercise(ExerciseCombat): self.config.task_delay(server_update=True) else: self.config.task_delay(success=False) + + +from module.scheduler_watcher import request_extend_task_deadline + + +class Exercise(Exercise): + def run(self): + self.ui_ensure(page_exercise) + + self.opponent_change_count = self._get_opponent_change_count() + logger.attr("Change_opponent_count", self.opponent_change_count) + logger.attr('Exercise_ExerciseStrategy', self.config.Exercise_ExerciseStrategy) + self.preserve, admiral_interval = self._get_exercise_strategy() + + if not self.server_support_ocr_reset_remain(): + logger.info(f'Server {self.config.SERVER} does not yet support OCR exercise reset remain time') + logger.info('Please contact the developer to improve as soon as possible') + remain_time = timedelta(days=0) + else: + remain_time = OCR_PERIOD_REMAIN.ocr(self.device.image) + logger.info(f'Exercise period remain: {remain_time}') + + if admiral_interval is not None and remain_time: + admiral_start, admiral_end = admiral_interval + + if admiral_start > int(remain_time.total_seconds() // 3600) >= admiral_end: # set time for getting admiral + logger.info('Reach set time for admiral trial, using all attempts.') + self.preserve = 0 + elif int(remain_time.total_seconds() // 3600) < 6: # if not set to "sun18", still depleting at sunday 18pm. + logger.info('Exercise period remain less than 6 hours, using all attempts.') + self.preserve = 0 + else: + logger.info(f'Preserve {self.preserve} exercise') + + while 1: + self.remain = OCR_EXERCISE_REMAIN.ocr(self.device.image) + if self.remain <= self.preserve: + break + + logger.hr(f'Exercise remain {self.remain}', level=1) + request_extend_task_deadline() + if self.config.Exercise_OpponentChooseMode == "easiest_else_exp": + success = self._exercise_easiest_else_exp() + else: + success = self._exercise_once() + if not success: + logger.info('New opponent exhausted') + break + + # self.equipment_take_off_when_finished() + + # Scheduler + with self.config.multi_set(): + self.config.set_record(Exercise_OpponentRefreshValue=self.opponent_change_count) + if self.remain <= self.preserve or self.opponent_change_count >= 5: + self.config.task_delay(server_update=True) + else: + self.config.task_delay(success=False) diff --git a/module/os/map.py b/module/os/map.py index 078ef35fa..bb1052583 100644 --- a/module/os/map.py +++ b/module/os/map.py @@ -948,3 +948,80 @@ class OSMap(OSFleet, Map, GlobeCamera, StrategicSearchHandler): logger.warning('Too many trial on map rescan, stop') self.fleet_set(self.config.OpsiFleet_Fleet) return False + + +from module.scheduler_watcher import request_extend_task_deadline + + +class OSMap(OSMap): + def os_auto_search_daemon(self, drop=None, strategic=False, skip_first_screenshot=True): + logger.hr('OS auto search', level=2) + self.on_auto_search_battle_count_reset() + unlock_checked = False + unlock_check_timer = Timer(5, count=10).start() + self.ash_popup_canceled = False + + success = True + finished_combat = 0 + died_timer = Timer(1.5, count=3) + self.hp_reset() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if not unlock_checked and unlock_check_timer.reached(): + logger.critical('Unable to use auto search in current zone') + logger.critical('Please finish the story mode of OpSi to unlock auto search ' + 'before using any OpSi functions') + raise RequestHumanTakeover + if self.is_in_map(): + self.device.stuck_record_clear() + if not success: + if died_timer.reached(): + logger.warning('Fleet died confirm') + break + else: + died_timer.reset() + else: + died_timer.reset() + + if not unlock_checked: + if self.appear(AUTO_SEARCH_OS_MAP_OPTION_OFF, offset=(5, 120)): + unlock_checked = True + elif self.appear(AUTO_SEARCH_OS_MAP_OPTION_OFF_DISABLED, offset=(5, 120)): + unlock_checked = True + elif self.appear(AUTO_SEARCH_OS_MAP_OPTION_ON, offset=(5, 120)): + unlock_checked = True + + if self.handle_os_auto_search_map_option( + drop=drop, + enable=success + ): + unlock_checked = True + continue + if self.handle_retirement(): + # Retire will interrupt auto search, need a retry + self.ash_popup_canceled = True + continue + if self.combat_appear(): + self.on_auto_search_battle_count_add() + if strategic and self.config.task_switched(): + self.interrupt_auto_search() + request_extend_task_deadline() + result = self.auto_search_combat(drop=drop) + if result: + finished_combat += 1 + else: + self.hp_get() + if any(self.need_repair): + success = False + logger.warning('Fleet died, stop auto search') + continue + if self.handle_map_event(): + # Auto search can not handle siren searching device. + continue + + return finished_combat diff --git a/module/scheduler_watcher.py b/module/scheduler_watcher.py new file mode 100644 index 000000000..8dadf0609 --- /dev/null +++ b/module/scheduler_watcher.py @@ -0,0 +1,102 @@ +import datetime +import os +import threading +import time + +from module.counter import MaxCounter, CounterReachMaxCountException +from module.logger import logger +from module.notify import handle_notify +from module.config.config import AzurLaneConfig + + +class SchedulerWatcher: + instance = None + + def __init__(self): + self.alas_obj = None + self.config: AzurLaneConfig = None + self.watcher: threading.Thread = None + self.warning_count = None + self.warning_time = None + self.current_task = None + self.min_timedelta = None + + def set_alas_obj(self, alas_obj): + self.alas_obj = alas_obj + self.config = alas_obj.config + + def start_watch(self, min_timedelta, max_warning_count): + if self.watcher is not None: + if self.watcher.is_alive(): + return + self.min_timedelta = datetime.timedelta(minutes=min_timedelta) + self.warning_count = MaxCounter(max_warning_count) + logger.info(f"Scheduler watcher start") + self.watcher = threading.Thread(target=self.watcher_thread, daemon=True) + self.watcher.start() + + def request_extend_task_deadline(self): + if self.watcher is None: + return + now = datetime.datetime.now() + if self.warning_count.current_count > 0: + self.warning_count.reset() + self.warning_time = now + self.min_timedelta + logger.info(f"Current task requests to extend the task deadline") + else: + if self.warning_time - now < datetime.timedelta(minutes=3): + self.warning_time = now + self.min_timedelta + logger.info(f"Current task requests to extend the task deadline") + + def no_task(self): + if self.watcher is None: + return + self.current_task = None + self.warning_time = None + + def switch_task(self, task): + if self.watcher is None: + return + self.current_task = task + self.warning_count.reset() + self.warning_time = datetime.datetime.now() + self.min_timedelta + + def watcher_thread(self): + while 1: + time.sleep(60) + if self.warning_time is None or self.current_task is None: + continue + + now = datetime.datetime.now() + if now > self.warning_time: + try: + self.warning_count.count_once() + logger.warning(f"Current task reached time limit once") + self.warning_time = now + self.min_timedelta + except CounterReachMaxCountException: + logger.error(f"Current task reached final time limit, assuming the scheduler is stuck") + + if not self.config.full_config.Restart_InstanceRestart_Enable: + logger.error(f"Instance restart disabled, notify user") + handle_notify( + self.config.full_config.Alas_Error_OnePushConfig, + title=f"Alas <{self.alas_obj.config_name}>: Scheduler Stuck", + content=f"Task {self.current_task} reached final time limit, assuming the scheduler is stuck", + ) + os._exit(-1) + + @staticmethod + def get_instance() -> "SchedulerWatcher": + if SchedulerWatcher.instance is None: + SchedulerWatcher.instance = SchedulerWatcher() + return SchedulerWatcher.instance + + +def request_extend_task_deadline(): + ins = SchedulerWatcher.get_instance() + ins.request_extend_task_deadline() + + +def no_task(): + ins = SchedulerWatcher.get_instance() + ins.no_task()