/*
* CSB Rank System
* Copyright (C) 2026 counter-strike-boost.com
*
* Persistent per-SteamID statistics (kills, deaths, headshots, damage dealt,
* rounds played and time on the server) stored in nVault. Stats are loaded when
* a client authorises and saved when he leaves or the map ends. A weighted
* score turns the raw numbers into a named rank, shown with "say /rank", and a
* small set of natives lets other CSB plugins read a player's score and his
* position on the current server.
*
* Inspired by the classic CSStats / rank plugins (e.g. Zhorka's rank system).
* This is an independent GPL re-implementation; no original code is 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 for details.
*/
#include
#include
#include
new const PLUGIN[] = "CSB Rank System"
new const VERSION[] = "1.0.0"
new const AUTHOR[] = "counter-strike-boost.com"
#define NO_VAULT (-1)
enum _:Stats
{
ST_KILLS,
ST_DEATHS,
ST_HS,
ST_DAMAGE,
ST_ROUNDS,
ST_TIME /* seconds */
}
new g_iStats[33][Stats]
new g_iConnect[33]
new g_szAuth[33][35]
new bool:g_bLoaded[33]
new g_iVault = NO_VAULT
new const g_szRanks[][] =
{
"Newbie",
"Rookie",
"Soldier",
"Veteran",
"Elite",
"Master",
"Legend"
}
new const g_iRankScore[] =
{
0,
250,
1000,
3000,
7000,
15000,
30000
}
public plugin_init()
{
register_plugin(PLUGIN, VERSION, AUTHOR)
register_event("DeathMsg", "onDeath", "a")
register_event("HLTV", "onNewRound", "a", "1=0", "2=0")
RegisterHam(Ham_TakeDamage, "player", "fw_TakeDamage")
register_clcmd("say /rank", "cmdRank")
register_clcmd("say_team /rank", "cmdRank")
register_clcmd("say /stats", "cmdRank")
}
public plugin_natives()
{
register_native("csb_get_score", "native_get_score")
register_native("csb_get_position", "native_get_position")
register_native("csb_get_kills", "native_get_kills")
}
public plugin_cfg()
{
g_iVault = nvault_open("csb_ranks")
if (g_iVault == NO_VAULT)
log_amx("[CSB Rank] could not open nVault 'csb_ranks' - stats will not persist.")
}
public plugin_end()
{
/* save everyone still on the server, then close the vault */
for (new id = 1; id <= 32; id++)
if (g_bLoaded[id])
saveStats(id)
if (g_iVault != NO_VAULT)
nvault_close(g_iVault)
}
/* ---------- load / save ---------- */
public client_authorized(id)
{
resetSlot(id)
if (is_user_bot(id) || is_user_hltv(id))
return
get_user_authid(id, g_szAuth[id], charsmax(g_szAuth[]))
if (g_iVault != NO_VAULT && g_szAuth[id][0])
{
new buffer[64]
if (nvault_get(g_iVault, g_szAuth[id], buffer, charsmax(buffer)))
unpackStats(id, buffer)
}
g_iConnect[id] = get_systime()
g_bLoaded[id] = true
}
public client_disconnected(id)
{
if (g_bLoaded[id])
saveStats(id)
resetSlot(id)
}
saveStats(id)
{
if (g_iVault == NO_VAULT || !g_szAuth[id][0])
return
/* fold the current session's playtime in before writing */
if (g_iConnect[id])
{
g_iStats[id][ST_TIME] += get_systime() - g_iConnect[id]
g_iConnect[id] = get_systime()
}
new buffer[64]
formatex(buffer, charsmax(buffer), "%d %d %d %d %d %d",
g_iStats[id][ST_KILLS], g_iStats[id][ST_DEATHS], g_iStats[id][ST_HS],
g_iStats[id][ST_DAMAGE], g_iStats[id][ST_ROUNDS], g_iStats[id][ST_TIME])
nvault_pset(g_iVault, g_szAuth[id], buffer)
}
resetSlot(id)
{
for (new i = 0; i < Stats; i++)
g_iStats[id][i] = 0
g_iConnect[id] = 0
g_szAuth[id][0] = 0
g_bLoaded[id] = false
}
unpackStats(id, const buffer[])
{
new part[6][12]
new count = 0, i = 0, j = 0
while (buffer[i] && count < 6)
{
if (buffer[i] == ' ')
{
part[count][j] = 0
count++
j = 0
i++
continue
}
if (j < charsmax(part[]))
part[count][j++] = buffer[i]
i++
}
if (count < 6)
part[count][j] = 0
g_iStats[id][ST_KILLS] = str_to_num(part[0])
g_iStats[id][ST_DEATHS] = str_to_num(part[1])
g_iStats[id][ST_HS] = str_to_num(part[2])
g_iStats[id][ST_DAMAGE] = str_to_num(part[3])
g_iStats[id][ST_ROUNDS] = str_to_num(part[4])
g_iStats[id][ST_TIME] = str_to_num(part[5])
}
/* ---------- gameplay hooks ---------- */
public onDeath()
{
new killer = read_data(1)
new victim = read_data(2)
new headshot = read_data(3)
if (victim >= 1 && victim <= 32 && g_bLoaded[victim])
g_iStats[victim][ST_DEATHS]++
if (killer >= 1 && killer <= 32 && killer != victim && g_bLoaded[killer])
{
g_iStats[killer][ST_KILLS]++
if (headshot)
g_iStats[killer][ST_HS]++
}
}
public fw_TakeDamage(victim, inflictor, attacker, Float:damage, damagebits)
{
if (attacker < 1 || attacker > 32 || attacker == victim || !g_bLoaded[attacker])
return HAM_IGNORED
g_iStats[attacker][ST_DAMAGE] += floatround(damage)
return HAM_IGNORED
}
public onNewRound()
{
for (new id = 1; id <= 32; id++)
if (g_bLoaded[id])
g_iStats[id][ST_ROUNDS]++
}
/* ---------- score / rank ---------- */
scoreOf(id)
{
return g_iStats[id][ST_KILLS] * 2
+ g_iStats[id][ST_HS]
+ g_iStats[id][ST_DAMAGE] / 50
+ g_iStats[id][ST_ROUNDS]
- g_iStats[id][ST_DEATHS]
}
rankTitle(score, title[], len)
{
new idx = 0
for (new i = 0; i < sizeof(g_iRankScore); i++)
if (score >= g_iRankScore[i])
idx = i
copy(title, len, g_szRanks[idx])
}
positionOf(id)
{
new myScore = scoreOf(id)
new pos = 1
new players[32], num
get_players(players, num, "ch")
for (new i = 0; i < num; i++)
{
new pid = players[i]
if (pid == id || !g_bLoaded[pid])
continue
if (scoreOf(pid) > myScore)
pos++
}
return pos
}
public cmdRank(id)
{
if (!g_bLoaded[id])
{
client_print(id, print_chat, "[CSB] Your stats are not loaded yet.")
return PLUGIN_HANDLED
}
new score = scoreOf(id)
new title[24]
rankTitle(score, title, charsmax(title))
new online, dummy[32], num
get_players(dummy, num, "ch")
online = num
new mins = g_iStats[id][ST_TIME] / 60
if (g_iConnect[id])
mins += (get_systime() - g_iConnect[id]) / 60
client_print_color(id, print_team_default,
"^x04[CSB]^x03 Rank:^x04 %s^x01 - score^x04 %d^x01 - position^x04 %d^x01/%d",
title, score, positionOf(id), online)
client_print(id, print_chat,
"[CSB] Kills: %d | Deaths: %d | HS: %d | Damage: %d | Rounds: %d | Time: %d min",
g_iStats[id][ST_KILLS], g_iStats[id][ST_DEATHS], g_iStats[id][ST_HS],
g_iStats[id][ST_DAMAGE], g_iStats[id][ST_ROUNDS], mins)
return PLUGIN_HANDLED
}
/* ---------- natives ---------- */
public native_get_score(plugin, params)
{
new id = get_param(1)
if (id < 1 || id > 32 || !g_bLoaded[id])
return 0
return scoreOf(id)
}
public native_get_position(plugin, params)
{
new id = get_param(1)
if (id < 1 || id > 32 || !g_bLoaded[id])
return 0
return positionOf(id)
}
public native_get_kills(plugin, params)
{
new id = get_param(1)
if (id < 1 || id > 32 || !g_bLoaded[id])
return 0
return g_iStats[id][ST_KILLS]
}