#!/bin/bash ### PORTS ### # GAME PORT RANGE: 13000-13099 # QUERY PORT RANGE: 13100-13199 # RCON PORT RANGE: 13200-13299 echo_rgb() { # Echo a colored string to the terminal based on rgb values # # Positional Arguments: # # message # - The message to be printed to stdout # red # - The red value from 0 to 255 # green # - The green value from 0 to 255 # blue # - The blue value from 0 to 255 # # Usage: # echo_rgb "Yep" 10 8 30 # # POSIX Compliant: # N/A # local red local green local blue local input input="${1}" red="${2}" green="${3}" blue="${4}" printf "\e[0;38;2;%s;%s;%sm%s\e[m\n" "${red}" "${green}" "${blue}" "${input}" } log() { # Print a message and send it to stdout or stderr depending upon log level, also configurable with debug etc. # # Arguments: # level # - The log level, defined within a case check in this function # message # - The info message # line_number # - The line number of the calling function (${LINNO}) # # Usage: # log "info" "Could not find that directory" # # POSIX Compliant: # Yes # # Set debug status depending if a global debug variable has been set to either 1 or 0 local debug if [ ${DEBUG} ]; then debug=${DEBUG} else debug=0 fi local FORMAT FORMAT="[$(echo_rgb "$(date +%Y-%m-%dT%H:%M:%S)" 180 140 255)]" # Convert the level to uppercase local level level=$(echo "${1}" | tr '[:lower:]' '[:upper:]') local message message="${2}" case "${level}" in INFO) # Output all info log levels to stdout printf "${FORMAT}[$(echo_rgb "INFO" 0 140 255)] %s\n" "${message}" >&1 return 0 ;; WARN | WARNING) # Output all info log levels to stdout printf "${FORMAT}[$(echo_rgb "WARNING" 255 255 0)] %s\n" "${message}" >&1 return 0 ;; DEBUG) [[ ${debug} == 0 ]] && return printf "${FORMAT}[$(echo_rgb "DEBUG" 0 160 110)] %s\n" "${message}" >&1 return 0 ;; ERROR) # Output all error log levels to stderr printf "${FORMAT}[$(echo_rgb "ERROR" 255 0 0)] %s\n" "${message}" >&2 return 0 ;; # Further log levels can be added by extending this switch statement with more comparisons *) # Default case, no matches # Returns non-zero code as an improper log option was passed, this helps with using `set -e` printf "${FORMAT}[ERROR] %s\n" "Invalid log level passed, received level \"${level}\" with message \"${message}\"" >&2 return 1 ;; esac } confirmation() { # Receive confirmation from user as y, Y, n, or N # returns 0 when answer is yes and 1 when answer is no # # Arguments: # message # - The confirmation prompt sent to the user, for example: # Would you like to overwrite foobar.txt (y/N)? # # Usage: # confirmation "Some prompt" # - Sends "Some prompt" to the user and gets their input # # POSIX Compliant: # Yes # local message message="${1}" local choice while true; do read -p "${message} " -n 1 -r choice case "$choice" in y | Y) echo "" return 0 ;; n | N) echo "" return 1 ;; *) echo -e "\nInput must be either y, Y, n, or N" ;; esac done } usage() { # Print out usage instructions for the local script # # Arguments: # None # # Usage: # usage # # POSIX Compliant: # Yes # printf "Usage: %s\n" \ "$(important "$(basename "${0}") -s | $(basename "${0}") -s -v") -s | --server Which Squad server to start, see the ~/Squad directory -- each number corresponds to an ID Example: --server 0 -v | --verify Forces a verification check of the given Squad installation Example: --verify" } important() { echo_rgb "${1}" 145 233 255 } server_id="" verify=0 parse_args() { # Parse input arguments # # Arguments: # Consult the `usage` function # # Usage: # parse_args "$@" # - All arguments should be ingested by parse_args first for variable setting # # POSIX Compliant: # Yes # while :; do case ${1} in -h | -\? | --help) usage # Display a usage synopsis. exit ;; --) # End of all options. break ;; -s | --server) shift server_id="${1}" [[ -z "${server_id}" ]] && \ log "error" "Server requires an argument, e.g. --server 0" && \ exit 1 [[ ! "${server_id}" =~ [0-9] ]] && \ log "error" "Server ID must be a number, received: ${server_id}" && \ exit 1 ;; -v | --verify) verify=1 ;; -?*) printf 'Unknown option: %s\n' "$1" >&2 usage ;; *) # Default case: No more options, so break out of the loop. break ;; esac shift done } parse_args "$@" [[ -z "${server_id}" ]] \ && log "error" "A server id must be passed via the -s flag" \ && exit 1 ### CONSTANTS ### TICK_RATE="30" ### CONSTANTS ### squad_directory=~/Squad/Server-"${server_id}" [[ ! -d "${squad_directory}" ]] && \ log "error" "The Squad directory does not exist at ${squad_directory}, unable to start $(important "${squad_directory}")" && \ exit 1 squad_start_script="${squad_directory}"/SquadGameServer.sh [[ ! -f "${squad_start_script}" ]] && log "error" "The given Squad server lacks a start script, could not find $(importamt "${squad_start_script}")" && exit 1 squad_rcon_config="${squad_directory}"/SquadGame/ServerConfig/Rcon.cfg [[ ! -f "${squad_rcon_config}" ]] && \ log "error" "The given Squad server lacks a Rcon.cfg, could not find $(important "${squad_rcon_config}")" && \ exit 1 # If the tmux session exists then we prompt the user if they want to kill it off and start the new session tmux has-session -t "Squad-Server-${server_id}" >/dev/null 2>&1 if [ "${?}" == 0 ]; then log "warning" "Squad server $(important "Squad-Server-${server_id}") is currently running" confirmation "Would you like to kill it and then run the new one (y/N)?" if [ "${?}" == 0 ]; then log "info" "Ok, killing server $(important "Squad-Server-${server_id}")" tmux kill-session -t "Squad-Server-${server_id}" && log "info" "Successfully killed $(important "Squad-Server-${server_id}")" else log "info" "Not ending current server $(important "Squad-Server-${server_id}"), exiting..." exit 0 fi fi # Create our relevant ports game_port=$(("13000" + $(("${server_id}" * 2)))) query_port=$(("13100" + $(("${server_id}" * 2)))) rcon_port=$(("13200" + "${server_id}")) # Create backups before modifying files backup_extension="$(date +%Y-%m-%dT%H:%M:%S%z)" mkdir -p "${squad_directory}"/Backups/ log "info" "Creating a backup of $(important "Rcon.cfg")" cp "${squad_directory}"/SquadGame/ServerConfig/Rcon.cfg "${squad_directory}"/Backups/Rcon.cfg."${backup_extension}" && log "info" "Successfully created a backup of $(important "Rcon.cfg")" ## RCON configuration # If we haven't created a rcon.password file go ahead and make it and generate a password for it if [ ! -f ~/Squad/rcon.password ]; then log "info" "Creating a global rcon password in $(important ~/Squad/rcon.password) as it did not exist" openssl rand -base64 48 >~/Squad/rcon.password fi # Get the rcon password rcon_password="$(cat ~/Squad/rcon.password)" log "info" "Updating $(important "Squad-Server-${server_id}")'s RCON config" # Update the password, since sed is PIA in regards to base64 & raw strings we fix this one up line by line while read -r line; do if [[ "${line}" == Password=* ]]; then # Overwrites the password echo "${line//Password=*/Password="${rcon_password}"}" elif [[ "${line}" == Port=* ]]; then # Overwrites the rcon port echo "${line//Port=*/Port="${rcon_port}"}" else echo "${line}" fi done <"${squad_rcon_config}" >"rcon.temp" && mv "rcon.temp" "${squad_rcon_config}" log "info" "Successfully updated RCON config" if [ "${verify}" -eq "1" ]; then log "info" "Verification of game files requested" log "info" "Verifying the Squad install located at $(important "${squad_directory}")" steamcmd +login anonymous +force_install_dir "${squad_directory}" +app_update 403240 validate +quit log "info" "Finished verifying files" fi log "info" \ "Starting $(important "Squad-Server-${server_id}") with the following parameters: $(important \ "Port=${game_port} QueryPort=${query_port} FIXEDMAXTICKRATE=${TICK_RATE} USEALLAVAILABLECORES LOG")" # Kick off a new tmux session containing the Squad Server echo | tmux new-session -d -s "Squad-Server-${server_id}" \ "${squad_start_script}" Port="${game_port}" QueryPort="${query_port}" FIXEDMAXTICKRATE="${TICK_RATE}" \ USEALLAVILABLECORES LOG && log "info" \ "Started Squad-Server-${server_id} with the following ports: $(important \ "Game Port: ${game_port} Query Port: ${query_port} RCON Port ${rcon_port}")"