#!/usr/bin/env bash
############################################################################
# Star Citizen Linux Users Group Helper Script
############################################################################
#
# Greetings, Space Penguin!
#
# This script is designed to help you run Star Citizen on Linux.
#
# Please see the project's github repo for more information:
# https://github.com/starcitizen-lug/lug-helper
#
# made with <3
# Author: https://github.com/the-sane
# Contributor: https://github.com/Termuellinator
# Contributor: https://github.com/pstn
# Contributor: https://github.com/gort818
# Contributor: https://github.com/victort
# Contributor: https://github.com/Wrzlprnft
# Contributor: https://github.com/LovingMelody
# Contributor: https://github.com/mactan-sc
# Contributor: https://github.com/ProjectSynchro
# Runner Downloader inspired by:
# https://github.com/richardtatum/sc-runner-updater
#
# License: GPLv3.0
############################################################################
# Check if script is run as root
if [ "$(id -u)" -eq 0 ]; then
echo "This script is not supposed to be run as root!"
exit 1
fi
# Check for dependencies
if [ ! -x "$(command -v curl)" ]; then
# Print to stderr and also try warning the user through notify-send
printf "lug-helper.sh: The required package 'curl' was not found on this system.\n" 1>&2
notify-send "lug-helper" "The required package 'curl' was not found on this system.\n" --icon=dialog-warning
exit 1
fi
if [ ! -x "$(command -v mktemp)" ] || [ ! -x "$(command -v chmod)" ] || [ ! -x "$(command -v sort)" ] || [ ! -x "$(command -v basename)" ] || [ ! -x "$(command -v realpath)" ] || [ ! -x "$(command -v dirname)" ] || [ ! -x "$(command -v cut)" ] || [ ! -x "$(command -v numfmt)" ] || [ ! -x "$(command -v tr)" ] || [ ! -x "$(command -v od)" ] || [ ! -x "$(command -v readlink)" ]; then
# coreutils
# Print to stderr and also try warning the user through notify-send
printf "lug-helper.sh: One or more required packages were not found on this system.\nPlease check that coreutils is installed!\n" 1>&2
notify-send "lug-helper" "One or more required packages were not found on this system.\nPlease check that coreutils is installed!\n" --icon=dialog-warning
exit 1
fi
if [ ! -x "$(command -v xargs)" ]; then
# findutils
# Print to stderr and also try warning the user through notify-send
printf "lug-helper.sh: One or more required packages were not found on this system.\nPlease check that the following findutils packages are installed:\n- xargs\n" 1>&2
notify-send "lug-helper" "One or more required packages were not found on this system.\nPlease check that the following findutils packages are installed:\n- xargs\n" --icon=dialog-warning
exit 1
fi
if [ ! -x "$(command -v cabextract)" ] || [ ! -x "$(command -v unzip)" ]; then
# winetricks dependencies
# Print to stderr and also try warning the user through notify-send
printf "lug-helper.sh: One or more required packages were not found on this system.\nPlease check that the following winetricks dependencies (or winetricks itself) are installed:\n- cabextract\n- unzip\n" 1>&2
notify-send "lug-helper" "One or more required packages were not found on this system.\nPlease check that the following winetricks dependencies (or winetricks itself) are installed:\n- cabextract\n- unzip\n" --icon=dialog-warning
exit 1
fi
######## Config ############################################################
wine_conf="winedir.conf"
game_conf="gamedir.conf"
firstrun_conf="firstrun.conf"
# Use XDG base directories if defined
if [ -f "${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs" ]; then
# Source the user's xdg directories
source "${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs"
fi
conf_dir="${XDG_CONFIG_HOME:-$HOME/.config}"
data_dir="${XDG_DATA_HOME:-$HOME/.local/share}"
# .config subdirectory
conf_subdir="starcitizen-lug"
# Helper directory
helper_dir="$(realpath "$0" | xargs -0 dirname)"
# Temporary directory
tmp_dir="$(mktemp -d -t "lughelper.XXXXXXXXXX")"
trap 'rm -r --interactive=never "$tmp_dir"' EXIT
# Set a maximum number of versions to display from each download url
max_download_items=25
######## Game Directories ##################################################
# The game's base directory name
sc_base_dir="StarCitizen"
# The default install location within a WINE prefix:
default_install_path="drive_c/Program Files/Roberts Space Industries"
# Remaining directory paths are set at the end of the getdirs() function
######## Bundled Files #####################################################
rsi_icon_name="rsi-launcher.png"
wine_launch_script_name="sc-launch.sh"
# Default to files in the Helper directory for a git download
rsi_icon="$helper_dir/$rsi_icon_name"
wine_launch_script="$helper_dir/lib/$wine_launch_script_name"
# Build our array of search paths, supporting packaged versions of this script
# Search XDG_DATA_DIRS and fall back to /usr/share/
IFS=':' read -r -a data_dirs_array <<< "$XDG_DATA_DIRS:/usr/share/"
# Locate our files in the search array
for searchdir in "${data_dirs_array[@]}"; do
# Check if we've found all our files and break the loop
if [ -f "$rsi_icon" ] && [ -f "$wine_launch_script" ]; then
break
fi
# rsi-launcher.png
if [ ! -f "$rsi_icon" ] && [ -f "$searchdir/icons/hicolor/256x256/apps/$rsi_icon_name" ]; then
rsi_icon="$searchdir/icons/hicolor/256x256/apps/$rsi_icon_name"
fi
# sc-launch.sh
if [ ! -f "$wine_launch_script" ] && [ -f "$searchdir/lug-helper/$wine_launch_script_name" ]; then
wine_launch_script="$searchdir/lug-helper/$wine_launch_script_name"
fi
done
######## Runners ###########################################################
# URLs for downloading Wine runners
# Elements in this array must be added in quoted pairs of: "description" "url"
# The first string in the pair is expected to contain the runner description
# The second is expected to contain the api releases url
# ie. "RawFox" "https://api.github.com/repos/rawfoxDE/raw-wine/releases"
runner_sources=(
"Kron4ek" "https://api.github.com/repos/Kron4ek/Wine-Builds/releases"
"RawFox" "https://api.github.com/repos/starcitizen-lug/raw-wine/releases"
"Mactan" "https://api.github.com/repos/mactan-sc/mactan-sc-wine/releases"
)
# Set the default runner to install when the system wine doesn't meet requirements
# default_runner_source corresponds to an EVEN NUMBER index in runner_sources above
default_runner="wine-10.5-amd64.tar.xz"
default_runner_source=0
######## Requirements ######################################################
# Wine minimum version
wine_required="9.4"
# Minimum amount of RAM in GiB
memory_required="16"
# Minimum amount of combined RAM + swap in GiB
memory_combined_required="40"
######## Links / Versions ##################################################
# LUG Wiki
lug_wiki="https://starcitizen-lug.github.io"
# NixOS section in Wiki
lug_wiki_nixos="https://github.com/starcitizen-lug/knowledge-base/wiki/Tips-and-Tricks#nixos"
# RSI Installer version and url
rsi_installer="RSI Launcher-Setup-2.4.0.exe"
rsi_installer_url="https://install.robertsspaceindustries.com/rel/2/$rsi_installer"
# Winetricks download url
winetricks_version="20250102"
winetricks_url="https://raw.githubusercontent.com/Winetricks/winetricks/refs/tags/$winetricks_version/src/winetricks"
# Github repo and script version info
repo="starcitizen-lug/lug-helper"
releases_url="https://github.com/$repo/releases"
current_version="v4.0"
############################################################################
############################################################################
############################################################################
# MARK: try_exec()
# Try to execute a supplied command with either user or root privileges
# Expects two string arguments
# Usage: try_exec [root|user] "command"
try_exec() {
# This function expects two string arguments
if [ "$#" -lt 2 ]; then
printf "\nScript error: The try_exec() function expects two arguments. Aborting.\n"
read -n 1 -s -p "Press any key..."
exit 0
fi
exec_type="$1"
exec_command="$2"
if [ "$exec_type" = "root" ]; then
# Use pollkit's pkexec for gui authentication with a fallback to sudo
if [ -x "$(command -v pkexec)" ]; then
pkexec sh -c "$exec_command"
# Check the exit status
if [ "$?" -eq 126 ] || [ "$?" -eq 127 ]; then
# User cancel or error
debug_print continue "pkexec returned an error. Falling back to sudo..."
else
# Successful execution, return here
return 0
fi
fi
# Fall back to sudo if pkexec is unavailable or returned an error
if [ -x "$(command -v sudo)" ]; then
sudo sh -c "$exec_command"
# Check the exit status
if [ "$?" -eq 1 ]; then
# Error
return 1
fi
else
# We don't know how to perform this operation with elevated privileges
printf "\nNeither Polkit nor sudo appear to be installed. Unable to execute the command with the required privileges.\n"
return 1
fi
elif [ "$exec_type" = "user" ]; then
sh -c "$exec_command"
# Check the exit status
if [ "$?" -eq 1 ]; then
# Error
return 1
fi
else
debug_print exit "Script Error: Invalid arguemnt passed to the try_exec function. Aborting."
fi
return 0
}
# MARK: debug_print()
# Echo a formatted debug message to the terminal and optionally exit
# Accepts either "continue" or "exit" as the first argument
# followed by the string to be echoed
debug_print() {
# This function expects two string arguments
if [ "$#" -lt 2 ]; then
printf "\nScript error: The debug_print function expects two arguments. Aborting.\n"
read -n 1 -s -p "Press any key..."
exit 0
fi
# Echo the provided string and, optionally, exit the script
case "$1" in
"continue")
printf "\n%s\n" "$2"
;;
"exit")
# Write an error to stderr and exit
printf "%s\n" "lug-helper.sh: $2" 1>&2
read -n 1 -s -p "Press any key..."
exit 1
;;
*)
printf "%s\n" "lug-helper.sh: Unknown argument provided to debug_print function. Aborting." 1>&2
read -n 1 -s -p "Press any key..."
exit 0
;;
esac
}
# MARK: message()
# Display a message to the user.
# Expects the first argument to indicate the message type, followed by
# a string of arguments that will be passed to zenity or echoed to the user.
#
# To call this function, use the following format: message [type] "[string]"
# See the message types below for instructions on formatting the string.
message() {
# Sanity check
if [ "$#" -lt 2 ]; then
debug_print exit "Script error: The message function expects at least two arguments. Aborting."
fi
# Use zenity messages if available
if [ "$use_zenity" -eq 1 ]; then
case "$1" in
"info")
# info message
# call format: message info "text to display"
margs=("--info" "--no-wrap" "--text=")
shift 1 # drop the message type argument and shift up to the text
;;
"warning")
# warning message
# call format: message warning "text to display"
margs=("--warning" "--text=")
shift 1 # drop the message type argument and shift up to the text
;;
"error")
# error message
# call format: message error "text to display"
margs=("--error" "--text=")
shift 1 # drop the message type argument and shift up to the text
;;
"question")
# question
# call format: if message question "question to ask?"; then...
margs=("--question" "--text=")
shift 1 # drop the message type argument and shift up to the text
;;
"options")
# formats the buttons with two custom options
# call format: if message options left_button_name right_button_name "which one do you want?"; then...
# The right button returns 0 (ok), the left button returns 1 (cancel)
if [ "$#" -lt 4 ]; then
debug_print exit "Script error: The options type in the message function expects four arguments. Aborting."
fi
margs=("--question" "--cancel-label=$2" "--ok-label=$3" "--text=")
shift 3 # drop the type and button label arguments and shift up to the text
;;
*)
debug_print exit "Script Error: Invalid message type passed to the message function. Aborting."
;;
esac
# Display the message
zenity "${margs[@]}""$@" --width="420" --title="Star Citizen LUG Helper"
else
# Fall back to text-based messages when zenity is not available
case "$1" in
"info")
# info message
# call format: message info "text to display"
printf "\n$2\n\n"
if [ "$cmd_line" != "true" ]; then
# Don't pause if we've been invoked via command line arguments
read -n 1 -s -p "Press any key..."
fi
;;
"warning")
# warning message
# call format: message warning "text to display"
printf "\n$2\n\n"
read -n 1 -s -p "Press any key..."
;;
"error")
# error message. Does not clear the screen
# call format: message error "text to display"
printf "\n$2\n\n"
read -n 1 -s -p "Press any key..."
;;
"question")
# question
# call format: if message question "question to ask?"; then...
printf "\n$2\n"
while read -p "[y/n]: " yn; do
case "$yn" in
[Yy]*)
return 0
;;
[Nn]*)
return 1
;;
*)
printf "Please type 'y' or 'n'\n"
;;
esac
done
;;
"options")
# Choose from two options
# call format: if message options left_button_name right_button_name "which one do you want?"; then...
printf "\n$4\n1: $3\n2: $2\n"
while read -p "[1/2]: " option; do
case "$option" in
1*)
return 0
;;
2*)
return 1
;;
*)
printf "Please type '1' or '2'\n"
;;
esac
done
;;
*)
debug_print exit "Script Error: Invalid message type passed to the message function. Aborting."
;;
esac
fi
}
# MARK: menu()
# Display a menu to the user.
# Uses Zenity for a gui menu with a fallback to plain old text.
#
# How to call this function:
#
# Requires the following variables:
# - The array "menu_options" should contain the strings of each option.
# - The array "menu_actions" should contain function names to be called.
# - The strings "menu_text_zenity" and "menu_text_terminal" should contain
# the menu description formatted for zenity and the terminal, respectively.
# This text will be displayed above the menu options.
# Zenity supports Pango Markup for text formatting.
# - The integer "menu_height" specifies the height of the zenity menu.
# - The string "menu_type" should contain either "radiolist" or "checklist".
# - The string "cancel_label" should contain the text of the cancel button.
#
# The final element in each array is expected to be a quit option.
#
# IMPORTANT: The indices of the elements in "menu_actions"
# *MUST* correspond to the indeces in "menu_options".
# In other words, it is expected that menu_actions[1] is the correct action
# to be executed when menu_options[1] is selected, and so on for each element.
#
# See MAIN at the bottom of this script for an example of generating a menu.
menu() {
# Sanity checks
if [ "${#menu_options[@]}" -eq 0 ]; then
debug_print exit "Script error: The array 'menu_options' was not set before calling the menu function. Aborting."
elif [ "${#menu_actions[@]}" -eq 0 ]; then
debug_print exit "Script error: The array 'menu_actions' was not set before calling the menu function. Aborting."
elif [ -z "$menu_text_zenity" ]; then
debug_print exit "Script error: The string 'menu_text_zenity' was not set before calling the menu function. Aborting."
elif [ -z "$menu_text_terminal" ]; then
debug_print exit "Script error: The string 'menu_text_terminal' was not set before calling the menu function. Aborting."
elif [ -z "$menu_height" ]; then
debug_print exit "Script error: The string 'menu_height' was not set before calling the menu function. Aborting."
elif [ "$menu_type" != "radiolist" ] && [ "$menu_type" != "checklist" ]; then
debug_print exit "Script error: Unknown menu_type in menu() function. Aborting."
elif [ -z "$cancel_label" ]; then
debug_print exit "Script error: The string 'cancel_label' was not set before calling the menu function. Aborting."
fi
# Use Zenity if it is available
if [ "$use_zenity" -eq 1 ]; then
# Format the options array for Zenity by adding
# TRUE or FALSE to indicate default selections
# ie: "TRUE" "List item 1" "FALSE" "List item 2" "FALSE" "List item 3"
unset zen_options
for (( i=0; i<"${#menu_options[@]}"-1; i++ )); do
if [ "$i" -eq 0 ]; then
# Set the first element
if [ "$menu_type" = "radiolist" ]; then
# Select the first radio button by default
zen_options=("TRUE")
else
# Don't select the first checklist item
zen_options=("FALSE")
fi
else
# Deselect all remaining items
zen_options+=("FALSE")
fi
# Add the menu list item
zen_options+=("${menu_options[i]}")
done
# Display the zenity radio button menu
choice="$(zenity --list --"$menu_type" --width="510" --height="$menu_height" --text="$menu_text_zenity" --title="Star Citizen LUG Helper" --hide-header --cancel-label "$cancel_label" --column="" --column="Option" "${zen_options[@]}")"
# Match up choice with an element in menu_options
matched="false"
if [ "$menu_type" = "radiolist" ]; then
# Loop through the options array to match the chosen option
for (( i=0; i<"${#menu_options[@]}"; i++ )); do
if [ "$choice" = "${menu_options[i]}" ]; then
# Execute the corresponding action for a radiolist menu
${menu_actions[i]}
matched="true"
break
fi
done
elif [ "$menu_type" = "checklist" ]; then
# choice will be empty if no selection was made
# Unfortunately, it's also empty when the user presses cancel
# so we can't differentiate between those two states
# Convert choice string to array elements for checklists
IFS='|' read -r -a choices <<< "$choice"
# Fetch the function to be called
function_call="$(echo "${menu_actions[0]}" | awk '{print $1}')"
# Loop through the options array to match the chosen option(s)
unset arguments_array
for (( i=0; i<"${#menu_options[@]}"; i++ )); do
for (( j=0; j<"${#choices[@]}"; j++ )); do
if [ "${choices[j]}" = "${menu_options[i]}" ]; then
arguments_array+=("$(echo "${menu_actions[i]}" | awk '{print $2}')")
matched="true"
fi
done
done
# Call the function with all matched elements as arguments
if [ "$matched" = "true" ]; then
$function_call "${arguments_array[@]}"
fi
fi
# If no match was found, the user clicked cancel
if [ "$matched" = "false" ]; then
# Execute the last option in the actions array
"${menu_actions[${#menu_actions[@]}-1]}"
fi
else
# Use a text menu if Zenity is not available
clear
printf "\n$menu_text_terminal\n\n"
PS3="Enter selection number: "
select choice in "${menu_options[@]}"
do
# Loop through the options array to match the chosen option
matched="false"
for (( i=0; i<"${#menu_options[@]}"; i++ )); do
if [ "$choice" = "${menu_options[i]}" ]; then
clear
# Execute the corresponding action
${menu_actions[i]}
matched="true"
break
fi
done
# Check if we're done looping the menu
if [ "$matched" = "true" ]; then
# Match was found and actioned, so exit the menu
break
else
# If no match was found, the user entered an invalid option
printf "\nInvalid selection.\n"
continue
fi
done
fi
}
# MARK: menu_loop_done()
# Called when the user clicks cancel on a looping menu
# Causes a return to the main menu
menu_loop_done() {
looping_menu="false"
}
# MARK: getdirs()
# Get paths to the user's wine prefix, game directory, and a backup directory
# Returns 3 if the user was asked to select new directories
getdirs() {
# Sanity checks
if [ ! -d "$conf_dir" ]; then
message error "Config directory not found. The Helper is unable to proceed.\n\n$conf_dir"
return 1
fi
if [ ! -d "$conf_dir/$conf_subdir" ]; then
mkdir -p "$conf_dir/$conf_subdir"
fi
# Initialize a return value
retval=0
# Check if the config files already exist
if [ -f "$conf_dir/$conf_subdir/$wine_conf" ]; then
wine_prefix="$(cat "$conf_dir/$conf_subdir/$wine_conf")"
if [ ! -d "$wine_prefix" ]; then
debug_print continue "The saved wine prefix does not exist, ignoring."
wine_prefix=""
rm --interactive=never "${conf_dir:?}/$conf_subdir/$wine_conf"
fi
fi
if [ -f "$conf_dir/$conf_subdir/$game_conf" ]; then
game_path="$(cat "$conf_dir/$conf_subdir/$game_conf")"
# Note: We check for the parent dir here because the game may not have been fully installed yet
# which means sc_base_dir may not yet have been created. But the parent RSI dir must exist
if [ ! -d "$(dirname "$game_path")" ] || [ "$(basename "$game_path")" != "$sc_base_dir" ]; then
debug_print continue "Unexpected game path found in config file, ignoring."
game_path=""
rm --interactive=never "${conf_dir:?}/$conf_subdir/$game_conf"
fi
fi
# If we don't have the directory paths we need yet,
# ask the user to provide them
if [ -z "$wine_prefix" ] || [ -z "$game_path" ]; then
message info "At the next screen, please select the directory where you installed Star Citizen (your Wine prefix)\nIt will be remembered for future use.\n\nDefault install path: ~/Games/star-citizen"
if [ "$use_zenity" -eq 1 ]; then
# Using Zenity file selection menus
# Get the wine prefix directory
while [ -z "$wine_prefix" ]; do
wine_prefix="$(zenity --file-selection --directory --title="Select your Star Citizen Wine prefix directory" --filename="$HOME/Games/star-citizen" 2>/dev/null)"
if [ "$?" -eq -1 ]; then
message error "An unexpected error has occurred. The Helper is unable to proceed."
return 1
elif [ -z "$wine_prefix" ]; then
# User clicked cancel
message warning "Operation cancelled.\nNo changes have been made to your game."
return 1
fi
if ! message question "You selected:\n\n$wine_prefix\n\nIs this correct?"; then
wine_prefix=""
fi
done
# Get the game path
if [ -z "$game_path" ]; then
if [ -d "$wine_prefix/$default_install_path" ]; then
# Default: prefix/drive_c/Program Files/Roberts Space Industries/StarCitizen
game_path="$wine_prefix/$default_install_path/$sc_base_dir"
else
message info "Unable to detect the default game install path!\n\n$wine_prefix/$default_install_path/$sc_base_dir\n\nDid you change the install location in the RSI Setup?\nDoing that is generally a bad idea but, if you are sure you want to proceed,\nselect your '$sc_base_dir' game directory on the next screen"
while true; do
game_path="$(zenity --file-selection --directory --title="Select your Star Citizen directory" --filename="$wine_prefix/$default_install_path" 2>/dev/null)"
if [ "$?" -eq -1 ]; then
message error "An unexpected error has occurred. The Helper is unable to proceed."
return 1
elif [ -z "$game_path" ]; then
# User clicked cancel or something else went wrong
message warning "Operation cancelled.\nNo changes have been made to your game."
return 1
elif [ "$(basename "$game_path")" != "$sc_base_dir" ]; then
message warning "You must select the base game directory named '$sc_base_dir'\n\nie. [prefix]/drive_c/Program Files/Roberts Space Industries/StarCitizen"
else
# All good
break
fi
done
fi
fi
else
# No Zenity, use terminal-based menus
clear
# Get the wine prefix directory
if [ -z "$wine_prefix" ]; then
printf "Enter the full path to your Star Citizen Wine prefix directory (case sensitive)\n"
printf "ie. /home/USER/Games/star-citizen\n"
while read -rp ": " wine_prefix; do
if [ ! -d "$wine_prefix" ]; then
printf "That directory is invalid or does not exist. Please try again.\n\n"
else
break
fi
done
fi
# Get the game path
if [ -z "$game_path" ]; then
if [ -d "$wine_prefix/$default_install_path/s" ]; then
# Default: prefix/drive_c/Program Files/Roberts Space Industries/StarCitizen
game_path="$wine_prefix/$default_install_path/$sc_base_dir"
else
printf "\nUnable to detect the default game install path!\nDid you change the install location in the RSI Setup?\nDoing that is generally a bad idea but, if you are sure you want to proceed...\n\n"
printf "Enter the full path to your $sc_base_dir installation directory (case sensitive)\n"
printf "ie. /home/USER/Games/star-citizen/drive_c/Program Files/Roberts Space Industries/StarCitizen\n"
while read -rp ": " game_path; do
if [ ! -d "$game_path" ]; then
printf "That directory is invalid or does not exist. Please try again.\n\n"
elif [ "$(basename "$game_path")" != "$sc_base_dir" ]; then
printf "You must enter the full path to the directory named '%s'\n\n" "$sc_base_dir"
else
break
fi
done
fi
fi
fi
# Set a return code to indicate to other functions in this script that the user had to select new directories here
retval=3
fi
# Save the paths to config files
if [ ! -f "$conf_dir/$conf_subdir/$wine_conf" ]; then
echo "$wine_prefix" > "$conf_dir/$conf_subdir/$wine_conf"
fi
if [ ! -f "$conf_dir/$conf_subdir/$game_conf" ]; then
echo "$game_path" > "$conf_dir/$conf_subdir/$game_conf"
fi
return "$retval"
}
############################################################################
######## begin preflight check functions ###################################
############################################################################
# MARK: preflight_check()
# Check that the system is optimized for Star Citizen
# Accepts an optional string argument, "wine"
# This argument is used by the install functions to indicate which
# Preflight Check functions should be called and cause the Preflight Check
# to only output problems that must be fixed
#
# There are two options for automatically fixing problems:
# See existing functions for examples of setting
# preflight_root_actions or preflight_user_actions
preflight_check() {
# Initialize variables
unset preflight_pass
unset preflight_fail
unset preflight_action_funcs
unset preflight_root_actions
unset preflight_user_actions
unset preflight_fix_results
unset preflight_manual
unset preflight_followup
unset preflight_fail_string
unset preflight_pass_string
unset preflight_fix_results_string
unset install_mode
retval=0
# Capture optional argument that determines which install function called us
install_mode="$1"
# Check the optional argument for valid values
if [ -n "$install_mode" ] && [ "$install_mode" != "wine" ]; then
debug_print exit "Script error: Unexpected argument passed to the preflight_check function. Aborting."
fi
# Call the optimization functions to perform the checks
wine_check
memory_check
avx_check
mapcount_check
filelimit_check
# Populate info strings with the results and add formatting
if [ "${#preflight_fail[@]}" -gt 0 ]; then
# Failed checks
preflight_fail_string="Failed Checks:"
for (( i=0; i<"${#preflight_fail[@]}"; i++ )); do
if [ "$i" -eq 0 ]; then
preflight_fail_string="$preflight_fail_string\n- ${preflight_fail[i]//\\n/\\n }"
else
preflight_fail_string="$preflight_fail_string\n\n- ${preflight_fail[i]//\\n/\\n }"
fi
done
# Add extra newlines if there are also passes to report
if [ "${#preflight_pass[@]}" -gt 0 ]; then
preflight_fail_string="$preflight_fail_string\n\n"
fi
fi
if [ "${#preflight_pass[@]}" -gt 0 ]; then
# Passed checks
preflight_pass_string="Passed Checks:"
for (( i=0; i<"${#preflight_pass[@]}"; i++ )); do
preflight_pass_string="$preflight_pass_string\n- ${preflight_pass[i]//\\n/\\n }"
done
fi
for (( i=0; i<"${#preflight_manual[@]}"; i++ )); do
# Instructions for manually fixing problems
if [ "$i" -eq 0 ]; then
preflight_manual_string="${preflight_manual[i]}"
else
preflight_manual_string="$preflight_manual_string\n\n${preflight_manual[i]}"
fi
done
# Format a message heading
message_heading="Preflight Check Results"
if [ "$use_zenity" -eq 1 ]; then
message_heading="$message_heading"
fi
# Display the results of the preflight check
if [ -z "$preflight_fail_string" ]; then
# If install_mode was set by an install function, we won't bother the user when all checks pass
if [ -z "$install_mode" ]; then
# All checks pass!
message info "$message_heading\n\nYour system is optimized for Star Citizen!\n\n$preflight_pass_string"
fi
return 0
else
if [ "${#preflight_action_funcs[@]}" -eq 0 ]; then
# We have failed checks, but they're issues we can't automatically fix
message warning "$message_heading\n\n$preflight_fail_string$preflight_pass_string"
elif message question "$message_heading\n\n$preflight_fail_string$preflight_pass_string\n\nWould you like these configuration issues to be fixed for you?"; then
# We have failed checks, but we can fix them for the user
# Call functions to build fixes for any issues found
for (( i=0; i<"${#preflight_action_funcs[@]}"; i++ )); do
${preflight_action_funcs[i]}
done
# Populate a string of actions to be executed with root privileges
for (( i=0; i<"${#preflight_root_actions[@]}"; i++ )); do
if [ "$i" -eq 0 ]; then
preflight_root_actions_string="${preflight_root_actions[i]}"
else
preflight_root_actions_string="$preflight_root_actions_string; ${preflight_root_actions[i]}"
fi
done
# Populate a string of actions to be executed with user privileges
for (( i=0; i<"${#preflight_user_actions[@]}"; i++ )); do
if [ "$i" -eq 0 ]; then
preflight_user_actions_string="${preflight_user_actions[i]}"
else
preflight_user_actions_string="$preflight_user_actions_string; ${preflight_user_actions[i]}"
fi
done
# Execute the root privilege actions set by the functions
if [ -n "$preflight_root_actions_string" ]; then
# Try to execute the actions as root
try_exec root "$preflight_root_actions_string"
if [ "$?" -eq 1 ]; then
message error "The Preflight Check was unable to finish fixing problems.\nDid authentication fail? See terminal for more information.\n\nReturning to main menu."
return 0
fi
fi
# Execute the user privilege actions set by the functions
if [ -n "$preflight_user_actions_string" ]; then
# Try to execute the actions as root
try_exec user "$preflight_user_actions_string"
if [ "$?" -eq 1 ]; then
message error "The Preflight Check was unable to finish fixing problems.\nSee terminal for more information.\n\nReturning to main menu."
return 0
fi
fi
# Call any followup functions
for (( i=0; i<"${#preflight_followup[@]}"; i++ )); do
${preflight_followup[i]}
done
# Populate the results string
for (( i=0; i<"${#preflight_fix_results[@]}"; i++ )); do
if [ "$i" -eq 0 ]; then
preflight_fix_results_string="${preflight_fix_results[i]}"
else
preflight_fix_results_string="$preflight_fix_results_string\n\n${preflight_fix_results[i]}"
fi
done
# Display the results
message info "$preflight_fix_results_string"
else
# User declined to automatically fix configuration issues
# Show manual configuration options
if [ -n "$preflight_manual_string" ]; then
message info "$preflight_manual_string"
fi
fi
return 1
fi
}
# MARK: wine_check()
# Check the system Wine version
# Tells the preflight check whether or not wine is installed
# Additionally sets system_wine_ok if system wine meets the minimum version requirement
wine_check() {
# Initialize variable
system_wine_ok="false"
# Is wine installed?
if [ ! -x "$(command -v wine)" ]; then
preflight_fail+=("Wine does not appear to be installed.\nPlease refer to our Quick Start Guide:\n$lug_wiki")
return 1
else
preflight_pass+=("Wine is installed on your system.")
fi
# Get the current wine version
wine_current="$(wine --version 2>/dev/null | awk '{print $1}' | awk -F '-' '{print $2}')"
# Check it against the required version
if [ -z "$wine_current" ]; then
system_wine_ok="false"
elif [ "$wine_required" != "$wine_current" ] &&
[ "$wine_current" = "$(printf "%s\n%s" "$wine_current" "$wine_required" | sort -V | head -n1)" ]; then
system_wine_ok="false"
else
system_wine_ok="true"
fi
}
# MARK: memory_check()
# Check system memory and swap space
memory_check() {
# Get totals in bytes
memtotal="$(LC_NUMERIC=C awk '/MemTotal/ {printf $2}' /proc/meminfo)"
swaptotal="$(LC_NUMERIC=C awk '/SwapTotal/ {printf $2}' /proc/meminfo)"
memtotal="$(($memtotal * 1024))"
swaptotal="$(($swaptotal * 1024))"
combtotal="$(($memtotal + $swaptotal))"
# Convert to whole number GiB
memtotal="$(numfmt --to=iec-i --format="%.0f" --suffix="B" "$memtotal")"
swaptotal="$(numfmt --to=iec-i --format="%.0f" --suffix="B" "$swaptotal")"
combtotal="$(numfmt --to=iec-i --format="%.0f" --suffix="B" "$combtotal")"
if [ "${memtotal: -3}" != "GiB" ] || [ "${memtotal::-3}" -lt "$(($memory_required-1))" ]; then
# Minimum requirements are not met
preflight_fail+=("Your system has $memtotal of memory.\n${memory_required}GiB is the minimum required to avoid crashes.")
elif [ "${memtotal::-3}" -ge "$memory_combined_required" ]; then
# System has sufficient RAM
preflight_pass+=("Your system has $memtotal of memory.")
elif [ "${combtotal::-3}" -ge "$memory_combined_required" ]; then
# System has sufficient combined RAM + swap
preflight_pass+=("Your system has $memtotal memory and $swaptotal swap.")
else
# Recommend swap
swap_recommended="$(($memory_combined_required - ${memtotal::-3}))"
preflight_fail+=("Your system has $memtotal memory and $swaptotal swap.\nWe recommend at least ${swap_recommended}GiB swap to avoid crashes.")
fi
}
# MARK: avx_check()
# Check CPU for the required AVX extension
avx_check() {
if grep -q "avx" /proc/cpuinfo; then
preflight_pass+=("Your CPU supports the necessary AVX instruction set.")
else
preflight_fail+=("Your CPU does not appear to support AVX instructions.\nThis requirement was added to Star Citizen in version 3.11")
fi
}
############################################################################
######## begin mapcount functions ##########################################
############################################################################
# MARK: mapcount_check()
# Check vm.max_map_count for the correct setting
mapcount_check() {
mapcount="$(cat /proc/sys/vm/max_map_count)"
# Add to the results and actions arrays
if [ "$mapcount" -ge 16777216 ]; then
# All good
preflight_pass+=("vm.max_map_count is set to $mapcount.")
elif grep -E -x -q "vm.max_map_count" /etc/sysctl.conf /etc/sysctl.d/* 2>/dev/null; then
# Was it supposed to have been set by sysctl?
preflight_fail+=("vm.max_map_count is configured to at least 16777216 but the setting has not been loaded by your system.")
# Add the function that will be called to change the configuration
preflight_action_funcs+=("mapcount_once")
# Add info for manually changing the setting
preflight_manual+=("To change vm.max_map_count until the next reboot, run:\nsudo sysctl -w vm.max_map_count=16777216")
else
# The setting should be changed
preflight_fail+=("vm.max_map_count is $mapcount\nand should be set to at least 16777216\nto give the game access to sufficient memory.")
# Add the function that will be called to change the configuration
preflight_action_funcs+=("mapcount_set")
# Add info for manually changing the setting
if [ -d "/etc/sysctl.d" ]; then
# Newer versions of sysctl
preflight_manual+=("To change vm.max_map_count permanently, add the following line to\n'/etc/sysctl.d/99-starcitizen-max_map_count.conf' and reload with 'sudo sysctl --system'\n vm.max_map_count = 16777216\n\nOr, to change vm.max_map_count temporarily until next boot, run:\n sudo sysctl -w vm.max_map_count=16777216")
else
# Older versions of sysctl
preflight_manual+=("To change vm.max_map_count permanently, add the following line to\n'/etc/sysctl.conf' and reload with 'sudo sysctl -p':\n vm.max_map_count = 16777216\n\nOr, to change vm.max_map_count temporarily until next boot, run:\n sudo sysctl -w vm.max_map_count=16777216")
fi
fi
}
# MARK: mapcount_set()
# Set vm.max_map_count
mapcount_set() {
if [ -d "/etc/sysctl.d" ]; then
# Newer versions of sysctl
preflight_root_actions+=('printf "\n# Added by LUG-Helper:\nvm.max_map_count = 16777216\n" > /etc/sysctl.d/99-starcitizen-max_map_count.conf && sysctl --quiet --system')
preflight_fix_results+=("The vm.max_map_count configuration has been added to:\n/etc/sysctl.d/99-starcitizen-max_map_count.conf")
else
# Older versions of sysctl
preflight_root_actions+=('printf "\n# Added by LUG-Helper:\nvm.max_map_count = 16777216" >> /etc/sysctl.conf && sysctl -p')
preflight_fix_results+=("The vm.max_map_count configuration has been added to:\n/etc/sysctl.conf")
fi
# Verify that the setting took effect
preflight_followup+=("mapcount_confirm")
}
# MARK: mapcount_once()
# Sets vm.max_map_count for the current session only
mapcount_once() {
preflight_root_actions+=('sysctl -w vm.max_map_count=16777216')
preflight_fix_results+=("vm.max_map_count was changed until the next boot.")
preflight_followup+=("mapcount_confirm")
}
# MARK: mapcount_confirm()
# Check if setting vm.max_map_count was successful
mapcount_confirm() {
if [ "$(cat /proc/sys/vm/max_map_count)" -lt 16777216 ]; then
preflight_fix_results+=("WARNING: As far as this Helper can detect, vm.max_map_count\nwas not successfully configured on your system.\nYou will most likely experience crashes.")
fi
}
############################################################################
######## end mapcount functions ############################################
############################################################################
############################################################################
######## begin filelimit functions #########################################
############################################################################
# MARK: filelimit_check()
# Check the open file descriptors limit
filelimit_check() {
filelimit="$(ulimit -Hn)"
# Add to the results and actions arrays
if [ "$filelimit" -ge 524288 ]; then
# All good
preflight_pass+=("Hard open file descriptors limit is set to $filelimit.")
else
# The file limit should be changed
preflight_fail+=("Your hard open file descriptors limit is $filelimit\nand should be set to at least 524288\nto increase the maximum number of open files.")
# Add the function that will be called to change the configuration
preflight_action_funcs+=("filelimit_set")
# Add info for manually changing the settings
if [ -f "/etc/systemd/system.conf" ]; then
# Using systemd
preflight_manual+=("To change your open file descriptors limit, add the following to\n'/etc/systemd/system.conf.d/99-starcitizen-filelimit.conf':\n\n[Manager]\nDefaultLimitNOFILE=524288")
elif [ -f "/etc/security/limits.conf" ]; then
# Using limits.conf
preflight_manual+=("To change your open file descriptors limit, add the following line to\n'/etc/security/limits.conf':\n * hard nofile 524288")
else
# Don't know what method to use
preflight_manual+=("This Helper is unable to detect the correct method of setting\nthe open file descriptors limit on your system.\n\nWe recommend manually configuring this limit to at least 524288.")
fi
fi
}
# MARK: filelimit_set()
# Set the open file descriptors limit
filelimit_set() {
if [ -f "/etc/systemd/system.conf" ]; then
# Using systemd
# Append to the file
preflight_root_actions+=('mkdir -p /etc/systemd/system.conf.d && printf "[Manager]\n# Added by LUG-Helper:\nDefaultLimitNOFILE=524288\n" > /etc/systemd/system.conf.d/99-starcitizen-filelimit.conf && systemctl daemon-reexec')
preflight_fix_results+=("The open files limit configuration has been added to:\n/etc/systemd/system.conf.d/99-starcitizen-filelimit.conf")
elif [ -f "/etc/security/limits.conf" ]; then
# Using limits.conf
# Insert before the last line in the file
preflight_root_actions+=('sed -i "\$i#Added by LUG-Helper:" /etc/security/limits.conf; sed -i "\$i* hard nofile 524288" /etc/security/limits.conf')
preflight_fix_results+=("The open files limit configuration has been appended to:\n/etc/security/limits.conf")
else
# Don't know what method to use
preflight_fix_results+=("This Helper is unable to detect the correct method of setting\nthe open file descriptors limit on your system.\n\nWe recommend manually configuring this limit to at least 524288.")
fi
# Verify that setting the limit was successful
preflight_followup+=("filelimit_confirm")
}
# MARK: filelimit_confirm()
# Check if setting the open file descriptors limit was successful
filelimit_confirm() {
if [ "$(ulimit -Hn)" -lt 524288 ]; then
preflight_fix_results+=("WARNING: As far as this Helper can detect, the open files limit\nwas not successfully configured on your system.\nYou may experience crashes.")
fi
}
############################################################################
######## end filelimit functions ###########################################
############################################################################
############################################################################
######## end preflight check functions #####################################
############################################################################
############################################################################
######## begin download functions ##########################################
############################################################################
# MARK: download_manage()
# Manage downloads. Called by a dedicated download type manage function, ie runner_manage_wine()
#
# This function expects the following variables to be set:
#
# - The string download_sources is a formatted array containing the URLs
# of items to download. It should be pointed to the appropriate
# array set at the top of the script using indirect expansion.
# See runner_sources at the top and runner_manage_wine() for examples.
# - The array download_dirs should contain the locations the downloaded item
# will be installed to. Must be formatted in pairs of ("[type]" "[directory]")
# - The string "download_menu_heading" should contain the type of item
# being downloaded. It will appear in the menu heading.
# - The string "download_menu_description" should contain a description of
# the item being downloaded. It will appear in the menu subheading.
# - The integer "download_menu_height" specifies the height of the zenity menu.
#
# This function also expects one string argument containing the type of item to
# be downloaded. ie. runner or dxvk.
#
# See runner_manage_wine() for a configuration example.
download_manage() {
# This function expects a string to be passed as an argument
if [ -z "$1" ]; then
debug_print exit "Script error: The download_manage function expects a string argument. Aborting."
fi
# Sanity checks
if [ -z "$download_sources" ]; then
debug_print exit "Script error: The string 'download_sources' was not set before calling the download_manage function. Aborting."
elif [ "${#download_dirs[@]}" -eq 0 ]; then
debug_print exit "Script error: The array 'download_dirs' was not set before calling the download_manage function. Aborting."
elif [ -z "$download_menu_heading" ]; then
debug_print exit "Script error: The string 'download_menu_heading' was not set before calling the download_manage function. Aborting."
elif [ -z "$download_menu_description" ]; then
debug_print exit "Script error: The string 'download_menu_description' was not set before calling the download_manage function. Aborting."
elif [ -z "$download_menu_height" ]; then
debug_print exit "Script error: The string 'download_menu_height' was not set before calling the download_manage function. Aborting."
fi
# Get the type of item we're downloading from the function arguments
download_type="$1"
# The download management menu will loop until the user cancels
looping_menu="true"
while [ "$looping_menu" = "true" ]; do
# Configure the menu
menu_text_zenity="Manage Your $download_menu_heading\n\n$download_menu_description\n\nYou may choose from the following options:"
menu_text_terminal="Manage Your $download_menu_heading\n\n$download_menu_description\nYou may choose from the following options:"
menu_text_height="$download_menu_height"
menu_type="radiolist"
# Configure the menu options
delete="Remove an installed $download_type"
back="Return to the main menu"
unset menu_options
unset menu_actions
# Initialize success
unset download_action_success
# Loop through the download_sources array and create a menu item
# for each one. Even numbered elements will contain the item name
for (( i=0; i<"${#download_sources[@]}"; i=i+2 )); do
# Set the options to be displayed in the menu
menu_options+=("Install a $download_type from ${download_sources[i]}")
# Set the corresponding functions to be called for each of the options
menu_actions+=("download_select_install $i")
done
# Complete the menu by adding options to uninstall an item
# or go back to the previous menu
menu_options+=("$delete" "$back")
menu_actions+=("download_select_delete" "menu_loop_done")
# Calculate the total height the menu should be
# menu_option_height = pixels per menu option
# #menu_options[@] = number of menu options
# menu_text_height = height of the title/description text
# menu_text_height_zenity4 = added title/description height for libadwaita bigness
menu_height="$(($menu_option_height * ${#menu_options[@]} + $menu_text_height + $menu_text_height_zenity4))"
# Set the label for the cancel button
cancel_label="Go Back"
# Call the menu function. It will use the options as configured above
menu
# Perform post-download actions and display messages or instructions
if [ -n "$download_action_success" ] && [ "$post_download_type" != "none" ]; then
post_download
fi
done
}
# MARK: runner_manage_wine()
# Configure the download_manage function for wine runners
runner_manage_wine() {
# We'll want to instruct the user on how to use the downloaded runner
# Valid options are "none", "info", or "configure-wine"
post_download_type="configure-wine"
# Use indirect expansion to point download_sources
# to the runner_sources array set at the top of the script
declare -n download_sources=runner_sources
# Get directories so we know where the wine prefix is
getdirs
# Set the download directory for wine runners
# Only installing to one directory is supported
# Do not include multiple download destinations in this array
# Must be formatted in pairs of ("[type]" "[directory]")
download_dirs=("wine" "$wine_prefix/runners")
# Configure the text displayed in the menus
download_menu_heading="Wine Runners"
download_menu_description="The runners listed below are wine builds created for Star Citizen"
download_menu_height="320"
# Configure the post install and delete messages
# Format:
# post_install_msg is displayed below the header
# post_delete_msg is displayed with no header
post_install_msg_heading="Download Complete"
post_install_msg="The launch script needs to be updated\n\nWould you like to automatically configure it to use this runner?"
post_delete_msg="The launch script needs to be updated\n\nWould you like to automatically revert to using your system wine?"
# Set the string sed will match against when editing the launch script
# This will be used to detect the appropriate variable and replace its value
# with the path to the downloaded item
post_download_sed_string="export wine_path="
# Set the value of the above variable that will be restored after a runner is deleted
# In this case, we want to revert to calling system wine
post_delete_restore_value="$(command -v wine | xargs dirname)"
# Call the download_manage function with the above configuration
# The argument passed to the function is used for special handling
# and displayed in the menus and dialogs.
download_manage "runner"
}
# MARK: download_select_install()
# List available items for download. Called by download_manage()
#
# The following variables are expected to be set before calling this function:
# - download_sources (array)
# - download_type (string)
# - download_dirs (array)
download_select_install() {
# This function expects an element number for the sources array to be passed in as an argument
if [ -z "$1" ]; then
debug_print exit "Script error: The download_select_install function expects a numerical argument. Aborting."
fi
# Sanity checks
if [ "${#download_sources[@]}" -eq 0 ]; then
debug_print exit "Script error: The array 'download_sources' was not set before calling the download_select_install function. Aborting."
elif [ -z "$download_type" ]; then
debug_print exit "Script error: The string 'download_type' was not set before calling the download_select_install function. Aborting."
elif [ "${#download_dirs[@]}" -eq 0 ]; then
debug_print exit "Script error: The array 'download_dirs' was not set before calling the download_select_install function. Aborting."
fi
# Store info from the selected contributor
contributor_name="${download_sources[$1]}"
contributor_url="${download_sources[$1+1]}"
# For runners, check GlibC version against runner requirements
if [ "$download_type" = "runner" ] && { [ "$contributor_name" = "TKG" ] || [ "$contributor_name" = "RawFox" ] || [ "$contributor_name" = "Mactan" ]; }; then
glibc_fail="false"
required_glibc="2.38"
# Check the system glibc
if [ -x "$(command -v ldd)" ]; then
system_glibc="$(ldd --version | awk '/ldd/{print $NF}')"
else
system_glibc="0 (Not installed)"
fi
# Sort the versions and check if the installed glibc is smaller
if [ "$required_glibc" != "$system_glibc" ] &&
[ "$system_glibc" = "$(printf "%s\n%s" "$system_glibc" "$required_glibc" | sort -V | head -n1)" ]; then
glibc_fail="true"
fi
# Display a warning message
if [ "$glibc_fail" = "true" ]; then
message warning "Your glibc version is incompatible with the selected runner\n\nSystem glibc: ${system_glibc}\nMinimum required glibc: $required_glibc"
return 1
fi
fi
# Check the provided contributor url to make sure we know how to handle it
# To add new sources, add them here and handle in the if statement
# just below and in the download_install function
case "$contributor_url" in
https://api.github.com/*)
download_url_type="github"
;;
https://gitlab.com/api/v4/projects/*)
download_url_type="gitlab"
;;
*)
debug_print exit "Script error: Unknown api/url format in ${download_type}_sources array. Aborting."
;;
esac
# Set the search keys we'll use to parse the api for the download url
# To add new sources, handle them here, in the if statement
# just above, and in the download_install function
if [ "$download_url_type" = "github" ]; then
# Which json key are we looking for?
search_key="browser_download_url"
# Optional: Only match urls containing a keyword
match_url_keyword=""
# Optional: Filter out game-specific builds by keyword
# Format for grep extended regex (ie: "word1|word2|word3")
if [ "$download_type" = "runner" ] && [ "$contributor_name" = "GloriousEggroll" ]; then
filter_keywords="lol|diablo"
elif [ "$download_type" = "runner" ] && [ "$contributor_name" = "Kron4ek" ]; then
filter_keywords="x86|wow64"
else
filter_keywords="oh hi there. this is just placeholder text. how are you today?"
fi
# Add a query string to the url
query_string="?per_page=$max_download_items"
elif [ "$download_url_type" = "gitlab" ]; then
# Which json key are we looking for?
search_key="direct_asset_url"
# Only match urls containing a keyword
match_url_keyword="releases"
# Optional: Filter out game-specific builds by keyword
# Format for grep extended regex (ie: "word1|word2|word3")
filter_keywords="oh hi there. this is just placeholder text. how are you today?"
# Add a query string to the url
query_string="?per_page=$max_download_items"
else
debug_print exit "Script error: Unknown api/url format in ${download_type}_sources array. Aborting."
fi
# Fetch a list of versions from the selected contributor
unset download_versions
while IFS='' read -r line; do
download_versions+=("$line")
done < <(curl -s "$contributor_url$query_string" | grep -Eo "\"$search_key\": ?\"[^\"]+\"" | grep "$match_url_keyword" | cut -d '"' -f4 | cut -d '?' -f1 | xargs basename -a | grep -viE "$filter_keywords")
# Note: match from search_key until " or EOL (Handles embedded commas and escaped quotes). Cut out quotes and gitlab's extraneous query strings.
# Sanity check
if [ "${#download_versions[@]}" -eq 0 ]; then
message warning "No $download_type versions were found. The source API may be down or rate limited."
return 1
fi
# Configure the menu
menu_text_zenity="Select the $download_type you want to install:"
menu_text_terminal="Select the $download_type you want to install:"
menu_text_height="320"
menu_type="radiolist"
goback="Return to the $download_type management menu"
unset menu_options
unset menu_actions
# Iterate through the versions, check if they are installed,
# and add them to the menu options
# To add new file extensions, handle them here and in
# the download_install function
for (( i=0,num_download_items=0; i<"${#download_versions[@]}" && "$num_download_items"<"$max_download_items"; i++ )); do
# Get the file name minus the extension
case "${download_versions[i]}" in
*.sha*sum | *.ini | proton* | *.txt)
# Ignore hashes, configs, and proton downloads
continue
;;
*.tar.gz)
download_basename="$(basename "${download_versions[i]}" .tar.gz)"
;;
*.tgz)
download_basename="$(basename "${download_versions[i]}" .tgz)"
;;
*.tar.xz)
download_basename="$(basename "${download_versions[i]}" .tar.xz)"
;;
*.tar.zst)
download_basename="$(basename "${download_versions[i]}" .tar.zst)"
;;
*)
# Print a warning and move on to the next item
debug_print continue "Warning: Unknown archive filetype in download_select_install() function. Offending String: ${download_versions[i]}"
continue
;;
esac
# Create a list of locations where the file is already installed
unset installed_types
for (( j=0; j<"${#download_dirs[@]}"; j=j+2 )); do
# Loop through all download destinations to get installed types
# Even numbered elements will contain the download destination type (ie. native/flatpak for lutris)
if [ -d "${download_dirs[j+1]}/$download_basename" ]; then
installed_types+=("${download_dirs[j]}")
fi
done
# Build the menu item
unset menu_option_text
if [ "${#download_dirs[@]}" -eq 2 ]; then
# We're only installing to one location
if [ -d "${download_dirs[1]}/$download_basename" ]; then
menu_option_text="$download_basename [installed]"
else
# The file is not installed
menu_option_text="$download_basename"
fi
else
# We're installing to multiple locations
if [ "${#installed_types[@]}" -gt 0 ]; then
# The file is already installed
menu_option_text="$download_basename [installed:"
for (( j=0; j<"${#installed_types[@]}"; j++ )); do
# Add labels for each installed location
menu_option_text="$menu_option_text ${installed_types[j]}"
done
# Complete the menu text
menu_option_text="$menu_option_text]"
else
# The file is not installed
menu_option_text="$download_basename"
fi
fi
# Add the file names to the menu
menu_options+=("$menu_option_text")
menu_actions+=("download_install $i")
# Increment the added items counter
num_download_items="$(($num_download_items+1))"
done
# Complete the menu by adding the option to go back to the previous menu
menu_options+=("$goback")
menu_actions+=(":") # no-op
# Calculate the total height the menu should be
# menu_option_height = pixels per menu option
# #menu_options[@] = number of menu options
# menu_text_height = height of the title/description text
# menu_text_height_zenity4 = added title/description height for libadwaita bigness
menu_height="$(($menu_option_height * ${#menu_options[@]} + $menu_text_height + $menu_text_height_zenity4))"
# Cap menu height
if [ "$menu_height" -gt "$menu_height_max" ]; then
menu_height="$menu_height_max"
fi
# Set the label for the cancel button
cancel_label="Go Back"
# Call the menu function. It will use the options as configured above
menu
}
# MARK: download_install()
# Download and install the selected item. Called by download_select_install()
#
# The following variables are expected to be set before calling this function:
# - download_versions (array)
# - contributor_url (string)
# - download_url_type (string)
# - download_type (string)
# - download_dirs (array)
download_install() {
# This function expects an index number for the array
# download_versions to be passed in as an argument
if [ -z "$1" ]; then
debug_print exit "Script error: The download_install function expects a numerical argument. Aborting."
fi
# Sanity checks
if [ "${#download_versions[@]}" -eq 0 ]; then
debug_print exit "Script error: The array 'download_versions' was not set before calling the download_install function. Aborting."
elif [ -z "$contributor_url" ]; then
debug_print exit "Script error: The string 'contributor_url' was not set before calling the download_install function. Aborting."
elif [ -z "$download_url_type" ]; then
debug_print exit "Script error: The string 'download_url_type' was not set before calling the download_install function. Aborting."
elif [ -z "$download_type" ]; then
debug_print exit "Script error: The string 'download_type' was not set before calling the download_install function. Aborting."
elif [ "${#download_dirs[@]}" -eq 0 ]; then
debug_print exit "Script error: The array 'download_dirs' was not set before calling the download_install function. Aborting."
fi
# Get the filename including file extension
download_filename="${download_versions[$1]}"
# Get the selected item name minus the file extension
# To add new file extensions, handle them here and in
# the download_select_install function
case "$download_filename" in
*.tar.gz)
download_basename="$(basename "$download_filename" .tar.gz)"
;;
*.tgz)
download_basename="$(basename "$download_filename" .tgz)"
;;
*.tar.xz)
download_basename="$(basename "$download_filename" .tar.xz)"
;;
*.tar.zst)
download_basename="$(basename "$download_filename" .tar.zst)"
;;
*)
debug_print exit "Script error: Unknown archive filetype in download_install function. Aborting."
;;
esac
# Set the search keys we'll use to parse the api for the download url
# To add new sources, handle them here and in the
# download_select_install function
if [ "$download_url_type" = "github" ]; then
# Which json key are we looking for?
search_key="browser_download_url"
# Add a query string to the url
query_string="?per_page=$max_download_items"
elif [ "$download_url_type" = "gitlab" ]; then
# Which json key are we looking for?
search_key="direct_asset_url"
# Add a query string to the url
query_string="?per_page=$max_download_items"
else
debug_print exit "Script error: Unknown api/url format in ${download_type}_sources array. Aborting."
fi
# Get the selected download url
download_url="$(curl -s "$contributor_url$query_string" | grep -Eo "\"$search_key\": ?\"[^\"]+\"" | grep "$download_filename" | cut -d '"' -f4 | cut -d '?' -f1 | sed 's|/-/blob/|/-/raw/|')"
# Sanity check
if [ -z "$download_url" ]; then
message warning "Could not find the requested ${download_type}. The source API may be down or rate limited."
return 1
fi
# Download the item to the tmp directory
download_file "$download_url" "$download_filename" "$download_type"
# Sanity check
if [ ! -f "$tmp_dir/$download_filename" ]; then
# Something went wrong with the download and the file doesn't exist
message error "Something went wrong and the requested $download_type file could not be downloaded!"
debug_print continue "Download failed! File not found: $tmp_dir/$download_filename"
return 1
fi
# Extract the archive to the tmp directory
debug_print continue "Extracting $download_type into $tmp_dir/$download_basename..."
if [ "$use_zenity" -eq 1 ]; then
# Use Zenity progress bar
mkdir "$tmp_dir/$download_basename" && tar -xf "$tmp_dir/$download_filename" -C "$tmp_dir/$download_basename" | \
zenity --progress --pulsate --no-cancel --auto-close --title="Star Citizen LUG Helper" --text="Extracting ${download_type}...\n" 2>/dev/null
else
mkdir "$tmp_dir/$download_basename" && tar -xf "$tmp_dir/$download_filename" -C "$tmp_dir/$download_basename"
fi
# Check the contents of the extracted archive to determine the
# directory structure we must create upon installation
num_dirs=0
num_files=0
for extracted_item in "$tmp_dir/$download_basename"/*; do
if [ -d "$extracted_item" ]; then
num_dirs="$(($num_dirs+1))"
extracted_dir="$(basename "$extracted_item")"
elif [ -f "$extracted_item" ]; then
num_files="$(($num_files+1))"
fi
done
# Create the correct directory structure and install the item
if [ "$num_dirs" -eq 0 ] && [ "$num_files" -eq 0 ]; then
# Sanity check
message warning "The downloaded archive is empty. There is nothing to do."
elif [ "$num_dirs" -eq 1 ] && [ "$num_files" -eq 0 ]; then
# If the archive contains only one directory, install that directory
# We rename it to the name of the archive in case it is different
# so we can easily detect installed items in download_select_install()
for (( i=1; i<"${#download_dirs[@]}"; i=i+2 )); do
# Loop through all download destinations, installing to each one
# Odd numbered elements will contain the download destination's path
if [ -d "${download_dirs[i]}/$download_basename" ]; then
# This item has already been installed. Delete it before reinstalling
debug_print continue "$download_type exists, deleting ${download_dirs[i]}/$download_basename..."
rm -r --interactive=never "${download_dirs[i]:?}/$download_basename"
debug_print continue "Reinstalling $download_type into ${download_dirs[i]}/$download_basename..."
else
debug_print continue "Installing $download_type into ${download_dirs[i]}/$download_basename..."
fi
if [ "$use_zenity" -eq 1 ]; then
# Use Zenity progress bar
mkdir -p "${download_dirs[i]}" && cp -r "$tmp_dir/$download_basename/$extracted_dir" "${download_dirs[i]}/$download_basename" | \
zenity --progress --pulsate --no-cancel --auto-close --title="Star Citizen LUG Helper" --text="Installing ${download_type}...\n" 2>/dev/null
else
mkdir -p "${download_dirs[i]}" && cp -r "$tmp_dir/$download_basename/$extracted_dir" "${download_dirs[i]}/$download_basename"
fi
done
# Store the final name of the downloaded item
downloaded_item_name="$download_basename"
# Mark success for triggering post-download actions
download_action_success="installed"
elif [ "$num_dirs" -gt 1 ] || [ "$num_files" -gt 0 ]; then
# If the archive contains more than one directory or
# one or more files, we must create a subdirectory
for (( i=1; i<"${#download_dirs[@]}"; i=i+2 )); do
# Loop through all download destinations, installing to each one
# Odd numbered elements will contain the download destination's path
if [ -d "${download_dirs[i]}/$download_basename" ]; then
# This item has already been installed. Delete it before reinstalling
debug_print continue "$download_type exists, deleting ${download_dirs[i]}/$download_basename..."
rm -r --interactive=never "${download_dirs[i]:?}/$download_basename"
debug_print continue "Reinstalling $download_type into ${download_dirs[i]}/$download_basename..."
else
debug_print continue "Installing $download_type into ${download_dirs[i]}/$download_basename..."
fi
if [ "$use_zenity" -eq 1 ]; then
# Use Zenity progress bar
mkdir -p "${download_dirs[i]}/$download_basename" && cp -r "$tmp_dir"/"$download_basename"/* "${download_dirs[i]}"/"$download_basename" | \
zenity --progress --pulsate --no-cancel --auto-close --title="Star Citizen LUG Helper" --text="Installing ${download_type}...\n" 2>/dev/null
else
mkdir -p "${download_dirs[i]}/$download_basename" && cp -r "$tmp_dir"/"$download_basename"/* "${download_dirs[i]}"/"$download_basename"
fi
done
# Store the final name of the downloaded item
downloaded_item_name="$download_basename"
# Mark success for triggering post-download actions
download_action_success="installed"
else
# Some unexpected combination of directories and files
debug_print exit "Script error: Unexpected archive contents in download_install function. Aborting"
fi
# Cleanup tmp download
debug_print continue "Cleaning up $tmp_dir/$download_filename..."
rm --interactive=never "${tmp_dir:?}/$download_filename"
rm -r --interactive=never "${tmp_dir:?}/$download_basename"
return 0
}
# MARK: download_select_delete()
# List installed items for deletion. Called by download_manage()
#
# The following variables are expected to be set before calling this function:
# - download_type (string)
# - download_dirs (array)
download_select_delete() {
# Sanity checks
if [ -z "$download_type" ]; then
debug_print exit "Script error: The string 'download_type' was not set before calling the download_select_delete function. Aborting."
elif [ "${#download_dirs[@]}" -eq 0 ]; then
debug_print exit "Script error: The array 'download_dirs' was not set before calling the download_select_delete function. Aborting."
fi
# Configure the menu
menu_text_zenity="Select the $download_type(s) you want to remove:"
menu_text_terminal="Select the $download_type you want to remove:"
menu_text_height="320"
menu_type="checklist"
goback="Return to the $download_type management menu"
unset installed_items
unset installed_item_names
unset menu_options
unset menu_actions
# Find all installed items in the download destinations
for (( i=1; i<"${#download_dirs[@]}"; i=i+2 )); do
# Skip if the directory doesn't exist
if [ ! -d "${download_dirs[i]}" ]; then
continue
fi
# Loop through all download destinations
# Odd numbered elements will contain the download destination's path
for item in "${download_dirs[i]}"/*; do
if [ -d "$item" ]; then
if [ "${#download_dirs[@]}" -eq 2 ]; then
# We're deleting from one location
installed_item_names+=("$(basename "$item")")
else
# We're deleting from multiple locations so label each one
installed_item_names+=("$(basename "$item [${download_dirs[i-1]}]")")
fi
installed_items+=("$item")
fi
done
done
# Create menu options for the installed items
for (( i=0; i<"${#installed_items[@]}"; i++ )); do
menu_options+=("${installed_item_names[i]}")
menu_actions+=("download_delete $i")
done
# Print a message and return if no installed items were found
if [ "${#menu_options[@]}" -eq 0 ]; then
message info "No installed ${download_type}s found."
return 0
fi
# Complete the menu by adding the option to go back to the previous menu
menu_options+=("$goback")
menu_actions+=(":") # no-op
# Calculate the total height the menu should be
# menu_option_height = pixels per menu option
# #menu_options[@] = number of menu options
# menu_text_height = height of the title/description text
# menu_text_height_zenity4 = added title/description height for libadwaita bigness
menu_height="$(($menu_option_height * ${#menu_options[@]} + $menu_text_height + $menu_text_height_zenity4))"
# Cap menu height
if [ "$menu_height" -gt "$menu_height_max" ]; then
menu_height="$menu_height_max"
fi
# Set the label for the cancel button
cancel_label="Go Back"
# Call the menu function. It will use the options as configured above
menu
}
# MARK: download_delete()
# Uninstall the selected item(s). Called by download_select_install()
# Accepts array index numbers as an argument
#
# The following variables are expected to be set before calling this function:
# - download_type (string)
# - installed_items (array)
# - installed_item_names (array)
download_delete() {
# This function expects at least one index number for the array installed_items to be passed in as an argument
if [ -z "$1" ]; then
debug_print exit "Script error: The download_delete function expects an argument. Aborting."
fi
# Sanity checks
if [ -z "$download_type" ]; then
debug_print exit "Script error: The string 'download_type' was not set before calling the download_delete function. Aborting."
elif [ "${#installed_items[@]}" -eq 0 ]; then
debug_print exit "Script error: The array 'installed_items' was not set before calling the download_delete function. Aborting."
elif [ "${#installed_item_names[@]}" -eq 0 ]; then
debug_print exit "Script error: The array 'installed_item_names' was not set before calling the download_delete function. Aborting."
fi
# Capture arguments and format a list of items
item_to_delete=("$@")
unset list_to_delete
unset deleted_item_names
for (( i=0; i<"${#item_to_delete[@]}"; i++ )); do
list_to_delete+="\n${installed_items[${item_to_delete[i]}]}"
done
if message question "Are you sure you want to delete the following ${download_type}(s)?\n$list_to_delete"; then
# Loop through the arguments
for (( i=0; i<"${#item_to_delete[@]}"; i++ )); do
rm -r --interactive=never "${installed_items[${item_to_delete[i]}]}"
debug_print continue "Deleted ${installed_items[${item_to_delete[i]}]}"
# Store the names of deleted items for post_download() processing
deleted_item_names+=("${installed_item_names[${item_to_delete[i]}]}")
done
# Mark success for triggering post-deletion actions
download_action_success="deleted"
fi
}
# MARK: post_download()
# Perform post-download actions or display a message/instructions
#
# The following variables are expected to be set before calling this function:
# - post_download_type (string. "none", "info", "configure-wine")
# - post_install_msg_heading (string)
# - post_install_msg (string)
# - post_delete_msg (string)
# - post_download_sed_string (string. For type configure-wine)
# - post_delete_restore_value (string. For type configure-wine)
# - download_action_success (string. Set automatically in install/delete functions)
# - downloaded_item_name (string. For installs only. Set automatically in download_install function)
# - deleted_item_names (array. For deletions only. Set automatically in download_delete function)
#
# Details for post_download_sed_string:
# This is the string sed will match against when editing configs or files
# For the wine install, it replaces values in the default launch script
# with the appropriate paths and values after installation.
#
# Message display format:
# A header is automatically displayed that reads: Download Complete
# post_install_msg is displayed below the header
post_download() {
# Sanity checks
if [ -z "$post_download_type" ]; then
debug_print exit "Script error: The string 'post_download_type' was not set before calling the post_download function. Aborting."
elif [ -z "$post_install_msg_heading" ]; then
debug_print exit "Script error: The string 'post_install_msg_heading' was not set before calling the post_download function. Aborting."
elif [ -z "$post_install_msg" ]; then
debug_print exit "Script error: The string 'post_install_msg' was not set before calling the post_download function. Aborting."
elif [ -z "$post_delete_msg" ]; then
debug_print exit "Script error: The string 'post_delete_msg' was not set before calling the post_download function. Aborting."
elif [ -z "$post_download_sed_string" ] && [ "$post_download_type" = "configure-wine" ]; then
debug_print exit "Script error: The string 'post_download_sed_string' was not set before calling the post_download function. Aborting."
elif [ -z "$post_delete_restore_value" ] && [ "$post_download_type" = "configure-wine" ]; then
debug_print exit "Script error: The string 'post_delete_restore_value' was not set before calling the post_download function. Aborting."
fi
# Return if we don't have anything to do
if [ "$post_download_type" = "none" ]; then
return 0
fi
# Configure the message heading and format it for zenity
if [ "$use_zenity" -eq 1 ]; then
post_install_msg_heading="$post_install_msg_heading"
fi
# Display appropriate post-download message
if [ "$post_download_type" = "info" ]; then
# Just displaying an informational message
message info "$post_install_msg_heading\n\n$post_install_msg"
elif [ "$post_download_type" = "configure-wine" ]; then
# We handle installs and deletions differently
if [ "$download_action_success" = "installed" ]; then
# We are installing a wine version and updating the launch script to use it
if message question "$post_install_msg_heading\n\n$post_install_msg"; then
# Make sure we can locate the launch script
if [ ! -f "$wine_prefix/$wine_launch_script_name" ]; then
message error "Unable to find $wine_prefix/$wine_launch_script_name"
return 1
fi
# Make sure the launch script has the appropriate string to be replaced
if ! grep -q "^${post_download_sed_string}" "$wine_prefix/$wine_launch_script_name"; then
message error "Unable to to find a required variable in\n$wine_prefix/$wine_launch_script_name\n\nYour launch script may be out of date and will need to be edited manually!"
return 1
fi
# Replace the specified variable
sed -i "s|^${post_download_sed_string}.*|${post_download_sed_string}\"${wine_prefix}/runners/${downloaded_item_name}/bin\"|" "$wine_prefix/$wine_launch_script_name"
else
message warning "The launch script will need to be edited manually!\n\n$wine_prefix/$wine_launch_script_name"
fi
elif [ "$download_action_success" = "deleted" ]; then
# We deleted a custom wine version and need to revert the launch script to use the system wine
if message question "$post_delete_msg"; then
# Make sure we can locate the launch script
if [ ! -f "$wine_prefix/$wine_launch_script_name" ]; then
message error "Unable to find $wine_prefix/$wine_launch_script_name"
return 1
fi
# Make sure the launch script has the appropriate string to be replaced
if ! grep -q "^${post_download_sed_string}" "$wine_prefix/$wine_launch_script_name"; then
message error "Unable to to find a required variable in\n$wine_prefix/$wine_launch_script_name\n\nYour launch script may be out of date and will need to be edited manually!"
return 1
fi
# Restore the specified variable
sed -i "s#^${post_download_sed_string}.*#${post_download_sed_string}\"${post_delete_restore_value}\"#" "$wine_prefix/$wine_launch_script_name"
else
message warning "The launch script will need to be edited manually!\n\n$wine_prefix/$wine_launch_script_name"
fi
else
debug_print exit "Script error: Unknown download_action_success value in post_download function. Aborting."
fi
else
debug_print exit "Script error: Unknown post_download_type value in post_download function. Aborting."
fi
}
# MARK: download_file()
# Download a file to the tmp directory
# Expects three arguments: The download URL, file name, and download type
download_file() {
# This function expects three string arguments
if [ "$#" -lt 3 ]; then
printf "\nScript error: The download_file function expects three arguments. Aborting.\n"
read -n 1 -s -p "Press any key..."
exit 0
fi
# Capture the arguments and encode spaces in urls
download_url="${1// /%20}"
download_filename="$2"
download_type="$3"
# Download the item to the tmp directory
debug_print continue "Downloading $download_url into $tmp_dir/$download_filename..."
if [ "$use_zenity" -eq 1 ]; then
# Format the curl progress bar for zenity
mkfifo "$tmp_dir/lugpipe"
cd "$tmp_dir" && curl -#L "$download_url" -o "$download_filename" > "$tmp_dir/lugpipe" 2>&1 & curlpid="$!"
stdbuf -oL tr '\r' '\n' < "$tmp_dir/lugpipe" | \
grep --line-buffered -ve "100" | grep --line-buffered -o "[0-9]*\.[0-9]" | \
(
trap 'kill "$curlpid"; trap - ERR' ERR
zenity --progress --auto-close --title="Star Citizen LUG Helper" --text="Downloading ${download_type}. This might take a moment.\n" 2>/dev/null
)
if [ "$?" -eq 1 ]; then
# User clicked cancel
debug_print continue "Download aborted. Removing $tmp_dir/$download_filename..."
rm --interactive=never "${tmp_dir:?}/$download_filename"
rm --interactive=never "${tmp_dir:?}/lugpipe"
return 1
fi
rm --interactive=never "${tmp_dir:?}/lugpipe"
else
# Standard curl progress bar
(cd "$tmp_dir" && curl -#L "$download_url" -o "$download_filename")
fi
}
############################################################################
######## end download functions ############################################
############################################################################
############################################################################
######## begin maintenance functions #######################################
############################################################################
# MARK: maintenance_menu()
# Show maintenance/troubleshooting options
maintenance_menu() {
# Loop the menu until the user selects quit
looping_menu="true"
while [ "$looping_menu" = "true" ]; do
# Fetch wine prefix
if [ -f "$conf_dir/$conf_subdir/$wine_conf" ]; then
maint_prefix="$(cat "$conf_dir/$conf_subdir/$wine_conf")"
else
maint_prefix="Not configured"
fi
# Configure the menu
menu_text_zenity="Game Maintenance and Troubleshooting\n\nLUG Wiki: $lug_wiki\n\nWine prefix: $maint_prefix"
menu_text_terminal="Game Maintenance and Troubleshooting\n\nLUG Wiki: $lug_wiki\n\nWine prefix: $maint_prefix"
menu_text_height="320"
menu_type="radiolist"
# Configure the menu options
prefix_msg="Target a different Star Citizen installation"
launcher_msg="Update launch script"
launchscript_msg="Edit launch script"
config_msg="Open Wine prefix configuration"
controllers_msg="Open Wine controller configuration"
powershell_msg="Install PowerShell into Wine prefix"
dirs_msg="Display Helper and Star Citizen directories"
reset_msg="Reset Helper configs"
quit_msg="Return to the main menu"
# Set the options to be displayed in the menu
menu_options=("$prefix_msg" "$launcher_msg" "$launchscript_msg" "$config_msg" "$controllers_msg" "$powershell_msg" "$dirs_msg" "$reset_msg" "$quit_msg")
# Set the corresponding functions to be called for each of the options
menu_actions=("switch_prefix" "update_launcher" "edit_wine_launch_script" "call_launch_script config" "call_launch_script controllers" "install_powershell" "display_dirs" "reset_helper" "menu_loop_done")
# Calculate the total height the menu should be
# menu_option_height = pixels per menu option
# #menu_options[@] = number of menu options
# menu_text_height = height of the title/description text
# menu_text_height_zenity4 = added title/description height for libadwaita bigness
menu_height="$(($menu_option_height * ${#menu_options[@]} + $menu_text_height + $menu_text_height_zenity4))"
# Set the label for the cancel button
cancel_label="Go Back"
# Call the menu function. It will use the options as configured above
menu
done
}
# MARK: switch_prefix()
# Target the Helper at a different Star Citizen prefix/installation
switch_prefix() {
# Check if the config file exists
if [ -f "$conf_dir/$conf_subdir/$wine_conf" ] && [ -f "$conf_dir/$conf_subdir/$game_conf" ]; then
getdirs
# Above will return code 3 if the user had to select new directories. This can happen if the stored directories are now invalid.
# We check this so we don't prompt the user to set directories twice here.
if [ "$?" -ne 3 ] && message question "The Helper is currently targeting this Star Citizen install\nWould you like to change it?\n\n$wine_prefix"; then
reset_helper "switchprefix"
# Prompt the user for a new set of game paths
getdirs
fi
else
# Prompt the user for game paths
getdirs
fi
}
# MARK: update_launcher()
# Update the game launch script if necessary
update_launcher() {
getdirs
if [ ! -f "$wine_prefix/$wine_launch_script_name" ]; then
message warning "Game launch script not found!\n\n$wine_prefix/$wine_launch_script_name"
return 1
fi
current_launcher_ver="$(grep "^# version:" "$wine_prefix/$wine_launch_script_name" | awk '{print $3}')"
latest_launcher_ver="$(grep "^# version:" $wine_launch_script | awk '{print $3}')"
if [ "$latest_launcher_ver" != "$current_launcher_ver" ] &&
[ "$current_launcher_ver" = "$(printf "%s\n%s" "$current_launcher_ver" "$latest_launcher_ver" | sort -V | head -n1)" ]; then
# Backup the file
cp "$wine_prefix/$wine_launch_script_name" "$wine_prefix/$(basename "$wine_launch_script_name" .sh).bak"
# Backup the variables we know we need
bak_wineprefix="$(grep "^export WINEPREFIX=" "$wine_prefix/$wine_launch_script_name" | awk -F '=' '{print $2}')"
bak_winepath="$(grep -e "^export wine_path=" -e "^wine_path=" "$wine_prefix/$wine_launch_script_name" | awk -F '=' '{print $2}')"
# If wineprefix isn't found in the file, something is wrong and we shouldn't proceed
if [ -z "$bak_wineprefix" ]; then
message error "The WINEPREFIX env var was not found in your launch script. Unable to proceed!\n\n$wine_prefix/$wine_launch_script_name"
return 1
fi
# If wine_path is empty, it may be an older version of the launch script. Default to system wine
if [ -z "$bak_winepath" ]; then
bak_winepath="$(command -v wine | xargs dirname)"
fi
# Copy in the new launch script
cp "$wine_launch_script" "$wine_prefix"
# Restore the variables
sed -i "s|^export WINEPREFIX=.*|export WINEPREFIX=$bak_wineprefix|" "$wine_prefix/$wine_launch_script_name"
sed -i "s#^export wine_path=.*#export wine_path=$bak_winepath#" "$wine_prefix/$wine_launch_script_name"
message info "Your game launch script has been updated!\n\nIf you had customized your script, you'll need to re-add your changes.\nA backup was created at:\n\n$wine_prefix/$(basename "$wine_launch_script_name" .sh).bak"
else
message info "Your game launch script is already up to date!"
fi
}
# MARK: call_launch_script()
# Call our launch script and pass it the given command line argument
call_launch_script()
{
# This function expects a string to be passed in as an argument
if [ -z "$1" ]; then
debug_print exit "Script error: The call_launch_script function expects an argument. Aborting."
fi
launch_arg="$1"
# Get/Set directory paths
getdirs
if [ "$?" -eq 1 ]; then
# User cancelled and wants to return to the main menu
# or there was an error
return 0
fi
# Make sure the launch script exists
if [ ! -f "$wine_prefix/$wine_launch_script_name" ]; then
message error "Unable to find $wine_prefix/$wine_launch_script_name"
return 1
fi
# Check if the launch script is the correct version
current_launcher_ver="$(grep "^# version:" "$wine_prefix/$wine_launch_script_name" | awk '{print $3}')"
req_launcher_ver="1.5"
if [ "$req_launcher_ver" != "$current_launcher_ver" ] &&
[ "$current_launcher_ver" = "$(printf "%s\n%s" "$current_launcher_ver" "$req_launcher_ver" | sort -V | head -n1)" ]; then
message error "Your launch script is out of date!\nPlease update your launch script before proceeding."
return 1
fi
# Launch a wine shell using the launch script
"$wine_prefix/$wine_launch_script_name" "$launch_arg"
}
# MARK: edit_wine_launch_script()
# Edit the launch script
edit_wine_launch_script() {
# Get/Set directory paths
getdirs
if [ "$?" -eq 1 ]; then
# User cancelled and wants to return to the main menu
# or there was an error
return 0
fi
# Make sure the launch script exists
if [ ! -f "$wine_prefix/$wine_launch_script_name" ]; then
message error "Unable to find $wine_prefix/$wine_launch_script_name"
return 1
fi
# Open the launch script in the user's preferred editor
if [ -x "$(command -v xdg-open)" ]; then
xdg-open "$wine_prefix/$wine_launch_script_name"
else
message error "xdg-open is not installed.\nYou may open the launch script manually:\n\n$wine_prefix/$wine_launch_script_name"
fi
}
# MARK: display_dirs()
# Display all directories currently used by this helper and Star Citizen
display_dirs() {
dirs_list="\n"
# Helper configs and keybinds
if [ -d "$conf_dir/$conf_subdir" ]; then
dirs_list+="Helper configuration:\n$conf_dir/$conf_subdir\n\n"
fi
# Wine prefix
if [ -f "$conf_dir/$conf_subdir/$wine_conf" ]; then
dirs_list+="Wine prefix:\n$(cat "$conf_dir/$conf_subdir/$wine_conf")\n\n"
fi
# Star Citizen installation
if [ -f "$conf_dir/$conf_subdir/$game_conf" ]; then
dirs_list+="Star Citizen game directory:\n$(cat "$conf_dir/$conf_subdir/$game_conf")\n\n"
fi
# Format the info header
message_heading="These directories are currently being used by this Helper and Star Citizen"
if [ "$use_zenity" -eq 1 ]; then
message_heading="$message_heading"
fi
message info "$message_heading\n$dirs_list"
}
# MARK: display_wiki()
# Display the LUG Wiki
display_wiki() {
# Display a message containing the URL
message info "See the Wiki for our Quick-Start Guide, Troubleshooting,\nand Performance Tuning Recommendations:\n\n$lug_wiki"
}
# MARK: reset_helper()
# Delete the helper's config directory
reset_helper() {
if [ "$1" = "switchprefix" ]; then
# This gets called by the switch_prefix and install_game_wine functions
# We only want to delete configs related to the game path in order to target a different game install
debug_print continue "Deleting $conf_dir/$conf_subdir/{$wine_conf,$game_conf}..."
rm --interactive=never "${conf_dir:?}/$conf_subdir/"{"$wine_conf","$game_conf"}
elif message question "All config files will be deleted from:\n\n$conf_dir/$conf_subdir\n\nDo you want to proceed?"; then
# Called normally by the user, wipe all the things!
debug_print continue "Deleting $conf_dir/$conf_subdir/*.conf..."
rm --interactive=never "${conf_dir:?}/$conf_subdir/"*.conf
message info "The Helper has been reset!"
fi
# Also wipe path variables so the reset takes immediate effect
wine_prefix=""
game_path=""
}
############################################################################
######## end maintenance functions #########################################
############################################################################
# MARK: install_game_wine()
# Install the game with Wine
install_game_wine() {
# Double check that wine is installed
if [ ! -x "$(command -v wine)" ]; then
message error "Wine does not appear to be installed.\nPlease refer to our Quick Start Guide:\n$lug_wiki"
return 1
fi
# Check if the install script exists
if [ ! -f "$wine_launch_script" ]; then
message error "Game launch script not found! Unable to proceed.\n\n$wine_launch_script\n\nIt is included in our official releases here:\n$releases_url"
return 1
fi
# Call the preflight check and confirm the user is ready to proceed
preflight_check "wine"
if [ "$?" -eq 1 ]; then
# There were errors
install_question="Before proceeding, be sure all Preflight Checks have passed!\n\nPlease refer to our Quick Start Guide:\n$lug_wiki\n\nAre you ready to continue?"
else
# No errors
install_question="Before proceeding, please refer to our Quick Start Guide:\n$lug_wiki\n\nAll Preflight Checks have passed\nAre you ready to continue?"
fi
if ! message question "$install_question"; then
return 1
fi
# Get the install path from the user
if message question "Would you like to use the default install path?\n\n$HOME/Games/star-citizen"; then
# Set the default install path
install_dir="$HOME/Games/star-citizen"
else
if [ "$use_zenity" -eq 1 ]; then
message info "On the next screen, select your Star Citizen install location"
# Get the install path from the user
while true; do
install_dir="$(zenity --file-selection --directory --title="Choose your Star Citizen install directory" --filename="$HOME/" 2>/dev/null)"
if [ "$?" -eq -1 ]; then
message error "An unexpected error has occurred. The Helper is unable to proceed."
return 1
elif [ -z "$install_dir" ]; then
# User clicked cancel or something else went wrong
message warning "Installation cancelled."
return 1
fi
# Add the wine prefix subdirectory to the install path
install_dir="$install_dir/star-citizen"
# Sanity check the chosen directory a bit to catch some possible mistakes
if [ -d "$install_dir" ]; then
message warning "A directory named \"star-citizen\" already exists!\nPlease choose a different install location.\n\n$install_dir"
else
# All good, break out of the loop and continue
break
fi
done
else
# No Zenity, use terminal-based menus
clear
# Get the install path from the user
printf "Enter the desired Star Citizen install path (case sensitive)\nie. /home/USER/Games/star-citizen\n\n"
while read -rp "Install path: " install_dir; do
if [ -z "$install_dir" ]; then
printf "Invalid directory. Please try again.\n\n"
elif [ ! -d "$install_dir" ]; then
if message question "That directory does not exist.\nWould you like it to be created for you?\n"; then
break
fi
else
break
fi
done
fi
fi
if [ "$(ls -A "$install_dir" 2>/dev/null)" ]; then
# The directory is not empty, ask the user if they want to continue
if ! message options "Cancel" "Continue" "The install directory is not empty!\n\nRe-using an existing wine prefix may result in unexpected behavior!\n\n$install_dir"; then
return 1
fi
fi
# Create the game path
mkdir -p "$install_dir"
# If we can't use the system wine, we'll need to have the user select a custom wine runner to use
wine_path="$(command -v wine | xargs dirname)"
if [ "$system_wine_ok" = "false" ]; then
debug_print continue "Your system Wine does not meet the minimum requirements for Star Citizen!"
debug_print continue "A custom wine runner will be automatically downloaded and used."
download_dirs=("wine" "$install_dir/runners")
# Install the default wine runner into the prefix
download_wine
# Make sure the wine download worked
if [ "$?" -eq 1 ]; then
message error "Something went wrong while installing ${default_runner}!\nGame installation cannot proceed."
return 1
fi
wine_path="$install_dir/runners/$downloaded_item_name/bin"
fi
# Download winetricks
download_winetricks
# Abort if the winetricks download failed
if [ "$?" -eq 1 ]; then
message error "Unable to install Star Citizen without winetricks. Aborting."
return 1
fi
# Download RSI installer to tmp
download_file "$rsi_installer_url" "$rsi_installer" "installer"
# Sanity check
if [ ! -f "$tmp_dir/$rsi_installer" ]; then
# Something went wrong with the download and the file doesn't exist
message error "Something went wrong; the installer could not be downloaded!"
return 1
fi
# Create a temporary log file
tmp_install_log="$(mktemp --suffix=".log" -t "lughelper-install-XXX")"
debug_print continue "Installation log file created at $tmp_install_log"
# Create the new prefix and install powershell
export WINE="$wine_path/wine"
export WINESERVER="$wine_path/wineserver"
export WINEPREFIX="$install_dir"
export WINEDLLOVERRIDES="dxwebsetup.exe,dotNetFx45_Full_setup.exe,winemenubuilder.exe=d"
# Show a zenity pulsating progress bar and get its process ID to kill when we're done
while true; do
sleep 1
done | zenity --progress --pulsate --no-cancel --auto-close --title="Star Citizen LUG Helper" --text="Preparing Wine prefix and installing RSI Launcher. Please wait..." 2>/dev/null &
zenity_pid="$!"
trap 'kill "$zenity_pid"; trap - SIGINT' SIGINT
debug_print continue "Preparing Wine prefix. Please wait; this will take a moment..."
"$winetricks_bin" -q arial tahoma dxvk powershell win11 >"$tmp_install_log" 2>&1
if [ "$?" -eq 1 ]; then
if message question "Wine prefix creation failed. Aborting installation.\nThe install log was written to\n$tmp_install_log\n\nDo you want to delete\n${install_dir}?"; then
debug_print continue "Deleting $install_dir..."
rm -r --interactive=never "$install_dir"
fi
"$wine_path"/wineserver -k
return 1
fi
# Add registry key that prevents wine from creating unnecessary file type associations
"$wine_path"/wine reg add "HKEY_CURRENT_USER\Software\Wine\FileOpenAssociations" /v Enable /d N /f >>"$tmp_install_log" 2>&1
# Run the installer
debug_print continue "Installing RSI Launcher. Please wait; this will take a moment..."
"$wine_path"/wine "$tmp_dir/$rsi_installer" /S >>"$tmp_install_log" 2>&1
if [ "$?" -eq 1 ]; then
# User cancelled or there was an error
if message question "Installation aborted. The install log was written to\n$tmp_install_log\n\nDo you want to delete\n${install_dir}?"; then
debug_print continue "Deleting $install_dir..."
rm -r --interactive=never "$install_dir"
fi
kill "$zenity_pid" 2>/dev/null
trap - SIGINT # Remove the trap
"$wine_path"/wineserver -k
return 0
fi
# Kill the zenity progress window
kill "$zenity_pid" 2>/dev/null
trap - SIGINT # Remove the trap
# Kill the wine process after installation
# To prevent unexpected lingering background wine processes, it should be launched by the user attached to a terminal
"$wine_path"/wineserver -k
# Save the install location to the Helper's config files
reset_helper "switchprefix"
wine_prefix="$install_dir"
if [ -d "$wine_prefix/$default_install_path" ]; then
game_path="$wine_prefix/$default_install_path/$sc_base_dir"
fi
getdirs
# Verify that we have an installed game path
if [ -z "$game_path" ]; then
message error "Something went wrong during installation. Unable to locate the expected game path. Aborting."
return 1
fi
# Copy game launch script to the wine prefix root directory
debug_print continue "Copying game launch script to ${install_dir}..."
if [ -f "$install_dir/$wine_launch_script_name" ]; then
# Back it up if it already exists
cp "$install_dir/$wine_launch_script_name" "$install_dir/$(basename "$wine_launch_script_name" .sh).bak"
fi
cp "$wine_launch_script" "$install_dir"
installed_launch_script="$install_dir/$wine_launch_script_name"
# Update WINEPREFIX in game launch script
sed -i "s|^export WINEPREFIX=.*|export WINEPREFIX=\"$install_dir\"|" "$installed_launch_script"
# Update Wine binary in game launch script
post_download_sed_string="export wine_path="
sed -i "s|^${post_download_sed_string}.*|${post_download_sed_string}\"${wine_path}\"|" "$installed_launch_script"
# Create .desktop files
debug_print continue "Creating .desktop files..."
# Copy the bundled RSI Launcher icon to the .local icons directory
if [ -f "$rsi_icon" ]; then
mkdir -p "$data_dir/icons/hicolor/256x256/apps" &&
cp "$rsi_icon" "$data_dir/icons/hicolor/256x256/apps"
fi
# $HOME/Desktop/RSI Launcher.desktop
home_desktop_file="${XDG_DESKTOP_DIR:-$HOME/Desktop}/RSI Launcher.desktop"
# $HOME/.local/share/applications/RSI Launcher.desktop
localshare_desktop_file="$data_dir/applications/RSI Launcher.desktop"
echo "[Desktop Entry]
Name=RSI Launcher
Type=Application
Comment=RSI Launcher
Icon=rsi-launcher.png
Exec=\"$installed_launch_script\"
Path=$(echo $install_dir | sed 's/ /\\\s/g')/dosdevices/c:/Program\sFiles/Roberts\sSpace\sIndustries/RSI\sLauncher" > "$localshare_desktop_file"
# Copy the new desktop file to the user's desktop directory
cp "$localshare_desktop_file" "$home_desktop_file"
# Update the .desktop file database if the command is available
if [ -x "$(command -v update-desktop-database)" ]; then
debug_print continue "Running update-desktop-database..."
update-desktop-database "$data_dir/applications"
fi
# Check if the desktop files were created successfully
if [ ! -f "$home_desktop_file" ]; then
# Desktop file couldn't be created
message warning "Warning: The .desktop file could not be created!\n\n$home_desktop_file"
fi
if [ ! -f "$localshare_desktop_file" ]; then
# Desktop file couldn't be created
message warning "Warning: The .desktop file could not be created!\n\n$localshare_desktop_file"
fi
message info "Installation has finished. The install log was written to $tmp_install_log\n\nTo start the RSI Launcher, run the following launch script in a terminal\nEdit the environment variables in the script as needed:\n $installed_launch_script\n\nYou may also start the RSI Launcher using the following .desktop files:\n $home_desktop_file\n $localshare_desktop_file"
}
# MARK: download_wine()
# Download a default wine runner for use by the installer
# Expects download_dirs to be set before calling
download_wine() {
if [ "${#download_dirs[@]}" -eq 0 ]; then
debug_print exit "Script error: The array 'download_dirs' was not set before calling the download_wine function. Aborting."
fi
# Set up variables needed for the download functions, quick and dirty
# For more details, see their usage in the download_select_install and download_install functions
declare -n download_sources=runner_sources
download_type="runner"
download_versions=("$default_runner")
contributor_name="${download_sources[$default_runner_source]}"
contributor_url="${download_sources[$default_runner_source+1]}"
case "$contributor_url" in
https://api.github.com/*)
download_url_type="github"
;;
https://gitlab.com/api/v4/projects/*)
download_url_type="gitlab"
;;
*)
debug_print exit "Script error: Unknown api/url format in ${download_type}_sources array. Aborting."
;;
esac
# Call the download_install function with the above options to install the default wine runner
download_install 0
if [ "$?" -eq 1 ]; then
return 1
fi
}
# MARK: download_winetricks()
# Download winetricks to a temporary file
download_winetricks() {
download_file "$winetricks_url" "winetricks" "winetricks"
# Sanity check
if [ ! -f "$tmp_dir/winetricks" ]; then
# Something went wrong with the download and the file doesn't exist
message error "Something went wrong; winetricks could not be downloaded!"
return 1
fi
# Save the path to the downloaded binary
winetricks_bin="$tmp_dir/winetricks"
# Make it executable
chmod +x "$winetricks_bin"
}
# MARK: install_powershell()
# Install powershell verb into the game's wine prefix
install_powershell() {
# Download winetricks
download_winetricks
# Abort if the winetricks download failed
if [ "$?" -eq 1 ]; then
message error "Unable to install powershell without winetricks. Aborting."
return 1
fi
# Update directories
getdirs
# Install powershell
if [ "$?" -ne 1 ]; then
debug_print continue "Installing PowerShell into ${wine_prefix}..."
WINEPREFIX="$wine_prefix" "$winetricks_bin" -q powershell
message info "PowerShell operation complete. See terminal output for details."
fi
}
# MARK: dxvk_update_wine()
# Update dxvk for native wine installs
dxvk_update_wine() {
# Download winetricks
download_winetricks
# Abort if the winetricks download failed
if [ "$?" -eq 1 ]; then
message error "Unable to install powershell without winetricks. Aborting."
return 1
fi
# Update directories
getdirs
# Update dxvk
if [ "$?" -ne 1 ]; then
debug_print continue "Updating DXVK in ${wine_prefix}..."
WINEPREFIX="$wine_prefix" "$winetricks_bin" -f dxvk
message info "DXVK update complete. See terminal output for details."
fi
}
# MARK: format_urls()
# Format some URLs for Zenity
format_urls() {
if [ "$use_zenity" -eq 1 ]; then
releases_url="$releases_url"
lug_wiki="$lug_wiki"
lug_wiki_nixos="$lug_wiki_nixos"
fi
}
# MARK: get_latest_release()
# Get the latest release version of a repo. Expects "user/repo_name" as input
# Credits for this go to https://gist.github.com/lukechilds/a83e1d7127b78fef38c2914c4ececc3c
get_latest_release() {
# Sanity check
if [ "$#" -lt 1 ]; then
debug_print exit "Script error: The get_latest_release function expects one argument. Aborting."
fi
curl --silent "https://api.github.com/repos/$1/releases/latest" | # Get latest release from GitHub api
grep '"tag_name":' | # Get tag line
sed -E 's/.*"([^"]+)".*/\1/' # Pluck JSON value
}
# MARK: referral_randomizer()
# Get a random Penguin's Star Citizen referral code
referral_randomizer() {
# Populate the referral codes array
referral_codes=("STAR-4TZD-6KMM" "STAR-4XM2-VM99" "STAR-2NPY-FCR2" "STAR-T9Z9-7W6P" "STAR-VLBF-W2QR" "STAR-BYR6-YHMF" "STAR-3X2H-VZMX" "STAR-BRWN-FB9T" "STAR-FG6Y-N4Q4" "STAR-VLD6-VZRG" "STAR-T9KF-LV77" "STAR-4XHB-R7RF" "STAR-9NVF-MRN7" "STAR-3Q4W-9TC3" "STAR-3SBK-7QTT" "STAR-XFBT-9TTK" "STAR-F3H9-YPHN" "STAR-BYK6-RCCL" "STAR-XCKH-W6T7" "STAR-H292-39WK" "STAR-ZRT5-PJB7" "STAR-GMBP-SH9Y" "STAR-PLWB-LMFY" "STAR-TNZN-H4ZT" "STAR-T5G5-L2GJ" "STAR-6TPV-7QH2" "STAR-THHD-TV3Y" "STAR-7ZFS-PK2L" "STAR-SRQN-43TB" "STAR-9TDG-D4H9" "STAR-BPH3-THJC" "STAR-HL3M-R5KC" "STAR-GBS5-LTVB" "STAR-CJ3Y-KZZ4" "STAR-5GRM-7HBY" "STAR-G2GX-Y2QJ" "STAR-YWY3-H4XX" "STAR-6VGM-PTKC" "STAR-T6MZ-QFHX" "STAR-T2K6-LXFW" "STAR-XN25-9CJJ" "STAR-47V3-4QGB" "STAR-YD4Z-TQZV" "STAR-XLN7-9XNJ" "STAR-N62T-2R39" "STAR-3S3D-9HXQ" "STAR-TRZF-NMCV" "STAR-TLLJ-SMG4" "STAR-MFT6-Q44H" "STAR-TZX2-TPWF" "STAR-WCHN-4ZMX" "STAR-2GHY-WB4F" "STAR-KLM2-R4SX" "STAR-RYXQ-PBZB" "STAR-BSTC-NQPW" "STAR-X32P-J2NS" "STAR-9DMZ-CXWW" "STAR-ZDC2-TDP9" "STAR-J3PJ-RH2K" "STAR-Q6QW-5CC4" "STAR-FLVX-2KGT")
# Pick a random array element. Scale a floating point number for
# a more random distribution than simply calling RANDOM
random_code="${referral_codes[$(awk '{srand($2); print int(rand()*$1)}' <<< "${#referral_codes[@]} $RANDOM")]}"
message info "Your random Penguin's referral code is:\n\n$random_code\n\nThank you!"
}
# MARK: quit()
quit() {
exit 0
}
############################################################################
######## MAIN ##############################################################
############################################################################
# MARK: MAIN
# Zenity availability/version check
use_zenity=0
# Initialize some variables
menu_option_height="0"
menu_text_height_zenity4="0"
menu_height_max="0"
if [ -x "$(command -v zenity)" ]; then
if zenity --version >/dev/null; then
use_zenity=1
zenity_version="$(zenity --version)"
# Zenity 4.0.0 uses libadwaita, which changes fonts/sizing
# Add pixels to each menu option depending on the version of zenity in use
# used to dynamically determine the height of menus
# menu_text_height_zenity4 = Add extra pixels to the menu title/description height for libadwaita bigness
if [ "$zenity_version" != "4.0.0" ] &&
[ "$zenity_version" = "$(printf "%s\n%s" "$zenity_version" "4.0.0" | sort -V | head -n1)" ]; then
# zenity 3.x menu sizing
menu_option_height="26"
menu_text_height_zenity4="0"
menu_height_max="400"
else
# zenity 4.x+ menu sizing
menu_option_height="26"
menu_text_height_zenity4="0"
menu_height_max="800"
fi
else
# Zenity is broken
debug_print continue "Zenity failed to start. Falling back to terminal menus"
fi
fi
# Check if this is the user's first run of the Helper
if [ -f "$conf_dir/$conf_subdir/$firstrun_conf" ]; then
is_firstrun="$(cat "$conf_dir/$conf_subdir/$firstrun_conf")"
fi
if [ "$is_firstrun" != "false" ]; then
is_firstrun="true"
fi
# Format some URLs for Zenity if the Helper was not invoked with command-line arguments (handle those separately below)
if [ "$#" -eq 0 ]; then
format_urls
fi
# Check if a newer verison of the script is available
latest_version="$(get_latest_release "$repo")"
# Sort the versions and check if the installed Helper is smaller
if [ "$latest_version" != "$current_version" ] &&
[ "$current_version" = "$(printf "%s\n%s" "$current_version" "$latest_version" | sort -V | head -n1)" ]; then
message info "The latest version of the LUG Helper is $latest_version\nYou are using $current_version\n\nYou can download new releases here:\n$releases_url"
fi
# MARK: Cmdline arguments
# If invoked with command line arguments, process them and exit
if [ "$#" -gt 0 ]; then
while [ "$#" -gt 0 ]
do
# Victor_Tramp expects the spanish inquisition.
case "$1" in
--help | -h )
printf "Star Citizen Linux Users Group Helper Script
Usage: lug-helper
-p, --preflight-check Run system optimization checks
-i, --install Install Star Citizen
-m, --manage-runners Install or remove Wine runners
-k, --update-dxvk Update DXVK in the Wine prefix
-e, --edit-launch-script Edit the game launch script
-c, --wine-config Launch winecfg for the game's prefix
-j, --wine-controllers Launch Wine controllers configuration
-g, --no-gui Use terminal menus instead of a Zenity GUI
-r, --get-referral Get a random LUG member's referral code
-d, --show-directories Show all Star Citizen and Helper directories
-w, --show-wiki Show the LUG Wiki
-x, --reset-helper Delete saved lug-helper configs
-v, --version Display version info and exit
"
exit 0
;;
--preflight-check | -p )
cargs+=("preflight_check")
;;
--install | -i )
cargs+=("install_game_wine")
;;
--manage-runners | -m )
cargs+=("runner_manage_wine")
;;
--update-dxvk | -k )
cargs+=("dxvk_update_wine")
;;
--edit-launch-script | -e )
cargs+=("edit_wine_launch_script")
;;
--wine-config | -c )
cargs+=("call_launch_script config")
;;
--wine-controllers | -j )
cargs+=("call_launch_script controllers")
;;
--no-gui | -g )
# If zenity is unavailable, it has already been set to 0
# and this setting has no effect
use_zenity=0
;;
--get-referral | -r )
cargs+=("referral_randomizer")
;;
--show-directories | -d )
cargs+=("display_dirs")
;;
--show-wiki | -w )
cargs+=("display_wiki")
;;
--reset-helper | -x )
cargs+=("reset_helper")
;;
--version | -v )
printf "LUG Helper %s\n" "$current_version"
exit 0
;;
* )
printf "$0: Invalid option '%s'\n" "$1"
exit 0
;;
esac
# Shift forward to the next argument and loop again
shift
done
# Format some URLs for Zenity
format_urls
# Call the requested functions and exit
if [ "${#cargs[@]}" -gt 0 ]; then
cmd_line="true"
for (( x=0; x<"${#cargs[@]}"; x++ )); do
${cargs[x]}
done
exit 0
fi
fi
# Detect if NixOS is being used and direct user to wiki
if (grep -q '^NAME=NixOS' /etc/os-release 2> /dev/null ); then
message info "It looks like you're using NixOS\nPlease see our wiki for NixOS-specific configuration requirements:\n\n$lug_wiki_nixos"
fi
# Set up the main menu heading
menu_heading_zenity="Greetings, Space Penguin!\n\nThis tool is provided by the Star Citizen Linux Users Group\nFor help, see our wiki: $lug_wiki"
menu_heading_terminal="Greetings, Space Penguin!\n\nThis tool is provided by the Star Citizen Linux Users Group\nFor help, see our wiki: $lug_wiki"
# MARK: First Run
# First run
firstrun_message="It looks like this is your first time running the Helper\n\nWould you like to run the Preflight Check and install Star Citizen?"
if [ "$use_zenity" -eq 1 ]; then
firstrun_message="$menu_heading_zenity\n\n$firstrun_message"
else
firstrun_message="$menu_heading_terminal\n\n$firstrun_message"
fi
if [ "$is_firstrun" = "true" ]; then
if message question "$firstrun_message"; then
install_game_wine
fi
# Store the first run state for subsequent launches
if [ ! -d "$conf_dir/$conf_subdir" ]; then
mkdir -p "$conf_dir/$conf_subdir"
fi
echo "false" > "$conf_dir/$conf_subdir/$firstrun_conf"
fi
# MARK: Main Menu
# Loop the main menu until the user selects quit
while true; do
# Configure the menu
menu_text_zenity="$menu_heading_zenity"
menu_text_terminal="$menu_heading_terminal"
menu_text_height="300"
menu_type="radiolist"
# Configure the menu options
preflight_msg="Preflight Check (System Optimization)"
install_msg_wine="Install Star Citizen"
runners_msg_wine="Manage Wine Runners"
dxvk_msg_wine="Update DXVK"
maintenance_msg="Maintenance and Troubleshooting"
randomizer_msg="Get a random Penguin's Star Citizen referral code"
quit_msg="Quit"
# Set the options to be displayed in the menu
menu_options=("$preflight_msg" "$install_msg_wine" "$runners_msg_wine" "$dxvk_msg_wine" "$maintenance_msg" "$randomizer_msg" "$quit_msg")
# Set the corresponding functions to be called for each of the options
menu_actions=("preflight_check" "install_game_wine" "runner_manage_wine" "dxvk_update_wine" "maintenance_menu" "referral_randomizer" "quit")
# Calculate the total height the menu should be
# menu_option_height = pixels per menu option
# #menu_options[@] = number of menu options
# menu_text_height = height of the title/description text
# menu_text_height_zenity4 = added title/description height for libadwaita bigness
menu_height="$(($menu_option_height * ${#menu_options[@]} + $menu_text_height + $menu_text_height_zenity4))"
# Set the label for the cancel button
cancel_label="Quit"
# Call the menu function. It will use the options as configured above
menu
done