Pushes end-of-map player stats to MySQL with one threaded multi-row INSERT for external ladder websites.
csb_stats_webexport.sma v1.0.0
1
/*
2
* CSB Stats Web Export
3
* Copyright (C) 2026 counter-strike-boost.com
4
*
5
* Pushes end-of-map player stats into a MySQL table so an external website
6
* can render ladders. At map end (plugin_end) all connected players are
7
* written in a single multi-row INSERT ... ON DUPLICATE KEY UPDATE through a
8
* threaded SQLx query, so the game thread never blocks on the database and
9
* the query count stays at one per map.
10
*
11
* Table layout (create once):
12
* CREATE TABLE csb_webstats (
13
* steamid VARCHAR(40) NOT NULL,
14
* name VARCHAR(32) NOT NULL DEFAULT '',
15
* kills INT NOT NULL DEFAULT 0,
16
* deaths INT NOT NULL DEFAULT 0,
17
* headshots INT NOT NULL DEFAULT 0,
18
* damage INT NOT NULL DEFAULT 0,
19
* seconds INT NOT NULL DEFAULT 0,
20
* PRIMARY KEY (steamid)
21
* );
22
*
23
* This program is free software: you can redistribute it and/or modify it under
24
* the terms of the GNU General Public License as published by the Free Software
25
* Foundation, either version 3 of the License, or (at your option) any later
26
* version. It is distributed in the hope that it will be useful, but WITHOUT
27
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
28
* FOR A PARTICULAR PURPOSE. See <https://www.gnu.org/licenses/> for details.
29
*/
30
31
#include <amxmodx>
32
#include <hamsandwich>
33
#include <sqlx>
34
35
new const PLUGIN[] = "CSB Stats Web Export"
36
new const VERSION[] = "1.0.0"
37
new const AUTHOR[] = "counter-strike-boost.com"
38
39
new g_iKills[33]
40
new g_iDeaths[33]
41
new g_iHs[33]
42
new g_iDamage[33]
43
new Float:g_fJoined[33]
44
45
new g_pHost, g_pUser, g_pPass, g_pDb
46
47
public plugin_init()
48
{
49
register_plugin(PLUGIN, VERSION, AUTHOR)
50
51
g_pHost = register_cvar("csb_sql_host", "localhost")
52
g_pUser = register_cvar("csb_sql_user", "amxx")
53
g_pPass = register_cvar("csb_sql_pass", "")
54
g_pDb = register_cvar("csb_sql_db", "csb_stats")
55
56
register_event("DeathMsg", "onDeath", "a")
57
RegisterHam(Ham_TakeDamage, "player", "OnDamage", 1)
58
}
59
60
public client_putinserver(id)
61
{
62
g_iKills[id] = 0
63
g_iDeaths[id] = 0
64
g_iHs[id] = 0
65
g_iDamage[id] = 0
66
g_fJoined[id] = get_gametime()
67
}
68
69
public onDeath()
70
{
71
new killer = read_data(1)
72
new victim = read_data(2)
73
new hs = read_data(3)
74
75
if (victim < 1 || victim > 32)
76
return
77
78
g_iDeaths[victim]++
79
80
if (killer >= 1 && killer <= 32 && killer != victim
81
&& get_user_team(killer) != get_user_team(victim))
82
{
83
g_iKills[killer]++
84
85
if (hs)
86
g_iHs[killer]++
87
}
88
}
89
90
public OnDamage(victim, inflictor, attacker, Float:damage, damagetype)
91
{
92
if (attacker < 1 || attacker > 32 || attacker == victim)
93
return HAM_IGNORED
94
95
if (get_user_team(attacker) == get_user_team(victim))
96
return HAM_IGNORED
97
98
g_iDamage[attacker] += floatround(damage)
99
100
return HAM_IGNORED
101
}
102
103
public plugin_end()
104
{
105
exportStats()
106
}
107
108
exportStats()
109
{
110
new host[64], user[64], pass[64], db[64]
111
get_pcvar_string(g_pHost, host, charsmax(host))
112
get_pcvar_string(g_pUser, user, charsmax(user))
113
get_pcvar_string(g_pPass, pass, charsmax(pass))
114
get_pcvar_string(g_pDb, db, charsmax(db))
115
116
new Handle:tuple = SQL_MakeDbTuple(host, user, pass, db)
117
118
static query[4096]
119
new len
120
121
len = formatex(query, charsmax(query),
122
"INSERT INTO csb_webstats (steamid, name, kills, deaths, headshots, damage, seconds) VALUES ")
123
124
new rows = 0
125
126
for (new id = 1; id <= 32; id++)
127
{
128
if (!is_user_connected(id) || is_user_bot(id))
129
continue
130
131
new auth[36], name[32], ename[64]
132
get_user_authid(id, auth, charsmax(auth))
133
get_user_name(id, name, charsmax(name))
134
135
/* cheap escape: single quotes are doubled */
136
copy(ename, charsmax(ename), name)
137
replace_all(ename, charsmax(ename), "'", "''")
138
139
new secs = floatround(get_gametime() - g_fJoined[id])
140
141
len += formatex(query[len], charsmax(query) - len,
142
"%s('%s','%s',%d,%d,%d,%d,%d)",
143
rows > 0 ? "," : "",
144
auth, ename, g_iKills[id], g_iDeaths[id], g_iHs[id], g_iDamage[id], secs)
145
146
rows++
147
148
if (len > charsmax(query) - 200)
149
break
150
}
151
152
if (rows < 1)
153
{
154
SQL_FreeHandle(tuple)
155
return
156
}
157
158
len += formatex(query[len], charsmax(query) - len,
159
" ON DUPLICATE KEY UPDATE name=VALUES(name), kills=kills+VALUES(kills), deaths=deaths+VALUES(deaths), headshots=headshots+VALUES(headshots), damage=damage+VALUES(damage), seconds=seconds+VALUES(seconds)")
160
161
SQL_ThreadQuery(tuple, "onExported", query)
162
}
163
164
public onExported(failstate, Handle:query, error[], errcode, data[], datasize)
165
{
166
if (failstate != TQUERY_SUCCESS)
167
log_amx("[CSB WebExport] query failed (%d): %s", errcode, error)
168
}
169









