From 70b18004e1f7e386c3bbc1d751d16367dac5d2c6 Mon Sep 17 00:00:00 2001 From: Omni Date: Mon, 15 Sep 2025 20:18:13 +0100 Subject: [PATCH] Sync from development - prepare for v0.1.1 --- CHANGELOG.md | 39 + .../binaries/WabbajackProton-pre-testing.sh | 2 +- Legacy/binaries/WabbajackProton.sh | 2 +- binaries/WabbajackProton.sh | 880 ++++++++ binaries/WabbajackWine.sh | 494 +++++ binaries/omni-guides-testing.sh | 1783 +++++++++++++++++ binaries/omni-guides.sh | 1711 ++++++++++++++++ jackify/__init__.py | 2 +- jackify/backend/handlers/diagnostic_helper.py | 2 +- jackify/backend/handlers/modlist_handler.py | 9 +- .../backend/handlers/modlist_install_cli.py | 42 +- jackify/backend/handlers/path_handler.py | 7 +- jackify/backend/handlers/self_update.py | 141 -- .../services/automated_prefix_service.py | 18 +- .../backend/services/native_steam_service.py | 18 +- jackify/backend/services/update_service.py | 100 +- jackify/engine/Wabbajack.CLI.Builder.dll | Bin 13824 -> 13824 bytes jackify/engine/Wabbajack.Common.dll | Bin 199680 -> 199680 bytes jackify/engine/Wabbajack.Compiler.dll | Bin 160256 -> 160256 bytes jackify/engine/Wabbajack.Compression.BSA.dll | Bin 94720 -> 94720 bytes jackify/engine/Wabbajack.Compression.Zip.dll | Bin 18944 -> 18944 bytes jackify/engine/Wabbajack.Configuration.dll | Bin 5120 -> 5120 bytes jackify/engine/Wabbajack.DTOs.dll | Bin 142336 -> 142336 bytes .../engine/Wabbajack.Downloaders.Bethesda.dll | Bin 18432 -> 18432 bytes .../Wabbajack.Downloaders.Dispatcher.dll | Bin 27136 -> 27136 bytes .../engine/Wabbajack.Downloaders.GameFile.dll | Bin 16384 -> 16384 bytes .../Wabbajack.Downloaders.GoogleDrive.dll | Bin 17920 -> 17920 bytes jackify/engine/Wabbajack.Downloaders.Http.dll | Bin 15872 -> 15872 bytes ...ajack.Downloaders.IPS4OAuth2Downloader.dll | Bin 35328 -> 35328 bytes .../Wabbajack.Downloaders.Interfaces.dll | Bin 7168 -> 7168 bytes .../engine/Wabbajack.Downloaders.Manual.dll | Bin 9216 -> 9216 bytes .../Wabbajack.Downloaders.MediaFire.dll | Bin 15872 -> 15872 bytes jackify/engine/Wabbajack.Downloaders.Mega.dll | Bin 16384 -> 16384 bytes .../engine/Wabbajack.Downloaders.ModDB.dll | Bin 19456 -> 19456 bytes .../engine/Wabbajack.Downloaders.Nexus.dll | Bin 19968 -> 19968 bytes ...abbajack.Downloaders.VerificationCache.dll | Bin 13824 -> 13824 bytes .../Wabbajack.Downloaders.WabbajackCDN.dll | Bin 24576 -> 24576 bytes jackify/engine/Wabbajack.FileExtractor.dll | Bin 74240 -> 74240 bytes jackify/engine/Wabbajack.Hashing.PHash.dll | Bin 41984 -> 42496 bytes jackify/engine/Wabbajack.Hashing.xxHash64.dll | Bin 21504 -> 21504 bytes jackify/engine/Wabbajack.IO.Async.dll | Bin 15360 -> 15360 bytes jackify/engine/Wabbajack.Installer.dll | Bin 117760 -> 118784 bytes .../Wabbajack.Networking.BethesdaNet.dll | Bin 39936 -> 39936 bytes .../engine/Wabbajack.Networking.Discord.dll | Bin 14336 -> 14336 bytes .../engine/Wabbajack.Networking.GitHub.dll | Bin 21504 -> 21504 bytes .../Wabbajack.Networking.Http.Interfaces.dll | Bin 5120 -> 5120 bytes jackify/engine/Wabbajack.Networking.Http.dll | Bin 27648 -> 27648 bytes .../engine/Wabbajack.Networking.NexusApi.dll | Bin 62976 -> 62976 bytes ...abbajack.Networking.WabbajackClientApi.dll | Bin 77824 -> 77824 bytes jackify/engine/Wabbajack.Paths.IO.dll | Bin 32768 -> 33280 bytes jackify/engine/Wabbajack.Paths.dll | Bin 17408 -> 17408 bytes jackify/engine/Wabbajack.RateLimiter.dll | Bin 24064 -> 24064 bytes jackify/engine/Wabbajack.Server.Lib.dll | Bin 6656 -> 6656 bytes .../Wabbajack.Services.OSIntegrated.dll | Bin 55808 -> 55808 bytes jackify/engine/Wabbajack.VFS.Interfaces.dll | Bin 5120 -> 5120 bytes jackify/engine/Wabbajack.VFS.dll | Bin 64512 -> 64512 bytes jackify/engine/jackify-engine.dll | Bin 183296 -> 183296 bytes .../gui/dialogs/ulimit_guidance_dialog.py | 6 +- .../frontends/gui/dialogs/update_dialog.py | 66 +- jackify/frontends/gui/main.py | 8 +- .../frontends/gui/screens/install_modlist.py | 2 +- .../gui/screens/tuxborn_installer.py | 2 +- jackify/shared/appimage_utils.py | 14 +- jackify/shared/paths.py | 958 --------- 64 files changed, 5142 insertions(+), 1164 deletions(-) create mode 100644 binaries/WabbajackProton.sh create mode 100644 binaries/WabbajackWine.sh create mode 100644 binaries/omni-guides-testing.sh create mode 100644 binaries/omni-guides.sh delete mode 100644 jackify/backend/handlers/self_update.py delete mode 100644 jackify/shared/paths.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b8469dc..cdbffaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Jackify Changelog +## v0.1.1 - Self-Updater Implementation +**Release Date:** September 17, 2025 + +### New Features +- **Self-Updater System**: Complete automatic update mechanism for Jackify AppImages + - **GitHub Integration**: Automatic detection of new releases from GitHub + - **GUI Update Dialog**: Professional update notification with Jackify theme styling + - **CLI Update Command**: `--update` flag for manual update checks and installation + - **Startup Checks**: Automatic update detection on application launch + - **User Control**: Skip version, remind later, and download & install options + +### Technical Implementation +- **UpdateService**: Core service handling version detection, download, and installation +- **Full AppImage Replacement**: Reliable update mechanism using helper scripts +- **User-Writable Directories**: All update files stored in `~/Jackify/updates/` for consistency with existing directory structure +- **Progress Indication**: Download progress bars for both GUI and CLI +- **Error Handling**: Graceful fallbacks and comprehensive error messages + +### Security Enhancements +- **AppImage Validation**: Prevents accidental updating of other AppImages when running from development environments +- **Path Verification**: Validates target AppImage contains "jackify" in filename before applying updates + +### User Experience +- **Seamless Updates**: Users receive notifications when updates are available +- **Professional Interface**: Update dialog matches Jackify's visual theme +- **Flexible Options**: Users can choose when and how to update +- **No External Dependencies**: Works on all systems including SteamOS and immutable OSes + +### Bug Fixes +- **Path Regression Fix**: Resolved regression where Configure New/Existing Modlist workflows were creating malformed paths + - Fixed duplicate steamapps/common path generation + - Corrected Steam library root path detection + - Removed broken duplicate PathHandler causing path duplication +- **Enhanced Download Error Messages**: Added Nexus mod URLs to failed download errors for easier troubleshooting + - Automatically appends direct Nexus mod page links + - Supports all major games (Skyrim, Fallout 4, FNV, Oblivion, Starfield) + +--- + ## v0.1.0.1 - Engine Update and Stability Improvements **Release Date:** September 14, 2025 diff --git a/Legacy/binaries/WabbajackProton-pre-testing.sh b/Legacy/binaries/WabbajackProton-pre-testing.sh index 4ab67e0..ac66817 100644 --- a/Legacy/binaries/WabbajackProton-pre-testing.sh +++ b/Legacy/binaries/WabbajackProton-pre-testing.sh @@ -817,7 +817,7 @@ configuration_phase() { display "For example, if you wanted to install a modlist to /home/user/Games/Skyrim/Modlistname, you would need to run:" "$YELLOW" display "flatpak override --user com.valvesoftware.Steam --filesystem=\"/home/user/Games\"" "$YELLOW" fi - echo -e "\n${YELLOW}⚠️ IMPORTANT: For best compatibility, add the following line to the Launch Options of your Wabbajack Steam entry:${RESET}" + echo -e "\n${YELLOW}IMPORTANT: For best compatibility, add the following line to the Launch Options of your Wabbajack Steam entry:${RESET}" echo -e "\n${GREEN}PROTON_USE_WINED3D=1 %command%${RESET}\n" echo -e "This can help resolve certain graphics issues with Wabbajack running under Proton." exit 0 diff --git a/Legacy/binaries/WabbajackProton.sh b/Legacy/binaries/WabbajackProton.sh index 3f52e0c..10631b2 100644 --- a/Legacy/binaries/WabbajackProton.sh +++ b/Legacy/binaries/WabbajackProton.sh @@ -858,7 +858,7 @@ configuration_phase() { display "For example, if you wanted to install a modlist to /home/user/Games/Skyrim/Modlistname, you would need to run:" "$YELLOW" display "flatpak override --user com.valvesoftware.Steam --filesystem=\"/home/user/Games\"" "$YELLOW" fi - echo -e "\n${YELLOW}⚠️ IMPORTANT: For best compatibility, add the following line to the Launch Options of your Wabbajack Steam entry:${RESET}" + echo -e "\n${YELLOW}IMPORTANT: For best compatibility, add the following line to the Launch Options of your Wabbajack Steam entry:${RESET}" echo -e "\n${GREEN}PROTON_USE_WINED3D=1 %command%${RESET}\n" echo -e "This can help resolve certain graphics issues with Wabbajack running under Proton." exit 0 diff --git a/binaries/WabbajackProton.sh b/binaries/WabbajackProton.sh new file mode 100644 index 0000000..10631b2 --- /dev/null +++ b/binaries/WabbajackProton.sh @@ -0,0 +1,880 @@ +#!/usr/bin/env bash +# +################################################################## +# # +# Attempt to automate installing Wabbajack on Linux Steam/Proton # +# # +# v0.20 - Refactored #1 +# # +################################################################## + +# Set up logging +LOGFILE="$HOME/wabbajack-via-proton-sh.log" +echo "" > "$LOGFILE" + +# Script configuration +SCRIPT_VERSION="0.20" +STEAM_IS_FLATPAK=0 +VERBOSE=0 +CURRENT_TASK="" +TOTAL_TASKS=10 +CURRENT_TASK_NUM=0 +IN_MODIFICATION_PHASE=0 # Add this flag + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -v|--verbose) + VERBOSE=1 + shift + ;; + *) + display "Unknown option: $1" "$RED" + display "Usage: $0 [-v|--verbose]" "$YELLOW" + exit 1 + ;; + esac +done + +# URLs for resources +WABBALIST_URL="https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/README.md" +WEBVIEW_INSTALLER_URL="https://files.omnigaming.org/MicrosoftEdgeWebView2RuntimeInstallerX64-WabbajackProton.exe" +SYSTEM_REG_URL="https://github.com/Omni-guides/Wabbajack-Modlist-Linux/raw/refs/heads/main/files/system.reg.github" +USER_REG_URL="https://github.com/Omni-guides/Wabbajack-Modlist-Linux/raw/refs/heads/main/files/user.reg.github" + +# Color codes for pretty output +GREEN="\e[32m" +YELLOW="\e[33m" +RED="\e[31m" +RESET="\e[0m" + +# Logging function +log() { + local message="$1" + local log_level="${2:-INFO}" + local timestamp=$(date +'%Y-%m-%d %H:%M:%S') + echo "[$timestamp] [$log_level] $message" >> "$LOGFILE" + + # If verbose mode is enabled, also print to console + if [[ $VERBOSE -eq 1 ]]; then + echo "[$timestamp] [$log_level] $message" + fi +} + +# Display and logging function +display() { + local message="$1" + local color="${2:-$RESET}" + # Only log to file if it's not a user prompt or selection + if [[ ! "$message" =~ "Please select" ]] && [[ ! "$message" =~ "Enter the number" ]]; then + log "$message" + fi + echo -e "${color}${message}${RESET}" +} + +# Verbose logging function +verbose_log() { + if [[ $VERBOSE -eq 1 ]]; then + log "$1" "VERBOSE" + fi +} + +# Section header function +log_section() { + local message="$1" + local separator="============================================" + log "$separator" + log "$message" + log "$separator" + if [[ $VERBOSE -eq 1 ]]; then + echo -e "${YELLOW}$separator${RESET}" + echo -e "${YELLOW}$message${RESET}" + echo -e "${YELLOW}$separator${RESET}" + fi +} + +# Error handling function +error_exit() { + display "$1" "$RED" + log "$1" "ERROR" + cleanup_wine_procs + exit 1 +} + +# Progress bar function +update_progress() { + # Only show progress bar during modification phase + if [[ $IN_MODIFICATION_PHASE -eq 0 ]]; then + return + fi + + local percent=$1 + local bar_length=50 + local filled_length=$((percent * bar_length / 100)) + local bar="" + + # Create the bar string with = for filled portions + for ((i = 0; i < bar_length; i++)); do + if [ $i -lt $filled_length ]; then + bar+="=" + else + bar+=" " + fi + done + + # Use \r to return to start of line and overwrite previous progress + printf "\r[%-${bar_length}s] %d%% - %s" "$bar" "$percent" "$CURRENT_TASK" +} + +# Set current task function +set_current_task() { + CURRENT_TASK="$1" + + # Only increment and show progress during modification phase + if [[ $IN_MODIFICATION_PHASE -eq 1 ]]; then + # Calculate percentage based on modification phase tasks + local total_mod_tasks=11 # Updated to account for split configure_prefix tasks + local percent=0 + + # Only show 100% when we're actually complete + if [[ "$CURRENT_TASK" == "Complete" ]]; then + percent=100 + else + # Calculate percentage based on current task number + percent=$((CURRENT_TASK_NUM * 100 / total_mod_tasks)) + # Ensure we don't hit 100% before completion + if [[ $percent -ge 100 ]]; then + percent=99 + fi + fi + + # Clear the current line before updating progress + printf "\r%-100s\r" "" + update_progress "$percent" + + # Increment task counter after displaying progress + CURRENT_TASK_NUM=$((CURRENT_TASK_NUM + 1)) + fi +} + +# Download function that uses either wget or curl +download_file() { + local url="$1" + local output_path="$2" + local description="${3:-file}" + + # Only log to file, don't display to user + log "Downloading $description..." + + if command -v wget &>/dev/null; then + if wget "$url" -O "$output_path" >>"$LOGFILE" 2>&1; then + log "Downloaded $description successfully using wget" + return 0 + else + error_exit "Failed to download $description with wget" + fi + elif command -v curl &>/dev/null; then + if curl -sLo "$output_path" "$url" >>"$LOGFILE" 2>&1; then + log "Downloaded $description successfully using curl" + return 0 + else + error_exit "Failed to download $description with curl" + fi + else + error_exit "Neither wget nor curl is available. Cannot download $description" + fi +} + +display_banner() { + echo "╔══════════════════════════════════════════════════════════════════╗" + echo "║ Wabbajack Proton Setup v$SCRIPT_VERSION ║" + echo "║ ║" + echo "║ A tool for running Wabbajack on Linux via Proton ║" + echo "╚══════════════════════════════════════════════════════════════════╝" + + echo "" + display "This script automates setting up Wabbajack to run on Linux via Steam's Proton compatibility layer." "$YELLOW" + echo "───────────────────────────────────────────────────────────────────" + display "Please be aware that this is experimental software and is *NOT* supported by the Wabbajack team." "$YELLOW" + display "If you encounter issues, please report them on GitHub or the #unofficial-linux-support channel on Discord." "$YELLOW" + echo "───────────────────────────────────────────────────────────────────" + display "⚠ IMPORTANT: Use this script at your own risk." "$RED" + echo "" + echo -e "\e[33mPress any key to continue...\e[0m" + read -n 1 -s -r -p "" + echo "" +} + +detect_steamdeck() { + if [ -f "/etc/os-release" ] && grep -q "steamdeck" "/etc/os-release"; then + STEAMDECK=1 + log "Running on Steam Deck" + else + STEAMDECK=0 + log "NOT running on Steam Deck" + fi +} + +detect_protontricks() { + # Only log to file, don't display to user + log "Detecting protontricks installation..." + + if command -v protontricks >/dev/null 2>&1; then + PROTONTRICKS_PATH=$(command -v protontricks) + # Check if the detected binary is actually a Flatpak wrapper + if [[ -f "$PROTONTRICKS_PATH" ]] && grep -q "flatpak run" "$PROTONTRICKS_PATH"; then + log "Detected Protontricks is a Flatpak wrapper at $PROTONTRICKS_PATH" + WHICH_PROTONTRICKS="flatpak" + return 0 + else + log "Native Protontricks found at $PROTONTRICKS_PATH" + WHICH_PROTONTRICKS="native" + return 0 + fi + elif flatpak list | grep -iq protontricks; then + log "Flatpak Protontricks is installed" + WHICH_PROTONTRICKS="flatpak" + return 0 + else + log "Protontricks not found. Do you wish to install it? (y/n): " + display "Protontricks not found. Do you wish to install it? (y/n): " "$RED" + read -p " " answer + if [[ $answer =~ ^[Yy]$ ]]; then + if [[ $STEAMDECK -eq 1 ]]; then + if flatpak install -u -y --noninteractive flathub com.github.Matoking.protontricks; then + WHICH_PROTONTRICKS="flatpak" + return 0 + else + display "\n\e[31mFailed to install Protontricks via Flatpak. Please install it manually and rerun this script.\e[0m" "$RED" + exit 1 + fi + else + read -p "Choose installation method: 1) Flatpak (preferred) 2) Native: " choice + if [[ $choice =~ 1 ]]; then + if flatpak install -u -y --noninteractive flathub com.github.Matoking.protontricks; then + WHICH_PROTONTRICKS="flatpak" + return 0 + else + display "\n\e[31mFailed to install Protontricks via Flatpak. Please install it manually and rerun this script.\e[0m" "$RED" + exit 1 + fi + else + display "Sorry, there are too many distros to automate this!" + display "Please check how to install Protontricks using your OS package manager (yum, dnf, apt, pacman, etc.)" + display "\e[31mProtontricks is required for this script to function. Exiting.\e[0m" "$RED" + exit 1 + fi + fi + else + display "\e[31mProtontricks is required for this script to function. Exiting.\e[0m" "$RED" + exit 1 + fi + fi +} + +setup_protontricks_alias() { + set_current_task "Setting up Protontricks aliases" + if [[ "$WHICH_PROTONTRICKS" = "flatpak" ]]; then + local protontricks_alias_exists=$(grep "^alias protontricks=" ~/.bashrc) + local launch_alias_exists=$(grep "^alias protontricks-launch" ~/.bashrc) + + if [[ -z "$protontricks_alias_exists" ]]; then + display "Adding protontricks alias to ~/.bashrc" "$YELLOW" + echo "alias protontricks='flatpak run com.github.Matoking.protontricks'" >> ~/.bashrc + source ~/.bashrc + else + log "protontricks alias already exists in ~/.bashrc" + fi + + if [[ -z "$launch_alias_exists" ]]; then + display "Adding protontricks-launch alias to ~/.bashrc" "$YELLOW" + echo "alias protontricks-launch='flatpak run --command=protontricks-launch com.github.Matoking.protontricks'" >> ~/.bashrc + source ~/.bashrc + else + log "protontricks-launch alias already exists in ~/.bashrc" + fi + else + log "Protontricks is not installed via flatpak, skipping alias creation" + fi +} + +run_protontricks() { + # Determine the protontricks binary path + verbose_log "Running protontricks with arguments: $*" + + if [ "$WHICH_PROTONTRICKS" = "flatpak" ]; then + verbose_log "Using Flatpak protontricks" + # Redirect Wine output to /dev/null but keep protontricks output + if [[ "$*" == *"-c"* ]]; then + # For Wine commands, suppress output but check exit code + if flatpak run com.github.Matoking.protontricks "$@" >/dev/null 2>&1; then + return 0 + else + return 1 + fi + else + # For non-Wine commands, show output but redirect stderr to /dev/null + flatpak run com.github.Matoking.protontricks "$@" 2>/dev/null + fi + else + verbose_log "Using native protontricks" + # Redirect Wine output to /dev/null but keep protontricks output + if [[ "$*" == *"-c"* ]]; then + # For Wine commands, suppress output but check exit code + if protontricks "$@" >/dev/null 2>&1; then + return 0 + else + return 1 + fi + else + # For non-Wine commands, show output but redirect stderr to /dev/null + protontricks "$@" 2>/dev/null + fi + fi +} + +check_protontricks_version() { + set_current_task "Checking Protontricks version" + # Get the current version of protontricks + local protontricks_version=$(run_protontricks -V | cut -d ' ' -f 2 | sed 's/[()]//g' | sed 's/\.[0-9]$//') + local protontricks_version_cleaned=$(echo "$protontricks_version" | sed 's/[^0-9.]//g') + + log "Protontricks Version: $protontricks_version_cleaned" + + # Compare version strings + if [[ "$protontricks_version_cleaned" < "1.12" ]]; then + error_exit "Your protontricks version is too old! Update to version 1.12 or newer and rerun this script." + fi +} + +get_wabbajack_path() { + set_current_task "Detecting Wabbajack path" + local wabbajack_path="" + local wabbajack_entries=() + local app_ids=() + local app_names=() + local all_app_ids=() + local all_app_names=() + local selection="" + local use_all_shortcuts=0 + + log "Detecting Wabbajack Install Directory..." + verbose_log "Attempting to find Wabbajack entries using protontricks -l" + + # First, try to find Wabbajack entries using protontricks + local protontricks_entries=$(run_protontricks -l | grep -i 'Non-Steam shortcut' | grep -i wabbajack) + verbose_log "Protontricks output: $protontricks_entries" + + if [[ -n "$protontricks_entries" ]]; then + log "Found Wabbajack entries via protontricks (name match)" + while IFS= read -r line; do + local app_id=$(echo "$line" | awk '{print $NF}' | sed 's:^.\(.*\).$:\1:') + local app_name=$(echo "$line" | sed 's/^Non-Steam shortcut: //i' | sed 's: ([0-9]*)$::') + if [[ -n "$app_id" ]]; then + app_ids+=("$app_id") + app_names+=("$app_name") + log "Found App ID: $app_id, Name: $app_name" + fi + done <<< "$protontricks_entries" + + echo "" + display "Wabbajack-related Steam entries found. Please select which one you wish to configure:" "$RED" + for i in "${!app_ids[@]}"; do + echo "$((i + 1)). ${app_names[i]} (App ID: ${app_ids[i]})" + done + local extra_option=$(( ${#app_ids[@]} + 1 )) + echo "$extra_option. List all Steam shortcuts" + echo "Please select the entry you want to use (1-$extra_option):" + read -r selection + if [[ "$selection" == "$extra_option" ]]; then + use_all_shortcuts=1 + elif [[ "$selection" =~ ^[0-9]+$ ]] && ((selection >= 1 && selection <= ${#app_ids[@]})); then + APPID="${app_ids[$((selection - 1))]}" + log "Selected App ID: $APPID, Name: ${app_names[$((selection - 1))]}" + else + use_all_shortcuts=1 + fi + else + use_all_shortcuts=1 + fi + + # If requested, list all Non-Steam shortcuts + if [[ $use_all_shortcuts -eq 1 ]]; then + local all_entries=$(run_protontricks -l | grep -i 'Non-Steam shortcut') + if [[ -z "$all_entries" ]]; then + error_exit "No Non-Steam shortcuts found via protontricks. Please ensure you've added your entry as a non-Steam game and run it once via Steam." + fi + while IFS= read -r line; do + local app_id=$(echo "$line" | awk '{print $NF}' | sed 's:^.\(.*\).$:\1:') + local app_name=$(echo "$line" | sed 's/^Non-Steam shortcut: //i' | sed 's: ([0-9]*)$::') + if [[ -n "$app_id" ]]; then + all_app_ids+=("$app_id") + all_app_names+=("$app_name") + log "Found App ID: $app_id, Name: $app_name (all shortcuts)" + fi + done <<< "$all_entries" + echo "" + display "All Steam shortcuts detected. Please select which one you wish to configure:" "$RED" + for i in "${!all_app_ids[@]}"; do + echo "$((i + 1)). ${all_app_names[i]} (App ID: ${all_app_ids[i]})" + done + echo "Please select the entry you want to use (1-${#all_app_ids[@]}):" + read -r selection + if ! [[ "$selection" =~ ^[0-9]+$ ]] || ((selection < 1 || selection > ${#all_app_ids[@]})); then + error_exit "Invalid selection" + fi + APPID="${all_app_ids[$((selection - 1))]}" + log "Selected App ID: $APPID, Name: ${all_app_names[$((selection - 1))]}" + echo "" + echo "If you don't see your Wabbajack entry in the list, please make sure you have added it to Steam, set the Proton version, and run it once (then closed Wabbajack)." + fi + + # Now that we have the App ID, try to find the executable path in shortcuts.vdf + verbose_log "Attempting to find executable path for App ID: $APPID" + local steam_userdata_paths=( + "$HOME/.steam/steam/userdata" + "$HOME/.local/share/Steam/userdata" + "$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/userdata" + ) + local wabbajack_entries=() + for path in "${steam_userdata_paths[@]}"; do + if [[ -d "$path" ]]; then + verbose_log "Checking directory: $path" + local vdf_files=$(find "$path" -name "shortcuts.vdf" 2>/dev/null) + for vdf_file in $vdf_files; do + if [[ "$vdf_file" == *"12345678"* ]]; then + verbose_log "Skipping test directory shortcuts.vdf: $vdf_file" + continue + fi + verbose_log "Checking shortcuts.vdf: $vdf_file" + while IFS= read -r line; do + if [[ "$line" == */Wabbajack.exe* ]]; then + local path=$(echo "$line" | sed -E 's/.*"([^"*Wabbajack\\.exe[^"]*)".*$/\1/') + if [[ -n "$path" ]]; then + if [[ "$path" != *".wabbajack_test"* ]]; then + verbose_log "Found Wabbajack.exe path: $path" + wabbajack_entries+=("$path") + else + verbose_log "Skipping test directory entry: $path" + fi + fi + fi + done < <(strings "$vdf_file" | grep -i "/Wabbajack.exe") + done + fi + done + readarray -t unique_entries < <(printf '%s\n' "${wabbajack_entries[@]}" | sort -u) + wabbajack_entries=("${unique_entries[@]}") + # Sort alphabetically for user-friendly order + IFS=$'\n' wabbajack_entries=($(sort <<<"${wabbajack_entries[*]}")) + unset IFS + + # Build a mapping from App ID to Wabbajack.exe path + declare -A appid_to_path + for path in "${wabbajack_entries[@]}"; do + # Try to extract the App ID from the path's parent directory name (assumes unique per shortcut) + for i in "${!all_app_ids[@]}"; do + if [[ "$path" == *"${all_app_names[$i]}"* ]]; then + appid_to_path["${all_app_ids[$i]}"]="$path" + fi + done + done + # If we have a sorted app_ids array, display the paths in that order + ordered_paths=() + for id in "${app_ids[@]}"; do + if [[ -n "${appid_to_path[$id]}" ]]; then + ordered_paths+=("${appid_to_path[$id]}") + fi + done + # Only use ordered_paths if it matches the number of app_ids + if [[ ${#ordered_paths[@]} -eq ${#app_ids[@]} ]]; then + wabbajack_entries=("${ordered_paths[@]}") + fi + local entry_count=${#wabbajack_entries[@]} + verbose_log "Found $entry_count unique Wabbajack.exe entries (ordered by shortcut name)" + if [[ "$entry_count" -eq 0 ]]; then + error_exit "No Wabbajack.exe entries found in shortcuts.vdf. Please ensure you've added Wabbajack.exe as a non-Steam game and run it once via Steam." + elif [[ "$entry_count" -gt 1 ]]; then + echo "" + display "Multiple Wabbajack.exe paths found, please select which one you wish to configure:" "$RED" + local i=1 + for path in "${wabbajack_entries[@]}"; do + echo "$i) $path" + ((i++)) + done + local selected_entry="" + while [[ ! "$selected_entry" =~ ^[0-9]+$ || "$selected_entry" -lt 1 || "$selected_entry" -gt "$entry_count" ]]; do + read -p "Enter the number of the desired entry (1-$entry_count): " selected_entry + if [[ ! "$selected_entry" =~ ^[0-9]+$ || "$selected_entry" -lt 1 || "$selected_entry" -gt "$entry_count" ]]; then + display "Invalid selection. Please enter a number between 1 and $entry_count" "$RED" + fi + done + wabbajack_path="${wabbajack_entries[$((selected_entry - 1))]}" + log "Selected Wabbajack path: $wabbajack_path" + echo "" + else + wabbajack_path="${wabbajack_entries[0]}" + log "Single Wabbajack path found: $wabbajack_path" + fi + if [[ -n "$wabbajack_path" ]]; then + log "Using Wabbajack path: $wabbajack_path" + APPLICATION_DIRECTORY=$(dirname "$wabbajack_path") + # Sanitize: remove any stray quotes and trim whitespace/newlines + APPLICATION_DIRECTORY=$(echo "$APPLICATION_DIRECTORY" | tr -d '"' | xargs) + log "Application Directory: $APPLICATION_DIRECTORY" + return 0 + else + error_exit "Failed to determine Wabbajack path" + fi +} + +detect_compatdata_path() { + set_current_task "Detecting compatdata path" + # Check common Steam library locations first + local steam_paths=( + "$HOME/.local/share/Steam/steamapps/compatdata" + "$HOME/.steam/steam/steamapps/compatdata" + "$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/compatdata" + ) + + for path in "${steam_paths[@]}"; do + if [[ -d "$path/$APPID" ]]; then + COMPAT_DATA_PATH="$path/$APPID" + log "compatdata Path detected: $COMPAT_DATA_PATH" + return 0 + fi + done + + # If not found in common locations, use find command with specific paths + local found=0 + for base_path in "${steam_paths[@]}"; do + if [[ -d "$base_path" ]]; then + if [[ -d "$base_path/$APPID" ]]; then + COMPAT_DATA_PATH="$base_path/$APPID" + log "compatdata Path detected: $COMPAT_DATA_PATH" + found=1 + break + fi + fi + done + + if [[ $found -eq 0 ]]; then + error_exit "Directory named '$APPID' not found in any compatdata directories. Please ensure you have started the Steam entry for Wabbajack at least once." + fi +} + +set_protontricks_perms() { + set_current_task "Setting Protontricks permissions" + if [ "$WHICH_PROTONTRICKS" = "flatpak" ]; then + # Only log to file, don't display to user + log "Setting Protontricks permissions..." + + # Always set Flatpak override for the application directory, suppressing error output + flatpak override --user com.github.Matoking.protontricks --filesystem="$APPLICATION_DIRECTORY" 2>>"$LOGFILE" + + if [[ "$STEAMDECK" = 1 ]]; then + log "Checking for SDCard and setting permissions appropriately..." + # Set protontricks SDCard permissions early to suppress warning + sdcard_path=$(df -h | grep "/run/media" | awk '{print $NF}') + log "SD Card path: $sdcard_path" + if [[ -n "$sdcard_path" ]]; then + flatpak override --user --filesystem="$sdcard_path" com.github.Matoking.protontricks 2>>"$LOGFILE" + log "SD Card permission set" + fi + fi + else + log "Using Native protontricks, skip setting permissions" + fi +} + +webview_installer() { + set_current_task "Downloading WebView installer" + log "Setting up WebView..." + local installer_path="$APPLICATION_DIRECTORY/MicrosoftEdgeWebView2RuntimeInstallerX64-WabbajackProton.exe" + # Download if not present + if [ ! -f "$installer_path" ]; then + download_file "$WEBVIEW_INSTALLER_URL" "$installer_path" "WebView Installer" + else + log "WebView Installer already exists, skipping download" + fi + # Check installer exists before running + if [ ! -f "$installer_path" ]; then + log "ERROR: WebView installer not found at $installer_path" + error_exit "WebView installer missing" + fi + # Always run the installer in the correct prefix using run_protontricks, capturing output + set_current_task "Installing WebView runtime (this may take a while)..." + log "Installing WebView..." + local webview_tmp_log + webview_tmp_log="$(mktemp)" + if ! run_protontricks -c "wine \"$installer_path\" /silent /install" "$APPID" > "$webview_tmp_log" 2>&1; then + log "ERROR: Failed to install WebView. See $LOGFILE for details." + echo "--- WebView Installer Output ---" >> "$LOGFILE" + cat "$webview_tmp_log" >> "$LOGFILE" + echo "--- End WebView Installer Output ---" >> "$LOGFILE" + rm -f "$webview_tmp_log" + display "Failed to install WebView. See $LOGFILE for details.\nYou may need to install the WebView2 runtime manually inside the Proton prefix." "$RED" + error_exit "Failed to install WebView. See $LOGFILE for details." + fi + rm -f "$webview_tmp_log" +} + +detect_link_steam_library() { + local steam_library_paths=() + local libraryfolders_vdf="" + + # Only log to file, don't display to user + log "Discovering Steam libraries..." + + # Find libraryfolders.vdf and extract library paths + local vdf_paths=( + "$HOME/.steam/steam/steamapps/libraryfolders.vdf" + "$HOME/.local/share/Steam/steamapps/libraryfolders.vdf" + "$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/libraryfolders.vdf" + ) + + for vdf_path in "${vdf_paths[@]}"; do + if [[ -f "$vdf_path" ]]; then + if [[ ! -r "$vdf_path" ]]; then + log "Found libraryfolders.vdf at $vdf_path but it's not readable" + continue + fi + libraryfolders_vdf="$vdf_path" + log "Found readable libraryfolders.vdf at $vdf_path" + break + fi + done + + if [[ -z "$libraryfolders_vdf" ]]; then + display "Steam libraryfolders.vdf not found. Manual input required." "$RED" + read -e -p "Enter the path to your main Steam directory: " steam_library_path + + while true; do + if [[ ! -d "$steam_library_path" ]]; then + display "Invalid path. Please enter a valid directory." "$RED" + elif [[ ! -f "$steam_library_path/steamapps/libraryfolders.vdf" ]]; then + display "The specified path does not appear to be a Steam directory. Do not enter a secondary Steam Library path, only the main Steam install path." "$RED" + elif [[ ! -r "$steam_library_path/steamapps/libraryfolders.vdf" ]]; then + display "The libraryfolders.vdf file exists but is not readable. Please check permissions." "$RED" + else + read -p "Confirm using '$steam_library_path' as the Steam directory path? (y/n): " -r choice + if [[ "$choice" =~ ^[Yy]$ ]]; then + libraryfolders_vdf="$steam_library_path/steamapps/libraryfolders.vdf" + CHOSEN_LIBRARY="$steam_library_path" + break + fi + fi + read -e -p "Enter the path to your Steam library: " steam_library_path + done + fi + + if [[ -n "$libraryfolders_vdf" ]]; then + # Parse libraryfolders.vdf + while IFS= read -r line; do + if [[ "$line" =~ \"path\" ]]; then + local path=$(echo "$line" | sed 's/.*"\(.*\)".*/\1/') + if [[ -d "$path" && -r "$path" ]]; then + steam_library_paths+=("$path") + log "Found valid Steam library at: $path" + else + log "Found Steam library path but it's not accessible: $path" + fi + fi + done < <(grep "\"path\"" "$libraryfolders_vdf") + + if [[ ${#steam_library_paths[@]} -gt 0 ]]; then + # Use the first library path found as the chosen library + CHOSEN_LIBRARY="${steam_library_paths[0]}" + log "Selected Steam library: $CHOSEN_LIBRARY" + else + error_exit "No accessible Steam library paths found in libraryfolders.vdf" + fi + else + error_exit "Steam library not found" + fi +} + +configure_steam_libraries() { + set_current_task "Configuring Steam libraries" + # Only log to file, don't display to user + log "Configuring Steam libraries..." + + # Make directories + local steam_config_directory="$CHOSEN_LIBRARY/steamapps/compatdata/$APPID/pfx/drive_c/Program Files (x86)/Steam/config" + log "Creating directory $steam_config_directory" + + mkdir -p "$steam_config_directory" || error_exit "Failed to create directory $steam_config_directory" + + # Copy or symlink libraryfolders.vdf to config directory + if [[ "$CHOSEN_LIBRARY" == "$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam" ]]; then + STEAM_IS_FLATPAK=1 + # For Flatpak Steam, adjust the paths accordingly + log "Symlinking libraryfolders.vdf to config directory for Flatpak Steam" + ln -sf "$CHOSEN_LIBRARY/config/libraryfolders.vdf" "$steam_config_directory/libraryfolders.vdf" || + log "Failed to symlink libraryfolders.vdf (Flatpak Steam)" + else + log "Symlinking libraryfolders.vdf to config directory" + ln -sf "$CHOSEN_LIBRARY/config/libraryfolders.vdf" "$steam_config_directory/libraryfolders.vdf" || + log "Failed to symlink libraryfolders.vdf" + fi + + # Backup existing libraryfolders.vdf if it exists + local pfx_libraryfolders="$CHOSEN_LIBRARY/steamapps/compatdata/$APPID/pfx/drive_c/Program Files (x86)/Steam/steamapps/libraryfolders.vdf" + if [[ -f "$pfx_libraryfolders" ]]; then + mv "$pfx_libraryfolders" "${pfx_libraryfolders}.bak" || log "Failed to backup libraryfolders.vdf" + fi +} + +create_dotnet_cache_dir() { + set_current_task "Setting up .NET cache directory" + # Only log to file, don't display to user + log "Setting up .NET cache directory..." + + local user_name=$(whoami) + local cache_dir="$APPLICATION_DIRECTORY/home/$user_name/.cache/dotnet_bundle_extract" + + # Check if the directory already exists + if [ -d "$cache_dir" ]; then + log "Directory already exists: $cache_dir, skipping..." + return 0 + fi + + # Create the directory + mkdir -p "$cache_dir" || error_exit "Failed to create directory: $cache_dir" + log "Directory successfully created: $cache_dir" +} + +cleanup_wine_procs() { + # Only log to file, don't display to user + log "Cleaning up any hanging Wine processes..." + + # Find and kill processes + local processes=$(pgrep -f "WabbajackProton.exe|renderer=vulkan|win7|win10|ShowDotFiles|MicrosoftEdgeWebView2RuntimeInstallerX64-WabbajackProton.exe") + if [[ -n "$processes" ]]; then + echo "$processes" | xargs -r kill -9 + log "Processes killed successfully" + else + log "No matching wine processes found" + fi +} + +# Show detection summary and ask for confirmation +show_detection_summary() { + echo "" + echo "───────────────────────────────────────────────────────────────────" + echo -e "\e[1mDetection Summary:\e[0m" | tee -a "$LOGFILE" + echo -e "===================" | tee -a "$LOGFILE" + echo -e "Wabbajack Path: \e[32m\"$APPLICATION_DIRECTORY\"\e[0m" | tee -a "$LOGFILE" + echo -e "Steam App ID: \e[32m$APPID\e[0m" | tee -a "$LOGFILE" + echo -e "Compatdata Path: \e[32m$COMPAT_DATA_PATH\e[0m" | tee -a "$LOGFILE" + echo -e "Steam Library: \e[32m$CHOSEN_LIBRARY\e[0m" | tee -a "$LOGFILE" + echo -e "Protontricks: \e[32m$WHICH_PROTONTRICKS\e[0m" | tee -a "$LOGFILE" + + # Show Steam Deck status if detected + if [[ $STEAMDECK -eq 1 ]]; then + echo -e "Running on: \e[32mSteam Deck\e[0m" | tee -a "$LOGFILE" + fi + + # Show SD Card status if detected + if [[ "$CHOSEN_LIBRARY" == "/run/media"* ]] || [[ "$APPLICATION_DIRECTORY" == "/run/media"* ]]; then + echo -e "SD Card: \e[32mDetected\e[0m" | tee -a "$LOGFILE" + fi + echo "───────────────────────────────────────────────────────────────────" + + # Show confirmation with retry loop + while true; do + read -rp $'\e[32mDo you want to proceed with the installation? (y/N)\e[0m ' proceed + + if [[ $proceed =~ ^[Yy]$ ]]; then + break + elif [[ $proceed =~ ^[Nn]$ ]] || [[ -z $proceed ]]; then + log "Installation cancelled by user" + display "Installation cancelled." "$YELLOW" + cleanup_wine_procs + exit 0 + fi + + display "Please enter 'y' for yes or 'n' for no." "$YELLOW" + done + + # Add padding after user confirmation + echo "" +} + +# --- Discovery Phase --- +discovery_phase() { + # All detection, user input, and variable gathering + display_banner + log_section "Initial Setup" + cleanup_wine_procs + CURRENT_TASK_NUM=0 + IN_MODIFICATION_PHASE=0 + log_section "Environment Detection" + detect_steamdeck + detect_protontricks + setup_protontricks_alias + check_protontricks_version + log_section "Path Detection" + get_wabbajack_path + detect_compatdata_path + detect_link_steam_library + show_detection_summary +} + +# --- Configuration Phase --- +configuration_phase() { + # All actions that change the system, using only variables set above + IN_MODIFICATION_PHASE=1 + CURRENT_TASK_NUM=0 + log_section "Environment Configuration" + set_protontricks_perms + set_current_task "Applying initial system.reg (phase 1)" + download_file "https://raw.githubusercontent.com/Omni-guides/Wabbajack-Modlist-Linux/main/files/system.reg.wj.win7" "$COMPAT_DATA_PATH/pfx/system.reg" "Phase 1 system.reg" + webview_installer + set_current_task "Applying final system.reg and user.reg" + download_file "https://raw.githubusercontent.com/Omni-guides/Wabbajack-Modlist-Linux/main/files/system.reg.wj" "$COMPAT_DATA_PATH/pfx/system.reg" "Final system.reg" + download_file "https://raw.githubusercontent.com/Omni-guides/Wabbajack-Modlist-Linux/main/files/user.reg.wj" "$COMPAT_DATA_PATH/pfx/user.reg" "Final user.reg" + configure_steam_libraries + create_dotnet_cache_dir + log_section "Final Cleanup" + cleanup_wine_procs + set_current_task "Complete" + echo -e "\n" + echo "───────────────────────────────────────────────────────────────────" + log_section "Setup Complete" + display "✓ Installation completed successfully!" "$GREEN" + echo -e "\n📝 Next Steps:" + echo " • Launch Wabbajack through Steam" + echo " • When Wabbajack opens, verify you can log in to Nexus from the Settings option" + echo " • Begin downloading and installing your modlist" + echo -e "\n💡 If you encounter any issues:" + echo " • Check the log file at: $LOGFILE" + echo " • Join the #unofficial-linux-support channel on the Wabbajack Discord" + echo " • Ensure you've followed all modlist-specific requirements" + echo "───────────────────────────────────────────────────────────────────" + echo -e "\n" + if [[ $STEAM_IS_FLATPAK -eq 1 ]]; then + display "Flatpak Steam is in use. You may need to add a permissions override so that Wabbajack can access the directories." "$YELLOW" + display "For example, if you wanted to install a modlist to /home/user/Games/Skyrim/Modlistname, you would need to run:" "$YELLOW" + display "flatpak override --user com.valvesoftware.Steam --filesystem=\"/home/user/Games\"" "$YELLOW" + fi + echo -e "\n${YELLOW}IMPORTANT: For best compatibility, add the following line to the Launch Options of your Wabbajack Steam entry:${RESET}" + echo -e "\n${GREEN}PROTON_USE_WINED3D=1 %command%${RESET}\n" + echo -e "This can help resolve certain graphics issues with Wabbajack running under Proton." + exit 0 +} + +# --- Main Execution --- +main() { + log_section "Script version $SCRIPT_VERSION started at: $(date +'%Y-%m-%d %H:%M:%S')" + if [[ $VERBOSE -eq 1 ]]; then + display "Verbose mode enabled" "$YELLOW" + fi + # Discovery Phase + discovery_phase + # Configuration Phase + configuration_phase +} + +# Run the main function +main diff --git a/binaries/WabbajackWine.sh b/binaries/WabbajackWine.sh new file mode 100644 index 0000000..aa7c974 --- /dev/null +++ b/binaries/WabbajackWine.sh @@ -0,0 +1,494 @@ +#!/usr/bin/env bash +# +############################################################## +# # +# Attempt to automate installing Wabbajack on Linux via Wine # +# # +# Alpha v0.13 - Omni, from 08/02/25 # +# # +############################################################## + +# - v0.01 - Initial script structure. +# - v0.02 - Added function for detecting the wine version. +# - v0.02 - Added function for setting Wabbajack directry path. +# - v0.02 - Added function for setting wine prefix path. +# - v0.02 - Added function to create the Wabbajack directory and wine prefix. +# - v0.03 - Added function to download required .exe files. +# - v0.03 - Added function to install and configure WebView and set up Wabbajack Application entry. +# - v0.04 - Added function to try to detect the Steam library. +# - v0.04 - Added function to create a Desktop item. +# - v0.04 - Added function to ask if Wabbajack should be started now. +# - v0.05 - Tweak to wine version comparison removing the requirement for 'bc'. +# - v0.06 - Remove references to $HOME for downloading and installing WebView. +# - v0.07 - Added capture of spaces in provided directory name - unsupported. +# - v0.08 - Added colouring to the text output to better distinguish questions, warnings and informationals. +# - v0.09 - Reworked the steam library detection to include confirmation if library detected, user defined path as desired. +# - v0.10 - Completely replace Steam Library symlink with modified copy of libraryfolders.vdf - this should handle all Steam Libraries, and not just the default library +# - v0.11 - create a dotnet_bundle_extract directory which seems required on some distros (harmless on others) +# - v0.12 - Fixed incorrect path in Desktop Shortcut creation (thanks valkari) +# - v0.13 - Modified Wine Version detection so that Wine 10 as well as future versions should be handled correctly. + +# Current Script Version (alpha) +script_ver=0.13 + +# Today's date +date=$(date +"%d%m%y") + +# Set up and blank logs +LOGFILE=$HOME/wabbajack-via-wine-sh.log +echo "" >$HOME/wabbajack-via-wine-sh.log +#set -x + +###################### +# Fancy banner thing # +###################### + +if [ -f "/usr/bin/toilet" ]; then + toilet -t -f smmono12 -F border:metal "Omni-Guides (alpha)" +else + echo "==================================================================================================" + echo "| ####### ## ## ## ## #### ###### ## ## #### ######## ######## ###### |" + echo "| ## ## ### ### ### ## ## ## ## ## ## ## ## ## ## ## ## |" + echo "| ## ## #### #### #### ## ## ## ## ## ## ## ## ## ## |" + echo "| ## ## ## ### ## ## ## ## ## ####### ## #### ## ## ## ## ## ###### ###### |" + echo "| ## ## ## ## ## #### ## ## ## ## ## ## ## ## ## ## |" + echo "| ## ## ## ## ## ### ## ## ## ## ## ## ## ## ## ## ## |" + echo "| ####### ## ## ## ## #### ###### ####### #### ######## ######## ###### |" + echo "============================================================================~~--(alpha)--~~=======" +fi + +######### +# Intro # +######### + +echo "" +echo -e "This is an experimental script - an attempt to automate as much as possible of the process of getting" +echo -e "Wabbajack running on Linux. Please be aware that stability of the Wabbajack application is not guaranteed." +echo -e "Please use at your own risk and accept that in the worst case, you may have to re-run this script to " +echo -e "create a new prefix for WabbaJack. You can report back to me via GitHub or the Official Wabbajack Discord" +echo -e "if you discover an issue with this script. Any other feedback, positive or negative, is also most welcome." + +echo -e "\e[32m\nPress any key to continue...\e[0m" +echo +read -n 1 -s -r -p "" + +############# +# Functions # +############# + +###################################### +# Detect Wine and winetricks version # +###################################### + +detect_wine_version() { + # Which version of wine is installed? + wine_binary=$(which wine) + echo -e "Wine Binary Path: $wine_binary" >>$LOGFILE 2>&1 + + # Extract the Wine version numbers + wine_version=$(wine --version | grep -oE '[0-9]+\.[0-9]+') + echo -e "Wine Version: $wine_version" >>$LOGFILE 2>&1 + + # Split major and minor version + major_version=$(echo "$wine_version" | cut -d. -f1) + minor_version=$(echo "$wine_version" | cut -d. -f2) + + # Convert to integers for proper numerical comparison + if (( major_version < 9 )) || (( major_version == 9 && minor_version < 15 )); then + echo -e "Wabbajack requires Wine newer than 9.15. Please arrange this on your system and rerun this script." + exit 0 + else + echo -e "Wine version $wine_version, should be fine" >>$LOGFILE 2>&1 + fi + + # Is winetricks installed? + if [[ $(which winetricks) ]]; then + echo -e "Winetricks found at: $(which winetricks)" >>$LOGFILE 2>&1 + else + echo -e "Winetricks not detected. Please arrange this on your system and rerun this script." + exit 0 + fi +} + +########################### +# Get Wabbajack Directory # +########################### + +get_wineprefix_and_application_directory() { + local application_directory_prompt="Enter the path where you want to store your application directory: " + + while true; do + # Prompt for the application directory + read -e -p "$application_directory_prompt" application_directory + echo + + # Check for spaces in the directory path + if [[ $application_directory =~ " " ]]; then + # Suggest an alternative path without spaces + local suggested_path="${application_directory// /_}" + echo -e "\e[31m\nWARNING:\e[0m Spaces in directory paths can cause compatibility issues with some applications." + echo -e "\e[32m\nWould you like to use the following path instead: $suggested_path? (y/n)\e[0m" + read -r confirm + echo + + if [[ $confirm == "y" || $confirm == "Y" ]]; then + application_directory="$suggested_path" + break # Break out of the outer loop + elif [[ $confirm == "n" || $confirm == "N" ]]; then + continue # Loop back to the beginning + else + echo -e "\e[31m\nInvalid input.\e[0m Please enter 'y' or 'n'." + fi + fi + + # Confirm the application directory + while true; do + echo -e "\e[32m\nAre you sure you want to store the application directory in \"$application_directory\"? (y/n): \e[0m" + read -r confirm + echo + + if [[ $confirm == "y" || $confirm == "Y" ]]; then + # Check for existing application directory and warn + break 2 # Break out of both loops + elif [[ $confirm == "n" || $confirm == "N" ]]; then + break # Break out of the inner loop, continue the outer loop + else + echo -e "\e[31m\nInvalid input.\e[0m Please enter 'y' or 'n'." + fi + done + done + + local wineprefix_prompt="Do you want to create the Wine prefix in the default location (\"$application_directory/.wine\")? (y/n): " + + # Ask about the default Wine prefix location + read -e -p "$wineprefix_prompt" confirm + + if [[ $confirm == "y" || $confirm == "Y" ]]; then + # Set the Wine prefix in the default location + export wineprefix="$application_directory/.wine" + else + # Call the get_wineprefix function to get the custom Wine prefix + set_wineprefix + fi + + echo "Application Directory Path: $application_directory." >>$LOGFILE 2>&1 + echo "Wine Prefix Path: $wineprefix" >>$LOGFILE 2>&1 +} + +################### +# Set Wine Prefix # +################### + +set_wineprefix() { + + local wineprefix_prompt="Enter the path where you want to store your Wine prefix: " + + while true; do + # Prompt for the path, allowing tab completion + read -e -p "$wineprefix_prompt" wineprefix + echo + + # Confirm the path + while true; do + echo -e "\e[32m\nAre you sure you want to store the Wine prefix in \"$wineprefix\"? (y/n): \e[0m" + read -r confirm + echo + + if [[ $confirm == "y" || $confirm == "Y" ]]; then + break + elif [[ $confirm == "n" || $confirm == "N" ]]; then + read -e -p "$wineprefix_prompt" wineprefix + else + echo -e "\e[31m\nInvalid input.\e[0m Please enter 'y' or 'n'." + fi + done + + # Check for existing .wine directory + if [[ -d "$wineprefix/.wine" ]]; then + echo -e "\e[31m\nWARNING:\e[0m This will overwrite any existing directory in \"$wineprefix/.wine\"." + while true; do + echo "Continue? (y/n): " + read -r confirm + echo + + if [[ $confirm == "y" || $confirm == "Y" ]]; then + break + elif [[ $confirm == "n" || $confirm == "N" ]]; then + read -e -p "$wineprefix_prompt" wineprefix + else + echo -e "\e[31m\nInvalid input.\e[0m Please enter 'y' or 'n'." + fi + done + else + break + fi + done + + echo + + # Set the wineprefix variable + export wineprefix + +} + +######################################### +# Create Wabbajack Directory and prefix # +######################################### + +create_wine_environment() { + # Create the application directory if it doesn't exist + mkdir -p "$application_directory" + + # Check if the Wine prefix exists and delete it if necessary + if [[ -d "$wineprefix" ]]; then + rm -rf "$wineprefix" + fi + + # Create the Wine prefix directory + mkdir -p "$wineprefix" + + # Set the WINEPREFIX variable and run wineboot + export WINEPREFIX="$wineprefix" + #export WINEDLLOVERRIDES="mscoree=d;mshtml=d" + wineboot >>$LOGFILE 2>&1 +} + +######################################################## +# Download Webview Installer and Wabbajack Application # +######################################################## + +download_apps() { + + echo -e "\e[33m\nDownloading Wabbajack Application...\e[0m" + + # Check if Wabbajack.exe exists and skip download if so + if ! [ -f "$application_directory/Wabbajack.exe" ]; then + wget https://github.com/wabbajack-tools/wabbajack/releases/latest/download/Wabbajack.exe -O "$application_directory/Wabbajack.exe" + # Set as executable + chmod +x "$application_directory/Wabbajack.exe" + else + echo "Wabbajack.exe already exists, skipping download." + fi + + echo -e "\e[33m\nDownloading WebView Installer...\e[0m" + + # Check if MicrosoftEdgeWebView2RuntimeInstallerX64.exe exists and skip download if so + if ! [ -f "$application_directory/MicrosoftEdgeWebView2RuntimeInstallerX64.exe" ]; then + wget https://msedge.sf.dl.delivery.mp.microsoft.com/filestreamingservice/files/6d376ab4-4a07-4679-8918-e0dc3c0735c8/MicrosoftEdgeWebView2RuntimeInstallerX64.exe -O "$application_directory/MicrosoftEdgeWebView2RuntimeInstallerX64.exe" + else + echo "MicrosoftEdgeWebView2RuntimeInstallerX64.exe already exists, skipping download." + fi + +} + +############################################ +# Install WebView, configure Wine settings # +############################################ + +install_and_configure() { + + # set based on distro? harware?... + echo -e "\e[33m\nChanging the default renderer used..\e[0m" >>$LOGFILE 2>&1 + WINEPREFIX=$wineprefix winetricks renderer=vulkan >>$LOGFILE 2>&1 + + # Install WebView + echo -e "\e[33m\nInstalling Webview, this can take a while, please be patient..\e[0m" >>$LOGFILE 2>&1 + WINEPREFIX=$wineprefix wine $application_directory/MicrosoftEdgeWebView2RuntimeInstallerX64.exe >>$LOGFILE 2>&1 + + # Change prefix version + echo -e "\e[33m\nChange the default prefix version to win7..\e[0m" >>$LOGFILE 2>&1 + WINEPREFIX=$wineprefix winecfg -v win7 >>$LOGFILE 2>&1 + + # Add Wabbajack as an application + echo -e "\e[33m\nAdding Wabbajack Application to customise settings..\e[0m" >>$LOGFILE 2>&1 + cat <$application_directory/WJApplication.reg +Windows Registry Editor Version 5.00 + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\Wabbajack.exe] +"Version"="win10" +EOF + + WINEPREFIX=$wineprefix wine regedit $application_directory/WJApplication.reg >>$LOGFILE 2>&1 + + echo +} + +################################# +# Detect and Link Steam Library # +################################# + +detect_link_steam_library() { + # Possible Steam library locations + steam_library_locations=( + "$HOME/.local/share/Steam" + #"$HOME/.steam/steam/steamapps" + "$HOME/Library/Application Support/Steam" + "/opt/steam" + "/usr/share/Steam" + "/usr/local/share/Steam" + ) + + # Function to check if a directory is a Steam library + is_steam_library() { + local location="$1" + + if [[ -d "$location" ]]; then + if find "$location/steamapps" -type f -name "libraryfolders.vdf" -print | grep -q "$location/steamapps/libraryfolders.vdf"; then + return 0 + fi + fi + + return 1 + } + + echo -e "\e[33mDiscovering Steam libraries..\e[0m" + + # Find the first valid Steam library location + for location in "${steam_library_locations[@]}"; do + if is_steam_library "$location"; then + read -p "Found Steam install at '$location' Is this path correct for your Steam install? (y/n): " -r choice + if [[ "$choice" =~ ^[Yy]$ ]]; then + chosen_library="$location" + break + fi + fi + done + + # If no library was found or the user declined, ask for a custom path + if [[ -z "$chosen_library" ]]; then + read -e -p "Enter the path to your main Steam directory: " steam_library_path + while true; do + if [[ ! -d "$steam_library_path" ]]; then + echo -e "\e[31m\nInvalid path.\e[0m Please enter a valid directory." + elif ! is_steam_library "$steam_library_path"; then + echo -e "\e[31m\nThe specified path does not appear to be a Steam directory. Please check the path and try again. Do not enter the path for a secondary Steam Library, only the path for your actual Steam install.\e[0m" + else + read -p "Confirm using '$steam_library_path' as the Steam directory path? (y/n): " -r choice + if [[ "$choice" =~ ^[Yy]$ ]]; then + chosen_library="$steam_library_path" + break + fi + fi + read -e -p "Enter the path to your Steam library: " steam_library_path + done + fi + + # If a valid library was found, print its location and create the symlink + if [[ -n "$chosen_library" ]]; then + echo "Steam library found at: $chosen_library" >>$LOGFILE 2>&1 + configure_steam_libraries + else + echo -e "\e[31m\nSteam library not found. Please check the installation.\e[0m" + fi + +} + +configure_steam_libraries() { + +# Make directories +#wineprefix=/home/deck/WJTest +steam_config_directory="$wineprefix/drive_c/Program Files (x86)/Steam/config" +echo -e "Creating directory $steam_config_directory" >>$LOGFILE 2>&1 +mkdir -p "$steam_config_directory" + +# copy real libraryfolders.vdf to config directory +echo -e "Copying libraryfolders.vdf to config directory" >>$LOGFILE 2>&1 +cp "$chosen_library/config/libraryfolders.vdf" "$steam_config_directory/." + +# Edit this new libraryfolders.vdf file to convert linux path to Z:\ path with double backslashes + +sed -E 's|("path"[[:space:]]+)"(/)|\1"Z:\\\\|; s|/|\\\\|g' "$steam_config_directory/libraryfolders.vdf" > "$steam_config_directory/libraryfolders2.vdf" +cp "$steam_config_directory/libraryfolders2.vdf" "$steam_config_directory/libraryfolders.vdf" +rm "$steam_config_directory/libraryfolders2.vdf" + +} + +########################################## +# Create dotnet_bundle_extract directory # +########################################## + +create_dotnet_cache_dir() { + local user_name=$(whoami) + local cache_dir="$application_directory/home/$user_name/.cache/dotnet_bundle_extract" + + mkdir -p "$cache_dir" +} + +############################ +# Create Desktop Shortcut? # +############################ + +create_desktop_shortcut() { + echo -e "\e[32m\nDo you want to create a desktop shortcut for Wabbajack? (y/n):\e[0m" + read -r create_shortcut + + if [[ $create_shortcut == "y" || $create_shortcut == "Y" ]]; then + desktop_file="$HOME/Desktop/Wabbajack.desktop" + cat >"$desktop_file" <>$LOGFILE 2>&1 + fi +} + +##################### +# Run the Functions # +##################### + +# Detect Wine and winetricks version +detect_wine_version + +# Get Wabbajack Directory +get_wineprefix_and_application_directory + +# Create Wabbajack Directory +create_wine_environment + +# Download Webview Installer and Wabbajack Application +download_apps + +# Install WebView, configure Wine settings +install_and_configure + +# Detect and Link Steam Library +detect_link_steam_library + +# Create dotnet_bundle_extract directory +create_dotnet_cache_dir + +# Create Desktop Shortcut? +create_desktop_shortcut + +# Start Wabbajack? +start_wabbajack + +echo -e "\e[32m\nSet up complete.\e[0m" + +exit diff --git a/binaries/omni-guides-testing.sh b/binaries/omni-guides-testing.sh new file mode 100644 index 0000000..df8de52 --- /dev/null +++ b/binaries/omni-guides-testing.sh @@ -0,0 +1,1783 @@ +#!/usr/bin/env bash +# +################################################### +# # +# A tool for running Wabbajack modlists on Linux # +# # +# Beta v0.69t - Omni 03/18/2025 # +# # +################################################### + +# Full Changelog can be found here: https://github.com/Omni-guides/Wabbajack-Modlist-Linux/blob/main/binaries/omni-guides-sh.changelog.txt + +# Parse --debug or -d flag before anything else +DEBUG=0 +for arg in "$@"; do + if [[ "$arg" == "--debug" || "$arg" == "-d" ]]; then + DEBUG=1 + export DEBUG + break + fi + # Do not shift here, just scan args + +done + +# Current Script Version (beta) +script_ver=0.69t + +# Define modlist-specific configurations +declare -A modlist_configs=( + ["wildlander"]="dotnet472" + ["librum|apostasy"]="dotnet40 dotnet8" + ["nordicsouls"]="dotnet40" + ["livingskyrim|lsiv|ls4"]="dotnet40" + ["lostlegacy"]="dotnet48" +) + +# Set up and blank logs (simplified) +LOGFILE=$HOME/omni-guides-sh.log +echo "" >$HOME/omni-guides-sh.log + +# Add our new logging function +log_status() { + local level="$1" + local message="$2" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + + # Always write to log file with timestamp but without color codes + echo "[$timestamp] [$level] $(echo "$message" | sed 's/\x1b\[[0-9;]*m//g')" >> "$LOGFILE" + + # If DEBUG=1, print all messages to terminal, including DEBUG + if [[ "$DEBUG" == "1" ]]; then + echo -e "$message" + else + # Only display non-DEBUG messages to the user, preserving color codes + if [ "$level" != "DEBUG" ]; then + echo -e "$message" + fi + fi +} + +# Display banner +echo "╔══════════════════════════════════════════════════════════════════╗" +echo "║ Omni-Guides (beta) ║" +echo "║ ║" +echo "║ A tool for running Wabbajack modlists on Linux ║" +echo "╚══════════════════════════════════════════════════════════════════╝" + +######### +# Intro # +######### +echo "" +log_status "INFO" "Omni-Guides Wabbajack Post-Install Script v$script_ver" +echo "───────────────────────────────────────────────────────────────────" +log_status "INFO" "This script automates the post-install steps for Wabbajack modlists on Linux/Steam Deck." +log_status "INFO" "It will configure your modlist location, install required components, and apply necessary fixes." +echo "" +log_status "WARN" "⚠ IMPORTANT: Use this script at your own risk." +log_status "INFO" "Please report any issues via GitHub (Omni-guides/Wabbajack-Modlist-Linux)." +echo "───────────────────────────────────────────────────────────────────" +echo -e "\e[33mPress any key to continue...\e[0m" +read -n 1 -s -r -p "" + +############# +# Functions # +############# + +########################## +# Cleanup Wine Processes # +########################## + +cleanup_wine_procs() { + + # Find and kill processes containing various process names + processes=$(pgrep -f "win7|win10|ShowDotFiles|protontricks") + if [[ -n "$processes" ]]; then + echo "$processes" | xargs kill -9 + echo "Processes killed successfully." >>$LOGFILE 2>&1 + else + echo "No matching processes found." >>$LOGFILE 2>&1 + fi + + pkill -9 winetricks + +} + +############# +# Set APPID # +############# + +set_appid() { + + echo "DEBUG: Extracting APPID from choice: '$choice'" >>$LOGFILE 2>&1 + APPID=$(echo "$choice" | awk -F'[()]' '{print $2}') + echo "DEBUG: Extracted APPID: '$APPID'" >>$LOGFILE 2>&1 + + #APPID=$(echo $choice | awk {'print $NF'} | sed 's:^.\(.*\).$:\1:') + echo "APPID=$APPID" >>$LOGFILE 2>&1 + + if [ -z "$APPID" ]; then + echo "Error: APPID cannot be empty, exiting... Please tell Omni :(" + cleaner_exit + fi + +} + +############################# +# Detect if running on deck # +############################# + +detect_steamdeck() { + # Steamdeck or nah? + + if [ -f "/etc/os-release" ] && grep -q "steamdeck" "/etc/os-release"; then + steamdeck=1 + echo "Running on Steam Deck" >>$LOGFILE 2>&1 + else + steamdeck=0 + echo "NOT A steamdeck" >>$LOGFILE 2>&1 + fi + +} + +# Modlist-specific steps spinner wrapper +modlist_specific_steps_spinner() { + echo "[DEBUG] modlist_specific_steps_spinner: DEBUG=$DEBUG" >&2 + if [ "$DEBUG" = "1" ]; then + modlist_specific_steps + else + local pid + local spinner=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') + modlist_specific_steps >>"$LOGFILE" 2>&1 & + pid=$! + local i=0 + local bar="=============== " + local msg="Progress: [${bar}] 30% - Running modlist-specific steps... " + while kill -0 "$pid" 2>/dev/null; do + printf "\r\033[K%s%s" "$msg" "${spinner[i]}" + i=$(( (i+1) % 10 )) + sleep 0.1 + done + wait "$pid" + printf "\r\033[K" + fi +} + +########################################### +# Detect Protontricks (flatpak or native) # +########################################### + +detect_protontricks() { + echo -ne "\nDetecting if protontricks is installed..." >>$LOGFILE 2>&1 + + # Check if protontricks exists + if command -v protontricks >/dev/null 2>&1; then + protontricks_path=$(command -v protontricks) + # Check if the detected binary is actually a Flatpak wrapper + if [[ -f "$protontricks_path" ]] && grep -q "flatpak run" "$protontricks_path"; then + echo -e "Detected Protontricks is actually a Flatpak wrapper at $protontricks_path." >>$LOGFILE 2>&1 + which_protontricks=flatpak + return 0 + else + echo -e "Native Protontricks found at $protontricks_path." | tee -a $LOGFILE + which_protontricks=native + return 0 # Exit function since we confirmed native protontricks + fi + else + echo -e "Non-Flatpak Protontricks not found. Checking flatpak..." >>$LOGFILE 2>&1 + if flatpak list | grep -iq protontricks; then + echo -e "Flatpak Protontricks is already installed." >>$LOGFILE 2>&1 + which_protontricks=flatpak + else + echo -e "\e[31m\n** Protontricks not found. Do you wish to install it? (y/n): **\e[0m" + read -p " " answer + if [[ $answer =~ ^[Yy]$ ]]; then + if [[ $steamdeck -eq 1 ]]; then + if flatpak install -u -y --noninteractive flathub com.github.Matoking.protontricks; then + which_protontricks=flatpak + else + echo -e "\n\e[31mFailed to install Protontricks via Flatpak. Please install it manually and rerun this script.\e[0m" | tee -a $LOGFILE + exit 1 + fi + else + read -p "Choose installation method: 1) Flatpak (preferred) 2) Native: " choice + if [[ $choice =~ 1 ]]; then + if flatpak install -u -y --noninteractive flathub com.github.Matoking.protontricks; then + which_protontricks=flatpak + else + echo -e "\n\e[31mFailed to install Protontricks via Flatpak. Please install it manually and rerun this script.\e[0m" | tee -a $LOGFILE + exit 1 + fi + else + echo -e "\nSorry, there are too many distros to automate this!" | tee -a $LOGFILE + echo -e "Please check how to install Protontricks using your OS package manager (yum, dnf, apt, pacman, etc.)" | tee -a $LOGFILE + echo -e "\e[31mProtontricks is required for this script to function. Exiting.\e[0m" | tee -a $LOGFILE + exit 1 + fi + fi + else + echo -e "\e[31mProtontricks is required for this script to function. Exiting.\e[0m" | tee -a $LOGFILE + exit 1 + fi + fi + # After any install attempt, re-check for protontricks + if ! command -v protontricks >/dev/null 2>&1 && ! flatpak list | grep -iq protontricks; then + echo -e "\e[31mProtontricks is still not installed after attempted installation. Exiting.\e[0m" | tee -a $LOGFILE + exit 1 + fi + fi +} + +############################# +# Run protontricks commands # +############################# + +run_protontricks() { + # Determine the protontricks binary path and create command array + if [ "$which_protontricks" = "flatpak" ]; then + local cmd=(flatpak run com.github.Matoking.protontricks) + else + local cmd=(protontricks) + fi + + # Execute the command with all arguments + "${cmd[@]}" "$@" +} + +############################### +# Detect Protontricks Version # +############################### + +protontricks_version() { + # Get the current version of protontricks + protontricks_version=$(run_protontricks -V | cut -d ' ' -f 2 | sed 's/[()]//g') + + # Remove any non-numeric characters from the version number + protontricks_version_cleaned=$(echo "$protontricks_version" | sed 's/[^0-9.]//g') + + echo "Protontricks Version Cleaned = $protontricks_version_cleaned" >> "$LOGFILE" 2>&1 + + # Split the version into digits + IFS='.' read -r first_digit second_digit third_digit <<< "$protontricks_version_cleaned" + + # Check if the second digit is defined and greater than or equal to 12 + if [[ -n "$second_digit" && "$second_digit" -lt 12 ]]; then + echo "Your protontricks version is too old! Update to version 1.12 or newer and rerun this script. If 'flatpak run com.github.Matoking.protontricks -V' returns 'unknown', then please update via flatpak." | tee -a "$LOGFILE" + cleaner_exit + fi +} + +####################################### +# Detect Skyrim or Fallout 4 Function # +####################################### + +detect_game() { + # Define lookup table for games + declare -A game_lookup=( + ["Skyrim"]="Skyrim Special Edition" + ["Fallout 4"]="Fallout 4" + ["Fallout New Vegas"]="Fallout New Vegas" + ["FNV"]="Fallout New Vegas" + ["Oblivion"]="Oblivion" + ) + + # Try direct match first + for pattern in "${!game_lookup[@]}"; do + if [[ $choice == *"$pattern"* ]]; then + gamevar="${game_lookup[$pattern]}" + which_game="${gamevar%% *}" + echo "Game variable set to $which_game." >>"$LOGFILE" 2>&1 + echo "Game variable: $gamevar" >>"$LOGFILE" 2>&1 + return 0 + fi + done + + # Handle generic "Fallout" case + if [[ $choice == *"Fallout"* ]]; then + PS3="Please select a Fallout game (enter the number): " + select fallout_opt in "Fallout 4" "Fallout New Vegas"; do + if [[ -n $fallout_opt ]]; then + gamevar="$fallout_opt" + which_game="${gamevar%% *}" + echo "Game variable set to $which_game." >>"$LOGFILE" 2>&1 + echo "Game variable: $gamevar" >>"$LOGFILE" 2>&1 + return 0 + else + echo "Invalid option" + fi + done + fi + + # If no match found, show selection menu + PS3="Please select a game (enter the number): " + select opt in "Skyrim" "Fallout 4" "Fallout New Vegas" "Oblivion"; do + if [[ -n $opt ]]; then + gamevar="${game_lookup[$opt]}" + which_game="${gamevar%% *}" + echo "Game variable set to $which_game." >>"$LOGFILE" 2>&1 + echo "Game variable: $gamevar" >>"$LOGFILE" 2>&1 + return 0 + else + echo "Invalid option" + fi + done +} + +################################### +# Try to detect the Steam Library # +################################### + +detect_steam_library() { + + local libraryfolders_vdf="$HOME/.steam/steam/config/libraryfolders.vdf" + + if [[ ! -f "$libraryfolders_vdf" ]]; then + echo "libraryfolders.vdf not found in ~/.steam/steam/config/. Please ensure Steam is installed." | tee -a "$LOGFILE" + return 1 + fi + + local library_paths=() + while IFS='' read -r line; do + if [[ "$line" =~ \"path\" ]]; then + local path=$(echo "$line" | sed 's/.*"path"\s*"\(.*\)"/\1/') + if [[ -n "$path" ]]; then + library_paths+=("$path/steamapps/common") + fi + fi + done <"$libraryfolders_vdf" + + local found=0 + for library_path in "${library_paths[@]}"; do + if [[ -d "$library_path/$gamevar" ]]; then + steam_library="$library_path" + found=1 + echo "Found '$gamevar' in $steam_library." >>$LOGFILE 2>&1 + break + else + echo "Checking $library_path: '$gamevar' not found." >>$LOGFILE 2>&1 + fi + done + + if [[ "$found" -eq 0 ]]; then + echo "Vanilla game not found in Steam library locations." | tee -a "$LOGFILE" + + while true; do + echo -e "\n** Enter the path to your Vanilla $gamevar directory manually (e.g. /data/SteamLibrary/steamapps/common/$gamevar): **" + read -e -r gamevar_input + + steam_library_input="${gamevar_input%/*}/" + + if [[ -d "$steam_library_input/$gamevar" ]]; then + steam_library="$steam_library_input" + echo "Found $gamevar in $steam_library_input." | tee -a "$LOGFILE" + echo "Steam Library set to: $steam_library" >>$LOGFILE 2>&1 + break + else + echo "Game not found in $steam_library_input. Please enter a valid path to Vanilla $gamevar." | tee -a "$LOGFILE" + fi + done + fi + + echo "Steam Library Location: $steam_library" >>$LOGFILE 2>&1 + + if [[ "$steamdeck" -eq 1 && "$steam_library" == "/run/media"* ]]; then + basegame_sdcard=1 + fi + +} + +################################# +# Detect Modlist Directory Path # +################################# + +detect_modlist_dir_path() { + log_status "DEBUG" "Detecting $MODLIST Install Directory..." + local modlist_paths=() + local choice modlist_ini_temp + local pattern=$(echo "$MODLIST" | sed 's/ /.*\|/g') + + # Search for ModOrganizer.exe entries matching the modlist pattern + while IFS= read -r entry; do + modlist_paths+=("$(dirname "${entry//[\"\']/}")") + done < <(strings ~/.steam/steam/userdata/*/config/shortcuts.vdf | grep -iE "ModOrganizer.exe" | grep -iE "$pattern") + + # If no exact matches, get all ModOrganizer.exe instances + if [[ ${#modlist_paths[@]} -eq 0 ]]; then + echo "No exact matches found. Searching for all ModOrganizer.exe instances..." + while IFS= read -r entry; do + modlist_paths+=("$(dirname "${entry//[\"\']/}")") + done < <(strings ~/.steam/steam/userdata/*/config/shortcuts.vdf | grep -iE "ModOrganizer.exe") + fi + + # Handle different cases based on number of paths found + if [[ ${#modlist_paths[@]} -eq 0 ]]; then + # No paths found - must enter manually + echo -e "\e[34mNo ModOrganizer.exe entries found. Please enter the directory manually:\e[0m" + read -r -e modlist_dir + elif [[ ${#modlist_paths[@]} -eq 1 ]]; then + # Single path found - use it directly without output + modlist_dir="${modlist_paths[0]}" + else + # Multiple paths found - show selection menu + echo "Select the ModOrganizer directory:" + for i in "${!modlist_paths[@]}"; do + echo -e "\e[33m$((i + 1))) ${modlist_paths[i]}\e[0m" + done + echo -e "\e[34m$(( ${#modlist_paths[@]} + 1 ))) Enter path manually\e[0m" + + while true; do + read -p "Enter your choice (1-$((${#modlist_paths[@]} + 1))): " choice + if [[ "$choice" =~ ^[0-9]+$ && "$choice" -ge 1 && "$choice" -le $(( ${#modlist_paths[@]} + 1 )) ]]; then + if [[ "$choice" -eq $(( ${#modlist_paths[@]} + 1 )) ]]; then + echo -ne "\e[34mEnter the ModOrganizer directory path: \e[0m" + read -r -e modlist_dir + else + modlist_dir="${modlist_paths[choice - 1]}" + fi + break + else + echo "Invalid selection. Please try again." + fi + done + fi + + # Validate selection + modlist_ini_temp="$modlist_dir/ModOrganizer.ini" + while [[ ! -f "$modlist_ini_temp" ]]; do + echo "ModOrganizer.ini not found in $modlist_dir. Please enter a valid path." + echo -ne "\e[34mEnter the ModOrganizer directory path: \e[0m" + read -r -e modlist_dir + modlist_ini_temp="$modlist_dir/ModOrganizer.ini" + done + + # Save and log results + modlist_ini="$modlist_ini_temp" + echo "Modlist directory: $modlist_dir" >> "$LOGFILE" + echo "Modlist INI location: $modlist_ini" >> "$LOGFILE" +} + +##################################################### +# Set protontricks permissions on Modlist Directory # +##################################################### + +set_protontricks_perms() { + if [ "$which_protontricks" = "flatpak" ]; then + log_status "INFO" "\nSetting Protontricks permissions..." + flatpak override --user com.github.Matoking.protontricks --filesystem="$modlist_dir" + log_status "SUCCESS" "Done!" + + if [[ $steamdeck = 1 ]]; then + log_status "WARN" "\nChecking for SDCard and setting permissions appropriately.." + sdcard_path=$(df -h | grep "/run/media" | awk {'print $NF'}) + echo "$sdcard_path" >>$LOGFILE 2>&1 + flatpak override --user --filesystem=$sdcard_path com.github.Matoking.protontricks + flatpak override --user --filesystem=/run/media/mmcblk0p1 com.github.Matoking.protontricks + log_status "SUCCESS" "Done." + fi + else + log_status "DEBUG" "Using Native protontricks, skip setting permissions" + fi +} + +##################################### +# Enable Visibility of (.)dot files # +##################################### + +enable_dotfiles() { + log_status "DEBUG" "APPID=$APPID" + log_status "INFO" "\nEnabling visibility of (.)dot files..." + + # Completely redirect all output to avoid any wine debug messages + if [[ "$DEBUG" == "1" ]]; then + dotfiles_check=$(run_protontricks -c 'wine reg query "HKEY_CURRENT_USER\Software\Wine" /v ShowDotFiles' $APPID | grep ShowDotFiles | awk '{gsub(/\r/,""); print $NF}') + else + dotfiles_check=$(WINEDEBUG=-all run_protontricks -c 'wine reg query "HKEY_CURRENT_USER\Software\Wine" /v ShowDotFiles' $APPID > /dev/null 2>&1; \ + WINEDEBUG=-all run_protontricks -c 'wine reg query "HKEY_CURRENT_USER\Software\Wine" /v ShowDotFiles' $APPID 2>/dev/null | grep ShowDotFiles | awk '{gsub(/\r/,""); print $NF}') + fi + + log_status "DEBUG" "Current dotfiles setting: $dotfiles_check" + + if [[ "$dotfiles_check" = "Y" ]]; then + log_status "INFO" "DotFiles already enabled via registry... skipping" + else + # Method 2: Set registry key (standard approach) + log_status "DEBUG" "Setting ShowDotFiles registry key..." + if [[ "$DEBUG" == "1" ]]; then + run_protontricks -c 'wine reg add "HKEY_CURRENT_USER\Software\Wine" /v ShowDotFiles /d Y /f' $APPID + else + WINEDEBUG=-all run_protontricks -c 'wine reg add "HKEY_CURRENT_USER\Software\Wine" /v ShowDotFiles /d Y /f' $APPID > /dev/null 2>&1 + fi + # Method 3: Also try direct winecfg approach as backup + log_status "DEBUG" "Also setting via winecfg command..." + if [[ "$DEBUG" == "1" ]]; then + run_protontricks -c 'winecfg /v wine' $APPID + else + WINEDEBUG=-all run_protontricks -c 'winecfg /v wine' $APPID > /dev/null 2>&1 + fi + # Method 4: Create user.reg entry if it doesn't exist + log_status "DEBUG" "Ensuring user.reg has correct entry..." + if [[ "$DEBUG" == "1" ]]; then + prefix_path=$(run_protontricks -c 'echo $WINEPREFIX' $APPID 2>/dev/null) + else + prefix_path=$(WINEDEBUG=-all run_protontricks -c 'echo $WINEPREFIX' $APPID 2>/dev/null) + fi + if [[ -n "$prefix_path" && -d "$prefix_path" ]]; then + if [[ -f "$prefix_path/user.reg" ]]; then + if ! grep -q "ShowDotFiles" "$prefix_path/user.reg" 2>/dev/null; then + echo '[Software\\Wine] 1603891765' >> "$prefix_path/user.reg" 2>/dev/null + echo '"ShowDotFiles"="Y"' >> "$prefix_path/user.reg" 2>/dev/null + fi + fi + fi + # Verify the setting took effect + if [[ "$DEBUG" == "1" ]]; then + dotfiles_verify=$(run_protontricks -c 'wine reg query "HKEY_CURRENT_USER\Software\Wine" /v ShowDotFiles' $APPID | grep ShowDotFiles | awk '{gsub(/\r/,""); print $NF}') + else + dotfiles_verify=$(WINEDEBUG=-all run_protontricks -c 'wine reg query "HKEY_CURRENT_USER\Software\Wine" /v ShowDotFiles' $APPID > /dev/null 2>&1; \ + WINEDEBUG=-all run_protontricks -c 'wine reg query "HKEY_CURRENT_USER\Software\Wine" /v ShowDotFiles' $APPID 2>/dev/null | grep ShowDotFiles | awk '{gsub(/\r/,""); print $NF}') + fi + log_status "DEBUG" "Verification check: $dotfiles_verify" + log_status "SUCCESS" "Done!" + fi +} + +############################################### +# Set Windows 10 version in the proton prefix # +############################################### + +set_win10_prefix() { + if [[ "$DEBUG" == "1" ]]; then + run_protontricks --no-bwrap $APPID win10 + else + WINEDEBUG=-all run_protontricks --no-bwrap $APPID win10 >/dev/null 2>&1 + fi +} + +###################################### +# Install Wine Components & VCRedist # +###################################### + +install_wine_components() { + log_status "INFO" "Installing Wine Components... This can take some time, be patient!" + + # Define game-specific component sets + local protontricks_appid="$APPID" + local protontricks_components=() + + # Common components for all games + local common_components=("fontsmooth=rgb" "xact" "xact_x64" "vcrun2022") + + # Game-specific configuration + case "$gamevar" in + "Skyrim Special Edition"|"Fallout 4") + protontricks_components=("${common_components[@]}" "d3dcompiler_47" "d3dx11_43" "d3dcompiler_43" "dotnet6" "dotnet7") + ;; + "Fallout New Vegas") + protontricks_components=("${common_components[@]}" "d3dx9_43" "d3dx9") + protontricks_appid="22380" # Force appid for FNV + ;; + "Oblivion") + protontricks_components=("${common_components[@]}" "d3dx9_43" "d3dx9") + ;; + *) + echo "Unsupported game: $gamevar" | tee -a "$LOGFILE" + return 1 + ;; + esac + + # Log the command we're about to run + echo "Installing components: ${protontricks_components[*]}" >>$LOGFILE 2>&1 + + # Run the installation with progress indicator + printf "Protontricks running... " + + # Try up to 3 times to install components + local max_attempts=3 + local attempt=1 + local success=false + + while [[ $attempt -le $max_attempts && $success == false ]]; do + if [[ $attempt -gt 1 ]]; then + echo "Retry attempt $attempt/$max_attempts..." | tee -a "$LOGFILE" + sleep 2 + fi + if [[ "$DEBUG" == "1" ]]; then + run_protontricks --no-bwrap "$protontricks_appid" -q "${protontricks_components[@]}" + result=$? + else + WINEDEBUG=-all run_protontricks --no-bwrap "$protontricks_appid" -q "${protontricks_components[@]}" >/dev/null 2>&1 + result=$? + fi + if [[ $result -eq 0 ]]; then + success=true + else + echo "Attempt $attempt failed, cleaning up wine processes before retry..." >>$LOGFILE 2>&1 + cleanup_wine_procs + attempt=$((attempt+1)) + fi + done + + if [[ $success == true ]]; then + printf "Done.\n" + log_status "SUCCESS" "Wine Component installation completed." + else + printf "Failed.\n" + log_status "ERROR" "Component install failed after $max_attempts attempts." + return 1 + fi + + # Verify installation + log_status "DEBUG" "Verifying installed components..." + local output + output=$(run_protontricks --no-bwrap "$protontricks_appid" list-installed 2>/dev/null) + + # Clean up and deduplicate the component list + local cleaned_output + cleaned_output=$(echo "$output" | grep -v "Using winetricks" | sort -u | grep -v '^$') + log_status "DEBUG" "Installed components (unique):" + echo "$cleaned_output" >> "$LOGFILE" + + # Check for critical components only to avoid false negatives + local critical_components=("vcrun2022" "xact") + local missing_components=() + + for component in "${critical_components[@]}"; do + if ! grep -q "$component" <<<"$output"; then + missing_components+=("$component") + fi + done + + if [[ ${#missing_components[@]} -gt 0 ]]; then + echo -e "\nERROR: Some critical components are missing: ${missing_components[*]}" | tee -a "$LOGFILE" + return 1 + else + echo "Critical components verified successfully." >>$LOGFILE 2>&1 + fi + + return 0 +} + +############################################ +# Detect default compatdata Directory Path # +############################################ +default_steam_compatdata_dir() { + # Prefer ~/.local/share/Steam if it exists + if [[ -d "$HOME/.local/share/Steam/steamapps/compatdata" ]]; then + echo "$HOME/.local/share/Steam/steamapps/compatdata" + elif [[ -d "$HOME/.steam/steam/steamapps/compatdata" ]]; then + echo "$HOME/.steam/steam/steamapps/compatdata" + else + # Do not create the directory; just return empty string + echo "" + fi +} + +# Helper to get all Steam library folders from libraryfolders.vdf +get_all_steam_libraries() { + local vdf_file="$HOME/.steam/steam/config/libraryfolders.vdf" + local libraries=("$HOME/.local/share/Steam" "$HOME/.steam/steam") + if [[ -f "$vdf_file" ]]; then + while IFS='' read -r line; do + if [[ "$line" =~ "path" ]]; then + local path=$(echo "$line" | sed 's/.*"path"\s*"\(.*\)"/\1/') + if [[ -n "$path" ]]; then + libraries+=("$path") + fi + fi + done <"$vdf_file" + fi + echo "${libraries[@]}" +} + +#################################### +# Detect compatdata Directory Path # +#################################### + +detect_compatdata_path() { + local appid_to_check="$APPID" + if [[ "$gamevar" == "Fallout New Vegas" ]]; then + appid_to_check="22380" + local vdf_file="$HOME/.steam/steam/config/libraryfolders.vdf" + local libraries=("$HOME/.local/share/Steam" "$HOME/.steam/steam") + # Parse all additional libraries from the VDF + if [[ -f "$vdf_file" ]]; then + while IFS= read -r line; do + if [[ "$line" =~ "path" ]]; then + local path=$(echo "$line" | sed -E 's/.*"path"[ \t]*"([^"]+)".*/\1/') + if [[ "$path" == /* ]]; then + libraries+=("$path") + else + libraries+=("$HOME/$path") + fi + fi + done < "$vdf_file" + fi + compat_data_path="" + for lib in "${libraries[@]}"; do + local compat_path="$lib/steamapps/compatdata/$appid_to_check" + log_status "DEBUG" "Checking for compatdata at: $compat_path" + if [[ -d "$compat_path" ]]; then + compat_data_path="$compat_path" + log_status "DEBUG" "Found FNV compatdata: $compat_data_path" + break + fi + done + if [[ -z "$compat_data_path" ]]; then + log_status "ERROR" "Could not find compatdata directory for Fallout New Vegas (22380) in any Steam library." + log_status "ERROR" "Please ensure you have launched the vanilla Fallout New Vegas game at least once via Steam in the correct library." + return 1 + fi + return 0 + fi + # ... (existing logic for other games) + # Check common Steam library locations first + for path in "$HOME/.local/share/Steam/steamapps/compatdata" "$HOME/.steam/steam/steamapps/compatdata"; do + if [[ -d "$path/$appid_to_check" ]]; then + compat_data_path="$path/$appid_to_check" + log_status "DEBUG" "compatdata Path detected: $compat_data_path" + break + fi + done + + # If not found in common locations, use find command + if [[ -z "$compat_data_path" ]]; then + find / -type d -name "compatdata" 2>/dev/null | while read -r compatdata_dir; do + if [[ -d "$compatdata_dir/$appid_to_check" ]]; then + compat_data_path="$compatdata_dir/$appid_to_check" + log_status "DEBUG" "compatdata Path detected: $compat_data_path" + break + fi + done + fi + + if [[ -z "$compat_data_path" ]]; then + log_status "ERROR" "Directory named '$appid_to_check' not found in any compatdata directories." + log_status "ERROR" "Please ensure you have started the Steam entry for the modlist at least once, even if it fails.." + else + log_status "DEBUG" "Found compatdata directory with '$appid_to_check': $compat_data_path" + fi +} + +######################### +# Detect Proton Version # +######################### + +detect_proton_version() { + log_status "DEBUG" "Detecting Proton version..." + + # Validate the compatdata path exists + if [[ ! -d "$compat_data_path" ]]; then + log_status "WARN" "Compatdata directory not found at '$compat_data_path'" + proton_ver="Unknown" + return 1 + fi + + # First try to get Proton version from the registry + if [[ -f "$compat_data_path/pfx/system.reg" ]]; then + local reg_output + reg_output=$(grep -A 3 "\"SteamClientProtonVersion\"" "$compat_data_path/pfx/system.reg" | grep "=" | cut -d= -f2 | tr -d '"' | tr -d ' ') + + if [[ -n "$reg_output" ]]; then + # Keep GE versions as is, otherwise prefix with "Proton" + if [[ "$reg_output" == *"GE"* ]]; then + proton_ver="$reg_output" # Keep GE versions as is + else + proton_ver="Proton $reg_output" + fi + log_status "DEBUG" "Detected Proton version from registry: $proton_ver" + return 0 + fi + fi + + # Fallback to config_info if registry method fails + if [[ -f "$compat_data_path/config_info" ]]; then + local config_ver + config_ver=$(head -n 1 "$compat_data_path/config_info") + if [[ -n "$config_ver" ]]; then + # Keep GE versions as is, otherwise prefix with "Proton" + if [[ "$config_ver" == *"GE"* ]]; then + proton_ver="$config_ver" # Keep GE versions as is + else + proton_ver="Proton $config_ver" + fi + log_status "DEBUG" "Detected Proton version from config_info: $proton_ver" + return 0 + fi + fi + + proton_ver="Unknown" + log_status "WARN" "Could not detect Proton version" + return 1 +} + +############################### +# Confirmation before running # +############################### + +confirmation_before_running() { + + echo "" | tee -a $LOGFILE + echo -e "Detail Checklist:" | tee -a $LOGFILE + echo -e "=================" | tee -a $LOGFILE + echo -e "Modlist: $MODLIST .....\e[32m OK.\e[0m" | tee -a $LOGFILE + echo -e "Directory: $modlist_dir .....\e[32m OK.\e[0m" | tee -a $LOGFILE + echo -e "Proton Version: $proton_ver .....\e[32m OK.\e[0m" | tee -a $LOGFILE + echo -e "App ID: $APPID" | tee -a $LOGFILE + +} + +################################# +# chown/chmod modlist directory # +################################# + +chown_chmod_modlist_dir() { + log_status "WARN" "Changing Ownership and Permissions of modlist directory (may require sudo password)" + + user=$(whoami) + group=$(id -gn) + log_status "DEBUG" "User is $user and Group is $group" + + sudo chown -R "$user:$group" "$modlist_dir" + sudo chmod -R 755 "$modlist_dir" +} + + +############################################### +# Backup ModOrganizer.ini and backup gamePath # +############################################### + +backup_modorganizer() { + log_status "DEBUG" "Backing up ModOrganizer.ini: $modlist_ini" + cp "$modlist_ini" "$modlist_ini.$(date +"%Y%m%d_%H%M%S").bak" + grep gamePath "$modlist_ini" | sed '/^backupPath/! s/gamePath/backupPath/' >> "$modlist_ini" +} + +######################################## +# Blank or set MO2 Downloads Directory # +######################################## + +blank_downloads_dir() { + log_status "INFO" "\nEditing download_directory..." + sed -i "/download_directory/c\download_directory =" "$modlist_ini" + log_status "SUCCESS" "Done." +} + +############################################ +# Replace the gamePath in ModOrganizer.ini # +############################################ + +replace_gamepath() { + log_status "INFO" "Setting game path in ModOrganizer.ini..." + + log_status "DEBUG" "Using Steam Library Path: $steam_library" + log_status "DEBUG" "Use SDCard?: $basegame_sdcard" + + # Check if Modlist uses Game Root, Stock Game, etc. + game_path_line=$(grep '^gamePath' "$modlist_ini") + log_status "DEBUG" "Game Path Line: $game_path_line" + + if [[ "$game_path_line" == *Stock\ Game* || "$game_path_line" == *STOCK\ GAME* || "$game_path_line" == *Stock\ Game\ Folder* || "$game_path_line" == *Stock\ Folder* || "$game_path_line" == *Skyrim\ Stock* || "$game_path_line" == *Game\ Root* || $game_path_line == *root\\\\Skyrim\ Special\ Edition* ]]; then + # Stock Game, Game Root or equivalent directory found + log_status "INFO" "Found Game Root/Stock Game or equivalent directory, editing Game Path..." + + # Get the end of our path + if [[ $game_path_line =~ Stock\ Game\ Folder ]]; then + modlist_gamedir="$modlist_dir/Stock Game Folder" + log_status "DEBUG" "Modlist Gamedir: $modlist_gamedir" + elif [[ $game_path_line =~ Stock\ Folder ]]; then + modlist_gamedir="$modlist_dir/Stock Folder" + elif [[ $game_path_line =~ Skyrim\ Stock ]]; then + modlist_gamedir="$modlist_dir/Skyrim Stock" + log_status "DEBUG" "Modlist Gamedir: $modlist_gamedir" + elif [[ $game_path_line =~ Game\ Root ]]; then + modlist_gamedir="$modlist_dir/Game Root" + log_status "DEBUG" "Modlist Gamedir: $modlist_gamedir" + elif [[ $game_path_line =~ STOCK\ GAME ]]; then + modlist_gamedir="$modlist_dir/STOCK GAME" + log_status "DEBUG" "Modlist Gamedir: $modlist_gamedir" + elif [[ $game_path_line =~ Stock\ Game ]]; then + modlist_gamedir="$modlist_dir/Stock Game" + log_status "DEBUG" "Modlist Gamedir: $modlist_gamedir" + elif [[ $game_path_line =~ root\\\\Skyrim\ Special\ Edition ]]; then + modlist_gamedir="$modlist_dir/root/Skyrim Special Edition" + log_status "DEBUG" "Modlist Gamedir: $modlist_gamedir" + fi + + if [[ "$modlist_sdcard" -eq "1" && "$steamdeck" -eq "1" ]]; then + log_status "DEBUG" "Using SDCard on Steam Deck" + modlist_gamedir_sdcard="${modlist_gamedir#*mmcblk0p1}" + sdcard_new_path="$modlist_gamedir_sdcard" + + # Strip /run/media/deck/UUID if present + if [[ "$sdcard_new_path" == /run/media/deck/* ]]; then + sdcard_new_path="/${sdcard_new_path#*/run/media/deck/*/*}" + log_status "DEBUG" "SD Card Path after stripping: $sdcard_new_path" + fi + + new_string="@ByteArray(D:${sdcard_new_path//\//\\\\})" + log_status "DEBUG" "New String: $new_string" + else + new_string="@ByteArray(Z:${modlist_gamedir//\//\\\\})" + log_status "DEBUG" "New String: $new_string" + fi + + elif [[ "$game_path_line" == *steamapps* ]]; then + log_status "INFO" "Vanilla Game Directory required, editing Game Path..." + modlist_gamedir="$steam_library/$gamevar" + log_status "DEBUG" "Modlist Gamedir: $modlist_gamedir" + + if [[ "$basegame_sdcard" -eq "1" && "$steamdeck" -eq "1" ]]; then + log_status "DEBUG" "Using SDCard on Steam Deck" + modlist_gamedir_sdcard="${modlist_gamedir#*mmcblk0p1}" + sdcard_new_path="$modlist_gamedir_sdcard/$gamevar" + new_string="@ByteArray(D:${sdcard_new_path//\//\\\\})" + log_status "DEBUG" "New String: $new_string" + else + new_string="@ByteArray(Z:${modlist_gamedir//\//\\\\})" + log_status "DEBUG" "New String: $new_string" + fi + else + log_status "WARN" "Neither Game Root, Stock Game or Vanilla Game directory found, Please launch MO and set path manually..." + return 1 + fi + + # Replace the string in the file + file_to_modify="$modlist_dir/ModOrganizer.ini" + escaped_new_string=$(printf '%s\n' "$new_string" | sed -e 's/[\/&]/\\&/g') + sed -i "/^gamePath/c\gamePath=$escaped_new_string" "$file_to_modify" + + log_status "SUCCESS" "Game path set successfully" +} + +########################################## +# Update Executables in ModOrganizer.ini # +########################################## + +update_executables() { + + # Take the line passed to the function + echo "Original Line: $orig_line_path" >>$LOGFILE 2>&1 + + skse_loc=$(echo "$orig_line_path" | cut -d '=' -f 2-) + echo "SKSE Loc: $skse_loc" >>$LOGFILE 2>&1 + + # Drive letter + if [[ "$modlist_sdcard" -eq 1 && "$steamdeck" -eq 1 ]]; then + echo "Using SDCard on Steam Deck" >>$LOGFILE 2>&1 + drive_letter=" = D:" + else + drive_letter=" = Z:" + fi + + # Find the workingDirectory number + + binary_num=$(echo "$orig_line_path" | cut -d '=' -f -1) + echo "Binary Num: $binary_num" >>$LOGFILE 2>&1 + + # Find the equvalent workingDirectory + justnum=$(echo "$binary_num" | cut -d '\' -f 1) + bin_path_start=$(echo "$binary_num" | tr -d ' ' | sed 's/\\/\\\\/g') + path_start=$(echo "$justnum\\workingDirectory" | sed 's/\\/\\\\/g') + echo "Path Start: $path_start" >>$LOGFILE 2>&1 + # Decide on steam apps or Stock Game etc + + if [[ "$orig_line_path" == *"mods"* ]]; then + # mods path type found + echo -e "mods path Found" >>$LOGFILE 2>&1 + + # Path Middle / modlist_dr + if [[ "$modlist_sdcard" -eq 1 && "$steamdeck" -eq 1 ]]; then + echo "Using SDCard on Steam Deck" >>$LOGFILE 2>&1 + drive_letter=" = D:" + echo "$modlist_dir" >>$LOGFILE 2>&1 + path_middle="${modlist_dir#*mmcblk0p1}" + # Strip /run/media/deck/UUID + if [[ "$path_middle" == /run/media/*/* ]]; then + path_middle="/${path_middle#*/run/media/*/*/*}" + echo "Path Middle after stripping: $path_middle" >>$LOGFILE 2>&1 + fi + else + path_middle="$modlist_dir" + fi + + echo "Path Middle: $path_middle" >>$LOGFILE 2>&1 + + path_end=$(echo "${skse_loc%/*}" | sed 's/.*\/mods/\/mods/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end=$(echo "$skse_loc" | sed 's/.*\/mods/\/mods/') + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + elif grep -q -E "(Stock Game|Game Root|STOCK GAME|Stock Game Folder|Stock Folder|Skyrim Stock|root/Skyrim Special Edition)" <<<"$orig_line_path"; then + # STOCK GAME ROOT FOUND + echo -e "Stock/Game Root Found" >>$LOGFILE 2>&1 + + # Path Middle / modlist_dr + if [[ "$modlist_sdcard" -eq 1 && "$steamdeck" -eq 1 ]]; then + echo "Using SDCard on Steam Deck" >>$LOGFILE 2>&1 + drive_letter=" = D:" + echo "Modlist Dir: $modlist_dir" >>$LOGFILE 2>&1 + path_middle="${modlist_dir#*mmcblk0p1}" + # Strip /run/media/deck/UUID + if [[ "$path_middle" == /run/media/*/* ]]; then + path_middle="/${path_middle#*/run/media/*/*/*}" + echo "Path Middle after stripping: $path_middle" >>$LOGFILE 2>&1 + fi + else + path_middle="$modlist_dir" + fi + echo "Path Middle: $path_middle" >>$LOGFILE 2>&1 + + # Get the end of our path + if [[ $orig_line_path =~ Stock\ Game ]]; then + dir_type="stockgame" + path_end=$(echo "${skse_loc%/*}" | sed 's/.*\/Stock Game/\/Stock Game/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end=$(echo "$skse_loc" | sed 's/.*\/Stock Game/\/Stock Game/') + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + elif [[ $orig_line_path =~ Game\ Root ]]; then + dir_type="gameroot" + path_end=$(echo "${skse_loc%/*}" | sed 's/.*\/Game Root/\/Game Root/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end=$(echo "$skse_loc" | sed 's/.*\/Game Root/\/Game Root/') + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + elif [[ $orig_line_path =~ STOCK\ GAME ]]; then + dir_type="STOCKGAME" + path_end=$(echo "${skse_loc%/*}" | sed 's/.*\/STOCK GAME/\/STOCK GAME/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end=$(echo "$skse_loc" | sed 's/.*\/STOCK GAME/\/STOCK GAME/') + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + elif [[ $orig_line_path =~ Stock\ Folder ]]; then + dir_type="stockfolder" + path_end=$(echo "${skse_loc%/*}" | sed 's/.*\/Stock Folder/\/Stock Folder/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end=$(echo "$skse_loc" | sed 's/.*\/Stock Folder/\/Stock Folder/') + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + elif [[ $orig_line_path =~ Skyrim\ Stock ]]; then + dir_type="skyrimstock" + path_end=$(echo "${skse_loc%/*}" | sed 's/.*\/Skyrim Stock/\/Skyrim Stock/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end=$(echo "$skse_loc" | sed 's/.*\/Skyrim Stock/\/Skyrim Stock/') + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + elif [[ $orig_line_path =~ Stock\ Game\ Folder ]]; then + dir_type="stockgamefolder" + path_end=$(echo "$skse_loc" | sed 's/.*\/Stock Game Folder/\/Stock Game Folder/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + elif [[ $orig_line_path =~ root\/Skyrim\ Special\ Edition ]]; then + dir_type="rootskyrimse" + path_end="/${skse_loc# }" + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end="/${skse_loc# }" + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + fi + elif [[ "$orig_line_path" == *"steamapps"* ]]; then + # STEAMAPPS FOUND + echo -e "steamapps Found" >>$LOGFILE 2>&1 + + # Path Middle / modlist_dr + if [[ "$basegame_sdcard" -eq "1" && "$steamdeck" -eq "1" ]]; then + echo "Using SDCard on Steam Deck" >>$LOGFILE 2>&1 + path_middle="${steam_library#*mmcblk0p1}" + drive_letter=" = D:" + else + echo "Steamapps Steam Library Path: $steam_library" + path_middle=${steam_library%%steamapps*} + fi + echo "Path Middle: $path_middle" >>$LOGFILE 2>&1 + path_end=$(echo "${skse_loc%/*}" | sed 's/.*\/steamapps/\/steamapps/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end=$(echo "$skse_loc" | sed 's/.*\/steamapps/\/steamapps/') + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + + else + echo "No matching pattern found in the path: $orig_line_path" >>$LOGFILE 2>&1 + bail_out=1 + echo $bail_out >>$LOGFILE 2>&1 + + fi + + echo "Bail Out: $bail_out" >>$LOGFILE 2>&1 + + if [[ $bail_out -eq 1 ]]; then + echo "Exiting function due to bail_out" >>$LOGFILE 2>&1 + return + else + # Combine them all together + full_bin_path="$bin_path_start$drive_letter$path_middle$bin_path_end" + echo "Full Bin Path: $full_bin_path" >>$LOGFILE 2>&1 + full_path="$path_start$drive_letter$path_middle$path_end" + echo "Full Path: $full_path" >>$LOGFILE 2>&1 + + # Replace forwardslashes with double backslashes + new_path=${full_path//\//\\\\\\\\} + echo "New Path: $new_path" >>$LOGFILE 2>&1 + + # Convert the lines in ModOrganizer.ini, if it isn't already + + sed -i "\|^${bin_path_start}|s|^.*$|${full_bin_path}|" "$modlist_ini" + # Convert workingDirectory entries + sed -i "\|^${path_start}|s|^.*$|${new_path}|" "$modlist_ini" + fi + +} + +################################################# +# Edit Custom binary and workingDirectory paths # +################################################# + +edit_binary_working_paths() { + + grep -E -e "skse64_loader\.exe" -e "f4se_loader\.exe" "$modlist_ini" | while IFS= read -r orig_line_path; do + update_executables + done + +} + +################################ +# Set or Select the Resolution # +################################ + +select_resolution() { + if [ "$steamdeck" -eq 1 ]; then + set_res="1280x800" + else + while true; do + echo -e "\e[31m ** Enter your desired resolution in the format 1920x1200: ** \e[0m" + read -p " " user_res + + # Validate the input format + if [[ "$user_res" =~ ^[0-9]+x[0-9]+$ ]]; then + # Ask for confirmation + echo -e "\e[31m \n** Is $user_res your desired resolution? (y/N): ** \e[0m" + read -p " " confirm + if [[ "$confirm" =~ ^[Yy]$ ]]; then + set_res="$user_res" + break + else + echo "Please enter the resolution again." | tee -a $LOGFILE + fi + else + echo "Invalid input format. Please enter the resolution in the format 1920x1200." | tee -a $LOGFILE + fi + done + fi + + echo "Resolution set to: $set_res" | tee -a $LOGFILE +} + +###################################### +# Update the resolution in INI files # +###################################### + +update_ini_resolution() { + + echo -ne "\nEditing Resolution in prefs files... " | tee -a "$LOGFILE" + + # Find all SSEDisplayTweaks.ini files in the specified directory and its subdirectories + ini_files=$(find "$modlist_dir" -name "SSEDisplayTweaks.ini") + + if [[ "$gamevar" == "Skyrim Special Edition" && -n "$ini_files" ]]; then + while IFS= read -r ini_file; do + # Use awk to replace the lines with the new values, handling spaces in paths + awk -v res="$set_res" '/^(#?)Resolution[[:space:]]*=/ { print "Resolution=" res; next } \ + /^(#?)Fullscreen[[:space:]]*=/ { print "Fullscreen=false"; next } \ + /^(#?)#Fullscreen[[:space:]]*=/ { print "#Fullscreen=false"; next } \ + /^(#?)Borderless[[:space:]]*=/ { print "Borderless=true"; next } \ + /^(#?)#Borderless[[:space:]]*=/ { print "#Borderless=true"; next }1' "$ini_file" >"$ini_file.new" + + cp "$ini_file.new" "$ini_file" + echo "Updated $ini_file with Resolution=$res, Fullscreen=false, Borderless=true" >>"$LOGFILE" 2>&1 + echo -e " Done." >>"$LOGFILE" 2>&1 + done <<<"$ini_files" + elif [[ "$gamevar" == "Fallout 4" ]]; then + echo "Not Skyrim, skipping SSEDisplayTweaks" >>"$LOGFILE" 2>&1 + fi + + ########## + + # Split $set_res into two variables + isize_w=$(echo "$set_res" | cut -d'x' -f1) + isize_h=$(echo "$set_res" | cut -d'x' -f2) + + # Find all instances of skyrimprefs.ini, Fallout4Prefs.ini, falloutprefs.ini, or Oblivion.ini in specified directories + + if [[ "$gamevar" == "Skyrim Special Edition" ]]; then + ini_files=$(find "$modlist_dir/profiles" "$modlist_dir/Stock Game" "$modlist_dir/Game Root" "$modlist_dir/STOCK GAME" "$modlist_dir/Stock Game Folder" "$modlist_dir/Stock Folder" "$modlist_dir/Skyrim Stock" -iname "skyrimprefs.ini" 2>/dev/null) + elif [[ "$gamevar" == "Fallout 4" ]]; then + ini_files=$(find "$modlist_dir/profiles" "$modlist_dir/Stock Game" "$modlist_dir/Game Root" "$modlist_dir/STOCK GAME" "$modlist_dir/Stock Game Folder" "$modlist_dir/Stock Folder" -iname "Fallout4Prefs.ini" 2>/dev/null) + elif [[ "$gamevar" == "Fallout New Vegas" ]]; then + ini_files=$(find "$modlist_dir/profiles" "$modlist_dir/Stock Game" "$modlist_dir/Game Root" "$modlist_dir/STOCK GAME" "$modlist_dir/Stock Game Folder" "$modlist_dir/Stock Folder" -iname "falloutprefs.ini" 2>/dev/null) + elif [[ "$gamevar" == "Oblivion" ]]; then + ini_files=$(find "$modlist_dir/profiles" "$modlist_dir/Stock Game" "$modlist_dir/Game Root" "$modlist_dir/STOCK GAME" "$modlist_dir/Stock Game Folder" "$modlist_dir/Stock Folder" -iname "Oblivion.ini" 2>/dev/null) + fi + + if [ -n "$ini_files" ]; then + while IFS= read -r ini_file; do + # Use awk to replace the lines with the new values in the appropriate ini file + if [[ "$gamevar" == "Skyrim Special Edition" ]] || [[ "$gamevar" == "Fallout 4" ]] || [[ "$gamevar" == "Fallout New Vegas" ]]; then + awk -v isize_w="$isize_w" -v isize_h="$isize_h" '/^iSize W/ { print "iSize W = " isize_w; next } \ + /^iSize H/ { print "iSize H = " isize_h; next }1' "$ini_file" >"$HOME/temp_file" && mv "$HOME/temp_file" "$ini_file" + elif [[ "$gamevar" == "Oblivion" ]]; then + awk -v isize_w="$isize_w" -v isize_h="$isize_h" '/^iSize W=/ { print "iSize W=" isize_w; next } \ + /^iSize H=/ { print "iSize H=" isize_h; next }1' "$ini_file" >"$HOME/temp_file" && mv "$HOME/temp_file" "$ini_file" + fi + + echo "Updated $ini_file with iSize W=$isize_w, iSize H=$isize_h" >>"$LOGFILE" 2>&1 + done <<<"$ini_files" + else + echo "No suitable prefs.ini files found in specified directories. Please set manually using the INI Editor in MO2." | tee -a "$LOGFILE" + fi + + echo -e "Done." | tee -a "$LOGFILE" + +} + +################### +# Edit resolution # +################### + +edit_resolution() { + if [[ -n "$selected_resolution" ]]; then + log_status "DEBUG" "Applying resolution: $selected_resolution" + set_res="$selected_resolution" + update_ini_resolution + else + log_status "DEBUG" "Resolution setup skipped" + fi +} + +########################## +# Small additional tasks # +########################## + +small_additional_tasks() { + + # Delete MO2 plugins that don't work via Proton + + file_to_delete="$modlist_dir/plugins/FixGameRegKey.py" + + if [ -e "$file_to_delete" ]; then + rm "$file_to_delete" + echo "File deleted: $file_to_delete" >>$LOGFILE 2>&1 + else + echo "File does not exist: $file_to_delete" >>"$LOGFILE" 2>&1 + fi + + # Download Font to support Bethini + wget https://github.com/mrbvrz/segoe-ui-linux/raw/refs/heads/master/font/seguisym.ttf -q -nc -O "$compat_data_path/pfx/drive_c/windows/Fonts/seguisym.ttf" + +} + +############################### +# Set Steam Artwork Function # +############################### + +set_steam_artwork() { + # Only run for Tuxborn modlist + if [[ "$MODLIST" == *"Tuxborn"* ]]; then + log_status "DEBUG" "Setting up Steam artwork for Tuxborn..." + + # Source directory with artwork + local source_dir="$modlist_dir/Steam Icons" + + if [[ ! -d "$source_dir" ]]; then + log_status "WARN" "Steam Icons directory not found at $source_dir" + return 1 + fi + + # Find all Steam userdata directories + for userdata_dir in "$HOME/.local/share/Steam/userdata" "$HOME/.steam/steam/userdata"; do + if [[ ! -d "$userdata_dir" ]]; then + continue + fi + + # Process each user ID directory + for user_id_dir in "$userdata_dir"/*; do + if [[ ! -d "$user_id_dir" || "$user_id_dir" == *"0"* ]]; then + continue # Skip non-directories and the anonymous user + fi + + # Create grid directory if it doesn't exist + local grid_dir="$user_id_dir/config/grid" + mkdir -p "$grid_dir" + + # Copy grid-tall.png to both APPID.png and APPIDp.png + if [[ -f "$source_dir/grid-tall.png" ]]; then + cp "$source_dir/grid-tall.png" "$grid_dir/${APPID}.png" + log_status "DEBUG" "Copied grid-tall.png to ${APPID}.png" + cp "$source_dir/grid-tall.png" "$grid_dir/${APPID}p.png" + log_status "DEBUG" "Copied grid-tall.png to ${APPID}p.png" + fi + + # Copy grid-hero.png to APPID_hero.png + if [[ -f "$source_dir/grid-hero.png" ]]; then + cp "$source_dir/grid-hero.png" "$grid_dir/${APPID}_hero.png" + log_status "DEBUG" "Copied grid-hero.png to ${APPID}_hero.png" + fi + + # Copy grid-logo.png to APPID_logo.png + if [[ -f "$source_dir/grid-logo.png" ]]; then + cp "$source_dir/grid-logo.png" "$grid_dir/${APPID}_logo.png" + log_status "DEBUG" "Copied grid-logo.png to ${APPID}_logo.png" + fi + + log_status "DEBUG" "Tuxborn artwork copied for user ID $(basename "$user_id_dir")" + done + done + + log_status "DEBUG" "Steam artwork setup complete for Tuxborn" + fi +} + + +########################## +# Modlist Specific Steps # +########################## + +modlist_specific_steps() { + local modlist_lower=$(echo "${MODLIST// /}" | tr '[:upper:]' '[:lower:]') + + # Call the Steam artwork function for all modlists + set_steam_artwork | tee -a "$LOGFILE" + + # Handle Wildlander specially due to its custom spinner animation + if [[ "$MODLIST" == *"Wildlander"* ]]; then + log_status "INFO" "\nRunning steps specific to \e[32m$MODLIST\e[0m. This can take some time, be patient!" + + # Install dotnet with spinner animation + spinner=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') + run_protontricks --no-bwrap "$APPID" -q dotnet472 >/dev/null 2>&1 & + + pid=$! # Store the PID of the background process + + while kill -0 "$pid" >/dev/null 2>&1; do + for i in "${spinner[@]}"; do + echo -en "\r${i}\c" + sleep 0.1 + done + done + + wait "$pid" # Wait for the process to finish + + # Clear the spinner and move to the next line + echo -en "\r\033[K" # Clear the spinner line + + if [[ $? -ne 0 ]]; then + log_status "ERROR" "Component install failed with exit code $?" + else + log_status "SUCCESS" "Wine Component install completed successfully." + fi + + new_output="$(run_protontricks --no-bwrap "$APPID" list-installed 2>/dev/null)" + log_status "DEBUG" "Components Found: $new_output" + return 0 + fi + + # Handle the rest of the modlists with the compact approach + for pattern in "${!modlist_configs[@]}"; do + if [[ "$pattern" != "wildlander" ]] && [[ "$modlist_lower" =~ ${pattern//|/|.*} ]]; then + log_status "INFO" "\nRunning steps specific to \e[32m$MODLIST\e[0m. This can take some time, be patient!" + + IFS=' ' read -ra components <<< "${modlist_configs[$pattern]}" + for component in "${components[@]}"; do + if [[ "$component" == "dotnet8" ]]; then + log_status "INFO" "\nDownloading .NET 8 Runtime" + wget https://download.visualstudio.microsoft.com/download/pr/77284554-b8df-4697-9a9e-4c70a8b35f29/6763c16069d1ab8fa2bc506ef0767366/dotnet-runtime-8.0.5-win-x64.exe -q -nc --show-progress --progress=bar:force:noscroll -O "$HOME/Downloads/dotnet-runtime-8.0.5-win-x64.exe" + log_status "INFO" "Installing .NET 8 Runtime...." + if [[ "$DEBUG" == "1" ]]; then + run_protontricks --no-bwrap -c 'wine "$HOME/Downloads/dotnet-runtime-8.0.5-win-x64.exe" /Q' "$APPID" + else + WINEDEBUG=-all run_protontricks --no-bwrap -c 'wine "$HOME/Downloads/dotnet-runtime-8.0.5-win-x64.exe" /Q' "$APPID" >/dev/null 2>&1 + fi + log_status "SUCCESS" "Done." + else + log_status "INFO" "Installing .NET ${component#dotnet}..." + if [[ "$DEBUG" == "1" ]]; then + run_protontricks --no-bwrap "$APPID" -q "$component" + else + WINEDEBUG=-all run_protontricks --no-bwrap "$APPID" -q "$component" >/dev/null 2>&1 + fi + log_status "SUCCESS" "Done." + fi + done + + set_win10_prefix + new_output="$(run_protontricks --no-bwrap "$APPID" list-installed 2>/dev/null)" + log_status "DEBUG" "Components Found: $new_output" + break + fi + done +} + +###################################### +# Create DXVK Graphics Pipeline file # +###################################### + +create_dxvk_file() { + echo "Use SDCard for DXVK File?: $basegame_sdcard" >>"$LOGFILE" 2>&1 + echo -e "\nCreating dxvk.conf file - Checking if Modlist uses Game Root, Stock Game or Vanilla Game Directory.." >>"$LOGFILE" 2>&1 + + game_path_line=$(grep '^gamePath' "$modlist_ini") + echo "Game Path Line: $game_path_line" >>"$LOGFILE" 2>&1 + + if [[ "$game_path_line" == *Stock\ Game* || "$game_path_line" == *STOCK\ GAME* || "$game_path_line" == *Stock\ Game\ Folder* || "$game_path_line" == *Stock\ Folder* || "$game_path_line" == *Skyrim\ Stock* || "$game_path_line" == *Game\ Root* ]]; then + # Get the end of our path + if [[ $game_path_line =~ Stock\ Game\ Folder ]]; then + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/Stock Game Folder/dxvk.conf" + elif [[ $game_path_line =~ Stock\ Folder ]]; then + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/Stock Folder/dxvk.conf" + elif [[ $game_path_line =~ Skyrim\ Stock ]]; then + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/Skyrim Stock/dxvk.conf" + elif [[ $game_path_line =~ Game\ Root ]]; then + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/Game Root/dxvk.conf" + elif [[ $game_path_line =~ STOCK\ GAME ]]; then + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/STOCK GAME/dxvk.conf" + elif [[ $game_path_line =~ Stock\ Game ]]; then + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/Stock Game/dxvk.conf" + elif [[ $game_path_line =~ root\\Skyrim\ Special\ Edition ]]; then + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/root/Skyrim Special Edition/dxvk.conf" + fi + + if [[ "$modlist_sdcard" -eq "1" ]]; then + echo "Using SDCard" >>"$LOGFILE" 2>&1 + modlist_gamedir_sdcard="${modlist_gamedir#*mmcblk0p1}" + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_gamedir/dxvk.conf" + fi + + elif [[ "$game_path_line" == *steamapps* ]]; then + echo -ne "Vanilla Game Directory required, editing Game Path.. " >>"$LOGFILE" 2>&1 + modlist_gamedir="$steam_library" + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_gamedir/dxvk.conf" + if [[ "$basegame_sdcard" -eq "1" ]]; then + echo "Using SDCard" >>"$LOGFILE" 2>&1 + modlist_gamedir_sdcard="${modlist_gamedir#*mmcblk0p1}" + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/$gamevar/dxvk.conf" + fi + fi +} + +############################# +# Create protontricks alias # +############################# + +protontricks_alias() { + if [[ "$which_protontricks" = "flatpak" ]]; then + local protontricks_alias_exists=$(grep "^alias protontricks=" ~/.bashrc) + local launch_alias_exists=$(grep "^alias protontricks-launch" ~/.bashrc) + + if [[ -z "$protontricks_alias_exists" ]]; then + echo -e "\nAdding protontricks alias to ~/.bashrc" + echo "alias protontricks='flatpak run com.github.Matoking.protontricks'" >> ~/.bashrc + source ~/.bashrc + else + echo "protontricks alias already exists in ~/.bashrc" >> "$LOGFILE" 2>&1 + fi + + if [[ -z "$launch_alias_exists" ]]; then + echo -e "\nAdding protontricks-launch alias to ~/.bashrc" + echo "alias protontricks-launch='flatpak run --command=protontricks-launch com.github.Matoking.protontricks'" >> ~/.bashrc + source ~/.bashrc + else + echo "protontricks-launch alias already exists in ~/.bashrc" >> "$LOGFILE" 2>&1 + fi + else + echo "Protontricks is not installed via flatpak, skipping alias creation." >> "$LOGFILE" 2>&1 + fi +} + +############################ +# FNV Launch Option Notice # +############################ + +fnv_launch_options() { + log_status "DEBUG" "fnv_launch_options: gamevar='$gamevar', compat_data_path='$compat_data_path'" + if [[ "$gamevar" == "Fallout New Vegas" ]]; then + if [[ -n "$compat_data_path" && -d "$compat_data_path" ]]; then + log_status "WARN" "\n\e[41;97m======================= CRITICAL: STEAM LAUNCH OPTIONS =======================\e[0m" + log_status "SUCCESS" "\n\e[1;32mOpen the properties of your '$MODLIST' entry in Steam and add the following to the launch options:\e[0m" + log_status "SUCCESS" "\n\e[1;32mSTEAM_COMPAT_DATA_PATH=\"$compat_data_path\" %command%\e[0m" + log_status "WARN" "\e[41;97m=============================================================================\e[0m" + log_status "WARN" "\nThis is \e[1;4mESSENTIAL\e[0m for the modlist to load correctly. You \e[1;4mMUST\e[0m add it to your Steam launch options." + else + log_status "ERROR" "\nCould not determine the compatdata path for Fallout New Vegas. Please manually set the correct path in the Launch Options." + fi + fi +} + +##################### +# Exit more cleanly # +##################### + +cleaner_exit() { + # Clean up wine and winetricks processes + cleanup_wine_procs + log_status "DEBUG" "Cleanup complete" + exit 1 +} + +#################### +# END OF FUNCTIONS # +#################### + +####################### +# Note Script Version # +####################### + +echo -e "Script Version $script_ver" >>"$LOGFILE" 2>&1 + +###################### +# Note Date and Time # +###################### + +echo -e "Script started at: $(date +'%Y-%m-%d %H:%M:%S')" >>"$LOGFILE" 2>&1 + +############################# +# Detect if running on deck # +############################# + +detect_steamdeck + +########################################### +# Detect Protontricks (flatpak or native) # +########################################### + +detect_protontricks + +############################### +# Detect Protontricks Version # +############################### + +protontricks_version + +########################################## +# Create protontricks alias in ~/.bashrc # +########################################## + +protontricks_alias + +############################################################## +# List Skyrim and Fallout Modlists from Steam (protontricks) # +############################################################## + +IFS=$'\n' readarray -t output_array < <(run_protontricks -l | tr -d $'\r' | grep -i 'Non-Steam shortcut' | grep -i 'Skyrim\|Fallout\|FNV\|Oblivion' | cut -d ' ' -f 3-) + +if [[ ${#output_array[@]} -eq 0 ]]; then + echo "" | tee -a "$LOGFILE" + log_status "ERROR" "No modlists detected for Skyrim, Oblivion or Fallout/FNV!" + log_status "INFO" "Please make sure your entry in Steam is something like 'Skyrim - ModlistName'" + log_status "INFO" "or 'Fallout - ModlistName' AND that you have pressed play in Steam at least once!" + cleaner_exit +fi + +echo "" | tee -a "$LOGFILE" +echo -e "\e[33mDetected Modlists:\e[0m" | tee -a "$LOGFILE" + +# Print numbered list with color +for i in "${!output_array[@]}"; do + echo -e "\e[32m$((i + 1)))\e[0m ${output_array[$i]}" +done + +# Read user selection with proper prompt +echo "───────────────────────────────────────────────────────────────────" +while true; do + read -p $'\e[33mSelect a modlist (1-'"${#output_array[@]}"$'): \e[0m' choice_num + + # Add a debug flag at the top for easy toggling + DEBUG_MODLIST_SELECTION=0 # Set to 1 to enable extra debug output + + # After reading user input for choice_num: + if [[ $DEBUG_MODLIST_SELECTION -eq 1 ]]; then + echo "[DEBUG] Raw user input: '$choice_num'" | tee -a "$LOGFILE" + fi + choice_num=$(echo "$choice_num" | xargs) # Trim whitespace + if [[ $DEBUG_MODLIST_SELECTION -eq 1 ]]; then + echo "[DEBUG] Trimmed user input: '$choice_num'" | tee -a "$LOGFILE" + fi + + # Before the selection validation if-statement: + if [[ $DEBUG_MODLIST_SELECTION -eq 1 ]]; then + echo "[DEBUG] Validating: '$choice_num' =~ ^[0-9]+$ && $choice_num -ge 1 && $choice_num -le ${#output_array[@]}" | tee -a "$LOGFILE" + fi + + # Validate selection properly + if [[ "$choice_num" =~ ^[0-9]+$ ]] && [[ "$choice_num" -ge 1 ]] && [[ "$choice_num" -le "${#output_array[@]}" ]]; then + if [[ $DEBUG_MODLIST_SELECTION -eq 1 ]]; then + echo "[DEBUG] Selection valid. Index: $((choice_num - 1)), Value: '${output_array[$((choice_num - 1))]}'" | tee -a "$LOGFILE" + fi + choice="${output_array[$((choice_num - 1))]}" + MODLIST=$(echo "$choice" | cut -d ' ' -f 3- | rev | cut -d ' ' -f 2- | rev) + log_status "DEBUG" "MODLIST: $MODLIST" + break # Exit the loop if selection is valid + else + if [[ $DEBUG_MODLIST_SELECTION -eq 1 ]]; then + echo "[DEBUG] Invalid selection. choice_num: '$choice_num', output_array length: ${#output_array[@]}" | tee -a "$LOGFILE" + fi + log_status "ERROR" "Invalid selection. Please enter a number between 1 and ${#output_array[@]}." + # Removed exit 1, so the loop continues + fi +done + +# Add a newline after the selection for cleaner output +echo "" + +# Initial detection phase +cleanup_wine_procs +set_appid +detect_game +detect_steam_library +detect_modlist_dir_path + +# Set modlist_sdcard if required +modlist_sdcard=0 +if [[ "$modlist_dir" =~ ^/run/media ]]; then + modlist_sdcard=1 +fi + +# Detect compatdata path and Proton version +detect_compatdata_path +detect_proton_version + +# Get resolution preference +if [ "$steamdeck" -eq 1 ]; then + selected_resolution="1280x800" + log_status "INFO" "Steam Deck detected - Resolution will be set to 1280x800" +else + echo -e "Do you wish to set the display resolution? (This can be changed manually later)" + read -p $'\e[33mSet resolution? (y/N): \e[0m' response + + if [[ "$response" =~ ^[Yy]$ ]]; then + while true; do + read -p $'\e[33mEnter resolution (e.g., 1920x1080): \e[0m' user_res + if [[ "$user_res" =~ ^[0-9]+x[0-9]+$ ]]; then + selected_resolution="$user_res" + log_status "DEBUG" "Resolution will be set to: $selected_resolution" + break + else + log_status "ERROR" "Invalid format. Please use format: 1920x1080" + fi + done + else + log_status "INFO" "Resolution setup skipped" + fi +fi + +# Then show the detection summary including the resolution if set +echo -e "\n\e[1mDetection Summary:\e[0m" | tee -a "$LOGFILE" +echo -e "===================" | tee -a "$LOGFILE" +echo -e "Selected Modlist: \e[32m$MODLIST\e[0m" | tee -a "$LOGFILE" +echo -e "Game Type: \e[32m$gamevar\e[0m" | tee -a "$LOGFILE" +echo -e "Steam App ID: \e[32m$APPID\e[0m" | tee -a "$LOGFILE" +echo -e "Modlist Directory: \e[32m$modlist_dir\e[0m" | tee -a "$LOGFILE" +echo -e "Proton Version: \e[32m$proton_ver\e[0m" | tee -a "$LOGFILE" +if [[ -n "$selected_resolution" ]]; then + echo -e "Resolution: \e[32m$selected_resolution\e[0m" | tee -a "$LOGFILE" +fi + +# Show simple confirmation with minimal info +read -rp $'\e[32mDo you want to proceed with the installation? (y/N)\e[0m ' proceed + +if [[ $proceed =~ ^[Yy]$ ]]; then + if [ "$DEBUG" = "1" ]; then + { + # Protontricks setup (10%) + set_protontricks_perms + # Dotfiles (20%) + enable_dotfiles + # Modlist-specific steps (30%) + modlist_specific_steps + # Wine components (40%) + install_wine_components + # Windows 10 prefix (50%) + set_win10_prefix + # ModOrganizer configuration (70%) + backup_modorganizer + blank_downloads_dir + replace_gamepath + edit_binary_working_paths + # Resolution and additional tasks (90%) + edit_resolution + small_additional_tasks + create_dxvk_file + # Final steps (100%) + chown_chmod_modlist_dir + fnv_launch_options + } 2>&1 | tee -a "$LOGFILE" + else + # Function to update progress + update_progress() { + local percent=$1 + local bar_length=50 + local filled_length=$((percent * bar_length / 100)) + local bar="" + for ((i = 0; i < bar_length; i++)); do + if [ $i -lt $filled_length ]; then + bar+="=" + else + bar+=" " + fi + done + printf "\r[%-${bar_length}s] %d%%" "$bar" "$percent" + } + { + if [ "$DEBUG" != "1" ]; then + printf "\r\033[KProgress: [%-50s] %d%% - Setting up Protontricks..." " " "10" + fi + set_protontricks_perms >/dev/null 2>&1 + if [ "$DEBUG" != "1" ]; then + printf "\r\033[KProgress: [%-50s] %d%% - Enabling dotfiles..." "========== " "20" + fi + enable_dotfiles >/dev/null 2>&1 + if [ "$DEBUG" != "1" ]; then + modlist_specific_steps_spinner + else + modlist_specific_steps + fi + if [ "$DEBUG" != "1" ]; then + printf "\r\033[KProgress: [%-50s] %d%% - Installing Wine components..." "==================== " "40" + fi + install_wine_components >/dev/null 2>&1 + if [ "$DEBUG" != "1" ]; then + printf "\r\033[KProgress: [%-50s] %d%% - Setting Windows 10 prefix..." "========================= " "50" + fi + set_win10_prefix >/dev/null 2>&1 + if [ "$DEBUG" != "1" ]; then + printf "\r\033[KProgress: [%-50s] %d%% - Configuring Mod Organizer..." "=================================== " "70" + fi + backup_modorganizer >/dev/null 2>&1 + blank_downloads_dir >/dev/null 2>&1 + replace_gamepath >/dev/null 2>&1 + edit_binary_working_paths >/dev/null 2>&1 + if [ "$DEBUG" != "1" ]; then + printf "\r\033[KProgress: [%-50s] %d%% - Setting resolution and additional tasks..." "============================================ " "90" + fi + edit_resolution >/dev/null 2>&1 + small_additional_tasks >/dev/null 2>&1 + create_dxvk_file >/dev/null 2>&1 + if [ "$DEBUG" != "1" ]; then + printf "\r\033[KProgress: [%-50s] %d%% - Completing installation..." "==================================================" "100" + fi + chown_chmod_modlist_dir + fnv_launch_options + } 2>>$LOGFILE + fi + # Show completion message + { + echo "" # Add blank line before success message + echo -e "\e[32m✓ Installation completed successfully!\e[0m" + echo -e "\n📝 Next Steps:" + echo " • Launch your modlist through Steam" + echo -e "\n💡 Detailed log available at: $LOGFILE\n" + } | tee -a "$LOGFILE" + # Show SD Card status if detected + if [[ "$steamdeck" -eq 1 ]]; then + if [[ "$modlist_dir" =~ ^/run/media/deck/[^/]+(/.*)?$ ]] || [[ "$modlist_dir" == "/run/media/mmcblk0p1"* ]]; then + echo -e "SD Card: \e[32mDetected\e[0m" | tee -a "$LOGFILE" + fi + else + if [[ "$modlist_dir" == "/run/media"* ]]; then + echo -e "Removable Media: \e[33mDetected at $modlist_dir\e[0m" | tee -a "$LOGFILE" + fi + fi +else + log_status "INFO" "Installation cancelled." + cleaner_exit +fi + diff --git a/binaries/omni-guides.sh b/binaries/omni-guides.sh new file mode 100644 index 0000000..955db29 --- /dev/null +++ b/binaries/omni-guides.sh @@ -0,0 +1,1711 @@ +#!/usr/bin/env bash +# +################################################### +# # +# A tool for running Wabbajack modlists on Linux # +# # +# Beta v0.69 - Omni 03/18/2025 # +# # +################################################### + +# Full Changelog can be found here: https://github.com/Omni-guides/Wabbajack-Modlist-Linux/blob/main/binaries/omni-guides-sh.changelog.txt + + +# Current Script Version (beta) +script_ver=0.69 + +# Define modlist-specific configurations +declare -A modlist_configs=( + ["wildlander"]="dotnet472" + ["librum|apostasy"]="dotnet40 dotnet8" + ["nordicsouls"]="dotnet40" + ["livingskyrim|lsiv|ls4"]="dotnet40" + ["lostlegacy"]="dotnet48" +) + +# Set up and blank logs (simplified) +LOGFILE=$HOME/omni-guides-sh.log +echo "" >$HOME/omni-guides-sh.log + +# Add our new logging function +log_status() { + local level="$1" + local message="$2" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + + # Always write to log file with timestamp but without color codes + echo "[$timestamp] [$level] $(echo "$message" | sed 's/\x1b\[[0-9;]*m//g')" >> "$LOGFILE" + + # Only display non-DEBUG messages to the user, preserving color codes + if [ "$level" != "DEBUG" ]; then + echo -e "$message" + fi +} + +#set -x +#Protontricks Bug +#export PROTON_VERSION="Proton Experimental" + +# Display banner +echo "╔══════════════════════════════════════════════════════════════════╗" +echo "║ Omni-Guides (beta) ║" +echo "║ ║" +echo "║ A tool for running Wabbajack modlists on Linux ║" +echo "╚══════════════════════════════════════════════════════════════════╝" + +######### +# Intro # +######### +echo "" +log_status "INFO" "Omni-Guides Wabbajack Post-Install Script v$script_ver" +echo "───────────────────────────────────────────────────────────────────" +log_status "INFO" "This script automates the post-install steps for Wabbajack modlists on Linux/Steam Deck." +log_status "INFO" "It will configure your modlist location, install required components, and apply necessary fixes." +echo "" +log_status "WARN" "⚠ IMPORTANT: Use this script at your own risk." +log_status "INFO" "Please report any issues via GitHub (Omni-guides/Wabbajack-Modlist-Linux)." +echo "───────────────────────────────────────────────────────────────────" +echo -e "\e[33mPress any key to continue...\e[0m" +read -n 1 -s -r -p "" + +############# +# Functions # +############# + +########################## +# Cleanup Wine Processes # +########################## + +cleanup_wine_procs() { + + # Find and kill processes containing various process names + processes=$(pgrep -f "win7|win10|ShowDotFiles|protontricks") + if [[ -n "$processes" ]]; then + echo "$processes" | xargs kill -9 + echo "Processes killed successfully." >>$LOGFILE 2>&1 + else + echo "No matching processes found." >>$LOGFILE 2>&1 + fi + + pkill -9 winetricks + +} + +############# +# Set APPID # +############# + +set_appid() { + + echo "DEBUG: Extracting APPID from choice: '$choice'" >>$LOGFILE 2>&1 + APPID=$(echo "$choice" | awk -F'[()]' '{print $2}') + echo "DEBUG: Extracted APPID: '$APPID'" >>$LOGFILE 2>&1 + + #APPID=$(echo $choice | awk {'print $NF'} | sed 's:^.\(.*\).$:\1:') + echo "APPID=$APPID" >>$LOGFILE 2>&1 + + if [ -z "$APPID" ]; then + echo "Error: APPID cannot be empty, exiting... Please tell Omni :(" + cleaner_exit + fi + +} + +############################# +# Detect if running on deck # +############################# + +detect_steamdeck() { + # Steamdeck or nah? + + if [ -f "/etc/os-release" ] && grep -q "steamdeck" "/etc/os-release"; then + steamdeck=1 + echo "Running on Steam Deck" >>$LOGFILE 2>&1 + else + steamdeck=0 + echo "NOT A steamdeck" >>$LOGFILE 2>&1 + fi + +} + +########################################### +# Detect Protontricks (flatpak or native) # +########################################### + +detect_protontricks() { + echo -ne "\nDetecting if protontricks is installed..." >>$LOGFILE 2>&1 + + # Check if native protontricks exists + if command -v protontricks >/dev/null 2>&1; then + protontricks_path=$(command -v protontricks) + # Check if the detected binary is actually a Flatpak wrapper + if [[ -f "$protontricks_path" ]] && grep -q "flatpak run" "$protontricks_path"; then + echo -e "Detected Protontricks is actually a Flatpak wrapper at $protontricks_path." >>$LOGFILE 2>&1 + which_protontricks=flatpak + else + echo -e "Native Protontricks found at $protontricks_path." | tee -a $LOGFILE + which_protontricks=native + return 0 # Exit function since we confirmed native protontricks + fi + fi + + # If not found, check for Flatpak protontricks + if flatpak list | grep -iq protontricks; then + echo -e "Flatpak Protontricks is already installed." >>$LOGFILE 2>&1 + which_protontricks=flatpak + return 0 + fi + + # If neither found, offer to install Flatpak + echo -e "\e[31m\n** Protontricks not found. Do you wish to install it? (y/n): **\e[0m" + read -p " " answer + if [[ $answer =~ ^[Yy]$ ]]; then + if [[ $steamdeck -eq 1 ]]; then + if flatpak install -u -y --noninteractive flathub com.github.Matoking.protontricks; then + which_protontricks=flatpak + return 0 + else + echo -e "\n\e[31mFailed to install Protontricks via Flatpak. Please install it manually and rerun this script.\e[0m" | tee -a $LOGFILE + exit 1 + fi + else + read -p "Choose installation method: 1) Flatpak (preferred) 2) Native: " choice + if [[ $choice =~ 1 ]]; then + if flatpak install -u -y --noninteractive flathub com.github.Matoking.protontricks; then + which_protontricks=flatpak + return 0 + else + echo -e "\n\e[31mFailed to install Protontricks via Flatpak. Please install it manually and rerun this script.\e[0m" | tee -a $LOGFILE + exit 1 + fi + else + echo -e "\nSorry, there are too many distros to automate this!" | tee -a $LOGFILE + echo -e "Please check how to install Protontricks using your OS package manager (yum, dnf, apt, pacman, etc.)" | tee -a $LOGFILE + echo -e "\e[31mProtontricks is required for this script to function. Exiting.\e[0m" | tee -a $LOGFILE + exit 1 + fi + fi + else + echo -e "\e[31mProtontricks is required for this script to function. Exiting.\e[0m" | tee -a $LOGFILE + exit 1 + fi +} + +############################# +# Run protontricks commands # +############################# + +run_protontricks() { + # Determine the protontricks binary path and create command array + if [ "$which_protontricks" = "flatpak" ]; then + local cmd=(flatpak run com.github.Matoking.protontricks) + else + local cmd=(protontricks) + fi + + # Execute the command with all arguments + "${cmd[@]}" "$@" +} + +############################### +# Detect Protontricks Version # +############################### + +protontricks_version() { + # Get the current version of protontricks + protontricks_version=$(run_protontricks -V | cut -d ' ' -f 2 | sed 's/[()]//g') + + # Remove any non-numeric characters from the version number + protontricks_version_cleaned=$(echo "$protontricks_version" | sed 's/[^0-9.]//g') + + echo "Protontricks Version Cleaned = $protontricks_version_cleaned" >> "$LOGFILE" 2>&1 + + # Split the version into digits + IFS='.' read -r first_digit second_digit third_digit <<< "$protontricks_version_cleaned" + + # Check if the second digit is defined and greater than or equal to 12 + if [[ -n "$second_digit" && "$second_digit" -lt 12 ]]; then + echo "Your protontricks version is too old! Update to version 1.12 or newer and rerun this script. If 'flatpak run com.github.Matoking.protontricks -V' returns 'unknown', then please update via flatpak." | tee -a "$LOGFILE" + cleaner_exit + fi +} + +####################################### +# Detect Skyrim or Fallout 4 Function # +####################################### + +detect_game() { + # Define lookup table for games + declare -A game_lookup=( + ["Skyrim"]="Skyrim Special Edition" + ["Fallout 4"]="Fallout 4" + ["Fallout New Vegas"]="Fallout New Vegas" + ["FNV"]="Fallout New Vegas" + ["Oblivion"]="Oblivion" + ) + + # Try direct match first + for pattern in "${!game_lookup[@]}"; do + if [[ $choice == *"$pattern"* ]]; then + gamevar="${game_lookup[$pattern]}" + which_game="${gamevar%% *}" + echo "Game variable set to $which_game." >>"$LOGFILE" 2>&1 + echo "Game variable: $gamevar" >>"$LOGFILE" 2>&1 + return 0 + fi + done + + # Handle generic "Fallout" case + if [[ $choice == *"Fallout"* ]]; then + PS3="Please select a Fallout game (enter the number): " + select fallout_opt in "Fallout 4" "Fallout New Vegas"; do + if [[ -n $fallout_opt ]]; then + gamevar="$fallout_opt" + which_game="${gamevar%% *}" + echo "Game variable set to $which_game." >>"$LOGFILE" 2>&1 + echo "Game variable: $gamevar" >>"$LOGFILE" 2>&1 + return 0 + else + echo "Invalid option" + fi + done + fi + + # If no match found, show selection menu + PS3="Please select a game (enter the number): " + select opt in "Skyrim" "Fallout 4" "Fallout New Vegas" "Oblivion"; do + if [[ -n $opt ]]; then + gamevar="${game_lookup[$opt]}" + which_game="${gamevar%% *}" + echo "Game variable set to $which_game." >>"$LOGFILE" 2>&1 + echo "Game variable: $gamevar" >>"$LOGFILE" 2>&1 + return 0 + else + echo "Invalid option" + fi + done +} + +################################### +# Try to detect the Steam Library # +################################### + +detect_steam_library() { + + local libraryfolders_vdf="$HOME/.steam/steam/config/libraryfolders.vdf" + + if [[ ! -f "$libraryfolders_vdf" ]]; then + echo "libraryfolders.vdf not found in ~/.steam/steam/config/. Please ensure Steam is installed." | tee -a "$LOGFILE" + return 1 + fi + + local library_paths=() + while IFS='' read -r line; do + if [[ "$line" =~ \"path\" ]]; then + local path=$(echo "$line" | sed 's/.*"path"\s*"\(.*\)"/\1/') + if [[ -n "$path" ]]; then + library_paths+=("$path/steamapps/common") + fi + fi + done <"$libraryfolders_vdf" + + local found=0 + for library_path in "${library_paths[@]}"; do + if [[ -d "$library_path/$gamevar" ]]; then + steam_library="$library_path" + found=1 + echo "Found '$gamevar' in $steam_library." >>$LOGFILE 2>&1 + break + else + echo "Checking $library_path: '$gamevar' not found." >>$LOGFILE 2>&1 + fi + done + + if [[ "$found" -eq 0 ]]; then + echo "Vanilla game not found in Steam library locations." | tee -a "$LOGFILE" + + while true; do + echo -e "\n** Enter the path to your Vanilla $gamevar directory manually (e.g. /data/SteamLibrary/steamapps/common/$gamevar): **" + read -e -r gamevar_input + + steam_library_input="${gamevar_input%/*}/" + + if [[ -d "$steam_library_input/$gamevar" ]]; then + steam_library="$steam_library_input" + echo "Found $gamevar in $steam_library_input." | tee -a "$LOGFILE" + echo "Steam Library set to: $steam_library" >>$LOGFILE 2>&1 + break + else + echo "Game not found in $steam_library_input. Please enter a valid path to Vanilla $gamevar." | tee -a "$LOGFILE" + fi + done + fi + + echo "Steam Library Location: $steam_library" >>$LOGFILE 2>&1 + + if [[ "$steamdeck" -eq 1 && "$steam_library" == "/run/media"* ]]; then + basegame_sdcard=1 + fi + +} + +################################# +# Detect Modlist Directory Path # +################################# + +detect_modlist_dir_path() { + log_status "DEBUG" "Detecting $MODLIST Install Directory..." + local modlist_paths=() + local choice modlist_ini_temp + local pattern=$(echo "$MODLIST" | sed 's/ /.*\|/g') + + # Search for ModOrganizer.exe entries matching the modlist pattern + while IFS= read -r entry; do + modlist_paths+=("$(dirname "${entry//[\"\']/}")") + done < <(strings ~/.steam/steam/userdata/*/config/shortcuts.vdf | grep -iE "ModOrganizer.exe" | grep -iE "$pattern") + + # If no exact matches, get all ModOrganizer.exe instances + if [[ ${#modlist_paths[@]} -eq 0 ]]; then + echo "No exact matches found. Searching for all ModOrganizer.exe instances..." + while IFS= read -r entry; do + modlist_paths+=("$(dirname "${entry//[\"\']/}")") + done < <(strings ~/.steam/steam/userdata/*/config/shortcuts.vdf | grep -iE "ModOrganizer.exe") + fi + + # Handle different cases based on number of paths found + if [[ ${#modlist_paths[@]} -eq 0 ]]; then + # No paths found - must enter manually + echo -e "\e[34mNo ModOrganizer.exe entries found. Please enter the directory manually:\e[0m" + read -r -e modlist_dir + elif [[ ${#modlist_paths[@]} -eq 1 ]]; then + # Single path found - use it directly without output + modlist_dir="${modlist_paths[0]}" + else + # Multiple paths found - show selection menu + echo "Select the ModOrganizer directory:" + for i in "${!modlist_paths[@]}"; do + echo -e "\e[33m$((i + 1))) ${modlist_paths[i]}\e[0m" + done + echo -e "\e[34m$(( ${#modlist_paths[@]} + 1 ))) Enter path manually\e[0m" + + while true; do + read -p "Enter your choice (1-$((${#modlist_paths[@]} + 1))): " choice + if [[ "$choice" =~ ^[0-9]+$ && "$choice" -ge 1 && "$choice" -le $(( ${#modlist_paths[@]} + 1 )) ]]; then + if [[ "$choice" -eq $(( ${#modlist_paths[@]} + 1 )) ]]; then + echo -ne "\e[34mEnter the ModOrganizer directory path: \e[0m" + read -r -e modlist_dir + else + modlist_dir="${modlist_paths[choice - 1]}" + fi + break + else + echo "Invalid selection. Please try again." + fi + done + fi + + # Validate selection + modlist_ini_temp="$modlist_dir/ModOrganizer.ini" + while [[ ! -f "$modlist_ini_temp" ]]; do + echo "ModOrganizer.ini not found in $modlist_dir. Please enter a valid path." + echo -ne "\e[34mEnter the ModOrganizer directory path: \e[0m" + read -r -e modlist_dir + modlist_ini_temp="$modlist_dir/ModOrganizer.ini" + done + + # Save and log results + modlist_ini="$modlist_ini_temp" + echo "Modlist directory: $modlist_dir" >> "$LOGFILE" + echo "Modlist INI location: $modlist_ini" >> "$LOGFILE" +} + +##################################################### +# Set protontricks permissions on Modlist Directory # +##################################################### + +set_protontricks_perms() { + if [ "$which_protontricks" = "flatpak" ]; then + log_status "INFO" "\nSetting Protontricks permissions..." + flatpak override --user com.github.Matoking.protontricks --filesystem="$modlist_dir" + log_status "SUCCESS" "Done!" + + if [[ $steamdeck = 1 ]]; then + log_status "WARN" "\nChecking for SDCard and setting permissions appropriately.." + sdcard_path=$(df -h | grep "/run/media" | awk {'print $NF'}) + echo "$sdcard_path" >>$LOGFILE 2>&1 + flatpak override --user --filesystem=$sdcard_path com.github.Matoking.protontricks + flatpak override --user --filesystem=/run/media/mmcblk0p1 com.github.Matoking.protontricks + log_status "SUCCESS" "Done." + fi + else + log_status "DEBUG" "Using Native protontricks, skip setting permissions" + fi +} + +##################################### +# Enable Visibility of (.)dot files # +##################################### + +enable_dotfiles() { + log_status "DEBUG" "APPID=$APPID" + log_status "INFO" "\nEnabling visibility of (.)dot files..." + + # Completely redirect all output to avoid any wine debug messages + dotfiles_check=$(WINEDEBUG=-all run_protontricks -c 'wine reg query "HKEY_CURRENT_USER\Software\Wine" /v ShowDotFiles' $APPID > /dev/null 2>&1; + WINEDEBUG=-all run_protontricks -c 'wine reg query "HKEY_CURRENT_USER\Software\Wine" /v ShowDotFiles' $APPID 2>/dev/null | grep ShowDotFiles | awk '{gsub(/\r/,""); print $NF}') + + log_status "DEBUG" "Current dotfiles setting: $dotfiles_check" + + if [[ "$dotfiles_check" = "Y" ]]; then + log_status "INFO" "DotFiles already enabled via registry... skipping" + else + # Method 2: Set registry key (standard approach) + log_status "DEBUG" "Setting ShowDotFiles registry key..." + WINEDEBUG=-all run_protontricks -c 'wine reg add "HKEY_CURRENT_USER\Software\Wine" /v ShowDotFiles /d Y /f' $APPID > /dev/null 2>&1 + + # Method 3: Also try direct winecfg approach as backup + log_status "DEBUG" "Also setting via winecfg command..." + WINEDEBUG=-all run_protontricks -c 'winecfg /v wine' $APPID > /dev/null 2>&1 + + # Method 4: Create user.reg entry if it doesn't exist + log_status "DEBUG" "Ensuring user.reg has correct entry..." + prefix_path=$(WINEDEBUG=-all run_protontricks -c 'echo $WINEPREFIX' $APPID 2>/dev/null) + if [[ -n "$prefix_path" && -d "$prefix_path" ]]; then + if [[ -f "$prefix_path/user.reg" ]]; then + if ! grep -q "ShowDotFiles" "$prefix_path/user.reg" 2>/dev/null; then + echo '[Software\\Wine] 1603891765' >> "$prefix_path/user.reg" 2>/dev/null + echo '"ShowDotFiles"="Y"' >> "$prefix_path/user.reg" 2>/dev/null + fi + fi + fi + + # Verify the setting took effect + dotfiles_verify=$(WINEDEBUG=-all run_protontricks -c 'wine reg query "HKEY_CURRENT_USER\Software\Wine" /v ShowDotFiles' $APPID > /dev/null 2>&1; + WINEDEBUG=-all run_protontricks -c 'wine reg query "HKEY_CURRENT_USER\Software\Wine" /v ShowDotFiles' $APPID 2>/dev/null | grep ShowDotFiles | awk '{gsub(/\r/,""); print $NF}') + log_status "DEBUG" "Verification check: $dotfiles_verify" + + log_status "SUCCESS" "Done!" + fi +} + +############################################### +# Set Windows 10 version in the proton prefix # +############################################### + +set_win10_prefix() { + WINEDEBUG=-all run_protontricks --no-bwrap $APPID win10 >/dev/null 2>&1 +} + +###################################### +# Install Wine Components & VCRedist # +###################################### + +install_wine_components() { + log_status "INFO" "Installing Wine Components... This can take some time, be patient!" + + # Define game-specific component sets + local protontricks_appid="$APPID" + local protontricks_components=() + + # Common components for all games + local common_components=("fontsmooth=rgb" "xact" "xact_x64" "vcrun2022") + + # Game-specific configuration + case "$gamevar" in + "Skyrim Special Edition"|"Fallout 4") + protontricks_components=("${common_components[@]}" "d3dcompiler_47" "d3dx11_43" "d3dcompiler_43" "dotnet6" "dotnet7") + ;; + "Fallout New Vegas") + protontricks_components=("${common_components[@]}" "d3dx9_43" "d3dx9") + protontricks_appid="22380" # Force appid for FNV + ;; + "Oblivion") + protontricks_components=("${common_components[@]}" "d3dx9_43" "d3dx9") + ;; + *) + echo "Unsupported game: $gamevar" | tee -a "$LOGFILE" + return 1 + ;; + esac + + # Log the command we're about to run + echo "Installing components: ${protontricks_components[*]}" >>$LOGFILE 2>&1 + + # Run the installation with progress indicator + printf "Protontricks running... " + + # Try up to 3 times to install components + local max_attempts=3 + local attempt=1 + local success=false + + while [[ $attempt -le $max_attempts && $success == false ]]; do + if [[ $attempt -gt 1 ]]; then + echo "Retry attempt $attempt/$max_attempts..." | tee -a "$LOGFILE" + sleep 2 + fi + + if WINEDEBUG=-all run_protontricks --no-bwrap "$protontricks_appid" -q "${protontricks_components[@]}" >/dev/null 2>&1; then + success=true + else + echo "Attempt $attempt failed, cleaning up wine processes before retry..." >>$LOGFILE 2>&1 + cleanup_wine_procs + attempt=$((attempt+1)) + fi + done + + if [[ $success == true ]]; then + printf "Done.\n" + log_status "SUCCESS" "Wine Component installation completed." + else + printf "Failed.\n" + log_status "ERROR" "Component install failed after $max_attempts attempts." + return 1 + fi + + # Verify installation + log_status "DEBUG" "Verifying installed components..." + local output + output=$(run_protontricks --no-bwrap "$protontricks_appid" list-installed 2>/dev/null) + + # Clean up and deduplicate the component list + local cleaned_output + cleaned_output=$(echo "$output" | grep -v "Using winetricks" | sort -u | grep -v '^$') + log_status "DEBUG" "Installed components (unique):" + echo "$cleaned_output" >> "$LOGFILE" + + # Check for critical components only to avoid false negatives + local critical_components=("vcrun2022" "xact") + local missing_components=() + + for component in "${critical_components[@]}"; do + if ! grep -q "$component" <<<"$output"; then + missing_components+=("$component") + fi + done + + if [[ ${#missing_components[@]} -gt 0 ]]; then + echo -e "\nWarning: Some critical components may be missing: ${missing_components[*]}" | tee -a "$LOGFILE" + echo "Installation will continue, but you may encounter issues." | tee -a "$LOGFILE" + else + echo "Critical components verified successfully." >>$LOGFILE 2>&1 + fi + + return 0 +} + +############################################ +# Detect default compatdata Directory Path # +############################################ +default_steam_compatdata_dir() { + # Prefer ~/.local/share/Steam if it exists + if [[ -d "$HOME/.local/share/Steam/steamapps/compatdata" ]]; then + echo "$HOME/.local/share/Steam/steamapps/compatdata" + elif [[ -d "$HOME/.steam/steam/steamapps/compatdata" ]]; then + echo "$HOME/.steam/steam/steamapps/compatdata" + else + # Do not create the directory; just return empty string + echo "" + fi +} + +# Helper to get all Steam library folders from libraryfolders.vdf +get_all_steam_libraries() { + local vdf_file="$HOME/.steam/steam/config/libraryfolders.vdf" + local libraries=("$HOME/.local/share/Steam" "$HOME/.steam/steam") + if [[ -f "$vdf_file" ]]; then + while IFS='' read -r line; do + if [[ "$line" =~ "path" ]]; then + local path=$(echo "$line" | sed 's/.*"path"\s*"\(.*\)"/\1/') + if [[ -n "$path" ]]; then + libraries+=("$path") + fi + fi + done <"$vdf_file" + fi + echo "${libraries[@]}" +} + +#################################### +# Detect compatdata Directory Path # +#################################### + +detect_compatdata_path() { + local appid_to_check="$APPID" + if [[ "$gamevar" == "Fallout New Vegas" ]]; then + appid_to_check="22380" + local vdf_file="$HOME/.local/share/Steam/config/libraryfolders.vdf" + local libraries=("$HOME/.local/share/Steam") + # Parse all additional libraries from the VDF + if [[ -f "$vdf_file" ]]; then + while IFS= read -r line; do + if [[ "$line" =~ "path" ]]; then + # Extract the path value using sed + local path=$(echo "$line" | sed -E 's/.*"path"[ \t]*"([^"]+)".*/\1/') + if [[ "$path" == /* ]]; then + libraries+=("$path") + else + libraries+=("$HOME/$path") + fi + fi + done < "$vdf_file" + fi + compat_data_path="" + for lib in "${libraries[@]}"; do + local compat_path="$lib/steamapps/compatdata/$appid_to_check" + log_status "DEBUG" "Checking for compatdata at: $compat_path" + if [[ -d "$compat_path" ]]; then + compat_data_path="$compat_path" + log_status "DEBUG" "Found FNV compatdata: $compat_data_path" + break + fi + done + if [[ -z "$compat_data_path" ]]; then + log_status "ERROR" "Could not find compatdata directory for Fallout New Vegas (22380) in any Steam library." + log_status "ERROR" "Please ensure you have launched the vanilla Fallout New Vegas game at least once via Steam." + return 1 + fi + return 0 + fi + # ... (existing logic for other games) + # Check common Steam library locations first + for path in "$HOME/.local/share/Steam/steamapps/compatdata" "$HOME/.steam/steam/steamapps/compatdata"; do + if [[ -d "$path/$appid_to_check" ]]; then + compat_data_path="$path/$appid_to_check" + log_status "DEBUG" "compatdata Path detected: $compat_data_path" + break + fi + done + + # If not found in common locations, use find command + if [[ -z "$compat_data_path" ]]; then + find / -type d -name "compatdata" 2>/dev/null | while read -r compatdata_dir; do + if [[ -d "$compatdata_dir/$appid_to_check" ]]; then + compat_data_path="$compatdata_dir/$appid_to_check" + log_status "DEBUG" "compatdata Path detected: $compat_data_path" + break + fi + done + fi + + if [[ -z "$compat_data_path" ]]; then + log_status "ERROR" "Directory named '$appid_to_check' not found in any compatdata directories." + log_status "ERROR" "Please ensure you have started the Steam entry for the modlist at least once, even if it fails.." + else + log_status "DEBUG" "Found compatdata directory with '$appid_to_check': $compat_data_path" + fi +} + +######################### +# Detect Proton Version # +######################### + +detect_proton_version() { + log_status "DEBUG" "Detecting Proton version..." + + # Validate the compatdata path exists + if [[ ! -d "$compat_data_path" ]]; then + log_status "WARN" "Compatdata directory not found at '$compat_data_path'" + proton_ver="Unknown" + return 1 + fi + + # First try to get Proton version from the registry + if [[ -f "$compat_data_path/pfx/system.reg" ]]; then + local reg_output + reg_output=$(grep -A 3 "\"SteamClientProtonVersion\"" "$compat_data_path/pfx/system.reg" | grep "=" | cut -d= -f2 | tr -d '"' | tr -d ' ') + + if [[ -n "$reg_output" ]]; then + # Keep GE versions as is, otherwise prefix with "Proton" + if [[ "$reg_output" == *"GE"* ]]; then + proton_ver="$reg_output" # Keep GE versions as is + else + proton_ver="Proton $reg_output" + fi + log_status "DEBUG" "Detected Proton version from registry: $proton_ver" + return 0 + fi + fi + + # Fallback to config_info if registry method fails + if [[ -f "$compat_data_path/config_info" ]]; then + local config_ver + config_ver=$(head -n 1 "$compat_data_path/config_info") + if [[ -n "$config_ver" ]]; then + # Keep GE versions as is, otherwise prefix with "Proton" + if [[ "$config_ver" == *"GE"* ]]; then + proton_ver="$config_ver" # Keep GE versions as is + else + proton_ver="Proton $config_ver" + fi + log_status "DEBUG" "Detected Proton version from config_info: $proton_ver" + return 0 + fi + fi + + proton_ver="Unknown" + log_status "WARN" "Could not detect Proton version" + return 1 +} + +############################### +# Confirmation before running # +############################### + +confirmation_before_running() { + + echo "" | tee -a $LOGFILE + echo -e "Detail Checklist:" | tee -a $LOGFILE + echo -e "=================" | tee -a $LOGFILE + echo -e "Modlist: $MODLIST .....\e[32m OK.\e[0m" | tee -a $LOGFILE + echo -e "Directory: $modlist_dir .....\e[32m OK.\e[0m" | tee -a $LOGFILE + echo -e "Proton Version: $proton_ver .....\e[32m OK.\e[0m" | tee -a $LOGFILE + echo -e "App ID: $APPID" | tee -a $LOGFILE + +} + +################################# +# chown/chmod modlist directory # +################################# + +chown_chmod_modlist_dir() { + log_status "WARN" "Changing Ownership and Permissions of modlist directory (may require sudo password)" + + user=$(whoami) + group=$(id -gn) + log_status "DEBUG" "User is $user and Group is $group" + + sudo chown -R "$user:$group" "$modlist_dir" + sudo chmod -R 755 "$modlist_dir" +} + + +############################################### +# Backup ModOrganizer.ini and backup gamePath # +############################################### + +backup_modorganizer() { + log_status "DEBUG" "Backing up ModOrganizer.ini: $modlist_ini" + cp "$modlist_ini" "$modlist_ini.$(date +"%Y%m%d_%H%M%S").bak" + grep gamePath "$modlist_ini" | sed '/^backupPath/! s/gamePath/backupPath/' >> "$modlist_ini" +} + +######################################## +# Blank or set MO2 Downloads Directory # +######################################## + +blank_downloads_dir() { + log_status "INFO" "\nEditing download_directory..." + sed -i "/download_directory/c\download_directory =" "$modlist_ini" + log_status "SUCCESS" "Done." +} + +############################################ +# Replace the gamePath in ModOrganizer.ini # +############################################ + +replace_gamepath() { + log_status "INFO" "Setting game path in ModOrganizer.ini..." + + log_status "DEBUG" "Using Steam Library Path: $steam_library" + log_status "DEBUG" "Use SDCard?: $basegame_sdcard" + + # Check if Modlist uses Game Root, Stock Game, etc. + game_path_line=$(grep '^gamePath' "$modlist_ini") + log_status "DEBUG" "Game Path Line: $game_path_line" + + if [[ "$game_path_line" == *Stock\ Game* || "$game_path_line" == *STOCK\ GAME* || "$game_path_line" == *Stock\ Game\ Folder* || "$game_path_line" == *Stock\ Folder* || "$game_path_line" == *Skyrim\ Stock* || "$game_path_line" == *Game\ Root* || $game_path_line == *root\\\\Skyrim\ Special\ Edition* ]]; then + # Stock Game, Game Root or equivalent directory found + log_status "INFO" "Found Game Root/Stock Game or equivalent directory, editing Game Path..." + + # Get the end of our path + if [[ $game_path_line =~ Stock\ Game\ Folder ]]; then + modlist_gamedir="$modlist_dir/Stock Game Folder" + log_status "DEBUG" "Modlist Gamedir: $modlist_gamedir" + elif [[ $game_path_line =~ Stock\ Folder ]]; then + modlist_gamedir="$modlist_dir/Stock Folder" + elif [[ $game_path_line =~ Skyrim\ Stock ]]; then + modlist_gamedir="$modlist_dir/Skyrim Stock" + log_status "DEBUG" "Modlist Gamedir: $modlist_gamedir" + elif [[ $game_path_line =~ Game\ Root ]]; then + modlist_gamedir="$modlist_dir/Game Root" + log_status "DEBUG" "Modlist Gamedir: $modlist_gamedir" + elif [[ $game_path_line =~ STOCK\ GAME ]]; then + modlist_gamedir="$modlist_dir/STOCK GAME" + log_status "DEBUG" "Modlist Gamedir: $modlist_gamedir" + elif [[ $game_path_line =~ Stock\ Game ]]; then + modlist_gamedir="$modlist_dir/Stock Game" + log_status "DEBUG" "Modlist Gamedir: $modlist_gamedir" + elif [[ $game_path_line =~ root\\\\Skyrim\ Special\ Edition ]]; then + modlist_gamedir="$modlist_dir/root/Skyrim Special Edition" + log_status "DEBUG" "Modlist Gamedir: $modlist_gamedir" + fi + + if [[ "$modlist_sdcard" -eq "1" && "$steamdeck" -eq "1" ]]; then + log_status "DEBUG" "Using SDCard on Steam Deck" + modlist_gamedir_sdcard="${modlist_gamedir#*mmcblk0p1}" + sdcard_new_path="$modlist_gamedir_sdcard" + + # Strip /run/media/deck/UUID if present + if [[ "$sdcard_new_path" == /run/media/deck/* ]]; then + sdcard_new_path="/${sdcard_new_path#*/run/media/deck/*/*}" + log_status "DEBUG" "SD Card Path after stripping: $sdcard_new_path" + fi + + new_string="@ByteArray(D:${sdcard_new_path//\//\\\\})" + log_status "DEBUG" "New String: $new_string" + else + new_string="@ByteArray(Z:${modlist_gamedir//\//\\\\})" + log_status "DEBUG" "New String: $new_string" + fi + + elif [[ "$game_path_line" == *steamapps* ]]; then + log_status "INFO" "Vanilla Game Directory required, editing Game Path..." + modlist_gamedir="$steam_library/$gamevar" + log_status "DEBUG" "Modlist Gamedir: $modlist_gamedir" + + if [[ "$basegame_sdcard" -eq "1" && "$steamdeck" -eq "1" ]]; then + log_status "DEBUG" "Using SDCard on Steam Deck" + modlist_gamedir_sdcard="${modlist_gamedir#*mmcblk0p1}" + sdcard_new_path="$modlist_gamedir_sdcard/$gamevar" + new_string="@ByteArray(D:${sdcard_new_path//\//\\\\})" + log_status "DEBUG" "New String: $new_string" + else + new_string="@ByteArray(Z:${modlist_gamedir//\//\\\\})" + log_status "DEBUG" "New String: $new_string" + fi + else + log_status "WARN" "Neither Game Root, Stock Game or Vanilla Game directory found, Please launch MO and set path manually..." + return 1 + fi + + # Replace the string in the file + file_to_modify="$modlist_dir/ModOrganizer.ini" + escaped_new_string=$(printf '%s\n' "$new_string" | sed -e 's/[\/&]/\\&/g') + sed -i "/^gamePath/c\gamePath=$escaped_new_string" "$file_to_modify" + + log_status "SUCCESS" "Game path set successfully" +} + +########################################## +# Update Executables in ModOrganizer.ini # +########################################## + +update_executables() { + + # Take the line passed to the function + echo "Original Line: $orig_line_path" >>$LOGFILE 2>&1 + + skse_loc=$(echo "$orig_line_path" | cut -d '=' -f 2-) + echo "SKSE Loc: $skse_loc" >>$LOGFILE 2>&1 + + # Drive letter + if [[ "$modlist_sdcard" -eq 1 && "$steamdeck" -eq 1 ]]; then + echo "Using SDCard on Steam Deck" >>$LOGFILE 2>&1 + drive_letter=" = D:" + else + drive_letter=" = Z:" + fi + + # Find the workingDirectory number + + binary_num=$(echo "$orig_line_path" | cut -d '=' -f -1) + echo "Binary Num: $binary_num" >>$LOGFILE 2>&1 + + # Find the equvalent workingDirectory + justnum=$(echo "$binary_num" | cut -d '\' -f 1) + bin_path_start=$(echo "$binary_num" | tr -d ' ' | sed 's/\\/\\\\/g') + path_start=$(echo "$justnum\\workingDirectory" | sed 's/\\/\\\\/g') + echo "Path Start: $path_start" >>$LOGFILE 2>&1 + # Decide on steam apps or Stock Game etc + + if [[ "$orig_line_path" == *"mods"* ]]; then + # mods path type found + echo -e "mods path Found" >>$LOGFILE 2>&1 + + # Path Middle / modlist_dr + if [[ "$modlist_sdcard" -eq 1 && "$steamdeck" -eq 1 ]]; then + echo "Using SDCard on Steam Deck" >>$LOGFILE 2>&1 + drive_letter=" = D:" + echo "$modlist_dir" >>$LOGFILE 2>&1 + path_middle="${modlist_dir#*mmcblk0p1}" + # Strip /run/media/deck/UUID + if [[ "$path_middle" == /run/media/*/* ]]; then + path_middle="/${path_middle#*/run/media/*/*/*}" + echo "Path Middle after stripping: $path_middle" >>$LOGFILE 2>&1 + fi + else + path_middle="$modlist_dir" + fi + + echo "Path Middle: $path_middle" >>$LOGFILE 2>&1 + + path_end=$(echo "${skse_loc%/*}" | sed 's/.*\/mods/\/mods/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end=$(echo "$skse_loc" | sed 's/.*\/mods/\/mods/') + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + elif grep -q -E "(Stock Game|Game Root|STOCK GAME|Stock Game Folder|Stock Folder|Skyrim Stock|root/Skyrim Special Edition)" <<<"$orig_line_path"; then + # STOCK GAME ROOT FOUND + echo -e "Stock/Game Root Found" >>$LOGFILE 2>&1 + + # Path Middle / modlist_dr + if [[ "$modlist_sdcard" -eq 1 && "$steamdeck" -eq 1 ]]; then + echo "Using SDCard on Steam Deck" >>$LOGFILE 2>&1 + drive_letter=" = D:" + echo "Modlist Dir: $modlist_dir" >>$LOGFILE 2>&1 + path_middle="${modlist_dir#*mmcblk0p1}" + # Strip /run/media/deck/UUID + if [[ "$path_middle" == /run/media/*/* ]]; then + path_middle="/${path_middle#*/run/media/*/*/*}" + echo "Path Middle after stripping: $path_middle" >>$LOGFILE 2>&1 + fi + else + path_middle="$modlist_dir" + fi + echo "Path Middle: $path_middle" >>$LOGFILE 2>&1 + + # Get the end of our path + if [[ $orig_line_path =~ Stock\ Game ]]; then + dir_type="stockgame" + path_end=$(echo "${skse_loc%/*}" | sed 's/.*\/Stock Game/\/Stock Game/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end=$(echo "$skse_loc" | sed 's/.*\/Stock Game/\/Stock Game/') + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + elif [[ $orig_line_path =~ Game\ Root ]]; then + dir_type="gameroot" + path_end=$(echo "${skse_loc%/*}" | sed 's/.*\/Game Root/\/Game Root/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end=$(echo "$skse_loc" | sed 's/.*\/Game Root/\/Game Root/') + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + elif [[ $orig_line_path =~ STOCK\ GAME ]]; then + dir_type="STOCKGAME" + path_end=$(echo "${skse_loc%/*}" | sed 's/.*\/STOCK GAME/\/STOCK GAME/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end=$(echo "$skse_loc" | sed 's/.*\/STOCK GAME/\/STOCK GAME/') + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + elif [[ $orig_line_path =~ Stock\ Folder ]]; then + dir_type="stockfolder" + path_end=$(echo "${skse_loc%/*}" | sed 's/.*\/Stock Folder/\/Stock Folder/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end=$(echo "$skse_loc" | sed 's/.*\/Stock Folder/\/Stock Folder/') + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + elif [[ $orig_line_path =~ Skyrim\ Stock ]]; then + dir_type="skyrimstock" + path_end=$(echo "${skse_loc%/*}" | sed 's/.*\/Skyrim Stock/\/Skyrim Stock/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end=$(echo "$skse_loc" | sed 's/.*\/Skyrim Stock/\/Skyrim Stock/') + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + elif [[ $orig_line_path =~ Stock\ Game\ Folder ]]; then + dir_type="stockgamefolder" + path_end=$(echo "$skse_loc" | sed 's/.*\/Stock Game Folder/\/Stock Game Folder/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + elif [[ $orig_line_path =~ root\/Skyrim\ Special\ Edition ]]; then + dir_type="rootskyrimse" + path_end="/${skse_loc# }" + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end="/${skse_loc# }" + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + fi + elif [[ "$orig_line_path" == *"steamapps"* ]]; then + # STEAMAPPS FOUND + echo -e "steamapps Found" >>$LOGFILE 2>&1 + + # Path Middle / modlist_dr + if [[ "$basegame_sdcard" -eq "1" && "$steamdeck" -eq "1" ]]; then + echo "Using SDCard on Steam Deck" >>$LOGFILE 2>&1 + path_middle="${steam_library#*mmcblk0p1}" + drive_letter=" = D:" + else + echo "Steamapps Steam Library Path: $steam_library" + path_middle=${steam_library%%steamapps*} + fi + echo "Path Middle: $path_middle" >>$LOGFILE 2>&1 + path_end=$(echo "${skse_loc%/*}" | sed 's/.*\/steamapps/\/steamapps/') + echo "Path End: $path_end" >>$LOGFILE 2>&1 + bin_path_end=$(echo "$skse_loc" | sed 's/.*\/steamapps/\/steamapps/') + echo "Bin Path End: $bin_path_end" >>$LOGFILE 2>&1 + + else + echo "No matching pattern found in the path: $orig_line_path" >>$LOGFILE 2>&1 + bail_out=1 + echo $bail_out >>$LOGFILE 2>&1 + + fi + + echo "Bail Out: $bail_out" >>$LOGFILE 2>&1 + + if [[ $bail_out -eq 1 ]]; then + echo "Exiting function due to bail_out" >>$LOGFILE 2>&1 + return + else + # Combine them all together + full_bin_path="$bin_path_start$drive_letter$path_middle$bin_path_end" + echo "Full Bin Path: $full_bin_path" >>$LOGFILE 2>&1 + full_path="$path_start$drive_letter$path_middle$path_end" + echo "Full Path: $full_path" >>$LOGFILE 2>&1 + + # Replace forwardslashes with double backslashes + new_path=${full_path//\//\\\\\\\\} + echo "New Path: $new_path" >>$LOGFILE 2>&1 + + # Convert the lines in ModOrganizer.ini, if it isn't already + + sed -i "\|^${bin_path_start}|s|^.*$|${full_bin_path}|" "$modlist_ini" + # Convert workingDirectory entries + sed -i "\|^${path_start}|s|^.*$|${new_path}|" "$modlist_ini" + fi + +} + +################################################# +# Edit Custom binary and workingDirectory paths # +################################################# + +edit_binary_working_paths() { + + grep -E -e "skse64_loader\.exe" -e "f4se_loader\.exe" "$modlist_ini" | while IFS= read -r orig_line_path; do + update_executables + done + +} + +################################ +# Set or Select the Resolution # +################################ + +select_resolution() { + if [ "$steamdeck" -eq 1 ]; then + set_res="1280x800" + else + while true; do + echo -e "\e[31m ** Enter your desired resolution in the format 1920x1200: ** \e[0m" + read -p " " user_res + + # Validate the input format + if [[ "$user_res" =~ ^[0-9]+x[0-9]+$ ]]; then + # Ask for confirmation + echo -e "\e[31m \n** Is $user_res your desired resolution? (y/N): ** \e[0m" + read -p " " confirm + if [[ "$confirm" =~ ^[Yy]$ ]]; then + set_res="$user_res" + break + else + echo "Please enter the resolution again." | tee -a $LOGFILE + fi + else + echo "Invalid input format. Please enter the resolution in the format 1920x1200." | tee -a $LOGFILE + fi + done + fi + + echo "Resolution set to: $set_res" | tee -a $LOGFILE +} + +###################################### +# Update the resolution in INI files # +###################################### + +update_ini_resolution() { + + echo -ne "\nEditing Resolution in prefs files... " | tee -a "$LOGFILE" + + # Find all SSEDisplayTweaks.ini files in the specified directory and its subdirectories + ini_files=$(find "$modlist_dir" -name "SSEDisplayTweaks.ini") + + if [[ "$gamevar" == "Skyrim Special Edition" && -n "$ini_files" ]]; then + while IFS= read -r ini_file; do + # Use awk to replace the lines with the new values, handling spaces in paths + awk -v res="$set_res" '/^(#?)Resolution[[:space:]]*=/ { print "Resolution=" res; next } \ + /^(#?)Fullscreen[[:space:]]*=/ { print "Fullscreen=false"; next } \ + /^(#?)#Fullscreen[[:space:]]*=/ { print "#Fullscreen=false"; next } \ + /^(#?)Borderless[[:space:]]*=/ { print "Borderless=true"; next } \ + /^(#?)#Borderless[[:space:]]*=/ { print "#Borderless=true"; next }1' "$ini_file" >"$ini_file.new" + + cp "$ini_file.new" "$ini_file" + echo "Updated $ini_file with Resolution=$res, Fullscreen=false, Borderless=true" >>"$LOGFILE" 2>&1 + echo -e " Done." >>"$LOGFILE" 2>&1 + done <<<"$ini_files" + elif [[ "$gamevar" == "Fallout 4" ]]; then + echo "Not Skyrim, skipping SSEDisplayTweaks" >>"$LOGFILE" 2>&1 + fi + + ########## + + # Split $set_res into two variables + isize_w=$(echo "$set_res" | cut -d'x' -f1) + isize_h=$(echo "$set_res" | cut -d'x' -f2) + + # Find all instances of skyrimprefs.ini, Fallout4Prefs.ini, falloutprefs.ini, or Oblivion.ini in specified directories + + if [[ "$gamevar" == "Skyrim Special Edition" ]]; then + ini_files=$(find "$modlist_dir/profiles" "$modlist_dir/Stock Game" "$modlist_dir/Game Root" "$modlist_dir/STOCK GAME" "$modlist_dir/Stock Game Folder" "$modlist_dir/Stock Folder" "$modlist_dir/Skyrim Stock" -iname "skyrimprefs.ini" 2>/dev/null) + elif [[ "$gamevar" == "Fallout 4" ]]; then + ini_files=$(find "$modlist_dir/profiles" "$modlist_dir/Stock Game" "$modlist_dir/Game Root" "$modlist_dir/STOCK GAME" "$modlist_dir/Stock Game Folder" "$modlist_dir/Stock Folder" -iname "Fallout4Prefs.ini" 2>/dev/null) + elif [[ "$gamevar" == "Fallout New Vegas" ]]; then + ini_files=$(find "$modlist_dir/profiles" "$modlist_dir/Stock Game" "$modlist_dir/Game Root" "$modlist_dir/STOCK GAME" "$modlist_dir/Stock Game Folder" "$modlist_dir/Stock Folder" -iname "falloutprefs.ini" 2>/dev/null) + elif [[ "$gamevar" == "Oblivion" ]]; then + ini_files=$(find "$modlist_dir/profiles" "$modlist_dir/Stock Game" "$modlist_dir/Game Root" "$modlist_dir/STOCK GAME" "$modlist_dir/Stock Game Folder" "$modlist_dir/Stock Folder" -iname "Oblivion.ini" 2>/dev/null) + fi + + if [ -n "$ini_files" ]; then + while IFS= read -r ini_file; do + # Use awk to replace the lines with the new values in the appropriate ini file + if [[ "$gamevar" == "Skyrim Special Edition" ]] || [[ "$gamevar" == "Fallout 4" ]] || [[ "$gamevar" == "Fallout New Vegas" ]]; then + awk -v isize_w="$isize_w" -v isize_h="$isize_h" '/^iSize W/ { print "iSize W = " isize_w; next } \ + /^iSize H/ { print "iSize H = " isize_h; next }1' "$ini_file" >"$HOME/temp_file" && mv "$HOME/temp_file" "$ini_file" + elif [[ "$gamevar" == "Oblivion" ]]; then + awk -v isize_w="$isize_w" -v isize_h="$isize_h" '/^iSize W=/ { print "iSize W=" isize_w; next } \ + /^iSize H=/ { print "iSize H=" isize_h; next }1' "$ini_file" >"$HOME/temp_file" && mv "$HOME/temp_file" "$ini_file" + fi + + echo "Updated $ini_file with iSize W=$isize_w, iSize H=$isize_h" >>"$LOGFILE" 2>&1 + done <<<"$ini_files" + else + echo "No suitable prefs.ini files found in specified directories. Please set manually using the INI Editor in MO2." | tee -a "$LOGFILE" + fi + + echo -e "Done." | tee -a "$LOGFILE" + +} + +################### +# Edit resolution # +################### + +edit_resolution() { + if [[ -n "$selected_resolution" ]]; then + log_status "DEBUG" "Applying resolution: $selected_resolution" + set_res="$selected_resolution" + update_ini_resolution + else + log_status "DEBUG" "Resolution setup skipped" + fi +} + +########################## +# Small additional tasks # +########################## + +small_additional_tasks() { + + # Delete MO2 plugins that don't work via Proton + + file_to_delete="$modlist_dir/plugins/FixGameRegKey.py" + + if [ -e "$file_to_delete" ]; then + rm "$file_to_delete" + echo "File deleted: $file_to_delete" >>$LOGFILE 2>&1 + else + echo "File does not exist: $file_to_delete" >>"$LOGFILE" 2>&1 + fi + + # Download Font to support Bethini + wget https://github.com/mrbvrz/segoe-ui-linux/raw/refs/heads/master/font/seguisym.ttf -q -nc -O "$compat_data_path/pfx/drive_c/windows/Fonts/seguisym.ttf" + +} + +############################### +# Set Steam Artwork Function # +############################### + +set_steam_artwork() { + # Only run for Tuxborn modlist + if [[ "$MODLIST" == *"Tuxborn"* ]]; then + log_status "DEBUG" "Setting up Steam artwork for Tuxborn..." + + # Source directory with artwork + local source_dir="$modlist_dir/Steam Icons" + + if [[ ! -d "$source_dir" ]]; then + log_status "WARN" "Steam Icons directory not found at $source_dir" + return 1 + fi + + # Find all Steam userdata directories + for userdata_dir in "$HOME/.local/share/Steam/userdata" "$HOME/.steam/steam/userdata"; do + if [[ ! -d "$userdata_dir" ]]; then + continue + fi + + # Process each user ID directory + for user_id_dir in "$userdata_dir"/*; do + if [[ ! -d "$user_id_dir" || "$user_id_dir" == *"0"* ]]; then + continue # Skip non-directories and the anonymous user + fi + + # Create grid directory if it doesn't exist + local grid_dir="$user_id_dir/config/grid" + mkdir -p "$grid_dir" + + # Copy grid-tall.png to both APPID.png and APPIDp.png + if [[ -f "$source_dir/grid-tall.png" ]]; then + cp "$source_dir/grid-tall.png" "$grid_dir/${APPID}.png" + log_status "DEBUG" "Copied grid-tall.png to ${APPID}.png" + cp "$source_dir/grid-tall.png" "$grid_dir/${APPID}p.png" + log_status "DEBUG" "Copied grid-tall.png to ${APPID}p.png" + fi + + # Copy grid-hero.png to APPID_hero.png + if [[ -f "$source_dir/grid-hero.png" ]]; then + cp "$source_dir/grid-hero.png" "$grid_dir/${APPID}_hero.png" + log_status "DEBUG" "Copied grid-hero.png to ${APPID}_hero.png" + fi + + # Copy grid-logo.png to APPID_logo.png + if [[ -f "$source_dir/grid-logo.png" ]]; then + cp "$source_dir/grid-logo.png" "$grid_dir/${APPID}_logo.png" + log_status "DEBUG" "Copied grid-logo.png to ${APPID}_logo.png" + fi + + log_status "DEBUG" "Tuxborn artwork copied for user ID $(basename "$user_id_dir")" + done + done + + log_status "DEBUG" "Steam artwork setup complete for Tuxborn" + fi +} + + +########################## +# Modlist Specific Steps # +########################## + +modlist_specific_steps() { + local modlist_lower=$(echo "${MODLIST// /}" | tr '[:upper:]' '[:lower:]') + + # Call the Steam artwork function for all modlists + set_steam_artwork | tee -a "$LOGFILE" + + # Handle Wildlander specially due to its custom spinner animation + if [[ "$MODLIST" == *"Wildlander"* ]]; then + log_status "INFO" "\nRunning steps specific to \e[32m$MODLIST\e[0m. This can take some time, be patient!" + + # Install dotnet with spinner animation + spinner=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') + run_protontricks --no-bwrap "$APPID" -q dotnet472 >/dev/null 2>&1 & + + pid=$! # Store the PID of the background process + + while kill -0 "$pid" >/dev/null 2>&1; do + for i in "${spinner[@]}"; do + echo -en "\r${i}\c" + sleep 0.1 + done + done + + wait "$pid" # Wait for the process to finish + + # Clear the spinner and move to the next line + echo -en "\r\033[K" # Clear the spinner line + + if [[ $? -ne 0 ]]; then + log_status "ERROR" "Component install failed with exit code $?" + else + log_status "SUCCESS" "Wine Component install completed successfully." + fi + + new_output="$(run_protontricks --no-bwrap "$APPID" list-installed 2>/dev/null)" + log_status "DEBUG" "Components Found: $new_output" + return 0 + fi + + # Handle the rest of the modlists with the compact approach + for pattern in "${!modlist_configs[@]}"; do + if [[ "$pattern" != "wildlander" ]] && [[ "$modlist_lower" =~ ${pattern//|/|.*} ]]; then + log_status "INFO" "\nRunning steps specific to \e[32m$MODLIST\e[0m. This can take some time, be patient!" + + IFS=' ' read -ra components <<< "${modlist_configs[$pattern]}" + for component in "${components[@]}"; do + if [[ "$component" == "dotnet8" ]]; then + log_status "INFO" "\nDownloading .NET 8 Runtime" + wget https://download.visualstudio.microsoft.com/download/pr/77284554-b8df-4697-9a9e-4c70a8b35f29/6763c16069d1ab8fa2bc506ef0767366/dotnet-runtime-8.0.5-win-x64.exe -q -nc --show-progress --progress=bar:force:noscroll -O "$HOME/Downloads/dotnet-runtime-8.0.5-win-x64.exe" + + log_status "INFO" "Installing .NET 8 Runtime...." + WINEDEBUG=-all run_protontricks --no-bwrap -c 'wine "$HOME/Downloads/dotnet-runtime-8.0.5-win-x64.exe" /Q' "$APPID" >/dev/null 2>&1 + log_status "SUCCESS" "Done." + else + log_status "INFO" "Installing .NET ${component#dotnet}..." + WINEDEBUG=-all run_protontricks --no-bwrap "$APPID" -q "$component" >/dev/null 2>&1 + log_status "SUCCESS" "Done." + fi + done + + set_win10_prefix + new_output="$(run_protontricks --no-bwrap "$APPID" list-installed 2>/dev/null)" + log_status "DEBUG" "Components Found: $new_output" + break + fi + done +} + +###################################### +# Create DXVK Graphics Pipeline file # +###################################### + +create_dxvk_file() { + echo "Use SDCard for DXVK File?: $basegame_sdcard" >>"$LOGFILE" 2>&1 + echo -e "\nCreating dxvk.conf file - Checking if Modlist uses Game Root, Stock Game or Vanilla Game Directory.." >>"$LOGFILE" 2>&1 + + game_path_line=$(grep '^gamePath' "$modlist_ini") + echo "Game Path Line: $game_path_line" >>"$LOGFILE" 2>&1 + + if [[ "$game_path_line" == *Stock\ Game* || "$game_path_line" == *STOCK\ GAME* || "$game_path_line" == *Stock\ Game\ Folder* || "$game_path_line" == *Stock\ Folder* || "$game_path_line" == *Skyrim\ Stock* || "$game_path_line" == *Game\ Root* ]]; then + # Get the end of our path + if [[ $game_path_line =~ Stock\ Game\ Folder ]]; then + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/Stock Game Folder/dxvk.conf" + elif [[ $game_path_line =~ Stock\ Folder ]]; then + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/Stock Folder/dxvk.conf" + elif [[ $game_path_line =~ Skyrim\ Stock ]]; then + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/Skyrim Stock/dxvk.conf" + elif [[ $game_path_line =~ Game\ Root ]]; then + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/Game Root/dxvk.conf" + elif [[ $game_path_line =~ STOCK\ GAME ]]; then + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/STOCK GAME/dxvk.conf" + elif [[ $game_path_line =~ Stock\ Game ]]; then + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/Stock Game/dxvk.conf" + elif [[ $game_path_line =~ root\\Skyrim\ Special\ Edition ]]; then + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/root/Skyrim Special Edition/dxvk.conf" + fi + + if [[ "$modlist_sdcard" -eq "1" ]]; then + echo "Using SDCard" >>"$LOGFILE" 2>&1 + modlist_gamedir_sdcard="${modlist_gamedir#*mmcblk0p1}" + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_gamedir/dxvk.conf" + fi + + elif [[ "$game_path_line" == *steamapps* ]]; then + echo -ne "Vanilla Game Directory required, editing Game Path.. " >>"$LOGFILE" 2>&1 + modlist_gamedir="$steam_library" + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_gamedir/dxvk.conf" + if [[ "$basegame_sdcard" -eq "1" ]]; then + echo "Using SDCard" >>"$LOGFILE" 2>&1 + modlist_gamedir_sdcard="${modlist_gamedir#*mmcblk0p1}" + echo "dxvk.enableGraphicsPipelineLibrary = False" >"$modlist_dir/$gamevar/dxvk.conf" + fi + fi +} + +############################# +# Create protontricks alias # +############################# + +protontricks_alias() { + if [[ "$which_protontricks" = "flatpak" ]]; then + local protontricks_alias_exists=$(grep "^alias protontricks=" ~/.bashrc) + local launch_alias_exists=$(grep "^alias protontricks-launch" ~/.bashrc) + + if [[ -z "$protontricks_alias_exists" ]]; then + echo -e "\nAdding protontricks alias to ~/.bashrc" + echo "alias protontricks='flatpak run com.github.Matoking.protontricks'" >> ~/.bashrc + source ~/.bashrc + else + echo "protontricks alias already exists in ~/.bashrc" >> "$LOGFILE" 2>&1 + fi + + if [[ -z "$launch_alias_exists" ]]; then + echo -e "\nAdding protontricks-launch alias to ~/.bashrc" + echo "alias protontricks-launch='flatpak run --command=protontricks-launch com.github.Matoking.protontricks'" >> ~/.bashrc + source ~/.bashrc + else + echo "protontricks-launch alias already exists in ~/.bashrc" >> "$LOGFILE" 2>&1 + fi + else + echo "Protontricks is not installed via flatpak, skipping alias creation." >> "$LOGFILE" 2>&1 + fi +} + +############################ +# FNV Launch Option Notice # +############################ + +fnv_launch_options() { + log_status "DEBUG" "fnv_launch_options: gamevar='$gamevar', compat_data_path='$compat_data_path'" + if [[ "$gamevar" == "Fallout New Vegas" ]]; then + if [[ -n "$compat_data_path" && -d "$compat_data_path" ]]; then + log_status "WARN" "\nFor $MODLIST, please add the following line to the Launch Options in Steam for your '$MODLIST' entry:" + log_status "SUCCESS" "\nSTEAM_COMPAT_DATA_PATH=\"$compat_data_path\" %command%" + log_status "WARN" "\nThis is essential for the modlist to load correctly." + else + log_status "ERROR" "\nCould not determine the compatdata path for Fallout New Vegas. Please manually set the correct path in the Launch Options." + fi + fi +} + +##################### +# Exit more cleanly # +##################### + +cleaner_exit() { + # Clean up wine and winetricks processes + cleanup_wine_procs + log_status "DEBUG" "Cleanup complete" + exit 1 +} + +#################### +# END OF FUNCTIONS # +#################### + +####################### +# Note Script Version # +####################### + +echo -e "Script Version $script_ver" >>"$LOGFILE" 2>&1 + +###################### +# Note Date and Time # +###################### + +echo -e "Script started at: $(date +'%Y-%m-%d %H:%M:%S')" >>"$LOGFILE" 2>&1 + +############################# +# Detect if running on deck # +############################# + +detect_steamdeck + +########################################### +# Detect Protontricks (flatpak or native) # +########################################### + +detect_protontricks + +############################### +# Detect Protontricks Version # +############################### + +protontricks_version + +########################################## +# Create protontricks alias in ~/.bashrc # +########################################## + +protontricks_alias + +############################################################## +# List Skyrim and Fallout Modlists from Steam (protontricks) # +############################################################## + +IFS=$'\n' readarray -t output_array < <(run_protontricks -l | tr -d $'\r' | grep -i 'Non-Steam shortcut' | grep -i 'Skyrim\|Fallout\|FNV\|Oblivion' | cut -d ' ' -f 3-) + +if [[ ${#output_array[@]} -eq 0 ]]; then + echo "" | tee -a "$LOGFILE" + log_status "ERROR" "No modlists detected for Skyrim, Oblivion or Fallout/FNV!" + log_status "INFO" "Please make sure your entry in Steam is something like 'Skyrim - ModlistName'" + log_status "INFO" "or 'Fallout - ModlistName' AND that you have pressed play in Steam at least once!" + cleaner_exit +fi + +echo "" | tee -a "$LOGFILE" +echo -e "\e[33mDetected Modlists:\e[0m" | tee -a "$LOGFILE" + +# Print numbered list with color +for i in "${!output_array[@]}"; do + echo -e "\e[32m$((i + 1)))\e[0m ${output_array[$i]}" +done + +# Read user selection with proper prompt +echo "───────────────────────────────────────────────────────────────────" +while true; do + read -p $'\e[33mSelect a modlist (1-'"${#output_array[@]}"$'): \e[0m' choice_num + + # Add a debug flag at the top for easy toggling + DEBUG_MODLIST_SELECTION=0 # Set to 1 to enable extra debug output + + # After reading user input for choice_num: + if [[ $DEBUG_MODLIST_SELECTION -eq 1 ]]; then + echo "[DEBUG] Raw user input: '$choice_num'" | tee -a "$LOGFILE" + fi + choice_num=$(echo "$choice_num" | xargs) # Trim whitespace + if [[ $DEBUG_MODLIST_SELECTION -eq 1 ]]; then + echo "[DEBUG] Trimmed user input: '$choice_num'" | tee -a "$LOGFILE" + fi + + # Before the selection validation if-statement: + if [[ $DEBUG_MODLIST_SELECTION -eq 1 ]]; then + echo "[DEBUG] Validating: '$choice_num' =~ ^[0-9]+$ && $choice_num -ge 1 && $choice_num -le ${#output_array[@]}" | tee -a "$LOGFILE" + fi + + # Validate selection properly + if [[ "$choice_num" =~ ^[0-9]+$ ]] && [[ "$choice_num" -ge 1 ]] && [[ "$choice_num" -le "${#output_array[@]}" ]]; then + if [[ $DEBUG_MODLIST_SELECTION -eq 1 ]]; then + echo "[DEBUG] Selection valid. Index: $((choice_num - 1)), Value: '${output_array[$((choice_num - 1))]}'" | tee -a "$LOGFILE" + fi + choice="${output_array[$((choice_num - 1))]}" + MODLIST=$(echo "$choice" | cut -d ' ' -f 3- | rev | cut -d ' ' -f 2- | rev) + log_status "DEBUG" "MODLIST: $MODLIST" + break # Exit the loop if selection is valid + else + if [[ $DEBUG_MODLIST_SELECTION -eq 1 ]]; then + echo "[DEBUG] Invalid selection. choice_num: '$choice_num', output_array length: ${#output_array[@]}" | tee -a "$LOGFILE" + fi + log_status "ERROR" "Invalid selection. Please enter a number between 1 and ${#output_array[@]}." + # Removed exit 1, so the loop continues + fi +done + +# Add a newline after the selection for cleaner output +echo "" + +# Initial detection phase +cleanup_wine_procs +set_appid +detect_game +detect_steam_library +detect_modlist_dir_path + +# Set modlist_sdcard if required +modlist_sdcard=0 +if [[ "$modlist_dir" =~ ^/run/media ]]; then + modlist_sdcard=1 +fi + +# Detect compatdata path and Proton version +detect_compatdata_path +detect_proton_version +fnv_launch_options + +# Get resolution preference +if [ "$steamdeck" -eq 1 ]; then + selected_resolution="1280x800" + log_status "INFO" "Steam Deck detected - Resolution will be set to 1280x800" +else + echo -e "Do you wish to set the display resolution? (This can be changed manually later)" + read -p $'\e[33mSet resolution? (y/N): \e[0m' response + + if [[ "$response" =~ ^[Yy]$ ]]; then + while true; do + read -p $'\e[33mEnter resolution (e.g., 1920x1080): \e[0m' user_res + if [[ "$user_res" =~ ^[0-9]+x[0-9]+$ ]]; then + selected_resolution="$user_res" + log_status "DEBUG" "Resolution will be set to: $selected_resolution" + break + else + log_status "ERROR" "Invalid format. Please use format: 1920x1080" + fi + done + else + log_status "INFO" "Resolution setup skipped" + fi +fi + +# Then show the detection summary including the resolution if set +echo -e "\n\e[1mDetection Summary:\e[0m" | tee -a "$LOGFILE" +echo -e "===================" | tee -a "$LOGFILE" +echo -e "Selected Modlist: \e[32m$MODLIST\e[0m" | tee -a "$LOGFILE" +echo -e "Game Type: \e[32m$gamevar\e[0m" | tee -a "$LOGFILE" +echo -e "Steam App ID: \e[32m$APPID\e[0m" | tee -a "$LOGFILE" +echo -e "Modlist Directory: \e[32m$modlist_dir\e[0m" | tee -a "$LOGFILE" +echo -e "Proton Version: \e[32m$proton_ver\e[0m" | tee -a "$LOGFILE" +if [[ -n "$selected_resolution" ]]; then + echo -e "Resolution: \e[32m$selected_resolution\e[0m" | tee -a "$LOGFILE" +fi + +# Show simple confirmation with minimal info +read -rp $'\e[32mDo you want to proceed with the installation? (y/N)\e[0m ' proceed + +if [[ $proceed =~ ^[Yy]$ ]]; then + # Function to update progress + update_progress() { + local percent=$1 + local bar_length=50 + local filled_length=$((percent * bar_length / 100)) + local bar="" + + # Create the bar string with = for filled portions + for ((i = 0; i < bar_length; i++)); do + if [ $i -lt $filled_length ]; then + bar+="=" + else + bar+=" " + fi + done + + # Use \r to return to start of line and overwrite previous progress + printf "\r[%-${bar_length}s] %d%%" "$bar" "$percent" + } + + { + # Add newline before progress bar starts + echo "" + + # Protontricks setup (10%) + printf "\r\033[KProgress: [%-50s] %d%% - Setting up Protontricks..." " " "10" + set_protontricks_perms >/dev/null 2>&1 + + # Dotfiles (20%) + printf "\r\033[KProgress: [%-50s] %d%% - Enabling dotfiles..." "========== " "20" + enable_dotfiles >/dev/null 2>&1 + + # Wine components (40%) + printf "\r\033[KProgress: [%-50s] %d%% - Installing Wine components..." "==================== " "40" + install_wine_components >/dev/null 2>&1 + + # Windows 10 prefix (50%) + printf "\r\033[KProgress: [%-50s] %d%% - Setting Windows 10 prefix..." "========================= " "50" + set_win10_prefix >/dev/null 2>&1 + + # ModOrganizer configuration (70%) + printf "\r\033[KProgress: [%-50s] %d%% - Configuring Mod Organizer..." "=================================== " "70" + backup_modorganizer >/dev/null 2>&1 + blank_downloads_dir >/dev/null 2>&1 + replace_gamepath >/dev/null 2>&1 + edit_binary_working_paths >/dev/null 2>&1 + + # Resolution and additional tasks (90%) + printf "\r\033[KProgress: [%-50s] %d%% - Setting resolution and additional tasks..." "============================================ " "90" + edit_resolution >/dev/null 2>&1 + small_additional_tasks >/dev/null 2>&1 + create_dxvk_file >/dev/null 2>&1 + + # Final steps (100%) + printf "\r\033[KProgress: [%-50s] %d%% - Completing installation...\n" "==================================================" "100" + + # Remove user-facing artwork and debug output + # echo "" # Add spacing + # echo "ABOUT TO CALL MODLIST_SPECIFIC_STEPS FOR: $MODLIST" | tee -a "$LOGFILE" + modlist_specific_steps + # echo "FINISHED CALLING MODLIST_SPECIFIC_STEPS" | tee -a "$LOGFILE" + + # Add two newlines after progress bar completes + # printf "\n\n" + + chown_chmod_modlist_dir + fnv_launch_options >/dev/null 2>&1 + + } 2>>$LOGFILE + + # Show completion message + { + echo "" # Add blank line before success message + echo -e "\e[32m✓ Installation completed successfully!\e[0m" + echo -e "\n📝 Next Steps:" + echo " • Launch your modlist through Steam" + echo " • When Mod Organizer opens, verify the game path is correct" + if [[ "$gamevar" == "Skyrim Special Edition" || "$gamevar" == "Fallout 4" ]]; then + echo " • Run the game through SKSE/F4SE launcher" + fi + echo -e "\n💡 Detailed log available at: $LOGFILE\n" + } | tee -a "$LOGFILE" + + # Show SD Card status if detected + if [[ "$steamdeck" -eq 1 ]]; then + # On Steam Deck, SD card is /run/media/deck/ or /run/media/mmcblk0p1 + if [[ "$modlist_dir" =~ ^/run/media/deck/[^/]+(/.*)?$ ]] || [[ "$modlist_dir" == "/run/media/mmcblk0p1"* ]]; then + echo -e "SD Card: \e[32mDetected\e[0m" | tee -a "$LOGFILE" + fi + else + # On non-Deck, just show the path if it's /run/media, but don't call it SD card + if [[ "$modlist_dir" == "/run/media"* ]]; then + echo -e "Removable Media: \e[33mDetected at $modlist_dir\e[0m" | tee -a "$LOGFILE" + fi + fi +else + log_status "INFO" "Installation cancelled." + cleaner_exit +fi + +# After the block that prints the completion message and next steps: +# (Find the line: echo -e "\n💡 Detailed log available at: $LOGFILE\n") +# Add this immediately after: +fnv_launch_options \ No newline at end of file diff --git a/jackify/__init__.py b/jackify/__init__.py index d5f1880..5eb60ef 100644 --- a/jackify/__init__.py +++ b/jackify/__init__.py @@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing Wabbajack modlists natively on Linux systems. """ -__version__ = "0.1.2" +__version__ = "0.1.1" diff --git a/jackify/backend/handlers/diagnostic_helper.py b/jackify/backend/handlers/diagnostic_helper.py index 5ee955a..d119b81 100644 --- a/jackify/backend/handlers/diagnostic_helper.py +++ b/jackify/backend/handlers/diagnostic_helper.py @@ -168,7 +168,7 @@ def main(): print(f"Error: {diagnosis['error']}") return - print(f"\n📊 Diagnosis Results:") + print(f"\nDiagnosis Results:") print(f" Average CPU: {diagnosis['avg_cpu']:.1f}% (Range: {diagnosis['min_cpu']:.1f}% - {diagnosis['max_cpu']:.1f}%)") print(f" Memory usage: {diagnosis['avg_memory_mb']:.1f}MB (Peak: {diagnosis['max_memory_mb']:.1f}MB)") print(f" Low CPU samples: {diagnosis['low_cpu_samples']}/{diagnosis['samples']} " diff --git a/jackify/backend/handlers/modlist_handler.py b/jackify/backend/handlers/modlist_handler.py index 141583d..9741c88 100644 --- a/jackify/backend/handlers/modlist_handler.py +++ b/jackify/backend/handlers/modlist_handler.py @@ -761,7 +761,14 @@ class ModlistHandler: # Conditionally update binary and working directory paths # Skip for jackify-engine workflows since paths are already correct if not getattr(self, 'engine_installed', False): - steam_libraries = [self.steam_library] if self.steam_library else None + # Convert steamapps/common path to library root path + steam_libraries = None + if self.steam_library: + # self.steam_library is steamapps/common, need to go up 2 levels to get library root + steam_library_root = Path(self.steam_library).parent.parent + steam_libraries = [steam_library_root] + self.logger.debug(f"Using Steam library root: {steam_library_root}") + if not self.path_handler.edit_binary_working_paths( modlist_ini_path=modlist_ini_path_obj, modlist_dir_path=modlist_dir_path_obj, diff --git a/jackify/backend/handlers/modlist_install_cli.py b/jackify/backend/handlers/modlist_install_cli.py index 6fd3ead..a0c2d0f 100644 --- a/jackify/backend/handlers/modlist_install_cli.py +++ b/jackify/backend/handlers/modlist_install_cli.py @@ -723,13 +723,17 @@ class ModlistInstallCLI: if chunk == b'\n': # Complete line - decode and print line = buffer.decode('utf-8', errors='replace') - print(line, end='') + # Enhance Nexus download errors with modlist context + enhanced_line = self._enhance_nexus_error(line) + print(enhanced_line, end='') buffer = b'' last_progress_time = time.time() elif chunk == b'\r': # Carriage return - decode and print without newline line = buffer.decode('utf-8', errors='replace') - print(line, end='') + # Enhance Nexus download errors with modlist context + enhanced_line = self._enhance_nexus_error(line) + print(enhanced_line, end='') sys.stdout.flush() buffer = b'' last_progress_time = time.time() @@ -1098,4 +1102,36 @@ class ModlistInstallCLI: print(f"Nexus API Key: [SET]") else: print(f"Nexus API Key: [NOT SET - WILL LIKELY FAIL]") - print(f"{COLOR_INFO}----------------------------------------{COLOR_RESET}") \ No newline at end of file + print(f"{COLOR_INFO}----------------------------------------{COLOR_RESET}") + + def _enhance_nexus_error(self, line: str) -> str: + """ + Enhance Nexus download error messages by adding the mod URL for easier troubleshooting. + """ + import re + + # Pattern to match Nexus download errors with ModID and FileID + nexus_error_pattern = r"Failed to download '[^']+' from Nexus \(Game: ([^,]+), ModID: (\d+), FileID: \d+\):" + + match = re.search(nexus_error_pattern, line) + if match: + game_name = match.group(1) + mod_id = match.group(2) + + # Map game names to Nexus URL segments + game_url_map = { + 'SkyrimSpecialEdition': 'skyrimspecialedition', + 'Skyrim': 'skyrim', + 'Fallout4': 'fallout4', + 'FalloutNewVegas': 'newvegas', + 'Oblivion': 'oblivion', + 'Starfield': 'starfield' + } + + game_url = game_url_map.get(game_name, game_name.lower()) + mod_url = f"https://www.nexusmods.com/{game_url}/mods/{mod_id}" + + # Add URL on next line for easier debugging + return f"{line}\n Nexus URL: {mod_url}" + + return line \ No newline at end of file diff --git a/jackify/backend/handlers/path_handler.py b/jackify/backend/handlers/path_handler.py index d9e0cd6..2daab2c 100644 --- a/jackify/backend/handlers/path_handler.py +++ b/jackify/backend/handlers/path_handler.py @@ -815,11 +815,12 @@ class PathHandler: subpath = value_part[idx:].lstrip('/') correct_steam_lib = None for lib in steam_libraries: - if (lib / subpath.split('/')[2]).exists(): - correct_steam_lib = lib.parent + # Check if the actual game folder exists in this library + if len(subpath.split('/')) > 3 and (lib / subpath.split('/')[2] / subpath.split('/')[3]).exists(): + correct_steam_lib = lib break if not correct_steam_lib and steam_libraries: - correct_steam_lib = steam_libraries[0].parent + correct_steam_lib = steam_libraries[0] if correct_steam_lib: new_binary_path = f"{drive_prefix}/{correct_steam_lib}/{subpath}".replace('\\', '/').replace('//', '/') else: diff --git a/jackify/backend/handlers/self_update.py b/jackify/backend/handlers/self_update.py deleted file mode 100644 index 536cbe3..0000000 --- a/jackify/backend/handlers/self_update.py +++ /dev/null @@ -1,141 +0,0 @@ -import os -import sys -import json -import requests -import shutil -import tempfile -import time -from pathlib import Path - -GITHUB_OWNER = "Omni-guides" -GITHUB_REPO = "Jackify" -ASSET_NAME = "jackify" -CONFIG_DIR = os.path.expanduser("~/.config/jackify") -TOKEN_PATH = os.path.join(CONFIG_DIR, "github_token") -LAST_CHECK_PATH = os.path.join(CONFIG_DIR, "last_update_check.json") - -THROTTLE_HOURS = 6 - -def get_github_token(): - if os.path.exists(TOKEN_PATH): - with open(TOKEN_PATH, "r") as f: - return f.read().strip() - return None - -def get_latest_release_info(): - url = f"https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/releases/latest" - headers = {} - token = get_github_token() - if token: - headers["Authorization"] = f"token {token}" - resp = requests.get(url, headers=headers, verify=True) - if resp.status_code == 200: - return resp.json() - else: - raise RuntimeError(f"Failed to fetch release info: {resp.status_code} {resp.text}") - -def get_current_version(): - # This should match however Jackify stores its version - try: - from jackify import __version__ - return __version__ - except ImportError: - return None - -def should_check_for_update(): - try: - if os.path.exists(LAST_CHECK_PATH): - with open(LAST_CHECK_PATH, "r") as f: - data = json.load(f) - last_check = data.get("last_check", 0) - now = int(time.time()) - if now - last_check < THROTTLE_HOURS * 3600: - return False - return True - except Exception as e: - print(f"[WARN] Could not read last update check timestamp: {e}") - return True - -def record_update_check(): - try: - with open(LAST_CHECK_PATH, "w") as f: - json.dump({"last_check": int(time.time())}, f) - except Exception as e: - print(f"[WARN] Could not write last update check timestamp: {e}") - -def check_for_update(): - if not should_check_for_update(): - return False, None, None - try: - release = get_latest_release_info() - latest_version = release["tag_name"].lstrip("v") - current_version = get_current_version() - if current_version is None: - print("[WARN] Could not determine current version.") - record_update_check() - return False, None, None - if latest_version > current_version: - record_update_check() - return True, latest_version, release - record_update_check() - return False, latest_version, release - except Exception as e: - print(f"[ERROR] Update check failed: {e}") - record_update_check() - return False, None, None - -def download_latest_asset(release): - token = get_github_token() - headers = {"Accept": "application/octet-stream"} - if token: - headers["Authorization"] = f"token {token}" - for asset in release["assets"]: - if asset["name"] == ASSET_NAME: - download_url = asset["url"] - resp = requests.get(download_url, headers=headers, stream=True, verify=True) - if resp.status_code == 200: - return resp.content - else: - raise RuntimeError(f"Failed to download asset: {resp.status_code} {resp.text}") - raise RuntimeError(f"Asset '{ASSET_NAME}' not found in release.") - -def replace_current_binary(new_binary_bytes): - current_exe = os.path.realpath(sys.argv[0]) - backup_path = current_exe + ".bak" - try: - # Write to a temp file first - with tempfile.NamedTemporaryFile(delete=False, dir=os.path.dirname(current_exe)) as tmpf: - tmpf.write(new_binary_bytes) - tmp_path = tmpf.name - # Backup current binary - shutil.copy2(current_exe, backup_path) - # Replace atomically - os.replace(tmp_path, current_exe) - os.chmod(current_exe, 0o755) - print(f"[INFO] Updated binary written to {current_exe}. Backup at {backup_path}.") - return True - except Exception as e: - print(f"[ERROR] Failed to replace binary: {e}") - return False - -def main(): - if '--update' in sys.argv: - print("Checking for updates...") - update_available, latest_version, release = check_for_update() - if update_available: - print(f"A new version (v{latest_version}) is available. Downloading...") - try: - new_bin = download_latest_asset(release) - if replace_current_binary(new_bin): - print("Update complete! Please restart Jackify.") - else: - print("Update failed during binary replacement.") - except Exception as e: - print(f"[ERROR] Update failed: {e}") - else: - print("You are already running the latest version.") - sys.exit(0) - -# For direct CLI testing -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/jackify/backend/services/automated_prefix_service.py b/jackify/backend/services/automated_prefix_service.py index 8d63b06..6c0f3f0 100644 --- a/jackify/backend/services/automated_prefix_service.py +++ b/jackify/backend/services/automated_prefix_service.py @@ -101,7 +101,7 @@ class AutomatedPrefixService: 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") + logger.error("Native Steam service failed to create shortcut") return False, None except Exception as e: @@ -471,7 +471,7 @@ exit""" logger.warning(f"Error running protontricks -l on attempt {i+1}: {e}") time.sleep(1) - logger.error(f"❌ Shortcut '{shortcut_name}' not found in protontricks after 30 seconds") + logger.error(f"Shortcut '{shortcut_name}' not found in protontricks after 30 seconds") return None except Exception as e: @@ -939,7 +939,7 @@ echo Prefix creation complete. except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.TimeoutExpired): continue - logger.info("ℹ️ No more processes to kill") + logger.info("No more processes to kill") return True except Exception as e: @@ -1296,7 +1296,7 @@ echo Prefix creation complete. time.sleep(1) - logger.warning(f"❌ Timeout waiting for prefix completion after {timeout} seconds") + logger.warning(f"Timeout waiting for prefix completion after {timeout} seconds") return False except Exception as e: @@ -1356,7 +1356,7 @@ echo Prefix creation complete. if killed_count > 0: logger.info(f" Killed {killed_count} ModOrganizer processes") else: - logger.warning("❌ No ModOrganizer processes found to kill") + logger.warning("No ModOrganizer processes found to kill") return killed_count @@ -1624,11 +1624,11 @@ echo Prefix creation complete. return True - logger.error(f"❌ Shortcut '{shortcut_name}' not found for CompatTool setting") + logger.error(f"Shortcut '{shortcut_name}' not found for CompatTool setting") return False except Exception as e: - logger.error(f"❌ Error setting CompatTool on shortcut: {e}") + logger.error(f"Error setting CompatTool on shortcut: {e}") return False def _set_proton_on_shortcut(self, shortcut_name: str) -> bool: @@ -2633,7 +2633,7 @@ echo Prefix creation complete. logger.info(f" Proton prefix created at: {pfx}") return True else: - logger.warning(f"⚠️ Proton prefix not found at: {pfx}") + logger.warning(f"Proton prefix not found at: {pfx}") return False except subprocess.TimeoutExpired: @@ -2735,7 +2735,7 @@ echo Prefix creation complete. logger.info(" Compatibility tool persists") return True else: - logger.warning("⚠️ Compatibility tool not found") + logger.warning("Compatibility tool not found") return False except Exception as e: diff --git a/jackify/backend/services/native_steam_service.py b/jackify/backend/services/native_steam_service.py index 4b7cc6c..3b4e101 100644 --- a/jackify/backend/services/native_steam_service.py +++ b/jackify/backend/services/native_steam_service.py @@ -228,14 +228,14 @@ class NativeSteamService: # Write back to file if self.write_shortcuts_vdf(data): - logger.info(f"✅ Shortcut created successfully at index {next_index}") + logger.info(f"Shortcut created successfully at index {next_index}") return True, unsigned_app_id else: - logger.error("❌ Failed to write shortcut to VDF") + logger.error("Failed to write shortcut to VDF") return False, None except Exception as e: - logger.error(f"❌ Error creating shortcut: {e}") + logger.error(f"Error creating shortcut: {e}") return False, None def set_proton_version(self, app_id: int, proton_version: str = "proton_experimental") -> bool: @@ -320,11 +320,11 @@ class NativeSteamService: with open(config_path, 'w', encoding='utf-8') as f: f.write(new_config_text) - logger.info(f"✅ Successfully set Proton version '{proton_version}' for AppID {app_id} using config.vdf only (steam-conductor method)") + logger.info(f"Successfully set Proton version '{proton_version}' for AppID {app_id} using config.vdf only (steam-conductor method)") return True except Exception as e: - logger.error(f"❌ Error setting Proton version: {e}") + logger.error(f"Error setting Proton version: {e}") return False def create_shortcut_with_proton(self, app_name: str, exe_path: str, start_dir: str = None, @@ -351,7 +351,7 @@ class NativeSteamService: logger.error("Failed to set Proton version (shortcut still created)") return False, app_id # Shortcut exists but Proton setting failed - logger.info(f"✅ Complete workflow successful: '{app_name}' with '{proton_version}'") + logger.info(f"Complete workflow successful: '{app_name}' with '{proton_version}'") return True, app_id def list_shortcuts(self) -> Dict[str, str]: @@ -388,12 +388,12 @@ class NativeSteamService: # Write back if self.write_shortcuts_vdf(data): - logger.info(f"✅ Removed shortcut '{app_name}'") + logger.info(f"Removed shortcut '{app_name}'") return True else: - logger.error("❌ Failed to write updated shortcuts") + logger.error("Failed to write updated shortcuts") return False except Exception as e: - logger.error(f"❌ Error removing shortcut: {e}") + logger.error(f"Error removing shortcut: {e}") return False \ No newline at end of file diff --git a/jackify/backend/services/update_service.py b/jackify/backend/services/update_service.py index 3fada9c..4ab93b9 100644 --- a/jackify/backend/services/update_service.py +++ b/jackify/backend/services/update_service.py @@ -33,6 +33,7 @@ class UpdateInfo: download_url: str file_size: Optional[int] = None is_critical: bool = False + is_delta_update: bool = False class UpdateService: @@ -72,24 +73,44 @@ class UpdateService: latest_version = release_data['tag_name'].lstrip('v') if self._is_newer_version(latest_version): - # Find AppImage asset + # Check if this version was skipped + if self._is_version_skipped(latest_version): + logger.debug(f"Version {latest_version} was skipped by user") + return None + + # Find AppImage asset (prefer delta update if available) download_url = None file_size = None + # Look for delta update first (smaller download) for asset in release_data.get('assets', []): - if asset['name'].endswith('.AppImage'): + if asset['name'].endswith('.AppImage.delta') or 'delta' in asset['name'].lower(): download_url = asset['browser_download_url'] file_size = asset['size'] + logger.debug(f"Found delta update: {asset['name']} ({file_size} bytes)") break + # Fallback to full AppImage if no delta available + if not download_url: + for asset in release_data.get('assets', []): + if asset['name'].endswith('.AppImage'): + download_url = asset['browser_download_url'] + file_size = asset['size'] + logger.debug(f"Found full AppImage: {asset['name']} ({file_size} bytes)") + break + if download_url: + # Determine if this is a delta update + is_delta = '.delta' in download_url or 'delta' in download_url.lower() + return UpdateInfo( version=latest_version, tag_name=release_data['tag_name'], release_date=release_data['published_at'], changelog=release_data.get('body', ''), download_url=download_url, - file_size=file_size + file_size=file_size, + is_delta_update=is_delta ) else: logger.warning(f"No AppImage found in release {latest_version}") @@ -123,6 +144,25 @@ class UpdateService: logger.warning(f"Could not parse version: {version}") return False + def _is_version_skipped(self, version: str) -> bool: + """ + Check if a version was skipped by the user. + + Args: + version: Version to check + + Returns: + bool: True if version was skipped, False otherwise + """ + try: + from ...backend.handlers.config_handler import ConfigHandler + config_handler = ConfigHandler() + skipped_versions = config_handler.get('skipped_versions', []) + return version in skipped_versions + except Exception as e: + logger.warning(f"Error checking skipped versions: {e}") + return False + def check_for_updates_async(self, callback: Callable[[Optional[UpdateInfo]], None]) -> None: """ Check for updates in background thread. @@ -152,16 +192,25 @@ class UpdateService: logger.debug("Not running as AppImage - updates not supported") return False + appimage_path = get_appimage_path() + if not appimage_path: + logger.debug("AppImage path validation failed - updates not supported") + return False + if not can_self_update(): logger.debug("Cannot write to AppImage - updates not possible") return False + logger.debug(f"Self-updating enabled for AppImage: {appimage_path}") return True def download_update(self, update_info: UpdateInfo, progress_callback: Optional[Callable[[int, int], None]] = None) -> Optional[Path]: """ - Download update to temporary location. + Download update using full AppImage replacement. + + Since we can't rely on external tools being available, we use a reliable + full replacement approach that works on all systems without dependencies. Args: update_info: Information about the update to download @@ -171,7 +220,27 @@ class UpdateService: Path to downloaded file, or None if download failed """ try: - logger.info(f"Downloading update {update_info.version} from {update_info.download_url}") + logger.info(f"Downloading update {update_info.version} (full replacement)") + return self._download_update_manual(update_info, progress_callback) + + except Exception as e: + logger.error(f"Failed to download update: {e}") + return None + + def _download_update_manual(self, update_info: UpdateInfo, + progress_callback: Optional[Callable[[int, int], None]] = None) -> Optional[Path]: + """ + Fallback manual download method. + + Args: + update_info: Information about the update to download + progress_callback: Optional callback for download progress + + Returns: + Path to downloaded file, or None if download failed + """ + try: + logger.info(f"Manual download of update {update_info.version} from {update_info.download_url}") response = requests.get(update_info.download_url, stream=True) response.raise_for_status() @@ -179,11 +248,12 @@ class UpdateService: total_size = int(response.headers.get('content-length', 0)) downloaded_size = 0 - # Create temporary file - temp_dir = Path(tempfile.gettempdir()) / "jackify_updates" - temp_dir.mkdir(exist_ok=True) + # Create update directory in user's home directory + home_dir = Path.home() + update_dir = home_dir / "Jackify" / "updates" + update_dir.mkdir(parents=True, exist_ok=True) - temp_file = temp_dir / f"Jackify-{update_info.version}.AppImage" + temp_file = update_dir / f"Jackify-{update_info.version}.AppImage" with open(temp_file, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): @@ -197,11 +267,11 @@ class UpdateService: # Make executable temp_file.chmod(0o755) - logger.info(f"Update downloaded successfully to {temp_file}") + logger.info(f"Manual update downloaded successfully to {temp_file}") return temp_file except Exception as e: - logger.error(f"Failed to download update: {e}") + logger.error(f"Failed to download update manually: {e}") return None def apply_update(self, new_appimage_path: Path) -> bool: @@ -252,10 +322,12 @@ class UpdateService: Path to helper script, or None if creation failed """ try: - temp_dir = Path(tempfile.gettempdir()) / "jackify_updates" - temp_dir.mkdir(exist_ok=True) + # Create update directory in user's home directory + home_dir = Path.home() + update_dir = home_dir / "Jackify" / "updates" + update_dir.mkdir(parents=True, exist_ok=True) - helper_script = temp_dir / "update_helper.sh" + helper_script = update_dir / "update_helper.sh" script_content = f'''#!/bin/bash # Jackify Update Helper Script diff --git a/jackify/engine/Wabbajack.CLI.Builder.dll b/jackify/engine/Wabbajack.CLI.Builder.dll index 141ad96366b471488a56c7a013a9518fecc43a08..f24ff8da9c2f1ac17ab8345e924a211f176625e3 100644 GIT binary patch delta 235 zcmZq3X~>z-!7}UO&kGxS9MlAQ558D2(QU0mdzr{)V@CoFNfNrvZ5>Kpsf7F;FBKsNM`HXAEQ+0%g*G`arU#o7Wl# GGXVg+ct=zK delta 235 zcmZq3X~>z-!Lp@u$*zq(4r&5_`k1;leHM3J&M#?D@+trK<~wS$ST)iN43bPO49yKJ z3``R(Ow3KvjLnP;6HQHw4UCgh43mwFl2a2c%u*-M)0xI1U-RJdWDDH@fkWRV|CTjX zI5PDkf2{`6r*d)4j6iJLX_%vk-?7z}_giNTb? zg29l%9LTl+VpE1hAZ-HVn=qs?7&DkL7y)Gyf#N1Wo&gXiGo%1Tl7ZqzKr$7q-i#r2 J^IGFzCIFBqPv!ss diff --git a/jackify/engine/Wabbajack.Common.dll b/jackify/engine/Wabbajack.Common.dll index 3dba4a4f554b5b29e8d92c72a08c88616e1e8482..c763518b1d5a0fa62f1bd139ec9c654a3df29b23 100644 GIT binary patch delta 261 zcmZpe!P78-H^FgaEjXkYBj9Yt{UMvw1sBv3nr+(34b9>y8i}NKmZ*N)3q;9Wg znrvcbX<}wEX=ZMmY?zW}U}9>@z+leEU^IQsQzp&r zOi!8o1X!Fo8*fby`N|X^@U-h<$?XZhyr%Q4f5Ecg^zP|fzA~jLKt*rCL_z95GP)$S zDQ$~*#G?FayVWI2D|Zg1seu4V!NwB1$1 delta 261 zcmZpe!P78-{dS$@%#-7$5#;rX}FO~?XbJY5rx%9zjMRoS9=DJHWx3?^1Qn%Mj zGcZUpwJe&#|A2w?E-#;_%Wn zO*S#JG%+(aNHt0}Pfjv1PfSWnPBAe}Hcm{oG&46&HcUw~Fflb{U|?ZnnC=(GWU}2i zj4520<>xV--svkcnF0h}omrKCLwJeD!lVTo-~VQqHJu}iDN6w=ssR%PsbA#iI-`H@ zn>XQM&zEkWl*P1z#ov@6nZbm?jKLC28Z#I$q%s%*#ms>?2`FaHkO-vHfV>nS52V@{ dD3T0RZw8bz2C@u+GHF14AX(GxGxM3!nE)m*R*(Py delta 260 zcmZp8!rAbIb3zA8`3{MjjXkYBj9Yt{Jh%nYV&7=2=A7xXv-oR1&(C_3?N7LwIJ|Vz z3=EP?Eey>KEDTH&ElkW!(u~cF3=>UFj17#FQw)=hjFM9mEzD9G7+4q?ru&64nQZqB zV+t2$S#fp$#_20EnF0hdOwH51oD{vcO#QWtb@%(O=^R;1Sqe~5X_zQbz2Q#>#hA+* zj&_=KRNdGt}0T3oJm@-%}7&4dx*%m--%8&@8O@Mq8hBO9a1~UdDpll*g h+yuxo0ODkZ6re~lP}~Serh?U*F{Eyvna`BY1OR7kPKy8l diff --git a/jackify/engine/Wabbajack.Compression.BSA.dll b/jackify/engine/Wabbajack.Compression.BSA.dll index 5c900cc87b3316026f68e97cd3a4895ab4c3bd04..5b490b2f992195b2add41adcabee227b98699145 100644 GIT binary patch delta 247 zcmZp8!rJhJbwUS=?A@-8#-6P`jAyI`6Q80{1_Op{H_ zEKSUe4N{Gg&6AT%%oCH+l2c4flZ_LTEzQi0lMPeS3`|T-x1Wn;{Kdgy`TFa->1Way z0|buGJFD1v=$6asy?&-O7q=;yte*7?}oX?k(ys;%oDE!m!7!)T|dk!E0! zWNKk(ZeU?xnrLBSZjxqfW@MOXYGQ0)oSb5qY-E(2nrLB`y8T=%<1Y@D|M6@ur=Lk< z3=o*XJ4dHlOUSEdgKvtJ(1f_@I_ZoR3Q*B3AE+o$edhNy#yvd0jxVXX5pj9@k#xpG zto~^X20)m^V9H>@V8~z&WLp5SDMKQVHUaWY7}6Mw8O#`rfU=1|aT6fV0Em+rQh*}K VKyf1=nF>~K#*n)GYA&NT3jn^zON0Ob diff --git a/jackify/engine/Wabbajack.Compression.Zip.dll b/jackify/engine/Wabbajack.Compression.Zip.dll index 570a92dad90fd8c4d8707426d2af7a4f66db472b..90ad292e78bc9bad748b161a870564ecea858622 100644 GIT binary patch delta 238 zcmZpe!q_l{aY6^n!PwW=H}-f~2qc*ccMDzS@m}8?KIiYz=iHm0SR7#0FikcwvotX? zHb^x}Hcw76F;7fNOHMH{O*T$Uwlp(0PBu(QGcYkV-F(LG3?qwLg4N;4a~uK$Z2H5C zJS0usr)GQql{C}UoBYS2LIEoHT@)$^QvLb$^q|AXj6c};mmJ_|s(Bx4R>`_#mj`D2UgNWQisL6{|1<^zAWUK~ zWw2l{WH1M^Er8gRArVNM0Qn{iX$-~;W(-C^*+ihY36N(1#K{aPK#^pixDk*{1* LNZowZRhtC>SKCT_ diff --git a/jackify/engine/Wabbajack.Configuration.dll b/jackify/engine/Wabbajack.Configuration.dll index 23d2d7186ee5cc7e69dda413a6cc27d55aa83147..f64e986146891f116720af1ba7123b7bd7f6bcf7 100644 GIT binary patch delta 235 zcmZqBXwaC@!Qz&x{&{1M3A@0tYQ`6A!K}W$z1fztmI-=qzQE4Ps9~CHVrFS#W^9mZ zlx&`yWMZC}l$M-gVw!B6m~3fgZk%kGl4f9HYC8EZhd7HoSp7{Ik{L`G%or@e zq%ng5Ln?z2P|O^NlYnC842eKG4aiFY@<6JMfg;I3^=3dhV<5{AD3b=%2a+}2e30Ls F1pww}M+N`@ delta 235 zcmZqBXwaC@!SY0O&9jX?ChP*2JA5}qS>E)Uv`?q-Z^5lKn=i1lGHRq57$lim7@8Yc z7?>tnn3$WS8JigyCYqWU8yF|27$zGTC8s7@n59ns%OTFP{>!B^lO;F<1oDLv1I0S* zotyZhPo#_e**m#{vqS+ZxY-FR2vS|m8Zzhh1NlyS*V?|#JX~q4{%H&bK$yf}%3#4@ z$Y2g+TL7^sLn4qi0rE{4(in^x%ovP-vWY-(6Clq3h?5yofFj92aU&p^3RZ8%kh=LG HzdZ{8#PCKC diff --git a/jackify/engine/Wabbajack.DTOs.dll b/jackify/engine/Wabbajack.DTOs.dll index 123e7ed87f83516be7f653e57bee713abeaafce4..fe84a4a25956470eacd8d803e836ff52b80d2939 100644 GIT binary patch delta 247 zcmZp;!O?JoV?qau#P91{8hf_(Fy?#_m{8;se}09rQ@iK4@G2jlm)kkMGBU|(m?oQ; zS(=y`8>AW~nJwmDbavMN=|_~9 z0tBW{Hn_;UR@-fziOR~Y=TomuS5an4QhmjdL0R2u_Dl7Z^YfO5t_mLX6k Q4X6(!Yr4Hqlc}Ex0Ev!IT>t<8 delta 247 zcmZp;!O?JoV?qauzPsVn#-6P`j5%KfrsnuQZl9Uuv8jBCo1wm#+IEhwj7+i`X$A&K zrWS_g1{MaUi54d2CTYfIMuv%|CdLNF$ti}(Mn=i0i56z5+l_>n`UF@?4&?VvKcd7G zAW(Uo+vP>s$um{ zV=w^1BnDFk3kE|5b0FISh)o$1fwT#bZ^DqqV9a2~U<8y+1d5vgc?Lk7%#Z>UNd}4= S0m)RbdNYR9?S-05{Y(IP>PaU6 diff --git a/jackify/engine/Wabbajack.Downloaders.Bethesda.dll b/jackify/engine/Wabbajack.Downloaders.Bethesda.dll index e49890a00d7710c9da8b7e8c1f5af1cb2ebc9bb8..7225b3d67183aa9e5d36b4615378b2e565a18ea2 100644 GIT binary patch delta 238 zcmZpez}PT>aY6^nBWwMm8+$s81T+s$WA9$!?c2xlw@**x`Q^H=h(C8ZQ2VWms?)lEN8cb)!&pMnZbm? zjKLC28Z#I$q%s%*#ms>?2`FaHkO-vHfV>nS52V@{D3T0RZw8bz2C@u+GHF14AX(GR If-diw0sHevu>b%7 delta 238 zcmZpez}PT>aY6^nr-(Iu8+$s81Xz!)GSiKyc3Ht*rtyuxoPD#3@kVxyGy{VqQwu|L z0}BJwL<Z) z`)*D5Ygo5)iRd98lgU%!+Z3RJ>lmPdAl2s$ce%dZClj@SMd;IJtAshM{-zAc3?>X_ z43=Qhn8AP{mB9!oW)8$jKrwTML?E38b+bdv2PT&5Ci1@~o5TkQEEWn` zdg;8n$G%g#=O&!oEi-vae47GPFybFn5U9H9PpE;Otk385wuaFcHd`gkVf9aAFaW|N z22%zL215pOAlm|nO&Jn_vAW~n&f+|fdbOyQ!l-I zeb9e)iEL#{o^Q_N2c}I5P{ID&P(h&LpzZ5jzy7Uw;jXwwY4_$PGY3|GQ-)*)69zK| zOE787V8D>dU<4F12jV24m^nitkWK^gQh+>=YGa^CGElu4P|g_0G6c$`0ri1oO*aeL Hyk`agplVIc delta 236 zcmZo@U~Fh$oY28ymCDz#u}4H#z$WR?x}`FC&daBV&fy8@n6Y_-?lU%xGy{VqQwu|L z0}BJwL<v{c8rgwY;H1hVD(R9FaW|N22%zL z215pOAlm|nO&Jn_vAW~nyukO-trfP52%GzMb^GX^7|Y$8zH1jsW0;$(&tphz-M+z3dfg4LTb Lq;6Jqy3Gs#c7jXm diff --git a/jackify/engine/Wabbajack.Downloaders.Http.dll b/jackify/engine/Wabbajack.Downloaders.Http.dll index c31bfc88c6f08cecb1845b0e4cec62a46f659b2b..afad480a91837ff4e53267fe76dc7134c885178a 100644 GIT binary patch delta 245 zcmZpuX{edd!E)F0<(rK?KH36x{j2AQFOKrtvRYTt>Hq8Bn_p<(WYaTEHZij_F*7zu zHA*&5PBJl1OiD{mF)>XxPE58mGdE5)Oi42^F*Rjiuw!JfoLpe6y;;h*j*-Q3rdjLc zIVJ)8k;gv2*>V4t`%)_(xvxxRlYg63D?r7bt3t#KL5h#a6-Ioux~%UvJ$L5jX{IbJ z{-zAc3?>X_43=Qhn8AP{mB9!oW)8$jKrwTML?E38tnn3$WS8JigyCYqWU8yF|27$zGTC8s7@n58l>*fBC#PA)Lk-YjKY$H>BXGV<8u zIVJ)8>!)v!jhXtzW-wzg0?H-=#Z7=b10YUjNCAo@ U1I3MiWGYy_8AIykmzLrz0HB{tW&i*H diff --git a/jackify/engine/Wabbajack.Downloaders.IPS4OAuth2Downloader.dll b/jackify/engine/Wabbajack.Downloaders.IPS4OAuth2Downloader.dll index 3bf3d99cb4cd2ca8849a570a32bf294de7e0d3ae..b5a18816dbd10c16fa7a234dc0ef5bf428be1dd8 100644 GIT binary patch delta 238 zcmZpe!qhN@X+j4}RQ-zG8+(rA2~@`!#6IvY^ITaTWM!=Rw{>$u{t|8t(_|AfOA|9= zgH)qr^W-EG^TedI` z>i6N!+ph0ha(_+txyd{YvlO6$k70s9)!v$J$1iU1(~4q$cY4<5V+|#&{-zAc3?>X_ z43=Qhn8AP{mB9!oW)8$jKrwTML?E38$2grS>i@3o3zad`AfJp(hLleOf3w} z4J-^y6D>^4P120bj0_V^O^gkUlT!?njf|306D`bAH|tc_u&{Wh2*01aranMmvCsCB zNoP2`7XFV?z4}Dt#blm_Sqf0WTMwavK-Kxh=l?J;Y>T-n^x?ni=3@;dto~^X20)m^ zV9H>@V8~z&WLp5SDMKQVHUaWY7}6Mw8O#`rfU=1|aT6fV0Em+rQh*}KKyf1=nF>~K M#*n(Xv#p#N014hpS^xk5 diff --git a/jackify/engine/Wabbajack.Downloaders.Interfaces.dll b/jackify/engine/Wabbajack.Downloaders.Interfaces.dll index 32fe92199492071fe7cb8016c887f11b53e39193..9c9eb914eefe5cfd8008b904f33e6ad2055411da 100644 GIT binary patch delta 251 zcmZp$Xt0>j!LoFp^0kdU6ZiyNvS&>&+QaCuuUX%zI>2TAW*vTJCP&j`6EjN_Gh>5P zqh#~sBop(*q_pG|6Vqhl#AHh|bK_*glr#eqQ&R>8Rz?OU1_lmB25trzaBySTIk`YE zK;U7kV&mIMW-gtwzPUUZ_t++15NuO`3Z|Td3IbJIPQSkQzPsx4sc+^_Z=wjDTY1K%4{=GiOKy(rG|m3Xlg1I`l+spvESVIc{ delta 251 zcmZp$Xt0>j!LlkjZRf_G348*33q#pBCGNTIW3z1izChr?W*vTJCdV`bgCtW6LvsTQ z1Jgtc6LXU^V>2VeL{k%E1LNcr!(=0)Ut3+KPQu79dx@|MT7$G2`S6G~w9 zPh&6u!XyS$1`7s526G_W0*Fl+5`nY{kZ;0}#$e1~#$W`LO$3UY0C@&LoXn5{6iEh( S8v)5wuzE9w)Xl0Ax0wMRlDTCCOdP^W*f1;Od6)iCT5l8)5Y`GRDCK$YUjgR^W5 zT-q``>=p!ta85Rms#Aapim*WiL8?E!&bS^FdopX+K3AK~=cFF6_?t2$Gng=#F<630 zV+I3;R0boUm^lz90maN25`lCYke34Ffm9mn+a delta 235 zcmZqhXz-ZO!P0*6$bpSL3q%DbH86X9k12F%e{t$;*u~t|%{F3xnKaT243bPO49yKJ z3``R(Ow3KvjLnP;6HQHw4UCgh43mwFl2a2c%u**ON@lY>n9$ZX`GRDCz_r&~3oo$5 zI8EHz(#$5+!#vqQs!jnam?Q`l1gTcmt!lJix752WrETx#b5ajj{L>f=fG~-{l)-|* zkii_twg6&NhD0E30_2-8q%jyXm@yauWfOtoCP1D65GOOF07a64;zmF+6|CNjA$9X# Hd1e*>gXBoz diff --git a/jackify/engine/Wabbajack.Downloaders.MediaFire.dll b/jackify/engine/Wabbajack.Downloaders.MediaFire.dll index 289333d18e679f5437fe73a41476df81250bbd6c..fe0dac637df0f1a494c22e9649efc8f2aef50119 100644 GIT binary patch delta 254 zcmZpuX{edd!Lr6??Y50QKH376>N>lR+gJGQ?dk<1#z2-KP$mti4GB@Nn_p;uWz$PDFi0}BFf=!? zFfdKDFflhtGd43aOf)qyHZV?3F-$fxN={9*FiT}%2w-IJp8QW;Z1Q?V&dJrra+^Jj z4H#LfQ>)fbt}zJ^*f~LCmt*>B$FAf{SC27$*f9ByNs9th@FPqRq*|dy+m%n@weOia z(L0+POr2Q#(-;hZFp0sG!Ggh%!5ql80Af>yL?CSfAW~nJw?~%x zdr?`Z)u!_E*iDWfp8UtGN&zZZ1QP_R&Rsio{s!+{{s(T~IcIF1X3oOmZ_1F&V8UR= zUL H*5WJx`V~Z} delta 238 zcmZo@U~Fh$oY2AY%i-|yjXf2*0z9)kQV(iWyDV@$Tot6bF>tep-a|HxGy{VqQwu|L z0}BJwL<&ND4<}57!X$%HHn8aYp zV8LL>U=Czk0I?}UB9Jx#@=X}h7>pUr7>t0ji9m4^AkP4ZlNnNgBFR8;BOsXyR&U0T Ky7{HGI12!%F+@cG diff --git a/jackify/engine/Wabbajack.Downloaders.ModDB.dll b/jackify/engine/Wabbajack.Downloaders.ModDB.dll index 56b5a97bfedfd9d52591b38914a8b2caec8070e9..b16a7ff1d78fe853cf1b4497f3a8df115b4b3c70 100644 GIT binary patch delta 256 zcmZpe!Pqc^aY6^n>^-c@H})u)39vuBoPI4?&2?_W1lcWo$1*qXF{|g)Gfg%zvotX? zHb^x}Hcw76F;7fNOHMH{O*T$Uwlp(0PBu(QGcYkVWnl1NWN@B5(Me`9zoX#f`wkqN z=Q&t0vdodQ?3#SXF+d=be=58BjZpW=vkLn;k63C=wsERafC^rR2?AAf-TYg({iIsa znc%Zeer>+tbcn^@lp&eHgu#r#5=H})u)33OFmlU|y2&}&oqBZi95i=3PHnALOYr5P9`nOYc{ z8(0{aCR&)7o1__=85t&;niv}xC#M)D8yO|1CR&)KGB9{BGB{73=p-|l-%)V#eFu)s z^Bk-gSvb?XzfHd57$C4DLdI#njj!)!!vg#Bu9qiFwsERafC|dO1c9o{Hry;*zV64A z6*?i!wwrG_9b)lMV=w^1BnDFk3kE|5b0FISh)o$1fwT#bZ^DqqV9a2~U<8y+1d5vg dc?Lk7%#Z>UNd}4=0m)RbdNYR9%`e@>SpfS}O%ebA diff --git a/jackify/engine/Wabbajack.Downloaders.Nexus.dll b/jackify/engine/Wabbajack.Downloaders.Nexus.dll index db535b905fb6a224e750a34ba481eba254ba3d73..169b90e4143e40456bb7d3ead7e1bc6e1c225ccc 100644 GIT binary patch delta 250 zcmZpe!`Lu~aY6@+t+DjZjXfEb0;g=b&n{wab=>~i^AJy;ZpLO7tK}SerpYE|mL_J# z2B}8L=E+GW=7~vZ$tfnL$;OGvmS*P0$%ZLu1}3Ja3=CF`45pJE^rR>2JBv)_aQ?!g zVU@aRl1qSq!q0aCGrOOAEvc}7W^v>|+2jP58U?7}uN0^tP_Yrig(lCyMy-$APnb7v z{^K%_#ov@6nZbm?jKLC28Z#I$q%s%*#ms>?2`FaHkO-vHfV>nS52V@{D3T0RZw8bz U2C@u+GHF14AX(GRFFnOs0NHI!hyVZp delta 250 zcmZpe!`Lu~aY6@6gz2k^8+$S=1=0_@Mp~xqa@^Ok!(l(iza^Vltd?`=r5P9`nOYc{ z8(0{aCR&)7o1__=85t&;niv}xC#M)D8yO|1CR&)KGB8*%GMG+w(3765?<_Kz!}$wK zl>66~NiG2b?SGE>X31$gclaqQ`W`whI61+kMgc0=3=;$?eybbc#get_>e0Nevdw>7 z=CSyvF&F@05`!s&1%n}jIgo7u#HI|1K-vVzH(^L)FlI1gFapXZ0>w>$JOdz3W=H{w VBm>2bfMhCIy%|I5=9ix0EC3B^NumG% diff --git a/jackify/engine/Wabbajack.Downloaders.VerificationCache.dll b/jackify/engine/Wabbajack.Downloaders.VerificationCache.dll index e9ccb61d95cb229433d0f00b0eec0632fbad14ee..16907801396394e5972a26f97bc641124a8cf552 100644 GIT binary patch delta 236 zcmZq3X~>z-!E!i7{^`aZ8C3zX!sD47^1Oa?KKBV#dPr{ByhBxoMZ+}N#LUvf%-A5+ zDA_zY$;3P{DJ?n0#5CDBG1=10+&I}VCC$LZ)O0hCRtzIcVx9BF$vN5q0#2nuXPtK5 zabG#r=dfe&_1BZnXirgq3NF`#3IbIJ{`0@>bMyU7=K1*xzHV;NnZ)XE%8<-p!eGW= z2_}sh3>Z=wjDTY1K%4{=GiOKy(rG|m3XlgE=YE G`OE-NWJ!Dg delta 236 zcmZq3X~>z-!BV}>_u$4J8C3zNyw2PwrG}2pE4W`wj(JzGd55YFi$=yo}8l{ARs(j=(OX} zP{*a}(R;)j)7DHrqdi3dDtJ&8DhN_twD@?@+B-2W^QLW$+uWctiPb-i!2k%87)%)~ z7z`QAfouyPHf2Zz(k4K@2}2r#F@qU{5l}V}C~gAe831uILkdtN87OW9BvZla%@|TQ JCmPLX1^~_`NcR8$ diff --git a/jackify/engine/Wabbajack.Downloaders.WabbajackCDN.dll b/jackify/engine/Wabbajack.Downloaders.WabbajackCDN.dll index 353cf7425270fe32de4985e0491899e4e14005a1..b7c3ad9d3f5471f8dd01de459b750cfd5769c4d1 100644 GIT binary patch delta 248 zcmZoTz}RqraY6^nsxNcrZ|rGs7kJWS(eK6)=(4nOMf(@!k20HOJQi~4nI@ZAW~nGT2WJ3=!MR7%a@p;+cPG z#$<`G0D)V1X<99!*$y4?<)<@jTK`P02wQHuf~Q3-H%6DTF+UcWJB17hswpwR5wK$3iZ>Gy{VqQwu|L z0}BJwL<K2K&i@A!3^ugN2z{T#b2m zPL>D@5V*-P?WtwPERU|g%T}iF9X~s{BCJaRD%jx#6$GlbENHCMP2+RC-T8al>di{w zCs_Q`7z}_giNTb?g29l%9LTl+VpE1hAZ-HVn=qs?7&DkL7y)Gyf#N1Wo&gXiGo%1T Vl7ZqzKr$7q-i#r2vt`V2W&qG9NVNa} diff --git a/jackify/engine/Wabbajack.FileExtractor.dll b/jackify/engine/Wabbajack.FileExtractor.dll index 7164ed14fb05b406fe5df7682e8edb06ddcdb9fc..ac3cc158c6d01f5ced2e241bc6a86a318ff91298 100644 GIT binary patch delta 262 zcmZoT!qRYrWkLtb(gO?jZ0z~-Md0-X@0OQmN_>|3RNpvl^L*Ln8DD2f>6s>*m|2>b z85^V;C7UNFnV2Ugr6s4Bm?j%1CR>`B8z&p4q#2l)nldoBFf!OrXB1^rnmjdL0 gR2u_Dl7Z^YfO5t_mLX6k4X6(!Yr6fQETcUO0PZ+c1poj5 delta 262 zcmZoT!qRYrWkLtbm4U&9k->gCqbQ@=G$BU8$^Kh8 zw=WQ2bYNpSej~SK`V%q60D+>zw%tKT6J7WBY258xdT0A|7jecC1*j-1OcbO(f4jGH zf~J1YVT-vBx8D$FoW<&&#$W)1Nere877T_A=0LUu5Sub20%;Q<--IEJ!I;5}!3Zdu j2oyH~@(h4DnIQ!zk_;3#0+Oj<^=1sI+Yibz+Oq%v#(7Zw diff --git a/jackify/engine/Wabbajack.Hashing.PHash.dll b/jackify/engine/Wabbajack.Hashing.PHash.dll index 0545efe62707907ef9c67ba2d40c8118f98fe5d5..440f92c3c9090b585a25714a53ba6d8d3cf0a999 100644 GIT binary patch delta 18789 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 Y)I(k=r2}^FzvoOvIWly&*z%S5f96+9umAu6 delta 17704 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 diff --git a/jackify/engine/Wabbajack.Hashing.xxHash64.dll b/jackify/engine/Wabbajack.Hashing.xxHash64.dll index 10a0184a1c5ec2cb55130f295c425a5871e28a7d..6d48caad0280a8f362fe7a26e3e7a63a8ca0e8b6 100644 GIT binary patch delta 238 zcmZoz!Pu~ZaY6@+{uK338+&Bz1(dCK=Ifo<;XXsNPwd~KHPbimu>ZxOVVZ1WW@%z( zY>;Y{Y@VECVxE|kmYiZ@nrxhyY-wh0oNSnqW?*7!y7`vJG)9)DL%#bamv{vTgfj9m z-M;+LrTNM(_Wm{2t&^{KRVkng0#(}yzx?LbbpC_*tK3WbHs^Wku=txYBr}*Wm@!y_ zNn-{BhExV4pqM!jCjrIG84`hX8jzO)dk<1#z2-KP$mti4EDZg8`$FASJ!~PeCMw)>^lBtEE zxq*d&X`+RRxk;L_nUP_lsfn?HadL`bvXN18YNCZ%>gHP>(->K1<^*1tT;dfVu+>m~ z*5f=fG~-{ zl)-|*kii_twg6&NhD0E30_2-8q%jyXm@yauWfOtoCP1D65GOOF07a64;zmF+6|CNj LA$9Xhe{mK7eCkad diff --git a/jackify/engine/Wabbajack.IO.Async.dll b/jackify/engine/Wabbajack.IO.Async.dll index e8f1d72470e2c99d715e215b1e99775598b3d5bb..30039f684e621d67afc754fa60dbc52071e868b7 100644 GIT binary patch delta 236 zcmZpuXsDRb!IB?6Y0k!;8=3-%e&1KCy%cp{ZMEv&e2=z@%_UkntQw}tCT5l4o?dLA^Xs>}cm=P{+I-JAht)rg!2k%87)%)~ z7z`QAfouyPHf2Zz(k4K@2}2r#F@qU{5l}V}C~gAe831uILkdtN87OW9BvZla%@|TQ J&$LWu0s!*9NUs0@ diff --git a/jackify/engine/Wabbajack.Installer.dll b/jackify/engine/Wabbajack.Installer.dll index df52c15633e5a3fe5fad5b3ce2faed792ce34075..509205fe7309c18f9ec6f6f1a50c8a13b9334f8a 100644 GIT binary patch delta 27101 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 Zm$ik>t9FW7>Bhe8hWEq|VtKCi{{xP}+pGWp delta 25835 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

9HtOt9XYl0pYzbxvD4(CR}4K^}*8CRfBa-(4m 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


<19JO)9X0PPYOC?aaz!5oEGtpM(GT; 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 diff --git a/jackify/engine/Wabbajack.Networking.BethesdaNet.dll b/jackify/engine/Wabbajack.Networking.BethesdaNet.dll index e02375e6a9dab2ac1f88f913729fefa55fb4b2f9..cc398bbd3821703ba6ca7e3d72f0b51b85d2dca9 100644 GIT binary patch delta 248 zcmZqJ!PKyWX+j5!X_43=Qhn8AP{mB9!oW)8$jKrwTML?E38FJ$&HuhYp6$nu-P;-yA^Xm*dp0;C^Gw$23>5^bmYy-o_Po6D+@^Im!dGqH zHBpAmKaIfv2$L9087vqK8O(uf3m`USNCeU*K)wk>8iO%|8G{i}HW4Up0^}J0aWX>+ XP$U^BZUiJ#!RpNzQa7v4xXlazFb7bp diff --git a/jackify/engine/Wabbajack.Networking.Discord.dll b/jackify/engine/Wabbajack.Networking.Discord.dll index 15b2a46f7fd9a98594803e946343ff0d16dfa191..2c7f155890658bcdb6cd9f616fad288600635a20 100644 GIT binary patch delta 246 zcmZoDXegM_!4i|Tv}t1xhnfKIk2elx3=s}{w_Kiolk@SD%}dlWS@cYkP0TD!%#00E zjgrlilT6GLlhTq?OiYuF6O%2?%#D)`Q_>7fOidXW92psGCM&9lZVu4aVPdfqlWd&a zpc^3Y<^Gx>pTodQ(Q`Y%)vsG4!td-KU*XArVNY0eLAv9!Rw@P$U_s-V7*b3}hJs RWzvB9K(eNr{~9y1003GXOCtaP delta 246 zcmZoDXegM_!BTg^_}|7J4mANCy8~IDUfB4pTx^nLq7mjvOf0eb-*!%J z&FcWXTa*8 z#$W)1Nere877T_A=0LUu5Sub20%;Q<--IEJ!I;5}!3Zdu2oyH~@(h4DnIQ!zk_;3# S0+Oj<^=1sIoBtX!vj6~&97<{c diff --git a/jackify/engine/Wabbajack.Networking.GitHub.dll b/jackify/engine/Wabbajack.Networking.GitHub.dll index eaed6906aa4ac41039ac160793c32e86d9642c02..0e8a74c1324864da72fc8822ed7ecfd5643c2db7 100644 GIT binary patch delta 242 zcmZoz!Pu~ZaY6?Rm)EmL8+&-{1RUNRE~sX#b)V(U*XArVNY0eLAv9!Rw@P$U_s-V7*b3}hJsWzvB9 NK(eNrU;2x)002vwPtgDX delta 242 zcmZoz!Pu~ZaY6^nBkty<8+&-{1pF1Rq?P!n`Yzl!=}w{{)7H%^?8?|R(hLleOf3w} z4J-^y6D>^4P120bj0_V^O^gkUlT!?njf|306D`bAC%<(w-CXVVmYHSON439`bG!ls zj2TY<4F9&tanjFkKPOyNjGcVOt40ATXea~~1gfqrd1yCt`8D=?B^{L?H)nVovG}Jk z7yw}sgDHargCT=CkZl3PrVNQd+62fqVMt>zW-wzg0?H-=#Z7=b10YUjNCAo@1I3Mi QWGYy_8AIykm;T}`0PaOkz5oCK diff --git a/jackify/engine/Wabbajack.Networking.Http.Interfaces.dll b/jackify/engine/Wabbajack.Networking.Http.Interfaces.dll index 083b854e02cd2909c1c1d885387687302f891b37..40f681dc9b47fa9667736e237510e1d225081b4a 100644 GIT binary patch delta 235 zcmZqBXwaC@!Ll~a=+wp@9S(s{^W|e5cg*qJ@%!H1{tXe0n@@0vGHRG6o0wUem>C^T0E@pVLo$O2gBgP* zm^5ZEU`S;!0*aXfaS~9>oFNfNrvZ5>Kpsf7F;FBKsNM`HXAEQ+0%g*G`arU#n|%c~ GGXntQ+erfe delta 235 zcmZqBXwaC@!4i4PdD6xn9S(sr?{;xWXlOdkif4OpSt4=9<`W#Ej2dYM21%wChUNwq z2BwJ?Cgvt-#%4x_iKZsT2FA%LhRH@o$*GAJW~q}|xzt(y?fcL**?=oR;Qyr=88vms zo%i%|JZfhAHh=O2u092*;O82sAW$_|x}u!XD-@uDyv0yKpz5mpi~09|-EmQ{SRT;2`A*_0R)155WCjxk zGX_gAY0O~2kjh{L6f+0nB%qi%Ln4q)1M*UUJdkQ*phz-My%|u>7|1dN%A^7Hfn-fL JUrpC$0RY!UO^N^j delta 238 zcmZp;!PszvaY6@6$A`(sHukIt6nL|^_UWrd@!nnU%aS?X+-2G9611ONBhA1d$<)Ho z+`z)XG||Gu+$7D|%*Zg&)Wq1pI61{I*~ln4HPON>b+dNdRwkB1D|f7%{3IbjAVkn# zNkKKzt&inimb#PKfypk36$((nZGKQekm};-u6r&^>U%$ke!I8%PU0$7|1<^zAWUK~ zWw2l{WH1M^Er8gRArVNM0Qn{iX$-~;W(-C^*+ihY36N(1#K{aPK#^pixDk*{1* LNZoujU7G~}60=Ik diff --git a/jackify/engine/Wabbajack.Networking.NexusApi.dll b/jackify/engine/Wabbajack.Networking.NexusApi.dll index 2caf9237a44c2f17ab71ec56c088800141cd5935..8be85e1103427173da0072c728c3495ef67a62d1 100644 GIT binary patch delta 242 zcmZp8!`$$Oc|r$^WcY~3n;Y{Y@VECVxE|kmYiZ@nrxhyY-wh0oNSnqW?*7!Iyvu=-sa^G1-V#m2KM}!yyta* zz-FE$8JqT=^X@Yhb};&O|J`JnHw_9Cl-HG zhGYg41~UdrFlo$Sz>vyd1Qas|;v}G$IYT0lP6P5%fIN_DW1vVfP`w#Y&KSrt1j?iV O^?_tfH~;;}%mM&v;Z&~x delta 242 zcmZp8!`$$Oc|r$^(6kL-H}?EFDUii%Ef*8L+->^(3sZNB^ww>jacZZSMw)>^lBtEE zxq*d&X`+RRxk;L_nUP_lsfn?HadL`bvXN18YNCZ%>g2pfdYhL&6y#!It}vZ9dC%(r zfoF+(-*Eoy^V#_8dgAKKb5=~2dDEZ(75wuNDhO09_r&U79&@}+%Y`>R=9~At`NZO% z#$W)1Nere877T_A=0LUu5Sub20%;Q<--IEJ!I;5}!3Zdu2oyH~@(h4DnIQ!zk_;3# S0+Oj<^=1sIoBw`fW&r@Cu2sSS diff --git a/jackify/engine/Wabbajack.Networking.WabbajackClientApi.dll b/jackify/engine/Wabbajack.Networking.WabbajackClientApi.dll index 56bf0df2ea75a8d6c912c8e609e9b05ffb9a7c33..13407483fa1d23774d88aa369431fc5f98730934 100644 GIT binary patch delta 253 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 A*Z=?k delta 5816 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 C2cV8B8btGnARkVr{~b%bT}h zvW<0sK(zGc&RX>hk5*+PZcXj$yC%=EPEmjgO27m`s?V$UMkO(3F-0v4eZ5)1hKbeR zlp&eHgu#r#5=NZnlM*v|w2+m=RS diff --git a/jackify/engine/Wabbajack.RateLimiter.dll b/jackify/engine/Wabbajack.RateLimiter.dll index 172f3db506b70a6f500a1db6c7276f984aec8159..c9d590ad951937992e0ddea96fbd87c6844c2610 100644 GIT binary patch delta 238 zcmZqJ!`QHgaY6^nasHlF8+$su1TJ03nx(=a>9=EERPHyv{~tH2c-L`im?oQ;S(=y` z8>AW~np H(ZNgr$i+*B delta 238 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 KFR=ddjQ;|%{y;+j delta 4998 zcmbW53viQF7RS%cm!!05n$om+ws}&ZVoTCcsG!npDWV`PFL`S|(iRGWLQ4h2O=3W+ zg@yXb!<9#}MX{pmI)N2u*fEg;t1ANwsLU=(YXN6`uFCotS=n<>z9PCaI80=AYIQ7{M2H6;H{}f=l zU zwG~U%1x|R=nv1+rXewYZrnm@s+3YUx!kOa1=)Y?19>7577>@qk`J>VCn#LkqT<-!O zTa9|ZaUxaLA>XvlK&ERl3+JN_WG_J07A=%^*BRMz>6N;Nv@Hb&I18%^M#2y3^^$*z zQSn%Tbn8@$bZAPx)HY>~Vq<|+In`x<3}=nA91VjvR2t-1wT&@2spkGUVXIU;wIQ^- zKn+6&p8++@El^|4c%cikS<3X-tyK9(tcmDsObyemKi1d$LOp|TvfMbfQscgE>_OBe zj@OZvoHKYHab_Kr#g}sQx}taFBA&k9pDXka&c*GNs-_u2{hicsD(@1`hW+Gr4D4CU z(HW>&PP_Cm?$|u|7qV6TJyMa8VKCqd@{x8n7&%3#L>4ITKyFdaL+Vt^kiS)hk!Ms- zAn#D`LH<*H0QqNl-DXh3zMKHA>j&p?)c3khA$J@2W@cI6mA;x5H=i@4m-XWY_Fzqk z8Ya6{$UZ%vWf{|IsG+$P!2Y zC#n>yK{?)!%O9HIuLFmfqc2=&upxQ+6TpoZv;?ue4^>IrvBD z5z~bthTAslTJ$L9TPV834&`(h6_D$sO1R&)O*%j0ye?+czyuffF0l=|z0&c!MulE7 zYT*%g+Iu|*KOi7c6ai0h%BX{GIeO9jSBxeV^U4mB!gk!43yUb)3J2VO$7sJCB{0$h z4Ca-X)1EgPV3Hh(+K-GT*e*xAwUF${ukinc| z%(8^)O+z5JjE3X1YfU#m6-5!~)IV-2g|+D%Y#X@QR0b#IXjta&O&+ig?;rQ9$qOSX zy2Pd#Uo@3NwH(dMJYe#{csVLC9x{!9IdX(2B|SZ>WY|&DCI&T^S zyRJnaOP|jw$^FJO7H;wM&n=jxp|gh;>CDyeA(@xhScBO-4nC73e%woE*9>!-Yv4IA z4TF99Tg(&TTRFO|xW+sQCY7gibBbq6@68t3acS%vt8RsPI;^SS89I8YX^xdWm{>pO zWfnSatcT|&(;&K$_3$T(qR;^A&GpbNMr#=UXCuREtdP?FLLx6xGhrFe9Md*EGxh~CEfZUev+GEs2ts)^jVr= zha8<&-fU@s!l~)Xa#f9GC9IXBxN5Sc6|$$LbHib#w0gd^C}3Fy!gTJXKgFv-Ls1ki z!+p}r^PggC5)<#9s|!i`9B6|r$PEDbd9V?_$j^bzFb4S~OhRsloyZ+<5*dTj$UVqB zs9?NumB9VA!UxE(a1ptVoHlYckh56;z;49t!bkbV_@J%EArOSYA$J*Q;3-QP=wTPq z3VV@mIEowwCy+kSTFYPzlq0KQEOH{;iJV6MT%-@%by;ih%-0&Hkrv@W>=}e5u+1K% z$r^~wl#NhUA{~lT&`G78RFotqMLbOYVe;Rg0X^jR68k9IN7+7ljEl60uc#6jUxdK; zA_T^tSjG4f9pt+if9&EAC924(rqXJ1YKQ^y8%Q@JyR2by)?uT?uJze|A?3ci6y$mt_} zku(#U`IZVit3r*7BVA0on)Em!3h%mV$O)3uKsqe&rANu>M24QSJVS}3&<;BalL9}L z!<6np=e@!na{9;tWji>EK*?(aC7<4b&V5A=a@Loff?NI4-YIvz%!&?V5yoE?kjC7oIin2ZAII?+-Gn*cRv`9KajFA&3oy_LD z*hB2iruz?CZZR#N%t6|z<&8vQSj$^SFuT~5B;89syQowY)ky6Jmno<`9tJ_J=Ak?|;Djh#FKRE%?BIz*c2-aY0(sA;W zq*FS+h;DSctUcuPl5+uPdD;cJ*fV5z@He|Nmrq|s+E02Ma+^Isjz~_Jbc`6!jlxP- zl5{WXhh1zS@8B3n-3Ib(fOMD`p=^wF9J8BTNz$PndUNEpL&WLK<1PF|kr>P4n;g&M z?NWJka^&=qcIfHU^}L;*7$8R^9YL;i#q|9DgP6V@K6J*(Nuq;yj&yH`-XNdOGM^7{ z606AZlNO0#aw4STNPL3yC<9-#ljtXk#0W7(>^1P)BN};Sgcv9G8bjP-Ce9SmY3ABb z6p1M_AKXit;g=GejOZsuh$#y{(G+QBr4B@q7$>qK8cP(5_&tjkMJ;%7n2i!Peu@0Z zO)il;((u-ewQUxFT}I9$2F@1JRm8S|8__wg=YE=xl0HUkaCD#_F6R7Q_7>!Ot}O}8 z14|W5n^?V?l`&u9!M%QEZ6kQ_-vX$BO7Oxc)Rp+E01@9uU_1gE!3QIu9J3<&BQWyd zD+u-I1ko=?HcT63vqZ^?KRzWGW;uOvJz7%8r9i<7D!9t8htPjE>j9YP3De4vIHKUXJ^HtaV-1zm! z825|M0p;cRnC1AurTBgm(umEKX;gs0L| zAy$qUHKNfsvfL|HjPQAU!Fq47ygb+-R*q~)96h!%!@JHAxB~3XIt6~m?ZDS3_!pi} zPVjHM=^ut~_&Xx--{2nl3}4dv6YgB$Z=Zkhz|2WCn|5|zwlvw>P5<^xCUWnZtkI_NDN-v4unYe$FEz|JPtpY$^+=hUs*@yh-*z<6o)sS2i*Hl1o> z&r08&a0pW)ILG)4shmG4Sl?tizM>Uvoa9b^rhX diff --git a/jackify/engine/Wabbajack.VFS.Interfaces.dll b/jackify/engine/Wabbajack.VFS.Interfaces.dll index 0438c90b4339e0a2fc887c994aa3f7372fcbe341..082ac65fd763adf85954af49b71fd90f94e3c915 100644 GIT binary patch delta 234 zcmZqBXwaC@!SZR!w*MP@Lf8ei?VWUX$!ATU9XCIg$%n42nf!tMr-o^=iJ7H|nXy5t zQL=e*l8Jd@Qd)9~iD|NNVzQ-~xpA^#N}7R*sp(`vPGy$Fn>?2%TW|&l2-{j7y0U}O ztI6e3>&+L^j+19_mMK65xnP1I)qal4r~InS_K8c~TD)10D}vSElp&eHgu#r#5=Es!lWeQNidYB+kwf^GmN&&BBI=38*-zTtHk1K-JKaIfv2$L9087vqK z8O(uf3m`USNCeU*K)wk>8iO%|8G{i}HW4Up0^}J0aWX>+P$U^BZUiJ#!RpNzQa4}a H*Jc3#oHRvM diff --git a/jackify/engine/Wabbajack.VFS.dll b/jackify/engine/Wabbajack.VFS.dll index eb34e2b98d5f7e9a6dc235569c138b7c40f2f55a..bbf8192e6f5c8384592a47cc30f7f54e720b2c87 100644 GIT binary patch delta 238 zcmZqp!QAkJc|r$^$_kF#8+!`Q39Rei^~(BHiO;I!{LMX+GLtv+oVS+OFikcwvotX? zHb^x}Hcw76F;7fNOHMH{O*T$Uwlp(0PBu(QGcYkV-JJi9k&UH&2G@njC0_yr;+xx+ z?0PfVas3yOh`P)uD3L%!}}_BUlnW-wte zW3U91#ta4wsSHLyF>@eJ0*aY4Bm(I)ATI^T1F1F!iX;Qon*rsFfhq|Ph09Mi diff --git a/jackify/engine/jackify-engine.dll b/jackify/engine/jackify-engine.dll index 92eb41f3c7358f4c4922027849fe92a6703507ae..6c78cb200f0b4edd7c01aace2567ddc44f5a956e 100644 GIT binary patch delta 265 zcmZpe!QC)}dqM}xM8UFMjXhg?7?*8f2~|oOr{bn za&_Skr&kb-Vp)CQ}I( z`PJrArdJ$h3J{QuTmR+ZhiTrsAE|w9uuW>7e&aAxjsjFv`ZrV*s9wZ!Pj27CgqSIk zE5x2`FFe9DmvM3c&{=%Pn0%Q1(-;hZFp0sG!Ggh%!5ql80Af>yL?CSf Optional[Path]: This uses the APPIMAGE environment variable set by the AppImage runtime. This is the standard, reliable method for AppImage path detection. + For security, this validates that the AppImage is actually Jackify to prevent + accidentally updating other AppImages when running from development environments. + Returns: - Optional[Path]: Path to the AppImage file if running as AppImage, None otherwise + Optional[Path]: Path to the AppImage file if running as Jackify AppImage, None otherwise """ if not is_appimage(): return None appimage_path = os.environ.get('APPIMAGE') if appimage_path and os.path.exists(appimage_path): - return Path(appimage_path) + path = Path(appimage_path) + + # Validate this is actually a Jackify AppImage to prevent updating wrong apps + if 'jackify' in path.name.lower(): + return path + else: + # Running from different AppImage (e.g., development in Cursor.AppImage) + return None return None diff --git a/jackify/shared/paths.py b/jackify/shared/paths.py deleted file mode 100644 index bbe9a8c..0000000 --- a/jackify/shared/paths.py +++ /dev/null @@ -1,958 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Path Handler Module -Handles path-related operations for ModOrganizer.ini and other configuration files -""" - -import os -import re -import logging -import shutil -from pathlib import Path -from typing import Optional, Union, Dict, Any, List, Tuple -from datetime import datetime - -# Initialize logger -logger = logging.getLogger(__name__) - -# --- Configuration (Adapted from Proposal) --- -# Define known script extender executables (lowercase for comparisons) -TARGET_EXECUTABLES_LOWER = ["skse64_loader.exe", "f4se_loader.exe", "nvse_loader.exe", "obse_loader.exe", "falloutnv.exe"] -# Define known stock game folder names (case-sensitive, as they appear on disk) -STOCK_GAME_FOLDERS = ["Stock Game", "Game Root", "Stock Folder", "Skyrim Stock"] -# Define the SD card path prefix on Steam Deck/Linux -SDCARD_PREFIX = '/run/media/mmcblk0p1/' - -class PathHandler: - """ - Handles path-related operations for ModOrganizer.ini and other configuration files - """ - - @staticmethod - def _strip_sdcard_path_prefix(path_obj: Path) -> str: - """ - Removes the '/run/media/mmcblk0p1/' prefix if present. - Returns the path as a POSIX-style string (using /). - """ - path_str = path_obj.as_posix() # Work with consistent forward slashes - if path_str.lower().startswith(SDCARD_PREFIX.lower()): - # Return the part *after* the prefix, ensuring no leading slash remains unless root - relative_part = path_str[len(SDCARD_PREFIX):] - return relative_part if relative_part else "." # Return '.' if it was exactly the prefix - return path_str - - @staticmethod - def update_mo2_ini_paths( - modlist_ini_path: Path, - modlist_dir_path: Path, - modlist_sdcard: bool, - steam_library_common_path: Optional[Path] = None, - basegame_dir_name: Optional[str] = None, - basegame_sdcard: bool = False # Default to False if not provided - ) -> bool: - logger.info(f"[DEBUG] update_mo2_ini_paths called with: modlist_ini_path={modlist_ini_path}, modlist_dir_path={modlist_dir_path}, modlist_sdcard={modlist_sdcard}, steam_library_common_path={steam_library_common_path}, basegame_dir_name={basegame_dir_name}, basegame_sdcard={basegame_sdcard}") - if not modlist_ini_path.is_file(): - logger.error(f"ModOrganizer.ini not found at specified path: {modlist_ini_path}") - # Attempt to create a minimal INI - try: - logger.warning("Creating minimal ModOrganizer.ini with [General] section.") - with open(modlist_ini_path, 'w', encoding='utf-8') as f: - f.write('[General]\n') - # Continue as if file existed - except Exception as e: - logger.critical(f"Failed to create minimal ModOrganizer.ini: {e}") - return False - if not modlist_dir_path.is_dir(): - logger.error(f"Modlist directory not found or not a directory: {modlist_dir_path}") - # Warn but continue - - # --- Bulletproof game directory detection --- - # 1. Get all Steam libraries and log them - all_steam_libraries = PathHandler.get_all_steam_library_paths() - logger.info(f"[DEBUG] Detected Steam libraries: {all_steam_libraries}") - import sys - if hasattr(sys, 'argv') and any(arg in ('--debug', '-d') for arg in sys.argv): - # Debug logging for Steam libraries detection - use logger if available - if hasattr(globals(), 'logger') and logger: - logger.debug(f"Detected Steam libraries: {all_steam_libraries}") - # If no logger available, this debug info is not critical for user operation - - # 2. For each library, check for the canonical vanilla game directory - GAME_DIR_NAMES = { - "Skyrim Special Edition": "Skyrim Special Edition", - "Fallout 4": "Fallout 4", - "Fallout New Vegas": "Fallout New Vegas", - "Oblivion": "Oblivion" - } - canonical_name = None - if basegame_dir_name and basegame_dir_name in GAME_DIR_NAMES: - canonical_name = GAME_DIR_NAMES[basegame_dir_name] - elif basegame_dir_name: - canonical_name = basegame_dir_name # fallback, but should match above - gamepath_target_dir = None - gamepath_target_is_sdcard = modlist_sdcard - checked_candidates = [] - if canonical_name: - for lib in all_steam_libraries: - candidate = lib / "steamapps" / "common" / canonical_name - checked_candidates.append(str(candidate)) - logger.info(f"[DEBUG] Checking for vanilla game directory: {candidate}") - if candidate.is_dir(): - gamepath_target_dir = candidate - logger.info(f"Found vanilla game directory: {candidate}") - break - if not gamepath_target_dir: - logger.error(f"Could not find vanilla game directory '{canonical_name}' in any Steam library. Checked: {checked_candidates}") - # 4. Prompt the user for the path - print("\nCould not automatically detect a Stock Game or vanilla game directory.") - print("Please enter the full path to your vanilla game directory (e.g., /path/to/Skyrim Special Edition):") - while True: - user_input = input("Game directory path: ").strip() - user_path = Path(user_input) - logger.info(f"[DEBUG] User entered: {user_input}") - if user_path.is_dir(): - exe_candidates = list(user_path.glob('*.exe')) - logger.info(f"[DEBUG] .exe files in user path: {exe_candidates}") - if exe_candidates: - gamepath_target_dir = user_path - logger.info(f"User provided valid vanilla game directory: {gamepath_target_dir}") - break - else: - print("Directory exists but does not appear to contain the game executable. Please check and try again.") - logger.warning("User path exists but no .exe files found.") - else: - print("Directory not found. Please enter a valid path.") - logger.warning("User path does not exist.") - if not gamepath_target_dir: - logger.critical("[FATAL] Could not determine a valid target directory for gamePath. Check configuration and paths. Aborting update.") - return False - - # 3. Update gamePath, binary, and workingDirectory entries in the INI - logger.debug(f"Determined gamePath target directory: {gamepath_target_dir}") - logger.debug(f"gamePath target is on SD card: {gamepath_target_is_sdcard}") - try: - logger.debug(f"Reading original INI file: {modlist_ini_path}") - with open(modlist_ini_path, 'r', encoding='utf-8', errors='ignore') as f: - original_lines = f.readlines() - - # --- Find and robustly update gamePath line --- - gamepath_line_num = -1 - general_section_line = -1 - for i, line in enumerate(original_lines): - if re.match(r'^\s*\[General\]\s*$', line, re.IGNORECASE): - general_section_line = i - if re.match(r'^\s*gamepath\s*=\s*', line, re.IGNORECASE): - gamepath_line_num = i - break - processed_str = PathHandler._strip_sdcard_path_prefix(gamepath_target_dir) - windows_style_single = processed_str.replace('/', '\\') - gamepath_drive_letter = "D:" if gamepath_target_is_sdcard else "Z:" - # Use robust formatter - formatted_gamepath = PathHandler._format_gamepath_for_mo2(f'{gamepath_drive_letter}{windows_style_single}') - new_gamepath_line = f'gamePath = @ByteArray({formatted_gamepath})\n' - if gamepath_line_num != -1: - logger.info(f"Updating existing gamePath line: {original_lines[gamepath_line_num].strip()} -> {new_gamepath_line.strip()}") - original_lines[gamepath_line_num] = new_gamepath_line - else: - insert_at = general_section_line + 1 if general_section_line != -1 else 0 - logger.info(f"Adding missing gamePath line at line {insert_at+1}: {new_gamepath_line.strip()}") - original_lines.insert(insert_at, new_gamepath_line) - - # --- Update customExecutables binaries and workingDirectories --- - TARGET_EXECUTABLES_LOWER = [ - "skse64_loader.exe", "f4se_loader.exe", "nvse_loader.exe", "obse_loader.exe", "falloutnv.exe" - ] - in_custom_exec = False - for i, line in enumerate(original_lines): - if re.match(r'^\s*\[customExecutables\]\s*$', line, re.IGNORECASE): - in_custom_exec = True - continue - if in_custom_exec and re.match(r'^\s*\[.*\]\s*$', line): - in_custom_exec = False - if in_custom_exec: - m = re.match(r'^(\d+)\\binary\s*=\s*(.*)$', line.strip(), re.IGNORECASE) - if m: - idx, old_path = m.group(1), m.group(2) - exe_name = os.path.basename(old_path).lower() - if exe_name in TARGET_EXECUTABLES_LOWER: - new_path = f'{gamepath_drive_letter}/{PathHandler._strip_sdcard_path_prefix(gamepath_target_dir)}/{exe_name}' - # Use robust formatter - new_path = PathHandler._format_binary_for_mo2(new_path) - logger.info(f"Updating binary for entry {idx}: {old_path} -> {new_path}") - original_lines[i] = f'{idx}\\binary = {new_path}\n' - m_wd = re.match(r'^(\d+)\\workingDirectory\s*=\s*(.*)$', line.strip(), re.IGNORECASE) - if m_wd: - idx, old_wd = m_wd.group(1), m_wd.group(2) - new_wd = f'{gamepath_drive_letter}{windows_style_single}' - # Use robust formatter - new_wd = PathHandler._format_workingdir_for_mo2(new_wd) - logger.info(f"Updating workingDirectory for entry {idx}: {old_wd} -> {new_wd}") - original_lines[i] = f'{idx}\\workingDirectory = {new_wd}\n' - - # --- Backup and Write New INI --- - backup_path = modlist_ini_path.with_suffix(f".{datetime.now().strftime('%Y%m%d_%H%M%S')}.bak") - try: - shutil.copy2(modlist_ini_path, backup_path) - logger.info(f"Backed up original INI to: {backup_path}") - except Exception as bak_err: - logger.error(f"Failed to backup original INI file: {bak_err}") - return False - try: - with open(modlist_ini_path, 'w', encoding='utf-8') as f: - f.writelines(original_lines) - logger.info(f"Successfully wrote updated paths to {modlist_ini_path}") - return True - except Exception as write_err: - logger.error(f"Failed to write updated INI file {modlist_ini_path}: {write_err}", exc_info=True) - logger.error("Attempting to restore from backup...") - try: - shutil.move(backup_path, modlist_ini_path) - logger.info("Successfully restored original INI from backup.") - except Exception as restore_err: - logger.critical(f"CRITICAL FAILURE: Could not write new INI and failed to restore backup {backup_path}. Manual intervention required at {modlist_ini_path}! Error: {restore_err}") - return False - except Exception as e: - logger.error(f"An unexpected error occurred during INI path update: {e}", exc_info=True) - return False - - @staticmethod - def edit_resolution(modlist_ini, resolution): - """ - Edit resolution settings in ModOrganizer.ini - - Args: - modlist_ini (str): Path to ModOrganizer.ini - resolution (str): Resolution in the format "1920x1080" - - Returns: - bool: True on success, False on failure - """ - try: - logger.info(f"Editing resolution settings to {resolution}...") - - # Parse resolution - width, height = resolution.split('x') - - # Read the current ModOrganizer.ini - with open(modlist_ini, 'r') as f: - content = f.read() - - # Replace width and height settings - content = re.sub(r'^width\s*=\s*\d+$', f'width = {width}', content, flags=re.MULTILINE) - content = re.sub(r'^height\s*=\s*\d+$', f'height = {height}', content, flags=re.MULTILINE) - - # Write the updated content back to the file - with open(modlist_ini, 'w') as f: - f.write(content) - - logger.info("Resolution settings edited successfully") - return True - - except Exception as e: - logger.error(f"Error editing resolution settings: {e}") - return False - - @staticmethod - def create_dxvk_conf(modlist_dir, modlist_sdcard, steam_library, basegame_sdcard, game_var_full): - """ - Create dxvk.conf file in the appropriate location - - Args: - modlist_dir (str): Path to the modlist directory - modlist_sdcard (bool): Whether the modlist is on an SD card - steam_library (str): Path to the Steam library - basegame_sdcard (bool): Whether the base game is on an SD card - game_var_full (str): Full name of the game (e.g., "Skyrim Special Edition") - - Returns: - bool: True on success, False on failure - """ - try: - logger.info("Creating dxvk.conf file...") - - # Determine the location for dxvk.conf - dxvk_conf_path = None - - # Check for common stock game directories - stock_game_paths = [ - os.path.join(modlist_dir, "Stock Game"), - os.path.join(modlist_dir, "STOCK GAME"), - os.path.join(modlist_dir, "Game Root"), - os.path.join(modlist_dir, "Stock Folder"), - os.path.join(modlist_dir, "Skyrim Stock"), - os.path.join(modlist_dir, "root", "Skyrim Special Edition"), - os.path.join(steam_library, game_var_full) - ] - - for path in stock_game_paths: - if os.path.exists(path): - dxvk_conf_path = os.path.join(path, "dxvk.conf") - break - - if not dxvk_conf_path: - logger.error("Could not determine location for dxvk.conf") - return False - - # Create dxvk.conf content - dxvk_conf_content = "dxvk.enableGraphicsPipelineLibrary = False\n" - - # Write dxvk.conf to the appropriate location - with open(dxvk_conf_path, 'w') as f: - f.write(dxvk_conf_content) - - logger.info(f"dxvk.conf created successfully at {dxvk_conf_path}") - return True - - except Exception as e: - logger.error(f"Error creating dxvk.conf: {e}") - return False - - @staticmethod - def find_steam_config_vdf() -> Optional[Path]: - """Finds the active Steam config.vdf file.""" - logger.debug("Searching for Steam config.vdf...") - possible_steam_paths = [ - Path.home() / ".steam/steam", - Path.home() / ".local/share/Steam", - Path.home() / ".steam/root" - ] - for steam_path in possible_steam_paths: - potential_path = steam_path / "config/config.vdf" - if potential_path.is_file(): - logger.info(f"Found config.vdf at: {potential_path}") - return potential_path # Return Path object - - logger.warning("Could not locate Steam's config.vdf file in standard locations.") - return None - - @staticmethod - def find_steam_library() -> Optional[Path]: - """Find the primary Steam library common directory containing games.""" - logger.debug("Attempting to find Steam library...") - - # Potential locations for libraryfolders.vdf - libraryfolders_vdf_paths = [ - os.path.expanduser("~/.steam/steam/config/libraryfolders.vdf"), - os.path.expanduser("~/.local/share/Steam/config/libraryfolders.vdf"), - # Add other potential standard locations if necessary - ] - - # Simple backup mechanism (optional but good practice) - for path in libraryfolders_vdf_paths: - if os.path.exists(path): - backup_dir = os.path.join(os.path.dirname(path), "backups") - if not os.path.exists(backup_dir): - try: - os.makedirs(backup_dir) - except OSError as e: - logger.warning(f"Could not create backup directory {backup_dir}: {e}") - - # Create timestamped backup if it doesn't exist for today - timestamp = datetime.now().strftime("%Y%m%d") - backup_filename = f"libraryfolders_{timestamp}.vdf.bak" - backup_path = os.path.join(backup_dir, backup_filename) - - if not os.path.exists(backup_path): - try: - import shutil - shutil.copy2(path, backup_path) - logger.debug(f"Created backup of libraryfolders.vdf at {backup_path}") - except Exception as e: - logger.error(f"Failed to create backup of libraryfolders.vdf: {e}") - # Continue anyway, as we're only reading the file - pass - - libraryfolders_vdf_path_obj = None # Will hold the Path object - found_path_str = None - for path_str in libraryfolders_vdf_paths: - if os.path.exists(path_str): - found_path_str = path_str # Keep the string path for logging/opening - libraryfolders_vdf_path_obj = Path(path_str) # Convert to Path object here - logger.debug(f"Found libraryfolders.vdf at: {path_str}") - break - - # Check using the Path object's is_file() method - if not libraryfolders_vdf_path_obj or not libraryfolders_vdf_path_obj.is_file(): - logger.warning("libraryfolders.vdf not found or is not a file. Cannot automatically detect Steam Library.") - return None - - # Parse the VDF file to extract library paths - library_paths = [] - try: - # Open using the original string path is fine, or use the Path object - with open(found_path_str, 'r') as f: # Or use libraryfolders_vdf_path_obj - content = f.read() - - # Use regex to find all path entries - path_matches = re.finditer(r'"path"\s*"([^"]+)"', content) - for match in path_matches: - library_path_str = match.group(1).replace('\\\\', '\\') # Fix potential double escapes - common_path = os.path.join(library_path_str, "steamapps", "common") - if os.path.isdir(common_path): # Verify the common path exists - library_paths.append(Path(common_path)) - logger.debug(f"Found potential common path: {common_path}") - else: - logger.debug(f"Skipping non-existent common path derived from VDF: {common_path}") - - logger.debug(f"Found {len(library_paths)} valid library common paths from VDF.") - - # Return the first valid path found - if library_paths: - logger.info(f"Using Steam library common path: {library_paths[0]}") - return library_paths[0] - - # If no valid paths found in VDF, try the default structure - logger.debug("No valid common paths found in VDF, checking default location...") - default_common_path = Path.home() / ".steam/steam/steamapps/common" - if default_common_path.is_dir(): - logger.info(f"Using default Steam library common path: {default_common_path}") - return default_common_path - - default_common_path_local = Path.home() / ".local/share/Steam/steamapps/common" - if default_common_path_local.is_dir(): - logger.info(f"Using default local Steam library common path: {default_common_path_local}") - return default_common_path_local - - logger.error("No valid Steam library common path found in VDF or default locations.") - return None - - except Exception as e: - logger.error(f"Error parsing libraryfolders.vdf or finding Steam library: {e}", exc_info=True) - return None - - @staticmethod - def find_compat_data(appid: str) -> Optional[Path]: - """Find the compatdata directory for a given AppID.""" - if not appid or not appid.isdigit(): - logger.error(f"Invalid AppID provided for compatdata search: {appid}") - return None - - logger.debug(f"Searching for compatdata directory for AppID: {appid}") - - # Prefer standard Steam locations - possible_bases = [ - Path.home() / ".steam/steam/steamapps/compatdata", - Path.home() / ".local/share/Steam/steamapps/compatdata", - # Add likely SD card mount points if applicable - # Path("/run/media/mmcblk0p1/steamapps/compatdata") - ] - - # Check user's Steam Library path if available (more reliable) - # Assuming PathHandler might store or be passed the library path - # steam_lib_path = self.find_steam_library() # Or get from instance var if stored - # if steam_lib_path and (steam_lib_path / "steamapps/compatdata").is_dir(): - # possible_bases.insert(0, steam_lib_path / "steamapps/compatdata") # Prioritize - - for base_path in possible_bases: - if not base_path.is_dir(): - logger.debug(f"Compatdata base path does not exist or is not a directory: {base_path}") - continue - - potential_path = base_path / appid - if potential_path.is_dir(): - logger.info(f"Found compatdata directory: {potential_path}") - return potential_path # Return Path object - else: - logger.debug(f"Compatdata for {appid} not found in {base_path}") - - # Fallback: Broad search (can be slow, consider if needed) - # try: - # logger.debug(f"Compatdata not found in standard locations, attempting wider search...") - # # This can be very slow and resource-intensive - # # find_output = subprocess.check_output(['find', '/', '-type', 'd', '-name', appid, '-path', '*/compatdata/*', '-print', '-quit', '2>/dev/null'], text=True).strip() - # # if find_output: - # # logger.info(f"Found compatdata via find command: {find_output}") - # # return Path(find_output) - # except Exception as e: - # logger.warning(f"Error during 'find' command for compatdata: {e}") - - logger.warning(f"Compatdata directory for AppID {appid} not found.") - return None - - @staticmethod - def detect_stock_game_path(game_type: str, steam_library: Path) -> Optional[Path]: - """ - Detect the stock game path for a given game type and Steam library - Returns the path if found, None otherwise - """ - try: - # Map of game types to their Steam App IDs - game_app_ids = { - 'skyrim': '489830', # Skyrim Special Edition - 'fallout4': '377160', # Fallout 4 - 'fnv': '22380', # Fallout: New Vegas - 'oblivion': '22330' # The Elder Scrolls IV: Oblivion - } - - if game_type not in game_app_ids: - return None - - app_id = game_app_ids[game_type] - game_path = steam_library / 'steamapps' / 'common' - - # List of possible game directory names - possible_names = { - 'skyrim': ['Skyrim Special Edition', 'Skyrim'], - 'fallout4': ['Fallout 4'], - 'fnv': ['Fallout New Vegas', 'FalloutNV'], - 'oblivion': ['Oblivion'] - } - - if game_type not in possible_names: - return None - - # Check each possible directory name - for name in possible_names[game_type]: - potential_path = game_path / name - if potential_path.exists(): - return potential_path - - return None - - except Exception as e: - logging.error(f"Error detecting stock game path: {e}") - return None - - @staticmethod - def get_steam_library_path(steam_path: str) -> Optional[str]: - """Get the Steam library path from libraryfolders.vdf.""" - try: - libraryfolders_path = os.path.join(steam_path, 'steamapps', 'libraryfolders.vdf') - if not os.path.exists(libraryfolders_path): - return None - - with open(libraryfolders_path, 'r', encoding='utf-8') as f: - content = f.read() - - # Parse the VDF content - libraries = {} - current_library = None - for line in content.split('\n'): - line = line.strip() - if line.startswith('"path"'): - current_library = line.split('"')[3].replace('\\\\', '\\') - elif line.startswith('"apps"') and current_library: - libraries[current_library] = True - - # Return the first library path that exists - for library_path in libraries: - if os.path.exists(library_path): - return library_path - - return None - except Exception as e: - logger.error(f"Error getting Steam library path: {str(e)}") - return None - - @staticmethod - def get_all_steam_library_paths() -> List[Path]: - """Finds all Steam library paths listed in all known libraryfolders.vdf files (including Flatpak).""" - logger.info("[DEBUG] Searching for all Steam libraryfolders.vdf files...") - vdf_paths = [ - Path.home() / ".steam/steam/config/libraryfolders.vdf", - Path.home() / ".local/share/Steam/config/libraryfolders.vdf", - Path.home() / ".steam/root/config/libraryfolders.vdf", - Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf", # Flatpak - ] - library_paths = set() - for vdf_path in vdf_paths: - if vdf_path.is_file(): - logger.info(f"[DEBUG] Parsing libraryfolders.vdf: {vdf_path}") - try: - with open(vdf_path) as f: - for line in f: - m = re.search(r'"path"\s*"([^"]+)"', line) - if m: - lib_path = Path(m.group(1)) - library_paths.add(lib_path) - except Exception as e: - logger.error(f"[DEBUG] Failed to parse {vdf_path}: {e}") - logger.info(f"[DEBUG] All detected Steam libraries: {library_paths}") - return list(library_paths) - - # Moved _find_shortcuts_vdf here from ShortcutHandler - def _find_shortcuts_vdf(self) -> Optional[str]: - """Helper to find the active shortcuts.vdf file for a user. - - Iterates through userdata directories and returns the path to the - first found shortcuts.vdf file. - - Returns: - Optional[str]: The full path to the shortcuts.vdf file, or None if not found. - """ - # This implementation was moved from ShortcutHandler - 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 - def find_game_install_paths(target_appids: Dict[str, str]) -> Dict[str, Path]: - """ - Find installation paths for multiple specified games using Steam app IDs. - - Args: - target_appids: Dictionary mapping game names to app IDs - - Returns: - Dictionary mapping game names to their installation paths - """ - # Get all Steam library paths - library_paths = PathHandler.get_all_steam_library_paths() - if not library_paths: - logger.warning("Failed to find any Steam library paths") - return {} - - results = {} - - # For each library path, look for each target game - for library_path in library_paths: - # Check if the common directory exists - common_dir = library_path / "common" - if not common_dir.is_dir(): - logger.debug(f"No 'common' directory in library: {library_path}") - continue - - # Get subdirectories in common dir - try: - game_dirs = [d for d in common_dir.iterdir() if d.is_dir()] - except (PermissionError, OSError) as e: - logger.warning(f"Cannot access directory {common_dir}: {e}") - continue - - # For each app ID, check if we find its directory - for game_name, app_id in target_appids.items(): - if game_name in results: - continue # Already found this game - - # Try to find by appmanifest - appmanifest_path = library_path / f"appmanifest_{app_id}.acf" - if appmanifest_path.is_file(): - # Find the installdir value - try: - with open(appmanifest_path, 'r', encoding='utf-8') as f: - content = f.read() - match = re.search(r'"installdir"\s+"([^"]+)"', content) - if match: - install_dir_name = match.group(1) - install_path = common_dir / install_dir_name - if install_path.is_dir(): - results[game_name] = install_path - logger.info(f"Found {game_name} at {install_path}") - continue - except Exception as e: - logger.warning(f"Error reading appmanifest for {game_name}: {e}") - - return results - - def replace_gamepath(self, modlist_ini_path: Path, new_game_path: Path, modlist_sdcard: bool = False) -> bool: - """ - Updates the gamePath value in ModOrganizer.ini to the specified path. - Strictly matches the bash script: only replaces an existing gamePath line. - If the file or line does not exist, logs error and aborts. - """ - logger.info(f"Replacing gamePath in {modlist_ini_path} with {new_game_path}") - if not modlist_ini_path.is_file(): - logger.error(f"ModOrganizer.ini not found at: {modlist_ini_path}") - return False - try: - with open(modlist_ini_path, 'r', encoding='utf-8', errors='ignore') as f: - lines = f.readlines() - drive_letter = "D:" if modlist_sdcard else "Z:" - processed_path = self._strip_sdcard_path_prefix(new_game_path) - windows_style = processed_path.replace('/', '\\') - windows_style_double = windows_style.replace('\\', '\\\\') - new_gamepath_line = f'gamePath=@ByteArray({drive_letter}{windows_style_double})\n' - gamepath_found = False - for i, line in enumerate(lines): - # Make the check case-insensitive and robust to whitespace - if re.match(r'^\s*gamepath\s*=.*$', line, re.IGNORECASE): - lines[i] = new_gamepath_line - gamepath_found = True - break - if not gamepath_found: - logger.error("No gamePath line found in ModOrganizer.ini") - return False - with open(modlist_ini_path, 'w', encoding='utf-8') as f: - f.writelines(lines) - logger.info(f"Successfully updated gamePath to {new_game_path}") - return True - except Exception as e: - logger.error(f"Error replacing gamePath: {e}", exc_info=True) - return False - - # ===================================================================================== - # CRITICAL: DO NOT CHANGE THIS FUNCTION WITHOUT UPDATING TESTS AND CONSULTING PROJECT LEAD - # This function implements the exact path rewriting logic required for ModOrganizer.ini - # to match the original, robust bash script. Any change here risks breaking modlist - # configuration for users. If you must change this, update all relevant tests and - # consult the Project Lead for Jackify. See also omni-guides.sh for reference logic. - # ===================================================================================== - def edit_binary_working_paths(self, modlist_ini_path: Path, modlist_dir_path: Path, modlist_sdcard: bool, steam_libraries: Optional[List[Path]] = None) -> bool: - """ - Update all binary paths and working directories in a ModOrganizer.ini file. - Handles various ModOrganizer.ini formats (single or double backslashes in keys). - When updating gamePath, binary, and workingDirectory, retain the original stock folder (Stock Game, Game Root, etc) if present in the current value. - steam_libraries: Optional[List[Path]] - already-discovered Steam library paths to use for vanilla detection. - - # DO NOT CHANGE THIS LOGIC WITHOUT UPDATING TESTS AND CONSULTING THE PROJECT LEAD - # This is a critical, regression-prone area. See omni-guides.sh for reference. - """ - try: - logger.debug(f"Updating binary paths and working directories in {modlist_ini_path} to use root: {modlist_dir_path}") - if not modlist_ini_path.is_file(): - logger.error(f"INI file {modlist_ini_path} does not exist") - return False - with open(modlist_ini_path, 'r', encoding='utf-8') as f: - lines = f.readlines() - game_path_updated = False - binary_paths_updated = 0 - working_dirs_updated = 0 - binary_lines = [] - working_dir_lines = [] - for i, line in enumerate(lines): - stripped = line.strip() - binary_match = re.match(r'^(\d+)(\\+)\s*binary\s*=.*$', stripped, re.IGNORECASE) - if binary_match: - index = binary_match.group(1) - backslash_style = binary_match.group(2) - binary_lines.append((i, stripped, index, backslash_style)) - wd_match = re.match(r'^(\d+)(\\+)\s*workingDirectory\s*=.*$', stripped, re.IGNORECASE) - if wd_match: - index = wd_match.group(1) - backslash_style = wd_match.group(2) - working_dir_lines.append((i, stripped, index, backslash_style)) - binary_paths_by_index = {} - # Use provided steam_libraries if available, else detect - if steam_libraries is None or not steam_libraries: - steam_libraries = PathHandler.get_all_steam_library_paths() - for i, line, index, backslash_style in binary_lines: - parts = line.split('=', 1) - if len(parts) != 2: - logger.error(f"Malformed binary line: {line}") - continue - key_part, value_part = parts - exe_name = os.path.basename(value_part) - drive_prefix = "D:" if modlist_sdcard else "Z:" - rel_path = None - # --- BEGIN: FULL PARITY LOGIC --- - if 'steamapps' in value_part: - idx = value_part.index('steamapps') - subpath = value_part[idx:].lstrip('/') - correct_steam_lib = None - for lib in steam_libraries: - if (lib / subpath.split('/')[2]).exists(): - correct_steam_lib = lib.parent - break - if not correct_steam_lib and steam_libraries: - correct_steam_lib = steam_libraries[0].parent - if correct_steam_lib: - new_binary_path = f"{drive_prefix}/{correct_steam_lib}/{subpath}".replace('\\', '/').replace('//', '/') - else: - logger.error("Could not determine correct Steam library for vanilla game path.") - continue - else: - found_stock = None - for folder in STOCK_GAME_FOLDERS: - folder_pattern = f"/{folder.replace(' ', '')}".lower() - value_part_lower = value_part.replace(' ', '').lower() - if folder_pattern in value_part_lower: - idx = value_part_lower.index(folder_pattern) - rel_path = value_part[idx:].lstrip('/') - found_stock = folder - break - if not rel_path: - mods_pattern = "/mods/" - if mods_pattern in value_part: - idx = value_part.index(mods_pattern) - rel_path = value_part[idx:].lstrip('/') - else: - rel_path = exe_name - new_binary_path = f"{drive_prefix}/{modlist_dir_path}/{rel_path}".replace('\\', '/').replace('//', '/') - formatted_binary_path = PathHandler._format_binary_for_mo2(new_binary_path) - new_binary_line = f"{index}{backslash_style}binary={formatted_binary_path}" - logger.debug(f"Updating binary path: {line.strip()} -> {new_binary_line}") - lines[i] = new_binary_line + "\n" - binary_paths_updated += 1 - binary_paths_by_index[index] = formatted_binary_path - for j, wd_line, index, backslash_style in working_dir_lines: - if index in binary_paths_by_index: - binary_path = binary_paths_by_index[index] - wd_path = os.path.dirname(binary_path) - drive_prefix = "D:" if modlist_sdcard else "Z:" - if wd_path.startswith("D:") or wd_path.startswith("Z:"): - wd_path = wd_path[2:] - wd_path = drive_prefix + wd_path - formatted_wd_path = PathHandler._format_workingdir_for_mo2(wd_path) - key_part = f"{index}{backslash_style}workingDirectory" - new_wd_line = f"{key_part}={formatted_wd_path}" - logger.debug(f"Updating working directory: {wd_line.strip()} -> {new_wd_line}") - lines[j] = new_wd_line + "\n" - working_dirs_updated += 1 - with open(modlist_ini_path, 'w', encoding='utf-8') as f: - f.writelines(lines) - logger.info(f"edit_binary_working_paths completed: Game path updated: {game_path_updated}, Binary paths updated: {binary_paths_updated}, Working directories updated: {working_dirs_updated}") - return True - except Exception as e: - logger.error(f"Error updating binary paths in {modlist_ini_path}: {str(e)}") - return False - - def _format_path_for_mo2(self, path: str) -> str: - """Format a path for MO2's ModOrganizer.ini file (working directories).""" - # Replace forward slashes with double backslashes - formatted = path.replace('/', '\\') - # Ensure we have a Windows drive letter format - if not re.match(r'^[A-Za-z]:', formatted): - formatted = 'D:' + formatted - # Double the backslashes for the INI file format - formatted = formatted.replace('\\', '\\\\') - return formatted - - def _format_binary_path_for_mo2(self, path_str): - """Format a binary path for MO2 config file. - - Binary paths need forward slashes (/) in the path portion. - """ - # Replace backslashes with forward slashes - return path_str.replace('\\', '/') - - def _format_working_dir_for_mo2(self, path_str): - """ - Format a working directory path for MO2 config file. - Ensures double backslashes throughout, as required by ModOrganizer.ini. - """ - import re - path = path_str.replace('/', '\\') - path = path.replace('\\', '\\\\') # Double all backslashes - # Ensure only one double backslash after drive letter - path = re.sub(r'^([A-Z]:)\\\\+', r'\1\\\\', path) - return path - - @staticmethod - def find_vanilla_game_paths(game_names=None) -> Dict[str, Path]: - """ - For each known game, iterate all Steam libraries and look for the canonical game directory name in steamapps/common. - Returns a dict of found games and their paths. - Args: - game_names: Optional list of game names to check. If None, uses default supported games. - Returns: - Dict[str, Path]: Mapping of game name to found install Path. - """ - # Canonical game directory names (allow list for Fallout 3) - GAME_DIR_NAMES = { - "Skyrim Special Edition": ["Skyrim Special Edition"], - "Fallout 4": ["Fallout 4"], - "Fallout New Vegas": ["Fallout New Vegas"], - "Oblivion": ["Oblivion"], - "Fallout 3": ["Fallout 3", "Fallout 3 goty"] - } - if game_names is None: - game_names = list(GAME_DIR_NAMES.keys()) - all_steam_libraries = PathHandler.get_all_steam_library_paths() - logger.info(f"[DEBUG] Detected Steam libraries: {all_steam_libraries}") - found_games = {} - for game in game_names: - possible_names = GAME_DIR_NAMES.get(game, [game]) - for lib in all_steam_libraries: - for name in possible_names: - candidate = lib / "steamapps" / "common" / name - logger.info(f"[DEBUG] Checking for vanilla game directory: {candidate}") - if candidate.is_dir(): - found_games[game] = candidate - logger.info(f"Found vanilla game directory for {game}: {candidate}") - break # Stop after first found location - if game in found_games: - break - return found_games - - def _detect_stock_game_path(self): - """Detects common 'Stock Game' or 'Game Root' directories within the modlist path.""" - self.logger.info("Step 7a: Detecting Stock Game/Game Root directory...") - if not self.modlist_dir: - self.logger.error("Modlist directory not set, cannot detect stock game path.") - return False - - modlist_path = Path(self.modlist_dir) - # Always prefer 'Stock Game' if it exists, then fallback to others - preferred_order = [ - "Stock Game", - "STOCK GAME", - "Skyrim Stock", - "Stock Game Folder", - "Stock Folder", - Path("root/Skyrim Special Edition"), - "Game Root" # 'Game Root' is now last - ] - - found_path = None - for name in preferred_order: - potential_path = modlist_path / name - if potential_path.is_dir(): - found_path = str(potential_path) - self.logger.info(f"Found potential stock game directory: {found_path}") - break # Found the first match - if found_path: - self.stock_game_path = found_path - return True - else: - self.stock_game_path = None - self.logger.info("No common Stock Game/Game Root directory found. Will assume vanilla game path is needed for some operations.") - return True - - # --- Add robust path formatters for INI fields --- - @staticmethod - def _format_gamepath_for_mo2(path: str) -> str: - import re - path = path.replace('/', '\\') - path = re.sub(r'\\+', r'\\', path) # Collapse multiple backslashes - # Ensure only one double backslash after drive letter - path = re.sub(r'^([A-Z]:)\\+', r'\1\\', path) - return path - - @staticmethod - def _format_binary_for_mo2(path: str) -> str: - import re - path = path.replace('\\', '/') - # Collapse multiple forward slashes after drive letter - path = re.sub(r'^([A-Z]:)//+', r'\1/', path) - return path - - @staticmethod - def _format_workingdir_for_mo2(path: str) -> str: - import re - path = path.replace('/', '\\') - path = path.replace('\\', '\\\\') # Double all backslashes - # Ensure only one double backslash after drive letter - path = re.sub(r'^([A-Z]:)\\\\+', r'\1\\\\', path) - return path - -# --- End of PathHandler --- \ No newline at end of file