/*
* CSB Match Stats Export
* Copyright (C) 2026 counter-strike-boost.com
*
* At the end of a match it writes the full scoreboard (per-player kills, deaths,
* headshots, damage and ADR) into MySQL with threaded SQLx and a numeric match
* id, so a website can render the match page. If the database is not configured
* it falls back to a JSON-ish text file under addons/amxmodx/logs.
*
* 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 for details.
*/
#include
#include
#include
#include
new const PLUGIN[] = "CSB Match Stats Export"
new const VERSION[] = "1.0.0"
new const AUTHOR[] = "counter-strike-boost.com"
new const TABLE[] = "csb_match_players"
new g_pHost, g_pUser, g_pPass, g_pDb
new Handle:g_hTuple = Empty_Handle
new g_iKills[33], g_iDeaths[33], g_iHs[33], g_iDamage[33]
new g_iRounds
new g_szAuth[33][35]
public plugin_init()
{
register_plugin(PLUGIN, VERSION, AUTHOR)
g_pHost = register_cvar("csb_export_host", "127.0.0.1")
g_pUser = register_cvar("csb_export_user", "csb")
g_pPass = register_cvar("csb_export_pass", "")
g_pDb = register_cvar("csb_export_db", "csb")
register_event("DeathMsg", "evDeath", "a")
register_logevent("evRoundStart", 2, "1=Round_Start")
RegisterHam(Ham_TakeDamage, "player", "fwDamage")
register_concmd("amx_exportstats", "cmdExport", ADMIN_BAN, "- export the current match scoreboard")
register_concmd("amx_resetstats", "cmdReset", ADMIN_BAN, "- clear tracked match stats")
}
public plugin_cfg()
{
new host[64], user[64], pass[64], db[64]
get_pcvar_string(g_pHost, host, charsmax(host))
get_pcvar_string(g_pUser, user, charsmax(user))
get_pcvar_string(g_pPass, pass, charsmax(pass))
get_pcvar_string(g_pDb, db, charsmax(db))
if (host[0] && db[0])
{
g_hTuple = SQL_MakeDbTuple(host, user, pass, db)
new q[640]
formatex(q, charsmax(q), "CREATE TABLE IF NOT EXISTS `%s` (`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `match_id` INT UNSIGNED NOT NULL, `map` VARCHAR(32) NOT NULL DEFAULT '', `authid` VARCHAR(34) NOT NULL DEFAULT '', `name` VARCHAR(32) NOT NULL DEFAULT '', `kills` INT NOT NULL DEFAULT 0, `deaths` INT NOT NULL DEFAULT 0, `hs` INT NOT NULL DEFAULT 0, `damage` INT NOT NULL DEFAULT 0, `adr` INT NOT NULL DEFAULT 0, KEY `k_match` (`match_id`)) ENGINE=InnoDB", TABLE)
SQL_ThreadQuery(g_hTuple, "handlerSimple", q)
}
}
public plugin_end()
{
if (g_hTuple != Empty_Handle)
SQL_FreeHandle(g_hTuple)
}
public client_authorized(id)
{
get_user_authid(id, g_szAuth[id], charsmax(g_szAuth[]))
}
public client_putinserver(id)
{
g_iKills[id] = 0
g_iDeaths[id] = 0
g_iHs[id] = 0
g_iDamage[id] = 0
}
public evRoundStart()
{
g_iRounds++
}
public evDeath()
{
new killer = read_data(1)
new victim = read_data(2)
new hs = read_data(3)
if (victim >= 1 && victim <= 32)
g_iDeaths[victim]++
if (killer >= 1 && killer <= 32 && killer != victim)
{
g_iKills[killer]++
if (hs)
g_iHs[killer]++
}
}
public fwDamage(victim, inflictor, attacker, Float:damage, damagebits)
{
if (attacker >= 1 && attacker <= 32 && attacker != victim)
g_iDamage[attacker] += floatround(damage)
return HAM_IGNORED
}
public cmdReset(id, level, cid)
{
if (!cmd_access(id, level, cid, 1))
return PLUGIN_HANDLED
for (new i = 1; i <= 32; i++)
{
g_iKills[i] = 0
g_iDeaths[i] = 0
g_iHs[i] = 0
g_iDamage[i] = 0
}
g_iRounds = 0
console_print(id, "[CSB] Match stats cleared.")
return PLUGIN_HANDLED
}
public cmdExport(id, level, cid)
{
if (!cmd_access(id, level, cid, 1))
return PLUGIN_HANDLED
new matchId = get_systime()
new rounds = g_iRounds > 0 ? g_iRounds : 1
new mapname[32]
get_mapname(mapname, charsmax(mapname))
new players[32], num
get_players(players, num, "ch")
if (g_hTuple == Empty_Handle)
{
exportFile(matchId, mapname, players, num, rounds)
console_print(id, "[CSB] DB not configured; wrote match %d to a text file.", matchId)
return PLUGIN_HANDLED
}
new safeMap[70]
sqlEscape(mapname, safeMap, charsmax(safeMap))
for (new i = 0; i < num; i++)
{
new pid = players[i]
new adr = g_iDamage[pid] / rounds
new name[32], safeName[70], safeAuth[70]
get_user_name(pid, name, charsmax(name))
sqlEscape(name, safeName, charsmax(safeName))
sqlEscape(g_szAuth[pid], safeAuth, charsmax(safeAuth))
new q[600]
formatex(q, charsmax(q), "INSERT INTO `%s` (`match_id`, `map`, `authid`, `name`, `kills`, `deaths`, `hs`, `damage`, `adr`) VALUES (%d, '%s', '%s', '%s', %d, %d, %d, %d, %d)",
TABLE, matchId, safeMap, safeAuth, safeName,
g_iKills[pid], g_iDeaths[pid], g_iHs[pid], g_iDamage[pid], adr)
SQL_ThreadQuery(g_hTuple, "handlerSimple", q)
}
console_print(id, "[CSB] Exported %d players for match id %d.", num, matchId)
client_print(0, print_chat, "[CSB] Match scoreboard exported (id %d).", matchId)
return PLUGIN_HANDLED
}
exportFile(matchId, const mapname[], players[], num, rounds)
{
new path[128]
formatex(path, charsmax(path), "addons/amxmodx/logs/csb_match_%d.txt", matchId)
new fp = fopen(path, "wt")
if (!fp)
return
fprintf(fp, "{^n ^"match_id^": %d,^n ^"map^": ^"%s^",^n ^"players^": [^n", matchId, mapname)
for (new i = 0; i < num; i++)
{
new pid = players[i]
new adr = g_iDamage[pid] / rounds
new name[32]
get_user_name(pid, name, charsmax(name))
fprintf(fp, " {^"name^": ^"%s^", ^"authid^": ^"%s^", ^"kills^": %d, ^"deaths^": %d, ^"hs^": %d, ^"damage^": %d, ^"adr^": %d}%s^n",
name, g_szAuth[pid], g_iKills[pid], g_iDeaths[pid], g_iHs[pid], g_iDamage[pid], adr,
(i < num - 1) ? "," : "")
}
fprintf(fp, " ]^n}^n")
fclose(fp)
}
public handlerSimple(failstate, Handle:query, error[], errnum, data[], size, Float:queuetime)
{
if (failstate != TQUERY_SUCCESS)
log_amx("[CSB Export] query failed (%d): %s", errnum, error)
}
stock sqlEscape(const input[], output[], len)
{
new i = 0, j = 0, c
while (input[i] != 0 && j < len)
{
c = input[i]
if (c == 39 || c == 34 || c == 92 || c == 59 || c == 37 || c == 96 || c < 32)
{
i++
continue
}
output[j++] = c
i++
}
output[j] = 0
}