Twój serwer tutaj Licytuj w cotygodniowej aukcji Promuj serwer
csb-kz-timer / addons / amxmodx / scripting / csb_kz_timer.sma
391 lines 9.4 KiB Pawn Raw Permalink History
/*
 * CSB KZ Timer
 * Copyright (C) 2026 counter-strike-boost.com
 *
 * A Kreedz (climb) timer. The run starts when a player presses a start button
 * (targetname contains "start") and stops on the end/counter button. A live
 * HUD shows the elapsed time and jump count, checkpoints let you save/teleport
 * (/cp, /tp), and finish times are stored per map in nVault. /pb shows your
 * personal best, /top shows the map's ten best runs.
 *
 * Inspired by the classic KZ Timer / xJ Timer plugins. Independent GPL
 * re-implementation.
 *
 * 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. Distributed WITHOUT ANY WARRANTY. See the GNU General
 * Public License for more details: <https://www.gnu.org/licenses/>.
 */

#include <amxmodx>
#include <fakemeta>
#include <hamsandwich>
#include <fun>
#include <nvault>

new const PLUGIN[] = "CSB KZ Timer"
new const VERSION[] = "1.0.0"
new const AUTHOR[]  = "counter-strike-boost.com"

#define TOP_MAX 10

new g_pEnabled

new bool:g_bRunning[33]
new Float:g_fStart[33]
new g_iJumps[33]
new bool:g_bHasCp[33]
new Float:g_fCp[33][3]
new Float:g_fCpAngles[33][3]

new g_iVault
new g_szMap[32]

// in-memory top table for the current map
new Float:g_fTopTime[TOP_MAX]
new g_szTopName[TOP_MAX][32]
new g_iTopCount

public plugin_init()
{
    register_plugin(PLUGIN, VERSION, AUTHOR)

    g_pEnabled = register_cvar("csb_kz_enabled", "1")

    RegisterHam(Ham_Use, "func_button", "fwButtonUse", 1)

    register_forward(FM_PlayerPreThink, "fwPreThink")

    register_clcmd("say /cp", "cmdSaveCp")
    register_clcmd("say /tp", "cmdTeleCp")
    register_clcmd("say /start", "cmdRestart")
    register_clcmd("say /pb", "cmdPersonalBest")
    register_clcmd("say /top", "cmdTop")

    get_mapname(g_szMap, charsmax(g_szMap))
    g_iVault = nvault_open("csb_kz_timer")

    loadTop()

    set_task(0.1, "taskHud", _, _, _, "b")
}

public plugin_end()
{
    if (g_iVault != INVALID_HANDLE)
        nvault_close(g_iVault)
}

public client_putinserver(id)
{
    resetRun(id)
    g_bHasCp[id] = false
}

resetRun(id)
{
    g_bRunning[id] = false
    g_fStart[id] = 0.0
    g_iJumps[id] = 0
}

public fwButtonUse(ent, idcaller, idactivator, use_type, Float:value)
{
    if (!get_pcvar_num(g_pEnabled))
        return HAM_IGNORED

    if (idactivator < 1 || idactivator > 32 || !is_user_alive(idactivator))
        return HAM_IGNORED

    new tname[64]
    pev(ent, pev_targetname, tname, charsmax(tname))
    strtolower(tname)

    if (contain(tname, "start") != -1)
    {
        startRun(idactivator)
    }
    else if (contain(tname, "end") != -1 || contain(tname, "finish") != -1 || contain(tname, "counter") != -1)
    {
        stopRun(idactivator)
    }

    return HAM_IGNORED
}

startRun(id)
{
    g_bRunning[id] = true
    g_fStart[id] = get_gametime()
    g_iJumps[id] = 0
    client_print(id, print_center, "Timer started -- go!")
}

stopRun(id)
{
    if (!g_bRunning[id])
        return

    new Float:elapsed = get_gametime() - g_fStart[id]
    g_bRunning[id] = false

    new name[32]
    get_user_name(id, name, charsmax(name))

    new tbuf[16]
    formatTime(elapsed, tbuf, charsmax(tbuf))

    client_print(0, print_chat, "[KZ] %s finished %s in %s (%d jumps).", name, g_szMap, tbuf, g_iJumps[id])

    savePersonal(id, elapsed)
    tryInsertTop(name, elapsed)
}

public fwPreThink(id)
{
    if (!is_user_alive(id))
        return

    new button = pev(id, pev_button)
    new oldbutton = pev(id, pev_oldbuttons)
    new flags = pev(id, pev_flags)

    // count a take-off: jump pressed this frame, not last, while on ground
    if ((button & IN_JUMP) && !(oldbutton & IN_JUMP) && (flags & FL_ONGROUND))
    {
        if (g_bRunning[id])
            g_iJumps[id]++
    }
}

public cmdSaveCp(id)
{
    if (!is_user_alive(id))
        return PLUGIN_HANDLED

    pev(id, pev_origin, g_fCp[id])
    pev(id, pev_v_angle, g_fCpAngles[id])
    g_bHasCp[id] = true
    client_print(id, print_center, "Checkpoint saved.")
    return PLUGIN_HANDLED
}

public cmdTeleCp(id)
{
    if (!is_user_alive(id))
        return PLUGIN_HANDLED

    if (!g_bHasCp[id])
    {
        client_print(id, print_chat, "[KZ] No checkpoint saved yet. Use /cp first.")
        return PLUGIN_HANDLED
    }

    set_pev(id, pev_origin, g_fCp[id])
    set_pev(id, pev_velocity, Float:{0.0, 0.0, 0.0})
    set_pev(id, pev_v_angle, g_fCpAngles[id])
    set_pev(id, pev_fixangle, 1)
    client_print(id, print_center, "Teleported to checkpoint.")
    return PLUGIN_HANDLED
}

public cmdRestart(id)
{
    resetRun(id)
    client_print(id, print_center, "Run reset. Hit the start button to begin.")
    return PLUGIN_HANDLED
}

public cmdPersonalBest(id)
{
    new authid[35], key[80], data[16], timestamp
    get_user_authid(id, authid, charsmax(authid))
    formatex(key, charsmax(key), "%s#%s", g_szMap, authid)

    if (nvault_lookup(g_iVault, key, data, charsmax(data), timestamp))
    {
        new Float:best = str_to_float(data)
        new tbuf[16]
        formatTime(best, tbuf, charsmax(tbuf))
        client_print(id, print_chat, "[KZ] Your personal best on %s: %s", g_szMap, tbuf)
    }
    else
    {
        client_print(id, print_chat, "[KZ] You have no recorded time on %s yet.", g_szMap)
    }
    return PLUGIN_HANDLED
}

public cmdTop(id)
{
    if (g_iTopCount == 0)
    {
        client_print(id, print_chat, "[KZ] No records on %s yet -- be the first!", g_szMap)
        return PLUGIN_HANDLED
    }

    client_print(id, print_chat, "[KZ] Top times on %s:", g_szMap)
    for (new i = 0; i < g_iTopCount; i++)
    {
        new tbuf[16]
        formatTime(g_fTopTime[i], tbuf, charsmax(tbuf))
        client_print(id, print_chat, "  %d. %s -- %s", i + 1, g_szTopName[i], tbuf)
    }
    return PLUGIN_HANDLED
}

savePersonal(id, Float:elapsed)
{
    new authid[35], key[80], data[16], stored[16], timestamp
    get_user_authid(id, authid, charsmax(authid))
    formatex(key, charsmax(key), "%s#%s", g_szMap, authid)

    new bool:save = true
    if (nvault_lookup(g_iVault, key, stored, charsmax(stored), timestamp))
    {
        if (str_to_float(stored) <= elapsed)
            save = false
    }

    if (save)
    {
        formatex(data, charsmax(data), "%f", elapsed)
        nvault_set(g_iVault, key, data)
        client_print(id, print_chat, "[KZ] New personal best saved!")
    }
}

tryInsertTop(const name[], Float:elapsed)
{
    // find insertion slot (ascending time)
    new pos = -1
    for (new i = 0; i < g_iTopCount; i++)
    {
        if (elapsed < g_fTopTime[i])
        {
            pos = i
            break
        }
    }

    if (pos == -1)
    {
        if (g_iTopCount >= TOP_MAX)
            return
        pos = g_iTopCount
    }

    if (g_iTopCount < TOP_MAX)
        g_iTopCount++

    // shift down from the end
    for (new i = g_iTopCount - 1; i > pos; i--)
    {
        g_fTopTime[i] = g_fTopTime[i - 1]
        copy(g_szTopName[i], charsmax(g_szTopName[]), g_szTopName[i - 1])
    }

    g_fTopTime[pos] = elapsed
    copy(g_szTopName[pos], charsmax(g_szTopName[]), name)

    saveTop()
}

saveTop()
{
    // serialise as "time|name;time|name;..."
    new blob[512], piece[80]
    blob[0] = 0

    for (new i = 0; i < g_iTopCount; i++)
    {
        formatex(piece, charsmax(piece), "%f|%s;", g_fTopTime[i], g_szTopName[i])
        add(blob, charsmax(blob), piece)
    }

    new key[48]
    formatex(key, charsmax(key), "TOP#%s", g_szMap)
    nvault_set(g_iVault, key, blob)
}

loadTop()
{
    g_iTopCount = 0

    new key[48], blob[512], timestamp
    formatex(key, charsmax(key), "TOP#%s", g_szMap)

    if (!nvault_lookup(g_iVault, key, blob, charsmax(blob), timestamp))
        return

    new record[96], pos = 0
    while (g_iTopCount < TOP_MAX && (pos = kzNext(blob, pos, ';', record, charsmax(record))))
    {
        if (!record[0])
            continue

        new bar = contain(record, "|")
        if (bar < 1)
            continue

        new stime[24], sname[32]
        new copylen = bar < charsmax(stime) ? bar : charsmax(stime)
        copy(stime, copylen, record)
        copy(sname, charsmax(sname), record[bar + 1])

        g_fTopTime[g_iTopCount] = str_to_float(stime)
        copy(g_szTopName[g_iTopCount], charsmax(g_szTopName[]), sname)
        g_iTopCount++
    }
}

// walk a delimited blob; returns new position after the delimiter, 0 at end
kzNext(const source[], start, delim, dest[], len)
{
    new slen = strlen(source)
    if (start >= slen)
        return 0

    new i = 0
    while (start < slen && source[start] != delim)
    {
        if (i < len)
            dest[i++] = source[start]
        start++
    }
    dest[i] = 0

    if (start < slen && source[start] == delim)
        start++

    return start
}

public taskHud()
{
    if (!get_pcvar_num(g_pEnabled))
        return

    for (new id = 1; id <= 32; id++)
    {
        if (!is_user_alive(id) || !g_bRunning[id])
            continue

        new Float:elapsed = get_gametime() - g_fStart[id]
        new tbuf[16]
        formatTime(elapsed, tbuf, charsmax(tbuf))

        set_hudmessage(0, 255, 100, 0.02, 0.25, 0, 0.0, 0.15, 0.0, 0.0, -1)
        show_hudmessage(id, "Time: %s^nJumps: %d", tbuf, g_iJumps[id])
    }
}

formatTime(Float:seconds, out[], len)
{
    new total_ms = floatround(seconds * 1000.0)
    new mins = total_ms / 60000
    new secs = (total_ms / 1000) % 60
    new ms   = total_ms % 1000
    formatex(out, len, "%02d:%02d.%03d", mins, secs, ms)
}