Promote server
csb-accuracy-tracker / addons / amxmodx / scripting / csb_accuracy_tracker.sma
241 lines 5.9 KiB Pawn Raw Permalink History
/*
 * CSB Accuracy Tracker
 * Copyright (C) 2026 counter-strike-boost.com
 *
 * Counts shots fired (Ham_Weapon_PrimaryAttack) and bullet hits
 * (Ham_TakeDamage with DMG_BULLET) per weapon for every player, keeps per-map
 * and lifetime totals (lifetime persisted in nVault per SteamID) and prints a
 * console table with /acc. Useful on public servers for spotting who actually
 * hits what they shoot at.
 *
 * Inspired by the weapon accuracy reporting of AMXX StatsX; independent GPL
 * re-implementation, no original code reused.
 *
 * 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 <hamsandwich>
#include <fakemeta>
#include <nvault>

new const PLUGIN[] = "CSB Accuracy Tracker"
new const VERSION[] = "1.0.0"
new const AUTHOR[]  = "counter-strike-boost.com"

#if !defined DMG_BULLET
#define DMG_BULLET (1<<1)
#endif

#define MAX_WEAPONS 24

new const g_szWeapons[][] =
{
    "weapon_glock18", "weapon_usp", "weapon_p228", "weapon_deagle",
    "weapon_fiveseven", "weapon_elite", "weapon_m3", "weapon_xm1014",
    "weapon_tmp", "weapon_mac10", "weapon_mp5navy", "weapon_ump45",
    "weapon_p90", "weapon_galil", "weapon_famas", "weapon_ak47",
    "weapon_m4a1", "weapon_scout", "weapon_sg552", "weapon_aug",
    "weapon_awp", "weapon_sg550", "weapon_g3sg1", "weapon_m249"
}

new g_iMapShots[33][MAX_WEAPONS]
new g_iMapHits[33][MAX_WEAPONS]
new g_iLifeShots[33][MAX_WEAPONS]
new g_iLifeHits[33][MAX_WEAPONS]

new g_iLastWeapon[33]
new g_iVault

public plugin_init()
{
    register_plugin(PLUGIN, VERSION, AUTHOR)

    register_cvar("csb_acc_enabled", "1")

    for (new w = 0; w < MAX_WEAPONS; w++)
        RegisterHam(Ham_Weapon_PrimaryAttack, g_szWeapons[w], "OnFire", 1)

    RegisterHam(Ham_TakeDamage, "player", "OnDamage", 1)

    register_clcmd("say /acc", "cmdAcc")
    register_clcmd("say_team /acc", "cmdAcc")

    g_iVault = nvault_open("csb_accuracy")
}

public plugin_end()
{
    if (g_iVault != -1)
        nvault_close(g_iVault)
}

public client_putinserver(id)
{
    for (new w = 0; w < MAX_WEAPONS; w++)
    {
        g_iMapShots[id][w]  = 0
        g_iMapHits[id][w]   = 0
        g_iLifeShots[id][w] = 0
        g_iLifeHits[id][w]  = 0
    }

    loadPlayer(id)
}

public client_disconnect(id)
{
    savePlayer(id)
}

public OnFire(ent)
{
    new id = pev(ent, pev_owner)

    if (id < 1 || id > 32 || !is_user_connected(id))
        return HAM_IGNORED

    new w = weaponIndex(ent)

    if (w == -1)
        return HAM_IGNORED

    g_iLastWeapon[id] = w
    g_iMapShots[id][w]++
    g_iLifeShots[id][w]++

    return HAM_IGNORED
}

public OnDamage(victim, inflictor, attacker, Float:damage, damagetype)
{
    if (attacker < 1 || attacker > 32 || attacker == victim)
        return HAM_IGNORED

    if (!(damagetype & DMG_BULLET))
        return HAM_IGNORED

    if (get_user_team(attacker) == get_user_team(victim))
        return HAM_IGNORED

    new w = g_iLastWeapon[attacker]

    if (w < 0 || w >= MAX_WEAPONS)
        return HAM_IGNORED

    g_iMapHits[attacker][w]++
    g_iLifeHits[attacker][w]++

    return HAM_IGNORED
}

weaponIndex(ent)
{
    static cls[32]
    pev(ent, pev_classname, cls, charsmax(cls))

    for (new w = 0; w < MAX_WEAPONS; w++)
    {
        if (equal(cls, g_szWeapons[w]))
            return w
    }

    return -1
}

public cmdAcc(id)
{
    client_print(id, print_console, " ")
    client_print(id, print_console, "----- CSB Accuracy (this map / lifetime) -----")

    for (new w = 0; w < MAX_WEAPONS; w++)
    {
        if (g_iMapShots[id][w] == 0 && g_iLifeShots[id][w] == 0)
            continue

        new name[24]
        copy(name, charsmax(name), g_szWeapons[w])
        replace(name, charsmax(name), "weapon_", "")

        new Float:mAcc = g_iMapShots[id][w] > 0
            ? 100.0 * float(g_iMapHits[id][w]) / float(g_iMapShots[id][w]) : 0.0
        new Float:lAcc = g_iLifeShots[id][w] > 0
            ? 100.0 * float(g_iLifeHits[id][w]) / float(g_iLifeShots[id][w]) : 0.0

        client_print(id, print_console, "%-12s  map %4d/%4d (%5.1f%%)   life %5d/%5d (%5.1f%%)",
            name, g_iMapHits[id][w], g_iMapShots[id][w], mAcc,
            g_iLifeHits[id][w], g_iLifeShots[id][w], lAcc)
    }

    client_print(id, print_chat, "[CSB] Accuracy table printed to your console.")
    return PLUGIN_HANDLED
}

loadPlayer(id)
{
    if (g_iVault == -1)
        return

    new auth[36], key[44], data[1536]
    get_user_authid(id, auth, charsmax(auth))

    if (auth[0] == 0 || equal(auth, "STEAM_ID_PENDING"))
        return

    formatex(key, charsmax(key), "acc_%s", auth)

    if (nvault_get(g_iVault, key, data, charsmax(data)) < 1)
        return

    /* data: "shots0 hits0 shots1 hits1 ..." */
    new pos = 0, w = 0
    new val[16]

    while (w < MAX_WEAPONS)
    {
        pos = argparse(data, pos, val, charsmax(val))

        if (pos == -1)
            break

        g_iLifeShots[id][w] = str_to_num(val)

        pos = argparse(data, pos, val, charsmax(val))

        if (pos == -1)
            break

        g_iLifeHits[id][w] = str_to_num(val)
        w++
    }
}

savePlayer(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), "acc_%s", auth)

    new data[1536], len
    len = 0

    for (new w = 0; w < MAX_WEAPONS; w++)
    {
        len += formatex(data[len], charsmax(data) - len, "%d %d ",
            g_iLifeShots[id][w], g_iLifeHits[id][w])
    }

    nvault_set(g_iVault, key, data)
}