Files
Jackify/jackify/frontends/gui/screens/install_ttw_integration.py
2026-04-20 20:57:23 +01:00

337 lines
15 KiB
Python

"""Modlist integration workflow for InstallTTWScreen (Mixin)."""
from PySide6.QtCore import QThread, Signal, Qt
from PySide6.QtWidgets import QProgressDialog, QApplication
from jackify.frontends.gui.services.message_service import MessageService
from pathlib import Path
import traceback
import os
import json
import shutil
import re
import logging
logger = logging.getLogger(__name__)
class TTWIntegrationMixin:
"""Mixin providing modlist integration workflow for InstallTTWScreen."""
def set_modlist_integration_mode(self, modlist_name: str, install_dir: str):
"""Set the screen to modlist integration mode
This mode is activated when TTW needs to be installed and integrated
into an existing modlist. In this mode, after TTW installation completes,
the TTW output will be automatically integrated into the modlist.
Args:
modlist_name: Name of the modlist to integrate TTW into
install_dir: Installation directory of the modlist
"""
self._integration_mode = True
self._integration_modlist_name = modlist_name
self._integration_install_dir = install_dir
# Pre-populate output dir to install TTW directly into the modlist mods folder,
# avoiding the wasteful copy step during integration.
ttw_target = Path(install_dir) / "mods" / "[NoDelete] Tale of Two Wastelands"
self.install_dir_edit.setText(str(ttw_target))
# Reset saved geometry so showEvent can properly collapse from current window size
self._saved_geometry = None
self._saved_min_size = None
logger.debug(f"TTW screen set to integration mode for modlist: {modlist_name}")
logger.debug(f"TTW output pre-populated to: {ttw_target}")
def _perform_modlist_integration(self):
"""Integrate TTW into the modlist automatically
This is called when in integration mode. It will:
1. Copy TTW output to modlist's mods folder
2. Update modlist.txt for all profiles
3. Update plugins.txt with TTW ESMs in correct order
4. Emit integration_complete signal
"""
try:
from pathlib import Path
import re
from PySide6.QtCore import QThread, Signal
# Get TTW output directory
ttw_output_dir = Path(self.install_dir_edit.text())
if not ttw_output_dir.exists():
error_msg = f"TTW output directory not found: {ttw_output_dir}"
self._safe_append_text(f"\nError: {error_msg}")
self.integration_complete.emit(False, "")
return
# Extract version from .mpi filename
mpi_path = self.file_edit.text().strip()
ttw_version = ""
if mpi_path:
mpi_filename = Path(mpi_path).stem
version_match = re.search(r'v?(\d+\.\d+(?:\.\d+)?)', mpi_filename, re.IGNORECASE)
if version_match:
ttw_version = version_match.group(1)
# If TTW was installed directly into the modlist mods dir (integration mode
# pre-populate), rename to the versioned folder name and skip the copy step.
skip_copy = False
mods_dir = Path(self._integration_install_dir) / "mods"
if ttw_output_dir.parent == mods_dir:
versioned_name = f"[NoDelete] Tale of Two Wastelands {ttw_version}".strip() if ttw_version else "[NoDelete] Tale of Two Wastelands"
versioned_path = mods_dir / versioned_name
if ttw_output_dir != versioned_path and ttw_output_dir.exists():
logger.debug(f"Renaming TTW output: {ttw_output_dir.name} -> {versioned_name}")
ttw_output_dir.rename(versioned_path)
ttw_output_dir = versioned_path
skip_copy = True
logger.debug("TTW already in mods dir - skipping copy step")
# Create background thread for integration
class IntegrationThread(QThread):
finished = Signal(bool, str) # success, ttw_version
progress = Signal(str) # progress message
def __init__(self, ttw_output_path, modlist_install_dir, ttw_version, skip_copy):
super().__init__()
self.ttw_output_path = ttw_output_path
self.modlist_install_dir = modlist_install_dir
self.ttw_version = ttw_version
self.skip_copy = skip_copy
def run(self):
try:
from jackify.backend.handlers.ttw_installer_handler import TTWInstallerHandler
self.progress.emit("Integrating TTW into modlist...")
success = TTWInstallerHandler.integrate_ttw_into_modlist(
ttw_output_path=self.ttw_output_path,
modlist_install_dir=self.modlist_install_dir,
ttw_version=self.ttw_version,
skip_copy=self.skip_copy,
)
self.finished.emit(success, self.ttw_version)
except Exception as e:
logger.debug(f"ERROR: Integration thread failed: {e}")
import traceback
traceback.print_exc()
self.finished.emit(False, self.ttw_version)
# Show progress message
self._safe_append_text("\nIntegrating TTW into modlist (this may take a few minutes)...")
# Update status banner (only in integration mode - visible when collapsed)
if self._integration_mode:
self.status_banner.setText("Integrating TTW into modlist (this may take a few minutes)...")
self.status_banner.setStyleSheet(f"""
QLabel {{
background-color: #FFA500;
color: white;
font-weight: bold;
padding: 8px;
border-radius: 5px;
}}
""")
# Create progress dialog for integration
progress_dialog = QProgressDialog(
f"Integrating TTW {ttw_version} into modlist...\n\n"
"This involves copying several GB of files and may take a few minutes.\n"
"Please wait...",
None, # No cancel button
0, 0, # Indeterminate progress
self
)
progress_dialog.setWindowTitle("Integrating TTW")
progress_dialog.setMinimumDuration(0) # Show immediately
progress_dialog.setWindowModality(Qt.ApplicationModal)
progress_dialog.setCancelButton(None)
progress_dialog.show()
QApplication.processEvents()
# Store reference to close later
self._integration_progress_dialog = progress_dialog
# Create and start integration thread
self.integration_thread = IntegrationThread(
ttw_output_dir,
Path(self._integration_install_dir),
ttw_version,
skip_copy,
)
self.integration_thread.progress.connect(self._safe_append_text)
self.integration_thread.finished.connect(self._on_integration_thread_finished)
self.integration_thread.start()
except Exception as e:
# Close progress dialog if it exists
if hasattr(self, '_integration_progress_dialog'):
self._integration_progress_dialog.close()
delattr(self, '_integration_progress_dialog')
error_msg = f"Integration error: {str(e)}"
self._safe_append_text(f"\nError: {error_msg}")
logger.debug(f"ERROR: {error_msg}")
import traceback
traceback.print_exc()
self.integration_complete.emit(False, "")
def _on_integration_thread_finished(self, success: bool, ttw_version: str):
"""Handle completion of integration thread"""
try:
# Close progress dialog
if hasattr(self, '_integration_progress_dialog'):
self._integration_progress_dialog.close()
delattr(self, '_integration_progress_dialog')
if success:
self._safe_append_text("\nTTW integration completed successfully!")
# Update status banner (only in integration mode)
if self._integration_mode:
self.status_banner.setText("TTW integration completed successfully!")
self.status_banner.setStyleSheet(f"""
QLabel {{
background-color: #28a745;
color: white;
font-weight: bold;
padding: 8px;
border-radius: 5px;
}}
""")
MessageService.information(
self, "Integration Complete",
f"TTW {ttw_version} has been successfully integrated into {self._integration_modlist_name}!",
safety_level="medium"
)
self.integration_complete.emit(True, ttw_version)
else:
self._safe_append_text("\nTTW integration failed!")
# Update status banner (only in integration mode)
if self._integration_mode:
self.status_banner.setText("TTW integration failed!")
self.status_banner.setStyleSheet(f"""
QLabel {{
background-color: #dc3545;
color: white;
font-weight: bold;
padding: 8px;
border-radius: 5px;
}}
""")
MessageService.critical(
self, "Integration Failed",
"Failed to integrate TTW into the modlist. Check the log for details."
)
self.integration_complete.emit(False, ttw_version)
except Exception as e:
logger.debug(f"ERROR: Failed to handle integration completion: {e}")
self.integration_complete.emit(False, ttw_version)
def _create_ttw_mod_archive(self, automated=False):
"""Create a zipped mod archive of TTW output for MO2 installation.
Args:
automated: If True, runs silently without user prompts (for automation)
"""
try:
from pathlib import Path
import re
from PySide6.QtCore import QThread, Signal
output_dir = Path(self.install_dir_edit.text())
if not output_dir.exists():
if not automated:
MessageService.warning(self, "Output Directory Not Found",
f"Output directory does not exist:\n{output_dir}")
return False
# Extract version from .mpi filename (e.g., "TTW v3.4.mpi" -> "3.4")
mpi_path = self.file_edit.text().strip()
version_suffix = ""
if mpi_path:
mpi_filename = Path(mpi_path).stem
version_match = re.search(r'v?(\d+\.\d+(?:\.\d+)?)', mpi_filename, re.IGNORECASE)
if version_match:
version_suffix = f" {version_match.group(1)}"
# Create archive filename
archive_name = f"[NoDelete] Tale of Two Wastelands{version_suffix}"
archive_path = output_dir.parent / archive_name
# Create background thread for zip creation
class ZipCreationThread(QThread):
finished = Signal(bool, str) # success, result_message
def __init__(self, output_dir, archive_path):
super().__init__()
self.output_dir = output_dir
self.archive_path = archive_path
def run(self):
try:
import shutil
final_archive = shutil.make_archive(
str(self.archive_path),
'zip',
str(self.output_dir)
)
self.finished.emit(True, str(final_archive))
except Exception as e:
self.finished.emit(False, str(e))
# Create progress dialog (non-modal so UI stays responsive)
progress_dialog = QProgressDialog(
f"Creating mod archive: {archive_name}.zip\n\n"
"This may take several minutes depending on installation size...",
"Cancel",
0, 0, # 0,0 = indeterminate progress bar
self
)
progress_dialog.setWindowTitle("Creating Archive")
progress_dialog.setMinimumDuration(0) # Show immediately
progress_dialog.setWindowModality(Qt.ApplicationModal)
progress_dialog.setCancelButton(None) # Cannot cancel zip operation safely
progress_dialog.show()
QApplication.processEvents()
# Create and start thread
zip_thread = ZipCreationThread(output_dir, archive_path)
def on_zip_finished(success, result):
progress_dialog.close()
if success:
final_archive = result
if not automated:
self._safe_append_text(f"\nArchive created successfully: {Path(final_archive).name}")
MessageService.information(
self, "Archive Created",
f"TTW mod archive created successfully!\n\n"
f"Location: {final_archive}\n\n"
f"You can now install this archive as a mod in MO2.",
safety_level="medium"
)
else:
error_msg = f"Failed to create mod archive: {result}"
if not automated:
self._safe_append_text(f"\nError: {error_msg}")
MessageService.critical(self, "Archive Creation Failed", error_msg)
zip_thread.finished.connect(on_zip_finished)
zip_thread.start()
# Keep reference to prevent garbage collection
self._zip_thread = zip_thread
return True
except Exception as e:
error_msg = f"Failed to create mod archive: {str(e)}"
if not automated:
self._safe_append_text(f"\nError: {error_msg}")
MessageService.critical(self, "Archive Creation Failed", error_msg)
return False