mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe14e4ecfb | ||
|
|
9680814bbb | ||
|
|
91ac08afb2 | ||
|
|
06bd94d119 |
66
CHANGELOG.md
66
CHANGELOG.md
@@ -1,5 +1,71 @@
|
||||
# Jackify Changelog
|
||||
|
||||
## v0.1.7.1 - Wine Component Verification & Flatpak Steam Fixes
|
||||
**Release Date:** November 11, 2025
|
||||
|
||||
### Critical Bug Fixes
|
||||
- **FIXED: Wine Component Installation Verification** - Jackify now verifies components are actually installed before reporting success
|
||||
|
||||
### Bug Fixes
|
||||
- **Steam Deck SD Card Paths**: Fixed ModOrganizer.ini path corruption on SD card installs using regex-based stripping
|
||||
- **Flatpak Steam Detection**: Fixed libraryfolders.vdf path detection for Flatpak Steam installations
|
||||
- **Flatpak Steam Restart**: Steam restart service now properly detects and controls Flatpak Steam
|
||||
- **Path Manipulation**: Fixed path corruption in Configure Existing/New Modlist (paths with spaces)
|
||||
|
||||
### Improvements
|
||||
- Added network diagnostics before winetricks fallback to protontricks
|
||||
- Enhanced component installation logging with verification status
|
||||
- Added GE-Proton 10-14 recommendation to success message (ENB compatibility note for Valve's Proton 10)
|
||||
|
||||
### Engine Updates
|
||||
- **jackify-engine 0.3.18**: Archive extraction fixes for Windows symlinks, bandwidth limiting fix, improved error messages
|
||||
|
||||
---
|
||||
|
||||
## v0.1.7 - TTW Automation & Bug Fixes
|
||||
**Release Date:** November 1, 2025
|
||||
|
||||
### Major Features
|
||||
- **TTW (Tale of Two Wastelands) Installation and Automation**
|
||||
laf - 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
|
||||
|
||||
### Bug Fixes
|
||||
- **Fixed AppImage bundling issue** causing legacy code to be retained in rare circumstances
|
||||
|
||||
---
|
||||
|
||||
## v0.1.6.5 - Steam Deck SD Card Path Fix
|
||||
**Release Date:** October 27, 2025
|
||||
|
||||
### Bug Fixes
|
||||
- **Fixed Steam Deck SD card path manipulation** when jackify-engine installed
|
||||
- **Fixed Ubuntu Qt platform plugin errors** by bundling XCB libraries
|
||||
- **Added Flatpak GE-Proton detection** and protontricks installation choices
|
||||
- **Extended Steam Deck SD card timeouts** for slower I/O operations
|
||||
|
||||
---
|
||||
|
||||
## v0.1.6.4 - Flatpak Steam Detection Hotfix
|
||||
**Release Date:** October 24, 2025
|
||||
|
||||
|
||||
@@ -77,6 +77,9 @@ Currently, there are two main functions that Jackify will perform at this stage
|
||||
- **FUSE** (required for AppImage execution)
|
||||
- Pre-installed on most Linux distributions
|
||||
- If AppImage fails to run, install FUSE using your distribution's package manager
|
||||
- **Ubuntu/Debian only**: Qt platform plugin library
|
||||
- `sudo apt install libxcb-cursor-dev`
|
||||
- Required for Qt GUI to initialize properly
|
||||
|
||||
### Installation
|
||||
|
||||
|
||||
@@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing
|
||||
Wabbajack modlists natively on Linux systems.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.6.4"
|
||||
__version__ = "0.1.7.1"
|
||||
|
||||
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
|
||||
@@ -86,7 +100,8 @@ class ConfigHandler:
|
||||
libraryfolders_vdf_paths = [
|
||||
os.path.expanduser("~/.steam/steam/config/libraryfolders.vdf"),
|
||||
os.path.expanduser("~/.local/share/Steam/config/libraryfolders.vdf"),
|
||||
os.path.expanduser("~/.steam/root/config/libraryfolders.vdf")
|
||||
os.path.expanduser("~/.steam/root/config/libraryfolders.vdf"),
|
||||
os.path.expanduser("~/.var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf") # Flatpak
|
||||
]
|
||||
|
||||
for vdf_path in libraryfolders_vdf_paths:
|
||||
@@ -113,6 +128,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"""
|
||||
|
||||
@@ -784,7 +784,8 @@ class FileSystemHandler:
|
||||
possible_vdf_paths = [
|
||||
Path.home() / ".steam/steam/config/libraryfolders.vdf",
|
||||
Path.home() / ".local/share/Steam/config/libraryfolders.vdf",
|
||||
Path.home() / ".steam/root/config/libraryfolders.vdf"
|
||||
Path.home() / ".steam/root/config/libraryfolders.vdf",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf" # Flatpak
|
||||
]
|
||||
|
||||
libraryfolders_vdf_path: Optional[Path] = None
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -152,8 +152,10 @@ class ModlistMenuHandler:
|
||||
self.path_handler = PathHandler()
|
||||
self.vdf_handler = VDFHandler()
|
||||
|
||||
# Determine Steam Deck status (already done by ConfigHandler, use it)
|
||||
self.steamdeck = config_handler.settings.get('steamdeck', False)
|
||||
# Determine Steam Deck status using centralized detection
|
||||
from ..services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
self.steamdeck = platform_service.is_steamdeck
|
||||
|
||||
# Create the resolution handler
|
||||
self.resolution_handler = ResolutionHandler()
|
||||
@@ -178,7 +180,13 @@ class ModlistMenuHandler:
|
||||
self.logger.error(f"Error initializing ModlistMenuHandler: {e}")
|
||||
# Initialize with defaults/empty to prevent errors
|
||||
self.filesystem_handler = FileSystemHandler()
|
||||
self.steamdeck = False
|
||||
# Use centralized detection even in fallback
|
||||
try:
|
||||
from ..services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
self.steamdeck = platform_service.is_steamdeck
|
||||
except:
|
||||
self.steamdeck = False # Final fallback
|
||||
self.modlist_handler = None
|
||||
|
||||
def show_modlist_menu(self):
|
||||
@@ -642,6 +650,10 @@ class ModlistMenuHandler:
|
||||
print("Modlist Install and Configuration complete!")
|
||||
print(f"• You should now be able to Launch '{context.get('name')}' through Steam")
|
||||
print("• Congratulations and enjoy the game!")
|
||||
print("")
|
||||
print("NOTE: If you experience ENB issues, consider using GE-Proton 10-14 instead of")
|
||||
print(" Valve's Proton 10 (known ENB compatibility issues in Valve's Proton 10).")
|
||||
print("")
|
||||
print("Detailed log available at: ~/Jackify/logs/Configure_New_Modlist_workflow.log")
|
||||
# Only wait for input in CLI mode, not GUI mode
|
||||
if not gui_mode:
|
||||
|
||||
@@ -109,6 +109,12 @@ class ModlistHandler:
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.propagate = False
|
||||
self.steamdeck = steamdeck
|
||||
|
||||
# DEBUG: Log ModlistHandler instantiation details for SD card path debugging
|
||||
import traceback
|
||||
caller_info = traceback.extract_stack()[-2] # Get caller info
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] ModlistHandler created: id={id(self)}, steamdeck={steamdeck}")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] Created from: {caller_info.filename}:{caller_info.lineno} in {caller_info.name}()")
|
||||
self.steam_path: Optional[Path] = None
|
||||
self.verbose = verbose # Store verbose flag
|
||||
self.mo2_path: Optional[Path] = None
|
||||
@@ -321,12 +327,19 @@ class ModlistHandler:
|
||||
|
||||
# Determine if modlist is on SD card (Steam Deck only)
|
||||
# On non-Steam Deck systems, /media mounts should use Z: drive, not D: drive
|
||||
if (str(self.modlist_dir).startswith("/run/media") or str(self.modlist_dir).startswith("/media")) and self.steamdeck:
|
||||
is_on_sdcard_path = str(self.modlist_dir).startswith("/run/media") or str(self.modlist_dir).startswith("/media")
|
||||
|
||||
# Log SD card detection for debugging
|
||||
self.logger.debug(f"SD card detection: modlist_dir={self.modlist_dir}, is_sdcard_path={is_on_sdcard_path}, steamdeck={self.steamdeck}")
|
||||
|
||||
if is_on_sdcard_path and self.steamdeck:
|
||||
self.modlist_sdcard = True
|
||||
self.logger.info("Modlist appears to be on an SD card (Steam Deck).")
|
||||
self.logger.debug(f"Set modlist_sdcard=True")
|
||||
else:
|
||||
self.modlist_sdcard = False
|
||||
if (str(self.modlist_dir).startswith("/run/media") or str(self.modlist_dir).startswith("/media")) and not self.steamdeck:
|
||||
self.logger.debug(f"Set modlist_sdcard=False (is_on_sdcard_path={is_on_sdcard_path}, steamdeck={self.steamdeck})")
|
||||
if is_on_sdcard_path and not self.steamdeck:
|
||||
self.logger.info("Modlist on /media mount detected on non-Steam Deck system - using Z: drive mapping.")
|
||||
|
||||
# Find and set compatdata path now that we have appid
|
||||
@@ -672,25 +685,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)")
|
||||
@@ -724,6 +718,8 @@ class ModlistHandler:
|
||||
success = self.winetricks_handler.install_wine_components(wineprefix, self.game_var_full, specific_components=components)
|
||||
if success:
|
||||
self.logger.info("Wine component installation completed successfully")
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Wine components verified and installed successfully")
|
||||
else:
|
||||
self.logger.error("Wine component installation failed")
|
||||
print("Error: Failed to install necessary Wine components.")
|
||||
@@ -734,6 +730,39 @@ 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 4.6: Enable dotfiles visibility for Wine prefix
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Enabling dotfiles visibility")
|
||||
self.logger.info("Step 4.6: Enabling dotfiles visibility in Wine prefix...")
|
||||
try:
|
||||
if self.protontricks_handler.enable_dotfiles(self.appid):
|
||||
self.logger.info("Dotfiles visibility enabled successfully")
|
||||
else:
|
||||
self.logger.warning("Failed to enable dotfiles visibility (non-critical, continuing)")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error enabling dotfiles visibility: {e} (non-critical, continuing)")
|
||||
self.logger.info("Step 4.6: Enabling dotfiles visibility... Done")
|
||||
|
||||
# Step 5: Ensure permissions of Modlist directory
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Setting ownership and permissions for modlist directory")
|
||||
@@ -812,6 +841,15 @@ class ModlistHandler:
|
||||
# Conditionally update binary and working directory paths
|
||||
# Skip for jackify-engine workflows since paths are already correct
|
||||
# Exception: Always run for SD card installs to fix Z:/run/media/... to D:/... paths
|
||||
|
||||
# DEBUG: Add comprehensive logging to identify Steam Deck SD card path manipulation issues
|
||||
engine_installed = getattr(self, 'engine_installed', False)
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] ModlistHandler instance: id={id(self)}")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] engine_installed: {engine_installed}")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] modlist_sdcard: {self.modlist_sdcard}")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] steamdeck parameter passed to constructor: {getattr(self, 'steamdeck', 'NOT_SET')}")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] Path manipulation condition: not {engine_installed} or {self.modlist_sdcard} = {not engine_installed or self.modlist_sdcard}")
|
||||
|
||||
if not getattr(self, 'engine_installed', False) or self.modlist_sdcard:
|
||||
# Convert steamapps/common path to library root path
|
||||
steam_libraries = None
|
||||
@@ -831,7 +869,8 @@ class ModlistHandler:
|
||||
print("Error: Failed to update binary and working directory paths in ModOrganizer.ini.")
|
||||
return False # Abort on failure
|
||||
else:
|
||||
self.logger.debug("Skipping path manipulation - jackify-engine already set correct paths in ModOrganizer.ini")
|
||||
self.logger.debug("[SD_CARD_DEBUG] Skipping path manipulation - jackify-engine already set correct paths in ModOrganizer.ini")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] SKIPPED because: engine_installed={engine_installed} and modlist_sdcard={self.modlist_sdcard}")
|
||||
self.logger.info("Step 8: Updating ModOrganizer.ini paths... Done")
|
||||
|
||||
# Step 9: Update Resolution Settings (if applicable)
|
||||
@@ -1501,14 +1540,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()
|
||||
@@ -1516,11 +1559,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...")
|
||||
@@ -1530,7 +1589,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:
|
||||
@@ -1545,18 +1604,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}")
|
||||
@@ -390,7 +390,7 @@ class PathHandler:
|
||||
libraryfolders_vdf_paths = [
|
||||
os.path.expanduser("~/.steam/steam/config/libraryfolders.vdf"),
|
||||
os.path.expanduser("~/.local/share/Steam/config/libraryfolders.vdf"),
|
||||
# Add other potential standard locations if necessary
|
||||
os.path.expanduser("~/.var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf"), # Flatpak
|
||||
]
|
||||
|
||||
# Simple backup mechanism (optional but good practice)
|
||||
@@ -622,7 +622,9 @@ class PathHandler:
|
||||
m = re.search(r'"path"\s*"([^"]+)"', line)
|
||||
if m:
|
||||
lib_path = Path(m.group(1))
|
||||
library_paths.add(lib_path)
|
||||
# Resolve symlinks for consistency (mmcblk0p1 -> deck/UUID)
|
||||
resolved_path = lib_path.resolve()
|
||||
library_paths.add(resolved_path)
|
||||
except Exception as e:
|
||||
logger.error(f"[DEBUG] Failed to parse {vdf_path}: {e}")
|
||||
logger.info(f"[DEBUG] All detected Steam libraries: {library_paths}")
|
||||
@@ -676,10 +678,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 +696,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:
|
||||
@@ -777,17 +779,36 @@ class PathHandler:
|
||||
|
||||
# Extract existing gamePath to use as source of truth for vanilla game location
|
||||
existing_game_path = None
|
||||
for line in lines:
|
||||
gamepath_line_index = -1
|
||||
for i, line in enumerate(lines):
|
||||
if re.match(r'^\s*gamepath\s*=.*@ByteArray\(([^)]+)\)', line, re.IGNORECASE):
|
||||
match = re.search(r'@ByteArray\(([^)]+)\)', line)
|
||||
if match:
|
||||
raw_path = match.group(1)
|
||||
gamepath_line_index = i
|
||||
# Convert Windows path back to Linux path
|
||||
if raw_path.startswith(('Z:', 'D:')):
|
||||
linux_path = raw_path[2:].replace('\\\\', '/').replace('\\', '/')
|
||||
existing_game_path = linux_path
|
||||
logger.debug(f"Extracted existing gamePath: {existing_game_path}")
|
||||
break
|
||||
|
||||
# Special handling for gamePath in three-true scenario (engine_installed + steamdeck + sdcard)
|
||||
if modlist_sdcard and existing_game_path and existing_game_path.startswith('/run/media') and gamepath_line_index != -1:
|
||||
# Simple manual stripping of /run/media/deck/UUID pattern for SD card paths
|
||||
# Match /run/media/deck/[UUID]/Games/... and extract just /Games/...
|
||||
sdcard_pattern = r'^/run/media/deck/[^/]+(/Games/.*)$'
|
||||
match = re.match(sdcard_pattern, existing_game_path)
|
||||
if match:
|
||||
stripped_path = match.group(1) # Just the /Games/... part
|
||||
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()}")
|
||||
lines[gamepath_line_index] = new_gamepath_line
|
||||
else:
|
||||
logger.warning(f"SD card path doesn't match expected pattern: {existing_game_path}")
|
||||
|
||||
game_path_updated = False
|
||||
binary_paths_updated = 0
|
||||
@@ -852,10 +873,9 @@ class PathHandler:
|
||||
else:
|
||||
found_stock = None
|
||||
for folder in STOCK_GAME_FOLDERS:
|
||||
folder_pattern = f"/{folder.replace(' ', '')}".lower()
|
||||
value_part_lower = value_part.replace(' ', '').lower()
|
||||
if folder_pattern in value_part_lower:
|
||||
idx = value_part_lower.index(folder_pattern)
|
||||
folder_pattern = f"/{folder}"
|
||||
if folder_pattern in value_part:
|
||||
idx = value_part.index(folder_pattern)
|
||||
rel_path = value_part[idx:].lstrip('/')
|
||||
found_stock = folder
|
||||
break
|
||||
|
||||
@@ -117,23 +117,21 @@ class ProtontricksHandler:
|
||||
try:
|
||||
# PyInstaller fix: Comprehensive environment cleaning for subprocess calls
|
||||
env = self._get_clean_subprocess_env()
|
||||
|
||||
|
||||
result = subprocess.run(
|
||||
["flatpak", "list"],
|
||||
capture_output=True,
|
||||
["flatpak", "list"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, # Suppress stderr to avoid error messages when flatpak not installed
|
||||
text=True,
|
||||
check=True,
|
||||
env=env # Use comprehensively cleaned environment
|
||||
)
|
||||
if "com.github.Matoking.protontricks" in result.stdout:
|
||||
if result.returncode == 0 and "com.github.Matoking.protontricks" in result.stdout:
|
||||
logger.info("Flatpak Protontricks is installed")
|
||||
self.which_protontricks = 'flatpak'
|
||||
flatpak_installed = True
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
logger.warning("'flatpak' command not found. Cannot check for Flatpak Protontricks.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning(f"Error checking flatpak list: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error checking flatpak: {e}")
|
||||
|
||||
@@ -149,9 +147,29 @@ class ProtontricksHandler:
|
||||
should_install = True
|
||||
else:
|
||||
try:
|
||||
response = input("Protontricks not found. Install the Flatpak version? (Y/n): ").lower()
|
||||
if response == 'y' or response == '':
|
||||
print("\nProtontricks not found. Choose installation method:")
|
||||
print("1. Install via Flatpak (automatic)")
|
||||
print("2. Install via native package manager (manual)")
|
||||
print("3. Skip (Use bundled winetricks instead)")
|
||||
choice = input("Enter choice (1/2/3): ").strip()
|
||||
|
||||
if choice == '1' or choice == '':
|
||||
should_install = True
|
||||
elif choice == '2':
|
||||
print("\nTo install protontricks via your system package manager:")
|
||||
print("• Ubuntu/Debian: sudo apt install protontricks")
|
||||
print("• Fedora: sudo dnf install protontricks")
|
||||
print("• Arch Linux: sudo pacman -S protontricks")
|
||||
print("• openSUSE: sudo zypper install protontricks")
|
||||
print("\nAfter installation, please rerun Jackify.")
|
||||
return False
|
||||
elif choice == '3':
|
||||
print("Skipping protontricks installation. Will use bundled winetricks for component installation.")
|
||||
logger.info("User chose to skip protontricks and use winetricks fallback")
|
||||
return False
|
||||
else:
|
||||
print("Invalid choice. Installation cancelled.")
|
||||
return False
|
||||
except KeyboardInterrupt:
|
||||
print("\nInstallation cancelled.")
|
||||
return False
|
||||
@@ -687,8 +705,15 @@ class ProtontricksHandler:
|
||||
result = self.run_protontricks("--no-bwrap", appid, "-q", *components_to_install, env=env, timeout=600)
|
||||
self.logger.debug(f"Protontricks output: {result.stdout if result else ''}")
|
||||
if result and result.returncode == 0:
|
||||
self.logger.info("Wine Component installation command completed successfully.")
|
||||
return True
|
||||
self.logger.info("Wine Component installation command completed.")
|
||||
|
||||
# Verify components were actually installed
|
||||
if self._verify_components_installed(appid, components_to_install):
|
||||
self.logger.info("Component verification successful - all components installed correctly.")
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Component verification failed (Attempt {attempt}/{max_attempts})")
|
||||
# Continue to retry
|
||||
else:
|
||||
self.logger.error(f"Protontricks command failed (Attempt {attempt}/{max_attempts}). Return Code: {result.returncode if result else 'N/A'}")
|
||||
self.logger.error(f"Stdout: {result.stdout.strip() if result else ''}")
|
||||
@@ -698,14 +723,73 @@ class ProtontricksHandler:
|
||||
self.logger.error(f"Failed to install Wine components after {max_attempts} attempts.")
|
||||
return False
|
||||
|
||||
def _verify_components_installed(self, appid: str, components: List[str]) -> bool:
|
||||
"""
|
||||
Verify that Wine components were actually installed by querying protontricks.
|
||||
|
||||
Args:
|
||||
appid: Steam AppID
|
||||
components: List of components that should be installed
|
||||
|
||||
Returns:
|
||||
bool: True if all critical components are verified, False otherwise
|
||||
"""
|
||||
try:
|
||||
self.logger.info("Verifying installed components...")
|
||||
|
||||
# Run protontricks list-installed to get actual installed components
|
||||
result = self.run_protontricks("--no-bwrap", appid, "list-installed", timeout=30)
|
||||
|
||||
if not result or result.returncode != 0:
|
||||
self.logger.error("Failed to query installed components")
|
||||
self.logger.debug(f"list-installed stderr: {result.stderr if result else 'N/A'}")
|
||||
return False
|
||||
|
||||
installed_output = result.stdout.lower()
|
||||
self.logger.debug(f"Installed components output: {installed_output}")
|
||||
|
||||
# Define critical components that MUST be installed
|
||||
# These are the core components that determine success
|
||||
critical_components = ["vcrun2022", "xact"]
|
||||
|
||||
# Check for critical components
|
||||
missing_critical = []
|
||||
for component in critical_components:
|
||||
if component.lower() not in installed_output:
|
||||
missing_critical.append(component)
|
||||
|
||||
if missing_critical:
|
||||
self.logger.error(f"CRITICAL: Missing essential components: {missing_critical}")
|
||||
self.logger.error("Installation reported success but components are NOT installed")
|
||||
return False
|
||||
|
||||
# Check for requested components (warn but don't fail)
|
||||
missing_requested = []
|
||||
for component in components:
|
||||
# Handle settings like fontsmooth=rgb (just check the base component name)
|
||||
base_component = component.split('=')[0].lower()
|
||||
if base_component not in installed_output and component.lower() not in installed_output:
|
||||
missing_requested.append(component)
|
||||
|
||||
if missing_requested:
|
||||
self.logger.warning(f"Some requested components may not be installed: {missing_requested}")
|
||||
self.logger.warning("This may cause issues, but critical components are present")
|
||||
|
||||
self.logger.info(f"Verification passed - critical components confirmed: {critical_components}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error verifying components: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def _cleanup_wine_processes(self):
|
||||
"""
|
||||
Internal method to clean up wine processes during component installation
|
||||
"""
|
||||
try:
|
||||
subprocess.run("pgrep -f 'win7|win10|ShowDotFiles|protontricks' | xargs -r kill -9",
|
||||
subprocess.run("pgrep -f 'win7|win10|ShowDotFiles|protontricks' | xargs -r kill -9",
|
||||
shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
subprocess.run("pkill -9 winetricks",
|
||||
subprocess.run("pkill -9 winetricks",
|
||||
shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up wine processes: {e}")
|
||||
|
||||
@@ -200,40 +200,55 @@ class WineUtils:
|
||||
@staticmethod
|
||||
def _get_sd_card_mounts():
|
||||
"""
|
||||
Dynamically detect all current SD card mount points
|
||||
Returns list of mount point paths
|
||||
Detect SD card mount points using df.
|
||||
Returns list of actual mount paths from /run/media (e.g., /run/media/deck/MicroSD).
|
||||
"""
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(['df', '-h'], capture_output=True, text=True, timeout=5)
|
||||
sd_mounts = []
|
||||
for line in result.stdout.split('\n'):
|
||||
# Look for common SD card mount patterns
|
||||
if '/run/media' in line or ('/mnt' in line and 'sdcard' in line.lower()):
|
||||
parts = line.split()
|
||||
if len(parts) >= 6: # df output has 6+ columns
|
||||
mount_point = parts[-1] # Last column is mount point
|
||||
if mount_point.startswith(('/run/media', '/mnt')):
|
||||
sd_mounts.append(mount_point)
|
||||
return sd_mounts
|
||||
except Exception:
|
||||
# Fallback to common patterns if df fails
|
||||
return ['/run/media/mmcblk0p1', '/run/media/deck']
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
result = subprocess.run(['df', '-h'], capture_output=True, text=True, timeout=5)
|
||||
sd_mounts = []
|
||||
|
||||
for line in result.stdout.split('\n'):
|
||||
if '/run/media' in line:
|
||||
parts = line.split()
|
||||
if len(parts) >= 6:
|
||||
mount_point = parts[-1] # Last column is the mount point
|
||||
if mount_point.startswith('/run/media/'):
|
||||
sd_mounts.append(mount_point)
|
||||
|
||||
# Sort by length (longest first) to match most specific paths first
|
||||
sd_mounts.sort(key=len, reverse=True)
|
||||
logger.debug(f"Detected SD card mounts from df: {sd_mounts}")
|
||||
return sd_mounts
|
||||
|
||||
@staticmethod
|
||||
def _strip_sdcard_path(path):
|
||||
"""
|
||||
Strip any detected SD card mount prefix from paths
|
||||
Handles both /run/media/mmcblk0p1 and /run/media/deck/UUID patterns
|
||||
Strip SD card mount prefix from path.
|
||||
Handles both /run/media/mmcblk0p1 and /run/media/deck/UUID patterns.
|
||||
Pattern: /run/media/deck/UUID/Games/... becomes /Games/...
|
||||
Pattern: /run/media/mmcblk0p1/Games/... becomes /Games/...
|
||||
"""
|
||||
sd_mounts = WineUtils._get_sd_card_mounts()
|
||||
import re
|
||||
|
||||
for mount in sd_mounts:
|
||||
if path.startswith(mount):
|
||||
# Strip the mount prefix and ensure proper leading slash
|
||||
relative_path = path[len(mount):].lstrip('/')
|
||||
return "/" + relative_path if relative_path else "/"
|
||||
# Pattern 1: /run/media/deck/UUID/... strip everything up to and including UUID
|
||||
# This matches the bash: "${path#*/run/media/deck/*/*}"
|
||||
deck_pattern = r'^/run/media/deck/[^/]+(/.*)?$'
|
||||
match = re.match(deck_pattern, path)
|
||||
if match:
|
||||
stripped = match.group(1) if match.group(1) else "/"
|
||||
logger.debug(f"Stripped SD card path (deck pattern): {path} -> {stripped}")
|
||||
return stripped
|
||||
|
||||
# Pattern 2: /run/media/mmcblk0p1/... strip /run/media/mmcblk0p1
|
||||
# This matches the bash: "${path#*mmcblk0p1}"
|
||||
if path.startswith('/run/media/mmcblk0p1/'):
|
||||
stripped = path.replace('/run/media/mmcblk0p1', '', 1)
|
||||
logger.debug(f"Stripped SD card path (mmcblk pattern): {path} -> {stripped}")
|
||||
return stripped
|
||||
|
||||
# No SD card pattern matched
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
@@ -668,7 +683,10 @@ class WineUtils:
|
||||
# Add standard compatibility tool locations (covers edge cases like Flatpak)
|
||||
compatibility_paths.extend([
|
||||
Path.home() / ".steam/root/compatibilitytools.d",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/compatibilitytools.d"
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/compatibilitytools.d",
|
||||
# Flatpak GE-Proton extension paths
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam.CompatibilityTool.Proton-GE/.local/share/Steam/compatibilitytools.d",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/compatibilitytools.d/GE-Proton"
|
||||
])
|
||||
# Special handling for Proton 9: try all possible directory names
|
||||
if proton_version.strip().startswith("Proton 9"):
|
||||
@@ -822,7 +840,12 @@ class WineUtils:
|
||||
"""
|
||||
compat_paths = [
|
||||
Path.home() / ".steam/steam/compatibilitytools.d",
|
||||
Path.home() / ".local/share/Steam/compatibilitytools.d"
|
||||
Path.home() / ".local/share/Steam/compatibilitytools.d",
|
||||
Path.home() / ".steam/root/compatibilitytools.d",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/compatibilitytools.d",
|
||||
# Flatpak GE-Proton extension paths
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam.CompatibilityTool.Proton-GE/.local/share/Steam/compatibilitytools.d",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/compatibilitytools.d/GE-Proton"
|
||||
]
|
||||
|
||||
# Return only existing paths
|
||||
|
||||
@@ -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,
|
||||
@@ -315,10 +349,17 @@ class WinetricksHandler:
|
||||
|
||||
self.logger.debug(f"Winetricks output: {result.stdout}")
|
||||
if result.returncode == 0:
|
||||
self.logger.info("Wine Component installation command completed successfully.")
|
||||
# Set Windows 10 mode after component installation (matches legacy script timing)
|
||||
self._set_windows_10_mode(wineprefix, env.get('WINE', ''))
|
||||
return True
|
||||
self.logger.info("Wine Component installation command completed.")
|
||||
|
||||
# Verify components were actually installed
|
||||
if self._verify_components_installed(wineprefix, components_to_install, env):
|
||||
self.logger.info("Component verification successful - all components installed correctly.")
|
||||
# Set Windows 10 mode after component installation (matches legacy script timing)
|
||||
self._set_windows_10_mode(wineprefix, env.get('WINE', ''))
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Component verification failed (Attempt {attempt}/{max_attempts})")
|
||||
# Continue to retry
|
||||
else:
|
||||
# Special handling for dotnet40 verification issue (mimics protontricks behavior)
|
||||
if "dotnet40" in components_to_install and "ngen.exe not found" in result.stderr:
|
||||
@@ -337,14 +378,117 @@ 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.")
|
||||
|
||||
# Network diagnostics before fallback (non-fatal)
|
||||
self.logger.warning("=" * 80)
|
||||
self.logger.warning("NETWORK DIAGNOSTICS: Testing connectivity to component download sources...")
|
||||
try:
|
||||
# Check if curl is available
|
||||
curl_check = subprocess.run(['which', 'curl'], capture_output=True, timeout=5)
|
||||
if curl_check.returncode == 0:
|
||||
# Test Microsoft download servers (used by winetricks for .NET, VC runtimes, DirectX)
|
||||
test_result = subprocess.run(['curl', '-I', '--max-time', '10', 'https://download.microsoft.com'],
|
||||
capture_output=True, text=True, timeout=15)
|
||||
if test_result.returncode == 0:
|
||||
self.logger.warning("Can reach download.microsoft.com")
|
||||
else:
|
||||
self.logger.error("Cannot reach download.microsoft.com - network/DNS issue likely")
|
||||
self.logger.error(f" Curl exit code: {test_result.returncode}")
|
||||
if test_result.stderr:
|
||||
self.logger.error(f" Curl error: {test_result.stderr.strip()}")
|
||||
else:
|
||||
self.logger.warning("curl not available, skipping network diagnostic test")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Network diagnostic test skipped: {e}")
|
||||
self.logger.warning("=" * 80)
|
||||
|
||||
# Check if protontricks is available for fallback using centralized handler
|
||||
try:
|
||||
from .protontricks_handler import ProtontricksHandler
|
||||
protontricks_handler = ProtontricksHandler()
|
||||
protontricks_available = protontricks_handler.is_available()
|
||||
|
||||
if protontricks_available:
|
||||
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:
|
||||
@@ -609,13 +753,6 @@ class WinetricksHandler:
|
||||
|
||||
if success:
|
||||
self.logger.info(f"Legacy .NET components {legacy_components} installed successfully with protontricks")
|
||||
|
||||
# Enable dotfiles and symlinks for the prefix
|
||||
if protontricks_handler.enable_dotfiles(appid):
|
||||
self.logger.info("Enabled dotfiles and symlinks support")
|
||||
else:
|
||||
self.logger.warning("Failed to enable dotfiles/symlinks (non-critical)")
|
||||
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Legacy .NET components {legacy_components} installation failed with protontricks")
|
||||
@@ -732,11 +869,18 @@ class WinetricksHandler:
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.logger.info(f"Winetricks components installed successfully: {components}")
|
||||
# Set Windows 10 mode after component installation (matches legacy script timing)
|
||||
wine_binary = env.get('WINE', '')
|
||||
self._set_windows_10_mode(env.get('WINEPREFIX', ''), wine_binary)
|
||||
return True
|
||||
self.logger.info(f"Winetricks components installation command completed.")
|
||||
|
||||
# Verify components were actually installed
|
||||
if self._verify_components_installed(wineprefix, components, env):
|
||||
self.logger.info("Component verification successful - all components installed correctly.")
|
||||
# Set Windows 10 mode after component installation (matches legacy script timing)
|
||||
wine_binary = env.get('WINE', '')
|
||||
self._set_windows_10_mode(env.get('WINEPREFIX', ''), wine_binary)
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Component verification failed (attempt {attempt})")
|
||||
# Continue to retry
|
||||
else:
|
||||
self.logger.error(f"Winetricks failed (attempt {attempt}): {result.stderr.strip()}")
|
||||
|
||||
@@ -880,6 +1024,70 @@ class WinetricksHandler:
|
||||
self.logger.error(f"Error getting wine binary for prefix: {e}")
|
||||
return ""
|
||||
|
||||
def _verify_components_installed(self, wineprefix: str, components: List[str], env: dict) -> bool:
|
||||
"""
|
||||
Verify that Wine components were actually installed by checking winetricks.log.
|
||||
|
||||
Args:
|
||||
wineprefix: Wine prefix path
|
||||
components: List of components that should be installed
|
||||
env: Environment variables (includes WINE path)
|
||||
|
||||
Returns:
|
||||
bool: True if all critical components are verified, False otherwise
|
||||
"""
|
||||
try:
|
||||
self.logger.info("Verifying installed components...")
|
||||
|
||||
# Check winetricks.log file for installed components
|
||||
winetricks_log = os.path.join(wineprefix, 'winetricks.log')
|
||||
|
||||
if not os.path.exists(winetricks_log):
|
||||
self.logger.error(f"winetricks.log not found at {winetricks_log}")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(winetricks_log, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
log_content = f.read().lower()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to read winetricks.log: {e}")
|
||||
return False
|
||||
|
||||
self.logger.debug(f"winetricks.log length: {len(log_content)} bytes")
|
||||
|
||||
# Define critical components that MUST be installed
|
||||
critical_components = ["vcrun2022", "xact"]
|
||||
|
||||
# Check for critical components
|
||||
missing_critical = []
|
||||
for component in critical_components:
|
||||
if component.lower() not in log_content:
|
||||
missing_critical.append(component)
|
||||
|
||||
if missing_critical:
|
||||
self.logger.error(f"CRITICAL: Missing essential components: {missing_critical}")
|
||||
self.logger.error("Installation reported success but components are NOT in winetricks.log")
|
||||
return False
|
||||
|
||||
# Check for requested components (warn but don't fail)
|
||||
missing_requested = []
|
||||
for component in components:
|
||||
# Handle settings like fontsmooth=rgb (just check the base component name)
|
||||
base_component = component.split('=')[0].lower()
|
||||
if base_component not in log_content and component.lower() not in log_content:
|
||||
missing_requested.append(component)
|
||||
|
||||
if missing_requested:
|
||||
self.logger.warning(f"Some requested components may not be installed: {missing_requested}")
|
||||
self.logger.warning("This may cause issues, but critical components are present")
|
||||
|
||||
self.logger.info(f"Verification passed - critical components confirmed: {critical_components}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error verifying components: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def _cleanup_wine_processes(self):
|
||||
"""
|
||||
Internal method to clean up wine processes during component installation
|
||||
|
||||
@@ -2697,9 +2697,18 @@ echo Prefix creation complete.
|
||||
# Run proton run wineboot -u to initialize the prefix
|
||||
cmd = [str(proton_path), 'run', 'wineboot', '-u']
|
||||
logger.info(f"Running: {' '.join(cmd)}")
|
||||
|
||||
|
||||
# Adjust timeout for SD card installations on Steam Deck (slower I/O)
|
||||
from ..services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
is_steamdeck_sdcard = (platform_service.is_steamdeck and
|
||||
str(proton_path).startswith('/run/media/'))
|
||||
timeout = 180 if is_steamdeck_sdcard else 60
|
||||
if is_steamdeck_sdcard:
|
||||
logger.info(f"Using extended timeout ({timeout}s) for Steam Deck SD card Proton installation")
|
||||
|
||||
# Use jackify-engine's approach: UseShellExecute=false, CreateNoWindow=true equivalent
|
||||
result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=60,
|
||||
result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=timeout,
|
||||
shell=False, creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
|
||||
logger.info(f"Proton exit code: {result.returncode}")
|
||||
|
||||
@@ -2895,10 +2904,21 @@ echo Prefix creation complete.
|
||||
"""Find a Steam game installation path by AppID and common names"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Get Steam libraries from libraryfolders.vdf
|
||||
steam_config_path = Path.home() / ".steam/steam/config/libraryfolders.vdf"
|
||||
if not steam_config_path.exists():
|
||||
|
||||
# Get Steam libraries from libraryfolders.vdf - check multiple possible locations
|
||||
possible_config_paths = [
|
||||
Path.home() / ".steam/steam/config/libraryfolders.vdf",
|
||||
Path.home() / ".local/share/Steam/config/libraryfolders.vdf",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf" # Flatpak
|
||||
]
|
||||
|
||||
steam_config_path = None
|
||||
for path in possible_config_paths:
|
||||
if path.exists():
|
||||
steam_config_path = path
|
||||
break
|
||||
|
||||
if not steam_config_path:
|
||||
return None
|
||||
|
||||
steam_libraries = []
|
||||
@@ -3011,7 +3031,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:
|
||||
@@ -3026,7 +3046,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:
|
||||
|
||||
@@ -34,8 +34,10 @@ class ModlistService:
|
||||
"""Lazy initialization of modlist handler."""
|
||||
if self._modlist_handler is None:
|
||||
from ..handlers.modlist_handler import ModlistHandler
|
||||
# Initialize with proper dependencies
|
||||
self._modlist_handler = ModlistHandler()
|
||||
from ..services.platform_detection_service import PlatformDetectionService
|
||||
# Initialize with proper dependencies and centralized Steam Deck detection
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
self._modlist_handler = ModlistHandler(steamdeck=platform_service.is_steamdeck)
|
||||
return self._modlist_handler
|
||||
|
||||
def _get_wabbajack_handler(self):
|
||||
|
||||
@@ -135,6 +135,9 @@ class NativeSteamOperationsService:
|
||||
steam_locations = [
|
||||
Path.home() / ".steam/steam",
|
||||
Path.home() / ".local/share/Steam",
|
||||
# Flatpak Steam - direct data directory
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam",
|
||||
# Flatpak Steam - symlinked home paths
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/home/.steam/steam",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/home/.local/share/Steam"
|
||||
]
|
||||
@@ -161,6 +164,9 @@ class NativeSteamOperationsService:
|
||||
standard_locations = [
|
||||
Path.home() / ".steam/steam/steamapps/compatdata",
|
||||
Path.home() / ".local/share/Steam/steamapps/compatdata",
|
||||
# Flatpak Steam - direct data directory
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/compatdata",
|
||||
# Flatpak Steam - symlinked home paths
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/home/.steam/steam/steamapps/compatdata",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/home/.local/share/Steam/steamapps/compatdata"
|
||||
]
|
||||
|
||||
67
jackify/backend/services/platform_detection_service.py
Normal file
67
jackify/backend/services/platform_detection_service.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Platform Detection Service
|
||||
|
||||
Centralizes platform detection logic (Steam Deck, etc.) to be performed once at application startup
|
||||
and shared across all components.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlatformDetectionService:
|
||||
"""
|
||||
Service for detecting platform-specific information once at startup
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
_is_steamdeck = None
|
||||
|
||||
def __new__(cls):
|
||||
"""Singleton pattern to ensure only one instance"""
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize platform detection if not already done"""
|
||||
if self._is_steamdeck is None:
|
||||
self._detect_platform()
|
||||
|
||||
def _detect_platform(self):
|
||||
"""Perform platform detection once"""
|
||||
logger.debug("Performing platform detection...")
|
||||
|
||||
# Steam Deck detection
|
||||
self._is_steamdeck = False
|
||||
try:
|
||||
if os.path.exists('/etc/os-release'):
|
||||
with open('/etc/os-release', 'r') as f:
|
||||
content = f.read().lower()
|
||||
if 'steamdeck' in content:
|
||||
self._is_steamdeck = True
|
||||
logger.info("Steam Deck platform detected")
|
||||
else:
|
||||
logger.debug("Non-Steam Deck Linux platform detected")
|
||||
else:
|
||||
logger.debug("No /etc/os-release found - assuming non-Steam Deck platform")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error detecting Steam Deck platform: {e}")
|
||||
self._is_steamdeck = False
|
||||
|
||||
logger.debug(f"Platform detection complete: is_steamdeck={self._is_steamdeck}")
|
||||
|
||||
@property
|
||||
def is_steamdeck(self) -> bool:
|
||||
"""Get Steam Deck detection result"""
|
||||
if self._is_steamdeck is None:
|
||||
self._detect_platform()
|
||||
return self._is_steamdeck
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""Get the singleton instance"""
|
||||
return cls()
|
||||
@@ -127,20 +127,18 @@ class ProtontricksDetectionService:
|
||||
try:
|
||||
env = handler._get_clean_subprocess_env()
|
||||
result = subprocess.run(
|
||||
["flatpak", "list"],
|
||||
capture_output=True,
|
||||
["flatpak", "list"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, # Suppress stderr to avoid error messages
|
||||
text=True,
|
||||
check=True,
|
||||
env=env
|
||||
)
|
||||
if "com.github.Matoking.protontricks" in result.stdout:
|
||||
if result.returncode == 0 and "com.github.Matoking.protontricks" in result.stdout:
|
||||
logger.info("Flatpak Protontricks is installed")
|
||||
handler.which_protontricks = 'flatpak'
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
logger.warning("'flatpak' command not found. Cannot check for Flatpak Protontricks.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning(f"Error checking flatpak list: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error checking flatpak: {e}")
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import signal
|
||||
import psutil
|
||||
import logging
|
||||
import sys
|
||||
import shutil
|
||||
from typing import Callable, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -86,6 +87,25 @@ def is_steam_deck() -> bool:
|
||||
logger.debug(f"Error detecting Steam Deck: {e}")
|
||||
return False
|
||||
|
||||
def is_flatpak_steam() -> bool:
|
||||
"""Detect if Steam is installed as a Flatpak."""
|
||||
try:
|
||||
# First check if flatpak command exists
|
||||
if not shutil.which('flatpak'):
|
||||
return False
|
||||
|
||||
# Verify the app is actually installed (not just directory exists)
|
||||
result = subprocess.run(['flatpak', 'list', '--app'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, # Suppress stderr to avoid error messages
|
||||
text=True,
|
||||
timeout=5)
|
||||
if result.returncode == 0 and 'com.valvesoftware.Steam' in result.stdout:
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Error detecting Flatpak Steam: {e}")
|
||||
return False
|
||||
|
||||
def get_steam_processes() -> list:
|
||||
"""Return a list of psutil.Process objects for running Steam processes."""
|
||||
steam_procs = []
|
||||
@@ -122,16 +142,37 @@ def start_steam() -> bool:
|
||||
"""Attempt to start Steam using the exact methods from existing working logic."""
|
||||
env = _get_clean_subprocess_env()
|
||||
try:
|
||||
# Try systemd user service (Steam Deck)
|
||||
# Try systemd user service (Steam Deck) - HIGHEST PRIORITY
|
||||
if is_steam_deck():
|
||||
subprocess.Popen(["systemctl", "--user", "restart", "app-steam@autostart.service"], env=env)
|
||||
return True
|
||||
|
||||
|
||||
# Check if Flatpak Steam (only if not Steam Deck)
|
||||
if is_flatpak_steam():
|
||||
logger.info("Flatpak Steam detected - using flatpak run command")
|
||||
try:
|
||||
# Redirect flatpak's stderr to suppress "app not installed" errors on systems without flatpak Steam
|
||||
# Steam's own stdout/stderr will still go through (flatpak forwards them)
|
||||
subprocess.Popen(["flatpak", "run", "com.valvesoftware.Steam", "-silent"],
|
||||
env=env, stderr=subprocess.DEVNULL)
|
||||
time.sleep(5)
|
||||
check_result = subprocess.run(['pgrep', '-f', 'steam'], capture_output=True, timeout=10, env=env)
|
||||
if check_result.returncode == 0:
|
||||
logger.info("Flatpak Steam process detected after start.")
|
||||
return True
|
||||
else:
|
||||
logger.warning("Flatpak Steam process not detected after start attempt.")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting Flatpak Steam: {e}")
|
||||
return False
|
||||
|
||||
# Use startup methods with only -silent flag (no -minimized or -no-browser)
|
||||
# Don't redirect stdout/stderr or use start_new_session to allow Steam to connect to display/tray
|
||||
start_methods = [
|
||||
{"name": "Popen", "cmd": ["steam", "-silent"], "kwargs": {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL, "stdin": subprocess.DEVNULL, "start_new_session": True, "env": env}},
|
||||
{"name": "setsid", "cmd": ["setsid", "steam", "-silent"], "kwargs": {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL, "stdin": subprocess.DEVNULL, "env": env}},
|
||||
{"name": "nohup", "cmd": ["nohup", "steam", "-silent"], "kwargs": {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL, "stdin": subprocess.DEVNULL, "start_new_session": True, "preexec_fn": os.setpgrp, "env": env}}
|
||||
{"name": "Popen", "cmd": ["steam", "-silent"], "kwargs": {"env": env}},
|
||||
{"name": "setsid", "cmd": ["setsid", "steam", "-silent"], "kwargs": {"env": env}},
|
||||
{"name": "nohup", "cmd": ["nohup", "steam", "-silent"], "kwargs": {"preexec_fn": os.setpgrp, "env": env}}
|
||||
]
|
||||
|
||||
for method in start_methods:
|
||||
@@ -174,17 +215,26 @@ def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = No
|
||||
progress_callback(msg)
|
||||
|
||||
report("Shutting down Steam...")
|
||||
|
||||
# Steam Deck: Use systemctl for shutdown (special handling)
|
||||
|
||||
# Steam Deck: Use systemctl for shutdown (special handling) - HIGHEST PRIORITY
|
||||
if is_steam_deck():
|
||||
try:
|
||||
report("Steam Deck detected - using systemctl shutdown...")
|
||||
subprocess.run(['systemctl', '--user', 'stop', 'app-steam@autostart.service'],
|
||||
subprocess.run(['systemctl', '--user', 'stop', 'app-steam@autostart.service'],
|
||||
timeout=15, check=False, capture_output=True, env=env)
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
logger.debug(f"systemctl stop failed on Steam Deck: {e}")
|
||||
|
||||
# Flatpak Steam: Use flatpak kill command (only if not Steam Deck)
|
||||
elif is_flatpak_steam():
|
||||
try:
|
||||
report("Flatpak Steam detected - stopping via flatpak...")
|
||||
subprocess.run(['flatpak', 'kill', 'com.valvesoftware.Steam'],
|
||||
timeout=15, check=False, capture_output=True, stderr=subprocess.DEVNULL, env=env)
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
logger.debug(f"flatpak kill failed: {e}")
|
||||
|
||||
# All systems: Use pkill approach (proven 15/16 test success rate)
|
||||
try:
|
||||
# Skip unreliable steam -shutdown, go straight to pkill
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v8.0": {},
|
||||
".NETCoreApp,Version=v8.0/linux-x64": {
|
||||
"jackify-engine/0.3.17": {
|
||||
"jackify-engine/0.3.18": {
|
||||
"dependencies": {
|
||||
"Markdig": "0.40.0",
|
||||
"Microsoft.Extensions.Configuration.Json": "9.0.1",
|
||||
@@ -22,16 +22,16 @@
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"System.CommandLine": "2.0.0-beta4.22272.1",
|
||||
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
|
||||
"Wabbajack.CLI.Builder": "0.3.17",
|
||||
"Wabbajack.Downloaders.Bethesda": "0.3.17",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.17",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17",
|
||||
"Wabbajack.Networking.Discord": "0.3.17",
|
||||
"Wabbajack.Networking.GitHub": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17",
|
||||
"Wabbajack.Server.Lib": "0.3.17",
|
||||
"Wabbajack.Services.OSIntegrated": "0.3.17",
|
||||
"Wabbajack.VFS": "0.3.17",
|
||||
"Wabbajack.CLI.Builder": "0.3.18",
|
||||
"Wabbajack.Downloaders.Bethesda": "0.3.18",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.18",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.18",
|
||||
"Wabbajack.Networking.Discord": "0.3.18",
|
||||
"Wabbajack.Networking.GitHub": "0.3.18",
|
||||
"Wabbajack.Paths.IO": "0.3.18",
|
||||
"Wabbajack.Server.Lib": "0.3.18",
|
||||
"Wabbajack.Services.OSIntegrated": "0.3.18",
|
||||
"Wabbajack.VFS": "0.3.18",
|
||||
"MegaApiClient": "1.0.0.0",
|
||||
"runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.19"
|
||||
},
|
||||
@@ -1781,7 +1781,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Wabbajack.CLI.Builder/0.3.17": {
|
||||
"Wabbajack.CLI.Builder/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Json": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
@@ -1791,109 +1791,109 @@
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"System.CommandLine": "2.0.0-beta4.22272.1",
|
||||
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
|
||||
"Wabbajack.Paths": "0.3.17"
|
||||
"Wabbajack.Paths": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.CLI.Builder.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Common/0.3.17": {
|
||||
"Wabbajack.Common/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"System.Reactive": "6.0.1",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
"Wabbajack.DTOs": "0.3.18",
|
||||
"Wabbajack.Networking.Http": "0.3.18",
|
||||
"Wabbajack.Paths.IO": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Common.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Compiler/0.3.17": {
|
||||
"Wabbajack.Compiler/0.3.18": {
|
||||
"dependencies": {
|
||||
"F23.StringSimilarity": "6.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.17",
|
||||
"Wabbajack.Installer": "0.3.17",
|
||||
"Wabbajack.VFS": "0.3.17",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.18",
|
||||
"Wabbajack.Installer": "0.3.18",
|
||||
"Wabbajack.VFS": "0.3.18",
|
||||
"ini-parser-netstandard": "2.5.2"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Compiler.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Compression.BSA/0.3.17": {
|
||||
"Wabbajack.Compression.BSA/0.3.18": {
|
||||
"dependencies": {
|
||||
"K4os.Compression.LZ4.Streams": "1.3.8",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"SharpZipLib": "1.4.2",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.DTOs": "0.3.17"
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.DTOs": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Compression.BSA.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Compression.Zip/0.3.17": {
|
||||
"Wabbajack.Compression.Zip/0.3.18": {
|
||||
"dependencies": {
|
||||
"Wabbajack.IO.Async": "0.3.17"
|
||||
"Wabbajack.IO.Async": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Compression.Zip.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Configuration/0.3.17": {
|
||||
"Wabbajack.Configuration/0.3.18": {
|
||||
"runtime": {
|
||||
"Wabbajack.Configuration.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.17": {
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.18": {
|
||||
"dependencies": {
|
||||
"LibAES-CTR": "1.1.0",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"SharpZipLib": "1.4.2",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.17"
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.18",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Bethesda.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Dispatcher/0.3.17": {
|
||||
"Wabbajack.Downloaders.Dispatcher/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"Wabbajack.Downloaders.Bethesda": "0.3.17",
|
||||
"Wabbajack.Downloaders.GameFile": "0.3.17",
|
||||
"Wabbajack.Downloaders.GoogleDrive": "0.3.17",
|
||||
"Wabbajack.Downloaders.Http": "0.3.17",
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Downloaders.Manual": "0.3.17",
|
||||
"Wabbajack.Downloaders.MediaFire": "0.3.17",
|
||||
"Wabbajack.Downloaders.Mega": "0.3.17",
|
||||
"Wabbajack.Downloaders.ModDB": "0.3.17",
|
||||
"Wabbajack.Downloaders.Nexus": "0.3.17",
|
||||
"Wabbajack.Downloaders.VerificationCache": "0.3.17",
|
||||
"Wabbajack.Downloaders.WabbajackCDN": "0.3.17",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.17"
|
||||
"Wabbajack.Downloaders.Bethesda": "0.3.18",
|
||||
"Wabbajack.Downloaders.GameFile": "0.3.18",
|
||||
"Wabbajack.Downloaders.GoogleDrive": "0.3.18",
|
||||
"Wabbajack.Downloaders.Http": "0.3.18",
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.18",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.18",
|
||||
"Wabbajack.Downloaders.Manual": "0.3.18",
|
||||
"Wabbajack.Downloaders.MediaFire": "0.3.18",
|
||||
"Wabbajack.Downloaders.Mega": "0.3.18",
|
||||
"Wabbajack.Downloaders.ModDB": "0.3.18",
|
||||
"Wabbajack.Downloaders.Nexus": "0.3.18",
|
||||
"Wabbajack.Downloaders.VerificationCache": "0.3.18",
|
||||
"Wabbajack.Downloaders.WabbajackCDN": "0.3.18",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Dispatcher.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.GameFile/0.3.17": {
|
||||
"Wabbajack.Downloaders.GameFile/0.3.18": {
|
||||
"dependencies": {
|
||||
"GameFinder.StoreHandlers.EADesktop": "4.5.0",
|
||||
"GameFinder.StoreHandlers.EGS": "4.5.0",
|
||||
@@ -1903,360 +1903,360 @@
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.VFS": "0.3.17"
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.18",
|
||||
"Wabbajack.VFS": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.GameFile.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.17": {
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.18": {
|
||||
"dependencies": {
|
||||
"HtmlAgilityPack": "1.11.72",
|
||||
"Microsoft.AspNetCore.Http.Extensions": "2.3.0",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.DTOs": "0.3.18",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.18",
|
||||
"Wabbajack.Networking.Http": "0.3.18",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.GoogleDrive.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Http/0.3.17": {
|
||||
"Wabbajack.Downloaders.Http/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.DTOs": "0.3.18",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.18",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.18",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.18",
|
||||
"Wabbajack.Paths.IO": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Http.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Interfaces/0.3.17": {
|
||||
"Wabbajack.Downloaders.Interfaces/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Wabbajack.Compression.Zip": "0.3.17",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
"Wabbajack.Compression.Zip": "0.3.18",
|
||||
"Wabbajack.DTOs": "0.3.18",
|
||||
"Wabbajack.Paths.IO": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Interfaces.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.17": {
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.18": {
|
||||
"dependencies": {
|
||||
"F23.StringSimilarity": "6.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.18",
|
||||
"Wabbajack.Networking.Http": "0.3.18",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Manual/0.3.17": {
|
||||
"Wabbajack.Downloaders.Manual/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17"
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Manual.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.17": {
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.18": {
|
||||
"dependencies": {
|
||||
"HtmlAgilityPack": "1.11.72",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.18",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.MediaFire.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Mega/0.3.17": {
|
||||
"Wabbajack.Downloaders.Mega/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.18",
|
||||
"Wabbajack.Paths.IO": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Mega.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.ModDB/0.3.17": {
|
||||
"Wabbajack.Downloaders.ModDB/0.3.18": {
|
||||
"dependencies": {
|
||||
"HtmlAgilityPack": "1.11.72",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.18",
|
||||
"Wabbajack.Networking.Http": "0.3.18",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.ModDB.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Nexus/0.3.17": {
|
||||
"Wabbajack.Downloaders.Nexus/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.NexusApi": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17"
|
||||
"Wabbajack.DTOs": "0.3.18",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.18",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.18",
|
||||
"Wabbajack.Networking.Http": "0.3.18",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.18",
|
||||
"Wabbajack.Networking.NexusApi": "0.3.18",
|
||||
"Wabbajack.Paths": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Nexus.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.VerificationCache/0.3.17": {
|
||||
"Wabbajack.Downloaders.VerificationCache/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Stub.System.Data.SQLite.Core.NetStandard": "1.0.119",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
"Wabbajack.DTOs": "0.3.18",
|
||||
"Wabbajack.Paths.IO": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.VerificationCache.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.17": {
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Microsoft.Toolkit.HighPerformance": "7.1.2",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.RateLimiter": "0.3.17"
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.18",
|
||||
"Wabbajack.Networking.Http": "0.3.18",
|
||||
"Wabbajack.RateLimiter": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.WabbajackCDN.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.DTOs/0.3.17": {
|
||||
"Wabbajack.DTOs/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17"
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.18",
|
||||
"Wabbajack.Paths": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.DTOs.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.FileExtractor/0.3.17": {
|
||||
"Wabbajack.FileExtractor/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"OMODFramework": "3.0.1",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Compression.BSA": "0.3.17",
|
||||
"Wabbajack.Hashing.PHash": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17"
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.Compression.BSA": "0.3.18",
|
||||
"Wabbajack.Hashing.PHash": "0.3.18",
|
||||
"Wabbajack.Paths": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.FileExtractor.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Hashing.PHash/0.3.17": {
|
||||
"Wabbajack.Hashing.PHash/0.3.18": {
|
||||
"dependencies": {
|
||||
"BCnEncoder.Net.ImageSharp": "1.1.1",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Shipwreck.Phash": "0.5.0",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.DTOs": "0.3.18",
|
||||
"Wabbajack.Paths": "0.3.18",
|
||||
"Wabbajack.Paths.IO": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Hashing.PHash.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Hashing.xxHash64/0.3.17": {
|
||||
"Wabbajack.Hashing.xxHash64/0.3.18": {
|
||||
"dependencies": {
|
||||
"Wabbajack.Paths": "0.3.17",
|
||||
"Wabbajack.RateLimiter": "0.3.17"
|
||||
"Wabbajack.Paths": "0.3.18",
|
||||
"Wabbajack.RateLimiter": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Hashing.xxHash64.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Installer/0.3.17": {
|
||||
"Wabbajack.Installer/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"Octopus.Octodiff": "2.0.548",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.17",
|
||||
"Wabbajack.Downloaders.GameFile": "0.3.17",
|
||||
"Wabbajack.FileExtractor": "0.3.17",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17",
|
||||
"Wabbajack.VFS": "0.3.17",
|
||||
"Wabbajack.DTOs": "0.3.18",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.18",
|
||||
"Wabbajack.Downloaders.GameFile": "0.3.18",
|
||||
"Wabbajack.FileExtractor": "0.3.18",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.18",
|
||||
"Wabbajack.Paths": "0.3.18",
|
||||
"Wabbajack.Paths.IO": "0.3.18",
|
||||
"Wabbajack.VFS": "0.3.18",
|
||||
"ini-parser-netstandard": "2.5.2"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Installer.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.IO.Async/0.3.17": {
|
||||
"Wabbajack.IO.Async/0.3.18": {
|
||||
"runtime": {
|
||||
"Wabbajack.IO.Async.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.17": {
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
"Wabbajack.DTOs": "0.3.18",
|
||||
"Wabbajack.Networking.Http": "0.3.18",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.BethesdaNet.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.Discord/0.3.17": {
|
||||
"Wabbajack.Networking.Discord/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.Discord.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.GitHub/0.3.17": {
|
||||
"Wabbajack.Networking.GitHub/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Octokit": "14.0.0",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
"Wabbajack.DTOs": "0.3.18",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.GitHub.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.Http/0.3.17": {
|
||||
"Wabbajack.Networking.Http/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Http": "9.0.1",
|
||||
"Microsoft.Extensions.Logging": "9.0.1",
|
||||
"Wabbajack.Configuration": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
"Wabbajack.Configuration": "0.3.18",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.18",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.18",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.18",
|
||||
"Wabbajack.Paths": "0.3.18",
|
||||
"Wabbajack.Paths.IO": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.Http.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.17": {
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.18": {
|
||||
"dependencies": {
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17"
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.Http.Interfaces.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.NexusApi/0.3.17": {
|
||||
"Wabbajack.Networking.NexusApi/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.17"
|
||||
"Wabbajack.DTOs": "0.3.18",
|
||||
"Wabbajack.Networking.Http": "0.3.18",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.18",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.NexusApi.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.WabbajackClientApi/0.3.17": {
|
||||
"Wabbajack.Networking.WabbajackClientApi/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Octokit": "14.0.0",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17",
|
||||
"Wabbajack.VFS.Interfaces": "0.3.17",
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.DTOs": "0.3.18",
|
||||
"Wabbajack.Paths.IO": "0.3.18",
|
||||
"Wabbajack.VFS.Interfaces": "0.3.18",
|
||||
"YamlDotNet": "16.3.0"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.WabbajackClientApi.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Paths/0.3.17": {
|
||||
"Wabbajack.Paths/0.3.18": {
|
||||
"runtime": {
|
||||
"Wabbajack.Paths.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Paths.IO/0.3.17": {
|
||||
"Wabbajack.Paths.IO/0.3.18": {
|
||||
"dependencies": {
|
||||
"Wabbajack.Paths": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.18",
|
||||
"shortid": "4.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Paths.IO.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.RateLimiter/0.3.17": {
|
||||
"Wabbajack.RateLimiter/0.3.18": {
|
||||
"runtime": {
|
||||
"Wabbajack.RateLimiter.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Server.Lib/0.3.17": {
|
||||
"Wabbajack.Server.Lib/0.3.18": {
|
||||
"dependencies": {
|
||||
"FluentFTP": "52.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
@@ -2264,58 +2264,58 @@
|
||||
"Nettle": "3.0.0",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17",
|
||||
"Wabbajack.Services.OSIntegrated": "0.3.17"
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.18",
|
||||
"Wabbajack.Services.OSIntegrated": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Server.Lib.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Services.OSIntegrated/0.3.17": {
|
||||
"Wabbajack.Services.OSIntegrated/0.3.18": {
|
||||
"dependencies": {
|
||||
"DeviceId": "6.8.0",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"Wabbajack.Compiler": "0.3.17",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.17",
|
||||
"Wabbajack.Installer": "0.3.17",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.17",
|
||||
"Wabbajack.Networking.Discord": "0.3.17",
|
||||
"Wabbajack.VFS": "0.3.17"
|
||||
"Wabbajack.Compiler": "0.3.18",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.18",
|
||||
"Wabbajack.Installer": "0.3.18",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.18",
|
||||
"Wabbajack.Networking.Discord": "0.3.18",
|
||||
"Wabbajack.VFS": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Services.OSIntegrated.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.VFS/0.3.17": {
|
||||
"Wabbajack.VFS/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"System.Data.SQLite.Core": "1.0.119",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.FileExtractor": "0.3.17",
|
||||
"Wabbajack.Hashing.PHash": "0.3.17",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17",
|
||||
"Wabbajack.VFS.Interfaces": "0.3.17"
|
||||
"Wabbajack.Common": "0.3.18",
|
||||
"Wabbajack.FileExtractor": "0.3.18",
|
||||
"Wabbajack.Hashing.PHash": "0.3.18",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.18",
|
||||
"Wabbajack.Paths": "0.3.18",
|
||||
"Wabbajack.Paths.IO": "0.3.18",
|
||||
"Wabbajack.VFS.Interfaces": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.VFS.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.VFS.Interfaces/0.3.17": {
|
||||
"Wabbajack.VFS.Interfaces/0.3.18": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17"
|
||||
"Wabbajack.DTOs": "0.3.18",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.18",
|
||||
"Wabbajack.Paths": "0.3.18"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.VFS.Interfaces.dll": {}
|
||||
@@ -2332,7 +2332,7 @@
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"jackify-engine/0.3.17": {
|
||||
"jackify-engine/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
@@ -3021,202 +3021,202 @@
|
||||
"path": "yamldotnet/16.3.0",
|
||||
"hashPath": "yamldotnet.16.3.0.nupkg.sha512"
|
||||
},
|
||||
"Wabbajack.CLI.Builder/0.3.17": {
|
||||
"Wabbajack.CLI.Builder/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Common/0.3.17": {
|
||||
"Wabbajack.Common/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Compiler/0.3.17": {
|
||||
"Wabbajack.Compiler/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Compression.BSA/0.3.17": {
|
||||
"Wabbajack.Compression.BSA/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Compression.Zip/0.3.17": {
|
||||
"Wabbajack.Compression.Zip/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Configuration/0.3.17": {
|
||||
"Wabbajack.Configuration/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.17": {
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Dispatcher/0.3.17": {
|
||||
"Wabbajack.Downloaders.Dispatcher/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.GameFile/0.3.17": {
|
||||
"Wabbajack.Downloaders.GameFile/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.17": {
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Http/0.3.17": {
|
||||
"Wabbajack.Downloaders.Http/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Interfaces/0.3.17": {
|
||||
"Wabbajack.Downloaders.Interfaces/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.17": {
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Manual/0.3.17": {
|
||||
"Wabbajack.Downloaders.Manual/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.17": {
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Mega/0.3.17": {
|
||||
"Wabbajack.Downloaders.Mega/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.ModDB/0.3.17": {
|
||||
"Wabbajack.Downloaders.ModDB/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Nexus/0.3.17": {
|
||||
"Wabbajack.Downloaders.Nexus/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.VerificationCache/0.3.17": {
|
||||
"Wabbajack.Downloaders.VerificationCache/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.17": {
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.DTOs/0.3.17": {
|
||||
"Wabbajack.DTOs/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.FileExtractor/0.3.17": {
|
||||
"Wabbajack.FileExtractor/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Hashing.PHash/0.3.17": {
|
||||
"Wabbajack.Hashing.PHash/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Hashing.xxHash64/0.3.17": {
|
||||
"Wabbajack.Hashing.xxHash64/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Installer/0.3.17": {
|
||||
"Wabbajack.Installer/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.IO.Async/0.3.17": {
|
||||
"Wabbajack.IO.Async/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.17": {
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.Discord/0.3.17": {
|
||||
"Wabbajack.Networking.Discord/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.GitHub/0.3.17": {
|
||||
"Wabbajack.Networking.GitHub/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.Http/0.3.17": {
|
||||
"Wabbajack.Networking.Http/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.17": {
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.NexusApi/0.3.17": {
|
||||
"Wabbajack.Networking.NexusApi/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.WabbajackClientApi/0.3.17": {
|
||||
"Wabbajack.Networking.WabbajackClientApi/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Paths/0.3.17": {
|
||||
"Wabbajack.Paths/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Paths.IO/0.3.17": {
|
||||
"Wabbajack.Paths.IO/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.RateLimiter/0.3.17": {
|
||||
"Wabbajack.RateLimiter/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Server.Lib/0.3.17": {
|
||||
"Wabbajack.Server.Lib/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Services.OSIntegrated/0.3.17": {
|
||||
"Wabbajack.Services.OSIntegrated/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.VFS/0.3.17": {
|
||||
"Wabbajack.VFS/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.VFS.Interfaces/0.3.17": {
|
||||
"Wabbajack.VFS.Interfaces/0.3.18": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
|
||||
Binary file not shown.
BIN
jackify/engine/test.png
Normal file
BIN
jackify/engine/test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 335 B |
@@ -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:
|
||||
|
||||
@@ -183,7 +183,7 @@ class NextStepsDialog(QDialog):
|
||||
def _build_completion_text(self) -> str:
|
||||
"""
|
||||
Build the completion text matching the CLI version from menu_handler.py.
|
||||
|
||||
|
||||
Returns:
|
||||
Formatted completion text string
|
||||
"""
|
||||
@@ -195,6 +195,9 @@ Modlist Install and Configuration complete!:
|
||||
• You should now be able to Launch '{self.modlist_name}' through Steam.
|
||||
• Congratulations and enjoy the game!
|
||||
|
||||
NOTE: If you experience ENB issues, consider using GE-Proton 10-14 instead of
|
||||
Valve's Proton 10 (known ENB compatibility issues in Valve's Proton 10).
|
||||
|
||||
Detailed log available at: ~/Jackify/logs/Configure_New_Modlist_workflow.log"""
|
||||
|
||||
|
||||
return completion_text
|
||||
@@ -40,12 +40,12 @@ class SuccessDialog(QDialog):
|
||||
self.setWindowTitle("Success!")
|
||||
self.setWindowModality(Qt.NonModal)
|
||||
self.setAttribute(Qt.WA_ShowWithoutActivating, True)
|
||||
self.setFixedSize(500, 420)
|
||||
self.setFixedSize(500, 500)
|
||||
self.setWindowFlag(Qt.WindowDoesNotAcceptFocus, True)
|
||||
self.setStyleSheet("QDialog { background: #181818; color: #fff; border-radius: 12px; }" )
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setContentsMargins(30, 30, 30, 30)
|
||||
|
||||
# --- Card background for content ---
|
||||
card = QFrame(self)
|
||||
@@ -53,6 +53,7 @@ class SuccessDialog(QDialog):
|
||||
card.setFrameShape(QFrame.StyledPanel)
|
||||
card.setFrameShadow(QFrame.Raised)
|
||||
card.setFixedWidth(440)
|
||||
card.setMinimumHeight(380)
|
||||
card_layout = QVBoxLayout(card)
|
||||
card_layout.setSpacing(12)
|
||||
card_layout.setContentsMargins(28, 28, 28, 28)
|
||||
@@ -65,23 +66,6 @@ class SuccessDialog(QDialog):
|
||||
)
|
||||
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
# Trophy icon (smaller, more subtle)
|
||||
trophy_label = QLabel()
|
||||
trophy_label.setAlignment(Qt.AlignCenter)
|
||||
trophy_icon_path = Path(__file__).parent.parent.parent.parent.parent / "Files" / "trophy.png"
|
||||
if trophy_icon_path.exists():
|
||||
pixmap = QPixmap(str(trophy_icon_path)).scaled(36, 36, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
trophy_label.setPixmap(pixmap)
|
||||
else:
|
||||
trophy_label.setText("✓")
|
||||
trophy_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 28px; "
|
||||
" margin-bottom: 4px; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(trophy_label)
|
||||
|
||||
# Success title (less saturated green)
|
||||
title_label = QLabel("Success!")
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
@@ -137,11 +121,12 @@ class SuccessDialog(QDialog):
|
||||
next_steps_label = QLabel(next_steps_text)
|
||||
next_steps_label.setAlignment(Qt.AlignCenter)
|
||||
next_steps_label.setWordWrap(True)
|
||||
next_steps_label.setMinimumHeight(100)
|
||||
next_steps_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 13px; "
|
||||
" color: #b0b0b0; "
|
||||
" line-height: 1.2; "
|
||||
" line-height: 1.4; "
|
||||
" padding: 6px; "
|
||||
" background-color: transparent; "
|
||||
" border-radius: 6px; "
|
||||
@@ -232,15 +217,22 @@ class SuccessDialog(QDialog):
|
||||
def _build_next_steps(self) -> str:
|
||||
"""
|
||||
Build the next steps guidance based on workflow type.
|
||||
|
||||
|
||||
Returns:
|
||||
Formatted next steps string
|
||||
"""
|
||||
game_display = self.game_name or self.modlist_name
|
||||
|
||||
base_message = ""
|
||||
if self.workflow_type == "tuxborn":
|
||||
return f"You can now launch Tuxborn from Steam and enjoy your modded {game_display} experience!"
|
||||
base_message = f"You can now launch Tuxborn from Steam and enjoy your modded {game_display} experience!"
|
||||
else:
|
||||
return f"You can now launch {self.modlist_name} from Steam and enjoy your modded {game_display} experience!"
|
||||
base_message = f"You can now launch {self.modlist_name} from Steam and enjoy your modded {game_display} experience!"
|
||||
|
||||
# Add GE-Proton recommendation
|
||||
proton_note = "\n\nNOTE: If you experience ENB issues, consider using GE-Proton 10-14 instead of Valve's Proton 10 (known ENB compatibility issues)."
|
||||
|
||||
return base_message + proton_note
|
||||
|
||||
def _update_countdown(self):
|
||||
if self._countdown > 0:
|
||||
|
||||
@@ -438,19 +438,37 @@ class SettingsDialog(QDialog):
|
||||
|
||||
advanced_layout.addWidget(resource_group)
|
||||
|
||||
# Component Installation Method Section
|
||||
component_group = QGroupBox("Component Installation")
|
||||
# Advanced Tool Options Section
|
||||
component_group = QGroupBox("Advanced Tool Options")
|
||||
component_group.setStyleSheet("QGroupBox { border: 1px solid #555; border-radius: 6px; margin-top: 8px; padding: 8px; background: #23282d; } QGroupBox:title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; font-weight: bold; color: #fff; }")
|
||||
component_layout = QVBoxLayout()
|
||||
component_group.setLayout(component_layout)
|
||||
|
||||
self.use_winetricks_checkbox = QCheckBox("Use winetricks for component installation (faster)")
|
||||
self.use_winetricks_checkbox.setChecked(self.config_handler.get('use_winetricks_for_components', True))
|
||||
self.use_winetricks_checkbox.setToolTip(
|
||||
"When enabled: Uses winetricks for most components (faster) and protontricks for legacy .NET versions (dotnet40, dotnet472, dotnet48) which are more reliable.\n"
|
||||
"When disabled: Uses protontricks for all components (legacy behavior, slower but more compatible)."
|
||||
# Label for the toggle button
|
||||
method_label = QLabel("Wine Components Installation:")
|
||||
component_layout.addWidget(method_label)
|
||||
|
||||
# Toggle button for winetricks/protontricks selection
|
||||
self.component_toggle = QPushButton("Winetricks")
|
||||
self.component_toggle.setCheckable(True)
|
||||
use_winetricks = self.config_handler.get('use_winetricks_for_components', True)
|
||||
self.component_toggle.setChecked(use_winetricks)
|
||||
|
||||
# Function to update button text based on state
|
||||
def update_button_text():
|
||||
if self.component_toggle.isChecked():
|
||||
self.component_toggle.setText("Winetricks")
|
||||
else:
|
||||
self.component_toggle.setText("Protontricks")
|
||||
|
||||
self.component_toggle.toggled.connect(update_button_text)
|
||||
update_button_text() # Set initial text
|
||||
|
||||
self.component_toggle.setToolTip(
|
||||
"Winetricks: Faster, uses bundled tools (Default)\n"
|
||||
"Protontricks: Legacy mode, slower but system-compatible"
|
||||
)
|
||||
component_layout.addWidget(self.use_winetricks_checkbox)
|
||||
component_layout.addWidget(self.component_toggle)
|
||||
|
||||
advanced_layout.addWidget(component_group)
|
||||
advanced_layout.addStretch() # Add stretch to push content to top
|
||||
@@ -726,7 +744,7 @@ class SettingsDialog(QDialog):
|
||||
self.config_handler.set("game_proton_version", resolved_game_version)
|
||||
|
||||
# Save component installation method preference
|
||||
self.config_handler.set("use_winetricks_for_components", self.use_winetricks_checkbox.isChecked())
|
||||
self.config_handler.set("use_winetricks_for_components", self.component_toggle.isChecked())
|
||||
|
||||
# Force immediate save and verify
|
||||
save_result = self.config_handler.save_config()
|
||||
@@ -891,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
|
||||
@@ -915,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)
|
||||
@@ -1007,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)
|
||||
@@ -1162,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)
|
||||
@@ -37,7 +37,9 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
self.refresh_paths()
|
||||
|
||||
# --- Detect Steam Deck ---
|
||||
steamdeck = os.path.exists('/etc/os-release') and 'steamdeck' in open('/etc/os-release').read().lower()
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
steamdeck = platform_service.is_steamdeck
|
||||
self.shortcut_handler = ShortcutHandler(steamdeck=steamdeck)
|
||||
|
||||
# Initialize services early
|
||||
@@ -410,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
|
||||
@@ -451,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
|
||||
@@ -591,7 +594,9 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
return
|
||||
# --- Shortcut creation will be handled by automated workflow ---
|
||||
from jackify.backend.handlers.shortcut_handler import ShortcutHandler
|
||||
steamdeck = os.path.exists('/etc/os-release') and 'steamdeck' in open('/etc/os-release').read().lower()
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
steamdeck = platform_service.is_steamdeck
|
||||
shortcut_handler = ShortcutHandler(steamdeck=steamdeck) # Still needed for Steam restart
|
||||
|
||||
# Check if auto-restart is enabled
|
||||
@@ -663,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()
|
||||
@@ -670,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)
|
||||
|
||||
@@ -723,16 +732,10 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
except Exception as e:
|
||||
self.error_occurred.emit(str(e))
|
||||
|
||||
# Detect Steam Deck once
|
||||
try:
|
||||
import os
|
||||
_is_steamdeck = False
|
||||
if os.path.exists('/etc/os-release'):
|
||||
with open('/etc/os-release') as f:
|
||||
if 'steamdeck' in f.read().lower():
|
||||
_is_steamdeck = True
|
||||
except Exception:
|
||||
_is_steamdeck = False
|
||||
# Detect Steam Deck once using centralized service
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
_is_steamdeck = platform_service.is_steamdeck
|
||||
|
||||
# Create and start the thread
|
||||
self.automated_prefix_thread = AutomatedPrefixThread(modlist_name, install_dir, mo2_exe_path, _is_steamdeck)
|
||||
@@ -928,7 +931,10 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
# Steam assigns a NEW AppID during restart, different from the one we initially created
|
||||
self._safe_append_text(f"Re-detecting AppID for shortcut '{modlist_name}' after Steam restart...")
|
||||
from jackify.backend.handlers.shortcut_handler import ShortcutHandler
|
||||
shortcut_handler = ShortcutHandler(steamdeck=False)
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
shortcut_handler = ShortcutHandler(steamdeck=platform_service.is_steamdeck)
|
||||
current_appid = shortcut_handler.get_appid_for_shortcut(modlist_name, mo2_exe_path)
|
||||
|
||||
if not current_appid or not current_appid.isdigit():
|
||||
@@ -952,7 +958,12 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
|
||||
# Initialize ModlistHandler with correct parameters
|
||||
path_handler = PathHandler()
|
||||
modlist_handler = ModlistHandler(steamdeck=False, verbose=False)
|
||||
|
||||
# Use centralized Steam Deck detection
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
|
||||
modlist_handler = ModlistHandler(steamdeck=platform_service.is_steamdeck, verbose=False)
|
||||
|
||||
# Set required properties manually after initialization
|
||||
modlist_handler.modlist_dir = install_dir
|
||||
|
||||
@@ -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",
|
||||
@@ -2112,7 +2315,10 @@ class InstallModlistScreen(QWidget):
|
||||
# Steam assigns a NEW AppID during restart, different from the one we initially created
|
||||
self._safe_append_text(f"Re-detecting AppID for shortcut '{modlist_name}' after Steam restart...")
|
||||
from jackify.backend.handlers.shortcut_handler import ShortcutHandler
|
||||
shortcut_handler = ShortcutHandler(steamdeck=False)
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
shortcut_handler = ShortcutHandler(steamdeck=platform_service.is_steamdeck)
|
||||
current_appid = shortcut_handler.get_appid_for_shortcut(modlist_name, mo2_exe_path)
|
||||
|
||||
if not current_appid or not current_appid.isdigit():
|
||||
@@ -2133,7 +2339,12 @@ class InstallModlistScreen(QWidget):
|
||||
|
||||
# Initialize ModlistHandler with correct parameters
|
||||
path_handler = PathHandler()
|
||||
modlist_handler = ModlistHandler(steamdeck=False, verbose=False)
|
||||
|
||||
# Use centralized Steam Deck detection
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
|
||||
modlist_handler = ModlistHandler(steamdeck=platform_service.is_steamdeck, verbose=False)
|
||||
|
||||
# Set required properties manually after initialization
|
||||
modlist_handler.modlist_dir = install_dir
|
||||
@@ -2351,15 +2562,21 @@ class InstallModlistScreen(QWidget):
|
||||
self.context = updated_context # Ensure context is always set
|
||||
debug_print(f"Updated context with new AppID: {new_appid}")
|
||||
|
||||
# Get Steam Deck detection once and pass to ConfigThread
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
is_steamdeck = platform_service.is_steamdeck
|
||||
|
||||
# Create new config thread with updated context
|
||||
class ConfigThread(QThread):
|
||||
progress_update = Signal(str)
|
||||
configuration_complete = Signal(bool, str, str)
|
||||
error_occurred = Signal(str)
|
||||
|
||||
def __init__(self, context):
|
||||
|
||||
def __init__(self, context, is_steamdeck):
|
||||
super().__init__()
|
||||
self.context = context
|
||||
self.is_steamdeck = is_steamdeck
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
@@ -2368,8 +2585,8 @@ class InstallModlistScreen(QWidget):
|
||||
from jackify.backend.models.modlist import ModlistContext
|
||||
from pathlib import Path
|
||||
|
||||
# Initialize backend service
|
||||
system_info = SystemInfo(is_steamdeck=False)
|
||||
# Initialize backend service with passed Steam Deck detection
|
||||
system_info = SystemInfo(is_steamdeck=self.is_steamdeck)
|
||||
modlist_service = ModlistService(system_info)
|
||||
|
||||
# Convert context to ModlistContext for service
|
||||
@@ -2416,7 +2633,7 @@ class InstallModlistScreen(QWidget):
|
||||
self.error_occurred.emit(str(e))
|
||||
|
||||
# Start configuration thread
|
||||
self.config_thread = ConfigThread(updated_context)
|
||||
self.config_thread = ConfigThread(updated_context, is_steamdeck)
|
||||
self.config_thread.progress_update.connect(self.on_configuration_progress)
|
||||
self.config_thread.configuration_complete.connect(self.on_configuration_complete)
|
||||
self.config_thread.error_occurred.connect(self.on_configuration_error)
|
||||
@@ -2477,15 +2694,21 @@ class InstallModlistScreen(QWidget):
|
||||
def _create_config_thread(self, context):
|
||||
"""Create a new ConfigThread with proper lifecycle management"""
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
|
||||
|
||||
# Get Steam Deck detection once
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
is_steamdeck = platform_service.is_steamdeck
|
||||
|
||||
class ConfigThread(QThread):
|
||||
progress_update = Signal(str)
|
||||
configuration_complete = Signal(bool, str, str)
|
||||
error_occurred = Signal(str)
|
||||
|
||||
def __init__(self, context, parent=None):
|
||||
|
||||
def __init__(self, context, is_steamdeck, parent=None):
|
||||
super().__init__(parent)
|
||||
self.context = context
|
||||
self.is_steamdeck = is_steamdeck
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
@@ -2494,8 +2717,8 @@ class InstallModlistScreen(QWidget):
|
||||
from jackify.backend.models.modlist import ModlistContext
|
||||
from pathlib import Path
|
||||
|
||||
# Initialize backend service
|
||||
system_info = SystemInfo(is_steamdeck=False)
|
||||
# Initialize backend service with passed Steam Deck detection
|
||||
system_info = SystemInfo(is_steamdeck=self.is_steamdeck)
|
||||
modlist_service = ModlistService(system_info)
|
||||
|
||||
# Convert context to ModlistContext for service
|
||||
@@ -2544,7 +2767,7 @@ class InstallModlistScreen(QWidget):
|
||||
self.progress_update.emit(f"DEBUG: {error_details}")
|
||||
self.error_occurred.emit(str(e))
|
||||
|
||||
return ConfigThread(context, parent=self)
|
||||
return ConfigThread(context, is_steamdeck, parent=self)
|
||||
|
||||
def handle_validation_failure(self, missing_text):
|
||||
"""Handle failed validation with retry logic"""
|
||||
@@ -2727,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