End-of-map vote with random maps from the map cycle, a HUD countdown, recent-map exclusion and an optional extend option.
csb_mapchooser.sma v1.0.0
1
/*
2
* CSB Map Chooser
3
* Copyright (C) 2026 counter-strike-boost.com
4
*
5
* End-of-map vote. A short time before the time limit runs out the plugin opens
6
* a menu with a handful of random maps taken from the map cycle (excluding the
7
* current map and the last few that were played, which are tracked in a file),
8
* counts the votes with a HUD countdown and sets amx_nextmap to the winner. If
9
* enabled, players can instead vote to extend the current map, up to a capped
10
* number of extensions.
11
*
12
* Inspired by the original AMX Mod X MapChooser. This is an independent GPL
13
* re-implementation; no original code is reused.
14
*
15
* This program is free software: you can redistribute it and/or modify it under
16
* the terms of the GNU General Public License as published by the Free Software
17
* Foundation, either version 3 of the License, or (at your option) any later
18
* version. It is distributed in the hope that it will be useful, but WITHOUT
19
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
20
* FOR A PARTICULAR PURPOSE. See <https://www.gnu.org/licenses/> for details.
21
*/
22
23
#include <amxmodx>
24
25
new const PLUGIN[] = "CSB Map Chooser"
26
new const VERSION[] = "1.0.0"
27
new const AUTHOR[] = "counter-strike-boost.com"
28
29
#define MAX_CHOICES 6
30
#define EXTEND_SLOT MAX_CHOICES /* votes[] index used by the extend option */
31
32
new g_pEnabled, g_pStart, g_pChoices, g_pDuration
33
new g_pExtend, g_pExtendStep, g_pExtendMax, g_pRecent
34
35
new g_szChoice[MAX_CHOICES][32]
36
new g_iVotes[MAX_CHOICES + 1]
37
new g_iNumChoices
38
new bool:g_bExtendOffered
39
40
new bool:g_bVoted[33]
41
new bool:g_bVoteActive
42
new bool:g_bDecided
43
new g_iExtendsDone
44
new g_iSecondsLeft
45
new g_menu = -1
46
47
public plugin_init()
48
{
49
register_plugin(PLUGIN, VERSION, AUTHOR)
50
51
g_pEnabled = register_cvar("csb_mc_enabled", "1")
52
g_pStart = register_cvar("csb_mc_start", "90") /* seconds before end to open the vote */
53
g_pChoices = register_cvar("csb_mc_choices", "5")
54
g_pDuration = register_cvar("csb_mc_duration", "20")
55
g_pExtend = register_cvar("csb_mc_extend", "1")
56
g_pExtendStep = register_cvar("csb_mc_extendstep", "15") /* minutes added per extend */
57
g_pExtendMax = register_cvar("csb_mc_extendmax", "2")
58
g_pRecent = register_cvar("csb_mc_recent", "3")
59
60
register_clcmd("say /nextmap", "cmdNextmap")
61
62
set_task(15.0, "checkTime", 0, _, _, "b")
63
}
64
65
public cmdNextmap(id)
66
{
67
new nextmap[32]
68
get_cvar_string("amx_nextmap", nextmap, charsmax(nextmap))
69
client_print(id, print_chat, "[CSB] Next map: %s", nextmap[0] ? nextmap : "not decided yet")
70
return PLUGIN_HANDLED
71
}
72
73
public checkTime()
74
{
75
if (!get_pcvar_num(g_pEnabled) || g_bVoteActive || g_bDecided)
76
return
77
78
new left = get_timeleft()
79
80
/* no timelimit, or too early */
81
if (left <= 10 || left > get_pcvar_num(g_pStart))
82
return
83
84
startVote()
85
}
86
87
startVote()
88
{
89
new pool[64][32], poolCount = 0
90
gatherMaps(pool, poolCount)
91
92
if (poolCount < 2)
93
{
94
log_amx("[CSB MapChooser] not enough eligible maps to hold a vote.")
95
g_bDecided = true
96
return
97
}
98
99
new want = get_pcvar_num(g_pChoices)
100
if (want < 2) want = 2
101
if (want > MAX_CHOICES) want = MAX_CHOICES
102
if (want > poolCount) want = poolCount
103
104
g_iNumChoices = 0
105
for (new i = 0; i < want; i++)
106
{
107
new pick = random_num(0, poolCount - 1)
108
copy(g_szChoice[g_iNumChoices], charsmax(g_szChoice[]), pool[pick])
109
g_iNumChoices++
110
111
/* remove the chosen map so it cannot be picked twice */
112
pool[pick] = pool[poolCount - 1]
113
poolCount--
114
}
115
116
for (new i = 0; i <= MAX_CHOICES; i++)
117
g_iVotes[i] = 0
118
119
for (new id = 1; id <= 32; id++)
120
g_bVoted[id] = false
121
122
g_bExtendOffered = (get_pcvar_num(g_pExtend) != 0 && g_iExtendsDone < get_pcvar_num(g_pExtendMax))
123
124
buildMenu()
125
126
new players[32], num
127
get_players(players, num, "ch")
128
for (new i = 0; i < num; i++)
129
menu_display(players[i], g_menu, 0)
130
131
g_bVoteActive = true
132
133
new dur = get_pcvar_num(g_pDuration)
134
if (dur < 5) dur = 5
135
g_iSecondsLeft = dur
136
137
set_task(1.0, "voteTick", 0, _, _, "a", dur)
138
set_task(float(dur), "endVote")
139
140
client_print_color(0, print_team_default, "^x04[CSB]^x03 Vote for the next map!^x01 Check the menu.")
141
}
142
143
buildMenu()
144
{
145
if (g_menu != -1)
146
{
147
menu_destroy(g_menu)
148
g_menu = -1
149
}
150
151
g_menu = menu_create("\yVote for the next map\w", "voteHandler")
152
153
new item[48]
154
for (new i = 0; i < g_iNumChoices; i++)
155
{
156
formatex(item, charsmax(item), "%s", g_szChoice[i])
157
menu_additem(g_menu, item)
158
}
159
160
if (g_bExtendOffered)
161
{
162
formatex(item, charsmax(item), "\rExtend the current map (+%d min)", get_pcvar_num(g_pExtendStep))
163
menu_additem(g_menu, item)
164
}
165
166
menu_setprop(g_menu, MPROP_EXIT, MEXIT_ALL)
167
}
168
169
public voteHandler(id, menu, item)
170
{
171
if (item == MENU_EXIT || !g_bVoteActive)
172
return PLUGIN_HANDLED
173
174
if (g_bVoted[id])
175
return PLUGIN_HANDLED
176
177
/* extend option sits right after the map items */
178
if (g_bExtendOffered && item == g_iNumChoices)
179
{
180
g_iVotes[EXTEND_SLOT]++
181
g_bVoted[id] = true
182
client_print(id, print_chat, "[CSB] You voted to extend the current map.")
183
return PLUGIN_HANDLED
184
}
185
186
if (item >= 0 && item < g_iNumChoices)
187
{
188
g_iVotes[item]++
189
g_bVoted[id] = true
190
client_print(id, print_chat, "[CSB] You voted for %s.", g_szChoice[item])
191
}
192
193
return PLUGIN_HANDLED
194
}
195
196
public voteTick()
197
{
198
if (!g_bVoteActive)
199
return
200
201
if (g_iSecondsLeft > 0)
202
g_iSecondsLeft--
203
204
set_hudmessage(255, 200, 0, -1.0, 0.20, 0, 0.0, 1.1, 0.0, 0.0, -1)
205
show_hudmessage(0, "Map vote: %d second(s) left", g_iSecondsLeft)
206
}
207
208
public endVote()
209
{
210
g_bVoteActive = false
211
212
new best = -1, bestVotes = -1
213
214
for (new i = 0; i < g_iNumChoices; i++)
215
{
216
if (g_iVotes[i] > bestVotes)
217
{
218
bestVotes = g_iVotes[i]
219
best = i
220
}
221
}
222
223
new bool:extendWon = false
224
if (g_bExtendOffered && g_iVotes[EXTEND_SLOT] > bestVotes)
225
{
226
extendWon = true
227
bestVotes = g_iVotes[EXTEND_SLOT]
228
}
229
230
if (extendWon)
231
{
232
new step = get_pcvar_num(g_pExtendStep)
233
new Float:cur = get_cvar_float("mp_timelimit")
234
set_cvar_float("mp_timelimit", cur + float(step))
235
g_iExtendsDone++
236
g_bDecided = false /* allow another vote later */
237
238
client_print_color(0, print_team_default,
239
"^x04[CSB]^x03 The current map was extended by^x04 %d^x01 minute(s) (%d vote(s)).",
240
step, bestVotes)
241
}
242
else if (best != -1)
243
{
244
set_cvar_string("amx_nextmap", g_szChoice[best])
245
g_bDecided = true
246
rememberCurrentMap()
247
248
client_print_color(0, print_team_default,
249
"^x04[CSB]^x03 Next map will be^x04 %s^x01 (%d vote(s)).",
250
g_szChoice[best], bestVotes)
251
}
252
else
253
{
254
g_bDecided = true
255
}
256
257
if (g_menu != -1)
258
{
259
menu_destroy(g_menu)
260
g_menu = -1
261
}
262
}
263
264
/* ---------- map pool ---------- */
265
266
gatherMaps(pool[][], &count)
267
{
268
count = 0
269
270
new cur[32]
271
get_mapname(cur, charsmax(cur))
272
273
new recent[16][32], recentNum = 0
274
loadRecent(recent, recentNum)
275
276
new cyclefile[64]
277
get_cvar_string("mapcyclefile", cyclefile, charsmax(cyclefile))
278
if (!cyclefile[0])
279
copy(cyclefile, charsmax(cyclefile), "mapcycle.txt")
280
281
new fp = fopen(cyclefile, "rt")
282
if (!fp)
283
return
284
285
new line[64], map[32]
286
287
while (!feof(fp) && count < 64)
288
{
289
fgets(fp, line, charsmax(line))
290
trim(line)
291
292
if (!line[0] || line[0] == ';' || (line[0] == '/' && line[1] == '/'))
293
continue
294
295
/* first token is the map name */
296
new i = 0, j = 0
297
while (line[i] && line[i] != ' ' && line[i] != '^t' && j < charsmax(map))
298
map[j++] = line[i++]
299
map[j] = 0
300
301
if (!map[0] || equali(map, cur) || !is_map_valid(map))
302
continue
303
304
if (inList(map, recent, recentNum) || inList(map, pool, count))
305
continue
306
307
copy(pool[count], 31, map)
308
count++
309
}
310
311
fclose(fp)
312
}
313
314
bool:inList(const map[], const list[][], num)
315
{
316
for (new i = 0; i < num; i++)
317
if (equali(list[i], map))
318
return true
319
return false
320
}
321
322
loadRecent(recent[][], &num)
323
{
324
num = 0
325
326
new path[160]
327
recentPath(path, charsmax(path))
328
329
if (!file_exists(path))
330
return
331
332
new fp = fopen(path, "rt")
333
if (!fp)
334
return
335
336
new line[32]
337
new keep = get_pcvar_num(g_pRecent)
338
339
while (!feof(fp) && num < 16)
340
{
341
fgets(fp, line, charsmax(line))
342
trim(line)
343
344
if (!line[0])
345
continue
346
347
copy(recent[num], 31, line)
348
num++
349
}
350
351
fclose(fp)
352
353
/* only the last 'keep' entries actually count */
354
if (keep >= 0 && num > keep)
355
{
356
new drop = num - keep
357
for (new i = 0; i < keep; i++)
358
copy(recent[i], 31, recent[i + drop])
359
num = keep
360
}
361
}
362
363
rememberCurrentMap()
364
{
365
new cur[32]
366
get_mapname(cur, charsmax(cur))
367
368
new path[160]
369
recentPath(path, charsmax(path))
370
371
new lines[16][32], num = 0
372
373
if (file_exists(path))
374
{
375
new fp = fopen(path, "rt")
376
if (fp)
377
{
378
new line[32]
379
while (!feof(fp) && num < 16)
380
{
381
fgets(fp, line, charsmax(line))
382
trim(line)
383
if (line[0])
384
{
385
copy(lines[num], 31, line)
386
num++
387
}
388
}
389
fclose(fp)
390
}
391
}
392
393
/* keep the most recent 15, then append the current map */
394
new start = 0
395
if (num >= 15)
396
start = num - 14
397
398
new fw = fopen(path, "wt")
399
if (!fw)
400
return
401
402
for (new i = start; i < num; i++)
403
{
404
fputs(fw, lines[i])
405
fputs(fw, "^n")
406
}
407
fputs(fw, cur)
408
fputs(fw, "^n")
409
fclose(fw)
410
}
411
412
recentPath(out[], len)
413
{
414
new dir[128]
415
get_localinfo("amxx_configsdir", dir, charsmax(dir))
416
formatex(out, len, "%s/csb_mc_recent.ini", dir)
417
}
418









