Kreedz climb timer: start/end button detection, live HUD, checkpoints and per-map records in nVault.
csb_kz_timer.sma v1.0.0
1
/*
2
* CSB KZ Timer
3
* Copyright (C) 2026 counter-strike-boost.com
4
*
5
* A Kreedz (climb) timer. The run starts when a player presses a start button
6
* (targetname contains "start") and stops on the end/counter button. A live
7
* HUD shows the elapsed time and jump count, checkpoints let you save/teleport
8
* (/cp, /tp), and finish times are stored per map in nVault. /pb shows your
9
* personal best, /top shows the map's ten best runs.
10
*
11
* Inspired by the classic KZ Timer / xJ Timer plugins. Independent GPL
12
* re-implementation.
13
*
14
* This program is free software: you can redistribute it and/or modify it
15
* under the terms of the GNU General Public License as published by the Free
16
* Software Foundation, either version 3 of the License, or (at your option)
17
* any later version. Distributed WITHOUT ANY WARRANTY. See the GNU General
18
* Public License for more details: <https://www.gnu.org/licenses/>.
19
*/
20
21
#include <amxmodx>
22
#include <fakemeta>
23
#include <hamsandwich>
24
#include <fun>
25
#include <nvault>
26
27
new const PLUGIN[] = "CSB KZ Timer"
28
new const VERSION[] = "1.0.0"
29
new const AUTHOR[] = "counter-strike-boost.com"
30
31
#define TOP_MAX 10
32
33
new g_pEnabled
34
35
new bool:g_bRunning[33]
36
new Float:g_fStart[33]
37
new g_iJumps[33]
38
new bool:g_bHasCp[33]
39
new Float:g_fCp[33][3]
40
new Float:g_fCpAngles[33][3]
41
42
new g_iVault
43
new g_szMap[32]
44
45
// in-memory top table for the current map
46
new Float:g_fTopTime[TOP_MAX]
47
new g_szTopName[TOP_MAX][32]
48
new g_iTopCount
49
50
public plugin_init()
51
{
52
register_plugin(PLUGIN, VERSION, AUTHOR)
53
54
g_pEnabled = register_cvar("csb_kz_enabled", "1")
55
56
RegisterHam(Ham_Use, "func_button", "fwButtonUse", 1)
57
58
register_forward(FM_PlayerPreThink, "fwPreThink")
59
60
register_clcmd("say /cp", "cmdSaveCp")
61
register_clcmd("say /tp", "cmdTeleCp")
62
register_clcmd("say /start", "cmdRestart")
63
register_clcmd("say /pb", "cmdPersonalBest")
64
register_clcmd("say /top", "cmdTop")
65
66
get_mapname(g_szMap, charsmax(g_szMap))
67
g_iVault = nvault_open("csb_kz_timer")
68
69
loadTop()
70
71
set_task(0.1, "taskHud", _, _, _, "b")
72
}
73
74
public plugin_end()
75
{
76
if (g_iVault != INVALID_HANDLE)
77
nvault_close(g_iVault)
78
}
79
80
public client_putinserver(id)
81
{
82
resetRun(id)
83
g_bHasCp[id] = false
84
}
85
86
resetRun(id)
87
{
88
g_bRunning[id] = false
89
g_fStart[id] = 0.0
90
g_iJumps[id] = 0
91
}
92
93
public fwButtonUse(ent, idcaller, idactivator, use_type, Float:value)
94
{
95
if (!get_pcvar_num(g_pEnabled))
96
return HAM_IGNORED
97
98
if (idactivator < 1 || idactivator > 32 || !is_user_alive(idactivator))
99
return HAM_IGNORED
100
101
new tname[64]
102
pev(ent, pev_targetname, tname, charsmax(tname))
103
strtolower(tname)
104
105
if (contain(tname, "start") != -1)
106
{
107
startRun(idactivator)
108
}
109
else if (contain(tname, "end") != -1 || contain(tname, "finish") != -1 || contain(tname, "counter") != -1)
110
{
111
stopRun(idactivator)
112
}
113
114
return HAM_IGNORED
115
}
116
117
startRun(id)
118
{
119
g_bRunning[id] = true
120
g_fStart[id] = get_gametime()
121
g_iJumps[id] = 0
122
client_print(id, print_center, "Timer started -- go!")
123
}
124
125
stopRun(id)
126
{
127
if (!g_bRunning[id])
128
return
129
130
new Float:elapsed = get_gametime() - g_fStart[id]
131
g_bRunning[id] = false
132
133
new name[32]
134
get_user_name(id, name, charsmax(name))
135
136
new tbuf[16]
137
formatTime(elapsed, tbuf, charsmax(tbuf))
138
139
client_print(0, print_chat, "[KZ] %s finished %s in %s (%d jumps).", name, g_szMap, tbuf, g_iJumps[id])
140
141
savePersonal(id, elapsed)
142
tryInsertTop(name, elapsed)
143
}
144
145
public fwPreThink(id)
146
{
147
if (!is_user_alive(id))
148
return
149
150
new button = pev(id, pev_button)
151
new oldbutton = pev(id, pev_oldbuttons)
152
new flags = pev(id, pev_flags)
153
154
// count a take-off: jump pressed this frame, not last, while on ground
155
if ((button & IN_JUMP) && !(oldbutton & IN_JUMP) && (flags & FL_ONGROUND))
156
{
157
if (g_bRunning[id])
158
g_iJumps[id]++
159
}
160
}
161
162
public cmdSaveCp(id)
163
{
164
if (!is_user_alive(id))
165
return PLUGIN_HANDLED
166
167
pev(id, pev_origin, g_fCp[id])
168
pev(id, pev_v_angle, g_fCpAngles[id])
169
g_bHasCp[id] = true
170
client_print(id, print_center, "Checkpoint saved.")
171
return PLUGIN_HANDLED
172
}
173
174
public cmdTeleCp(id)
175
{
176
if (!is_user_alive(id))
177
return PLUGIN_HANDLED
178
179
if (!g_bHasCp[id])
180
{
181
client_print(id, print_chat, "[KZ] No checkpoint saved yet. Use /cp first.")
182
return PLUGIN_HANDLED
183
}
184
185
set_pev(id, pev_origin, g_fCp[id])
186
set_pev(id, pev_velocity, Float:{0.0, 0.0, 0.0})
187
set_pev(id, pev_v_angle, g_fCpAngles[id])
188
set_pev(id, pev_fixangle, 1)
189
client_print(id, print_center, "Teleported to checkpoint.")
190
return PLUGIN_HANDLED
191
}
192
193
public cmdRestart(id)
194
{
195
resetRun(id)
196
client_print(id, print_center, "Run reset. Hit the start button to begin.")
197
return PLUGIN_HANDLED
198
}
199
200
public cmdPersonalBest(id)
201
{
202
new authid[35], key[80], data[16], timestamp
203
get_user_authid(id, authid, charsmax(authid))
204
formatex(key, charsmax(key), "%s#%s", g_szMap, authid)
205
206
if (nvault_lookup(g_iVault, key, data, charsmax(data), timestamp))
207
{
208
new Float:best = str_to_float(data)
209
new tbuf[16]
210
formatTime(best, tbuf, charsmax(tbuf))
211
client_print(id, print_chat, "[KZ] Your personal best on %s: %s", g_szMap, tbuf)
212
}
213
else
214
{
215
client_print(id, print_chat, "[KZ] You have no recorded time on %s yet.", g_szMap)
216
}
217
return PLUGIN_HANDLED
218
}
219
220
public cmdTop(id)
221
{
222
if (g_iTopCount == 0)
223
{
224
client_print(id, print_chat, "[KZ] No records on %s yet -- be the first!", g_szMap)
225
return PLUGIN_HANDLED
226
}
227
228
client_print(id, print_chat, "[KZ] Top times on %s:", g_szMap)
229
for (new i = 0; i < g_iTopCount; i++)
230
{
231
new tbuf[16]
232
formatTime(g_fTopTime[i], tbuf, charsmax(tbuf))
233
client_print(id, print_chat, " %d. %s -- %s", i + 1, g_szTopName[i], tbuf)
234
}
235
return PLUGIN_HANDLED
236
}
237
238
savePersonal(id, Float:elapsed)
239
{
240
new authid[35], key[80], data[16], stored[16], timestamp
241
get_user_authid(id, authid, charsmax(authid))
242
formatex(key, charsmax(key), "%s#%s", g_szMap, authid)
243
244
new bool:save = true
245
if (nvault_lookup(g_iVault, key, stored, charsmax(stored), timestamp))
246
{
247
if (str_to_float(stored) <= elapsed)
248
save = false
249
}
250
251
if (save)
252
{
253
formatex(data, charsmax(data), "%f", elapsed)
254
nvault_set(g_iVault, key, data)
255
client_print(id, print_chat, "[KZ] New personal best saved!")
256
}
257
}
258
259
tryInsertTop(const name[], Float:elapsed)
260
{
261
// find insertion slot (ascending time)
262
new pos = -1
263
for (new i = 0; i < g_iTopCount; i++)
264
{
265
if (elapsed < g_fTopTime[i])
266
{
267
pos = i
268
break
269
}
270
}
271
272
if (pos == -1)
273
{
274
if (g_iTopCount >= TOP_MAX)
275
return
276
pos = g_iTopCount
277
}
278
279
if (g_iTopCount < TOP_MAX)
280
g_iTopCount++
281
282
// shift down from the end
283
for (new i = g_iTopCount - 1; i > pos; i--)
284
{
285
g_fTopTime[i] = g_fTopTime[i - 1]
286
copy(g_szTopName[i], charsmax(g_szTopName[]), g_szTopName[i - 1])
287
}
288
289
g_fTopTime[pos] = elapsed
290
copy(g_szTopName[pos], charsmax(g_szTopName[]), name)
291
292
saveTop()
293
}
294
295
saveTop()
296
{
297
// serialise as "time|name;time|name;..."
298
new blob[512], piece[80]
299
blob[0] = 0
300
301
for (new i = 0; i < g_iTopCount; i++)
302
{
303
formatex(piece, charsmax(piece), "%f|%s;", g_fTopTime[i], g_szTopName[i])
304
add(blob, charsmax(blob), piece)
305
}
306
307
new key[48]
308
formatex(key, charsmax(key), "TOP#%s", g_szMap)
309
nvault_set(g_iVault, key, blob)
310
}
311
312
loadTop()
313
{
314
g_iTopCount = 0
315
316
new key[48], blob[512], timestamp
317
formatex(key, charsmax(key), "TOP#%s", g_szMap)
318
319
if (!nvault_lookup(g_iVault, key, blob, charsmax(blob), timestamp))
320
return
321
322
new record[96], pos = 0
323
while (g_iTopCount < TOP_MAX && (pos = kzNext(blob, pos, ';', record, charsmax(record))))
324
{
325
if (!record[0])
326
continue
327
328
new bar = contain(record, "|")
329
if (bar < 1)
330
continue
331
332
new stime[24], sname[32]
333
new copylen = bar < charsmax(stime) ? bar : charsmax(stime)
334
copy(stime, copylen, record)
335
copy(sname, charsmax(sname), record[bar + 1])
336
337
g_fTopTime[g_iTopCount] = str_to_float(stime)
338
copy(g_szTopName[g_iTopCount], charsmax(g_szTopName[]), sname)
339
g_iTopCount++
340
}
341
}
342
343
// walk a delimited blob; returns new position after the delimiter, 0 at end
344
kzNext(const source[], start, delim, dest[], len)
345
{
346
new slen = strlen(source)
347
if (start >= slen)
348
return 0
349
350
new i = 0
351
while (start < slen && source[start] != delim)
352
{
353
if (i < len)
354
dest[i++] = source[start]
355
start++
356
}
357
dest[i] = 0
358
359
if (start < slen && source[start] == delim)
360
start++
361
362
return start
363
}
364
365
public taskHud()
366
{
367
if (!get_pcvar_num(g_pEnabled))
368
return
369
370
for (new id = 1; id <= 32; id++)
371
{
372
if (!is_user_alive(id) || !g_bRunning[id])
373
continue
374
375
new Float:elapsed = get_gametime() - g_fStart[id]
376
new tbuf[16]
377
formatTime(elapsed, tbuf, charsmax(tbuf))
378
379
set_hudmessage(0, 255, 100, 0.02, 0.25, 0, 0.0, 0.15, 0.0, 0.0, -1)
380
show_hudmessage(id, "Time: %s^nJumps: %d", tbuf, g_iJumps[id])
381
}
382
}
383
384
formatTime(Float:seconds, out[], len)
385
{
386
new total_ms = floatround(seconds * 1000.0)
387
new mins = total_ms / 60000
388
new secs = (total_ms / 1000) % 60
389
new ms = total_ms % 1000
390
formatex(out, len, "%02d:%02d.%03d", mins, secs, ms)
391
}
392









