Files
Jackify/jackify/backend/services/automated_prefix_stl.py
2026-02-07 18:26:54 +00:00

191 lines
7.5 KiB
Python

"""STL algorithm methods for AutomatedPrefixService (Mixin)."""
from pathlib import Path
from typing import Optional
import logging
import vdf
import binascii
logger = logging.getLogger(__name__)
class STLAlgorithmMixin:
"""Mixin providing Steam Tools Library algorithm methods for AutomatedPrefixService."""
def generate_steam_short_id(self, signed_appid: int) -> int:
"""
Convert signed 32-bit integer to unsigned 32-bit integer (same as STL's generateSteamShortID).
Args:
signed_appid: Signed 32-bit integer AppID
Returns:
Unsigned 32-bit integer AppID
"""
return signed_appid & 0xFFFFFFFF
def find_appid_in_shortcuts_vdf(self, shortcut_name: str) -> Optional[str]:
"""
Find the AppID for a shortcut by name directly in shortcuts.vdf.
This is a fallback method when protontricks detection fails.
Args:
shortcut_name: Name of the shortcut to find
Returns:
AppID as string, or None if not found
"""
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 shortcut by name
for i in range(len(shortcuts)):
shortcut = shortcuts[str(i)]
name = shortcut.get('AppName', '')
if shortcut_name == name:
appid = shortcut.get('appid')
if appid:
logger.info(f"Found AppID {appid} for shortcut '{shortcut_name}' in shortcuts.vdf")
return str(appid)
logger.warning(f"Shortcut '{shortcut_name}' not found in shortcuts.vdf")
return None
except Exception as e:
logger.error(f"Error finding AppID in shortcuts.vdf: {e}")
return None
def predict_appid_using_stl_algorithm(self, shortcut_name: str, exe_path: str) -> Optional[int]:
"""
Predict the AppID using SteamTinkerLaunch's exact algorithm.
This implements the same logic as STL's generateShortcutVDFAppId and generateSteamShortID functions:
1. Combine AppName + ExePath
2. Generate MD5 hash, take first 8 characters
3. Convert to decimal, make negative, ensure < 1 billion
4. Convert to unsigned 32-bit integer
Args:
shortcut_name: Name of the shortcut
exe_path: Path to the executable
Returns:
Predicted AppID as integer, or None if failed
"""
try:
import hashlib
# Step 1: Combine AppName + ExePath (exactly like STL)
combined_string = f"{shortcut_name}{exe_path}"
logger.debug(f"Combined string for AppID prediction: '{combined_string}'")
# Step 2: Generate MD5 hash and take first 8 characters
md5_hash = hashlib.md5(combined_string.encode()).hexdigest()
seed_hex = md5_hash[:8]
logger.debug(f"MD5 hash: {md5_hash}, seed hex: {seed_hex}")
# Step 3: Convert to decimal, make negative, ensure < 1 billion
seed_decimal = int(seed_hex, 16)
signed_appid = -(seed_decimal % 1000000000)
logger.debug(f"Seed decimal: {seed_decimal}, signed AppID: {signed_appid}")
# Step 4: Convert to unsigned 32-bit integer (STL's generateSteamShortID)
unsigned_appid = signed_appid & 0xFFFFFFFF
logger.debug(f"Unsigned AppID: {unsigned_appid}")
logger.info(f"Predicted AppID using STL algorithm: {unsigned_appid} (signed: {signed_appid})")
return unsigned_appid
except Exception as e:
logger.error(f"Error predicting AppID using STL algorithm: {e}")
return None
def create_shortcut_with_stl_algorithm(self, shortcut_name: str, exe_path: str, start_dir: str, compatibility_tool: str = None) -> bool:
"""
Create a shortcut using STL's exact algorithm for consistent AppID calculation.
Args:
shortcut_name: Name of the shortcut
exe_path: Path to the executable
start_dir: Start directory
compatibility_tool: Optional compatibility tool to set immediately (like STL does)
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 next available index
next_index = str(len(shortcuts))
# Calculate AppID using STL's algorithm
predicted_appid = self.predict_appid_using_stl_algorithm(shortcut_name, exe_path)
if not predicted_appid:
logger.error("Failed to predict AppID for shortcut creation")
return False
# Convert to signed AppID (STL stores the signed version in shortcuts.vdf)
signed_appid = predicted_appid
if predicted_appid > 0x7FFFFFFF: # If it's a large positive number, make it negative
signed_appid = predicted_appid - 0x100000000
# Create new shortcut entry
new_shortcut = {
'AppName': shortcut_name,
'Exe': f'"{exe_path}"',
'StartDir': f'"{start_dir}"',
'appid': signed_appid, # Use the signed AppID
'icon': '',
'ShortcutPath': '',
'LaunchOptions': '',
'IsHidden': 0,
'AllowDesktopConfig': 1,
'AllowOverlay': 1,
'openvr': 0,
'Devkit': 0,
'DevkitGameID': '',
'LastPlayTime': 0,
'FlatpakAppID': '',
'tags': {},
'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 with STL algorithm: {shortcut_name} with AppID {signed_appid} (unsigned: {predicted_appid})")
# Set compatibility tool immediately if provided (like STL does)
if compatibility_tool:
logger.info(f"Setting compatibility tool immediately: {compatibility_tool}")
success = self.set_compatibility_tool_complete_stl_style(predicted_appid, compatibility_tool)
if not success:
logger.warning("Failed to set compatibility tool immediately")
return True
except Exception as e:
logger.error(f"Error creating shortcut with STL algorithm: {e}")
return False