#!/bin/bash # Copyright 2024 Daniel Almeida Martins # License GPL-3.0-or-later # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see . # NOTE # This script performs two main operations, backup and restore. # - On backup, it archives and compresses the savegame files. These archives are # stored in a directory defined below. # - On restore, it restores the savegame files based on a backup archive. For # added cautiousness, right before the restore operation, a backup of the # current savegame files is performed to a temporary directory. Check the # restore_savegame function for these implementation details. # Backup archives are identified by their TAG, which is simply the date and time # when they were created (using the ISO 8601 format YYYYMMDDThhmm). # NOTE # The following variables control the core behaviour of the script: # backup_folder : The folder where the backup archives are stored. # backup_extension : File extension that determines the compression used. declare version=2.1 declare backup_folder="${HOME}/Documents/savegame" declare backup_extension="tzst" # NOTE # Below is the list of supported games. Each game needs three entries: # game_GAME : The display name of the game. # include_GAME : The directory to be backed up. # exclude_GAME : A list of files to be excluded from the backup. # Each game must use a different GAME which should be an acronym since it will # also be used from the command line when you want to reference a specific game. declare game_ash="A Short Hike" declare include_ash="${HOME}/.config/unity3d/adamgryu/A Short Hike" declare exclude_ash=() declare game_aermoo="AER Memories of Old" declare include_aermoo="${HOME}/.local/share/Daedalic Entertainment GmbH/Aer" declare exclude_aermoo=("./CLog.log" "config.ini") declare game_aor="Art of Rally" declare include_aor="${HOME}/.config/unity3d/Funselektor Labs/art of rally/Save" declare exclude_aor=() declare game_darwin="Darwinia" declare include_darwin="${HOME}/.darwinia/full/users" declare exclude_darwin=() declare game_emud="Dolphin Emulator" declare include_emud="${HOME}/.local/share/dolphin-emu/GC" declare exclude_emud=() declare game_epistory="Epistory" declare include_epistory="${HOME}/.config/unity3d/Fishing Cactus/Epistory" declare exclude_epistory=() declare game_exap="EXAPUNKS" declare include_exap="${HOME}/.local/share/EXAPUNKS" declare exclude_exap=("*/config.cfg" "*/log.dat") declare game_lcbbs="Last Call BBS" declare include_lcbbs="${HOME}/.local/share/Last Call BBS" declare exclude_lcbbs=("*/config.cfg" "*/log.dat") declare game_oxen="OXENFREE" declare include_oxen="${HOME}/.config/unity3d/Night School Studio/Oxenfree/save" declare exclude_oxen=() declare game_pa="Prison Architect" declare include_pa="${HOME}/.Prison Architect/saves" declare exclude_pa=() declare game_quake="Quake" declare include_quake="${HOME}/.quakespasm" declare exclude_quake=() declare game_q3a="Quake 3 Arena" declare include_q3a="${HOME}/.q3a" declare exclude_q3a=("*/*.pk3") declare game_rtmi="Return to Monkey Island" declare include_rtmi="${HOME}/.local/share/Terrible Toybox/Return to Monkey Island" declare exclude_rtmi=() declare game_sh="Super Hexagon" declare include_sh="${HOME}/.local/share/SuperHexagon" declare exclude_sh=() declare game_wfto="War for the Overworld" declare include_wfto="${HOME}/Games/War for the Overworld/game/WFTOGame_Data/GameData" declare exclude_wfto=("./Options.txt") declare game_yuzu="YUZU" declare include_yuzu="${HOME}/Games/yuzu/files/data/.local/yuzu/nand" declare exclude_yuzu=("*/Contents") print_game_info() { declare game=$1 declare game_def="game_${game}" declare -n include="include_${game}" declare installed_status=$([ -d "${include}" ] && echo "[installed]" || echo "") printf "%-24s\t%-8s\t%s\n" "${!game_def}" "${game}" "${installed_status}" return } list_savegames() { declare game=$1 declare savegames=(`ls "${backup_folder}" | grep "^${game}_" | sort`) declare it for it in ${savegames[@]}; do declare basename=$(basename ${it}) declare filename=${basename%%.*} declare parts=(${filename//_/ }) declare game=${parts[0]} declare date=${parts[1]} declare date_iso="${date:(0):4}-${date:(4):2}-${date:(6):2} ${date:(9):2}:${date:(11):2}:${date:(13):2}" printf "%-24s\t%-8s\t%s\n" "${date_iso}" "${game}" "${date}" done return } backup_savegame() { declare game=$1 declare tag=$2 declare -n include="include_${game}" declare -n exclude="exclude_${game}" # Check if game is installed. if ! [ -d "${include}" ]; then printf "${script}: backup not performed because '$game' is not installed.\n" exit 1 fi printf "Backing up '${game}' '${tag}'..." tar --auto-compress --create --file "${backup_folder}/${game}_${tag}.${backup_extension}" "${exclude[@]/#/--exclude=}" --directory "${include}" . printf " done.\n" return } restore_savegame() { declare game=$1 declare tag=$2 declare -n include="include_${game}" declare -n exclude="exclude_${game}" declare date=`date +"%Y%m%dT%H%M%S"` # Check if game is installed. if ! [ -d "${include}" ]; then printf "${script}: restore not performed because '$game' is not installed.\n" exit 1 fi printf "Restoring '${game}' '${tag}'..." # Backup savegame directory before restoring. declare savegame_backup=$(mktemp --tmpdir=/tmp "savegame.backup.${game}_${date}.XXXXXXXX.${backup_extension}") tar --auto-compress --create --file "${savegame_backup}" "${exclude[@]/#/--exclude=}" --directory "${include}" . # Extract selected savegame. tar --auto-compress --extract --file "${backup_folder}/${game}_${tag}.${backup_extension}" --directory "${include}" printf " done.\n" return } declare script=$(basename $0) declare action=$1 declare -a supported_games=(`compgen -v game_`) supported_games=("${supported_games[@]#game_}") mkdir -p "$backup_folder" case "${action}" in # List available games or backups of game defined in argument. "--list" | "-l") declare game=$2 if [ -z "$game" ]; then for it in ${supported_games[@]}; do print_game_info $it done elif ! [ -v "game_${game}" ]; then # Check if game is undefined. echo "${script}: game '${game}' not found." exit 22 else list_savegames $game fi ;; # List backups for all games. "--list-all" | "-L") # declare -a supported_games=(`compgen -v game_`) declare last_item="${supported_games[-1]#game_}" for it in ${supported_games[@]}; do declare game="${it#game_}" print_game_info $it list_savegames $game if [ "$game" != "$last_item" ]; then printf "\n" fi done ;; # Backup save game. "--backup" | "-b") declare game=$2 if ! [ -v "game_${game}" ]; then # Check if game is undefined. echo "${script}: game '${game}' not found." exit 22 fi declare tag=`date +"%Y%m%dT%H%M%S"` backup_savegame $game $tag ;; # Restore save game. "--restore" | "-r") declare game=$2 declare tag=$3 if ! [ -v "game_${game}" ]; then # Check if game is undefined. echo "${script}: game '${game}' not found." exit 22 fi if [ -z "$tag" ]; then # Check if tag is undefined. # Search for the latest backup. declare savegames=(`ls "${backup_folder}" | grep "^${game}_" | sort`) if [ ${#savegames[@]} -eq 0 ]; then echo "${script}: no backups found." exit 61 fi declare selected="${savegames[-1]}" tag="${selected#${game}_}" # Substring removal: game name from front. tag="${tag%%.*}" # Substring removal: file extension from back. fi restore_savegame $game $tag ;; # Show help. "--help" | "-h") printf -- "Usage: ${script} OPTION [GAME] [TAG]\n" declare options=( "-l, --list [GAME] " "List supported games. Provide GAME to list backup TAGs." "-L, --list-all " "List all backup TAGs." "-b, --backup GAME " "Backup GAME savegame data. Displays new backup TAG." "-r, --restore GAME [TAG] " "Restore GAME savegame data. Omit TAG to use last backup." "-h, --help " "Display this help and exit." "-v, --version " "Output version information and exit." ) for ((idx=0; idx<${#options[@]}; idx+=2)); do printf -- " %s\t%s\n" "${options[$idx]}" "${options[$idx+1]}" done printf "\nNotes\n" printf -- "- Backup folder '%s'.\n" ${backup_folder} printf -- "- Backup extension '%s.'\n" ${backup_extension} ;; # Show version. "--version" | "-v") echo "SaveGame Saver version ${version}" echo "Copyright 2024 Daniel Martins" echo "License GPL-3.0-or-later" ;; # Unknown option. *) echo "${script}: invalid option '${action}'." echo "Try '${script} --help' for more information." exit 22 ;; esac exit 0