Daemon666 / CSB KZ Timer Публичен

Kreedz climb timer: start/end button detection, live HUD, checkpoints and per-map records in nVault.

v1.0.0 120 изтегляния GPL-3.0 Последна актуализация Aug 24, 2026
csb_kz_timer.sma v1.0.0 392 реда · 9.4 KB Суров
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