/* * 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 }