dayzdockerserver/server/bin/dz
Daniel Ceregatti 80adb60aa8 As of today, we have a release version! I can't believe it!
We no longer need to lower case the files.
Switch to the release server id.
Move rcon from the web container to the server container.
Handle .c files on a per-map basis.
Update per-map .c files.
2024-02-20 14:28:59 -08:00

652 lines
19 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
w|wipe - Wipes the current storage_1
${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() {
if [[ ${DONT_START} != "" ]]
then
exit 0
fi
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}"
# Back these files up into a new directory with the current time stamp in the name
DIR="${SERVER_PROFILE}/logs/$(date +%Y-%m-%d-%H-%M-%S)"
mkdir -p ${DIR}
cd ${SERVER_PROFILE}
mv -v *.log *.RPT *.mdmp ${DIR} 2> /dev/null
}
mergexml(){
echo
# First copy the pristine files from upstream
echo -e "${green}Copying upstream files into local mpmissions for map ${MAP}${default}":
find /mpmissions/${MAP} \( \
-name "cfgeconomycore.xml" \
-o -name "cfgenvironment.xml" \
-o -name "cfgeventgroups.xml" \
-o -name "cfgeventspawns.xml" \
-o -name "cfggameplay.json" \
-o -name "cfgweather.xml" \
-o -name "init.c" \
\) -exec cp -v {} ${SERVER_FILES}{} \;
# Follow https://community.bistudio.com/wiki/DayZ:Central_Economy_mission_files_modding
# Remove any existing files, via the mod_ and custom_ prefixes
rm -rf ${MPMISSIONS}/${MAP}/mod_*
rm -rf ${MPMISSIONS}/${MAP}/custom_*
for link in $(ls -tdr ${SERVER_PROFILE}/@* 2> /dev/null)
do
ID=$(readlink ${link} | awk -F/ '{print $NF}')
C=""
FOUND=0
# This loop handles Central Economy files
# A matrix of file names -> root node -> child node permutations
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
if [[ ${FOUND} = 0 ]]
then
MODNAME=$(get_mod_name ${ID})
echo
echo -e "${green}Adding mod integration ${MODNAME}${default}"
FOUND=1
fi
echo -n "Copy "
mkdir -p ${MPMISSIONS}/${MAP}/mod_${ID}
cp -v ${WORKSHOP_DIR}/${ID}/${var,,}.xml ${MPMISSIONS}/${MAP}/mod_${ID}/${var,,}.xml
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 the current mpmissions file
echo "Create new XML node <ce folder=\"mod_${ID}\"> -> ${MPMISSIONS}/${MAP}/cfgeconomycore.xml"
find ${MPMISSIONS}/${MAP} -name cfgeconomycore.xml -exec \
xmlstarlet ed -L -s / -t elem -n ce \
-a /ce -t attr -n folder -v "mod_${ID}" ${C} \
-m /ce /economycore {} \;
fi
# These are merged directly into the upstream file
for i in "CFGEVENTGROUPS:eventgroupdef:group" "CFGEVENTSPAWNS:eventposdef:event" "CFGENVIRONMENT:env:territories/territory"
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 "Merge XML ${WORKSHOP_DIR}/${ID}/${var,,}.xml -> ${MPMISSIONS}/${MAP}/${var,,}.xml"
rm -f /tmp/x /tmp/y
xmlmerge -o /tmp/x ${WORKSHOP_DIR}/${ID}/${var,,}.xml ${MPMISSIONS}/${MAP}/${var,,}.xml
xmlstarlet fo /tmp/x > /tmp/y
# Ensure the XML is valid
xmllint --noout /tmp/y || (
echo
echo "Merged XML file ${MPMISSIONS}/${MAP}/${var,,}.xml is not valid! Can't continue!"
echo
exit 1
)
mv /tmp/y ${MPMISSIONS}/${MAP}/${var,,}.xml
fi
done
# These are merged directly into the upstream file, but are JSON
for var in "CFGGAMEPLAY"
do
if [ -f "${WORKSHOP_DIR}/${ID}/${var,,}.json" ]
then
if [[ ${FOUND} = 0 ]]
then
MODNAME=$(get_mod_name ${ID})
echo
echo -e "${green}Adding mod integration ${MODNAME}${default}"
FOUND=1
fi
echo "Merge JSON '${WORKSHOP_DIR}/${ID}/${var,,}.json' -> '${MPMISSIONS}/${MAP}/${var,,}.json'"
rm -f /tmp/x /tmp/y
jq -s '.[0] * .[1]' ${MPMISSIONS}/${MAP}/${var,,}.json ${WORKSHOP_DIR}/${ID}/${var,,}.json > /tmp/x
mv /tmp/x ${MPMISSIONS}/${MAP}/${var,,}.json
fi
done
# These are merged directly into the upstream file, but are C
for var in "INIT"
do
if [ -f "${WORKSHOP_DIR}/${ID}/${var,,}.c.${MAP}" ]
then
if [[ ${FOUND} = 0 ]]
then
MODNAME=$(get_mod_name ${ID})
echo
echo -e "${green}Adding mod integration ${MODNAME}${default}"
FOUND=1
fi
echo "Patch '${WORKSHOP_DIR}/${ID}/${var,,}.c(.diff)' -> '${MPMISSIONS}/${MAP}/${var,,}.c'"
patch -s -p0 ${MPMISSIONS}/${MAP}/${var,,}.c.${MAP} < ${WORKSHOP_DIR}/${ID}/${var,,}.c || (
echo "Patch failed!"
exit 1
)
fi
done
# These are copied verbatim
for var in "CFGWEATHER"
do
if [ -f "${WORKSHOP_DIR}/${ID}/${var,,}.xml" ]
then
if [[ ${FOUND} = 0 ]]
then
MODNAME=$(get_mod_name ${ID})
echo
echo -e "${green}Adding mod integration ${MODNAME}${default}"
FOUND=1
fi
echo "Copy -> '${WORKSHOP_DIR}/${ID}/${var,,}.xml' -> '${MPMISSIONS}/${MAP}/${var,,}.xml'"
cp ${WORKSHOP_DIR}/${ID}/${var,,}.xml ${MPMISSIONS}/${MAP}/${var,,}.xml
fi
done
# Here are where start actions happen
if [ -f "${WORKSHOP_DIR}/${ID}/start.sh" ]
then
echo "Running start script -> ${WORKSHOP_DIR}/${ID}/start.sh"
pushd ${MPMISSIONS}/${MAP} > /dev/null
bash -x "${WORKSHOP_DIR}/${ID}/start.sh"
popd > /dev/null
fi
done
if [ -d ${SERVER_PROFILE}/custom ]
then
for dir in $(ls ${SERVER_PROFILE}/custom 2> /dev/null)
do
FOUND=0
C=""
for i in "CFGEVENTGROUPS:eventgroupdef:group" "CFGSPAWNABLETYPES:spawnabletypes:type" "EVENTS:events:event" "TYPES:types:type" "GLOBALS:globals:var"
do
var=$(echo ${i} | cut -d: -f1)
CHECK=$(echo ${i} | cut -d: -f2)
CHILD=$(echo ${i} | cut -d: -f3)
if [ -f "${SERVER_PROFILE}/custom/${dir}/${var,,}.xml" ]
then
if [[ ${FOUND} = 0 ]]
then
echo
echo -e "${green}Adding custom integration ${dir}${default}"
FOUND=1
fi
mkdir -p ${MPMISSIONS}/${MAP}/custom_${dir}
echo -n "Copy "
cp -v ${SERVER_PROFILE}/custom/${dir}/${var,,}.xml ${MPMISSIONS}/${MAP}/custom_${dir}/${var,,}.xml
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 the current mpmissions file
echo "Create new XML node <ce folder=\"custom_${dir}\"> -> ${MPMISSIONS}/${MAP}/cfgeconomycore.xml"
find ${MPMISSIONS}/${MAP} -name cfgeconomycore.xml -exec \
xmlstarlet ed -L -s / -t elem -n ce \
-a /ce -t attr -n folder -v "custom_${dir}" ${C} \
-m /ce /economycore {} \;
fi
# These are merged directly into the upstream file, but are JSON
for var in "CFGGAMEPLAY"
do
if [ -f "${SERVER_PROFILE}/custom/${dir}/${var,,}.json" ]
then
if [[ ${FOUND} = 0 ]]
then
echo
echo -e "${green}Adding custom integration ${dir}${default}"
FOUND=1
fi
echo "Merge JSON '${SERVER_PROFILE}/custom/${dir}/${var,,}.json' -> '${MPMISSIONS}/${MAP}/${var,,}.json'"
rm -f /tmp/x /tmp/y
jq -s '.[0] * .[1]' ${MPMISSIONS}/${MAP}/${var,,}.json ${SERVER_PROFILE}/custom/${dir}/${var,,}.json > /tmp/x
mv /tmp/x ${MPMISSIONS}/${MAP}/${var,,}.json
fi
done
if [[ ${FOUND} = 1 ]]
then
# Copy any other files in the mod directory into a custom directory under mpmissions
mkdir -p ${MPMISSIONS}/${MAP}/custom_${dir}
for file in $(ls ${SERVER_PROFILE}/custom/${dir} 2> /dev/null)
do
echo -n "Copy "
cp -av ${SERVER_PROFILE}/custom/${dir}/${file} ${MPMISSIONS}/${MAP}/custom_${dir}
done
fi
done
fi
}
# Start the server in the foreground
start(){
# If we're developing, just block the container
if [[ ${DEVELOPMENT} = "1" ]] && [[ ${DONT_START} = "" ]]
then
echo "DEVELOPMENT mode, blocking. Unset DEVELOPMENT in the current environment to run the server."
trap '
echo "Caught SIGTERM/SIGINT..."
exit 0
' SIGTERM SIGINT
while [ true ]
do
sleep 1
done
exit 0
fi
# Ensure mpmissions has at least one map. If not, copy it from the local read-only volume that stores pristine mpmissons directories
if [ ! -d "${MPMISSIONS}/${MAP}" ] && [ -d "/mpmissions/${MAP}" ]
then
echo
echo "Performing one-time copy of ${MAP} mpmissions..."
echo
cp -av /mpmissions/${MAP} ${MPMISSIONS}
fi
# 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
if [[ ${DONT_START} != "" ]]
then
echo
echo "Not starting server, as DONT_START is set"
echo
exit 0
fi
echo
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(hich?) a or d
W=${1}
# Pop that off so we can loop over the rest
shift
# Default values are when activating
WW=""
COLOR="${green}"
if [[ ${W} = 0 ]]
then
# Deactivating instead
WW="de"
COLOR="${red}"
fi
# Loop over the rest of the argument(s)
for i in ${@}
do
# Get the mod id and name
ID=$(get_mod_id ${i} ${W})
MODNAME=$(get_mod_name ${ID})
# 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
done
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 %.30s %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
}
get_map_name(){
MAP="none"
# Map name
if [[ -f ${SERVER_CFG_DST} ]]
then
MAP=$(grep -E "template=" ${SERVER_CFG_DST} | grep -vE "^//" | cut -d= -f2 | cut -d\; -f1 | tr -d '"')
fi
echo ${MAP}
}
# 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="${DAYS} days "$(date -d@$(($(date +%s) - $(date +%s -r ${SERVER_PROFILE}/server.pid))) -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=${MAP}
# 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
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} 1> /dev/null
cp -a "${i}" "${B}"
done
cp -a /profiles ${B}
}
wipe(){
DIR="${MPMISSIONS}/${MAP}/storage_1"
if ! [ -d "${DIR}" ]
then
echo "Storage directory ${DIR} does not exist"
return
fi
if prompt_yn "Wipe server storage directory '${DIR}'?"
then
rm -rf "${DIR}"
echo "Storage ${DIR} removed"
else
echo "Storage directory ${DIR} NOT removed..."
fi
}
MAP=$(get_map_name)
# 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|restart)
restart "${@}"
;;
start)
start "${@}"
;;
s|status)
status "${@}"
;;
stop)
stop "${@}"
;;
w|wipe)
wipe "${@}"
;;
*)
usage "$*"
;;
esac