mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 11:37:01 +01:00
Sync from development - prepare for v0.1.7
This commit is contained in:
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,5 +1,30 @@
|
||||
# Jackify Changelog
|
||||
|
||||
## v0.1.7 - TTW Automation & Bug Fixes
|
||||
**Release Date:** November 1, 2025
|
||||
|
||||
### Major Features
|
||||
- **TTW (Tale of Two Wastelands) Installation and Automation**
|
||||
- TTW Installation function using Hoolamike application - https://github.com/Niedzwiedzw/hoolamike
|
||||
- Automated workflow for TTW installation and integration into FNV modlists, where possible
|
||||
- Automatic detection of TTW-compatible modlists
|
||||
- User prompt after modlist installation with option to install TTW
|
||||
- Automated integration: file copying, load order updates, modlist.txt updates
|
||||
- Available in both CLI and GUI workflows
|
||||
|
||||
### Bug Fixes
|
||||
- **Registry UTF-8 Decode Error**: Fixed crash during dotnet4.x installation when Wine outputs binary data
|
||||
- **Python 3.10 Compatibility**: Fixed startup crash on Python 3.10 systems
|
||||
- **TTW Steam Deck Layout**: Fixed window sizing issues on Steam Deck when entering/exiting TTW screen
|
||||
- **TTW Integration Status**: Added visible status banner updates during modlist integration for collapsed mode
|
||||
- **TTW Accidental Input Protection**: Added 3-second countdown to TTW installation prompt to prevent accidental dismissal
|
||||
- **Settings Persistence**: Settings changes now persist correctly across workflows
|
||||
- **Steam Deck Keyboard Input**: Fixed keyboard input failure on Steam Deck
|
||||
- **Application Close Crash**: Fixed crash when closing application on Steam Deck
|
||||
- **Winetricks Diagnostics**: Enhanced error detection with automatic fallback
|
||||
|
||||
---
|
||||
|
||||
## v0.1.6.6 - AppImage Bundling Fix
|
||||
**Release Date:** October 29, 2025
|
||||
|
||||
|
||||
@@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing
|
||||
Wabbajack modlists natively on Linux systems.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.6.6"
|
||||
__version__ = "0.1.7"
|
||||
|
||||
3
jackify/backend/data/__init__.py
Normal file
3
jackify/backend/data/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Data package for static configuration and reference data.
|
||||
"""
|
||||
46
jackify/backend/data/ttw_compatible_modlists.py
Normal file
46
jackify/backend/data/ttw_compatible_modlists.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
TTW-Compatible Modlists Configuration
|
||||
|
||||
Defines which Fallout New Vegas modlists support Tale of Two Wastelands.
|
||||
This whitelist determines when Jackify should offer TTW installation after
|
||||
a successful modlist installation.
|
||||
"""
|
||||
|
||||
TTW_COMPATIBLE_MODLISTS = {
|
||||
# Exact modlist names that support/require TTW
|
||||
"exact_matches": [
|
||||
"Begin Again",
|
||||
"Uranium Fever",
|
||||
"The Badlands",
|
||||
"Wild Card TTW",
|
||||
],
|
||||
|
||||
# Pattern matching for modlist names (regex)
|
||||
"patterns": [
|
||||
r".*TTW.*", # Any modlist with TTW in name
|
||||
r".*Tale.*Two.*Wastelands.*",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def is_ttw_compatible(modlist_name: str) -> bool:
|
||||
"""Check if modlist name matches TTW compatibility criteria
|
||||
|
||||
Args:
|
||||
modlist_name: Name of the modlist to check
|
||||
|
||||
Returns:
|
||||
bool: True if modlist is TTW-compatible, False otherwise
|
||||
"""
|
||||
import re
|
||||
|
||||
# Check exact matches
|
||||
if modlist_name in TTW_COMPATIBLE_MODLISTS['exact_matches']:
|
||||
return True
|
||||
|
||||
# Check pattern matches
|
||||
for pattern in TTW_COMPATIBLE_MODLISTS['patterns']:
|
||||
if re.match(pattern, modlist_name, re.IGNORECASE):
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -20,10 +20,23 @@ logger = logging.getLogger(__name__)
|
||||
class ConfigHandler:
|
||||
"""
|
||||
Handles application configuration and settings
|
||||
Singleton pattern ensures all code shares the same instance
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
_initialized = False
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(ConfigHandler, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize configuration handler with default settings"""
|
||||
# Only initialize once (singleton pattern)
|
||||
if ConfigHandler._initialized:
|
||||
return
|
||||
ConfigHandler._initialized = True
|
||||
|
||||
self.config_dir = os.path.expanduser("~/.config/jackify")
|
||||
self.config_file = os.path.join(self.config_dir, "config.json")
|
||||
self.settings = {
|
||||
@@ -45,13 +58,14 @@ class ConfigHandler:
|
||||
|
||||
# Load configuration if exists
|
||||
self._load_config()
|
||||
|
||||
|
||||
# If steam_path is not set, detect it
|
||||
if not self.settings["steam_path"]:
|
||||
self.settings["steam_path"] = self._detect_steam_path()
|
||||
|
||||
# Auto-detect and set Proton version on first run
|
||||
if not self.settings.get("proton_path"):
|
||||
# Auto-detect and set Proton version ONLY on first run (config file doesn't exist)
|
||||
# Do NOT overwrite user's saved settings!
|
||||
if not os.path.exists(self.config_file) and not self.settings.get("proton_path"):
|
||||
self._auto_detect_proton()
|
||||
|
||||
# If jackify_data_dir is not set, initialize it to default
|
||||
@@ -113,6 +127,10 @@ class ConfigHandler:
|
||||
self._create_config_dir()
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading configuration: {e}")
|
||||
|
||||
def reload_config(self):
|
||||
"""Reload configuration from disk to pick up external changes"""
|
||||
self._load_config()
|
||||
|
||||
def _create_config_dir(self):
|
||||
"""Create configuration directory if it doesn't exist"""
|
||||
|
||||
@@ -15,7 +15,7 @@ from .filesystem_handler import FileSystemHandler
|
||||
from .config_handler import ConfigHandler
|
||||
# Import color constants needed for print statements in this module
|
||||
from .ui_colors import COLOR_ERROR, COLOR_SUCCESS, COLOR_WARNING, COLOR_RESET, COLOR_INFO, COLOR_PROMPT, COLOR_SELECTION
|
||||
# Standard logging (no file handler) - LoggingHandler import removed
|
||||
from .logging_handler import LoggingHandler
|
||||
from .status_utils import show_status, clear_status
|
||||
from .subprocess_utils import get_clean_subprocess_env
|
||||
|
||||
@@ -55,8 +55,10 @@ class HoolamikeHandler:
|
||||
self.filesystem_handler = filesystem_handler
|
||||
self.config_handler = config_handler
|
||||
self.menu_handler = menu_handler
|
||||
# Use standard logging (no file handler)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
# Set up dedicated log file for TTW operations
|
||||
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')
|
||||
|
||||
# --- Discovered/Managed State ---
|
||||
self.game_install_paths: Dict[str, Path] = {}
|
||||
@@ -213,7 +215,7 @@ class HoolamikeHandler:
|
||||
if not self.hoolamike_config.get("games"):
|
||||
f.write("# No games were detected by Jackify. Add game paths manually if needed.\n")
|
||||
# Dump the actual YAML
|
||||
yaml.dump(self.hoolamike_config, f, default_flow_style=False, sort_keys=False)
|
||||
yaml.dump(self.hoolamike_config, f, default_flow_style=False, sort_keys=False, width=float('inf'))
|
||||
self.logger.info("Configuration saved successfully.")
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -224,9 +226,12 @@ class HoolamikeHandler:
|
||||
"""Execute all discovery steps."""
|
||||
self.logger.info("Starting Hoolamike feature discovery phase...")
|
||||
|
||||
# Check if Hoolamike is installed
|
||||
self._check_hoolamike_installation()
|
||||
|
||||
# Detect game paths and update internal state + config
|
||||
self._detect_and_update_game_paths()
|
||||
|
||||
|
||||
self.logger.info("Hoolamike discovery phase complete.")
|
||||
|
||||
def _detect_and_update_game_paths(self):
|
||||
@@ -242,22 +247,143 @@ class HoolamikeHandler:
|
||||
self.logger.debug("Updating loaded hoolamike.yaml with detected game paths.")
|
||||
if "games" not in self.hoolamike_config or not isinstance(self.hoolamike_config.get("games"), dict):
|
||||
self.hoolamike_config["games"] = {} # Ensure games section exists
|
||||
|
||||
|
||||
# Define a unified format for game names in config - no spaces
|
||||
# Clear existing entries first to avoid duplicates
|
||||
self.hoolamike_config["games"] = {}
|
||||
|
||||
|
||||
# Add detected paths with proper formatting - no spaces
|
||||
for game_name, detected_path in detected_paths.items():
|
||||
formatted_name = self._format_game_name(game_name)
|
||||
self.hoolamike_config["games"][formatted_name] = {"root_directory": str(detected_path)}
|
||||
|
||||
|
||||
self.logger.info(f"Updated config with {len(detected_paths)} game paths using correct naming format (no spaces)")
|
||||
|
||||
# Save the updated config to disk so Hoolamike can read it
|
||||
if detected_paths:
|
||||
self.logger.info("Saving updated game paths to hoolamike.yaml")
|
||||
self.save_hoolamike_config()
|
||||
else:
|
||||
self.logger.warning("Cannot update game paths in config because config is not loaded.")
|
||||
|
||||
# --- Methods for Hoolamike Tasks (To be implemented later) ---
|
||||
# TODO: Update these methods to accept necessary parameters and update/save config
|
||||
# --- Methods for Hoolamike Tasks ---
|
||||
# GUI-safe, non-interactive installer used by Install TTW screen
|
||||
def install_hoolamike(self, install_dir: Optional[Path] = None) -> tuple[bool, str]:
|
||||
"""Non-interactive install/update of Hoolamike for GUI usage.
|
||||
|
||||
Downloads the latest Linux x86_64 release from GitHub, extracts it to the
|
||||
Jackify-managed directory (~/Jackify/Hoolamike by default or provided install_dir),
|
||||
sets executable permissions, and saves the install path to Jackify config.
|
||||
|
||||
Returns:
|
||||
(success, message)
|
||||
"""
|
||||
try:
|
||||
self._ensure_hoolamike_dirs_exist()
|
||||
# Determine target install directory
|
||||
target_dir = Path(install_dir) if install_dir else self.hoolamike_app_install_path
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Fetch latest release info
|
||||
release_url = "https://api.github.com/repos/Niedzwiedzw/hoolamike/releases/latest"
|
||||
self.logger.info(f"Fetching latest Hoolamike release info from {release_url}")
|
||||
resp = requests.get(release_url, timeout=15, verify=True)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
release_tag = data.get("tag_name") or data.get("name")
|
||||
|
||||
linux_asset = None
|
||||
for asset in data.get("assets", []):
|
||||
name = asset.get("name", "").lower()
|
||||
if "linux" in name and (name.endswith(".tar.gz") or name.endswith(".tgz") or name.endswith(".zip")) and ("x86_64" in name or "amd64" in name):
|
||||
linux_asset = asset
|
||||
break
|
||||
|
||||
if not linux_asset:
|
||||
return False, "No suitable Linux x86_64 Hoolamike asset found in latest release"
|
||||
|
||||
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
|
||||
if not self.filesystem_handler.download_file(download_url, temp_path, overwrite=True, quiet=True):
|
||||
return False, "Failed to download Hoolamike asset"
|
||||
|
||||
# Extract
|
||||
try:
|
||||
if asset_name.lower().endswith((".tar.gz", ".tgz")):
|
||||
with tarfile.open(temp_path, "r:*") as tar:
|
||||
tar.extractall(path=target_dir)
|
||||
elif asset_name.lower().endswith(".zip"):
|
||||
with zipfile.ZipFile(temp_path, "r") as zf:
|
||||
zf.extractall(target_dir)
|
||||
else:
|
||||
return False, f"Unknown archive format: {asset_name}"
|
||||
finally:
|
||||
try:
|
||||
temp_path.unlink(missing_ok=True) # cleanup
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Ensure executable bit on binary
|
||||
exe_path = target_dir / HOOLAMIKE_EXECUTABLE_NAME
|
||||
if not exe_path.is_file():
|
||||
# Some archives may include a subfolder; try to locate the binary
|
||||
for p in target_dir.rglob(HOOLAMIKE_EXECUTABLE_NAME):
|
||||
if p.is_file():
|
||||
exe_path = p
|
||||
break
|
||||
if not exe_path.is_file():
|
||||
return False, "Hoolamike binary not found after extraction"
|
||||
try:
|
||||
os.chmod(exe_path, 0o755)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to chmod +x on {exe_path}: {e}")
|
||||
|
||||
# Mark installed and persist path
|
||||
self.hoolamike_app_install_path = target_dir
|
||||
self.hoolamike_executable_path = exe_path
|
||||
self.hoolamike_installed = True
|
||||
self.config_handler.set('hoolamike_install_path', str(target_dir))
|
||||
if release_tag:
|
||||
self.config_handler.set('hoolamike_version', str(release_tag))
|
||||
self.config_handler.save_config()
|
||||
|
||||
return True, f"Hoolamike installed at {target_dir}"
|
||||
except Exception as e:
|
||||
self.logger.error("Hoolamike installation failed", exc_info=True)
|
||||
return False, f"Error installing Hoolamike: {e}"
|
||||
|
||||
def get_installed_hoolamike_version(self) -> Optional[str]:
|
||||
"""Return the installed Hoolamike version stored in Jackify config, if any."""
|
||||
try:
|
||||
v = self.config_handler.get('hoolamike_version')
|
||||
return str(v) if v else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def is_hoolamike_update_available(self) -> tuple[bool, Optional[str], Optional[str]]:
|
||||
"""
|
||||
Check GitHub for the latest Hoolamike release and compare with installed version.
|
||||
Returns (update_available, installed_version, latest_version).
|
||||
"""
|
||||
installed = self.get_installed_hoolamike_version()
|
||||
try:
|
||||
release_url = "https://api.github.com/repos/Niedzwiedzw/hoolamike/releases/latest"
|
||||
resp = requests.get(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 but installed may exist; treat as update available
|
||||
return (True, None, latest)
|
||||
return (installed != str(latest), installed, str(latest))
|
||||
except Exception:
|
||||
return (False, installed, None)
|
||||
|
||||
def install_update_hoolamike(self, context=None) -> bool:
|
||||
"""Install or update Hoolamike application.
|
||||
@@ -654,18 +780,89 @@ class HoolamikeHandler:
|
||||
input(f"{COLOR_PROMPT}Press Enter to return to the Hoolamike menu...{COLOR_RESET}")
|
||||
return False
|
||||
|
||||
def install_ttw(self, ttw_mpi_path=None, ttw_output_path=None, context=None):
|
||||
"""Install Tale of Two Wastelands (TTW) using Hoolamike.
|
||||
def install_ttw_backend(self, ttw_mpi_path, ttw_output_path):
|
||||
"""Clean backend function for TTW installation - no user interaction.
|
||||
|
||||
Args:
|
||||
ttw_mpi_path: Path to the TTW installer .mpi file
|
||||
ttw_output_path: Target installation directory for TTW
|
||||
ttw_mpi_path: Path to the TTW installer .mpi file (required)
|
||||
ttw_output_path: Target installation directory for TTW (required)
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, message: str)
|
||||
"""
|
||||
self.logger.info(f"Starting Tale of Two Wastelands installation via Hoolamike")
|
||||
|
||||
# Validate required 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"
|
||||
|
||||
# Convert to Path objects
|
||||
ttw_mpi_path = Path(ttw_mpi_path)
|
||||
ttw_output_path = Path(ttw_output_path)
|
||||
|
||||
# Validate paths exist
|
||||
if not ttw_mpi_path.exists():
|
||||
return False, f"TTW .mpi file not found: {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 Hoolamike installation
|
||||
self._check_hoolamike_installation()
|
||||
|
||||
# Ensure config is loaded
|
||||
if self.hoolamike_config is None:
|
||||
loaded = self._load_hoolamike_config()
|
||||
if not loaded or self.hoolamike_config is None:
|
||||
self.logger.error("Failed to load or generate hoolamike.yaml configuration.")
|
||||
return False, "Failed to load or generate Hoolamike configuration"
|
||||
|
||||
# Verify required games are detected
|
||||
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:
|
||||
self.logger.error(f"Missing required games for TTW installation: {', '.join(missing_games)}")
|
||||
return False, f"Missing required games: {', '.join(missing_games)}. TTW requires both Fallout 3 and Fallout New Vegas."
|
||||
|
||||
# Update TTW configuration
|
||||
self._update_hoolamike_config_for_ttw(ttw_mpi_path, ttw_output_path)
|
||||
if not self.save_hoolamike_config():
|
||||
self.logger.error("Failed to save hoolamike.yaml configuration.")
|
||||
return False, "Failed to save Hoolamike configuration"
|
||||
|
||||
# Construct and execute command
|
||||
cmd = [
|
||||
str(self.hoolamike_executable_path),
|
||||
"tale-of-two-wastelands"
|
||||
]
|
||||
self.logger.info(f"Executing Hoolamike command: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
ret = subprocess.call(cmd, cwd=str(self.hoolamike_app_install_path), env=get_clean_subprocess_env())
|
||||
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 Hoolamike TTW installation: {e}", exc_info=True)
|
||||
return False, f"Error executing Hoolamike TTW installation: {e}"
|
||||
|
||||
def install_ttw(self, ttw_mpi_path=None, ttw_output_path=None, context=None):
|
||||
"""CLI interface for TTW installation - handles user interaction and calls backend.
|
||||
|
||||
Args:
|
||||
ttw_mpi_path: Path to the TTW installer .mpi file (optional for CLI)
|
||||
ttw_output_path: Target installation directory for TTW (optional for CLI)
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
self.logger.info(f"Starting Tale of Two Wastelands installation via Hoolamike")
|
||||
self._check_hoolamike_installation()
|
||||
menu = self.menu_handler
|
||||
print(f"\n{'='*60}")
|
||||
print(f"{COLOR_INFO}Hoolamike: Tale of Two Wastelands Installation{COLOR_RESET}")
|
||||
@@ -676,123 +873,90 @@ class HoolamikeHandler:
|
||||
print(f" • You must provide the path to your TTW .mpi installer file.")
|
||||
print(f" • You must select an output directory for the TTW install.\n")
|
||||
|
||||
# Ensure config is loaded
|
||||
if self.hoolamike_config is None:
|
||||
loaded = self._load_hoolamike_config()
|
||||
if not loaded or self.hoolamike_config is None:
|
||||
self.logger.error("Failed to load or generate hoolamike.yaml configuration.")
|
||||
print(f"{COLOR_ERROR}Error: Could not load or generate Hoolamike configuration. Aborting TTW install.{COLOR_RESET}")
|
||||
return False
|
||||
|
||||
# Verify required games are in configuration
|
||||
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:
|
||||
self.logger.error(f"Missing required games for TTW installation: {', '.join(missing_games)}")
|
||||
print(f"{COLOR_ERROR}Error: The following required games were not found: {', '.join(missing_games)}{COLOR_RESET}")
|
||||
print("TTW requires both Fallout 3 and Fallout New Vegas to be installed.")
|
||||
return False
|
||||
|
||||
# Prompt for TTW .mpi file
|
||||
print(f"{COLOR_INFO}Please provide the path to your TTW .mpi installer file.{COLOR_RESET}")
|
||||
print(f"You can download this from: {COLOR_INFO}https://mod.pub/ttw/133/files{COLOR_RESET}")
|
||||
print(f"(Extract the .mpi file from the downloaded archive.)\n")
|
||||
while not ttw_mpi_path:
|
||||
candidate = menu.get_existing_file_path(
|
||||
prompt_message="Enter the path to your TTW .mpi file (or 'q' to cancel):",
|
||||
extension_filter=".mpi",
|
||||
no_header=True
|
||||
)
|
||||
if candidate is None:
|
||||
# If parameters provided, use them directly
|
||||
if ttw_mpi_path and ttw_output_path:
|
||||
print(f"{COLOR_INFO}Using provided parameters:{COLOR_RESET}")
|
||||
print(f"- TTW .mpi file: {ttw_mpi_path}")
|
||||
print(f"- Output directory: {ttw_output_path}")
|
||||
print(f"{COLOR_WARNING}Proceed with these settings and start TTW installation? (This can take MANY HOURS){COLOR_RESET}")
|
||||
confirm = input(f"{COLOR_PROMPT}[Y/n]: {COLOR_RESET}").strip().lower()
|
||||
if confirm and not confirm.startswith('y'):
|
||||
print(f"{COLOR_WARNING}Cancelled by user.{COLOR_RESET}")
|
||||
return False
|
||||
if str(candidate).strip().lower() == 'q':
|
||||
else:
|
||||
# Interactive mode - collect user input
|
||||
print(f"{COLOR_INFO}Please provide the path to your TTW .mpi installer file.{COLOR_RESET}")
|
||||
print(f"You can download this from: {COLOR_INFO}https://mod.pub/ttw/133/files{COLOR_RESET}")
|
||||
print(f"(Extract the .mpi file from the downloaded archive.)\n")
|
||||
while not ttw_mpi_path:
|
||||
candidate = menu.get_existing_file_path(
|
||||
prompt_message="Enter the path to your TTW .mpi file (or 'q' to cancel):",
|
||||
extension_filter=".mpi",
|
||||
no_header=True
|
||||
)
|
||||
if candidate is None:
|
||||
print(f"{COLOR_WARNING}Cancelled by user.{COLOR_RESET}")
|
||||
return False
|
||||
if str(candidate).strip().lower() == 'q':
|
||||
print(f"{COLOR_WARNING}Cancelled by user.{COLOR_RESET}")
|
||||
return False
|
||||
ttw_mpi_path = candidate
|
||||
|
||||
# Prompt for output directory
|
||||
print(f"\n{COLOR_INFO}Please select the output directory where TTW will be installed.{COLOR_RESET}")
|
||||
print(f"(This should be an empty or new directory.)\n")
|
||||
while not ttw_output_path:
|
||||
ttw_output_path = menu.get_directory_path(
|
||||
prompt_message="Select the TTW output directory:",
|
||||
default_path=self.hoolamike_app_install_path / "TTW_Output",
|
||||
create_if_missing=True,
|
||||
no_header=False
|
||||
)
|
||||
if not ttw_output_path:
|
||||
print(f"{COLOR_WARNING}Cancelled by user.{COLOR_RESET}")
|
||||
return False
|
||||
if ttw_output_path.exists() and any(ttw_output_path.iterdir()):
|
||||
print(f"{COLOR_WARNING}Warning: The selected directory '{ttw_output_path}' already exists and is not empty. Its contents may be overwritten!{COLOR_RESET}")
|
||||
confirm = input(f"{COLOR_PROMPT}This directory is not empty and may be overwritten. Proceed? (y/N): {COLOR_RESET}").strip().lower()
|
||||
if not confirm.startswith('y'):
|
||||
print(f"{COLOR_INFO}Please select a different directory.\n{COLOR_RESET}")
|
||||
ttw_output_path = None
|
||||
continue
|
||||
|
||||
# Summary & Confirmation
|
||||
print(f"\n{'-'*60}")
|
||||
print(f"{COLOR_INFO}Summary of configuration:{COLOR_RESET}")
|
||||
print(f"- TTW .mpi file: {ttw_mpi_path}")
|
||||
print(f"- Output directory: {ttw_output_path}")
|
||||
print(f"{'-'*60}")
|
||||
print(f"{COLOR_WARNING}Proceed with these settings and start TTW installation? (This can take MANY HOURS){COLOR_RESET}")
|
||||
confirm = input(f"{COLOR_PROMPT}[Y/n]: {COLOR_RESET}").strip().lower()
|
||||
if confirm and not confirm.startswith('y'):
|
||||
print(f"{COLOR_WARNING}Cancelled by user.{COLOR_RESET}")
|
||||
return False
|
||||
ttw_mpi_path = candidate
|
||||
|
||||
# Prompt for output directory
|
||||
print(f"\n{COLOR_INFO}Please select the output directory where TTW will be installed.{COLOR_RESET}")
|
||||
print(f"(This should be an empty or new directory.)\n")
|
||||
while not ttw_output_path:
|
||||
ttw_output_path = menu.get_directory_path(
|
||||
prompt_message="Select the TTW output directory:",
|
||||
default_path=self.hoolamike_app_install_path / "TTW_Output",
|
||||
create_if_missing=True,
|
||||
no_header=False
|
||||
)
|
||||
if not ttw_output_path:
|
||||
print(f"{COLOR_WARNING}Cancelled by user.{COLOR_RESET}")
|
||||
return False
|
||||
if ttw_output_path.exists() and any(ttw_output_path.iterdir()):
|
||||
print(f"{COLOR_WARNING}Warning: The selected directory '{ttw_output_path}' already exists and is not empty. Its contents may be overwritten!{COLOR_RESET}")
|
||||
confirm = input(f"{COLOR_PROMPT}This directory is not empty and may be overwritten. Proceed? (y/N): {COLOR_RESET}").strip().lower()
|
||||
if not confirm.startswith('y'):
|
||||
print(f"{COLOR_INFO}Please select a different directory.\n{COLOR_RESET}")
|
||||
ttw_output_path = None
|
||||
continue
|
||||
# Call the clean backend function
|
||||
success, message = self.install_ttw_backend(ttw_mpi_path, ttw_output_path)
|
||||
|
||||
# --- Summary & Confirmation ---
|
||||
print(f"\n{'-'*60}")
|
||||
print(f"{COLOR_INFO}Summary of configuration:{COLOR_RESET}")
|
||||
print(f"- TTW .mpi file: {ttw_mpi_path}")
|
||||
print(f"- Output directory: {ttw_output_path}")
|
||||
print("- Games:")
|
||||
for game in required_games:
|
||||
found = detected_games.get(game)
|
||||
print(f" {game}: {found if found else 'Not Found'}")
|
||||
print(f"{'-'*60}")
|
||||
print(f"{COLOR_WARNING}Proceed with these settings and start TTW installation? (This can take MANY HOURS){COLOR_RESET}")
|
||||
confirm = input(f"{COLOR_PROMPT}[Y/n]: {COLOR_RESET}").strip().lower()
|
||||
if confirm and not confirm.startswith('y'):
|
||||
print(f"{COLOR_WARNING}Cancelled by user.{COLOR_RESET}")
|
||||
return False
|
||||
if success:
|
||||
print(f"\n{COLOR_SUCCESS}{message}{COLOR_RESET}")
|
||||
|
||||
# --- Always re-detect games before updating config ---
|
||||
detected_games = self.path_handler.find_vanilla_game_paths()
|
||||
if not detected_games:
|
||||
print(f"{COLOR_ERROR}No supported games were detected on your system. TTW requires Fallout 3 and Fallout New Vegas to be installed.{COLOR_RESET}")
|
||||
return False
|
||||
# Update the games section with correct keys
|
||||
if self.hoolamike_config is None:
|
||||
self.hoolamike_config = {}
|
||||
self.hoolamike_config['games'] = {
|
||||
self._format_game_name(game): {"root_directory": str(path)}
|
||||
for game, path in detected_games.items()
|
||||
}
|
||||
# Offer to create MO2 zip archive
|
||||
print(f"\n{COLOR_INFO}Would you like to create a zipped mod archive for MO2?{COLOR_RESET}")
|
||||
print(f"This will package the TTW files for easy installation into Mod Organizer 2.")
|
||||
create_zip = input(f"{COLOR_PROMPT}Create zip archive? [Y/n]: {COLOR_RESET}").strip().lower()
|
||||
|
||||
# Update TTW configuration
|
||||
self._update_hoolamike_config_for_ttw(ttw_mpi_path, ttw_output_path)
|
||||
if not self.save_hoolamike_config():
|
||||
self.logger.error("Failed to save hoolamike.yaml configuration.")
|
||||
print(f"{COLOR_ERROR}Error: Failed to save Hoolamike configuration.{COLOR_RESET}")
|
||||
print("Attempting to continue anyway...")
|
||||
|
||||
# Construct command to execute
|
||||
cmd = [
|
||||
str(self.hoolamike_executable_path),
|
||||
"tale-of-two-wastelands"
|
||||
]
|
||||
self.logger.info(f"Executing Hoolamike command: {' '.join(cmd)}")
|
||||
print(f"\n{COLOR_INFO}Executing Hoolamike for TTW Installation...{COLOR_RESET}")
|
||||
print(f"Command: {' '.join(cmd)}")
|
||||
print(f"{COLOR_INFO}Streaming output below. Press Ctrl+C to cancel and return to Jackify menu.{COLOR_RESET}\n")
|
||||
try:
|
||||
ret = subprocess.call(cmd, cwd=str(self.hoolamike_app_install_path), env=get_clean_subprocess_env())
|
||||
if ret == 0:
|
||||
self.logger.info("TTW installation completed successfully.")
|
||||
print(f"\n{COLOR_SUCCESS}TTW installation completed successfully!{COLOR_RESET}")
|
||||
input(f"{COLOR_PROMPT}Press Enter to return to the Hoolamike menu...{COLOR_RESET}")
|
||||
return True
|
||||
if not create_zip or create_zip.startswith('y'):
|
||||
zip_success = self._create_ttw_mod_archive_cli(ttw_mpi_path, ttw_output_path)
|
||||
if not zip_success:
|
||||
print(f"\n{COLOR_WARNING}Archive creation failed, but TTW installation completed successfully.{COLOR_RESET}")
|
||||
else:
|
||||
self.logger.error(f"TTW installation process returned non-zero exit code: {ret}")
|
||||
print(f"\n{COLOR_ERROR}Error: TTW installation failed with exit code {ret}.{COLOR_RESET}")
|
||||
input(f"{COLOR_PROMPT}Press Enter to return to the Hoolamike menu...{COLOR_RESET}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error executing Hoolamike TTW installation: {e}", exc_info=True)
|
||||
print(f"\n{COLOR_ERROR}Error executing Hoolamike TTW installation: {e}{COLOR_RESET}")
|
||||
print(f"\n{COLOR_INFO}Skipping archive creation. You can manually use the TTW files from the output directory.{COLOR_RESET}")
|
||||
|
||||
input(f"\n{COLOR_PROMPT}Press Enter to return to the Hoolamike menu...{COLOR_RESET}")
|
||||
return True
|
||||
else:
|
||||
print(f"\n{COLOR_ERROR}{message}{COLOR_RESET}")
|
||||
input(f"{COLOR_PROMPT}Press Enter to return to the Hoolamike menu...{COLOR_RESET}")
|
||||
return False
|
||||
|
||||
@@ -818,27 +982,125 @@ class HoolamikeHandler:
|
||||
# Set destination variable
|
||||
ttw_config["variables"]["DESTINATION"] = str(ttw_output_path)
|
||||
|
||||
# Set USERPROFILE to a Jackify-managed directory for TTW
|
||||
userprofile_path = str(self.hoolamike_app_install_path / "USERPROFILE")
|
||||
# Set USERPROFILE to Fallout New Vegas Wine prefix Documents folder
|
||||
userprofile_path = self._detect_fallout_nv_userprofile()
|
||||
if "variables" not in self.hoolamike_config["extras"]["tale_of_two_wastelands"]:
|
||||
self.hoolamike_config["extras"]["tale_of_two_wastelands"]["variables"] = {}
|
||||
self.hoolamike_config["extras"]["tale_of_two_wastelands"]["variables"]["USERPROFILE"] = userprofile_path
|
||||
|
||||
# Make sure game paths are set correctly
|
||||
# Make sure game paths are set correctly using proper Hoolamike naming format
|
||||
for game in ['Fallout 3', 'Fallout New Vegas']:
|
||||
if game in self.game_install_paths:
|
||||
game_key = game.replace(' ', '').lower()
|
||||
|
||||
# Use _format_game_name to ensure correct naming (removes spaces)
|
||||
formatted_game_name = self._format_game_name(game)
|
||||
|
||||
if "games" not in self.hoolamike_config:
|
||||
self.hoolamike_config["games"] = {}
|
||||
|
||||
if game not in self.hoolamike_config["games"]:
|
||||
self.hoolamike_config["games"][game] = {}
|
||||
|
||||
self.hoolamike_config["games"][game]["root_directory"] = str(self.game_install_paths[game])
|
||||
|
||||
|
||||
if formatted_game_name not in self.hoolamike_config["games"]:
|
||||
self.hoolamike_config["games"][formatted_game_name] = {}
|
||||
|
||||
self.hoolamike_config["games"][formatted_game_name]["root_directory"] = str(self.game_install_paths[game])
|
||||
|
||||
self.logger.info("Updated Hoolamike configuration with TTW settings.")
|
||||
|
||||
def _create_ttw_mod_archive_cli(self, ttw_mpi_path: Path, ttw_output_path: Path) -> bool:
|
||||
"""Create a zipped mod archive of TTW output for MO2 installation (CLI version).
|
||||
|
||||
Args:
|
||||
ttw_mpi_path: Path to the TTW .mpi file (used for version extraction)
|
||||
ttw_output_path: Path to the TTW output directory to archive
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
import shutil
|
||||
import re
|
||||
|
||||
if not ttw_output_path.exists():
|
||||
print(f"{COLOR_ERROR}Output directory does not exist: {ttw_output_path}{COLOR_RESET}")
|
||||
return False
|
||||
|
||||
# Extract version from .mpi filename (e.g., "TTW v3.4.mpi" -> "3.4")
|
||||
version_suffix = ""
|
||||
if ttw_mpi_path:
|
||||
mpi_filename = ttw_mpi_path.stem # Get filename without extension
|
||||
# Look for version pattern like "3.4", "v3.4", etc.
|
||||
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 - [NoDelete] prefix is used by MO2 workflows
|
||||
archive_name = f"[NoDelete] Tale of Two Wastelands{version_suffix}"
|
||||
|
||||
# Place archive in parent directory of output
|
||||
archive_path = ttw_output_path.parent / archive_name
|
||||
|
||||
print(f"\n{COLOR_INFO}Creating mod archive: {archive_name}.zip{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}This may take several minutes...{COLOR_RESET}")
|
||||
|
||||
# Create the zip archive
|
||||
# shutil.make_archive returns the path without .zip extension
|
||||
final_archive = shutil.make_archive(
|
||||
str(archive_path), # base name (without extension)
|
||||
'zip', # format
|
||||
str(ttw_output_path) # directory to archive
|
||||
)
|
||||
|
||||
print(f"\n{COLOR_SUCCESS}Archive created successfully: {Path(final_archive).name}{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}Location: {final_archive}{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}You can now install this archive as a mod in MO2.{COLOR_RESET}")
|
||||
|
||||
self.logger.info(f"Created TTW mod archive: {final_archive}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n{COLOR_ERROR}Failed to create mod archive: {e}{COLOR_RESET}")
|
||||
self.logger.error(f"Failed to create TTW mod archive: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def _detect_fallout_nv_userprofile(self) -> str:
|
||||
"""
|
||||
Detect the Fallout New Vegas Wine prefix Documents folder for USERPROFILE.
|
||||
|
||||
Returns:
|
||||
str: Path to the Fallout New Vegas Wine prefix Documents folder,
|
||||
or fallback to Jackify-managed directory if not found.
|
||||
"""
|
||||
try:
|
||||
# Fallout New Vegas AppID
|
||||
fnv_appid = "22380"
|
||||
|
||||
# Find the compatdata directory for Fallout New Vegas
|
||||
compatdata_path = self.path_handler.find_compat_data(fnv_appid)
|
||||
if not compatdata_path:
|
||||
self.logger.warning(f"Could not find compatdata directory for Fallout New Vegas (AppID: {fnv_appid})")
|
||||
# Fallback to Jackify-managed directory
|
||||
fallback_path = str(self.hoolamike_app_install_path / "USERPROFILE")
|
||||
self.logger.info(f"Using fallback USERPROFILE path: {fallback_path}")
|
||||
return fallback_path
|
||||
|
||||
# Construct the Wine prefix Documents path
|
||||
wine_documents_path = compatdata_path / "pfx" / "drive_c" / "users" / "steamuser" / "Documents" / "My Games" / "FalloutNV"
|
||||
|
||||
if wine_documents_path.exists():
|
||||
self.logger.info(f"Found Fallout New Vegas Wine prefix Documents folder: {wine_documents_path}")
|
||||
return str(wine_documents_path)
|
||||
else:
|
||||
self.logger.warning(f"Fallout New Vegas Wine prefix Documents folder not found at: {wine_documents_path}")
|
||||
# Fallback to Jackify-managed directory
|
||||
fallback_path = str(self.hoolamike_app_install_path / "USERPROFILE")
|
||||
self.logger.info(f"Using fallback USERPROFILE path: {fallback_path}")
|
||||
return fallback_path
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error detecting Fallout New Vegas USERPROFILE: {e}", exc_info=True)
|
||||
# Fallback to Jackify-managed directory
|
||||
fallback_path = str(self.hoolamike_app_install_path / "USERPROFILE")
|
||||
self.logger.info(f"Using fallback USERPROFILE path: {fallback_path}")
|
||||
return fallback_path
|
||||
|
||||
def reset_config(self):
|
||||
"""Resets the hoolamike.yaml to default settings, backing up any existing file."""
|
||||
if self.hoolamike_config_path.is_file():
|
||||
@@ -973,6 +1235,165 @@ class HoolamikeHandler:
|
||||
self.logger.error(f"Error launching or waiting for editor: {e}")
|
||||
print(f"{COLOR_ERROR}An error occurred while launching the editor: {e}{COLOR_RESET}")
|
||||
|
||||
@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
|
||||
import re
|
||||
|
||||
# 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"Failed to integrate TTW into modlist: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
# Example usage (for testing, remove later)
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
|
||||
@@ -689,25 +689,6 @@ class ModlistHandler:
|
||||
return False
|
||||
self.logger.info("Step 3: Curated user.reg.modlist and system.reg.modlist applied successfully.")
|
||||
|
||||
# Step 3.5: Apply universal dotnet4.x compatibility registry fixes
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Applying universal dotnet4.x compatibility fixes")
|
||||
self.logger.info("Step 3.5: Applying universal dotnet4.x compatibility registry fixes...")
|
||||
registry_success = False
|
||||
try:
|
||||
registry_success = self._apply_universal_dotnet_fixes()
|
||||
except Exception as e:
|
||||
self.logger.error(f"CRITICAL: Registry fixes failed - modlist may have .NET compatibility issues: {e}")
|
||||
registry_success = False
|
||||
|
||||
if not registry_success:
|
||||
self.logger.error("=" * 80)
|
||||
self.logger.error("WARNING: Universal dotnet4.x registry fixes FAILED!")
|
||||
self.logger.error("This modlist may experience .NET Framework compatibility issues.")
|
||||
self.logger.error("Consider manually setting mscoree=native in winecfg if problems occur.")
|
||||
self.logger.error("=" * 80)
|
||||
# Continue but user should be aware of potential issues
|
||||
|
||||
# Step 4: Install Wine Components
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Installing Wine components (this may take a while)")
|
||||
@@ -751,6 +732,26 @@ class ModlistHandler:
|
||||
return False
|
||||
self.logger.info("Step 4: Installing Wine components... Done")
|
||||
|
||||
# Step 4.5: Apply universal dotnet4.x compatibility registry fixes AFTER wine components
|
||||
# This ensures the fixes are not overwritten by component installation processes
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Applying universal dotnet4.x compatibility fixes")
|
||||
self.logger.info("Step 4.5: Applying universal dotnet4.x compatibility registry fixes...")
|
||||
registry_success = False
|
||||
try:
|
||||
registry_success = self._apply_universal_dotnet_fixes()
|
||||
except Exception as e:
|
||||
self.logger.error(f"CRITICAL: Registry fixes failed - modlist may have .NET compatibility issues: {e}")
|
||||
registry_success = False
|
||||
|
||||
if not registry_success:
|
||||
self.logger.error("=" * 80)
|
||||
self.logger.error("WARNING: Universal dotnet4.x registry fixes FAILED!")
|
||||
self.logger.error("This modlist may experience .NET Framework compatibility issues.")
|
||||
self.logger.error("Consider manually setting mscoree=native in winecfg if problems occur.")
|
||||
self.logger.error("=" * 80)
|
||||
# Continue but user should be aware of potential issues
|
||||
|
||||
# Step 5: Ensure permissions of Modlist directory
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Setting ownership and permissions for modlist directory")
|
||||
@@ -1528,14 +1529,18 @@ class ModlistHandler:
|
||||
return False
|
||||
|
||||
def _apply_universal_dotnet_fixes(self):
|
||||
"""Apply universal dotnet4.x compatibility registry fixes to ALL modlists"""
|
||||
"""
|
||||
Apply universal dotnet4.x compatibility registry fixes to ALL modlists.
|
||||
Now called AFTER wine component installation to prevent overwrites.
|
||||
Includes wineserver shutdown/flush to ensure persistence.
|
||||
"""
|
||||
try:
|
||||
prefix_path = os.path.join(str(self.compat_data_path), "pfx")
|
||||
if not os.path.exists(prefix_path):
|
||||
self.logger.warning(f"Prefix path not found: {prefix_path}")
|
||||
return False
|
||||
|
||||
self.logger.info("Applying universal dotnet4.x compatibility registry fixes...")
|
||||
self.logger.info("Applying universal dotnet4.x compatibility registry fixes (post-component installation)...")
|
||||
|
||||
# Find the appropriate Wine binary to use for registry operations
|
||||
wine_binary = self._find_wine_binary_for_registry()
|
||||
@@ -1543,11 +1548,27 @@ class ModlistHandler:
|
||||
self.logger.error("Could not find Wine binary for registry operations")
|
||||
return False
|
||||
|
||||
# Find wineserver binary for flushing registry changes
|
||||
wine_dir = os.path.dirname(wine_binary)
|
||||
wineserver_binary = os.path.join(wine_dir, 'wineserver')
|
||||
if not os.path.exists(wineserver_binary):
|
||||
self.logger.warning(f"wineserver not found at {wineserver_binary}, registry flush may not work")
|
||||
wineserver_binary = None
|
||||
|
||||
# Set environment for Wine registry operations
|
||||
env = os.environ.copy()
|
||||
env['WINEPREFIX'] = prefix_path
|
||||
env['WINEDEBUG'] = '-all' # Suppress Wine debug output
|
||||
|
||||
# Shutdown any running wineserver processes to ensure clean slate
|
||||
if wineserver_binary:
|
||||
self.logger.debug("Shutting down wineserver before applying registry fixes...")
|
||||
try:
|
||||
subprocess.run([wineserver_binary, '-w'], env=env, timeout=30, capture_output=True)
|
||||
self.logger.debug("Wineserver shutdown complete")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Wineserver shutdown failed (non-critical): {e}")
|
||||
|
||||
# Registry fix 1: Set mscoree=native DLL override
|
||||
# This tells Wine to use native .NET runtime instead of Wine's implementation
|
||||
self.logger.debug("Setting mscoree=native DLL override...")
|
||||
@@ -1557,7 +1578,7 @@ class ModlistHandler:
|
||||
'/v', 'mscoree', '/t', 'REG_SZ', '/d', 'native', '/f'
|
||||
]
|
||||
|
||||
result1 = subprocess.run(cmd1, env=env, capture_output=True, text=True)
|
||||
result1 = subprocess.run(cmd1, env=env, capture_output=True, text=True, errors='replace', timeout=30)
|
||||
if result1.returncode == 0:
|
||||
self.logger.info("Successfully applied mscoree=native DLL override")
|
||||
else:
|
||||
@@ -1572,18 +1593,57 @@ class ModlistHandler:
|
||||
'/v', 'OnlyUseLatestCLR', '/t', 'REG_DWORD', '/d', '1', '/f'
|
||||
]
|
||||
|
||||
result2 = subprocess.run(cmd2, env=env, capture_output=True, text=True)
|
||||
result2 = subprocess.run(cmd2, env=env, capture_output=True, text=True, errors='replace', timeout=30)
|
||||
if result2.returncode == 0:
|
||||
self.logger.info("Successfully applied OnlyUseLatestCLR=1 registry entry")
|
||||
else:
|
||||
self.logger.warning(f"Failed to set OnlyUseLatestCLR: {result2.stderr}")
|
||||
|
||||
# Both fixes applied - this should eliminate dotnet4.x installation requirements
|
||||
if result1.returncode == 0 and result2.returncode == 0:
|
||||
self.logger.info("Universal dotnet4.x compatibility fixes applied successfully")
|
||||
# Force wineserver to flush registry changes to disk
|
||||
if wineserver_binary:
|
||||
self.logger.debug("Flushing registry changes to disk via wineserver shutdown...")
|
||||
try:
|
||||
subprocess.run([wineserver_binary, '-w'], env=env, timeout=30, capture_output=True)
|
||||
self.logger.debug("Registry changes flushed to disk")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Registry flush failed (non-critical): {e}")
|
||||
|
||||
# VERIFICATION: Confirm the registry entries persisted
|
||||
self.logger.info("Verifying registry entries were applied and persisted...")
|
||||
verification_passed = True
|
||||
|
||||
# Verify mscoree=native
|
||||
verify_cmd1 = [
|
||||
wine_binary, 'reg', 'query',
|
||||
'HKEY_CURRENT_USER\\Software\\Wine\\DllOverrides',
|
||||
'/v', 'mscoree'
|
||||
]
|
||||
verify_result1 = subprocess.run(verify_cmd1, env=env, capture_output=True, text=True, errors='replace', timeout=30)
|
||||
if verify_result1.returncode == 0 and 'native' in verify_result1.stdout:
|
||||
self.logger.info("VERIFIED: mscoree=native is set correctly")
|
||||
else:
|
||||
self.logger.error(f"VERIFICATION FAILED: mscoree=native not found in registry. Query output: {verify_result1.stdout}")
|
||||
verification_passed = False
|
||||
|
||||
# Verify OnlyUseLatestCLR=1
|
||||
verify_cmd2 = [
|
||||
wine_binary, 'reg', 'query',
|
||||
'HKEY_LOCAL_MACHINE\\Software\\Microsoft\\.NETFramework',
|
||||
'/v', 'OnlyUseLatestCLR'
|
||||
]
|
||||
verify_result2 = subprocess.run(verify_cmd2, env=env, capture_output=True, text=True, errors='replace', timeout=30)
|
||||
if verify_result2.returncode == 0 and ('0x1' in verify_result2.stdout or 'REG_DWORD' in verify_result2.stdout):
|
||||
self.logger.info("VERIFIED: OnlyUseLatestCLR=1 is set correctly")
|
||||
else:
|
||||
self.logger.error(f"VERIFICATION FAILED: OnlyUseLatestCLR=1 not found in registry. Query output: {verify_result2.stdout}")
|
||||
verification_passed = False
|
||||
|
||||
# Both fixes applied and verified
|
||||
if result1.returncode == 0 and result2.returncode == 0 and verification_passed:
|
||||
self.logger.info("Universal dotnet4.x compatibility fixes applied, flushed, and verified successfully")
|
||||
return True
|
||||
else:
|
||||
self.logger.warning("Some dotnet4.x registry fixes failed, but continuing...")
|
||||
self.logger.error("Registry fixes failed verification - fixes may not persist across prefix restarts")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -945,6 +945,9 @@ class ModlistInstallCLI:
|
||||
|
||||
if configuration_success:
|
||||
self.logger.info("Post-installation configuration completed successfully")
|
||||
|
||||
# Check for TTW integration eligibility
|
||||
self._check_and_prompt_ttw_integration(install_dir_str, detected_game, modlist_name)
|
||||
else:
|
||||
self.logger.warning("Post-installation configuration had issues")
|
||||
else:
|
||||
@@ -1134,5 +1137,159 @@ class ModlistInstallCLI:
|
||||
|
||||
# Add URL on next line for easier debugging
|
||||
return f"{line}\n Nexus URL: {mod_url}"
|
||||
|
||||
return line
|
||||
|
||||
return line
|
||||
|
||||
def _check_and_prompt_ttw_integration(self, install_dir: str, game_type: str, modlist_name: str):
|
||||
"""Check if modlist is eligible for TTW integration and prompt user"""
|
||||
try:
|
||||
# Check eligibility: FNV game, TTW-compatible modlist, no existing TTW
|
||||
if not self._is_ttw_eligible(install_dir, game_type, modlist_name):
|
||||
return
|
||||
|
||||
# Prompt user for TTW installation
|
||||
print(f"\n{COLOR_PROMPT}═══════════════════════════════════════════════════════════════{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}TTW Integration Available{COLOR_RESET}")
|
||||
print(f"{COLOR_PROMPT}═══════════════════════════════════════════════════════════════{COLOR_RESET}")
|
||||
print(f"\nThis modlist ({modlist_name}) supports Tale of Two Wastelands (TTW).")
|
||||
print(f"TTW combines Fallout 3 and New Vegas into a single game.")
|
||||
print(f"\nWould you like to install TTW now?")
|
||||
|
||||
user_input = input(f"{COLOR_PROMPT}Install TTW? (yes/no): {COLOR_RESET}").strip().lower()
|
||||
|
||||
if user_input in ['yes', 'y']:
|
||||
self._launch_ttw_installation(modlist_name, install_dir)
|
||||
else:
|
||||
print(f"{COLOR_INFO}Skipping TTW installation. You can install it later from the main menu.{COLOR_RESET}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during TTW eligibility check: {e}", exc_info=True)
|
||||
|
||||
def _is_ttw_eligible(self, install_dir: str, game_type: str, modlist_name: str) -> bool:
|
||||
"""Check if modlist is eligible for TTW integration"""
|
||||
try:
|
||||
from pathlib import Path
|
||||
|
||||
# Check 1: Must be Fallout New Vegas
|
||||
if not game_type or game_type.lower() not in ['falloutnv', 'fallout new vegas', 'fallout_new_vegas']:
|
||||
return False
|
||||
|
||||
# Check 2: Must be on TTW compatibility whitelist
|
||||
from jackify.backend.data.ttw_compatible_modlists import is_ttw_compatible
|
||||
if not is_ttw_compatible(modlist_name):
|
||||
return False
|
||||
|
||||
# Check 3: TTW must not already be installed
|
||||
if self._detect_existing_ttw(install_dir):
|
||||
self.logger.info(f"TTW already installed in {install_dir}, skipping prompt")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error checking TTW eligibility: {e}")
|
||||
return False
|
||||
|
||||
def _detect_existing_ttw(self, install_dir: str) -> bool:
|
||||
"""Detect if TTW is already installed in the modlist"""
|
||||
try:
|
||||
from pathlib import Path
|
||||
|
||||
install_path = Path(install_dir)
|
||||
|
||||
# Search for TTW indicators in common locations
|
||||
search_paths = [
|
||||
install_path,
|
||||
install_path / "mods",
|
||||
install_path / "Stock Game",
|
||||
install_path / "Game Root"
|
||||
]
|
||||
|
||||
for search_path in search_paths:
|
||||
if not search_path.exists():
|
||||
continue
|
||||
|
||||
# Look for folders containing "tale" and "two" and "wastelands"
|
||||
for folder in search_path.iterdir():
|
||||
if not folder.is_dir():
|
||||
continue
|
||||
|
||||
folder_name_lower = folder.name.lower()
|
||||
if all(keyword in folder_name_lower for keyword in ['tale', 'two', 'wastelands']):
|
||||
# Verify it has the TTW ESM file
|
||||
for file in folder.rglob('*.esm'):
|
||||
if 'taleoftwowastelands' in file.name.lower():
|
||||
self.logger.info(f"Found existing TTW installation: {file}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error detecting existing TTW: {e}")
|
||||
return False
|
||||
|
||||
def _launch_ttw_installation(self, modlist_name: str, install_dir: str):
|
||||
"""Launch TTW installation workflow"""
|
||||
try:
|
||||
print(f"\n{COLOR_INFO}Starting TTW installation workflow...{COLOR_RESET}")
|
||||
|
||||
# Import TTW installation handler
|
||||
from jackify.backend.handlers.hoolamike_handler import HoolamikeHandler
|
||||
from jackify.backend.models.configuration import SystemInfo
|
||||
|
||||
system_info = SystemInfo()
|
||||
hoolamike_handler = HoolamikeHandler(system_info)
|
||||
|
||||
# Check if Hoolamike is installed
|
||||
is_installed, installed_version = hoolamike_handler.check_installation_status()
|
||||
|
||||
if not is_installed:
|
||||
print(f"{COLOR_INFO}Hoolamike (TTW installer) is not installed.{COLOR_RESET}")
|
||||
user_input = input(f"{COLOR_PROMPT}Install Hoolamike? (yes/no): {COLOR_RESET}").strip().lower()
|
||||
|
||||
if user_input not in ['yes', 'y']:
|
||||
print(f"{COLOR_INFO}TTW installation cancelled.{COLOR_RESET}")
|
||||
return
|
||||
|
||||
# Install Hoolamike
|
||||
print(f"{COLOR_INFO}Installing Hoolamike...{COLOR_RESET}")
|
||||
success, message = hoolamike_handler.install_hoolamike()
|
||||
|
||||
if not success:
|
||||
print(f"{COLOR_ERROR}Failed to install Hoolamike: {message}{COLOR_RESET}")
|
||||
return
|
||||
|
||||
print(f"{COLOR_INFO}Hoolamike installed successfully.{COLOR_RESET}")
|
||||
|
||||
# Get Hoolamike MPI path
|
||||
mpi_path = hoolamike_handler.get_mpi_path()
|
||||
if not mpi_path or not os.path.exists(mpi_path):
|
||||
print(f"{COLOR_ERROR}Hoolamike MPI file not found at: {mpi_path}{COLOR_RESET}")
|
||||
return
|
||||
|
||||
# Prompt for TTW installation directory
|
||||
print(f"\n{COLOR_PROMPT}TTW Installation Directory{COLOR_RESET}")
|
||||
print(f"Default: {os.path.join(install_dir, 'TTW')}")
|
||||
ttw_install_dir = input(f"{COLOR_PROMPT}TTW install directory (Enter for default): {COLOR_RESET}").strip()
|
||||
|
||||
if not ttw_install_dir:
|
||||
ttw_install_dir = os.path.join(install_dir, "TTW")
|
||||
|
||||
# Run Hoolamike installation
|
||||
print(f"\n{COLOR_INFO}Installing TTW using Hoolamike...{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}This may take a while (15-30 minutes depending on your system).{COLOR_RESET}")
|
||||
|
||||
success = hoolamike_handler.run_hoolamike_install(mpi_path, ttw_install_dir)
|
||||
|
||||
if success:
|
||||
print(f"\n{COLOR_INFO}═══════════════════════════════════════════════════════════════{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}TTW Installation Complete!{COLOR_RESET}")
|
||||
print(f"{COLOR_PROMPT}═══════════════════════════════════════════════════════════════{COLOR_RESET}")
|
||||
print(f"\nTTW has been installed to: {ttw_install_dir}")
|
||||
print(f"The modlist '{modlist_name}' is now ready to use with TTW.")
|
||||
else:
|
||||
print(f"\n{COLOR_ERROR}TTW installation failed. Check the logs for details.{COLOR_RESET}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during TTW installation: {e}", exc_info=True)
|
||||
print(f"{COLOR_ERROR}Error during TTW installation: {e}{COLOR_RESET}")
|
||||
@@ -676,10 +676,10 @@ class PathHandler:
|
||||
|
||||
# For each library path, look for each target game
|
||||
for library_path in library_paths:
|
||||
# Check if the common directory exists
|
||||
common_dir = library_path / "common"
|
||||
# Check if the common directory exists (games are in steamapps/common)
|
||||
common_dir = library_path / "steamapps" / "common"
|
||||
if not common_dir.is_dir():
|
||||
logger.debug(f"No 'common' directory in library: {library_path}")
|
||||
logger.debug(f"No 'steamapps/common' directory in library: {library_path}")
|
||||
continue
|
||||
|
||||
# Get subdirectories in common dir
|
||||
@@ -694,8 +694,8 @@ class PathHandler:
|
||||
if game_name in results:
|
||||
continue # Already found this game
|
||||
|
||||
# Try to find by appmanifest
|
||||
appmanifest_path = library_path / f"appmanifest_{app_id}.acf"
|
||||
# Try to find by appmanifest (manifests are in steamapps subdirectory)
|
||||
appmanifest_path = library_path / "steamapps" / f"appmanifest_{app_id}.acf"
|
||||
if appmanifest_path.is_file():
|
||||
# Find the installdir value
|
||||
try:
|
||||
@@ -799,7 +799,8 @@ class PathHandler:
|
||||
match = re.match(sdcard_pattern, existing_game_path)
|
||||
if match:
|
||||
stripped_path = match.group(1) # Just the /Games/... part
|
||||
new_gamepath_value = f"D:\\\\{stripped_path.replace('/', '\\\\')}"
|
||||
windows_path = stripped_path.replace('/', '\\\\')
|
||||
new_gamepath_value = f"D:\\\\{windows_path}"
|
||||
new_gamepath_line = f"gamePath = @ByteArray({new_gamepath_value})\n"
|
||||
|
||||
logger.info(f"Updating gamePath for SD card: {lines[gamepath_line_index].strip()} -> {new_gamepath_line.strip()}")
|
||||
|
||||
@@ -291,6 +291,9 @@ class WinetricksHandler:
|
||||
|
||||
# For non-dotnet40 installations, install all components together (faster)
|
||||
max_attempts = 3
|
||||
winetricks_failed = False
|
||||
last_error_details = None
|
||||
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
if attempt > 1:
|
||||
self.logger.warning(f"Retrying component installation (attempt {attempt}/{max_attempts})...")
|
||||
@@ -301,9 +304,40 @@ class WinetricksHandler:
|
||||
cmd = [self.winetricks_path, '--unattended'] + components_to_install
|
||||
|
||||
self.logger.debug(f"Running: {' '.join(cmd)}")
|
||||
self.logger.debug(f"Environment WINE={env.get('WINE', 'NOT SET')}")
|
||||
self.logger.debug(f"Environment DISPLAY={env.get('DISPLAY', 'NOT SET')}")
|
||||
self.logger.debug(f"Environment WINEPREFIX={env.get('WINEPREFIX', 'NOT SET')}")
|
||||
|
||||
# Enhanced diagnostics for bundled winetricks
|
||||
self.logger.debug("=== Winetricks Environment Diagnostics ===")
|
||||
self.logger.debug(f"Bundled winetricks path: {self.winetricks_path}")
|
||||
self.logger.debug(f"Winetricks exists: {os.path.exists(self.winetricks_path)}")
|
||||
self.logger.debug(f"Winetricks executable: {os.access(self.winetricks_path, os.X_OK)}")
|
||||
if os.path.exists(self.winetricks_path):
|
||||
try:
|
||||
winetricks_stat = os.stat(self.winetricks_path)
|
||||
self.logger.debug(f"Winetricks permissions: {oct(winetricks_stat.st_mode)}")
|
||||
self.logger.debug(f"Winetricks size: {winetricks_stat.st_size} bytes")
|
||||
except Exception as stat_err:
|
||||
self.logger.debug(f"Could not stat winetricks: {stat_err}")
|
||||
|
||||
self.logger.debug(f"WINE binary: {env.get('WINE', 'NOT SET')}")
|
||||
wine_binary = env.get('WINE', '')
|
||||
if wine_binary and os.path.exists(wine_binary):
|
||||
self.logger.debug(f"WINE binary exists: True")
|
||||
else:
|
||||
self.logger.debug(f"WINE binary exists: False")
|
||||
|
||||
self.logger.debug(f"WINEPREFIX: {env.get('WINEPREFIX', 'NOT SET')}")
|
||||
wineprefix = env.get('WINEPREFIX', '')
|
||||
if wineprefix and os.path.exists(wineprefix):
|
||||
self.logger.debug(f"WINEPREFIX exists: True")
|
||||
self.logger.debug(f"WINEPREFIX/pfx exists: {os.path.exists(os.path.join(wineprefix, 'pfx'))}")
|
||||
else:
|
||||
self.logger.debug(f"WINEPREFIX exists: False")
|
||||
|
||||
self.logger.debug(f"DISPLAY: {env.get('DISPLAY', 'NOT SET')}")
|
||||
self.logger.debug(f"WINETRICKS_CACHE: {env.get('WINETRICKS_CACHE', 'NOT SET')}")
|
||||
self.logger.debug(f"Components to install: {components_to_install}")
|
||||
self.logger.debug("==========================================")
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
env=env,
|
||||
@@ -337,14 +371,92 @@ class WinetricksHandler:
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not read winetricks.log: {e}")
|
||||
|
||||
# Store detailed error information for fallback diagnostics
|
||||
last_error_details = {
|
||||
'returncode': result.returncode,
|
||||
'stdout': result.stdout.strip(),
|
||||
'stderr': result.stderr.strip(),
|
||||
'attempt': attempt
|
||||
}
|
||||
|
||||
self.logger.error(f"Winetricks command failed (Attempt {attempt}/{max_attempts}). Return Code: {result.returncode}")
|
||||
self.logger.error(f"Stdout: {result.stdout.strip()}")
|
||||
self.logger.error(f"Stderr: {result.stderr.strip()}")
|
||||
|
||||
# Enhanced error diagnostics with actionable information
|
||||
stderr_lower = result.stderr.lower()
|
||||
stdout_lower = result.stdout.lower()
|
||||
|
||||
if "command not found" in stderr_lower or "no such file" in stderr_lower:
|
||||
self.logger.error("DIAGNOSTIC: Winetricks or dependency binary not found")
|
||||
self.logger.error(" - Bundled winetricks may be missing dependencies")
|
||||
self.logger.error(" - Will attempt protontricks fallback if all attempts fail")
|
||||
elif "permission denied" in stderr_lower:
|
||||
self.logger.error("DIAGNOSTIC: Permission issue detected")
|
||||
self.logger.error(f" - Check permissions on: {self.winetricks_path}")
|
||||
self.logger.error(f" - Check permissions on WINEPREFIX: {env.get('WINEPREFIX', 'N/A')}")
|
||||
elif "timeout" in stderr_lower:
|
||||
self.logger.error("DIAGNOSTIC: Timeout issue detected during component download/install")
|
||||
elif "sha256sum mismatch" in stderr_lower or "sha256sum" in stdout_lower:
|
||||
self.logger.error("DIAGNOSTIC: Checksum verification failed")
|
||||
self.logger.error(" - Component download may be corrupted")
|
||||
self.logger.error(" - Network issue or upstream file change")
|
||||
elif "curl" in stderr_lower or "wget" in stderr_lower:
|
||||
self.logger.error("DIAGNOSTIC: Download tool (curl/wget) issue")
|
||||
self.logger.error(" - Network connectivity problem or missing download tool")
|
||||
elif "cabextract" in stderr_lower:
|
||||
self.logger.error("DIAGNOSTIC: cabextract missing or failed")
|
||||
self.logger.error(" - Required for extracting Windows cabinet files")
|
||||
elif "unzip" in stderr_lower:
|
||||
self.logger.error("DIAGNOSTIC: unzip missing or failed")
|
||||
self.logger.error(" - Required for extracting zip archives")
|
||||
else:
|
||||
self.logger.error("DIAGNOSTIC: Unknown winetricks failure")
|
||||
self.logger.error(" - Check full logs for details")
|
||||
self.logger.error(" - Will attempt protontricks fallback if all attempts fail")
|
||||
|
||||
winetricks_failed = True
|
||||
|
||||
except subprocess.TimeoutExpired as e:
|
||||
self.logger.error(f"Winetricks timed out (Attempt {attempt}/{max_attempts}): {e}")
|
||||
last_error_details = {'error': 'timeout', 'attempt': attempt}
|
||||
winetricks_failed = True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during winetricks run (Attempt {attempt}/{max_attempts}): {e}", exc_info=True)
|
||||
last_error_details = {'error': str(e), 'attempt': attempt}
|
||||
winetricks_failed = True
|
||||
|
||||
# All winetricks attempts failed - try automatic fallback to protontricks
|
||||
if winetricks_failed:
|
||||
self.logger.error(f"Winetricks failed after {max_attempts} attempts.")
|
||||
|
||||
# Check if protontricks is available for fallback
|
||||
try:
|
||||
protontricks_check = subprocess.run(['which', 'protontricks'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if protontricks_check.returncode == 0:
|
||||
self.logger.warning("=" * 80)
|
||||
self.logger.warning("AUTOMATIC FALLBACK: Winetricks failed, attempting protontricks fallback...")
|
||||
self.logger.warning(f"Last winetricks error: {last_error_details}")
|
||||
self.logger.warning("=" * 80)
|
||||
|
||||
# Attempt fallback to protontricks
|
||||
fallback_success = self._install_components_protontricks_only(components_to_install, wineprefix, game_var)
|
||||
|
||||
if fallback_success:
|
||||
self.logger.info("SUCCESS: Protontricks fallback succeeded where winetricks failed")
|
||||
return True
|
||||
else:
|
||||
self.logger.error("FAILURE: Both winetricks and protontricks fallback failed")
|
||||
return False
|
||||
else:
|
||||
self.logger.error("Protontricks not available for fallback")
|
||||
self.logger.error(f"Final winetricks error details: {last_error_details}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Could not check for protontricks fallback: {e}")
|
||||
return False
|
||||
|
||||
self.logger.error(f"Failed to install Wine components after {max_attempts} attempts.")
|
||||
return False
|
||||
|
||||
def _reorder_components_for_installation(self, components: list) -> list:
|
||||
|
||||
@@ -3020,7 +3020,7 @@ echo Prefix creation complete.
|
||||
'/v', 'mscoree', '/t', 'REG_SZ', '/d', 'native', '/f'
|
||||
]
|
||||
|
||||
result1 = subprocess.run(cmd1, env=env, capture_output=True, text=True)
|
||||
result1 = subprocess.run(cmd1, env=env, capture_output=True, text=True, errors='replace')
|
||||
if result1.returncode == 0:
|
||||
logger.info("Successfully applied mscoree=native DLL override")
|
||||
else:
|
||||
@@ -3035,7 +3035,7 @@ echo Prefix creation complete.
|
||||
'/v', 'OnlyUseLatestCLR', '/t', 'REG_DWORD', '/d', '1', '/f'
|
||||
]
|
||||
|
||||
result2 = subprocess.run(cmd2, env=env, capture_output=True, text=True)
|
||||
result2 = subprocess.run(cmd2, env=env, capture_output=True, text=True, errors='replace')
|
||||
if result2.returncode == 0:
|
||||
logger.info("Successfully applied OnlyUseLatestCLR=1 registry entry")
|
||||
else:
|
||||
|
||||
@@ -451,10 +451,8 @@ class JackifyCLI:
|
||||
elif choice == "wabbajack":
|
||||
self.menus['wabbajack'].show_wabbajack_tasks_menu(self)
|
||||
# HIDDEN FOR FIRST RELEASE - UNCOMMENT WHEN READY
|
||||
# elif choice == "hoolamike":
|
||||
# self.menus['hoolamike'].show_hoolamike_menu(self)
|
||||
# elif choice == "additional":
|
||||
# self.menus['additional'].show_additional_tasks_menu(self)
|
||||
elif choice == "additional":
|
||||
self.menus['additional'].show_additional_tasks_menu(self)
|
||||
else:
|
||||
logger.warning(f"Invalid choice '{choice}' received from show_main_menu.")
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Extracted from src.modules.menu_handler.MenuHandler.show_additional_tasks_menu()
|
||||
import time
|
||||
|
||||
from jackify.shared.colors import (
|
||||
COLOR_SELECTION, COLOR_RESET, COLOR_ACTION, COLOR_PROMPT, COLOR_INFO, COLOR_DISABLED
|
||||
COLOR_SELECTION, COLOR_RESET, COLOR_ACTION, COLOR_PROMPT, COLOR_INFO, COLOR_DISABLED, COLOR_WARNING
|
||||
)
|
||||
from jackify.shared.ui_utils import print_jackify_banner, print_section_header, clear_screen
|
||||
|
||||
@@ -24,29 +24,26 @@ class AdditionalMenuHandler:
|
||||
clear_screen()
|
||||
|
||||
def show_additional_tasks_menu(self, cli_instance):
|
||||
"""Show the MO2, NXM Handling & Recovery submenu"""
|
||||
"""Show the Additional Tasks & Tools submenu"""
|
||||
while True:
|
||||
self._clear_screen()
|
||||
print_jackify_banner()
|
||||
print_section_header("Additional Utilities") # Broader title
|
||||
|
||||
print(f"{COLOR_SELECTION}1.{COLOR_RESET} Install Mod Organizer 2 (Base Setup)")
|
||||
print(f" {COLOR_ACTION}→ Proton setup for a standalone MO2 instance{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}2.{COLOR_RESET} Configure NXM Handling {COLOR_DISABLED}(Not Implemented){COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}3.{COLOR_RESET} Jackify Recovery Tools")
|
||||
print(f" {COLOR_ACTION}→ Restore files modified or backed up by Jackify{COLOR_RESET}")
|
||||
print_section_header("Additional Tasks & Tools")
|
||||
print(f"{COLOR_INFO}Additional Tasks & Tools, such as TTW Installation{COLOR_RESET}\n")
|
||||
|
||||
print(f"{COLOR_SELECTION}1.{COLOR_RESET} Tale of Two Wastelands (TTW) Installation")
|
||||
print(f" {COLOR_ACTION}→ Install TTW using Hoolamike native automation{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}2.{COLOR_RESET} Coming Soon...")
|
||||
print(f"{COLOR_SELECTION}0.{COLOR_RESET} Return to Main Menu")
|
||||
selection = input(f"\n{COLOR_PROMPT}Enter your selection (0-3): {COLOR_RESET}").strip()
|
||||
selection = input(f"\n{COLOR_PROMPT}Enter your selection (0-2): {COLOR_RESET}").strip()
|
||||
|
||||
if selection.lower() == 'q': # Allow 'q' to re-display menu
|
||||
continue
|
||||
if selection == "1":
|
||||
self._execute_legacy_install_mo2(cli_instance)
|
||||
self._execute_hoolamike_ttw_install(cli_instance)
|
||||
elif selection == "2":
|
||||
print(f"{COLOR_INFO}Configure NXM Handling is not yet implemented.{COLOR_RESET}")
|
||||
input("\nPress Enter to return to the Utilities menu...")
|
||||
elif selection == "3":
|
||||
self._execute_legacy_recovery_menu(cli_instance)
|
||||
print(f"\n{COLOR_INFO}More features coming soon!{COLOR_RESET}")
|
||||
input("\nPress Enter to return to menu...")
|
||||
elif selection == "0":
|
||||
break
|
||||
else:
|
||||
@@ -69,4 +66,59 @@ class AdditionalMenuHandler:
|
||||
|
||||
recovery_handler = RecoveryMenuHandler()
|
||||
recovery_handler.logger = self.logger
|
||||
recovery_handler.show_recovery_menu(cli_instance)
|
||||
recovery_handler.show_recovery_menu(cli_instance)
|
||||
|
||||
def _execute_hoolamike_ttw_install(self, cli_instance):
|
||||
"""Execute TTW installation using Hoolamike handler"""
|
||||
from ....backend.handlers.hoolamike_handler import HoolamikeHandler
|
||||
from ....backend.models.configuration import SystemInfo
|
||||
from ....shared.colors import COLOR_ERROR
|
||||
|
||||
system_info = SystemInfo(is_steamdeck=cli_instance.system_info.is_steamdeck)
|
||||
hoolamike_handler = HoolamikeHandler(
|
||||
steamdeck=system_info.is_steamdeck,
|
||||
verbose=cli_instance.verbose,
|
||||
filesystem_handler=cli_instance.filesystem_handler,
|
||||
config_handler=cli_instance.config_handler,
|
||||
menu_handler=cli_instance.menu_handler
|
||||
)
|
||||
|
||||
# First check if Hoolamike is installed
|
||||
if not hoolamike_handler.hoolamike_installed:
|
||||
print(f"\n{COLOR_WARNING}Hoolamike is not installed. Installing Hoolamike first...{COLOR_RESET}")
|
||||
if not hoolamike_handler.install_update_hoolamike():
|
||||
print(f"{COLOR_ERROR}Failed to install Hoolamike. Cannot proceed with TTW installation.{COLOR_RESET}")
|
||||
input("Press Enter to return to menu...")
|
||||
return
|
||||
|
||||
# Run TTW installation workflow
|
||||
print(f"\n{COLOR_INFO}Starting TTW installation workflow...{COLOR_RESET}")
|
||||
result = hoolamike_handler.install_ttw()
|
||||
if result is None:
|
||||
print(f"\n{COLOR_WARNING}TTW installation returned without result.{COLOR_RESET}")
|
||||
input("Press Enter to return to menu...")
|
||||
|
||||
def _execute_hoolamike_modlist_install(self, cli_instance):
|
||||
"""Execute modlist installation using Hoolamike handler"""
|
||||
from ....backend.handlers.hoolamike_handler import HoolamikeHandler
|
||||
from ....backend.models.configuration import SystemInfo
|
||||
|
||||
system_info = SystemInfo(is_steamdeck=cli_instance.system_info.is_steamdeck)
|
||||
hoolamike_handler = HoolamikeHandler(
|
||||
steamdeck=system_info.is_steamdeck,
|
||||
verbose=cli_instance.verbose,
|
||||
filesystem_handler=cli_instance.filesystem_handler,
|
||||
config_handler=cli_instance.config_handler,
|
||||
menu_handler=cli_instance.menu_handler
|
||||
)
|
||||
|
||||
# First check if Hoolamike is installed
|
||||
if not hoolamike_handler.hoolamike_installed:
|
||||
print(f"\n{COLOR_WARNING}Hoolamike is not installed. Installing Hoolamike first...{COLOR_RESET}")
|
||||
if not hoolamike_handler.install_update_hoolamike():
|
||||
print(f"{COLOR_ERROR}Failed to install Hoolamike. Cannot proceed with modlist installation.{COLOR_RESET}")
|
||||
input("Press Enter to return to menu...")
|
||||
return
|
||||
|
||||
# Run modlist installation
|
||||
hoolamike_handler.install_modlist()
|
||||
@@ -42,36 +42,17 @@ class MainMenuHandler:
|
||||
print(f"{COLOR_SELECTION}{'-'*22}{COLOR_RESET}") # Standard separator
|
||||
print(f"{COLOR_SELECTION}1.{COLOR_RESET} Modlist Tasks")
|
||||
print(f" {COLOR_ACTION}→ Install & Configure Modlists{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}2.{COLOR_RESET} Coming Soon...")
|
||||
print(f" {COLOR_ACTION}→ More features coming in future releases{COLOR_RESET}")
|
||||
if self.dev_mode:
|
||||
print(f"{COLOR_SELECTION}3.{COLOR_RESET} Hoolamike Tasks")
|
||||
print(f" {COLOR_ACTION}→ Wabbajack alternative: Install Modlists, TTW, etc{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}4.{COLOR_RESET} Additional Tasks")
|
||||
print(f" {COLOR_ACTION}→ Install Wabbajack (via WINE), MO2, NXM Handling, Jackify Recovery{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}2.{COLOR_RESET} Additional Tasks & Tools")
|
||||
print(f" {COLOR_ACTION}→ TTW automation, Wabbajack via Wine, MO2, NXM Handling, Recovery{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}0.{COLOR_RESET} Exit Jackify")
|
||||
if self.dev_mode:
|
||||
choice = input(f"\n{COLOR_PROMPT}Enter your selection (0-4): {COLOR_RESET}").strip()
|
||||
else:
|
||||
choice = input(f"\n{COLOR_PROMPT}Enter your selection (0-2): {COLOR_RESET}").strip()
|
||||
choice = input(f"\n{COLOR_PROMPT}Enter your selection (0-2): {COLOR_RESET}").strip()
|
||||
|
||||
if choice.lower() == 'q': # Allow 'q' to re-display menu
|
||||
continue
|
||||
if choice == "1":
|
||||
return "wabbajack"
|
||||
elif choice == "2":
|
||||
# Additional features are coming in future releases
|
||||
print(f"\n{COLOR_PROMPT}Coming Soon!{COLOR_RESET}")
|
||||
print(f"More features will be added in future releases.")
|
||||
print(f"Please use 'Modlist Tasks' for all current functionality.")
|
||||
print(f"Press Enter to continue...")
|
||||
input()
|
||||
continue # Return to main menu
|
||||
if self.dev_mode:
|
||||
if choice == "3":
|
||||
return "hoolamike"
|
||||
elif choice == "4":
|
||||
return "additional"
|
||||
return "additional"
|
||||
elif choice == "0":
|
||||
return "exit"
|
||||
else:
|
||||
|
||||
@@ -909,18 +909,24 @@ class JackifyMainWindow(QMainWindow):
|
||||
|
||||
# Create screens using refactored codebase
|
||||
from jackify.frontends.gui.screens import (
|
||||
MainMenu, ModlistTasksScreen,
|
||||
MainMenu, ModlistTasksScreen, AdditionalTasksScreen,
|
||||
InstallModlistScreen, ConfigureNewModlistScreen, ConfigureExistingModlistScreen
|
||||
)
|
||||
from jackify.frontends.gui.screens.install_ttw import InstallTTWScreen
|
||||
|
||||
self.main_menu = MainMenu(stacked_widget=self.stacked_widget, dev_mode=dev_mode)
|
||||
self.feature_placeholder = FeaturePlaceholder(stacked_widget=self.stacked_widget)
|
||||
|
||||
self.modlist_tasks_screen = ModlistTasksScreen(
|
||||
stacked_widget=self.stacked_widget,
|
||||
stacked_widget=self.stacked_widget,
|
||||
main_menu_index=0,
|
||||
dev_mode=dev_mode
|
||||
)
|
||||
self.additional_tasks_screen = AdditionalTasksScreen(
|
||||
stacked_widget=self.stacked_widget,
|
||||
main_menu_index=0,
|
||||
system_info=self.system_info
|
||||
)
|
||||
self.install_modlist_screen = InstallModlistScreen(
|
||||
stacked_widget=self.stacked_widget,
|
||||
main_menu_index=0
|
||||
@@ -933,14 +939,26 @@ class JackifyMainWindow(QMainWindow):
|
||||
stacked_widget=self.stacked_widget,
|
||||
main_menu_index=0
|
||||
)
|
||||
self.install_ttw_screen = InstallTTWScreen(
|
||||
stacked_widget=self.stacked_widget,
|
||||
main_menu_index=0,
|
||||
system_info=self.system_info
|
||||
)
|
||||
# Let TTW screen request window resize for expand/collapse
|
||||
try:
|
||||
self.install_ttw_screen.resize_request.connect(self._on_child_resize_request)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add screens to stacked widget
|
||||
self.stacked_widget.addWidget(self.main_menu) # Index 0: Main Menu
|
||||
self.stacked_widget.addWidget(self.feature_placeholder) # Index 1: Placeholder
|
||||
self.stacked_widget.addWidget(self.modlist_tasks_screen) # Index 2: Modlist Tasks
|
||||
self.stacked_widget.addWidget(self.install_modlist_screen) # Index 3: Install Modlist
|
||||
self.stacked_widget.addWidget(self.configure_new_modlist_screen) # Index 4: Configure New
|
||||
self.stacked_widget.addWidget(self.configure_existing_modlist_screen) # Index 5: Configure Existing
|
||||
self.stacked_widget.addWidget(self.additional_tasks_screen) # Index 3: Additional Tasks
|
||||
self.stacked_widget.addWidget(self.install_modlist_screen) # Index 4: Install Modlist
|
||||
self.stacked_widget.addWidget(self.install_ttw_screen) # Index 5: Install TTW
|
||||
self.stacked_widget.addWidget(self.configure_new_modlist_screen) # Index 6: Configure New
|
||||
self.stacked_widget.addWidget(self.configure_existing_modlist_screen) # Index 7: Configure Existing
|
||||
|
||||
# Add debug tracking for screen changes
|
||||
self.stacked_widget.currentChanged.connect(self._debug_screen_change)
|
||||
@@ -1025,9 +1043,11 @@ class JackifyMainWindow(QMainWindow):
|
||||
0: "Main Menu",
|
||||
1: "Feature Placeholder",
|
||||
2: "Modlist Tasks Menu",
|
||||
3: "Install Modlist Screen",
|
||||
4: "Configure New Modlist",
|
||||
5: "Configure Existing Modlist"
|
||||
3: "Additional Tasks Menu",
|
||||
4: "Install Modlist Screen",
|
||||
5: "Install TTW Screen",
|
||||
6: "Configure New Modlist",
|
||||
7: "Configure Existing Modlist"
|
||||
}
|
||||
screen_name = screen_names.get(index, f"Unknown Screen (Index {index})")
|
||||
widget = self.stacked_widget.widget(index)
|
||||
@@ -1180,6 +1200,80 @@ class JackifyMainWindow(QMainWindow):
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _on_child_resize_request(self, mode: str):
|
||||
debug_print(f"DEBUG: _on_child_resize_request called with mode='{mode}', current_size={self.size()}")
|
||||
# On Steam Deck we keep the stable, full-size layout and ignore child resize
|
||||
try:
|
||||
if self.system_info and self.system_info.is_steamdeck:
|
||||
debug_print("DEBUG: Steam Deck detected, ignoring resize request")
|
||||
# Hide the checkbox if present (Deck uses full layout)
|
||||
try:
|
||||
if hasattr(self, 'install_ttw_screen') and self.install_ttw_screen.show_details_checkbox:
|
||||
self.install_ttw_screen.show_details_checkbox.setVisible(False)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Ensure we can actually resize
|
||||
self.showNormal()
|
||||
self.setMaximumHeight(16777215)
|
||||
debug_print(f"DEBUG: Set max height to unlimited, current_size={self.size()}")
|
||||
|
||||
if mode == 'expand':
|
||||
# Restore a sensible minimum and expand height
|
||||
min_width = max(1200, self.minimumWidth())
|
||||
min_height = 900
|
||||
debug_print(f"DEBUG: Expand mode - min_width={min_width}, min_height={min_height}")
|
||||
try:
|
||||
from PySide6.QtCore import QSize
|
||||
self.setMinimumSize(QSize(min_width, min_height))
|
||||
except Exception:
|
||||
self.setMinimumSize(min_width, min_height)
|
||||
# Animate to target height
|
||||
target_height = max(self.size().height(), min_height)
|
||||
self._animate_height(target_height)
|
||||
else:
|
||||
# Collapse to compact height computed from the TTW screen's sizeHint
|
||||
try:
|
||||
content_hint = self.install_ttw_screen.sizeHint().height()
|
||||
except Exception:
|
||||
content_hint = 460
|
||||
compact_height = max(440, min(560, content_hint + 20))
|
||||
debug_print(f"DEBUG: Collapse mode - content_hint={content_hint}, compact_height={compact_height}")
|
||||
from PySide6.QtCore import QSize
|
||||
self.setMaximumHeight(compact_height)
|
||||
self.setMinimumSize(QSize(max(1200, self.minimumWidth()), compact_height))
|
||||
# Animate to compact height
|
||||
self._animate_height(compact_height)
|
||||
|
||||
def _animate_height(self, target_height: int, duration_ms: int = 180):
|
||||
"""Smoothly animate the window height to target_height.
|
||||
|
||||
Kept local imports to minimize global impact and avoid touching module headers.
|
||||
"""
|
||||
try:
|
||||
from PySide6.QtCore import QEasingCurve, QPropertyAnimation, QRect
|
||||
except Exception:
|
||||
# Fallback to immediate resize if animation types are unavailable
|
||||
before = self.size()
|
||||
self.resize(self.size().width(), target_height)
|
||||
debug_print(f"DEBUG: Animated fallback resize from {before} to {self.size()}")
|
||||
return
|
||||
|
||||
# Build end rect with same x/y/width and target height
|
||||
start_rect = self.geometry()
|
||||
end_rect = QRect(start_rect.x(), start_rect.y(), start_rect.width(), target_height)
|
||||
|
||||
# Hold reference to avoid GC stopping the animation
|
||||
self._resize_anim = QPropertyAnimation(self, b"geometry")
|
||||
self._resize_anim.setDuration(duration_ms)
|
||||
self._resize_anim.setEasingCurve(QEasingCurve.OutCubic)
|
||||
self._resize_anim.setStartValue(start_rect)
|
||||
self._resize_anim.setEndValue(end_rect)
|
||||
self._resize_anim.start()
|
||||
|
||||
|
||||
def resource_path(relative_path):
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
|
||||
@@ -6,6 +6,7 @@ Contains all the GUI screen components for Jackify.
|
||||
|
||||
from .main_menu import MainMenu
|
||||
from .modlist_tasks import ModlistTasksScreen
|
||||
from .additional_tasks import AdditionalTasksScreen
|
||||
from .install_modlist import InstallModlistScreen
|
||||
from .configure_new_modlist import ConfigureNewModlistScreen
|
||||
from .configure_existing_modlist import ConfigureExistingModlistScreen
|
||||
@@ -13,6 +14,7 @@ from .configure_existing_modlist import ConfigureExistingModlistScreen
|
||||
__all__ = [
|
||||
'MainMenu',
|
||||
'ModlistTasksScreen',
|
||||
'AdditionalTasksScreen',
|
||||
'InstallModlistScreen',
|
||||
'ConfigureNewModlistScreen',
|
||||
'ConfigureExistingModlistScreen'
|
||||
|
||||
169
jackify/frontends/gui/screens/additional_tasks.py
Normal file
169
jackify/frontends/gui/screens/additional_tasks.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
Additional Tasks & Tools Screen
|
||||
|
||||
Simple screen for TTW automation only.
|
||||
Follows the same pattern as ModlistTasksScreen.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QGridLayout
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont
|
||||
|
||||
from jackify.backend.models.configuration import SystemInfo
|
||||
from ..shared_theme import JACKIFY_COLOR_BLUE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdditionalTasksScreen(QWidget):
|
||||
"""Simple Additional Tasks screen for TTW only"""
|
||||
|
||||
def __init__(self, stacked_widget=None, main_menu_index=0, system_info: Optional[SystemInfo] = None):
|
||||
super().__init__()
|
||||
self.stacked_widget = stacked_widget
|
||||
self.main_menu_index = main_menu_index
|
||||
self.system_info = system_info or SystemInfo(is_steamdeck=False)
|
||||
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the user interface following ModlistTasksScreen pattern"""
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(40, 40, 40, 40)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# Header section
|
||||
self._setup_header(layout)
|
||||
|
||||
# Menu buttons section
|
||||
self._setup_menu_buttons(layout)
|
||||
|
||||
# Bottom spacer
|
||||
layout.addStretch()
|
||||
self.setLayout(layout)
|
||||
|
||||
def _setup_header(self, layout):
|
||||
"""Set up the header section"""
|
||||
header_layout = QVBoxLayout()
|
||||
header_layout.setSpacing(0)
|
||||
|
||||
# Title
|
||||
title = QLabel("<b>Additional Tasks & Tools</b>")
|
||||
title.setStyleSheet(f"font-size: 24px; color: {JACKIFY_COLOR_BLUE};")
|
||||
title.setAlignment(Qt.AlignHCenter)
|
||||
header_layout.addWidget(title)
|
||||
|
||||
# Add a spacer to match main menu vertical spacing
|
||||
header_layout.addSpacing(16)
|
||||
|
||||
# Description
|
||||
desc = QLabel(
|
||||
"TTW automation and additional tools.<br> "
|
||||
)
|
||||
desc.setWordWrap(True)
|
||||
desc.setStyleSheet("color: #ccc;")
|
||||
desc.setAlignment(Qt.AlignHCenter)
|
||||
header_layout.addWidget(desc)
|
||||
|
||||
header_layout.addSpacing(24)
|
||||
|
||||
# Separator (shorter like main menu)
|
||||
sep = QLabel()
|
||||
sep.setFixedHeight(2)
|
||||
sep.setFixedWidth(400) # Match button width
|
||||
sep.setStyleSheet("background: #fff;")
|
||||
header_layout.addWidget(sep, alignment=Qt.AlignHCenter)
|
||||
|
||||
header_layout.addSpacing(16)
|
||||
layout.addLayout(header_layout)
|
||||
|
||||
def _setup_menu_buttons(self, layout):
|
||||
"""Set up the menu buttons section"""
|
||||
# Menu options - ONLY TTW and placeholder
|
||||
MENU_ITEMS = [
|
||||
("Install TTW", "ttw_install", "Install Tale of Two Wastelands using Hoolamike automation"),
|
||||
("Coming Soon...", "coming_soon", "Additional tools will be added in future updates"),
|
||||
("Return to Main Menu", "return_main_menu", "Go back to the main menu"),
|
||||
]
|
||||
|
||||
# Create grid layout for buttons (mirror ModlistTasksScreen pattern)
|
||||
button_grid = QGridLayout()
|
||||
button_grid.setSpacing(16)
|
||||
button_grid.setAlignment(Qt.AlignHCenter)
|
||||
|
||||
button_width = 400
|
||||
button_height = 50
|
||||
|
||||
for i, (label, action_id, description) in enumerate(MENU_ITEMS):
|
||||
# Create button
|
||||
btn = QPushButton(label)
|
||||
btn.setFixedSize(button_width, button_height)
|
||||
btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: #4a5568;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: #5a6578;
|
||||
}}
|
||||
QPushButton:pressed {{
|
||||
background-color: {JACKIFY_COLOR_BLUE};
|
||||
}}
|
||||
""")
|
||||
btn.clicked.connect(lambda checked, a=action_id: self._handle_button_click(a))
|
||||
|
||||
# Description label
|
||||
desc_label = QLabel(description)
|
||||
desc_label.setAlignment(Qt.AlignHCenter)
|
||||
desc_label.setStyleSheet("color: #999; font-size: 12px;")
|
||||
desc_label.setWordWrap(True)
|
||||
desc_label.setFixedWidth(button_width)
|
||||
|
||||
# Add to grid (button row, then description row)
|
||||
button_grid.addWidget(btn, i * 2, 0, Qt.AlignHCenter)
|
||||
button_grid.addWidget(desc_label, i * 2 + 1, 0, Qt.AlignHCenter)
|
||||
|
||||
layout.addLayout(button_grid)
|
||||
|
||||
# Removed _create_menu_button; using same pattern as ModlistTasksScreen
|
||||
|
||||
def _handle_button_click(self, action_id):
|
||||
"""Handle button clicks"""
|
||||
if action_id == "ttw_install":
|
||||
self._show_ttw_info()
|
||||
elif action_id == "coming_soon":
|
||||
self._show_coming_soon_info()
|
||||
elif action_id == "return_main_menu":
|
||||
self._return_to_main_menu()
|
||||
|
||||
def _show_ttw_info(self):
|
||||
"""Navigate to TTW installation screen"""
|
||||
if self.stacked_widget:
|
||||
# Navigate to TTW installation screen (index 5)
|
||||
self.stacked_widget.setCurrentIndex(5)
|
||||
|
||||
def _show_coming_soon_info(self):
|
||||
"""Show coming soon info"""
|
||||
from ..services.message_service import MessageService
|
||||
MessageService.information(
|
||||
self,
|
||||
"Coming Soon",
|
||||
"Additional tools and features will be added in future updates.\n\n"
|
||||
"Check back later for more functionality!"
|
||||
)
|
||||
|
||||
def _return_to_main_menu(self):
|
||||
"""Return to main menu"""
|
||||
if self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(self.main_menu_index)
|
||||
@@ -412,6 +412,9 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
pass
|
||||
|
||||
def validate_and_start_configure(self):
|
||||
# Reload config to pick up any settings changes made in Settings dialog
|
||||
self.config_handler.reload_config()
|
||||
|
||||
# Rotate log file at start of each workflow run (keep 5 backups)
|
||||
from jackify.backend.handlers.logging_handler import LoggingHandler
|
||||
from pathlib import Path
|
||||
@@ -453,10 +456,14 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
|
||||
def start_workflow(self, modlist_name, install_dir, resolution):
|
||||
"""Start the configuration workflow using backend service directly"""
|
||||
# CRITICAL: Reload config from disk to pick up any settings changes from Settings dialog
|
||||
# This ensures Proton version and winetricks settings are current
|
||||
self.config_handler._load_config()
|
||||
|
||||
try:
|
||||
# Start time tracking
|
||||
self._workflow_start_time = time.time()
|
||||
|
||||
|
||||
self._safe_append_text("[Jackify] Starting post-install configuration...")
|
||||
|
||||
# Create configuration thread using backend service
|
||||
|
||||
@@ -554,6 +554,9 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
return True # Continue anyway
|
||||
|
||||
def validate_and_start_configure(self):
|
||||
# Reload config to pick up any settings changes made in Settings dialog
|
||||
self.config_handler.reload_config()
|
||||
|
||||
# Check protontricks before proceeding
|
||||
if not self._check_protontricks():
|
||||
return
|
||||
@@ -665,6 +668,10 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
MessageService.critical(self, "Steam Restart Failed", "Failed to restart Steam automatically. Please restart Steam manually, then try again.", safety_level="medium")
|
||||
|
||||
def configure_modlist(self):
|
||||
# CRITICAL: Reload config from disk to pick up any settings changes from Settings dialog
|
||||
# This ensures Proton version and winetricks settings are current
|
||||
self.config_handler._load_config()
|
||||
|
||||
install_dir = os.path.dirname(self.install_dir_edit.text().strip()) if self.install_dir_edit.text().strip().endswith('ModOrganizer.exe') else self.install_dir_edit.text().strip()
|
||||
modlist_name = self.modlist_name_edit.text().strip()
|
||||
mo2_exe_path = self.install_dir_edit.text().strip()
|
||||
@@ -672,12 +679,12 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
if not install_dir or not modlist_name:
|
||||
MessageService.warning(self, "Missing Info", "Install directory or modlist name is missing.", safety_level="low")
|
||||
return
|
||||
|
||||
|
||||
# Use automated prefix service instead of manual steps
|
||||
self._safe_append_text("")
|
||||
self._safe_append_text("=== Steam Integration Phase ===")
|
||||
self._safe_append_text("Starting automated Steam setup workflow...")
|
||||
|
||||
|
||||
# Start automated prefix workflow
|
||||
self._start_automated_prefix_workflow(modlist_name, install_dir, mo2_exe_path, resolution)
|
||||
|
||||
|
||||
@@ -396,7 +396,7 @@ class InstallModlistScreen(QWidget):
|
||||
header_layout.addWidget(title)
|
||||
# Description
|
||||
desc = QLabel(
|
||||
"This screen allows you to install a Wabbajack modlist using Jackify's native Linux tools. "
|
||||
"This screen allows you to install a Wabbajack modlist using Jackify. "
|
||||
"Configure your options and start the installation."
|
||||
)
|
||||
desc.setWordWrap(True)
|
||||
@@ -1072,7 +1072,8 @@ class InstallModlistScreen(QWidget):
|
||||
line_lower = line.lower()
|
||||
if (
|
||||
("jackify-engine" in line_lower or "7zz" in line_lower or "texconv" in line_lower or
|
||||
"wine" in line_lower or "wine64" in line_lower or "protontricks" in line_lower)
|
||||
"wine" in line_lower or "wine64" in line_lower or "protontricks" in line_lower or
|
||||
"hoolamike" in line_lower)
|
||||
and "jackify-gui.py" not in line_lower
|
||||
):
|
||||
cols = line.strip().split(None, 3)
|
||||
@@ -1091,29 +1092,198 @@ class InstallModlistScreen(QWidget):
|
||||
"""Check if protontricks is available before critical operations"""
|
||||
try:
|
||||
is_installed, installation_type, details = self.protontricks_service.detect_protontricks()
|
||||
|
||||
|
||||
if not is_installed:
|
||||
# Show protontricks error dialog
|
||||
from jackify.frontends.gui.dialogs.protontricks_error_dialog import ProtontricksErrorDialog
|
||||
dialog = ProtontricksErrorDialog(self.protontricks_service, self)
|
||||
result = dialog.exec()
|
||||
|
||||
|
||||
if result == QDialog.Rejected:
|
||||
return False
|
||||
|
||||
|
||||
# Re-check after dialog
|
||||
is_installed, _, _ = self.protontricks_service.detect_protontricks(use_cache=False)
|
||||
return is_installed
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error checking protontricks: {e}")
|
||||
MessageService.warning(self, "Protontricks Check Failed",
|
||||
MessageService.warning(self, "Protontricks Check Failed",
|
||||
f"Unable to verify protontricks installation: {e}\n\n"
|
||||
"Continuing anyway, but some features may not work correctly.")
|
||||
return True # Continue anyway
|
||||
|
||||
def _check_ttw_eligibility(self, modlist_name: str, game_type: str, install_dir: str) -> bool:
|
||||
"""Check if modlist is FNV, TTW-compatible, and doesn't already have TTW
|
||||
|
||||
Args:
|
||||
modlist_name: Name of the installed modlist
|
||||
game_type: Game type (e.g., 'falloutnv')
|
||||
install_dir: Modlist installation directory
|
||||
|
||||
Returns:
|
||||
bool: True if should offer TTW integration
|
||||
"""
|
||||
try:
|
||||
# Check 1: Must be Fallout New Vegas
|
||||
if game_type.lower() not in ['falloutnv', 'fallout new vegas', 'fallout_new_vegas']:
|
||||
return False
|
||||
|
||||
# Check 2: Must be on whitelist
|
||||
from jackify.backend.data.ttw_compatible_modlists import is_ttw_compatible
|
||||
if not is_ttw_compatible(modlist_name):
|
||||
return False
|
||||
|
||||
# Check 3: TTW must not already be installed
|
||||
if self._detect_existing_ttw(install_dir):
|
||||
debug_print("DEBUG: TTW already installed, skipping prompt")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"DEBUG: Error checking TTW eligibility: {e}")
|
||||
return False
|
||||
|
||||
def _detect_existing_ttw(self, install_dir: str) -> bool:
|
||||
"""Check if TTW is already installed in the modlist
|
||||
|
||||
Args:
|
||||
install_dir: Modlist installation directory
|
||||
|
||||
Returns:
|
||||
bool: True if TTW is already present
|
||||
"""
|
||||
try:
|
||||
from pathlib import Path
|
||||
|
||||
mods_dir = Path(install_dir) / "mods"
|
||||
if not mods_dir.exists():
|
||||
return False
|
||||
|
||||
# Check for folders containing "Tale of Two Wastelands" that have actual TTW content
|
||||
# Exclude separators and placeholder folders
|
||||
for folder in mods_dir.iterdir():
|
||||
if not folder.is_dir():
|
||||
continue
|
||||
|
||||
folder_name_lower = folder.name.lower()
|
||||
|
||||
# Skip separator folders and placeholders
|
||||
if "_separator" in folder_name_lower or "put" in folder_name_lower or "here" in folder_name_lower:
|
||||
continue
|
||||
|
||||
# Check if folder name contains TTW indicator
|
||||
if "tale of two wastelands" in folder_name_lower:
|
||||
# Verify it has actual TTW content by checking for the main ESM
|
||||
ttw_esm = folder / "TaleOfTwoWastelands.esm"
|
||||
if ttw_esm.exists():
|
||||
debug_print(f"DEBUG: Found existing TTW installation: {folder.name}")
|
||||
return True
|
||||
else:
|
||||
debug_print(f"DEBUG: Found TTW folder but no ESM, skipping: {folder.name}")
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"DEBUG: Error detecting existing TTW: {e}")
|
||||
return False # Assume not installed on error
|
||||
|
||||
def _initiate_ttw_workflow(self, modlist_name: str, install_dir: str):
|
||||
"""Navigate to TTW screen and set it up for modlist integration
|
||||
|
||||
Args:
|
||||
modlist_name: Name of the modlist that needs TTW integration
|
||||
install_dir: Path to the modlist installation directory
|
||||
"""
|
||||
try:
|
||||
# Store modlist context for later use when TTW completes
|
||||
self._ttw_modlist_name = modlist_name
|
||||
self._ttw_install_dir = install_dir
|
||||
|
||||
# Get reference to TTW screen BEFORE navigation
|
||||
if self.stacked_widget:
|
||||
ttw_screen = self.stacked_widget.widget(5)
|
||||
|
||||
# Set integration mode BEFORE navigating to avoid showEvent race condition
|
||||
if hasattr(ttw_screen, 'set_modlist_integration_mode'):
|
||||
ttw_screen.set_modlist_integration_mode(modlist_name, install_dir)
|
||||
|
||||
# Connect to completion signal to show success dialog after TTW
|
||||
if hasattr(ttw_screen, 'integration_complete'):
|
||||
ttw_screen.integration_complete.connect(self._on_ttw_integration_complete)
|
||||
else:
|
||||
debug_print("WARNING: TTW screen does not support modlist integration mode yet")
|
||||
|
||||
# Navigate to TTW screen AFTER setting integration mode
|
||||
self.stacked_widget.setCurrentIndex(5)
|
||||
|
||||
# Force collapsed state shortly after navigation to avoid any
|
||||
# showEvent/layout timing races that may leave it expanded
|
||||
try:
|
||||
from PySide6.QtCore import QTimer
|
||||
QTimer.singleShot(50, lambda: getattr(ttw_screen, 'force_collapsed_state', lambda: None)())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"ERROR: Failed to initiate TTW workflow: {e}")
|
||||
MessageService.critical(
|
||||
self,
|
||||
"TTW Navigation Failed",
|
||||
f"Failed to navigate to TTW installation screen: {str(e)}"
|
||||
)
|
||||
|
||||
def _on_ttw_integration_complete(self, success: bool, ttw_version: str = ""):
|
||||
"""Handle completion of TTW integration and show final success dialog
|
||||
|
||||
Args:
|
||||
success: Whether TTW integration completed successfully
|
||||
ttw_version: Version of TTW that was installed
|
||||
"""
|
||||
try:
|
||||
if not success:
|
||||
MessageService.critical(
|
||||
self,
|
||||
"TTW Integration Failed",
|
||||
"Tale of Two Wastelands integration did not complete successfully."
|
||||
)
|
||||
return
|
||||
|
||||
# Navigate back to this screen to show success dialog
|
||||
if self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(4)
|
||||
|
||||
# Build success message including TTW installation
|
||||
modlist_name = getattr(self, '_ttw_modlist_name', 'Unknown')
|
||||
time_str = getattr(self, '_elapsed_time_str', '0m 0s')
|
||||
game_name = "Fallout New Vegas"
|
||||
|
||||
# Show enhanced success dialog
|
||||
success_dialog = SuccessDialog(
|
||||
modlist_name=modlist_name,
|
||||
workflow_type="install",
|
||||
time_taken=time_str,
|
||||
game_name=game_name,
|
||||
parent=self
|
||||
)
|
||||
|
||||
# Add TTW installation info to dialog if possible
|
||||
if hasattr(success_dialog, 'add_info_line'):
|
||||
success_dialog.add_info_line(f"TTW {ttw_version} integrated successfully")
|
||||
|
||||
success_dialog.show()
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"ERROR: Failed to show final success dialog: {e}")
|
||||
MessageService.critical(
|
||||
self,
|
||||
"Display Error",
|
||||
f"TTW integration completed but failed to show success dialog: {str(e)}"
|
||||
)
|
||||
|
||||
def _on_api_key_save_toggled(self, checked):
|
||||
"""Handle immediate API key saving with silent validation when checkbox is toggled"""
|
||||
try:
|
||||
@@ -1188,11 +1358,14 @@ class InstallModlistScreen(QWidget):
|
||||
import time
|
||||
self._install_workflow_start_time = time.time()
|
||||
debug_print('DEBUG: validate_and_start_install called')
|
||||
|
||||
|
||||
# Reload config to pick up any settings changes made in Settings dialog
|
||||
self.config_handler.reload_config()
|
||||
|
||||
# Check protontricks before proceeding
|
||||
if not self._check_protontricks():
|
||||
return
|
||||
|
||||
|
||||
# Disable all controls during installation (except Cancel)
|
||||
self._disable_controls_during_operation()
|
||||
|
||||
@@ -1764,6 +1937,11 @@ class InstallModlistScreen(QWidget):
|
||||
MessageService.critical(self, "Steam Restart Failed", "Failed to restart Steam automatically. Please restart Steam manually, then try again.")
|
||||
|
||||
def start_automated_prefix_workflow(self):
|
||||
"""Start the automated prefix creation workflow"""
|
||||
# CRITICAL: Reload config from disk to pick up any settings changes from Settings dialog
|
||||
# This ensures Proton version and winetricks settings are current
|
||||
self.config_handler._load_config()
|
||||
|
||||
# Ensure _current_resolution is always set before starting workflow
|
||||
if not hasattr(self, '_current_resolution') or self._current_resolution is None:
|
||||
resolution = self.resolution_combo.currentText() if hasattr(self, 'resolution_combo') else None
|
||||
@@ -1775,7 +1953,7 @@ class InstallModlistScreen(QWidget):
|
||||
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()
|
||||
@@ -2002,6 +2180,31 @@ class InstallModlistScreen(QWidget):
|
||||
'enderal': 'Enderal'
|
||||
}
|
||||
game_name = display_names.get(self._current_game_type, self._current_game_name)
|
||||
|
||||
# Check for TTW eligibility before showing final success dialog
|
||||
install_dir = self.install_dir_edit.text().strip()
|
||||
if self._check_ttw_eligibility(modlist_name, self._current_game_type, install_dir):
|
||||
# Offer TTW installation
|
||||
reply = MessageService.question(
|
||||
self,
|
||||
"Install TTW?",
|
||||
f"{modlist_name} requires Tale of Two Wastelands!\n\n"
|
||||
"Would you like to install and configure TTW automatically now?\n\n"
|
||||
"This will:\n"
|
||||
"• Guide you through TTW installation\n"
|
||||
"• Automatically integrate TTW into your modlist\n"
|
||||
"• Configure load order correctly\n\n"
|
||||
"Note: TTW installation can take a while. You can also install TTW later from Additional Tasks & Tools.",
|
||||
critical=False,
|
||||
safety_level="medium"
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
# Navigate to TTW screen
|
||||
self._initiate_ttw_workflow(modlist_name, install_dir)
|
||||
return # Don't show success dialog yet, will show after TTW completes
|
||||
|
||||
# Show normal success dialog
|
||||
success_dialog = SuccessDialog(
|
||||
modlist_name=modlist_name,
|
||||
workflow_type="install",
|
||||
@@ -2747,7 +2950,4 @@ https://wiki.scenicroute.games/Somnium/1_Installation.html</i>"""
|
||||
# Re-enable controls (in case they were disabled from previous errors)
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle window close event - clean up processes"""
|
||||
self.cleanup_processes()
|
||||
event.accept()
|
||||
|
||||
2932
jackify/frontends/gui/screens/install_ttw.py
Normal file
2932
jackify/frontends/gui/screens/install_ttw.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -47,12 +47,9 @@ class MainMenu(QWidget):
|
||||
button_height = 60
|
||||
MENU_ITEMS = [
|
||||
("Modlist Tasks", "modlist_tasks", "Manage your modlists with native Linux tools"),
|
||||
("Coming Soon...", "coming_soon", "More features coming soon!"),
|
||||
("Additional Tasks", "additional_tasks", "Additional Tasks & Tools, such as TTW Installation"),
|
||||
("Exit Jackify", "exit_jackify", "Close the application"),
|
||||
]
|
||||
if self.dev_mode:
|
||||
MENU_ITEMS.append(("Hoolamike Tasks", "hoolamike_tasks", "Manage Hoolamike modding tools"))
|
||||
MENU_ITEMS.append(("Additional Tasks", "additional_tasks", "Additional utilities and tools"))
|
||||
MENU_ITEMS.append(("Exit Jackify", "exit_jackify", "Close the application"))
|
||||
|
||||
for label, action_id, description in MENU_ITEMS:
|
||||
# Main button
|
||||
@@ -121,8 +118,10 @@ class MainMenu(QWidget):
|
||||
msg.exec()
|
||||
elif action_id == "modlist_tasks" and self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(2)
|
||||
elif action_id == "additional_tasks" and self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(3)
|
||||
elif action_id == "return_main_menu":
|
||||
# This is the main menu, so do nothing
|
||||
pass
|
||||
elif self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(2) # Placeholder for now
|
||||
self.stacked_widget.setCurrentIndex(1) # Default to placeholder
|
||||
@@ -198,11 +198,11 @@ class ModlistTasksScreen(QWidget):
|
||||
if action_id == "return_main_menu":
|
||||
self.stacked_widget.setCurrentIndex(0)
|
||||
elif action_id == "install_modlist":
|
||||
self.stacked_widget.setCurrentIndex(3)
|
||||
self.stacked_widget.setCurrentIndex(4) # Install Modlist Screen
|
||||
elif action_id == "configure_new_modlist":
|
||||
self.stacked_widget.setCurrentIndex(4)
|
||||
self.stacked_widget.setCurrentIndex(6) # Configure New Modlist Screen
|
||||
elif action_id == "configure_existing_modlist":
|
||||
self.stacked_widget.setCurrentIndex(5)
|
||||
self.stacked_widget.setCurrentIndex(7) # Configure Existing Modlist Screen
|
||||
|
||||
def go_back(self):
|
||||
"""Return to main menu"""
|
||||
|
||||
@@ -220,6 +220,8 @@ class MessageService:
|
||||
msg_box = MessageService._create_base_message_box(parent, critical, safety_level)
|
||||
msg_box.setIcon(QMessageBox.Information)
|
||||
msg_box.setWindowTitle(title)
|
||||
msg_box.setTextFormat(Qt.RichText)
|
||||
msg_box.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||
msg_box.setText(message)
|
||||
msg_box.setStandardButtons(buttons)
|
||||
msg_box.setDefaultButton(default_button)
|
||||
|
||||
@@ -9,6 +9,21 @@ ANSI_COLOR_MAP = {
|
||||
}
|
||||
ANSI_RE = re.compile(r'\x1b\[(\d+)(;\d+)?m')
|
||||
|
||||
# Pattern to match terminal control codes (cursor movement, line clearing, etc.)
|
||||
ANSI_CONTROL_RE = re.compile(
|
||||
r'\x1b\[' # CSI sequence start
|
||||
r'[0-9;]*' # Parameters
|
||||
r'[A-Za-z]' # Command letter
|
||||
)
|
||||
|
||||
def strip_ansi_control_codes(text):
|
||||
"""Remove ALL ANSI escape sequences including control codes.
|
||||
|
||||
This is useful for Hoolamike output which uses terminal control codes
|
||||
for progress bars that don't render well in QTextEdit.
|
||||
"""
|
||||
return ANSI_CONTROL_RE.sub('', text)
|
||||
|
||||
def ansi_to_html(text):
|
||||
"""Convert ANSI color codes to HTML"""
|
||||
result = ''
|
||||
|
||||
Reference in New Issue
Block a user