Skip to content

Instantly share code, notes, and snippets.

@nosoop
Last active April 27, 2019 22:55
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nosoop/688303af4ddb7d193eaf644c6b6bb8f3 to your computer and use it in GitHub Desktop.
Save nosoop/688303af4ddb7d193eaf644c6b6bb8f3 to your computer and use it in GitHub Desktop.
ctrl+c, ctrl+v srcds/tf2 but smol
#!/usr/bin/python3
'''
gameinfo cloner for TF2
takes an existing TF2 dedicated server installation and an empty directory and links / copies /
modifies files in the latter to create a runnable server
'''
import os
import pathlib
import platform
import re
import sys
import shutil
ENGINE_PATH = pathlib.Path('..', 'core')
GAME_PATH = ENGINE_PATH / 'tf'
DEST_ENGINE_PATH = pathlib.Path('.')
DEST_GAME_PATH = DEST_ENGINE_PATH / 'tf'
DEST_GAME_PATH.mkdir(exist_ok = True)
# support absolute paths on linux, windows must use relative paths
# might want to use relative for both, but linux generally avoids spaces in file paths anyways
if platform.system() == 'Linux':
ENGINE_PATH = ENGINE_PATH.resolve()
GAME_PATH = GAME_PATH.resolve()
DEST_ENGINE_PATH = DEST_ENGINE_PATH.resolve()
DEST_GAME_PATH = DEST_GAME_PATH.resolve()
# process gameinfo.txt file, rewriting relative paths to point to our existing files
with (GAME_PATH / 'gameinfo.txt').open('rt') as input, (DEST_GAME_PATH / 'gameinfo.txt').open('wt') as output:
for line in filter(lambda s: not (s.strip().startswith('//') or len(s.strip()) == 0), input):
line = re.sub('tf/(.*\.vpk|bin)', r'{gamedir}/\1'.format(gamedir = GAME_PATH.as_posix()), line)
line = re.sub('game\+game_write.*$', r'game\t{gamedir}\n\t\t\tgame_write\t\ttf'.format(gamedir = GAME_PATH.as_posix()), line)
line = re.sub('\|all_source_engine_paths\|', r'{sedir}/'.format(sedir = ENGINE_PATH.as_posix()), line)
output.write(line)
# list of required files / directories
ENGINE_LINKS = [ "bin" ]
GAME_LINKS = [ "steam.inf" ]
GAME_COPIES = [ "scripts" ]
if platform.system() == 'Linux':
shutil.copy2(ENGINE_PATH / 'srcds_run', 'srcds_run')
ENGINE_LINKS.append("srcds_linux")
elif platform.system() == 'Windows':
shutil.copy2(ENGINE_PATH / 'srcds.exe', 'srcds.exe')
for link in filter(lambda l: not (DEST_ENGINE_PATH / l).exists(), ENGINE_LINKS):
os.symlink(ENGINE_PATH / link, DEST_ENGINE_PATH / link)
for link in filter(lambda l: not (DEST_GAME_PATH / l).exists(), GAME_LINKS):
# get a proper relative path on windows (os.symlink src is relative to target)
link_rel = pathlib.Path(os.path.relpath((GAME_PATH / link).resolve(), DEST_GAME_PATH))
os.symlink(link_rel, DEST_GAME_PATH / link)
for file in filter(lambda f: not (DEST_GAME_PATH / f).exists(), GAME_COPIES):
shutil.copytree(GAME_PATH / file, DEST_GAME_PATH / file)
#!/bin/bash
# cd into destination directory before running
# /path/to/clone_gameinfo.sh "/path/to/installed/tf"
TF_PATH=`realpath $1`
SOURCE_ENGINE_PATH=`realpath $TF_PATH/..`
process_gameinfo() {
# rewrite gameinfo, replace relative bin and .vpk paths
# also strips comments
sed -E "s:tf/(.*\.vpk|bin):${TF_PATH}/\1:" \
| sed -E "s:game\+game_write.*$:game\t${TF_PATH}\n\t\t\tgame_write\t\ttf:" \
| sed "s:|all_source_engine_paths|:${SOURCE_ENGINE_PATH}/:" \
| grep -v '\t*//'
}
# cat "$1/gameinfo.txt" | process_gameinfo "$1"
# exit
# list of required directories
GAME_LINKS=("scripts")
ENGINE_LINKS=("bin" "srcds_linux")
if [ ! -d "tf" ]; then
mkdir tf
fi
cp "${SOURCE_ENGINE_PATH}/srcds_run" .
for link in "${ENGINE_LINKS[@]}"; do
ln -nsf "${SOURCE_ENGINE_PATH}/$link" "$link"
done
for link in "${GAME_LINKS[@]}"; do
# copy instead of symlink, server may update the file themselves
cp -r "${TF_PATH}/$link" "tf"
done
cat "${TF_PATH}/gameinfo.txt" | process_gameinfo > tf/gameinfo.txt
#!/bin/bash
# script to set up a gameinfo-cloning TF2 server
# cd into your base directory
mkdir -p core/steam
pushd core
# install steam and install game above
wget -qO- "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" | tar xzf - -C steam
steam/steamcmd.sh +login anonymous +force_install_dir ../ +app_update 232250 +quit
popd
#!/usr/bin/python3
import os, vdf, subprocess, sys, signal
# insert default command line args here, or pass the args to the script
base_opts = [ ]
GAME_DIR = 'tf'
if __name__ == '__main__':
src_dir = os.path.dirname(os.path.realpath(__file__))
os.chdir(src_dir)
plugins = []
# add plugins from vdf files
for f in filter(lambda f: f.path.endswith('.vdf'), os.scandir(os.path.join(GAME_DIR, 'addons'))):
with open(f.path) as vdf_file:
data = vdf.load(vdf_file)
plugin_file = data.get('Plugin', {}).get('file', None)
if plugin_file:
plugins.append(plugin_file)
cmd = [ './srcds_run' ]
cmd.extend(base_opts)
cmd.extend(sys.argv[1:])
for plugin in plugins:
print('Loading detected server plugin', plugin)
cmd.extend(['+plugin_load', os.path.join(src_dir, GAME_DIR, plugin)])
# prevent sigint from ruining our day
s = signal.signal(signal.SIGINT, signal.SIG_IGN)
subprocess.call(cmd)
signal.signal(signal.SIGINT, s)
#!/bin/sh
# Wrapper script to load addons.
cd "$(dirname "$0")" || return
if [ -f "$PWD/tf/addons/metamod.vdf" ]; then
echo "MetaMod:Source detected, loading plugin."
# TODO you'd want to read the actual locations from the VDF entries
LOAD_MMS="+plugin_load $(realpath .)/tf/addons/metamod/bin/server"
fi
./srcds_run "$@" "${LOAD_MMS}"
@nosoop
Copy link
Author

nosoop commented Feb 26, 2018

Note that clean server installs will not be able to load MetaMod:Source by default.

Add the following to some startup configuration file (must be run before a map loads in), or append it to your ./srcds_run command line as an engine command; MM:S assumed to be installed in the default directory:

plugin_load "${TF_PATH}/addons/metamod/bin/server";

Replace ${TF_PATH} with the full path to your TF directory, obviously.

@nosoop
Copy link
Author

nosoop commented Feb 27, 2018

Added a wrapper script; copy that to your cloned server's "root" directory and use that in place of srcds_run.

A more robust solution would extract the paths from tf/addons/*.vdf and adding those, but that's left as an exercise for those that care.

@nosoop
Copy link
Author

nosoop commented Jul 11, 2018

Added a Python version of the wrapper script, which is able to detect plugins that should be automatically loaded in.
Requires vdf.

@nosoop
Copy link
Author

nosoop commented Feb 9, 2019

Installation instructions

  1. cd yourself into an empty directory where all the files will go (e.g., /home/me/tf-servers)
  2. Run install-tf-multi.sh, will install TF2 game files and SteamCMD into core and core/steam
  3. mkdir and cd into a new "server instance" directory, run clone_gameinfo.sh ../core/tf in the new directory to generate the rewritten gameinfo and symlink the necessary files
  4. (Optional) Copy srcds_wrapper.py or srcds_wrapper.sh into the directory; intended as a srcds_run wrapper that automatically handles loading Valve server plugins (e.g., MetaMod:Source)
  5. (Optional) Install MM:S, SM, etc., they should work as expected. Source.Python needs the server bin symlinked, so do that if you're installing it as well.
  6. Run srcds_run (or srcds_wrapper) as you normally would

For new server instances cd into the directory created in step 1 then start from step 3

@nosoop
Copy link
Author

nosoop commented Mar 5, 2019

Ported the clone_gameinfo shell script to Python with support for Windows systems (run as Administrator to get the symlinks working).
Requires Python 3.6+, as we use path-like objects in file operations.

Same installation method as before, though at the moment the Python script takes no arguments and assumes the parent SRCDS installation is ../core.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment