RegAdminCmd

RegAdminCmd

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

#pragma semicolon 1

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

#define PLUGIN_VERSION "0.1"

public Plugin:myinfo =
{
    name = "InGameAudio",
    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 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 bool:g_IsInCooldown[MAXPLAYERS+1];
new g_PallNextFree = 0;

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_plast", Command_Plast, "Play the last played song for yourself");

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

public Action:Command_P(client, args)
{
    if (args < 1)
    {
        ReplyToCommand(client, "[IGA] Usage: !p <song>");
        return Plugin_Handled;
    }
    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:song[MAX_SONG_LENGTH];
        GetCmdArgString(song, sizeof(song));
        QuerySong(client, song, false);
    }

    return Plugin_Handled;
}

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

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

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

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

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

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

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

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

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

public Event_ClientConnect(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 QuerySong(client, String:song[MAX_SONG_LENGTH], bool:pall, 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, "player", GetClientUserId(client));
    Steam_SetHTTPRequestGetOrPostParameter(request, "song", song);

    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 String:song[MAX_SONG_LENGTH], String:description[64];
        json_object_get_string(json, "song", song, sizeof(song));
        json_object_get_string(json, "description", description, sizeof(description));

        if(pall)
        {
            PrintToChatAll("Now Playing: %s", description);
            PlaySongAll(song);
        }else if(client > 0){
            PrintToChat(client, "Now Playing: %s", description);
            PlaySong(client, song);
        }
    }

    CloseHandle(json);
    Steam_ReleaseHTTPRequest(request);
}

public PlaySongAll(song[])
{
    //TODO update PALL duration
    for (new client=1; client <= MaxClients; client++)
    {
        if (ClientHasPallEnabled(client))
        {
            PlaySong(client, song);
        }
    }
}

public PlaySong(client, song[])
{
    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/%s", base_url, SONGS_ROUTE, song);

    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 bool:ClientHasPallEnabled(client)
{
    //TODO do cookie check
    return IsClientAuthorized(client);
}

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

#pragma semicolon 1

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

#define PLUGIN_VERSION "0.1"

public Plugin:myinfo =
{
    name = "InGameAudio",
    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 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 bool:g_IsInCooldown[MAXPLAYERS+1];
new g_PallNextFree = 0;

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");
    RegConsoleCmd("sm_fstop", Command_Fstop, "[ADMIN] Stop the current pall for everyone");
    RegConsoleCmd("sm_fpall", Command_Fpall, "[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_plast", Command_Plast, "Play the last played song for yourself");

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

public Action:Command_P(client, args)
{
    if (args < 1)
    {
        ReplyToCommand(client, "[IGA] Usage: !p <song>");
        return Plugin_Handled;
    }
    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:song[MAX_SONG_LENGTH];
        GetCmdArgString(song, sizeof(song));
        QuerySong(client, song, false);
    }

    return Plugin_Handled;
}

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

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

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

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

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

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

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

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

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

public Event_ClientConnect(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 QuerySong(client, String:song[MAX_SONG_LENGTH], bool:pall, 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, "player", GetClientUserId(client));
    Steam_SetHTTPRequestGetOrPostParameter(request, "song", song);

    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 String:song[MAX_SONG_LENGTH], String:description[64];
        json_object_get_string(json, "song", song, sizeof(song));
        json_object_get_string(json, "description", description, sizeof(description));

        if(pall)
        {
            PrintToChatAll("Now Playing: %s", description);
            PlaySongAll(song);
        }else if(client > 0){
            PrintToChat(client, "Now Playing: %s", description);
            PlaySong(client, song);
        }
    }

    CloseHandle(json);
    Steam_ReleaseHTTPRequest(request);
}

public PlaySongAll(song[])
{
    //TODO update PALL duration
    for (new client=1; client <= MaxClients; client++)
    {
        if (ClientHasPallEnabled(client))
        {
            PlaySong(client, song);
        }
    }
}

public PlaySong(client, song[])
{
    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/%s", base_url, SONGS_ROUTE, song);

    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 bool:ClientHasPallEnabled(client)
{
    //TODO do cookie check
    return IsClientAuthorized(client);
}


Use new play route

Use new play route


Give client a default jetpack

Give client a default jetpack

Handle case where a client who did not have a cached jetpack cookie
would recieve whatever the previous client index had as their jetpack

/**
 * vim: set ts=4 :
 * =============================================================================
 * Jetpack Plus
 * Handles the sounds and particle effects for the jetpacks.  Also allows
 * donators to customize their jetpack.
 *
 * Copyright 2014 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <jetpack_plus>
#include <clientprefs>
#include <sdktools>
#include <particle>
#undef REQUIRE_PLUGIN
#include <donator>

#define PLUGIN_VERSION "1.0"

#define MAX_JETPACK_TYPES 64
#define DEFAULT_JETPACK   0

public Plugin:myinfo =
{
    name = "Jetpack Plus (Bling)",
    author = "CrimsonTautology",
    description = "Handles sounds and particle effects for jetpacks",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_jetpack_plus"
};

new Handle:g_Cvar_DonatorsOnly = INVALID_HANDLE;

new bool:g_DonatorLibraryExists = false;

//Player options
new Handle:g_Cookie_SelectedJetpack = INVALID_HANDLE;
new g_SelectedJetpack[MAXPLAYERS+1] = {DEFAULT_JETPACK, ...};
new g_JetpackParticle[MAXPLAYERS+1];

//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 OnPluginStart()
{
    LoadTranslations("jetpack_plus.phrases");

    g_Cvar_DonatorsOnly = CreateConVar("sm_jetpack_donators_only", "0", "Whether only dontaors can change their jetpack bling");

    RegConsoleCmd("sm_bling", Command_Bling, "Change jetpack bling.");

    g_Cookie_SelectedJetpack = RegClientCookie("selected_jetpack", "Selected jetpack type", CookieAccess_Private);
    g_DonatorLibraryExists = LibraryExists("donator.core");
}

public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max)
{
    MarkNativeAsOptional("IsPlayerDonator");
    MarkNativeAsOptional("Donator_RegisterMenuItem");

    return APLRes_Success;
}

public OnAllPluginsLoaded()
{
    if (g_DonatorLibraryExists)
    {
        Donator_RegisterMenuItem("Jetpack Effects", 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 OnClientCookiesCached(client)
{
    new String:buffer[11];

    GetClientCookie(client, g_Cookie_SelectedJetpack, buffer, sizeof(buffer));
    new type_of = StringToInt(buffer);
    if (strlen(buffer) > 0 && type_of < g_JetpackTypeCount){
        g_SelectedJetpack[client] = type_of;
    }else{
        g_SelectedJetpack[client] = DEFAULT_JETPACK;
    }
}

public OnMapStart()
{
    ReadJetpacks();
}

ReadJetpacks()
{
    //Create default jetpack
    g_JetpackTypeName[0] = "Default";
    g_JetpackTypeParticle[0] = "burninggibs";
    g_JetpackTypeSound[0] = "vehicles/airboat/fan_blade_fullthrottle_loop1.wav";
    PrecacheSound(g_JetpackTypeSound[0], true);
    g_JetpackTypeCount = 1;

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

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

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

        do
        {
            KvGetSectionName(kv, tmp, sizeof(tmp));
            if(StrEqual(tmp, "Jetpack") )
            {
                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 Action:Command_Bling(client, args)
{
    if(!DonatorCheck(client))
    {
        ReplyToCommand(client, "\x04%t", "donators_only");
        return Plugin_Handled;
    }

    if(client)
    {
        ChangeJetpackMenu(client);
    }

    return Plugin_Handled;
}

public Action:OnStartJetpack(client)
{
    ApplyJetpackEffects(client);
    return Plugin_Continue;
}

public Action:OnStopJetpack(client)
{
    ClearJetpackEffects(client);

    return Plugin_Continue;
}

GetSelectedJetpackOfClient(client)
{
    return g_SelectedJetpack[client];
}

SetSelectedJetpackOfClient(client, type_of)
{
    new String:tmp[11];
    IntToString(type_of, tmp, sizeof(tmp));
    SetClientCookie(client, g_Cookie_SelectedJetpack, tmp);
    g_SelectedJetpack[client] = type_of;
}

ApplyJetpackEffects(client)
{
    new selected = GetSelectedJetpackOfClient(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] = CreateParticle(g_JetpackTypeParticle[selected], 0.0, client, Attach, "flag", pos, ang);
}

ClearJetpackEffects(client)
{
    new selected = GetSelectedJetpackOfClient(client);
    StopSound(client, SNDCHAN_AUTO, g_JetpackTypeSound[selected]);
    DeleteParticle(g_JetpackParticle[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_DonatorLibraryExists || !GetConVarBool(g_Cvar_DonatorsOnly))
        return true;
    else
        return IsPlayerDonator(client);
}
//Menus
public DonatorMenu:JetpackBlingMenu(client) ChangeJetpackMenu(client);
ChangeJetpackMenu(client)
{
    new Handle:menu = CreateMenu(ChangeJetpackMenuHandler);
    new selected = GetSelectedJetpackOfClient(client);

    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],
                i == selected ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
    }

    DisplayMenu(menu, client, 20);
}

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

                //Clear running particle and sound to prevent overlap
                ClearJetpackEffects(client);

                SetSelectedJetpackOfClient(client, type_of);
            }
        case MenuAction_End: CloseHandle(menu);
    }
}

/**
 * vim: set ts=4 :
 * =============================================================================
 * Jetpack Plus
 * Handles the sounds and particle effects for the jetpacks.  Also allows
 * donators to customize their jetpack.
 *
 * Copyright 2014 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <jetpack_plus>
#include <clientprefs>
#include <sdktools>
#include <particle>
#undef REQUIRE_PLUGIN
#include <donator>

#define PLUGIN_VERSION "1.0"

#define MAX_JETPACK_TYPES 64

public Plugin:myinfo =
{
    name = "Jetpack Plus (Bling)",
    author = "CrimsonTautology",
    description = "Handles sounds and particle effects for jetpacks",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_jetpack_plus"
};

new Handle:g_Cvar_DonatorsOnly = INVALID_HANDLE;

new bool:g_DonatorLibraryExists = false;

//Player options
new Handle:g_Cookie_SelectedJetpack = INVALID_HANDLE;
new g_SelectedJetpack[MAXPLAYERS+1] = {0, ...};
new g_JetpackParticle[MAXPLAYERS+1];

//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 OnPluginStart()
{
    LoadTranslations("jetpack_plus.phrases");

    g_Cvar_DonatorsOnly = CreateConVar("sm_jetpack_donators_only", "0", "Whether only dontaors can change their jetpack bling");

    RegConsoleCmd("sm_bling", Command_Bling, "Change jetpack bling.");

    g_Cookie_SelectedJetpack = RegClientCookie("selected_jetpack", "Selected jetpack type", CookieAccess_Private);
    g_DonatorLibraryExists = LibraryExists("donator.core");
}

public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max)
{
    MarkNativeAsOptional("IsPlayerDonator");
    MarkNativeAsOptional("Donator_RegisterMenuItem");

    return APLRes_Success;
}

public OnAllPluginsLoaded()
{
    if (g_DonatorLibraryExists)
    {
        Donator_RegisterMenuItem("Jetpack Effects", 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 OnClientCookiesCached(client)
{
    new String:buffer[11];

    GetClientCookie(client, g_Cookie_SelectedJetpack, buffer, sizeof(buffer));
    new type_of = StringToInt(buffer);
    if (strlen(buffer) > 0 && type_of < g_JetpackTypeCount){
        g_SelectedJetpack[client] = type_of;
    }
}

public OnMapStart()
{
    ReadJetpacks();
}

ReadJetpacks()
{
    //Create default jetpack
    g_JetpackTypeName[0] = "Default";
    g_JetpackTypeParticle[0] = "burninggibs";
    g_JetpackTypeSound[0] = "vehicles/airboat/fan_blade_fullthrottle_loop1.wav";
    PrecacheSound(g_JetpackTypeSound[0], true);
    g_JetpackTypeCount = 1;

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

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

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

        do
        {
            KvGetSectionName(kv, tmp, sizeof(tmp));
            if(StrEqual(tmp, "Jetpack") )
            {
                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 Action:Command_Bling(client, args)
{
    if(!DonatorCheck(client))
    {
        ReplyToCommand(client, "\x04%t", "donators_only");
        return Plugin_Handled;
    }

    if(client)
    {
        ChangeJetpackMenu(client);
    }

    return Plugin_Handled;
}

public Action:OnStartJetpack(client)
{
    ApplyJetpackEffects(client);
    return Plugin_Continue;
}

public Action:OnStopJetpack(client)
{
    ClearJetpackEffects(client);

    return Plugin_Continue;
}

GetSelectedJetpackOfClient(client)
{
    return g_SelectedJetpack[client];
}

SetSelectedJetpackOfClient(client, type_of)
{
    new String:tmp[11];
    IntToString(type_of, tmp, sizeof(tmp));
    SetClientCookie(client, g_Cookie_SelectedJetpack, tmp);
    g_SelectedJetpack[client] = type_of;
}

ApplyJetpackEffects(client)
{
    new selected = GetSelectedJetpackOfClient(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] = CreateParticle(g_JetpackTypeParticle[selected], 0.0, client, Attach, "flag", pos, ang);
}

ClearJetpackEffects(client)
{
    new selected = GetSelectedJetpackOfClient(client);
    StopSound(client, SNDCHAN_AUTO, g_JetpackTypeSound[selected]);
    DeleteParticle(g_JetpackParticle[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_DonatorLibraryExists || !GetConVarBool(g_Cvar_DonatorsOnly))
        return true;
    else
        return IsPlayerDonator(client);
}
//Menus
public DonatorMenu:JetpackBlingMenu(client) ChangeJetpackMenu(client);
ChangeJetpackMenu(client)
{
    new Handle:menu = CreateMenu(ChangeJetpackMenuHandler);
    new selected = GetSelectedJetpackOfClient(client);

    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],
                i == selected ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
    }

    DisplayMenu(menu, client, 20);
}

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

                //Clear running particle and sound to prevent overlap
                ClearJetpackEffects(client);

                SetSelectedJetpackOfClient(client, type_of);
            }
        case MenuAction_End: CloseHandle(menu);
    }
}


Fix errors during spit helper build

Fix errors during spit helper build

class Docker { }
class DockerImg { }

constant File $:docker-socket = '/var/run/docker.sock';
constant File $:docker-install-cli-path = '/usr/local/bin/docker';

constant Cmd $:docker is logged-as("\c[WHALE]") = on {
    Spit-Helper { 'docker' }
    Linux {
        Cmd<docker> or (
            if $:docker-socket {
                debug "$_ exists so only installing docker cli";
                Docker.install-cli;
            } else {
                debug "$_ doesn't exist so installing the full Docker package";
                Docker.install;
                'docker';
            }
        )
    }
}

constant $:moby-github = GitHubRepo<moby/moby>;

constant File $?docker-cleanup = ${mktemp};
constant File $?docker-img-cleanup = ${mktemp};

augment Docker {

    static method latest-build-ver~ {
        my $gh-release-url = $:moby-github.latest-release-url;
        die "unable to get latest docker version" unless $gh-release-url;
        $gh-release-url.${sed 's/.*\/v//'};
    }

    static method latest-build-tgz-url-->HTTP {
        my $latest-v = Docker.latest-build-ver;
        "https://download.docker.com/linux/static/stable/x86_64/docker-{$latest-v}.tgz";
    }
    static method install-cli-->File {
        my $url = Docker.latest-build-tgz-url;
        info "Getting latest docker build from $url";
        my $docker-dir = $url.get.extract.cleanup;
        info "Installing to $:docker-install-cli-path";
        $docker-dir.add('docker').move-to($:docker-install-cli-path);
        $:docker-install-cli-path;
    }

    static method install {
        info 'running https://get.docker.com script';
        ${
            sh -c (HTTP<https://get.docker.com>.get)
            >debug('docker-install.sh')
            !>debug('docker-install.sh')
        };
    }

    static method hello-world? {
        ${ $:docker run -i --rm hello-world *>debug('hello-world') };
    }

    method cleanup -->Docker {
        $?docker-cleanup.push($self);
        END {
            $?docker-cleanup.slurp-->List[Docker].remove;
            $?docker-cleanup.remove;
        }
    }

    static method create(
        $from,
        :$name,
        :@args,
        Bool :$mount-socket
    ) -->Docker {
        my $id = ${
            $:docker create -i !>error
            ("--name=$_" if $name)
            ("-v=$:docker-socket:$:docker-socket" if $mount-socket)
            --entrypoint ''
            @args $from sh -c ':;sh'
        }.substr(0,12); # 12 is a sufficient length of hash
        $name || $id;
    }

    method exists? ${ $:docker container inspect $self *>X }
    method Bool    { $self.exists }

    method copy($from, File $to) -->File {
        ${ $:docker cp $from "$self:$to" !>error};
        $to.add($from);
    }

    method copy-from($from,$to) --> File {
        ${ $:docker cp "$self:$from" $to !>error};
        $to;
    }

    method remove? { $self-->List[Docker].remove }

    method running? {
        ${ $:docker inspect -f '{{.State.Status}}' $self } eq 'running';
    }

    method start-sleep {
        debug "Starting docker container $self with sleep";
        'while true; do sleep 1; done'.${ $:docker start $self >X !>error};
    }

    method exec(Str $eval)? {
        if $self.running {
            $eval.${ $:docker exec -i $self sh >debug/warn($self) };
        } else {
            $eval.${ $:docker start >debug/warn($self) -i $self };
        }
    }

    method commit(
DockerImg :$name,
          :$tag,
          :@cmd,
          :$env,
          :@entrypoint,
          :$expose,
     Pair :@labels,
          :$onbuild,
          :$user,
          :$volume,
          :$workdir,
     Bool :$remove-old
    )-->DockerImg {
        if $remove-old and $name.exists {
            info "Removing previously existing $name";
            $name.remove;
        }

        my @args = \${
            ("-c=CMD " ~ .JSON if @cmd )
            ("-c=ENV $_" if $env )
            ("-c=ENTRYPOINT " ~ .JSON if @entrypoint)
            ("-c=EXPOSE $_" if $expose)
            ("-c=USER $_" if $user)
            ("-c=VOLUME $_" if $volume)
            ("-c=WORKDIR $_" if $workdir)
            (if @labels {
                '-c=LABEL ' ~ (“{.key}="{.value}"” for @labels).join(" ")
            })
            $self
            @$name
        };

        debug "docker commit: ‘{@args.join('’, ‘')}’";
        my $image = ${$:docker commit  !>fatal @args |
                      sed -r 's/^sha256:(.{12}).*/\1/'};
        $name || $image;
    }
}

augment DockerImg {
    method exists? { ~$self and ${ $:docker image inspect $self *>X } }
    method remove? { $self-->List[DockerImg].remove }
    method Bool { $self.exists }

    method cleanup^ {
        $?docker-img-cleanup.push: $self;
        END {
            $?docker-img-cleanup.slurp-->List[DockerImg].remove;
            $?docker-cleanup.remove;
        }
    }
    method add-tag ($tag)^ {
        my $tagged-name = $tag.contains(':') ?? $tag !! "{$self.name}:$tag";
        debug "Tagging $self with $tagged-name";
        ${$:docker tag $self $tagged-name !>fatal} ?? $tagged-name !! ()
    }

    method name~ {
        $self.${sed -r 's/(.*):.*/\1/'}
    }

    method tag~ {
        $self.${sed 's/.*://;t;s/.*/latest/' }
    }

    method id -->DockerImg ${ $:docker image inspect $self '-f={{.Id}}' !>error }

    method tags -->List[DockerImg] ${
        $:docker image inspect $self
        ｢-f={{range $i, $e := .RepoTags}}{{if $i}}{{"\n"}}{{end}}{{$e}}{{end}}｣
    }

    method copy-from($from,$to) -->File {
        debug "copying $from in $self to $to";
        my $tmp = Docker.create($self);
        $tmp.copy-from($from,$to);
        ${ $:docker rm -f $tmp >X};
        $to;
    }

    method labels --> List[Pair] {
        ${$:docker image inspect
          '-f={{range $key, $value := .Config.Labels}}{{print $key "\t" $value "\n"}}{{end}}'
          $self
         }
    }

    static method images(
        :@has-labels,
        Pair :@labels,
        Bool :$dangling,
        :$reference
    ) --> List[DockerImg] ${
            $:docker images -qa
            ("-f=label={.key}={.value}" for @labels)
            ("-f=label=$_" for @has-labels)
            ("-f=dangling=true" if $dangling)
            ("-f=reference=$_" if $reference)
            | uniq # docker images can return duplicates
    }
}

augment List[Docker] {
    method remove? {
        debug "Removing containers: {$self.join(',')}" if $self;
        ${ $:docker rm -f @$self *>X }
    }
}

augment List[DockerImg] {
    method remove? {
        if $self {
            debug "Removing images: {$self.join(',')}";
            ${ $:docker rmi -f @$self >debug/warn };
        }
    }
}

class Docker { }
class DockerImg { }

constant File $:docker-socket = '/var/run/docker.sock';
constant File $:docker-install-cli-path = '/usr/local/bin/docker';

constant Cmd $:docker is logged-as("\c[WHALE]") = on {
    Spit-Helper { 'docker' }
    Linux {
        Cmd<docker> or (
            if $:docker-socket {
                debug "$_ exists so only installing docker cli";
                Docker.install-cli;
            } else {
                debug "$_ doesn't exist so installing the full Docker package";
                Docker.install;
                'docker';
            }
        )
    }
}

constant $:moby-github = GitHubRepo<moby/moby>;

constant File $?docker-cleanup = ${mktemp};
constant File $?docker-img-cleanup = ${mktemp};

augment Docker {

    static method latest-build-ver~ {
        my $gh-release-url = $:moby-github.latest-release-url;
        die "unable to get latest docker version" unless $gh-release-url;
        $gh-release-url.${sed 's/.*\/v//'};
    }

    static method latest-build-tgz-url-->HTTP {
        my $latest-v = Docker.latest-build-ver;
        "https://download.docker.com/linux/static/stable/x86_64/docker-{$latest-v}.tgz";
    }
    static method install-cli-->File {
        my $url = Docker.latest-build-tgz-url;
        info "Getting latest docker build from $url";
        my $docker-dir = $url.get.extract.cleanup;
        info "Installing to $:docker-install-cli-path";
        $docker-dir.add('docker').move-to($:docker-install-cli-path);
        $:docker-install-cli-path;
    }

    static method install {
        info 'running https://get.docker.com script';
        ${
            sh -c (HTTP<https://get.docker.com>.get)
            >debug('docker-install.sh')
            !>debug('docker-install.sh')
        };
    }

    static method hello-world? {
        ${ $:docker run -i --rm hello-world *>debug('hello-world') };
    }

    method cleanup -->Docker {
        $?docker-cleanup.push($self);
        END {
            $?docker-cleanup.slurp-->List[Docker].remove;
            $?docker-cleanup.remove;
        }
    }

    static method create(
        $from,
        :$name,
        :@args,
        Bool :$mount-socket
    ) -->Docker {
        my $id = ${
            $:docker create -i !>error
            ("--name=$_" if $name)
            ("-v=$:docker-socket:$:docker-socket" if $mount-socket)
            --entrypoint ''
            @args $from sh -c ':;sh'
        }.substr(0,12); # 12 is a sufficient length of hash
        $name || $id;
    }

    method exists? ${ $:docker container inspect $self *>X }
    method Bool    { $self.exists }

    method copy($from, File $to) -->File {
        ${ $:docker cp $from "$self:$to" !>error};
        $to.add($from);
    }

    method copy-from($from,$to) --> File {
        ${ $:docker cp "$self:$from" $to !>error};
        $to;
    }

    method remove? { $self-->List[Docker].remove }

    method running? {
        ${ $:docker inspect -f '{{.State.Status}}' $self } eq 'running';
    }

    method start-sleep {
        debug "Starting docker container $self with sleep";
        'while true; do sleep 1; done'.${ $:docker start $self >X !>error};
    }

    method exec(Str $eval)? {
        if $self.running {
            $eval.${ $:docker exec -i $self sh >debug/warn($self) };
        } else {
            $eval.${ $:docker start >debug/warn($self) -i $self };
        }
    }

    method commit(
DockerImg :$name,
          :$tag,
          :@cmd,
          :$env,
          :@entrypoint,
          :$expose,
     Pair :@labels,
          :$onbuild,
          :$user,
          :$volume,
          :$workdir,
     Bool :$remove-old
    )-->DockerImg {
        if $remove-old and $name.exists {
            info "Removing previously existing $name";
            $name.remove;
        }

        my @args = \${
            ("-c=CMD " ~ .JSON if @cmd )
            ("-c=ENV $_" if $env )
            ("-c=ENTRYPOINT " ~ .JSON if @entrypoint)
            ("-c=EXPOSE $_" if $expose)
            ("-c=USER $_" if $user)
            ("-c=VOLUME $_" if $volume)
            ("-c=WORKDIR $_" if $workdir)
            (if @labels {
                '-c=LABEL ' ~ (“{.key}="{.value}"” for @labels).join(" ")
            })
            $self
            @$name
        };

        debug "docker commit: ‘{@args.join('’, ‘')}’";
        my $image = ${$:docker commit  !>fatal @args |
                      sed -r 's/^sha256:(.{12}).*/\1/'};
        $name || $image;
    }
}

augment DockerImg {
    method exists? { ~$self and ${ $:docker image inspect $self *>X } }
    method remove? { $self-->List[DockerImg].remove }
    method Bool { $self.exists }

    method cleanup^ {
        $?docker-img-cleanup.push: $self;
        END {
            $?docker-img-cleanup.slurp-->List[DockerImg].remove;
            $?docker-cleanup.remove;
        }
    }
    method add-tag ($tag)^ {
        my $tagged-name = $tag.contains(':') ?? $tag !! "{$self.name}:$tag";
        debug "Tagging $self with $tagged-name";
        ${$:docker tag $self $tagged-name !>fatal} ?? $tagged-name !! ()
    }

    method name~ {
        $self.${sed -r 's/(.*):.*/\1/'}
    }

    method tag~ {
        $self.${sed 's/.*://;t;s/.*/latest/' }
    }

    method id -->DockerImg ${ $:docker image inspect $self '-f={{.Id}}' !>error }

    method tags -->List[DockerImg] ${
        $:docker image inspect $self
        ｢-f={{range $i, $e := .RepoTags}}{{if $i}}{{"\n"}}{{end}}{{$e}}{{end}}｣
    }

    method copy-from($from,$to) -->File {
        debug "copying $from in $self to $to";
        my $tmp = Docker.create($self);
        $tmp.copy-from($from,$to);
        ${ $:docker rm -f $tmp >X};
        $to;
    }

    method labels --> List[Pair] {
        ${$:docker image inspect
          '-f={{range $key, $value := .Config.Labels}}{{print $key "\t" $value "\n"}}{{end}}'
          $self
         }
    }

    static method images(
        :@has-labels,
        Pair :@labels,
        Bool :$dangling,
        :$reference
    ) --> List[DockerImg] ${
            $:docker images -qa
            ("-f=label={.key}={.value}" for @labels)
            ("-f=label=$_" for @has-labels)
            ("-f=dangling=true" if $dangling)
            ("-f=reference=$_" if $reference)
            | uniq
    }
}

augment List[Docker] {
    method remove? {
        debug "Removing containers: {$self.join(',')}" if $self;
        ${ $:docker rm -f @$self *>X }
    }
}

augment List[DockerImg] {
    method remove? {
        debug "Removing images: {$self.join(',')}";
        ${ $:docker rmi @$self >debug/warn }
    }
}


BUMP that escalation

BUMP that escalation

#pragma newdecls required
#pragma semicolon 1

#include <sourcemod>
#include <clientprefs>
#include <morecolors>
#include <sourcebans>
#include <sourcecomms>

#define PLUGIN_VERSION "1.0.0"

ConVar cvarAWPVersion;
Menu menuHome, menuPunishments, menuSpecificPunishments, menuTips;
int chatsPunishEscalation[5] =  { 20, 30, 60, 1440, 0 };
int banPunishEscalation[2] =  { 10080, 0 };

//--------------------//

public Plugin myinfo = {
	name = "[NGS] AutoWarnPunishr",
	author = "TheXeon",
	description = "Introduces menu-based administration/escalation to NGS.",
	version = PLUGIN_VERSION,
	url = "https://www.neogenesisnetwork.net"
}

public void OnPluginStart()
{
	// Version CVAR and RESET
	cvarAWPVersion = CreateConVar("sm_ngs_autowarnpunishr_version", PLUGIN_VERSION, "Version of the plugin (used in some strings)");
	cvarAWPVersion.AddChangeHook(OnVersionCvarChanged);

	// Register commands for menus
	RegAdminCmd("sm_ngsadminmenu", CommandAdminMenuHome, ADMFLAG_GENERIC, "Usage: sm_ngsadminmenu");

	/*******************************************
	*
	*					MENUES
	*
	*******************************************/

	// Main Menu
	menuHome = new Menu(MenuHomeHandler);
	menuHome.SetTitle("=== NGS Admin Menu ===");
	menuHome.AddItem("punishments", "Punishments!");
	menuHome.AddItem("admincommands", "Available commands!");
	menuHome.AddItem("tips", "Tips and Reminders!");

	/******************
	*	 SUBMENUES
	******************/

	// Punishments Menu
	menuPunishments = new Menu(PunishmentsMenuHandler);
	menuPunishments.SetTitle("=== NGS Punishments: Choose Broken Rule ===");
	menuPunishments.AddItem("gamemapexploits", "Game/Map Exploits!");
	menuPunishments.AddItem("hackingcheating", "Hacking/Cheating!");
	menuPunishments.AddItem("runningfrombet", "Running from bet!"); // TODO: Make this a tip
	menuPunishments.AddItem("voicetextspam", "Voice/Text Chat Spam)!");
	menuPunishments.AddItem("earrapenondj", "Earrape/Non-DJ Spam!");
	menuPunishments.AddItem("toxicity", "Toxicity (arguments, etc)!");
	menuPunishments.AddItem("cursing", "Cursing (slurs, excessive)!");
	menuPunishments.AddItem("shocksprays", "Shock/illegal-content sprays!");
	menuPunishments.AddItem("conversationtopics", "Political/Religious/etc beliefs!");
	menuPunishments.AddItem("adminimpersionation", "Admin impersonating!");
	menuPunishments.AddItem("begging", "Asking for donations/begging!");
	menuPunishments.AddItem("holdingpaidevents", "Hosting paid events/raffles/etc!");
	menuPunishments.ExitBackButton = true;

	// Punishments Sub-Menu (Specifics)
	menuSpecificPunishments = new Menu(PunishmentsSubMenuHandler);
	menuSpecificPunishments.SetTitle("=== NGS Punishments: Specify ===");
	menuSpecificPunishments.ExitBackButton = true;

	// Load translations
	LoadTranslations("common.phrases");
}

public void OnVersionCvarChanged(ConVar convar, char[] oldValue, char[] newValue)
{
	convar.SetString(PLUGIN_VERSION);
}

public void OnClientPutInServer(int client)
{
	VoicesEnabled[client] = false;
}

public Action CommandAdminMenuHome(int client, int args)
{
	if (!IsValidClient(client)) return Plugin_Handled;
	menuHome.Display(client, MENU_TIME_FOREVER);
	return Plugin_Handled;
}

/*******************************************
*										   *
*			  MENUES HANDLERS			   *
*										   *
*******************************************/

// Main menu handler
public int MenuHomeHandler(Menu menu, MenuAction action, int param1, int param2)
{
	if (action == MenuAction_Select)
	{
		char info[32];
		menu.GetItem(param2, info, sizeof(info));
		if (StrEqual(info, "punishments", false))
			menuPunishments.Display(param1, MENU_TIME_FOREVER);
		else if (StrEqual(info, "admincommands", false))
			serverCommandsMainMenu.Display(param1, MENU_TIME_FOREVER);
		else if (StrEqual(info, "tips", false))
			menuTips.Display(param1, MENU_TIME_FOREVER);
	}
	// no delete because reuse of handle
}

public int PunishmentsMenuHandler(Menu menu, MenuAction action, int param1, int param2)
{
	if (action == MenuAction_Cancel && param2 == MenuCancel_ExitBack)
	{
		menuHome.Display(param1, MENU_TIME_FOREVER);
	}
	else if (action == MenuAction_Select)
	{
		menuSpecificPunishments.RemoveAllItems();
		char info[32];
		menu.GetItem(param2, info, sizeof(info));
		if (StrEqual("gamemapexploits", info))
		{
			menuSpecificPunishments.AddItem("gameexploit", "Game Exploit!");
			menuSpecificPunishments.AddItem("mapexploit", "Map Exploit!");
		}
		else if (StrEqual("hackingcheating", info))
		{

		}
		menuSpecificPunishments.Display(param1, MENU_TIME_FOREVER);
	}
	// no delete because reuse of handle
}

public bool IsValidClient(int client)
{
	if(client > 4096) client = EntRefToEntIndex(client);
	if(client < 1 || client > MaxClients) return false;
	if(!IsClientInGame(client)) return false;
	if(IsFakeClient(client)) return false;
	if(GetEntProp(client, Prop_Send, "m_bIsCoaching")) return false;
	return true;
}

#pragma newdecls required
#pragma semicolon 1

#include <sourcemod>
#include <clientprefs>
#include <morecolors>
#include <sourcebans>
#include <sourcecomms>

#define PLUGIN_VERSION "1.0.0"

ConVar cvarAWPVersion;
Menu menuHome, menuPunishments, menuSpecificPunishments, menuTips;
int[] chatsSpamEscalation =  { 20, 30, 60, 1440, 10080, 0 };

//--------------------//

public Plugin myinfo = {
	name = "[NGS] AutoWarnPunishr",
	author = "TheXeon",
	description = "Introduces menu-based administration/escalation to NGS.",
	version = PLUGIN_VERSION,
	url = "https://www.neogenesisnetwork.net"
}

public void OnPluginStart()
{
	// Version CVAR and RESET
	cvarAWPVersion = CreateConVar("sm_ngs_autowarnpunishr_version", PLUGIN_VERSION, "Version of the plugin (used in some strings)");
	cvarAWPVersion.AddChangeHook(OnVersionCvarChanged);

	// Register commands for menus
	RegAdminCmd("sm_ngsadminmenu", CommandAdminMenuHome, ADMFLAG_GENERIC, "Usage: sm_ngsadminmenu");

	/*******************************************
	*
	*					MENUES
	*
	*******************************************/

	// Main Menu
	menuHome = new Menu(MenuHomeHandler);
	menuHome.SetTitle("=== NGS Admin Menu ===");
	menuHome.AddItem("punishments", "Punishments!");
	menuHome.AddItem("admincommands", "Available commands!");
	menuHome.AddItem("tips", "Tips and Reminders!");

	/******************
	*	 SUBMENUES
	******************/

	// Punishments Menu
	menuPunishments = new Menu(PunishmentsMenuHandler);
	menuPunishments.SetTitle("=== NGS Punishments: Choose Broken Rule ===");
	menuPunishments.AddItem("gamemapexploits", "Game/Map Exploits!");
	menuPunishments.AddItem("hackingcheating", "Hacking/Cheating!");
	menuPunishments.AddItem("runningfrombet", "Running from bet!"); // TODO: Make this a tip
	menuPunishments.AddItem("voicetextspam", "Voice/Text Chat Spam)!");
	menuPunishments.AddItem("earrapenondj", "Earrape/Non-DJ Spam!");
	menuPunishments.AddItem("toxicity", "Toxicity (arguments, etc)!");
	menuPunishments.AddItem("cursing", "Cursing (slurs, excessive)!");
	menuPunishments.AddItem("shocksprays", "Shock/illegal-content sprays!");
	menuPunishments.AddItem("conversationtopics", "Political/Religious/etc beliefs!");
	menuPunishments.AddItem("adminimpersionation", "Admin impersonating!");
	menuPunishments.AddItem("begging", "Asking for donations/begging!");
	menuPunishments.AddItem("holdingpaidevents", "Hosting paid events/raffles/etc!");
	menuPunishments.ExitBackButton = true;

	// Punishments Sub-Menu (Specifics)
	menuSpecificPunishments = new Menu(PunishmentsSubMenuHandler);
	menuSpecificPunishments.SetTitle("=== NGS Punishments: Specify ===");
	menuSpecificPunishments.ExitBackButton = true;

	// Load translations
	LoadTranslations("common.phrases");
}

public void OnVersionCvarChanged(ConVar convar, char[] oldValue, char[] newValue)
{
	convar.SetString(PLUGIN_VERSION);
}

public void OnClientPutInServer(int client)
{
	VoicesEnabled[client] = false;
}

public Action CommandAdminMenuHome(int client, int args)
{
	if (!IsValidClient(client)) return Plugin_Handled;
	menuHome.Display(client, MENU_TIME_FOREVER);
	return Plugin_Handled;
}

/*******************************************
*										   *
*			  MENUES HANDLERS			   *
*										   *
*******************************************/

// Main menu handler
public int MenuHomeHandler(Menu menu, MenuAction action, int param1, int param2)
{
	if (action == MenuAction_Select)
	{
		char info[32];
		menu.GetItem(param2, info, sizeof(info));
		if (StrEqual(info, "punishments", false))
			menuPunishments.Display(param1, MENU_TIME_FOREVER);
		else if (StrEqual(info, "admincommands", false))
			serverCommandsMainMenu.Display(param1, MENU_TIME_FOREVER);
		else if (StrEqual(info, "tips", false))
			menuTips.Display(param1, MENU_TIME_FOREVER);
	}
	// no delete because reuse of handle
}

public int PunishmentsMenuHandler(Menu menu, MenuAction action, int param1, int param2)
{
	if (action == MenuAction_Cancel && param2 == MenuCancel_ExitBack)
	{
		menuHome.Display(param1, MENU_TIME_FOREVER);
	}
	else if (action == MenuAction_Select)
	{
		menuSpecificPunishments.RemoveAllItems();
		char info[32];
		menu.GetItem(param2, info, sizeof(info));
		if (StrEqual("gamemapexploits", info))
		{
			menuSpecificPunishments.AddItem("gameexploit", "Game Exploit!");
			menuSpecificPunishments.AddItem("mapexploit", "Map Exploit!");
		}
		else if (StrEqual("hackingcheating", info))
		menuSpecificPunishments.Display(param1, MENU_TIME_FOREVER);
	}
	// no delete because reuse of handle
}

public bool IsValidClient(int client)
{
	if(client > 4096) client = EntRefToEntIndex(client);
	if(client < 1 || client > MaxClients) return false;
	if(!IsClientInGame(client)) return false;
	if(IsFakeClient(client)) return false;
	if(GetEntProp(client, Prop_Send, "m_bIsCoaching")) return false;
	return true;
}


Do not call stopjetpack on disconnecting clients when jetpacks are off

/**
 * vim: set ts=4 :
 * =============================================================================
 * Jetpack Plus
 * Rewrite of the Sourcemod plugin to make it easier to extend and modify.
 *
 * Copyright 2014 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <sdktools>
#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 = "Let client's fly around by holding jump.",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_jetpack_plus"
};

new Handle:g_Cvar_Enabled = INVALID_HANDLE;
new Handle:g_Cvar_JetpackForce = INVALID_HANDLE;
new Handle:g_Cvar_JumpDelay = INVALID_HANDLE;

new Handle:g_Forward_OnStartJetpack = INVALID_HANDLE;
new Handle:g_Forward_OnStopJetpack = INVALID_HANDLE;
new Handle:g_Forward_OnJetpackStep= 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");

    CreateNative("IsClientUsingJetpack", _IsClientUsingJetpack);
    CreateNative("AreJetpacksEnabled", _AreJetpacksEnabled);

    return APLRes_Success;
}

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

    g_Cvar_Enabled = CreateConVar(
            "sm_jetpack",
            "1",
            "Set to 1 to enable the jetpack plugin",
            0,
            true,
            0.0,
            true,
            1.0);
    g_Cvar_JetpackForce = CreateConVar(
            "sm_jetpack_force",
            "8.0",
            "Strength at which the jetpack pushes the player"
            );
    g_Cvar_JumpDelay = CreateConVar(
            "sm_jetpack_jump_delay",
            "0.15",
            "The time in seconds the jump key needs to be pressed before the jetpack starts"
            );

    HookConVarChange(g_Cvar_Enabled, OnEnabledChange);

    g_Forward_OnStartJetpack = CreateGlobalForward("OnStartJetpack", ET_Event, Param_Cell);
    g_Forward_OnStopJetpack =  CreateGlobalForward("OnStopJetpack",  ET_Event, Param_Cell);
    g_Forward_OnJetpackStep =  CreateGlobalForward("OnJetpackStep",  ET_Event, Param_Cell, Param_FloatByRef, Param_CellByRef);

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

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

public OnGameFrame()
{
    if(AreJetpacksEnabled())
    {
        StepAllJetpacks();
    }
}

public OnEnabledChange(Handle:cvar, const String:oldValue[], const String:newValue[])
{
    if(cvar != g_Cvar_Enabled) return;

    new bool:was_on = !!StringToInt(oldValue);
    new bool:now_on = !!StringToInt(newValue);

    //When changing from on to off
    if(was_on && !now_on)
    {
        StopAllJetpacks();
    }
}

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);
        new Float:delay = GetConVarFloat(g_Cvar_JumpDelay);
        CreateTimer(delay, 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;
}

public _IsClientUsingJetpack(Handle:plugin, args) { return _:IsClientUsingJetpack(GetNativeCell(1)); }
bool:IsClientUsingJetpack(client)
{
    return g_IsUsingJetpack[client];
}

public _AreJetpacksEnabled(Handle:plugin, args) { return _:AreJetpacksEnabled(); }
bool:AreJetpacksEnabled()
{
    return GetConVarBool(g_Cvar_Enabled);
}

StartJetpack(client)
{
    //Forward event
    decl Action:result;
    Call_StartForward(g_Forward_OnStartJetpack);
    Call_PushCell(client);
    Call_Finish(result);
    if(result == Plugin_Handled) return;

    SetEntityMoveType(client, MOVETYPE_FLY);
    SetEntityMoveCollide(client, MOVECOLLIDE_FLY_BOUNCE);
    ChangeEdictState(client);
    g_IsUsingJetpack[client] = true;
}

StopJetpack(client)
{
    //Forward event
    decl Action:result;
    Call_StartForward(g_Forward_OnStopJetpack);
    Call_PushCell(client);
    Call_Finish(result);
    if(result == Plugin_Handled) return;

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

//Called each frame a client is using a jetpack
JetpackStep(client)
{
    new Float:force = GetConVarFloat(g_Cvar_JetpackForce);
    new bool:force_stop = false;

    //Forward event
    decl Action:result;
    Call_StartForward(g_Forward_OnJetpackStep);
    Call_PushCell(client);
    Call_PushFloatRef(force);
    Call_PushCellRef(force_stop);
    Call_Finish(result);
    if(result == Plugin_Handled) return;

    new buttons = GetClientButtons(client);
    if(!force_stop && IsPlayerAlive(client) && (buttons & IN_JUMP))
    {
        JetpackPush(client, force);
    }
    else
    {
        StopJetpack(client);
    }
}

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

StopAllJetpacks()
{
    for (new client=1; client <= MaxClients; client++)
    {
        //For each client using a jetpack
        if(IsClientUsingJetpack(client))
        {
            StopJetpack(client);
        }
    }
}

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

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

/**
 * vim: set ts=4 :
 * =============================================================================
 * Jetpack Plus
 * Rewrite of the Sourcemod plugin to make it easier to extend and modify.
 *
 * Copyright 2014 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <sdktools>
#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 = "Let client's fly around by holding jump.",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_jetpack_plus"
};

new Handle:g_Cvar_Enabled = INVALID_HANDLE;
new Handle:g_Cvar_JetpackForce = INVALID_HANDLE;
new Handle:g_Cvar_JumpDelay = INVALID_HANDLE;

new Handle:g_Forward_OnStartJetpack = INVALID_HANDLE;
new Handle:g_Forward_OnStopJetpack = INVALID_HANDLE;
new Handle:g_Forward_OnJetpackStep= 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");

    CreateNative("IsClientUsingJetpack", _IsClientUsingJetpack);
    CreateNative("AreJetpacksEnabled", _AreJetpacksEnabled);

    return APLRes_Success;
}

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

    g_Cvar_Enabled = CreateConVar(
            "sm_jetpack",
            "1",
            "Set to 1 to enable the jetpack plugin",
            0,
            true,
            0.0,
            true,
            1.0);
    g_Cvar_JetpackForce = CreateConVar(
            "sm_jetpack_force",
            "8.0",
            "Strength at which the jetpack pushes the player"
            );
    g_Cvar_JumpDelay = CreateConVar(
            "sm_jetpack_jump_delay",
            "0.15",
            "The time in seconds the jump key needs to be pressed before the jetpack starts"
            );

    HookConVarChange(g_Cvar_Enabled, OnEnabledChange);

    g_Forward_OnStartJetpack = CreateGlobalForward("OnStartJetpack", ET_Event, Param_Cell);
    g_Forward_OnStopJetpack =  CreateGlobalForward("OnStopJetpack",  ET_Event, Param_Cell);
    g_Forward_OnJetpackStep =  CreateGlobalForward("OnJetpackStep",  ET_Event, Param_Cell, Param_FloatByRef, Param_CellByRef);

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

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

public OnGameFrame()
{
    if(AreJetpacksEnabled())
    {
        StepAllJetpacks();
    }
}

public OnEnabledChange(Handle:cvar, const String:oldValue[], const String:newValue[])
{
    if(cvar != g_Cvar_Enabled) return;

    new bool:was_on = !!StringToInt(oldValue);
    new bool:now_on = !!StringToInt(newValue);

    //When changing from on to off
    if(was_on && !now_on)
    {
        StopAllJetpacks();
    }
}

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);
        new Float:delay = GetConVarFloat(g_Cvar_JumpDelay);
        CreateTimer(delay, 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;
}

public _IsClientUsingJetpack(Handle:plugin, args) { return _:IsClientUsingJetpack(GetNativeCell(1)); }
bool:IsClientUsingJetpack(client)
{
    return g_IsUsingJetpack[client];
}

public _AreJetpacksEnabled(Handle:plugin, args) { return _:AreJetpacksEnabled(); }
bool:AreJetpacksEnabled()
{
    return GetConVarBool(g_Cvar_Enabled);
}

StartJetpack(client)
{
    //Forward event
    decl Action:result;
    Call_StartForward(g_Forward_OnStartJetpack);
    Call_PushCell(client);
    Call_Finish(result);
    if(result == Plugin_Handled) return;

    SetEntityMoveType(client, MOVETYPE_FLY);
    SetEntityMoveCollide(client, MOVECOLLIDE_FLY_BOUNCE);
    ChangeEdictState(client);
    g_IsUsingJetpack[client] = true;
}

StopJetpack(client)
{
    //Forward event
    decl Action:result;
    Call_StartForward(g_Forward_OnStopJetpack);
    Call_PushCell(client);
    Call_Finish(result);
    if(result == Plugin_Handled) return;

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

//Called each frame a client is using a jetpack
JetpackStep(client)
{
    new Float:force = GetConVarFloat(g_Cvar_JetpackForce);
    new bool:force_stop = false;

    //Forward event
    decl Action:result;
    Call_StartForward(g_Forward_OnJetpackStep);
    Call_PushCell(client);
    Call_PushFloatRef(force);
    Call_PushCellRef(force_stop);
    Call_Finish(result);
    if(result == Plugin_Handled) return;

    new buttons = GetClientButtons(client);
    if(!force_stop && IsPlayerAlive(client) && (buttons & IN_JUMP))
    {
        JetpackPush(client, force);
    }
    else
    {
        StopJetpack(client);
    }
}

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

StopAllJetpacks()
{
    for (new client=1; client <= MaxClients; client++)
    {
        //For each client using a jetpack
        if(IsClientUsingJetpack(client))
        {
            StopJetpack(client);
        }
    }
}

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

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