mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-07 20:27:45 +02:00
345 lines
15 KiB
Python
345 lines
15 KiB
Python
"""Shortcut operation methods for AutomatedPrefixService (Mixin)."""
|
|
from pathlib import Path
|
|
from typing import Optional, Tuple, List, Dict
|
|
import logging
|
|
import os
|
|
import time
|
|
import vdf
|
|
import subprocess
|
|
|
|
from .automated_prefix_shortcuts_cleanup import AutomatedPrefixShortcutsCleanupMixin
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ShortcutOperationsMixin(AutomatedPrefixShortcutsCleanupMixin):
|
|
"""Mixin providing shortcut operation methods for AutomatedPrefixService."""
|
|
|
|
def create_shortcut_with_native_service(self, shortcut_name: str, exe_path: str,
|
|
modlist_install_dir: str, custom_launch_options: str = None,
|
|
download_dir=None) -> Tuple[bool, Optional[int]]:
|
|
"""
|
|
Create a Steam shortcut using the native Steam service (no STL).
|
|
|
|
Args:
|
|
shortcut_name: Name for the shortcut
|
|
exe_path: Path to the executable
|
|
modlist_install_dir: Directory where the modlist is installed
|
|
custom_launch_options: Pre-generated launch options (overrides default generation)
|
|
download_dir: Optional download path; its mountpoint is added to STEAM_COMPAT_MOUNTS
|
|
|
|
Returns:
|
|
(success, unsigned_app_id)
|
|
"""
|
|
logger.info(f"Creating shortcut with native service: {shortcut_name}")
|
|
|
|
try:
|
|
from ..services.native_steam_service import NativeSteamService
|
|
|
|
# Initialize native Steam service
|
|
steam_service = NativeSteamService()
|
|
|
|
# Always compute STEAM_COMPAT_MOUNTS; custom_launch_options replaces %command% but
|
|
# still needs mounts so game assets on other drives are reachable inside the prefix.
|
|
mounts_prefix = ""
|
|
try:
|
|
from ..handlers.path_handler import PathHandler
|
|
path_handler = PathHandler()
|
|
mount_paths = path_handler.get_steam_compat_mount_paths(
|
|
install_dir=modlist_install_dir, download_dir=download_dir
|
|
)
|
|
if mount_paths:
|
|
mounts_prefix = f'STEAM_COMPAT_MOUNTS="{":".join(mount_paths)}"'
|
|
logger.info(f"Generated STEAM_COMPAT_MOUNTS: {mounts_prefix}")
|
|
except Exception as e:
|
|
logger.warning(f"Could not generate STEAM_COMPAT_MOUNTS: {e}")
|
|
|
|
if custom_launch_options:
|
|
launch_options = f"{mounts_prefix} {custom_launch_options}".strip() if mounts_prefix else custom_launch_options
|
|
logger.info(f"Launch options (custom + mounts): {launch_options}")
|
|
elif mounts_prefix:
|
|
launch_options = f'{mounts_prefix} %command%'
|
|
logger.info(f"Launch options (mounts only): {launch_options}")
|
|
else:
|
|
launch_options = "%command%"
|
|
|
|
# Get user's preferred Proton version (with Lorerim-specific override)
|
|
proton_version = self._get_user_proton_version(shortcut_name)
|
|
|
|
# Create shortcut with Proton using native service
|
|
success, app_id = steam_service.create_shortcut_with_proton(
|
|
app_name=shortcut_name,
|
|
exe_path=exe_path,
|
|
start_dir=modlist_install_dir,
|
|
launch_options=launch_options,
|
|
tags=["Jackify"],
|
|
proton_version=proton_version
|
|
)
|
|
|
|
if success and app_id:
|
|
logger.info(f" Native Steam service created shortcut successfully with AppID: {app_id}")
|
|
return True, app_id
|
|
else:
|
|
logger.error("Native Steam service failed to create shortcut")
|
|
return False, None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating shortcut with native service: {e}")
|
|
return False, None
|
|
|
|
def verify_shortcut_created(self, shortcut_name: str) -> Optional[int]:
|
|
"""
|
|
Verify the shortcut was created and get its AppID.
|
|
|
|
Args:
|
|
shortcut_name: Name of the shortcut to look for
|
|
|
|
Returns:
|
|
AppID if found, None otherwise
|
|
"""
|
|
try:
|
|
shortcuts_path = self._get_shortcuts_path()
|
|
if not shortcuts_path:
|
|
return None
|
|
|
|
with open(shortcuts_path, 'rb') as f:
|
|
shortcuts_data = vdf.binary_load(f)
|
|
|
|
shortcuts = shortcuts_data.get('shortcuts', {})
|
|
|
|
# Look for our shortcut by name
|
|
for i in range(len(shortcuts)):
|
|
shortcut = shortcuts[str(i)]
|
|
name = shortcut.get('AppName', '')
|
|
|
|
if shortcut_name in name:
|
|
appid = shortcut.get('appid')
|
|
exe_path = shortcut.get('Exe', '').strip('"')
|
|
|
|
logger.info(f"Found shortcut: {name}")
|
|
logger.info(f" AppID: {appid}")
|
|
logger.info(f" Exe: {exe_path}")
|
|
logger.info(f" CompatTool: {shortcut.get('CompatTool', 'NOT_SET')}")
|
|
|
|
return appid
|
|
|
|
logger.error(f"Shortcut '{shortcut_name}' not found")
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error reading shortcuts: {e}")
|
|
return None
|
|
|
|
def create_shortcut_directly(self, shortcut_name: str, exe_path: str, modlist_install_dir: str) -> bool:
|
|
"""
|
|
Create a Steam shortcut directly by modifying shortcuts.vdf.
|
|
This is a fallback when STL fails.
|
|
|
|
Args:
|
|
shortcut_name: Name for the shortcut
|
|
exe_path: Path to the executable
|
|
modlist_install_dir: Directory where the modlist is installed
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
try:
|
|
logger.debug(f"[DEBUG] create_shortcut_directly called for '{shortcut_name}' - this is the fallback method")
|
|
shortcuts_path = self._get_shortcuts_path()
|
|
if not shortcuts_path:
|
|
logger.debug("[DEBUG] No shortcuts path found")
|
|
return False
|
|
|
|
# Read current shortcuts
|
|
with open(shortcuts_path, 'rb') as f:
|
|
shortcuts_data = vdf.binary_load(f)
|
|
|
|
shortcuts = shortcuts_data.get('shortcuts', {})
|
|
|
|
# Find the next available index
|
|
next_index = str(len(shortcuts))
|
|
|
|
# Calculate AppID for the new shortcut (negative for non-Steam shortcuts)
|
|
import hashlib
|
|
app_name_bytes = shortcut_name.encode('utf-8')
|
|
exe_bytes = exe_path.encode('utf-8')
|
|
combined = app_name_bytes + exe_bytes
|
|
hash_value = int(hashlib.md5(combined).hexdigest()[:8], 16)
|
|
appid = -(hash_value & 0x7FFFFFFF) # Make it negative and within 32-bit range
|
|
|
|
# Create new shortcut entry
|
|
new_shortcut = {
|
|
'AppName': shortcut_name,
|
|
'Exe': f'"{exe_path}"',
|
|
'StartDir': f'"{modlist_install_dir}"',
|
|
'appid': appid,
|
|
'icon': '',
|
|
'ShortcutPath': '',
|
|
'LaunchOptions': '',
|
|
'IsHidden': 0,
|
|
'AllowDesktopConfig': 1,
|
|
'AllowOverlay': 1,
|
|
'openvr': 0,
|
|
'Devkit': 0,
|
|
'DevkitGameID': '',
|
|
'LastPlayTime': 0,
|
|
'FlatpakAppID': '',
|
|
'tags': {},
|
|
'CompatTool': 'proton_experimental', # Set Proton Experimental
|
|
'IsInstalled': 1 # Make it appear in "Locally Installed" filter
|
|
}
|
|
|
|
# Add the new shortcut
|
|
shortcuts[next_index] = new_shortcut
|
|
|
|
# Write back to file
|
|
with open(shortcuts_path, 'wb') as f:
|
|
vdf.binary_dump(shortcuts_data, f)
|
|
|
|
logger.info(f"Created shortcut directly: {shortcut_name}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating shortcut directly: {e}")
|
|
return False
|
|
|
|
def modify_shortcut_to_final_exe(self, shortcut_name: str, final_exe_path: str,
|
|
final_start_dir: str) -> bool:
|
|
"""
|
|
Update the existing batch file shortcut to point to the final executable.
|
|
This preserves the AppID and prefix association while changing the target.
|
|
|
|
Args:
|
|
shortcut_name: Name of the shortcut to modify
|
|
final_exe_path: Path to the final executable (e.g., ModOrganizer.exe)
|
|
final_start_dir: Start directory for the executable
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
try:
|
|
shortcuts_path = self._get_shortcuts_path()
|
|
if not shortcuts_path:
|
|
return False
|
|
|
|
# Read current shortcuts
|
|
with open(shortcuts_path, 'rb') as f:
|
|
shortcuts_data = vdf.binary_load(f)
|
|
|
|
shortcuts = shortcuts_data.get('shortcuts', {})
|
|
|
|
# Find the batch file shortcut that created the prefix
|
|
logger.info(f"Looking for batch file shortcut '{shortcut_name}' among {len(shortcuts)} shortcuts...")
|
|
target_shortcut = None
|
|
target_index = None
|
|
|
|
for i in range(len(shortcuts)):
|
|
shortcut = shortcuts[str(i)]
|
|
name = shortcut.get('AppName', '')
|
|
exe = shortcut.get('Exe', '')
|
|
|
|
# Find the specific shortcut that points to our batch file (handle quoted paths)
|
|
if (name == shortcut_name and
|
|
exe and 'prefix_creation_' in exe and (exe.endswith('.bat') or exe.endswith('.bat"'))):
|
|
target_shortcut = shortcut
|
|
target_index = str(i)
|
|
logger.info(f"Found batch file shortcut '{shortcut_name}' at index {i}")
|
|
logger.info(f" Current Exe: {exe}")
|
|
logger.info(f" Current StartDir: {shortcut.get('StartDir', '')}")
|
|
logger.info(f" Current CompatTool: {shortcut.get('CompatTool', 'NOT_SET')}")
|
|
logger.info(f" AppID: {shortcut.get('appid', 'NOT_SET')}")
|
|
break
|
|
|
|
if target_shortcut is None:
|
|
logger.error(f"No batch file shortcut found with name '{shortcut_name}'")
|
|
# Debug: show all available shortcuts
|
|
logger.debug("Available shortcuts:")
|
|
for i in range(len(shortcuts)):
|
|
shortcut = shortcuts[str(i)]
|
|
name = shortcut.get('AppName', '')
|
|
exe = shortcut.get('Exe', '')
|
|
logger.debug(f" [{i}] {name} -> {exe}")
|
|
return False
|
|
|
|
# Update the existing shortcut IN-PLACE (preserves AppID and all other fields)
|
|
logger.info(f"Updating shortcut at index {target_index} IN-PLACE...")
|
|
|
|
# Only change Exe and StartDir - preserve everything else including AppID
|
|
old_exe = target_shortcut.get('Exe', '')
|
|
old_start_dir = target_shortcut.get('StartDir', '')
|
|
|
|
target_shortcut['Exe'] = f'"{final_exe_path}"'
|
|
target_shortcut['StartDir'] = f'"{final_start_dir}"'
|
|
|
|
# Ensure CompatTool is set (STL should have set this, but make sure)
|
|
if not target_shortcut.get('CompatTool', '').strip():
|
|
target_shortcut['CompatTool'] = 'proton_experimental'
|
|
logger.info("Set CompatTool to proton_experimental (was not set)")
|
|
|
|
logger.info(f" Updated shortcut '{shortcut_name}' at index {target_index}:")
|
|
logger.info(f" Exe: {old_exe} → {target_shortcut['Exe']}")
|
|
logger.info(f" StartDir: {old_start_dir} → {target_shortcut['StartDir']}")
|
|
logger.info(f" AppID: {target_shortcut.get('appid', 'NOT_SET')} (preserved)")
|
|
logger.info(f" CompatTool: {target_shortcut.get('CompatTool', 'NOT_SET')} (preserved)")
|
|
|
|
# Write back to file
|
|
with open(shortcuts_path, 'wb') as f:
|
|
vdf.binary_dump(shortcuts_data, f)
|
|
|
|
logger.info(" Shortcut updated successfully - no duplicates created")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error modifying shortcut: {e}")
|
|
return False
|
|
|
|
def verify_final_shortcut(self, shortcut_name: str, expected_exe_path: str) -> bool:
|
|
"""
|
|
Verify the shortcut now points to the final executable.
|
|
|
|
Args:
|
|
shortcut_name: Name of the shortcut to verify
|
|
expected_exe_path: Expected executable path
|
|
|
|
Returns:
|
|
True if shortcut is correct, False otherwise
|
|
"""
|
|
try:
|
|
shortcuts_path = self._get_shortcuts_path()
|
|
if not shortcuts_path:
|
|
return False
|
|
|
|
with open(shortcuts_path, 'rb') as f:
|
|
shortcuts_data = vdf.binary_load(f)
|
|
|
|
shortcuts = shortcuts_data.get('shortcuts', {})
|
|
|
|
# Find our shortcut
|
|
for i in range(len(shortcuts)):
|
|
shortcut = shortcuts[str(i)]
|
|
name = shortcut.get('AppName', '')
|
|
|
|
if shortcut_name in name:
|
|
exe_path = shortcut.get('Exe', '')
|
|
start_dir = shortcut.get('StartDir', '')
|
|
|
|
logger.info(f"Final shortcut configuration:")
|
|
logger.info(f" Name: {name}")
|
|
logger.info(f" Exe: {exe_path}")
|
|
logger.info(f" StartDir: {start_dir}")
|
|
|
|
# Verify it points to the final executable
|
|
if expected_exe_path in exe_path:
|
|
logger.info("Shortcut correctly points to final executable")
|
|
return True
|
|
else:
|
|
logger.error("Shortcut does not point to final executable")
|
|
return False
|
|
|
|
logger.error(f"Shortcut '{shortcut_name}' not found")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error reading shortcuts: {e}")
|
|
return False
|
|
|