feat(core): 集成补丁检测器以增强补丁管理功能

- 在主窗口中添加补丁检测器,支持补丁的检测和验证。
- 更新补丁管理器以使用补丁检测器进行补丁安装状态检查。
- 优化下载管理器和离线模式管理器,整合补丁检测逻辑,提升用户体验。
- 添加进度窗口以显示下载状态,增强用户反馈。
- 重构相关逻辑以支持新功能,确保代码可维护性和可读性。
This commit is contained in:
hyb-oyqq
2025-08-07 15:24:22 +08:00
parent d12739baab
commit bf80c19fe1
10 changed files with 874 additions and 554 deletions

View File

@@ -10,6 +10,7 @@ from .privacy_manager import PrivacyManager
from .cloudflare_optimizer import CloudflareOptimizer from .cloudflare_optimizer import CloudflareOptimizer
from .download_task_manager import DownloadTaskManager from .download_task_manager import DownloadTaskManager
from .extraction_handler import ExtractionHandler from .extraction_handler import ExtractionHandler
from .patch_detector import PatchDetector
__all__ = [ __all__ = [
'MultiStageAnimations', 'MultiStageAnimations',
@@ -23,5 +24,6 @@ __all__ = [
'PrivacyManager', 'PrivacyManager',
'CloudflareOptimizer', 'CloudflareOptimizer',
'DownloadTaskManager', 'DownloadTaskManager',
'ExtractionHandler' 'ExtractionHandler',
'PatchDetector'
] ]

View File

@@ -75,35 +75,25 @@ class CloudflareOptimizer:
# 解析域名 # 解析域名
hostname = urlparse(url).hostname hostname = urlparse(url).hostname
# 检查hosts文件中是否已有该域名的IP记录
existing_ips = self.hosts_manager.get_hostname_entries(hostname) if hostname else []
# 判断是否继续优选的逻辑 # 判断是否继续优选的逻辑
if existing_ips and self.has_optimized_in_session: if self.has_optimized_in_session:
# 如果本次会话中已执行过优选且hosts中存在记录,则跳过优选过程 # 如果本次会话中已执行过优选,则跳过优选过程
logger.info(f"发现hosts文件中已有域名 {hostname} 的优选IP记录且本次会话已优选,跳过优选过程") logger.info("本次会话已执行过优选,跳过优选过程")
# 设置标记为已优选完成 # 设置标记为已优选完成
self.optimization_done = True self.optimization_done = True
self.countdown_finished = True self.countdown_finished = True
# 尝试获取现有的IPv4和IPv6地址
ipv4_entries = [ip for ip in existing_ips if ':' not in ip] # IPv4地址不含冒号
ipv6_entries = [ip for ip in existing_ips if ':' in ip] # IPv6地址包含冒号
if ipv4_entries:
self.optimized_ip = ipv4_entries[0]
if ipv6_entries:
self.optimized_ipv6 = ipv6_entries[0]
logger.info(f"使用已存在的优选IP - IPv4: {self.optimized_ip}, IPv6: {self.optimized_ipv6}")
return True return True
else: else:
# 如果本次会话尚未优选过,或hosts中没有记录则显示优选窗口 # 如果本次会话尚未优选过,则清理可能存在的旧记录
if existing_ips: if hostname:
logger.info(f"发现hosts文件中已有域名 {hostname} 的优选IP记录但本次会话尚未优选过") # 检查hosts文件中是否已有域名的IP记录
# 清理已有的hosts记录准备重新优选 existing_ips = self.hosts_manager.get_hostname_entries(hostname)
self.hosts_manager.clean_hostname_entries(hostname) if existing_ips:
logger.info(f"发现hosts文件中已有域名 {hostname} 的优选IP记录但本次会话尚未优选过")
# 清理已有的hosts记录准备重新优选
self.hosts_manager.clean_hostname_entries(hostname)
# 创建取消状态标记 # 创建取消状态标记
self.optimization_cancelled = False self.optimization_cancelled = False
@@ -282,6 +272,9 @@ class CloudflareOptimizer:
def _process_optimization_results(self): def _process_optimization_results(self):
"""处理优选的IP结果显示相应提示""" """处理优选的IP结果显示相应提示"""
# 无论优选结果如何,都标记本次会话已执行过优选
self.has_optimized_in_session = True
use_ipv6 = False use_ipv6 = False
if hasattr(self.main_window, 'config'): if hasattr(self.main_window, 'config'):
use_ipv6 = self.main_window.config.get("ipv6_enabled", False) use_ipv6 = self.main_window.config.get("ipv6_enabled", False)
@@ -376,9 +369,6 @@ class CloudflareOptimizer:
from utils import save_config from utils import save_config
save_config(self.main_window.config) save_config(self.main_window.config)
# 记录本次会话已执行过优选
self.has_optimized_in_session = True
if success: if success:
msg_box = QtWidgets.QMessageBox(self.main_window) msg_box = QtWidgets.QMessageBox(self.main_window)
msg_box.setWindowTitle(f"成功 - {self.main_window.APP_NAME}") msg_box.setWindowTitle(f"成功 - {self.main_window.APP_NAME}")

View File

@@ -201,44 +201,37 @@ class DownloadManager:
return safe_config return safe_config
def download_action(self): def download_action(self):
"""开始下载流程""" """下载操作的主入口点"""
self.main_window.download_queue_history = [] if not self.selected_folder:
QtWidgets.QMessageBox.warning(
# 清除游戏检测器的目录缓存,确保获取最新的目录状态 self.main_window, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n"
if hasattr(self.main_window, 'game_detector') and hasattr(self.main_window.game_detector, 'clear_directory_cache'): )
self.main_window.game_detector.clear_directory_cache() return
if self.is_debug_mode():
logger.debug("DEBUG: 已清除游戏目录缓存,确保获取最新状态") # 识别游戏目录
game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder) game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder)
debug_mode = self.is_debug_mode()
if debug_mode:
logger.debug(f"DEBUG: 开始下载流程, 识别到 {len(game_dirs)} 个游戏目录")
if not game_dirs: if not game_dirs:
if debug_mode:
logger.warning("DEBUG: 未识别到任何游戏目录,设置目录未找到错误")
self.main_window.last_error_message = "directory_not_found"
QtWidgets.QMessageBox.warning( QtWidgets.QMessageBox.warning(
self.main_window, self.main_window, f"通知 - {APP_NAME}", "\n未在选择的目录中找到支持的游戏\n"
f"目录错误 - {APP_NAME}",
"\n未能识别到任何游戏目录。\n\n请确认您选择的是游戏的上级目录并且该目录中包含NEKOPARA系列游戏文件夹。\n"
) )
self.main_window.setEnabled(True) self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装") self.main_window.ui.start_install_text.setText("开始安装")
return return
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre", is_offline=False) # 显示文件检验窗口
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre")
# 获取安装路径
install_paths = self.get_install_paths() install_paths = self.get_install_paths()
# 创建并启动哈希线程进行预检查
self.main_window.hash_thread = self.main_window.create_hash_thread("pre", install_paths) self.main_window.hash_thread = self.main_window.create_hash_thread("pre", install_paths)
self.main_window.hash_thread.pre_finished.connect( self.main_window.hash_thread.pre_finished.connect(
lambda updated_status: self.on_pre_hash_finished_with_dirs(updated_status, game_dirs) lambda updated_status: self.on_pre_hash_finished_with_dirs(updated_status, game_dirs)
) )
self.main_window.hash_thread.start() self.main_window.hash_thread.start()
def on_pre_hash_finished_with_dirs(self, updated_status, game_dirs): def on_pre_hash_finished_with_dirs(self, updated_status, game_dirs):
"""优化的哈希预检查完成处理,带有游戏目录信息 """优化的哈希预检查完成处理,带有游戏目录信息
@@ -255,36 +248,8 @@ class DownloadManager:
self.main_window.setEnabled(True) self.main_window.setEnabled(True)
installable_games = [] # 使用patch_detector检测可安装的游戏
already_installed_games = [] already_installed_games, installable_games, disabled_patch_games = self.main_window.patch_detector.detect_installable_games(game_dirs)
disabled_patch_games = [] # 存储检测到禁用补丁的游戏
for game_version, game_dir in game_dirs.items():
# 首先通过文件检查确认补丁是否已安装
is_patch_installed = self.main_window.patch_manager.check_patch_installed(game_dir, game_version)
# 同时考虑哈希检查结果
hash_check_passed = self.main_window.installed_status.get(game_version, False)
# 如果补丁文件存在或哈希检查通过,认为已安装
if is_patch_installed or hash_check_passed:
if debug_mode:
logger.info(f"DEBUG: {game_version} 已安装补丁,不需要再次安装")
logger.info(f"DEBUG: 文件检查结果: {is_patch_installed}, 哈希检查结果: {hash_check_passed}")
already_installed_games.append(game_version)
# 更新安装状态
self.main_window.installed_status[game_version] = True
else:
# 检查是否存在被禁用的补丁
is_disabled, disabled_path = self.main_window.patch_manager.check_patch_disabled(game_dir, game_version)
if is_disabled:
if debug_mode:
logger.info(f"DEBUG: {game_version} 存在被禁用的补丁: {disabled_path}")
disabled_patch_games.append(game_version)
else:
if debug_mode:
logger.info(f"DEBUG: {game_version} 未安装补丁,可以安装")
logger.info(f"DEBUG: 文件检查结果: {is_patch_installed}, 哈希检查结果: {hash_check_passed}")
installable_games.append(game_version)
status_message = "" status_message = ""
if already_installed_games: if already_installed_games:
@@ -423,7 +388,11 @@ class DownloadManager:
self._fill_download_queue(config, selected_game_dirs) self._fill_download_queue(config, selected_game_dirs)
if not self.download_queue: if not self.download_queue:
self.main_window.after_hash_compare() # 所有下载任务都已完成,进行后检查
if debug_mode:
logger.debug("DEBUG: 所有下载任务完成,进行后检查")
# 使用patch_detector进行安装后哈希比较
self.main_window.patch_detector.after_hash_compare()
return return
# 如果是离线模式,直接开始下一个下载任务 # 如果是离线模式,直接开始下一个下载任务
@@ -544,30 +513,18 @@ class DownloadManager:
"""显示Cloudflare加速选择对话框""" """显示Cloudflare加速选择对话框"""
if self.download_queue: if self.download_queue:
first_url = self.download_queue[0][0] first_url = self.download_queue[0][0]
hostname = urlparse(first_url).hostname
if hostname: # 直接检查是否本次会话已执行过优选
existing_ips = self.cloudflare_optimizer.hosts_manager.get_hostname_entries(hostname) if self.cloudflare_optimizer.has_optimized_in_session:
logger.info("本次会话已执行过优选,跳过询问直接使用")
self.cloudflare_optimizer.optimization_done = True
self.cloudflare_optimizer.countdown_finished = True
self.main_window.current_url = first_url
self.next_download_task()
return
if existing_ips:
logger.info(f"发现hosts文件中已有域名 {hostname} 的优选IP记录跳过询问直接使用")
self.cloudflare_optimizer.optimization_done = True
self.cloudflare_optimizer.countdown_finished = True
ipv4_entries = [ip for ip in existing_ips if ':' not in ip]
ipv6_entries = [ip for ip in existing_ips if ':' in ip]
if ipv4_entries:
self.cloudflare_optimizer.optimized_ip = ipv4_entries[0]
if ipv6_entries:
self.cloudflare_optimizer.optimized_ipv6 = ipv6_entries[0]
self.main_window.current_url = first_url
self.next_download_task()
return
self.main_window.setEnabled(True) self.main_window.setEnabled(True)
msg_box = QtWidgets.QMessageBox(self.main_window) msg_box = QtWidgets.QMessageBox(self.main_window)
@@ -619,7 +576,11 @@ class DownloadManager:
def next_download_task(self): def next_download_task(self):
"""处理下载队列中的下一个任务""" """处理下载队列中的下一个任务"""
if not self.download_queue: if not self.download_queue:
self.main_window.after_hash_compare() # 所有下载任务都已完成,进行后检查
if debug_mode:
logger.debug("DEBUG: 所有下载任务完成,进行后检查")
# 使用patch_detector进行安装后哈希比较
self.main_window.patch_detector.after_hash_compare()
return return
if self.download_task_manager.current_download_thread and self.download_task_manager.current_download_thread.isRunning(): if self.download_task_manager.current_download_thread and self.download_task_manager.current_download_thread.isRunning():
@@ -735,21 +696,18 @@ class DownloadManager:
self.download_task_manager.start_download(url, _7z_path, game_version, game_folder, plugin_path) self.download_task_manager.start_download(url, _7z_path, game_version, game_folder, plugin_path)
def on_download_finished(self, success, error, url, game_folder, game_version, _7z_path, plugin_path): def on_download_finished(self, success, error, url, game_folder, game_version, _7z_path, plugin_path):
"""下载完成后的处理 """下载完成后的回调函数
Args: Args:
success: 是否下载成功 success: 是否下载成功
error: 错误信息 error: 错误信息
url: 下载URL url: 下载URL
game_folder: 游戏文件夹路径 game_folder: 游戏文件夹路径
game_version: 游戏版本名称 game_version: 游戏版本
_7z_path: 7z文件保存路径 _7z_path: 7z文件保存路径
plugin_path: 插件路径 plugin_path: 插件保存路径
""" """
if self.main_window.progress_window and self.main_window.progress_window.isVisible(): # 如果下载失败,显示错误并询问是否重试
self.main_window.progress_window.reject()
self.main_window.progress_window = None
if not success: if not success:
logger.error(f"--- Download Failed: {game_version} ---") logger.error(f"--- Download Failed: {game_version} ---")
logger.error(error) logger.error(error)
@@ -805,7 +763,6 @@ class DownloadManager:
return return
self.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version) self.extraction_handler.start_extraction(_7z_path, game_folder, plugin_path, game_version)
self.extraction_handler.extraction_finished.connect(self.on_extraction_finished)
def on_extraction_finished(self, continue_download): def on_extraction_finished(self, continue_download):
"""解压完成后的回调,决定是否继续下载队列 """解压完成后的回调,决定是否继续下载队列

View File

@@ -26,6 +26,7 @@ class OfflineModeManager:
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 ""
self.offline_patches = {} # 存储离线补丁信息 {补丁名称: 文件路径} self.offline_patches = {} # 存储离线补丁信息 {补丁名称: 文件路径}
self.is_offline_mode = False self.is_offline_mode = False
self.installed_games = [] # 跟踪本次实际安装的游戏
def _is_debug_mode(self): def _is_debug_mode(self):
"""检查是否处于调试模式 """检查是否处于调试模式
@@ -208,7 +209,7 @@ class OfflineModeManager:
return False return False
def verify_patch_hash(self, game_version, file_path): def verify_patch_hash(self, game_version, file_path):
"""验证补丁文件的哈希值 """验证补丁文件的哈希值使用patch_detector模块
Args: Args:
game_version: 游戏版本名称 game_version: 游戏版本名称
@@ -217,165 +218,9 @@ class OfflineModeManager:
Returns: Returns:
bool: 哈希值是否匹配 bool: 哈希值是否匹配
""" """
# 获取预期的哈希值 # 使用patch_detector模块验证哈希值
expected_hash = None return self.main_window.patch_detector.verify_patch_hash(game_version, file_path)
if "Vol.1" in game_version:
expected_hash = PLUGIN_HASH.get("vol1", "")
elif "Vol.2" in game_version:
expected_hash = PLUGIN_HASH.get("vol2", "")
elif "Vol.3" in game_version:
expected_hash = PLUGIN_HASH.get("vol3", "")
elif "Vol.4" in game_version:
expected_hash = PLUGIN_HASH.get("vol4", "")
elif "After" in game_version:
expected_hash = PLUGIN_HASH.get("after", "")
if not expected_hash:
logger.warning(f"DEBUG: 未找到 {game_version} 的预期哈希值")
return False
debug_mode = self._is_debug_mode()
if debug_mode:
logger.debug(f"DEBUG: 开始验证离线补丁文件: {file_path}")
logger.debug(f"DEBUG: 游戏版本: {game_version}")
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
try:
# 检查文件是否存在
if not os.path.exists(file_path):
if debug_mode:
logger.warning(f"DEBUG: 补丁文件不存在: {file_path}")
return False
# 检查文件大小
file_size = os.path.getsize(file_path)
if debug_mode:
logger.debug(f"DEBUG: 补丁文件大小: {file_size} 字节")
if file_size == 0:
if debug_mode:
logger.warning(f"DEBUG: 补丁文件大小为0无效文件")
return False
# 创建临时目录用于解压文件
with tempfile.TemporaryDirectory() as temp_dir:
if debug_mode:
logger.debug(f"DEBUG: 创建临时目录: {temp_dir}")
# 解压补丁文件
try:
if debug_mode:
logger.debug(f"DEBUG: 开始解压文件: {file_path}")
with py7zr.SevenZipFile(file_path, mode="r") as archive:
# 获取压缩包内文件列表
file_list = archive.getnames()
if debug_mode:
logger.debug(f"DEBUG: 压缩包内文件列表: {file_list}")
# 解压所有文件
archive.extractall(path=temp_dir)
if debug_mode:
logger.debug(f"DEBUG: 解压完成")
# 列出解压后的文件
extracted_files = []
for root, dirs, files in os.walk(temp_dir):
for file in files:
extracted_files.append(os.path.join(root, file))
logger.debug(f"DEBUG: 解压后的文件列表: {extracted_files}")
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()}")
return False
# 获取补丁文件路径
patch_file = None
if "Vol.1" in game_version:
patch_file = os.path.join(temp_dir, "vol.1", "adultsonly.xp3")
elif "Vol.2" in game_version:
patch_file = os.path.join(temp_dir, "vol.2", "adultsonly.xp3")
elif "Vol.3" in game_version:
patch_file = os.path.join(temp_dir, "vol.3", "update00.int")
elif "Vol.4" in game_version:
patch_file = os.path.join(temp_dir, "vol.4", "vol4adult.xp3")
elif "After" in game_version:
patch_file = os.path.join(temp_dir, "after", "afteradult.xp3")
if not patch_file or not os.path.exists(patch_file):
if debug_mode:
logger.warning(f"DEBUG: 未找到解压后的补丁文件: {patch_file}")
# 尝试查找可能的替代文件
alternative_files = []
for root, dirs, files in os.walk(temp_dir):
for file in files:
if file.endswith('.xp3') or file.endswith('.int'):
alternative_files.append(os.path.join(root, file))
if alternative_files:
logger.debug(f"DEBUG: 找到可能的替代文件: {alternative_files}")
# 检查解压目录结构
logger.debug(f"DEBUG: 检查解压目录结构:")
for root, dirs, files in os.walk(temp_dir):
logger.debug(f"DEBUG: 目录: {root}")
logger.debug(f"DEBUG: 子目录: {dirs}")
logger.debug(f"DEBUG: 文件: {files}")
return False
if debug_mode:
logger.debug(f"DEBUG: 找到解压后的补丁文件: {patch_file}")
# 计算补丁文件哈希值
try:
with open(patch_file, "rb") as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
# 比较哈希值
result = file_hash.lower() == expected_hash.lower()
if debug_mode:
logger.debug(f"DEBUG: 补丁文件 {patch_file} 哈希值验证: {'成功' if result else '失败'}")
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
logger.debug(f"DEBUG: 实际哈希值: {file_hash}")
return result
except Exception as e:
if debug_mode:
logger.error(f"DEBUG: 计算补丁文件哈希值失败: {e}")
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
return False
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()}")
return False
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 install_offline_patches(self, selected_games): def install_offline_patches(self, selected_games):
"""直接安装离线补丁,完全绕过下载模块 """直接安装离线补丁,完全绕过下载模块
@@ -419,78 +264,29 @@ class OfflineModeManager:
logger.warning("DEBUG: 未识别到任何游戏目录") logger.warning("DEBUG: 未识别到任何游戏目录")
return False return False
# 显示文件检验窗口 self.main_window.setEnabled(False)
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre", is_offline=True)
# 获取安装路径 # 重置已安装游戏列表
install_paths = self.main_window.download_manager.get_install_paths() self.installed_games = []
# 创建并启动哈希线程进行预检查 # 设置到主窗口,供结果显示使用
self.main_window.hash_thread = self.main_window.create_hash_thread("pre", install_paths) self.main_window.download_queue_history = selected_games
self.main_window.hash_thread.pre_finished.connect(
lambda updated_status: self.on_offline_pre_hash_finished(updated_status, game_dirs, selected_games)
)
self.main_window.hash_thread.start()
return True
def on_offline_pre_hash_finished(self, updated_status, game_dirs, selected_games):
"""离线模式下的哈希预检查完成处理
Args:
updated_status: 更新后的安装状态
game_dirs: 识别到的游戏目录
selected_games: 用户选择安装的游戏列表
"""
debug_mode = self._is_debug_mode()
# 更新安装状态
self.main_window.installed_status = updated_status
# 关闭哈希检查窗口
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
self.main_window.hash_msg_box.accept()
self.main_window.hash_msg_box = None
# 重新启用主窗口
self.main_window.setEnabled(True)
# 过滤出需要安装的游戏
installable_games = []
for game_version in selected_games:
if game_version in game_dirs and not self.main_window.installed_status.get(game_version, False):
# 检查是否有对应的离线补丁
if self.get_offline_patch_path(game_version):
installable_games.append(game_version)
elif debug_mode:
logger.warning(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过")
if not installable_games:
if debug_mode:
logger.info("DEBUG: 没有需要安装的游戏或未找到对应的离线补丁")
msgbox_frame(
f"离线安装信息 - {self.app_name}",
"\n没有需要安装的游戏或未找到对应的离线补丁文件。\n",
QMessageBox.StandardButton.Ok
).exec()
self.main_window.ui.start_install_text.setText("开始安装")
return
# 开始安装流程
if debug_mode:
logger.info(f"DEBUG: 开始离线安装流程,安装游戏: {installable_games}")
# 创建安装任务列表 # 创建安装任务列表
install_tasks = [] install_tasks = []
for game_version in installable_games: for game_version in selected_games:
# 获取离线补丁文件路径 # 获取离线补丁文件路径
patch_file = self.get_offline_patch_path(game_version) patch_file = self.get_offline_patch_path(game_version)
if not patch_file: if not patch_file:
if debug_mode:
logger.warning(f"DEBUG: 未找到 {game_version} 的离线补丁文件,跳过")
continue continue
# 获取游戏目录 # 获取游戏目录
game_folder = game_dirs.get(game_version) game_folder = game_dirs.get(game_version)
if not game_folder: if not game_folder:
if debug_mode:
logger.warning(f"DEBUG: 未找到 {game_version} 的游戏目录,跳过")
continue continue
# 获取目标路径 # 获取目标路径
@@ -510,6 +306,8 @@ class OfflineModeManager:
_7z_path = os.path.join(PLUGIN, "after.7z") _7z_path = os.path.join(PLUGIN, "after.7z")
plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"])
else: else:
if debug_mode:
logger.warning(f"DEBUG: {game_version} 不是支持的游戏版本,跳过")
continue continue
# 添加到安装任务列表 # 添加到安装任务列表
@@ -517,10 +315,22 @@ class OfflineModeManager:
# 开始执行第一个安装任务 # 开始执行第一个安装任务
if install_tasks: if install_tasks:
if debug_mode:
logger.info(f"DEBUG: 开始离线安装流程,安装游戏数量: {len(install_tasks)}")
self.process_next_offline_install_task(install_tasks) self.process_next_offline_install_task(install_tasks)
else: 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("开始安装") self.main_window.ui.start_install_text.setText("开始安装")
return True
def process_next_offline_install_task(self, install_tasks): def process_next_offline_install_task(self, install_tasks):
"""处理下一个离线安装任务 """处理下一个离线安装任务
@@ -533,7 +343,9 @@ class OfflineModeManager:
# 所有任务完成,进行后检查 # 所有任务完成,进行后检查
if debug_mode: if debug_mode:
logger.info("DEBUG: 所有离线安装任务完成,进行后检查") logger.info("DEBUG: 所有离线安装任务完成,进行后检查")
self.main_window.after_hash_compare()
# 使用patch_detector进行安装后哈希比较
self.main_window.patch_detector.after_hash_compare()
return return
# 获取下一个任务 # 获取下一个任务
@@ -555,22 +367,6 @@ class OfflineModeManager:
logger.debug(f"DEBUG: 已复制补丁文件到缓存目录: {_7z_path}") logger.debug(f"DEBUG: 已复制补丁文件到缓存目录: {_7z_path}")
logger.debug(f"DEBUG: 开始验证补丁文件哈希值") logger.debug(f"DEBUG: 开始验证补丁文件哈希值")
# 获取预期的哈希值
expected_hash = None
if "Vol.1" in game_version:
expected_hash = PLUGIN_HASH.get("vol1", "")
elif "Vol.2" in game_version:
expected_hash = PLUGIN_HASH.get("vol2", "")
elif "Vol.3" in game_version:
expected_hash = PLUGIN_HASH.get("vol3", "")
elif "Vol.4" in game_version:
expected_hash = PLUGIN_HASH.get("vol4", "")
elif "After" in game_version:
expected_hash = PLUGIN_HASH.get("after", "")
if debug_mode and expected_hash:
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
# 显示哈希验证窗口 - 使用离线特定消息 # 显示哈希验证窗口 - 使用离线特定消息
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)
@@ -683,20 +479,31 @@ class OfflineModeManager:
else: else:
# 更新安装状态 # 更新安装状态
self.main_window.installed_status[game_version] = True 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) self.process_next_offline_install_task(remaining_tasks)
def on_offline_extraction_finished(self, remaining_tasks): def is_offline_mode_available(self):
"""离线模式下的解压完成处理(旧方法,保留兼容性) """检查是否可以使用离线模式
Args: Returns:
remaining_tasks: 剩余的安装任务列表 bool: 是否可以使用离线模式
""" """
debug_mode = self._is_debug_mode() # 在调试模式下始终允许离线模式
if self._is_debug_mode():
if debug_mode: return True
logger.debug("DEBUG: 离线解压完成,继续处理下一个任务")
# 处理下一个任务 # 检查是否有离线补丁文件
self.process_next_offline_install_task(remaining_tasks) return self.has_offline_patches()
def is_in_offline_mode(self):
"""检查当前是否处于离线模式
Returns:
bool: 是否处于离线模式
"""
return self.is_offline_mode

View File

@@ -0,0 +1,599 @@
import os
import hashlib
import tempfile
import py7zr
import traceback
from utils.logger import setup_logger
from PySide6.QtWidgets import QMessageBox
from PySide6.QtCore import QTimer
from data.config import PLUGIN_HASH, APP_NAME
from workers.hash_thread import HashThread
# 初始化logger
logger = setup_logger("patch_detector")
class PatchDetector:
"""补丁检测与校验模块,用于统一处理在线和离线模式下的补丁检测和校验"""
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.game_info = {}
self.plugin_hash = {}
# 从配置中加载游戏信息和补丁哈希值
self._load_game_info()
def _load_game_info(self):
"""从配置中加载游戏信息和补丁哈希值"""
try:
from data.config import GAME_INFO, PLUGIN_HASH
self.game_info = GAME_INFO
self.plugin_hash = PLUGIN_HASH
except ImportError:
logger.error("无法加载游戏信息或补丁哈希值配置")
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 check_patch_installed(self, game_dir, game_version):
"""检查游戏是否已安装补丁
Args:
game_dir: 游戏目录路径
game_version: 游戏版本
Returns:
bool: 如果已安装补丁或有被禁用的补丁文件返回True否则返回False
"""
debug_mode = self._is_debug_mode()
if game_version not in self.game_info:
return False
# 获取可能的补丁文件路径
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
patch_file_path = os.path.join(game_dir, install_path_base)
# 尝试查找补丁文件,支持不同大小写
patch_files_to_check = [
patch_file_path,
patch_file_path.lower(),
patch_file_path.upper(),
patch_file_path.replace("_", ""),
patch_file_path.replace("_", "-"),
]
# 查找补丁文件
for patch_path in patch_files_to_check:
if os.path.exists(patch_path):
if debug_mode:
logger.debug(f"找到补丁文件: {patch_path}")
return True
# 检查是否存在被禁用的补丁文件(带.fain后缀
disabled_path = f"{patch_path}.fain"
if os.path.exists(disabled_path):
if debug_mode:
logger.debug(f"找到被禁用的补丁文件: {disabled_path}")
return True
# 检查是否有补丁文件夹
patch_folders_to_check = [
os.path.join(game_dir, "patch"),
os.path.join(game_dir, "Patch"),
os.path.join(game_dir, "PATCH"),
]
for patch_folder in patch_folders_to_check:
if os.path.exists(patch_folder):
if debug_mode:
logger.debug(f"找到补丁文件夹: {patch_folder}")
return True
# 检查game/patch文件夹
game_folders = ["game", "Game", "GAME"]
patch_folders = ["patch", "Patch", "PATCH"]
for game_folder in game_folders:
for patch_folder in patch_folders:
game_patch_folder = os.path.join(game_dir, game_folder, patch_folder)
if os.path.exists(game_patch_folder):
if debug_mode:
logger.debug(f"找到game/patch文件夹: {game_patch_folder}")
return True
# 检查配置文件
config_files = ["config.json", "Config.json", "CONFIG.JSON"]
script_files = ["scripts.json", "Scripts.json", "SCRIPTS.JSON"]
for game_folder in game_folders:
game_path = os.path.join(game_dir, game_folder)
if os.path.exists(game_path):
# 检查配置文件
for config_file in config_files:
config_path = os.path.join(game_path, config_file)
if os.path.exists(config_path):
if debug_mode:
logger.debug(f"找到配置文件: {config_path}")
return True
# 检查脚本文件
for script_file in script_files:
script_path = os.path.join(game_path, script_file)
if os.path.exists(script_path):
if debug_mode:
logger.debug(f"找到脚本文件: {script_path}")
return True
# 没有找到补丁文件或文件夹
if debug_mode:
logger.debug(f"{game_version}{game_dir} 中没有安装补丁")
return False
def check_patch_disabled(self, game_dir, game_version):
"""检查游戏的补丁是否已被禁用
Args:
game_dir: 游戏目录路径
game_version: 游戏版本
Returns:
bool: 如果补丁被禁用返回True否则返回False
str: 禁用的补丁文件路径如果没有禁用返回None
"""
debug_mode = self._is_debug_mode()
if game_version not in self.game_info:
return False, None
# 获取可能的补丁文件路径
install_path_base = os.path.basename(self.game_info[game_version]["install_path"])
patch_file_path = os.path.join(game_dir, install_path_base)
# 检查是否存在禁用的补丁文件(.fain后缀
disabled_patch_files = [
f"{patch_file_path}.fain",
f"{patch_file_path.lower()}.fain",
f"{patch_file_path.upper()}.fain",
f"{patch_file_path.replace('_', '')}.fain",
f"{patch_file_path.replace('_', '-')}.fain",
]
# 检查是否有禁用的补丁文件
for disabled_path in disabled_patch_files:
if os.path.exists(disabled_path):
if debug_mode:
logger.debug(f"找到禁用的补丁文件: {disabled_path}")
return True, disabled_path
if debug_mode:
logger.debug(f"{game_version}{game_dir} 的补丁未被禁用")
return False, None
def detect_installable_games(self, game_dirs):
"""检测可安装补丁的游戏
Args:
game_dirs: 游戏版本到游戏目录的映射字典
Returns:
tuple: (已安装补丁的游戏列表, 可安装补丁的游戏列表, 禁用补丁的游戏列表)
"""
debug_mode = self._is_debug_mode()
if debug_mode:
logger.debug(f"开始检测可安装补丁的游戏,游戏目录: {game_dirs}")
already_installed_games = []
installable_games = []
disabled_patch_games = []
for game_version, game_dir in game_dirs.items():
# 首先通过文件检查确认补丁是否已安装
is_patch_installed = self.check_patch_installed(game_dir, game_version)
# 同时考虑哈希检查结果
hash_check_passed = self.main_window.installed_status.get(game_version, False)
# 如果补丁文件存在或哈希检查通过,认为已安装
if is_patch_installed or hash_check_passed:
if debug_mode:
logger.info(f"DEBUG: {game_version} 已安装补丁,不需要再次安装")
logger.info(f"DEBUG: 文件检查结果: {is_patch_installed}, 哈希检查结果: {hash_check_passed}")
already_installed_games.append(game_version)
# 更新安装状态
self.main_window.installed_status[game_version] = True
else:
# 检查是否存在被禁用的补丁
is_disabled, disabled_path = self.check_patch_disabled(game_dir, game_version)
if is_disabled:
if debug_mode:
logger.info(f"DEBUG: {game_version} 存在被禁用的补丁: {disabled_path}")
disabled_patch_games.append(game_version)
else:
if debug_mode:
logger.info(f"DEBUG: {game_version} 未安装补丁,可以安装")
logger.info(f"DEBUG: 文件检查结果: {is_patch_installed}, 哈希检查结果: {hash_check_passed}")
installable_games.append(game_version)
if debug_mode:
logger.debug(f"检测结果 - 已安装补丁: {already_installed_games}")
logger.debug(f"检测结果 - 可安装补丁: {installable_games}")
logger.debug(f"检测结果 - 禁用补丁: {disabled_patch_games}")
return already_installed_games, installable_games, disabled_patch_games
def verify_patch_hash(self, game_version, file_path):
"""验证补丁文件的哈希值
Args:
game_version: 游戏版本名称
file_path: 补丁压缩包文件路径
Returns:
bool: 哈希值是否匹配
"""
# 获取预期的哈希值
expected_hash = None
# 直接使用完整游戏名称作为键
expected_hash = self.plugin_hash.get(game_version, "")
if not expected_hash:
logger.warning(f"DEBUG: 未找到 {game_version} 的预期哈希值")
return False
debug_mode = self._is_debug_mode()
if debug_mode:
logger.debug(f"DEBUG: 开始验证补丁文件: {file_path}")
logger.debug(f"DEBUG: 游戏版本: {game_version}")
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
try:
# 检查文件是否存在
if not os.path.exists(file_path):
if debug_mode:
logger.warning(f"DEBUG: 补丁文件不存在: {file_path}")
return False
# 检查文件大小
file_size = os.path.getsize(file_path)
if debug_mode:
logger.debug(f"DEBUG: 补丁文件大小: {file_size} 字节")
if file_size == 0:
if debug_mode:
logger.warning(f"DEBUG: 补丁文件大小为0无效文件")
return False
# 创建临时目录用于解压文件
with tempfile.TemporaryDirectory() as temp_dir:
if debug_mode:
logger.debug(f"DEBUG: 创建临时目录: {temp_dir}")
# 解压补丁文件
try:
if debug_mode:
logger.debug(f"DEBUG: 开始解压文件: {file_path}")
with py7zr.SevenZipFile(file_path, mode="r") as archive:
# 获取压缩包内文件列表
file_list = archive.getnames()
if debug_mode:
logger.debug(f"DEBUG: 压缩包内文件列表: {file_list}")
# 解压所有文件
archive.extractall(path=temp_dir)
if debug_mode:
logger.debug(f"DEBUG: 解压完成")
# 列出解压后的文件
extracted_files = []
for root, dirs, files in os.walk(temp_dir):
for file in files:
extracted_files.append(os.path.join(root, file))
logger.debug(f"DEBUG: 解压后的文件列表: {extracted_files}")
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()}")
return False
# 获取补丁文件路径
patch_file = None
if "Vol.1" in game_version:
patch_file = os.path.join(temp_dir, "vol.1", "adultsonly.xp3")
elif "Vol.2" in game_version:
patch_file = os.path.join(temp_dir, "vol.2", "adultsonly.xp3")
elif "Vol.3" in game_version:
patch_file = os.path.join(temp_dir, "vol.3", "update00.int")
elif "Vol.4" in game_version:
patch_file = os.path.join(temp_dir, "vol.4", "vol4adult.xp3")
elif "After" in game_version:
patch_file = os.path.join(temp_dir, "after", "afteradult.xp3")
if not patch_file or not os.path.exists(patch_file):
if debug_mode:
logger.warning(f"DEBUG: 未找到解压后的补丁文件: {patch_file}")
# 尝试查找可能的替代文件
alternative_files = []
for root, dirs, files in os.walk(temp_dir):
for file in files:
if file.endswith('.xp3') or file.endswith('.int'):
alternative_files.append(os.path.join(root, file))
if alternative_files:
logger.debug(f"DEBUG: 找到可能的替代文件: {alternative_files}")
# 检查解压目录结构
logger.debug(f"DEBUG: 检查解压目录结构:")
for root, dirs, files in os.walk(temp_dir):
logger.debug(f"DEBUG: 目录: {root}")
logger.debug(f"DEBUG: 子目录: {dirs}")
logger.debug(f"DEBUG: 文件: {files}")
return False
if debug_mode:
logger.debug(f"DEBUG: 找到解压后的补丁文件: {patch_file}")
# 计算补丁文件哈希值
try:
with open(patch_file, "rb") as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
# 比较哈希值
result = file_hash.lower() == expected_hash.lower()
if debug_mode:
logger.debug(f"DEBUG: 补丁文件 {patch_file} 哈希值验证: {'成功' if result else '失败'}")
logger.debug(f"DEBUG: 预期哈希值: {expected_hash}")
logger.debug(f"DEBUG: 实际哈希值: {file_hash}")
return result
except Exception as e:
if debug_mode:
logger.error(f"DEBUG: 计算补丁文件哈希值失败: {e}")
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
return False
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()}")
return False
def create_hash_thread(self, mode, install_paths):
"""创建哈希检查线程
Args:
mode: 检查模式,"pre""after"
install_paths: 安装路径字典
Returns:
HashThread: 哈希检查线程实例
"""
return HashThread(mode, install_paths, PLUGIN_HASH, self.main_window.installed_status, self.main_window)
def after_hash_compare(self):
"""进行安装后哈希比较"""
# 禁用窗口已在安装流程开始时完成
# 检查是否处于离线模式
is_offline = False
if hasattr(self.main_window, 'offline_mode_manager'):
is_offline = self.main_window.offline_mode_manager.is_in_offline_mode()
self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="after", is_offline=is_offline)
install_paths = self.main_window.download_manager.get_install_paths()
self.main_window.hash_thread = self.create_hash_thread("after", install_paths)
self.main_window.hash_thread.after_finished.connect(self.on_after_hash_finished)
self.main_window.hash_thread.start()
def on_after_hash_finished(self, result):
"""哈希比较完成后的处理
Args:
result: 哈希比较结果
"""
# 确保哈希检查窗口关闭,无论是否还在显示
if self.main_window.hash_msg_box:
try:
if self.main_window.hash_msg_box.isVisible():
self.main_window.hash_msg_box.close()
else:
# 如果窗口已经不可见但没有关闭,也要尝试关闭
self.main_window.hash_msg_box.close()
except:
pass # 忽略任何关闭窗口时的错误
self.main_window.hash_msg_box = None
if not result["passed"]:
# 启用窗口以显示错误消息
self.main_window.setEnabled(True)
game = result.get("game", "未知游戏")
message = result.get("message", "发生未知错误。")
msg_box = QMessageBox.critical(
self.main_window,
f"文件校验失败 - {APP_NAME}",
message,
QMessageBox.StandardButton.Ok,
)
# 恢复窗口状态
self.main_window.setEnabled(True)
self.main_window.ui.start_install_text.setText("开始安装")
# 添加短暂延迟确保UI更新
QTimer.singleShot(100, self.main_window.show_result)
def on_offline_pre_hash_finished(self, updated_status, game_dirs):
"""离线模式下的哈希预检查完成处理
Args:
updated_status: 更新后的安装状态
game_dirs: 识别到的游戏目录
"""
# 更新安装状态
self.main_window.installed_status = updated_status
# 关闭哈希检查窗口
if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible():
self.main_window.hash_msg_box.accept()
self.main_window.hash_msg_box = None
# 重新启用主窗口
self.main_window.setEnabled(True)
# 使用patch_detector检测可安装的游戏
already_installed_games, installable_games, disabled_patch_games = self.detect_installable_games(game_dirs)
debug_mode = self._is_debug_mode()
status_message = ""
if already_installed_games:
status_message += f"已安装补丁的游戏:\n{chr(10).join(already_installed_games)}\n\n"
# 处理禁用补丁的情况
if disabled_patch_games:
# 构建提示消息
disabled_msg = f"检测到以下游戏的补丁已被禁用:\n{chr(10).join(disabled_patch_games)}\n\n是否要启用这些补丁?"
from PySide6 import QtWidgets
reply = QtWidgets.QMessageBox.question(
self.main_window,
f"检测到禁用补丁 - {APP_NAME}",
disabled_msg,
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No
)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
# 用户选择启用补丁
if debug_mode:
logger.debug(f"DEBUG: 用户选择启用被禁用的补丁")
# 为每个禁用的游戏创建目录映射
disabled_game_dirs = {game: game_dirs[game] for game in disabled_patch_games}
# 批量启用补丁
success_count, fail_count, results = self.main_window.patch_manager.batch_toggle_patches(
disabled_game_dirs,
operation="enable"
)
# 显示启用结果
self.main_window.patch_manager.show_toggle_result(success_count, fail_count, results)
# 更新安装状态
for game_version in disabled_patch_games:
self.main_window.installed_status[game_version] = True
if game_version in installable_games:
installable_games.remove(game_version)
if game_version not in already_installed_games:
already_installed_games.append(game_version)
else:
if debug_mode:
logger.info(f"DEBUG: 用户选择不启用被禁用的补丁,这些游戏将被添加到可安装列表")
# 用户选择不启用,将这些游戏视为可以安装补丁
installable_games.extend(disabled_patch_games)
# 更新status_message
if disabled_patch_games:
status_message += f"禁用补丁的游戏:\n{chr(10).join(disabled_patch_games)}\n\n"
if not installable_games:
# 没有可安装的游戏显示信息并重置UI
if already_installed_games:
# 有已安装的游戏,显示已安装信息
QMessageBox.information(
self.main_window,
f"信息 - {APP_NAME}",
f"\n所有游戏已安装补丁,无需重复安装。\n\n{status_message}",
QMessageBox.StandardButton.Ok,
)
else:
# 没有已安装的游戏,可能是未检测到游戏
QMessageBox.warning(
self.main_window,
f"警告 - {APP_NAME}",
"\n未检测到任何需要安装补丁的游戏。\n\n请确保游戏文件夹位于选择的目录中。\n",
QMessageBox.StandardButton.Ok,
)
self.main_window.ui.start_install_text.setText("开始安装")
return
# 显示游戏选择对话框
from PySide6 import QtWidgets
dialog = QtWidgets.QDialog(self.main_window)
dialog.setWindowTitle(f"选择要安装的游戏 - {APP_NAME}")
dialog.setMinimumWidth(300)
layout = QtWidgets.QVBoxLayout()
# 添加说明标签
label = QtWidgets.QLabel("请选择要安装补丁的游戏:")
layout.addWidget(label)
# 添加游戏列表
list_widget = QtWidgets.QListWidget()
list_widget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.MultiSelection)
for game in installable_games:
item = QtWidgets.QListWidgetItem(game)
list_widget.addItem(item)
item.setSelected(True) # 默认全选
layout.addWidget(list_widget)
# 添加按钮
button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.StandardButton.Ok |
QtWidgets.QDialogButtonBox.StandardButton.Cancel
)
button_box.accepted.connect(dialog.accept)
button_box.rejected.connect(dialog.reject)
layout.addWidget(button_box)
dialog.setLayout(layout)
# 显示对话框
result = dialog.exec()
if result != QtWidgets.QDialog.DialogCode.Accepted or list_widget.selectedItems() == []:
self.main_window.ui.start_install_text.setText("开始安装")
return
# 获取用户选择的游戏
selected_games = [item.text() for item in list_widget.selectedItems()]
# 开始安装
if debug_mode:
logger.debug(f"DEBUG: 用户选择了以下游戏进行安装: {selected_games}")
# 调用离线模式管理器安装补丁
self.main_window.offline_mode_manager.install_offline_patches(selected_games)

View File

@@ -3,23 +3,36 @@ import shutil
import traceback import traceback
from PySide6.QtWidgets import QMessageBox from PySide6.QtWidgets import QMessageBox
from utils.logger import setup_logger from utils.logger import setup_logger
from data.config import APP_NAME
from utils import msgbox_frame
class PatchManager: class PatchManager:
"""补丁管理器,用于处理补丁的安装和卸载""" """补丁管理器,用于处理补丁的安装和卸载"""
def __init__(self, app_name, game_info, debug_manager=None): def __init__(self, app_name, game_info, debug_manager=None, main_window=None):
"""初始化补丁管理器 """初始化补丁管理器
Args: Args:
app_name: 应用程序名称,用于显示消息框标题 app_name: 应用程序名称,用于显示消息框标题
game_info: 游戏信息字典,包含各版本的安装路径和可执行文件名 game_info: 游戏信息字典,包含各版本的安装路径和可执行文件名
debug_manager: 调试管理器实例,用于输出调试信息 debug_manager: 调试管理器实例,用于输出调试信息
main_window: 主窗口实例用于访问UI和状态
""" """
self.app_name = app_name self.app_name = app_name
self.game_info = game_info self.game_info = game_info
self.debug_manager = debug_manager self.debug_manager = debug_manager
self.main_window = main_window # 添加main_window属性
self.installed_status = {} # 游戏版本的安装状态 self.installed_status = {} # 游戏版本的安装状态
self.logger = setup_logger("patch_manager") self.logger = setup_logger("patch_manager")
self.patch_detector = None # 将在main_window初始化后设置
def set_patch_detector(self, patch_detector):
"""设置补丁检测器实例
Args:
patch_detector: 补丁检测器实例
"""
self.patch_detector = patch_detector
def _is_debug_mode(self): def _is_debug_mode(self):
"""检查是否处于调试模式 """检查是否处于调试模式
@@ -331,7 +344,7 @@ class PatchManager:
) )
def check_patch_installed(self, game_dir, game_version): def check_patch_installed(self, game_dir, game_version):
"""检查游戏是否已安装补丁 """检查游戏是否已安装补丁调用patch_detector
Args: Args:
game_dir: 游戏目录路径 game_dir: 游戏目录路径
@@ -340,6 +353,10 @@ class PatchManager:
Returns: Returns:
bool: 如果已安装补丁或有被禁用的补丁文件返回True否则返回False bool: 如果已安装补丁或有被禁用的补丁文件返回True否则返回False
""" """
if self.patch_detector:
return self.patch_detector.check_patch_installed(game_dir, game_version)
# 如果patch_detector未设置使用原始逻辑应该不会执行到这里
debug_mode = self._is_debug_mode() debug_mode = self._is_debug_mode()
if game_version not in self.game_info: if game_version not in self.game_info:
@@ -425,7 +442,7 @@ class PatchManager:
return False return False
def check_patch_disabled(self, game_dir, game_version): def check_patch_disabled(self, game_dir, game_version):
"""检查游戏的补丁是否已被禁用 """检查游戏的补丁是否已被禁用调用patch_detector
Args: Args:
game_dir: 游戏目录路径 game_dir: 游戏目录路径
@@ -435,6 +452,10 @@ class PatchManager:
bool: 如果补丁被禁用返回True否则返回False bool: 如果补丁被禁用返回True否则返回False
str: 禁用的补丁文件路径如果没有禁用返回None str: 禁用的补丁文件路径如果没有禁用返回None
""" """
if self.patch_detector:
return self.patch_detector.check_patch_disabled(game_dir, game_version)
# 如果patch_detector未设置使用原始逻辑应该不会执行到这里
debug_mode = self._is_debug_mode() debug_mode = self._is_debug_mode()
if game_version not in self.game_info: if game_version not in self.game_info:
@@ -743,4 +764,86 @@ class PatchManager:
f"批量操作完成 - {self.app_name}", f"批量操作完成 - {self.app_name}",
result_text, result_text,
QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.Ok,
)
def show_result(self):
"""显示安装结果,区分不同情况"""
# 获取当前安装状态
installed_versions = [] # 成功安装的版本
skipped_versions = [] # 已有补丁跳过的版本
failed_versions = [] # 安装失败的版本
not_found_versions = [] # 未找到的版本
# 获取所有游戏版本路径
install_paths = self.main_window.download_manager.get_install_paths() if hasattr(self.main_window.download_manager, "get_install_paths") else {}
# 检查是否处于离线模式
is_offline_mode = False
if hasattr(self.main_window, 'offline_mode_manager'):
is_offline_mode = self.main_window.offline_mode_manager.is_in_offline_mode()
# 获取本次实际安装的游戏列表
installed_games = []
# 在线模式下使用download_queue_history
if hasattr(self.main_window, 'download_queue_history') and self.main_window.download_queue_history:
installed_games = self.main_window.download_queue_history
# 离线模式下使用offline_mode_manager.installed_games
if is_offline_mode and hasattr(self.main_window.offline_mode_manager, 'installed_games'):
installed_games = self.main_window.offline_mode_manager.installed_games
debug_mode = self._is_debug_mode()
if debug_mode:
self.logger.debug(f"DEBUG: 显示安装结果,离线模式: {is_offline_mode}")
self.logger.debug(f"DEBUG: 本次安装的游戏: {installed_games}")
for game_version, is_installed in self.main_window.installed_status.items():
# 只处理install_paths中存在的游戏版本
if game_version in install_paths:
path = install_paths[game_version]
# 检查游戏是否存在但未通过本次安装补丁
if is_installed:
# 游戏已安装补丁
if game_version in installed_games:
# 本次成功安装
installed_versions.append(game_version)
else:
# 已有补丁,被跳过下载
skipped_versions.append(game_version)
else:
# 游戏未安装补丁
if os.path.exists(path):
# 游戏文件夹存在,但安装失败
failed_versions.append(game_version)
else:
# 游戏文件夹不存在
not_found_versions.append(game_version)
# 构建结果信息
result_text = f"\n安装结果:\n"
# 总数统计 - 只显示本次实际安装的数量
total_installed = len(installed_versions)
total_failed = len(failed_versions)
result_text += f"安装成功:{total_installed} 个 安装失败:{total_failed}\n\n"
# 详细列表
if installed_versions:
result_text += f"【成功安装】:\n{chr(10).join(installed_versions)}\n\n"
if failed_versions:
result_text += f"【安装失败】:\n{chr(10).join(failed_versions)}\n\n"
if not_found_versions:
# 只有在真正检测到了游戏但未安装补丁时才显示
result_text += f"【尚未安装补丁的游戏】:\n{chr(10).join(not_found_versions)}\n"
QMessageBox.information(
self.main_window,
f"安装完成 - {APP_NAME}",
result_text
) )

View File

@@ -720,7 +720,7 @@ class UIManager:
log_datetime = "-".join(os.path.basename(latest_log)[4:-4].split("-")[:2]) log_datetime = "-".join(os.path.basename(latest_log)[4:-4].split("-")[:2])
log_date = log_datetime.split("-")[0] log_date = log_datetime.split("-")[0]
log_time = log_datetime.split("-")[1] if "-" in log_datetime else "未知时间" log_time = log_datetime.split("-")[1] if "-" in log_datetime else "未知时间"
date_info = f"日期: {log_date[:4]}-{log_date[4:6]}-{log_date[6:]} " date_info = f"日期: {log_date[:4]}-{log_date[4:6]}-{log_date[6:]}"
time_info = f"时间: {log_time[:2]}:{log_time[2:4]}:{log_time[4:]}" time_info = f"时间: {log_time[:2]}:{log_time[2:4]}:{log_time[4:]}"
except: except:
date_info = "日期未知 " date_info = "日期未知 "
@@ -780,8 +780,8 @@ class UIManager:
"""手动删除软件添加的hosts条目""" """手动删除软件添加的hosts条目"""
if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'): if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'):
try: try:
# 调用清理hosts条目的方法 # 调用清理hosts条目的方法,强制清理即使禁用了自动还原
result = self.main_window.download_manager.hosts_manager.check_and_clean_all_entries() result = self.main_window.download_manager.hosts_manager.check_and_clean_all_entries(force_clean=True)
if result: if result:
msg_box = self._create_message_box("成功", "\n已成功清理软件添加的hosts条目。\n") msg_box = self._create_message_box("成功", "\n已成功清理软件添加的hosts条目。\n")

View File

@@ -10,6 +10,7 @@ from PySide6.QtCore import QTimer, Qt, QPoint, QRect, QSize
from PySide6.QtWidgets import QMainWindow, QMessageBox, QGraphicsOpacityEffect, QGraphicsColorizeEffect from PySide6.QtWidgets import QMainWindow, QMessageBox, QGraphicsOpacityEffect, QGraphicsColorizeEffect
from PySide6.QtGui import QPalette, QColor, QPainterPath, QRegion, QFont from PySide6.QtGui import QPalette, QColor, QPainterPath, QRegion, QFont
from PySide6.QtGui import QAction # Added for menu actions from PySide6.QtGui import QAction # Added for menu actions
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QProgressBar, QLabel # Added for progress window
from ui.Ui_install import Ui_MainWindows from ui.Ui_install import Ui_MainWindows
from data.config import ( from data.config import (
@@ -26,7 +27,7 @@ from workers import (
) )
from core import ( from core import (
MultiStageAnimations, UIManager, DownloadManager, DebugManager, MultiStageAnimations, UIManager, DownloadManager, DebugManager,
WindowManager, GameDetector, PatchManager, ConfigManager WindowManager, GameDetector, PatchManager, ConfigManager, PatchDetector
) )
from core.ipv6_manager import IPv6Manager from core.ipv6_manager import IPv6Manager
from handlers import PatchToggleHandler, UninstallHandler from handlers import PatchToggleHandler, UninstallHandler
@@ -79,16 +80,22 @@ class MainWindow(QMainWindow):
# 5. 初始化其他管理器 # 5. 初始化其他管理器
self.config_manager = ConfigManager(APP_NAME, CONFIG_URL, UA, self.debug_manager) self.config_manager = ConfigManager(APP_NAME, CONFIG_URL, UA, self.debug_manager)
self.game_detector = GameDetector(GAME_INFO, self.debug_manager) self.game_detector = GameDetector(GAME_INFO, self.debug_manager)
self.patch_manager = PatchManager(APP_NAME, GAME_INFO, self.debug_manager) self.patch_manager = PatchManager(APP_NAME, GAME_INFO, self.debug_manager, self)
# 6. 初始化离线模式管理器 # 6. 初始化补丁检测模块
self.patch_detector = PatchDetector(self)
# 7. 设置补丁检测器到补丁管理器
self.patch_manager.set_patch_detector(self.patch_detector)
# 8. 初始化离线模式管理器
from core.offline_mode_manager import OfflineModeManager from core.offline_mode_manager import OfflineModeManager
self.offline_mode_manager = OfflineModeManager(self) self.offline_mode_manager = OfflineModeManager(self)
# 7. 初始化下载管理器 - 放在最后,因为它可能依赖于其他管理器 # 9. 初始化下载管理器 - 放在最后,因为它可能依赖于其他管理器
self.download_manager = DownloadManager(self) self.download_manager = DownloadManager(self)
# 8. 初始化功能处理程序 # 10. 初始化功能处理程序
self.uninstall_handler = UninstallHandler(self) self.uninstall_handler = UninstallHandler(self)
self.patch_toggle_handler = PatchToggleHandler(self) self.patch_toggle_handler = PatchToggleHandler(self)
@@ -325,33 +332,40 @@ class MainWindow(QMainWindow):
Args: Args:
url: 下载URL url: 下载URL
_7z_path: 7z文件保存路径 _7z_path: 7z文件保存路径
game_version: 游戏版本名称 game_version: 游戏版本
Returns: Returns:
DownloadThread: 下载线程实例 DownloadThread: 下载线程实例
""" """
from workers import DownloadThread return DownloadThread(url, _7z_path, game_version, self)
return DownloadThread(url, _7z_path, game_version, parent=self)
def create_progress_window(self): def create_progress_window(self):
"""创建下载进度窗口 """创建进度窗口
Returns: Returns:
ProgressWindow: 进度窗口实例 QDialog: 进度窗口实例
""" """
return ProgressWindow(self) progress_window = QDialog(self)
progress_window.setWindowTitle(f"下载进度 - {APP_NAME}")
progress_window.setFixedSize(400, 150)
def create_hash_thread(self, mode, install_paths): layout = QVBoxLayout()
"""创建哈希检查线程
Args: # 添加进度条
mode: 检查模式,"pre""after" progress_bar = QProgressBar()
install_paths: 安装路径字典 progress_bar.setRange(0, 100)
progress_bar.setValue(0)
Returns: layout.addWidget(progress_bar)
HashThread: 哈希检查线程实例
""" # 添加标签
return HashThread(mode, install_paths, PLUGIN_HASH, self.installed_status, self) status_label = QLabel("准备下载...")
layout.addWidget(status_label)
progress_window.setLayout(layout)
progress_window.progress_bar = progress_bar
progress_window.status_label = status_label
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):
"""创建解压线程 """创建解压线程
@@ -367,121 +381,10 @@ class MainWindow(QMainWindow):
""" """
return ExtractionThread(_7z_path, game_folder, plugin_path, game_version, self) return ExtractionThread(_7z_path, game_folder, plugin_path, game_version, self)
def after_hash_compare(self):
"""进行安装后哈希比较"""
# 禁用窗口已在安装流程开始时完成
# 检查是否处于离线模式
is_offline = False
if hasattr(self, 'offline_mode_manager'):
is_offline = self.offline_mode_manager.is_in_offline_mode()
self.hash_msg_box = self.hash_manager.hash_pop_window(check_type="after", is_offline=is_offline)
install_paths = self.download_manager.get_install_paths()
self.hash_thread = self.create_hash_thread("after", install_paths)
self.hash_thread.after_finished.connect(self.on_after_hash_finished)
self.hash_thread.start()
def on_after_hash_finished(self, result):
"""哈希比较完成后的处理
Args:
result: 哈希比较结果
"""
# 确保哈希检查窗口关闭,无论是否还在显示
if self.hash_msg_box:
try:
if self.hash_msg_box.isVisible():
self.hash_msg_box.close()
else:
# 如果窗口已经不可见但没有关闭,也要尝试关闭
self.hash_msg_box.close()
except:
pass # 忽略任何关闭窗口时的错误
self.hash_msg_box = None
if not result["passed"]:
# 启用窗口以显示错误消息
self.setEnabled(True)
game = result.get("game", "未知游戏")
message = result.get("message", "发生未知错误。")
msg_box = msgbox_frame(
f"文件校验失败 - {APP_NAME}",
message,
QMessageBox.StandardButton.Ok,
)
msg_box.exec()
# 恢复窗口状态
self.setEnabled(True)
self.ui.start_install_text.setText("开始安装")
# 添加短暂延迟确保UI更新
QTimer.singleShot(100, self.show_result)
def show_result(self): def show_result(self):
"""显示安装结果,区分不同情况""" """显示安装结果,调用patch_manager的show_result方法"""
# 获取当前安装状态 self.patch_manager.show_result()
installed_versions = [] # 成功安装的版本
skipped_versions = [] # 已有补丁跳过的版本
failed_versions = [] # 安装失败的版本
not_found_versions = [] # 未找到的版本
# 获取所有游戏版本路径
install_paths = self.download_manager.get_install_paths() if hasattr(self.download_manager, "get_install_paths") else {}
for game_version, is_installed in self.installed_status.items():
# 只处理install_paths中存在的游戏版本
if game_version in install_paths:
path = install_paths[game_version]
# 检查游戏是否存在但未通过本次安装补丁
if is_installed:
# 游戏已安装补丁
if hasattr(self, 'download_queue_history') and game_version not in self.download_queue_history:
# 已有补丁,被跳过下载
skipped_versions.append(game_version)
else:
# 本次成功安装
installed_versions.append(game_version)
else:
# 游戏未安装补丁
if os.path.exists(path):
# 游戏文件夹存在,但安装失败
failed_versions.append(game_version)
else:
# 游戏文件夹不存在
not_found_versions.append(game_version)
# 构建结果信息
result_text = f"\n安装结果:\n"
# 总数统计 - 不再显示已跳过的数量
total_installed = len(installed_versions)
total_failed = len(failed_versions)
result_text += f"安装成功:{total_installed} 个 安装失败:{total_failed}\n\n"
# 详细列表
if installed_versions:
result_text += f"【成功安装】:\n{chr(10).join(installed_versions)}\n\n"
if failed_versions:
result_text += f"【安装失败】:\n{chr(10).join(failed_versions)}\n\n"
if not_found_versions:
# 只有在真正检测到了游戏但未安装补丁时才显示
result_text += f"【尚未安装补丁的游戏】:\n{chr(10).join(not_found_versions)}\n"
QMessageBox.information(
self,
f"安装完成 - {APP_NAME}",
result_text
)
def closeEvent(self, event): def closeEvent(self, event):
"""窗口关闭事件处理 """窗口关闭事件处理
@@ -651,64 +554,18 @@ class MainWindow(QMainWindow):
self.ui.start_install_text.setText("开始安装") self.ui.start_install_text.setText("开始安装")
return return
# 显示游戏选择对话框 # 显示文件检验窗口
dialog = QtWidgets.QDialog(self) self.hash_msg_box = self.hash_manager.hash_pop_window(check_type="pre", is_offline=True)
dialog.setWindowTitle("选择要安装的游戏")
dialog.resize(400, 300)
layout = QtWidgets.QVBoxLayout(dialog) # 获取安装路径
install_paths = self.download_manager.get_install_paths()
# 添加"选择要安装的游戏"标签 # 创建并启动哈希线程进行预检查
title_label = QtWidgets.QLabel("选择要安装的游戏", dialog) self.hash_thread = self.patch_detector.create_hash_thread("pre", install_paths)
title_label.setFont(QFont(title_label.font().family(), title_label.font().pointSize(), QFont.Bold)) self.hash_thread.pre_finished.connect(
layout.addWidget(title_label) lambda updated_status: self.patch_detector.on_offline_pre_hash_finished(updated_status, game_dirs)
)
# 添加游戏列表控件 self.hash_thread.start()
list_widget = QtWidgets.QListWidget(dialog)
list_widget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection) # 允许多选
for game_version in game_dirs.keys():
list_widget.addItem(game_version)
# 默认选中所有项目
list_widget.item(list_widget.count() - 1).setSelected(True)
layout.addWidget(list_widget)
# 添加全选按钮
select_all_btn = QtWidgets.QPushButton("全选", dialog)
select_all_btn.clicked.connect(lambda: list_widget.selectAll())
layout.addWidget(select_all_btn)
# 添加确定和取消按钮
buttons_layout = QtWidgets.QHBoxLayout()
ok_button = QtWidgets.QPushButton("确定", dialog)
cancel_button = QtWidgets.QPushButton("取消", dialog)
buttons_layout.addWidget(ok_button)
buttons_layout.addWidget(cancel_button)
layout.addLayout(buttons_layout)
# 连接按钮事件
ok_button.clicked.connect(dialog.accept)
cancel_button.clicked.connect(dialog.reject)
# 显示对话框
if dialog.exec() == QtWidgets.QDialog.DialogCode.Accepted:
# 获取选择的游戏
selected_games = [item.text() for item in list_widget.selectedItems()]
if selected_games:
# 使用离线模式管理器进行安装
self.offline_mode_manager.install_offline_patches(selected_games)
else:
QtWidgets.QMessageBox.information(
self,
f"通知 - {APP_NAME}",
"\n未选择任何游戏,安装已取消。\n"
)
self.setEnabled(True)
self.ui.start_install_text.setText("开始安装")
else:
# 用户取消了选择
self.setEnabled(True)
self.ui.start_install_text.setText("开始安装")
else: else:
# 在线模式下,检查版本是否过低 # 在线模式下,检查版本是否过低
if hasattr(self, 'version_warning') and self.version_warning: if hasattr(self, 'version_warning') and self.version_warning:
@@ -723,6 +580,8 @@ class MainWindow(QMainWindow):
# 版本正常,使用原有的下载流程 # 版本正常,使用原有的下载流程
self.download_manager.file_dialog() self.download_manager.file_dialog()
# 移除on_offline_pre_hash_finished方法
def check_and_set_offline_mode(self): def check_and_set_offline_mode(self):
"""检查是否有离线补丁文件,如果有则自动启用离线模式 """检查是否有离线补丁文件,如果有则自动启用离线模式

View File

@@ -567,14 +567,17 @@ class HostsManager:
self.auto_restore_disabled = auto_restore_disabled self.auto_restore_disabled = auto_restore_disabled
return auto_restore_disabled return auto_restore_disabled
def check_and_clean_all_entries(self): def check_and_clean_all_entries(self, force_clean=False):
"""检查并清理所有由本应用程序添加的hosts记录 """检查并清理所有由本应用程序添加的hosts记录
Args:
force_clean: 是否强制清理,即使禁用了自动还原
Returns: Returns:
bool: 清理是否成功 bool: 清理是否成功
""" """
# 如果禁用了自动还原,则不执行清理操作 # 如果禁用了自动还原,且不是强制清理,则不执行清理操作
if self.is_auto_restore_disabled(): if self.is_auto_restore_disabled() and not force_clean:
logger.info("已禁用自动还原hosts跳过清理操作") logger.info("已禁用自动还原hosts跳过清理操作")
return True return True

View File

@@ -16,7 +16,7 @@ def censor_url(text):
return text # 直接返回原始文本,不做任何隐藏 return text # 直接返回原始文本,不做任何隐藏
# 以下是原始代码,现在被注释掉 # 以下是原始代码,现在被注释掉
''' r'''
# 匹配URL并替换为固定文本 # 匹配URL并替换为固定文本
url_pattern = re.compile(r'https?://[^\s/$.?#].[^\s]*') url_pattern = re.compile(r'https?://[^\s/$.?#].[^\s]*')
censored = url_pattern.sub('***URL protection***', text) censored = url_pattern.sub('***URL protection***', text)