#!/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