Promuj serwer
csb-mapchooser / addons / amxmodx / scripting / csb_mapchooser.sma
417 lines 10.1 KiB Pawn Raw Permalink History
/*
 * CSB Map Chooser
 * Copyright (C) 2026 counter-strike-boost.com
 *
 * End-of-map vote. A short time before the time limit runs out the plugin opens
 * a menu with a handful of random maps taken from the map cycle (excluding the
 * current map and the last few that were played, which are tracked in a file),
 * counts the votes with a HUD countdown and sets amx_nextmap to the winner. If
 * enabled, players can instead vote to extend the current map, up to a capped
 * number of extensions.
 *
 * Inspired by the original AMX Mod X MapChooser. This is an independent GPL
 * re-implementation; no original code is reused.
 *
 * 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. It is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See <https://www.gnu.org/licenses/> for details.
 */

#include <amxmodx>

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

#define MAX_CHOICES  6
#define EXTEND_SLOT  MAX_CHOICES     /* votes[] index used by the extend option */

new g_pEnabled, g_pStart, g_pChoices, g_pDuration
new g_pExtend, g_pExtendStep, g_pExtendMax, g_pRecent

new g_szChoice[MAX_CHOICES][32]
new g_iVotes[MAX_CHOICES + 1]
new g_iNumChoices
new bool:g_bExtendOffered

new bool:g_bVoted[33]
new bool:g_bVoteActive
new bool:g_bDecided
new g_iExtendsDone
new g_iSecondsLeft
new g_menu = -1

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

    g_pEnabled    = register_cvar("csb_mc_enabled", "1")
    g_pStart      = register_cvar("csb_mc_start", "90")     /* seconds before end to open the vote */
    g_pChoices    = register_cvar("csb_mc_choices", "5")
    g_pDuration   = register_cvar("csb_mc_duration", "20")
    g_pExtend     = register_cvar("csb_mc_extend", "1")
    g_pExtendStep = register_cvar("csb_mc_extendstep", "15") /* minutes added per extend */
    g_pExtendMax  = register_cvar("csb_mc_extendmax", "2")
    g_pRecent     = register_cvar("csb_mc_recent", "3")

    register_clcmd("say /nextmap", "cmdNextmap")

    set_task(15.0, "checkTime", 0, _, _, "b")
}

public cmdNextmap(id)
{
    new nextmap[32]
    get_cvar_string("amx_nextmap", nextmap, charsmax(nextmap))
    client_print(id, print_chat, "[CSB] Next map: %s", nextmap[0] ? nextmap : "not decided yet")
    return PLUGIN_HANDLED
}

public checkTime()
{
    if (!get_pcvar_num(g_pEnabled) || g_bVoteActive || g_bDecided)
        return

    new left = get_timeleft()

    /* no timelimit, or too early */
    if (left <= 10 || left > get_pcvar_num(g_pStart))
        return

    startVote()
}

startVote()
{
    new pool[64][32], poolCount = 0
    gatherMaps(pool, poolCount)

    if (poolCount < 2)
    {
        log_amx("[CSB MapChooser] not enough eligible maps to hold a vote.")
        g_bDecided = true
        return
    }

    new want = get_pcvar_num(g_pChoices)
    if (want < 2) want = 2
    if (want > MAX_CHOICES) want = MAX_CHOICES
    if (want > poolCount) want = poolCount

    g_iNumChoices = 0
    for (new i = 0; i < want; i++)
    {
        new pick = random_num(0, poolCount - 1)
        copy(g_szChoice[g_iNumChoices], charsmax(g_szChoice[]), pool[pick])
        g_iNumChoices++

        /* remove the chosen map so it cannot be picked twice */
        pool[pick] = pool[poolCount - 1]
        poolCount--
    }

    for (new i = 0; i <= MAX_CHOICES; i++)
        g_iVotes[i] = 0

    for (new id = 1; id <= 32; id++)
        g_bVoted[id] = false

    g_bExtendOffered = (get_pcvar_num(g_pExtend) != 0 && g_iExtendsDone < get_pcvar_num(g_pExtendMax))

    buildMenu()

    new players[32], num
    get_players(players, num, "ch")
    for (new i = 0; i < num; i++)
        menu_display(players[i], g_menu, 0)

    g_bVoteActive = true

    new dur = get_pcvar_num(g_pDuration)
    if (dur < 5) dur = 5
    g_iSecondsLeft = dur

    set_task(1.0, "voteTick", 0, _, _, "a", dur)
    set_task(float(dur), "endVote")

    client_print_color(0, print_team_default, "^x04[CSB]^x03 Vote for the next map!^x01 Check the menu.")
}

buildMenu()
{
    if (g_menu != -1)
    {
        menu_destroy(g_menu)
        g_menu = -1
    }

    g_menu = menu_create("\yVote for the next map\w", "voteHandler")

    new item[48]
    for (new i = 0; i < g_iNumChoices; i++)
    {
        formatex(item, charsmax(item), "%s", g_szChoice[i])
        menu_additem(g_menu, item)
    }

    if (g_bExtendOffered)
    {
        formatex(item, charsmax(item), "\rExtend the current map (+%d min)", get_pcvar_num(g_pExtendStep))
        menu_additem(g_menu, item)
    }

    menu_setprop(g_menu, MPROP_EXIT, MEXIT_ALL)
}

public voteHandler(id, menu, item)
{
    if (item == MENU_EXIT || !g_bVoteActive)
        return PLUGIN_HANDLED

    if (g_bVoted[id])
        return PLUGIN_HANDLED

    /* extend option sits right after the map items */
    if (g_bExtendOffered && item == g_iNumChoices)
    {
        g_iVotes[EXTEND_SLOT]++
        g_bVoted[id] = true
        client_print(id, print_chat, "[CSB] You voted to extend the current map.")
        return PLUGIN_HANDLED
    }

    if (item >= 0 && item < g_iNumChoices)
    {
        g_iVotes[item]++
        g_bVoted[id] = true
        client_print(id, print_chat, "[CSB] You voted for %s.", g_szChoice[item])
    }

    return PLUGIN_HANDLED
}

public voteTick()
{
    if (!g_bVoteActive)
        return

    if (g_iSecondsLeft > 0)
        g_iSecondsLeft--

    set_hudmessage(255, 200, 0, -1.0, 0.20, 0, 0.0, 1.1, 0.0, 0.0, -1)
    show_hudmessage(0, "Map vote: %d second(s) left", g_iSecondsLeft)
}

public endVote()
{
    g_bVoteActive = false

    new best = -1, bestVotes = -1

    for (new i = 0; i < g_iNumChoices; i++)
    {
        if (g_iVotes[i] > bestVotes)
        {
            bestVotes = g_iVotes[i]
            best = i
        }
    }

    new bool:extendWon = false
    if (g_bExtendOffered && g_iVotes[EXTEND_SLOT] > bestVotes)
    {
        extendWon = true
        bestVotes = g_iVotes[EXTEND_SLOT]
    }

    if (extendWon)
    {
        new step = get_pcvar_num(g_pExtendStep)
        new Float:cur = get_cvar_float("mp_timelimit")
        set_cvar_float("mp_timelimit", cur + float(step))
        g_iExtendsDone++
        g_bDecided = false      /* allow another vote later */

        client_print_color(0, print_team_default,
            "^x04[CSB]^x03 The current map was extended by^x04 %d^x01 minute(s) (%d vote(s)).",
            step, bestVotes)
    }
    else if (best != -1)
    {
        set_cvar_string("amx_nextmap", g_szChoice[best])
        g_bDecided = true
        rememberCurrentMap()

        client_print_color(0, print_team_default,
            "^x04[CSB]^x03 Next map will be^x04 %s^x01 (%d vote(s)).",
            g_szChoice[best], bestVotes)
    }
    else
    {
        g_bDecided = true
    }

    if (g_menu != -1)
    {
        menu_destroy(g_menu)
        g_menu = -1
    }
}

/* ---------- map pool ---------- */

gatherMaps(pool[][], &count)
{
    count = 0

    new cur[32]
    get_mapname(cur, charsmax(cur))

    new recent[16][32], recentNum = 0
    loadRecent(recent, recentNum)

    new cyclefile[64]
    get_cvar_string("mapcyclefile", cyclefile, charsmax(cyclefile))
    if (!cyclefile[0])
        copy(cyclefile, charsmax(cyclefile), "mapcycle.txt")

    new fp = fopen(cyclefile, "rt")
    if (!fp)
        return

    new line[64], map[32]

    while (!feof(fp) && count < 64)
    {
        fgets(fp, line, charsmax(line))
        trim(line)

        if (!line[0] || line[0] == ';' || (line[0] == '/' && line[1] == '/'))
            continue

        /* first token is the map name */
        new i = 0, j = 0
        while (line[i] && line[i] != ' ' && line[i] != '^t' && j < charsmax(map))
            map[j++] = line[i++]
        map[j] = 0

        if (!map[0] || equali(map, cur) || !is_map_valid(map))
            continue

        if (inList(map, recent, recentNum) || inList(map, pool, count))
            continue

        copy(pool[count], 31, map)
        count++
    }

    fclose(fp)
}

bool:inList(const map[], const list[][], num)
{
    for (new i = 0; i < num; i++)
        if (equali(list[i], map))
            return true
    return false
}

loadRecent(recent[][], &num)
{
    num = 0

    new path[160]
    recentPath(path, charsmax(path))

    if (!file_exists(path))
        return

    new fp = fopen(path, "rt")
    if (!fp)
        return

    new line[32]
    new keep = get_pcvar_num(g_pRecent)

    while (!feof(fp) && num < 16)
    {
        fgets(fp, line, charsmax(line))
        trim(line)

        if (!line[0])
            continue

        copy(recent[num], 31, line)
        num++
    }

    fclose(fp)

    /* only the last 'keep' entries actually count */
    if (keep >= 0 && num > keep)
    {
        new drop = num - keep
        for (new i = 0; i < keep; i++)
            copy(recent[i], 31, recent[i + drop])
        num = keep
    }
}

rememberCurrentMap()
{
    new cur[32]
    get_mapname(cur, charsmax(cur))

    new path[160]
    recentPath(path, charsmax(path))

    new lines[16][32], num = 0

    if (file_exists(path))
    {
        new fp = fopen(path, "rt")
        if (fp)
        {
            new line[32]
            while (!feof(fp) && num < 16)
            {
                fgets(fp, line, charsmax(line))
                trim(line)
                if (line[0])
                {
                    copy(lines[num], 31, line)
                    num++
                }
            }
            fclose(fp)
        }
    }

    /* keep the most recent 15, then append the current map */
    new start = 0
    if (num >= 15)
        start = num - 14

    new fw = fopen(path, "wt")
    if (!fw)
        return

    for (new i = start; i < num; i++)
    {
        fputs(fw, lines[i])
        fputs(fw, "^n")
    }
    fputs(fw, cur)
    fputs(fw, "^n")
    fclose(fw)
}

recentPath(out[], len)
{
    new dir[128]
    get_localinfo("amxx_configsdir", dir, charsmax(dir))
    formatex(out, len, "%s/csb_mc_recent.ini", dir)
}