This commit is contained in:
2024-10-20 16:20:55 +02:00
commit 33702fce0b
122 changed files with 9293 additions and 0 deletions

81
web/Dockerfile Normal file
View File

@@ -0,0 +1,81 @@
FROM debian:bookworm-slim
# Replace shell with bash so we can source files
RUN rm /bin/sh && ln -s /bin/bash /bin/sh
# Set debconf to run non-interactively and agree to the SteamCMD EULA
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \
&& echo steam steam/question select "I AGREE" | debconf-set-selections \
&& echo steam steam/license note '' | debconf-set-selections \
&& dpkg --add-architecture i386
# Add backports and contrib
RUN sed -i /etc/apt/sources.list.d/debian.sources -e 's/Components: main/Components: main contrib non-free/g'
# Install _only_ the necessary packages
RUN apt-get update && apt-get -y upgrade && apt-get -y install --no-install-recommends \
binutils \
curl \
git \
gwenhywfar-tools \
jq \
libxml2-utils \
locales \
nano \
procps \
wget \
rename \
steamcmd \
xmlstarlet
# Set the locale
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
# Steamcmd needs its path added, as it ends up in /usr/games.
# Our server script is bind mounted in /files in docker-compose.
ENV PATH /usr/games:/files/bin:/web/bin:${PATH}
# Install nodejs
RUN mkdir /usr/local/nvm
ENV NVM_DIR /usr/local/nvm
ENV NODE_VERSION 20.12.2
RUN echo $NODE_VERSION
# Install nvm with node and npm
RUN wget -O - https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash \
&& . $NVM_DIR/nvm.sh \
&& nvm install $NODE_VERSION \
&& nvm alias default $NODE_VERSION \
&& nvm use default
ENV NODE_PATH $NVM_DIR/versions/node/v$NODE_VERSION/lib/node_modules
ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
# Setup a non-privileged user
ARG USER_ID
RUN groupadd -g ${USER_ID} user && \
useradd -l -u ${USER_ID} -m -g user user && \
mkdir -p /home/user /serverfiles/mpmissions /serverfiles/steamapps/workshop/content /web && \
chown -R user:user /home/user /serverfiles /web
# Shut steamcmd up
RUN cd /usr/lib/i386-linux-gnu && ln -s /web/bin/steamservice.so
# Add bercon https://github.com/WoozyMasta/bercon
RUN wget https://github.com/WoozyMasta/bercon/releases/download/1.0.0/bercon \
&& chmod +x bercon \
&& mv bercon /usr/bin
# Use our non-privileged user
USER user
# The dayzserver script expects a home directory to itself.
WORKDIR /home/user
# Run the web server
ENTRYPOINT ["entrypoint.sh"]
CMD ["start.sh"]

378
web/bin/dz Normal file
View File

@@ -0,0 +1,378 @@
#!/usr/bin/env bash
source dz-common
# An array to store Workshop items. Each element contains the mod's ID, name, and state (active or not).
WORKSHOP_DIR="/mods/${release_client_appid}"
if [ ! -d ${WORKSHOP_DIR} ]
then
mkdir -p ${WORKSHOP_DIR}
fi
workshoplist=""
# Functions
# Usage
usage(){
echo -e "
${red}Bad option or arguments! ${yellow}${*}${default}
Usage: ${green}$(basename $0)${yellow} option [ arg1 [ arg2 ] ]
Options and arguments:
a|add id - Add a DayZ Workshop item by id. Added items become active by default
i|install - Install the DayZ server files
g|login - Login to Steam.
m|modupdate - Update the mod files
p|map id - Install a mod's mpmissions files by id. (Presumes template exists)
r|remove id - Remove all files and directories of a Workshop item by id
l|s|status - Shows Steam login status, if base files are installed, installed mods
u|update - Update the server files
x|xml id - Get and normalize XML files from a mod's template by id (Presumes template exists)
${default}"
exit 1
}
# Manage the mod symlink
symlink(){
W=${1}
ID=${2}
NAME=${3}
if [ ! -L "${SERVER_FILES}/@${NAME}" ] && [[ ${W} = 1 ]]
then
ln -sv ${WORKSHOP_DIR}/${ID} "${SERVER_FILES}/@${NAME}"
elif [[ "${W}" = "0" ]]
then
rm -vf "${SERVER_FILES}/@${NAME}"
fi
}
installxml(){
ID=${1}
# Going to have to maintain a matrix of file names -> root node -> child node permutations
for i in "CFGEVENTGROUPS:eventgroupdef:group" "CFGEVENTSPAWNS:eventposdef:event" "CFGSPAWNABLETYPES:spawnabletypes:type" "EVENTS:events:event" "TYPES:types:type"
do
var=$(echo ${i} | cut -d: -f1)
CHECK=$(echo ${i} | cut -d: -f2)
if [ -f "${WORKSHOP_DIR}/${ID}/${var,,}.xml" ]
then
echo "Normalizing ${WORKSHOP_DIR}/${ID}/${var,,}.xml..."
cp ${WORKSHOP_DIR}/${ID}/${var,,}.xml /tmp/x
# Quirks
# Some cfgeventspanws.xml files have <events> instead of <eventposdef>. Let's just try to fix that first.
if [[ ${var} = "CFGEVENTSPAWNS" ]]
then
if grep -q '<events>' /tmp/x
then
echo " - (Quirk) has <events> instead of <eventposdef>. fixing..."
xmlstarlet ed -L -r "events" -v "eventposdef" /tmp/x
fi
fi
if ! grep -q '<'${CHECK}'>' /tmp/x
then
echo " - has no root node <${CHECK}>. fixing..."
echo '<'${CHECK}'>' > /tmp/y
cat /tmp/x >> /tmp/y
echo '</'${CHECK}'>' >> /tmp/y
xmlstarlet fo /tmp/y > /tmp/x
fi
if ! grep -q '<?xml' /tmp/x
then
echo " - has no XML node, fixing..."
xmlstarlet fo /tmp/x > /tmp/y
mv /tmp/y /tmp/x
fi
xmllint --noout /tmp/x && (
# Keep the normalized version in the /mods directory
cp /tmp/x ${WORKSHOP_DIR}/${ID}/${var,,}.xml
echo -e "${green}${WORKSHOP_DIR}/${ID}/${var,,}.xml passes XML lint test!${default}"
) || (
echo -e "${yellow}The final ${WORKSHOP_DIR}/${ID}/${var,,}.xml does not pass XML lint test! IT WAS NOT COPIED!${default}"
)
fi
done
exit 0
}
# Add a mod
add(){
if [ -d "${WORKSHOP_DIR}/${1}" ]
then
echo -e "${yellow}Warning: The mod directory ${WORKSHOP_DIR}/${1} already exists!${default}"
MODNAME=$(get_mod_name ${1})
fi
if [ -L "${SERVER_FILES}/@${MODNAME}" ]
then
echo -e "${yellow}Warning: The mod symlink ${SERVER_FILES}/@${MODNAME} already exists!${default}"
fi
echo "Adding mod id ${1}"
dologin
${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" +workshop_download_item "${release_client_appid}" "${1}" +quit
# Make sure the install succeeded
if [ ! -d "${WORKSHOP_DIR}/${1}" ]
then
echo -e "${red}Mod installation failed: The mod directory ${WORKSHOP_DIR}/${1} was not created!${default}"
echo "Installation failed! See above (You probably need to use a real Steam login)"
return
fi
# Get the name of the newly added mod
MODNAME=$(get_mod_name ${1})
symlink 1 ${1} "${MODNAME}"
echo -e "Mod id ${1} - ${green}${MODNAME}${default} - added"
xml ${ID}
map ${ID}
}
# Remove a mod
remove(){
DIR="${WORKSHOP_DIR}/${1:?}"
if [ -d "${DIR}" ]
then
MODNAME=$(get_mod_name ${1})
echo "Removing directory ${DIR}"
rm -rf "${DIR}"
else
echo "Directory ${DIR} doesn't exist?"
fi
if [ -L "${SERVER_FILES}/@${MODNAME}" ]
then
echo "Removing symlink ${SERVER_FILES}/@${MODNAME}"
rm -f "${SERVER_FILES}/@${MODNAME}"
else
echo "Symlink ${SERVER_FILES}/@${MODNAME} doesn't exist?"
fi
echo -e "Mod id ${1} - ${red}${MODNAME}${default} - removed"
}
# Handle the Steam login information.
login(){
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(){
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(){
if [ ! -f "${SERVER_INSTALL_FILE}" ] || [[ ${1} = "force" ]]
then
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
# This installs the mpmissions for charnarusplus and enoch (AKA Livonia) from github. The game once allowed the full server
# to be downloaded, but now you get the server without any mpmissions. This is a workaround.
echo "Installing mpmissions for ChernarusPlus and Livonia from github..."
map default
else
printf "[ ${lightblue}DayZ${default} ] The server is already installed.\n"
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}" +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
}
# Update mods
modupdate(){
echo "Updating mods..."
dologin
get_mods
${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" ${workshoplist} +quit
# Updated files come in with mixed cases. Fix that.
echo "done"
echo
}
# Display the status of everything
status(){
INSTALLED="${NO}"
LOGGED_IN="${NO}"
# DayZ Server files installation
if [ -f "${SERVER_INSTALL_FILE}" ]
then
INSTALLED="${YES}"
if [[ ${release_client_appid} = "221100" ]]
then
RELEASE="Stable"
else
RELEASE="Experimental"
fi
VERSION="$(strings /serverfiles/DayZServer | grep -P "DayZ \d\.\d+\.\d+" | cut -c6-) - ${RELEASE}"
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
echo -ne "
Logged in to Steam: ${LOGGED_IN} ${ANONYMOUS}
Server files installed: ${INSTALLED}"
if [[ "${INSTALLED}" = "${NO}" ]]
then
echo
echo
exit 0
fi
# Version of DayZ Server files
echo -ne "
Version: ${VERSION}"
# Mods
echo -ne "
Mods: "
MODS=$(list)
if [[ ${MODS} == "" ]]
then
echo -ne "${red}none${default}"
fi
echo -e "${MODS}"
}
map(){
# Install map mpmissions for mods that have them. Presumes a map.env was created for the mod, with the required metadata (git URL, etc.)
TERM="map"
if [[ "${1}" =~ ^[0-9]+$ ]]
then
TERM="mod id"
fi
if [ -f "${FILES}/mods/${1}/map.env" ]
then
echo "Installing mpmissions files for ${TERM} ${1}..."
source ${FILES}/mods/${1}/map.env
${FILES}/bin/map.sh ${1} install
fi
}
mod_install(){
if [ -f ${FILES}/mods/${1}/${2}.sh ]
then
echo "An ${2}.sh was found for mod id ${1}. Running..."
${FILES}/mods/${1}/${2}.sh
fi
# A generic map install script. Presumes a git repo as the source
}
# "Manage" XML files.
xml(){
/files/bin/xml.sh ${1}
installxml ${1}
}
# Capture the first argument and shift it off so we can pass $@ to every function
C=${1}
shift || {
usage
}
case "${C}" in
a|add)
add "${@}"
;;
i|install)
install "${@}"
;;
g|login)
login "${@}"
;;
m|modupdate)
modupdate "${@}"
;;
r|remove)
remove "${@}"
;;
l|s|status)
status "${@}"
;;
p|map)
map "${@}"
;;
u|update)
update "${@}"
;;
x|xml)
xml "${@}"
;;
*)
usage "$*"
;;
esac

3
web/bin/entrypoint.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
exec "$@"

27
web/bin/start.sh Normal file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Set PS1 so we know we're in the container
if ! echo .bashrc | grep -q "dz-web"
then
echo "Adding PS1 to .bashrc..."
cat > .bashrc <<EOF
alias ls='ls --color'
export PS1="${debian_chroot:+($debian_chroot)}\u@dz-web:\w\$ "
unset DEVELOPMENT
EOF
fi
# Shut steamcmd up
if ! [ -d ${HOME}/.steam ]
then
mkdir -p ${HOME}/.steam
fi
cd /web
npm i
export DEBUG='express:*'
npx nodemon web.js &
cd docroot
npm i
exec npm run dev

BIN
web/bin/steamservice.so Normal file

Binary file not shown.

13
web/docroot/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DayZ Docker Server</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="src/main.js"></script>
</body>
</html>

1365
web/docroot/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
web/docroot/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "docroot",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite --host",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"@vueuse/core": "^10.1.2",
"bootstrap": "^5.3.0",
"bootstrap-icons": "^1.10.5",
"pinia": "^2.1.3",
"vue": "^3.3.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"vite": "^4.3.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

17
web/docroot/src/App.vue Normal file
View File

@@ -0,0 +1,17 @@
<script setup>
import Body from '@/components/Body.vue'
import Error from '@/components/Error.vue'
import Header from '@/components/Header.vue'
</script>
<template>
<Suspense>
<main>
<Error />
<div class="container-fluid min-vh-100 d-flex flex-column bg-light">
<Header />
<Body />
</div>
</main>
</Suspense>
</template>

View File

@@ -0,0 +1,9 @@
<script setup>
import Mods from '@/components/Mods.vue'
import SearchResults from "@/components/SearchResults.vue";
</script>
<template>
<SearchResults />
<Mods />
</template>

View File

@@ -0,0 +1,38 @@
<script setup>
import { onMounted } from 'vue'
import { Modal } from 'bootstrap'
import { useAppStore } from '@/stores/app.js'
const store = useAppStore()
let modal = {}
onMounted(() => {
modal = new Modal('#errorModal', {})
// modal.show()
})
</script>
<template>
<div
class="modal"
id="errorModal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="errorModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="errorModalLabel">Error</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{{ store.errorText }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,40 @@
<script setup>
import Search from '@/components/Search.vue'
import Status from '@/components/Status.vue'
import Servers from '@/components/Servers.vue'
import { useFetch } from '@vueuse/core'
import { useAppStore } from '@/stores/app.js'
const store = useAppStore()
import { config } from '@/config'
const { error, data } = await useFetch(config.baseUrl + '/status').get().json()
const set = (w, e) => {
store.section = w
const active = Array.from(document.getElementsByClassName('active'))
active.forEach((a) => a.classList.remove('active'))
e.target.classList.add('active')
}
</script>
<template>
<div v-if="data" class="row">
<div class="col-3 text-center">
<h1>DayZ Docker Server</h1>
</div>
<div class="col-5">
<button
@click="installbase"
:class="'btn btn-sm ' + (data.installed ? 'btn-danger' : 'btn-success')"
>
Install Server Files
</button>
<button @click="updatebase" class="btn btn-sm btn-outline-success">Update Server Files</button>
<button @click="updatemods" class="btn btn-sm btn-outline-success">Update Mods</button>
<button type="button" @click="set('servers', $event)" class="btn btn-sm btn-outline-primary">Servers</button>
<button type="button" @click="set('mods', $event)" class="btn btn-sm btn-outline-primary active" data-bs-toggle="button">Mods</button>
<button type="button" @click="set('search', $event)" class="btn btn-sm btn-outline-primary">Search</button>
</div>
<Search />
<Status :status="data" />
<Servers />
</div>
</template>

View File

@@ -0,0 +1,47 @@
<script setup>
import { useFetch } from "@vueuse/core"
import XmlFile from '@/components/XmlFile.vue'
import { useAppStore } from '@/stores/app.js'
const store = useAppStore()
import { config } from '@/config'
const { data, error } = useFetch(() => config.baseUrl + `/mod/${store.modId}`, {
immediate: false,
refetch: true
}).get().json()
</script>
<template>
<div class="col-md-9 border">
<div v-if="error" class="d-flex">Error: {{ error }}</div>
<div v-else-if="data" class="d-flex">
<div>
<div>
<strong>{{ data.name }}</strong>
</div>
<div>
ID: {{ data.id }}
</div>
<div>
Size: {{ data.size.toLocaleString("en-US") }}
</div>
<div v-if="data.customXML.length > 0">
Custom XML files:
<ul>
<li v-for="info in data.customXML">
<a
:class="'simulink xmlfile ' + (store.modFile === info.name ? 'active' : '')"
@click="store.modFile=info.name"
>
{{ info.name }}
</a>
</li>
</ul>
</div>
</div>
<div class="col-1"></div>
<div>
<XmlFile />
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,55 @@
<script setup>
import { config } from '@/config'
import { useFetch } from '@vueuse/core'
import { useAppStore } from '@/stores/app.js'
import ModInfo from '@/components/Modinfo.vue'
const store = useAppStore()
const { data, error } = useFetch(config.baseUrl + '/mods', {
afterFetch(ctx) {
store.mods = ctx.data.mods
return ctx
}
}).get().json()
</script>
<template>
<div class="row flex-grow-1" v-if="store.section === 'mods'">
<div v-if="error" class="row text-danger">
{{ error }}
</div>
<div class="col-md-3 border" v-if="data">
<div>
<h4 class="text-center">Installed Mods</h4>
<table>
<tr>
<th>Steam Link</th>
<th>Mod Name</th>
</tr>
<template
v-for="mod in data.mods.sort( (a,b) => { return a.name.localeCompare(b.name) } )"
>
<tr>
<td>
<a
target="_blank"
:href="config.steamUrl + mod.id"
>
{{ mod.id }}
</a>
</td>
<td>
<a
:class="'simulink' + (store.modId === parseInt(mod.id) ? ' active' : '')"
@click="store.modFile='';store.modId=parseInt(mod.id)"
>
{{ mod.name }}
</a>
</td>
</tr>
</template>
</table>
</div>
</div>
<ModInfo />
</div>
</template>

View File

@@ -0,0 +1,12 @@
<script setup>
import { useAppStore } from '@/stores/app.js'
const store = useAppStore()
</script>
<template>
<div class="col form-control-lg text-center">
<form @submit.prevent="(e) => {store.searchText=e.target.search.value; store.section='search'}">
<input name="search" placeholder="Search mods..." autofocus>
</form>
</div>
</template>

View File

@@ -0,0 +1,77 @@
<script setup>
import { config } from '@/config'
import { BKMG } from '@/util'
import { useFetch} from '@vueuse/core'
import { useAppStore } from '@/stores/app.js'
const store = useAppStore()
const { data: searchResults, error, isFetching } = useFetch(() => config.baseUrl + `/search/${store.searchText}`, {
immediate: false,
refetch: true,
afterFetch(response) {
// const sortField = "time_updated"
const sortField = "lifetime_subscriptions"
response.data.response.publishedfiledetails.sort((a, b) =>
a[sortField] < b[sortField] ? 1 : -1
)
return response
}
}).get().json()
</script>
<template>
<div v-if="store.section === 'search'">
<div v-if="error" class="row text-danger">
{{ error }}
</div>
<div v-if="store.searchText === ''">
<div class="row justify-content-center">
<div class="col-4">
<h2>Search for something...</h2>
</div>
</div>
</div>
<div v-if="isFetching" class="row justify-content-center">
<div class="col-1 text-end">
<div class="spinner-border" role="status"></div>
</div>
<div class="col-4">
<h2>Searching for <strong>"{{ store.searchText }}"...</strong></h2>
</div>
</div>
<template v-if="searchResults && ! isFetching">
<div class="text-center">
<h2>{{ searchResults.response.total }} results for <strong>"{{ store.searchText }}"</strong></h2>
</div>
<div class="d-flex">
<table>
<tr>
<th>Steam Link</th>
<th>Title</th>
<th>Size</th>
<th>Last Updated</th>
<th>Subscriptions</th>
<th></th>
</tr>
<tr v-for="result in searchResults.response.publishedfiledetails">
<td>
<a
target="_blank"
:href="config.steamUrl + result.publishedfileid"
>
<img :alt="result.short_description" data-bs-toggle="tooltip" data-bs-placement="left" :title="result.short_description" width="160" height="90" :src="result.preview_url">
</a>
</td>
<td>{{ result.title }}</td>
<td>{{ BKMG(result.file_size) }}</td>
<td>{{ new Date(result.time_updated * 1000).toLocaleDateString("en-us") }}</td>
<td>{{ result.lifetime_subscriptions }}</td>
<td>
<button v-if="store.mods.find(o => o.id === result.publishedfileid)" @click="removeMod(result.publishedfileid)" type="button" class="btn btn-danger">Remove</button>
<button v-else @click="installMod(result.publishedfileid)" type="button" class="btn btn-success">Install</button>
</td>
</tr>
</table>
</div>
</template>
</div>
</template>

View File

@@ -0,0 +1,20 @@
<script setup>
import { useAppStore } from '@/stores/app.js'
const store = useAppStore()
</script>
<template>
<div class="row" v-if="store.section === 'servers'">
<div class="col text-center">
<div class="row">
<div class="col">
<h2>Servers</h2>
</div>
</div>
<div class="row flex-grow-1">
<div class="col-6 justify-content-end">Running</div>
<div class="col-6">Stopped</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup>
const { status } = defineProps(['status'])
</script>
<template>
<div class="col">
<div>
Server files installed:
<span v-if="status.installed" class="bi bi-check h2 text-success"></span>
<span v-else class="bi bi-x h2 danger text-danger"></span>
</div>
<div v-if="status.version">
Version: <span class="text-success fw-bold">{{ status.version }}</span>
<span class="text-success fw-bold">({{ status.appid }})</span>
</div>
</div>
</template>

View File

@@ -0,0 +1,28 @@
<script setup>
import { useFetch } from '@vueuse/core'
import { config } from '@/config'
import { useAppStore } from '@/stores/app.js'
import XmlTree from '@/components/XmlTree.vue'
const store = useAppStore()
const { data, error } = await useFetch(() => config.baseUrl + `/mod/${store.modId}/${store.modFile}`, {
immediate: false,
refetch: true,
afterFetch(response) {
const parser = new DOMParser()
try {
response.data = parser.parseFromString(response.data, "text/xml").documentElement
} catch(e) {
console.error(e)
response.error = e
}
return response
}
}).get()
</script>
<template>
<div v-if="error">{{ error }}</div>
<div v-else-if="data">
<XmlTree :element="data" :depth="0" />
</div>
</template>

View File

@@ -0,0 +1,49 @@
<script setup>
const props = defineProps({
depth: Number,
element: [Element, Text],
})
function collapse(e) {
console.log(e)
// e.children?.forEach(x => x.classList?.add("d-none"))
}
function children(e) {
let children = []
let node = e.firstChild
while (node) {
children.push(node)
node = node.nextSibling
}
return children
}
</script>
<template>
<div v-if="props.element.nodeType === 1" :style="'padding-left: ' + (props.depth * 10) + 'px'">
<span class="d-flex">
<span
v-if="props.element.children.length > 0"
class="bi-dash simulink text-center"
@click="collapse"
/>
<span>&lt;{{props.element.nodeName}}</span>
<span v-if="props.element.hasAttributes()" v-for="attribute in props.element.attributes">
<span>&nbsp;{{attribute.name}}</span>
<span>=</span>
<span>"{{attribute.value}}"</span>
</span>
<span v-if="props.element.children.length === 0">&nbsp;/</span>
<span>></span>
</span>
<span v-for="child in children(props.element)">
<XmlTree v-if="child.nodeType !== 8" :element="child" :depth="props.depth + 1" />
</span>
<span
v-if="props.element.nodeType === 1"
style="padding-left: -10px"
>
<span>&lt;/{{props.element.nodeName}}></span>
</span>
</div>
<span v-if="props.element.nodeType === 3">{{props.element.data.trim()}}</span>
</template>

View File

@@ -0,0 +1,6 @@
const config = {
baseUrl: window.location.protocol + '//' + window.location.hostname + ':8000',
steamUrl: 'https://steamcommunity.com/sharedfiles/filedetails/?id='
}
export { config }

View File

@@ -0,0 +1,21 @@
button {
padding: 5px;
margin: 10px;
}
th, td {
padding-right: 10px
}
.active {
background-color: cyan;
}
.simulink {
cursor: pointer;
text-underline: blue;
}
.simulink:hover {
background-color: green;
}

25
web/docroot/src/main.js Normal file
View File

@@ -0,0 +1,25 @@
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap'
import 'bootstrap-icons/font/bootstrap-icons.css'
import './css/index.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import {useAppStore} from "@/stores/app";
// Create an instance of our Vue app
const app = createApp(App)
// Add the store
app.use(createPinia())
// A global error handler
app.config.errorHandler = (err, instance, info) => {
const store = useAppStore()
store.errorText = err.message
console.error('GLOBAL ERROR HANDLER! ', err, instance, info)
}
// Mount it
app.mount('#app')

View File

@@ -0,0 +1,12 @@
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => ({
errorText: '',
modId: 0,
modFile: '',
mods: [],
searchText: '',
section: 'mods',
})
})

11
web/docroot/src/util.js Normal file
View File

@@ -0,0 +1,11 @@
const units = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
const BKMG = (val) => {
let l = 0, n = parseInt(val, 10) || 0
while(n >= 1024 && ++l){
n = n/1024
}
return(n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l])
}
export { BKMG }

View File

@@ -0,0 +1,17 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
port: 8001
}
})

1658
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
web/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "web",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2"
},
"type": "module",
"devDependencies": {
"nodemon": "^2.0.22"
}
}

BIN
web/root/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

22
web/root/index.css Normal file
View File

@@ -0,0 +1,22 @@
button {
padding: 5px;
margin: 10px;
}
th, td {
padding-right: 10px
}
.selected {
background-color: cyan;
}
.simulink {
cursor: pointer;
text-underline: blue;
}
.simulink:hover {
background-color: cyan;
}

23
web/root/index.html Normal file
View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DayZ Docker Server</title>
<link rel="icon" type="image/x-icon" href="/favicon.png">
<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
<!-- Our CSS -->
<link rel="stylesheet" href="/index.css">
</head>
<body>
<div id="app"></div>
<script type="module">
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
import app from '/index.js'
createApp(app).mount('#app')
</script>
</body>
</html>

313
web/root/index.js Normal file
View File

@@ -0,0 +1,313 @@
const template = `
<div
class="modal"
id="errorModal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="errorModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="errorModalLabel">Error</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{{ fetchError }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="container-fluid min-vh-100 d-flex flex-column bg-light">
<div class="row">
<div class="col-3 text-center">
<h1>DayZ Docker Server</h1>
</div>
<div class="col-5">
<button
@click="installbase"
:class="'btn ' + (installed ? 'btn-danger' : 'btn-success')"
>
Install Server Files
</button>
<button @click="updatebase" class="btn btn-success">Update Server Files</button>
<button @click="updatemods" class="btn btn-success">Update Mods</button>
<button @click="servers" class="btn btn-primary">Servers</button>
<button @click="listmods" class="btn btn-primary">Mods</button>
</div>
<div class="col form-control-lg text-center">
<form @submit="handleSubmit">
<input name="search" placeholder="Search mods..." autofocus>
</form>
</div>
<div class="col">
<div>
Server files installed:
<span class="bi bi-check h2 text-success" v-if="installed"></span>
<span class="bi bi-x h2 danger text-danger" v-else></span>
</div>
<div v-if="version != ''">
Version: <span class="text-success font-weight-bold">{{ version }}</span>
</div>
</div>
</div>
<div class="row flex-grow-1">
<div class="col-md-3 border">
<div>
<h4 class="text-center">Installed Mods</h4>
<table>
<tr>
<th>Steam Link</th>
<th>Mod Info</th>
</tr>
<template
v-for="mod in mods"
>
<tr>
<td>
<a
target="_blank"
:href="steamURL + mod.id"
>
{{ mod.id }}
</a>
</td>
<td>
<a class="simulink" @click="getModInfo(mod.id)">{{ mod.name }}</a>
</td>
</tr>
</template>
</table>
</div>
</div>
<div class="col-md-9 border">
<div class="d-flex" v-if="modInfo != ''">
<div>
<div>
<strong>{{ modInfo.name }}</strong>
</div>
<div>
ID: {{ modInfo.id }}
</div>
<div>
Size: {{ modInfo.size.toLocaleString("en-US") }}
</div>
<div v-if="modInfo.customXML.length > 0">
Custom XML files:
<ul>
<li v-for="info in modInfo.customXML">
<a
:class="'simulink xmlfile ' + info.name"
@click="getXMLInfo(modInfo.id,info.name)"
>
{{ info.name }}
</a>
</li>
</ul>
</div>
</div>
<div class="col-1"></div>
<div>
<xmltree v-if="XMLInfo != ''" :xmlData="XMLInfo" />
</div>
</div>
<div v-if="searchResults != ''" class="d-flex">
<table>
<tr>
<th>Steam Link</th>
<th>Title</th>
<th>Size</th>
<th>Last Updated</th>
<th>Subscriptions</th>
<th></th>
</tr>
<tr v-for="result in searchResults">
<td>
<a
target="_blank"
:href="steamURL + result.publishedfileid"
>
<img :alt="result.short_description" data-bs-toggle="tooltip" data-bs-placement="left" :title="result.short_description" width="160" height="90" :src="result.preview_url">
</a>
</td>
<td>{{ result.title }}</td>
<td>{{ BKMG(result.file_size) }}</td>
<td>{{ new Date(result.time_updated * 1000).toLocaleDateString("en-us") }}</td>
<td>{{ result.lifetime_subscriptions }}</td>
<td>
<button v-if="mods.find(o => o.id == result.publishedfileid)" @click="removeMod(result.publishedfileid)" type="button" class="btn btn-danger">Remove</button>
<button v-else @click="installMod(result.publishedfileid)" type="button" class="btn btn-success">Install</button>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
`
import xmltree from "/xmltree.js"
const fetcher = (args) => {
fetch(args.url)
.then(response => (
args.type === "json" ? response.json() : response.text()
))
.then(response => args.callback(response))
.catch((error) => {
console.error(error)
this.fetchError = error.message
})
}
export default {
name: 'DazDockerServer',
template: template,
components: {
xmltree
},
data() {
return {
fetchError: "",
installed: false,
mods: [],
modInfo: "",
searchResults: [],
steamURL: 'https://steamcommunity.com/sharedfiles/filedetails/?id=',
version: "Unknown",
XMLFile: "",
XMLInfo: "",
}
},
methods: {
getModInfo(modId) {
fetcher ({
url: '/mod/' + modId,
type: "json",
callback: (response) => {
this.modInfo = response
this.XMLInfo = ""
this.searchResults = ""
}
})
},
getXMLInfo(modId, file) {
for (const e of document.getElementsByClassName("selected")) e.classList.remove("selected")
fetch('/mod/' + modId + '/' + file)
.then(response => response.text())
.then(response => {
this.XMLFile = file
this.XMLInfo = response
for (const e of document.getElementsByClassName(file)) e.classList.add("selected")
})
.catch((error) => {
console.error(error)
this.fetchError = error.message
})
},
handleSubmit(e) {
e.preventDefault()
fetch('/search/' + e.target.search.value)
.then(response => response.json())
.then(response => {
this.modInfo = ""
this.XMLInfo = ""
// const sortField = "time_updated"
const sortField = "lifetime_subscriptions"
response.response.publishedfiledetails.sort((a, b) =>
a[sortField] < b[sortField] ? 1 : -1
)
this.searchResults = response.response.publishedfiledetails
})
.then(() => {
// Enable all tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
// Enable all alerts
$('.alert').alert()
})
.catch((error) => {
console.error(error)
this.fetchError = error.message
})
},
installMod(modId) {
fetch('/install/' + modId)
.then(response => response.text())
.then(response => {
console.log(response)
})
.catch((error) => {
console.error(error)
this.fetchError = error.message
})
},
removeMod(modId) {
fetch('/remove/' + modId)
.then(response => response.text())
.then(response => {
console.log(response)
})
.catch((error) => {
console.error(error)
this.fetchError = error.message
})
},
BKMG(val) {
const units = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
let l = 0, n = parseInt(val, 10) || 0
while(n >= 1024 && ++l){
n = n/1024
}
return(n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l])
},
installbase() {
console.log("Install base files")
},
servers() {
console.log("List servers")
},
listmods() {
console.log("List mods")
},
updatebase() {
console.log("Update base files")
},
updatemods() {
console.log("Update mod files")
}
},
mounted() {
// Get the data
fetch('/status')
.then(response => response.json())
.then(response => {
this.installed = response.installed
this.version = response.version
this.mods = response.mods
if(response.error) {
this.fetchError = response.error
// Since it's a modal, we have to manually show it...?
const modal = new bootstrap.Modal(document.getElementById('errorModal'))
modal.show()
}
})
.catch((error) => {
console.error(error)
this.fetchError = error.message
})
}
}
/*
{ "result": 1, "publishedfileid": "2489240546", "creator": "76561199068873691", "creator_appid": 221100, "consumer_appid": 221100, "consumer_shortcutid": 0, "filename": "", "file_size": "276817803", "preview_file_size": "27678", "preview_url": "https://steamuserimages-a.akamaihd.net/ugc/2011465736408144669/A7137390FBB9F4F94E0BFE5389932F6DE7AB7B87/", "url": "", "hcontent_file": "4050838808220661564", "hcontent_preview": "2011465736408144669", "title": "LastDayZ_Helis", "short_description": "The author of the helicopter mod https://sibnic.info on the site you can download the latest version of free helicopters, If you need help with installation, go to discord https://sibnic.info/discord", "time_created": 1621186063, "time_updated": 1684985831, "visibility": 0, "flags": 5632, "workshop_file": false, "workshop_accepted": false, "show_subscribe_all": false, "num_comments_public": 0, "banned": false, "ban_reason": "", "banner": "76561197960265728", "can_be_deleted": true, "app_name": "DayZ", "file_type": 0, "can_subscribe": true, "subscriptions": 7935, "favorited": 3, "followers": 0, "lifetime_subscriptions": 22759, "lifetime_favorited": 5, "lifetime_followers": 0, "lifetime_playtime": "0", "lifetime_playtime_sessions": "0", "views": 535, "num_children": 0, "num_reports": 0, "tags": [ { "tag": "Animation", "display_name": "Animation" }, { "tag": "Environment", "display_name": "Environment" }, { "tag": "Sound", "display_name": "Sound" }, { "tag": "Vehicle", "display_name": "Vehicle" }, { "tag": "Mod", "display_name": "Mod" } ], "language": 0, "maybe_inappropriate_sex": false, "maybe_inappropriate_violence": false, "revision_change_number": "14", "revision": 1, "ban_text_check_result": 5 }
*/

103
web/root/xmltree.js Normal file
View File

@@ -0,0 +1,103 @@
const template = `
<div
v-if="elem.nodeType === 1 && isText"
:style="'padding-left: ' + (depth * 10) + 'px'"
@click="collapse"
>
<span class="xml-tree-tag">&lt;{{elem.nodeName}}</span>
<span v-if="elem.hasAttributes()" v-for="attribute in elem.attributes">
<span class="xml-tree-attr">&nbsp;{{attribute.name}}</span>
<span>=</span>
<span class="xml-tree-attr">"{{attribute.value}}"</span>
</span>
<span class="xml-tree-tag">></span>
<span>{{this.children[0].data.trim()}}</span>
<span class="xml-tree-tag">&lt;/{{elem.nodeName}}></span>
</div>
<div v-else :style="'padding-left: ' + (depth * 10) + 'px'">
<span v-if="elem.nodeType === 1" class="d-flex">
<span
v-if="elem.children.length > 0"
class="bi-dash simulink text-center"
@click="collapse"
/>
<span v-else></span>
<span class="xml-tree-tag">&lt;{{elem.nodeName}}</span>
<span v-if="elem.hasAttributes()" v-for="attribute in elem.attributes">
<span class="xml-tree-attr">&nbsp;{{attribute.name}}</span>
<span>=</span>
<span class="xml-tree-attr">"{{attribute.value}}"</span>
</span>
<span v-if="elem.children.length === 0" class="xml-tree-tag">&nbsp;/></span>
<span v-else class="xml-tree-tag">></span>
</span>
<span v-if="elem.nodeType === 3">{{elem.data.trim()}}</span>
<div v-for="child in children">
<xmltree v-if="child.nodeType !== 8" :element="child" :d="depth" />
</div>
<span
v-if="elem.nodeType === 1 && elem.children.length > 0"
style="padding-left: -10px"
>
<span style="padding-left: 20px" class="xml-tree-tag">&lt;/{{elem.nodeName}}></span>
</span>
</div>
`
export default {
name: "xmltree",
props: {
d: {
type: Number,
default: 0
},
element: {
type: [Element, Text],
default: undefined
},
xmlData: String
},
template: template,
data() {
return {
depth: 1
}
},
methods: {
collapse() {
this.children.forEach(x => x.classList?.add("d-none"))
},
log(message) {
console.log(message)
}
},
computed: {
elem() {
this.depth = parseInt(this.d) + 1
if (this.element) {
return this.element
} else {
const parser = new DOMParser()
const xmlDoc = parser.parseFromString(this.xmlData, "text/xml")
return xmlDoc.documentElement
}
},
children() {
let children = []
let node = this.elem.firstChild
while (node) {
children.push(node)
node = node.nextSibling
}
return children
},
isText() {
if (this.children.length === 1) {
if (this.children[0].nodeType === 3) {
return true
}
}
return false
}
}
}

299
web/web.js Normal file
View File

@@ -0,0 +1,299 @@
/*
A DayZ Linux server provisioning system.
This is the web UI for provisioning a DayZ server running under Linux.
It manages the main container that installs and maintains the base DayZ server files
along with all mod base files. The goal being to keep all of these centralized and consistent,
but to also make them available for the creation of server containers.
*/
import express from 'express'
import path from 'path'
import fs from 'fs'
import https from 'https'
import { spawn } from 'child_process'
const app = express()
app.use((req, res, next) => {
res.append('Access-Control-Allow-Origin', ['*'])
res.append('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
res.append('Access-Control-Allow-Headers', 'Content-Type')
next()
})
/*
The DayZ server Steam app ID. USE ONE OR THE OTHER!!
Presumably once the Linux server is officially released, the binaries will come from this ID.
Meanwhile, if we have a release-compatible binary, the base files must be installed from this id,
even if the server binary and required shared objects don't come from it. (They'd come from...elsewhere...)
*/
//const server_appid = "223350"
/*
Without a release binary, we must use the experimental server app ID.
*/
const server_appid = "1042420"
/*
DayZ release client Steam app ID. This is for mods, as only the release client has them.
*/
const client_appid = "221100"
/*
Denote if it's release or experimental
*/
const versions = {
"1042420": "Experimental",
"223350": "Release"
}
const appid_version = versions[server_appid]
/*
Base file locations
*/
const modDir = "/mods"
const serverFiles = "/serverfiles"
/*
File path delimiter
*/
const d = '/'
/*
XML config files the system can handle. These are retrieved from values in templates located in /files/mods/:modId
*/
const configFiles = [
'cfgeventspawns.xml',
'cfgspawnabletypes.xml',
'events.xml',
'types.xml',
]
// From https://helpthedeadreturn.wordpress.com/2019/07/17/dayz-sa-mission-file
const allConfigFiles = {
"db": [ // global server config and core loot economy files
"events.xml", // dynamic events
"globals.xml", // global settings
"messages.xml", // server broadcast messages and shutdown
"types.xml" // loot table
],
"env": [ // coordinates, static and dynamic spawns for each entity
"cattle_territories.xml",
"domestic_animals_territories.xml",
"hare_territories.xml",
"hen_territories.xml",
"pig_territories.xml",
"red_deer_territories.xml",
"roe_deer_territories.xml",
"sheep_goat_territories.xml",
"wild_boar_territories.xml",
"wolf_territories.xml",
"zombie_territories.xml"
],
"root": [
"cfgeconomycore.xml", // loot economy core settings and extensions
"cfgeffectarea.json", // static contaminated area coordinates and other properties
"cfgenvironment.xml", // includes env\* files and parameters
"cfgeventgroups.xml", // definitions of groups of objects that spawn together in a dynamic event
"cfgeventspawns.xml", // coordinates where events may occur
"cfggameplay.json", // gameplay configuration settings.
"cfgIgnoreList.xml", // list of items that wont be loaded from the storage
"cfglimitsdefinition.xml", // list of valid categories, tags, usageflags and valueflags
"cfglimitsdefinitionuser.xml", // shortcut groups of usageflags and valueflags
"cfgplayerspawnpoints.xml", // new character spawn points
"cfgrandompresets.xml", // collection of groups of items
"cfgspawnabletypes.xml", // loot categorization (ie hoarder) as well as set of items that spawn as cargo or as attachment on weapons, vehicles or infected.
"cfgundergroundtriggers.json", // used for triggering light and sounds in the Livonia bunker, not used for Chernarus
"cfgweather.xml", // weather configuration
"init.c", // mission startup file (PC only)
"map*.xml",
"mapgroupproto.xml", // structures, tags, maxloot and lootpoints
"mapgrouppos.xml" // all valid lootpoints
]
}
const config = {
installFile: serverFiles + "/DayZServer",
modDir: modDir + "/" + client_appid,
port: 8000,
steamAPIKey: process.env["STEAMAPIKEY"]
}
const getVersion = (installed) => {
if(installed) {
return "1.22.bogus"
}
return ""
}
const getDirSize = (dirPath) => {
let size = 0
if (! fs.existsSync(dirPath)) return size
const files = fs.readdirSync(dirPath)
for (let i = 0; i < files.length; i++) {
const filePath = path.join(dirPath, files[i])
const stats = fs.statSync(filePath)
if (stats.isFile()) {
size += stats.size
} else if (stats.isDirectory()) {
size += getDirSize(filePath)
}
}
return size
}
const getCustomXML = (modId) => {
const ret = []
if (! fs.existsSync(config.modDir)) return ret
for(const file of configFiles) {
if (fs.existsSync(config.modDir + d + modId + d + file)) {
ret.push({name:file})
}
}
return ret
}
const getModNameById = (id) => {
const files = fs.readdirSync(serverFiles, {encoding: 'utf8', withFileTypes: true})
for (const file of files) {
if (file.isSymbolicLink()) {
const sym = fs.readlinkSync(serverFiles + d + file.name)
if(sym.indexOf(id) > -1) return file.name
}
}
return ''
}
const getMods = () => {
const mods = []
if (! fs.existsSync(config.modDir)) return mods
fs.readdirSync(config.modDir).forEach(file => {
const name = getModNameById(file)
mods.push({name:name,id:file})
})
return mods
}
const login = () => {
const args = "+force_install_dir " + serverFiles + " +login '" + config.steamLogin + "' +quit"
steamcmd(args)
}
const steamcmd = (args) => {
const proc = spawn('steamcmd ' + args)
proc.stdout.on('data', (data) => {
res.write(data)
})
proc.stderr.on('data', (data) => {
res.write(data)
})
proc.on('error', (error) => {
res.write(error)
})
proc.on('close', (error) => {
if(error) res.write(error)
res.end()
})
}
app.use(express.static('root'))
// Get mod metadata by ID
app.get('/mod/:modId', (req, res) => {
const modId = req.params["modId"]
const modDir = config.modDir + d + modId
const customXML = getCustomXML(modId)
const ret = {
id: modId,
name: getModNameById(modId),
size: getDirSize(modDir),
customXML: customXML
}
res.send(ret)
})
// Get a mod's XML file
app.get('/mod/:modId/:file', (req, res) => {
const modId = req.params["modId"]
const file = req.params["file"]
if (fs.existsSync(config.modDir + d + modId + d + file)) {
const contents = fs.readFileSync(config.modDir + d + modId + d + file)
res.set('Content-type', 'application/xml')
res.send(contents)
}
})
// Search for a mod
app.get(('/search/:searchString'), (req, res) => {
const searchString = req.params["searchString"]
const url = "https://api.steampowered.com/IPublishedFileService/QueryFiles/v1/?numperpage=1000&appid=221100&return_short_description=true&strip_description_bbcode=true&key=" + config.steamAPIKey + "&search_text=" + searchString
https.get(url, resp => {
let data = '';
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
res.send(JSON.parse(data))
})
}).on('error', err => {
console.log(err.message)
})
})
// Install a mod
app.get(('/install/:modId'), (req, res) => {
const modId = req.params["modId"]
// Shell out to steamcmd, monitor the process, and display the output as it runs
res.send(modId + " was installed")
})
// Remove a mod
app.get(('/remove/:modId'), (req, res) => {
const modId = req.params["modId"]
// Shell out to steamcmd, monitor the process, and display the output as it runs
res.send(modId + " was removed")
})
// Update base files
app.get('/updatebase', (req, res) => {
login()
res.send("Base files were updates")
})
// Update mods
app.get('/updatemods', (req, res) => {
res.send("Mod files were updates")
})
/*
Get the status of things:
If the base files are installed, the version of the server, the appid (If release or experimental)
*/
app.get('/status', (req, res) => {
// FIXME Async/await this stuff...
const installed = fs.existsSync(config.installFile)
const version = getVersion(installed)
const ret = {
"appid": appid_version,
"installed": installed,
"version": version
}
ret.error = "This is a test error from the back end"
res.send(ret)
})
/*
Get all mod metadata
*/
app.get('/mods', (req, res) => {
const mods = getMods()
const ret = {
"mods": mods
}
res.send(ret)
})
app.listen(config.port, () => {
console.log(`Listening on port ${config.port}`)
})