mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Sync from development - prepare for v0.1.5.3
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
|||||||
# Jackify Changelog
|
# Jackify Changelog
|
||||||
|
|
||||||
|
## v0.1.5.3 - Critical Bug Fixes
|
||||||
|
**Release Date:** October 2, 2025
|
||||||
|
|
||||||
|
### Critical Bug Fixes
|
||||||
|
- **Fixed Multi-User Steam Detection**: Properly reads loginusers.vdf and converts SteamID64 to SteamID3 for accurate user identification
|
||||||
|
- **Fixed dotnet40 Installation Failures**: Hybrid approach uses protontricks for dotnet40 (reliable), winetricks for other components (fast)
|
||||||
|
- **Fixed dotnet8 Installation**: Now properly handled by winetricks instead of unimplemented pass statement
|
||||||
|
- **Fixed D: Drive Detection**: SD card detection now only applies to Steam Deck systems, not regular Linux systems
|
||||||
|
- **Fixed SD Card Mount Patterns**: Replaced hardcoded mmcblk0p1 references with dynamic path detection
|
||||||
|
- **Fixed Debug Restart UX**: Replaced PyInstaller detection with AppImage detection for proper restart behavior
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.1.5.2 - Proton Configuration & Engine Updates
|
## v0.1.5.2 - Proton Configuration & Engine Updates
|
||||||
**Release Date:** September 30, 2025
|
**Release Date:** September 30, 2025
|
||||||
|
|
||||||
|
|||||||
@@ -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.1.5.2"
|
__version__ = "0.1.5.3"
|
||||||
|
|||||||
@@ -315,13 +315,15 @@ class ModlistHandler:
|
|||||||
self.modlist_dir = Path(modlist_dir_path_str)
|
self.modlist_dir = Path(modlist_dir_path_str)
|
||||||
self.modlist_ini = modlist_ini_path
|
self.modlist_ini = modlist_ini_path
|
||||||
|
|
||||||
# Determine if modlist is on SD card
|
# Determine if modlist is on SD card (Steam Deck only)
|
||||||
# Use str() for startswith check
|
# On non-Steam Deck systems, /media mounts should use Z: drive, not D: drive
|
||||||
if str(self.modlist_dir).startswith("/run/media") or str(self.modlist_dir).startswith("/media"):
|
if (str(self.modlist_dir).startswith("/run/media") or str(self.modlist_dir).startswith("/media")) and self.steamdeck:
|
||||||
self.modlist_sdcard = True
|
self.modlist_sdcard = True
|
||||||
self.logger.info("Modlist appears to be on an SD card.")
|
self.logger.info("Modlist appears to be on an SD card (Steam Deck).")
|
||||||
else:
|
else:
|
||||||
self.modlist_sdcard = False
|
self.modlist_sdcard = False
|
||||||
|
if (str(self.modlist_dir).startswith("/run/media") or str(self.modlist_dir).startswith("/media")) and not self.steamdeck:
|
||||||
|
self.logger.info("Modlist on /media mount detected on non-Steam Deck system - using Z: drive mapping.")
|
||||||
|
|
||||||
# Find and set compatdata path now that we have appid
|
# Find and set compatdata path now that we have appid
|
||||||
# Ensure PathHandler is available (should be initialized in __init__)
|
# Ensure PathHandler is available (should be initialized in __init__)
|
||||||
|
|||||||
@@ -653,41 +653,7 @@ class PathHandler:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error using multi-user detection for shortcuts.vdf: {e}")
|
logger.error(f"Error using multi-user detection for shortcuts.vdf: {e}")
|
||||||
|
return None
|
||||||
# Fallback to legacy behavior if multi-user detection fails
|
|
||||||
logger.warning("Falling back to legacy shortcuts.vdf detection (first-found user)")
|
|
||||||
userdata_base_paths = [
|
|
||||||
os.path.expanduser("~/.steam/steam/userdata"),
|
|
||||||
os.path.expanduser("~/.local/share/Steam/userdata"),
|
|
||||||
os.path.expanduser("~/.var/app/com.valvesoftware.Steam/.local/share/Steam/userdata")
|
|
||||||
]
|
|
||||||
found_vdf_path = None
|
|
||||||
for base_path in userdata_base_paths:
|
|
||||||
if not os.path.isdir(base_path):
|
|
||||||
logger.debug(f"Userdata base path not found or not a directory: {base_path}")
|
|
||||||
continue
|
|
||||||
logger.debug(f"Searching for user IDs in: {base_path}")
|
|
||||||
try:
|
|
||||||
for item in os.listdir(base_path):
|
|
||||||
user_path = os.path.join(base_path, item)
|
|
||||||
if os.path.isdir(user_path) and item.isdigit():
|
|
||||||
logger.debug(f"Checking user directory: {user_path}")
|
|
||||||
config_path = os.path.join(user_path, "config")
|
|
||||||
shortcuts_file = os.path.join(config_path, "shortcuts.vdf")
|
|
||||||
if os.path.isfile(shortcuts_file):
|
|
||||||
logger.info(f"Found shortcuts.vdf at: {shortcuts_file}")
|
|
||||||
found_vdf_path = shortcuts_file
|
|
||||||
break # Found it for this base path
|
|
||||||
else:
|
|
||||||
logger.debug(f"shortcuts.vdf not found in {config_path}")
|
|
||||||
except OSError as e:
|
|
||||||
logger.warning(f"Could not access directory {base_path}: {e}")
|
|
||||||
continue # Try next base path
|
|
||||||
if found_vdf_path:
|
|
||||||
break # Found it in this base path
|
|
||||||
if not found_vdf_path:
|
|
||||||
logger.error("Could not find any shortcuts.vdf file in common Steam locations.")
|
|
||||||
return found_vdf_path
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_game_install_paths(target_appids: Dict[str, str]) -> Dict[str, Path]:
|
def find_game_install_paths(target_appids: Dict[str, str]) -> Dict[str, Path]:
|
||||||
|
|||||||
@@ -537,10 +537,7 @@ class WineUtils:
|
|||||||
if "mods" in binary_path:
|
if "mods" in binary_path:
|
||||||
# mods path type found
|
# mods path type found
|
||||||
if modlist_sdcard:
|
if modlist_sdcard:
|
||||||
path_middle = modlist_dir.split('mmcblk0p1', 1)[1] if 'mmcblk0p1' in modlist_dir else modlist_dir
|
path_middle = WineUtils._strip_sdcard_path(modlist_dir)
|
||||||
# Strip /run/media/deck/UUID if present
|
|
||||||
if '/run/media/' in path_middle:
|
|
||||||
path_middle = '/' + path_middle.split('/run/media/', 1)[1].split('/', 2)[2]
|
|
||||||
else:
|
else:
|
||||||
path_middle = modlist_dir
|
path_middle = modlist_dir
|
||||||
|
|
||||||
@@ -550,10 +547,7 @@ class WineUtils:
|
|||||||
elif any(x in binary_path for x in ["Stock Game", "Game Root", "STOCK GAME", "Stock Game Folder", "Stock Folder", "Skyrim Stock", "root/Skyrim Special Edition"]):
|
elif any(x in binary_path for x in ["Stock Game", "Game Root", "STOCK GAME", "Stock Game Folder", "Stock Folder", "Skyrim Stock", "root/Skyrim Special Edition"]):
|
||||||
# Stock/Game Root found
|
# Stock/Game Root found
|
||||||
if modlist_sdcard:
|
if modlist_sdcard:
|
||||||
path_middle = modlist_dir.split('mmcblk0p1', 1)[1] if 'mmcblk0p1' in modlist_dir else modlist_dir
|
path_middle = WineUtils._strip_sdcard_path(modlist_dir)
|
||||||
# Strip /run/media/deck/UUID if present
|
|
||||||
if '/run/media/' in path_middle:
|
|
||||||
path_middle = '/' + path_middle.split('/run/media/', 1)[1].split('/', 2)[2]
|
|
||||||
else:
|
else:
|
||||||
path_middle = modlist_dir
|
path_middle = modlist_dir
|
||||||
|
|
||||||
@@ -589,7 +583,7 @@ class WineUtils:
|
|||||||
elif "steamapps" in binary_path:
|
elif "steamapps" in binary_path:
|
||||||
# Steamapps found
|
# Steamapps found
|
||||||
if basegame_sdcard:
|
if basegame_sdcard:
|
||||||
path_middle = steam_library.split('mmcblk0p1', 1)[1] if 'mmcblk0p1' in steam_library else steam_library
|
path_middle = WineUtils._strip_sdcard_path(steam_library)
|
||||||
drive_letter = "D:"
|
drive_letter = "D:"
|
||||||
else:
|
else:
|
||||||
path_middle = steam_library.split('steamapps', 1)[0] if 'steamapps' in steam_library else steam_library
|
path_middle = steam_library.split('steamapps', 1)[0] if 'steamapps' in steam_library else steam_library
|
||||||
|
|||||||
@@ -257,10 +257,10 @@ class WinetricksHandler:
|
|||||||
components_to_install = self._reorder_components_for_installation(all_components)
|
components_to_install = self._reorder_components_for_installation(all_components)
|
||||||
self.logger.info(f"WINEPREFIX: {wineprefix}, Game: {game_var}, Ordered Components: {components_to_install}")
|
self.logger.info(f"WINEPREFIX: {wineprefix}, Game: {game_var}, Ordered Components: {components_to_install}")
|
||||||
|
|
||||||
# Install components separately if dotnet40 is present (mimics protontricks behavior)
|
# Hybrid approach: Use protontricks for dotnet40 only, winetricks for everything else
|
||||||
if "dotnet40" in components_to_install:
|
if "dotnet40" in components_to_install:
|
||||||
self.logger.info("dotnet40 detected - installing components separately like protontricks")
|
self.logger.info("dotnet40 detected - using hybrid approach: protontricks for dotnet40, winetricks for others")
|
||||||
return self._install_components_separately(components_to_install, wineprefix, wine_binary, env)
|
return self._install_components_hybrid_approach(components_to_install, wineprefix, game_var)
|
||||||
|
|
||||||
# For non-dotnet40 installations, install all components together (faster)
|
# For non-dotnet40 installations, install all components together (faster)
|
||||||
max_attempts = 3
|
max_attempts = 3
|
||||||
@@ -482,6 +482,225 @@ class WinetricksHandler:
|
|||||||
self.logger.info("✓ All components installed successfully using separate sessions")
|
self.logger.info("✓ All components installed successfully using separate sessions")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _install_components_hybrid_approach(self, components: list, wineprefix: str, game_var: str) -> bool:
|
||||||
|
"""
|
||||||
|
Hybrid approach: Install dotnet40 with protontricks (known to work),
|
||||||
|
then install remaining components with winetricks (faster for other components).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
components: List of all components to install
|
||||||
|
wineprefix: Wine prefix path
|
||||||
|
game_var: Game variable for AppID detection
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if all installations succeeded, False otherwise
|
||||||
|
"""
|
||||||
|
self.logger.info("Starting hybrid installation approach")
|
||||||
|
|
||||||
|
# Separate dotnet40 (protontricks) from other components (winetricks)
|
||||||
|
protontricks_components = [comp for comp in components if comp == "dotnet40"]
|
||||||
|
other_components = [comp for comp in components if comp != "dotnet40"]
|
||||||
|
|
||||||
|
self.logger.info(f"Protontricks components: {protontricks_components}")
|
||||||
|
self.logger.info(f"Other components: {other_components}")
|
||||||
|
|
||||||
|
# Step 1: Install dotnet40 with protontricks if present
|
||||||
|
if protontricks_components:
|
||||||
|
self.logger.info(f"Installing {protontricks_components} using protontricks...")
|
||||||
|
if not self._install_dotnet40_with_protontricks(wineprefix, game_var):
|
||||||
|
self.logger.error(f"Failed to install {protontricks_components} with protontricks")
|
||||||
|
return False
|
||||||
|
self.logger.info(f"✓ {protontricks_components} installation completed successfully with protontricks")
|
||||||
|
|
||||||
|
# Step 2: Install remaining components with winetricks if any
|
||||||
|
if other_components:
|
||||||
|
self.logger.info(f"Installing remaining components with winetricks: {other_components}")
|
||||||
|
|
||||||
|
# Use existing winetricks logic for other components
|
||||||
|
env = self._prepare_winetricks_environment(wineprefix)
|
||||||
|
if not env:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self._install_components_with_winetricks(other_components, wineprefix, env)
|
||||||
|
|
||||||
|
self.logger.info("✓ Hybrid component installation completed successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _install_dotnet40_with_protontricks(self, wineprefix: str, game_var: str) -> bool:
|
||||||
|
"""
|
||||||
|
Install dotnet40 using protontricks (known to work reliably).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wineprefix: Wine prefix path
|
||||||
|
game_var: Game variable for AppID detection
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if installation succeeded, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Extract AppID from wineprefix path (e.g., /path/to/compatdata/123456789/pfx -> 123456789)
|
||||||
|
appid = None
|
||||||
|
if 'compatdata' in wineprefix:
|
||||||
|
# Standard Steam compatdata structure
|
||||||
|
path_parts = Path(wineprefix).parts
|
||||||
|
for i, part in enumerate(path_parts):
|
||||||
|
if part == 'compatdata' and i + 1 < len(path_parts):
|
||||||
|
potential_appid = path_parts[i + 1]
|
||||||
|
if potential_appid.isdigit():
|
||||||
|
appid = potential_appid
|
||||||
|
break
|
||||||
|
|
||||||
|
if not appid:
|
||||||
|
self.logger.error(f"Could not extract AppID from wineprefix path: {wineprefix}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.logger.info(f"Using AppID {appid} for protontricks dotnet40 installation")
|
||||||
|
|
||||||
|
# Import and use protontricks handler
|
||||||
|
from .protontricks_handler import ProtontricksHandler
|
||||||
|
|
||||||
|
# Determine if we're on Steam Deck (for protontricks handler)
|
||||||
|
steamdeck = os.path.exists('/home/deck')
|
||||||
|
|
||||||
|
protontricks_handler = ProtontricksHandler(steamdeck=steamdeck, logger=self.logger)
|
||||||
|
|
||||||
|
# Detect protontricks availability
|
||||||
|
if not protontricks_handler.detect_protontricks():
|
||||||
|
self.logger.error("Protontricks not available for dotnet40 installation")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Install dotnet40 using protontricks
|
||||||
|
success = protontricks_handler.install_wine_components(appid, game_var, ["dotnet40"])
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.logger.info("✓ dotnet40 installed successfully with protontricks")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.logger.error("✗ dotnet40 installation failed with protontricks")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error installing dotnet40 with protontricks: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _prepare_winetricks_environment(self, wineprefix: str) -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
Prepare the environment for winetricks installation.
|
||||||
|
This reuses the existing environment setup logic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wineprefix: Wine prefix path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Environment variables for winetricks, or None if failed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
env = os.environ.copy()
|
||||||
|
env['WINEDEBUG'] = '-all'
|
||||||
|
env['WINEPREFIX'] = wineprefix
|
||||||
|
env['WINETRICKS_GUI'] = 'none'
|
||||||
|
|
||||||
|
# Existing Proton detection logic
|
||||||
|
from ..handlers.config_handler import ConfigHandler
|
||||||
|
from ..handlers.wine_utils import WineUtils
|
||||||
|
|
||||||
|
config = ConfigHandler()
|
||||||
|
user_proton_path = config.get_proton_path()
|
||||||
|
|
||||||
|
wine_binary = None
|
||||||
|
if user_proton_path != 'auto':
|
||||||
|
if os.path.exists(user_proton_path):
|
||||||
|
resolved_proton_path = os.path.realpath(user_proton_path)
|
||||||
|
valve_proton_wine = os.path.join(resolved_proton_path, 'dist', 'bin', 'wine')
|
||||||
|
ge_proton_wine = os.path.join(resolved_proton_path, 'files', 'bin', 'wine')
|
||||||
|
|
||||||
|
if os.path.exists(valve_proton_wine):
|
||||||
|
wine_binary = valve_proton_wine
|
||||||
|
elif os.path.exists(ge_proton_wine):
|
||||||
|
wine_binary = ge_proton_wine
|
||||||
|
|
||||||
|
if not wine_binary:
|
||||||
|
best_proton = WineUtils.select_best_proton()
|
||||||
|
if best_proton:
|
||||||
|
wine_binary = WineUtils.find_proton_binary(best_proton['name'])
|
||||||
|
|
||||||
|
if not wine_binary or not (os.path.exists(wine_binary) and os.access(wine_binary, os.X_OK)):
|
||||||
|
self.logger.error(f"Cannot prepare winetricks environment: No compatible Proton found")
|
||||||
|
return None
|
||||||
|
|
||||||
|
env['WINE'] = str(wine_binary)
|
||||||
|
|
||||||
|
# Set up protontricks-compatible environment (existing logic)
|
||||||
|
proton_dist_path = os.path.dirname(os.path.dirname(wine_binary))
|
||||||
|
env['WINEDLLPATH'] = f"{proton_dist_path}/lib64/wine:{proton_dist_path}/lib/wine"
|
||||||
|
env['PATH'] = f"{proton_dist_path}/bin:{env.get('PATH', '')}"
|
||||||
|
|
||||||
|
# Existing DLL overrides
|
||||||
|
dll_overrides = {
|
||||||
|
"beclient": "b,n", "beclient_x64": "b,n", "dxgi": "n", "d3d9": "n",
|
||||||
|
"d3d10core": "n", "d3d11": "n", "d3d12": "n", "d3d12core": "n",
|
||||||
|
"nvapi": "n", "nvapi64": "n", "nvofapi64": "n", "nvcuda": "b"
|
||||||
|
}
|
||||||
|
|
||||||
|
env['WINEDLLOVERRIDES'] = ';'.join(f"{name}={setting}" for name, setting in dll_overrides.items())
|
||||||
|
env['WINE_LARGE_ADDRESS_AWARE'] = '1'
|
||||||
|
env['DXVK_ENABLE_NVAPI'] = '1'
|
||||||
|
|
||||||
|
# Set up winetricks cache
|
||||||
|
from jackify.shared.paths import get_jackify_data_dir
|
||||||
|
jackify_cache_dir = get_jackify_data_dir() / 'winetricks_cache'
|
||||||
|
jackify_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
env['WINETRICKS_CACHE'] = str(jackify_cache_dir)
|
||||||
|
|
||||||
|
return env
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to prepare winetricks environment: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _install_components_with_winetricks(self, components: list, wineprefix: str, env: dict) -> bool:
|
||||||
|
"""
|
||||||
|
Install components using winetricks with the prepared environment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
components: List of components to install
|
||||||
|
wineprefix: Wine prefix path
|
||||||
|
env: Prepared environment variables
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if installation succeeded, False otherwise
|
||||||
|
"""
|
||||||
|
max_attempts = 3
|
||||||
|
for attempt in range(1, max_attempts + 1):
|
||||||
|
if attempt > 1:
|
||||||
|
self.logger.warning(f"Retrying winetricks installation (attempt {attempt}/{max_attempts})")
|
||||||
|
self._cleanup_wine_processes()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = [self.winetricks_path, '--unattended'] + components
|
||||||
|
self.logger.debug(f"Running winetricks: {' '.join(cmd)}")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
env=env,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
timeout=600
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
self.logger.info(f"✓ Winetricks components installed successfully: {components}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.logger.error(f"✗ Winetricks failed (attempt {attempt}): {result.stderr.strip()}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error during winetricks run (attempt {attempt}): {e}")
|
||||||
|
|
||||||
|
self.logger.error(f"Failed to install components with winetricks after {max_attempts} attempts")
|
||||||
|
return False
|
||||||
|
|
||||||
def _cleanup_wine_processes(self):
|
def _cleanup_wine_processes(self):
|
||||||
"""
|
"""
|
||||||
Internal method to clean up wine processes during component installation
|
Internal method to clean up wine processes during component installation
|
||||||
|
|||||||
@@ -30,100 +30,83 @@ class NativeSteamService:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.steam_path = Path.home() / ".steam" / "steam"
|
self.steam_paths = [
|
||||||
self.userdata_path = self.steam_path / "userdata"
|
Path.home() / ".steam" / "steam",
|
||||||
|
Path.home() / ".local" / "share" / "Steam",
|
||||||
|
Path.home() / ".var" / "app" / "com.valvesoftware.Steam" / ".local" / "share" / "Steam"
|
||||||
|
]
|
||||||
|
self.steam_path = None
|
||||||
|
self.userdata_path = None
|
||||||
self.user_id = None
|
self.user_id = None
|
||||||
self.user_config_path = None
|
self.user_config_path = None
|
||||||
|
|
||||||
def find_steam_user(self) -> bool:
|
def find_steam_user(self) -> bool:
|
||||||
"""Find the active Steam user directory"""
|
"""
|
||||||
|
Find the active Steam user directory using Steam's own configuration files.
|
||||||
|
No more guessing - uses loginusers.vdf to get the most recent user and converts SteamID64 to SteamID3.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
if not self.userdata_path.exists():
|
# Step 1: Find Steam installation using Steam's own file structure
|
||||||
logger.error("Steam userdata directory not found")
|
if not self._find_steam_installation():
|
||||||
|
logger.error("No Steam installation found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Find user directories (excluding user 0 which is a system account)
|
# Step 2: Parse loginusers.vdf to get the most recent user (SteamID64)
|
||||||
user_dirs = [d for d in self.userdata_path.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"]
|
steamid64 = self._get_most_recent_user_from_loginusers()
|
||||||
if not user_dirs:
|
if not steamid64:
|
||||||
logger.error("No valid Steam user directories found (user 0 is not valid for shortcuts)")
|
logger.error("Could not determine most recent Steam user from loginusers.vdf")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Detect the correct Steam user
|
# Step 3: Convert SteamID64 to SteamID3 (userdata directory format)
|
||||||
user_dir = self._detect_active_steam_user(user_dirs)
|
steamid3 = self._convert_steamid64_to_steamid3(steamid64)
|
||||||
if not user_dir:
|
logger.info(f"Most recent Steam user: SteamID64={steamid64}, SteamID3={steamid3}")
|
||||||
logger.error("Could not determine active Steam user")
|
|
||||||
|
# Step 4: Verify the userdata directory exists
|
||||||
|
user_dir = self.userdata_path / str(steamid3)
|
||||||
|
if not user_dir.exists():
|
||||||
|
logger.error(f"Userdata directory does not exist: {user_dir}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.user_id = user_dir.name
|
config_dir = user_dir / "config"
|
||||||
self.user_config_path = user_dir / "config"
|
if not config_dir.exists():
|
||||||
|
logger.error(f"User config directory does not exist: {config_dir}")
|
||||||
|
return False
|
||||||
|
|
||||||
logger.info(f"Found Steam user: {self.user_id}")
|
# Step 5: Set up the service state
|
||||||
|
self.user_id = str(steamid3)
|
||||||
|
self.user_config_path = config_dir
|
||||||
|
|
||||||
|
logger.info(f"VERIFIED Steam user: {self.user_id}")
|
||||||
logger.info(f"User config path: {self.user_config_path}")
|
logger.info(f"User config path: {self.user_config_path}")
|
||||||
|
logger.info(f"Shortcuts.vdf will be at: {self.user_config_path / 'shortcuts.vdf'}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error finding Steam user: {e}")
|
logger.error(f"Error finding Steam user: {e}", exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _detect_active_steam_user(self, user_dirs: List[Path]) -> Optional[Path]:
|
def _find_steam_installation(self) -> bool:
|
||||||
|
"""Find Steam installation by checking for config/loginusers.vdf"""
|
||||||
|
for steam_path in self.steam_paths:
|
||||||
|
loginusers_path = steam_path / "config" / "loginusers.vdf"
|
||||||
|
userdata_path = steam_path / "userdata"
|
||||||
|
|
||||||
|
if loginusers_path.exists() and userdata_path.exists():
|
||||||
|
self.steam_path = steam_path
|
||||||
|
self.userdata_path = userdata_path
|
||||||
|
logger.info(f"Found Steam installation at: {steam_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_most_recent_user_from_loginusers(self) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Detect the active Steam user from available user directories.
|
Parse loginusers.vdf to get the SteamID64 of the most recent user.
|
||||||
|
Uses Steam's own MostRecent flag and Timestamp.
|
||||||
Priority:
|
|
||||||
1. Single non-0 user: Use automatically
|
|
||||||
2. Multiple users: Parse loginusers.vdf to find logged-in user
|
|
||||||
3. Fallback: Most recently active user directory
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_dirs: List of valid user directories
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path to the active user directory, or None if detection fails
|
|
||||||
"""
|
|
||||||
if len(user_dirs) == 1:
|
|
||||||
logger.info(f"Single Steam user found: {user_dirs[0].name}")
|
|
||||||
return user_dirs[0]
|
|
||||||
|
|
||||||
logger.info(f"Multiple Steam users found: {[d.name for d in user_dirs]}")
|
|
||||||
|
|
||||||
# Try to parse loginusers.vdf to find logged-in user
|
|
||||||
loginusers_path = self.steam_path / "loginusers.vdf"
|
|
||||||
active_user = self._parse_loginusers_vdf(loginusers_path)
|
|
||||||
|
|
||||||
if active_user:
|
|
||||||
# Find matching user directory
|
|
||||||
for user_dir in user_dirs:
|
|
||||||
if user_dir.name == active_user:
|
|
||||||
logger.info(f"Found logged-in Steam user from loginusers.vdf: {active_user}")
|
|
||||||
return user_dir
|
|
||||||
|
|
||||||
logger.warning(f"Logged-in user {active_user} from loginusers.vdf not found in user directories")
|
|
||||||
|
|
||||||
# Fallback: Use most recently modified user directory
|
|
||||||
try:
|
|
||||||
most_recent = max(user_dirs, key=lambda d: d.stat().st_mtime)
|
|
||||||
logger.info(f"Using most recently active Steam user directory: {most_recent.name}")
|
|
||||||
return most_recent
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error determining most recent user directory: {e}")
|
|
||||||
# Final fallback: Use first user
|
|
||||||
logger.warning(f"Using first user directory as final fallback: {user_dirs[0].name}")
|
|
||||||
return user_dirs[0]
|
|
||||||
|
|
||||||
def _parse_loginusers_vdf(self, loginusers_path: Path) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Parse loginusers.vdf to find the currently logged-in Steam user.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
loginusers_path: Path to loginusers.vdf
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Steam user ID as string, or None if parsing fails
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not loginusers_path.exists():
|
loginusers_path = self.steam_path / "config" / "loginusers.vdf"
|
||||||
logger.debug(f"loginusers.vdf not found at {loginusers_path}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Load VDF data
|
# Load VDF data
|
||||||
vdf_data = VDFHandler.load(str(loginusers_path), binary=False)
|
vdf_data = VDFHandler.load(str(loginusers_path), binary=False)
|
||||||
@@ -133,27 +116,52 @@ class NativeSteamService:
|
|||||||
|
|
||||||
users_section = vdf_data.get("users", {})
|
users_section = vdf_data.get("users", {})
|
||||||
if not users_section:
|
if not users_section:
|
||||||
logger.debug("No users section found in loginusers.vdf")
|
logger.error("No users section found in loginusers.vdf")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Find user marked as logged in
|
most_recent_user = None
|
||||||
for user_id, user_data in users_section.items():
|
most_recent_timestamp = 0
|
||||||
if isinstance(user_data, dict):
|
|
||||||
# Check for indicators of logged-in status
|
|
||||||
if user_data.get("MostRecent") == "1" or user_data.get("WantsOfflineMode") == "0":
|
|
||||||
logger.debug(f"Found most recent/logged-in user: {user_id}")
|
|
||||||
return user_id
|
|
||||||
|
|
||||||
# If no specific user found, try to get the first valid user
|
# Find user with MostRecent=1 or highest timestamp
|
||||||
if users_section:
|
for steamid64, user_data in users_section.items():
|
||||||
first_user = next(iter(users_section.keys()))
|
if isinstance(user_data, dict):
|
||||||
logger.debug(f"No specific logged-in user found, using first user: {first_user}")
|
# Check for MostRecent flag first
|
||||||
return first_user
|
if user_data.get("MostRecent") == "1":
|
||||||
|
logger.info(f"Found user marked as MostRecent: {steamid64}")
|
||||||
|
return steamid64
|
||||||
|
|
||||||
|
# Also track highest timestamp as fallback
|
||||||
|
timestamp = int(user_data.get("Timestamp", "0"))
|
||||||
|
if timestamp > most_recent_timestamp:
|
||||||
|
most_recent_timestamp = timestamp
|
||||||
|
most_recent_user = steamid64
|
||||||
|
|
||||||
|
# Return user with highest timestamp if no MostRecent flag found
|
||||||
|
if most_recent_user:
|
||||||
|
logger.info(f"Found most recent user by timestamp: {most_recent_user}")
|
||||||
|
return most_recent_user
|
||||||
|
|
||||||
|
logger.error("No valid users found in loginusers.vdf")
|
||||||
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error parsing loginusers.vdf: {e}")
|
logger.error(f"Error parsing loginusers.vdf: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _convert_steamid64_to_steamid3(self, steamid64: str) -> int:
|
||||||
|
"""
|
||||||
|
Convert SteamID64 to SteamID3 (used in userdata directory names).
|
||||||
|
Formula: SteamID3 = SteamID64 - 76561197960265728
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
steamid64_int = int(steamid64)
|
||||||
|
steamid3 = steamid64_int - 76561197960265728
|
||||||
|
logger.debug(f"Converted SteamID64 {steamid64} to SteamID3 {steamid3}")
|
||||||
|
return steamid3
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"Invalid SteamID64 format: {steamid64}")
|
||||||
|
raise
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_shortcuts_vdf_path(self) -> Optional[Path]:
|
def get_shortcuts_vdf_path(self) -> Optional[Path]:
|
||||||
"""Get the path to shortcuts.vdf"""
|
"""Get the path to shortcuts.vdf"""
|
||||||
|
|||||||
@@ -636,16 +636,17 @@ class SettingsDialog(QDialog):
|
|||||||
reply = MessageService.question(self, "Restart Required", "Debug mode change requires a restart. Restart Jackify now?", safety_level="low")
|
reply = MessageService.question(self, "Restart Required", "Debug mode change requires a restart. Restart Jackify now?", safety_level="low")
|
||||||
if reply == QMessageBox.Yes:
|
if reply == QMessageBox.Yes:
|
||||||
import os, sys
|
import os, sys
|
||||||
if getattr(sys, 'frozen', False):
|
# User requested restart - do it regardless of execution environment
|
||||||
# PyInstaller bundle: safe to restart
|
self.accept()
|
||||||
self.accept()
|
|
||||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
# Check if running from AppImage
|
||||||
return
|
if os.environ.get('APPIMAGE'):
|
||||||
|
# AppImage: restart the AppImage
|
||||||
|
os.execv(os.environ['APPIMAGE'], [os.environ['APPIMAGE']] + sys.argv[1:])
|
||||||
else:
|
else:
|
||||||
# Dev mode: show message instead of auto-restart
|
# Dev mode: restart the Python module
|
||||||
MessageService.information(self, "Manual Restart Required", "Please restart Jackify manually to apply debug mode changes.", safety_level="low")
|
os.execv(sys.executable, [sys.executable, '-m', 'jackify.frontends.gui'] + sys.argv[1:])
|
||||||
self.accept()
|
return
|
||||||
return
|
|
||||||
MessageService.information(self, "Settings Saved", "Settings have been saved successfully.", safety_level="low")
|
MessageService.information(self, "Settings Saved", "Settings have been saved successfully.", safety_level="low")
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user