/** * vim: set ts=4 : * ============================================================================= * SourceMod Mapchooser Plugin * Creates a map vote at appropriate times, setting sm_nextmap to the winning * vote * * SourceMod (C)2004-2007 AlliedModders LLC. All rights reserved. * ============================================================================= * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 3.0, as published by the * Free Software Foundation. * * This program 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 the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . * * As a special exception, AlliedModders LLC gives you permission to link the * code of this program (as well as its derivative works) to "Half-Life 2," the * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software * by the Valve Corporation. You must obey the GNU General Public License in * all respects for all other code used. Additionally, AlliedModders LLC grants * this exception to all derivative works. AlliedModders LLC defines further * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), * or . * * Version: $Id$ */ #pragma semicolon 1 #include #include #include #undef REQUIRE_EXTENSIONS #include #define REQUIRE_EXTENSIONS public Plugin:myinfo = { name = "MapChooser", author = "AlliedModders LLC", description = "Automated Map Voting", version = SOURCEMOD_VERSION, url = "http://www.sourcemod.net/" }; /* Valve ConVars */ new Handle:g_Cvar_Winlimit = INVALID_HANDLE; new Handle:g_Cvar_Maxrounds = INVALID_HANDLE; new Handle:g_Cvar_Fraglimit = INVALID_HANDLE; new Handle:g_Cvar_Bonusroundtime = INVALID_HANDLE; /* Plugin ConVars */ new Handle:g_Cvar_StartTime = INVALID_HANDLE; new Handle:g_Cvar_StartRounds = INVALID_HANDLE; new Handle:g_Cvar_StartFrags = INVALID_HANDLE; new Handle:g_Cvar_ExtendTimeStep = INVALID_HANDLE; new Handle:g_Cvar_ExtendRoundStep = INVALID_HANDLE; new Handle:g_Cvar_ExtendFragStep = INVALID_HANDLE; new Handle:g_Cvar_ExcludeMaps = INVALID_HANDLE; new Handle:g_Cvar_IncludeMaps = INVALID_HANDLE; new Handle:g_Cvar_NoVoteMode = INVALID_HANDLE; new Handle:g_Cvar_Extend = INVALID_HANDLE; new Handle:g_Cvar_DontChange = INVALID_HANDLE; new Handle:g_Cvar_EndOfMapVote = INVALID_HANDLE; new Handle:g_Cvar_VoteDuration = INVALID_HANDLE; new Handle:g_VoteTimer = INVALID_HANDLE; new Handle:g_RetryTimer = INVALID_HANDLE; /* Data Handles */ new Handle:g_MapList = INVALID_HANDLE; new Handle:g_NominateList = INVALID_HANDLE; new Handle:g_NominateOwners = INVALID_HANDLE; new Handle:g_OldMapList = INVALID_HANDLE; new Handle:g_NextMapList = INVALID_HANDLE; new Handle:g_VoteMenu = INVALID_HANDLE; new g_Extends; new g_TotalRounds; new bool:g_HasVoteStarted; new bool:g_WaitingForVote; new bool:g_MapVoteCompleted; new bool:g_ChangeMapAtRoundEnd; new g_mapFileSerial = -1; new g_NominateCount = 0; new MapChange:g_ChangeTime; new Handle:g_NominationsResetForward = INVALID_HANDLE; /* Upper bound of how many team there could be */ #define MAXTEAMS 10 new g_winCount[MAXTEAMS]; #define VOTE_EXTEND "##extend##" #define VOTE_DONTCHANGE "##dontchange##" public OnPluginStart() { LoadTranslations("mapchooser.phrases"); new arraySize = ByteCountToCells(33); g_MapList = CreateArray(arraySize); g_NominateList = CreateArray(arraySize); g_NominateOwners = CreateArray(1); g_OldMapList = CreateArray(arraySize); g_NextMapList = CreateArray(arraySize); g_Cvar_EndOfMapVote = CreateConVar("sm_mapvote_endvote", "1", "Specifies if MapChooser should run an end of map vote", _, true, 0.0, true, 1.0); HookConVarChange(g_Cvar_EndOfMapVote, EndVoteChanged); g_Cvar_StartTime = CreateConVar("sm_mapvote_start", "3.0", "Specifies when to start the vote based on time remaining.", _, true, 1.0); 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); g_Cvar_StartFrags = CreateConVar("sm_mapvote_startfrags", "5.0", "Specifies when to start the vote base on frags remaining.", _, true, 1.0); g_Cvar_ExtendTimeStep = CreateConVar("sm_extendmap_timestep", "15", "Specifies how much many more minutes each extension makes", _, true, 5.0); g_Cvar_ExtendRoundStep = CreateConVar("sm_extendmap_roundstep", "5", "Specifies how many more rounds each extension makes", _, true, 1.0); g_Cvar_ExtendFragStep = CreateConVar("sm_extendmap_fragstep", "10", "Specifies how many more frags are allowed when map is extended.", _, true, 5.0); g_Cvar_ExcludeMaps = CreateConVar("sm_mapvote_exclude", "5", "Specifies how many past maps to exclude from the vote.", _, true, 0.0); g_Cvar_IncludeMaps = CreateConVar("sm_mapvote_include", "5", "Specifies how many maps to include in the vote.", _, true, 2.0, true, 6.0); 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); g_Cvar_Extend = CreateConVar("sm_mapvote_extend", "0", "Number of extensions allowed each map.", _, true, 0.0); g_Cvar_DontChange = CreateConVar("sm_mapvote_dontchange", "1", "Specifies if a 'Don't Change' option should be added to early votes", _, true, 0.0); g_Cvar_VoteDuration = CreateConVar("sm_mapvote_voteduration", "20", "Specifies how long the mapvote should be available for.", _, true, 5.0, true, 25.0); RegAdminCmd("sm_mapvote", Command_Mapvote, ADMFLAG_CHANGEMAP, "sm_mapvote - Forces MapChooser to attempt to run a map vote now."); g_Cvar_Winlimit = FindConVar("mp_winlimit"); g_Cvar_Maxrounds = FindConVar("mp_maxrounds"); g_Cvar_Fraglimit = FindConVar("mp_fraglimit"); g_Cvar_Bonusroundtime = FindConVar("mp_bonusroundtime"); if (g_Cvar_Winlimit != INVALID_HANDLE || g_Cvar_Maxrounds != INVALID_HANDLE) { HookEvent("round_end", Event_RoundEnd); HookEventEx("teamplay_win_panel", Event_TeamPlayWinPanel); HookEventEx("teamplay_restart_round", Event_TFRestartRound); } if (g_Cvar_Fraglimit != INVALID_HANDLE) { HookEvent("player_death", Event_PlayerDeath); } AutoExecConfig(true, "mapchooser"); //Change the mp_bonusroundtime max so that we have time to display the vote //If you display a vote during bonus time good defaults are 17 vote duration and 19 mp_bonustime if (g_Cvar_Bonusroundtime != INVALID_HANDLE) { SetConVarBounds(g_Cvar_Bonusroundtime, ConVarBound_Upper, true, 30.0); } g_NominationsResetForward = CreateGlobalForward("OnNominationRemoved", ET_Ignore, Param_String, Param_Cell); } public bool:AskPluginLoad(Handle:myself, bool:late, String:error[], err_max) { RegPluginLibrary("mapchooser"); CreateNative("NominateMap", Native_NominateMap); CreateNative("InitiateMapChooserVote", Native_InitiateVote); CreateNative("CanMapChooserStartVote", Native_CanVoteStart); CreateNative("HasEndOfMapVoteFinished", Native_CheckVoteDone); CreateNative("GetExcludeMapList", Native_GetExcludeMapList); return true; } public OnConfigsExecuted() { if (ReadMapList(g_MapList, g_mapFileSerial, "mapchooser", MAPLIST_FLAG_CLEARARRAY|MAPLIST_FLAG_MAPSFOLDER) != INVALID_HANDLE) { if (g_mapFileSerial == -1) { LogError("Unable to create a valid map list."); } } CreateNextVote(); SetupTimeleftTimer(); g_TotalRounds = 0; g_Extends = 0; g_MapVoteCompleted = false; g_NominateCount = 0; ClearArray(g_NominateList); ClearArray(g_NominateOwners); for (new i=0; i GetConVarInt(g_Cvar_ExcludeMaps)) { RemoveFromArray(g_OldMapList, 0); } } public OnClientDisconnect(client) { new index = FindValueInArray(g_NominateOwners, client); if (index == -1) { return; } new String:oldmap[33]; GetArrayString(g_NominateList, index, oldmap, sizeof(oldmap)); Call_StartForward(g_NominationsResetForward); Call_PushString(oldmap); Call_PushCell(GetArrayCell(g_NominateOwners, index)); Call_Finish(); RemoveFromArray(g_NominateOwners, index); RemoveFromArray(g_NominateList, index); g_NominateCount--; } public EndVoteChanged(Handle:convar, const String:oldValue[], const String:newValue[]) { if (newValue[0] == '1') { SetNextMap("Pending Vote"); } else { SetNextMap(""); } } public OnMapTimeLeftChanged() { if (GetArraySize(g_MapList)) { SetupTimeleftTimer(); } } SetupTimeleftTimer() { new time; if (GetMapTimeLeft(time) && time > 0) { new startTime = GetConVarInt(g_Cvar_StartTime) * 60; if (time - startTime < 0 && GetConVarBool(g_Cvar_EndOfMapVote) && !g_MapVoteCompleted && !g_HasVoteStarted) { InitiateVote(MapChange_MapEnd, INVALID_HANDLE); } else { if (g_VoteTimer != INVALID_HANDLE) { KillTimer(g_VoteTimer); g_VoteTimer = INVALID_HANDLE; } g_VoteTimer = CreateTimer(float(time - startTime), Timer_StartMapVote); } } } public Action:Timer_StartMapVote(Handle:timer) { if (!GetArraySize(g_MapList) || !GetConVarBool(g_Cvar_EndOfMapVote) || g_MapVoteCompleted || g_HasVoteStarted) { return Plugin_Stop; } if (timer == g_RetryTimer) { g_RetryTimer = INVALID_HANDLE; } else { g_VoteTimer = INVALID_HANDLE; } InitiateVote(MapChange_MapEnd, INVALID_HANDLE); return Plugin_Stop; } public Event_TFRestartRound(Handle:event, const String:name[], bool:dontBroadcast) { /* Game got restarted - reset our round count tracking */ g_TotalRounds = 0; } public Event_TeamPlayWinPanel(Handle:event, const String:name[], bool:dontBroadcast) { if (g_ChangeMapAtRoundEnd) { g_ChangeMapAtRoundEnd = false; CreateTimer(2.0, Timer_ChangeMap, INVALID_HANDLE); } if (!GetArraySize(g_MapList) || g_HasVoteStarted || g_MapVoteCompleted || !GetConVarBool(g_Cvar_EndOfMapVote)) { return; } new bluescore = GetEventInt(event, "blue_score"); new redscore = GetEventInt(event, "red_score"); if(GetEventInt(event, "round_complete") == 1) { switch(GetEventInt(event, "winning_team")) { case TFTeam_Blue: { CheckWinLimit(bluescore); } case TFTeam_Red: { CheckWinLimit(redscore); } //We need to do nothing on winning_team == 0 this indicates stalemate. default: { return; } } g_TotalRounds++; CheckMaxRounds(g_TotalRounds); } } /* You ask, why don't you just use team_score event? And I answer... Because CSS doesn't. */ public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast) { if (g_ChangeMapAtRoundEnd) { g_ChangeMapAtRoundEnd = false; CreateTimer(2.0, Timer_ChangeMap, INVALID_HANDLE); } if (!GetArraySize(g_MapList) || g_HasVoteStarted || g_MapVoteCompleted) { return; } new winner = GetEventInt(event, "winner"); if (winner == 0 || winner == 1 || !GetConVarBool(g_Cvar_EndOfMapVote)) { return; } if (winner >= MAXTEAMS) { SetFailState("Mod exceed maximum team count - Please file a bug report."); } g_TotalRounds++; g_winCount[winner]++; CheckWinLimit(g_winCount[winner]); CheckMaxRounds(g_TotalRounds); } public CheckWinLimit(winner_score) { if (g_Cvar_Winlimit != INVALID_HANDLE) { new winlimit = GetConVarInt(g_Cvar_Winlimit); if (winlimit) { if (winner_score >= (winlimit - GetConVarInt(g_Cvar_StartRounds))) { InitiateVote(MapChange_MapEnd, INVALID_HANDLE); } } } } public CheckMaxRounds(roundcount) { if (g_Cvar_Maxrounds != INVALID_HANDLE) { new maxrounds = GetConVarInt(g_Cvar_Maxrounds); if (maxrounds) { if (roundcount >= (maxrounds - GetConVarInt(g_Cvar_StartRounds))) { InitiateVote(MapChange_MapEnd, INVALID_HANDLE); } } } } public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast) { if (!GetArraySize(g_MapList) || g_Cvar_Fraglimit == INVALID_HANDLE || g_HasVoteStarted) { return; } if (!GetConVarInt(g_Cvar_Fraglimit) || !GetConVarBool(g_Cvar_EndOfMapVote)) { return; } new fragger = GetClientOfUserId(GetEventInt(event, "attacker")); if (fragger && GetClientFrags(fragger) >= (GetConVarInt(g_Cvar_Fraglimit) - GetConVarInt(g_Cvar_StartFrags))) { InitiateVote(MapChange_MapEnd, INVALID_HANDLE); } } public Action:Command_Mapvote(client, args) { InitiateVote(MapChange_MapEnd, INVALID_HANDLE); return Plugin_Handled; } InitiateVote(MapChange:when, Handle:inputlist) { if (!CanVoteStart()) { return; } g_WaitingForVote = true; if (IsVoteInProgress()) { // Can't start a vote, try again in 5 seconds. g_RetryTimer = CreateTimer(5.0, Timer_StartMapVote); return; } g_ChangeTime = when; g_WaitingForVote = false; g_HasVoteStarted = true; g_VoteMenu = CreateMenu(Handler_MapVoteMenu, MenuAction:MENU_ACTIONS_ALL); SetMenuTitle(g_VoteMenu, "Vote Nextmap"); SetVoteResultCallback(g_VoteMenu, Handler_MapVoteFinished); /** * TODO: Make a proper decision on when to clear the nominations list. * Currently it clears when used, and stays if an external list is provided. * Is this the right thing to do? External lists will probably come from places * like sm_mapvote from the adminmenu in the future. */ decl String:map[32]; /* No input given - User our internal nominations and maplist */ if (inputlist == INVALID_HANDLE) { new nominateCount = GetArraySize(g_NominateList); new voteSize = GetConVarInt(g_Cvar_IncludeMaps); /* Smaller of the two - It should be impossible for nominations to exceed the size though (cvar changed mid-map?) */ new nominationsToAdd = nominateCount >= voteSize ? voteSize : nominateCount; for (new i=0; i= availableMaps) { //Run out of maps, this will have to do. break; } } /* Wipe out our nominations list - Nominations have already been informed of this */ ClearArray(g_NominateOwners); ClearArray(g_NominateList); } else //We were given a list of maps to start the vote with { new size = GetArraySize(inputlist); for (new i=0; i 0) { ExtendMapTimeLimit(GetConVarInt(g_Cvar_ExtendTimeStep)*60); } } if (g_Cvar_Winlimit != INVALID_HANDLE) { new winlimit = GetConVarInt(g_Cvar_Winlimit); if (winlimit) { SetConVarInt(g_Cvar_Winlimit, winlimit + GetConVarInt(g_Cvar_ExtendRoundStep)); } } if (g_Cvar_Maxrounds != INVALID_HANDLE) { new maxrounds = GetConVarInt(g_Cvar_Maxrounds); if (maxrounds) { SetConVarInt(g_Cvar_Maxrounds, maxrounds + GetConVarInt(g_Cvar_ExtendRoundStep)); } } if (g_Cvar_Fraglimit != INVALID_HANDLE) { new fraglimit = GetConVarInt(g_Cvar_Fraglimit); if (fraglimit) { SetConVarInt(g_Cvar_Fraglimit, fraglimit + GetConVarInt(g_Cvar_ExtendFragStep)); } } PrintToChatAll("[SM] %t", "Current Map Extended", RoundToFloor(float(item_info[0][VOTEINFO_ITEM_VOTES])/float(num_votes)*100), num_votes); LogMessage("Voting for next map has finished. The current map has been extended."); // We extended, so we'll have to vote again. g_HasVoteStarted = false; CreateNextVote(); SetupTimeleftTimer(); } else if (strcmp(map, VOTE_DONTCHANGE, false) == 0) { PrintToChatAll("[SM] %t", "Current Map Stays", RoundToFloor(float(item_info[0][VOTEINFO_ITEM_VOTES])/float(num_votes)*100), num_votes); LogMessage("Voting for next map has finished. 'No Change' was the winner"); g_HasVoteStarted = false; CreateNextVote(); SetupTimeleftTimer(); } else { if (g_ChangeTime == MapChange_MapEnd) { SetNextMap(map); } else if (g_ChangeTime == MapChange_Instant) { new Handle:data; CreateDataTimer(2.0, Timer_ChangeMap, data); WritePackString(data, map); } else // MapChange_RoundEnd { SetNextMap(map); g_ChangeMapAtRoundEnd = true; } g_HasVoteStarted = false; g_MapVoteCompleted = true; PrintToChatAll("[SM] %t", "Nextmap Voting Finished", map, RoundToFloor(float(item_info[0][VOTEINFO_ITEM_VOTES])/float(num_votes)*100), num_votes); LogMessage("Voting for next map has finished. Nextmap: %s.", map); } } public Handler_MapVoteMenu(Handle:menu, MenuAction:action, param1, param2) { switch (action) { case MenuAction_End: { g_VoteMenu = INVALID_HANDLE; CloseHandle(menu); } case MenuAction_Display: { decl String:buffer[255]; Format(buffer, sizeof(buffer), "%T", "Vote Nextmap", param1); new Handle:panel = Handle:param2; SetPanelTitle(panel, buffer); } case MenuAction_DisplayItem: { if (GetMenuItemCount(menu) - 1 == param2) { decl String:map[64], String:buffer[255]; GetMenuItem(menu, param2, map, sizeof(map)); if (strcmp(map, VOTE_EXTEND, false) == 0) { Format(buffer, sizeof(buffer), "%T", "Extend Map", param1); return RedrawMenuItem(buffer); } else if (strcmp(map, VOTE_DONTCHANGE, false) == 0) { Format(buffer, sizeof(buffer), "%T", "Dont Change", param1); return RedrawMenuItem(buffer); } } } case MenuAction_VoteCancel: { // If we receive 0 votes, pick at random. if (param1 == VoteCancel_NoVotes && GetConVarBool(g_Cvar_NoVoteMode)) { new count = GetMenuItemCount(menu); new item = GetRandomInt(0, count - 1); decl String:map[32]; GetMenuItem(menu, item, map, sizeof(map)); while (strcmp(map, VOTE_EXTEND, false) == 0) { item = GetRandomInt(0, count - 1); GetMenuItem(menu, item, map, sizeof(map)); } SetNextMap(map); } else { // We were actually cancelled. What should we do? g_HasVoteStarted = false; CreateNextVote(); SetupTimeleftTimer(); } } } return 0; } public Action:Timer_ChangeMap(Handle:hTimer, Handle:dp) { new String:map[65]; if (dp == INVALID_HANDLE) { if (!GetNextMap(map, sizeof(map))) { //No passed map and no set nextmap. fail! return Plugin_Stop; } } else { ResetPack(dp); ReadPackString(dp, map, sizeof(map)); } ServerCommand("changelevel \"%s\"", map); return Plugin_Stop; } CreateNextVote() { if(g_NextMapList != INVALID_HANDLE) { ClearArray(g_NextMapList); } decl String:map[32]; new index, Handle:tempMaps = CloneArray(g_MapList); GetCurrentMap(map, sizeof(map)); index = FindStringInArray(tempMaps, map); if (index != -1) { RemoveFromArray(tempMaps, index); } if (GetConVarInt(g_Cvar_ExcludeMaps) && GetArraySize(tempMaps) > GetConVarInt(g_Cvar_ExcludeMaps)) { for (new i = 0; i < GetArraySize(g_OldMapList); i++) { GetArrayString(g_OldMapList, i, map, sizeof(map)); index = FindStringInArray(tempMaps, map); if (index != -1) { RemoveFromArray(tempMaps, index); } } } new limit = (GetConVarInt(g_Cvar_IncludeMaps) < GetArraySize(tempMaps) ? GetConVarInt(g_Cvar_IncludeMaps) : GetArraySize(tempMaps)); for (new i = 0; i < limit; i++) { new b = GetRandomInt(0, GetArraySize(tempMaps) - 1); GetArrayString(tempMaps, b, map, sizeof(map)); PushArrayString(g_NextMapList, map); RemoveFromArray(tempMaps, b); } CloseHandle(tempMaps); } bool:CanVoteStart() { if (g_WaitingForVote || g_HasVoteStarted) { return false; } return true; } NominateResult:InternalNominateMap(String:map[], bool:force, owner) { if (!IsMapValid(map)) { return Nominate_InvalidMap; } new index; if ((index = FindValueInArray(g_NominateOwners, owner)) != -1) { new String:oldmap[33]; GetArrayString(g_NominateList, index, oldmap, sizeof(oldmap)); Call_StartForward(g_NominationsResetForward); Call_PushString(oldmap); Call_PushCell(owner); Call_Finish(); SetArrayString(g_NominateList, index, map); return Nominate_Replaced; } /* Too many nominated maps. */ if (g_NominateCount >= GetConVarInt(g_Cvar_IncludeMaps) && !force) { return Nominate_VoteFull; } /* Map already in the vote */ if (FindStringInArray(g_NominateList, map) != -1) { return Nominate_AlreadyInVote; } PushArrayString(g_NominateList, map); PushArrayCell(g_NominateOwners, owner); g_NominateCount++; while (GetArraySize(g_NominateList) > GetConVarInt(g_Cvar_IncludeMaps)) { new String:oldmap[33]; GetArrayString(g_NominateList, 0, oldmap, sizeof(oldmap)); Call_StartForward(g_NominationsResetForward); Call_PushString(oldmap); Call_PushCell(GetArrayCell(g_NominateOwners, 0)); Call_Finish(); RemoveFromArray(g_NominateList, 0); RemoveFromArray(g_NominateOwners, 0); } return Nominate_Added; } /* Add natives to allow nominate and initiate vote to be call */ /* native bool:NominateMap(const String:map[], bool:force, &NominateError:error); */ public Native_NominateMap(Handle:plugin, numParams) { new len; GetNativeStringLength(1, len); if (len <= 0) { return false; } new String:map[len+1]; GetNativeString(1, map, len+1); return _:InternalNominateMap(map, GetNativeCell(2), GetNativeCell(3)); } /* native InitiateMapChooserVote(); */ public Native_InitiateVote(Handle:plugin, numParams) { new MapChange:when = MapChange:GetNativeCell(1); new Handle:inputarray = Handle:GetNativeCell(2); LogMessage("Starting map vote because outside request"); InitiateVote(when, inputarray); } public Native_CanVoteStart(Handle:plugin, numParams) { return CanVoteStart(); } public Native_CheckVoteDone(Handle:plugin, numParams) { return g_MapVoteCompleted; } public Native_GetExcludeMapList(Handle:plugin, numParams) { new Handle:array = Handle:GetNativeCell(1); if (array == INVALID_HANDLE) { return; } new size = GetArraySize(g_OldMapList); decl String:map[33]; for (new i=0; i