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