5 Commits

Author SHA1 Message Date
Omni
b55e1cf768 Sync from development - prepare for v0.2.2.2 2026-01-28 22:19:40 +00:00
Omni
8e49602714 Sync from development - prepare for v0.2.2.2 2026-01-28 22:16:55 +00:00
Omni
98a9a4c7c6 Sync from development - prepare for v0.2.2.2 2026-01-28 22:13:51 +00:00
Omni
286d51e6a1 Sync from development - prepare for v0.2.2.1 2026-01-24 22:02:29 +00:00
Omni
53af9f26a2 Sync from development - prepare for v0.2.2 2026-01-21 21:59:42 +00:00
72 changed files with 3006 additions and 609 deletions

View File

@@ -1,5 +1,46 @@
# Jackify Changelog # Jackify Changelog
## v0.2.2.2 - ModOrganizer.ini Path Fixes for SD Card Installations
**Release Date:** 2026-01-28
### Bug Fixes
- **ModOrganizer.ini Path Mangling**: Fixed incorrect drive letter assignment when modlist is on SD card but vanilla game is on internal storage. Now uses gamePath drive letter as source of truth for vanilla game paths.
- **Proton Config Name Mismatch (Issues #150, #151)**: Fixed incorrect Proton names written to Steam config.vdf CompatToolMapping. Naive string conversion produced wrong names (e.g., `proton_9.0_(beta)` instead of `proton_9`). Now resolves correct internal names from `compatibilitytool.vdf` (third-party) or App ID mapping (Valve Proton). CachyOS and other community Proton builds in `compatibilitytools.d/` are now detected and selectable.
- **Removed Lorerim/Lost Legacy Proton Override**: No longer forces Proton 9 for specific modlists. ENB compatibility warnings are handled by the success dialog instead.
### Engine Updates
- **jackify-engine 0.4.7**: Fixed incorrect quoting/escaping of MO2 `customExecutables` by writing clean, unquoted Proton `Z:\...` paths in `ModOrganizer.ini`. This eliminates engine-side quote corruption that previously triggered SD card path mangling issues.
### Improvements
- **Improved Wine Component install debug log output**: Will now print the full command being used for winetricks and protontricks when debug mode is enabled, making it easier to reproduce issues manually.
---
## v0.2.2.1 - TTW Installer Pinning and Configure New Modlist CLI Fix
**Release Date:** 2026-01-24
### Bug Fixes
- **Configure New Modlist CLI**: Fixed manual Proton setup prompts appearing in CLI. Now uses automated prefix workflow like the install command.
- **TTW_Linux_Installer Version Pinning**: Pinned to v0.0.7. Will re-introduce latest version following more testing.
---
## v0.2.2 - VNV Automation and First-Launch Improvements
**Release Date:** 2026-01-21
### Major Features
- **Viva New Vegas Post-Install Automation (experimental)**: Full automated workflow for the Viva New Vegas modlist. Handles root files copying, 4GB patcher, and BSA decompression as per the VNV install guide. This is an initial pass at automating this, so considered experimental.
- **Game Directory Pre-Creation**: Automatically creates My Documents/My Games and AppData/Local directories for some. Prevents some first-launch failures where games can't initialize under Proton. Supports Skyrim SE, FNV, FO4, Oblivion, Oblivion Remastered, Enderal, and Starfield so far.
### Bug Fixes
- **Configure Existing Modlist**: Fixed AttributeError when VNV automation check runs after configuration completes
- **Enderal Directory Creation**: Fixed bug where Enderal My Documents directory was created for all modlists instead of only Enderal
### Improvements
- **Winetricks Bundling**: Implemented Wine wrapper scripts that replicate protontricks' environment setup for improved reliability
---
## v0.2.1.1 - Bug Fixes and Improvements ## v0.2.1.1 - Bug Fixes and Improvements
**Release Date:** 2026-01-15 **Release Date:** 2026-01-15

View File

@@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing
Wabbajack modlists natively on Linux systems. Wabbajack modlists natively on Linux systems.
""" """
__version__ = "0.2.1.1" __version__ = "0.2.2.2"

View File

@@ -81,7 +81,7 @@ class LoggingHandler:
if log_file or is_general: if log_file or is_general:
file_path = self.log_dir / (log_file if log_file else "jackify-cli.log") file_path = self.log_dir / (log_file if log_file else "jackify-cli.log")
file_handler = logging.handlers.RotatingFileHandler( file_handler = logging.handlers.RotatingFileHandler(
file_path, mode='a', encoding='utf-8', maxBytes=1024*1024, backupCount=5 file_path, mode='a', encoding='utf-8', maxBytes=100*1024*1024, backupCount=5
) )
file_handler.setLevel(logging.DEBUG) file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(file_formatter) file_handler.setFormatter(file_formatter)
@@ -90,7 +90,7 @@ class LoggingHandler:
return logger return logger
def rotate_logs(self, max_bytes: int = 1024 * 1024, backup_count: int = 5) -> None: def rotate_logs(self, max_bytes: int = 100 * 1024 * 1024, backup_count: int = 5) -> None:
"""Rotate log files based on size.""" """Rotate log files based on size."""
for log_file in self.get_log_files(): for log_file in self.get_log_files():
try: try:

View File

@@ -392,78 +392,88 @@ class ModlistMenuHandler:
except Exception as e: except Exception as e:
self.logger.warning(f"Failed to rename 'Steam Icons' to 'SteamIcons': {e}") self.logger.warning(f"Failed to rename 'Steam Icons' to 'SteamIcons': {e}")
self.logger.debug(f"[DEBUG] After normalization, SteamIcons exists: {os.path.isdir(steamicons_path)}") self.logger.debug(f"[DEBUG] After normalization, SteamIcons exists: {os.path.isdir(steamicons_path)}")
# --- Create shortcut with working NativeSteamService --- # --- Use automated prefix workflow (replaces old manual workflow) ---
try: try:
from ..services.native_steam_service import NativeSteamService
steam_service = NativeSteamService()
success, app_id = steam_service.create_shortcut_with_proton(
app_name=modlist_name,
exe_path=mo2_path,
start_dir=os.path.dirname(mo2_path),
launch_options="%command%",
tags=["Jackify"],
proton_version="proton_experimental"
)
if not success or not app_id:
self.logger.error("Failed to create Steam shortcut.")
print(f"\n{COLOR_ERROR}Failed to create Steam shortcut. Check the logs for details.{COLOR_RESET}")
return True
mo2_dir = os.path.dirname(mo2_path) mo2_dir = os.path.dirname(mo2_path)
if os.environ.get('JACKIFY_GUI_MODE'): install_dir = mo2_dir
print('[PROMPT:RESTART_STEAM]')
input() # Wait for GUI to send confirmation # Use automated prefix service for modern workflow
print('[PROMPT:MANUAL_STEPS]') print(f"\n{COLOR_INFO}Using automated Steam setup workflow...{COLOR_RESET}")
input() # Wait for GUI to send confirmation
# Continue as before from ..services.automated_prefix_service import AutomatedPrefixService
else: prefix_service = AutomatedPrefixService()
print("\n───────────────────────────────────────────────────────────────────")
print(f"{COLOR_INFO}Important:{COLOR_RESET} Steam needs to restart to detect the new shortcut.") # Define progress callback for CLI with jackify-engine style timestamps
print("This process involves several manual steps after the restart.") import time
restart_choice = input("\nRestart Steam automatically now? (Y/n): ").strip().lower() start_time = time.time()
if restart_choice == 'n':
self.logger.info("User opted out of automatic Steam restart.") def progress_callback(message):
print("\nPlease restart Steam manually to see your new shortcut:") elapsed = time.time() - start_time
print("1. Exit Steam completely (Steam -> Exit or right-click tray icon -> Exit)") hours = int(elapsed // 3600)
print("2. Wait a few seconds") minutes = int((elapsed % 3600) // 60)
print("3. Start Steam again") seconds = int(elapsed % 60)
print("\nAfter restarting, you MUST perform the manual Proton setup steps:") timestamp = f"[{hours:02d}:{minutes:02d}:{seconds:02d}]"
self._display_manual_proton_steps(modlist_name) print(f"{COLOR_INFO}{timestamp} {message}{COLOR_RESET}")
print(f"\n{COLOR_ERROR}You will need to re-run this configuration option after completing these steps.{COLOR_RESET}")
print("───────────────────────────────────────────────────────────────────") # Run the automated workflow
return True result = prefix_service.run_working_workflow(
self.logger.info("Attempting secure Steam restart...") modlist_name, install_dir, mo2_path, progress_callback, steamdeck=self.steamdeck
print() )
status_line = ""
def update_status(msg): # Handle the result
nonlocal status_line if isinstance(result, tuple) and len(result) == 4:
if status_line: if result[0] == "CONFLICT":
print("\r" + " " * len(status_line), end="\r") # Handle conflict - ask user what to do
status_line = f"\r{COLOR_INFO}{msg}{COLOR_RESET}" conflicts = result[1]
print(status_line, end="", flush=True) print(f"\n{COLOR_WARNING}Found existing Steam shortcut(s) with the same name and path:{COLOR_RESET}")
# Actually restart Steam and wait for completion for i, conflict in enumerate(conflicts, 1):
if self.shortcut_handler.secure_steam_restart(status_callback=update_status): print(f" {i}. Name: {conflict['name']}")
print() print(f" Executable: {conflict['exe']}")
self.logger.info("Secure Steam restart successful.") print(f" Start Directory: {conflict['startdir']}")
self._display_manual_proton_steps(modlist_name) print(f"\n{COLOR_PROMPT}Options:{COLOR_RESET}")
print() print(" 1. Use existing shortcut (recommended)")
input(f"{COLOR_PROMPT}Once you have completed ALL the steps above, press Enter to continue...{COLOR_RESET}") print(" 2. Create new shortcut anyway")
self.logger.info("User confirmed completion of manual steps.") choice = input(f"{COLOR_PROMPT}Enter choice (1-2): {COLOR_RESET}").strip()
# Re-detect the shortcut and get the new, positive AppID if choice == "1":
new_app_id = self.shortcut_handler.get_appid_for_shortcut(modlist_name, mo2_path) # Use existing shortcut
self.logger.info(f"Pre-launch AppID: {app_id}, Post-launch AppID: {new_app_id}") existing_appid = conflicts[0].get('appid')
if not new_app_id or not new_app_id.isdigit() or int(new_app_id) < 0: if existing_appid:
print(f"{COLOR_ERROR}Could not find a valid AppID for '{modlist_name}' after launch. Please ensure you launched the shortcut from Steam at least once, then try again.{COLOR_RESET}") context = {
"name": modlist_name,
"appid": str(existing_appid),
"path": mo2_dir,
"manual_steps_completed": True,
"resolution": None
}
return self.run_modlist_configuration_phase(context)
elif choice == "2":
# Create new shortcut - would need to handle this, but for now just fail
print(f"{COLOR_ERROR}Creating new shortcut with same name not supported in this flow.{COLOR_RESET}")
return True return True
context = { else:
"name": modlist_name, print(f"{COLOR_ERROR}Invalid choice.{COLOR_RESET}")
"appid": new_app_id, return True
"path": mo2_dir, else:
"manual_steps_completed": True, # Success - get the results
"resolution": None success, prefix_path, appid_int, last_timestamp = result
} if success and appid_int:
self.logger.debug(f"[DEBUG] New Modlist Context (post-launch): {context}") context = {
return self.run_modlist_configuration_phase(context) "name": modlist_name,
"appid": str(appid_int),
"path": mo2_dir,
"manual_steps_completed": True,
"resolution": None
}
self.logger.debug(f"[DEBUG] New Modlist Context (automated workflow): {context}")
return self.run_modlist_configuration_phase(context)
else:
print(f"{COLOR_ERROR}Automated workflow completed but no AppID was returned.{COLOR_RESET}")
return True
else:
# Unexpected result format
print(f"{COLOR_ERROR}Automated workflow returned unexpected format.{COLOR_RESET}")
self.logger.error(f"Unexpected result format from automated workflow: {result}")
return True
except Exception as e: except Exception as e:
self.logger.error(f"Error creating Steam shortcut: {e}", exc_info=True) self.logger.error(f"Error creating Steam shortcut: {e}", exc_info=True)
print(f"\n{COLOR_ERROR}Failed to create Steam shortcut: {e}{COLOR_RESET}") print(f"\n{COLOR_ERROR}Failed to create Steam shortcut: {e}{COLOR_RESET}")
@@ -666,7 +676,36 @@ class ModlistMenuHandler:
except Exception as e: except Exception as e:
self.logger.warning(f"ENB configuration skipped due to error: {e}") self.logger.warning(f"ENB configuration skipped due to error: {e}")
# Continue workflow - ENB config is optional # Continue workflow - ENB config is optional
# Run modlist-specific post-install automation (e.g., VNV) before showing completion
# Only in CLI mode - GUI handles this in install_modlist.py
if not gui_mode:
from jackify.backend.services.vnv_integration_helper import run_vnv_automation_if_applicable
from jackify.backend.services.automated_prefix_service import AutomatedPrefixService
from pathlib import Path
modlist_name = context.get('name', '')
modlist_path = Path(context.get('path', ''))
try:
print("")
print("Running VNV post-install automation...")
automation_ran, error = run_vnv_automation_if_applicable(
modlist_name=modlist_name,
modlist_install_location=modlist_path,
game_root=None, # Will be auto-detected
ttw_installer_path=AutomatedPrefixService.get_ttw_installer_path(),
progress_callback=lambda msg: print(msg),
manual_file_callback=None, # CLI doesn't support manual file callback yet
confirmation_callback=None # Will use default confirmation in CLI
)
if error:
print(f"{COLOR_WARNING}VNV automation encountered an error: {error}{COLOR_RESET}")
print("You can complete these steps manually by following: https://vivanewvegas.moddinglinked.com/wabbajack.html")
except Exception as e:
self.logger.debug(f"VNV automation check skipped: {e}")
# Not an error - just means VNV automation wasn't applicable
print("") print("")
print("") print("")
print("") # Extra blank line before completion print("") # Extra blank line before completion

View File

@@ -892,6 +892,7 @@ class PathHandler:
# Extract existing gamePath to use as source of truth for vanilla game location # Extract existing gamePath to use as source of truth for vanilla game location
existing_game_path = None existing_game_path = None
gamepath_drive_letter = None
gamepath_line_index = -1 gamepath_line_index = -1
for i, line in enumerate(lines): for i, line in enumerate(lines):
if re.match(r'^\s*gamepath\s*=.*@ByteArray\(([^)]+)\)', line, re.IGNORECASE): if re.match(r'^\s*gamepath\s*=.*@ByteArray\(([^)]+)\)', line, re.IGNORECASE):
@@ -899,11 +900,16 @@ class PathHandler:
if match: if match:
raw_path = match.group(1) raw_path = match.group(1)
gamepath_line_index = i gamepath_line_index = i
# Extract drive letter from gamePath (Z: or D:)
if raw_path.startswith('Z:'):
gamepath_drive_letter = 'Z:'
elif raw_path.startswith('D:'):
gamepath_drive_letter = 'D:'
# Convert Windows path back to Linux path # Convert Windows path back to Linux path
if raw_path.startswith(('Z:', 'D:')): if raw_path.startswith(('Z:', 'D:')):
linux_path = raw_path[2:].replace('\\\\', '/').replace('\\', '/') linux_path = raw_path[2:].replace('\\\\', '/').replace('\\', '/')
existing_game_path = linux_path existing_game_path = linux_path
logger.debug(f"Extracted existing gamePath: {existing_game_path}") logger.debug(f"Extracted existing gamePath: {existing_game_path}, drive letter: {gamepath_drive_letter}")
break break
# Special handling for gamePath in three-true scenario (engine_installed + steamdeck + sdcard) # Special handling for gamePath in three-true scenario (engine_installed + steamdeck + sdcard)
@@ -957,19 +963,33 @@ class PathHandler:
logger.error(f"Malformed binary line: {line}") logger.error(f"Malformed binary line: {line}")
continue continue
key_part, value_part = parts key_part, value_part = parts
exe_name = os.path.basename(value_part).lower()
# Clean up malformed paths (quotes in wrong places, etc.)
cleaned_value = PathHandler._clean_malformed_binary_path(value_part)
exe_name = os.path.basename(cleaned_value).lower()
# SELECTIVE FILTERING: Only process target executables (script extenders, etc.) # SELECTIVE FILTERING: Only process target executables (script extenders, etc.)
if exe_name not in TARGET_EXECUTABLES_LOWER: if exe_name not in TARGET_EXECUTABLES_LOWER:
logger.debug(f"Skipping non-target executable: {exe_name}") logger.debug(f"Skipping non-target executable: {exe_name}")
continue continue
drive_prefix = "D:" if modlist_sdcard else "Z:"
rel_path = None rel_path = None
# --- BEGIN: FULL PARITY LOGIC --- # --- BEGIN: FULL PARITY LOGIC ---
if 'steamapps' in value_part: if 'steamapps' in cleaned_value:
idx = value_part.index('steamapps') # Vanilla game path detected - always rebuild to ensure correct format
subpath = value_part[idx:].lstrip('/') if not gamepath_drive_letter:
logger.warning(f"Vanilla game path detected but gamePath drive letter not found. Skipping binary path update for: {exe_name}")
logger.warning("This may indicate jackify-engine already configured paths correctly, or gamePath is malformed.")
continue
# Check if path is malformed (has quotes or wrong structure)
is_malformed = '"' in cleaned_value or cleaned_value != value_part.strip().strip('"')
# Extract subpath from cleaned value (includes exe name)
idx = cleaned_value.index('steamapps')
subpath = cleaned_value[idx:].lstrip('/')
# Find correct Steam library
correct_steam_lib = None correct_steam_lib = None
for lib in steam_libraries: for lib in steam_libraries:
# Check if the actual game folder exists in this library # Check if the actual game folder exists in this library
@@ -979,39 +999,62 @@ class PathHandler:
if not correct_steam_lib and steam_libraries: if not correct_steam_lib and steam_libraries:
correct_steam_lib = steam_libraries[0] correct_steam_lib = steam_libraries[0]
if correct_steam_lib: if correct_steam_lib:
# Always rebuild path using gamePath drive letter to ensure correct format
drive_prefix = gamepath_drive_letter
if is_malformed:
logger.info(f"Fixing malformed binary path for {exe_name}: {value_part.strip()}")
logger.debug(f"Vanilla game path detected: Using drive letter from gamePath: {drive_prefix}")
new_binary_path = f"{drive_prefix}/{correct_steam_lib}/{subpath}".replace('\\', '/').replace('//', '/') new_binary_path = f"{drive_prefix}/{correct_steam_lib}/{subpath}".replace('\\', '/').replace('//', '/')
else: else:
logger.error("Could not determine correct Steam library for vanilla game path.") logger.error("Could not determine correct Steam library for vanilla game path.")
continue continue
else: else:
# For modlist-relative paths (Stock Game, mods, etc.), use modlist location
drive_prefix = "D:" if modlist_sdcard else "Z:"
found_stock = None found_stock = None
for folder in STOCK_GAME_FOLDERS: for folder in STOCK_GAME_FOLDERS:
folder_pattern = f"/{folder}" folder_pattern = f"/{folder}"
if folder_pattern in value_part: if folder_pattern in cleaned_value:
idx = value_part.index(folder_pattern) idx = cleaned_value.index(folder_pattern)
rel_path = value_part[idx:].lstrip('/') rel_path = cleaned_value[idx:].lstrip('/')
found_stock = folder found_stock = folder
break break
if not rel_path: if not rel_path:
mods_pattern = "/mods/" mods_pattern = "/mods/"
if mods_pattern in value_part: if mods_pattern in cleaned_value:
idx = value_part.index(mods_pattern) idx = cleaned_value.index(mods_pattern)
rel_path = value_part[idx:].lstrip('/') rel_path = cleaned_value[idx:].lstrip('/')
else: else:
rel_path = exe_name rel_path = exe_name
processed_modlist_path = PathHandler._strip_sdcard_path_prefix(modlist_dir_path) if modlist_sdcard else str(modlist_dir_path) processed_modlist_path = PathHandler._strip_sdcard_path_prefix(modlist_dir_path) if modlist_sdcard else str(modlist_dir_path)
new_binary_path = f"{drive_prefix}/{processed_modlist_path}/{rel_path}".replace('\\', '/').replace('//', '/') new_binary_path = f"{drive_prefix}/{processed_modlist_path}/{rel_path}".replace('\\', '/').replace('//', '/')
formatted_binary_path = PathHandler._format_binary_for_mo2(new_binary_path) formatted_binary_path = PathHandler._format_binary_for_mo2(new_binary_path)
# Ensure no quotes in formatted path (binary paths should never have quotes)
if '"' in formatted_binary_path:
logger.warning(f"Formatted binary path still contains quotes, removing: {formatted_binary_path}")
formatted_binary_path = formatted_binary_path.replace('"', '')
new_binary_line = f"{index}{backslash_style}binary = {formatted_binary_path}" new_binary_line = f"{index}{backslash_style}binary = {formatted_binary_path}"
logger.debug(f"Updating binary path: {line.strip()} -> {new_binary_line}") logger.info(f"Updating binary path: {line.strip()} -> {new_binary_line}")
lines[i] = new_binary_line + "\n" # Preserve original line ending - lines from readlines() should have newline, but ensure it
original_line = lines[i]
if original_line.endswith('\n'):
lines[i] = new_binary_line + '\n'
else:
lines[i] = new_binary_line + '\n'
binary_paths_updated += 1 binary_paths_updated += 1
binary_paths_by_index[index] = formatted_binary_path binary_paths_by_index[index] = formatted_binary_path
for j, wd_line, index, backslash_style in working_dir_lines: for j, wd_line, index, backslash_style in working_dir_lines:
if index in binary_paths_by_index: if index in binary_paths_by_index:
binary_path = binary_paths_by_index[index] binary_path = binary_paths_by_index[index]
wd_path = os.path.dirname(binary_path) wd_path = os.path.dirname(binary_path)
drive_prefix = "D:" if modlist_sdcard else "Z:" # Derive drive letter from binary path, not modlist location
if binary_path.startswith("D:"):
drive_prefix = "D:"
elif binary_path.startswith("Z:"):
drive_prefix = "Z:"
else:
# Fallback: use modlist location if binary path doesn't have drive letter
drive_prefix = "D:" if modlist_sdcard else "Z:"
if wd_path.startswith("D:") or wd_path.startswith("Z:"): if wd_path.startswith("D:") or wd_path.startswith("Z:"):
wd_path = wd_path[2:] wd_path = wd_path[2:]
wd_path = drive_prefix + wd_path wd_path = drive_prefix + wd_path
@@ -1019,7 +1062,12 @@ class PathHandler:
key_part = f"{index}{backslash_style}workingDirectory" key_part = f"{index}{backslash_style}workingDirectory"
new_wd_line = f"{key_part} = {formatted_wd_path}" new_wd_line = f"{key_part} = {formatted_wd_path}"
logger.debug(f"Updating working directory: {wd_line.strip()} -> {new_wd_line}") logger.debug(f"Updating working directory: {wd_line.strip()} -> {new_wd_line}")
lines[j] = new_wd_line + "\n" # Preserve original line ending - ensure newline is present
original_wd_line = lines[j]
if original_wd_line.endswith('\n'):
lines[j] = new_wd_line + '\n'
else:
lines[j] = new_wd_line + '\n'
working_dirs_updated += 1 working_dirs_updated += 1
with open(modlist_ini_path, 'w', encoding='utf-8') as f: with open(modlist_ini_path, 'w', encoding='utf-8') as f:
f.writelines(lines) f.writelines(lines)
@@ -1141,6 +1189,33 @@ class PathHandler:
path = re.sub(r'^([A-Z]:)\\+', r'\1\\', path) path = re.sub(r'^([A-Z]:)\\+', r'\1\\', path)
return path return path
@staticmethod
def _clean_malformed_binary_path(value_part: str) -> str:
"""
Clean up malformed binary paths from engine (e.g., quotes in wrong places).
Example: "Z:/path/to/game"/exe.exe -> Z:/path/to/game/exe.exe
"""
cleaned = value_part.strip()
# Remove quotes if they wrap only part of the path (malformed)
if cleaned.startswith('"') and '"' in cleaned[1:]:
# Find the closing quote
quote_end = cleaned.find('"', 1)
if quote_end > 0:
# Check if there's content after the quote (malformed)
after_quote = cleaned[quote_end + 1:].strip()
if after_quote.startswith('/') or after_quote:
# Malformed: quotes wrap only part of path
# Remove quotes and join
path_part = cleaned[1:quote_end]
remaining = after_quote.lstrip('/')
cleaned = f"{path_part}/{remaining}" if remaining else path_part
logger.info(f"Cleaned malformed binary path: {value_part} -> {cleaned}")
# Remove any remaining quotes (handles fully quoted paths too)
cleaned = cleaned.strip('"')
# Normalize slashes
cleaned = cleaned.replace('\\', '/')
return cleaned
@staticmethod @staticmethod
def _format_binary_for_mo2(path: str) -> str: def _format_binary_for_mo2(path: str) -> str:
import re import re

View File

@@ -367,6 +367,13 @@ class ProtontricksHandler:
**kwargs # Allow overriding defaults (like stderr=DEVNULL) **kwargs # Allow overriding defaults (like stderr=DEVNULL)
} }
# Log full command for advanced users to reproduce manually (debug mode only)
cmd_str = ' '.join(map(str, cmd))
logger.debug("=" * 80)
logger.debug("PROTONTRICKS COMMAND (for manual reproduction):")
logger.debug(f" {cmd_str}")
logger.debug("=" * 80)
# Handle environment: if env was passed in kwargs, merge it with our clean env # Handle environment: if env was passed in kwargs, merge it with our clean env
# Otherwise create a clean env from scratch # Otherwise create a clean env from scratch
if 'env' in kwargs and kwargs['env']: if 'env' in kwargs and kwargs['env']:

View File

@@ -31,6 +31,9 @@ TTW_INSTALLER_EXECUTABLE_NAME = "ttw_linux_gui" # Same executable, runs in CLI
# GitHub release info # GitHub release info
TTW_INSTALLER_REPO = "SulfurNitride/TTW_Linux_Installer" TTW_INSTALLER_REPO = "SulfurNitride/TTW_Linux_Installer"
TTW_INSTALLER_RELEASE_URL = f"https://api.github.com/repos/{TTW_INSTALLER_REPO}/releases/latest" TTW_INSTALLER_RELEASE_URL = f"https://api.github.com/repos/{TTW_INSTALLER_REPO}/releases/latest"
# Pin to 0.0.7 - last version with old format (ttw_linux_gui, universal-mpi-installer)
# Set to None to use latest release
TTW_INSTALLER_PINNED_VERSION = "0.0.7"
class TTWInstallerHandler: class TTWInstallerHandler:
@@ -70,18 +73,26 @@ class TTWInstallerHandler:
self.ttw_installer_dir.mkdir(parents=True, exist_ok=True) self.ttw_installer_dir.mkdir(parents=True, exist_ok=True)
def _check_installation(self): def _check_installation(self):
"""Check if TTW_Linux_Installer is installed at expected location.""" """Check if TTW_Linux_Installer is installed at expected location.
Checks for both old format (ttw_linux_gui) and new format (mpi_installer) executables.
"""
self._ensure_dirs_exist() self._ensure_dirs_exist()
potential_exe_path = self.ttw_installer_dir / TTW_INSTALLER_EXECUTABLE_NAME # Check for both old (ttw_linux_gui) and new (mpi_installer) executable names
if potential_exe_path.is_file() and os.access(potential_exe_path, os.X_OK): exe_names = [TTW_INSTALLER_EXECUTABLE_NAME, "mpi_installer"]
self.ttw_installer_executable_path = potential_exe_path for exe_name in exe_names:
self.ttw_installer_installed = True potential_exe_path = self.ttw_installer_dir / exe_name
self.logger.info(f"Found TTW_Linux_Installer at: {self.ttw_installer_executable_path}") if potential_exe_path.is_file() and os.access(potential_exe_path, os.X_OK):
else: self.ttw_installer_executable_path = potential_exe_path
self.ttw_installer_installed = False self.ttw_installer_installed = True
self.ttw_installer_executable_path = None self.logger.info(f"Found TTW_Linux_Installer at: {self.ttw_installer_executable_path}")
self.logger.info(f"TTW_Linux_Installer not found at {potential_exe_path}") return
# Not found
self.ttw_installer_installed = False
self.ttw_installer_executable_path = None
self.logger.info(f"TTW_Linux_Installer not found (searched for: {', '.join(exe_names)})")
def install_ttw_installer(self, install_dir: Optional[Path] = None) -> Tuple[bool, str]: def install_ttw_installer(self, install_dir: Optional[Path] = None) -> Tuple[bool, str]:
"""Download and install TTW_Linux_Installer from GitHub releases. """Download and install TTW_Linux_Installer from GitHub releases.
@@ -97,9 +108,15 @@ class TTWInstallerHandler:
target_dir = Path(install_dir) if install_dir else self.ttw_installer_dir target_dir = Path(install_dir) if install_dir else self.ttw_installer_dir
target_dir.mkdir(parents=True, exist_ok=True) target_dir.mkdir(parents=True, exist_ok=True)
# Fetch latest release info # Fetch release info (pinned version or latest)
self.logger.info(f"Fetching latest TTW_Linux_Installer release from {TTW_INSTALLER_RELEASE_URL}") if TTW_INSTALLER_PINNED_VERSION:
resp = requests.get(TTW_INSTALLER_RELEASE_URL, timeout=15, verify=True) release_url = f"https://api.github.com/repos/{TTW_INSTALLER_REPO}/releases/tags/{TTW_INSTALLER_PINNED_VERSION}"
self.logger.info(f"Fetching pinned TTW_Linux_Installer version {TTW_INSTALLER_PINNED_VERSION} from {release_url}")
else:
release_url = TTW_INSTALLER_RELEASE_URL
self.logger.info(f"Fetching latest TTW_Linux_Installer release from {release_url}")
resp = requests.get(release_url, timeout=15, verify=True)
resp.raise_for_status() resp.raise_for_status()
data = resp.json() data = resp.json()
release_tag = data.get("tag_name") or data.get("name") release_tag = data.get("tag_name") or data.get("name")
@@ -151,17 +168,39 @@ class TTWInstallerHandler:
except Exception: except Exception:
pass pass
# Find executable (may be in subdirectory or root) # Find executable - support both old (ttw_linux_gui) and new (mpi_installer) names
exe_path = target_dir / TTW_INSTALLER_EXECUTABLE_NAME # Try old name first (since we're pinning to 0.0.7)
if not exe_path.is_file(): exe_names = [TTW_INSTALLER_EXECUTABLE_NAME, "mpi_installer"]
# Search for it exe_path = None
for p in target_dir.rglob(TTW_INSTALLER_EXECUTABLE_NAME):
for exe_name in exe_names:
potential_path = target_dir / exe_name
if potential_path.is_file():
exe_path = potential_path
self.logger.info(f"Found executable: {exe_name}")
break
# Search recursively
for p in target_dir.rglob(exe_name):
if p.is_file(): if p.is_file():
exe_path = p exe_path = p
self.logger.info(f"Found executable: {exe_name} at {p}")
break break
if exe_path:
break
if not exe_path.is_file(): if not exe_path or not exe_path.is_file():
return False, "TTW_Linux_Installer executable not found after extraction" return False, f"TTW_Linux_Installer executable not found after extraction (searched for: {', '.join(exe_names)})"
# Remove any other executable versions to avoid confusion
for exe_name in exe_names:
if exe_name != exe_path.name:
other_exe = target_dir / exe_name
if other_exe.is_file():
self.logger.info(f"Removing other version executable: {other_exe}")
try:
other_exe.unlink()
except Exception as e:
self.logger.warning(f"Failed to remove {other_exe}: {e}")
# Set executable permissions # Set executable permissions
try: try:
@@ -194,13 +233,36 @@ class TTWInstallerHandler:
def is_ttw_installer_update_available(self) -> Tuple[bool, Optional[str], Optional[str]]: def is_ttw_installer_update_available(self) -> Tuple[bool, Optional[str], Optional[str]]:
""" """
Check GitHub for the latest TTW_Linux_Installer release and compare with installed version. Check if TTW_Linux_Installer update is available.
Returns (update_available, installed_version, latest_version). If a version is pinned, compares against pinned version instead of latest.
Returns (update_available, installed_version, target_version).
""" """
installed = self.get_installed_ttw_installer_version() installed = self.get_installed_ttw_installer_version()
# If we have a pinned version, compare against that instead of latest
if TTW_INSTALLER_PINNED_VERSION:
if not installed:
# No version recorded - check if executable exists to infer version
if self.ttw_installer_installed and self.ttw_installer_executable_path:
exe_name = self.ttw_installer_executable_path.name
# If pinned to 0.0.7 but found mpi_installer, it's wrong version
if TTW_INSTALLER_PINNED_VERSION == "0.0.7" and exe_name == "mpi_installer":
return (True, None, TTW_INSTALLER_PINNED_VERSION)
# If pinned to 0.0.7 and found ttw_linux_gui, assume correct
elif TTW_INSTALLER_PINNED_VERSION == "0.0.7" and exe_name == "ttw_linux_gui":
return (False, None, TTW_INSTALLER_PINNED_VERSION)
# Not installed - don't show as update available
return (False, None, TTW_INSTALLER_PINNED_VERSION)
# Compare against pinned version
if installed != TTW_INSTALLER_PINNED_VERSION:
# Installed version doesn't match pinned - show as out of date (allows downgrade)
return (True, installed, TTW_INSTALLER_PINNED_VERSION)
else:
return (False, installed, TTW_INSTALLER_PINNED_VERSION)
# No pinned version - check against latest release (original behavior)
# If executable exists but no version is recorded, don't show as "out of date" # If executable exists but no version is recorded, don't show as "out of date"
# This can happen if the executable was installed before version tracking was added
if not installed and self.ttw_installer_installed: if not installed and self.ttw_installer_installed:
self.logger.info("TTW_Linux_Installer executable found but no version recorded in config") self.logger.info("TTW_Linux_Installer executable found but no version recorded in config")
# Don't treat as update available - just show as "Ready" (unknown version) # Don't treat as update available - just show as "Ready" (unknown version)

View File

@@ -19,6 +19,15 @@ from .subprocess_utils import get_clean_subprocess_env
# Initialize logger # Initialize logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Known Valve Proton App ID -> config.vdf internal name mapping
VALVE_PROTON_APPID_MAP = {
'2805730': 'proton_9',
'3658110': 'proton_10',
'1493710': 'proton_experimental',
'2180100': 'proton_hotfix',
'1887720': 'proton_8',
}
class WineUtils: class WineUtils:
""" """
@@ -849,6 +858,155 @@ class WineUtils:
# Return only existing paths # Return only existing paths
return [path for path in compat_paths if path.exists()] return [path for path in compat_paths if path.exists()]
@staticmethod
def _parse_compat_tool_name(proton_dir: Path) -> Optional[str]:
"""Parse the Steam internal name from a compatibilitytool.vdf file.
The key under compat_tools is what Steam uses in config.vdf CompatToolMapping."""
vdf_path = proton_dir / "compatibilitytool.vdf"
if not vdf_path.exists():
return None
try:
with open(vdf_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
match = re.search(r'"compat_tools"\s*\{[^{]*"([^"]+)"\s*(?://[^\n]*)?\s*\{', content, re.DOTALL)
if match:
return match.group(1)
except Exception as e:
logger.warning(f"Failed to parse {vdf_path}: {e}")
return None
@staticmethod
def _find_valve_proton_appid(proton_dir_name: str) -> Optional[str]:
"""Find the Steam App ID for a Valve Proton by matching appmanifest installdir."""
steam_libs = WineUtils.get_steam_library_paths()
for lib_path in steam_libs:
steamapps_dir = lib_path.parent
for manifest in steamapps_dir.glob("appmanifest_*.acf"):
try:
with open(manifest, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
installdir_match = re.search(r'"installdir"\s+"([^"]+)"', content)
appid_match = re.search(r'"appid"\s+"(\d+)"', content)
if installdir_match and appid_match:
if installdir_match.group(1) == proton_dir_name:
return appid_match.group(1)
except Exception:
continue
return None
@staticmethod
def resolve_steam_compat_name(proton_path) -> Optional[str]:
"""Resolve the correct Steam config.vdf internal name for a Proton installation.
For third-party Protons (GE, CachyOS, etc.): parses compatibilitytool.vdf
For Valve Protons: maps via App ID from appmanifest files.
Args:
proton_path: Path to the Proton directory (str or Path)
Returns:
Internal name for config.vdf CompatToolMapping, or None if unresolvable
"""
proton_path = Path(proton_path)
if not proton_path.is_dir():
logger.warning(f"Proton path not found: {proton_path}")
return None
# Third-party Proton: check for compatibilitytool.vdf
compat_name = WineUtils._parse_compat_tool_name(proton_path)
if compat_name:
logger.debug(f"Resolved compat name from vdf: {proton_path.name} -> {compat_name}")
return compat_name
# Valve Proton: look up App ID from appmanifest, then map
dir_name = proton_path.name
appid = WineUtils._find_valve_proton_appid(dir_name)
if appid and appid in VALVE_PROTON_APPID_MAP:
name = VALVE_PROTON_APPID_MAP[appid]
logger.debug(f"Resolved Valve Proton: {dir_name} (AppID {appid}) -> {name}")
return name
# Fallback for GE-Proton dirs without a vdf (shouldn't happen, but safe)
if dir_name.startswith('GE-Proton'):
return dir_name
logger.warning(f"Could not resolve Steam compat name for: {proton_path}")
return None
@staticmethod
def scan_thirdparty_proton_versions() -> List[Dict[str, any]]:
"""Scan for non-GE third-party Proton versions in compatibilitytools.d directories.
Discovers CachyOS, TKG, and other community builds by parsing compatibilitytool.vdf.
Returns:
List of dicts with version info, sorted by name
"""
logger.info("Scanning for third-party Proton versions...")
found_versions = []
seen_names = set()
compat_paths = WineUtils.get_compatibility_tool_paths()
if not compat_paths:
return []
for compat_path in compat_paths:
try:
for proton_dir in compat_path.iterdir():
if not proton_dir.is_dir():
continue
dir_name = proton_dir.name
# Skip GE-Proton (handled by scan_ge_proton_versions)
if dir_name.startswith("GE-Proton"):
continue
# Must have a wine binary to be a usable Proton
wine_bin = proton_dir / "files" / "bin" / "wine"
if not wine_bin.exists():
continue
# Must have a compatibilitytool.vdf (proves it's a Proton compat tool)
compat_name = WineUtils._parse_compat_tool_name(proton_dir)
if not compat_name:
continue
# Skip non-Proton tools (e.g., LegacyRuntime)
vdf_path = proton_dir / "compatibilitytool.vdf"
try:
with open(vdf_path, 'r', encoding='utf-8', errors='ignore') as f:
vdf_content = f.read()
if '"from_oslist" "linux"' in vdf_content:
continue
except Exception:
pass
# Skip Proton Hotfix
if 'hotfix' in compat_name.lower():
continue
if compat_name in seen_names:
continue
seen_names.add(compat_name)
found_versions.append({
'name': dir_name,
'path': proton_dir,
'wine_bin': wine_bin,
'priority': 175,
'type': 'ThirdParty-Proton',
'steam_compat_name': compat_name,
})
logger.debug(f"Found third-party Proton: {dir_name} (compat name: {compat_name})")
except Exception as e:
logger.warning(f"Error scanning {compat_path}: {e}")
logger.info(f"Found {len(found_versions)} third-party Proton version(s)")
return found_versions
@staticmethod @staticmethod
def scan_ge_proton_versions() -> List[Dict[str, any]]: def scan_ge_proton_versions() -> List[Dict[str, any]]:
""" """
@@ -895,6 +1053,7 @@ class WineUtils:
# Priority format: 200 (base) + major*10 + minor (e.g., 200 + 100 + 16 = 316) # Priority format: 200 (base) + major*10 + minor (e.g., 200 + 100 + 16 = 316)
priority = 200 + (major_ver * 10) + minor_ver priority = 200 + (major_ver * 10) + minor_ver
compat_name = WineUtils._parse_compat_tool_name(proton_dir) or dir_name
found_versions.append({ found_versions.append({
'name': dir_name, 'name': dir_name,
'path': proton_dir, 'path': proton_dir,
@@ -902,7 +1061,8 @@ class WineUtils:
'priority': priority, 'priority': priority,
'major_version': major_ver, 'major_version': major_ver,
'minor_version': minor_ver, 'minor_version': minor_ver,
'type': 'GE-Proton' 'type': 'GE-Proton',
'steam_compat_name': compat_name,
}) })
logger.debug(f"Found {dir_name} at {proton_dir} (priority: {priority})") logger.debug(f"Found {dir_name} at {proton_dir} (priority: {priority})")
else: else:
@@ -951,12 +1111,14 @@ class WineUtils:
wine_bin = proton_path / "files" / "bin" / "wine" wine_bin = proton_path / "files" / "bin" / "wine"
if wine_bin.exists() and wine_bin.is_file(): if wine_bin.exists() and wine_bin.is_file():
compat_name = WineUtils.resolve_steam_compat_name(proton_path)
found_versions.append({ found_versions.append({
'name': version_name, 'name': version_name,
'path': proton_path, 'path': proton_path,
'wine_bin': wine_bin, 'wine_bin': wine_bin,
'priority': priority, 'priority': priority,
'type': 'Valve-Proton' 'type': 'Valve-Proton',
'steam_compat_name': compat_name,
}) })
logger.debug(f"Found {version_name} at {proton_path}") logger.debug(f"Found {version_name} at {proton_path}")
@@ -998,6 +1160,10 @@ class WineUtils:
ge_versions = WineUtils.scan_ge_proton_versions() ge_versions = WineUtils.scan_ge_proton_versions()
all_versions.extend(ge_versions) all_versions.extend(ge_versions)
# Scan third-party Proton versions (CachyOS, TKG, etc.)
thirdparty_versions = WineUtils.scan_thirdparty_proton_versions()
all_versions.extend(thirdparty_versions)
# Scan Valve Proton versions # Scan Valve Proton versions
valve_versions = WineUtils.scan_valve_proton_versions() valve_versions = WineUtils.scan_valve_proton_versions()
all_versions.extend(valve_versions) all_versions.extend(valve_versions)
@@ -1025,6 +1191,7 @@ class WineUtils:
def select_best_proton() -> Optional[Dict[str, any]]: def select_best_proton() -> Optional[Dict[str, any]]:
""" """
Select the best available Proton version (GE-Proton or Valve Proton) using unified precedence. Select the best available Proton version (GE-Proton or Valve Proton) using unified precedence.
Excludes third-party builds (CachyOS, etc.) which may have compatibility issues.
Returns: Returns:
Dict with version info for the best Proton, or None if none found Dict with version info for the best Proton, or None if none found
@@ -1035,8 +1202,16 @@ class WineUtils:
logger.warning("No compatible Proton versions found") logger.warning("No compatible Proton versions found")
return None return None
# Filter out third-party Protons - they may have compatibility issues with component installation
# Only include GE-Proton and Valve-Proton types
compatible_versions = [v for v in available_versions if v.get('type') in ('GE-Proton', 'Valve-Proton')]
if not compatible_versions:
logger.warning("No compatible Proton versions found (only third-party builds available)")
return None
# Return the highest priority version (first in sorted list) # Return the highest priority version (first in sorted list)
best_version = available_versions[0] best_version = compatible_versions[0]
logger.info(f"Selected best Proton version: {best_version['name']} ({best_version['type']})") logger.info(f"Selected best Proton version: {best_version['name']} ({best_version['type']})")
return best_version return best_version
@@ -1079,11 +1254,11 @@ class WineUtils:
if best_proton: if best_proton:
# Compatible Proton found # Compatible Proton found
proton_type = best_proton.get('type', 'Unknown') proton_type = best_proton.get('type', 'Unknown')
status_msg = f" Using {best_proton['name']} ({proton_type}) for this workflow" status_msg = f"[OK] Using {best_proton['name']} ({proton_type}) for this workflow"
logger.info(f"Proton requirements satisfied: {best_proton['name']} ({proton_type})") logger.info(f"Proton requirements satisfied: {best_proton['name']} ({proton_type})")
return True, status_msg, best_proton return True, status_msg, best_proton
else: else:
# No compatible Proton found # No compatible Proton found
status_msg = " No compatible Proton version found (GE-Proton 10+, Proton 9+, 10, or Experimental required)" status_msg = "[FAIL] No compatible Proton version found (GE-Proton 10+, Proton 9+, 10, or Experimental required)"
logger.warning("Proton requirements not met - no compatible version found") logger.warning("Proton requirements not met - no compatible version found")
return False, status_msg, None return False, status_msg, None

View File

@@ -353,7 +353,7 @@ class WinetricksHandler:
for cmd in commands: for cmd in commands:
bundled_tool = self._get_bundled_tool(cmd, fallback_to_system=False) bundled_tool = self._get_bundled_tool(cmd, fallback_to_system=False)
if bundled_tool: if bundled_tool:
dep_msg = f" {dep_name}: {bundled_tool} (bundled)" dep_msg = f" {dep_name}: {bundled_tool} (bundled)"
self.logger.info(dep_msg) self.logger.info(dep_msg)
if status_callback: if status_callback:
status_callback(dep_msg) status_callback(dep_msg)
@@ -367,7 +367,7 @@ class WinetricksHandler:
result = subprocess.run(['which', cmd], capture_output=True, timeout=2) result = subprocess.run(['which', cmd], capture_output=True, timeout=2)
if result.returncode == 0: if result.returncode == 0:
cmd_path = result.stdout.decode().strip() cmd_path = result.stdout.decode().strip()
dep_msg = f" {dep_name}: {cmd_path} (system)" dep_msg = f" {dep_name}: {cmd_path} (system)"
self.logger.info(dep_msg) self.logger.info(dep_msg)
if status_callback: if status_callback:
status_callback(dep_msg) status_callback(dep_msg)
@@ -379,9 +379,9 @@ class WinetricksHandler:
if not found: if not found:
missing_deps.append(dep_name) missing_deps.append(dep_name)
if dep_name in bundled_tools_list: if dep_name in bundled_tools_list:
self.logger.warning(f" {dep_name}: NOT FOUND (neither bundled nor system)") self.logger.warning(f" {dep_name}: NOT FOUND (neither bundled nor system)")
else: else:
self.logger.warning(f" {dep_name}: NOT FOUND (system only - not bundled)") self.logger.warning(f" {dep_name}: NOT FOUND (system only - not bundled)")
if missing_deps: if missing_deps:
# Separate critical vs optional dependencies # Separate critical vs optional dependencies
@@ -484,7 +484,16 @@ class WinetricksHandler:
# Build winetricks command - using --unattended for silent installation # Build winetricks command - using --unattended for silent installation
cmd = [self.winetricks_path, '--unattended'] + components_to_install cmd = [self.winetricks_path, '--unattended'] + components_to_install
self.logger.debug(f"Running: {' '.join(cmd)}") # Log full command for advanced users to reproduce manually (debug mode only)
cmd_str = ' '.join(cmd)
self.logger.debug("=" * 80)
self.logger.debug("WINETRICKS COMMAND (for manual reproduction):")
self.logger.debug(f" {cmd_str}")
self.logger.debug("")
self.logger.debug("Environment variables required:")
self.logger.debug(f" WINEPREFIX={env.get('WINEPREFIX', 'NOT SET')}")
self.logger.debug(f" WINE={env.get('WINE', 'NOT SET')}")
self.logger.debug("=" * 80)
# Enhanced diagnostics for bundled winetricks # Enhanced diagnostics for bundled winetricks
self.logger.debug("=== Winetricks Environment Diagnostics ===") self.logger.debug("=== Winetricks Environment Diagnostics ===")
@@ -567,8 +576,31 @@ class WinetricksHandler:
self.logger.error("") self.logger.error("")
self.logger.error("STDERR:") self.logger.error("STDERR:")
if result.stderr.strip(): if result.stderr.strip():
# Filter out verbose winetricks "Executing..." messages - these are informational, not errors
error_lines = []
verbose_lines = []
for line in result.stderr.strip().split('\n'): for line in result.stderr.strip().split('\n'):
self.logger.error(f" {line}") line_lower = line.lower().strip()
# Skip verbose informational messages
if (line_lower.startswith('executing ') or
(line_lower.startswith('grep: warning:') and 'stray' in line_lower) or
('warning; possible' in line_lower and 'extra bytes' in line_lower)):
# These are verbose info messages, log at debug level instead
verbose_lines.append(line)
else:
# Actual error/warning messages (including "returned status", "aborting", dbus errors, etc.)
error_lines.append(line)
if error_lines:
self.logger.error(" Actual errors/warnings:")
for line in error_lines:
self.logger.error(f" {line}")
if verbose_lines:
self.logger.debug(f" ({len(verbose_lines)} verbose 'Executing...' lines suppressed - see debug log for details)")
else:
self.logger.error(" (only verbose output, no actual errors)")
if verbose_lines:
self.logger.debug(f" ({len(verbose_lines)} verbose lines suppressed)")
else: else:
self.logger.error(" (empty)") self.logger.error(" (empty)")
self.logger.error("=" * 80) self.logger.error("=" * 80)

View File

@@ -41,60 +41,32 @@ class AutomatedPrefixService:
from jackify.shared.timing import get_timestamp from jackify.shared.timing import get_timestamp
return get_timestamp() return get_timestamp()
def _get_user_proton_version(self, modlist_name: str = None): def _get_user_proton_version(self):
"""Get user's preferred Proton version from config, with fallback to auto-detection """Get user's preferred Proton version from config, with fallback to auto-detection."""
Args:
modlist_name: Optional modlist name for special handling (e.g., Lorerim)
"""
try: try:
from jackify.backend.handlers.config_handler import ConfigHandler from jackify.backend.handlers.config_handler import ConfigHandler
from jackify.backend.handlers.wine_utils import WineUtils from jackify.backend.handlers.wine_utils import WineUtils
# Check for Lorerim-specific Proton override first
modlist_normalized = modlist_name.lower().replace(" ", "") if modlist_name else ""
if modlist_normalized == 'lorerim':
lorerim_proton = self._get_lorerim_preferred_proton()
if lorerim_proton:
logger.info(f"Lorerim detected: Using {lorerim_proton} instead of user settings")
self._store_proton_override_notification("Lorerim", lorerim_proton)
return lorerim_proton
# Check for Lost Legacy-specific Proton override (needs Proton 9 for ENB compatibility)
if modlist_normalized == 'lostlegacy':
lostlegacy_proton = self._get_lorerim_preferred_proton() # Use same logic as Lorerim
if lostlegacy_proton:
logger.info(f"Lost Legacy detected: Using {lostlegacy_proton} instead of user settings (ENB compatibility)")
self._store_proton_override_notification("Lost Legacy", lostlegacy_proton)
return lostlegacy_proton
config_handler = ConfigHandler() config_handler = ConfigHandler()
user_proton_path = config_handler.get_game_proton_path() user_proton_path = config_handler.get_game_proton_path()
if not user_proton_path or user_proton_path == 'auto': if not user_proton_path or user_proton_path == 'auto':
# Use enhanced fallback logic with GE-Proton preference logger.info("User selected auto-detect, using GE-Proton -> Experimental -> Proton precedence")
logger.info("User selected auto-detect, using GE-Proton → Experimental → Proton precedence") best = WineUtils.select_best_proton()
return WineUtils.select_best_proton() if best:
return best.get('steam_compat_name') or WineUtils.resolve_steam_compat_name(best['path'])
return "proton_experimental"
else: else:
# User has selected a specific Proton version steam_proton_name = WineUtils.resolve_steam_compat_name(user_proton_path)
# Use the exact directory name for Steam config.vdf if steam_proton_name:
try:
proton_version = os.path.basename(user_proton_path)
# GE-Proton uses exact directory name, Valve Proton needs lowercase conversion
if proton_version.startswith('GE-Proton'):
# Keep GE-Proton name exactly as-is
steam_proton_name = proton_version
else:
# Convert Valve Proton names to Steam's format
steam_proton_name = proton_version.lower().replace(' - ', '_').replace(' ', '_').replace('-', '_')
if not steam_proton_name.startswith('proton'):
steam_proton_name = f"proton_{steam_proton_name}"
logger.info(f"Using user-selected Proton: {steam_proton_name}") logger.info(f"Using user-selected Proton: {steam_proton_name}")
return steam_proton_name return steam_proton_name
except Exception as e:
logger.warning(f"Invalid user Proton path '{user_proton_path}', falling back to auto: {e}") logger.warning(f"Could not resolve compat name for '{user_proton_path}', falling back to auto")
return WineUtils.select_best_proton() best = WineUtils.select_best_proton()
if best:
return best.get('steam_compat_name') or WineUtils.resolve_steam_compat_name(best['path'])
return "proton_experimental"
except Exception as e: except Exception as e:
logger.error(f"Failed to get user Proton preference, using default: {e}") logger.error(f"Failed to get user Proton preference, using default: {e}")
@@ -148,8 +120,7 @@ class AutomatedPrefixService:
logger.warning(f"Could not generate STEAM_COMPAT_MOUNTS, using default: {e}") logger.warning(f"Could not generate STEAM_COMPAT_MOUNTS, using default: {e}")
launch_options = "%command%" launch_options = "%command%"
# Get user's preferred Proton version (with Lorerim-specific override) proton_version = self._get_user_proton_version()
proton_version = self._get_user_proton_version(shortcut_name)
# Create shortcut with Proton using native service # Create shortcut with Proton using native service
success, app_id = steam_service.create_shortcut_with_proton( success, app_id = steam_service.create_shortcut_with_proton(
@@ -1619,8 +1590,6 @@ echo Prefix creation complete.
if progress_callback: if progress_callback:
progress_callback(f"{self._get_progress_timestamp()} Steam Configuration complete!") progress_callback(f"{self._get_progress_timestamp()} Steam Configuration complete!")
# Show Proton override notification if applicable
self._show_proton_override_notification(progress_callback)
logger.info(" Simple automated prefix creation workflow completed successfully") logger.info(" Simple automated prefix creation workflow completed successfully")
return True, prefix_path, actual_appid return True, prefix_path, actual_appid
@@ -1785,22 +1754,36 @@ echo Prefix creation complete.
logger.error(f"Error setting Proton on shortcut: {e}") logger.error(f"Error setting Proton on shortcut: {e}")
return False return False
def run_working_workflow(self, shortcut_name: str, modlist_install_dir: str, @staticmethod
def get_ttw_installer_path() -> Optional[Path]:
"""Get path to TTW_Linux_Installer if available"""
try:
from jackify.shared.paths import get_jackify_data_dir
ttw_path = get_jackify_data_dir() / "TTW_Linux_Installer" / "ttw_linux_gui"
if ttw_path.exists():
return ttw_path
except Exception:
pass
return None
def run_working_workflow(self, shortcut_name: str, modlist_install_dir: str,
final_exe_path: str, progress_callback=None, steamdeck: Optional[bool] = None) -> Tuple[bool, Optional[Path], Optional[int], Optional[str]]: final_exe_path: str, progress_callback=None, steamdeck: Optional[bool] = None) -> Tuple[bool, Optional[Path], Optional[int], Optional[str]]:
""" """
Run the proven working automated prefix creation workflow. Run the proven working automated prefix creation workflow.
This implements our tested and working approach: This implements our tested and working approach:
1. Create shortcut with native Steam service (pointing to ModOrganizer.exe initially) 1. Create shortcut with native Steam service (pointing to ModOrganizer.exe initially)
2. Restart Steam using Jackify's robust method 2. Restart Steam using Jackify's robust method
3. Create Proton prefix invisibly using Proton wrapper with DISPLAY= 3. Create Proton prefix invisibly using Proton wrapper with DISPLAY=
4. Verify everything persists 4. Verify everything persists
Args: Args:
shortcut_name: Name for the Steam shortcut shortcut_name: Name for the Steam shortcut
modlist_install_dir: Directory where the modlist is installed modlist_install_dir: Directory where the modlist is installed
final_exe_path: Path to ModOrganizer.exe final_exe_path: Path to ModOrganizer.exe
progress_callback: Optional callback for progress updates
steamdeck: Optional Steam Deck detection override
Returns: Returns:
Tuple of (success, prefix_path, appid, last_timestamp) Tuple of (success, prefix_path, appid, last_timestamp)
""" """
@@ -1922,13 +1905,23 @@ echo Prefix creation complete.
progress_callback(f"{self._get_progress_timestamp()} Injecting {special_game_type.upper()} game registry entries...") progress_callback(f"{self._get_progress_timestamp()} Injecting {special_game_type.upper()} game registry entries...")
if prefix_path: if prefix_path:
self._inject_game_registry_entries(str(prefix_path)) self._inject_game_registry_entries(str(prefix_path), special_game_type)
else: else:
logger.warning("Could not find prefix path for registry injection") logger.warning("Could not find prefix path for registry injection")
else: else:
logger.info("Step 5: Skipping registry injection for standard modlist") logger.info("Step 5: Skipping registry injection for standard modlist")
if progress_callback: if progress_callback:
progress_callback(f"{self._get_progress_timestamp()} No special game registry injection needed") progress_callback(f"{self._get_progress_timestamp()} No special game registry injection needed")
# Step 5.5: Pre-create game-specific directories for all modlists
logger.info(f"Step 5.5: Creating game-specific user directories")
if progress_callback:
progress_callback(f"{self._get_progress_timestamp()} Creating game user directories...")
if prefix_path:
self._create_game_user_directories(str(prefix_path), special_game_type)
else:
logger.warning("Could not find prefix path for directory creation")
last_timestamp = self._get_progress_timestamp() last_timestamp = self._get_progress_timestamp()
logger.info(f" Working workflow completed successfully! AppID: {appid}, Prefix: {prefix_path}") logger.info(f" Working workflow completed successfully! AppID: {appid}, Prefix: {prefix_path}")
@@ -1936,13 +1929,10 @@ echo Prefix creation complete.
progress_callback(f"{last_timestamp} Steam integration complete") progress_callback(f"{last_timestamp} Steam integration complete")
progress_callback("") # Blank line after Steam integration complete progress_callback("") # Blank line after Steam integration complete
# Show Proton override notification if applicable
self._show_proton_override_notification(progress_callback)
if progress_callback: if progress_callback:
progress_callback("") # Extra blank line to span across Configuration Summary progress_callback("") # Extra blank line to span across Configuration Summary
progress_callback("") # And one more to create space before Prefix Configuration progress_callback("") # And one more to create space before Prefix Configuration
return True, prefix_path, appid, last_timestamp return True, prefix_path, appid, last_timestamp
except Exception as e: except Exception as e:
@@ -2776,7 +2766,7 @@ echo Prefix creation complete.
platform_service = PlatformDetectionService.get_instance() platform_service = PlatformDetectionService.get_instance()
is_steamdeck_sdcard = (platform_service.is_steamdeck and is_steamdeck_sdcard = (platform_service.is_steamdeck and
str(proton_path).startswith('/run/media/')) str(proton_path).startswith('/run/media/'))
timeout = 180 if is_steamdeck_sdcard else 60 timeout = 180 if is_steamdeck_sdcard else 120
if is_steamdeck_sdcard: if is_steamdeck_sdcard:
logger.info(f"Using extended timeout ({timeout}s) for Steam Deck SD card Proton installation") logger.info(f"Using extended timeout ({timeout}s) for Steam Deck SD card Proton installation")
@@ -3250,7 +3240,7 @@ echo Prefix creation complete.
logger.debug(f"Error during recursive wine search in {proton_path}: {e}") logger.debug(f"Error during recursive wine search in {proton_path}: {e}")
return None return None
def _inject_game_registry_entries(self, modlist_compatdata_path: str): def _inject_game_registry_entries(self, modlist_compatdata_path: str, special_game_type: str):
"""Detect and inject FNV/Enderal game paths and apply universal dotnet4.x compatibility fixes""" """Detect and inject FNV/Enderal game paths and apply universal dotnet4.x compatibility fixes"""
system_reg_path = os.path.join(modlist_compatdata_path, "pfx", "system.reg") system_reg_path = os.path.join(modlist_compatdata_path, "pfx", "system.reg")
if not os.path.exists(system_reg_path): if not os.path.exists(system_reg_path):
@@ -3290,15 +3280,6 @@ echo Prefix creation complete.
) )
if success: if success:
logger.info(f"Updated registry entry for {config['name']}") logger.info(f"Updated registry entry for {config['name']}")
# Special handling for Enderal: Create required user directory
if app_id == "976620": # Enderal Special Edition
try:
enderal_docs_path = os.path.join(modlist_compatdata_path, "pfx", "drive_c", "users", "steamuser", "Documents", "My Games", "Enderal Special Edition")
os.makedirs(enderal_docs_path, exist_ok=True)
logger.info(f"Created Enderal user directory: {enderal_docs_path}")
except Exception as e:
logger.warning(f"Failed to create Enderal user directory: {e}")
else: else:
logger.warning(f"Failed to update registry entry for {config['name']}") logger.warning(f"Failed to update registry entry for {config['name']}")
else: else:
@@ -3306,91 +3287,48 @@ echo Prefix creation complete.
logger.info("Game registry injection completed") logger.info("Game registry injection completed")
def _get_lorerim_preferred_proton(self): def _create_game_user_directories(self, modlist_compatdata_path: str, special_game_type: str):
"""Get Lorerim's preferred Proton 9 version with specific priority order""" """
try: Pre-create game-specific user directories to prevent first-launch issues.
from jackify.backend.handlers.wine_utils import WineUtils
# Get all available Proton versions Creates both My Documents/My Games and AppData/Local directories for the game.
available_versions = WineUtils.scan_all_proton_versions() This prevents issues where games fail to create these on first launch under Proton.
"""
# Map game types to their directory names
game_dir_names = {
"skyrim": "Skyrim Special Edition",
"fnv": "FalloutNV",
"fo4": "Fallout4",
"oblivion": "Oblivion",
"oblivion_remastered": "Oblivion Remastered",
"enderal": "Enderal Special Edition",
"starfield": "Starfield"
}
if not available_versions: # Get the directory name for this game type
logger.warning("No Proton versions found for Lorerim override") game_dir_name = game_dir_names.get(special_game_type)
return None if not game_dir_name:
logger.debug(f"No user directory mapping for game type: {special_game_type}")
return
# Priority order for Lorerim: base_path = os.path.join(modlist_compatdata_path, "pfx", "drive_c", "users", "steamuser")
# 1. GEProton9-27 (specific version)
# 2. Other GEProton-9 versions (latest first)
# 3. Valve Proton 9 (any version)
preferred_candidates = [] directories_to_create = [
os.path.join(base_path, "Documents", "My Games", game_dir_name),
os.path.join(base_path, "AppData", "Local", game_dir_name)
]
for version in available_versions: created_count = 0
version_name = version['name'] for directory in directories_to_create:
try:
os.makedirs(directory, exist_ok=True)
logger.info(f"Created user directory: {directory}")
created_count += 1
except Exception as e:
logger.warning(f"Failed to create directory {directory}: {e}")
if created_count > 0:
logger.info(f"Created {created_count} user directories for {game_dir_name}")
# Priority 1: GEProton9-27 specifically
if version_name == 'GE-Proton9-27':
logger.info(f"Lorerim: Found preferred GE-Proton9-27")
return version_name
# Priority 2: Other GE-Proton 9 versions
elif version_name.startswith('GE-Proton9-'):
preferred_candidates.append(('ge_proton_9', version_name, version))
# Priority 3: Valve Proton 9
elif 'Proton 9' in version_name:
preferred_candidates.append(('valve_proton_9', version_name, version))
# Return best candidate if any found
if preferred_candidates:
# Sort by priority (GE-Proton first, then by name for latest)
preferred_candidates.sort(key=lambda x: (x[0], x[1]), reverse=True)
best_candidate = preferred_candidates[0]
logger.info(f"Lorerim: Selected {best_candidate[1]} as best Proton 9 option")
return best_candidate[1]
logger.warning("Lorerim: No suitable Proton 9 versions found, will use user settings")
return None
except Exception as e:
logger.error(f"Error detecting Lorerim Proton preference: {e}")
return None
def _store_proton_override_notification(self, modlist_name: str, proton_version: str):
"""Store Proton override information for end-of-install notification"""
try:
# Store override info for later display
if not hasattr(self, '_proton_overrides'):
self._proton_overrides = []
self._proton_overrides.append({
'modlist': modlist_name,
'proton_version': proton_version,
'reason': f'{modlist_name} requires Proton 9 for optimal compatibility'
})
logger.debug(f"Stored Proton override notification: {modlist_name}{proton_version}")
except Exception as e:
logger.error(f"Failed to store Proton override notification: {e}")
def _show_proton_override_notification(self, progress_callback=None):
"""Display any Proton override notifications to the user"""
try:
if hasattr(self, '_proton_overrides') and self._proton_overrides:
for override in self._proton_overrides:
notification_msg = f"PROTON OVERRIDE: {override['modlist']} configured to use {override['proton_version']} for optimal compatibility"
if progress_callback:
progress_callback("")
progress_callback(f"{self._get_progress_timestamp()} {notification_msg}")
logger.info(notification_msg)
# Clear notifications after display
self._proton_overrides = []
except Exception as e:
logger.error(f"Failed to show Proton override notification: {e}")

View File

@@ -485,24 +485,23 @@ class NativeSteamService:
if proton_version is None: if proton_version is None:
try: try:
from jackify.backend.handlers.config_handler import ConfigHandler from jackify.backend.handlers.config_handler import ConfigHandler
from jackify.backend.handlers.wine_utils import WineUtils
config_handler = ConfigHandler() config_handler = ConfigHandler()
game_proton_path = config_handler.get_game_proton_path() game_proton_path = config_handler.get_game_proton_path()
if game_proton_path and game_proton_path != 'auto': if game_proton_path and game_proton_path != 'auto':
# User has selected Game Proton - use it resolved = WineUtils.resolve_steam_compat_name(game_proton_path)
proton_version = os.path.basename(game_proton_path) if resolved:
# Convert to Steam format proton_version = resolved
if not proton_version.startswith('GE-Proton'): logger.info(f"Using Game Proton from settings: {proton_version}")
proton_version = proton_version.lower().replace(' - ', '_').replace(' ', '_').replace('-', '_') else:
if not proton_version.startswith('proton'): logger.warning(f"Could not resolve compat name for '{game_proton_path}', falling back to auto")
proton_version = f"proton_{proton_version}" game_proton_path = None
logger.info(f"Using Game Proton from settings: {proton_version}")
else: if not game_proton_path or game_proton_path == 'auto':
# Fallback to auto-detect if Game Proton not set
from jackify.backend.handlers.wine_utils import WineUtils
best_proton = WineUtils.select_best_proton() best_proton = WineUtils.select_best_proton()
if best_proton: if best_proton:
proton_version = best_proton['name'] proton_version = best_proton.get('steam_compat_name') or WineUtils.resolve_steam_compat_name(best_proton['path'])
logger.info(f"Auto-detected Game Proton: {proton_version}") logger.info(f"Auto-detected Game Proton: {proton_version}")
else: else:
proton_version = "proton_experimental" proton_version = "proton_experimental"

View File

@@ -0,0 +1,220 @@
"""
Nexus Download Service
Handles downloading mod files from Nexus Mods using OAuth authentication.
"""
import logging
import requests
import time
from pathlib import Path
from typing import Optional, Callable, Tuple
logger = logging.getLogger(__name__)
class NexusDownloadService:
"""Service for downloading files from Nexus Mods"""
NEXUS_API_BASE = "https://api.nexusmods.com/v1"
def __init__(self, auth_token: str):
"""
Initialize Nexus download service.
Args:
auth_token: OAuth access token or API key
"""
self.auth_token = auth_token
self.headers = {
"Authorization": f"Bearer {auth_token}",
"User-Agent": "jackify"
}
def get_mod_files(self, game_domain: str, mod_id: int) -> Optional[list]:
"""
Get list of files for a mod.
Args:
game_domain: Game domain (e.g., 'newvegas')
mod_id: Mod ID number
Returns:
List of file metadata dicts, or None if failed
"""
try:
url = f"{self.NEXUS_API_BASE}/games/{game_domain}/mods/{mod_id}/files.json"
response = requests.get(url, headers=self.headers, timeout=30)
response.raise_for_status()
data = response.json()
files = data.get('files', [])
logger.info(f"Found {len(files)} files for mod {mod_id}")
return files
except Exception as e:
logger.error(f"Failed to get mod files: {e}")
return None
def get_download_link(self, game_domain: str, mod_id: int, file_id: int) -> Optional[str]:
"""
Get download link for a specific file.
Args:
game_domain: Game domain (e.g., 'newvegas')
mod_id: Mod ID number
file_id: File ID number
Returns:
Download URL, or None if failed
"""
try:
url = f"{self.NEXUS_API_BASE}/games/{game_domain}/mods/{mod_id}/files/{file_id}/download_link.json"
response = requests.get(url, headers=self.headers, timeout=30)
# Check for specific error codes
if response.status_code == 403:
logger.error(f"Download link request forbidden (403) - Nexus Premium required for file {file_id}")
return None
elif response.status_code == 404:
logger.error(f"Download link request not found (404) - file {file_id} may not exist")
return None
response.raise_for_status()
data = response.json()
# API returns list of download servers
if isinstance(data, list) and len(data) > 0:
download_url = data[0].get('URI')
logger.info(f"Got download link for file {file_id}")
return download_url
else:
logger.error(f"No download link returned for file {file_id}")
return None
except requests.exceptions.HTTPError as e:
logger.error(f"Failed to get download link: HTTP {e.response.status_code} - {e}")
return None
except Exception as e:
logger.error(f"Failed to get download link: {e}")
return None
def download_file(
self,
download_url: str,
output_path: Path,
progress_callback: Optional[Callable[[int, int], None]] = None
) -> bool:
"""
Download a file from Nexus.
Args:
download_url: Download URL from get_download_link()
output_path: Where to save the file
progress_callback: Optional callback(downloaded_bytes, total_bytes)
Returns:
True if successful
"""
try:
output_path.parent.mkdir(parents=True, exist_ok=True)
response = requests.get(download_url, stream=True, timeout=60)
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if progress_callback and total_size > 0:
progress_callback(downloaded, total_size)
logger.info(f"Downloaded {output_path.name} ({downloaded} bytes)")
return True
except Exception as e:
logger.error(f"Download failed: {e}")
if output_path.exists():
output_path.unlink()
return False
def download_latest_file(
self,
game_domain: str,
mod_id: int,
output_dir: Path,
file_name_filter: Optional[str] = None,
progress_callback: Optional[Callable[[str], None]] = None
) -> Tuple[bool, Optional[Path], str]:
"""
Download the latest file from a mod.
Args:
game_domain: Game domain (e.g., 'newvegas')
mod_id: Mod ID number
output_dir: Directory to save file
file_name_filter: Optional substring to filter files (e.g., 'linux', 'mpi')
progress_callback: Optional callback for status updates
Returns:
Tuple of (success, file_path, message)
"""
def update_progress(msg: str):
if progress_callback:
progress_callback(msg)
logger.info(msg)
try:
update_progress(f"Fetching file list for mod {mod_id}...")
files = self.get_mod_files(game_domain, mod_id)
if not files:
return False, None, "Failed to get mod file list"
# Filter files if requested
if file_name_filter:
filtered = [f for f in files if file_name_filter.lower() in f.get('file_name', '').lower()]
if not filtered:
return False, None, f"No files found matching '{file_name_filter}'"
files = filtered
# Get the most recent file
files.sort(key=lambda f: f.get('uploaded_timestamp', 0), reverse=True)
latest_file = files[0]
file_id = latest_file['file_id']
file_name = latest_file['file_name']
update_progress(f"Downloading {file_name}...")
download_url = self.get_download_link(game_domain, mod_id, file_id)
if not download_url:
return False, None, "Failed to get download link"
output_path = output_dir / file_name
def download_progress(downloaded, total):
if total > 0:
percent = (downloaded / total) * 100
update_progress(f"Downloading: {percent:.1f}%")
success = self.download_file(download_url, output_path, download_progress)
if success:
return True, output_path, f"Downloaded {file_name}"
else:
return False, None, "Download failed"
except Exception as e:
error_msg = f"Download failed: {str(e)}"
logger.error(error_msg, exc_info=True)
return False, None, error_msg

View File

@@ -0,0 +1,198 @@
"""
VNV Integration Helper
Helper functions to integrate VNV post-install automation into modlist workflows.
Handles detection, confirmation, and execution for:
- Install Modlist
- Configure New Modlist
- Configure Existing Modlist
"""
import logging
import configparser
import re
from pathlib import Path
from typing import Optional, Callable, Tuple
from .vnv_post_install_service import VNVPostInstallService
logger = logging.getLogger(__name__)
def _parse_bytearray_value(value: str) -> str:
"""
Parse Qt @ByteArray format to extract the actual string value.
Format: @ByteArray(Viva New Vegas Extended)
Returns: Viva New Vegas Extended
"""
match = re.match(r'@ByteArray\((.*)\)', value)
if match:
return match.group(1)
return value
def _check_modorganizer_ini_profile(modlist_install_location: Path) -> bool:
"""
Check ModOrganizer.ini for VNV profile names.
Args:
modlist_install_location: Path to modlist installation directory
Returns:
True if selected_profile is "Viva New Vegas" or "Viva New Vegas Extended"
"""
try:
mo_ini_path = modlist_install_location / "ModOrganizer.ini"
if not mo_ini_path.exists():
logger.debug(f"ModOrganizer.ini not found at {mo_ini_path}")
return False
config = configparser.ConfigParser()
# Read with UTF-8-sig to handle BOM
config.read(mo_ini_path, encoding='utf-8-sig')
if 'General' not in config:
logger.debug("No [General] section in ModOrganizer.ini")
return False
selected_profile_raw = config.get('General', 'selected_profile', fallback='')
if not selected_profile_raw:
logger.debug("No selected_profile in ModOrganizer.ini")
return False
# Parse @ByteArray format
selected_profile = _parse_bytearray_value(selected_profile_raw)
logger.debug(f"Found selected_profile: {selected_profile}")
# Check if it's one of the VNV profiles
vnv_profiles = ["Viva New Vegas", "Viva New Vegas Extended"]
return selected_profile in vnv_profiles
except Exception as e:
logger.debug(f"Error checking ModOrganizer.ini for VNV profile: {e}")
return False
def should_offer_vnv_automation(modlist_name: str, modlist_install_location: Optional[Path] = None) -> bool:
"""
Check if VNV automation should be offered for this modlist.
Detection methods (in order of reliability):
1. Check ModOrganizer.ini selected_profile (most reliable)
2. Check modlist name for VNV patterns
Args:
modlist_name: Name of the modlist
modlist_install_location: Optional path to modlist installation directory
Returns:
True if VNV automation should be offered
"""
# Method 1: Check ModOrganizer.ini profile (most reliable)
if modlist_install_location:
if _check_modorganizer_ini_profile(modlist_install_location):
logger.info(f"VNV detected via ModOrganizer.ini profile in {modlist_install_location}")
return True
# Method 2: Check modlist name patterns
modlist_name_lower = modlist_name.lower()
vnv_patterns = [
"viva new vegas",
"vnv", # Common abbreviation
"viva new vegas extended"
]
for pattern in vnv_patterns:
if pattern in modlist_name_lower:
logger.info(f"VNV detected via name pattern '{pattern}' in '{modlist_name}'")
return True
return False
def run_vnv_automation_if_applicable(
modlist_name: str,
modlist_install_location: Path,
game_root: Path,
ttw_installer_path: Optional[Path] = None,
progress_callback: Optional[Callable[[str], None]] = None,
manual_file_callback: Optional[Callable[[str, str], Optional[Path]]] = None,
confirmation_callback: Optional[Callable[[str], bool]] = None
) -> Tuple[bool, Optional[str]]:
"""
Check if VNV automation should run, get user confirmation, and execute if confirmed.
Args:
modlist_name: Name of the installed modlist
modlist_install_location: Path to modlist installation
game_root: Path to game root directory
ttw_installer_path: Optional path to TTW_Linux_Installer (for BSA decompression)
progress_callback: Optional callback for progress updates
manual_file_callback: Optional callback for manual file selection (non-Premium)
confirmation_callback: Optional callback for user confirmation
Takes description string, returns True if user confirms
Returns:
Tuple of (automation_was_run: bool, error_message: Optional[str])
"""
try:
# Check if this is VNV (pass install location for ModOrganizer.ini check)
if not should_offer_vnv_automation(modlist_name, modlist_install_location):
logger.debug(f"Modlist '{modlist_name}' does not require VNV automation")
return False, None
logger.info(f"VNV detected: {modlist_name}")
# Initialize service
vnv_service = VNVPostInstallService(
modlist_install_location=modlist_install_location,
game_root=game_root,
ttw_installer_path=ttw_installer_path
)
# Check what's already done
completed = vnv_service.check_already_completed()
# Only skip if ALL three steps are completed
if completed['root_mods'] and completed['4gb_patch'] and completed['bsa_decompressed']:
logger.info("VNV automation steps already completed")
if progress_callback:
progress_callback("VNV post-install steps already completed")
return False, None
# Get confirmation from user (required)
if not confirmation_callback:
logger.error("VNV automation requires confirmation_callback")
return False, "VNV automation requires user confirmation"
if confirmation_callback:
description = vnv_service.get_automation_description()
if not confirmation_callback(description):
logger.info("User declined VNV automation")
if progress_callback:
progress_callback("VNV automation skipped by user")
return False, None
# Run automation
logger.info("Starting VNV post-install automation")
if progress_callback:
progress_callback("Running VNV post-install automation...")
success, message = vnv_service.run_all_steps(
progress_callback=progress_callback,
manual_file_callback=manual_file_callback
)
if success:
logger.info(f"VNV automation completed: {message}")
if progress_callback:
progress_callback(f"VNV automation: {message}")
return True, None
else:
logger.error(f"VNV automation failed: {message}")
return True, message
except Exception as e:
error_msg = f"VNV automation error: {str(e)}"
logger.error(error_msg, exc_info=True)
return True, error_msg

View File

@@ -0,0 +1,622 @@
"""
Viva New Vegas Post-Install Service
Automates the post-installation steps required for Viva New Vegas modlist:
1. Root Mods - Copy files from '__Files Requiring Manual Install' to game root
2. 4GB Patcher - Download Linux version from Nexus, run natively
3. BSA Decompression - Download FNV BSA Decompressor MPI, run via TTW_Linux_Installer
These steps are documented at: https://vivanewvegas.moddinglinked.com/wabbajack.html
Uses native Linux tools (no Wine required) by downloading from Nexus with OAuth.
"""
import logging
import os
import shutil
import subprocess
import stat
import tempfile
import zipfile
from pathlib import Path
from typing import Optional, Callable
from ..handlers.subprocess_utils import get_clean_subprocess_env
from .nexus_download_service import NexusDownloadService
from .nexus_auth_service import NexusAuthService
logger = logging.getLogger(__name__)
class VNVPostInstallService:
"""Handles automated post-installation tasks for Viva New Vegas modlist."""
# Nexus mod IDs for required tools
LINUX_4GB_PATCHER_MOD_ID = 62552
FNV_BSA_DECOMPRESSOR_MOD_ID = 65854
GAME_DOMAIN = "newvegas"
def __init__(self, modlist_install_location: Path, game_root: Path,
ttw_installer_path: Optional[Path] = None):
"""
Initialize VNV post-install service.
Args:
modlist_install_location: Path to the VNV installation (e.g., ~/VNV)
game_root: Path to Fallout New Vegas game root
ttw_installer_path: Path to TTW_Linux_Installer executable (for BSA decompression)
"""
self.modlist_install = modlist_install_location
self.game_root = game_root
self.ttw_installer_path = ttw_installer_path
# VNV-specific paths
self.manual_install_dir = self.modlist_install / "__Files Requiring Manual Install"
# Download cache directory
from jackify.shared.paths import get_jackify_data_dir
self.cache_dir = get_jackify_data_dir() / "vnv_post_install_cache"
self.cache_dir.mkdir(parents=True, exist_ok=True)
# Initialize authentication
self.auth_service = NexusAuthService()
self.download_service = None
def _ensure_auth(self, progress_callback: Optional[Callable[[str], None]] = None) -> bool:
"""
Ensure we have valid Nexus authentication for downloads.
Args:
progress_callback: Optional callback for progress updates
Returns:
True if authenticated
"""
auth_token = self.auth_service.ensure_valid_auth()
if not auth_token:
if progress_callback:
progress_callback("Nexus authentication required for post-install steps")
logger.error("No Nexus authentication available")
return False
self.download_service = NexusDownloadService(auth_token)
return True
def should_run_automation(self, modlist_name: str) -> bool:
"""
Check if this modlist should trigger VNV automation.
Args:
modlist_name: Name of the installed modlist
Returns:
True if VNV automation should be offered
"""
return "viva new vegas" in modlist_name.lower()
def get_automation_description(self) -> str:
"""
Get user-friendly description of what VNV automation does.
Returns:
Description string for confirmation dialog
"""
return (
"Viva New Vegas Automation\n\n"
"Jackify can automatically perform the following post-install steps:\n\n"
"1. Copy root mods to game directory\n"
"2. Download and run Linux 4GB patcher\n"
"3. Download and run BSA decompressor (reduces loading times)\n\n"
"Premium users: Downloads happen automatically\n"
"Non-Premium users: You'll be prompted to download files manually\n\n"
"Would you like Jackify to automate these steps?"
)
def check_already_completed(self) -> dict:
"""
Check which VNV automation steps have already been completed.
Returns:
Dict with keys: 'root_mods', '4gb_patch', 'bsa_decompressed'
"""
# Check if 4GB patch already applied
backup_exe = self.game_root / "FalloutNV_backup.exe"
already_patched = backup_exe.exists()
# Check if root mods copied (look for FNVpatch.exe in game root)
root_mods_copied = (self.game_root / "FNVpatch.exe").exists()
# Check for BSA decompression marker file
marker_file = self.game_root / ".jackify_bsa_decompressed"
bsa_decompressed = marker_file.exists()
return {
'root_mods': root_mods_copied,
'4gb_patch': already_patched,
'bsa_decompressed': bsa_decompressed
}
def run_all_steps(self, progress_callback: Optional[Callable[[str], None]] = None,
manual_file_callback: Optional[Callable[[str, str], Optional[Path]]] = None,
skip_confirmation: bool = False) -> tuple[bool, str]:
"""
Run all VNV post-install steps in sequence.
Args:
progress_callback: Optional callback for progress updates
manual_file_callback: Optional callback for manual file selection (non-Premium users)
Takes (title, instructions) returns Path or None
skip_confirmation: Skip user confirmation (for programmatic use)
Returns:
(success: bool, message: str)
"""
def update_progress(msg: str):
if progress_callback:
progress_callback(msg)
logger.info(msg)
try:
# Ensure authentication
update_progress("Checking Nexus authentication...")
if not self._ensure_auth(progress_callback):
return False, "Nexus authentication required. Please authenticate in Settings."
# Step 1: Copy root mods
update_progress("Step 1/3: Copying root mods to game directory...")
success, msg = self.copy_root_mods()
if not success:
return False, f"Root mods failed: {msg}"
update_progress(f"Root mods: {msg}")
# Step 2: Run 4GB patcher
update_progress("Step 2/3: Downloading and running 4GB patcher...")
success, msg = self.run_4gb_patcher(update_progress, manual_file_callback)
if not success:
return False, f"4GB patcher failed: {msg}"
update_progress(f"4GB patcher: {msg}")
# Step 3: Run BSA decompressor
update_progress("Step 3/3: Downloading and running BSA decompressor...")
success, msg = self.run_bsa_decompressor(update_progress, manual_file_callback)
if not success:
return False, f"BSA decompression failed: {msg}"
update_progress(f"BSA decompression: {msg}")
return True, "VNV post-install completed successfully"
except Exception as e:
error_msg = f"VNV post-install failed: {str(e)}"
logger.error(error_msg, exc_info=True)
return False, error_msg
def copy_root_mods(self) -> tuple[bool, str]:
"""
Copy files from '__Files Requiring Manual Install' to game root.
Returns:
(success: bool, message: str)
"""
try:
if not self.manual_install_dir.exists():
return False, f"Manual install directory not found: {self.manual_install_dir}"
if not self.game_root.exists():
return False, f"Game root directory not found: {self.game_root}"
# Copy all files from manual install to game root
copied_files = []
for item in self.manual_install_dir.iterdir():
dest = self.game_root / item.name
if item.is_file():
shutil.copy2(item, dest)
copied_files.append(item.name)
logger.debug(f"Copied: {item.name}")
elif item.is_dir():
# Merge directories to preserve vanilla game files (e.g., BSA files in Data/)
# dirs_exist_ok=True allows adding NVSE to Data/ without deleting vanilla BSAs
shutil.copytree(item, dest, dirs_exist_ok=True)
copied_files.append(f"{item.name}/")
logger.debug(f"Copied directory: {item.name}/")
if not copied_files:
return False, "No files found to copy"
logger.info(f"Copied {len(copied_files)} items to game root")
return True, f"Copied {len(copied_files)} items to game root"
except Exception as e:
error_msg = f"Failed to copy root mods: {str(e)}"
logger.error(error_msg, exc_info=True)
return False, error_msg
def run_4gb_patcher(self, progress_callback: Optional[Callable[[str], None]] = None,
manual_file_callback: Optional[Callable[[str, str], Optional[Path]]] = None) -> tuple[bool, str]:
"""
Download and run native Linux 4GB patcher.
Args:
progress_callback: Optional callback for progress updates
manual_file_callback: Optional callback for manual file selection
Takes (title, instructions) returns Path or None
Returns:
(success: bool, message: str)
"""
try:
# Check if already patched
backup_exe = self.game_root / "FalloutNV_backup.exe"
if backup_exe.exists():
logger.info("Game already has 4GB patch (backup exists)")
return True, "Game already patched (backup exists)"
# Check cache first - look for extracted executable or zip
patcher_path = None
cached_extracted = list(self.cache_dir.glob("*4gb*_extracted/*"))
if cached_extracted:
# Use already extracted executable
for f in cached_extracted:
if f.is_file():
patcher_path = f
logger.info(f"Using cached extracted 4GB patcher: {patcher_path}")
break
if not patcher_path:
cached_files = list(self.cache_dir.glob("*4gb*.zip"))
if cached_files:
patcher_path = cached_files[0]
logger.info(f"Using cached 4GB patcher zip: {patcher_path}")
if not patcher_path:
# Try to download from Nexus
# Note: The Linux version is named "FNV4GB for Proton", not "linux"
success, patcher_path, msg = self.download_service.download_latest_file(
self.GAME_DOMAIN,
self.LINUX_4GB_PATCHER_MOD_ID,
self.cache_dir,
file_name_filter="proton",
progress_callback=progress_callback
)
if not success:
# Download failed - offer manual download
logger.error(f"Automatic download failed: {msg}")
logger.debug(f"Looking for file with 'proton' in name on mod {self.LINUX_4GB_PATCHER_MOD_ID}")
if not manual_file_callback:
return False, f"Failed to download 4GB patcher: {msg}\n\nPlease download manually from: https://www.nexusmods.com/newvegas/mods/62552"
instructions = (
"Automatic download failed (requires Nexus Premium).\n\n"
"Please download the Linux 4GB Patcher manually:\n\n"
"1. Visit: https://www.nexusmods.com/newvegas/mods/62552\n\n"
"2. Download the file named 'FNV4GB for Linux'\n\n"
"3. Select the downloaded file below"
)
patcher_path = manual_file_callback("4GB Patcher Required", instructions)
if not patcher_path or not patcher_path.exists():
return False, "4GB patcher file not provided"
# Copy to cache for future use
cached_path = self.cache_dir / patcher_path.name
shutil.copy2(patcher_path, cached_path)
patcher_path = cached_path
logger.info(f"Using manually selected 4GB patcher: {patcher_path}")
# Extract if it's a zip file and not already extracted
if patcher_path.suffix.lower() == '.zip':
extract_dir = self.cache_dir / f"{patcher_path.stem}_extracted"
# Extract if not already done
if not extract_dir.exists():
logger.info(f"Extracting {patcher_path.name}...")
extract_dir.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(patcher_path, 'r') as zip_ref:
zip_ref.extractall(extract_dir)
logger.info(f"Extracted to {extract_dir}")
# Find the executable
executables = list(extract_dir.glob("*"))
if not executables:
return False, "No files found in 4GB patcher zip"
# Look for executable file (FalloutNVPatcher or similar)
patcher_exe = None
for f in executables:
if f.is_file() and ('fallout' in f.name.lower() or 'patcher' in f.name.lower() or 'fnv' in f.name.lower()):
patcher_exe = f
break
if not patcher_exe:
# Use first file if no obvious match
patcher_exe = next((f for f in executables if f.is_file()), None)
if not patcher_exe:
return False, "No executable found in 4GB patcher zip"
patcher_path = patcher_exe
logger.info(f"Using patcher executable: {patcher_path.name}")
# Make executable
patcher_path.chmod(patcher_path.stat().st_mode | stat.S_IEXEC)
# Run patcher
if progress_callback:
progress_callback("Running 4GB patcher...")
result = subprocess.run(
[str(patcher_path)],
cwd=str(self.game_root),
capture_output=True,
text=True,
timeout=60
)
# Check if backup was created (indicates success)
if backup_exe.exists():
logger.info("4GB patch applied successfully")
return True, "4GB patch applied successfully"
else:
logger.warning(f"Patcher output: {result.stdout}")
if result.stderr:
logger.warning(f"Patcher errors: {result.stderr}")
return False, "Patcher ran but FalloutNV_backup.exe not created"
except subprocess.TimeoutExpired:
return False, "4GB patcher timed out after 60 seconds"
except Exception as e:
error_msg = f"Failed to run 4GB patcher: {str(e)}"
logger.error(error_msg, exc_info=True)
return False, error_msg
def run_bsa_decompressor(self, progress_callback: Optional[Callable[[str], None]] = None,
manual_file_callback: Optional[Callable[[str, str], Optional[Path]]] = None) -> tuple[bool, str]:
"""
Download FNV BSA Decompressor MPI and run via TTW_Linux_Installer.
Args:
progress_callback: Optional callback for progress updates
manual_file_callback: Optional callback for manual file selection
Takes (title, instructions) returns Path or None
Returns:
(success: bool, message: str)
"""
try:
# Check if already completed
marker_file = self.game_root / ".jackify_bsa_decompressed"
if marker_file.exists():
logger.info("BSA decompression already completed (marker file exists)")
return True, "BSA decompression already completed"
if not self.ttw_installer_path or not self.ttw_installer_path.exists():
logger.warning("TTW_Linux_Installer not found, skipping BSA decompression")
return True, "BSA decompression skipped (TTW_Linux_Installer not available)"
# Check cache first
cached_files = list(self.cache_dir.glob("*BSA*.mpi"))
if cached_files:
mpi_path = cached_files[0]
logger.info(f"Using cached BSA Decompressor MPI: {mpi_path}")
else:
# Also check for exact filename match (handles spaces in filename)
exact_path = self.cache_dir / "FNV BSA Decompressor.mpi"
if exact_path.exists():
mpi_path = exact_path
logger.info(f"Using cached BSA Decompressor MPI: {mpi_path}")
else:
# Try to download from Nexus
success, mpi_path, msg = self.download_service.download_latest_file(
self.GAME_DOMAIN,
self.FNV_BSA_DECOMPRESSOR_MOD_ID,
self.cache_dir,
file_name_filter="mpi",
progress_callback=progress_callback
)
if not success:
# Download failed - offer manual download
logger.warning(f"Automatic download failed: {msg}")
if not manual_file_callback:
return False, f"Failed to download BSA Decompressor MPI: {msg}\n\nPlease download manually from: https://www.nexusmods.com/newvegas/mods/65854"
instructions = (
"Automatic download failed (requires Nexus Premium).\n\n"
"Please download the FNV BSA Decompressor manually:\n"
"1. Visit: https://www.nexusmods.com/newvegas/mods/65854\n"
"2. Download the .mpi file\n"
"3. Select the downloaded file below"
)
mpi_path = manual_file_callback("BSA Decompressor Required", instructions)
if not mpi_path or not mpi_path.exists():
return False, "BSA Decompressor MPI file not provided"
# Validate it's an MPI file
if not mpi_path.suffix.lower() == '.mpi':
return False, f"Selected file is not an MPI file: {mpi_path}"
# Copy to cache for future use
cached_path = self.cache_dir / mpi_path.name
shutil.copy2(mpi_path, cached_path)
mpi_path = cached_path
logger.info(f"Using manually selected BSA Decompressor MPI: {mpi_path}")
# Create temp output directory
with tempfile.TemporaryDirectory() as temp_output:
temp_output_path = Path(temp_output)
# Create config file for TTW_Linux_Installer (handles spaces in paths better)
config_file = self.ttw_installer_path.parent / "ttw-config.json"
import json
config_data = {
"FalloutNVRoot": str(self.game_root),
"MpiPackagePath": str(mpi_path),
"DestinationPath": str(temp_output_path)
}
with open(config_file, 'w') as f:
json.dump(config_data, f, indent=2)
logger.debug(f"Created MPI config file: {config_file}")
# Run via TTW_Linux_Installer
if progress_callback:
progress_callback("Running BSA decompressor...")
cmd = [
str(self.ttw_installer_path),
"--start"
]
logger.info(f"Running BSA decompressor: {' '.join(cmd)}")
logger.debug(f"Using config file: {config_file}")
logger.debug(f"Config: {json.dumps(config_data, indent=2)}")
env = get_clean_subprocess_env()
# Stream output and parse progress
import re
process = subprocess.Popen(
cmd,
cwd=str(self.ttw_installer_path.parent),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
)
# Pattern to match progress: "Assets processed: 12345/48649"
progress_pattern = re.compile(r'Assets processed: (\d+)/(\d+)')
last_progress = None
# Capture all output for diagnostics
all_output = []
already_modified_detected = False
# Stream output line by line
for line in process.stdout:
line = line.rstrip()
all_output.append(line)
# Check for "already modified" messages
if "already" in line.lower() and ("modified" in line.lower() or "decompressed" in line.lower()):
already_modified_detected = True
logger.info(f"BSA decompressor reports: {line}")
# Check for progress updates
match = progress_pattern.search(line)
if match:
current = int(match.group(1))
total = int(match.group(2))
percent = (current / total * 100) if total > 0 else 0
progress_msg = f"Decompressing BSA files: {current}/{total} ({percent:.1f}%)"
# Only send update if progress changed significantly
if last_progress is None or current - last_progress >= total // 100:
if progress_callback:
progress_callback(progress_msg)
# Log progress updates (not every single file)
logger.debug(f"BSA decompression progress: {current}/{total} ({percent:.1f}%)")
last_progress = current
# Wait for process to complete
return_code = process.wait(timeout=600)
# Log full output for debugging failures
if return_code != 0:
logger.debug(f"BSA decompressor output:\n" + "\n".join(all_output[-50:])) # Last 50 lines
# Clean up config file after execution
try:
if config_file.exists():
config_file.unlink()
logger.debug(f"Cleaned up config file: {config_file}")
except Exception as e:
logger.warning(f"Failed to clean up config file: {e}")
if return_code == 0:
# Check if files were actually extracted to temp directory
extracted_files = list(temp_output_path.rglob("*"))
if extracted_files:
logger.info(f"BSA decompression extracted {len(extracted_files)} files")
# Copy extracted files back to game Data directory
data_dir = self.game_root / "Data"
copied_count = 0
for extracted_file in extracted_files:
if extracted_file.is_file():
# Preserve relative path structure
relative_path = extracted_file.relative_to(temp_output_path)
dest_file = data_dir / relative_path
dest_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(extracted_file, dest_file)
copied_count += 1
logger.info(f"Copied {copied_count} decompressed files to {data_dir}")
# Create marker file to indicate completion
marker_file = self.game_root / ".jackify_bsa_decompressed"
marker_file.touch()
logger.info("BSA decompression completed successfully")
return True, "BSA decompression completed successfully"
else:
# No files extracted - might be already decompressed or failed silently
logger.warning("BSA decompressor returned 0 but no files were extracted")
# Check if already decompressed by looking for marker
marker_file = self.game_root / ".jackify_bsa_decompressed"
if marker_file.exists():
logger.info("BSA files already decompressed (marker file exists)")
return True, "BSA files already decompressed"
else:
return False, "BSA decompressor completed but no files were extracted"
else:
# Exit code 1 often means "already decompressed" - check output and marker
marker_file = self.game_root / ".jackify_bsa_decompressed"
# If output explicitly said "already modified/decompressed", treat as success
if already_modified_detected:
logger.info("BSA decompressor reports files already modified - marking as completed")
marker_file.touch()
return True, "BSA files already decompressed"
# Check marker file
if marker_file.exists():
logger.info("BSA decompressor returned error but marker file exists - assuming already completed")
return True, "BSA decompression already completed"
# Try to provide helpful error message based on exit code and output
logger.error(f"BSA decompressor failed with exit code {return_code}")
error_details = f"BSA decompressor failed with exit code {return_code}."
if return_code == 1:
error_details += (
"\n\nThis may indicate the BSA files are already decompressed or modified. "
"If you've run this before, the step may have already completed. "
"Otherwise, try running the decompressor manually from: "
"https://www.nexusmods.com/newvegas/mods/65854"
)
else:
error_details += (
f"\n\nPlease check that:\n"
f"1. Fallout New Vegas is properly installed at: {self.game_root}\n"
f"2. The BSA files exist in the Data directory\n"
f"3. You have write permissions to the game directory\n\n"
f"You can complete this step manually using the guide at:\n"
f"https://vivanewvegas.moddinglinked.com/wabbajack.html"
)
return False, error_details
except subprocess.TimeoutExpired:
return False, "BSA decompression timed out after 10 minutes"
except Exception as e:
error_msg = f"Failed to run BSA decompressor: {str(e)}"
logger.error(error_msg, exc_info=True)
return False, error_msg

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,7 +7,7 @@
"targets": { "targets": {
".NETCoreApp,Version=v8.0": {}, ".NETCoreApp,Version=v8.0": {},
".NETCoreApp,Version=v8.0/linux-x64": { ".NETCoreApp,Version=v8.0/linux-x64": {
"jackify-engine/0.4.6": { "jackify-engine/0.4.7": {
"dependencies": { "dependencies": {
"Markdig": "0.40.0", "Markdig": "0.40.0",
"Microsoft.Extensions.Configuration.Json": "9.0.1", "Microsoft.Extensions.Configuration.Json": "9.0.1",
@@ -22,16 +22,16 @@
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"System.CommandLine": "2.0.0-beta4.22272.1", "System.CommandLine": "2.0.0-beta4.22272.1",
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1", "System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
"Wabbajack.CLI.Builder": "0.4.6", "Wabbajack.CLI.Builder": "0.4.7",
"Wabbajack.Downloaders.Bethesda": "0.4.6", "Wabbajack.Downloaders.Bethesda": "0.4.7",
"Wabbajack.Downloaders.Dispatcher": "0.4.6", "Wabbajack.Downloaders.Dispatcher": "0.4.7",
"Wabbajack.Hashing.xxHash64": "0.4.6", "Wabbajack.Hashing.xxHash64": "0.4.7",
"Wabbajack.Networking.Discord": "0.4.6", "Wabbajack.Networking.Discord": "0.4.7",
"Wabbajack.Networking.GitHub": "0.4.6", "Wabbajack.Networking.GitHub": "0.4.7",
"Wabbajack.Paths.IO": "0.4.6", "Wabbajack.Paths.IO": "0.4.7",
"Wabbajack.Server.Lib": "0.4.6", "Wabbajack.Server.Lib": "0.4.7",
"Wabbajack.Services.OSIntegrated": "0.4.6", "Wabbajack.Services.OSIntegrated": "0.4.7",
"Wabbajack.VFS": "0.4.6", "Wabbajack.VFS": "0.4.7",
"MegaApiClient": "1.0.0.0", "MegaApiClient": "1.0.0.0",
"runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.22" "runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.22"
}, },
@@ -1781,7 +1781,7 @@
} }
} }
}, },
"Wabbajack.CLI.Builder/0.4.6": { "Wabbajack.CLI.Builder/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.Json": "9.0.1", "Microsoft.Extensions.Configuration.Json": "9.0.1",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
@@ -1791,109 +1791,109 @@
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"System.CommandLine": "2.0.0-beta4.22272.1", "System.CommandLine": "2.0.0-beta4.22272.1",
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1", "System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
"Wabbajack.Paths": "0.4.6" "Wabbajack.Paths": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.CLI.Builder.dll": {} "Wabbajack.CLI.Builder.dll": {}
} }
}, },
"Wabbajack.Common/0.4.6": { "Wabbajack.Common/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"System.Reactive": "6.0.1", "System.Reactive": "6.0.1",
"Wabbajack.DTOs": "0.4.6", "Wabbajack.DTOs": "0.4.7",
"Wabbajack.Networking.Http": "0.4.6", "Wabbajack.Networking.Http": "0.4.7",
"Wabbajack.Paths.IO": "0.4.6" "Wabbajack.Paths.IO": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Common.dll": {} "Wabbajack.Common.dll": {}
} }
}, },
"Wabbajack.Compiler/0.4.6": { "Wabbajack.Compiler/0.4.7": {
"dependencies": { "dependencies": {
"F23.StringSimilarity": "6.0.0", "F23.StringSimilarity": "6.0.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Downloaders.Dispatcher": "0.4.6", "Wabbajack.Downloaders.Dispatcher": "0.4.7",
"Wabbajack.Installer": "0.4.6", "Wabbajack.Installer": "0.4.7",
"Wabbajack.VFS": "0.4.6", "Wabbajack.VFS": "0.4.7",
"ini-parser-netstandard": "2.5.2" "ini-parser-netstandard": "2.5.2"
}, },
"runtime": { "runtime": {
"Wabbajack.Compiler.dll": {} "Wabbajack.Compiler.dll": {}
} }
}, },
"Wabbajack.Compression.BSA/0.4.6": { "Wabbajack.Compression.BSA/0.4.7": {
"dependencies": { "dependencies": {
"K4os.Compression.LZ4.Streams": "1.3.8", "K4os.Compression.LZ4.Streams": "1.3.8",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"SharpZipLib": "1.4.2", "SharpZipLib": "1.4.2",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.DTOs": "0.4.6" "Wabbajack.DTOs": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Compression.BSA.dll": {} "Wabbajack.Compression.BSA.dll": {}
} }
}, },
"Wabbajack.Compression.Zip/0.4.6": { "Wabbajack.Compression.Zip/0.4.7": {
"dependencies": { "dependencies": {
"Wabbajack.IO.Async": "0.4.6" "Wabbajack.IO.Async": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Compression.Zip.dll": {} "Wabbajack.Compression.Zip.dll": {}
} }
}, },
"Wabbajack.Configuration/0.4.6": { "Wabbajack.Configuration/0.4.7": {
"runtime": { "runtime": {
"Wabbajack.Configuration.dll": {} "Wabbajack.Configuration.dll": {}
} }
}, },
"Wabbajack.Downloaders.Bethesda/0.4.6": { "Wabbajack.Downloaders.Bethesda/0.4.7": {
"dependencies": { "dependencies": {
"LibAES-CTR": "1.1.0", "LibAES-CTR": "1.1.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"SharpZipLib": "1.4.2", "SharpZipLib": "1.4.2",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.Downloaders.Interfaces": "0.4.6", "Wabbajack.Downloaders.Interfaces": "0.4.7",
"Wabbajack.Networking.BethesdaNet": "0.4.6" "Wabbajack.Networking.BethesdaNet": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.Bethesda.dll": {} "Wabbajack.Downloaders.Bethesda.dll": {}
} }
}, },
"Wabbajack.Downloaders.Dispatcher/0.4.6": { "Wabbajack.Downloaders.Dispatcher/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Downloaders.Bethesda": "0.4.6", "Wabbajack.Downloaders.Bethesda": "0.4.7",
"Wabbajack.Downloaders.GameFile": "0.4.6", "Wabbajack.Downloaders.GameFile": "0.4.7",
"Wabbajack.Downloaders.GoogleDrive": "0.4.6", "Wabbajack.Downloaders.GoogleDrive": "0.4.7",
"Wabbajack.Downloaders.Http": "0.4.6", "Wabbajack.Downloaders.Http": "0.4.7",
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.4.6", "Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.4.7",
"Wabbajack.Downloaders.Interfaces": "0.4.6", "Wabbajack.Downloaders.Interfaces": "0.4.7",
"Wabbajack.Downloaders.Manual": "0.4.6", "Wabbajack.Downloaders.Manual": "0.4.7",
"Wabbajack.Downloaders.MediaFire": "0.4.6", "Wabbajack.Downloaders.MediaFire": "0.4.7",
"Wabbajack.Downloaders.Mega": "0.4.6", "Wabbajack.Downloaders.Mega": "0.4.7",
"Wabbajack.Downloaders.ModDB": "0.4.6", "Wabbajack.Downloaders.ModDB": "0.4.7",
"Wabbajack.Downloaders.Nexus": "0.4.6", "Wabbajack.Downloaders.Nexus": "0.4.7",
"Wabbajack.Downloaders.VerificationCache": "0.4.6", "Wabbajack.Downloaders.VerificationCache": "0.4.7",
"Wabbajack.Downloaders.WabbajackCDN": "0.4.6", "Wabbajack.Downloaders.WabbajackCDN": "0.4.7",
"Wabbajack.Networking.WabbajackClientApi": "0.4.6" "Wabbajack.Networking.WabbajackClientApi": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.Dispatcher.dll": {} "Wabbajack.Downloaders.Dispatcher.dll": {}
} }
}, },
"Wabbajack.Downloaders.GameFile/0.4.6": { "Wabbajack.Downloaders.GameFile/0.4.7": {
"dependencies": { "dependencies": {
"GameFinder.StoreHandlers.EADesktop": "4.5.0", "GameFinder.StoreHandlers.EADesktop": "4.5.0",
"GameFinder.StoreHandlers.EGS": "4.5.0", "GameFinder.StoreHandlers.EGS": "4.5.0",
@@ -1903,360 +1903,360 @@
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Downloaders.Interfaces": "0.4.6", "Wabbajack.Downloaders.Interfaces": "0.4.7",
"Wabbajack.VFS": "0.4.6" "Wabbajack.VFS": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.GameFile.dll": {} "Wabbajack.Downloaders.GameFile.dll": {}
} }
}, },
"Wabbajack.Downloaders.GoogleDrive/0.4.6": { "Wabbajack.Downloaders.GoogleDrive/0.4.7": {
"dependencies": { "dependencies": {
"HtmlAgilityPack": "1.11.72", "HtmlAgilityPack": "1.11.72",
"Microsoft.AspNetCore.Http.Extensions": "2.3.0", "Microsoft.AspNetCore.Http.Extensions": "2.3.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.DTOs": "0.4.6", "Wabbajack.DTOs": "0.4.7",
"Wabbajack.Downloaders.Interfaces": "0.4.6", "Wabbajack.Downloaders.Interfaces": "0.4.7",
"Wabbajack.Networking.Http": "0.4.6", "Wabbajack.Networking.Http": "0.4.7",
"Wabbajack.Networking.Http.Interfaces": "0.4.6" "Wabbajack.Networking.Http.Interfaces": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.GoogleDrive.dll": {} "Wabbajack.Downloaders.GoogleDrive.dll": {}
} }
}, },
"Wabbajack.Downloaders.Http/0.4.6": { "Wabbajack.Downloaders.Http/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.DTOs": "0.4.6", "Wabbajack.DTOs": "0.4.7",
"Wabbajack.Downloaders.Interfaces": "0.4.6", "Wabbajack.Downloaders.Interfaces": "0.4.7",
"Wabbajack.Networking.BethesdaNet": "0.4.6", "Wabbajack.Networking.BethesdaNet": "0.4.7",
"Wabbajack.Networking.Http.Interfaces": "0.4.6", "Wabbajack.Networking.Http.Interfaces": "0.4.7",
"Wabbajack.Paths.IO": "0.4.6" "Wabbajack.Paths.IO": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.Http.dll": {} "Wabbajack.Downloaders.Http.dll": {}
} }
}, },
"Wabbajack.Downloaders.Interfaces/0.4.6": { "Wabbajack.Downloaders.Interfaces/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.Compression.Zip": "0.4.6", "Wabbajack.Compression.Zip": "0.4.7",
"Wabbajack.DTOs": "0.4.6", "Wabbajack.DTOs": "0.4.7",
"Wabbajack.Paths.IO": "0.4.6" "Wabbajack.Paths.IO": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.Interfaces.dll": {} "Wabbajack.Downloaders.Interfaces.dll": {}
} }
}, },
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.6": { "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.7": {
"dependencies": { "dependencies": {
"F23.StringSimilarity": "6.0.0", "F23.StringSimilarity": "6.0.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.Downloaders.Interfaces": "0.4.6", "Wabbajack.Downloaders.Interfaces": "0.4.7",
"Wabbajack.Networking.Http": "0.4.6", "Wabbajack.Networking.Http": "0.4.7",
"Wabbajack.Networking.Http.Interfaces": "0.4.6" "Wabbajack.Networking.Http.Interfaces": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {} "Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {}
} }
}, },
"Wabbajack.Downloaders.Manual/0.4.6": { "Wabbajack.Downloaders.Manual/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.Downloaders.Interfaces": "0.4.6" "Wabbajack.Downloaders.Interfaces": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.Manual.dll": {} "Wabbajack.Downloaders.Manual.dll": {}
} }
}, },
"Wabbajack.Downloaders.MediaFire/0.4.6": { "Wabbajack.Downloaders.MediaFire/0.4.7": {
"dependencies": { "dependencies": {
"HtmlAgilityPack": "1.11.72", "HtmlAgilityPack": "1.11.72",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.Downloaders.Interfaces": "0.4.6", "Wabbajack.Downloaders.Interfaces": "0.4.7",
"Wabbajack.Networking.Http.Interfaces": "0.4.6" "Wabbajack.Networking.Http.Interfaces": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.MediaFire.dll": {} "Wabbajack.Downloaders.MediaFire.dll": {}
} }
}, },
"Wabbajack.Downloaders.Mega/0.4.6": { "Wabbajack.Downloaders.Mega/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.Downloaders.Interfaces": "0.4.6", "Wabbajack.Downloaders.Interfaces": "0.4.7",
"Wabbajack.Paths.IO": "0.4.6" "Wabbajack.Paths.IO": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.Mega.dll": {} "Wabbajack.Downloaders.Mega.dll": {}
} }
}, },
"Wabbajack.Downloaders.ModDB/0.4.6": { "Wabbajack.Downloaders.ModDB/0.4.7": {
"dependencies": { "dependencies": {
"HtmlAgilityPack": "1.11.72", "HtmlAgilityPack": "1.11.72",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.Downloaders.Interfaces": "0.4.6", "Wabbajack.Downloaders.Interfaces": "0.4.7",
"Wabbajack.Networking.Http": "0.4.6", "Wabbajack.Networking.Http": "0.4.7",
"Wabbajack.Networking.Http.Interfaces": "0.4.6" "Wabbajack.Networking.Http.Interfaces": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.ModDB.dll": {} "Wabbajack.Downloaders.ModDB.dll": {}
} }
}, },
"Wabbajack.Downloaders.Nexus/0.4.6": { "Wabbajack.Downloaders.Nexus/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.4.6", "Wabbajack.DTOs": "0.4.7",
"Wabbajack.Downloaders.Interfaces": "0.4.6", "Wabbajack.Downloaders.Interfaces": "0.4.7",
"Wabbajack.Hashing.xxHash64": "0.4.6", "Wabbajack.Hashing.xxHash64": "0.4.7",
"Wabbajack.Networking.Http": "0.4.6", "Wabbajack.Networking.Http": "0.4.7",
"Wabbajack.Networking.Http.Interfaces": "0.4.6", "Wabbajack.Networking.Http.Interfaces": "0.4.7",
"Wabbajack.Networking.NexusApi": "0.4.6", "Wabbajack.Networking.NexusApi": "0.4.7",
"Wabbajack.Paths": "0.4.6" "Wabbajack.Paths": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.Nexus.dll": {} "Wabbajack.Downloaders.Nexus.dll": {}
} }
}, },
"Wabbajack.Downloaders.VerificationCache/0.4.6": { "Wabbajack.Downloaders.VerificationCache/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Stub.System.Data.SQLite.Core.NetStandard": "1.0.119", "Stub.System.Data.SQLite.Core.NetStandard": "1.0.119",
"Wabbajack.DTOs": "0.4.6", "Wabbajack.DTOs": "0.4.7",
"Wabbajack.Paths.IO": "0.4.6" "Wabbajack.Paths.IO": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.VerificationCache.dll": {} "Wabbajack.Downloaders.VerificationCache.dll": {}
} }
}, },
"Wabbajack.Downloaders.WabbajackCDN/0.4.6": { "Wabbajack.Downloaders.WabbajackCDN/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Microsoft.Toolkit.HighPerformance": "7.1.2", "Microsoft.Toolkit.HighPerformance": "7.1.2",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.Downloaders.Interfaces": "0.4.6", "Wabbajack.Downloaders.Interfaces": "0.4.7",
"Wabbajack.Networking.Http": "0.4.6", "Wabbajack.Networking.Http": "0.4.7",
"Wabbajack.RateLimiter": "0.4.6" "Wabbajack.RateLimiter": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.WabbajackCDN.dll": {} "Wabbajack.Downloaders.WabbajackCDN.dll": {}
} }
}, },
"Wabbajack.DTOs/0.4.6": { "Wabbajack.DTOs/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.Hashing.xxHash64": "0.4.6", "Wabbajack.Hashing.xxHash64": "0.4.7",
"Wabbajack.Paths": "0.4.6" "Wabbajack.Paths": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.DTOs.dll": {} "Wabbajack.DTOs.dll": {}
} }
}, },
"Wabbajack.FileExtractor/0.4.6": { "Wabbajack.FileExtractor/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"OMODFramework": "3.0.1", "OMODFramework": "3.0.1",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.Compression.BSA": "0.4.6", "Wabbajack.Compression.BSA": "0.4.7",
"Wabbajack.Hashing.PHash": "0.4.6", "Wabbajack.Hashing.PHash": "0.4.7",
"Wabbajack.Paths": "0.4.6" "Wabbajack.Paths": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.FileExtractor.dll": {} "Wabbajack.FileExtractor.dll": {}
} }
}, },
"Wabbajack.Hashing.PHash/0.4.6": { "Wabbajack.Hashing.PHash/0.4.7": {
"dependencies": { "dependencies": {
"BCnEncoder.Net.ImageSharp": "1.1.1", "BCnEncoder.Net.ImageSharp": "1.1.1",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Shipwreck.Phash": "0.5.0", "Shipwreck.Phash": "0.5.0",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.DTOs": "0.4.6", "Wabbajack.DTOs": "0.4.7",
"Wabbajack.Paths": "0.4.6", "Wabbajack.Paths": "0.4.7",
"Wabbajack.Paths.IO": "0.4.6" "Wabbajack.Paths.IO": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Hashing.PHash.dll": {} "Wabbajack.Hashing.PHash.dll": {}
} }
}, },
"Wabbajack.Hashing.xxHash64/0.4.6": { "Wabbajack.Hashing.xxHash64/0.4.7": {
"dependencies": { "dependencies": {
"Wabbajack.Paths": "0.4.6", "Wabbajack.Paths": "0.4.7",
"Wabbajack.RateLimiter": "0.4.6" "Wabbajack.RateLimiter": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Hashing.xxHash64.dll": {} "Wabbajack.Hashing.xxHash64.dll": {}
} }
}, },
"Wabbajack.Installer/0.4.6": { "Wabbajack.Installer/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"Octopus.Octodiff": "2.0.548", "Octopus.Octodiff": "2.0.548",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"Wabbajack.DTOs": "0.4.6", "Wabbajack.DTOs": "0.4.7",
"Wabbajack.Downloaders.Dispatcher": "0.4.6", "Wabbajack.Downloaders.Dispatcher": "0.4.7",
"Wabbajack.Downloaders.GameFile": "0.4.6", "Wabbajack.Downloaders.GameFile": "0.4.7",
"Wabbajack.FileExtractor": "0.4.6", "Wabbajack.FileExtractor": "0.4.7",
"Wabbajack.Networking.WabbajackClientApi": "0.4.6", "Wabbajack.Networking.WabbajackClientApi": "0.4.7",
"Wabbajack.Paths": "0.4.6", "Wabbajack.Paths": "0.4.7",
"Wabbajack.Paths.IO": "0.4.6", "Wabbajack.Paths.IO": "0.4.7",
"Wabbajack.VFS": "0.4.6", "Wabbajack.VFS": "0.4.7",
"ini-parser-netstandard": "2.5.2" "ini-parser-netstandard": "2.5.2"
}, },
"runtime": { "runtime": {
"Wabbajack.Installer.dll": {} "Wabbajack.Installer.dll": {}
} }
}, },
"Wabbajack.IO.Async/0.4.6": { "Wabbajack.IO.Async/0.4.7": {
"runtime": { "runtime": {
"Wabbajack.IO.Async.dll": {} "Wabbajack.IO.Async.dll": {}
} }
}, },
"Wabbajack.Networking.BethesdaNet/0.4.6": { "Wabbajack.Networking.BethesdaNet/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.4.6", "Wabbajack.DTOs": "0.4.7",
"Wabbajack.Networking.Http": "0.4.6", "Wabbajack.Networking.Http": "0.4.7",
"Wabbajack.Networking.Http.Interfaces": "0.4.6" "Wabbajack.Networking.Http.Interfaces": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Networking.BethesdaNet.dll": {} "Wabbajack.Networking.BethesdaNet.dll": {}
} }
}, },
"Wabbajack.Networking.Discord/0.4.6": { "Wabbajack.Networking.Discord/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Networking.Http.Interfaces": "0.4.6" "Wabbajack.Networking.Http.Interfaces": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Networking.Discord.dll": {} "Wabbajack.Networking.Discord.dll": {}
} }
}, },
"Wabbajack.Networking.GitHub/0.4.6": { "Wabbajack.Networking.GitHub/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Octokit": "14.0.0", "Octokit": "14.0.0",
"Wabbajack.DTOs": "0.4.6", "Wabbajack.DTOs": "0.4.7",
"Wabbajack.Networking.Http.Interfaces": "0.4.6" "Wabbajack.Networking.Http.Interfaces": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Networking.GitHub.dll": {} "Wabbajack.Networking.GitHub.dll": {}
} }
}, },
"Wabbajack.Networking.Http/0.4.6": { "Wabbajack.Networking.Http/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Http": "9.0.1", "Microsoft.Extensions.Http": "9.0.1",
"Microsoft.Extensions.Logging": "9.0.1", "Microsoft.Extensions.Logging": "9.0.1",
"Wabbajack.Configuration": "0.4.6", "Wabbajack.Configuration": "0.4.7",
"Wabbajack.Downloaders.Interfaces": "0.4.6", "Wabbajack.Downloaders.Interfaces": "0.4.7",
"Wabbajack.Hashing.xxHash64": "0.4.6", "Wabbajack.Hashing.xxHash64": "0.4.7",
"Wabbajack.Networking.Http.Interfaces": "0.4.6", "Wabbajack.Networking.Http.Interfaces": "0.4.7",
"Wabbajack.Paths": "0.4.6", "Wabbajack.Paths": "0.4.7",
"Wabbajack.Paths.IO": "0.4.6" "Wabbajack.Paths.IO": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Networking.Http.dll": {} "Wabbajack.Networking.Http.dll": {}
} }
}, },
"Wabbajack.Networking.Http.Interfaces/0.4.6": { "Wabbajack.Networking.Http.Interfaces/0.4.7": {
"dependencies": { "dependencies": {
"Wabbajack.Hashing.xxHash64": "0.4.6" "Wabbajack.Hashing.xxHash64": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Networking.Http.Interfaces.dll": {} "Wabbajack.Networking.Http.Interfaces.dll": {}
} }
}, },
"Wabbajack.Networking.NexusApi/0.4.6": { "Wabbajack.Networking.NexusApi/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.4.6", "Wabbajack.DTOs": "0.4.7",
"Wabbajack.Networking.Http": "0.4.6", "Wabbajack.Networking.Http": "0.4.7",
"Wabbajack.Networking.Http.Interfaces": "0.4.6", "Wabbajack.Networking.Http.Interfaces": "0.4.7",
"Wabbajack.Networking.WabbajackClientApi": "0.4.6" "Wabbajack.Networking.WabbajackClientApi": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Networking.NexusApi.dll": {} "Wabbajack.Networking.NexusApi.dll": {}
} }
}, },
"Wabbajack.Networking.WabbajackClientApi/0.4.6": { "Wabbajack.Networking.WabbajackClientApi/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Octokit": "14.0.0", "Octokit": "14.0.0",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.DTOs": "0.4.6", "Wabbajack.DTOs": "0.4.7",
"Wabbajack.Paths.IO": "0.4.6", "Wabbajack.Paths.IO": "0.4.7",
"Wabbajack.VFS.Interfaces": "0.4.6", "Wabbajack.VFS.Interfaces": "0.4.7",
"YamlDotNet": "16.3.0" "YamlDotNet": "16.3.0"
}, },
"runtime": { "runtime": {
"Wabbajack.Networking.WabbajackClientApi.dll": {} "Wabbajack.Networking.WabbajackClientApi.dll": {}
} }
}, },
"Wabbajack.Paths/0.4.6": { "Wabbajack.Paths/0.4.7": {
"runtime": { "runtime": {
"Wabbajack.Paths.dll": {} "Wabbajack.Paths.dll": {}
} }
}, },
"Wabbajack.Paths.IO/0.4.6": { "Wabbajack.Paths.IO/0.4.7": {
"dependencies": { "dependencies": {
"Wabbajack.Paths": "0.4.6", "Wabbajack.Paths": "0.4.7",
"shortid": "4.0.0" "shortid": "4.0.0"
}, },
"runtime": { "runtime": {
"Wabbajack.Paths.IO.dll": {} "Wabbajack.Paths.IO.dll": {}
} }
}, },
"Wabbajack.RateLimiter/0.4.6": { "Wabbajack.RateLimiter/0.4.7": {
"runtime": { "runtime": {
"Wabbajack.RateLimiter.dll": {} "Wabbajack.RateLimiter.dll": {}
} }
}, },
"Wabbajack.Server.Lib/0.4.6": { "Wabbajack.Server.Lib/0.4.7": {
"dependencies": { "dependencies": {
"FluentFTP": "52.0.0", "FluentFTP": "52.0.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
@@ -2264,58 +2264,58 @@
"Nettle": "3.0.0", "Nettle": "3.0.0",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.Networking.Http.Interfaces": "0.4.6", "Wabbajack.Networking.Http.Interfaces": "0.4.7",
"Wabbajack.Services.OSIntegrated": "0.4.6" "Wabbajack.Services.OSIntegrated": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Server.Lib.dll": {} "Wabbajack.Server.Lib.dll": {}
} }
}, },
"Wabbajack.Services.OSIntegrated/0.4.6": { "Wabbajack.Services.OSIntegrated/0.4.7": {
"dependencies": { "dependencies": {
"DeviceId": "6.8.0", "DeviceId": "6.8.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Compiler": "0.4.6", "Wabbajack.Compiler": "0.4.7",
"Wabbajack.Downloaders.Dispatcher": "0.4.6", "Wabbajack.Downloaders.Dispatcher": "0.4.7",
"Wabbajack.Installer": "0.4.6", "Wabbajack.Installer": "0.4.7",
"Wabbajack.Networking.BethesdaNet": "0.4.6", "Wabbajack.Networking.BethesdaNet": "0.4.7",
"Wabbajack.Networking.Discord": "0.4.6", "Wabbajack.Networking.Discord": "0.4.7",
"Wabbajack.VFS": "0.4.6" "Wabbajack.VFS": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.Services.OSIntegrated.dll": {} "Wabbajack.Services.OSIntegrated.dll": {}
} }
}, },
"Wabbajack.VFS/0.4.6": { "Wabbajack.VFS/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"System.Data.SQLite.Core": "1.0.119", "System.Data.SQLite.Core": "1.0.119",
"Wabbajack.Common": "0.4.6", "Wabbajack.Common": "0.4.7",
"Wabbajack.FileExtractor": "0.4.6", "Wabbajack.FileExtractor": "0.4.7",
"Wabbajack.Hashing.PHash": "0.4.6", "Wabbajack.Hashing.PHash": "0.4.7",
"Wabbajack.Hashing.xxHash64": "0.4.6", "Wabbajack.Hashing.xxHash64": "0.4.7",
"Wabbajack.Paths": "0.4.6", "Wabbajack.Paths": "0.4.7",
"Wabbajack.Paths.IO": "0.4.6", "Wabbajack.Paths.IO": "0.4.7",
"Wabbajack.VFS.Interfaces": "0.4.6" "Wabbajack.VFS.Interfaces": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.VFS.dll": {} "Wabbajack.VFS.dll": {}
} }
}, },
"Wabbajack.VFS.Interfaces/0.4.6": { "Wabbajack.VFS.Interfaces/0.4.7": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.4.6", "Wabbajack.DTOs": "0.4.7",
"Wabbajack.Hashing.xxHash64": "0.4.6", "Wabbajack.Hashing.xxHash64": "0.4.7",
"Wabbajack.Paths": "0.4.6" "Wabbajack.Paths": "0.4.7"
}, },
"runtime": { "runtime": {
"Wabbajack.VFS.Interfaces.dll": {} "Wabbajack.VFS.Interfaces.dll": {}
@@ -2332,7 +2332,7 @@
} }
}, },
"libraries": { "libraries": {
"jackify-engine/0.4.6": { "jackify-engine/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
@@ -3021,202 +3021,202 @@
"path": "yamldotnet/16.3.0", "path": "yamldotnet/16.3.0",
"hashPath": "yamldotnet.16.3.0.nupkg.sha512" "hashPath": "yamldotnet.16.3.0.nupkg.sha512"
}, },
"Wabbajack.CLI.Builder/0.4.6": { "Wabbajack.CLI.Builder/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Common/0.4.6": { "Wabbajack.Common/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Compiler/0.4.6": { "Wabbajack.Compiler/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Compression.BSA/0.4.6": { "Wabbajack.Compression.BSA/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Compression.Zip/0.4.6": { "Wabbajack.Compression.Zip/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Configuration/0.4.6": { "Wabbajack.Configuration/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.Bethesda/0.4.6": { "Wabbajack.Downloaders.Bethesda/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.Dispatcher/0.4.6": { "Wabbajack.Downloaders.Dispatcher/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.GameFile/0.4.6": { "Wabbajack.Downloaders.GameFile/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.GoogleDrive/0.4.6": { "Wabbajack.Downloaders.GoogleDrive/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.Http/0.4.6": { "Wabbajack.Downloaders.Http/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.Interfaces/0.4.6": { "Wabbajack.Downloaders.Interfaces/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.6": { "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.Manual/0.4.6": { "Wabbajack.Downloaders.Manual/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.MediaFire/0.4.6": { "Wabbajack.Downloaders.MediaFire/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.Mega/0.4.6": { "Wabbajack.Downloaders.Mega/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.ModDB/0.4.6": { "Wabbajack.Downloaders.ModDB/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.Nexus/0.4.6": { "Wabbajack.Downloaders.Nexus/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.VerificationCache/0.4.6": { "Wabbajack.Downloaders.VerificationCache/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.WabbajackCDN/0.4.6": { "Wabbajack.Downloaders.WabbajackCDN/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.DTOs/0.4.6": { "Wabbajack.DTOs/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.FileExtractor/0.4.6": { "Wabbajack.FileExtractor/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Hashing.PHash/0.4.6": { "Wabbajack.Hashing.PHash/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Hashing.xxHash64/0.4.6": { "Wabbajack.Hashing.xxHash64/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Installer/0.4.6": { "Wabbajack.Installer/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.IO.Async/0.4.6": { "Wabbajack.IO.Async/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Networking.BethesdaNet/0.4.6": { "Wabbajack.Networking.BethesdaNet/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Networking.Discord/0.4.6": { "Wabbajack.Networking.Discord/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Networking.GitHub/0.4.6": { "Wabbajack.Networking.GitHub/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Networking.Http/0.4.6": { "Wabbajack.Networking.Http/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Networking.Http.Interfaces/0.4.6": { "Wabbajack.Networking.Http.Interfaces/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Networking.NexusApi/0.4.6": { "Wabbajack.Networking.NexusApi/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Networking.WabbajackClientApi/0.4.6": { "Wabbajack.Networking.WabbajackClientApi/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Paths/0.4.6": { "Wabbajack.Paths/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Paths.IO/0.4.6": { "Wabbajack.Paths.IO/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.RateLimiter/0.4.6": { "Wabbajack.RateLimiter/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Server.Lib/0.4.6": { "Wabbajack.Server.Lib/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Services.OSIntegrated/0.4.6": { "Wabbajack.Services.OSIntegrated/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.VFS/0.4.6": { "Wabbajack.VFS/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.VFS.Interfaces/0.4.6": { "Wabbajack.VFS.Interfaces/0.4.7": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""

Binary file not shown.

View File

@@ -40,6 +40,7 @@ class SuccessDialog(QDialog):
self.setWindowTitle("Success!") self.setWindowTitle("Success!")
self.setWindowModality(Qt.NonModal) self.setWindowModality(Qt.NonModal)
self.setAttribute(Qt.WA_ShowWithoutActivating, True) self.setAttribute(Qt.WA_ShowWithoutActivating, True)
self.setAttribute(Qt.WA_DeleteOnClose, True)
self.setFixedSize(500, 500) self.setFixedSize(500, 500)
self.setWindowFlag(Qt.WindowDoesNotAcceptFocus, True) self.setWindowFlag(Qt.WindowDoesNotAcceptFocus, True)
self.setStyleSheet("QDialog { background: #181818; color: #fff; border-radius: 12px; }" ) self.setStyleSheet("QDialog { background: #181818; color: #fff; border-radius: 12px; }" )
@@ -184,7 +185,7 @@ class SuccessDialog(QDialog):
self._update_countdown() self._update_countdown()
self._timer.start(1000) self._timer.start(1000)
self.return_btn.clicked.connect(self.accept) self.return_btn.clicked.connect(self.accept)
self.exit_btn.clicked.connect(QApplication.quit) self.exit_btn.clicked.connect(self._safe_exit)
# Set the Wabbajack icon if available # Set the Wabbajack icon if available
self._set_dialog_icon() self._set_dialog_icon()
@@ -256,4 +257,15 @@ class SuccessDialog(QDialog):
self.return_btn.setText(self._orig_return_text) self.return_btn.setText(self._orig_return_text)
self.return_btn.setEnabled(True) self.return_btn.setEnabled(True)
self.exit_btn.setEnabled(True) self.exit_btn.setEnabled(True)
self._timer.stop() self._timer.stop()
def _safe_exit(self):
"""Safely exit the application with proper cleanup"""
try:
if self._timer.isActive():
self._timer.stop()
self.close()
QApplication.quit()
except Exception as e:
logger.error(f"Error during safe exit: {e}")
QApplication.quit()

View File

@@ -0,0 +1,112 @@
"""
VNV Automation Confirmation Dialog
Custom dialog for VNV automation confirmation with optional BSA decompression checkbox.
"""
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QCheckBox, QFrame, QTextEdit, QScrollArea
)
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
class VNVAutomationDialog(QDialog):
"""Dialog for confirming VNV automation with optional BSA decompression."""
def __init__(self, parent=None, description: str = ""):
super().__init__(parent)
self.setWindowTitle("VNV Post-Install Automation")
self.setModal(True)
self.setFixedSize(600, 450)
self.setStyleSheet("QDialog { background: #181818; color: #fff; }")
# Result: (confirmed: bool, include_bsa: bool)
self.result_data = (False, True)
self.setup_ui(description)
def setup_ui(self, description: str):
"""Set up the dialog UI."""
main_layout = QVBoxLayout(self)
main_layout.setSpacing(0)
main_layout.setContentsMargins(20, 20, 20, 20)
# Card background for content
card = QFrame(self)
card.setObjectName("vnvCard")
card.setFrameShape(QFrame.StyledPanel)
card.setFrameShadow(QFrame.Raised)
card.setStyleSheet(
"QFrame#vnvCard { "
" background: #2d2d2d; "
" border-radius: 12px; "
" border: 1px solid #555; "
"}"
)
card_layout = QVBoxLayout(card)
card_layout.setSpacing(16)
card_layout.setContentsMargins(28, 28, 28, 28)
# Description text - use QTextEdit for scrollable long text
description_text = QTextEdit()
description_text.setPlainText(description)
description_text.setReadOnly(True)
description_text.setMaximumHeight(200)
description_text.setStyleSheet(
"QTextEdit { "
" background: #1a1a1a; "
" color: #fff; "
" border: 1px solid #555; "
" border-radius: 4px; "
" padding: 8px; "
"}"
)
card_layout.addWidget(description_text)
# BSA Decompression checkbox
self.bsa_checkbox = QCheckBox("Include BSA Decompression")
self.bsa_checkbox.setChecked(True) # Default to checked
self.bsa_checkbox.setStyleSheet("color: #fff; padding: 5px;")
card_layout.addWidget(self.bsa_checkbox)
card_layout.addStretch()
# Buttons
button_layout = QHBoxLayout()
button_layout.addStretch()
self.yes_button = QPushButton("Yes")
self.yes_button.setDefault(True)
self.yes_button.setMinimumWidth(100)
self.yes_button.clicked.connect(self.accept_dialog)
button_layout.addWidget(self.yes_button)
self.no_button = QPushButton("No")
self.no_button.setMinimumWidth(100)
self.no_button.clicked.connect(self.reject_dialog)
button_layout.addWidget(self.no_button)
card_layout.addLayout(button_layout)
main_layout.addWidget(card)
def accept_dialog(self):
"""Handle Yes button click."""
self.result_data = (True, self.bsa_checkbox.isChecked())
self.accept()
def reject_dialog(self):
"""Handle No button click."""
self.result_data = (False, False)
self.reject()
def get_result(self) -> tuple[bool, bool]:
"""
Get the dialog result.
Returns:
Tuple of (confirmed: bool, include_bsa_decompression: bool)
"""
return self.result_data

View File

@@ -550,29 +550,29 @@ class SettingsDialog(QDialog):
self.component_method_group = QButtonGroup() self.component_method_group = QButtonGroup()
component_method_layout = QVBoxLayout() component_method_layout = QVBoxLayout()
# Get current setting # Get current setting (default to winetricks)
current_method = self.config_handler.get('component_installation_method', 'system_protontricks') current_method = self.config_handler.get('component_installation_method', 'winetricks')
# Migrate old bundled_protontricks users to system_protontricks # Migrate old bundled_protontricks users to system_protontricks
if current_method == 'bundled_protontricks': if current_method == 'bundled_protontricks':
current_method = 'system_protontricks' current_method = 'system_protontricks'
# Protontricks (default) # Winetricks (default)
self.protontricks_radio = QRadioButton("Protontricks (Default)") self.winetricks_radio = QRadioButton("Winetricks (Default)")
self.protontricks_radio.setChecked(current_method == 'system_protontricks')
self.protontricks_radio.setToolTip(
"Use system-installed protontricks (flatpak or native). Required for component installation."
)
self.component_method_group.addButton(self.protontricks_radio, 0)
component_method_layout.addWidget(self.protontricks_radio)
# Winetricks (alternative)
self.winetricks_radio = QRadioButton("Winetricks (Alternative)")
self.winetricks_radio.setChecked(current_method == 'winetricks') self.winetricks_radio.setChecked(current_method == 'winetricks')
self.winetricks_radio.setToolTip( self.winetricks_radio.setToolTip(
"Use bundled winetricks instead. May work when protontricks unavailable." "Use bundled winetricks for component installation. Faster and more reliable."
) )
self.component_method_group.addButton(self.winetricks_radio, 1) self.component_method_group.addButton(self.winetricks_radio, 0)
component_method_layout.addWidget(self.winetricks_radio) component_method_layout.addWidget(self.winetricks_radio)
# Protontricks (alternative)
self.protontricks_radio = QRadioButton("Protontricks (Alternative)")
self.protontricks_radio.setChecked(current_method == 'system_protontricks')
self.protontricks_radio.setToolTip(
"Use system-installed protontricks (flatpak or native). Fallback option if winetricks fails."
)
self.component_method_group.addButton(self.protontricks_radio, 1)
component_method_layout.addWidget(self.protontricks_radio)
component_layout.addLayout(component_method_layout) component_layout.addLayout(component_method_layout)
@@ -746,7 +746,8 @@ class SettingsDialog(QDialog):
else: else:
self.install_proton_dropdown.addItem("No Proton Versions Detected", "none") self.install_proton_dropdown.addItem("No Proton Versions Detected", "none")
# Filter for fast Proton versions only # Filter to only known-compatible Protons for component installation
# Third-party builds (CachyOS, etc.) may have compatibility issues with Windows installers
fast_protons = [] fast_protons = []
slow_protons = [] slow_protons = []
@@ -754,30 +755,39 @@ class SettingsDialog(QDialog):
proton_name = proton.get('name', 'Unknown Proton') proton_name = proton.get('name', 'Unknown Proton')
proton_type = proton.get('type', 'Unknown') proton_type = proton.get('type', 'Unknown')
is_fast_proton = False # Only include known-compatible Proton types for Install Proton
# Exclude third-party builds that may have component installation issues
if proton_type not in ('GE-Proton', 'Valve-Proton'):
# Skip third-party Protons (CachyOS, etc.) - they may not work reliably for component installation
logger.debug(f"Skipping {proton_name} ({proton_type}) from Install Proton dropdown - third-party builds may have compatibility issues")
continue
# Fast Protons: Experimental, GE-Proton 10+ # Determine if this Proton is explicitly slow for texture processing
if proton_name == "Proton - Experimental": slow_warning = False
is_fast_proton = True
elif proton_type == 'GE-Proton':
# For GE-Proton, check major_version field
major_version = proton.get('major_version', 0)
if major_version >= 10:
is_fast_proton = True
if is_fast_proton: if proton_type == 'GE-Proton':
if proton_type == 'GE-Proton': # Older GE (< 10) are known to be slower for heavy texture processing.
display_name = f"{proton_name} (GE)" major_version = proton.get('major_version')
else: # Check if we have a valid major_version and it's < 10
display_name = proton_name if major_version is not None and isinstance(major_version, int) and major_version < 10:
fast_protons.append((display_name, str(proton['path']))) slow_warning = True
else: # Also check name pattern as fallback (e.g., "GE-Proton9-27")
# Slow Protons: Valve 9, 10 beta, older GE-Proton, etc. elif 'GE-Proton9' in proton_name or 'GE-Proton8' in proton_name:
if proton_type == 'GE-Proton': slow_warning = True
display_name = f"{proton_name} (GE) (Slow texture processing)" display_name = f"{proton_name} (GE)"
else: elif proton_type == 'Valve-Proton':
display_name = f"{proton_name} (Slow texture processing)" # Valve Proton 9.x is slower for BC7/BC6H workloads; newer Valve Proton is fine.
display_name = proton_name
if proton_name.startswith("Proton 9") or "9.0" in proton_name:
slow_warning = True
# Add slow label if needed
if slow_warning:
display_name = f"{display_name} (Slow texture processing)"
slow_protons.append((display_name, str(proton['path']))) slow_protons.append((display_name, str(proton['path'])))
else:
# Everything else (fast) goes above the separator
fast_protons.append((display_name, str(proton['path'])))
# Add fast Protons first # Add fast Protons first
for display_name, path in fast_protons: for display_name, path in fast_protons:
@@ -964,7 +974,7 @@ class SettingsDialog(QDialog):
# Save component installation method preference # Save component installation method preference
if self.winetricks_radio.isChecked(): if self.winetricks_radio.isChecked():
method = 'winetricks' method = 'winetricks'
else: # protontricks_radio (default) else: # protontricks_radio (alternative)
method = 'system_protontricks' method = 'system_protontricks'
old_method = self.config_handler.get('component_installation_method', 'winetricks') old_method = self.config_handler.get('component_installation_method', 'winetricks')
@@ -1628,11 +1638,24 @@ class JackifyMainWindow(QMainWindow):
def cleanup_processes(self): def cleanup_processes(self):
"""Clean up any running processes before closing""" """Clean up any running processes before closing"""
try: try:
# Clean up background threads first
if hasattr(self, '_update_thread') and self._update_thread is not None:
if self._update_thread.isRunning():
self._update_thread.quit()
self._update_thread.wait(2000)
self._update_thread = None
if hasattr(self, '_gallery_cache_preload_thread') and self._gallery_cache_preload_thread is not None:
if self._gallery_cache_preload_thread.isRunning():
self._gallery_cache_preload_thread.quit()
self._gallery_cache_preload_thread.wait(2000)
self._gallery_cache_preload_thread = None
# Clean up GUI services # Clean up GUI services
for service in self.gui_services.values(): for service in self.gui_services.values():
if hasattr(service, 'cleanup'): if hasattr(service, 'cleanup'):
service.cleanup() service.cleanup()
# Clean up screen processes # Clean up screen processes
screens = [ screens = [
self.modlist_tasks_screen, self.install_modlist_screen, self.modlist_tasks_screen, self.install_modlist_screen,

View File

@@ -635,6 +635,9 @@ class ConfigureExistingModlistScreen(QWidget):
# This ensures Proton version and winetricks settings are current # This ensures Proton version and winetricks settings are current
self.config_handler._load_config() self.config_handler._load_config()
# Store install_dir for later use in on_configuration_complete
self._current_install_dir = install_dir
try: try:
# Start time tracking # Start time tracking
self._workflow_start_time = time.time() self._workflow_start_time = time.time()
@@ -733,8 +736,13 @@ class ConfigureExistingModlistScreen(QWidget):
"""Handle configuration completion""" """Handle configuration completion"""
# Re-enable all controls when workflow completes # Re-enable all controls when workflow completes
self._enable_controls_after_operation() self._enable_controls_after_operation()
if success: if success:
# Check for VNV post-install automation after configuration
install_dir = getattr(self, '_current_install_dir', None)
if install_dir:
self._check_and_run_vnv_automation(modlist_name, install_dir)
# Calculate time taken # Calculate time taken
time_taken = self._calculate_time_taken() time_taken = self._calculate_time_taken()
@@ -759,10 +767,94 @@ class ConfigureExistingModlistScreen(QWidget):
"""Handle configuration error""" """Handle configuration error"""
# Re-enable all controls on error # Re-enable all controls on error
self._enable_controls_after_operation() self._enable_controls_after_operation()
self._safe_append_text(f"Configuration error: {error_message}") self._safe_append_text(f"Configuration error: {error_message}")
MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}", safety_level="medium") MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}", safety_level="medium")
def _check_and_run_vnv_automation(self, modlist_name: str, install_dir: str):
"""Check if VNV automation should run and execute if applicable
Args:
modlist_name: Name of the installed modlist
install_dir: Installation directory path
"""
try:
from pathlib import Path
from jackify.backend.services.vnv_integration_helper import run_vnv_automation_if_applicable, should_offer_vnv_automation
from jackify.backend.services.automated_prefix_service import AutomatedPrefixService
from jackify.backend.handlers.path_handler import PathHandler
# Get paths first (needed for VNV detection)
install_path = Path(install_dir)
# Quick check before importing more (pass install location for ModOrganizer.ini check)
if not should_offer_vnv_automation(modlist_name, install_path):
return
game_paths = PathHandler().find_vanilla_game_paths()
game_root = game_paths.get('Fallout New Vegas')
if not game_root:
debug_print("DEBUG: VNV automation skipped - FNV game root not found")
return
# Confirmation callback - show dialog to user
def confirmation_callback(description: str) -> bool:
from ..services.message_service import MessageService
reply = MessageService.question(
self,
"VNV Post-Install Automation",
description,
critical=False,
safety_level="medium"
)
return reply == QMessageBox.Yes
# Manual file callback for non-Premium users
def manual_file_callback(title: str, instructions: str) -> Optional[Path]:
from PySide6.QtWidgets import QFileDialog
from ..services.message_service import MessageService
# Show instructions
MessageService.information(self, title, instructions)
# Open file picker
file_path, _ = QFileDialog.getOpenFileName(
self,
title,
str(Path.home() / "Downloads"),
"All Files (*.*)"
)
if file_path:
return Path(file_path)
return None
# Run automation
automation_ran, error = run_vnv_automation_if_applicable(
modlist_name=modlist_name,
modlist_install_location=install_path,
game_root=game_root,
ttw_installer_path=AutomatedPrefixService.get_ttw_installer_path(),
progress_callback=None, # GUI doesn't need progress updates for post-install
manual_file_callback=manual_file_callback,
confirmation_callback=confirmation_callback
)
if error:
from ..services.message_service import MessageService
MessageService.warning(
self,
"VNV Automation Failed",
f"VNV post-install automation encountered an error:\n\n{error}\n\n"
"You can complete these steps manually by following the guide at:\n"
"https://vivanewvegas.moddinglinked.com/wabbajack.html"
)
except Exception as e:
debug_print(f"ERROR: Failed to run VNV automation: {e}")
import traceback
debug_print(f"Traceback: {traceback.format_exc()}")
def show_manual_steps_dialog(self, extra_warning=""): def show_manual_steps_dialog(self, extra_warning=""):
modlist_name = self.shortcut_combo.currentText().split('(')[0].strip() or "your modlist" modlist_name = self.shortcut_combo.currentText().split('(')[0].strip() or "your modlist"
msg = ( msg = (

View File

@@ -1462,8 +1462,13 @@ class ConfigureNewModlistScreen(QWidget):
"""Handle configuration completion (same as Tuxborn)""" """Handle configuration completion (same as Tuxborn)"""
# Re-enable all controls when workflow completes # Re-enable all controls when workflow completes
self._enable_controls_after_operation() self._enable_controls_after_operation()
if success: if success:
# Check for VNV post-install automation after configuration
install_dir = self.install_dir_edit.text().strip()
if install_dir:
self._check_and_run_vnv_automation(modlist_name, install_dir)
# Calculate time taken # Calculate time taken
time_taken = self._calculate_time_taken() time_taken = self._calculate_time_taken()
@@ -1541,6 +1546,90 @@ class ConfigureNewModlistScreen(QWidget):
else: else:
return f"{elapsed_seconds_remainder} seconds" return f"{elapsed_seconds_remainder} seconds"
def _check_and_run_vnv_automation(self, modlist_name: str, install_dir: str):
"""Check if VNV automation should run and execute if applicable
Args:
modlist_name: Name of the installed modlist
install_dir: Installation directory path
"""
try:
from pathlib import Path
from jackify.backend.services.vnv_integration_helper import run_vnv_automation_if_applicable, should_offer_vnv_automation
from jackify.backend.services.automated_prefix_service import AutomatedPrefixService
from jackify.backend.handlers.path_handler import PathHandler
# Get paths first (needed for VNV detection)
install_path = Path(install_dir)
# Quick check before importing more (pass install location for ModOrganizer.ini check)
if not should_offer_vnv_automation(modlist_name, install_path):
return
game_paths = PathHandler().find_vanilla_game_paths()
game_root = game_paths.get('Fallout New Vegas')
if not game_root:
debug_print("DEBUG: VNV automation skipped - FNV game root not found")
return
# Confirmation callback - show dialog to user
def confirmation_callback(description: str) -> bool:
from ..services.message_service import MessageService
reply = MessageService.question(
self,
"VNV Post-Install Automation",
description,
critical=False,
safety_level="medium"
)
return reply == QMessageBox.Yes
# Manual file callback for non-Premium users
def manual_file_callback(title: str, instructions: str) -> Optional[Path]:
from PySide6.QtWidgets import QFileDialog
from ..services.message_service import MessageService
# Show instructions
MessageService.information(self, title, instructions)
# Open file picker
file_path, _ = QFileDialog.getOpenFileName(
self,
title,
str(Path.home() / "Downloads"),
"All Files (*.*)"
)
if file_path:
return Path(file_path)
return None
# Run automation
automation_ran, error = run_vnv_automation_if_applicable(
modlist_name=modlist_name,
modlist_install_location=install_path,
game_root=game_root,
ttw_installer_path=AutomatedPrefixService.get_ttw_installer_path(),
progress_callback=None, # GUI doesn't need progress updates for post-install
manual_file_callback=manual_file_callback,
confirmation_callback=confirmation_callback
)
if error:
from ..services.message_service import MessageService
MessageService.warning(
self,
"VNV Automation Failed",
f"VNV post-install automation encountered an error:\n\n{error}\n\n"
"You can complete these steps manually by following the guide at:\n"
"https://vivanewvegas.moddinglinked.com/wabbajack.html"
)
except Exception as e:
debug_print(f"ERROR: Failed to run VNV automation: {e}")
import traceback
debug_print(f"Traceback: {traceback.format_exc()}")
def show_next_steps_dialog(self, message): def show_next_steps_dialog(self, message):
dlg = QDialog(self) dlg = QDialog(self)
dlg.setWindowTitle("Next Steps") dlg.setWindowTitle("Next Steps")

View File

@@ -1784,6 +1784,27 @@ class InstallModlistScreen(QWidget):
modlist_name = getattr(self, '_ttw_modlist_name', 'Unknown') modlist_name = getattr(self, '_ttw_modlist_name', 'Unknown')
game_name = "Fallout New Vegas" game_name = "Fallout New Vegas"
# Check for VNV post-install automation after TTW installation
vnv_automation_running = False
if hasattr(self, '_ttw_install_dir') and hasattr(self, '_ttw_modlist_name'):
vnv_automation_running = self._check_and_run_vnv_automation(self._ttw_modlist_name, self._ttw_install_dir)
if vnv_automation_running:
# Store success dialog params for later (after VNV automation completes)
self._pending_success_dialog_params = {
'modlist_name': modlist_name,
'time_taken': time_str,
'game_name': game_name,
'enb_detected': False, # TTW installs don't have ENB
'ttw_version': ttw_version if 'ttw_version' in locals() else None
}
# Keep post-install feedback active during VNV automation
# Don't show success dialog yet - will be shown in _on_vnv_complete
return
# No VNV automation - end post-install feedback now
self._end_post_install_feedback(True)
# Clear Activity window before showing success dialog # Clear Activity window before showing success dialog
self.file_progress_list.clear() self.file_progress_list.clear()
@@ -1797,7 +1818,7 @@ class InstallModlistScreen(QWidget):
) )
# Add TTW installation info to dialog if possible # Add TTW installation info to dialog if possible
if hasattr(success_dialog, 'add_info_line'): if 'ttw_version' in locals() and hasattr(success_dialog, 'add_info_line'):
success_dialog.add_info_line(f"TTW {ttw_version} integrated successfully") success_dialog.add_info_line(f"TTW {ttw_version} integrated successfully")
success_dialog.show() success_dialog.show()
@@ -1810,6 +1831,216 @@ class InstallModlistScreen(QWidget):
f"TTW integration completed but failed to show success dialog: {str(e)}" f"TTW integration completed but failed to show success dialog: {str(e)}"
) )
def _check_and_run_vnv_automation(self, modlist_name: str, install_dir: str) -> bool:
"""Check if VNV automation should run and execute if applicable in background thread
Args:
modlist_name: Name of the installed modlist
install_dir: Installation directory path
Returns:
True if VNV automation is starting (success dialog should be deferred)
False if no VNV automation needed (show success dialog immediately)
"""
try:
from pathlib import Path
from jackify.backend.services.vnv_integration_helper import should_offer_vnv_automation
from jackify.backend.handlers.path_handler import PathHandler
from jackify.backend.services.vnv_post_install_service import VNVPostInstallService
from jackify.backend.services.automated_prefix_service import AutomatedPrefixService
# Get paths first (needed for VNV detection)
install_path = Path(install_dir)
# Quick check before importing more (pass install location for ModOrganizer.ini check)
if not should_offer_vnv_automation(modlist_name, install_path):
return False
game_paths = PathHandler().find_vanilla_game_paths()
game_root = game_paths.get('Fallout New Vegas')
if not game_root:
debug_print("DEBUG: VNV automation skipped - FNV game root not found")
return False
# Initialize service to check completion status
vnv_service = VNVPostInstallService(
modlist_install_location=install_path,
game_root=game_root,
ttw_installer_path=AutomatedPrefixService.get_ttw_installer_path()
)
# Check what's already done
completed = vnv_service.check_already_completed()
# Only skip if ALL three steps are completed
if completed['root_mods'] and completed['4gb_patch'] and completed['bsa_decompressed']:
logger.info("VNV automation steps already completed")
return False
# Get automation description for confirmation
description = vnv_service.get_automation_description()
# Show confirmation dialog ON MAIN THREAD (not in worker thread!)
from ..services.message_service import MessageService
reply = MessageService.question(
self,
"VNV Post-Install Automation",
description,
critical=False,
safety_level="medium"
)
if reply != QMessageBox.Yes:
logger.info("User declined VNV automation")
return False
# Manual file callback for non-Premium users
def manual_file_callback(title: str, instructions: str) -> Optional[Path]:
from PySide6.QtWidgets import QFileDialog
from ..services.message_service import MessageService
# Show instructions
MessageService.information(self, title, instructions)
# Open file picker
file_path, _ = QFileDialog.getOpenFileName(
self,
title,
str(Path.home() / "Downloads"),
"All Files (*.*)"
)
if file_path:
return Path(file_path)
return None
# Enable post-install progress tracking for VNV automation
self._begin_post_install_feedback()
# User confirmed - start automation in background thread
self._run_vnv_automation_threaded(
modlist_name,
install_path,
game_root,
manual_file_callback
)
return True # VNV automation is running, defer success dialog
except Exception as e:
debug_print(f"ERROR: Failed to start VNV automation: {e}")
import traceback
debug_print(f"Traceback: {traceback.format_exc()}")
return False # Error - show success dialog anyway
def _run_vnv_automation_threaded(self, modlist_name, install_path, game_root,
manual_file_callback):
"""Run VNV automation in a background thread with progress updates
Note: User confirmation should already be obtained before calling this method.
"""
from PySide6.QtCore import QThread, Signal
from jackify.backend.services.vnv_integration_helper import run_vnv_automation_if_applicable
from jackify.backend.services.automated_prefix_service import AutomatedPrefixService
class VNVAutomationWorker(QThread):
progress_update = Signal(str)
completed = Signal(bool, str) # (success, error_message)
def __init__(self, modlist_name, install_path, game_root, ttw_installer_path,
manual_file_callback):
super().__init__()
self.modlist_name = modlist_name
self.install_path = install_path
self.game_root = game_root
self.ttw_installer_path = ttw_installer_path
self.manual_file_callback = manual_file_callback
def run(self):
try:
# User already confirmed, pass lambda that always returns True
automation_ran, error = run_vnv_automation_if_applicable(
modlist_name=self.modlist_name,
modlist_install_location=self.install_path,
game_root=self.game_root,
ttw_installer_path=self.ttw_installer_path,
progress_callback=self.progress_update.emit,
manual_file_callback=self.manual_file_callback,
confirmation_callback=lambda desc: True # Already confirmed on main thread
)
self.completed.emit(error is None, error or "")
except Exception as e:
import traceback
self.completed.emit(False, f"Exception: {str(e)}\n{traceback.format_exc()}")
# Create and start worker
self.vnv_worker = VNVAutomationWorker(
modlist_name,
install_path,
game_root,
AutomatedPrefixService.get_ttw_installer_path(),
manual_file_callback
)
# Connect signals
self.vnv_worker.progress_update.connect(self._on_vnv_progress)
self.vnv_worker.completed.connect(self._on_vnv_complete)
self.vnv_worker.finished.connect(self.vnv_worker.deleteLater)
# Start worker
self.vnv_worker.start()
def _on_vnv_progress(self, message: str):
"""Handle VNV automation progress updates"""
self._safe_append_text(message)
# Also update progress indicator, Activity window, and Details window
self._handle_post_install_progress(message)
def _on_vnv_complete(self, success: bool, error: str):
"""Handle VNV automation completion and show deferred success dialog"""
# End post-install feedback now that VNV automation is complete
self._end_post_install_feedback(True)
if not success and error:
from ..services.message_service import MessageService
MessageService.warning(
self,
"VNV Automation Failed",
f"VNV post-install automation encountered an error:\n\n{error}\n\n"
"You can complete these steps manually by following the guide at:\n"
"https://vivanewvegas.moddinglinked.com/wabbajack.html"
)
elif success:
self._safe_append_text("VNV post-install automation completed successfully")
# Show the deferred success dialog now that VNV automation is complete
if hasattr(self, '_pending_success_dialog_params'):
params = self._pending_success_dialog_params
del self._pending_success_dialog_params # Clean up
# Clear Activity window before showing success dialog
self.file_progress_list.clear()
# Show success dialog
from ..dialogs import SuccessDialog
success_dialog = SuccessDialog(
modlist_name=params['modlist_name'],
workflow_type="install",
time_taken=params['time_taken'],
game_name=params['game_name'],
parent=self
)
success_dialog.show()
# Show ENB Proton dialog if ENB was detected
if params.get('enb_detected'):
try:
from ..dialogs.enb_proton_dialog import ENBProtonDialog
enb_dialog = ENBProtonDialog(modlist_name=params['modlist_name'], parent=self)
enb_dialog.exec() # Modal dialog - blocks until user clicks OK
except Exception as e:
# Non-blocking: if dialog fails, just log and continue
logger.warning(f"Failed to show ENB dialog: {e}")
def validate_and_start_install(self): def validate_and_start_install(self):
@@ -3255,6 +3486,39 @@ class InstallModlistScreen(QWidget):
"backup", "backup",
], ],
}, },
{
'id': 'vnv_root_mods',
'label': "VNV: Copying root mods",
'keywords': [
"step 1/3: copying root mods",
"copying root mods to game directory",
"root mods:",
],
},
{
'id': 'vnv_4gb_patch',
'label': "VNV: Applying 4GB patch",
'keywords': [
"step 2/3: downloading and running 4gb patcher",
"downloading fnv4gb",
"downloading:",
"fetching file list",
"running 4gb patcher",
"4gb patcher:",
],
},
{
'id': 'vnv_bsa_decompress',
'label': "VNV: Decompressing BSA files",
'keywords': [
"step 3/3: downloading and running bsa decompressor",
"downloading:",
"fetching file list",
"running bsa decompressor",
"decompressing bsa files:",
"bsa decompression:",
],
},
{ {
'id': 'config_finalize', 'id': 'config_finalize',
'label': "Finalising Jackify configuration", 'label': "Finalising Jackify configuration",
@@ -3262,7 +3526,8 @@ class InstallModlistScreen(QWidget):
"configuration completed successfully", "configuration completed successfully",
"configuration complete", "configuration complete",
"manual steps validation failed", "manual steps validation failed",
"configuration failed" "configuration failed",
"vnv post-install completed successfully"
], ],
}, },
] ]
@@ -3865,8 +4130,9 @@ class InstallModlistScreen(QWidget):
self.file_progress_list.stop_cpu_tracking() self.file_progress_list.stop_cpu_tracking()
# Re-enable controls now that installation/configuration is complete # Re-enable controls now that installation/configuration is complete
self._enable_controls_after_operation() self._enable_controls_after_operation()
self._end_post_install_feedback(success) # Don't end post-install feedback yet - may continue with VNV automation
# Will be called in _on_vnv_complete or after VNV check
if success: if success:
# Check if we need to show Somnium guidance # Check if we need to show Somnium guidance
if self._show_somnium_guidance: if self._show_somnium_guidance:
@@ -3917,6 +4183,24 @@ class InstallModlistScreen(QWidget):
self._initiate_ttw_workflow(modlist_name, install_dir) self._initiate_ttw_workflow(modlist_name, install_dir)
return # Don't show success dialog yet, will show after TTW completes return # Don't show success dialog yet, will show after TTW completes
# Check for VNV post-install automation after TTW check
vnv_automation_running = self._check_and_run_vnv_automation(modlist_name, install_dir)
if vnv_automation_running:
# Store success dialog params for later (after VNV automation completes)
self._pending_success_dialog_params = {
'modlist_name': modlist_name,
'time_taken': time_str,
'game_name': game_name,
'enb_detected': enb_detected
}
# Keep post-install feedback active during VNV automation
# Don't show success dialog yet - will be shown in _on_vnv_complete
return
# No VNV automation - end post-install feedback now
self._end_post_install_feedback(True)
# Clear Activity window before showing success dialog # Clear Activity window before showing success dialog
self.file_progress_list.clear() self.file_progress_list.clear()
@@ -3929,7 +4213,7 @@ class InstallModlistScreen(QWidget):
parent=self parent=self
) )
success_dialog.show() success_dialog.show()
# Show ENB Proton dialog if ENB was detected (use stored detection result, no re-detection) # Show ENB Proton dialog if ENB was detected (use stored detection result, no re-detection)
if enb_detected: if enb_detected:
try: try:
@@ -3941,11 +4225,13 @@ class InstallModlistScreen(QWidget):
logger.warning(f"Failed to show ENB dialog: {e}") logger.warning(f"Failed to show ENB dialog: {e}")
elif hasattr(self, '_manual_steps_retry_count') and self._manual_steps_retry_count >= 3: elif hasattr(self, '_manual_steps_retry_count') and self._manual_steps_retry_count >= 3:
# Max retries reached - show failure message # Max retries reached - show failure message
MessageService.critical(self, "Manual Steps Failed", self._end_post_install_feedback(False)
MessageService.critical(self, "Manual Steps Failed",
"Manual steps validation failed after multiple attempts.") "Manual steps validation failed after multiple attempts.")
else: else:
# Configuration failed for other reasons # Configuration failed for other reasons
MessageService.critical(self, "Configuration Failed", self._end_post_install_feedback(False)
MessageService.critical(self, "Configuration Failed",
"Post-install configuration failed. Please check the console output.") "Post-install configuration failed. Please check the console output.")
except Exception as e: except Exception as e:
# Ensure controls are re-enabled even on unexpected errors # Ensure controls are re-enabled even on unexpected errors

View File

@@ -489,10 +489,25 @@ class InstallTTWScreen(QWidget):
ttw_installer_handler._check_installation() ttw_installer_handler._check_installation()
if ttw_installer_handler.ttw_installer_installed: if ttw_installer_handler.ttw_installer_installed:
# Check version against latest # Check version against pinned/latest
update_available, installed_v, latest_v = ttw_installer_handler.is_ttw_installer_update_available() update_available, installed_v, target_v = ttw_installer_handler.is_ttw_installer_update_available()
if update_available: if update_available:
version_text = f"Out of date (v{installed_v} → v{latest_v})" if installed_v and latest_v else "Out of date" # Determine if this is a downgrade or upgrade
from jackify.backend.handlers.ttw_installer_handler import TTW_INSTALLER_PINNED_VERSION
if TTW_INSTALLER_PINNED_VERSION and installed_v and target_v:
# If we have a pinned version and installed is newer, it's a downgrade
try:
# Simple version comparison - if installed version string is longer/more complex, likely newer
# For now, just check if they're different and show appropriate message
if installed_v != target_v:
version_text = f"Update to v{target_v} (currently v{installed_v})"
else:
version_text = f"Update available (v{installed_v} → v{target_v})"
except Exception:
version_text = f"Update to v{target_v}" if target_v else "Update available"
else:
# Normal update (newer version available)
version_text = f"Update available (v{installed_v} → v{target_v})" if installed_v and target_v else "Update available"
self.ttw_installer_status.setText(version_text) self.ttw_installer_status.setText(version_text)
self.ttw_installer_status.setStyleSheet("color: #f44336;") self.ttw_installer_status.setStyleSheet("color: #f44336;")
self.ttw_installer_btn.setText("Update now") self.ttw_installer_btn.setText("Update now")

View File

@@ -106,7 +106,7 @@ class LoggingHandler:
if log_file or is_general: if log_file or is_general:
file_path = self.log_dir / (log_file if log_file else "jackify-cli.log") file_path = self.log_dir / (log_file if log_file else "jackify-cli.log")
file_handler = logging.handlers.RotatingFileHandler( file_handler = logging.handlers.RotatingFileHandler(
file_path, mode='a', encoding='utf-8', maxBytes=1024*1024, backupCount=5 file_path, mode='a', encoding='utf-8', maxBytes=100*1024*1024, backupCount=5
) )
# File handler always accepts DEBUG - root logger level controls what gets through # File handler always accepts DEBUG - root logger level controls what gets through
file_handler.setLevel(logging.DEBUG) file_handler.setLevel(logging.DEBUG)
@@ -116,7 +116,7 @@ class LoggingHandler:
return logger return logger
def rotate_logs(self, max_bytes: int = 1024 * 1024, backup_count: int = 5) -> None: def rotate_logs(self, max_bytes: int = 100 * 1024 * 1024, backup_count: int = 5) -> None:
"""Rotate log files based on size.""" """Rotate log files based on size."""
for log_file in self.get_log_files(): for log_file in self.get_log_files():
try: try:

19
jackify/tools/.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
# Tools downloaded during AppImage build
# These are fetched from upstream sources during build_appimage_simple.sh
# They are NOT stored in git to keep the repository clean
# See THIRD_PARTY_NOTICES.md for licensing information
# Downloaded during build:
winetricks
cabextract
7z
wget
unzip
sha256sum
# lz4 is copied from system during build
lz4
# Keep this .gitignore and README
!.gitignore
!README.md

Binary file not shown.

52
jackify/tools/README.md Normal file
View File

@@ -0,0 +1,52 @@
# Jackify Tools Directory
This directory contains utility tools bundled with Jackify for self-contained AppImage distribution.
## How It Works
**In Git Repository**: This directory only contains this README and .gitignore (no binaries).
**During Build**: The `build_appimage_simple.sh` script downloads required tools from their official sources.
**In AppImage**: Tools are bundled for self-contained distribution (users don't need to install dependencies).
## Tools Downloaded During Build
### winetricks
- **Source**: https://github.com/Winetricks/winetricks
- **License**: LGPL v2.1
- **Purpose**: Wine prefix configuration and Windows component installation
- **Downloaded from**: GitHub master branch (latest version)
### cabextract
- **Source**: https://github.com/kyz/cabextract
- **License**: GPL v3
- **Purpose**: Microsoft Cabinet file extraction
- **Downloaded from**: GitHub releases (v1.11)
### 7-Zip (7z / 7zz)
- **Source**: https://www.7-zip.org/
- **License**: GNU LGPL + unRAR restriction
- **Purpose**: Archive extraction and compression
- **Downloaded from**: Official 7-Zip website
### lz4
- **Source**: System package (copied from /usr/bin/lz4)
- **License**: BSD 2-Clause
- **Purpose**: Fast compression for TTW installer patch decompression
- **Note**: Copied from build system, not downloaded
## Why Not Store in Git?
1. **Repository Size**: Binaries total ~11MB, bloating git history
2. **Updates**: Tools update independently of Jackify code
3. **Professional Practice**: Industry standard separates source code from build artifacts
4. **Licensing**: Clear separation between our code and third-party binaries
## For Developers
When building locally with `./build_appimage_simple.sh`, these tools are automatically downloaded.
If build fails to download tools, the script will attempt to use system versions as fallback.
See `THIRD_PARTY_NOTICES.md` in project root for complete licensing information.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -6,7 +6,7 @@
# Name of this version of winetricks (YYYYMMDD) # Name of this version of winetricks (YYYYMMDD)
# (This doesn't change often, use the sha256sum of the file when reporting problems) # (This doesn't change often, use the sha256sum of the file when reporting problems)
WINETRICKS_VERSION=20250102-next WINETRICKS_VERSION=20260125-next
# This is a UTF-8 file # This is a UTF-8 file
# You should see an o with two dots over it here [ö] # You should see an o with two dots over it here [ö]
@@ -76,6 +76,7 @@ WINETRICKS_VERSION=20250102-next
# Copyright (C) 2013,2016 André Hentschel <nerv!dawncrow.de> # Copyright (C) 2013,2016 André Hentschel <nerv!dawncrow.de>
# Copyright (C) 2023 Georgi Georgiev (RacerBG) <g.georgiev.shumen!gmail.com> # Copyright (C) 2023 Georgi Georgiev (RacerBG) <g.georgiev.shumen!gmail.com>
# Copyright (C) 2025 ykla <yklaxds!gmail.com> # Copyright (C) 2025 ykla <yklaxds!gmail.com>
# Copyright (C) 2025 Allan Rémy
# #
# License: # License:
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
@@ -360,6 +361,31 @@ _w_get_broken_messages()
broken_only_bad_version_known_win64="Пакетът (${W_PACKAGE}) е повреден при 64-битовата архитектура на wine-${_wine_version_stripped}. Повреден е от версия ${bad_version}. Използвайте папка, създадена с WINEARCH=win32. Вижте ${bug_link} за повече информация. Използвайте --force, за да опитате въпреки това." broken_only_bad_version_known_win64="Пакетът (${W_PACKAGE}) е повреден при 64-битовата архитектура на wine-${_wine_version_stripped}. Повреден е от версия ${bad_version}. Използвайте папка, създадена с WINEARCH=win32. Вижте ${bug_link} за повече информация. Използвайте --force, за да опитате въпреки това."
broken_no_version_known_win64="Пакетът (${W_PACKAGE}) е повреден, когато wine е създаден без mingw. Вижте ${bug_link} за повече информация. Използвайте --force, за да опитате въпреки това." broken_no_version_known_win64="Пакетът (${W_PACKAGE}) е повреден, когато wine е създаден без mingw. Вижте ${bug_link} за повече информация. Използвайте --force, за да опитате въпреки това."
;; ;;
fr*)
# default broken messages
broken_good_version_known_default="Ce paquet (${W_PACKAGE}) ne fonctionne pas correctement dans wine-${_wine_version_stripped}. Effectuez une mise à niveau vers >=${good_version}. Consultez ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
broken_good_and_bad_version_known_default="Ce paquet (${W_PACKAGE}) est cassé dans wine-${_wine_version_stripped}. Cassé depuis ${bad_version}. Mettez à jour vers >=${good_version}. Voir ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
broken_only_bad_version_known_default="Ce paquet (${W_PACKAGE}) est cassé dans wine-${_wine_version_stripped}. Cassé depuis ${bad_version}. Voir ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
broken_no_version_known_default="Ce paquet (${W_PACKAGE}) est cassé. Voir ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
# mingw broken messages
broken_good_version_known_mingw="Ce paquet (${W_PACKAGE}) ne fonctionne pas sous wine-${_wine_version_stripped} lorsque wine est compilé avec mingw. Effectuez une mise à niveau vers >=${good_version} ou recompilez wine sans mingw. Consultez ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
broken_good_and_bad_version_known_mingw="Ce paquet (${W_PACKAGE}) est cassé dans wine-${_wine_version_stripped}. Cassé depuis ${bad_version} lorsque wine est compilé avec mingw. Mettez à jour vers >=${good_version} ou recompilez wine sans mingw. Voir ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
broken_only_bad_version_known_mingw="Ce paquet (${W_PACKAGE}) est cassé dans wine-${_wine_version_stripped}. Cassé depuis ${bad_version} lorsque wine est compilé avec mingw. Voir ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
broken_no_version_known_mingw="Ce paquet (${W_PACKAGE}) est cassé lorsque Wine est compilé avec mingw. Voir ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
# no mingw broken messages
broken_good_version_known_no_mingw="Ce paquet (${W_PACKAGE}) est défectueux dans wine-${_wine_version_stripped} lorsque wine est compilé sans mingw. Effectuez une mise à niveau vers >=${good_version}. Consultez ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
broken_good_and_bad_version_known_no_mingw="Ce paquet (${W_PACKAGE}) est cassé dans wine-${_wine_version_stripped}. Cassé depuis ${bad_version} lorsque wine est compilé sans mingw. Mettez à jour vers >=${good_version}. Voir ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
broken_only_bad_version_known_no_mingw="Ce paquet (${W_PACKAGE}) est cassé dans wine-${_wine_version_stripped}. Cassé depuis ${bad_version} lorsque wine est compilé sans mingw. Voir ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
broken_no_version_known_no_mingw="Ce paquet (${W_PACKAGE}) est cassé lorsque wine est compilé sans mingw. Voir ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
# win64 broken messages
broken_good_version_known_win64="Ce paquet (${W_PACKAGE}) ne fonctionne pas sous Wine 64 bits-${_wine_version_stripped}. Utilisez un préfixe créé avec WINEARCH=win32 ou mettez à niveau Wine vers >=${good_version} pour contourner ce problème. Ou utilisez --force pour essayer quand même. Voir ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
broken_good_and_bad_version_known_win64="Ce paquet (${W_PACKAGE}) est cassé sur wine-${_wine_version_stripped} 64 bits. Cassé depuis ${bad_version}. Utilisez un préfixe créé avec WINEARCH=win32 ou mettez à jour wine vers >=${good_version} pour contourner ce problème. Voir ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
broken_only_bad_version_known_win64="Ce paquet (${W_PACKAGE}) est cassé sur wine-${_wine_version_stripped} 64 bits. Cassé depuis ${bad_version}. Utilisez un préfixe créé avec WINEARCH=win32 pour contourner ce problème. Voir ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
broken_no_version_known_win64="Ce paquet (${W_PACKAGE}) est cassé lorsque wine est compilé sans mingw. Voir ${bug_link} pour plus d'informations. Utilisez --force pour essayer quand même."
;;
pt*) pt*)
# default broken messages # default broken messages
broken_good_version_known_default="O pacote (${W_PACKAGE}) está quebrado no wine-${_wine_version_stripped}. Atualize para >=${good_version}. Veja ${bug_link} para mais informações. Use --force para tentar forçar de toda forma." broken_good_version_known_default="O pacote (${W_PACKAGE}) está quebrado no wine-${_wine_version_stripped}. Atualize para >=${good_version}. Veja ${bug_link} para mais informações. Use --force para tentar forçar de toda forma."
@@ -1154,6 +1180,29 @@ w_get_github_latest_prerelease()
echo "${latest_version}" echo "${latest_version}"
} }
# Get the latest tagged release from gitlab.com API
w_get_gitlab_latest_release()
{
# FIXME: can we get releases that aren't on master branch?
org="$1"
repo="$2"
# release.json might still exists from the previous verb
w_try rm -f "${W_TMP_EARLY}/release.json"
WINETRICKS_SUPER_QUIET=1 w_download_to "${W_TMP_EARLY}" "https://gitlab.com/api/v4/projects/${org}%2F${repo}/releases" "" "release.json" >/dev/null 2>&1
# aria2c condenses the json (https://github.com/aria2/aria2/issues/1389)
# but curl/wget don't, so handle both cases:
json_length="$(wc -l "${W_TMP_EARLY}/release.json")"
case "${json_length}" in
0*) latest_version="$(sed -e "s/\",\"/|/g" "${W_TMP_EARLY}/release.json" | tr '|' '\n' | grep tag_name | sed 's@.*"@@' | head -n 1)";;
*) latest_version="$(grep -w tag_name "${W_TMP_EARLY}/release.json" | cut -d '"' -f 4 | head -n 1)";;
esac
echo "${latest_version}"
}
# get sha256sum string and set $_W_gotsha256sum to it # get sha256sum string and set $_W_gotsha256sum to it
w_get_sha256sum() w_get_sha256sum()
{ {
@@ -3193,6 +3242,17 @@ winetricks_prefixmenu()
_W_msg_unattended1="Включване на автоматичното инсталиране" _W_msg_unattended1="Включване на автоматичното инсталиране"
_W_msg_help="Отваряне на помощта" _W_msg_help="Отваряне на помощта"
;; ;;
fr*) _W_msg_title="Winetricks - Choisir un préfixe"
_W_msg_body="Que souhaitez-vous faire ?"
_W_msg_apps="Installer une application"
_W_msg_benchmarks="Installer un benchmark"
_W_msg_default="Sélectionner le préfixe par défaut"
_W_msg_mkprefix="Créer un nouveau préfixe"
_W_msg_unattended0="Désactiver une installation silencieuse"
_W_msg_unattended1="Activer une installation silencieuse"
_W_msg_help="Voir aide"
_W_msg_cancel="Quitter"
;;
ru*) _W_msg_title="Winetricks - выберите путь wine (префикс)" ru*) _W_msg_title="Winetricks - выберите путь wine (префикс)"
_W_msg_body='Что вы хотите сделать?' _W_msg_body='Что вы хотите сделать?'
_W_msg_apps='Установить программу' _W_msg_apps='Установить программу'
@@ -3373,6 +3433,10 @@ winetricks_mkprefixmenu()
_W_msg_name="Name" _W_msg_name="Name"
_W_msg_arch="Architektur" _W_msg_arch="Architektur"
;; ;;
fr*) _W_msg_title="Winetricks - Créer un nouveau préfixe"
_W_msg_name="Nom :"
_W_msg_arch="Architecture"
;;
pt*) _W_msg_title="Winetricks - criar novo wineprefix" pt*) _W_msg_title="Winetricks - criar novo wineprefix"
_W_msg_name="Nome" _W_msg_name="Nome"
_W_msg_arch="Arquitetura" _W_msg_arch="Arquitetura"
@@ -3458,6 +3522,23 @@ winetricks_mainmenu()
_W_msg_folder='Ordner durchsuchen' _W_msg_folder='Ordner durchsuchen'
_W_msg_annihilate="ALLE DATEIEN UND PROGRAMME IN DIESEM WINEPREFIX Löschen" _W_msg_annihilate="ALLE DATEIEN UND PROGRAMME IN DIESEM WINEPREFIX Löschen"
;; ;;
fr*) _W_msg_title="Winetricks - Le préfixe actuel est \"${WINEPREFIX}\""
_W_msg_body='Que souhaitez-vous faire avec ce préfixe ?'
_W_msg_cancel="Revenir en arrière"
_W_msg_dlls="Installer une DLL ou un composant Windows"
_W_msg_fonts='Installer une police'
_W_msg_settings='Modifier les paramètres'
_W_msg_winecfg='Exécuter winecfg'
_W_msg_regedit='Exécuter regedit'
_W_msg_taskmgr='Exécuter taskmgr'
_W_msg_explorer='Exécuter explorer'
_W_msg_uninstaller='Exécuter uninstaller'
_W_msg_winecmd='Exécuter une invite de commande'
_W_msg_wine_misc_exe='Exécuter un exécutable arbitraire (.exe/.msi/.msu)'
_W_msg_shell='Exécuter une invite de commande pour le débogage'
_W_msg_folder='Parcourir les fichiers'
_W_msg_annihilate="Supprimer TOUTES LES DONNÉES ET APPLICATIONS CONTENUES DANS CE PRÉFIXE"
;;
pl*) _W_msg_title="Winetricks - obecny prefiks to \"${WINEPREFIX}\"" pl*) _W_msg_title="Winetricks - obecny prefiks to \"${WINEPREFIX}\""
_W_msg_body='Co chcesz zrobić w tym prefiksie?' _W_msg_body='Co chcesz zrobić w tym prefiksie?'
_W_msg_dlls="Zainstalować windowsową bibliotekę DLL lub komponent" _W_msg_dlls="Zainstalować windowsową bibliotekę DLL lub komponent"
@@ -3472,7 +3553,7 @@ winetricks_mainmenu()
_W_msg_wine_misc_exe='Run an arbitrary executable (.exe/.msi/.msu)' _W_msg_wine_misc_exe='Run an arbitrary executable (.exe/.msi/.msu)'
_W_msg_shell='Uruchomić powłokę wiersza poleceń (dla debugowania)' _W_msg_shell='Uruchomić powłokę wiersza poleceń (dla debugowania)'
_W_msg_folder='Przeglądać pliki' _W_msg_folder='Przeglądać pliki'
_W_msg_annihilate="Usuńąć WSZYSTKIE DANE I APLIKACJE WEWNĄTRZ TEGO PREFIKSU WINE" _W_msg_annihilate="Usunąć WSZYSTKIE DANE I APLIKACJE WEWNĄTRZ TEGO PREFIKSU WINE"
;; ;;
pt*) _W_msg_title="Winetricks - o prefixo atual é \"${WINEPREFIX}\"" pt*) _W_msg_title="Winetricks - o prefixo atual é \"${WINEPREFIX}\""
_W_msg_body='O que você gostaria de fazer com este prefixo wineprefix?' _W_msg_body='O que você gostaria de fazer com este prefixo wineprefix?'
@@ -3644,6 +3725,9 @@ winetricks_settings_menu()
de*) _W_msg_title="Winetricks - Aktueller Präfix ist \"${WINEPREFIX}\"" de*) _W_msg_title="Winetricks - Aktueller Präfix ist \"${WINEPREFIX}\""
_W_msg_body='Welche Einstellungen möchten Sie ändern?' _W_msg_body='Welche Einstellungen möchten Sie ändern?'
;; ;;
fr*) _W_msg_title="Winetricks - Le préfixe actuel est \"${WINEPREFIX}\""
_W_msg_body='Quels paramètres souhaitez-vous modifier ?'
;;
pl*) _W_msg_title="Winetricks - obecny prefiks to \"${WINEPREFIX}\"" pl*) _W_msg_title="Winetricks - obecny prefiks to \"${WINEPREFIX}\""
_W_msg_body='Jakie ustawienia chcesz zmienić?' _W_msg_body='Jakie ustawienia chcesz zmienić?'
;; ;;
@@ -3706,6 +3790,19 @@ winetricks_settings_menu()
--width ${WINETRICKS_MENU_WIDTH} \ --width ${WINETRICKS_MENU_WIDTH} \
" "
;; ;;
fr*) printf %s "zenity \
--title '${_W_msg_title}' \
--text '${_W_msg_body}' \
--cancel-label 'Revenir en arrière' \
--list \
--checklist \
--column '' \
--column Paramètres \
--column Titre \
--height ${WINETRICKS_MENU_HEIGHT} \
--width ${WINETRICKS_MENU_WIDTH} \
"
;;
pl*) printf %s "zenity \ pl*) printf %s "zenity \
--title '${_W_msg_title}' \ --title '${_W_msg_title}' \
--text '${_W_msg_body}' \ --text '${_W_msg_body}' \
@@ -3867,6 +3964,10 @@ winetricks_showmenu()
_W_msg_body='Welche Paket(e) möchten Sie installieren?' _W_msg_body='Welche Paket(e) möchten Sie installieren?'
_W_cached="gecached" _W_cached="gecached"
;; ;;
fr*) _W_msg_title="Winetricks - Le préfixe actuel est \"${WINEPREFIX}\""
_W_msg_body="Quel(s) paquet(s) souhaitez-vous installer ?"
_W_cached="mis en cache"
;;
pl*) _W_msg_title="Winetricks - obecny prefiks to \"${WINEPREFIX}\"" pl*) _W_msg_title="Winetricks - obecny prefiks to \"${WINEPREFIX}\""
_W_msg_body='Które paczki chesz zainstalować?' _W_msg_body='Które paczki chesz zainstalować?'
_W_cached="zarchiwizowane" _W_cached="zarchiwizowane"
@@ -3949,6 +4050,22 @@ winetricks_showmenu()
--width ${WINETRICKS_MENU_WIDTH} \ --width ${WINETRICKS_MENU_WIDTH} \
" "
;; ;;
fr*) printf %s "zenity \
--title '${_W_msg_title}' \
--text '${_W_msg_body}' \
--list \
--checklist \
--column '' \
--column Package \
--column Titre \
--column Éditeur \
--column Année \
--column Media \
--column Status \
--height ${WINETRICKS_MENU_HEIGHT} \
--width ${WINETRICKS_MENU_WIDTH} \
"
;;
pl*) printf %s "zenity \ pl*) printf %s "zenity \
--title '${_W_msg_title}' \ --title '${_W_msg_title}' \
--text '${_W_msg_body}' \ --text '${_W_msg_body}' \
@@ -4289,6 +4406,7 @@ winetricks_list_all()
bg*) _W_cached="кеширано" ; _W_download="за изтегляне" ;; bg*) _W_cached="кеширано" ; _W_download="за изтегляне" ;;
da*) _W_cached="cached" ; _W_download="kan hentes" ;; da*) _W_cached="cached" ; _W_download="kan hentes" ;;
de*) _W_cached="gecached" ; _W_download="herunterladbar";; de*) _W_cached="gecached" ; _W_download="herunterladbar";;
fr*) _W_cached="mis en cache" ; _W_download="téléchargeable";;
pl*) _W_cached="zarchiwizowane" ; _W_download="do pobrania" ;; pl*) _W_cached="zarchiwizowane" ; _W_download="do pobrania" ;;
pt*) _W_cached="em cache" ; _W_download="para download" ;; pt*) _W_cached="em cache" ; _W_download="para download" ;;
ru*) _W_cached="в кэше" ; _W_download="доступно для скачивания" ;; ru*) _W_cached="в кэше" ; _W_download="доступно для скачивания" ;;
@@ -4487,7 +4605,7 @@ winetricks_set_wineprefix()
_W_wineserver_binary_arch="$(winetricks_get_file_arch "${WINE_BINDIR}/wineserver")" _W_wineserver_binary_arch="$(winetricks_get_file_arch "${WINE_BINDIR}/wineserver")"
fi fi
fi fi
if [ -z "${_W_wineserver_binary_arch}" ]; then if [ -z "${W_OPT_UNATTENDED}" ] && [ -z "${_W_wineserver_binary_arch}" ]; then
w_warn "Unknown file arch of ${WINESERVER_BIN}." w_warn "Unknown file arch of ${WINESERVER_BIN}."
fi fi
@@ -4504,7 +4622,7 @@ winetricks_set_wineprefix()
_W_wine_binary_arch="$(winetricks_get_file_arch "${WINE_BINDIR}/wine")" _W_wine_binary_arch="$(winetricks_get_file_arch "${WINE_BINDIR}/wine")"
fi fi
fi fi
if [ -z "${_W_wine_binary_arch}" ]; then if [ -z "${W_OPT_UNATTENDED}" ] && [ -z "${_W_wine_binary_arch}" ]; then
w_warn "Unknown file arch of ${WINE_BIN}." w_warn "Unknown file arch of ${WINE_BIN}."
fi fi
@@ -4609,6 +4727,7 @@ winetricks_set_wineprefix()
if [ "${W_NO_WIN64_WARNINGS}" = 0 ]; then if [ "${W_NO_WIN64_WARNINGS}" = 0 ]; then
case ${LANG} in case ${LANG} in
bg*) w_warn "Използвате 64-битова папка. Повечето програми са за 32-битова архитектура. Ако възникнат проблеми, моля, използвайте 32-битова папка, преди да ги докладвате." ;; bg*) w_warn "Използвате 64-битова папка. Повечето програми са за 32-битова архитектура. Ако възникнат проблеми, моля, използвайте 32-битова папка, преди да ги докладвате." ;;
fr*) w_warn "Vous utilisez un WINEPREFIX 64 bits. Notez que de nombreux verbes n'installent que des versions 32 bits des paquets. Si vous rencontrez des problèmes, veuillez refaire un test dans un WINEPREFIX 32 bits propre avant de signaler un bug." ;;
ru*) w_warn "Вы используете 64-битный WINEPREFIX. Важно: многие ветки устанавливают только 32-битные версии пакетов. Если у вас возникли проблемы, пожалуйста, проверьте еще раз на чистом 32-битном WINEPREFIX до отправки отчета об ошибке." ;; ru*) w_warn "Вы используете 64-битный WINEPREFIX. Важно: многие ветки устанавливают только 32-битные версии пакетов. Если у вас возникли проблемы, пожалуйста, проверьте еще раз на чистом 32-битном WINEPREFIX до отправки отчета об ошибке." ;;
pt*) w_warn "Você está usando um WINEPREFIX de 64-bit. Observe que muitos casos instalam apenas versões de pacotes de 32-bit. Se você encontrar problemas, teste novamente em um WINEPREFIX limpo de 32-bit antes de relatar um bug." ;; pt*) w_warn "Você está usando um WINEPREFIX de 64-bit. Observe que muitos casos instalam apenas versões de pacotes de 32-bit. Se você encontrar problemas, teste novamente em um WINEPREFIX limpo de 32-bit antes de relatar um bug." ;;
zh_CN*) w_warn "您正在使用 64 位的 WINEPREFIX。请注意许多脚本verbs只安装 32 位版本的软件包。如果遇到问题,请先在干净的 32 位 WINEPREFIX 中重新测试,然后再报告错误。" ;; zh_CN*) w_warn "您正在使用 64 位的 WINEPREFIX。请注意许多脚本verbs只安装 32 位版本的软件包。如果遇到问题,请先在干净的 32 位 WINEPREFIX 中重新测试,然后再报告错误。" ;;
@@ -4721,6 +4840,7 @@ winetricks_annihilate_wineprefix()
case ${LANG} in case ${LANG} in
bg*) w_askpermission "Изтриване на ${WINEPREFIX}, нейните приложения, икони и менюта?" ;; bg*) w_askpermission "Изтриване на ${WINEPREFIX}, нейните приложения, икони и менюта?" ;;
fr*) w_askpermission "Supprimer ${WINEPREFIX}, ses applications, ses icônes et ses éléments de menu ?" ;;
uk*) w_askpermission "Бажаєте видалити '${WINEPREFIX}'?" ;; uk*) w_askpermission "Бажаєте видалити '${WINEPREFIX}'?" ;;
pl*) w_askpermission "Czy na pewno chcesz usunąć prefiks ${WINEPREFIX} i wszystkie jego elementy?" ;; pl*) w_askpermission "Czy na pewno chcesz usunąć prefiks ${WINEPREFIX} i wszystkie jego elementy?" ;;
pt*) w_askpermission "Apagar ${WINEPREFIX}, Estes apps, ícones e ítens do menu?" ;; pt*) w_askpermission "Apagar ${WINEPREFIX}, Estes apps, ícones e ítens do menu?" ;;
@@ -5082,6 +5202,50 @@ arch=32|64 Neues wineprefix mit 32 oder 64 bit erstellen, diese Optio
nicht im Falle des Standard Wineprefix. nicht im Falle des Standard Wineprefix.
prefix=foobar WINEPREFIX=${W_PREFIXES_ROOT}/foobar auswählen prefix=foobar WINEPREFIX=${W_PREFIXES_ROOT}/foobar auswählen
annihilate ALLE DATEIEN UND PROGRAMME IN DIESEM WINEPREFIX Löschen annihilate ALLE DATEIEN UND PROGRAMME IN DIESEM WINEPREFIX Löschen
_EOF_
;;
fr*)
cat <<_EOF_
Utilisation : $0 [options] [commande|verbe|chemin-vers-le-verbe] ...
Exécute les verbes donnés. Chaque verbe installe une application ou modifie un paramètre.
Options :
--country=CC Définit le code pays sur CC et ne détecte pas votre adresse IP
-f, --force Ne vérifie pas si les paquets sont déjà installés
--gui Affiche les diagnostics de l'interface graphique même lorsque l'application est lancée en ligne de commande
--gui=OPT Définit OPT sur kdialog ou zenity pour remplacer le moteur GUI
--isolate Installe chaque application ou jeu dans son propre bottle (WINEPREFIX)
--self-update Mettre à jour cette application vers la dernière version (non Debian)
--update-rollback Annuler la dernière mise à jour automatique
-k, --keep_isos Mettre en cache les images ISO (permet une installation ultérieure sans disque)
--no-clean Ne pas supprimer les répertoires temporaires (utile pendant le débogage)
--optin Activer le rapport des verbes que vous utilisez aux responsables de Winetricks
--optout Désactiver le rapport des verbes que vous utilisez aux responsables de Winetricks
-q, --unattended Ne poser aucune question, installer automatiquement
-r, --ddrescue Réessayer plusieurs fois lors de la mise en cache de disques rayés
-t --torify Exécute les téléchargements sous torify, si disponible
--verify Exécute des tests GUI (automatisés) pour les verbes, si disponibles
-v, --verbose Affiche toutes les commandes au fur et à mesure de leur exécution
-h, --help Affiche ce message et quitte
-V, --version Affiche la version et quitte
Commandes :
list Liste des catégories
list-all Liste toutes les catégories et leurs verbes
apps list Liste les verbes dans la catégorie « applications »
benchmarks list Liste les verbes dans la catégorie « benchmarks »
dlls list Liste les verbes de la catégorie « dlls »
fonts list Liste les verbes de la catégorie « polices »
settings list Liste les verbes de la catégorie « paramètres »
list-cached Liste les verbes mis en cache et prêts à être installés
list-download Liste les verbes qui se téléchargent automatiquement
list-manual-download Liste les verbes qui se téléchargent avec l'aide de l'utilisateur
list-installed Liste les verbes déjà installés
arch=32|64 Crée un préfixe en 32 ou 64 bits, cette option doit être
Spécifiée avant prefix=foobar et ne fonctionnera pas dans le cas du
wineprefix par défaut.
prefix=foobar Sélectionner WINEPREFIX=${W_PREFIXES_ROOT}/foobar
annihilate Supprimer TOUTES LES DONNÉES ET APPLICATIONS CONTENUES DANS CE WINEPREFIX
_EOF_ _EOF_
;; ;;
zh_CN*) zh_CN*)
@@ -5122,6 +5286,7 @@ list-installed 列出已安装的脚本
arch=32|64 创建 32 位或 64 位 wineprefix此选项必须在 prefix=foobar 之前指定,默认 wineprefix 不支持此选项 arch=32|64 创建 32 位或 64 位 wineprefix此选项必须在 prefix=foobar 之前指定,默认 wineprefix 不支持此选项
prefix=foobar 选择 WINEPREFIX 为 ${W_PREFIXES_ROOT}/foobar prefix=foobar 选择 WINEPREFIX 为 ${W_PREFIXES_ROOT}/foobar
annihilate 删除该 WINEPREFIX 内的所有数据和应用程序 annihilate 删除该 WINEPREFIX 内的所有数据和应用程序
_EOF_
;; ;;
*) *)
cat <<_EOF_ cat <<_EOF_
@@ -6452,7 +6617,7 @@ load_dinput8()
#---------------------------------------------------------------- #----------------------------------------------------------------
w_metadata dinputto8 dlls \ w_metadata dinputto8 dlls \
title="A dll module that is designed to improve compatibility in games using DirectInput 1-7 by converting all API calls to their equivalent DirectInput 8 (1.0.78.0)" \ title="A dll module that is designed to improve compatibility in games using DirectInput 1-7 by converting all API calls to their equivalent DirectInput 8 (1.0.92.0)" \
homepage="https://github.com/elishacloud/dinputto8" \ homepage="https://github.com/elishacloud/dinputto8" \
publisher="Elisha Riedlinger" \ publisher="Elisha Riedlinger" \
year="2018" \ year="2018" \
@@ -6463,7 +6628,7 @@ w_metadata dinputto8 dlls \
load_dinputto8() load_dinputto8()
{ {
w_download https://github.com/elishacloud/dinputto8/releases/download/v1.0.78.0/dinput.dll 467f50cac676635ed68658b8be32e1a2cacece37a22bb13e8ec8330706a32ca7 w_download https://github.com/elishacloud/dinputto8/releases/download/v1.0.92.0/dinput.dll 8f1e53a55c66f870b91c1e43a39c02a4a87900cf453776de41643863bdfa00e6
w_try_cp_dll "${W_CACHE}/${W_PACKAGE}/dinput.dll" "${W_SYSTEM32_DLLS}/dinput.dll" w_try_cp_dll "${W_CACHE}/${W_PACKAGE}/dinput.dll" "${W_SYSTEM32_DLLS}/dinput.dll"
w_override_dlls native dinput w_override_dlls native dinput
} }
@@ -7841,6 +8006,86 @@ load_dxvk()
#---------------------------------------------------------------- #----------------------------------------------------------------
# $1 - dxvk async archive name (required)
# $2 - dxvk async version (required)
# $3 - minimum Wine version (required)
# $4 - minimum Vulkan API version (required)
# $5 - [dxgi,][d3d8,][d3d9,][d3d10core,]d3d11 (required)
helper_dxvk_async()
{
_W_package_archive="${1}"
_W_package_version="${2}"
_W_min_wine_version="${3}"
_W_min_vulkan_version="${4}"
_W_dll_overrides="$(echo "${5}" | sed 's/,/ /g')"
# dxvk async repository, for d3d8/d3d9/d3d10/d3d11 support
_W_repository="Ph42oN/dxvk-gplasync"
_W_supported_overrides="dxgi d3d8 d3d9 d3d10core d3d11"
_W_invalid_overrides="$(echo "${_W_dll_overrides}" | awk -vvalid_overrides_regex="$(echo "${_W_supported_overrides}" | sed 's/ /|/g')" '{ gsub(valid_overrides_regex,""); sub("[ ]*",""); print $0 }')"
if [ "${_W_invalid_overrides}" != "" ]; then
w_die "parameter (4) unsupported dll override: '${_W_invalid_overrides}' ; supported dll overrides: ${_W_supported_overrides}"
fi
_W_package_dir="${_W_package_archive%.tar.gz}"
w_warn "Please refer to ${_W_repository#*/} version ${_W_package_version} release notes... See: https://gitlab.com/${_W_repository}/-/releases/v${_W_package_version}"
w_warn "Please refer to current dxvk base graphics driver requirements... See: https://github.com/doitsujin/dxvk/wiki/Driver-support" # The async fork lacks a wiki, and upstream's is most relevant anyway
if w_wine_version_in ",${_W_min_wine_version}" ; then
# shellcheck disable=SC2140
w_warn "${_W_repository#*/} ${_W_package_version} does not support wine version ${_wine_version_stripped} . "\
"${_W_repository#*/} ${_W_package_version} requires wine version ${_W_min_wine_version} (or newer). "\
"Vulkan ${_W_min_vulkan_version} API (or newer) support is recommended."
fi
if [ "${_W_package_archive##*.}" = "zip" ]; then
w_try_unzip "${W_TMP}" "${W_CACHE}/${W_PACKAGE}/${_W_package_archive}"
else
w_try tar -C "${W_TMP}" -zxf "${W_CACHE}/${W_PACKAGE}/${_W_package_archive}"
fi
for _W_dll in ${_W_dll_overrides}; do
w_try mv "${W_TMP}/${_W_package_dir}/x32/${_W_dll}.dll" "${W_SYSTEM32_DLLS}/"
done
if test "${W_ARCH}" = "win64"; then
for _W_dll in ${_W_dll_overrides}; do
w_try mv "${W_TMP}/${_W_package_dir}/x64/${_W_dll}.dll" "${W_SYSTEM64_DLLS}/"
done
fi
# shellcheck disable=SC2086
w_override_dlls native ${_W_dll_overrides}
unset _W_dll _W_dll_overrides _W_invalid_overrides _W_min_vulkan_version _W_min_wine_version \
_W_package_archive _W_package_dir _W_package_version \
_W_repository _W_supported_overrides
}
#----------------------------------------------------------------
w_metadata dxvk_async dlls \
title="DXVK with Async and GPL patches [USE AT OWN RISK IN GAMES WITH ANTICHEAT] (latest)" \
publisher="Ph42oN" \
year="2025" \
media="download" \
installed_file1="${W_SYSTEM32_DLLS_WIN}/d3d8.dll" \
installed_file2="${W_SYSTEM32_DLLS_WIN}/d3d9.dll" \
installed_file3="${W_SYSTEM32_DLLS_WIN}/d3d10core.dll" \
installed_file4="${W_SYSTEM32_DLLS_WIN}/d3d11.dll" \
installed_file5="${W_SYSTEM32_DLLS_WIN}/dxgi.dll"
load_dxvk_async()
{
# https://gitlab.com/Ph42oN/dxvk-gplasync
_W_dxvk_async_version="$(w_get_gitlab_latest_release Ph42oN dxvk-gplasync)"
_W_dxvk_async_version="${_W_dxvk_async_version#v}"
w_linkcheck_ignore=1 w_download "https://gitlab.com/Ph42oN/dxvk-gplasync/-/raw/main/releases/dxvk-gplasync-v${_W_dxvk_async_version}.tar.gz"
helper_dxvk_async "dxvk-gplasync-v${_W_dxvk_async_version}.tar.gz" "${_W_dxvk_async_version}" "7.1" "1.3.204" "dxgi,d3d8,d3d9,d3d10core,d3d11"
unset _W_dxvk_async_version
}
#----------------------------------------------------------------
# $1 - dxvk-nvapi archive name (required) # $1 - dxvk-nvapi archive name (required)
# $2 - minimum Wine version (required) # $2 - minimum Wine version (required)
# $3 - nvapi,[nvapi64] (required) # $3 - nvapi,[nvapi64] (required)
@@ -9648,6 +9893,56 @@ load_dotnetdesktop9()
#---------------------------------------------------------------- #----------------------------------------------------------------
w_metadata dotnet10 dlls \
title="MS .NET Runtime 10.0 LTS" \
publisher="Microsoft" \
year="2025" \
media="download" \
file1="dotnet-runtime-10.0.0-win-x86.exe" \
installed_file1="${W_PROGRAMS_WIN}/dotnet/dotnet.exe"
load_dotnet10()
{
# Official version, see https://dotnet.microsoft.com/en-us/download/dotnet/10.0
w_download https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.0/dotnet-runtime-10.0.0-win-x86.exe 90bc5667c2a35c030a2e964e7083fe8fbdbc461377d27b4f0d9bf7b400d7b982
w_try_cd "${W_CACHE}"/"${W_PACKAGE}"
w_try "${WINE}" "${file1}" ${W_OPT_UNATTENDED:+/quiet}
if [ "${W_ARCH}" = "win64" ]; then
# Also install the 64-bit version
w_download https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.0/dotnet-runtime-10.0.0-win-x64.exe ca2dd25d477174767a55756d9ef92bbda0d3c7ef12cef284a542689b2ba52767
w_try "${WINE}" "dotnet-runtime-10.0.0-win-x64.exe" ${W_OPT_UNATTENDED:+/quiet}
fi
}
#----------------------------------------------------------------
w_metadata dotnetdesktop10 dlls \
title="MS .NET Desktop Runtime 10.0 LTS" \
publisher="Microsoft" \
year="2025" \
media="download" \
file1="windowsdesktop-runtime-10.0.0-win-x86.exe" \
installed_file1="${W_PROGRAMS_WIN}/dotnet/dotnet.exe"
load_dotnetdesktop10()
{
# Official version, see https://dotnet.microsoft.com/en-us/download/dotnet/10.0
w_download https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/10.0.0/windowsdesktop-runtime-10.0.0-win-x86.exe ac38c81fef78c565d6bfbddf49ac5bcca354616176c0108c5f0e23333c3d093a
w_try_cd "${W_CACHE}"/"${W_PACKAGE}"
w_try "${WINE}" "${file1}" ${W_OPT_UNATTENDED:+/quiet}
if [ "${W_ARCH}" = "win64" ]; then
# Also install the 64-bit version
w_download https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/10.0.0/windowsdesktop-runtime-10.0.0-win-x64.exe fc0494eaf529b15f74b10d920784cc618ff0845bebfdfd5d85d585e921157a5c
w_try "${WINE}" "windowsdesktop-runtime-10.0.0-win-x64.exe" ${W_OPT_UNATTENDED:+/quiet}
fi
}
#----------------------------------------------------------------
w_metadata dotnet_verifier dlls \ w_metadata dotnet_verifier dlls \
title="MS .NET Verifier" \ title="MS .NET Verifier" \
publisher="Microsoft" \ publisher="Microsoft" \
@@ -9787,7 +10082,7 @@ load_dsound()
# Don't try to register native dsound; it doesn't export DllRegisterServer(). # Don't try to register native dsound; it doesn't export DllRegisterServer().
#w_try_regsvr32 dsound.dll #w_try_regsvr32 dsound.dll
w_override_dlls native dsound w_override_dlls native,builtin dsound
} }
#---------------------------------------------------------------- #----------------------------------------------------------------
@@ -12734,7 +13029,7 @@ load_vcrun2003()
{ {
# Sadly, I know of no Microsoft URL for these # Sadly, I know of no Microsoft URL for these
# winetricks-test can't handle ${file1} in url since it does a raw parsing :/ # winetricks-test can't handle ${file1} in url since it does a raw parsing :/
w_download https://sourceforge.net/projects/bzflag/files/bzedit%20win32/1.6.5/BZEditW32_1.6.5.exe 84d1bda5dbf814742898a2e1c0e4bc793e9bc1fba4b7a93d59a7ef12bd0fd802 w_download https://downloads.sourceforge.net/project/bzflag/bzedit%20win32/1.6.5/BZEditW32_1.6.5.exe 84d1bda5dbf814742898a2e1c0e4bc793e9bc1fba4b7a93d59a7ef12bd0fd802
w_try_7z "${W_SYSTEM32_DLLS}" "${W_CACHE}/vcrun2003/BZEditW32_1.6.5.exe" "mfc71.dll" "msvcp71.dll" "msvcr71.dll" -y w_try_7z "${W_SYSTEM32_DLLS}" "${W_CACHE}/vcrun2003/BZEditW32_1.6.5.exe" "mfc71.dll" "msvcp71.dll" "msvcr71.dll" -y
} }
@@ -12749,7 +13044,7 @@ w_metadata mfc71 dlls \
load_mfc71() load_mfc71()
{ {
w_download_to vcrun2003 https://sourceforge.net/projects/bzflag/files/bzedit%20win32/1.6.5/BZEditW32_1.6.5.exe 84d1bda5dbf814742898a2e1c0e4bc793e9bc1fba4b7a93d59a7ef12bd0fd802 w_download_to vcrun2003 https://downloads.sourceforge.net/project/bzflag/bzedit%20win32/1.6.5/BZEditW32_1.6.5.exe 84d1bda5dbf814742898a2e1c0e4bc793e9bc1fba4b7a93d59a7ef12bd0fd802
w_try_7z "${W_SYSTEM32_DLLS}" "${W_CACHE}/vcrun2003/BZEditW32_1.6.5.exe" "mfc71.dll" -y w_try_7z "${W_SYSTEM32_DLLS}" "${W_CACHE}/vcrun2003/BZEditW32_1.6.5.exe" "mfc71.dll" -y
} }
@@ -15222,8 +15517,10 @@ w_metadata dxwnd apps \
load_dxwnd() load_dxwnd()
{ {
# 2022/10/02 v2_05_88_build.rar a80ad1246493b3b34fba2131494052423ac298a39592d4e06a685568b829922e # 2022/10/02 v2_05_88_build.rar a80ad1246493b3b34fba2131494052423ac298a39592d4e06a685568b829922e
w_download https://versaweb.dl.sourceforge.net/project/dxwnd/Latest%20build/v2_05_88_build.rar a80ad1246493b3b34fba2131494052423ac298a39592d4e06a685568b829922e w_download https://downloads.sourceforge.net/project/dxwnd/Latest%20build/v2_05_88_build.rar a80ad1246493b3b34fba2131494052423ac298a39592d4e06a685568b829922e
w_try_7z "${W_PROGRAMS_X86_UNIX}"/dxwnd "${W_CACHE}"/"${W_PACKAGE}"/"${file1}" -aoa w_try_mkdir "${W_PROGRAMS_X86_UNIX}"/dxwnd
w_try_cd "${W_PROGRAMS_X86_UNIX}"/dxwnd
w_try_unrar "${W_CACHE}/${W_PACKAGE}/${file1}"
} }
#---------------------------------------------------------------- #----------------------------------------------------------------
@@ -15247,40 +15544,6 @@ load_emu8086()
#---------------------------------------------------------------- #----------------------------------------------------------------
w_metadata ev3 apps \
title="Lego Mindstorms EV3 Home Edition" \
publisher="Lego" \
year="2014" \
media="download" \
file1="LMS-EV3-WIN32-ENUS-01-02-01-full-setup.exe" \
installed_exe1="${W_PROGRAMS_X86_WIN}/LEGO Software/LEGO MINDSTORMS EV3 Home Edition/MindstormsEV3.exe"
load_ev3()
{
if w_workaround_wine_bug 40192 "Installing vcrun2005 as Wine does not have MFC80.dll"; then
w_call vcrun2005
fi
if w_workaround_wine_bug 40193 "Installing IE8 as built-in Gecko is not sufficient"; then
w_call ie8
fi
w_call dotnet40
# 2016/03/22: LMS-EV3-WIN32-ENUS-01-02-01-full-setup.exe c47341f08242f0f6f01996530e7c93bda2d666747ada60ab93fa773a55d40a19
w_download http://esd.lego.com.edgesuite.net/digitaldelivery/mindstorms/6ecda7c2-1189-4816-b2dd-440e22d65814/public/LMS-EV3-WIN32-ENUS-01-02-01-full-setup.exe c47341f08242f0f6f01996530e7c93bda2d666747ada60ab93fa773a55d40a19
w_try_cd "${W_CACHE}"/"${W_PACKAGE}"
w_try "${WINE}" "${file1}" ${W_OPT_UNATTENDED:+/qb /AcceptLicenses yes}
if w_workaround_wine_bug 40729 "Setting override for urlmon.dll to native to avoid crash"; then
w_override_dlls native urlmon
fi
}
#----------------------------------------------------------------
w_metadata firefox apps \ w_metadata firefox apps \
title="Firefox 51.0" \ title="Firefox 51.0" \
publisher="Mozilla" \ publisher="Mozilla" \
@@ -17828,6 +18091,32 @@ w_metadata windowmanagerdecorated=n settings \
#---------------------------------------------------------------- #----------------------------------------------------------------
w_metadata useegl=y settings \
title="Enable EGL (default)"
w_metadata useegl=n settings \
title="Disable EGL, use GLX instead"
load_useegl()
{
case "$1" in
y) arg="Y";;
n) arg="N";;
*) w_die "illegal value $1 for UseEGL";;
esac
echo "Setting UseEGL to ${arg}"
cat > "${W_TMP}"/set-useegl.reg <<_EOF_
REGEDIT4
[HKEY_CURRENT_USER\\Software\\Wine\\X11 Driver]
"UseEGL"="${arg}"
_EOF_
w_try_regedit "${W_TMP}"/set-useegl.reg
}
#----------------------------------------------------------------
w_metadata usetakefocus=y settings \ w_metadata usetakefocus=y settings \
title_bg="Включете UseTakeFocus" \ title_bg="Включете UseTakeFocus" \
title_cz="Aktivovat UseTakeFocus" \ title_cz="Aktivovat UseTakeFocus" \