Tweak alert display and delay

Tweak alert display and delay

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio_karaoke
 * Karaoke player for IGA
 *
 * Copyright 2013 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <sdktools>
#include <in_game_audio>
#include <morecolors>

#define PLUGIN_VERSION "1.8.1"
#define PLUGIN_NAME "In Game Audio Karaoke"

#define MAX_KARAOKE_SONGS    64
#define MAX_LRC_LINE_LENGTH  256
#define MAX_KARAOKE_LYRICS   1024

#define SOUND_ATTENTION "vo/announcer_attention.mp3"

public Plugin:myinfo =
{
    name = PLUGIN_NAME,
    author = "CrimsonTautology",
    description = "Karaoke player for IGA",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

//Parallel arrays to store karaoke songs
new String:g_KaraokeName[MAX_KARAOKE_SONGS][PLATFORM_MAX_PATH];
new String:g_KaraokeLRCPath[MAX_KARAOKE_SONGS][PLATFORM_MAX_PATH];
new g_KaraokeSongId[MAX_KARAOKE_SONGS];
new g_KaraokeSongCount = 0;

new String:g_KaraokeLyrics[MAX_KARAOKE_LYRICS][MAX_LRC_LINE_LENGTH];
new Float:g_KaraokeTimestamps[MAX_KARAOKE_LYRICS];

public OnPluginStart()
{
    LoadTranslations("in_game_audio.phrases");
    RegAdminCmd("sm_karaoke", Command_Karaoke, ADMFLAG_SLAY, "[ADMIN] Start a karaoke.");

    CreateConVar("sm_iga_karaoke_version", PLUGIN_VERSION, PLUGIN_NAME, FCVAR_PLUGIN | FCVAR_SPONLY | FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DONTRECORD);
}

public OnMapStart()
{
    ReadKaraokeSongs();

    PrecacheSound(SOUND_ATTENTION);
    PrecacheSound("vo/announcer_begins_1sec.mp3");
    PrecacheSound("vo/announcer_begins_2sec.mp3");
    PrecacheSound("vo/announcer_begins_3sec.mp3");
    PrecacheSound("vo/announcer_begins_4sec.mp3");
    PrecacheSound("vo/announcer_begins_5sec.mp3");
}

public Action:Command_Karaoke(client, args)
{
    if(!IsIGAEnabled())
    {
        CReplyToCommand(client, "%t", "not_enabled");
        return Plugin_Handled;
    }

    if(IsInPall())
    {
        CReplyToCommand(client, "%t", "pall_currently_in_use");
        return Plugin_Handled;
    }

    KaraokeMenu(client);

    return Plugin_Handled;
}

public Action:Timer_DisplayLyric(Handle:timer, any:index)
{
    PrintCenterTextAll("%s", g_KaraokeLyrics[index]);
}

public Action:Timer_StartSong(Handle:timer, any:song_id)
{
    QuerySong(0, "", true, true, song_id);
}

public Action:Timer_CountDown(Handle:timer, any:second)
{
    decl String:sound[PLATFORM_MAX_PATH];
    FormatEx(sound, sizeof(sound), "vo/announcer_begins_%dsec.mp3", second);
    EmitSoundToAll(sound);
}

ReadKaraokeSongs()
{
    g_KaraokeSongCount = 0;

    new Handle:kv = CreateKeyValues("Karaoke");

    decl String:path[PLATFORM_MAX_PATH], String:tmp[PLATFORM_MAX_PATH], String:game_folder[PLATFORM_MAX_PATH];
    BuildPath(Path_SM, path, sizeof(path), "configs/iga.karaoke.cfg");//TODO make this path a cvar

    if(FileExists(path))
    {
        FileToKeyValues(kv, path);
        KvGotoFirstSubKey(kv);

        do
        {
            KvGetSectionName(kv, tmp, sizeof(tmp));
            if(StrEqual(tmp, "Song") )
            {
                KvGetKaraokeSong(kv, g_KaraokeSongCount);
            }

        } while(KvGotoNextKey(kv) && g_KaraokeSongCount < MAX_KARAOKE_SONGS);

    } else {
        LogError("File Not Found: %s", path);
    }

    CloseHandle(kv);
}

KvGetKaraokeSong(Handle:kv, &index)
{
    KvGetString(kv, "name", g_KaraokeName[index], PLATFORM_MAX_PATH);
    KvGetString(kv, "lrc_file", g_KaraokeLRCPath[index], PLATFORM_MAX_PATH);
    g_KaraokeSongId[index] = KvGetNum(kv, "song_id", 0);

    PrintToConsole(0, "---%s %s %d", g_KaraokeName[index], g_KaraokeLRCPath[index], g_KaraokeSongId[index]);//TODO

    index++;
}

ParseLRCFile(const String:file_name[], String:lyrics[][], Float:timestamps[])
{
    decl String:path[PLATFORM_MAX_PATH];
    new total_lyrics = 0;
    BuildPath(Path_SM, path, sizeof(path), "data/karaoke/%s", file_name);

    if(FileExists(path))
    {
        new Handle:file = OpenFile(path, "r");

        //Line by line reading
        decl String:line[MAX_LRC_LINE_LENGTH], index, split, Float:timestamp;
        while (total_lyrics < MAX_KARAOKE_LYRICS && !IsEndOfFile(file) && ReadFileLine(file, line, sizeof(line)))
        {
            //Skip non timestamp lines
            if(SimpleRegexMatch(line, "\\[[0-9]{2}\\:[0-6][0-9](\\.[0-6][0-9])?\\]([\\w:\\s]+)") <= 0) continue;

            //Strip linefeeds and returns
            ReplaceString(line, sizeof(line), "\r", "");
            ReplaceString(line, sizeof(line), "\n", "");
            ReplaceString(line, sizeof(line), "\xEF\xBB\xBF", ""); // UTF-8 "BOM"

            //Parse out timestamp and lyric
            index = FindCharInString(line, ']');
            if(index <= 0) continue; //Skip invalid line

            line[index] = '\0';
            split = FindCharInString(line, ':');

            g_KaraokeTimestamps[total_lyrics] = StringToInt(line[1]) * 60 + StringToFloat(line[split + 1]);
            strcopy(g_KaraokeLyrics[total_lyrics], MAX_LRC_LINE_LENGTH, line[index + 1]);

            total_lyrics++;
        }

    } else {
        LogError("Karaoke Lyrics File Not Found: %s", path);
    }

    return total_lyrics;
}

StartKaraoke(selected, Float:delay)
{
    if(IsInPall()) return;

    new total, i;
    total = ParseLRCFile(g_KaraokeLRCPath[selected], g_KaraokeLyrics, g_KaraokeTimestamps);//TODO

    //Build timers to display lyrics for each parsed lyric
    for(i=0; i < total; i++)
    {
        CreateTimer(g_KaraokeTimestamps[i] + delay, Timer_DisplayLyric, i, TIMER_FLAG_NO_MAPCHANGE);
    }

    EmitSoundToAll(SOUND_ATTENTION);
    PrintCenterTextAll("%s\nKaraoke Will Begin in 15 Seconds\nAdjust your volume now if needed!", g_KaraokeName[selected]);

    CreateTimer(delay - 5.0, Timer_CountDown, 5);
    CreateTimer(delay - 4.0, Timer_CountDown, 4);
    CreateTimer(delay - 3.0, Timer_CountDown, 3);
    CreateTimer(delay - 2.0, Timer_CountDown, 2);
    CreateTimer(delay - 1.0, Timer_CountDown, 1);

    PrintToConsole(0, "%s %s %d", g_KaraokeName[selected], g_KaraokeLRCPath[selected], g_KaraokeSongId[selected]);//TODO

    CreateTimer(delay, Timer_StartSong, g_KaraokeSongId[selected]);
}

KaraokeMenu(client)
{
    new Handle:menu = CreateMenu(KaraokeMenuHandler);

    SetMenuTitle(menu, "Choose Karaoke Song");

    decl String:buf[16];
    for(new i=0; i < g_KaraokeSongCount && i < MAX_KARAOKE_SONGS; i++)
    {
        IntToString(i, buf, sizeof(buf));
        AddMenuItem(menu,
                buf,
                g_KaraokeName[i],
                ITEMDRAW_DEFAULT);
    }

    DisplayMenu(menu, client, 20);
}

public KaraokeMenuHandler(Handle:menu, MenuAction:action, param1, param2)
{
    switch (action)
    {
        case MenuAction_Select:
            {
                new client = param1;
                new String:info[32];
                GetMenuItem(menu, param2, info, sizeof(info));
                new selected = StringToInt(info);

                StartKaraoke(selected, 15.0);
            }
        case MenuAction_End: CloseHandle(menu);
    }
}

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio_karaoke
 * Karaoke player for IGA
 *
 * Copyright 2013 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <sdktools>
#include <in_game_audio>
#include <morecolors>

#define PLUGIN_VERSION "1.8.1"
#define PLUGIN_NAME "In Game Audio Karaoke"

#define MAX_KARAOKE_SONGS    64
#define MAX_LRC_LINE_LENGTH  256
#define MAX_KARAOKE_LYRICS   1024

#define SOUND_ATTENTION "vo/announcer_attention.mp3"

public Plugin:myinfo =
{
    name = PLUGIN_NAME,
    author = "CrimsonTautology",
    description = "Karaoke player for IGA",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

//Parallel arrays to store karaoke songs
new String:g_KaraokeName[MAX_KARAOKE_SONGS][PLATFORM_MAX_PATH];
new String:g_KaraokeLRCPath[MAX_KARAOKE_SONGS][PLATFORM_MAX_PATH];
new g_KaraokeSongId[MAX_KARAOKE_SONGS];
new g_KaraokeSongCount = 0;

new String:g_KaraokeLyrics[MAX_KARAOKE_LYRICS][MAX_LRC_LINE_LENGTH];
new Float:g_KaraokeTimestamps[MAX_KARAOKE_LYRICS];

public OnPluginStart()
{
    LoadTranslations("in_game_audio.phrases");
    RegAdminCmd("sm_karaoke", Command_Karaoke, ADMFLAG_SLAY, "[ADMIN] Start a karaoke.");

    CreateConVar("sm_iga_karaoke_version", PLUGIN_VERSION, PLUGIN_NAME, FCVAR_PLUGIN | FCVAR_SPONLY | FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DONTRECORD);
}

public OnMapStart()
{
    ReadKaraokeSongs();

    PrecacheSound(SOUND_ATTENTION);
    PrecacheSound("vo/announcer_begins_1sec.mp3");
    PrecacheSound("vo/announcer_begins_2sec.mp3");
    PrecacheSound("vo/announcer_begins_3sec.mp3");
    PrecacheSound("vo/announcer_begins_4sec.mp3");
    PrecacheSound("vo/announcer_begins_5sec.mp3");
}

public Action:Command_Karaoke(client, args)
{
    if(!IsIGAEnabled())
    {
        CReplyToCommand(client, "%t", "not_enabled");
        return Plugin_Handled;
    }

    if(IsInPall())
    {
        CReplyToCommand(client, "%t", "pall_currently_in_use");
        return Plugin_Handled;
    }

    KaraokeMenu(client);

    return Plugin_Handled;
}

public Action:Timer_DisplayLyric(Handle:timer, any:index)
{
    PrintCenterTextAll("%s", g_KaraokeLyrics[index]);
}

public Action:Timer_StartSong(Handle:timer, any:song_id)
{
    QuerySong(0, "", true, true, song_id);
}

public Action:Timer_CountDown(Handle:timer, any:second)
{
    decl String:sound[PLATFORM_MAX_PATH];
    FormatEx(sound, sizeof(sound), "vo/announcer_begins_%dsec.mp3", second);
    EmitSoundToAll(sound);
}

ReadKaraokeSongs()
{
    g_KaraokeSongCount = 0;

    new Handle:kv = CreateKeyValues("Karaoke");

    decl String:path[PLATFORM_MAX_PATH], String:tmp[PLATFORM_MAX_PATH], String:game_folder[PLATFORM_MAX_PATH];
    BuildPath(Path_SM, path, sizeof(path), "configs/iga.karaoke.cfg");//TODO make this path a cvar

    if(FileExists(path))
    {
        FileToKeyValues(kv, path);
        KvGotoFirstSubKey(kv);

        do
        {
            KvGetSectionName(kv, tmp, sizeof(tmp));
            if(StrEqual(tmp, "Song") )
            {
                KvGetKaraokeSong(kv, g_KaraokeSongCount);
            }

        } while(KvGotoNextKey(kv) && g_KaraokeSongCount < MAX_KARAOKE_SONGS);

    } else {
        LogError("File Not Found: %s", path);
    }

    CloseHandle(kv);
}

KvGetKaraokeSong(Handle:kv, &index)
{
    KvGetString(kv, "name", g_KaraokeName[index], PLATFORM_MAX_PATH);
    KvGetString(kv, "lrc_file", g_KaraokeLRCPath[index], PLATFORM_MAX_PATH);
    g_KaraokeSongId[index] = KvGetNum(kv, "song_id", 0);

    PrintToConsole(0, "---%s %s %d", g_KaraokeName[index], g_KaraokeLRCPath[index], g_KaraokeSongId[index]);//TODO

    index++;
}

ParseLRCFile(const String:file_name[], String:lyrics[][], Float:timestamps[])
{
    decl String:path[PLATFORM_MAX_PATH];
    new total_lyrics = 0;
    BuildPath(Path_SM, path, sizeof(path), "data/karaoke/%s", file_name);

    if(FileExists(path))
    {
        new Handle:file = OpenFile(path, "r");

        //Line by line reading
        decl String:line[MAX_LRC_LINE_LENGTH], index, split, Float:timestamp;
        while (total_lyrics < MAX_KARAOKE_LYRICS && !IsEndOfFile(file) && ReadFileLine(file, line, sizeof(line)))
        {
            //Skip non timestamp lines
            if(SimpleRegexMatch(line, "\\[[0-9]{2}\\:[0-6][0-9](\\.[0-6][0-9])?\\]([\\w:\\s]+)") <= 0) continue;

            //Strip linefeeds and returns
            ReplaceString(line, sizeof(line), "\r", "");
            ReplaceString(line, sizeof(line), "\n", "");
            ReplaceString(line, sizeof(line), "\xEF\xBB\xBF", ""); // UTF-8 "BOM"

            //Parse out timestamp and lyric
            index = FindCharInString(line, ']');
            if(index <= 0) continue; //Skip invalid line

            line[index] = '\0';
            split = FindCharInString(line, ':');

            g_KaraokeTimestamps[total_lyrics] = StringToInt(line[1]) * 60 + StringToFloat(line[split + 1]);
            strcopy(g_KaraokeLyrics[total_lyrics], MAX_LRC_LINE_LENGTH, line[index + 1]);

            total_lyrics++;
        }

    } else {
        LogError("Karaoke Lyrics File Not Found: %s", path);
    }

    return total_lyrics;
}

StartKaraoke(selected, Float:delay)
{
    if(IsInPall()) return;

    new total, i;
    total = ParseLRCFile(g_KaraokeLRCPath[selected], g_KaraokeLyrics, g_KaraokeTimestamps);//TODO

    //Build timers to display lyrics for each parsed lyric
    for(i=0; i < total; i++)
    {
        CreateTimer(g_KaraokeTimestamps[i] + delay, Timer_DisplayLyric, i, TIMER_FLAG_NO_MAPCHANGE);
    }

    EmitSoundToAll(SOUND_ATTENTION);
    PrintCenterTextAll("Karaoke started; \"%s\"", g_KaraokeName[selected]);

    CreateTimer(delay - 5.0, Timer_CountDown, 5);
    CreateTimer(delay - 4.0, Timer_CountDown, 4);
    CreateTimer(delay - 3.0, Timer_CountDown, 3);
    CreateTimer(delay - 2.0, Timer_CountDown, 2);
    CreateTimer(delay - 1.0, Timer_CountDown, 1);

    PrintToConsole(0, "%s %s %d", g_KaraokeName[selected], g_KaraokeLRCPath[selected], g_KaraokeSongId[selected]);//TODO

    CreateTimer(delay, Timer_StartSong, g_KaraokeSongId[selected]);
}

KaraokeMenu(client)
{
    new Handle:menu = CreateMenu(KaraokeMenuHandler);

    SetMenuTitle(menu, "Choose Karaoke Song");

    decl String:buf[16];
    for(new i=0; i < g_KaraokeSongCount && i < MAX_KARAOKE_SONGS; i++)
    {
        IntToString(i, buf, sizeof(buf));
        AddMenuItem(menu,
                buf,
                g_KaraokeName[i],
                ITEMDRAW_DEFAULT);
    }

    DisplayMenu(menu, client, 20);
}

public KaraokeMenuHandler(Handle:menu, MenuAction:action, param1, param2)
{
    switch (action)
    {
        case MenuAction_Select:
            {
                new client = param1;
                new String:info[32];
                GetMenuItem(menu, param2, info, sizeof(info));
                new selected = StringToInt(info);

                StartKaraoke(selected, 10.0);
            }
        case MenuAction_End: CloseHandle(menu);
    }
}


More work

More work

/*
 * Copyright (c) 2016 Alex "ZeroKnight" George
 *
 * This file is licensed under the MIT License. For details, please see the
 * LICENSE file that should be included with the source code, or
 * <https://opensource.org/licenses/MIT>
 */

#include <sourcemod>

#pragma semicolon 1
#pragma newdecls required

char PLUGIN_NAME[]    = "Multi-Giveaway";
char PLUGIN_AUTHOR[]  = "Alex \"ZeroKnight\" George";
char PLUGIN_DESC[]    = "Expansive Giveaway system with numerous types of Giveaway events and stats";
char PLUGIN_VERSION[] = "0.1.0";
char PLUGIN_URL[]     = "http:/github.com/ZeroKnight/sm-Multi-Giveaway";
char PLUGIN_TAG[]     = "[MG]";

public Plugin myinfo =
{
  name        = PLUGIN_NAME,
  author      = PLUGIN_AUTHOR,
  description = PLUGIN_DESC,
  version     = PLUGIN_VERSION,
  url         = PLUGIN_URL
};

// NOTE: (<<= 1) is actually a neat thing that SourcePawn does. It lets you
// specify how to 'increment' each enumeration. In this case, each one will be
// the previous value left shifted 1, which gives us our bit-field.
enum GiveawayType (<<= 1)
{
  GT_Invalid = -1, GT_Dice = 1, GT_Number, GT_Kill, GT_All = 7
}

//File configfile = ...

// TODO: Put things like this in a personal core library?
#define CVAR(%1) ConVar cv_%1
char CVAR_PREFIX[] = "multi_giveaway";
CVAR(version);
CVAR(flags);
CVAR(enabled_types);
CVAR(player_state);
CVAR(min_conn_time);
CVAR(dice_min);
CVAR(dice_max);
CVAR(dice_show_rolls);
CVAR(dice_rerolls);
CVAR(number_min);
CVAR(number_max);
CVAR(number_show_guesses);

void RegisterConVars()
{
  // XXX: Function overloads would be nice. Or casting that worked on strings.
  char sGT_All[32]; IntToString(GT_All, sGT_All, sizeof(sGT_All));

  cv_version = CreateConVar(
    "multi_givaway_version",
    PLUGIN_VERSION,
    "Plugin version",
    FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY|FCVAR_DONTRECORD);
  cv_flags = CreateConVar(
    "multi_givaway_flags",
    "b",
    "List of admin flags allowed to manage giveaways");
  cv_enabled_types = CreateConVar(
    "multi_giveaway_enabled_types",
    sGT_All,
    "Bit-field (sum of options) of enabled Giveaway types",
    FCVAR_NONE,
    true, 1.0,
    true, view_as<float>(GT_All));
  cv_player_state = CreateConVar(
    "multi_giveaway_player_state",
    "7",
    "Bit-field (sum of options) of player states that are valid for giveaway participation: 1 - Spectator, 2 - Dead, 4 - Alive",
    FCVAR_NONE,
    true, 1.0,
    true, 7.0);
  cv_min_conn_time = CreateConVar( // TODO: Allow certain giveaways to override (eg. kill objective)
    "multi_giveaway_min_conn_time",
    "300", // 5 minutes
    "Amount of time in seconds a player must be connected to participate in a giveaway",
    FCVAR_NONE,
    true, 0.0);

  cv_dice_min = CreateConVar(
    "multi_giveaway_dice_min",
    "1",
    "Lowest possible number a player may roll",
    FCVAR_NONE,
    true, 1.0);
  cv_dice_max = CreateConVar(
    "multi_giveaway_dice_max",
    "50",
    "Highest possible number a player may roll");
  cv_dice_show_rolls = CreateConVar(
    "multi_giveaway_dice_show_rolls",
    "0",
    "Who can see other clients' dice rolls: 0 - Nobody, 1 - Admins, 2 - Everyone",
    FCVAR_NONE,
    true, 0.0,
    true, 2.0);
  cv_dice_rerolls = CreateConVar(
    "multi_giveaway_dice_reroll",
    "0",
    "Number of 'do-overs' a player gets when rolling. Only the last is counted, for better or for worse");

  cv_number_min = CreateConVar(
    "multi_giveaway_number_min",
    "1",
    "Lower bound of the range of numbers",
    FCVAR_NONE,
    true, 1.0);
  cv_number_max = CreateConVar(
    "multi_giveaway_number_max",
    "100",
    "Upper bound of the range of numbers");
  cv_number_show_guesses = CreateConVar(
    "multi_giveaway_number_show_guesses",
    "2",
    "Who can see other clients' guesses: 0 - Nobody, 1 - Admins, 2 - Everyone",
    FCVAR_NONE,
    true, 0.0,
    true, 2.0);
}

void RegisterCommands()
{
  // XXX: Is there a better way to create aliases?
  // RegConsoleCmd for client commands (ie participation)
  // RegAdminCmd for admin commands (ie (start|stop)ing giveaways
  // methodmap may be helpful

  RegAdminCmd("sm_mg_reload", Command_ReloadConfig, ADMFLAG_CONFIG,
    "Reload configuration file");
  // start/stop/status <type> - style command
  RegAdminCmd("sm_multigiveaway", Command_MultiGiveaway, ADMFLAG_GENERIC,
    "Manage giveaways");
  RegAdminCmd("sm_mg", Command_MultiGiveaway, ADMFLAG_GENERIC,
    "Manage giveaways");
}

void GetTypeName(const GiveawayType type, char[] str, int sz)
{
  switch (type)
  {
    case GT_Invalid: return;
    case GT_Dice:    StrCopy(str, sz, "Dice");
    case GT_Number:  StrCopy(str, sz, "Number");
    case GT_Kill:    StrCopy(str, sz, "Kill");
    default:         return;
  }
}

GiveawayType GetType(const char[] typename)
{
  if      (StrEqual(typename, "dice", false))   return GT_Dice;
  else if (StrEqual(typename, "number", false)) return GT_Number;
  else if (StrEqual(typename, "kill", false))   return GT_Kill;
  else return GT_Invalid;
}

bool LoadConfig()
{
  //if (configfile != null) // is this boilerplate necessary?
    //CloseHandle(configfile);
  // ...
  return true;
}

// TODO: Return something meaningful if possible, rather than void
void Giveaway_Start(const GiveawayType type, const ArrayList args, int client)
{
  char typename[32];
  GetTypeName(type, typename, sizeof(typename));
  ReplyToCommand(client, "%s Starting Giveaway '%s'", PLUGIN_TAG, typename);
  // ...
}

void Giveaway_Stop(const GiveawayType type, int client)
{
  char typename[32];
  GetTypeName(type, typename, sizeof(typename));
  ReplyToCommand(client, "%s Stopping Giveaway '%s'", PLUGIN_TAG, typename);
  // ...
}

void Giveaway_Restart(const GiveawayType type, int client)
{
  char typename[32];
  GetTypeName(type, typename, sizeof(typename));
  ReplyToCommand(client, "%s Restarting Giveaway '%s'", PLUGIN_TAG, typename);
  // ...
}

void Giveaway_Status()
{
  // return what giveaway is running, if any
  // do other things
}

// Forwards  ///////////////////////

public void OnPluginStart()
{
  // NOTE:
  // load our config. make use of the Kv*/KeyValue functions
  // for flag cvars, ReadFlagString(), GetUserFlagBits() and & are our friends
  // register event forwards (CreateGlobalForward())? ie kill-objectives

  RegisterCommands();
  RegisterConVars();

  char translatefile[128];
  Format(translatefile, sizeof(translatefile), "%s.phrases", PLUGIN_NAME);
  //LoadTranslations(translatefile);
}

public void OnClientDisconnect(int client)
{
  // remove client from any current lists, etc
}

// Callbacks ///////////////////////

public Action Command_ReloadConfig(int client, int args)
{
  bool success = LoadConfig();
  LogAction(client, -1, "%s configuration reload by %L", PLUGIN_NAME, client);
  if (success)
    ReplyToCommand(client, "%s Configuration reloaded!", PLUGIN_TAG);
  else // TODO: Provide error information, etc
    ReplyToCommand(client, "%s Configuration error!", PLUGIN_TAG);

  return Plugin_Handled;
}

public Action Command_MultiGiveaway(int client, int args)
{
  // sm_mg [start|stop|restart|status] <dice|number|kill|...> [params]...

  if (!args)
  {
    // Interactive Mode: Menu-based interface
    ReplyToCommand(client, "%s Interactive mode not yet implemented!",
                   PLUGIN_TAG);
    return Plugin_Handled;
  }

  char action[32], typename[32];

  GetCmdArg(1, action, sizeof(action));
  GetCmdArg(2, typename, sizeof(typename));
  GiveawayType type = GetType(typename);

  if (StrEqual(action, "start", false))
  {
    ArrayList typeargs = CreateArray();
    for (int i = 3; i <= GetCmdArgs(); ++i)
    {
      char arg[64];
      GetCmdArg(i, arg, sizeof(arg));
      typeargs.PushString(arg);
    }
    Giveaway_Start(type, typeargs, client);
  }
  else if (StrEqual(action, "stop", false))
    Giveaway_Stop(type, client);
  else if (StrEqual(action, "restart", false))
    Giveaway_Restart(type, client);
  else if (StrEqual(action, "status", false))
    Giveaway_Status();
  else
  {
    // TODO: List valid actions in error message, or inform of a help command
    ReplyToCommand(client, "%s Invalid action '%s'", PLUGIN_TAG, action);
    return Plugin_Handled;
  }

  return Plugin_Handled;
}

// vim: et sts=2 sw=2

/*
 * Copyright (c) 2016 Alex "ZeroKnight" George
 *
 * This file is licensed under the MIT License. For details, please see the
 * LICENSE file that should be included with the source code, or
 * <https://opensource.org/licenses/MIT>
 */

#include <sourcemod>

#pragma semicolon 1
#pragma newdecls required

char PLUGIN_NAME[]    = "Multi-Giveaway";
char PLUGIN_AUTHOR[]  = "Alex \"ZeroKnight\" George";
char PLUGIN_DESC[]    = "Expansive Giveaway system with numerous types of Giveaway events and stats";
char PLUGIN_VERSION[] = "0.1.0";
char PLUGIN_URL[]     = "http:/github.com/ZeroKnight/sm-Multi-Giveaway";
char PLUGIN_TAG[]     = "[MG]";

public Plugin myinfo =
{
  name        = PLUGIN_NAME,
  author      = PLUGIN_AUTHOR,
  description = PLUGIN_DESC,
  version     = PLUGIN_VERSION,
  url         = PLUGIN_URL
};

enum GiveawayType { GT_Invalid = -1, GT_Dice = 1, GT_Number = 2, GT_Kill = 4, GT_All = 7 };

//File configfile = ...

// TODO: Put things like this in a personal core library?
#define CVAR(%1) ConVar cv_%1
char CVAR_PREFIX[] = "multi_giveaway";
CVAR(version);
CVAR(flags);
CVAR(enabled_types);
CVAR(player_state);
CVAR(min_conn_time);
CVAR(dice_min);
CVAR(dice_max);
CVAR(dice_show_rolls);
CVAR(dice_rerolls);
CVAR(number_min);
CVAR(number_max);
CVAR(number_show_guesses);

void RegisterConVars()
{
  // XXX: Why is CreateConVar() not overloaded?! Are there even overloads?!
  char sGT_All[32]; IntToString(GT_All, sGT_All, sizeof(sGT_All));

  cv_version = CreateConVar(
    "multi_givaway_version",
    PLUGIN_VERSION,
    "Plugin version",
    FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY|FCVAR_DONTRECORD);
  cv_flags = CreateConVar(
    "multi_givaway_flags",
    "b",
    "List of admin flags allowed to manage giveaways");
  cv_enabled_types = CreateConVar(
    "multi_giveaway_enabled_types",
    sGT_All,
    "Bit-field (sum of options) of enabled Giveaway types",
    FCVAR_NONE,
    true, 1.0,
    true, GT_All);
  cv_player_state = CreateConVar(
    "multi_giveaway_player_state",
    "7",
    "Bit-field (sum of options) of player states that are valid for giveaway participation: 1 - Spectator, 2 - Dead, 4 - Alive",
    FCVAR_NONE,
    true, 1.0,
    true, 7.0);
  cv_min_conn_time = CreateConVar( // TODO: Allow certain giveaways to override (eg. kill objective)
    "multi_giveaway_min_conn_time",
    "300", // 5 minutes
    "Amount of time in seconds a player must be connected to participate in a giveaway",
    FCVAR_NONE,
    true, 0.0);

  cv_dice_min = CreateConVar(
    "multi_giveaway_dice_min",
    "1",
    "Lowest possible number a player may roll",
    FCVAR_NONE,
    true, 1.0);
  cv_dice_max = CreateConVar(
    "multi_giveaway_dice_max",
    "50",
    "Highest possible number a player may roll");
  cv_dice_show_rolls = CreateConVar(
    "multi_giveaway_dice_show_rolls",
    "0",
    "Who can see other clients' dice rolls: 0 - Nobody, 1 - Admins, 2 - Everyone",
    FCVAR_NONE,
    true, 0.0,
    true, 2.0);
  cv_dice_rerolls = CreateConVar(
    "multi_giveaway_dice_reroll",
    "0",
    "Number of 'do-overs' a player gets when rolling. Only the last is counted, for better or for worse");

  cv_number_min = CreateConVar(
    "multi_giveaway_number_min",
    "1",
    "Lower bound of the range of numbers",
    FCVAR_NONE,
    true, 1.0);
  cv_number_max = CreateConVar(
    "multi_giveaway_number_max",
    "100",
    "Upper bound of the range of numbers");
  cv_number_show_guesses = CreateConVar(
    "multi_giveaway_number_show_guesses",
    "2",
    "Who can see other clients' guesses: 0 - Nobody, 1 - Admins, 2 - Everyone",
    FCVAR_NONE,
    true, 0.0,
    true, 2.0);
}

void RegisterCommands()
{
  // XXX: Is there a better way to create aliases?
  // RegConsoleCmd for client commands (ie participation)
  // RegAdminCmd for admin commands (ie (start|stop)ing giveaways
  // methodmap may be helpful

  RegAdminCmd("sm_mg_reload", Command_ReloadConfig, ADMFLAG_CONFIG,
    "Reload configuration file");
  // start/stop/status <type> - style command
  RegAdminCmd("sm_multigiveaway", Command_MultiGiveaway, ADMFLAG_GENERIC,
    "Manage giveaways");
  RegAdminCmd("sm_mg", Command_MultiGiveaway, ADMFLAG_GENERIC,
    "Manage giveaways");
}

void LoadConfig()
{
  //if (configfile != null) // is this boilerplate necessary?
    //CloseHandle(configfile);
  // ...
}

// TODO: If (Source)Pawn has function overloading, implement this as GetType()
// This is sufficient. I refuse to use disgusting macro hacks.
GiveawayType Type_Str2Enum(const char[] type)
{
  if (StrEqual(type, "dice", false)) return GT_Dice;
  else if (StrEqual(type, "number", false)) return GT_Number;
  else if (StrEqual(type, "kill", false)) return GT_Kill;
  else return GT_Invalid;
}

char[] Type_Enum2Str(const GiveawayType type)
{
  switch (type)
  {
    case GT_Dice: return "Dice";
    case GT_Number: return "Number";
    case GT_Kill: return "Kill";
    case GT_Invalid: return "";
    default: return ""; // Blah, no fallthrough
  }
}

// TODO: Return something meaningful if possible, rather than void
void Giveaway_Start(const GiveawayType type, const ArrayList args)
{
  // ...
}

void Giveaway_Stop(const GiveawayType type)
{
  // ...
}

void Giveaway_Restart(const GiveawayType type)
{
  // ...
}

void Giveaway_Status()
{
  // return what giveaway is running, if any
  // do other things
}

// Forwards  ///////////////////////

public void OnPluginStart()
{
  // NOTE:
  // load our config. make use of the Kv*/KeyValue functions
  // for flag cvars, ReadFlagString(), GetUserFlagBits() and & are our friends
  // register event forwards (CreateGlobalForward())? ie kill-objectives

  RegisterCommands();
  RegisterConVars();

  char translatefile[128];
  Format(translatefile, sizeof(translatefile), "%s.phrases", PLUGIN_NAME);
  LoadTranslations(translatefile);
}

public void OnClientDisconnect(int client)
{
  // remove client from any current lists, etc
}

// Callbacks ///////////////////////

public Action Command_ReloadConfig(int client, int args)
{
  LoadConfig();
  LogAction(client, -1, "%s configuration reloaded by %L", PLUGIN_NAME, client);
  ReplyToCommand(client, "%s Configuration reloaded!", PLUGIN_TAG);
  return Plugin_Handled;
}

public Action Command_MultiGiveaway(int client, int args)
{
  // sm_mg [start|stop|restart|status] <dice|number|kill|...> [params]...

  // XXX: should we use GetCmdArgs() or args for this?
  if (!GetCmdArgs())
  {
    // interactive. open a menu
  }

  char action[32], type_str[32];

  // ???: is this a blank string if there is no arg?
  GetCmdArg(1, action, sizeof(action));
  GetCmdArg(2, type_str, sizeof(type_str));
  GiveawayType type = Type_Str2Enum(type_str);

  if (StrEqual(action, "start", false))
  {
    ArrayList typeargs = CreateArray();
    for (int i = 3; i <= GetCmdArgs(); ++i)
    {
      char arg[64];
      GetCmdArg(i, arg, sizeof(arg));
      typeargs.PushString(arg);
    }
    Giveaway_Start(type, typeargs);
  }
  else if (StrEqual(action, "stop", false))
    Giveaway_Stop(type);
  else if (StrEqual(action, "restart", false))
    Giveaway_Restart(type);
  else if (StrEqual(action, "status", false))
    Giveaway_Status();
  else
  {
    // TODO: List valid actions in error message, or inform of a help command
    ReplyToCommand(client, "%s Invalid action '%s'", PLUGIN_TAG, action);
    return Plugin_Handled;
  }
}

// vim: et sts=2 sw=2


Translation Player Constant

Translation Player Constant

#pragma semicolon 1

#define PLUGIN_AUTHOR "RumbleFrog, SourceBans++ Dev Team"
#define PLUGIN_VERSION "1.6.3-rc.1"

#include <sourcemod>
#include <sourcebans>

#pragma newdecls required

#define Chat_Prefix "[SourceBans++] "

enum
{
	Cooldown = 0,
	MinLen,
	Settings_Count
};

ConVar Convars[Settings_Count];

bool bInReason[MAXPLAYERS + 1];

int iMinLen = 10
	, iTargetCache[MAXPLAYERS + 1];

float fCooldown = 60.0
	, fNextUse[MAXPLAYERS + 1];

public Plugin myinfo =
{
	name = "SourceBans++ Report Plugin",
	author = PLUGIN_AUTHOR,
	description = "Adds ability for player to report offending players",
	version = PLUGIN_VERSION,
	url = "https://sbpp.github.io"
};

public void OnPluginStart()
{
	CreateConVar("sbpp_report_version", PLUGIN_VERSION, "SBPP Report Version", FCVAR_REPLICATED | FCVAR_SPONLY | FCVAR_DONTRECORD | FCVAR_NOTIFY);

	Convars[Cooldown] = CreateConVar("sbpp_report_cooldown", "60.0", "Cooldown in seconds between per report per user", FCVAR_NONE, true, 0.0, false);
	Convars[MinLen] = CreateConVar("sbpp_report_minlen", "10", "Minimum reason length", FCVAR_NONE, true, 0.0, false);

	LoadTranslations("sbpp_report.phrases");

	RegConsoleCmd("sm_report", CmdReport, "Initialize Report");

	Convars[Cooldown].AddChangeHook(OnConvarChanged);
	Convars[MinLen].AddChangeHook(OnConvarChanged);
}

public Action CmdReport(int iClient, int iArgs)
{
	if (!IsValidClient(iClient))
		return Plugin_Handled;

	if (OnCooldown(iClient))
	{
		PrintToChat(iClient, "%s%T", Chat_Prefix, "In Cooldown", iClient, GetRemainingTime(iClient));

		return Plugin_Handled;
	}

	Menu PList = new Menu(ReportMenu);

	char sName[MAX_NAME_LENGTH], sIndex[4];

	for (int i = 0; i <= MaxClients; i++)
	{
		if (!IsValidClient(i))
			continue;

		GetClientName(i, sName, sizeof sName);
		IntToString(i, sIndex, sizeof sIndex);

		PList.AddItem(sIndex, sName);
	}

	PList.Display(iClient, MENU_TIME_FOREVER);

	return Plugin_Handled;
}

public int ReportMenu(Menu menu, MenuAction action, int iClient, int iItem)
{
	switch (action)
	{
		case MenuAction_Select:
		{
			char sIndex[4];

			menu.GetItem(iItem, sIndex, sizeof sIndex);

			iTargetCache[iClient] = StringToInt(sIndex);

			bInReason[iClient] = true;

			PrintToChat(iClient, "%s%T", Chat_Prefix, "Reason Prompt", iClient);
		}
		case MenuAction_End:
			delete menu;
	}
}

public Action OnClientSayCommand(int iClient, const char[] sCommand, const char[] sArgs)
{
	if (!bInReason[iClient])
		return Plugin_Continue;

	if (!IsValidClient(iClient) || (iTargetCache[iClient] != -1 && !IsValidClient(iTargetCache[iClient])))
	{
		ResetInReason(iClient);

		return Plugin_Continue;
	}

	if (StrEqual(sArgs, "cancel", false))
	{
		PrintToChat(iClient, "%s%T", Chat_Prefix, "Report Canceled", iClient);

		ResetInReason(iClient);

		return Plugin_Stop;
	}

	if (strlen(sArgs) < iMinLen)
	{
		PrintToChat(iClient, "%s%T", Chat_Prefix, "Reason Short Need More", iClient);

		return Plugin_Stop;
	}

	SourceBans_ReportPlayer(iClient, iTargetCache[iClient], sArgs);

	AddCooldown(iClient);

	ResetInReason(iClient);

	PrintToChat(iClient, "%s%T", Chat_Prefix, "Report Sent", iClient);

	return Plugin_Stop;
}

void ResetInReason(int iClient)
{
	bInReason[iClient] = false;
	iTargetCache[iClient] = -1;
}

void AddCooldown(int iClient)
{
	fNextUse[iClient] = GetGameTime() + fCooldown;
}

bool OnCooldown(int iClient)
{
	return (fNextUse[iClient] - GetGameTime()) > 0.0;
}

float GetRemainingTime(int iClient)
{
	float fOffset = fNextUse[iClient] - GetGameTime();

	if (fOffset > 0.0)
		return fOffset;
	else
		return 0.0;
}

public void OnConvarChanged(ConVar convar, const char[] oldValue, const char[] newValue)
{
	if (convar == Convars[Cooldown])
		fCooldown = Convars[Cooldown].FloatValue;
	else if (convar == Convars[MinLen])
		iMinLen = Convars[MinLen].IntValue;
}

stock bool IsValidClient(int iClient, bool bAlive = false)
{
	if (iClient >= 1 &&
	iClient <= MaxClients &&
	IsClientConnected(iClient) &&
	IsClientInGame(iClient) &&
	!IsFakeClient(iClient) &&
	(bAlive == false || IsPlayerAlive(iClient)))
	{
		return true;
	}

	return false;
}

#pragma semicolon 1

#define PLUGIN_AUTHOR "RumbleFrog, SourceBans++ Dev Team"
#define PLUGIN_VERSION "1.6.3-rc.1"

#include <sourcemod>
#include <sourcebans>

#pragma newdecls required

#define Chat_Prefix "[SourceBans++] "

enum
{
	Cooldown = 0,
	MinLen,
	Settings_Count
};

ConVar Convars[Settings_Count];

bool bInReason[MAXPLAYERS + 1];

int iMinLen = 10
	, iTargetCache[MAXPLAYERS + 1];

float fCooldown = 60.0
	, fNextUse[MAXPLAYERS + 1];

public Plugin myinfo =
{
	name = "SourceBans++ Report Plugin",
	author = PLUGIN_AUTHOR,
	description = "Adds ability for player to report offending players",
	version = PLUGIN_VERSION,
	url = "https://sbpp.github.io"
};

public void OnPluginStart()
{
	CreateConVar("sbpp_report_version", PLUGIN_VERSION, "SBPP Report Version", FCVAR_REPLICATED | FCVAR_SPONLY | FCVAR_DONTRECORD | FCVAR_NOTIFY);

	Convars[Cooldown] = CreateConVar("sbpp_report_cooldown", "60.0", "Cooldown in seconds between per report per user", FCVAR_NONE, true, 0.0, false);
	Convars[MinLen] = CreateConVar("sbpp_report_minlen", "10", "Minimum reason length", FCVAR_NONE, true, 0.0, false);

	LoadTranslations("sbpp_report.phrases");

	RegConsoleCmd("sm_report", CmdReport, "Initialize Report");

	Convars[Cooldown].AddChangeHook(OnConvarChanged);
	Convars[MinLen].AddChangeHook(OnConvarChanged);
}

public Action CmdReport(int iClient, int iArgs)
{
	if (!IsValidClient(iClient))
		return Plugin_Handled;

	if (OnCooldown(iClient))
	{
		PrintToChat(iClient, "%s%T", Chat_Prefix, "In Cooldown", GetRemainingTime(iClient));

		return Plugin_Handled;
	}

	Menu PList = new Menu(ReportMenu);

	char sName[MAX_NAME_LENGTH], sIndex[4];

	for (int i = 0; i <= MaxClients; i++)
	{
		if (!IsValidClient(i))
			continue;

		GetClientName(i, sName, sizeof sName);
		IntToString(i, sIndex, sizeof sIndex);

		PList.AddItem(sIndex, sName);
	}

	PList.Display(iClient, MENU_TIME_FOREVER);

	return Plugin_Handled;
}

public int ReportMenu(Menu menu, MenuAction action, int iClient, int iItem)
{
	switch (action)
	{
		case MenuAction_Select:
		{
			char sIndex[4];

			menu.GetItem(iItem, sIndex, sizeof sIndex);

			iTargetCache[iClient] = StringToInt(sIndex);

			bInReason[iClient] = true;

			PrintToChat(iClient, "%s%T", Chat_Prefix, "Reason Prompt");
		}
		case MenuAction_End:
			delete menu;
	}
}

public Action OnClientSayCommand(int iClient, const char[] sCommand, const char[] sArgs)
{
	if (!bInReason[iClient])
		return Plugin_Continue;

	if (!IsValidClient(iClient) || (iTargetCache[iClient] != -1 && !IsValidClient(iTargetCache[iClient])))
	{
		ResetInReason(iClient);

		return Plugin_Continue;
	}

	if (StrEqual(sArgs, "cancel", false))
	{
		PrintToChat(iClient, "%s%T", Chat_Prefix, "Report Canceled");

		ResetInReason(iClient);

		return Plugin_Stop;
	}

	if (strlen(sArgs) < iMinLen)
	{
		PrintToChat(iClient, "%s%T", Chat_Prefix, "Reason Short Need More");

		return Plugin_Stop;
	}

	SourceBans_ReportPlayer(iClient, iTargetCache[iClient], sArgs);

	AddCooldown(iClient);

	ResetInReason(iClient);

	PrintToChat(iClient, "%s%T", Chat_Prefix, "Report Sent");

	return Plugin_Stop;
}

void ResetInReason(int iClient)
{
	bInReason[iClient] = false;
	iTargetCache[iClient] = -1;
}

void AddCooldown(int iClient)
{
	fNextUse[iClient] = GetGameTime() + fCooldown;
}

bool OnCooldown(int iClient)
{
	return (fNextUse[iClient] - GetGameTime()) > 0.0;
}

float GetRemainingTime(int iClient)
{
	float fOffset = fNextUse[iClient] - GetGameTime();

	if (fOffset > 0.0)
		return fOffset;
	else
		return 0.0;
}

public void OnConvarChanged(ConVar convar, const char[] oldValue, const char[] newValue)
{
	if (convar == Convars[Cooldown])
		fCooldown = Convars[Cooldown].FloatValue;
	else if (convar == Convars[MinLen])
		iMinLen = Convars[MinLen].IntValue;
}

stock bool IsValidClient(int iClient, bool bAlive = false)
{
	if (iClient >= 1 &&
	iClient <= MaxClients &&
	IsClientConnected(iClient) &&
	IsClientInGame(iClient) &&
	!IsFakeClient(iClient) &&
	(bAlive == false || IsPlayerAlive(iClient)))
	{
		return true;
	}

	return false;
}


Have HaveNotVoted called for players automaticly

Called after a round ends for players who played for more than a certain
amount of time

Fixed array building in query param

/**
 * vim: set ts=4 :
 * =============================================================================
 * map_votes
 * Interact with the Map Votes web api - https://github.com/CrimsonTautology/map_votes
 *
 * Copyright 2013 The Crimson Tautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <steamtools>
#include <base64>
#include <smjansson>
#include <morecolors>

#define PLUGIN_VERSION "0.1"

public Plugin:myinfo = {
    name = "MapVotes",
    author = "CrimsonTautology",
    description = "Interact with the Map Votes web api",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_map_votes"
};

#define CAST_VOTE_ROUTE "/v1/api/cast_vote"
#define WRITE_MESSAGE_ROUTE "/v1/api/write_message"
#define FAVORITE_ROUTE "/v1/api/favorite"
#define UNFAVORITE_ROUTE "/v1/api/unfavorite"
#define GET_FAVORITES_ROUTE "/v1/api/get_favorites"
#define HAVE_NOT_VOTED_ROUTE "/v1/api/have_not_voted"
#define SERVER_QUERY_ROUTE "/v1/api/server_query"
#define UPDATE_MAP_PLAY_TIME_ROUTE "/v1/api/update_map_play_time"
#define MAPS_ROUTE "/maps"

#define MAX_STEAMID_LENGTH 21
#define MAX_COMMUNITYID_LENGTH 18

new Handle:g_Cvar_MapVotesUrl = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesApiKey = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesVotingEnabled = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesCommentingEnabled = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesNominationsName = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesRequestCooldownTime = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesCallAutoVoteTime = INVALID_HANDLE;

new g_ClientCooldown[MAXPLAYERS+1];
new g_ClientMapStartTimestamp[MAXPLAYERS+1];

new g_MapStartTimestamp = 0;

new g_MapFileSerial = -1;
new Handle:g_MapList = INVALID_HANDLE;
new Handle:g_MapTrie = INVALID_HANDLE;

new Handle:g_nominations = INVALID_HANDLE;
new Function:g_Handler_MapSelectMenu = INVALID_FUNCTION;

//Sourcemod older version support
public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) {
    MarkNativeAsOptional("GetUserMessageType");
    return APLRes_Success;
}

public OnPluginStart()
{

    LoadTranslations("map_votes.phrases");
    g_Cvar_MapVotesUrl = CreateConVar("sm_map_votes_url", "", "URL to your Map Votes web page.  Remeber the \"http://\" part and to quote the entire url!");
    g_Cvar_MapVotesApiKey = CreateConVar("sm_map_votes_api_key", "", "The API key you generated to interact with the Map Votes web page");
    g_Cvar_MapVotesVotingEnabled = CreateConVar("sm_map_votes_voting_enabled", "1", "Whether players are allowed to vote on the current map");
    g_Cvar_MapVotesCommentingEnabled = CreateConVar("sm_map_votes_commenting_enabled", "1", "Whether players are allowed to comment on the current map");
    g_Cvar_MapVotesNominationsName = CreateConVar("sm_map_votes_nominations_plugin", "nominations.smx", "The nominations plugin used by the server");
    g_Cvar_MapVotesRequestCooldownTime = CreateConVar("sm_map_votes_request_cooldown_time", "2", "How long in seconds before a client can send another http request");
    g_Cvar_MapVotesCallAutoVoteTime = CreateConVar("sm_map_votes_call_auto_vote_time", "1200", "How long in seconds before a client is automaticly asked to vote on the current map");

    RegConsoleCmd("sm_votemenu", Command_VoteMenu, "Bring up a menu to vote on the current map");
    RegConsoleCmd("sm_voteup", Command_VoteUp, "Vote that you like the current map");
    RegConsoleCmd("sm_like", Command_VoteUp, "Vote that you like the current map");
    RegConsoleCmd("sm_votedown", Command_VoteDown, "Vote that you hate the current map");
    RegConsoleCmd("sm_hate", Command_VoteDown, "Vote that you hate the current map");

    RegConsoleCmd("sm_fav", Command_Favorite, "Add this map to your favorites");
    RegConsoleCmd("sm_unfav", Command_Unfavorite, "Remove this map to your favorites");
    RegConsoleCmd("sm_nomfav", Command_GetFavorites, "Nominate from a list of your favorites");

    RegConsoleCmd("sm_mapcomment", Command_MapComment, "Comment on the current map");
    RegConsoleCmd("sm_mc", Command_MapComment, "Comment on the current map");

    RegConsoleCmd("sm_viewmap", Command_ViewMap, "View the Map Votes web page for this map");

    RegAdminCmd("sm_have_not_voted", Command_HaveNotVoted, ADMFLAG_VOTE, "Popup a vote panel to every player on the server that has not yet voted on this map");

    RegConsoleCmd("sm_test", Command_Test, "TODO - TEST");//TODO

    HookEvent("round_start", Event_RoundStart);
    HookEvent("teamplay_round_start", Event_RoundStart);
    new array_size = ByteCountToCells(PLATFORM_MAX_PATH);
    g_MapList = CreateArray(array_size);
    g_MapTrie = CreateTrie();
    BuildMapListAndTrie();
}

public OnMapStart()
{

    g_MapStartTimestamp = GetTime();
}

public OnMapEnd()
{
    decl String:map[PLATFORM_MAX_PATH];
    GetCurrentMap(map, sizeof(map));
    UpdateMapPlayTime(g_MapStartTimestamp);
}

public OnClientPutInServer(client)
{
    g_ClientMapStartTimestamp[client] = GetTime();
}

public OnConfigsExecuted()
{
    BuildMapListAndTrie();
}

public OnAllPluginsLoaded() {
    new String:noms[PLATFORM_MAX_PATH];
    GetConVarString(g_Cvar_MapVotesNominationsName, noms, sizeof(noms));
    g_nominations = FindPluginByFile(noms);

    //Check if nominations.smx is both available and currently running
    if(g_nominations == INVALID_HANDLE || GetPluginStatus(g_nominations) != Plugin_Running){
        SetFailState("[MapVotes] Error, nominations is currently not running");
    }
    else{
        //We should be clear to link the MapSelectMenu function
        g_Handler_MapSelectMenu = GetFunctionByName(g_nominations, "Handler_MapSelectMenu");
    }
}

public Action:Command_VoteMenu(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        CReplyToCommand(client, "%t", "voting_not_enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        CallVoteOnClient(client);
    }

    return Plugin_Handled;
}

public Action:Command_VoteUp(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        CReplyToCommand(client, "%t", "voting_not_enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        CastVote(client, 1);
    }

    return Plugin_Handled;
}

public Action:Command_VoteDown(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        CReplyToCommand(client, "%t", "voting_not_enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        CastVote(client, -1);
    }

    return Plugin_Handled;
}

public Action:Command_Favorite(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        new String:map[PLATFORM_MAX_PATH];
        if (args <= 0)
        {
            GetCurrentMap(map, sizeof(map));
            Favorite(map, client, true);
        }else{
            GetCmdArg(1, map, sizeof(map));
            MapSearch(client, map, g_MapList, FavoriteSearchHandler);
        }
    }

    return Plugin_Handled;
}
public Action:Command_Unfavorite(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        new String:map[PLATFORM_MAX_PATH];
        if (args <= 0)
        {
            GetCurrentMap(map, sizeof(map));
            Favorite(map, client, false);
        }else{
            GetCmdArg(1, map, sizeof(map));
            MapSearch(client, map, g_MapList, UnfavoriteSearchHandler);
        }

    }

    return Plugin_Handled;
}
public Action:Command_GetFavorites(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        GetFavorites(client);
    }

    return Plugin_Handled;
}

public Action:Command_ViewMap(client, args)
{
    if(client && IsClientAuthorized(client)){
        ViewMap(client);
    }

    return Plugin_Handled;
}

public Action:Command_MapComment(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if (!GetConVarBool(g_Cvar_MapVotesCommentingEnabled))
    {
        CReplyToCommand(client, "%t", "voting_not_enabled");
        return Plugin_Handled;
    }

    if (args < 1)
    {
        CReplyToCommand(client, "%t", "map_comment_usage");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        decl String:comment[256];
        GetCmdArgString(comment, sizeof(comment));
        WriteMessage(client, comment);
    }

    return Plugin_Handled;
}

public Action:Command_HaveNotVoted(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        CReplyToCommand(client, "%t", "voting_not_enabled");
        return Plugin_Handled;
    }

    HaveNotVoted(client, 0);

    return Plugin_Handled;
}

public Action:Event_RoundStart(Handle:event, const String:name[], bool:dontBroadcast)
{
    //TODO
    PrintToChatAll("Event_RoundStart");
    HaveNotVoted(0, GetConVarInt(g_Cvar_MapVotesCallAutoVoteTime));
}

BuildMapListAndTrie()
{
    //Build the map list
    if (ReadMapList(g_MapList,
                g_MapFileSerial,
                "nominations",
                MAPLIST_FLAG_CLEARARRAY|MAPLIST_FLAG_MAPSFOLDER)
            == INVALID_HANDLE)
    {
        if (g_MapFileSerial == -1)
        {
            SetFailState("Unable to create a valid map list.");
        }else{
            //Build the map trie; note we don't care about the value, just if the map exists in the trie
            ClearTrie(g_MapTrie);
            new String:map[PLATFORM_MAX_PATH];

            for (new i = 0; i < GetArraySize(g_MapList); i++)
            {
                GetArrayString(g_MapList, i, map, sizeof(map));
                SetTrieValue(g_MapTrie, map, 1);
            }
        }
    }
}

public SetAccessCode(&HTTPRequestHandle:request)
{
    //All MapVotes api calls require this code
    decl String:api_key[128];
    GetConVarString(g_Cvar_MapVotesApiKey, api_key, sizeof(api_key));
    Steam_SetHTTPRequestGetOrPostParameter(request, "access_token", api_key);
}

public HTTPRequestHandle:CreateMapVotesRequest(const String:route[])
{
    decl String:base_url[256], String:url[512];
    GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));
    TrimString(base_url);
    new trim_length = strlen(base_url) - 1;

    if(trim_length < 0)
    {
        //MapVotes Url not set
        return INVALID_HTTP_HANDLE;
    }

    //check for forward slash after base_url;
    if(base_url[trim_length] == '/')
    {
        strcopy(base_url, trim_length + 1, base_url);
    }

    Format(url, sizeof(url),
            "%s%s", base_url, route);

    new HTTPRequestHandle:request = Steam_CreateHTTPRequest(HTTPMethod_POST, url);
    SetAccessCode(request);

    return request;
}

public Steam_SetHTTPRequestGetOrPostParameterInt(&HTTPRequestHandle:request, const String:param[], value)
{
    new String:tmp[64];
    IntToString(value, tmp, sizeof(tmp));
    Steam_SetHTTPRequestGetOrPostParameter(request, param, tmp);
}

public StartCooldown(client)
{
    //Ignore the server console
    if (client == 0)
        return;

    g_ClientCooldown[client] = GetTime() + GetConVarInt(g_Cvar_MapVotesRequestCooldownTime);
    PrintToChatAll("%d -> %d", GetTime(), g_ClientCooldown[client]);
}

public bool:IsClientInCooldown(client)
{
    if(client == 0)
        return false;
    else
        return g_ClientCooldown[client] > GetTime();
}

public bool:ClientHasPlayedMapLongerThan(client, min_play_time)
{
    if (min_play_time <=0)
        return true;

    return GetTime() - g_ClientMapStartTimestamp[client] > min_play_time;
}

public WriteMessage(client, String:message[256])
{
    //Encode the message to be url safe
    decl String:base64[256], String:base64_url[256];
    EncodeBase64(base64, sizeof(base64), message);
    Base64MimeToUrl(base64_url, sizeof(base64_url), base64);

    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    decl String:map[PLATFORM_MAX_PATH];
    GetCurrentMap(map, sizeof(map));

    new HTTPRequestHandle:request = CreateMapVotesRequest(WRITE_MESSAGE_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        CReplyToCommand(client, "%t", "url_invalid");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SetHTTPRequestGetOrPostParameter(request, "comment", base64_url);
    Steam_SetHTTPRequestGetOrPostParameter(request, "base64", "1");
    Steam_SendHTTPRequest(request, ReceiveWriteMessage, GetClientUserId(client));
    StartCooldown(client);
}

public ReceiveWriteMessage(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);

    if(client && successful)
    {
        CPrintToChat(client, "%t", "comment_added");
    }

    Steam_ReleaseHTTPRequest(request);
}

public CastVote(client, value)
{

    if(!(value == -1 || value == 0 || value == 1)){
        LogError("[MapVotes] invalid vote value %d (client: %d)", value, client);
    }else{
        decl String:map[PLATFORM_MAX_PATH];
        decl String:uid[MAX_COMMUNITYID_LENGTH];
        Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

        GetCurrentMap(map, sizeof(map));

        new HTTPRequestHandle:request = CreateMapVotesRequest(CAST_VOTE_ROUTE);

        if(request == INVALID_HTTP_HANDLE)
        {
            CReplyToCommand(client, "%t", "url_invalid");
            return;
        }

        Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
        Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
        Steam_SetHTTPRequestGetOrPostParameterInt(request, "value", value);
        Steam_SendHTTPRequest(request, ReceiveCastVote, GetClientUserId(client));
        StartCooldown(client);
    }

}

public ReceiveCastVote(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);

    if(client && successful)
    {
        decl String:data[4096];
        Steam_GetHTTPResponseBodyData(request, data, sizeof(data));

        new Handle:json = json_load(data);
        new value = json_object_get_int(json, "value");
        CloseHandle(json);

        new String:name[64];
        GetClientName(client, name, sizeof(name));

        if (value == 1)
        {
            CPrintToChatAll("%t", "announce_up_vote", name);
        }else if(value == -1)
        {
            CPrintToChatAll("%t", "announce_down_vote", name);
        }else
        {
            CPrintToChat(client, "%t", "vote_cast");
        }
    }

    Steam_ReleaseHTTPRequest(request);
}

public Favorite(String:map[PLATFORM_MAX_PATH], client, bool:favorite)
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    new HTTPRequestHandle:request;
    if(favorite)
    {
        request = CreateMapVotesRequest(FAVORITE_ROUTE);
    }else{
        request = CreateMapVotesRequest(UNFAVORITE_ROUTE);
    }

    if(request == INVALID_HTTP_HANDLE)
    {
        CReplyToCommand(client, "%t", "url_invalid");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SendHTTPRequest(request, ReceiveFavorite, GetClientUserId(client));
    StartCooldown(client);
}

public ReceiveFavorite(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);

    if(client && successful)
    {
        decl String:data[4096];
        Steam_GetHTTPResponseBodyData(request, data, sizeof(data));

        new Handle:json = json_load(data);
        new bool:favorite = json_object_get_bool(json, "favorite");
        new String:map[PLATFORM_MAX_PATH];
        json_object_get_string(json, "map", map, sizeof(map));
        CloseHandle(json);

        new String:name[64];
        GetClientName(client, name, sizeof(name));

        if (favorite)
        {
            CPrintToChatAll("%t", "announce_favorite", name, map);
        }else
        {
            CPrintToChat(client, "%t", "updated_favorites");
        }
    }

    Steam_ReleaseHTTPRequest(request);
}

public MapSearch(client, String:search_key[PLATFORM_MAX_PATH], Handle:map_list, MenuHandler:handler)
{
    new String:map[PLATFORM_MAX_PATH];
    new Handle:menu = CreateMenu(handler, MENU_ACTIONS_DEFAULT|MenuAction_DrawItem|MenuAction_DisplayItem);
    new bool:found = false;

    for(new i=0; i<GetArraySize(map_list); i++)
    {
        GetArrayString(map_list, i, map, sizeof(map));

        //If this map matches the search key, add it to the menu
        if(StrContains(map, search_key, false) >= 0){
            AddMenuItem(menu, map, map);
            found = true;
        }
    }

    if(found)
    {
        SetMenuTitle(menu, "Found Maps");
        DisplayMenu(menu, client, MENU_TIME_FOREVER);
    }

}

public FavoriteSearchHandler(Handle:menu, MenuAction:action, param1, param2)
{
    if (action == MenuAction_End)
    {
        CloseHandle(menu);
    } else if (action == MenuAction_VoteCancel)
    {
    } else if (action == MenuAction_Select)
    {
        new client=param1;
        new String:map[PLATFORM_MAX_PATH];
        GetMenuItem(menu, param2, map, sizeof(map));
        Favorite(map, client, true);
    }
}

public UnfavoriteSearchHandler(Handle:menu, MenuAction:action, param1, param2)
{
    if (action == MenuAction_End)
    {
        CloseHandle(menu);
    } else if (action == MenuAction_VoteCancel)
    {
    } else if (action == MenuAction_Select)
    {
        new client=param1;
        new String:map[PLATFORM_MAX_PATH];
        GetMenuItem(menu, param2, map, sizeof(map));
        Favorite(map, client, false);
    }
}

public GetFavorites(client)
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    new HTTPRequestHandle:request = CreateMapVotesRequest(GET_FAVORITES_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        CReplyToCommand(client, "%t", "url_invalid");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameterInt(request, "player", GetClientUserId(client));
    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SendHTTPRequest(request, ReceiveGetFavorites, GetClientUserId(client));
    StartCooldown(client);
}

public ReceiveGetFavorites(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);
    if(!client)
    {
        //User logged off
        Steam_ReleaseHTTPRequest(request);
        return;
    }
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[MapVotes] Error at RecivedGetFavorites (HTTP Code %d; success %d)", code, successful);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    decl String:data[4096];
    Steam_GetHTTPResponseBodyData(request, data, sizeof(data));
    Steam_ReleaseHTTPRequest(request);

    new Handle:json = json_load(data);
    new Handle:maps = json_object_get(json, "maps");
    new String:map_buffer[PLATFORM_MAX_PATH];
    new junk;
    new bool:found;

    new Handle:menu = CreateMenu(NominateMapHandler, MENU_ACTIONS_DEFAULT|MenuAction_DrawItem|MenuAction_DisplayItem);

    for(new i = 0; i < json_array_size(maps); i++)
    {
        json_array_get_string(maps, i, map_buffer, sizeof(map_buffer));
        TrimString(map_buffer);
        found = GetTrieValue(g_MapTrie, map_buffer, junk);
        if(found)
        {
            AddMenuItem(menu, map_buffer, map_buffer);
        }
    }

    CloseHandle(json);
    CloseHandle(maps);

    //If no maps were found don't even bother displaying a menu
    if(GetMenuItemCount(menu) > 0){
        SetMenuTitle(menu, "Favorited Maps");
        DisplayMenu(menu, client, MENU_TIME_FOREVER);
    }else{
        CReplyToCommand(client, "%t", "no_favorited_maps");
    }
}

public NominateMapHandler(Handle:menu, MenuAction:action, param1, param2)
{

    decl result;

    // Start function call
    Call_StartFunction(g_nominations, g_Handler_MapSelectMenu);

    // Push parameters one at a time
    Call_PushCell(menu);
    Call_PushCell(action);
    Call_PushCell(param1);
    Call_PushCell(param2);

    // Finish the call, get the result
    Call_Finish(result);

    return result;
}

public CallVoteOnClient(client)
{
    new Handle:menu = CreateMenu(VoteMenuHandler);
    SetMenuTitle(menu, "Do you like this map?");
    AddMenuItem(menu, "1","Like it.");
    AddMenuItem(menu, "-1","Hate it.");
    AddMenuItem(menu, "0","I have no strong feelings one way or the other.");
    DisplayMenu(menu, client, 20);
}

public VoteMenuHandler(Handle:menu, MenuAction:action, param1, param2)
{
    if (action == MenuAction_End)
    {
        CloseHandle(menu);
    } else if (action == MenuAction_VoteCancel)
    {
    } else if (action == MenuAction_Select)
    {
        new String:info[32];
        GetMenuItem(menu, param2, info, sizeof(info));
        new value = StringToInt(info);

        CastVote(param1, value);
    }
}

public HaveNotVoted(caller, min_play_time)
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    decl String:map[PLATFORM_MAX_PATH];
    decl String:tmp[64];
    new player;
    new HTTPRequestHandle:request = CreateMapVotesRequest(HAVE_NOT_VOTED_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        CReplyToCommand(caller, "%t", "url_invalid");
        return;
    }

    GetCurrentMap(map, sizeof(map));
    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);

    new i = 0;
    for (new client=1; client <= MaxClients; client++)
    {
        if (!IsClientInGame(client) || IsFakeClient(client))
        {
            continue;
        }

        if(!ClientHasPlayedMapLongerThan(client, min_play_time))
        {
            continue;
        }
        Steam_GetCSteamIDForClient(client, uid, sizeof(uid));
        player = GetClientUserId(client);

        Format(tmp, sizeof(tmp), "uids[%d]", i);
        Steam_SetHTTPRequestGetOrPostParameter(request, tmp, uid);
        Format(tmp, sizeof(tmp), "players[%d]", i);
        Steam_SetHTTPRequestGetOrPostParameterInt(request, tmp, player);
        i++;
    }

    new userid=0;
    if (caller > 0)
    {
        userid=GetClientUserId(caller);
    }

    //Don't bother if nothing matched
    if(i > 0)
    {
        Steam_SendHTTPRequest(request, ReceiveHaveNotVoted, userid);
    }
}

public ReceiveHaveNotVoted(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[MapVotes] Error at RecivedHaveNotVoted (HTTP Code %d)", code);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    decl String:data[4096];
    Steam_GetHTTPResponseBodyData(request, data, sizeof(data));
    Steam_ReleaseHTTPRequest(request);

    new Handle:json = json_load(data);
    new Handle:players = json_object_get(json, "players");
    decl p;

    new count=json_array_size(players);
    if(client)
    {
        CPrintToChat(client, "%t", "have_not_voted", count);
    }

    for(new i = 0; i < count; i++)
    {
        p = json_array_get_int(players, i);
        CallVoteOnClient(GetClientOfUserId(p));
    }
    CloseHandle(json);
    CloseHandle(players);
}

public UpdateMapPlayTime(start_time)
{
    new time_played = GetTime() - start_time;

    //Reject way off values
    if(time_played < 0 || time_played > 86400000)
    {
        LogError("[MapVotes] cannot update map time played; time_played=%d", time_played);
        return;
    }

    decl String:map[PLATFORM_MAX_PATH];
    GetCurrentMap(map, sizeof(map));

    new HTTPRequestHandle:request = CreateMapVotesRequest(UPDATE_MAP_PLAY_TIME_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        LogError("[MapVotes] sm_map_votes_url invalid; cannot create HTTP request");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "time_played", time_played);
    Steam_SendHTTPRequest(request, ReceiveUpdateMapPlayTime);
}

public ReceiveUpdateMapPlayTime(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[MapVotes] Error at ReceiveUpdateMapPlayTime (HTTP Code %d; success %d)", code, successful);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    Steam_ReleaseHTTPRequest(request);
}

public ViewMap(client)
{
    decl String:map[PLATFORM_MAX_PATH], String:url[256], String:base_url[128];
    GetCurrentMap(map, sizeof(map));
    GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));

    TrimString(base_url);
    new trim_length = strlen(base_url) - 1;

    if(base_url[trim_length] == '/')
    {
        strcopy(base_url, trim_length + 1, base_url);
    }

    Format(url, sizeof(url),
            "%s%s/%s", base_url, MAPS_ROUTE, map);

    ShowMOTDPanel(client, "Map Viewer", url, MOTDPANEL_TYPE_URL);

}

//TODO - delete everything below
public Action:Command_Test(client, args)
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    decl String:map[PLATFORM_MAX_PATH];
    new player;
    new HTTPRequestHandle:request = CreateMapVotesRequest(HAVE_NOT_VOTED_ROUTE);

    GetCurrentMap(map, sizeof(map));
    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);

    Steam_SetHTTPRequestGetOrPostParameter(request, "uids[0]", "76561198013926395");
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "players[0]", 1);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uids[1]", "76561198005175623");
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "players[1]", 2);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uids[2]", "76561198013600577");
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "players[2]", 6);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uids[3]", "76561198055205178");
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "players[3]", 17);

    new userid= client ? GetClientUserId(client) : 0;
    Steam_SendHTTPRequest(request, ReceiveTest, userid);

    return Plugin_Handled;
}
public ReceiveTest(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[MapVotes] Error at RecivedHaveNotVoted (HTTP Code %d)", code);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    decl String:data[4096];
    Steam_GetHTTPResponseBodyData(request, data, sizeof(data));

    new Handle:json = json_load(data);
    new Handle:players = json_object_get(json, "players");
    decl p;

    new count=json_array_size(players);
    CPrintToChatAll("%t", "have_not_voted", count);
    CPrintToChatAll("%s", data);
    PrintToConsole(0, "%s", data);

    CloseHandle(json);
    CloseHandle(players);
    Steam_ReleaseHTTPRequest(request);
}

/**
 * vim: set ts=4 :
 * =============================================================================
 * map_votes
 * Interact with the Map Votes web api - https://github.com/CrimsonTautology/map_votes
 *
 * Copyright 2013 The Crimson Tautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <steamtools>
#include <base64>
#include <smjansson>
#include <morecolors>

#define PLUGIN_VERSION "0.1"

public Plugin:myinfo = {
    name = "MapVotes",
    author = "CrimsonTautology",
    description = "Interact with the Map Votes web api",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_map_votes"
};

#define CAST_VOTE_ROUTE "/v1/api/cast_vote"
#define WRITE_MESSAGE_ROUTE "/v1/api/write_message"
#define FAVORITE_ROUTE "/v1/api/favorite"
#define UNFAVORITE_ROUTE "/v1/api/unfavorite"
#define GET_FAVORITES_ROUTE "/v1/api/get_favorites"
#define HAVE_NOT_VOTED_ROUTE "/v1/api/have_not_voted"
#define SERVER_QUERY_ROUTE "/v1/api/server_query"
#define UPDATE_MAP_PLAY_TIME_ROUTE "/v1/api/update_map_play_time"
#define MAPS_ROUTE "/maps"

#define MAX_STEAMID_LENGTH 21
#define MAX_COMMUNITYID_LENGTH 18

new Handle:g_Cvar_MapVotesUrl = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesApiKey = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesVotingEnabled = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesCommentingEnabled = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesNominationsName = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesRequestCooldownTime = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesCallAutoVoteTime = INVALID_HANDLE;

new g_ClientCooldown[MAXPLAYERS+1];
new g_ClientMapStartTime[MAXPLAYERS+1];

new g_MapStartTimestamp = 0;

new g_MapFileSerial = -1;
new Handle:g_MapList = INVALID_HANDLE;
new Handle:g_MapTrie = INVALID_HANDLE;

new Handle:g_nominations = INVALID_HANDLE;
new Function:g_Handler_MapSelectMenu = INVALID_FUNCTION;

//Sourcemod older version support
public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) {
    MarkNativeAsOptional("GetUserMessageType");
    return APLRes_Success;
}

public OnPluginStart()
{

    LoadTranslations("map_votes.phrases");
    g_Cvar_MapVotesUrl = CreateConVar("sm_map_votes_url", "", "URL to your Map Votes web page.  Remeber the \"http://\" part and to quote the entire url!");
    g_Cvar_MapVotesApiKey = CreateConVar("sm_map_votes_api_key", "", "The API key you generated to interact with the Map Votes web page");
    g_Cvar_MapVotesVotingEnabled = CreateConVar("sm_map_votes_voting_enabled", "1", "Whether players are allowed to vote on the current map");
    g_Cvar_MapVotesCommentingEnabled = CreateConVar("sm_map_votes_commenting_enabled", "1", "Whether players are allowed to comment on the current map");
    g_Cvar_MapVotesNominationsName = CreateConVar("sm_map_votes_nominations_plugin", "nominations.smx", "The nominations plugin used by the server");
    g_Cvar_MapVotesRequestCooldownTime = CreateConVar("sm_map_votes_request_cooldown_time", "2", "How long in seconds before a client can send another http request");
    g_Cvar_MapVotesCallAutoVoteTime = CreateConVar("sm_map_votes_call_auto_vote_time", "1200", "How long in seconds before a client is automaticly asked to vote on the current map");

    RegConsoleCmd("sm_votemenu", Command_VoteMenu, "Bring up a menu to vote on the current map");
    RegConsoleCmd("sm_voteup", Command_VoteUp, "Vote that you like the current map");
    RegConsoleCmd("sm_like", Command_VoteUp, "Vote that you like the current map");
    RegConsoleCmd("sm_votedown", Command_VoteDown, "Vote that you hate the current map");
    RegConsoleCmd("sm_hate", Command_VoteDown, "Vote that you hate the current map");

    RegConsoleCmd("sm_fav", Command_Favorite, "Add this map to your favorites");
    RegConsoleCmd("sm_unfav", Command_Unfavorite, "Remove this map to your favorites");
    RegConsoleCmd("sm_nomfav", Command_GetFavorites, "Nominate from a list of your favorites");

    RegConsoleCmd("sm_mapcomment", Command_MapComment, "Comment on the current map");
    RegConsoleCmd("sm_mc", Command_MapComment, "Comment on the current map");

    RegConsoleCmd("sm_viewmap", Command_ViewMap, "View the Map Votes web page for this map");

    RegAdminCmd("sm_have_not_voted", Command_HaveNotVoted, ADMFLAG_VOTE, "Popup a vote panel to every player on the server that has not yet voted on this map");

    RegConsoleCmd("sm_test", Command_Test, "TODO - TEST");//TODO

    new array_size = ByteCountToCells(PLATFORM_MAX_PATH);
    g_MapList = CreateArray(array_size);
    g_MapTrie = CreateTrie();
    BuildMapListAndTrie();
}

public OnMapStart()
{

    g_MapStartTimestamp = GetTime();
}

public OnMapEnd()
{
    decl String:map[PLATFORM_MAX_PATH];
    GetCurrentMap(map, sizeof(map));
    UpdateMapPlayTime(g_MapStartTimestamp);
}

public OnConfigsExecuted()
{
    BuildMapListAndTrie();
}

public OnAllPluginsLoaded() {
    new String:noms[PLATFORM_MAX_PATH];
    GetConVarString(g_Cvar_MapVotesNominationsName, noms, sizeof(noms));
    g_nominations = FindPluginByFile(noms);

    //Check if nominations.smx is both available and currently running
    if(g_nominations == INVALID_HANDLE || GetPluginStatus(g_nominations) != Plugin_Running){
        SetFailState("[MapVotes] Error, nominations is currently not running");
    }
    else{
        //We should be clear to link the MapSelectMenu function
        g_Handler_MapSelectMenu = GetFunctionByName(g_nominations, "Handler_MapSelectMenu");
    }
}

public Action:Command_VoteMenu(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        CReplyToCommand(client, "%t", "voting_not_enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        CallVoteOnClient(client);
    }

    return Plugin_Handled;
}

public Action:Command_VoteUp(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        CReplyToCommand(client, "%t", "voting_not_enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        CastVote(client, 1);
    }

    return Plugin_Handled;
}

public Action:Command_VoteDown(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        CReplyToCommand(client, "%t", "voting_not_enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        CastVote(client, -1);
    }

    return Plugin_Handled;
}

public Action:Command_Favorite(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        new String:map[PLATFORM_MAX_PATH];
        if (args <= 0)
        {
            GetCurrentMap(map, sizeof(map));
            Favorite(map, client, true);
        }else{
            GetCmdArg(1, map, sizeof(map));
            MapSearch(client, map, g_MapList, FavoriteSearchHandler);
        }
    }

    return Plugin_Handled;
}
public Action:Command_Unfavorite(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        new String:map[PLATFORM_MAX_PATH];
        if (args <= 0)
        {
            GetCurrentMap(map, sizeof(map));
            Favorite(map, client, false);
        }else{
            GetCmdArg(1, map, sizeof(map));
            MapSearch(client, map, g_MapList, UnfavoriteSearchHandler);
        }

    }

    return Plugin_Handled;
}
public Action:Command_GetFavorites(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        GetFavorites(client);
    }

    return Plugin_Handled;
}

public Action:Command_ViewMap(client, args)
{
    if(client && IsClientAuthorized(client)){
        ViewMap(client);
    }

    return Plugin_Handled;
}

public Action:Command_MapComment(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if (!GetConVarBool(g_Cvar_MapVotesCommentingEnabled))
    {
        CReplyToCommand(client, "%t", "voting_not_enabled");
        return Plugin_Handled;
    }

    if (args < 1)
    {
        CReplyToCommand(client, "%t", "map_comment_usage");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        decl String:comment[256];
        GetCmdArgString(comment, sizeof(comment));
        WriteMessage(client, comment);
    }

    return Plugin_Handled;
}

public Action:Command_HaveNotVoted(client, args)
{
    if(IsClientInCooldown(client))
    {
        CReplyToCommand(client, "%t", "user_cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        CReplyToCommand(client, "%t", "voting_not_enabled");
        return Plugin_Handled;
    }

    HaveNotVoted(client);

    return Plugin_Handled;
}

BuildMapListAndTrie()
{
    //Build the map list
    if (ReadMapList(g_MapList,
                g_MapFileSerial,
                "nominations",
                MAPLIST_FLAG_CLEARARRAY|MAPLIST_FLAG_MAPSFOLDER)
            == INVALID_HANDLE)
    {
        if (g_MapFileSerial == -1)
        {
            SetFailState("Unable to create a valid map list.");
        }else{
            //Build the map trie; note we don't care about the value, just if the map exists in the trie
            ClearTrie(g_MapTrie);
            new String:map[PLATFORM_MAX_PATH];

            for (new i = 0; i < GetArraySize(g_MapList); i++)
            {
                GetArrayString(g_MapList, i, map, sizeof(map));
                SetTrieValue(g_MapTrie, map, 1);
            }
        }
    }
}

public SetAccessCode(&HTTPRequestHandle:request)
{
    //All MapVotes api calls require this code
    decl String:api_key[128];
    GetConVarString(g_Cvar_MapVotesApiKey, api_key, sizeof(api_key));
    Steam_SetHTTPRequestGetOrPostParameter(request, "access_token", api_key);
}

public HTTPRequestHandle:CreateMapVotesRequest(const String:route[])
{
    decl String:base_url[256], String:url[512];
    GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));
    TrimString(base_url);
    new trim_length = strlen(base_url) - 1;

    if(trim_length < 0)
    {
        //MapVotes Url not set
        return INVALID_HTTP_HANDLE;
    }

    //check for forward slash after base_url;
    if(base_url[trim_length] == '/')
    {
        strcopy(base_url, trim_length + 1, base_url);
    }

    Format(url, sizeof(url),
            "%s%s", base_url, route);

    new HTTPRequestHandle:request = Steam_CreateHTTPRequest(HTTPMethod_POST, url);
    SetAccessCode(request);

    return request;
}

public Steam_SetHTTPRequestGetOrPostParameterInt(&HTTPRequestHandle:request, const String:param[], value)
{
    new String:tmp[64];
    IntToString(value, tmp, sizeof(tmp));
    Steam_SetHTTPRequestGetOrPostParameter(request, param, tmp);
}

public StartCooldown(client)
{
    //Ignore the server console
    if (client == 0)
        return;

    g_ClientCooldown[client] = GetTime() + GetConVarInt(g_Cvar_MapVotesRequestCooldownTime);
    PrintToChatAll("%d -> %d", GetTime(), g_ClientCooldown[client]);
}

public bool:IsClientInCooldown(client)
{
    if(client == 0)
        return false;
    else
        return g_ClientCooldown[client] > GetTime();
}

public WriteMessage(client, String:message[256])
{
    //Encode the message to be url safe
    decl String:base64[256], String:base64_url[256];
    EncodeBase64(base64, sizeof(base64), message);
    Base64MimeToUrl(base64_url, sizeof(base64_url), base64);

    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    decl String:map[PLATFORM_MAX_PATH];
    GetCurrentMap(map, sizeof(map));

    new HTTPRequestHandle:request = CreateMapVotesRequest(WRITE_MESSAGE_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        CReplyToCommand(client, "%t", "url_invalid");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SetHTTPRequestGetOrPostParameter(request, "comment", base64_url);
    Steam_SetHTTPRequestGetOrPostParameter(request, "base64", "1");
    Steam_SendHTTPRequest(request, ReceiveWriteMessage, GetClientUserId(client));
    StartCooldown(client);
}

public ReceiveWriteMessage(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);

    if(client && successful)
    {
        CPrintToChat(client, "%t", "comment_added");
    }

    Steam_ReleaseHTTPRequest(request);
}

public CastVote(client, value)
{

    if(!(value == -1 || value == 0 || value == 1)){
        LogError("[MapVotes] invalid vote value %d (client: %d)", value, client);
    }else{
        decl String:map[PLATFORM_MAX_PATH];
        decl String:uid[MAX_COMMUNITYID_LENGTH];
        Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

        GetCurrentMap(map, sizeof(map));

        new HTTPRequestHandle:request = CreateMapVotesRequest(CAST_VOTE_ROUTE);

        if(request == INVALID_HTTP_HANDLE)
        {
            CReplyToCommand(client, "%t", "url_invalid");
            return;
        }

        Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
        Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
        Steam_SetHTTPRequestGetOrPostParameterInt(request, "value", value);
        Steam_SendHTTPRequest(request, ReceiveCastVote, GetClientUserId(client));
        StartCooldown(client);
    }

}

public ReceiveCastVote(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);

    if(client && successful)
    {
        decl String:data[4096];
        Steam_GetHTTPResponseBodyData(request, data, sizeof(data));

        new Handle:json = json_load(data);
        new value = json_object_get_int(json, "value");
        CloseHandle(json);

        new String:name[64];
        GetClientName(client, name, sizeof(name));

        if (value == 1)
        {
            CPrintToChatAll("%t", "announce_up_vote", name);
        }else if(value == -1)
        {
            CPrintToChatAll("%t", "announce_down_vote", name);
        }else
        {
            CPrintToChat(client, "%t", "vote_cast");
        }
    }

    Steam_ReleaseHTTPRequest(request);
}

public Favorite(String:map[PLATFORM_MAX_PATH], client, bool:favorite)
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    new HTTPRequestHandle:request;
    if(favorite)
    {
        request = CreateMapVotesRequest(FAVORITE_ROUTE);
    }else{
        request = CreateMapVotesRequest(UNFAVORITE_ROUTE);
    }

    if(request == INVALID_HTTP_HANDLE)
    {
        CReplyToCommand(client, "%t", "url_invalid");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SendHTTPRequest(request, ReceiveFavorite, GetClientUserId(client));
    StartCooldown(client);
}

public ReceiveFavorite(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);

    if(client && successful)
    {
        decl String:data[4096];
        Steam_GetHTTPResponseBodyData(request, data, sizeof(data));

        new Handle:json = json_load(data);
        new bool:favorite = json_object_get_bool(json, "favorite");
        new String:map[PLATFORM_MAX_PATH];
        json_object_get_string(json, "map", map, sizeof(map));
        CloseHandle(json);

        new String:name[64];
        GetClientName(client, name, sizeof(name));

        if (favorite)
        {
            CPrintToChatAll("%t", "announce_favorite", name, map);
        }else
        {
            CPrintToChat(client, "%t", "updated_favorites");
        }
    }

    Steam_ReleaseHTTPRequest(request);
}

public MapSearch(client, String:search_key[PLATFORM_MAX_PATH], Handle:map_list, MenuHandler:handler)
{
    new String:map[PLATFORM_MAX_PATH];
    new Handle:menu = CreateMenu(handler, MENU_ACTIONS_DEFAULT|MenuAction_DrawItem|MenuAction_DisplayItem);
    new bool:found = false;

    for(new i=0; i<GetArraySize(map_list); i++)
    {
        GetArrayString(map_list, i, map, sizeof(map));

        //If this map matches the search key, add it to the menu
        if(StrContains(map, search_key, false) >= 0){
            AddMenuItem(menu, map, map);
            found = true;
        }
    }

    if(found)
    {
        SetMenuTitle(menu, "Found Maps");
        DisplayMenu(menu, client, MENU_TIME_FOREVER);
    }

}

public FavoriteSearchHandler(Handle:menu, MenuAction:action, param1, param2)
{
    if (action == MenuAction_End)
    {
        CloseHandle(menu);
    } else if (action == MenuAction_VoteCancel)
    {
    } else if (action == MenuAction_Select)
    {
        new client=param1;
        new String:map[PLATFORM_MAX_PATH];
        GetMenuItem(menu, param2, map, sizeof(map));
        Favorite(map, client, true);
    }
}

public UnfavoriteSearchHandler(Handle:menu, MenuAction:action, param1, param2)
{
    if (action == MenuAction_End)
    {
        CloseHandle(menu);
    } else if (action == MenuAction_VoteCancel)
    {
    } else if (action == MenuAction_Select)
    {
        new client=param1;
        new String:map[PLATFORM_MAX_PATH];
        GetMenuItem(menu, param2, map, sizeof(map));
        Favorite(map, client, false);
    }
}

public GetFavorites(client)
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    new HTTPRequestHandle:request = CreateMapVotesRequest(GET_FAVORITES_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        CReplyToCommand(client, "%t", "url_invalid");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameterInt(request, "player", GetClientUserId(client));
    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SendHTTPRequest(request, ReceiveGetFavorites, GetClientUserId(client));
    StartCooldown(client);
}

public ReceiveGetFavorites(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);
    if(!client)
    {
        //User logged off
        Steam_ReleaseHTTPRequest(request);
        return;
    }
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[MapVotes] Error at RecivedGetFavorites (HTTP Code %d; success %d)", code, successful);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    decl String:data[4096];
    Steam_GetHTTPResponseBodyData(request, data, sizeof(data));
    Steam_ReleaseHTTPRequest(request);

    new Handle:json = json_load(data);
    new Handle:maps = json_object_get(json, "maps");
    new String:map_buffer[PLATFORM_MAX_PATH];
    new junk;
    new bool:found;

    new Handle:menu = CreateMenu(NominateMapHandler, MENU_ACTIONS_DEFAULT|MenuAction_DrawItem|MenuAction_DisplayItem);

    for(new i = 0; i < json_array_size(maps); i++)
    {
        json_array_get_string(maps, i, map_buffer, sizeof(map_buffer));
        TrimString(map_buffer);
        found = GetTrieValue(g_MapTrie, map_buffer, junk);
        if(found)
        {
            AddMenuItem(menu, map_buffer, map_buffer);
        }
    }

    CloseHandle(json);
    CloseHandle(maps);

    //If no maps were found don't even bother displaying a menu
    if(GetMenuItemCount(menu) > 0){
        SetMenuTitle(menu, "Favorited Maps");
        DisplayMenu(menu, client, MENU_TIME_FOREVER);
    }else{
        CReplyToCommand(client, "%t", "no_favorited_maps");
    }
}

public NominateMapHandler(Handle:menu, MenuAction:action, param1, param2)
{

    decl result;

    // Start function call
    Call_StartFunction(g_nominations, g_Handler_MapSelectMenu);

    // Push parameters one at a time
    Call_PushCell(menu);
    Call_PushCell(action);
    Call_PushCell(param1);
    Call_PushCell(param2);

    // Finish the call, get the result
    Call_Finish(result);

    return result;
}

public CallVoteOnClient(client)
{
    new Handle:menu = CreateMenu(VoteMenuHandler);
    SetMenuTitle(menu, "Do you like this map?");
    AddMenuItem(menu, "1","Like it.");
    AddMenuItem(menu, "-1","Hate it.");
    AddMenuItem(menu, "0","I have no strong feelings one way or the other.");
    DisplayMenu(menu, client, 20);
}

public VoteMenuHandler(Handle:menu, MenuAction:action, param1, param2)
{
    if (action == MenuAction_End)
    {
        CloseHandle(menu);
    } else if (action == MenuAction_VoteCancel)
    {
    } else if (action == MenuAction_Select)
    {
        new String:info[32];
        GetMenuItem(menu, param2, info, sizeof(info));
        new value = StringToInt(info);

        CastVote(param1, value);
    }
}

public HaveNotVoted(caller)
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    decl String:map[PLATFORM_MAX_PATH];
    decl String:tmp[64];
    new player;
    new HTTPRequestHandle:request = CreateMapVotesRequest(HAVE_NOT_VOTED_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        CReplyToCommand(caller, "%t", "url_invalid");
        return;
    }

    GetCurrentMap(map, sizeof(map));
    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);

    new i = 0;
    for (new client=1; client <= MaxClients; client++)
    {
        if (!IsClientInGame(client) || IsFakeClient(client))
        {
            continue;
        }
        Steam_GetCSteamIDForClient(client, uid, sizeof(uid));
        player = GetClientUserId(client);

        Format(tmp, sizeof(tmp), "uids[%d]", i);
        Steam_SetHTTPRequestGetOrPostParameter(request, "uids[]", uid);
        Format(tmp, sizeof(tmp), "players[%d]", i);
        Steam_SetHTTPRequestGetOrPostParameterInt(request, "players[]", player);
        i++;
    }

    new userid=GetClientUserId(caller);
    Steam_SendHTTPRequest(request, ReceiveHaveNotVoted, userid);
}

public ReceiveHaveNotVoted(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[MapVotes] Error at RecivedHaveNotVoted (HTTP Code %d)", code);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    decl String:data[4096];
    Steam_GetHTTPResponseBodyData(request, data, sizeof(data));
    Steam_ReleaseHTTPRequest(request);

    new Handle:json = json_load(data);
    new Handle:players = json_object_get(json, "players");
    decl p;

    new count=json_array_size(players);
    if(client)
    {
        CPrintToChat(client, "%t", "have_not_voted", count);
    }

    for(new i = 0; i < count; i++)
    {
        p = json_array_get_int(players, i);
        CallVoteOnClient(GetClientOfUserId(p));
    }
    CloseHandle(json);
    CloseHandle(players);
}

public UpdateMapPlayTime(start_time)
{
    new time_played = GetTime() - start_time;

    //Reject way off values
    if(time_played < 0 || time_played > 86400000)
    {
        LogError("[MapVotes] cannot update map time played; time_played=%d", time_played);
        return;
    }

    decl String:map[PLATFORM_MAX_PATH];
    GetCurrentMap(map, sizeof(map));

    new HTTPRequestHandle:request = CreateMapVotesRequest(UPDATE_MAP_PLAY_TIME_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        LogError("[MapVotes] sm_map_votes_url invalid; cannot create HTTP request");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "time_played", time_played);
    Steam_SendHTTPRequest(request, ReceiveUpdateMapPlayTime);
}

public ReceiveUpdateMapPlayTime(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[MapVotes] Error at ReceiveUpdateMapPlayTime (HTTP Code %d; success %d)", code, successful);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    Steam_ReleaseHTTPRequest(request);
}

public ViewMap(client)
{
    decl String:map[PLATFORM_MAX_PATH], String:url[256], String:base_url[128];
    GetCurrentMap(map, sizeof(map));
    GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));

    TrimString(base_url);
    new trim_length = strlen(base_url) - 1;

    if(base_url[trim_length] == '/')
    {
        strcopy(base_url, trim_length + 1, base_url);
    }

    Format(url, sizeof(url),
            "%s%s/%s", base_url, MAPS_ROUTE, map);

    ShowMOTDPanel(client, "Map Viewer", url, MOTDPANEL_TYPE_URL);

}

//TODO - delete everything below
public Action:Command_Test(client, args)
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    decl String:map[PLATFORM_MAX_PATH];
    new player;
    new HTTPRequestHandle:request = CreateMapVotesRequest(HAVE_NOT_VOTED_ROUTE);

    GetCurrentMap(map, sizeof(map));
    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);

    Steam_SetHTTPRequestGetOrPostParameter(request, "uids[0]", "76561198013926395");
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "players[0]", 1);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uids[1]", "76561198005175623");
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "players[1]", 2);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uids[2]", "76561198013600577");
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "players[2]", 6);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uids[3]", "76561198055205178");
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "players[3]", 17);

    new userid= client ? GetClientUserId(client) : 0;
    Steam_SendHTTPRequest(request, ReceiveTest, userid);

    return Plugin_Handled;
}
public ReceiveTest(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[MapVotes] Error at RecivedHaveNotVoted (HTTP Code %d)", code);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    decl String:data[4096];
    Steam_GetHTTPResponseBodyData(request, data, sizeof(data));

    new Handle:json = json_load(data);
    new Handle:players = json_object_get(json, "players");
    decl p;

    new count=json_array_size(players);
    CPrintToChatAll("%t", "have_not_voted", count);
    CPrintToChatAll("%s", data);
    PrintToConsole(0, "%s", data);

    CloseHandle(json);
    CloseHandle(players);
    Steam_ReleaseHTTPRequest(request);
}