Changeset - 1c0c8958dde8
[Not reviewed]
0 1 0
Branko Majic (branko) - 4 years ago 2020-06-29 16:20:28
branko@majic.rs
Noticket: Added ability to create and launch server instances via Factorio Manager.
1 file changed with 518 insertions and 2 deletions:
0 comments (0 inline, 0 general)
games/factorio_manager.sh
Show inline comments
 
@@ -31,6 +31,7 @@ Usage:
 
  $program [OPTIONS] list
 

	
 
  $program [OPTIONS] create INSTANCE
 
  $program [OPTIONS] create-server INSTANCE
 
  $program [OPTIONS] copy SOURCE_INSTANCE DESTINATION_INSTANCE
 
  $program [OPTIONS] remove INSTANCE
 
  $program [OPTIONS] import INSTANCE SOURCE_DIRECTORY
 
@@ -263,6 +264,330 @@ function bold_blue() {
 
    echo -n "${_text_bold}${_text_blue}$@${_text_reset}"
 
}
 

	
 
#
 
# Reads servers settings from user.
 
#
 
# Function will go over a number of question, providing the user with
 
# default values, and allowing editing in-place. Once all questions
 
# have been answered, user will be shown the settings, and then
 
# allowed to update them again until satisfied.
 
#
 
# Once the user is done, the function will transform the answers so
 
# they are fully usable in the server configuration file, adding
 
# quoting for strings etc.
 
#
 
# Arguments:
 
#
 
#   $1 (server_name)
 
#     Default name to use for the server. Normally should be an
 
#     instance name.
 
#
 
# Sets:
 
#
 
#   settings_value
 
#     Associative array storing settings and their values. Keys are
 
#     equivalent to setting names in the server configuration file.
 
#
 
function read_server_settings() {
 

	
 
    # Read arguments.
 
    local server_name="$1"
 

	
 
    # Local variables.
 
    local key value prompt confirmed item
 
    declare -A settings_prompt
 
    declare -A settings_description
 
    declare -A settings_type
 
    declare -a settings_order
 
    declare -a possible_options
 

	
 
    # Global variables.
 
    declare -g -A settings_value option
 

	
 
    # Set-up listings of server settings. Each setting is described
 
    # with name, prompt, description, default value, and
 
    # type. Maintain additional array with keys in order to maintain
 
    # order when displaying questions to users.
 
    key="name"
 
    settings_prompt["$key"]="Name"
 
    settings_description["$key"]="Name of the game as it will appear in the game listing"
 
    settings_value["$key"]="$server_name"
 
    settings_type["$key"]="str"
 
    settings_order+=("$key")
 

	
 
    key="description"
 
    settings_prompt["$key"]="Description"
 
    settings_description["$key"]="Description of the game that will appear in the listing"
 
    settings_value["$key"]=""
 
    settings_type["$key"]="str"
 
    settings_order+=("$key")
 

	
 
    key="tags"
 
    settings_prompt["$key"]="Tags"
 
    settings_description["$key"]="Tags for the game that will appear in the listing (space-separated)"
 
    settings_value["$key"]=""
 
    settings_type["$key"]="list"
 
    settings_order+=("$key")
 

	
 
    key="max_players"
 
    settings_prompt["$key"]="Maximum players"
 
    settings_description["$key"]="Maximum number of players allowed, admins can join even a full server. 0 means unlimited."
 
    settings_value["$key"]="0"
 
    settings_type["$key"]="int"
 
    settings_order+=("$key")
 

	
 
    key="public"
 
    settings_prompt["$key"]="Publicly available"
 
    settings_description["$key"]="Game will be published on the official Factorio matching server."
 
    settings_value["$key"]="false"
 
    settings_type["$key"]="bool"
 
    settings_order+=("$key")
 

	
 
    key="lan"
 
    settings_prompt["$key"]="Broadcast on LAN"
 
    settings_description["$key"]="Game will be broadcast on LAN."
 
    settings_value["$key"]="false"
 
    settings_type["$key"]="bool"
 
    settings_order+=("$key")
 

	
 
    key="username"
 
    settings_prompt["$key"]="Factorio login username"
 
    settings_description["$key"]="Your factorio.com login credentials. Required for games with visibility public."
 
    settings_value["$key"]=""
 
    settings_type["$key"]="str"
 
    settings_order+=("$key")
 

	
 
    key="password"
 
    settings_prompt["$key"]="Factorio login password"
 
    settings_description["$key"]="Your factorio.com login credentials. Required for games with visibility public."
 
    settings_value["$key"]=""
 
    settings_type["$key"]="str"
 
    settings_order+=("$key")
 

	
 
    key="token"
 
    settings_prompt["$key"]="Factorio authentication token"
 
    settings_description["$key"]="Authentication token. May be used instead of 'password' for factorio.com login credentials."
 
    settings_value["$key"]=""
 
    settings_type["$key"]="str"
 
    settings_order+=("$key")
 

	
 
    key="game_password"
 
    settings_prompt["$key"]="Server password"
 
    settings_description["$key"]="Server password used to authenticate users towards the server itself. Default value has been randomly generated using /dev/urandom."
 
    settings_value["$key"]=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 33)
 
    settings_type["$key"]="str"
 
    settings_order+=("$key")
 

	
 
    key="require_user_verification"
 
    settings_prompt["$key"]="Require user verification"
 
    settings_description["$key"]="When set to true, the server will only allow clients that have a valid Factorio.com account."
 
    settings_value["$key"]="false"
 
    settings_type["$key"]="bool"
 
    settings_order+=("$key")
 

	
 
    key="max_upload_in_kilobytes_per_second"
 
    settings_prompt["$key"]="Maxiumum upload in kilobytes per second"
 
    settings_description["$key"]="Limits the maximum upload speed from server towards the clients (for map transfers etc). 0 means unlimited."
 
    settings_value["$key"]="0"
 
    settings_type["$key"]="int"
 
    settings_order+=("$key")
 

	
 
    key="max_upload_slots"
 
    settings_prompt["$key"]="Maximum upload slots"
 
    settings_description["$key"]="Limits the number of simulataneous uploads from server towards clients. 0 means unlimited."
 
    settings_value["$key"]="5"
 
    settings_type["$key"]="int"
 
    settings_order+=("$key")
 

	
 
    key="minimum_latency_in_ticks"
 
    settings_prompt["$key"]="Minimum latency in ticks"
 
    settings_description["$key"]="Minimum tolerable latency in ticks for connecting clients. One tick is 16ms in default speed. 0 means no minimum."
 
    settings_value["$key"]="0"
 
    settings_type["$key"]="int"
 
    settings_order+=("$key")
 

	
 
    key="ignore_player_limit_for_returning_players"
 
    settings_prompt["$key"]="Ignore player limit for returning players"
 
    settings_description["$key"]="Players that played on this map already can join even when the max player limit was reached."
 
    settings_value["$key"]="false"
 
    settings_type["$key"]="int"
 
    settings_order+=("$key")
 

	
 
    key="allow_commands"
 
    settings_prompt["$key"]="Allow commands"
 
    settings_description["$key"]="Allow execution of commands via Lua console."
 
    settings_value["$key"]="admins-only"
 
    settings_type["$key"]="true|false|\"admins-only\""
 
    settings_order+=("$key")
 

	
 
    key="autosave_interval"
 
    settings_prompt["$key"]="Autosave interval"
 
    settings_description["$key"]="Autosave interval in minutes."
 
    settings_value["$key"]="10"
 
    settings_type["$key"]="int"
 
    settings_order+=("$key")
 

	
 
    key="autosave_slots"
 
    settings_prompt["$key"]="Autosave slots"
 
    settings_description["$key"]="Number of autosave slots to use. Autosave slots are cycled through when the server autosaves."
 
    settings_value["$key"]="5"
 
    settings_type["$key"]="int"
 
    settings_order+=("$key")
 

	
 
    key="afk_autokick_interval"
 
    settings_prompt["$key"]="AFK auto-kick interval"
 
    settings_description["$key"]="How many minutes until someone is kicked when doing nothing, 0 for never."
 
    settings_value["$key"]="0"
 
    settings_type["$key"]="int"
 
    settings_order+=("$key")
 

	
 
    key="auto_pause"
 
    settings_prompt["$key"]="Auto-pause"
 
    settings_description["$key"]="Whether should the server be paused when no players are present."
 
    settings_value["$key"]="true"
 
    settings_type["$key"]="bool"
 
    settings_order+=("$key")
 

	
 
    key="only_admins_can_pause_the_game"
 
    settings_prompt["$key"]="Only admins can pause the game"
 
    settings_description["$key"]="Specify if only admins should be able to pause the game."
 
    settings_value["$key"]="true"
 
    settings_type["$key"]="bool"
 
    settings_order+=("$key")
 

	
 
    key="autosave_only_on_server"
 
    settings_prompt["$key"]="Autosave only on server"
 
    settings_description["$key"]="Whether autosaves should be saved only on server or also on all connected clients."
 
    settings_value["$key"]="true"
 
    settings_type["$key"]="bool"
 
    settings_order+=("$key")
 

	
 
    key="non_blocking_saving"
 
    settings_prompt["$key"]="[EXPERT] Non-blocking saving"
 
    settings_description["$key"]="Highly experimental feature, enable only at your own risk of losing your saves. On UNIX systems, server will fork itself to create an autosave. Autosaving on connected Windows clients will be disabled regardless of autosave_only_on_server option."
 
    settings_value["$key"]="false"
 
    settings_type["$key"]="bool"
 
    settings_order+=("$key")
 

	
 
    key="minimum_segment_size"
 
    settings_prompt["$key"]="[EXPERT] Minimum segment size"
 
    settings_description["$key"]="Long network messages are split into segments that are sent over multiple ticks. Their size depends on the number of peers currently connected. Increasing the segment size will increase upload bandwidth requirement for the server and download bandwidth requirement for clients. This setting only affects server outbound messages. Changing these settings can have a negative impact on connection stability for some clients."
 
    settings_value["$key"]="25"
 
    settings_type["$key"]="int"
 
    settings_order+=("$key")
 

	
 
    key="minimum_segment_size_peer_count"
 
    settings_prompt["$key"]="[EXPERT] Minimum segment size peer count"
 
    settings_description["$key"]="See description for minimum_segment_size"
 
    settings_value["$key"]="20"
 
    settings_type["$key"]="int"
 
    settings_order+=("$key")
 

	
 
    key="maximum_segment_size"
 
    settings_prompt["$key"]="[EXPERT] Maximum segment size"
 
    settings_description["$key"]="See description for minimum_segment_size"
 
    settings_value["$key"]="100"
 
    settings_type["$key"]="int"
 
    settings_order+=("$key")
 

	
 
    key="maximum_segment_size_peer_count"
 
    settings_prompt["$key"]="[EXPERT] Maxmium segment size peer count"
 
    settings_description["$key"]="See description for minimum_segment_size"
 
    settings_value["$key"]="10"
 
    settings_type["$key"]="int"
 
    settings_order+=("$key")
 

	
 
    # Loop until user has confirmed the settings.
 
    while [[ ${confirmed,,} != 'y' ]]; do
 
        confirmed=""
 

	
 
        # Display the settings.
 
        echo "Current settings:"
 
        echo
 
        for key in "${settings_order[@]}"; do
 
            echo -e "${_text_yellow}${settings_prompt[$key]}: ${_text_reset}${settings_value[$key]}"
 
        done
 

	
 
        # Prompt user to confirm settings.
 
        while [[ ${confirmed,,} != 'y' && ${confirmed,,} != 'n' ]]; do
 
            echo
 
            read -n1 -p "${_text_green}Are you satisfied with current settings (y/n)? ${_text_reset}" confirmed
 
            echo
 
        done
 

	
 
        # Allow user to provide changed values if not satisfied with
 
        # settings.
 
        if [[ ${confirmed,,} == 'n' ]]; then
 
            echo
 

	
 
            local total_questions="${#settings_order[@]}"
 
            local current_question=1
 

	
 
            for key in "${settings_order[@]}"; do
 
                if [[ ${settings_type[$key]} == "bool" ]]; then
 
                    prompt="${settings_prompt[$key]} [true|false]: "
 
                elif [[ ${settings_type[$key]} == "int" ]]; then
 
                    prompt="${settings_prompt[$key]} [number]: "
 
                elif [[ ${settings_type[$key]} == "str" ]]; then
 
                    prompt="${settings_prompt[$key]} [text]: "
 
                elif [[ ${settings_type[$key]} == "list" ]]; then
 
                    prompt="${settings_prompt[$key]} [space-delimited list]: "
 
                else
 
                    prompt="${settings_prompt[$key]} [${settings_type[$key]}]: "
 
                fi
 

	
 
                echo "${_text_white}${settings_description[$key]}${_text_reset}" | fold -s
 
                printf "${_text_green}%02d/%02d ${prompt}${_text_reset}" "$current_question" "$total_questions"
 
                read -e -i "${settings_value[$key]}" value
 

	
 
                settings_value[$key]="$value"
 

	
 
                echo
 
                let current_question++
 
            done
 
        fi
 
    done
 

	
 
    # Prepare values so they can be used within the JSON file.
 
    # @TODO: Perform proper validation of settings during prompts.
 
    for key in "${settings_order[@]}"; do
 
        debug "Raw value for $key: ${settings_value[$key]}"
 

	
 
        if [[ ${settings_type[$key]} == "str" ]]; then
 
            settings_value[$key]="\"${settings_value[$key]}\""
 

	
 
        # @TODO: This is a hack for true|false|"admins-only"
 
        # really. If moved into proper validation process, could be
 
        # dealt away with probably.
 
        elif [[ ${settings_type[$key]} =~ .*\|.* ]]; then
 
            readarray -t -d '|' <<<"${settings_type[$key]}" possible_options
 

	
 
            # If we find an exact match, that is the value we want to
 
            # use. Otherwise add quotes around the settings value.
 
            value=""
 
            for option in "${possible_options[@]}"; do
 
                if [[ ${settings_value[$key]} == $option ]]; then
 
                    value="${settings_value[$key]}"
 
                fi
 
            done
 

	
 
            if [[ -z $value ]]; then
 
                value="\"${settings_value[$key]}\""
 
            fi
 
            settings_value[$key]="$value"
 

	
 
        elif [[ ${settings_type[$key]} == "list" ]]; then
 
            value=""
 
            for item in ${settings_value[$key]}; do
 
                value="$value, \"$item\""
 
            done
 

	
 
            settings_value[$key]="[${value##, }]"
 
        fi
 
        debug "Processed value for $key: ${settings_value[$key]}"
 
    done
 
}
 

	
 
# Define error codes.
 
SUCCESS=0
 
ERROR_ARGUMENTS=1
 
@@ -472,7 +797,7 @@ EOF
 
    cat <<EOF > "$game_config"
 
[path]
 
read-data=__PATH__executable__/../../data
 
write-data=${instance_dir}
 
write-data=${instance_directory}
 

	
 
[general]
 
locale=
 
@@ -515,6 +840,7 @@ elif [[ $command == launch ]]; then
 
    instance_directory="$manager_directory/$instance"
 
    instance_config="$instance_directory/instance.conf"
 
    game_config="$instance_directory/config.ini"
 
    server_config="$instance_directory/server-settings.json"
 

	
 
    # Verify arguments.
 
    if [[ -z $instance ]]; then
 
@@ -566,7 +892,11 @@ elif [[ $command == launch ]]; then
 
    fi
 

	
 
    # Launch instance
 
    "$factorio_bin" --config "$game_config"
 
    if [[ -e $server_config ]]; then
 
        "$factorio_bin" --config "$game_config" --start-server "$instance_directory/saves/default.zip"
 
    else
 
        "$factorio_bin" --config "$game_config"
 
    fi
 

	
 
elif [[ $command == backup ]]; then
 
    # Read positional arguments.
 
@@ -1597,6 +1927,192 @@ EOF
 
        success "Finished import of instance $(bold_green "$instance")."
 

	
 
    ) 200>"$source_lock_file"
 

	
 

	
 
#===============#
 
# create-server #
 
#===============#
 
elif [[ $command == create-server ]]; then
 
    instance="$1"
 

	
 
    # 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"
 
    server_config="$instance_directory/server-settings.json"
 
    saves_directory="$instance_directory/saves"
 
    main_save="$saves_directory/default.zip"
 

	
 
    # 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
 

	
 
    # Grab server settings from user.
 
    echo "You will now be prompted to provide settings for the server, with some pre-filled settings."
 
    echo "Do not change settings marked with [EXPERT] unless you know what you are doing."
 
    echo
 
    read_server_settings "$instance"
 

	
 
    # 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
 
    cat <<EOF > "$game_config"
 
[path]
 
read-data=__PATH__executable__/../../data
 
write-data=${instance_directory}
 

	
 
[general]
 
locale=
 

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

	
 

	
 
[interface]
 

	
 
[controls]
 

	
 
[sound]
 

	
 
[map-view]
 

	
 
[debug]
 

	
 
[multiplayer-lobby]
 

	
 
[graphics]
 
EOF
 

	
 
    echo
 
    cat <<EOF >> "$server_config"
 
{
 
  "name": ${settings_value[name]},
 
  "description": ${settings_value[description]},
 
  "tags": ${settings_value[tags]},
 
  "max_players": ${settings_value[max_players]},
 
  "visibility":
 
  {
 
    "public": ${settings_value[public]},
 
    "lan": ${settings_value[lan]}
 
  },
 

	
 
  "_comment_credentials": "Your factorio.com login credentials. Required for games with visibility public",
 
  "username": ${settings_value[username]},
 
  "password": ${settings_value[password]},
 

	
 
  "_comment_token": "Authentication token. May be used instead of 'password' above.",
 
  "token": ${settings_value[token]},
 

	
 
  "game_password": ${settings_value[game_password]},
 

	
 
  "_comment_require_user_verification": "When set to true, the server will only allow clients that have a valid Factorio.com account",
 
  "require_user_verification": ${settings_value[require_user_verification]},
 

	
 
  "_comment_max_upload_in_kilobytes_per_second" : "optional, default value is 0. 0 means unlimited.",
 
  "max_upload_in_kilobytes_per_second": ${settings_value[max_upload_in_kilobytes_per_second]},
 

	
 
  "_comment_max_upload_slots" : "optional, default value is 5. 0 means unlimited.",
 
  "max_upload_slots": ${settings_value[max_upload_slots]},
 

	
 
  "_comment_minimum_latency_in_ticks": "optional one tick is 16ms in default speed, default value is 0. 0 means no minimum.",
 
  "minimum_latency_in_ticks": ${settings_value[minimum_latency_in_ticks]},
 

	
 
  "_comment_ignore_player_limit_for_returning_players": "Players that played on this map already can join even when the max player limit was reached.",
 
  "ignore_player_limit_for_returning_players": ${settings_value[ignore_player_limit_for_returning_players]},
 

	
 
  "_comment_allow_commands": "possible values are, true, false and admins-only",
 
  "allow_commands": ${settings_value[allow_commands]},
 

	
 
  "_comment_autosave_interval": "Autosave interval in minutes",
 
  "autosave_interval": ${settings_value[autosave_interval]},
 

	
 
  "_comment_autosave_slots": "server autosave slots, it is cycled through when the server autosaves.",
 
  "autosave_slots": ${settings_value[autosave_slots]},
 

	
 
  "_comment_afk_autokick_interval": "How many minutes until someone is kicked when doing nothing, 0 for never.",
 
  "afk_autokick_interval": ${settings_value[afk_autokick_interval]},
 

	
 
  "_comment_auto_pause": "Whether should the server be paused when no players are present.",
 
  "auto_pause": ${settings_value[auto_pause]},
 

	
 
  "only_admins_can_pause_the_game": ${settings_value[only_admins_can_pause_the_game]},
 

	
 
  "_comment_autosave_only_on_server": "Whether autosaves should be saved only on server or also on all connected clients. Default is true.",
 
  "autosave_only_on_server": ${settings_value[autosave_only_on_server]},
 

	
 
  "_comment_non_blocking_saving": "Highly experimental feature, enable only at your own risk of losing your saves. On UNIX systems, server will fork itself to create an autosave. Autosaving on connected Windows clients will be disabled regardless of autosave_only_on_server option.",
 
  "non_blocking_saving": ${settings_value[non_blocking_saving]},
 

	
 
  "_comment_segment_sizes": "Long network messages are split into segments that are sent over multiple ticks. Their size depends on the number of peers currently connected. Increasing the segment size will increase upload bandwidth requirement for the server and download bandwidth requirement for clients. This setting only affects server outbound messages. Changing these settings can have a negative impact on connection stability for some clients.",
 
  "minimum_segment_size": ${settings_value[minimum_segment_size]},
 
  "minimum_segment_size_peer_count": ${settings_value[minimum_segment_size_peer_count]},
 
  "maximum_segment_size": ${settings_value[maximum_segment_size]},
 
  "maximum_segment_size_peer_count": ${settings_value[maximum_segment_size_peer_count]}
 
}
 
EOF
 

	
 
    # Generate main save/map.
 
    info "Generating default savegame/map."
 
    game_directory="${game_installations_directory}/${game_version_selected}"
 
    factorio_bin="$game_directory/bin/x64/factorio"
 

	
 
    if ! "$factorio_bin" --config "$game_config" --create "$saves_directory/default.zip"; then
 
        error "Failed to generate default savegame/map under: $saves_directory/default.zip"
 
        exit "$ERROR_GENERAL"
 
    fi
 

	
 
    success "Created new server instance $(bold_green "$instance")"
 
else
 
    error "Invalid command: $command"
 

	
0 comments (0 inline, 0 general)