mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 11:37:01 +01:00
735 lines
32 KiB
Python
735 lines
32 KiB
Python
"""
|
|
TTW_Linux_Installer Handler
|
|
|
|
Handles downloading, installation, and execution of TTW_Linux_Installer for TTW installations.
|
|
Replaces hoolamike for TTW-specific functionality.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import tarfile
|
|
import zipfile
|
|
from pathlib import Path
|
|
from typing import Optional, Tuple
|
|
import requests
|
|
|
|
from .path_handler import PathHandler
|
|
from .filesystem_handler import FileSystemHandler
|
|
from .config_handler import ConfigHandler
|
|
from .logging_handler import LoggingHandler
|
|
from .subprocess_utils import get_clean_subprocess_env
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Define default TTW_Linux_Installer paths
|
|
from jackify.shared.paths import get_jackify_data_dir
|
|
JACKIFY_BASE_DIR = get_jackify_data_dir()
|
|
DEFAULT_TTW_INSTALLER_DIR = JACKIFY_BASE_DIR / "TTW_Linux_Installer"
|
|
TTW_INSTALLER_EXECUTABLE_NAME = "ttw_linux_gui" # Same executable, runs in CLI mode with args
|
|
|
|
# GitHub release info
|
|
TTW_INSTALLER_REPO = "SulfurNitride/TTW_Linux_Installer"
|
|
TTW_INSTALLER_RELEASE_URL = f"https://api.github.com/repos/{TTW_INSTALLER_REPO}/releases/latest"
|
|
|
|
|
|
class TTWInstallerHandler:
|
|
"""Handles TTW installation using TTW_Linux_Installer (replaces hoolamike for TTW)."""
|
|
|
|
def __init__(self, steamdeck: bool, verbose: bool, filesystem_handler: FileSystemHandler,
|
|
config_handler: ConfigHandler, menu_handler=None):
|
|
"""Initialize the handler."""
|
|
self.steamdeck = steamdeck
|
|
self.verbose = verbose
|
|
self.path_handler = PathHandler()
|
|
self.filesystem_handler = filesystem_handler
|
|
self.config_handler = config_handler
|
|
self.menu_handler = menu_handler
|
|
|
|
# Set up logging
|
|
logging_handler = LoggingHandler()
|
|
logging_handler.rotate_log_for_logger('ttw-install', 'TTW_Install_workflow.log')
|
|
self.logger = logging_handler.setup_logger('ttw-install', 'TTW_Install_workflow.log')
|
|
|
|
# Installation paths
|
|
self.ttw_installer_dir: Path = DEFAULT_TTW_INSTALLER_DIR
|
|
self.ttw_installer_executable_path: Optional[Path] = None
|
|
self.ttw_installer_installed: bool = False
|
|
|
|
# Load saved install path from config
|
|
saved_path_str = self.config_handler.get('ttw_installer_install_path')
|
|
if saved_path_str and Path(saved_path_str).is_dir():
|
|
self.ttw_installer_dir = Path(saved_path_str)
|
|
self.logger.info(f"Loaded TTW_Linux_Installer path from config: {self.ttw_installer_dir}")
|
|
|
|
# Check if already installed
|
|
self._check_installation()
|
|
|
|
def _ensure_dirs_exist(self):
|
|
"""Ensure base directories exist."""
|
|
self.ttw_installer_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
def _check_installation(self):
|
|
"""Check if TTW_Linux_Installer is installed at expected location."""
|
|
self._ensure_dirs_exist()
|
|
|
|
potential_exe_path = self.ttw_installer_dir / TTW_INSTALLER_EXECUTABLE_NAME
|
|
if potential_exe_path.is_file() and os.access(potential_exe_path, os.X_OK):
|
|
self.ttw_installer_executable_path = potential_exe_path
|
|
self.ttw_installer_installed = True
|
|
self.logger.info(f"Found TTW_Linux_Installer at: {self.ttw_installer_executable_path}")
|
|
else:
|
|
self.ttw_installer_installed = False
|
|
self.ttw_installer_executable_path = None
|
|
self.logger.info(f"TTW_Linux_Installer not found at {potential_exe_path}")
|
|
|
|
def install_ttw_installer(self, install_dir: Optional[Path] = None) -> Tuple[bool, str]:
|
|
"""Download and install TTW_Linux_Installer from GitHub releases.
|
|
|
|
Args:
|
|
install_dir: Optional directory to install to (defaults to ~/Jackify/TTW_Linux_Installer)
|
|
|
|
Returns:
|
|
(success: bool, message: str)
|
|
"""
|
|
try:
|
|
self._ensure_dirs_exist()
|
|
target_dir = Path(install_dir) if install_dir else self.ttw_installer_dir
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Fetch latest release info
|
|
self.logger.info(f"Fetching latest TTW_Linux_Installer release from {TTW_INSTALLER_RELEASE_URL}")
|
|
resp = requests.get(TTW_INSTALLER_RELEASE_URL, timeout=15, verify=True)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
release_tag = data.get("tag_name") or data.get("name")
|
|
|
|
# Find Linux asset - universal-mpi-installer pattern (can be .zip or .tar.gz)
|
|
linux_asset = None
|
|
asset_names = [asset.get("name", "") for asset in data.get("assets", [])]
|
|
self.logger.info(f"Available release assets: {asset_names}")
|
|
|
|
for asset in data.get("assets", []):
|
|
name = asset.get("name", "").lower()
|
|
# Look for universal-mpi-installer pattern
|
|
if "universal-mpi-installer" in name and name.endswith((".zip", ".tar.gz")):
|
|
linux_asset = asset
|
|
self.logger.info(f"Found Linux asset: {asset.get('name')}")
|
|
break
|
|
|
|
if not linux_asset:
|
|
# Log all available assets for debugging
|
|
all_assets = [asset.get("name", "") for asset in data.get("assets", [])]
|
|
self.logger.error(f"No suitable Linux asset found. Available assets: {all_assets}")
|
|
return False, f"No suitable Linux TTW_Linux_Installer asset found in latest release. Available assets: {', '.join(all_assets)}"
|
|
|
|
download_url = linux_asset.get("browser_download_url")
|
|
asset_name = linux_asset.get("name")
|
|
if not download_url or not asset_name:
|
|
return False, "Latest release is missing required asset metadata"
|
|
|
|
# Download to target directory
|
|
temp_path = target_dir / asset_name
|
|
self.logger.info(f"Downloading {asset_name} from {download_url}")
|
|
if not self.filesystem_handler.download_file(download_url, temp_path, overwrite=True, quiet=True):
|
|
return False, "Failed to download TTW_Linux_Installer asset"
|
|
|
|
# Extract archive (zip or tar.gz)
|
|
try:
|
|
self.logger.info(f"Extracting {asset_name} to {target_dir}")
|
|
if asset_name.lower().endswith('.tar.gz'):
|
|
with tarfile.open(temp_path, "r:gz") as tf:
|
|
tf.extractall(path=target_dir)
|
|
elif asset_name.lower().endswith('.zip'):
|
|
with zipfile.ZipFile(temp_path, "r") as zf:
|
|
zf.extractall(path=target_dir)
|
|
else:
|
|
return False, f"Unsupported archive format: {asset_name}"
|
|
finally:
|
|
try:
|
|
temp_path.unlink(missing_ok=True) # cleanup
|
|
except Exception:
|
|
pass
|
|
|
|
# Find executable (may be in subdirectory or root)
|
|
exe_path = target_dir / TTW_INSTALLER_EXECUTABLE_NAME
|
|
if not exe_path.is_file():
|
|
# Search for it
|
|
for p in target_dir.rglob(TTW_INSTALLER_EXECUTABLE_NAME):
|
|
if p.is_file():
|
|
exe_path = p
|
|
break
|
|
|
|
if not exe_path.is_file():
|
|
return False, "TTW_Linux_Installer executable not found after extraction"
|
|
|
|
# Set executable permissions
|
|
try:
|
|
os.chmod(exe_path, 0o755)
|
|
except Exception as e:
|
|
self.logger.warning(f"Failed to chmod +x on {exe_path}: {e}")
|
|
|
|
# Update state
|
|
self.ttw_installer_dir = target_dir
|
|
self.ttw_installer_executable_path = exe_path
|
|
self.ttw_installer_installed = True
|
|
self.config_handler.set('ttw_installer_install_path', str(target_dir))
|
|
if release_tag:
|
|
self.config_handler.set('ttw_installer_version', release_tag)
|
|
|
|
self.logger.info(f"TTW_Linux_Installer installed successfully at {exe_path}")
|
|
return True, f"TTW_Linux_Installer installed at {target_dir}"
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error installing TTW_Linux_Installer: {e}", exc_info=True)
|
|
return False, f"Error installing TTW_Linux_Installer: {e}"
|
|
|
|
def get_installed_ttw_installer_version(self) -> Optional[str]:
|
|
"""Return the installed TTW_Linux_Installer version stored in Jackify config, if any."""
|
|
try:
|
|
v = self.config_handler.get('ttw_installer_version')
|
|
return str(v) if v else None
|
|
except Exception:
|
|
return None
|
|
|
|
def is_ttw_installer_update_available(self) -> Tuple[bool, Optional[str], Optional[str]]:
|
|
"""
|
|
Check GitHub for the latest TTW_Linux_Installer release and compare with installed version.
|
|
Returns (update_available, installed_version, latest_version).
|
|
"""
|
|
installed = self.get_installed_ttw_installer_version()
|
|
|
|
# If executable exists but no version is recorded, don't show as "out of date"
|
|
# This can happen if the executable was installed before version tracking was added
|
|
if not installed and self.ttw_installer_installed:
|
|
self.logger.info("TTW_Linux_Installer executable found but no version recorded in config")
|
|
# Don't treat as update available - just show as "Ready" (unknown version)
|
|
return (False, None, None)
|
|
|
|
try:
|
|
resp = requests.get(TTW_INSTALLER_RELEASE_URL, timeout=10, verify=True)
|
|
resp.raise_for_status()
|
|
latest = resp.json().get('tag_name') or resp.json().get('name')
|
|
if not latest:
|
|
return (False, installed, None)
|
|
if not installed:
|
|
# No version recorded and executable doesn't exist; treat as not installed
|
|
return (False, None, str(latest))
|
|
return (installed != str(latest), installed, str(latest))
|
|
except Exception as e:
|
|
self.logger.warning(f"Error checking for TTW_Linux_Installer updates: {e}")
|
|
return (False, installed, None)
|
|
|
|
def install_ttw_backend(self, ttw_mpi_path: Path, ttw_output_path: Path) -> Tuple[bool, str]:
|
|
"""Install TTW using TTW_Linux_Installer.
|
|
|
|
Args:
|
|
ttw_mpi_path: Path to TTW .mpi file
|
|
ttw_output_path: Target installation directory
|
|
|
|
Returns:
|
|
(success: bool, message: str)
|
|
"""
|
|
self.logger.info("Starting Tale of Two Wastelands installation via TTW_Linux_Installer")
|
|
|
|
# Validate parameters
|
|
if not ttw_mpi_path or not ttw_output_path:
|
|
return False, "Missing required parameters: ttw_mpi_path and ttw_output_path are required"
|
|
|
|
ttw_mpi_path = Path(ttw_mpi_path)
|
|
ttw_output_path = Path(ttw_output_path)
|
|
|
|
# Validate paths
|
|
if not ttw_mpi_path.exists():
|
|
return False, f"TTW .mpi file not found: {ttw_mpi_path}"
|
|
|
|
if not ttw_mpi_path.is_file():
|
|
return False, f"TTW .mpi path is not a file: {ttw_mpi_path}"
|
|
|
|
if ttw_mpi_path.suffix.lower() != '.mpi':
|
|
return False, f"TTW path does not have .mpi extension: {ttw_mpi_path}"
|
|
|
|
if not ttw_output_path.exists():
|
|
try:
|
|
ttw_output_path.mkdir(parents=True, exist_ok=True)
|
|
except Exception as e:
|
|
return False, f"Failed to create output directory: {e}"
|
|
|
|
# Check installation
|
|
if not self.ttw_installer_installed:
|
|
# Try to install automatically
|
|
self.logger.info("TTW_Linux_Installer not found, attempting to install...")
|
|
success, message = self.install_ttw_installer()
|
|
if not success:
|
|
return False, f"TTW_Linux_Installer not installed and auto-install failed: {message}"
|
|
|
|
if not self.ttw_installer_executable_path or not self.ttw_installer_executable_path.is_file():
|
|
return False, "TTW_Linux_Installer executable not found"
|
|
|
|
# Detect game paths
|
|
required_games = ['Fallout 3', 'Fallout New Vegas']
|
|
detected_games = self.path_handler.find_vanilla_game_paths()
|
|
missing_games = [game for game in required_games if game not in detected_games]
|
|
if missing_games:
|
|
return False, f"Missing required games: {', '.join(missing_games)}. TTW requires both Fallout 3 and Fallout New Vegas."
|
|
|
|
fallout3_path = detected_games.get('Fallout 3')
|
|
falloutnv_path = detected_games.get('Fallout New Vegas')
|
|
|
|
if not fallout3_path or not falloutnv_path:
|
|
return False, "Could not detect Fallout 3 or Fallout New Vegas installation paths"
|
|
|
|
# Construct command - run in CLI mode with arguments
|
|
cmd = [
|
|
str(self.ttw_installer_executable_path),
|
|
"--fo3", str(fallout3_path),
|
|
"--fnv", str(falloutnv_path),
|
|
"--mpi", str(ttw_mpi_path),
|
|
"--output", str(ttw_output_path),
|
|
"--start"
|
|
]
|
|
|
|
self.logger.info(f"Executing TTW_Linux_Installer: {' '.join(cmd)}")
|
|
|
|
try:
|
|
env = get_clean_subprocess_env()
|
|
# CRITICAL: cwd must be the directory containing the executable, not the extraction root
|
|
# This is because AppContext.BaseDirectory (used by TTW installer to find BundledBinaries)
|
|
# is the directory containing the executable, not the working directory
|
|
exe_dir = str(self.ttw_installer_executable_path.parent)
|
|
process = subprocess.Popen(
|
|
cmd,
|
|
cwd=exe_dir,
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
bufsize=1,
|
|
universal_newlines=True
|
|
)
|
|
|
|
# Stream output to logger
|
|
if process.stdout:
|
|
for line in process.stdout:
|
|
line = line.rstrip()
|
|
if line:
|
|
self.logger.info(f"TTW_Linux_Installer: {line}")
|
|
|
|
process.wait()
|
|
ret = process.returncode
|
|
|
|
if ret == 0:
|
|
self.logger.info("TTW installation completed successfully.")
|
|
return True, "TTW installation completed successfully!"
|
|
else:
|
|
self.logger.error(f"TTW installation process returned non-zero exit code: {ret}")
|
|
return False, f"TTW installation failed with exit code {ret}"
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error executing TTW_Linux_Installer: {e}", exc_info=True)
|
|
return False, f"Error executing TTW_Linux_Installer: {e}"
|
|
|
|
def start_ttw_installation(self, ttw_mpi_path: Path, ttw_output_path: Path, output_file: Path):
|
|
"""Start TTW installation process (non-blocking).
|
|
|
|
Starts the TTW_Linux_Installer subprocess with output redirected to a file.
|
|
Returns immediately with process handle. Caller should poll process and read output file.
|
|
|
|
Args:
|
|
ttw_mpi_path: Path to TTW .mpi file
|
|
ttw_output_path: Target installation directory
|
|
output_file: Path to file where stdout/stderr will be written
|
|
|
|
Returns:
|
|
(process: subprocess.Popen, error_message: str) - process is None if failed
|
|
"""
|
|
self.logger.info("Starting TTW installation (non-blocking mode)")
|
|
|
|
# Validate parameters
|
|
if not ttw_mpi_path or not ttw_output_path:
|
|
return None, "Missing required parameters: ttw_mpi_path and ttw_output_path are required"
|
|
|
|
ttw_mpi_path = Path(ttw_mpi_path)
|
|
ttw_output_path = Path(ttw_output_path)
|
|
|
|
# Validate paths
|
|
if not ttw_mpi_path.exists():
|
|
return None, f"TTW .mpi file not found: {ttw_mpi_path}"
|
|
|
|
if not ttw_mpi_path.is_file():
|
|
return None, f"TTW .mpi path is not a file: {ttw_mpi_path}"
|
|
|
|
if ttw_mpi_path.suffix.lower() != '.mpi':
|
|
return None, f"TTW path does not have .mpi extension: {ttw_mpi_path}"
|
|
|
|
if not ttw_output_path.exists():
|
|
try:
|
|
ttw_output_path.mkdir(parents=True, exist_ok=True)
|
|
except Exception as e:
|
|
return None, f"Failed to create output directory: {e}"
|
|
|
|
# Check installation
|
|
if not self.ttw_installer_installed:
|
|
self.logger.info("TTW_Linux_Installer not found, attempting to install...")
|
|
success, message = self.install_ttw_installer()
|
|
if not success:
|
|
return None, f"TTW_Linux_Installer not installed and auto-install failed: {message}"
|
|
|
|
if not self.ttw_installer_executable_path or not self.ttw_installer_executable_path.is_file():
|
|
return None, "TTW_Linux_Installer executable not found"
|
|
|
|
# Detect game paths
|
|
required_games = ['Fallout 3', 'Fallout New Vegas']
|
|
detected_games = self.path_handler.find_vanilla_game_paths()
|
|
missing_games = [game for game in required_games if game not in detected_games]
|
|
if missing_games:
|
|
return None, f"Missing required games: {', '.join(missing_games)}. TTW requires both Fallout 3 and Fallout New Vegas."
|
|
|
|
fallout3_path = detected_games.get('Fallout 3')
|
|
falloutnv_path = detected_games.get('Fallout New Vegas')
|
|
|
|
if not fallout3_path or not falloutnv_path:
|
|
return None, "Could not detect Fallout 3 or Fallout New Vegas installation paths"
|
|
|
|
# Construct command
|
|
cmd = [
|
|
str(self.ttw_installer_executable_path),
|
|
"--fo3", str(fallout3_path),
|
|
"--fnv", str(falloutnv_path),
|
|
"--mpi", str(ttw_mpi_path),
|
|
"--output", str(ttw_output_path),
|
|
"--start"
|
|
]
|
|
|
|
self.logger.info(f"Executing TTW_Linux_Installer: {' '.join(cmd)}")
|
|
|
|
try:
|
|
env = get_clean_subprocess_env()
|
|
# Note: TTW_Linux_Installer bundles its own lz4 and will find it via AppContext.BaseDirectory
|
|
# We set cwd to the executable's directory so AppContext.BaseDirectory matches the working directory
|
|
|
|
# Open output file for writing
|
|
output_fh = open(output_file, 'w', encoding='utf-8', buffering=1)
|
|
|
|
# Start process with output redirected to file
|
|
# CRITICAL: cwd must be the directory containing the executable, not the extraction root
|
|
# This is because AppContext.BaseDirectory (used by TTW installer to find BundledBinaries)
|
|
# is the directory containing the executable, not the working directory
|
|
exe_dir = str(self.ttw_installer_executable_path.parent)
|
|
process = subprocess.Popen(
|
|
cmd,
|
|
cwd=exe_dir,
|
|
env=env,
|
|
stdout=output_fh,
|
|
stderr=subprocess.STDOUT,
|
|
bufsize=1
|
|
)
|
|
|
|
self.logger.info(f"TTW_Linux_Installer process started (PID: {process.pid}), output to {output_file}")
|
|
|
|
# Store file handle so it can be closed later
|
|
process._output_fh = output_fh
|
|
|
|
return process, None
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error starting TTW_Linux_Installer: {e}", exc_info=True)
|
|
return None, f"Error starting TTW_Linux_Installer: {e}"
|
|
|
|
@staticmethod
|
|
def cleanup_ttw_process(process):
|
|
"""Clean up after TTW installation process.
|
|
|
|
Closes file handles and ensures process is terminated properly.
|
|
|
|
Args:
|
|
process: subprocess.Popen object from start_ttw_installation()
|
|
"""
|
|
if process:
|
|
# Close output file handle if attached
|
|
if hasattr(process, '_output_fh'):
|
|
try:
|
|
process._output_fh.close()
|
|
except Exception:
|
|
pass
|
|
|
|
# Terminate if still running
|
|
if process.poll() is None:
|
|
try:
|
|
process.terminate()
|
|
process.wait(timeout=5)
|
|
except Exception:
|
|
try:
|
|
process.kill()
|
|
except Exception:
|
|
pass
|
|
|
|
def install_ttw_backend_with_output_stream(self, ttw_mpi_path: Path, ttw_output_path: Path, output_callback=None):
|
|
"""Install TTW with streaming output for GUI (DEPRECATED - use start_ttw_installation instead).
|
|
|
|
Args:
|
|
ttw_mpi_path: Path to TTW .mpi file
|
|
ttw_output_path: Target installation directory
|
|
output_callback: Optional callback function(line: str) for real-time output
|
|
|
|
Returns:
|
|
(success: bool, message: str)
|
|
"""
|
|
self.logger.info("Starting Tale of Two Wastelands installation via TTW_Linux_Installer (with output stream)")
|
|
|
|
# Validate parameters (same as install_ttw_backend)
|
|
if not ttw_mpi_path or not ttw_output_path:
|
|
return False, "Missing required parameters: ttw_mpi_path and ttw_output_path are required"
|
|
|
|
ttw_mpi_path = Path(ttw_mpi_path)
|
|
ttw_output_path = Path(ttw_output_path)
|
|
|
|
# Validate paths
|
|
if not ttw_mpi_path.exists():
|
|
return False, f"TTW .mpi file not found: {ttw_mpi_path}"
|
|
|
|
if not ttw_mpi_path.is_file():
|
|
return False, f"TTW .mpi path is not a file: {ttw_mpi_path}"
|
|
|
|
if ttw_mpi_path.suffix.lower() != '.mpi':
|
|
return False, f"TTW path does not have .mpi extension: {ttw_mpi_path}"
|
|
|
|
if not ttw_output_path.exists():
|
|
try:
|
|
ttw_output_path.mkdir(parents=True, exist_ok=True)
|
|
except Exception as e:
|
|
return False, f"Failed to create output directory: {e}"
|
|
|
|
# Check installation
|
|
if not self.ttw_installer_installed:
|
|
if output_callback:
|
|
output_callback("TTW_Linux_Installer not found, installing...")
|
|
self.logger.info("TTW_Linux_Installer not found, attempting to install...")
|
|
success, message = self.install_ttw_installer()
|
|
if not success:
|
|
return False, f"TTW_Linux_Installer not installed and auto-install failed: {message}"
|
|
|
|
if not self.ttw_installer_executable_path or not self.ttw_installer_executable_path.is_file():
|
|
return False, "TTW_Linux_Installer executable not found"
|
|
|
|
# Detect game paths
|
|
required_games = ['Fallout 3', 'Fallout New Vegas']
|
|
detected_games = self.path_handler.find_vanilla_game_paths()
|
|
missing_games = [game for game in required_games if game not in detected_games]
|
|
if missing_games:
|
|
return False, f"Missing required games: {', '.join(missing_games)}. TTW requires both Fallout 3 and Fallout New Vegas."
|
|
|
|
fallout3_path = detected_games.get('Fallout 3')
|
|
falloutnv_path = detected_games.get('Fallout New Vegas')
|
|
|
|
if not fallout3_path or not falloutnv_path:
|
|
return False, "Could not detect Fallout 3 or Fallout New Vegas installation paths"
|
|
|
|
# Construct command
|
|
cmd = [
|
|
str(self.ttw_installer_executable_path),
|
|
"--fo3", str(fallout3_path),
|
|
"--fnv", str(falloutnv_path),
|
|
"--mpi", str(ttw_mpi_path),
|
|
"--output", str(ttw_output_path),
|
|
"--start"
|
|
]
|
|
|
|
self.logger.info(f"Executing TTW_Linux_Installer: {' '.join(cmd)}")
|
|
|
|
try:
|
|
env = get_clean_subprocess_env()
|
|
# CRITICAL: cwd must be the directory containing the executable, not the extraction root
|
|
# This is because AppContext.BaseDirectory (used by TTW installer to find BundledBinaries)
|
|
# is the directory containing the executable, not the working directory
|
|
exe_dir = str(self.ttw_installer_executable_path.parent)
|
|
process = subprocess.Popen(
|
|
cmd,
|
|
cwd=exe_dir,
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
bufsize=1,
|
|
universal_newlines=True
|
|
)
|
|
|
|
# Stream output to both logger and callback
|
|
if process.stdout:
|
|
for line in process.stdout:
|
|
line = line.rstrip()
|
|
if line:
|
|
self.logger.info(f"TTW_Linux_Installer: {line}")
|
|
if output_callback:
|
|
output_callback(line)
|
|
|
|
process.wait()
|
|
ret = process.returncode
|
|
|
|
if ret == 0:
|
|
self.logger.info("TTW installation completed successfully.")
|
|
return True, "TTW installation completed successfully!"
|
|
else:
|
|
self.logger.error(f"TTW installation process returned non-zero exit code: {ret}")
|
|
return False, f"TTW installation failed with exit code {ret}"
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error executing TTW_Linux_Installer: {e}", exc_info=True)
|
|
return False, f"Error executing TTW_Linux_Installer: {e}"
|
|
|
|
@staticmethod
|
|
def integrate_ttw_into_modlist(ttw_output_path: Path, modlist_install_dir: Path, ttw_version: str) -> bool:
|
|
"""Integrate TTW output into a modlist's MO2 structure
|
|
|
|
This method:
|
|
1. Copies TTW output to the modlist's mods folder
|
|
2. Updates modlist.txt for all profiles
|
|
3. Updates plugins.txt with TTW ESMs in correct order
|
|
|
|
Args:
|
|
ttw_output_path: Path to TTW output directory
|
|
modlist_install_dir: Path to modlist installation directory
|
|
ttw_version: TTW version string (e.g., "3.4")
|
|
|
|
Returns:
|
|
bool: True if integration successful, False otherwise
|
|
"""
|
|
logging_handler = LoggingHandler()
|
|
logging_handler.rotate_log_for_logger('ttw-install', 'TTW_Install_workflow.log')
|
|
logger = logging_handler.setup_logger('ttw-install', 'TTW_Install_workflow.log')
|
|
|
|
try:
|
|
import shutil
|
|
|
|
# Validate paths
|
|
if not ttw_output_path.exists():
|
|
logger.error(f"TTW output path does not exist: {ttw_output_path}")
|
|
return False
|
|
|
|
mods_dir = modlist_install_dir / "mods"
|
|
profiles_dir = modlist_install_dir / "profiles"
|
|
|
|
if not mods_dir.exists() or not profiles_dir.exists():
|
|
logger.error(f"Invalid modlist directory structure: {modlist_install_dir}")
|
|
return False
|
|
|
|
# Create mod folder name with version
|
|
mod_folder_name = f"[NoDelete] Tale of Two Wastelands {ttw_version}" if ttw_version else "[NoDelete] Tale of Two Wastelands"
|
|
target_mod_dir = mods_dir / mod_folder_name
|
|
|
|
# Copy TTW output to mods directory
|
|
logger.info(f"Copying TTW output to {target_mod_dir}")
|
|
if target_mod_dir.exists():
|
|
logger.info(f"Removing existing TTW mod at {target_mod_dir}")
|
|
shutil.rmtree(target_mod_dir)
|
|
|
|
shutil.copytree(ttw_output_path, target_mod_dir)
|
|
logger.info("TTW output copied successfully")
|
|
|
|
# TTW ESMs in correct load order
|
|
ttw_esms = [
|
|
"Fallout3.esm",
|
|
"Anchorage.esm",
|
|
"ThePitt.esm",
|
|
"BrokenSteel.esm",
|
|
"PointLookout.esm",
|
|
"Zeta.esm",
|
|
"TaleOfTwoWastelands.esm",
|
|
"YUPTTW.esm"
|
|
]
|
|
|
|
# Process each profile
|
|
for profile_dir in profiles_dir.iterdir():
|
|
if not profile_dir.is_dir():
|
|
continue
|
|
|
|
profile_name = profile_dir.name
|
|
logger.info(f"Processing profile: {profile_name}")
|
|
|
|
# Update modlist.txt
|
|
modlist_file = profile_dir / "modlist.txt"
|
|
if modlist_file.exists():
|
|
# Read existing modlist
|
|
with open(modlist_file, 'r', encoding='utf-8') as f:
|
|
lines = f.readlines()
|
|
|
|
# Find the TTW placeholder separator and insert BEFORE it
|
|
separator_found = False
|
|
ttw_mod_line = f"+{mod_folder_name}\n"
|
|
new_lines = []
|
|
|
|
for line in lines:
|
|
# Skip existing TTW mod entries (but keep separators and other TTW-related mods)
|
|
# Match patterns: "+[NoDelete] Tale of Two Wastelands", "+[NoDelete] TTW", etc.
|
|
stripped = line.strip()
|
|
if stripped.startswith('+') and '[nodelete]' in stripped.lower():
|
|
# Check if it's the main TTW mod (not other TTW-related mods like "TTW Quick Start")
|
|
if ('tale of two wastelands' in stripped.lower() and 'quick start' not in stripped.lower() and
|
|
'loading wheel' not in stripped.lower()) or stripped.lower().startswith('+[nodelete] ttw '):
|
|
logger.info(f"Removing existing TTW mod entry: {stripped}")
|
|
continue
|
|
|
|
# Insert TTW mod BEFORE the placeholder separator (MO2 order is bottom-up)
|
|
# Check BEFORE appending so TTW mod appears before separator in file
|
|
if "put tale of two wastelands mod here" in line.lower() and "_separator" in line.lower():
|
|
new_lines.append(ttw_mod_line)
|
|
separator_found = True
|
|
logger.info(f"Inserted TTW mod before separator: {line.strip()}")
|
|
|
|
new_lines.append(line)
|
|
|
|
# If no separator found, append at the end
|
|
if not separator_found:
|
|
new_lines.append(ttw_mod_line)
|
|
logger.warning(f"No TTW separator found in {profile_name}, appended to end")
|
|
|
|
# Write back
|
|
with open(modlist_file, 'w', encoding='utf-8') as f:
|
|
f.writelines(new_lines)
|
|
|
|
logger.info(f"Updated modlist.txt for {profile_name}")
|
|
else:
|
|
logger.warning(f"modlist.txt not found for profile {profile_name}")
|
|
|
|
# Update plugins.txt
|
|
plugins_file = profile_dir / "plugins.txt"
|
|
if plugins_file.exists():
|
|
# Read existing plugins
|
|
with open(plugins_file, 'r', encoding='utf-8') as f:
|
|
lines = f.readlines()
|
|
|
|
# Remove any existing TTW ESMs
|
|
ttw_esm_set = set(esm.lower() for esm in ttw_esms)
|
|
lines = [line for line in lines if line.strip().lower() not in ttw_esm_set]
|
|
|
|
# Find CaravanPack.esm and insert TTW ESMs after it
|
|
insert_index = None
|
|
for i, line in enumerate(lines):
|
|
if line.strip().lower() == "caravanpack.esm":
|
|
insert_index = i + 1
|
|
break
|
|
|
|
if insert_index is not None:
|
|
# Insert TTW ESMs in correct order
|
|
for esm in reversed(ttw_esms):
|
|
lines.insert(insert_index, f"{esm}\n")
|
|
else:
|
|
logger.warning(f"CaravanPack.esm not found in {profile_name}, appending TTW ESMs to end")
|
|
for esm in ttw_esms:
|
|
lines.append(f"{esm}\n")
|
|
|
|
# Write back
|
|
with open(plugins_file, 'w', encoding='utf-8') as f:
|
|
f.writelines(lines)
|
|
|
|
logger.info(f"Updated plugins.txt for {profile_name}")
|
|
else:
|
|
logger.warning(f"plugins.txt not found for profile {profile_name}")
|
|
|
|
logger.info("TTW integration completed successfully")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error integrating TTW into modlist: {e}", exc_info=True)
|
|
return False
|