Files
FRAISEMOE-Addons-Installer-…/source/core/managers/offline_mode_manager.py
hyb-oyqq d07ef20e51 feat(core): 更新窗口状态管理,统一使用window_manager
- 在多个模块中,将安装按钮状态管理从ui_manager迁移至window_manager,确保窗口状态的一致性和可维护性。
- 优化了ExtractionHandler、CloudflareOptimizer、DownloadManager、OfflineModeManager、PatchDetector等类中的状态更新逻辑,提升了代码的可读性和一致性。
2025-08-13 11:58:43 +08:00

1046 lines
44 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import hashlib
import shutil
import tempfile
import py7zr
import traceback
from PySide6 import QtWidgets, QtCore
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QMessageBox
from config.config import PLUGIN, PLUGIN_HASH, GAME_INFO
from utils import msgbox_frame
from utils.logger import setup_logger
# 初始化logger
logger = setup_logger("offline_mode_manager")
class OfflineModeManager:
"""离线模式管理器,用于管理离线模式下的补丁安装和检测"""
def __init__(self, main_window):
"""初始化离线模式管理器
Args:
main_window: 主窗口实例用于访问UI和状态
"""
self.main_window = main_window
self.app_name = main_window.APP_NAME if hasattr(main_window, 'APP_NAME') else ""
self.offline_patches = {} # 存储离线补丁信息 {补丁名称: 文件路径}
self.is_offline_mode = False
self.installed_games = [] # 跟踪本次实际安装的游戏
# 保持对哈希线程的引用,避免运行中被销毁
self.hash_thread = None
# 解压线程与进度窗口引用避免运行中被销毁且确保UI可更新
self.extraction_thread = None
self.extraction_progress_window = None
def _is_debug_mode(self):
"""检查是否处于调试模式
Returns:
bool: 是否处于调试模式
"""
try:
if hasattr(self.main_window, 'debug_manager') and self.main_window.debug_manager:
if hasattr(self.main_window.debug_manager, '_is_debug_mode'):
# 尝试直接从debug_manager获取状态
return self.main_window.debug_manager._is_debug_mode()
elif hasattr(self.main_window, 'config'):
# 如果debug_manager还没准备好尝试从配置中获取
return self.main_window.config.get('debug_mode', False)
# 如果以上都不可行返回False
return False
except Exception:
# 捕获任何异常默认返回False
return False
def scan_for_offline_patches(self, directory=None):
"""扫描指定目录(默认为软件所在目录)查找离线补丁文件
Args:
directory: 要扫描的目录如果为None则使用软件所在目录
Returns:
dict: 找到的补丁文件 {补丁名称: 文件路径}
"""
if directory is None:
# 获取软件所在目录 - 直接使用最简单的方式
try:
import sys
if getattr(sys, 'frozen', False):
# 如果是PyInstaller打包的环境使用可执行文件所在目录
directory = os.path.dirname(sys.executable)
else:
# 直接取当前工作目录
directory = os.getcwd()
# 对于开发环境的特殊处理:
# 如果当前目录路径中包含'source'则可能是在开发模式下从source目录运行
# 尝试找到项目根目录
if 'source' in directory:
# 尝试向上一级查找补丁文件
parent_dir = os.path.dirname(directory)
# 看看父目录是否存在补丁文件
potential_patches = ["vol.1.7z", "vol.2.7z", "vol.3.7z", "vol.4.7z", "after.7z"]
for patch_file in potential_patches:
if os.path.exists(os.path.join(parent_dir, patch_file)):
# 如果在父目录找到了补丁文件,使用父目录作为扫描目录
directory = parent_dir
break
if self._is_debug_mode():
logger.debug(f"DEBUG: 使用目录 {directory} 扫描离线补丁文件")
current_dir = os.getcwd()
logger.debug(f"DEBUG: 当前工作目录: {current_dir}")
logger.debug(f"DEBUG: 是否为打包环境: {getattr(sys, 'frozen', False)}")
if getattr(sys, 'frozen', False):
logger.debug(f"DEBUG: 可执行文件路径: {sys.executable}")
except Exception as e:
# 如果出现异常,使用当前工作目录
directory = os.getcwd()
if self._is_debug_mode():
logger.debug(f"DEBUG: 路径计算出错,使用工作目录: {directory}, 错误: {e}")
debug_mode = self._is_debug_mode()
# 记录扫描操作
logger.debug(f"扫描离线补丁文件,目录: {directory}")
# 要查找的补丁文件名
patch_files = ["vol.1.7z", "vol.2.7z", "vol.3.7z", "vol.4.7z", "after.7z"]
# 只在指定目录中查找,不查找父目录和其他目录
search_dirs = [directory]
if debug_mode:
logger.debug(f"DEBUG: 将在以下目录中查找补丁文件: {search_dirs}")
found_patches = {}
# 扫描目录查找补丁文件
try:
# 搜索指定目录
search_dir = directory
if debug_mode:
logger.debug(f"DEBUG: 正在搜索目录: {search_dir}")
if not os.path.exists(search_dir):
if debug_mode:
logger.debug(f"DEBUG: 目录不存在,跳过: {search_dir}")
else:
for file in os.listdir(search_dir):
if file.lower() in patch_files:
file_path = os.path.join(search_dir, file)
if os.path.isfile(file_path):
patch_name = file.lower()
found_patches[patch_name] = file_path
# 无论是否为调试模式,都记录找到的补丁文件
logger.info(f"找到离线补丁文件: {patch_name} 路径: {file_path}")
if debug_mode:
logger.debug(f"DEBUG: 找到离线补丁文件: {patch_name} 路径: {file_path}")
except Exception as e:
logger.error(f"扫描目录时出错: {str(e)}")
self.offline_patches = found_patches
# 记录扫描结果
if found_patches:
logger.info(f"共找到 {len(found_patches)} 个离线补丁文件: {list(found_patches.keys())}")
else:
logger.debug("未找到任何离线补丁文件")
return found_patches
def has_offline_patches(self):
"""检查是否有可用的离线补丁文件
Returns:
bool: 是否有可用的离线补丁
"""
if not self.offline_patches:
self.scan_for_offline_patches()
return len(self.offline_patches) > 0
def set_offline_mode(self, enabled):
"""设置离线模式状态
Args:
enabled: 是否启用离线模式
Returns:
bool: 是否成功设置离线模式
"""
debug_mode = self._is_debug_mode()
if enabled:
# 检查是否有离线补丁文件
if not self.has_offline_patches() and not debug_mode:
msgbox_frame(
f"离线模式错误 - {self.app_name}",
"\n未找到任何离线补丁文件,无法启用离线模式。\n\n请将补丁文件放置在软件所在目录后再尝试。\n",
QMessageBox.StandardButton.Ok
).exec()
logger.warning("尝试启用离线模式失败:未找到任何离线补丁文件")
return False
if debug_mode:
logger.debug("DEBUG: 已启用离线模式(调试模式下允许强制启用)")
self.is_offline_mode = enabled
# 更新窗口标题
if hasattr(self.main_window, 'setWindowTitle'):
from config.config import APP_NAME, APP_VERSION
mode_indicator = "[离线模式]" if enabled else "[在线模式]"
self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
# 同时更新UI中的标题标签
if hasattr(self.main_window, 'ui') and hasattr(self.main_window.ui, 'title_label'):
self.main_window.ui.title_label.setText(f"{APP_NAME} v{APP_VERSION} {mode_indicator}")
# 同步更新UI菜单中的模式选择状态
if hasattr(self.main_window, 'ui_manager'):
ui_manager = self.main_window.ui_manager
if hasattr(ui_manager, 'online_mode_action') and hasattr(ui_manager, 'offline_mode_action'):
ui_manager.online_mode_action.setChecked(not enabled)
ui_manager.offline_mode_action.setChecked(enabled)
# 记录离线模式状态变化
logger.debug(f"离线模式已{'启用' if enabled else '禁用'}")
if debug_mode:
logger.debug(f"DEBUG: 离线模式已{'启用' if enabled else '禁用'}")
return True
def get_offline_patch_path(self, game_version):
"""根据游戏版本获取对应的离线补丁文件路径
Args:
game_version: 游戏版本名称,如"NEKOPARA Vol.1"
Returns:
str: 离线补丁文件路径如果没有找到则返回None
"""
# 确保已扫描过补丁文件
if not self.offline_patches:
self.scan_for_offline_patches()
# 根据游戏版本获取对应的补丁文件名
patch_file = None
if "Vol.1" in game_version:
patch_file = "vol.1.7z"
elif "Vol.2" in game_version:
patch_file = "vol.2.7z"
elif "Vol.3" in game_version:
patch_file = "vol.3.7z"
elif "Vol.4" in game_version:
patch_file = "vol.4.7z"
elif "After" in game_version:
patch_file = "after.7z"
# 检查是否有对应的补丁文件
if patch_file and patch_file in self.offline_patches:
return self.offline_patches[patch_file]
return None
def prepare_offline_patch(self, game_version, target_path):
"""准备离线补丁文件,复制到缓存目录
Args:
game_version: 游戏版本名称
target_path: 目标路径(通常是缓存目录中的路径)
Returns:
bool: 是否成功准备补丁文件
"""
source_path = self.get_offline_patch_path(game_version)
if not source_path:
return False
debug_mode = self._is_debug_mode()
try:
# 确保目标目录存在
os.makedirs(os.path.dirname(target_path), exist_ok=True)
# 复制文件
shutil.copy2(source_path, target_path)
if debug_mode:
logger.debug(f"DEBUG: 已复制离线补丁文件 {source_path}{target_path}")
return True
except Exception as e:
if debug_mode:
logger.error(f"DEBUG: 复制离线补丁文件失败: {e}")
return False
def verify_patch_hash(self, game_version, file_path):
"""验证补丁文件的哈希值使用patch_detector模块
Args:
game_version: 游戏版本名称
file_path: 补丁压缩包文件路径
Returns:
bool: 哈希值是否匹配
"""
debug_mode = self._is_debug_mode()
if debug_mode:
logger.debug(f"DEBUG: 开始验证补丁文件哈希: {file_path}")
# 创建进度对话框
from utils.helpers import ProgressHashVerifyDialog
from config.config import PLUGIN_HASH
from workers.hash_thread import OfflineHashVerifyThread
# 创建并显示进度对话框
progress_dialog = ProgressHashVerifyDialog(
f"验证补丁文件 - {self.app_name}",
f"正在验证 {game_version} 的补丁文件完整性...",
self.main_window
)
# 创建哈希验证线程
hash_thread = OfflineHashVerifyThread(game_version, file_path, PLUGIN_HASH, self.main_window)
# 连接信号
hash_thread.progress.connect(progress_dialog.update_progress)
hash_thread.finished.connect(lambda result, error, extracted_path: self._on_hash_verify_finished(result, error, extracted_path, progress_dialog))
# 启动线程
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()
return False
# 返回对话框中存储的验证结果
return hasattr(progress_dialog, 'hash_result') and progress_dialog.hash_result
def _on_hash_verify_finished(self, result, error, extracted_path, dialog):
"""哈希验证线程完成后的回调
Args:
result: 验证结果
error: 错误信息
extracted_path: 解压后的补丁文件路径,如果哈希验证成功则包含此路径
dialog: 进度对话框
"""
debug_mode = self._is_debug_mode()
# 存储结果到对话框以便在exec()返回后获取
dialog.hash_result = result
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)
else:
if debug_mode:
logger.debug(f"DEBUG: 哈希验证失败: {error}")
dialog.set_status(f"验证失败: {error}")
dialog.set_message("补丁文件验证失败,可能已损坏或被篡改。")
# 将取消按钮改为关闭按钮
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 config.config import GAME_INFO, PLUGIN
# 存储结果到对话框以便在exec()返回后获取
dialog.hash_result = result
# 关闭哈希验证窗口
self.main_window.close_hash_msg_box()
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: 哈希验证成功,开始安装")
# 显示安装进度窗口
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_filename = None
if "Vol.1" in game_version:
target_filename = "adultsonly.xp3"
elif "Vol.2" in game_version:
target_filename = "adultsonly.xp3"
elif "Vol.3" in game_version:
target_filename = "update00.int"
elif "Vol.4" in game_version:
target_filename = "vol4adult.xp3"
elif "After" in game_version:
target_filename = "afteradult.xp3"
if not target_filename:
raise ValueError(f"未知的游戏版本: {game_version}")
# 直接解压文件到游戏目录
import py7zr
if debug_mode:
logger.debug(f"DEBUG: 直接解压文件 {_7z_path} 到游戏目录 {game_folder}")
# 解压文件
with py7zr.SevenZipFile(_7z_path, mode="r") as archive:
# 获取压缩包内的文件列表
file_list = archive.getnames()
if debug_mode:
logger.debug(f"DEBUG: 压缩包内文件列表: {file_list}")
# 解析压缩包内的文件结构
target_file_in_archive = None
for file_path in file_list:
if target_filename in file_path:
target_file_in_archive = file_path
break
if not target_file_in_archive:
if debug_mode:
logger.warning(f"DEBUG: 在压缩包中未找到目标文件 {target_filename}")
raise FileNotFoundError(f"在压缩包中未找到目标文件 {target_filename}")
# 准备解压特定文件到游戏目录
target_path = os.path.join(game_folder, target_filename)
# 创建一个临时目录用于解压单个文件
with tempfile.TemporaryDirectory() as temp_dir:
# 解压特定文件到临时目录
archive.extract(path=temp_dir, targets=[target_file_in_archive])
# 找到解压后的文件
extracted_file_path = os.path.join(temp_dir, target_file_in_archive)
# 复制到目标位置
shutil.copy2(extracted_file_path, target_path)
if debug_mode:
logger.debug(f"DEBUG: 已解压并复制文件到 {target_path}")
# 对于NEKOPARA After还需要复制签名文件
if game_version == "NEKOPARA After":
sig_filename = f"{target_filename}.sig"
sig_file_in_archive = None
# 查找签名文件
for file_path in file_list:
if sig_filename in file_path:
sig_file_in_archive = file_path
break
if sig_file_in_archive:
# 解压签名文件
archive.extract(path=temp_dir, targets=[sig_file_in_archive])
extracted_sig_path = os.path.join(temp_dir, sig_file_in_archive)
sig_target = os.path.join(game_folder, sig_filename)
shutil.copy2(extracted_sig_path, sig_target)
if debug_mode:
logger.debug(f"DEBUG: 已解压并复制签名文件到 {sig_target}")
else:
if debug_mode:
logger.warning(f"DEBUG: 未找到签名文件 {sig_filename}")
# 进行安装后的哈希校验
self._perform_hash_check(game_version, install_tasks)
except Exception as e:
if debug_mode:
logger.error(f"DEBUG: 安装补丁文件失败: {e}")
import traceback
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
# 关闭安装进度窗口
self.main_window.close_hash_msg_box()
# 显示错误消息
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 _perform_hash_check(self, game_version, install_tasks):
"""安装完成后进行哈希校验
Args:
game_version: 游戏版本
install_tasks: 剩余的安装任务列表
"""
debug_mode = self._is_debug_mode()
# 导入所需模块
from config.config import GAME_INFO, PLUGIN_HASH
from workers.hash_thread import HashThread
# 获取安装路径
install_paths = {}
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
self.main_window.download_manager.selected_folder
)
for game, info in 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
# 添加到已安装游戏列表
if game_version not in self.installed_games:
self.installed_games.append(game_version)
# 关闭安装进度窗口
self.main_window.close_hash_msg_box()
# 继续下一个任务
self.process_next_offline_install_task(install_tasks)
return
# 关闭可能存在的哈希校验窗口,然后创建新窗口
self.main_window.close_hash_msg_box()
# 显示哈希校验窗口
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="post", is_offline=True)
# 直接创建并启动哈希线程进行校验,而不是通过主窗口
hash_thread = HashThread(
"after",
install_paths,
PLUGIN_HASH,
self.main_window.installed_status,
self.main_window
)
hash_thread.after_finished.connect(
lambda result: self._on_hash_check_finished(result, game_version, install_tasks)
)
# 保存引用以便后续使用
self.hash_thread = hash_thread
try:
self.hash_thread.finished.connect(lambda: setattr(self, 'hash_thread', None))
except Exception:
pass
hash_thread.start()
def _on_hash_check_finished(self, result, game_version, install_tasks):
"""哈希校验完成后的处理
Args:
result: 校验结果,包含通过状态、游戏版本和消息
game_version: 游戏版本
install_tasks: 剩余的安装任务列表
"""
debug_mode = self._is_debug_mode()
# 关闭哈希检查窗口
self.main_window.close_hash_msg_box()
if not result["passed"]:
# 记录校验失败信息
logger.error(f"===== {game_version} 哈希校验失败 =====")
logger.error(f"校验失败消息: {result.get('message', '无错误消息')}")
# 校验失败,删除已解压的文件并提示重新安装
error_message = result["message"]
# 获取安装路径
install_path = None
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
self.main_window.download_manager.selected_folder
)
from config.config import GAME_INFO
if game_version in game_dirs and game_version in GAME_INFO:
game_dir = game_dirs[game_version]
install_path = os.path.join(game_dir, os.path.basename(GAME_INFO[game_version]["install_path"]))
logger.info(f"找到安装路径: {install_path}")
# 记录安装路径的文件信息
if os.path.exists(install_path):
file_size = os.path.getsize(install_path)
logger.info(f"文件存在,大小: {file_size} 字节")
# 检查是否为NEKOPARA After记录签名文件信息
if game_version == "NEKOPARA After":
sig_path = f"{install_path}.sig"
if os.path.exists(sig_path):
sig_size = os.path.getsize(sig_path)
logger.info(f"签名文件存在: {sig_path}, 大小: {sig_size} 字节")
else:
logger.info(f"签名文件不存在: {sig_path}")
else:
logger.warning(f"文件不存在: {install_path}")
else:
logger.warning(f"未找到 {game_version} 的安装路径")
# 如果找到安装路径,尝试删除已解压的文件
if install_path and os.path.exists(install_path):
try:
os.remove(install_path)
logger.debug(f"已删除校验失败的文件: {install_path}")
# 检查是否为NEKOPARA After同时删除签名文件
if game_version == "NEKOPARA After":
sig_path = f"{install_path}.sig"
if os.path.exists(sig_path):
os.remove(sig_path)
logger.debug(f"已删除签名文件: {sig_path}")
except Exception as e:
logger.error(f"删除文件失败: {e}")
# 显示错误消息
msgbox_frame(
f"校验失败 - {self.app_name}",
f"{error_message}\n\n跳过此游戏的安装。",
QMessageBox.StandardButton.Ok
).exec()
# 更新安装状态
self.main_window.installed_status[game_version] = False
else:
# 校验通过,更新安装状态
self.main_window.installed_status[game_version] = True
logger.info(f"===== {game_version} 哈希校验通过 =====")
# 添加到已安装游戏列表
if game_version not in self.installed_games:
self.installed_games.append(game_version)
# 显示安装成功消息
if debug_mode:
logger.debug(f"DEBUG: {game_version} 安装成功并通过哈希校验")
# 继续处理下一个任务
self.process_next_offline_install_task(install_tasks)
def _on_extraction_finished_with_hash_check(self, success, error_message, game_version, install_tasks):
"""解压完成后进行哈希校验(后台线程回调)"""
# 关闭解压进度窗口
try:
if self.extraction_progress_window and self.extraction_progress_window.isVisible():
self.extraction_progress_window.close()
except Exception:
pass
self.extraction_progress_window = None
# 清理线程引用
self.extraction_thread = None
if not success:
# 解压失败,提示并继续下一个任务
msgbox_frame(
f"安装错误 - {self.app_name}",
error_message or f"\n{game_version} 的安装过程中发生错误。\n",
QMessageBox.StandardButton.Ok
).exec()
self.process_next_offline_install_task(install_tasks)
return
# 解压成功,进入安装后哈希校验
self._perform_hash_check(game_version, install_tasks)
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):
"""直接安装离线补丁,完全绕过下载模块
Args:
selected_games: 用户选择安装的游戏列表
Returns:
bool: 是否成功启动安装流程
"""
debug_mode = self._is_debug_mode()
if debug_mode:
logger.debug(f"DEBUG: 开始离线安装流程,选择的游戏: {selected_games}")
if not self.is_in_offline_mode():
if debug_mode:
logger.warning("DEBUG: 当前不是离线模式,无法使用离线安装")
return False
# 确保已扫描过补丁文件
if not self.offline_patches:
self.scan_for_offline_patches()
if not self.offline_patches:
if debug_mode:
logger.warning("DEBUG: 未找到任何离线补丁文件")
msgbox_frame(
f"离线安装错误 - {self.app_name}",
"\n未找到任何离线补丁文件,无法进行离线安装。\n\n请将补丁文件放置在软件所在目录后再尝试。\n",
QMessageBox.StandardButton.Ok
).exec()
return False
# 获取游戏目录
game_dirs = self.main_window.game_detector.identify_game_directories_improved(
self.main_window.download_manager.selected_folder
)
if not game_dirs:
if debug_mode:
logger.warning("DEBUG: 未识别到任何游戏目录")
return False
self.main_window.setEnabled(False)
# 重置已安装游戏列表
self.installed_games = []
# 设置到主窗口,供结果显示使用
self.main_window.download_queue_history = selected_games
# 记录未找到离线补丁文件的游戏
self.missing_offline_patches = []
# 创建安装任务列表
install_tasks = []
for game_version in selected_games:
# 获取离线补丁文件路径
patch_file = self.get_offline_patch_path(game_version)
if not patch_file:
if debug_mode:
logger.warning(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过")
# 记录未找到离线补丁文件的游戏
self.missing_offline_patches.append(game_version)
continue
# 获取游戏目录
game_folder = game_dirs.get(game_version)
if not game_folder:
if debug_mode:
logger.warning(f"DEBUG: 未找到 {game_version} 的游戏目录,跳过")
continue
# 获取目标路径
if "Vol.1" in game_version:
_7z_path = os.path.join(PLUGIN, "vol.1.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
elif "Vol.2" in game_version:
_7z_path = os.path.join(PLUGIN, "vol.2.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
elif "Vol.3" in game_version:
_7z_path = os.path.join(PLUGIN, "vol.3.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
elif "Vol.4" in game_version:
_7z_path = os.path.join(PLUGIN, "vol.4.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
elif "After" in game_version:
_7z_path = os.path.join(PLUGIN, "after.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
else:
if debug_mode:
logger.warning(f"DEBUG: {game_version} 不是支持的游戏版本,跳过")
continue
# 添加到安装任务列表
install_tasks.append((patch_file, game_folder, game_version, _7z_path, plugin_path))
# 开始执行第一个安装任务
if install_tasks:
if debug_mode:
logger.debug(f"开始离线安装流程,安装游戏数量: {len(install_tasks)}")
self.process_next_offline_install_task(install_tasks)
return True
else:
if debug_mode:
logger.warning("DEBUG: 没有可安装的游戏,安装流程结束")
# 如果没有找到任何可安装的游戏,显示一般消息
msgbox_frame(
f"离线安装信息 - {self.app_name}",
"\n没有可安装的游戏或未找到对应的离线补丁文件。\n",
QMessageBox.StandardButton.Ok
).exec()
self.main_window.setEnabled(True)
if hasattr(self.main_window, 'window_manager'):
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
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):
"""处理下一个离线安装任务
Args:
install_tasks: 安装任务列表,每个任务是一个元组 (patch_file, game_folder, game_version, _7z_path, plugin_path)
"""
debug_mode = self._is_debug_mode()
if not install_tasks:
# 所有任务完成,进行后检查
if debug_mode:
logger.debug("所有离线安装任务完成,进行后检查")
# 使用patch_detector进行安装后哈希比较
self.main_window.patch_detector.after_hash_compare()
# 检查是否有未找到离线补丁文件的游戏
if hasattr(self, 'missing_offline_patches') and self.missing_offline_patches:
if debug_mode:
logger.debug(f"DEBUG: 有未找到离线补丁文件的游戏: {self.missing_offline_patches}")
# 先显示已安装的结果
if self.installed_games:
installed_msg = f"已成功安装以下补丁:\n\n{chr(10).join(self.installed_games)}\n\n"
else:
installed_msg = ""
# 在安装完成后询问用户是否切换到在线模式
self._show_missing_patches_dialog(installed_msg)
else:
# 恢复UI状态
self.main_window.setEnabled(True)
if hasattr(self.main_window, 'window_manager'):
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)
return
# 获取下一个任务
patch_file, game_folder, game_version, _7z_path, plugin_path = install_tasks.pop(0)
if debug_mode:
logger.debug(f"DEBUG: 处理离线安装任务: {game_version}")
logger.debug(f"DEBUG: 补丁文件: {patch_file}")
logger.debug(f"DEBUG: 游戏目录: {game_folder}")
# 使用后台线程进行解压避免阻塞UI
try:
# 确保游戏目录存在
os.makedirs(game_folder, exist_ok=True)
# 创建非阻塞的解压进度窗口
self.extraction_progress_window = self.main_window.create_extraction_progress_window()
try:
self.extraction_progress_window.show()
QtWidgets.QApplication.processEvents()
except Exception:
pass
# 启动解压线程
from workers.extraction_thread import ExtractionThread
self.extraction_thread = ExtractionThread(
patch_file, game_folder, plugin_path, game_version, self.main_window
)
# 连接进度更新到窗口控件
if self.extraction_thread and self.extraction_progress_window:
self.extraction_thread.progress.connect(
lambda percent, status: (
self.extraction_progress_window.progress_bar.setValue(percent),
self.extraction_progress_window.status_label.setText(status)
)
)
# 完成后进入哈希校验
self.extraction_thread.finished.connect(
lambda success, error, gv: self._on_extraction_finished_with_hash_check(success, error, gv, install_tasks)
)
# 线程结束时清理引用
try:
self.extraction_thread.finished.connect(lambda *_: setattr(self, 'extraction_thread', None))
except Exception:
pass
self.extraction_thread.start()
except Exception as e:
if debug_mode:
logger.error(f"DEBUG: 离线安装任务处理失败: {e}")
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
# 关闭可能存在的解压进度窗口
try:
if self.extraction_progress_window and self.extraction_progress_window.isVisible():
self.extraction_progress_window.close()
except Exception:
pass
self.extraction_progress_window = 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 is_offline_mode_available(self):
"""检查是否可以使用离线模式
Returns:
bool: 是否可以使用离线模式
"""
# 在调试模式下始终允许离线模式
if self._is_debug_mode():
return True
# 检查是否有离线补丁文件
return self.has_offline_patches()
def is_in_offline_mode(self):
"""检查当前是否处于离线模式
Returns:
bool: 是否处于离线模式
"""
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)
if hasattr(self.main_window, 'window_manager'):
self.main_window.window_manager.change_window_state(self.main_window.window_manager.STATE_READY)