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 }