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