Promouvoir serveur
csb-match-stats-export / addons / amxmodx / scripting / csb_match_stats_export.sma
238 lines 6.9 KiB Pawn Raw Permalink History
/*
 * 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 <https://www.gnu.org/licenses/> for details.
 */

#include <amxmodx>
#include <amxmisc>
#include <hamsandwich>
#include <sqlx>

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
}