diff --git a/dev_tools/route_extract.py b/dev_tools/route_extract.py new file mode 100644 index 000000000..08d341f25 --- /dev/null +++ b/dev_tools/route_extract.py @@ -0,0 +1,89 @@ +import os +import re +from dataclasses import dataclass, fields + +from module.base.code_generator import CodeGenerator + + +@dataclass +class RouteData: + name: str + route: str + plane: str + floor: str = 'F1' + position: tuple = None + + +class RouteExtract: + def __init__(self, folder): + self.folder = folder + + def iter_files(self): + for path, folders, files in os.walk(self.folder): + path = path.replace('\\', '/') + for file in files: + if file.endswith('.py'): + yield f'{path}/{file}' + + def extract_route(self, file): + print(f'Extract {file}') + with open(file, 'r', encoding='utf-8') as f: + content = f.read() + + """ + def route_item_enemy(self): + self.enter_himeko_trial() + self.map_init(plane=Jarilo_BackwaterPass, position=(519.9, 361.5)) + """ + regex = re.compile( + r'def (?P[a-zA-Z0-9_]*?)\(self\):.*?' + r'self\.map_init\((.*?)\)' + , re.DOTALL) + file = file.replace(self.folder, '').replace('.py', '').replace('/', '_').strip('_') + module = f"{self.folder.strip('./').replace('/', '.')}.{file.replace('_', '.')}" + + for result in regex.findall(content): + func, data = result + + res = re.search(r'plane=([a-zA-Z_]*)', data) + if res: + plane = res.group(1) + else: + # Must contain plane + continue + res = re.search(r'floor=([\'"a-zA-Z0-9_]*)', data) + if res: + floor = res.group(1).strip('"\'') + else: + floor = 'F1' + res = re.search(r'position=\(([0-9.]*)[, ]+([0-9.]*)', data) + if res: + position = (float(res.group(1)), float(res.group(2))) + else: + position = None + + yield RouteData( + name=f'{file}__{func}', + route=f'{module}:{func}', + plane=plane, + floor=floor, + position=position, + ) + + def write(self, file): + gen = CodeGenerator() + gen.Import(""" + from tasks.map.route.base import RouteData + """) + for f in self.iter_files(): + for row in self.extract_route(f): + with gen.Object(key=row.name, object_class='RouteData'): + for key in fields(row): + value = getattr(row, key.name) + gen.ObjectAttr(key.name, value) + gen.write(file) + + +if __name__ == '__main__': + os.chdir(os.path.join(os.path.dirname(__file__), '../')) + RouteExtract('./route/daily').write('./tasks/map/route/route/daily.py') diff --git a/route/daily/forgotten_hall/stage_1.py b/route/daily/ForgottenHallStage1.py similarity index 100% rename from route/daily/forgotten_hall/stage_1.py rename to route/daily/ForgottenHallStage1.py diff --git a/tasks/daily/daily_quest.py b/tasks/daily/daily_quest.py index dea9e4b89..217577d22 100644 --- a/tasks/daily/daily_quest.py +++ b/tasks/daily/daily_quest.py @@ -23,6 +23,7 @@ from tasks.dungeon.ui import DungeonUI from tasks.item.consumable_usage import ConsumableUsageUI from tasks.item.relics import RelicsUI from tasks.map.route.loader import RouteLoader +from tasks.map.route.route import ROUTE_DAILY class DailyQuestOcr(Ocr): @@ -260,7 +261,7 @@ class DailyQuestUI(DungeonUI, RouteLoader): if RelicsUI(self.config, self.device).salvage_relic(): done += 1 if KEYWORDS_DAILY_QUEST.Complete_Forgotten_Hall_1_time in quests: - self.route_run('daily.forgotten_hall.stage_1') + self.route_run(ROUTE_DAILY.ForgottenHallStage1__route) done += 1 return done diff --git a/tasks/map/route/base.py b/tasks/map/route/base.py index 17f4c06bb..7615270ad 100644 --- a/tasks/map/route/base.py +++ b/tasks/map/route/base.py @@ -1,8 +1,19 @@ +from dataclasses import dataclass + from tasks.map.control.control import MapControl from tasks.map.control.waypoint import Waypoint from tasks.map.keywords import MapPlane +@dataclass +class RouteData: + name: str + route: str + plane: str + floor: str = 'F1' + position: tuple = None + + class RouteBase(MapControl): """ Base class of `Route` diff --git a/tasks/map/route/loader.py b/tasks/map/route/loader.py index cf1d97d4a..5fb791ba5 100644 --- a/tasks/map/route/loader.py +++ b/tasks/map/route/loader.py @@ -1,38 +1,71 @@ import importlib import os -from module.exception import RequestHumanTakeover +from module.base.decorator import del_cached_property +from module.exception import ScriptError from module.logger import logger from tasks.base.ui import UI -from tasks.map.route.base import RouteBase +from tasks.map.route.base import RouteBase, RouteData class RouteLoader(UI): - route: RouteBase + route_module: str = '' + route_func: str = '' + route_obj: RouteBase - def route_run(self, route: str): + def route_delete(self): + del_cached_property(self, 'route_obj') + self.route_module = '' + self.route_func = '' + + def route_run(self, route: RouteData | str): """ Args: - route: .py module path such as `daily.forgotten_hall.stage1` - which will load `./route/daily/forgotten_hall/stage1.py` + route: .py module path such as `route.daily.ForgottenHallStage1:route` + which will load `./route/daily/ForgottenHallStage1.py` and run `Route.route()` """ - folder, name = route.rsplit('.', maxsplit=1) - path = f'./route/{route.replace(".", "/")}.py' + logger.hr('Route run', level=1) + if isinstance(route, RouteData): + route = route.route + logger.attr('Route', route) try: - module = importlib.import_module(f'route.{folder}.{name}') + module, func = route.split(':') + except ValueError: + logger.critical(f'Route invalid: {route}') + raise ScriptError + path = f'./{module.replace(".", "/")}.py' + + # Import route file + try: + module_obj = importlib.import_module(f'{module}') except ModuleNotFoundError: - logger.critical(f'Route file not found: {route} ({path})') + logger.critical(f'Route file not found: {module} ({path})') if not os.path.exists(path): logger.critical(f'Route file not exists: {path}') - raise RequestHumanTakeover + raise ScriptError - # config = copy.deepcopy(self.config).merge(module.Config()) - config = self.config - device = self.device + # Create route object + # Reuse the previous one + if self.route_module != module: + # config = copy.deepcopy(self.config).merge(module.Config()) + config = self.config + device = self.device + try: + self.route_obj = module_obj.Route(config=config, device=device) + except AttributeError as e: + logger.critical(e) + logger.critical(f'Route file {route} ({path}) must define class Route') + raise ScriptError + self.route_module = module + + # Get route func try: - self.route = module.Route(config=config, device=device) - return self.route.route() + func_obj = self.route_obj.__getattribute__(func) except AttributeError as e: logger.critical(e) - logger.critical(f'Route file {route} ({path}) must define Route.route()') - raise RequestHumanTakeover + logger.critical(f'Route class in {route} ({path}) does not have method {func}') + raise ScriptError + self.route_func = func + + # Run + func_obj() diff --git a/tasks/map/route/route/__init__.py b/tasks/map/route/route/__init__.py new file mode 100644 index 000000000..6a7dd76c8 --- /dev/null +++ b/tasks/map/route/route/__init__.py @@ -0,0 +1 @@ +import tasks.map.route.route.daily as ROUTE_DAILY diff --git a/tasks/map/route/route/daily.py b/tasks/map/route/route/daily.py new file mode 100644 index 000000000..09e1a4e8c --- /dev/null +++ b/tasks/map/route/route/daily.py @@ -0,0 +1,38 @@ +from tasks.map.route.base import RouteData + + +ForgottenHallStage1__route = RouteData( + name='ForgottenHallStage1__route', + route='route.daily.ForgottenHallStage1:route', + plane='Jarilo_BackwaterPass', + floor='F1', + position=(369.4, 643.4), +) +HimekoTrial__route_item_enemy = RouteData( + name='HimekoTrial__route_item_enemy', + route='route.daily.HimekoTrial:route_item_enemy', + plane='Jarilo_BackwaterPass', + floor='F1', + position=(519.9, 361.5), +) +HimekoTrial__route_item = RouteData( + name='HimekoTrial__route_item', + route='route.daily.HimekoTrial:route_item', + plane='Jarilo_BackwaterPass', + floor='F1', + position=(519.9, 361.5), +) +HimekoTrial__route_enemy = RouteData( + name='HimekoTrial__route_enemy', + route='route.daily.HimekoTrial:route_enemy', + plane='Jarilo_BackwaterPass', + floor='F1', + position=(519.9, 361.5), +) +HimekoTrial__exit = RouteData( + name='HimekoTrial__exit', + route='route.daily.HimekoTrial:exit', + plane='Jarilo_BackwaterPass', + floor='F1', + position=(519.9, 361.5), +)