#!/bin/bash ### CONSTANTS ### START_PORT_RANGE=30000 BASE_DIR=~/"Mordhau" CONFIG_DIRECTORY="${BASE_DIR}/config" MODS_CONFIG="${CONFIG_DIRECTORY}/mods.conf" ADMINS_CONFIG="${CONFIG_DIRECTORY}/admins.conf" RCON_PASS_LOCATION="${CONFIG_DIRECTORY}/rcon.pass" SERVER_PASS_LOCATION="${CONFIG_DIRECTORY}/server.pass" ### CONSTANTS ### 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 } important() { echo_rgb "${1}" 135 195 255 } kill_server() { local prefix local tmux_response local server_id server_id="" prefix="Mordhau" while :; do case ${1} in -h | -\? | --help) printf "Usage: %s\n" \ "kill --server | kill -s --server | -s Kills the server with the given id Example: --server 3" exit ;; --) # End of all options. break ;; --server | -s) shift server_id="${1}" [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 ;; -?*) printf 'Unknown option: %s\n' "$1" >&2 ;; *) # Default case: No more options, so break out of the loop. break ;; esac shift done [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 tmux kill-session -t "${prefix}-Server-${server_id}" >/dev/null 2>&1 tmux_response="${?}" if [ "${tmux_response}" == "0" ]; then log "info" "Stopped $(important "${prefix}-Server-${server_id}")" return "${tmux_response}" else log "error" "Could not find $(important "${prefix}-Server-${server_id}") or unable to shut down ${prefix}-Server-${server_id}" return "${tmux_response}" fi } should_kill() { local prefix local server_id prefix="Mordhau" server_id="${1}" tmux has-session -t "${prefix}-Server-${server_id}" >/dev/null 2>&1 if [ "${?}" == 0 ]; then log "warning" "${prefix} server $(important "${prefix}-Server-${server_id}") is currently running" confirmation "Would you like to kill it (y/N)?" if [[ "${?}" -eq 0 ]]; then log "info" "Ok, killing server $(important "${prefix}-Server-${server_id}")" tmux kill-session -t "${prefix}-Server-${server_id}" && log "info" "Successfully killed $(important "${prefix}-Server-${server_id}")" return 0 else log "info" "Not ending current server $(important "${prefix}-Server-${server_id}"), exiting..." return 1 fi fi return 0 } run_and_kill() { local prefix local server_id prefix="Mordhau" server_id="${1}" local server_directory server_directory="${BASE_DIR}/Server-${server_id}" log "info" "Launching ${prefix}-Server-${server_id} to install any missing configurations..." tmux new-session -d -s "${prefix}-Server-${server_id}" \ "${server_directory}/MordhauServer.sh" sleep 5 tmux kill-session -t "${prefix}-Server-${server_id}" && log "info" "Successfully ran the Mordhau session and stopped the session" return 0 } generate_default_configs() { mkdir -p "${BASE_DIR}" && log "info" "Created base directory if it didn't exist" mkdir -p "${CONFIG_DIRECTORY}" && log "info" "Created config directory if it didn't exist" if [ ! -f "${RCON_PASS_LOCATION}" ]; then local rcon_pass rcon_pass="$(openssl rand -base64 48)" echo "${rcon_pass}" >"${RCON_PASS_LOCATION}" log "info" "Generated a new rcon password as it did not exist" fi if [ ! -f "${SERVER_PASS_LOCATION}" ]; then echo "20rserver" >"${SERVER_PASS_LOCATION}" log "info" "Generated the server password file as it did not exist" fi if [ ! -f "${MODS_CONFIG}" ]; then cat <<__EOF__ >"${MODS_CONFIG}" # This is the default mods configuration file # To define mods use the following syntax: Mods=mod_id # For example: # Mods=1121745 __EOF__ log "info" "Created a new mods configuration with no mods at ${MODS_CONFIG}" fi if [ ! -f "${ADMINS_CONFIG}" ]; then cat <<__EOF__ >"${ADMINS_CONFIG}" # This is the default admins configuration file # To define admins use the following syntax: Admins=PLAYFAB_ID # For example: # Admins=5E92E0B55E90869C __EOF__ log "info" "Created a new admins configurations with no admins at ${ADMINS_CONFIG}" fi log "info" "Generated all configs if they did not exist" } configure() { local server_id local list local verbose local edit server_id="" list=0 verbose=0 edit=0 while :; do case ${1} in -h | -\? | --help) printf "Usage: %s\n" \ "configure [options] --server | -s Lists the path to the Game-Primary.ini for the given server, useful for cat and other scripting. Required if --list not passed Example: --server 3 --list | -l List all paths to the Game-Primary.ini configs for all servers, very useful for cat and other scripting. Required if --server not passed Example: --list --edit | -e Opens each Game-Primary.ini in nano Example: --edit --verbose | -v Shows further output -- useful for a glance, should not be used with scripts Example: --verbose" exit ;; --) # End of all options. break ;; --server | -s) shift server_id="${1}" [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 ;; --list | -l) list=1 ;; --verbose | -v) verbose=1 ;; --edit | -e) edit=1 ;; -?*) printf 'Unknown option: %s\n' "$1" >&2 ;; *) # Default case: No more options, so break out of the loop. break ;; esac shift done display_verbose_output() { # First argument is the server number echo "$(important "Server-${1}") ($(important "Mordhau-Server-${1}"))" echo "==================================" } if [ -z "${server_id}" ] && [ "${list}" -eq 0 ]; then log "error" "Neither list nor server option passed, check configure --help" exit 1 fi if [ -n "${server_id}" ]; then local server_directory local primary_server_config server_directory="${BASE_DIR}/Server-${server_id}" server_num="${server_id}" primary_server_config="${server_directory}/Mordhau/Saved/Config/LinuxServer/Game-Primary.ini" [[ ! -f "${primary_server_config}" ]] && log "error" "Unable to find a config for $(important "Server-${server_num}") (${primary_server_config})" && exit 1 if [ "${verbose}" -eq 1 ]; then display_verbose_output "${server_num}" fi echo "${primary_server_config}" if [[ "${edit}" -eq 1 ]]; then log "info" "Editing $(important "Server-${server_num}")'s config located at ${primary_server_config}..." nano "${primary_server_config}" fi fi if [[ "${list}" -eq 1 ]]; then for server in "${BASE_DIR}/Server-"*; do local server_directory local primary_server_config server_directory="${server}" server_num="${server: -1}" primary_server_config="${server_directory}/Mordhau/Saved/Config/LinuxServer/Game-Primary.ini" [[ ! -f "${primary_server_config}" ]] && log "error" "Unable to find a config for $(important "Server-${server_num}") (${primary_server_config})" && continue if [[ "${verbose}" -eq 1 ]]; then echo "" display_verbose_output "${server_num}" fi echo "${primary_server_config}" if [[ "${edit}" -eq 1 ]]; then log "info" "Editing $(important "Server-${server_num}")'s config located at ${primary_server_config}..." sleep 2 nano "${primary_server_config}" fi done fi } start() { local server_id local can_kill server_id="" can_kill=0 while :; do case ${1} in -h | -\? | --help) printf "Usage: %s\n" \ "start --server | kill -s --server | -s Starts the given server id Example: --server 3 --can-kill | -c Automatically kills the server if it is running without prompting Example: --can-kill" exit ;; --) # End of all options. break ;; --server | -s) shift server_id="${1}" [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 ;; --can-kill | -c) can_kill=1 ;; -?*) printf 'Unknown option: %s\n' "$1" >&2 ;; *) # Default case: No more options, so break out of the loop. break ;; esac shift done generate_default_configs [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 local server_directory local server_config server_directory="${BASE_DIR}/Server-${server_id}" server_config_directory="${server_directory}/Mordhau/Saved/Config/LinuxServer" server_config="${server_config_directory}/Game.ini" [[ ! -d "${server_directory}" ]] && log "error" "Unable to find the server directory for ${server_id}, verify the server exists at ${server_directory}" && exit 1 [[ ! -f "${server_config}" ]] && log "error" "Unable to find a valid Game.ini for Server-${server_id}, verify the installation is correct" && exit 1 local rcon_pass local server_pass local mods local admins rcon_pass="$(cat "${RCON_PASS_LOCATION}")" server_pass="$(cat "${SERVER_PASS_LOCATION}")" if [[ "${can_kill}" -eq "1" ]]; then kill_server -s "${server_id}" >/dev/null 2>&1 else should_kill "${server_id}" [[ "${?}" == "1" ]] && exit 1 fi while read -r line; do if [[ "${line}" == "#"* ]]; then : else echo "${line}" fi done <"${MODS_CONFIG}" >"${BASE_DIR}/mods.tmp" mods="$(cat "${BASE_DIR}/mods.tmp")" rm -f "${BASE_DIR}/mods.tmp" while read -r line; do if [[ "${line}" == "#"* ]]; then : else echo "${line}" fi done <"${ADMINS_CONFIG}" >"${BASE_DIR}/admins.tmp" admins="$(cat "${BASE_DIR}/admins.tmp")" rm -f "${BASE_DIR}/admins.tmp" local game_port local query_port local beacon_port local rcon_port game_port=$(("${START_PORT_RANGE}" + $(("${server_id}" * 4)) + 0)) query_port=$(("${START_PORT_RANGE}" + $(("${server_id}" * 4)) + 1)) beacon_port=$(("${START_PORT_RANGE}" + $(("${server_id}" * 4)) + 2)) rcon_port=$(("${START_PORT_RANGE}" + $(("${server_id}" * 4)) + 3)) log "info" "Updating Game-Primary.ini" local backup_extension backup_extension="$(date +%Y-%m-%dT%H:%M:%S%z)" # Necessary as the external config to Game.ini local game_primary game_primary="${server_config_directory}/Game-Primary.ini" [[ ! -f "${game_primary}" ]] && cp "${server_config}" "${game_primary}" && log "info" "Created a Game-Primary.ini config as it did not exist" log "info" "Creating Game-Primary.ini backup as Game-Primary.ini.${backup_extension}" cp "${game_primary}" "${game_primary}.${backup_extension}" log "info" "Created backup of Game-Primary.ini located at ${game_primary}.${backup_extension}" # The below booleans are used to avoid duplicating lines, a sort of failsafe local updated_rcon_port local updated_rcon_pass updated_rcon_port=0 updated_rcon_pass=0 # Loading primary config bool is incredibly important as it controls loading the external configurations to the server local primary_config_admins_mods local loading_primary_config local loaded_primary_config loading_primary_config=0 loaded_primary_config=0 primary_config_admins_mods="$( cat <<__EOF__ # BEGIN PRIMARY CONFIG - DO NOT INCLUDE SERVER SPECIFIC CONFIGURATION BETWEEN THIS LINE AND "END PRIMARY CONFIG" # Primary config Mods ${mods} # Primary config Admins ${admins} # END PRIMARY CONFIG - DO NOT INCLUDE SERVER SPECIFIC CONFIGURATION BETWEEN THIS LINE AND "BEGIN PRIMARY CONFIG" __EOF__ )" log "info" "Overwriting Game.ini..." while read -r line; do if [[ "${line}" == "RconPort="* ]] && [ "${updated_rcon_port}" -eq 0 ]; then echo "RconPort=${rcon_port}" updated_rcon_port=1 elif [[ "${line}" == "RconPassword="* ]] && [ "${updated_rcon_pass}" -eq 0 ]; then echo "RconPassword=${rcon_pass}" updated_rcon_pass=1 elif [[ "${line}" == *"BEGIN PRIMARY CONFIG"* ]]; then loading_primary_config=1 elif [[ "${line}" == *"END PRIMARY CONFIG"* ]]; then loading_primary_config=0 echo "${primary_config_admins_mods}" loaded_primary_config=1 elif [ "${loading_primary_config}" -eq 0 ]; then echo "${line}" fi done <"${game_primary}" >"serv.temp" if [ "${loaded_primary_config}" -eq 0 ]; then log "info" "Primary config not injected, loading it now..." echo "${primary_config_admins_mods}" >>"serv.temp" log "info" "Successfully injected primary config" fi mv "serv.temp" "${game_primary}" cat "${game_primary}" >"${server_config}" log "info" "Finished updating Game-Primary.ini ($(important "${game_primary}"))" log "info" "Ports: $(important " Game Port: ${game_port}") $(important " Query Port: ${query_port}") $(important " Beacon Port: ${beacon_port}") $(important " RCON Port: ${rcon_port}")" log "info" "Startup Arguments: $(important " -Port=${game_port}") $(important " -QueryPort=${query_port}") $(important " -BeaconPort=${beacon_port}") $(important " -LOG") $(important " -USEALLAVAILABLE")" tmux new-session -d -s "Mordhau-Server-${server_id}" \ ${BASE_DIR}/Server-"${server_id}"/MordhauServer.sh \ -Port="${game_port}" \ -QueryPort="${query_port}" \ -BeaconPort="${beacon_port}" \ -LOG \ -USEALLAVAILABLE && log "info" "Successfully started Mordhau-Server-${server_id}" } update() { local server_id local redownload_mods server_id="" redownload_mods=0 while :; do case ${1} in -h | -\? | --help) printf "Usage: %s\n" \ "update --server | kill -s --server | -s Starts the given server id Example: --server 3 --redownload-mods | -r Redownloads all mods by clearing the .modio folder" exit ;; --) # End of all options. break ;; --server | -s) shift server_id="${1}" [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 ;; -?*) printf 'Unknown option: %s\n' "$1" >&2 ;; *) # Default case: No more options, so break out of the loop. break ;; esac shift done [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 kill_server -s "${server_id}" >/dev/null 2>&1 local server_directory local server_config server_directory="${BASE_DIR}/Server-${server_id}" server_config="${server_directory}/Mordhau/Saved/Config/LinuxServer/Game.ini" [[ ! -d "${server_directory}" ]] && log "error" "No server directory found for ${server_id}, checked at ${server_directory}" && exit 1 if [ "${redownload_mods}" == "1" ]; then rm -rf "${server_directory}/Mordhau/Content/.modio" log "info" ".modio cleared, start Server-${server_id} to download mods" fi log "info" "Verifying and updating server" steamcmd +login anonymous +force_install_dir "${server_directory}" +app_update 629800 validate +quit run_and_kill "${server_id}" log "info" "Successfully verified and updated $(important "Server-${server_id}")" } install() { local server_id server_id="" while :; do case ${1} in -h | -\? | --help) printf "Usage: %s\n" \ "install --server | install -s --server | -s Installs the server to the given id if it doesn't exist Example: --server 3" exit ;; --) # End of all options. break ;; --server | -s) shift server_id="${1}" [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 ;; -?*) printf 'Unknown option: %s\n' "$1" >&2 ;; *) # Default case: No more options, so break out of the loop. break ;; esac shift done [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 local prefix prefix="Mordhau" local server_directory local server_config server_directory="${BASE_DIR}/Server-${server_id}" server_config="${server_directory}/Mordhau/Saved/Config/LinuxServer/Game.ini" [[ -d "${server_directory}" ]] && log "error" "A server already exists at ${server_directory}, delete it and try again" && exit 1 kill_server -s "${server_id}" >/dev/null 2>&1 mkdir -p "${server_directory}" && log "info" "Created server directory ${server_directory}" steamcmd +login anonymous +force_install_dir "${server_directory}" +app_update 629800 validate +quit log "info" "Successfully installed Server-${server_id}" log "info" "Starting server to install default configuration files, please wait..." run_and_kill "${server_id}" cp "${server_config}" "Game-Primary.ini" && log "info" "Created the Game-Primary.ini file" log "info" "Finished setting up Server-${server_id}" } usage() { # Print out usage instructions for the local script # # Arguments: # None # # Usage: # usage # # POSIX Compliant: # Yes # printf "Usage: %s\n" \ "$(basename ${0}) -h start Exposes options to start Mordhau Servers, pass -h to it for details kill Exposes options to kill Mordhau Servers, pass -h to it for details install Exposes options to install Mordhau Servers, pass -h to it for details update Exposes options to update Mordhau Servers, pass -h to it for details configure Exposes options to configure Mordhau Servers, pass -h to it for details" } 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 ;; start | s) shift start "$@" break ;; kill | k) shift kill_server "$@" break ;; install | i) shift install "$@" break ;; update | u) shift update "$@" break ;; configure | c) shift configure "$@" break ;; -?*) printf 'Unknown option: %s\n' "$1" >&2 usage exit 1 ;; *) # Default case: No more options, so break out of the loop. break ;; esac shift done } parse_args "$@"