plugins/mapchooser.sp
author Nicholas Hastings <skamonkey@gmail.com>
Fri Oct 14 15:21:12 2011 -0400 (2011-10-14)
changeset 3412 dae51f7f4147
parent 3399 c81e3ec1123a
child 3440 0d9a99f41a67
permissions -rw-r--r--
Updated GMod gamedata.
     1 /**
     2  * vim: set ts=4 :
     3  * =============================================================================
     4  * SourceMod Mapchooser Plugin
     5  * Creates a map vote at appropriate times, setting sm_nextmap to the winning
     6  * vote
     7  *
     8  * SourceMod (C)2004-2007 AlliedModders LLC.  All rights reserved.
     9  * =============================================================================
    10  *
    11  * This program is free software; you can redistribute it and/or modify it under
    12  * the terms of the GNU General Public License, version 3.0, as published by the
    13  * Free Software Foundation.
    14  * 
    15  * This program is distributed in the hope that it will be useful, but WITHOUT
    16  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    17  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
    18  * details.
    19  *
    20  * You should have received a copy of the GNU General Public License along with
    21  * this program.  If not, see <http://www.gnu.org/licenses/>.
    22  *
    23  * As a special exception, AlliedModders LLC gives you permission to link the
    24  * code of this program (as well as its derivative works) to "Half-Life 2," the
    25  * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
    26  * by the Valve Corporation.  You must obey the GNU General Public License in
    27  * all respects for all other code used.  Additionally, AlliedModders LLC grants
    28  * this exception to all derivative works.  AlliedModders LLC defines further
    29  * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
    30  * or <http://www.sourcemod.net/license.php>.
    31  *
    32  * Version: $Id$
    33  */
    34 
    35 #pragma semicolon 1
    36 #include <sourcemod>
    37 #include <mapchooser>
    38 #include <nextmap>
    39 
    40 public Plugin:myinfo =
    41 {
    42 	name = "MapChooser",
    43 	author = "AlliedModders LLC",
    44 	description = "Automated Map Voting",
    45 	version = SOURCEMOD_VERSION,
    46 	url = "http://www.sourcemod.net/"
    47 };
    48 
    49 /* Valve ConVars */
    50 new Handle:g_Cvar_Winlimit = INVALID_HANDLE;
    51 new Handle:g_Cvar_Maxrounds = INVALID_HANDLE;
    52 new Handle:g_Cvar_Fraglimit = INVALID_HANDLE;
    53 new Handle:g_Cvar_Bonusroundtime = INVALID_HANDLE;
    54 
    55 /* Plugin ConVars */
    56 new Handle:g_Cvar_StartTime = INVALID_HANDLE;
    57 new Handle:g_Cvar_StartRounds = INVALID_HANDLE;
    58 new Handle:g_Cvar_StartFrags = INVALID_HANDLE;
    59 new Handle:g_Cvar_ExtendTimeStep = INVALID_HANDLE;
    60 new Handle:g_Cvar_ExtendRoundStep = INVALID_HANDLE;
    61 new Handle:g_Cvar_ExtendFragStep = INVALID_HANDLE;
    62 new Handle:g_Cvar_ExcludeMaps = INVALID_HANDLE;
    63 new Handle:g_Cvar_IncludeMaps = INVALID_HANDLE;
    64 new Handle:g_Cvar_NoVoteMode = INVALID_HANDLE;
    65 new Handle:g_Cvar_Extend = INVALID_HANDLE;
    66 new Handle:g_Cvar_DontChange = INVALID_HANDLE;
    67 new Handle:g_Cvar_EndOfMapVote = INVALID_HANDLE;
    68 new Handle:g_Cvar_VoteDuration = INVALID_HANDLE;
    69 
    70 new Handle:g_VoteTimer = INVALID_HANDLE;
    71 new Handle:g_RetryTimer = INVALID_HANDLE;
    72 
    73 /* Data Handles */
    74 new Handle:g_MapList = INVALID_HANDLE;
    75 new Handle:g_NominateList = INVALID_HANDLE;
    76 new Handle:g_NominateOwners = INVALID_HANDLE;
    77 new Handle:g_OldMapList = INVALID_HANDLE;
    78 new Handle:g_NextMapList = INVALID_HANDLE;
    79 new Handle:g_VoteMenu = INVALID_HANDLE;
    80 
    81 new g_Extends;
    82 new g_TotalRounds;
    83 new bool:g_HasVoteStarted;
    84 new bool:g_WaitingForVote;
    85 new bool:g_MapVoteCompleted;
    86 new bool:g_ChangeMapAtRoundEnd;
    87 new bool:g_ChangeMapInProgress;
    88 new g_mapFileSerial = -1;
    89 
    90 new g_NominateCount = 0;
    91 new MapChange:g_ChangeTime;
    92 
    93 new Handle:g_NominationsResetForward = INVALID_HANDLE;
    94 new Handle:g_MapVoteStartedForward = INVALID_HANDLE;
    95 
    96 /* Upper bound of how many team there could be */
    97 #define MAXTEAMS 10
    98 new g_winCount[MAXTEAMS];
    99 
   100 #define VOTE_EXTEND "##extend##"
   101 #define VOTE_DONTCHANGE "##dontchange##"
   102 
   103 public OnPluginStart()
   104 {
   105 	LoadTranslations("mapchooser.phrases");
   106 	LoadTranslations("common.phrases");
   107 	
   108 	new arraySize = ByteCountToCells(33);
   109 	g_MapList = CreateArray(arraySize);
   110 	g_NominateList = CreateArray(arraySize);
   111 	g_NominateOwners = CreateArray(1);
   112 	g_OldMapList = CreateArray(arraySize);
   113 	g_NextMapList = CreateArray(arraySize);
   114 	
   115 	g_Cvar_EndOfMapVote = CreateConVar("sm_mapvote_endvote", "1", "Specifies if MapChooser should run an end of map vote", _, true, 0.0, true, 1.0);
   116 
   117 	g_Cvar_StartTime = CreateConVar("sm_mapvote_start", "3.0", "Specifies when to start the vote based on time remaining.", _, true, 1.0);
   118 	g_Cvar_StartRounds = CreateConVar("sm_mapvote_startround", "2.0", "Specifies when to start the vote based on rounds remaining. Use 0 on TF2 to start vote during bonus round time", _, true, 0.0);
   119 	g_Cvar_StartFrags = CreateConVar("sm_mapvote_startfrags", "5.0", "Specifies when to start the vote base on frags remaining.", _, true, 1.0);
   120 	g_Cvar_ExtendTimeStep = CreateConVar("sm_extendmap_timestep", "15", "Specifies how much many more minutes each extension makes", _, true, 5.0);
   121 	g_Cvar_ExtendRoundStep = CreateConVar("sm_extendmap_roundstep", "5", "Specifies how many more rounds each extension makes", _, true, 1.0);
   122 	g_Cvar_ExtendFragStep = CreateConVar("sm_extendmap_fragstep", "10", "Specifies how many more frags are allowed when map is extended.", _, true, 5.0);	
   123 	g_Cvar_ExcludeMaps = CreateConVar("sm_mapvote_exclude", "5", "Specifies how many past maps to exclude from the vote.", _, true, 0.0);
   124 	g_Cvar_IncludeMaps = CreateConVar("sm_mapvote_include", "5", "Specifies how many maps to include in the vote.", _, true, 2.0, true, 6.0);
   125 	g_Cvar_NoVoteMode = CreateConVar("sm_mapvote_novote", "1", "Specifies whether or not MapChooser should pick a map if no votes are received.", _, true, 0.0, true, 1.0);
   126 	g_Cvar_Extend = CreateConVar("sm_mapvote_extend", "0", "Number of extensions allowed each map.", _, true, 0.0);
   127 	g_Cvar_DontChange = CreateConVar("sm_mapvote_dontchange", "1", "Specifies if a 'Don't Change' option should be added to early votes", _, true, 0.0);
   128 	g_Cvar_VoteDuration = CreateConVar("sm_mapvote_voteduration", "20", "Specifies how long the mapvote should be available for.", _, true, 5.0);
   129 	
   130 	RegAdminCmd("sm_mapvote", Command_Mapvote, ADMFLAG_CHANGEMAP, "sm_mapvote - Forces MapChooser to attempt to run a map vote now.");
   131 	RegAdminCmd("sm_setnextmap", Command_SetNextmap, ADMFLAG_CHANGEMAP, "sm_setnextmap <map>");
   132 
   133 	g_Cvar_Winlimit = FindConVar("mp_winlimit");
   134 	g_Cvar_Maxrounds = FindConVar("mp_maxrounds");
   135 	g_Cvar_Fraglimit = FindConVar("mp_fraglimit");
   136 	g_Cvar_Bonusroundtime = FindConVar("mp_bonusroundtime");
   137 	
   138 	if (g_Cvar_Winlimit != INVALID_HANDLE || g_Cvar_Maxrounds != INVALID_HANDLE)
   139 	{
   140 		decl String:folder[64];
   141 		GetGameFolderName(folder, sizeof(folder));
   142 
   143 		if (strcmp(folder, "tf") == 0)
   144 		{
   145 			HookEvent("teamplay_win_panel", Event_TeamPlayWinPanel);
   146 			HookEvent("teamplay_restart_round", Event_TFRestartRound);
   147 			HookEvent("arena_win_panel", Event_TeamPlayWinPanel);
   148 		}
   149 		else if (strcmp(folder, "nucleardawn") == 0)
   150 		{
   151 			HookEvent("round_win", Event_RoundEnd);
   152 		}
   153 		else
   154 		{
   155 			HookEvent("round_end", Event_RoundEnd);
   156 		}
   157 	}
   158 	
   159 	if (g_Cvar_Fraglimit != INVALID_HANDLE)
   160 	{
   161 		HookEvent("player_death", Event_PlayerDeath);		
   162 	}
   163 	
   164 	AutoExecConfig(true, "mapchooser");
   165 	
   166 	//Change the mp_bonusroundtime max so that we have time to display the vote
   167 	//If you display a vote during bonus time good defaults are 17 vote duration and 19 mp_bonustime
   168 	if (g_Cvar_Bonusroundtime != INVALID_HANDLE)
   169 	{
   170 		SetConVarBounds(g_Cvar_Bonusroundtime, ConVarBound_Upper, true, 30.0);		
   171 	}
   172 	
   173 	g_NominationsResetForward = CreateGlobalForward("OnNominationRemoved", ET_Ignore, Param_String, Param_Cell);
   174 	g_MapVoteStartedForward = CreateGlobalForward("OnMapVoteStarted", ET_Ignore);
   175 }
   176 
   177 public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max)
   178 {
   179 	RegPluginLibrary("mapchooser");	
   180 	
   181 	CreateNative("NominateMap", Native_NominateMap);
   182 	CreateNative("RemoveNominationByMap", Native_RemoveNominationByMap);
   183 	CreateNative("RemoveNominationByOwner", Native_RemoveNominationByOwner);
   184 	CreateNative("InitiateMapChooserVote", Native_InitiateVote);
   185 	CreateNative("CanMapChooserStartVote", Native_CanVoteStart);
   186 	CreateNative("HasEndOfMapVoteFinished", Native_CheckVoteDone);
   187 	CreateNative("GetExcludeMapList", Native_GetExcludeMapList);
   188 	CreateNative("GetNominatedMapList", Native_GetNominatedMapList);
   189 	CreateNative("EndOfMapVoteEnabled", Native_EndOfMapVoteEnabled);
   190 
   191 	return APLRes_Success;
   192 }
   193 
   194 public OnConfigsExecuted()
   195 {
   196 	if (ReadMapList(g_MapList,
   197 					 g_mapFileSerial, 
   198 					 "mapchooser",
   199 					 MAPLIST_FLAG_CLEARARRAY|MAPLIST_FLAG_MAPSFOLDER)
   200 		!= INVALID_HANDLE)
   201 		
   202 	{
   203 		if (g_mapFileSerial == -1)
   204 		{
   205 			LogError("Unable to create a valid map list.");
   206 		}
   207 	}
   208 	
   209 	CreateNextVote();
   210 	SetupTimeleftTimer();
   211 	
   212 	g_TotalRounds = 0;
   213 	
   214 	g_Extends = 0;
   215 	
   216 	g_MapVoteCompleted = false;
   217 	
   218 	g_NominateCount = 0;
   219 	ClearArray(g_NominateList);
   220 	ClearArray(g_NominateOwners);
   221 	
   222 	for (new i=0; i<MAXTEAMS; i++)
   223 	{
   224 		g_winCount[i] = 0;	
   225 	}
   226 	
   227 
   228 	/* Check if mapchooser will attempt to start mapvote during bonus round time - TF2 Only */
   229 	if ((g_Cvar_Bonusroundtime != INVALID_HANDLE) && !GetConVarInt(g_Cvar_StartRounds))
   230 	{
   231 		if (GetConVarFloat(g_Cvar_Bonusroundtime) <= GetConVarFloat(g_Cvar_VoteDuration))
   232 		{
   233 			LogError("Warning - Bonus Round Time shorter than Vote Time. Votes during bonus round may not have time to complete");
   234 		}
   235 	}
   236 }
   237 
   238 public OnMapEnd()
   239 {
   240 	g_HasVoteStarted = false;
   241 	g_WaitingForVote = false;
   242 	g_ChangeMapAtRoundEnd = false;
   243 	g_ChangeMapInProgress = false;
   244 	
   245 	g_VoteTimer = INVALID_HANDLE;
   246 	g_RetryTimer = INVALID_HANDLE;
   247 	
   248 	decl String:map[32];
   249 	GetCurrentMap(map, sizeof(map));
   250 	PushArrayString(g_OldMapList, map);
   251 				
   252 	if (GetArraySize(g_OldMapList) > GetConVarInt(g_Cvar_ExcludeMaps))
   253 	{
   254 		RemoveFromArray(g_OldMapList, 0);
   255 	}	
   256 }
   257 
   258 public OnClientDisconnect(client)
   259 {
   260 	new index = FindValueInArray(g_NominateOwners, client);
   261 	
   262 	if (index == -1)
   263 	{
   264 		return;
   265 	}
   266 	
   267 	new String:oldmap[33];
   268 	GetArrayString(g_NominateList, index, oldmap, sizeof(oldmap));
   269 	Call_StartForward(g_NominationsResetForward);
   270 	Call_PushString(oldmap);
   271 	Call_PushCell(GetArrayCell(g_NominateOwners, index));
   272 	Call_Finish();
   273 	
   274 	RemoveFromArray(g_NominateOwners, index);
   275 	RemoveFromArray(g_NominateList, index);
   276 	g_NominateCount--;
   277 }
   278 
   279 public Action:Command_SetNextmap(client, args)
   280 {
   281 	if (args < 1)
   282 	{
   283 		ReplyToCommand(client, "[SM] Usage: sm_setnextmap <map>");
   284 		return Plugin_Handled;
   285 	}
   286 
   287 	decl String:map[64];
   288 	GetCmdArg(1, map, sizeof(map));
   289 
   290 	if (!IsMapValid(map))
   291 	{
   292 		ReplyToCommand(client, "[SM] %t", "Map was not found", map);
   293 		return Plugin_Handled;
   294 	}
   295 
   296 	ShowActivity(client, "%t", "Changed Next Map", map);
   297 	LogAction(client, -1, "\"%L\" changed nextmap to \"%s\"", client, map);
   298 
   299 	SetNextMap(map);
   300 	g_MapVoteCompleted = true;
   301 
   302 	return Plugin_Handled;
   303 }
   304 
   305 public OnMapTimeLeftChanged()
   306 {
   307 	if (GetArraySize(g_MapList))
   308 	{
   309 		SetupTimeleftTimer();
   310 	}
   311 }
   312 
   313 SetupTimeleftTimer()
   314 {
   315 	new time;
   316 	if (GetMapTimeLeft(time) && time > 0)
   317 	{
   318 		new startTime = GetConVarInt(g_Cvar_StartTime) * 60;
   319 		if (time - startTime < 0 && GetConVarBool(g_Cvar_EndOfMapVote) && !g_MapVoteCompleted && !g_HasVoteStarted)
   320 		{
   321 			InitiateVote(MapChange_MapEnd, INVALID_HANDLE);		
   322 		}
   323 		else
   324 		{
   325 			if (g_VoteTimer != INVALID_HANDLE)
   326 			{
   327 				KillTimer(g_VoteTimer);
   328 				g_VoteTimer = INVALID_HANDLE;
   329 			}	
   330 			
   331 			//g_VoteTimer = CreateTimer(float(time - startTime), Timer_StartMapVote, _, TIMER_FLAG_NO_MAPCHANGE);
   332 			new Handle:data;
   333 			g_VoteTimer = CreateDataTimer(float(time - startTime), Timer_StartMapVote, data, TIMER_FLAG_NO_MAPCHANGE);
   334 			WritePackCell(data, _:MapChange_MapEnd);
   335 			WritePackCell(data, _:INVALID_HANDLE);
   336 			ResetPack(data);
   337 		}		
   338 	}
   339 }
   340 
   341 public Action:Timer_StartMapVote(Handle:timer, Handle:data)
   342 {
   343 	if (timer == g_RetryTimer)
   344 	{
   345 		g_WaitingForVote = false;
   346 		g_RetryTimer = INVALID_HANDLE;
   347 	}
   348 	else
   349 	{
   350 		g_VoteTimer = INVALID_HANDLE;
   351 	}
   352 	
   353 	if (!GetArraySize(g_MapList) || !GetConVarBool(g_Cvar_EndOfMapVote) || g_MapVoteCompleted || g_HasVoteStarted)
   354 	{
   355 		return Plugin_Stop;
   356 	}
   357 	
   358 	new MapChange:mapChange = MapChange:ReadPackCell(data);
   359 	new Handle:hndl = Handle:ReadPackCell(data);
   360 
   361 	InitiateVote(mapChange, hndl);
   362 
   363 	return Plugin_Stop;
   364 }
   365 
   366 public Event_TFRestartRound(Handle:event, const String:name[], bool:dontBroadcast)
   367 {
   368 	/* Game got restarted - reset our round count tracking */
   369 	g_TotalRounds = 0;	
   370 }
   371 
   372 public Event_TeamPlayWinPanel(Handle:event, const String:name[], bool:dontBroadcast)
   373 {
   374 	if (g_ChangeMapAtRoundEnd)
   375 	{
   376 		g_ChangeMapAtRoundEnd = false;
   377 		CreateTimer(2.0, Timer_ChangeMap, INVALID_HANDLE, TIMER_FLAG_NO_MAPCHANGE);
   378 		g_ChangeMapInProgress = true;
   379 	}
   380 	
   381 	new bluescore = GetEventInt(event, "blue_score");
   382 	new redscore = GetEventInt(event, "red_score");
   383 		
   384 	if(GetEventInt(event, "round_complete") == 1 || StrEqual(name, "arena_win_panel"))
   385 	{
   386 		g_TotalRounds++;
   387 		
   388 		if (!GetArraySize(g_MapList) || g_HasVoteStarted || g_MapVoteCompleted || !GetConVarBool(g_Cvar_EndOfMapVote))
   389 		{
   390 			return;
   391 		}
   392 		
   393 		CheckMaxRounds(g_TotalRounds);
   394 		
   395 		switch(GetEventInt(event, "winning_team"))
   396 		{
   397 			case 3:
   398 			{
   399 				CheckWinLimit(bluescore);
   400 			}
   401 			case 2:
   402 			{
   403 				CheckWinLimit(redscore);				
   404 			}			
   405 			//We need to do nothing on winning_team == 0 this indicates stalemate.
   406 			default:
   407 			{
   408 				return;
   409 			}			
   410 		}
   411 	}
   412 }
   413 /* You ask, why don't you just use team_score event? And I answer... Because CSS doesn't. */
   414 public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast)
   415 {
   416 	if (g_ChangeMapAtRoundEnd)
   417 	{
   418 		g_ChangeMapAtRoundEnd = false;
   419 		CreateTimer(2.0, Timer_ChangeMap, INVALID_HANDLE, TIMER_FLAG_NO_MAPCHANGE);
   420 		g_ChangeMapInProgress = true;
   421 	}
   422 	
   423 	new winner;
   424 	if (strcmp(name, "round_win") == 0)
   425 	{
   426 		// Nuclear Dawn
   427 		winner = GetEventInt(event, "team");
   428 	}
   429 	else
   430 	{
   431 		winner = GetEventInt(event, "winner");
   432 	}
   433 	
   434 	if (winner == 0 || winner == 1 || !GetConVarBool(g_Cvar_EndOfMapVote))
   435 	{
   436 		return;
   437 	}
   438 	
   439 	if (winner >= MAXTEAMS)
   440 	{
   441 		SetFailState("Mod exceed maximum team count - Please file a bug report.");	
   442 	}
   443 
   444 	g_TotalRounds++;
   445 	
   446 	g_winCount[winner]++;
   447 	
   448 	if (!GetArraySize(g_MapList) || g_HasVoteStarted || g_MapVoteCompleted)
   449 	{
   450 		return;
   451 	}
   452 	
   453 	CheckWinLimit(g_winCount[winner]);
   454 	CheckMaxRounds(g_TotalRounds);
   455 }
   456 
   457 public CheckWinLimit(winner_score)
   458 {	
   459 	if (g_Cvar_Winlimit != INVALID_HANDLE)
   460 	{
   461 		new winlimit = GetConVarInt(g_Cvar_Winlimit);
   462 		if (winlimit)
   463 		{			
   464 			if (winner_score >= (winlimit - GetConVarInt(g_Cvar_StartRounds)))
   465 			{
   466 				InitiateVote(MapChange_MapEnd, INVALID_HANDLE);
   467 			}
   468 		}
   469 	}
   470 }
   471 
   472 public CheckMaxRounds(roundcount)
   473 {		
   474 	if (g_Cvar_Maxrounds != INVALID_HANDLE)
   475 	{
   476 		new maxrounds = GetConVarInt(g_Cvar_Maxrounds);
   477 		if (maxrounds)
   478 		{
   479 			if (roundcount >= (maxrounds - GetConVarInt(g_Cvar_StartRounds)))
   480 			{
   481 				InitiateVote(MapChange_MapEnd, INVALID_HANDLE);
   482 			}			
   483 		}
   484 	}
   485 }
   486 
   487 public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast)
   488 {
   489 	if (!GetArraySize(g_MapList) || g_Cvar_Fraglimit == INVALID_HANDLE || g_HasVoteStarted)
   490 	{
   491 		return;
   492 	}
   493 	
   494 	if (!GetConVarInt(g_Cvar_Fraglimit) || !GetConVarBool(g_Cvar_EndOfMapVote))
   495 	{
   496 		return;
   497 	}
   498 
   499 	if (g_MapVoteCompleted)
   500 	{
   501 		return;
   502 	}
   503 
   504 	new fragger = GetClientOfUserId(GetEventInt(event, "attacker"));
   505 
   506 	if (!fragger)
   507 	{
   508 		return;
   509 	}
   510 
   511 	if (GetClientFrags(fragger) >= (GetConVarInt(g_Cvar_Fraglimit) - GetConVarInt(g_Cvar_StartFrags)))
   512 	{
   513 		InitiateVote(MapChange_MapEnd, INVALID_HANDLE);
   514 	}
   515 }
   516 
   517 public Action:Command_Mapvote(client, args)
   518 {
   519 	InitiateVote(MapChange_MapEnd, INVALID_HANDLE);
   520 
   521 	return Plugin_Handled;	
   522 }
   523 
   524 /**
   525  * Starts a new map vote
   526  *
   527  * @param when			When the resulting map change should occur.
   528  * @param inputlist		Optional list of maps to use for the vote, otherwise an internal list of nominations + random maps will be used.
   529  */
   530 InitiateVote(MapChange:when, Handle:inputlist=INVALID_HANDLE)
   531 {
   532 	g_WaitingForVote = true;
   533 	
   534 	if (IsVoteInProgress())
   535 	{
   536 		// Can't start a vote, try again in 5 seconds.
   537 		//g_RetryTimer = CreateTimer(5.0, Timer_StartMapVote, _, TIMER_FLAG_NO_MAPCHANGE);
   538 		
   539 		new Handle:data;
   540 		g_RetryTimer = CreateDataTimer(5.0, Timer_StartMapVote, data, TIMER_FLAG_NO_MAPCHANGE);
   541 		WritePackCell(data, _:when);
   542 		WritePackCell(data, _:inputlist);
   543 		ResetPack(data);
   544 		return;
   545 	}
   546 	
   547 	/* If the main map vote has completed (and chosen result) and its currently changing (not a delayed change) we block further attempts */
   548 	if (g_MapVoteCompleted && g_ChangeMapInProgress)
   549 	{
   550 		return;
   551 	}
   552 	
   553 	g_ChangeTime = when;
   554 	
   555 	g_WaitingForVote = false;
   556 		
   557 	g_HasVoteStarted = true;
   558 	g_VoteMenu = CreateMenu(Handler_MapVoteMenu, MenuAction:MENU_ACTIONS_ALL);
   559 	SetMenuTitle(g_VoteMenu, "Vote Nextmap");
   560 	SetVoteResultCallback(g_VoteMenu, Handler_MapVoteFinished);
   561 
   562 	/* Call OnMapVoteStarted() Forward */
   563 	Call_StartForward(g_MapVoteStartedForward);
   564 	Call_Finish();
   565 	
   566 	/**
   567 	 * TODO: Make a proper decision on when to clear the nominations list.
   568 	 * Currently it clears when used, and stays if an external list is provided.
   569 	 * Is this the right thing to do? External lists will probably come from places
   570 	 * like sm_mapvote from the adminmenu in the future.
   571 	 */
   572 	 
   573 	decl String:map[32];
   574 	
   575 	/* No input given - User our internal nominations and maplist */
   576 	if (inputlist == INVALID_HANDLE)
   577 	{
   578 		new nominateCount = GetArraySize(g_NominateList);
   579 		new voteSize = GetConVarInt(g_Cvar_IncludeMaps);
   580 		
   581 		/* Smaller of the two - It should be impossible for nominations to exceed the size though (cvar changed mid-map?) */
   582 		new nominationsToAdd = nominateCount >= voteSize ? voteSize : nominateCount;
   583 		
   584 		
   585 		for (new i=0; i<nominationsToAdd; i++)
   586 		{
   587 			GetArrayString(g_NominateList, i, map, sizeof(map));
   588 			AddMenuItem(g_VoteMenu, map, map);
   589 			
   590 			/* Notify Nominations that this map is now free */
   591 			Call_StartForward(g_NominationsResetForward);
   592 			Call_PushString(map);
   593 			Call_PushCell(GetArrayCell(g_NominateOwners, i));
   594 			Call_Finish();
   595 		}
   596 		
   597 		/* Clear out the rest of the nominations array */
   598 		for (new i=nominationsToAdd; i<nominateCount; i++)
   599 		{
   600 			GetArrayString(g_NominateList, i, map, sizeof(map));
   601 			
   602 			/* Notify Nominations that this map is now free */
   603 			Call_StartForward(g_NominationsResetForward);
   604 			Call_PushString(map);
   605 			Call_PushCell(GetArrayCell(g_NominateOwners, i));
   606 			Call_Finish();			
   607 		}
   608 		
   609 		/* There should currently be 'nominationsToAdd' unique maps in the vote */
   610 		
   611 		new i = nominationsToAdd;
   612 		new count = 0;
   613 		new availableMaps = GetArraySize(g_NextMapList);
   614 		
   615 		while (i < voteSize)
   616 		{
   617 			GetArrayString(g_NextMapList, count, map, sizeof(map));
   618 			count++;
   619 			
   620 			//Check if this map is in the nominate list (and thus already in the vote) */
   621 			if (FindStringInArray(g_NominateList, map) == -1)
   622 			{
   623 				/* Insert the map and increment our count */
   624 				AddMenuItem(g_VoteMenu, map, map);
   625 				i++;
   626 			}
   627 			
   628 			if (count >= availableMaps)
   629 			{
   630 				//Run out of maps, this will have to do.
   631 				break;	
   632 			}
   633 		}
   634 		
   635 		/* Wipe out our nominations list - Nominations have already been informed of this */
   636 		ClearArray(g_NominateOwners);
   637 		ClearArray(g_NominateList);
   638 	}
   639 	else //We were given a list of maps to start the vote with
   640 	{
   641 		new size = GetArraySize(inputlist);
   642 		
   643 		for (new i=0; i<size; i++)
   644 		{
   645 			GetArrayString(inputlist, i, map, sizeof(map));
   646 			
   647 			if (IsMapValid(map))
   648 			{
   649 				AddMenuItem(g_VoteMenu, map, map);
   650 			}	
   651 		}
   652 	}
   653 	
   654 	/* Do we add any special items? */
   655 	if ((when == MapChange_Instant || when == MapChange_RoundEnd) && GetConVarBool(g_Cvar_DontChange))
   656 	{
   657 		AddMenuItem(g_VoteMenu, VOTE_DONTCHANGE, "Don't Change");
   658 	}
   659 	else if (GetConVarBool(g_Cvar_Extend) && g_Extends < GetConVarInt(g_Cvar_Extend))
   660 	{
   661 		AddMenuItem(g_VoteMenu, VOTE_EXTEND, "Extend Map");
   662 	}
   663 	
   664 	new voteDuration = GetConVarInt(g_Cvar_VoteDuration);
   665 
   666 	SetMenuExitButton(g_VoteMenu, false);
   667 	VoteMenuToAll(g_VoteMenu, voteDuration);
   668 
   669 	LogAction(-1, -1, "Voting for next map has started.");
   670 	PrintToChatAll("[SM] %t", "Nextmap Voting Started");
   671 }
   672 
   673 public Handler_MapVoteFinished(Handle:menu,
   674 						   num_votes, 
   675 						   num_clients,
   676 						   const client_info[][2], 
   677 						   num_items,
   678 						   const item_info[][2])
   679 {
   680 	if (num_votes == 0)
   681 	{
   682 		LogError("No Votes recorded yet Advanced callback fired - Tell pRED* to fix this");
   683 		return;	
   684 	}
   685 	
   686 	decl String:map[32];
   687 	GetMenuItem(menu, item_info[0][VOTEINFO_ITEM_INDEX], map, sizeof(map));
   688 
   689 	if (strcmp(map, VOTE_EXTEND, false) == 0)
   690 	{
   691 		g_Extends++;
   692 		
   693 		new time;
   694 		if (GetMapTimeLimit(time))
   695 		{
   696 			if (time > 0)
   697 			{
   698 				ExtendMapTimeLimit(GetConVarInt(g_Cvar_ExtendTimeStep)*60);						
   699 			}
   700 		}
   701 		
   702 		if (g_Cvar_Winlimit != INVALID_HANDLE)
   703 		{
   704 			new winlimit = GetConVarInt(g_Cvar_Winlimit);
   705 			if (winlimit)
   706 			{
   707 				SetConVarInt(g_Cvar_Winlimit, winlimit + GetConVarInt(g_Cvar_ExtendRoundStep));
   708 			}					
   709 		}
   710 		
   711 		if (g_Cvar_Maxrounds != INVALID_HANDLE)
   712 		{
   713 			new maxrounds = GetConVarInt(g_Cvar_Maxrounds);
   714 			if (maxrounds)
   715 			{
   716 				SetConVarInt(g_Cvar_Maxrounds, maxrounds + GetConVarInt(g_Cvar_ExtendRoundStep));
   717 			}
   718 		}
   719 		
   720 		if (g_Cvar_Fraglimit != INVALID_HANDLE)
   721 		{
   722 			new fraglimit = GetConVarInt(g_Cvar_Fraglimit);
   723 			if (fraglimit)
   724 			{
   725 				SetConVarInt(g_Cvar_Fraglimit, fraglimit + GetConVarInt(g_Cvar_ExtendFragStep));						
   726 			}
   727 		}
   728 
   729 		PrintToChatAll("[SM] %t", "Current Map Extended", RoundToFloor(float(item_info[0][VOTEINFO_ITEM_VOTES])/float(num_votes)*100), num_votes);
   730 		LogAction(-1, -1, "Voting for next map has finished. The current map has been extended.");
   731 		
   732 		// We extended, so we'll have to vote again.
   733 		g_HasVoteStarted = false;
   734 		CreateNextVote();
   735 		SetupTimeleftTimer();
   736 		
   737 	}
   738 	else if (strcmp(map, VOTE_DONTCHANGE, false) == 0)
   739 	{
   740 		PrintToChatAll("[SM] %t", "Current Map Stays", RoundToFloor(float(item_info[0][VOTEINFO_ITEM_VOTES])/float(num_votes)*100), num_votes);
   741 		LogAction(-1, -1, "Voting for next map has finished. 'No Change' was the winner");
   742 		
   743 		g_HasVoteStarted = false;
   744 		CreateNextVote();
   745 		SetupTimeleftTimer();
   746 	}
   747 	else
   748 	{
   749 		if (g_ChangeTime == MapChange_MapEnd)
   750 		{
   751 			SetNextMap(map);
   752 		}
   753 		else if (g_ChangeTime == MapChange_Instant)
   754 		{
   755 			new Handle:data;
   756 			CreateDataTimer(2.0, Timer_ChangeMap, data);
   757 			WritePackString(data, map);
   758 			g_ChangeMapInProgress = false;
   759 		}
   760 		else // MapChange_RoundEnd
   761 		{
   762 			SetNextMap(map);
   763 			g_ChangeMapAtRoundEnd = true;
   764 		}
   765 		
   766 		g_HasVoteStarted = false;
   767 		g_MapVoteCompleted = true;
   768 		
   769 		PrintToChatAll("[SM] %t", "Nextmap Voting Finished", map, RoundToFloor(float(item_info[0][VOTEINFO_ITEM_VOTES])/float(num_votes)*100), num_votes);
   770 		LogAction(-1, -1, "Voting for next map has finished. Nextmap: %s.", map);
   771 	}	
   772 }
   773 
   774 public Handler_MapVoteMenu(Handle:menu, MenuAction:action, param1, param2)
   775 {
   776 	switch (action)
   777 	{
   778 		case MenuAction_End:
   779 		{
   780 			g_VoteMenu = INVALID_HANDLE;
   781 			CloseHandle(menu);
   782 		}
   783 		
   784 		case MenuAction_Display:
   785 		{
   786 	 		decl String:buffer[255];
   787 			Format(buffer, sizeof(buffer), "%T", "Vote Nextmap", param1);
   788 
   789 			new Handle:panel = Handle:param2;
   790 			SetPanelTitle(panel, buffer);
   791 		}		
   792 		
   793 		case MenuAction_DisplayItem:
   794 		{
   795 			if (GetMenuItemCount(menu) - 1 == param2)
   796 			{
   797 				decl String:map[64], String:buffer[255];
   798 				GetMenuItem(menu, param2, map, sizeof(map));
   799 				if (strcmp(map, VOTE_EXTEND, false) == 0)
   800 				{
   801 					Format(buffer, sizeof(buffer), "%T", "Extend Map", param1);
   802 					return RedrawMenuItem(buffer);
   803 				}
   804 				else if (strcmp(map, VOTE_DONTCHANGE, false) == 0)
   805 				{
   806 					Format(buffer, sizeof(buffer), "%T", "Dont Change", param1);
   807 					return RedrawMenuItem(buffer);					
   808 				}
   809 			}
   810 		}		
   811 	
   812 		case MenuAction_VoteCancel:
   813 		{
   814 			// If we receive 0 votes, pick at random.
   815 			if (param1 == VoteCancel_NoVotes && GetConVarBool(g_Cvar_NoVoteMode))
   816 			{
   817 				new count = GetMenuItemCount(menu);
   818 				new item = GetRandomInt(0, count - 1);
   819 				decl String:map[32];
   820 				GetMenuItem(menu, item, map, sizeof(map));
   821 				
   822 				while (strcmp(map, VOTE_EXTEND, false) == 0)
   823 				{
   824 					item = GetRandomInt(0, count - 1);
   825 					GetMenuItem(menu, item, map, sizeof(map));
   826 				}
   827 				
   828 				SetNextMap(map);			
   829 			}
   830 			else
   831 			{
   832 				// We were actually cancelled. I guess we do nothing.
   833 			}
   834 			
   835 			g_HasVoteStarted = false;
   836 			g_MapVoteCompleted = true;
   837 		}
   838 	}
   839 	
   840 	return 0;
   841 }
   842 
   843 public Action:Timer_ChangeMap(Handle:hTimer, Handle:dp)
   844 {
   845 	g_ChangeMapInProgress = false;
   846 	
   847 	new String:map[65];
   848 	
   849 	if (dp == INVALID_HANDLE)
   850 	{
   851 		if (!GetNextMap(map, sizeof(map)))
   852 		{
   853 			//No passed map and no set nextmap. fail!
   854 			return Plugin_Stop;	
   855 		}
   856 	}
   857 	else
   858 	{
   859 		ResetPack(dp);
   860 		ReadPackString(dp, map, sizeof(map));		
   861 	}
   862 	
   863 	ForceChangeLevel(map, "Map Vote");
   864 	
   865 	return Plugin_Stop;
   866 }
   867 
   868 CreateNextVote()
   869 {
   870 	if(g_NextMapList != INVALID_HANDLE)
   871 	{
   872 		ClearArray(g_NextMapList);
   873 	}
   874 	
   875 	decl String:map[32];
   876 	new index, Handle:tempMaps  = CloneArray(g_MapList);
   877 	
   878 	GetCurrentMap(map, sizeof(map));
   879 	index = FindStringInArray(tempMaps, map);
   880 	if (index != -1)
   881 	{
   882 		RemoveFromArray(tempMaps, index);
   883 	}	
   884 	
   885 	if (GetConVarInt(g_Cvar_ExcludeMaps) && GetArraySize(tempMaps) > GetConVarInt(g_Cvar_ExcludeMaps))
   886 	{
   887 		for (new i = 0; i < GetArraySize(g_OldMapList); i++)
   888 		{
   889 			GetArrayString(g_OldMapList, i, map, sizeof(map));
   890 			index = FindStringInArray(tempMaps, map);
   891 			if (index != -1)
   892 			{
   893 				RemoveFromArray(tempMaps, index);
   894 			}
   895 		}	
   896 	}
   897 
   898 	new limit = (GetConVarInt(g_Cvar_IncludeMaps) < GetArraySize(tempMaps) ? GetConVarInt(g_Cvar_IncludeMaps) : GetArraySize(tempMaps));
   899 	for (new i = 0; i < limit; i++)
   900 	{
   901 		new b = GetRandomInt(0, GetArraySize(tempMaps) - 1);
   902 		GetArrayString(tempMaps, b, map, sizeof(map));		
   903 		PushArrayString(g_NextMapList, map);
   904 		RemoveFromArray(tempMaps, b);
   905 	}
   906 	
   907 	CloseHandle(tempMaps);
   908 }
   909 
   910 bool:CanVoteStart()
   911 {
   912 	if (g_WaitingForVote || g_HasVoteStarted)
   913 	{
   914 		return false;	
   915 	}
   916 	
   917 	return true;
   918 }
   919 
   920 NominateResult:InternalNominateMap(String:map[], bool:force, owner)
   921 {
   922 	if (!IsMapValid(map))
   923 	{
   924 		return Nominate_InvalidMap;
   925 	}
   926 	
   927 	new index;
   928 
   929 	/* Look to replace an existing nomination by this client - Nominations made with owner = 0 aren't replaced */
   930 	if (owner && ((index = FindValueInArray(g_NominateOwners, owner)) != -1))
   931 	{
   932 		new String:oldmap[33];
   933 		GetArrayString(g_NominateList, index, oldmap, sizeof(oldmap));
   934 		Call_StartForward(g_NominationsResetForward);
   935 		Call_PushString(oldmap);
   936 		Call_PushCell(owner);
   937 		Call_Finish();
   938 		
   939 		SetArrayString(g_NominateList, index, map);
   940 		return Nominate_Replaced;
   941 	}
   942 	
   943 	/* Too many nominated maps. */
   944 	if (g_NominateCount >= GetConVarInt(g_Cvar_IncludeMaps) && !force)
   945 	{
   946 		return Nominate_VoteFull;
   947 	}
   948 	
   949 	/* Map already in the vote */
   950 	if (FindStringInArray(g_NominateList, map) != -1)
   951 	{
   952 		return Nominate_AlreadyInVote;	
   953 	}
   954 	
   955 	
   956 	PushArrayString(g_NominateList, map);
   957 	PushArrayCell(g_NominateOwners, owner);
   958 	g_NominateCount++;
   959 	
   960 	while (GetArraySize(g_NominateList) > GetConVarInt(g_Cvar_IncludeMaps))
   961 	{
   962 		new String:oldmap[33];
   963 		GetArrayString(g_NominateList, 0, oldmap, sizeof(oldmap));
   964 		Call_StartForward(g_NominationsResetForward);
   965 		Call_PushString(oldmap);
   966 		Call_PushCell(GetArrayCell(g_NominateOwners, 0));
   967 		Call_Finish();
   968 		
   969 		RemoveFromArray(g_NominateList, 0);
   970 		RemoveFromArray(g_NominateOwners, 0);
   971 	}
   972 	
   973 	return Nominate_Added;
   974 }
   975 
   976 /* Add natives to allow nominate and initiate vote to be call */
   977 
   978 /* native  bool:NominateMap(const String:map[], bool:force, &NominateError:error); */
   979 public Native_NominateMap(Handle:plugin, numParams)
   980 {
   981 	new len;
   982 	GetNativeStringLength(1, len);
   983 	
   984 	if (len <= 0)
   985 	{
   986 	  return false;
   987 	}
   988 	
   989 	new String:map[len+1];
   990 	GetNativeString(1, map, len+1);
   991 	
   992 	return _:InternalNominateMap(map, GetNativeCell(2), GetNativeCell(3));
   993 }
   994 
   995 bool:InternalRemoveNominationByMap(String:map[])
   996 {	
   997 	for (new i = 0; i < GetArraySize(g_NominateList); i++)
   998 	{
   999 		new String:oldmap[33];
  1000 		GetArrayString(g_NominateList, i, oldmap, sizeof(oldmap));
  1001 
  1002 		if(strcmp(map, oldmap, false) == 0)
  1003 		{
  1004 			Call_StartForward(g_NominationsResetForward);
  1005 			Call_PushString(oldmap);
  1006 			Call_PushCell(GetArrayCell(g_NominateOwners, i));
  1007 			Call_Finish();
  1008 
  1009 			RemoveFromArray(g_NominateList, i);
  1010 			RemoveFromArray(g_NominateOwners, i);
  1011 			g_NominateCount--;
  1012 
  1013 			return true;
  1014 		}
  1015 	}
  1016 	
  1017 	return false;
  1018 }
  1019 
  1020 /* native  bool:RemoveNominationByMap(const String:map[]); */
  1021 public Native_RemoveNominationByMap(Handle:plugin, numParams)
  1022 {
  1023 	new len;
  1024 	GetNativeStringLength(1, len);
  1025 	
  1026 	if (len <= 0)
  1027 	{
  1028 	  return false;
  1029 	}
  1030 	
  1031 	new String:map[len+1];
  1032 	GetNativeString(1, map, len+1);
  1033 	
  1034 	return _:InternalRemoveNominationByMap(map);
  1035 }
  1036 
  1037 bool:InternalRemoveNominationByOwner(owner)
  1038 {	
  1039 	new index;
  1040 
  1041 	if (owner && ((index = FindValueInArray(g_NominateOwners, owner)) != -1))
  1042 	{
  1043 		new String:oldmap[33];
  1044 		GetArrayString(g_NominateList, index, oldmap, sizeof(oldmap));
  1045 
  1046 		Call_StartForward(g_NominationsResetForward);
  1047 		Call_PushString(oldmap);
  1048 		Call_PushCell(owner);
  1049 		Call_Finish();
  1050 
  1051 		RemoveFromArray(g_NominateList, index);
  1052 		RemoveFromArray(g_NominateOwners, index);
  1053 		g_NominateCount--;
  1054 
  1055 		return true;
  1056 	}
  1057 	
  1058 	return false;
  1059 }
  1060 
  1061 /* native  bool:RemoveNominationByOwner(owner); */
  1062 public Native_RemoveNominationByOwner(Handle:plugin, numParams)
  1063 {	
  1064 	return _:InternalRemoveNominationByOwner(GetNativeCell(1));
  1065 }
  1066 
  1067 /* native InitiateMapChooserVote(); */
  1068 public Native_InitiateVote(Handle:plugin, numParams)
  1069 {
  1070 	new MapChange:when = MapChange:GetNativeCell(1);
  1071 	new Handle:inputarray = Handle:GetNativeCell(2);
  1072 	
  1073 	LogAction(-1, -1, "Starting map vote because outside request");
  1074 	InitiateVote(when, inputarray);
  1075 }
  1076 
  1077 public Native_CanVoteStart(Handle:plugin, numParams)
  1078 {
  1079 	return CanVoteStart();	
  1080 }
  1081 
  1082 public Native_CheckVoteDone(Handle:plugin, numParams)
  1083 {
  1084 	return g_MapVoteCompleted;
  1085 }
  1086 
  1087 public Native_EndOfMapVoteEnabled(Handle:plugin, numParams)
  1088 {
  1089 	return GetConVarBool(g_Cvar_EndOfMapVote);
  1090 }
  1091 
  1092 public Native_GetExcludeMapList(Handle:plugin, numParams)
  1093 {
  1094 	new Handle:array = Handle:GetNativeCell(1);
  1095 	
  1096 	if (array == INVALID_HANDLE)
  1097 	{
  1098 		return;	
  1099 	}
  1100 	new size = GetArraySize(g_OldMapList);
  1101 	decl String:map[33];
  1102 	
  1103 	for (new i=0; i<size; i++)
  1104 	{
  1105 		GetArrayString(g_OldMapList, i, map, sizeof(map));
  1106 		PushArrayString(array, map);	
  1107 	}
  1108 	
  1109 	return;
  1110 }
  1111 
  1112 public Native_GetNominatedMapList(Handle:plugin, numParams)
  1113 {
  1114 	new Handle:maparray = Handle:GetNativeCell(1);
  1115 	new Handle:ownerarray = Handle:GetNativeCell(2);
  1116 	
  1117 	if (maparray == INVALID_HANDLE)
  1118 		return;
  1119 
  1120 	decl String:map[33];
  1121 
  1122 	for (new i = 0; i < GetArraySize(g_NominateList); i++)
  1123 	{
  1124 		GetArrayString(g_NominateList, i, map, sizeof(map));
  1125 		PushArrayString(maparray, map);
  1126 
  1127 		// If the optional parameter for an owner list was passed, then we need to fill that out as well
  1128 		if(ownerarray != INVALID_HANDLE)
  1129 		{
  1130 			new index = GetArrayCell(g_NominateOwners, i);
  1131 			PushArrayCell(ownerarray, index);
  1132 		}
  1133 	}
  1134 
  1135 	return;
  1136 }