#!/usr/bin/env bash # # Unreal Tournament 2004 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/UT2004]) # 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 2004]) # 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/UT2004" _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 2004" 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/UT2004')" 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-ut2004.sh" "1.2.1" 'Install Unreal Tournament 2004' 'OldUnreal ' exit 0 ;; -v*) printf '%s %s\n\n%s\n%s\n' "install-ut2004.sh" "1.2.1" 'Install Unreal Tournament 2004' '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 2004" local PRODUCT_SHORTNAME="UT2004" local PRODUCT_KEYWORDS=("UT2004" "UT2K4") local PRODUCT_URLSCHEME="ut2004" local MAIN_BINARY_NAME="UT2004" # 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 PATCH_METADATA_URL="https://api.github.com/repos/OldUnreal/UT2004Patches/releases" # Download Steps # shellcheck disable=SC2034 # Used dynamically below local TITLE_PRIMARY_DOWNLOAD_SOURCES=( "https://files.oldunreal.net/UT2004.ISO|2995322880|43e9182ae20bcbc0f6f4588fee6c1b336c261f1465403118a1973b09b1a22541" "https://files2.oldunreal.net/UT2004.ISO|2995322880|43e9182ae20bcbc0f6f4588fee6c1b336c261f1465403118a1973b09b1a22541" "https://files3.oldunreal.net/UT2004.ISO|2995322880|43e9182ae20bcbc0f6f4588fee6c1b336c261f1465403118a1973b09b1a22541" ) # shellcheck disable=SC2034 # Used dynamically below local TITLE_BACKUP_DOWNLOAD_SOURCES=( "https://archive.org/download/ut-2004/UT2004.ISO|3751510016|7ae95242aa23d5e31b353811e1e920a4377fa53cf728b8edcb17006b7f3c4e97" ) # Build Download sources declare -A DOWNLOADS_SOURCE_LIST=( [game]="$(downloader::build_download_source_definition "TITLE_PRIMARY_DOWNLOAD_SOURCES" "TITLE_BACKUP_DOWNLOAD_SOURCES")" ) declare -A DOWNLOADS_FILENAME_LIST=( [game]="UT2004.iso" ) # Game Data Unpacking # shellcheck disable=SC2034 # Used dynamically local UNPACK_IGNORE_PATTERNS=( # Old Setup Files 'AutoRunData' 'Disk1/layout.bin' 'Disk1/Setup.*' 'Disk1/setup.*' 'SoNow' '*.*' ) # 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="" 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 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%/}/Installer/.staging" "UNPACK_IGNORE_PATTERNS" # shellcheck shell=bash step::ut2004_unpack_cabs() { term::step::new "Unpack Install CABs" local STAGING_PATH="${_arg_destination%/}/Installer/.staging" local CABS_PATH="${STAGING_PATH}/Cabs" local TARGET_PATH="${STAGING_PATH}/Data" if [[ -d "${CABS_PATH}" ]]; then rm -rf "${CABS_PATH}" &>/dev/null || { term::step::failed_with_error "User does not have permission to create staging folder. Aborting installation." return 77 #E_PERM } fi # Create the cabs folder mkdir -p "${CABS_PATH}" &>/dev/null || { term::step::failed_with_error "User does not have permission to create staging folder. Aborting installation." return 77 #E_PERM } { local DISK_FOLDER for DISK_FOLDER in "${STAGING_PATH}"/Disk*; do if [[ ! -d "${DISK_FOLDER}" ]]; then continue fi local DISK_CAB for DISK_CAB in "${DISK_FOLDER}"/*.cab; do if [[ ! -f "${DISK_CAB}" ]]; then continue fi local DISK_CAB_BASE="${DISK_CAB##*/}" ln -fs "${DISK_CAB}" "${CABS_PATH}/${DISK_CAB_BASE}" done for DISK_CAB in "${DISK_FOLDER}"/*.hdr; do if [[ ! -f "${DISK_CAB}" ]]; then continue fi local DISK_CAB_BASE="${DISK_CAB##*/}" ln -fs "${DISK_CAB}" "${CABS_PATH}/${DISK_CAB_BASE}" done done } || { term::step::failed_with_error "Failed to prepare cabs for unpacking. Aborting installation." return 1 } 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 local KDIALOG_DBUS_ADDRESS=() if [[ "${_arg_ui_mode:-none}" == "kdialog" ]]; then read -ra KDIALOG_DBUS_ADDRESS < <(kdialog --title "${CURRENT_STEP_NAME}" --progressbar "Unpacking install CABs..." 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::ut2004_unpack_cabs::run "${CABS_PATH}/data1.cab" "${TARGET_PATH}" | 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 FILE_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}") FILE_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} - ${FILE_NAME}" >&6 local DIALOG_TEXT="Unpacking ${FILE_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 CABs. 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 rm -rf "${CABS_PATH}" &>/dev/null || { term::step::failed_with_error "User does not have permission to remove temporary staging folder for CABs. Aborting installation." return 77 #E_PERM } term::step::complete } __step::ut2004_unpack_cabs::run() { local CAB_FILE="${1:-}" local WRITE_TO_PATH="${2:-}" local UNSHIELD_BIN="unshield" if [[ "${DOWNLOADS_FILENAME_LIST[unshield]:-}" == "unshield" ]]; then UNSHIELD_BIN="${_arg_destination%/}/Installer/unshield" if [[ ! -f "${UNSHIELD_BIN}" ]]; then term::error "unshield binary not present" return 1 fi chmod +x "${UNSHIELD_BIN}" fi local RAW_TOTAL_FILES_TO_EXTRACT TOTAL_FILES_TO_EXTRACT RAW_TOTAL_FILES_TO_EXTRACT=$("${UNSHIELD_BIN}" l "${CAB_FILE}" | tail -n1) local LINE_MATCH_REGEX='^\s*([0-9]+)' if [[ "${RAW_TOTAL_FILES_TO_EXTRACT}" =~ ${LINE_MATCH_REGEX} ]]; then TOTAL_FILES_TO_EXTRACT="${BASH_REMATCH[1]}" fi local CURRENT_FILE_INDEX=0 local EXTRACTING_MATCH_REGEX='^\s*extracting:\s+(.+)$' "${UNSHIELD_BIN}" -d "${WRITE_TO_PATH}" x "${CAB_FILE}" | while IFS= read -r UNPACK_PROGRESS; do if [[ "${UNPACK_PROGRESS}" =~ ${EXTRACTING_MATCH_REGEX} ]]; then local CURRENT_FILE="${BASH_REMATCH[1]##*/}" local PERCENT_PROGRESS="$(((CURRENT_FILE_INDEX * 100) / TOTAL_FILES_TO_EXTRACT))" CURRENT_FILE_INDEX=$((CURRENT_FILE_INDEX + 1)) echo "${TOTAL_FILES_TO_EXTRACT}|${PERCENT_PROGRESS}|${CURRENT_FILE_INDEX}|${CURRENT_FILE}" fi done } step::ut2004_unpack_cabs # shellcheck shell=bash step::ut2004_install_files() { term::step::new "Install Files" local STAGING_DATA_FOLDER="${_arg_destination%/}/Installer/.staging/Data" local FOLDERS_AND_TARGETS=( 'All_Animations|Animations' 'All_Benchmark|Benchmark' 'All_ForceFeedback|ForceFeedback' 'All_Help|Help' 'All_KarmaData|KarmaData' 'All_Maps|Maps' 'All_Music|Music' 'All_StaticMeshes|StaticMeshes' 'All_Textures|Textures' 'All_Web|Web' 'All_UT2004.EXE|System' 'English_Manual|Manual' 'English_Sounds_Speech_System_Help|' ) # Backup file doesn't have US_License file group if [[ -d "${STAGING_DATA_FOLDER}/US_License.int" ]]; then FOLDERS_AND_TARGETS+=('US_License.int|System') fi # Let's first remove any files which are not required due to us targeting Linux rm -f "${STAGING_DATA_FOLDER}/All_UT2004.EXE/"*.exe 2>/dev/null || true rm -f "${STAGING_DATA_FOLDER}/English_Sounds_Speech_System_Help/System/"*.bat 2>/dev/null || true rm -f "${STAGING_DATA_FOLDER}/English_Sounds_Speech_System_Help/System/"*.dll 2>/dev/null || true rm -f "${STAGING_DATA_FOLDER}/English_Sounds_Speech_System_Help/System/"*.exe 2>/dev/null || true # Check if we are istalling on top of a Steam install of UT2004, and rename the lower-case Maps folder is that's the case if [[ -d "${_arg_destination%/}/maps" ]] && [[ ! -d "${_arg_destination%/}/Maps" ]]; then mv "${_arg_destination%/}/maps" "${_arg_destination%/}/Maps" fi local FOLDER_PAIR for FOLDER_PAIR in "${FOLDERS_AND_TARGETS[@]}"; do local FOLDER_SOURCE="${FOLDER_PAIR%|*}" local FOLDER_TARGET="${FOLDER_PAIR#*|}" local RESOLVED_TARGET="${_arg_destination}" term::step::progress "${FOLDER_SOURCE}" if [[ -n "${FOLDER_TARGET}" ]]; then mkdir -p "${_arg_destination%/}/${FOLDER_TARGET}" 2>/dev/null || { term::step::failed_with_error "User does not have permission to create the target folder (${FOLDER_TARGET}). Aborting installation." return 77 #E_PERM } RESOLVED_TARGET="${_arg_destination%/}/${FOLDER_TARGET}" fi cp -rf "${STAGING_DATA_FOLDER}/${FOLDER_SOURCE}/"* "${RESOLVED_TARGET}/" || { term::step::failed_with_error "Failed to copy files to target folder (${FOLDER_TARGET}). Aborting installation." return 77 #E_PERM } done term::step::progress "Removing extra files" rm -rf "${_arg_destination%/}/Installer/.staging" || { term::step::failed_with_error "Failed to remove staging files. Aborting installation." return 77 #E_PERM } term::step::complete } step::ut2004_install_files 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::ut2004_special_fixes() { term::step::new "Apply UT2004 Specific Fixes" local SYSTEM_FOLDER="${_arg_destination%/}/System${UE_SYSTEM_FOLDER_SUFFIX}" # Remove files with different casing than the patch payload COMMON_WRONG_CASINGS=( "Bonuspack.u" "Gui2K4.u" "Gameplay.u" "Ipdrv.u" "Skaarjpack.u" "StreamLineFX.u" "UT2K4Assault.u" "UT2K4AssaultFull.u" "XVoting.u" "xWebAdmin.u" ) for WRONG_CASING in "${COMMON_WRONG_CASINGS[@]}"; do if [[ -f "${SYSTEM_FOLDER}/${WRONG_CASING}" ]]; then rm -f "${SYSTEM_FOLDER}/${WRONG_CASING}" fi if [[ -f "${_arg_destination%/}/System/${WRONG_CASING}" ]]; then rm -f "${_arg_destination%/}/System/${WRONG_CASING}" fi done # Remove provided libopenal if provided by the system if [[ -f "${SYSTEM_FOLDER}/libopenal.so.1" ]] && [[ -n "$(step::ut2004_special_fixes::find_library libopenal.so.1)" ]]; then rm -f "${SYSTEM_FOLDER}/libopenal.so.1" "${SYSTEM_FOLDER}/libopenal.so.1."* fi # Remove provided libSDL3 if provided by the system if [[ -f "${SYSTEM_FOLDER}/libSDL3.so.0" ]] && [[ -n "$(step::ut2004_special_fixes::find_library libSDL3.so.0)" ]]; then rm -f "${SYSTEM_FOLDER}/libSDL3.so.0" "${SYSTEM_FOLDER}/libSDL3.so.0."* fi # libomp fixes local SYSTEM_PROVIDED_LIBOMP_PATH="" local LIBOMP_REQUIRES_SYMLINK="no" SYSTEM_PROVIDED_LIBOMP_PATH=$(step::ut2004_special_fixes::find_library "libomp.so.5") if [[ -z "${SYSTEM_PROVIDED_LIBOMP_PATH}" ]]; then SYSTEM_PROVIDED_LIBOMP_PATH=$(step::ut2004_special_fixes::find_library "libomp.so") if [[ -n "${SYSTEM_PROVIDED_LIBOMP_PATH}" ]]; then LIBOMP_REQUIRES_SYMLINK="yes" fi fi # Remove provided libomp.so.5 is one is provided by the system if [[ -f "${SYSTEM_FOLDER}/libomp.so.5" ]] && [[ -n "${SYSTEM_PROVIDED_LIBOMP_PATH}" ]]; then rm -f "${SYSTEM_FOLDER}/libomp.so.5" "${SYSTEM_FOLDER}/libomp.so.5."* fi if [[ "${LIBOMP_REQUIRES_SYMLINK}" == "yes" ]]; then ln -s "${SYSTEM_PROVIDED_LIBOMP_PATH}" "${SYSTEM_FOLDER}/libomp.so.5" fi step::ut2004_special_fixes::replace_line_in_file "${HOME}/.ut2004/System/UT2004.ini" "MainMenuClass=GUI2K4.UT2K4MainMenu" "MainMenuClass=GUI2K4.UT2K4MainMenuWS" step::ut2004_special_fixes::replace_line_in_file "${SYSTEM_FOLDER}/UT2004.ini" "MainMenuClass=GUI2K4.UT2K4MainMenu" "MainMenuClass=GUI2K4.UT2K4MainMenuWS" term::step::complete } # To avoid having a dependency on 'sed', this is being done manually step::ut2004_special_fixes::replace_line_in_file() { local FILENAME="${1:-}" local LINE_TO_REPLACE="${2:-}" local REPLACEMENT_LINE="${3:-}" if [[ -z "${FILENAME}" ]] || [[ -z "${LINE_TO_REPLACE}" ]] || [[ -z "${REPLACEMENT_LINE}" ]]; then term::step::failed_with_error "ASSERT FAILED. Missing required arg in step::ut2004_special_fixes::replace_line_in_file" return 1 fi if [[ ! -f "${FILENAME}" ]]; then return 0 fi local FILE_CONTENTS FILE_LINE FILE_CONTENTS="$(cat "${FILENAME}")" local FILE_NEW_CONTENTS="" local IS_CONTENT_FOUND="no" while IFS= read -r FILE_LINE; do if [[ "${FILE_LINE}" == "${LINE_TO_REPLACE}" ]]; then IS_CONTENT_FOUND="yes" FILE_NEW_CONTENTS="${FILE_NEW_CONTENTS}"$'\n'"${REPLACEMENT_LINE}" else FILE_NEW_CONTENTS="${FILE_NEW_CONTENTS}"$'\n'"${FILE_LINE}" fi done <<<"${FILE_CONTENTS}" if [[ "${IS_CONTENT_FOUND}" == "yes" ]]; then echo "${FILE_NEW_CONTENTS}" >"${FILENAME}" fi } step::ut2004_special_fixes::find_library() { local LIBRARY_NAME="${1:-}" if [[ -z "${LIBRARY_NAME}" ]]; then term::step::failed_with_error "ASSERT FAILED. Missing required arg in step::ut2004_special_fixes::find_library" return 1 fi if command -v "ldconfig" &>/dev/null; then local LIB_PATH LIB_PATH=$( (ldconfig -p | grep -F "${LIBRARY_NAME}" | head -n 1) || true) if [ -z "${LIB_PATH}" ]; then return 0 fi LIB_PATH="${LIB_PATH##*=>}" shopt -s extglob LIB_PATH="${LIB_PATH##+([[:space:]])}" shopt -u extglob if [ -f "${LIB_PATH}" ]; then echo "${LIB_PATH}" fi return 0 fi local SEARCH_PATHS=("/usr/lib/${DETECTED_ARCHITECTURE}-linux-gnu" "/usr/lib64" "/usr/local/lib" "/lib/${DETECTED_ARCHITECTURE}-linux-gnu") local CURRENT_PATH for CURRENT_PATH in "${SEARCH_PATHS[@]}"; do if [[ -f "${CURRENT_PATH}/${LIBRARY_NAME}" ]]; then echo "${LIBRARY_NAME}" break fi done } step::ut2004_special_fixes # 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