cs16-dm-server: CS 1.6 deathmatch server in Docker

One-command setup (SteamCMD + pinned, checksum-verified ReHLDS / ReGameDLL /
Metamod-R / AMXX / ReAPI / ReUnion / CSDM-ReAPI / YaPB / GunGame), .env
settings, ./cs menu (map, DM/GunGame mode, bots, kill counter, restart,
update), own plugins (extras, rtv, mode switch), all-time kill counter,
watchdog + nightly backup, docs for admins and players.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
esefandClaude Opus 5.5 committed 2026-10-11 05:03:07 +03:00
commit 7e9cbee14d
33 files changed
+1890

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
# cs16-dm-server settings. setup.sh copies this to .env (and asks for the important ones).
# After editing .env: docker compose up -d (or ./cs -> "apply .env")
# No double quotes inside values.
# Name in the server browser and on the scoreboard
CS_HOSTNAME=CS 1.6 Deathmatch
# Join password (empty = open server, anyone can join)
CS_PASSWORD=
CS_MAXPLAYERS=16
# Game port (UDP). Change it to run a second server on the same machine.
CS_PORT=27015
# Map the server starts on (after that it follows hlds/cstrike/mapcycle.txt)
CS_MAP=de_dust2
# Game mode on the FIRST install: dm (free-for-all deathmatch) or gg (GunGame).
# After that the mode is switched live with ./cs (or rcon esef_mode dm|gg) and remembered.
CS_MODE=dm
# YaPB bots: fill the server up to this many players (bots leave as humans join). 0 = no bots.
CS_BOTS=6
# 0 = easy .. 4 = expert
CS_BOT_DIFFICULTY=2
# Docker container name (unique per server on this machine)
CS_NAME=cs16
# User/group the server runs as (owner of this folder). setup.sh fills these in.
CS_UID=1000
CS_GID=1000
+2
View File
@@ -0,0 +1,2 @@
* text=auto eol=lf
*.wav binary
+13
View File
@@ -0,0 +1,13 @@
# per-machine settings and secrets
.env
secrets/
# downloaded / generated (setup.sh recreates them)
hlds/
steamcmd-home/
dl/
# runtime state
backups/
kills-tally.json*
watchdog.log
.watchdog-fails
__pycache__/
+11
View File
@@ -0,0 +1,11 @@
# CS 1.6 dedicated server runtime + steamcmd. Game files live in ./hlds (bind mount).
# unzip/xz-utils: tools/install-addons.sh unpacks the pinned downloads inside this image,
# so the host only needs Docker, git and python3.
FROM debian:bookworm-slim
RUN dpkg --add-architecture i386 && apt-get update && \
apt-get install -y --no-install-recommends ca-certificates curl lib32gcc-s1 lib32stdc++6 libc6-i386 \
unzip xz-utils && \
rm -rf /var/lib/apt/lists/*
RUN mkdir -p /opt/steamcmd && curl -sSL https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz | tar xz -C /opt/steamcmd && \
chmod -R a+rwX /opt/steamcmd
WORKDIR /cs/hlds
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 cs16-dm-server contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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 OR COPYRIGHT HOLDERS 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.
+119
View File
@@ -0,0 +1,119 @@
# cs16-dm-server
A **Counter-Strike 1.6 deathmatch server** in Docker. One command sets it up, and a menu runs it.
Free-for-all deathmatch: instant respawn, a gun menu, no rounds, everyone against everyone. Switch to **GunGame** any time. Bots keep it busy when nobody's on, and they leave as people join. Your friends can join with the Steam version or a non-Steam client.
- **One command:** `./setup.sh` asks 5 questions, then downloads the game (SteamCMD) and the mods (pinned versions, checksum-verified), compiles the plugins and starts the server.
- **One menu:** `./cs` shows status, and lets you change the map, switch DM ↔ GunGame, set the bot count, see the kill counter, restart, or update. No console commands to memorize.
- **Game feel:** damage numbers, killstreak and multikill announcer (Quake/UT voices), headshot and knife sounds, +HP per kill, a K/D/streak HUD line, rock-the-vote (`rtv`), `/rank` and `/top15` stats.
- **All-time kill counter** (`./kills`), with top fraggers and favourite weapons. Bots count too, so it climbs fast.
- **Recovers on its own:** Docker restarts the server after a crash, a watchdog restarts it if it hangs, and backups run nightly.
Built on ReHLDS + ReGameDLL + Metamod-R + AMX Mod X + ReAPI + ReUnion + CSDM-ReAPI + YaPB + GunGame. Details: [docs/HOW-IT-WORKS.md](docs/HOW-IT-WORKS.md).
## Quick start
You need:
- a Linux machine on x86-64 (the game server is 32-bit x86, so no Raspberry Pi or other ARM; any small box or VPS works: with 6 bots the server uses ~10% of one core and ~100 MB of RAM)
- [Docker with the compose plugin](https://docs.docker.com/engine/install/)
- `git` and `python3`
- ~1.5 GB of disk
```sh
git clone https://git.userevolt.app/esef/cs16-dm-server.git
cd cs16-dm-server
./setup.sh # asks: server name, join password, port, mode, bots
./scripts/install-cron.sh # optional but recommended: watchdog + nightly backup
```
Run it as a normal user in the `docker` group, not as root. When setup finishes it prints the `connect` addresses.
### Letting people in from the internet
Pick one:
- **Port forward:** forward **UDP 27015** (your `CS_PORT`) on your router to this machine. Players use `connect <your public IP>:27015`.
- **No port forwarding** (CGNAT, dorm, mobile internet): make a free [playit.gg](https://playit.gg) account, install their agent on this machine, and add a tunnel. Choose type **UDP** and local address `127.0.0.1:27015`. Players then use the address playit gives you, for example `connect something.ply.gg:12345`. Every player arrives from playit's IP, and the server is set up for that: `sv_rehlds_maxclients_from_single_ip 16`, short RCON bans. Never ban players by IP, because an IP ban would kick everyone.
- **Just friends:** a VPN like Tailscale or ZeroTier. Everyone joins with `connect <vpn IP>:27015`.
How players join, Steam or not: [docs/PLAYERS.md](docs/PLAYERS.md).
## Running it
```
./cs menu: pick a number
./cs status who's on, map, mode
./cs map de_inferno change map
./cs mode gg GunGame (./cs mode dm = back to deathmatch; the mode is remembered)
./cs bots 6 fill up to 6 players with bots (0 = none; saved in .env)
./cs kills all-time kill counter
./rcon server console (./rcon <command> for one command)
```
For admins, maps, fast downloads, several servers and backups, see [docs/ADMIN.md](docs/ADMIN.md).
## Settings
The basics live in `.env` (created from [`.env.example`](.env.example) by setup). After editing it, run `./cs apply`, which is the same as `docker compose up -d`.
| Setting | Default | |
|---|---|---|
| `CS_HOSTNAME` | `CS 1.6 Deathmatch` | name in the server browser |
| `CS_PASSWORD` | empty | join password; empty = open server |
| `CS_MAXPLAYERS` | `16` | |
| `CS_PORT` | `27015` | game port (UDP) |
| `CS_MAP` | `de_dust2` | first map; after that `hlds/cstrike/mapcycle.txt` |
| `CS_MODE` | `dm` | starting mode, `dm` or `gg` (first install only; after that use `./cs mode`) |
| `CS_BOTS` | `6` | bots fill the server up to this many players; `0` = no bots |
| `CS_BOT_DIFFICULTY` | `2` | `0` easy … `4` expert |
| `CS_NAME` | `cs16` | Docker container name |
Everything else goes in **`hlds/cstrike/custom.cfg`**. It runs after `server.cfg` on every map and setup never overwrites it. Examples are inside: time limit, alltalk, the extras' HP-per-kill and sounds.
### What the modes are
| | Deathmatch (`dm`) | GunGame (`gg`) |
|---|---|---|
| Plugin | CSDM-ReAPI | GunGame 2.13c |
| Respawn | 0.7 s, 2 s spawn protection | 1 s |
| Weapons | gun menu on spawn | glock → pistols → shotguns → SMGs → rifles → M249 → HE → knife, 2 kills per level |
| Teams | free for all | free for all |
| Map | 20 min | 30 min, 30 s knife warmup |
Both use the DM spawn points for the 11 stock maps in the default mapcycle. To add your own maps, see [docs/ADMIN.md](docs/ADMIN.md).
## Layout
```
setup.sh install / re-run / --update
cs, rcon, kills menu, console, kill counter
.env.example settings template (.env is yours, not in git)
docker-compose.yml the container; Dockerfile = Debian + 32-bit libs + SteamCMD
cstrike/ configs copied into the game on every setup (server.cfg, plugin lists, GunGame cfg)
defaults/ mapcycle.txt + custom.cfg, copied once and then yours
plugins/ source of the own plugins (extras, rtv, mode switch), compiled by setup
tools/ versions.txt (pinned downloads + sha256), install-addons.sh, start.sh, hlq.py
scripts/ watchdog, backup, install-cron
hlds/ the game server (downloaded; maps, logs, stats live here)
```
## Updating
`git pull && ./setup.sh` picks up new configs and plugins. `./setup.sh --update` also updates the game from Steam. Neither changes your `.env`, secrets, admins (`users.ini`), `mapcycle.txt`, `custom.cfg`, CSDM `config.ini`, stats or the kill counter.
## Credits
Downloaded by setup from their own projects (not part of this repo):
- [ReHLDS](https://github.com/rehlds/ReHLDS), [ReGameDLL_CS](https://github.com/rehlds/ReGameDLL_CS), [Metamod-R](https://github.com/rehlds/Metamod-R), [ReAPI](https://github.com/rehlds/ReAPI), [ReUnion](https://github.com/rehlds/ReUnion)
- [AMX Mod X](https://www.amxmodx.org)
- [CSDM-ReAPI](https://github.com/wopox1337/CSDM-ReAPI)
- [YaPB](https://github.com/yapb/yapb)
- [GunGame by Avalanche](https://github.com/fysiks1/amxx-gungame_cstrike)
- DM spawn points from [CSDM 2.1 by Arkshine](https://github.com/Arkshine/CSDM)
- announcer sounds from [hpoon/HLDS-CS1.6](https://github.com/hpoon/HLDS-CS1.6)
Counter-Strike is Valve's. The game server files come from Steam (SteamCMD, anonymous login).
The scripts, configs and plugins in this repo are MIT licensed ([LICENSE](LICENSE)).
Executable
+88
View File
@@ -0,0 +1,88 @@
#!/bin/sh
# Control menu: ./cs (pick a number)
# or directly: ./cs status | map <name> | mode dm|gg | bots <n> | kills | say <text>
# ./cs restart | stop | start | logs | console | apply | update
D=$(cd "$(dirname "$0")" && pwd)
cd "$D" || exit 1
[ -f .env ] || { echo "no .env yet: run ./setup.sh first"; exit 1; }
getenv() { sed -n "s/^$1=//p" .env | tr -d '\r'; }
setenv() { sed -i "s|^$1=.*|$1=$2|" .env; }
NAME=$(getenv CS_NAME); NAME=${NAME:-cs16}
PORT=$(getenv CS_PORT); PORT=${PORT:-27015}
C=hlds/cstrike
status() {
st=$(docker inspect -f '{{.State.Status}}' "$NAME" 2>/dev/null || echo "not created")
if [ "$st" = running ]; then
if out=$(python3 tools/hlq.py info 127.0.0.1 "$PORT" 2>/dev/null); then echo " ${out#A2S_INFO ok: }"
else echo " container running, server not answering yet (starting or changing map?)"; fi
if [ -f $C/addons/amxmodx/configs/plugins-gungame.ini ]; then echo " mode: GunGame"; else echo " mode: deathmatch"; fi
else
echo " server is $st (start it: ./cs start)"
fi
}
players() { ./rcon status | grep -a -E '^#|^map|^players' ; }
setmap() {
m=$1
if [ -z "$m" ]; then
echo "Maps on this server:"
ls $C/maps/*.bsp 2>/dev/null | sed 's|.*/||; s|\.bsp$||' | nl -w3 -s') ' | pr -3 -t -w 90
printf 'map name or number: '; read -r m
case "$m" in ''|*[!0-9]*) ;; *) m=$(ls $C/maps/*.bsp | sed 's|.*/||; s|\.bsp$||' | sed -n "${m}p") ;; esac
fi
[ -n "$m" ] || return
[ -f "$C/maps/$m.bsp" ] || { echo "no map called '$m' in $C/maps"; return; }
r=$(./rcon changelevel "$m"); case "$r" in "(no answer"*) echo "$r" ;; *) echo "changing to $m" ;; esac
}
setmode() {
m=$1
[ -n "$m" ] || { printf 'mode: dm = deathmatch, gg = GunGame: '; read -r m; }
case "$m" in dm|gg) ./rcon esef_mode "$m" | grep -a esef_mode ;; *) echo "use dm or gg" ;; esac
}
setbots() {
n=$1
[ -n "$n" ] || { printf 'fill the server up to how many players with bots? (0 = no bots, now %s): ' "$(getenv CS_BOTS)"; read -r n; }
case "$n" in ''|*[!0-9]*) echo "give a number"; return ;; esac
setenv CS_BOTS "$n" # remembered for restarts
r=$(./rcon yb_quota "$n"); case "$r" in "(no answer"*) echo "saved in .env, applies when the server is back: $r" ;; *) echo "bots: up to $n players (saved in .env)" ;; esac
}
run() {
case "$1" in
status|1) status; players ;;
map|2) shift; setmap "$1" ;;
mode|3) shift; setmode "$1" ;;
bots|4) shift; setbots "$1" ;;
kills|5) python3 kills ;;
say|6) shift; t="$*"; [ -n "$t" ] || { printf 'message: '; read -r t; }; [ -n "$t" ] && ./rcon say "$t" > /dev/null && echo sent ;;
restart|7) docker restart "$NAME" > /dev/null && echo "restarted (back in ~10 s)" ;;
stop|8) docker compose stop && echo "stopped (./cs start to start again)" ;;
start|9) docker compose up -d ;;
logs|10) echo "live server output, Ctrl+C to leave"; docker logs -f --tail 30 "$NAME" 2>&1 | grep --line-buffered -v -E 'rcon_password|sv_password|Rcon:' ;;
console|11) ./rcon ;;
apply|12) docker compose up -d && echo "applied .env (server restarted if anything changed)" ;;
update|13) ./setup.sh --yes --update ;;
*) echo "unknown: $1 (try ./cs with no arguments)"; return 1 ;;
esac
}
if [ $# -gt 0 ]; then run "$@"; exit $?; fi
while true; do
echo
echo "== $(getenv CS_HOSTNAME) == (container $NAME, UDP $PORT)"
status
cat <<'EOF'
1) who's playing 8) stop server
2) change map 9) start server
3) game mode (DM / GG) 10) live log
4) bots 11) console (type server commands)
5) kill counter 12) apply .env changes
6) message to players 13) update game + mods
7) restart server 0) quit
EOF
printf '> '; read -r c || break
case "$c" in 0|q|quit|exit|'') break ;; esac
run "$c"
done
+138
View File
@@ -0,0 +1,138 @@
// GunGame AMXX 2.13c
// cs16-dm-server: FFA GunGame (gg_tk_penalty -1 + mp_freeforall 1 in gg_map_setup); objectives blocked; 30s knife warmup
// basic cvars
gg_enabled 1
gg_vote_setting 2
gg_vote_custom ""
gg_changelevel_custom ""
gg_map_setup "mp_timelimit 30; mp_winlimit 0; mp_chattime 10; mp_freeforall 1; mp_forcerespawn 0; mp_respawn_immunitytime 0; mp_friendlyfire 0"
gg_endmap_setup ""
gg_join_msg 1
gg_messages 1
gg_save_temp 300
gg_status_display 1
gg_map_iterations 1
gg_ignore_bots 0
gg_block_equips 2
gg_leader_display 1
gg_leader_display_x -1.0
gg_leader_display_y 0.0
gg_allow_changeteam 2
gg_disable_money 1
gg_winner_motd 1
gg_afk_protection 0
gg_top10_ppp 8
// autovote cvars
gg_autovote_mode 0
gg_autovote_rounds 1
gg_autovote_delay 8.0
gg_autovote_ratio 0.51
gg_autovote_time 10.0
// stats cvars
gg_stats_file "gungame.stats" // not used with SQL
gg_stats_streak_file "gungame.streaks" // not used with SQL
gg_stats_ip 0
gg_stats_prune 2592000
gg_stats_mode 2
gg_stats_split 0
gg_stats_winbonus 1.5
// SQL-only cvars
//gg_sql_host "127.0.0.1"
//gg_sql_user "root"
//gg_sql_pass ""
//gg_sql_db "amx"
//gg_sql_table "gg_stats"
//gg_sql_streak_table "gg_streaks"
//gg_sql_winmotd_table "gg_winmotd"
// deathmatch cvars
gg_dm 1
gg_dm_sp_time 1.0
gg_dm_sp_mode 1
gg_dm_spawn_random 2
gg_dm_start_random 1
gg_dm_spawn_delay 1.0
gg_dm_spawn_afterplant 1
gg_dm_corpses 1
gg_dm_countdown 2
// objective cvars
gg_block_objectives 1
gg_bomb_defuse_lvl 1
gg_host_touch_reward 2
gg_host_rescue_reward 2
gg_host_kill_reward 1
gg_host_kill_penalty 1
// teamplay cvars
gg_teamplay 0
gg_teamplay_knife_mod 0.33
gg_teamplay_nade_mod 0.50
gg_teamplay_timeratio 1
// gameplay cvars
gg_ff_auto 0
gg_weapon_order "glock18,usp,p228,deagle,fiveseven,elite,m3,xm1014,tmp,mac10,mp5navy,ump45,p90,galil,famas,ak47,scout,m4a1,sg552,aug,m249,hegrenade,knife"
gg_max_lvl 3
gg_triple_on 0
gg_turbo 1
gg_knife_pro 1
gg_knife_elite 0
gg_suicide_penalty 1
gg_worldspawn_suicide 1
gg_pickup_others 0
gg_handicap_on 1
gg_top10_handicap 1
gg_warmup_timer_setting 30
gg_warmup_weapon "knife"
gg_warmup_multi 0
gg_nade_glock 1
gg_nade_smoke 0
gg_nade_flash 0
gg_extra_nades 1
gg_nade_refresh 5.0
gg_kills_per_lvl 2
gg_kills_botmod 1.0
gg_give_armor 100
gg_give_helmet 1
gg_ammo_amount 200
gg_refill_on_kill 1
gg_tk_penalty -1
gg_awp_oneshot 1
gg_bots_skipnade 0
gg_bots_knifeable 1
// sound cvars
gg_sound_levelup "sound/gungame/gg_levelup.wav"
gg_sound_leveldown "sound/ambience/xtal_down1(e70)"
gg_sound_levelsteal "sound/turret/tu_die.wav"
gg_sound_nade "sound/gungame/gg_nade_level.wav"
gg_sound_knife "sound/gungame/gg_knife_level.wav"
gg_sound_welcome "sound/gungame/gg_welcome.wav"
gg_sound_triple "sound/gungame/gg_triple.wav"
gg_sound_winner "media/Half-Life03.mp3;media/Half-Life08.mp3;media/Half-Life11.mp3;media/Half-Life17.mp3"
gg_sound_takenlead "sound/gungame/gg_takenlead.wav"
gg_sound_tiedlead "sound/gungame/gg_tiedlead.wav"
gg_sound_lostlead "sound/gungame/gg_lostlead.wav"
gg_lead_sounds 0.9
// random weapon order cvars (leave blank to disable)
gg_weapon_order1 ""
gg_weapon_order2 ""
gg_weapon_order3 ""
gg_weapon_order4 ""
gg_weapon_order5 ""
gg_weapon_order6 ""
gg_weapon_order7 ""
gg_weapon_order8 ""
gg_weapon_order9 ""
gg_weapon_order10 ""
// valid weapon names for gg_weapon_order:
//
// ak47, aug, awp, deagle, elite, famas, fiveseven, g3sg1, galil, glock18, hegrenade, knife,
// m249, m3, m4a1, mac10, mp5navy, p228, p90, scout, sg550, sg552, tmp, ump45, usp, xm1014
@@ -0,0 +1,46 @@
;;;
; To enable a module, remove the semi-colon (;) in front of its name.
; If it's not here, simply add it its name, one per line.
; You don't need to write the _amxx part or the file extension.
;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SQL Modules usually need to be enabled manually ;;
;; You can have any number on at a time. Use ;;
;; amx_sql_type in sql.cfg to specify the default ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;mysql
;sqlite
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Put third party modules below here. ;;
;; You can just list their names, without the _amxx ;;
;; or file extension. ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; These modules will be auto-detected and loaded ;;
;; as needed. You do not need to enable them here ;;
;; unless you have problems. ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;fun
;engine
;fakemeta
;geoip
;sockets
;regex
;nvault
;cstrike
;csx
;hamsandwich
; cs16-dm-server: GunGame needs these three. Loaded at boot on purpose: if they first load on a
; GunGame map they miss the game's message setup and the server crashes when a player joins
; ("PF_MessageBegin_I: bogus message type"). Never add reapi here (AMXX autoloads it).
cstrike
fakemeta
hamsandwich
@@ -0,0 +1,2 @@
; GunGame (cs16-dm-server). Active only in gungame mode: esef_mode.amxx renames this file to .ini / .ini.off
gungame.amxx
@@ -0,0 +1,47 @@
; AMX Mod X plugins
; Admin Base - Always one has to be activated
admin.amxx ; admin base (required for any admin-related)
;admin_sql.amxx ; admin base - SQL version (comment admin.amxx)
; Basic
admincmd.amxx ; basic admin console commands
adminhelp.amxx ; help command for admin console commands
adminslots.amxx ; slot reservation
multilingual.amxx ; Multi-Lingual management
; Menus
menufront.amxx ; front-end for admin menus
cmdmenu.amxx ; command menu (speech, settings)
plmenu.amxx ; players menu (kick, ban, client cmds.)
;telemenu.amxx ; teleport menu (Fun Module required!)
mapsmenu.amxx ; maps menu (vote, changelevel)
pluginmenu.amxx ; Menus for commands/cvars organized by plugin
; Chat / Messages
adminchat.amxx ; console chat commands
antiflood.amxx ; prevent clients from chat-flooding the server
scrollmsg.amxx ; displays a scrolling message
imessage.amxx ; displays information messages
adminvote.amxx ; vote commands
; Map related
nextmap.amxx ; displays next map in mapcycle
mapchooser.amxx ; allows to vote for next map
rtv.amxx ; rock the vote (cs16-dm-server, src: plugins/rtv.sma)
esef_extras.amxx ; damage numbers, killstreaks, announcer, HP on kill (cs16-dm-server)
esef_mode.amxx ; esef_mode dm|gg via RCON (cs16-dm-server)
timeleft.amxx ; displays time left on map
; Configuration
pausecfg.amxx ; allows to pause and unpause some plugins
statscfg.amxx ; allows to manage stats plugins via menu and commands
; Counter-Strike
;restmenu.amxx ; restrict weapons menu
statsx.amxx ; stats on death or round end (CSX Module required!)
;miscstats.amxx ; bunch of events announcement for Counter-Strike
;stats_logging.amxx ; weapons stats logging (CSX Module required!)
; Custom - Add 3rd party plugins here
+3
View File
@@ -0,0 +1,3 @@
linux addons/reunion/reunion_mm_i386.so
linux addons/amxmodx/dlls/amxmodx_mm_i386.so
linux addons/yapb/bin/yapb.so
+30
View File
@@ -0,0 +1,30 @@
// cs16-dm-server: runs at server start and every map change.
// This file comes from the repo and is replaced by ./setup.sh. Put your own changes in
// custom.cfg (next to this file, never overwritten) and the basics in .env.
sv_lan 0 // 0 = internet; must stay 0 for VPN (Tailscale) and tunnel (playit) players too
sv_region 255
sv_contact ""
mp_timelimit 20
mp_friendlyfire 0
mp_autoteambalance 1
mp_limitteams 2
mp_footsteps 1
mp_flashlight 1
mp_logmessages 1 // kill lines in cstrike/logs (the ./kills counter reads them)
sv_allowdownload 1 // players download the announcer/GunGame sounds (~2 MB) on first join
sv_allowupload 1
sv_maxrate 100000
sv_minrate 20000
sv_maxupdaterate 102
sv_minupdaterate 30
sys_ticrate 300 // server fps; 300 is plenty for DM and keeps CPU use low
sv_alltalk 1
sv_voiceenable 1
sv_rehlds_maxclients_from_single_ip 16 // tunnel players (playit etc.) all arrive from one IP
log on
sv_rcon_banpenalty 1 // minutes. Wrong-RCON bans hit an IP; with a tunnel that is everyone
sv_rcon_condebug 0 // don't echo RCON commands (they contain the password) to the console
esef_hudinfo 1 // extras HUD line (hidden automatically in GunGame mode)
exec env.cfg // hostname from .env (written at every start)
exec secrets.cfg // RCON + join password (written at every start)
exec custom.cfg // your own settings, run last so they win
+10
View File
@@ -0,0 +1,10 @@
// Your own server settings. Runs after server.cfg at every map change and is never
// overwritten by ./setup.sh. Examples (remove the // to use one):
//
// mp_timelimit 30 // minutes per map
// sv_alltalk 0 // voice chat only with your team
// esef_kill_hp 10 // HP gained per kill (0 = off)
// esef_kill_hp_hs 20 // HP gained per headshot kill
// esef_damage 1 // damage numbers (0 = off)
// esef_sounds 1 // killstreak / announcer sounds (0 = off)
// sv_downloadurl "http://example.com/cstrike/" // fast map/sound downloads from a web server
+11
View File
@@ -0,0 +1,11 @@
de_dust2
de_inferno
de_nuke
de_train
cs_office
de_aztec
de_dust
cs_italy
de_cbble
de_prodigy
cs_assault
+31
View File
@@ -0,0 +1,31 @@
# CS 1.6 deathmatch server. Settings: .env (see .env.example). Setup: ./setup.sh
# Game: UDP ${CS_PORT} on all interfaces (network_mode host, so no port mapping needed).
services:
cs:
build: .
image: cs16-dm-server:local
container_name: ${CS_NAME:-cs16}
user: "${CS_UID:-1000}:${CS_GID:-1000}"
network_mode: host
restart: unless-stopped
init: true # tini as PID 1: if hlds crashes the container exits and Docker restarts it
ulimits:
core: 0 # a crash would otherwise write a ~170 MB core file
logging: # cap docker logs (console output grows forever)
driver: json-file
options:
max-size: "10m"
max-file: "3"
stdin_open: true
tty: true
env_file: .env
environment:
- HOME=/cs/steamcmd-home
volumes:
- ./hlds:/cs/hlds
- ./steamcmd-home:/cs/steamcmd-home
- ./secrets:/cs/secrets:ro
- ./tools:/cs/tools:ro
# start.sh writes .env values into the game configs, then runs hlds_run
# (-norestart: on a crash the container exits and Docker's restart policy takes over).
command: ["/bin/sh", "/cs/tools/start.sh"]
+91
View File
@@ -0,0 +1,91 @@
# Running the server
Start with `./cs`, the menu that covers day-to-day use. This page covers everything else. The paths below are relative to the repo folder.
## Admins (in-game admin menu)
1. While the person is on the server, run `./rcon status` and copy their ID from the `uniqueid` column (`STEAM_0:1:12345678`).
2. Add a line at the end of `hlds/cstrike/addons/amxmodx/configs/users.ini`:
```
"STEAM_0:1:12345678" "" "abcdefghijklmnopqrstu" "ce"
```
The flags `abcdefghijklmnopqrstu` mean all rights. `ce` means "match by SteamID, no password".
3. Run `./rcon amx_reloadadmins`, or wait for the next map.
4. In game the admin opens the console and types `amxmodmenu`. From there they can kick, ban (by SteamID), change the map, and start votes.
Don't ban by IP (`addip`, or `amx_banip`) when players come through a tunnel like playit. Everyone shares one IP there, so an IP ban kicks everybody.
Setup keeps `users.ini` when you re-run it.
## RCON from inside the game
The password is in `secrets/rcon-password.txt`. In the game console:
```
rcon_password <the password>
rcon changelevel de_nuke
```
`./rcon` on the server does the same without the password ever leaving the machine.
To change the password, put a new one in `secrets/rcon-password.txt`, then run `./cs restart`.
## Maps
The default mapcycle has 11 stock maps with deathmatch spawn points: de_dust2, de_inferno, de_nuke, de_train, cs_office, de_aztec, de_dust, cs_italy, de_cbble, de_prodigy and cs_assault. Every stock map works with `./cs map`.
To add a custom map:
1. Copy `mapname.bsp` into `hlds/cstrike/maps/`. If the map comes with extra files (`.wad`, `.res`, `sound/`, `models/`, `gfx/`), copy those into `hlds/cstrike/` with the same folder layout.
2. Add `mapname` as a line in `hlds/cstrike/mapcycle.txt`, which controls rotation and the rtv vote.
3. **Spawn points:** without a spawn file, deathmatch uses the map's normal team spawns. That works, but people spawn in two clumps. To add proper spawn points, give yourself admin (see above) and type `csdm_edit_spawns` in the console on that map. Save, and the file lands in `addons/amxmodx/configs/csdm/spawns/mapname.spawns.cfg`. For GunGame to use the same points, also run `ln -s spawns/mapname.spawns.cfg hlds/cstrike/addons/amxmodx/configs/csdm/mapname.spawns.cfg` from the repo folder.
4. **Bots** download navigation for well-known maps automatically. On unknown maps they learn as they go, and they're clumsy at first.
**Fast downloads:** the game's built-in download is slow (and slower through a tunnel). Put a copy of the custom maps and sounds on any web server, laid out like `cstrike/` (`maps/`, `sound/`, ...). Then add this to `hlds/cstrike/custom.cfg`:
```
sv_downloadurl "http://your.site/cstrike/"
```
## Changing settings
- `.env`: name, password, slots, port, first map, bots. Then run `./cs apply`.
- `hlds/cstrike/custom.cfg`: any server cvar. It applies on the next map (`./cs map <current map>` to apply now).
- `hlds/cstrike/addons/amxmodx/configs/csdm/config.ini`: deathmatch settings (respawn time, spawn protection, gun menu contents, `free_for_all = 0` for team DM). Setup keeps your copy.
- `hlds/cstrike/addons/amxmodx/configs/gungame.cfg`: GunGame (weapon order, kills per level, warmup). This file comes from the repo, so a setup re-run replaces it. To keep GunGame changes, put the `gg_...` lines in `custom.cfg` instead.
- Extras cvars, all of which also go in `custom.cfg`: `esef_kill_hp` (10), `esef_kill_hp_hs` (20), `esef_damage` (1), `esef_sounds` (1), `esef_hudinfo` (1). Set any of them to `0` to turn it off.
## Backups
`./scripts/install-cron.sh` runs `scripts/backup.sh` every night at 04:30. It writes `backups/cs16-YYYY-MM-DD.tar.gz` and keeps 14 days. The backup holds `.env`, secrets, `mapcycle.txt`, `custom.cfg`, the AMXX configs, stats, the GunGame stats and the kill tally. The game itself isn't included (setup downloads it again). The backups sit on the same disk as the server, so copy them somewhere else if that matters to you.
To restore one file, run this from the repo folder: `tar xzf backups/cs16-<date>.tar.gz <path inside>`.
The backup also deletes server logs older than 30 days. Before it does, it adds their kills to the counter, so the all-time total keeps counting.
## Watchdog
The same cron install runs `scripts/watchdog.sh` every 5 minutes. If the server stops answering for about 15 minutes while its container is "running", the watchdog restarts it and writes a line to `watchdog.log`. Crashes are handled by Docker itself (`restart: unless-stopped`). After a crash, `docker inspect -f '{{.RestartCount}}' cs16` counts how many restarts there have been.
## More than one server
Clone the repo again **into a folder with a different name**, for example `git clone <repo url> cs16-gg`. Docker Compose names a project after its folder, so two folders that are both called `cs16-dm-server` would control each other's container. Then give the new server its own settings before setup:
```sh
cd cs16-gg
cp .env.example .env # then edit: CS_PORT=27016, CS_NAME=cs16-gg (both must differ from the first server)
./setup.sh
./scripts/install-cron.sh
```
Every folder is independent: its own game files, configs, stats and cron lines.
## Logs and privacy
- `docker logs cs16` or `./cs logs` shows the live console. `./cs logs` hides password lines.
- `hlds/cstrike/logs/` has one file per map: kills, chat, connects. HLDS also writes every cvar into these files at each map change, **including the join password**. Don't paste them publicly.
## Uninstall
```sh
./scripts/install-cron.sh --remove
docker compose down
cd .. && rm -rf cs16-dm-server # everything lives in this folder
docker image rm cs16-dm-server:local
```
+85
View File
@@ -0,0 +1,85 @@
# How it works
## The stack
| Part | Version | Why |
|---|---|---|
| HLDS app 90, beta `steam_legacy` | (frozen branch) | Valve's dedicated server. ReHLDS needs engine build ≤ 8684, so the legacy branch is used |
| [ReHLDS](https://github.com/rehlds/ReHLDS) | 3.15.0.896 | drop-in engine with fixes and better performance; replaces `engine_i486.so`, `hlds_linux` etc. |
| [ReGameDLL_CS](https://github.com/rehlds/ReGameDLL_CS) | 5.30.0.814 | drop-in game logic (`cstrike/dlls/cs.so`) with `mp_freeforall`, `mp_forcerespawn` and more |
| [Metamod-R](https://github.com/rehlds/Metamod-R) | 1.3.0.149 | plugin loader; `liblist.gam` points the game at it |
| [AMX Mod X](https://www.amxmodx.org) | 1.10.0.5486 | scripting platform for the plugins |
| [ReAPI](https://github.com/rehlds/ReAPI) | 5.29.0.358 | AMXX module that gives plugins access to ReHLDS/ReGameDLL hooks |
| [ReUnion](https://github.com/rehlds/ReUnion) | 0.2.0.25 | lets non-Steam clients (protocol 47/48) join |
| [CSDM-ReAPI](https://github.com/wopox1337/CSDM-ReAPI) | 1.0.8 | deathmatch: respawn, gun menu, spawn points, spawn protection |
| [YaPB](https://github.com/yapb/yapb) | 4.4.957 | bots (Metamod plugin) |
| [GunGame](https://github.com/fysiks1/amxx-gungame_cstrike) | 2.13c | GunGame mode, compiled from source by setup |
`tools/versions.txt` pins every download by URL and sha256. If anything upstream changes a file, setup stops instead of installing it.
## Install flow (`setup.sh`)
1. Creates `.env` and asks the questions. Generates `secrets/rcon-password.txt` and `secrets/reunion-salt.txt`, which are kept forever. The salt derives non-Steam players' IDs, so a new salt would reset their stats and admin entries.
2. Builds the image: Debian bookworm, 32-bit libs, SteamCMD, unzip/xz.
3. Downloads the game with SteamCMD, anonymously, with up to 4 tries, because app 90 often fails the first time. It runs `validate` only on the very first download. Later, validate would put back Valve's version of files you edited, and it also silently removes ReHLDS and Metamod.
4. `tools/install-addons.sh` runs inside the image (repo mounted read-only, only `dl/` writable):
- downloads and checks the pinned files
- unpacks ReHLDS, then sets its exec bits, which the zip loses
- unpacks ReGameDLL and comments out the `sv_alltalk 0` that its `game.cfg` would force on every map
- unpacks Metamod and points `liblist.gam` at it (a copy of the original is kept as `liblist.gam.orig`)
- unpacks AMXX, ReAPI and ReUnion. ReUnion gets the salt and `IDClientsLimit 0`, so several non-Steam players with the same placeholder ID can all join.
- unpacks CSDM-ReAPI and the spawn files. de_prodigy and de_train are in CSDM 2.1's old 6-field format and get converted to the 10 fields CSDM-ReAPI reads, because it silently skips lines with any other count. Symlinks one folder up let GunGame find the files too.
- unpacks YaPB, with `chmod 775` on its folders (the tarball's folders have no x bit, so the bots couldn't save their path caches), `yb_csdm_mode 2` (FFA-aware) and `yb_quota_mode fill`
- copies the GunGame source, lang file and sounds, plus the 14 announcer sounds
- copies `cstrike/` from the repo over the game
- compiles `rtv`, `esef_extras`, `esef_mode` and `gungame` with AMXX's `amxxpc`
- leaves exactly one of `plugins-CSDM_ReAPI.ini` / `plugins-gungame.ini` active. It keeps the mode that was running before, or uses `CS_MODE` on the first install.
- symlinks `steamclient.so` into `~/.steam/sdk32`. Without it you get "Unable to initialize Steam" in Docker.
- saves and restores what people edit: `users.ini`, `amxx.cfg`, `csdm/config.ini`. `mapcycle.txt` and `custom.cfg` are only written on the first install.
5. Stops the old container if it's running (the engine files are in use), starts the server, and waits until it answers an A2S query.
## At every container start (`tools/start.sh`)
- writes `cstrike/env.cfg` (hostname) and `cstrike/secrets.cfg` (RCON + join password, mode 600) from `.env` and `secrets/`
- sets `yb_quota` / `yb_difficulty` in `addons/yapb/conf/yapb.cfg` from `CS_BOTS` / `CS_BOT_DIFFICULTY`
- runs `hlds_run -game cstrike -port $CS_PORT +maxplayers $CS_MAXPLAYERS +map $CS_MAP -pingboost 2 -norestart +log on`
`server.cfg` runs `env.cfg`, then `secrets.cfg`, then `custom.cfg`, on every map.
## Game modes
`esef_mode` (own plugin) is a server command: `esef_mode`, `esef_mode dm` or `esef_mode gg`. AMXX reads every `configs/plugins-*.ini` at each map change, so a mode is simply which plugin list is named `.ini` and which is named `.ini.off`. The plugin renames them, then reloads the map. The rename stays on disk, so the mode survives restarts.
GunGame settings (`configs/gungame.cfg`):
- free for all: `gg_tk_penalty -1`, plus `mp_freeforall 1` in `gg_map_setup`
- bomb and hostages blocked
- 30 s knife warmup, 1 s respawn, 30 min maps
- 2 kills per level
`gg_map_setup` also switches off the respawn cvars CSDM leaves behind (`mp_forcerespawn 0`, `mp_respawn_immunitytime 0`). CSDM sets them again in dm mode.
**Important:** GunGame needs the AMXX modules `cstrike`, `fakemeta` and `hamsandwich`. They're listed in `configs/modules.ini` so they load at server start. If they load later, on the first GunGame map, they miss the game's message registration and the server crashes when a player joins (`PF_MessageBegin_I: bogus message type ( 0 )`). Because of this, Metamod logs `Not loading plugin 'CStrike'; already loaded` on every map. Those errors are harmless. Never add `reapi` to `modules.ini`: AMXX autoloads it, and listing it loads it twice.
## Own plugins (`plugins/`)
- **esef_extras:** damage numbers (DHUD; blue = dealt, red = taken, real HP lost), killstreak announcements at 3/5/7/9/11/13/15 (to everyone), multikills within 2 s (to the killer), headshot / knife / first blood sounds, +HP per kill (capped at 100), and a "K D | streak (best)" HUD line that hides itself in GunGame.
- **rtv:** a minimal rock-the-vote. 60% of players (`amx_rtv_ratio`) starts a 15 s vote between 5 random mapcycle maps. It's blocked in the first 2 minutes (`amx_rtv_delay`) and in the last 3 minutes, when mapchooser's end-of-map vote runs.
- **esef_mode:** the mode switch described above.
## Server settings worth knowing (`cstrike/server.cfg`)
- `sv_lan 0`: has to stay 0 even for VPN or tunnel players, because they aren't "LAN" to the engine.
- `sv_rehlds_maxclients_from_single_ip 16`: a tunnel like playit delivers every player from one IP.
- `sv_rcon_banpenalty 1`: a wrong RCON password bans the source IP for 1 minute instead of forever, which matters when everyone shares the tunnel's IP.
- `sv_rcon_condebug 0`: RCON commands contain the password, so they aren't echoed to the console or `docker logs`.
- `sys_ticrate 300`: plenty for DM, and it keeps CPU use low.
## Crash handling
- **Crash:** the container runs with `init: true`, so tini is PID 1, and hlds runs with `-norestart`. When hlds dies the container exits and Docker's `restart: unless-stopped` starts it again. Without `-norestart`, hlds_run's own restart loop can hang after a crash, with the container still showing "running". Core dumps are off (`ulimits: core: 0`), because a single crash writes ~170 MB.
- **Hang:** `scripts/watchdog.sh` runs from cron and A2S-queries the server. After 3 failed checks in a row it runs `docker restart`.
- **Logs:** Docker's json log is capped at 3 × 10 MB. `scripts/backup.sh` deletes game logs older than 30 days, after counting their kills.
## Kill counter
`./kills` counts the `"X<..>" killed "Y<..>" with "weapon"` lines in `hlds/cstrike/logs/*.log`, keeping the counts per log file in `kills-tally.json`. Because the counts live in that file, the totals survive the 30-day log cleanup.
+48
View File
@@ -0,0 +1,48 @@
# Joining the server
Send this page to your players along with the server address (for example `1.2.3.4:27015` or a playit address like `something.ply.gg:12345`).
## Steam version
1. Install **Counter-Strike** (1.6) from Steam.
2. Turn on the console: **Options → Keyboard → Advanced → Enable developer console**.
3. In the main menu press **~** (the key under Esc) and type:
```
connect 1.2.3.4:27015
```
4. To find it again later: **Find Servers → Favorites → Add a server**, then enter the same address.
The first time you join, the game downloads ~2 MB of sounds. After that, joining is instant.
## Non-Steam clients
Non-Steam builds that speak protocol 47 or 48 can join (the server runs ReUnion). Use the same `connect` command in the console.
## In-game
| Type in chat | What it does |
|---|---|
| `guns` | reopen the deathmatch gun menu |
| `rtv` | vote to change the map (needs 60% of players; not in the first 2 or the last 3 minutes) |
| `/rank`, `/top15`, `/stats` | your rank, top 15, your stats |
| `!rules`, `!level`, `!score`, `!top`, `!weapons` | GunGame info (GunGame mode only) |
How the game works here:
- **Deathmatch:** free for all, so shoot anyone. You respawn in under a second with 2 seconds of spawn protection, and pick your guns from the menu.
- **Every kill gives you HP:** +10, or +20 for a headshot, up to 100.
- **Numbers on screen:** blue numbers are damage you dealt, red numbers are damage you took.
- **Announcer:** killstreaks at 3, 5, 7… kills are announced to everyone. Fast double, multi, ultra and monster kills are announced to you.
- **GunGame mode:** every 2 kills you move to the next weapon, from glock up to the knife. The first player to finish the knife level wins the map.
- **Bots:** they leave the server as people join.
## Tips for smoother play
Put these in the console. They're the usual settings for 1.6 online:
```
rate 100000
cl_updaterate 102
cl_cmdrate 105
ex_interp 0.01
fps_max 100
```
Executable
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""All-time kill counter (bots and humans).
./kills show the scoreboard (and save the tally)
Counts the " killed " lines in hlds/cstrike/logs. Per-log counts are kept in kills-tally.json,
so the totals survive scripts/backup.sh deleting logs older than 30 days (it runs this first)."""
import collections, json, os, re
D = os.path.dirname(os.path.abspath(__file__))
LOGS = os.path.join(D, "hlds", "cstrike", "logs")
STATE = os.path.join(D, "kills-tally.json")
KILL = re.compile(r'^L (\d\d/\d\d/\d{4}) - [\d:]+: "(.+?)<\d+><[^>]*><[^>]*>" killed '
r'".+?<\d+><[^>]*><[^>]*>" with "([^"]+)"')
try:
st = json.load(open(STATE))
except (OSError, ValueError):
st = {"files": {}}
for f in sorted(os.listdir(LOGS)) if os.path.isdir(LOGS) else []:
if not f.endswith(".log"):
continue
n, who, wpn, days = 0, collections.Counter(), collections.Counter(), set()
for line in open(os.path.join(LOGS, f), encoding="utf-8", errors="replace"):
m = KILL.match(line)
if m:
n += 1; days.add(m[1]); who[m[2]] += 1; wpn[m[3]] += 1
if n or f not in st["files"]:
st["files"][f] = {"kills": n, "who": who, "wpn": wpn, "days": sorted(days)}
tmp = STATE + ".tmp"
json.dump(st, open(tmp, "w"))
os.replace(tmp, STATE)
total, who, wpn, days = 0, collections.Counter(), collections.Counter(), set()
for v in st["files"].values():
total += v["kills"]; who.update(v["who"]); wpn.update(v["wpn"]); days.update(v["days"])
print(f"\n ALL-TIME KILLS: {total:,} ({len(days)} days of carnage, {len(st['files'])} maps played)\n")
print(" top fraggers:")
for i, (n, k) in enumerate(who.most_common(5), 1):
print(f" {i}. {n:<18} {k:,}")
print("\n weapons of choice:")
for n, k in wpn.most_common(5):
print(f" {n:<18} {k:,}")
print()
+233
View File
@@ -0,0 +1,233 @@
// esef_extras.sma - game-feel extras for cs16-dm-server.
// - damage numbers: health you took off (blue, right of crosshair) / lost (red, left)
// - killstreak + multikill announcements (DHUD) with Quake/UT announcer sounds
// - headshot / knife / first blood sounds
// - HP on kill (esef_kill_hp, esef_kill_hp_hs), capped at 100
// - small HUD line: kills, deaths, streak, best streak (esef_hudinfo; hidden while GunGame runs)
// Sounds live in cstrike/sound/misc and are precached, so clients download them once (~800 KB).
#include <amxmodx>
#include <reapi>
#define MULTIKILL_WINDOW 2.0
#define TASK_HUD 4100
new const SND_HEADSHOT[] = "misc/headshot.wav"
new const SND_FIRSTBLOOD[] = "misc/firstblood.wav"
new const SND_KNIFE[] = "misc/humiliation.wav"
// multikill chain: 2, 3, 4, 5+ kills inside MULTIKILL_WINDOW
new const SND_MULTI[][] = { "misc/doublekill.wav", "misc/multikill.wav", "misc/ultrakill.wav", "misc/monsterkill.wav" }
new const TXT_MULTI[][] = { "DOUBLE KILL", "MULTI KILL", "ULTRA KILL", "MONSTER KILL" }
// streaks without dying
new const STREAK_AT[] = { 3, 5, 7, 9, 11, 13, 15 }
new const SND_STREAK[][] = {
"misc/killingspree.wav", "misc/rampage.wav", "misc/dominating.wav", "misc/unstoppable.wav",
"misc/godlike.wav", "misc/wickedsick.wav", "misc/holyshit.wav"
}
new const TXT_STREAK[][] = {
"is on a KILLING SPREE", "is on a RAMPAGE", "is DOMINATING", "is UNSTOPPABLE",
"is GODLIKE", "is WICKED SICK", "HOLY SHIT"
}
new g_streak[MAX_PLAYERS + 1], g_best[MAX_PLAYERS + 1], g_kills[MAX_PLAYERS + 1], g_deaths[MAX_PLAYERS + 1]
new g_chain[MAX_PLAYERS + 1], Float:g_lastKill[MAX_PLAYERS + 1]
new Float:g_preHealth[MAX_PLAYERS + 1]
new bool:g_firstBlood
new g_syncDealt, g_syncTaken, g_syncInfo
new g_pKillHp, g_pKillHpHs, g_pHudInfo, g_pSounds, g_pDamage
new bool:g_gunGame // GunGame shows its own level/kills HUD, so skip ours
public plugin_precache()
{
precache_sound(SND_HEADSHOT)
precache_sound(SND_FIRSTBLOOD)
precache_sound(SND_KNIFE)
for (new i = 0; i < sizeof SND_MULTI; i++)
precache_sound(SND_MULTI[i])
for (new i = 0; i < sizeof SND_STREAK; i++)
precache_sound(SND_STREAK[i])
}
public plugin_init()
{
register_plugin("DM extras", "1.0", "cs16-dm-server")
g_pKillHp = register_cvar("esef_kill_hp", "10")
g_pKillHpHs = register_cvar("esef_kill_hp_hs", "20")
g_pHudInfo = register_cvar("esef_hudinfo", "1")
g_pSounds = register_cvar("esef_sounds", "1")
g_pDamage = register_cvar("esef_damage", "1")
RegisterHookChain(RG_CBasePlayer_TakeDamage, "TakeDamage_Pre", .post = false)
RegisterHookChain(RG_CBasePlayer_TakeDamage, "TakeDamage_Post", .post = true)
RegisterHookChain(RG_CBasePlayer_Killed, "Killed_Post", .post = true)
g_syncDealt = CreateHudSyncObj()
g_syncTaken = CreateHudSyncObj()
g_syncInfo = CreateHudSyncObj()
set_task(1.0, "ShowInfoAll", TASK_HUD, .flags = "b")
}
public plugin_cfg()
{
g_gunGame = (find_plugin_byfile("gungame.amxx") != INVALID_PLUGIN_ID)
}
public client_putinserver(id)
{
g_streak[id] = g_best[id] = g_kills[id] = g_deaths[id] = g_chain[id] = 0
g_lastKill[id] = 0.0
}
// --- damage numbers -------------------------------------------------------
public TakeDamage_Pre(victim, inflictor, attacker, Float:damage, damagebits)
{
g_preHealth[victim] = Float:get_entvar(victim, var_health)
return HC_CONTINUE
}
public TakeDamage_Post(victim, inflictor, attacker, Float:damage, damagebits)
{
if (!get_pcvar_num(g_pDamage))
return HC_CONTINUE
new Float:now = Float:get_entvar(victim, var_health)
if (now < 0.0)
now = 0.0
new lost = floatround(g_preHealth[victim] - now)
if (lost <= 0)
return HC_CONTINUE
if (is_user_connected(victim) && !is_user_bot(victim))
{
set_hudmessage(220, 40, 40, 0.44, 0.52, 0, 0.0, 1.0, 0.0, 0.3, -1)
ShowSyncHudMsg(victim, g_syncTaken, "%d", lost)
}
if (attacker != victim && is_user_connected(attacker) && !is_user_bot(attacker))
{
set_hudmessage(40, 160, 255, 0.54, 0.47, 0, 0.0, 1.0, 0.0, 0.3, -1)
ShowSyncHudMsg(attacker, g_syncDealt, "%d", lost)
}
return HC_CONTINUE
}
// --- kills -----------------------------------------------------------------
public Killed_Post(victim, killer, gib)
{
if (is_user_connected(victim))
{
g_deaths[victim]++
g_streak[victim] = 0
g_chain[victim] = 0
ShowInfo(victim)
}
if (killer == victim || !is_user_connected(killer))
return HC_CONTINUE
g_kills[killer]++
g_streak[killer]++
if (g_streak[killer] > g_best[killer])
g_best[killer] = g_streak[killer]
new Float:t = get_gametime()
g_chain[killer] = (t - g_lastKill[killer] <= MULTIKILL_WINDOW) ? g_chain[killer] + 1 : 1
g_lastKill[killer] = t
new bool:headshot = bool:get_member(victim, m_bHeadshotKilled)
new bool:knife = (get_user_weapon(killer) == CSW_KNIFE && get_entvar(victim, var_dmg_inflictor) == killer)
// HP on kill
if (is_user_alive(killer))
{
new bonus = headshot ? get_pcvar_num(g_pKillHpHs) : get_pcvar_num(g_pKillHp)
if (bonus > 0)
{
new Float:hp = Float:get_entvar(killer, var_health) + float(bonus)
set_entvar(killer, var_health, hp > 100.0 ? 100.0 : hp)
}
}
new name[32]
get_user_name(killer, name, charsmax(name))
// pick one sound per kill: streak (everyone) > multikill (killer) > knife (everyone) > headshot (killer) > first blood
new streakIdx = -1
for (new i = 0; i < sizeof STREAK_AT; i++)
if (g_streak[killer] == STREAK_AT[i])
streakIdx = i
if (streakIdx >= 0)
{
set_dhudmessage(255, 160, 0, -1.0, 0.25, 0, 0.0, 2.5, 0.1, 0.4)
show_dhudmessage(0, "%s %s! (%d)", name, TXT_STREAK[streakIdx], g_streak[killer])
PlaySound(0, SND_STREAK[streakIdx])
}
else if (g_chain[killer] >= 2)
{
new m = g_chain[killer] - 2
if (m >= sizeof TXT_MULTI)
m = sizeof TXT_MULTI - 1
set_dhudmessage(255, 60, 60, -1.0, 0.30, 0, 0.0, 1.5, 0.1, 0.3)
show_dhudmessage(killer, TXT_MULTI[m])
PlaySound(killer, SND_MULTI[m])
}
else if (knife)
{
new vname[32]
get_user_name(victim, vname, charsmax(vname))
set_dhudmessage(255, 255, 255, -1.0, 0.25, 0, 0.0, 2.0, 0.1, 0.3)
show_dhudmessage(0, "%s knifed %s!", name, vname)
PlaySound(0, SND_KNIFE)
}
else if (headshot)
{
PlaySound(killer, SND_HEADSHOT)
}
else if (!g_firstBlood)
{
set_dhudmessage(255, 0, 0, -1.0, 0.25, 0, 0.0, 2.0, 0.1, 0.3)
show_dhudmessage(0, "%s drew FIRST BLOOD", name)
PlaySound(0, SND_FIRSTBLOOD)
}
g_firstBlood = true
ShowInfo(killer)
return HC_CONTINUE
}
PlaySound(id, const snd[])
{
if (!get_pcvar_num(g_pSounds))
return
// spk wants the path without "sound/" and without extension
new path[64]
copy(path, charsmax(path), snd)
replace(path, charsmax(path), ".wav", "")
client_cmd(id, "spk ^"%s^"", path)
}
// --- HUD info line ----------------------------------------------------------
public ShowInfoAll()
{
if (g_gunGame || !get_pcvar_num(g_pHudInfo))
return
new players[MAX_PLAYERS], num
get_players(players, num, "ach") // alive, no bots, no HLTV
for (new i = 0; i < num; i++)
ShowInfo(players[i])
}
ShowInfo(id)
{
if (g_gunGame || !get_pcvar_num(g_pHudInfo) || !is_user_connected(id) || is_user_bot(id))
return
set_hudmessage(200, 200, 200, 0.01, 0.88, 0, 0.0, 1.2, 0.0, 0.0, -1)
ShowSyncHudMsg(id, g_syncInfo, "K %d D %d | streak %d (best %d)", g_kills[id], g_deaths[id], g_streak[id], g_best[id])
}
+84
View File
@@ -0,0 +1,84 @@
// esef_mode.sma - switch the server between FFA deathmatch (CSDM-ReAPI) and GunGame.
// Server command (use via RCON): esef_mode -> prints the current mode
// esef_mode gg | dm -> switches and reloads the current map
// AMXX loads every configs/plugins-*.ini at each map change, so a mode is just which of the
// two plugin lists is named .ini and which .ini.off. The choice survives restarts.
#include <amxmodx>
#include <amxmisc>
new const GG_INI[] = "plugins-gungame.ini"
new const DM_INI[] = "plugins-CSDM_ReAPI.ini"
new g_dir[64]
public plugin_init()
{
register_plugin("DM/GG mode switch", "1.0", "cs16-dm-server")
register_srvcmd("esef_mode", "CmdMode", -1, "[dm|gg] - show or switch game mode (reloads the map)")
get_configsdir(g_dir, charsmax(g_dir))
}
bool:IsOn(const ini[])
{
new path[128]
formatex(path, charsmax(path), "%s/%s", g_dir, ini)
return bool:file_exists(path)
}
// enable: <ini>.off -> <ini> disable: <ini> -> <ini>.off
SetIni(const ini[], bool:enable)
{
new on[128], off[128]
formatex(on, charsmax(on), "%s/%s", g_dir, ini)
formatex(off, charsmax(off), "%s/%s.off", g_dir, ini)
if (enable && !file_exists(on) && file_exists(off))
rename_file(off, on, 1)
else if (!enable && file_exists(on))
rename_file(on, off, 1)
}
public CmdMode()
{
new arg[8]
read_argv(1, arg, charsmax(arg))
new bool:gg = IsOn(GG_INI)
if (!arg[0])
{
server_print("[esef_mode] current mode: %s", gg ? "gg (GunGame)" : "dm (FFA deathmatch)")
return PLUGIN_HANDLED
}
new bool:wantGG
if (equali(arg, "gg"))
wantGG = true
else if (equali(arg, "dm"))
wantGG = false
else
{
server_print("[esef_mode] usage: esef_mode [dm|gg]")
return PLUGIN_HANDLED
}
SetIni(GG_INI, wantGG)
SetIni(DM_INI, !wantGG)
if (IsOn(GG_INI) != wantGG || IsOn(DM_INI) == wantGG)
{
server_print("[esef_mode] could not rename the plugin lists in %s", g_dir)
return PLUGIN_HANDLED
}
server_print("[esef_mode] switching to %s, reloading map in 3s", wantGG ? "gg" : "dm")
client_print_color(0, print_team_default, "^4[mode]^1 Switching to ^3%s^1 - map reloads in 3 seconds...", wantGG ? "GunGame" : "Deathmatch")
set_task(3.0, "Reload")
return PLUGIN_HANDLED
}
public Reload()
{
new map[32]
get_mapname(map, charsmax(map))
server_cmd("changelevel %s", map)
}
+197
View File
@@ -0,0 +1,197 @@
// rtv.sma - minimal "rock the vote" for cs16-dm-server.
// Players type rtv (or /rtv, rockthevote). When amx_rtv_ratio of the players
// agree, a 15s vote between up to 5 random mapcycle maps starts, then the map changes.
// Blocked early in a map (amx_rtv_delay) and near map end, where mapchooser's own vote runs.
#include <amxmodx>
#define VOTE_MAPS 5
#define VOTE_TIME 15
#define MENU_KEYS (MENU_KEY_1|MENU_KEY_2|MENU_KEY_3|MENU_KEY_4|MENU_KEY_5)
new bool:g_rocked[MAX_PLAYERS + 1]
new g_rocks
new bool:g_voting, bool:g_finished
new g_choices[VOTE_MAPS][32], g_numChoices, g_count[VOTE_MAPS]
new Float:g_mapStart
new g_pRatio, g_pDelay
public plugin_init()
{
register_plugin("RTV", "1.0", "cs16-dm-server")
register_clcmd("say rtv", "CmdRtv")
register_clcmd("say /rtv", "CmdRtv")
register_clcmd("say rockthevote", "CmdRtv")
register_clcmd("say_team rtv", "CmdRtv")
register_clcmd("say_team /rtv", "CmdRtv")
g_pRatio = register_cvar("amx_rtv_ratio", "0.6")
g_pDelay = register_cvar("amx_rtv_delay", "120")
register_menucmd(register_menuid("RTV Vote"), MENU_KEYS, "VoteHandler")
g_mapStart = get_gametime()
}
CountPlayers()
{
new players[MAX_PLAYERS], num
get_players(players, num, "ch")
return num
}
RocksNeeded()
{
new need = floatround(float(CountPlayers()) * get_pcvar_float(g_pRatio), floatround_ceil)
return need < 1 ? 1 : need
}
public CmdRtv(id)
{
if (g_voting || g_finished)
{
client_print_color(id, print_team_default, "^4[RTV]^1 A map vote already ran on this map.")
return PLUGIN_CONTINUE
}
new wait = get_pcvar_num(g_pDelay) - floatround(get_gametime() - g_mapStart)
if (wait > 0)
{
client_print_color(id, print_team_default, "^4[RTV]^1 Too early, try again in ^3%d^1 seconds.", wait)
return PLUGIN_CONTINUE
}
if (get_cvar_float("mp_timelimit") > 0.0 && get_timeleft() < 180)
{
client_print_color(id, print_team_default, "^4[RTV]^1 The end-of-map vote is about to start.")
return PLUGIN_CONTINUE
}
if (g_rocked[id])
{
client_print_color(id, print_team_default, "^4[RTV]^1 You already voted (^3%d^1/^3%d^1).", g_rocks, RocksNeeded())
return PLUGIN_CONTINUE
}
g_rocked[id] = true
g_rocks++
new name[32]
get_user_name(id, name, charsmax(name))
client_print_color(0, id, "^4[RTV]^3 %s^1 wants to change the map (^3%d^1/^3%d^1). Type ^4rtv^1 to agree.", name, g_rocks, RocksNeeded())
CheckRocks()
return PLUGIN_CONTINUE
}
public client_disconnected(id)
{
if (g_rocked[id])
{
g_rocked[id] = false
g_rocks--
}
// fewer players can mean the remaining votes are now enough
if (g_rocks > 0)
set_task(0.1, "CheckRocks")
}
public CheckRocks()
{
if (!g_voting && !g_finished && g_rocks > 0 && g_rocks >= RocksNeeded())
StartVote()
}
StartVote()
{
g_voting = true
new current[32], cyclefile[64]
get_mapname(current, charsmax(current))
get_cvar_string("mapcyclefile", cyclefile, charsmax(cyclefile))
new Array:maps = ArrayCreate(32)
new fp = fopen(cyclefile, "rt")
if (fp)
{
new line[64]
while (!feof(fp))
{
fgets(fp, line, charsmax(line))
trim(line)
if (!line[0] || line[0] == ';' || (line[0] == '/' && line[1] == '/'))
continue
if (equali(line, current) || !is_map_valid(line))
continue
ArrayPushString(maps, line)
}
fclose(fp)
}
g_numChoices = 0
while (g_numChoices < VOTE_MAPS && ArraySize(maps) > 0)
{
new pick = random(ArraySize(maps))
ArrayGetString(maps, pick, g_choices[g_numChoices], charsmax(g_choices[]))
ArrayDeleteItem(maps, pick)
g_count[g_numChoices] = 0
g_numChoices++
}
ArrayDestroy(maps)
if (!g_numChoices)
{
g_voting = false
client_print_color(0, print_team_default, "^4[RTV]^1 No other maps in the mapcycle.")
return
}
new menu[512], len, keys
len = formatex(menu, charsmax(menu), "\yRock the vote - pick the next map:^n^n")
for (new i = 0; i < g_numChoices; i++)
{
len += formatex(menu[len], charsmax(menu) - len, "\r%d.\w %s^n", i + 1, g_choices[i])
keys |= (1 << i)
}
show_menu(0, keys, menu, VOTE_TIME, "RTV Vote")
client_print_color(0, print_team_default, "^4[RTV]^1 Vote started! You have ^3%d^1 seconds.", VOTE_TIME)
set_task(float(VOTE_TIME) + 1.0, "FinishVote")
}
public VoteHandler(id, key)
{
if (!g_voting || key < 0 || key >= g_numChoices)
return PLUGIN_HANDLED
g_count[key]++
new name[32]
get_user_name(id, name, charsmax(name))
client_print_color(0, id, "^4[RTV]^3 %s^1 chose ^4%s", name, g_choices[key])
return PLUGIN_HANDLED
}
public FinishVote()
{
g_voting = false
g_finished = true
new best = 0
for (new i = 1; i < g_numChoices; i++)
{
if (g_count[i] > g_count[best])
best = i
}
if (!g_count[best])
best = random(g_numChoices)
set_cvar_string("amx_nextmap", g_choices[best])
client_print_color(0, print_team_default, "^4[RTV]^1 Next map: ^4%s^1 (^3%d^1 votes). Changing in 5 seconds...", g_choices[best], g_count[best])
set_task(5.0, "ChangeMap", best)
}
public ChangeMap(best)
{
server_cmd("changelevel %s", g_choices[best])
}
Executable
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Server console from the terminal (the RCON password never leaves this machine).
./rcon status one command
./rcon interactive prompt (Ctrl+D or 'exit' to quit)
Reads the port from .env and the password from secrets/rcon-password.txt."""
import os, socket, sys
D = os.path.dirname(os.path.abspath(__file__))
env = {}
for line in open(os.path.join(D, ".env"), encoding="utf-8"):
k, _, v = line.strip().partition("=")
env[k] = v
PW = open(os.path.join(D, "secrets", "rcon-password.txt")).read().strip()
ADDR = ("127.0.0.1", int(env.get("CS_PORT") or 27015))
H = b"\xff\xff\xff\xff"
def rcon(cmd):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(3)
try:
s.sendto(H + b"challenge rcon\n", ADDR)
ch = s.recvfrom(4096)[0].split()[2]
except (OSError, IndexError):
return "(no answer: server down or changing map?)"
s.sendto(H + b"rcon " + ch + b' "' + PW.encode() + b'" ' + cmd.encode(), ADDR)
out = []
while True: # long replies (status, cvarlist) come in several packets
try:
data = s.recvfrom(65535)[0]
except OSError:
break
out.append(data[5:].decode(errors="replace") if data[:4] == H else data.decode(errors="replace"))
s.settimeout(0.4)
text = "".join(out)
# never show passwords (a map change prints the whole cvar list)
return "\n".join(l for l in text.splitlines()
if "rcon_password" not in l and "sv_password" not in l) or "(ok, no output)"
if __name__ == "__main__":
if len(sys.argv) > 1:
print(rcon(" ".join(sys.argv[1:])))
sys.exit()
print("CS 1.6 console: status, changelevel de_nuke, yb_quota 6, esef_mode gg, say hi. Ctrl+D to quit.")
while True:
try:
line = input("cs16> ").strip()
except (EOFError, KeyboardInterrupt):
print()
break
if line in ("exit", "quit"):
break
if line:
print(rcon(line))
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
# Nightly backup (cron 04:30, see install-cron.sh): settings, secrets, configs, stats and the
# kill tally into backups/cs16-YYYY-MM-DD.tar.gz, kept 14 days. Game files are not included
# (./setup.sh downloads them again). Also saves the kill tally and prunes logs older than 30 days.
. "$(dirname "$0")/env.sh"
set -e
cd "$D"
umask 077
mkdir -p backups
C=hlds/cstrike
# keep only paths that exist (a fresh install has no stats yet)
LIST=
for p in .env secrets kills-tally.json $C/mapcycle.txt $C/custom.cfg $C/reunion.cfg \
$C/addons/amxmodx/configs $C/addons/amxmodx/data $C/gungame.stats $C/gungame.streaks \
$C/addons/yapb/conf; do
[ -e "$p" ] && LIST="$LIST $p"
done
tar czf "backups/cs16-$(date +%F).tar.gz" $LIST
find backups -name "cs16-*.tar.gz" -mtime +14 -delete
# count the kills in the logs before old logs go away
python3 kills > /dev/null || true
find $C/logs $C/addons/amxmodx/logs -name "*.log" -mtime +30 -delete 2>/dev/null || true
echo "$(date '+%F %T') backup ok"
Executable
+5
View File
@@ -0,0 +1,5 @@
# Sourced by the scripts: D = repo folder, NAME = container, PORT = game port (from .env).
D=$(cd "$(dirname "$0")/.." && pwd)
getenv() { sed -n "s/^$1=//p" "$D/.env" 2>/dev/null | tr -d '\r'; }
NAME=$(getenv CS_NAME); NAME=${NAME:-cs16}
PORT=$(getenv CS_PORT); PORT=${PORT:-27015}
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# Adds the watchdog (every 5 min) and the backup (04:30) to your crontab.
# Safe to run again: replaces its own lines (tagged with this folder's path), leaves others alone.
# ./scripts/install-cron.sh --remove takes them out again.
D=$(cd "$(dirname "$0")/.." && pwd)
TAG="# cs16-dm-server $D"
OLD=$(crontab -l 2>/dev/null | grep -vF "$TAG")
if [ "$1" = "--remove" ]; then
printf '%s\n' "$OLD" | crontab - && echo "removed cron lines for $D"; exit 0
fi
{
[ -n "$OLD" ] && printf '%s\n' "$OLD"
echo "*/5 * * * * $D/scripts/watchdog.sh $TAG"
echo "30 4 * * * $D/scripts/backup.sh >> $D/backups/backup.log 2>&1 $TAG"
} | crontab -
echo "cron installed for $D:"; crontab -l | grep -F "$TAG"
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
# Hang watchdog (cron every 5 min, see install-cron.sh). Docker already restarts the server when
# it crashes; this covers the other case: container "running" but the server not answering.
# Restarts it after 3 failed checks in a row (~15 min). Skips a container stopped on purpose.
. "$(dirname "$0")/env.sh"
cd "$D"
STATE=.watchdog-fails
LOG=watchdog.log
[ "$(docker inspect -f '{{.State.Status}}' "$NAME" 2>/dev/null)" = running ] || { rm -f $STATE; exit 0; }
if python3 tools/hlq.py info 127.0.0.1 "$PORT" >/dev/null 2>&1 || \
{ sleep 5; python3 tools/hlq.py info 127.0.0.1 "$PORT" >/dev/null 2>&1; }; then
rm -f $STATE
else
n=$(( $(cat $STATE 2>/dev/null || echo 0) + 1 ))
echo $n > $STATE
echo "$(date '+%F %T') no answer ($n/3)" >> $LOG
if [ $n -ge 3 ]; then
echo "$(date '+%F %T') restarting $NAME" >> $LOG
docker restart "$NAME" >/dev/null 2>&1
rm -f $STATE
fi
fi
Executable
+131
View File
@@ -0,0 +1,131 @@
#!/bin/sh
# cs16-dm-server setup: run from the repo folder on a Linux machine with Docker.
# ./setup.sh first install (asks a few questions), or re-run safely any time
# ./setup.sh --yes no questions: use .env as it is (or the defaults)
# ./setup.sh --update also update the game files from Steam (then re-installs the mods)
# Steps: check tools -> .env -> secrets -> build image -> download CS 1.6 (SteamCMD, ~600 MB)
# -> install the mod stack (pinned downloads) -> start -> check the server answers.
set -e
cd "$(dirname "$0")"
YES=; UPDATE=
for a in "$@"; do
case "$a" in
--yes|-y) YES=1 ;;
--update) UPDATE=1 ;;
*) echo "unknown option: $a"; exit 1 ;;
esac
done
say() { printf '\n\033[1m== %s\033[0m\n' "$*"; }
die() { printf '\033[31merror:\033[0m %s\n' "$*" >&2; exit 1; }
setenv() { # setenv KEY VALUE: replace the KEY= line in .env
v=$(printf '%s' "$2" | tr -d '"' | sed 's/[\\&|]/\\&/g')
sed -i "s|^$1=.*|$1=$v|" .env
}
getenv() { sed -n "s/^$1=//p" .env | tr -d '\r'; }
ask() { # ask "question" default -> answer (default when --yes or no terminal)
if [ -n "$YES" ] || [ ! -t 0 ]; then printf '%s' "$2"; return; fi
printf '%s [%s]: ' "$1" "$2" >&2; read -r r; printf '%s' "${r:-$2}"
}
randpw() { python3 -c 'import secrets,string; print("".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(32)))'; }
say "Checking tools"
[ "$(id -u)" != 0 ] || die "don't run this as root: use a normal user in the 'docker' group"
command -v docker >/dev/null || die "docker is not installed (https://docs.docker.com/engine/install/)"
docker info >/dev/null 2>&1 || die "can't talk to docker: add yourself to the docker group (sudo usermod -aG docker \$USER, then log in again)"
docker compose version >/dev/null 2>&1 || die "'docker compose' (v2 plugin) is missing"
command -v python3 >/dev/null || die "python3 is missing (used by ./rcon, ./kills and the health checks)"
echo "ok"
say "Settings (.env)"
if [ ! -f .env ]; then
cp .env.example .env
setenv CS_HOSTNAME "$(ask 'Server name' "$(getenv CS_HOSTNAME)")"
setenv CS_PASSWORD "$(ask 'Join password (empty = open server)' '')"
setenv CS_PORT "$(ask 'Game port (UDP)' "$(getenv CS_PORT)")"
m=$(ask 'Game mode: dm = deathmatch, gg = GunGame' "$(getenv CS_MODE)")
case "$m" in gg|GG) setenv CS_MODE gg ;; *) setenv CS_MODE dm ;; esac
b=$(ask 'Bots (fill the server up to this many players, 0 = none)' "$(getenv CS_BOTS)")
case "$b" in ''|*[!0-9]*) b=0 ;; esac
setenv CS_BOTS "$b"
echo "created .env (edit it any time, then: docker compose up -d)"
else
echo "using existing .env"
fi
setenv CS_UID "$(id -u)"
setenv CS_GID "$(id -g)"
PORT=$(getenv CS_PORT); NAME=$(getenv CS_NAME)
RUNNING=$(docker inspect -f '{{.State.Status}}' "$NAME" 2>/dev/null || true)
if [ "$RUNNING" != running ] && command -v ss >/dev/null; then
! ss -lun | grep -q ":$PORT " || die "UDP port $PORT is already in use: set another CS_PORT in .env"
fi
echo "server '$(getenv CS_HOSTNAME)', container $NAME, UDP $PORT, bots $(getenv CS_BOTS)"
say "Secrets"
mkdir -p secrets hlds steamcmd-home dl backups
chmod 700 secrets backups
umask 077
[ -s secrets/rcon-password.txt ] || { randpw > secrets/rcon-password.txt; echo "new RCON password in secrets/rcon-password.txt"; }
# ReUnion salt: non-Steam players' IDs are derived from it. Keep it, or their stats/admin entries change.
[ -s secrets/reunion-salt.txt ] || randpw > secrets/reunion-salt.txt
umask 022
echo "ok"
say "Building the Docker image"
docker compose build -q
echo "ok"
# the running server holds the engine files open: stop it while they're replaced (it starts again below)
if [ "$RUNNING" = running ]; then
say "Stopping the server for the update"
docker compose stop
fi
if [ ! -x hlds/hlds_linux ] || [ -n "$UPDATE" ]; then
say "Downloading Counter-Strike 1.6 server files (SteamCMD, ~600 MB)"
# steam_legacy: ReHLDS needs engine build <= 8684. app 90 often fails on the first try, so retry.
# validate only on the first download: it would also put back Valve's versions of files you edited
VALIDATE=validate; [ -x hlds/hlds_linux ] && VALIDATE=
[ -f hlds/cstrike/mapcycle.txt ] && cp hlds/cstrike/mapcycle.txt dl/mapcycle.txt.keep
ok=
for i in 1 2 3 4; do
if docker compose run --rm --no-deps --entrypoint /opt/steamcmd/steamcmd.sh cs \
+force_install_dir /cs/hlds +login anonymous +app_set_config 90 mod cstrike \
+app_update 90 -beta steam_legacy $VALIDATE +quit && [ -f hlds/cstrike/liblist.gam ]; then
ok=1; break
fi
echo "SteamCMD attempt $i didn't finish, trying again..."
done
[ -n "$ok" ] || die "game download failed: run ./setup.sh again"
[ -f dl/mapcycle.txt.keep ] && mv dl/mapcycle.txt.keep hlds/cstrike/mapcycle.txt
else
echo; echo "game files present (./setup.sh --update to update them)"
fi
say "Installing the mod stack"
docker compose run --rm --no-deps -e CS_MODE="$(getenv CS_MODE)" \
-v "$PWD:/cs/repo:ro" -v "$PWD/dl:/cs/repo/dl" \
--entrypoint /bin/sh cs /cs/repo/tools/install-addons.sh
say "Starting"
docker compose up -d --force-recreate
printf 'waiting for the server to answer'
i=0
until python3 tools/hlq.py info 127.0.0.1 "$PORT" >/dev/null 2>&1; do
i=$((i+1)); [ $i -lt 40 ] || { echo; die "no answer after 2 min: look at 'docker logs --tail 50 $NAME'"; }
printf '.'; sleep 3
done
echo " up"
python3 tools/hlq.py info 127.0.0.1 "$PORT" || true
say "Done"
IPS=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -v ':' | grep -v '^172\.' | head -3 | tr '\n' ' ')
echo "Join from Counter-Strike 1.6 (console, ~ key):"
for ip in $IPS; do echo " connect $ip:$PORT"; done
echo "From the internet: forward UDP $PORT on your router to this machine, then connect <your public IP>:$PORT"
echo "No port forwarding possible? A free playit.gg UDP tunnel to 127.0.0.1:$PORT works too (README)."
echo
echo "Control menu: ./cs (status, map, mode, bots, kill counter, restart, ...)"
echo "Console: ./rcon status ./rcon changelevel de_inferno"
echo "Auto-care: ./scripts/install-cron.sh (watchdog + nightly backup)"
echo "Docs: README.md, docs/ADMIN.md, docs/PLAYERS.md"
Executable
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Asks a CS 1.6 / GoldSrc server for its info (A2S_INFO), like the in-game server browser does.
python3 tools/hlq.py info <host> <port>
Prints: name | map | players x/y (bots n) | password yes/no. Exit code 1 when there's no answer.
Works against any address (LAN, VPN, a playit tunnel), so it also tests the way in from outside."""
import socket, sys
H = b"\xff\xff\xff\xff"
def info(host, port, timeout=4):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(timeout)
q = H + b"TSource Engine Query\x00"
s.sendto(q, (host, port))
d = s.recvfrom(65535)[0]
if d[4:5] == b"A": # newer servers answer with a challenge first: ask again with it
s.sendto(q + d[5:9], (host, port))
d = s.recvfrom(65535)[0]
if d[4:5] != b"I":
raise ValueError("unexpected reply type %r" % d[4:5])
name, mp, folder, game, rest = d[6:].split(b"\x00", 4)
players, maxp, bots, pw = rest[2], rest[3], rest[4], rest[7]
return (f"{name.decode(errors='replace')} | map {mp.decode()} | players {players}/{maxp} (bots {bots})"
f" | password {'yes' if pw else 'no'}")
if __name__ == "__main__":
if len(sys.argv) != 4 or sys.argv[1] != "info":
sys.exit(__doc__)
try:
print("A2S_INFO ok:", info(sys.argv[2], int(sys.argv[3])))
except (OSError, ValueError) as e:
print("no answer:", e)
sys.exit(1)
+130
View File
@@ -0,0 +1,130 @@
#!/bin/sh
# Puts the mod stack on top of the Valve game files in hlds/. setup.sh runs this inside the
# image after every game download/update (a Steam "validate" puts Valve's engine back and
# silently removes ReHLDS + Metamod, so this always has to run after it).
# Safe to run again. Kept as they are on re-runs: maps, stats, bot navigation graphs, and your
# edits to users.ini (admins), amxx.cfg, csdm/config.ini, mapcycle.txt and custom.cfg.
# Mounts (see setup.sh): repo at /cs/repo (read-only) with dl/ writable, hlds at /cs/hlds.
set -e
R=/cs/repo; DL=$R/dl; H=/cs/hlds; C=$H/cstrike; A=$C/addons; CF=$C/addons/amxmodx/configs
X=$(mktemp -d)
trap 'rm -rf "$X"' EXIT
step() { printf ' - %s\n' "$*"; }
die() { printf '\033[31merror:\033[0m %s\n' "$*" >&2; exit 1; }
sha() { sha256sum < "$1" | cut -c1-64; }
[ -x $H/hlds_linux ] || die "no game files in hlds/ (the Steam download didn't finish)"
step "downloads (pinned, checked against tools/versions.txt)"
mkdir -p $DL/gg $DL/snd $DL/spawns
grep -v '^#' $R/tools/versions.txt | while read -r f sum url; do
[ -n "$f" ] || continue
[ -f "$DL/$f" ] && [ "$(sha "$DL/$f")" = "$sum" ] && continue
curl -fsSL --retry 3 -o "$DL/$f.part" "$url" || die "download failed: $url"
got=$(sha "$DL/$f.part")
[ "$got" = "$sum" ] || { rm -f "$DL/$f.part"; die "checksum mismatch for $f: expected $sum, got $got"; }
mv "$DL/$f.part" "$DL/$f"
done
ls $DL/rehlds-bin-*.zip >/dev/null
# Game mode in use right now (re-run) or from .env (first install)
if [ -f $CF/plugins-gungame.ini ]; then MODE=gg
elif [ -f $CF/plugins-CSDM_ReAPI.ini ] || [ -f $CF/plugins-CSDM_ReAPI.ini.off ]; then MODE=dm
else MODE=${CS_MODE:-dm}; fi
case "$MODE" in dm|gg) ;; *) die "CS_MODE must be dm or gg (is: $MODE)" ;; esac
# Files people edit: save them now, put them back after the archives are unpacked
FIRST=; [ -f $C/.cs16-dm-server ] || FIRST=1
KEEP="addons/amxmodx/configs/users.ini addons/amxmodx/configs/amxx.cfg addons/amxmodx/configs/csdm/config.ini"
if [ -z "$FIRST" ]; then
for f in $KEEP; do [ -f "$C/$f" ] && mkdir -p "$X/keep/$(dirname "$f")" && cp -p "$C/$f" "$X/keep/$f"; done
fi
step "ReHLDS (engine)"
unzip -qo $DL/rehlds-bin-3.15.0.896.zip 'bin/linux32/*' -d $X/rehlds
cp -a $X/rehlds/bin/linux32/. $H/
chmod +x $H/hlds_linux $H/hltv # the zip has no exec bits
step "ReGameDLL (game logic)"
unzip -qo $DL/regamedll-bin-5.30.0.814.zip 'bin/linux32/cstrike/*' -d $X/regame
cp -a $X/regame/bin/linux32/cstrike/. $C/
# game.cfg runs at every map start and would force alltalk off: server.cfg decides that instead
sed -i 's|^sv_alltalk "0"|// sv_alltalk "0" // cs16-dm-server: set in server.cfg instead|' $C/game.cfg
step "Metamod-R"
unzip -qo $DL/metamod-bin-1.3.0.149.zip 'addons/*' -d $X/mm
cp -a $X/mm/addons/. $A/
[ -f $C/liblist.gam.orig ] || cp $C/liblist.gam $C/liblist.gam.orig
sed -i 's|^gamedll_linux .*|gamedll_linux "addons/metamod/metamod_i386.so"|' $C/liblist.gam
step "AMX Mod X + ReAPI"
tar xzf $DL/amxmodx-1.10.0-git5486-base-linux.tar.gz -C $C
tar xzf $DL/amxmodx-1.10.0-git5486-cstrike-linux.tar.gz -C $C
unzip -qo $DL/reapi-bin-5.29.0.358.zip 'addons/*' -d $X/reapi
cp -a $X/reapi/addons/. $A/
step "ReUnion (non-Steam clients)"
unzip -qo $DL/reunion-0.2.0.25.zip bin/Linux/reunion_mm_i386.so reunion.cfg -d $X/reu
mkdir -p $A/reunion
cp $X/reu/bin/Linux/reunion_mm_i386.so $A/reunion/
SALT=$(tr -d '\r\n' < /cs/secrets/reunion-salt.txt)
[ -n "$SALT" ] || die "secrets/reunion-salt.txt is empty"
# IDClientsLimit 0: several non-Steam players with the same placeholder ID can all join
( umask 077; sed -e "s|^SteamIdHashSalt =.*|SteamIdHashSalt = $SALT|" -e 's|^IDClientsLimit = .*|IDClientsLimit = 0|' \
$X/reu/reunion.cfg > $C/reunion.cfg )
step "CSDM-ReAPI (deathmatch) + spawn points"
unzip -qo $DL/CSDM-ReAPI-v1.0.8.zip 'cstrike/*' -d $X/csdm
cp -a $X/csdm/cstrike/. $C/
mkdir -p $CF/csdm/spawns
for f in $DL/spawns/*.spawns.cfg; do
b=$(basename "$f")
# old 6-field lines (de_prodigy, de_train) -> the 10 fields CSDM-ReAPI reads (others are skipped)
awk 'NF==6 {print $0" 0 0 0 0"; next} {print}' "$f" > $CF/csdm/spawns/$b
ln -sfn spawns/$b $CF/csdm/$b # GunGame looks for them one folder up
done
step "YaPB (bots)"
tar xJf $DL/yapb-4.4.957-linux.tar.xz -C $C
find $A/yapb -type d -exec chmod 775 {} + # the tarball's dirs have no x bit
sed -i -e 's/^yb_csdm_mode ".*"/yb_csdm_mode "2"/' -e 's/^yb_quota_mode ".*"/yb_quota_mode "fill"/' \
$A/yapb/conf/yapb.cfg # FFA-aware bots; quota = fill up to CS_BOTS players (start.sh sets it)
step "GunGame + announcer sounds"
cp $DL/gg/gungame.sma $A/amxmodx/scripting/
cp $DL/gg/gungame.txt $A/amxmodx/data/lang/
mkdir -p $C/sound/gungame $C/sound/misc
cp $DL/gg/*.wav $C/sound/gungame/
cp $DL/snd/*.wav $C/sound/misc/
step "configs from the repo"
cp -a $R/cstrike/. $C/
if [ -n "$FIRST" ]; then
cp $R/defaults/mapcycle.txt $C/mapcycle.txt
fi
[ -f $C/custom.cfg ] || cp $R/defaults/custom.cfg $C/custom.cfg
if [ -z "$FIRST" ] && [ -d $X/keep ]; then cp -a $X/keep/. $C/; fi
step "compiling plugins"
cp $R/plugins/*.sma $A/amxmodx/scripting/
cd $A/amxmodx/scripting
for p in rtv esef_extras esef_mode gungame; do
./amxxpc $p.sma -o../plugins/$p.amxx > $X/$p.log 2>&1 || { cat $X/$p.log; die "compiling $p.sma failed"; }
done
cd - >/dev/null
step "game mode: $MODE"
# exactly one of the two plugin lists is active (.ini), the other one is .ini.off
for ini in plugins-gungame.ini plugins-CSDM_ReAPI.ini; do
[ -f $CF/$ini ] && rm -f $CF/$ini.off # fresh copy from the archive/repo wins
[ -f $CF/$ini ] || mv $CF/$ini.off $CF/$ini
done
if [ "$MODE" = gg ]; then mv $CF/plugins-CSDM_ReAPI.ini $CF/plugins-CSDM_ReAPI.ini.off
else mv $CF/plugins-gungame.ini $CF/plugins-gungame.ini.off; fi
# hlds looks for steamclient.so here; without it: "Unable to initialize Steam"
mkdir -p /cs/steamcmd-home/.steam/sdk32
ln -sfn /cs/hlds/steamclient.so /cs/steamcmd-home/.steam/sdk32/steamclient.so
touch $C/.cs16-dm-server
echo " mod stack installed"
Executable
+28
View File
@@ -0,0 +1,28 @@
#!/bin/sh
# Container entry point: writes the .env values into the game configs, then starts the server.
# Runs at every container start, so edit .env (not these files) and run: docker compose up -d
set -e
C=/cs/hlds/cstrike
clean() { printf '%s' "$1" | tr -d '"\r'; } # no double quotes inside cfg strings
{
echo "// written from .env by tools/start.sh at every start: edit .env instead"
echo "hostname \"$(clean "$CS_HOSTNAME")\""
} > $C/env.cfg
# secrets.cfg holds the RCON + join password: readable only by the server user
umask 077
{
echo "// written by tools/start.sh at every start (RCON: secrets/rcon-password.txt, join: CS_PASSWORD in .env)"
echo "rcon_password \"$(tr -d '"\r\n' < /cs/secrets/rcon-password.txt)\""
echo "sv_password \"$(clean "$CS_PASSWORD")\""
} > $C/secrets.cfg
umask 022
sed -i -e "s/^yb_quota \".*\"/yb_quota \"${CS_BOTS:-0}\"/" \
-e "s/^yb_difficulty \".*\"/yb_difficulty \"${CS_BOT_DIFFICULTY:-2}\"/" \
$C/addons/yapb/conf/yapb.cfg
cd /cs/hlds
exec ./hlds_run -game cstrike -port "${CS_PORT:-27015}" +maxplayers "${CS_MAXPLAYERS:-16}" \
+map "${CS_MAP:-de_dust2}" -pingboost 2 -norestart +log on
+59
View File
@@ -0,0 +1,59 @@
# Pinned third-party downloads: <file in dl/> <sha256> <url>
# setup.sh fetches each one into dl/ and refuses to install it if the hash differs.
# Bump a version: change url + file name, run ./setup.sh, put the new hash it prints here.
# engine + game: ReHLDS, ReGameDLL_CS (MIT)
rehlds-bin-3.15.0.896.zip 997baeb7ef3842dab3e034d82dc651ebfe560c23b158adce660a6b97976b4e2b https://github.com/rehlds/ReHLDS/releases/download/3.15.0.896/rehlds-bin-3.15.0.896.zip
regamedll-bin-5.30.0.814.zip 457f5c96a4d10280fcad47f106cbd5be86363a47df1ccab91c514e9d1bc6fd18 https://github.com/rehlds/ReGameDLL_CS/releases/download/5.30.0.814/regamedll-bin-5.30.0.814.zip
# plugin loaders + API: Metamod-R, AMX Mod X, ReAPI, ReUnion (GPL-3.0)
metamod-bin-1.3.0.149.zip ede7f59c4e0220afe8c02aa348a130cce527f87d36ffdb674e37a501ce57be94 https://github.com/rehlds/Metamod-R/releases/download/1.3.0.149/metamod-bin-1.3.0.149.zip
amxmodx-1.10.0-git5486-base-linux.tar.gz 9f5041325cc656dcc292cafb9d2c9cb4deeedb57192d8a7ed43947530e04e5b7 https://www.amxmodx.org/amxxdrop/1.10/amxmodx-1.10.0-git5486-base-linux.tar.gz
amxmodx-1.10.0-git5486-cstrike-linux.tar.gz 686d875010792d84ac765d2f78af227ab7000c9b567995ea56b836e0a41fc7a7 https://www.amxmodx.org/amxxdrop/1.10/amxmodx-1.10.0-git5486-cstrike-linux.tar.gz
reapi-bin-5.29.0.358.zip f33a7435540bea8706db3fa948e51a85b511f5b18c383b97402a204dc5419195 https://github.com/rehlds/ReAPI/releases/download/5.29.0.358/reapi-bin-5.29.0.358.zip
reunion-0.2.0.25.zip 0f238276719274216bd169c578eac2db4e041946b8b9f0e407e41ef9d85efdf5 https://github.com/rehlds/ReUnion/releases/download/0.2.0.25/reunion-0.2.0.25.zip
# deathmatch + bots: CSDM-ReAPI, YaPB (Unlicense)
CSDM-ReAPI-v1.0.8.zip e7650204080be0819e9dfe6239fb9e9b0aca92e5e33c956053adcb5117fc5fc8 https://github.com/wopox1337/CSDM-ReAPI/releases/download/v1.0.8/CSDM-ReAPI-v1.0.8.zip
yapb-4.4.957-linux.tar.xz 8c095ac89b9b2ccc70a66a71d608e1a570b5268c57c6083ced8c06161533a4b1 https://github.com/yapb/yapb/releases/download/4.4.957/yapb-4.4.957-linux.tar.xz
# GunGame 2.13c (Avalanche), github.com/fysiks1/amxx-gungame_cstrike @ 8024d1e
gg/gungame.sma 027cae2b5c8cea14a96cd8f627b0123ac2929f725c5352739f136cab7c11f975 https://raw.githubusercontent.com/fysiks1/amxx-gungame_cstrike/8024d1ef35cddf357ce18b277bde2c6422cab56d/scripting/gungame.sma
gg/gungame.txt 7759207c3f2e397b3823cd584fce48bb5dfb7f9db17154910bb1d6d376cec8d8 https://raw.githubusercontent.com/fysiks1/amxx-gungame_cstrike/8024d1ef35cddf357ce18b277bde2c6422cab56d/data/lang/gungame.txt
gg/gg_brass_bell.wav 818ce90dc03f34664aee15f28efd30a5873e852043957b8be11c37f65c39eaa7 https://raw.githubusercontent.com/fysiks1/amxx-gungame_cstrike/8024d1ef35cddf357ce18b277bde2c6422cab56d/resources/sound/gungame/gg_brass_bell.wav
gg/gg_knife_level.wav 33183e49d6a877d5b3d604838c392d904cf19eb23b675912a07558c20662f84e https://raw.githubusercontent.com/fysiks1/amxx-gungame_cstrike/8024d1ef35cddf357ce18b277bde2c6422cab56d/resources/sound/gungame/gg_knife_level.wav
gg/gg_levelup.wav 7e03c12c2af7671cf6ebcfbe1cf4ad964cbeb979439afaad9a8b4b55a0976b62 https://raw.githubusercontent.com/fysiks1/amxx-gungame_cstrike/8024d1ef35cddf357ce18b277bde2c6422cab56d/resources/sound/gungame/gg_levelup.wav
gg/gg_lostlead.wav e2210ef24033762ad4ea2f71b7cbc2e0c6e23dc1e0511147cdb3d14b61e1c9ca https://raw.githubusercontent.com/fysiks1/amxx-gungame_cstrike/8024d1ef35cddf357ce18b277bde2c6422cab56d/resources/sound/gungame/gg_lostlead.wav
gg/gg_nade_level.wav 6f89f7b9ce94a9e7762eb3733f7b8d26dd55fe167155102081c6cd2b121054ac https://raw.githubusercontent.com/fysiks1/amxx-gungame_cstrike/8024d1ef35cddf357ce18b277bde2c6422cab56d/resources/sound/gungame/gg_nade_level.wav
gg/gg_takenlead.wav c68b8dcefc4db9e22b2dc5dca6c2865a4a31744bffae268c9f0c726bf4dbb27d https://raw.githubusercontent.com/fysiks1/amxx-gungame_cstrike/8024d1ef35cddf357ce18b277bde2c6422cab56d/resources/sound/gungame/gg_takenlead.wav
gg/gg_tiedlead.wav 87dfb48d7398a2c75db64e55a834ef0f7107f85d14bd62f70cc55a8652c829a5 https://raw.githubusercontent.com/fysiks1/amxx-gungame_cstrike/8024d1ef35cddf357ce18b277bde2c6422cab56d/resources/sound/gungame/gg_tiedlead.wav
gg/gg_triple.wav 301d63628d74952c347b55e036914a62255a3a8a3f766db4ff61124286c3092b https://raw.githubusercontent.com/fysiks1/amxx-gungame_cstrike/8024d1ef35cddf357ce18b277bde2c6422cab56d/resources/sound/gungame/gg_triple.wav
gg/gg_welcome.wav 1544afd42fc7926ce923ad499a264cec24e0aaae4884926d399457fc80956003 https://raw.githubusercontent.com/fysiks1/amxx-gungame_cstrike/8024d1ef35cddf357ce18b277bde2c6422cab56d/resources/sound/gungame/gg_welcome.wav
# Announcer sounds for the extras plugin, github.com/hpoon/HLDS-CS1.6 @ b0abdc3 (cstrike/sound/misc/)
snd/dominating.wav 8cf8fa1f85b1d329e08db4f467398f6a7724eb335acf7704aa81442b34df44ae https://raw.githubusercontent.com/hpoon/HLDS-CS1.6/b0abdc37d1da/cstrike/sound/misc/dominating.wav
snd/doublekill.wav 66e07ad6ba4ac978b401d717391d9be42d188123072c5883d4aa360ad79baed9 https://raw.githubusercontent.com/hpoon/HLDS-CS1.6/b0abdc37d1da/cstrike/sound/misc/doublekill.wav
snd/firstblood.wav 6882d8ce2fe548cc1f4464b594163196ebf0f550b03f61d8864b35ab973d5183 https://raw.githubusercontent.com/hpoon/HLDS-CS1.6/b0abdc37d1da/cstrike/sound/misc/firstblood.wav
snd/godlike.wav 924c13260a1375484d7204b64e17c0cafe9ec4981fbb3e4cb51ead5ef2620596 https://raw.githubusercontent.com/hpoon/HLDS-CS1.6/b0abdc37d1da/cstrike/sound/misc/godlike.wav
snd/headshot.wav 50993616bcc6a18de917d83181e60fc38ab9da21a29fe6e06f0f6ac6b3353580 https://raw.githubusercontent.com/hpoon/HLDS-CS1.6/b0abdc37d1da/cstrike/sound/misc/headshot.wav
snd/holyshit.wav 8fa0f0e881f166274748409f0cb932f7911e3bd38f11f455f169c029fd59dda7 https://raw.githubusercontent.com/hpoon/HLDS-CS1.6/b0abdc37d1da/cstrike/sound/misc/holyshit.wav
snd/humiliation.wav 026c862bb145486d6c87a6426e864cf45f1dd750c6c194391d2717725064da52 https://raw.githubusercontent.com/hpoon/HLDS-CS1.6/b0abdc37d1da/cstrike/sound/misc/humiliation.wav
snd/killingspree.wav 57e11e963a7bff2d4f788b0ac9f65d7805ffb78ece44ecc40564b7b682411077 https://raw.githubusercontent.com/hpoon/HLDS-CS1.6/b0abdc37d1da/cstrike/sound/misc/killingspree.wav
snd/monsterkill.wav 4aa258c88105f1e24f89b63f69a35c1d75c83d1c27d43cc948eba9b76bb3606d https://raw.githubusercontent.com/hpoon/HLDS-CS1.6/b0abdc37d1da/cstrike/sound/misc/monsterkill.wav
snd/multikill.wav 526e1c99ee930cec729f269a916e983c18a3ce54449929f423953fb543c24b67 https://raw.githubusercontent.com/hpoon/HLDS-CS1.6/b0abdc37d1da/cstrike/sound/misc/multikill.wav
snd/rampage.wav 42063b331c00467b919accce3602446a29b226ca8111a164192cee5d88e0765a https://raw.githubusercontent.com/hpoon/HLDS-CS1.6/b0abdc37d1da/cstrike/sound/misc/rampage.wav
snd/ultrakill.wav 842fa328d92c91cc0bd0315f78b153006b6046cbf41341c63f74663b994f96b6 https://raw.githubusercontent.com/hpoon/HLDS-CS1.6/b0abdc37d1da/cstrike/sound/misc/ultrakill.wav
snd/unstoppable.wav df525291a6db921734fa19e64b4a95e884090eea3bf29bbd906a1ba2de7374f6 https://raw.githubusercontent.com/hpoon/HLDS-CS1.6/b0abdc37d1da/cstrike/sound/misc/unstoppable.wav
snd/wickedsick.wav 8dbc96ba7961fed9d83ada3434ac5b61d41fac7a2d0c9f8ddd11e7eb2373a0b2 https://raw.githubusercontent.com/hpoon/HLDS-CS1.6/b0abdc37d1da/cstrike/sound/misc/wickedsick.wav
# DM spawn points (CSDM 2.1 by Arkshine), github.com/Arkshine/CSDM @ 3df2741. de_prodigy/de_train
# are in the old 6-field format: install converts them to the 10 fields CSDM-ReAPI needs.
spawns/cs_assault.spawns.cfg 6035adc7f5d59e5f0388936fd843a9a1fb68cdf86e2dc0454241c6390d832cce https://raw.githubusercontent.com/Arkshine/CSDM/3df2741b09c0e039ebd6cf96d66a5f2acd58b70f/amxmodx/configs/csdm/cs_assault.spawns.cfg
spawns/cs_italy.spawns.cfg b8bb8839f67b3718ac18ef97a17a403d1b0163286e5e389da4cf1d21fdf1003d https://raw.githubusercontent.com/Arkshine/CSDM/3df2741b09c0e039ebd6cf96d66a5f2acd58b70f/amxmodx/configs/csdm/cs_italy.spawns.cfg
spawns/cs_office.spawns.cfg 6d6964dd54b62e8af0904360131985ebc7b5c9111e0832798e96e2fbaaa19c52 https://raw.githubusercontent.com/Arkshine/CSDM/3df2741b09c0e039ebd6cf96d66a5f2acd58b70f/amxmodx/configs/csdm/cs_office.spawns.cfg
spawns/de_aztec.spawns.cfg 9454d9c86838e9b36b5214c15acb06af383aafdbefbab6c6494f09c8093b4cd1 https://raw.githubusercontent.com/Arkshine/CSDM/3df2741b09c0e039ebd6cf96d66a5f2acd58b70f/amxmodx/configs/csdm/de_aztec.spawns.cfg
spawns/de_cbble.spawns.cfg 4f32377713f03cec6086d09e4567ee9196c296b85c7b434670f243269b01db2c https://raw.githubusercontent.com/Arkshine/CSDM/3df2741b09c0e039ebd6cf96d66a5f2acd58b70f/amxmodx/configs/csdm/de_cbble.spawns.cfg
spawns/de_dust.spawns.cfg 3fe16dc87db890b1ad8b75bd2cada44cb7faecbca290049c05410cd6a439e532 https://raw.githubusercontent.com/Arkshine/CSDM/3df2741b09c0e039ebd6cf96d66a5f2acd58b70f/amxmodx/configs/csdm/de_dust.spawns.cfg
spawns/de_dust2.spawns.cfg 98eefa633147ab4e781ba957dfce6172d2bdd975191dcaf4403f69c3393870ea https://raw.githubusercontent.com/Arkshine/CSDM/3df2741b09c0e039ebd6cf96d66a5f2acd58b70f/amxmodx/configs/csdm/de_dust2.spawns.cfg
spawns/de_inferno.spawns.cfg a93a0b628f8079b89ceb8a845a2e2d5e8bdd775911dc569294fdb3710476866c https://raw.githubusercontent.com/Arkshine/CSDM/3df2741b09c0e039ebd6cf96d66a5f2acd58b70f/amxmodx/configs/csdm/de_inferno.spawns.cfg
spawns/de_nuke.spawns.cfg 39e628ea43eb00877e2006eadef9d834673a9a6a879e77199cd49e3a0899b759 https://raw.githubusercontent.com/Arkshine/CSDM/3df2741b09c0e039ebd6cf96d66a5f2acd58b70f/amxmodx/configs/csdm/de_nuke.spawns.cfg
spawns/de_prodigy.spawns.cfg 9e1e340b3160009824e6810fdba8ff4bf6968cdfd13ace2eceddb9c869b30ea9 https://raw.githubusercontent.com/Arkshine/CSDM/3df2741b09c0e039ebd6cf96d66a5f2acd58b70f/amxmodx/configs/csdm/de_prodigy.spawns.cfg
spawns/de_train.spawns.cfg 787e97ae9ed75f55eb771c8fe9871cc14be44df8f0595a851263b2468ae63d96 https://raw.githubusercontent.com/Arkshine/CSDM/3df2741b09c0e039ebd6cf96d66a5f2acd58b70f/amxmodx/configs/csdm/de_train.spawns.cfg