/**
 * 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 <http://www.gnu.org/licenses/>.
 *
 * 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 <http://www.sourcemod.net/license.php>.
 *
 * 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 <sourcemod>
#include <mapchooser>
#include <mapchooser_extended>
#include <unloze_playtime>
#include <nextmap>
#include <sdktools>
#include <cstrike>
#include <multicolors>
#include <PlayerManager>
#include <nominations_extended>
#include <rockthevote_extended>
#include <AFKManager>
#include <leader>

#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;
bool g_SaveCDOnMapEnd = true;
int g_mapFileSerial = -1;
int g_iPlayerCount_excludeSpec;
int g_iPlayerAFKTime;


int g_NominateCount = 0;
int g_NominateReservedCount = 0;
int g_iInterval;
float player_mapvote_worth[MAXPLAYERS + 1];
char player_mapvote[MAXPLAYERS + 1][PLATFORM_MAX_PATH];
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;

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_PushCell(i);
            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);
        if (IsValidClient(i))
        {
            OnClientPostAdminCheck(i);
        }
    }

    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, 9.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);


    ConVar cvar1;
    HookConVarChange((cvar1 = CreateConVar("sm_active_players_required", "15", "Amount of players required to be considered active before any restrictions are enabled at all.")), Cvar_playerExcludeSpec);
    g_iPlayerCount_excludeSpec = cvar1.IntValue;
    delete cvar1;

    ConVar cvar;
    HookConVarChange((cvar = CreateConVar("sm_mapchooser_afk_time", "120", "Time in seconds until a player is considered as AFK and therefore excluded from player average")), Cvar_playerAFKTime);
    g_iPlayerAFKTime = cvar.IntValue;
    delete cvar;

    // 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 <map>");

    // 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);


    InternalRestoreMapCooldowns();
}

public void Cvar_playerExcludeSpec(ConVar convar, const char[] oldValue, const char[] newValue)
{
    g_iPlayerCount_excludeSpec = convar.IntValue;
}

public void Cvar_playerAFKTime(ConVar convar, const char[] oldValue, const char[] newValue)
{
    g_iPlayerAFKTime = convar.IntValue;
}

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()
{
    ServerCommand("sm_cvar sm_vote_progress_hintbox 1"); //yeah its cheesy but does the job.
    g_iInterval = 0;
    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();
    //this does not detect obvioulsy when there is more or less than 15 players active because its the mapstart
    // nobody has connected yet. OnMapEnd also does not work for checking this. so no point setting it to false here.
    if(InternalAreRestrictionsActive(true))
        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()
{
    ServerCommand("sm_cvar sm_vote_progress_hintbox 1"); //yeah its cheesy but does the job.
    g_iInterval = 0;
    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;


    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]);
    }
}

public void OnClientDisconnect(int client)
{
    is_bot_player[client] = false;
    player_mapvote_worth[client] = 0.0;
    Format(player_mapvote[client], 128, "");
    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_PushCell(client);
        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 <map>");
        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;
    //checking on MapStart and MapEnd is not good enough. Players are not considered alive and on teams at those points in time.
    //therefore instead applying the bool here after the mapvote completed.
    if(InternalAreRestrictionsActive(false))
        g_SaveCDOnMapEnd = true;
    else
        g_SaveCDOnMapEnd = false;
    return Plugin_Handled;
}

public Action Command_ReloadMaps(int client, int args)
{
	InitializeOfficialMapList();
	return Plugin_Handled;
}

public Action Command_hours_average(int client, int args)
{
    float total_votes = 0.0;
    for (int i = 0; i < MaxClients; i++)
    {
        if (IsValidClient(i) && !is_bot_player[i] && PM_IsPlayerSteam(i))
        {
            GetPlayerWorthRTV_(i);
            float nominate_worth = GetPlayerWorthRTV_boost_(i);
            if (nominate_worth < 1.0)
            {
                nominate_worth = 1.0;
            }
            total_votes += nominate_worth;
        }
    }
    float nominate_worth = GetPlayerWorthRTV_boost_(client);
    if (nominate_worth < 1.0)
    {
        nominate_worth = 1.0;
    }

    CReplyToCommand(client, "Average hour count for nominations is: %i \nAverage hour count for mapvote and rtv boost is: %i \nYour mapvote and rtv boost is %0.1f \n(%i%% mapvote)   (%i%% rtv)", 
        GetAveragePlayerTimeOnServer(),
        GetAveragePlayerTimeOnServerRTV(),
        nominate_worth,
        RoundToFloor((nominate_worth/total_votes) * 100),
        GetRtvPercentage(client));
    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))
    {
        //LogMessage("Mapchooser_extended_avg OnMapTimeLeftChanged call.");
        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 && !g_WaitingForVote)
        {
            SetupWarningTimer(WarningType_Vote);
            //LogMessage("Mapchooser_extended_avg SetupWarningTimer call 2");
        }
        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);
                //LogMessage("Mapchooser_extended_avg g_VoteTimer Timer_StartWarningTimer");
                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)
    {
        //LogMessage("Mapchooser_extended_avg SetupWarningTimer call 3");
        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;
        //LogMessage("Mapchooser_extended_avg Timer_Start MapVote Plugin_Stop");
        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<TimerLocation>(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);
            }
        }
    }

    //LogMessage("timePassed: %i \n warningMaxTime: %i", timePassed, warningMaxTime);
    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<MapChange>(ReadPackCell(data));
        Handle hndl = view_as<Handle>(ReadPackCell(data));
        //LogMessage("Mapchooser_extended_avg calling InitiateVote()");
        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);
                //LogMessage("Mapchooser_extended_avg SetupWarningTimer call 4");
            }
        }
    }
}

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);
                //LogMessage("Mapchooser_extended_avg SetupWarningTimer call 5");
            }
        }
    }
}

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);
            //LogMessage("Mapchooser_extended_avg SetupWarningTimer call 6");
        }
    }
    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);
    //LogMessage("Mapchooser_extended_avg SetupWarningTimer call 7");

    return Plugin_Handled;
}

public Handle get_most_nominated_maps(bool create_next_vote)
{
    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);
            int nominate_count_for_particular_map = 0;
            sm.GetValue(map_iteration, nominate_count_for_particular_map);

            //if i is 0 its admin nominated map that must come into the vote.
            if (!i)
            {
                nominate_count_for_particular_map = 1999;
            }
            else
            {
                int nominate_worth = RoundToFloor(GetPlayerWorthRTV_boost_(i));
                if (nominate_worth < 1)
                {
                    nominate_worth = 1;
                }
                nominate_count_for_particular_map += nominate_worth;
            }
            sm.SetValue(map_iteration, nominate_count_for_particular_map, true);

            if (!create_next_vote)
            {
                /* Notify Nominations that this map is now free */
                Call_StartForward(g_NominationsResetForward);
                Call_PushString(map_iteration);
                Call_PushCell(i + 100); //differentiate between all other calls and the call invoked by get_most_nominated_maps()
                Call_Finish();
            }
        }
    }
    static char map_[PLATFORM_MAX_PATH];

    StringMapSnapshot sm_snapshot = sm.Snapshot();
    ArrayList SortedList = CreateArray(arraySize);

    for(int i = 0; i < sm_snapshot.Length; i++)
    {
        sm_snapshot.GetKey(i, map_, sizeof(map_));
        SortedList.PushString(map_);
    }

    SortedList.SortCustom(NominateListSortCmp, sm);

    for(int i = 0; i < GetArraySize(SortedList); i++)
    {
        char picked_map[MAX_NAME_LENGTH];
        GetArrayString(SortedList, i, picked_map, sizeof(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);
    }

    delete sm;
    return most_nominated_maps;
}

int NominateListSortCmp(int index1, int index2, Handle array, Handle hndl)
{
	char map1[PLATFORM_MAX_PATH];
	char map2[PLATFORM_MAX_PATH];
	GetArrayString(array, index1, map1, sizeof(map1));
	GetArrayString(array, index2, map2, sizeof(map2));

	int count1, count2;
	
	StringMap sm = view_as<StringMap>(hndl);
	
	sm.GetValue(map1, count1);
	sm.GetValue(map2, count2);

	if (count1 == count2)
		return 0;

	return count1 > count2 ? -1 : 1;
}

/**
 * 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);
        //LogMessage("Mapchooser_extended_avg: called Timer_StartMapVote");

        /* 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<int>(when));
        WritePackCell(data, view_as<int>(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)
    {
        //LogMessage("Mapchooser_extended_avg ended here: g_MapVoteCompleted: %i \n g_ChangeMapInProgress: %i", g_MapVoteCompleted, g_ChangeMapInProgress);
        return;
    }

    CreateNextVote();

    g_ChangeTime = when;

    ServerCommand("sm_cvar sm_vote_progress_hintbox 0"); //yeah its cheesy but does the job.
    g_iInterval = GetConVarInt(g_Cvar_VoteDuration);
    CreateTimer(1.0, Timer_Countdown, _, TIMER_REPEAT);

    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(false);

        int voteSize = GetVoteSize(0); //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);
        }


        /* 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(0) <= 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);
    //2023 excluding nosteamers and autismbots.

    int clients[MAXPLAYERS + 1];
    int count = 0;
    //here we pick clients to show the vote to, we dont show it to autismbots and nosteamers.
    for (int i = 0; i < MaxClients; i++)
    {
        player_mapvote_worth[i] = 0.0;
        Format(player_mapvote[i], 128, "");
        if (IsValidClient(i) && PM_IsPlayerSteam(i) && !is_bot_player[i])
        {
            GetPlayerWorthRTV_(i);
            float nominate_worth = GetPlayerWorthRTV_boost_(i);
            if (nominate_worth < 1.0)
            {
                nominate_worth = 1.0;
            }
            player_mapvote_worth[i] = nominate_worth;
            clients[count] = i;
            count++;
        }
    }
    VoteMenu(g_VoteMenu, clients, sizeof(clients), 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 Action Timer_Countdown(Handle timer)
{
    if (g_iInterval <= 0)
        return Plugin_Stop;

    float most_voted[3];
    char most_voted_map[3][PLATFORM_MAX_PATH];

    for (int j = 0; j < 3; j++)
    {
        float max_count = 0.0;
        StringMap sm = new StringMap();
        char picked_map[PLATFORM_MAX_PATH];
        for (int i = 0; i < MaxClients; i++)
        {
            if (IsValidClient(i) && !StrEqual(player_mapvote[i], "") && !is_bot_player[i] && PM_IsPlayerSteam(i))
            {
                if (StrEqual(player_mapvote[i], most_voted_map[0]) || StrEqual(player_mapvote[i], most_voted_map[1]) || 
                    StrEqual(player_mapvote[i], most_voted_map[2]))
                {
                    continue;
                }
                float value = 0.0;
                sm.GetValue(player_mapvote[i], value);
                value += player_mapvote_worth[i];
                sm.SetValue(player_mapvote[i], value, true);
                if (value >= max_count)
                {
                    max_count = value;
                    strcopy(picked_map, sizeof(picked_map), player_mapvote[i]);
                }
            }
        }
        most_voted[j] = max_count;
        Format(most_voted_map[j], PLATFORM_MAX_PATH, picked_map);
        delete sm;
    }

    float total_votes = 0.0;
    float voted_so_far = 0.0;
    for (int i = 0; i < MaxClients; i++)
    {
        if (IsValidClient(i) && !is_bot_player[i] && PM_IsPlayerSteam(i))
        {
            total_votes += player_mapvote_worth[i];
            //LogMessage("client: %N player_mapvote_worth i: %f", i, player_mapvote_worth[i]);
            if (!StrEqual(player_mapvote[i], ""))
            {
                voted_so_far += player_mapvote_worth[i];
            }
        }
    }
    //why on earth is %%%s needed for formatting it to show a % lmao. just %s, "%" or %% on their own dont work.
    if (strlen(most_voted_map[2]) > 0)
    {
        //displaying 3 most voted maps
        PrintHintTextToAll("Votes: %i/100%%%s, %ds left\n1. %s: (%i%%%s)\n2. %s: (%i%%%s)\n3. %s: (%i%%%s)", RoundToFloor((voted_so_far/total_votes) * 100), 
        "%", g_iInterval, most_voted_map[0], RoundToFloor((most_voted[0]/total_votes) * 100), "%", most_voted_map[1], RoundToFloor((most_voted[1]/total_votes) * 100), 
        "%", most_voted_map[2], RoundToFloor((most_voted[2]/total_votes) * 100), "%");
    }
    else if (strlen(most_voted_map[1]) > 0)
    {
        //displaying 2 most voted maps
        PrintHintTextToAll("Votes: %i/100%%%s, %ds left\n1. %s: (%i%%%s)\n2. %s: (%i%%%s)", RoundToFloor((voted_so_far/total_votes) * 100), "%", g_iInterval, 
        most_voted_map[0], RoundToFloor((most_voted[0]/total_votes) * 100), "%", most_voted_map[1], RoundToFloor((most_voted[1]/total_votes) * 100), "%");
    }
    else if (strlen(most_voted_map[0]) > 0)
    {
        //displaying the most voted map
        PrintHintTextToAll("Votes: %i/100%%%s, %ds left\n1. %s: (%i%%%s)", RoundToFloor((voted_so_far/total_votes) * 100), 
        "%", g_iInterval, most_voted_map[0], RoundToFloor((most_voted[0]/total_votes) * 100), "%");
    }
    else
    {
        //displaying just the votecount and vote duration remaining
        PrintHintTextToAll("Votes: %i/100%%%s, %ds left", RoundToFloor((voted_so_far/total_votes) * 100), "%", g_iInterval);
    }
    g_iInterval--;
    return Plugin_Continue;
}

public void Handler_VoteFinishedGeneric(char[] map,
										float num_votes,
										float map_votes)
{
    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));

        PrintToChatAll("The current map has been extended. (Received %i%s of votes)", RoundToFloor((map_votes /num_votes)*100.0), "%");
        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)
    {
        PrintToChatAll("Current map continues! The Vote has spoken! (Received %i%s of votes)", RoundToFloor((map_votes /num_votes)*100.0), "%");
        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;
        //checking on MapStart and MapEnd is not good enough. Players are not considered alive and on teams at those points in time.
        //therefore instead applying the bool here after the mapvote completed.
        if(InternalAreRestrictionsActive(false))
            g_SaveCDOnMapEnd = true;
        else
            g_SaveCDOnMapEnd = false;

        //PrintToChatAll("map: %s", map);
        CPrintToChatAll("[MCE] %t", "Nextmap Voting Finished", map, RoundToFloor((map_votes /num_votes)*100.0));
        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)
{
    g_iInterval = 1; //we display it shortly after finishing
    ServerCommand("sm_cvar sm_vote_progress_hintbox 1"); //yeah its cheesy but does the job.
    //reweighting votes
    StringMap sm = new StringMap();
    for (int i = 0; i < num_clients; i++)
    {
        int client = client_info[i][0];
        if (IsValidClient(client) && !StrEqual(player_mapvote[client], ""))
        {
            //default intention is 1-5 votes, just like rtv.
            int item_index = client_info[i][VOTEINFO_CLIENT_ITEM];
            static char map[PLATFORM_MAX_PATH];
            for(int j = 0; j < num_items; j++)
            {
                if (item_info[j][VOTEINFO_ITEM_INDEX] == item_index)
                {
                    GetMapItem(menu, item_info[j][VOTEINFO_ITEM_INDEX], map, PLATFORM_MAX_PATH);
                    break;
                }
            }
            float value = 0.0;
            sm.GetValue(map, value);

            float nominate_worth = GetPlayerWorthRTV_boost_(client);
            if (nominate_worth < 1.0)
            {
                nominate_worth = 1.0;
            }
            value += nominate_worth;
            sm.SetValue(map, value, true);
        }
    }

    //ordering stringmap by voteweight
    float[] weighted_votes = new float[num_items];
    char[][] weighted_maps = new char[num_items][PLATFORM_MAX_PATH];
    for (int i = 0; i < num_items; i++)
    {
        StringMapSnapshot keys = sm.Snapshot();
        float max_count = 0.0;
        char picked_map[PLATFORM_MAX_PATH];
        for (int j = 0; j < keys.Length; j++) 
        {
            int size = keys.KeyBufferSize(j);
            char[] buffer = new char[size];
            keys.GetKey(j, buffer, size);
            float value = 0.0;
            sm.GetValue(buffer, value);

            //first selection has most votes, second selection second most etc etc
            if (value >= max_count)
            {
                max_count = value;
                strcopy(picked_map, sizeof(picked_map), buffer);
            }
        }
        sm.Remove(picked_map);
        weighted_votes[i] = max_count;
        Format(weighted_maps[i], 128, picked_map);
        delete keys;
    }
    delete sm;

    float total_votes = 0.0;
    for (int i = 0; i < MaxClients; i++)
    {
        if (IsValidClient(i) && !is_bot_player[i] && PM_IsPlayerSteam(i))
        {
            total_votes += player_mapvote_worth[i];
        }
    }
    // Implement revote logic - Only run this` block if revotes are enabled and this isn't the last revote'
    //LogMessage("Mapchooser_extended_avg Handler_MapVoteFinished.");
    int required_percent = GetConVarInt(g_Cvar_RunOffPercent);
    int most_voted_map_percentage = RoundToFloor((weighted_votes[0] / total_votes) * 100);
    if(GetConVarBool(g_Cvar_RunOff) && g_RunoffCount < GetConVarInt(g_Cvar_MaxRunOffs) && num_items > 1 &&
        (weighted_votes[0] == weighted_votes[1] || most_voted_map_percentage < required_percent))
    {
        //LogMessage("Mapchooser_extended_avg Handler_MapVoteFinished passed check1.");
        g_RunoffCount++;
        if(weighted_votes[0] == weighted_votes[1])
        {
            g_HasVoteStarted = false;

            //Revote is needed
            int arraySize = ByteCountToCells(PLATFORM_MAX_PATH);
            Handle mapList = CreateArray(arraySize);

            //adding all maps with same amount of votes
            for(int i = 0; i < num_items; i++)
            {
                if(weighted_votes[i] == weighted_votes[0])
                {
                    PushArrayString(mapList, weighted_maps[i]);
                }
                else
                    break;
            }

            CPrintToChatAll("[MCE] %t", "Tie Vote", GetArraySize(mapList));
            //LogMessage("Mapchooser_extended_avg reached WarningType_Revote 1");
            SetupWarningTimer(WarningType_Revote, view_as<MapChange>(g_ChangeTime), mapList);
            return;
        }
        else if(most_voted_map_percentage < required_percent)
        {
            g_HasVoteStarted = false;

            //Revote is needed
            int arraySize = ByteCountToCells(PLATFORM_MAX_PATH);
            Handle mapList = CreateArray(arraySize);

            PushArrayString(mapList, weighted_maps[0]);
            // 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 || weighted_votes[i] == weighted_votes[i - 1])
                {
                    PushArrayString(mapList, weighted_maps[i]);
                }
                else
                    break;
            }

            CPrintToChatAll("[MCE] %t", "Revote Is Needed", required_percent);
            //LogMessage("Mapchooser_extended_avg reached WarningType_Revote 2");
            SetupWarningTimer(WarningType_Revote, view_as<MapChange>(g_ChangeTime), mapList);
            return;
        }
    }

    g_WaitingForVote = false;
    //LogMessage("Mapchooser_extended_avg no revote needed, continue as normal.");
    // No revote needed, continue as normal.
    Handler_VoteFinishedGeneric(weighted_maps[0], total_votes, weighted_votes[0]);
}

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_Select:
        {
            char map[PLATFORM_MAX_PATH];
            GetMenuItem(menu, param2, map, PLATFORM_MAX_PATH, _, _, _, param1);
            if (StrEqual(map, "##dontchange##"))
            {
                Format(map, sizeof(map), "Dont change");
            }
            else if (StrEqual(map, "##extend##"))
            {
                Format(map, sizeof(map), "Extend");
            }
            Format(player_mapvote[param1], 128, map);
        }
        case MenuAction_Display:
        {
            Format(player_mapvote[param1], 128, "");
            static char buffer[255];

            //displaying to the client how much percent his vote is worth.
            float total_votes = 0.0;
            for (int i = 0; i < MaxClients; i++)
            {
                if (IsValidClient(i) && !is_bot_player[i] && PM_IsPlayerSteam(i))
                {
                    total_votes += player_mapvote_worth[i];
                }
            }
            //second parameter shows for example 5.0, last parameter is how much percentage of the entire vote the client decides.
            Format(buffer, sizeof(buffer), "%T", "Vote Nextmap", param1, player_mapvote_worth[param1], RoundToFloor((player_mapvote_worth[param1]/total_votes) * 100));
            Handle panel = view_as<Handle>(param2);
            SetPanelTitle(panel, buffer);
            //char PannelText[256] = "Warning: The Position of the Maps are different for each Player.";
            //DrawPanelText(panel, PannelText);
        }

        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_WaitingForVote = false;
            g_HasVoteStarted = false;
            g_RunoffCount = 0;
            g_iInterval = 1; //we display it shortly after finishing
            ServerCommand("sm_cvar sm_vote_progress_hintbox 1"); //yeah its cheesy but does the job.
        }
    }
    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(false))
    {
        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(false))
    {
        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(0);
    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(true);
    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(false))
                break;

            if(InternalGetMapVIPRestriction(map))
                continue;

            if(InternalGetMapCooldownTime(map) > GetTime())
                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_PushCell(owner);
        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(0))
        g_NominateReservedCount++;
    else
        g_NominateCount++;

    while(GetArraySize(g_NominateList[owner]) > GetVoteSize(0)) 
    {
        char oldmap[PLATFORM_MAX_PATH];
        GetArrayString(g_NominateList[owner], 0, oldmap, PLATFORM_MAX_PATH);
        Call_StartForward(g_NominationsResetForward);
        Call_PushString(oldmap);
        Call_PushCell(owner);
        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<int>(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_PushCell(client);
                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<int>(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_PushCell(owner);
        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<int>(InternalRemoveNominationByOwner(GetNativeCell(1)));
}

/* native InitiateMapChooserVote(); */
public int Native_InitiateVote(Handle plugin, int numParams)
{
    MapChange when = view_as<MapChange>(GetNativeCell(1));
    Handle inputarray = view_as<Handle>(GetNativeCell(2));

    LogAction(-1, -1, "Starting map vote because outside request");

    SetupWarningTimer(WarningType_Vote, when, inputarray);
    //LogMessage("Mapchooser_extended_avg SetupWarningTimer call 1");
    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<Handle>(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<Handle>(GetNativeCell(1));
    Handle ownerarray = view_as<Handle>(GetNativeCell(2));

    if(maparray == INVALID_HANDLE)
        return 0;

    static char map[PLATFORM_MAX_PATH];
    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");
            //LogMessage("Mapchooser_extended_avg WarningType_Vote");
        }

        case WarningType_Revote:
        {
            forwardVote = g_MapVoteRunoffStartForward;
            cvarTime = g_Cvar_RunOffWarningTime;
            strcopy(translationKey, sizeof(translationKey), "Revote Warning");
            //LogMessage("Mapchooser_extended_avg WarningType_Revote. cvarTime: %i", GetConVarInt(cvarTime));
        }
    }

    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<int>(when));
    WritePackCell(data, view_as<int>(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<int>(CanNominate_No_VoteInProgress);
	}

	if(g_MapVoteCompleted)
	{
		return view_as<int>(CanNominate_No_VoteComplete);
	}

	if(g_NominateCount >= GetVoteSize())
	{
		return view_as<int>(CanNominate_No_VoteFull);
	}

	return view_as<int>(CanNominate_Yes);
}

public int Native_ExcludeMap(Handle plugin, int numParams)
{
	if(!InternalAreRestrictionsActive(false))
		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(false))
		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(false))
		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(false))
		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(false);
}

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;
}

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;
    player_mapvote_worth[client] = 0.0;
    Format(player_mapvote[client], 128, "");
    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;
}

/*
false means we never put the map on cooldown if time range is inside the no map restriction time or 
if less than the minimum active players for enabling map restrictions are on the server.

true means we put maps on Cooldown even if there was no map restrictions because of less than 15 active players (the number is a cvar, its not static)
true still respects the time range with no map restrictions (23:30 to 10:30 right now) and wont apply cooldown there.
*/
stock bool InternalAreRestrictionsActive(bool skip_player_check)
{
    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;
    }

    int ActivePlayerCount = 0;
    for (int i = 0; i < MaxClients; i++)
    {
        if (IsValidClient(i) && !IsFakeClient(i) && !IsClientSourceTV(i) && !is_bot_player[i] && GetClientIdleTime(i) < g_iPlayerAFKTime
            && (GetClientTeam(i) == CS_TEAM_T || GetClientTeam(i) == CS_TEAM_CT))
        {
            ActivePlayerCount++;
        }
    }

    if (ActivePlayerCount <= g_iPlayerCount_excludeSpec && !skip_player_check)
    {
        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<bool>(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;
}

stock bool IsValidClient(int client)
{
    if (client > 0 && client <= MaxClients && IsClientConnected(client) && IsClientInGame(client) && !IsFakeClient(client) && !IsClientSourceTV(client))
        return true;
    return false;
}
