From 09d68834322cc3237693be6fadfd34ff49a216ea Mon Sep 17 00:00:00 2001 From: hyb-oyqq <1512383570@qq.com> Date: Fri, 8 Aug 2025 11:27:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E4=BC=98=E5=8C=96=E8=A7=A3?= =?UTF-8?q?=E5=8E=8B=E5=92=8C=E5=93=88=E5=B8=8C=E9=AA=8C=E8=AF=81=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在解压线程中添加已解压文件路径参数,支持直接使用已解压的补丁文件,提升解压效率。 - 更新下载管理器,简化下载成功后的处理逻辑,直接进入解压阶段,去除冗余的哈希验证步骤。 - 在离线模式管理器中增强哈希验证功能,确保在解压后进行哈希校验,提升补丁文件的完整性检查。 - 增强日志记录,确保在关键操作中提供详细的调试信息,便于后续排查和用户反馈。 --- source/core/download_manager.py | 205 +++++++----- source/core/extraction_handler.py | 152 ++++++++- source/core/offline_mode_manager.py | 463 ++++++++++++++++++++-------- source/main_window.py | 5 +- source/workers/extraction_thread.py | 43 ++- source/workers/hash_thread.py | 38 ++- 6 files changed, 657 insertions(+), 249 deletions(-) diff --git a/source/core/download_manager.py b/source/core/download_manager.py index 7a20866..cea7803 100644 --- a/source/core/download_manager.py +++ b/source/core/download_manager.py @@ -763,91 +763,21 @@ class DownloadManager: self.on_download_stopped() return - # 下载成功后,使用与离线模式相同的哈希校验机制 + # 下载成功后,直接进入解压阶段 debug_mode = self.is_debug_mode() - if debug_mode: - logger.debug(f"DEBUG: 下载完成,开始验证补丁文件哈希: {_7z_path}") - # 关闭进度窗口 if hasattr(self.main_window, 'progress_window') and self.main_window.progress_window: if self.main_window.progress_window.isVisible(): self.main_window.progress_window.accept() self.main_window.progress_window = None - # 使用与离线模式相同的哈希校验机制 - from utils.helpers import ProgressHashVerifyDialog - from data.config import PLUGIN_HASH - from workers.hash_thread import OfflineHashVerifyThread - - # 创建并显示进度对话框 - progress_dialog = ProgressHashVerifyDialog( - f"验证补丁文件 - {APP_NAME}", - f"正在验证 {game_version} 的补丁文件完整性...", - self.main_window - ) - - # 创建哈希验证线程 - hash_thread = OfflineHashVerifyThread(game_version, _7z_path, PLUGIN_HASH, self.main_window) - - # 连接信号 - hash_thread.progress.connect(progress_dialog.update_progress) - hash_thread.finished.connect(lambda result, error: self._on_hash_verify_finished( - result, error, progress_dialog, _7z_path, game_folder, plugin_path, game_version - )) - - # 启动线程 - hash_thread.start() - - # 显示对话框,阻塞直到对话框关闭 - result = progress_dialog.exec() - - # 如果用户取消了验证,停止线程 - if result == ProgressHashVerifyDialog.Rejected and hash_thread.isRunning(): - if debug_mode: - logger.debug(f"DEBUG: 用户取消了哈希验证") - hash_thread.terminate() - hash_thread.wait() - # 取消后继续下一个任务 - self.next_download_task() - - def _on_hash_verify_finished(self, result, error, dialog, _7z_path, game_folder, plugin_path, game_version): - """哈希验证线程完成后的回调 - - Args: - result: 验证结果 - error: 错误信息 - dialog: 进度对话框 - _7z_path: 7z文件保存路径 - game_folder: 游戏文件夹路径 - plugin_path: 插件路径 - game_version: 游戏版本 - """ - debug_mode = self.is_debug_mode() - - # 存储结果到对话框,以便在exec()返回后获取 - dialog.hash_result = result - - if result: - if debug_mode: - logger.debug(f"DEBUG: 哈希验证成功") - dialog.set_status("验证成功") - # 短暂延时后关闭对话框 - QTimer.singleShot(500, dialog.accept) + if debug_mode: + logger.debug(f"DEBUG: 下载完成,直接进入解压阶段") - # 验证成功,进入解压阶段 - self.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version) - else: - if debug_mode: - logger.debug(f"DEBUG: 哈希验证失败: {error}") - dialog.set_status(f"验证失败: {error}") - dialog.set_message("补丁文件验证失败,可能已损坏或被篡改。") - # 将取消按钮改为关闭按钮 - dialog.cancel_button.setText("关闭") - # 不自动关闭,让用户查看错误信息 - - # 在用户关闭对话框后,继续下一个任务 - dialog.rejected.connect(lambda: self.next_download_task()) + # 直接进入解压阶段 + self.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version) + self.main_window.extraction_handler.extraction_finished.connect(self.on_extraction_finished) def on_extraction_finished(self, continue_download): """解压完成后的回调,决定是否继续下载队列 @@ -895,4 +825,125 @@ class DownloadManager: def show_download_thread_settings(self): """显示下载线程设置对话框""" - return self.download_task_manager.show_download_thread_settings() \ No newline at end of file + return self.download_task_manager.show_download_thread_settings() + + def direct_download_action(self, games_to_download): + """直接下载指定游戏的补丁,绕过补丁判断,用于从离线模式转接过来的任务 + + Args: + games_to_download: 要下载的游戏列表 + """ + debug_mode = self.is_debug_mode() + if debug_mode: + logger.debug(f"DEBUG: 直接下载模式,绕过补丁判断,游戏列表: {games_to_download}") + + if not self.selected_folder: + QtWidgets.QMessageBox.warning( + self.main_window, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n" + ) + return + + # 识别游戏目录 + game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder) + + if not game_dirs: + QtWidgets.QMessageBox.warning( + self.main_window, f"通知 - {APP_NAME}", "\n未在选择的目录中找到支持的游戏\n" + ) + self.main_window.setEnabled(True) + self.main_window.ui.start_install_text.setText("开始安装") + return + + # 过滤出存在的游戏目录 + selected_game_dirs = {game: game_dirs[game] for game in games_to_download if game in game_dirs} + + if not selected_game_dirs: + QtWidgets.QMessageBox.warning( + self.main_window, f"通知 - {APP_NAME}", "\n未找到指定游戏的安装目录\n" + ) + self.main_window.setEnabled(True) + self.main_window.ui.start_install_text.setText("开始安装") + return + + self.main_window.setEnabled(False) + + # 获取下载配置 + config = self.get_download_url() + if not config: + QtWidgets.QMessageBox.critical( + self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n" + ) + self.main_window.setEnabled(True) + self.main_window.ui.start_install_text.setText("开始安装") + return + + # 填充下载队列 + self._fill_direct_download_queue(config, selected_game_dirs) + + if not self.download_queue: + # 所有下载任务都已完成,进行后检查 + if debug_mode: + logger.debug("DEBUG: 所有下载任务完成,进行后检查") + # 使用patch_detector进行安装后哈希比较 + self.main_window.patch_detector.after_hash_compare() + return + + # 显示Cloudflare优化选项 + self._show_cloudflare_option() + + def _fill_direct_download_queue(self, config, game_dirs): + """直接填充下载队列,不检查补丁是否已安装 + + Args: + config: 包含下载URL的配置字典 + game_dirs: 包含游戏文件夹路径的字典 + """ + self.download_queue.clear() + + if not hasattr(self.main_window, 'download_queue_history'): + self.main_window.download_queue_history = [] + + debug_mode = self.is_debug_mode() + if debug_mode: + logger.debug(f"DEBUG: 直接填充下载队列, 游戏目录: {game_dirs}") + + # 记录要下载的游戏,用于历史记录 + games_to_download = list(game_dirs.keys()) + self.main_window.download_queue_history = games_to_download + + for i in range(1, 5): + game_version = f"NEKOPARA Vol.{i}" + if game_version in game_dirs: + # 从配置中获取下载URL + url_key = f"vol.{i}.data" + if url_key in config and "url" in config[url_key]: + url = config[url_key]["url"] + + game_folder = game_dirs[game_version] + if debug_mode: + logger.debug(f"DEBUG: 添加下载任务 {game_version}: {game_folder}") + + _7z_path = os.path.join(PLUGIN, f"vol.{i}.7z") + plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) + self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) + else: + if debug_mode: + logger.warning(f"DEBUG: 未找到 {game_version} 的下载URL") + + game_version = "NEKOPARA After" + if game_version in game_dirs: + # 从配置中获取下载URL + url_key = "after.data" + if url_key in config and "url" in config[url_key]: + url = config[url_key]["url"] + + game_folder = game_dirs[game_version] + if debug_mode: + logger.debug(f"DEBUG: 添加下载任务 {game_version}: {game_folder}") + + _7z_path = os.path.join(PLUGIN, "after.7z") + plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) + self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) + else: + if debug_mode: + logger.warning(f"DEBUG: 未找到 {game_version} 的下载URL") \ No newline at end of file diff --git a/source/core/extraction_handler.py b/source/core/extraction_handler.py index f0d00e8..777762e 100644 --- a/source/core/extraction_handler.py +++ b/source/core/extraction_handler.py @@ -1,7 +1,13 @@ import os +import shutil from PySide6 import QtWidgets from PySide6.QtWidgets import QMessageBox +from PySide6.QtCore import QTimer +from utils.logger import setup_logger + +# 初始化logger +logger = setup_logger("extraction_handler") class ExtractionHandler: """解压处理器,负责管理解压任务和结果处理""" @@ -15,7 +21,7 @@ class ExtractionHandler: self.main_window = main_window self.APP_NAME = main_window.APP_NAME if hasattr(main_window, 'APP_NAME') else "" - def start_extraction(self, _7z_path, game_folder, plugin_path, game_version): + def start_extraction(self, _7z_path, game_folder, plugin_path, game_version, extracted_path=None): """开始解压任务 Args: @@ -23,6 +29,7 @@ class ExtractionHandler: game_folder: 游戏文件夹路径 plugin_path: 插件路径 game_version: 游戏版本名称 + extracted_path: 已解压的补丁文件路径,如果提供则直接使用它而不进行解压 """ # 检查是否处于离线模式 is_offline = False @@ -37,13 +44,13 @@ class ExtractionHandler: # 创建并启动解压线程 self.main_window.extraction_thread = self.main_window.create_extraction_thread( - _7z_path, game_folder, plugin_path, game_version + _7z_path, game_folder, plugin_path, game_version, extracted_path ) - self.main_window.extraction_thread.finished.connect(self.on_extraction_finished) + self.main_window.extraction_thread.finished.connect(self.on_extraction_finished_with_hash_check) self.main_window.extraction_thread.start() - def on_extraction_finished(self, success, error_message, game_version): - """解压完成后的处理 + def on_extraction_finished_with_hash_check(self, success, error_message, game_version): + """解压完成后进行哈希校验 Args: success: 是否解压成功 @@ -55,7 +62,7 @@ class ExtractionHandler: self.main_window.hash_msg_box.close() self.main_window.hash_msg_box = None - # 处理解压结果 + # 如果解压失败,显示错误并询问是否继续 if not success: # 临时启用窗口以显示错误消息 self.main_window.setEnabled(True) @@ -82,8 +89,137 @@ class ExtractionHandler: self.main_window.ui.start_install_text.setText("开始安装") # 通知DownloadManager停止下载队列 self.main_window.download_manager.on_extraction_finished(False) + return + + # 解压成功,进行哈希校验 + self._perform_hash_check(game_version) + + def _perform_hash_check(self, game_version): + """解压成功后进行哈希校验 + + Args: + game_version: 游戏版本 + """ + # 获取安装路径 + install_paths = {} + if hasattr(self.main_window, 'game_detector') and hasattr(self.main_window, 'download_manager'): + game_dirs = self.main_window.game_detector.identify_game_directories_improved( + self.main_window.download_manager.selected_folder + ) + + for game, info in self.main_window.GAME_INFO.items(): + if game in game_dirs and game == game_version: + game_dir = game_dirs[game] + install_path = os.path.join(game_dir, os.path.basename(info["install_path"])) + install_paths[game] = install_path + break + + if not install_paths: + # 如果找不到安装路径,直接认为安装成功 + logger.warning(f"未找到 {game_version} 的安装路径,跳过哈希校验") + self.main_window.installed_status[game_version] = True + self.main_window.download_manager.on_extraction_finished(True) + return + + # 显示哈希校验窗口 + self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="post") + + # 创建并启动哈希线程进行校验 + self.main_window.hash_thread = self.main_window.create_hash_thread( + "after", + install_paths, + self.main_window.plugin_hash, + self.main_window.installed_status + ) + self.main_window.hash_thread.after_finished.connect(self.on_hash_check_finished) + self.main_window.hash_thread.start() + + def on_hash_check_finished(self, result): + """哈希校验完成后的处理 + + Args: + result: 校验结果,包含通过状态、游戏版本和消息 + """ + # 关闭哈希检查窗口 + if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible(): + self.main_window.hash_msg_box.close() + self.main_window.hash_msg_box = None + + if not result["passed"]: + # 校验失败,删除已解压的文件并提示重新下载 + game_version = result["game"] + error_message = result["message"] + + # 临时启用窗口以显示错误消息 + self.main_window.setEnabled(True) + + # 获取安装路径 + install_path = None + if hasattr(self.main_window, 'game_detector') and hasattr(self.main_window, 'download_manager'): + game_dirs = self.main_window.game_detector.identify_game_directories_improved( + self.main_window.download_manager.selected_folder + ) + + if game_version in game_dirs and game_version in self.main_window.GAME_INFO: + game_dir = game_dirs[game_version] + install_path = os.path.join(game_dir, os.path.basename(self.main_window.GAME_INFO[game_version]["install_path"])) + + # 如果找到安装路径,尝试删除已解压的文件 + if install_path and os.path.exists(install_path): + try: + os.remove(install_path) + logger.info(f"已删除校验失败的文件: {install_path}") + except Exception as e: + logger.error(f"删除文件失败: {e}") + + # 显示错误消息并询问是否重试 + reply = QtWidgets.QMessageBox.question( + self.main_window, + f"校验失败 - {self.APP_NAME}", + f"{error_message}\n\n是否重新下载并安装?", + QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, + QtWidgets.QMessageBox.StandardButton.Yes + ) + + if reply == QtWidgets.QMessageBox.StandardButton.Yes: + # 重新下载,将游戏重新添加到下载队列 + self.main_window.setEnabled(False) + self.main_window.installed_status[game_version] = False + + # 获取游戏目录和下载URL + if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window, 'game_detector'): + game_dirs = self.main_window.game_detector.identify_game_directories_improved( + self.main_window.download_manager.selected_folder + ) + + if game_version in game_dirs: + # 重新将游戏添加到下载队列 + self.main_window.download_manager.download_queue.appendleft([game_version]) + # 继续下一个下载任务 + self.main_window.download_manager.next_download_task() + else: + # 如果找不到游戏目录,继续下一个 + self.main_window.download_manager.on_extraction_finished(True) + else: + # 如果无法重新下载,继续下一个 + self.main_window.download_manager.on_extraction_finished(True) + else: + # 用户选择不重试,继续下一个 + self.main_window.installed_status[game_version] = False + self.main_window.download_manager.on_extraction_finished(True) else: - # 更新安装状态 + # 校验通过,更新安装状态 self.main_window.installed_status[game_version] = True # 通知DownloadManager继续下一个下载任务 - self.main_window.download_manager.on_extraction_finished(True) \ No newline at end of file + self.main_window.download_manager.on_extraction_finished(True) + + def on_extraction_finished(self, success, error_message, game_version): + """兼容旧版本的回调函数 + + Args: + success: 是否解压成功 + error_message: 错误信息 + game_version: 游戏版本 + """ + # 调用新的带哈希校验的回调函数 + self.on_extraction_finished_with_hash_check(success, error_message, game_version) \ No newline at end of file diff --git a/source/core/offline_mode_manager.py b/source/core/offline_mode_manager.py index 8851c32..3daaf68 100644 --- a/source/core/offline_mode_manager.py +++ b/source/core/offline_mode_manager.py @@ -4,8 +4,9 @@ import shutil import tempfile import py7zr import traceback -from PySide6.QtWidgets import QMessageBox +from PySide6 import QtWidgets, QtCore from PySide6.QtCore import QTimer +from PySide6.QtWidgets import QMessageBox from data.config import PLUGIN, PLUGIN_HASH, GAME_INFO from utils import msgbox_frame @@ -254,7 +255,7 @@ class OfflineModeManager: # 连接信号 hash_thread.progress.connect(progress_dialog.update_progress) - hash_thread.finished.connect(lambda result, error: self._on_hash_verify_finished(result, error, progress_dialog)) + hash_thread.finished.connect(lambda result, error, extracted_path: self._on_hash_verify_finished(result, error, extracted_path, progress_dialog)) # 启动线程 hash_thread.start() @@ -273,12 +274,13 @@ class OfflineModeManager: # 返回对话框中存储的验证结果 return hasattr(progress_dialog, 'hash_result') and progress_dialog.hash_result - def _on_hash_verify_finished(self, result, error, dialog): + def _on_hash_verify_finished(self, result, error, extracted_path, dialog): """哈希验证线程完成后的回调 Args: result: 验证结果 error: 错误信息 + extracted_path: 解压后的补丁文件路径,如果哈希验证成功则包含此路径 dialog: 进度对话框 """ debug_mode = self._is_debug_mode() @@ -289,6 +291,8 @@ class OfflineModeManager: if result: if debug_mode: logger.debug(f"DEBUG: 哈希验证成功") + if extracted_path: + logger.debug(f"DEBUG: 解压后的补丁文件路径: {extracted_path}") dialog.set_status("验证成功") # 短暂延时后关闭对话框 QTimer.singleShot(500, dialog.accept) @@ -301,6 +305,173 @@ class OfflineModeManager: dialog.cancel_button.setText("关闭") # 不自动关闭,让用户查看错误信息 + def _on_offline_install_hash_finished(self, result, error, extracted_path, dialog, game_version, _7z_path, game_folder, plugin_path, install_tasks): + """离线安装哈希验证线程完成后的回调 + + Args: + result: 验证结果 + error: 错误信息 + extracted_path: 解压后的补丁文件路径 + dialog: 进度对话框 + game_version: 游戏版本 + _7z_path: 7z文件路径 + game_folder: 游戏文件夹路径 + plugin_path: 插件路径 + install_tasks: 剩余的安装任务列表 + """ + debug_mode = self._is_debug_mode() + + # 导入所需模块 + from data.config import GAME_INFO + + # 存储结果到对话框,以便在exec()返回后获取 + dialog.hash_result = result + + # 关闭哈希验证窗口 + if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible(): + self.main_window.hash_msg_box.close() + self.main_window.hash_msg_box = None + + if not result: + # 哈希验证失败 + if debug_mode: + logger.warning(f"DEBUG: 补丁文件哈希验证失败: {error}") + + # 显示错误消息 + msgbox_frame( + f"哈希验证失败 - {self.app_name}", + f"\n{game_version} 的补丁文件哈希验证失败,可能已损坏或被篡改。\n\n跳过此游戏的安装。\n", + QMessageBox.StandardButton.Ok + ).exec() + + # 继续下一个任务 + self.process_next_offline_install_task(install_tasks) + return + + # 哈希验证成功,直接进行安装(复制文件) + if debug_mode: + logger.debug(f"DEBUG: 哈希验证成功,直接进行安装") + if extracted_path: + logger.debug(f"DEBUG: 使用已解压的补丁文件: {extracted_path}") + + # 显示安装进度窗口 + self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="offline_installation", is_offline=True) + + try: + # 直接复制已解压的文件到游戏目录 + os.makedirs(game_folder, exist_ok=True) + + # 获取目标文件路径 + target_file = None + if "Vol.1" in game_version: + target_file = os.path.join(game_folder, "adultsonly.xp3") + elif "Vol.2" in game_version: + target_file = os.path.join(game_folder, "adultsonly.xp3") + elif "Vol.3" in game_version: + target_file = os.path.join(game_folder, "update00.int") + elif "Vol.4" in game_version: + target_file = os.path.join(game_folder, "vol4adult.xp3") + elif "After" in game_version: + target_file = os.path.join(game_folder, "afteradult.xp3") + + if not target_file: + raise ValueError(f"未知的游戏版本: {game_version}") + + # 复制文件 + shutil.copy2(extracted_path, target_file) + + # 对于NEKOPARA After,还需要复制签名文件 + if game_version == "NEKOPARA After": + # 从已解压文件的目录中获取签名文件 + extracted_dir = os.path.dirname(extracted_path) + sig_filename = os.path.basename(GAME_INFO[game_version]["sig_path"]) + sig_path = os.path.join(extracted_dir, sig_filename) + + # 如果签名文件存在,则复制它 + if os.path.exists(sig_path): + shutil.copy(sig_path, game_folder) + else: + # 如果签名文件不存在,则使用原始路径 + sig_path = os.path.join(PLUGIN, GAME_INFO[game_version]["sig_path"]) + shutil.copy(sig_path, game_folder) + + # 更新安装状态 + self.main_window.installed_status[game_version] = True + + # 添加到已安装游戏列表 + if game_version not in self.installed_games: + self.installed_games.append(game_version) + + if debug_mode: + logger.debug(f"DEBUG: 成功安装 {game_version} 补丁文件") + + # 关闭安装进度窗口 + if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible(): + self.main_window.hash_msg_box.close() + self.main_window.hash_msg_box = None + + # 继续下一个任务 + self.process_next_offline_install_task(install_tasks) + except Exception as e: + if debug_mode: + logger.error(f"DEBUG: 安装补丁文件失败: {e}") + + # 关闭安装进度窗口 + if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible(): + self.main_window.hash_msg_box.close() + self.main_window.hash_msg_box = None + + # 显示错误消息 + msgbox_frame( + f"安装错误 - {self.app_name}", + f"\n{game_version} 的安装过程中发生错误: {str(e)}\n\n跳过此游戏的安装。\n", + QMessageBox.StandardButton.Ok + ).exec() + + # 继续下一个任务 + self.process_next_offline_install_task(install_tasks) + + def _on_extraction_finished_with_hash_check(self, success, error_message, game_version, install_tasks): + """解压完成后进行哈希校验 + + Args: + success: 是否解压成功 + error_message: 错误信息 + game_version: 游戏版本 + install_tasks: 剩余的安装任务列表 + """ + # 这个方法已不再使用,保留为空以兼容旧版本调用 + pass + + def on_extraction_thread_finished(self, success, error_message, game_version, install_tasks): + """解压线程完成后的处理(兼容旧版本) + + Args: + success: 是否解压成功 + error_message: 错误信息 + game_version: 游戏版本 + install_tasks: 剩余的安装任务列表 + """ + # 这个方法已不再使用,但为了兼容性,我们直接处理下一个任务 + if success: + # 更新安装状态 + self.main_window.installed_status[game_version] = True + + # 添加到已安装游戏列表 + if game_version not in self.installed_games: + self.installed_games.append(game_version) + else: + # 更新安装状态 + self.main_window.installed_status[game_version] = False + + # 显示错误消息 + debug_mode = self._is_debug_mode() + if debug_mode: + logger.error(f"DEBUG: 解压失败: {error_message}") + + # 继续下一个任务 + self.process_next_offline_install_task(install_tasks) + def install_offline_patches(self, selected_games): """直接安装离线补丁,完全绕过下载模块 @@ -406,16 +577,77 @@ class OfflineModeManager: else: if debug_mode: logger.warning("DEBUG: 没有可安装的游戏,安装流程结束") - msgbox_frame( - f"离线安装信息 - {self.app_name}", - "\n没有可安装的游戏或未找到对应的离线补丁文件。\n", - QMessageBox.StandardButton.Ok - ).exec() - self.main_window.setEnabled(True) - self.main_window.ui.start_install_text.setText("开始安装") + + # 检查是否有未找到离线补丁文件的游戏 + if self.missing_offline_patches: + if debug_mode: + logger.debug(f"DEBUG: 有未找到离线补丁文件的游戏: {self.missing_offline_patches}") + + # 询问用户是否切换到在线模式 + msg_box = msgbox_frame( + f"离线安装信息 - {self.app_name}", + f"\n本地未发现对应离线文件,是否切换为在线模式安装?\n\n以下游戏未找到对应的离线补丁文件:\n\n{chr(10).join(self.missing_offline_patches)}\n", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + result = msg_box.exec() + + if result == QMessageBox.StandardButton.Yes: + if debug_mode: + logger.debug("DEBUG: 用户选择切换到在线模式") + + # 切换到在线模式 + if hasattr(self.main_window, 'ui_manager'): + self.main_window.ui_manager.switch_work_mode("online") + + # 直接启动下载流程 + self.main_window.setEnabled(True) + # 保存当前选择的游戏列表,以便在线模式使用 + missing_games = self.missing_offline_patches.copy() + # 启动下载流程 + QTimer.singleShot(500, lambda: self._start_online_download(missing_games)) + else: + if debug_mode: + logger.debug("DEBUG: 用户选择不切换到在线模式") + + # 恢复UI状态 + self.main_window.setEnabled(True) + self.main_window.ui.start_install_text.setText("开始安装") + else: + # 没有缺少离线补丁的游戏,显示一般消息 + msgbox_frame( + f"离线安装信息 - {self.app_name}", + "\n没有可安装的游戏或未找到对应的离线补丁文件。\n", + QMessageBox.StandardButton.Ok + ).exec() + self.main_window.setEnabled(True) + self.main_window.ui.start_install_text.setText("开始安装") return True + def _start_online_download(self, games_to_download): + """启动在线下载流程 + + Args: + games_to_download: 要下载的游戏列表 + """ + debug_mode = self._is_debug_mode() + if debug_mode: + logger.debug(f"DEBUG: 启动在线下载流程,游戏列表: {games_to_download}") + + # 确保下载管理器已初始化 + if hasattr(self.main_window, 'download_manager'): + # 使用直接下载方法,绕过补丁判断 + self.main_window.download_manager.direct_download_action(games_to_download) + else: + if debug_mode: + logger.error("DEBUG: 下载管理器未初始化,无法启动下载流程") + # 显示错误消息 + msgbox_frame( + f"错误 - {self.app_name}", + "\n下载管理器未初始化,无法启动下载流程。\n", + QMessageBox.StandardButton.Ok + ).exec() + def process_next_offline_install_task(self, install_tasks): """处理下一个离线安装任务 @@ -437,32 +669,14 @@ class OfflineModeManager: if debug_mode: logger.debug(f"DEBUG: 有未找到离线补丁文件的游戏: {self.missing_offline_patches}") - # 在安装完成后询问用户是否切换到在线模式 - msg_box = msgbox_frame( - f"离线安装完成 - {self.app_name}", - f"\n以下游戏未找到对应的离线补丁文件:\n\n{chr(10).join(self.missing_offline_patches)}\n\n是否切换到在线模式继续安装?\n", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No - ) - result = msg_box.exec() - - if result == QMessageBox.StandardButton.Yes: - if debug_mode: - logger.debug("DEBUG: 用户选择切换到在线模式") - - # 切换到在线模式 - if hasattr(self.main_window, 'ui_manager'): - self.main_window.ui_manager.switch_work_mode("online") - - # 重置UI状态 - self.main_window.setEnabled(True) - self.main_window.ui.start_install_text.setText("开始安装") + # 先显示已安装的结果 + if self.installed_games: + installed_msg = f"已成功安装以下补丁:\n\n{chr(10).join(self.installed_games)}\n\n" else: - if debug_mode: - logger.debug("DEBUG: 用户选择不切换到在线模式") - - # 恢复UI状态 - self.main_window.setEnabled(True) - self.main_window.ui.start_install_text.setText("开始安装") + installed_msg = "" + + # 使用QTimer延迟显示询问对话框,确保安装结果窗口先显示并关闭 + QTimer.singleShot(500, lambda: self._show_missing_patches_dialog(installed_msg)) else: # 恢复UI状态 self.main_window.setEnabled(True) @@ -489,70 +703,53 @@ class OfflineModeManager: logger.debug(f"DEBUG: 已复制补丁文件到缓存目录: {_7z_path}") logger.debug(f"DEBUG: 开始验证补丁文件哈希值") + # 验证补丁文件哈希 + hash_valid = False + extracted_path = None + # 显示哈希验证窗口 - 使用离线特定消息 self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="offline_verify", is_offline=True) # 验证补丁文件哈希 - hash_valid = self.verify_patch_hash(game_version, _7z_path) + # 使用特殊版本的verify_patch_hash方法,它会返回哈希验证结果和解压后的文件路径 + from utils.helpers import ProgressHashVerifyDialog + from data.config import PLUGIN_HASH + from workers.hash_thread import OfflineHashVerifyThread - # 关闭哈希验证窗口 - if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible(): - self.main_window.hash_msg_box.close() - self.main_window.hash_msg_box = None + # 创建并显示进度对话框 + progress_dialog = ProgressHashVerifyDialog( + f"验证补丁文件 - {self.app_name}", + f"正在验证 {game_version} 的补丁文件完整性...", + self.main_window + ) - if hash_valid: - if debug_mode: - logger.info(f"DEBUG: 补丁文件哈希验证成功,开始解压") - - # 显示解压窗口 - 使用离线特定消息 - self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="offline_extraction", is_offline=True) - - try: - # 创建解压线程 - extraction_thread = self.main_window.create_extraction_thread( - _7z_path, game_folder, plugin_path, game_version - ) - - # 正确连接信号 - extraction_thread.finished.connect( - lambda success, error, game_ver: self.on_extraction_thread_finished( - success, error, game_ver, install_tasks - ) - ) - - # 启动解压线程 - extraction_thread.start() - except Exception as e: - if debug_mode: - logger.error(f"DEBUG: 创建或启动解压线程失败: {e}") - - # 关闭解压窗口 - if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible(): - self.main_window.hash_msg_box.close() - self.main_window.hash_msg_box = None - - # 显示错误消息 - msgbox_frame( - f"解压错误 - {self.app_name}", - f"\n{game_version} 的解压过程中发生错误: {str(e)}\n\n跳过此游戏的安装。\n", - QMessageBox.StandardButton.Ok - ).exec() - - # 继续下一个任务 - self.process_next_offline_install_task(install_tasks) - else: - if debug_mode: - logger.warning(f"DEBUG: 补丁文件哈希验证失败") - - # 显示错误消息 - msgbox_frame( - f"哈希验证失败 - {self.app_name}", - f"\n{game_version} 的补丁文件哈希验证失败,可能已损坏或被篡改。\n\n跳过此游戏的安装。\n", - QMessageBox.StandardButton.Ok - ).exec() - - # 继续下一个任务 + # 创建哈希验证线程 + hash_thread = OfflineHashVerifyThread(game_version, _7z_path, PLUGIN_HASH, self.main_window) + + # 存储解压后的文件路径 + extracted_file_path = "" + + # 连接信号 + hash_thread.progress.connect(progress_dialog.update_progress) + hash_thread.finished.connect( + lambda result, error, path: self._on_offline_install_hash_finished( + result, error, path, progress_dialog, game_version, _7z_path, game_folder, plugin_path, install_tasks + ) + ) + + # 启动线程 + hash_thread.start() + + # 显示对话框,阻塞直到对话框关闭 + progress_dialog.exec() + + # 如果用户取消了验证,停止线程并继续下一个任务 + if hash_thread.isRunning(): + hash_thread.terminate() + hash_thread.wait() self.process_next_offline_install_task(install_tasks) + return + except Exception as e: if debug_mode: logger.error(f"DEBUG: 离线安装任务处理失败: {e}") @@ -566,48 +763,6 @@ class OfflineModeManager: # 继续下一个任务 self.process_next_offline_install_task(install_tasks) - - def on_extraction_thread_finished(self, success, error_message, game_version, remaining_tasks): - """解压线程完成后的处理 - - Args: - success: 是否解压成功 - error_message: 错误信息 - game_version: 游戏版本 - remaining_tasks: 剩余的安装任务列表 - """ - debug_mode = self._is_debug_mode() - - # 关闭解压窗口 - if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible(): - self.main_window.hash_msg_box.close() - self.main_window.hash_msg_box = None - - if debug_mode: - logger.debug(f"DEBUG: 离线解压完成,状态: {'成功' if success else '失败'}") - if not success: - logger.error(f"DEBUG: 错误信息: {error_message}") - - if not success: - # 显示错误消息 - msgbox_frame( - f"解压失败 - {self.app_name}", - f"\n{game_version} 的补丁解压失败。\n\n错误信息: {error_message}\n\n跳过此游戏的安装。\n", - QMessageBox.StandardButton.Ok - ).exec() - - # 更新安装状态 - self.main_window.installed_status[game_version] = False - else: - # 更新安装状态 - self.main_window.installed_status[game_version] = True - - # 添加到已安装游戏列表 - if game_version not in self.installed_games: - self.installed_games.append(game_version) - - # 处理下一个任务 - self.process_next_offline_install_task(remaining_tasks) def is_offline_mode_available(self): """检查是否可以使用离线模式 @@ -628,4 +783,42 @@ class OfflineModeManager: Returns: bool: 是否处于离线模式 """ - return self.is_offline_mode \ No newline at end of file + return self.is_offline_mode + + def _show_missing_patches_dialog(self, installed_msg): + """显示缺少离线补丁文件的对话框 + + Args: + installed_msg: 已安装的补丁信息 + """ + debug_mode = self._is_debug_mode() + + # 在安装完成后询问用户是否切换到在线模式 + msg_box = msgbox_frame( + f"离线安装完成 - {self.app_name}", + f"\n{installed_msg}以下游戏未找到对应的离线补丁文件:\n\n{chr(10).join(self.missing_offline_patches)}\n\n是否切换到在线模式继续安装?\n", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + result = msg_box.exec() + + if result == QMessageBox.StandardButton.Yes: + if debug_mode: + logger.debug("DEBUG: 用户选择切换到在线模式") + + # 切换到在线模式 + if hasattr(self.main_window, 'ui_manager'): + self.main_window.ui_manager.switch_work_mode("online") + + # 直接启动下载流程 + self.main_window.setEnabled(True) + # 保存当前选择的游戏列表,以便在线模式使用 + missing_games = self.missing_offline_patches.copy() + # 启动下载流程 + QTimer.singleShot(500, lambda: self._start_online_download(missing_games)) + else: + if debug_mode: + logger.debug("DEBUG: 用户选择不切换到在线模式") + + # 恢复UI状态 + self.main_window.setEnabled(True) + self.main_window.ui.start_install_text.setText("开始安装") \ No newline at end of file diff --git a/source/main_window.py b/source/main_window.py index 4350fd0..3cae4a9 100644 --- a/source/main_window.py +++ b/source/main_window.py @@ -367,7 +367,7 @@ class MainWindow(QMainWindow): return progress_window - def create_extraction_thread(self, _7z_path, game_folder, plugin_path, game_version): + def create_extraction_thread(self, _7z_path, game_folder, plugin_path, game_version, extracted_path=None): """创建解压线程 Args: @@ -375,11 +375,12 @@ class MainWindow(QMainWindow): game_folder: 游戏文件夹路径 plugin_path: 插件路径 game_version: 游戏版本 + extracted_path: 已解压的补丁文件路径,如果提供则直接使用它而不进行解压 Returns: ExtractionThread: 解压线程实例 """ - return ExtractionThread(_7z_path, game_folder, plugin_path, game_version, self) + return ExtractionThread(_7z_path, game_folder, plugin_path, game_version, self, extracted_path) def show_result(self): """显示安装结果,调用patch_manager的show_result方法""" diff --git a/source/workers/extraction_thread.py b/source/workers/extraction_thread.py index 42d5f58..0f60780 100644 --- a/source/workers/extraction_thread.py +++ b/source/workers/extraction_thread.py @@ -7,25 +7,48 @@ from data.config import PLUGIN, GAME_INFO class ExtractionThread(QThread): finished = Signal(bool, str, str) # success, error_message, game_version - def __init__(self, _7z_path, game_folder, plugin_path, game_version, parent=None): + def __init__(self, _7z_path, game_folder, plugin_path, game_version, parent=None, extracted_path=None): super().__init__(parent) self._7z_path = _7z_path self.game_folder = game_folder self.plugin_path = plugin_path self.game_version = game_version + self.extracted_path = extracted_path # 添加已解压文件路径参数 def run(self): try: - with py7zr.SevenZipFile(self._7z_path, mode="r") as archive: - archive.extractall(path=PLUGIN) - - os.makedirs(self.game_folder, exist_ok=True) - shutil.copy(self.plugin_path, self.game_folder) - - if self.game_version == "NEKOPARA After": - sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"]) - shutil.copy(sig_path, self.game_folder) + # 如果提供了已解压文件路径,直接使用它 + if self.extracted_path and os.path.exists(self.extracted_path): + # 直接复制已解压的文件到游戏目录 + os.makedirs(self.game_folder, exist_ok=True) + shutil.copy(self.extracted_path, self.game_folder) + # 对于NEKOPARA After,还需要复制签名文件 + if self.game_version == "NEKOPARA After": + # 从已解压文件的目录中获取签名文件 + extracted_dir = os.path.dirname(self.extracted_path) + sig_filename = os.path.basename(GAME_INFO[self.game_version]["sig_path"]) + sig_path = os.path.join(extracted_dir, sig_filename) + + # 如果签名文件存在,则复制它 + if os.path.exists(sig_path): + shutil.copy(sig_path, self.game_folder) + else: + # 如果签名文件不存在,则使用原始路径 + sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"]) + shutil.copy(sig_path, self.game_folder) + else: + # 如果没有提供已解压文件路径,执行正常的解压流程 + with py7zr.SevenZipFile(self._7z_path, mode="r") as archive: + archive.extractall(path=PLUGIN) + + os.makedirs(self.game_folder, exist_ok=True) + shutil.copy(self.plugin_path, self.game_folder) + + if self.game_version == "NEKOPARA After": + sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"]) + shutil.copy(sig_path, self.game_folder) + self.finished.emit(True, "", self.game_version) except (py7zr.Bad7zFile, FileNotFoundError, Exception) as e: self.finished.emit(False, f"\n文件操作失败,请重试\n\n【错误信息】:{e}\n", self.game_version) \ No newline at end of file diff --git a/source/workers/hash_thread.py b/source/workers/hash_thread.py index e9ea7f4..655d4f9 100644 --- a/source/workers/hash_thread.py +++ b/source/workers/hash_thread.py @@ -132,23 +132,27 @@ class OfflineHashVerifyThread(QThread): """离线模式下验证补丁文件哈希的线程,支持进度更新""" progress = Signal(int) # 进度信号,0-100 - finished = Signal(bool, str) # 完成信号,(成功/失败, 错误信息) + finished = Signal(bool, str, str) # 完成信号,(成功/失败, 错误信息, 解压后的补丁文件路径) def __init__(self, game_version, file_path, plugin_hash, main_window=None): - """初始化离线哈希验证线程 - - Args: - game_version: 游戏版本名称 - file_path: 补丁压缩包文件路径 - plugin_hash: 插件哈希值字典 - main_window: 主窗口实例,用于访问UI和状态 - """ super().__init__() self.game_version = game_version self.file_path = file_path self.plugin_hash = plugin_hash self.main_window = main_window + self.extracted_patch_path = None # 添加解压后的补丁文件路径 + # 获取预期的哈希值 + self.expected_hash = None + + # 直接使用完整游戏名称作为键 + self.expected_hash = self.plugin_hash.get(game_version, "") + + # 设置调试模式标志 + self.debug_mode = False + if main_window and hasattr(main_window, 'debug_manager'): + self.debug_mode = main_window.debug_manager._is_debug_mode() + def run(self): """运行线程""" debug_mode = False @@ -162,7 +166,7 @@ class OfflineHashVerifyThread(QThread): if not expected_hash: logger.warning(f"DEBUG: 未找到 {self.game_version} 的预期哈希值") - self.finished.emit(False, f"未找到 {self.game_version} 的预期哈希值") + self.finished.emit(False, f"未找到 {self.game_version} 的预期哈希值", "") return if debug_mode: @@ -175,7 +179,7 @@ class OfflineHashVerifyThread(QThread): if not os.path.exists(self.file_path): if debug_mode: logger.warning(f"DEBUG: 补丁文件不存在: {self.file_path}") - self.finished.emit(False, f"补丁文件不存在: {self.file_path}") + self.finished.emit(False, f"补丁文件不存在: {self.file_path}", "") return # 检查文件大小 @@ -186,7 +190,7 @@ class OfflineHashVerifyThread(QThread): if file_size == 0: if debug_mode: logger.warning(f"DEBUG: 补丁文件大小为0,无效文件") - self.finished.emit(False, "补丁文件大小为0,无效文件") + self.finished.emit(False, "补丁文件大小为0,无效文件", "") return # 创建临时目录用于解压文件 @@ -224,7 +228,7 @@ class OfflineHashVerifyThread(QThread): logger.error(f"DEBUG: 解压补丁文件失败: {e}") logger.error(f"DEBUG: 错误类型: {type(e).__name__}") logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}") - self.finished.emit(False, f"解压补丁文件失败: {str(e)}") + self.finished.emit(False, f"解压补丁文件失败: {str(e)}", "") return # 发送进度信号 - 50% @@ -261,7 +265,7 @@ class OfflineHashVerifyThread(QThread): logger.debug(f"DEBUG: 目录: {root}") logger.debug(f"DEBUG: 子目录: {dirs}") logger.debug(f"DEBUG: 文件: {files}") - self.finished.emit(False, f"未找到解压后的补丁文件") + self.finished.emit(False, f"未找到解压后的补丁文件", "") return # 发送进度信号 - 70% @@ -299,15 +303,15 @@ class OfflineHashVerifyThread(QThread): logger.debug(f"DEBUG: 预期哈希值: {expected_hash}") logger.debug(f"DEBUG: 实际哈希值: {file_hash}") - self.finished.emit(result, "" if result else "补丁文件哈希验证失败,文件可能已损坏或被篡改") + self.finished.emit(result, "" if result else "补丁文件哈希验证失败,文件可能已损坏或被篡改", patch_file) except Exception as e: if debug_mode: logger.error(f"DEBUG: 计算补丁文件哈希值失败: {e}") logger.error(f"DEBUG: 错误类型: {type(e).__name__}") - self.finished.emit(False, f"计算补丁文件哈希值失败: {str(e)}") + self.finished.emit(False, f"计算补丁文件哈希值失败: {str(e)}", "") except Exception as e: if debug_mode: logger.error(f"DEBUG: 验证补丁哈希值失败: {e}") logger.error(f"DEBUG: 错误类型: {type(e).__name__}") logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}") - self.finished.emit(False, f"验证补丁哈希值失败: {str(e)}") \ No newline at end of file + self.finished.emit(False, f"验证补丁哈希值失败: {str(e)}", "") \ No newline at end of file