mirror of
https://github.com/LmeSzinc/StarRailCopilot.git
synced 2024-11-15 22:19:18 +00:00
Add: Alas scheduler
This commit is contained in:
parent
9928d5b4cc
commit
1f8a9383f7
314
module/alas.py
Normal file
314
module/alas.py
Normal file
@ -0,0 +1,314 @@
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import inflection
|
||||
from cached_property import cached_property
|
||||
|
||||
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.notify import handle_notify
|
||||
|
||||
|
||||
class AzurLaneAutoScript:
|
||||
stop_event: threading.Event = None
|
||||
|
||||
def __init__(self, config_name='alas'):
|
||||
logger.hr('Start', level=0)
|
||||
self.config_name = config_name
|
||||
# Skip first restart
|
||||
self.is_first_task = True
|
||||
# Failure count of tasks
|
||||
# Key: str, task name, value: int, failure count
|
||||
self.failure_record = {}
|
||||
|
||||
@cached_property
|
||||
def config(self):
|
||||
try:
|
||||
config = AzurLaneConfig(config_name=self.config_name)
|
||||
return config
|
||||
except RequestHumanTakeover:
|
||||
logger.critical('Request human takeover')
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
exit(1)
|
||||
|
||||
@cached_property
|
||||
def device(self):
|
||||
try:
|
||||
from module.device.device import Device
|
||||
device = Device(config=self.config)
|
||||
return device
|
||||
except RequestHumanTakeover:
|
||||
logger.critical('Request human takeover')
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
exit(1)
|
||||
|
||||
@cached_property
|
||||
def checker(self):
|
||||
try:
|
||||
from module.server_checker import ServerChecker
|
||||
checker = ServerChecker(server=self.config.Emulator_PackageName)
|
||||
return checker
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
exit(1)
|
||||
|
||||
def run(self, command):
|
||||
try:
|
||||
self.device.screenshot()
|
||||
self.__getattribute__(command)()
|
||||
return True
|
||||
except TaskEnd:
|
||||
return True
|
||||
except GameNotRunningError as e:
|
||||
logger.warning(e)
|
||||
self.config.task_call('Restart')
|
||||
return True
|
||||
except (GameStuckError, GameTooManyClickError) as e:
|
||||
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
|
||||
except GameBugError as e:
|
||||
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
|
||||
except GamePageUnknownError:
|
||||
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
|
||||
except ScriptError as e:
|
||||
logger.critical(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)
|
||||
except RequestHumanTakeover:
|
||||
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)
|
||||
except Exception as e:
|
||||
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 save_error_log(self):
|
||||
"""
|
||||
Save last 60 screenshots in ./log/error/<timestamp>
|
||||
Save logs to ./log/error/<timestamp>/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:
|
||||
if not os.path.exists('./log/error'):
|
||||
os.mkdir('./log/error')
|
||||
folder = f'./log/error/{int(time.time() * 1000)}'
|
||||
logger.warning(f'Saving error: {folder}')
|
||||
os.mkdir(folder)
|
||||
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')
|
||||
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)
|
||||
|
||||
def wait_until(self, future):
|
||||
"""
|
||||
Wait until a specific time.
|
||||
|
||||
Args:
|
||||
future (datetime):
|
||||
|
||||
Returns:
|
||||
bool: True if wait finished, False if config changed.
|
||||
"""
|
||||
future = future + timedelta(seconds=1)
|
||||
self.config.start_watching()
|
||||
while 1:
|
||||
if datetime.now() > future:
|
||||
return True
|
||||
if self.stop_event is not None:
|
||||
if self.stop_event.is_set():
|
||||
logger.info("Update event detected")
|
||||
logger.info(f"[{self.config_name}] exited. Reason: Update")
|
||||
exit(0)
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
if self.config.should_reload():
|
||||
return False
|
||||
|
||||
def get_next_task(self):
|
||||
"""
|
||||
Returns:
|
||||
str: Name of the next task.
|
||||
"""
|
||||
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():
|
||||
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
|
||||
self.run('start')
|
||||
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):
|
||||
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
|
||||
# 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
|
||||
|
||||
# Check failures
|
||||
failed = deep_get(self.failure_record, keys=task, default=0)
|
||||
failed = 0 if success else failed + 1
|
||||
deep_set(self.failure_record, keys=task, value=failed)
|
||||
if failed >= 3:
|
||||
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:
|
||||
del_cached_property(self, 'config')
|
||||
continue
|
||||
else:
|
||||
# self.config.task_delay(success=False)
|
||||
del_cached_property(self, 'config')
|
||||
self.checker.check_now()
|
||||
continue
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
alas = AzurLaneAutoScript()
|
||||
alas.loop()
|
@ -19,19 +19,13 @@ class PreservedAssets:
|
||||
def ui(self):
|
||||
assets = set()
|
||||
assets |= get_assets_from_file(
|
||||
file='./module/ui/assets.py',
|
||||
file='./tasks/base/assets/assets_base_page.py',
|
||||
regex=re.compile(r'^([A-Za-z][A-Za-z0-9_]+) = ')
|
||||
)
|
||||
assets |= get_assets_from_file(
|
||||
file='./module/ui/ui.py',
|
||||
regex=re.compile(r'\(([A-Z][A-Z0-9_]+),')
|
||||
file='./tasks/base/assets/assets_base_popup.py',
|
||||
regex=re.compile(r'^([A-Za-z][A-Za-z0-9_]+) = ')
|
||||
)
|
||||
assets |= get_assets_from_file(
|
||||
file='./module/handler/info_handler.py',
|
||||
regex=re.compile(r'\(([A-Z][A-Z0-9_]+),')
|
||||
)
|
||||
# MAIN_CHECK == MAIN_GOTO_CAMPAIGN
|
||||
assets.add('MAIN_GOTO_CAMPAIGN')
|
||||
return assets
|
||||
|
||||
|
||||
@ -65,25 +59,8 @@ class Resource:
|
||||
continue
|
||||
logger.info(f'{obj}: {key}')
|
||||
|
||||
def release_resources(next_task=''):
|
||||
# Release all OCR models
|
||||
# Usually to have 2 models loaded and each model takes about 20MB
|
||||
# This will release 20-40MB
|
||||
from module.webui.setting import State
|
||||
if not State.deploy_config.UseOcrServer:
|
||||
# Release only when using per-instance OCR
|
||||
from module.ocr.ocr import OCR_MODEL
|
||||
if 'Opsi' in next_task or 'commission' in next_task:
|
||||
# OCR models will be used soon, don't release
|
||||
models = []
|
||||
elif next_task:
|
||||
# Release OCR models except 'azur_lane'
|
||||
models = ['cnocr', 'jp', 'tw']
|
||||
else:
|
||||
models = ['azur_lane', 'cnocr', 'jp', 'tw']
|
||||
for model in models:
|
||||
del_cached_property(OCR_MODEL, model)
|
||||
|
||||
def release_resources(next_task=''):
|
||||
# Release assets cache
|
||||
# module.ui has about 80 assets and takes about 3MB
|
||||
# Alas has about 800 assets, but they are not all loaded.
|
||||
@ -96,20 +73,5 @@ def release_resources(next_task=''):
|
||||
# logger.info(f'Release {obj}')
|
||||
obj.resource_release()
|
||||
|
||||
# Release cached images for map detection
|
||||
from module.map_detection.utils_assets import ASSETS
|
||||
attr_list = [
|
||||
'ui_mask',
|
||||
'ui_mask_os',
|
||||
'ui_mask_stroke',
|
||||
'ui_mask_in_map',
|
||||
'ui_mask_os_in_map',
|
||||
'tile_center_image',
|
||||
'tile_corner_image',
|
||||
'tile_corner_image_list'
|
||||
]
|
||||
for attr in attr_list:
|
||||
del_cached_property(ASSETS, attr)
|
||||
|
||||
# Useless in most cases, but just call it
|
||||
# gc.collect()
|
||||
|
15
module/handler/sensitive_info.py
Normal file
15
module/handler/sensitive_info.py
Normal file
@ -0,0 +1,15 @@
|
||||
def handle_sensitive_image(image):
|
||||
"""
|
||||
Args:
|
||||
image:
|
||||
|
||||
Returns:
|
||||
np.ndarray:
|
||||
"""
|
||||
# Paint UID to black
|
||||
image[680:720, 0:180, :] = 0
|
||||
return image
|
||||
|
||||
|
||||
def handle_sensitive_logs(logs):
|
||||
return logs
|
4
module/notify.py
Normal file
4
module/notify.py
Normal file
@ -0,0 +1,4 @@
|
||||
from module.logger import logger
|
||||
|
||||
def handle_notify(*args, **kwargs):
|
||||
logger.error('Error notify is not supported yet')
|
16
module/server_checker.py
Normal file
16
module/server_checker.py
Normal file
@ -0,0 +1,16 @@
|
||||
class ServerChecker:
|
||||
# Create a fake server check since server check is not supported yet.
|
||||
def __init__(self, server):
|
||||
pass
|
||||
|
||||
def check_now(self):
|
||||
pass
|
||||
|
||||
def is_available(self):
|
||||
return True
|
||||
|
||||
def wait_until_available(self):
|
||||
pass
|
||||
|
||||
def is_recovered(self):
|
||||
return False
|
@ -141,11 +141,12 @@ class ProcessManager:
|
||||
try:
|
||||
# Run alas
|
||||
if func == "alas":
|
||||
from alas import AzurLaneAutoScript
|
||||
from module.alas import AzurLaneAutoScript
|
||||
from src import StarRailCopilot
|
||||
|
||||
if e is not None:
|
||||
AzurLaneAutoScript.stop_event = e
|
||||
AzurLaneAutoScript(config_name=config_name).loop()
|
||||
StarRailCopilot(config_name=config_name).loop()
|
||||
else:
|
||||
logger.critical(f"No function matched: {func}")
|
||||
logger.info(f"[{config_name}] exited. Reason: Finish\n")
|
||||
|
28
src.py
Normal file
28
src.py
Normal file
@ -0,0 +1,28 @@
|
||||
from module.alas import AzurLaneAutoScript
|
||||
from module.logger import logger
|
||||
|
||||
|
||||
class StarRailCopilot(AzurLaneAutoScript):
|
||||
def restart(self):
|
||||
from tasks.login.login import Login
|
||||
Login(self.config, device=self.device).app_restart()
|
||||
|
||||
def start(self):
|
||||
from tasks.login.login import Login
|
||||
Login(self.config, device=self.device).app_start()
|
||||
|
||||
def goto_main(self):
|
||||
from tasks.login.login import Login
|
||||
from tasks.base.ui import UI
|
||||
if self.device.app_is_running():
|
||||
logger.info('App is already running, goto main page')
|
||||
UI(self.config, device=self.device).ui_goto_main()
|
||||
else:
|
||||
logger.info('App is not running, start app and goto main page')
|
||||
Login(self.config, device=self.device).app_start()
|
||||
UI(self.config, device=self.device).ui_goto_main()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
src = StarRailCopilot('src')
|
||||
src.loop()
|
Loading…
Reference in New Issue
Block a user