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:
commit
7e9cbee14d
33 files changed
+1890
No files matched your search
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
* text=auto eol=lf
|
||||
*.wav binary
|
||||
+13
@@ -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
@@ -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
|
||||
@@ -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.
|
||||
@@ -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)).
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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()
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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
@@ -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])
|
||||
}
|
||||
@@ -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))
|
||||
Executable
+24
@@ -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
@@ -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}
|
||||
Executable
+16
@@ -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"
|
||||
Executable
+22
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
Executable
+130
@@ -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
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user