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

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

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

No files matched your search

+233
View File
@@ -0,0 +1,233 @@
// esef_extras.sma - game-feel extras for cs16-dm-server.
// - damage numbers: health you took off (blue, right of crosshair) / lost (red, left)
// - killstreak + multikill announcements (DHUD) with Quake/UT announcer sounds
// - headshot / knife / first blood sounds
// - HP on kill (esef_kill_hp, esef_kill_hp_hs), capped at 100
// - small HUD line: kills, deaths, streak, best streak (esef_hudinfo; hidden while GunGame runs)
// Sounds live in cstrike/sound/misc and are precached, so clients download them once (~800 KB).
#include <amxmodx>
#include <reapi>
#define MULTIKILL_WINDOW 2.0
#define TASK_HUD 4100
new const SND_HEADSHOT[] = "misc/headshot.wav"
new const SND_FIRSTBLOOD[] = "misc/firstblood.wav"
new const SND_KNIFE[] = "misc/humiliation.wav"
// multikill chain: 2, 3, 4, 5+ kills inside MULTIKILL_WINDOW
new const SND_MULTI[][] = { "misc/doublekill.wav", "misc/multikill.wav", "misc/ultrakill.wav", "misc/monsterkill.wav" }
new const TXT_MULTI[][] = { "DOUBLE KILL", "MULTI KILL", "ULTRA KILL", "MONSTER KILL" }
// streaks without dying
new const STREAK_AT[] = { 3, 5, 7, 9, 11, 13, 15 }
new const SND_STREAK[][] = {
"misc/killingspree.wav", "misc/rampage.wav", "misc/dominating.wav", "misc/unstoppable.wav",
"misc/godlike.wav", "misc/wickedsick.wav", "misc/holyshit.wav"
}
new const TXT_STREAK[][] = {
"is on a KILLING SPREE", "is on a RAMPAGE", "is DOMINATING", "is UNSTOPPABLE",
"is GODLIKE", "is WICKED SICK", "HOLY SHIT"
}
new g_streak[MAX_PLAYERS + 1], g_best[MAX_PLAYERS + 1], g_kills[MAX_PLAYERS + 1], g_deaths[MAX_PLAYERS + 1]
new g_chain[MAX_PLAYERS + 1], Float:g_lastKill[MAX_PLAYERS + 1]
new Float:g_preHealth[MAX_PLAYERS + 1]
new bool:g_firstBlood
new g_syncDealt, g_syncTaken, g_syncInfo
new g_pKillHp, g_pKillHpHs, g_pHudInfo, g_pSounds, g_pDamage
new bool:g_gunGame // GunGame shows its own level/kills HUD, so skip ours
public plugin_precache()
{
precache_sound(SND_HEADSHOT)
precache_sound(SND_FIRSTBLOOD)
precache_sound(SND_KNIFE)
for (new i = 0; i < sizeof SND_MULTI; i++)
precache_sound(SND_MULTI[i])
for (new i = 0; i < sizeof SND_STREAK; i++)
precache_sound(SND_STREAK[i])
}
public plugin_init()
{
register_plugin("DM extras", "1.0", "cs16-dm-server")
g_pKillHp = register_cvar("esef_kill_hp", "10")
g_pKillHpHs = register_cvar("esef_kill_hp_hs", "20")
g_pHudInfo = register_cvar("esef_hudinfo", "1")
g_pSounds = register_cvar("esef_sounds", "1")
g_pDamage = register_cvar("esef_damage", "1")
RegisterHookChain(RG_CBasePlayer_TakeDamage, "TakeDamage_Pre", .post = false)
RegisterHookChain(RG_CBasePlayer_TakeDamage, "TakeDamage_Post", .post = true)
RegisterHookChain(RG_CBasePlayer_Killed, "Killed_Post", .post = true)
g_syncDealt = CreateHudSyncObj()
g_syncTaken = CreateHudSyncObj()
g_syncInfo = CreateHudSyncObj()
set_task(1.0, "ShowInfoAll", TASK_HUD, .flags = "b")
}
public plugin_cfg()
{
g_gunGame = (find_plugin_byfile("gungame.amxx") != INVALID_PLUGIN_ID)
}
public client_putinserver(id)
{
g_streak[id] = g_best[id] = g_kills[id] = g_deaths[id] = g_chain[id] = 0
g_lastKill[id] = 0.0
}
// --- damage numbers -------------------------------------------------------
public TakeDamage_Pre(victim, inflictor, attacker, Float:damage, damagebits)
{
g_preHealth[victim] = Float:get_entvar(victim, var_health)
return HC_CONTINUE
}
public TakeDamage_Post(victim, inflictor, attacker, Float:damage, damagebits)
{
if (!get_pcvar_num(g_pDamage))
return HC_CONTINUE
new Float:now = Float:get_entvar(victim, var_health)
if (now < 0.0)
now = 0.0
new lost = floatround(g_preHealth[victim] - now)
if (lost <= 0)
return HC_CONTINUE
if (is_user_connected(victim) && !is_user_bot(victim))
{
set_hudmessage(220, 40, 40, 0.44, 0.52, 0, 0.0, 1.0, 0.0, 0.3, -1)
ShowSyncHudMsg(victim, g_syncTaken, "%d", lost)
}
if (attacker != victim && is_user_connected(attacker) && !is_user_bot(attacker))
{
set_hudmessage(40, 160, 255, 0.54, 0.47, 0, 0.0, 1.0, 0.0, 0.3, -1)
ShowSyncHudMsg(attacker, g_syncDealt, "%d", lost)
}
return HC_CONTINUE
}
// --- kills -----------------------------------------------------------------
public Killed_Post(victim, killer, gib)
{
if (is_user_connected(victim))
{
g_deaths[victim]++
g_streak[victim] = 0
g_chain[victim] = 0
ShowInfo(victim)
}
if (killer == victim || !is_user_connected(killer))
return HC_CONTINUE
g_kills[killer]++
g_streak[killer]++
if (g_streak[killer] > g_best[killer])
g_best[killer] = g_streak[killer]
new Float:t = get_gametime()
g_chain[killer] = (t - g_lastKill[killer] <= MULTIKILL_WINDOW) ? g_chain[killer] + 1 : 1
g_lastKill[killer] = t
new bool:headshot = bool:get_member(victim, m_bHeadshotKilled)
new bool:knife = (get_user_weapon(killer) == CSW_KNIFE && get_entvar(victim, var_dmg_inflictor) == killer)
// HP on kill
if (is_user_alive(killer))
{
new bonus = headshot ? get_pcvar_num(g_pKillHpHs) : get_pcvar_num(g_pKillHp)
if (bonus > 0)
{
new Float:hp = Float:get_entvar(killer, var_health) + float(bonus)
set_entvar(killer, var_health, hp > 100.0 ? 100.0 : hp)
}
}
new name[32]
get_user_name(killer, name, charsmax(name))
// pick one sound per kill: streak (everyone) > multikill (killer) > knife (everyone) > headshot (killer) > first blood
new streakIdx = -1
for (new i = 0; i < sizeof STREAK_AT; i++)
if (g_streak[killer] == STREAK_AT[i])
streakIdx = i
if (streakIdx >= 0)
{
set_dhudmessage(255, 160, 0, -1.0, 0.25, 0, 0.0, 2.5, 0.1, 0.4)
show_dhudmessage(0, "%s %s! (%d)", name, TXT_STREAK[streakIdx], g_streak[killer])
PlaySound(0, SND_STREAK[streakIdx])
}
else if (g_chain[killer] >= 2)
{
new m = g_chain[killer] - 2
if (m >= sizeof TXT_MULTI)
m = sizeof TXT_MULTI - 1
set_dhudmessage(255, 60, 60, -1.0, 0.30, 0, 0.0, 1.5, 0.1, 0.3)
show_dhudmessage(killer, TXT_MULTI[m])
PlaySound(killer, SND_MULTI[m])
}
else if (knife)
{
new vname[32]
get_user_name(victim, vname, charsmax(vname))
set_dhudmessage(255, 255, 255, -1.0, 0.25, 0, 0.0, 2.0, 0.1, 0.3)
show_dhudmessage(0, "%s knifed %s!", name, vname)
PlaySound(0, SND_KNIFE)
}
else if (headshot)
{
PlaySound(killer, SND_HEADSHOT)
}
else if (!g_firstBlood)
{
set_dhudmessage(255, 0, 0, -1.0, 0.25, 0, 0.0, 2.0, 0.1, 0.3)
show_dhudmessage(0, "%s drew FIRST BLOOD", name)
PlaySound(0, SND_FIRSTBLOOD)
}
g_firstBlood = true
ShowInfo(killer)
return HC_CONTINUE
}
PlaySound(id, const snd[])
{
if (!get_pcvar_num(g_pSounds))
return
// spk wants the path without "sound/" and without extension
new path[64]
copy(path, charsmax(path), snd)
replace(path, charsmax(path), ".wav", "")
client_cmd(id, "spk ^"%s^"", path)
}
// --- HUD info line ----------------------------------------------------------
public ShowInfoAll()
{
if (g_gunGame || !get_pcvar_num(g_pHudInfo))
return
new players[MAX_PLAYERS], num
get_players(players, num, "ach") // alive, no bots, no HLTV
for (new i = 0; i < num; i++)
ShowInfo(players[i])
}
ShowInfo(id)
{
if (g_gunGame || !get_pcvar_num(g_pHudInfo) || !is_user_connected(id) || is_user_bot(id))
return
set_hudmessage(200, 200, 200, 0.01, 0.88, 0, 0.0, 1.2, 0.0, 0.0, -1)
ShowSyncHudMsg(id, g_syncInfo, "K %d D %d | streak %d (best %d)", g_kills[id], g_deaths[id], g_streak[id], g_best[id])
}
+84
View File
@@ -0,0 +1,84 @@
// esef_mode.sma - switch the server between FFA deathmatch (CSDM-ReAPI) and GunGame.
// Server command (use via RCON): esef_mode -> prints the current mode
// esef_mode gg | dm -> switches and reloads the current map
// AMXX loads every configs/plugins-*.ini at each map change, so a mode is just which of the
// two plugin lists is named .ini and which .ini.off. The choice survives restarts.
#include <amxmodx>
#include <amxmisc>
new const GG_INI[] = "plugins-gungame.ini"
new const DM_INI[] = "plugins-CSDM_ReAPI.ini"
new g_dir[64]
public plugin_init()
{
register_plugin("DM/GG mode switch", "1.0", "cs16-dm-server")
register_srvcmd("esef_mode", "CmdMode", -1, "[dm|gg] - show or switch game mode (reloads the map)")
get_configsdir(g_dir, charsmax(g_dir))
}
bool:IsOn(const ini[])
{
new path[128]
formatex(path, charsmax(path), "%s/%s", g_dir, ini)
return bool:file_exists(path)
}
// enable: <ini>.off -> <ini> disable: <ini> -> <ini>.off
SetIni(const ini[], bool:enable)
{
new on[128], off[128]
formatex(on, charsmax(on), "%s/%s", g_dir, ini)
formatex(off, charsmax(off), "%s/%s.off", g_dir, ini)
if (enable && !file_exists(on) && file_exists(off))
rename_file(off, on, 1)
else if (!enable && file_exists(on))
rename_file(on, off, 1)
}
public CmdMode()
{
new arg[8]
read_argv(1, arg, charsmax(arg))
new bool:gg = IsOn(GG_INI)
if (!arg[0])
{
server_print("[esef_mode] current mode: %s", gg ? "gg (GunGame)" : "dm (FFA deathmatch)")
return PLUGIN_HANDLED
}
new bool:wantGG
if (equali(arg, "gg"))
wantGG = true
else if (equali(arg, "dm"))
wantGG = false
else
{
server_print("[esef_mode] usage: esef_mode [dm|gg]")
return PLUGIN_HANDLED
}
SetIni(GG_INI, wantGG)
SetIni(DM_INI, !wantGG)
if (IsOn(GG_INI) != wantGG || IsOn(DM_INI) == wantGG)
{
server_print("[esef_mode] could not rename the plugin lists in %s", g_dir)
return PLUGIN_HANDLED
}
server_print("[esef_mode] switching to %s, reloading map in 3s", wantGG ? "gg" : "dm")
client_print_color(0, print_team_default, "^4[mode]^1 Switching to ^3%s^1 - map reloads in 3 seconds...", wantGG ? "GunGame" : "Deathmatch")
set_task(3.0, "Reload")
return PLUGIN_HANDLED
}
public Reload()
{
new map[32]
get_mapname(map, charsmax(map))
server_cmd("changelevel %s", map)
}
+197
View File
@@ -0,0 +1,197 @@
// rtv.sma - minimal "rock the vote" for cs16-dm-server.
// Players type rtv (or /rtv, rockthevote). When amx_rtv_ratio of the players
// agree, a 15s vote between up to 5 random mapcycle maps starts, then the map changes.
// Blocked early in a map (amx_rtv_delay) and near map end, where mapchooser's own vote runs.
#include <amxmodx>
#define VOTE_MAPS 5
#define VOTE_TIME 15
#define MENU_KEYS (MENU_KEY_1|MENU_KEY_2|MENU_KEY_3|MENU_KEY_4|MENU_KEY_5)
new bool:g_rocked[MAX_PLAYERS + 1]
new g_rocks
new bool:g_voting, bool:g_finished
new g_choices[VOTE_MAPS][32], g_numChoices, g_count[VOTE_MAPS]
new Float:g_mapStart
new g_pRatio, g_pDelay
public plugin_init()
{
register_plugin("RTV", "1.0", "cs16-dm-server")
register_clcmd("say rtv", "CmdRtv")
register_clcmd("say /rtv", "CmdRtv")
register_clcmd("say rockthevote", "CmdRtv")
register_clcmd("say_team rtv", "CmdRtv")
register_clcmd("say_team /rtv", "CmdRtv")
g_pRatio = register_cvar("amx_rtv_ratio", "0.6")
g_pDelay = register_cvar("amx_rtv_delay", "120")
register_menucmd(register_menuid("RTV Vote"), MENU_KEYS, "VoteHandler")
g_mapStart = get_gametime()
}
CountPlayers()
{
new players[MAX_PLAYERS], num
get_players(players, num, "ch")
return num
}
RocksNeeded()
{
new need = floatround(float(CountPlayers()) * get_pcvar_float(g_pRatio), floatround_ceil)
return need < 1 ? 1 : need
}
public CmdRtv(id)
{
if (g_voting || g_finished)
{
client_print_color(id, print_team_default, "^4[RTV]^1 A map vote already ran on this map.")
return PLUGIN_CONTINUE
}
new wait = get_pcvar_num(g_pDelay) - floatround(get_gametime() - g_mapStart)
if (wait > 0)
{
client_print_color(id, print_team_default, "^4[RTV]^1 Too early, try again in ^3%d^1 seconds.", wait)
return PLUGIN_CONTINUE
}
if (get_cvar_float("mp_timelimit") > 0.0 && get_timeleft() < 180)
{
client_print_color(id, print_team_default, "^4[RTV]^1 The end-of-map vote is about to start.")
return PLUGIN_CONTINUE
}
if (g_rocked[id])
{
client_print_color(id, print_team_default, "^4[RTV]^1 You already voted (^3%d^1/^3%d^1).", g_rocks, RocksNeeded())
return PLUGIN_CONTINUE
}
g_rocked[id] = true
g_rocks++
new name[32]
get_user_name(id, name, charsmax(name))
client_print_color(0, id, "^4[RTV]^3 %s^1 wants to change the map (^3%d^1/^3%d^1). Type ^4rtv^1 to agree.", name, g_rocks, RocksNeeded())
CheckRocks()
return PLUGIN_CONTINUE
}
public client_disconnected(id)
{
if (g_rocked[id])
{
g_rocked[id] = false
g_rocks--
}
// fewer players can mean the remaining votes are now enough
if (g_rocks > 0)
set_task(0.1, "CheckRocks")
}
public CheckRocks()
{
if (!g_voting && !g_finished && g_rocks > 0 && g_rocks >= RocksNeeded())
StartVote()
}
StartVote()
{
g_voting = true
new current[32], cyclefile[64]
get_mapname(current, charsmax(current))
get_cvar_string("mapcyclefile", cyclefile, charsmax(cyclefile))
new Array:maps = ArrayCreate(32)
new fp = fopen(cyclefile, "rt")
if (fp)
{
new line[64]
while (!feof(fp))
{
fgets(fp, line, charsmax(line))
trim(line)
if (!line[0] || line[0] == ';' || (line[0] == '/' && line[1] == '/'))
continue
if (equali(line, current) || !is_map_valid(line))
continue
ArrayPushString(maps, line)
}
fclose(fp)
}
g_numChoices = 0
while (g_numChoices < VOTE_MAPS && ArraySize(maps) > 0)
{
new pick = random(ArraySize(maps))
ArrayGetString(maps, pick, g_choices[g_numChoices], charsmax(g_choices[]))
ArrayDeleteItem(maps, pick)
g_count[g_numChoices] = 0
g_numChoices++
}
ArrayDestroy(maps)
if (!g_numChoices)
{
g_voting = false
client_print_color(0, print_team_default, "^4[RTV]^1 No other maps in the mapcycle.")
return
}
new menu[512], len, keys
len = formatex(menu, charsmax(menu), "\yRock the vote - pick the next map:^n^n")
for (new i = 0; i < g_numChoices; i++)
{
len += formatex(menu[len], charsmax(menu) - len, "\r%d.\w %s^n", i + 1, g_choices[i])
keys |= (1 << i)
}
show_menu(0, keys, menu, VOTE_TIME, "RTV Vote")
client_print_color(0, print_team_default, "^4[RTV]^1 Vote started! You have ^3%d^1 seconds.", VOTE_TIME)
set_task(float(VOTE_TIME) + 1.0, "FinishVote")
}
public VoteHandler(id, key)
{
if (!g_voting || key < 0 || key >= g_numChoices)
return PLUGIN_HANDLED
g_count[key]++
new name[32]
get_user_name(id, name, charsmax(name))
client_print_color(0, id, "^4[RTV]^3 %s^1 chose ^4%s", name, g_choices[key])
return PLUGIN_HANDLED
}
public FinishVote()
{
g_voting = false
g_finished = true
new best = 0
for (new i = 1; i < g_numChoices; i++)
{
if (g_count[i] > g_count[best])
best = i
}
if (!g_count[best])
best = random(g_numChoices)
set_cvar_string("amx_nextmap", g_choices[best])
client_print_color(0, print_team_default, "^4[RTV]^1 Next map: ^4%s^1 (^3%d^1 votes). Changing in 5 seconds...", g_choices[best], g_count[best])
set_task(5.0, "ChangeMap", best)
}
public ChangeMap(best)
{
server_cmd("changelevel %s", g_choices[best])
}