scraketools

administrative aid for hosting multiple Killing Floor 1 dedicated servers
git clone git://git.boymiasma.net/scraketools
Log | Files | Refs | README

commit d9da37a98a05b77ee98b755ac0c1fff372067340
Author: Giygas <none>
Date:   Fri,  3 Oct 2025 13:57:09 +0200

Initial commit

Diffstat:
Ascrake.ini | 40++++++++++++++++++++++++++++++++++++++++
Ascrake.sh | 325+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 365 insertions(+), 0 deletions(-)

diff --git a/scrake.ini b/scrake.ini @@ -0,0 +1,40 @@ +[Scrake] +Engine.AccessControl/AdminPassword=AdminPassword +Engine.AccessControl/GamePassword= +Engine.GameInfo/bVACSecured=True +Engine.GameReplicationInfo/ServerName=My Server +Engine.GameReplicationInfo/ShortName= +Engine.GameReplicationInfo/AdminName= +Engine.GameReplicationInfo/AdminEmail= +Engine.GameReplicationInfo/MessageOfTheDay= +IpDrv.MasterServerUplink/DoUplink=True +IpDrv.MasterServerUplink/UplinkToGamespy=False +IpDrv.MasterServerUplink/SendStats=True +IpDrv.MasterServerUplink/ServerBehindNAT=False +IpDrv.MasterServerUplink/DoLANBroadcast=False +IpDrv.HTTPDownload/RedirectToURL= +-Engine.GameEngine/ServerActors=IpDrv.MasterServerUplink ++Engine.GameEngine/ServerActors=CustomServerDetails.CSDMasterServerUplink ++Engine.GameEngine/ServerActors=MutKillMessage.MutKillMessage ++Engine.GameEngine/ServerActors=ReloadOptionsMut.ReloadOptionsMut + +[Scrake.Normal] +URL/Port=7707 +IpDrv.UdpGamespyQuery/OldQueryPortNumber=7717 +UWeb.WebServer/bEnabled=True +UWeb.WebServer/ListenPort=8075 +Engine.GameInfo/GameDifficulty=2 + +[Scrake.Hard] +URL/Port=7807 +IpDrv.UdpGamespyQuery/OldQueryPortNumber=7817 +UWeb.WebServer/bEnabled=True +UWeb.WebServer/ListenPort=8175 +Engine.GameInfo/GameDifficulty=4 + +[Scrake.Suicidal] +URL/Port=7907 +IpDrv.UdpGamespyQuery/OldQueryPortNumber=7917 +UWeb.WebServer/bEnabled=True +UWeb.WebServer/ListenPort=8275 +Engine.GameInfo/GameDifficulty=5 diff --git a/scrake.sh b/scrake.sh @@ -0,0 +1,325 @@ +#!/bin/bash +PROGNAME="$(basename "$0")" +unset VERBOSE + +[ -z "$STEAM_APPID" ] && STEAM_APPID="215360" +[ -z "$STEAM_ROOT" ] && STEAM_ROOT="$HOME/.steam" +[ -z "$SCRAKE_BASE" ] && SCRAKE_BASE="$STEAM_ROOT/SteamApps/common/Killing Floor Dedicated Server - Linux" +[ -z "$SCRAKE_ROOT" ] && SCRAKE_ROOT="$HOME/Scrake" +[ -z "$SCRAKE_CFG" ] && SCRAKE_CFG="$SCRAKE_ROOT/scrake.ini" +[ -z "$FIRSTRUN_FIFO" ] && FIRSTRUN_FIFO="$(mktemp -u)" + +notify() { echo "$PROGNAME: $@"; } +warn() { echo "$PROGNAME: $@" >&2; } +fatal() { + trap "exit 1" TERM + echo "$PROGNAME: $@" >&2 + kill $$ +} + +clean() { if [ -e "$FIRSTRUN_FIFO" ]; then rm "$FIRSTRUN_FIFO" 2>&-; fi } + +trap "clean" TERM INT + +init_paths() { + if [ ! -d "$STEAM_ROOT" ]; then + fatal "could not find steam installation in '$STEAM_ROOT'. "\ + "Change the STEAM_ROOT environment variable to the correct path." >&2 + fi + + if [ ! -d "$SCRAKE_BASE" ]; then + fatal "could not find base game in '$SCRAKE_BASE'. "\ + "Change the SCRAKE_BASE environment variable to the correct path." >&2 + fi + + if [ ! -d "$SCRAKE_ROOT" ]; then + mkdir -p "$SCRAKE_ROOT" || exit 1 + mkdir "$SCRAKE_ROOT/"{Servers,Mutators,Maps} || exit 1 + fi +} + +# Usage: init_base username [path] +init_base() { + typeset STEAM_LOGIN PID + if [ $# -lt 1 ] || [ $# -gt 2 ]; then fatal "usage: init_base username [path]"; fi + if [ -e "$FIRSTRUN_FIFO" ]; then fatal "init_base: '$FIRSTRUN_FIFO' exists"; fi + STEAM_LOGIN="$1" + if [ ! -z "$2" ]; then + INSTALL_DIR="+force_install_dir $2" + notify "init_base: installing server base with steamcmd ($INSTALL_DIR)" + else + notify "init_base installing server base with steamcmd (default path)" + fi + + steamcmd $INSTALL_DIR +login "$STEAM_LOGIN" +app_update "$STEAM_APPID" +quit || fatal "steamcmd error" + if [ ! -z "$INSTALL_DIR" ]; then + notify "init_base: custom install path specified; do not forget to set 'SCRAKE_BASE'" \ + "to the server's root in your shell environment." + SCRAKE_BASE="$INSTALL_DIR" + fi + + notify "init_base: allowing the server to run once to initialise the default configuration (this will take a second)" + mkfifo "$FIRSTRUN_FIFO" + run_server "$SCRAKE_BASE" server KF-Forgotten?game=KFmod.KFGameType -nohomedir >"$FIRSTRUN_FIFO" & + PID=$! + notify "init_base: first-run pid: $PID" + cat "$FIRSTRUN_FIFO" | while read -r LINE; do + echo "$LINE" + if echo "$LINE" | grep -q '^Webserver is not enabled.'; then + kill -9 "$PID" + break + fi + done + rm "$FIRSTRUN_FIFO" + notify "init_base: first run successful" +} + +# Usage: skeleton_link -i src dst +skeleton_link() +{ + typeset SRC DST INI_REPLACE DIR DIRNAME + if [ "$1" == "-i" ]; then INI_REPLACE=1; shift; fi + if [ $# -ne 2 ]; then fatal "usage: skeleton_link source destination"; fi + SRC="$(realpath "$1")" + if [ ! -d "$SRC" ]; then fatal "skeleton_link: no such directory '$SRC'"; fi + DST="$(realpath "$2")" + if [ ! -d "$DST" ]; then fatal "skeleton_link: no such directory '$SRC'"; fi + + find "$SRC" -mindepth 1 -type d | while read DIR; do + DIRNAME="$(realpath --relative-to "$SRC" "$DIR")" + [ -d "$DST/$DIRNAME" ] || mkdir $VERBOSE "$DST/$DIRNAME" + find "$DIR" -type f -exec ln $VERBOSE -s {} "$DST/$DIRNAME" \; + done + if [ ! -z "$INI_REPLACE" ]; then + find "$DST" -type l -name '*.ini' -exec mv $VERBOSE {} {}.defaults \; + fi +} + +# Usage: skeleton_unlink src dst +skeleton_unlink() +{ + typeset SRC DST DIR + if [ $# -ne 2 ]; then fatal "usage: skeleton_unlink source destination"; fi + SRC="$(realpath "$1")" + DST="$(realpath "$2")" + if [ ! -d "$DST" ]; then fatal "skeleton_unlink: no such directory '$DST'"; fi + + find "$DST" -mindepth 1 -type l -lname "$SRC/*" -exec rm $VERBOSE {} \; + find "$DST" -mindepth 1 -type d | while read DIR; do + [ -z "$(ls -A "$DIR")" ] && rmdir $VERBOSE "$DIR" + done +} + + +# Usage: new_instance name [name [...]] +new_instance() { + typeset SRC DST + if [ $# -lt 1 ]; then fatal "usage: new_instance name [name [...]]" >&2; fi + + while [ $# -ge 1 ]; do + notify "creating instance '$1'" + SRC="$(realpath "$SCRAKE_BASE")" + DST="$(realpath "$SCRAKE_ROOT/Servers/$1")" + if [ -d "$DST" ]; then notify "new_instance: instance '$1' already exists. Skipping..."; fi + + mkdir $VERBOSE "$DST" && skeleton_link -i "$SRC" "$DST" + + shift + done +} + +usage_mod_instance() { echo "mod_instance instance [map|mutator] [add|remove] component"; } +mod_instance() { + typeset TARGET WHAT ACTION SRC DST + if [ $# -lt 3 ]; then fatal "$(usage_mod_instance)"; exit 1; fi + TARGET="$1" + WHAT="$2" + ACTION="$3"; shift 3 + + case "$WHAT" in + map) SRC="$SCRAKE_ROOT/Maps";; + mutator) SRC="$SCRAKE_ROOT/Mutators";; + *) fatal "$(usage_mod_instance)"; exit 1;; + esac + + DST="$SCRAKE_ROOT/Servers/$TARGET" + if [ ! -d "$DST" ]; then fatal "mod_mutator: fatal: no such instance '$TARGET'"; fi + + case "$ACTION" in + add) + while [ $# -ge 1 ]; do + if [ ! -d "$SRC/$1" ]; then + notify "mod_mutator: no such $WHAT '$SRC/$1'. Skipping..." >&2 + shift + continue; + fi + notify "mod_mutator: installing $WHAT '$1' to $TARGET" + skeleton_link -i "$SRC/$1" "$DST" + shift + done;; + remove) + while [ $# -ge 1 ]; do + notify "mod_mutator: removing $WHAT '$1' from $TARGET" + skeleton_unlink "$SRC/$1" "$DST" + shift + done;; + *) + fatal "$(usage_mod_instance)";; + esac +} + +inimerge() { + if [ "$#" -ne 3 ]; then fatal "usage: iniawk overrides instance ini"; fi + cat "$3" | tr -d '\r' | awk \ + -v overrides=<(sed -n '/^ *\[Scrake\] *$/,/^ *$/p; /^ *\[Scrake\.'$2'\] *$/,/^ *$/p' "$1") \ + -f <(sed -n '/^## begin inimerge.awk/,$p' "$0") +} + +# usage: sync_cfg inifile instance_name ..." +sync_cfg() { + typeset DST INIFILE + if [ "$#" -lt 2 ]; then fatal "usage: sync_cfg inifile instance_name ..."; fi + INIFILE="$1"; shift + + while [ "$#" -ge 1 ]; do + DST="$(realpath "$SCRAKE_ROOT/Servers/$1")" + if [ ! -d "$DST" ]; then + notify "sync_cfg: no such instance '$DST'. Skipping..." >&2 + shift + continue; + fi + find "$DST" -name '*.ini.defaults' | while read FILE; do + BASENAME="$(echo "$FILE" | sed 's/\.defaults$//')" + notify "sync_cfg: applying overrides from '$1' to $BASENAME" + inimerge "$INIFILE" "$1" "$FILE" >"$BASENAME" + done + + shift + done +} + +run_server() { + ROOT="$1"; shift + if [ ! -e "$ROOT/System/ucc-bin" ]; then fatal "run_server: no server found under '$ROOT'"; fi + cd "$ROOT/System" + export LD_LIBRARY_PATH="$STEAM_ROOT/steamcmd/linux32" + exec ./ucc-bin $* # ucc-bin-real? +} + +# usage: run_instance instance_name map +run_instance() { + if [ "$#" -ne 2 ]; then fatal "usage: run_instance instance_name map"; fi + DST="$(realpath "$SCRAKE_ROOT/Servers/$1")" + if [ ! -d "$DST" ]; then fatal "run_server: no such instance '$DST'"; fi + if [ ! -e "$DST/Maps/$2" ]; then fatal "run_server: map not found '$DST/Maps/$2'"; fi + + run_server "$DST" server "$2?game=KFmod.KFGameType" -nohomedir +} + +usage_scrake() { + cat <<EOF +usage: + $PROGNAME [global-options] create instance_name + $PROGNAME [global-options] mod instance_name mutator add|remove mutname ... + $PROGNAME [global-options] mod instance_name map add|remove mapname ... + $PROGNAME [global-options] sync [-i inifile] instance_name ... +global options: + -h show this help message + -s alternative steam install directory (overrides STEAM_ROOT) + -b alternative server base directory (overrides SCRAKE_BASE) + -r alternative scrake root directory (overrides SCRAKE_ROOT) + -v verbose flag +sync options: + -i alternative configuration override file (overrides SCRAKE_CFG) +EOF +} +while getopts "b:hr:s:v" OPT; do + case "$OPT" in + b) SCRAKE_BASE="${OPTARG}";; + h) usage_scrake; exit;; + r) SCRAKE_ROOT="${OPTARG}";; + s) STEAM_ROOT="${OPTARG}";; + v) VERBOSE="-v";; + *) fatal "$(usage_scrake)";; + esac +done +shift $((OPTIND-1)) + +init_base "STEAMUSERNAME" +init_paths +new_instance "Normal" "Hard" "Suicidal" +mod_instance "Normal" mutator add "CustomServerDetails" "ReloadOptions" "MutKillMessage" +mod_instance "Hard" mutator add "CustomServerDetails" "ReloadOptions" "MutKillMessage" +mod_instance "Suicidal" mutator add "CustomServerDetails" "ReloadOptions" "MutKillMessage" +sync_cfg "$HOME/scrake.ini" "Normal" "Hard" "Suicidal" +run_instance "$1" "KF-Forgotten.rom" + +bullshit_debug() { + new_server T + mod_server "T" mutator add "CustomServerDetails" "ReloadOptions" "MutKillMessage" + mod_server "T" map add KF-Affairs KF-Awe KF-Headquarters + cd "$SCRAKE_ROOT/Servers/T" + echo "Inspect and ctrl+d when done.." + bash + mod_server "T" mutator remove "CustomServerDetails" "ReloadOptions" "MutKillMessage" + mod_server "T" map remove KF-Affairs KF-Awe KF-Headquarters + echo "Inspect and ctrl+d when done.." + bash + rm -r "$SCRAKE_ROOT/Servers/T" + echo "all done.." +} + +clean +exit +## begin inimerge.awk +# apply settings from the file described by the 'overrides' variable (supplied through -v) to the +# ini data read from stdin, and write to stdout. +BEGIN { + n=0 + FS=IFS=OFS="=" + while (getline <overrides) { + split($1, parts, "/"); + value=substr($0, length($1)+2) + + switch(parts[1]) { + case /^\+/: add[substr(parts[1],2)][parts[2]][n++]=value; break + case /^\-/: remove[substr(parts[1],2)][parts[2]][n++]=value; break + default: change[parts[1]][parts[2]][n++]=value; break + } + } +} + +/^ *\[[^\[\]]+\] *$/ { + print + gsub(/[\[\]]/,"") + SECTION=$0 + + if (SECTION in add) { + for (key in add[SECTION]) { + for (i in add[SECTION][key]) { + print key"="add[SECTION][key][i] + } + } + } + next +} + +(SECTION in remove) && ($1 in remove[SECTION]) { + key=$1 + value=substr($0, length($1)+2); + for (i in remove[SECTION][key]) { + if (remove[SECTION][key][i]==value) { + delete remove[SECTION][key][i] + next + } + } +} + +(SECTION in change) && ($1 in change[SECTION]) { + for (i in change[SECTION][$1]) + print $1"="change[SECTION][$1][i] + + next +} + +{ print }