Your server here Bid in the weekly auction Promote server
csb-admin-map-menu / addons / amxmodx / scripting / csb_admin_map_menu.sma
241 lines 6 KiB Pawn Raw Permalink History
/*
 * CSB Admin Map Menu
 * Copyright (C) 2026 counter-strike-boost.com
 *
 * Reads mapcycle.txt (or addons/amxmodx/configs/maps.ini) into a paginated menu.
 * Picking a map asks for confirmation, then changes level immediately or waits
 * for the end of the round. amx_mapsearch <name> finds a map by substring, and
 * every candidate is checked with is_map_valid before it is offered.
 *
 * Inspired by the AMX Mod X maps menu (AMX Mod X Development Team). 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. 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>
#include <amxmisc>

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

#define MAX_MAPS 128

new g_szMaps[MAX_MAPS][32]
new g_iMapCount = 0

new g_szPending[33][32]      /* map chosen, awaiting confirm */
new bool:g_bChangePending = false
new g_szNextMap[32]

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

    register_concmd("amx_mapmenu", "cmdMapMenu", ADMIN_MAP, "- open the map change menu")
    register_concmd("amx_mapsearch", "cmdSearch", ADMIN_MAP, "<part of name> - search maps and open a menu")

    register_logevent("eventRoundEnd", 2, "1=Round_End")
}

public plugin_cfg()
{
    loadMaps()
}

loadMaps()
{
    g_iMapCount = 0

    if (readMapFile("mapcycle.txt"))
        return

    readMapFile("addons/amxmodx/configs/maps.ini")
}

bool:readMapFile(const path[])
{
    new line[64], name[32], len, i = 0

    while (g_iMapCount < MAX_MAPS && read_file(path, i, line, charsmax(line), len))
    {
        i++
        trim(line)

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

        /* mapcycle lines can be "de_dust2" or "de_dust2 { ... }" */
        strtok(line, name, charsmax(name), line, charsmax(line), ' ')
        trim(name)

        if (!name[0] || !is_map_valid(name))
            continue

        copy(g_szMaps[g_iMapCount], charsmax(g_szMaps[]), name)
        g_iMapCount++
    }

    return g_iMapCount > 0
}

public cmdMapMenu(id, level, cid)
{
    if (!cmd_access(id, level, cid, 1))
        return PLUGIN_HANDLED

    if (!g_iMapCount)
    {
        console_print(id, "[CSB] No maps loaded (mapcycle.txt / maps.ini empty).")
        return PLUGIN_HANDLED
    }

    buildMenu(id, "")
    return PLUGIN_HANDLED
}

public cmdSearch(id, level, cid)
{
    if (!cmd_access(id, level, cid, 1))
        return PLUGIN_HANDLED

    new needle[32]
    read_argv(1, needle, charsmax(needle))

    if (!needle[0])
    {
        console_print(id, "[CSB] Usage: amx_mapsearch <part of name>")
        return PLUGIN_HANDLED
    }

    buildMenu(id, needle)
    return PLUGIN_HANDLED
}

buildMenu(id, const needle[])
{
    new title[64]
    formatex(title, charsmax(title), "\yCSB Map Menu\w^nPick a map to change to")

    new menu = menu_create(title, "menuPick")
    new added = 0

    for (new i = 0; i < g_iMapCount; i++)
    {
        if (needle[0] && containi(g_szMaps[i], needle) == -1)
            continue

        menu_additem(menu, g_szMaps[i], g_szMaps[i], 0)
        added++
    }

    if (!added)
    {
        menu_destroy(menu)
        console_print(id, "[CSB] No maps matched '%s'.", needle)
        return
    }

    menu_display(id, menu, 0)
}

public menuPick(id, menu, item)
{
    if (item == MENU_EXIT || item < 0)
    {
        menu_destroy(menu)
        return PLUGIN_HANDLED
    }

    new info[32], name[32], access, callback
    menu_item_getinfo(menu, item, access, info, charsmax(info), name, charsmax(name), callback)
    menu_destroy(menu)

    copy(g_szPending[id], charsmax(g_szPending[]), info)
    confirmMenu(id)
    return PLUGIN_HANDLED
}

confirmMenu(id)
{
    new title[96]
    formatex(title, charsmax(title), "\yChange to %s?\w", g_szPending[id])

    new menu = menu_create(title, "menuConfirm")
    menu_additem(menu, "\rChange now", "now", 0)
    menu_additem(menu, "\yChange at round end", "end", 0)
    menu_additem(menu, "\wCancel", "cancel", 0)
    menu_setprop(menu, MPROP_EXIT, MEXIT_ALL)
    menu_display(id, menu, 0)
}

public menuConfirm(id, menu, item)
{
    if (item == MENU_EXIT || item < 0)
    {
        menu_destroy(menu)
        return PLUGIN_HANDLED
    }

    new info[8], name[32], access, callback
    menu_item_getinfo(menu, item, access, info, charsmax(info), name, charsmax(name), callback)
    menu_destroy(menu)

    if (equal(info, "cancel") || !g_szPending[id][0])
        return PLUGIN_HANDLED

    if (!is_map_valid(g_szPending[id]))
    {
        client_print(id, print_chat, "[CSB] %s is no longer valid.", g_szPending[id])
        return PLUGIN_HANDLED
    }

    new aname[32]
    getName(id, aname, charsmax(aname))

    if (equal(info, "now"))
    {
        client_print(0, print_chat, "[CSB] %s is changing the map to %s...", aname, g_szPending[id])
        log_amx("[CSB Maps] %s changed the map to %s", aname, g_szPending[id])
        new mp[32]
        copy(mp, charsmax(mp), g_szPending[id])
        set_task(2.0, "taskChange", 0, mp, charsmax(mp))
    }
    else
    {
        copy(g_szNextMap, charsmax(g_szNextMap), g_szPending[id])
        g_bChangePending = true
        client_print(0, print_chat, "[CSB] %s scheduled %s for the end of this round.", aname, g_szNextMap)
        log_amx("[CSB Maps] %s scheduled %s at round end", aname, g_szNextMap)
    }

    return PLUGIN_HANDLED
}

public taskChange(const map[])
{
    server_cmd("changelevel %s", map)
}

public eventRoundEnd()
{
    if (!g_bChangePending)
        return

    g_bChangePending = false
    server_cmd("changelevel %s", g_szNextMap)
}

getName(id, name[], len)
{
    if (id)
        get_user_name(id, name, len)
    else
        copy(name, len, "CONSOLE")
}