#!/usr/bin/env bash # If you want/need the server and rcon ports to be different, set them here. # The steam query port is set in serverDZ.cfg. # Server port port=2302 rcon_port=2303 # Don't change anything else. # Colors default="\e[0m" red="\e[31m" green="\e[32m" yellow="\e[93m" lightblue="\e[94m" blue="\e[34m" magenta="\e[35m" cyan="\e[36m" # DayZ Experimental server Steam app ID, because there is no release version of the Linux server yet #experimental_server_appid=1042420 # DayZ release server Steam app ID. Presumably once the Linux server is released, the binaries will come # from this ID. release_server_appid=223350 # DayZ release client SteamID. This is for mods, as only the release client has them. release_client_appid=221100 # Base directories CFG_SRC_FILES="/files" SERVER_FILES="${HOME}/serverfiles" SERVER_PROFILE="${HOME}/profiles" # Server configuration file SERVER_CFG_FILE="serverDZ.cfg" SERVER_CFG_DST="${SERVER_FILES}/${SERVER_CFG_FILE}" SERVER_CFG_SRC="${CFG_SRC_FILES}/${SERVER_CFG_FILE}" # Command line parameters except mod, as that is handled separately. parameters="-config=${SERVER_CFG_FILE} -port=${port} -freezecheck -fps=60 -BEpath=${SERVER_FILES}/battleye -profiles=${SERVER_PROFILE} -nologs" # Used to check if dayZ is installed SERVER_INSTALL_FILE="${SERVER_FILES}/DayZServer" # Steam files STEAM_LOGIN="${HOME}/steamlogin" STEAMCMD=steamcmd # Workshop. This file will store metadata about what mods are installed. WORKSHOP_CFG="${HOME}/workshop.cfg" if [ ! -f "${WORKSHOP_CFG}" ] then touch "${WORKSHOP_CFG}" fi # An array to store Workshop items. Each element contains the mod's ID, name, and state (active or not). declare -a workshopID workshopfolder="${SERVER_FILES}/steamapps/workshop/content/${release_client_appid}" # Other stuff YES="${green}yes${default}" NO="${red}no${default}" # Functions # Usage usage(){ echo -e " ${red}Bad option or arguments! ${yellow}${*}${default} Usage: ${green}$(basename $0)${yellow} option [ arg1 [ arg2 ] ] Options and arguments: a|activate id [id...] - Activate one or many installed DayZ Workshop items by id add id [id...] - Add one or many DayZ Workshop items by id. Added items become active by default c|config - Update the internal serverDZ.cfg file from files/serverDZ.cfg on the host. Presents a unified diff if the internal file doesn't match the host file d|deactivate id [id...] - Deactivate one or many installed DayZ Workshop items by id - Keeps the mod files but excludes from -mod= i|install - Install the DayZ server files l|list - List Workshop items and if they are active or not g|login - Login to Steam m|modupdate - Update the mod files n|rcon - Connect to the server using a python RCON client r|remove id [id...] - Remove all vestiges of one or many Workshop items by id restart - Restart the server without restarting the container t|status - Shows the server's status as well as the state of the server files and mods (Needs install, update, etc.) stop - Stop the server u|update - Update the server files ${default}" exit 1 } # Make sure to clean up and report on exit, as these files remain in the container's volume report() { rm -f /tmp/mod_command_line /tmp/parameters echo echo -e "${yellow}========================================== error.log ==========================================" find "${HOME}" -name error.log -exec head {} \; -exec tail {} \; -exec rm -f {} \; echo echo -e "========================================== script*.log ========================================" find "${HOME}" -name "script*.log" -exec head {} \; -exec tail {} \; -exec rm -f {} \; echo echo -e "========================================== *.RPT ==============================================" find "${HOME}" -name "*.RPT" -exec ls -la {} \; -exec tail {} \; -exec rm -f {} \; echo echo -e "========================================== End crash log ======================================${default}" } # Convenience function prompt_yn(){ echo -n "${1} (y|N) " >&2 read -s -n 1 a a=$(echo ${a} | tr A-Z a-z) echo if [[ "${a}" = "y" ]] then return 0 else return 1 fi } check_install(){ if [ ! -f "${SERVER_INSTALL_FILE}" ] then echo echo -e "The DayZ server files are not installed. Run '${green}docker-compose run --rm main dayzserver install${default}'" echo exit 1 fi } # Ensures all is installed and ready before allowing operations that depends on things being ready. # Installs the initial server config file from its template. # Handles the importing of changes to that template. # Installs the initial Battleye RCON config. loadconfig(){ check_install # Handle the initial server configuration file if [ ! -f ${SERVER_CFG_DST} ] then echo "Creating initial server configuration file" cp "${SERVER_CFG_SRC}" "${SERVER_CFG_DST}" fi # battleye config and rconpassword setup # The server creates a new file from this file, which it then uses. # Let's make sure to delete it first BE_SERVER_FILE="${HOME}/serverfiles/battleye/beserver_x64.cfg" ALT_BE_SERVER_FILE=$(find ${HOME}/serverfiles/battleye -name "beserver_x64_active*") if [ ! -f "${BE_SERVER_FILE}" ] && [ ! -f "${ALT_BE_SERVER_FILE}" ] then passwd=$(openssl rand -base64 8 | tr -dc 'A-Za-z0-9') if [ "${passwd}" == "" ] then passwd=$(< /dev/urandom tr -dc 'A-Za-z0-9' | head -c10) fi if [ "${passwd}" == "" ] then printf "[ ${red}FAIL${default} ] Could not generate a passwort for RCON!\nOpen the Battleye config with 'dayzserver rcon'." exit 1 else cat > "${BE_SERVER_FILE}" < ~/py3rcon.config.json } # Start the server in the foreground start(){ # Do the report on exit. Set here so that it only happens once we're starting the server, and not for other actions. trap ' report ' EXIT mod_cmd cd ${SERVER_FILES} # Run the server. Allow docker to restart the container if the script exits with a code other than 0. This is so we can # safely shut the container down without killing the server within. printf "[ ${green}DayZ${default} ] Server starting...\n" # Save the mod command line and parameters that were used to start the server, so status reflects the running server's # actual status with those echo ${mod_command_line} > /tmp/mod_command_line echo ${parameters} > /tmp/parameters ./DayZServer ${mod_command_line} ${parameters} EXIT_CODE=$? if [ -f ${SERVER_FILES}/restart ] then rm -f ${SERVER_FILES}/restart EXIT_CODE=42 fi printf "\n[ ${yellow}DayZ${default} ] Server exited. Exit code: ${EXIT_CODE}\n" exit ${EXIT_CODE} } # Restarts the server by forcing an exit code other than 0, causing docker to restart the container. restart(){ touch "${SERVER_FILES}/restart" echo "Restarting DayZ server..." kill -TERM $(pidof DayZServer) } # Stops the server cleanly and exits 0, which will stop the container. stop(){ echo "Stopping DayZ server..." kill -TERM $(pidof DayZServer) } # Forcibly kill the server, should it be necessary. force(){ echo "Forcibly stopping DayZ server..." kill -KILL $(pidof DayZServer) } # Hanle the Steam login information. login(){ loadconfig if [ -f "${STEAM_LOGIN}" ] then if prompt_yn "The steam login is already set. Reset it?" then rm -f "${STEAM_LOGIN}" else echo "Not reset." exit 0 fi fi if [ ! -f "${STEAM_LOGIN}" ] then echo "Setting up Steam credentials" echo -n "Steam Username (anonymous): " read steamlogin if [[ "${steamlogin}" = "" ]] then echo "Steam login set to 'anonymous'" steamlogin="anonymous" fi echo "steamlogin=${steamlogin}" > "${STEAM_LOGIN}" ${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" +quit fi } # "Perform" the Steam login. This just sources the file with the Steam login name. dologin(){ loadconfig if [ -f "${STEAM_LOGIN}" ] then source "${STEAM_LOGIN}" else echo "No cached Steam credentials. Please configure this now: " login fi } # Perform the installation of the server files. install(){ loadconfig if [ ! -f "${SERVER_INSTALL_FILE}" ] || [[ ${1} = "force" ]] then mkdir -p "${SERVER_FILES}" mkdir -p "${SERVER_PROFILE}" printf "[ ${yellow}DayZ${default} ] Downloading DayZ Server-Files!\n" dologin ${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" +app_update "${release_server_appid}" validate +quit else printf "[ ${lightblue}DayZ${default} ] The Server is already installed.\n" fi } # Handle any changes in the server config file by allowing them to be merged after viewing a diff. config(){ if ! diff -q "${SERVER_CFG_SRC}" "${SERVER_CFG_DST}" then echo "=========================================================================" diff -Nau --color "${SERVER_CFG_SRC}" "${SERVER_CFG_DST}" || echo "" echo "=========================================================================" if prompt_yn "The new server configuration file differs from what's installed. Use it?" then echo "Updating the server configuration file" cp "${SERVER_CFG_SRC}" "${SERVER_CFG_DST}" else echo "NOT updating the server configuration file" fi fi } # Update the server files. update(){ dologin appmanifestfile=${SERVER_FILES}/steamapps/appmanifest_"${release_server_appid}".acf printf "[ ... ] Checking for update:" # gets currentbuild currentbuild=$(grep buildid "${appmanifestfile}" | tr '[:blank:]"' ' ' | tr -s ' ' | cut -d \ -f3) # Removes appinfo.vdf as a fix for not always getting up to date version info from SteamCMD if [ -f "${HOME}/Steam/appcache/appinfo.vdf" ] then rm -f "${HOME}/Steam/appcache/appinfo.vdf" fi # check for new build availablebuild=$(${STEAMCMD} +login "${steamlogin}" +app_info_update 1 +app_info_print "${release_server_appid}" +app_info_print \ "${release_server_appid}" +quit | sed -n '/branch/,$p' | grep -m 1 buildid | tr -cd '[:digit:]') if [ -z "${availablebuild}" ] then printf "\r[ ${red}FAIL${default} ] Checking for update:\n" printf "\r[ ${red}FAIL${default} ] Checking for update:: Not returning version info\n" exit else printf "\r[ ${green}OK${default} ] Checking for update:" fi # compare builds if [ "${currentbuild}" != "${availablebuild}" ] || [[ ${1} = "force" ]] then printf "\r[ ${green}OK${default} ] Checking for update:: Update available\n" printf "Update available:\n" printf "\tCurrent build: ${red}${currentbuild}${default}\n" printf "\tAvailable build: ${green}${availablebuild}${default}\n" printf "\thttps://steamdb.info/app/${release_server_appid}/\n" printf "\nApplying update" # run update dologin ${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" +app_update "${release_server_appid}" validate +quit modupdate else printf "\r[ ${green}OK${default} ] Checking for update:: No update available\n" printf "\nNo update available:\n" printf "\tCurrent version: ${green}${currentbuild}${default}\n" printf "\tAvailable version: ${green}${availablebuild}${default}\n" printf "\thttps://steamdb.info/app/${release_server_appid}/\n\n" fi } # Assemble the workshop list variable get_mods(){ mapfile -t workshopID < "${WORKSHOP_CFG}" workshoplist="" for i in "${workshopID[@]}" do workshoplist+=" +workshop_download_item "${release_client_appid}" "$i"" done } # Update mods modupdate(){ get_mods echo "Updating mods..." dologin ${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" ${workshoplist} +quit # Updated files come in with mixed cases. Fix that. echo -n "Fixing file names..." find "${workshopfolder}" -depth -exec rename -f 's/(.*)\/([^\/]*)/$1\/\L$2/' {} \; echo "done" echo } # Add a mod add(){ if [ -d "${workshopfolder}/${1}" ] then echo -e "${yellow}Warning: The mod directory ${workshopfolder}/${1} already exists!${default}" MODNAME=$(cut -d '"' -f 2 <<< $(grep name ${workshopfolder}/${1}/meta.cpp)) fi if [ -L "${SERVER_FILES}/@${MODNAME}" ] then echo -e "${yellow}Warning: The mod symlink ${SERVER_FILES}/@${MODNAME} already exists!${default}" fi if grep -qP "\b${1}\b" "${WORKSHOP_CFG}" then echo "The mod with id ${1} is already in the workshop configuration." return fi echo "Adding mod id ${1}" echo "${1}:MODNAME:1" >> ${WORKSHOP_CFG} modupdate # Make sure the install succeeded if [ ! -d "${workshopfolder}/${1}" ] then echo -e "${red}Mod installation failed: The mod directory ${workshopfolder}/${1} was not created!${default}" echo "Installation failed! See above (You probably need to use a real Steam login)" # The mod is added temporarily into the workshop config. Since the installation failed, reemove it instead of updating it. head -n-1 "${WORKSHOP_CFG}" > /tmp/workshop.cfg.tmp mv /tmp/workshop.cfg.tmp "${WORKSHOP_CFG}" return fi # Get the name of the newly added mod MODNAME=$(cut -d '"' -f 2 <<< $(grep name ${workshopfolder}/${1}/meta.cpp)) symlink ${1} "${MODNAME}" # Lower case all the files in mod directories. find "${workshopfolder}/${1}" -depth -exec rename -f 's/(.*)\/([^\/]*)/$1\/\L$2/' {} \; # Copy the key files copy_keys ${1} # Set the mod name in the workshop config file, as we don't know this at the start. sed -i "${WORKSHOP_CFG}" -e "s/${1}:MODNAME/${1}:${MODNAME}/" echo -e "Mod id ${1} - ${green}${MODNAME}${default} - added" } # Remove a mod remove(){ if [ -d "${workshopfolder}/${1}" ] then MODNAME=$(cut -d '"' -f 2 <<< $(grep name ${workshopfolder}/${1}/meta.cpp)) echo "Removing directory ${workshopfolder}/${1}" rm -rf "${workshopfolder}/${1}" fi if [ -L "${SERVER_FILES}/@${MODNAME}" ] then echo "Removing symlink ${SERVER_FILES}/@${MODNAME}" rm -f "${SERVER_FILES}/@${MODNAME}" fi if grep -q ${1} "${WORKSHOP_CFG}" then echo "Removing workshop file entry" sed -i "${WORKSHOP_CFG}" -e "/${1}:/d" fi echo "Mod id ${1} - ${MODNAME} - removed" } # Activate / Deactivate a mod activate(){ W=${1} get_mods X=1 for i in "${workshopID[@]}" do ID=$(echo ${i} | cut -d: -f1) NAME=$(echo ${i} | cut -d: -f2) ACTIVE=$(echo ${i} | cut -d: -f3) if [[ ${ID} = ${1} ]] || [[ ${X} = ${1} ]] then if [[ ${ACTIVE} = "0" ]] then sed -i "${WORKSHOP_CFG}" -e 's/'${ID}':\(.*\):0/'${ID}':\1:1/' symlink ${ID} "${NAME}" copy_keys ${ID} echo "Activated mod id ${ID}" else echo "Mod id ${ID} was already active" fi fi X=$((X+1)) done # list } # List mods list(){ get_mods X=1 spaces=" " echo -e " ID Name Active URL" echo "---------------------------------------------------------------------------------------------------------------------" for i in "${workshopID[@]}" do ID=$(echo ${i} | cut -d: -f1) NAME=$(echo ${i} | cut -d: -f2) ACTIVE=$(echo ${i} | cut -d: -f3) if [[ ${ACTIVE} = "1" ]] then C="${green}" else C="${red}" fi printf "${C}%.3d %s %.23s %s %s https://steamcommunity.com/sharedfiles/filedetails/?id=%s${default}\n" ${X} ${ID} "${NAME}" "${spaces:${#NAME}+1}" ${ACTIVE} ${ID} X=$((X+1)) done } # Copy mod keys copy_keys(){ echo "Copying key files..." cp -v ${workshopfolder}/${1}/keys/* "${SERVER_FILES}/keys/" } # Symlink mods symlink(){ # Symlink it if [ ! -L "${SERVER_FILES}/@${2}" ] then ln -s ${workshopfolder}/${1} "${SERVER_FILES}/@${2}" echo "Created symlink ${workshopfolder}/${1} ${SERVER_FILES}/@${2}" else echo "Symlink already existed" fi } # Assemble the mod command line mod_cmd(){ get_mods mod_command_line="" for i in "${workshopID[@]}" do NAME=$(echo ${i} | cut -d: -f2) ACTIVE=$(echo ${i} | cut -d: -f3) if [[ ${ACTIVE} = "1" ]] then mod_command_line="${mod_command_line}@${NAME};" fi done if [[ ${mod_command_line} != "" ]] then mod_command_line='-mod='${mod_command_line::-1} fi } # Our internal RCON rcon(){ exec /usr/local/py3rcon/py3rcon.py --gui ~/py3rcon.config.json } # Display the status of everything status(){ INSTALLED="${NO}" LOGGED_IN="${NO}" RUNNING="${NO}" get_mods MODS_INSTALLED="" A=0 for i in "${workshopID[@]}" do ID=$(echo ${i} | cut -d: -f1) NAME=$(echo ${i} | cut -d: -f2) ACTIVE=$(echo ${i} | cut -d: -f3) if [[ ${ACTIVE} = "1" ]] then C="${green}" A=$((A+1)) else C="${red}" fi MODS_INSTALLED="${MODS_INSTALLED} ${C}${NAME}${default}\n" done MODS_INSTALLED="Mods installed: ${green}${#workshopID[@]}${default} Active: ${green}${A}${default}\n${MODS_INSTALLED}" # DayZ Server files installation if [ -f "${SERVER_INSTALL_FILE}" ] then INSTALLED="${YES}" fi # Logged into Steam if [ -f "${STEAM_LOGIN}" ] then LOGGED_IN="${YES}" if grep -q anonymous "${STEAM_LOGIN}" then ANONYMOUS="${yellow}(as anonymous)${default}" else ANONYMOUS="${green}(not anonymous)${default}" fi fi # Running or not if pidof DayZServer > /dev/null then # Uptime UPTIME=$(date -d@$(($(date +%s) - $(date +%s -r ${SERVER_PROFILE}/server_console.log))) -u +%H:%M:%S) RUNNING="${YES}\nUptime: ${green}${UPTIME}${default}" # Current parameters RUNNING="${RUNNING}\nRunning Parameters: $(cat /tmp/parameters)\nRunning mod parameter: $(cat /tmp/mod_command_line)" fi mod_cmd # Number of mods plus the list denoting on or off echo -e " Logged in to Steam: ${LOGGED_IN} ${ANONYMOUS} Server files installed: ${INSTALLED} ${MODS_INSTALLED}Server running: ${RUNNING} Working parameters:${parameters} Working mod parameter: ${mod_command_line} " } # Capture the first argument and shift it off so we can pass $@ to every function C=${1} shift case "${C}" in a|activate) activate 1 "${@}" ;; add) add "${@}" ;; cmd) mod_cmd "${@}" ;; c|config) config "${@}" ;; d|deactivate) activate 0 "${@}" ;; f|force) force ;; i|install) install "${@}" ;; l|list) list "${@}" ;; login) login "${@}" ;; m|modupdate) modupdate "${@}" ;; n|rcon) rcon "${@}" ;; r|remove) remove "${@}" ;; restart) restart "${@}" ;; start) start "${@}" ;; s|status) status "${@}" ;; stop) stop "${@}" ;; u|update) update "${@}" ;; **) usage "$*" ;; esac