#!/usr/bin/env bash # # Unreal Tournament: GOTY Linux Installer # # shellcheck source-path=SCRIPTDIR # ARGBASH_SET_INDENT([ ]) # ARG_OPTIONAL_SINGLE([destination],[d],[Install directory. Will be created if it doesn't exist.],[${XDG_DATA_HOME:-${HOME}/.local/share}/OldUnreal/UnrealTournament]) # ARG_OPTIONAL_SINGLE([ui-mode],[],[UI library to use during install.],[auto]) # ARG_TYPE_GROUP_SET([uimode],[MODE],[ui-mode],[auto,kdialog,zenity,none]) # ARG_OPTIONAL_SINGLE([application-entry],[],[Action to take when installing the XDG Application Entry.],[prompt]) # ARG_OPTIONAL_SINGLE([desktop-shortcut],[],[Action to take when installing a desktop shortcut.],[prompt]) # ARG_TYPE_GROUP_SET([entryhandlingmode],[ACTION],[application-entry,desktop-shortcut],[install,prompt,skip]) # ARG_OPTIONAL_BOOLEAN([unrealed],[e],[Install UnrealEd (Windows, umu-launcher recommended).],[]) # ARG_OPTIONAL_BOOLEAN([keep-installer-files],[k],[Keep ISO and Patch files.],[]) # ARG_HELP([Install Unreal Tournament: GOTY]) # ARG_VERSION_AUTO([1.2.1],['OldUnreal ']) # DEFINE_SCRIPT_DIR([_SCRIPT_DIR]) # ARGBASH_GO() # needed because of Argbash --> m4_ignore([ ### START OF CODE GENERATED BY Argbash v2.11.0 one line above ### # Argbash is a bash code generator used to get arguments parsing right. # Argbash is FREE SOFTWARE, see https://argbash.dev for more info die() { local _ret="${2:-1}" test "${_PRINT_HELP:-no}" = yes && print_help >&2 echo "$1" >&2 exit "${_ret}" } # validators uimode() { local _allowed=("auto" "kdialog" "zenity" "none") _seeking="$1" for element in "${_allowed[@]}"; do test "$element" = "$_seeking" && echo "$element" && return 0 done die "Value '$_seeking' (of argument '$2') doesn't match the list of allowed values: 'auto', 'kdialog', 'zenity' and 'none'" 4 } entryhandlingmode() { local _allowed=("install" "prompt" "skip") _seeking="$1" for element in "${_allowed[@]}"; do test "$element" = "$_seeking" && echo "$element" && return 0 done die "Value '$_seeking' (of argument '$2') doesn't match the list of allowed values: 'install', 'prompt' and 'skip'" 4 } begins_with_short_option() { local first_option all_short_options='dekhv' first_option="${1:0:1}" test "$all_short_options" = "${all_short_options/$first_option/}" && return 1 || return 0 } # THE DEFAULTS INITIALIZATION - OPTIONALS _arg_destination="${XDG_DATA_HOME:-${HOME}/.local/share}/OldUnreal/UnrealTournament" _arg_ui_mode="auto" _arg_application_entry="prompt" _arg_desktop_shortcut="prompt" _arg_unrealed="off" _arg_keep_installer_files="off" print_help() { printf '%s\n' "Install Unreal Tournament: GOTY" printf 'Usage: %s [-d|--destination ] [--ui-mode ] [--application-entry ] [--desktop-shortcut ] [-e|--(no-)unrealed] [-k|--(no-)keep-installer-files] [-h|--help] [-v|--version]\n' "$0" printf '\t%s\n' "-d, --destination: Install directory. Will be created if it doesn't exist. (default: '${XDG_DATA_HOME:-${HOME}/.local/share}/OldUnreal/UnrealTournament')" printf '\t%s\n' "--ui-mode: UI library to use during install.. Can be one of: 'auto', 'kdialog', 'zenity' and 'none' (default: 'auto')" printf '\t%s\n' "--application-entry: Action to take when installing the XDG Application Entry.. Can be one of: 'install', 'prompt' and 'skip' (default: 'prompt')" printf '\t%s\n' "--desktop-shortcut: Action to take when installing a desktop shortcut.. Can be one of: 'install', 'prompt' and 'skip' (default: 'prompt')" printf '\t%s\n' "-e, --unrealed, --no-unrealed: Install UnrealEd (Windows, umu-launcher recommended). (off by default)" printf '\t%s\n' "-k, --keep-installer-files, --no-keep-installer-files: Keep ISO and Patch files. (off by default)" printf '\t%s\n' "-h, --help: Prints help" printf '\t%s\n' "-v, --version: Prints version" } parse_commandline() { local _key while test $# -gt 0; do _key="$1" case "$_key" in -d | --destination) test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 _arg_destination="$2" shift ;; --destination=*) _arg_destination="${_key##--destination=}" ;; -d*) _arg_destination="${_key##-d}" ;; --ui-mode) test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 _arg_ui_mode="$(uimode "$2" "ui-mode")" || exit 1 shift ;; --ui-mode=*) _arg_ui_mode="$(uimode "${_key##--ui-mode=}" "ui-mode")" || exit 1 ;; --application-entry) test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 _arg_application_entry="$(entryhandlingmode "$2" "application-entry")" || exit 1 shift ;; --application-entry=*) _arg_application_entry="$(entryhandlingmode "${_key##--application-entry=}" "application-entry")" || exit 1 ;; --desktop-shortcut) test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 _arg_desktop_shortcut="$(entryhandlingmode "$2" "desktop-shortcut")" || exit 1 shift ;; --desktop-shortcut=*) _arg_desktop_shortcut="$(entryhandlingmode "${_key##--desktop-shortcut=}" "desktop-shortcut")" || exit 1 ;; -e | --no-unrealed | --unrealed) _arg_unrealed="on" test "${1:0:5}" = "--no-" && _arg_unrealed="off" ;; -e*) _arg_unrealed="on" _next="${_key##-e}" if test -n "$_next" -a "$_next" != "$_key"; then { begins_with_short_option "$_next" && shift && set -- "-e" "-${_next}" "$@"; } || die "The short option '$_key' can't be decomposed to ${_key:0:2} and -${_key:2}, because ${_key:0:2} doesn't accept value and '-${_key:2:1}' doesn't correspond to a short option." fi ;; -k | --no-keep-installer-files | --keep-installer-files) _arg_keep_installer_files="on" test "${1:0:5}" = "--no-" && _arg_keep_installer_files="off" ;; -k*) _arg_keep_installer_files="on" _next="${_key##-k}" if test -n "$_next" -a "$_next" != "$_key"; then { begins_with_short_option "$_next" && shift && set -- "-k" "-${_next}" "$@"; } || die "The short option '$_key' can't be decomposed to ${_key:0:2} and -${_key:2}, because ${_key:0:2} doesn't accept value and '-${_key:2:1}' doesn't correspond to a short option." fi ;; -h | --help) print_help exit 0 ;; -h*) print_help exit 0 ;; -v | --version) printf '%s %s\n\n%s\n%s\n' "install-ut99.sh" "1.2.1" 'Install Unreal Tournament: GOTY' 'OldUnreal ' exit 0 ;; -v*) printf '%s %s\n\n%s\n%s\n' "install-ut99.sh" "1.2.1" 'Install Unreal Tournament: GOTY' 'OldUnreal ' exit 0 ;; *) _PRINT_HELP=yes die "FATAL ERROR: Got an unexpected argument '$1'" 1 ;; esac shift done } parse_commandline "$@" # OTHER STUFF GENERATED BY Argbash _SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || { echo "Couldn't determine the script's running directory, which probably matters, bailing out" >&2 exit 2 } # Validation of values ### END OF CODE GENERATED BY Argbash (sortof) ### ]) # [ <-- needed because of Argbash # Enable Bash Strict Mode set -euo pipefail installer::entrypoint() { # Defining Installation Parameters local PRODUCT_NAME="Unreal Tournament: GOTY" local PRODUCT_SHORTNAME="UnrealTournament" local PRODUCT_KEYWORDS=("UT" "UT99" "Unreal Tournament 1999") local PRODUCT_URLSCHEME="unreal" local MAIN_BINARY_NAME="ut-bin" # Import Library files # shellcheck shell=bash declare -A ansi_colornum=( [black]=0 [red]=1 [green]=2 [yellow]=3 [blue]=4 [magenta]=5 [cyan]=6 [white]=7 ) declare -A ansi_stylenum=( [reset]=0 [bright]=1 [dim]=2 [italic]=3 [underline]=4 [flash]=5 [highlight]=7 [normal]=22 ) # UI Mode auto-detection if [[ -z "${_arg_ui_mode:-}" ]] || [[ "${_arg_ui_mode:-}" == "auto" ]]; then if ! { [[ -n "${DISPLAY:-}" ]] || [[ -n "${WAYLAND_DISPLAY:-}" ]]; } || { [[ -n "${SSH_CLIENT:-}" ]] || [[ -n "${SSH_TTY:-}" ]]; }; then # If we do not have a display, or we are in a SSH session, fallback to text mode _arg_ui_mode="none" elif { [[ "${XDG_CURRENT_DESKTOP:-}" == "KDE" ]] && command -v kdialog &>/dev/null && command -v busctl &>/dev/null; }; then # If we are on KDE, and kdialog + busctl is available, use kdialog _arg_ui_mode="kdialog" elif command -v zenity &>/dev/null; then # Use Zenity if available _arg_ui_mode="zenity" elif command -v kdialog &>/dev/null && command -v busctl &>/dev/null; then # If kdialog is available, but zenity is not (outside of KDE) _arg_ui_mode="kdialog" else # If nothing is available, only run in text mode _arg_ui_mode="none" fi fi # Set-up fd6 as an alternate STDOUT for when we are doing operations with pipes, but # still want to output progress exec 6>&1 local PROGRESS_MIN_REFRESH=0.1 ansi::styled() { ALLOW_ESCAPES="off" if [[ "${1:-}" == "-e" ]]; then ALLOW_ESCAPES="on" shift fi local TEXT_TO_PRINT="${1:-}" local STYLE="${2:--1}" local FORE_COLOR="${3:--1}" local BACK_COLOR="${4:--1}" declare -a CODES RESETCODES if [[ "${STYLE}" -eq 22 ]] || [[ "${STYLE}" -eq -1 ]]; then RESETCODES=("$(printf "\033[%sm" "22")" "${RESETCODES[@]}") else CODES=("${CODES[@]}" "$(printf "\033[%sm" "${STYLE}")") fi if [[ "${BACK_COLOR}" -eq -1 ]]; then RESETCODES=("$(printf "\033[%sm" "49")" "${RESETCODES[@]}") else CODES=("${CODES[@]}" "$(printf "\033[%sm" "$((BACK_COLOR + 40))")") fi if [[ "${FORE_COLOR}" -eq -1 ]]; then RESETCODES=("$(printf "\033[%sm" "39")" "${RESETCODES[@]}") else CODES=("${CODES[@]}" "$(printf "\033[%sm" "$((FORE_COLOR + 30))")") fi local rc for rc in "${RESETCODES[@]}"; do echo -en "$rc" done local c for c in "${CODES[@]}"; do echo -en "$c" done if [[ "${ALLOW_ESCAPES}" == "on" ]]; then echo -en "${TEXT_TO_PRINT}" else echo -n "${TEXT_TO_PRINT}" fi echo -en "\033[m" } ansi::banner() { local TOPEDGE BOTTOMEDGE TOPEDGE="┏━${1//?/━}━┓" BOTTOMEDGE="┗━${1//?/━}━┛" printf "%s\n" "$(ansi::styled "${TOPEDGE}" "${ansi_stylenum[bright]}")" printf "%s\n" "$(ansi::styled "┃ ${1} ┃" "${ansi_stylenum[bright]}")" printf "%s\n" "$(ansi::styled "${BOTTOMEDGE}" "${ansi_stylenum[bright]}")" } term::error() { echo -e "$(ansi::styled "Error:" "${ansi_stylenum[bright]}" "${ansi_colornum[red]}") $*" 1>&2 } term::yesno() { local QUESTION="${1:-}" local DEFAULT="${2:-N}" local DEFAULT_PROMPT="[yN]" if [[ "${DEFAULT}" =~ ^[Yy]$ ]]; then DEFAULT_PROMPT="[Yn]" fi # In case the user was impatient and typed a whole bunch of crap in their terminal while waiting for a download # clear the STDIN buffer if test -t 0; then local discard read -r -n 1000000 -t 0.001 discard || true if [[ -n "${discard:-}" ]]; then echo fi fi read -p "$(ansi::styled "?" "${ansi_stylenum[bright]}" "${ansi_colornum[green]}") ${QUESTION} $(ansi::styled "${DEFAULT_PROMPT}" "${ansi_stylenum[bright]}") " -n 1 -r echo if [[ "${DEFAULT}" =~ ^[Nn]$ ]]; then if [[ ! "${REPLY}" =~ ^[Yy]$ ]]; then return 1 fi elif [[ "${DEFAULT}" =~ ^[Yy]$ ]]; then if [[ "${REPLY}" =~ ^[Nn]$ ]]; then return 1 fi fi return 0 } local CURRENT_STEP_NAME="" local STEP_PROGRESS_SPINNER_CHARS=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏") local STEP_PROGRESS_SPINNER_CURRENT=0 term::step::new() { CURRENT_STEP_NAME="$*" echo -en " $(ansi::styled "[ ]" "${ansi_stylenum[bright]}") $*" } term::step::replace() { CURRENT_STEP_NAME="$*" echo -en "\r\033[K $(ansi::styled "[ ]" "${ansi_stylenum[bright]}") $*" } term::step::progress() { local STEP_PROGRESS_TEXT="" if [[ -n "${1:-}" ]]; then STEP_PROGRESS_TEXT="$(ansi::styled " ($*)" "${ansi_stylenum[dim]}")" fi echo -en "\r $(ansi::styled "[" "${ansi_stylenum[bright]}") $(ansi::styled "${STEP_PROGRESS_SPINNER_CHARS[$STEP_PROGRESS_SPINNER_CURRENT]}" "${ansi_stylenum[dim]}") $(ansi::styled "]" "${ansi_stylenum[bright]}") ${CURRENT_STEP_NAME}${STEP_PROGRESS_TEXT}\033[K" STEP_PROGRESS_SPINNER_CURRENT=$((STEP_PROGRESS_SPINNER_CURRENT + 1)) if [[ "${STEP_PROGRESS_SPINNER_CURRENT}" -ge "${#STEP_PROGRESS_SPINNER_CHARS[@]}" ]]; then STEP_PROGRESS_SPINNER_CURRENT=0 fi } term::step::complete() { echo -e "\r\033[K $(ansi::styled "[" "${ansi_stylenum[bright]}") $(ansi::styled "✓" "${ansi_stylenum[bright]}" "${ansi_colornum[green]}") $(ansi::styled "]" "${ansi_stylenum[bright]}") ${CURRENT_STEP_NAME}" } term::step::failed() { echo -e "\r\033[K $(ansi::styled "[" "${ansi_stylenum[bright]}") $(ansi::styled "✗" "${ansi_stylenum[bright]}" "${ansi_colornum[red]}") $(ansi::styled "]" "${ansi_stylenum[bright]}") ${CURRENT_STEP_NAME}" } term::step::skipped() { local STEP_SKIPPED_REASON="" if [[ -n "${1:-}" ]]; then STEP_SKIPPED_REASON="$(ansi::styled " ($*)" "${ansi_stylenum[dim]}")" fi echo -e "\r\033[K $(ansi::styled "[" "${ansi_stylenum[bright]}") $(ansi::styled "-" "${ansi_stylenum[dim]}") $(ansi::styled "]" "${ansi_stylenum[bright]}") ${CURRENT_STEP_NAME}${STEP_SKIPPED_REASON}" } term::step::failed_with_error() { term::step::failed echo local ERROR_TEXT="$*" term::error "${ERROR_TEXT}" 1>&2 if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then kdialog --title "Error" \ --error "${ERROR_TEXT}" 2>/dev/null elif [[ "${_arg_ui_mode:-none}" == "zenity" ]]; then zenity --error --text="${ERROR_TEXT}" --width=350 2>/dev/null fi } xdgdirs::get_user_dir() { local USER_DIR_NAME="${1}" if command -v xdg-user-dir &>/dev/null; then local USER_DIR_RETURNED USER_DIR_RETURNED=$(xdg-user-dir "${USER_DIR_NAME}") if [[ -d "${USER_DIR_RETURNED}" ]]; then echo "${USER_DIR_RETURNED}" fi return 0 fi local USER_DIR_VAR_NAME="XDG_${USER_DIR_NAME}_DIR" if [[ -n "${!USER_DIR_VAR_NAME:-}" ]] && [[ -d "${!USER_DIR_VAR_NAME}" ]]; then echo "${!USER_DIR_VAR_NAME}" fi } helper::progress::run_with_progress() { local LAST_UPDATE="" local PROGRESS_MESSAGE="${1}" shift while IFS= read -r UPDATE; do term::step::progress "${PROGRESS_MESSAGE}" >&6 LAST_UPDATE="${UPDATE}" done < <(helper::progress::make_consistant "$@") echo "${LAST_UPDATE}" } # This function throttles the output to avoid buffering, while also making sure the last update # is repeated at a set frequency for the throbber to work helper::progress::make_consistant() { local LAST_UPDATE="" local CURRENT_UPDATE="" exec 3< <(helper::run_as_proc_group "$@") local PROC_SUB_PID=$! trap '[ -n "${PROC_SUB_PID:-}" ] && { kill -- "${PROC_SUB_PID}"; }' EXIT local CAN_WRITE="y" trap 'CAN_WRITE="n"' SIGPIPE while kill -0 "${PROC_SUB_PID}" 2>/dev/null; do while IFS= read -r -u 3 -t 0.001 CURRENT_UPDATE; do LAST_UPDATE="${CURRENT_UPDATE}" done if [[ "${CAN_WRITE}" == "y" ]]; then echo "${LAST_UPDATE}" 2>/dev/null sleep "${PROGRESS_MIN_REFRESH}" else break fi done trap - SIGPIPE EXIT local EXIT_CODE=1 if [[ "${CAN_WRITE}" == "y" ]]; then # Final drain of remaining data while IFS= read -r -u 3 CURRENT_UPDATE; do LAST_UPDATE="${CURRENT_UPDATE}" done echo "${LAST_UPDATE}" else if kill -0 "${PROC_SUB_PID}" 2>/dev/null; then # Explicitly kill if the loop exited due to CAN_WRITE="n" kill -TERM -- "${PROC_SUB_PID}" fi return 1 fi wait "${PROC_SUB_PID}" EXIT_CODE=$? exec 3<&- return "${EXIT_CODE}" } helper::run_as_proc_group() { set -m { "$@"; } & set +m local PROC_SUB_PID=$! trap 'trap - EXIT; [[ -n "${PROC_SUB_PID:-}" ]] && { kill -- -"${PROC_SUB_PID}"; wait "${PROC_SUB_PID}"; return $?; }' EXIT wait "${PROC_SUB_PID}" local EXIT_CODE=$? trap - EXIT return $? } helper::string::unshift::next_value() { __helper::string::unshift "${1:-}" "${2:-}" "v" } helper::string::unshift::remainder() { __helper::string::unshift "${1:-}" "${2:-}" "r" } __helper::string::unshift() { local VAR_CURRENT_VALUE="${1:-}" local SEPARATOR="${2:-|}" local RETURN="${3}" local SHIFTED_VALUE local REMAINING_VALUE if [[ "${VAR_CURRENT_VALUE}" == *"${SEPARATOR}"* ]]; then SHIFTED_VALUE="${VAR_CURRENT_VALUE%%"${SEPARATOR}"*}" REMAINING_VALUE="${VAR_CURRENT_VALUE#*"${SEPARATOR}"}" else SHIFTED_VALUE="${VAR_CURRENT_VALUE}" REMAINING_VALUE="" fi if [[ "${RETURN}" == "r" ]]; then echo "${REMAINING_VALUE}" return 0 fi echo "${SHIFTED_VALUE}" } # shellcheck shell=bash local ARCHITECTURE_SUFFIX ARCHITECTURE_BINARY_SUFFIX UE_SYSTEM_FOLDER_SUFFIX UEED_SYSTEM_FOLDER_SUFFIX local DETECTED_ARCHITECTURE DETECTED_ARCHITECTURE=$(uname -m) case "${DETECTED_ARCHITECTURE}" in x86_64 | amd64) case "${PRODUCT_SHORTNAME}" in UT2004) ARCHITECTURE_SUFFIX='amd64' ARCHITECTURE_BINARY_SUFFIX='' UE_SYSTEM_FOLDER_SUFFIX='' UEED_SYSTEM_FOLDER_SUFFIX='' ;; UnrealTournament) ARCHITECTURE_SUFFIX='amd64' ARCHITECTURE_BINARY_SUFFIX='-amd64' UE_SYSTEM_FOLDER_SUFFIX='64' UEED_SYSTEM_FOLDER_SUFFIX='' ;; *) ARCHITECTURE_SUFFIX='amd64' ARCHITECTURE_BINARY_SUFFIX='-amd64' UE_SYSTEM_FOLDER_SUFFIX='64' UEED_SYSTEM_FOLDER_SUFFIX='64' ;; esac ;; aarch64) case "${PRODUCT_SHORTNAME}" in UT2004) ARCHITECTURE_SUFFIX='arm64' ARCHITECTURE_BINARY_SUFFIX='' UE_SYSTEM_FOLDER_SUFFIX='ARM64' UEED_SYSTEM_FOLDER_SUFFIX='' ;; UnrealTournament) ARCHITECTURE_SUFFIX='arm64' ARCHITECTURE_BINARY_SUFFIX='-arm64' UE_SYSTEM_FOLDER_SUFFIX='ARM64' UEED_SYSTEM_FOLDER_SUFFIX='' ;; *) ARCHITECTURE_SUFFIX='arm64' ARCHITECTURE_BINARY_SUFFIX='-arm64' UE_SYSTEM_FOLDER_SUFFIX='ARM64' UEED_SYSTEM_FOLDER_SUFFIX='64' ;; esac ;; i386 | i686) case "${PRODUCT_SHORTNAME}" in UT2004) ARCHITECTURE_SUFFIX='NOT_SUPPORTED' ARCHITECTURE_BINARY_SUFFIX='' UE_SYSTEM_FOLDER_SUFFIX='' UEED_SYSTEM_FOLDER_SUFFIX='' ;; *) ARCHITECTURE_SUFFIX='x86' ARCHITECTURE_BINARY_SUFFIX='-x86' UE_SYSTEM_FOLDER_SUFFIX='' UEED_SYSTEM_FOLDER_SUFFIX='' ;; esac ;; *) ARCHITECTURE_SUFFIX='NOT_SUPPORTED' ARCHITECTURE_BINARY_SUFFIX='' UE_SYSTEM_FOLDER_SUFFIX='' UEED_SYSTEM_FOLDER_SUFFIX='' ;; esac # shellcheck shell=bash # Downloader local DOWNLOADER_TIMEOUT=15 local DOWNLOADER_API_BIN="" local DOWNLOADER_API_TYPE="" local DOWNLOADER_DL_BIN="" local DOWNLOADER_DL_TYPE="" local DOWNLOADER_USER_AGENT="OldUnreal-${PRODUCT_SHORTNAME}-Linux-Installer/1.2.1" # For archive.org links, aria2c will be instructed to open multiple connections at the same time local ARIA2C_ARCHIVEORG_CONNECTIONS="${OLDUNREAL_ARCHIVEORG_ARIA2C_CONNECTIONS:-4}" # Check which command should be used for API calls if command -v "curl" &>/dev/null; then DOWNLOADER_API_BIN="curl" DOWNLOADER_API_TYPE="curl" elif command -v "wget" &>/dev/null; then DOWNLOADER_API_BIN="wget" DOWNLOADER_API_TYPE="wget" # Check if provided wget version is Wget2... Thanks Fedora :( if [[ "$(wget --version)" =~ " Wget2 " ]]; then DOWNLOADER_API_TYPE="wget2" fi elif command -v "wget2" &>/dev/null; then DOWNLOADER_API_BIN="wget2" DOWNLOADER_API_TYPE="wget2" fi # Check which command should be used for downloads if command -v "aria2c" &>/dev/null; then DOWNLOADER_DL_BIN="aria2c" DOWNLOADER_DL_TYPE="aria2c" else DOWNLOADER_DL_BIN="${DOWNLOADER_API_BIN}" DOWNLOADER_DL_TYPE="${DOWNLOADER_API_TYPE}" fi downloader::download_file() { if [[ -z "${1:-}" ]] || [[ -z "${2:-}" ]]; then return 1 fi local CURRENT_DOWNLOAD_URL_SET REMAINING_DOWNLOAD_URL_SETS CURRENT_DOWNLOAD_URL_SET=$(helper::string::unshift::next_value "${1}" ";;") REMAINING_DOWNLOAD_URL_SETS=$(helper::string::unshift::remainder "${1}" ";;") local DOWNLOAD_PATH="${2}" local IS_RETRY="${3:-no}" __downloader::download_file "${CURRENT_DOWNLOAD_URL_SET}" "${DOWNLOAD_PATH}" "${IS_RETRY}" || downloader::download_file "${REMAINING_DOWNLOAD_URL_SETS}" "${DOWNLOAD_PATH}" "yes" } downloader::fetch_json() { local ENDPOINT_URL="${1:-}" if [[ -z "${ENDPOINT_URL}" ]]; then return 1 fi local IS_SPECIAL_URL IS_SPECIAL_URL=$(__downloader::identify_special_host "${ENDPOINT_URL}") local RESOLVED_GITHUB_TOKEN="" if [[ "${IS_SPECIAL_URL}" == "github.com" ]]; then RESOLVED_GITHUB_TOKEN="${GITHUB_TOKEN:-${GH_TOKEN:-}}" fi if [[ "${DOWNLOADER_API_TYPE}" == "curl" ]]; then local ADDITIONAL_ARGS=() if [[ "${IS_SPECIAL_URL}" == "github.com" ]] && [[ -n "${RESOLVED_GITHUB_TOKEN}" ]]; then ADDITIONAL_ARGS+=("--header" "Authorization: Bearer ${RESOLVED_GITHUB_TOKEN}") fi "${DOWNLOADER_API_BIN}" -Ls "${ADDITIONAL_ARGS[@]}" \ --compressed --connect-timeout "${DOWNLOADER_TIMEOUT}" \ --user-agent "${DOWNLOADER_USER_AGENT}" \ "${ENDPOINT_URL}" 2>/dev/null elif [[ "${DOWNLOADER_API_TYPE}" == "wget" ]] || [[ "${DOWNLOADER_API_TYPE}" == "wget2" ]]; then local ADDITIONAL_ARGS=() if [[ "${IS_SPECIAL_URL}" == "github.com" ]] && [[ -n "${RESOLVED_GITHUB_TOKEN}" ]]; then ADDITIONAL_ARGS+=("--header=Authorization: Bearer ${RESOLVED_GITHUB_TOKEN}") fi "${DOWNLOADER_API_BIN}" -q "${ADDITIONAL_ARGS[@]}" \ --timeout="${DOWNLOADER_TIMEOUT}" \ --user-agent="${DOWNLOADER_USER_AGENT}" \ "${ENDPOINT_URL}" -O - -o /dev/null 2>/dev/null fi } downloader::build_download_source_definition() { local SEPARATOR=";;" local SOURCE_DEF="" if [[ $# -eq 0 ]]; then return 1 fi while [[ $# -gt 0 ]]; do local SOURCES_GROUP_VAR_NAME="${1}" shift if [[ -z "${SOURCES_GROUP_VAR_NAME}" ]]; then return 1 fi local SOURCES_GROUP=() local SOURCES_GROUP_VAR_REF="${SOURCES_GROUP_VAR_NAME}[@]" SOURCES_GROUP=("${!SOURCES_GROUP_VAR_REF}") if [[ "${#SOURCES_GROUP[@]}" -gt 0 ]]; then while IFS= read -r SHUFFLED_ITEM; do if [[ -n "${SOURCE_DEF}" ]]; then SOURCE_DEF="${SOURCE_DEF}${SEPARATOR}" fi SOURCE_DEF="${SOURCE_DEF}${SHUFFLED_ITEM}" done < <(shuf -e "${SOURCES_GROUP[@]}") fi done echo "${SOURCE_DEF}" } downloader::compute_sha256sum() { local FILEPATH="${1:-}" if [[ -z "${FILEPATH}" ]] || [[ ! -f "${FILEPATH}" ]]; then return 1 fi if command -v sha256sum &>/dev/null; then sha256sum "${FILEPATH}" | cut -f1 -d' ' elif command -v shasum &>/dev/null; then shasum -a 256 "${FILEPATH}" | cut -f1 -d' ' fi } __downloader::download_file() { local CURRENT_DOWNLOAD_URL_SET="${1:-}" local DOWNLOAD_PATH="${2:-}" local IS_RETRY="${3:-no}" if [[ -z "${CURRENT_DOWNLOAD_URL_SET}" ]] || [[ -z "${DOWNLOAD_PATH}" ]]; then return 1 fi local DOWNLOAD_URL DOWNLOAD_EXPECTED_SIZE DOWNLOAD_EXPECTED_HASH DOWNLOAD_URL=$(helper::string::unshift::next_value "${CURRENT_DOWNLOAD_URL_SET}") CURRENT_DOWNLOAD_URL_SET=$(helper::string::unshift::remainder "${CURRENT_DOWNLOAD_URL_SET}") DOWNLOAD_EXPECTED_SIZE=$(helper::string::unshift::next_value "${CURRENT_DOWNLOAD_URL_SET}") CURRENT_DOWNLOAD_URL_SET=$(helper::string::unshift::remainder "${CURRENT_DOWNLOAD_URL_SET}") DOWNLOAD_EXPECTED_HASH=$(helper::string::unshift::next_value "${CURRENT_DOWNLOAD_URL_SET}") local DOWNLOAD_FILE="${DOWNLOAD_PATH##*/}" local TARGET_STEP_NAME="Download ${DOWNLOAD_FILE}" if [[ "${IS_RETRY}" == "no" ]]; then if [[ "${CURRENT_STEP_NAME}" != "${TARGET_STEP_NAME}" ]]; then term::step::new "${TARGET_STEP_NAME}" fi term::step::progress "Starting..." else term::step::new "${TARGET_STEP_NAME} (retry)" fi local DOWNLOAD_PROGRESS local KDIALOG_DBUS_ADDRESS=() if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then read -ra KDIALOG_DBUS_ADDRESS < <(kdialog --title "${CURRENT_STEP_NAME}" --progressbar "Starting download..." 0 2>/dev/null) fi helper::progress::make_consistant __downloader::download_file_with_progress "${DOWNLOAD_URL}" "${DOWNLOAD_PATH}" | while IFS= read -r DOWNLOAD_PROGRESS; do if [[ -z "${DOWNLOAD_PROGRESS}" ]]; then term::step::progress "Starting..." >&6 if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then busctl --user call "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "setLabelText" s "Starting Download..." 2>/dev/null || break busctl --user set-property "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "maximum" i 0 2>/dev/null || break fi continue fi local TOTAL_SIZE CURRENT_PROGRESS RECEIVED SPEED DOWNLOAD_PROGRESS=$(helper::string::unshift::remainder "${DOWNLOAD_PROGRESS}") TOTAL_SIZE=$(helper::string::unshift::next_value "${DOWNLOAD_PROGRESS}") DOWNLOAD_PROGRESS=$(helper::string::unshift::remainder "${DOWNLOAD_PROGRESS}") CURRENT_PROGRESS=$(helper::string::unshift::next_value "${DOWNLOAD_PROGRESS}") DOWNLOAD_PROGRESS=$(helper::string::unshift::remainder "${DOWNLOAD_PROGRESS}") RECEIVED=$(helper::string::unshift::next_value "${DOWNLOAD_PROGRESS}") DOWNLOAD_PROGRESS=$(helper::string::unshift::remainder "${DOWNLOAD_PROGRESS}") SPEED=$(helper::string::unshift::next_value "${DOWNLOAD_PROGRESS}") term::step::progress "${CURRENT_PROGRESS}% @ ${SPEED}/s" >&6 local DIALOG_TEXT="Downloading ${DOWNLOAD_FILE}\n${RECEIVED}" if [[ -n "${TOTAL_SIZE:-}" ]]; then DIALOG_TEXT="${DIALOG_TEXT} of ${TOTAL_SIZE} (${CURRENT_PROGRESS}%)" else DIALOG_TEXT="${DIALOG_TEXT} downloaded (${CURRENT_PROGRESS}%)" fi DIALOG_TEXT="${DIALOG_TEXT}\nSpeed : ${SPEED}/s" if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then local ESCAPED_DIALOG_TEXT ESCAPED_DIALOG_TEXT="$(echo -e "${DIALOG_TEXT}")" busctl --user set-property "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "maximum" i 100 2>/dev/null || break busctl --user set-property "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "value" i "${CURRENT_PROGRESS}" 2>/dev/null || break busctl --user call "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "setLabelText" s "${ESCAPED_DIALOG_TEXT}" 2>/dev/null || break # Check for cancellation local WAS_CANCELLED WAS_CANCELLED="$(busctl --user call "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "wasCancelled" 2>/dev/null || echo "b true")" if [[ "${WAS_CANCELLED}" == "b true" ]]; then break fi elif [[ "${_arg_ui_mode:-none}" == "zenity" ]]; then echo "${CURRENT_PROGRESS}" echo "# ${DIALOG_TEXT}" fi done | { if [[ "${_arg_ui_mode:-none}" == "zenity" ]]; then zenity --progress --percentage=0 --text="Starting download..." --time-remaining --auto-close 2>/dev/null else cat - >/dev/null fi } local DOWNLOAD_STATUS="${PIPESTATUS[0]}" if [[ "${DOWNLOAD_STATUS}" -ne 0 ]]; then if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then busctl --user call "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "close" 2>/dev/null || true fi term::step::failed return "${DOWNLOAD_STATUS}" fi if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then busctl --user call "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "close" 2>/dev/null || true fi local DL_RESULT_FILESIZE DL_RESULT_HASH if [[ -n "${DOWNLOAD_EXPECTED_SIZE:-}" ]]; then DL_RESULT_FILESIZE=$(stat --format=%s "${DOWNLOAD_PATH}") || { term::step::failed return 1 } if [[ "${DL_RESULT_FILESIZE}" -ne "${DOWNLOAD_EXPECTED_SIZE}" ]]; then term::step::failed return 1 fi fi if [[ -n "${DOWNLOAD_EXPECTED_HASH:-}" ]]; then DL_RESULT_HASH=$(helper::progress::run_with_progress "Verifying file" downloader::compute_sha256sum "${DOWNLOAD_PATH}") if [[ -n "${DL_RESULT_HASH}" ]] && [[ "${DL_RESULT_HASH}" != "${DOWNLOAD_EXPECTED_HASH}" ]]; then term::step::failed return 1 fi fi term::step::complete } # This function parses the output of the various downloaders to report status information __downloader::download_file_with_progress() { local DOWNLOAD_URL="${1:-}" local DOWNLOAD_PATH="${2:-}" if [[ -z "${DOWNLOAD_URL}" ]] || [[ -z "${DOWNLOAD_PATH}" ]]; then return 1 fi local DOWNLOAD_FILE="${DOWNLOAD_PATH##*/}" local TRANSFER_PROGRESS="" local IS_SPECIAL_URL IS_SPECIAL_URL=$(__downloader::identify_special_host "${DOWNLOAD_URL}") local RESOLVED_GITHUB_TOKEN="" if [[ "${IS_SPECIAL_URL}" == "github.com" ]]; then RESOLVED_GITHUB_TOKEN="${GITHUB_TOKEN:-${GH_TOKEN:-}}" fi if [[ "${DOWNLOADER_DL_TYPE}" == "aria2c" ]]; then local ARIA2C_CAPTURE_REGEX='^\[#[a-z0-9]+\s+([1-9][0-9.]*(B|KiB|MiB|GiB|TiB))/([1-9][0-9.]*(B|KiB|MiB|GiB|TiB))\(([0-9]+)%\)\s+CN:[0-9]+\s+DL:([1-9][0-9.]*(B|KiB|MiB|GiB|TiB))\s+ETA:.+\]$' local ARIA2C_DOWNLOAD_DIRECTORY="${DOWNLOAD_PATH%/*}" local ARIA2C_DOWNLOAD_FILE="${DOWNLOAD_PATH##*/}" if [[ "${IS_SPECIAL_URL}" == "github.com" ]] && [[ -n "${RESOLVED_GITHUB_TOKEN}" ]]; then ADDITIONAL_ARGS+=("--header=Authorization: Bearer ${RESOLVED_GITHUB_TOKEN}") elif [[ "${IS_SPECIAL_URL}" == "archive.org" ]]; then ADDITIONAL_ARGS+=("-x" "${ARIA2C_ARCHIVEORG_CONNECTIONS}") fi stdbuf -o0 -- "${DOWNLOADER_DL_BIN}" --no-conf=true --allow-overwrite=true --remove-control-file=true \ --daemon=false --enable-color=false --stop-with-process="$$" \ --truncate-console-readout=false --console-log-level=warn --summary-interval=0 \ --connect-timeout="${DOWNLOADER_TIMEOUT}" \ --user-agent="${DOWNLOADER_USER_AGENT}" \ --dir="${ARIA2C_DOWNLOAD_DIRECTORY}" --out="${ARIA2C_DOWNLOAD_FILE}" \ "${ADDITIONAL_ARGS[@]}" \ "${DOWNLOAD_URL}" | stdbuf -oL -- tr $'\r' $'\n' | while IFS= read -r TRANSFER_PROGRESS; do if [[ "${TRANSFER_PROGRESS}" =~ ${ARIA2C_CAPTURE_REGEX} ]]; then echo "${DOWNLOAD_FILE}|${BASH_REMATCH[3]}|${BASH_REMATCH[5]}|${BASH_REMATCH[1]}|${BASH_REMATCH[6]}" fi done elif [[ "${DOWNLOADER_DL_TYPE}" == "curl" ]]; then # % Total % Received % Xfered AvgSpdDown AvgSpdUp Time Total Time Spent Time Left Current Speed local CURL_CAPTURE_REGEX='^\s*[0-9]+\s+([1-9][0-9.]*[kKMGT]?)\s+([0-9]+)\s+([0-9.]+[kKMGT]?)\s+[0-9]+\s+[0-9.]+[kKMGT]?\s+[0-9.]+[kKMGT]?\s+[0-9.]+[kKMGT]?\s+[-0-9:]+\s+[-0-9:]+\s+[-0-9:]+\s+([0-9.]+[kKMGT]?)\s*$' local ADDITIONAL_ARGS=() if [[ "${IS_SPECIAL_URL}" == "github.com" ]] && [[ -n "${RESOLVED_GITHUB_TOKEN}" ]]; then ADDITIONAL_ARGS+=("--header" "Authorization: Bearer ${RESOLVED_GITHUB_TOKEN}") fi "${DOWNLOADER_DL_BIN}" -LN --progress-meter "${ADDITIONAL_ARGS[@]}" \ --connect-timeout "${DOWNLOADER_TIMEOUT}" \ --user-agent "${DOWNLOADER_USER_AGENT}" \ "${DOWNLOAD_URL}" -o "${DOWNLOAD_PATH}" 2>&1 | stdbuf -oL -- tr $'\r' $'\n' | while IFS= read -r TRANSFER_PROGRESS; do if [[ "${TRANSFER_PROGRESS}" =~ ${CURL_CAPTURE_REGEX} ]]; then echo "${DOWNLOAD_FILE}|${BASH_REMATCH[1]}|${BASH_REMATCH[2]}|${BASH_REMATCH[3]}|${BASH_REMATCH[4]}" fi done return "${PIPESTATUS[0]}" elif [[ "${DOWNLOADER_DL_TYPE}" == "wget" ]]; then local WGET_LENGTH_CAPTURE_REGEX='^Length:\s+[0-9]+\s+\((.*)\).*$' local WGET_CAPTURE_REGEX='^\s*([0-9.]+[BKMG])\s+[. ]+\s+([0-9]+)%\s+([0-9.]+[BKMG])\s+.*$' local TOTAL_SIZE local ADDITIONAL_ARGS=() if [[ "${IS_SPECIAL_URL}" == "github.com" ]] && [[ -n "${RESOLVED_GITHUB_TOKEN}" ]]; then ADDITIONAL_ARGS+=("--header=Authorization: Bearer ${RESOLVED_GITHUB_TOKEN}") fi "${DOWNLOADER_DL_BIN}" --progress=dot "${ADDITIONAL_ARGS[@]}" \ --timeout="${DOWNLOADER_TIMEOUT}" \ --user-agent="${DOWNLOADER_USER_AGENT}" \ "${DOWNLOAD_URL}" -O "${DOWNLOAD_PATH}" -o - 2>&1 | while IFS= read -r TRANSFER_PROGRESS; do if [[ "${TRANSFER_PROGRESS}" =~ ${WGET_LENGTH_CAPTURE_REGEX} ]]; then TOTAL_SIZE="${BASH_REMATCH[1]}" elif [[ "${TRANSFER_PROGRESS}" =~ ${WGET_CAPTURE_REGEX} ]]; then echo "${DOWNLOAD_FILE}|${TOTAL_SIZE}|${BASH_REMATCH[2]}|${BASH_REMATCH[1]}|${BASH_REMATCH[3]}" fi done return "${PIPESTATUS[0]}" elif [[ "${DOWNLOADER_DL_TYPE}" == "wget2" ]]; then local WGET2_CAPTURE_REGEX='^\[1G.+\s+([0-9]+)%\s+\[.+\]\s+([0-9.]+[BKMG])\s+([0-9.]+[BKMG])(B?/s)?\s*$' local ADDITIONAL_ARGS=() if [[ "${IS_SPECIAL_URL}" == "github.com" ]] && [[ -n "${RESOLVED_GITHUB_TOKEN}" ]]; then ADDITIONAL_ARGS+=("--header=Authorization: Bearer ${RESOLVED_GITHUB_TOKEN}") fi "${DOWNLOADER_DL_BIN}" --progress=bar --force-progress "${ADDITIONAL_ARGS[@]}" \ --timeout="${DOWNLOADER_TIMEOUT}" \ --user-agent="${DOWNLOADER_USER_AGENT}" \ "${DOWNLOAD_URL}" -O "${DOWNLOAD_PATH}" 2>&1 | stdbuf -oL -- tr $'\r\033' $'\n\n' | while IFS= read -r TRANSFER_PROGRESS; do if [[ "${TRANSFER_PROGRESS}" =~ ${WGET2_CAPTURE_REGEX} ]]; then echo "${DOWNLOAD_FILE}||${BASH_REMATCH[1]}|${BASH_REMATCH[2]}|${BASH_REMATCH[3]}" fi done return "${PIPESTATUS[0]}" fi } __downloader::identify_special_host() { local URL="${1:-}" if [[ "${URL}" =~ ^https://(github\.com|.+\.github\.com|.+\.githubusercontent\.com)/ ]]; then echo "github.com" elif [[ "${URL}" =~ ^https://(archive\.org|.+\.archive\.org)/ ]]; then echo "archive.org" fi } # shellcheck shell=bash local UNARCHIVER_BIN="" if command -v "7z" &>/dev/null; then UNARCHIVER_BIN="7z" elif command -v "7zz" &>/dev/null; then UNARCHIVER_BIN="7zz" fi unarchiver::unarchive_file() { local ITEM_NAME="${1:-}" local ARCHIVE_PATH="${2:-}" local TARGET_PATH="${3:-}" local IGNORE_PATTERNS_VAR_NAME="${4:-}" local IGNORE_PATTERNS_RECURSIVE_VAR_NAME="${5:-}" if [[ -z "${ARCHIVE_PATH}" ]] || [[ -z "${TARGET_PATH}" ]]; then return 1 fi if [[ ! -f "${ARCHIVE_PATH}" ]]; then return 1 fi term::step::new "Extract ${ITEM_NAME}" local IS_TARBALL="n" if __unarchiver::is_tarball "${ARCHIVE_PATH}"; then IS_TARBALL="y" fi local UNARCHIVE_PROGRESS local KDIALOG_DBUS_ADDRESS=() if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then read -ra KDIALOG_DBUS_ADDRESS < <(kdialog --title "${CURRENT_STEP_NAME}" --progressbar "Extracting ${ITEM_NAME}..." 0 2>/dev/null) busctl --user call "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "showCancelButton" b "false" 2>/dev/null || true fi helper::progress::make_consistant __unarchiver::unarchive_file_with_progress "${ARCHIVE_PATH}" "${TARGET_PATH}" "${IGNORE_PATTERNS_VAR_NAME}" "${IGNORE_PATTERNS_RECURSIVE_VAR_NAME}" | while IFS= read -r UNARCHIVE_PROGRESS; do if [[ -z "${UNARCHIVE_PROGRESS}" ]]; then term::step::progress "" >&6 if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then busctl --user set-property "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "maximum" i 0 2>/dev/null || true fi continue fi local PROGRESS_TEXT="${UNARCHIVE_PROGRESS}" if [[ "${IS_TARBALL}" == "n" ]]; then PROGRESS_TEXT="${PROGRESS_TEXT}%" fi term::step::progress "${PROGRESS_TEXT}" >&6 local DIALOG_TEXT="Extracting ${ITEM_NAME} (${PROGRESS_TEXT})" if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then local ESCAPED_DIALOG_TEXT ESCAPED_DIALOG_TEXT="$(echo -e "${DIALOG_TEXT}")" if [[ "${IS_TARBALL}" == "n" ]]; then busctl --user set-property "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "maximum" i 100 2>/dev/null || true busctl --user set-property "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "value" i "${UNARCHIVE_PROGRESS}" 2>/dev/null || true fi busctl --user call "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "setLabelText" s "${ESCAPED_DIALOG_TEXT}" 2>/dev/null || true elif [[ "${_arg_ui_mode:-none}" == "zenity" ]]; then if [[ "${IS_TARBALL}" == "n" ]]; then echo "${UNARCHIVE_PROGRESS}" fi echo "# ${DIALOG_TEXT}" fi done | { if [[ "${_arg_ui_mode:-none}" == "zenity" ]]; then if [[ "${IS_TARBALL}" == "n" ]]; then zenity --progress --percentage=0 --text="Extracting ${ITEM_NAME}..." --no-cancel --time-remaining --auto-close 2>/dev/null else zenity --progress --pulsate --text="Extracting ${ITEM_NAME}..." --no-cancel --auto-close 2>/dev/null fi else cat - >/dev/null fi } local UNARCHIVE_STATUS="${PIPESTATUS[0]}" if [[ "${UNARCHIVE_STATUS}" -ne 0 ]]; then if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then busctl --user call "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "close" 2>/dev/null || true fi term::step::failed return "${UNARCHIVE_STATUS}" fi if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then busctl --user call "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "close" 2>/dev/null || true fi term::step::complete } local SEVENZ_VERSION_MAJOR="0" local SEVENZ_VERSION_MINOR="0" local SEVENZ_REQUIRE_SNLD="no" __unarchiver::read_7z_version() { if [[ "${SEVENZ_VERSION_MAJOR}" -gt 0 ]]; then return 0 fi local SEZENV_VERSION_REGEX='^7-Zip( \[[0-9a-zA-Z_-]\]+)? ([0-9]+)(\.([0-9]+)| |:)' local SEVENZ_OUTPUT_LINE while IFS= read -r SEVENZ_OUTPUT_LINE; do if [[ "${SEVENZ_VERSION_MAJOR}" -gt 0 ]]; then continue fi if [[ "${SEVENZ_OUTPUT_LINE}" =~ ${SEZENV_VERSION_REGEX} ]]; then SEVENZ_VERSION_MAJOR="${BASH_REMATCH[2]}" SEVENZ_VERSION_MINOR="${BASH_REMATCH[4]:-0}" if [[ "${SEVENZ_VERSION_MAJOR}" -gt 25 ]]; then SEVENZ_REQUIRE_SNLD="yes" elif [[ "${SEVENZ_VERSION_MAJOR}" -eq 25 ]] && [[ "${SEVENZ_VERSION_MINOR}" -ge 1 ]]; then SEVENZ_REQUIRE_SNLD="yes" fi fi done < <("${UNARCHIVER_BIN}" 2>&1) } __unarchiver::is_tarball() { local ARCHIVE_PATH="${1:-}" local ARCHIVE_FILENAME="${ARCHIVE_PATH##*/}" case "${ARCHIVE_FILENAME}" in *.tar.gz | *.tgz) return 0 ;; *.tar.bz2 | *.tbz) return 0 ;; *.tar.xz | *.txz) return 0 ;; *.tar.zst | *.tar.zstd) return 0 ;; esac return 1 } __unarchiver::unarchive_file_with_progress() { local ARCHIVE_PATH="${1:-}" local TARGET_PATH="${2:-}" local IGNORE_PATTERNS_VAR_NAME="${3:-}" local IGNORE_PATTERNS_RECURSIVE_VAR_NAME="${4:-}" if [[ -z "${ARCHIVE_PATH}" ]] || [[ -z "${TARGET_PATH}" ]]; then return 1 fi local IS_TARBALL="n" if __unarchiver::is_tarball "${ARCHIVE_PATH}"; then IS_TARBALL="y" fi local UNARCHIVE_PROGRESS if [[ "${IS_TARBALL}" == "y" ]]; then local TAR_REGEX='^tar:\s+r:\s+[0-9]+\s+\((.+)\)$' __unarchiver::unarchive_file::call_tar \ "${ARCHIVE_PATH}" "${TARGET_PATH}" \ "${IGNORE_PATTERNS_VAR_NAME}" "${IGNORE_PATTERNS_RECURSIVE_VAR_NAME}" | while IFS= read -r UNARCHIVE_PROGRESS; do if [[ "${UNARCHIVE_PROGRESS}" =~ ${TAR_REGEX} ]]; then echo "${BASH_REMATCH[1]}" else echo "${UNARCHIVE_PROGRESS}" 1>&2 fi done return "${PIPESTATUS[0]}" else local SEVENZ_REGEX='^\s*([0-9]+)%' __unarchiver::unarchive_file::call_7z \ "${ARCHIVE_PATH}" "${TARGET_PATH}" \ "${IGNORE_PATTERNS_VAR_NAME}" "${IGNORE_PATTERNS_RECURSIVE_VAR_NAME}" | stdbuf -oL -- tr $'\b\r' $'\n\n' | while IFS= read -r UNARCHIVE_PROGRESS; do if [[ "${UNARCHIVE_PROGRESS}" =~ ${SEVENZ_REGEX} ]]; then echo "${BASH_REMATCH[1]}" fi done return "${PIPESTATUS[0]}" fi } __unarchiver::unarchive_file::call_7z() { local ARCHIVE_PATH="${1:-}" local TARGET_PATH="${2:-}" if [[ -z "${ARCHIVE_PATH}" ]] || [[ -z "${TARGET_PATH}" ]]; then return 1 fi local IGNORE_PATTERNS=() local IGNORE_PATTERNS_VAR_NAME="${3:-}" if [[ -n "${IGNORE_PATTERNS_VAR_NAME}" ]]; then local IGNORE_PATTERN_VAR_REF="${IGNORE_PATTERNS_VAR_NAME}[@]" IGNORE_PATTERNS=("${!IGNORE_PATTERN_VAR_REF}") fi local IGNORE_PATTERNS_RECURSIVE=() local IGNORE_PATTERNS_RECURSIVE_VAR_NAME="${4:-}" if [[ -n "${IGNORE_PATTERNS_RECURSIVE_VAR_NAME}" ]]; then local IGNORE_PATTERN_RECURSIVE_VAR_REF="${IGNORE_PATTERNS_RECURSIVE_VAR_NAME}[@]" IGNORE_PATTERNS_RECURSIVE=("${!IGNORE_PATTERN_RECURSIVE_VAR_REF}") fi __unarchiver::read_7z_version local SEZENZ_ARGS=() SEZENZ_ARGS=("x" "${ARCHIVE_PATH}" "-y" "-bsp1" "-bso0" "-bse2" "-aoa" "-o${TARGET_PATH}") if [[ "${SEVENZ_REQUIRE_SNLD}" == "yes" ]]; then SEZENZ_ARGS+=("-snld") else SEZENZ_ARGS+=("-snl") fi local IGNORE for IGNORE in "${IGNORE_PATTERNS[@]}"; do SEZENZ_ARGS+=("-x!${IGNORE}") done for IGNORE in "${IGNORE_PATTERNS_RECURSIVE[@]}"; do SEZENZ_ARGS+=("-xr!${IGNORE}") done "${UNARCHIVER_BIN}" "${SEZENZ_ARGS[@]}" } __unarchiver::unarchive_file::call_tar() { local ARCHIVE_PATH="${1:-}" local TARGET_PATH="${2:-}" if [[ -z "${ARCHIVE_PATH}" ]] || [[ -z "${TARGET_PATH}" ]]; then return 1 fi local IGNORE_PATTERNS=() local IGNORE_PATTERNS_VAR_NAME="${3:-}" if [[ -n "${IGNORE_PATTERNS_VAR_NAME}" ]]; then local IGNORE_PATTERN_VAR_REF="${IGNORE_PATTERNS_VAR_NAME}[@]" IGNORE_PATTERNS=("${!IGNORE_PATTERN_VAR_REF}") fi local IGNORE_PATTERNS_RECURSIVE=() local IGNORE_PATTERNS_RECURSIVE_VAR_NAME="${4:-}" if [[ -n "${IGNORE_PATTERNS_RECURSIVE_VAR_NAME}" ]]; then local IGNORE_PATTERN_RECURSIVE_VAR_REF="${IGNORE_PATTERNS_RECURSIVE_VAR_NAME}[@]" IGNORE_PATTERNS_RECURSIVE=("${!IGNORE_PATTERN_RECURSIVE_VAR_REF}") fi local TAR_ARGS=() TAR_ARGS=("xf" "${ARCHIVE_PATH}" "--checkpoint=10" '--checkpoint-action=echo=%{r}T' "--overwrite" "-C" "${TARGET_PATH}") local IGNORE for IGNORE in "${IGNORE_PATTERNS[@]}"; do TAR_ARGS+=("--exclude=${IGNORE}") done for IGNORE in "${IGNORE_PATTERNS_RECURSIVE[@]}"; do TAR_ARGS+=("--exclude=${IGNORE}") done tar "${TAR_ARGS[@]}" 2>&1 } # Patch Metadata Download Step local PATCH_METADATA_URL="https://api.github.com/repos/OldUnreal/UnrealTournamentPatches/releases/latest" # UT:GOTY patches are separated per architecture step::read_patch_meta_from_github::metadata_filter() { local PATCH_OS_NAME="${1:-}" local PATCH_TARGET_ARCHITECTURE="${2:-}" if [[ "${PATCH_OS_NAME}" == "windows" ]]; then echo "-windows-x86.zip" else echo "-${PATCH_OS_NAME}-${PATCH_TARGET_ARCHITECTURE}" fi } # Download Steps # shellcheck disable=SC2034 # Used dynamically below local TITLE_PRIMARY_DOWNLOAD_SOURCES=( "https://files.oldunreal.net/UT_GOTY_CD1.ISO|649633792|e184984ca88f001c5ddd52035d76cd64e266e26c74975161b5ed72366c74704f" "https://files2.oldunreal.net/UT_GOTY_CD1.ISO|649633792|e184984ca88f001c5ddd52035d76cd64e266e26c74975161b5ed72366c74704f" "https://files3.oldunreal.net/UT_GOTY_CD1.ISO|649633792|e184984ca88f001c5ddd52035d76cd64e266e26c74975161b5ed72366c74704f" ) # shellcheck disable=SC2034 # Used dynamically below local TITLE_BACKUP_DOWNLOAD_SOURCES=( "https://archive.org/download/ut-goty/UT_GOTY_CD1.iso|649633792|e184984ca88f001c5ddd52035d76cd64e266e26c74975161b5ed72366c74704f" ) # shellcheck disable=SC2034 # Used dynamically below local BP4_DOWNLOAD_SOURCES=( "https://files.oldunreal.net/utbonuspack4-zip.7z|11268844|5b7a1080724a122a596c226c50d4dc7c2d7636ceaf067e9c12112014a170ffba" "https://files2.oldunreal.net/utbonuspack4-zip.7z|11268844|5b7a1080724a122a596c226c50d4dc7c2d7636ceaf067e9c12112014a170ffba" "https://files3.oldunreal.net/utbonuspack4-zip.7z|11268844|5b7a1080724a122a596c226c50d4dc7c2d7636ceaf067e9c12112014a170ffba" ) # Build Download sources declare -A DOWNLOADS_SOURCE_LIST=( [game]="$(downloader::build_download_source_definition "TITLE_PRIMARY_DOWNLOAD_SOURCES" "TITLE_BACKUP_DOWNLOAD_SOURCES")" [bp4]="$(downloader::build_download_source_definition "BP4_DOWNLOAD_SOURCES")" ) declare -A DOWNLOADS_FILENAME_LIST=( [game]="UT_GOTY_CD1.iso" [bp4]="utbonuspack4-zip.7z" ) # Game Data Unpacking # shellcheck disable=SC2034 # Used dynamically local UNPACK_IGNORE_PATTERNS=( # Default ini files 'System/UnrealTournament.ini' 'System/User.ini' # Windows specific binaries 'System/*.bat' 'System/*.dll' 'System/*.exe' # Old Setup Files 'Autorun.inf' 'Setup.exe' 'DirectX7' 'GameSpy' 'Microsoft' 'NetGamesUSA.com' 'System400' # Translation Files 'System/*.ctt' 'System/*.det' 'System/*.elt' 'System/*.est' 'System/*.frt' 'System/*.int' 'System/*.itt' 'System/*.nlt' 'System/*.ptt' 'System/*.rut' ) # shellcheck shell=bash step::welcome_banner() { ansi::banner "OldUnreal ${PRODUCT_NAME} Linux Installer" echo if [[ "${_arg_ui_mode:-none}" != "none" ]]; then echo -e "$(ansi::styled "Installer is running in GUI mode. If no window is displayed," "${ansi_stylenum[dim]}")" echo -e "$(ansi::styled "type " "${ansi_stylenum[dim]}")CTRL+C$(ansi::styled " to kill the installer, and start it with the" "${ansi_stylenum[dim]}")" echo -e "--ui-mode=none$(ansi::styled " argument." "${ansi_stylenum[dim]}")" echo fi } step::welcome_banner # shellcheck shell=bash step::check_dependencies() { term::step::new "Checking Dependencies" if [[ "${ARCHITECTURE_SUFFIX}" == 'NOT_SUPPORTED' ]]; then term::step::failed_with_error "CPU Architecture ${DETECTED_ARCHITECTURE} is not currently supported." return 1 fi local MISSING_DEPS=() local MISSING_DEPS_RHEL=() local MISSING_DEPS_DEB=() local MISSING_DEPS_ARCH=() local MISSING_DEPS_OPENSUSE=() local MISSING_DEPS_BREW=() local UI_MODE_DEPS_MET="yes" # Check UI Mode Dependencies if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then term::step::progress "kdialog" if ! command -v "kdialog" &>/dev/null; then MISSING_DEPS+=("kdialog") MISSING_DEPS_RHEL+=("kdialog") MISSING_DEPS_DEB+=("kdialog") MISSING_DEPS_ARCH+=("kdialog") MISSING_DEPS_OPENSUSE+=("kdialog") UI_MODE_DEPS_MET="no" fi term::step::progress "busctl" if ! command -v "busctl" &>/dev/null; then MISSING_DEPS+=("systemd") MISSING_DEPS_RHEL+=("systemd") MISSING_DEPS_DEB+=("systemd") MISSING_DEPS_ARCH+=("systemd") MISSING_DEPS_OPENSUSE+=("systemd") UI_MODE_DEPS_MET="no" fi elif [[ "${_arg_ui_mode:-none}" == "zenity" ]]; then term::step::progress "zenity" if ! command -v "zenity" &>/dev/null; then MISSING_DEPS+=("zenity") MISSING_DEPS_RHEL+=("zenity") MISSING_DEPS_DEB+=("zenity") MISSING_DEPS_ARCH+=("zenity") MISSING_DEPS_OPENSUSE+=("zenity") MISSING_DEPS_BREW+=("zenity") UI_MODE_DEPS_MET="no" fi fi # Check Downloaders term::step::progress "curl" if ! command -v "curl" &>/dev/null && ! command -v "wget" &>/dev/null && ! command -v "wget2" &>/dev/null; then MISSING_DEPS+=("curl (or wget)") MISSING_DEPS_RHEL+=("curl") MISSING_DEPS_DEB+=("curl") MISSING_DEPS_ARCH+=("curl") MISSING_DEPS_OPENSUSE+=("curl") MISSING_DEPS_BREW+=("curl") fi # Check Archivers term::step::progress "tar" if ! command -v "tar" &>/dev/null; then MISSING_DEPS+=("tar") MISSING_DEPS_RHEL+=("tar") MISSING_DEPS_DEB+=("tar") MISSING_DEPS_ARCH+=("tar") MISSING_DEPS_OPENSUSE+=("tar") fi term::step::progress "7zip" if ! command -v "7z" &>/dev/null && ! command -v "7zz" &>/dev/null; then MISSING_DEPS+=("7zip, 7zip [Debian > bookworm], p7zip-full [Debian <= bookworm], or 7zip-standalone-all [Fedora/RHEL]") MISSING_DEPS_RHEL+=("7zip-standalone-all") MISSING_DEPS_DEB+=("p7zip-full") MISSING_DEPS_ARCH+=("7zip") MISSING_DEPS_OPENSUSE+=("7zip") MISSING_DEPS_BREW+=("7zip") fi # Check jq term::step::progress "jq" if ! command -v "jq" &>/dev/null; then MISSING_DEPS+=("jq") MISSING_DEPS_RHEL+=("jq") MISSING_DEPS_DEB+=("jq") MISSING_DEPS_ARCH+=("jq") MISSING_DEPS_OPENSUSE+=("jq") MISSING_DEPS_BREW+=("jq") fi if [[ "${PRODUCT_SHORTNAME}" == "UT2004" ]]; then # Check unshield term::step::progress "unshield" if ! command -v "unshield" &>/dev/null; then # Can it be downloaded? case "${ARCHITECTURE_SUFFIX}" in amd64 | arm64) # shellcheck disable=SC2034 # May not be used in all installers DOWNLOADS_SOURCE_LIST[unshield]="https://raw.githubusercontent.com/OldUnreal/FullGameInstallers/master/Linux/deps/unshield-${ARCHITECTURE_SUFFIX}||" DOWNLOADS_FILENAME_LIST[unshield]="unshield" ;; *) MISSING_DEPS+=("unshield") MISSING_DEPS_RHEL+=("unshield") MISSING_DEPS_DEB+=("unshield") MISSING_DEPS_ARCH+=("unshield") MISSING_DEPS_OPENSUSE+=("unshield") MISSING_DEPS_BREW+=("unshield") ;; esac fi fi if [[ "${UI_MODE_DEPS_MET}" == "no" ]]; then _arg_ui_mode="none" fi if [[ "${#MISSING_DEPS[@]}" -gt 0 ]]; then local DISTRO_DERIVATIVE="" local DISTRO_PKG_INSTALL_CMD="" if command -v "pacman" &>/dev/null; then DISTRO_DERIVATIVE="Arch" DISTRO_PKG_INSTALL_CMD="sudo pacman -S ${MISSING_DEPS_ARCH[*]}" elif command -v "dnf" &>/dev/null; then DISTRO_DERIVATIVE="Fedora/RHEL" DISTRO_PKG_INSTALL_CMD="sudo dnf install ${MISSING_DEPS_RHEL[*]}" elif command -v "apt" &>/dev/null; then DISTRO_DERIVATIVE="Debian" DISTRO_PKG_INSTALL_CMD="sudo apt install ${MISSING_DEPS_DEB[*]}" elif command -v "zypper" &>/dev/null; then DISTRO_DERIVATIVE="OpenSUSE" DISTRO_PKG_INSTALL_CMD="sudo zypper install ${MISSING_DEPS_OPENSUSE[*]}" fi local ERROR_TEXT="Missing required dependencies.\n\nYour system is missing dependencies that are required by this installer.\nPlease install the following required packages:" local PKG for PKG in "${MISSING_DEPS[@]}"; do ERROR_TEXT="${ERROR_TEXT}\n - ${PKG}" done if [[ -n "${DISTRO_DERIVATIVE}" ]]; then ERROR_TEXT="${ERROR_TEXT}\n\nOn ${DISTRO_DERIVATIVE} or derivatives, you should be able to install" ERROR_TEXT="${ERROR_TEXT}\nthe required package(s) using the following command:\n" ERROR_TEXT="${ERROR_TEXT}\n ${DISTRO_PKG_INSTALL_CMD}" fi if command -v "brew" &>/dev/null && [[ "${#MISSING_DEPS_BREW[@]}" -gt 0 ]]; then local BREW_INSTALL_CMD="brew install" for PKG in "${MISSING_DEPS_BREW[@]}"; do BREW_INSTALL_CMD="${BREW_INSTALL_CMD} ${PKG}" done ERROR_TEXT="${ERROR_TEXT}\n\nYou appear to have brew installed. Some of the required packages are available\n" ERROR_TEXT="${ERROR_TEXT}\non brew. You can install them using the following command:\n" ERROR_TEXT="${ERROR_TEXT}\n ${BREW_INSTALL_CMD}" fi term::step::failed_with_error "${ERROR_TEXT}" if [[ "${UI_MODE_DEPS_MET}" == "no" ]]; then echo 1>&2 echo "You do not have the required dependencies for the selected UI mode." 1>&2 echo -e "Please relaunch using the $(ansi::styled "--ui-mode=none" "${ansi_stylenum[bright]}") argument," 1>&2 echo "or install the required dependencies." 1>&2 fi return 1 else term::step::complete fi } step::check_dependencies # shellcheck shell=bash local DESTINATION_HOMIFIED="" # shellcheck disable=SC2034 # Used in some installers local IS_DESTINATION_CASE_SENSITIVE_FS="" step::check_destination() { term::step::new "Checking Destination Folder" if [[ -z "${_arg_destination:-}" ]]; then term::step::failed_with_error "No destination folder set. Aborting installation." return 78 fi if [[ -d "${_arg_destination}" ]] && [[ ! -w "${_arg_destination}" ]]; then term::step::failed_with_error "Destination folder not writable by user. Aborting installation." return 77 #E_PERM elif [[ ! -d "${_arg_destination}" ]]; then # Create folder if it doesn't exist mkdir -p "${_arg_destination}" &>/dev/null || { term::step::failed_with_error "User does not have permission to create destination folder. Aborting installation." return 77 #E_PERM } fi _arg_destination="$(realpath "${_arg_destination}")" if [[ ! -d "${_arg_destination%/}/Installer" ]]; then # Create the installation folder mkdir -p "${_arg_destination%/}/Installer" &>/dev/null || { term::step::failed_with_error "User does not have permission to create destination folder. Aborting installation." return 77 #E_PERM } fi if [[ ! -w "${_arg_destination%/}/Installer" ]]; then term::step::failed_with_error "The ./Installer subfolder of the destination cannot be written by this user." return 77 #E_PERM fi # Check Case-Sensitivity { if [[ -f "${_arg_destination%/}/Installer/.check_casesensitive" ]]; then rm -f "${_arg_destination%/}/Installer/.check_casesensitive" fi if [[ ! -f "${_arg_destination%/}/Installer/.Check_caseSensitive" ]]; then touch "${_arg_destination%/}/Installer/.Check_caseSensitive" fi if [[ -f "${_arg_destination%/}/Installer/.check_casesensitive" ]]; then # shellcheck disable=SC2034 # Used in some installers IS_DESTINATION_CASE_SENSITIVE_FS="no" else # shellcheck disable=SC2034 # Used in some installers IS_DESTINATION_CASE_SENSITIVE_FS="yes" fi rm -f "${_arg_destination%/}/Installer/.Check_caseSensitive" } || { term::step::failed_with_error "Unable to determine case sensitivity. Aborting installation." return 77 #E_PERM } DESTINATION_HOMIFIED="${_arg_destination}" { [[ "${DESTINATION_HOMIFIED}" =~ ^"${HOME}"(/|$) ]] && DESTINATION_HOMIFIED="~${_arg_destination#"${HOME}"}"; } || true term::step::complete } step::check_destination # shellcheck shell=bash local EPIC_TOS_URL="https://legal.epicgames.com/en-US/epicgames/tos" step::eula() { if [[ "${_arg_ui_mode:-none}" == "none" ]]; then __step::eula::text return 0 fi term::step::new "Terms of Service" local DIALOG_TEXT="The Epic Games Terms of Service apply to the use and distribution of this game, and they supersede any other end user agreements that may accompany the game. You may read the Terms of Service at this URL: ${EPIC_TOS_URL}" local DIALOG_ARGS=() if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then DIALOG_ARGS=( kdialog --title "Terms of Service" --yesno "${DIALOG_TEXT}" ) elif [[ "${_arg_ui_mode:-none}" == "zenity" ]]; then DIALOG_ARGS=( zenity --text-info "Terms of Service" "--checkbox=I agree to Epic Games Terms of Service" --width=450 --height=400 ) fi if ! echo "${DIALOG_TEXT}" | "${DIALOG_ARGS[@]}" &>/dev/null; then term::step::failed_with_error "Installation Aborted." 1>&2 return 1 fi term::step::complete } __step::eula::text() { echo echo -e "The $(ansi::styled "Epic Games Terms of Service" "${ansi_stylenum[bright]}") apply to the use and distribution of this game," echo "and they supersede any other end user agreements that may accompany the game." echo echo "You may read the Terms of Service at this URL:" echo " $(ansi::styled "${EPIC_TOS_URL}" "${ansi_stylenum[underline]}")" echo if ! term::yesno "Do you agree to the Terms of Service?"; then echo term::step::new "Terms of Service" term::step::failed_with_error "Installation Aborted." 1>&2 return 1 fi echo term::step::new "Terms of Service" term::step::complete } step::eula # shellcheck shell=bash local PATCH_METADATA_JSON step::read_patch_meta_from_github() { local PATCH_FOR_OS="${1:-linux}" local PATCH_FOR_REASON="${2:-}" local STEP_NAME="Fetch Patch Info from GitHub" if [[ -n "${PATCH_FOR_REASON}" ]]; then STEP_NAME="${STEP_NAME} (${PATCH_FOR_REASON})" fi term::step::new "${STEP_NAME}" if [[ -z "${PATCH_METADATA_URL:-}" ]]; then term::step::failed_with_error "Implementation error, PATCH_METADATA_URL not set." return 1 fi if [[ -z "${PATCH_METADATA_JSON:-}" ]]; then term::step::progress "Downloading Metadata" { PATCH_METADATA_JSON=$(downloader::fetch_json "${PATCH_METADATA_URL}"); } || { term::step::failed_with_error "Failed to read patch metadata from GitHub. Installation aborted." return 1 } fi if ! type "step::read_patch_meta_from_github::metadata_filter" &>/dev/null; then step::read_patch_meta_from_github::metadata_filter() { local PATCH_OS_NAME="${1:-}" # shellcheck disable=SC2034 # present by convention local PATCH_TARGET_ARCHITECTURE="${2:-}" if [[ "${PATCH_OS_NAME}" == "windows" ]]; then echo "-${PATCH_OS_NAME}(-.+)?.zip" return 0 fi echo "-${PATCH_OS_NAME}" } fi local IS_GITHUB_RELEASES_ARRAY="no" { IS_GITHUB_RELEASES_ARRAY=$(echo "${PATCH_METADATA_JSON}" | jq -r 'if (. | type) == "array" then "yes" else "no" end'); } || { term::step::failed_with_error "Couldn't determine if we received a single release, or an array of releases. Installation aborted." return 1 } local JQ_FILTER=".assets[]" if [[ "${IS_GITHUB_RELEASES_ARRAY}" == "yes" ]]; then JQ_FILTER=".[0].assets[]" fi local METADATA_FILTER METADATA_FILTER=$(step::read_patch_meta_from_github::metadata_filter "${PATCH_FOR_OS}" "${ARCHITECTURE_SUFFIX}") local JQ_FILTER { JQ_FILTER+=' | select(.browser_download_url | ascii_downcase | test("'"${METADATA_FILTER}"'"))'; } || { term::step::failed_with_error "Implementation error, step::read_patch_meta_from_github::metadata_filter runtime error." return 1 } local PATCH_DOWNLOAD_URL { PATCH_DOWNLOAD_URL=$(echo "${PATCH_METADATA_JSON}" | jq -r "[ ${JQ_FILTER} ] | .[0].browser_download_url"); } || { term::step::failed_with_error "Implementation error, step::read_patch_meta_from_github::metadata_filter runtime error." return 1 } if [[ -z "${PATCH_DOWNLOAD_URL:-}" ]]; then term::step::failed_with_error "Couldn't determine which patch to download. Installation aborted." return 1 fi PATCH_FILENAME="${PATCH_DOWNLOAD_URL##*/}" PATCH_FILENAME="${PATCH_FILENAME%%\?*}" local PATCH_DOWNLOAD_SIZE { PATCH_DOWNLOAD_SIZE=$(echo "${PATCH_METADATA_JSON}" | jq -r "[ ${JQ_FILTER} ] | .[0].size"); } || { term::step::failed_with_error "Couldn't determine patch size. Installation aborted." return 1 } DOWNLOADS_SOURCE_LIST[patch_${PATCH_FOR_OS}]="${PATCH_DOWNLOAD_URL}|${PATCH_DOWNLOAD_SIZE}|" DOWNLOADS_FILENAME_LIST[patch_${PATCH_FOR_OS}]="${PATCH_FILENAME}" term::step::complete } step::read_patch_meta_from_github if [[ "${_arg_unrealed}" == "on" ]]; then step::read_patch_meta_from_github "windows" "UnrealEd" fi # shellcheck shell=bash step::download_files() { local DOWNLOAD_SOURCE_KEY for DOWNLOAD_SOURCE_KEY in "${!DOWNLOADS_SOURCE_LIST[@]}"; do local DOWNLOAD_URL_SETS="${DOWNLOADS_SOURCE_LIST[${DOWNLOAD_SOURCE_KEY}]}" local DOWNLOAD_FILENAME="${DOWNLOADS_FILENAME_LIST[${DOWNLOAD_SOURCE_KEY}]}" local IS_DOWNLOAD_SKIPPED="no" DOWNLOAD_PATH="${_arg_destination%/}/Installer/${DOWNLOAD_FILENAME}" local TARGET_STEP_NAME="Download ${DOWNLOAD_FILENAME}" # If the file exist and isn't a partial download if [[ -f "${DOWNLOAD_PATH}" ]] && [[ ! -f "${DOWNLOAD_PATH}.aria2" ]]; then term::step::new "${TARGET_STEP_NAME}" local EXISTING_FILESIZE EXISTING_HASH EXISTING_FILESIZE=$(stat --format=%s "${DOWNLOAD_PATH}") EXISTING_HASH=$(helper::progress::run_with_progress "Verifying existing file" downloader::compute_sha256sum "${DOWNLOAD_PATH}") local REMAINING_DOWNLOAD_URL_SETS="${DOWNLOAD_URL_SETS}" while true; do # We looped through all the possible values, we can break out if [[ -z "${REMAINING_DOWNLOAD_URL_SETS}" ]]; then break fi CURRENT_DOWNLOAD_URL_SET=$(helper::string::unshift::next_value "${REMAINING_DOWNLOAD_URL_SETS}" ";;") REMAINING_DOWNLOAD_URL_SETS=$(helper::string::unshift::remainder "${REMAINING_DOWNLOAD_URL_SETS}" ";;") local DOWNLOAD_URL DOWNLOAD_SIZE DOWNLOAD_HASH DOWNLOAD_URL=$(helper::string::unshift::next_value "${CURRENT_DOWNLOAD_URL_SET}") CURRENT_DOWNLOAD_URL_SET=$(helper::string::unshift::remainder "${CURRENT_DOWNLOAD_URL_SET}") DOWNLOAD_SIZE=$(helper::string::unshift::next_value "${CURRENT_DOWNLOAD_URL_SET}") CURRENT_DOWNLOAD_URL_SET=$(helper::string::unshift::remainder "${CURRENT_DOWNLOAD_URL_SET}") DOWNLOAD_HASH=$(helper::string::unshift::next_value "${CURRENT_DOWNLOAD_URL_SET}") if [[ -n "${DOWNLOAD_SIZE}" ]] && [[ "${EXISTING_FILESIZE}" -ne "${DOWNLOAD_SIZE}" ]]; then continue fi if [[ -n "${DOWNLOAD_HASH}" ]] && [[ "${EXISTING_HASH}" != "${DOWNLOAD_HASH}" ]]; then continue fi IS_DOWNLOAD_SKIPPED="yes" break done fi if [[ "${IS_DOWNLOAD_SKIPPED}" == "yes" ]]; then term::step::skipped "SKIPPED: File already exists" continue fi downloader::download_file "${DOWNLOAD_URL_SETS}" "${DOWNLOAD_PATH}" done } step::download_files # shellcheck shell=bash step::unarchive_generic() { local ITEM_NAME="${1:-}" local ARCHIVE_PATH="${2:-}" local TARGET_PATH="${3:-}" local IGNORE_PATTERNS_VAR_NAME="${4:-}" local IGNORE_PATTERNS_RECURSIVE_VAR_NAME="${5:-}" if [[ -z "${ITEM_NAME}" ]] || [[ -z "${ARCHIVE_PATH}" ]] || [[ -z "${TARGET_PATH}" ]]; then return 1 fi if [[ ! -d "${TARGET_PATH}" ]]; then # Create the installation folder mkdir -p "${TARGET_PATH}" &>/dev/null || { term::step::failed_with_error "User does not have permission to create staging folder. Aborting installation." return 77 #E_PERM } fi unarchiver::unarchive_file "${ITEM_NAME}" "${ARCHIVE_PATH}" "${TARGET_PATH}" "${IGNORE_PATTERNS_VAR_NAME}" "${IGNORE_PATTERNS_RECURSIVE_VAR_NAME}" } step::unarchive_generic "Game Files" "${_arg_destination%/}/Installer/${DOWNLOADS_FILENAME_LIST[game]}" "${_arg_destination%/}" "UNPACK_IGNORE_PATTERNS" step::unarchive_generic "Bonus Pack 4" "${_arg_destination%/}/Installer/${DOWNLOADS_FILENAME_LIST[bp4]}" "${_arg_destination%/}" if [[ "${_arg_unrealed}" == "on" ]]; then step::unarchive_generic "Latest Patch (UnrealEd)" "${_arg_destination%/}/Installer/${DOWNLOADS_FILENAME_LIST[patch_windows]}" "${_arg_destination%/}" fi step::unarchive_generic "Latest Patch" "${_arg_destination%/}/Installer/${DOWNLOADS_FILENAME_LIST[patch_linux]}" "${_arg_destination%/}" # shellcheck shell=bash step::unpack_uz_maps() { term::step::new "Unpack Maps" local KDIALOG_DBUS_ADDRESS=() if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then read -ra KDIALOG_DBUS_ADDRESS < <(kdialog --title "${CURRENT_STEP_NAME}" --progressbar "Unpacking maps..." 0 2>/dev/null) busctl --user call "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "showCancelButton" b "false" 2>/dev/null || true fi helper::progress::make_consistant __step::unpack_uz_maps::run | while IFS= read -r UNPACK_PROGRESS; do if [[ -z "${UNPACK_PROGRESS}" ]]; then term::step::progress "" >&6 if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then busctl --user set-property "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "maximum" i 0 2>/dev/null || true fi continue fi local TOTAL_FILES PERCENT CURRENT_FILE_INDEX MAP_NAME TOTAL_FILES=$(helper::string::unshift::next_value "${UNPACK_PROGRESS}") UNPACK_PROGRESS=$(helper::string::unshift::remainder "${UNPACK_PROGRESS}") PERCENT=$(helper::string::unshift::next_value "${UNPACK_PROGRESS}") UNPACK_PROGRESS=$(helper::string::unshift::remainder "${UNPACK_PROGRESS}") CURRENT_FILE_INDEX=$(helper::string::unshift::next_value "${UNPACK_PROGRESS}") UNPACK_PROGRESS=$(helper::string::unshift::remainder "${UNPACK_PROGRESS}") MAP_NAME=$(helper::string::unshift::next_value "${UNPACK_PROGRESS}") UNPACK_PROGRESS=$(helper::string::unshift::remainder "${UNPACK_PROGRESS}") term::step::progress "${CURRENT_FILE_INDEX} of ${TOTAL_FILES} - ${MAP_NAME}" >&6 local DIALOG_TEXT="Unpacking ${MAP_NAME} (${CURRENT_FILE_INDEX} of ${TOTAL_FILES})" if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then local ESCAPED_DIALOG_TEXT ESCAPED_DIALOG_TEXT="$(echo -e "${DIALOG_TEXT}")" busctl --user set-property "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "maximum" i "${TOTAL_FILES}" 2>/dev/null || true busctl --user set-property "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "value" i "${CURRENT_FILE_INDEX}" 2>/dev/null || true busctl --user call "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "setLabelText" s "${ESCAPED_DIALOG_TEXT}" 2>/dev/null || true elif [[ "${_arg_ui_mode:-none}" == "zenity" ]]; then echo "${PERCENT}" echo "# ${DIALOG_TEXT}" fi done | { if [[ "${_arg_ui_mode:-none}" == "zenity" ]]; then zenity --progress --percentage=0 --text="Unpacking maps..." --no-cancel --time-remaining --auto-close 2>/dev/null else cat - >/dev/null fi } || { if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then busctl --user call "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "close" 2>/dev/null || true fi term::step::failed_with_error "Failed to unpack all maps. Installation aborted." return 1 } if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then busctl --user call "${KDIALOG_DBUS_ADDRESS[@]}" "org.kde.kdialog.ProgressDialog" "close" 2>/dev/null || true fi term::step::complete } __step::unpack_uz_maps::run() { local UCC_BIN_PATH="${_arg_destination%/}/System${UE_SYSTEM_FOLDER_SUFFIX:-}/ucc-bin${ARCHITECTURE_BINARY_SUFFIX}" local MAP_FILES=() local MAP_FILE for MAP_FILE in "${_arg_destination%/}/Maps"/*.unr.uz; do if [[ ! -f "${MAP_FILE}" ]]; then continue fi local MAP_BASENAME="${MAP_FILE##*/}" MAP_FILES+=("${MAP_BASENAME}") done local TOTAL_MAP_FILES="${#MAP_FILES[@]}" local MAP_FILES_PROCESSED=0 for MAP_FILE in "${MAP_FILES[@]}"; do local MAP_BASENAME_UNCOMPRESSED="${MAP_FILE%.uz}" local COMPRESSED_FILE="${_arg_destination%/}/Maps/${MAP_FILE}" local DECOMPRESS_STAGING="${_arg_destination%/}/System${UE_SYSTEM_FOLDER_SUFFIX:-}/${MAP_BASENAME_UNCOMPRESSED}" local DECOMPRESS_TARGET="${_arg_destination%/}/Maps/${MAP_BASENAME_UNCOMPRESSED}" local PERCENT_PROGRESS="$(((MAP_FILES_PROCESSED * 100) / TOTAL_MAP_FILES))" MAP_FILES_PROCESSED=$((MAP_FILES_PROCESSED + 1)) echo "${TOTAL_MAP_FILES}|${PERCENT_PROGRESS}|${MAP_FILES_PROCESSED}|${MAP_BASENAME_UNCOMPRESSED}" if [[ -f "${DECOMPRESS_TARGET}" ]]; then rm -f "${COMPRESSED_FILE}" else "${UCC_BIN_PATH}" decompress "../Maps/${MAP_FILE}" -nohomedir &>/dev/null || { return 1 } if [[ -f "${DECOMPRESS_STAGING}" ]]; then { mv -f "${DECOMPRESS_STAGING}" "${DECOMPRESS_TARGET}" && rm -f "${COMPRESSED_FILE}"; } || { return 1 } else return 1 fi fi done } step::unpack_uz_maps # shellcheck shell=bash step::ut99_special_fixes() { term::step::new "Apply UT:GOTY Specific Fixes" # DM-Cybrosis][ is both a DM and a DOM map if [[ -f "${_arg_destination%/}/Maps/DM-Cybrosis][.unr" ]] && [[ ! -f "${_arg_destination%/}/Maps/DOM-Cybrosis][.unr" ]]; then cp -f "${_arg_destination%/}/Maps/DM-Cybrosis][.unr" "${_arg_destination%/}/Maps/DOM-Cybrosis][.unr" || { term::step::failed_with_error "Failed to copy DM-Cybrosis][.unr to DOM-Cybrosis][.unr. Aborting installation." return 77 #E_PERM } fi term::step::complete } step::ut99_special_fixes # shellcheck shell=bash # This step is mostly provided in case a user installs on top of an existing install (from Steam for example) step::remove_extra_i18n_files() { local I18N_IDENTIFIERS=( "ctt" "det" "elt" "est" "frt" "kot" "int" "itt" "nlt" "ptt" "rut" "smt" "tmt" ) local SYS_LOCALIZED_PATH="${_arg_destination%/}/SystemLocalized" local SYS_PATH="${_arg_destination%/}/System" if [[ ! -d "${SYS_LOCALIZED_PATH}" ]]; then return 0 fi term::step::new "Remove extra localization files" { local I18N_IDENTIFIER for I18N_IDENTIFIER in "${I18N_IDENTIFIERS[@]}"; do local LOC_FILES=() local LOC_FILE if [[ -d "${SYS_LOCALIZED_PATH}/${I18N_IDENTIFIER}" ]]; then for LOC_FILE in "${SYS_LOCALIZED_PATH}/${I18N_IDENTIFIER}/"*".${I18N_IDENTIFIER}"; do if [[ ! -f "${LOC_FILE}" ]]; then continue fi LOC_FILES+=("${LOC_FILE##*/}") done fi for LOC_FILE in "${SYS_LOCALIZED_PATH}/"*".${I18N_IDENTIFIER}"; do if [[ ! -f "${LOC_FILE}" ]]; then continue fi LOC_FILES+=("${LOC_FILE##*/}") done for LOC_FILE in "${LOC_FILES[@]}"; do if [[ -f "${SYS_PATH}/${LOC_FILE}" ]]; then rm -f "${SYS_PATH}/${LOC_FILE}" fi done done term::step::complete } || { term::step::failed_with_error "Unable to remove extra localization files" return 1 } } step::remove_extra_i18n_files # shellcheck shell=bash step::create_unrealed_launcher() { term::step::new "Create UnrealEd Launch Script" local UNREALED_WINE_PREFIX_PATH="${_arg_destination%/}/.wine-unrealed" local UNREALED_LAUNCH_SCRIPT_PATH="${_arg_destination%/}/System${UEED_SYSTEM_FOLDER_SUFFIX:-}/UnrealEd-launch-linux.sh" mkdir -p "${UNREALED_WINE_PREFIX_PATH}" step::create_unrealed_launcher::write "${UNREALED_LAUNCH_SCRIPT_PATH}" || { term::step::failed_with_error "Failed to create UnrealEd launch script. Installation aborted." return 1 } chmod +x "${UNREALED_LAUNCH_SCRIPT_PATH}" || { term::step::failed_with_error "Failed to set UnrealEd launch script as executable. Installation aborted." return 1 } term::step::complete } step::create_unrealed_launcher::write() { local FILEPATH="${1:-}" { cat <<"EOFLAUNCHER" #!/usr/bin/env bash # # UnrealEd Linux Launcher # # shellcheck source-path=SCRIPTDIR # ARGBASH_SET_INDENT([ ]) # ARG_OPTIONAL_SINGLE([mode],[],[Informs the script on which Windows compatibility tool to use to launch UnrealEd],[auto]) # ARG_OPTIONAL_SINGLE([wine],[],[Path to wine (when using --mode=wine)],[]) # ARG_OPTIONAL_SINGLE([proton],[],[Path to proton compatibility tool (when using --mode=proton)],[]) # ARG_TYPE_GROUP_SET([mode],[MODE],[mode],[auto,umu-launcher,proton,wine]) # ARG_HELP([Launch UnrealEd]) # ARG_VERSION_AUTO([1.0],['OldUnreal ']) # DEFINE_SCRIPT_DIR([_SCRIPT_DIR]) # ARGBASH_GO() # needed because of Argbash --> m4_ignore([ ### START OF CODE GENERATED BY Argbash v2.11.0 one line above ### # Argbash is a bash code generator used to get arguments parsing right. # Argbash is FREE SOFTWARE, see https://argbash.dev for more info die() { local _ret="${2:-1}" test "${_PRINT_HELP:-no}" = yes && print_help >&2 echo "$1" >&2 exit "${_ret}" } # validators mode() { local _allowed=("auto" "umu-launcher" "proton" "wine") _seeking="$1" for element in "${_allowed[@]}"; do test "$element" = "$_seeking" && echo "$element" && return 0 done die "Value '$_seeking' (of argument '$2') doesn't match the list of allowed values: 'auto', 'umu-launcher', 'proton' and 'wine'" 4 } begins_with_short_option() { local first_option all_short_options='hv' first_option="${1:0:1}" test "$all_short_options" = "${all_short_options/$first_option/}" && return 1 || return 0 } # THE DEFAULTS INITIALIZATION - OPTIONALS _arg_mode="auto" _arg_wine= _arg_proton= print_help() { printf '%s\n' "Launch UnrealEd" printf 'Usage: %s [--mode ] [--wine ] [--proton ] [-h|--help] [-v|--version]\n' "$0" printf '\t%s\n' "--mode: Informs the script on which Windows compatibility tool to use to launch UnrealEd. Can be one of: 'auto', 'umu-launcher', 'proton' and 'wine' (default: 'auto')" printf '\t%s\n' "--wine: Path to wine (when using --mode=wine) (no default)" printf '\t%s\n' "--proton: Path to proton compatibility tool (when using --mode=proton) (no default)" printf '\t%s\n' "-h, --help: Prints help" printf '\t%s\n' "-v, --version: Prints version" } parse_commandline() { local _key while test $# -gt 0; do _key="$1" case "$_key" in --mode) test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 _arg_mode="$(mode "$2" "mode")" || exit 1 shift ;; --mode=*) _arg_mode="$(mode "${_key##--mode=}" "mode")" || exit 1 ;; --wine) test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 _arg_wine="$2" shift ;; --wine=*) _arg_wine="${_key##--wine=}" ;; --proton) test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 _arg_proton="$2" shift ;; --proton=*) _arg_proton="${_key##--proton=}" ;; -h | --help) print_help exit 0 ;; -h*) print_help exit 0 ;; -v | --version) printf '%s %s\n\n%s\n%s\n' "unrealed-launch-linux.sh" "1.0" 'Launch UnrealEd' 'OldUnreal ' exit 0 ;; -v*) printf '%s %s\n\n%s\n%s\n' "unrealed-launch-linux.sh" "1.0" 'Launch UnrealEd' 'OldUnreal ' exit 0 ;; *) _PRINT_HELP=yes die "FATAL ERROR: Got an unexpected argument '$1'" 1 ;; esac shift done } parse_commandline "$@" # OTHER STUFF GENERATED BY Argbash _SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || { echo "Couldn't determine the script's running directory, which probably matters, bailing out" >&2 exit 2 } # Validation of values ### END OF CODE GENERATED BY Argbash (sortof) ### ]) # [ <-- needed because of Argbash # Enable Bash Strict Mode set -euo pipefail unrealed::launcher() { declare -A ansi_colornum=( [black]=0 [red]=1 [green]=2 [yellow]=3 [blue]=4 [magenta]=5 [cyan]=6 [white]=7 ) declare -A ansi_stylenum=( [reset]=0 [bright]=1 [dim]=2 [italic]=3 [underline]=4 [flash]=5 [highlight]=7 [normal]=22 ) ansi::styled() { ALLOW_ESCAPES="off" if [[ "${1:-}" == "-e" ]]; then ALLOW_ESCAPES="on" shift fi local TEXT_TO_PRINT="${1:-}" local STYLE="${2:--1}" local FORE_COLOR="${3:--1}" local BACK_COLOR="${4:--1}" declare -a CODES RESETCODES if [[ "${STYLE}" -eq 22 ]] || [[ "${STYLE}" -eq -1 ]]; then RESETCODES=("$(printf "\033[%sm" "22")" "${RESETCODES[@]}") else CODES=("${CODES[@]}" "$(printf "\033[%sm" "${STYLE}")") fi if [[ "${BACK_COLOR}" -eq -1 ]]; then RESETCODES=("$(printf "\033[%sm" "49")" "${RESETCODES[@]}") else CODES=("${CODES[@]}" "$(printf "\033[%sm" "$((BACK_COLOR + 40))")") fi if [[ "${FORE_COLOR}" -eq -1 ]]; then RESETCODES=("$(printf "\033[%sm" "39")" "${RESETCODES[@]}") else CODES=("${CODES[@]}" "$(printf "\033[%sm" "$((FORE_COLOR + 30))")") fi local rc for rc in "${RESETCODES[@]}"; do echo -en "$rc" done local c for c in "${CODES[@]}"; do echo -en "$c" done if [[ "${ALLOW_ESCAPES}" == "on" ]]; then echo -en "${TEXT_TO_PRINT}" else echo -n "${TEXT_TO_PRINT}" fi echo -en "\033[m" } local OU_INSTALL_PATH OU_INSTALL_PATH="$(cd "$(dirname "${_SCRIPT_DIR}")" && pwd)" || { echo "Couldn't determine game installation directory." >&2 exit 2 } local PREFERRED_WINEPREFIX="${WINEPREFIX:-${OU_INSTALL_PATH}/.wine-unrealed}" unrealed::warning() { echo -e "$(ansi::styled "Warning:" "${ansi_stylenum[bright]}" "${ansi_colornum[yellow]}") $*" 1>&2 } unrealed::error() { echo -e "$(ansi::styled "Error:" "${ansi_stylenum[bright]}" "${ansi_colornum[red]}") $*" 1>&2 } local DEFAULT_PROTON="${HOME}/.steam/steam/steamapps/common/Proton - Experimental/proton" if [[ "${_arg_mode}" == "auto" ]]; then if [[ -n "${_arg_proton:-}" ]]; then _arg_mode="proton" elif [[ -n "${_arg_wine:-}" ]]; then _arg_mode="wine" fi if command -v umu-run &>/dev/null; then _arg_mode="umu-launcher" elif [[ -x "${DEFAULT_PROTON}" ]]; then _arg_mode="proton" _arg_proton="${DEFAULT_PROTON}" elif command -v wine &>/dev/null; then _arg_mode="wine" _arg_wine="wine" fi fi if [[ "${_arg_mode}" == "umu-launcher" ]]; then if ! command -v umu-run &>/dev/null; then unrealed::error "Unable to find umu-run in PATH." return 1 fi local PREFERRED_PROTON="${_arg_proton:-${PROTONPATH:-}}" if [[ -z "${PREFERRED_PROTON}" ]]; then if [[ -x "/usr/share/steam/compatibilitytools.d/proton-ge-custom/proton" ]]; then PREFERRED_PROTON="/usr/share/steam/compatibilitytools.d/proton-ge-custom" else PREFERRED_PROTON="GE-Proton" fi fi cd "${OU_INSTALL_PATH}" WINEPREFIX="${PREFERRED_WINEPREFIX}" \ GAMEID=umu-default \ STORE=none \ PROTONPATH="${PREFERRED_PROTON}" \ exec umu-run "${_SCRIPT_DIR}/UnrealEd.exe" elif [[ "${_arg_mode}" == "proton" ]]; then if ! command -v "${_arg_proton}" &>/dev/null; then unrealed::error "--proton option is invalid." return 1 fi cd "${OU_INSTALL_PATH}" STEAM_COMPAT_CLIENT_INSTALL_PATH="${STEAM_COMPAT_CLIENT_INSTALL_PATH:-~/.steam/steam}" \ STEAM_COMPAT_DATA_PATH="${PREFERRED_WINEPREFIX}" \ exec "${_arg_proton}" run "${_SCRIPT_DIR}/UnrealEd.exe" elif [[ "${_arg_mode}" == "wine" ]]; then if ! command -v "${_arg_wine}" &>/dev/null; then unrealed::error "--wine option is invalid." return 1 fi unrealed::warning "Wine is untested, OldUnreal recommends running UnrealEd via Proton or UMU" cd "${OU_INSTALL_PATH}" WINEPREFIX="${PREFERRED_WINEPREFIX}" \ exec "${_arg_wine}" "${_SCRIPT_DIR}/UnrealEd.exe" fi } unrealed::launcher # ] <-- needed because of Argbash EOFLAUNCHER } >"${FILEPATH}" } if [[ "${_arg_unrealed}" == "on" ]]; then step::create_unrealed_launcher fi # shellcheck shell=bash step::xdg_desktop_entry() { local STEP_NAME="${1:-}" local STEP_PROMPT="${2:-}" local DESKTOP_ENTRY_PATH="${3:-}" local DESKTOP_ENTRY_PATH_UNREALED="${4:-}" local APPLICATION_ENTRY_HANDLING_MODE="${5:-}" local SKIP_IF_ENTRY_FOLDER_DOESNT_EXIST="${6:-no}" if [[ -z "${STEP_NAME}" ]] || [[ -z "${STEP_PROMPT}" ]] || [[ -z "${DESKTOP_ENTRY_PATH}" ]] || [[ -z "${DESKTOP_ENTRY_PATH_UNREALED}" ]] || [[ -z "${APPLICATION_ENTRY_HANDLING_MODE}" ]]; then return 1 fi if [[ "${APPLICATION_ENTRY_HANDLING_MODE:-skip}" == "skip" ]]; then term::step::new "${STEP_NAME}" term::step::skipped "SKIPPED: User opted out" return 0 fi if [[ "${SKIP_IF_ENTRY_FOLDER_DOESNT_EXIST}" == "yes" ]]; then local DESKTOP_ENTRY_FOLDER="${DESKTOP_ENTRY_PATH%/*}" if [[ ! -d "${DESKTOP_ENTRY_FOLDER}" ]]; then term::step::new "${STEP_NAME}" term::step::skipped "SKIPPED: Not Available" return 0 fi fi # Prompt before showing step name in text mode if [[ "${_arg_ui_mode:-none}" == "none" ]] && [[ "${APPLICATION_ENTRY_HANDLING_MODE}" == "prompt" ]]; then echo if ! term::yesno "${STEP_PROMPT}"; then echo term::step::new "${STEP_NAME}" term::step::skipped "SKIPPED: User opted out" return 0 fi echo fi term::step::new "${STEP_NAME}" if [[ "${_arg_ui_mode:-none}" != "none" ]] && [[ "${APPLICATION_ENTRY_HANDLING_MODE}" == "prompt" ]]; then local DIALOG_ARGS=() if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then DIALOG_ARGS=( kdialog --yesno "${STEP_PROMPT}" ) elif [[ "${_arg_ui_mode:-none}" == "zenity" ]]; then DIALOG_ARGS=( zenity --question "--text=${STEP_PROMPT}" ) fi if ! "${DIALOG_ARGS[@]}" &>/dev/null; then term::step::skipped "SKIPPED: User opted out" return 0 fi fi __step::xdg_desktop_entry::create "${DESKTOP_ENTRY_PATH}" || { term::step::failed_with_error "User does not have permission to create .desktop file." return 77 #E_PERM } if [[ "${_arg_unrealed}" == "on" ]]; then __step::xdg_desktop_entry::unrealed::create "${DESKTOP_ENTRY_PATH_UNREALED}" || { term::step::failed_with_error "User does not have permission to create .desktop file." return 77 #E_PERM } fi term::step::complete } step::xdg_desktop_entry::xdg_dir() { local STEP_NAME="${1:-}" local STEP_PROMPT="${2:-}" local XDG_DIR_NAME="${3:-}" local DESKTOP_ENTRY_PATH="${4:-}" local DESKTOP_ENTRY_PATH_UNREALED="${5:-}" local APPLICATION_ENTRY_HANDLING_MODE="${6:-}" if [[ -z "${STEP_NAME}" ]] || [[ -z "${STEP_PROMPT}" ]] || [[ -z "${XDG_DIR_NAME}" ]] || [[ -z "${DESKTOP_ENTRY_PATH}" ]] || [[ -z "${DESKTOP_ENTRY_PATH_UNREALED}" ]] || [[ -z "${APPLICATION_ENTRY_HANDLING_MODE}" ]]; then return 1 fi local XDG_DIR_PATH XDG_DIR_PATH=$(xdgdirs::get_user_dir "${XDG_DIR_NAME}") if [[ -z "${XDG_DIR_PATH}" ]]; then term::step::new "${STEP_NAME}" term::step::skipped "SKIPPED: Not Available" return 0 fi step::xdg_desktop_entry \ "${STEP_NAME}" \ "${STEP_PROMPT}" \ "${XDG_DIR_PATH}/${DESKTOP_ENTRY_PATH}" \ "${XDG_DIR_PATH}/${DESKTOP_ENTRY_PATH_UNREALED}" \ "${APPLICATION_ENTRY_HANDLING_MODE}" \ "yes" } __step::xdg_desktop_entry::create() { local DESKTOP_ENTRY_PATH="${1:-}" if [[ -z "${DESKTOP_ENTRY_PATH}" ]]; then return 1 fi local DESKTOP_ENTRY_FOLDER="${DESKTOP_ENTRY_PATH%/*}" local SOURCE_ICON="${_arg_destination%/}/${PRODUCT_ICONPATH:-Help/Unreal.ico}" local ICON_NAME ICON_NAME=$(__step::xdg_desktop_entry::get_icon_name "${SOURCE_ICON}" "OldUnreal_${PRODUCT_SHORTNAME}" || echo "${SOURCE_ICON}") # Create destination folder if it doesn't exist if [[ ! -d "${DESKTOP_ENTRY_FOLDER}" ]]; then mkdir -p "${DESKTOP_ENTRY_FOLDER}" 2>/dev/null fi { echo "[Desktop Entry]" echo "Name=${PRODUCT_NAME}" if [[ -n "${PRODUCT_KEYWORDS[*]:-}" ]]; then echo -n "Keywords=" local KEYWORD for KEYWORD in "${PRODUCT_KEYWORDS[@]}"; do echo -n "${KEYWORD};" done echo fi if [[ -n "${PRODUCT_URLSCHEME:-}" ]]; then echo "Exec="'"'"${_arg_destination%/}/System${UE_SYSTEM_FOLDER_SUFFIX:-}/${MAIN_BINARY_NAME}${ARCHITECTURE_BINARY_SUFFIX}"'"' %u echo "MimeType=x-scheme-handler/${PRODUCT_URLSCHEME};" else echo "Exec="'"'"${_arg_destination%/}/System${UE_SYSTEM_FOLDER_SUFFIX:-}/${MAIN_BINARY_NAME}${ARCHITECTURE_BINARY_SUFFIX}"'"' fi echo "Icon=${ICON_NAME}" echo "Terminal=false" echo "Type=Application" echo "Categories=Game;" } >"${DESKTOP_ENTRY_PATH}" } __step::xdg_desktop_entry::unrealed::create() { local DESKTOP_ENTRY_PATH="${1:-}" if [[ -z "${DESKTOP_ENTRY_PATH}" ]]; then return 1 fi local DESKTOP_ENTRY_FOLDER="${DESKTOP_ENTRY_PATH%/*}" local SOURCE_ICON="${_arg_destination%/}/${PRODUCT_ICONPATH:-Help/UnrealEd.ico}" local ICON_NAME ICON_NAME=$(__step::xdg_desktop_entry::get_icon_name "${SOURCE_ICON}" "OldUnreal_${PRODUCT_SHORTNAME}_UnrealEd" || echo "${SOURCE_ICON}") # Create destination folder if it doesn't exist if [[ ! -d "${DESKTOP_ENTRY_FOLDER}" ]]; then mkdir -p "${DESKTOP_ENTRY_FOLDER}" 2>/dev/null fi { echo "[Desktop Entry]" echo "Name=UnrealEd for ${PRODUCT_NAME}" echo "Exec="'"'"${_arg_destination%/}/System${UEED_SYSTEM_FOLDER_SUFFIX:-}/UnrealEd-launch-linux.sh"'"' echo "Icon=${ICON_NAME}" echo "Terminal=false" echo "Type=Application" echo "Categories=Game;" } >"${DESKTOP_ENTRY_PATH}" } # Automatically create png files from .ico for DEs that don't support Windows icons declare -A STEP_XDG_DESKTOP_ENTRY_ICONS=() __step::xdg_desktop_entry::get_icon_name() { local ICON_PATH="${1:-}" local TARGET_ICON_NAME="${2:-}" if [[ -n "${STEP_XDG_DESKTOP_ENTRY_ICONS[${ICON_PATH}]:-}" ]]; then echo "${STEP_XDG_DESKTOP_ENTRY_ICONS[${ICON_PATH}]}" return 0 fi if [[ ! -f "${ICON_PATH}" ]]; then return 1 fi if ! command -v magick &>/dev/null; then STEP_XDG_DESKTOP_ENTRY_ICONS[${ICON_PATH}]="${ICON_PATH}" echo "${ICON_PATH}" return 0 fi local XDG_UNREAL_ICONS_PATH="${XDG_DATA_HOME:-${HOME}/.local/share}/icons/hicolor" local IS_48PX_FOUND="no" local IS_32PX_FOUND="no" local INDIVIDUAL_ICON_INFO local REGEX_ICON_INFO='^([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)$' local ICON_INFORMATION ICON_INFORMATION=$(magick identify -format "%p %h %z %k\n" "${ICON_PATH}" | sort -k3nr -k2nr -k4nr) while IFS= read -r INDIVIDUAL_ICON_INFO; do if [[ "${INDIVIDUAL_ICON_INFO}" =~ ${REGEX_ICON_INFO} ]]; then local ICON_INDEX="${BASH_REMATCH[1]}" local ICON_SIZE="${BASH_REMATCH[2]}" case "${ICON_SIZE}" in 48) if [[ "${IS_48PX_FOUND}" == "yes" ]]; then continue fi IS_48PX_FOUND="yes" mkdir -p "${XDG_UNREAL_ICONS_PATH}/48x48/apps" magick -quiet "${ICON_PATH}[${ICON_INDEX}]" "${XDG_UNREAL_ICONS_PATH}/48x48/apps/${TARGET_ICON_NAME}.png" >/dev/null STEP_XDG_DESKTOP_ENTRY_ICONS[${ICON_PATH}]="${TARGET_ICON_NAME}" ;; 32) if [[ "${IS_32PX_FOUND}" == "yes" ]]; then continue fi IS_32PX_FOUND="yes" mkdir -p "${XDG_UNREAL_ICONS_PATH}/32x32/apps" magick -quiet "${ICON_PATH}[${ICON_INDEX}]" "${XDG_UNREAL_ICONS_PATH}/32x32/apps/${TARGET_ICON_NAME}.png" >/dev/null STEP_XDG_DESKTOP_ENTRY_ICONS[${ICON_PATH}]="${TARGET_ICON_NAME}" ;; esac fi done <<<"${ICON_INFORMATION}" if [[ -n "${STEP_XDG_DESKTOP_ENTRY_ICONS[${ICON_PATH}]:-}" ]]; then echo "${STEP_XDG_DESKTOP_ENTRY_ICONS[${ICON_PATH}]}" else STEP_XDG_DESKTOP_ENTRY_ICONS[${ICON_PATH}]="${ICON_PATH}" echo "${ICON_PATH}" fi } step::xdg_desktop_entry \ "Application Menu Entry" \ "Do you want to create an application menu entry?" \ "${XDG_DATA_HOME:-${HOME}/.local/share}/applications/OldUnreal-${PRODUCT_SHORTNAME}.desktop" \ "${XDG_DATA_HOME:-${HOME}/.local/share}/applications/OldUnreal-${PRODUCT_SHORTNAME}-UnrealEd.desktop" \ "${_arg_application_entry}" step::xdg_desktop_entry::xdg_dir \ "Desktop Shortcut" \ "Do you want to create a shortcut on your desktop?" \ "DESKTOP" \ "${PRODUCT_SHORTNAME}.desktop" \ "UnrealEd-${PRODUCT_SHORTNAME}.desktop" \ "${_arg_desktop_shortcut}" # shellcheck shell=bash step::notify_install_finished() { # Remove installation files if [[ "${_arg_keep_installer_files:-off}" != "on" ]] && [[ -d "${_arg_destination%/}/Installer" ]]; then rm -rf "${_arg_destination%/}/Installer" fi echo echo -e "$(ansi::styled "${PRODUCT_NAME}" "${ansi_stylenum[bright]}" "${ansi_colornum[green]}") $(ansi::styled "is now installed!" "" "${ansi_colornum[green]}")" echo echo "You can launch the game by running:" echo " ${DESTINATION_HOMIFIED}/System${UE_SYSTEM_FOLDER_SUFFIX:-}/${MAIN_BINARY_NAME}${ARCHITECTURE_BINARY_SUFFIX}" local DIALOG_TEXT=""" ${PRODUCT_NAME} is now installed! You can launch the game by running: ${DESTINATION_HOMIFIED}/System${UE_SYSTEM_FOLDER_SUFFIX:-}/${MAIN_BINARY_NAME}${ARCHITECTURE_BINARY_SUFFIX} """ if [[ "${_arg_ui_mode:-none}" == "none" ]]; then return 0 elif [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then DIALOG_ARGS=( kdialog --msgbox "${DIALOG_TEXT}" ) elif [[ "${_arg_ui_mode:-none}" == "zenity" ]]; then DIALOG_ARGS=( zenity --info --width=400 "--text=${DIALOG_TEXT}" ) fi "${DIALOG_ARGS[@]}" &>/dev/null || true } step::notify_install_finished } installer::entrypoint # ] <-- needed because of Argbash