#!/usr/bin/env bash

#
#       Copyright (c) 2002, Valve LLC. All rights reserved.
#
#   a wrapper script for the main hl dedicated server binary.
#   Performs auto-restarting of the server on crash. You can
#   extend this to log crashes and more.
#

# Set locale
LC_ALL=C

# Warn user if running from a unsupported shell (eg.: old sh) -R4to0
if [ -z "${BASH}" ] ; then
	echo "WARNING: You're running from an unsupported shell, some functions might not work! Please use bash instead."
fi

# Fetch GNUC version and warn if running from a outdated distro -R4to0
GNUC_VER="$(ldd --version | awk '/ldd/{print $NF}')" # current running on the system
GNUC_MIN="2.24" # minimum supported version for the game binaries

# major || major && minor
if (( ${GNUC_VER%%.*} < ${GNUC_MIN%%.*} || ( ${GNUC_VER%%.*} <= ${GNUC_MIN%%.*} && ${GNUC_VER##*.} < ${GNUC_MIN##*.} ) )); then
	echo -ne "WARNING: Unsupported distro GNU C Library detected. Please update your distro.\n\nYour system has: ${GNUC_VER}\nMinimum required: ${GNUC_MIN}\n\n"
fi

# Check if user is running as root and warn
if [ "$(id -u)" -eq 0 ]; then
	rootmsg=(
		""
		""
		"           ************** WARNING ***************"
		"           Running the dedicated server as root  "
		"           is highly discouraged. It is generally"
		"           unnecessary to use root privileges to "
		"           execute the dedicated server.         "
		"           **************************************"
		""
		""
	)
	for dsprmsg in "${rootmsg[@]}"
	do
		echo "${dsprmsg}"
	done
	for iCount in {10..1}; do
		echo -ne "The server will continue to launch in $iCount $([[ $iCount -le 1 ]] && echo "second" || echo "seconds")...\033[0K\r";
		sleep 1;
	done

	echo -ne "\033[0K\r" # clear line
fi

init() {
	# Get SvenDS path from where this script is executed
	SVENDS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
	
	# Fallback to old method in case the bash one fails (sh compatible)
	# non-obvious, the ${0%/*} pulls the path out of $0, cd's into the
	# specified directory, then uses $PWD to figure out where that
	# directory lives - and all this in a subshell, so we don't affect
	# $PWD
	if [ -z "${SVENDS_DIR}" ]; then
		SVENDS_DIR="$(cd "${0%/*}" && echo "${PWD}")"
	fi
	
	# Fallback to current relative dir in case both lookup methods fails -R4to0
	if [ -z "${SVENDS_DIR}" ]; then
		SVENDS_DIR="."
	fi

	# setup the libraries, local dir first!
	export LD_LIBRARY_PATH="${SVENDS_DIR}:${SVENDS_DIR}/redist:${LD_LIBRARY_PATH}"

	# Initialises the various variables
	# Set up the defaults
	GAME=""
	DEBUG=0
	RESTART="yes"
	GAME_BIN="./svends_i686"
	HL_DETECT=1 # to use with auto detect arch in future? -R4to0
	TIMEOUT=10 # time to wait after a crash (in seconds)
	CRASH_DEBUG_MSG="Support available on our Discord server: https://discord.gg/svencoop"
	GDB="gdb" # the gdb binary to run
	DEBUG_LOG="debug.log"
	PID_FILE="" # only needed if DEBUG is set so init later
	STEAMERR=""
	SIGINT_ACTION="quit 0" # exit normally on sig int
	NO_TRAP=0
	AUTO_UPDATE=""
	BETA_VERSION=""
	BETA_PASSWORD=""
	STEAM_DIR=""
	STEAMCMD_SCRIPT=""
	PARAMS="$*"
	NO_DEFAULT_MAP=0
	DEFAULT_GAME="svencoop"
	DEFAULT_MAP="_server_start"

	# Remove any old default pid files
	# Cant do this as they may be still running
	#rm -f svends.*.pid

	# use the $FORCE environment variable if its set
	if [ -n "${FORCE}" ]; then
		# Note: command line -binary will override this
		GAME_BIN="${FORCE}"
		HL_DETECT=0
	fi

	while [ $# -gt 0 ]; do
		case "${1}" in
		"+map")
			MAP="${2}"
			shift;;
		"-nodefaultmap")
			NO_DEFAULT_MAP=1 ;;
		"-game")
			GAME="${2}"
			shift ;;
		"-debug")
			DEBUG=1
			# Ensure that PID_FILE is set
			if [ -z "${PID_FILE}" ]; then
				PID_FILE="svends.${$}.pid"
			fi ;;
		"-norestart")
			RESTART="" ;;
		"-pidfile")
			PID_FILE="${2}"
			shift ;;
		"-binary")
			GAME_BIN="${2}"
			HL_DETECT=0
			shift ;;
		"-timeout")
			TIMEOUT="${2}"
			shift ;;
		"-gdb")
			GDB="${2}"
			shift ;;
		"-debuglog")
			DEBUG_LOG="${2}"
			shift ;;
		"-autoupdate")
			AUTO_UPDATE="yes"
			RESTART="yes" ;;
		"-steamerr")
			STEAMERR=1 ;;
		"-ignoresigint")
			SIGINT_ACTION="" ;;
		"-notrap")
			NO_TRAP=1 ;;
		"-beta")
			BETA_VERSION="${2}"
			shift ;;
		"-betapassword")
			BETA_PASSWORD="${2}"
			shift ;;
		"-steam_dir")
			STEAM_DIR="${2}"
			shift ;;
		"-steamcmd_script")
			STEAMCMD_SCRIPT="${2}"
			shift ;;
		"-help")
			# quit with syntax
			quit 2
			;;
		esac
		shift
	done

	# Ensure we have a game specified
	if [ -z "${GAME}" ]; then
		GAME="${DEFAULT_GAME}"
		PARAMS="${PARAMS} -game ${GAME}"
	fi

	# Check game directory
	if [ ! -d "${GAME}" ]; then
		echo "ERROR: Invalid game type '${GAME}' sepecified."
		quit 1
	fi

	# Force a map to start if not specified
	if [ -z "${MAP}" ] && [ 0 -eq "${NO_DEFAULT_MAP}" ]; then
		echo "WARNING: No map specified! Defaulting to ${DEFAULT_MAP}"
		PARAMS=("${PARAMS}" +map "${DEFAULT_MAP}")
	fi

	if [ 0 -eq "${NO_TRAP}" ]; then
		# Set up the int handler
		# N.B. Dont use SIGINT symbolic value
		#  as its just INT under ksh
		trap '$SIGINT_ACTION' 2
	fi

	# Only detect the CPU if it hasnt been set with
	# either environment or command line
	# Disabled for now as AMD binaries aren't parsing config files correctly -R4to0 (07 December 2022)
	#if [ "${HL_DETECT}" -eq 1 ]; then
	#	detectcpu
	#fi

	if [ ! -f "${GAME_BIN}" ]; then
		echo "ERROR: Game binary '${GAME_BIN}' not found, exiting"
		quit 1
	elif [ ! -x "${GAME_BIN}" ]; then
		# Could try chmod but dont know what we will be
		# chmoding so just fail.
		echo "ERROR: Game binary '${GAME_BIN}' not executable, exiting"
		quit 1
	fi

	# Setup debugging
	if [ "${DEBUG}" -ne 0 ]; then
		#turn on core dumps :) (if possible)
		echo "Enabling debug mode"
		CORESIZE="$(ulimit -c)"
		if [ "${CORESIZE}" != "unlimited" ]; then
			ulimit -c "unlimited"
		fi
		GDB_TEST="$(${GDB} -v 2>/dev/null)" # Suppressed "not found" message -R4to0
		if [ -z "${GDB_TEST}" ]; then
			echo "WARNING: Please install gdb first."
			echo "  goto http://www.gnu.org/software/gdb/ "
			DEBUG=0 # turn off debugging cause gdb isn't installed
		fi
	fi

	# Not needed anymore, it falls back to hardcoded if there is no script -R4to0
	#if [ -z "${STEAM_DIR}" -a -z "${STEAMCMD_SCRIPT}" ]; then
	#   echo "ERROR: You must set both the steam_dir and steamcmd_script."
	#   quit 1
	#fi

	PID_IN_PARAMS=$(echo "${PARAMS[@]}" | grep -e -pidfile)

	if [ -z "${PID_IN_PARAMS}" ] && [ -n "${PID_FILE}" ]; then
		GAME_BIN_CMD="${GAME_BIN} ${PARAMS[*]} -pidfile ${PID_FILE}"
	else
		GAME_BIN_CMD="${GAME_BIN} ${PARAMS[*]}"
	fi
}

syntax() {
	# Prints script syntax

	helpmsg=(
		"Syntax:"
		"${0} [-game <game>] [-debug] [-norestart] [-pidfile]"
		"   [-binary [svends_i686]"
		"   [-timeout <number>] [-gdb <gdb>] [-autoupdate]"
		"   [-steam_dir <path>] [-steamcmd_script <path>] [-steamerr]"
		"   [-ignoresigint]"
		"   [-beta <branch>] [-betapassword <branchpassword>]"
		"   [-debuglog <logname>]"
		"   [-nodefaultmap]"
		""
		"Params:"
		"-game <game>             Specifies the <game> to run. [Default: ${DEFAULT_GAME}]"
		"-debug                   Run debugging on failed servers if possible."
		"-debuglog <logname>      Log debug output to this file."
		"-norestart               Don't attempt to restart failed servers."
		"-pidfile <pidfile>       Use the specified <pidfile> to store the server pid."
		"-binary <binary>         Use the specified binary ( no auto detection )."
		"-timeout <number>        Sleep for <number> seconds before restarting"
		"                         a failed server."
		"-gdb <gdb>               Use <gdb> as the debugger of failed servers."
		"-autoupdate              Autoupdate the game. Requires -steam_dir with steamcmd"
		"                         path, or a copy of steamcmd folder in"
		"                         ${SVENDS_DIR} dir,"
		"                         or steamcmd:i386 package installed. [Default: Disabled]"
		"-steam_dir <path>        Dir that steamcmd.sh resides in."
		"                         Example: ~/path/to/steamcmd. [Default: not set]"
		"-steamcmd_script <path>  Path to the steamcmd script to execute."
		"                         Example: ~/path/to/svencoop_ds.txt. [Default: not set]"
		"-steamerr                Quit on steam update failure."
		"-beta                    Beta branch to update (steamcmd_script overrides this)."
		"-betapassword            Beta branch password if required."
		"-ignoresigint            Ignore signal INT (prevents CTRL+C quitting"
		"                         the script)."
		"-notrap                  Don't use trap. This prevents automatic"
		"                         removal of old lock files."
		"-nodefaultmap            Suppresses the addition of '+map \"${DEFAULT_MAP}\"'"
		"                         to the command line options."
		""
		"Note: All parameters specified as passed through to the server"
		"including any not listed."
	)
	for dsphelp in "${helpmsg[@]}"
	do
		echo "${dsphelp}"
	done
}

debugcore() {
	# Debugs any core file if DEBUG is set and
	# the exitcode is none 0

	exitcode=$1

	if [ "${exitcode}" -ne 0 ]; then
		echo "Uh oh it seems the server has crashed or failed to run (╯°□°）╯︵ ┻━┻" # Remove before release? -R4to0; No, he'll never find it. :) - AdamR; You monsters! - GeckoN; O SHI- - AdamR;
		echo "${CRASH_DEBUG_MSG}"
		if [ "${DEBUG}" -ne 0 ]; then
			{
				echo "bt"
				echo "info locals"
				echo "info registers"
				echo "info sharedlibrary"
				echo "disassemble" # do we need ASM stuff in the debug.log? -R4to0
				echo "info frame" # works, but gives an error... must be last
			} >> debug.cmds
			{
				echo "----------------------------------------------"
				echo "CRASH: $(date)"
				echo "Start Line: ${GAME_BIN_CMD}"
			} >> "${DEBUG_LOG}"

			# check to see if a core was dumped
			if [ -f core ]; then
				# On some systems (eg.: Debian 9) this file doesn't get pid suffix in its name,
				# let's rename to avoid another dump to overwrite this one. -R4to0
				mv "core" "core.$(cat "${PID_FILE}")"
				CORE="core.$(cat "${PID_FILE}")"
			elif [ -f "core.$(cat "${PID_FILE}")" ]; then
				CORE="core.$(cat "${PID_FILE}")"
			elif [ -f "${GAME_BIN}.core" ]; then
				CORE="${GAME_BIN}.core"
			fi

			if [ -n "${CORE}" ]; then
				${GDB} "${GAME_BIN}" "${CORE}" -x debug.cmds -batch >> "${DEBUG_LOG}"
			fi

			echo "End of crash report" >> "${DEBUG_LOG}"
			echo "----------------------------------------------" >> "${DEBUG_LOG}"
			rm debug.cmds
		else
			echo "Add \"-debug\" to the ${0} command line to generate a debug.log to help with solving this problem"
		fi
	fi
}

# TODO: Extend this to detect arch for future use with x64 bins -R4to0
detectcpu() {
	# Attempts to auto detect the CPU
	echo "Auto detecting CPU"

	SYSTYPE="$(uname)"

	if [ "$SYSTYPE" = "FreeBSD" ]; then
		PROC="/usr/compat/linux/proc"
	else
		PROC="/proc"
	fi

	PROCINFO="$(cat ${PROC}/cpuinfo)"

	if [ -n "${PROCINFO}" ]; then
		CPU_VERSION="$(grep "cpu family" <<< "${PROCINFO}" | cut -f2 -d":" | tr -d " " | uniq)";
		CPUVENDOR="$(grep "vendor_id" <<< "${PROCINFO}" | cut -f2 -d":" | tr -d " " | uniq)"

		if [ "${CPU_VERSION}" -lt 6 ]; then # it was 4 before (80486). Surely you're not using a pre-1995 CPU right? -R4to0
			echo "Error: SvenDS REQUIRES a i686 compatible CPU or better"
			quit 1
		elif [ "${CPUVENDOR}" = "AuthenticAMD" ]; then
			echo "Using AMD Optimised binary."
			GAME_BIN="./svends_amd"
		elif [ "${CPUVENDOR}" = "GenuineIntel" ]; then
			echo "Using Intel Optimised binary."
			GAME_BIN="./svends_i686"
		else
			echo "Using default binary."
		fi
	else
    	echo "Failed to query ${PROC}/cpuinfo, using default binary."
	fi
}

update() {
	updatesingle
}

updatesingle() {
	# Run the steamcmd update
	# Exits on failure if STEAMERR is set
	# Fallbacks to hardcoded goldsrc style if -steam_dir is not specified or not found

	if [ -n "${AUTO_UPDATE}" ]; then

		# Use steamcmd script if exists, use other related paramas or default if not -R4to0
		if [ -f "${STEAMCMD_SCRIPT}" ]; then
			STEAMCMD_PARAMS="+runscript ${STEAMCMD_SCRIPT}"
		else
			STEAMCMD_PARAMS="+logon anonymous +force_install_dir ${SVENDS_DIR} +app_update 276060"
			if [ -n "${BETA_VERSION}" ]; then
				STEAMCMD_PARAMS="$STEAMCMD_PARAMS -beta ${BETA_VERSION}"
			fi
			if [ -n "${BETA_PASSWORD}" ]; then
				STEAMCMD_PARAMS="${STEAMCMD_PARAMS} -betapassword ${BETA_PASSWORD}"
			fi
			STEAMCMD_PARAMS="${STEAMCMD_PARAMS} +quit" #needs to be at end? -R4to0
		fi

		# Detect from where we can use SteamCMD (installed from APT/yum/whatever, -steam_dir and/or hlds_run hardcoded)
		# User defined -steam_dir always override others even if it is invalid! -R4to0
		STEAMCMD_TEST="$(command -v steamcmd)"
		if [ -z "${STEAM_DIR}" ] && [ -n "${STEAMCMD_TEST}" ]; then # if -steam_dir is not specified and system installed is found
			STEAMCMD_BINARY="${STEAMCMD_TEST}"
		elif [ -n "${STEAM_DIR}" ]; then # if -steam_dir is specified
			STEAMCMD_BINARY="${STEAM_DIR}/steamcmd.sh"
		elif [ -z "${STEAM_DIR}" ] && [ -f "${SVENDS_DIR}/steamcmd/steamcmd.sh" ]; then # like in the original hlds_run, hardcoded srvdir/steamcmd
			STEAMCMD_BINARY="${SVENDS_DIR}/steamcmd/steamcmd.sh"
		fi

		if [ -f "${STEAMCMD_BINARY}" ]; then
			echo "Updating server using SteamCMD"
			echo "----------------------------"
			echo "Detected SteamCMD binary: ${STEAMCMD_BINARY}"
			eval "${STEAMCMD_BINARY} ${STEAMCMD_PARAMS}"
			eval "cd ${SVENDS_DIR}"
			echo "----------------------------"
		else
			if [ -n "$STEAMERR" ]; then
				echo "ERROR: Could not locate steamcmd binary: ${STEAMCMD_BINARY}, exiting.";
				quit 1
			else #when you specify -autoupdate without a valid steamdir
				echo "WARNING: Could not locate steamcmd binary, ignoring autoupdate."
				return 0
			fi
		fi
	fi

	return 1
}

run() {
	# Runs the steam update and server
	# Loops if RESTART is set
	# Debugs if server failure is detected
	# Note: if RESTART is not set then
	# 1. DEBUG is set then the server is NOT exec'd
	# 2. DEBUG is not set the the server is exec'd

	# Workaround for "Unable to initialize Steam" error. -R4to0 (14 August 2021)
	if [ ! -f "steam_appid.txt" ]; then
		echo 225840 > "steam_appid.txt"
	fi

	if [ -n "${RESTART}" ]; then
		echo "Server will auto-restart if there is a crash."

		#loop forever
		while true
		do
			# Update if needed
			update

			# Run the server
			$GAME_BIN_CMD
			retval=$?
			if [ $retval -eq 0 ] && [ -n "${RESTART}" ]; then
				break; # if 0 is returned then just quit
			fi

			debugcore $retval

			echo "$(date): Server restart in ${TIMEOUT} seconds"

			# don't thrash the hard disk if the server dies, wait a little
			sleep "${TIMEOUT}"
		done # while true
	else
		# Update if needed
		update

		# Run the server
		if [ "${DEBUG}" -eq 0 ]; then
			# debug not requested we can exec
			$GAME_BIN_CMD
		else
			# debug requested we can't exec
			$GAME_BIN_CMD
			debugcore $?
		fi
	fi
}

quit() {
	# Exits with the give error code, 1
	# if none specified.
	# exit code 2 also prints syntax
	exitcode="$1"

	# default to failure
	if [ -z "${exitcode}" ]; then
		exitcode=1
	fi

	case "${exitcode}" in
	0)
		echo "$(date): Server Quit" ;;
	2)
		syntax ;;
	*)
		echo "$(date): Server Failed" ;;
	esac

	# Remove pid file
	if [ -n "${PID_FILE}" ] && [ -f "${PID_FILE}" ]; then
		# The specified pid file
		rm -f "${PID_FILE}"
	fi

	# reset SIGINT and then kill ourselves properly
	trap - 2
	kill -2 $$
}

# Initialise
init "${@}"

# Run
run

# Quit normally
quit 0
