diff --git a/binaries/WabbajackProton.sh b/binaries/WabbajackProton.sh deleted file mode 100644 index 3f52e0c..0000000 --- a/binaries/WabbajackProton.sh +++ /dev/null @@ -1,880 +0,0 @@ -#!/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 deleted file mode 100644 index aa7c974..0000000 --- a/binaries/WabbajackWine.sh +++ /dev/null @@ -1,494 +0,0 @@ -#!/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 deleted file mode 100644 index df8de52..0000000 --- a/binaries/omni-guides-testing.sh +++ /dev/null @@ -1,1783 +0,0 @@ -#!/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 deleted file mode 100644 index 955db29..0000000 --- a/binaries/omni-guides.sh +++ /dev/null @@ -1,1711 +0,0 @@ -#!/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 570236d..d5f1880 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.0.1" +__version__ = "0.1.2" diff --git a/jackify/backend/handlers/self_update.py b/jackify/backend/handlers/self_update.py index 89f1648..536cbe3 100644 --- a/jackify/backend/handlers/self_update.py +++ b/jackify/backend/handlers/self_update.py @@ -37,8 +37,8 @@ def get_latest_release_info(): def get_current_version(): # This should match however Jackify stores its version try: - from src import version - return version.__version__ + from jackify import __version__ + return __version__ except ImportError: return None diff --git a/jackify/backend/services/update_service.py b/jackify/backend/services/update_service.py new file mode 100644 index 0000000..3fada9c --- /dev/null +++ b/jackify/backend/services/update_service.py @@ -0,0 +1,315 @@ +""" +Update service for checking and applying Jackify updates. + +This service handles checking for updates via GitHub releases API +and coordinating the update process. +""" + +import json +import logging +import os +import subprocess +import tempfile +import threading +from dataclasses import dataclass +from datetime import datetime, timedelta +from pathlib import Path +from typing import Optional, Callable +import requests + +from ...shared.appimage_utils import get_appimage_path, is_appimage, can_self_update + + +logger = logging.getLogger(__name__) + + +@dataclass +class UpdateInfo: + """Information about an available update.""" + version: str + tag_name: str + release_date: str + changelog: str + download_url: str + file_size: Optional[int] = None + is_critical: bool = False + + +class UpdateService: + """Service for checking and applying Jackify updates.""" + + def __init__(self, current_version: str): + """ + Initialize the update service. + + Args: + current_version: Current version of Jackify (e.g. "0.1.1") + """ + self.current_version = current_version + self.github_repo = "Omni-guides/Jackify" + self.github_api_base = "https://api.github.com" + self.update_check_timeout = 10 # seconds + + def check_for_updates(self) -> Optional[UpdateInfo]: + """ + Check for available updates via GitHub releases API. + + Returns: + UpdateInfo if update available, None otherwise + """ + try: + url = f"{self.github_api_base}/repos/{self.github_repo}/releases/latest" + headers = { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': f'Jackify/{self.current_version}' + } + + logger.debug(f"Checking for updates at {url}") + response = requests.get(url, headers=headers, timeout=self.update_check_timeout) + response.raise_for_status() + + release_data = response.json() + latest_version = release_data['tag_name'].lstrip('v') + + if self._is_newer_version(latest_version): + # Find AppImage asset + download_url = None + file_size = None + + for asset in release_data.get('assets', []): + if asset['name'].endswith('.AppImage'): + download_url = asset['browser_download_url'] + file_size = asset['size'] + break + + if download_url: + 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 + ) + else: + logger.warning(f"No AppImage found in release {latest_version}") + + return None + + except requests.RequestException as e: + logger.error(f"Failed to check for updates: {e}") + return None + except Exception as e: + logger.error(f"Unexpected error checking for updates: {e}") + return None + + def _is_newer_version(self, version: str) -> bool: + """ + Compare versions to determine if update is newer. + + Args: + version: Version to compare against current + + Returns: + bool: True if version is newer than current + """ + try: + # Simple version comparison for semantic versioning + def version_tuple(v): + return tuple(map(int, v.split('.'))) + + return version_tuple(version) > version_tuple(self.current_version) + except ValueError: + logger.warning(f"Could not parse version: {version}") + return False + + def check_for_updates_async(self, callback: Callable[[Optional[UpdateInfo]], None]) -> None: + """ + Check for updates in background thread. + + Args: + callback: Function to call with update info (or None) + """ + def check_worker(): + try: + update_info = self.check_for_updates() + callback(update_info) + except Exception as e: + logger.error(f"Error in background update check: {e}") + callback(None) + + thread = threading.Thread(target=check_worker, daemon=True) + thread.start() + + def can_update(self) -> bool: + """ + Check if updating is possible in current environment. + + Returns: + bool: True if updating is possible + """ + if not is_appimage(): + logger.debug("Not running as AppImage - updates not supported") + return False + + if not can_self_update(): + logger.debug("Cannot write to AppImage - updates not possible") + return False + + return True + + def download_update(self, update_info: UpdateInfo, + progress_callback: Optional[Callable[[int, int], None]] = None) -> Optional[Path]: + """ + Download update to temporary location. + + Args: + update_info: Information about the update to download + progress_callback: Optional callback for download progress (bytes_downloaded, total_bytes) + + Returns: + Path to downloaded file, or None if download failed + """ + try: + logger.info(f"Downloading update {update_info.version} from {update_info.download_url}") + + response = requests.get(update_info.download_url, stream=True) + response.raise_for_status() + + 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) + + temp_file = temp_dir / f"Jackify-{update_info.version}.AppImage" + + with open(temp_file, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + downloaded_size += len(chunk) + + if progress_callback: + progress_callback(downloaded_size, total_size) + + # Make executable + temp_file.chmod(0o755) + + logger.info(f"Update downloaded successfully to {temp_file}") + return temp_file + + except Exception as e: + logger.error(f"Failed to download update: {e}") + return None + + def apply_update(self, new_appimage_path: Path) -> bool: + """ + Apply update by replacing current AppImage. + + This creates a helper script that waits for Jackify to exit, + then replaces the AppImage and restarts it. + + Args: + new_appimage_path: Path to downloaded update + + Returns: + bool: True if update application was initiated successfully + """ + current_appimage = get_appimage_path() + if not current_appimage: + logger.error("Cannot determine current AppImage path") + return False + + try: + # Create update helper script + helper_script = self._create_update_helper(current_appimage, new_appimage_path) + + if helper_script: + # Launch helper script and exit + logger.info("Launching update helper and exiting") + subprocess.Popen(['nohup', 'bash', str(helper_script)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + return True + + return False + + except Exception as e: + logger.error(f"Failed to apply update: {e}") + return False + + def _create_update_helper(self, current_appimage: Path, new_appimage: Path) -> Optional[Path]: + """ + Create helper script for update replacement. + + Args: + current_appimage: Path to current AppImage + new_appimage: Path to new AppImage + + Returns: + Path to helper script, or None if creation failed + """ + try: + temp_dir = Path(tempfile.gettempdir()) / "jackify_updates" + temp_dir.mkdir(exist_ok=True) + + helper_script = temp_dir / "update_helper.sh" + + script_content = f'''#!/bin/bash +# Jackify Update Helper Script +# This script replaces the current AppImage with the new version + +CURRENT_APPIMAGE="{current_appimage}" +NEW_APPIMAGE="{new_appimage}" + +echo "Jackify Update Helper" +echo "Waiting for Jackify to exit..." + +# Wait for Jackify to exit (give it a few seconds) +sleep 3 + +echo "Replacing AppImage..." + +# Backup current version (optional) +if [ -f "$CURRENT_APPIMAGE" ]; then + cp "$CURRENT_APPIMAGE" "$CURRENT_APPIMAGE.backup" +fi + +# Replace with new version +if cp "$NEW_APPIMAGE" "$CURRENT_APPIMAGE"; then + chmod +x "$CURRENT_APPIMAGE" + echo "Update completed successfully!" + + # Clean up temporary file + rm -f "$NEW_APPIMAGE" + + # Restart Jackify + echo "Restarting Jackify..." + exec "$CURRENT_APPIMAGE" +else + echo "Update failed - could not replace AppImage" + # Restore backup if replacement failed + if [ -f "$CURRENT_APPIMAGE.backup" ]; then + mv "$CURRENT_APPIMAGE.backup" "$CURRENT_APPIMAGE" + echo "Restored original AppImage" + fi +fi + +# Clean up this script +rm -f "{helper_script}" +''' + + with open(helper_script, 'w') as f: + f.write(script_content) + + # Make executable + helper_script.chmod(0o755) + + logger.debug(f"Created update helper script: {helper_script}") + return helper_script + + except Exception as e: + logger.error(f"Failed to create update helper script: {e}") + return None \ No newline at end of file diff --git a/jackify/frontends/cli/main.py b/jackify/frontends/cli/main.py index 7c2ff3d..dad0a1e 100755 --- a/jackify/frontends/cli/main.py +++ b/jackify/frontends/cli/main.py @@ -174,13 +174,103 @@ class JackifyCLI: Returns: Dictionary of backend service instances """ - # For now, create a basic modlist service - # TODO: Add other services as needed + # Initialize update service + from jackify.backend.services.update_service import UpdateService + update_service = UpdateService(jackify_version) + services = { - 'modlist_service': ModlistService(self.system_info) + 'modlist_service': ModlistService(self.system_info), + 'update_service': update_service } return services + def _check_for_updates_on_startup(self): + """Check for updates on startup in background thread""" + try: + self._debug_print("Checking for updates on startup...") + + def update_check_callback(update_info): + """Handle update check results""" + try: + if update_info: + print(f"\n{COLOR_INFO}Update available: v{update_info.version}{COLOR_RESET}") + print(f"Current version: v{jackify_version}") + print(f"Release date: {update_info.release_date}") + if update_info.changelog: + print(f"Changelog: {update_info.changelog[:200]}...") + print(f"Download size: {update_info.file_size / (1024*1024):.1f} MB" if update_info.file_size else "Download size: Unknown") + print(f"\nTo update, run: jackify --update") + print("Or visit: https://github.com/Omni-guides/Jackify/releases") + else: + self._debug_print("No updates available") + except Exception as e: + self._debug_print(f"Error showing update info: {e}") + + # Check for updates in background + self.backend_services['update_service'].check_for_updates_async(update_check_callback) + + except Exception as e: + self._debug_print(f"Error checking for updates on startup: {e}") + # Continue anyway - don't block startup on update check errors + + def _handle_update(self): + """Handle manual update check and installation""" + try: + print("Checking for updates...") + update_service = self.backend_services['update_service'] + + # Check if updating is possible + if not update_service.can_update(): + print(f"{COLOR_ERROR}Update not possible: not running as AppImage or insufficient permissions{COLOR_RESET}") + return 1 + + # Check for updates + update_info = update_service.check_for_updates() + + if update_info: + print(f"{COLOR_INFO}Update available: v{update_info.version}{COLOR_RESET}") + print(f"Current version: v{jackify_version}") + print(f"Release date: {update_info.release_date}") + if update_info.changelog: + print(f"Changelog: {update_info.changelog}") + print(f"Download size: {update_info.file_size / (1024*1024):.1f} MB" if update_info.file_size else "Download size: Unknown") + + # Ask for confirmation + response = input("\nDo you want to download and install this update? (y/N): ").strip().lower() + if response in ['y', 'yes']: + print("Downloading update...") + + def progress_callback(downloaded, total): + if total > 0: + percentage = int((downloaded / total) * 100) + downloaded_mb = downloaded / (1024 * 1024) + total_mb = total / (1024 * 1024) + print(f"\rDownloaded {downloaded_mb:.1f} MB of {total_mb:.1f} MB ({percentage}%)", end='', flush=True) + + downloaded_path = update_service.download_update(update_info, progress_callback) + + if downloaded_path: + print(f"\nDownload completed. Installing update...") + if update_service.apply_update(downloaded_path): + print(f"{COLOR_INFO}Update applied successfully! Jackify will restart...{COLOR_RESET}") + return 0 + else: + print(f"{COLOR_ERROR}Failed to apply update{COLOR_RESET}") + return 1 + else: + print(f"\n{COLOR_ERROR}Failed to download update{COLOR_RESET}") + return 1 + else: + print("Update cancelled.") + return 0 + else: + print(f"{COLOR_INFO}You are already running the latest version (v{jackify_version}){COLOR_RESET}") + return 0 + + except Exception as e: + print(f"{COLOR_ERROR}Update failed: {e}{COLOR_RESET}") + return 1 + def _initialize_command_handlers(self): """Initialize command handler instances. @@ -271,6 +361,11 @@ class JackifyCLI: self._debug_print('JackifyCLI.run() called') self._debug_print(f'Parsed args: {self.args}') + # Handle update functionality + if getattr(self.args, 'update', False): + self._debug_print('Entering update workflow') + return self._handle_update() + # Handle legacy restart-steam functionality (temporary) if getattr(self.args, 'restart_steam', False): self._debug_print('Entering restart_steam workflow') @@ -290,6 +385,9 @@ class JackifyCLI: if getattr(self.args, 'command', None): return self._run_command(self.args.command, self.args) + # Check for updates on startup (non-blocking) + self._check_for_updates_on_startup() + # Run interactive mode (legacy for now) self._run_interactive() @@ -303,6 +401,7 @@ class JackifyCLI: parser.add_argument("--resolution", type=str, help="Resolution to set (optional)") parser.add_argument('--restart-steam', action='store_true', help='Restart Steam (native, for GUI integration)') parser.add_argument('--dev', action='store_true', help='Enable development features (show hidden menu items)') + parser.add_argument('--update', action='store_true', help='Check for and install updates') # Add command-specific arguments self.commands['tuxborn'].add_args(parser) diff --git a/jackify/frontends/gui/dialogs/update_dialog.py b/jackify/frontends/gui/dialogs/update_dialog.py new file mode 100644 index 0000000..c0f155c --- /dev/null +++ b/jackify/frontends/gui/dialogs/update_dialog.py @@ -0,0 +1,293 @@ +""" +Update notification and download dialog for Jackify. + +This dialog handles notifying users about available updates and +managing the download/installation process. +""" + +import logging +from pathlib import Path +from typing import Optional + +from PySide6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, + QTextEdit, QProgressBar, QGroupBox, QCheckBox +) +from PySide6.QtCore import Qt, QThread, Signal +from PySide6.QtGui import QPixmap, QFont + +from ....backend.services.update_service import UpdateService, UpdateInfo + + +logger = logging.getLogger(__name__) + + +class UpdateDownloadThread(QThread): + """Background thread for downloading updates.""" + + progress_updated = Signal(int, int) # downloaded, total + download_finished = Signal(object) # Path or None + + def __init__(self, update_service: UpdateService, update_info: UpdateInfo): + super().__init__() + self.update_service = update_service + self.update_info = update_info + self.downloaded_path = None + + def run(self): + """Download the update in background.""" + try: + def progress_callback(downloaded: int, total: int): + self.progress_updated.emit(downloaded, total) + + self.downloaded_path = self.update_service.download_update( + self.update_info, progress_callback + ) + + self.download_finished.emit(self.downloaded_path) + + except Exception as e: + logger.error(f"Error in download thread: {e}") + self.download_finished.emit(None) + + +class UpdateDialog(QDialog): + """Dialog for notifying users about updates and handling downloads.""" + + def __init__(self, update_info: UpdateInfo, update_service: UpdateService, parent=None): + super().__init__(parent) + self.update_info = update_info + self.update_service = update_service + self.downloaded_path = None + self.download_thread = None + + self.setup_ui() + self.setup_connections() + + def setup_ui(self): + """Set up the dialog UI.""" + self.setWindowTitle("Jackify Update Available") + self.setModal(True) + self.setMinimumSize(500, 400) + self.setMaximumSize(600, 600) + + layout = QVBoxLayout(self) + + # Header + header_layout = QHBoxLayout() + + # Update icon (if available) + icon_label = QLabel() + icon_label.setText("🔄") # Simple emoji for now + icon_label.setStyleSheet("font-size: 32px;") + header_layout.addWidget(icon_label) + + # Update title + title_layout = QVBoxLayout() + title_label = QLabel(f"Update Available: v{self.update_info.version}") + title_font = QFont() + title_font.setPointSize(14) + title_font.setBold(True) + title_label.setFont(title_font) + title_layout.addWidget(title_label) + + subtitle_label = QLabel(f"Current version: v{self.update_service.current_version}") + subtitle_label.setStyleSheet("color: #666;") + title_layout.addWidget(subtitle_label) + + header_layout.addLayout(title_layout) + header_layout.addStretch() + + layout.addLayout(header_layout) + + # File size info + if self.update_info.file_size: + size_mb = self.update_info.file_size / (1024 * 1024) + size_label = QLabel(f"Download size: {size_mb:.1f} MB") + size_label.setStyleSheet("color: #666; margin-bottom: 10px;") + layout.addWidget(size_label) + + # Changelog group + changelog_group = QGroupBox("What's New") + changelog_layout = QVBoxLayout(changelog_group) + + self.changelog_text = QTextEdit() + self.changelog_text.setPlainText(self.update_info.changelog or "No changelog available.") + self.changelog_text.setMaximumHeight(150) + self.changelog_text.setReadOnly(True) + changelog_layout.addWidget(self.changelog_text) + + layout.addWidget(changelog_group) + + # Progress section (initially hidden) + self.progress_group = QGroupBox("Download Progress") + progress_layout = QVBoxLayout(self.progress_group) + + self.progress_bar = QProgressBar() + self.progress_bar.setVisible(False) + progress_layout.addWidget(self.progress_bar) + + self.progress_label = QLabel("Preparing download...") + self.progress_label.setVisible(False) + progress_layout.addWidget(self.progress_label) + + layout.addWidget(self.progress_group) + self.progress_group.setVisible(False) + + # Options + options_group = QGroupBox("Update Options") + options_layout = QVBoxLayout(options_group) + + self.auto_restart_checkbox = QCheckBox("Automatically restart Jackify after update") + self.auto_restart_checkbox.setChecked(True) + options_layout.addWidget(self.auto_restart_checkbox) + + layout.addWidget(options_group) + + # Buttons + button_layout = QHBoxLayout() + + self.later_button = QPushButton("Remind Me Later") + self.later_button.clicked.connect(self.remind_later) + button_layout.addWidget(self.later_button) + + self.skip_button = QPushButton("Skip This Version") + self.skip_button.clicked.connect(self.skip_version) + button_layout.addWidget(self.skip_button) + + button_layout.addStretch() + + self.download_button = QPushButton("Download & Install Update") + self.download_button.setDefault(True) + self.download_button.clicked.connect(self.start_download) + button_layout.addWidget(self.download_button) + + self.install_button = QPushButton("Install & Restart") + self.install_button.setVisible(False) + self.install_button.clicked.connect(self.install_update) + button_layout.addWidget(self.install_button) + + layout.addLayout(button_layout) + + # Style the download button + self.download_button.setStyleSheet(""" + QPushButton { + background-color: #0d7377; + color: white; + font-weight: bold; + padding: 8px 16px; + border-radius: 4px; + } + QPushButton:hover { + background-color: #14a085; + } + """) + + def setup_connections(self): + """Set up signal connections.""" + pass + + def start_download(self): + """Start downloading the update.""" + if not self.update_service.can_update(): + self.show_error("Update not possible", + "Cannot update: not running as AppImage or insufficient permissions.") + return + + # Show progress UI + self.progress_group.setVisible(True) + self.progress_bar.setVisible(True) + self.progress_label.setVisible(True) + self.progress_label.setText("Starting download...") + + # Disable buttons during download + self.download_button.setEnabled(False) + self.later_button.setEnabled(False) + self.skip_button.setEnabled(False) + + # Start download thread + self.download_thread = UpdateDownloadThread(self.update_service, self.update_info) + self.download_thread.progress_updated.connect(self.update_progress) + self.download_thread.download_finished.connect(self.download_completed) + self.download_thread.start() + + def update_progress(self, downloaded: int, total: int): + """Update download progress.""" + if total > 0: + percentage = int((downloaded / total) * 100) + self.progress_bar.setValue(percentage) + + downloaded_mb = downloaded / (1024 * 1024) + total_mb = total / (1024 * 1024) + + self.progress_label.setText(f"Downloaded {downloaded_mb:.1f} MB of {total_mb:.1f} MB ({percentage}%)") + else: + self.progress_label.setText(f"Downloaded {downloaded / (1024 * 1024):.1f} MB...") + + def download_completed(self, downloaded_path: Optional[Path]): + """Handle download completion.""" + if downloaded_path: + self.downloaded_path = downloaded_path + self.progress_label.setText("Download completed successfully!") + self.progress_bar.setValue(100) + + # Show install button + self.download_button.setVisible(False) + self.install_button.setVisible(True) + + # Re-enable other buttons + self.later_button.setEnabled(True) + self.skip_button.setEnabled(True) + + else: + self.show_error("Download Failed", "Failed to download the update. Please try again later.") + + # Reset UI + self.progress_group.setVisible(False) + self.download_button.setEnabled(True) + self.later_button.setEnabled(True) + self.skip_button.setEnabled(True) + + def install_update(self): + """Install the downloaded update.""" + if not self.downloaded_path: + self.show_error("No Download", "No update has been downloaded.") + return + + self.progress_label.setText("Installing update...") + + if self.update_service.apply_update(self.downloaded_path): + self.progress_label.setText("Update applied successfully! Jackify will restart...") + + # Close dialog and exit application (update helper will restart) + self.accept() + + # The update helper script will handle the restart + import sys + sys.exit(0) + + else: + self.show_error("Installation Failed", "Failed to apply the update. Please try again.") + + def remind_later(self): + """Close dialog and remind later.""" + self.reject() + + def skip_version(self): + """Skip this version (could save preference).""" + # TODO: Save preference to skip this version + self.reject() + + def show_error(self, title: str, message: str): + """Show error message to user.""" + from PySide6.QtWidgets import QMessageBox + QMessageBox.warning(self, title, message) + + def closeEvent(self, event): + """Handle dialog close event.""" + if self.download_thread and self.download_thread.isRunning(): + # Cancel download if in progress + self.download_thread.terminate() + self.download_thread.wait() + + event.accept() \ No newline at end of file diff --git a/jackify/frontends/gui/main.py b/jackify/frontends/gui/main.py index ce1bf65..f311011 100644 --- a/jackify/frontends/gui/main.py +++ b/jackify/frontends/gui/main.py @@ -528,6 +528,10 @@ class JackifyMainWindow(QMainWindow): from jackify.backend.services.protontricks_detection_service import ProtontricksDetectionService self.protontricks_service = ProtontricksDetectionService(steamdeck=self.system_info.is_steamdeck) + # Initialize update service + from jackify.backend.services.update_service import UpdateService + self.update_service = UpdateService(__version__) + debug_print(f"GUI Backend initialized - Steam Deck: {self.system_info.is_steamdeck}") def _is_steamdeck(self): @@ -735,6 +739,32 @@ class JackifyMainWindow(QMainWindow): print(f"Error checking protontricks: {e}") # Continue anyway - don't block startup on detection errors + def _check_for_updates_on_startup(self): + """Check for updates on startup in background thread""" + try: + debug_print("Checking for updates on startup...") + + def update_check_callback(update_info): + """Handle update check results""" + try: + if update_info: + debug_print(f"Update available: v{update_info.version}") + # Show update dialog + from jackify.frontends.gui.dialogs.update_dialog import UpdateDialog + dialog = UpdateDialog(update_info, self.update_service, self) + dialog.show() # Non-blocking + else: + debug_print("No updates available") + except Exception as e: + debug_print(f"Error showing update dialog: {e}") + + # Check for updates in background + self.update_service.check_for_updates_async(update_check_callback) + + except Exception as e: + debug_print(f"Error checking for updates on startup: {e}") + # Continue anyway - don't block startup on update check errors + def cleanup_processes(self): """Clean up any running processes before closing""" try: @@ -843,6 +873,9 @@ def main(): window = JackifyMainWindow(dev_mode=dev_mode) window.show() + # Start background update check after window is shown + window._check_for_updates_on_startup() + # Ensure cleanup on exit import atexit atexit.register(emergency_cleanup) diff --git a/jackify/shared/appimage_utils.py b/jackify/shared/appimage_utils.py new file mode 100644 index 0000000..59eb679 --- /dev/null +++ b/jackify/shared/appimage_utils.py @@ -0,0 +1,87 @@ +""" +AppImage utilities for self-updating functionality. + +This module provides utilities for detecting if Jackify is running as an AppImage +and getting the path to the current AppImage file. +""" + +import os +import sys +from pathlib import Path +from typing import Optional + + +def is_appimage() -> bool: + """ + Check if Jackify is currently running as an AppImage. + + Returns: + bool: True if running as AppImage, False otherwise + """ + return 'APPIMAGE' in os.environ + + +def get_appimage_path() -> Optional[Path]: + """ + Get the path to the current AppImage file. + + This uses the APPIMAGE environment variable set by the AppImage runtime. + This is the standard, reliable method for AppImage path detection. + + Returns: + Optional[Path]: Path to the AppImage file if running as 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) + + return None + + +def can_self_update() -> bool: + """ + Check if self-updating is possible. + + Returns: + bool: True if self-updating is possible, False otherwise + """ + appimage_path = get_appimage_path() + if not appimage_path: + return False + + # Check if we can write to the AppImage file (for replacement) + try: + return os.access(appimage_path, os.W_OK) + except (OSError, PermissionError): + return False + + +def get_appimage_info() -> dict: + """ + Get information about the current AppImage. + + Returns: + dict: Information about the AppImage including path, writability, etc. + """ + appimage_path = get_appimage_path() + + info = { + 'is_appimage': is_appimage(), + 'path': appimage_path, + 'can_update': can_self_update(), + 'size_mb': None, + 'writable': False + } + + if appimage_path and appimage_path.exists(): + try: + stat = appimage_path.stat() + info['size_mb'] = round(stat.st_size / (1024 * 1024), 1) + info['writable'] = os.access(appimage_path, os.W_OK) + except (OSError, PermissionError): + pass + + return info \ No newline at end of file