Implement volume control

Implement volume control

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio
 * TODO - Add your project's description
 *
 * Copyright 2013 ???
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <clientprefs>
#include <steamtools>
#include <base64>
#include <smjansson>
#undef REQUIRE_PLUGIN
#include <donator>

#define PLUGIN_VERSION "0.1"

public Plugin:myinfo =
{
    name = "In Game Audio",
    author = "CrimsonTautology",
    description = "Interact with the In Game Audio web api",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

#define QUERY_SONG_ROUTE "/v1/api/query_song"
#define RANDOM_SONG_ROUTE ""
#define SONGS_ROUTE "/songs"
#define DIRECTORIES_ROUTE "/directories"

#define MAX_STEAMID_LENGTH 21
#define MAX_COMMUNITYID_LENGTH 18
#define MAX_SONG_LENGTH 64

new Handle:g_Cvar_IGAApiKey = INVALID_HANDLE;
new Handle:g_Cvar_IGAUrl = INVALID_HANDLE;
new Handle:g_Cvar_IGADonatorsOnly = INVALID_HANDLE;
new Handle:g_Cvar_IGAEnabled = INVALID_HANDLE;
new Handle:g_Cvar_IGARequestCooldownTime = INVALID_HANDLE;

new Handle:g_Cookie_PallEnabled = INVALID_HANDLE;
new Handle:g_Cookie_Volume = INVALID_HANDLE;

new bool:g_IsInCooldown[MAXPLAYERS+1];
new bool:g_IsPallEnabled[MAXPLAYERS+1];
new bool:g_IsDonator[MAXPLAYERS+1];
new g_PNextFree[MAXPLAYERS+1];
new g_PallNextFree = 0;
new g_Volume[MAXPLAYERS+1];

new bool:g_DonatorLibrary = false;
public OnPluginStart()
{

    g_Cvar_IGAApiKey = CreateConVar("sm_iga_api_key", "", "API Key for your IGA webpage");
    g_Cvar_IGAUrl = CreateConVar("sm_iga_url", "", "URL to your IGA webpage");
    g_Cvar_IGADonatorsOnly = CreateConVar("sm_iga_donators_only", "1", "Whether or not only donators have access to pall");
    g_Cvar_IGAEnabled = CreateConVar("sm_iga_enabled", "1", "Whether or not pall is enabled");
    g_Cvar_IGARequestCooldownTime = CreateConVar("sm_iga_request_cooldown_time", "2.0", "How long in seconds before a client can send another http request");

    RegConsoleCmd("sm_p", Command_P, "Play a song for yourself");
    RegConsoleCmd("sm_pall", Command_Pall, "Play a song for everyone");
    RegConsoleCmd("sm_plist", Command_Plist, "Pop-up the song list");
    RegConsoleCmd("sm_stop", Command_Stop, "Stop the current song");
    RegAdminCmd("sm_fstop", Command_Fstop, ADMFLAG_VOTE, "[ADMIN] Stop the current pall for everyone");
    RegAdminCmd("sm_fpall", Command_Fpall, ADMFLAG_VOTE, "[ADMIN] Force everyone to listen to a song");
    RegConsoleCmd("sm_vol", Command_Vol, "Adjust your play volume");
    RegConsoleCmd("sm_nopall", Command_Nopall, "Turn off pall for yourself");
    RegConsoleCmd("sm_yespall", Command_Yespall, "Turn on pall for yourself");
    RegConsoleCmd("sm_plast", Command_Plast, "Play the last played song for yourself");

    g_Cookie_Volume = RegClientCookie("iga_volume", "Volume to play at [0-10]; 0 muted, 10 loudest", CookieAccess_Private);
    g_Cookie_PallEnabled = RegClientCookie("iga_pall_enabled", "Whether you want pall enabled or not. If yes, you will hear music when other players call !pall", CookieAccess_Private);

    g_DonatorLibrary = LibraryExists("donators");

    //HookEvent("map_change", Event_MapChange);
    //HookEvent("client_connect", Event_ClientConnect);
}

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

public OnLibraryAdded(const String:name[])
{
	if (StrEqual(name, "donators"))
	{
		g_DonatorLibrary = true;
	}
}

public OnClientConnected(client)
{
    g_IsInCooldown[client] = false;
    g_PNextFree[client] = 0;
    g_Volume[client] = 10;
    g_IsPallEnabled[client] = true;

}
public OnClientCookiesCached(client)
{
    new String:buffer[11];

    GetClientCookie(client, g_Cookie_Volume, buffer, sizeof(buffer));
    if (strlen(buffer) > 0)
        g_Volume[client] = StringToInt(buffer);

    GetClientCookie(client, g_Cookie_PallEnabled, buffer, sizeof(buffer));
    if (strlen(buffer) > 0)
        g_IsPallEnabled[client] = bool:StringToInt(buffer);
}

public OnPostDonatorCheck(client)
{
    g_IsDonator[client] = true;
}

public OnClientDisconnect(client)
{
    g_IsDonator[client] = false;
}

public Action:Command_P(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[IGA] User in cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_IGAEnabled))
    {
        ReplyToCommand(client, "[IGA] IGA not enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        decl String:path[MAX_SONG_LENGTH];
        GetCmdArgString(path, sizeof(path));
        QuerySong(client, path, false);
    }

    return Plugin_Handled;
}

public Action:Command_Pall(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[IGA] User in cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_IGAEnabled))
    {
        ReplyToCommand(client, "[IGA] IGA not enabled");
        return Plugin_Handled;
    }

    if(!DonatorCheck(client))
    {
        ReplyToCommand(client, "[IGA] Only donators can use this command");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        decl String:path[MAX_SONG_LENGTH];
        GetCmdArgString(path, sizeof(path));
        QuerySong(client, path, true);
    }

    return Plugin_Handled;
}

public Action:Command_Plist(client, args)
{
    if(client && IsClientAuthorized(client)){
        SongList(client);
    }

    return Plugin_Handled;
}

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

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

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

public Action:Command_Vol(client, args)
{
    //FIXME cleanup
    if (args != 1)
    {
        ReplyToCommand(client, "[IGA] usage \"!vol <0-10>\"");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client))
    {
        decl String:buffer[11];
        new volume;
        GetCmdArgString(buffer, sizeof(buffer));
        volume = StringToInt(buffer);
        if (volume >=0 && volume <= 10)
        {
            SetClientCookie(client, g_Cookie_Volume, buffer);
            g_Volume[client] = volume;
            ReplyToCommand(client, "[IGA] set volume to %d", volume);
        }else{
            ReplyToCommand(client, "[IGA] usage \"!vol <0-10>\"");
        }
    }

    return Plugin_Handled;
}

public Action:Command_Nopall(client, args)
{
    //FIXME cleanup
    if (client && IsClientAuthorized(client))
    {
        SetClientCookie(client, g_Cookie_PallEnabled, "0");
        g_IsPallEnabled[client] = false;
        ReplyToCommand(client, "[IGA] Disabled pall.  Type !yespall to renable it.");
    }
    return Plugin_Handled;
}

public Action:Command_Yespall(client, args)
{
    //FIXME cleanup
    if (client && IsClientAuthorized(client))
    {
        SetClientCookie(client, g_Cookie_PallEnabled, "1");
        g_IsPallEnabled[client] = true;
        ReplyToCommand(client, "[IGA] Enabled pall.  Type !nopall to disable it.");
    }
    return Plugin_Handled;
}

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

public Event_MapChange(Handle:event, const String:name[], bool:dontBroadcast)
{
    //TODO
}

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

public SetAccessCode(&HTTPRequestHandle:request)
{
    decl String:api_key[128];
    GetConVarString(g_Cvar_IGAApiKey, api_key, sizeof(api_key));
    Steam_SetHTTPRequestGetOrPostParameter(request, "access_token", api_key);
}

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

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

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

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

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

    return request;
}

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

    g_IsInCooldown[client] = true;
    CreateTimer(GetConVarFloat(g_Cvar_IGARequestCooldownTime), RemoveCooldown, client);
}

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

public Action:RemoveCooldown(Handle:timer, any:client)
{
    g_IsInCooldown[client] = false;
}

public bool:IsInPall()
{
    return GetTime() < g_PallNextFree;
}
public bool:IsInP(client)
{
    return GetTime() < g_PNextFree[client];
}

//True if client can use a donator action. If donations are not enabled this
//will always be true, otherwise check if client is a donator.
public bool:DonatorCheck(client)
{
    if(!g_DonatorLibrary || !GetConVarBool(g_Cvar_IGADonatorsOnly))
        return true;
    else
        return g_IsDonator[client];
}

stock QuerySong(client, String:path[MAX_SONG_LENGTH], bool:pall = false, bool:force=false, client_theme = 0, String:map_theme[] ="")
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    new HTTPRequestHandle:request = CreateIGARequest(QUERY_SONG_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        ReplyToCommand(client, "[IGA] sm_iga_url invalid; cannot create HTTP request");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "pall", pall);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "force", force);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "player", GetClientUserId(client));
    Steam_SetHTTPRequestGetOrPostParameter(request, "path", path);

    if(client_theme > 0)
    {
        //Find the user's theme
        decl String:uid_theme[MAX_COMMUNITYID_LENGTH];
        Steam_GetCSteamIDForClient(client, uid_theme, sizeof(uid_theme));
        Steam_SetHTTPRequestGetOrPostParameter(request, "uid_theme", uid_theme);
    }else if(strlen(map_theme) > 0){
        //Find the map's theme
        Steam_SetHTTPRequestGetOrPostParameter(request, "map_theme", map_theme);
    }

    Steam_SendHTTPRequest(request, ReceiveQuerySong, GetClientUserId(client));

    StartCooldown(client);
}

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

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

    new Handle:json = json_load(data);
    new bool:found = json_object_get_bool(json, "found");

    if(found)
    {
        new duration = json_object_get_int(json, "duration");
        new bool:pall = json_object_get_bool(json, "pall");
        //new bool:force = json_object_get_bool(json, "force");
        new String:song_id[64], String:full_path[64], String:description[64], String:duration_formated[64];
        json_object_get_string(json, "song_id", song_id, sizeof(song_id));
        json_object_get_string(json, "full_path", full_path, sizeof(full_path));
        json_object_get_string(json, "description", description, sizeof(description));
        json_object_get_string(json, "duration_formated", duration_formated, sizeof(duration_formated));

        if(pall)
        {
            if(!IsInPall())
            {
                g_PallNextFree = duration + GetTime();
                PrintToChatAll("[IGA] Started Playing \"%s\" to all", description);
                PrintToChatAll("Duration %s", duration_formated);
                PrintToChatAll("Type !stop to cancel or !nopall to mute");
                PlaySongAll(song_id);
            }else{
                PrintToChat(client, "[IGA] pall currently in use");
            }
        }else if(client > 0){
            g_PNextFree[client] = duration + GetTime();
            PrintToChat(client, "[IGA] Started Playing \"%s\"", description);
            PrintToChat(client, "Duration %s", duration_formated);
            PrintToChat(client, "Type !stop to cancel");
            PlaySong(client, song_id);
        }
    }else{
        PrintToChat(client, "[IGA] Could not find specified sound or directory");
    }

    CloseHandle(json);
}

public PlaySongAll(String:song[])
{
    for (new client=1; client <= MaxClients; client++)
    {
        if (ClientHasPallEnabled(client) && !IsInP(client))
        {
            PlaySong(client, song);
        }
    }
}

public PlaySong(client, String:song_id[])
{
    decl String:url[256], String:base_url[128];
    GetConVarString(g_Cvar_IGAUrl, base_url, sizeof(base_url));

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

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

    decl String:api_key[128];
    GetConVarString(g_Cvar_IGAApiKey, api_key, sizeof(api_key));

    Format(url, sizeof(url),
            "%s%s/%s/play?access_token=%s&volume=%f", base_url, SONGS_ROUTE, song_id, api_key, (g_Volume[client] / 10.0));

    new Handle:panel = CreateKeyValues("data");
    KvSetString(panel, "title", "In Game Audio");
    KvSetNum(panel, "type", MOTDPANEL_TYPE_URL);
    KvSetString(panel, "msg", url);

    ShowVGUIPanel(client, "info", panel, false);
    CloseHandle(panel);
    return;
}

public StopSong(client)
{
    g_PNextFree[client] = 0;
    PlaySong(client, "stop");//TODO
}
public StopSongAll()
{
    g_PallNextFree = 0;
    PlaySongAll("stop");//TODO
}

public bool:ClientHasPallEnabled(client)
{
    return g_IsPallEnabled[client];
}

public SongList(client)
{
    decl String:url[256], String:base_url[128];
    GetConVarString(g_Cvar_IGAUrl, base_url, sizeof(base_url));

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

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

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

    ShowMOTDPanel(client, "Song List", url, MOTDPANEL_TYPE_URL);

}

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio
 * TODO - Add your project's description
 *
 * Copyright 2013 ???
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <clientprefs>
#include <steamtools>
#include <base64>
#include <smjansson>
#undef REQUIRE_PLUGIN
#include <donator>

#define PLUGIN_VERSION "0.1"

public Plugin:myinfo =
{
    name = "In Game Audio",
    author = "CrimsonTautology",
    description = "Interact with the In Game Audio web api",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

#define QUERY_SONG_ROUTE "/v1/api/query_song"
#define RANDOM_SONG_ROUTE ""
#define SONGS_ROUTE "/songs"
#define DIRECTORIES_ROUTE "/directories"

#define MAX_STEAMID_LENGTH 21
#define MAX_COMMUNITYID_LENGTH 18
#define MAX_SONG_LENGTH 64

new Handle:g_Cvar_IGAApiKey = INVALID_HANDLE;
new Handle:g_Cvar_IGAUrl = INVALID_HANDLE;
new Handle:g_Cvar_IGADonatorsOnly = INVALID_HANDLE;
new Handle:g_Cvar_IGAEnabled = INVALID_HANDLE;
new Handle:g_Cvar_IGARequestCooldownTime = INVALID_HANDLE;

new Handle:g_Cookie_PallEnabled = INVALID_HANDLE;
new Handle:g_Cookie_Volume = INVALID_HANDLE;

new bool:g_IsInCooldown[MAXPLAYERS+1];
new bool:g_IsPallEnabled[MAXPLAYERS+1];
new g_PNextFree[MAXPLAYERS+1];
new g_PallNextFree = 0;
new g_Volume[MAXPLAYERS+1];

new bool:g_DonatorLibrary = false;
public OnPluginStart()
{

    g_Cvar_IGAApiKey = CreateConVar("sm_iga_api_key", "", "API Key for your IGA webpage");
    g_Cvar_IGAUrl = CreateConVar("sm_iga_url", "", "URL to your IGA webpage");
    g_Cvar_IGADonatorsOnly = CreateConVar("sm_iga_donators_only", "1", "Whether or not only donators have access to pall");
    g_Cvar_IGAEnabled = CreateConVar("sm_iga_enabled", "1", "Whether or not pall is enabled");
    g_Cvar_IGARequestCooldownTime = CreateConVar("sm_iga_request_cooldown_time", "2.0", "How long in seconds before a client can send another http request");

    RegConsoleCmd("sm_p", Command_P, "Play a song for yourself");
    RegConsoleCmd("sm_pall", Command_Pall, "Play a song for everyone");
    RegConsoleCmd("sm_plist", Command_Plist, "Pop-up the song list");
    RegConsoleCmd("sm_stop", Command_Stop, "Stop the current song");
    RegAdminCmd("sm_fstop", Command_Fstop, ADMFLAG_VOTE, "[ADMIN] Stop the current pall for everyone");
    RegAdminCmd("sm_fpall", Command_Fpall, ADMFLAG_VOTE, "[ADMIN] Force everyone to listen to a song");
    RegConsoleCmd("sm_vol", Command_Vol, "Adjust your play volume");
    RegConsoleCmd("sm_nopall", Command_Nopall, "Turn off pall for yourself");
    RegConsoleCmd("sm_yespall", Command_Yespall, "Turn on pall for yourself");
    RegConsoleCmd("sm_plast", Command_Plast, "Play the last played song for yourself");

    g_Cookie_Volume = RegClientCookie("iga_volume", "Volume to play at [0-10]; 0 muted, 10 loudest", CookieAccess_Private);
    g_Cookie_PallEnabled = RegClientCookie("iga_pall_enabled", "Whether you want pall enabled or not. If yes, you will hear music when other players call !pall", CookieAccess_Private);

    g_DonatorLibrary = LibraryExists("donators");

    //HookEvent("map_change", Event_MapChange);
    //HookEvent("client_connect", Event_ClientConnect);
}

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

public OnLibraryAdded(const String:name[])
{
	if (StrEqual(name, "donators"))
	{
		g_DonatorLibrary = true;
	}
}

public OnClientConnected(client)
{
    g_IsInCooldown[client] = false;
    g_PNextFree[client] = 0;
    g_Volume[client] = 10;
    g_IsPallEnabled[client] = true;

    if (AreClientCookiesCached(client))
    {
        new String:buffer[11];

        GetClientCookie(client, g_Cookie_Volume, buffer, sizeof(buffer));
        if (strlen(buffer) > 0)
            g_Volume[client] = StringToInt(buffer);

        GetClientCookie(client, g_Cookie_PallEnabled, buffer, sizeof(buffer));
        if (strlen(buffer) > 0)
            g_IsPallEnabled[client] = bool:StringToInt(buffer);
    }
}

public Action:Command_P(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[IGA] User in cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_IGAEnabled))
    {
        ReplyToCommand(client, "[IGA] IGA not enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        decl String:path[MAX_SONG_LENGTH];
        GetCmdArgString(path, sizeof(path));
        QuerySong(client, path, false);
    }

    return Plugin_Handled;
}

public Action:Command_Pall(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[IGA] User in cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_IGAEnabled))
    {
        ReplyToCommand(client, "[IGA] IGA not enabled");
        return Plugin_Handled;
    }

    if(!DonatorCheck(client))
    {
        ReplyToCommand(client, "[IGA] Only donators can use this command");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        decl String:path[MAX_SONG_LENGTH];
        GetCmdArgString(path, sizeof(path));
        QuerySong(client, path, true);
    }

    return Plugin_Handled;
}

public Action:Command_Plist(client, args)
{
    if(client && IsClientAuthorized(client)){
        SongList(client);
    }

    return Plugin_Handled;
}

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

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

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

public Action:Command_Vol(client, args)
{
    //FIXME cleanup
    if (args != 1)
    {
        ReplyToCommand(client, "[IGA] usage \"!vol <0-10>\"");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client))
    {
        decl String:buffer[11];
        new volume;
        GetCmdArgString(buffer, sizeof(buffer));
        volume = StringToInt(buffer);
        if (volume >=0 && volume <= 10)
        {
            SetClientCookie(client, g_Cookie_Volume, buffer);
            g_Volume[client] = volume;
            ReplyToCommand(client, "[IGA] set volume to %d", volume);
        }else{
            ReplyToCommand(client, "[IGA] usage \"!vol <0-10>\"");
        }
    }

    return Plugin_Handled;
}

public Action:Command_Nopall(client, args)
{
    //FIXME cleanup
    if (client && IsClientAuthorized(client))
    {
        SetClientCookie(client, g_Cookie_PallEnabled, "0");
        g_IsPallEnabled[client] = false;
        ReplyToCommand(client, "[IGA] Disabled pall.  Type !yespall to renable it.");
    }
    return Plugin_Handled;
}

public Action:Command_Yespall(client, args)
{
    //FIXME cleanup
    if (client && IsClientAuthorized(client))
    {
        SetClientCookie(client, g_Cookie_PallEnabled, "1");
        g_IsPallEnabled[client] = true;
        ReplyToCommand(client, "[IGA] Enabled pall.  Type !nopall to disable it.");
    }
    return Plugin_Handled;
}

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

public Event_MapChange(Handle:event, const String:name[], bool:dontBroadcast)
{
    //TODO
}

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

public SetAccessCode(&HTTPRequestHandle:request)
{
    decl String:api_key[128];
    GetConVarString(g_Cvar_IGAApiKey, api_key, sizeof(api_key));
    Steam_SetHTTPRequestGetOrPostParameter(request, "access_token", api_key);
}

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

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

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

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

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

    return request;
}

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

    g_IsInCooldown[client] = true;
    CreateTimer(GetConVarFloat(g_Cvar_IGARequestCooldownTime), RemoveCooldown, client);
}

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

public Action:RemoveCooldown(Handle:timer, any:client)
{
    g_IsInCooldown[client] = false;
}

public bool:IsInPall()
{
    return GetTime() < g_PallNextFree;
}
public bool:IsInP(client)
{
    return GetTime() < g_PNextFree[client];
}

//True if client can use a donator action. If donations are not enabled this
//will always be true, otherwise check if client is a donator.
public bool:DonatorCheck(client)
{
    if(!g_DonatorLibrary || !GetConVarBool(g_Cvar_IGADonatorsOnly))
        return true;
    else
        return IsPlayerDonator(client);
}

stock QuerySong(client, String:path[MAX_SONG_LENGTH], bool:pall = false, bool:force=false, client_theme = 0, String:map_theme[] ="")
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    new HTTPRequestHandle:request = CreateIGARequest(QUERY_SONG_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        ReplyToCommand(client, "[IGA] sm_iga_url invalid; cannot create HTTP request");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "pall", pall);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "force", force);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "player", GetClientUserId(client));
    Steam_SetHTTPRequestGetOrPostParameter(request, "path", path);

    if(client_theme > 0)
    {
        //Find the user's theme
        decl String:uid_theme[MAX_COMMUNITYID_LENGTH];
        Steam_GetCSteamIDForClient(client, uid_theme, sizeof(uid_theme));
        Steam_SetHTTPRequestGetOrPostParameter(request, "uid_theme", uid_theme);
    }else if(strlen(map_theme) > 0){
        //Find the map's theme
        Steam_SetHTTPRequestGetOrPostParameter(request, "map_theme", map_theme);
    }

    Steam_SendHTTPRequest(request, ReceiveQuerySong, GetClientUserId(client));

    StartCooldown(client);
}

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

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

    new Handle:json = json_load(data);
    new bool:found = json_object_get_bool(json, "found");

    if(found)
    {
        new duration = json_object_get_int(json, "duration");
        new bool:pall = json_object_get_bool(json, "pall");
        //new bool:force = json_object_get_bool(json, "force");
        new String:song_id[64], String:full_path[64], String:description[64], String:duration_formated[64];
        json_object_get_string(json, "song_id", song_id, sizeof(song_id));
        json_object_get_string(json, "full_path", full_path, sizeof(full_path));
        json_object_get_string(json, "description", description, sizeof(description));
        json_object_get_string(json, "duration_formated", duration_formated, sizeof(duration_formated));

        if(pall)
        {
            if(!IsInPall())
            {
                g_PallNextFree = duration + GetTime();
                PrintToChatAll("[IGA] Started Playing \"%s\" to all", description);
                PrintToChatAll("Duration %s", duration_formated);
                PrintToChatAll("Type !stop to cancel or !nopall to mute");
                PlaySongAll(song_id);
            }else{
                PrintToChat(client, "[IGA] pall currently in use");
            }
        }else if(client > 0){
            g_PNextFree[client] = duration + GetTime();
            PrintToChat(client, "[IGA] Started Playing \"%s\"", description);
            PrintToChat(client, "Duration %s", duration_formated);
            PrintToChat(client, "Type !stop to cancel");
            PlaySong(client, song_id);
        }
    }else{
        PrintToChat(client, "[IGA] Could not find specified sound or directory");
    }

    CloseHandle(json);
}

public PlaySongAll(String:song[])
{
    for (new client=1; client <= MaxClients; client++)
    {
        if (ClientHasPallEnabled(client) && !IsInP(client))
        {
            PlaySong(client, song);
        }
    }
}

public PlaySong(client, String:song_id[])
{
    decl String:url[256], String:base_url[128];
    GetConVarString(g_Cvar_IGAUrl, base_url, sizeof(base_url));

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

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

    decl String:api_key[128];
    GetConVarString(g_Cvar_IGAApiKey, api_key, sizeof(api_key));

    Format(url, sizeof(url),
            "%s%s/%s/play?access_token=%s", base_url, SONGS_ROUTE, song_id, api_key);

    new Handle:panel = CreateKeyValues("data");
    KvSetString(panel, "title", "In Game Audio");
    KvSetNum(panel, "type", MOTDPANEL_TYPE_URL);
    KvSetString(panel, "msg", url);

    ShowVGUIPanel(client, "info", panel, false);
    CloseHandle(panel);
    return;
}

public StopSong(client)
{
    g_PNextFree[client] = 0;
    PlaySong(client, "stop");//TODO
}
public StopSongAll()
{
    g_PallNextFree = 0;
    PlaySongAll("stop");//TODO
}

public bool:ClientHasPallEnabled(client)
{
    return g_IsPallEnabled[client];
}

public SongList(client)
{
    decl String:url[256], String:base_url[128];
    GetConVarString(g_Cvar_IGAUrl, base_url, sizeof(base_url));

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

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

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

    ShowMOTDPanel(client, "Song List", url, MOTDPANEL_TYPE_URL);

}


Add more convars for sound and speed

Add more convars for sound and speed

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

#pragma semicolon 1

#include <sourcemod>
#include <smlib/entities>
#include <jetpack_plus>

#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];

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_test", Command_Test, "TODO: TEST");
    //RegConsoleCmd("+sm_jetpack", Command_JetpackStart, "", FCVAR_GAMEDLL); //TODO remove
    //RegConsoleCmd("-sm_jetpack", Command_JetpackStop,  "", FCVAR_GAMEDLL);

    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_Cvar_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(buttons & IN_JUMP && !IsClientUsingJetpack(client))
    {
        //TODO send player id, not client id
        CreateTimer(0.2, HeldJump, client);
    }

    return Plugin_Continue;
}

//Call back for when a player presses jump.  Checks if jump is still held down
public Action:HeldJump(Handle:timer, any:client)
{
    //TODO retrieve client id from player id
    if(!IsClientInGame(client)) return Plugin_Handled;

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

    return Plugin_Handled;
}

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

public Action:Command_JetpackStart(client, args)
{
    //TODO
    StartJetpack(client);
    return Plugin_Continue;
}

public Action:Command_JetpackStop(client, args)
{
    //TODO
    StopJetpack(client);
    return Plugin_Continue;
}

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

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

//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 <smlib/entities>
#include <jetpack_plus>

#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 g_Offset_movecollide = -1;

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

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_enabled", "1", "Enabled");

    RegConsoleCmd("sm_test", Command_Test, "TODO: TEST");
    //RegConsoleCmd("+sm_jetpack", Command_JetpackStart, "", FCVAR_GAMEDLL); //TODO remove
    //RegConsoleCmd("-sm_jetpack", Command_JetpackStop,  "", FCVAR_GAMEDLL);

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

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(buttons & IN_JUMP && !IsClientUsingJetpack(client))
    {
        //TODO send player id, not client id
        CreateTimer(0.2, HeldJump, client);
    }

    return Plugin_Continue;
}

//Call back for when a player presses jump.  Checks if jump is still held down
public Action:HeldJump(Handle:timer, any:client)
{
    //TODO retrieve client id from player id
    if(!IsClientInGame(client)) return Plugin_Handled;

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

    return Plugin_Handled;
}

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

public Action:Command_JetpackStart(client, args)
{
    //TODO
    StartJetpack(client);
    return Plugin_Continue;
}

public Action:Command_JetpackStop(client, args)
{
    //TODO
    StopJetpack(client);
    return Plugin_Continue;
}

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

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

//Called each frame a client is using a jetpack
JetpackStep(client)
{
    new buttons = GetClientButtons(client);
    if(IsPlayerAlive(client) && (buttons & IN_JUMP))
    {
        JetpackPush(client, 8.0);
    }
    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);
}


Add missing semicolon

Add missing semicolon

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio_uploader
 * Allows users to upload songs via the MOTD browser
 *
 * Copyright 2014 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <in_game_audio>
#include <steamtools>
#include <smjansson>

#define PLUGIN_VERSION "1.0"

public Plugin:myinfo =
{
    name = "In Game Audio Music Uploader",
    author = "CrimsonTautology",
    description = "Allow users to upload music to IGA via the MOTD browser",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

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

    RegConsoleCmd("sm_upload", Command_Upload, "Upload a song to IGA");

}

public OnAllPluginsLoaded()
{
    IGA_RegisterMenuItem("Upload songs", UploadSongMenu);
}

public Action:Command_Upload(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "\x04%t", "user_in_cooldown");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        GenerateLoginToken(client, ReceiveGenerateLoginToken);
    }

    return Plugin_Handled;
}

public IGAMenu:UploadSongMenu(client) GenerateLoginToken(client, ReceiveGenerateLoginToken);

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

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

        new Handle:json = json_load(data);
        new String:login_token[128];
        json_object_get_string(json, "login_token", login_token, sizeof(login_token));

        UploadPage(client, login_token);

        CloseHandle(json);
    }else{
        Steam_ReleaseHTTPRequest(request);
    }
}

UploadPage(client, String:login_token[])
{
    decl String:args[256]="";
    Format(args, sizeof(args),
            "?login_token=%s", login_token);
    CreateIGAPopup(client, NEW_SONGS_ROUTE, args);
}

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio_uploader
 * Allows users to upload songs via the MOTD browser
 *
 * Copyright 2014 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <in_game_audio>
#include <steamtools>
#include <smjansson>

#define PLUGIN_VERSION "1.0"

public Plugin:myinfo =
{
    name = "In Game Audio Music Uploader",
    author = "CrimsonTautology",
    description = "Allow users to upload music to IGA via the MOTD browser",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

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

    RegConsoleCmd("sm_upload", Command_Upload, "Upload a song to IGA");

}

public OnAllPluginsLoaded()
{
    IGA_RegisterMenuItem("Upload songs", UploadSongMenu);
}

public Action:Command_Upload(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "\x04%t", "user_in_cooldown");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        GenerateLoginToken(client, ReceiveGenerateLoginToken);
    }

    return Plugin_Handled;
}

public IGAMenu:UploadSongMenu(client) GenerateLoginToken(client, ReceiveGenerateLoginToken);

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

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

        new Handle:json = json_load(data);
        new String:login_token[128];
        json_object_get_string(json, "login_token", login_token, sizeof(login_token));

        UploadPage(client, login_token)

        CloseHandle(json);
    }else{
        Steam_ReleaseHTTPRequest(request);
    }
}

UploadPage(client, String:login_token[])
{
    decl String:args[256]="";
    Format(args, sizeof(args),
            "?login_token=%s", login_token);
    CreateIGAPopup(client, NEW_SONGS_ROUTE, args);
}


Prioritize mapname, else loop

Prioritize mapname, else loop

/**
* TheXeon
* ngs_spycrab.sp
*
* Files:
* addons/sourcemod/plugins/ngs_spycrab.smx
* addons/sourcemod/configs/crablocations.cfg
*
* Dependencies:
* sdkhooks.inc, adminmenu.inc, multicolors.inc, tf2_stocks.inc,
* ngsutils.inc, ngsupdater.inc
*/
#include <sdkhooks>
#include <adminmenu>
#include <multicolors>
#include <tf2_stocks>
#include <ngsutils>
#include <ngsupdater>

//// Obviously inspired by Erreur 500
//enum CrabData
//{
//	Challenger,
//	bool:Enabled,
//	Score,
//	SMTimer:MovementTimer,
//	TimesTaunted,
//	Deads,
//	ClassRestrict,
//	GodMod,
//	bool:HeadShot,
//	TimeLeft,
//	CSprite,
//	SpriteParent,
//	bool:HideSprite,
//}

ConVar mapNameContains;
KeyValues mapLocations;

bool spycrabInProgress, spycrabMenuInUse;
//int playerCrabData[MAXPLAYERS + 1][CrabData];
int firstClient = -1, secondClient = -1, firstClientScore, secondClientScore,
	firstClientTimesTaunted, secondClientTimesTaunted, hudTextChannel;
float firstClientOrigin[3], secondClientOrigin[3];
TFTeam firstClientTeam, secondClientTeam;

SMTimer firstClientMovement, secondClientMovement;

public Plugin myinfo = {
	name = "[NGS] Spycrab Suite",
	author = "TheXeon / EasyE",
	description = "Automate a spycrab with people.",
	version = "1.1.0",
	url = "https://www.neogenesisnetwork.net"
}

public void OnPluginStart()
{
	RegConsoleCmd("sm_spycrab", CommandCrab, "Displays spycrab target menu.");
	RegConsoleCmd("sm_crab", CommandCrab, "Displays spycrab target menu.");
	RegAdminCmd("sm_cancelcrab", CommandCancelCrab, ADMFLAG_GENERIC, "Cancels the spycrab.");
	RegAdminCmd("sm_reloadcrab", CommandReloadCrabConfig, ADMFLAG_GENERIC, "Reloads the map config file.");

	mapNameContains = CreateConVar("sm_spycrab_config_contains", "1", "Whether map names in config will be checked partially or fully.");
	HookEvent("player_death", OnPlayerDeath);
	HookEvent("player_disconnect", OnPlayerDisconnect, EventHookMode_Pre);

	LoadConfig();
}

// Thank you Dr.Mckay and your CCC
public void LoadConfig()
{
	char configFile[PLATFORM_MAX_PATH];
	BuildPath(Path_SM, configFile, sizeof(configFile), "configs/crablocations.cfg");
	if (!FileExists(configFile))
	{
		SetFailState("Missing config file (should be at %s)! Please get it from the repo!", configFile);
	}
	delete mapLocations;
	mapLocations = new KeyValues("Locations");
	if (!mapLocations.ImportFromFile(configFile))
	{
		SetFailState("Invalid config file at %s! Please fix it!", configFile);
	}
}

public void OnMapStart()
{
	char mapName[MAX_BUFFER_LENGTH];
	GetCurrentMap(mapName, sizeof(mapName));
	FindMapCoords(mapName);
}

void FindMapCoords(char[] mapName)
{
	firstClientOrigin = NULL_VECTOR;
	secondClientOrigin = NULL_VECTOR;
	char buffer[MAX_BUFFER_LENGTH];
	mapLocations.Rewind();
	if (!mapLocations.JumpToKey(mapName))
	{
		if (mapNameContains.BoolValue)
		{
			mapLocations.Rewind();
			mapLocations.GotoFirstSubKey();
			do
			{
				mapLocations.GetSectionName(buffer, sizeof(buffer));
				if (StrContains(mapName, buffer, false) != -1)
				{
					SetMapCoords(buffer);
					break;
				}
			}
			while (mapLocations.GotoNextKey());
		}
		else
		{
			LogError("Map %s is not in config file!", mapName);
		}
	}
	else
	{
		SetMapCoords(mapName);
	}
}

void SetMapCoords(char[] sectionName)
{
	char firstClientBuffer[MAX_BUFFER_LENGTH], secondClientBuffer[MAX_BUFFER_LENGTH],
		firstClientVector[3][MAX_BUFFER_LENGTH], secondClientVector[3][MAX_BUFFER_LENGTH];
	mapLocations.GetString("firstClient", firstClientBuffer, sizeof(firstClientBuffer));
	mapLocations.GetString("secondClient", secondClientBuffer, sizeof(secondClientBuffer));
	if (firstClientBuffer[0] != '\0' && secondClientBuffer[0] != '\0')
	{
		ExplodeString(firstClientBuffer, ",", firstClientVector, sizeof(firstClientVector), sizeof(firstClientVector[]));
		ExplodeString(secondClientBuffer, ",", secondClientVector, sizeof(secondClientVector), sizeof(secondClientVector[]));
		for (int i = 0; i < 3; i++)
		{
			firstClientOrigin[i] = StringToFloat(firstClientVector[i]);
			secondClientOrigin[i] = StringToFloat(secondClientVector[i]);\
		}
	}
	else
	{
		LogError("Invalidly formatted location string in config file around Section \'%s\'. Make sure it is a comma separated 3d vector!", sectionName);
	}
}

public Action CommandReloadCrabConfig(int client, int args)
{
	if (mapLocations == null)
	{
		CReplyToCommand(client, "The config file does not currently exist!");
	}
	else
	{
		char mapName[MAX_BUFFER_LENGTH];
		GetCurrentMap(mapName, sizeof(mapName));
		LoadConfig();
		FindMapCoords(mapName);
		CReplyToCommand(client, "Locations have been reloaded!");
	}
	return Plugin_Handled;
}

public Action CommandCrab(int client, int args)
{
	if (!IsValidClient(client, true)) return Plugin_Handled;
	if (spycrabInProgress)
	{
		CReplyToCommand(client, "{LIGHTGREEN}[Crab]{DEFAULT} There is currently another spycrab happening.");
	}
	else if (spycrabMenuInUse)
	{
		CReplyToCommand(client, "{LIGHTGREEN}[Crab]{DEFAULT} The spycrab menu is currently being used by someone else right now.");
	}
	else if (firstClientOrigin[0] == NULL_VECTOR[0])
	{
		CReplyToCommand(client, "{LIGHTGREEN}[Crab]{DEFAULT} The map is not configured for this plugin, notify an admin!");
	}
	else
	{
		Menu spycrabMenu = new Menu(SpycrabMenuHandler);
		spycrabMenu.SetTitle("Select a player:");
		AddTargetsToMenu(spycrabMenu, 0, true, true);
		spycrabMenu.Display(client, 20);
		spycrabMenuInUse = true;
	}
	return Plugin_Handled;
}

public Action CommandCancelCrab(int client, int args)
{
	if (!spycrabInProgress)
		CReplyToCommand(client, "{LIGHTGREEN}[Crab]{DEFAULT} There isn\'t any spycrab going on right now.");
	else
		ResetSpycrabClients(true);
	return Plugin_Handled;
}

public int SpycrabMenuHandler(Menu menu, MenuAction action, int param1, int param2)
{
	if (action == MenuAction_Select)
	{
		char target[16];
		int iTarget;
		menu.GetItem(param2, target, sizeof(target));
		iTarget = GetClientOfUserId(StringToInt(target));
		if (param1 == iTarget)
		{
			CPrintToChat(param1, "{LIGHTGREEN}[Crab]{DEFAULT} You may not target yourself!");
			spycrabMenuInUse = false;
			return;
		}
		CPrintToChatAll("{LIGHTGREEN}[Crab]{DEFAULT} %N has challenged %N to a spycrab showdown!", param1, iTarget);
		Menu acceptMenu = new Menu(AcceptMenuHandler);
		acceptMenu.SetTitle("Do you accept?");
		acceptMenu.AddItem("yes", "Yes!");
		acceptMenu.AddItem("no", "No!");
		acceptMenu.Display(iTarget, 20);
		firstClient = param1;
	}
	if (action == MenuAction_End)
	{
		delete menu;
	}
	if (action == MenuAction_Cancel)
	{
		spycrabMenuInUse = false;
	}
}

public int AcceptMenuHandler(Menu menu, MenuAction action, int param1, int param2)
{
	if (action == MenuAction_Select)
	{
		char answer[8];
		if (menu.GetItem(param2, answer, sizeof(answer)))
		{
			if (StrEqual(answer, "yes", false))
			{
				CPrintToChatAll("{LIGHTGREEN}[Crab]{DEFAULT} %N accepted %N\'s spycrab!", param1, firstClient);
				secondClient = param1;
				LogAction(secondClient, firstClient, "%N has accepted %N\'s spycrab.", secondClient, firstClient);
				StartSpyCrab();
			}
			else
			{
				CPrintToChatAll("{LIGHTGREEN}[Crab]{DEFAULT} %N declined %N\'s spycrab!", param1, firstClient);
				ResetSpycrabClients();
			}
		}
		spycrabMenuInUse = false;
	}
	if (action == MenuAction_Cancel)
	{
		ResetSpycrabClients();
		spycrabMenuInUse = false;
	}
	if (action == MenuAction_End)
	{
		delete menu;
		spycrabMenuInUse = false;
	}
}

public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2])
{
	if ((client == firstClient || client == secondClient) && impulse >= 221 && impulse <=239 && impulse != 230)
	{
		CPrintToChat(client, "{LIGHTGREEN}[Crab]{DEFAULT} Please do not attempt to disguise!");
		return Plugin_Handled;
	}
	return Plugin_Continue;
}

public void StartSpyCrab()
{
	spycrabInProgress = true;
	// SetHudTextParams(0.1, 0.5, 22.0, 102, 51, 153, 255);
	SetHudTextParams(0.1, 0.5, 22.0, 0, 255, 0, 255);
	hudTextChannel = ShowHudText(firstClient, -1, "Starting spycrab...");
	// Have to double each command, bleh
	TF2_SetPlayerClass(firstClient, TFClass_Spy);
	TF2_SetPlayerClass(secondClient, TFClass_Spy);
	TF2_RemovePlayerDisguise(firstClient);
	TF2_RemovePlayerDisguise(secondClient);
	TF2_RegeneratePlayer(firstClient);
	TF2_RegeneratePlayer(secondClient);
	StripToPDA(firstClient);
	StripToPDA(secondClient);
	TeleportEntity(firstClient, firstClientOrigin, NULL_VECTOR, NULL_VECTOR);
	TeleportEntity(secondClient, secondClientOrigin, NULL_VECTOR, NULL_VECTOR);
	firstClientMovement = new SMTimer(0.5, testFirstClientMov, _, TIMER_REPEAT);
	secondClientMovement = new SMTimer(0.5, testSecondClientMov, _, TIMER_REPEAT);
	firstClientTeam = TF2_GetClientTeam(firstClient);
	secondClientTeam = TF2_GetClientTeam(secondClient);
	if (CommandExists("sm_tauntspeed"))
	{
		ServerCommand("sm_tauntspeed #%d 1.3", GetClientUserId(firstClient));
		ServerCommand("sm_tauntspeed #%d 1.3", GetClientUserId(secondClient));
	}
}

public void StripToPDA(int client)
{
	if (CommandExists("sm_spycrabpda"))
	{
		TF2_RemoveAllWeapons(client);
		ServerCommand("sm_spycrabpda #%d", GetClientUserId(client));
	}
	else
	{
		// Taken from Shaders Allen's TF2_StripToMelee

		TF2_RemoveWeaponSlot(client, TFWeaponSlot_Building);
		TF2_RemoveWeaponSlot(client, TFWeaponSlot_Primary);
		TF2_RemoveWeaponSlot(client, TFWeaponSlot_Secondary);
		TF2_RemoveWeaponSlot(client, TFWeaponSlot_Melee);
		TF2_RemoveWeaponSlot(client, TFWeaponSlot_Item1);
		TF2_RemoveWeaponSlot(client, TFWeaponSlot_Item2);
		TF2_RemoveWeaponSlot(client, TFWeaponSlot_PDA);

		int disguisekit = GetPlayerWeaponSlot(client, TFWeaponSlot_Grenade);

		if (IsValidEntity(disguisekit))
		{
			EquipPlayerWeapon(client, disguisekit);
		}
	}
}

void ResetSpycrabClients(bool endOfCrab = false)
{
	for (int i = 1; i <= MaxClients; i++)
	{
		if (IsValidClient(i))
		{
			ShowHudText(i, hudTextChannel, "");
		}
	}
	if (endOfCrab)
	{
		if (IsValidClient(firstClient))
		{
			TF2_RespawnPlayer(firstClient);
			if (CommandExists("sm_tauntspeed"))
			{
				ServerCommand("sm_tauntspeed #%d 1", GetClientUserId(firstClient));
			}
		}
		if (IsValidClient(secondClient))
		{
			TF2_RespawnPlayer(secondClient);
			if (CommandExists("sm_tauntspeed"))
			{
				ServerCommand("sm_tauntspeed #%d 1", GetClientUserId(secondClient));
			}
		}
	}
	firstClient = -1;
	secondClient = -1;
	delete firstClientMovement;
	delete secondClientMovement;
	firstClientScore = 0;
	secondClientScore = 0;
	firstClientTimesTaunted = 0;
	secondClientTimesTaunted = 0;
	spycrabInProgress = false;
}

public Action testFirstClientMov(Handle timer)
{
	float location[3], noMovement[3] = {0.0, 0.0, 0.0};
	GetClientAbsOrigin(firstClient, location);
	if (GetVectorDistance(location, firstClientOrigin) > 100.0)
	{
		TeleportEntity(firstClient, firstClientOrigin, NULL_VECTOR, noMovement);
		CPrintToChat(firstClient, "{LIGHTGREEN}[Crab]{DEFAULT} Please do not move!");
	}
}

public Action testSecondClientMov(Handle timer)
{
	float location[3], noMovement[3] = {0.0, 0.0, 0.0};
	GetClientAbsOrigin(secondClient, location);
	if (GetVectorDistance(location, secondClientOrigin) > 100.0)
	{
		TeleportEntity(secondClient, secondClientOrigin, NULL_VECTOR, noMovement);
		CPrintToChat(secondClient, "{LIGHTGREEN}[Crab]{DEFAULT} Please do not move!");
	}
}

public void OnEntityCreated(int entity, const char[] classname)
{
    if (!StrEqual(classname, "instanced_scripted_scene", false)) return;
    SDKHook(entity, SDKHook_Spawn, OnSceneSpawned);
}

public Action OnSceneSpawned(int entity)
{
	if (!spycrabInProgress) return;
	int client = GetEntPropEnt(entity, Prop_Data, "m_hOwner");
	if (client != firstClient && client != secondClient) return;
	// SetHudTextParams(0.1, 0.5, 22.0, 102, 51, 153, 255);
	SetHudTextParams(0.1, 0.5, 22.0, 0, 255, 0, 255);
	char scenefile[128];
	GetEntPropString(entity, Prop_Data, "m_iszSceneFile", scenefile, sizeof(scenefile));
	if (StrContains(scenefile, "scenes/player/spy/low/taunt04", false) == -1 && StrContains(scenefile, "scenes/player/spy/low/taunt05", false) == -1 && StrContains(scenefile, "scenes/player/spy/low/taunt06", false) == -1) return;
	if (client == firstClient) firstClientTimesTaunted++;
	else if (client == secondClient) secondClientTimesTaunted++;
	int absoluteTimesTaunted = abs(secondClientTimesTaunted - firstClientTimesTaunted);
	if (absoluteTimesTaunted > 1)
	{
		if (secondClientTimesTaunted > firstClientTimesTaunted)
		{
			CPrintToChat(firstClient, "{LIGHTGREEN}[Crab]{DEFAULT} You must taunt %d more times for any more points to be counted.", absoluteTimesTaunted);
			CPrintToChat(secondClient, "{LIGHTGREEN}[Crab]{DEFAULT} %N must taunt %d more times for any more points to be counted.", firstClient, absoluteTimesTaunted);
			secondClientTimesTaunted--;
			return;
		}
		else
		{
			CPrintToChat(secondClient, "{LIGHTGREEN}[Crab]{DEFAULT} You must taunt %d more times for any more points to be counted.", absoluteTimesTaunted);
			CPrintToChat(firstClient, "{LIGHTGREEN}[Crab]{DEFAULT} %N must taunt %d more times for any more points to be counted.", secondClient, absoluteTimesTaunted);
			firstClientTimesTaunted--;
			return;
		}
	}
	if (StrEqual(scenefile, "scenes/player/spy/low/taunt05.vcd"))
	{
		if (client == firstClient) firstClientScore++;
		else if (client == secondClient) secondClientScore++;
		CPrintToChatAll("{LIGHTGREEN}[Crab]{DEFAULT} %N just crabbed!", client);
		if (absoluteTimesTaunted == 0 && (firstClientScore > 2 || secondClientScore > 2))
		{
			CPrintToChatAll("{LIGHTGREEN}[Crab]{DEFAULT} We have a winner! Congrats to %N.", (firstClientScore > secondClientScore) ? secondClient : firstClient);
			LogMessage("%N has won the spycrab between them and %N.", (firstClientScore > secondClientScore) ? secondClient : firstClient, (firstClientScore > secondClientScore) ? firstClient : secondClient)
			SMTimer.Make(3.5, ResetCrabOnWinTimer);
			return;
		}
		for (int i = 1; i <= MaxClients; i++)
			if (IsValidClient(i)) ShowHudText(i, hudTextChannel, "%N: %d / %d\n%N: %d / %d", firstClient, firstClientScore, firstClientTimesTaunted, secondClient, secondClientScore, secondClientTimesTaunted);
	}
	else
	{
		for (int i = 1; i <= MaxClients; i++)
			if (IsValidClient(i)) ShowHudText(i, hudTextChannel, "%N: %d / %d\n%N: %d / %d", firstClient, firstClientScore, firstClientTimesTaunted, secondClient, secondClientScore, secondClientTimesTaunted);
		if (firstClientScore > 2 || secondClientScore > 2)
		{
			CPrintToChatAll("{LIGHTGREEN}[Crab]{DEFAULT} We have a winner! Congrats to %N.", (firstClientScore > secondClientScore) ? secondClient : firstClient);
			LogMessage("%N has won the spycrab between them and %N.", (firstClientScore > secondClientScore) ? secondClient : firstClient, (firstClientScore > secondClientScore) ? firstClient : secondClient)
			SMTimer.Make(3.5, ResetCrabOnWinTimer);
		}
	}
}

public Action ResetCrabOnWinTimer(Handle timer)
{
	ResetSpycrabClients(true);
}

///////////////////////////////////
///////////// CHECKS //////////////
///////////////////////////////////

public Action OnPlayerDisconnect(Event event, const char[] name, bool dontBroadcast)
{
	int userid = event.GetInt("userid");
	int client = GetClientOfUserId(userid);
	if (client == firstClient || client == secondClient)
	{
		if (spycrabMenuInUse) spycrabMenuInUse = false;
		if (spycrabInProgress)
		{
			LogMessage("%L may have just run from a spycrab!", IsValidClient(firstClient) ? secondClient : firstClient);
			CPrintToChatAll("{LIGHTGREEN}[Crab]{DEFAULT} %L just ran from a spycrab!", IsValidClient(firstClient) ? secondClient : firstClient);
			ResetSpycrabClients(true);
		}
	}
	return Plugin_Continue;
}

public void OnPlayerDeath(Event event, const char[] name, bool dontBroadcast)
{
	if (!spycrabInProgress) return;
	int userid = event.GetInt("userid");
	int client = GetClientOfUserId(userid);
	if (client == firstClient || client == secondClient)
	{
		SMTimer.Make(0.2, OnPlayerDeathTimer, userid);
	}
}

public Action OnPlayerDeathTimer(Handle timer, any userid)
{
	float noMovement[3] = {0.0, 0.0, 0.0};
	int client = GetClientOfUserId(userid);
	if (!IsValidClient(client))
	{
		ResetSpycrabClients(false);
		return;
	}
	if (client == firstClient && TF2_GetClientTeam(client) != firstClientTeam)
	{
		TF2_ChangeClientTeam(client, firstClientTeam);
	}
	else if (client == secondClient && TF2_GetClientTeam(client) != secondClientTeam)
	{
		TF2_ChangeClientTeam(client, secondClientTeam);
	}
	TF2_SetPlayerClass(client, TFClass_Spy, false);
	TF2_RespawnPlayer(client);
	StripToPDA(client);
	if (client == firstClient) TeleportEntity(firstClient, firstClientOrigin, NULL_VECTOR, noMovement);
	else TeleportEntity(secondClient, secondClientOrigin, NULL_VECTOR, noMovement);
}

stock int abs(int x) { return RoundToNearest(FloatAbs(float(x))); }

/**
* TheXeon
* ngs_spycrab.sp
*
* Files:
* addons/sourcemod/plugins/ngs_spycrab.smx
* addons/sourcemod/configs/crablocations.cfg
*
* Dependencies:
* sdkhooks.inc, adminmenu.inc, multicolors.inc, tf2_stocks.inc,
* ngsutils.inc, ngsupdater.inc
*/
#include <sdkhooks>
#include <adminmenu>
#include <multicolors>
#include <tf2_stocks>
#include <ngsutils>
#include <ngsupdater>

//// Obviously inspired by Erreur 500
//enum CrabData
//{
//	Challenger,
//	bool:Enabled,
//	Score,
//	SMTimer:MovementTimer,
//	TimesTaunted,
//	Deads,
//	ClassRestrict,
//	GodMod,
//	bool:HeadShot,
//	TimeLeft,
//	CSprite,
//	SpriteParent,
//	bool:HideSprite,
//}

ConVar mapNameContains;
KeyValues mapLocations;

bool spycrabInProgress, spycrabMenuInUse;
//int playerCrabData[MAXPLAYERS + 1][CrabData];
int firstClient = -1, secondClient = -1, firstClientScore, secondClientScore,
	firstClientTimesTaunted, secondClientTimesTaunted, hudTextChannel;
float firstClientOrigin[3], secondClientOrigin[3];
TFTeam firstClientTeam, secondClientTeam;

SMTimer firstClientMovement, secondClientMovement;

public Plugin myinfo = {
	name = "[NGS] Spycrab Suite",
	author = "TheXeon / EasyE",
	description = "Automate a spycrab with people.",
	version = "1.1.0",
	url = "https://www.neogenesisnetwork.net"
}

public void OnPluginStart()
{
	RegConsoleCmd("sm_spycrab", CommandCrab, "Displays spycrab target menu.");
	RegConsoleCmd("sm_crab", CommandCrab, "Displays spycrab target menu.");
	RegAdminCmd("sm_cancelcrab", CommandCancelCrab, ADMFLAG_GENERIC, "Cancels the spycrab.");
	RegAdminCmd("sm_reloadcrab", CommandReloadCrabConfig, ADMFLAG_GENERIC, "Reloads the map config file.");

	mapNameContains = CreateConVar("sm_spycrab_config_contains", "1", "Whether map names in config will be checked partially or fully.");
	HookEvent("player_death", OnPlayerDeath);
	HookEvent("player_disconnect", OnPlayerDisconnect, EventHookMode_Pre);

	LoadConfig();
}

// Thank you Dr.Mckay and your CCC
public void LoadConfig()
{
	char configFile[PLATFORM_MAX_PATH];
	BuildPath(Path_SM, configFile, sizeof(configFile), "configs/crablocations.cfg");
	if (!FileExists(configFile))
	{
		SetFailState("Missing config file (should be at %s)! Please get it from the repo!", configFile);
	}
	delete mapLocations;
	mapLocations = new KeyValues("Locations");
	if (!mapLocations.ImportFromFile(configFile))
	{
		SetFailState("Invalid config file at %s! Please fix it!", configFile);
	}
}

public void OnMapStart()
{
	char mapName[MAX_BUFFER_LENGTH];
	GetCurrentMap(mapName, sizeof(mapName));
	FindMapCoords(mapName);
}

void FindMapCoords(char[] mapName)
{
	char buffer[MAX_BUFFER_LENGTH];
	mapLocations.Rewind();
	if (mapNameContains.BoolValue)
	{
		mapLocations.GotoFirstSubKey();
		do
		{
			mapLocations.GetSectionName(buffer, sizeof(buffer));
			if (StrContains(mapName, buffer, false) != -1)
			{
				SetMapCoords(buffer);
				break;
			}
		}
		while (mapLocations.GotoNextKey());
	}
	else
	{
		if (!mapLocations.JumpToKey(mapName))
		{
			LogError("Map %s is not in config file!", mapName);
		}
		else
		{
			SetMapCoords(mapName);
		}
	}
}

void SetMapCoords(char[] sectionName)
{
	firstClientOrigin = NULL_VECTOR;
	secondClientOrigin = NULL_VECTOR;
	char firstClientBuffer[MAX_BUFFER_LENGTH], secondClientBuffer[MAX_BUFFER_LENGTH],
		firstClientVector[3][MAX_BUFFER_LENGTH], secondClientVector[3][MAX_BUFFER_LENGTH];
	mapLocations.GetString("firstClient", firstClientBuffer, sizeof(firstClientBuffer));
	mapLocations.GetString("secondClient", secondClientBuffer, sizeof(secondClientBuffer));
	if (firstClientBuffer[0] != '\0' && secondClientBuffer[0] != '\0')
	{
		ExplodeString(firstClientBuffer, ",", firstClientVector, sizeof(firstClientVector), sizeof(firstClientVector[]));
		ExplodeString(secondClientBuffer, ",", secondClientVector, sizeof(secondClientVector), sizeof(secondClientVector[]));
		for (int i = 0; i < 3; i++)
		{
			firstClientOrigin[i] = StringToFloat(firstClientVector[i]);
			secondClientOrigin[i] = StringToFloat(secondClientVector[i]);\
		}
	}
	else
	{
		LogError("Invalidly formatted location string in config file around Section \'%s\'. Make sure it is a comma separated 3d vector!", sectionName);
	}
}

public Action CommandReloadCrabConfig(int client, int args)
{
	if (mapLocations == null)
	{
		CReplyToCommand(client, "The config file does not currently exist!");
	}
	else
	{
		char mapName[MAX_BUFFER_LENGTH];
		GetCurrentMap(mapName, sizeof(mapName));
		LoadConfig();
		FindMapCoords(mapName);
		CReplyToCommand(client, "Locations have been reloaded!");
	}
	return Plugin_Handled;
}

public Action CommandCrab(int client, int args)
{
	if (!IsValidClient(client, true)) return Plugin_Handled;
	if (spycrabInProgress)
	{
		CReplyToCommand(client, "{LIGHTGREEN}[Crab]{DEFAULT} There is currently another spycrab happening.");
	}
	else if (spycrabMenuInUse)
	{
		CReplyToCommand(client, "{LIGHTGREEN}[Crab]{DEFAULT} The spycrab menu is currently being used by someone else right now.");
	}
	else if (firstClientOrigin[0] == NULL_VECTOR[0])
	{
		CReplyToCommand(client, "{LIGHTGREEN}[Crab]{DEFAULT} The map is not configured for this plugin, notify an admin!");
	}
	else
	{
		Menu spycrabMenu = new Menu(SpycrabMenuHandler);
		spycrabMenu.SetTitle("Select a player:");
		AddTargetsToMenu(spycrabMenu, 0, true, true);
		spycrabMenu.Display(client, 20);
		spycrabMenuInUse = true;
	}
	return Plugin_Handled;
}

public Action CommandCancelCrab(int client, int args)
{
	if (!spycrabInProgress)
		CReplyToCommand(client, "{LIGHTGREEN}[Crab]{DEFAULT} There isn\'t any spycrab going on right now.");
	else
		ResetSpycrabClients(true);
	return Plugin_Handled;
}

public int SpycrabMenuHandler(Menu menu, MenuAction action, int param1, int param2)
{
	if (action == MenuAction_Select)
	{
		char target[16];
		int iTarget;
		menu.GetItem(param2, target, sizeof(target));
		iTarget = GetClientOfUserId(StringToInt(target));
		if (param1 == iTarget)
		{
			CPrintToChat(param1, "{LIGHTGREEN}[Crab]{DEFAULT} You may not target yourself!");
			spycrabMenuInUse = false;
			return;
		}
		CPrintToChatAll("{LIGHTGREEN}[Crab]{DEFAULT} %N has challenged %N to a spycrab showdown!", param1, iTarget);
		Menu acceptMenu = new Menu(AcceptMenuHandler);
		acceptMenu.SetTitle("Do you accept?");
		acceptMenu.AddItem("yes", "Yes!");
		acceptMenu.AddItem("no", "No!");
		acceptMenu.Display(iTarget, 20);
		firstClient = param1;
	}
	if (action == MenuAction_End)
	{
		delete menu;
	}
	if (action == MenuAction_Cancel)
	{
		spycrabMenuInUse = false;
	}
}

public int AcceptMenuHandler(Menu menu, MenuAction action, int param1, int param2)
{
	if (action == MenuAction_Select)
	{
		char answer[8];
		if (menu.GetItem(param2, answer, sizeof(answer)))
		{
			if (StrEqual(answer, "yes", false))
			{
				CPrintToChatAll("{LIGHTGREEN}[Crab]{DEFAULT} %N accepted %N\'s spycrab!", param1, firstClient);
				secondClient = param1;
				LogAction(secondClient, firstClient, "%N has accepted %N\'s spycrab.", secondClient, firstClient);
				StartSpyCrab();
			}
			else
			{
				CPrintToChatAll("{LIGHTGREEN}[Crab]{DEFAULT} %N declined %N\'s spycrab!", param1, firstClient);
				ResetSpycrabClients();
			}
		}
		spycrabMenuInUse = false;
	}
	if (action == MenuAction_Cancel)
	{
		ResetSpycrabClients();
		spycrabMenuInUse = false;
	}
	if (action == MenuAction_End)
	{
		delete menu;
		spycrabMenuInUse = false;
	}
}

public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2])
{
	if ((client == firstClient || client == secondClient) && impulse >= 221 && impulse <=239 && impulse != 230)
	{
		CPrintToChat(client, "{LIGHTGREEN}[Crab]{DEFAULT} Please do not attempt to disguise!");
		return Plugin_Handled;
	}
	return Plugin_Continue;
}

public void StartSpyCrab()
{
	spycrabInProgress = true;
	// SetHudTextParams(0.1, 0.5, 22.0, 102, 51, 153, 255);
	SetHudTextParams(0.1, 0.5, 22.0, 0, 255, 0, 255);
	hudTextChannel = ShowHudText(firstClient, -1, "Starting spycrab...");
	// Have to double each command, bleh
	TF2_SetPlayerClass(firstClient, TFClass_Spy);
	TF2_SetPlayerClass(secondClient, TFClass_Spy);
	TF2_RemovePlayerDisguise(firstClient);
	TF2_RemovePlayerDisguise(secondClient);
	TF2_RegeneratePlayer(firstClient);
	TF2_RegeneratePlayer(secondClient);
	StripToPDA(firstClient);
	StripToPDA(secondClient);
	TeleportEntity(firstClient, firstClientOrigin, NULL_VECTOR, NULL_VECTOR);
	TeleportEntity(secondClient, secondClientOrigin, NULL_VECTOR, NULL_VECTOR);
	firstClientMovement = new SMTimer(0.5, testFirstClientMov, _, TIMER_REPEAT);
	secondClientMovement = new SMTimer(0.5, testSecondClientMov, _, TIMER_REPEAT);
	firstClientTeam = TF2_GetClientTeam(firstClient);
	secondClientTeam = TF2_GetClientTeam(secondClient);
	if (CommandExists("sm_tauntspeed"))
	{
		ServerCommand("sm_tauntspeed #%d 1.3", GetClientUserId(firstClient));
		ServerCommand("sm_tauntspeed #%d 1.3", GetClientUserId(secondClient));
	}
}

public void StripToPDA(int client)
{
	if (CommandExists("sm_spycrabpda"))
	{
		TF2_RemoveAllWeapons(client);
		ServerCommand("sm_spycrabpda #%d", GetClientUserId(client));
	}
	else
	{
		// Taken from Shaders Allen's TF2_StripToMelee

		TF2_RemoveWeaponSlot(client, TFWeaponSlot_Building);
		TF2_RemoveWeaponSlot(client, TFWeaponSlot_Primary);
		TF2_RemoveWeaponSlot(client, TFWeaponSlot_Secondary);
		TF2_RemoveWeaponSlot(client, TFWeaponSlot_Melee);
		TF2_RemoveWeaponSlot(client, TFWeaponSlot_Item1);
		TF2_RemoveWeaponSlot(client, TFWeaponSlot_Item2);
		TF2_RemoveWeaponSlot(client, TFWeaponSlot_PDA);

		int disguisekit = GetPlayerWeaponSlot(client, TFWeaponSlot_Grenade);

		if (IsValidEntity(disguisekit))
		{
			EquipPlayerWeapon(client, disguisekit);
		}
	}
}

void ResetSpycrabClients(bool endOfCrab = false)
{
	for (int i = 1; i <= MaxClients; i++)
	{
		if (IsValidClient(i))
		{
			ShowHudText(i, hudTextChannel, "");
		}
	}
	if (endOfCrab)
	{
		if (IsValidClient(firstClient))
		{
			TF2_RespawnPlayer(firstClient);
			if (CommandExists("sm_tauntspeed"))
			{
				ServerCommand("sm_tauntspeed #%d 1", GetClientUserId(firstClient));
			}
		}
		if (IsValidClient(secondClient))
		{
			TF2_RespawnPlayer(secondClient);
			if (CommandExists("sm_tauntspeed"))
			{
				ServerCommand("sm_tauntspeed #%d 1", GetClientUserId(secondClient));
			}
		}
	}
	firstClient = -1;
	secondClient = -1;
	delete firstClientMovement;
	delete secondClientMovement;
	firstClientScore = 0;
	secondClientScore = 0;
	firstClientTimesTaunted = 0;
	secondClientTimesTaunted = 0;
	spycrabInProgress = false;
}

public Action testFirstClientMov(Handle timer)
{
	float location[3], noMovement[3] = {0.0, 0.0, 0.0};
	GetClientAbsOrigin(firstClient, location);
	if (GetVectorDistance(location, firstClientOrigin) > 100.0)
	{
		TeleportEntity(firstClient, firstClientOrigin, NULL_VECTOR, noMovement);
		CPrintToChat(firstClient, "{LIGHTGREEN}[Crab]{DEFAULT} Please do not move!");
	}
}

public Action testSecondClientMov(Handle timer)
{
	float location[3], noMovement[3] = {0.0, 0.0, 0.0};
	GetClientAbsOrigin(secondClient, location);
	if (GetVectorDistance(location, secondClientOrigin) > 100.0)
	{
		TeleportEntity(secondClient, secondClientOrigin, NULL_VECTOR, noMovement);
		CPrintToChat(secondClient, "{LIGHTGREEN}[Crab]{DEFAULT} Please do not move!");
	}
}

public void OnEntityCreated(int entity, const char[] classname)
{
    if (!StrEqual(classname, "instanced_scripted_scene", false)) return;
    SDKHook(entity, SDKHook_Spawn, OnSceneSpawned);
}

public Action OnSceneSpawned(int entity)
{
	if (!spycrabInProgress) return;
	int client = GetEntPropEnt(entity, Prop_Data, "m_hOwner");
	if (client != firstClient && client != secondClient) return;
	// SetHudTextParams(0.1, 0.5, 22.0, 102, 51, 153, 255);
	SetHudTextParams(0.1, 0.5, 22.0, 0, 255, 0, 255);
	char scenefile[128];
	GetEntPropString(entity, Prop_Data, "m_iszSceneFile", scenefile, sizeof(scenefile));
	if (StrContains(scenefile, "scenes/player/spy/low/taunt04", false) == -1 && StrContains(scenefile, "scenes/player/spy/low/taunt05", false) == -1 && StrContains(scenefile, "scenes/player/spy/low/taunt06", false) == -1) return;
	if (client == firstClient) firstClientTimesTaunted++;
	else if (client == secondClient) secondClientTimesTaunted++;
	int absoluteTimesTaunted = abs(secondClientTimesTaunted - firstClientTimesTaunted);
	if (absoluteTimesTaunted > 1)
	{
		if (secondClientTimesTaunted > firstClientTimesTaunted)
		{
			CPrintToChat(firstClient, "{LIGHTGREEN}[Crab]{DEFAULT} You must taunt %d more times for any more points to be counted.", absoluteTimesTaunted);
			CPrintToChat(secondClient, "{LIGHTGREEN}[Crab]{DEFAULT} %N must taunt %d more times for any more points to be counted.", firstClient, absoluteTimesTaunted);
			secondClientTimesTaunted--;
			return;
		}
		else
		{
			CPrintToChat(secondClient, "{LIGHTGREEN}[Crab]{DEFAULT} You must taunt %d more times for any more points to be counted.", absoluteTimesTaunted);
			CPrintToChat(firstClient, "{LIGHTGREEN}[Crab]{DEFAULT} %N must taunt %d more times for any more points to be counted.", secondClient, absoluteTimesTaunted);
			firstClientTimesTaunted--;
			return;
		}
	}
	if (StrEqual(scenefile, "scenes/player/spy/low/taunt05.vcd"))
	{
		if (client == firstClient) firstClientScore++;
		else if (client == secondClient) secondClientScore++;
		CPrintToChatAll("{LIGHTGREEN}[Crab]{DEFAULT} %N just crabbed!", client);
		if (absoluteTimesTaunted == 0 && (firstClientScore > 2 || secondClientScore > 2))
		{
			CPrintToChatAll("{LIGHTGREEN}[Crab]{DEFAULT} We have a winner! Congrats to %N.", (firstClientScore > secondClientScore) ? secondClient : firstClient);
			LogMessage("%N has won the spycrab between them and %N.", (firstClientScore > secondClientScore) ? secondClient : firstClient, (firstClientScore > secondClientScore) ? firstClient : secondClient)
			SMTimer.Make(3.5, ResetCrabOnWinTimer);
			return;
		}
		for (int i = 1; i <= MaxClients; i++)
			if (IsValidClient(i)) ShowHudText(i, hudTextChannel, "%N: %d / %d\n%N: %d / %d", firstClient, firstClientScore, firstClientTimesTaunted, secondClient, secondClientScore, secondClientTimesTaunted);
	}
	else
	{
		for (int i = 1; i <= MaxClients; i++)
			if (IsValidClient(i)) ShowHudText(i, hudTextChannel, "%N: %d / %d\n%N: %d / %d", firstClient, firstClientScore, firstClientTimesTaunted, secondClient, secondClientScore, secondClientTimesTaunted);
		if (firstClientScore > 2 || secondClientScore > 2)
		{
			CPrintToChatAll("{LIGHTGREEN}[Crab]{DEFAULT} We have a winner! Congrats to %N.", (firstClientScore > secondClientScore) ? secondClient : firstClient);
			LogMessage("%N has won the spycrab between them and %N.", (firstClientScore > secondClientScore) ? secondClient : firstClient, (firstClientScore > secondClientScore) ? firstClient : secondClient)
			SMTimer.Make(3.5, ResetCrabOnWinTimer);
		}
	}
}

public Action ResetCrabOnWinTimer(Handle timer)
{
	ResetSpycrabClients(true);
}

///////////////////////////////////
///////////// CHECKS //////////////
///////////////////////////////////

public Action OnPlayerDisconnect(Event event, const char[] name, bool dontBroadcast)
{
	int userid = event.GetInt("userid");
	int client = GetClientOfUserId(userid);
	if (client == firstClient || client == secondClient)
	{
		if (spycrabMenuInUse) spycrabMenuInUse = false;
		if (spycrabInProgress)
		{
			LogMessage("%L may have just run from a spycrab!", IsValidClient(firstClient) ? secondClient : firstClient);
			CPrintToChatAll("{LIGHTGREEN}[Crab]{DEFAULT} %L just ran from a spycrab!", IsValidClient(firstClient) ? secondClient : firstClient);
			ResetSpycrabClients(true);
		}
	}
	return Plugin_Continue;
}

public void OnPlayerDeath(Event event, const char[] name, bool dontBroadcast)
{
	if (!spycrabInProgress) return;
	int userid = event.GetInt("userid");
	int client = GetClientOfUserId(userid);
	if (client == firstClient || client == secondClient)
	{
		SMTimer.Make(0.2, OnPlayerDeathTimer, userid);
	}
}

public Action OnPlayerDeathTimer(Handle timer, any userid)
{
	float noMovement[3] = {0.0, 0.0, 0.0};
	int client = GetClientOfUserId(userid);
	if (!IsValidClient(client))
	{
		ResetSpycrabClients(false);
		return;
	}
	if (client == firstClient && TF2_GetClientTeam(client) != firstClientTeam)
	{
		TF2_ChangeClientTeam(client, firstClientTeam);
	}
	else if (client == secondClient && TF2_GetClientTeam(client) != secondClientTeam)
	{
		TF2_ChangeClientTeam(client, secondClientTeam);
	}
	TF2_SetPlayerClass(client, TFClass_Spy, false);
	TF2_RespawnPlayer(client);
	StripToPDA(client);
	if (client == firstClient) TeleportEntity(firstClient, firstClientOrigin, NULL_VECTOR, noMovement);
	else TeleportEntity(secondClient, secondClientOrigin, NULL_VECTOR, noMovement);
}

stock int abs(int x) { return RoundToNearest(FloatAbs(float(x))); }