#!/bin/bash directory_browse() { # This function browses for a directory and returns the path chosen # USAGE: path_to_be_browsed_for=$(directory_browse $action_text) local path_selected=false while [ $path_selected == false ] do local target="$(rd_zenity --file-selection --title="Choose $1" --directory)" if [ ! -z "$target" ] #yes then rd_zenity --question --no-wrap --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --title "RetroDECK" --cancel-label="No" --ok-label "Yes" \ --text="Directory $target chosen, is this correct?" if [ $? == 0 ] then path_selected=true echo "$target" break fi else rd_zenity --question --no-wrap --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --title "RetroDECK" --cancel-label="No" --ok-label "Yes" \ --text="No directory selected. Do you want to exit the selection process?" if [ $? == 0 ] then break fi fi done } file_browse() { # This function browses for a file and returns the path chosen # USAGE: file_to_be_browsed_for=$(file_browse $action_text) local file_selected=false while [ $file_selected == false ] do local target="$(rd_zenity --file-selection --title="Choose $1")" if [ ! -z "$target" ] #yes then rd_zenity --question --no-wrap --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --title "RetroDECK" --cancel-label="No" --ok-label "Yes" \ --text="File $target chosen, is this correct?" if [ $? == 0 ] then file_selected=true echo "$target" break fi else rd_zenity --question --no-wrap --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --title "RetroDECK" --cancel-label="No" --ok-label "Yes" \ --text="No file selected. Do you want to exit the selection process?" if [ $? == 0 ] then break fi fi done } verify_space() { # Function used for verifying adequate space before moving directories around # USAGE: verify_space $source_dir $dest_dir # Function returns "true" if there is enough space, "false" if there is not source_size=$(du -sk "$1" | awk '{print $1}') source_size=$((source_size+(source_size/10))) # Add 10% to source size for safety dest_avail=$(df -k --output=avail "$2" | tail -1) if [[ $source_size -ge $dest_avail ]]; then echo "false" else echo "true" fi } move() { # Function to move a directory from one parent to another # USAGE: move $source_dir $dest_dir source_dir="$(echo "$1" | sed 's![^/]$!&/!')" # Add trailing slash if it is missing dest_dir="$(echo "$2" | sed 's![^/]$!&/!')" # Add trailing slash if it is missing log d "Moving \"$source_dir\" to \"$dest_dir\"" ( rsync -a --remove-source-files --ignore-existing --mkpath "$source_dir" "$dest_dir" # Copy files but don't overwrite conflicts find "$source_dir" -type d -empty -delete # Cleanup empty folders that were left behind ) | rd_zenity --icon-name=net.retrodeck.retrodeck --progress --no-cancel --pulsate --auto-close \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK Configurator Utility - Move in Progress" \ --text="Moving directory $(basename "$1") to new location of $2, please wait." if [[ -d "$source_dir" ]]; then # Some conflicting files remain rd_zenity --icon-name=net.retrodeck.retrodeck --error --no-wrap \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK Configurator Utility - Move Directories" \ --text="There were some conflicting files that were not moved.\n\nAll files that could be moved are in the new location,\nany files that already existed at the new location have not been moved and will need to be handled manually." fi } create_dir() { # A simple function that creates a directory checking if is still there while logging the activity # If -d it will delete it prior the creation if [[ "$1" == "-d" ]]; then # If "force" flag is provided, delete the directory first shift # Remove the first argument (-f) if [[ -e "$1" ]]; then rm -rf "$1" # Forcefully delete the directory log d "Found \"$1\", deleting it." fi fi if [[ ! -d "$1" ]]; then mkdir -p "$1" # Create directory if it doesn't exist log d "Created directory: $1" else log d "Directory \"$1\" already exists, skipping." fi } download_file() { # Function to download file from the Internet, with Zenity progress bar # USAGE: download_file $source_url $file_dest $file_name # source_url is the location the file is downloaded from # file_dest is the destination the file should be in the filesystem, needs filename included! # file_name is a user-readable file name or description to be put in the Zenity dialog ( wget "$1" -O "$2" -q ) | rd_zenity --progress \ --title="Downloading File" \ --text="Downloading $3..." \ --pulsate \ --auto-close } update_rd_conf() { # This function will import a default retrodeck.cfg file and update it with any current settings. This will allow us to expand the file over time while retaining current user settings. # USAGE: update_rd_conf # STAGE 1: For current files that haven't been broken into sections yet, where every setting name is unique conf_read # Read current settings into memory mv -f "$rd_conf" "$rd_conf_backup" # Backup config file before update cp "$rd_defaults" "$rd_conf" # Copy defaults file into place conf_write # Write old values into new retrodeck.cfg file # STAGE 2: To handle presets sections that use duplicate setting names generate_single_patch "$rd_defaults" "$rd_conf_backup" "$rd_update_patch" retrodeck # Create a patch file for differences between defaults and current user settings sed -i '/change^^version/d' "$rd_update_patch" # Remove version line from temporary patch file deploy_single_patch "$rd_defaults" "$rd_update_patch" "$rd_conf" # Re-apply user settings to defaults file set_setting_value "$rd_conf" "version" "$hard_version" retrodeck # Set version of currently running RetroDECK to updated retrodeck.cfg rm -f "$rd_update_patch" # Cleanup temporary patch file conf_read # Read all settings into memory # STAGE 3: Create new folders that were added to the shipped retrodeck.cfg, if any new_paths=$(awk -F= 'FNR==NR {keys[$1]=1; next} !($1 in keys)' \ <(grep -v '^\s*$' "$rd_conf_backup" | awk '/^\[paths\]/{f=1;next} /^\[/{f=0} f') \ <(grep -v '^\s*$' "$rd_conf" | awk '/^\[paths\]/{f=1;next} /^\[/{f=0} f')) if [[ -n "$new_paths" ]]; then log i "New paths found in RetroDECK config: \n $new_paths" if [[ ! "$rdhome" == "/home/deck/retrodeck" ]]; then # If rdhome is not the normal internal path, set new folders to rdhome as their root while read -r config_line; do local current_setting_name=$(get_setting_name "$config_line" "retrodeck") local current_setting_value=$(get_setting_value "$rd_conf" "$current_setting_name" "retrodeck" "paths") local relative_path="${current_setting_value#*retrodeck/}" local new_setting_value="$rdhome/$relative_path" set_setting_value "$rd_conf" "$current_setting_name" "$new_setting_value" "retrodeck" "paths" create_dir "$new_setting_value" done <<< "$new_paths" else # rdhome is default internal location, so imported new paths are valid while read -r config_line; do local current_setting_name=$(get_setting_name "$config_line" "retrodeck") local current_setting_value=$(get_setting_value "$rd_conf" "$current_setting_name" "retrodeck" "paths") create_dir "$current_setting_value" done <<< "$new_paths" fi conf_read fi # STAGE 4: Eliminate any preset incompatibility with existing user settings and new defaults # Fetch incompatible presets from JSON and create a lookup list incompatible_presets=$(jq -r ' .incompatible_presets | to_entries[] | [ "\(.key):\(.value)", "\(.value):\(.key)" ] | join("\n") ' "$features") while IFS= read -r current_setting_line # Read the existing retrodeck.cfg do if [[ (! -z "$current_setting_line") && (! "$current_setting_line" == "#"*) && (! "$current_setting_line" == "[]") ]]; then # If the line has a valid entry in it if [[ ! -z $(grep -o -P "^\[.+?\]$" <<< "$current_setting_line") ]]; then # If the line is a section header local current_section=$(sed 's^[][]^^g' <<< "$current_setting_line") # Remove brackets from section name else if [[ ! ("$current_section" == "" || "$current_section" == "paths" || "$current_section" == "options" || "$current_section" == "cheevos" || "$current_section" == "cheevos_hardcore") ]]; then local system_name=$(get_setting_name "$current_setting_line" "retrodeck") # Read the variable name from the current line local system_enabled=$(get_setting_value "$rd_conf" "$system_name" "retrodeck" "$current_section") # Read the variables value from active retrodeck.cfg local default_setting=$(get_setting_value "$rd_defaults" "$system_name" "retrodeck" "$current_section") # Read the variable value from the retrodeck defaults if [[ "$system_enabled" == "true" ]]; then while IFS=: read -r preset_being_checked known_incompatible_preset; do if [[ "$current_section" == "$preset_being_checked" ]]; then if [[ $(get_setting_value "$rd_conf" "$system_name" "retrodeck" "$known_incompatible_preset") == "true" ]]; then set_setting_value "$rd_conf" "$system_name" "false" "retrodeck" "$current_section" fi fi done <<< "$incompatible_presets" fi fi fi fi done < "$rd_conf" } conf_read() { # This function will read the RetroDECK config file into memory # USAGE: conf_read while IFS= read -r current_setting_line # Read the existing retrodeck.cfg do if [[ (! -z "$current_setting_line") && (! "$current_setting_line" == "#"*) && (! "$current_setting_line" == "[]") ]]; then # If the line has a valid entry in it if [[ ! -z $(grep -o -P "^\[.+?\]$" <<< "$current_setting_line") ]]; then # If the line is a section header local current_section=$(sed 's^[][]^^g' <<< "$current_setting_line") # Remove brackets from section name else if [[ "$current_section" == "" || "$current_section" == "paths" || "$current_section" == "options" ]]; then local current_setting_name=$(cut -d'=' -f1 <<< "$current_setting_line" | xargs) # Extract name local current_setting_value=$(cut -d'=' -f2 <<< "$current_setting_line" | xargs) # Extract value declare -g "$current_setting_name=$current_setting_value" # Write the current setting name and value to memory export "$current_setting_name" fi fi fi done < "$rd_conf" } conf_write() { # This function will update the RetroDECK config file with matching variables from memory # USAGE: conf_write while IFS= read -r current_setting_line # Read the existing retrodeck.cfg do if [[ (! -z "$current_setting_line") && (! "$current_setting_line" == "#"*) && (! "$current_setting_line" == "[]") ]]; then # If the line has a valid entry in it if [[ ! -z $(grep -o -P "^\[.+?\]$" <<< "$current_setting_line") ]]; then # If the line is a section header local current_section=$(sed 's^[][]^^g' <<< "$current_setting_line") # Remove brackets from section name else if [[ "$current_section" == "" || "$current_section" == "paths" || "$current_section" == "options" ]]; then local current_setting_name=$(get_setting_name "$current_setting_line" "retrodeck") # Read the variable name from the current line local current_setting_value=$(get_setting_value "$rd_conf" "$current_setting_name" "retrodeck" "$current_section") # Read the variables value from retrodeck.cfg local memory_setting_value=$(eval "echo \$${current_setting_name}") # Read the variable names' value from memory if [[ ! "$current_setting_value" == "$memory_setting_value" && ! -z "$memory_setting_value" ]]; then # If the values are different... set_setting_value "$rd_conf" "$current_setting_name" "$memory_setting_value" "retrodeck" "$current_section" # Update the value in retrodeck.cfg fi fi fi fi done < "$rd_conf" log d "retrodeck.cfg written" } dir_prep() { # This script is creating a symlink preserving old folder contents and moving them in the new one # Call me with: # dir prep "real dir" "symlink location" real="$(realpath -s "$1")" symlink="$(realpath -s "$2")" log d "Preparing directory $symlink in $real" # if the symlink dir is already a symlink, unlink it first, to prevent recursion if [ -L "$symlink" ]; then log d "$symlink is already a symlink, unlinking to prevent recursives" unlink "$symlink" fi # if the dest dir exists we want to backup it if [ -d "$symlink" ]; then log d "$symlink found" mv -f "$symlink" "$symlink.old" fi # if the real dir is already a symlink, unlink it first if [ -L "$real" ]; then log d "$real is already a symlink, unlinking to prevent recursives" #DEBUG unlink "$real" fi # if the real dir doesn't exist we create it if [ ! -d "$real" ]; then log d "$real not found, creating it" #DEBUG create_dir "$real" fi # creating the symlink log d "linking $real in $symlink" #DEBUG create_dir "$(dirname "$symlink")" # creating the full path except the last folder ln -svf "$real" "$symlink" # moving everything from the old folder to the new one, delete the old one if [ -d "$symlink.old" ]; then log d "Moving the data from $symlink.old to $real" #DEBUG mv -f "$symlink.old"/{.[!.],}* "$real" log d "Removing $symlink.old" #DEBUG rm -rf "$symlink.old" fi log i "$symlink is now $real" } rd_zenity() { # This function replaces the standard 'zenity' command and filters out annoying GTK errors on Steam Deck export CONFIGURATOR_GUI="zenity" # env GDK_SCALE=1.5 \ # GDK_DPI_SCALE=1.5 \ zenity 2> >(grep -v 'Gtk' >&2) "$@" local status=${PIPESTATUS[0]} # Capture the exit code of 'zenity' unset CONFIGURATOR_GUI return "$status" } update_rpcs3_firmware() { create_dir "$roms_folder/ps3/tmp" chmod 777 "$roms_folder/ps3/tmp" download_file "$rpcs3_firmware" "$roms_folder/ps3/tmp/PS3UPDAT.PUP" "RPCS3 Firmware" rpcs3 --installfw "$roms_folder/ps3/tmp/PS3UPDAT.PUP" rm -rf "$roms_folder/ps3/tmp" } update_vita3k_firmware() { download_file "http://dus01.psv.update.playstation.net/update/psv/image/2022_0209/rel_f2c7b12fe85496ec88a0391b514d6e3b/PSVUPDAT.PUP" "/tmp/PSVUPDAT.PUP" "Vita3K Firmware file: PSVUPDAT.PUP" download_file "http://dus01.psp2.update.playstation.net/update/psp2/image/2019_0924/sd_8b5f60b56c3da8365b973dba570c53a5/PSP2UPDAT.PUP?dest=us" "/tmp/PSP2UPDAT.PUP" "Vita3K Firmware file: PSP2UPDAT.PUP" Vita3K --firmware /tmp/PSVUPDAT.PUP Vita3K --firmware /tmp/PSP2UPDAT.PUP } backup_retrodeck_userdata() { # This function can compress one or more RetroDECK userdata folders into a single zip file for backup. # The function can do a "complete" backup of all userdata including ROMs and ES-DE media, so can end up being very large. # The function can also do a "core" backup of all the very important userdata files (like saves, states and gamelists) or a "custom" backup of only specified paths # The function can take both folder names as defined in retrodeck.cfg or full paths as arguments for folders to backup # It will also validate that all the provided paths exist and that there is enough free space to perform the backup before actually proceeding. # It will also rotate backups so that there are only 3 maximum of each type (complete, core or custom) # USAGE: backup_retrodeck_userdata complete # backup_retrodeck_userdata core # backup_retrodeck_userdata custom saves_folder states_folder /some/other/path create_dir "$backups_folder" backup_date=$(date +"%0m%0d_%H%M") backup_log_file="$logs_folder/${backup_date}_${backup_type}_backup_log.log" # Check if first argument is the type if [[ "$1" == "complete" || "$1" == "core" || "$1" == "custom" ]]; then backup_type="$1" shift # Remove the first argument else if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "No valid backup option chosen. Valid options are and ." fi log e "No valid backup option chosen. Valid options are , and ." return 1 fi zip_file="$backups_folder/retrodeck_${backup_date}_${backup_type}.zip" # Initialize paths arrays paths_to_backup=() declare -A config_paths # Requires an associative (dictionary) array to work # Build array of folder names and real paths from retrodeck.cfg while read -r config_line; do local current_setting_name=$(get_setting_name "$config_line" "retrodeck") if [[ ! $current_setting_name =~ (rdhome|sdcard|backups_folder) ]]; then # Ignore these locations local current_setting_value=$(get_setting_value "$rd_conf" "$current_setting_name" "retrodeck" "paths") config_paths["$current_setting_name"]="$current_setting_value" fi done < <(grep -v '^\s*$' "$rd_conf" | awk '/^\[paths\]/{f=1;next} /^\[/{f=0} f') # Determine which paths to backup if [[ "$backup_type" == "complete" ]]; then for folder_name in "${!config_paths[@]}"; do path_value="${config_paths[$folder_name]}" if [[ -e "$path_value" ]]; then paths_to_backup+=("$path_value") log i "Adding to backup: $folder_name = $path_value" else if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "The $folder_name was not found at its expected location, $path_value\nSomething may be wrong with your RetroDECK installation." fi log i "Warning: Path does not exist: $folder_name = $path_value" fi done # Add static paths not defined in retrodeck.cfg if [[ -e "$rdhome/ES-DE/collections" ]]; then paths_to_backup+=("$rdhome/ES-DE/collections") else if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "The ES-DE collections folder was not found at its expected location, $rdhome/ES-DE/collections\nSomething may be wrong with your RetroDECK installation." fi log i "Warning: Path does not exist: ES-DE/collections = $rdhome/ES-DE/collections" fi if [[ -e "$rdhome/ES-DE/gamelists" ]]; then paths_to_backup+=("$rdhome/ES-DE/gamelists") else if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "The ES-DE gamelists folder was not found at its expected location, $rdhome/ES-DE/gamelists\nSomething may be wrong with your RetroDECK installation." fi log i "Warning: Path does not exist: ES-DE/gamelists = $rdhome/ES-DE/gamelists" fi if [[ -e "$rdhome/ES-DE/custom_systems" ]]; then paths_to_backup+=("$rdhome/ES-DE/custom_systems") else if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "The ES-DE custom_systems folder was not found at its expected location, $rdhome/ES-DE/custom_systems\nSomething may be wrong with your RetroDECK installation." fi log i "Warning: Path does not exist: ES-DE/custom_systems = $rdhome/ES-DE/custom_systems" fi # Check if we found any valid paths if [[ ${#paths_to_backup[@]} -eq 0 ]]; then if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "No valid userdata folders were found.\nSomething may be wrong with your RetroDECK installation." fi log e "Error: No valid paths found in config file" return 1 fi elif [[ "$backup_type" == "core" ]]; then for folder_name in "${!config_paths[@]}"; do if [[ $folder_name =~ (saves_folder|states_folder|logs_folder) ]]; then # Only include these paths path_value="${config_paths[$folder_name]}" if [[ -e "$path_value" ]]; then paths_to_backup+=("$path_value") log i "Adding to backup: $folder_name = $path_value" else if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "The $folder_name was not found at its expected location, $path_value\nSomething may be wrong with your RetroDECK installation." fi log i "Warning: Path does not exist: $folder_name = $path_value" fi fi done # Add static paths not defined in retrodeck.cfg if [[ -e "$rdhome/ES-DE/collections" ]]; then paths_to_backup+=("$rdhome/ES-DE/collections") else if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "The ES-DE collections folder was not found at its expected location, $rdhome/ES-DE/collections\nSomething may be wrong with your RetroDECK installation." fi log i "Warning: Path does not exist: ES-DE/collections = $rdhome/ES-DE/collections" fi if [[ -e "$rdhome/ES-DE/gamelists" ]]; then paths_to_backup+=("$rdhome/ES-DE/gamelists") else if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "The ES-DE gamelists folder was not found at its expected location, $rdhome/ES-DE/gamelists\nSomething may be wrong with your RetroDECK installation." fi log i "Warning: Path does not exist: ES-DE/gamelists = $rdhome/ES-DE/gamelists" fi if [[ -e "$rdhome/ES-DE/custom_systems" ]]; then paths_to_backup+=("$rdhome/ES-DE/custom_systems") else if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "The ES-DE custom_systems folder was not found at its expected location, $rdhome/ES-DE/custom_systems\nSomething may be wrong with your RetroDECK installation." fi log i "Warning: Path does not exist: ES-DE/custom_systems = $rdhome/ES-DE/custom_systems" fi # Check if we found any valid paths if [[ ${#paths_to_backup[@]} -eq 0 ]]; then if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "No valid userdata folders were found.\nSomething may be wrong with your RetroDECK installation." fi log e "Error: No valid paths found in config file" return 1 fi elif [[ "$backup_type" == "custom" ]]; then if [[ "$#" -eq 0 ]]; then # Check if any paths were provided in the arguments if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "No valid backup locations were specified. Please try again." fi log e "Error: No paths specified for custom backup" return 1 fi # Process each argument - it could be a variable name or a direct path for arg in "$@"; do # Check if argument is a variable name in the config if [[ -n "${config_paths[$arg]}" ]]; then path_value="${config_paths[$arg]}" if [[ -e "$path_value" ]]; then paths_to_backup+=("$path_value") log i "Added to backup: $arg = $path_value" else if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "The $arg was not found at its expected location, $path_value.\nSomething may be wrong with your RetroDECK installation." fi log e "Error: Path from variable '$arg' does not exist: $path_value" return 1 fi # Otherwise treat it as a direct path elif [[ -e "$arg" ]]; then paths_to_backup+=("$arg") log i "Added to backup: $arg" else if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "The path $arg was not found at its expected location.\nPlease check the path and try again." fi log e "Error: '$arg' is neither a valid variable name nor an existing path" return 1 fi done fi # Calculate total size of selected paths log i "Calculating size of backup data..." total_size=0 if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then # Show progress dialog if running Zenity Configurator total_size_file=$(mktemp) # Create temp file for Zenity subshell data extraction ( for path in "${paths_to_backup[@]}"; do if [[ -e "$path" ]]; then log d "Checking size of path $path" path_size=$(du -sk "$path" 2>/dev/null | cut -f1) # Get size in KB path_size=$((path_size * 1024)) # Convert to bytes for calculation total_size=$((total_size + path_size)) echo "$total_size" > "$total_size_file" fi done ) | rd_zenity --icon-name=net.retrodeck.retrodeck --progress --no-cancel --pulsate --auto-close \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK Configurator Utility - Userdata Backup" \ --text="Verifying there is enough free space for the backup, please wait..." total_size=$(cat "$total_size_file") rm "$total_size_file" # Clean up temp file else # If running in CLI for path in "${paths_to_backup[@]}"; do if [[ -e "$path" ]]; then log d "Checking size of path $path" path_size=$(du -sk "$path" 2>/dev/null | cut -f1) # Get size in KB path_size=$((path_size * 1024)) # Convert to bytes for calculation total_size=$((total_size + path_size)) fi done fi # Get available space at destination available_space=$(df -B1 "$backups_folder" | awk 'NR==2 {print $4}') # Log sizes for reference log i "Total size of backup data: $(numfmt --to=iec-i --suffix=B "$total_size")" log i "Available space at destination: $(numfmt --to=iec-i --suffix=B "$available_space")" # Check if we have enough space (using uncompressed size as a conservative estimate) if [[ "$available_space" -lt "$total_size" ]]; then if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then configurator_generic_dialog "RetroDECK Userdata Backup" "There is not enough free space to perform this backup.\n\nYou need at least $(numfmt --to=iec-i --suffix=B "$total_size"),\nplease free up some space and try again." fi log e "Error: Not enough space to perform backup. Need at least $(numfmt --to=iec-i --suffix=B "$total_size")" return 1 fi log i "Starting backup process..." if [[ "$CONFIGURATOR_GUI" == "zenity" ]]; then # Show progress dialog if running Zenity Configurator ( # Create zip with selected paths if zip -rq9 "$zip_file" "${paths_to_backup[@]}" >> "$backup_log_file" 2>&1; then # Rotate backups for the specific type cd "$backups_folder" || return 1 ls -t *_${backup_type}.zip | tail -n +4 | xargs -r rm final_size=$(du -h "$zip_file" | cut -f1) configurator_generic_dialog "RetroDECK Userdata Backup" "The backup to $zip_file was successful, final size is $final_size.\n\nThe backups have been rotated, keeping the last 3 of the $backup_type backup type." log i "Backup completed successfully: $zip_file (Size: $final_size)" log i "Older backups rotated, keeping latest 3 of type $backup_type" if [[ ! -s "$backup_log_file" ]]; then # If the backup log file is empty, meaning zip threw no errors rm -f "$backup_log_file" fi else configurator_generic_dialog "RetroDECK Userdata Backup" "Something went wrong with the backup process. Please check the log $backup_log_file for more information." log i "Error: Backup failed" return 1 fi ) | rd_zenity --icon-name=net.retrodeck.retrodeck --progress --no-cancel --pulsate --auto-close \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK Configurator Utility - Userdata Backup" \ --text="Compressing files into backup, please wait..." else if zip -rq9 "$zip_file" "${paths_to_backup[@]}" >> "$backup_log_file" 2>&1; then # Rotate backups for the specific type cd "$backups_folder" || return 1 ls -t *_${backup_type}.zip | tail -n +4 | xargs -r rm final_size=$(du -h "$zip_file" | cut -f1) log i "Backup completed successfully: $zip_file (Size: $final_size)" log i "Older backups rotated, keeping latest 3 of type $backup_type" if [[ ! -s "$backup_log_file" ]]; then # If the backup log file is empty, meaning zip threw no errors rm -f "$backup_log_file" fi else log i "Error: Backup failed" return 1 fi fi } make_name_pretty() { # This function will take an internal system name (like "gbc") and return a pretty version for user display ("Nintendo GameBoy Color") # If the name is nout found it only returns the short name such as "gbc" # USAGE: make_name_pretty "system name" local system_name="$1" # Use jq to parse the JSON and find the pretty name local pretty_name=$(jq -r --arg name "$system_name" '.system[$name].name // $name' "$features") echo "$pretty_name" } finit_browse() { # Function for choosing data directory location during first/forced init path_selected=false while [ $path_selected == false ] do local target="$(rd_zenity --file-selection --title="Choose RetroDECK data directory location" --directory)" if [[ ! -z "$target" ]]; then if [[ -w "$target" ]]; then rd_zenity --question --no-wrap --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --title "RetroDECK" \ --cancel-label="No" \ --ok-label "Yes" \ --text="Your RetroDECK data folder will be:\n\n$target/retrodeck\n\nis that ok?" if [ $? == 0 ] #yes then path_selected=true echo "$target/retrodeck" break else rd_zenity --question --no-wrap --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --title "RetroDECK" --cancel-label="No" --ok-label "Yes" --text="Do you want to quit?" if [ $? == 0 ] # yes, quit then quit_retrodeck fi fi fi else rd_zenity --error --no-wrap \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK" \ --ok-label "Quit" \ --text="No location was selected. Please run RetroDECK again to retry." quit_retrodeck fi done } finit_user_options_dialog() { finit_available_options=() finit_extracted_options=$(jq -r '.finit_default_options | to_entries[] | "\(.value.enabled)^\(.value.name)^\(.value.description)^\(.key)"' "$features") # Read finit_default_options from features.json using jq while IFS="^" read -r enabled option_name option_desc option_tag; do finit_available_options+=("$enabled" "$option_name" "$option_desc" "$option_tag") done <<< "$finit_extracted_options" local choices=$(rd_zenity \ --list --width=1200 --height=720 \ --checklist --hide-column=4 --ok-label="Confirm Selections" --extra-button="Enable All" \ --separator=" " --print-column=4 \ --text="Choose which options to enable:" \ --column "Enabled?" \ --column "Option" \ --column "Description" \ --column "option_flag" \ "${finit_available_options[@]}") echo "${choices[*]}" } finit() { # Force/First init, depending on the situation log i "Executing finit" # Internal or SD Card? local finit_dest_choice=$(configurator_destination_choice_dialog "RetroDECK data" "Welcome to the first setup of RetroDECK.\nPlease carefully read each message prompted during the installation process to avoid any unwanted misconfigurations.\n\nWhere do you want your RetroDECK data folder to be located?\nIn this location a \"retrodeck\" folder will be created.\nThis is the folder that you will use to contain all your important files, such as your own ROMs, BIOSs, Saves and Scraped Data." ) if [[ "$finit_dest_choice" == "" ]]; then log i "User closed the window" else log i "User choice: $finit_dest_choice" fi case "$finit_dest_choice" in "Quit" | "Back" | "" ) # Back, Quit or X button quits rm -f "$rd_conf" # Cleanup unfinished retrodeck.cfg if first install is interrupted log i "Now quitting" quit_retrodeck ;; "Internal Storage" ) # Internal log i "Internal selected" rdhome="$HOME/retrodeck" if [[ -L "$rdhome" ]]; then #Remove old symlink from existing install, if it exists unlink "$rdhome" fi ;; "SD Card" ) log i "SD Card selected" if [ ! -d "$sdcard" ] # SD Card path is not existing then log e "SD card not found" rd_zenity --error --no-wrap \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK" \ --ok-label "Browse" \ --text="SD Card was not found in the default location.\nPlease choose the SD Card root.\nA retrodeck folder will be created starting from the directory that you selected." rdhome="$(finit_browse)" # Calling the browse function if [[ -z "$rdhome" ]]; then # If user hit the cancel button rm -f "$rd_conf" # Cleanup unfinished retrodeck.cfg if first install is interrupted exit 2 fi elif [ ! -w "$sdcard" ] #SD card found but not writable then log e "SD card found but not writable" rd_zenity --error --no-wrap \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK" \ --ok-label "Quit" \ --text="SD card was found but is not writable\nThis can happen with cards formatted on PC.\nPlease format the SD card through the Steam Deck's Game Mode and run RetroDECK again." rm -f "$rd_conf" # Cleanup unfinished retrodeck.cfg if first install is interrupted log i "Now quitting" quit_retrodeck else rdhome="$sdcard/retrodeck" fi ;; "Custom Location" ) log i "Custom Location selected" rd_zenity --info --no-wrap \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK" \ --ok-label "Browse" \ --text="Please choose the root folder for the RetroDECK data.\nA retrodeck folder will be created starting from the directory that you selected." rdhome="$(finit_browse)" # Calling the browse function if [[ -z "$rdhome" ]]; then # If user hit the cancel button rm -f "$rd_conf" # Cleanup unfinished retrodeck.cfg if first install is interrupted exit 2 fi ;; esac log i "\"retrodeck\" folder will be located in \"$rdhome\"" prepare_component "reset" "retrodeck" # Parse the [paths] section of retrodeck.cfg and set the value of / create all needed folders conf_write # Write the new values to retrodeck.cfg configurator_generic_dialog "RetroDECK Initial Setup" "The next dialog will be a list of optional actions to take during the initial setup.\n\nIf you choose to not do any of these now, they can be done later through the Configurator." local finit_options_choices=$(finit_user_options_dialog) if [[ "$finit_options_choices" =~ (rpcs3_firmware|Enable All) ]]; then # Additional information on the firmware install process, as the emulator needs to be manually closed configurator_generic_dialog "RPCS3 Firmware Install" "You have chosen to install the RPCS3 firmware during the RetroDECK first setup.\n\nThis process will take several minutes and requires network access.\n\nRPCS3 will be launched automatically at the end of the RetroDECK setup process.\nOnce the firmware is installed, please close the emulator to finish the process." fi if [[ "$finit_options_choices" =~ (vita3k_firmware|Enable All) ]]; then # Additional information on the firmware install process, as the emulator needs to be manually closed configurator_generic_dialog "Vita3K Firmware Install" "You have chosen to install the Vita3K firmware during the RetroDECK first setup.\n\nThis process will take several minutes and requires network access.\n\nVita3K will be launched automatically at the end of the RetroDECK setup process.\nOnce the firmware is installed, please close the emulator to finish the process." fi rd_zenity --icon-name=net.retrodeck.retrodeck --info --no-wrap \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --title "RetroDECK" \ --text="RetroDECK will now install the needed files, which can take up to one minute.\nRetroDECK will start once the process is completed.\n\nPress OK to continue." ( prepare_component "reset" "all" build_retrodeck_current_presets deploy_helper_files # Optional actions based on user choices if [[ "$finit_options_choices" =~ (rpcs3_firmware|Enable All) ]]; then if [[ $(check_network_connectivity) == "true" ]]; then update_rpcs3_firmware fi fi if [[ "$finit_options_choices" =~ (vita3k_firmware|Enable All) ]]; then if [[ $(check_network_connectivity) == "true" ]]; then update_vita3k_firmware fi fi if [[ "$finit_options_choices" =~ (rd_controller_profile|Enable All) ]]; then install_retrodeck_controller_profile fi if [[ "$finit_options_choices" =~ (rd_prepacks|Enable All) ]]; then install_retrodeck_starterpack fi ) | rd_zenity --icon-name=net.retrodeck.retrodeck --progress --no-cancel --pulsate --auto-close \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK Finishing Initialization" \ --text="RetroDECK is finishing the initial setup process, please wait.\n\n" add_retrodeck_to_steam create_lock # Inform the user where to put the ROMs and BIOS files rd_zenity --info --no-wrap \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK Setup Complete" \ --text="RetroDECK setup is complete!\n\nPlease place your game files in the following directory: $rdhome/roms\n\nand your BIOS files in: $rdhome/bios\n\nYou can use the BIOS checker tool available trough the RetroDECK Configurator\nor refer to the RetroDECK WIKI for more information about the required BIOS files and their proper paths.\n\nYou can now start using RetroDECK." } install_retrodeck_starterpack() { # This function will install the roms, gamelists and metadata for the RetroDECK Starter Pack, a curated selection of games the creators of RetroDECK enjoy. # USAGE: install_retrodeck_starterpack ## DOOM section ## cp /app/retrodeck/extras/doom1.wad "$roms_folder/doom/doom1.wad" # No -f in case the user already has it create_dir "$XDG_CONFIG_HOME/ES-DE/gamelists/doom" if [[ ! -f "$XDG_CONFIG_HOME/ES-DE/gamelists/doom/gamelist.xml" ]]; then # Don't overwrite an existing gamelist cp "/app/retrodeck/rd_prepacks/doom/gamelist.xml" "$XDG_CONFIG_HOME/ES-DE/gamelists/doom/gamelist.xml" fi create_dir "$media_folder/doom" unzip -oq "/app/retrodeck/rd_prepacks/doom/doom.zip" -d "$media_folder/doom/" } install_retrodeck_controller_profile() { # This function will install the needed files for the custom RetroDECK controller profile # NOTE: These files need to be stored in shared locations for Steam, outside of the normal RetroDECK folders and should always be an optional user choice # BIGGER NOTE: As part of this process, all emulators will need to have their configs hard-reset to match the controller mappings of the profile # USAGE: install_retrodeck_controller_profile if [[ -d "$HOME/.steam/steam/controller_base/templates/" || -d "$HOME/.var/app/com.valvesoftware.Steam/.steam/steam/controller_base/templates/" ]]; then if [[ -d "$HOME/.steam/steam/controller_base/templates/" ]]; then # If a normal binary Steam install exists rsync -rlD --mkpath "/app/retrodeck/binding_icons/" "$HOME/.steam/steam/tenfoot/resource/images/library/controller/binding_icons/" rsync -rlD --mkpath "$config/retrodeck/controller_configs/" "$HOME/.steam/steam/controller_base/templates/" fi if [[ -d "$HOME/.var/app/com.valvesoftware.Steam/.steam/steam/controller_base/templates/" ]]; then # If a Flatpak Steam install exists rsync -rlD --mkpath "/app/retrodeck/binding_icons/" "$HOME/.var/app/com.valvesoftware.Steam/.steam/steam/tenfoot/resource/images/library/controller/binding_icons/" rsync -rlD --mkpath "$config/retrodeck/controller_configs/" "$HOME/.var/app/com.valvesoftware.Steam/.steam/steam/controller_base/templates/" fi else configurator_generic_dialog "RetroDECK Controller Profile Install" "The target directories for the controller profile do not exist.\n\nThis may happen if you do not have Steam installed or the location is does not have permission to be read." fi } create_lock() { # creating RetroDECK's lock file and writing the version in the config file version=$hard_version touch "$lockfile" conf_write } update_splashscreens() { # This script will purge any existing ES graphics and reload them from RO space into somewhere ES will look for it # USAGE: update_splashscreens log i "Updating splash screen" rm -rf "$XDG_CONFIG_HOME/ES-DE/resources/graphics" rsync -rlD --mkpath "/app/retrodeck/graphics/" "$XDG_CONFIG_HOME/ES-DE/resources/graphics/" } deploy_helper_files() { # This script will distribute helper documentation files throughout the filesystem according to the JSON configuration # USAGE: deploy_helper_files # Extract helper files information using jq helper_files=$(jq -r '.helper_files | to_entries | map("\(.value.filename)^\(.value.location)")[]' "$features") # Iterate through each helper file entry while IFS='^' read -r file dest; do if [[ ! -z "$file" ]] && [[ ! -z "$dest" ]]; then eval current_dest="$dest" cp -f "$helper_files_folder/$file" "$current_dest/$file" fi done <<< "$helper_files" } splash_screen() { # This function will replace the RetroDECK startup splash screen with a different image if the day and time match a listing in the JSON data. # USAGE: splash_screen current_day=$(date +"%m%d") # Read the current date in a format that can be calculated in ranges current_time=$(date +"%H%M") # Read the current time in a format that can be calculated in ranges # Read the JSON file and extract splash screen data using jq splash_screen=$(jq -r --arg current_day "$current_day" --arg current_time "$current_time" ' .splash_screens | to_entries[] | select( ($current_day | tonumber) >= (.value.start_date | tonumber) and ($current_day | tonumber) <= (.value.end_date | tonumber) and ($current_time | tonumber) >= (.value.start_time | tonumber) and ($current_time | tonumber) <= (.value.end_time | tonumber) ) | .value.filename' "$features") # Determine the splash file to use if [[ -n "$splash_screen" ]]; then new_splash_file="$splashscreen_dir/$splash_screen" else new_splash_file="$default_splash_file" fi cp -f "$new_splash_file" "$current_splash_file" # Deploy assigned splash screen } install_release() { # Logging the release tag and URL log d "Attempting to install release: $1 from repo $update_repo" # Construct the URL for the flatpak file if [ "$(get_setting_value "$rd_conf" "update_repo" "retrodeck" "options")" == "RetroDECK" ]; then iscooker="" else iscooker="-cooker" fi local flatpak_url="https://github.com/$git_organization_name/$update_repo/releases/download/$1/RetroDECK$iscooker.flatpak" log d "Constructed flatpak URL: $flatpak_url" # Confirm installation with the user rd_zenity --question --icon-name=net.retrodeck.retrodeck --no-wrap \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK Updater" \ --text="$1 will be now installed.\nThe update process may take several minutes.\n\nAfter the update is complete, RetroDECK will close. When you run it again, you will be using the latest version.\n\nDo you want to continue?" rc=$? # Capture return code if [[ $rc == "1" ]]; then # If the user clicks "Cancel" return 0 fi ( mkdir -p "$rdhome/RetroDECK_Updates" # Download the flatpak file wget -P "$rdhome/RetroDECK_Updates" "$flatpak_url" -O "$rdhome/RetroDECK_Updates/RetroDECK$iscooker.flatpak" # Check if the download was successful if [[ $? -ne 0 ]]; then configurator_generic_dialog "Error" "Failed to download the flatpak file. Please check the release tag and try again." return 1 fi # Remove the current version before installing the new one to avoid duplicates flatpak-spawn --host flatpak remove --noninteractive -y net.retrodeck.retrodeck # Install the new version flatpak-spawn --host flatpak install --user --bundle --noninteractive -y "$rdhome/RetroDECK_Updates/RetroDECK$iscooker.flatpak" # Cleanup old bundles to save space rm -rf "$rdhome/RetroDECK_Updates" ) | rd_zenity --icon-name=net.retrodeck.retrodeck --progress --no-cancel --pulsate --auto-close \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK Updater" \ --text="RetroDECK is updating to the selected version, please wait." configurator_generic_dialog "RetroDECK Online Update" "The update process is now complete!\n\nRetroDECK will now quit." quit_retrodeck } ponzu() { # This function is used to extract some specific appimages # Check if any of the specified files exist # If RetroDECK is reset Ponzu must re-cooked log d "Checking for Ponzu" local tmp_folder="/tmp/extracted" local ponzu_files=("$rdhome"/ponzu/Citra*.AppImage "$rdhome"/ponzu/citra*.AppImage "$rdhome"/ponzu/Yuzu*.AppImage "$rdhome"/ponzu/yuzu*.AppImage) local data_dir local appimage local executable # if the binaries are found, ponzu should be set as true into the retrodeck config if [ -f "$XDG_DATA_HOME/ponzu/Citra/bin/citra-qt" ]; then log d "Citra binaries has already been installed, checking for updates and forcing the setting as true." set_setting_value "$rd_conf" "akai_ponzu" "true" retrodeck "options" fi if [ -f "$XDG_DATA_HOME/ponzu/Yuzu/bin/yuzu" ]; then log d "Yuzu binaries has already been installed, checking for updates and forcing the setting as true." set_setting_value "$rd_conf" "kiroi_ponzu" "true" retrodeck "options" fi # Loop through all ponzu files for ponzu_file in "${ponzu_files[@]}"; do # Check if the current ponzu file exists if [ -f "$ponzu_file" ]; then if [[ "$ponzu_file" == *itra* ]]; then log i "Found akai ponzu! Elaborating it" data_dir="$XDG_DATA_HOME/ponzu/Citra" local message="Akai ponzu is served, enjoy" elif [[ "$ponzu_file" == *uzu* ]]; then log i "Found kiroi ponzu! Elaborating it" data_dir="$XDG_DATA_HOME/ponzu/Yuzu" local message="Kiroi ponzu is served, enjoy" else log e "AppImage not recognized, not a ponzu ingredient!" exit 1 fi appimage="$ponzu_file" chmod +x "$ponzu_file" create_dir "$data_dir" log d "Moving AppImage in \"$data_dir\"" mv "$appimage" "$data_dir" cd "$data_dir" local filename=$(basename "$ponzu_file") log d "Setting appimage=$data_dir/$filename" appimage="$data_dir/$filename" log d "Extracting AppImage" "$appimage" --appimage-extract create_dir "$tmp_folder" log d "Cleaning up" cp -r squashfs-root/* "$tmp_folder" rm -rf * if [[ "$ponzu_file" == *itra* ]]; then mv "$tmp_folder/usr/"** . executable="$data_dir/bin/citra" log d "Making $executable and $executable-qt executable" chmod +x "$executable" chmod +x "$executable-qt" prepare_component "reset" "citra" set_setting_value "$rd_conf" "akai_ponzu" "true" retrodeck "options" elif [[ "$ponzu_file" == *uzu* ]]; then mv "$tmp_folder/usr/"** . executable="$data_dir/bin/yuzu" log d "Making $executable executable" chmod +x "$executable" prepare_component "reset" "yuzu" set_setting_value "$rd_conf" "kiroi_ponzu" "true" retrodeck "options" fi cd - log i "$message" rm -rf "$tmp_folder" fi done rm -rf "$rdhome/ponzu" } ponzu_remove() { # Call me with yuzu or citra and I will remove them if [[ "$1" == "citra" ]]; then if [[ $(configurator_generic_question_dialog "Ponzu - Remove Citra" "Do you really want to remove Citra binaries?\n\nYour games and saves will not be deleted.") == "true" ]]; then log i "Ponzu: removing Citra" rm -rf "$XDG_DATA_HOME/ponzu/Citra" set_setting_value "$rd_conf" "akai_ponzu" "false" retrodeck "options" configurator_generic_dialog "Ponzu - Remove Citra" "Done, Citra is now removed from RetroDECK" fi elif [[ "$1" == "yuzu" ]]; then if [[ $(configurator_generic_question_dialog "Ponzu - Remove Yuzu" "Do you really want to remove Yuzu binaries?\n\nYour games and saves will not be deleted.") == "true" ]]; then log i "Ponzu: removing Yuzu" rm -rf "$XDG_DATA_HOME/ponzu/Yuzu" set_setting_value "$rd_conf" "kiroi_ponzu" "false" retrodeck "options" configurator_generic_dialog "Ponzu - Remove Yuzu" "Done, Yuzu is now removed from RetroDECK" fi else log e "Ponzu: \"$1\" is not a vaild choice for removal, quitting" fi configurator_tools_dialog } release_selector() { # Show a progress bar ( while true; do echo "# Fetching all available releases from GitHub repositories... Please wait. This may take some time." ; sleep 1 done ) | rd_zenity --progress --title="Fetching Releases" --text="Fetching releases..." --pulsate --no-cancel --auto-close --width=500 --height=150 & progress_pid=$! # save process PID to kill it later log d "Fetching releases from GitHub API for repository $cooker_repository_name" # Fetch the main release from the RetroDECK repository log d "Fetching latest main release from GitHub API for repository RetroDECK" local main_release=$(curl -s "https://api.github.com/repos/$git_organization_name/RetroDECK/releases/latest") if [[ -z "$main_release" ]]; then log e "Failed to fetch the main release" kill $progress_pid # kill the progress bar configurator_generic_dialog "Error" "Unable to fetch the main release. Please check your network connection or try again later." return 1 fi main_tag_name=$(echo "$main_release" | jq -r '.tag_name') main_published_at=$(echo "$main_release" | jq -r '.published_at') # Convert published_at to human-readable format for the main release main_human_readable_date=$(date -d "$main_published_at" +"%d %B %Y %H:%M") # Add the main release as the first entry in the release array local release_array=("Main Release" "$main_tag_name" "$main_human_readable_date") # Fetch all releases (including draft and pre-release) from the Cooker repository local releases=$(curl -s "https://api.github.com/repos/$git_organization_name/$cooker_repository_name/releases?per_page=100") if [[ -z "$releases" ]]; then log e "Failed to fetch releases or no releases available" kill $progress_pid # kill the progress bar configurator_generic_dialog "Error" "Unable to fetch releases. Please check your network connection or try again later." return 1 fi # Loop through each release and add to the release array while IFS= read -r release; do tag_name=$(echo "$release" | jq -r '.tag_name') published_at=$(echo "$release" | jq -r '.published_at') draft=$(echo "$release" | jq -r '.draft') prerelease=$(echo "$release" | jq -r '.prerelease') # Classifying releases if echo "$tag_name" | grep -q "PR"; then status="Pull Request" elif [[ "$draft" == "true" ]]; then status="Draft" elif [[ "$prerelease" == "true" ]]; then status="Pre-release" elif [[ "$cooker_repository_name" == *"Cooker"* ]]; then status="Cooker" else status="Main" fi # Convert published_at to human-readable format, if available if [[ "$published_at" != "null" ]]; then human_readable_date=$(date -d "$published_at" +"%d %B %Y %H:%M") else human_readable_date="Not published" fi # Ensure fields are properly aligned for Zenity release_array+=("$status" "$tag_name" "$human_readable_date") done < <(echo "$releases" | jq -c '.[]' | sort -t: -k3,3r) # kill the progress bar before opening the release list window kill $progress_pid if [[ ${#release_array[@]} -eq 0 ]]; then configurator_generic_dialog "RetroDECK Updater" "No available releases found, exiting." log d "No available releases found" return 1 fi log d "Showing available releases" # Display releases in a Zenity list dialog with three columns selected_release=$( rd_zenity --list \ --icon-name=net.retrodeck.retrodeck \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK Configurator Cooker Releases - Select Release" \ --column="Branch" --column="Release Tag" --column="Published Date" --width=1280 --height=800 \ --separator="|" --print-column='ALL' "${release_array[@]}" ) log i "Selected release: $selected_release" if [[ -z "$selected_release" ]]; then log d "No release selected, user exited." return 1 fi # Parse the selected release using the pipe separator IFS='|' read -r selected_branch selected_tag selected_date <<< "$selected_release" selected_branch=$(echo "$selected_branch" | xargs) # Trim any extra spaces selected_tag=$(echo "$selected_tag" | xargs) selected_date=$(echo "$selected_date" | xargs) log d "Selected branch: $selected_branch, release: $selected_tag, date: $selected_date" rd_zenity --question --icon-name=net.retrodeck.retrodeck --no-wrap \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK Configurator Cooker Release - Confirm Selection" \ --text="Are you sure you want to install the following release?\n\n$selected_branch: \"$selected_tag\"\nPublished on $selected_date?" if [[ $? -eq 0 ]]; then log d "User confirmed installation of release $selected_tag" if echo "$selected_release" | grep -q "Main Release"; then set_setting_value "$rd_conf" "update_repo" "$main_repository_name" retrodeck "options" log i "Switching to main channel" else set_setting_value "$rd_conf" "update_repo" "$cooker_repository_name" retrodeck "options" log i "Switching to cooker channel" fi set_setting_value "$rd_conf" "branch" "$selected_branch" "retrodeck" "options" log d "Set branch to $selected_branch in configuration" install_release "$selected_tag" else log d "User canceled installation" return 0 fi } quit_retrodeck() { log i "Quitting ES-DE" pkill -f "es-de" # if steam sync is on do the magic if [[ $(get_setting_value "$rd_conf" "steam_sync" "retrodeck" "options") == "true" ]]; then export CONFIGURATOR_GUI="zenity" steam_sync fi log i "Shutting down RetroDECK's framework" pkill -f "retrodeck" log i "See you next time" exit } start_retrodeck() { get_steam_user # get steam user info splash_screen # Check if today has a surprise splashscreen and load it if so ponzu log d "Checking if PortMaster should be shown" if [[ $(get_setting_value "$rd_conf" "portmaster_show" "retrodeck" "options") == "false" ]]; then log d "Assuring that PortMaster is hidden on ES-DE" portmaster_show "false" else log d "Assuring that PortMaster is shown on ES-DE" portmaster_show "true" fi log i "Starting RetroDECK v$version" es-de } # Function to convert XML tags to Markdown convert_to_markdown() { local xml_content=$(cat "$1") local output_file="$1.md" # Convert main tags echo "$xml_content" | \ sed -e 's|

\(.*\)

|## \1|g' \ -e 's|
    ||g' \ -e 's|
||g' \ -e 's|

\(.*\)

|# \1|g' \ -e 's|
  • \(.*\)
  • |- \1|g' \ -e 's|||g' \ -e 's|||g' \ -e '/<[^>]*>/d' > "$output_file" # Remove any other XML tags and output to .md file } # This function updates RetroArch by synchronizing shaders, cores, and border overlays. # It should be called whenever RetroArch is reset or updated. retroarch_updater(){ log i "Running RetroArch updater" # Synchronize cores from the application share directory to the RetroArch cores directory rsync -rlD --mkpath "/app/share/libretro/cores/" "$XDG_CONFIG_HOME/retroarch/cores/" && log d "RetroArch cores updated correctly" # Synchronize border overlays from the RetroDeck configuration directory to the RetroArch overlays directory rsync -rlD --mkpath "/app/retrodeck/config/retroarch/borders/" "$XDG_CONFIG_HOME/retroarch/overlays/borders/" && log d "RetroArch overlays and borders updated correctly" } portmaster_show(){ log d "Setting PortMaster visibility in ES-DE" if [ "$1" = "true" ]; then log d "\"$roms_folder/portmaster/PortMaster.sh\" is not found, installing it" install -Dm755 "$XDG_DATA_HOME/PortMaster/PortMaster.sh" "$roms_folder/portmaster/PortMaster.sh" && log d "PortMaster is correctly showing in ES-DE" set_setting_value "$rd_conf" "portmaster_show" "true" retrodeck "options" elif [ "$1" = "false" ]; then rm -rf "$roms_folder/portmaster/PortMaster.sh" && log d "PortMaster is correctly hidden in ES-DE" set_setting_value "$rd_conf" "portmaster_show" "false" retrodeck "options" else log e "\"$1\" is not a valid choice, quitting" fi } open_component(){ if [[ -z "$1" ]]; then cmd=$(jq -r '.emulator[] | select(.ponzu != true) | .name' "$features") if [[ $(get_setting_value "$rd_conf" "akai_ponzu" "retrodeck" "options") == "true" ]]; then cmd+="\n$(jq -r '.emulator.citra | .name' "$features")" fi if [[ $(get_setting_value "$rd_conf" "kiroi_ponzu" "retrodeck" "options") == "true" ]]; then cmd+="\n$(jq -r '.emulator.yuzu | .name' "$features")" fi echo -e "This command expects one of the following components as arguments:\n$(echo -e "$cmd")" return fi if [[ "$1" == "--list" ]]; then cmd=$(jq -r '.emulator[] | select(.ponzu != true) | .name' "$features") if [[ $(get_setting_value "$rd_conf" "akai_ponzu" "retrodeck" "options") == "true" ]]; then cmd+="\n$(jq -r '.emulator.citra | .name' "$features")" fi if [[ $(get_setting_value "$rd_conf" "kiroi_ponzu" "retrodeck" "options") == "true" ]]; then cmd+="\n$(jq -r '.emulator.yuzu | .name' "$features")" fi echo -e "$cmd" return fi if [[ "$1" == "--getdesc" ]]; then cmd=$(jq -r '.emulator[] | select(.ponzu != true) | "\(.description)"' "$features") if [[ $(get_setting_value "$rd_conf" "akai_ponzu" "retrodeck" "options") == "true" ]]; then cmd+="\n$(jq -r '.emulator.citra | "\(.description)"' "$features")" fi if [[ $(get_setting_value "$rd_conf" "kiroi_ponzu" "retrodeck" "options") == "true" ]]; then cmd+="\n$(jq -r '.emulator.yuzu | "\(.description)"' "$features")" fi echo -e "$cmd" return fi launch_exists=$(jq -r --arg name "$1" '.emulator[] | select(.name == $name) | has("launch")' "$features") if [[ "$launch_exists" != "true" ]]; then echo "Error: The component '$1' cannot be opened." return 1 fi cmd=$(jq -r --arg name "$1" '.emulator[] | select(.name == $name and .ponzu != true) | .launch' "$features") if [[ -z "$cmd" && $(get_setting_value "$rd_conf" "akai_ponzu" "retrodeck" "options") == "true" && "$1" == "citra" ]]; then cmd=$(jq -r '.emulator.citra | .launch' "$features") fi if [[ -z "$cmd" && $(get_setting_value "$rd_conf" "kiroi_ponzu" "retrodeck" "options") == "true" && "$1" == "yuzu" ]]; then cmd=$(jq -r '.emulator.yuzu | .launch' "$features") fi if [[ -n "$cmd" ]]; then eval "$cmd" "${@:2}" else echo "Invalid component name: $1" echo "Please ensure the name is correctly spelled (case sensitive) and quoted if it contains spaces." fi } add_retrodeck_to_steam(){ log i "Checking if user wants to add RetroDECK to Steam" rd_zenity --question --no-wrap --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --title "RetroDECK" --cancel-label="No" --ok-label "Yes" \ --text="Do you want to add RetroDECK to Steam?" if [ $? == 0 ]; then ( log i "RetroDECK has been added to Steam" steam-rom-manager enable --names "RetroDECK Launcher" steam-rom-manager add ) | rd_zenity --progress --no-cancel --pulsate --auto-close \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "Adding RetroDECK to Steam" \ --text="Please wait while RetroDECK is being added to Steam...\n\n" rd_zenity --info --no-wrap --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" --title "RetroDECK" --text="RetroDECK has been added to Steam.\n\nPlease close and reopen Steam to see the changes." fi } repair_paths() { # This function will verify that all folders defined in the [paths] section of retrodeck.cfg exist # If a folder doesn't exist and is defined outside of rdhome, it will check in rdhome first and have the user browse for them manually if it isn't there either # USAGE: repair_paths invalid_path_found="false" log i "Checking that all RetroDECK paths are valid" while read -r config_line; do local current_setting_name=$(get_setting_name "$config_line" "retrodeck") if [[ ! $current_setting_name =~ (rdhome|sdcard) ]]; then # Ignore these locations local current_setting_value=$(get_setting_value "$rd_conf" "$current_setting_name" "retrodeck" "paths") if [[ ! -d "$current_setting_value" ]]; then # If the folder doesn't exist as defined log i "$current_setting_name does not exist as defined, config is incorrect" if [[ ! -d "$rdhome/${current_setting_value#*retrodeck/}" ]]; then # If the folder doesn't exist within defined rdhome path if [[ ! -d "$sdcard/${current_setting_value#*retrodeck/}" ]]; then # If the folder doesn't exist within defined sdcard path log i "$current_setting_name cannot be found at any expected location, having user locate it manually" configurator_generic_dialog "RetroDECK Path Repair" "The RetroDECK $current_setting_name was not found in the expected location.\nThis may happen when the folder is moved manually.\n\nPlease browse to the current location of the $current_setting_name." new_path=$(directory_browse "RetroDECK $current_setting_name location") set_setting_value "$rd_conf" "$current_setting_name" "$new_path" retrodeck "paths" invalid_path_found="true" else # Folder does exist within defined sdcard path, update accordingly log i "$current_setting_name found in $sdcard/retrodeck, correcting path config" new_path="$sdcard/retrodeck/${current_setting_value#*retrodeck/}" set_setting_value "$rd_conf" "$current_setting_name" "$new_path" retrodeck "paths" invalid_path_found="true" fi else # Folder does exist within defined rdhome path, update accordingly log i "$current_setting_name found in $rdhome, correcting path config" new_path="$rdhome/${current_setting_value#*retrodeck/}" set_setting_value "$rd_conf" "$current_setting_name" "$new_path" retrodeck "paths" invalid_path_found="true" fi fi fi done < <(grep -v '^\s*$' "$rd_conf" | awk '/^\[paths\]/{f=1;next} /^\[/{f=0} f') if [[ $invalid_path_found == "true" ]]; then log i "One or more invalid paths repaired, fixing internal RetroDECK structures" conf_read dir_prep "$logs_folder" "$rd_logs_folder" prepare_component "postmove" "all" configurator_generic_dialog "RetroDECK Path Repair" "One or more incorrectly configured paths were repaired." else log i "All folders were found at their expected locations" configurator_generic_dialog "RetroDECK Path Repair" "All RetroDECK folders were found at their expected locations." fi } # Function to sanitize strings for filenames sanitize() { # Replace sequences of underscores with a single space echo "$1" | sed -e 's/_\{2,\}/ /g' -e 's/_/ /g' -e 's/:/ -/g' -e 's/&/and/g' -e 's%/%and%g' -e 's/ / /g' } get_cheevos_token() { # This function will attempt to authenticate with the RA API with the supplied credentials and will return a JSON object if successful # USAGE get_cheevos_token $username $password local cheevos_api_response=$(curl --silent --data "r=login&u=$1&p=$2" "$RA_API_URL") local cheevos_success=$(echo "$cheevos_api_response" | jq -r '.Success') if [[ "$cheevos_success" == "true" ]]; then log d "login succeeded" echo "$cheevos_api_response" else log d "login failed" return 1 fi } check_if_updated() { # Check if an update has happened if [ -f "$lockfile" ]; then if [ "$hard_version" != "$version" ]; then log d "Update triggered" log d "Lockfile found but the version doesn't match with the config file" log i "Config file's version is $version but the actual version is $hard_version" if grep -qF "cooker" <<< "$hard_version"; then # If newly-installed version is a "cooker" build log d "Newly-installed version is a \"cooker\" build" configurator_generic_dialog "RetroDECK Cooker Warning" "RUNNING COOKER VERSIONS OF RETRODECK CAN BE EXTREMELY DANGEROUS AND ALL OF YOUR RETRODECK DATA\n(INCLUDING BIOS FILES, BORDERS, DOWNLOADED MEDIA, GAMELISTS, MODS, ROMS, SAVES, STATES, SCREENSHOTS, TEXTURE PACKS AND THEMES)\nARE AT RISK BY CONTINUING!" set_setting_value "$rd_conf" "update_repo" "$cooker_repository_name" retrodeck "options" set_setting_value "$rd_conf" "update_check" "true" retrodeck "options" set_setting_value "$rd_conf" "developer_options" "true" retrodeck "options" set_setting_value "$rd_conf" "logging_level" "debug" retrodeck "options" cooker_base_version=$(echo "$version" | cut -d'-' -f2) choice=$(rd_zenity --icon-name=net.retrodeck.retrodeck --info --no-wrap --ok-label="Upgrade" --extra-button="Don't Upgrade" --extra-button="Full Wipe and Fresh Install" \ --window-icon="/app/share/icons/hicolor/scalable/apps/net.retrodeck.retrodeck.svg" \ --title "RetroDECK Cooker Upgrade" \ --text="You appear to be upgrading to a \"cooker\" build of RetroDECK.\n\nWould you like to perform the standard post-update process, skip the post-update process or remove ALL existing RetroDECK folders and data (including ROMs and saves) to start from a fresh install?\n\nPerforming the normal post-update process multiple times may lead to unexpected results.") rc=$? # Capture return code, as "Yes" button has no text value if [[ $rc == "1" ]]; then # If any button other than "Yes" was clicked if [[ "$choice" == "Don't Upgrade" ]]; then # If user wants to bypass the post_update.sh process this time. log i "Skipping upgrade process for cooker build, updating stored version in retrodeck.cfg" set_setting_value "$rd_conf" "version" "$hard_version" retrodeck # Set version of currently running RetroDECK to updated retrodeck.cfg elif [[ "$choice" == "Full Wipe and Fresh Install" ]]; then # Remove all RetroDECK data and start a fresh install if [[ $(configurator_generic_question_dialog "RetroDECK Cooker Reset" "This is going to remove all of the data in all locations used by RetroDECK!\n\n(INCLUDING BIOS FILES, BORDERS, DOWNLOADED MEDIA, GAMELISTS, MODS, ROMS, SAVES, STATES, SCREENSHOTS, TEXTURE PACKS AND THEMES)\n\nAre you sure you want to contine?") == "true" ]]; then if [[ $(configurator_generic_question_dialog "RetroDECK Cooker Reset" "Are you super sure?\n\nThere is no going back from this process, everything is gonzo.\nDust in the wind.\n\nYesterdays omelette.") == "true" ]]; then if [[ $(configurator_generic_question_dialog "RetroDECK Cooker Reset" "But are you super DUPER sure? We REAAAALLLLLYY want to make sure you know what is happening here.\n\nThe ~/retrodeck and ~/.var/app/net.retrodeck.retrodeck folders and ALL of their contents\nare about to be PERMANENTLY removed.\n\nStill sure you want to proceed?") == "true" ]]; then configurator_generic_dialog "RetroDECK Cooker Reset" "Ok, if you're that sure, here we go!" if [[ $(configurator_generic_question_dialog "RetroDECK Cooker Reset" "(Are you actually being serious here? Because we are...\n\nNo backsies.)") == "true" ]]; then log w "Removing RetroDECK data and starting fresh" rm -rf /var rm -rf "$HOME/retrodeck" rm -rf "$rdhome" source /app/libexec/global.sh finit fi fi fi fi fi else log i "Performing normal upgrade process for version $cooker_base_version" version="$cooker_base_version" # Temporarily assign cooker base version to $version so update script can read it properly. post_update fi else # If newly-installed version is a normal build. if grep -qF "cooker" <<< "$version"; then # If previously installed version was a cooker build cooker_base_version=$(echo "$version" | cut -d'-' -f2) version="$cooker_base_version" # Temporarily assign cooker base version to $version so update script can read it properly. set_setting_value "$rd_conf" "update_repo" "RetroDECK" retrodeck "options" set_setting_value "$rd_conf" "update_check" "false" retrodeck "options" set_setting_value "$rd_conf" "update_ignore" "" retrodeck "options" set_setting_value "$rd_conf" "developer_options" "false" retrodeck "options" set_setting_value "$rd_conf" "logging_level" "info" retrodeck "options" fi post_update # Executing post update script fi fi # Else, LOCKFILE IS NOT EXISTING (WAS REMOVED) # if the lock file doesn't exist at all means that it's a fresh install or a triggered reset else log w "Lockfile not found" finit # Executing First/Force init fi }