From 160b6bdb1fd6b4bb0b214fdcb85796004040d9db Mon Sep 17 00:00:00 2001 From: 0O0o0oOoO00 <11174151+0O0o0oOoO00@users.noreply.github.com> Date: Tue, 2 Sep 2025 00:48:40 +0800 Subject: [PATCH] add: base framework of game restart and instance restart --- alas.py | 201 ++++++++++++++++++++++++- config/template.json | 10 ++ module/config/argument/args.json | 28 ++++ module/config/argument/argument.yaml | 13 +- module/config/argument/task.yaml | 2 + module/config/config_generated.py | 10 ++ module/config/full_config_generated.py | 6 + module/config/i18n/en-US.json | 36 +++++ module/config/i18n/ja-JP.json | 36 +++++ module/config/i18n/zh-CN.json | 36 +++++ module/config/i18n/zh-TW.json | 36 +++++ module/counter.py | 32 ++++ 12 files changed, 441 insertions(+), 5 deletions(-) create mode 100644 module/counter.py diff --git a/alas.py b/alas.py index 46e161799..bb6c252e2 100644 --- a/alas.py +++ b/alas.py @@ -587,6 +587,27 @@ class AzurLaneAutoScript: from module.luahook.crack import * +from module.counter import * + + +class FailedTaskCounterManager: + def __init__(self, config: AzurLaneConfig): + self.task_counter = {} + + if config.full_config.Restart_GameRestart_Enable: + self.counter_max_count = config.full_config.Restart_GameRestart_MaxRetryTimes + self.counter_class = MaxCounter + else: + self.counter_max_count = 0 + self.counter_class = Counter + + def count(self, task, throw=True) -> bool: + counter = self.task_counter.get(task, self.counter_class(self.counter_max_count)) + return counter.count_once(throw=throw) + + def reset(self, task): + counter = self.task_counter.get(task, self.counter_class(self.counter_max_count)) + counter.reset() class AzurLaneAutoScript(AzurLaneAutoScript): @@ -602,6 +623,8 @@ class AzurLaneAutoScript(AzurLaneAutoScript): elif self.class_name == "ArknightsAutoScript": self.is_ark = True + self.failed_task_counter = FailedTaskCounterManager(self.config) + def pre_init(self): from PIL import Image Image.init() @@ -616,10 +639,113 @@ class AzurLaneAutoScript(AzurLaneAutoScript): Image.register_mime(PngImagePlugin.PngImageFile.format, "image/png") + def handle_TaskEnd(self, e) -> bool: + return True + + def handle_GameNotRunningError(self, e) -> bool: + logger.warning(e) + self.config.task_call('Restart') + return False + + def handle_GameStuckError(self, e) -> bool: + logger.error(e) + self.save_error_log() + logger.warning(f'Game stuck, {self.device.package} will be restarted in 10 seconds') + logger.warning('If you are playing by hand, please stop Alas') + self.config.task_call('Restart') + self.device.sleep(10) + return False + + def handle_GameTooManyClickError(self, e) -> bool: + logger.error(e) + self.save_error_log() + logger.warning(f'Game stuck, {self.device.package} will be restarted in 10 seconds') + logger.warning('If you are playing by hand, please stop Alas') + self.config.task_call('Restart') + self.device.sleep(10) + return False + + def handle_GameBugError(self, e) -> bool: + logger.warning(e) + self.save_error_log() + logger.warning('An error has occurred in Azur Lane game client, Alas is unable to handle') + logger.warning(f'Restarting {self.device.package} to fix it') + self.config.task_call('Restart') + self.device.sleep(10) + return False + + def handle_GamePageUnknownError(self, e) -> bool: + logger.info('Game server may be under maintenance or network may be broken, check server status now') + self.checker.check_now() + if self.checker.is_available(): + logger.critical('Game page unknown') + self.save_error_log() + handle_notify( + self.config.Error_OnePushConfig, + title=f"Alas <{self.config_name}> crashed", + content=f"<{self.config_name}> GamePageUnknownError", + ) + exit(1) + else: + self.checker.wait_until_available() + return False + + def handle_ScriptError(self, e) -> bool: + logger.exception(e) + logger.critical('This is likely to be a mistake of developers, but sometimes just random issues') + handle_notify( + self.config.Error_OnePushConfig, + title=f"Alas <{self.config_name}> crashed", + content=f"<{self.config_name}> ScriptError", + ) + exit(1) + + def handle_RequestHumanTakeover(self, e) -> bool: + logger.critical('Request human takeover') + handle_notify( + self.config.Error_OnePushConfig, + title=f"Alas <{self.config_name}> crashed", + content=f"<{self.config_name}> RequestHumanTakeover", + ) + exit(1) + + def handle_Exception(self, e) -> bool: + logger.exception(e) + self.save_error_log() + handle_notify( + self.config.Error_OnePushConfig, + title=f"Alas <{self.config_name}> crashed", + content=f"<{self.config_name}> Exception occured", + ) + exit(1) + def run(self, command, skip_first_screenshot=False): - if self.is_azur: - luahook_crack_all(self.config, self.device) - super().run(command, skip_first_screenshot=skip_first_screenshot) + try: + if self.is_azur: + luahook_disable_all(self.config, self.device) + luahook_crack_all(self.config, self.device) + if not skip_first_screenshot: + self.device.screenshot() + self.__getattribute__(command)() + return True + except TaskEnd as e: + return self.handle_TaskEnd(e) + except GameNotRunningError as e: + return self.handle_GameNotRunningError(e) + except GameStuckError as e: + return self.handle_GameStuckError(e) + except GameTooManyClickError as e: + return self.handle_GameTooManyClickError(e) + except GameBugError as e: + return self.handle_GameBugError(e) + except GamePageUnknownError as e: + return self.handle_GamePageUnknownError(e) + except ScriptError as e: + return self.handle_ScriptError(e) + except RequestHumanTakeover as e: + return self.handle_RequestHumanTakeover(e) + except Exception as e: + return self.handle_Exception(e) def main4(self): from module.campaign.run import CampaignRun @@ -695,7 +821,74 @@ class AzurLaneAutoScript(AzurLaneAutoScript): def loop(self): if self.is_azur: CrackResource(self.config, self.device).ensure() - super().loop() + + logger.set_file_logger(self.config_name) + logger.info(f'Start scheduler loop: {self.config_name}') + + while 1: + # Check update event from GUI + if self.stop_event is not None: + if self.stop_event.is_set(): + logger.info("Update event detected") + logger.info(f"Alas [{self.config_name}] exited.") + break + # Check game server maintenance + self.checker.wait_until_available() + if self.checker.is_recovered(): + # There is an accidental bug hard to reproduce + # Sometimes, config won't be updated due to blocking + # even though it has been changed + # So update it once recovered + del_cached_property(self, 'config') + logger.info('Server or network is recovered. Restart game client') + self.config.task_call('Restart') + # Get task + task = self.get_next_task() + # Init device and change server + _ = self.device + self.device.config = self.config + # Skip first restart + if self.is_first_task and task == 'Restart': + logger.info('Skip task `Restart` at scheduler start') + self.config.task_delay(server_update=True) + del_cached_property(self, 'config') + continue + + # Run + logger.info(f'Scheduler: Start task `{task}`') + self.device.stuck_record_clear() + self.device.click_record_clear() + logger.hr(task, level=0) + success = self.run(inflection.underscore(task)) + logger.info(f'Scheduler: End task `{task}`') + self.is_first_task = False + + if not success: + if not self.failed_task_counter.count(task, throw=False): + logger.critical(f"Task `{task}` failed 3 or more times.") + logger.critical("Possible reason #1: You haven't used it correctly. " + "Please read the help text of the options.") + logger.critical("Possible reason #2: There is a problem with this task. " + "Please contact developers or try to fix it yourself.") + logger.critical('Request human takeover') + handle_notify( + self.config.Error_OnePushConfig, + title=f"Alas <{self.config_name}> crashed", + content=f"<{self.config_name}> RequestHumanTakeover\nTask `{task}` failed 3 or more times.", + ) + exit(1) + + if success: + self.failed_task_counter.reset(task) + del_cached_property(self, 'config') + continue + elif self.config.Error_HandleError: + # self.config.task_delay(success=False) + del_cached_property(self, 'config') + self.checker.check_now() + continue + else: + break if __name__ == '__main__': diff --git a/config/template.json b/config/template.json index b4087564a..865a0a7b0 100644 --- a/config/template.json +++ b/config/template.json @@ -73,6 +73,16 @@ "FailureInterval": 0, "ServerUpdate": "00:00" }, + "GameRestart": { + "Enable": false, + "MaxRetryTimes": 5, + "Notify": false + }, + "InstanceRestart": { + "Enable": false, + "MaxRetryTimes": 5, + "Notify": false + }, "Storage": { "Storage": {} } diff --git a/module/config/argument/args.json b/module/config/argument/args.json index b2ef4835c..f826e1373 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -413,6 +413,34 @@ "display": "hide" } }, + "GameRestart": { + "Enable": { + "type": "checkbox", + "value": false + }, + "MaxRetryTimes": { + "type": "input", + "value": 5 + }, + "Notify": { + "type": "checkbox", + "value": false + } + }, + "InstanceRestart": { + "Enable": { + "type": "checkbox", + "value": false + }, + "MaxRetryTimes": { + "type": "input", + "value": 5 + }, + "Notify": { + "type": "checkbox", + "value": false + } + }, "Storage": { "Storage": { "type": "storage", diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 57b170ae3..cbc556a8c 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -829,4 +829,15 @@ Misc: ExerciseMorePower: -1.0 FastWave: false MonsterKillSelf: false - SkipBattleCelebrate: false \ No newline at end of file + SkipBattleCelebrate: false + +# ==================== Cheat ==================== + +GameRestart: + Enable: false + MaxRetryTimes: 5 + Notify: false +InstanceRestart: + Enable: false + MaxRetryTimes: 5 + Notify: false \ No newline at end of file diff --git a/module/config/argument/task.yaml b/module/config/argument/task.yaml index f7687d187..b2efb9e58 100644 --- a/module/config/argument/task.yaml +++ b/module/config/argument/task.yaml @@ -21,6 +21,8 @@ Alas: - OldRetire Restart: - Scheduler + - GameRestart + - InstanceRestart # ==================== Cheat ==================== diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 8d636e00f..9f763422e 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -509,5 +509,15 @@ class GeneratedConfig: Misc_MonsterKillSelf = False Misc_SkipBattleCelebrate = False + # Group `GameRestart` + GameRestart_Enable = False + GameRestart_MaxRetryTimes = 5 + GameRestart_Notify = False + + # Group `InstanceRestart` + InstanceRestart_Enable = False + InstanceRestart_MaxRetryTimes = 5 + InstanceRestart_Notify = False + # Group `Storage` Storage_Storage = {} diff --git a/module/config/full_config_generated.py b/module/config/full_config_generated.py index ab116db3f..71f82f6b5 100644 --- a/module/config/full_config_generated.py +++ b/module/config/full_config_generated.py @@ -57,6 +57,12 @@ class FullGeneratedConfig: Restart_Scheduler_SuccessInterval = None Restart_Scheduler_FailureInterval = None Restart_Scheduler_ServerUpdate = None + Restart_GameRestart_Enable = None + Restart_GameRestart_MaxRetryTimes = None + Restart_GameRestart_Notify = None + Restart_InstanceRestart_Enable = None + Restart_InstanceRestart_MaxRetryTimes = None + Restart_InstanceRestart_Notify = None Restart_Storage_Storage = None # Task `Hook` diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 4bbb1b7c0..ce6e66ee6 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -2867,6 +2867,42 @@ "help": "Misc.SkipBattleCelebrate.help" } }, + "GameRestart": { + "_info": { + "name": "GameRestart._info.name", + "help": "GameRestart._info.help" + }, + "Enable": { + "name": "GameRestart.Enable.name", + "help": "GameRestart.Enable.help" + }, + "MaxRetryTimes": { + "name": "GameRestart.MaxRetryTimes.name", + "help": "GameRestart.MaxRetryTimes.help" + }, + "Notify": { + "name": "GameRestart.Notify.name", + "help": "GameRestart.Notify.help" + } + }, + "InstanceRestart": { + "_info": { + "name": "InstanceRestart._info.name", + "help": "InstanceRestart._info.help" + }, + "Enable": { + "name": "InstanceRestart.Enable.name", + "help": "InstanceRestart.Enable.help" + }, + "MaxRetryTimes": { + "name": "InstanceRestart.MaxRetryTimes.name", + "help": "InstanceRestart.MaxRetryTimes.help" + }, + "Notify": { + "name": "InstanceRestart.Notify.name", + "help": "InstanceRestart.Notify.help" + } + }, "Storage": { "_info": { "name": "Task status", diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 48e4a49e5..c434e8804 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -2867,6 +2867,42 @@ "help": "Misc.SkipBattleCelebrate.help" } }, + "GameRestart": { + "_info": { + "name": "GameRestart._info.name", + "help": "GameRestart._info.help" + }, + "Enable": { + "name": "GameRestart.Enable.name", + "help": "GameRestart.Enable.help" + }, + "MaxRetryTimes": { + "name": "GameRestart.MaxRetryTimes.name", + "help": "GameRestart.MaxRetryTimes.help" + }, + "Notify": { + "name": "GameRestart.Notify.name", + "help": "GameRestart.Notify.help" + } + }, + "InstanceRestart": { + "_info": { + "name": "InstanceRestart._info.name", + "help": "InstanceRestart._info.help" + }, + "Enable": { + "name": "InstanceRestart.Enable.name", + "help": "InstanceRestart.Enable.help" + }, + "MaxRetryTimes": { + "name": "InstanceRestart.MaxRetryTimes.name", + "help": "InstanceRestart.MaxRetryTimes.help" + }, + "Notify": { + "name": "InstanceRestart.Notify.name", + "help": "InstanceRestart.Notify.help" + } + }, "Storage": { "_info": { "name": "Storage._info.name", diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 5ceaa3b63..50d7852f6 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -2867,6 +2867,42 @@ "help": "" } }, + "GameRestart": { + "_info": { + "name": "游戏重启", + "help": "该功能的启用和关闭需要重启这个Alas实例" + }, + "Enable": { + "name": "启用", + "help": "" + }, + "MaxRetryTimes": { + "name": "最大尝试重启次数", + "help": "" + }, + "Notify": { + "name": "重启时通知", + "help": "不建议开启,防止消息轰炸" + } + }, + "InstanceRestart": { + "_info": { + "name": "实例重启", + "help": "" + }, + "Enable": { + "name": "启用", + "help": "" + }, + "MaxRetryTimes": { + "name": "最大尝试重启次数", + "help": "" + }, + "Notify": { + "name": "重启时通知", + "help": "谨慎开启,小心消息轰炸" + } + }, "Storage": { "_info": { "name": "任务状态", diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 50bcb6281..a764fd838 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -2867,6 +2867,42 @@ "help": "Misc.SkipBattleCelebrate.help" } }, + "GameRestart": { + "_info": { + "name": "GameRestart._info.name", + "help": "GameRestart._info.help" + }, + "Enable": { + "name": "GameRestart.Enable.name", + "help": "GameRestart.Enable.help" + }, + "MaxRetryTimes": { + "name": "GameRestart.MaxRetryTimes.name", + "help": "GameRestart.MaxRetryTimes.help" + }, + "Notify": { + "name": "GameRestart.Notify.name", + "help": "GameRestart.Notify.help" + } + }, + "InstanceRestart": { + "_info": { + "name": "InstanceRestart._info.name", + "help": "InstanceRestart._info.help" + }, + "Enable": { + "name": "InstanceRestart.Enable.name", + "help": "InstanceRestart.Enable.help" + }, + "MaxRetryTimes": { + "name": "InstanceRestart.MaxRetryTimes.name", + "help": "InstanceRestart.MaxRetryTimes.help" + }, + "Notify": { + "name": "InstanceRestart.Notify.name", + "help": "InstanceRestart.Notify.help" + } + }, "Storage": { "_info": { "name": "任務狀態", diff --git a/module/counter.py b/module/counter.py new file mode 100644 index 000000000..7f97a0df6 --- /dev/null +++ b/module/counter.py @@ -0,0 +1,32 @@ +class CounterReachMaxCountException(Exception): + ... + + +class MaxCounter: + def __init__(self, max_count): + self.max_count = max_count + self.current_count = 0 + + def count_once(self, throw=True) -> bool: + self.current_count += 1 + if self.current_count >= self.max_count: + if throw: + raise CounterReachMaxCountException() + else: + return False + return True + + def reset(self): + self.current_count = 0 + + +class Counter: + def __init__(self, start=0): + self.current_count = start + + def count_once(self, throw=True) -> bool: + self.current_count += 1 + return True + + def reset(self): + self.current_count = 0