feat(core): 优化解压和哈希验证流程

- 在解压线程中添加已解压文件路径参数,支持直接使用已解压的补丁文件,提升解压效率。
- 更新下载管理器,简化下载成功后的处理逻辑,直接进入解压阶段,去除冗余的哈希验证步骤。
- 在离线模式管理器中增强哈希验证功能,确保在解压后进行哈希校验,提升补丁文件的完整性检查。
- 增强日志记录,确保在关键操作中提供详细的调试信息,便于后续排查和用户反馈。
This commit is contained in:
hyb-oyqq
2025-08-08 11:27:11 +08:00
parent ee72f76952
commit 09d6883432
6 changed files with 657 additions and 249 deletions

View File

@@ -763,91 +763,21 @@ class DownloadManager:
self.on_download_stopped() self.on_download_stopped()
return return
# 下载成功后,使用与离线模式相同的哈希校验机制 # 下载成功后,直接进入解压阶段
debug_mode = self.is_debug_mode() 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 hasattr(self.main_window, 'progress_window') and self.main_window.progress_window:
if self.main_window.progress_window.isVisible(): if self.main_window.progress_window.isVisible():
self.main_window.progress_window.accept() self.main_window.progress_window.accept()
self.main_window.progress_window = None self.main_window.progress_window = None
# 使用与离线模式相同的哈希校验机制 if debug_mode:
from utils.helpers import ProgressHashVerifyDialog logger.debug(f"DEBUG: 下载完成,直接进入解压阶段")
from data.config import PLUGIN_HASH
from workers.hash_thread import OfflineHashVerifyThread
# 创建并显示进度对话框 # 直接进入解压阶段
progress_dialog = ProgressHashVerifyDialog( self.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version)
f"验证补丁文件 - {APP_NAME}", self.main_window.extraction_handler.extraction_finished.connect(self.on_extraction_finished)
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)
# 验证成功,进入解压阶段
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())
def on_extraction_finished(self, continue_download): def on_extraction_finished(self, continue_download):
"""解压完成后的回调,决定是否继续下载队列 """解压完成后的回调,决定是否继续下载队列
@@ -896,3 +826,124 @@ class DownloadManager:
def show_download_thread_settings(self): def show_download_thread_settings(self):
"""显示下载线程设置对话框""" """显示下载线程设置对话框"""
return self.download_task_manager.show_download_thread_settings() 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")

View File

@@ -1,7 +1,13 @@
import os import os
import shutil
from PySide6 import QtWidgets from PySide6 import QtWidgets
from PySide6.QtWidgets import QMessageBox from PySide6.QtWidgets import QMessageBox
from PySide6.QtCore import QTimer
from utils.logger import setup_logger
# 初始化logger
logger = setup_logger("extraction_handler")
class ExtractionHandler: class ExtractionHandler:
"""解压处理器,负责管理解压任务和结果处理""" """解压处理器,负责管理解压任务和结果处理"""
@@ -15,7 +21,7 @@ class ExtractionHandler:
self.main_window = main_window self.main_window = main_window
self.APP_NAME = main_window.APP_NAME if hasattr(main_window, 'APP_NAME') else "" 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: Args:
@@ -23,6 +29,7 @@ class ExtractionHandler:
game_folder: 游戏文件夹路径 game_folder: 游戏文件夹路径
plugin_path: 插件路径 plugin_path: 插件路径
game_version: 游戏版本名称 game_version: 游戏版本名称
extracted_path: 已解压的补丁文件路径,如果提供则直接使用它而不进行解压
""" """
# 检查是否处于离线模式 # 检查是否处于离线模式
is_offline = False is_offline = False
@@ -37,13 +44,13 @@ class ExtractionHandler:
# 创建并启动解压线程 # 创建并启动解压线程
self.main_window.extraction_thread = self.main_window.create_extraction_thread( 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() 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: Args:
success: 是否解压成功 success: 是否解压成功
@@ -55,7 +62,7 @@ class ExtractionHandler:
self.main_window.hash_msg_box.close() self.main_window.hash_msg_box.close()
self.main_window.hash_msg_box = None self.main_window.hash_msg_box = None
# 处理解压结果 # 如果解压失败,显示错误并询问是否继续
if not success: if not success:
# 临时启用窗口以显示错误消息 # 临时启用窗口以显示错误消息
self.main_window.setEnabled(True) self.main_window.setEnabled(True)
@@ -82,8 +89,137 @@ class ExtractionHandler:
self.main_window.ui.start_install_text.setText("开始安装") self.main_window.ui.start_install_text.setText("开始安装")
# 通知DownloadManager停止下载队列 # 通知DownloadManager停止下载队列
self.main_window.download_manager.on_extraction_finished(False) 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: else:
# 更新安装状态 # 校验通过,更新安装状态
self.main_window.installed_status[game_version] = True self.main_window.installed_status[game_version] = True
# 通知DownloadManager继续下一个下载任务 # 通知DownloadManager继续下一个下载任务
self.main_window.download_manager.on_extraction_finished(True) 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)

View File

@@ -4,8 +4,9 @@ import shutil
import tempfile import tempfile
import py7zr import py7zr
import traceback import traceback
from PySide6.QtWidgets import QMessageBox from PySide6 import QtWidgets, QtCore
from PySide6.QtCore import QTimer from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QMessageBox
from data.config import PLUGIN, PLUGIN_HASH, GAME_INFO from data.config import PLUGIN, PLUGIN_HASH, GAME_INFO
from utils import msgbox_frame from utils import msgbox_frame
@@ -254,7 +255,7 @@ class OfflineModeManager:
# 连接信号 # 连接信号
hash_thread.progress.connect(progress_dialog.update_progress) 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() hash_thread.start()
@@ -273,12 +274,13 @@ class OfflineModeManager:
# 返回对话框中存储的验证结果 # 返回对话框中存储的验证结果
return hasattr(progress_dialog, 'hash_result') and progress_dialog.hash_result 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: Args:
result: 验证结果 result: 验证结果
error: 错误信息 error: 错误信息
extracted_path: 解压后的补丁文件路径,如果哈希验证成功则包含此路径
dialog: 进度对话框 dialog: 进度对话框
""" """
debug_mode = self._is_debug_mode() debug_mode = self._is_debug_mode()
@@ -289,6 +291,8 @@ class OfflineModeManager:
if result: if result:
if debug_mode: if debug_mode:
logger.debug(f"DEBUG: 哈希验证成功") logger.debug(f"DEBUG: 哈希验证成功")
if extracted_path:
logger.debug(f"DEBUG: 解压后的补丁文件路径: {extracted_path}")
dialog.set_status("验证成功") dialog.set_status("验证成功")
# 短暂延时后关闭对话框 # 短暂延时后关闭对话框
QTimer.singleShot(500, dialog.accept) QTimer.singleShot(500, dialog.accept)
@@ -301,6 +305,173 @@ class OfflineModeManager:
dialog.cancel_button.setText("关闭") 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): def install_offline_patches(self, selected_games):
"""直接安装离线补丁,完全绕过下载模块 """直接安装离线补丁,完全绕过下载模块
@@ -406,16 +577,77 @@ class OfflineModeManager:
else: else:
if debug_mode: if debug_mode:
logger.warning("DEBUG: 没有可安装的游戏,安装流程结束") logger.warning("DEBUG: 没有可安装的游戏,安装流程结束")
msgbox_frame(
f"离线安装信息 - {self.app_name}", # 检查是否有未找到离线补丁文件的游戏
"\n没有可安装的游戏或未找到对应的离线补丁文件。\n", if self.missing_offline_patches:
QMessageBox.StandardButton.Ok if debug_mode:
).exec() logger.debug(f"DEBUG: 有未找到离线补丁文件的游戏: {self.missing_offline_patches}")
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装") # 询问用户是否切换到在线模式
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 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): def process_next_offline_install_task(self, install_tasks):
"""处理下一个离线安装任务 """处理下一个离线安装任务
@@ -437,32 +669,14 @@ class OfflineModeManager:
if debug_mode: if debug_mode:
logger.debug(f"DEBUG: 有未找到离线补丁文件的游戏: {self.missing_offline_patches}") logger.debug(f"DEBUG: 有未找到离线补丁文件的游戏: {self.missing_offline_patches}")
# 在安装完成后询问用户是否切换到在线模式 # 先显示已安装的结果
msg_box = msgbox_frame( if self.installed_games:
f"离线安装完成 - {self.app_name}", installed_msg = f"已成功安装以下补丁:\n\n{chr(10).join(self.installed_games)}\n\n"
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("开始安装")
else: else:
if debug_mode: installed_msg = ""
logger.debug("DEBUG: 用户选择不切换到在线模式")
# 恢复UI状态 # 使用QTimer延迟显示询问对话框确保安装结果窗口先显示并关闭
self.main_window.setEnabled(True) QTimer.singleShot(500, lambda: self._show_missing_patches_dialog(installed_msg))
self.main_window.ui.start_install_text.setText("开始安装")
else: else:
# 恢复UI状态 # 恢复UI状态
self.main_window.setEnabled(True) self.main_window.setEnabled(True)
@@ -489,70 +703,53 @@ class OfflineModeManager:
logger.debug(f"DEBUG: 已复制补丁文件到缓存目录: {_7z_path}") logger.debug(f"DEBUG: 已复制补丁文件到缓存目录: {_7z_path}")
logger.debug(f"DEBUG: 开始验证补丁文件哈希值") 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) 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(): progress_dialog = ProgressHashVerifyDialog(
self.main_window.hash_msg_box.close() f"验证补丁文件 - {self.app_name}",
self.main_window.hash_msg_box = None f"正在验证 {game_version} 的补丁文件完整性...",
self.main_window
)
if hash_valid: # 创建哈希验证线程
if debug_mode: hash_thread = OfflineHashVerifyThread(game_version, _7z_path, PLUGIN_HASH, self.main_window)
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) extracted_file_path = ""
try: # 连接信号
# 创建解压线程 hash_thread.progress.connect(progress_dialog.update_progress)
extraction_thread = self.main_window.create_extraction_thread( hash_thread.finished.connect(
_7z_path, game_folder, plugin_path, game_version 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
)
)
# 正确连接信号 # 启动线程
extraction_thread.finished.connect( hash_thread.start()
lambda success, error, game_ver: self.on_extraction_thread_finished(
success, error, game_ver, install_tasks
)
)
# 启动解压线程 # 显示对话框,阻塞直到对话框关闭
extraction_thread.start() progress_dialog.exec()
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(): if hash_thread.isRunning():
self.main_window.hash_msg_box.close() hash_thread.terminate()
self.main_window.hash_msg_box = None hash_thread.wait()
# 显示错误消息
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()
# 继续下一个任务
self.process_next_offline_install_task(install_tasks) self.process_next_offline_install_task(install_tasks)
return
except Exception as e: except Exception as e:
if debug_mode: if debug_mode:
logger.error(f"DEBUG: 离线安装任务处理失败: {e}") logger.error(f"DEBUG: 离线安装任务处理失败: {e}")
@@ -567,48 +764,6 @@ class OfflineModeManager:
# 继续下一个任务 # 继续下一个任务
self.process_next_offline_install_task(install_tasks) 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): def is_offline_mode_available(self):
"""检查是否可以使用离线模式 """检查是否可以使用离线模式
@@ -629,3 +784,41 @@ class OfflineModeManager:
bool: 是否处于离线模式 bool: 是否处于离线模式
""" """
return self.is_offline_mode 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("开始安装")

View File

@@ -367,7 +367,7 @@ class MainWindow(QMainWindow):
return progress_window 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: Args:
@@ -375,11 +375,12 @@ class MainWindow(QMainWindow):
game_folder: 游戏文件夹路径 game_folder: 游戏文件夹路径
plugin_path: 插件路径 plugin_path: 插件路径
game_version: 游戏版本 game_version: 游戏版本
extracted_path: 已解压的补丁文件路径,如果提供则直接使用它而不进行解压
Returns: Returns:
ExtractionThread: 解压线程实例 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): def show_result(self):
"""显示安装结果调用patch_manager的show_result方法""" """显示安装结果调用patch_manager的show_result方法"""

View File

@@ -7,24 +7,47 @@ from data.config import PLUGIN, GAME_INFO
class ExtractionThread(QThread): class ExtractionThread(QThread):
finished = Signal(bool, str, str) # success, error_message, game_version 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) super().__init__(parent)
self._7z_path = _7z_path self._7z_path = _7z_path
self.game_folder = game_folder self.game_folder = game_folder
self.plugin_path = plugin_path self.plugin_path = plugin_path
self.game_version = game_version self.game_version = game_version
self.extracted_path = extracted_path # 添加已解压文件路径参数
def run(self): def run(self):
try: try:
with py7zr.SevenZipFile(self._7z_path, mode="r") as archive: # 如果提供了已解压文件路径,直接使用它
archive.extractall(path=PLUGIN) 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)
os.makedirs(self.game_folder, exist_ok=True) # 对于NEKOPARA After还需要复制签名文件
shutil.copy(self.plugin_path, self.game_folder) 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 self.game_version == "NEKOPARA After": # 如果签名文件存在,则复制它
sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"]) if os.path.exists(sig_path):
shutil.copy(sig_path, self.game_folder) 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) self.finished.emit(True, "", self.game_version)
except (py7zr.Bad7zFile, FileNotFoundError, Exception) as e: except (py7zr.Bad7zFile, FileNotFoundError, Exception) as e:

View File

@@ -132,22 +132,26 @@ class OfflineHashVerifyThread(QThread):
"""离线模式下验证补丁文件哈希的线程,支持进度更新""" """离线模式下验证补丁文件哈希的线程,支持进度更新"""
progress = Signal(int) # 进度信号0-100 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): def __init__(self, game_version, file_path, plugin_hash, main_window=None):
"""初始化离线哈希验证线程
Args:
game_version: 游戏版本名称
file_path: 补丁压缩包文件路径
plugin_hash: 插件哈希值字典
main_window: 主窗口实例用于访问UI和状态
"""
super().__init__() super().__init__()
self.game_version = game_version self.game_version = game_version
self.file_path = file_path self.file_path = file_path
self.plugin_hash = plugin_hash self.plugin_hash = plugin_hash
self.main_window = main_window 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): def run(self):
"""运行线程""" """运行线程"""
@@ -162,7 +166,7 @@ class OfflineHashVerifyThread(QThread):
if not expected_hash: if not expected_hash:
logger.warning(f"DEBUG: 未找到 {self.game_version} 的预期哈希值") logger.warning(f"DEBUG: 未找到 {self.game_version} 的预期哈希值")
self.finished.emit(False, f"未找到 {self.game_version} 的预期哈希值") self.finished.emit(False, f"未找到 {self.game_version} 的预期哈希值", "")
return return
if debug_mode: if debug_mode:
@@ -175,7 +179,7 @@ class OfflineHashVerifyThread(QThread):
if not os.path.exists(self.file_path): if not os.path.exists(self.file_path):
if debug_mode: if debug_mode:
logger.warning(f"DEBUG: 补丁文件不存在: {self.file_path}") logger.warning(f"DEBUG: 补丁文件不存在: {self.file_path}")
self.finished.emit(False, f"补丁文件不存在: {self.file_path}") self.finished.emit(False, f"补丁文件不存在: {self.file_path}", "")
return return
# 检查文件大小 # 检查文件大小
@@ -186,7 +190,7 @@ class OfflineHashVerifyThread(QThread):
if file_size == 0: if file_size == 0:
if debug_mode: if debug_mode:
logger.warning(f"DEBUG: 补丁文件大小为0无效文件") logger.warning(f"DEBUG: 补丁文件大小为0无效文件")
self.finished.emit(False, "补丁文件大小为0无效文件") self.finished.emit(False, "补丁文件大小为0无效文件", "")
return return
# 创建临时目录用于解压文件 # 创建临时目录用于解压文件
@@ -224,7 +228,7 @@ class OfflineHashVerifyThread(QThread):
logger.error(f"DEBUG: 解压补丁文件失败: {e}") logger.error(f"DEBUG: 解压补丁文件失败: {e}")
logger.error(f"DEBUG: 错误类型: {type(e).__name__}") logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}") logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
self.finished.emit(False, f"解压补丁文件失败: {str(e)}") self.finished.emit(False, f"解压补丁文件失败: {str(e)}", "")
return return
# 发送进度信号 - 50% # 发送进度信号 - 50%
@@ -261,7 +265,7 @@ class OfflineHashVerifyThread(QThread):
logger.debug(f"DEBUG: 目录: {root}") logger.debug(f"DEBUG: 目录: {root}")
logger.debug(f"DEBUG: 子目录: {dirs}") logger.debug(f"DEBUG: 子目录: {dirs}")
logger.debug(f"DEBUG: 文件: {files}") logger.debug(f"DEBUG: 文件: {files}")
self.finished.emit(False, f"未找到解压后的补丁文件") self.finished.emit(False, f"未找到解压后的补丁文件", "")
return return
# 发送进度信号 - 70% # 发送进度信号 - 70%
@@ -299,15 +303,15 @@ class OfflineHashVerifyThread(QThread):
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}") logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
logger.debug(f"DEBUG: 实际哈希值: {file_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: except Exception as e:
if debug_mode: if debug_mode:
logger.error(f"DEBUG: 计算补丁文件哈希值失败: {e}") logger.error(f"DEBUG: 计算补丁文件哈希值失败: {e}")
logger.error(f"DEBUG: 错误类型: {type(e).__name__}") 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: except Exception as e:
if debug_mode: if debug_mode:
logger.error(f"DEBUG: 验证补丁哈希值失败: {e}") logger.error(f"DEBUG: 验证补丁哈希值失败: {e}")
logger.error(f"DEBUG: 错误类型: {type(e).__name__}") logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}") logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
self.finished.emit(False, f"验证补丁哈希值失败: {str(e)}") self.finished.emit(False, f"验证补丁哈希值失败: {str(e)}", "")