/*
* CSB Multikill Tracker
* Copyright (C) 2026 counter-strike-boost.com
*
* Detects double, triple and quad kills by counting a player's kills inside a
* short rolling time window, and aces by counting five kills in one round.
* Multikills are announced in chat with a sound; aces are persisted per
* SteamID in nVault and shown with /aces.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version. It is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See <https://www.gnu.org/licenses/> for details.
*/
#include <amxmodx>
#include <nvault>
new const PLUGIN[] = "CSB Multikill Tracker"
new const VERSION[] = "1.0.0"
new const AUTHOR[] = "counter-strike-boost.com"
#define WINDOW_KILLS 5
new Float:g_fKillTimes[33][WINDOW_KILLS]
new g_iKillPos[33]
new g_iRoundKills[33]
new g_iAces[33]
new g_iVault
new g_pWindow
public plugin_init()
{
register_plugin(PLUGIN, VERSION, AUTHOR)
g_pWindow = register_cvar("csb_multikill_window", "3.0")
register_event("DeathMsg", "onDeath", "a")
register_logevent("onRoundStart", 2, "1=Round_Start")
register_clcmd("say /aces", "cmdAces")
register_clcmd("say_team /aces", "cmdAces")
g_iVault = nvault_open("csb_multikill")
}
public plugin_end()
{
if (g_iVault != -1)
nvault_close(g_iVault)
}
public client_putinserver(id)
{
g_iRoundKills[id] = 0
g_iKillPos[id] = 0
g_iAces[id] = 0
for (new i = 0; i < WINDOW_KILLS; i++)
g_fKillTimes[id][i] = 0.0
loadPlayer(id)
}
public client_disconnect(id)
{
savePlayer(id)
}
public onRoundStart()
{
for (new id = 1; id <= 32; id++)
g_iRoundKills[id] = 0
}
public onDeath()
{
new killer = read_data(1)
new victim = read_data(2)
if (killer < 1 || killer > 32 || killer == victim)
return
if (get_user_team(killer) == get_user_team(victim))
return
new Float:now = get_gametime()
new Float:window = get_pcvar_float(g_pWindow)
/* record this kill in the ring buffer */
g_fKillTimes[killer][g_iKillPos[killer]] = now
g_iKillPos[killer] = (g_iKillPos[killer] + 1) % WINDOW_KILLS
/* count kills inside the window, and the newest one */
new inWindow = 0
for (new i = 0; i < WINDOW_KILLS; i++)
{
if (g_fKillTimes[killer][i] > 0.0 && now - g_fKillTimes[killer][i] <= window)
inWindow++
}
g_iRoundKills[killer]++
new name[32]
get_user_name(killer, name, charsmax(name))
if (inWindow == 2)
{
client_print(0, print_chat, "[CSB] %s gets a DOUBLE kill!", name)
client_cmd(0, "spk buttons/blip2.wav")
}
else if (inWindow == 3)
{
client_print(0, print_chat, "[CSB] %s is on fire - TRIPLE kill!", name)
client_cmd(0, "spk buttons/blip2.wav")
}
else if (inWindow >= 4)
{
client_print(0, print_chat, "[CSB] %s is UNSTOPPABLE - QUAD kill!", name)
client_cmd(0, "spk buttons/bell1.wav")
}
if (g_iRoundKills[killer] == 5)
{
g_iAces[killer]++
savePlayer(killer)
client_print(0, print_chat, "[CSB] ACE! %s wiped the whole enemy team this round!", name)
client_cmd(0, "spk buttons/bell1.wav")
set_hudmessage(255, 60, 60, -1.0, 0.32, 0, 0.0, 4.0, 0.5, 0.5, 2)
show_hudmessage(0, "ACE by %s!", name)
}
}
public cmdAces(id)
{
client_print(id, print_chat, "[CSB] You have %d ace(s).", g_iAces[id])
new best = 0, bestCount = 0
for (new p = 1; p <= 32; p++)
{
if (is_user_connected(p) && g_iAces[p] > bestCount)
{
bestCount = g_iAces[p]
best = p
}
}
if (best > 0)
{
new name[32]
get_user_name(best, name, charsmax(name))
client_print(id, print_chat, "[CSB] Server ace leader online: %s (%d).", name, bestCount)
}
return PLUGIN_HANDLED
}
loadPlayer(id)
{
if (g_iVault == -1)
return
new auth[36], key[44]
get_user_authid(id, auth, charsmax(auth))
if (auth[0] == 0 || equal(auth, "STEAM_ID_PENDING"))
return
formatex(key, charsmax(key), "aces_%s", auth)
g_iAces[id] = nvault_get(g_iVault, key)
}
savePlayer(id)
{
if (g_iVault == -1)
return
new auth[36], key[44], val[12]
get_user_authid(id, auth, charsmax(auth))
if (auth[0] == 0 || equal(auth, "STEAM_ID_PENDING"))
return
formatex(key, charsmax(key), "aces_%s", auth)
num_to_str(g_iAces[id], val, charsmax(val))
nvault_set(g_iVault, key, val)
}