diff --git a/source/core/__init__.py b/source/core/__init__.py index 0c0afa1..dfc8f81 100644 --- a/source/core/__init__.py +++ b/source/core/__init__.py @@ -10,6 +10,7 @@ from .privacy_manager import PrivacyManager from .cloudflare_optimizer import CloudflareOptimizer from .download_task_manager import DownloadTaskManager from .extraction_handler import ExtractionHandler +from .patch_detector import PatchDetector __all__ = [ 'MultiStageAnimations', @@ -23,5 +24,6 @@ __all__ = [ 'PrivacyManager', 'CloudflareOptimizer', 'DownloadTaskManager', - 'ExtractionHandler' + 'ExtractionHandler', + 'PatchDetector' ] \ No newline at end of file diff --git a/source/core/cloudflare_optimizer.py b/source/core/cloudflare_optimizer.py index 39b6323..93d7c88 100644 --- a/source/core/cloudflare_optimizer.py +++ b/source/core/cloudflare_optimizer.py @@ -75,35 +75,25 @@ class CloudflareOptimizer: # 解析域名 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: - # 如果本次会话中已执行过优选且hosts中存在记录,则跳过优选过程 - logger.info(f"发现hosts文件中已有域名 {hostname} 的优选IP记录且本次会话已优选过,跳过优选过程") + if self.has_optimized_in_session: + # 如果本次会话中已执行过优选,则跳过优选过程 + logger.info("本次会话已执行过优选,跳过优选过程") # 设置标记为已优选完成 self.optimization_done = 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 else: - # 如果本次会话尚未优选过,或hosts中没有记录,则显示优选窗口 - if existing_ips: - logger.info(f"发现hosts文件中已有域名 {hostname} 的优选IP记录,但本次会话尚未优选过") - # 清理已有的hosts记录,准备重新优选 - self.hosts_manager.clean_hostname_entries(hostname) + # 如果本次会话尚未优选过,则清理可能存在的旧记录 + if hostname: + # 检查hosts文件中是否已有该域名的IP记录 + existing_ips = self.hosts_manager.get_hostname_entries(hostname) + if existing_ips: + logger.info(f"发现hosts文件中已有域名 {hostname} 的优选IP记录,但本次会话尚未优选过") + # 清理已有的hosts记录,准备重新优选 + self.hosts_manager.clean_hostname_entries(hostname) # 创建取消状态标记 self.optimization_cancelled = False @@ -282,6 +272,9 @@ class CloudflareOptimizer: def _process_optimization_results(self): """处理优选的IP结果,显示相应提示""" + # 无论优选结果如何,都标记本次会话已执行过优选 + self.has_optimized_in_session = True + use_ipv6 = False if hasattr(self.main_window, 'config'): use_ipv6 = self.main_window.config.get("ipv6_enabled", False) @@ -376,9 +369,6 @@ class CloudflareOptimizer: from utils import save_config save_config(self.main_window.config) - # 记录本次会话已执行过优选 - self.has_optimized_in_session = True - if success: msg_box = QtWidgets.QMessageBox(self.main_window) msg_box.setWindowTitle(f"成功 - {self.main_window.APP_NAME}") diff --git a/source/core/download_manager.py b/source/core/download_manager.py index 0fdfe82..8ee957c 100644 --- a/source/core/download_manager.py +++ b/source/core/download_manager.py @@ -201,44 +201,37 @@ class DownloadManager: return safe_config def download_action(self): - """开始下载流程""" - self.main_window.download_queue_history = [] - - # 清除游戏检测器的目录缓存,确保获取最新的目录状态 - 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() - if self.is_debug_mode(): - logger.debug("DEBUG: 已清除游戏目录缓存,确保获取最新状态") - + """下载操作的主入口点""" + 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) - debug_mode = self.is_debug_mode() - if debug_mode: - logger.debug(f"DEBUG: 开始下载流程, 识别到 {len(game_dirs)} 个游戏目录") - if not game_dirs: - if debug_mode: - logger.warning("DEBUG: 未识别到任何游戏目录,设置目录未找到错误") - self.main_window.last_error_message = "directory_not_found" QtWidgets.QMessageBox.warning( - self.main_window, - f"目录错误 - {APP_NAME}", - "\n未能识别到任何游戏目录。\n\n请确认您选择的是游戏的上级目录,并且该目录中包含NEKOPARA系列游戏文件夹。\n" + 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.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() + # 创建并启动哈希线程进行预检查 self.main_window.hash_thread = self.main_window.create_hash_thread("pre", install_paths) self.main_window.hash_thread.pre_finished.connect( lambda updated_status: self.on_pre_hash_finished_with_dirs(updated_status, game_dirs) ) self.main_window.hash_thread.start() - + def on_pre_hash_finished_with_dirs(self, updated_status, game_dirs): """优化的哈希预检查完成处理,带有游戏目录信息 @@ -255,36 +248,8 @@ class DownloadManager: self.main_window.setEnabled(True) - installable_games = [] - already_installed_games = [] - 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) + # 使用patch_detector检测可安装的游戏 + already_installed_games, installable_games, disabled_patch_games = self.main_window.patch_detector.detect_installable_games(game_dirs) status_message = "" if already_installed_games: @@ -423,7 +388,11 @@ class DownloadManager: self._fill_download_queue(config, selected_game_dirs) 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 # 如果是离线模式,直接开始下一个下载任务 @@ -544,30 +513,18 @@ class DownloadManager: """显示Cloudflare加速选择对话框""" if self.download_queue: 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) msg_box = QtWidgets.QMessageBox(self.main_window) @@ -619,7 +576,11 @@ class DownloadManager: def next_download_task(self): """处理下载队列中的下一个任务""" 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 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) def on_download_finished(self, success, error, url, game_folder, game_version, _7z_path, plugin_path): - """下载完成后的处理 + """下载完成后的回调函数 Args: success: 是否下载成功 error: 错误信息 url: 下载URL game_folder: 游戏文件夹路径 - game_version: 游戏版本名称 + game_version: 游戏版本 _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: logger.error(f"--- Download Failed: {game_version} ---") logger.error(error) @@ -805,7 +763,6 @@ class DownloadManager: return 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): """解压完成后的回调,决定是否继续下载队列 diff --git a/source/core/offline_mode_manager.py b/source/core/offline_mode_manager.py index 39f9709..dfec152 100644 --- a/source/core/offline_mode_manager.py +++ b/source/core/offline_mode_manager.py @@ -26,6 +26,7 @@ class OfflineModeManager: 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 = [] # 跟踪本次实际安装的游戏 def _is_debug_mode(self): """检查是否处于调试模式 @@ -208,7 +209,7 @@ class OfflineModeManager: return False def verify_patch_hash(self, game_version, file_path): - """验证补丁文件的哈希值 + """验证补丁文件的哈希值,使用patch_detector模块 Args: game_version: 游戏版本名称 @@ -217,165 +218,9 @@ class OfflineModeManager: Returns: bool: 哈希值是否匹配 """ - # 获取预期的哈希值 - 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", "") + # 使用patch_detector模块验证哈希值 + return self.main_window.patch_detector.verify_patch_hash(game_version, file_path) - 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): """直接安装离线补丁,完全绕过下载模块 @@ -419,78 +264,29 @@ class OfflineModeManager: logger.warning("DEBUG: 未识别到任何游戏目录") return False - # 显示文件检验窗口 - self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window(check_type="pre", is_offline=True) + self.main_window.setEnabled(False) - # 获取安装路径 - 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.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}") + # 设置到主窗口,供结果显示使用 + self.main_window.download_queue_history = selected_games # 创建安装任务列表 install_tasks = [] - for game_version in installable_games: + 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} 的离线补丁文件,跳过") continue # 获取游戏目录 game_folder = game_dirs.get(game_version) if not game_folder: + if debug_mode: + logger.warning(f"DEBUG: 未找到 {game_version} 的游戏目录,跳过") continue # 获取目标路径 @@ -510,6 +306,8 @@ class OfflineModeManager: _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 # 添加到安装任务列表 @@ -517,10 +315,22 @@ class OfflineModeManager: # 开始执行第一个安装任务 if install_tasks: + if debug_mode: + logger.info(f"DEBUG: 开始离线安装流程,安装游戏数量: {len(install_tasks)}") self.process_next_offline_install_task(install_tasks) 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("开始安装") + return True + def process_next_offline_install_task(self, install_tasks): """处理下一个离线安装任务 @@ -533,7 +343,9 @@ class OfflineModeManager: # 所有任务完成,进行后检查 if debug_mode: logger.info("DEBUG: 所有离线安装任务完成,进行后检查") - self.main_window.after_hash_compare() + + # 使用patch_detector进行安装后哈希比较 + self.main_window.patch_detector.after_hash_compare() return # 获取下一个任务 @@ -555,22 +367,6 @@ class OfflineModeManager: logger.debug(f"DEBUG: 已复制补丁文件到缓存目录: {_7z_path}") 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) @@ -683,20 +479,31 @@ class OfflineModeManager: 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 on_offline_extraction_finished(self, remaining_tasks): - """离线模式下的解压完成处理(旧方法,保留兼容性) + def is_offline_mode_available(self): + """检查是否可以使用离线模式 - Args: - remaining_tasks: 剩余的安装任务列表 + Returns: + bool: 是否可以使用离线模式 """ - debug_mode = self._is_debug_mode() - - if debug_mode: - logger.debug("DEBUG: 离线解压完成,继续处理下一个任务") + # 在调试模式下始终允许离线模式 + if self._is_debug_mode(): + return True - # 处理下一个任务 - self.process_next_offline_install_task(remaining_tasks) \ No newline at end of file + # 检查是否有离线补丁文件 + return self.has_offline_patches() + + def is_in_offline_mode(self): + """检查当前是否处于离线模式 + + Returns: + bool: 是否处于离线模式 + """ + return self.is_offline_mode \ No newline at end of file diff --git a/source/core/patch_detector.py b/source/core/patch_detector.py new file mode 100644 index 0000000..20f19f8 --- /dev/null +++ b/source/core/patch_detector.py @@ -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) \ No newline at end of file diff --git a/source/core/patch_manager.py b/source/core/patch_manager.py index 692f87e..750c342 100644 --- a/source/core/patch_manager.py +++ b/source/core/patch_manager.py @@ -3,23 +3,36 @@ import shutil import traceback from PySide6.QtWidgets import QMessageBox from utils.logger import setup_logger +from data.config import APP_NAME +from utils import msgbox_frame 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: app_name: 应用程序名称,用于显示消息框标题 game_info: 游戏信息字典,包含各版本的安装路径和可执行文件名 debug_manager: 调试管理器实例,用于输出调试信息 + main_window: 主窗口实例,用于访问UI和状态 """ self.app_name = app_name self.game_info = game_info self.debug_manager = debug_manager + self.main_window = main_window # 添加main_window属性 self.installed_status = {} # 游戏版本的安装状态 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): """检查是否处于调试模式 @@ -331,7 +344,7 @@ class PatchManager: ) def check_patch_installed(self, game_dir, game_version): - """检查游戏是否已安装补丁 + """检查游戏是否已安装补丁(调用patch_detector) Args: game_dir: 游戏目录路径 @@ -340,6 +353,10 @@ class PatchManager: Returns: 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() if game_version not in self.game_info: @@ -425,7 +442,7 @@ class PatchManager: return False def check_patch_disabled(self, game_dir, game_version): - """检查游戏的补丁是否已被禁用 + """检查游戏的补丁是否已被禁用(调用patch_detector) Args: game_dir: 游戏目录路径 @@ -435,6 +452,10 @@ class PatchManager: bool: 如果补丁被禁用返回True,否则返回False 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() if game_version not in self.game_info: @@ -743,4 +764,86 @@ class PatchManager: f"批量操作完成 - {self.app_name}", result_text, 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 ) \ No newline at end of file diff --git a/source/core/ui_manager.py b/source/core/ui_manager.py index dd780de..009f515 100644 --- a/source/core/ui_manager.py +++ b/source/core/ui_manager.py @@ -720,7 +720,7 @@ class UIManager: log_datetime = "-".join(os.path.basename(latest_log)[4:-4].split("-")[:2]) log_date = log_datetime.split("-")[0] 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:]}" except: date_info = "日期未知 " @@ -780,8 +780,8 @@ class UIManager: """手动删除软件添加的hosts条目""" if hasattr(self.main_window, 'download_manager') and hasattr(self.main_window.download_manager, 'hosts_manager'): try: - # 调用清理hosts条目的方法 - result = self.main_window.download_manager.hosts_manager.check_and_clean_all_entries() + # 调用清理hosts条目的方法,强制清理即使禁用了自动还原 + result = self.main_window.download_manager.hosts_manager.check_and_clean_all_entries(force_clean=True) if result: msg_box = self._create_message_box("成功", "\n已成功清理软件添加的hosts条目。\n") diff --git a/source/main_window.py b/source/main_window.py index a5d7e7c..9026cb7 100644 --- a/source/main_window.py +++ b/source/main_window.py @@ -10,6 +10,7 @@ from PySide6.QtCore import QTimer, Qt, QPoint, QRect, QSize from PySide6.QtWidgets import QMainWindow, QMessageBox, QGraphicsOpacityEffect, QGraphicsColorizeEffect from PySide6.QtGui import QPalette, QColor, QPainterPath, QRegion, QFont 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 data.config import ( @@ -26,7 +27,7 @@ from workers import ( ) from core import ( MultiStageAnimations, UIManager, DownloadManager, DebugManager, - WindowManager, GameDetector, PatchManager, ConfigManager + WindowManager, GameDetector, PatchManager, ConfigManager, PatchDetector ) from core.ipv6_manager import IPv6Manager from handlers import PatchToggleHandler, UninstallHandler @@ -79,16 +80,22 @@ class MainWindow(QMainWindow): # 5. 初始化其他管理器 self.config_manager = ConfigManager(APP_NAME, CONFIG_URL, UA, 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 self.offline_mode_manager = OfflineModeManager(self) - # 7. 初始化下载管理器 - 放在最后,因为它可能依赖于其他管理器 + # 9. 初始化下载管理器 - 放在最后,因为它可能依赖于其他管理器 self.download_manager = DownloadManager(self) - # 8. 初始化功能处理程序 + # 10. 初始化功能处理程序 self.uninstall_handler = UninstallHandler(self) self.patch_toggle_handler = PatchToggleHandler(self) @@ -325,33 +332,40 @@ class MainWindow(QMainWindow): Args: url: 下载URL _7z_path: 7z文件保存路径 - game_version: 游戏版本名称 + game_version: 游戏版本 Returns: DownloadThread: 下载线程实例 """ - from workers import DownloadThread - return DownloadThread(url, _7z_path, game_version, parent=self) + return DownloadThread(url, _7z_path, game_version, self) def create_progress_window(self): - """创建下载进度窗口 + """创建进度窗口 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" - install_paths: 安装路径字典 - - Returns: - HashThread: 哈希检查线程实例 - """ - return HashThread(mode, install_paths, PLUGIN_HASH, self.installed_status, self) + # 添加进度条 + progress_bar = QProgressBar() + progress_bar.setRange(0, 100) + progress_bar.setValue(0) + layout.addWidget(progress_bar) + + # 添加标签 + 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): """创建解压线程 @@ -367,121 +381,10 @@ class MainWindow(QMainWindow): """ 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): - """显示安装结果,区分不同情况""" - # 获取当前安装状态 - installed_versions = [] # 成功安装的版本 - skipped_versions = [] # 已有补丁跳过的版本 - failed_versions = [] # 安装失败的版本 - not_found_versions = [] # 未找到的版本 + """显示安装结果,调用patch_manager的show_result方法""" + self.patch_manager.show_result() - # 获取所有游戏版本路径 - 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): """窗口关闭事件处理 @@ -651,64 +554,18 @@ class MainWindow(QMainWindow): self.ui.start_install_text.setText("开始安装") return - # 显示游戏选择对话框 - dialog = QtWidgets.QDialog(self) - dialog.setWindowTitle("选择要安装的游戏") - dialog.resize(400, 300) + # 显示文件检验窗口 + self.hash_msg_box = self.hash_manager.hash_pop_window(check_type="pre", is_offline=True) - layout = QtWidgets.QVBoxLayout(dialog) + # 获取安装路径 + install_paths = self.download_manager.get_install_paths() - # 添加"选择要安装的游戏"标签 - title_label = QtWidgets.QLabel("选择要安装的游戏", dialog) - title_label.setFont(QFont(title_label.font().family(), title_label.font().pointSize(), QFont.Bold)) - layout.addWidget(title_label) - - # 添加游戏列表控件 - 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("开始安装") + # 创建并启动哈希线程进行预检查 + self.hash_thread = self.patch_detector.create_hash_thread("pre", install_paths) + self.hash_thread.pre_finished.connect( + lambda updated_status: self.patch_detector.on_offline_pre_hash_finished(updated_status, game_dirs) + ) + self.hash_thread.start() else: # 在线模式下,检查版本是否过低 if hasattr(self, 'version_warning') and self.version_warning: @@ -723,6 +580,8 @@ class MainWindow(QMainWindow): # 版本正常,使用原有的下载流程 self.download_manager.file_dialog() + # 移除on_offline_pre_hash_finished方法 + def check_and_set_offline_mode(self): """检查是否有离线补丁文件,如果有则自动启用离线模式 diff --git a/source/utils/helpers.py b/source/utils/helpers.py index 430d042..ee14fcc 100644 --- a/source/utils/helpers.py +++ b/source/utils/helpers.py @@ -567,14 +567,17 @@ class HostsManager: self.auto_restore_disabled = 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记录 + Args: + force_clean: 是否强制清理,即使禁用了自动还原 + Returns: bool: 清理是否成功 """ - # 如果禁用了自动还原,则不执行清理操作 - if self.is_auto_restore_disabled(): + # 如果禁用了自动还原,且不是强制清理,则不执行清理操作 + if self.is_auto_restore_disabled() and not force_clean: logger.info("已禁用自动还原hosts,跳过清理操作") return True diff --git a/source/utils/url_censor.py b/source/utils/url_censor.py index 3116d22..70fa7b8 100644 --- a/source/utils/url_censor.py +++ b/source/utils/url_censor.py @@ -16,7 +16,7 @@ def censor_url(text): return text # 直接返回原始文本,不做任何隐藏 # 以下是原始代码,现在被注释掉 - ''' + r''' # 匹配URL并替换为固定文本 url_pattern = re.compile(r'https?://[^\s/$.?#].[^\s]*') censored = url_pattern.sub('***URL protection***', text)