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