mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 11:37:01 +01:00
689 lines
32 KiB
Python
689 lines
32 KiB
Python
"""
|
|
Modlist Service
|
|
|
|
High-level service for modlist operations, orchestrating various handlers.
|
|
"""
|
|
|
|
import logging
|
|
from typing import List, Optional, Dict, Any
|
|
from pathlib import Path
|
|
|
|
from ..models.modlist import ModlistContext, ModlistInfo
|
|
from ..models.configuration import SystemInfo
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ModlistService:
|
|
"""Service for managing modlist operations."""
|
|
|
|
def __init__(self, system_info: SystemInfo):
|
|
"""Initialize the modlist service.
|
|
|
|
Args:
|
|
system_info: System information context
|
|
"""
|
|
self.system_info = system_info
|
|
|
|
# Handlers will be initialized when needed
|
|
self._modlist_handler = None
|
|
self._wabbajack_handler = None
|
|
self._filesystem_handler = None
|
|
|
|
def _get_modlist_handler(self):
|
|
"""Lazy initialization of modlist handler."""
|
|
if self._modlist_handler is None:
|
|
from ..handlers.modlist_handler import ModlistHandler
|
|
from ..services.platform_detection_service import PlatformDetectionService
|
|
# Initialize with proper dependencies and centralized Steam Deck detection
|
|
platform_service = PlatformDetectionService.get_instance()
|
|
self._modlist_handler = ModlistHandler(steamdeck=platform_service.is_steamdeck)
|
|
return self._modlist_handler
|
|
|
|
def _get_wabbajack_handler(self):
|
|
"""Lazy initialization of wabbajack handler."""
|
|
if self._wabbajack_handler is None:
|
|
from ..handlers.wabbajack_handler import InstallWabbajackHandler
|
|
# Initialize with proper dependencies
|
|
self._wabbajack_handler = InstallWabbajackHandler()
|
|
return self._wabbajack_handler
|
|
|
|
def _get_filesystem_handler(self):
|
|
"""Lazy initialization of filesystem handler."""
|
|
if self._filesystem_handler is None:
|
|
from ..handlers.filesystem_handler import FileSystemHandler
|
|
self._filesystem_handler = FileSystemHandler()
|
|
return self._filesystem_handler
|
|
|
|
def list_modlists(self, game_type: Optional[str] = None) -> List[ModlistInfo]:
|
|
"""List available modlists.
|
|
|
|
Args:
|
|
game_type: Optional filter by game type
|
|
|
|
Returns:
|
|
List of available modlists
|
|
"""
|
|
logger.info(f"Listing modlists for game_type: {game_type}")
|
|
|
|
try:
|
|
# Use the working ModlistInstallCLI to get modlists from engine
|
|
from ..core.modlist_operations import ModlistInstallCLI
|
|
|
|
# Use new SystemInfo pattern
|
|
modlist_cli = ModlistInstallCLI(self.system_info)
|
|
|
|
# Get all modlists and do client-side filtering for better control
|
|
raw_modlists = modlist_cli.get_all_modlists_from_engine(game_type=None)
|
|
|
|
# Apply client-side filtering based on game_type
|
|
if game_type:
|
|
game_type_lower = game_type.lower()
|
|
|
|
if game_type_lower == 'skyrim':
|
|
# Include both "Skyrim" and "Skyrim Special Edition" and "Skyrim VR"
|
|
raw_modlists = [m for m in raw_modlists if 'skyrim' in m.get('game', '').lower()]
|
|
|
|
elif game_type_lower == 'fallout4':
|
|
raw_modlists = [m for m in raw_modlists if 'fallout 4' in m.get('game', '').lower()]
|
|
|
|
elif game_type_lower == 'falloutnv':
|
|
raw_modlists = [m for m in raw_modlists if 'fallout new vegas' in m.get('game', '').lower()]
|
|
|
|
elif game_type_lower == 'oblivion':
|
|
raw_modlists = [m for m in raw_modlists if 'oblivion' in m.get('game', '').lower() and 'remastered' not in m.get('game', '').lower()]
|
|
|
|
elif game_type_lower == 'starfield':
|
|
raw_modlists = [m for m in raw_modlists if 'starfield' in m.get('game', '').lower()]
|
|
|
|
elif game_type_lower == 'oblivion_remastered':
|
|
raw_modlists = [m for m in raw_modlists if 'oblivion remastered' in m.get('game', '').lower()]
|
|
|
|
elif game_type_lower == 'enderal':
|
|
raw_modlists = [m for m in raw_modlists if 'enderal' in m.get('game', '').lower()]
|
|
|
|
elif game_type_lower == 'other':
|
|
# Exclude all main category games to show only "Other" games
|
|
main_category_keywords = ['skyrim', 'fallout 4', 'fallout new vegas', 'oblivion', 'starfield', 'enderal']
|
|
def is_main_category(game_name):
|
|
game_lower = game_name.lower()
|
|
return any(keyword in game_lower for keyword in main_category_keywords)
|
|
|
|
raw_modlists = [m for m in raw_modlists if not is_main_category(m.get('game', ''))]
|
|
|
|
# Convert to ModlistInfo objects with enhanced metadata
|
|
modlists = []
|
|
for m_info in raw_modlists:
|
|
modlist_info = ModlistInfo(
|
|
id=m_info.get('id', ''),
|
|
name=m_info.get('name', m_info.get('id', '')), # Use name from enhanced data
|
|
game=m_info.get('game', ''),
|
|
description='', # Engine doesn't provide description yet
|
|
version='', # Engine doesn't provide version yet
|
|
size=f"{m_info.get('download_size', '')}|{m_info.get('install_size', '')}|{m_info.get('total_size', '')}" # Store all three sizes
|
|
)
|
|
|
|
# Add enhanced metadata as additional properties
|
|
if hasattr(modlist_info, '__dict__'):
|
|
modlist_info.__dict__.update({
|
|
'download_size': m_info.get('download_size', ''),
|
|
'install_size': m_info.get('install_size', ''),
|
|
'total_size': m_info.get('total_size', ''),
|
|
'machine_url': m_info.get('machine_url', ''), # Store machine URL for installation
|
|
'status_down': m_info.get('status_down', False),
|
|
'status_nsfw': m_info.get('status_nsfw', False)
|
|
})
|
|
|
|
# No client-side filtering needed - engine handles it
|
|
modlists.append(modlist_info)
|
|
|
|
logger.info(f"Found {len(modlists)} modlists")
|
|
return modlists
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to list modlists: {e}")
|
|
raise
|
|
|
|
def install_modlist(self, context: ModlistContext,
|
|
progress_callback=None,
|
|
output_callback=None) -> bool:
|
|
"""Install a modlist (ONLY installation, no configuration).
|
|
|
|
This method only runs the engine installation phase.
|
|
Configuration must be called separately after Steam setup.
|
|
|
|
Args:
|
|
context: Modlist installation context
|
|
progress_callback: Optional callback for progress updates
|
|
output_callback: Optional callback for output/logging
|
|
|
|
Returns:
|
|
True if installation successful, False otherwise
|
|
"""
|
|
logger.info(f"Installing modlist (INSTALLATION ONLY): {context.name}")
|
|
|
|
try:
|
|
# Validate context
|
|
if not self._validate_install_context(context):
|
|
logger.error("Invalid installation context")
|
|
return False
|
|
|
|
# Prepare directories
|
|
fs_handler = self._get_filesystem_handler()
|
|
fs_handler.ensure_directory(context.install_dir)
|
|
fs_handler.ensure_directory(context.download_dir)
|
|
|
|
# Use the working ModlistInstallCLI for discovery phase only
|
|
from ..core.modlist_operations import ModlistInstallCLI
|
|
|
|
# Use new SystemInfo pattern
|
|
modlist_cli = ModlistInstallCLI(self.system_info)
|
|
|
|
# Build context for ModlistInstallCLI
|
|
install_context = {
|
|
'modlist_name': context.name,
|
|
'install_dir': context.install_dir,
|
|
'download_dir': context.download_dir,
|
|
'nexus_api_key': context.nexus_api_key,
|
|
'game_type': context.game_type,
|
|
'modlist_value': context.modlist_value,
|
|
'resolution': getattr(context, 'resolution', None),
|
|
'skip_confirmation': True # Service layer should be non-interactive
|
|
}
|
|
|
|
# Set GUI mode for non-interactive operation
|
|
import os
|
|
original_gui_mode = os.environ.get('JACKIFY_GUI_MODE')
|
|
os.environ['JACKIFY_GUI_MODE'] = '1'
|
|
|
|
try:
|
|
# Run discovery phase with pre-filled context
|
|
confirmed_context = modlist_cli.run_discovery_phase(context_override=install_context)
|
|
if not confirmed_context:
|
|
logger.error("Discovery phase failed or was cancelled")
|
|
return False
|
|
|
|
# Now run ONLY the installation part (NOT configuration)
|
|
success = self._run_installation_only(
|
|
confirmed_context,
|
|
progress_callback=progress_callback,
|
|
output_callback=output_callback
|
|
)
|
|
|
|
if success:
|
|
logger.info("Modlist installation completed successfully (configuration will be done separately)")
|
|
return True
|
|
else:
|
|
logger.error("Modlist installation failed")
|
|
return False
|
|
|
|
finally:
|
|
# Restore original GUI mode
|
|
if original_gui_mode is not None:
|
|
os.environ['JACKIFY_GUI_MODE'] = original_gui_mode
|
|
else:
|
|
os.environ.pop('JACKIFY_GUI_MODE', None)
|
|
|
|
except Exception as e:
|
|
error_message = str(e)
|
|
logger.error(f"Failed to install modlist {context.name}: {error_message}")
|
|
|
|
# Check for file descriptor limit issues and attempt to handle them
|
|
from .resource_manager import handle_file_descriptor_error
|
|
try:
|
|
if any(indicator in error_message.lower() for indicator in ['too many open files', 'emfile', 'resource temporarily unavailable']):
|
|
result = handle_file_descriptor_error(error_message, "modlist installation")
|
|
if result['auto_fix_success']:
|
|
logger.info(f"File descriptor limit increased automatically. {result['recommendation']}")
|
|
elif result['error_detected']:
|
|
logger.warning(f"File descriptor limit issue detected but automatic fix failed. {result['recommendation']}")
|
|
if result['manual_instructions']:
|
|
distro = result['manual_instructions']['distribution']
|
|
logger.info(f"Manual ulimit increase instructions available for {distro} distribution")
|
|
except Exception as resource_error:
|
|
logger.debug(f"Error checking for resource limit issues: {resource_error}")
|
|
|
|
return False
|
|
|
|
def _run_installation_only(self, context, progress_callback=None, output_callback=None) -> bool:
|
|
"""Run only the installation phase using the engine (COPIED FROM WORKING CODE)."""
|
|
import subprocess
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from ..core.modlist_operations import get_jackify_engine_path
|
|
|
|
try:
|
|
# COPIED EXACTLY from working Archive_Do_Not_Write/modules/modlist_install_cli.py
|
|
|
|
# Process paths (copied from working code)
|
|
install_dir_context = context['install_dir']
|
|
if isinstance(install_dir_context, tuple):
|
|
actual_install_path = Path(install_dir_context[0])
|
|
if install_dir_context[1]:
|
|
actual_install_path.mkdir(parents=True, exist_ok=True)
|
|
else:
|
|
actual_install_path = Path(install_dir_context)
|
|
install_dir_str = str(actual_install_path)
|
|
|
|
download_dir_context = context['download_dir']
|
|
if isinstance(download_dir_context, tuple):
|
|
actual_download_path = Path(download_dir_context[0])
|
|
if download_dir_context[1]:
|
|
actual_download_path.mkdir(parents=True, exist_ok=True)
|
|
else:
|
|
actual_download_path = Path(download_dir_context)
|
|
download_dir_str = str(actual_download_path)
|
|
|
|
api_key = context['nexus_api_key']
|
|
|
|
# Path to the engine binary (copied from working code)
|
|
engine_path = get_jackify_engine_path()
|
|
engine_dir = os.path.dirname(engine_path)
|
|
if not os.path.isfile(engine_path) or not os.access(engine_path, os.X_OK):
|
|
if output_callback:
|
|
output_callback(f"Jackify Install Engine not found or not executable at: {engine_path}")
|
|
return False
|
|
|
|
# Build command (copied from working code)
|
|
cmd = [engine_path, 'install']
|
|
modlist_value = context.get('modlist_value')
|
|
if modlist_value and modlist_value.endswith('.wabbajack') and os.path.isfile(modlist_value):
|
|
cmd += ['-w', modlist_value]
|
|
elif modlist_value:
|
|
cmd += ['-m', modlist_value]
|
|
elif context.get('machineid'):
|
|
cmd += ['-m', context['machineid']]
|
|
cmd += ['-o', install_dir_str, '-d', download_dir_str]
|
|
|
|
# NOTE: API key is passed via environment variable only, not as command line argument
|
|
|
|
# Store original environment values (copied from working code)
|
|
original_env_values = {
|
|
'NEXUS_API_KEY': os.environ.get('NEXUS_API_KEY'),
|
|
'DOTNET_SYSTEM_GLOBALIZATION_INVARIANT': os.environ.get('DOTNET_SYSTEM_GLOBALIZATION_INVARIANT')
|
|
}
|
|
|
|
try:
|
|
# Environment setup (copied from working code)
|
|
if api_key:
|
|
os.environ['NEXUS_API_KEY'] = api_key
|
|
elif 'NEXUS_API_KEY' in os.environ:
|
|
del os.environ['NEXUS_API_KEY']
|
|
|
|
os.environ['DOTNET_SYSTEM_GLOBALIZATION_INVARIANT'] = "1"
|
|
|
|
pretty_cmd = ' '.join([f'"{arg}"' if ' ' in arg else arg for arg in cmd])
|
|
if output_callback:
|
|
output_callback(f"Launching Jackify Install Engine with command: {pretty_cmd}")
|
|
|
|
# Temporarily increase file descriptor limit for engine process
|
|
from jackify.backend.handlers.subprocess_utils import increase_file_descriptor_limit
|
|
success, old_limit, new_limit, message = increase_file_descriptor_limit()
|
|
if output_callback:
|
|
if success:
|
|
output_callback(f"File descriptor limit: {message}")
|
|
else:
|
|
output_callback(f"File descriptor limit warning: {message}")
|
|
|
|
# Subprocess call (copied from working code)
|
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=False, env=None, cwd=engine_dir)
|
|
|
|
# Output processing (copied from working code)
|
|
buffer = b''
|
|
while True:
|
|
chunk = proc.stdout.read(1)
|
|
if not chunk:
|
|
break
|
|
buffer += chunk
|
|
|
|
if chunk == b'\n':
|
|
line = buffer.decode('utf-8', errors='replace')
|
|
if output_callback:
|
|
output_callback(line.rstrip())
|
|
buffer = b''
|
|
elif chunk == b'\r':
|
|
line = buffer.decode('utf-8', errors='replace')
|
|
if output_callback:
|
|
output_callback(line.rstrip())
|
|
buffer = b''
|
|
|
|
if buffer:
|
|
line = buffer.decode('utf-8', errors='replace')
|
|
if output_callback:
|
|
output_callback(line.rstrip())
|
|
|
|
proc.wait()
|
|
if proc.returncode != 0:
|
|
if output_callback:
|
|
output_callback(f"Jackify Install Engine exited with code {proc.returncode}.")
|
|
return False
|
|
else:
|
|
if output_callback:
|
|
output_callback("Installation completed successfully")
|
|
return True
|
|
|
|
finally:
|
|
# Restore environment (copied from working code)
|
|
for key, original_value in original_env_values.items():
|
|
if original_value is not None:
|
|
os.environ[key] = original_value
|
|
else:
|
|
if key in os.environ:
|
|
del os.environ[key]
|
|
|
|
except Exception as e:
|
|
error_msg = f"Error running Jackify Install Engine: {e}"
|
|
logger.error(error_msg)
|
|
if output_callback:
|
|
output_callback(error_msg)
|
|
return False
|
|
|
|
def configure_modlist_post_steam(self, context: ModlistContext,
|
|
progress_callback=None,
|
|
manual_steps_callback=None,
|
|
completion_callback=None) -> bool:
|
|
"""Configure a modlist after Steam setup is complete.
|
|
|
|
This method should only be called AFTER:
|
|
1. Modlist installation is complete
|
|
2. Steam shortcut has been created
|
|
3. Steam has been restarted
|
|
4. Manual Proton steps have been completed
|
|
|
|
Args:
|
|
context: Modlist context with updated app_id
|
|
progress_callback: Optional callback for progress updates
|
|
manual_steps_callback: Called when manual steps needed
|
|
completion_callback: Called when configuration is complete
|
|
|
|
Returns:
|
|
True if configuration successful, False otherwise
|
|
"""
|
|
logger.info(f"Configuring modlist after Steam setup: {context.name}")
|
|
|
|
# Check if debug mode is enabled and create debug callback
|
|
from ..handlers.config_handler import ConfigHandler
|
|
config_handler = ConfigHandler()
|
|
debug_mode = config_handler.get('debug_mode', False)
|
|
|
|
def debug_callback(message):
|
|
"""Send debug message to GUI if debug mode is enabled"""
|
|
if debug_mode and progress_callback:
|
|
progress_callback(f"[DEBUG] {message}")
|
|
|
|
debug_callback(f"Starting configuration for {context.name}")
|
|
debug_callback(f"Debug mode enabled: {debug_mode}")
|
|
debug_callback(f"Install directory: {context.install_dir}")
|
|
debug_callback(f"Resolution: {getattr(context, 'resolution', 'Not set')}")
|
|
debug_callback(f"App ID: {getattr(context, 'app_id', 'Not set')}")
|
|
|
|
# Set up a custom logging handler to capture backend DEBUG messages
|
|
gui_log_handler = None
|
|
if debug_mode and progress_callback:
|
|
import logging
|
|
|
|
class GuiLogHandler(logging.Handler):
|
|
def __init__(self, callback):
|
|
super().__init__()
|
|
self.callback = callback
|
|
self.setLevel(logging.DEBUG)
|
|
|
|
def emit(self, record):
|
|
try:
|
|
msg = self.format(record)
|
|
if record.levelno == logging.DEBUG:
|
|
self.callback(f"[DEBUG] {msg}")
|
|
elif record.levelno >= logging.WARNING:
|
|
self.callback(f"[{record.levelname}] {msg}")
|
|
except Exception:
|
|
pass
|
|
|
|
gui_log_handler = GuiLogHandler(progress_callback)
|
|
gui_log_handler.setFormatter(logging.Formatter('%(message)s'))
|
|
|
|
# Add the GUI handler to key backend loggers
|
|
backend_logger_names = [
|
|
'jackify.backend.handlers.menu_handler',
|
|
'jackify.backend.handlers.modlist_handler',
|
|
'jackify.backend.handlers.install_wabbajack_handler',
|
|
'jackify.backend.handlers.wabbajack_handler',
|
|
'jackify.backend.handlers.shortcut_handler',
|
|
'jackify.backend.handlers.protontricks_handler',
|
|
'jackify.backend.handlers.validation_handler',
|
|
'jackify.backend.handlers.resolution_handler'
|
|
]
|
|
|
|
for logger_name in backend_logger_names:
|
|
backend_logger = logging.getLogger(logger_name)
|
|
backend_logger.addHandler(gui_log_handler)
|
|
backend_logger.setLevel(logging.DEBUG)
|
|
|
|
debug_callback("GUI logging handler installed for backend services")
|
|
|
|
try:
|
|
# COPY THE WORKING LOGIC: Use menu handler for configuration only
|
|
from ..handlers.menu_handler import ModlistMenuHandler
|
|
|
|
# Initialize handlers (same as working code)
|
|
modlist_menu = ModlistMenuHandler(config_handler)
|
|
|
|
# Build configuration context (copied from working code)
|
|
config_context = {
|
|
'name': context.name,
|
|
'path': str(context.install_dir),
|
|
'mo2_exe_path': str(context.install_dir / 'ModOrganizer.exe'),
|
|
'resolution': getattr(context, 'resolution', None),
|
|
'skip_confirmation': True, # Service layer should be non-interactive
|
|
'manual_steps_completed': True, # Manual steps were done in GUI
|
|
'appid': getattr(context, 'app_id', None), # Use updated app_id from Steam
|
|
'engine_installed': getattr(context, 'engine_installed', False) # Path manipulation flag
|
|
}
|
|
|
|
debug_callback(f"Configuration context built: {config_context}")
|
|
debug_callback("Setting up GUI mode and stdout redirection")
|
|
|
|
# Set GUI mode for proper callback handling
|
|
import os
|
|
original_gui_mode = os.environ.get('JACKIFY_GUI_MODE')
|
|
original_stdout = None
|
|
|
|
try:
|
|
# Force GUI mode to prevent input prompts
|
|
os.environ['JACKIFY_GUI_MODE'] = '1'
|
|
|
|
# CRITICAL FIX: Redirect print output to capture progress messages
|
|
import sys
|
|
from io import StringIO
|
|
|
|
# Create a custom stdout that forwards to GUI
|
|
class GuiRedirectStdout:
|
|
def __init__(self, callback):
|
|
self.callback = callback
|
|
self.buffer = ""
|
|
|
|
def write(self, text):
|
|
if self.callback and text.strip():
|
|
# Convert ANSI codes to HTML for colored GUI output
|
|
try:
|
|
from ...frontends.gui.utils import ansi_to_html
|
|
# Clean up carriage returns but preserve ANSI colors
|
|
clean_text = text.replace('\r', '').strip()
|
|
if clean_text and clean_text != "Current Task: ":
|
|
# Convert ANSI to HTML for colored display
|
|
html_text = ansi_to_html(clean_text)
|
|
self.callback(html_text)
|
|
except ImportError:
|
|
# Fallback: strip ANSI codes if ansi_to_html not available
|
|
import re
|
|
clean_text = re.sub(r'\x1b\[[0-9;]*[mK]', '', text)
|
|
clean_text = clean_text.replace('\r', '').strip()
|
|
if clean_text and clean_text != "Current Task: ":
|
|
self.callback(clean_text)
|
|
return len(text)
|
|
|
|
def flush(self):
|
|
pass
|
|
|
|
# Redirect stdout to capture print statements
|
|
if progress_callback:
|
|
original_stdout = sys.stdout
|
|
sys.stdout = GuiRedirectStdout(progress_callback)
|
|
|
|
# Call the working configuration-only method
|
|
debug_callback("Calling run_modlist_configuration_phase")
|
|
success = modlist_menu.run_modlist_configuration_phase(config_context)
|
|
debug_callback(f"Configuration phase result: {success}")
|
|
|
|
# Restore stdout before calling completion callback
|
|
if original_stdout:
|
|
sys.stdout = original_stdout
|
|
original_stdout = None
|
|
|
|
if completion_callback:
|
|
if success:
|
|
debug_callback("Configuration completed successfully, calling completion callback")
|
|
completion_callback(True, "Configuration completed successfully!", context.name)
|
|
else:
|
|
debug_callback("Configuration failed, calling completion callback with failure")
|
|
completion_callback(False, "Configuration failed", context.name)
|
|
|
|
return success
|
|
|
|
finally:
|
|
# Always restore stdout and environment
|
|
if original_stdout:
|
|
sys.stdout = original_stdout
|
|
|
|
if original_gui_mode is not None:
|
|
os.environ['JACKIFY_GUI_MODE'] = original_gui_mode
|
|
else:
|
|
os.environ.pop('JACKIFY_GUI_MODE', None)
|
|
|
|
# Remove GUI log handler to avoid memory leaks
|
|
if gui_log_handler:
|
|
for logger_name in [
|
|
'jackify.backend.handlers.menu_handler',
|
|
'jackify.backend.handlers.modlist_handler',
|
|
'jackify.backend.handlers.install_wabbajack_handler',
|
|
'jackify.backend.handlers.wabbajack_handler',
|
|
'jackify.backend.handlers.shortcut_handler',
|
|
'jackify.backend.handlers.protontricks_handler',
|
|
'jackify.backend.handlers.validation_handler',
|
|
'jackify.backend.handlers.resolution_handler'
|
|
]:
|
|
backend_logger = logging.getLogger(logger_name)
|
|
backend_logger.removeHandler(gui_log_handler)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to configure modlist {context.name}: {e}")
|
|
if completion_callback:
|
|
completion_callback(False, f"Configuration failed: {e}", context.name)
|
|
|
|
# Clean up GUI log handler on exception
|
|
if gui_log_handler:
|
|
for logger_name in [
|
|
'jackify.backend.handlers.menu_handler',
|
|
'jackify.backend.handlers.modlist_handler',
|
|
'jackify.backend.handlers.install_wabbajack_handler',
|
|
'jackify.backend.handlers.wabbajack_handler',
|
|
'jackify.backend.handlers.shortcut_handler',
|
|
'jackify.backend.handlers.protontricks_handler',
|
|
'jackify.backend.handlers.validation_handler',
|
|
'jackify.backend.handlers.resolution_handler'
|
|
]:
|
|
backend_logger = logging.getLogger(logger_name)
|
|
backend_logger.removeHandler(gui_log_handler)
|
|
|
|
return False
|
|
|
|
def configure_modlist(self, context: ModlistContext,
|
|
progress_callback=None,
|
|
manual_steps_callback=None,
|
|
completion_callback=None,
|
|
output_callback=None) -> bool:
|
|
"""Configure a modlist after installation.
|
|
|
|
Args:
|
|
context: Modlist context
|
|
progress_callback: Optional callback for progress updates
|
|
manual_steps_callback: Optional callback for manual steps
|
|
completion_callback: Optional callback for completion
|
|
output_callback: Optional callback for output/logging
|
|
|
|
Returns:
|
|
True if configuration successful, False otherwise
|
|
"""
|
|
logger.info(f"Configuring modlist: {context.name}")
|
|
|
|
try:
|
|
# Use the working ModlistMenuHandler for configuration
|
|
from ..handlers.menu_handler import ModlistMenuHandler
|
|
from ..handlers.config_handler import ConfigHandler
|
|
|
|
config_handler = ConfigHandler()
|
|
modlist_menu = ModlistMenuHandler(config_handler)
|
|
|
|
# Build configuration context
|
|
config_context = {
|
|
'name': context.name,
|
|
'path': str(context.install_dir),
|
|
'mo2_exe_path': str(context.install_dir / 'ModOrganizer.exe'),
|
|
'resolution': getattr(context, 'resolution', None),
|
|
'skip_confirmation': True, # Service layer should be non-interactive
|
|
'manual_steps_completed': False,
|
|
'appid': getattr(context, 'app_id', None) # Fix: Include appid like other configuration paths
|
|
}
|
|
|
|
# DEBUG: Log what resolution we're passing
|
|
logger.info(f"DEBUG: config_context resolution = {config_context['resolution']}")
|
|
logger.info(f"DEBUG: context.resolution = {getattr(context, 'resolution', 'NOT_SET')}")
|
|
|
|
# Run the complete configuration phase
|
|
success = modlist_menu.run_modlist_configuration_phase(config_context)
|
|
|
|
if success:
|
|
logger.info("Modlist configuration completed successfully")
|
|
if completion_callback:
|
|
completion_callback(True, "Configuration completed successfully", context.name)
|
|
else:
|
|
logger.warning("Modlist configuration had issues")
|
|
if completion_callback:
|
|
completion_callback(False, "Configuration failed", context.name)
|
|
|
|
return success
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to configure modlist {context.name}: {e}")
|
|
return False
|
|
|
|
def _validate_install_context(self, context: ModlistContext) -> bool:
|
|
"""Validate that the installation context is complete and valid.
|
|
|
|
Args:
|
|
context: The context to validate
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
"""
|
|
if not context.name:
|
|
logger.error("Modlist name is required")
|
|
return False
|
|
|
|
if not context.install_dir:
|
|
logger.error("Install directory is required")
|
|
return False
|
|
|
|
if not context.download_dir:
|
|
logger.error("Download directory is required")
|
|
return False
|
|
|
|
if not context.nexus_api_key:
|
|
logger.error("Nexus API key is required")
|
|
return False
|
|
|
|
if not context.game_type:
|
|
logger.error("Game type is required")
|
|
return False
|
|
|
|
return True |