scraketools

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

scrake.sh (11426B)


      1 #!/bin/bash
      2 PROGNAME="$(basename "$0")"
      3 unset VERBOSE
      4 
      5 [ -z "$STEAM_APPID" ] && STEAM_APPID="215360"
      6 [ -z "$STEAM_ROOT" ] && STEAM_ROOT="$HOME/.steam"
      7 [ -z "$SCRAKE_BASE" ] && SCRAKE_BASE="$STEAM_ROOT/SteamApps/common/Killing Floor Dedicated Server - Linux"
      8 [ -z "$SCRAKE_ROOT" ] && SCRAKE_ROOT="$HOME/Scrake"
      9 [ -z "$SCRAKE_CFG" ] && SCRAKE_CFG="$SCRAKE_ROOT/scrake.ini"
     10 [ -z "$FIRSTRUN_FIFO" ] && FIRSTRUN_FIFO="$(mktemp -u)"
     11 
     12 notify() { echo "$PROGNAME: $@"; }
     13 warn() { echo "$PROGNAME: $@" >&2; }
     14 fatal() {
     15 	trap "exit 1" TERM
     16 	echo "$PROGNAME: $@" >&2
     17 	kill $$
     18 }
     19 
     20 clean() { if [ -e "$FIRSTRUN_FIFO" ]; then rm "$FIRSTRUN_FIFO" 2>&-; fi }
     21 
     22 trap "clean" EXIT TERM INT
     23 
     24 init_paths() {
     25 	if [ ! -d "$STEAM_ROOT" ]; then
     26 		fatal	"could not find steam installation in '$STEAM_ROOT'. "\
     27 			"Change the STEAM_ROOT environment variable to the correct path." >&2
     28 	fi
     29 
     30 	if [ ! -d "$SCRAKE_BASE" ]; then
     31 		fatal	"could not find base game in '$SCRAKE_BASE'. "\
     32 			"Change the SCRAKE_BASE environment variable to the correct path." >&2
     33 	fi
     34 
     35 	if [ ! -d "$SCRAKE_ROOT" ]; then
     36 		mkdir $VERBOSE -p "$SCRAKE_ROOT" || exit 1
     37 		mkdir $VERBOSE "$SCRAKE_ROOT/"{Servers,Mutators,Maps} || exit 1
     38 	fi
     39 
     40 	if [ ! -f "$SCRAKE_CFG" ]; then echo "[Scrake]" >"$SCRAKE_CFG"; fi
     41 }
     42 
     43 # Usage: init_base username [path]
     44 init_base() {
     45 	typeset STEAM_LOGIN PID
     46 	if [ "$1" == "-N" ]; then NO_FIRSTRUN=1; shift; fi
     47 	if [ $# -lt 1 ] || [ $# -gt 2 ]; then fatal "usage: init_base username [path]"; fi
     48 	if [ -e "$FIRSTRUN_FIFO" ]; then fatal "init_base: '$FIRSTRUN_FIFO' exists"; fi
     49 	STEAM_LOGIN="$1"
     50 	if [ ! -z "$2" ]; then
     51 		INSTALL_DIR="+force_install_dir $2"
     52 		notify "init_base: installing server base with steamcmd ($INSTALL_DIR)"
     53 	else
     54 		notify "init_base installing server base with steamcmd (default path)"
     55 	fi
     56 
     57 	steamcmd $INSTALL_DIR +login "$STEAM_LOGIN" +app_update "$STEAM_APPID" +quit || fatal "steamcmd error"
     58 	if [ ! -z "$INSTALL_DIR" ]; then
     59 		notify	"init_base: custom install path specified; do not forget to set 'SCRAKE_BASE'" \
     60 			"to the server's root in your shell environment."
     61 		SCRAKE_BASE="$INSTALL_DIR"
     62 	fi
     63 
     64 	if [ ! -z "$NO_FIRSTRUN" ]; then exit; fi
     65 
     66 	notify "init_base: allowing the server to run once to initialise the default configuration (this will take a second)"
     67 	mkfifo "$FIRSTRUN_FIFO"
     68 	run_server "$SCRAKE_BASE" server KF-Forgotten?game=KFmod.KFGameType -nohomedir >"$FIRSTRUN_FIFO" &
     69 	PID=$!
     70 	notify "init_base: first-run pid: $PID"
     71 	cat "$FIRSTRUN_FIFO" | while read -r LINE; do
     72 		if [ ! -z "$VERBOSE" ]; then echo "$LINE"; fi
     73 		if echo "$LINE" | grep -q '^Webserver is not enabled.'; then
     74 			kill -9 "$PID"
     75 			break
     76 		fi
     77 	done
     78 	rm "$FIRSTRUN_FIFO"
     79 	notify "init_base: first run successful"
     80 
     81 	tr -d '\r' <"$SCRAKE_BASE/System/Default.ini" | sed -n '/^\[KFmod.KFGameType\]$/,/^ *$/p' >> "$SCRAKE_BASE/System/KillingFloor.ini"
     82 }
     83 
     84 # Usage: skeleton_link src dst
     85 skeleton_link()
     86 {
     87 	typeset SRC DST DIR DIRNAME
     88 	if [ $# -ne 2 ]; then fatal "usage: skeleton_link source destination"; fi
     89 	SRC="$(realpath "$1")"
     90 	if [ ! -d "$SRC" ]; then fatal "skeleton_link: no such directory '$SRC'"; fi
     91 	DST="$(realpath "$2")"
     92 	if [ ! -d "$DST" ]; then fatal "skeleton_link: no such directory '$SRC'"; fi
     93 
     94 	find "$SRC" -mindepth 1 -type d | while read DIR; do
     95 		DIRNAME="$(realpath --relative-to "$SRC" "$DIR")"
     96 		[ -d "$DST/$DIRNAME" ] || mkdir $VERBOSE "$DST/$DIRNAME"
     97 		find "$DIR" -type f -exec ln $VERBOSE -s {} "$DST/$DIRNAME" \;
     98 	done
     99 }
    100 
    101 # Usage: skeleton_unlink src dst
    102 skeleton_unlink()
    103 {
    104 	typeset SRC DST DIR
    105 	if [ $# -ne 2 ]; then fatal "usage: skeleton_unlink source destination"; fi
    106 	SRC="$(realpath "$1")"
    107 	DST="$(realpath "$2")"
    108 	if [ ! -d "$DST" ]; then fatal "skeleton_unlink: no such directory '$DST'"; fi
    109 
    110 	find "$DST" -mindepth 1 -type l -lname "$SRC/*" -exec rm $VERBOSE {} \;
    111 	find "$DST" -mindepth 1 -type d | while read DIR; do
    112 		[ -z "$(ls -A "$DIR")" ] && rmdir $VERBOSE "$DIR"
    113 	done
    114 }
    115 
    116 # Usage: fix_steam dst
    117 fix_steam() {
    118 	typeset SRC DST FILE
    119 	if [ $# -ne 1 ]; then fatal "usage: fix_steam dst"; fi
    120 	SRC="$(realpath "$STEAM_ROOT/steamcmd/linux32")"
    121 	if [ ! -d "$SRC" ]; then warn "fix_steam: could not find '$SRC'. Fix will fail."; fi
    122 	DST="$(realpath "$1")"
    123 	if [ ! -d "$DST" ]; then fatal "fix_steam: no such directory '$DST'"; exit 1; fi
    124 
    125 	notify "applying steam library fix to restore master server list discoverability."
    126 	for FILE in steamclient.so libtier0_s.so libvstdlib_s.so; do
    127 		if [ ! -f "$SRC/$FILE" ]; then warn "library not found: '$SRC/FILE'. Skipping."; continue; fi
    128 		ln $VERBOSE -fs "$SRC/$FILE" "$DST/$FILE"
    129 	done
    130 }
    131 
    132 # Usage: ini_rename dst
    133 ini_rename() {
    134 	typeset DST
    135 	if [ $# -ne 1 ]; then fatal "usage: ini_rename dst"; exit 1; fi
    136 	DST="$(realpath "$1")"
    137 	if [ ! -d "$DST" ]; then fatal "ini_rename: no such directory '$DST'"; exit 1; fi
    138 
    139 	notify "changing the .ini extension on instance symlinks to .ini.defaults"
    140 	find "$DST" -type l -name '*.ini' -exec mv $VERBOSE {} {}.defaults \;
    141 }
    142 
    143 # Usage: meta_remove dst
    144 meta_remove() {
    145 	typeset DST FILE
    146 	if [ $# -ne 1 ]; then fatal "usage: meta_remove dst"; exit 1; fi
    147 	DST="$(realpath "$1")"
    148 	if [ ! -d "$DST" ]; then fatal "meta_remove: no such directory '$DST'"; exit 1; fi
    149 
    150 	notify "removing meta maps"
    151 	for FILE in KFintro.rom KF-Menu.rom Entry.rom; do
    152 		rm $VERBOSE "$DST/$FILE"
    153 	done
    154 }
    155 
    156 usage_new_instance() { echo "new_instance [-DLM] name [name [...]]"; }
    157 new_instance() {
    158 	typeset SRC DST FILE OPT OPTIND STEAMFIX INIRENAME METAREMOVE
    159 	if [ $# -lt 1 ]; then fatal "$(usage_new_instance)" >&2; exit 1; fi
    160 
    161 	STEAMFIX=1; INIRENAME=1; METAREMOVE=1;
    162 	while getopts "DLM" OPT; do
    163 		case "$OPT" in
    164 		D) INIRENAME=0;;
    165 		L) STEAMFIX=0;;
    166 		M) METAREMOVE=0;;
    167 		*) fatal "$(usage_new_instance)"; exit 1;;
    168 		esac
    169 	done
    170 	shift $((OPTIND-1))
    171 
    172 	while [ $# -ge 1 ]; do
    173 		notify "creating instance '$1'"
    174 		SRC="$(realpath "$SCRAKE_BASE")"
    175 		DST="$(realpath "$SCRAKE_ROOT/Servers/$1")"
    176 		if [ -d "$DST" ]; then notify "new_instance: instance '$1' already exists.";
    177 		else mkdir $VERBOSE "$DST" && skeleton_link "$SRC" "$DST"; fi
    178 
    179 		if [ $STEAMFIX -eq 1 ]; then fix_steam "$DST/System"; fi
    180 		if [ $INIRENAME -eq 1 ]; then ini_rename "$DST/System"; fi
    181 		if [ $METAREMOVE -eq 1 ]; then meta_remove "$DST/Maps"; fi
    182 		shift
    183 	done
    184 }
    185 
    186 usage_mod_instance() { echo "mod_instance instance [map|mutator] [add|remove] component ..."; }
    187 mod_instance() {
    188 	typeset TARGET WHAT ACTION SRC DST 
    189 	if [ $# -lt 3 ]; then fatal "$(usage_mod_instance)"; exit 1; fi
    190 	TARGET="$1"
    191 	WHAT="$2"
    192 	ACTION="$3"; shift 3
    193 
    194 	case "$WHAT" in
    195 	map) SRC="$SCRAKE_ROOT/Maps";;
    196 	mutator) SRC="$SCRAKE_ROOT/Mutators";;
    197 	*) fatal "$(usage_mod_instance)"; exit 1;;
    198 	esac
    199 
    200 	DST="$SCRAKE_ROOT/Servers/$TARGET"
    201 	if [ ! -d "$DST" ]; then fatal "mod_mutator: fatal: no such instance '$TARGET'"; fi 
    202 
    203 	case "$ACTION" in
    204 	add)
    205 		while [ $# -ge 1 ]; do
    206 			if [ ! -d "$SRC/$1" ]; then
    207 				notify "mod_mutator: no such $WHAT '$SRC/$1'. Skipping..." >&2
    208 				shift
    209 				continue;
    210 			fi
    211 			notify "mod_mutator: installing $WHAT '$1' to $TARGET"
    212 			skeleton_link "$SRC/$1" "$DST"
    213 			ini_rename "$DST/System"
    214 			shift
    215 		done;;
    216 	remove)
    217 		while [ $# -ge 1 ]; do
    218 			notify "mod_mutator: removing $WHAT '$1' from $TARGET"
    219 			skeleton_unlink "$SRC/$1" "$DST"
    220 			shift
    221 		done;;
    222 	*)
    223 		fatal "$(usage_mod_instance)";; 
    224 	esac
    225 }
    226 
    227 inimerge() {
    228 	if [ "$#" -ne 3 ]; then fatal "usage: iniawk overrides instance ini"; fi 
    229 	cat "$3" | tr -d '\r' | awk \
    230 		-v overrides=<(sed -n '/^ *\[Scrake\] *$/,/^ *$/p; /^ *\[Scrake\.'$2'\] *$/,/^ *$/p' "$1") \
    231 		-f <(sed -n '/^## begin inimerge.awk/,$p' "$0")
    232 }
    233 
    234 usage_sync_instance() { echo "usage: sync_cfg instance_name ..."; }
    235 sync_cfg() {
    236 	typeset DST
    237 	if [ "$#" -lt 1 ]; then fatal "$(usage_sync_instance)"; fi
    238 
    239 	while [ "$#" -ge 1 ]; do
    240 		DST="$(realpath "$SCRAKE_ROOT/Servers/$1")"
    241 		if [ ! -d "$DST" ]; then
    242 			notify "sync_cfg: no such instance '$DST'. Skipping..." >&2
    243 			shift
    244 			continue;
    245 		fi
    246 		find "$DST" -name '*.ini.defaults' | while read FILE; do
    247 			BASENAME="$(echo "$FILE" | sed 's/\.defaults$//')"
    248 			notify "sync_cfg: applying overrides from '$1' to $BASENAME"
    249 			inimerge "$SCRAKE_CFG" "$1" "$FILE" >"$BASENAME"
    250 		done
    251 
    252 		shift
    253 	done
    254 }
    255 
    256 run_server() {
    257 	ROOT="$1"; shift
    258 	if [ ! -e "$ROOT/System/ucc-bin" ]; then fatal "run_server: no server found under '$ROOT'"; fi
    259 	cd "$ROOT/System" 
    260 	export LD_LIBRARY_PATH="$STEAM_ROOT/steamcmd/linux32"
    261 	exec ./ucc-bin $* # ucc-bin-real?
    262 }
    263 
    264 # usage: run_instance instance_name map
    265 run_instance() {
    266 	if [ "$#" -ne 2 ]; then fatal "usage: run_instance instance_name map"; fi
    267 	DST="$(realpath "$SCRAKE_ROOT/Servers/$1")"
    268 	if [ ! -d "$DST" ]; then fatal "run_server: no such instance '$DST'"; fi
    269 	if [ ! -e "$DST/Maps/$2" ]; then fatal "run_server: map not found '$DST/Maps/$2'"; fi
    270 
    271 	run_server "$DST" server "$2?game=KFmod.KFGameType" -nohomedir
    272 }
    273 
    274 usage_scrake() {
    275 	cat <<EOF
    276 usage:
    277 	$PROGNAME [global-options] init [-N] steam_username [alt_install_dir]
    278 	$PROGNAME [global-options] create [-DLM] instance_name ...
    279 	$PROGNAME [global-options] mod instance_name mutator add|remove mutname ...
    280 	$PROGNAME [global-options] mod instance_name map add|remove mapname ...
    281 	$PROGNAME [global-options] sync instance_name ...
    282 	$PROGNAME [global-options] run instance_name map_name
    283 global options:
    284 	-h	show this help message
    285 	-s	alternative steam install directory (overrides STEAM_ROOT)
    286 	-b	alternative server base directory (overrides SCRAKE_BASE)
    287 	-r	alternative scrake root directory (overrides SCRAKE_ROOT)
    288 	-i	alternative configuration override file (overrides SCRAKE_CFG)
    289 	-v	verbose flag
    290 init options:
    291 	-N	do not perform the automatic server first-run (not recommended)
    292 create options:
    293 	-D	do not rename .ini symlinks to .defaults (not recommended)
    294 	-L	do not apply master server list discoverability fix
    295 	-M	do not remove meta-maps (KFintro.rom, Entry.rom, KF-Menu.rom)
    296 EOF
    297 }
    298 while getopts "b:i:Nhr:s:v" OPT; do
    299 	case "$OPT" in
    300 	b) SCRAKE_BASE="${OPTARG}";;
    301 	h) usage_scrake; exit;;
    302 	i) SCRAKE_CFG="${OPTARG}";;
    303 	r) SCRAKE_ROOT="${OPTARG}";;
    304 	s) STEAM_ROOT="${OPTARG}";;
    305 	v) VERBOSE="-v";;
    306 	*) fatal "$(usage_scrake)";;
    307 	esac
    308 done
    309 shift $((OPTIND-1))
    310 
    311 VERB="$1"; shift
    312 case "$VERB" in
    313 init)	init_base $*;;
    314 create)	init_paths; new_instance $*;;
    315 mod)	init_paths; mod_instance $*;;
    316 sync)	init_paths; sync_cfg $*;;
    317 run)	init_paths; run_instance $*;;
    318 *)	fatal "$(usage_scrake)";;
    319 esac
    320 
    321 clean
    322 exit
    323 ## begin inimerge.awk
    324 # apply settings from the file described by the 'overrides' variable (supplied through -v) to the
    325 # ini data read from stdin, and write to stdout.
    326 BEGIN {
    327 	n=0
    328 	FS=IFS=OFS="="
    329 	while (getline <overrides) {
    330 		split($1, parts, "/");
    331 		for (i=3;i<=length(parts);i++)
    332 			parts[2]=parts[2] "/" parts[i]
    333 		value=substr($0, length($1)+2)
    334 
    335 		switch(parts[1]) {
    336 			case /^\+/: add[substr(parts[1],2)][parts[2]][n++]=value; break
    337 			case /^\-/: remove[substr(parts[1],2)][parts[2]][n++]=value; break
    338 			default: change[parts[1]][parts[2]][n++]=value; break
    339 		}
    340 	} 
    341 }
    342 
    343 /^ *\[[^\[\]]+\] *$/ {
    344 	print
    345 	gsub(/[\[\]]/,"")
    346 	SECTION=$0
    347 
    348 	if (SECTION in add) {
    349 		for (key in add[SECTION]) {
    350 			for (i in add[SECTION][key]) {
    351 				print key"="add[SECTION][key][i]
    352 			}
    353 		}
    354 	}
    355 	next
    356 }
    357 
    358 (SECTION in remove) && ($1 in remove[SECTION]) {
    359 	key=$1
    360 	value=substr($0, length($1)+2);
    361 	for (i in remove[SECTION][key]) {
    362 		if (remove[SECTION][key][i]==value) {
    363 			delete remove[SECTION][key][i]
    364 			next
    365 		}
    366 	}
    367 }
    368 
    369 (SECTION in change) && ($1 in change[SECTION]) {
    370 	for (i in change[SECTION][$1])
    371 		print $1"="change[SECTION][$1][i]
    372 
    373 	next
    374 }
    375 
    376 { print }