#!/bin/bash

# Minecraft Server Manager
# ========================
#
# A single init script for managing multiple Minecraft servers.
# Created by Marcus Whybrow
#
# https://github.com/msmhq/msm
#

### BEGIN INIT INFO
# Provides:	  msm
# Required-Start: $local_fs $remote_fs
# Required-Stop:  $local_fs $remote_fs
# Should-Start:	  $network
# Should-Stop:	  $network
# Default-Start:  2 3 4 5
# Default-Stop:	  0 1 6
# Short-Description: MSM: Minecraft init script
# Description: Minecraft Server Manager, an init script for Minecraft/Bukkit servers
### END INIT INFO


# See http://www.debian.org/doc/debian-policy/ch-opersys.html#s-sysvinit for
# more information on debian init.d scripts, which may help you understand
# this script.


# The Minecraft Server Manager version, use "msm version" to check yours.
VERSION="0.9.4"


# Source, if it exists, the msm profile.d script
if [ -f "/etc/profile.d/msm.sh" ]; then
	source "/etc/profile.d/msm.sh"
fi

# $1: The file to follow links for
follow_links() {
	unset RETURN
	local file="$1"
	while [[ -L "$file" ]]; do
		file="$(readlink "$file")"
	done
	RETURN="$file"
}

# Get real script file location
follow_links "$0"; SCRIPT="$RETURN"
# Get the MSM_CONF environment variable or use the default location
CONF="${MSM_CONF:-/etc/msm.conf}"
# Get the MSM_BASH_COMPLETION environment variable or use default location
COMPLETION="${MSM_BASH_COMPLETION:-/etc/bash_completion.d/msm}"
follow_links "$COMPLETION"; COMPLETION="$RETURN"


### Config variables the user should not need/want to change

# Lazy allocation status
ALLOCATED_SERVERS="false"
ALLOCATED_WORLDS="false"

# Global totals
NUM_WORLDS=0
NUM_SERVERS=0
COMMAND_COUNT=0
SETTING_COUNT=0
SERVER_SETTING_COUNT=0
VERSIONS_COUNT=0


### Utility Functions

# Executes the command "$2" as user "$1"
# $1: The user to execute the command as
# $2: The command to execute
as_user() {
	local user="$(whoami)"
	if [ "$user" == "$1" ]; then
		bash -c "$2"
	else
		if [ "$user" == "root" ]; then
			su - "$1" -s /bin/bash -c "$2"
		else
			if [[ "$1" == "root" ]]; then
				error_exit INVALID_USER "This command must be executed as the user \"$1\"."
			else
				error_exit INVALID_USER "This command must be executed as the user \"$1\" or \"root\"."
			fi
		fi
	fi
}

# Executes the command "$1" as SERVER_USER but returns stderr instead
as_user_stderr() {
	as_user "$@"  > /dev/null 2>&1
}

# Echo to stderr
echoerr() {
	echo -e "$@" 1>&2
}

COLOUR_PURPLE="\e[1;35m"
COLOUR_RED="\e[1;31m"
COLOUR_CYAN="\e[1;36m"
COLOUR_GREEN="\e[1;32m"
COLOUR_RESET="\e[0m"

# Creates a coloured warning line
# $1 The warning to echo
msm_warning() {
	echoerr "${COLOUR_PURPLE}[MSM Warning: ${1}]${COLOUR_RESET}"
}

msm_error() {
	echoerr "${COLOUR_RED}[MSM Error: ${1}]${COLOUR_RESET}"
}

msm_info() {
	echo -e "${COLOUR_CYAN}[MSM Info: ${1}]${COLOUR_RESET}"
}

msm_success() {
	echo -e "${COLOUR_CYAN}[MSM: ${1}]${COLOUR_RESET}"
}

# Echoes the first non-empty string in the arguments list
# $1->: Candidate strings for echoing
echo_fallback() {
	for arg in "$@"; do
		[ -z "$arg" ] && continue
		echo "$arg" && break
	done
}

# $1: The string to echo if present
echo_if() {
	[ ! -z "$1" ] && echo "$1"
}

# Exit's the script
error_exit() {
	case "$1" in
		INVALID_USER) code=64;;
		INVALID_COMMAND) code=65;;
		INVALID_ARGUMENT) code=66;;
		SERVER_STOPPED) code=67;;
		SERVER_RUNNING) code=68;;
		NAME_NOT_FOUND) code=69;;
		FILE_NOT_FOUND) code=70;;
		DUPLICATE_NAME) code=71;;
		LOGS_NOT_ROLLED) code=72;;
		CONF_ERROR) code=73;;
		FATAL_ERROR) code=74;;
		JAVA_NOT_INSTALLED) code=75;;
	esac

	echo "${2:-"Unknown Error"}" 1>&2
	exit "${code:-$1}"
}

# Tests the bash version installed
# $1: The bash version required
is_bash_version() {
	if [[ "$BASH_VERSION" =~ ^$1 ]]; then
		return 0
	fi

	return 1
}

# Converts a string to be ready for use as a global
# variable name.
# $1: The string to convert
# RETURN: The name in uppercase and with underscores
to_global_name() {
	unset RETURN
	# Translate to uppercase, and replace dashes with underscores
	local result="$1"
	if is_bash_version 4; then
		# Much faster than the `tr` command
		result="${result//-/_}"
		result="${result//./_}"
		result="${result^^}" # to uppercase
	else
		result="$(echo "$result" | tr '[\-\.a-z]' '[\_\_A-Z]')"
	fi

	RETURN="$result"
}

# Converts a global BASH variable name to a server.properties file
# variable name.
# $1: The string to convert
# RETURN: The name in lowercase and with dashes
to_properties_name() {
	unset RETURN
	# Translate to uppercase, and replace dashes with underscores
	local result="$1"
	if is_bash_version 4; then
		# Much faster than the `tr` command
		result="${result//_/-}"
		result="${result,,}" # to lowercase
	else
		result="$(echo "$result" | tr '[\_A-Z]' '[\-a-z]')"
	fi

	RETURN="$result"
}

# A custom basename function which is faster
# than opening a subshell
# $1: The path to get the basename of
# RETURN: The basename of the path
quick_basename() {
	unset RETURN
	if [[ "$1" =~ \/([^\/]*)$ ]]; then
		RETURN="${BASH_REMATCH[1]}"
	fi
}

# A function used to print debug messages to stdout. Prevents messages from
# appearing unless in debug mode, and allows debug statements to be easily
# distinguished from necessary echo statements.
# $1: The message to output
debug() {
	manager_property DEBUG

	if [[ "$SETTINGS_DEBUG" == "true" ]]; then
		echoerr "$1"
	fi
}

# Determines whether "$1" is a valid name for a server or jar group directory
# It must only contain upper or lower case letters, digits, dashes or
# underscores.
# It must also not be one of a list of reserved names.
# $1: The name to check
is_valid_name() {
	local valid="^[a-zA-Z0-9\_\-]+$"
	local invalid="^(start|stop|restart|version|server|jargroup|all|config|update|help|\-\-.*)$"

	if [[ "$1" =~ $valid ]]; then
		if [[ "$1" =~ $invalid ]]; then
			error_exit INVALID_ARGUMENT "Invalid name \"$1\": A name may not be any of the following reserved worlds \"start\", \"stop\", \"restart\", \"server\", \"version\", \"jargroup\", \"all\", \"config\", \"update\" or \"help\" or start with two dashes (--)."
		else
			return 0
		fi
	else
		error_exit INVALID_ARGUMENT "Invalid name \"$1\": A name may only contain letters, numbers, dashes and underscores."
	fi
}

# Gets the latest jar from a jar group, based upon the date and time encoded
# in the file name.
# $1: The directory to search
# RETURN: The latest file
get_latest_file() {
	unset RETURN

	local best_time=0
	local best_file=""

	while IFS= read -r -d $'\0' file; do
		# Remove the path, leaving just the file name
		local date_time="$(basename "$file" | awk -F '-' '{print $1 "-" $2 "-" $3 " " $4 ":" $5 ":" $6}')"

		# Get the time in seconds since 1970 from file name
		local seconds="$(date -d "$date_time" "+%s" 2> /dev/null)"

		# If that is newer than the current best, override variables
		if [[ "$seconds" -gt "$best_time" ]]; then
			best_time="$seconds"
			best_file="$file"
		fi
	done < <(find "$1" -maxdepth 1 -type f -print0)

	RETURN="$best_file"
}

# Returns the current time as a UNIX timestamp (in seconds since 1970)
now() {
	date +%s
}


### Log Utility Functions

# Gets the UNIX timestamp for a server log line
# $1: A server log line
# returns: Time in seconds since 1970-01-01 00:00:00 UTC
log_line_get_time() {
	time_string="$(echo "$1" | awk -F'[] [/:]+' '{print  $1 " " $2 ":" $3 ":" $4}')"
	date -d "$time_string" "+%s" 2> /dev/null
}










### World Utility Functions
### -----------------------

# Moves a world to RAM
# $1: the ID of the world to move
world_to_ram() {
	manager_property RAMDISK_STORAGE_ENABLED
	manager_property RAMDISK_STORAGE_PATH
	server_property "${WORLD_SERVER_ID[$1]}" USERNAME
	world_property "$1" RAMDISK_PATH
	world_property "$1" FLAG_INRAM
	world_property "$1" PATH

	if [[ "$SETTINGS_RAMDISK_STORAGE_ENABLED" == "true" ]]; then
		as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"${WORLD_RAMDISK_PATH[$1]}\" && rsync -rt --exclude '$(basename "${WORLD_FLAG_INRAM[$1]}")' \"${WORLD_PATH[$1]}/\" \"${WORLD_RAMDISK_PATH[$1]}\""
	fi
}

# Moves a world in RAM to disk
# $1: the ID of the world to move
world_to_disk() {
	server_property "${WORLD_SERVER_ID[$1]}" USERNAME
	world_property "$1" FLAG_INRAM
	world_property "$1" RAMDISK_PATH
	world_property "$1" PATH

	as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "rsync -rt --exclude '$(basename "${WORLD_FLAG_INRAM[$1]}")' \"${WORLD_RAMDISK_PATH[$1]}/\" \"${WORLD_PATH[$1]}\""
}

# Toggles a worlds ram disk state
# $1: The ID of the world
world_toggle_ramdisk_state() {
	world_property "$1" FLAG_INRAM
	world_property "$1" RAMDISK_PATH
	local sid="${WORLD_SERVER_ID[$1]}"
	server_property "$sid" USERNAME


	if [ -f "${WORLD_FLAG_INRAM[$1]}" ]; then
		echo -n "Synchronising world \"${WORLD_NAME[$1]}\" to disk... "
		world_to_disk "$1"
		echo "Done."

		echo -n "Removing RAM flag from world \"${WORLD_NAME[$1]}\"... "
		as_user "${SERVER_USERNAME[$sid]}" "rm -f \"${WORLD_FLAG_INRAM[$1]}\""
		echo "Done."

		echo -n "Removing world \"${WORLD_NAME[$1]}\" from RAM... "
		as_user "${SERVER_USERNAME[$sid]}" "rm -r \"${WORLD_RAMDISK_PATH[$1]}\""
		echo "Done."
	else
		echo -n "Adding RAM flag to world \"${WORLD_NAME[$1]}\"... "
		as_user "${SERVER_USERNAME[$sid]}" "touch \"${WORLD_FLAG_INRAM[$1]}\""
		echo "Done."

		echo -n "Copying world to RAM... "
		world_to_ram "$1"
		echo "Done."
	fi
	echo "Changes will only take effect after server is restarted."
}

# Backs up a world
# $1: The ID of the world
world_backup() {
	manager_property WORLD_ARCHIVE_ENABLED
	manager_property RDIFF_BACKUP_ENABLED
	manager_property RSYNC_BACKUP_ENABLED
	local server_id="${WORLD_SERVER_ID[$1]}"
	local containing_dir="$(dirname "${WORLD_PATH[$1]}")"
	local dir_name="$(basename "${WORLD_PATH[$1]}")"
	world_property "$1" PATH
	world_property "$1" BACKUP_PATH

	echo -n "Entering in backup function ... "


	if [[ "$SETTINGS_WORLD_ARCHIVE_ENABLED" == "true" ]]; then
		echo -n "Backing up world \"${WORLD_NAME[$1]}\"... "
		file_name="$(date "+%F-%H-%M-%S").zip"
		server_property "$server_id" USERNAME
		as_user "${SERVER_USERNAME[$server_id]}" "mkdir -p \"${WORLD_BACKUP_PATH[$1]}\" && cd \"$containing_dir\" && zip -rq \"${WORLD_BACKUP_PATH[$1]}/${file_name}\" \"${dir_name}\""
		echo "Done."
	fi

	if [[ "$SETTINGS_RDIFF_BACKUP_ENABLED" == "true" ]]; then
		echo -n "rdiff-backup world \"${WORLD_NAME[$1]}\"... "
    server_property "$server_id" USERNAME
    as_user "${SERVER_USERNAME[$server_id]}" "mkdir -p \"${RDIFF_BACKUP_PATH[$1]}\" && cd \"$containing_dir\" && nice -n \"$SETTINGS_RDIFF_BACKUP_NICE\" rdiff-backup \"${dir_name}\" \"${RDIFF_BACKUP_PATH[$1]}\" && nice -n \"$SETTINGS_RDIFF_BACKUP_NICE\" rdiff-backup --remove-older-than \"$SETTINGS_RDIFF_BACKUP_ROTATION\"D --force \"${RDIFF_BACKUP_PATH[$1]}\""
		echo "Done."
	fi

	if [[ "$SETTINGS_RSYNC_BACKUP_ENABLED" == "true" ]]; then
		echo -n "rsync-backup world \"${WORLD_NAME[$1]}\"... "
		file_name="$(date "+%F-%H-%M-%S")"
		server_property "$server_id" USERNAME
		as_user "${SERVER_USERNAME[$server_id]}" "mkdir -p \"${RSYNC_BACKUP_PATH[$1]}\" && cd \"$containing_dir\" && rsync -aH --link-dest=\"${RSYNC_BACKUP_PATH[$1]}/latest\" \"${dir_name}\" \"${RSYNC_BACKUP_PATH[$1]}/${file_name}\" && rm -f \"${RSYNC_BACKUP_PATH[$1]}/latest\" && ln -s \"${file_name}\" \"${RSYNC_BACKUP_PATH[$1]}/latest\""
		echo "Done."
	fi

}

# Activates a world
# $1: The ID of the world
world_activate() {
	server_property "${WORLD_SERVER_ID[$1]}" USERNAME
	server_property "${WORLD_SERVER_ID[$1]}" WORLD_STORAGE_PATH
	world_property "$1" INACTIVE_PATH
	world_property "$1" ACTIVE_PATH

	if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then
		echo -n "Moving world \"${WORLD_NAME[$1]}\" to the active worldstorage directory... "
		local new_path="${SERVER_WORLD_STORAGE_PATH[${WORLD_SERVER_ID[$1]}]}"
		as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"$new_path\" && mv \"${WORLD_INACTIVE_PATH[$1]}\" \"$new_path\""
		echo "Done."
	else
		if [ -d "${WORLD_ACTIVE_PATH[$1]}" ]; then
			echo "World \"${WORLD_NAME[$1]}\" is already activate."
		else
			error_exit DIR_NOT_FOUND "Directory \"${WORLD_INACTIVE_PATH[$1]}\" could not be found."
		fi
	fi




}

# Deactivates a world
# $1: The ID of the world
world_deactivate() {
	server_property "${WORLD_SERVER_ID[$1]}" USERNAME
	server_property "${WORLD_SERVER_ID[$1]}" WORLD_STORAGE_INACTIVE_PATH
	world_property "$1" ACTIVE_PATH
	world_property "$1" INACTIVE_PATH
	world_property "$1" PATH

	if server_is_running "${WORLD_SERVER_ID[$1]}"; then
		error_exit 68 "Worlds cannot be deactivated whilst the server is running."
	else
		if [ -d "${WORLD_ACTIVE_PATH[$1]}" ]; then
			echo -n "Moving world \"${WORLD_NAME[$1]}\" to the inactive worldstorage directory... "
			local new_path="${SERVER_WORLD_STORAGE_INACTIVE_PATH[${WORLD_SERVER_ID[$1]}]}"
			as_user "${SERVER_USERNAME[${WORLD_SERVER_ID[$1]}]}" "mkdir -p \"$new_path\" && mv \"${WORLD_PATH[$1]}\" \"$new_path\""
			echo "Done."
		else
			if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then
				echo "World \"${WORLD_NAME[$1]}\" is already deactivate."
			else
				error_exit DIR_NOT_FOUND "Directory \"${WORLD_ACTIVE_PATH[$1]}\" could not be found."
			fi
		fi
	fi
}

# Get the value of a world property
# $1: The world ID
# $2: The property name
world_property() {
	# Get the current value
	eval local value=\"\${WORLD_$2[$1]}\"

	# If it is empty, then set it
	if [ -z "$value" ]; then
		local sid="${WORLD_SERVER_ID[$1]}"
		case "$2" in
			NAME|PATH)
				# Defined at allocation
				return 0
				;;
			ACTIVE_PATH)
				server_property "$sid" WORLD_STORAGE_PATH
				WORLD_ACTIVE_PATH[$1]="${SERVER_WORLD_STORAGE_PATH[$sid]}/${WORLD_NAME[$1]}"
				;;
			INACTIVE_PATH)
				server_property "$sid" WORLD_STORAGE_INACTIVE_PATH
				WORLD_INACTIVE_PATH[$1]="${SERVER_WORLD_STORAGE_INACTIVE_PATH[$sid]}/${WORLD_NAME[$1]}"
				;;
			STATUS)
				world_property "$1" ACTIVE_PATH

				if [ -d "${WORLD_ACTIVE_PATH[$1]}" ]; then
					WORLD_STATUS[$1]="active"
				else
					world_property "$1" INACTIVE_PATH
					if [ -d "${WORLD_INACTIVE_PATH[$1]}" ]; then
						WORLD_STATUS[$1]="inactive"
					else
						WORLD_STATUS[$1]="unknown"
					fi
				fi
				;;
			FLAG_INRAM)
				world_property "$1" PATH
				server_property "$sid" WORLDS_FLAG_INRAM
				WORLD_FLAG_INRAM[$1]="${WORLD_PATH[$1]}/${SERVER_WORLDS_FLAG_INRAM[$sid]}"
				;;
			LINK)
				server_property "$sid" PATH
				WORLD_LINK[$1]="${SERVER_PATH[$sid]}/${WORLD_NAME[$1]}"
				;;
			BACKUP_PATH)
				manager_property WORLD_ARCHIVE_PATH
				manager_property WORLD_RDIFF_PATH
				manager_property WORLD_RSYNC_PATH
				WORLD_BACKUP_PATH[$1]="$SETTINGS_WORLD_ARCHIVE_PATH/${SERVER_NAME[$sid]}/${WORLD_NAME[$1]}"
				RDIFF_BACKUP_PATH[$1]="$SETTINGS_WORLD_RDIFF_PATH/${SERVER_NAME[$sid]}/${WORLD_NAME[$1]}"
				RSYNC_BACKUP_PATH[$1]="$SETTINGS_WORLD_RSYNC_PATH/${SERVER_NAME[$sid]}/${WORLD_NAME[$1]}"
				;;
			RAMDISK_PATH)
				manager_property RAMDISK_STORAGE_ENABLED
				# If the ram disk path is set, get the path for this world
				if [[ "$SETTINGS_RAMDISK_STORAGE_ENABLED" == "true" ]]; then
					manager_property RAMDISK_STORAGE_PATH
					WORLD_RAMDISK_PATH[$1]="${SETTINGS_RAMDISK_STORAGE_PATH}/${SERVER_NAME[$sid]}/${WORLD_NAME[$1]}"
				fi
				;;
			INRAM)
				world_property "$1" FLAG_INRAM
				# Detect whether this world should be in ram
				if [[ -e "${WORLD_FLAG_INRAM[$1]}" ]]; then
					WORLD_INRAM[$1]="true"
				else
					WORLD_INRAM[$1]="false"
				fi
				;;
		esac
	fi
}

# $1: The world ID
world_dirty_properties() {
	local index

	# Removes properties for all servers if an index
	# is not specified
	if [ ! -z "$1" ] && [[ "$1" -ge 0 ]]; then
		index="[$1]"
	else
		index=""
	fi

	unset WORLD_NAME$index
	unset WORLD_PATH$index
	unset WORLD_ACTIVE_PATH$index
	unset WORLD_INACTIVE_PATH$index
	unset WORLD_STATUS$index
	unset WORLD_FLAG_INRAM$index
	unset WORLD_LINK$index
	unset WORLD_BACKUP_PATH$index
	unset RDIFF_BACKUP_PATH$index
	unset RSYNC_BACKUP_PATH$index
	unset WORLD_RAMDISK_PATH$index
	unset WORLD_INRAM$index
}










### Server Utility Functions
### ------------------------

# Returns the ID for a server.
# An ID is given to a server when loaded into memory, and can be used to lookup
# config information for that server
# $1: The name of the server
server_get_id() {
	unset RETURN
	for ((server=0; server<$NUM_SERVERS; server++)); do
		if [[ "${SERVER_NAME[$server]}" == "$1" ]]; then
			RETURN="$server"
			return 0
		fi
	done

	error_exit NAME_NOT_FOUND "Could not find id for server name \"$1\"."
}

# Returns the ID of a server's world.
# $1: The ID of the server
# $2: The name of the world
server_world_get_id() {
	server_property "$1" WORLD_STORAGE_PATH
	server_property "$1" WORLD_STORAGE_INACTIVE_PATH

	unset RETURN
	if [ -d "${SERVER_WORLD_STORAGE_PATH[$1]}/$2" ] || [ -d "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$1]}/$2" ]; then
		# If the directory exists

		local start="${SERVER_WORLD_OFFSET[$1]}"
		local max="$(( $start + ${SERVER_NUM_WORLDS[$1]} ))"

		# For each of the servers worlds:
		for ((i=$start; i<$max; i++)); do
			if [[ "${WORLD_NAME[$i]}" == "$2" ]]; then
				RETURN="$i"
				return 0
			fi
		done
	fi

	error_exit NAME_NOT_FOUND "Could not find id for world \"$2\" for server \"${SERVER_NAME[$1]}\"."
}

# Returns 0 if the server $1 is running and 1 if not
# $1: The ID of the server
server_is_running() {
	server_property "$1" SCREEN_NAME
	server_property "$1" INVOCATION

	if ps ax | grep -v grep | grep "${SERVER_SCREEN_NAME[$1]} ${SERVER_INVOCATION[$1]}" > /dev/null
	then
		return 0
	else
		return 1
	fi
}

# Ensures the server has a jar file where it is expected to be
# $1: The id of the server
server_ensure_jar() {
	server_property "$1" JAR_PATH

	if [ -f "${SERVER_JAR_PATH[$1]}" ]; then
		return 0
	fi

	error_exit FILE_NOT_FOUND "Could not find jar for server \"${SERVER_NAME[$1]}\": Expected \"${SERVER_JAR_PATH[$1]}\"."
}

# Read a value from the server configuration file
# $1: The id of the server
# $2: The setting name to read
server_read_config() {
	unset RETURN
	# Convert name into uppercase with underscores
	# msm-setting => SERVER_SETTING
	# setting => SERVER_PROPERTIES_SETTING
	if [[ "$2" =~ ^msm\-(.*)$ ]]; then
		to_global_name "${BASH_REMATCH[1]}"
	else
		to_global_name "PROPERTIES_$2"
	fi
	local name="$RETURN"

	# Display the value of that setting
	unset RETURN
	server_property "$1" "$name"
	eval RETURN=\"\${SERVER_$name[$1]}\"
}

# Creates symbolic links in the server directory (SETTINGS_SERVER_STORAGE_PATH) for each
# of the Minecraft worlds located in the worldstorage directory.
# $1: The id of the server for which links should be ensured
server_ensure_links() {
	server_property "$1" USERNAME
	server_property "$1" WORLD_STORAGE_PATH

	# Ensure a directory for level-name exists in worldstorage.
	# This allows a symlink to be created, and prevents new worlds
	# being generated outside of worldstorage.
	command_server_config "$1" "level-name"
	as_user "${SERVER_USERNAME[$1]}" "mkdir -p \"${SERVER_WORLD_STORAGE_PATH[$1]}/$RETURN\""
	server_worlds_allocate "$1"

	echo -n "Maintaining world symbolic links... "
	local start="${SERVER_WORLD_OFFSET[$1]}"
	local max="$(( $start + ${SERVER_NUM_WORLDS[$1]} ))"
	local output="false"

	for ((i=$start; i<$max; i++)); do
		world_property "$i" STATUS
		world_property "$i" LINK

		if [[ "${WORLD_STATUS[$i]}" != "active" ]]; then
			# Remove the symbolic link if it exists
			as_user "${SERVER_USERNAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\""
			continue
		fi

		world_property "$i" INRAM

		# -L checks for the path being a link rather than a file
		# ! -a, since it is within double square brackets means: the negation of
		# the existence of the file. In other words: true if does not exist
		if [[ -L "${WORLD_LINK[$i]}" || ! -a "${WORLD_LINK[$i]}" ]]; then
			# If there is a symbolic link in the server directory to this world,
			# or there is not a directory in the server directory containing this world.

			# Get the original file path the symbolic link is pointing to
			# If there is no link, link_target will contain nothing
			link_target="$(readlink "${WORLD_LINK[$i]}")"

			if "${WORLD_INRAM[$i]}"; then
				# If this world is marked as loaded into RAM

				world_property "$i" RAMDISK_PATH

				if [ "${link_target}" != "${WORLD_RAMDISK_PATH[$i]}" ]; then
					# If the symbolic link does not point to the RAM version of the world

					# Remove the symbolic link if it exists
					as_user "${SERVER_USERNAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\""

					# Create a new symbolic link pointing to the RAM version of the world
					as_user "${SERVER_USERNAME[$1]}" "ln -s \"${WORLD_RAMDISK_PATH[$i]}\" \"${WORLD_LINK[$i]}\""
				fi
			else
				# Otherwise the world is not loaded into RAM, and is just on disk

				world_property "$i" PATH

				if [ "${link_target}" != "${WORLD_PATH[$i]}" ]; then
					# If the symbolic link does not point to the disk version of the world

					# Remove the symbolic link if it exists
					as_user "${SERVER_USERNAME[$1]}" "rm -f \"${WORLD_LINK[$i]}\""

					# Create a new symbolic link pointing to the disk version of the world
					as_user "${SERVER_USERNAME[$1]}" "ln -s \"${WORLD_PATH[$i]}\" \"${WORLD_LINK[$i]}\""
				fi
			fi
		else
			echoerr -en "\n    Error: Could not create link for world \"${WORLD_NAME[$i]}\". The file \"${WORLD_LINK[$i]}\" already exists, and should not be overwritten automatically. Either remove this file, or rename \"${WORLD_NAME[$i]}\"."
			output="true"
		fi
	done

	if [[ "$output" == "true" ]]; then
		echo -e "\nDone."
	else
		echo "Done."
	fi
}

# Moves a servers worlds into RAM
# $1: The ID of the server
server_worlds_to_ram() {
	manager_property RAMDISK_STORAGE_ENABLED

	# Only proceed if there is a ram disk path set in config
	if [[ "$SETTINGS_RAMDISK_STORAGE_ENABLED" == "true" ]]; then
		echo -n "Synchronising flagged worlds on disk to RAM... "
		local i="${SERVER_WORLD_OFFSET[$1]}"
		local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))"

		# For each of the servers worlds:
		while [[ "$i" -lt "$max" ]]; do
			world_property "$i" INRAM
			world_property "$i" LINK

			if "${WORLD_INRAM[$i]}" && [ -L "${WORLD_LINK[$i]}" ]; then
				world_to_ram "$i"
			fi

			i="$(( $i + 1 ))"
		done
		echo "Done."
	fi
}

# Moves a servers "in RAM" worlds back to disk
# $1: The ID of the server
server_worlds_to_disk() {
	manager_property RAMDISK_STORAGE_ENABLED

	if [[ "$SETTINGS_RAMDISK_STORAGE_ENABLED" == "true" ]]; then
		echo -n "Synchronising worlds in RAM to disk... "
		local i="${SERVER_WORLD_OFFSET[$1]}"
		local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))"

		# For each of the servers worlds:
		while [[ "$i" -lt "$max" ]]; do
			world_property "$i" RAMDISK_PATH
			if [ -d "${WORLD_RAMDISK_PATH[$i]}" ]; then
				world_to_disk "$i"
			fi

			i="$(( $i + 1 ))"
		done
		echo "Done."
	fi
}

# Watches a server's log for a specific line
# $1: The ID for the server
# $2: A UNIX timestamp (seconds since 1970) which the $3 line must be after
# $3: The regex that matches log lines
# $4: A timeout in seconds
# returns: When the line is found
server_log_get_line() {
	server_property "$1" USERNAME
	server_property "$1" LOG_PATH
	server_property "$1" CONSOLE_EVENT_REGEX

	unset RETURN

	local regex="${SERVER_CONSOLE_EVENT_OUTPUT_REGEX[$1]} ($3)"
	local timeout_deadline=$(( $(now) + $4 ))

	# Read log, break if nothing is read in $4 seconds
	while read -t $4 line; do
		line_time="$(log_line_get_time "$line")"

		# If the time is after the timeout deadline, break
		[[ "$(now)" -gt "$timeout_deadline" ]] && break

		# If the entry is old enough
		if [[ "$line_time" -ge "$2" ]] && [[ "$line" =~ $regex ]]; then
			# Return the line
			RETURN="${BASH_REMATCH[1]}"
			return 0
		fi
	done < <(as_user "${SERVER_USERNAME[$1]}" "tail --pid=$$ --follow=name --retry --lines=20 --sleep-interval=0.1 \"${SERVER_LOG_PATH[$1]}\" 2>/dev/null")
}

# The same as server_log_get_line, but prints a dot instead of the log line
# to stdout, and returns when line is found.
# $1: the ID of the server
# $2: A UNIX timestamp (seconds since 1970) which the $3 line must be after
# $3: The regex that matches log lines
# $4: A timeout in seconds
# returns: When the line is found
server_log_dots_for_lines() {
	server_property "$1" USERNAME
	server_property "$1" LOG_PATH
	server_property "$1" CONSOLE_EVENT_REGEX

	local regex="${SERVER_CONSOLE_EVENT_OUTPUT_REGEX[$1]} ($3)"
	local timeout_deadline=$(( $(now) + $4 ))

	# Read log, break if nothing is read in $4 seconds
	while read -t $4 line; do
		line_time="$(log_line_get_time "$line")"

		# If the time is after the timeout deadline, break
		[[ "$(now)" -gt "$timeout_deadline" ]] && break

		# If the entry is old enough
		if [[ "$line_time" -ge "$2" ]]; then

			# Print a dot for this line
			echo -n '.'

			# and if it matches the regular expression, return
			if [[ "$line" =~ $regex ]]; then
				return 0
			fi
		fi
	done < <(as_user "${SERVER_USERNAME[$1]}" "tail --pid=$$ --follow=name --retry --lines=100 --sleep-interval=0.1 \"${SERVER_LOG_PATH[$1]}\" 2>/dev/null")
}

# Sends as string to a server for execution
# $1: The ID of the server
# $2: The line of text to enter into the server console
server_eval() {
	server_property "$1" USERNAME
	server_property "$1" SCREEN_NAME

	as_user "${SERVER_USERNAME[$1]}" "screen -p 0 -S ${SERVER_SCREEN_NAME[$1]} -X eval 'stuff \"$2\"\015'"
}

# The same as server_eval, but also waits for a log entry before returning
# $1: The ID of the server
# $2: A line of text to enter into the server console
# $3: The regex that matches log lines
# $4: A timeout in seconds
# RETURN: The full entry found in the logs
server_eval_and_get_line() {
	unset RETURN

	time_now="$(now)"
	server_eval "$1" "$2"
	server_log_get_line "$1" "$time_now" "$3" "$4"

	RETURN="$RETURN"
}

# The same as server_eval_and_get_line, but does not set RETURN
server_eval_and_wait() {
	server_eval_and_get_line "$@"
	unset RETURN # Do not return anything
}

# Executes a "version correct" command in a server's console.
# If the command has output to watch for, then wait until that
# output is found and return it, or until the timeout for that
# command
# $1: The ID of the server
# $2: The name of the command
# $3->: Command arguments in the form "argname=argvalue"
# $RETURN: The output found, if any
server_command() {
	unset RETURN

	# Load variables
	eval server_property $1 CONSOLE_COMMAND_OUTPUT_$2
	eval server_property $1 CONSOLE_COMMAND_PATTERN_$2
	eval server_property $1 CONSOLE_COMMAND_TIMEOUT_$2

	eval local output_regex=\"\${SERVER_CONSOLE_COMMAND_OUTPUT_$2[$1]}\"
	eval local pattern=\"\${SERVER_CONSOLE_COMMAND_PATTERN_$2[$1]}\"

	# Replace arguments in pattern
	for arg in "${@:3}"; do
		if [[ "$arg" =~ (.*)=(.*) ]]; then
			pattern="${pattern//<${BASH_REMATCH[1]}>/${BASH_REMATCH[2]}}"
			output_regex="${output_regex//<${BASH_REMATCH[1]}>/${BASH_REMATCH[2]}}"
		fi
	done

	# If there is no output to watch for, execute the command immediately
	# and return immediately
	if [ -z "$output_regex" ]; then
		server_eval "$1" "$pattern"
		unset RETURN
	else
		# Otherwise execute the command and wait for the specified output
		# or the timeout
		eval local timeout=\"\${SERVER_CONSOLE_COMMAND_TIMEOUT_$2[$1]}\"

		server_eval_and_get_line "$1" "$pattern" "$output_regex" "$timeout"
		RETURN="$RETURN"
	fi
}

# Gets the process ID for a server if running, otherwise it outputs nothing
# $1: The ID of the server
server_pid() {
	server_property "$1" SCREEN_NAME
	server_property "$1" INVOCATION

	ps ax | grep -v grep | grep "${SERVER_SCREEN_NAME[$1]} ${SERVER_INVOCATION[$1]}" | awk '{print $1}'
}

# Waits for a server to stop by polling 10 times a second
# This approach is fairly intensive, so only use when you are expecting the
# server to stop soon
# $1: The ID of the server to wait for
server_wait_for_stop() {
	local pid="$(server_pid "$1")"

	# if the process is still running, wait for it to stop
	if [ ! -z "$pid" ]; then
		while ps -p "$pid" > /dev/null; do
			sleep 0.1
		done
	fi
}

# Sets a server's active/inactive state
# $1: The ID of the server
# $2: A string containing "active" or "inactive"
server_set_active() {
	server_property "$1" USERNAME
	server_property "$1" FLAG_ACTIVE_PATH

	case "$2" in
		active)
			as_user "${SERVER_USERNAME[$1]}" "touch \"${SERVER_FLAG_ACTIVE_PATH[$1]}\""
			SERVER_ACTIVE[$1]="true"
			;;
		inactive)
			as_user "${SERVER_USERNAME[$1]}" "rm -f \"${SERVER_FLAG_ACTIVE_PATH[$1]}\""
			SERVER_ACTIVE[$1]="false"
			;;
		*)
			error_exit INVALID_ARGUMENT "Invalid argument."
			;;
	esac
}










### Jar Group Functions
### -------------------

# Lists the jar files grouped by jar groups.
jargroup_list() {
	manager_property JAR_STORAGE_PATH

	if [[ -d "${SETTINGS_JAR_STORAGE_PATH}" ]]; then
		local jargroup_name
		local jar_name

		while IFS= read -r -d $'\0' jargroup_path; do
			jargroup_name="$(basename "${jargroup_path}")"
			echo "$jargroup_name"
			while IFS= read -r -d $'\0' jar_path; do
				jar_name="$(basename "${jar_path}")"
				if [[ "$jar_name" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}- ]]; then
					echo "    $jar_name"
				fi
			done < <(find "${SETTINGS_JAR_STORAGE_PATH}/${jargroup_name}" -mindepth 1 -maxdepth 1 -type f -print0)
		done < <(find "${SETTINGS_JAR_STORAGE_PATH}" -mindepth 1 -maxdepth 1 -type d -print0)
	fi
}

# Creates a new jargroup
# $1: The name for the jargroup
# $2: The URL target for the jargroup
jargroup_create() {
	if is_valid_name "$1"; then
		manager_property JAR_STORAGE_PATH
		manager_property USERNAME
		manager_property JARGROUP_TARGET

		if [[ ! -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then
			echo -n "Creating jar group... "

			local error="$(as_user_stderr "$SETTINGS_USERNAME" "mkdir -p \"$SETTINGS_JAR_STORAGE_PATH/$1\"")"
			if [[ "$error" != "" ]]; then
				echo "Failed."
				error_exit FILE_NOT_FOUND "$error"
			fi

			error="$(as_user "$SETTINGS_USERNAME" "echo \"$2\" > \"$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET\"")"
			if [[ "$error" != "" ]]; then
				echo "Failed."
				error_exit FILE_NOT_FOUND "$error"
			fi

			echo "Done."
		else
			error_exit DUPLICATE_NAME "A jar group with that name already exists."
		fi
	fi
}

# Changes an existing jargroups target URL
# $1: The jargroup name to change the url of
# $2: The new target URL to set
jargroup_changeurl() {
	manager_property JAR_STORAGE_PATH
	manager_property USERNAME
	manager_property JARGROUP_TARGET

	echo -n "Changing target URL... "

	local target="$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET"

	if [ -e "${target}" ]; then
		as_user "$SETTINGS_USERNAME" "echo \"$2\" > \"${target}\""
		echo "Done."
	else
		echo "Failed."
		error_exit FILE_NOT_FOUND "Could not find URL target file \"${target}\""
	fi
}

# Downloads the latest version for a jargroup, using the target URL for that
# group. Saves the download with the date and time encoded in the start of the
# file name, in the jar group directory in question. Removes the file if there
# is no difference between it and the current version.
# $1: The jargroup name to download the latest version for
jargroup_getlatest() {
	if is_valid_name "$1"; then
		manager_property JAR_STORAGE_PATH
		manager_property JARGROUP_TARGET
		manager_property USERNAME
		manager_property JARGROUP_DOWNLOAD_DIR

		if [[ -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then
			if [[ -f "$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET" ]]; then
				printf "Downloading latest version... "

				# Try and make
				local error="$(as_user_stderr "$SETTINGS_USERNAME" "mkdir -p '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR'")"
				if [[ "$error" != "" ]]; then
					echo "Failed."
					error_exit FILE_NOT_FOUND "$error"
				fi

                                # test wget for --trust-server-names option
                                local wget_opts="--trust-server-names"
                                wget $wget_opts >/dev/null 2>&1
                                if [[ $? != 1 ]]; then
                                    wget_opts=""
                                fi

                # If target contains the word 'minecraft' or 'minecraft-snapshot', check JSON version file for correct filename
                # This method allows for backwards compatibility with previous releases
                local target="$(as_user "$SETTINGS_USERNAME" "cat $SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET")"
                if [[ "$target" =~ ^minecraft ]]; then
                    if [[ "$target" == "minecraft" ]]; then
                        local versions_target="release"
                    elif [[ "$target" == "minecraft-snapshot" ]]; then
                        local versions_target="snapshot"
                    fi
                    printf "Checking minecraft version JSON... "
                    local versions_url="http://s3.amazonaws.com/Minecraft.Download/versions/versions.json"
                    local versions_file="/tmp/minecraft_versions.json"
                    as_user "$SETTINGS_USERNAME" "wget --quiet $wget_opts --no-check-certificate -O '$versions_file' '$versions_url'"
                    local latest_version=$(as_user "$SETTINGS_USERNAME" "sed -n '/"latest"/,/}/p' $versions_file | grep $versions_target | egrep -o '([0-9]+\.?)+|([0-9]+[a-zA-Z])+'")
                    if [[ -n "$latest_version" ]]; then
                        local jar_url="https://s3.amazonaws.com/Minecraft.Download/versions/$latest_version/minecraft_server.$latest_version.jar"

                    fi
                fi
                if [[ -n "$jar_url" ]]; then
				    as_user "$SETTINGS_USERNAME" "wget --quiet $wget_opts --no-check-certificate --directory-prefix='$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR' '$jar_url'"
                else
				    as_user "$SETTINGS_USERNAME" "wget --quiet $wget_opts --no-check-certificate --input-file='$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_TARGET' --directory-prefix='$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR'"
                fi
				echo "Done."

				local num_files="$(as_user "$SETTINGS_USERNAME" "ls -1 '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR' | wc -l")"

				if [[ "$num_files" == 1 ]]; then
					# There was 1 file downloaded

					local file_name="$(ls -1 "$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR")"
					local new_name="$(date +%F-%H-%M-%S)-$file_name"

					get_latest_file "$SETTINGS_JAR_STORAGE_PATH/$1"
					local most_recent_jar="$RETURN"


					if [[ ! -f "$most_recent_jar" ]] || ! diff "$most_recent_jar" "$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR/$file_name" > /dev/null; then
						# There is not a previous version to do a comparison against, or
						# The previous version is different:
						# Add it to the group

						[[ -f "$most_recent_jar" ]]
						local was_previous="$?"

						as_user "$SETTINGS_USERNAME" "mv '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR/$file_name' '$SETTINGS_JAR_STORAGE_PATH/$1/$new_name'"

						if [[ ! -z "$most_recent_jar" ]]; then
							echo "Downloaded version was different to previous latest. Saved as \"$SETTINGS_JAR_STORAGE_PATH/$1/$new_name\"."
						else
							echo "Saved as \"$SETTINGS_JAR_STORAGE_PATH/$1/$new_name\"."
						fi
					else
						echo "Existing version \"$most_recent_jar\" was already up to date."
					fi

				elif [[ "$num_files" == 0 ]]; then
					# No file was downloaded
					echo "Failed. No files were downloaded."
				else
					# Multiple files were
					echo "Error. URL downloads multiple files."
				fi

				# Clean up the temp download folder
				as_user "$SETTINGS_USERNAME" "rm -fr '$SETTINGS_JAR_STORAGE_PATH/$1/$SETTINGS_JARGROUP_DOWNLOAD_DIR'"
			else
				error_exit FILE_NOT_FOUND "Target URL not found, use $0 jargroup seturl <download-url>"
			fi
		else
			error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"."
		fi
	fi
}

# Deletes an existing jargroup
# $1: The name of the existing jargroup
jargroup_delete() {
	if is_valid_name "$1"; then
		manager_property JAR_STORAGE_PATH
		manager_property USERNAME

		if [[ -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then
			printf "Are you sure you want to delete this jar group [y/N]: "

			read answer
			if [[ "$answer" =~ ^y|Y|yes$ ]]; then
				as_user "$SETTINGS_USERNAME" "rm -rf \"$SETTINGS_JAR_STORAGE_PATH/$1\""
				echo "Jar group deleted."
			else
				echo "Jar group was NOT deleted."
			fi
		else
			error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"."
		fi
	fi
}

# Renames an existing jargroup
# $1: The name of the existing jargroup
# $2: The new name
jargroup_rename() {
	if is_valid_name "$1"; then
		manager_property JAR_STORAGE_PATH
		manager_property USERNAME

		if [[ -d "$SETTINGS_JAR_STORAGE_PATH/$1" ]]; then
			# If the jar group name is valid,
			# and there is no other jar group with the name $1

			if is_valid_name "$2"; then
				if [[ -e "$SETTINGS_JAR_STORAGE_PATH/$2" ]]; then
					error_exit DUPLICATE_NAME "Could not be renamed, there is already a jar group with the name \"$2\"."
				else
					# TODO: Update any symbolic links which point to a jar in this directory
					as_user "$SETTINGS_USERNAME" "mv '$SETTINGS_JAR_STORAGE_PATH/$1' '$SETTINGS_JAR_STORAGE_PATH/$2'"
					echo "Renamed jar group \"$1\" to \"$2\"."
				fi
			fi
		else
			error_exit NAME_NOT_FOUND "There is no jar group with the name \"$1\"."
		fi
	fi
}










### Server Functions
### ----------------

# Echoes a list of servers in the SETTINGS_SERVER_STORAGE_PATH
server_list() {
	if [ "$NUM_SERVERS" -gt 0 ]; then
		for ((server=0; server<$NUM_SERVERS; server++)); do
			server_property "$server" ACTIVE

			if "${SERVER_ACTIVE[$server]}"; then
				echo -n "[ ACTIVE ] "
			else
				echo -n "[INACTIVE] "
			fi

			echo -n "\"${SERVER_NAME[$server]}\" "


			if "${SERVER_ACTIVE[$server]}"; then
				if server_is_running "$server"; then
					echo "is running. Everything is OK."
				else
					echo "is stopped. Server is down!"
				fi
			else
				if server_is_running "$server"; then
					echo "is running. It should not be running!"
				else
					echo "is stopped. Everything is OK."
				fi
			fi
		done
	else
		echo "[There are no servers]"
	fi
}

# Creates a new server
# $1: The server name to create
server_create() {
	if is_valid_name "$1"; then
		manager_property USERNAME
		manager_property SERVER_STORAGE_PATH
		manager_property DEFAULT_WHITELIST_PATH
		manager_property DEFAULT_BANNED_IPS_PATH
		manager_property DEFAULT_BANNED_PLAYERS_PATH
		manager_property DEFAULT_OPS_PATH
		manager_property DEFAULT_OPS_LIST
		manager_property SERVER_PROPERTIES
		manager_property DEFAULT_WORLD_STORAGE_PATH
		manager_property JAR_STORAGE_PATH

		if [[ -d "$SETTINGS_SERVER_STORAGE_PATH/$1" ]]; then
			error_exit DUPLICATE_NAME "A server with that name already exists."
		else
			printf "Creating server directory... "
			as_user "$SETTINGS_USERNAME" "mkdir -p '$SETTINGS_SERVER_STORAGE_PATH/$1'"
			as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_WHITELIST_PATH'"
			as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_BANNED_IPS_PATH'"
			as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_BANNED_PLAYERS_PATH'"
			as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_OPS_PATH'"

			# Set default ops users as appropriate
			if [ ! -z "$SETTINGS_DEFAULT_OPS_LIST" ]; then
				IFS=","; for default_ops_user in $SETTINGS_DEFAULT_OPS_LIST; do
					as_user "$SETTINGS_USERNAME" "echo $default_ops_user | tr -d ' ' >> '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_OPS_PATH'"
				done
			fi

			as_user "$SETTINGS_USERNAME" "touch '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_SERVER_PROPERTIES'"
			as_user "$SETTINGS_USERNAME" "mkdir -p '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_WORLD_STORAGE_PATH'"
			as_user "$SETTINGS_USERNAME" "echo \"MSM requires all your worlds be moved into this directory.\" > '$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_DEFAULT_WORLD_STORAGE_PATH/readme.txt'"
			echo "Done."

			# Creates a server stub in memory, enough to use server_properties for.
			SERVER_NAME[$NUM_SERVERS]="$1"
			SERVER_PATH[$NUM_SERVERS]="$SETTINGS_SERVER_STORAGE_PATH/$1"
			SERVER_CONF[$NUM_SERVERS]="$SETTINGS_SERVER_STORAGE_PATH/$1/$SETTINGS_SERVER_PROPERTIES"
			NUM_SERVERS=$(($NUM_SERVERS+1))

			# TODO: Dirty all server variables, or don't allow further in script access

			# TODO: Handle server default setup stuff better than just using
			#       the "minecraft" jar group. And make it configurable.
			if [ -d "$SETTINGS_JAR_STORAGE_PATH/minecraft" ]; then
				server_get_id "$1"
				server_set_jar "$RETURN" "minecraft"
			fi
		fi
	fi
}

# Deletes an existing server
# $1: The server name to delete
server_delete() {
	if is_valid_name "$1"; then
		manager_property SERVER_STORAGE_PATH
		manager_property USERNAME

		if [[ -d "$SETTINGS_SERVER_STORAGE_PATH/$1" ]]; then
			printf "Are you sure you want to delete server \"$1\" and its worlds? (note: backups are preserved) [y/N]: "

			read answer
			if [[ "$answer" =~ ^(y|Y|yes)$ ]]; then
				server_get_id "$1"
				local existing_id="$RETURN"

				if server_is_running "$existing_id"; then
					echo "Server \"$1\" is running."
					server_stop_now "$existing_id"
				fi
				as_user "$SETTINGS_USERNAME" "rm -rf '$SETTINGS_SERVER_STORAGE_PATH/$1'"
				echo "Server deleted."
			else
				echo "Server was NOT deleted."
			fi
		else
			error_exit NAME_NOT_FOUND "There is no server with the name \"$1\"."
		fi
	fi
}

# Renames an existing server
# $1: The server name to change
# $2: The new name for the server
server_rename() {
	if is_valid_name "$1"; then
		manager_property SERVER_STORAGE_PATH
		manager_property USERNAME

		if [ -d "$SETTINGS_SERVER_STORAGE_PATH/$1" ]; then
			# If the server name is valid and exists

			server_get_id "$1"
			local existing_id="$RETURN"

			if server_is_running "$existing_id"; then
				error_exit SERVER_RUNNING "Can only rename a stopped server."
			else
				if is_valid_name "$2"; then
					# If the server name is valid
					if [[ -e "$SETTINGS_SERVER_STORAGE_PATH/$2" ]]; then
						# and there is not already a server with the name $2
						error_exit DUPLICATE_NAME "Could not be renamed, there is already a server with the name \"$2\"."
					else
						as_user "$SETTINGS_USERNAME" "mv '$SETTINGS_SERVER_STORAGE_PATH/$1' '$SETTINGS_SERVER_STORAGE_PATH/$2'"
						echo "Renamed server \"$1\" to \"$2\"."
					fi
				fi
			fi
		else
			error_exit NAME_NOT_FOUND "There is no server with the name \"$1\"."
		fi
	fi
}

# Starts a single server
# $1: The ID of the server
server_start() {
	server_property "$1" USERNAME
	server_property "$1" SCREEN_NAME
	server_property "$1" INVOCATION
	server_property "$1" CONSOLE_EVENT_START

	if server_is_running "$1"; then
		echo "Server \"${SERVER_NAME[$1]}\" is already running!"
	else
		if ! which java > /dev/null; then
			error_exit JAVA_NOT_INSTALLED "Could not start server as Java is not installed."
		fi
		server_ensure_jar "$1"
		server_ensure_links "$1"
		server_worlds_to_ram "$1"

		local time_now="$(now)"

		printf "Starting server..."

		# This is the important line! Let's start this server!
		as_user "${SERVER_USERNAME[$1]}" "cd \"${SERVER_PATH[$1]}\" && screen -dmS \"${SERVER_SCREEN_NAME[$1]}\" ${SERVER_INVOCATION[$1]}"

		# Wait for the server to fully start
		server_log_dots_for_lines "$1" "$time_now" "${SERVER_CONSOLE_EVENT_OUTPUT_START[$1]}" "${SERVER_CONSOLE_EVENT_TIMEOUT_START[$1]}"

		if [[ -f "${SERVER_PATH[$1]}"/eula.txt ]]; then
			if ! grep -q -i 'eula=true' "${SERVER_PATH[$1]}"/eula.txt; then
				echo " Could not start the server as you first need to agree to an EULA. See eula.txt for more info."
				return
			fi
		fi

		echo " Done."
	fi
}

# Sends the "save-all" command to a server
# $1: The ID of the server
server_save_all() {
	if server_is_running "$1"; then
		echo -n "Forcing save... "
		server_command "$1" SAVE_ALL
		echo "Done."
	else
		echo "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Sends the "save-off" command to a server
# $1: The ID of the server
server_save_off() {
	if server_is_running "$1"; then
		echo -n "Disabling level saving... "
		server_command "$1" SAVE_OFF
		echo "Done."

		# Writes any in-memory data managed by the kernel to disk
		sync
	else
		echo "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Sends the "save-on" command to a server
# $1: The ID of the server
server_save_on() {
	if server_is_running "$1"; then
		echo -n "Enabling level saving... "
		server_command "$1" SAVE_ON
		echo "Done."
	else
		echo "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Stops a single server after a delay
# $1: The ID of the server
server_stop() {
	server_property "$1" MESSAGE_STOP
	server_property "$1" STOP_DELAY

	if server_is_running "$1"; then
		# Change the state of the script
		STOP_COUNTDOWN[$1]="true"

	    server_eval "$1" "say ${SERVER_MESSAGE_STOP[$1]}"
	    echo "Issued the warning \"${SERVER_MESSAGE_STOP[$1]}\" to players."

		echo -n "Shutting down... "

		for ((i="${SERVER_STOP_DELAY[$1]}"; i>0; i--)); do
			tput sc # Save cursor position
			echo -n "in $i seconds."
			sleep 1

			tput rc # Restore cursor to position of last `sc'
			tput el # Clear to end of line
		done

		echo -e "Now."
		server_stop_now "$1"
   	else
		echo "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Stops a single server right now
# $1: The ID of the server
server_stop_now() {
	if server_is_running "$1"; then
		server_save_all "$1"

		echo -n "Stopping the server... "

		server_eval "$1" "stop"
		STOP_COUNTDOWN[$1]="false"
		RESTART_COUNTDOWN[$1]="false"
		server_wait_for_stop "$1"

		echo "Done."

		# Synchronise all worlds in RAM to disk
		server_worlds_to_disk "$1"
	else
		echo "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Restarts a single server after a delay
# $1: The ID of the server
server_restart() {
	server_property "$1" MESSAGE_RESTART
	server_property "$1" RESTART_DELAY

	# Restarts the server if it is already running
	if server_is_running "$1"; then
		# Change the state of the script
		RESTART_COUNTDOWN[$1]="true"

	    server_eval "$1" "say ${SERVER_MESSAGE_RESTART[$1]}"
	    echo "Issued the warning \"${SERVER_MESSAGE_RESTART[$1]}\" to players."

		echo -n "Restarting... "

		for ((i="${SERVER_RESTART_DELAY[$1]}"; i>0; i--)); do
			tput sc # Save cursor position
			echo -n "in $i seconds."
			sleep 1

			tput rc # Restore cursor to position of last `sc'
			tput el # Clear to end of line
		done

		echo -e "Now."

		server_stop_now "$1"
	fi

	server_start "$1"
}

# Restarts a single server right away
# $1: The ID of the server
server_restart_now() {
	# Restarts the server if it is already running
	if server_is_running "$1"; then
		server_stop_now "$1"
	fi

	server_start "$1"
}

# List the worlds available for a server
# $1: The ID of the server
server_worlds_list() {
	if [[ "${SERVER_NUM_WORLDS[$1]}" -eq 0 ]]; then
		echo "There are no worlds in worldstorage."
		return 0
	fi

	local i="${SERVER_WORLD_OFFSET[$1]}"
	local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))"

	# For each of the servers worlds:
	for ((i=$i; i<$max; i++)); do
		world_property "$i" INRAM
		if "${WORLD_INRAM[$i]}"; then
			echo "RAM  ${WORLD_NAME[$i]}"
		else
			echo "     ${WORLD_NAME[$i]}"
		fi
	done
}

# Backs up the worlds for a server
# $1: The ID of the server
server_worlds_backup() {
	local i="${SERVER_WORLD_OFFSET[$1]}"
	local max="$(( $i + ${SERVER_NUM_WORLDS[$1]} ))"

	# For each of the servers worlds:
	for ((i=$i; i<$max; i++)); do
		world_property "$i" STATUS
		if [[ "${WORLD_STATUS[$i]}" == "active" ]]; then
			world_backup "$i"
		fi
	done
}

# Moves a servers log into another file, leaving the original log file empty
# $1: The ID of the server
server_log_roll() {
	server_property "$1" LOG_PATH
	server_property "$1" USERNAME
	server_property "$1" LOG_ARCHIVE_PATH

	# Moves and Gzips the logfile, a big log file slows down the
	# server A LOT

	# Creates the server log if not already present. Prevents errors.
	as_user "${SERVER_USERNAME[$1]}" "touch \"${SERVER_LOG_PATH[$1]}\""

	local log_lines="$(cat "${SERVER_LOG_PATH[$1]}" | wc -l )"

	if [ "$log_lines" -le '1' ]; then
		echo "No new log entries to roll. No change made."
		return 0
	fi

	echo -n "Rolling server logs... "

	if [ -e "${SERVER_LOG_PATH[$1]}" ]; then
		file_name="${SERVER_NAME[$1]}-$(date +%F-%H-%M-%S).log"
		as_user "${SERVER_USERNAME[$1]}" "mkdir -p \"${SERVER_LOG_ARCHIVE_PATH[$1]}\" && cp \"${SERVER_LOG_PATH[$1]}\" \"${SERVER_LOG_ARCHIVE_PATH[$1]}/${file_name}\" && gzip \"${SERVER_LOG_ARCHIVE_PATH[$1]}/${file_name}\""

		if [ -e "${SERVER_LOG_ARCHIVE_PATH[$1]}/${file_name}.gz" ]; then
			as_user "${SERVER_USERNAME[$1]}" "cp \"/dev/null\" \"${SERVER_LOG_PATH[$1]}\""
			as_user "${SERVER_USERNAME[$1]}" "echo \"Previous logs can be found at \\\"${SERVER_LOG_ARCHIVE_PATH[$1]}\\\"\" > \"${SERVER_LOG_PATH[$1]}\""
		else
			echo "Failed."
			error_exit LOGS_NOT_ROLLED "Logs were not rolled."
		fi
	fi

	echo "Done."
}

# Backups a server's directory
# $1: The ID of the server
server_backup() {
	manager_property SERVER_STORAGE_PATH
	server_property "$1" COMPLETE_BACKUP_FOLLOW_SYMLINKS
	server_property "$1" BACKUP_PATH
	server_property "$1" USERNAME

	echo -n "Backing up the entire server directory... "

	zip_flags="-rq"
	# Add the "y" flag if symbolic links should not be followed
	if [ "${SERVER_COMPLETE_BACKUP_FOLLOW_SYMLINKS[$1]}" != "true" ]; then
		zip_flags="${zip_flags}y"
	fi

	# Zip up the server directory
	file_name="${SERVER_BACKUP_PATH[$1]}/$(date "+%F-%H-%M-%S").zip"
	as_user "${SERVER_USERNAME[$1]}" "mkdir -p \"${SERVER_BACKUP_PATH[$1]}\" && cd \"$SETTINGS_SERVER_STORAGE_PATH\" && zip ${zip_flags} \"${file_name}\" \"${SERVER_NAME[$1]}\""

	echo "Done."
}

# Sets a server's jar file
# $1: The ID of the server
# $2: The name of the jar group
# $3: Optionally, a specific jar to use.
server_set_jar() {
	manager_property JAR_STORAGE_PATH
	server_property "$1" JAR_PATH
	server_property "$1" USERNAME

	if [ -d "$SETTINGS_JAR_STORAGE_PATH/$2" ]; then

		if [ -z "$3" ]; then
			# If a specific jar file is not mentioned

			# Download the latest version
			jargroup_getlatest "$2"
			get_latest_file "$SETTINGS_JAR_STORAGE_PATH/$2"
			local jar="$RETURN"
		else
			# If a specific jar IS mentioned use that
			local jar="$SETTINGS_JAR_STORAGE_PATH/$2/$3"

			if [[ ! -e "$jar" ]]; then
				error_exit NAME_NOT_FOUND "There is no jar named \"$3\" in jargroup \"$2\"."
			fi
		fi

		if [[ ! -z "$jar" ]]; then
			as_user "${SERVER_USERNAME[$1]}" "ln -sf \"$jar\" \"${SERVER_JAR_PATH[$1]}\""
			echo "Server \"${SERVER_NAME[$1]}\" is now using \"$jar\"."
		fi
	else
		error_exit NAME_NOT_FOUND "There is no jargroup named \"$2\"."
	fi
}

# Lists the players currently connected to a server
# $1: The ID of the server
server_connected() {
	if server_is_running "$1"; then
		server_command "$1" CONNECTED
		echo_fallback "$RETURN" "No players are connected."
	else
		echo "Server \"${SERVER_NAME[$1]}\" is not running. No users are connected."
	fi
}

# Sets the value of a server property
# $1: The ID of the server
# $2: The name of the server property
# $3: The value for the property
server_set_property() {
	eval SERVER_$2[$1]=\"$3\"
	### Changes to values before setting
	case "$2" in
		*_PATH)
			if [[ ! "$3" =~ ^/.+ ]]; then
				server_property "$1" PATH
				eval SERVER_$2[$1]=\"${SERVER_PATH[$1]}/$3\"
			fi
			;;
		SCREEN_NAME)
			eval SERVER_$2[$1]=\"${SERVER_SCREEN_NAME[$1]//\{SERVER_NAME\}/${SERVER_NAME[$1]}}\"
			;;
		MESSAGE_STOP)
			server_property "$1" STOP_DELAY
			eval SERVER_$2[$1]=\"${SERVER_MESSAGE_STOP[$1]//\{DELAY\}/${SERVER_STOP_DELAY[$1]}}\"
			;;
		MESSAGE_RESTART)
			server_property "$1" RESTART_DELAY
			eval SERVER_$2[$1]=\"${SERVER_MESSAGE_RESTART[$1]//\{DELAY\}/${SERVER_RESTART_DELAY[$1]}}\"
			;;
		INVOCATION)
			server_property "$1" RAM
			server_property "$1" JAR_PATH
			eval SERVER_$2[$1]=\"${SERVER_INVOCATION[$1]//\{RAM\}/${SERVER_RAM[$1]}}\"
			eval SERVER_$2[$1]=\"${SERVER_INVOCATION[$1]//\{JAR\}/${SERVER_JAR_PATH[$1]}}\"
			;;
	esac
}

# Get the value of a server property
# $1: The ID of the server
# $2: The name of the server property
server_property() {
	local versionable_properties="LOG_PATH;WHITELIST_PATH;BANNED_PLAYERS_PATH;BANNED_IPS_PATH;OPS_PATH;OPS_LIST;"

	# Do nothing if we want to load a property handled
	# by a versioning file that is already loaded.
	if [[ "$2" =~ ^CONSOLE_ ]] && [ "${SERVER_VERSIONING_LOADED[$1]}" == "true" ]; then
		return 0
	fi

	eval local value=\"\${SERVER_$2[$1]}\"

	if [ -z "$value" ]; then
		# If the value is empty it has not been loaded yet

		# These properties are not overridable
		case "$2" in
			NAME|PATH)
				# Defined at allocation
				return 0
				;;
			CONF)
				manager_property SERVER_PROPERTIES
				server_set_property "$1" "$2" "${SERVER_PATH[$1]}/$SETTINGS_SERVER_PROPERTIES"
				return 0
				;;
			VERSION_CONF)
				manager_property VERSIONING_STORAGE_PATH
				server_property "$1" VERSION
				get_closest_version "${SERVER_VERSION[$1]}"
				local version="$RETURN"

				if [[ "$version" == "unknown" ]]; then
					# Use the latest Minecraft version if there is no explicit setting
					if [[ -z "${VERSIONS_NEWEST_MINECRAFT_PATH}" ]]; then
						msm_warning "No version set for server, and no default found. Please use 'msm update' to download defaults"
					else
						if [[ ! "$arg" =~ logroll|config ]]; then
							msm_info "Assuming 'minecraft/${VERSIONS_NEWEST_MINECRAFT_VERSION}' for this server. You should override this value by adding 'msm-version=minecraft/x.x.x' to '${SERVER_CONF[$1]}' to make this message go away"
						fi
						SERVER_VERSION_CONF[$1]="${VERSIONS_NEWEST_MINECRAFT_PATH}"
					fi
				else
					SERVER_VERSION_CONF[$1]="${SETTINGS_VERSIONING_STORAGE_PATH}/${version}.${SETTINGS_VERSIONING_FILE_EXTENSION}"
				fi
				return 0
				;;
			BACKUP_PATH)
				manager_property BACKUP_ARCHIVE_PATH
				server_set_property "$1" "$2" "$SETTINGS_BACKUP_ARCHIVE_PATH/${SERVER_NAME[$1]}"
				return 0
				;;
			LOG_ARCHIVE_PATH)
				manager_property LOG_ARCHIVE_PATH
				server_set_property "$1" "$2" "$SETTINGS_LOG_ARCHIVE_PATH/${SERVER_NAME[$1]}"
				return 0
				;;
			ACTIVE)
				server_property "$1" FLAG_ACTIVE_PATH
				if [[ -e "${SERVER_FLAG_ACTIVE_PATH[$1]}" ]]; then
					server_set_property "$1" "$2" "true"
				else
					server_set_property "$1" "$2" "false"
				fi
				return 0
				;;
		esac

		# If its a command lookup or server path, load from versioning files
		if [[ "$2" =~ ^CONSOLE_ ]] || [[ "$versionable_properties" == *"$2;"* ]]; then
			server_property "$1" VERSION_CONF

			if [[ -f "${SERVER_VERSION_CONF[$1]}" ]]; then
				VERSIONING_SERVER_ID="$1"
				source "${SERVER_VERSION_CONF[$1]}"
				unset VERSIONING_SERVER_ID
				SERVER_VERSIONING_LOADED[$1]="true"
			fi

			if [[ "$2" =~ ^CONSOLE_ ]]; then
				return 0
			fi
		fi

		# If not a non-overridable load from conf
		read_server_conf "$1" "$2"

		local target_varname=SERVER_$2[$1]
		if [ -z ${!target_varname} ]; then
			# if its still empty use the default value
			manager_property "DEFAULT_$2"
			server_set_property "$1" "$2" "\$SETTINGS_DEFAULT_$2"
		fi
	fi
}

#Checks server config for overriding value
# $1: The ID of the server
# $2: The name of the server property
read_server_conf(){
	server_property "$1" CONF

	to_properties_name "$2"
	local name="$RETURN"

	if [[ "$name" =~ ^properties\-(.*)$ ]]; then
		name="${BASH_REMATCH[1]}"
	else
		name="msm-$name"
	fi

	local from_conf="$(sed -rn "s/^$name=('|\"|)(.*)\1/\2/ip" "${SERVER_CONF[$1]}" | tail -n 1)"

	if [ ! -z "$from_conf" ]; then
		# If the value is found in the server conf file (server.properties)
		# then set that as value for the property
		server_set_property "$1" "$2" "$from_conf"
	fi
}

# $1: The server ID
server_dirty_properties() {
	local index

	# Removes properties for all servers if an index
	# is not specified
	if [ ! -z "$1" ] && [[ "$1" -ge 0 ]]; then
		index="[$1]"
	else
		index=""
	fi

	for ((i=0; i<$SERVER_SETTING_COUNT; i++)); do
		eval unset SERVER_${SERVER_SETTING_NAME[$i]}$index
	done

	unset SERVER_CONF$index
	unset SERVER_BACKUP_PATH$index
	unset SERVER_LOG_ARCHIVE_PATH$index
	unset SERVER_ACTIVE$index
}










### Manager Functions
### -----------------

# Stops all running servers after a servers specified delay
# $1: String containing "stop" or "restart". Represents whether the stop is
#     with a mind to stop the server, or just to restart it. And affects
#     the message issued to players on a server.
manager_stop_all_servers() {
	# An array of true/false for each server
	local was_running
	# False if no servers were running at all
	local any_running="false"

	# For all running servers issue the stop warning
	local max_countdown=0

	for ((server=0; server<${NUM_SERVERS}; server++)); do
		if server_is_running "$server"; then
			any_running="true"
			was_running[$server]="true"
			STOP_COUNTDOWN[$server]="true"

			server_property "$server" STOP_DELAY
			server_property "$server" MESSAGE_STOP
			server_property "$server" MESSAGE_RESTART

			if [[ "${SERVER_STOP_DELAY[$server]}" -gt "$max_countdown" ]]; then
				max_countdown="${SERVER_STOP_DELAY[$server]}"
			fi

			# Send a warning message to the server
			case "$1" in
				stop) server_eval "$server" "say ${SERVER_MESSAGE_STOP[$server]}";;
				restart) server_eval "$server" "say ${SERVER_MESSAGE_RESTART[$server]}";;
			esac

			# Send message to stdout
			echo "Server \"${SERVER_NAME[$server]}\" was running, now stopping:"

			case "$1" in
				stop) echo "    Issued the warning \"${SERVER_MESSAGE_STOP[$server]}\" to players.";;
				restart) echo "    Issued the warning \"${SERVER_MESSAGE_RESTART[$server]}\" to players.";;
			esac

			case "${SERVER_STOP_DELAY[$server]}" in
				0) echo "    Stopping without delay.";;
				1) echo "    Stopping after 1 second.";;
				*) echo "    Stopping after ${SERVER_STOP_DELAY[$server]} seconds.";;
			esac
		else
			echo "Server \"${SERVER_NAME[$server]}\" was NOT running."
			was_running[$server]="false"
		fi
	done

	if "$any_running"; then
		# Wait for the maximum possible delay, stopping servers
		# at the correct times
		echo -n "All servers will have been issued the stop command... "
		for ((tick="${max_countdown}"; tick>=0; tick--)); do
			tput sc # Save cursor position

			if [[ "$tick" -le 1 ]]; then
				echo -n "in $tick second."
			else
				echo -n "in $tick seconds."
			fi

			# Each second check all servers, to see if it's their time to
			# stop. If so issue the stop command, and don't wait.
			for ((server=0; server<${NUM_SERVERS}; server++)); do
				if server_is_running "$server"; then
					stop_tick="$(( ${max_countdown} - ${SERVER_STOP_DELAY[$server]} ))"
					if [[ "$stop_tick" == "$tick" ]]; then
						server_eval "$server" "stop"
						STOP_COUNTDOWN[$server]="false"
					fi
				fi
			done

			if [[ "$tick" > 0 ]]; then
				sleep 1
			fi

			tput rc # Restore cursor to position of last `sc'
			tput el # Clear to end of line
		done

		# Start a new line
		echo "Now."

		# Finally check all servers have stopped
		for ((server=0; server<${NUM_SERVERS}; server++)); do
			if "${was_running[$server]}"; then
				echo -n "Ensuring server \"${SERVER_NAME[$server]}\" has stopped... "
				server_wait_for_stop "$server"
				echo "Done."
			fi
		done
	else
		echo "No servers were running."
	fi
}

# Stops all running servers without delay
manager_stop_all_servers_now() {
	# An array of true/false for each server
	local was_running
	# False if no servers were running at all
	local any_running="false"

	# Stop all servers at the same time
	for ((server=0; server<${NUM_SERVERS}; server++)); do
		if server_is_running "$server"; then
			any_running="true"
			was_running[$server]="true"

			echo "Server \"${SERVER_NAME[$server]}\" was running, now stopping."
			server_eval "$server" "stop"
		else
			echo "Server \"${SERVER_NAME[$server]}\" was NOT running."
			was_running[$server]="false"
		fi
	done

	if "$any_running"; then
		# Ensure all the servers have stopped
		for ((server=0; server<${NUM_SERVERS}; server++)); do
			if "${was_running[$server]}"; then
				echo -n "Ensuring server \"${SERVER_NAME[$server]}\" has stopped... "
				server_wait_for_stop "$server"
				echo "Done."
			fi
		done
	else
		echo "No servers were running."
	fi
}

# Get the value of a global manager property
# $1: The name of the property
manager_property() {
	local from_conf="$(sed -rn "s/^$1=('|\"|)(.*)\1/\2/ip" "$CONF" | tail -n 1)"

	# If this property has not yet been loaded, load it:
	eval local loaded=\"\$LOADED_$1\"
	if [ ! -z "$loaded" ] && ! "$loaded"; then
		if [ ! -z "$from_conf" ]; then
			# Override the default value
			eval SETTINGS_$1=\"$from_conf\"
		fi

		# State that this property has now been loaded
		eval LOADED_$1=\"true\"
	fi
}

manager_dirty_properties() {
	for ((i=0; i<$SETTING_COUNT; i++)); do
		eval LOADED_${SETTING_NAME[$i]}=\"false\"
	done
}

manager_dirty_all() {
	manager_dirty_properties
	server_dirty_properties
	world_dirty_properties
}










### Command Handler Functions
### -------------------------


# Starts all servers
command_start() {
	# Required start option, for debian init.d scripts
	for ((server=0; server<${NUM_SERVERS}; server++)); do

		server_property "$server" ACTIVE

		# Only starts active servers
		if "${SERVER_ACTIVE[$server]}"; then
			if server_is_running "$server"; then
				echo "[ACTIVE] Server \"${SERVER_NAME[$server]}\" already started."
			else
				echo "[ACTIVE] Server \"${SERVER_NAME[$server]}\" starting:"
				server_start "$server"
			fi
		else
			if server_is_running "$server"; then
				echo "[INACTIVE] Server \"${SERVER_NAME[$server]}\" already started. It should not be running! Use \"$0 ${SERVER_NAME[$server]} stop\" to stop this server."
			else
				echo "[INACTIVE] Server \"${SERVER_NAME[$server]}\" leaving stopped, as this server is inactive."
			fi
		fi
	done
}

# Stops all servers after a delay
command_stop() {
	manager_stop_all_servers "stop"
}

# Stops all servers without delay
command_stop_now() {
	manager_stop_all_servers_now
}

# Restarts all servers
command_restart() {
	echo "Stopping servers:"
	manager_stop_all_servers "restart"

	echo "Starting servers:"
	command_start
}

# Restarts all servers without delay
command_restart_now() {
	echo "Stopping servers:"
	manager_stop_all_servers_now

	echo "Starting servers:"
	command_start
}

# Displays the MSM version
command_version() {
	local version="$VERSION"

	if [ "${version:0:1}" -eq 0 ]; then
		version="$version Beta"
	fi

	echo "Minecraft Server Manager $version"
}

# Displays config values used by MSM
command_config() {
	for ((i=0; i<$SETTING_COUNT; i++)); do
		manager_property "${SETTING_NAME[$i]}"
		echo -n "${SETTING_NAME[$i]}=\""
		eval echo -n \"\$SETTINGS_${SETTING_NAME[$i]}\"
		echo '"'
	done
}

# Downloads latest versions of all MSM files
command_update() {
	echo -n "Checking for updates to version ${VERSION}..."

	local any_files_updated="false"

	# Check flags, semi-colon ';' delimits flags for example
	# COMMAND_FLAGS could contain ";--noinput;--quiet;-q;-ni;"
	if [[ "$COMMAND_FLAGS" =~ \;--noinput\; ]]; then
		local noinput="true"
	fi

	manager_property UPDATE_URL
	manager_property USERNAME

	# Create the temp download directory
	local output_dir="/tmp/msmupdate"

	# Clean up the temp directory created for downloads
	cleanup() {
		as_user "$SETTINGS_USERNAME" "rm -rf \"${output_dir}\""
	}

	# Remove the directory if it exists already
	cleanup

	# $1: The file name to download
	download_file() {
		local dir_name="$(dirname "${output_dir}/${1}")"
		as_user "${SETTINGS_USERNAME}" "mkdir -p \"${dir_name}\""
		as_user "${SETTINGS_USERNAME}" "wget --quiet --no-check-certificate ${SETTINGS_UPDATE_URL}/$1 -O ${output_dir}/$1"
	}

	# $1: The newly download file (relative to download dir)
	# $2: The current file that may be overwritten
	# $RETURN: The "current file" path if it should be overwritten
	#          since it is different to the new version
	compare_file() {
		unset RETURN
		local new_file

		# Make relative URLs absolute, using the download dir
		if [[ "$1" =~ ^/ ]]; then
			new_file="$1"
		else
			new_file="${output_dir}/$1"
		fi

		# If the new file path is wrong return
		[ ! -e "$new_file" ] && return 1

		if [ -e "$2" ]; then
			if diff -q "$new_file" "$2" >/dev/null 2>/dev/null; then
				return 1
			else
				RETURN="$2"
			fi
		fi
	}

	# Download the latest MSM script and check its version number
	download_file "init/msm"
	local latest_version="$(sed -rn "s/^VERSION=('|\"|)(.*)\1/\2/ip" "${output_dir}/init/msm" | tail -n 1)"

	# Download the other files if that version is different (implicitly better) to the current version
	if [[ "$VERSION" == "$latest_version" ]]; then
		echo " Already at latest version."
	else
		echo " $latest_version is available."
	fi


	### BEGIN Fancy warnings


	echo -n "Checking if any files need to be updated..."
	download_file "bash_completion/msm"
	download_file "versioning/versions.txt"

	# Downloads all versioning files in the latest MSM version
	download_upstream_versions() {
		manager_property VERSIONING_FILE_EXTENSION

		while read line; do
			if [[ "$line" =~ ^([^#]{1}.*)$ ]]; then
				download_file "versioning/${BASH_REMATCH[1]}.${SETTINGS_VERSIONING_FILE_EXTENSION}"
			fi
		done < "${output_dir}/versioning/versions.txt"
	}

	# $returns: 0 if at least one file needs updating, 1 otherwise
	files_need_updating() {
		compare_file "bash_completion/msm" "$COMPLETION"
		[ ! -z "$RETURN" ] && return 0

		compare_file "init/msm" "$SCRIPT"
		[ ! -z "$RETURN" ] && return 0

		manager_property VERSIONING_STORAGE_PATH
		local version_name regex
		regex="/(([^/]+/[^/]+)\.[^/\.]*)$"
		while IFS= read -r -d $'\0' path; do
			if [[ "$path" =~ $regex ]]; then
				version_name="${BASH_REMATCH[1]}"
				version_name_without_ext="${BASH_REMATCH[2]}"
				compare_file "versioning/$version_name" "${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name_without_ext}.${SETTINGS_VERSIONING_FILE_EXTENSION}"
				[ ! -z "$RETURN" ] && return 0
			fi
		done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0)

		return 1
	}

	files_need_creating() {
		[ ! -e "$COMPLETION" ] && return 0
		[ ! -e "$SCRIPT" ] && return 0

		manager_property VERSIONING_STORAGE_PATH
		local version_name
		while IFS= read -r -d $'\0' path; do
			if [[ "$path" =~ /([^/]+/[^/]+)\.[^/\.]*$ ]]; then
				version_name_without_ext="${BASH_REMATCH[1]}"
				[ ! -e "${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name_without_ext}.${SETTINGS_VERSIONING_FILE_EXTENSION}" ] && return 0
			fi
		done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0)

		return 1
	}

	download_upstream_versions

	local updating="false"
	local creating="false"

	files_need_updating && updating="true"
	files_need_creating && creating="true"

	if [[ "$updating" == "false" ]] && [[ "$creating" == "false" ]]; then
		echo " No. We're all done."
		return 0
	else
		echo " Done."
	fi

	if [[ "$updating" == "true" ]]; then
		echo "Updating will overwrite the following files:"

		compare_file "init/msm" "$SCRIPT"
		[ ! -z "$RETURN" ] && echo "  > The main MSM script:        $SCRIPT"

		compare_file "bash_completion/msm" "$COMPLETION"
		[ ! -z "$RETURN" ] && echo "  > The bash completion script: $COMPLETION"

		manager_property VERSIONING_STORAGE_PATH
		local version_name version_path regex
		regex="/(([^/]+/[^/]+)\.[^/\.]*)$"
		while IFS= read -r -d $'\0' path; do
			if [[ "$path" =~ $regex ]]; then
				version_name="${BASH_REMATCH[1]}"
				version_name_without_ext="${BASH_REMATCH[2]}"

				version_path="${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name_without_ext}.${SETTINGS_VERSIONING_FILE_EXTENSION}"
				compare_file "versioning/$version_name" "$version_path"
				[ ! -z "$RETURN" ] && echo "  > Version file:               $version_path"
			fi
		done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0)
	fi

	if [[ "$creating" == "true" ]]; then
		echo "Updating will create the following files:"

		[ ! -e "$SCRIPT" ] && echo "  > The main MSM script:        $SCRIPT"
		[ ! -e "$COMPLETION" ] && echo "  > The bash completion script: $COMPLETION"

		manager_property VERSIONING_STORAGE_PATH

		local version_name version_path
		while IFS= read -r -d $'\0' path; do
			if [[ "$path" =~ /([^/]+/[^/]+)\.[^/\.]*$ ]]; then
				version_name="${BASH_REMATCH[1]}"
				version_path="${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name}.${SETTINGS_VERSIONING_FILE_EXTENSION}"
				[ ! -e "$version_path" ] && echo "  > Version file:               $version_path"
			fi
		done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0)
	fi


	### END Fancy warnings


	if [[ ! "$noinput" ]]; then
		echo -n "Do you want to continue [y/N]: "
		read answer
	else
		answer="y"
	fi

	if [[ "$answer" =~ ^(y|Y|yes)$ ]]; then
		echo "Updating MSM to ${latest_version}:"

		# Overwrite bash completion file
		local created="false"
		compare_file "bash_completion/msm" "$COMPLETION"
		if [ ! -z "$RETURN" ] || [ ! -e "$COMPLETION" ]; then
			[ ! -e "$COMPLETION" ] && created="true"

			any_files_updated="true"

			local dir="$(dirname "$COMPLETION")"
			as_user "root" "mkdir -p \"${dir}\""
			as_user "root" "mv -f \"${output_dir}/bash_completion/msm\" \"$COMPLETION\""
			source "$COMPLETION"

			if "$created"; then
				echo "  > Created: $COMPLETION"
			else
				echo "  > Updated: $COMPLETION"
			fi
		fi

		# Overwrite the MSM script itself
		created="false"
		compare_file "init/msm" "$SCRIPT"
		if [ ! -z "$RETURN" ] || [ ! -e "$SCRIPT" ]; then
			[ ! -e "$SCRIPT" ] && created="true"

			any_files_updated="true"

			dir="$(dirname "$SCRIPT")"
			as_user "root" "mkdir -p \"${dir}\""
			as_user "root" "mv -f \"${output_dir}/init/msm\" \"$SCRIPT\""
			as_user "root" "chmod +x \"$SCRIPT\""

			if "$created"; then
				echo "  > Created: $SCRIPT"
			else
				echo "  > Updated: $SCRIPT"
			fi
		fi

		# Overwrite the versioning files
		manager_property VERSIONING_STORAGE_PATH

		local version_name version_path regex
		regex="/(([^/]+/[^/]+)\.[^/\.]*)$"
		while IFS= read -r -d $'\0' path; do
			created="false"
			if [[ "$path" =~ $regex ]]; then
				version_name="${BASH_REMATCH[1]}"
				version_name_without_ext="${BASH_REMATCH[2]}"
				version_path="${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name_without_ext}.${SETTINGS_VERSIONING_FILE_EXTENSION}"

				compare_file "${output_dir}/versioning/$version_name" "$version_path"
				if [ ! -z "$RETURN" ] || [ ! -e "$version_path" ]; then
					[ ! -e "$version_path" ] && created="true"

					any_files_updated="true"

					dir="$(dirname ${SETTINGS_VERSIONING_STORAGE_PATH}/${version_name})"
					as_user "root" "mkdir -p \"${dir}\""
					as_user "root" "mv -f \"$path\" \"$version_path\""
					as_user "root" "chmod +x \"$version_path\""
					as_user "root" "chown ${SETTINGS_USERNAME}:${SETTINGS_USERNAME} \"$version_path\""

					if "$created"; then
						echo "  > Created: $version_path"
					else
						echo "  > Updated: $version_path"
					fi
				fi
			fi
		done < <(find "${output_dir}/versioning" -mindepth 1 -name \*.${SETTINGS_VERSIONING_FILE_EXTENSION} -type f -print0)

		echo "Done."
	else
		echo "MSM was not updated."
	fi

	cleanup

	# This script will now be replaced. So run the new script's
	# update code, in case there are new things to update that
	# this version of MSM does not know about yet.
	if [[ "$any_files_updated" == "true" ]]; then
		$0 update
	fi
}

# Displays a list of servers
command_server_list() {
	server_list
}

# Creates a new server with name $1
# $1: The new (valid) server name
command_server_create() {
	server_create "$1"
}

# Deletes an existing server with name $1
# $1: The name of the existing server
command_server_delete() {
	server_delete "$1"
}

# Renames an existing server
# $1: The existing server name
# $2: The new (valid) server name
command_server_rename() {
	server_rename "$1" "$2"
}

# Displays a list of all jar's in jar groups
command_jargroup_list() {
	jargroup_list
}

# Creates a new jar group
# $1: The new (valid) jar group name
# $2: The URL to use as the jar group target
command_jargroup_create() {
	jargroup_create "$1" "$2"
}

# Deletes and existing jar group
# $1: The name of a jar group to delete
command_jargroup_delete() {
	jargroup_delete "$1"
}

# Renames an existing jar group
# $1: The name of the existing jar group
# $2: The new (valid) name for the jar group
command_jargroup_rename() {
	jargroup_rename "$1" "$2"
}

# Changes a jar group's target url for automatic downloads
# $1: The jar group name
# $2: The new URL to use
command_jargroup_changeurl() {
	jargroup_changeurl "$1" "$2"
}

# Downloads the latest jar for a jar group
# $1: The name of the jar group
command_jargroup_getlatest() {
	jargroup_getlatest "$1"
}

# Displays a list of possible commands and help strings
command_help() {
	# Outputs a list of all commands
	echo -e "Usage: $0 command:"
	echo -e
	echo -e "--Setup Commands------------------------------------------------"
	echo -e "  server list                                   List servers"
	echo -e "  server create <name>                          Creates a new Minecraft server"
	echo -e "  server delete <name>                          Deletes an existing Minecraft server"
	echo -e "  server rename <name> <new-name>               Renames an existing Minecraft server"
	echo -e
	echo -e "--Server Management Commands------------------------------------"
	echo -e "  <server> start                                Starts a server"
	echo -e "  <server> stop [now]                           Stops a server after warning players, or right now"
	echo -e "  <server> restart [now]                        Restarts a server after warning players, or right now"
	echo -e "  <server> status                               Show the running/stopped status of a server"
	echo -e "  <server> connected                            List a servers connected players"
	echo -e "  <server> worlds list                          Lists the worlds a server has"
	echo -e "  <server> worlds load                          Creates links to worlds in storage for a server"
	echo -e "  <server> worlds ram <world>                   Toggles a world's \"in RAM\" status"
	echo -e "  <server> worlds todisk                        Synchronises any \"in RAM\" worlds to disk a server has"
	echo -e "  <server> worlds backup                        Makes a backup of all worlds a server has"
	echo -e "  <server> worlds on|off <world>                Activate or deactivate a world, inactive worlds are not backed up"
	echo -e "  <server> logroll                              Move a server log to a gziped archive, to reduce lag"
	echo -e "  <server> backup                               Makes a backup of an entire server directory"
	echo -e "  <server> jar <jargroup> [<file>]              Sets a server's jar file"
	echo -e "  <server> console                              Connects to the interactive console. Access may be limited"
	echo -e "  <server> config [<setting> <value>]           Lists server settings, or sets a specific setting."
	echo -e
	echo -e "--Server Pass Through Commands----------------------------------"
	echo -e "  <server> wl on|off                            Enables/disables server whitelist checking"
	echo -e "  <server> wl add|remove <player>               Add/remove a player to/from a server's whitelist"
	echo -e "  <server> wl list                              List the players whitelisted for a server"
	echo -e "  <server> bl player add|remove <player>        Ban/pardon a player from/for a server"
	echo -e "  <server> bl ip add|remove <ip address>        Ban/pardon an IP address from/for a server"
	echo -e "  <server> bl list                              Lists the banned players and IP address for a server"
	echo -e "  <server> op add|remove <player>               Add/remove operator status for a player on a server"
	echo -e "  <server> op list                              Lists the operator players for a server"
	echo -e "  <server> gm survival|creative <player>        Change the game mode for a player on a server"
	echo -e "  <server> kick <player>                        Forcibly disconnect a player from a server"
	echo -e "  <server> say <message>                        Broadcast a (pink) message to all players on a server"
	echo -e "  <server> time set|add <number>                Set/increment time on a server (0-24000)"
	echo -e "  <server> toggledownfall                       Toggles rain and snow on a server"
	echo -e "  <server> give <player> <item> [amount] [data] Gives an entity to a player"
	echo -e "  <server> xp <player> <amount>                 Gives XP to, or takes away (when negative) XP from, a player"
	echo -e "  <server> save on|off                          Enable/disable writing world changes to file"
	echo -e "  <server> save all                             Force the writing of all non-saved world changes to file"
	echo -e "  <server> cmd <command>                        Send a command string to the server and return"
	echo -e "  <server> cmdlog <command>                     Same as 'cmd' but shows log output afterwards (Ctrl+C to exit)"
	echo -e
	echo -e "--Jar Commands--------------------------------------------------"
	echo -e "  jargroup list                                 List the stored jar files."
	echo -e "  jargroup create <name> <download-url>         Create a new jar group, with a URL for new downloads"
	echo -e "  jargroup delete <name>                        Delete a jar group"
	echo -e "  jargroup rename <name> <new-name>             Rename a jar group"
	echo -e "  jargroup changeurl <name> <download-url>      Change the download URL for a jar group"
	echo -e "  jargroup getlatest <name>                     Download the latest jar file for a jar group"
	echo -e
	echo -e "--Global Commands-----------------------------------------------"
	echo -e "  start                                         Starts all active servers"
	echo -e "  stop [now]                                    Stops all running servers"
	echo -e "  restart [now]                                 Restarts all active servers"
	echo -e "  version                                       Prints the Minecraft Server Manager version installed"
	echo -e "  config                                        Displays a list of the config values used by MSM"
	echo -e "  update [--noinput]                            Replaces MSM files with the latest recommended versions"
}

# Starts an individual server
# $1: The server ID
command_server_start() {
	server_set_active "$1" "active"
	server_start "$1"
}

# Stops an individual server after a delay
# $1: The server ID
command_server_stop() {
	server_set_active "$1" "inactive"
	server_stop "$1"
}

# Stops an individual server without delay
# $1: The server ID
command_server_stop_now() {
	server_set_active "$1" "inactive"
	server_stop_now "$1"
}

# Restarts an individual server after a delay
# $1: The server ID
command_server_restart() {
	server_set_active "$1" "active"
	server_restart "$1"
}

# Restarts an individual server without delay
# $1: The server ID
command_server_restart_now() {
	server_set_active "$1" "active"
	server_restart_now "$1"
}

# Displays the running/stopped status of an individual server
# $1: The server ID
command_server_status() {
	if server_is_running "$1"; then
		echo "Server \"${SERVER_NAME[$1]}\" is running."
	else
		echo "Server \"${SERVER_NAME[$1]}\" is stopped."
	fi
}

# Displays a list of connected players for an individual server
# $1: The server ID
command_server_connected() {
	server_connected "$1"
}

# Displays a list of worlds for an individual server
# $1: The server ID
command_server_worlds_list() {
	server_worlds_list "$1"
}

# Creates symlinks for all active worlds so they can be used by the Minecraft
# server when running
# $1: The server ID
command_server_worlds_load() {
	server_ensure_links "$1"
}

# Toggles a world's in ram status
# $1: The server ID
# $2: The world ID
command_server_worlds_ram() {
	if server_is_running "$1"; then
		error_exit SERVER_RUNNING "Server \"${SERVER_NAME[$1]}\" is running. Please stop the server before altering a worlds in-ram status."
	else
		world_toggle_ramdisk_state "$2"
	fi
}

# Synchronises all in ram worlds back to disk for an individual server
# $1: The server ID
command_server_worlds_todisk() {
	if server_is_running "$1"; then
		server_save_off "$1"
		server_save_all "$1"
	fi

	server_worlds_to_disk "$1"

	if server_is_running "$1"; then
		server_save_on "$1"
	fi
}

# Makes a backup of all worlds for an individual server
# $1: The server ID
command_server_worlds_backup() {
	if server_is_running "$1"; then
		server_property "$1" MESSAGE_WORLD_BACKUP_STARTED
		server_command "$1" SAY message="${SERVER_MESSAGE_WORLD_BACKUP_STARTED[$1]}"
		server_save_off "$1"
		server_save_all "$1"
	fi

	server_worlds_to_disk "$1"
	server_worlds_backup "$1"

	if server_is_running "$1"; then
		server_save_on "$1"
		server_property "$1" MESSAGE_WORLD_BACKUP_FINISHED
		server_command "$1" SAY message="${SERVER_MESSAGE_WORLD_BACKUP_FINISHED[$1]}"
	fi

	echo "Backup took $SECONDS seconds".
}

# Enables a world to be used by its server
# $1: The server ID
# $2: The world ID
command_server_worlds_on() {
	world_activate "$2"
}

# Disables a world from being used by its server, also prevents it from being
# backed up with the other worlds.
# $1: The server ID
# $2: The world ID
command_server_worlds_off() {
	world_deactivate "$2"
}

# Moves an individual server's log text to another file, leaving it empty
# $1: The server ID
command_server_logroll() {
	server_log_roll "$1"
}

# Makes a backup of an entire server directory
# $1: The server ID
command_server_backup() {
	if server_is_running "$1"; then
		server_eval "$1" "say ${SERVER_MESSAGE_COMPLETE_BACKUP_STARTED[$1]}"
		server_save_off "$1"
		server_save_all "$1"
	fi

	server_worlds_to_disk "$1"
	server_backup "$1"

	if server_is_running "$1"; then
		server_save_on "$1"
		server_eval "$1" "say ${SERVER_MESSAGE_COMPLETE_BACKUP_FINISHED[$1]}"
	fi

	echo "Backup took $SECONDS seconds".
}

# Sets an individual server's jar file to use when starting up
# $1: The server ID
# $2: The jar group name
# $3: Optionally a specific jar file name which exists within that jargroup, if
#     not provided the latest version will be used.
command_server_jar() {
	server_set_jar "$1" "$2" "$3"
}

# Turns a server's whitelist protection on
# $1: The server ID
command_server_whitelist_on() {
	if server_is_running "$1"; then
		server_command "$1" WHITELIST_ON
		echo_fallback "$RETURN" "Whitelist enabled."
	else
		command_server_config "$1" "white-list" "true"
	fi
}

# Turns a server's whitelist protection off
# $1: The server ID
command_server_whitelist_off() {
	if server_is_running "$1"; then
		server_command "$1" WHITELIST_OFF
		echo_fallback "$RETURN" "Whitelist disabled."
	else
		command_server_config "$1" "white-list" "false"
	fi
}

# Adds a player name to a server's whitelist
# $1: The server ID
# $2->: The player names
command_server_whitelist_add() {
	# TODO: Support whitelisting multiple players (see blacklist player add)
	if server_is_running "$1"; then
		# Whitelist players
		for player in "${@:2}"; do
			server_command "$1" WHITELIST_ADD player="$player"
			echo_fallback "$RETURN" "Player $player is now whitelisted."
		done
	else
		server_property "$1" WHITELIST_PATH

		for player in "${@:2}"; do
			if ! grep "^$player\$" "${SERVER_WHITELIST_PATH[$1]}" >/dev/null; then
				echo "$player" >> "${SERVER_WHITELIST_PATH[$1]}"
				echo_fallback "$RETURN" "Player $player is now whitelisted."
			fi
		done
	fi
}

# Removes a player name from a server's whitelist
# $1: The server ID
# $2->: The player names
command_server_whitelist_remove() {
	# TODO: Support multiple player names
	if server_is_running "$1"; then
		for player in "${@:2}"; do
			server_command "$1" WHITELIST_REMOVE player="$player"
			echo_fallback "$RETURN" "Player $player is no longer whitelisted."
		done
	else
		server_property "$1" WHITELIST_PATH

		for player in "${@:2}"; do
			sed -ri "/^$player\$/d" "${SERVER_WHITELIST_PATH[$1]}"
			echo_fallback "$RETURN" "Player $player is no longer whitelisted."
		done
	fi
}

# Displays a list of whitelisted players for an individual server
# $1: The server ID
command_server_whitelist_list() {
	server_property "$1" WHITELIST_PATH

	if [ -f "${SERVER_WHITELIST_PATH[$1]}" ]; then
		local players="$(cat "${SERVER_WHITELIST_PATH[$1]}")"

		if [ -z "$players" ]; then
			echo "No players are whitelisted."
		else
			echo "$players"
		fi
	else
		echo "No players are whitelisted."
	fi
}

# Adds player names to a server's ban list
# $1: The server ID
# $2->: The player names
command_server_blacklist_player_add() {
	if server_is_running "$1"; then
		for player in "${@:2}"; do
			server_command "$1" BLACKLIST_PLAYER_ADD player="$player"
			echo_fallback "$RETURN" "Player $player is now blacklisted."
		done
	else
		server_property "$1" BANNED_PLAYERS_PATH

		for player in "${@:2}"; do
			if ! grep "^$player\$" "${SERVER_BANNED_PLAYERS_PATH[$1]}" >/dev/null; then
				echo "$player" >> "${SERVER_BANNED_PLAYERS_PATH[$1]}"
				echo "Player $player is now blacklisted."
			fi
		done
	fi
}

# Removes player names from a server's ban list
# $1: The server ID
# $2->: The player names
command_server_blacklist_player_remove() {
	if server_is_running "$1"; then
		for player in "${@:2}"; do
			server_command "$1" BLACKLIST_PLAYER_REMOVE player="$player"
			echo_fallback "$RETURN" "Player $player is no longer blacklisted."
		done
	else
		server_property "$1" BANNED_PLAYERS_PATH

		for player in "${@:2}"; do
			sed -ri "/^$player\$/d" "${SERVER_BANNED_PLAYERS_PATH[$1]}"
			echo "Player $player is no longer blacklisted."
		done
	fi
}

# Adds ip addresses to a server's ban list
# $1: The server ID
# $2->: The ip addresses
command_server_blacklist_ip_add() {
	if server_is_running "$1"; then
		for address in "${@:2}"; do
			server_command "$1" BLACKLIST_IP_ADD address="$address"
			echo_fallback "$RETURN" "IP address $address is now blacklisted."
		done
	else
		server_property "$1" BANNED_IPS_PATH

		for address in "${@:2}"; do
			if ! grep "^$address\$" "${SERVER_BANNED_IPS_PATH[$1]}" >/dev/null; then
				echo "$address" >> "${SERVER_BANNED_IPS_PATH[$1]}"
				echo "IP address $address is now blacklisted."
			fi
		done
	fi
}

# Removes ip addresses to a server's ban list
# $1: The server ID
# $2->: The ip addresses
command_server_blacklist_ip_remove() {
	if server_is_running "$1"; then
		for address in "${@:2}"; do
			server_command "$1" BLACKLIST_IP_REMOVE address="$address"
			echo_fallback "$RETURN" "IP address $address is no longer blacklisted."
		done
	else
		server_property "$1" BANNED_PLAYERS_PATH

		for address in "${@:2}"; do
			sed -ri "/^$address\$/d" "${SERVER_BANNED_PLAYERS_PATH[$1]}"
			echo "IP address $address is no longer blacklisted."
		done
	fi
}

# Displays a server's banned player names and ip addresses
# $1: The server ID
command_server_blacklist_list() {
	server_property "$1" BANNED_PLAYERS_PATH
	server_property "$1" BANNED_IPS_PATH

	local players
	local ips

	if [ -f "${SERVER_BANNED_PLAYERS_PATH[$1]}" ]; then
		players="$(cat "${SERVER_BANNED_PLAYERS_PATH[$1]}")"
	fi

	if [ -f "${SERVER_BANNED_IPS_PATH[$1]}" ]; then
		ips="$(cat "${SERVER_BANNED_IPS_PATH[$1]}")"
	fi

	if [[ -z "$players" && -z "$ips" ]]; then
		echo "The blacklist is empty."
	else
		if [[ ! -z "$players" ]]; then
			echo "Players:"
			for name in $players; do
				echo "    $name"
			done
		fi

		if [[ ! -z "$ips" ]]; then
			echo "IP Addresses:"
			for address in $ips; do
				echo "    $address"
			done
		fi
	fi
}

# Adds a player name to a server's list of operators
# $1: The server ID
# $2->: The player name
command_server_operator_add() {
	if server_is_running "$1"; then
		for player in "${@:2}"; do
			server_command "$1" OP_ADD player="$player"
			echo_fallback "$RETURN" "Player $player is now an operator."
		done
	else
		server_property "$1" OPS_PATH

		for player in "${@:2}"; do
			if ! grep "^$player\$" "${SERVER_OPS_PATH[$1]}" >/dev/null; then
				echo "$player" >> "${SERVER_OPS_PATH[$1]}"
			fi
		done
	fi

	if [[ $# -gt 2 ]]; then
		echo -n "The following players are now operators: "
		echo -n "$2"
		for player in "${@:3}"; do
		 	echo -n ", $player"
		done
		echo "."
	else
		echo "\"$2\" is now an operator."
	fi
}

# Removes a player name to a server's list of operators
# $1: The server ID
# $2: The player name
command_server_operator_remove() {
	# TODO: Support multiple player names
	if server_is_running "$1"; then
		for player in "${@:2}"; do
			server_command "$1" OP_REMOVE player="$player"
			echo_fallback "$RETURN" "Player $player is no longer an operator."
		done
	else
		server_property "$1" OPS_PATH

		for player in "${@:2}"; do
			for player in "${@:2}"; do
				sed -ri "/^$player\$/d" "${SERVER_OPS_PATH[$1]}"
			done
		done
	fi

	if [[ $# -gt 2 ]]; then
		echo -n "The following players are no longer operators: "
		echo -n "$2"
		for player in "${@:3}"; do
		 	echo -n ", $player"
		done
		echo "."
	else
		echo "\"$2\" is no longer an operator."
	fi
}

# Displays a list of operators for an individual server
# $1: The server ID
command_server_operator_list() {
	server_property "$1" OPS_PATH

	if [ -f "${SERVER_OPS_PATH[$1]}" ]; then
		local players="$(cat "${SERVER_OPS_PATH[$1]}")"

		if [ ! -z "$players" ]; then
			echo "$players"
			return 0
		fi
	fi

	echo "No players are operators."
}

# Sets the game mode for
# $1: The server ID
# $2: The game mode
# $3->: The player name
command_server_gamemode() {
	if server_is_running "$1"; then
		for player in "${@:3}"; do
			server_command "$1" GAMEMODE player="$player" mode="$2"
			echo_fallback "$RETURN" "No output found. It may have worked."
		done
	else
		error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Kicks a connected player from a server
# $1: The server ID
# $2->: The player name
command_server_kick() {
	if server_is_running "$1"; then
		for player in "${@:2}"; do
			server_command "$1" KICK player="$player"
			echo_fallback "$RETURN" "Player $player has been kicked."
		done
	else
		error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Broadcasts a message to all connected players for a server
# $1: The server ID
# $2->: Words of the message, will be concatenated with spaces
command_server_say() {
	if server_is_running "$1"; then
		server_command "$1" SAY message="${*:2}"
		echo_fallback "$RETURN" "Message sent to players."
	else
		error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Sets the time on an individual server
# $1: The server ID
# $2: The time
command_server_time_set() {
	if server_is_running "$1"; then
		server_command "$1" TIME_SET time="$2"
		echo_fallback "$RETURN" "Time set to $2."
	else
		error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Increments the time on an individual server
# $1: The server ID
# $2: The time to add
command_server_time_add() {
	if server_is_running "$1"; then
		server_command "$1" TIME_ADD time="$2"
		echo_fallback "$RETURN" "Time increased by $2."
	else
		error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Toggles the downfall of rain and snow on an individual server
# $1: The server ID
command_server_toggledownfall() {
	if server_is_running "$1"; then
		server_command "$1" TOGGLEDOWNFALL
		echo_fallback "$RETURN" "Downfall toggled."
	else
		error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Gives entities to players in game
# $1: The server ID
# $2: The player name
# $3: The entity id/name
# $4: The amount to give
# $5: The entity damage value
command_server_give() {
	if server_is_running "$1"; then
		server_command "$1" GIVE player="$2" item="$3" amount="$4" damage="$5"
		local amount="x1"
		[ ! -z "$4" ] && amount="x$4"
		echo_fallback "$RETURN" "Given item $3 ${amount} to $2."
	else
		error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Gives XP to a player in game
# $1: The server ID
# $2: The player name
# $3: The amount of XP to give (can be negative)
command_server_xp() {
	if server_is_running "$1"; then
		server_command "$1" XP player="$2" amount="$3"
		echo_fallback "$RETURN" "Given $3 experience to $2."
	else
		error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Turns world saving on for an individual server
# $1: The server ID
command_server_save_on() {
	server_save_on "$1"
}

# Turns world saving off for an individual server
# $1: The server ID
command_server_save_off() {
	server_save_off "$1"
}

# Forces the saving of all pending world saves
# $1: The server ID
command_server_save_all() {
	server_save_all "$1"
}

# Sends a command string to the server to be executed
# $1: The server ID
# $2->: A command, separate arguments are concatenated with spaces
command_server_cmd() {
	if server_is_running "$1"; then
		server_eval "$1" "${*:2}"
		echo "Command sent."
	else
		error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Sends a command string to the server to be executed, and then tails the
# server logs to watch fro results.
# $1: The server ID
# $2->: A command, separate arguments are concatenated with spaces
command_server_cmdlog() {
	if server_is_running "$1"; then
		server_property "$1" LOG_PATH
		server_property "$1" USERNAME

		echo "Now watching logs (press Ctrl+C to exit):"
		echo "..."
		server_eval "$1" "${*:2}"
		as_user "${SERVER_USERNAME[$1]}" "tail --pid=$$ --follow=name --retry --lines=5 --sleep-interval=0.1 ${SERVER_LOG_PATH[$1]} 2>/dev/null"
	else
		error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Resumes a server's screen session (requires ssh-ed in as server user, using
# the `su` command will not work.)
# $1: The server ID
command_server_console() {
	if server_is_running "$1"; then
		server_property "$1" USERNAME
		as_user "${SERVER_USERNAME[$1]}" "script -q -c 'screen -r ${SERVER_SCREEN_NAME[$1]}' /dev/null"
	else
		error_exit SERVER_STOPPED "Server \"${SERVER_NAME[$1]}\" is not running."
	fi
}

# Sets a parameter in the config file if it exists, otherwise inserts the
# parameter.
# $1: The server ID
# $2: Optionally, a setting name
# $3: Optionally, a value to set for $2
command_server_config() {
	# If both a setting name and value are given
	if [ ! -z "$2" ] && [ ! -z "$3" ]; then
		server_property "$1" CONF
		if [[ -f "${SERVER_CONF[$1]}" ]]; then
			if grep "$2" "${SERVER_CONF[$1]}" >/dev/null; then
				sed -i /$2=/s/.*/"$2=$3"/g "${SERVER_CONF[$1]}"
			else
				echo "$2=$3" >> "${SERVER_CONF[$1]}"
			fi

			if server_is_running "$1"; then
				echo "Changes to config may require a server restart to take effect: sudo $0 ${SERVER_NAME[$1]} restart";
			fi
		fi

		return 0
	fi

	# If only a setting name is given
	if [ ! -z "$2" ]; then
		server_read_config "$1" "$2"
		echo "$RETURN"
	fi

	# If no parameter name is given
	if [ -z "$2" ]; then
		# List all parameters
		for ((i=0; i<$SERVER_SETTING_COUNT; i++)); do
			server_property "$1" "${SERVER_SETTING_NAME[$i]}"

			to_properties_name "${SERVER_SETTING_NAME[$i]}"
			eval echo "msm-$RETURN=\\\"\${SERVER_${SERVER_SETTING_NAME[$i]}[$1]}\\\""
		done
	fi
}










### Register Functions
### ------------------

# Registers a setting that can be defined in /etc/msm.conf
# $1: Setting name to register
# $2: Optionally a default value for this setting
register_setting() {
	# Create the default version of the variable
	eval SETTINGS_$1=\"$2\"
	# State that the variable has not yet been loaded
	eval LOADED_$1=\"false\"

	# Keep track of the setting name in a list
	SETTING_NAME[$SETTING_COUNT]="$1"
	SETTING_COUNT=$(( $SETTING_COUNT + 1 ))
}

# Registers a setting that can be defined for each server
# $1: Server setting name to register
# $2: Optionally a default value
register_server_setting() {
	register_setting "DEFAULT_$1" "$2"

	SERVER_SETTING_NAME[$SERVER_SETTING_COUNT]="$1"
	SERVER_SETTING_COUNT=$(( $SERVER_SETTING_COUNT + 1 ))
}

# Register possible settings
register_settings() {
	register_setting DEBUG "false"

	register_setting USERNAME "minecraft"
	register_setting SERVER_STORAGE_PATH "/opt/msm/servers"
	register_setting JAR_STORAGE_PATH "/opt/msm/jars"
	register_setting VERSIONING_STORAGE_PATH "/opt/msm/versioning"
	register_setting VERSIONING_FILE_EXTENSION "sh"
	register_setting RAMDISK_STORAGE_ENABLED "true"
	register_setting RAMDISK_STORAGE_PATH "/dev/shm/msm"

	register_setting WORLD_ARCHIVE_ENABLED "true"
	register_setting WORLD_RDIFF_PATH "/opt/msm/rdiff-backup/worlds"
	register_setting RDIFF_BACKUP_ENABLED "false"
	register_setting RDIFF_BACKUP_NICE "19"
	register_setting RDIFF_BACKUP_ROTATION "7"


	register_setting UPDATE_URL "https://raw.githubusercontent.com/msmhq/msm/latest"

	register_setting WORLD_ARCHIVE_PATH "/opt/msm/archives/worlds"
	register_setting LOG_ARCHIVE_PATH "/opt/msm/archives/logs"
	register_setting BACKUP_ARCHIVE_PATH "/opt/msm/archives/backups"

	register_setting RSYNC_BACKUP_ENABLED "false"
	register_setting WORLD_RSYNC_PATH "/opt/msm/rsync/worlds"

	register_setting JARGROUP_TARGET "target.txt"
	register_setting JARGROUP_DOWNLOAD_DIR "downloads"
	register_setting SERVER_PROPERTIES "server.properties"

	register_server_setting USERNAME "minecraft"
	register_server_setting SCREEN_NAME "msm-{SERVER_NAME}"
	register_server_setting VERSION "unknown"

	register_server_setting WORLD_STORAGE_PATH "worldstorage"
	register_server_setting WORLD_STORAGE_INACTIVE_PATH "worldstorage_inactive"
	register_server_setting LOG_PATH "server.log"
	register_server_setting WHITELIST_PATH "white-list.txt"
	register_server_setting BANNED_PLAYERS_PATH "banned-players.txt"
	register_server_setting BANNED_IPS_PATH "banned-ips.txt"
	register_server_setting OPS_PATH "ops.txt"
	register_server_setting OPS_LIST ""
	register_server_setting JAR_PATH "server.jar"

	register_server_setting FLAG_ACTIVE_PATH "active"
	register_server_setting COMPLETE_BACKUP_FOLLOW_SYMLINKS "false"

	register_server_setting WORLDS_FLAG_INRAM "inram"

	register_server_setting RAM "1024"
	register_server_setting INVOCATION "java -Xms{RAM}M -Xmx{RAM}M -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalPacing -XX:+AggressiveOpts -jar {JAR} nogui"
	register_server_setting STOP_DELAY "10"
	register_server_setting RESTART_DELAY "10"

	# Message that are displayed in-game by the server
	register_server_setting MESSAGE_STOP "SERVER SHUTTING DOWN IN {DELAY} SECONDS!"
	register_server_setting MESSAGE_STOP_ABORT "Server shut down aborted."
	register_server_setting MESSAGE_RESTART "SERVER REBOOT IN {DELAY} SECONDS!"
	register_server_setting MESSAGE_RESTART_ABORT "Server reboot aborted."
	register_server_setting MESSAGE_WORLD_BACKUP_STARTED "Backing up world."
	register_server_setting MESSAGE_WORLD_BACKUP_FINISHED "Backup complete."
	register_server_setting MESSAGE_COMPLETE_BACKUP_STARTED "Backing up entire server."
	register_server_setting MESSAGE_COMPLETE_BACKUP_FINISHED "Backup complete."

	# No need for defaults, values fall back on versioning file info
	register_server_setting CONFIRM_SAVE_ON
	register_server_setting CONFIRM_SAVE_OFF
	register_server_setting CONFIRM_SAVE_ALL
	register_server_setting CONFIRM_START
	register_server_setting CONFIRM_KICK
	register_server_setting CONFIRM_TIME_SET
	register_server_setting CONFIRM_TIME_ADD
	register_server_setting CONFIRM_TOGGLEDOWNFALL
	register_server_setting CONFIRM_GAMEMODE
	register_server_setting CONFIRM_GIVE
	register_server_setting CONFIRM_XP
}

# Adds a command to the list, allowing it to be called from the command line.
# $1: The command signature, a coded string describing the structure of the
#     command.
# $2: The handler function to call, if this command is identified.
register_command() {
	# Here we build a regular expression which will match any user input
	# that could be passed to the given handler function. It is derived
	# automatically from the given command signature.

	local regex="^"

	# Iterate over each element in the command signature
	for word in $1; do
		# Variables are denoted by angle brackets (e.g. "<variable>") and can
		# at this stage be accepted as any non-zero string
		if [[ "$word" =~ ^\<.*\>$ ]]; then
			case "$word" in
				"<strings>")
					regex="${regex}([^ ]+|\\\"[^\\\"]*\\\")( [^ ]+|\\\"[^\\\"]*\\\")* "
					;;
				"<flags>")
					regex="${regex:0:${#regex}-1}( ((--|-)[^ ]+)( (--|-)[^ ]+)*)? "
					;;
				*)
					regex="${regex}([^ ]+|\\\"[^\\\"]*\\\") "
					;;
			esac
			continue
		fi

		# Sometimes different worlds may be used to call the same command, in
		# these cases, the different words may be written contiguously,
		# separated by the pipe character (i.e. "|") and any of the options
		# provided will be allowed as a match.
		if [[ "$word" =~ \| ]]; then
			regex="${regex}($word) "
			continue
		fi

		# Anything else found in the command signature will be taken to mean
		# a fixed string, which must be provided to match this command.
		regex="${regex}$word "
	done

	if [ ${#regex} -ge 1 ]; then
		regex="${regex:0:${#regex}-1}\$"

		# Sets the global command variables in order to register this command
		COMMAND_SIGNATURE[$COMMAND_COUNT]="$1"
		COMMAND_REGEX[$COMMAND_COUNT]="$regex"
		COMMAND_HANDLER[$COMMAND_COUNT]="$2"

		COMMAND_COUNT=$(( $COMMAND_COUNT + 1 ))
	else
		error_exit FATAL_ERROR "Fatal error: Sorry about this, would you be so kind as to file a bug at http://git.io/2f_x-A and cite: \"Erroneous command regex '${regex}' for signature '${1}'\""
	fi
}

# Match and call a command from user input
# $*: User input
call_command() {
	manager_property SERVER_STORAGE_PATH

	local args
	local space="\ "
	for arg in "$@"; do
		if [[ "$arg" =~ $space ]]; then
			args="$args\"$arg\" "
		else
			args="$args$arg "
		fi
	done

	if [ ${#args} -ge 1 ]; then
		args="${args:0:${#args}-1}"
	fi

	# Clear any command flags that might exist
	# Start it with the delimiter necessary later on
	COMMAND_FLAGS=";"

	for ((command=0; command<$COMMAND_COUNT; command++)); do
		if [[ "$args" =~ ${COMMAND_REGEX[$command]} ]]; then
			unset args
			local word_offset=1
			local args
			local arg_offset=0
			local sid=-1
			local wid=-1

			# Helper function to build the argument list
			# $1: The argument to push onto the list
			push_arg() {
				args[$arg_offset]="$1"
				arg_offset="$(( $arg_offset + 1 ))"
			}

			# The following loop builds a set of arguments to pass to the
			# matched command handler function. Rather than passing all args
			# given to the script, to the handler (which may contain constant
			# strings), it only includes variables.
			for word in ${COMMAND_SIGNATURE[$command]}; do
				# Whether a positional argument is a variable or not is
				# determined by the respective element in the command signature
				# given when registering.
				#
				# This case statement handles each possible type of signature
				# token, and pushes the respective user input onto the stack of
				# arguments.
				case "$word" in
					# The "<string>" token expects any type of string argument,
					# accepting spaces, limited to one argument.
					"<string>")
						# Do no checks, just push the argument onto the stack
						push_arg "${!word_offset}"
						;;


					# The "<strings>" token must only be placed at the end of a
					# command signature, and allows an arbitrary amount of
					# arguments to be passed to the command handler function.
					"<strings>")
						# Put all remaining user input onto the argument stack
						for input_arg in "${@:$word_offset}"; do
							push_arg "$input_arg"
						done
						# Break from analysing the rest of the input
						break
						;;

					# The "<flags>" token expects any string without spaces that
					# starts with one or two dashes: "--noinput -q" are examples.
					# All flags are consumed and stored in the COMMAND_FLAGS
					# variable.
					"<flags>")
						local num_flags=0
						for potential_flag in "${@:$word_offset}"; do
							if [[ "$potential_flag" =~ ^(\-\-|\-)[^\ ]+$ ]]; then
								COMMAND_FLAGS="${COMMAND_FLAGS}${potential_flag};"
								num_flags=$(( $num_flags + 1 ))
							else
								# Stop processing words, since all flags must be
								# contiguous
								break
							fi
						done

						# We may have consumed more than one "word", the outer
						# loop expects us to only take one, so must correct for
						# this if we have take two words or more
						if [[ "$num_flags" -ge 2 ]]; then
							word_offset=$(( $word_offset + $num_flags - 1 ))
						fi
						;;


					# The "<name>" token is similar to "<string>" but adds an
					# extra assurance that the string is a valid name, as used
					# for creating servers and other things.
					"<name>")
						# Check the argument is a valid name and then add push it onto the argument stack
						local specified_name="${!word_offset}"

						if is_valid_name "$specified_name"; then
							push_arg "$specified_name"
						fi
						;;


					# The "<name:server>" token improves on "<name>" by also
					# checking that the server exists, and passing the argument
					# on as the server id, instead of the server name to
					# command handler functions.
					"<name:server>")
						local specified_name="${!word_offset}"
						if [[ "$specified_name" == "all" ]]; then
							# Do for all servers
							sid="server:all"
						else
							if is_valid_name "$specified_name"; then
								if [ -d "$SETTINGS_SERVER_STORAGE_PATH/$specified_name" ]; then
									server_get_id "$specified_name"
									sid="$RETURN"
								fi
							fi

							if [[ "$sid" -eq "-1" ]]; then
								error_exit NAME_NOT_FOUND "There is no server with the name \"$specified_name\"."
							fi
						fi

						push_arg "$sid"
						;;


					# The "<name:world>" token also improves upon "<name>" by
					# ensuring that the world actually exists, and passes the
					# argument on to command handlers as the world ID, rather
					# than the original world name input by the user.
					"<name:world>")
						local specified_name="${!word_offset}"

						if [[ "$sid" -eq "-1" ]]; then
							# Server id not set yet
							error_exit 1 "Ill-defined command $*. Please file an issue by opening the following link: https://github.com/msmhq/msm/issues"
						fi

						if [[ "$sid" -eq "-2" ]]; then
							if [[ "$specified_name" == "all" ]]; then
								wid="world:all"
							else
								error_exit INVALID_ARGUMENT "When specifying \"all\" servers, \"all\" worlds must be specified also."
							fi
						fi

						if [[ "$sid" -ge "0" ]]; then
							if is_valid_name "$specified_name"; then
								server_property "$sid" WORLD_STORAGE_PATH
								server_property "$sid" WORLD_STORAGE_INACTIVE_PATH

								if [ -d "${SERVER_WORLD_STORAGE_PATH[$sid]}/$specified_name" ] || [ -d "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$sid]}/$specified_name" ]; then
									server_world_get_id "$sid" "$specified_name"
									wid="$RETURN"
								fi
							fi

							if [[ "$wid" -eq "-1" ]]; then
								error_exit NAME_NOT_FOUND "There is no world with the name \"$specified_name\"."
							fi

							push_arg "$wid"
						fi
						;;
				esac

				word_offset=$(( $word_offset + 1 ))
			done

			# The argument list for the call to the command handler has been
			# built. But there are several ways to call a handler. Either just
			# once, or multiple times based upon if multiple servers or worlds
			# were specified.

			# This code block calls the handler for all possible servers and
			# all possible worlds.
			if [[ "$sid" == "server:all" ]] && [[ "$wid" == "world:all" ]]; then
				for ((j=0; j<$NUM_WORLDS; j++)); do
					# Replace server and world id place holders with actual id's
					local replaced_args
					for k in ${!args[@]}; do
						replaced_args[$k]="${args[$k]//server:all/${WORLD_SERVER_ID[$j]}}"
						replaced_args[$k]="${args[$k]//world:all/$j}"
					done

					# Call the function with the specific replaced args
					${COMMAND_HANDLER[$command]} "${replaced_args[@]}"
				done

				# Prevent the default singular call later on.
				unset COMMAND_FLAGS; return
			fi

			# This calls the handler for all possible servers, and preserves
			# all other arguments.
			if [[ "$sid" == "server:all" ]]; then
				for ((j=0; j<$NUM_SERVERS; j++)); do
					local replaced_args
					for k in ${!args[@]}; do
						replaced_args[$k]="${args[$k]//server\:all/$j}"
					done

					${COMMAND_HANDLER[$command]} "${replaced_args[@]}"
				done

				unset COMMAND_FLAGS; return
			fi

			# This calls the handlers for all possible worlds for a specific
			# server.
			if [[ "$sid" != "server:all" ]] && [[ "$wid" == "world:all" ]]; then
				for ((j=${SERVER_WORLD_OFFSET[$sid]}; j<${SERVER_NUM_WORLDS[$sid]}; j++)); do
					local replaced_args
					for k in ${!args[@]}; do
						replaced_args[$k]="${args[$k]//world:all/$j}"
					done

					${COMMAND_HANDLER[$command]} "${replaced_args[@]}"
				done

				unset COMMAND_FLAGS; return
			fi

			# Otherwise it's a simple single call of the handler.
			${COMMAND_HANDLER[$command]} "${args[@]}"
			unset COMMAND_FLAGS; return
		fi
	done

	echo "No such command. See $0 help"
}

# Defines every MSM command.
register_commands() {
	# The following section registers commands to be available for use. The
	# register_command function accepts a command_signature and a
	# command_handler_function_name as positional arguments 1 and 2
	# respectively.
	#
	# A command signature consists of multiple elements separated by spaces,
	# the available options are as follows:
	#
	#     fixedstring    Matches an argument containing the specified
	#                    characters, in this case the characters "fixedstring"
	#
	#     <string>       Same as "fixedstring", but is variable and the value
	#                    is passed to the handler function as a positional
	#                    argument
	#
	#     <strings>      Same as "<string>", but matches multiple arguments,
	#                    must be final element
	#
	#     <flags>        Matches a list of space separated flags, such as
	#                    "--noinput --quiet -p -d". Not passed as a positional
	#                    argument. Instead set as the value of COMMAND_FLAGS.
	#
	#     <name>         Same as "<string>", also ensures it's a valid name
	#                    using the is_valid_name function
	#
	#     <name:server>  Same as "<name>", also converts value to server id or
	#                    fails if the server does not exist
	#
	#     <name:world>   Same as "<name>", also converts value to world id or
	#                    fails if the world does not exist. Must only be
	#                    included after a "<name:server>" element.
	#
	# Elements listed above encapsulated within angle brackets must be included
	# within a signature verbatim, as opposed to the "fixedstring" element
	# which is arbitrary.
	#
	# Variables passed to handler functions are of course positional and there
	# position matches the position of that element in the command signature.

	register_command "start" "command_start"
	register_command "stop" "command_stop"
	register_command "stop now" "command_stop_now"
	register_command "restart" "command_restart"
	register_command "restart now" "command_restart_now"
	register_command "version" "command_version"
	register_command "config" "command_config"
	register_command "update <flags>" "command_update"

	register_command "server list" "command_server_list"
	register_command "server create <name>" "command_server_create"
	register_command "server delete <name>" "command_server_delete"
	register_command "server rename <name> <name>" "command_server_rename"

	register_command "jargroup list" "command_jargroup_list"
	register_command "jargroup create <name> <string>" "command_jargroup_create"
	register_command "jargroup delete <name>" "command_jargroup_delete"
	register_command "jargroup rename <name> <name>" "command_jargroup_rename"
	register_command "jargroup changeurl <name> <string>" "command_jargroup_changeurl"
	register_command "jargroup getlatest <name>" "command_jargroup_getlatest"

	register_command "help" "command_help"

	register_command "<name:server> start" "command_server_start"
	register_command "<name:server> stop" "command_server_stop"
	register_command "<name:server> stop now" "command_server_stop_now"
	register_command "<name:server> restart" "command_server_restart"
	register_command "<name:server> restart now" "command_server_restart_now"
	register_command "<name:server> status" "command_server_status"
	register_command "<name:server> connected" "command_server_connected"
	register_command "<name:server> worlds list" "command_server_worlds_list"
	register_command "<name:server> worlds load" "command_server_worlds_load"
	register_command "<name:server> worlds ram <name:world>" "command_server_worlds_ram"
	register_command "<name:server> worlds todisk" "command_server_worlds_todisk"
	register_command "<name:server> worlds backup" "command_server_worlds_backup"
	register_command "<name:server> worlds on <name:world>" "command_server_worlds_on"
	register_command "<name:server> worlds off <name:world>" "command_server_worlds_off"
	register_command "<name:server> logroll" "command_server_logroll"
	register_command "<name:server> backup" "command_server_backup"
	register_command "<name:server> jar <name>" "command_server_jar"
	register_command "<name:server> jar <name> <string>" "command_server_jar"
	register_command "<name:server> console" "command_server_console"
	register_command "<name:server> config" "command_server_config"
	register_command "<name:server> config <string>" "command_server_config"
	register_command "<name:server> config <string> <string>" "command_server_config"

	register_command "<name:server> whitelist|wl on" "command_server_whitelist_on"
	register_command "<name:server> whitelist|wl off" "command_server_whitelist_off"
	register_command "<name:server> whitelist|wl add <strings>" "command_server_whitelist_add"
	register_command "<name:server> whitelist|wl remove <strings>" "command_server_whitelist_remove"
	register_command "<name:server> whitelist|wl list" "command_server_whitelist_list"
	register_command "<name:server> blacklist|bl player add <strings>" "command_server_blacklist_player_add"
	register_command "<name:server> blacklist|bl player remove <strings>" "command_server_blacklist_player_remove"
	register_command "<name:server> blacklist|bl ip add <strings>" "command_server_blacklist_ip_add"
	register_command "<name:server> blacklist|bl ip remove <strings>" "command_server_blacklist_ip_remove"
	register_command "<name:server> blacklist|bl list" "command_server_blacklist_list"
	register_command "<name:server> operator|op add <strings>" "command_server_operator_add"
	register_command "<name:server> operator|op remove <strings>" "command_server_operator_remove"
	register_command "<name:server> operator|op list" "command_server_operator_list"
	register_command "<name:server> gamemode|gm <string> <strings>" "command_server_gamemode"
	register_command "<name:server> kick <strings>" "command_server_kick"
	register_command "<name:server> say <strings>" "command_server_say"
	register_command "<name:server> time set <string>" "command_server_time_set"
	register_command "<name:server> time add <string>" "command_server_time_add"
	register_command "<name:server> toggledownfall|tdf" "command_server_toggledownfall"
	register_command "<name:server> give <string> <string>" "command_server_give"
	register_command "<name:server> give <string> <string> <string>" "command_server_give"
	register_command "<name:server> give <string> <string> <string> <string>" "command_server_give"
	register_command "<name:server> xp <string> <string>" "command_server_xp"
	register_command "<name:server> save on" "command_server_save_on"
	register_command "<name:server> save off" "command_server_save_off"
	register_command "<name:server> save all" "command_server_save_all"
	register_command "<name:server> cmd <strings>" "command_server_cmd"
	register_command "<name:server> cmdlog <strings>" "command_server_cmdlog"
}









# $1: Server path
server_allocate() {
	unset RETURN

	# Get an ID for this new server
	local server_id="$NUM_SERVERS"

	# Store the path for this new server
	SERVER_PATH[$server_id]="$1"
	# Store the name for this server
	quick_basename "${SERVER_PATH[$server_id]}"
	SERVER_NAME[$server_id]="$RETURN"

	NUM_SERVERS=$(( $NUM_SERVERS + 1 ))

	RETURN="$server_id"
}

# $1: Server ID
server_worlds_allocate() {
	local world_id

	# A server's worlds require contiguous ID's
	# thus they are loaded one after another all at once.

	# $1: Server ID
	# $2: World path
	world_allocate() {
		# Get an ID for this new world
		world_id="$NUM_WORLDS"

		# Store the path for this new world
		WORLD_PATH[$world_id]="$2"
		# Store the name for this world
		quick_basename "${WORLD_PATH[$world_id]}"
		WORLD_NAME[$world_id]="$RETURN"
		# Store the server ID this world belongs to
		WORLD_SERVER_ID[$world_id]="$1"

		NUM_WORLDS=$(( $NUM_WORLDS + 1 ))
	}

	server_property "$1" WORLD_STORAGE_PATH
	server_property "$1" WORLD_STORAGE_INACTIVE_PATH

	local world_name

	# Record the index at which worlds for this server will start
	SERVER_WORLD_OFFSET[$1]="$NUM_WORLDS"

	if [[ -d "${SERVER_WORLD_STORAGE_PATH[$1]}" ]]; then
		while IFS= read -r -d $'\0' path; do
			world_allocate "$1" "$path"
		done < <(find "${SERVER_WORLD_STORAGE_PATH[$1]}" -mindepth 1 -maxdepth 1 -type d -print0)
	fi

	if [[ -d "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$1]}" ]]; then
		while IFS= read -r -d $'\0' path; do
			world_allocate "$1" "$path"
		done < <(find "${SERVER_WORLD_STORAGE_INACTIVE_PATH[$1]}" -mindepth 1 -maxdepth 1 -type d -print0)
	fi

	# Record the number of worlds this server has
	SERVER_NUM_WORLDS[$1]="$(( $NUM_WORLDS - ${SERVER_WORLD_OFFSET[$1]} ))"
}

# Allocates stub variables, in this context a stub is
# enough data to be able to load in more data via
# the *_property functions.
allocate() {
	manager_property SERVER_STORAGE_PATH

	# Determine server names (but don't load them)
	if [ -d "$SETTINGS_SERVER_STORAGE_PATH" ]; then
		while IFS= read -r -d $'\0' path; do
			server_allocate "$path"
			server_worlds_allocate "$RETURN"
		done < <(find "$SETTINGS_SERVER_STORAGE_PATH" -mindepth 1 -maxdepth 1 -type d -print0)
	fi
}

# Loads stub data for available versions
load_versions() {
	manager_property USERNAME
	manager_property VERSIONING_STORAGE_PATH

	if [ -e "$SETTINGS_VERSIONING_STORAGE_PATH" ]; then
		local newest_minecraft_version="0.0.0"
		while IFS= read -r -d $'\0' path; do
			local dir="$(dirname "$path")"
			local file_name="$(basename "$path")"
			local version="${file_name%.*}"
			local version_type="$(basename "$dir")"

			# Determine the newest minecraft version
			if [[ "$version_type" == "minecraft" ]]; then
				_newest_version "$version" "$newest_minecraft_version"
				newest_minecraft_version="$RETURN"
			fi

			VERSIONS[$VERSIONS_COUNT]="${version_type}/$version"
			VERSIONS_PATH[$VERSIONS_COUNT]="$path"
			VERSIONS_COUNT=$(( $VERSIONS_COUNT + 1 ))
		done < <(find "$SETTINGS_VERSIONING_STORAGE_PATH" -mindepth 1 -type f -print0)

		# Record the latest minecraft version to use as a default
		if [[ "$newest_minecraft_version" == "0.0.0" ]]; then
			msm_warning "Could not find versioning files, please use 'msm update' to download them"
		else
			VERSIONS_NEWEST_MINECRAFT_VERSION="${newest_minecraft_version}"
			VERSIONS_NEWEST_MINECRAFT_PATH="${SETTINGS_VERSIONING_STORAGE_PATH}/minecraft/${newest_minecraft_version}.${SETTINGS_VERSIONING_FILE_EXTENSION}"
		fi
	else
		msm_warning "Could not find versioning files, please use 'msm update' to download them"
	fi
}

# $1: Version one
# $2: Version two
# $RETURN: The greater version
_newest_version() {
	unset RETURN
	# Compare the major versions [].0.0
	component_one=`echo $1 | awk -F'.' '{print $1}'`
	component_two=`echo $2 | awk -F'.' '{print $1}'`
	if [[ "$component_one" -lt "$component_two" ]]; then
		# Give up if the given major version is less than this one's
		RETURN="$2"; return 0
	fi

	# Compare the minor versions 0.[].0
	component_one=`echo $1 | awk -F'.' '{print $2}'`
	component_two=`echo $2 | awk -F'.' '{print $2}'`
	if [[ "$component_one" -lt "$component_two" ]]; then
		# Give up if the given minor version is less than this one's
		RETURN="$2"; return 0
	fi

	# Compare the patch versions 0.0.[]
	component_one=`echo $1 | awk -F'.' '{print $3}'`
	component_two=`echo $2 | awk -F'.' '{print $3}'`
	if [[ "$component_one" -lt "$component_two" ]]; then
		# Give up if the given patch version is less than this one's
		RETURN="$2"; return 0
	fi

	RETURN="$1"
}

# Checks available versions MSM supports and returns the
# closes match.
# $1: Version name preferred
# $RETURN: The closest available version, older or equal
#          to the given version $1
get_closest_version() {
	unset RETURN
	local given_type="${1%/*}"
	local given_version="${1##*/}"

	local closest_version cv_val
	local v v_version v_type v_full v_val given_val

	closest_version="0.0.0"

	for ((v=0; v<$VERSIONS_COUNT; v++)); do
		v_full="${VERSIONS[$v]}"
		v_type="${v_full%/*}"
		v_version="${v_full##*/}"

		if [[ "$given_type" == "$v_type" ]]; then
			# If this version type is the same as the given type (i.e. "minecraft")

			# Then check the version is before or equal to this version:

			_newest_version "$given_version" "$v_version"
			if [[ "$RETURN" == "$given_version" ]]; then
				# This version is older than or equal to the given version

				_newest_version "$closest_version" "$v_version"
				if [[ "$RETURN" == "$v_version" ]]; then
					# This version is newer than or equal to the closest version
					closest_version="$v_version"
				fi
			fi
		fi
	done

	if [[ "$closest_version" == "0.0.0" ]]; then
		RETURN="unknown"
	else
		RETURN="${given_type}/${closest_version}"
	fi
}

# Called if the script is interrupted before exiting naturally
interrupt() {
	local exit_message="false"

	for ((i=0; $i<$NUM_SERVERS; i++)); do
		if [[ "${STOP_COUNTDOWN[$i]}" == "true" ]] && server_is_running "$i"; then
			if [[ "$exit_message" == "false" ]]; then
				echo -e "\nInterrupted..."
				exit_message="true"
			fi
			server_eval "$i" "say ${SERVER_MESSAGE_STOP_ABORT[$i]}"
			echo "Server \"${SERVER_NAME[$i]}\" shutdown was aborted."
		fi

		if [[ "${RESTART_COUNTDOWN[$i]}" == "true" ]] && server_is_running "$i"; then
			if [[ "$exit_message" == "false" ]]; then
				echo -e "\nInterrupted..."
				exit_message="true"
			fi
			server_eval "$i" "say ${SERVER_MESSAGE_RESTART_ABORT[$i]}"
			echo "Server \"${SERVER_NAME[$i]}\" restart was aborted."
		fi
	done

	exit
}










### Versioning Functions
### --------------------

# Sources another versioning file
# $1: The name of the versioning file
extends() {
	manager_property VERSIONING_STORAGE_PATH
	source "${SETTINGS_VERSIONING_STORAGE_PATH}/$1.${SETTINGS_VERSIONING_FILE_EXTENSION}"
}

# Defines a servers console event variables, VERSIONING_SERVER_ID
# must be set before calling this function
# $1: The name of the event
# $2->: The log lines to accept as confirmation
console_event() {
	# Build a regex with all lines in
	local lines="$2"
	for line in "${@:3}"; do
		lines="$lines|$line"
	done

	local event_name event_timeout
	if [[ "$1" =~ (.*):(.*) ]]; then
		# If there is a colon in the name, use that
		# to extract the included delay
		event_name="${BASH_REMATCH[1]}"
		event_timeout="${BASH_REMATCH[2]}"
	else
		event_name="$1"
		event_timeout="1"
	fi

	# Set server variable
	eval SERVER_CONSOLE_EVENT_OUTPUT_${event_name}[$VERSIONING_SERVER_ID]=\"$lines\"
	eval SERVER_CONSOLE_EVENT_TIMEOUT_${event_name}[$VERSIONING_SERVER_ID]=\"$event_timeout\"
}

# Defines a servers console command variables, VERSIONING_SERVER_ID
# must be set before calling this function
# $1: The name of the command
# $2: The command pattern
# $3->: The log lines to accept as confirmation
console_command() {
	local command_name command_timeout
	if [[ "$1" =~ (.*):(.*) ]]; then
		# If there is a colon in the name, use that
		# to extract the included delay
		command_name="${BASH_REMATCH[1]}"
		command_timeout="${BASH_REMATCH[2]}"
	else
		command_name="$1"
		command_timeout="1"
	fi

	eval SERVER_CONSOLE_COMMAND_PATTERN_${command_name}[$VERSIONING_SERVER_ID]=\"$2\"

	# Build a regex with all lines in
	local lines="$3"
	for line in "${@:4}"; do
		lines="$lines|$line"
	done

	eval SERVER_CONSOLE_COMMAND_OUTPUT_${command_name}[$VERSIONING_SERVER_ID]=\"$lines\"
	eval SERVER_CONSOLE_COMMAND_TIMEOUT_${command_name}[$VERSIONING_SERVER_ID]=\"$command_timeout\"
}

# Defines a servers property variables, VERSIONING_SERVER_ID
# must be set before calling this function
# $1: The name of the property
# $2: The value of the property
set_property() {
	server_set_property "$VERSIONING_SERVER_ID" "$1" "$2"
	read_server_conf "$VERSIONING_SERVER_ID" "$1"
}









### Starting Code
### -------------

# The main function which starts the script
main() {
	register_settings
	register_commands
	load_versions
	allocate

	# Trap interrupts to the script by calling the interrupt function
	trap interrupt EXIT

	# This function call matches the user input to a registered command
	# signature, and then calls that command's handler function with positional
	# arguments containing any "variable" strings.
	call_command "$@"
}


### Start point

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
	# MSM was called from the command line

	main "$@"
	exit 0
else
	# MSM was sourced from another script.
	# Just register settings instead.

	register_settings
	load_versions
	allocate
fi