From 4cd0d710c7bf6d27f5c4340fede41c907ff4f20f Mon Sep 17 00:00:00 2001 From: moesnow <11678347+moesnow@users.noreply.github.com> Date: Sat, 2 Mar 2024 18:09:20 +0800 Subject: [PATCH 1/4] refactor: gamecontroller --- app/card/samplecardview1.py | 2 +- app/main_window.py | 28 +-- app/setting_interface.py | 2 +- main.py | 7 +- tasks/base/download.py | 8 +- tasks/base/pythonchecker.py | 7 +- tasks/base/{command.py => tasks.py} | 35 +--- tasks/base/windowswitcher.py | 43 ----- tasks/daily/daily.py | 3 +- tasks/daily/fight.py | 7 +- tasks/game/__init__.py | 244 ++++++++++++++++++++++++ tasks/game/game.py | 28 --- tasks/game/registry.py | 33 ---- tasks/game/resolution.py | 41 ---- tasks/game/start.py | 147 -------------- tasks/game/stop.py | 132 ------------- tasks/tools/game_screenshot/__init__.py | 5 +- tasks/version/__init__.py | 38 ++++ tasks/version/version.py | 38 ---- tasks/weekly/forgottenhall.py | 9 - tasks/weekly/universe.py | 7 +- utils/__init__.py | 0 utils/command.py | 28 +++ {tasks/base => utils}/date.py | 116 +++++------ utils/gamecontroller.py | 162 ++++++++++++++++ utils/registry/__init__.py | 0 utils/registry/star_rail_resolution.py | 100 ++++++++++ 27 files changed, 673 insertions(+), 597 deletions(-) rename tasks/base/{command.py => tasks.py} (50%) delete mode 100644 tasks/base/windowswitcher.py delete mode 100644 tasks/game/game.py delete mode 100644 tasks/game/registry.py delete mode 100644 tasks/game/resolution.py delete mode 100644 tasks/game/start.py delete mode 100644 tasks/game/stop.py delete mode 100644 tasks/version/version.py create mode 100644 utils/__init__.py create mode 100644 utils/command.py rename {tasks/base => utils}/date.py (97%) create mode 100644 utils/gamecontroller.py create mode 100644 utils/registry/__init__.py create mode 100644 utils/registry/star_rail_resolution.py diff --git a/app/card/samplecardview1.py b/app/card/samplecardview1.py index a727bbbe..2ee2aa76 100644 --- a/app/card/samplecardview1.py +++ b/app/card/samplecardview1.py @@ -8,7 +8,7 @@ from ..common.style_sheet import StyleSheet from managers.config_manager import config -from tasks.base.command import start_task +from tasks.base.tasks import start_task from ..tools.disclaimer import disclaimer import base64 diff --git a/app/main_window.py b/app/main_window.py index e3daf3eb..b0c734f2 100644 --- a/app/main_window.py +++ b/app/main_window.py @@ -20,9 +20,8 @@ from .tools.disclaimer import disclaimer from managers.config_manager import config -import subprocess +from utils.gamecontroller import GameController import base64 -import os class MainWindow(MSFluentWindow): @@ -111,8 +110,19 @@ def initNavigation(self): disclaimer(self) def startGame(self): + game = GameController(config.game_path, config.game_process_name, config.game_title_name, 'UnityWndClass') try: - if os.system(f"cmd /C start \"\" \"{config.game_path}\""): + if game.start_game(): + InfoBar.success( + title=self.tr('启动成功(^∀^●)'), + content="", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=1000, + parent=self + ) + else: InfoBar.warning( title=self.tr('启动失败(╥╯﹏╰╥)'), content="", @@ -122,17 +132,7 @@ def startGame(self): duration=1000, parent=self ) - return False - InfoBar.success( - title=self.tr('启动成功(^∀^●)'), - content="", - orient=Qt.Horizontal, - isClosable=True, - position=InfoBarPosition.TOP, - duration=1000, - parent=self - ) - except Exception: + except: InfoBar.warning( title=self.tr('启动失败(╥╯﹏╰╥)'), content="", diff --git a/app/setting_interface.py b/app/setting_interface.py index 7b425127..2d2156b1 100644 --- a/app/setting_interface.py +++ b/app/setting_interface.py @@ -10,7 +10,7 @@ from .card.rangesettingcard1 import RangeSettingCard1 from .card.pushsettingcard1 import PushSettingCardInstance, PushSettingCardEval, PushSettingCardDate, PushSettingCardKey, PushSettingCardTeam from managers.config_manager import config -from tasks.base.command import start_task +from tasks.base.tasks import start_task from .tools.check_update import checkUpdate import os diff --git a/main.py b/main.py index 51248d13..612bbc97 100644 --- a/main.py +++ b/main.py @@ -3,14 +3,13 @@ os.chdir(os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(os.path.abspath(__file__))) -from tasks.version.version import Version +from tasks.version import Version from managers.notify_manager import notify from managers.logger_manager import logger from managers.config_manager import config from managers.ocr_manager import ocr from managers.translate_manager import _ -from tasks.game.game import Game -from tasks.game.start import Start +from tasks.game import Game from tasks.daily.daily import Daily import tasks.activity as activity import tasks.reward as reward @@ -131,7 +130,7 @@ def main(action=None): automatic_plot() elif action == "game": - Start.launch_game() + Game.start() elif action == "notify": run_notify_action() diff --git a/tasks/base/download.py b/tasks/base/download.py index fb3914cc..b35cf105 100644 --- a/tasks/base/download.py +++ b/tasks/base/download.py @@ -1,10 +1,10 @@ +from tqdm import tqdm +import urllib.request +import subprocess +import os def download_with_progress(download_url, save_path): - from tqdm import tqdm - import urllib.request - import subprocess - import os aria2_path = os.path.abspath("./assets/aria2/aria2c.exe") diff --git a/tasks/base/pythonchecker.py b/tasks/base/pythonchecker.py index 58e7129f..e7a83cf1 100644 --- a/tasks/base/pythonchecker.py +++ b/tasks/base/pythonchecker.py @@ -2,8 +2,8 @@ from managers.logger_manager import logger from managers.config_manager import config from managers.translate_manager import _ -from tasks.base.command import subprocess_with_stdout -from tasks.base.windowswitcher import WindowSwitcher +from utils.command import subprocess_with_stdout +from utils.gamecontroller import GameController from packaging.version import parse import subprocess import tempfile @@ -68,7 +68,8 @@ def install(): if PythonChecker.check(destination_path): config.set_value("python_exe_path", destination_path) - WindowSwitcher.check_and_switch(config.game_title_name) + game = GameController(config.game_path, config.game_process_name, config.game_title_name, 'UnityWndClass', logger) + game.switch_to_game() return logger.info(_("安装完成,请重启程序,包括图形界面")) diff --git a/tasks/base/command.py b/tasks/base/tasks.py similarity index 50% rename from tasks/base/command.py rename to tasks/base/tasks.py index ac5622cb..d8f9c4f5 100644 --- a/tasks/base/command.py +++ b/tasks/base/tasks.py @@ -1,38 +1,11 @@ - -def subprocess_with_timeout(command, timeout, working_directory=None, env=None): - import subprocess - process = None - try: - process = subprocess.Popen(command, cwd=working_directory, env=env) - process.communicate(timeout=timeout) - if process.returncode == 0: - return True - except subprocess.TimeoutExpired: - if process is not None: - process.terminate() - process.wait() - return False - - -def subprocess_with_stdout(command): - import subprocess - try: - # 使用subprocess运行命令并捕获标准输出 - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - # 检查命令是否成功执行 - if result.returncode == 0: - # 返回标准输出的内容 - return result.stdout.strip() - return None - except Exception: - return None +from utils.command import subprocess_with_stdout +import subprocess +import sys +import os def start_task(command): # 为什么 Windows 这么难用呢 - import subprocess - import sys - import os # 检查是否是 PyInstaller 打包的可执行文件 if getattr(sys, 'frozen', False): if subprocess_with_stdout(["where", "wt.exe"]) is not None: diff --git a/tasks/base/windowswitcher.py b/tasks/base/windowswitcher.py deleted file mode 100644 index 64d8b485..00000000 --- a/tasks/base/windowswitcher.py +++ /dev/null @@ -1,43 +0,0 @@ -from managers.translate_manager import _ -from managers.logger_manager import logger -import pygetwindow as gw -import pyautogui -import win32gui -import time - - -class WindowSwitcher: - @staticmethod - def check_and_switch(title, max_retries=4): - for i in range(max_retries): - windows = gw.getWindowsWithTitle(title) - for window in windows: - if window.title == title: - WindowSwitcher._activate_window(window) - if WindowSwitcher._is_window_active(window, title): - return True - else: - logger.info(_("切换窗口失败,尝试 ALT+TAB")) - pyautogui.hotkey('alt', 'tab') - time.sleep(2) - break - return False - - @staticmethod - def _activate_window(window): - try: - window.restore() - window.activate() - time.sleep(2) - except Exception as e: - logger.warning(f"切换窗口失败: {e}") - - @staticmethod - def _is_window_active(window, title): - try: - hwnd = win32gui.FindWindow("UnityWndClass", title) - win32gui.GetWindowRect(hwnd) - return window.isActive - except Exception as e: - logger.warning(f"检测窗口状态失败: {e}") - return False diff --git a/tasks/daily/daily.py b/tasks/daily/daily.py index 296a33b3..170ad8ed 100644 --- a/tasks/daily/daily.py +++ b/tasks/daily/daily.py @@ -2,7 +2,7 @@ from managers.config_manager import config from managers.screen_manager import screen from managers.translate_manager import _ -from tasks.base.date import Date +from utils.date import Date from tasks.daily.photo import Photo from tasks.daily.fight import Fight from tasks.weekly.universe import Universe @@ -13,7 +13,6 @@ from tasks.power.power import Power from tasks.daily.tasks import Tasks from tasks.daily.himekotry import HimekoTry -from tasks.base.date import Date from tasks.weekly.echoofwar import Echoofwar diff --git a/tasks/daily/fight.py b/tasks/daily/fight.py index 19832840..0c6d791b 100644 --- a/tasks/daily/fight.py +++ b/tasks/daily/fight.py @@ -5,8 +5,8 @@ from tasks.base.base import Base from tasks.base.team import Team from tasks.base.pythonchecker import PythonChecker -from tasks.game.resolution import Resolution -from tasks.base.command import subprocess_with_timeout +from tasks.game import StarRailController +from utils.command import subprocess_with_timeout import subprocess import sys import os @@ -80,7 +80,8 @@ def before_start(): @staticmethod def start(): logger.hr(_("准备锄大地"), 0) - Resolution.check(config.game_title_name, 1920, 1080) + game = StarRailController(config.game_path, config.game_process_name, config.game_title_name, 'UnityWndClass', logger) + game.check_resolution(1920, 1080) if Fight.before_start(): # 切换队伍 if config.fight_team_enable: diff --git a/tasks/game/__init__.py b/tasks/game/__init__.py index e69de29b..1a0f6cdb 100644 --- a/tasks/game/__init__.py +++ b/tasks/game/__init__.py @@ -0,0 +1,244 @@ +from managers.logger_manager import logger +from managers.screen_manager import screen +from managers.automation_manager import auto +from managers.config_manager import config +from managers.notify_manager import notify +from managers.ocr_manager import ocr +from tasks.power.power import Power +from utils.date import Date +from utils.gamecontroller import GameController +from utils.registry.star_rail_resolution import get_game_resolution, set_game_resolution +from typing import Literal, Optional +import time +import logging +import pyautogui +import psutil +import random +import sys +import os + + +class StarRailController(GameController): + def __init__(self, game_path: str, process_name: str, window_name: str, window_class: Optional[str], logger: Optional[logging.Logger] = None) -> None: + super().__init__(game_path, process_name, window_name, window_class, logger) + self.game_resolution = None + self.screen_resolution = pyautogui.size() + + def change_resolution(self, width: int, height: int): + """通过注册表修改游戏分辨率""" + try: + self.game_resolution = get_game_resolution() + if self.game_resolution: + screen_width, screen_height = self.screen_resolution + is_fullscreen = False if screen_width > width and screen_height > height else True + set_game_resolution(width, height, is_fullscreen) + self.log_debug(f"修改游戏分辨率: {self.game_resolution[0]}x{self.game_resolution[1]} ({'全屏' if self.game_resolution[2] else '窗口'}) --> {width}x{height} ({'全屏' if is_fullscreen else '窗口'})") + except FileNotFoundError: + self.log_debug("指定的注册表项未找到") + except Exception as e: + self.log_error("读取注册表值时发生错误:", e) + + def restore_resolution(self): + """通过注册表恢复游戏分辨率""" + try: + if self.game_resolution: + set_game_resolution(self.game_resolution[0], self.game_resolution[1], self.game_resolution[2]) + self.log_debug(f"恢复游戏分辨率: {self.game_resolution[0]}x{self.game_resolution[1]} ({'全屏' if self.game_resolution[2] else '窗口'})") + except Exception as e: + self.log_error("写入注册表值时发生错误:", e) + def check_resolution(self, target_width: int, target_height: int) -> None: + """ + 检查游戏窗口的分辨率是否匹配目标分辨率。 + + 如果游戏窗口的分辨率与目标分辨率不匹配,则记录错误并抛出异常。 + 如果桌面分辨率小于目标分辨率,也会记录错误建议。 + + 参数: + target_width (int): 目标分辨率的宽度。 + target_height (int): 目标分辨率的高度。 + """ + resolution = self.get_resolution() + if not resolution: + raise Exception("游戏分辨率获取失败") + window_width, window_height = resolution + + screen_width, screen_height = self.screen_resolution + if window_width != target_width or window_height != target_height: + self.log_error(f"游戏分辨率: {window_width}x{window_height},请在游戏设置内切换为 {target_width}x{target_height} 窗口或全屏运行") + if screen_width < target_width or screen_height < target_height: + self.log_error(f"桌面分辨率: {screen_width}x{screen_height},你可能需要更大的显示器或使用 HDMI/VGA 显卡欺骗器") + raise Exception("游戏分辨率过低") + else: + self.log_debug(f"游戏分辨率: {window_width}x{window_height}") + + def check_resolution_ratio(self, target_width: int, target_height: int) -> None: + """ + 检查游戏窗口的分辨率和比例是否符合目标设置。 + + 如果游戏窗口的分辨率小于目标分辨率或比例不正确,则记录错误并抛出异常。 + 如果桌面分辨率不符合最小推荐值,也会记录错误建议。 + + 参数: + target_width (int): 目标分辨率的宽度。 + target_height (int): 目标分辨率的高度。 + """ + resolution = self.get_resolution() + if not resolution: + raise Exception("游戏分辨率获取失败") + window_width, window_height = resolution + + screen_width, screen_height = self.screen_resolution + + if window_width < target_width or window_height < target_height: + self.log_error(f"游戏分辨率: {window_width}x{window_height} 请在游戏设置内切换为 {target_width}x{target_height} 窗口或全屏运行") + if screen_width < 1920 or screen_height < 1080: + self.log_error(f"桌面分辨率: {screen_width}x{screen_height} 你可能需要更大的显示器或使用 HDMI/VGA 显卡欺骗器") + raise Exception("游戏分辨率过低") + elif abs(window_width / window_height - (target_width / target_height)) > 0.01: + self.log_error(f"游戏分辨率: {window_width}x{window_height} 请在游戏设置内切换为 {target_width}:{target_height} 比例") + raise Exception("游戏分辨率比例不正确") + else: + if window_width != target_width or window_height != target_height: + self.log_warning(f"游戏分辨率: {window_width}x{window_height} ≠ {target_width}x{target_height} 可能出现未预期的错误") + time.sleep(2) + else: + self.log_debug(f"游戏分辨率: {window_width}x{window_height}") + + +class Game: + @staticmethod + def start(): + logger.hr("开始运行", 0) + Game.start_game() + logger.hr("完成", 2) + + @staticmethod + def start_game(): + game = StarRailController(config.game_path, config.game_process_name, config.game_title_name, 'UnityWndClass', logger) + MAX_RETRY = 3 + + def wait_until(condition, timeout, period=1): + """等待直到条件满足或超时""" + end_time = time.time() + timeout + while time.time() < end_time: + if condition(): + return True + time.sleep(period) + return False + + def check_and_click_enter(): + # 点击进入 + if auto.click_element("./assets/images/screen/click_enter.png", "image", 0.9): + return True + # 游戏热更新,需要确认重启 + auto.click_element("./assets/images/zh_CN/base/confirm.png", "image", 0.9, take_screenshot=False) + # 网络异常等问题,需要重新启动 + auto.click_element("./assets/images/zh_CN/base/restart.png", "image", 0.9, take_screenshot=False) + # 适配国际服,需要点击“开始游戏” + auto.click_element("./assets/images/screen/start_game.png", "image", 0.9, take_screenshot=False) + return False + + def get_process_path(name): + # 通过进程名获取运行路径 + for proc in psutil.process_iter(attrs=['pid', 'name']): + if name in proc.info['name']: + process = psutil.Process(proc.info['pid']) + return process.exe() + return None + + for retry in range(MAX_RETRY): + try: + if not game.switch_to_game(): + if config.auto_set_resolution_enable: + game.change_resolution(1920, 1080) + + if not game.start_game(): + raise Exception("启动游戏失败") + time.sleep(10) + + if not wait_until(lambda: game.switch_to_game(), 60): + game.restore_resolution() + raise TimeoutError("切换到游戏超时") + + time.sleep(10) + game.restore_resolution() + game.check_resolution_ratio(1920, 1080) + + if not wait_until(lambda: check_and_click_enter(), 600): + raise TimeoutError("查找并点击进入按钮超时") + time.sleep(10) + else: + game.check_resolution_ratio(1920, 1080) + if config.auto_set_game_path_enable: + program_path = get_process_path(config.game_process_name) + if program_path is not None and program_path != config.game_path: + config.set_value("game_path", program_path) + logger.info("游戏路径更新成功:{program_path}") + + if not wait_until(lambda: screen.get_current_screen(), 180): + raise TimeoutError("获取当前界面超时") + break # 成功启动游戏,跳出重试循环 + except Exception as e: + logger.error(f"尝试启动游戏时发生错误:{e}") + game.stop_game() # 确保在重试前停止游戏 + if retry == MAX_RETRY - 1: + raise # 如果是最后一次尝试,则重新抛出异常 + + @staticmethod + def stop(detect_loop=False): + logger.hr("停止运行", 0) + game = StarRailController(config.game_path, config.game_process_name, config.game_title_name, 'UnityWndClass', logger) + + def play_audio(): + logger.info("开始播放音频") + os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" + import pygame.mixer + + pygame.init() + pygame.mixer.music.load("./assets/audio/pa.mp3") + pygame.mixer.music.play() + + while pygame.mixer.music.get_busy(): + pygame.time.Clock().tick(10) + logger.info("播放音频完成") + + if config.play_audio: + play_audio() + + if detect_loop and config.after_finish == "Loop": + Game.after_finish_is_loop(game) + else: + if config.after_finish in ["Exit", "Loop", "Shutdown", "Hibernate", "Sleep"]: + game.shutdown(config.after_finish) + logger.hr("完成", 2) + if config.after_finish not in ["Shutdown", "Hibernate", "Sleep"]: + input("按回车键关闭窗口. . .") + sys.exit(0) + + @staticmethod + def after_finish_is_loop(game): + + def get_wait_time(current_power): + # 距离体力到达配置文件指定的上限剩余秒数 + wait_time_power_limit = (config.power_limit - current_power) * 6 * 60 + # 距离第二天凌晨4点剩余秒数,+30避免显示3点59分不美观,#7 + wait_time_next_day = Date.get_time_next_x_am(config.refresh_hour) + random.randint(30, 600) + # 取最小值 + wait_time = min(wait_time_power_limit, wait_time_next_day) + return wait_time + + current_power = Power.get() + if current_power >= config.power_limit: + logger.info(f"🟣开拓力 >= {config.power_limit}") + logger.info("即将再次运行") + logger.hr("完成", 2) + else: + game.stop_game() + wait_time = get_wait_time(current_power) + future_time = Date.calculate_future_time(wait_time) + logger.info(f"📅将在{future_time}继续运行") + notify.notify(f"📅将在{future_time}继续运行") + logger.hr("完成", 2) + # 等待状态退出OCR避免内存占用 + ocr.exit_ocr() + time.sleep(wait_time) diff --git a/tasks/game/game.py b/tasks/game/game.py deleted file mode 100644 index d542f260..00000000 --- a/tasks/game/game.py +++ /dev/null @@ -1,28 +0,0 @@ -from managers.logger_manager import logger -from managers.screen_manager import screen -from managers.automation_manager import auto -from managers.translate_manager import _ -from managers.config_manager import config -from managers.notify_manager import notify -from .start import Start -from .stop import Stop -import sys - - -class Game: - @staticmethod - def start(): - logger.hr(_("开始运行"), 0) - logger.info(_("开始启动游戏")) - if not auto.retry_with_timeout(lambda: Start.start_game(), 1200, 1): - raise RuntimeError(_("⚠️启动游戏超时,退出程序⚠️")) - logger.hr(_("完成"), 2) - - @staticmethod - def stop(detect_loop=False): - logger.hr(_("停止运行"), 0) - Stop.play_audio() - if detect_loop and config.after_finish == "Loop": - Stop.after_finish_is_loop() - else: - Stop.after_finish_not_loop() diff --git a/tasks/game/registry.py b/tasks/game/registry.py deleted file mode 100644 index d716d11f..00000000 --- a/tasks/game/registry.py +++ /dev/null @@ -1,33 +0,0 @@ -from managers.logger_manager import logger -import winreg - - -class Registry: - @staticmethod - def read_registry_value(key, sub_key, value_name): - try: - # 打开指定的注册表项 - registry_key = winreg.OpenKey(key, sub_key) - # 读取注册表中指定值的内容 - value, _ = winreg.QueryValueEx(registry_key, value_name) - # 关闭注册表项 - winreg.CloseKey(registry_key) - return value - except FileNotFoundError: - logger.debug("指定的注册表项未找到") - except Exception as e: - logger.error("发生错误:", e) - return None - - @staticmethod - def write_registry_value(key, sub_key, value_name, data): - try: - # 打开或创建指定的注册表项 - registry_key = winreg.CreateKey(key, sub_key) - # 将数据写入注册表 - winreg.SetValueEx(registry_key, value_name, 0, winreg.REG_BINARY, data) - # 关闭注册表项 - winreg.CloseKey(registry_key) - logger.debug("成功写入注册表值") - except Exception as e: - logger.error("写入注册表值时发生错误:", e) diff --git a/tasks/game/resolution.py b/tasks/game/resolution.py deleted file mode 100644 index 2ebefb0d..00000000 --- a/tasks/game/resolution.py +++ /dev/null @@ -1,41 +0,0 @@ -from managers.logger_manager import logger -from managers.translate_manager import _ -import win32gui -import pyautogui -import time -import sys - - -class Resolution: - @staticmethod - def check(title, width, height): - screen_width, screen_height = pyautogui.size() - hwnd = win32gui.FindWindow("UnityWndClass", title) - x, y, w, h = win32gui.GetClientRect(hwnd) - if w != width or h != height: - logger.error(_("游戏分辨率 {w}*{h} 请在游戏设置内切换为 {width}*{height} 窗口或全屏运行").format(w=w, h=h, width=width, height=height)) - if screen_width < 1920 or screen_height < 1080: - logger.error(_("桌面分辨率 {w}*{h} 你可能需要更大的显示器或使用 HDMI/VGA 显卡欺骗器").format(w=w, h=h)) - raise WindowsError("游戏分辨率过低") - else: - logger.debug(_("游戏分辨率 {w}*{h}").format(w=w, h=h)) - - @staticmethod - def check_scale(title, width, height): - screen_width, screen_height = pyautogui.size() - hwnd = win32gui.FindWindow("UnityWndClass", title) - x, y, w, h = win32gui.GetClientRect(hwnd) - if w < width or h < height: - logger.error(_("游戏分辨率 {w}*{h} 请在游戏设置内切换为 {width}*{height} 窗口或全屏运行").format(w=w, h=h, width=width, height=height)) - if screen_width < 1920 or screen_height < 1080: - logger.error(_("桌面分辨率 {w}*{h} 你可能需要更大的显示器或使用 HDMI/VGA 显卡欺骗器").format(w=w, h=h)) - raise WindowsError("游戏分辨率过低") - elif abs(w / h - (width / height)) > 0.01: - logger.error(_("游戏分辨率 {w}*{h} 请在游戏设置内切换为 {width}:{height} 比例").format(w=w, h=h, width=width, height=height)) - raise WindowsError("游戏分辨率比例不正确") - else: - if w != width or h != height: - logger.warning(_("游戏分辨率 {w}*{h} ≠ {width}*{height} 可能出现未预期的错误").format(w=w, h=h, width=width, height=height)) - time.sleep(2) - else: - logger.debug(_("游戏分辨率 {w}*{h}").format(w=w, h=h)) diff --git a/tasks/game/start.py b/tasks/game/start.py deleted file mode 100644 index b3ed15fa..00000000 --- a/tasks/game/start.py +++ /dev/null @@ -1,147 +0,0 @@ -from managers.logger_manager import logger -from managers.screen_manager import screen -from managers.automation_manager import auto -from managers.translate_manager import _ -from managers.config_manager import config -from managers.ocr_manager import ocr -from tasks.base.windowswitcher import WindowSwitcher -from .stop import Stop -from .resolution import Resolution -from .registry import Registry -import subprocess -import pyautogui -import winreg -import psutil -import json -import time -import sys -import os - - -class Start: - @staticmethod - def check_path(game_path): - # 检测路径是否存在 - if not os.path.exists(game_path): - logger.error(_("游戏路径不存在: {path}").format(path=game_path)) - logger.error(_("第一次使用请手动启动游戏进入主界面后重新运行,程序会自动保存游戏路径")) - logger.error(_("注意:程序只支持PC端运行,不支持任何模拟器")) - raise FileNotFoundError(_("游戏路径不存在")) - - @staticmethod - def get_process_path(name): - # 通过进程名获取运行路径 - for proc in psutil.process_iter(attrs=['pid', 'name']): - if name in proc.info['name']: - process = psutil.Process(proc.info['pid']) - return process.exe() - return None - - @staticmethod - def check_and_click_enter(): - # 点击进入 - if auto.click_element("./assets/images/screen/click_enter.png", "image", 0.9): - return True - # 游戏热更新,需要确认重启 - auto.click_element("./assets/images/zh_CN/base/confirm.png", "image", 0.9) - # 网络异常等问题,需要重新启动 - auto.click_element("./assets/images/zh_CN/base/restart.png", "image", 0.9) - # 适配国际服,需要点击“开始游戏” - auto.click_element("./assets/images/screen/start_game.png", "image", 0.9) - return False - - @staticmethod - def set_resolution(): - resolution_value = None - if config.auto_set_resolution_enable: - # 指定注册表项路径 - registry_key_path = r"SOFTWARE\miHoYo\崩坏:星穹铁道" - # 指定要获取的值的名称 - value_name = "GraphicsSettings_PCResolution_h431323223" - # 读取注册表中指定路径的值 - resolution_value = Registry.read_registry_value(winreg.HKEY_CURRENT_USER, registry_key_path, value_name) - - if resolution_value: - # 去除末尾的\x00字符并尝试解析JSON - data_dict = json.loads(resolution_value.decode('utf-8').strip('\x00')) - data_dict['width'] = 1920 - data_dict['height'] = 1080 - # 获取屏幕的宽度和高度 - screen_width, screen_height = pyautogui.size() - if screen_width <= 1920 and screen_height <= 1080: - data_dict['isFullScreen'] = True - elif screen_width > 1920 and screen_height > 1080: - data_dict['isFullScreen'] = False - - # 修改数据并添加\x00字符 - modified_data = (json.dumps(data_dict) + '\x00').encode('utf-8') - - # 写入注册表 - Registry.write_registry_value(winreg.HKEY_CURRENT_USER, registry_key_path, value_name, modified_data) - return resolution_value - - def restore_resolution(resolution_value): - # 指定注册表项路径 - registry_key_path = r"SOFTWARE\miHoYo\崩坏:星穹铁道" - # 指定要获取的值的名称 - value_name = "GraphicsSettings_PCResolution_h431323223" - # 读取注册表中指定路径的值 - if resolution_value: - Registry.write_registry_value(winreg.HKEY_CURRENT_USER, registry_key_path, value_name, resolution_value) - - @staticmethod - def launch_game(): - logger.info(_("🖥️启动游戏中...")) - Start.check_path(config.game_path) - - resolution_value = Start.set_resolution() - logger.debug(_("运行命令: cmd /C start \"\" \"{path}\"").format(path=config.game_path)) - if os.system(f"cmd /C start \"\" \"{config.game_path}\""): - return False - logger.debug(_("游戏启动成功: {path}").format(path=config.game_path)) - - time.sleep(10) - if not auto.retry_with_timeout(lambda: WindowSwitcher.check_and_switch(config.game_title_name), 60, 1): - Start.restore_resolution(resolution_value) - logger.error(_("无法切换游戏到前台")) - return False - else: - Start.restore_resolution(resolution_value) - - @staticmethod - def launch_process(): - Start.launch_game() - Resolution.check_scale(config.game_title_name, 1920, 1080) - - if not auto.retry_with_timeout(lambda: Start.check_and_click_enter(), 600, 1): - logger.error(_("无法找到点击进入按钮")) - return False - - time.sleep(10) - if not auto.retry_with_timeout(lambda: screen.get_current_screen(), 180, 1): - logger.error(_("无法进入主界面")) - return False - - return True - - @staticmethod - def start_game(): - # 判断是否已经启动 - if not WindowSwitcher.check_and_switch(config.game_title_name): - if not Start.launch_process(): - logger.error(_("游戏启动失败,退出游戏进程")) - Stop.stop_game() - return False - else: - logger.info(_("游戏启动成功")) - else: - logger.info(_("游戏已经启动了")) - - if config.auto_set_game_path_enable: - program_path = Start.get_process_path(config.game_process_name) - if program_path is not None and program_path != config.game_path: - config.set_value("game_path", program_path) - logger.info(_("游戏路径更新成功:{path}").format(path=program_path)) - - Resolution.check_scale(config.game_title_name, 1920, 1080) - return True diff --git a/tasks/game/stop.py b/tasks/game/stop.py deleted file mode 100644 index 4eeefb1a..00000000 --- a/tasks/game/stop.py +++ /dev/null @@ -1,132 +0,0 @@ -from managers.logger_manager import logger -from managers.automation_manager import auto -from managers.translate_manager import _ -from managers.config_manager import config -from managers.notify_manager import notify -from managers.ocr_manager import ocr -from tasks.power.power import Power -from tasks.base.date import Date -from tasks.base.windowswitcher import WindowSwitcher -import psutil -import random -import time -import sys -import os - - -class Stop: - @staticmethod - def terminate_process(name, timeout=10): - # 根据进程名和用户名中止进程 - for proc in psutil.process_iter(attrs=["pid", "name"]): - # 判断当前进程是否属于当前用户,防止多用户环境下关闭其他用户进程 - if name in proc.info["name"]: - system_username = os.getlogin() # 返回的是用户名 - process_username = proc.username() # 返回的是设备名\用户名,需要处理 - process_username = process_username.split("\\")[-1] - if system_username == process_username: - try: - process = psutil.Process(proc.info["pid"]) - process.terminate() - process.wait(timeout) - return True - except ( - psutil.NoSuchProcess, - psutil.TimeoutExpired, - psutil.AccessDenied, - ): - pass - return False - - @staticmethod - def stop_game(): - logger.info(_("开始退出游戏")) - if WindowSwitcher.check_and_switch(config.game_title_name): - if not auto.retry_with_timeout( - lambda: Stop.terminate_process(config.game_process_name), 10, 1 - ): - logger.error(_("游戏退出失败")) - return False - logger.info(_("游戏退出成功")) - else: - logger.warning(_("游戏已经退出了")) - return True - - @staticmethod - def get_wait_time(current_power): - # 距离体力到达配置文件指定的上限剩余秒数 - wait_time_power_limit = (config.power_limit - current_power) * 6 * 60 - # 距离第二天凌晨4点剩余秒数,+30避免显示3点59分不美观,#7 - wait_time_next_day = Date.get_time_next_x_am(config.refresh_hour) + random.randint(30, 600) - # 取最小值 - wait_time = min(wait_time_power_limit, wait_time_next_day) - return wait_time - - @staticmethod - def play_audio(): - if config.play_audio: - logger.info(_("开始播放音频")) - os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" - import pygame.mixer - - pygame.init() - pygame.mixer.music.load("./assets/audio/pa.mp3") - pygame.mixer.music.play() - - while pygame.mixer.music.get_busy(): - pygame.time.Clock().tick(10) - logger.info(_("播放音频完成")) - - @staticmethod - def shutdown(): - logger.warning(_("将在{num}分钟后自动关机").format(num=1)) - time.sleep(60) - os.system("shutdown /s /t 0") - - @staticmethod - def hibernate(): - logger.warning(_("将在{num}分钟后自动休眠").format(num=1)) - time.sleep(60) - os.system("shutdown /h") - - @staticmethod - def sleep(): - logger.warning(_("将在{num}分钟后自动睡眠").format(num=1)) - time.sleep(60) - os.system("powercfg -h off") - # 必须先关闭休眠,否则下面的指令不会进入睡眠,而是优先休眠,无语了,Windows为什么这么难用 - os.system("rundll32.exe powrprof.dll,SetSuspendState 0,1,0") - os.system("powercfg -h on") - - @staticmethod - def after_finish_is_loop(): - current_power = Power.get() - if current_power >= config.power_limit: - logger.info(_("🟣开拓力 >= {limit}").format(limit=config.power_limit)) - logger.info(_("即将再次运行")) - logger.hr(_("完成"), 2) - else: - Stop.stop_game() - wait_time = Stop.get_wait_time(current_power) - future_time = Date.calculate_future_time(wait_time) - logger.info(_("📅将在{future_time}继续运行").format(future_time=future_time)) - notify.notify(_("📅将在{future_time}继续运行").format(future_time=future_time)) - logger.hr(_("完成"), 2) - # 等待状态退出OCR避免内存占用 - ocr.exit_ocr() - time.sleep(wait_time) - - @staticmethod - def after_finish_not_loop(): - if config.after_finish in ["Exit", "Loop", "Shutdown", "Hibernate", "Sleep"]: - Stop.stop_game() - if config.after_finish == "Shutdown": - Stop.shutdown() - elif config.after_finish == "Hibernate": - Stop.hibernate() - elif config.after_finish == "Sleep": - Stop.sleep() - logger.hr(_("完成"), 2) - if config.after_finish not in ["Shutdown", "Hibernate", "Sleep"]: - input(_("按回车键关闭窗口. . .")) - sys.exit(0) diff --git a/tasks/tools/game_screenshot/__init__.py b/tasks/tools/game_screenshot/__init__.py index d62784a7..54f9a4e4 100644 --- a/tasks/tools/game_screenshot/__init__.py +++ b/tasks/tools/game_screenshot/__init__.py @@ -1,4 +1,4 @@ -from tasks.base.windowswitcher import WindowSwitcher +from utils.gamecontroller import GameController from module.automation.screenshot import Screenshot from managers.config_manager import config from managers.logger_manager import logger @@ -8,8 +8,9 @@ def run_gui(): + game = GameController(config.game_path, config.game_process_name, config.game_title_name, 'UnityWndClass', logger) try: - if WindowSwitcher.check_and_switch(config.game_title_name): + if game.switch_to_game(): result = Screenshot.take_screenshot(config.game_title_name) if result: root = tk.Tk() diff --git a/tasks/version/__init__.py b/tasks/version/__init__.py index e69de29b..b6a207e3 100644 --- a/tasks/version/__init__.py +++ b/tasks/version/__init__.py @@ -0,0 +1,38 @@ +from tasks.base.fastest_mirror import FastestMirror +from managers.logger_manager import logger +from managers.translate_manager import _ +from managers.config_manager import config +from managers.notify_manager import notify +from packaging.version import parse +import requests + + +class Version: + @staticmethod + def start(): + try: + if config.update_prerelease_enable: + response = requests.get(FastestMirror.get_github_api_mirror("moesnow", "March7thAssistant", False), timeout=10, headers=config.useragent) + else: + response = requests.get(FastestMirror.get_github_api_mirror("moesnow", "March7thAssistant"), timeout=10, headers=config.useragent) + if not config.check_update: + return + logger.hr(_("开始检测更新"), 0) + if response.status_code == 200: + if config.update_prerelease_enable: + data = response.json()[0] + else: + data = response.json() + version = data["tag_name"] + if parse(version.lstrip('v')) > parse(config.version.lstrip('v')): + notify.notify(_("发现新版本:{v}").format(v=version)) + logger.info(_("发现新版本:{v0} ——→ {v}").format(v0=config.version, v=version)) + logger.info(data["html_url"]) + else: + logger.info(_("已经是最新版本:{v0}").format(v0=config.version)) + else: + logger.warning(_("检测更新失败")) + logger.debug(response.text) + logger.hr(_("完成"), 2) + except Exception: + pass diff --git a/tasks/version/version.py b/tasks/version/version.py deleted file mode 100644 index b6a207e3..00000000 --- a/tasks/version/version.py +++ /dev/null @@ -1,38 +0,0 @@ -from tasks.base.fastest_mirror import FastestMirror -from managers.logger_manager import logger -from managers.translate_manager import _ -from managers.config_manager import config -from managers.notify_manager import notify -from packaging.version import parse -import requests - - -class Version: - @staticmethod - def start(): - try: - if config.update_prerelease_enable: - response = requests.get(FastestMirror.get_github_api_mirror("moesnow", "March7thAssistant", False), timeout=10, headers=config.useragent) - else: - response = requests.get(FastestMirror.get_github_api_mirror("moesnow", "March7thAssistant"), timeout=10, headers=config.useragent) - if not config.check_update: - return - logger.hr(_("开始检测更新"), 0) - if response.status_code == 200: - if config.update_prerelease_enable: - data = response.json()[0] - else: - data = response.json() - version = data["tag_name"] - if parse(version.lstrip('v')) > parse(config.version.lstrip('v')): - notify.notify(_("发现新版本:{v}").format(v=version)) - logger.info(_("发现新版本:{v0} ——→ {v}").format(v0=config.version, v=version)) - logger.info(data["html_url"]) - else: - logger.info(_("已经是最新版本:{v0}").format(v0=config.version)) - else: - logger.warning(_("检测更新失败")) - logger.debug(response.text) - logger.hr(_("完成"), 2) - except Exception: - pass diff --git a/tasks/weekly/forgottenhall.py b/tasks/weekly/forgottenhall.py index 8d3696b7..9512408b 100644 --- a/tasks/weekly/forgottenhall.py +++ b/tasks/weekly/forgottenhall.py @@ -13,15 +13,6 @@ class ForgottenHall: def wait_fight(count, boss_count, max_recursion, switch_team): logger.info(_("进入战斗")) time.sleep(2) - # for i in range(20): - # if auto.find_element("./assets/images/share/base/not_auto.png", "image", 0.95, crop=(0.0 / 1920, 903.0 / 1080, 144.0 / 1920, 120.0 / 1080)): - # logger.info(_("尝试开启自动战斗")) - # auto.press_key("v") - # elif auto.find_element("./assets/images/zh_CN/base/auto.png", "image", 0.95, take_screenshot=False): - # logger.info(_("自动战斗已开启")) - # break - # time.sleep(0.5) - # logger.info(_("等待战斗")) def check_fight(): if auto.find_element("./assets/images/forgottenhall/prepare_fight.png", "image", 0.9, crop=(64 / 1920, 277 / 1080, 167 / 1920, 38 / 1080)): diff --git a/tasks/weekly/universe.py b/tasks/weekly/universe.py index 0cbd6ea1..964a5e0a 100644 --- a/tasks/weekly/universe.py +++ b/tasks/weekly/universe.py @@ -6,8 +6,8 @@ from tasks.base.base import Base from tasks.power.relicset import Relicset from tasks.base.pythonchecker import PythonChecker -from tasks.game.resolution import Resolution -from tasks.base.command import subprocess_with_timeout +from tasks.game import StarRailController +from utils.command import subprocess_with_timeout import subprocess import sys import os @@ -76,7 +76,8 @@ def before_start(): @staticmethod def start(get_reward=False, nums=config.universe_count, save=True): logger.hr(_("准备模拟宇宙"), 0) - Resolution.check(config.game_title_name, 1920, 1080) + game = StarRailController(config.game_path, config.game_process_name, config.game_title_name, 'UnityWndClass', logger) + game.check_resolution(1920, 1080) if Universe.before_start(): screen.change_to('main') diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/utils/command.py b/utils/command.py new file mode 100644 index 00000000..a52b961a --- /dev/null +++ b/utils/command.py @@ -0,0 +1,28 @@ +import subprocess + + +def subprocess_with_timeout(command, timeout, working_directory=None, env=None): + process = None + try: + process = subprocess.Popen(command, cwd=working_directory, env=env) + process.communicate(timeout=timeout) + if process.returncode == 0: + return True + except subprocess.TimeoutExpired: + if process is not None: + process.terminate() + process.wait() + return False + + +def subprocess_with_stdout(command): + try: + # 使用subprocess运行命令并捕获标准输出 + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + # 检查命令是否成功执行 + if result.returncode == 0: + # 返回标准输出的内容 + return result.stdout.strip() + return None + except Exception: + return None diff --git a/tasks/base/date.py b/utils/date.py similarity index 97% rename from tasks/base/date.py rename to utils/date.py index d96ced25..02f20015 100644 --- a/tasks/base/date.py +++ b/utils/date.py @@ -1,58 +1,58 @@ -from datetime import datetime, timedelta - - -class Date: - @staticmethod - def is_next_x_am(timestamp, hour=4): - dt_object = datetime.fromtimestamp(timestamp) - current_time = datetime.now() - - if dt_object.hour < hour: - next_x_am = dt_object.replace(hour=hour, minute=0, second=0, microsecond=0) - else: - next_x_am = dt_object.replace(hour=hour, minute=0, second=0, microsecond=0) + timedelta(days=1) - - if current_time >= next_x_am: - return True - - return False - - @staticmethod - def is_next_mon_x_am(timestamp, hour=4): - dt_object = datetime.fromtimestamp(timestamp) - current_time = datetime.now() - - if dt_object.weekday() == 0 and dt_object.hour < hour: - next_monday_x_am = dt_object.replace(hour=hour, minute=0, second=0, microsecond=0) - else: - days_until_next_monday = (7 - dt_object.weekday()) % 7 if dt_object.weekday() != 0 else 7 - next_monday_x_am = dt_object.replace(hour=hour, minute=0, second=0, microsecond=0) + timedelta(days=days_until_next_monday) - - if current_time >= next_monday_x_am: - return True - - return False - - @staticmethod - def get_time_next_x_am(hour=4): - now = datetime.now() - next_x_am = now.replace(hour=hour, minute=0, second=0, microsecond=0) - - if now >= next_x_am: - next_x_am += timedelta(days=1) - - time_until_next_x_am = next_x_am - now - - return int(time_until_next_x_am.total_seconds()) - - @staticmethod - def calculate_future_time(seconds): - current_time = datetime.now() - future_time = current_time + timedelta(seconds=seconds) - - if future_time.date() == current_time.date(): - return f"今天{future_time.hour}时{future_time.minute}分" - elif future_time.date() == current_time.date() + timedelta(days=1): - return f"明天{future_time.hour}时{future_time.minute}分" - else: - return "输入秒数不合法" +from datetime import datetime, timedelta + + +class Date: + @staticmethod + def is_next_x_am(timestamp, hour=4): + dt_object = datetime.fromtimestamp(timestamp) + current_time = datetime.now() + + if dt_object.hour < hour: + next_x_am = dt_object.replace(hour=hour, minute=0, second=0, microsecond=0) + else: + next_x_am = dt_object.replace(hour=hour, minute=0, second=0, microsecond=0) + timedelta(days=1) + + if current_time >= next_x_am: + return True + + return False + + @staticmethod + def is_next_mon_x_am(timestamp, hour=4): + dt_object = datetime.fromtimestamp(timestamp) + current_time = datetime.now() + + if dt_object.weekday() == 0 and dt_object.hour < hour: + next_monday_x_am = dt_object.replace(hour=hour, minute=0, second=0, microsecond=0) + else: + days_until_next_monday = (7 - dt_object.weekday()) % 7 if dt_object.weekday() != 0 else 7 + next_monday_x_am = dt_object.replace(hour=hour, minute=0, second=0, microsecond=0) + timedelta(days=days_until_next_monday) + + if current_time >= next_monday_x_am: + return True + + return False + + @staticmethod + def get_time_next_x_am(hour=4): + now = datetime.now() + next_x_am = now.replace(hour=hour, minute=0, second=0, microsecond=0) + + if now >= next_x_am: + next_x_am += timedelta(days=1) + + time_until_next_x_am = next_x_am - now + + return int(time_until_next_x_am.total_seconds()) + + @staticmethod + def calculate_future_time(seconds): + current_time = datetime.now() + future_time = current_time + timedelta(seconds=seconds) + + if future_time.date() == current_time.date(): + return f"今天{future_time.hour}时{future_time.minute}分" + elif future_time.date() == current_time.date() + timedelta(days=1): + return f"明天{future_time.hour}时{future_time.minute}分" + else: + return "输入秒数不合法" diff --git a/utils/gamecontroller.py b/utils/gamecontroller.py new file mode 100644 index 00000000..190b0d8b --- /dev/null +++ b/utils/gamecontroller.py @@ -0,0 +1,162 @@ +from typing import Literal, Tuple, Optional +import os +import logging +import time +import psutil +import win32gui +import ctypes + + +class GameController: + def __init__(self, game_path: str, process_name: str, window_name: str, window_class: Optional[str], logger: Optional[logging.Logger] = None) -> None: + self.game_path = os.path.normpath(game_path) + self.process_name = process_name + self.window_name = window_name + self.window_class = window_class + self.logger = logger + + def log_debug(self, message: str) -> None: + """记录调试日志,如果logger不为None""" + if self.logger is not None: + self.logger.debug(message) + + def log_info(self, message: str) -> None: + """记录信息日志,如果logger不为None""" + if self.logger is not None: + self.logger.info(message) + + def log_error(self, message: str) -> None: + """记录错误日志,如果logger不为None""" + if self.logger is not None: + self.logger.error(message) + + def log_warning(self, message: str) -> None: + """记录警告日志,如果logger不为None""" + if self.logger is not None: + self.logger.warning(message) + + def start_game(self) -> bool: + """启动游戏""" + if not os.path.exists(self.game_path): + self.log_error(f"游戏路径不存在:{self.game_path}") + return False + + if not os.system(f'cmd /C start "" "{self.game_path}"'): + self.log_info(f"游戏启动:{self.game_path}") + return True + else: + self.log_error("启动游戏时发生错误") + return False + + @staticmethod + def terminate_named_process(target_process_name, termination_timeout=10): + """ + 根据进程名终止属于当前用户的进程。 + + 参数: + - target_process_name (str): 要终止的进程名。 + - termination_timeout (int, optional): 终止进程前等待的超时时间(秒)。 + + 返回值: + - bool: 如果成功终止进程则返回True,否则返回False。 + """ + system_username = os.getlogin() # 获取当前系统用户名 + # 遍历所有运行中的进程 + for process in psutil.process_iter(attrs=["pid", "name"]): + # 检查当前进程名是否匹配并属于当前用户 + if target_process_name in process.info["name"]: + process_username = process.username().split("\\")[-1] # 从进程所有者中提取用户名 + if system_username == process_username: + proc_to_terminate = psutil.Process(process.info["pid"]) + proc_to_terminate.terminate() # 尝试终止进程 + proc_to_terminate.wait(termination_timeout) # 等待进程终止 + + def stop_game(self) -> bool: + """终止游戏""" + try: + # os.system(f'taskkill /f /im {self.process_name}') + self.terminate_named_process(self.process_name) + self.log_info(f"游戏终止:{self.process_name}") + return True + except Exception as e: + self.log_error(f"终止游戏时发生错误:{e}") + return False + + @staticmethod + def set_foreground_window_with_retry(hwnd): + """尝试将窗口设置为前台,失败时先最小化再恢复。""" + + def toggle_window_state(hwnd, minimize=False): + """最小化或恢复窗口。""" + SW_MINIMIZE = 6 + SW_RESTORE = 9 + state = SW_MINIMIZE if minimize else SW_RESTORE + ctypes.windll.user32.ShowWindow(hwnd, state) + + toggle_window_state(hwnd, minimize=False) + if ctypes.windll.user32.SetForegroundWindow(hwnd) == 0: + toggle_window_state(hwnd, minimize=True) + toggle_window_state(hwnd, minimize=False) + if ctypes.windll.user32.SetForegroundWindow(hwnd) == 0: + raise Exception("Failed to set window foreground") + + def switch_to_game(self) -> bool: + """将游戏窗口切换到前台""" + try: + hwnd = win32gui.FindWindow(self.window_class, self.window_name) + if hwnd == 0: + self.log_debug("游戏窗口未找到") + return False + self.set_foreground_window_with_retry(hwnd) + self.log_info("游戏窗口已切换到前台") + return True + except Exception as e: + self.log_error(f"激活游戏窗口时发生错误:{e}") + return False + + def get_resolution(self) -> Optional[Tuple[int, int]]: + """检查游戏窗口的分辨率""" + try: + hwnd = win32gui.FindWindow(self.window_class, self.window_name) + if hwnd == 0: + self.log_debug("游戏窗口未找到") + return None + _, _, window_width, window_height = win32gui.GetClientRect(hwnd) + return window_width, window_height + except IndexError: + self.log_debug("游戏窗口未找到") + return None + + def shutdown(self, action: Literal['Exit', 'Loop', 'Shutdown', 'Sleep', 'Hibernate'], delay: int = 60) -> bool: + """ + 终止游戏并在指定的延迟后执行系统操作:关机、休眠、睡眠。 + + 参数: + action: 要执行的系统操作。 + delay: 延迟时间,单位为秒,默认为60秒。 + + 返回: + 操作成功执行返回True,否则返回False。 + """ + self.stop_game() + if action not in ["Shutdown", "Hibernate", "Sleep"]: + return True + + self.log_warning(f"将在{delay}秒后开始执行系统操作:{action}") + time.sleep(delay) # 暂停指定的秒数 + + try: + if action == 'Shutdown': + os.system("shutdown /s /t 0") + elif action == 'Sleep': + # 必须先关闭休眠,否则下面的指令不会进入睡眠,而是优先休眠 + os.system("powercfg -h off") + os.system("rundll32.exe powrprof.dll,SetSuspendState 0,1,0") + os.system("powercfg -h on") + elif action == 'Hibernate': + os.system("shutdown /h") + self.log_info(f"执行系统操作:{action}") + return True + except Exception as e: + self.log_error(f"执行系统操作时发生错误:{action}, 错误:{e}") + return False diff --git a/utils/registry/__init__.py b/utils/registry/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/utils/registry/star_rail_resolution.py b/utils/registry/star_rail_resolution.py new file mode 100644 index 00000000..e0efed54 --- /dev/null +++ b/utils/registry/star_rail_resolution.py @@ -0,0 +1,100 @@ +from typing import Tuple, Optional +import winreg +import json + +# Specify the registry key path +registry_key_path = r"SOFTWARE\miHoYo\崩坏:星穹铁道" +# Specify the value name +value_name = "GraphicsSettings_PCResolution_h431323223" + + +def get_game_resolution() -> Optional[Tuple[int, int, bool]]: + """ + Return the game resolution from the registry value. + + This function does not take any parameters. + + Returns: + - If the registry value exists and data is valid, it returns a tuple (width, height, isFullScreen) representing the game resolution. + - If the registry value does not exist or data is invalid, it returns None or raises ValueError. + """ + value = read_registry_value(winreg.HKEY_CURRENT_USER, registry_key_path, value_name) + if value: + data_dict = json.loads(value.decode('utf-8').strip('\x00')) + + # Validate data format + if 'width' in data_dict and 'height' in data_dict and 'isFullScreen' in data_dict: + if isinstance(data_dict['width'], int) and isinstance(data_dict['height'], int) and isinstance(data_dict['isFullScreen'], bool): + return data_dict['width'], data_dict['height'], data_dict['isFullScreen'] + else: + raise ValueError("Registry data is invalid: width, height, and isFullScreen must be of type int, int, and bool respectively.") + else: + raise ValueError("Registry data is missing required fields: width, height, or isFullScreen.") + + return None + + +def set_game_resolution(width: int, height: int, is_fullscreen: bool) -> None: + """ + Set the resolution of the game and whether it should run in fullscreen mode. + + Parameters: + - width: The width of the game window. + - height: The height of the game window. + - is_fullscreen: Whether the game should run in fullscreen mode. + """ + data_dict = { + 'width': width, + 'height': height, + 'isFullScreen': is_fullscreen + } + data = (json.dumps(data_dict) + '\x00').encode('utf-8') + write_registry_value(winreg.HKEY_CURRENT_USER, registry_key_path, value_name, data, winreg.REG_BINARY) + + +def read_registry_value(key, sub_key, value_name): + """ + Read the content of the specified registry value. + + Parameters: + - key: The handle of an open registry key. + - sub_key: The name of the key, relative to key, to open. + - value_name: The name of the value to query. + + Returns: + The content of the specified value in the registry. + """ + try: + # Open the specified registry key + registry_key = winreg.OpenKey(key, sub_key) + # Read the content of the specified value in the registry + value, _ = winreg.QueryValueEx(registry_key, value_name) + # Close the registry key + winreg.CloseKey(registry_key) + return value + except FileNotFoundError: + raise FileNotFoundError(f"Specified registry key or value not found: {sub_key}\\{value_name}") + except Exception as e: + raise Exception(f"Error reading registry value: {e}") + + +def write_registry_value(key, sub_key, value_name, data, mode): + """ + Write a registry value to the specified registry key. + + Parameters: + - key: The registry key. + - sub_key: The subkey under the specified registry key. + - value_name: The name of the registry value. + - data: The data to be written to the registry. + - mode: The type of data. + """ + try: + # Open or create the specified registry key + registry_key = winreg.CreateKey(key, sub_key) + # Write data to the registry + winreg.SetValueEx(registry_key, value_name, 0, mode, data) + # Close the registry key + winreg.CloseKey(registry_key) + except Exception as e: + raise Exception(f"Error writing registry value: {e}") From aa42d972d5187bc4b5089421571672a72de7bd45 Mon Sep 17 00:00:00 2001 From: moesnow <11678347+moesnow@users.noreply.github.com> Date: Sat, 2 Mar 2024 18:10:12 +0800 Subject: [PATCH 2/4] feat: auto hdr --- app/setting_interface.py | 4 +- tasks/game/__init__.py | 26 ++++++++++ utils/registry/game_auto_hdr.py | 85 +++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 utils/registry/game_auto_hdr.py diff --git a/app/setting_interface.py b/app/setting_interface.py index 2d2156b1..3014086f 100644 --- a/app/setting_interface.py +++ b/app/setting_interface.py @@ -505,8 +505,8 @@ def __initCard(self): ) self.autoSetResolutionEnableCard = SwitchSettingCard1( FIF.FULL_SCREEN, - self.tr('启用自动修改分辨率'), - "通过软件启动游戏会自动修改 1920x1080 分辨率,不影响手动启动游戏(未测试国际服)", + self.tr('启用自动修改分辨率并关闭自动 HDR'), + "通过软件启动游戏会自动修改 1920x1080 分辨率并关闭自动 HDR,不影响手动启动游戏(未测试国际服)", "auto_set_resolution_enable" ) self.autoSetGamePathEnableCard = SwitchSettingCard1( diff --git a/tasks/game/__init__.py b/tasks/game/__init__.py index 1a0f6cdb..43234bb8 100644 --- a/tasks/game/__init__.py +++ b/tasks/game/__init__.py @@ -8,6 +8,7 @@ from utils.date import Date from utils.gamecontroller import GameController from utils.registry.star_rail_resolution import get_game_resolution, set_game_resolution +from utils.registry.game_auto_hdr import get_game_auto_hdr, set_game_auto_hdr from typing import Literal, Optional import time import logging @@ -22,6 +23,7 @@ class StarRailController(GameController): def __init__(self, game_path: str, process_name: str, window_name: str, window_class: Optional[str], logger: Optional[logging.Logger] = None) -> None: super().__init__(game_path, process_name, window_name, window_class, logger) self.game_resolution = None + self.game_auto_hdr = None self.screen_resolution = pyautogui.size() def change_resolution(self, width: int, height: int): @@ -46,6 +48,27 @@ def restore_resolution(self): self.log_debug(f"恢复游戏分辨率: {self.game_resolution[0]}x{self.game_resolution[1]} ({'全屏' if self.game_resolution[2] else '窗口'})") except Exception as e: self.log_error("写入注册表值时发生错误:", e) + + def change_auto_hdr(self, status: Literal["enable", "disable", "unset"] = "unset"): + """通过注册表修改游戏自动 HDR 设置""" + status_map = {"enable": "启用", "disable": "禁用", "unset": "未设置"} + try: + self.game_auto_hdr = get_game_auto_hdr(self.game_path) + set_game_auto_hdr(self.game_path, status) + self.log_debug(f"修改游戏自动 HDR: {status_map.get(self.game_auto_hdr)} --> {status_map.get(status)}") + except Exception as e: + self.log_debug(f"修改游戏自动 HDR 设置时发生错误:{e}") + + def restore_auto_hdr(self): + """通过注册表恢复游戏自动 HDR 设置""" + status_map = {"enable": "启用", "disable": "禁用", "unset": "未设置"} + try: + if self.game_auto_hdr: + set_game_auto_hdr(self.game_path, self.game_auto_hdr) + self.log_debug(f"恢复游戏自动 HDR: {status_map.get(self.game_auto_hdr)}") + except Exception as e: + self.log_debug(f"恢复游戏自动 HDR 设置时发生错误:{e}") + def check_resolution(self, target_width: int, target_height: int) -> None: """ 检查游戏窗口的分辨率是否匹配目标分辨率。 @@ -151,6 +174,7 @@ def get_process_path(name): if not game.switch_to_game(): if config.auto_set_resolution_enable: game.change_resolution(1920, 1080) + game.change_auto_hdr("disable") if not game.start_game(): raise Exception("启动游戏失败") @@ -158,10 +182,12 @@ def get_process_path(name): if not wait_until(lambda: game.switch_to_game(), 60): game.restore_resolution() + game.restore_auto_hdr() raise TimeoutError("切换到游戏超时") time.sleep(10) game.restore_resolution() + game.restore_auto_hdr() game.check_resolution_ratio(1920, 1080) if not wait_until(lambda: check_and_click_enter(), 600): diff --git a/utils/registry/game_auto_hdr.py b/utils/registry/game_auto_hdr.py new file mode 100644 index 00000000..ec8e11a4 --- /dev/null +++ b/utils/registry/game_auto_hdr.py @@ -0,0 +1,85 @@ +from typing import Literal +import winreg +import os + + +def get_game_auto_hdr(game_path: str) -> Literal["enable", "disable", "unset"]: + """ + Get the Auto HDR setting for a specific game via Windows Registry. + + Parameters: + - game_path: The file path to the game executable, ensuring Windows path conventions. + + Returns: + - A Literal indicating the status of Auto HDR for the game: "enable", "disable", or "unset". + """ + if not os.path.isabs(game_path): + raise ValueError(f"'{game_path}' is not an absolute path.") + + game_path = os.path.normpath(game_path) + reg_path = r"Software\Microsoft\DirectX\UserGpuPreferences" + reg_key = game_path + + try: + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, reg_path) as key: + existing_value, _ = winreg.QueryValueEx(key, reg_key) + settings = dict(item.split("=") for item in existing_value.split(";") if item) + hdr_status = settings.get("AutoHDREnable", None) + if hdr_status == "2097": + return "enable" + elif hdr_status == "2096": + return "disable" + else: + return "unset" + except FileNotFoundError: + return "unset" + except Exception as e: + raise Exception(f"Error getting Auto HDR status for '{game_path}': {e}") + + +def set_game_auto_hdr(game_path: str, status: Literal["enable", "disable", "unset"] = "unset"): + """ + Set, update, or unset the Auto HDR setting for a specific game via Windows Registry, + without affecting other settings. Ensures the game path is an absolute path + and raises exceptions on errors instead of printing. + + Parameters: + - game_path: The file path to the game executable, ensuring Windows path conventions. + - status: Literal indicating the desired status for Auto HDR. One of "enable", "disable", or "unset". + """ + if not os.path.isabs(game_path): + raise ValueError(f"'{game_path}' is not an absolute path.") + + game_path = os.path.normpath(game_path) + reg_path = r"Software\Microsoft\DirectX\UserGpuPreferences" + reg_key = game_path + + hdr_value = {"enable": "2097", "disable": "2096"}.get(status, None) + + try: + with winreg.CreateKey(winreg.HKEY_CURRENT_USER, reg_path) as key: + if status == "unset": + try: + existing_value, _ = winreg.QueryValueEx(key, reg_key) + settings = dict(item.split("=") for item in existing_value.split(";") if item) + if "AutoHDREnable" in settings: + del settings["AutoHDREnable"] + updated_value = ";".join([f"{k}={v}" for k, v in settings.items()]) + ";" + if settings: + winreg.SetValueEx(key, reg_key, 0, winreg.REG_SZ, updated_value) + else: + winreg.DeleteValue(key, reg_key) + except FileNotFoundError: + pass + else: + try: + existing_value, _ = winreg.QueryValueEx(key, reg_key) + except FileNotFoundError: + existing_value = "" + settings = dict(item.split("=") for item in existing_value.split(";") if item) + if hdr_value is not None: + settings["AutoHDREnable"] = hdr_value + updated_value = ";".join([f"{k}={v}" for k, v in settings.items()]) + ";" + winreg.SetValueEx(key, reg_key, 0, winreg.REG_SZ, updated_value) + except Exception as e: + raise Exception(f"Error setting Auto HDR for '{game_path}' with status '{status}': {e}") From 00b6744a578b7829828052f15bd628b0c4be6f6a Mon Sep 17 00:00:00 2001 From: moesnow <11678347+moesnow@users.noreply.github.com> Date: Sat, 2 Mar 2024 18:28:37 +0800 Subject: [PATCH 3/4] refactor: StarRailController --- tasks/daily/fight.py | 2 +- tasks/game/__init__.py | 132 +++---------------------------- tasks/game/starrailcontroller.py | 116 +++++++++++++++++++++++++++ tasks/weekly/universe.py | 2 +- utils/gamecontroller.py | 4 +- 5 files changed, 130 insertions(+), 126 deletions(-) create mode 100644 tasks/game/starrailcontroller.py diff --git a/tasks/daily/fight.py b/tasks/daily/fight.py index 0c6d791b..b735045a 100644 --- a/tasks/daily/fight.py +++ b/tasks/daily/fight.py @@ -5,7 +5,7 @@ from tasks.base.base import Base from tasks.base.team import Team from tasks.base.pythonchecker import PythonChecker -from tasks.game import StarRailController +from tasks.game.starrailcontroller import StarRailController from utils.command import subprocess_with_timeout import subprocess import sys diff --git a/tasks/game/__init__.py b/tasks/game/__init__.py index 43234bb8..3ff38d3c 100644 --- a/tasks/game/__init__.py +++ b/tasks/game/__init__.py @@ -1,131 +1,19 @@ +import os +import sys +import time +import psutil +import random + +from .starrailcontroller import StarRailController + +from utils.date import Date +from tasks.power.power import Power from managers.logger_manager import logger from managers.screen_manager import screen from managers.automation_manager import auto from managers.config_manager import config from managers.notify_manager import notify from managers.ocr_manager import ocr -from tasks.power.power import Power -from utils.date import Date -from utils.gamecontroller import GameController -from utils.registry.star_rail_resolution import get_game_resolution, set_game_resolution -from utils.registry.game_auto_hdr import get_game_auto_hdr, set_game_auto_hdr -from typing import Literal, Optional -import time -import logging -import pyautogui -import psutil -import random -import sys -import os - - -class StarRailController(GameController): - def __init__(self, game_path: str, process_name: str, window_name: str, window_class: Optional[str], logger: Optional[logging.Logger] = None) -> None: - super().__init__(game_path, process_name, window_name, window_class, logger) - self.game_resolution = None - self.game_auto_hdr = None - self.screen_resolution = pyautogui.size() - - def change_resolution(self, width: int, height: int): - """通过注册表修改游戏分辨率""" - try: - self.game_resolution = get_game_resolution() - if self.game_resolution: - screen_width, screen_height = self.screen_resolution - is_fullscreen = False if screen_width > width and screen_height > height else True - set_game_resolution(width, height, is_fullscreen) - self.log_debug(f"修改游戏分辨率: {self.game_resolution[0]}x{self.game_resolution[1]} ({'全屏' if self.game_resolution[2] else '窗口'}) --> {width}x{height} ({'全屏' if is_fullscreen else '窗口'})") - except FileNotFoundError: - self.log_debug("指定的注册表项未找到") - except Exception as e: - self.log_error("读取注册表值时发生错误:", e) - - def restore_resolution(self): - """通过注册表恢复游戏分辨率""" - try: - if self.game_resolution: - set_game_resolution(self.game_resolution[0], self.game_resolution[1], self.game_resolution[2]) - self.log_debug(f"恢复游戏分辨率: {self.game_resolution[0]}x{self.game_resolution[1]} ({'全屏' if self.game_resolution[2] else '窗口'})") - except Exception as e: - self.log_error("写入注册表值时发生错误:", e) - - def change_auto_hdr(self, status: Literal["enable", "disable", "unset"] = "unset"): - """通过注册表修改游戏自动 HDR 设置""" - status_map = {"enable": "启用", "disable": "禁用", "unset": "未设置"} - try: - self.game_auto_hdr = get_game_auto_hdr(self.game_path) - set_game_auto_hdr(self.game_path, status) - self.log_debug(f"修改游戏自动 HDR: {status_map.get(self.game_auto_hdr)} --> {status_map.get(status)}") - except Exception as e: - self.log_debug(f"修改游戏自动 HDR 设置时发生错误:{e}") - - def restore_auto_hdr(self): - """通过注册表恢复游戏自动 HDR 设置""" - status_map = {"enable": "启用", "disable": "禁用", "unset": "未设置"} - try: - if self.game_auto_hdr: - set_game_auto_hdr(self.game_path, self.game_auto_hdr) - self.log_debug(f"恢复游戏自动 HDR: {status_map.get(self.game_auto_hdr)}") - except Exception as e: - self.log_debug(f"恢复游戏自动 HDR 设置时发生错误:{e}") - - def check_resolution(self, target_width: int, target_height: int) -> None: - """ - 检查游戏窗口的分辨率是否匹配目标分辨率。 - - 如果游戏窗口的分辨率与目标分辨率不匹配,则记录错误并抛出异常。 - 如果桌面分辨率小于目标分辨率,也会记录错误建议。 - - 参数: - target_width (int): 目标分辨率的宽度。 - target_height (int): 目标分辨率的高度。 - """ - resolution = self.get_resolution() - if not resolution: - raise Exception("游戏分辨率获取失败") - window_width, window_height = resolution - - screen_width, screen_height = self.screen_resolution - if window_width != target_width or window_height != target_height: - self.log_error(f"游戏分辨率: {window_width}x{window_height},请在游戏设置内切换为 {target_width}x{target_height} 窗口或全屏运行") - if screen_width < target_width or screen_height < target_height: - self.log_error(f"桌面分辨率: {screen_width}x{screen_height},你可能需要更大的显示器或使用 HDMI/VGA 显卡欺骗器") - raise Exception("游戏分辨率过低") - else: - self.log_debug(f"游戏分辨率: {window_width}x{window_height}") - - def check_resolution_ratio(self, target_width: int, target_height: int) -> None: - """ - 检查游戏窗口的分辨率和比例是否符合目标设置。 - - 如果游戏窗口的分辨率小于目标分辨率或比例不正确,则记录错误并抛出异常。 - 如果桌面分辨率不符合最小推荐值,也会记录错误建议。 - - 参数: - target_width (int): 目标分辨率的宽度。 - target_height (int): 目标分辨率的高度。 - """ - resolution = self.get_resolution() - if not resolution: - raise Exception("游戏分辨率获取失败") - window_width, window_height = resolution - - screen_width, screen_height = self.screen_resolution - - if window_width < target_width or window_height < target_height: - self.log_error(f"游戏分辨率: {window_width}x{window_height} 请在游戏设置内切换为 {target_width}x{target_height} 窗口或全屏运行") - if screen_width < 1920 or screen_height < 1080: - self.log_error(f"桌面分辨率: {screen_width}x{screen_height} 你可能需要更大的显示器或使用 HDMI/VGA 显卡欺骗器") - raise Exception("游戏分辨率过低") - elif abs(window_width / window_height - (target_width / target_height)) > 0.01: - self.log_error(f"游戏分辨率: {window_width}x{window_height} 请在游戏设置内切换为 {target_width}:{target_height} 比例") - raise Exception("游戏分辨率比例不正确") - else: - if window_width != target_width or window_height != target_height: - self.log_warning(f"游戏分辨率: {window_width}x{window_height} ≠ {target_width}x{target_height} 可能出现未预期的错误") - time.sleep(2) - else: - self.log_debug(f"游戏分辨率: {window_width}x{window_height}") class Game: diff --git a/tasks/game/starrailcontroller.py b/tasks/game/starrailcontroller.py new file mode 100644 index 00000000..faff2ab3 --- /dev/null +++ b/tasks/game/starrailcontroller.py @@ -0,0 +1,116 @@ +import time +import logging +import pyautogui +from typing import Literal, Optional +from utils.gamecontroller import GameController +from utils.registry.star_rail_resolution import get_game_resolution, set_game_resolution +from utils.registry.game_auto_hdr import get_game_auto_hdr, set_game_auto_hdr + + +class StarRailController(GameController): + def __init__(self, game_path: str, process_name: str, window_name: str, window_class: Optional[str], logger: Optional[logging.Logger] = None) -> None: + super().__init__(game_path, process_name, window_name, window_class, logger) + self.game_resolution = None + self.game_auto_hdr = None + self.screen_resolution = pyautogui.size() + + def change_resolution(self, width: int, height: int): + """通过注册表修改游戏分辨率""" + try: + self.game_resolution = get_game_resolution() + if self.game_resolution: + screen_width, screen_height = self.screen_resolution + is_fullscreen = False if screen_width > width and screen_height > height else True + set_game_resolution(width, height, is_fullscreen) + self.log_debug(f"修改游戏分辨率: {self.game_resolution[0]}x{self.game_resolution[1]} ({'全屏' if self.game_resolution[2] else '窗口'}) --> {width}x{height} ({'全屏' if is_fullscreen else '窗口'})") + except FileNotFoundError: + self.log_debug("指定的注册表项未找到") + except Exception as e: + self.log_error("读取注册表值时发生错误:", e) + + def restore_resolution(self): + """通过注册表恢复游戏分辨率""" + try: + if self.game_resolution: + set_game_resolution(self.game_resolution[0], self.game_resolution[1], self.game_resolution[2]) + self.log_debug(f"恢复游戏分辨率: {self.game_resolution[0]}x{self.game_resolution[1]} ({'全屏' if self.game_resolution[2] else '窗口'})") + except Exception as e: + self.log_error("写入注册表值时发生错误:", e) + + def change_auto_hdr(self, status: Literal["enable", "disable", "unset"] = "unset"): + """通过注册表修改游戏自动 HDR 设置""" + status_map = {"enable": "启用", "disable": "禁用", "unset": "未设置"} + try: + self.game_auto_hdr = get_game_auto_hdr(self.game_path) + set_game_auto_hdr(self.game_path, status) + self.log_debug(f"修改游戏自动 HDR: {status_map.get(self.game_auto_hdr)} --> {status_map.get(status)}") + except Exception as e: + self.log_debug(f"修改游戏自动 HDR 设置时发生错误:{e}") + + def restore_auto_hdr(self): + """通过注册表恢复游戏自动 HDR 设置""" + status_map = {"enable": "启用", "disable": "禁用", "unset": "未设置"} + try: + if self.game_auto_hdr: + set_game_auto_hdr(self.game_path, self.game_auto_hdr) + self.log_debug(f"恢复游戏自动 HDR: {status_map.get(self.game_auto_hdr)}") + except Exception as e: + self.log_debug(f"恢复游戏自动 HDR 设置时发生错误:{e}") + + def check_resolution(self, target_width: int, target_height: int) -> None: + """ + 检查游戏窗口的分辨率是否匹配目标分辨率。 + + 如果游戏窗口的分辨率与目标分辨率不匹配,则记录错误并抛出异常。 + 如果桌面分辨率小于目标分辨率,也会记录错误建议。 + + 参数: + target_width (int): 目标分辨率的宽度。 + target_height (int): 目标分辨率的高度。 + """ + resolution = self.get_resolution() + if not resolution: + raise Exception("游戏分辨率获取失败") + window_width, window_height = resolution + + screen_width, screen_height = self.screen_resolution + if window_width != target_width or window_height != target_height: + self.log_error(f"游戏分辨率: {window_width}x{window_height},请在游戏设置内切换为 {target_width}x{target_height} 窗口或全屏运行") + if screen_width < target_width or screen_height < target_height: + self.log_error(f"桌面分辨率: {screen_width}x{screen_height},你可能需要更大的显示器或使用 HDMI/VGA 显卡欺骗器") + raise Exception("游戏分辨率过低") + else: + self.log_debug(f"游戏分辨率: {window_width}x{window_height}") + + def check_resolution_ratio(self, target_width: int, target_height: int) -> None: + """ + 检查游戏窗口的分辨率和比例是否符合目标设置。 + + 如果游戏窗口的分辨率小于目标分辨率或比例不正确,则记录错误并抛出异常。 + 如果桌面分辨率不符合最小推荐值,也会记录错误建议。 + + 参数: + target_width (int): 目标分辨率的宽度。 + target_height (int): 目标分辨率的高度。 + """ + resolution = self.get_resolution() + if not resolution: + raise Exception("游戏分辨率获取失败") + window_width, window_height = resolution + + screen_width, screen_height = self.screen_resolution + + if window_width < target_width or window_height < target_height: + self.log_error(f"游戏分辨率: {window_width}x{window_height} 请在游戏设置内切换为 {target_width}x{target_height} 窗口或全屏运行") + if screen_width < 1920 or screen_height < 1080: + self.log_error(f"桌面分辨率: {screen_width}x{screen_height} 你可能需要更大的显示器或使用 HDMI/VGA 显卡欺骗器") + raise Exception("游戏分辨率过低") + elif abs(window_width / window_height - (target_width / target_height)) > 0.01: + self.log_error(f"游戏分辨率: {window_width}x{window_height} 请在游戏设置内切换为 {target_width}:{target_height} 比例") + raise Exception("游戏分辨率比例不正确") + else: + if window_width != target_width or window_height != target_height: + self.log_warning(f"游戏分辨率: {window_width}x{window_height} ≠ {target_width}x{target_height} 可能出现未预期的错误") + time.sleep(2) + else: + self.log_debug(f"游戏分辨率: {window_width}x{window_height}") diff --git a/tasks/weekly/universe.py b/tasks/weekly/universe.py index 964a5e0a..c4df3524 100644 --- a/tasks/weekly/universe.py +++ b/tasks/weekly/universe.py @@ -6,7 +6,7 @@ from tasks.base.base import Base from tasks.power.relicset import Relicset from tasks.base.pythonchecker import PythonChecker -from tasks.game import StarRailController +from tasks.game.starrailcontroller import StarRailController from utils.command import subprocess_with_timeout import subprocess import sys diff --git a/utils/gamecontroller.py b/utils/gamecontroller.py index 190b0d8b..babcfaf1 100644 --- a/utils/gamecontroller.py +++ b/utils/gamecontroller.py @@ -1,10 +1,10 @@ -from typing import Literal, Tuple, Optional import os -import logging import time +import logging import psutil import win32gui import ctypes +from typing import Literal, Tuple, Optional class GameController: From f41773d2fa69a6f95fc11eb78d3260c43cde21cc Mon Sep 17 00:00:00 2001 From: moesnow <11678347+moesnow@users.noreply.github.com> Date: Sat, 2 Mar 2024 19:23:06 +0800 Subject: [PATCH 4/4] feat: unlock 120 fps --- app/tools_interface.py | 47 +++++++++++++++- tasks/game/starrailcontroller.py | 2 +- ...ail_resolution.py => star_rail_setting.py} | 54 +++++++++++++++++-- 3 files changed, 97 insertions(+), 6 deletions(-) rename utils/registry/{star_rail_resolution.py => star_rail_setting.py} (67%) diff --git a/app/tools_interface.py b/app/tools_interface.py index 4f02985e..14f93e56 100644 --- a/app/tools_interface.py +++ b/app/tools_interface.py @@ -1,8 +1,9 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout, QSpacerItem from qfluentwidgets import FluentIcon as FIF -from qfluentwidgets import SettingCardGroup, PushSettingCard, ScrollArea +from qfluentwidgets import SettingCardGroup, PushSettingCard, ScrollArea, InfoBar, InfoBarPosition from .common.style_sheet import StyleSheet +from utils.registry.star_rail_setting import get_game_fps, set_game_fps class ToolsInterface(ScrollArea): @@ -27,6 +28,12 @@ def __init__(self, parent=None): self.tr("游戏截图"), self.tr("检查程序获取的图像是否正确,支持OCR识别文字(可用于复制副本名称)") ) + self.unlockfpsCard = PushSettingCard( + self.tr('解锁'), + FIF.SPEED_HIGH, + self.tr("解锁帧率"), + self.tr("通过修改注册表解锁120帧率,如已解锁,再次点击将恢复60帧率(未测试国际服)") + ) self.__initWidget() @@ -49,6 +56,7 @@ def __initLayout(self): self.ToolsGroup.addSettingCard(self.automaticPlotCard) self.ToolsGroup.addSettingCard(self.gameScreenshotCard) + self.ToolsGroup.addSettingCard(self.unlockfpsCard) self.ToolsGroup.titleLabel.setHidden(True) @@ -70,6 +78,43 @@ def __onAutomaticPlotCardClicked(self): from tasks.tools.automatic_plot import automatic_plot automatic_plot() + def __onUnlockfpsCardClicked(self): + try: + fps = get_game_fps() + if fps == 120: + set_game_fps(60) + InfoBar.success( + title=self.tr('恢复60成功(^∀^●)'), + content="", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=1000, + parent=self + ) + else: + set_game_fps(120) + InfoBar.success( + title=self.tr('解锁120成功(^∀^●)'), + content="", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=1000, + parent=self + ) + except: + InfoBar.warning( + title=self.tr('解锁失败(╥╯﹏╰╥)'), + content="", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=1000, + parent=self + ) + def __connectSignalToSlot(self): self.gameScreenshotCard.clicked.connect(self.__onGameScreenshotCardClicked) self.automaticPlotCard.clicked.connect(self.__onAutomaticPlotCardClicked) + self.unlockfpsCard.clicked.connect(self.__onUnlockfpsCardClicked) diff --git a/tasks/game/starrailcontroller.py b/tasks/game/starrailcontroller.py index faff2ab3..09ebc14a 100644 --- a/tasks/game/starrailcontroller.py +++ b/tasks/game/starrailcontroller.py @@ -3,7 +3,7 @@ import pyautogui from typing import Literal, Optional from utils.gamecontroller import GameController -from utils.registry.star_rail_resolution import get_game_resolution, set_game_resolution +from utils.registry.star_rail_setting import get_game_resolution, set_game_resolution from utils.registry.game_auto_hdr import get_game_auto_hdr, set_game_auto_hdr diff --git a/utils/registry/star_rail_resolution.py b/utils/registry/star_rail_setting.py similarity index 67% rename from utils/registry/star_rail_resolution.py rename to utils/registry/star_rail_setting.py index e0efed54..a839fa7c 100644 --- a/utils/registry/star_rail_resolution.py +++ b/utils/registry/star_rail_setting.py @@ -5,7 +5,8 @@ # Specify the registry key path registry_key_path = r"SOFTWARE\miHoYo\崩坏:星穹铁道" # Specify the value name -value_name = "GraphicsSettings_PCResolution_h431323223" +resolution_value_name = "GraphicsSettings_PCResolution_h431323223" +graphics_value_name = "GraphicsSettings_Model_h2986158309" def get_game_resolution() -> Optional[Tuple[int, int, bool]]: @@ -18,7 +19,7 @@ def get_game_resolution() -> Optional[Tuple[int, int, bool]]: - If the registry value exists and data is valid, it returns a tuple (width, height, isFullScreen) representing the game resolution. - If the registry value does not exist or data is invalid, it returns None or raises ValueError. """ - value = read_registry_value(winreg.HKEY_CURRENT_USER, registry_key_path, value_name) + value = read_registry_value(winreg.HKEY_CURRENT_USER, registry_key_path, resolution_value_name) if value: data_dict = json.loads(value.decode('utf-8').strip('\x00')) @@ -49,7 +50,52 @@ def set_game_resolution(width: int, height: int, is_fullscreen: bool) -> None: 'isFullScreen': is_fullscreen } data = (json.dumps(data_dict) + '\x00').encode('utf-8') - write_registry_value(winreg.HKEY_CURRENT_USER, registry_key_path, value_name, data, winreg.REG_BINARY) + write_registry_value(winreg.HKEY_CURRENT_USER, registry_key_path, resolution_value_name, data, winreg.REG_BINARY) + + +def get_game_fps() -> Optional[int]: + """ + Return the game FPS settings from the registry value. + + This function does not take any parameters. + """ + value = read_registry_value(winreg.HKEY_CURRENT_USER, registry_key_path, graphics_value_name) + if value: + data_dict = json.loads(value.decode('utf-8').strip('\x00')) + + # Validate data format + if 'FPS' in data_dict: + if isinstance(data_dict['FPS'], int): + return data_dict['FPS'] + else: + raise ValueError("Registry data is invalid: FPS must be of type int.") + else: + raise ValueError("Registry data is missing required fields: FPS.") + + return None + + +def set_game_fps(fps: int) -> None: + """ + Set the FPS of the game. + + Parameters: + - fps + """ + value = read_registry_value(winreg.HKEY_CURRENT_USER, registry_key_path, graphics_value_name) + + data_dict = json.loads(value.decode('utf-8').strip('\x00')) + + # Validate data format + if 'FPS' in data_dict: + if isinstance(data_dict['FPS'], int): + data_dict['FPS'] = fps + data = (json.dumps(data_dict) + '\x00').encode('utf-8') + write_registry_value(winreg.HKEY_CURRENT_USER, registry_key_path, graphics_value_name, data, winreg.REG_BINARY) + else: + raise ValueError("Registry data is invalid: FPS must be of type int.") + else: + raise ValueError("Registry data is missing required fields: FPS.") def read_registry_value(key, sub_key, value_name): @@ -78,7 +124,7 @@ def read_registry_value(key, sub_key, value_name): raise Exception(f"Error reading registry value: {e}") -def write_registry_value(key, sub_key, value_name, data, mode): +def write_registry_value(key, sub_key, value_name, data, mode) -> None: """ Write a registry value to the specified registry key.