diff --git a/lib/linguist/languages.yml b/lib/linguist/languages.yml index 9ba89a2f..c226172d 100644 --- a/lib/linguist/languages.yml +++ b/lib/linguist/languages.yml @@ -1720,6 +1720,13 @@ Smalltalk: Smarty: primary_extension: .tpl +SourcePawn: + type: programming + color: "#f69e1d" + aliases: + - sourcemod + primary_extension: .sp + Squirrel: type: programming lexer: C++ diff --git a/lib/linguist/samples.json b/lib/linguist/samples.json index 863e0b74..9864f7a1 100644 --- a/lib/linguist/samples.json +++ b/lib/linguist/samples.json @@ -455,6 +455,9 @@ "Slash": [ ".sl" ], + "SourcePawn": [ + ".sp" + ], "Squirrel": [ ".nut" ], @@ -597,8 +600,8 @@ ".gemrc" ] }, - "tokens_total": 523444, - "languages_total": 634, + "tokens_total": 525524, + "languages_total": 635, "tokens": { "ABAP": { "*/**": 1, @@ -49324,6 +49327,343 @@ "ast.eval": 1, "Env.new": 1 }, + "SourcePawn": { + "//#define": 1, + "DEBUG": 2, + "#if": 1, + "defined": 1, + "#define": 7, + "assert": 2, + "(": 233, + "%": 18, + ")": 234, + "if": 44, + "ThrowError": 2, + ";": 213, + "assert_msg": 2, + "#else": 1, + "#endif": 1, + "#pragma": 1, + "semicolon": 1, + "#include": 3, + "": 1, + "": 1, + "": 1, + "public": 21, + "Plugin": 1, + "myinfo": 1, + "{": 73, + "name": 7, + "author": 1, + "description": 1, + "version": 1, + "SOURCEMOD_VERSION": 1, + "url": 1, + "}": 71, + "new": 62, + "Handle": 51, + "g_Cvar_Winlimit": 5, + "INVALID_HANDLE": 56, + "g_Cvar_Maxrounds": 5, + "g_Cvar_Fraglimit": 6, + "g_Cvar_Bonusroundtime": 6, + "g_Cvar_StartTime": 3, + "g_Cvar_StartRounds": 5, + "g_Cvar_StartFrags": 3, + "g_Cvar_ExtendTimeStep": 2, + "g_Cvar_ExtendRoundStep": 2, + "g_Cvar_ExtendFragStep": 2, + "g_Cvar_ExcludeMaps": 3, + "g_Cvar_IncludeMaps": 2, + "g_Cvar_NoVoteMode": 2, + "g_Cvar_Extend": 2, + "g_Cvar_DontChange": 2, + "g_Cvar_EndOfMapVote": 8, + "g_Cvar_VoteDuration": 3, + "g_Cvar_RunOff": 2, + "g_Cvar_RunOffPercent": 2, + "g_VoteTimer": 7, + "g_RetryTimer": 4, + "g_MapList": 8, + "g_NominateList": 7, + "g_NominateOwners": 7, + "g_OldMapList": 7, + "g_NextMapList": 2, + "g_VoteMenu": 1, + "g_Extends": 2, + "g_TotalRounds": 7, + "bool": 10, + "g_HasVoteStarted": 7, + "g_WaitingForVote": 4, + "g_MapVoteCompleted": 9, + "g_ChangeMapAtRoundEnd": 6, + "g_ChangeMapInProgress": 4, + "g_mapFileSerial": 3, + "-": 12, + "g_NominateCount": 3, + "MapChange": 4, + "g_ChangeTime": 1, + "g_NominationsResetForward": 3, + "g_MapVoteStartedForward": 2, + "MAXTEAMS": 4, + "g_winCount": 4, + "[": 19, + "]": 19, + "VOTE_EXTEND": 1, + "VOTE_DONTCHANGE": 1, + "OnPluginStart": 1, + "LoadTranslations": 2, + "arraySize": 5, + "ByteCountToCells": 1, + "PLATFORM_MAX_PATH": 6, + "CreateArray": 5, + "CreateConVar": 15, + "_": 18, + "true": 26, + "RegAdminCmd": 2, + "Command_Mapvote": 2, + "ADMFLAG_CHANGEMAP": 2, + "Command_SetNextmap": 2, + "FindConVar": 4, + "||": 15, + "decl": 5, + "String": 11, + "folder": 5, + "GetGameFolderName": 1, + "sizeof": 6, + "strcmp": 3, + "HookEvent": 6, + "Event_TeamPlayWinPanel": 3, + "Event_TFRestartRound": 2, + "else": 5, + "Event_RoundEnd": 3, + "Event_PlayerDeath": 2, + "AutoExecConfig": 1, + "//Change": 1, + "the": 5, + "mp_bonusroundtime": 1, + "max": 1, + "so": 1, + "that": 2, + "we": 2, + "have": 2, + "time": 9, + "to": 4, + "display": 2, + "vote": 6, + "//If": 1, + "you": 1, + "a": 1, + "during": 2, + "bonus": 2, + "good": 1, + "defaults": 1, + "are": 1, + "duration": 1, + "and": 1, + "mp_bonustime": 1, + "SetConVarBounds": 1, + "ConVarBound_Upper": 1, + "CreateGlobalForward": 2, + "ET_Ignore": 2, + "Param_String": 1, + "Param_Cell": 1, + "APLRes": 1, + "AskPluginLoad2": 1, + "myself": 1, + "late": 1, + "error": 1, + "err_max": 1, + "RegPluginLibrary": 1, + "CreateNative": 9, + "Native_NominateMap": 1, + "Native_RemoveNominationByMap": 1, + "Native_RemoveNominationByOwner": 1, + "Native_InitiateVote": 1, + "Native_CanVoteStart": 2, + "Native_CheckVoteDone": 2, + "Native_GetExcludeMapList": 2, + "Native_GetNominatedMapList": 2, + "Native_EndOfMapVoteEnabled": 2, + "return": 23, + "APLRes_Success": 1, + "OnConfigsExecuted": 1, + "ReadMapList": 1, + "MAPLIST_FLAG_CLEARARRAY": 1, + "|": 1, + "MAPLIST_FLAG_MAPSFOLDER": 1, + "LogError": 2, + "CreateNextVote": 1, + "SetupTimeleftTimer": 3, + "false": 8, + "ClearArray": 2, + "for": 9, + "i": 13, + "<": 5, + "+": 12, + "&&": 5, + "GetConVarInt": 10, + "GetConVarFloat": 2, + "<=>": 1, + "Warning": 1, + "Bonus": 1, + "Round": 1, + "Time": 2, + "shorter": 1, + "than": 1, + "Vote": 4, + "Votes": 1, + "round": 1, + "may": 1, + "not": 1, + "complete": 1, + "OnMapEnd": 1, + "map": 27, + "GetCurrentMap": 1, + "PushArrayString": 3, + "GetArraySize": 8, + "RemoveFromArray": 3, + "OnClientDisconnect": 1, + "client": 9, + "index": 8, + "FindValueInArray": 1, + "oldmap": 4, + "GetArrayString": 3, + "Call_StartForward": 1, + "Call_PushString": 1, + "Call_PushCell": 1, + "GetArrayCell": 2, + "Call_Finish": 1, + "Action": 3, + "args": 3, + "ReplyToCommand": 2, + "Plugin_Handled": 4, + "GetCmdArg": 1, + "IsMapValid": 1, + "ShowActivity": 1, + "LogAction": 1, + "SetNextMap": 1, + "OnMapTimeLeftChanged": 1, + "GetMapTimeLeft": 1, + "startTime": 4, + "*": 1, + "GetConVarBool": 6, + "InitiateVote": 8, + "MapChange_MapEnd": 6, + "KillTimer": 1, + "//g_VoteTimer": 1, + "CreateTimer": 3, + "float": 2, + "Timer_StartMapVote": 3, + "TIMER_FLAG_NO_MAPCHANGE": 4, + "data": 8, + "CreateDataTimer": 1, + "WritePackCell": 2, + "ResetPack": 1, + "timer": 2, + "Plugin_Stop": 2, + "mapChange": 2, + "ReadPackCell": 2, + "hndl": 2, + "event": 11, + "const": 4, + "dontBroadcast": 4, + "Timer_ChangeMap": 2, + "bluescore": 2, + "GetEventInt": 7, + "redscore": 2, + "StrEqual": 1, + "CheckMaxRounds": 3, + "switch": 1, + "case": 2, + "CheckWinLimit": 4, + "//We": 1, + "need": 2, + "do": 1, + "nothing": 1, + "on": 1, + "winning_team": 1, + "this": 1, + "indicates": 1, + "stalemate.": 1, + "default": 1, + "winner": 9, + "//": 3, + "Nuclear": 1, + "Dawn": 1, + "SetFailState": 1, + "winner_score": 2, + "winlimit": 3, + "roundcount": 2, + "maxrounds": 3, + "fragger": 3, + "GetClientOfUserId": 1, + "GetClientFrags": 1, + "when": 2, + "inputlist": 1, + "IsVoteInProgress": 1, + "Can": 1, + "t": 7, + "be": 1, + "excluded": 1, + "from": 1, + "as": 2, + "they": 1, + "weren": 1, + "nominationsToAdd": 1, + "Change": 2, + "Extend": 2, + "Map": 5, + "Voting": 7, + "next": 5, + "has": 5, + "started.": 1, + "SM": 5, + "Nextmap": 5, + "Started": 1, + "Current": 2, + "Extended": 1, + "finished.": 3, + "The": 1, + "current": 1, + "been": 1, + "extended.": 1, + "Stays": 1, + "was": 3, + "Finished": 1, + "s.": 1, + "Runoff": 2, + "Starting": 2, + "indecisive": 1, + "beginning": 1, + "runoff": 1, + "T": 3, + "Dont": 1, + "because": 1, + "outside": 1, + "request": 1, + "inputarray": 1, + "plugin": 5, + "numParams": 5, + "CanVoteStart": 1, + "array": 3, + "GetNativeCell": 3, + "size": 2, + "maparray": 3, + "ownerarray": 3, + "If": 1, + "optional": 1, + "parameter": 1, + "an": 1, + "owner": 1, + "list": 1, + "passed": 1, + "then": 1, + "fill": 1, + "out": 1, + "well": 1, + "PushArrayCell": 1 + }, "Squirrel": { "//example": 1, "from": 1, @@ -54106,6 +54446,7 @@ "Shell": 3744, "Shen": 3472, "Slash": 187, + "SourcePawn": 2080, "Squirrel": 130, "Standard ML": 6405, "Stylus": 76, @@ -54264,6 +54605,7 @@ "Shell": 37, "Shen": 3, "Slash": 1, + "SourcePawn": 1, "Squirrel": 1, "Standard ML": 4, "Stylus": 1, @@ -54290,5 +54632,5 @@ "Xtend": 2, "YAML": 2 }, - "md5": "856e1bcc53c067f8a7e56e6aa1563b0e" + "md5": "bea2304b61e5920c044b83aec7788dd8" } \ No newline at end of file diff --git a/samples/SourcePawn/foo.sp b/samples/SourcePawn/foo.sp new file mode 100644 index 00000000..3d50b3ac --- /dev/null +++ b/samples/SourcePawn/foo.sp @@ -0,0 +1,1192 @@ +/** + * 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$ + */ + +//#define DEBUG + +#if defined DEBUG + #define assert(%1) if (!(%1)) ThrowError("Debug Assertion Failed"); + #define assert_msg(%1,%2) if (!(%1)) ThrowError(%2); +#else + #define assert(%1) + #define assert_msg(%1,%2) +#endif + +#pragma semicolon 1 +#include +#include +#include + +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_Cvar_RunOff = INVALID_HANDLE; +new Handle:g_Cvar_RunOffPercent = 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 bool:g_ChangeMapInProgress; +new g_mapFileSerial = -1; + +new g_NominateCount = 0; +new MapChange:g_ChangeTime; + +new Handle:g_NominationsResetForward = INVALID_HANDLE; +new Handle:g_MapVoteStartedForward = 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"); + LoadTranslations("common.phrases"); + + new arraySize = ByteCountToCells(PLATFORM_MAX_PATH); + 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); + + 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); + g_Cvar_RunOff = CreateConVar("sm_mapvote_runoff", "0", "Hold run of votes if winning choice is less than a certain margin", _, true, 0.0, true, 1.0); + g_Cvar_RunOffPercent = CreateConVar("sm_mapvote_runoffpercent", "50", "If winning choice has less than this percent of votes, hold a runoff", _, true, 0.0, true, 100.0); + + RegAdminCmd("sm_mapvote", Command_Mapvote, ADMFLAG_CHANGEMAP, "sm_mapvote - Forces MapChooser to attempt to run a map vote now."); + RegAdminCmd("sm_setnextmap", Command_SetNextmap, ADMFLAG_CHANGEMAP, "sm_setnextmap "); + + 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) + { + decl String:folder[64]; + GetGameFolderName(folder, sizeof(folder)); + + if (strcmp(folder, "tf") == 0) + { + HookEvent("teamplay_win_panel", Event_TeamPlayWinPanel); + HookEvent("teamplay_restart_round", Event_TFRestartRound); + HookEvent("arena_win_panel", Event_TeamPlayWinPanel); + } + else if (strcmp(folder, "nucleardawn") == 0) + { + HookEvent("round_win", Event_RoundEnd); + } + else + { + HookEvent("round_end", Event_RoundEnd); + } + } + + 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); + g_MapVoteStartedForward = CreateGlobalForward("OnMapVoteStarted", ET_Ignore); +} + +public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) +{ + RegPluginLibrary("mapchooser"); + + CreateNative("NominateMap", Native_NominateMap); + CreateNative("RemoveNominationByMap", Native_RemoveNominationByMap); + CreateNative("RemoveNominationByOwner", Native_RemoveNominationByOwner); + CreateNative("InitiateMapChooserVote", Native_InitiateVote); + CreateNative("CanMapChooserStartVote", Native_CanVoteStart); + CreateNative("HasEndOfMapVoteFinished", Native_CheckVoteDone); + CreateNative("GetExcludeMapList", Native_GetExcludeMapList); + CreateNative("GetNominatedMapList", Native_GetNominatedMapList); + CreateNative("EndOfMapVoteEnabled", Native_EndOfMapVoteEnabled); + + return APLRes_Success; +} + +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[PLATFORM_MAX_PATH]; + 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 Action:Command_SetNextmap(client, args) +{ + if (args < 1) + { + ReplyToCommand(client, "[SM] Usage: sm_setnextmap "); + return Plugin_Handled; + } + + decl String:map[PLATFORM_MAX_PATH]; + GetCmdArg(1, map, sizeof(map)); + + if (!IsMapValid(map)) + { + ReplyToCommand(client, "[SM] %t", "Map was not found", map); + return Plugin_Handled; + } + + ShowActivity(client, "%t", "Changed Next Map", map); + LogAction(client, -1, "\"%L\" changed nextmap to \"%s\"", client, map); + + SetNextMap(map); + g_MapVoteCompleted = true; + + return Plugin_Handled; +} + +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, _, TIMER_FLAG_NO_MAPCHANGE); + new Handle:data; + g_VoteTimer = CreateDataTimer(float(time - startTime), Timer_StartMapVote, data, TIMER_FLAG_NO_MAPCHANGE); + WritePackCell(data, _:MapChange_MapEnd); + WritePackCell(data, _:INVALID_HANDLE); + ResetPack(data); + } + } +} + +public Action:Timer_StartMapVote(Handle:timer, Handle:data) +{ + if (timer == g_RetryTimer) + { + g_WaitingForVote = false; + g_RetryTimer = INVALID_HANDLE; + } + else + { + g_VoteTimer = INVALID_HANDLE; + } + + if (!GetArraySize(g_MapList) || !GetConVarBool(g_Cvar_EndOfMapVote) || g_MapVoteCompleted || g_HasVoteStarted) + { + return Plugin_Stop; + } + + new MapChange:mapChange = MapChange:ReadPackCell(data); + new Handle:hndl = Handle:ReadPackCell(data); + + InitiateVote(mapChange, hndl); + + 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, TIMER_FLAG_NO_MAPCHANGE); + g_ChangeMapInProgress = true; + } + + new bluescore = GetEventInt(event, "blue_score"); + new redscore = GetEventInt(event, "red_score"); + + if(GetEventInt(event, "round_complete") == 1 || StrEqual(name, "arena_win_panel")) + { + g_TotalRounds++; + + if (!GetArraySize(g_MapList) || g_HasVoteStarted || g_MapVoteCompleted || !GetConVarBool(g_Cvar_EndOfMapVote)) + { + return; + } + + CheckMaxRounds(g_TotalRounds); + + switch(GetEventInt(event, "winning_team")) + { + case 3: + { + CheckWinLimit(bluescore); + } + case 2: + { + CheckWinLimit(redscore); + } + //We need to do nothing on winning_team == 0 this indicates stalemate. + default: + { + return; + } + } + } +} +/* 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, TIMER_FLAG_NO_MAPCHANGE); + g_ChangeMapInProgress = true; + } + + new winner; + if (strcmp(name, "round_win") == 0) + { + // Nuclear Dawn + winner = GetEventInt(event, "team"); + } + else + { + 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]++; + + if (!GetArraySize(g_MapList) || g_HasVoteStarted || g_MapVoteCompleted) + { + return; + } + + 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; + } + + if (g_MapVoteCompleted) + { + return; + } + + new fragger = GetClientOfUserId(GetEventInt(event, "attacker")); + + if (!fragger) + { + return; + } + + if (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; +} + +/** + * Starts a new map vote + * + * @param when When the resulting map change should occur. + * @param inputlist Optional list of maps to use for the vote, otherwise an internal list of nominations + random maps will be used. + * @param noSpecials Block special vote options like extend/nochange (upgrade this to bitflags instead?) + */ +InitiateVote(MapChange:when, Handle:inputlist=INVALID_HANDLE) +{ + g_WaitingForVote = true; + + if (IsVoteInProgress()) + { + // Can't start a vote, try again in 5 seconds. + //g_RetryTimer = CreateTimer(5.0, Timer_StartMapVote, _, TIMER_FLAG_NO_MAPCHANGE); + + new Handle:data; + g_RetryTimer = CreateDataTimer(5.0, Timer_StartMapVote, data, TIMER_FLAG_NO_MAPCHANGE); + WritePackCell(data, _:when); + WritePackCell(data, _:inputlist); + ResetPack(data); + return; + } + + /* If the main map vote has completed (and chosen result) and its currently changing (not a delayed change) we block further attempts */ + if (g_MapVoteCompleted && g_ChangeMapInProgress) + { + 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); + + /* Call OnMapVoteStarted() Forward */ + Call_StartForward(g_MapVoteStartedForward); + Call_Finish(); + + /** + * 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[PLATFORM_MAX_PATH]; + + /* 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); + LogAction(-1, -1, "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); + LogAction(-1, -1, "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); + g_ChangeMapInProgress = false; + } + 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); + LogAction(-1, -1, "Voting for next map has finished. Nextmap: %s.", map); + } +} + +public Handler_MapVoteFinished(Handle:menu, + num_votes, + num_clients, + const client_info[][2], + num_items, + const item_info[][2]) +{ + if (GetConVarBool(g_Cvar_RunOff) && num_items > 1) + { + new Float:winningvotes = float(item_info[0][VOTEINFO_ITEM_VOTES]); + new Float:required = num_votes * (GetConVarFloat(g_Cvar_RunOffPercent) / 100.0); + + if (winningvotes <= required) + { + /* Insufficient Winning margin - Lets do a runoff */ + g_VoteMenu = CreateMenu(Handler_MapVoteMenu, MenuAction:MENU_ACTIONS_ALL); + SetMenuTitle(g_VoteMenu, "Runoff Vote Nextmap"); + SetVoteResultCallback(g_VoteMenu, Handler_VoteFinishedGeneric); + + decl String:map[PLATFORM_MAX_PATH]; + decl String:info1[PLATFORM_MAX_PATH]; + decl String:info2[PLATFORM_MAX_PATH]; + + GetMenuItem(menu, item_info[0][VOTEINFO_ITEM_INDEX], map, sizeof(map), _, info1, sizeof(info1)); + AddMenuItem(g_VoteMenu, map, info1); + GetMenuItem(menu, item_info[1][VOTEINFO_ITEM_INDEX], map, sizeof(map), _, info2, sizeof(info2)); + AddMenuItem(g_VoteMenu, map, info2); + + new voteDuration = GetConVarInt(g_Cvar_VoteDuration); + SetMenuExitButton(g_VoteMenu, false); + VoteMenuToAll(g_VoteMenu, voteDuration); + + /* Notify */ + new Float:map1percent = float(item_info[0][VOTEINFO_ITEM_VOTES])/ float(num_votes) * 100; + new Float:map2percent = float(item_info[1][VOTEINFO_ITEM_VOTES])/ float(num_votes) * 100; + + + PrintToChatAll("[SM] %t", "Starting Runoff", GetConVarFloat(g_Cvar_RunOffPercent), info1, map1percent, info2, map2percent); + LogMessage("Voting for next map was indecisive, beginning runoff vote"); + + return; + } + } + + Handler_VoteFinishedGeneric(menu, num_votes, num_clients, client_info, num_items, item_info); +} + +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[PLATFORM_MAX_PATH], 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[PLATFORM_MAX_PATH]; + 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. I guess we do nothing. + } + + g_HasVoteStarted = false; + g_MapVoteCompleted = true; + } + } + + return 0; +} + +public Action:Timer_ChangeMap(Handle:hTimer, Handle:dp) +{ + g_ChangeMapInProgress = false; + + new String:map[PLATFORM_MAX_PATH]; + + 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)); + } + + ForceChangeLevel(map, "Map Vote"); + + return Plugin_Stop; +} + +bool:RemoveStringFromArray(Handle:array, String:str[]) +{ + new index = FindStringInArray(array, str); + if (index != -1) + { + RemoveFromArray(array, index); + return true; + } + + return false; +} + +CreateNextVote() +{ + assert(g_NextMapList) + ClearArray(g_NextMapList); + + decl String:map[PLATFORM_MAX_PATH]; + new Handle:tempMaps = CloneArray(g_MapList); + + GetCurrentMap(map, sizeof(map)); + RemoveStringFromArray(tempMaps, map); + + 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)); + RemoveStringFromArray(tempMaps, map); + } + } + + 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; + + /* Look to replace an existing nomination by this client - Nominations made with owner = 0 aren't replaced */ + if (owner && ((index = FindValueInArray(g_NominateOwners, owner)) != -1)) + { + new String:oldmap[PLATFORM_MAX_PATH]; + 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[PLATFORM_MAX_PATH]; + 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)); +} + +bool:InternalRemoveNominationByMap(String:map[]) +{ + for (new i = 0; i < GetArraySize(g_NominateList); i++) + { + new String:oldmap[PLATFORM_MAX_PATH]; + GetArrayString(g_NominateList, i, oldmap, sizeof(oldmap)); + + if(strcmp(map, oldmap, false) == 0) + { + Call_StartForward(g_NominationsResetForward); + Call_PushString(oldmap); + Call_PushCell(GetArrayCell(g_NominateOwners, i)); + Call_Finish(); + + RemoveFromArray(g_NominateList, i); + RemoveFromArray(g_NominateOwners, i); + g_NominateCount--; + + return true; + } + } + + return false; +} + +/* native bool:RemoveNominationByMap(const String:map[]); */ +public Native_RemoveNominationByMap(Handle:plugin, numParams) +{ + new len; + GetNativeStringLength(1, len); + + if (len <= 0) + { + return false; + } + + new String:map[len+1]; + GetNativeString(1, map, len+1); + + return _:InternalRemoveNominationByMap(map); +} + +bool:InternalRemoveNominationByOwner(owner) +{ + new index; + + if (owner && ((index = FindValueInArray(g_NominateOwners, owner)) != -1)) + { + new String:oldmap[PLATFORM_MAX_PATH]; + GetArrayString(g_NominateList, index, oldmap, sizeof(oldmap)); + + Call_StartForward(g_NominationsResetForward); + Call_PushString(oldmap); + Call_PushCell(owner); + Call_Finish(); + + RemoveFromArray(g_NominateList, index); + RemoveFromArray(g_NominateOwners, index); + g_NominateCount--; + + return true; + } + + return false; +} + +/* native bool:RemoveNominationByOwner(owner); */ +public Native_RemoveNominationByOwner(Handle:plugin, numParams) +{ + return _:InternalRemoveNominationByOwner(GetNativeCell(1)); +} + +/* native InitiateMapChooserVote(); */ +public Native_InitiateVote(Handle:plugin, numParams) +{ + new MapChange:when = MapChange:GetNativeCell(1); + new Handle:inputarray = Handle:GetNativeCell(2); + + LogAction(-1, -1, "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_EndOfMapVoteEnabled(Handle:plugin, numParams) +{ + return GetConVarBool(g_Cvar_EndOfMapVote); +} + +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[PLATFORM_MAX_PATH]; + + for (new i=0; i