/* * 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 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 for details. */ #include #include 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, " - 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 ") 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") }