diff --git a/mapchooser_extended/scripting/mapchooser_extended_avg.sp b/mapchooser_extended/scripting/mapchooser_extended_avg.sp new file mode 100755 index 00000000..94c0f0e1 --- /dev/null +++ b/mapchooser_extended/scripting/mapchooser_extended_avg.sp @@ -0,0 +1,3034 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * MapChooser Extended + * Creates a map vote at appropriate times, setting sm_nextmap to the winning + * vote. Includes extra options not present in the SourceMod MapChooser + * + * MapChooser Extended (C)2011-2013 Powerlord (Ross Bemrose) + * 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 + +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma semicolon 1 +#pragma newdecls required + +#define MCE_VERSION "1.3.1" + +enum RoundCounting +{ + RoundCounting_Standard = 0, + RoundCounting_MvM, + RoundCounting_ArmsRace, +} + +// CSGO requires two cvars to get the game type +enum +{ + GameType_Classic = 0, + GameType_GunGame = 1, + GameType_Training = 2, + GameType_Custom = 3, +} + +enum +{ + GunGameMode_ArmsRace = 0, + GunGameMode_Demolition = 1, + GunGameMode_DeathMatch = 2, +} + +public Plugin myinfo = +{ + name = "MapChooser Extended", + author = "Powerlord, Zuko, BotoX and AlliedModders LLC", + description = "Automated Map Voting with Extensions", + version = MCE_VERSION, + url = "" +}; + +/* Valve ConVars */ +ConVar g_Cvar_Winlimit; +ConVar g_Cvar_Maxrounds; +ConVar g_Cvar_Fraglimit; +ConVar g_Cvar_Bonusroundtime; +ConVar g_Cvar_GameType; +ConVar g_Cvar_GameMode; + +/* Plugin ConVars */ +ConVar g_Cvar_StartTime; +ConVar g_Cvar_StartRounds; +ConVar g_Cvar_StartFrags; +ConVar g_Cvar_ExtendTimeStep; +ConVar g_Cvar_ExtendRoundStep; +ConVar g_Cvar_ExtendFragStep; +ConVar g_Cvar_ExcludeMaps; +ConVar g_Cvar_ExcludeMapsTime; +ConVar g_Cvar_IncludeMaps; +ConVar g_Cvar_IncludeMapsReserved; +ConVar g_Cvar_NoVoteMode; +ConVar g_Cvar_Extend; +ConVar g_Cvar_DontChange; +ConVar g_Cvar_EndOfMapVote; +ConVar g_Cvar_VoteDuration; + +Handle g_VoteTimer = INVALID_HANDLE; +Handle g_RetryTimer = INVALID_HANDLE; +Handle g_WarningTimer = INVALID_HANDLE; + +/* Data Handles */ +Handle g_MapList = INVALID_HANDLE; +Handle g_NominateList[MAXPLAYERS + 1]; +Handle g_NominateOwners = INVALID_HANDLE; +StringMap g_OldMapList; +StringMap g_TimeMapList; +Handle g_NextMapList = INVALID_HANDLE; +Handle g_VoteMenu = INVALID_HANDLE; +KeyValues g_Config; + +int g_Extends; +int g_TotalRounds; +bool g_HasVoteStarted; +bool g_WaitingForVote; +bool g_MapVoteCompleted; +bool g_ChangeMapAtRoundEnd; +bool g_ChangeMapInProgress; +bool g_HasIntermissionStarted = false; +int g_mapFileSerial = -1; + +bool prevent_rare_map_vote_bug_2023 = false; //this is not very cool -jenz + +int g_NominateCount = 0; +int g_NominateReservedCount = 0; +MapChange g_ChangeTime; + +//check if autismbot +bool is_bot_player[MAXPLAYERS + 1]; + +Handle g_NominationsResetForward = INVALID_HANDLE; +Handle g_MapVoteStartedForward = INVALID_HANDLE; + +/* Mapchooser Extended Plugin ConVars */ + +ConVar g_Cvar_RunOff; +ConVar g_Cvar_RunOffPercent; +ConVar g_Cvar_BlockSlots; +ConVar g_Cvar_MaxRunOffs; +ConVar g_Cvar_StartTimePercent; +ConVar g_Cvar_StartTimePercentEnable; +ConVar g_Cvar_WarningTime; +ConVar g_Cvar_RunOffWarningTime; +ConVar g_Cvar_TimerLocation; +ConVar g_Cvar_ExtendPosition; +ConVar g_Cvar_MarkCustomMaps; +ConVar g_Cvar_RandomizeNominations; +ConVar g_Cvar_HideTimer; +ConVar g_Cvar_NoVoteOption; +ConVar g_Cvar_ShufflePerClient; +ConVar g_Cvar_NoRestrictionTimeframeEnable; +ConVar g_Cvar_NoRestrictionTimeframeMinTime; +ConVar g_Cvar_NoRestrictionTimeframeMaxTime; + +/* Mapchooser Extended Data Handles */ +Handle g_OfficialList = INVALID_HANDLE; + +/* Mapchooser Extended Forwards */ +Handle g_MapVoteWarningStartForward = INVALID_HANDLE; +Handle g_MapVoteWarningTickForward = INVALID_HANDLE; +Handle g_MapVoteStartForward = INVALID_HANDLE; +Handle g_MapVoteEndForward = INVALID_HANDLE; +Handle g_MapVoteRunoffStartForward = INVALID_HANDLE; + +/* Mapchooser Extended Globals */ +int g_RunoffCount = 0; +int g_mapOfficialFileSerial = -1; +char g_GameModName[64]; +bool g_WarningInProgress = false; +bool g_AddNoVote = false; +bool g_SaveCDOnMapEnd = true; + +RoundCounting g_RoundCounting = RoundCounting_Standard; + +/* Upper bound of how many team there could be */ +#define MAXTEAMS 10 +int g_winCount[MAXTEAMS]; + +bool g_BlockedSlots = false; +int g_ObjectiveEnt = -1; + +enum TimerLocation +{ + TimerLocation_Hint = 0, + TimerLocation_Center = 1, + TimerLocation_Chat = 2, +} + +enum WarningType +{ + WarningType_Vote, + WarningType_Revote, +} + +#define VOTE_EXTEND "##extend##" +#define VOTE_DONTCHANGE "##dontchange##" + +/* Mapchooser Extended Defines */ +#define LINE_ONE "##lineone##" +#define LINE_TWO "##linetwo##" +#define LINE_SPACER "##linespacer##" +#define FAILURE_TIMER_LENGTH 5 + +//call forward to reset all nominations +public void OnPluginEnd() +{ + for (int i = 0; i < MaxClients; i++) + { + int index = FindValueInArray(g_NominateOwners, i); + if (index == -1) continue; + for (int j = 0; j < GetArraySize(g_NominateList[i]); j++) + { + char oldmap[PLATFORM_MAX_PATH]; + Call_StartForward(g_NominationsResetForward); + GetArrayString(g_NominateList[i], j, oldmap, PLATFORM_MAX_PATH); + Call_PushString(oldmap); + Call_Finish(); + } + } +} + +public void OnPluginStart() +{ + LoadTranslations("mapchooser_extended.phrases"); + LoadTranslations("basevotes.phrases"); + LoadTranslations("common.phrases"); + + int arraySize = ByteCountToCells(PLATFORM_MAX_PATH); + g_MapList = CreateArray(arraySize); + g_NominateOwners = CreateArray(1); + g_OldMapList = new StringMap(); + g_TimeMapList = new StringMap(); + g_NextMapList = CreateArray(arraySize); + g_OfficialList = CreateArray(arraySize); + + for (int i = 0; i < MaxClients; i++) + { + g_NominateList[i] = CreateArray(arraySize); + } + + GetGameFolderName(g_GameModName, sizeof(g_GameModName)); + + g_Cvar_EndOfMapVote = CreateConVar("mce_endvote", "1", "Specifies if MapChooser should run an end of map vote", _, true, 0.0, true, 1.0); + + g_Cvar_StartTime = CreateConVar("mce_starttime", "10.0", "Specifies when to start the vote based on time remaining.", _, true, 1.0); + g_Cvar_StartRounds = CreateConVar("mce_startround", "2.0", "Specifies when to start the vote based on rounds remaining. Use 0 on DoD:S, CS:S, and TF2 to start vote during bonus round time", _, true, 0.0); + g_Cvar_StartFrags = CreateConVar("mce_startfrags", "5.0", "Specifies when to start the vote base on frags remaining.", _, true, 1.0); + g_Cvar_ExtendTimeStep = CreateConVar("mce_extend_timestep", "15", "Specifies how much many more minutes each extension makes", _, true, 5.0); + g_Cvar_ExtendRoundStep = CreateConVar("mce_extend_roundstep", "5", "Specifies how many more rounds each extension makes", _, true, 1.0); + g_Cvar_ExtendFragStep = CreateConVar("mce_extend_fragstep", "10", "Specifies how many more frags are allowed when map is extended.", _, true, 5.0); + g_Cvar_ExcludeMaps = CreateConVar("mce_exclude", "5", "Specifies how many past maps to exclude from the vote.", _, true, 0.0); + g_Cvar_ExcludeMapsTime = CreateConVar("mce_exclude_time", "5h", "Specifies how long in minutes an old map is excluded from the vote."); + g_Cvar_IncludeMaps = CreateConVar("mce_include", "5", "Specifies how many maps to include in the vote.", _, true, 2.0, true, 7.0); + g_Cvar_IncludeMapsReserved = CreateConVar("mce_include_reserved", "2", "Specifies how many private/random maps to include in the vote.", _, true, 0.0, true, 5.0); + g_Cvar_NoVoteMode = CreateConVar("mce_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("mce_extend", "0", "Number of extensions allowed each map.", _, true, 0.0); + g_Cvar_DontChange = CreateConVar("mce_dontchange", "1", "Specifies if a 'Don't Change option should be added to early votes", _, true, 0.0); + g_Cvar_VoteDuration = CreateConVar("mce_voteduration", "20", "Specifies how long the mapvote should be available for.", _, true, 5.0); + + // MapChooser Extended cvars + CreateConVar("mce_version", MCE_VERSION, "MapChooser Extended Version", FCVAR_SPONLY|FCVAR_NOTIFY|FCVAR_DONTRECORD); + + g_Cvar_RunOff = CreateConVar("mce_runoff", "1", "Hold run off votes if winning choice has less than a certain percentage of votes", _, true, 0.0, true, 1.0); + g_Cvar_RunOffPercent = CreateConVar("mce_runoffpercent", "50", "If winning choice has less than this percent of votes, hold a runoff", _, true, 0.0, true, 100.0); + g_Cvar_BlockSlots = CreateConVar("mce_blockslots", "0", "Block slots to prevent accidental votes. Only applies when Voice Command style menus are in use.", _, true, 0.0, true, 1.0); + //g_Cvar_BlockSlotsCount = CreateConVar("mce_blockslots_count", "2", "Number of slots to block.", _, true, 1.0, true, 3.0); + g_Cvar_MaxRunOffs = CreateConVar("mce_maxrunoffs", "1", "Number of run off votes allowed each map.", _, true, 0.0); + g_Cvar_StartTimePercent = CreateConVar("mce_start_percent", "35.0", "Specifies when to start the vote based on percents.", _, true, 0.0, true, 100.0); + g_Cvar_StartTimePercentEnable = CreateConVar("mce_start_percent_enable", "0", "Enable or Disable percentage calculations when to start vote.", _, true, 0.0, true, 1.0); + g_Cvar_WarningTime = CreateConVar("mce_warningtime", "15.0", "Warning time in seconds.", _, true, 0.0, true, 60.0); + g_Cvar_RunOffWarningTime = CreateConVar("mce_runoffvotewarningtime", "5.0", "Warning time for runoff vote in seconds.", _, true, 0.0, true, 30.0); + g_Cvar_TimerLocation = CreateConVar("mce_warningtimerlocation", "0", "Location for the warning timer text. 0 is HintBox, 1 is Center text, 2 is Chat. Defaults to HintBox.", _, true, 0.0, true, 2.0); + g_Cvar_MarkCustomMaps = CreateConVar("mce_markcustommaps", "1", "Mark custom maps in the vote list. 0 = Disabled, 1 = Mark with *, 2 = Mark with phrase.", _, true, 0.0, true, 2.0); + g_Cvar_ExtendPosition = CreateConVar("mce_extendposition", "0", "Position of Extend/Dont Change options. 0 = at end, 1 = at start.", _, true, 0.0, true, 1.0); + g_Cvar_RandomizeNominations = CreateConVar("mce_randomizeorder", "0", "Randomize map order?", _, true, 0.0, true, 1.0); + g_Cvar_HideTimer = CreateConVar("mce_hidetimer", "0", "Hide the MapChooser Extended warning timer", _, true, 0.0, true, 1.0); + g_Cvar_NoVoteOption = CreateConVar("mce_addnovote", "1", "Add \"No Vote\" to vote menu?", _, true, 0.0, true, 1.0); + g_Cvar_ShufflePerClient = CreateConVar("mce_shuffle_per_client", "1", "Random shuffle map vote menu per client?", _, true, 0.0, true, 1.0); + g_Cvar_NoRestrictionTimeframeEnable = CreateConVar("mce_no_restriction_timeframe_enable", "1", "Enable timeframe where all nomination restrictions and cooldowns are disabled?", _, true, 0.0, true, 1.0); + g_Cvar_NoRestrictionTimeframeMinTime = CreateConVar("mce_no_restriction_timeframe_mintime", "0100", "Start of the timeframe where all nomination restrictions and cooldowns are disabled (Format: HHMM)", _, true, 0000.0, true, 2359.0); + g_Cvar_NoRestrictionTimeframeMaxTime = CreateConVar("mce_no_restriction_timeframe_maxtime", "0700", "End of the timeframe where all nomination restrictions and cooldowns are disabled (Format: HHMM)", _, true, 0000.0, true, 2359.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 "); + + // Mapchooser Extended Commands + RegAdminCmd("mce_reload_maplist", Command_ReloadMaps, ADMFLAG_CHANGEMAP, "mce_reload_maplist - Reload the Official Maplist file."); + + RegConsoleCmd("sm_extends", Command_ExtendsLeft, "sm_extends - Shows how many extends are left on the current map."); + RegConsoleCmd("sm_extendsleft", Command_ExtendsLeft, "sm_extendsleft - Shows how many extends are left on the current map."); + RegConsoleCmd("sm_houravg", Command_hours_average, "Prints in the chat what the current hour average of each player accumulated is."); + RegConsoleCmd("sm_avghour", Command_hours_average, "Prints in the chat what the current hour average of each player accumulated is."); + + g_Cvar_Winlimit = FindConVar("mp_winlimit"); + g_Cvar_Maxrounds = FindConVar("mp_maxrounds"); + g_Cvar_Fraglimit = FindConVar("mp_fraglimit"); + + EngineVersion version = GetEngineVersion(); + + static char mapListPath[PLATFORM_MAX_PATH]; + + BuildPath(Path_SM, mapListPath, PLATFORM_MAX_PATH, "configs/mapchooser_extended/maps/%s.txt", g_GameModName); + SetMapListCompatBind("official", mapListPath); + + switch(version) + { + case Engine_TF2: + { + g_Cvar_Bonusroundtime = FindConVar("mp_bonusroundtime"); + } + + case Engine_CSGO: + { + g_Cvar_GameType = FindConVar("game_type"); + g_Cvar_GameMode = FindConVar("game_mode"); + g_Cvar_Bonusroundtime = FindConVar("mp_round_restart_delay"); + } + + case Engine_DODS: + { + g_Cvar_Bonusroundtime = FindConVar("dod_bonusroundtime"); + } + + case Engine_CSS: + { + g_Cvar_Bonusroundtime = FindConVar("mp_round_restart_delay"); + } + + default: + { + g_Cvar_Bonusroundtime = FindConVar("mp_bonusroundtime"); + } + } + + switch(version) + { + case Engine_TF2: + { + HookEvent("teamplay_restart_round", Event_TFRestartRound); + HookEvent("pve_win_panel", Event_MvMWinPanel); + } + + case Engine_NuclearDawn: + { + HookEvent("round_win", Event_RoundEnd); + } + + case Engine_CSGO: + { + HookEvent("round_end", Event_RoundEnd); + HookEvent("cs_intermission", Event_Intermission); + HookEvent("announce_phase_end", Event_PhaseEnd); + } + + case Engine_DODS: + { + HookEvent("dod_round_win", Event_RoundEnd); + } + + default: + { + HookEvent("round_end", Event_RoundEnd); + } + } + + HookEvent("player_death", Event_PlayerDeath); + + AutoExecConfig(true, "mapchooser_extended"); + + //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 + 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); + + //MapChooser Extended Forwards + g_MapVoteStartForward = CreateGlobalForward("OnMapVoteStart", ET_Ignore); // Deprecated + g_MapVoteEndForward = CreateGlobalForward("OnMapVoteEnd", ET_Ignore, Param_String); + g_MapVoteWarningStartForward = CreateGlobalForward("OnMapVoteWarningStart", ET_Ignore); + g_MapVoteWarningTickForward = CreateGlobalForward("OnMapVoteWarningTick", ET_Ignore, Param_Cell); + g_MapVoteRunoffStartForward = CreateGlobalForward("OnMapVoteRunnoffWarningStart", ET_Ignore); + + prevent_rare_map_vote_bug_2023 = false; + + InternalRestoreMapCooldowns(); +} + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + if(LibraryExists("mapchooser")) + { + strcopy(error, err_max, "MapChooser already loaded, aborting."); + return APLRes_Failure; + } + + RegPluginLibrary("mapchooser"); + + MarkNativeAsOptional("GetEngineVersion"); + + 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); + + // MapChooser Extended natives + CreateNative("IsMapOfficial", Native_IsMapOfficial); + CreateNative("CanNominate", Native_CanNominate); + CreateNative("ExcludeMap", Native_ExcludeMap); + CreateNative("ExcludeMapTime", Native_ExcludeMapTime); + CreateNative("GetMapCooldown", Native_GetMapCooldown); + CreateNative("GetMapCooldownTime", Native_GetMapCooldownTime); + CreateNative("GetMapMinTime", Native_GetMapMinTime); + CreateNative("GetMapMaxTime", Native_GetMapMaxTime); + CreateNative("GetMapMinPlayers", Native_GetMapMinPlayers); + CreateNative("GetMapMaxPlayers", Native_GetMapMaxPlayers); + CreateNative("GetMapTimeRestriction", Native_GetMapTimeRestriction); + CreateNative("GetMapPlayerRestriction", Native_GetMapPlayerRestriction); + CreateNative("GetMapGroups", Native_GetMapGroups); + CreateNative("GetMapGroupRestriction", Native_GetMapGroupRestriction); + CreateNative("GetMapVIPRestriction", Native_GetMapVIPRestriction); + CreateNative("GetExtendsLeft", Native_GetExtendsLeft); + CreateNative("AreRestrictionsActive", Native_AreRestrictionsActive); + CreateNative("SimulateMapEnd", Native_SimulateMapEnd); + CreateNative("GetAveragePlayerTimeOnServerMapRestriction", Native_GetAveragePlayerTimeOnServerMapRestriction); + + return APLRes_Success; +} + +public void OnMapStart() +{ + static char folder[64]; + GetGameFolderName(folder, sizeof(folder)); + + g_RoundCounting = RoundCounting_Standard; + g_ObjectiveEnt = -1; + + if(strcmp(folder, "tf") == 0 && GameRules_GetProp("m_bPlayingMannVsMachine")) + { + g_RoundCounting = RoundCounting_MvM; + g_ObjectiveEnt = EntIndexToEntRef(FindEntityByClassname(-1, "tf_objective_resource")); + } + else if(strcmp(folder, "csgo") == 0 && GetConVarInt(g_Cvar_GameType) == GameType_GunGame && + GetConVarInt(g_Cvar_GameMode) == GunGameMode_ArmsRace) + { + g_RoundCounting = RoundCounting_ArmsRace; + } + + if(g_Config) + delete g_Config; + + char sConfigFile[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, sConfigFile, sizeof(sConfigFile), "configs/mapchooser_extended.cfg"); + if(!FileExists(sConfigFile)) + { + LogMessage("Could not find config: \"%s\"", sConfigFile); + return; + } + LogMessage("Found config: \"%s\"", sConfigFile); + + g_Config = new KeyValues("mapchooser_extended"); + if(!g_Config.ImportFromFile(sConfigFile)) + { + delete g_Config; + LogMessage("ImportFromFile() failed!"); + return; + } + g_Config.Rewind(); + + if(InternalAreRestrictionsActive()) + g_SaveCDOnMapEnd = true; + else + g_SaveCDOnMapEnd = false; +} + +public void 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."); + } + + + SetupTimeleftTimer(); + + g_TotalRounds = 0; + + g_Extends = 0; + + g_MapVoteCompleted = false; + + g_NominateCount = 0; + g_NominateReservedCount = 0; + for (int i = 0; i < MaxClients; i++) + { + if (g_NominateList[i] != INVALID_HANDLE) + { + ClearArray(g_NominateList[i]); + } + } + + ClearArray(g_NominateOwners); + + for(int i = 0; i < MAXTEAMS; i++) + g_winCount[i] = 0; + + /* Check if mapchooser will attempt to start mapvote during bonus round time */ + if (!GetConVarInt(g_Cvar_StartRounds)) + { + if(!GetConVarInt(g_Cvar_StartTime) && GetConVarFloat(g_Cvar_Bonusroundtime) <= GetConVarFloat(g_Cvar_VoteDuration)) + LogError("Warning - Bonus Round Time shorter than Vote Time. Votes during bonus round may not have time to complete"); + } + + InitializeOfficialMapList(); +} + +public void OnMapEnd() +{ + g_HasVoteStarted = false; + g_WaitingForVote = false; + g_ChangeMapAtRoundEnd = false; + g_ChangeMapInProgress = false; + g_HasIntermissionStarted = false; + + g_VoteTimer = INVALID_HANDLE; + g_RetryTimer = INVALID_HANDLE; + g_WarningTimer = INVALID_HANDLE; + g_RunoffCount = 0; + + prevent_rare_map_vote_bug_2023 = false; + + static char map[PLATFORM_MAX_PATH]; + int Cooldown; + + if(g_SaveCDOnMapEnd) + { + GetCurrentMap(map, PLATFORM_MAX_PATH); + Cooldown = InternalGetMapCooldown(map); + g_OldMapList.SetValue(map, Cooldown, true); + + Cooldown = GetTime() + InternalGetMapCooldownTime(map) - RoundToFloor(GetGameTime()); + g_TimeMapList.SetValue(map, Cooldown, true); + } + + StringMapSnapshot OldMapListSnapshot = g_OldMapList.Snapshot(); + for(int i = 0; i < OldMapListSnapshot.Length; i++) + { + OldMapListSnapshot.GetKey(i, map, sizeof(map)); + g_OldMapList.GetValue(map, Cooldown); + + Cooldown--; + if(Cooldown > 0) + g_OldMapList.SetValue(map, Cooldown, true); + else + g_OldMapList.Remove(map); + } + delete OldMapListSnapshot; + + StringMapSnapshot TimeMapListSnapshot = g_TimeMapList.Snapshot(); + for(int i = 0; i < TimeMapListSnapshot.Length; i++) + { + TimeMapListSnapshot.GetKey(i, map, sizeof(map)); + g_TimeMapList.GetValue(map, Cooldown); + + if(Cooldown < GetTime()) + g_TimeMapList.Remove(map); + } + delete OldMapListSnapshot; + + InternalStoreMapCooldowns(); +} + +public void OnClientPutInServer(int client) +{ + if (g_NominateList[client] != INVALID_HANDLE) + { + ClearArray(g_NominateList[client]); + } + CheckMapRestrictions(true, true); //when creating a mapvote its anyways respecting the time restrictions so we should also just respect them here already +} + +public void OnClientDisconnect_Post(int client) +{ + CheckMapRestrictions(true, true); //when creating a mapvote its anyways respecting the time restrictions so we should also just respect them here already +} + +public void OnClientDisconnect(int client) +{ + is_bot_player[client] = false; + int index = FindValueInArray(g_NominateOwners, client); + + if(index == -1) + return; + + //2023 edit for handling multiple nominations -jenz + for (int i = 0; i < GetArraySize(g_NominateList[client]); i++) + { + Call_StartForward(g_NominationsResetForward); + char oldmap[PLATFORM_MAX_PATH]; + GetArrayString(g_NominateList[client], i, oldmap, PLATFORM_MAX_PATH); + Call_PushString(oldmap); + Call_Finish(); + } + + RemoveFromArray(g_NominateOwners, index); + for (int i = 0; i < GetArraySize(g_NominateList[client]); i++) + { + RemoveFromArray(g_NominateList[client], i); + } + + ClearArray(g_NominateList[client]); + g_NominateCount--; +} + +public Action Command_SetNextmap(int client, int args) +{ + if(args < 1) + { + CReplyToCommand(client, "[MCE] Usage: sm_setnextmap "); + return Plugin_Handled; + } + + static char map[PLATFORM_MAX_PATH]; + GetCmdArg(1, map, PLATFORM_MAX_PATH); + + if(!IsMapValid(map)) + { + CReplyToCommand(client, "[MCE] %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 Action Command_ReloadMaps(int client, int args) +{ + InitializeOfficialMapList(); + return Plugin_Handled; +} + +public Action Command_hours_average(int client, int args) +{ + CReplyToCommand(client, "Average hour count is: %i", GetAveragePlayerTimeOnServer()); + return Plugin_Handled; +} + +public Action Command_ExtendsLeft(int client, int args) +{ + CReplyToCommand(client, "[MCE] Available Extends: %d", GetConVarInt(g_Cvar_Extend) - g_Extends); + return Plugin_Handled; +} + +public void OnMapTimeLeftChanged() +{ + if(GetArraySize(g_MapList)) + SetupTimeleftTimer(); +} + +void SetupTimeleftTimer() +{ + int time; + if(GetMapTimeLeft(time) && time > 0) + { + int startTime; + if(GetConVarBool(g_Cvar_StartTimePercentEnable)) + { + int timeLimit; + if(GetMapTimeLimit(timeLimit) && timeLimit > 0) + { + startTime = GetConVarInt(g_Cvar_StartTimePercent) * (timeLimit * 60) / 100; + } + } + else + startTime = GetConVarInt(g_Cvar_StartTime) * 60; + + if(time - startTime < 0 && GetConVarBool(g_Cvar_EndOfMapVote) && !g_MapVoteCompleted && !g_HasVoteStarted) + { + SetupWarningTimer(WarningType_Vote); + } + else + { + if(g_WarningTimer == INVALID_HANDLE) + { + if(g_VoteTimer != INVALID_HANDLE) + { + KillTimer(g_VoteTimer); + g_VoteTimer = INVALID_HANDLE; + } + + //g_VoteTimer = CreateTimer(float(time - startTime), Timer_StartMapVoteTimer_StartMapVote, _, TIMER_FLAG_NO_MAPCHANGE); + g_VoteTimer = CreateTimer(float(time - startTime), Timer_StartWarningTimer, _, TIMER_FLAG_NO_MAPCHANGE); + } + } + } +} + +public Action Timer_StartWarningTimer(Handle timer) +{ + g_VoteTimer = INVALID_HANDLE; + + if(!g_WarningInProgress || g_WarningTimer == INVALID_HANDLE) + SetupWarningTimer(WarningType_Vote); + return Plugin_Stop; +} + +public Action Timer_StartMapVote(Handle timer, Handle data) +{ + static int timePassed; + + // This is still necessary because InitiateVote still calls this directly via the retry timer + if(!GetArraySize(g_MapList) || !GetConVarBool(g_Cvar_EndOfMapVote) || g_MapVoteCompleted || g_HasVoteStarted) + { + g_WarningTimer = INVALID_HANDLE; + return Plugin_Stop; + } + + ResetPack(data); + int warningMaxTime = ReadPackCell(data); + int warningTimeRemaining = warningMaxTime - timePassed; + + char warningPhrase[32]; + ReadPackString(data, warningPhrase, sizeof(warningPhrase)); + + // Tick timer for external plugins + Call_StartForward(g_MapVoteWarningTickForward); + Call_PushCell(warningTimeRemaining); + Call_Finish(); + + if(timePassed == 0 || !GetConVarBool(g_Cvar_HideTimer)) + { + TimerLocation timerLocation = view_as(GetConVarInt(g_Cvar_TimerLocation)); + + switch(timerLocation) + { + case TimerLocation_Center: + { + PrintCenterTextAll("%t", warningPhrase, warningTimeRemaining); + } + + case TimerLocation_Chat: + { + PrintToChatAll("%t", warningPhrase, warningTimeRemaining); + } + + default: + { + PrintHintTextToAll("%t", warningPhrase, warningTimeRemaining); + } + } + } + + if(timePassed++ >= warningMaxTime) + { + if(timer == g_RetryTimer) + { + g_WaitingForVote = false; + g_RetryTimer = INVALID_HANDLE; + } + else + g_WarningTimer = INVALID_HANDLE; + + timePassed = 0; + MapChange mapChange = view_as(ReadPackCell(data)); + Handle hndl = view_as(ReadPackCell(data)); + + if (!prevent_rare_map_vote_bug_2023) + { + InitiateVote(mapChange, hndl); + } + + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public void Event_TFRestartRound(Handle event, const char[] name, bool dontBroadcast) +{ + /* Game got restarted - reset our round count tracking */ + g_TotalRounds = 0; +} + +public void Event_MvMWinPanel(Handle event, const char[] name, bool dontBroadcast) +{ + if(GetEventInt(event, "winning_team") == 2) + { + int objectiveEnt = EntRefToEntIndex(g_ObjectiveEnt); + if(objectiveEnt != INVALID_ENT_REFERENCE) + { + g_TotalRounds = GetEntProp(g_ObjectiveEnt, Prop_Send, "m_nMannVsMachineWaveCount"); + CheckMaxRounds(g_TotalRounds); + } + } +} + +public void Event_Intermission(Handle event, const char[] name, bool dontBroadcast) +{ + g_HasIntermissionStarted = true; +} + +public void Event_PhaseEnd(Handle event, const char[] name, bool dontBroadcast) +{ + /* announce_phase_end fires for both half time and the end of the map, but intermission fires first for end of the map. */ + if(g_HasIntermissionStarted) + return; + + /* No intermission yet, so this must be half time. Swap the score counters. */ + int t_score = g_winCount[2]; + g_winCount[2] = g_winCount[3]; + g_winCount[3] = t_score; +} + +public void Event_WeaponRank(Handle event, const char[] name, bool dontBroadcast) +{ + int rank = GetEventInt(event, "weaponrank"); + if(rank > g_TotalRounds) + { + g_TotalRounds = rank; + CheckMaxRounds(g_TotalRounds); + } +} + +/* You ask, why don't you just use team_score event? And I answer... Because CSS doesn't. */ +public void Event_RoundEnd(Handle event, const char[] name, bool dontBroadcast) +{ + int timeleft; + GetMapTimeLeft(timeleft); + + if(timeleft <= 0 || g_ChangeMapAtRoundEnd) + { + char map[32]; + GetNextMap(map, sizeof(map)); + PrintToChatAll("[MCE] Next Map: %s", map); + PrintToChatAll("[MCE] Next Map: %s", map); + PrintToChatAll("[MCE] Next Map: %s", map); + } + + if(g_RoundCounting == RoundCounting_ArmsRace) + return; + + if(g_ChangeMapAtRoundEnd) + { + g_ChangeMapAtRoundEnd = false; + CreateTimer(2.0, Timer_ChangeMap, INVALID_HANDLE, TIMER_FLAG_NO_MAPCHANGE); + g_ChangeMapInProgress = true; + } + + int winner; + if(strcmp(name, "round_win") == 0 || strcmp(name, "dod_round_win") == 0) + winner = GetEventInt(event, "team"); // Nuclear Dawn & DoD:S + 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 void CheckWinLimit(int winner_score) +{ + int winlimit = GetConVarInt(g_Cvar_Winlimit); + if(winlimit) + { + if(winner_score >= (winlimit - GetConVarInt(g_Cvar_StartRounds))) + { + if(!g_WarningInProgress || g_WarningTimer == INVALID_HANDLE) + { + SetupWarningTimer(WarningType_Vote, MapChange_MapEnd); + } + } + } +} + +public void CheckMaxRounds(int roundcount) +{ + int maxrounds = 0; + + if(g_RoundCounting == RoundCounting_ArmsRace) + maxrounds = GameRules_GetProp("m_iNumGunGameProgressiveWeaponsCT"); + else if(g_RoundCounting == RoundCounting_MvM) + maxrounds = GetEntProp(g_ObjectiveEnt, Prop_Send, "m_nMannVsMachineMaxWaveCount"); + else + return; + + if(maxrounds) + { + if(roundcount >= (maxrounds - GetConVarInt(g_Cvar_StartRounds))) + { + if(!g_WarningInProgress || g_WarningTimer == INVALID_HANDLE) + { + SetupWarningTimer(WarningType_Vote, MapChange_MapEnd); + } + } + } +} + +public Action Event_PlayerDeath(Handle event, const char[] name, bool dontBroadcast) +{ + if(!GetArraySize(g_MapList) || g_HasVoteStarted) + return Plugin_Continue; + + if(!GetConVarInt(g_Cvar_Fraglimit) || !GetConVarBool(g_Cvar_EndOfMapVote)) + return Plugin_Continue; + + if(g_MapVoteCompleted) + return Plugin_Continue; + + int fragger = GetClientOfUserId(GetEventInt(event, "attacker")); + + if(!fragger) + return Plugin_Continue; + + if(GetClientFrags(fragger) >= (GetConVarInt(g_Cvar_Fraglimit) - GetConVarInt(g_Cvar_StartFrags))) + { + if(!g_WarningInProgress || g_WarningTimer == INVALID_HANDLE) + { + SetupWarningTimer(WarningType_Vote, MapChange_MapEnd); + } + } + return Plugin_Continue; +} + +public Action Command_Mapvote(int client, int args) +{ + ShowActivity2(client, "[MCE] ", "%t", "Initiated Vote Map"); + + SetupWarningTimer(WarningType_Vote, MapChange_MapEnd, INVALID_HANDLE, true); + + return Plugin_Handled; +} + +public Handle get_most_nominated_maps() +{ + int voteSize = GetVoteSize(2); + int arraySize = ByteCountToCells(PLATFORM_MAX_PATH); + Handle most_nominated_maps = CreateArray(arraySize); + StringMap sm = new StringMap(); + + for (int i = 0; i < MaxClients; i++) + { + for (int j = 0; j < GetArraySize(g_NominateList[i]); j++) + { + char map_iteration[PLATFORM_MAX_PATH]; + GetArrayString(g_NominateList[i], j, map_iteration, PLATFORM_MAX_PATH); + //PrintToChatAll("map_iteration: %s", map_iteration); + int nominate_count_for_particular_map = 0; + sm.GetValue(map_iteration, nominate_count_for_particular_map); + nominate_count_for_particular_map++; + //if i is 0 its admin nominated map that most come into the vote + if(!i) + { + nominate_count_for_particular_map = 999; + } + sm.SetValue(map_iteration, nominate_count_for_particular_map, true); + } + } + static char map_[PLATFORM_MAX_PATH]; + + for (int i = 0; i < voteSize; i++) + { + int max_count = 0; + char picked_map[PLATFORM_MAX_PATH]; + StringMapSnapshot keys = sm.Snapshot(); + for (int j = 0; j < keys.Length; j++) + { + int size = keys.KeyBufferSize(j); + char[] buffer = new char[size]; + keys.GetKey(j, buffer, size); + //PrintToChatAll("buffer: %s", buffer); + if (StrEqual(buffer, "nominated_maps")) + { + continue; + } + int value = 0; + sm.GetValue(buffer, value); + //PrintToChatAll("value: %i", value); + + //first selection has most nominates, second selection second most etc etc + if (value >= max_count) + { + max_count = value; + strcopy(picked_map, sizeof(picked_map), buffer); + } + } + + delete keys; + if (strlen(picked_map) == 0) + { + continue; + } + sm.Remove(picked_map); + + //2023 edit: respecting that only right amount of maps per group is allowed in vote + int groups_[32]; + int groups[32]; + int groupsfound = InternalGetMapGroups(picked_map, groups, sizeof(groups)); + bool skip_nomination = false; + for (int group = 0; group < groupsfound; group ++) + { + int groupcur = 0; + int groupmax = InternalGetGroupMax(groups[group]); + if (groupmax >= 0) + { + for (int j = 0; j < GetArraySize(most_nominated_maps); j++) + { + //Native_GetMapGroupRestriction + GetArrayString(most_nominated_maps, j, map_, PLATFORM_MAX_PATH); + int tmp = InternalGetMapGroups(map_, groups_, sizeof(groups_)); + if (FindIntInArray(groups_, tmp, groups[group]) != -1) + { + groupcur++; + } + if (groupcur >= groupmax) + { + skip_nomination = true; + break; + } + } + if (skip_nomination) + { + break; + } + } + } + if (skip_nomination) + { + continue; + } + PushArrayString(most_nominated_maps, picked_map); + //PrintToChatAll("picked_map: %s", picked_map); + } + + delete sm; + return most_nominated_maps; +} + +/** + * 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. + */ +void InitiateVote(MapChange when, Handle inputlist=INVALID_HANDLE) +{ + g_WaitingForVote = true; + g_WarningInProgress = false; + int MenuRandomShuffleStart = 0; + int MenuRandomShuffleStop = 0; + + // Check if a vote is in progress first + if(IsVoteInProgress()) + { + CPrintToChatAll("[MCE] %t", "Cannot Start Vote", FAILURE_TIMER_LENGTH); + Handle data; + g_RetryTimer = CreateDataTimer(1.0, Timer_StartMapVote, data, TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); + + /* Mapchooser Extended */ + WritePackCell(data, FAILURE_TIMER_LENGTH); + + if(GetConVarBool(g_Cvar_RunOff) && g_RunoffCount > 0) + WritePackString(data, "Revote Warning"); + else + WritePackString(data, "Vote Warning"); + /* End Mapchooser Extended */ + + WritePackCell(data, view_as(when)); + WritePackCell(data, view_as(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; + + prevent_rare_map_vote_bug_2023 = true; + + CheckMapRestrictions(true, true); + CreateNextVote(); + + g_ChangeTime = when; + + g_WaitingForVote = false; + + g_HasVoteStarted = true; + + g_VoteMenu = CreateMenu(Handler_MapVoteMenu, MenuAction_End | MenuAction_Display | MenuAction_DisplayItem | MenuAction_VoteCancel); + + g_AddNoVote = GetConVarBool(g_Cvar_NoVoteOption); + + // Block Vote Slots + if(GetConVarBool(g_Cvar_BlockSlots)) + { + Handle radioStyle = GetMenuStyleHandle(MenuStyle_Radio); + + if(GetMenuStyle(g_VoteMenu) == radioStyle) + { + g_BlockedSlots = true; + AddMenuItem(g_VoteMenu, LINE_ONE, "Choose something...", ITEMDRAW_DISABLED); + AddMenuItem(g_VoteMenu, LINE_TWO, "...will ya?", ITEMDRAW_DISABLED); + MenuRandomShuffleStart += 2; + + if(!g_AddNoVote) { + AddMenuItem(g_VoteMenu, LINE_SPACER, "", ITEMDRAW_SPACER); + MenuRandomShuffleStart++; + } + } + else + g_BlockedSlots = false; + } + + if(g_AddNoVote) + SetMenuOptionFlags(g_VoteMenu, MENUFLAG_BUTTON_NOVOTE); + + 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. + */ + + static char map[PLATFORM_MAX_PATH]; + + /* No input given - User our internal nominations and maplist */ + if(inputlist == INVALID_HANDLE) + { + Handle randomizeList = INVALID_HANDLE; + //2023 edit to allow multiple nominations per player + Handle most_nominated_maps = get_most_nominated_maps(); + + int voteSize = GetVoteSize(2); //voteSize wrong size probably for my for loop + + if(GetConVarBool(g_Cvar_RandomizeNominations)) + randomizeList = CloneArray(most_nominated_maps); + + int nominateCount = GetArraySize(most_nominated_maps); + + + /* Smaller of the two - It should be impossible for nominations to exceed the size though (cvar changed mid-map?) */ + int nominationsToAdd = nominateCount >= voteSize ? voteSize : nominateCount; + + bool extendFirst = GetConVarBool(g_Cvar_ExtendPosition); + + if(extendFirst) { + AddExtendToMenu(g_VoteMenu, when); + MenuRandomShuffleStart++; + } + + for(int i = 0; i < nominationsToAdd; i++) + { + GetArrayString(most_nominated_maps, i, map, PLATFORM_MAX_PATH); + + if(randomizeList == INVALID_HANDLE) + AddMapItem(map); + + RemoveStringFromArray(g_NextMapList, map); + + /* Notify Nominations that this map is now free */ + Call_StartForward(g_NominationsResetForward); + Call_PushString(map); + Call_Finish(); + } + + /* Clear out the rest of the nominations array */ + for(int i = nominationsToAdd; i < nominateCount; i++) + { + //2023 edit: might need to run all g_NominateList[client] through this instead + GetArrayString(most_nominated_maps, i, map, PLATFORM_MAX_PATH); + /* These maps shouldn't be excluded from the vote as they weren't really nominated at all */ + + /* Notify Nominations that this map is now free */ + Call_StartForward(g_NominationsResetForward); + Call_PushString(map); + Call_Finish(); + } + + /* There should currently be 'nominationsToAdd' unique maps in the vote */ + + int i = nominationsToAdd; + int count = 0; + int availableMaps = GetArraySize(g_NextMapList); + + if(i < voteSize && availableMaps == 0) + { + if(i == 0) + { + LogError("No maps available for vote."); + return; + } + else + { + LogMessage("Not enough maps to fill map list, reducing map count. Adjust mce_include and mce_exclude to avoid this warning."); + voteSize = i; + } + } + + while(i < voteSize) + { + GetArrayString(g_NextMapList, count, map, PLATFORM_MAX_PATH); + count++; + + if(randomizeList == INVALID_HANDLE) + { + /* Insert the map and increment our count */ + AddMapItem(map); + } + else + PushArrayString(randomizeList, map); + i++; + + //Run out of maps, this will have to do. + if(count >= availableMaps) + break; + } + + if(randomizeList != INVALID_HANDLE) + { + // Fisher-Yates Shuffle + for(int j = GetArraySize(randomizeList) - 1; j >= 1; j--) + { + int k = GetRandomInt(0, j); + SwapArrayItems(randomizeList, j, k); + } + + for(int j = 0; j < GetArraySize(randomizeList); j++) + { + GetArrayString(randomizeList, j, map, PLATFORM_MAX_PATH); + AddMapItem(map); + } + + delete randomizeList; + randomizeList = INVALID_HANDLE; + delete most_nominated_maps; + most_nominated_maps = INVALID_HANDLE; + } + + /* Wipe out our nominations list - Nominations have already been informed of this */ + g_NominateCount = 0; + g_NominateReservedCount = 0; + ClearArray(g_NominateOwners); + for (int j = 0; j < MaxClients; j++) + { + ClearArray(g_NominateList[j]); + } + + if(!extendFirst) { + AddExtendToMenu(g_VoteMenu, when); + MenuRandomShuffleStop++; + } + } + else //We were given a list of maps to start the vote with + { + int size = GetArraySize(inputlist); + + for(int i = 0; i < size; i++) + { + GetArrayString(inputlist, i, map, PLATFORM_MAX_PATH); + + if(IsMapValid(map)) + { + AddMapItem(map); + } + // New in Mapchooser Extended + else if(StrEqual(map, VOTE_DONTCHANGE)) + { + AddMenuItem(g_VoteMenu, VOTE_DONTCHANGE, "Dont Change"); + } + else if(StrEqual(map, VOTE_EXTEND)) + { + AddMenuItem(g_VoteMenu, VOTE_EXTEND, "Extend Map"); + } + } + delete inputlist; + } + + int voteDuration = GetConVarInt(g_Cvar_VoteDuration); + + //SetMenuExitButton(g_VoteMenu, false); + + if(GetVoteSize(2) <= GetMaxPageItems(GetMenuStyle(g_VoteMenu))) + { + //This is necessary to get items 9 and 0 as usable voting items + SetMenuPagination(g_VoteMenu, MENU_NO_PAGINATION); + } + + if(GetConVarInt(g_Cvar_ShufflePerClient)) + MenuShufflePerClient(g_VoteMenu, MenuRandomShuffleStart, GetMenuItemCount(g_VoteMenu) - MenuRandomShuffleStop); + + VoteMenuToAll(g_VoteMenu, voteDuration); + + /* Call OnMapVoteStarted() Forward */ + Call_StartForward(g_MapVoteStartForward); // Deprecated + Call_Finish(); + + Call_StartForward(g_MapVoteStartedForward); + Call_Finish(); + + LogAction(-1, -1, "Voting for next map has started."); + CPrintToChatAll("[MCE] %t", "Nextmap Voting Started"); +} + +public void Handler_VoteFinishedGeneric(Handle menu, + int num_votes, + int num_clients, + const int[][] client_info, + int num_items, + const int[][] item_info) +{ + static char map[PLATFORM_MAX_PATH]; + GetMapItem(menu, item_info[0][VOTEINFO_ITEM_INDEX], map, PLATFORM_MAX_PATH); + + Call_StartForward(g_MapVoteEndForward); + Call_PushString(map); + Call_Finish(); + + if(strcmp(map, VOTE_EXTEND, false) == 0) + { + g_Extends++; + + int time; + if(GetMapTimeLimit(time)) + { + if(time > 0) + ExtendMapTimeLimit(GetConVarInt(g_Cvar_ExtendTimeStep)*60); + } + + int winlimit = GetConVarInt(g_Cvar_Winlimit); + if(winlimit) + SetConVarInt(g_Cvar_Winlimit, winlimit + GetConVarInt(g_Cvar_ExtendRoundStep)); + + int maxrounds = GetConVarInt(g_Cvar_Maxrounds); + if(maxrounds) + SetConVarInt(g_Cvar_Maxrounds, maxrounds + GetConVarInt(g_Cvar_ExtendRoundStep)); + + int fraglimit = GetConVarInt(g_Cvar_Fraglimit); + if(fraglimit) + SetConVarInt(g_Cvar_Fraglimit, fraglimit + GetConVarInt(g_Cvar_ExtendFragStep)); + + CPrintToChatAll("[MCE] %t", "Current Map Extended", RoundToFloor(float(item_info[0][VOTEINFO_ITEM_VOTES])/float(num_votes)*100.0), num_votes); + LogAction(-1, -1, "Voting for next map has finished. The current map has been extended."); + CPrintToChatAll("[MCE] Available Extends: %d", GetConVarInt(g_Cvar_Extend) - g_Extends); + + // We extended, so well have to vote again. + g_RunoffCount = 0; + g_HasVoteStarted = false; + SetupTimeleftTimer(); + } + else if(strcmp(map, VOTE_DONTCHANGE, false) == 0) + { + CPrintToChatAll("[MCE] %t", "Current Map Stays", RoundToFloor(float(item_info[0][VOTEINFO_ITEM_VOTES])/float(num_votes)*100.0), num_votes); + LogAction(-1, -1, "Voting for next map has finished. 'No Change' was the winner"); + + g_RunoffCount = 0; + g_HasVoteStarted = false; + SetupTimeleftTimer(); + } + else + { + if(g_ChangeTime == MapChange_MapEnd) + { + SetNextMap(map); + } + else if(g_ChangeTime == MapChange_Instant) + { + Handle data; + CreateDataTimer(4.0, Timer_ChangeMap, data); + WritePackString(data, map); + g_ChangeMapInProgress = false; + } + else // MapChange_RoundEnd + { + SetNextMap(map); + g_ChangeMapAtRoundEnd = true; + } + + g_HasVoteStarted = false; + g_MapVoteCompleted = true; + + CPrintToChatAll("[MCE] %t", "Nextmap Voting Finished", map, RoundToFloor(float(item_info[0][VOTEINFO_ITEM_VOTES])/float(num_votes)*100.0), num_votes); + LogAction(-1, -1, "Voting for next map has finished. Nextmap: %s.", map); + } +} + +public void Handler_MapVoteFinished(Handle menu, + int num_votes, + int num_clients, + const int[][] client_info, + int num_items, + const int[][] item_info) +{ + // Implement revote logic - Only run this` block if revotes are enabled and this isn't the last revote' + prevent_rare_map_vote_bug_2023 = false; + if(GetConVarBool(g_Cvar_RunOff) && num_items > 1 && g_RunoffCount < GetConVarInt(g_Cvar_MaxRunOffs)) + { + g_RunoffCount++; + int highest_votes = item_info[0][VOTEINFO_ITEM_VOTES]; + int required_percent = GetConVarInt(g_Cvar_RunOffPercent); + int required_votes = RoundToCeil(float(num_votes) * float(required_percent) / 100.0); + + if(highest_votes == item_info[1][VOTEINFO_ITEM_VOTES]) + { + g_HasVoteStarted = false; + + //Revote is needed + int arraySize = ByteCountToCells(PLATFORM_MAX_PATH); + Handle mapList = CreateArray(arraySize); + + for(int i = 0; i < num_items; i++) + { + if(item_info[i][VOTEINFO_ITEM_VOTES] == highest_votes) + { + static char map[PLATFORM_MAX_PATH]; + + GetMapItem(menu, item_info[i][VOTEINFO_ITEM_INDEX], map, PLATFORM_MAX_PATH); + PushArrayString(mapList, map); + } + else + break; + } + + CPrintToChatAll("[MCE] %t", "Tie Vote", GetArraySize(mapList)); + SetupWarningTimer(WarningType_Revote, view_as(g_ChangeTime), mapList); + return; + } + else if(highest_votes < required_votes) + { + g_HasVoteStarted = false; + + //Revote is needed + int arraySize = ByteCountToCells(PLATFORM_MAX_PATH); + Handle mapList = CreateArray(arraySize); + + static char map1[PLATFORM_MAX_PATH]; + GetMapItem(menu, item_info[0][VOTEINFO_ITEM_INDEX], map1, PLATFORM_MAX_PATH); + + PushArrayString(mapList, map1); + + // We allow more than two maps for a revote if they are tied + for(int i = 1; i < num_items; i++) + { + if(GetArraySize(mapList) < 2 || item_info[i][VOTEINFO_ITEM_VOTES] == item_info[i - 1][VOTEINFO_ITEM_VOTES]) + { + static char map[PLATFORM_MAX_PATH]; + GetMapItem(menu, item_info[i][VOTEINFO_ITEM_INDEX], map, PLATFORM_MAX_PATH); + PushArrayString(mapList, map); + } + else + break; + } + + CPrintToChatAll("[MCE] %t", "Revote Is Needed", required_percent); + SetupWarningTimer(WarningType_Revote, view_as(g_ChangeTime), mapList); + return; + } + } + + // No revote needed, continue as normal. + Handler_VoteFinishedGeneric(menu, num_votes, num_clients, client_info, num_items, item_info); +} + +public int Handler_MapVoteMenu(Handle menu, MenuAction action, int param1, int param2) +{ + switch(action) + { + case MenuAction_End: + { + g_VoteMenu = INVALID_HANDLE; + delete menu; + } + + case MenuAction_Display: + { + static char buffer[255]; + Format(buffer, sizeof(buffer), "%T", "Vote Nextmap", param1); + Handle panel = view_as(param2); + SetPanelTitle(panel, buffer); + DrawPanelText(panel, "Warning: The Position of the Maps are different for each Player."); + } + + case MenuAction_DisplayItem: + { + char map[PLATFORM_MAX_PATH]; + char buffer[255]; + int mark = GetConVarInt(g_Cvar_MarkCustomMaps); + + GetMenuItem(menu, param2, map, PLATFORM_MAX_PATH, _, _, _, param1); + + if(StrEqual(map, VOTE_EXTEND, false)) + { + Format(buffer, sizeof(buffer), "%T", "Extend Map", param1); + } + else if(StrEqual(map, VOTE_DONTCHANGE, false)) + { + Format(buffer, sizeof(buffer), "%T", "Dont Change", param1); + } + // Mapchooser Extended + else if(StrEqual(map, LINE_ONE, false)) + { + Format(buffer, sizeof(buffer),"%T", "Line One", param1); + } + else if(StrEqual(map, LINE_TWO, false)) + { + Format(buffer, sizeof(buffer),"%T", "Line Two", param1); + } + // Note that the first part is to discard the spacer line + else if(!StrEqual(map, LINE_SPACER, false)) + { + if(mark == 1 && !InternalIsMapOfficial(map)) + { + Format(buffer, sizeof(buffer), "%T", "Custom Marked", param1, map); + } + else if(mark == 2 && !InternalIsMapOfficial(map)) + { + Format(buffer, sizeof(buffer), "%T", "Custom", param1, map); + } + else if(InternalGetMapVIPRestriction(map)) + { + Format(buffer, sizeof(buffer), "%s (%T)", map, "VIP Nomination", param1); + } + } + + if(buffer[0] != '\0') + { + return RedrawMenuItem(buffer); + } + // End Mapchooser Extended + } + + case MenuAction_VoteCancel: + { + // If we receive 0 votes, pick at random. + if(param1 == VoteCancel_NoVotes && GetConVarBool(g_Cvar_NoVoteMode)) + { + int count = GetMenuItemCount(menu); + + int item; + static char map[PLATFORM_MAX_PATH]; + + do + { + int startInt = 0; + if(g_BlockedSlots) + { + if(g_AddNoVote) + { + startInt = 2; + } + else + { + startInt = 3; + } + } + item = GetRandomInt(startInt, count - 1); + + GetMenuItem(menu, item, map, PLATFORM_MAX_PATH, _, _, _, param1); + } + while(strcmp(map, VOTE_EXTEND, false) == 0); + + SetNextMap(map); + g_MapVoteCompleted = true; + } + + g_HasVoteStarted = false; + } + } + + return 0; +} + +public Action Timer_ChangeMap(Handle hTimer, Handle dp) +{ + g_ChangeMapInProgress = false; + + char map[PLATFORM_MAX_PATH]; + + if(dp == INVALID_HANDLE) + { + if(!GetNextMap(map, PLATFORM_MAX_PATH)) + { + //No passed map and no set nextmap. fail! + return Plugin_Stop; + } + } + else + { + ResetPack(dp); + ReadPackString(dp, map, PLATFORM_MAX_PATH); + } + + ForceChangeLevel(map, "Map Vote"); + + return Plugin_Stop; +} + +bool RemoveStringFromArray(Handle array, char[] str) +{ + int index = FindStringInArray(array, str); + if(index != -1) + { + RemoveFromArray(array, index); + return true; + } + + return false; +} + +void CreateNextVote() +{ + assert(g_NextMapList) + ClearArray(g_NextMapList); + + static char map[PLATFORM_MAX_PATH]; + Handle tempMaps = CloneArray(g_MapList); + + GetCurrentMap(map, PLATFORM_MAX_PATH); + RemoveStringFromArray(tempMaps, map); + + if(GetArraySize(tempMaps) > GetConVarInt(g_Cvar_ExcludeMaps) && InternalAreRestrictionsActive()) + { + StringMapSnapshot OldMapListSnapshot = g_OldMapList.Snapshot(); + for(int i = 0; i < OldMapListSnapshot.Length; i++) + { + OldMapListSnapshot.GetKey(i, map, sizeof(map)); + RemoveStringFromArray(tempMaps, map); + } + delete OldMapListSnapshot; + } + + if(InternalAreRestrictionsActive()) + { + StringMapSnapshot TimeMapListSnapshot = g_TimeMapList.Snapshot(); + for(int i = 0; i < TimeMapListSnapshot.Length; i++) + { + TimeMapListSnapshot.GetKey(i, map, sizeof(map)); + int Cooldown; + g_TimeMapList.GetValue(map, Cooldown); + + if(Cooldown > GetTime()) + RemoveStringFromArray(tempMaps, map); + } + delete TimeMapListSnapshot; + } + + int voteSize = GetVoteSize(2); + int limit = (voteSize < GetArraySize(tempMaps) ? voteSize : GetArraySize(tempMaps)); + + // group -> number of maps nominated from group + StringMap groupmap = new StringMap(); + char groupstr[8]; + + // populate groupmap with maps from nomination list + static char map_[PLATFORM_MAX_PATH]; + int groups_[32]; + + //2023 edit + Handle most_nominated_maps = get_most_nominated_maps(); + for(int i = 0; i < GetArraySize(most_nominated_maps); i++) + { + GetArrayString(most_nominated_maps, i, map_, PLATFORM_MAX_PATH); + int groupsfound = InternalGetMapGroups(map_, groups_, sizeof(groups_)); + for(int group = 0; group < groupsfound; group++) + { + IntToString(group, groupstr, sizeof(groupstr)); + int groupcur = 0; + groupmap.GetValue(groupstr, groupcur); + groupcur++; + groupmap.SetValue(groupstr, groupcur, true); + } + } + + // find random maps which honor all restrictions + for(int i = 0; i < limit; i++) + { + int b; + for(int j = 0; j < 1000; j++) + { + b = GetRandomInt(0, GetArraySize(tempMaps) - 1); + GetArrayString(tempMaps, b, map, PLATFORM_MAX_PATH); + + if(!InternalAreRestrictionsActive()) + break; + + if(InternalGetMapVIPRestriction(map)) + continue; + + if(InternalGetMapTimeRestriction(map) != 0) + continue; + + if(InternalGetMapPlayerRestriction(map) != 0) + continue; + + if (InternalGetAveragePlayerHourRestriction(map) != 0) + continue; + + bool okay = true; + + int groups[32]; + int groupsfound = InternalGetMapGroups(map, groups, sizeof(groups)); + for(int group = 0; group < groupsfound; group++) + { + IntToString(group, groupstr, sizeof(groupstr)); + + int groupmax = InternalGetGroupMax(groups[group]); + if(groupmax >= 0) + { + int groupcur = 0; + groupmap.GetValue(groupstr, groupcur); + + if(groupcur >= groupmax) + { + okay = false; + break; + } + + groupcur++; + groupmap.SetValue(groupstr, groupcur, true); + } + } + + if(okay) + break; + } + PushArrayString(g_NextMapList, map); + RemoveFromArray(tempMaps, b); + } + + delete groupmap; + delete tempMaps; +} + +bool CanVoteStart() +{ + if(g_WaitingForVote || g_HasVoteStarted) + return false; + return true; +} + +NominateResult InternalNominateMap(char[] map, int owner) +{ + if(!IsMapValid(map)) + { + return Nominate_InvalidMap; + } + + + /* Look to replace an existing nomination by this client - Nominations made with owner = 0 arent replaced */ + //2023 edit: change clients first nomination out of the clients multiple nominations, make a check if client filled all his nomination slots + //currently hard coded to 3 maps, just add a cvar to replace it with in the future + if(owner && ((FindValueInArray(g_NominateOwners, owner)) != -1) && GetArraySize(g_NominateList[owner]) > 2) + { + char oldmap[PLATFORM_MAX_PATH]; + GetArrayString(g_NominateList[owner], 0, oldmap, PLATFORM_MAX_PATH); + Call_StartForward(g_NominationsResetForward); + Call_PushString(oldmap); + Call_Finish(); + + RemoveFromArray(g_NominateList[owner], 0); + PushArrayString(g_NominateList[owner], map); + return Nominate_Replaced; + } + + + /* Too many nominated maps. */ + //2023 edit: we dont want this check + /* + if(g_NominateCount >= GetVoteSize(0) && !force) + { + return Nominate_VoteFull; + } + */ + + if (owner != 0 && g_NominateList[owner] != INVALID_HANDLE) + { + for (int j = 0; j < GetArraySize(g_NominateList[owner]); j++) + { + char map_iteration[PLATFORM_MAX_PATH]; + GetArrayString(g_NominateList[owner], j, map_iteration, PLATFORM_MAX_PATH); + if (StrEqual(map, map_iteration, false)) + { + return Nominate_InvalidMap; + } + } + } + + if (g_NominateList[owner] == INVALID_HANDLE) + { + if (IsClientConnected(owner) && IsClientInGame(owner)) + { + ReplyToCommand(owner, "This should be invalid."); + } + return Nominate_InvalidMap; + } + + PushArrayString(g_NominateList[owner], map); + PushArrayCell(g_NominateOwners, owner); //maybe i only want to do this for the first nomination of each client + if(owner == 0 && g_NominateReservedCount < GetVoteSize(1)) + g_NominateReservedCount++; + else + g_NominateCount++; + + while(GetArraySize(g_NominateList[owner]) > GetVoteSize(2)) + { + char oldmap[PLATFORM_MAX_PATH]; + GetArrayString(g_NominateList[owner], 0, oldmap, PLATFORM_MAX_PATH); + Call_StartForward(g_NominationsResetForward); + Call_PushString(oldmap); + int owner_ = GetArrayCell(g_NominateOwners, 0); + Call_Finish(); + + RemoveFromArray(g_NominateList[owner], 0); + RemoveFromArray(g_NominateOwners, 0); + if(owner_ == 0) + g_NominateReservedCount--; + else + g_NominateCount--; + } + + return Nominate_Added; +} + +/* Add natives to allow nominate and initiate vote to be call */ + +/* native bool NominateMap(const char[] map, bool force, &NominateError:error); */ +public int Native_NominateMap(Handle plugin, int numParams) +{ + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + return view_as(InternalNominateMap(map, GetNativeCell(3))); +} + +bool InternalRemoveNominationByMap(char[] map) +{ + for (int client = 0; client < MaxClients; client++) + { + for(int i = 0; i < GetArraySize(g_NominateList[client]); i++) + { + char oldmap[PLATFORM_MAX_PATH]; + GetArrayString(g_NominateList[client], i, oldmap, PLATFORM_MAX_PATH); + + if(strcmp(map, oldmap, false) == 0) + { + Call_StartForward(g_NominationsResetForward); + Call_PushString(oldmap); + Call_Finish(); + + int owner = GetArrayCell(g_NominateOwners, i); + if(owner) + g_NominateCount--; + else + g_NominateReservedCount--; + + RemoveFromArray(g_NominateList[client], i); + RemoveFromArray(g_NominateOwners, i); + + return true; + } + } + } + return false; +} + +/* native bool RemoveNominationByMap(const char[] map); */ +public int Native_RemoveNominationByMap(Handle plugin, int numParams) +{ + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + return view_as(InternalRemoveNominationByMap(map)); +} + +bool InternalRemoveNominationByOwner(int owner) +{ + int index; + + if(owner && ((index = FindValueInArray(g_NominateOwners, owner)) != -1)) + { + char oldmap[PLATFORM_MAX_PATH]; + GetArrayString(g_NominateList[owner], index, oldmap, PLATFORM_MAX_PATH); + + Call_StartForward(g_NominationsResetForward); + Call_PushString(oldmap); + Call_Finish(); + + RemoveFromArray(g_NominateList[owner], index); + //maybe only do once or change g_NominateOwners + RemoveFromArray(g_NominateOwners, index); + g_NominateCount--; + + return true; + } + + return false; +} + +/* native bool RemoveNominationByOwner(owner); */ +public int Native_RemoveNominationByOwner(Handle plugin, int numParams) +{ + return view_as(InternalRemoveNominationByOwner(GetNativeCell(1))); +} + +/* native InitiateMapChooserVote(); */ +public int Native_InitiateVote(Handle plugin, int numParams) +{ + MapChange when = view_as(GetNativeCell(1)); + Handle inputarray = view_as(GetNativeCell(2)); + + LogAction(-1, -1, "Starting map vote because outside request"); + + SetupWarningTimer(WarningType_Vote, when, inputarray); + return 0; +} + +public int Native_CanVoteStart(Handle plugin, int numParams) +{ + return CanVoteStart(); +} + +public int Native_CheckVoteDone(Handle plugin, int numParams) +{ + return g_MapVoteCompleted; +} + +public int Native_EndOfMapVoteEnabled(Handle plugin, int numParams) +{ + return GetConVarBool(g_Cvar_EndOfMapVote); +} + +public int Native_GetExcludeMapList(Handle plugin, int numParams) +{ + Handle array = view_as(GetNativeCell(1)); + if(array == INVALID_HANDLE) + return 0; + + static char map[PLATFORM_MAX_PATH]; + StringMapSnapshot OldMapListSnapshot = g_OldMapList.Snapshot(); + for(int i = 0; i < OldMapListSnapshot.Length; i++) + { + OldMapListSnapshot.GetKey(i, map, sizeof(map)); + PushArrayString(array, map); + } + delete OldMapListSnapshot; + return 0; +} + +//GetNominatedMapList +public int Native_GetNominatedMapList(Handle plugin, int numParams) +{ + Handle maparray = view_as(GetNativeCell(1)); + Handle ownerarray = view_as(GetNativeCell(2)); + + if(maparray == INVALID_HANDLE) + return 0; + + static char map[PLATFORM_MAX_PATH]; + CheckMapRestrictions(true, true); + /* + when a guy checks the nomlist the only thing that has changed is the time. + Its not smart to display a map in the nominate list just for Initiate Vote() to discard it anyways from the map vote just because of time based restriction + */ + for (int client = 0; client < MaxClients; client++) + { + for(int i = 0; i < GetArraySize(g_NominateList[client]); i++) + { + GetArrayString(g_NominateList[client], i, map, PLATFORM_MAX_PATH); + PushArrayString(maparray, map); + + // If the optional parameter for an owner list was passed, then we need to fill that out as well + if(ownerarray != INVALID_HANDLE) + { + //int index = GetArrayCell(g_NominateOwners, i); + PushArrayCell(ownerarray, client); + } + } + } + return 0; +} + +// Functions new to Mapchooser Extended +stock void SetupWarningTimer(WarningType type, MapChange when=MapChange_MapEnd, Handle mapList=INVALID_HANDLE, bool force=false) +{ + if(!GetArraySize(g_MapList) || g_ChangeMapInProgress || g_HasVoteStarted || (!force && ((when == MapChange_MapEnd && !GetConVarBool(g_Cvar_EndOfMapVote)) || g_MapVoteCompleted))) + return; + + bool interrupted = false; + if(g_WarningInProgress && g_WarningTimer != INVALID_HANDLE) + { + interrupted = true; + KillTimer(g_WarningTimer); + } + + g_WarningInProgress = true; + + Handle forwardVote; + Handle cvarTime; + static char translationKey[64]; + + switch(type) + { + case WarningType_Vote: + { + forwardVote = g_MapVoteWarningStartForward; + cvarTime = g_Cvar_WarningTime; + strcopy(translationKey, sizeof(translationKey), "Vote Warning"); + + } + + case WarningType_Revote: + { + forwardVote = g_MapVoteRunoffStartForward; + cvarTime = g_Cvar_RunOffWarningTime; + strcopy(translationKey, sizeof(translationKey), "Revote Warning"); + + } + } + + if(!interrupted) + { + Call_StartForward(forwardVote); + Call_Finish(); + } + + Handle data; + g_WarningTimer = CreateDataTimer(1.0, Timer_StartMapVote, data, TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); + WritePackCell(data, GetConVarInt(cvarTime)); + WritePackString(data, translationKey); + WritePackCell(data, view_as(when)); + WritePackCell(data, view_as(mapList)); + ResetPack(data); +} + +stock void InitializeOfficialMapList() +{ + // If this fails, we want it to have an empty adt_array + if(ReadMapList(g_OfficialList, + g_mapOfficialFileSerial, + "official", + MAPLIST_FLAG_CLEARARRAY|MAPLIST_FLAG_NO_DEFAULT) + != INVALID_HANDLE) + { + LogMessage("Loaded map list for %s.", g_GameModName); + } + // Check if the map list was ever loaded + else if(g_mapOfficialFileSerial == -1) + { + LogMessage("No official map list found for %s. Consider submitting one!", g_GameModName); + } +} + +stock bool IsMapEndVoteAllowed() +{ + if(!GetConVarBool(g_Cvar_EndOfMapVote) || g_MapVoteCompleted || g_HasVoteStarted) + return false; + return true; +} + +public int Native_IsMapOfficial(Handle plugin, int numParams) +{ + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + return InternalIsMapOfficial(map); +} + +bool InternalIsMapOfficial(const char[] mapname) +{ + int officialMapIndex = FindStringInArray(g_OfficialList, mapname); + return (officialMapIndex > -1); +} + +public int Native_IsWarningTimer(Handle plugin, int numParams) +{ + return g_WarningInProgress; +} + +public int Native_CanNominate(Handle plugin, int numParams) +{ + if(g_HasVoteStarted) + { + return view_as(CanNominate_No_VoteInProgress); + } + + if(g_MapVoteCompleted) + { + return view_as(CanNominate_No_VoteComplete); + } + + if(g_NominateCount >= GetVoteSize()) + { + return view_as(CanNominate_No_VoteFull); + } + + return view_as(CanNominate_Yes); +} + +public int Native_ExcludeMap(Handle plugin, int numParams) +{ + if(!InternalAreRestrictionsActive()) + return true; + + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + int Cooldown; + int Mode = GetNativeCell(3); + + if(Mode == 0) + { + Cooldown = InternalGetMapCooldown(map); + } + else if(Mode == 1) + { + Cooldown = GetNativeCell(2); + } + else if(Mode == 2) + { + g_OldMapList.GetValue(map, Cooldown); + int NewCooldown = GetNativeCell(2); + if(NewCooldown > Cooldown) + Cooldown = NewCooldown; + } + + g_OldMapList.SetValue(map, Cooldown, true); + InternalStoreMapCooldowns(); + + return true; +} + +public int Native_ExcludeMapTime(Handle plugin, int numParams) +{ + if(!InternalAreRestrictionsActive()) + return true; + + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + int Cooldown; + int Mode = GetNativeCell(3); + + if(Mode == 0) + { + Cooldown = InternalGetMapCooldownTime(map); + } + else if(Mode == 1) + { + Cooldown = GetNativeCell(2); + } + else if(Mode == 2) + { + g_TimeMapList.GetValue(map, Cooldown); + int NewCooldown = GetTime() + GetNativeCell(2); + if(NewCooldown > Cooldown) + Cooldown = GetNativeCell(2); + else + return true; + } + + Cooldown += GetTime(); + g_TimeMapList.SetValue(map, Cooldown, true); + InternalStoreMapCooldowns(); + + return true; +} + +public int Native_GetMapCooldown(Handle plugin, int numParams) +{ + if(!InternalAreRestrictionsActive()) + return 0; + + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + int Cooldown = 0; + g_OldMapList.GetValue(map, Cooldown); + + return Cooldown; +} + +public int Native_GetMapCooldownTime(Handle plugin, int numParams) +{ + if(!InternalAreRestrictionsActive()) + return 0; + + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + int Cooldown = 0; + g_TimeMapList.GetValue(map, Cooldown); + + return Cooldown; +} + +public int Native_GetMapMinTime(Handle plugin, int numParams) +{ + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + return InternalGetMapMinTime(map); +} + +public int Native_GetMapMaxTime(Handle plugin, int numParams) +{ + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + return InternalGetMapMaxTime(map); +} + +public int Native_GetMapMinPlayers(Handle plugin, int numParams) +{ + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + return InternalGetMapMinPlayers(map); +} + +public int Native_GetMapMaxPlayers(Handle plugin, int numParams) +{ + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + return InternalGetMapMaxPlayers(map); +} + +public int Native_GetMapTimeRestriction(Handle plugin, int numParams) +{ + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + return InternalGetMapTimeRestriction(map); +} + +//GetAveragePlayerTimeOnServerMapRestriction +public int Native_GetAveragePlayerTimeOnServerMapRestriction(Handle plugin, int numParams) +{ + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + return InternalGetAveragePlayerHourRestriction(map); +} + +stock int InternalGetAveragePlayerHourRestriction(const char[] map) +{ + int players_average_hours = GetAveragePlayerTimeOnServer(); + int MinAverageHours = 0; + if(g_Config && g_Config.JumpToKey(map)) + { + MinAverageHours = g_Config.GetNum("MinHoursAvg", MinAverageHours); + g_Config.Rewind(); + } + //0 means map can be nominated, anything above 0 means more hours are required + if (players_average_hours >= MinAverageHours) + { + return 0; + } + return MinAverageHours - players_average_hours; +} +//GetMapPlayerRestriction +public int Native_GetMapPlayerRestriction(Handle plugin, int numParams) +{ + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + return InternalGetMapPlayerRestriction(map); +} + +public int Native_GetMapGroups(Handle plugin, int numParams) +{ + int len; + GetNativeStringLength(1, len); + int size = GetNativeCell(3); + + if(len <= 0) + return -999; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + int[] groups = new int[size]; + int found = InternalGetMapGroups(map, groups, size); + if(found >= 0) + SetNativeArray(2, groups, size); + return found; +} + +//GetMapGroupRestriction +public int Native_GetMapGroupRestriction(Handle plugin, int numParams) +{ + GetNativeCell(2); + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return -999; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + int groups[32]; + int groupsfound = InternalGetMapGroups(map, groups, sizeof(groups)); + + for(int group = 0; group < groupsfound; group ++) + { + int groupcur = 0; + int groupmax = InternalGetGroupMax(groups[group]); + + if(groupmax >= 0) + { + static char map_[PLATFORM_MAX_PATH]; + char kv_map[PLATFORM_MAX_PATH]; + int groups_[32]; + KeyValues kv = CreateKeyValues("cur_groups"); + for (int clienti = 0; clienti < MaxClients; clienti++) + { + for(int i = 0; i < GetArraySize(g_NominateList[clienti]); i++) + { + GetArrayString(g_NominateList[clienti], i, map_, PLATFORM_MAX_PATH); + int tmp = InternalGetMapGroups(map_, groups_, sizeof(groups_)); + kv.GetString(map_, kv_map, sizeof(kv_map), "not_found"); + if(FindIntInArray(groups_, tmp, groups[group]) != -1 && StrEqual(kv_map, "not_found")) + { + groupcur++; + kv.SetString(map_, map_); + } + } + } + delete kv; + } + } + + return -1; +} + +public int Native_GetMapVIPRestriction(Handle plugin, int numParams) +{ + int client = GetNativeCell(2); + int len; + GetNativeStringLength(1, len); + + if(len <= 0) + return false; + + char[] map = new char[len+1]; + GetNativeString(1, map, len+1); + + // Check if client should bypass vip restrictions + if(client >= 1 && client <= MaxClients) + { + // Client has bypass flag, dont return vip restrictions + if(CheckCommandAccess(client, "sm_nominate_ignore", ADMFLAG_CHEATS)) + return false; + + // Client has vip flag, dont return vip restrictions + if(CheckCommandAccess(client, "sm_nominate_vip", ADMFLAG_CUSTOM1)) + return false; + } + + return InternalGetMapVIPRestriction(map); +} + +public int Native_GetExtendsLeft(Handle plugin, int numParams) +{ + return GetConVarInt(g_Cvar_Extend) - g_Extends; +} + +public int Native_AreRestrictionsActive(Handle plugin, int numParams) +{ + return InternalAreRestrictionsActive(); +} + +public int Native_SimulateMapEnd(Handle plugin, int numParams) +{ + OnMapEnd(); + return 0; +} + +stock void AddMapItem(const char[] map) +{ + AddMenuItem(g_VoteMenu, map, map); +} + +stock void GetMapItem(Handle menu, int position, char[] map, int mapLen) +{ + GetMenuItem(menu, position, map, mapLen, _, _, _, -1); +} + +stock void AddExtendToMenu(Handle menu, MapChange when) +{ + /* Do we add any special items? */ + // Moved for Mapchooser Extended + + if((when == MapChange_Instant || when == MapChange_RoundEnd) && GetConVarBool(g_Cvar_DontChange)) + { + // Built-in votes doesnt have "Dont Change", send Extend instead + AddMenuItem(menu, VOTE_DONTCHANGE, "Dont Change"); + } + else if(GetConVarBool(g_Cvar_Extend) && g_Extends < GetConVarInt(g_Cvar_Extend)) + { + AddMenuItem(menu, VOTE_EXTEND, "Extend Map"); + } +} + +// 0 = IncludeMaps, 1 = Reserved, 2 = IncludeMaps+Reserved +stock int GetVoteSize(int what=0) +{ + int includeMaps = GetConVarInt(g_Cvar_IncludeMaps); + int includeMapsReserved = GetConVarInt(g_Cvar_IncludeMapsReserved); + + if(what == 0) + return includeMaps; + else if(what == 1) + return includeMapsReserved; + else if(what == 2) + return includeMaps + includeMapsReserved; + + return 0; +} + +stock int InternalGetMapCooldown(const char[] map) +{ + int Cooldown = g_Cvar_ExcludeMaps.IntValue; + + if(g_Config && g_Config.JumpToKey(map)) + { + Cooldown = g_Config.GetNum("Cooldown", Cooldown); + g_Config.Rewind(); + } + + return Cooldown; +} + +stock int InternalGetMapCooldownTime(const char[] map) +{ + char time[16]; + g_Cvar_ExcludeMapsTime.GetString(time, sizeof(time)); + int Cooldown = TimeStrToSeconds(time); + + if(g_Config && g_Config.JumpToKey(map)) + { + g_Config.GetString("CooldownTime", time, sizeof(time), ""); + if(time[0]) + Cooldown = TimeStrToSeconds(time); + + g_Config.Rewind(); + } + + return Cooldown; +} + +void CheckMapRestrictions(bool time = false, bool players = false) +{ + if(!InternalAreRestrictionsActive()) + return; + + static char map[PLATFORM_MAX_PATH]; + + for (int client = 0; client < MaxClients; client++) + { + if(CheckCommandAccess(client, "sm_nominate_ignore", ADMFLAG_CHEATS, true)) + continue; + for (int i = 0; i < GetArraySize(g_NominateList[client]); i++) + { + bool remove; + GetArrayString(g_NominateList[client], i, map, PLATFORM_MAX_PATH); + + if (time) + { + int TimeRestriction = InternalGetMapTimeRestriction(map); + if(TimeRestriction) + { + remove = true; + + if (client > 0 && client <= MaxClients && IsClientConnected(client) && IsClientInGame(client)) + { + CPrintToChat(client, "[MCE] %t", "Nomination Removed Time Error", map); + } + } + } + + if (players) + { + int PlayerRestriction = InternalGetMapPlayerRestriction(map); + if(PlayerRestriction) + { + remove = true; + + if (client > 0 && client <= MaxClients && IsClientConnected(client) && IsClientInGame(client)) + { + if(PlayerRestriction < 0) + CPrintToChat(client, "[MCE] %t", "Nomination Removed MinPlayers Error", map); + else + CPrintToChat(client, "[MCE] %t", "Nomination Removed MaxPlayers Error", map); + } + } + if (InternalGetAveragePlayerHourRestriction(map) > 0 && !remove) + { + remove = true; + if (client > 0 && client <= MaxClients && IsClientConnected(client) && IsClientInGame(client)) + { + ReplyToCommand(client, "[MCE] Your nomination %s has been removed because it does not meet the minimum Average hour requirements.", map); + } + } + } + + if (remove) + { + if (strlen(map) > 0) + { + Call_StartForward(g_NominationsResetForward); + Call_PushString(map); + Call_Finish(); + } + + RemoveFromArray(g_NominateList[client], i); + RemoveFromArray(g_NominateOwners, i); + g_NominateCount--; + } + } + } +} + +stock int InternalGetMapMinTime(const char[] map) +{ + int MinTime = 0; + + if(g_Config && g_Config.JumpToKey(map)) + { + MinTime = g_Config.GetNum("MinTime", MinTime); + g_Config.Rewind(); + } + + return MinTime; +} + +stock int InternalGetMapMaxTime(const char[] map) +{ + int MaxTime = 0; + + if(g_Config && g_Config.JumpToKey(map)) + { + MaxTime = g_Config.GetNum("MaxTime", MaxTime); + g_Config.Rewind(); + } + + return MaxTime; +} + +stock int InternalGetMapMinPlayers(const char[] map) +{ + int MinPlayers = 0; + + if(g_Config && g_Config.JumpToKey(map)) + { + MinPlayers = g_Config.GetNum("MinPlayers", MinPlayers); + g_Config.Rewind(); + } + + return MinPlayers; +} + +stock int InternalGetMapMaxPlayers(const char[] map) +{ + int MaxPlayers = 0; + + if(g_Config && g_Config.JumpToKey(map)) + { + MaxPlayers = g_Config.GetNum("MaxPlayers", MaxPlayers); + g_Config.Rewind(); + } + + return MaxPlayers; +} + +stock int InternalGetMapGroups(const char[] map, int[] groups, int size) +{ + int found = 0; + if(g_Config && g_Config.JumpToKey("_groups")) + { + if(!g_Config.GotoFirstSubKey(false)) + { + g_Config.Rewind(); + return -999; + } + + do + { + char groupstr[8]; + g_Config.GetSectionName(groupstr, sizeof(groupstr)); + int group = StringToInt(groupstr); + if(g_Config.JumpToKey(map, false)) + { + groups[found++] = group; + if(found >= size) + { + g_Config.Rewind(); + return found; + } + g_Config.GoBack(); + } + } while(g_Config.GotoNextKey()); + + g_Config.Rewind(); + } + + return found; +} + +stock int InternalGetGroupMax(int group) +{ + char groupstr[8]; + IntToString(group, groupstr, sizeof(groupstr)); + if(g_Config && g_Config.JumpToKey("_groups")) + { + if(g_Config.JumpToKey(groupstr, false)) + { + int max = g_Config.GetNum("_max", -1); + g_Config.Rewind(); + return max; + } + + g_Config.Rewind(); + } + + return -1; +} + +// 0 = Okay +// >0 = Minutes till Okay +stock int InternalGetMapTimeRestriction(const char[] map) +{ + char sTime[8]; + FormatTime(sTime, sizeof(sTime), "%H%M"); + + int CurTime = StringToInt(sTime); + int MinTime = InternalGetMapMinTime(map); + int MaxTime = InternalGetMapMaxTime(map); + + //Wrap around. + CurTime = (CurTime <= MinTime) ? CurTime + 2400 : CurTime; + MaxTime = (MaxTime <= MinTime) ? MaxTime + 2400 : MaxTime; + + if (!(MinTime <= CurTime <= MaxTime)) + { + //Wrap around. + MinTime = (MinTime <= CurTime) ? MinTime + 2400 : MinTime; + MinTime = (MinTime <= MaxTime) ? MinTime + 2400 : MinTime; + + // Convert our 'time' to minutes. + CurTime = (RoundToFloor(float(CurTime / 100)) * 60) + (CurTime % 100); + MinTime = (RoundToFloor(float(MinTime / 100)) * 60) + (MinTime % 100); + MaxTime = (RoundToFloor(float(MaxTime / 100)) * 60) + (MaxTime % 100); + + return MinTime - CurTime; + } + + return 0; +} + +public void OnClientPostAdminCheck(int client) +{ + is_bot_player[client] = false; + char auth[50]; + GetClientAuthId(client, AuthId_Engine, auth, sizeof(auth)); + if (StrEqual("[U:1:1221121532]", auth, false) || StrEqual("STEAM_0:0:610560766", auth, false)) + { + is_bot_player[client] = true; + } + if (StrEqual("[U:1:408797742]", auth, false) || StrEqual("STEAM_0:0:204398871", auth, false)) + { + is_bot_player[client] = true; + } + if (StrEqual("[U:1:1036189204]", auth, false) || StrEqual("STEAM_0:0:518094602", auth, false)) + { + is_bot_player[client] = true; + } + if (StrEqual("[U:1:120378081]", auth, false) || StrEqual("STEAM_0:1:60189040", auth, false)) + { + is_bot_player[client] = true; + } +} + +// <0 = Less than MinPlayers +// 0 = Okay +// >0 = More than MaxPlayers +stock int InternalGetMapPlayerRestriction(const char[] map) +{ + //int NumPlayers = GetClientCount(false); + int NumPlayers = 0; + //excluding non connected players, fakeclients, sourceTV, autism bots and nosteamers from player count restriction + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientConnected(client) && IsClientInGame(client) && !IsFakeClient(client) && !IsClientSourceTV(client) && !is_bot_player[client] + && PM_IsPlayerSteam(client)) + { + NumPlayers++; + } + } + + int MinPlayers = InternalGetMapMinPlayers(map); + int MaxPlayers = InternalGetMapMaxPlayers(map); + + if(MinPlayers && NumPlayers < MinPlayers) + return NumPlayers - MinPlayers; + + if(MaxPlayers && NumPlayers > MaxPlayers) + return NumPlayers - MaxPlayers; + + return 0; +} + +stock bool InternalAreRestrictionsActive() +{ + if (!GetConVarBool(g_Cvar_NoRestrictionTimeframeEnable)) + return true; + + char sTime[8]; + FormatTime(sTime, sizeof(sTime), "%H%M"); + + int CurTime = StringToInt(sTime); + int MinTime = GetConVarInt(g_Cvar_NoRestrictionTimeframeMinTime); + int MaxTime = GetConVarInt(g_Cvar_NoRestrictionTimeframeMaxTime); + + //Wrap around. + CurTime = (CurTime <= MinTime) ? CurTime + 2400 : CurTime; + MaxTime = (MaxTime <= MinTime) ? MaxTime + 2400 : MaxTime; + + if ((MinTime <= CurTime <= MaxTime)) + { + return false; + } + + return true; +} + +stock int FindIntInArray(int[] array, int size, int value) +{ + for(int i = 0; i < size; i++) + { + if(array[i] == value) + return i; + } + + return -1; +} + +stock bool InternalGetMapVIPRestriction(const char[] map) +{ + int VIP = 0; + + if(g_Config && g_Config.JumpToKey(map)) + { + VIP = g_Config.GetNum("VIP", VIP); + g_Config.Rewind(); + } + + return view_as(VIP); +} + +stock void InternalRestoreMapCooldowns() +{ + char sCooldownFile[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, sCooldownFile, sizeof(sCooldownFile), "configs/mapchooser_extended/cooldowns.cfg"); + + if(!FileExists(sCooldownFile)) + { + LogMessage("Could not find cooldown file: \"%s\"", sCooldownFile); + return; + } + + KeyValues Cooldowns = new KeyValues("mapchooser_extended"); + + if(!Cooldowns.ImportFromFile(sCooldownFile)) + { + LogMessage("Unable to load cooldown file: \"%s\"", sCooldownFile); + delete Cooldowns; + return; + } + + if(!Cooldowns.GotoFirstSubKey(true)) + { + LogMessage("Unable to goto first sub key: \"%s\"", sCooldownFile); + delete Cooldowns; + return; + } + + int Cooldown; + char map[PLATFORM_MAX_PATH]; + + do + { + if(!Cooldowns.GetSectionName(map, sizeof(map))) + { + LogMessage("Unable to get section name: \"%s\"", sCooldownFile); + delete Cooldowns; + return; + } + + if((Cooldown = Cooldowns.GetNum("Cooldown", -1)) > 0) + { + LogMessage("Restored cooldown: %s -> %d", map, Cooldown); + g_OldMapList.SetValue(map, Cooldown, true); + } + + if((Cooldown = Cooldowns.GetNum("CooldownTime", -1)) > 0) + { + LogMessage("Restored time cooldown: %s -> %d", map, Cooldown); + g_TimeMapList.SetValue(map, Cooldown, true); + } + } while(Cooldowns.GotoNextKey(true)); + + delete Cooldowns; +} + +stock void InternalStoreMapCooldowns() +{ + char sCooldownFile[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, sCooldownFile, sizeof(sCooldownFile), "configs/mapchooser_extended/cooldowns.cfg"); + + if(!FileExists(sCooldownFile)) + { + LogMessage("Could not find cooldown file: \"%s\"", sCooldownFile); + return; + } + + KeyValues Cooldowns = new KeyValues("mapchooser_extended"); + + int Cooldown; + char map[PLATFORM_MAX_PATH]; + + StringMapSnapshot OldMapListSnapshot = g_OldMapList.Snapshot(); + for(int i = 0; i < OldMapListSnapshot.Length; i++) + { + OldMapListSnapshot.GetKey(i, map, sizeof(map)); + g_OldMapList.GetValue(map, Cooldown); + + if (!Cooldowns.JumpToKey(map, true)) + { + LogMessage("Unable to create/find key: %s", map); + delete OldMapListSnapshot; + delete Cooldowns; + return; + } + + Cooldowns.SetNum("Cooldown", Cooldown); + Cooldowns.Rewind(); + } + delete OldMapListSnapshot; + + StringMapSnapshot TimeMapListSnapshot = g_TimeMapList.Snapshot(); + for(int i = 0; i < TimeMapListSnapshot.Length; i++) + { + TimeMapListSnapshot.GetKey(i, map, sizeof(map)); + g_TimeMapList.GetValue(map, Cooldown); + + if (!Cooldowns.JumpToKey(map, true)) + { + LogMessage("Unable to create/find key: %s", map); + delete TimeMapListSnapshot; + delete Cooldowns; + return; + } + + Cooldowns.SetNum("CooldownTime", Cooldown); + Cooldowns.Rewind(); + } + delete TimeMapListSnapshot; + + if(!Cooldowns.ExportToFile(sCooldownFile)) + { + LogMessage("Unable to export cooldown file: \"%s\"", sCooldownFile); + delete Cooldowns; + return; + } + + delete Cooldowns; +} + +stock int TimeStrToSeconds(const char[] str) +{ + int seconds = 0; + int maxlen = strlen(str); + for(int i = 0; i < maxlen;) + { + int val = 0; + i += StringToIntEx(str[i], val); + if(str[i] == 'h') + { + val *= 60; + i++; + } + seconds += val * 60; + } + return seconds; +}