Fix initial value for player arrays

Fix initial value for player arrays

Client index 0 represents the server

/*
 * Copyright © 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.2.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
};

const int TYPEOPT_MAX_LEN = 64;

/* 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
};
const int nGTypes = 3;

// Global state variables/data structures
StringMap GiveawayData;
ArrayList Dice_PlayerRolls;
ArrayList Dice_ReRolls;
ArrayList Number_PlayerGuesses;

ArrayList Current_TypeOpts; // NOTE: Subject to change

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

int abs(const int i) { return (i > 0) ? i : -i; }
int min(const int i, const int k) { return (i < k) ? i : k; }
int max(const int i, const int k) { return (i > k) ? i : k; }
int clamp(const int i, const int minval, const int maxval)
{
  return max(minval, min(i, maxval));
}

void RegisterConVars()
{
  // NOTE: Use HookConVarChange() for anything that needs it

  // XXX: Function overloads would be nice. Or casting that worked on strings.
  char sGT_All[32]; IntToString(view_as<int>(GT_All), sGT_All, sizeof(sGT_All));

  /* Core */
  cv_version = CreateConVar(
    "multi_giveaway_version",
    PLUGIN_VERSION,
    "Plugin version",
    FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY|FCVAR_DONTRECORD);
  cv_flags = CreateConVar(
    "multi_giveaway_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, float(GT_All));
  cv_player_state = CreateConVar(
    "multi_giveaway_player_state",
    "2",
    "Player states eligible for Giveaway participation; each setting includes the previous one: 0 - Alive only, 1 - Dead, 2 - Spectator",
    FCVAR_NONE,
    true, 0.0,
    true, 2.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);

  /* Dice */
  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_rerolls",
    "0",
    "Number of 'do-overs' a player gets when rolling. Only the last is counted, for better or for worse",
    FCVAR_NONE,
    true, 0.0);

  /* Number Guess */
  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
  //
  // We can use AddCommandListener() to allow for "commands" that aren't
  // actually registered commands (think 'nominate')

  RegAdminCmd("sm_mg_reload", Command_ReloadConfig, ADMFLAG_CONFIG,
    "Reload configuration file");
  RegAdminCmd("sm_multigiveaway", Command_MultiGiveaway, ADMFLAG_GENERIC,
    "Manage giveaways");
  RegAdminCmd("sm_mg", Command_MultiGiveaway, ADMFLAG_GENERIC,
    "Manage giveaways");
  RegAdminCmd("sm_dice_rig", Command_Dice_Rig, ADMFLAG_CHEATS,
              "Rigs the Dice Giveaway so the chosen client wins");

  RegConsoleCmd("sm_rolldice", Command_Dice_Roll,
                "Rolls the dice during a Dice Giveaway");
  RegConsoleCmd("sm_reroll", Command_Dice_ReRoll,
                "Rolls the dice again during a Giveaway, giving you a new result (for better or worse!");
  RegConsoleCmd("sm_guess", Command_Number_Guess,
                "Guess a number for the Number-Guess Giveaway");
}

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

void GetTypeName(const GiveawayType type, char[] str, const 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 IsTypeEnabled(const GiveawayType type)
{
  return view_as<GiveawayType>(cv_enabled_types.IntValue) & type ? true : false;
}

GiveawayType GetCurrentGiveaway()
{
  GiveawayType cg = GetCurrentGiveaway();
  return cg;
}

void SetCurrentGiveaway(const GiveawayType type)
{
  GiveawayData.SetValue("Current", type);
}

bool CanParticipate(const int client)
{
  bool spectating = GetClientTeam(client) == 1;

  /* Check player state */
  if (!IsPlayerAlive(client))
  {
    char phrase[32];
    switch (cv_player_state.IntValue)
    {
      case 0:
      {
        strcopy(phrase, sizeof(phrase),
                spectating ? "MG_Ineligible_Spec" : "MG_Ineligible_Alive");
      }
      case 1:
      {
        if (spectating)
          strcopy(phrase, sizeof(phrase), "MG_Ineligible_Spec");
      }
    }
    if (strlen(phrase))
    {
      ReplyToCommand(client, "%s %t", PLUGIN_TAG, phrase);
      return false;
    }
  }

  /* Check connection time */
  float time = GetClientTime(client);
  int delta = cv_min_conn_time.IntValue - RoundToCeil(time);
  if (time < cv_min_conn_time.FloatValue)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Not_Connected_Long_Enough",
                   delta);
    return false;
  }
  return true;
}

void GetTypeOpts(char[] str, const int sz)
{
  char opt[64];
  for (int i = 0; i < Current_TypeOpts.Length; ++i)
  {
    Current_TypeOpts.GetString(i, opt, sizeof(opt));
    StrCat(str, sz, opt);
    StrCat(str, sz, "; ");
  }
}

void ArraySetAll(ArrayList& array, any value)
{
  for (int i = 0; i < array.Length; ++i)
    array.Set(i, value);
}

void SendToAdmins(const char[] str)
{
  for (int i = 1; i <= MaxClients; ++i)
  {
    if (IsClientInGame(i) && GetUserAdmin(i) != INVALID_ADMIN_ID)
      PrintToChat(i, "%s", str);
  }
}

bool Giveaway_Start(const GiveawayType type,
                    const ArrayList opts,
                    const int client,
                    const bool restarting=false)
{
  GiveawayType cg = GetCurrentGiveaway();
  char typename_current[32];
  GetTypeName(cg, typename_current, sizeof(typename_current));
  if (cg != GT_Invalid)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Giveaway_In_Progress", typename_current);
    return false;
  }

  char typename[32], typeopts[512];
  GetTypeName(type, typename, sizeof(typename));
  GetTypeOpts(typeopts, sizeof(typeopts));
  if (type == GT_Invalid)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Type_Invalid", typename);
    return false;
  }
  else if (!IsTypeEnabled(type) && !restarting)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Type_Disabled", typename);
    return false;
  }
  else if (type == GT_Kill) // XXX: Temporary
  {
    ReplyToCommand(client, "%s Not yet implemented!", PLUGIN_TAG);
    return false;
  }

  SetCurrentGiveaway(type);
  GiveawayData.SetValue("Participants", 0);
  GiveawayData.SetValue("Starter", client);

  if (!restarting)
  {
    ShowActivity2(client, PLUGIN_TAG, " %t", "MG_Start", typename);
    LogAction(client, -1, "%L started Giveaway \"%s\" with options: %s", client, typename, typeopts);
  }
  switch (type)
  {
    case GT_Dice:
    {
      // TODO: Allow changing of "roll" command
      PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Dice_Start", "rolldice");
      if (cv_dice_rerolls.IntValue)
        PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Dice_ReRoll", "reroll");
    }
    case GT_Number:
    {
      int rand = GetRandomInt(cv_number_min.IntValue, cv_number_max.IntValue);
      GiveawayData.SetValue("Number_Target", rand);
      PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Number_Start", "guess", cv_number_min.IntValue, cv_number_max.IntValue);
    }
    case GT_Kill:
    {
    }
  }
  return true;
}

void Giveaway_Stop(const int client, const bool restarting=false)
{
  GiveawayType cg = GetCurrentGiveaway();
  if (cg == GT_Invalid)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_No_Giveaway");
    return;
  }

  char typename[32];
  GetTypeName(cg, typename, sizeof(typename));
  if (!restarting)
  {
    ShowActivity2(client, PLUGIN_TAG, " %t", "MG_Stop", typename);
    LogAction(client, -1, "%L stopped Giveaway \"%s\"", client, typename);
  }

  int winner; GiveawayData.GetValue("Winner", winner);
  switch (cg)
  {
    case GT_Dice:
    {
      if (!restarting)
      {
        if (Dice_PlayerRolls.Length)
        {
          int bestroll = Dice_GetBestRoll();
          ArrayList tied = new ArrayList(1);
          int rigged; GiveawayData.GetValue("Winner", rigged);
          char name[MAX_NAME_LENGTH];

          /* Determine whether more than 1 player has the best roll */
          for (int i = 1; i < Dice_PlayerRolls.Length; ++i)
            if (Dice_PlayerRolls.Get(i) == bestroll) tied.Push(i);

          if (tied.Length > 1)
          {
            PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Giveaway_Tie");
            winner = tied.Get(GetRandomInt(0, tied.Length - 1));
          }
          else winner = tied.Get(0);

          /* Rig the Giveaway */
          if (rigged != -1) winner = rigged;

          GetClientName(winner, name, sizeof(name));
          PrintToChatAll("%s %t", PLUGIN_TAG,
                         bestroll == cv_dice_max.IntValue ?
                         "MG_Dice_Win_Perfect" : "MG_Dice_Win", name, bestroll);
        }
        else
          PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Giveaway_Cancelled", typename);
      }
      ArraySetAll(Dice_PlayerRolls, 0);
      ArraySetAll(Dice_ReRolls, cv_dice_rerolls.IntValue);
    }

    case GT_Number:
    {
      if (!restarting)
      {
        if (Number_PlayerGuesses.Length)
        {
          int target; GiveawayData.GetValue("Number_Target", target);
          char name[MAX_NAME_LENGTH];

          if (winner == -1)
          {
            /* Nobody guessed bang on, so find the closest */
            int closest = Number_GetClosestGuess();
            ArrayList tied = new ArrayList(1);

            /* Determine whether more than 1 player has the closest guess */
            for (int i = 1; i < Number_PlayerGuesses.Length; ++i)
              if (Number_PlayerGuesses.Get(i) == closest) tied.Push(i);

            if (tied.Length > 1)
            {
              PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Giveaway_Tie");
              winner = tied.Get(GetRandomInt(0, tied.Length - 1));
            }
            else winner = tied.Get(0);
            GetClientName(winner, name, sizeof(name));
            PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Number_Win_Close", name, closest, target);
          }
          else
          {
            GetClientName(winner, name, sizeof(name));
            PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Number_Win", name, target);
          }
        }
        else
          PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Giveaway_Cancelled", typename);
      }
      ArraySetAll(Number_PlayerGuesses, 0);
    }

    case GT_Kill:
    {
    }
  }
  SetCurrentGiveaway(GT_Invalid);
  GiveawayData.SetValue("Participants", -1);
  GiveawayData.SetValue("Starter", -1);
  GiveawayData.SetValue("Winner", -1);
  Current_TypeOpts.Clear();
}

void Giveaway_Restart(const int client)
{
  GiveawayType cg = GetCurrentGiveaway();
  if (cg == GT_Invalid)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_No_Giveaway");
    return;
  }

  char typename[32], typeopts[512];
  GetTypeName(cg, typename, sizeof(typename));
  GetTypeOpts(typeopts, sizeof(typeopts));

  ShowActivity2(client, PLUGIN_TAG, " %t", "MG_Restart", typename);
  LogAction(client, -1, "%L restarted Giveaway \"%s\" with options: %s", client, typename, typeopts);
  Giveaway_Stop(client, true);
  Giveaway_Start(cg, Current_TypeOpts, client, true);
}

void Giveaway_Status(const int client)
{
  GiveawayType cg = GetCurrentGiveaway();
  if (cg == GT_Invalid)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_No_Giveaway");
    return;
  }

  char typename[32], typeopts[512];
  GetTypeName(cg, typename, sizeof(typename));
  GetTypeOpts(typeopts, sizeof(typeopts));

  ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Status", typename, typeopts);
  // do other things
}

void Dice_Roll(const int client, const bool rerolling=false)
{
  if (!rerolling && Dice_PlayerRolls.Get(client))
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_Rolled");
    if (cv_dice_rerolls.IntValue)
    {
      if (Dice_ReRolls.Get(client))
        ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_ReRoll", "reroll");
      else
        ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_ReRoll_None");
    }
    return;
  }

  int rand = GetRandomInt(cv_dice_min.IntValue, cv_dice_max.IntValue);
  int p; GiveawayData.GetValue("Participants", p);
  int rigged; GiveawayData.GetValue("Winner", rigged);

  if (!rerolling) GiveawayData.SetValue("Participants", ++p);

  // Make the rig look convincing
  if (rigged != -1 && client == rigged) rand = cv_dice_max.IntValue;

  Dice_PlayerRolls.Set(client, rand);

  char name[MAX_NAME_LENGTH];
  GetClientName(client, name, sizeof(name));
  switch (cv_dice_show_rolls.IntValue)
  {
    case 0:
    {
      ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_Result", "You", rand);
    }
    case 1:
    {
      char str[128];
      Format(str, sizeof(str), "%s %T", PLUGIN_TAG, "MG_Dice_Result", LANG_SERVER, name, rand);
      SendToAdmins(str);
      ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_Result", name, rand);
    }
    case 2:
    {
      PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Dice_Result", name, rand);
    }
  }
}

int Dice_GetBestRoll()
{
  int best = -1;
  for (int i = 1; i < Dice_PlayerRolls.Length; ++i)
  {
    int roll = Dice_PlayerRolls.Get(i);
    if (roll > best) best = roll;
  }
  return best;
}

int Number_GetClosestGuess()
{
  int closest = 0;
  int target; GiveawayData.GetValue("Number_Target", target);
  PrintToServer("target = %d", target);
  for (int i = 1; i < Number_PlayerGuesses.Length; ++i)
  {
    int guess = Number_PlayerGuesses.Get(i);
    if (guess == 0) continue; // No guess for this client
    int diff = abs(target - guess);
    int cdiff = abs(target - closest);
    if (closest == 0 || (diff < cdiff)) closest = guess;
  }
  return closest;
}

bool Number_IsGuessUnique(const int client, const int guess)
{
  for (int i = 1; i < Number_PlayerGuesses.Length; ++i)
    if (i != client && guess == Number_PlayerGuesses.Get(i)) return false;
  return true;
}

/***************************************************************\
 * SourceMod Callbacks
\***************************************************************/

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[64];
  Format(translatefile, sizeof(translatefile), "%s.phrases", PLUGIN_NAME);
  LoadTranslations(translatefile);
  LoadTranslations("common.phrases");
}

public void OnMapStart()
{
  /* Initialize global state and data structures */
  GiveawayData         = new StringMap();
  Current_TypeOpts     = new ArrayList(TYPEOPT_MAX_LEN);
  Dice_PlayerRolls     = new ArrayList(1, MAXPLAYERS);
  Dice_ReRolls         = new ArrayList(1, MAXPLAYERS);
  Number_PlayerGuesses = new ArrayList(1, MAXPLAYERS);

  SetCurrentGiveaway(GT_Invalid);
  GiveawayData.SetValue("Participants", -1);
  GiveawayData.SetValue("Starter", -1);
  GiveawayData.SetValue("Winner", -1);

  // Dice
  ArraySetAll(Dice_PlayerRolls, 0);
  // FIXME: Hook into convar change and re-set this array
  ArraySetAll(Dice_ReRolls, cv_dice_rerolls.IntValue);

  // Number Guess
  GiveawayData.SetValue("Number_Target", -1);
  ArraySetAll(Number_PlayerGuesses, 0);
}

public void OnClientDisconnect(int client)
{
  int p; GiveawayData.GetValue("Participants", p);
  int s; GiveawayData.GetValue("Starter", s);
  int w; GiveawayData.GetValue("Winner", w);
  GiveawayData.SetValue("Participants", p-1);
  if (s == client) GiveawayData.SetValue("Starter", -1);
  if (w == client) GiveawayData.SetValue("Winner", -1);

  // Dice
  Dice_PlayerRolls.Set(client, 0);

  // Number Guess
  Number_PlayerGuesses.Set(client, 0);
}

/***************************************************************\
 * Command 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)
    ShowActivity2(client, PLUGIN_TAG, " %t", "MG_Config_Reloaded");
  else // TODO: Provide error information, etc
    ShowActivity2(client, PLUGIN_TAG, " %t", "MG_Config_Error");

  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))
  {
    if (args < 2)
    {
      /* No type specified, inform client of valid types */
      char typenames[32*nGTypes];
      for (int i = 0; i < nGTypes; ++i)
      {
        char name[32];
        GetTypeName(view_as<GiveawayType>(1<<i), name, sizeof(name));
        StrCat(typenames, sizeof(typenames), name);
        StrCat(typenames, sizeof(typenames), ", ");
      }
      ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Type_Needed", typenames);
      return Plugin_Handled;
    }

    for (int i = 3; i <= GetCmdArgs(); ++i)
    {
      char opt[TYPEOPT_MAX_LEN];
      GetCmdArg(i, opt, sizeof(opt));
      Current_TypeOpts.PushString(opt);
    }
    Giveaway_Start(type, Current_TypeOpts, client);
  }
  else if (StrEqual(action, "stop", false))
    Giveaway_Stop(client);
  else if (StrEqual(action, "restart", false))
    Giveaway_Restart(client);
  else if (StrEqual(action, "status", false))
    Giveaway_Status(client);
  else
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Action_Invalid", action, "start, stop, restart, status");

  return Plugin_Handled;
}

public Action Command_Dice_Roll(int client, int args)
{
  if (!CanParticipate(client)) return Plugin_Handled;

  GiveawayType cg = GetCurrentGiveaway();
  if (cg != GT_Dice)
  {
    char typename[32];
    GetTypeName(GT_Dice, typename, sizeof(typename));
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_No_Giveaway_Type", typename);
    return Plugin_Handled;
  }

  Dice_Roll(client);

  return Plugin_Handled;
}

public Action Command_Dice_ReRoll(int client, int args)
{
  if (!CanParticipate(client)) return Plugin_Handled;

  GiveawayType cg = GetCurrentGiveaway();
  if (cg != GT_Dice)
  {
    char typename[32];
    GetTypeName(GT_Dice, typename, sizeof(typename));
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_No_Giveaway_Type", typename);
    return Plugin_Handled;
  }

  int rerolls = cv_dice_rerolls.IntValue;
  if (!rerolls)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_ReRoll_Disabled");
    return Plugin_Handled;
  }
  if (!Dice_PlayerRolls.Get(client))
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_ReRoll_Roll_Needed");
    return Plugin_Handled;
  }

  int rolls_left = Dice_ReRolls.Get(client);
  if (rolls_left)
  {
    int last_roll = Dice_PlayerRolls.Get(client);
    Dice_Roll(client, true);
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_ReRolled", last_roll, --rolls_left);
    Dice_ReRolls.Set(client, rolls_left);
  }
  else
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_ReRoll_None");

  return Plugin_Handled;
}

public Action Command_Dice_Rig(int client, int args)
{
  GiveawayType cg = GetCurrentGiveaway();
  if (cg != GT_Dice)
  {
    char typename[32];
    GetTypeName(GT_Dice, typename, sizeof(typename));
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_No_Giveaway_Type", typename);
    return Plugin_Handled;
  }
  if (args < 1)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Target_Required");
    return Plugin_Handled;
  }

  char arg[MAX_NAME_LENGTH], target_name[MAX_NAME_LENGTH];
  int target[1];
  bool tnml;
  GetCmdArg(1, arg, sizeof(arg));
  int rv = ProcessTargetString(arg, client, target, 1, COMMAND_FILTER_CONNECTED,
                               target_name, sizeof(target_name), tnml);
  if (rv)
  {
    ShowActivity2(client, PLUGIN_TAG, " %t", "MG_Dice_Rigged", target_name);
    GiveawayData.SetValue("Winner", target[0]);
  }
  else ReplyToTargetError(client, rv);

  return Plugin_Handled;
}

public Action Command_Number_Guess(int client, int args)
{
  if (!CanParticipate(client)) return Plugin_Handled;

  GiveawayType cg = GetCurrentGiveaway();
  if (cg != GT_Number)
  {
    char typename[32];
    GetTypeName(GT_Dice, typename, sizeof(typename));
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_No_Giveaway_Type", typename);
    return Plugin_Handled;
  }
  if (args < 1)
  {
    ReplyToCommand(client, "%s Usage: sm_guess <number>", PLUGIN_TAG);
    return Plugin_Handled;
  }
  else if (Number_PlayerGuesses.Get(client))
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Number_Guessed");
    return Plugin_Handled;
  }

  int guess;
  char arg[8];
  GetCmdArg(1, arg, sizeof(arg));

  if (!StringToIntEx(arg, guess))
  {
    ReplyToCommand(client, "%s Usage: sm_guess <number>", PLUGIN_TAG);
    return Plugin_Handled;
  }
  else if (guess >= cv_number_min.IntValue && guess <= cv_number_max.IntValue)
  {
    int p; GiveawayData.GetValue("Participants", p);
    int target; GiveawayData.GetValue("Number_Target", target);

    GiveawayData.SetValue("Participants", ++p);
    Number_PlayerGuesses.Set(client, guess);
    if (guess == target)
    {
      GiveawayData.SetValue("Winner", client);
      Giveaway_Stop(0);
    }
    else if (!Number_IsGuessUnique(client, guess))
    {
      ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Number_Not_Unique");
      return Plugin_Handled;
    }
    else
    {
      char name[MAX_NAME_LENGTH];
      GetClientName(client, name, sizeof(name));
      switch (cv_number_show_guesses.IntValue)
      {
        case 0:
        {
          ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Number_Guess", "You", guess);
        }
        case 1:
        {
          char str[128];
          Format(str, sizeof(str), "%s %T", PLUGIN_TAG, "MG_Number_Guess", LANG_SERVER, name, guess);
          SendToAdmins(str);
          ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Number_Guess", name, guess);
        }
        case 2:
        {
          PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Number_Guess", name, guess);
        }
      }
    }
  }
  else
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Number_Bad_Range", cv_number_min.IntValue, cv_number_max.IntValue);

  return Plugin_Handled;
}

// vim: et sts=2 sw=2

/*
 * Copyright © 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.2.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
};

const int TYPEOPT_MAX_LEN = 64;

/* 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
};
const int nGTypes = 3;

// Global state variables/data structures
StringMap GiveawayData;
ArrayList Dice_PlayerRolls;
ArrayList Dice_ReRolls;
ArrayList Number_PlayerGuesses;

ArrayList Current_TypeOpts; // NOTE: Subject to change

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

int abs(const int i) { return (i > 0) ? i : -i; }
int min(const int i, const int k) { return (i < k) ? i : k; }
int max(const int i, const int k) { return (i > k) ? i : k; }
int clamp(const int i, const int minval, const int maxval)
{
  return max(minval, min(i, maxval));
}

void RegisterConVars()
{
  // NOTE: Use HookConVarChange() for anything that needs it

  // XXX: Function overloads would be nice. Or casting that worked on strings.
  char sGT_All[32]; IntToString(view_as<int>(GT_All), sGT_All, sizeof(sGT_All));

  /* Core */
  cv_version = CreateConVar(
    "multi_giveaway_version",
    PLUGIN_VERSION,
    "Plugin version",
    FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY|FCVAR_DONTRECORD);
  cv_flags = CreateConVar(
    "multi_giveaway_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, float(GT_All));
  cv_player_state = CreateConVar(
    "multi_giveaway_player_state",
    "2",
    "Player states eligible for Giveaway participation; each setting includes the previous one: 0 - Alive only, 1 - Dead, 2 - Spectator",
    FCVAR_NONE,
    true, 0.0,
    true, 2.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);

  /* Dice */
  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_rerolls",
    "0",
    "Number of 'do-overs' a player gets when rolling. Only the last is counted, for better or for worse",
    FCVAR_NONE,
    true, 0.0);

  /* Number Guess */
  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
  //
  // We can use AddCommandListener() to allow for "commands" that aren't
  // actually registered commands (think 'nominate')

  RegAdminCmd("sm_mg_reload", Command_ReloadConfig, ADMFLAG_CONFIG,
    "Reload configuration file");
  RegAdminCmd("sm_multigiveaway", Command_MultiGiveaway, ADMFLAG_GENERIC,
    "Manage giveaways");
  RegAdminCmd("sm_mg", Command_MultiGiveaway, ADMFLAG_GENERIC,
    "Manage giveaways");
  RegAdminCmd("sm_dice_rig", Command_Dice_Rig, ADMFLAG_CHEATS,
              "Rigs the Dice Giveaway so the chosen client wins");

  RegConsoleCmd("sm_rolldice", Command_Dice_Roll,
                "Rolls the dice during a Dice Giveaway");
  RegConsoleCmd("sm_reroll", Command_Dice_ReRoll,
                "Rolls the dice again during a Giveaway, giving you a new result (for better or worse!");
  RegConsoleCmd("sm_guess", Command_Number_Guess,
                "Guess a number for the Number-Guess Giveaway");
}

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

void GetTypeName(const GiveawayType type, char[] str, const 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 IsTypeEnabled(const GiveawayType type)
{
  return view_as<GiveawayType>(cv_enabled_types.IntValue) & type ? true : false;
}

GiveawayType GetCurrentGiveaway()
{
  GiveawayType cg = GetCurrentGiveaway();
  return cg;
}

void SetCurrentGiveaway(const GiveawayType type)
{
  GiveawayData.SetValue("Current", type);
}

bool CanParticipate(const int client)
{
  bool spectating = GetClientTeam(client) == 1;

  /* Check player state */
  if (!IsPlayerAlive(client))
  {
    char phrase[32];
    switch (cv_player_state.IntValue)
    {
      case 0:
      {
        strcopy(phrase, sizeof(phrase),
                spectating ? "MG_Ineligible_Spec" : "MG_Ineligible_Alive");
      }
      case 1:
      {
        if (spectating)
          strcopy(phrase, sizeof(phrase), "MG_Ineligible_Spec");
      }
    }
    if (strlen(phrase))
    {
      ReplyToCommand(client, "%s %t", PLUGIN_TAG, phrase);
      return false;
    }
  }

  /* Check connection time */
  float time = GetClientTime(client);
  int delta = cv_min_conn_time.IntValue - RoundToCeil(time);
  if (time < cv_min_conn_time.FloatValue)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Not_Connected_Long_Enough",
                   delta);
    return false;
  }
  return true;
}

void GetTypeOpts(char[] str, const int sz)
{
  char opt[64];
  for (int i = 0; i < Current_TypeOpts.Length; ++i)
  {
    Current_TypeOpts.GetString(i, opt, sizeof(opt));
    StrCat(str, sz, opt);
    StrCat(str, sz, "; ");
  }
}

void ArraySetAll(ArrayList& array, any value)
{
  for (int i = 0; i < array.Length; ++i)
    array.Set(i, value);
}

void SendToAdmins(const char[] str)
{
  for (int i = 1; i <= MaxClients; ++i)
  {
    if (IsClientInGame(i) && GetUserAdmin(i) != INVALID_ADMIN_ID)
      PrintToChat(i, "%s", str);
  }
}

bool Giveaway_Start(const GiveawayType type,
                    const ArrayList opts,
                    const int client,
                    const bool restarting=false)
{
  GiveawayType cg = GetCurrentGiveaway();
  char typename_current[32];
  GetTypeName(cg, typename_current, sizeof(typename_current));
  if (cg != GT_Invalid)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Giveaway_In_Progress", typename_current);
    return false;
  }

  char typename[32], typeopts[512];
  GetTypeName(type, typename, sizeof(typename));
  GetTypeOpts(typeopts, sizeof(typeopts));
  if (type == GT_Invalid)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Type_Invalid", typename);
    return false;
  }
  else if (!IsTypeEnabled(type) && !restarting)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Type_Disabled", typename);
    return false;
  }
  else if (type == GT_Kill) // XXX: Temporary
  {
    ReplyToCommand(client, "%s Not yet implemented!", PLUGIN_TAG);
    return false;
  }

  SetCurrentGiveaway(type);
  GiveawayData.SetValue("Participants", 0);
  GiveawayData.SetValue("Starter", client);

  if (!restarting)
  {
    ShowActivity2(client, PLUGIN_TAG, " %t", "MG_Start", typename);
    LogAction(client, -1, "%L started Giveaway \"%s\" with options: %s", client, typename, typeopts);
  }
  switch (type)
  {
    case GT_Dice:
    {
      // TODO: Allow changing of "roll" command
      PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Dice_Start", "rolldice");
      if (cv_dice_rerolls.IntValue)
        PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Dice_ReRoll", "reroll");
    }
    case GT_Number:
    {
      int rand = GetRandomInt(cv_number_min.IntValue, cv_number_max.IntValue);
      GiveawayData.SetValue("Number_Target", rand);
      PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Number_Start", "guess", cv_number_min.IntValue, cv_number_max.IntValue);
    }
    case GT_Kill:
    {
    }
  }
  return true;
}

void Giveaway_Stop(const int client, const bool restarting=false)
{
  GiveawayType cg = GetCurrentGiveaway();
  if (cg == GT_Invalid)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_No_Giveaway");
    return;
  }

  char typename[32];
  GetTypeName(cg, typename, sizeof(typename));
  if (!restarting)
  {
    ShowActivity2(client, PLUGIN_TAG, " %t", "MG_Stop", typename);
    LogAction(client, -1, "%L stopped Giveaway \"%s\"", client, typename);
  }

  int winner; GiveawayData.GetValue("Winner", winner);
  switch (cg)
  {
    case GT_Dice:
    {
      if (!restarting)
      {
        if (Dice_PlayerRolls.Length)
        {
          int bestroll = Dice_GetBestRoll();
          ArrayList tied = new ArrayList(1);
          int rigged; GiveawayData.GetValue("Winner", rigged);
          char name[MAX_NAME_LENGTH];

          /* Determine whether more than 1 player has the best roll */
          for (int i = 0; i < Dice_PlayerRolls.Length; ++i)
            if (Dice_PlayerRolls.Get(i) == bestroll) tied.Push(i);

          if (tied.Length > 1)
          {
            PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Giveaway_Tie");
            winner = tied.Get(GetRandomInt(0, tied.Length - 1));
          }
          else winner = tied.Get(0);

          /* Rig the Giveaway */
          if (rigged != -1) winner = rigged;

          GetClientName(winner, name, sizeof(name));
          PrintToChatAll("%s %t", PLUGIN_TAG,
                         bestroll == cv_dice_max.IntValue ?
                         "MG_Dice_Win_Perfect" : "MG_Dice_Win", name, bestroll);
        }
        else
          PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Giveaway_Cancelled", typename);
      }
      ArraySetAll(Dice_PlayerRolls, 0);
      ArraySetAll(Dice_ReRolls, cv_dice_rerolls.IntValue);
    }

    case GT_Number:
    {
      if (!restarting)
      {
        if (Number_PlayerGuesses.Length)
        {
          int target; GiveawayData.GetValue("Number_Target", target);
          char name[MAX_NAME_LENGTH];

          if (winner == -1)
          {
            /* Nobody guessed bang on, so find the closest */
            int closest = Number_GetClosestGuess();
            ArrayList tied = new ArrayList(1);

            /* Determine whether more than 1 player has the closest guess */
            for (int i = 0; i < Number_PlayerGuesses.Length; ++i)
              if (Number_PlayerGuesses.Get(i) == closest) tied.Push(i);

            if (tied.Length > 1)
            {
              PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Giveaway_Tie");
              winner = tied.Get(GetRandomInt(0, tied.Length - 1));
            }
            else winner = tied.Get(0);
            GetClientName(winner, name, sizeof(name));
            PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Number_Win_Close", name, closest, target);
          }
          else
          {
            GetClientName(winner, name, sizeof(name));
            PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Number_Win", name, target);
          }
        }
        else
          PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Giveaway_Cancelled", typename);
      }
      ArraySetAll(Number_PlayerGuesses, 0);
    }

    case GT_Kill:
    {
    }
  }
  SetCurrentGiveaway(GT_Invalid);
  GiveawayData.SetValue("Participants", -1);
  GiveawayData.SetValue("Starter", -1);
  GiveawayData.SetValue("Winner", -1);
  Current_TypeOpts.Clear();
}

void Giveaway_Restart(const int client)
{
  GiveawayType cg = GetCurrentGiveaway();
  if (cg == GT_Invalid)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_No_Giveaway");
    return;
  }

  char typename[32], typeopts[512];
  GetTypeName(cg, typename, sizeof(typename));
  GetTypeOpts(typeopts, sizeof(typeopts));

  ShowActivity2(client, PLUGIN_TAG, " %t", "MG_Restart", typename);
  LogAction(client, -1, "%L restarted Giveaway \"%s\" with options: %s", client, typename, typeopts);
  Giveaway_Stop(client, true);
  Giveaway_Start(cg, Current_TypeOpts, client, true);
}

void Giveaway_Status(const int client)
{
  GiveawayType cg = GetCurrentGiveaway();
  if (cg == GT_Invalid)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_No_Giveaway");
    return;
  }

  char typename[32], typeopts[512];
  GetTypeName(cg, typename, sizeof(typename));
  GetTypeOpts(typeopts, sizeof(typeopts));

  ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Status", typename, typeopts);
  // do other things
}

void Dice_Roll(const int client, const bool rerolling=false)
{
  if (!rerolling && Dice_PlayerRolls.Get(client))
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_Rolled");
    if (cv_dice_rerolls.IntValue)
    {
      if (Dice_ReRolls.Get(client))
        ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_ReRoll", "reroll");
      else
        ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_ReRoll_None");
    }
    return;
  }

  int rand = GetRandomInt(cv_dice_min.IntValue, cv_dice_max.IntValue);
  int p; GiveawayData.GetValue("Participants", p);
  int rigged; GiveawayData.GetValue("Winner", rigged);

  if (!rerolling) GiveawayData.SetValue("Participants", ++p);

  // Make the rig look convincing
  if (rigged != -1 && client == rigged) rand = cv_dice_max.IntValue;

  Dice_PlayerRolls.Set(client, rand);

  char name[MAX_NAME_LENGTH];
  GetClientName(client, name, sizeof(name));
  switch (cv_dice_show_rolls.IntValue)
  {
    case 0:
    {
      ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_Result", "You", rand);
    }
    case 1:
    {
      char str[128];
      Format(str, sizeof(str), "%s %T", PLUGIN_TAG, "MG_Dice_Result", LANG_SERVER, name, rand);
      SendToAdmins(str);
      ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_Result", name, rand);
    }
    case 2:
    {
      PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Dice_Result", name, rand);
    }
  }
}

int Dice_GetBestRoll()
{
  int best = -1;
  for (int i = 0; i < Dice_PlayerRolls.Length; ++i)
  {
    int roll = Dice_PlayerRolls.Get(i);
    if (roll > best) best = roll;
  }
  return best;
}

int Number_GetClosestGuess()
{
  int closest = 0;
  int target; GiveawayData.GetValue("Number_Target", target);
  PrintToServer("target = %d", target);
  for (int i = 0; i < Number_PlayerGuesses.Length; ++i)
  {
    int guess = Number_PlayerGuesses.Get(i);
    if (guess == 0) continue; // No guess for this client
    int diff = abs(target - guess);
    int cdiff = abs(target - closest);
    if (closest == 0 || (diff < cdiff)) closest = guess;
  }
  return closest;
}

bool Number_IsGuessUnique(const int client, const int guess)
{
  for (int i = 0; i < Number_PlayerGuesses.Length; ++i)
    if (i != client && guess == Number_PlayerGuesses.Get(i)) return false;
  return true;
}

/***************************************************************\
 * SourceMod Callbacks
\***************************************************************/

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[64];
  Format(translatefile, sizeof(translatefile), "%s.phrases", PLUGIN_NAME);
  LoadTranslations(translatefile);
  LoadTranslations("common.phrases");
}

public void OnMapStart()
{
  /* Initialize global state and data structures */
  GiveawayData         = new StringMap();
  Current_TypeOpts     = new ArrayList(TYPEOPT_MAX_LEN);
  Dice_PlayerRolls     = new ArrayList(1, MAXPLAYERS);
  Dice_ReRolls         = new ArrayList(1, MAXPLAYERS);
  Number_PlayerGuesses = new ArrayList(1, MAXPLAYERS);

  SetCurrentGiveaway(GT_Invalid);
  GiveawayData.SetValue("Participants", -1);
  GiveawayData.SetValue("Starter", -1);
  GiveawayData.SetValue("Winner", -1);

  // Dice
  ArraySetAll(Dice_PlayerRolls, 0);
  // FIXME: Hook into convar change and re-set this array
  ArraySetAll(Dice_ReRolls, cv_dice_rerolls.IntValue);

  // Number Guess
  GiveawayData.SetValue("Number_Target", -1);
  ArraySetAll(Number_PlayerGuesses, 0);
}

public void OnClientDisconnect(int client)
{
  int p; GiveawayData.GetValue("Participants", p);
  int s; GiveawayData.GetValue("Starter", s);
  int w; GiveawayData.GetValue("Winner", w);
  GiveawayData.SetValue("Participants", p-1);
  if (s == client) GiveawayData.SetValue("Starter", -1);
  if (w == client) GiveawayData.SetValue("Winner", -1);

  // Dice
  Dice_PlayerRolls.Set(client, 0);

  // Number Guess
  Number_PlayerGuesses.Set(client, 0);
}

/***************************************************************\
 * Command 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)
    ShowActivity2(client, PLUGIN_TAG, " %t", "MG_Config_Reloaded");
  else // TODO: Provide error information, etc
    ShowActivity2(client, PLUGIN_TAG, " %t", "MG_Config_Error");

  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))
  {
    if (args < 2)
    {
      /* No type specified, inform client of valid types */
      char typenames[32*nGTypes];
      for (int i = 0; i < nGTypes; ++i)
      {
        char name[32];
        GetTypeName(view_as<GiveawayType>(1<<i), name, sizeof(name));
        StrCat(typenames, sizeof(typenames), name);
        StrCat(typenames, sizeof(typenames), ", ");
      }
      ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Type_Needed", typenames);
      return Plugin_Handled;
    }

    for (int i = 3; i <= GetCmdArgs(); ++i)
    {
      char opt[TYPEOPT_MAX_LEN];
      GetCmdArg(i, opt, sizeof(opt));
      Current_TypeOpts.PushString(opt);
    }
    Giveaway_Start(type, Current_TypeOpts, client);
  }
  else if (StrEqual(action, "stop", false))
    Giveaway_Stop(client);
  else if (StrEqual(action, "restart", false))
    Giveaway_Restart(client);
  else if (StrEqual(action, "status", false))
    Giveaway_Status(client);
  else
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Action_Invalid", action, "start, stop, restart, status");

  return Plugin_Handled;
}

public Action Command_Dice_Roll(int client, int args)
{
  if (!CanParticipate(client)) return Plugin_Handled;

  GiveawayType cg = GetCurrentGiveaway();
  if (cg != GT_Dice)
  {
    char typename[32];
    GetTypeName(GT_Dice, typename, sizeof(typename));
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_No_Giveaway_Type", typename);
    return Plugin_Handled;
  }

  Dice_Roll(client);

  return Plugin_Handled;
}

public Action Command_Dice_ReRoll(int client, int args)
{
  if (!CanParticipate(client)) return Plugin_Handled;

  GiveawayType cg = GetCurrentGiveaway();
  if (cg != GT_Dice)
  {
    char typename[32];
    GetTypeName(GT_Dice, typename, sizeof(typename));
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_No_Giveaway_Type", typename);
    return Plugin_Handled;
  }

  int rerolls = cv_dice_rerolls.IntValue;
  if (!rerolls)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_ReRoll_Disabled");
    return Plugin_Handled;
  }
  if (!Dice_PlayerRolls.Get(client))
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_ReRoll_Roll_Needed");
    return Plugin_Handled;
  }

  int rolls_left = Dice_ReRolls.Get(client);
  if (rolls_left)
  {
    int last_roll = Dice_PlayerRolls.Get(client);
    Dice_Roll(client, true);
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_ReRolled", last_roll, --rolls_left);
    Dice_ReRolls.Set(client, rolls_left);
  }
  else
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Dice_ReRoll_None");

  return Plugin_Handled;
}

public Action Command_Dice_Rig(int client, int args)
{
  GiveawayType cg = GetCurrentGiveaway();
  if (cg != GT_Dice)
  {
    char typename[32];
    GetTypeName(GT_Dice, typename, sizeof(typename));
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_No_Giveaway_Type", typename);
    return Plugin_Handled;
  }
  if (args < 1)
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Target_Required");
    return Plugin_Handled;
  }

  char arg[MAX_NAME_LENGTH], target_name[MAX_NAME_LENGTH];
  int target[1];
  bool tnml;
  GetCmdArg(1, arg, sizeof(arg));
  int rv = ProcessTargetString(arg, client, target, 1, COMMAND_FILTER_CONNECTED,
                               target_name, sizeof(target_name), tnml);
  if (rv)
  {
    ShowActivity2(client, PLUGIN_TAG, " %t", "MG_Dice_Rigged", target_name);
    GiveawayData.SetValue("Winner", target[0]);
  }
  else ReplyToTargetError(client, rv);

  return Plugin_Handled;
}

public Action Command_Number_Guess(int client, int args)
{
  if (!CanParticipate(client)) return Plugin_Handled;

  GiveawayType cg = GetCurrentGiveaway();
  if (cg != GT_Number)
  {
    char typename[32];
    GetTypeName(GT_Dice, typename, sizeof(typename));
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_No_Giveaway_Type", typename);
    return Plugin_Handled;
  }
  if (args < 1)
  {
    ReplyToCommand(client, "%s Usage: sm_guess <number>", PLUGIN_TAG);
    return Plugin_Handled;
  }
  else if (Number_PlayerGuesses.Get(client))
  {
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Number_Guessed");
    return Plugin_Handled;
  }

  int guess;
  char arg[8];
  GetCmdArg(1, arg, sizeof(arg));

  if (!StringToIntEx(arg, guess))
  {
    ReplyToCommand(client, "%s Usage: sm_guess <number>", PLUGIN_TAG);
    return Plugin_Handled;
  }
  else if (guess >= cv_number_min.IntValue && guess <= cv_number_max.IntValue)
  {
    int p; GiveawayData.GetValue("Participants", p);
    int target; GiveawayData.GetValue("Number_Target", target);

    GiveawayData.SetValue("Participants", ++p);
    Number_PlayerGuesses.Set(client, guess);
    if (guess == target)
    {
      GiveawayData.SetValue("Winner", client);
      Giveaway_Stop(0);
    }
    else if (!Number_IsGuessUnique(client, guess))
    {
      ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Number_Not_Unique");
      return Plugin_Handled;
    }
    else
    {
      char name[MAX_NAME_LENGTH];
      GetClientName(client, name, sizeof(name));
      switch (cv_number_show_guesses.IntValue)
      {
        case 0:
        {
          ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Number_Guess", "You", guess);
        }
        case 1:
        {
          char str[128];
          Format(str, sizeof(str), "%s %T", PLUGIN_TAG, "MG_Number_Guess", LANG_SERVER, name, guess);
          SendToAdmins(str);
          ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Number_Guess", name, guess);
        }
        case 2:
        {
          PrintToChatAll("%s %t", PLUGIN_TAG, "MG_Number_Guess", name, guess);
        }
      }
    }
  }
  else
    ReplyToCommand(client, "%s %t", PLUGIN_TAG, "MG_Number_Bad_Range", cv_number_min.IntValue, cv_number_max.IntValue);

  return Plugin_Handled;
}

// vim: et sts=2 sw=2


Autoremove disguise + check team (testing)

/** Commented as tf2items isnt using new-style syntax yet.
#pragma newdecls required
*/
#pragma semicolon 1

#include <sdktools>
#include <tf2items>
#include <tf2>
#include <tf2_stocks>
#include <morecolors>

#define PLUGIN_VERSION "1.3"

public Plugin myinfo = {
	name = "[NGS] Taunt Menu",
	author = "FlaminSarge, Nighty, xCoderx / TheXeon",
	description = "Displays a nifty taunt menu. TF2 only.",
	version = PLUGIN_VERSION,
	url = "http://forums.alliedmods.net/showthread.php?t=242866"
}

Handle hPlayTaunt;

public void OnPluginStart()
{
	Handle conf = LoadGameConfigFile("tf2.tauntem");

	if (conf == null)
	{
		SetFailState("Unable to load gamedata/tf2.tauntem.txt. Good luck figuring that out.");
		return;
	}

	StartPrepSDKCall(SDKCall_Player);
	PrepSDKCall_SetFromConf(conf, SDKConf_Signature, "CTFPlayer::PlayTauntSceneFromItem");
	PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
	PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain);
	hPlayTaunt = EndPrepSDKCall();

	if (hPlayTaunt == null)
	{
		SetFailState("Unable to initialize call to CTFPlayer::PlayTauntSceneFromItem. Wait patiently for a fix.");
		CloseHandle(conf);
		return;
	}

	RegConsoleCmd("sm_taunt", Cmd_TauntMenu, "Taunt Menu");
	RegConsoleCmd("sm_taunts", Cmd_TauntMenu, "Taunt Menu");

	CloseHandle(conf);
	LoadTranslations("common.phrases");
	CreateConVar("tf_tauntmenu_version", PLUGIN_VERSION, "[NGS] Taunt Menu Version", FCVAR_NOTIFY);

	PrecacheModel("models/player/items/taunts/cash_wad.mdl", true);
	PrecacheModel("models/player/items/taunts/medic_xray_taunt.mdl", true);
	PrecacheModel("models/player/items/taunts/victory_mug.mdl", true);
	PrecacheModel("models/player/items/taunts/balloon_animal_pyro/balloon_animal_pyro.mdl", true);
	PrecacheModel("models/player/items/taunts/beer_crate/beer_crate.mdl", true);
	PrecacheModel("models/player/items/taunts/chicken_bucket/chicken_bucket.mdl", true);
	PrecacheModel("models/player/items/taunts/demo_nuke_bottle/demo_nuke_bottle.mdl", true);
	PrecacheModel("models/player/items/taunts/dizzy_bottle1/dizzy_bottle1.mdl", true);
	PrecacheModel("models/player/items/taunts/dizzy_bottle2/dizzy_bottle2.mdl", true);
	PrecacheModel("models/player/items/taunts/engys_new_chair/engys_new_chair.mdl", true);
	PrecacheModel("models/player/items/taunts/engys_new_chair/engys_new_chair_articulated.mdl", true);
	PrecacheModel("models/player/items/taunts/wupass_mug/wupass_mug.mdl", true);
	PrecacheModel("models/workshop/player/items/taunts/pyro_poolparty/pyro_poolparty.mdl", true);
	PrecacheModel("models/workshop/player/items/spy/taunt_spy_boxtrot/taunt_spy_boxtrot.mdl", true);
	PrecacheModel("models/workshop/player/items/sniper/killer_solo/killer_solo.mdl", true);
	PrecacheModel("models/workshop/player/items/sniper/taunt_most_wanted/taunt_most_wanted.mdl", true);
}

public Action Cmd_TauntMenu(int client, int args)
{
	if (GetClientTeam(client) < 1 || GetClientTeam(client) > 4 || !IsClientConnected(client))
	{
		CReplyToCommand(client, "{GREEN}[SM]{NORMAL} You must join a team to use this command.");
		return Plugin_Handled;
	}
	ShowMenu(client);
	return Plugin_Handled;
}

public Action ShowMenu(int client)
{
	TFClassType class = TF2_GetPlayerClass(client);
	Menu menu = CreateMenu(Taunt_MenuSelected);
	SetMenuTitle(menu, "===== NGS Taunt Menu =====");

	switch(class)
	{
		case TFClass_Scout:
		{
			AddMenuItem(menu, "1117", "Taunt: Battin' a Thousand");
			AddMenuItem(menu, "1119", "Taunt: Deep Fried Desire");
			AddMenuItem(menu, "1168", "Taunt: The Carlton");
			AddMenuItem(menu, "30572", "Taunt: The Boston Breakdance");
		}
		case TFClass_Sniper:
		{
			AddMenuItem(menu, "1116", "Taunt: I See You");
			AddMenuItem(menu, "30609", "Taunt: The Killer Solo");
			AddMenuItem(menu, "30614", "Taunt: Most Wanted");
		}
		case TFClass_Soldier:
		{
			AddMenuItem(menu, "1113", "Taunt: Fresh Brewed Victory");
			AddMenuItem(menu, "30673", "Taunt: Soldier's Requiem");
			AddMenuItem(menu, "30761", "Taunt: The Fubar Fanfare");
		}
		case TFClass_Heavy:
		{
			AddMenuItem(menu, "30616", "Taunt: The Proletariat Posedown");
		}
		case TFClass_DemoMan:
		{
			AddMenuItem(menu, "1114", "Taunt: Spent Well Spirits");
			AddMenuItem(menu, "1120", "Taunt: Oblooterated");
			AddMenuItem(menu, "30671", "Taunt: True Scotsman's Call");
		}
		case TFClass_Medic:
		{
			AddMenuItem(menu, "477", "Taunt: The Meet the Medic");
			AddMenuItem(menu, "1109", "Taunt: Results Are In");
		}

		case TFClass_Pyro:
		{
			AddMenuItem(menu, "1112", "Taunt: Party Trick");
			AddMenuItem(menu, "30570", "Taunt: Pool Party");
			AddMenuItem(menu, "30763", "Taunt: The Balloonibouncer");
		}
		case TFClass_Spy:
		{
			AddMenuItem(menu, "1108", "Taunt: Buy A Life");
			AddMenuItem(menu, "30615", "Taunt: The Box Trot");
			AddMenuItem(menu, "30762", "Taunt: Disco Fever");
		}
		case TFClass_Engineer:
		{
			AddMenuItem(menu, "1115", "Taunt: Rancho Relaxo");
			AddMenuItem(menu, "30618", "Taunt: Bucking Bronco");
		}
	}

	AddMenuItem(menu, "167", "Taunt: The High Five!");
	AddMenuItem(menu, "438", "Taunt: The Director's Vision");
	AddMenuItem(menu, "463", "Taunt: The Schadenfreude");
	AddMenuItem(menu, "1015", "Taunt: The Shred Alert");
	AddMenuItem(menu, "1106", "Taunt: Square Dance");
	AddMenuItem(menu, "1107", "Taunt: Flippin' Awesome");
	AddMenuItem(menu, "1110", "Taunt: Rock, Paper, Scissors");
	AddMenuItem(menu, "1111", "Taunt: Skullcracker");
	AddMenuItem(menu, "1118", "Taunt: Conga");
	AddMenuItem(menu, "1157", "Taunt: Kazotsky Kick");
	AddMenuItem(menu, "1162", "Taunt: Mannrobics");
	AddMenuItem(menu, "30621", "Taunt: Burstchester");
	AddMenuItem(menu, "30672", "Taunt: Zoomin' Broom");

	DisplayMenu(menu, client, 20);
}

public int Taunt_MenuSelected(Handle menu, MenuAction action, int iClient, int param2)
{
	if(action == MenuAction_End)
	{
		CloseHandle(menu);
	}

	if(action == MenuAction_Select)
	{
		char info[12];

		GetMenuItem(menu, param2, info, sizeof(info));
		ExecuteTaunt(iClient, StringToInt(info));
	}
}

public void ExecuteTaunt(int client, int itemdef)
{
	if (TF2_GetPlayerClass(client) == TFClass_Spy)
	{
		TF2_RemovePlayerDisguise(client);
	}
	Handle hItem = TF2Items_CreateItem(OVERRIDE_ALL|PRESERVE_ATTRIBUTES|FORCE_GENERATION);

	TF2Items_SetClassname(hItem, "tf_wearable_vm");
	TF2Items_SetQuality(hItem, 6);
	TF2Items_SetLevel(hItem, 1);
	TF2Items_SetNumAttributes(hItem, 0);
	TF2Items_SetItemIndex(hItem, itemdef);

	int ent = TF2Items_GiveNamedItem(client, hItem);
	Address pEconItemView = GetEntityAddress(ent) + view_as<Address>(FindSendPropInfo("CTFWearable", "m_Item"));

	SDKCall(hPlayTaunt, client, pEconItemView) ? 1 : 0;
	AcceptEntityInput(ent, "Kill");
}

/** Commented as tf2items isnt using new-style syntax yet.
#pragma newdecls required
*/
#pragma semicolon 1

#include <sdktools>
#include <tf2items>
#include <tf2_stocks>

#define PLUGIN_VERSION "1.3"

public Plugin myinfo = {
	name = "[NGS] Taunt Menu",
	author = "FlaminSarge, Nighty, xCoderx / TheXeon",
	description = "Displays a nifty taunt menu.",
	version = PLUGIN_VERSION,
	url = "http://forums.alliedmods.net/showthread.php?t=242866"
}

Handle hPlayTaunt;

public void OnPluginStart()
{
	Handle conf = LoadGameConfigFile("tf2.tauntem");

	if (conf == null)
	{
		SetFailState("Unable to load gamedata/tf2.tauntem.txt. Good luck figuring that out.");
		return;
	}

	StartPrepSDKCall(SDKCall_Player);
	PrepSDKCall_SetFromConf(conf, SDKConf_Signature, "CTFPlayer::PlayTauntSceneFromItem");
	PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
	PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain);
	hPlayTaunt = EndPrepSDKCall();

	if (hPlayTaunt == null)
	{
		SetFailState("Unable to initialize call to CTFPlayer::PlayTauntSceneFromItem. Wait patiently for a fix.");
		CloseHandle(conf);
		return;
	}

	RegConsoleCmd("sm_taunt", Cmd_TauntMenu, "Taunt Menu");
	RegConsoleCmd("sm_taunts", Cmd_TauntMenu, "Taunt Menu");

	CloseHandle(conf);
	LoadTranslations("common.phrases");
	CreateConVar("tf_tauntmenu_version", PLUGIN_VERSION, "[NGS] Taunt Menu Version", FCVAR_NOTIFY);

	PrecacheModel("models/player/items/taunts/cash_wad.mdl", true);
	PrecacheModel("models/player/items/taunts/medic_xray_taunt.mdl", true);
	PrecacheModel("models/player/items/taunts/victory_mug.mdl", true);
	PrecacheModel("models/player/items/taunts/balloon_animal_pyro/balloon_animal_pyro.mdl", true);
	PrecacheModel("models/player/items/taunts/beer_crate/beer_crate.mdl", true);
	PrecacheModel("models/player/items/taunts/chicken_bucket/chicken_bucket.mdl", true);
	PrecacheModel("models/player/items/taunts/demo_nuke_bottle/demo_nuke_bottle.mdl", true);
	PrecacheModel("models/player/items/taunts/dizzy_bottle1/dizzy_bottle1.mdl", true);
	PrecacheModel("models/player/items/taunts/dizzy_bottle2/dizzy_bottle2.mdl", true);
	PrecacheModel("models/player/items/taunts/engys_new_chair/engys_new_chair.mdl", true);
	PrecacheModel("models/player/items/taunts/engys_new_chair/engys_new_chair_articulated.mdl", true);
	PrecacheModel("models/player/items/taunts/wupass_mug/wupass_mug.mdl", true);
	PrecacheModel("models/workshop/player/items/taunts/pyro_poolparty/pyro_poolparty.mdl", true);
	PrecacheModel("models/workshop/player/items/spy/taunt_spy_boxtrot/taunt_spy_boxtrot.mdl", true);
	PrecacheModel("models/workshop/player/items/sniper/killer_solo/killer_solo.mdl", true);
	PrecacheModel("models/workshop/player/items/sniper/taunt_most_wanted/taunt_most_wanted.mdl", true);
}

public Action Cmd_TauntMenu(int client, int args)
{
	ShowMenu(client);
	return Plugin_Handled;
}

public Action ShowMenu(int client)
{
	TFClassType class = TF2_GetPlayerClass(client);
	Menu menu = CreateMenu(Taunt_MenuSelected);
	SetMenuTitle(menu, "===== NGS Taunt Menu =====");

	switch(class)
	{
		case TFClass_Scout:
		{
			AddMenuItem(menu, "1117", "Taunt: Battin' a Thousand");
			AddMenuItem(menu, "1119", "Taunt: Deep Fried Desire");
			AddMenuItem(menu, "1168", "Taunt: The Carlton");
			AddMenuItem(menu, "30572", "Taunt: The Boston Breakdance");
		}
		case TFClass_Sniper:
		{
			AddMenuItem(menu, "1116", "Taunt: I See You");
			AddMenuItem(menu, "30609", "Taunt: The Killer Solo");
			AddMenuItem(menu, "30614", "Taunt: Most Wanted");
		}
		case TFClass_Soldier:
		{
			AddMenuItem(menu, "1113", "Taunt: Fresh Brewed Victory");
			AddMenuItem(menu, "30673", "Taunt: Soldier's Requiem");
			AddMenuItem(menu, "30761", "Taunt: The Fubar Fanfare");
		}
		case TFClass_Heavy:
		{
			AddMenuItem(menu, "30616", "Taunt: The Proletariat Posedown");
		}
		case TFClass_DemoMan:
		{
			AddMenuItem(menu, "1114", "Taunt: Spent Well Spirits");
			AddMenuItem(menu, "1120", "Taunt: Oblooterated");
			AddMenuItem(menu, "30671", "Taunt: True Scotsman's Call");
		}
		case TFClass_Medic:
		{
			AddMenuItem(menu, "477", "Taunt: The Meet the Medic");
			AddMenuItem(menu, "1109", "Taunt: Results Are In");
		}

		case TFClass_Pyro:
		{
			AddMenuItem(menu, "1112", "Taunt: Party Trick");
			AddMenuItem(menu, "30570", "Taunt: Pool Party");
			AddMenuItem(menu, "30763", "Taunt: The Balloonibouncer");
		}
		case TFClass_Spy:
		{
			AddMenuItem(menu, "1108", "Taunt: Buy A Life");
			AddMenuItem(menu, "30615", "Taunt: The Box Trot");
			AddMenuItem(menu, "30762", "Taunt: Disco Fever");
		}
		case TFClass_Engineer:
		{
			AddMenuItem(menu, "1115", "Taunt: Rancho Relaxo");
			AddMenuItem(menu, "30618", "Taunt: Bucking Bronco");
		}
	}

	AddMenuItem(menu, "167", "Taunt: The High Five!");
	AddMenuItem(menu, "438", "Taunt: The Director's Vision");
	AddMenuItem(menu, "463", "Taunt: The Schadenfreude");
	AddMenuItem(menu, "1015", "Taunt: The Shred Alert");
	AddMenuItem(menu, "1106", "Taunt: Square Dance");
	AddMenuItem(menu, "1107", "Taunt: Flippin' Awesome");
	AddMenuItem(menu, "1110", "Taunt: Rock, Paper, Scissors");
	AddMenuItem(menu, "1111", "Taunt: Skullcracker");
	AddMenuItem(menu, "1118", "Taunt: Conga");
	AddMenuItem(menu, "1157", "Taunt: Kazotsky Kick");
	AddMenuItem(menu, "1162", "Taunt: Mannrobics");
	AddMenuItem(menu, "30621", "Taunt: Burstchester");
	AddMenuItem(menu, "30672", "Taunt: Zoomin' Broom");

	DisplayMenu(menu, client, 20);
}

public int Taunt_MenuSelected(Handle menu, MenuAction action, int iClient, int param2)
{
	if(action == MenuAction_End)
	{
		CloseHandle(menu);
	}

	if(action == MenuAction_Select)
	{
		char info[12];

		GetMenuItem(menu, param2, info, sizeof(info));
		ExecuteTaunt(iClient, StringToInt(info));
	}
}

public void ExecuteTaunt(int client, int itemdef)
{
	Handle hItem = TF2Items_CreateItem(OVERRIDE_ALL|PRESERVE_ATTRIBUTES|FORCE_GENERATION);

	TF2Items_SetClassname(hItem, "tf_wearable_vm");
	TF2Items_SetQuality(hItem, 6);
	TF2Items_SetLevel(hItem, 1);
	TF2Items_SetNumAttributes(hItem, 0);
	TF2Items_SetItemIndex(hItem, itemdef);

	int ent = TF2Items_GiveNamedItem(client, hItem);
	Address pEconItemView = GetEntityAddress(ent) + view_as<Address>(FindSendPropInfo("CTFWearable", "m_Item"));

	SDKCall(hPlayTaunt, client, pEconItemView) ? 1 : 0;
	AcceptEntityInput(ent, "Kill");
}


Add public version cvar

Add public version cvar

/**
 * vim: set ts=4 :
 * =============================================================================
 * _plugin_name_
 * TODO: Describe this plugin
 *
 * Copyright _the_year_ _your_name_
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <_your_plugin_>

#define PLUGIN_VERSION "0.1"
#define PLUGIN_NAME "_plugin_name_"

public Plugin:myinfo =
{
    name = PLUGIN_NAME,
    author = "_your_name_",
    description = "TODO: description",
    version = PLUGIN_VERSION,
    url = "https://github.com/_your_name_/_plugin_name_"
};

new Handle:g_Cvar_Enabled = INVALID_HANDLE;

public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max)
{
    if (LibraryExists("_plugin_name_"))
    {
        strcopy(error, err_max, "_plugin_name_ already loaded, aborting.");
        return APLRes_Failure;
    }

    RegPluginLibrary("_plugin_name_");

    return APLRes_Success;
}

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

    g_Cvar_Enabled = CreateConVar("sm__plugin_name__enabled", "1", "Enabled");

    RegConsoleCmd("sm_test", Command_Test, "TODO: TEST");
}

public Action:Command_Test(client, args)
{
    return Plugin_Handled;
}

/**
 * vim: set ts=4 :
 * =============================================================================
 * _plugin_name_
 * TODO: Describe this plugin
 *
 * Copyright _the_year_ _your_name_
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <_your_plugin_>

#define PLUGIN_VERSION "0.1"

public Plugin:myinfo =
{
    name = "_plugin_name_",
    author = "_your_name_",
    description = "TODO: description",
    version = PLUGIN_VERSION,
    url = "https://github.com/_your_name_/_plugin_name_"
};

new Handle:g_Cvar_Enabled = INVALID_HANDLE;

public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max)
{
    if (LibraryExists("_plugin_name_"))
    {
        strcopy(error, err_max, "_plugin_name_ already loaded, aborting.");
        return APLRes_Failure;
    }

    RegPluginLibrary("_plugin_name_");

    return APLRes_Success;
}

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

    g_Cvar_Enabled = CreateConVar("sm__plugin_name__enabled", "1", "Enabled");

    RegConsoleCmd("sm_test", Command_Test, "TODO: TEST");
}

public Action:Command_Test(client, args)
{
    return Plugin_Handled;
}


Organize fuel cvars

Organize fuel cvars

/**
 * vim: set ts=4 :
 * =============================================================================
 * Jetpack Plus
 * Adds a fuel system to the jetpack.  Using
 * the jetpack depletes fuel and you can only
 * use the jetpack if you have fuel.
 *
 * Copyright 2014 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <jetpack_plus>

#define PLUGIN_VERSION "1.0"

#define MAX_JETPACK_TYPES 64

public Plugin:myinfo =
{
    name = "Jetpack Plus (Fuel)",
    author = "CrimsonTautology",
    description = "Adds a fuel based system to jetpack",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_jetpack_plus"
};

//CVARs
new Handle:g_Cvar_Enabled = INVALID_HANDLE;
new Handle:g_Cvar_JetpackFuelMax = INVALID_HANDLE;
new Handle:g_Cvar_JetpackRefuelingTime = INVALID_HANDLE;

//Player values
new g_Fuel[MAXPLAYERS+1] = {0, ...};

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

    g_Cvar_Enabled = CreateConVar(
            "sm_jetpack_fuel_enabled",
            "1",
            "Set to 1 to require jetpacks to use fuel"
            0,
            true,
            0.0,
            true,
            1.0);
    g_Cvar_JetpackFuelMax = CreateConVar(
            "sm_jetpack_fuel",
            "100",
            "The ammount of fuel a player starts with");
    g_Cvar_JetpackRefuelingTime = CreateConVar("sm_jetpack_refueling_time", "30.0", "Time in seconds a player must wait until their jetpack refuels on their own");
}

public OnClientConnected(client)
{
    RefuelClient(client);
}

public Action:OnStartJetpack(client)
{
    if(!IsFuelEnabled()) return Plugin_Continue;
    if(GetFuelOfClient(client))return Plugin_Handled;
    return Plugin_Continue;
}

public Action:OnJetpackStep(client, &Float:force, &bool:force_stop)
{
    if(!IsFuelEnabled()) return Plugin_Continue;

    UseFuelOfClient(client);

    if(GetFuelOfClient(client) <= 0)
    {
        OutOfFuel(client);
        force_stop = true;
    }

    return Plugin_Continue;
}

OutOfFuel(client)
{
    new player = GetClientUserId(client);
    //EmitSoundToClient(client, g_EmptySound);
    CreateTimer(GetConVarFloat(g_Cvar_JetpackRefuelingTime), AutoRefuel, player);
}

Action:AutoRefuel(Handle:timer, any:player)
{
    new client = GetClientOfUserId(player);
    if(client && IsPlayerAlive(client))
    {
        RefuelClient(client);
        //EmitSoundToClient(client, g_RefuelSound);
    }
}

bool:IsFuelEnabled()
{
    return GetConVarBool(g_Cvar_Enabled);
}

GetFuelOfClient(client)
{
    return g_Fuel[client];
}

UseFuelOfClient(client)
{
    g_Fuel[client] -= 1
}

RefuelClient(client)
{
    g_Fuel[client] = GetConVarInt(g_Cvar_JetpackFuelMax)
}

/**
 * vim: set ts=4 :
 * =============================================================================
 * Jetpack Plus
 * Adds a fuel system to the jetpack.  Using
 * the jetpack depletes fuel and you can only
 * use the jetpack if you have fuel.
 *
 * Copyright 2014 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <jetpack_plus>

#define PLUGIN_VERSION "1.0"

#define MAX_JETPACK_TYPES 64

public Plugin:myinfo =
{
    name = "Jetpack Plus (Fuel)",
    author = "CrimsonTautology",
    description = "Adds a fuel based system to jetpack",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_jetpack_plus"
};

//CVARs
new Handle:g_Cvar_Enabled = INVALID_HANDLE;
new Handle:g_Cvar_JetpackFuelMax = INVALID_HANDLE;
new Handle:g_Cvar_JetpackRefuelingTime = INVALID_HANDLE;

//Player values
new g_Fuel[MAXPLAYERS+1] = {0, ...};

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

    g_Cvar_Enabled = CreateConVar("sm_jetpack_fuel_enabled", "1", "Whether the fueling system is enabled");
    g_Cvar_JetpackFuelMax = CreateConVar("sm_jetpack_fuel", "100", "The max fuel value");
    g_Cvar_JetpackRefuelingTime = CreateConVar("sm_jetpack_refueling_time", "30.0", "Time in seconds a player must wait until their jetpack refuels on their own");
}

public OnClientConnected(client)
{
    RefuelClient(client);
}

public Action:OnStartJetpack(client)
{
    if(!IsFuelEnabled()) return Plugin_Continue;
    if(GetFuelOfClient(client))return Plugin_Handled;
    return Plugin_Continue;
}

public Action:OnJetpackStep(client, &Float:force, &bool:force_stop)
{
    if(!IsFuelEnabled()) return Plugin_Continue;

    UseFuelOfClient(client);

    if(GetFuelOfClient(client) <= 0)
    {
        OutOfFuel(client);
        force_stop = true;
    }

    return Plugin_Continue;
}

OutOfFuel(client)
{
    new player = GetClientUserId(client);
    //EmitSoundToClient(client, g_EmptySound);
    CreateTimer(GetConVarFloat(g_Cvar_JetpackRefuelingTime), AutoRefuel, player);
}

Action:AutoRefuel(Handle:timer, any:player)
{
    new client = GetClientOfUserId(player);
    if(client && IsPlayerAlive(client))
    {
        RefuelClient(client);
        //EmitSoundToClient(client, g_RefuelSound);
    }
}

bool:IsFuelEnabled()
{
    return GetConVarBool(g_Cvar_Enabled);
}

GetFuelOfClient(client)
{
    return g_Fuel[client];
}

UseFuelOfClient(client)
{
    g_Fuel[client] -= 1
}

RefuelClient(client)
{
    g_Fuel[client] = GetConVarInt(g_Cvar_JetpackFuelMax)
}


Stub out particles

Stub out particles

/**
 * vim: set ts=4 :
 * =============================================================================
 * Jetpack Plus
 * TODO: Describe this plugin
 *
 * Copyright 2014 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <sdktools>
#include <particle>
#include <smlib/entities>

#define PLUGIN_VERSION "0.1"

#define MOVECOLLIDE_DEFAULT		0
#define MOVECOLLIDE_FLY_BOUNCE	1

public Plugin:myinfo =
{
    name = "Jetpack Plus",
    author = "CrimsonTautology",
    description = "TODO: description",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_jetpack_plus"
};

new Handle:g_Cvar_Enabled = INVALID_HANDLE;
new Handle:g_Cvar_JetpackSound = INVALID_HANDLE;
new Handle:g_Cvar_JetpackSpeed = INVALID_HANDLE;

new g_Offset_movecollide = -1;

new bool:g_IsUsingJetpack[MAXPLAYERS+1] = {false, ...};
new String:g_JetpackSound[PLATFORM_MAX_PATH];

new g_JetpackParticle[MAXPLAYERS+1][2];
new String:g_JetpackParticleName[64];

public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max)
{
    if (LibraryExists("jetpack_plus"))
    {
        strcopy(error, err_max, "Jetpack Plus already loaded, aborting.");
        return APLRes_Failure;
    }

    RegPluginLibrary("jetpack_plus");

    return APLRes_Success;
}

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

    g_Cvar_Enabled = CreateConVar(
        "sm_jetpack",
        "1",
        "Set to 1 to enable the jetpack plugin");
    g_Cvar_JetpackSound = CreateConVar(
        "sm_jetpack_sound",
        "vehicles/airboat/fan_blade_fullthrottle_loop1.wav",
        "The default sound for the jetpack"
        );
    g_Cvar_JetpackSpeed = CreateConVar(
        "sm_jetpack_speed",
        "8.0",
        "Speed of the jetpack"
        );

    RegConsoleCmd("sm_jp", Command_Jp, "Change jetpack particle");

    if((g_Offset_movecollide = FindSendPropOffs("CBaseEntity", "movecollide")) == -1)
        LogError("Could not find offset for CBaseEntity::movecollide");
}

public Action:Command_Jp(client, args)
{

    if(client && IsClientAuthorized(client)){
        decl String:path[64];
        GetCmdArgString(path, sizeof(path));
        strcopy(g_JetpackParticleName, 64, path);
    }

    return Plugin_Handled;
}

public OnConfigsExecuted()
{
    GetConVarString(g_Cvar_JetpackSound, g_JetpackSound, sizeof(g_JetpackSound));
    PrecacheSound(g_JetpackSound, true);
}

public OnClientDisconnect(client)
{
    StopJetpack(client);
}

public OnGameFrame()
{
    if(AreJetpacksEnabled())
    {
        for (new client=1; client <= MaxClients; client++)
        {
            //For each client using a jetpack
            if(IsClientUsingJetpack(client))
            {
                JetpackStep(client);
            }

        }

    }
}

public Action:OnPlayerRunCmd(client, &buttons, &impulse, Float:vel[3], Float:angles[3], &weapon, &subtype, &cmdnum, &tickcount, &seed, mouse[2])
{
    if(!AreJetpacksEnabled()) return Plugin_Continue;

    if(!IsClientUsingJetpack(client) && buttons & IN_JUMP)
    {
        new player = GetClientUserId(client);
        CreateTimer(0.17, HeldJump, player);
    }

    return Plugin_Continue;
}

//Call back for when a player presses jump.  Checks if jump is still held down
public Action:HeldJump(Handle:timer, any:player)
{
    new client = GetClientOfUserId(player);

    if(!IsClientInGame(client)) return Plugin_Handled;

    if(!IsClientUsingJetpack(client) && GetClientButtons(client) & IN_JUMP)
    {
        StartJetpack(client);
    }

    return Plugin_Handled;
}

bool:IsClientUsingJetpack(client)
{
    return g_IsUsingJetpack[client];
}

bool:AreJetpacksEnabled()
{
    return GetConVarBool(g_Cvar_Enabled);
}

StartJetpack(client)
{
    SetEntityMoveType(client, MOVETYPE_FLY);
    SetEntityMoveCollide(client, MOVECOLLIDE_FLY_BOUNCE);
    g_IsUsingJetpack[client] = true;

    EmitSoundToAll(g_JetpackSound, client, SNDCHAN_AUTO);

    static const Float:ang[3] = { -25.0, 90.0, 0.0 };
    static const Float:pos[3] = {   0.0, 10.0, 1.0 };

    //https://forums.alliedmods.net/showthread.php?t=127111
    //pyrovision_flaming_arrow
    //pyrovision_flying_flaming_arrow
    //ghost_pumpkin
    //ghost_pumpkin_flyingbits
    //burningplayer_rainbow
    //burningplayer_corpse_rainbow_stars
    //burningplayer_rainbow_OLD
    //flamethrower_rainbow_bubbles02
    //burninggibs
    //electrocuted_blue
    //spell_batball_blue
    //g_JetpackParticle[client][0] = CreateParticle("halloween_rockettrail", 0.0, client, Attach, "flag", pos, ang);
    g_JetpackParticle[client][0] = CreateParticle(g_JetpackParticleName, 0.0, client, Attach, "flag", pos, ang);
}

StopJetpack(client)
{
    SetEntityMoveType(client, MOVETYPE_WALK);
    SetEntityMoveCollide(client, MOVECOLLIDE_DEFAULT);
    g_IsUsingJetpack[client] = false;
    StopSound(client, SNDCHAN_AUTO, g_JetpackSound);
    DeleteParticle(g_JetpackParticle[client][0]);
}

//Called each frame a client is using a jetpack
JetpackStep(client)
{
    new buttons = GetClientButtons(client);
    if(IsPlayerAlive(client) && (buttons & IN_JUMP))
    {
        JetpackPush(client, GetConVarFloat(g_Cvar_JetpackSpeed));
    }
    else
    {
        StopJetpack(client);
    }
}

JetpackPush(client, Float:force)
{
    new Float:vec[3];
    Entity_GetBaseVelocity(client, vec);
    vec[2] += force;
    Entity_SetBaseVelocity(client, vec);
}

SetEntityMoveCollide(entity, movecollide)
{
    if(g_Offset_movecollide == -1) return;
    SetEntData(entity, g_Offset_movecollide, movecollide);
}

/**
 * vim: set ts=4 :
 * =============================================================================
 * Jetpack Plus
 * TODO: Describe this plugin
 *
 * Copyright 2014 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <sdktools>
#include <particle>
#include <smlib/entities>

#define PLUGIN_VERSION "0.1"

#define MOVECOLLIDE_DEFAULT		0
#define MOVECOLLIDE_FLY_BOUNCE	1

public Plugin:myinfo =
{
    name = "Jetpack Plus",
    author = "CrimsonTautology",
    description = "TODO: description",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_jetpack_plus"
};

new Handle:g_Cvar_Enabled = INVALID_HANDLE;
new Handle:g_Cvar_JetpackSound = INVALID_HANDLE;
new Handle:g_Cvar_JetpackSpeed = INVALID_HANDLE;

new g_Offset_movecollide = -1;

new bool:g_IsUsingJetpack[MAXPLAYERS+1] = {false, ...};
new String:g_JetpackSound[PLATFORM_MAX_PATH];

new g_JetpackParticle[MAXPLAYERS+1][2];

public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max)
{
    if (LibraryExists("jetpack_plus"))
    {
        strcopy(error, err_max, "Jetpack Plus already loaded, aborting.");
        return APLRes_Failure;
    }

    RegPluginLibrary("jetpack_plus");

    return APLRes_Success;
}

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

    g_Cvar_Enabled = CreateConVar(
        "sm_jetpack",
        "1",
        "Set to 1 to enable the jetpack plugin");
    g_Cvar_JetpackSound = CreateConVar(
        "sm_jetpack_sound",
        "vehicles/airboat/fan_blade_fullthrottle_loop1.wav",
        "The default sound for the jetpack"
        );
    g_Cvar_JetpackSpeed = CreateConVar(
        "sm_jetpack_speed",
        "8.0",
        "Speed of the jetpack"
        );

    if((g_Offset_movecollide = FindSendPropOffs("CBaseEntity", "movecollide")) == -1)
        LogError("Could not find offset for CBaseEntity::movecollide");
}

public OnConfigsExecuted()
{
    GetConVarString(g_Cvar_JetpackSound, g_JetpackSound, sizeof(g_JetpackSound));
    PrecacheSound(g_JetpackSound, true);
}

public OnClientDisconnect(client)
{
    StopJetpack(client);
}

public OnGameFrame()
{
    if(AreJetpacksEnabled())
    {
        for (new client=1; client <= MaxClients; client++)
        {
            //For each client using a jetpack
            if(IsClientUsingJetpack(client))
            {
                JetpackStep(client);
            }

        }

    }
}

public Action:OnPlayerRunCmd(client, &buttons, &impulse, Float:vel[3], Float:angles[3], &weapon, &subtype, &cmdnum, &tickcount, &seed, mouse[2])
{
    if(!AreJetpacksEnabled()) return Plugin_Continue;

    if(!IsClientUsingJetpack(client) && buttons & IN_JUMP)
    {
        new player = GetClientUserId(client);
        CreateTimer(0.11, HeldJump, player);
    }

    return Plugin_Continue;
}

//Call back for when a player presses jump.  Checks if jump is still held down
public Action:HeldJump(Handle:timer, any:player)
{
    new client = GetClientOfUserId(player);

    if(!IsClientInGame(client)) return Plugin_Handled;

    if(!IsClientUsingJetpack(client) && GetClientButtons(client) & IN_JUMP)
    {
        StartJetpack(client);
    }

    return Plugin_Handled;
}

bool:IsClientUsingJetpack(client)
{
    return g_IsUsingJetpack[client];
}

bool:AreJetpacksEnabled()
{
    return GetConVarBool(g_Cvar_Enabled);
}

StartJetpack(client)
{
    SetEntityMoveType(client, MOVETYPE_FLY);
    SetEntityMoveCollide(client, MOVECOLLIDE_FLY_BOUNCE);
    g_IsUsingJetpack[client] = true;

    EmitSoundToAll(g_JetpackSound, client, SNDCHAN_AUTO);

    static const Float:ang[3] = { -25.0, 90.0, 0.0 };
    static const Float:pos[3] = {   0.0, 10.0, 1.0 };

    CreateParticle("muzzle_minigun", 0.15, client, Attach, "flag", pos, ang);
}

StopJetpack(client)
{
    SetEntityMoveType(client, MOVETYPE_WALK);
    SetEntityMoveCollide(client, MOVECOLLIDE_DEFAULT);
    g_IsUsingJetpack[client] = false;
    StopSound(client, SNDCHAN_AUTO, g_JetpackSound);
}

//Called each frame a client is using a jetpack
JetpackStep(client)
{
    new buttons = GetClientButtons(client);
    if(IsPlayerAlive(client) && (buttons & IN_JUMP))
    {
        JetpackPush(client, GetConVarFloat(g_Cvar_JetpackSpeed));
    }
    else
    {
        StopJetpack(client);
    }
}

JetpackPush(client, Float:force)
{
    new Float:vec[3];
    Entity_GetBaseVelocity(client, vec);
    vec[2] += force;
    Entity_SetBaseVelocity(client, vec);
}

SetEntityMoveCollide(entity, movecollide)
{
    if(g_Offset_movecollide == -1) return;
    SetEntData(entity, g_Offset_movecollide, movecollide);
}


Implement dontator logic

Implement dontator logic

/**
 * vim: set ts=4 :
 * =============================================================================
 * Jetpack Plus
 * TODO: Describe this plugin
 *
 * Copyright 2014 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <sdktools>
#include <particle>
#include <smlib/entities>
#undef REQUIRE_PLUGIN
#include <donator>

#define PLUGIN_VERSION "0.1"

#define MOVECOLLIDE_DEFAULT		0
#define MOVECOLLIDE_FLY_BOUNCE	1

#define MAX_JETPACK_TYPES 64

public Plugin:myinfo =
{
    name = "Jetpack Plus",
    author = "CrimsonTautology",
    description = "TODO: description",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_jetpack_plus"
};

new Handle:g_Cvar_Enabled = INVALID_HANDLE;
new Handle:g_Cvar_JetpackSpeed = INVALID_HANDLE;

new bool:g_DonatorLibraryExists = false;

new g_Offset_movecollide = -1;

new bool:g_IsUsingJetpack[MAXPLAYERS+1] = {false, ...};

//Player options
new g_ClientSelectedJetpackType[MAXPLAYERS+1] = {0, ...};
new g_JetpackParticle[MAXPLAYERS+1][2]; //TODO

//Parallel arrays to store types of jetpacks
new String:g_JetpackTypeName[MAX_JETPACK_TYPES][PLATFORM_MAX_PATH];
new String:g_JetpackTypeParticle[MAX_JETPACK_TYPES][PLATFORM_MAX_PATH];
new String:g_JetpackTypeSound[MAX_JETPACK_TYPES][PLATFORM_MAX_PATH];
new g_JetpackTypeCount = 0;

public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max)
{
    if (LibraryExists("jetpack_plus"))
    {
        strcopy(error, err_max, "Jetpack Plus already loaded, aborting.");
        return APLRes_Failure;
    }

    RegPluginLibrary("jetpack_plus");

    MarkNativeAsOptional("IsPlayerDonator");
    MarkNativeAsOptional("Donator_RegisterMenuItem");

    return APLRes_Success;
}

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

    g_Cvar_Enabled = CreateConVar(
            "sm_jetpack",
            "1",
            "Set to 1 to enable the jetpack plugin");
    g_Cvar_JetpackSpeed = CreateConVar(
            "sm_jetpack_speed",
            "8.0",
            "Speed of the jetpack"
            );

    if((g_Offset_movecollide = FindSendPropOffs("CBaseEntity", "movecollide")) == -1)
        LogError("Could not find offset for CBaseEntity::movecollide");

    g_DonatorLibraryExists = LibraryExists("donator.core");
}

public OnAllPluginsLoaded()
{
    if (g_DonatorLibraryExists)
    {
        Donator_RegisterMenuItem("Jetpack Bling", JetpackBlingMenu);
    }
}

public OnLibraryRemoved(const String:name[])
{
    if (StrEqual(name, "donator.core"))
    {
        g_DonatorLibraryExists = false;
    }
}

public OnLibraryAdded(const String:name[])
{
    if (StrEqual(name, "donator.core"))
    {
        g_DonatorLibraryExists = true;
    }
}

public OnMapStart()
{
    ReadJetpacks();
}

ReadJetpacks()
{
    g_JetpackTypeCount = 0;
    new Handle:kv = CreateKeyValues("Jetpacks");

    decl String:path[PLATFORM_MAX_PATH];
    BuildPath(Path_SM, path, sizeof(path), "configs/jetpacks.cfg");

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

        do
        {
            KvGetString(kv, "name", g_JetpackTypeName[g_JetpackTypeCount], PLATFORM_MAX_PATH);
            KvGetString(kv, "particle", g_JetpackTypeParticle[g_JetpackTypeCount], PLATFORM_MAX_PATH);
            KvGetString(kv, "sound", g_JetpackTypeSound[g_JetpackTypeCount], PLATFORM_MAX_PATH);
            PrecacheSound(g_JetpackTypeSound[g_JetpackTypeCount], true);
            g_JetpackTypeCount++;

        } while(KvGotoNextKey(kv) && g_JetpackTypeCount < MAX_JETPACK_TYPES);

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

    CloseHandle(kv);
}

public OnClientDisconnect(client)
{
    StopJetpack(client);
}

public OnGameFrame()
{
    if(AreJetpacksEnabled())
    {
        for (new client=1; client <= MaxClients; client++)
        {
            //For each client using a jetpack
            if(IsClientUsingJetpack(client))
            {
                JetpackStep(client);
            }

        }

    }
}

public Action:OnPlayerRunCmd(client, &buttons, &impulse, Float:vel[3], Float:angles[3], &weapon, &subtype, &cmdnum, &tickcount, &seed, mouse[2])
{
    if(!AreJetpacksEnabled()) return Plugin_Continue;

    if(!IsClientUsingJetpack(client) && buttons & IN_JUMP)
    {
        new player = GetClientUserId(client);
        CreateTimer(0.17, HeldJump, player);
    }

    return Plugin_Continue;
}

//Call back for when a player presses jump.  Checks if jump is still held down
public Action:HeldJump(Handle:timer, any:player)
{
    new client = GetClientOfUserId(player);

    if(!IsClientInGame(client)) return Plugin_Handled;

    if(!IsClientUsingJetpack(client) && GetClientButtons(client) & IN_JUMP)
    {
        StartJetpack(client);
    }

    return Plugin_Handled;
}

bool:IsClientUsingJetpack(client)
{
    return g_IsUsingJetpack[client];
}

bool:AreJetpacksEnabled()
{
    return GetConVarBool(g_Cvar_Enabled);
}

StartJetpack(client)
{
    SetEntityMoveType(client, MOVETYPE_FLY);
    SetEntityMoveCollide(client, MOVECOLLIDE_FLY_BOUNCE);
    g_IsUsingJetpack[client] = true;

    new selected = g_ClientSelectedJetpackType[client];
    EmitSoundToAll(g_JetpackTypeSound[selected], client, SNDCHAN_AUTO);

    static const Float:ang[3] = { -25.0, 90.0, 0.0 };
    static const Float:pos[3] = {   0.0, 10.0, 1.0 };
    g_JetpackParticle[client][0] = CreateParticle(g_JetpackTypeParticle[selected], 0.0, client, Attach, "flag", pos, ang);

    //https://forums.alliedmods.net/showthread.php?t=127111
    //pyrovision_flaming_arrow
    //pyrovision_flying_flaming_arrow
    //ghost_pumpkin
    //ghost_pumpkin_flyingbits

    //burningplayer_corpse_rainbow_stars
    //burningplayer_rainbow_OLD
    //flamethrower_rainbow_bubbles02
    //burninggibs
    //electrocuted_blue
    //spell_batball_blue
    //g_JetpackParticle[client][0] = CreateParticle("halloween_rockettrail", 0.0, client, Attach, "flag", pos, ang);
}

StopJetpack(client)
{
    //TODO handle changes in sound and particle while jetpack active
    SetEntityMoveType(client, MOVETYPE_WALK);
    SetEntityMoveCollide(client, MOVECOLLIDE_DEFAULT);
    g_IsUsingJetpack[client] = false;

    new selected = g_ClientSelectedJetpackType[client];
    StopSound(client, SNDCHAN_AUTO, g_JetpackTypeSound[selected]);
    DeleteParticle(g_JetpackParticle[client][0]);
}

//Called each frame a client is using a jetpack
JetpackStep(client)
{
    new buttons = GetClientButtons(client);
    if(IsPlayerAlive(client) && (buttons & IN_JUMP))
    {
        JetpackPush(client, GetConVarFloat(g_Cvar_JetpackSpeed));
    }
    else
    {
        StopJetpack(client);
    }
}

JetpackPush(client, Float:force)
{
    new Float:vec[3];
    Entity_GetBaseVelocity(client, vec);
    vec[2] += force;
    Entity_SetBaseVelocity(client, vec);
}

SetEntityMoveCollide(entity, movecollide)
{
    if(g_Offset_movecollide == -1) return;
    SetEntData(entity, g_Offset_movecollide, movecollide);
}

//Menus
public DonatorMenu:JetpackBlingMenu(client) ChangeJetpackMenu;
public ChangeJetpackMenu(client)
{
    new Handle:menu = CreateMenu(ChangeJetpackMenuHandler);

    SetMenuTitle(menu, "Choose your jetpack");

    decl String:buf[16];
    for(new i=0; i < g_JetpackTypeCount && i < MAX_JETPACK_TYPES; i++)
    {
        IntToString(i, buf, sizeof(buf));
        AddMenuItem(menu, buf, g_JetpackTypeName[i]);
    }

    DisplayMenu(menu, client, 20);
}

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

/**
 * vim: set ts=4 :
 * =============================================================================
 * Jetpack Plus
 * TODO: Describe this plugin
 *
 * Copyright 2014 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <sdktools>
#include <particle>
#include <smlib/entities>

#define PLUGIN_VERSION "0.1"

#define MOVECOLLIDE_DEFAULT		0
#define MOVECOLLIDE_FLY_BOUNCE	1

#define MAX_JETPACK_TYPES 64

public Plugin:myinfo =
{
    name = "Jetpack Plus",
    author = "CrimsonTautology",
    description = "TODO: description",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_jetpack_plus"
};

new Handle:g_Cvar_Enabled = INVALID_HANDLE;
new Handle:g_Cvar_JetpackSpeed = INVALID_HANDLE;

new g_Offset_movecollide = -1;

new bool:g_IsUsingJetpack[MAXPLAYERS+1] = {false, ...};

//Player options
new g_ClientSelectedJetpackType[MAXPLAYERS+1] = {0, ...};
new g_JetpackParticle[MAXPLAYERS+1][2]; //TODO

//Parallel arrays to store types of jetpacks
new String:g_JetpackTypeName[MAX_JETPACK_TYPES][PLATFORM_MAX_PATH];
new String:g_JetpackTypeParticle[MAX_JETPACK_TYPES][PLATFORM_MAX_PATH];
new String:g_JetpackTypeSound[MAX_JETPACK_TYPES][PLATFORM_MAX_PATH];
new g_JetpackTypeCount = 0;

public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max)
{
    if (LibraryExists("jetpack_plus"))
    {
        strcopy(error, err_max, "Jetpack Plus already loaded, aborting.");
        return APLRes_Failure;
    }

    RegPluginLibrary("jetpack_plus");

    return APLRes_Success;
}

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

    g_Cvar_Enabled = CreateConVar(
            "sm_jetpack",
            "1",
            "Set to 1 to enable the jetpack plugin");
    g_Cvar_JetpackSpeed = CreateConVar(
            "sm_jetpack_speed",
            "8.0",
            "Speed of the jetpack"
            );

    if((g_Offset_movecollide = FindSendPropOffs("CBaseEntity", "movecollide")) == -1)
        LogError("Could not find offset for CBaseEntity::movecollide");
}

public OnMapStart()
{
    ReadJetpacks();
}

ReadJetpacks()
{
    g_JetpackTypeCount = 0;
    new Handle:kv = CreateKeyValues("Jetpacks");

    decl String:path[PLATFORM_MAX_PATH];
    BuildPath(Path_SM, path, sizeof(path), "configs/jetpacks.cfg");

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

        do
        {
            KvGetString(kv, "name", g_JetpackTypeName[g_JetpackTypeCount], PLATFORM_MAX_PATH);
            KvGetString(kv, "particle", g_JetpackTypeParticle[g_JetpackTypeCount], PLATFORM_MAX_PATH);
            KvGetString(kv, "sound", g_JetpackTypeSound[g_JetpackTypeCount], PLATFORM_MAX_PATH);
            PrecacheSound(g_JetpackTypeSound[g_JetpackTypeCount], true);
            g_JetpackTypeCount++;

        } while(KvGotoNextKey(kv) && g_JetpackTypeCount < MAX_JETPACK_TYPES);

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

    CloseHandle(kv);
}

public OnClientDisconnect(client)
{
    StopJetpack(client);
}

public OnGameFrame()
{
    if(AreJetpacksEnabled())
    {
        for (new client=1; client <= MaxClients; client++)
        {
            //For each client using a jetpack
            if(IsClientUsingJetpack(client))
            {
                JetpackStep(client);
            }

        }

    }
}

public Action:OnPlayerRunCmd(client, &buttons, &impulse, Float:vel[3], Float:angles[3], &weapon, &subtype, &cmdnum, &tickcount, &seed, mouse[2])
{
    if(!AreJetpacksEnabled()) return Plugin_Continue;

    if(!IsClientUsingJetpack(client) && buttons & IN_JUMP)
    {
        new player = GetClientUserId(client);
        CreateTimer(0.17, HeldJump, player);
    }

    return Plugin_Continue;
}

//Call back for when a player presses jump.  Checks if jump is still held down
public Action:HeldJump(Handle:timer, any:player)
{
    new client = GetClientOfUserId(player);

    if(!IsClientInGame(client)) return Plugin_Handled;

    if(!IsClientUsingJetpack(client) && GetClientButtons(client) & IN_JUMP)
    {
        StartJetpack(client);
    }

    return Plugin_Handled;
}

bool:IsClientUsingJetpack(client)
{
    return g_IsUsingJetpack[client];
}

bool:AreJetpacksEnabled()
{
    return GetConVarBool(g_Cvar_Enabled);
}

StartJetpack(client)
{
    SetEntityMoveType(client, MOVETYPE_FLY);
    SetEntityMoveCollide(client, MOVECOLLIDE_FLY_BOUNCE);
    g_IsUsingJetpack[client] = true;

    new selected = g_ClientSelectedJetpackType[client];
    EmitSoundToAll(g_JetpackTypeSound[selected], client, SNDCHAN_AUTO);

    static const Float:ang[3] = { -25.0, 90.0, 0.0 };
    static const Float:pos[3] = {   0.0, 10.0, 1.0 };
    g_JetpackParticle[client][0] = CreateParticle(g_JetpackTypeParticle[selected], 0.0, client, Attach, "flag", pos, ang);

    //https://forums.alliedmods.net/showthread.php?t=127111
    //pyrovision_flaming_arrow
    //pyrovision_flying_flaming_arrow
    //ghost_pumpkin
    //ghost_pumpkin_flyingbits

    //burningplayer_corpse_rainbow_stars
    //burningplayer_rainbow_OLD
    //flamethrower_rainbow_bubbles02
    //burninggibs
    //electrocuted_blue
    //spell_batball_blue
    //g_JetpackParticle[client][0] = CreateParticle("halloween_rockettrail", 0.0, client, Attach, "flag", pos, ang);
}

StopJetpack(client)
{
    //TODO handle changes in sound and particle while jetpack active
    SetEntityMoveType(client, MOVETYPE_WALK);
    SetEntityMoveCollide(client, MOVECOLLIDE_DEFAULT);
    g_IsUsingJetpack[client] = false;

    new selected = g_ClientSelectedJetpackType[client];
    StopSound(client, SNDCHAN_AUTO, g_JetpackTypeSound[selected]);
    DeleteParticle(g_JetpackParticle[client][0]);
}

//Called each frame a client is using a jetpack
JetpackStep(client)
{
    new buttons = GetClientButtons(client);
    if(IsPlayerAlive(client) && (buttons & IN_JUMP))
    {
        JetpackPush(client, GetConVarFloat(g_Cvar_JetpackSpeed));
    }
    else
    {
        StopJetpack(client);
    }
}

JetpackPush(client, Float:force)
{
    new Float:vec[3];
    Entity_GetBaseVelocity(client, vec);
    vec[2] += force;
    Entity_SetBaseVelocity(client, vec);
}

SetEntityMoveCollide(entity, movecollide)
{
    if(g_Offset_movecollide == -1) return;
    SetEntData(entity, g_Offset_movecollide, movecollide);
}

//Menus
//public DonatorMenu:SpriteControlCallback(client) ChangeJetpackMenu
public ChangeJetpackMenu(client)
{
    new Handle:menu = CreateMenu(ChangeJetpackMenuHandler);

    SetMenuTitle(menu, "Choose your jetpack");

    decl String:buf[16];
    for(new i=0; i < g_JetpackTypeCount; i++)
    {
        IntToString(i, buf, sizeof(buf));
        AddMenuItem(menu, buf, g_JetpackTypeName[i]);
    }

    DisplayMenu(menu, client, 20);
}

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