dayzdockerserver/server/bin/dz
2023-08-31 17:05:57 -07:00

435 lines
13 KiB
Bash
Executable file

#!/usr/bin/env bash
source dz-common
# Server container base directories
MPMISSIONS="${SERVER_FILES}/mpmissions"
mkdir -p ${SERVER_PROFILE}/battleye
# Server configuration file
SERVER_CFG_FILE="serverDZ.cfg"
SERVER_CFG_DST="${SERVER_PROFILE}/${SERVER_CFG_FILE}"
SERVER_CFG_SRC="${FILES}/${SERVER_CFG_FILE}"
# Command line parameters except mod, as that is handled separately.
parameters="-config=${SERVER_CFG_DST} -port=${port} -freezecheck -BEpath=${SERVER_PROFILE}/battleye -profiles=${SERVER_PROFILE} -nologs"
# Where mods are installed.
WORKSHOP_DIR="/mods/${release_client_appid}"
# Backups
BACKUP_DIR="${HOME}/backup"
if [ ! -d "${BACKUP_DIR}" ]
then
mkdir -p "${BACKUP_DIR}"
fi
mod_command_line=""
# 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 - Activate an installed DayZ Workshop items by id or index
b|backup - Backup the mission storage files in all mission directories
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 - Deactivate an installed DayZ Workshop items by id or index - Keeps the mod files but excludes it from the mod parameter
f|force - Forcibly kill the server. Use only as a last resort if the server won't shut down
l|list - List Workshop items and their details
n|rcon - Connect to the server using a python RCON client
r|restart - Restart the server without restarting the container
s|status - Shows the server's status: Running, uptime, mods, parameters, mod parameter, etc.
stop - Stop the server
${default}"
exit 1
}
loadconfig(){
if [ ! -f "${SERVER_INSTALL_FILE}" ]
then
echo
echo -e "The DayZ server files are not installed. You need to do this first in the web UI."
echo
exit 1
fi
# 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="${SERVER_PROFILE}/battleye/beserver_x64.cfg"
ALT_BE_SERVER_FILE=$(find ${SERVER_PROFILE}/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}" <<EOF
RConPassword ${passwd}
RestrictRCon 0
RConPort ${rcon_port}
EOF
fi
printf "[ ${cyan}INFO${default} ] New RCON password: ${yellow}${passwd}${default}\n"
else
if [ -f "${BE_SERVER_FILE}" ]
then
FILE="${BE_SERVER_FILE}"
elif [ -f "${ALT_BE_SERVER_FILE}" ]
then
FILE="${ALT_BE_SERVER_FILE}"
fi
passwd=$(grep RConPassword ${FILE} | awk '{print $2}')
# printf "[ ${cyan}INFO${default} ] Using existing RCON password: ${yellow}${passwd}${default}\n"
fi
cp /usr/local/py3rcon/configexample.json ~/py3rcon.config.json
jq --arg port 2303 --arg rcon_password b0fNIBVfkM \
'.logfile="py3rcon.log" | .loglevel=0 | .server.port=$port | .server.rcon_password=$rcon_password | del(.repeatMessage)' \
/usr/local/py3rcon/configexample.json \
> ~/py3rcon.config.json
}
# 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 "${SERVER_PROFILE}" -name error.log -exec head {} \; -exec tail -n 30 {} \; -exec rm -f {} \;
echo
echo -e "========================================== script*.log ========================================"
find "${SERVER_PROFILE}" -name "script*.log" -exec head {} \; -exec tail -n 30 {} \; -exec rm -f {} \;
echo
echo -e "========================================== *.RPT =============================================="
find "${SERVER_PROFILE}" -name "*.RPT" -exec ls -la {} \; -exec tail -n 30 {} \; -exec rm -f {} \;
echo
echo -e "========================================== End log ======================================${default}"
}
mergexml(){
# First copy the pristine files from upstream
echo "Copying pristine versions of cfgeconomycore.xml and cfgeventspawns.xml..."
find /mpmissions -name cfgeconomycore.xml -exec cp -v {} ${SERVER_FILES}{} \;
# find /mpmissions -name cfgeventspawns.xml -exec cp {} ${SERVER_FILES}{} \;
# Follow https://community.bistudio.com/wiki/DayZ:Central_Economy_mission_files_modding and make a single XML
# file for the ones that can go into it.
for link in $(ls -tdr ${SERVER_PROFILE}/@* 2> /dev/null)
do
ID=$(readlink ${link} | awk -F/ '{print $NF}')
# Going to have to maintain a matrix of file names -> root node -> child node permutations
C=""
for i in "CFGSPAWNABLETYPES:spawnabletypes:type" "EVENTS:events:event" "TYPES:types:type"
do
var=$(echo ${i} | cut -d: -f1)
CHECK=$(echo ${i} | cut -d: -f2)
CHILD=$(echo ${i} | cut -d: -f3)
if [ -f "${WORKSHOP_DIR}/${ID}/${var,,}.xml" ]
then
FOUND=1
echo "Adding ${WORKSHOP_DIR}/${ID}/${var,,}.xml to cfgeconomycore..."
for dir in $(ls ${MPMISSIONS})
do
mkdir -pv ${MPMISSIONS}/${dir}/${ID}
cp -v ${WORKSHOP_DIR}/${ID}/${var,,}.xml ${MPMISSIONS}/${dir}/${ID}/${var,,}.xml
done
C+="-s / -t elem -n file -a /file -t attr -n name -v ${var,,}.xml -a /file -t attr -n type -v ${CHECK} -m /file /ce "
fi
done
if [[ ${C} != "" ]]
then
# Merge into every mpmissions file
find ${MPMISSIONS} -name cfgeconomycore.xml -exec \
xmlstarlet ed -L -s / -t elem -n ce \
-a /ce -t attr -n folder -v "${ID}" \
${C} \
-m /ce /economycore {} \;
fi
# These still have to be merged into the upstream file
for i in "CFGEVENTSPAWNS:eventposdef:event"
do
var=$(echo ${i} | cut -d: -f1)
CHECK=$(echo ${i} | cut -d: -f2)
CHILD=$(echo ${i} | cut -d: -f3)
if [ -f "${WORKSHOP_DIR}/${ID}/${var,,}.xml" ]
then
echo "Merging ${var,,}.xml..."
# xmlstarlet ed -L -s / -t name event
fi
done
done
}
# 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
get_mods
mergexml
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)
}
# Handle any changes in the server config file by allowing them to be merged after viewing a diff.
config(){
if ! diff -q "${SERVER_CFG_DST}" "${SERVER_CFG_SRC}"
then
echo "========================================================================="
diff -Nau --color "${SERVER_CFG_DST}" "${SERVER_CFG_SRC}" | more
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
else
echo "No differences found between ${SERVER_CFG_SRC} and ${SERVER_CFG_DST}"
fi
}
# Activate / Deactivate a mod
activate(){
W=${1}
shift
WW=""
COLOR="${green}"
if [[ ${W} = 0 ]]
then
WW="de"
COLOR="${red}"
fi
ID=$(get_mod_id ${1} ${W})
MODNAME=$(get_mod_name ${ID})
# echo "ID: ${ID}, MODNAME: ${MODNAME}"
# exit 0
# Toggle state or report nothing burger
pushd "${SERVER_PROFILE}" > /dev/null
if [[ ${W} = 0 ]] && [ -L "${SERVER_PROFILE}/@${MODNAME}" ]
then
echo -n "Removing mod symlink: "
rm -vf "${SERVER_PROFILE}/@${MODNAME}"
elif [[ ${W} = 1 ]]
then
echo -n "Creating mod symlink: "
ln -sfv "${WORKSHOP_DIR}/${ID}" "${SERVER_PROFILE}/@${MODNAME}"
else
echo -e "Mod id ${ID} - ${COLOR}${MODNAME}${default} - is already ${WW}active"
fi
copy_keys ${W} ${ID}
echo -e "Mod id ${ID} - ${COLOR}${MODNAME}${default} ${WW}activated"
popd > /dev/null
status
}
# Our internal RCON
rcon(){
exec /usr/local/py3rcon/py3rcon.py --gui ~/py3rcon.config.json
}
# List mods
activelist(){
X=1
C="${green}"
spaces=" "
have=no
for link in $(ls -tdr ${SERVER_PROFILE}/@* 2> /dev/null)
do
if [[ ${have} = "no" ]]
then
have="yes"
echo -e "\n ID Name URL Size"
echo "------------------------------------------------------------------------------------------------------------------------"
fi
ID=$(readlink ${link} | awk -F/ '{print $NF}')
MODNAME=$(get_mod_name ${ID})
SIZE=$(du -sh "${WORKSHOP_DIR}/${ID}" | awk '{print $1}')
printf "${C}%.3d %s %.23s %s https://steamcommunity.com/sharedfiles/filedetails/?id=%s %s${default}\n" ${X} ${ID} "${MODNAME}" "${spaces:${#MODNAME}+1}" ${ID} ${SIZE}
X=$((X+1))
done
if [[ ${have} = "no" ]]
then
echo -ne "${red}none${default}"
fi
}
# Display the status of everything
status(){
loadconfig
INSTALLED="${NO}"
RUNNING="${NO}"
# DayZ Server files installation
if [ -f "${SERVER_INSTALL_FILE}" ]
then
INSTALLED="${YES}"
fi
# Running or not
if pidof DayZServer > /dev/null
then
# Uptime
D=$(date +%s)
F=$(date +%s -r ${SERVER_PROFILE}/server_console.log)
DAYS=$(( (${D} - ${F}) / 86400 ))
# UPTIME=$(date --date="$(( ${D} - ${F} ))" +"${DAYS} days %H:%M:%S")
UPTIME="${DAYS} days "$(date -d@$(($(date +%s) - $(date +%s -r ${SERVER_PROFILE}/server_console.log))) -u +"%H hours %M minutes %S seconds")
RUNNING="${YES}\nUptime: ${green}${UPTIME}${default}"
# Current parameters
RUNNING="${RUNNING}\nRunning Parameters: $(cat /tmp/parameters)\nRunning mod parameter: $(cat /tmp/mod_command_line)"
fi
MAP="none"
# Map name
if [[ -f ${SERVER_CFG_DST} ]]
then
MAP=$(grep -E "template=" ${SERVER_CFG_DST} | grep -vE "^//")
fi
# Number of mods plus the list denoting on or off
echo -ne "
Server files installed: ${INSTALLED}"
if [[ "${INSTALLED}" = "${NO}" ]]
then
echo
echo
exit 0
fi
get_mods
echo -ne "
Active mods: "
activelist
echo -e "${MODS}
Server running: ${RUNNING}
Working parameters: ${parameters}
Working mod parameter: ${mod_command_line}"
if [[ "${INSTALLED}" = "${YES}" ]]
then
MAP=$(grep template ${SERVER_CFG_DST} | grep -v "^//" | cut -d= -f2 | cut -d\; -f1)
echo "Map: ${MAP}"
fi
echo
}
backup(){
cd ${MPMISSIONS}
DATE=$(date +'%Y-%m-%d-%H-%M-%S')
for i in $(ls)
do
B="${BACKUP_DIR}/${DATE}/"
echo "Backing up ${i} to ${B}..."
mkdir -p ${B}
cp -a "${i}" "${B}"
done
}
# Capture the first argument and shift it off so we can pass $@ to every function
C=${1}
shift || {
usage
}
case "${C}" in
a|activate)
activate 1 "${@}"
;;
add)
add "${@}"
;;
b|backup)
backup "${@}"
;;
c|config)
config "${@}"
;;
d|deactivate)
activate 0 "${@}"
;;
f|force)
force
;;
i|install)
install "${@}"
;;
l|list)
list "${@}"
;;
login)
login "${@}"
;;
n|rcon)
rcon "${@}"
;;
r|remove)
remove "${@}"
;;
r|restart)
restart "${@}"
;;
start)
start "${@}"
;;
s|status)
status "${@}"
;;
stop)
stop "${@}"
;;
*)
usage "$*"
;;
esac