diff --git a/.github/README.md b/.github/README.md index 6221d9fb5..388c842c8 100644 --- a/.github/README.md +++ b/.github/README.md @@ -39,6 +39,10 @@ __崩坏:星穹铁道 - 一条龙__ - 若您遇到商家使用本软件进行代练并收费,产生的任何问题及后果与本软件无关。 +## 相关项目 + +模拟宇宙自动化 [https://github.com/CHNZYX/Auto_Simulated_Universe](https://github.com/CHNZYX/Auto_Simulated_Universe) + ## 贡献/参与者 感谢所有参与到开发的朋友们~ diff --git a/assets/game_data/screen_info/sim_uni.yml b/assets/game_data/screen_info/sim_uni.yml index 6a30d8252..7908827c1 100644 --- a/assets/game_data/screen_info/sim_uni.yml +++ b/assets/game_data/screen_info/sim_uni.yml @@ -7,7 +7,7 @@ area_list: pc_rect: - 360 - 950 - - 410 + - 420 - 1000 text: '' lcs_percent: 0.5 @@ -30,6 +30,20 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] +- area_name: 差分宇宙-积分奖励 + id_mark: false + pc_rect: + - 130 + - 970 + - 400 + - 1025 + text: 14000/14000 (其实没用) + lcs_percent: 1.0 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 宇宙入口-进行中-1 id_mark: false pc_rect: @@ -100,6 +114,20 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] +- area_name: 差分宇宙返回按钮 + id_mark: false + pc_rect: + - 30 + - 50 + - 100 + - 120 + text: '' + lcs_percent: 0.5 + template_sub_dir: normal_world + template_id: ui_icon_10 + template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 菜单-结束并结算 id_mark: false pc_rect: @@ -134,7 +162,7 @@ area_list: - 1022 - 651 - 1324 - - 697 + - 1097 text: 确认 lcs_percent: 0.5 template_sub_dir: '' @@ -147,7 +175,7 @@ area_list: pc_rect: - 50 - 0 - - 270 + - 420 - 60 text: '' lcs_percent: 0.5 @@ -184,6 +212,20 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] +- area_name: 差分宇宙返回主界面 + id_mark: false + pc_rect: + - 900 + - 950 + - 1100 + - 1000 + text: 返回主界面 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 事件标题 id_mark: false pc_rect: @@ -271,7 +313,7 @@ area_list: - area_name: 左上角标题 id_mark: false pc_rect: - - 100 + - 50 - 15 - 350 - 100 diff --git a/pyproject.toml b/pyproject.toml index 5380bfbec..3673d598b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ version = "3.4.0" description = "星穹铁道 一条龙 | 全自动 | 自动闪避 | 自动每日 | 自动空洞 | 支持手柄" requires-python = ">=3.11.9,<=3.11.12" dependencies = [ + "psutil==6.1.1", "pyside6==6.8.0.2", "pyside6-fluent-widgets==1.7.0", "pyyaml==6.0.1", diff --git a/src/one_dragon/base/matcher/template_matcher.py b/src/one_dragon/base/matcher/template_matcher.py index e02f47daa..acc5c98ff 100644 --- a/src/one_dragon/base/matcher/template_matcher.py +++ b/src/one_dragon/base/matcher/template_matcher.py @@ -46,6 +46,7 @@ def match_template(self, source: MatLike, mask_usage = cv2.bitwise_or(mask_usage, template.mask) if mask_usage is not None else template.mask if mask is not None: mask_usage = cv2.bitwise_or(mask_usage, mask) if mask_usage is not None else mask + # cv2.imwrite('y:/template.png', template.get_image(template_type)) return cv2_utils.match_template(source, template.get_image(template_type), threshold, mask=mask_usage, only_best=only_best, ignore_inf=ignore_inf) diff --git a/src/one_dragon/base/operation/operation.py b/src/one_dragon/base/operation/operation.py index 2d84b398a..5590df370 100644 --- a/src/one_dragon/base/operation/operation.py +++ b/src/one_dragon/base/operation/operation.py @@ -1098,6 +1098,19 @@ def round_by_ocr( else: return self.round_retry(f'找不到 {target_cn}', wait=retry_wait, wait_round_time=retry_wait_round) + def ocr(self, screen: MatLike, screen_name: str, area_name: str) -> dict[str, MatchResultList]: + """在区域内使用OCR。 + + Args: + screen: 截图图像。 + screen_name: 屏幕名称。 + area_name: 区域名称。 + + Returns: + OperationRoundResult: 匹配结果。 + """ + + return screen_utils.ocr(ctx=self.ctx, screen=screen, screen_name=screen_name, area_name=area_name) def round_by_goto_screen(self, screen: Optional[np.ndarray] = None, screen_name: Optional[str] = None, success_wait: Optional[float] = None, success_wait_round: Optional[float] = None, diff --git a/src/one_dragon/base/screen/screen_utils.py b/src/one_dragon/base/screen/screen_utils.py index 05e910c79..6de3d06af 100644 --- a/src/one_dragon/base/screen/screen_utils.py +++ b/src/one_dragon/base/screen/screen_utils.py @@ -8,6 +8,7 @@ from cv2.typing import MatLike from one_dragon.base.geometry.point import Point +from one_dragon.base.matcher.match_result import MatchResultList from one_dragon.base.screen.screen_area import ScreenArea from one_dragon.base.screen.screen_info import ScreenInfo from one_dragon.utils import cv2_utils, str_utils @@ -58,26 +59,7 @@ def find_area_in_screen(ctx: OneDragonContext, screen: MatLike, area: ScreenArea find: bool = False if area.is_text_area: - if ctx.env_config.ocr_cache: - ocr_result_map = ctx.ocr_service.get_ocr_result_map( - image=screen, - color_range=area.color_range, - rect=area.rect - ) - else: - rect = area.rect - part = cv2_utils.crop_image_only(screen, rect) - - if area.color_range is None: - to_ocr = part - else: - mask = cv2.inRange(part, - np.array(area.color_range[0], dtype=np.uint8), - np.array(area.color_range[1], dtype=np.uint8)) - mask = cv2_utils.dilate(mask, 2) - to_ocr = cv2.bitwise_and(part, part, mask=mask) - - ocr_result_map = ctx.ocr.run_ocr(to_ocr) + ocr_result_map = ocr_in_screen(ctx, screen, area) for ocr_result, mrl in ocr_result_map.items(): if str_utils.find_by_lcs(gt(area.text, 'game'), ocr_result, percent=area.lcs_percent): @@ -86,6 +68,8 @@ def find_area_in_screen(ctx: OneDragonContext, screen: MatLike, area: ScreenArea elif area.is_template_area: rect = area.rect part = cv2_utils.crop_image_only(screen, rect) + # cv2.imwrite('y:/screen.png', screen) + # cv2.imwrite('y:/part.png', part) mrl = ctx.tm.match_template(part, area.template_sub_dir, area.template_id, threshold=area.template_match_threshold) @@ -94,6 +78,60 @@ def find_area_in_screen(ctx: OneDragonContext, screen: MatLike, area: ScreenArea return FindAreaResultEnum.TRUE if find else FindAreaResultEnum.FALSE +def ocr(ctx: OneDragonContext, screen: MatLike, screen_name: str, area_name: str) -> dict[str, MatchResultList]: + """ + 游戏截图中 在对应区域中进行OCR以供进一步业务判断 + :param ctx: 上下文 + :param screen: 游戏截图 + :param screen_name: 画面名称 + :param area_name: 区域名称 + :return: 结果 + """ + area: ScreenArea = ctx.screen_loader.get_area(screen_name, area_name) + return ocr_in_screen(ctx, screen, area) + + +def ocr_in_screen(ctx: OneDragonContext, screen: MatLike, area: ScreenArea) -> dict[str, MatchResultList]: + """ + 游戏截图中 在对应区域中进行OCR以供进一步业务判断 + :param ctx: 上下文 + :param screen: 游戏截图 + :param area: 区域 + :return: 结果 + """ + if (area is None) or (not area.is_text_area): + return {} + + if ctx.env_config.ocr_cache: + ocr_result_map = ctx.ocr_service.get_ocr_result_map( + image=screen, + color_range=area.color_range, + rect=area.rect + ) + else: + rect = area.rect + part = cv2_utils.crop_image_only(screen, rect) + + if area.color_range is None: + to_ocr = part + else: + mask = cv2.inRange(part, + np.array(area.color_range[0], dtype=np.uint8), + np.array(area.color_range[1], dtype=np.uint8)) + mask = cv2_utils.dilate(mask, 2) + to_ocr = cv2.bitwise_and(part, part, mask=mask) + + ocr_result_map = ctx.ocr.run_ocr(to_ocr) + for mrl in ocr_result_map.values(): + mrl.add_offset(rect.left_top) + + # cv2.imwrite('y:/part.png', part) + + if ocr_result_map is None: + ocr_result_map = {} + return ocr_result_map + + def find_and_click_area(ctx: OneDragonContext, screen: MatLike, screen_name: str, area_name: str) -> OcrClickResultEnum: """ 在一个区域匹配成功后进行点击 @@ -114,6 +152,7 @@ def find_and_click_area(ctx: OneDragonContext, screen: MatLike, screen_name: str mask = cv2_utils.dilate(mask, 5) to_ocr_part = cv2.bitwise_and(to_ocr_part, to_ocr_part, mask=mask) # cv2_utils.show_image(to_ocr_part, win_name='debug', wait=1) + # cv2.imwrite('y:/part.png', to_ocr_part) ocr_result_map = ctx.ocr.run_ocr(to_ocr_part) for ocr_result, mrl in ocr_result_map.items(): @@ -283,4 +322,4 @@ def find_by_ocr(ctx: OneDragonContext, screen: MatLike, target_cn: str, to_click = mrl.max.center break - return to_click is not None + return to_click is not None \ No newline at end of file diff --git a/src/script_chainer/__init__.py b/src/script_chainer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/script_chainer/config/__init__.py b/src/script_chainer/config/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/script_chainer/config/script_config.py b/src/script_chainer/config/script_config.py new file mode 100644 index 000000000..3a503668d --- /dev/null +++ b/src/script_chainer/config/script_config.py @@ -0,0 +1,219 @@ +import os +from enum import Enum + +from one_dragon.base.config.config_item import ConfigItem, get_config_item_from_enum +from one_dragon.base.config.yaml_config import YamlConfig + + +class CheckDoneMethods(Enum): + + GAME_CLOSED = ConfigItem(label='游戏被关闭', value='game_closed', desc='游戏被关闭时 认为任务完成') + SCRIPT_CLOSED = ConfigItem(label='脚本被关闭', value='script_closed', desc='脚本被关闭时 认为任务完成') + GAME_OR_SCRIPT_CLOSED = ConfigItem(label='游戏或脚本被关闭', value='game_or_script_closed', desc='游戏或脚本被关闭时 认为任务完成') + + +class ScriptProcessName(Enum): + + ONE_DRAGON_LAUNCHER = ConfigItem(label='一条龙', value='pythonw.exe') + BGI = ConfigItem(label='BetterGI', value='BetterGI.exe') + March7th = ConfigItem(label='三月七小助手', value='March7th Assistant.exe') + + +class GameProcessName(Enum): + + GENSHIN_IMPACT_CN = ConfigItem(label='原神', value='YuanShen.exe') + GENSHIN_IMPACT_GLOBAL = ConfigItem(label='原神(国际服)', value='GenshinImpact.exe') + STAR_RAIL_CN = ConfigItem(label='崩坏:星穹铁道', value='StarRail.exe') + ZZZ_CN = ConfigItem(label='绝区零', value='ZenlessZoneZero.exe') + + +class ScriptConfig: + + def __init__(self, + script_path: str, + script_process_name: str, + game_process_name: str, + run_timeout_seconds: int, + check_done: str, + stop_chain_when_pause_pressed: bool, + kill_script_after_done: bool, + kill_game_after_done: bool, + script_arguments: str, + notify_start: bool, + notify_done: bool, + script_working_directory: str = None, + ): + + self.idx: int = 0 # 下标 由外面控制 + self.script_path: str = script_path # 运行脚本的路径 + self.script_process_name: str = script_process_name # 运行脚本的真实进程名称 + self.game_process_name: str = game_process_name # 运行游戏的真实进程名称 + self.run_timeout_seconds: int = run_timeout_seconds # 脚本超时时间 + self.check_done: str = check_done # 怎么判断脚本已经运行完毕 + self.stop_chain_when_pause_pressed: bool = stop_chain_when_pause_pressed # 是否在按下暂停键时关闭链式调用的脚本 + self.kill_script_after_done: bool = kill_script_after_done # 是否在运行完毕之后关闭脚本 + self.kill_game_after_done: bool = kill_game_after_done # 是否在运行完毕之后关闭游戏 + self.script_arguments: str = script_arguments # 运行脚本的附加参数 + self.notify_start: bool = notify_start # 是否在脚本开始时通知 + self.notify_done: bool = notify_done # 是否在脚本完成时通知 + self.script_working_directory: str = script_working_directory # 运行脚本的工作路径 + + @property + def script_display_name(self) -> str: + return os.path.basename(self.script_path) + + @property + def game_display_name(self) -> str: + game_process_enum = [i for i in GameProcessName if i.value.value == self.game_process_name] + return game_process_enum[0].value.label if len(game_process_enum) > 0 else self.game_process_name + + @property + def check_done_display_name(self) -> str: + config = get_config_item_from_enum(CheckDoneMethods, self.check_done) + if config is not None: + return config.value + else: + return '' + + @property + def invalid_message(self) -> str: + """ + 当前配置的非法信息 + """ + if self.script_path is None or len(self.script_path) == 0: + return '脚本路径为空' + elif not os.path.exists(self.script_path): + return f'脚本路径不存在 {self.script_path}' + elif get_config_item_from_enum(CheckDoneMethods, self.check_done) is None: + return f'检查完成方式非法 {self.check_done}' + elif ( + (self.check_done == CheckDoneMethods.GAME_OR_SCRIPT_CLOSED.value.value + or self.check_done == CheckDoneMethods.GAME_CLOSED.value.value + or self.kill_game_after_done) + and (self.game_process_name is None or len(self.game_process_name) == 0) + ): + return '游戏进程名称为空' + elif ( + (self.check_done == CheckDoneMethods.GAME_OR_SCRIPT_CLOSED.value.value + or self.check_done == CheckDoneMethods.SCRIPT_CLOSED.value.value + or self.kill_script_after_done) + and (self.script_process_name is None or len(self.script_process_name) == 0) + ): + return '脚本进程名称为空' + elif self.run_timeout_seconds <= 0: + return '运行超时时间必须大于0' + + +class ScriptChainConfig(YamlConfig): + + def __init__(self, module_name: str, is_mock: bool = False): + YamlConfig.__init__( + self, + module_name, + sub_dir=['script_chain'], + is_mock=is_mock, sample=False, copy_from_sample=False, + ) + + self.script_list: list[ScriptConfig] = [ + ScriptConfig( + script_path=i.get('script_path', ''), + script_process_name=i.get('script_process_name', ''), + game_process_name=i.get('game_process_name', ''), + run_timeout_seconds=i.get('run_timeout_seconds', 3600), + check_done=i.get('check_done', ''), + kill_script_after_done=i.get('kill_script_after_done', True), + kill_game_after_done=i.get('kill_game_after_done', True), + script_arguments=i.get('script_arguments', ''), + notify_start=i.get('notify_start', True), + notify_done=i.get('notify_done', True), + ) + for i in self.get('script_list', []) + ] + self.init_idx() + + def init_idx(self) -> None: + """ + 初始化下标 + :return: + """ + for i in range(len(self.script_list)): + self.script_list[i].idx = i + + def save(self): + self.data = { + 'script_list': [ + { + 'script_path': i.script_path, + 'script_process_name': i.script_process_name, + 'game_process_name': i.game_process_name, + 'run_timeout_seconds': i.run_timeout_seconds, + 'check_done': i.check_done, + 'kill_script_after_done': i.kill_script_after_done, + 'kill_game_after_done': i.kill_game_after_done, + 'script_arguments': i.script_arguments, + 'notify_start': i.notify_start, + 'notify_done': i.notify_done, + } + for i in self.script_list + ] + } + YamlConfig.save(self) + + def add_one(self) -> ScriptConfig: + """ + 新增一个配置 并返回 + :return: + """ + new_config = ScriptConfig( + script_path='', + script_process_name='', + game_process_name='', + run_timeout_seconds=3600, + check_done=CheckDoneMethods.GAME_OR_SCRIPT_CLOSED.value.value, + kill_script_after_done=True, + kill_game_after_done=True, + script_arguments='', + notify_start=True, + notify_done=True, + ) + self.script_list.append(new_config) + self.init_idx() + self.save() + return new_config + + def delete_one(self, index: int) -> None: + """ + 删除一个配置 + :param index: + :return: + """ + if index < 0 or index >= len(self.script_list): + return + del self.script_list[index] + self.init_idx() + self.save() + + def move_up(self, index: int) -> None: + """ + 向上移动一个配置 + :param index: + :return: + """ + if index <= 0 or index >= len(self.script_list): + return + self.script_list[index], self.script_list[index - 1] = self.script_list[index - 1], self.script_list[index] + self.init_idx() + self.save() + + def update_config(self, config: ScriptConfig) -> None: + """ + 更新一个配置 + :param config: + :return: + """ + if config.idx < 0 or config.idx >= len(self.script_list): + return + + self.script_list[config.idx] = config + self.init_idx() + self.save() diff --git a/src/script_chainer/win_exe/__init__.py b/src/script_chainer/win_exe/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/script_chainer/win_exe/script_runner.py b/src/script_chainer/win_exe/script_runner.py new file mode 100644 index 000000000..e59602682 --- /dev/null +++ b/src/script_chainer/win_exe/script_runner.py @@ -0,0 +1,321 @@ +# 添加进程退出的 pid 检测方式 + +import argparse +import subprocess +import time + +import psutil + +from one_dragon.base.operation.one_dragon_context import ContextRunStateEnum +from one_dragon.utils.log_utils import log +from script_chainer.config.script_config import ScriptConfig, ScriptChainConfig, CheckDoneMethods +# # 全局变量用于Push实例 +# _push_instance = None +# +# def get_push_instance(): +# """获取Push实例,延迟初始化""" +# global _push_instance +# if _push_instance is None: +# try: +# ctx = ScriptChainerContext() +# _push_instance = Push(ctx) +# except Exception as e: +# log.error(f'初始化Push实例失败: {e}') +# _push_instance = None +# return _push_instance +# +# def get_logger(): +# logger = logging.getLogger('OneDragon') +# logger.handlers.clear() +# logger.setLevel(logging.INFO) +# +# formatter = logging.Formatter('[%(asctime)s.%(msecs)03d] [%(filename)s %(lineno)d] [%(levelname)s]: %(message)s', '%H:%M:%S') +# +# log_file_path = os.path.join(os_utils.get_path_under_work_dir('.log'), 'log.txt') +# archive_handler = TimedRotatingFileHandler(log_file_path, when='midnight', interval=1, backupCount=3, encoding='utf-8') +# archive_handler.setLevel(logging.INFO) +# archive_handler.setFormatter(formatter) +# logger.addHandler(archive_handler) +# +# return logger +# +# +# log = get_logger() +from sr_od.context.sr_context import SrContext + + +# from colorama import init, Fore, Style +# from script_chainer.context.script_chainer_context import ScriptChainerContext + + +def is_process_existed(process_name) -> bool: + """ + 判断进程是否存在 + """ + if process_name is None or len(process_name) == 0: + return False + + for proc in psutil.process_iter(['pid', 'name']): + try: + if proc.info['name'] == process_name: + return True + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + pass + + return False + + +def kill_process(process_name): + """ + 关闭一个进程 + """ + if process_name is None or len(process_name) == 0: + return + + for proc in psutil.process_iter(['pid', 'name']): + try: + if proc.info['name'] == process_name: + proc.kill() + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + pass + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('--chain', type=str, default='01', help='脚本链名称') + parser.add_argument('--shutdown', action='store_true', help='结束后关机') + + return parser.parse_args() + + +def print_message(message: str, level="INFO"): + if level == "INFO": + log.info(message) + elif level == "WARN": + log.warn(message) + elif level == "ERROR": + log.error(message) + + +def is_pid_running(pid): + try: + # 检查进程是否存在 + process = psutil.Process(pid) + + # 进一步验证(防止PID复用):获取进程创建时间 + create_time = process.create_time() + + # 返回进程状态(running/sleeping等) + return { + "alive": True, + "status": process.status(), + "create_time": create_time + } + + except psutil.NoSuchProcess: + return {"alive": False, "reason": "process_exited"} + except psutil.AccessDenied: + # 权限不足但进程可能存在,标记为"不确定" + return {"alive": None, "reason": "access_denied"} + except psutil.ZombieProcess: + return {"alive": False, "reason": "zombie_process"} + except Exception as e: + # 捕获所有其他异常(如系统调用失败) + return {"alive": None, "reason": f"error: {str(e)}"} + + +def run_script(script_config: ScriptConfig, ctx: SrContext = None) -> None: + """ + 运行脚本 + """ + script_path = script_config.script_path + args = script_config.script_arguments + + invalid_message = script_config.invalid_message + if invalid_message is not None: + print_message(f'脚本配置不合法 跳过运行 {invalid_message}') + return + + command = [script_path] + if args and args.strip(): + command.extend(args.split()) + + start_time = time.time() + + subprocess_created: bool = False + subprocess_create_time: float = start_time # 子进程创建的时间 + process = None + + while True: + now = time.time() + if process is None: + try: + subprocess_create_time = now + if script_config.script_working_directory is None: + process = subprocess.Popen(command) + else: + process = subprocess.Popen(command,cwd=script_config.script_working_directory) + print_message(f'创建脚本子进程 {script_path}') + except Exception: + print_message(f'创建子进程失败 {script_path}') + log.error(f'创建子进程失败 {script_path}', exc_info=True) + else: + process_result = process.poll() + if process_result is None: + process_result_display = '运行中' + elif process_result == 0: + process_result_display = '运行成功' + else: + process_result_display = '运行失败' + + print_message(f'检测脚本子进程运行 {process_result_display}') + # None = 子进程正在运行中 未返回结果 + # 0 = 子进程运行结束 可能脚本的启动器自身启动了其它进程 + if process_result is None or process_result == 0: + if now - subprocess_create_time >= 5: # 已经运行超过5秒 + subprocess_created = True + else: # 子进程运行结束 返回异常 尝试重新调用 + process = None + + if subprocess_created: # 子进程正常 + break + + if now - start_time > 20: # 超时 + break + + time.sleep(1) + + if not subprocess_created: + print_message(f'子进程创建失败 {script_path}') + return + else: + print_message(f'脚本子进程创建成功 {script_path}', level='PASS') + + script_ever_existed: bool = False # 脚本进程是否存在 + game_ever_existed: bool = False # 游戏进程是否存在 + is_done: bool = False + while True: + if ((script_config.stop_chain_when_pause_pressed and ctx.context_running_state == ContextRunStateEnum.PAUSE) + or ctx.context_running_state == ContextRunStateEnum.STOP): + break + + game_current_existed: bool = is_process_existed(script_config.game_process_name) + game_closed = game_ever_existed and not game_current_existed + game_ever_existed = game_ever_existed or game_current_existed + + if len(script_config.game_display_name) > 0: + if not game_ever_existed: + print_message(f'等待打开 {script_config.game_display_name}') + elif game_current_existed: + print_message(f'正在运行 {script_config.game_display_name}', level='PASS') + else: + print_message(f'运行结束 {script_config.game_display_name}', level='PASS') + else: + print_message(f'等待 {script_config.check_done_display_name}') + + # 添加进程退出的 pid 检测方式 + if script_config.script_process_name == 'None': + pid_status = is_pid_running(process.pid) + script_current_existed: bool = pid_status.get('alive') and (pid_status.get('create_time') is not None) and pid_status.get('create_time') < subprocess_create_time + 5 + else: + script_current_existed: bool = is_process_existed(script_config.script_process_name) + script_closed = script_ever_existed and not script_current_existed + script_ever_existed = script_ever_existed or script_current_existed + + if script_config.check_done == CheckDoneMethods.GAME_OR_SCRIPT_CLOSED.value.value: + if game_closed or script_closed: + is_done = True + print_message(f'游戏或脚本被关闭 {script_config.game_display_name}', level='PASS') + elif script_config.check_done == CheckDoneMethods.GAME_CLOSED.value.value: + if game_closed: + is_done = True + print_message(f'游戏被关闭 {script_config.game_display_name}', level='PASS') + elif script_config.check_done == CheckDoneMethods.SCRIPT_CLOSED.value.value: + if script_closed: + is_done = True + print_message(f'脚本被关闭 {script_config.script_display_name}', level='PASS') + else: + print_message(f'未知的检查结束方式 {script_config.check_done}', level='ERROR') + is_done = True + + now = time.time() + + if now - start_time > script_config.run_timeout_seconds: + is_done = True + print_message(f'脚本运行超时 {script_config.script_display_name}', level='ERROR') + + if is_done: + break + + time.sleep(1) + + if script_config.kill_script_after_done: + print_message(f'尝试关闭脚本进程 {script_config.script_process_name}') + try: + process.kill() + except Exception: + log.error('关闭脚本子进程失败', exc_info=True) + + try: + if script_config.script_process_name is not None and len(script_config.script_process_name) > 0: + kill_process(script_config.script_process_name) + except Exception: + log.error('关闭脚本进程失败', exc_info=True) + + if script_config.kill_game_after_done: + print_message(f'尝试关闭游戏进程 {script_config.game_process_name}') + try: + if script_config.game_process_name is not None and len(script_config.game_process_name) > 0: + kill_process(script_config.game_process_name) + except Exception: + log.error('关闭游戏进程失败', exc_info=True) + + +def run(): + # colorama。init(autoreset=True) + args = parse_args() + module_name: str = args.chain + chain_config: ScriptChainConfig = ScriptChainConfig(module_name) + # push_instance = get_push_instance() + try: + if not chain_config.is_file_exists(): + print_message(f'脚本链配置不存在 {module_name}', "ERROR") + else: + for i in range(len(chain_config.script_list)): + script_config = chain_config.script_list[i] + # if script_config.notify_start: + # if push_instance is not None: + # push_instance.send( + # content=f'脚本链 {module_name} 开始运行: {script_config.script_display_name}' + # ) + run_script(script_config) + # if script_config.notify_done: + # if push_instance is not None: + # push_instance.send( + # content=f'脚本链 {module_name} 运行结束: {script_config.script_display_name}' + # ) + if i < len(chain_config.script_list) - 1: + print_message('10秒后开始下一个脚本') + time.sleep(10) + + print_message('已完成全部脚本') + + # if args.shutdown: + # cmd_utils.shutdown_sys(60) + # print_message('准备关机') + + # print_message('5秒后关闭本窗口') + # time.sleep(5) + finally: + # # 清理Push资源 + # global _push_instance + # if _push_instance is not None: + # try: + # _push_instance.ctx.after_app_shutdown() + # except Exception as e: + # log.error(f'清理Push资源失败: {e}') + pass + + +if __name__ == '__main__': + run() diff --git a/src/sr_od/app/sim_uni/operations/auto_run/sim_uni_wait_level_start.py b/src/sr_od/app/sim_uni/operations/auto_run/sim_uni_wait_level_start.py index 8dca87f57..2782f031b 100644 --- a/src/sr_od/app/sim_uni/operations/auto_run/sim_uni_wait_level_start.py +++ b/src/sr_od/app/sim_uni/operations/auto_run/sim_uni_wait_level_start.py @@ -58,12 +58,12 @@ def check_screen(self) -> OperationRoundResult: return self.round_wait(wait=1) else: return self.round_fail(status=op_result.status, data=op_result.data) - elif state == sim_uni_screen_state.ScreenState.SIM_CURIOS.value: - op = SimUniChooseCurio(self.ctx, self.config) - op_result = op.execute() - if op_result.success: - return self.round_wait() - else: - return self.round_by_op_result(op_result) + # elif state == sim_uni_screen_state.ScreenState.SIM_CURIOS.value: + # op = SimUniChooseCurio(self.ctx, self.config) + # op_result = op.execute() + # if op_result.success: + # return self.round_wait() + # else: + # return self.round_by_op_result(op_result) else: return self.round_retry('无法判断当前画面状态', wait=1) diff --git a/src/sr_od/app/sim_uni/operations/bless/bless_utils.py b/src/sr_od/app/sim_uni/operations/bless/bless_utils.py index 4942fe675..ca0278be7 100644 --- a/src/sr_od/app/sim_uni/operations/bless/bless_utils.py +++ b/src/sr_od/app/sim_uni/operations/bless/bless_utils.py @@ -97,6 +97,23 @@ def get_bless_pos(ctx: SrContext, screen: MatLike) -> list[SimUniBlessPos]: result_list = [i for i in result_list if i.find_path] log.info('识别到祝福 %s', [i.bless.title for i in result_list]) + # 如果没找到选项, 直接找固定高度范围内的字, 直接点 + if len(result_list) == 0: + rect = [100, 400, 1700, 900] + for ocr_result in ocr_result_list: + if ocr_result.rect.x1 > rect[0] and ocr_result.rect.y1 > rect[1] \ + and ocr_result.rect.x2 < rect[2] and ocr_result.rect.y2 < rect[3]: + pos = SimUniBlessPos( + bless=bless_list[0], + rect=Rect( + ocr_result.x, + ocr_result.y, + ocr_result.x+ocr_result.w, + ocr_result.y+ocr_result.h + ) + ) + result_list.append(pos) + return result_list @@ -171,10 +188,11 @@ def get_bless_by_priority(bless_list: List[SimUniBless], config: Optional[SimUni if bless.name.endswith('000'): # 命途内选最高级的祝福 for bless_level in SimUniBlessLevel: for idx, opt_bless in enumerate(bless_list): - if opt_bless.level == bless_level and opt_bless.path == bless.value.path: - if idx_priority[idx] == 99: - idx_priority[idx] = cnt - cnt += 1 + if hasattr(opt_bless, "level") and hasattr(opt_bless, "path") \ + and opt_bless.level == bless_level and opt_bless.path == bless.value.path and \ + idx_priority[idx] == 99: + idx_priority[idx] = cnt + cnt += 1 else: # 命中优先级的 for idx, opt_bless in enumerate(bless_list): if opt_bless == bless.value: @@ -188,10 +206,11 @@ def get_bless_by_priority(bless_list: List[SimUniBless], config: Optional[SimUni if bless.name.endswith('000'): # 命途内选最高级的祝福 for bless_level in SimUniBlessLevel: for idx, opt_bless in enumerate(bless_list): - if opt_bless.level == bless_level and opt_bless.path == bless.value.path: - if idx_priority[idx] == 99: - idx_priority[idx] = cnt - cnt += 1 + if hasattr(opt_bless, "level") and hasattr(opt_bless, "path") \ + and opt_bless.level == bless_level and opt_bless.path == bless.value.path and \ + idx_priority[idx] == 99: + idx_priority[idx] = cnt + cnt += 1 else: # 命中优先级的 for idx, opt_bless in enumerate(bless_list): if opt_bless == bless.value: @@ -203,8 +222,9 @@ def get_bless_by_priority(bless_list: List[SimUniBless], config: Optional[SimUni # 优先级无法命中的情况 随便选最高级的祝福 for bless_level in SimUniBlessLevel: for idx, opt_bless in enumerate(bless_list): - if opt_bless.level == bless_level: - if idx_priority[idx] == 99: + # 仅处理有效的 Bless + if hasattr(opt_bless, "level"): + if opt_bless.level == bless_level and idx_priority[idx] == 99: idx_priority[idx] = cnt cnt += 1 diff --git a/src/sr_od/app/sim_uni/operations/bless/sim_uni_choose_bless.py b/src/sr_od/app/sim_uni/operations/bless/sim_uni_choose_bless.py index 10ea9fb68..e12a5b220 100644 --- a/src/sr_od/app/sim_uni/operations/bless/sim_uni_choose_bless.py +++ b/src/sr_od/app/sim_uni/operations/bless/sim_uni_choose_bless.py @@ -102,12 +102,14 @@ def choose(self) -> OperationRoundResult: ) if result.is_success: return self.round_success(status=result.status, wait=0.1) - if self.before_level_start: - log.info('选择祝福后未识别到确认 尝试固定位置点击 第一间开始前') - confirm_point = SimUniChooseBless.CONFIRM_BEFORE_LEVEL_BTN.center - else: - log.info('选择祝福后未识别到确认 尝试固定位置点击') - confirm_point = SimUniChooseBless.CONFIRM_BTN.center + + # 选择祝福时先点右下角再点中间, 防止点到查看已有方程 + confirm_point = SimUniChooseBless.CONFIRM_BTN.center + log.info('选择祝福后未识别到确认 尝试固定位置点击 右下角') + self.ctx.controller.click(confirm_point) + time.sleep(0.28) + confirm_point = SimUniChooseBless.CONFIRM_BEFORE_LEVEL_BTN.center + log.info('选择祝福后未识别到确认 尝试固定位置点击 中间') self.ctx.controller.click(confirm_point) self.choose_bless_time = time.time() return self.round_success(wait=0.1) diff --git a/src/sr_od/app/sim_uni/operations/curio/sim_uni_choose_curio.py b/src/sr_od/app/sim_uni/operations/curio/sim_uni_choose_curio.py index 4d1de61d2..69e1c40c8 100644 --- a/src/sr_od/app/sim_uni/operations/curio/sim_uni_choose_curio.py +++ b/src/sr_od/app/sim_uni/operations/curio/sim_uni_choose_curio.py @@ -74,25 +74,26 @@ def handle_init(self) -> Optional[OperationRoundResult]: return None - @node_from(from_name='确认后画面判断', status=sim_uni_screen_state.ScreenState.SIM_CURIOS.value) - @operation_node(name='选择奇物', is_start_node=True) - def _choose_curio(self) -> OperationRoundResult: - screen = self.last_screenshot - - if not self.first_screen_check or not self.skip_first_screen_check: - self.first_screen_check = False - if not sim_uni_screen_state.in_sim_uni_choose_curio(screen, self.ctx.ocr): - return self.round_retry('未在模拟宇宙-选择奇物页面') - - curio_pos_list: List[MatchResult] = self._get_curio_pos(screen) - if len(curio_pos_list) == 0: - return self.round_retry('未识别到奇物', wait=1) - - target_curio_pos: Optional[MatchResult] = self._get_curio_to_choose(curio_pos_list) - self.ctx.controller.click(target_curio_pos.center) - time.sleep(0.25) - self.ctx.controller.click(SimUniChooseCurio.CONFIRM_BTN.center) - return self.round_success(wait=0.1) + # 选择奇物合并到选择祝福中, 并且固定选择第一项 + # @node_from(from_name='确认后画面判断', status=sim_uni_screen_state.ScreenState.SIM_CURIOS.value) + # @operation_node(name='选择奇物', is_start_node=True) + # def _choose_curio(self) -> OperationRoundResult: + # screen = self.last_screenshot + # + # if not self.first_screen_check or not self.skip_first_screen_check: + # self.first_screen_check = False + # if not sim_uni_screen_state.in_sim_uni_choose_curio(screen, self.ctx.ocr): + # return self.round_retry('未在模拟宇宙-选择奇物页面') + # + # curio_pos_list: List[MatchResult] = self._get_curio_pos(screen) + # if len(curio_pos_list) == 0: + # return self.round_retry('未识别到奇物', wait=1) + # + # target_curio_pos: Optional[MatchResult] = self._get_curio_to_choose(curio_pos_list) + # self.ctx.controller.click(target_curio_pos.center) + # time.sleep(0.25) + # self.ctx.controller.click(SimUniChooseCurio.CONFIRM_BTN.center) + # return self.round_success(wait=0.1) def _get_curio_pos(self, screen: MatLike) -> List[MatchResult]: """ @@ -196,15 +197,15 @@ def _check_after_confirm(self) -> OperationRoundResult: # 未知情况都先点击一下 self.round_by_click_area('模拟宇宙', '点击空白处关闭') return self.round_retry('未能判断当前页面', wait=1) - elif state == sim_uni_screen_state.ScreenState.SIM_CURIOS.value: - # 还在选奇物的画面 说明上一步没有选择到奇物 - # 只有2个奇物的时候,使用3个奇物的第1个位置 可能会识别到奇物(名字位置重叠) 这时候点击第1个位置是会失败的 - # 所以每次重试 curio_cnt_type-=1 即重试的时候 需要排除调3个奇物的位置 尝试2个奇物的位置 - self.curio_cnt_type -= 1 - if self.curio_cnt_type <= 0: - return self.round_fail("点击确认失败") - else: - return self.round_success(sim_uni_screen_state.ScreenState.SIM_CURIOS.value) + # elif state == sim_uni_screen_state.ScreenState.SIM_CURIOS.value: + # # 还在选奇物的画面 说明上一步没有选择到奇物 + # # 只有2个奇物的时候,使用3个奇物的第1个位置 可能会识别到奇物(名字位置重叠) 这时候点击第1个位置是会失败的 + # # 所以每次重试 curio_cnt_type-=1 即重试的时候 需要排除调3个奇物的位置 尝试2个奇物的位置 + # self.curio_cnt_type -= 1 + # if self.curio_cnt_type <= 0: + # return self.round_fail("点击确认失败") + # else: + # return self.round_success(sim_uni_screen_state.ScreenState.SIM_CURIOS.value) elif state in [sim_uni_screen_state.ScreenState.SIM_BLESS.value, sim_uni_screen_state.ScreenState.SIM_DROP_BLESS.value, sim_uni_screen_state.ScreenState.SIM_DROP_CURIOS.value]: diff --git a/src/sr_od/app/sim_uni/operations/sim_uni_enter_fight.py b/src/sr_od/app/sim_uni/operations/sim_uni_enter_fight.py index c11cfec33..02c429252 100644 --- a/src/sr_od/app/sim_uni/operations/sim_uni_enter_fight.py +++ b/src/sr_od/app/sim_uni/operations/sim_uni_enter_fight.py @@ -290,8 +290,8 @@ def _handle_not_in_world(self, screen: MatLike) -> OperationRoundResult: ) if state == sim_uni_screen_state.ScreenState.SIM_BLESS.value: return self._choose_bless() - elif state == sim_uni_screen_state.ScreenState.SIM_CURIOS.value: - return self._choose_curio() + # elif state == sim_uni_screen_state.ScreenState.SIM_CURIOS.value: + # return self._choose_curio() elif state == sim_uni_screen_state.ScreenState.EMPTY_TO_CLOSE.value: self.round_by_click_area('模拟宇宙', '点击空白处关闭') return self.round_wait(wait=1) diff --git a/src/sr_od/app/sim_uni/operations/sim_uni_event.py b/src/sr_od/app/sim_uni/operations/sim_uni_event.py index 861f61e14..6e5dfac69 100644 --- a/src/sr_od/app/sim_uni/operations/sim_uni_event.py +++ b/src/sr_od/app/sim_uni/operations/sim_uni_event.py @@ -294,16 +294,17 @@ def _upgrade_bless(self) -> OperationRoundResult: op = SimUniUpgradeBless(self.ctx) return self.round_by_op_result(op.execute()) - @node_from(from_name='确定后判断', status=sim_uni_screen_state.ScreenState.SIM_CURIOS.value) - @operation_node(name='选择奇物') - def _choose_curio(self) -> OperationRoundResult: - op = SimUniChooseCurio(self.ctx, config=self.config) - op_result = op.execute() - - if op_result.success: - return self.round_success() - else: - return self.round_retry(status=op_result.status) + # 选择奇物合并到选择祝福中, 并且固定选择第一项 + # @node_from(from_name='确定后判断', status=sim_uni_screen_state.ScreenState.SIM_CURIOS.value) + # @operation_node(name='选择奇物') + # def _choose_curio(self) -> OperationRoundResult: + # op = SimUniChooseCurio(self.ctx, config=self.config) + # op_result = op.execute() + # + # if op_result.success: + # return self.round_success() + # else: + # return self.round_retry(status=op_result.status) @node_from(from_name='确定后判断', status=sim_uni_screen_state.ScreenState.SIM_DROP_CURIOS.value) @operation_node(name='丢弃奇物') diff --git a/src/sr_od/app/sim_uni/operations/sim_uni_exit.py b/src/sr_od/app/sim_uni/operations/sim_uni_exit.py index 30d9a6722..ab1345975 100644 --- a/src/sr_od/app/sim_uni/operations/sim_uni_exit.py +++ b/src/sr_od/app/sim_uni/operations/sim_uni_exit.py @@ -16,7 +16,7 @@ class SimUniExit(SrOperation): STATUS_EXIT_CLICKED: ClassVar[str] = '点击结算' STATUS_BACK_MENU: ClassVar[str] = '返回菜单' - def __init__(self, ctx: SrContext): + def __init__(self, ctx: SrContext, is_in_x: bool = False): """ 模拟宇宙 结束并结算 :param ctx: @@ -26,6 +26,8 @@ def __init__(self, ctx: SrContext): (gt('模拟宇宙', 'game'), gt('结束并结算')) ) + # 差分宇宙中需要点一个返回主界面 + self.is_in_x = is_in_x @operation_node(name='画面识别', node_max_retry_times=10, is_start_node=True) def check_screen(self) -> OperationRoundResult: @@ -96,12 +98,15 @@ def click_confirm(self) -> OperationRoundResult: @operation_node(name='点击空白处继续') def click_empty(self) -> OperationRoundResult: """ - 结算画面点击空白 - :return: + 结算画面点击空白; 差分宇宙中需要点击返回主界面 """ screen = self.last_screenshot - return self.round_by_find_and_click_area(screen, '模拟宇宙', '点击空白处继续', - success_wait=2, retry_wait=1) + if self.is_in_x: + return self.round_by_find_and_click_area(screen, '模拟宇宙', '差分宇宙返回主界面', + success_wait=2, retry_wait=1) + else: + return self.round_by_find_and_click_area(screen, '模拟宇宙', '点击空白处继续', + success_wait=2, retry_wait=1) def __debug(): diff --git a/src/sr_od/app/sim_uni/sim_uni_app.py b/src/sr_od/app/sim_uni/sim_uni_app.py index 3595f8e23..2a4aeafc3 100644 --- a/src/sr_od/app/sim_uni/sim_uni_app.py +++ b/src/sr_od/app/sim_uni/sim_uni_app.py @@ -1,18 +1,25 @@ +import os +import shutil +import time from typing import Optional, ClassVar, Callable +from one_dragon.base.operation.one_dragon_context import ContextRunStateEnum from one_dragon.base.operation.operation_edge import node_from from one_dragon.base.operation.operation_node import operation_node -from one_dragon.base.operation.operation_round_result import OperationRoundResult +from one_dragon.base.operation.operation_round_result import OperationRoundResult, OperationRoundResultEnum +from one_dragon.utils import os_utils from one_dragon.utils.i18_utils import gt from one_dragon.utils.log_utils import log +from script_chainer.config.script_config import ScriptConfig +from script_chainer.win_exe.script_runner import run_script from sr_od.app.sim_uni import sim_uni_screen_state +from sr_od.app.sim_uni.operations.auto_run.sim_uni_run_world import SimUniRunWorld from sr_od.app.sim_uni.operations.bless.sim_uni_choose_path import SimUniChoosePath from sr_od.app.sim_uni.operations.entry.choose_sim_uni_diff import ChooseSimUniDiff from sr_od.app.sim_uni.operations.entry.choose_sim_uni_num import ChooseSimUniNum -from sr_od.app.sim_uni.operations.entry.sim_uni_start import SimUniStart from sr_od.app.sim_uni.operations.entry.sim_uni_claim_weekly_reward import SimUniClaimWeeklyReward +from sr_od.app.sim_uni.operations.entry.sim_uni_start import SimUniStart from sr_od.app.sim_uni.operations.sim_uni_exit import SimUniExit -from sr_od.app.sim_uni.operations.auto_run.sim_uni_run_world import SimUniRunWorld from sr_od.app.sim_uni.sim_uni_const import SimUniWorldEnum, SimUniPath from sr_od.app.sr_application import SrApplication from sr_od.context.sr_context import SrContext @@ -51,6 +58,34 @@ def __init__(self, ctx: SrContext, self.not_found_in_survival_times: int = 0 # 在生存索引中找不到模拟宇宙的次数 self.all_finished: bool = False + # 在差分宇宙入口处检查积分奖励 + def _check_points_reward(self) -> OperationRoundResult: + last_count_18000 = -1 + # 默认设置找不到 18000 返回重试 + result = self.round_retry('未找到积分奖励', wait=1) + # 识别到两次一致的结果就退出循环 + for _ in range(10): + ocr_result_map = self.ocr(self.screenshot(), '模拟宇宙', '差分宇宙-积分奖励') + + count_18000 = 0 + for ocr_result, _mrl in ocr_result_map.items(): + count_18000 += ocr_result.count('18000') + if last_count_18000 != count_18000: + last_count_18000 = count_18000 + time.sleep(1) + continue + + if count_18000 == 1: + # 只有一个 18000 + result = self.round_fail('未打满积分奖励') + elif count_18000 == 2: + # 如果周计划未完成, 设置为已完成 + if not self.ctx.sim_uni_record.points_reward_complete: + self.ctx.sim_uni_record.points_reward_complete = True + result = self.round_success('已打满积分奖励') + break + return result + @node_from(from_name='自动宇宙') @node_from(from_name='异常退出') @operation_node(name='检查运行次数', is_start_node=True) @@ -76,6 +111,7 @@ def _check_times(self) -> OperationRoundResult: return self.round_success() @node_from(from_name='检查运行次数') + @node_from(from_name='调用差分宇宙自动化', success=False) @operation_node(name='识别初始画面') def _check_initial_screen(self) -> OperationRoundResult: screen = self.last_screenshot @@ -84,6 +120,8 @@ def _check_initial_screen(self) -> OperationRoundResult: if state == sim_uni_screen_state.ScreenState.SIM_TYPE_NORMAL.value: if self.all_finished: return self.round_success(SimUniApp.STATUS_TO_WEEKLY_REWARD) + if self.ctx.sim_uni_config.weekly_uni_num == 'WORLD_X': + state = sim_uni_screen_state.ScreenState.SIM_TYPE_X.value # 差分宇宙 return self.round_success(state) @@ -92,12 +130,120 @@ def _check_initial_screen(self) -> OperationRoundResult: def transport(self) -> OperationRoundResult: tab = self.ctx.guide_data.best_match_tab_by_name(gt('旷宇纷争', 'game')) category = self.ctx.guide_data.best_match_category_by_name(gt('差分宇宙', 'game'), tab) - mission = self.ctx.guide_data.best_match_mission_by_name('前往模拟宇宙', category) + + if self.ctx.sim_uni_config.weekly_uni_num == 'WORLD_X': + # 差分宇宙 + mission = self.ctx.guide_data.best_match_mission_by_name('前往参与', category) + state = sim_uni_screen_state.ScreenState.SIM_TYPE_X.value + else: + # 模拟宇宙 + mission = self.ctx.guide_data.best_match_mission_by_name('前往模拟宇宙', category) + state = sim_uni_screen_state.ScreenState.SIM_TYPE_NORMAL.value + op = GuideTransport(self.ctx, mission) - return self.round_by_op_result(op.execute()) + op_result = op.execute() + if not op_result.success: + return self.round_by_op_result(op_result) + return self.round_success(state) + + @node_from(from_name='识别初始画面', status=sim_uni_screen_state.ScreenState.SIM_TYPE_X.value) # 最开始已经在模拟宇宙入口了 + @node_from(from_name='传送', status=sim_uni_screen_state.ScreenState.SIM_TYPE_X.value) # 传送到差分宇宙, 调用差分宇宙自动化脚本 + @operation_node(name='调用差分宇宙自动化') + def _execute_sim_universe_x(self) -> OperationRoundResult: + # 如果只要求打满奖励, 识别是否 18000/18000了 + if self.ctx.sim_uni_config.only_points_reward: + points_reward = self._check_points_reward() + if points_reward.result != OperationRoundResultEnum.FAIL: + return points_reward + + work_dir = os_utils.get_work_dir() + plugin_path = os.path.join(work_dir, *['plugins', 'Auto_Simulated_Universe']) + script_file = os.path.join(plugin_path, 'diver.py') + if not os.path.exists(plugin_path): + return self.round_fail(f'差分宇宙插件目录不存在: {plugin_path}') + if not os.path.exists(script_file): + return self.round_fail(f'差分宇宙脚本不存在: {script_file}') + + # (前面带一个空格) 运行参数 + args: str = ' --speed --cpu' # 强制使用cpu避免卡死机 + script_file += args + + # 使用自身的 python 环境链式启动脚本 + script_config = ScriptConfig( + script_path=self.ctx.python_service.env_config.python_path, + script_arguments=script_file, + script_working_directory=plugin_path, + script_process_name='None', # 脚本退出检测需要使用 pid 而不是 python.exe, 故此处填 None + game_process_name='', + run_timeout_seconds=2000, + check_done='script_closed', + stop_chain_when_pause_pressed=True, # 在按下暂停键时停止链式启动的脚本运行 + kill_script_after_done=True, + kill_game_after_done=False, + notify_start=False, + notify_done=False, + ) + BackToNormalWorldPlus(self.ctx).execute() + + # 删除运行记录 + plugin_run_result_path = os.path.join(plugin_path, 'logs', 'notif.txt') + if os.path.exists(plugin_run_result_path): + with open(plugin_run_result_path, 'w', encoding='utf-8') as file: + pass # 不写入任何内容, 仅清空 + # os.remove(plugin_run_result_path) + + # 复制配置文件 + config_file_path = os.path.join(work_dir, + *['config', '%02d' % self.ctx.current_instance_idx, 'sim_universe_plugin.yml']) + # 如果没有此用户的配置文件, 则复制默认配置文件到用户文件夹中; 默认 info.yml 存在 + plugin_config_file_path = os.path.join(plugin_path, 'info.yml') + if not os.path.exists(plugin_config_file_path): + return self.round_fail(f'差分宇宙默认配置文件不存在: {plugin_config_file_path}') + if not os.path.exists(config_file_path): + shutil.copy(plugin_config_file_path, config_file_path) + shutil.copy(config_file_path, plugin_config_file_path) + + # 运行脚本, 重试次数 = 3 + retry_count = 0 + max_retries = 3 + while retry_count < max_retries: + # auto_simulated_universe 运行前, 检查是否按了暂停/停止 + if self.ctx.context_running_state == ContextRunStateEnum.STOP: + break + elif self.ctx.context_running_state == ContextRunStateEnum.PAUSE: + time.sleep(1) + continue + # 阻塞式运行 auto_simulated_universe, 并在按下暂停键时结束其运行 + run_script(script_config, self.ctx) + # auto_simulated_universe 运行结束, 检查是否按了暂停键, 如果按了暂停键则认为是人为中止其运行从而忽略其返回值 + if self.ctx.context_running_state == ContextRunStateEnum.PAUSE: + time.sleep(1) + continue + retry_count += 1 + + # 进程退出, 检查运行情况 + for _ in range(3): + try: + with open(plugin_run_result_path, 'r', encoding='utf-8') as file: + line = file.readline().strip() + completed_num = int(line) if line else 0 + break + except (ValueError, FileNotFoundError) as e: + log.error(f'读取运行结果文件失败: {e}') + completed_num = 0 + time.sleep(5) + + if completed_num > 0: + # 记录完成次数, 返回失败然后下次运行即可领奖励 + self.ctx.sim_uni_record.add_elite_times() + return self.round_by_op_result(self.op_fail("已打完, 前往领奖励")) + + op = BackToNormalWorldPlus(self.ctx) + op.execute() + return self.round_by_op_result(self.op_fail("失败")) @node_from(from_name='识别初始画面', status=sim_uni_screen_state.ScreenState.SIM_TYPE_NORMAL.value) # 最开始已经在模拟宇宙入口了 - @node_from(from_name='传送') + @node_from(from_name='传送', status=sim_uni_screen_state.ScreenState.SIM_TYPE_NORMAL.value) @operation_node(name='选择宇宙') def _choose_sim_uni_num(self) -> OperationRoundResult: if self.specified_uni_num is None: @@ -172,6 +318,7 @@ def _exception_exit(self) -> OperationRoundResult: return self.round_by_op_result(op.execute()) @node_from(from_name='识别初始画面', status=STATUS_TO_WEEKLY_REWARD) + @node_from(from_name='调用差分宇宙自动化', success=True) @operation_node(name='领取每周奖励') def check_reward_before_exit(self) -> OperationRoundResult: op = SimUniClaimWeeklyReward(self.ctx) diff --git a/src/sr_od/app/sim_uni/sim_uni_config.py b/src/sr_od/app/sim_uni/sim_uni_config.py index bd3a2ce62..01c9a4359 100644 --- a/src/sr_od/app/sim_uni/sim_uni_config.py +++ b/src/sr_od/app/sim_uni/sim_uni_config.py @@ -48,13 +48,17 @@ def sim_uni_08(self) -> str: def sim_uni_09(self) -> str: return self.get('sim_uni_09', '05') + @property + def sim_uni_100(self) -> str: + return self.get('sim_uni_100', '05') + @property def weekly_uni_num(self) -> str: """ 每周挑战的第几宇宙设置 :return: """ - return self.get('weekly_uni_num', SimUniWorldEnum.WORLD_08.name) + return self.get('weekly_uni_num', SimUniWorldEnum.WORLD_X.name) @weekly_uni_num.setter def weekly_uni_num(self, new_value: str): @@ -62,7 +66,7 @@ def weekly_uni_num(self, new_value: str): @property def weekly_uni_num_adapter(self) -> YamlConfigAdapter: - return YamlConfigAdapter(self, 'weekly_uni_num', SimUniWorldEnum.WORLD_08.name) + return YamlConfigAdapter(self, 'weekly_uni_num', SimUniWorldEnum.WORLD_X.name) @property def weekly_uni_diff(self) -> int: @@ -80,6 +84,22 @@ def weekly_uni_diff(self, new_value: int): def weekly_uni_diff_adapter(self) -> YamlConfigAdapter: return YamlConfigAdapter(self, 'weekly_uni_diff', 0) + @property + def only_points_reward(self) -> bool: + """ + 是否仅完成周期奖励 (打满14000点后退出) + :return: + """ + return self.get('only_points_reward', True) + + @only_points_reward.setter + def only_points_reward(self, new_value: bool): + self.update('only_points_reward', new_value) + + @property + def only_points_reward_adapter(self) -> YamlConfigAdapter: + return YamlConfigAdapter(self, 'only_points_reward', True) + @property def elite_weekly_times(self) -> int: """ diff --git a/src/sr_od/app/sim_uni/sim_uni_const.py b/src/sr_od/app/sim_uni/sim_uni_const.py index 6bf5680e9..558729e3a 100644 --- a/src/sr_od/app/sim_uni/sim_uni_const.py +++ b/src/sr_od/app/sim_uni/sim_uni_const.py @@ -51,6 +51,7 @@ class SimUniWorldEnum(Enum): WORLD_07 = SimUniWorld(7, '第七宇宙', 4) WORLD_08 = SimUniWorld(8, '第八宇宙', 4) WORLD_09 = SimUniWorld(9, '第九宇宙', 4) + WORLD_X = SimUniWorld(100, '差分宇宙', -1) class OrnamentExtraction: diff --git a/src/sr_od/app/sim_uni/sim_uni_run_record.py b/src/sr_od/app/sim_uni/sim_uni_run_record.py index 3fb5589f5..5ade42a8f 100644 --- a/src/sr_od/app/sim_uni/sim_uni_run_record.py +++ b/src/sr_od/app/sim_uni/sim_uni_run_record.py @@ -21,12 +21,14 @@ def run_status_under_now(self): if self._should_reset_by_dt(): if os_utils.is_monday(self.get_current_dt()): return AppRunRecord.STATUS_WAIT - elif self.elite_weekly_times >= self.config.elite_weekly_times: # 已完成本周次数 + elif (self.points_reward_complete and self.config.only_points_reward) \ + or self.elite_weekly_times >= self.config.elite_weekly_times: # 已完成本周次数 return AppRunRecord.STATUS_SUCCESS else: return AppRunRecord.STATUS_WAIT else: - if self.elite_weekly_times >= self.config.elite_weekly_times or \ + if (self.points_reward_complete and self.config.only_points_reward) \ + or self.elite_weekly_times >= self.config.elite_weekly_times or \ self.elite_daily_times >= self.config.elite_daily_times: # 已完成次数 return AppRunRecord.STATUS_SUCCESS else: @@ -42,6 +44,7 @@ def reset_record(self): if os_utils.get_monday_dt(current_dt) != os_utils.get_monday_dt(self.dt): self.weekly_times = 0 self.elite_weekly_times = 0 + self.points_reward_complete = False self.daily_times = 0 self.elite_daily_times = 0 @@ -89,6 +92,18 @@ def elite_weekly_times(self) -> int: def elite_weekly_times(self, new_value: int): self.update('elite_weekly_times', new_value) + @property + def points_reward_complete(self) -> bool: + """ + 每周挑战精英的次数 + :return: + """ + return self.get('points_reward_complete', False) + + @points_reward_complete.setter + def points_reward_complete(self, new_value: bool): + self.update('points_reward_complete', new_value) + @property def elite_daily_times(self) -> int: """ diff --git a/src/sr_od/app/sim_uni/sim_uni_screen_state.py b/src/sr_od/app/sim_uni/sim_uni_screen_state.py index f1027c48a..a32199e99 100644 --- a/src/sr_od/app/sim_uni/sim_uni_screen_state.py +++ b/src/sr_od/app/sim_uni/sim_uni_screen_state.py @@ -21,10 +21,16 @@ class ScreenState(Enum): SIM_TYPE_NORMAL: str = '模拟宇宙' # 模拟宇宙 - 普通 SIM_TYPE_EXTEND: str = '扩展装置' # 模拟宇宙 - 拓展装置 SIM_TYPE_GOLD: str = '黄金与机械' # 模拟宇宙 - 黄金与机械 - SIM_BLESS: str = '选择祝福' + SIM_TYPE_X: str = '差分宇宙' # 差分宇宙 + + # 选择xxx都走这 (老板模拟宇宙都跑不起来, 于是选择buff的功能暂时改为看到哪个祝福选哪个) + SIM_BLESS: str = '选择' # 选择金血祝颂 选择祝福 选择方程 选择奇物 (哪有字就点哪) + # SIM_BLESS: str = '选择祝福' + # SIM_EQUATION: str = '选择方程' SIM_DROP_BLESS: str = '丢弃祝福' SIM_UPGRADE_BLESS: str = '祝福强化' - SIM_CURIOS: str = '选择奇物' + # 选择奇物合并到选择xxx中 + # SIM_CURIOS: str = '选择奇物' SIM_DROP_CURIOS: str = '丢弃奇物' SIM_EVENT: str = '事件' SIM_UNI_REGION: str = '模拟宇宙-区域' @@ -44,7 +50,7 @@ def get_level_type(ctx: SrContext, screen: MatLike) -> Optional[SimUniLevelType] region_name = ctx.ocr.run_ocr_single_line(part) level_type_list: List[SimUniLevelType] = [enum.value for enum in SimUniLevelTypeEnum] target_list = [gt(level_type.type_name, 'game') for level_type in level_type_list] - target_idx = str_utils.find_best_match_by_difflib(region_name, target_list) + target_idx = str_utils.find_best_match_by_lcs(region_name, target_list) if target_idx is None or target_idx < 0: return None @@ -110,8 +116,12 @@ def get_sim_uni_screen_state( titles = common_screen_state.get_ui_titles(ctx, screen, '模拟宇宙', '左上角标题') sim_uni_idx = str_utils.find_best_match_by_lcs(ScreenState.SIM_TYPE_NORMAL.value, titles) gold_idx = str_utils.find_best_match_by_lcs(ScreenState.SIM_TYPE_GOLD.value, titles) # 不知道是不是游戏bug 游戏内正常的模拟宇宙也会显示这个 + # 差分宇宙 + sim_uni_x_idx = str_utils.find_best_match_by_lcs(ScreenState.SIM_TYPE_X.value, titles) + # 金血祝颂 - 选择效果 + sim_uni_x_gold_idx = str_utils.find_best_match_by_lcs(ScreenState.SIM_BLESS.value, titles) - if sim_uni_idx is None and gold_idx is None: + if sim_uni_idx is None and gold_idx is None and sim_uni_x_idx is None and sim_uni_x_gold_idx is None: if battle: # 有判断的时候 不在前面的情况 就认为是战斗 return battle_screen_state.ScreenState.BATTLE.value return None @@ -119,7 +129,8 @@ def get_sim_uni_screen_state( if sim_uni and str_utils.find_best_match_by_lcs(ScreenState.SIM_TYPE_NORMAL.value, titles, lcs_percent_threshold=0.51) is not None: return ScreenState.SIM_TYPE_NORMAL.value - if bless and str_utils.find_best_match_by_lcs(ScreenState.SIM_BLESS.value, titles, lcs_percent_threshold=0.51) is not None: + # 选择xxx都走这 (老板模拟宇宙都跑不起来, 于是选择buff的功能暂时改为看到哪个祝福选哪个) + if bless and str_utils.find_best_match_by_lcs(ScreenState.SIM_BLESS.value, titles, lcs_percent_threshold=0.15) is not None: return ScreenState.SIM_BLESS.value if drop_bless and str_utils.find_best_match_by_lcs(ScreenState.SIM_DROP_BLESS.value, titles, lcs_percent_threshold=0.51) is not None: @@ -128,8 +139,9 @@ def get_sim_uni_screen_state( if upgrade_bless and str_utils.find_best_match_by_lcs(ScreenState.SIM_UPGRADE_BLESS.value, titles, lcs_percent_threshold=0.51) is not None: return ScreenState.SIM_UPGRADE_BLESS.value - if curio and str_utils.find_best_match_by_lcs(ScreenState.SIM_CURIOS.value, titles, lcs_percent_threshold=0.51): - return ScreenState.SIM_CURIOS.value + # 选择奇物合并到选择xxx中 + # if curio and str_utils.find_best_match_by_lcs(ScreenState.SIM_CURIOS.value, titles, lcs_percent_threshold=0.51): + # return ScreenState.SIM_CURIOS.value if drop_curio and str_utils.find_best_match_by_lcs(ScreenState.SIM_DROP_CURIOS.value, titles, lcs_percent_threshold=0.51): return ScreenState.SIM_DROP_CURIOS.value @@ -178,14 +190,14 @@ def in_sim_uni_choose_bless(ctx: SrContext, screen: MatLike) -> bool: return common_screen_state.in_secondary_ui(ctx, screen, ScreenState.SIM_BLESS.value, lcs_percent=0.55) -def in_sim_uni_choose_curio(ctx: SrContext, screen: MatLike) -> bool: - """ - 是否在模拟宇宙-选择奇物页面 - :param ctx: 上下文 - :param screen: 游戏画面 - :return: - """ - return common_screen_state.in_secondary_ui(ctx, screen, ScreenState.SIM_CURIOS.value, lcs_percent=0.55) +# def in_sim_uni_choose_curio(ctx: SrContext, screen: MatLike) -> bool: +# """ +# 是否在模拟宇宙-选择奇物页面 +# :param ctx: 上下文 +# :param screen: 游戏画面 +# :return: +# """ +# return common_screen_state.in_secondary_ui(ctx, screen, ScreenState.SIM_CURIOS.value, lcs_percent=0.55) def in_sim_uni_event(ctx: SrContext, screen: MatLike) -> bool: diff --git a/src/sr_od/gui/interface/sim_uni/sim_uni_setting_interface.py b/src/sr_od/gui/interface/sim_uni/sim_uni_setting_interface.py index da2cc9810..c5745497a 100644 --- a/src/sr_od/gui/interface/sim_uni/sim_uni_setting_interface.py +++ b/src/sr_od/gui/interface/sim_uni/sim_uni_setting_interface.py @@ -1,9 +1,17 @@ +import os +import shutil +import subprocess + from PySide6.QtWidgets import QWidget from numpy.core.defchararray import title from qfluentwidgets import FluentIcon, SettingCardGroup from one_dragon.base.config.config_item import ConfigItem +from one_dragon.utils import os_utils +from one_dragon.utils.log_utils import log from one_dragon_qt.widgets.column import Column +from one_dragon_qt.widgets.setting_card.push_setting_card import PushSettingCard +from one_dragon_qt.widgets.setting_card.switch_setting_card import SwitchSettingCard from one_dragon_qt.widgets.vertical_scroll_interface import VerticalScrollInterface from one_dragon_qt.widgets.setting_card.combo_box_setting_card import ComboBoxSettingCard from one_dragon_qt.widgets.setting_card.text_setting_card import TextSettingCard @@ -24,28 +32,97 @@ def __init__(self, ctx: SrContext, parent=None): nav_text_cn='每周配置' ) + def _on_auto_simulated_universe_settings_clicked(self) -> None: + """ + 启动 Auto_Simulated_Universe + """ + work_dir = os_utils.get_work_dir() + plugin_path = os.path.join(work_dir, *['plugins', 'Auto_Simulated_Universe']) + gui_script = os.path.join(plugin_path, 'gui.py') + + if not os.path.exists(gui_script): + log.error(f'GUI脚本不存在: {gui_script}') + return + + command = [self.ctx.python_service.env_config.python_path] + script_working_directory = plugin_path + command.append(gui_script) + try: + subprocess.Popen(command, cwd=script_working_directory) + log.info('差分宇宙GUI已启动') + except Exception as e: + log.error(f'启动差分宇宙GUI失败: {e}') + + def _on_save_auto_simulated_universe_settings_clicked(self) -> None: + """ + 保存配置: Auto_Simulated_Universe + """ + work_dir = os_utils.get_work_dir() + plugin_path = os.path.join(work_dir, *['plugins', 'Auto_Simulated_Universe']) + plugin_config_file_path = os.path.join(plugin_path, 'info.yml') + + config_file_path = os.path.join(work_dir, + *['config', '%02d' % self.ctx.current_instance_idx, 'sim_universe_plugin.yml']) + try: + if not os.path.exists(plugin_config_file_path): + log.error(f'差分宇宙默认配置文件不存在: {plugin_config_file_path}') + return + # 确保目标目录存在 + os.makedirs(os.path.dirname(config_file_path), exist_ok=True) + shutil.copy(plugin_config_file_path, config_file_path) + log.info('差分宇宙配置保存成功') + except Exception as e: + log.error(f'保存差分宇宙配置失败: {e}') + def get_content_widget(self) -> QWidget: content_widget = Column() self.weekly_sim_uni_num_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='模拟宇宙') content_widget.add_widget(self.weekly_sim_uni_num_opt) - self.weekly_sim_uni_diff_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='难度') - content_widget.add_widget(self.weekly_sim_uni_diff_opt) + # region 差分宇宙 + group_x = SettingCardGroup(title='差分宇宙配置') + content_widget.add_widget(group_x) - self.weekly_plan_times_opt = TextSettingCard(icon=FluentIcon.CALENDAR, title='每周精英次数') - content_widget.add_widget(self.weekly_plan_times_opt) + # todo 如何扔进启动器里/自动下载/更新, 以及下载之后放哪 + self.auto_simulated_universe_settings = PushSettingCard( + icon=FluentIcon.APPLICATION, + title='配置Auto_Simulated_Universe', + content='需要先去github下载该项目放到 plugins/Auto_Simulated_Universe文件夹中', + text='配置' + ) + self.auto_simulated_universe_settings.clicked.connect(self._on_auto_simulated_universe_settings_clicked) + group_x.addSettingCard(self.auto_simulated_universe_settings) - self.daily_plan_times_opt = TextSettingCard(icon=FluentIcon.CALENDAR, title='每日精英次数') - content_widget.add_widget(self.daily_plan_times_opt) + self.only_points_reward = SwitchSettingCard( + icon=FluentIcon.GAME, title='每周只打满14000', content='勾选此处的话, Auto_Simulated_Universe 的每周次数可以设置成1', + ) + group_x.addSettingCard(self.only_points_reward) + + self.save_auto_simulated_universe_settings = PushSettingCard( + icon=FluentIcon.SAVE_AS, + title='保存配置文件到当前用户目录', + content='不同用户主要是开怪秘技角色有区别', + text='保存' + ) + self.save_auto_simulated_universe_settings.clicked.connect(self._on_save_auto_simulated_universe_settings_clicked) + group_x.addSettingCard(self.save_auto_simulated_universe_settings) + # endregion - challenge_group = SettingCardGroup(title='挑战配置') + # region 模拟宇宙 + challenge_group = SettingCardGroup(title='模拟宇宙配置') content_widget.add_widget(challenge_group) + self.weekly_sim_uni_diff_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='难度') + challenge_group.addSettingCard(self.weekly_sim_uni_diff_opt) + self.weekly_plan_times_opt = TextSettingCard(icon=FluentIcon.CALENDAR, title='每周精英次数') + challenge_group.addSettingCard(self.weekly_plan_times_opt) + self.daily_plan_times_opt = TextSettingCard(icon=FluentIcon.CALENDAR, title='每日精英次数') + challenge_group.addSettingCard(self.daily_plan_times_opt) self.challenge_opt_list = {} for i in SimUniWorldEnum: - if i.name in ['WORLD_00', 'WORLD_01', 'WORLD_02']: + if i.name in ['WORLD_00', 'WORLD_01', 'WORLD_02', 'WORLD_X']: continue challenge_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title=i.value.name) @@ -55,6 +132,7 @@ def get_content_widget(self) -> QWidget: content_widget.add_stretch(1) return content_widget + # endregion def on_interface_shown(self) -> None: VerticalScrollInterface.on_interface_shown(self) @@ -75,6 +153,7 @@ def on_interface_shown(self) -> None: self.weekly_sim_uni_diff_opt.set_options_by_list(diff_opts) self.weekly_sim_uni_diff_opt.init_with_adapter(self.ctx.sim_uni_config.weekly_uni_diff_adapter) + self.only_points_reward.init_with_adapter(self.ctx.sim_uni_config.only_points_reward_adapter) self.weekly_plan_times_opt.init_with_adapter(self.ctx.sim_uni_config.elite_weekly_times_adapter) self.daily_plan_times_opt.init_with_adapter(self.ctx.sim_uni_config.elite_daily_times_adapter) diff --git a/src/sr_od/interastral_peace_guide/guide_choose_mission.py b/src/sr_od/interastral_peace_guide/guide_choose_mission.py index efeab5188..e81140c84 100644 --- a/src/sr_od/interastral_peace_guide/guide_choose_mission.py +++ b/src/sr_od/interastral_peace_guide/guide_choose_mission.py @@ -132,11 +132,13 @@ def find_transport_btn(self, screen: MatLike) -> Optional[Point]: return None log.info('匹配副本名称 %s', word_list[mission_idx]) - tp_idx = str_utils.find_best_match_by_difflib(gt('传送', 'game'), word_list, cutoff=0.5) # 模拟宇宙 + tp_idx = str_utils.find_best_match_by_difflib(gt('前往参与', 'game'), word_list, cutoff=0.75) # 差分宇宙 + if tp_idx is None: + tp_idx = str_utils.find_best_match_by_difflib(gt('传送', 'game'), word_list, cutoff=0.5) # 模拟宇宙 if tp_idx is None: tp_idx = str_utils.find_best_match_by_difflib(gt('进入', 'game'), word_list, cutoff=0.5) # 普通副本 if tp_idx is None: - log.error('匹配失败 传送/进入') + log.error('匹配失败 前往参与/传送/进入') return None mission_pos = mrl_list[mission_idx].max.center diff --git a/src/sr_od/interastral_peace_guide/guide_transport.py b/src/sr_od/interastral_peace_guide/guide_transport.py index 5ccd4d760..1c51635e4 100644 --- a/src/sr_od/interastral_peace_guide/guide_transport.py +++ b/src/sr_od/interastral_peace_guide/guide_transport.py @@ -47,6 +47,9 @@ def choose_mission(self) -> OperationRoundResult: @node_from(from_name='选择副本') @operation_node(name='等待加载', node_max_retry_times=20) def wait_at_last(self) -> OperationRoundResult: + if self.mission.cate.cn == '差分宇宙': + return self.round_by_op_result(self.op_success("成功")) + screen = self.last_screenshot # 培养目标特殊处理:尝试匹配任意一个等待加载区域 diff --git a/src/sr_od/operations/back_to_normal_world_plus.py b/src/sr_od/operations/back_to_normal_world_plus.py index 72a3f08b4..87c3aeb17 100644 --- a/src/sr_od/operations/back_to_normal_world_plus.py +++ b/src/sr_od/operations/back_to_normal_world_plus.py @@ -28,12 +28,16 @@ def check_screen(self) -> OperationRoundResult: screen = self.last_screenshot # 先看看左上角是否退出按钮 - result = self.round_by_find_area(screen, '模拟宇宙', '大世界返回按钮') + result = self.round_by_find_area(screen, '模拟宇宙', '差分宇宙返回按钮') + # 差分宇宙退出要点返回主界面 + is_in_x = result.is_success + if not result.is_success: + result = self.round_by_find_area(screen, '模拟宇宙', '大世界返回按钮') if result.is_success: # 判断是否在模拟宇宙内 sim_uni_level_type = sim_uni_screen_state.get_level_type(self.ctx, screen) if sim_uni_level_type is not None: - return self.sim_uni_exit() + return self.sim_uni_exit(is_in_x) # 如果有返回按钮 又不是在模拟宇宙 则就是在逐光捡金内 result = self.round_by_find_and_click_area(screen, '模拟宇宙', '大世界返回按钮') @@ -74,8 +78,8 @@ def check_screen(self) -> OperationRoundResult: if sim_uni_state == sim_uni_screen_state.ScreenState.SIM_DROP_BLESS.value: return self.sim_uni_drop_bless() - if sim_uni_state == sim_uni_screen_state.ScreenState.SIM_CURIOS.value: - return self.sim_uni_choose_curio() + # if sim_uni_state == sim_uni_screen_state.ScreenState.SIM_CURIOS.value: + # return self.sim_uni_choose_curio() if sim_uni_state == sim_uni_screen_state.ScreenState.SIM_DROP_CURIOS.value: return self.sim_uni_drop_curio() @@ -108,11 +112,12 @@ def check_screen(self) -> OperationRoundResult: return self.round_wait(result.status, wait=2) # 其他情况 - 均点击右上角触发返回上一级 + # result = self.ctx.controller.esc() result = self.round_by_click_area('菜单', '右上角返回') return self.round_wait(result.status, wait=1) - def sim_uni_exit(self) -> OperationRoundResult: - op = SimUniExit(self.ctx) + def sim_uni_exit(self, is_in_x: bool) -> OperationRoundResult: + op = SimUniExit(self.ctx, is_in_x) op_result = op.execute() if op_result.success: return self.round_wait(wait=1) diff --git a/src/sr_od/operations/enter_game/enter_game.py b/src/sr_od/operations/enter_game/enter_game.py index fcae182a0..a5e218781 100644 --- a/src/sr_od/operations/enter_game/enter_game.py +++ b/src/sr_od/operations/enter_game/enter_game.py @@ -93,7 +93,7 @@ def check_login_related(self, screen: MatLike) -> OperationRoundResult: else: result = self.round_by_find_and_click_area(screen, '进入游戏', '文本-点击进入') if result.is_success: - return self.round_wait(result.status, wait=1) + return self.round_success(result.status, wait=1) result = self.round_by_find_and_click_area(screen, '进入游戏', '提示-确认') if result.is_success: