Do not apply the LMS critboost unless the LMS is outnumbered 3 to 1

If the opposing team has fewer than three players, the LMS will not
receive a critboost.

/**
 * Copyright © 2014 awk
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#pragma semicolon 1

#define PLUGIN_VERSION "1.1.3-beta"

#include <sourcemod>
#include <sdktools>
#include <tf2>
#include <tf2_stocks>
#include <smlib>

public Plugin:myinfo = {

  name        = "Arena: Respawn",
  author      = "awk",
  description = "A gamemode that gives Arena second chances.",
  version     = PLUGIN_VERSION,
  url         = "http://steam.respawn.tf/"

}

new Handle:cvar_first_blood = INVALID_HANDLE;
new Handle:cvar_max_streak = INVALID_HANDLE;
new Handle:cvar_cap_enable_time = INVALID_HANDLE;
new Handle:cvar_caplinear = INVALID_HANDLE;
new Handle:cvar_invuln_time = INVALID_HANDLE;
new Handle:cvar_cap_time = INVALID_HANDLE;
new Handle:cvar_first_blood_threshold = INVALID_HANDLE;
new Handle:cvar_lms_critboost = INVALID_HANDLE;
new Handle:cvar_lms_minicrits = INVALID_HANDLE;
new Handle:cvar_logging = INVALID_HANDLE;
new Handle:cvar_force_arena = INVALID_HANDLE;

new cap_owner = 0;
new mid_index = 2;
new num_revived_players = 0;
new Float:last_cap_time = 0.0;
new bool:client_on_point[MAXPLAYERS+1] = { false, ... };
new bool:client_is_marked[MAXPLAYERS+1] = { false, ... };
new Handle:client_mark_timer[MAXPLAYERS+1] = { INVALID_HANDLE, ... };

// Hide these cvar changes, as they're handled by the plugin itself.
new String:quiet_cvars[][] = { "tf_arena_first_blood", "tf_arena_max_streak", "tf_caplinear" };

// Preserve these variables across players' lives, but only if they're playing the same class.
new String:preserved_int_names[][] = { "m_iKillStreak", "m_iDecapitations", "m_iRevengeCrits" };
new preserved_ints[MAXPLAYERS+1][sizeof(preserved_int_names)];
new String:preserved_float_names[][] = { "m_flRageMeter", "m_flHypeMeter" };
new Float:preserved_floats[MAXPLAYERS+1][sizeof(preserved_float_names)];

// Sound that plays to players whose team has respawned (and neutral spectators).
new String:sound_friendly_cap[] = "mvm/mvm_revive.wav";

// Sound that plays to players when the enemy team has respawned.
new String:sound_enemy_cap[] = "misc/ks_tier_03_kill_01.wav";

// Storage for the last played class of each player.
new TFClassType:client_last_class[MAXPLAYERS+1] = { TFClass_Unknown, ... };

// Load Respawn-specific functions
#include <respawn>

public OnPluginStart() {

  HookEntityOutput("tf_gamerules", "OnWonByTeam1", OnWin);
  HookEntityOutput("tf_gamerules", "OnWonByTeam2", OnWin);
  HookEntityOutput("trigger_capture_area", "OnCapTeam1", OnTeamCapture);
  HookEntityOutput("trigger_capture_area", "OnCapTeam2", OnTeamCapture);
  HookEntityOutput("trigger_capture_area", "OnStartTouch", OnPointTouch);
  HookEntityOutput("trigger_capture_area", "OnEndTouch", OnPointTouch);

  HookEvent("server_cvar", SilenceConVarChange, EventHookMode_Pre);
  HookEvent("teamplay_round_start", OnRoundStart);
  HookEvent("teamplay_broadcast_audio", OnTeamAudio, EventHookMode_Pre);
  HookEvent("teamplay_point_captured", OnPointCaptured);
  HookEvent("arena_round_start", OnArenaStart);
  HookEvent("player_death", OnPlayerDeath);

  cvar_first_blood = FindConVar("tf_arena_first_blood");
  cvar_max_streak = FindConVar("tf_arena_max_streak");
  cvar_cap_enable_time = FindConVar("tf_arena_override_cap_enable_time");
  cvar_caplinear = FindConVar("tf_caplinear");

  cvar_logging = CreateConVar("ars_log_enabled", "1",
    "1 to enable status messages in the console, 0 to disable.");

  cvar_force_arena = CreateConVar("ars_force_arena", "0",
    "(Experimental) Set to 1 to attempt to force any map into Arena mode.");

  cvar_invuln_time = CreateConVar("ars_invuln_time", "3.0",
    "How long to flash Uber on a newly respawned player.");

  cvar_cap_time = CreateConVar("ars_cap_time", "1.0",
    "Approximate amount of time (in seconds) it takes to capture the central control point.");

  cvar_first_blood_threshold = CreateConVar("ars_first_blood_threshold", "14",
    "Number of active players required to activate first blood (-1 to disable).");

  cvar_lms_critboost = CreateConVar("ars_lms_critboost", "8.0",
    "How many seconds of crit boost the Last Man Standing gets when a player on the opposing team dies or is killed.");

  cvar_lms_minicrits = CreateConVar("ars_lms_minicrits", "0",
    "Set to 1 to turn the Last Man Standing critboost into minicrits instead.");

}

// Fired on map load or on plugin load/reload.
public OnMapStart() {

  mid_index = 2;

  // Load sounds
  PrecacheSound(sound_friendly_cap);
  PrecacheSound(sound_enemy_cap);

}

// Handle cleanup on map end
public OnMapEnd() {

  for (new i = 1; i <= MaxClients; i++) {
    client_mark_timer[i] = INVALID_HANDLE;
  }

}

// Fired when a client connects to the server.
public OnClientPostAdminCheck(client) {

  CreateTimer(1.0, Timer_Credits, client);

}

// Fired on a client leaving the server.
public OnClientDisconnect_Post(client) {

  client_last_class[client] = TFClass_Unknown;
  client_on_point[client] = false;
  client_is_marked[client] = false;

}

// Fired on teamplay_round_start.
public OnRoundStart(Handle:event, const String:name[], bool:hide_broadcast) {

  if (GetConVarInt(cvar_force_arena) > 0) {
    Game_ForceArenaMode();
  }

  Game_ResetRoundState();

  if (Game_CountCapPoints() > 1) {
    SetConVarInt(cvar_cap_enable_time, -1);
    SetConVarInt(cvar_caplinear, 0);
  } else {
    SetConVarInt(cvar_caplinear, 1);
  }

  if (Game_CountCapPoints() == 5) {
    Log("Chosen midpoint: %d", mid_index);
    Game_SetupSpawns_FiveCp();
    for (new i = 1; i <= MaxClients; i++) {
      CreateTimer(0.01, Timer_RespawnPlayer, i);
    }
  }

}

// Fired after the gates open in Arena.
public OnArenaStart(Handle:event, const String:name[], bool:hide_broadcast) {

  Game_RegeneratePlayers();
  Game_SetupCapPoints();

  // Make our own round timer, if applicable.
  Game_CreateCapTimer();

  // Clear all MFD in case anything is lingering (if a round suddenly restarted, for example).
  for (new i = 1; i <= MaxClients; i++) {
    if (IsValidClient(i) && (GetClientTeam(i) == _:TFTeam_Red || GetClientTeam(i) == _:TFTeam_Blue)) {
      TF2_RemoveCondition(i, TFCond_MarkedForDeath);
    }
  }

  new num_players = Game_CountActivePlayers();

  Log("Round started with %d players.", num_players);

  new fb_threshold = GetConVarInt(cvar_first_blood_threshold);

  if (num_players < 6) {
    SetConVarInt(cvar_first_blood, 0);
    Client_PrintToChatAll(false, "{G}Small match rules active - capture the point for overheal!");
  } else if (num_players < fb_threshold || fb_threshold == -1 || Game_CountCapPoints() > 1) {
    SetConVarInt(cvar_first_blood, 0);
  } else {
    SetConVarInt(cvar_first_blood, 1);
    Client_PrintToChatAll(false, "{G}First blood critboost enabled!");
  }

}

// Fired by a trigger_capture_area on team capture.
public OnTeamCapture(const String:output[], caller, activator, Float:delay) {

  // Set the last capture time.
  last_cap_time = GetGameTime();

  // Determine the team that triggered this capture.
  new team = 0;
  if (StrEqual(output, "OnCapTeam1")) {
    team = _:TFTeam_Red;
  } else if (StrEqual(output, "OnCapTeam2")) {
    team = _:TFTeam_Blue;
  }

  if (Game_CountCapPoints() > 1 || cap_owner != team) {
    Log("Point captured by team #%d", team);
    cap_owner = team;

    num_revived_players = 0;
    for (new i = 1; i <= MaxClients ; i++) {
      if (IsValidClient(i)) {
        if (IsClientObserver(i) && GetClientTeam(i) == team) {
          Player_RespawnWithEffects(i);
          num_revived_players++;
        }
      }
    }
  } else {
    Log("Point re-captured by team #%d", team);

    new cap_master = FindEntityByClassname(-1, "team_control_point_master");
    if (IsValidEntity(cap_master)) {
      SetVariantInt(team);
      AcceptEntityInput(cap_master, "SetWinner");
    } else {
      Log("cap_master is not a valid entity (%d).", cap_master);
    }
  }

  // Reset everyone's death marks.
  for (new i = 1; i <= MaxClients; i++) {
    if (IsValidClient(i) && client_is_marked[i]) {
      TF2_RemoveCondition(i, TFCond_MarkedForDeath);
      client_is_marked[i] = false;
    }
  }

  // Set the time to capture.
  CapArea_SetCaptureTime(caller, true);

}

public OnWin(const String:output[], caller, activator, Float:delay) {

  if (Game_CountCapPoints() == 5) {

    // Disable team scramble
    SetConVarInt(cvar_max_streak, 0);

    if (StrEqual(output, "OnWonByTeam1")) {
      mid_index--;
    } else if (StrEqual(output, "OnWonByTeam2")) {
      mid_index++;
    }
    if (mid_index > 3 || mid_index < 1) {
      mid_index = 2;

      // Guarantee team scramble
      SetConVarInt(cvar_max_streak, 1);
    }
  }

  Game_ResetRoundState();

}

// Fired by a trigger_capture_area when a player starts or stops coming into contact with it.
public OnPointTouch(const String:output[], caller, activator, Float:delay) {

  // Ignore if the activator wasn't a client.
  if (!IsValidClient(activator)) {
    return;
  }

  // Ignore if the classname of what was touched is not a trigger_capture_area.
  if (!Entity_ClassNameMatches(caller, "trigger_capture_area")) {
    return;
  }

  // Set the state of the player to be touching or not touching the point.
  if (StrEqual(output, "OnEndTouch")) {
    client_on_point[activator] = false;
  } else {
    client_on_point[activator] = true;
  }

  // Don't bother marking anyone for death if there's more than one point.
  if (Game_CountCapPoints() > 1) {
    return;
  }

  new control_point = CapArea_GetControlPoint(caller);

  if (control_point > 0) {

    // Is the player a member of the team that owns this control point?
    if (GetClientTeam(activator) == Objective_GetPointOwner(GetEntProp(control_point, Prop_Data, "m_iPointIndex"))) {

      // Note: Always reset the timer state!
      if (client_mark_timer[activator] != INVALID_HANDLE) {
        KillTimer(client_mark_timer[activator]);
        client_mark_timer[activator] = INVALID_HANDLE;
      }

      // If so, and they've started touching the point, schedule a death mark.
      if (client_on_point[activator]) {
        new Float:time = (last_cap_time + 2.0) - GetGameTime();
        if (time < 0.0) {
          time = 0.0;
        }
        client_mark_timer[activator] = CreateTimer(time, Timer_Add_Mark, activator, TIMER_FLAG_NO_MAPCHANGE);

      // Otherwise, schedule a death mark removal if they're coming off of the point.
      } else {
        client_mark_timer[activator] = CreateTimer(2.0, Timer_Remove_Mark, activator, TIMER_FLAG_NO_MAPCHANGE);
      }

    }

  } else {

    // If this message appears, the map Respawn is running on could probably do with some special Stripper rules.
    Log("Client %d touched trigger_capture_area not associated with a control point.");

  }

}

// Fired as a netevent when a point has been successfully captured.
// Unlike the team_control_point output, this allows us access to the players who captured the point.
public OnPointCaptured(Handle:event, const String:name[], bool:hide_broadcast) {

  new String:cap_message[256];
  new String:player_name[64];
  new String:player_color[3];

  // Get the players who captured the point.
  new String:cappers[64];
  GetEventString(event, "cappers", cappers, sizeof(cappers));

  // Determine which color to use for highlighting player names in chat.
  new team = GetEventInt(event, "team");

  switch(team) {

    case (_:TFTeam_Red): {
      player_color = "{R}";
    }
    case (_:TFTeam_Blue): {
      player_color = "{B}";
    }
    default: {
      player_color = "{N}";
    }

  }

  for (new i = 0; i < strlen(cappers); i++) {

    new player = cappers[i];
    if (IsValidClient(player)) {

      if (Game_CountCapPoints() == 1) {
        // If this is the only point, mark the capturing players for death.
        if (client_mark_timer[player] != INVALID_HANDLE) {
          KillTimer(client_mark_timer[player]);
          client_mark_timer[player] = INVALID_HANDLE;
        }
        client_mark_timer[player] = CreateTimer(2.0, Timer_Add_Mark, player, TIMER_FLAG_NO_MAPCHANGE);
      }

      // If this is 1v1, overheal the capturing player.
      if (Game_CountActivePlayers() < 6) {
        new Float:health = float(Entity_GetMaxHealth(player));
        Entity_SetHealth(player, RoundToFloor(health * 1.5), true);
      }

      // Add the player name (team-colored) to the capture message.
      GetClientName(player, player_name, sizeof(player_name));
      StrCat(cap_message, sizeof(cap_message), player_color);
      StrCat(cap_message, sizeof(cap_message), player_name);
      if (i != strlen(cappers) - 1) {
        StrCat(cap_message, sizeof(cap_message), ", ");
      }

    }

  }

  // Announce the number of players respawned.
  new String:players_str[16] = "players!";
  if (num_revived_players == 1) {
    players_str = "player!";
  }

  new String:revived_message[64];
  Format(revived_message, sizeof(revived_message), "{N} revived %d %s", num_revived_players, players_str);

  StrCat(cap_message, sizeof(cap_message), revived_message);

  // Only announce and play sounds if at least one player was respawned.
  if (num_revived_players > 0) {
    Client_PrintToChatAll(false, cap_message);

    new enemy_team = Team_EnemyTeam(team);
    Team_EmitSound(_:TFTeam_Unassigned, sound_friendly_cap);
    Team_EmitSound(_:TFTeam_Spectator,  sound_friendly_cap);
    Team_EmitSound(team,                sound_friendly_cap);
    Team_EmitSound(enemy_team,          sound_enemy_cap);
  }

}

// Fired when a player dies or activates their Dead Ringer.
public OnPlayerDeath(Handle:event, const String:name[], bool:hide_broadcast) {

  new victim = GetClientOfUserId(GetEventInt(event, "userid"));
  new team = GetClientTeam(victim);
  new flags = GetEventInt(event, "death_flags");

  // Nobody cares about dead ringers.
  if(!IsValidClient(victim) || flags & 32) {
    return;
  }

  // Store information about the player's state for use upon respawn.
  client_last_class[victim] = TF2_GetPlayerClass(victim);
  for (new i = 0; i < sizeof(preserved_int_names); i++) {
    preserved_ints[victim][i] = GetEntProp(victim, Prop_Send, preserved_int_names[i]);
  }
  for (new i = 0; i < sizeof(preserved_float_names); i++) {
    preserved_floats[victim][i] = GetEntPropFloat(victim, Prop_Send, preserved_float_names[i]);
  }

  // If the opposing team has only one player remaining, grant that opposing player a five-second minicrit boost.
  new enemy_team = Team_EnemyTeam(team);
  new Float:critboost_time = GetConVarFloat(cvar_lms_critboost);
  if (critboost_time > 0.0 && Team_CountAlivePlayers(enemy_team) == 1 && Team_CountAlivePlayers(team) > 2) {
    for (new i = 1; i <= MaxClients; i++) {
      if (IsValidClient(i) && IsPlayerAlive(i) && GetClientTeam(i) == enemy_team) {
        if (GetConVarInt(cvar_lms_minicrits) > 0) {
          TF2_AddCondition(i, TFCond_CritCola, critboost_time);
        } else {
          TF2_AddCondition(i, TFCond_Kritzkrieged, critboost_time);
        }
      }
    }
  }

  // If this team has only one player remaining, play a special voice response.
  if (Team_CountAlivePlayers(team) == 2) {

    Team_PlayVO(team, "Announcer.AM_LastManAlive0%d", GetRandomInt(1,4));

  }

}

public Action:OnTeamAudio(Handle:event, const String:name[], bool:hide_broadcast) {

  new String:vo[64];
  GetEventString(event, "sound", vo, sizeof(vo));

  new team = GetEventInt(event, "team");

  // Special (disappointed) response on a team that lost on a single-cap map with players left alive.
  if (StrEqual(vo, "Game.YourTeamLost") && Team_CountAlivePlayers(team) > 0 && Game_CountCapPoints() == 1) {

    Team_PlayVO(team, "Announcer.AM_LastManForfeit0%d", GetRandomInt(1,4));
    return Plugin_Handled;

  }

  return Plugin_Continue;

}

// Intercepts configuration variable changes and silences some of them.
public Action:SilenceConVarChange(Handle:event, String:name[], bool:hide_broadcast) {

  new String:cvar_name[64];
  GetEventString(event, "cvarname", cvar_name, sizeof(cvar_name));

  for (new i = 0; i < sizeof(quiet_cvars); i++) {
    if (StrEqual(quiet_cvars[i], cvar_name)) {
      SetEventBroadcast(event, true);
      return Plugin_Changed;
    }
  }

  return Plugin_Continue;

}

// Prints a prefixed log to the console.
public Log(const String:to_log[], any:...) {

  if (GetConVarInt(cvar_logging) != 1) {
    return;
  }

  new String:prefixed_string[512];
  new String:log_string[512];
  prefixed_string = "[Arena: Respawn] ";
  StrCat(prefixed_string, sizeof(prefixed_string), to_log);
  VFormat(log_string, sizeof(log_string), prefixed_string, 2);

  PrintToServer(log_string);

}

/**
 * Copyright © 2014 awk
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#pragma semicolon 1

#define PLUGIN_VERSION "1.1.3-beta"

#include <sourcemod>
#include <sdktools>
#include <tf2>
#include <tf2_stocks>
#include <smlib>

public Plugin:myinfo = {

  name        = "Arena: Respawn",
  author      = "awk",
  description = "A gamemode that gives Arena second chances.",
  version     = PLUGIN_VERSION,
  url         = "http://steam.respawn.tf/"

}

new Handle:cvar_first_blood = INVALID_HANDLE;
new Handle:cvar_max_streak = INVALID_HANDLE;
new Handle:cvar_cap_enable_time = INVALID_HANDLE;
new Handle:cvar_caplinear = INVALID_HANDLE;
new Handle:cvar_invuln_time = INVALID_HANDLE;
new Handle:cvar_cap_time = INVALID_HANDLE;
new Handle:cvar_first_blood_threshold = INVALID_HANDLE;
new Handle:cvar_lms_critboost = INVALID_HANDLE;
new Handle:cvar_lms_minicrits = INVALID_HANDLE;
new Handle:cvar_logging = INVALID_HANDLE;
new Handle:cvar_force_arena = INVALID_HANDLE;

new cap_owner = 0;
new mid_index = 2;
new num_revived_players = 0;
new Float:last_cap_time = 0.0;
new bool:client_on_point[MAXPLAYERS+1] = { false, ... };
new bool:client_is_marked[MAXPLAYERS+1] = { false, ... };
new Handle:client_mark_timer[MAXPLAYERS+1] = { INVALID_HANDLE, ... };

// Hide these cvar changes, as they're handled by the plugin itself.
new String:quiet_cvars[][] = { "tf_arena_first_blood", "tf_arena_max_streak", "tf_caplinear" };

// Preserve these variables across players' lives, but only if they're playing the same class.
new String:preserved_int_names[][] = { "m_iKillStreak", "m_iDecapitations", "m_iRevengeCrits" };
new preserved_ints[MAXPLAYERS+1][sizeof(preserved_int_names)];
new String:preserved_float_names[][] = { "m_flRageMeter", "m_flHypeMeter" };
new Float:preserved_floats[MAXPLAYERS+1][sizeof(preserved_float_names)];

// Sound that plays to players whose team has respawned (and neutral spectators).
new String:sound_friendly_cap[] = "mvm/mvm_revive.wav";

// Sound that plays to players when the enemy team has respawned.
new String:sound_enemy_cap[] = "misc/ks_tier_03_kill_01.wav";

// Storage for the last played class of each player.
new TFClassType:client_last_class[MAXPLAYERS+1] = { TFClass_Unknown, ... };

// Load Respawn-specific functions
#include <respawn>

public OnPluginStart() {

  HookEntityOutput("tf_gamerules", "OnWonByTeam1", OnWin);
  HookEntityOutput("tf_gamerules", "OnWonByTeam2", OnWin);
  HookEntityOutput("trigger_capture_area", "OnCapTeam1", OnTeamCapture);
  HookEntityOutput("trigger_capture_area", "OnCapTeam2", OnTeamCapture);
  HookEntityOutput("trigger_capture_area", "OnStartTouch", OnPointTouch);
  HookEntityOutput("trigger_capture_area", "OnEndTouch", OnPointTouch);

  HookEvent("server_cvar", SilenceConVarChange, EventHookMode_Pre);
  HookEvent("teamplay_round_start", OnRoundStart);
  HookEvent("teamplay_broadcast_audio", OnTeamAudio, EventHookMode_Pre);
  HookEvent("teamplay_point_captured", OnPointCaptured);
  HookEvent("arena_round_start", OnArenaStart);
  HookEvent("player_death", OnPlayerDeath);

  cvar_first_blood = FindConVar("tf_arena_first_blood");
  cvar_max_streak = FindConVar("tf_arena_max_streak");
  cvar_cap_enable_time = FindConVar("tf_arena_override_cap_enable_time");
  cvar_caplinear = FindConVar("tf_caplinear");

  cvar_logging = CreateConVar("ars_log_enabled", "1",
    "1 to enable status messages in the console, 0 to disable.");

  cvar_force_arena = CreateConVar("ars_force_arena", "0",
    "(Experimental) Set to 1 to attempt to force any map into Arena mode.");

  cvar_invuln_time = CreateConVar("ars_invuln_time", "3.0",
    "How long to flash Uber on a newly respawned player.");

  cvar_cap_time = CreateConVar("ars_cap_time", "1.0",
    "Approximate amount of time (in seconds) it takes to capture the central control point.");

  cvar_first_blood_threshold = CreateConVar("ars_first_blood_threshold", "14",
    "Number of active players required to activate first blood (-1 to disable).");

  cvar_lms_critboost = CreateConVar("ars_lms_critboost", "8.0",
    "How many seconds of crit boost the Last Man Standing gets when a player on the opposing team dies or is killed.");

  cvar_lms_minicrits = CreateConVar("ars_lms_minicrits", "0",
    "Set to 1 to turn the Last Man Standing critboost into minicrits instead.");

}

// Fired on map load or on plugin load/reload.
public OnMapStart() {

  mid_index = 2;

  // Load sounds
  PrecacheSound(sound_friendly_cap);
  PrecacheSound(sound_enemy_cap);

}

// Handle cleanup on map end
public OnMapEnd() {

  for (new i = 1; i <= MaxClients; i++) {
    client_mark_timer[i] = INVALID_HANDLE;
  }

}

// Fired when a client connects to the server.
public OnClientPostAdminCheck(client) {

  CreateTimer(1.0, Timer_Credits, client);

}

// Fired on a client leaving the server.
public OnClientDisconnect_Post(client) {

  client_last_class[client] = TFClass_Unknown;
  client_on_point[client] = false;
  client_is_marked[client] = false;

}

// Fired on teamplay_round_start.
public OnRoundStart(Handle:event, const String:name[], bool:hide_broadcast) {

  if (GetConVarInt(cvar_force_arena) > 0) {
    Game_ForceArenaMode();
  }

  Game_ResetRoundState();

  if (Game_CountCapPoints() > 1) {
    SetConVarInt(cvar_cap_enable_time, -1);
    SetConVarInt(cvar_caplinear, 0);
  } else {
    SetConVarInt(cvar_caplinear, 1);
  }

  if (Game_CountCapPoints() == 5) {
    Log("Chosen midpoint: %d", mid_index);
    Game_SetupSpawns_FiveCp();
    for (new i = 1; i <= MaxClients; i++) {
      CreateTimer(0.01, Timer_RespawnPlayer, i);
    }
  }

}

// Fired after the gates open in Arena.
public OnArenaStart(Handle:event, const String:name[], bool:hide_broadcast) {

  Game_RegeneratePlayers();
  Game_SetupCapPoints();

  // Make our own round timer, if applicable.
  Game_CreateCapTimer();

  // Clear all MFD in case anything is lingering (if a round suddenly restarted, for example).
  for (new i = 1; i <= MaxClients; i++) {
    if (IsValidClient(i) && (GetClientTeam(i) == _:TFTeam_Red || GetClientTeam(i) == _:TFTeam_Blue)) {
      TF2_RemoveCondition(i, TFCond_MarkedForDeath);
    }
  }

  new num_players = Game_CountActivePlayers();

  Log("Round started with %d players.", num_players);

  new fb_threshold = GetConVarInt(cvar_first_blood_threshold);

  if (num_players < 6) {
    SetConVarInt(cvar_first_blood, 0);
    Client_PrintToChatAll(false, "{G}Small match rules active - capture the point for overheal!");
  } else if (num_players < fb_threshold || fb_threshold == -1 || Game_CountCapPoints() > 1) {
    SetConVarInt(cvar_first_blood, 0);
  } else {
    SetConVarInt(cvar_first_blood, 1);
    Client_PrintToChatAll(false, "{G}First blood critboost enabled!");
  }

}

// Fired by a trigger_capture_area on team capture.
public OnTeamCapture(const String:output[], caller, activator, Float:delay) {

  // Set the last capture time.
  last_cap_time = GetGameTime();

  // Determine the team that triggered this capture.
  new team = 0;
  if (StrEqual(output, "OnCapTeam1")) {
    team = _:TFTeam_Red;
  } else if (StrEqual(output, "OnCapTeam2")) {
    team = _:TFTeam_Blue;
  }

  if (Game_CountCapPoints() > 1 || cap_owner != team) {
    Log("Point captured by team #%d", team);
    cap_owner = team;

    num_revived_players = 0;
    for (new i = 1; i <= MaxClients ; i++) {
      if (IsValidClient(i)) {
        if (IsClientObserver(i) && GetClientTeam(i) == team) {
          Player_RespawnWithEffects(i);
          num_revived_players++;
        }
      }
    }
  } else {
    Log("Point re-captured by team #%d", team);

    new cap_master = FindEntityByClassname(-1, "team_control_point_master");
    if (IsValidEntity(cap_master)) {
      SetVariantInt(team);
      AcceptEntityInput(cap_master, "SetWinner");
    } else {
      Log("cap_master is not a valid entity (%d).", cap_master);
    }
  }

  // Reset everyone's death marks.
  for (new i = 1; i <= MaxClients; i++) {
    if (IsValidClient(i) && client_is_marked[i]) {
      TF2_RemoveCondition(i, TFCond_MarkedForDeath);
      client_is_marked[i] = false;
    }
  }

  // Set the time to capture.
  CapArea_SetCaptureTime(caller, true);

}

public OnWin(const String:output[], caller, activator, Float:delay) {

  if (Game_CountCapPoints() == 5) {

    // Disable team scramble
    SetConVarInt(cvar_max_streak, 0);

    if (StrEqual(output, "OnWonByTeam1")) {
      mid_index--;
    } else if (StrEqual(output, "OnWonByTeam2")) {
      mid_index++;
    }
    if (mid_index > 3 || mid_index < 1) {
      mid_index = 2;

      // Guarantee team scramble
      SetConVarInt(cvar_max_streak, 1);
    }
  }

  Game_ResetRoundState();

}

// Fired by a trigger_capture_area when a player starts or stops coming into contact with it.
public OnPointTouch(const String:output[], caller, activator, Float:delay) {

  // Ignore if the activator wasn't a client.
  if (!IsValidClient(activator)) {
    return;
  }

  // Ignore if the classname of what was touched is not a trigger_capture_area.
  if (!Entity_ClassNameMatches(caller, "trigger_capture_area")) {
    return;
  }

  // Set the state of the player to be touching or not touching the point.
  if (StrEqual(output, "OnEndTouch")) {
    client_on_point[activator] = false;
  } else {
    client_on_point[activator] = true;
  }

  // Don't bother marking anyone for death if there's more than one point.
  if (Game_CountCapPoints() > 1) {
    return;
  }

  new control_point = CapArea_GetControlPoint(caller);

  if (control_point > 0) {

    // Is the player a member of the team that owns this control point?
    if (GetClientTeam(activator) == Objective_GetPointOwner(GetEntProp(control_point, Prop_Data, "m_iPointIndex"))) {

      // Note: Always reset the timer state!
      if (client_mark_timer[activator] != INVALID_HANDLE) {
        KillTimer(client_mark_timer[activator]);
        client_mark_timer[activator] = INVALID_HANDLE;
      }

      // If so, and they've started touching the point, schedule a death mark.
      if (client_on_point[activator]) {
        new Float:time = (last_cap_time + 2.0) - GetGameTime();
        if (time < 0.0) {
          time = 0.0;
        }
        client_mark_timer[activator] = CreateTimer(time, Timer_Add_Mark, activator, TIMER_FLAG_NO_MAPCHANGE);

      // Otherwise, schedule a death mark removal if they're coming off of the point.
      } else {
        client_mark_timer[activator] = CreateTimer(2.0, Timer_Remove_Mark, activator, TIMER_FLAG_NO_MAPCHANGE);
      }

    }

  } else {

    // If this message appears, the map Respawn is running on could probably do with some special Stripper rules.
    Log("Client %d touched trigger_capture_area not associated with a control point.");

  }

}

// Fired as a netevent when a point has been successfully captured.
// Unlike the team_control_point output, this allows us access to the players who captured the point.
public OnPointCaptured(Handle:event, const String:name[], bool:hide_broadcast) {

  new String:cap_message[256];
  new String:player_name[64];
  new String:player_color[3];

  // Get the players who captured the point.
  new String:cappers[64];
  GetEventString(event, "cappers", cappers, sizeof(cappers));

  // Determine which color to use for highlighting player names in chat.
  new team = GetEventInt(event, "team");

  switch(team) {

    case (_:TFTeam_Red): {
      player_color = "{R}";
    }
    case (_:TFTeam_Blue): {
      player_color = "{B}";
    }
    default: {
      player_color = "{N}";
    }

  }

  for (new i = 0; i < strlen(cappers); i++) {

    new player = cappers[i];
    if (IsValidClient(player)) {

      if (Game_CountCapPoints() == 1) {
        // If this is the only point, mark the capturing players for death.
        if (client_mark_timer[player] != INVALID_HANDLE) {
          KillTimer(client_mark_timer[player]);
          client_mark_timer[player] = INVALID_HANDLE;
        }
        client_mark_timer[player] = CreateTimer(2.0, Timer_Add_Mark, player, TIMER_FLAG_NO_MAPCHANGE);
      }

      // If this is 1v1, overheal the capturing player.
      if (Game_CountActivePlayers() < 6) {
        new Float:health = float(Entity_GetMaxHealth(player));
        Entity_SetHealth(player, RoundToFloor(health * 1.5), true);
      }

      // Add the player name (team-colored) to the capture message.
      GetClientName(player, player_name, sizeof(player_name));
      StrCat(cap_message, sizeof(cap_message), player_color);
      StrCat(cap_message, sizeof(cap_message), player_name);
      if (i != strlen(cappers) - 1) {
        StrCat(cap_message, sizeof(cap_message), ", ");
      }

    }

  }

  // Announce the number of players respawned.
  new String:players_str[16] = "players!";
  if (num_revived_players == 1) {
    players_str = "player!";
  }

  new String:revived_message[64];
  Format(revived_message, sizeof(revived_message), "{N} revived %d %s", num_revived_players, players_str);

  StrCat(cap_message, sizeof(cap_message), revived_message);

  // Only announce and play sounds if at least one player was respawned.
  if (num_revived_players > 0) {
    Client_PrintToChatAll(false, cap_message);

    new enemy_team = Team_EnemyTeam(team);
    Team_EmitSound(_:TFTeam_Unassigned, sound_friendly_cap);
    Team_EmitSound(_:TFTeam_Spectator,  sound_friendly_cap);
    Team_EmitSound(team,                sound_friendly_cap);
    Team_EmitSound(enemy_team,          sound_enemy_cap);
  }

}

// Fired when a player dies or activates their Dead Ringer.
public OnPlayerDeath(Handle:event, const String:name[], bool:hide_broadcast) {

  new victim = GetClientOfUserId(GetEventInt(event, "userid"));
  new team = GetClientTeam(victim);
  new flags = GetEventInt(event, "death_flags");

  // Nobody cares about dead ringers.
  if(!IsValidClient(victim) || flags & 32) {
    return;
  }

  // Store information about the player's state for use upon respawn.
  client_last_class[victim] = TF2_GetPlayerClass(victim);
  for (new i = 0; i < sizeof(preserved_int_names); i++) {
    preserved_ints[victim][i] = GetEntProp(victim, Prop_Send, preserved_int_names[i]);
  }
  for (new i = 0; i < sizeof(preserved_float_names); i++) {
    preserved_floats[victim][i] = GetEntPropFloat(victim, Prop_Send, preserved_float_names[i]);
  }

  // If the opposing team has only one player remaining, grant that opposing player a five-second minicrit boost.
  new enemy_team = Team_EnemyTeam(team);
  new Float:critboost_time = GetConVarFloat(cvar_lms_critboost);
  if (critboost_time > 0.0 && Team_CountAlivePlayers(enemy_team) == 1) {
    for (new i = 1; i <= MaxClients; i++) {
      if (IsValidClient(i) && IsPlayerAlive(i) && GetClientTeam(i) == enemy_team) {
        if (GetConVarInt(cvar_lms_minicrits) > 0) {
          TF2_AddCondition(i, TFCond_CritCola, critboost_time);
        } else {
          TF2_AddCondition(i, TFCond_Kritzkrieged, critboost_time);
        }
      }
    }
  }

  // If this team has only one player remaining, play a special voice response.
  if (Team_CountAlivePlayers(team) == 2) {

    Team_PlayVO(team, "Announcer.AM_LastManAlive0%d", GetRandomInt(1,4));

  }

}

public Action:OnTeamAudio(Handle:event, const String:name[], bool:hide_broadcast) {

  new String:vo[64];
  GetEventString(event, "sound", vo, sizeof(vo));

  new team = GetEventInt(event, "team");

  // Special (disappointed) response on a team that lost on a single-cap map with players left alive.
  if (StrEqual(vo, "Game.YourTeamLost") && Team_CountAlivePlayers(team) > 0 && Game_CountCapPoints() == 1) {

    Team_PlayVO(team, "Announcer.AM_LastManForfeit0%d", GetRandomInt(1,4));
    return Plugin_Handled;

  }

  return Plugin_Continue;

}

// Intercepts configuration variable changes and silences some of them.
public Action:SilenceConVarChange(Handle:event, String:name[], bool:hide_broadcast) {

  new String:cvar_name[64];
  GetEventString(event, "cvarname", cvar_name, sizeof(cvar_name));

  for (new i = 0; i < sizeof(quiet_cvars); i++) {
    if (StrEqual(quiet_cvars[i], cvar_name)) {
      SetEventBroadcast(event, true);
      return Plugin_Changed;
    }
  }

  return Plugin_Continue;

}

// Prints a prefixed log to the console.
public Log(const String:to_log[], any:...) {

  if (GetConVarInt(cvar_logging) != 1) {
    return;
  }

  new String:prefixed_string[512];
  new String:log_string[512];
  prefixed_string = "[Arena: Respawn] ";
  StrCat(prefixed_string, sizeof(prefixed_string), to_log);
  VFormat(log_string, sizeof(log_string), prefixed_string, 2);

  PrintToServer(log_string);

}


Get nextmap before playing map theme

Get nextmap before playing map theme

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio_map_change
 * Plays a song to all clients just before the map ends so they can listen to
 * music while the next map loads.  Also triggers when the next map vote starts
 * when a RockTheVote is called.
 *
 * Copyright 2013 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <in_game_audio>

#define PLUGIN_VERSION "1.7.0"
#define PLUGIN_NAME "In Game Audio Map Change"

public Plugin:myinfo =
{
    name = PLUGIN_NAME,
    author = "CrimsonTautology",
    description = "Play a song during map change",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

public OnPluginStart()
{
    LoadTranslations("in_game_audio.phrases");

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

    HookEvent("teamplay_game_over", Event_MapChange);
}

public OnMapVoteStarted()
{
    //Don't let the map vote override a pall
    MapTheme(false);
}

public Action:Event_MapChange(Handle:event, const String:name[], bool:dontBroadcast)
{
    decl String:next_map[64]
    GetNextMap(next_map, sizeof(next_map))
    MapTheme(true, next_map);

    return Plugin_Continue;
}

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio_map_change
 * Plays a song to all clients just before the map ends so they can listen to
 * music while the next map loads.  Also triggers when the next map vote starts
 * when a RockTheVote is called.
 *
 * Copyright 2013 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <in_game_audio>

#define PLUGIN_VERSION "1.7.0"
#define PLUGIN_NAME "In Game Audio Map Change"

public Plugin:myinfo =
{
    name = PLUGIN_NAME,
    author = "CrimsonTautology",
    description = "Play a song during map change",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

public OnPluginStart()
{
    LoadTranslations("in_game_audio.phrases");

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

    HookEvent("teamplay_game_over", Event_MapChange);
}

public OnMapVoteStarted()
{
    //Don't let the map vote override a pall
    MapTheme(false);
}

public Action:Event_MapChange(Handle:event, const String:name[], bool:dontBroadcast)
{
    //TODO get next map
    MapTheme();
    return Plugin_Continue;
}


Make nominations plugin a cvar so modified plugins can be used

/**
 * 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 <socket>
#include <base64>
#include <json>

#undef REQUIRE_EXTENSIONS
#include <smjansson>

#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 MAPS_ROUTE "/maps"

#define MAX_STEAMID_LENGTH 21
#define MAX_COMMUNITYID_LENGTH 18

new Handle:g_Cvar_MapVotesUrl = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesPort = 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 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;

new bool:g_JanssonEnabled = false;

public OnPluginStart()
{

    g_Cvar_MapVotesUrl = CreateConVar("sm_map_votes_url", "", "URL to your Map Votes web page");
    g_Cvar_MapVotesPort = CreateConVar("sm_map_votes_port", "80", "HTTP Port used");
    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");

    RegConsoleCmd("sm_vote_menu", Command_VoteMenu, "Bring up a menu to vote on the current map");
    RegConsoleCmd("sm_vote_up", Command_VoteUp, "Vote that you like the current map");
    RegConsoleCmd("sm_vote_down", Command_VoteDown, "Vote that you hate the current map");

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

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

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

    RegConsoleCmd("sm_call_vote", Command_CallVote, "Popup a vote panel to every player on the server that has not yet voted on this map");

    RegConsoleCmd("test", Test);

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

public OnConfigsExecuted()
{
    BuildMapListAndTrie();
}

public OnAllPluginsLoaded() {
    if (LibraryExists("jansson")) {
        g_JanssonEnabled = true;
    }

    new noms = GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));
    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 OnLibraryAdded(const String:name[]) {
    if (StrEqual(name, "jansson")) {
        g_JanssonEnabled = true;
    }
}

public OnLibraryRemoved(const String:name[]) {
    if (StrEqual(name, "jansson")) {
        g_JanssonEnabled = false;
    }
}

public Action:Command_VoteMenu(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        CallVoteOnClient(client);
    }
}

public Action:Command_VoteUp(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        CastVote(client, 1);
    }
}

public Action:Command_VoteDown(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        CastVote(client, -1);
    }
}

public Action:Command_Favorite(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        Favorite(client, true);
    }
}
public Action:Command_Unfavorite(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        Favorite(client, false);
    }
}
public Action:Command_GetFavorites(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        GetFavorites(client);
    }
}

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

public Action:Command_MapComment(client, args)
{
    if (!GetConVarBool(g_Cvar_MapVotesCommentingEnabled))
    {
        return Plugin_Handled;
    }

    if (args < 1)
    {
        ReplyToCommand(client, "[MapVotes] Usage: !map_comment <comment>");
        return Plugin_Handled;
    }

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

    return Plugin_Handled;
}

public Action:Command_CallVote(client, args)
{
    //TODO
}

public OnSocketConnected(Handle:socket, any:headers_pack)
{
    decl String:request_string[1024];

    ResetPack(headers_pack);
    ReadPackString(headers_pack, request_string, sizeof(request_string));

    SocketSend(socket, request_string);
}

public OnSocketReceive(Handle:socket, String:receive_data[], const data_size, any:headers_pack) {
    //Used for data received back
    if(g_JanssonEnabled)
    {
        //TODO parse JSON response
        //PrintToConsole(0,"%s", receive_data);//TODO

        new String:raw[2][1024], String:line[2][1024];
        ExplodeString(receive_data, "\r\n\r\n", raw, sizeof(raw), sizeof(raw[]));
        ExplodeString(raw[1], "\r\n", line, sizeof(line), sizeof(line[]));
        //PrintToConsole(0,"%s", line[1]);//TODO

        new Handle:json = json_load(line[1]);
        new String:command[1024];
        //TODO have way to handle missing value
        json_object_get_string(json, "command", command, sizeof(command));
        //PrintToConsole(0,"%s", command);//TODO

        //TODO have integer based commands
        if(strcmp(command, "get_favorites") == 0)
        {
            ParseGetFavorites(json);
        }
        if(strcmp(command, "have_not_voted") == 0)
        {
            ParseHaveNotVoted(json);
        }

    } else
    {
        PrintToConsole(0,"Cannot parse JSON; SMJannson not installed");
    }
}

public OnSocketDisconnected(Handle:socket, any:headers_pack) {
    // Connection: close advises the webserver to close the connection when the transfer is finished
    // we're done here
    CloseHandle(headers_pack);
    CloseHandle(socket);
}

public OnSocketError(Handle:socket, const error_type, const error_num, any:headers_pack) {
    // a socket error occured
    if(error_type == EMPTY_HOST )
    {
        LogError("[MapVotes] Empty Host (errno %d)", error_num);
    } else if (error_type == NO_HOST )
    {
        LogError("[MapVotes] No Host (errno %d)", error_num);
    } else if (error_type == CONNECT_ERROR )
    {
        LogError("[MapVotes] Connection Error (errno %d)", error_num);
    } else if (error_type == SEND_ERROR )
    {
        LogError("[MapVotes] Send Error (errno %d)", error_num);
    } else if (error_type == BIND_ERROR )
    {
        LogError("[MapVotes] Bind Error (errno %d)", error_num);
    } else if (error_type == RECV_ERROR )
    {
        LogError("[MapVotes] Recieve Error (errno %d)", error_num);
    } else if (error_type == LISTEN_ERROR )
    {
        LogError("[MapVotes] Listen Error (errno %d)", error_num);
    } else
    {
        LogError("[MapVotes] socket error %d (errno %d)", error_type, error_num);
    }

    CloseHandle(headers_pack);
    CloseHandle(socket);
}

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

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

//By 11530
//GetSteamAccountID(client) does not work because we don't have 64 bit types
stock bool:GetCommunityIDString(const String:SteamID[], String:CommunityID[], const CommunityIDSize)
{
    decl String:SteamIDParts[3][11];
    new const String:Identifier[] = "76561197960265728";

    if ((CommunityIDSize < 1) || (ExplodeString(SteamID, ":", SteamIDParts, sizeof(SteamIDParts), sizeof(SteamIDParts[])) != 3))
    {
        CommunityID[0] = '\0';
        return false;
    }

    new Current, CarryOver = (SteamIDParts[1][0] == '1');
    for (new i = (CommunityIDSize - 2), j = (strlen(SteamIDParts[2]) - 1), k = (strlen(Identifier) - 1); i >= 0; i--, j--, k--)
    {
        Current = (j >= 0 ? (2 * (SteamIDParts[2][j] - '0')) : 0) + CarryOver + (k >= 0 ? ((Identifier[k] - '0') * 1) : 0);
        CarryOver = Current / 10;
        CommunityID[i] = (Current % 10) + '0';
    }

    CommunityID[CommunityIDSize - 1] = '\0';
    return true;
}

public MapVotesCall(String:route[128], String:query_params[512])
{
    new port= GetConVarInt(g_Cvar_MapVotesPort);
    decl String:base_url[128], String:api_key[128];
    GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));
    GetConVarString(g_Cvar_MapVotesApiKey, api_key, sizeof(api_key));

    ReplaceString(base_url, sizeof(base_url), "http://", "", false);
    ReplaceString(base_url, sizeof(base_url), "https://", "", false);

    Format(query_params, sizeof(query_params), "%s&access_token=%s", query_params, api_key);

    HTTPPost(base_url, route, query_params, port);
}

public HTTPPost(String:base_url[128], String:route[128], String:query_params[512], port)
{
    new Handle:socket = SocketCreate(SOCKET_TCP, OnSocketError);

    //This Formats the headers needed to make a HTTP/1.1 POST request.
    new String:request_string[1024];
    Format(request_string, sizeof(request_string),
            "POST %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\nContent-type: application/x-www-form-urlencoded\r\nContent-length: %d\r\n\r\n%s",
            route,
            base_url,
            strlen(query_params),
            query_params);

    new Handle:headers_pack = CreateDataPack();
    WritePackString(headers_pack, request_string);
    SocketSetArg(socket, headers_pack);

    SocketConnect(socket, OnSocketConnected, OnSocketReceive, OnSocketDisconnected, base_url, port);
}
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:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));

    decl String:query_params[512], String:map[PLATFORM_MAX_PATH];
    GetCurrentMap(map, sizeof(map));
    Format(query_params, sizeof(query_params),
            "map=%s&uid=%s&comment=%s&base64=true", map, uid, base64);

    MapVotesCall(WRITE_MESSAGE_ROUTE, query_params);
}

public CastVote(client, value)
{
    decl String:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));

    if(!(value == -1 || value == 0 || value == 1)){
        LogError("[MapVotes] invalid vote value %d (steam_user: %s)", value, uid);
    }else{
        decl String:query_params[512], String:map[PLATFORM_MAX_PATH];

        GetCurrentMap(map, sizeof(map));

        Format(query_params, sizeof(query_params),
                "map=%s&uid=%s&value=%d", map, uid, value);

        MapVotesCall(CAST_VOTE_ROUTE, query_params);
    }

}

public Favorite(client, bool:favorite)
{
    decl String:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));
    decl String:query_params[512], String:map[PLATFORM_MAX_PATH];

    GetCurrentMap(map, sizeof(map));

    Format(query_params, sizeof(query_params),
            "map=%s&uid=%s", map, uid);

    if(favorite)
    {
        MapVotesCall(FAVORITE_ROUTE, query_params);
    }else{
        MapVotesCall(UNFAVORITE_ROUTE, query_params);
    }
}

public GetFavorites(client)
{
    decl String:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));
    decl String:query_params[512];

    //NOTE: uid is the client's steamid64 while player is the client's userid; the index incremented for each client that joined the server
    Format(query_params, sizeof(query_params),
            "player=%i&uid=%s", GetClientUserId(client), uid);

    MapVotesCall(GET_FAVORITES_ROUTE, query_params);
}

public ParseGetFavorites(Handle:json)
{
    new player = json_object_get_int(json, "player");
    new client = GetClientOfUserId(player);

    new Handle:maps = json_object_get(json, "maps");
    new String:map_buffer[128];

    new Handle:menu = CreateMenu(NominateMapHandler);

    for(new i = 0; i < json_array_size(maps); i++)
    {
        json_array_get_string(maps, i, map_buffer, sizeof(map_buffer));
        AddMenuItem(menu, map, map);
        //PrintToChat(GetClientOfUserId(player), "%s", map_buffer);
    }

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

}

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 ParseHaveNotVoted(Handle:json)
{
    new Handle:players = json_object_get(json, "players");
    new p;
    new String:map_buffer[PLATFORM_MAX_PATH];

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

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));
    ReplaceString(base_url, sizeof(base_url), "http://", "", false);
    ReplaceString(base_url, sizeof(base_url), "https://", "", false);

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

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

}

public ServerQuery()
{
    //TODO
}

public Action:Test(client, args)
{
    decl String:query_params[512];

    //NOTE: uid is the client's steamid64 while player is the client's userid; the index incremented for each client that joined the server
    Format(query_params, sizeof(query_params),
            "player=%i&uid=%s", 7, "76561197998903004");

    MapVotesCall(GET_FAVORITES_ROUTE, query_params);
}

/**
 * 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 <socket>
#include <base64>
#include <json>

#undef REQUIRE_EXTENSIONS
#include <smjansson>

#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 MAPS_ROUTE "/maps"

#define MAX_STEAMID_LENGTH 21
#define MAX_COMMUNITYID_LENGTH 18

new Handle:g_Cvar_MapVotesUrl = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesPort = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesApiKey = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesVotingEnabled = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesCommentingEnabled = INVALID_HANDLE;

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;

new bool:g_JanssonEnabled = false;

public OnPluginStart()
{

    g_Cvar_MapVotesUrl = CreateConVar("sm_map_votes_url", "", "URL to your Map Votes web page");
    g_Cvar_MapVotesPort = CreateConVar("sm_map_votes_port", "80", "HTTP Port used");
    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");

    RegConsoleCmd("sm_vote_menu", Command_VoteMenu, "Bring up a menu to vote on the current map");
    RegConsoleCmd("sm_vote_up", Command_VoteUp, "Vote that you like the current map");
    RegConsoleCmd("sm_vote_down", Command_VoteDown, "Vote that you hate the current map");

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

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

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

    RegConsoleCmd("sm_call_vote", Command_CallVote, "Popup a vote panel to every player on the server that has not yet voted on this map");

    RegConsoleCmd("test", Test);

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

public OnConfigsExecuted()
{
    BuildMapListAndTrie();
}

public OnAllPluginsLoaded() {
    if (LibraryExists("jansson")) {
        g_JanssonEnabled = true;
    }

    g_nominations = FindPluginByFile("nominations.smx");

    //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 OnLibraryAdded(const String:name[]) {
    if (StrEqual(name, "jansson")) {
        g_JanssonEnabled = true;
    }
}

public OnLibraryRemoved(const String:name[]) {
    if (StrEqual(name, "jansson")) {
        g_JanssonEnabled = false;
    }
}

public Action:Command_VoteMenu(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        CallVoteOnClient(client);
    }
}

public Action:Command_VoteUp(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        CastVote(client, 1);
    }
}

public Action:Command_VoteDown(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        CastVote(client, -1);
    }
}

public Action:Command_Favorite(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        Favorite(client, true);
    }
}
public Action:Command_Unfavorite(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        Favorite(client, false);
    }
}
public Action:Command_GetFavorites(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        GetFavorites(client);
    }
}

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

public Action:Command_MapComment(client, args)
{
    if (!GetConVarBool(g_Cvar_MapVotesCommentingEnabled))
    {
        return Plugin_Handled;
    }

    if (args < 1)
    {
        ReplyToCommand(client, "[MapVotes] Usage: !map_comment <comment>");
        return Plugin_Handled;
    }

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

    return Plugin_Handled;
}

public Action:Command_CallVote(client, args)
{
    //TODO
}

public OnSocketConnected(Handle:socket, any:headers_pack)
{
    decl String:request_string[1024];

    ResetPack(headers_pack);
    ReadPackString(headers_pack, request_string, sizeof(request_string));

    SocketSend(socket, request_string);
}

public OnSocketReceive(Handle:socket, String:receive_data[], const data_size, any:headers_pack) {
    //Used for data received back
    if(g_JanssonEnabled)
    {
        //TODO parse JSON response
        //PrintToConsole(0,"%s", receive_data);//TODO

        new String:raw[2][1024], String:line[2][1024];
        ExplodeString(receive_data, "\r\n\r\n", raw, sizeof(raw), sizeof(raw[]));
        ExplodeString(raw[1], "\r\n", line, sizeof(line), sizeof(line[]));
        //PrintToConsole(0,"%s", line[1]);//TODO

        new Handle:json = json_load(line[1]);
        new String:command[1024];
        //TODO have way to handle missing value
        json_object_get_string(json, "command", command, sizeof(command));
        //PrintToConsole(0,"%s", command);//TODO

        //TODO have integer based commands
        if(strcmp(command, "get_favorites") == 0)
        {
            ParseGetFavorites(json);
        }
        if(strcmp(command, "have_not_voted") == 0)
        {
            ParseHaveNotVoted(json);
        }

    } else
    {
        PrintToConsole(0,"Cannot parse JSON; SMJannson not installed");
    }
}

public OnSocketDisconnected(Handle:socket, any:headers_pack) {
    // Connection: close advises the webserver to close the connection when the transfer is finished
    // we're done here
    CloseHandle(headers_pack);
    CloseHandle(socket);
}

public OnSocketError(Handle:socket, const error_type, const error_num, any:headers_pack) {
    // a socket error occured
    if(error_type == EMPTY_HOST )
    {
        LogError("[MapVotes] Empty Host (errno %d)", error_num);
    } else if (error_type == NO_HOST )
    {
        LogError("[MapVotes] No Host (errno %d)", error_num);
    } else if (error_type == CONNECT_ERROR )
    {
        LogError("[MapVotes] Connection Error (errno %d)", error_num);
    } else if (error_type == SEND_ERROR )
    {
        LogError("[MapVotes] Send Error (errno %d)", error_num);
    } else if (error_type == BIND_ERROR )
    {
        LogError("[MapVotes] Bind Error (errno %d)", error_num);
    } else if (error_type == RECV_ERROR )
    {
        LogError("[MapVotes] Recieve Error (errno %d)", error_num);
    } else if (error_type == LISTEN_ERROR )
    {
        LogError("[MapVotes] Listen Error (errno %d)", error_num);
    } else
    {
        LogError("[MapVotes] socket error %d (errno %d)", error_type, error_num);
    }

    CloseHandle(headers_pack);
    CloseHandle(socket);
}

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

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

//By 11530
//GetSteamAccountID(client) does not work because we don't have 64 bit types
stock bool:GetCommunityIDString(const String:SteamID[], String:CommunityID[], const CommunityIDSize)
{
    decl String:SteamIDParts[3][11];
    new const String:Identifier[] = "76561197960265728";

    if ((CommunityIDSize < 1) || (ExplodeString(SteamID, ":", SteamIDParts, sizeof(SteamIDParts), sizeof(SteamIDParts[])) != 3))
    {
        CommunityID[0] = '\0';
        return false;
    }

    new Current, CarryOver = (SteamIDParts[1][0] == '1');
    for (new i = (CommunityIDSize - 2), j = (strlen(SteamIDParts[2]) - 1), k = (strlen(Identifier) - 1); i >= 0; i--, j--, k--)
    {
        Current = (j >= 0 ? (2 * (SteamIDParts[2][j] - '0')) : 0) + CarryOver + (k >= 0 ? ((Identifier[k] - '0') * 1) : 0);
        CarryOver = Current / 10;
        CommunityID[i] = (Current % 10) + '0';
    }

    CommunityID[CommunityIDSize - 1] = '\0';
    return true;
}

public MapVotesCall(String:route[128], String:query_params[512])
{
    new port= GetConVarInt(g_Cvar_MapVotesPort);
    decl String:base_url[128], String:api_key[128];
    GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));
    GetConVarString(g_Cvar_MapVotesApiKey, api_key, sizeof(api_key));

    ReplaceString(base_url, sizeof(base_url), "http://", "", false);
    ReplaceString(base_url, sizeof(base_url), "https://", "", false);

    Format(query_params, sizeof(query_params), "%s&access_token=%s", query_params, api_key);

    HTTPPost(base_url, route, query_params, port);
}

public HTTPPost(String:base_url[128], String:route[128], String:query_params[512], port)
{
    new Handle:socket = SocketCreate(SOCKET_TCP, OnSocketError);

    //This Formats the headers needed to make a HTTP/1.1 POST request.
    new String:request_string[1024];
    Format(request_string, sizeof(request_string),
            "POST %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\nContent-type: application/x-www-form-urlencoded\r\nContent-length: %d\r\n\r\n%s",
            route,
            base_url,
            strlen(query_params),
            query_params);

    new Handle:headers_pack = CreateDataPack();
    WritePackString(headers_pack, request_string);
    SocketSetArg(socket, headers_pack);

    SocketConnect(socket, OnSocketConnected, OnSocketReceive, OnSocketDisconnected, base_url, port);
}
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:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));

    decl String:query_params[512], String:map[PLATFORM_MAX_PATH];
    GetCurrentMap(map, sizeof(map));
    Format(query_params, sizeof(query_params),
            "map=%s&uid=%s&comment=%s&base64=true", map, uid, base64);

    MapVotesCall(WRITE_MESSAGE_ROUTE, query_params);
}

public CastVote(client, value)
{
    decl String:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));

    if(!(value == -1 || value == 0 || value == 1)){
        LogError("[MapVotes] invalid vote value %d (steam_user: %s)", value, uid);
    }else{
        decl String:query_params[512], String:map[PLATFORM_MAX_PATH];

        GetCurrentMap(map, sizeof(map));

        Format(query_params, sizeof(query_params),
                "map=%s&uid=%s&value=%d", map, uid, value);

        MapVotesCall(CAST_VOTE_ROUTE, query_params);
    }

}

public Favorite(client, bool:favorite)
{
    decl String:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));
    decl String:query_params[512], String:map[PLATFORM_MAX_PATH];

    GetCurrentMap(map, sizeof(map));

    Format(query_params, sizeof(query_params),
            "map=%s&uid=%s", map, uid);

    if(favorite)
    {
        MapVotesCall(FAVORITE_ROUTE, query_params);
    }else{
        MapVotesCall(UNFAVORITE_ROUTE, query_params);
    }
}

public GetFavorites(client)
{
    decl String:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));
    decl String:query_params[512];

    //NOTE: uid is the client's steamid64 while player is the client's userid; the index incremented for each client that joined the server
    Format(query_params, sizeof(query_params),
            "player=%i&uid=%s", GetClientUserId(client), uid);

    MapVotesCall(GET_FAVORITES_ROUTE, query_params);
}

public ParseGetFavorites(Handle:json)
{
    new player = json_object_get_int(json, "player");
    new client = GetClientOfUserId(player);

    new Handle:maps = json_object_get(json, "maps");
    new String:map_buffer[128];

    new Handle:menu = CreateMenu(NominateMapHandler);

    for(new i = 0; i < json_array_size(maps); i++)
    {
        json_array_get_string(maps, i, map_buffer, sizeof(map_buffer));
        AddMenuItem(menu, map, map);
        //PrintToChat(GetClientOfUserId(player), "%s", map_buffer);
    }

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

}

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 ParseHaveNotVoted(Handle:json)
{
    new Handle:players = json_object_get(json, "players");
    new p;
    new String:map_buffer[PLATFORM_MAX_PATH];

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

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));
    ReplaceString(base_url, sizeof(base_url), "http://", "", false);
    ReplaceString(base_url, sizeof(base_url), "https://", "", false);

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

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

}

public ServerQuery()
{
    //TODO
}

public Action:Test(client, args)
{
    decl String:query_params[512];

    //NOTE: uid is the client's steamid64 while player is the client's userid; the index incremented for each client that joined the server
    Format(query_params, sizeof(query_params),
            "player=%i&uid=%s", 7, "76561197998903004");

    MapVotesCall(GET_FAVORITES_ROUTE, query_params);
}