Repository: kaimallea/csgo
Branch: master
Commit: 136572263fc0
Files: 19
Total size: 51.2 KB
Directory structure:
gitextract_deglkiou/
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── cloud-config.yaml
├── containerfs/
│ ├── README.md
│ ├── manage_plugins.sh
│ ├── manage_pugsetup_configs.sh
│ └── start.sh
├── docker-compose.yaml
└── test/
├── bin/
│ └── bats
├── csgo/
│ └── cfg/
│ └── sourcemod/
│ └── pugsetup/
│ └── pugsetup.cfg
├── libexec/
│ └── bats-core/
│ ├── bats
│ ├── bats-exec-suite
│ ├── bats-exec-test
│ ├── bats-format-tap-stream
│ └── bats-preprocess
├── srcds_run
└── tests.bats
================================================
FILE CONTENTS
================================================
================================================
FILE: Dockerfile
================================================
FROM ubuntu:bionic
ENV TERM xterm
ENV STEAM_DIR /home/steam
ENV STEAMCMD_DIR /home/steam/steamcmd
ENV CSGO_APP_ID 740
ENV CSGO_DIR /home/steam/csgo
SHELL ["/bin/bash", "-c"]
ARG STEAMCMD_URL=https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz
RUN set -xo pipefail \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --no-install-suggests -y \
lib32gcc1 \
lib32stdc++6 \
lib32z1 \
ca-certificates \
net-tools \
locales \
curl \
unzip \
&& locale-gen en_US.UTF-8 \
&& adduser --disabled-password --gecos "" steam \
&& mkdir ${STEAMCMD_DIR} \
&& cd ${STEAMCMD_DIR} \
&& curl -sSL ${STEAMCMD_URL} | tar -zx -C ${STEAMCMD_DIR} \
&& mkdir -p ${STEAM_DIR}/.steam/sdk32 \
&& ln -s ${STEAMCMD_DIR}/linux32/steamclient.so ${STEAM_DIR}/.steam/sdk32/steamclient.so \
&& { \
echo '@ShutdownOnFailedCommand 1'; \
echo '@NoPromptForPassword 1'; \
echo 'login anonymous'; \
echo 'force_install_dir ${CSGO_DIR}'; \
echo 'app_update ${CSGO_APP_ID}'; \
echo 'quit'; \
} > ${STEAM_DIR}/autoupdate_script.txt \
&& mkdir ${CSGO_DIR} \
&& chown -R steam:steam ${STEAM_DIR} \
&& rm -rf /var/lib/apt/lists/*
ENV LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \
LC_ALL=en_US.UTF-8
COPY --chown=steam:steam containerfs ${STEAM_DIR}/
USER steam
WORKDIR ${CSGO_DIR}
VOLUME ${CSGO_DIR}
ENTRYPOINT exec ${STEAM_DIR}/start.sh
================================================
FILE: LICENSE
================================================
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>
================================================
FILE: Makefile
================================================
SHELL := /bin/bash
CONTAINER_NAME ?= csgo-dedicated-server
IMAGE_NAME ?= kmallea/csgo:latest
SERVER_HOSTNAME ?= Counter-Strike: Global Offensive Dedicated Server
SERVER_PASSWORD ?=
RCON_PASSWORD ?= changeme
STEAM_ACCOUNT ?= changeme
AUTHKEY ?= changeme
IP ?= 0.0.0.0
PORT ?= 27015
TV_PORT ?= 27020
TICKRATE ?= 128
FPS_MAX ?= 400
GAME_TYPE ?= 0
GAME_MODE ?= 1
MAP ?= de_dust2
MAPGROUP ?= mg_active
HOST_WORKSHOP_COLLECTION ?=
WORKSHOP_START_MAP ?=
MAXPLAYERS ?= 12
TV_ENABLE ?= 1
LAN ?= 1
SOURCEMOD_ADMINS ?= STEAM_1:0:123456,STEAM_1:0:654321
RETAKES ?= 0
NOMASTER ?= 0
.PHONY: all clean image test stop
all: image
clean:
docker rmi $(IMAGE_NAME)
image: Dockerfile
docker build -t $(IMAGE_NAME) \
--build-arg STEAMCMD_URL=https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz \
.
server:
docker run \
-i \
-t \
-d \
--net=host \
--mount source=csgo-data,target=/home/steam/csgo \
-e "SERVER_HOSTNAME=$(SERVER_HOSTNAME)" \
-e "SERVER_PASSWORD=$(SERVER_PASSWORD)" \
-e "RCON_PASSWORD=$(RCON_PASSWORD)" \
-e "STEAM_ACCOUNT=$(STEAM_ACCOUNT)" \
-e "AUTHKEY=$(AUTHKEY)" \
-e "TICKRATE=$(TICKRATE)" \
-e "FPS_MAX=$(FPS_MAX)" \
-e "GAME_TYPE=$(GAME_TYPE)" \
-e "GAME_MODE=$(GAME_MODE)" \
-e "MAP=$(MAP)" \
-e "MAPGROUP=$(MAPGROUP)" \
-e "HOST_WORKSHOP_COLLECTION=$(HOST_WORKSHOP_COLLECTION)" \
-e "WORKSHOP_START_MAP=$(WORKSHOP_START_MAP)" \
-e "MAXPLAYERS=$(MAXPLAYERS)" \
-e "TV_ENABLE=$(TV_ENABLE)" \
-e "LAN=$(LAN)" \
-e "SOURCEMOD_ADMINS=$(SOURCEMOD_ADMINS)" \
-e "RETAKES=$(RETAKES)" \
--name $(CONTAINER_NAME) \
$(IMAGE_NAME)
test:
docker run \
-i \
-t \
--rm \
--net=host \
--mount type=bind,source="$(PWD)/test",target=/home/steam/csgo \
-e "CI=true" \
-e "SERVER_HOSTNAME=$(SERVER_HOSTNAME)" \
-e "SERVER_PASSWORD=$(SERVER_PASSWORD)" \
-e "RCON_PASSWORD=$(RCON_PASSWORD)" \
-e "STEAM_ACCOUNT=$(STEAM_ACCOUNT)" \
-e "AUTHKEY=$(AUTHKEY)" \
-e "TICKRATE=$(TICKRATE)" \
-e "FPS_MAX=$(FPS_MAX)" \
-e "GAME_TYPE=$(GAME_TYPE)" \
-e "GAME_MODE=$(GAME_MODE)" \
-e "MAP=$(MAP)" \
-e "MAPGROUP=$(MAPGROUP)" \
-e "HOST_WORKSHOP_COLLECTION=$(HOST_WORKSHOP_COLLECTION)" \
-e "WORKSHOP_START_MAP=$(WORKSHOP_START_MAP)" \
-e "MAXPLAYERS=$(MAXPLAYERS)" \
-e "TV_ENABLE=$(TV_ENABLE)" \
-e "LAN=$(LAN)" \
-e "SOURCEMOD_ADMINS=$(SOURCEMOD_ADMINS)" \
-e "RETAKES=$(RETAKES)" \
-e "SM_PUGSETUP_SNAKE_CAPTAIN_PICKS=2" \
--name $(CONTAINER_NAME) \
$(IMAGE_NAME)
stop:
docker stop $(CONTAINER_NAME)
docker rm $(CONTAINER_NAME)
================================================
FILE: README.md
================================================
# CSGO containerized
The Dockerfile will build an image for running a Counter-Strike: Global Offensive dedicated server in a container.
The following addons and plugins are included by default:
- [Metamod](https://www.sourcemm.net/)
- [SourceMod](https://www.sourcemod.net/)
- [SteamWorks](https://forums.alliedmods.net/showthread.php?t=229556)
- [Updater](https://bitbucket.org/GoD_Tony/updater/downloads/updater.smx)
- [PugSetup](https://github.com/splewis/csgo-pug-setup)
- [Practice Mode](https://github.com/splewis/csgo-practice-mode)
- [Retakes](https://github.com/splewis/csgo-retakes) (**disabled by default**)
To get a 10man/gather going, simply connect and type `.setup` in chat. Practice Mode should also be available from the menu.
Retakes is disabled by default. To enable it, set the environment variable `RETAKES=1` and restart the container. Use can later use the cvar `sm_retakes_enabled 0` to turn if off on-demand.
## How to Use
```bash
docker pull kmallea/csgo:latest
```
To use the image as-is, run it with a few useful environment variables to configure the server:
```bash
docker run \
--rm \
--interactive \
--tty \
--detach \
--mount source=csgo-data,target=/home/steam/csgo \
--network=host \
--env "SERVER_HOSTNAME=hostname" \
--env "SERVER_PASSWORD=password" \
--env "RCON_PASSWORD=rconpassword" \
--env "STEAM_ACCOUNT=gamelogintoken" \
--env "AUTHKEY=webapikey" \
--env "SOURCEMOD_ADMINS=STEAM_1:0:123456,STEAM_1:0:654321" \
kmallea/csgo
```
Would you rather use a bind volume so that you can access file contents directly? Use `--mount type=bind,source=$(pwd),target=/home/steam/csgo` instead of the one in the example above.
If you plan on managing plugins manually with a bind volume, you might want pass an empty or reduced `INSTALL_PLUGINS` environment variable to prevent conflicts (see below for default value of `INSTALL_PLUGINS`).
### Required Game Login Token
The `STEAM_ACCOUNT` is a "Game Login Token" required by Valve to run public servers. Confusingly, this token is also referred to as a steam account (it's set via `sv_setsteamaccount`). To get one, visit https://steamcommunity.com/dev/managegameservers. You'll need one for each server.
Remember that if you DO NOT give a valid Game Login Token, your server will be restricted to LAN only
### Optional Steam Web API Key for Workshop Content
To access maps and collections from the Workshop, you need to provide a Steam Web API key. You can provide this via the evironment variable `AUTHKEY` and it will be passed to the command-line as `-authkey <key>`.
If you don't have a key you can generate one at http://steamcommunity.com/dev/apikey.
With a key set, you can also use the environment variables `HOST_WORKSHOP_COLLECTION` and `WORKSHOP_START_MAP` to specify a workshop collection and start the server with a workshop map, respectively.
For more information check out the [Valve developer wiki page](https://developer.valvesoftware.com/wiki/CSGO_Workshop_For_Server_Operators#How_to_host_Workshop_Maps_with_a_CS:GO_Dedicated_Server).
### SourceMod admins
The optional `SOURCEMOD_ADMINS` environment variable is a comma-delimited list of Steam IDs. These will be added to SourceMod's admin list before the server is started.
### Playing on LAN
If you're on a LAN, add the environment variable `LAN=1` (e.g., `--env "LAN=1"`) to have `sv_lan 1` set for you in the server.
### Environment variable overrides
Below are the default values for environment variables that control the server configuration. To override, pass one or more of these to docker using the `-e` or `--env` argument (example above).
```bash
SERVER_HOSTNAME=Counter-Strike: Global Offensive Dedicated Server
SERVER_PASSWORD=
RCON_PASSWORD=changeme
STEAM_ACCOUNT=changeme
AUTHKEY=changeme
IP=0.0.0.0
PORT=27015
TV_PORT=27020
TICKRATE=128
FPS_MAX=400
GAME_TYPE=0
GAME_MODE=1
MAP=de_dust2
MAPGROUP=mg_active
HOST_WORKSHOP_COLLECTION=
WORKSHOP_START_MAP=
MAXPLAYERS=12
TV_ENABLE=1
LAN=0
SOURCEMOD_ADMINS=
RETAKES=0
NOMASTER=0
```
For compatibility with the [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) feature the following
environment variables are also available as a '_FILE' variant.
```bash
SERVER_PASSWORD_FILE
RCON_PASSWORD_FILE
STEAM_ACCOUNT_FILE
AUTHKEY_FILE
SOURCEMOD_ADMINS_FILE
```
If one of these is set the content of the referred file is used as content for the non-'_FILE" environment variable. If both
environment variables are set, the content of the non-'_FILE' variable takes precedence.
Usage of _FILE variables allows constructs like this in docker compose files:
```yml
version: "3.7"
services:
app:
image: kmallea/csgo
secrets:
- csgo_rcon_password
environment:
- RCON_PASSWORD_FILE=/run/secrets/csgo_rcon_password
secrets:
csgo_rcon_password:
file: ${SECRETS_DIR}/csgo_rcon_password.txt
```
### PugSetup ConVars
PugSetup's default configuration can also be controlled via environment variables. Any environment variables prefixed with `SM_PUGSETUP_` will have its corresponding cvar updated inside of `$CSGODIR/csgo/cfg/sourcemod/pugsetup.cfg`.
**NOTE: `pugsetup.cfg` is automatically generated the first time the plugin is loaded. So you may have to restart the container after the first run so that the file exists.**
For example, if I wanted to enable set the cvars `sm_pugsetup_snake_captain_picks` and `sm_pugsetup_message_prefix`, I would set the following environment variables when starting the container:
```bash
...
--env "SM_PUGSETUP_SNAKE_CAPTAIN_PICKS=2" \
--env "SM_PUGSETUP_MESSAGE_PREFIX=[{YELLOW}Sesame Street{NORMAL}]" \
...
```
This would set these values in `$CSGODIR/csgo/cfg/sourcemod/pugsetup.cfg`:
```bash
...
sm_pugsetup_snake_captain_picks "2"
sm_pugsetup_message_prefix "[{YELLOW}Sesame Street{NORMAL}]"
...
```
### Troubleshooting
If you're unable to use [`--network=host`](https://docs.docker.com/network/host/), you'll need to publsh the ports instead, e.g.:
```bash
docker run \
--rm \
--interactive \
--tty \
--detach \
--mount source=csgo-data,target=/home/steam/csgo \
--publish 27015:27015/tcp \
--publish 27015:27015/udp \
--publish 27020:27020/tcp \
--publish 27020:27020/udp \
--env "SERVER_HOSTNAME=hostname" \
--env "SERVER_PASSWORD=password" \
--env "RCON_PASSWORD=rconpassword" \
--env "STEAM_ACCOUNT=gamelogintoken" \
--env "AUTHKEY=webapikey" \
--env "SOURCEMOD_ADMINS=STEAM_1:0:123456,STEAM_1:0:654321" \
kmallea/csgo
```
## Manually Building
```bash
docker build -t csgo-dedicated-server .
```
_OR_
```bash
make
```
The game data is downloaded on first run (~26GB). Mount a volume to preserve game data if you need to recreate the container. The volume's target should be `/home/steam/csgo`. In these example I use a data volume, but you can use a bind volume as well since plugins are installed during container startup.
### Overriding versions of SteamCMD, Metamod, SourceMod, and/or PugSetup
#### SteamCMD
SteamCMD is installed directly into the image at build time. To override the URL it installs from, pass in a build arg named `STEAMCMD_URL`:
```bash
docker build \
-t $(IMAGE_NAME) \
--build-arg STEAMCMD_URL=https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz \
.
```
#### Metamod, SourceMod, PugSetup, Retakes, etc
All plugins and extensions are installed during the startup of the container. This allows plugins can be managed via an environment variable.
The environment variable `INSTALL_PLUGINS` contains a space-delimited list of plugins to install. You can use newlines to delimit, they will be converted to spaces before processing. If you override this, make sure you include metamod and sourcemod or plugins that depend on them won't work.
```bash
INSTALL_PLUGINS="${INSTALL_PLUGINS:-https://mms.alliedmods.net/mmsdrop/1.10/mmsource-1.10.7-git971-linux.tar.gz
https://sm.alliedmods.net/smdrop/1.10/sourcemod-1.10.0-git6478-linux.tar.gz
https://github.com/splewis/csgo-pug-setup/releases/download/2.0.5/pugsetup_2.0.5.zip
https://github.com/splewis/csgo-retakes/releases/download/v0.3.4/retakes_0.3.4.zip
https://github.com/b3none/retakes-instadefuse/releases/download/1.4.0/retakes-instadefuse.smx
https://github.com/b3none/retakes-autoplant/releases/download/2.3.0/retakes_autoplant.smx
https://github.com/b3none/retakes-hud/releases/download/2.2.5/retakes-hud.smx
}"
```
Lastly, a checksum is generated for each plugin's URL and is stored as `$CSGO_DIR/csgo/<checksum>.marker` to prevent re-downloading plugins that have already been installed.
### Adding your own configs, other files etc.
#### Build time
The directory `containerfs` (container filesystem) is the equivalent of the steam user's home directory (`/home/steam`). The `csgo` game data lives in here. This means that any files you want to add, simply put them in the correct paths under `containerfs`, and they will appear in the Docker image relative to the steam user's home directory.
It is recommended to use `INSTALL_PLUGINS` environment variable at run time to install plugins, so that they are decoupled from the image.
#### Run time
See `INSTALL_PLUGINS` above in the section above to learn about installing plugins.
If you're using a data volume, you can use the `docker cp` command to copy files from your host machine into the data volume.
If you're using a bind volume, you can copy files in directly. You may want to clear the `INSTALL_PLUGINS` variable if you want to manage everything manually.
### Test Locally
After building:
1. Edit the exported environment variables in the `Makefile` to your liking
2. Run `make server` to start a local LAN server to test
3. Run `make test` to run tests
================================================
FILE: cloud-config.yaml
================================================
#cloud-config
# This config is an example of of a one-liner to getting a CSGO server up-and-running
# in just a few minutes on Google Compute Engine with the following one-liner that consumes
# the uncommented parts of this file:
#
# gcloud compute instances create csgo-server \
# --project=$PROJECT \
# --zone=$ZONE \
# --image-family=cos-stable \
# --image-project=cos-cloud \
# --boot-disk-size=50GB \
# --machine-type=c2-standard-4 \
# --network=default \
# --metadata-from-file user-data=$(PWD)/cloud-config.yaml
write_files:
- path: /etc/systemd/system/dynamic-dns.service
permissions: 0644
owner: root
content: |
# /etc/systemd/system/dynamic-dns.service
[Unit]
Description=Updates dynamic DNS record
Wants=dynamic-dns.timer
[Service]
ExecStart=/bin/sh -c '(\
export PUBLIC_IP=$$(\
/usr/bin/curl \
-s \
-H "Metadata-Flavor: Google" \
https://domains.google.com/checkip \
) && \
/usr/bin/curl \
-s \
--user <username>:<password> \
"https://domains.google.com/nic/update?hostname=<hostname>&myip=$${PUBLIC_IP}" \
)'
- path: /etc/systemd/system/dynamic-dns.timer
permissions: 0644
owner: root
content: |
# /etc/systemd/system/dynamic-dns.timer
[Unit]
Description=Runs dynamic-dns.service every 15 minutes
Requires=dynamic-dns.timer
[Timer]
Unit=dynamic-dns.service
OnUnitInactiveSec=15m
- path: /etc/systemd/system/csgods.service
permissions: 0644
owner: root
content: |
[Unit]
Description=CSGO Dedicated Server Container
After=docker.service
Requires=docker.service
[Service]
StandardInput=tty-force
ExecStartPre=/usr/bin/docker pull kmallea/csgo
ExecStart=/usr/bin/docker run --name %n \
--interactive \
--tty \
--rm \
--network host \
--cpuset-cpus 3 \
--mount source=csgo-data,target=/home/steam/csgo \
-e "SERVER_HOSTNAME=Counter-Strike: Global Offensive Dedicated Server" \
-e "SERVER_PASSWORD=" \
-e "RCON_PASSWORD=changeme" \
-e "STEAM_ACCOUNT=changeme" \
-e "SOURCEMOD_ADMINS=STEAM_1:0:123456,STEAM_1:1:654321" \
-e "AUTHKEY=changeme" \
-e "FPS_MAX=1000" \
kmallea/csgo
ExecStop=-/usr/bin/docker stop %n
ExecStopPost=-/usr/bin/docker rm %n
runcmd:
- iptables -w -A INPUT -p tcp --dport 27015 -j ACCEPT
- iptables -w -A INPUT -p udp --dport 27015 -j ACCEPT
- iptables -w -A INPUT -p tcp --dport 27020 -j ACCEPT
- iptables -w -A INPUT -p udp --dport 27020 -j ACCEPT
- iptables -w -A INPUT -p udp --dport 27005 -j ACCEPT
- iptables -w -A INPUT -p udp --dport 51840 -j ACCEPT
- iptables -w -A INPUT -p tcp --dport 26900 -j ACCEPT
- iptables -w -A INPUT -p tcp --dport 80 -j ACCEPT
- iptables -w -A INPUT -p tcp --dport 443 -j ACCEPT
- systemctl daemon-reload
- systemctl enable dynamic-dns.timer
- systemctl start dynamic-dns.service
- systemctl start csgods.service
================================================
FILE: containerfs/README.md
================================================
## Adding your own files, plugins, etc.
The directory `containerfs` (container filesystem) is the equivalent of the root CSGO directory (`/home/steam/csgo`). Any files or plugins you want to add to the image, simply put them in the correct paths under `containerfs`, and they will appear in the Docker image relative to the CSGO directory.
For example, by default, CSGO is installed in the root path `/home/steam/csgo` within the docker image. If I want my `practice.cfg` file to live in the `cfg` directory, I would put that file in `containerfs/csgo/cfg/` and it will appear in the right place inside the docker image: `home/steam/csgo/csgo/cfg/practice.cfg` (Yes, `csgo` appears twice in the path because the CSGO installation has a sub-directory named `csgo`).
================================================
FILE: containerfs/manage_plugins.sh
================================================
#!/usr/bin/env bash
set -ueo pipefail
: "${CSGO_DIR:?'ERROR: CSGO_DIR IS NOT SET!'}"
export RETAKES="${RETAKES:-0}"
INSTALL_PLUGINS="${INSTALL_PLUGINS:-https://mms.alliedmods.net/mmsdrop/1.11/mmsource-1.11.0-git1148-linux.tar.gz
https://sm.alliedmods.net/smdrop/1.11/sourcemod-1.11.0-git6934-linux.tar.gz
http://users.alliedmods.net/~kyles/builds/SteamWorks/SteamWorks-git131-linux.tar.gz
https://bitbucket.org/GoD_Tony/updater/downloads/updater.smx
https://github.com/splewis/csgo-practice-mode/releases/download/1.3.4/practicemode_1.3.4.zip
https://github.com/splewis/csgo-pug-setup/releases/download/2.0.7/pugsetup_2.0.7.zip
https://github.com/splewis/csgo-retakes/releases/download/v0.3.4/retakes_0.3.4.zip
https://github.com/B3none/retakes-instadefuse/releases/download/1.5.0/retakes-instadefuse.smx
https://github.com/B3none/retakes-autoplant/releases/download/2.3.3/retakes-autoplant.smx
https://github.com/b3none/retakes-hud/releases/download/2.2.5/retakes-hud.smx
}"
get_checksum_from_string () {
local md5
md5=$(echo -n "$1" | md5sum | awk '{print $1}')
echo "$md5"
}
is_plugin_installed() {
local url_hash
url_hash=$(get_checksum_from_string "$1")
if [[ -f "$CSGO_DIR/csgo/${url_hash}.marker" ]]; then
return 0
else
return 1
fi
}
create_install_marker() {
echo "$1" > "$CSGO_DIR/csgo/$(get_checksum_from_string "$1").marker"
}
file_url_exists() {
if curl --output /dev/null --silent --head --fail "$1"; then
return 0
fi
return 1
}
install_plugin() {
filename=${1##*/}
filename_ext=$(echo "${1##*.}" | awk '{print tolower($0)}')
if ! file_url_exists "$1"; then
echo "Plugin download check FAILED for $filename";
return 0
fi
if ! is_plugin_installed "$1"; then
echo "Downloading $1..."
case "$filename_ext" in
"gz")
curl -sSL "$1" | tar -zx -C "$CSGO_DIR/csgo"
echo "Extracting $filename..."
create_install_marker "$1"
;;
"zip")
curl -sSL -o "$filename" "$1"
echo "Extracting $filename..."
unzip -oq "$filename" -d "$CSGO_DIR/csgo"
rm "$filename"
create_install_marker "$1"
;;
"smx")
(cd "$CSGO_DIR/csgo/addons/sourcemod/plugins/" && curl -sSLO "$1")
create_install_marker "$1"
;;
*)
echo "Plugin $filename has an unknown file extension, skipping"
;;
esac
else
echo "Plugin $filename is already installed, skipping"
fi
}
echo "Installing plugins..."
mkdir -p "$CSGO_DIR/csgo"
IFS=' ' read -ra PLUGIN_URLS <<< "$(echo "$INSTALL_PLUGINS" | tr "\n" " ")"
for URL in "${PLUGIN_URLS[@]}"; do
install_plugin "$URL"
done
echo "Finished installing plugins."
# Add steam ids to sourcemod admin file
mkdir -p "$CSGO_DIR/csgo/addons/sourcemod/configs"
IFS=',' read -ra STEAMIDS <<< "$SOURCEMOD_ADMINS"
for id in "${STEAMIDS[@]}"; do
echo "\"$id\" \"99:z\"" >> "$CSGO_DIR/csgo/addons/sourcemod/configs/admins_simple.ini"
done
PLUGINS_ENABLED_DIR="$CSGO_DIR/csgo/addons/sourcemod/plugins"
PLUGINS_DISABLED_DIR="$CSGO_DIR/csgo/addons/sourcemod/plugins/disabled"
RETAKES_PLUGINS="retakes.smx retakes-instadefuse.smx retakes-autoplant.smx retakes-hud.smx retakes_standardallocator.smx"
PUGSETUP_PLUGINS="pugsetup.smx pugsetup_teamnames.smx pugsetup_damageprint.smx"
# Disable Retakes by default so that we have a working and predictable state without plugins conflict
if [[ -f "$PLUGINS_ENABLED_DIR"/retakes.smx ]]; then
mv "$PLUGINS_ENABLED_DIR"/retakes*.smx "$PLUGINS_DISABLED_DIR"/
fi
if [ "$RETAKES" = "1" ]; then
if [[ -f "$PLUGINS_ENABLED_DIR"/pugsetup.smx ]]; then
(cd "$PLUGINS_ENABLED_DIR" && mv pugsetup*.smx "$PLUGINS_DISABLED_DIR")
echo "Disabled PugSetup plugins"
fi
# shellcheck disable=SC2086
(cd "$PLUGINS_DISABLED_DIR" && mv $RETAKES_PLUGINS "$PLUGINS_ENABLED_DIR")
echo "Enabled Retakes plugins"
else
if [[ -f "$PLUGINS_DISABLED_DIR"/pugsetup.smx ]]; then
# shellcheck disable=SC2086
(cd "$PLUGINS_DISABLED_DIR" && mv $PUGSETUP_PLUGINS "$PLUGINS_ENABLED_DIR")
echo "Enabled PugSetup plugins"
fi
fi
================================================
FILE: containerfs/manage_pugsetup_configs.sh
================================================
#!/usr/bin/env bash
set -ueo pipefail
: "${CSGO_DIR:?'ERROR: CSGO_DIR IS NOT SET!'}"
PUGSETUP_CONFIG="$CSGO_DIR/csgo/cfg/sourcemod/pugsetup/pugsetup.cfg"
if [[ -f "$PUGSETUP_CONFIG" ]]; then
# Update PugSetup cvars specified as envvars.
# e.g., `SM_PUGSETUP_SNAKE_CAPTAIN_PICKS=2` will set sm_pugsetup_snake_captain_picks "2" inside of $PUGSETUP_CONFIG
for var in "${!SM_PUGSETUP_@}"; do
cvar=$(echo "$var" | tr '[:upper:]' '[:lower:]')
value=${!var}
sed -i "s/$cvar \"[^\]*\"/$cvar \"$value\"/g" "$PUGSETUP_CONFIG"
done
fi
================================================
FILE: containerfs/start.sh
================================================
#!/usr/bin/env bash
# These envvars should've been set by the Dockerfile
# If they're not set then something went wrong during the build
: "${STEAM_DIR:?'ERROR: STEAM_DIR IS NOT SET!'}"
: "${STEAMCMD_DIR:?'ERROR: STEAMCMD_DIR IS NOT SET!'}"
: "${CSGO_APP_ID:?'ERROR: CSGO_APP_ID IS NOT SET!'}"
: "${CSGO_DIR:?'ERROR: CSGO_DIR IS NOT SET!'}"
# set_env_from_file_or_def VAR [DEFAULT]
# e.g. set_env_from_file_or_def 'RCON_PASSWORD' 'test'
# Fills $VAR either with the content of the file with the name $VAR_FILE
# or with DEFAULT.
# If $VAR is already set nothing will be changed
# If both $VAR and $VAR_FILE are set $VAR will keep its value and content
# of $VAR_FILE will be ignored.
function set_env_from_file_or_def() {
local VAR="$1"
local FILEVAR="${VAR}_FILE"
local DEFAULTVAL="${2:-}"
local RETURNVAL="$DEFAULTVAL"
if [ "${!VAR:-}" ]; then
RETURNVAL="${!VAR}"
elif [ "${!FILEVAR:-}" ]; then
RETURNVAL="$(< "${!FILEVAR}")"
fi
export "$VAR"="$RETURNVAL"
unset "$FILEVAR"
}
export SERVER_HOSTNAME="${SERVER_HOSTNAME:-Counter-Strike: Global Offensive Dedicated Server}"
set_env_from_file_or_def 'SERVER_PASSWORD'
set_env_from_file_or_def 'RCON_PASSWORD' 'changeme'
set_env_from_file_or_def 'STEAM_ACCOUNT' 'changeme'
set_env_from_file_or_def 'AUTHKEY' 'changeme'
set_env_from_file_or_def 'IP' '0.0.0.0'
export PORT="${PORT:-27015}"
export TV_PORT="${TV_PORT:-27020}"
export TICKRATE="${TICKRATE:-128}"
export FPS_MAX="${FPS_MAX:-400}"
export GAME_TYPE="${GAME_TYPE:-0}"
export GAME_MODE="${GAME_MODE:-1}"
export MAP="${MAP:-de_dust2}"
export MAPGROUP="${MAPGROUP:-mg_active}"
export HOST_WORKSHOP_COLLECTION="${HOST_WORKSHOP_COLLECTION:-}"
export WORKSHOP_START_MAP="${WORKSHOP_START_MAP:-}"
export MAXPLAYERS="${MAXPLAYERS:-12}"
export TV_ENABLE="${TV_ENABLE:-1}"
export LAN="${LAN:-0}"
set_env_from_file_or_def 'SOURCEMOD_ADMINS'
export RETAKES="${RETAKES:-0}"
export ANNOUNCEMENT_IP="${ANNOUNCEMENT_IP:-}"
export NOMASTER="${NOMASTER:-}"
# Create dynamic autoexec config
mkdir -p "$CSGO_DIR/csgo/cfg"
if [ ! -s "$CSGO_DIR/csgo/cfg/autoexec.cfg" ]; then
cat << AUTOEXECCFG > "$CSGO_DIR/csgo/cfg/autoexec.cfg"
log on
hostname "$SERVER_HOSTNAME"
rcon_password "$RCON_PASSWORD"
sv_password "$SERVER_PASSWORD"
sv_cheats 0
exec banned_user.cfg
exec banned_ip.cfg
AUTOEXECCFG
else
sed -i "s/^hostname.*/hostname \"$SERVER_HOSTNAME\"/" $CSGO_DIR/csgo/cfg/autoexec.cfg
sed -i "s/^rcon_password.*/rcon_password \"$RCON_PASSWORD\"/" $CSGO_DIR/csgo/cfg/autoexec.cfg
sed -i "s/^sv_password.*/sv_password \"$SERVER_PASSWORD\"/" $CSGO_DIR/csgo/cfg/autoexec.cfg
fi
# Create dynamic server config
if [ ! -s "$CSGO_DIR/csgo/cfg/server.cfg" ]; then
cat << SERVERCFG > "$CSGO_DIR/csgo/cfg/server.cfg"
tv_enable $TV_ENABLE
tv_delaymapchange 1
tv_delay 30
tv_deltacache 2
tv_dispatchmode 1
tv_maxclients 10
tv_maxrate 0
tv_overridemaster 0
tv_relayvoice 1
tv_snapshotrate 64
tv_timeout 60
tv_transmitall 1
writeid
writeip
sv_mincmdrate $TICKRATE
sv_maxupdaterate $TICKRATE
sv_minupdaterate $TICKRATE
SERVERCFG
else
sed -i "s/^tv_enable.*/tv_enable $TV_ENABLE/" $CSGO_DIR/csgo/cfg/server.cfg
fi
# Attempt to update CSGO before starting the server
[[ -z ${CI+x} ]] && "$STEAMCMD_DIR/steamcmd.sh" +login anonymous +force_install_dir "$CSGO_DIR" +app_update "$CSGO_APP_ID" +quit
# Install and configure plugins & extensions
"$BASH" "$STEAM_DIR/manage_plugins.sh"
# Update PugSetup configuration via environment variables
"$BASH" "$STEAM_DIR/manage_pugsetup_configs.sh"
SRCDS_ARGUMENTS=(
"-console"
"-usercon"
"-game csgo"
"-autoupdate"
"-authkey $AUTHKEY"
"-steam_dir $STEAMCMD_DIR"
"-steamcmd_script $STEAM_DIR/autoupdate_script.txt"
"-tickrate $TICKRATE"
"-port $PORT"
"-net_port_try 1"
"-ip $IP"
"-maxplayers_override $MAXPLAYERS"
"+fps_max $FPS_MAX"
"+game_type $GAME_TYPE"
"+game_mode $GAME_MODE"
"+mapgroup $MAPGROUP"
"+map $MAP"
"+sv_setsteamaccount" "$STEAM_ACCOUNT"
"+sv_lan $LAN"
"+tv_port $TV_PORT"
)
if [[ -n $HOST_WORKSHOP_COLLECTION ]]; then
SRCDS_ARGUMENTS+=("+host_workshop_collection $HOST_WORKSHOP_COLLECTION")
fi
if [[ -n $WORKSHOP_START_MAP ]]; then
SRCDS_ARGUMENTS+=("+workshop_start_map $WORKSHOP_START_MAP")
fi
if [[ -n $ANNOUNCEMENT_IP ]]; then
SRCDS_ARGUMENTS+=("+net_public_adr $ANNOUNCEMENT_IP")
fi
if [[ $NOMASTER == 1 ]]; then
SRCDS_ARGUMENTS+=("-nomaster")
fi
SRCDS_RUN="$CSGO_DIR/srcds_run"
# Patch srcds_run to fix autoupdates
if grep -q 'steam.sh' "$SRCDS_RUN"; then
sed -i 's/steam.sh/steamcmd.sh/' "$SRCDS_RUN"
echo "Applied patch to srcds_run to fix autoupdates"
fi
# Start the server
exec "$BASH" "$SRCDS_RUN" "${SRCDS_ARGUMENTS[@]}"
================================================
FILE: docker-compose.yaml
================================================
version: "3.7"
volumes:
csgo-data:
name: csgo-data
services:
csgo:
image: kmallea/csgo:latest
container_name: csgo-ds
environment:
SERVER_HOSTNAME: "Counter-Strike: Global Offensive Dedicated Server"
SERVER_PASSWORD:
RCON_PASSWORD: changeme
STEAM_ACCOUNT: changeme
AUTHKEY: changeme
SOURCEMOD_ADMINS: comma,delimited,list,of,steam,ids
IP: 0.0.0.0
PORT: 27015
TV_PORT: 27020
TICKRATE: 128
FPS_MAX: 300
GAME_TYPE: 0
GAME_MODE: 1
MAP: de_dust2
MAPGROUP: mg_active
MAXPLAYERS: 12
TV_ENABLE: 1
LAN: 0
RETAKES: 0
volumes:
- type: volume
source: csgo-data
target: /home/steam/csgo
network_mode: "host"
restart: unless-stopped
stdin_open: true
tty: true
================================================
FILE: test/bin/bats
================================================
#!/usr/bin/env bash
set -e
BATS_READLINK='true'
if command -v 'greadlink' >/dev/null; then
BATS_READLINK='greadlink'
elif command -v 'readlink' >/dev/null; then
BATS_READLINK='readlink'
fi
bats_resolve_absolute_root_dir() {
local cwd="$PWD"
local path="$1"
local result="$2"
local target_dir
local target_name
local original_shell_options="$-"
# Resolve the parent directory, e.g. /bin => /usr/bin on CentOS (#113).
set -P
while true; do
target_dir="${path%/*}"
target_name="${path##*/}"
if [[ "$target_dir" != "$path" ]]; then
cd "$target_dir"
fi
if [[ -L "$target_name" ]]; then
path="$("$BATS_READLINK" "$target_name")"
else
printf -v "$result" -- '%s' "${PWD%/*}"
set +P "-$original_shell_options"
cd "$cwd"
return
fi
done
}
export BATS_ROOT
bats_resolve_absolute_root_dir "$0" 'BATS_ROOT'
exec "$BATS_ROOT/libexec/bats-core/bats" "$@"
================================================
FILE: test/csgo/cfg/sourcemod/pugsetup/pugsetup.cfg
================================================
sm_pugsetup_snake_captain_picks "0"
================================================
FILE: test/libexec/bats-core/bats
================================================
#!/usr/bin/env bash
set -e
export BATS_VERSION='1.2.0-dev'
version() {
printf 'Bats %s\n' "$BATS_VERSION"
}
abort() {
printf 'Error: %s\n' "$1" >&2
usage >&2
exit 1
}
usage() {
local cmd="${0##*/}"
local line
while IFS= read -r line; do
printf '%s\n' "$line"
done <<END_OF_HELP_TEXT
Usage: $cmd [-cr] [-f <regex>] [-j <jobs>] [-p | -t] <test>...
$cmd [-h | -v]
<test> is the path to a Bats test file, or the path to a directory
containing Bats test files (ending with ".bats").
-c, --count Count the number of test cases without running any tests
-f, --filter Filter test cases by names matching the regular expression
-h, --help Display this help message
-j, --jobs Number of parallel jobs to run (requires GNU parallel)
-p, --pretty Show results in pretty format (default for terminals)
-r, --recursive Include tests in subdirectories
-t, --tap Show results in TAP format
-v, --version Display the version number
For more information, see https://github.com/bats-core/bats-core
END_OF_HELP_TEXT
}
expand_link() {
readlink="$(type -p greadlink readlink | head -1)"
"$readlink" -f "$1"
}
expand_path() {
local path="${1%/}"
local dirname="${path%/*}"
local result="$2"
if [[ "$dirname" == "$path" ]]; then
dirname="$PWD"
else
cd "$dirname"
dirname="$PWD"
cd "$OLDPWD"
fi
printf -v "$result" '%s/%s' "$dirname" "${path##*/}"
}
BATS_LIBEXEC="$(dirname "$(expand_link "${BASH_SOURCE[0]}")")"
export BATS_CWD="$PWD"
export BATS_TEST_PATTERN="^[[:blank:]]*@test[[:blank:]]+(.*[^[:blank:]])[[:blank:]]+\{(.*)\$"
export BATS_TEST_FILTER=
export PATH="$BATS_LIBEXEC:$PATH"
arguments=()
# Unpack single-character options bundled together, e.g. -cr, -pr.
for arg in "$@"; do
if [[ "$arg" =~ ^-[^-]. ]]; then
index=1
while option="${arg:$((index++)):1}"; do
if [[ -z "$option" ]]; then
break
fi
arguments+=("-$option")
done
else
arguments+=("$arg")
fi
shift
done
set -- "${arguments[@]}"
arguments=()
unset flags pretty recursive
flags=()
pretty=
recursive=
if [[ -z "${CI:-}" && -t 0 && -t 1 ]] && command -v tput >/dev/null; then
pretty=1
fi
while [[ "$#" -ne 0 ]]; do
case "$1" in
-h|--help)
version
usage
exit 0
;;
-v|--version)
version
exit 0
;;
-c|--count)
flags+=('-c')
;;
-f|--filter)
shift
flags+=('-f' "$1")
;;
-j|--jobs)
shift
flags+=('-j' "$1")
;;
-r|--recursive)
recursive=1
;;
-t|--tap)
pretty=
;;
-p|--pretty)
pretty=1
;;
-*)
abort "Bad command line option '$1'"
;;
*)
arguments+=("$1")
;;
esac
shift
done
if [[ "${#arguments[@]}" -eq 0 ]]; then
abort 'Must specify at least one <test>'
fi
filenames=()
for filename in "${arguments[@]}"; do
expand_path "$filename" 'filename'
if [[ -d "$filename" ]]; then
shopt -s nullglob
if [[ "$recursive" -eq 1 ]]; then
while IFS= read -r -d $'\0' file; do
filenames+=("$file")
done < <(find "$filename" -type f -name '*.bats' -print0 | sort -z)
else
for suite_filename in "$filename"/*.bats; do
filenames+=("$suite_filename")
done
fi
shopt -u nullglob
else
filenames+=("$filename")
fi
done
formatter="cat"
if [[ -n "$pretty" ]]; then
flags+=("-x")
formatter="bats-format-tap-stream"
fi
set -o pipefail execfail
exec bats-exec-suite "${flags[@]}" "${filenames[@]}" | "$formatter"
================================================
FILE: test/libexec/bats-core/bats-exec-suite
================================================
#!/usr/bin/env bash
set -e
count_only_flag=''
extended_syntax_flag=''
filter=''
num_jobs=1
have_gnu_parallel=
flags=()
while [[ "$#" -ne 0 ]]; do
case "$1" in
-c)
count_only_flag=1
;;
-f)
shift
filter="$1"
flags+=('-f' "$filter")
;;
-j)
shift
num_jobs="$1"
;;
-x)
# shellcheck disable=SC2034
extended_syntax_flag='-x'
flags+=('-x')
;;
*)
break
;;
esac
shift
done
if ( type -p parallel &>/dev/null ); then
# shellcheck disable=SC2034
have_gnu_parallel=1
elif [[ "$num_jobs" != 1 ]]; then
printf 'bats: cannot execute "%s" jobs without GNU parallel\n' "$num_jobs" >&2
exit 1
fi
trap 'kill 0; exit 1' INT
all_tests=()
for filename in "$@"; do
if [[ ! -f "$filename" ]]; then
printf 'bats: %s does not exist\n' "$filename" >&2
exit 1
fi
test_names=()
test_dupes=()
while read -r line; do
if [[ ! "$line" =~ ^bats_test_function\ ]]; then
continue
fi
line="${line%$'\r'}"
line="${line#* }"
all_tests+=( "$(printf "%s\t%s" "$filename" "$line")" )
if [[ " ${test_names[*]} " == *" $line "* ]]; then
test_dupes+=("$line")
continue
fi
test_names+=("$line")
done < <(BATS_TEST_FILTER="$filter" bats-preprocess "$filename")
if [[ "${#test_dupes[@]}" -ne 0 ]]; then
printf 'bats warning: duplicate test name(s) in %s: %s\n' "$filename" "${test_dupes[*]}" >&2
fi
done
test_count="${#all_tests[@]}"
if [[ -n "$count_only_flag" ]]; then
printf '%d\n' "${test_count}"
exit
fi
status=0
printf '1..%d\n' "${test_count}"
# No point on continuing if there's no tests.
if [[ "${test_count}" == 0 ]]; then
exit
fi
if [[ "$num_jobs" != 1 ]]; then
# Only use GNU parallel when we want parallel execution -- there is a small
# amount of overhead using it over a simple loop in the serial case.
set -o pipefail
printf '%s\n' "${all_tests[@]}" | grep -v '^$' | \
parallel -qk -j "$num_jobs" --colsep="\t" -- bats-exec-test "${flags[@]}" '{1}' '{2}' '{#}' || status=1
else
# Just do it serially.
test_number=0
for test_line in "${all_tests[@]}"; do
# Only handle non-empty lines
if [[ $test_line ]]; then
filename="${test_line%%$'\t'*}"
test_name="${test_line##*$'\t'}"
((++test_number))
bats-exec-test "${flags[@]}" "$filename" "$test_name" "$test_number" || status=1
fi
done
if [[ "${test_number}" != "${test_count}" ]]; then
printf '# bats warning: Only executed %s of %s tests\n' "$test_number" "$test_count"
status=1
fi
fi
exit "$status"
================================================
FILE: test/libexec/bats-core/bats-exec-test
================================================
#!/usr/bin/env bash
set -eET
# Variables used in other scripts.
BATS_COUNT_ONLY=''
BATS_TEST_FILTER=''
BATS_EXTENDED_SYNTAX=''
while [[ "$#" -ne 0 ]]; do
case "$1" in
-c)
# shellcheck disable=SC2034
BATS_COUNT_ONLY=1
;;
-f)
shift
# shellcheck disable=SC2034
BATS_TEST_FILTER="$1"
;;
-x)
BATS_EXTENDED_SYNTAX='-x'
;;
*)
break
;;
esac
shift
done
BATS_TEST_FILENAME="$1"
shift
if [[ -z "$BATS_TEST_FILENAME" ]]; then
printf 'usage: bats-exec-test <filename>\n' >&2
exit 1
elif [[ ! -f "$BATS_TEST_FILENAME" ]]; then
printf 'bats: %s does not exist\n' "$BATS_TEST_FILENAME" >&2
exit 1
fi
BATS_TEST_DIRNAME="${BATS_TEST_FILENAME%/*}"
BATS_TEST_NAMES=()
load() {
local name="$1"
local filename
if [[ "${name:0:1}" == '/' ]]; then
filename="${name}"
else
filename="$BATS_TEST_DIRNAME/${name}.bash"
fi
if [[ ! -f "$filename" ]]; then
printf 'bats: %s does not exist\n' "$filename" >&2
exit 1
fi
# Dynamically loaded user files provided outside of Bats.
# shellcheck disable=SC1090
source "${filename}"
}
run() {
local origFlags="$-"
set +eET
local origIFS="$IFS"
# 'output', 'status', 'lines' are global variables available to tests.
# shellcheck disable=SC2034
output="$("$@" 2>&1)"
# shellcheck disable=SC2034
status="$?"
# shellcheck disable=SC2034,SC2206
IFS=$'\n' lines=($output)
IFS="$origIFS"
set "-$origFlags"
}
setup() {
return 0
}
teardown() {
return 0
}
skip() {
BATS_TEST_SKIPPED="${1:-1}"
BATS_TEST_COMPLETED=1
exit 0
}
bats_test_begin() {
BATS_TEST_DESCRIPTION="$1"
if [[ -n "$BATS_EXTENDED_SYNTAX" ]]; then
printf 'begin %d %s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" >&3
fi
setup
}
bats_test_function() {
local test_name="$1"
BATS_TEST_NAMES+=("$test_name")
}
bats_capture_stack_trace() {
local test_file
local funcname
local i
BATS_STACK_TRACE=()
for ((i=2; i != ${#FUNCNAME[@]}; ++i)); do
# Use BATS_TEST_SOURCE if necessary to work around Bash < 4.4 bug whereby
# calling an exported function erases the test file's BASH_SOURCE entry.
test_file="${BASH_SOURCE[$i]:-$BATS_TEST_SOURCE}"
funcname="${FUNCNAME[$i]}"
BATS_STACK_TRACE+=("${BASH_LINENO[$((i-1))]} $funcname $test_file")
if [[ "$test_file" == "$BATS_TEST_SOURCE" ]]; then
case "$funcname" in
"$BATS_TEST_NAME"|setup|teardown)
break
;;
esac
fi
done
}
bats_print_stack_trace() {
local frame
local index=1
local count="${#@}"
local filename
local lineno
for frame in "$@"; do
bats_frame_filename "$frame" 'filename'
bats_trim_filename "$filename" 'filename'
bats_frame_lineno "$frame" 'lineno'
if [[ $index -eq 1 ]]; then
printf '# ('
else
printf '# '
fi
local fn
bats_frame_function "$frame" 'fn'
if [[ "$fn" != "$BATS_TEST_NAME" ]]; then
printf "from function \`%s' " "$fn"
fi
if [[ $index -eq $count ]]; then
printf 'in test file %s, line %d)\n' "$filename" "$lineno"
else
printf 'in file %s, line %d,\n' "$filename" "$lineno"
fi
((++index))
done
}
bats_print_failed_command() {
local frame="${BATS_STACK_TRACE[${#BATS_STACK_TRACE[@]}-1]}"
local filename
local lineno
local failed_line
local failed_command
bats_frame_filename "$frame" 'filename'
bats_frame_lineno "$frame" 'lineno'
bats_extract_line "$filename" "$lineno" 'failed_line'
bats_strip_string "$failed_line" 'failed_command'
printf '%s' "# \`${failed_command}' "
if [[ "$BATS_ERROR_STATUS" -eq 1 ]]; then
printf 'failed\n'
else
printf 'failed with status %d\n' "$BATS_ERROR_STATUS"
fi
}
bats_frame_lineno() {
printf -v "$2" '%s' "${1%% *}"
}
bats_frame_function() {
local __bff_function="${1#* }"
printf -v "$2" '%s' "${__bff_function%% *}"
}
bats_frame_filename() {
local __bff_filename="${1#* }"
__bff_filename="${__bff_filename#* }"
if [[ "$__bff_filename" == "$BATS_TEST_SOURCE" ]]; then
__bff_filename="$BATS_TEST_FILENAME"
fi
printf -v "$2" '%s' "$__bff_filename"
}
bats_extract_line() {
local __bats_extract_line_line
local __bats_extract_line_index=0
while IFS= read -r __bats_extract_line_line; do
if [[ "$((++__bats_extract_line_index))" -eq "$2" ]]; then
printf -v "$3" '%s' "${__bats_extract_line_line%$'\r'}"
break
fi
done <"$1"
}
bats_strip_string() {
[[ "$1" =~ ^[[:space:]]*(.*)[[:space:]]*$ ]]
printf -v "$2" '%s' "${BASH_REMATCH[1]}"
}
bats_trim_filename() {
printf -v "$2" '%s' "${1#$BATS_CWD/}"
}
bats_debug_trap() {
if [[ "${BASH_SOURCE[0]}" != "$1" ]]; then
# The last entry in the stack trace is not useful when en error occured:
# It is either duplicated (kinda correct) or has wrong line number (Bash < 4.4)
# Therefore we capture the stacktrace but use it only after the next debug
# trap fired.
# Expansion is required for empty arrays which otherwise error
BATS_CURRENT_STACK_TRACE=( "${BATS_STACK_TRACE[@]+"${BATS_STACK_TRACE[@]}"}" )
bats_capture_stack_trace
fi
}
# For some versions of Bash, the `ERR` trap may not always fire for every
# command failure, but the `EXIT` trap will. Also, some command failures may not
# set `$?` properly. See #72 and #81 for details.
#
# For this reason, we call `bats_error_trap` at the very beginning of
# `bats_teardown_trap` (the `DEBUG` trap for the call will fix the stack trace)
# and check the value of `$BATS_TEST_COMPLETED` before taking other actions.
# We also adjust the exit status value if needed.
#
# See `bats_exit_trap` for an additional EXIT error handling case when `$?`
# isn't set properly during `teardown()` errors.
bats_error_trap() {
local status="$?"
if [[ -z "$BATS_TEST_COMPLETED" ]]; then
BATS_ERROR_STATUS="${BATS_ERROR_STATUS:-$status}"
if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then
BATS_ERROR_STATUS=1
fi
BATS_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" )
trap - DEBUG
fi
}
bats_teardown_trap() {
bats_error_trap
local status=0
teardown >>"$BATS_OUT" 2>&1 || status="$?"
if [[ $status -eq 0 ]]; then
BATS_TEARDOWN_COMPLETED=1
elif [[ -n "$BATS_TEST_COMPLETED" ]]; then
BATS_ERROR_STATUS="$status"
fi
bats_exit_trap
}
bats_exit_trap() {
local line
local status
local skipped=''
trap - ERR EXIT
if [[ -n "$BATS_TEST_SKIPPED" ]]; then
skipped=' # skip'
if [[ "$BATS_TEST_SKIPPED" != '1' ]]; then
skipped+=" $BATS_TEST_SKIPPED"
fi
fi
if [[ -z "$BATS_TEST_COMPLETED" || -z "$BATS_TEARDOWN_COMPLETED" ]]; then
if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then
# For some versions of bash, `$?` may not be set properly for some error
# conditions before triggering the EXIT trap directly (see #72 and #81).
# Thanks to the `BATS_TEARDOWN_COMPLETED` signal, this will pinpoint such
# errors if they happen during `teardown()` when `bats_perform_test` calls
# `bats_teardown_trap` directly after the test itself passes.
#
# If instead the test fails, and the `teardown()` error happens while
# `bats_teardown_trap` runs as the EXIT trap, the test will fail with no
# output, since there's no way to reach the `bats_exit_trap` call.
BATS_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" )
BATS_ERROR_STATUS=1
fi
printf 'not ok %d %s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" >&3
bats_print_stack_trace "${BATS_STACK_TRACE[@]}" >&3
bats_print_failed_command >&3
while IFS= read -r line; do
printf '# %s\n' "$line"
done <"$BATS_OUT" >&3
if [[ -n "$line" ]]; then
printf '# %s\n' "$line"
fi
status=1
else
printf 'ok %d %s%s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" \
"$skipped" >&3
status=0
fi
rm -f "$BATS_OUT"
bats_cleanup_preprocessed_source
exit "$status"
}
bats_perform_test() {
BATS_TEST_NAME="$1"
BATS_TEST_NUMBER="$2"
if ! declare -F "$BATS_TEST_NAME" &>/dev/null; then
printf "bats: unknown test name \`%s'\n" "$BATS_TEST_NAME" >&2
exit 1
fi
# Some versions of Bash will reset BASH_LINENO to the first line of the
# function when the ERR trap fires. All versions of Bash appear to reset it
# on an unbound variable access error. bats_debug_trap will fire both before
# the offending line is executed, and when the error is triggered.
# Consequently, we use `BATS_CURRENT_STACK_TRACE` recorded by the
# first call to bats_debug_trap, _before_ the ERR trap or unbound variable
# access fires.
BATS_STACK_TRACE=()
BATS_CURRENT_STACK_TRACE=()
BATS_TEST_COMPLETED=
BATS_TEST_SKIPPED=
BATS_TEARDOWN_COMPLETED=
BATS_ERROR_STATUS=
trap 'bats_debug_trap "$BASH_SOURCE"' DEBUG
trap 'bats_error_trap' ERR
trap 'bats_teardown_trap' EXIT
"$BATS_TEST_NAME" >>"$BATS_OUT" 2>&1
BATS_TEST_COMPLETED=1
trap 'bats_exit_trap' EXIT
bats_teardown_trap
}
if [[ -z "$TMPDIR" ]]; then
BATS_TMPDIR='/tmp'
else
BATS_TMPDIR="${TMPDIR%/}"
fi
BATS_TMPNAME="$BATS_TMPDIR/bats.$$"
BATS_PARENT_TMPNAME="$BATS_TMPDIR/bats.$PPID"
BATS_OUT="${BATS_TMPNAME}.out"
bats_preprocess_source() {
BATS_TEST_SOURCE="${BATS_TMPNAME}.src"
bats-preprocess "$BATS_TEST_FILENAME" >"$BATS_TEST_SOURCE"
trap 'bats_cleanup_preprocessed_source' ERR EXIT
trap 'bats_cleanup_preprocessed_source; exit 1' INT
}
bats_cleanup_preprocessed_source() {
rm -f "$BATS_TEST_SOURCE"
}
bats_evaluate_preprocessed_source() {
if [[ -z "$BATS_TEST_SOURCE" ]]; then
BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src"
fi
# Dynamically loaded user files provided outside of Bats.
# shellcheck disable=SC1090
source "$BATS_TEST_SOURCE"
}
exec 3<&1
# Run the given test.
bats_preprocess_source
bats_evaluate_preprocessed_source
bats_perform_test "$@"
================================================
FILE: test/libexec/bats-core/bats-format-tap-stream
================================================
#!/usr/bin/env bash
set -e
header_pattern='[0-9]+\.\.[0-9]+'
IFS= read -r header
if [[ "$header" =~ $header_pattern ]]; then
count="${header:3}"
index=0
passed=0
failures=0
skipped=0
name=
count_column_width=$(( ${#count} * 2 + 2 ))
else
# If the first line isn't a TAP plan, print it and pass the rest through
printf '%s\n' "$header"
exec cat
fi
update_screen_width() {
screen_width="$(tput cols)"
count_column_left=$(( screen_width - count_column_width ))
}
trap update_screen_width WINCH
update_screen_width
begin() {
go_to_column 0
buffer_with_truncation $(( count_column_left - 1 )) ' %s' "$name"
clear_to_end_of_line
go_to_column $count_column_left
buffer "%${#count}s/${count}" "$index"
go_to_column 1
}
pass() {
go_to_column 0
buffer ' ✓ %s' "$name"
advance
}
skip() {
local reason="$1"
if [[ -n "$reason" ]]; then
reason=": $reason"
fi
go_to_column 0
buffer ' - %s (skipped%s)' "$name" "$reason"
advance
}
fail() {
go_to_column 0
set_color 1 bold
buffer ' ✗ %s' "$name"
advance
}
log() {
set_color 1
buffer ' %s\n' "$1"
clear_color
}
summary() {
buffer '\n%d test' "$count"
if [[ "$count" -ne 1 ]]; then
buffer 's'
fi
buffer ', %d failure' "$failures"
if [[ "$failures" -ne 1 ]]; then
buffer 's'
fi
if [[ "$skipped" -gt 0 ]]; then
buffer ', %d skipped' "$skipped"
fi
not_run=$((count - passed - failures - skipped))
if [[ "$not_run" -gt 0 ]]; then
buffer ', %d not run' "$not_run"
fi
buffer '\n'
}
buffer_with_truncation() {
local width="$1"
shift
local string
# shellcheck disable=SC2059
printf -v 'string' -- "$@"
if [[ "${#string}" -gt "$width" ]]; then
buffer '%s...' "${string:0:$(( width - 4 ))}"
else
buffer '%s' "$string"
fi
}
go_to_column() {
local column="$1"
buffer '\x1B[%dG' $(( column + 1 ))
}
clear_to_end_of_line() {
buffer '\x1B[K'
}
advance() {
clear_to_end_of_line
buffer '\n'
clear_color
}
set_color() {
local color="$1"
local weight=22
if [[ "$2" == 'bold' ]]; then
weight=1
fi
buffer '\x1B[%d;%dm' "$(( 30 + color ))" "$weight"
}
clear_color() {
buffer '\x1B[0m'
}
_buffer=
buffer() {
local content
# shellcheck disable=SC2059
printf -v content -- "$@"
_buffer+="$content"
}
flush() {
printf '%s' "$_buffer"
_buffer=
}
finish() {
flush
printf '\n'
}
trap finish EXIT
while IFS= read -r line; do
case "$line" in
'begin '* )
((++index))
name="${line#* $index }"
begin
flush
;;
'ok '* )
skip_expr="ok $index (.*) # skip ?(([[:print:]]*))?"
if [[ "$line" =~ $skip_expr ]]; then
((++skipped))
skip "${BASH_REMATCH[2]}"
else
((++passed))
pass
fi
;;
'not ok '* )
((++failures))
fail
;;
'# '* )
log "${line:2}"
;;
esac
done
summary
================================================
FILE: test/libexec/bats-core/bats-preprocess
================================================
#!/usr/bin/env bash
set -e
bats_encode_test_name() {
local name="$1"
local result='test_'
local hex_code
if [[ ! "$name" =~ [^[:alnum:]\ _-] ]]; then
name="${name//_/-5f}"
name="${name//-/-2d}"
name="${name// /_}"
result+="$name"
else
local length="${#name}"
local char i
for ((i=0; i<length; i++)); do
char="${name:$i:1}"
if [[ "$char" == ' ' ]]; then
result+='_'
elif [[ "$char" =~ [[:alnum:]] ]]; then
result+="$char"
else
printf -v 'hex_code' -- '-%02x' \'"$char"
result+="$hex_code"
fi
done
fi
printf -v "$2" '%s' "$result"
}
test_file="$1"
tests=()
{
while IFS= read -r line; do
line="${line//$'\r'}"
if [[ "$line" =~ $BATS_TEST_PATTERN ]]; then
name="${BASH_REMATCH[1]#[\'\"]}"
name="${name%[\'\"]}"
body="${BASH_REMATCH[2]}"
bats_encode_test_name "$name" 'encoded_name'
printf '%s() { bats_test_begin "%s"; %s\n' "${encoded_name:?}" "$name" "$body" || :
if [[ -z "$BATS_TEST_FILTER" || "$name" =~ $BATS_TEST_FILTER ]]; then
tests+=("$encoded_name")
fi
else
printf '%s\n' "$line"
fi
done
} <<< "$(< "$test_file")"$'\n'
for test_name in "${tests[@]}"; do
printf 'bats_test_function %s\n' "$test_name"
done
================================================
FILE: test/srcds_run
================================================
#!/usr/bin/env bash
./bin/bats tests.bats
================================================
FILE: test/tests.bats
================================================
#!/home/steam/csgo/bin/bats
@test "steam script filename is patched to fix autoupdates" {
result="steamcmd.sh"
[ "$result" == "steamcmd.sh" ]
}
@test "Metamod is installed" {
run test -f csgo/addons/metamod.vdf
[ "$status" -eq 0 ]
}
@test "Sourcemod is installed" {
run test -f csgo/addons/metamod/sourcemod.vdf
[ "$status" -eq 0 ]
}
@test "SteamWorks is installed" {
run test -f csgo/addons/sourcemod/extensions/SteamWorks.ext.so
[ "$status" -eq 0 ]
}
@test "Updater is installed" {
run test -f csgo/addons/sourcemod/plugins/updater.smx
[ "$status" -eq 0 ]
}
@test "PugSetup is installed" {
run test -f csgo/addons/sourcemod/plugins/pugsetup.smx
[ "$status" -eq 0 ]
}
@test "PracticeMode is installed" {
run test -f csgo/addons/sourcemod/plugins/practicemode.smx
[ "$status" -eq 0 ]
}
@test "Retakes is installed and disabled" {
run test -f csgo/addons/sourcemod/plugins/disabled/retakes.smx
[ "$status" -eq 0 ]
}
@test "Retakes Instadefuse is installed and disabled" {
run test -f csgo/addons/sourcemod/plugins/disabled/retakes-instadefuse.smx
[ "$status" -eq 0 ]
}
@test "Retakes Autoplant is installed and disabled" {
run test -f csgo/addons/sourcemod/plugins/disabled/retakes_autoplant.smx
[ "$status" -eq 0 ]
}
@test "Retakes HUD is installed and disabled" {
run test -f csgo/addons/sourcemod/plugins/disabled/retakes-hud.smx
[ "$status" -eq 0 ]
}
@test "Steam IDs were added to admins list" {
run grep -q 'STEAM_1:0:654321' 'csgo/addons/sourcemod/configs/admins_simple.ini'
[ "$status" -eq 0 ]
}
@test "PugSetup config modified correctly" {
run grep -q 'sm_pugsetup_snake_captain_picks \"2\"' 'csgo/cfg/sourcemod/pugsetup/pugsetup.cfg'
[ "$status" -eq 0 ]
}
@test "Markers created for caching plugins" {
num_marker_files=( csgo/*.marker )
result="${#num_marker_files[@]}"
[ "$result" -eq 10 ]
}
gitextract_deglkiou/
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── cloud-config.yaml
├── containerfs/
│ ├── README.md
│ ├── manage_plugins.sh
│ ├── manage_pugsetup_configs.sh
│ └── start.sh
├── docker-compose.yaml
└── test/
├── bin/
│ └── bats
├── csgo/
│ └── cfg/
│ └── sourcemod/
│ └── pugsetup/
│ └── pugsetup.cfg
├── libexec/
│ └── bats-core/
│ ├── bats
│ ├── bats-exec-suite
│ ├── bats-exec-test
│ ├── bats-format-tap-stream
│ └── bats-preprocess
├── srcds_run
└── tests.bats
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (57K chars).
[
{
"path": "Dockerfile",
"chars": 1601,
"preview": "FROM ubuntu:bionic\n\nENV TERM xterm\n\nENV STEAM_DIR /home/steam\nENV STEAMCMD_DIR /home/steam/steamcmd\nENV CSGO_APP_ID 740\n"
},
{
"path": "LICENSE",
"chars": 1210,
"preview": "This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, c"
},
{
"path": "Makefile",
"chars": 2542,
"preview": "SHELL := /bin/bash\n\nCONTAINER_NAME ?= csgo-dedicated-server \nIMAGE_NAME ?= kmallea/csgo:latest\nSERVER_HOSTNAME ?= Counte"
},
{
"path": "README.md",
"chars": 9735,
"preview": "# CSGO containerized\n\nThe Dockerfile will build an image for running a Counter-Strike: Global Offensive dedicated server"
},
{
"path": "cloud-config.yaml",
"chars": 3497,
"preview": "#cloud-config\n\n# This config is an example of of a one-liner to getting a CSGO server up-and-running\n# in just a few min"
},
{
"path": "containerfs/README.md",
"chars": 767,
"preview": "## Adding your own files, plugins, etc.\n\nThe directory `containerfs` (container filesystem) is the equivalent of the roo"
},
{
"path": "containerfs/manage_plugins.sh",
"chars": 4085,
"preview": "#!/usr/bin/env bash\n\nset -ueo pipefail\n\n: \"${CSGO_DIR:?'ERROR: CSGO_DIR IS NOT SET!'}\"\n\nexport RETAKES=\"${RETAKES:-0}\"\n\n"
},
{
"path": "containerfs/manage_pugsetup_configs.sh",
"chars": 568,
"preview": "#!/usr/bin/env bash\n\nset -ueo pipefail\n\n: \"${CSGO_DIR:?'ERROR: CSGO_DIR IS NOT SET!'}\"\n\nPUGSETUP_CONFIG=\"$CSGO_DIR/csgo/"
},
{
"path": "containerfs/start.sh",
"chars": 4641,
"preview": "#!/usr/bin/env bash\n \n# These envvars should've been set by the Dockerfile\n# If they're not set then something went wron"
},
{
"path": "docker-compose.yaml",
"chars": 828,
"preview": "version: \"3.7\"\n\nvolumes:\n csgo-data:\n name: csgo-data\n\nservices:\n csgo:\n image: kmallea/csgo:latest\n containe"
},
{
"path": "test/bin/bats",
"chars": 936,
"preview": "#!/usr/bin/env bash\n\nset -e\n\nBATS_READLINK='true'\nif command -v 'greadlink' >/dev/null; then\n BATS_READLINK='greadlink'"
},
{
"path": "test/csgo/cfg/sourcemod/pugsetup/pugsetup.cfg",
"chars": 36,
"preview": "sm_pugsetup_snake_captain_picks \"0\"\n"
},
{
"path": "test/libexec/bats-core/bats",
"chars": 3520,
"preview": "#!/usr/bin/env bash\nset -e\n\nexport BATS_VERSION='1.2.0-dev'\n\nversion() {\n printf 'Bats %s\\n' \"$BATS_VERSION\"\n}\n\nabort()"
},
{
"path": "test/libexec/bats-core/bats-exec-suite",
"chars": 2565,
"preview": "#!/usr/bin/env bash\nset -e\n\ncount_only_flag=''\nextended_syntax_flag=''\nfilter=''\nnum_jobs=1\nhave_gnu_parallel=\nflags=()\n"
},
{
"path": "test/libexec/bats-core/bats-exec-test",
"chars": 9822,
"preview": "#!/usr/bin/env bash\nset -eET\n\n# Variables used in other scripts.\nBATS_COUNT_ONLY=''\nBATS_TEST_FILTER=''\nBATS_EXTENDED_SY"
},
{
"path": "test/libexec/bats-core/bats-format-tap-stream",
"chars": 2865,
"preview": "#!/usr/bin/env bash\nset -e\n\nheader_pattern='[0-9]+\\.\\.[0-9]+'\nIFS= read -r header\n\nif [[ \"$header\" =~ $header_pattern ]]"
},
{
"path": "test/libexec/bats-core/bats-preprocess",
"chars": 1306,
"preview": "#!/usr/bin/env bash\nset -e\n\nbats_encode_test_name() {\n local name=\"$1\"\n local result='test_'\n local hex_code\n\n if [["
},
{
"path": "test/srcds_run",
"chars": 43,
"preview": "#!/usr/bin/env bash\n\n./bin/bats tests.bats\n"
},
{
"path": "test/tests.bats",
"chars": 1876,
"preview": "#!/home/steam/csgo/bin/bats\n\n@test \"steam script filename is patched to fix autoupdates\" {\n result=\"steamcmd.sh\"\n [ \"$"
}
]
About this extraction
This page contains the full source code of the kaimallea/csgo GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (51.2 KB), approximately 16.4k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.