Initial release 1.0.0
Browse Source1.0.0 — Initial release.
4 changed files
with 452 additions and 0 deletions
A
CHANGELOG.md
+3 -0
A
README.md
+1 -0
A
addons/amxmodx/scripting/csb_kz_timer.sma
+391 -0
A
csb.json
+57 -0
CHANGELOG.md
A
| @@ -0,0 +1,3 @@ | ||
| 1 | +## 1.0.0 (2025-12-17) | |
| 2 | + | |
| 3 | +1.0.0 — Initial release. |
README.md
A
| @@ -0,0 +1 @@ | ||
| 1 | +<p>A <strong>Kreedz (climb) timer</strong>. The run starts when a player presses a start button and stops on the end/counter button — detected by the button's <code>targetname</code> containing <code>start</code> or <code>end</code>/<code>finish</code>/<code>counter</code>, which is how most climb maps are built. A live HUD shows elapsed time and jump count, checkpoints let you practice, and finish times are stored per map in nVault.</p><p>Independent GPL re-implementation inspired by the classic <strong>KZ Timer / xJ Timer</strong> plugins.</p><h2>Features</h2><ul><li>Automatic start/stop via <code>Ham_Use</code> on <code>func_button</code> target names.</li><li>Live HUD timer (mm:ss.mmm) and jump counter.</li><li>Checkpoints: <code>/cp</code> to save, <code>/tp</code> to teleport back, <code>/start</code> to reset.</li><li>Personal best (<code>/pb</code>) and a top-ten (<code>/top</code>), persisted per map in nVault.</li></ul><h2>Installation</h2><ol><li>Put <code>csb_kz_timer.amxx</code> in <code>addons/amxmodx/plugins/</code>.</li><li>Add it to <code>plugins.ini</code>.</li><li>Make sure the <code>nvault</code> module is enabled in <code>modules.ini</code> (it is by default).</li><li>Change to a <code>kz_</code>/<code>climb_</code> map.</li></ol> |
addons/amxmodx/scripting/csb_kz_timer.sma
A
| @@ -0,0 +1,391 @@ | ||
| 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 | +} |
csb.json
A
| @@ -0,0 +1,57 @@ | ||
| 1 | +{ | |
| 2 | + "name": "CSB KZ Timer", | |
| 3 | + "category": "movement", | |
| 4 | + "tags": "kz, climb, timer", | |
| 5 | + "image": "", | |
| 6 | + "original_author": "KZ Timer / xJ Timer authors", | |
| 7 | + "original_url": "", | |
| 8 | + "license": "GPL-3.0", | |
| 9 | + "title": "CSB KZ Timer", | |
| 10 | + "short_desc": "Kreedz climb timer: start/end button detection, live HUD, checkpoints and per-map records in nVault.", | |
| 11 | + "version": "1.0.0", | |
| 12 | + "amxx_min": "1.9", | |
| 13 | + "amxx_compile_default": "1.9", | |
| 14 | + "rehlds_ok": true, | |
| 15 | + "hlds_ok": true, | |
| 16 | + "regamedll_required": false, | |
| 17 | + "metamod_flavor": "any", | |
| 18 | + "reapi_required": false, | |
| 19 | + "modules": "fakemeta,hamsandwich,fun,nvault", | |
| 20 | + "install_notes": "1. Put csb_kz_timer.amxx in addons/amxmodx/plugins/\n2. Add it to plugins.ini\n3. Ensure the nvault module is loaded\n4. Change to a kz_/climb_ map", | |
| 21 | + "known_conflicts": "Other timer/bhop plugins that also teleport or hook func_button use.", | |
| 22 | + "cvars": [ | |
| 23 | + { | |
| 24 | + "name": "csb_kz_enabled", | |
| 25 | + "default": "1", | |
| 26 | + "description": "Master switch for the KZ timer." | |
| 27 | + } | |
| 28 | + ], | |
| 29 | + "commands": [ | |
| 30 | + { | |
| 31 | + "name": "say /cp", | |
| 32 | + "access": "", | |
| 33 | + "description": "Save a checkpoint at your position." | |
| 34 | + }, | |
| 35 | + { | |
| 36 | + "name": "say /tp", | |
| 37 | + "access": "", | |
| 38 | + "description": "Teleport to your saved checkpoint." | |
| 39 | + }, | |
| 40 | + { | |
| 41 | + "name": "say /start", | |
| 42 | + "access": "", | |
| 43 | + "description": "Reset your run." | |
| 44 | + }, | |
| 45 | + { | |
| 46 | + "name": "say /pb", | |
| 47 | + "access": "", | |
| 48 | + "description": "Show your personal best for this map." | |
| 49 | + }, | |
| 50 | + { | |
| 51 | + "name": "say /top", | |
| 52 | + "access": "", | |
| 53 | + "description": "Show the map's top-ten times." | |
| 54 | + } | |
| 55 | + ], | |
| 56 | + "main": "addons/amxmodx/scripting/csb_kz_timer.sma" | |
| 57 | +} | |
| 0 | 58 |









