Changeset - 57b654290296
[Not reviewed]
0 0 1
Branko Majic (branko) - 4 years ago 2020-06-29 04:23:34
branko@majic.rs
Noticket: Added initial version of Factorio Manager script

Factorio Manager supports:

- Multiple instances of Factorio with separate savegame files,
configurations etc.
- Ability to use multiple versions of Factorio itself.
- Ability to create, copy, remove, and import instances. Import is
limited to importing existing configurations etc from Factorio
installation directory.
- Manage instance backups.

Currently lacks full documentation and needs to be refactored quite a
bit.
1 file changed with 1604 insertions and 0 deletions:
0 comments (0 inline, 0 general)
games/factorio_manager.sh
Show inline comments
 
new file 100755
 
#!/bin/bash
 
#
 
# factorio_manager.sh
 
#
 
# Copyright (C) 2020, Branko Majic <branko@majic.rs>
 
#
 
# 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 <http://www.gnu.org/licenses/>.
 
#
 

	
 
program="factorio_manager.sh"
 
version="0.1"
 

	
 
function synopsis() {
 
cat <<EOF
 
$program $version, helper tool for managing Factorio instances
 

	
 
Usage:
 
  $program [OPTIONS] launch INSTANCE
 
  $program [OPTIONS] info INSTANCE
 
  $program [OPTIONS] list
 

	
 
  $program [OPTIONS] create INSTANCE
 
  $program [OPTIONS] copy SOURCE_INSTANCE DESTINATION_INSTANCE
 
  $program [OPTIONS] remove INSTANCE
 
  $program [OPTIONS] import INSTANCE SOURCE_DIRECTORY
 

	
 
  $program [OPTIONS] versions
 
  $program [OPTIONS] set-version INSTANCE
 

	
 
  $program [OPTIONS] list-backups INSTANCE
 
  $program [OPTIONS] backup INSTANCE [DESCRIPTION]
 
  $program [OPTIONS] restore INSTANCE BACKUP_NAME
 
  $program [OPTIONS] remove-backup INSTANCE BACKUP_NAME
 

	
 
  $program [OPTIONS] set-game-dir GAME_INSTALLATIONS_DIRECTORY
 
EOF
 
}
 

	
 
function short_usage() {
 
cat <<EOF
 
$(synopsis)
 

	
 
For more details see $program -h.
 
EOF
 
}
 

	
 
function usage() {
 
    cat <<EOF
 
$(synopsis)
 

	
 
$program is a helper tool for managing multiple Factorio
 
instances.
 

	
 
Each instance is designated a dedicated directory, which contains all
 
of its configuration files, saves games, and mods, and is kept
 
separate from all other instances.
 

	
 
A single Factorio version installation (base files) can be used by
 
multiple instances, but each instance is bound to a specific version
 
of Factorio.
 

	
 
Factorio Manager keeps instances in sub-directories under the
 
~/.factorio/ directory. Sub-directories are named after the
 
instances. The following instance names are reserved for special use
 
by the tool:
 

	
 
 - .game_installations (used for storing symlink towards directory
 
   containing Factorio installations)
 

	
 
Multiple commands are provided for managing Factorio instances, and
 
they all expect different positional parameters:
 

	
 

	
 
set-game-dir GAME_INSTALLATIONS_DIRECTORY
 

	
 
    Sets the passed-in directory path as the base directory where
 
    different versions of Factorio will be looked-up. Each
 
    sub-directory within this directory should be named after the
 
    version of Factorio it represents, and should be the base
 
    directory under which the Factorio installation files can be
 
    found.
 

	
 

	
 
versions
 

	
 
    Shows locally available Factorio versions.
 

	
 

	
 
list
 

	
 
    Lists available Factorio instances, and shows some basic
 
    information about them.
 

	
 

	
 
create INSTANCE
 

	
 
    Creates a new Factorio instance with the given name. Command will
 
    prompt the user to pick between locally available Factorio
 
    versions.
 

	
 

	
 
launch INSTANCE
 

	
 
    Launches an instance with the given name.
 

	
 
    NOTE:: When launching the instance for the first time, Factorio
 
    will report that its configuration file is invalid, and offer to
 
    fix it. The reason is that the manager creates a minimal
 
    configuration file when creating an instance, and Factorio does
 
    not like this. It should be safe to allow Factorio to fix the
 
    configuration file (this will populate it with the necessary
 
    commented-out options).
 

	
 

	
 
backup INSTANCE [DESCRIPTION]
 

	
 
    Creates backup of an instance. All backups will be stored as
 
    subdirectories under the .bak directory within the instance
 
    directory. An optional description can be passed-in to make it
 
    easier to distinguish between different backups. Hidden files
 
    (names starting with '.') will be omitted from the backup.
 

	
 

	
 
list-backups INSTANCE
 

	
 
    Lists available backups of an instance, including description (if any is set).
 

	
 

	
 
$program accepts the following options:
 

	
 
    -q
 
        Quiet mode. Output a message only if newer packages are available.
 
    -d
 
        Enable debug mode.
 
    -v
 
        Show script licensing information.
 
    -h
 
        Show usage help.
 

	
 

	
 
Please report bugs and send feature requests to <branko@majic.rs>.
 
EOF
 
}
 

	
 
function version() {
 
    cat <<EOF
 
$program, version $version
 

	
 
+-----------------------------------------------------------------------+
 
| Copyright (C) 2020, Branko Majic <branko@majic.rs>                    |
 
|                                                                       |
 
| 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 <http://www.gnu.org/licenses/>. |
 
+-----------------------------------------------------------------------+
 

	
 
EOF
 
}
 

	
 
# Set-up colours for message printing if we're not piping and terminal is
 
# capable of outputting the colors.
 
_color_terminal=$(tput colors 2>&1)
 
if [[ -t 1 ]] && (( ${_color_terminal} > 0 )); then
 
    _text_black=$(tput setaf 0)
 
    _text_red=$(tput setaf 1)
 
    _text_green=$(tput setaf 2)
 
    _text_yellow=$(tput setaf 3)
 
    _text_blue=$(tput setaf 4)
 
    _text_purple=$(tput setaf 5)
 
    _text_cyan=$(tput setaf 6)
 
    _text_white=$(tput setaf 7)
 

	
 
    _text_bold=$(tput bold)
 
    _text_reset=$(tput sgr0)
 

	
 
    _bg_black=$(tput setab 0)
 
    _bg_red=$(tput setab 1)
 
    _bg_green=$(tput setab 2)
 
    _bg_yellow=$(tput setab 3)
 
    _bg_blue=$(tput setab 4)
 
    _bg_purple=$(tput setab 5)
 
    _bg_cyan=$(tput setab 6)
 
    _bg_white=$(tput setab 7)
 
else
 
    _text_black=""
 
    _text_red=""
 
    _text_green=""
 
    _text_yellow=""
 
    _text_blue=""
 
    _text_purple=""
 
    _text_cyan=""
 
    _text_white=""
 

	
 
    _text_bold=""
 
    _text_reset=""
 

	
 
    _bg_black=""
 
    _bg_red=""
 
    _bg_green=""
 
    _bg_yellow=""
 
    _bg_blue=""
 
    _bg_purple=""
 
    _bg_cyan=""
 
    _bg_white=""
 
fi
 

	
 
# Set-up functions for printing coloured messages.
 
function debug() {
 
    if [[ $debug != 0 ]]; then
 
        echo "${_text_bold}${_text_blue}[DEBUG]${_text_reset}" "$@"
 
    fi
 
}
 

	
 
function info() {
 
    echo "${_text_bold}${_text_white}[INFO] ${_text_reset}" "$@"
 
}
 

	
 
function success() {
 
    echo "${_text_bold}${_text_green}[OK]   ${_text_reset}" "$@"
 
}
 

	
 
function warning() {
 
    echo "${_text_bold}${_text_yellow}[WARN] ${_text_reset}" "$@"
 
}
 

	
 
function error() {
 
    echo "${_text_bold}${_text_red}[ERROR]${_text_reset}" "$@" >&2
 
}
 

	
 
function warning_prompt() {
 
    echo -n "${_text_bold}${_text_yellow}[WARN] ${_text_reset}" "$@"
 
}
 

	
 
function bold_green() {
 
    echo -n "${_text_bold}${_text_green}$@${_text_reset}"
 
}
 

	
 
function bold_red() {
 
    echo -n "${_text_bold}${_text_red}$@${_text_reset}"
 
}
 

	
 
function bold_blue() {
 
    echo -n "${_text_bold}${_text_blue}$@${_text_reset}"
 
}
 

	
 
# Define error codes.
 
SUCCESS=0
 
ERROR_ARGUMENTS=1
 
ERROR_CONFIGURATION=2
 
ERROR_GENERAL=3
 

	
 
# Disable debug and quiet modes by default.
 
debug=0
 
quiet=0
 

	
 
# Set-up some default paths.
 
manager_directory="$HOME/.factorio"
 
game_installations_directory="$manager_directory/.game_installations"
 

	
 
# If no arguments were given, just show usage help.
 
if [[ -z $1 ]]; then
 
    short_usage
 
    exit "$SUCCESS"
 
fi
 

	
 
# Parse the arguments
 
while getopts "qdvh" opt; do
 
    case "$opt" in
 
	q) quiet=1;;
 
	d) debug=1;;
 
        v) version
 
           exit "$SUCCESS";;
 
        h) usage
 
           exit "$SUCCESS";;
 
        *) usage
 
           exit "$ERROR_ARGUMENTS";;
 
    esac
 
done
 
i=$OPTIND
 
shift $(($i-1))
 

	
 
# Make sure the manager home directory exists.
 
if [[ ! -e $manager_directory ]]; then
 
    info "Creating Factorio Manager home directory under: $manager_directory"
 
    mkdir -p "$manager_directory"
 
fi
 

	
 
command="$1"
 
shift
 

	
 
#==============#
 
# set-game-dir #
 
#==============#
 
if [[ $command == set-game-dir ]]; then
 

	
 
    # Read and verify additional positional arguments.
 
    game_installations_directory="$1"
 
    shift
 

	
 
    if [[ -z $game_installations_directory ]]; then
 
        error "Missing argument: GAME_INSTALLATIONS_DIRECTORY"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ ! -e $game_installations_directory ]]; then
 
        error "No such directory: $game_installations_directory"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    # Set-up additional derived variables.
 
    symlink="$manager_directory/.game_installations"
 

	
 
    # Verify that at least one Factorio instance can be found.
 
    factorio_versions_found=0
 
    for candidate in "$game_installations_directory"/*; do
 
        if [[ -f $candidate/bin/x64/factorio ]]; then
 
            let factorio_versions_found++
 
        fi
 
    done
 

	
 
    if (( $factorio_versions_found == 0 )); then
 
        error "Could not locate any Factorio installations under: $game_installations_directory"
 

	
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    # Update the link
 
    if [[ -L $symlink ]]; then
 
        rm "$symlink"
 
    fi
 

	
 
    if ! ln -s "$game_installations_directory" "$symlink"; then
 
        error "Could not create symlink from $game_installations_directory to $symlink."
 

	
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
#==========#
 
# versions #
 
#==========#
 
elif [[ $command == versions ]]; then
 

	
 
    # Make sure user has set directory with game installations - test
 
    # both symlink and target destination.
 
    if [[ ! -L $game_installations_directory ]]; then
 
        error "Game installations directory has not been properly set. Please run the set-game-dir command first."
 
        exit "$ERROR_CONFIGURATION"
 
    fi
 

	
 
    if [[ ! -d $game_installations_directory ]]; then
 
        error "Game installations directory has not been properly set. Please run the set-game-dir command first."
 
        exit "$ERROR_CONFIGURATION"
 
    fi
 

	
 
    echo "Locally available Factorio versions:"
 
    echo
 

	
 
    # Find all sub-directories that are Factorio installations.
 
    for candidate in "$game_installations_directory"/*; do
 
        if [[ -d $candidate && -e $candidate/bin/x64/factorio ]]; then
 
            echo "  - $(basename "$candidate")"
 
        fi
 
    done
 
    echo
 

	
 
#======#
 
# list #
 
#======#
 
elif [[ $command == list ]]; then
 
    echo "Available instances:"
 
    echo
 

	
 
    # Find all sub-directories that are valid instances.
 
    for candidate in "$manager_directory"/*; do
 
        if [[ -f $candidate/instance.conf ]]; then
 
            source "$candidate/instance.conf"
 
            echo "  - $(basename "$candidate") ($game_version)"
 
        fi
 
    done
 
    echo
 

	
 
#========#
 
# create #
 
#========#
 
elif [[ $command == create ]]; then
 
    # Make sure user has set directory with game installations - test
 
    # both symlink and target destination.
 
    if [[ ! -L $game_installations_directory ]]; then
 
        error "Game installations directory has not been properly set. Please run the set-game-dir command first."
 
        exit "$ERROR_CONFIGURATION"
 
    fi
 

	
 
    if [[ ! -d $game_installations_directory ]]; then
 
        error "Game installations directory has not been properly set. Please run the set-game-dir command first."
 
        exit "$ERROR_CONFIGURATION"
 
    fi
 

	
 
    # Read positional arguments.
 
    instance="$1"
 
    shift
 

	
 
    # Calculate derived variables.
 
    instance_directory="$manager_directory/$instance"
 
    instance_config="$instance_directory/instance.conf"
 
    game_config="$instance_directory/config.ini"
 

	
 
    # Verify arguments.
 
    if [[ -z $instance ]]; then
 
        error "Missing argument: INSTANCE"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ -e $instance_directory ]]; then
 
        error "Instance already exists."
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    # Display list of available Factorio versions and let user pick one.
 
    echo "The following versions of Factorio are locally available:"
 
    echo
 

	
 
    for candidate in "$game_installations_directory"/*; do
 
        if [[ -f $candidate/bin/x64/factorio ]]; then
 
            echo "  - $(basename "$candidate")"
 
        fi
 
    done
 

	
 
    echo
 

	
 
    echo -n "Please specify what version you would like to use: "
 
    read game_version_selected
 

	
 
    # Validate user input.
 
    if [[ ! -f "$game_installations_directory/$game_version_selected/bin/x64/factorio" ]]; then
 
        error "Requested version not locally available: $game_version_selected"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    # Set-up the instance.
 
    mkdir "$instance_directory"
 
    mkdir "$instance_directory/mods"
 
    echo "{}" > "$instance_directory/mods/mod-list.json"
 

	
 
    cat <<EOF > "$instance_config"
 
game_version="$game_version_selected"
 
EOF
 

	
 
    # @TODO: If we could somehow obtain stock config.ini file without
 
    # having to first run Factorio, that would be great. As it is, the
 
    # user will presented with warning about corrupt config.ini when
 
    # running the instance for the first time.
 
    cat <<EOF > "$game_config"
 
[path]
 
read-data=__PATH__executable__/../../data
 
write-data=${instance_dir}
 

	
 
[general]
 
locale=
 

	
 
[other]
 
check-updates=false
 
enable-crash-log-uploading=false
 

	
 
[interface]
 

	
 
[controls]
 

	
 
[sound]
 

	
 
[map-view]
 

	
 
[debug]
 

	
 
[multiplayer-lobby]
 

	
 
[graphics]
 
EOF
 

	
 
    echo
 
    warning "Factorio Manager has created a minimal empty configuration file for Factorio under $game_config."
 
    warning "Since the generated configuration file is almost empty, Factorio will complain that the file seems corrupt."
 
    warning "Factorio will offer to fix the corrupted configuration file by filling-in the missing information during the first startup."
 
    warning "It should be safe to accept this. This warning will be shown by Factorio only the first time."
 
    echo
 
    echo "Please read the warning above, and press any key to continue."
 
    read
 

	
 
elif [[ $command == launch ]]; then
 

	
 
    # Read positional arguments.
 
    instance="$1"
 
    shift
 

	
 
    # Set-up derived variables.
 
    instance_directory="$manager_directory/$instance"
 
    instance_config="$instance_directory/instance.conf"
 
    game_config="$instance_directory/config.ini"
 

	
 
    # Verify arguments.
 
    if [[ -z $instance ]]; then
 
        error "Missing argument: INSTANCE"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ ! -f $instance_config ]]; then
 
        error "Missing instance configuration file: $instance_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    if [[ ! -f $game_config ]]; then
 
        error "Missing game configuration file: $game_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    # Update instance write directory prior to launching - this is a
 
    # failsafe in case it got changed by hand through some other means
 
    # (or maybe it was copied over from another instance).
 
    current_write_data=$(grep "^write-data=" "$game_config")
 
    expected_write_data="write-data=${instance_directory}"
 

	
 
    if [[ $current_write_data != $expected_write_data ]]; then
 
        warning "Incorrect path specified for write-data in game configuration file: $game_config"
 
        warning "Current configuration is: $current_write_data"
 
        warning "Configuration will be replaced with: $expected_write_data"
 
        sed -i -e "s#^write-data=.*#$expected_write_data#" "$game_config"
 
    fi
 

	
 
    # Read launcher configuration for the instance.
 
    source "$instance_config"
 

	
 
    if [[ -z $game_version ]]; then
 
        error "Missing game version information in $game_config."
 
        exit "$ERROR_CONFIGURATION"
 
    fi
 

	
 
    # Set-up paths for launching the game, and ensure they still exist
 
    # (versions can be removed by user).
 
    game_directory="${game_installations_directory}/${game_version}"
 
    factorio_bin="$game_directory/bin/x64/factorio"
 

	
 
    if [[ ! -e $factorio_bin ]]; then
 
        error "Could not locate Factorio binary under: $factorio_bin"
 
        error "Factorio $game_version installation may have been removed from game installations directory:"
 
        error "   $(readlink -f "$game_installations_directory")"
 
        exit "$ERROR_CONFIGURATION"
 
    fi
 

	
 
    # Launch instance
 
    "$factorio_bin" --config "$game_config"
 

	
 
elif [[ $command == backup ]]; then
 
    # Read positional arguments.
 
    instance="$1"
 
    description="$2"
 
    shift 2
 

	
 
    # Use timestamp-based names for backups.
 
    timestamp=$(date +%Y-%m-%d_%H:%M:%S)
 

	
 
    # Set-up derived variables.
 
    instance_directory="$manager_directory/$instance"
 
    instance_config="$instance_directory/instance.conf"
 
    game_config="$instance_directory/config.ini"
 
    lock_file="$instance_directory/.lock"
 

	
 
    backup_directory="$instance_directory/.bak"
 
    backup_destination="$backup_directory/$timestamp"
 
    backup_description="$backup_destination/.description"
 

	
 
    # Verify positional arguments.
 
    if [[ -z $instance ]]; then
 
        error "Missing argument: INSTANCE"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    # Verify that we are trying to backup an actual instance.
 
    if [[ ! -f $instance_config ]]; then
 
        error "Missing instance configuration file: $instance_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    if [[ ! -f $game_config ]]; then
 
        error "Missing game configuration file: $game_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    # Make sure we do not overwrite the backup destination by mistake
 
    # - although highly unlikely unless user spams the backup command.
 
    if [[ -e $backup_destination ]]; then
 
        error "Backup already exists: $backup_destination"
 
    fi
 

	
 
    (
 

	
 
        # Obtain lock - Factorio uses the same mechanism, so we should
 
        # be able to detect the game is running in this way.
 
        flock --exclusive --nonblock 200
 
        if [[ $? != 0 ]]; then
 
            error "Could not lock instance directory via lock file $lock_file. Is Factorio instance still running?"
 
            exit "$ERROR_GENERAL"
 
        fi
 

	
 
        function remove_lock() {
 
            rm "$lock_file"
 
        }
 

	
 
        trap remove_lock EXIT
 

	
 
        # Backup the instance. Clean-up the backup destination in case of failure.
 
        mkdir -p "$backup_destination"
 
        if ! cp -a "$instance_directory"/* "$backup_destination"; then
 
            error "Could not create backup under: $backup_destination"
 
            rm -rf "$backup_destination"
 
            exit "$ERROR_GENERAL"
 
        fi
 

	
 
        # Store (optional) description.
 
        if [[ -n $description ]]; then
 
            echo "$description" > "$backup_description"
 
        fi
 

	
 
        success "Backup saved to: $backup_destination"
 

	
 
    ) 200>"$lock_file"
 

	
 
#==============#
 
# list-backups #
 
#==============#
 
elif [[ $command == list-backups ]]; then
 

	
 
    # Read positional arguments.
 
    instance="$1"
 
    shift
 

	
 
    # Set-up derived variables.
 
    instance_directory="$manager_directory/$instance"
 
    instance_config="$instance_directory/instance.conf"
 
    game_config="$instance_directory/config.ini"
 
    backup_directory="$instance_directory/.bak"
 

	
 
    # Verify positional arguments.
 
    if [[ -z $instance ]]; then
 
        error "Missing argument: INSTANCE"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    # Verify instance.
 
    if [[ ! -d $instance_directory ]]; then
 
        error "No such instance: $instance"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ ! -f $instance_config ]]; then
 
        error "Missing instance configuration file: $instance_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    if [[ ! -f $game_config ]]; then
 
        error "Missing game configuration file: $game_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    # Extended regular expression for matching YYYY-MM-DD-hh:mm:ss format.
 
    backup_destination_name_pattern="[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}_[[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}"
 

	
 
    # Read current game version.
 
    source "$instance_config"
 
    current_game_version="$game_version"
 
    unset game_version
 

	
 
    # Detect and show available backups to the user.
 
    if [[ ! -d $backup_directory ]] || ! ls "$backup_directory" | grep -q -E "^$backup_destination_name_pattern\$"; then
 
        echo "No backups are available for instance $(bold_green $instance)."
 
    else
 
        echo "Available backups for instance $(bold_green $instance) (current version $(bold_green "$current_game_version"))."
 
        echo
 

	
 
        for backup_destination in "$backup_directory"/*; do
 

	
 
            if [[ $backup_destination =~ $backup_destination_name_pattern ]]; then
 
                backup_date=$(basename "$backup_destination")
 

	
 
                # Read instance configuration for backup.
 
                source "$backup_destination/instance.conf"
 

	
 
                if [[ -f "$backup_destination/.description" ]]; then
 
                    backup_description=$(<"$backup_destination/.description")
 
                    echo "  - $backup_date, version $(bold_green "$game_version") ($backup_description)"
 
                else
 
                    echo "  - $backup_date, version $(bold_green "$game_version")"
 
                fi
 

	
 

	
 
            fi
 
        done
 

	
 
        echo
 

	
 
    fi
 

	
 
#=========#
 
# restore #
 
#=========#
 
elif [[ $command == restore ]]; then
 

	
 
    # Read positional arguments.
 
    instance="$1"
 
    backup_name="$2"
 
    shift 2
 

	
 
    # Set-up derived values.
 
    instance_directory="$manager_directory/$instance"
 
    restore_source="$instance_directory/.bak/$backup_name"
 
    backup_instance_config="$restore_source/instance.conf"
 
    backup_game_config="$restore_source/config.ini"
 
    lock_file="$instance_directory/.lock"
 

	
 
    # Verify positional arguments.
 
    if [[ -z $instance ]]; then
 
        error "Missing argument: INSTANCE"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ -z $backup_name ]]; then
 
        error "Missing argument: BACKUP_NAME"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    # Verify we are working with legitimate instance and backup.
 
    if [[ ! -d $instance_directory ]]; then
 
        error "No such instance: $instance"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ ! -d $restore_source ]]; then
 
        error "Specified backup not available under: $restore_source"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ ! -f $backup_instance_config ]]; then
 
        error "Invalid backup, missing instance configuration file: $backup_instance_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    if [[ ! -f $backup_game_config ]]; then
 
        error "Invalid backup, missing game configuration file: $backup_game_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    (
 
        # Obtain lock - Factorio uses the same mechanism, so we should
 
        # be able to detect the game is running in this way.
 
        flock --exclusive --nonblock 200
 
        if [[ $? != 0 ]]; then
 
            error "Could not lock instance directory via lock file $lock_file. Is Factorio instance still running?"
 
            exit "$ERROR_GENERAL"
 
        fi
 

	
 
        function remove_lock() {
 
            rm "$lock_file"
 
        }
 
        trap remove_lock EXIT
 

	
 
        # Set-up a list of files and directories that will get removed.
 
        shopt -s nullglob
 
        entries_to_remove=($instance_directory/*)
 
        shopt -u nullglob
 

	
 
        # Present user with extensive warning on consequences.
 
        echo
 
        warning "You are about to replace current instance's files, including (but not limited to):"
 
        warning
 
        warning "  - configuration files"
 
        warning "  - blueprints"
 
        warning "  - savegames"
 
        warning "  - achievements"
 
        warning "  - mods"
 
        warning
 
        warning "All instance files will be replaced by files from specified backup."
 

	
 
        # Display backup information.
 
        echo
 
        if [[ -f "$restore_source/.description" ]]; then
 
            backup_description=$(<"$restore_source/.description")
 
            backup_info="$backup_name ($backup_description)"
 
        else
 
            backup_info="$backup_name"
 
        fi
 
        echo "Restore instance $(bold_green "$instance") from backup: $(bold_green "$backup_info")"
 
        echo
 

	
 
        # Show user what files will be removed.
 
        if [[ ${#entries_to_remove[@]} == 0 ]]; then
 
            echo "Instance directory is currently empty. No files will be removed."
 
            echo
 
        else
 
            echo "Files and directories that will be removed:"
 
            echo
 
            for entry in "${entries_to_remove[@]}"; do
 
                echo "  - $entry"
 
            done
 
            echo
 
        fi
 

	
 
        # Request from user to confirm the operation.
 
        warning_prompt "Are you sure you want to proceed? Type YES to confirm (default is no): "
 
        read confirm
 

	
 
        if [[ $confirm != "YES" ]]; then
 
            error "Aborted restore process, no changes have been made to instance files."
 
            exit "$ERROR_GENERAL"
 
        fi
 

	
 
        if [[ ${#entries_to_remove[@]} != 0 ]]; then
 
            if ! rm -rf "${entries_to_remove[@]}"; then
 
                error "Failed to remove existing instance files."
 
                exit "$ERROR_GENERAL"
 
            fi
 
        fi
 

	
 
        if ! cp -a "$restore_source"/* "$instance_directory"; then
 
            error "Failed to restore backup from: $restore_source"
 
            exit "$ERROR_GENERAL"
 
        fi
 

	
 
        success "Instance restored from backup."
 

	
 
    ) 200>"$lock_file"
 

	
 
#===============#
 
# remove-backup #
 
#===============#
 
elif [[ $command == remove-backup ]]; then
 

	
 
    # Read positional arguments.
 
    instance="$1"
 
    backup_name="$2"
 
    shift 2
 

	
 
    # Verify positional arguments.
 
    if [[ -z $instance ]]; then
 
        error "Missing argument: INSTANCE"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ -z $backup_name ]]; then
 
        error "Missing argument: BACKUP_NAME"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    # Set-up derived values.
 
    instance_directory="$manager_directory/$instance"
 
    removal_target="$instance_directory/.bak/$backup_name"
 
    backup_instance_config="$removal_target/instance.conf"
 
    backup_game_config="$removal_target/config.ini"
 
    lock_file="$instance_directory/.lock"
 

	
 
    # Verify we are working with legitimate instance and backup.
 
    if [[ ! -d $instance_directory ]]; then
 
        error "No such instance: $instance"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ ! -d $removal_target ]]; then
 
        error "Specified backup not available under: $removal_target"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ ! -f $backup_instance_config ]]; then
 
        error "Invalid backup, missing instance configuration file: $backup_instance_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    if [[ ! -f $backup_game_config ]]; then
 
        error "Invalid backup, missing game configuration file: $backup_game_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    # Present user with warning.
 
    echo
 
    if [[ -f "$removal_target/.description" ]]; then
 
        backup_description=$(<"$removal_target/.description")
 
        backup_info="$backup_name ($backup_description)"
 
    else
 
        backup_info="$backup_name"
 
    fi
 

	
 
    warning "You are about to remove an instance backup. All files belonging to specified backup will be removed."
 
    echo
 
    echo "Instance: $(bold_green "$instance")"
 
    echo
 
    echo "Backup: $(bold_green "$backup_info")"
 
    echo
 
    echo "Files and directories that will be removed:"
 
    echo
 
    echo " - $removal_target"
 
    echo
 

	
 
    # Request from user to confirm the operation.
 
    warning_prompt "Are you sure you want to proceed? Type YES to confirm (default is no): "
 
    read confirm
 

	
 
    if [[ $confirm != "YES" ]]; then
 
        error "Aborted backup removal, no changes have been made to backup files."
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    if ! rm -rf "$removal_target"; then
 
        error "Failed to remove existing instance files."
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    success "Backup removed."
 

	
 
#=============#
 
# set-version #
 
#=============#
 
elif [[ $command == set-version ]]; then
 
    # Read positional arguments.
 
    instance="$1"
 
    shift
 

	
 
    # Verify positional arguments.
 
    if [[ -z $instance ]]; then
 
        error "Missing argument: INSTANCE"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    # Make sure user has set directory with game installations - test
 
    # both symlink and target destination.
 
    if [[ ! -L $game_installations_directory ]]; then
 
        error "Game installations directory has not been properly set. Please run the set-game-dir command first."
 
        exit "$ERROR_CONFIGURATION"
 
    fi
 

	
 
    if [[ ! -d $game_installations_directory ]]; then
 
        error "Game installations directory has not been properly set. Please run the set-game-dir command first."
 
        exit "$ERROR_CONFIGURATION"
 
    fi
 

	
 
    # Set-up derived variables.
 
    instance_directory="$manager_directory/$instance"
 
    instance_config="$instance_directory/instance.conf"
 
    game_config="$instance_directory/config.ini"
 

	
 
    # Verify we are working with legitimate instance.
 
    if [[ ! -f $instance_config ]]; then
 
        error "Missing instance configuration file: $instance_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    if [[ ! -f $game_config ]]; then
 
        error "Missing game configuration file: $game_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    # Load instance configuration.
 
    source "$instance_config"
 

	
 
    # Display list of available Factorio versions.
 
    echo
 
    echo "The following versions of Factorio are locally available:"
 
    echo
 

	
 
    for candidate in "$game_installations_directory"/*; do
 
        if [[ -f $candidate/bin/x64/factorio ]]; then
 
            candidate_version=$(basename "$candidate")
 
            if [[ $candidate_version == $game_version ]]; then
 
                echo "  - $candidate_version $(bold_green "[current]")"
 
            else
 
                echo "  - $candidate_version"
 
            fi
 

	
 
        fi
 
    done
 
    echo
 

	
 
    # Display current version.
 
    echo "Current version used for instance $(bold_green "$instance") is $(bold_green "$game_version")."
 
    echo
 
    read -p "Please specify what version you would like to use (enter to keep current): " game_version_selected
 

	
 
    if [[ -z $game_version_selected ]]; then
 
        game_version_selected="$game_version"
 
    fi
 

	
 
    # Validate user input.
 
    if [[ ! -f "$game_installations_directory/$game_version_selected/bin/x64/factorio" ]]; then
 
        error "Requested version not locally available: $game_version_selected"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    # Change instance game version.
 
    if [[ $game_version_selected == $game_version ]]; then
 
        info "Current version has been kept."
 
    else
 
        sed -i -e "s/^game_version=.*/game_version=$game_version_selected/" "$instance_config"
 
        success "Version changed to: $(bold_green "$game_version_selected")"
 
    fi
 

	
 
#======#
 
# info #
 
#======#
 
elif [[ $command == info ]]; then
 
    # Make sure user has set directory with game installations - test
 
    # both symlink and target destination.
 
    if [[ ! -L $game_installations_directory ]]; then
 
        error "Game installations directory has not been properly set. Please run the set-game-dir command first."
 
        exit "$ERROR_CONFIGURATION"
 
    fi
 

	
 
    if [[ ! -d $game_installations_directory ]]; then
 
        error "Game installations directory has not been properly set. Please run the set-game-dir command first."
 
        exit "$ERROR_CONFIGURATION"
 
    fi
 

	
 
    # Read positional arguments.
 
    instance="$1"
 
    shift
 

	
 
    # Verify positional arguments.
 
    if [[ -z $instance ]]; then
 
        error "Missing argument: INSTANCE"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    # Set-up derived variables.
 
    instance_directory="$manager_directory/$instance"
 
    instance_config="$instance_directory/instance.conf"
 
    game_config="$instance_directory/config.ini"
 

	
 
    # Verify we are working with legitimate instance.
 
    if [[ ! -f $instance_config ]]; then
 
        error "Missing instance configuration file: $instance_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    if [[ ! -f $game_config ]]; then
 
        error "Missing game configuration file: $game_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    # Load instance configuration.
 
    source "$instance_config"
 

	
 
    # Basic information.
 
    echo "Instance name: $(bold_green "$instance")"
 

	
 
    if [[ -e "$game_installations_directory/$game_version" ]]; then
 
        echo "Game version: $(bold_green "$game_version")"
 
    else
 
        echo "Game version: $(bold_red "$game_version [not available locally]")"
 
    fi
 

	
 
    echo "Instance path: $(bold_green "$instance_directory")"
 
    echo
 

	
 
    # Mod information.
 
    shopt -s nullglob
 
    mod_files=($instance_directory/mods/*.zip)
 
    mod_list="$instance_directory/mods/mod-list.json"
 
    shopt -u nullglob
 

	
 
    if [[ ${#mod_files[@]} == 0 ]]; then
 
        echo "Available mods: none"
 

	
 
    # Plain listing.
 
    elif ! hash jqs 2>/dev/null; then
 
        echo "Available mods:"
 
        echo
 

	
 
        # Determine if mod is enabled by default or not when added to
 
        # mods directory (and is not yet listed in the mod list file).
 
        if grep -i -q "^[[:blank:]]*enable-new-mods=false" "$game_config"; then
 
            enable_new_mods="false"
 
        else
 
            enable_new_mods="true"
 
        fi
 

	
 
        # Process every mod found.
 
        for mod_file in "${mod_files[@]}"; do
 

	
 
            if [[ -f $mod_file ]]; then
 
                # Extract basic information about mod from the filename.
 
                mod_basename=$(basename "$mod_file" .zip)
 
                mod_name="${mod_basename%_*}"
 
                mod_version="${mod_basename##*_}"
 

	
 
                # Determine if mod is enabled or not.
 

	
 
                if grep -A1 "\"name\": \"$mod_name\"" "$mod_list" | tail -n1 | grep -q '"enabled": false'; then
 
                    color="$_text_yellow"
 
                elif grep -A1 "\"name\": \"$mod_name\"" "$mod_list" | tail -n1 | grep -q '"enabled": true'; then
 
                    color=""
 
                elif [[ $enable_new_mods == "false" ]]; then
 
                    color="$_text_yellow"
 
                else
 
                    color=""
 
                fi
 

	
 
                # Show colored-information for the mod.
 
                printf "$color  - %-48s %8s $_text_reset\n" "$mod_name" "$mod_version"
 
            fi
 

	
 
        done
 

	
 
    # Fancy listing with detection for enabled mods using jq.
 
    elif hash jq 2>/dev/null; then
 
        echo "Available mods (enabled/${_text_yellow}disabled${_text_reset}):"
 
        echo
 

	
 
        # Determine if mod is enabled by default or not when added to
 
        # mods directory (and is not yet listed in the mod list file).
 
        if grep -i -q "^[[:blank:]]*enable-new-mods=false" "$game_config"; then
 
            enable_new_mods="false"
 
        else
 
            enable_new_mods="true"
 
        fi
 

	
 
        # Query string used in jq tool to determine if mod is enabled
 
        # or not. Take note that this string should remain
 
        # single-quoted, and that $ expansions are actually done
 
        # internally in jq itself. Query accepts two vars - mod_name
 
        # and enable_new_mods.
 
        jq_is_enabled_query='.mods | map(select(.name==$mod_name))[0] // {"name": "default", "enabled": $enable_new_mods} | .enabled'
 

	
 
        # Process every mod found.
 
        for mod_file in "${mod_files[@]}"; do
 

	
 
            if [[ -f $mod_file ]]; then
 

	
 
                # Extract basic information about mod from the filename.
 
                mod_basename=$(basename "$mod_file" .zip)
 
                mod_name="${mod_basename%_*}"
 
                mod_version="${mod_basename##*_}"
 

	
 
                # Determine if mod is enabled or not.
 
                if jq -e \
 
                      --arg "mod_name" "$mod_name" \
 
                      --argjson "enable_new_mods" "$enable_new_mods" \
 
                      "$jq_is_enabled_query" "$mod_list" > /dev/null; then
 
                    color=""
 
                else
 
                    color="$_text_yellow"
 
                fi
 

	
 
                # Show colored-information for the mod.
 
                printf "$color  - %-48s %8s $_text_reset\n" "$mod_name" "$mod_version"
 
            fi
 
        done
 
    fi
 
    echo
 

	
 
    # Call self for displaying list of backups. Better than duplicating code.
 
    "$program" list-backups testing | sed -e "s/^Available backups for instance.*/Available backups:/"
 

	
 

	
 
#========#
 
# remove #
 
#========#
 
elif [[ $command == remove ]]; then
 

	
 
    # Read positional arguments.
 
    instance="$1"
 
    shift
 

	
 
    # Verify positional arguments.
 
    if [[ -z $instance ]]; then
 
        error "Missing argument: INSTANCE"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    # Set-up derived values.
 
    instance_directory="$manager_directory/$instance"
 
    instance_config="$instance_directory/instance.conf"
 
    lock_file="$instance_directory/.lock"
 

	
 
    # Verify we are working with legitimate instance and backup.
 
    if [[ ! -d $instance_directory ]]; then
 
        error "No such instance: $instance"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ ! -f $instance_config ]]; then
 
        error "Missing instance configuration file: $instance_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    (
 
        # Obtain lock - Factorio uses the same mechanism, so we should
 
        # be able to detect the game is running in this way.
 
        flock --exclusive --nonblock 200
 
        if [[ $? != 0 ]]; then
 
            error "Could not lock instance directory via lock file $lock_file. Is Factorio instance still running?"
 
            exit "$ERROR_GENERAL"
 
        fi
 

	
 
        # Removing lock file will most likely fail if we manage to
 
        # remove the instance.
 
        function remove_lock() {
 
            rm -f "$lock_file"
 
        }
 
        trap remove_lock EXIT
 

	
 
        # Set-up a list of files and directories that will get removed.
 
        shopt -s nullglob dotglob
 
        entries_to_remove=($instance_directory/*)
 
        shopt -u nullglob dotglob
 

	
 
        # Present user with extensive warning on consequences.
 
        echo
 
        warning "You are about to remove all instance's files, including (but not limited to):"
 
        warning
 
        warning "  - configuration files"
 
        warning "  - blueprints"
 
        warning "  - savegames"
 
        warning "  - achievements"
 
        warning "  - mods"
 
        warning "  - backups"
 
        warning
 
        warning "All instance files will be removed."
 
        echo
 

	
 
        # Show user what files will be removed.
 
        if [[ ${#entries_to_remove[@]} == 0 ]]; then
 
            echo "Instance directory is currently empty. No files will be removed."
 
            echo
 
        else
 
            echo "Files and directories that will be removed:"
 
            echo
 
            echo "  - $instance_directory"
 
            for entry in "${entries_to_remove[@]}"; do
 
                echo "  - $entry"
 
            done
 
            echo
 
        fi
 

	
 
        # Display instance information.
 
        source "$instance_config"
 
        echo "Instance name:    $(bold_green "$instance")"
 
        echo "Instance version: $(bold_green "$game_version")"
 
        echo
 

	
 
        # Request from user to confirm the operation.
 
        warning_prompt "Are you sure you want to proceed? Type YES to confirm (default is no): "
 
        read confirm
 

	
 
        if [[ $confirm != "YES" ]]; then
 
            error "Aborted instance removal, no changes have been made to instance files."
 
            exit "$ERROR_GENERAL"
 
        fi
 

	
 
        if [[ ${#entries_to_remove[@]} != 0 ]]; then
 
            if ! rm -rf "${entries_to_remove[@]}"; then
 
                error "Failed to remove instance files."
 
                exit "$ERROR_GENERAL"
 
            fi
 
        fi
 

	
 
        if ! rmdir "$instance_directory"; then
 
            error "Failed to remove instance files."
 
            exit "$ERROR_GENERAL"
 
        fi
 

	
 
        success "Instance removed."
 

	
 
    ) 200>"$lock_file"
 

	
 

	
 
#======#
 
# copy #
 
#======#
 
elif [[ $command == copy ]]; then
 

	
 
    # Make sure user has set directory with game installations - test
 
    # both symlink and target destination.
 
    if [[ ! -L $game_installations_directory ]]; then
 
        error "Game installations directory has not been properly set. Please run the set-game-dir command first."
 
        exit "$ERROR_CONFIGURATION"
 
    fi
 

	
 
    if [[ ! -d $game_installations_directory ]]; then
 
        error "Game installations directory has not been properly set. Please run the set-game-dir command first."
 
        exit "$ERROR_CONFIGURATION"
 
    fi
 

	
 
    # Read positional arguments.
 
    source_instance="$1"
 
    destination_instance="$2"
 
    shift 2
 

	
 
    # Verify positional arguments.
 
    if [[ -z $source_instance ]]; then
 
        error "Missing argument: SOURCE_INSTANCE"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ -z $destination_instance ]]; then
 
        error "Missing argument: DESTINATION_INSTANCE"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    # Calculate derived variables.
 
    source_instance_directory="$manager_directory/$source_instance"
 
    source_instance_config="$source_instance_directory/instance.conf"
 
    source_game_config="$source_instance_directory/config.ini"
 
    source_lock_file="$source_instance_directory/.lock"
 

	
 
    destination_instance_directory="$manager_directory/$destination_instance"
 
    destination_instance_config="$destination_instance_directory/instance.conf"
 

	
 
    # Verify we are working with legitimate source and destination.
 
    if [[ ! -d $source_instance_directory ]]; then
 
        error "No such instance: $source_instance"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ ! -f $source_instance_config ]]; then
 
        error "Missing instance configuration file: $source_instance_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    if [[ ! -f $source_game_config ]]; then
 
        error "Missing game configuration file: $source_game_config"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    if [[ -e $destination_instance_directory ]]; then
 
        error "Instance already exists."
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    (
 
        # Obtain lock - Factorio uses the same mechanism, so we should
 
        # be able to detect the game is running in this way.
 
        flock --exclusive --nonblock 200
 
        if [[ $? != 0 ]]; then
 
            error "Could not lock instance directory via lock file $lock_file. Is Factorio instance still running?"
 
            exit "$ERROR_GENERAL"
 
        fi
 

	
 
        function remove_lock() {
 
            rm "$source_lock_file"
 
        }
 

	
 
        trap remove_lock EXIT
 

	
 
        # Load source instance configuration.
 
        source "$source_instance_config"
 

	
 
        # Display list of available Factorio versions.
 
        echo
 
        echo "If you wish to, you can now change version of Factorio used for destination instance, or keep the same version as for source instance."
 
        echo
 
        echo "The following versions of Factorio are locally available:"
 
        echo
 

	
 
        for candidate in "$game_installations_directory"/*; do
 
            if [[ -f $candidate/bin/x64/factorio ]]; then
 
                candidate_version=$(basename "$candidate")
 
                if [[ $candidate_version == $game_version ]]; then
 
                    echo "  - $candidate_version $(bold_green "[current]")"
 
                else
 
                    echo "  - $candidate_version"
 
                fi
 
            fi
 
        done
 
        echo
 

	
 
        # Display current version.
 
        echo "Current version used for instance $(bold_green "$source_instance") is $(bold_green "$game_version")."
 
        echo
 
        read -p "Please specify what version you would like to use for new instance (press enter to keep the current version): " game_version_selected
 

	
 
        if [[ -z $game_version_selected ]]; then
 
            game_version_selected="$game_version"
 
        fi
 

	
 
        # Validate user input.
 
        if [[ ! -f "$game_installations_directory/$game_version_selected/bin/x64/factorio" ]]; then
 
            error "Requested version not locally available: $game_version_selected"
 
            exit "$ERROR_ARGUMENTS"
 
        fi
 

	
 
        # Check if user wants to copy backup files as well.
 
        copy_backups=""
 

	
 
        until [[ $copy_backups == "y" || $copy_backups == "n" ]]; do
 
            echo
 
            read -n1 -p "Would you like to copy backup files as well? (y/n)" copy_backups
 
            echo
 
            copy_backups="${copy_backups,,}"
 

	
 
            if [[ $copy_backups != "y" && $copy_backups != "n" ]]; then
 
                echo
 
                error "Please answer only with 'y' or 'n'."
 
            fi
 
        done
 

	
 
        # Set-up a list of files and directories to copy.
 
        entries_to_copy=($source_instance_directory/*)
 

	
 
        if [[ $copy_backups == "y" && -e "$source_instance_directory/.bak" ]]; then
 
             entries_to_copy+=("$source_instance_directory/.bak")
 
        fi
 

	
 
        # Create copy of source instance.
 
        mkdir "$destination_instance_directory"
 
        cp -a "${entries_to_copy[@]}" "$destination_instance_directory/"
 

	
 
        # Update write-data directory of destination instance,
 
        # including the backups.
 
        write_data="write-data=${destination_instance_directory}"
 
        find "$destination_instance_directory/" -type f -name config.ini -exec \
 
             sed -i -e "s#^write-data=.*#$write_data#" '{}' \;
 

	
 
        sed -i -e "s/^game_version=.*/game_version=$game_version_selected/" "$destination_instance_config"
 

	
 
        success "Created new instance $(bold_green "$destination_instance") using version $(bold_green "$game_version_selected") as copy of instance $(bold_green "$source_instance")."
 

	
 
    ) 200>"$source_lock_file"
 

	
 
#========#
 
# import #
 
#========#
 
elif [[ $command == import ]]; then
 

	
 
    # Make sure user has set directory with game installations - test
 
    # both symlink and target destination.
 
    if [[ ! -L $game_installations_directory ]]; then
 
        error "Game installations directory has not been properly set. Please run the set-game-dir command first."
 
        exit "$ERROR_CONFIGURATION"
 
    fi
 

	
 
    if [[ ! -d $game_installations_directory ]]; then
 
        error "Game installations directory has not been properly set. Please run the set-game-dir command first."
 
        exit "$ERROR_CONFIGURATION"
 
    fi
 

	
 
    # Read positional arguments.
 
    instance="$1"
 
    source_directory="$2"
 
    shift 2
 

	
 
    # Verify positional arguments.
 
    if [[ -z $instance ]]; then
 
        error "Missing argument: INSTANCE"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ -z $source_directory ]]; then
 
        error "Missing argument: SOURCE_DIRECTORY"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    # Calculate derived variables.
 
    instance_directory="$manager_directory/$instance"
 
    instance_config="$instance_directory/instance.conf"
 
    game_config="$instance_directory/config.ini"
 

	
 
    source_config="$source_directory/config/config.ini"
 
    source_lock_file="$source_directory/.lock"
 

	
 
    # List of entries to import from the source directory.
 
    declare -A import_entries
 
    import_entries["achievements-modded.dat"]="contains achivement information for modded plays"
 
    import_entries["achievements.dat"]="contains achievement information for vanilla plays"
 
    import_entries["archive"]="contains desync reports"
 
    import_entries["blueprint-storage.dat"]="contains global (non-savegame specific) blueprints"
 
    import_entries["config/config.ini"]="contains game configuration, including things like shortcuts etc."
 
    import_entries["crop-cache.dat"]="purpose is not known"
 
    import_entries["factorio-current.log"]="contains logs from the currently running game"
 
    import_entries["factorio-previous.log"]="contains logs from the previously running game"
 
    import_entries["mods"]="contains mods and mod settings"
 
    import_entries["player-data.json"]="contains global information about the player, such as username, login token, chat history, etc."
 
    import_entries["saves"]="contains savegames"
 

	
 
    # Ensure we are working with valid directories.
 
    if [[ -e $instance_directory ]]; then
 
        error "Instance already exists."
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    if [[ ! -f $source_directory/bin/x64/factorio ]]; then
 
        error "Could not locate Factorio binary in source directory under: $source_directory/bin/x64/factorio"
 
        error "Factorio Manager natively supports instance imports only when all game data (savegames etc) are stored within Factorio installation directory."
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    # Display list of available Factorio versions and let user pick one.
 
    echo "Factorio version must  be selected manually for imported instances."
 
    echo
 
    echo "The following versions of Factorio are locally available:"
 
    echo
 

	
 
    for candidate in "$game_installations_directory"/*; do
 
        if [[ -f $candidate/bin/x64/factorio ]]; then
 
            echo "  - $(basename "$candidate")"
 
        fi
 
    done
 

	
 
    echo
 

	
 
    echo -n "Please specify what version you would like to use: "
 
    read game_version_selected
 

	
 
    # Validate user input.
 
    if [[ ! -f "$game_installations_directory/$game_version_selected/bin/x64/factorio" ]]; then
 
        error "Requested version not locally available: $game_version_selected"
 
        exit "$ERROR_ARGUMENTS"
 
    fi
 

	
 
    (
 

	
 
        # Obtain lock - Factorio uses the same mechanism, so we should
 
        # be able to detect the game is running in this way.
 
        flock --exclusive --nonblock 200
 
        if [[ $? != 0 ]]; then
 
            error "Could not lock instance directory via lock file $source_lock_file. Is Factorio instance still running?"
 
            exit "$ERROR_GENERAL"
 
        fi
 

	
 
        function remove_lock() {
 
            rm "$source_lock_file"
 
        }
 

	
 
        trap remove_lock EXIT
 

	
 
        # Create instance directory.
 
        mkdir "$instance_directory"
 

	
 
        # Sort the associative array keys.
 
        IFS=$'\n' import_entries_keys=($(sort <<<"${!import_entries[*]}"))
 
        unset IFS
 

	
 
        # Copy files and directories.
 
        echo
 
        declare -a missing_import_entries
 
        for source_entry in "${import_entries_keys[@]}"; do
 
            source_entry_path="$source_directory/$source_entry"
 
            if [[ -e $source_entry_path ]]; then
 
                info "Importing $(bold_blue "$source_entry")..."
 

	
 
                if ! cp -a "$source_entry_path" "$instance_directory/"; then
 
                    error "Could not import $source_entry from $source_entry_path (see above for errors)."
 
                    exit "$ERROR_GENERAL"
 
                fi
 
            else
 
                missing_import_entries+=("$source_entry")
 
            fi
 

	
 
            # Copy the configuration file.
 
        done
 

	
 
        echo
 

	
 
        cat <<EOF > "$instance_config"
 
game_version="$game_version_selected"
 
EOF
 

	
 
        # Fix write-data in config.ini
 
        write_data="write-data=${instance_directory}"
 
        sed -i -e "s#^write-data=.*#$write_data#" "$game_config"
 

	
 
        if [[ ${#missing_import_entries[@]} != 0 ]]; then
 
            warning "A number of files or directories were missing from the specified source."
 
            warning "For some of the entries this is perfectly normal, but you should verify that no critical files have been missed by mistake before switching to using this instance."
 
            echo
 
            for missing_import_entry in "${missing_import_entries[@]}"; do
 
                echo "$(bold_blue "$missing_import_entry"), ${import_entries[$missing_import_entry]}"
 
                echo
 
            done
 
            warning "Press any key to continue."
 
            read -n1
 
        fi
 

	
 
        success "Finished import of instance $(bold_green "$instance")."
 

	
 
    ) 200>"$source_lock_file"
 
else
 
    error "Invalid command: $command"
 

	
 
    exit "$ERROR_ARGUMENTS"
 
fi
0 comments (0 inline, 0 general)