"""Configuration workflow methods for InstallTTWScreen (Mixin).""" from pathlib import Path from PySide6.QtCore import QTimer, Qt, QThread, Signal from PySide6.QtWidgets import QMessageBox, QProgressDialog import logging import os import threading import traceback # Runtime imports to avoid circular dependencies from jackify.frontends.gui.services.message_service import MessageService # Runtime import logger = logging.getLogger(__name__) def debug_print(message): """Print debug message only if debug mode is enabled""" from jackify.backend.handlers.config_handler import ConfigHandler config_handler = ConfigHandler() if config_handler.get('debug_mode', False): print(message) class TTWConfigMixin: """Mixin providing configuration workflow methods for InstallTTWScreen.""" def _detect_game_type_from_mo2_ini(self, install_dir: str) -> str: """Detect game type by checking ModOrganizer.ini for loader executables.""" from pathlib import Path import logging logger = logging.getLogger(__name__) mo2_ini = Path(install_dir) / "ModOrganizer.ini" if not mo2_ini.exists(): return 'skyrim' # Fallback to most common try: content = mo2_ini.read_text(encoding='utf-8', errors='ignore').lower() if 'skse64_loader.exe' in content or 'skyrim special edition' in content: return 'skyrim' elif 'f4se_loader.exe' in content or 'fallout 4' in content: return 'fallout4' elif 'nvse_loader.exe' in content or 'fallout new vegas' in content: return 'falloutnv' elif 'obse_loader.exe' in content or 'oblivion' in content: return 'oblivion' elif 'starfield' in content: return 'starfield' elif 'enderal' in content: return 'enderal' else: return 'skyrim' except Exception as e: logger.warning(f"Error detecting game type from ModOrganizer.ini: {e}") return 'skyrim' def restart_steam_and_configure(self): """Restart Steam using backend service directly - DECOUPLED FROM CLI""" debug_print("DEBUG: restart_steam_and_configure called - using direct backend service") progress = QProgressDialog("Restarting Steam...", None, 0, 0, self) progress.setWindowTitle("Restarting Steam") progress.setWindowModality(Qt.WindowModal) progress.setMinimumDuration(0) progress.setValue(0) progress.show() def do_restart(): debug_print("DEBUG: do_restart thread started - using direct backend service") try: from jackify.backend.handlers.shortcut_handler import ShortcutHandler # Use backend service directly instead of CLI subprocess # Get system_info from parent screen system_info = getattr(self, 'system_info', None) is_steamdeck = system_info.is_steamdeck if system_info else False shortcut_handler = ShortcutHandler(steamdeck=is_steamdeck) debug_print("DEBUG: About to call secure_steam_restart()") success = shortcut_handler.secure_steam_restart() debug_print(f"DEBUG: secure_steam_restart() returned: {success}") out = "Steam restart completed successfully." if success else "Steam restart failed." except Exception as e: debug_print(f"DEBUG: Exception in do_restart: {e}") success = False out = str(e) self.steam_restart_finished.emit(success, out) threading.Thread(target=do_restart, daemon=True).start() self._steam_restart_progress = progress # Store to close later def _on_steam_restart_finished(self, success, out): debug_print("DEBUG: _on_steam_restart_finished called") # Safely cleanup progress dialog on main thread if hasattr(self, '_steam_restart_progress') and self._steam_restart_progress: try: self._steam_restart_progress.close() self._steam_restart_progress.deleteLater() # Use deleteLater() for safer cleanup except Exception as e: debug_print(f"DEBUG: Error closing progress dialog: {e}") finally: self._steam_restart_progress = None # Controls are managed by the proper control management system if success: self._safe_append_text("Steam restarted successfully.") # Save context for later use in configuration self._manual_steps_retry_count = 0 self._current_modlist_name = "TTW Installation" # Fixed name for TTW self._current_resolution = None # TTW doesn't need resolution changes # Use automated prefix creation instead of manual steps debug_print("DEBUG: Starting automated prefix creation workflow") self._safe_append_text("Starting automated prefix creation workflow...") self.start_automated_prefix_workflow() else: self._safe_append_text("Failed to restart Steam.\n" + out) MessageService.critical(self, "Steam Restart Failed", "Failed to restart Steam automatically. Please restart Steam manually, then try again.") def start_automated_prefix_workflow(self): # Ensure _current_resolution is always set before starting workflow if not hasattr(self, '_current_resolution') or self._current_resolution is None: resolution = None # TTW doesn't need resolution changes # Extract resolution properly (e.g., "1280x800" from "1280x800 (Steam Deck)") if resolution and resolution != "Leave unchanged": if " (" in resolution: self._current_resolution = resolution.split(" (")[0] else: self._current_resolution = resolution else: self._current_resolution = None """Start the automated prefix creation workflow""" try: # Disable controls during installation self._disable_controls_during_operation() modlist_name = "TTW Installation" install_dir = self.install_dir_edit.text().strip() final_exe_path = os.path.join(install_dir, "ModOrganizer.exe") if not os.path.exists(final_exe_path): # Check if this is Somnium specifically (uses files/ subdirectory) modlist_name_lower = modlist_name.lower() if "somnium" in modlist_name_lower: somnium_exe_path = os.path.join(install_dir, "files", "ModOrganizer.exe") if os.path.exists(somnium_exe_path): final_exe_path = somnium_exe_path self._safe_append_text(f"Detected Somnium modlist - will proceed with automated setup") # Show Somnium guidance popup after automated workflow completes self._show_somnium_guidance = True self._somnium_install_dir = install_dir else: self._safe_append_text(f"ERROR: Somnium ModOrganizer.exe not found at {somnium_exe_path}") MessageService.critical(self, "Somnium ModOrganizer.exe Not Found", f"Expected Somnium ModOrganizer.exe not found at:\n{somnium_exe_path}\n\nCannot proceed with automated setup.") return else: self._safe_append_text(f"ERROR: ModOrganizer.exe not found at {final_exe_path}") MessageService.critical(self, "ModOrganizer.exe Not Found", f"ModOrganizer.exe not found at:\n{final_exe_path}\n\nCannot proceed with automated setup.") return # Run automated prefix creation in separate thread from PySide6.QtCore import QThread, Signal class AutomatedPrefixThread(QThread): finished = Signal(bool, str, str, str) # success, prefix_path, appid (as string), last_timestamp progress = Signal(str) # progress messages error = Signal(str) # error messages show_progress_dialog = Signal(str) # show progress dialog with message hide_progress_dialog = Signal() # hide progress dialog conflict_detected = Signal(list) # conflicts list def __init__(self, modlist_name, install_dir, final_exe_path): super().__init__() self.modlist_name = modlist_name self.install_dir = install_dir self.final_exe_path = final_exe_path def run(self): try: from jackify.backend.services.automated_prefix_service import AutomatedPrefixService def progress_callback(message): self.progress.emit(message) # Show progress dialog during Steam restart if "Steam restarted successfully" in message: self.hide_progress_dialog.emit() elif "Restarting Steam..." in message: self.show_progress_dialog.emit("Restarting Steam...") prefix_service = AutomatedPrefixService() # Determine Steam Deck once and pass through the workflow try: import os _is_steamdeck = False if os.path.exists('/etc/os-release'): with open('/etc/os-release') as f: if 'steamdeck' in f.read().lower(): _is_steamdeck = True except Exception: _is_steamdeck = False result = prefix_service.run_working_workflow( self.modlist_name, self.install_dir, self.final_exe_path, progress_callback, steamdeck=_is_steamdeck ) # Handle the result - check for conflicts if isinstance(result, tuple) and len(result) == 4: if result[0] == "CONFLICT": # Conflict detected - emit signal to main GUI conflicts = result[1] self.hide_progress_dialog.emit() self.conflict_detected.emit(conflicts) return else: # Normal result with timestamp success, prefix_path, new_appid, last_timestamp = result elif isinstance(result, tuple) and len(result) == 3: # Fallback for old format (backward compatibility) if result[0] == "CONFLICT": # Conflict detected - emit signal to main GUI conflicts = result[1] self.hide_progress_dialog.emit() self.conflict_detected.emit(conflicts) return else: # Normal result (old format) success, prefix_path, new_appid = result last_timestamp = None else: # Handle non-tuple result success = result prefix_path = "" new_appid = "0" last_timestamp = None # Ensure progress dialog is hidden when workflow completes self.hide_progress_dialog.emit() self.finished.emit(success, prefix_path or "", str(new_appid) if new_appid else "0", last_timestamp) except Exception as e: # Ensure progress dialog is hidden on error self.hide_progress_dialog.emit() self.error.emit(str(e)) # Create and start thread self.prefix_thread = AutomatedPrefixThread(modlist_name, install_dir, final_exe_path) self.prefix_thread.finished.connect(self.on_automated_prefix_finished) self.prefix_thread.error.connect(self.on_automated_prefix_error) self.prefix_thread.progress.connect(self.on_automated_prefix_progress) self.prefix_thread.show_progress_dialog.connect(self.show_steam_restart_progress) self.prefix_thread.hide_progress_dialog.connect(self.hide_steam_restart_progress) self.prefix_thread.conflict_detected.connect(self.show_shortcut_conflict_dialog) self.prefix_thread.start() except Exception as e: debug_print(f"DEBUG: Exception in start_automated_prefix_workflow: {e}") import traceback debug_print(f"DEBUG: Traceback: {traceback.format_exc()}") self._safe_append_text(f"ERROR: Failed to start automated workflow: {e}") # Re-enable controls on exception self._enable_controls_after_operation() def on_automated_prefix_finished(self, success, prefix_path, new_appid_str, last_timestamp=None): """Handle completion of automated prefix creation""" try: if success: debug_print(f"SUCCESS: Automated prefix creation completed!") debug_print(f"Prefix created at: {prefix_path}") if new_appid_str and new_appid_str != "0": debug_print(f"AppID: {new_appid_str}") # Convert string AppID back to integer for configuration new_appid = int(new_appid_str) if new_appid_str and new_appid_str != "0" else None # Continue with configuration using the new AppID and timestamp modlist_name = "TTW Installation" install_dir = self.install_dir_edit.text().strip() self.continue_configuration_after_automated_prefix(new_appid, modlist_name, install_dir, last_timestamp) else: self._safe_append_text(f"ERROR: Automated prefix creation failed") self._safe_append_text("Please check the logs for details") MessageService.critical(self, "Automated Setup Failed", "Automated prefix creation failed. Please check the console output for details.") # Re-enable controls on failure self._enable_controls_after_operation() finally: # Always ensure controls are re-enabled when workflow truly completes pass def on_automated_prefix_error(self, error_msg): """Handle error in automated prefix creation""" self._safe_append_text(f"ERROR: Error during automated prefix creation: {error_msg}") MessageService.critical(self, "Automated Setup Error", f"Error during automated prefix creation: {error_msg}") # Re-enable controls on error self._enable_controls_after_operation() def on_automated_prefix_progress(self, progress_msg): """Handle progress updates from automated prefix creation""" self._safe_append_text(progress_msg) def on_configuration_progress(self, progress_msg): """Handle progress updates from modlist configuration""" self._safe_append_text(progress_msg) def show_steam_restart_progress(self, message): """Show Steam restart progress dialog""" from PySide6.QtWidgets import QProgressDialog from PySide6.QtCore import Qt self.steam_restart_progress = QProgressDialog(message, None, 0, 0, self) self.steam_restart_progress.setWindowTitle("Restarting Steam") self.steam_restart_progress.setWindowModality(Qt.WindowModal) self.steam_restart_progress.setMinimumDuration(0) self.steam_restart_progress.setValue(0) self.steam_restart_progress.show() def hide_steam_restart_progress(self): """Hide Steam restart progress dialog""" if hasattr(self, 'steam_restart_progress') and self.steam_restart_progress: try: self.steam_restart_progress.close() self.steam_restart_progress.deleteLater() except Exception: pass finally: self.steam_restart_progress = None # Controls are managed by the proper control management system def on_configuration_complete(self, success, message, modlist_name, enb_detected=False): """Handle configuration completion on main thread""" try: # Re-enable controls now that installation/configuration is complete self._enable_controls_after_operation() if success: # Check if we need to show Somnium guidance if self._show_somnium_guidance: self._show_somnium_post_install_guidance() # Show celebration SuccessDialog after the entire workflow from ..dialogs import SuccessDialog import time if not hasattr(self, '_install_workflow_start_time'): self._install_workflow_start_time = time.time() time_taken = int(time.time() - self._install_workflow_start_time) mins, secs = divmod(time_taken, 60) time_str = f"{mins} minutes, {secs} seconds" if mins else f"{secs} seconds" display_names = { 'skyrim': 'Skyrim', 'fallout4': 'Fallout 4', 'falloutnv': 'Fallout New Vegas', 'oblivion': 'Oblivion', 'starfield': 'Starfield', 'oblivion_remastered': 'Oblivion Remastered', 'enderal': 'Enderal' } game_name = display_names.get(self._current_game_type, self._current_game_name) success_dialog = SuccessDialog( modlist_name=modlist_name, workflow_type="install", time_taken=time_str, game_name=game_name, parent=self ) success_dialog.show() # TTW workflow does NOT need ENB detection/dialog elif hasattr(self, '_manual_steps_retry_count') and self._manual_steps_retry_count >= 3: # Max retries reached - show failure message MessageService.critical(self, "Manual Steps Failed", "Manual steps validation failed after multiple attempts.") else: # Configuration failed for other reasons MessageService.critical(self, "Configuration Failed", "Post-install configuration failed. Please check the console output.") except Exception as e: # Ensure controls are re-enabled even on unexpected errors self._enable_controls_after_operation() raise # Clean up thread if hasattr(self, 'config_thread') and self.config_thread is not None: # Disconnect all signals to prevent "Internal C++ object already deleted" errors try: self.config_thread.progress_update.disconnect() self.config_thread.configuration_complete.disconnect() self.config_thread.error_occurred.disconnect() except: pass # Ignore errors if already disconnected if self.config_thread.isRunning(): self.config_thread.quit() self.config_thread.wait(5000) # Wait up to 5 seconds self.config_thread.deleteLater() self.config_thread = None def on_configuration_error(self, error_message): """Handle configuration error on main thread""" self._safe_append_text(f"Configuration failed with error: {error_message}") MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}") # Re-enable all controls on error self._enable_controls_after_operation() # Clean up thread if hasattr(self, 'config_thread') and self.config_thread is not None: # Disconnect all signals to prevent "Internal C++ object already deleted" errors try: self.config_thread.progress_update.disconnect() self.config_thread.configuration_complete.disconnect() self.config_thread.error_occurred.disconnect() except: pass # Ignore errors if already disconnected if self.config_thread.isRunning(): self.config_thread.quit() self.config_thread.wait(5000) # Wait up to 5 seconds self.config_thread.deleteLater() self.config_thread = None def continue_configuration_after_automated_prefix(self, new_appid, modlist_name, install_dir, last_timestamp=None): """Continue the configuration process with the new AppID after automated prefix creation""" # Headers are now shown at start of Steam Integration # No need to show them again here debug_print("Configuration phase continues after Steam Integration") debug_print(f"continue_configuration_after_automated_prefix called with appid: {new_appid}") try: # Update the context with the new AppID (same format as manual steps) updated_context = { 'name': modlist_name, 'path': install_dir, 'mo2_exe_path': self._get_mo2_path(install_dir, modlist_name), 'modlist_value': None, 'modlist_source': None, 'resolution': getattr(self, '_current_resolution', None), 'skip_confirmation': True, 'manual_steps_completed': True, # Mark as completed since automated prefix is done 'appid': new_appid, # Use the NEW AppID from automated prefix creation 'game_name': self.context.get('game_name', 'Skyrim Special Edition') if hasattr(self, 'context') else 'Skyrim Special Edition' } self.context = updated_context # Ensure context is always set debug_print(f"Updated context with new AppID: {new_appid}") # Get Steam Deck detection once and pass to ConfigThread from jackify.backend.services.platform_detection_service import PlatformDetectionService platform_service = PlatformDetectionService.get_instance() is_steamdeck = platform_service.is_steamdeck # Create new config thread with updated context class ConfigThread(QThread): progress_update = Signal(str) configuration_complete = Signal(bool, str, str) error_occurred = Signal(str) def __init__(self, context, is_steamdeck): super().__init__() self.context = context self.is_steamdeck = is_steamdeck def run(self): try: from jackify.backend.services.modlist_service import ModlistService from jackify.backend.models.configuration import SystemInfo from jackify.backend.models.modlist import ModlistContext from pathlib import Path # Initialize backend service with passed Steam Deck detection system_info = SystemInfo(is_steamdeck=self.is_steamdeck) modlist_service = ModlistService(system_info) # Detect game type from ModOrganizer.ini detected_game_type = self._detect_game_type_from_mo2_ini(self.context['path']) # Convert context to ModlistContext for service modlist_context = ModlistContext( name=self.context['name'], install_dir=Path(self.context['path']), download_dir=Path(self.context['path']).parent / 'Downloads', # Default game_type=detected_game_type, nexus_api_key='', # Not needed for configuration modlist_value=self.context.get('modlist_value'), modlist_source=self.context.get('modlist_source', 'identifier'), resolution=self.context.get('resolution'), skip_confirmation=True, engine_installed=True # Skip path manipulation for engine workflows ) # Add app_id to context modlist_context.app_id = self.context['appid'] # Define callbacks def progress_callback(message): self.progress_update.emit(message) def completion_callback(success, message, modlist_name, enb_detected=False): self.configuration_complete.emit(success, message, modlist_name, enb_detected) def manual_steps_callback(modlist_name, retry_count): # This shouldn't happen since automated prefix creation is complete self.progress_update.emit(f"Unexpected manual steps callback for {modlist_name}") # Call the service method for post-Steam configuration result = modlist_service.configure_modlist_post_steam( context=modlist_context, progress_callback=progress_callback, manual_steps_callback=manual_steps_callback, completion_callback=completion_callback ) if not result: self.progress_update.emit("Configuration failed to start") self.error_occurred.emit("Configuration failed to start") except Exception as e: self.error_occurred.emit(str(e)) # Start configuration thread self.config_thread = ConfigThread(updated_context, is_steamdeck) self.config_thread.progress_update.connect(self.on_configuration_progress) self.config_thread.configuration_complete.connect(self.on_configuration_complete) self.config_thread.error_occurred.connect(self.on_configuration_error) self.config_thread.start() except Exception as e: self._safe_append_text(f"Error continuing configuration: {e}") import traceback self._safe_append_text(f"Full traceback: {traceback.format_exc()}") self.on_configuration_error(str(e)) def continue_configuration_after_manual_steps(self, new_appid, modlist_name, install_dir): """Continue the configuration process with the corrected AppID after manual steps validation""" try: # Update the context with the new AppID updated_context = { 'name': modlist_name, 'path': install_dir, 'mo2_exe_path': self._get_mo2_path(install_dir, modlist_name), 'modlist_value': None, 'modlist_source': None, 'resolution': getattr(self, '_current_resolution', None), 'skip_confirmation': True, 'manual_steps_completed': True, # Mark as completed 'appid': new_appid # Use the NEW AppID from Steam } debug_print(f"Updated context with new AppID: {new_appid}") # Clean up old thread if exists and wait for it to finish if hasattr(self, 'config_thread') and self.config_thread is not None: # Disconnect all signals to prevent "Internal C++ object already deleted" errors try: self.config_thread.progress_update.disconnect() self.config_thread.configuration_complete.disconnect() self.config_thread.error_occurred.disconnect() except: pass # Ignore errors if already disconnected if self.config_thread.isRunning(): self.config_thread.quit() self.config_thread.wait(5000) # Wait up to 5 seconds self.config_thread.deleteLater() self.config_thread = None # Start new config thread self.config_thread = self._create_config_thread(updated_context) self.config_thread.progress_update.connect(self.on_configuration_progress) self.config_thread.configuration_complete.connect(self.on_configuration_complete) self.config_thread.error_occurred.connect(self.on_configuration_error) self.config_thread.start() except Exception as e: self._safe_append_text(f"Error continuing configuration: {e}") self.on_configuration_error(str(e)) def _create_config_thread(self, context): """Create a new ConfigThread with proper lifecycle management""" from PySide6.QtCore import QThread, Signal # Get Steam Deck detection once from jackify.backend.services.platform_detection_service import PlatformDetectionService platform_service = PlatformDetectionService.get_instance() is_steamdeck = platform_service.is_steamdeck class ConfigThread(QThread): progress_update = Signal(str) configuration_complete = Signal(bool, str, str) error_occurred = Signal(str) def __init__(self, context, is_steamdeck, parent=None): super().__init__(parent) self.context = context self.is_steamdeck = is_steamdeck def run(self): try: from jackify.backend.models.configuration import SystemInfo from jackify.backend.services.modlist_service import ModlistService from jackify.backend.models.modlist import ModlistContext from pathlib import Path # Initialize backend service with passed Steam Deck detection system_info = SystemInfo(is_steamdeck=self.is_steamdeck) modlist_service = ModlistService(system_info) # Detect game type from ModOrganizer.ini detected_game_type = self._detect_game_type_from_mo2_ini(self.context['path']) # Convert context to ModlistContext for service modlist_context = ModlistContext( name=self.context['name'], install_dir=Path(self.context['path']), download_dir=Path(self.context['path']).parent / 'Downloads', # Default game_type=detected_game_type, nexus_api_key='', # Not needed for configuration modlist_value=self.context.get('modlist_value', ''), modlist_source=self.context.get('modlist_source', 'identifier'), resolution=self.context.get('resolution'), # Pass resolution from GUI skip_confirmation=True, engine_installed=True # Skip path manipulation for engine workflows ) # Add app_id to context if 'appid' in self.context: modlist_context.app_id = self.context['appid'] # Define callbacks def progress_callback(message): self.progress_update.emit(message) def completion_callback(success, message, modlist_name): self.configuration_complete.emit(success, message, modlist_name) def manual_steps_callback(modlist_name, retry_count): # Should not reach here -- manual steps already complete self.progress_update.emit(f"Unexpected manual steps callback for {modlist_name}") # Call the new service method for post-Steam configuration result = modlist_service.configure_modlist_post_steam( context=modlist_context, progress_callback=progress_callback, manual_steps_callback=manual_steps_callback, completion_callback=completion_callback ) if not result: self.progress_update.emit("WARNING: configure_modlist_post_steam returned False") except Exception as e: import traceback error_details = f"Error in configuration: {e}\nTraceback: {traceback.format_exc()}" self.progress_update.emit(f"DEBUG: {error_details}") self.error_occurred.emit(str(e)) return ConfigThread(context, is_steamdeck, parent=self)