Get more data from twitch json

Get more data from twitch json

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

#pragma semicolon 1

#include <sourcemod>
#include <twitch_recognition>
#include <regex>
#include <steamtools>

#define PLUGIN_VERSION "0.1"

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

new bool:g_DoTwitchCheck[MAXPLAYERS+1];
new bool:g_HasTwitchChannel[MAXPLAYERS+1];
new bool:g_IsStreaming[MAXPLAYERS+1];

new Handle:g_Cvar_Enabled = INVALID_HANDLE;

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

    RegPluginLibrary("twitch_recognition");

    return APLRes_Success;
}

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

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

    AddCommandListener(Event_JoinClass, "joinclass");

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

public OnClientConnected(client)
{
    g_DoTwitchCheck[client] = true;
    g_HasTwitchChannel[client] = false;
    g_IsStreaming[client] = false;
}

public OnClientDisconnect(client)
{
    g_DoTwitchCheck[client] = false;
    g_HasTwitchChannel[client] = false;
    g_IsStreaming[client] = false;
}

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

public Action:Event_JoinClass(client, const String:command[], args)
{
    if(g_DoTwitchCheck[client])
    {
        QuerySteamWorksApi(client);
        g_DoTwitchCheck[client] = false;
    }

    return Plugin_Continue;
}

QuerySteamWorksApi(client)
{
    if (!IsEnabled()) return;

    //Build steamworks url
    decl String:url[256], String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));
    Format(url, sizeof(url),
            "%s%s", STEAM_WORKS_ROUTE, uid);

    new HTTPRequestHandle:request = CreateIGARequest(url);

    if(request == INVALID_HTTP_HANDLE)
    {
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "xml", "1");

    new player = client > 0 ? GetClientUserId(client) : 0;
    Steam_SendHTTPRequest(request, ReceiveSteamWorksApi, player);
}

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

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

    //NOTE : This is very sloppy; the text returned could well beover 20kb;
    //most of the group and owned game stuff is useless or could contain
    //false-positive twitch urls.  However the more useful SteamApi does not
    //query a steam profile's summary, headline or links fields.

    //Search user's steamworks data for twitch.tv urls.
    //We're assuming they would only put their own url on their page
    new channel_regex = CompileRegex("twitch.tv/(\\w+)");
    MatchRegex(channel_regex, data);

    if(GetRegexSubstring(channel_regex, 1, channel, sizeof(channel)))
    {
        //A twitch url was found; assume it is the player's
        g_HasTwitchChannel[client] = true;
        QueryTwitchApi(client, channel);
    }
}

QueryTwitchApi(client, String:channel[])
{
    if (!IsEnabled()) return;

    //Build twitch url
    decl String:url[256];
    Format(url, sizeof(url),
            "%s%s", TWITCH_ROUTE, channel);

    new HTTPRequestHandle:request = CreateIGARequest(url);

    if(request == INVALID_HTTP_HANDLE)
    {
        return;
    }

    new player = client > 0 ? GetClientUserId(client) : 0;
    Steam_SendHTTPRequest(request, ReceiveTwitchApi, player);
}

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

    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[Twitch] Error at RecieveTwitchApi (HTTP Code %d; success %d)", code, successful);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

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

    //parse JSON response
    new Handle:json = json_load(data);
    new Handle:stream = json_object_get(json, "stream");

    if(stream == INVALID_HANDLE)
    {
        //json is wrong
    }else if(json_is_null(stream))
    {
        //Stream object is null, thus player is not streaming
        g_IsStreaming[client] = false;
    }else{
        //Player is streaming
        g_IsStreaming[client] = true;

        //Get more data
        new String:display_name[128], String:status[128], name[128];
        new viewers = json_object_get_int(stream, "viewers");

        new Handle:channel = json_object_get(json, "channel");
        if(channel != INVALID_HANDLE)
        {
            json_object_get_string(json, "display_name", display_name, sizeof(display_name));
            json_object_get_string(json, "status", status, sizeof(status));
            json_object_get_string(json, "name", name, sizeof(name));
        }

        //Do whatever
    }
}

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

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

#pragma semicolon 1

#include <sourcemod>
#include <twitch_recognition>
#include <regex>
#include <steamtools>

#define PLUGIN_VERSION "0.1"

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

new bool:g_DoTwitchCheck[MAXPLAYERS+1];
new bool:g_HasTwitchChannel[MAXPLAYERS+1];
new bool:g_IsStreaming[MAXPLAYERS+1];

new Handle:g_Cvar_Enabled = INVALID_HANDLE;

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

    RegPluginLibrary("twitch_recognition");

    return APLRes_Success;
}

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

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

    AddCommandListener(Event_JoinClass, "joinclass");

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

public OnClientConnected(client)
{
    g_DoTwitchCheck[client] = true;
    g_HasTwitchChannel[client] = false;
    g_IsStreaming[client] = false;
}

public OnClientDisconnect(client)
{
    g_DoTwitchCheck[client] = false;
    g_HasTwitchChannel[client] = false;
    g_IsStreaming[client] = false;
}

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

public Action:Event_JoinClass(client, const String:command[], args)
{
    if(g_DoTwitchCheck[client])
    {
        QuerySteamWorksApi(client);
        g_DoTwitchCheck[client] = false;
    }

    return Plugin_Continue;
}

QuerySteamWorksApi(client)
{
    if (!IsEnabled()) return;

    //Build steamworks url
    decl String:url[256], String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));
    Format(url, sizeof(url),
            "%s%s", STEAM_WORKS_ROUTE, uid);

    new HTTPRequestHandle:request = CreateIGARequest(url);

    if(request == INVALID_HTTP_HANDLE)
    {
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "xml", "1");

    new player = client > 0 ? GetClientUserId(client) : 0;
    Steam_SendHTTPRequest(request, ReceiveSteamWorksApi, player);
}

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

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

    //NOTE : This is very sloppy; the text returned could well beover 20kb;
    //most of the group and owned game stuff is useless or could contain
    //false-positive twitch urls.  However the more useful SteamApi does not
    //query a steam profile's summary, headline or links fields.

    //Search user's steamworks data for twitch.tv urls.
    //We're assuming they would only put their own url on their page
    new channel_regex = CompileRegex("twitch.tv/(\\w+)");
    MatchRegex(channel_regex, data);

    if(GetRegexSubstring(channel_regex, 1, channel, sizeof(channel)))
    {
        //A twitch url was found; assume it is the player's
        g_HasTwitchChannel[client] = true;
        QueryTwitchApi(client, channel);
    }
}

QueryTwitchApi(client, String:channel[])
{
    if (!IsEnabled()) return;

    //Build twitch url
    decl String:url[256];
    Format(url, sizeof(url),
            "%s%s", TWITCH_ROUTE, channel);

    new HTTPRequestHandle:request = CreateIGARequest(url);

    if(request == INVALID_HTTP_HANDLE)
    {
        return;
    }

    new player = client > 0 ? GetClientUserId(client) : 0;
    Steam_SendHTTPRequest(request, ReceiveTwitchApi, player);
}

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

    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[Twitch] Error at RecieveTwitchApi (HTTP Code %d; success %d)", code, successful);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

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

    //parse JSON response
    new Handle:json = json_load(data);
    new Handle:stream = json_object_get(json, "stream");

    if(stream == INVALID_HANDLE)
    {
        //json is wrong
    }else if(json_is_null(stream))
    {
        //Stream object is null, thus player is not streaming
        g_IsStreaming[client] = false;
    }else{
        //Player is streaming
        g_IsStreaming[client] = true;

        //Get more data

    }
}

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


hack to stop broken pipe errors when logging is turned off

The real solution is to stop log being called at all when logging is
turned off.

my FD $:LOG = $:ERR;
constant $:log-level = 1;
constant $:log = False;
constant $:log-default-path = "\c[GHOST]";
my @:log-levels  = (
    none => '',
    debug => "\c[SMALL QUESTION MARK]",
    info  => "\c[INFORMATION SOURCE, VARIATION SELECTOR-16] ",
    warn => "\c[WARNING SIGN]",
    error => "\c[HEAVY EXCLAMATION MARK SYMBOL]",
    fatal => "\c[SKULL AND CROSSBONES]",
);

constant $:log-fatal-level = 5;

constant $:log-date-format = '%b %d %T';

class Log {
    method date~ {
        $self.${ cut -s '-d|' '-f1' }
    }
    method level-sym~ {
        $self.${cut -s '-d|' '-f2'};
    }
    method level-name~ {
        @:log-levels.key-for($self.level-sym);
    }
    method level+ {
        @:log-levels.values.pairs.key-for($self.level-sym);
    }
    method path~ {
        $self.${ cut -s '-d|' '-f3' }
    }
    method message~ {
        $self.${ cut -s '-d|' '-f4-' }
    }
}

# While logging is a WIP we'll just leave this here
augment Str {

    method log(Int $level, $path?) {
        if $:log and $level >= $:log-level {
            my $_path = $path || $:log-default-path;
            on {
                BusyBox {
                    $self.${
                        awk > $:LOG
                        -v "level_sym={@:log-levels[$level].value}"
                        -v "level=$level"
                        -v "path=$_path"
                        -v "pid=$?PID"
                        -v "date_format=$:log-date-format"
                        -v "fatal_level=$:log-fatal-level"
                        Q⟪
                            length() == 0 { next }
                            {
                                if ($0 ~ /^([^|]+\|){3}/) {
                                    print gensub(/^(([^|]+\|){2})([^|]+\|)/, "\\1"path"/\\3",1)
                                } else {
                                    "date '+"date_format"'" | getline date;
                                    printf "%s|%s|%s|%s\n",date,level_sym,path,$0;
                                }
                                if(level >= fatal_level)
                                    die = 1
                            }
                            END { if(die){ system("kill "pid) } }⟫
                    }
                }
                GNU {
                    $self.${
                        sed -ru > $:LOG
                        -e (
                            # if it looks like a log append its path to this one
                            # print and then branch to the end
                            ‘s§^(([^|]+\|){2})([^|]+\|)§\1’ ~ $_path ~ ‘/\3§;t;’ ~
                            # remove empty lines
                            '/^$/d;' ~
                            # Put the path on the front of the message
                            "s§^§$_path|§;" ~
                            # Put everything in hold space and put the date in pattern space
                            “h;s/.*/date '+$:log-date-format'/e;” ~
                            # Append the hold space to the date and add the
                            # insert the log level symbol between the two
                            ‘G;s§\n§|’ ~ @:log-levels[$level].value ~ ‘|§;’ ~
                            # Add a newline if it doesn't exist
                            ("\$e kill $?PID" if $level == $:log-fatal-level)
                        )
                        -e '$a\\'
                    }
                }
            }
        }
        else {
            #XXX: hack to stop broken pipe errors when .log is piped to when $:log
            # is False
            ${ cat >X };
        }
    }
}

sub fatal($msg, $path?){ $:log ?? $msg.log(5, $path) !! die $msg  }
sub error($msg, $path?){ $:log ?? $msg.log(4, $path) !! note $msg }
sub warn ($msg, $path?){ $:log ?? $msg.log(3, $path) !! note $msg }
sub info ($msg, $path?){ $:log ?? $msg.log(2, $path) !! note $msg }
sub debug($msg, $path?){ $:log ?? $msg.log(1, $path) !! () }

sub log-fifo(Int $level, $path?) --> File {
    my $log-fifo = File.tmp-fifo;
    # We don't really want to have to .cleanup. But if we don't and we
    # do a fatal log then the parent process will terminate and
    # therefore never reap this process. I wonder if there's a better
    # way to do it...
    (start {
        ${cat < $log-fifo}.log($level, $path);
        $log-fifo.remove;
    }).cleanup;
    $log-fifo;
}

my FD $:LOG = $:ERR;
constant $:log-level = 1;
constant $:log = False;
constant $:log-default-path = "\c[GHOST]";
my @:log-levels  = (
    none => '',
    debug => "\c[SMALL QUESTION MARK]",
    info  => "\c[INFORMATION SOURCE, VARIATION SELECTOR-16] ",
    warn => "\c[WARNING SIGN]",
    error => "\c[HEAVY EXCLAMATION MARK SYMBOL]",
    fatal => "\c[SKULL AND CROSSBONES]",
);

constant $:log-fatal-level = 5;

constant $:log-date-format = '%b %d %T';

class Log {
    method date~ {
        $self.${ cut -s '-d|' '-f1' }
    }
    method level-sym~ {
        $self.${cut -s '-d|' '-f2'};
    }
    method level-name~ {
        @:log-levels.key-for($self.level-sym);
    }
    method level+ {
        @:log-levels.values.pairs.key-for($self.level-sym);
    }
    method path~ {
        $self.${ cut -s '-d|' '-f3' }
    }
    method message~ {
        $self.${ cut -s '-d|' '-f4-' }
    }
}

# While logging is a WIP we'll just leave this here
augment Str {

    method log(Int $level, $path?) {
        if $:log and $level >= $:log-level {
            my $_path = $path || $:log-default-path;
            on {
                BusyBox {
                    $self.${
                        awk > $:LOG
                        -v "level_sym={@:log-levels[$level].value}"
                        -v "level=$level"
                        -v "path=$_path"
                        -v "pid=$?PID"
                        -v "date_format=$:log-date-format"
                        -v "fatal_level=$:log-fatal-level"
                        Q⟪
                            length() == 0 { next }
                            {
                                if ($0 ~ /^([^|]+\|){3}/) {
                                    print gensub(/^(([^|]+\|){2})([^|]+\|)/, "\\1"path"/\\3",1)
                                } else {
                                    "date '+"date_format"'" | getline date;
                                    printf "%s|%s|%s|%s\n",date,level_sym,path,$0;
                                }
                                if(level >= fatal_level)
                                    die = 1
                            }
                            END { if(die){ system("kill "pid) } }⟫
                    }
                }
                GNU {
                    $self.${
                        sed -ru > $:LOG
                        -e (
                            # if it looks like a log append its path to this one
                            # print and then branch to the end
                            ‘s§^(([^|]+\|){2})([^|]+\|)§\1’ ~ $_path ~ ‘/\3§;t;’ ~
                            # remove empty lines
                            '/^$/d;' ~
                            # Put the path on the front of the message
                            "s§^§$_path|§;" ~
                            # Put everything in hold space and put the date in pattern space
                            “h;s/.*/date '+$:log-date-format'/e;” ~
                            # Append the hold space to the date and add the
                            # insert the log level symbol between the two
                            ‘G;s§\n§|’ ~ @:log-levels[$level].value ~ ‘|§;’ ~
                            # Add a newline if it doesn't exist
                            ("\$e kill $?PID" if $level == $:log-fatal-level)
                        )
                        -e '$a\\'
                    }
                }
            }
        }
    }
}

sub fatal($msg, $path?){ $:log ?? $msg.log(5, $path) !! die $msg  }
sub error($msg, $path?){ $:log ?? $msg.log(4, $path) !! note $msg }
sub warn ($msg, $path?){ $:log ?? $msg.log(3, $path) !! note $msg }
sub info ($msg, $path?){ $:log ?? $msg.log(2, $path) !! note $msg }
sub debug($msg, $path?){ $:log ?? $msg.log(1, $path) !! () }

sub log-fifo(Int $level, $path?) --> File {
    my $log-fifo = File.tmp-fifo;
    # We don't really want to have to .cleanup. But if we don't and we
    # do a fatal log then the parent process will terminate and
    # therefore never reap this process. I wonder if there's a better
    # way to do it...
    (start {
        ${cat < $log-fifo}.log($level, $path);
        $log-fifo.remove;
    }).cleanup;
    $log-fifo;
}


Remove uploader plugin

Remove uploader plugin

The MOTD browser crashes when uploading files so this was a waste.

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


Use sed delimiter in perl subst-re for now

\c[GHOST] causes a padding space now so it was goofing.

constant $:sed-delimiter = "\x[1e]";

#| The match list variable. Like `$/` in Perl 6 it stores the what was
#| match after a something is matched against a regex.
#|{
    my $text = "The file is: foo.txt";
    $text.match(/:\s*(.+)\.(.+)$/);
    say @/[0]; #-> : foo.txt
    say @/[1]; #-> foo
    say @/[2]; #-> txt
}
my @/;
#| The Str class is the base primitive class of Spit-sh. It represents
#| a string in the shell. Since all shell constructs are made out of
#| strings all types inherits from this class.
#|{
   say "foo" ~~ Str; # true
   say <one two three> ~~ Str; #true
}
augment Str {
    #| Writes string to `$:OUT` followed by a newline if it doesn't
    #| end in one already.
    method say { $self.${awk '{print}' >$:OUT} }
    #| Writes string to $:ERR followed by a newline if it doesn't end
    #| in one already.
    method note { $self.${awk '{print}' >$:ERR} }
    #| Writes string to `$:OUT`.
    method print { $self.${ >$:OUT } }
    #| Returns an uppercase version of the string
    method uc~    { $self.${tr '[:lower:]' '[:upper:]'} }
    #| Returns an lowercase version of the string
    method lc~    { $self.${tr '[:upper:]' '[:lower:]'} }
    #| Returns the number of characters in the string. **note:** This
    #| will depend on the locale of the terminal the script is running in.
    method chars+ { $self.${wc -m} }
    #| Returns the number of bytes in the string.
    method bytes+ { $self.${wc -c} }

    method lines+ { $self.${wc -l} }

    #| Splits the string on a separator. Returns the string with each
    #| instance of the `$sep` replaced with `\n` as a [List].
    method split(#|[The separator to split on]$sep)@ {
        $self.${
          awk -v "FS=$sep" '{l = split($0,a); for (i = 0; i < l;) print a[++i]}'
         }
    }
    #| Returns true if the string isn't empty
    method Bool { ${test $self} }

    #| Returns the string with the target string replaced by a replacement string.
    #| Does not modify the original string.
    #|{
       my $a = "food";
       $a.subst('o','e').say;
       $a.subst('o','e',:g).say;
       say $a;
    }
    method subst(#|[The string to be replaced]$target,
                  #|[The string to replace it with]$replacement,
                  #|[Turns on global matching]Bool :$g)~ {

        $self.${
            awk -v "g=$g" -v 'RS=^$' :T($target) :R($replacement)
            Q⟪
            {
                r=ENVIRON["R"]; t=ENVIRON["T"]
                while((g || !i) && (i = index(substr($0,w + 1),t))){
                    $0 = substr($0,1,i + w - 1) r substr($0,w + i + length(t));
                    w += length(r) - length(t) + i
                    if(w < 0){ w = 0 }
                }
                print;
            }⟫
        }
    }

    method subst-eval($placeholder,$value)~ {
        $self.subst($placeholder,$value.subst("'","'\\''"));
    }
    #| Returns true if the string contains `$needle`.
    #|{
       say "Hello, World".contains('Wo'); #-> True
       say "Hello, World".contains('wo'); #-> False
       say "Hello, World".contains('wo',:i); #-> True
    }
    method contains(#|[The string being searched for]$needle,
                     #|[Turns on case insensitive matching]Bool :$i)? on {
        Any {
            $self.${awk -v 'RS=^$' -v "t=$needle" -v "i=$i"
                    'END{exit(!index(i ? tolower($0) :$0, i ? tolower(t) : t))}'}
        }
        BusyBox {
            $self.${awk -v 'RS=^$' -v "t=$needle" -v "i=$i"
                    'END{exit(!index(i ? tolower($0) :$0, i ? tolower(t) : t))}'}
            # BusyBox returns false if needle is empty
            || !$needle
        }
    }

    #| Returns true if the string starts with the argument.
    #|{
        my @urls = <http://github.com ftp://ftp.FreeBSD.org>;
        for @urls {
            print "$_ is:";
            when .starts-with('http') { say "hyper text transfer" }
            when .starts-with('ftp')  { say "file transfer" }
            default { "well I'm not sure.." }
        }
    }
    method starts-with(#|[True if the string starts-with this]$starts-with)? {
        $self.${
            awk :T($starts-with) -v 'RS=^$'
            Q⟪{exit(index($0,ENVIRON["T"])!=1)}⟫
        }
    }

    #| Returns true if the string ends with the argument.
    #|{
        my @urls = <github.com ftp://ftp.FreeBSD.org>;
        for @urls {
            print "$_ might be: ";
            when .ends-with('.com') { say 'commercial' }
            when .ends-with('.org') { say 'an organisation' }
            when .ends-with('.io')  { say 'a moon of Jupiter' }
        }
    }
    method ends-with(#|[True if the string ends-with this]$ends-with)? {
        $self.${
            awk :T($ends-with) -v 'RS=^$' Q⟪{
                T=ENVIRON["T"]
                while (i = index(substr($0,w + 1),T)){
                    if(i = length($0)-length(T)+1)
                      exit 0
                    w = i
                }
                exit 1
            }⟫
        }
    }

    #| Returns true if the the string matches the regex and sets the
    #| `@/` match variable to the match and its capture groups (one per line).
    #|{
        my $regex = rx‘^(.+)://([^/]+)/?(.*)$’;
        if 'https://github.com/spitsh/spitsh'.match($regex) {
            say @/[0]; #-> https://github.com/spitsh/spitsh
            say @/[1]; #-> https
            say @/[2]; #-> github.com
            say @/[3]; #-> spitsh/spitsh
        }
    }
    method match(#|[The regular expression to match against]Regex $r)? is impure on {
        RHEL {
            @/ = $self.${
                awk :$r
                    # gawk match lets you pass an array which will get filled with
                    # all the matches
                    Q⟪{
                        if (match($0,ENVIRON["r"],a)){
                            i=0; while(a[i,"start"]) print a[i++]
                        } else {
                            exit 1;
                        }
                    }⟫
            };
            $?;
        }
        GNU {
            @/ = '';
            # note: GNU doesn't mean it has gawk
            if $self.matches($r) {
                @/ = {
                    my $i = 0;
                    $i++ while $self.${
                        sed -nr !>X
                        (
                            # slurp the input
                            'H;1h;$!d;x;' ~
                            # surround match with 0x1c
                            "s§$r§\\$i\\n§;" ~
                            # remove delims and print
                            's/^[^]*|[^]*$//gp'
                        )
                    };
                    ();
                };
                $?;
            }
        }
        BusyBox {
            @/ = "";
            if $self.matches($r) {
                @/ = {
                    my $i = 0;
                    $i++ while $i < 10 and $self.${
                        awk :$r -v 'RS=^$' -v "i=$i" q⟪
                            {
                                $0 = gensub(ENVIRON["r"], "§\\\\"i"§",1);
                                print gensub(/^[^§]*§|§[^§]*$/,"","G");
                            }⟫
                    };
                    ();
                }
                True;
            }
        }
    }

    #| Returns true if the string matches the regex and **doesn't**
    #| set or modify `@/` match variable.
    #|{
        my $regex = rx‘^(.+)://([^/]+)/?(.*)$’;
        my $url = 'https://github.com/spitsh/spitsh';
        if $url.match($regex) {
            my $host = @/[2];
            if $host.matches(/(www\.)?github.com/) {
                # @/ is preserved.
                my @user-repo = @/[3].split('/');
                say "The owner is @user-repo[0]. The repo is @user-repo[1]";
            } else {
                say "it's not github";
            }
        }
    }
    method matches(Regex $r)? on {
        Debian {
            # because mawk is the worst
            $self.${perl -e '(join "",<STDIN>) =~ /@ARGV[0]/s or exit 1' $r}
        }
        Any {
            $self.${
                awk :$r -v 'RS=^$'
                'END{if(!match($0,ENVIRON["r"])){ exit 1 }}'
            }
        }
    }

    method write-to(File $to)^ {
        $self.${awk -v "f=$to" -v 'RS=^$' '{printf "%s",$0>f;printf "%s",$0}'}
    }

    method append-to(File $to)^ {
        $self.${tee -a $to}
    }

    method extract-->File {
        $self.${ tar xvz | sed -n '1s/\/.*//p' };
    }
    method gist~ { $self }

    #| Returns True if the invocant and argument string are equal.
    method ACCEPTS($b)? { $b eq $self }

    method JSON {
        $self.${
            awk -v 'RS=^$' Q⟪
                END {
                    e="\\\\-\\\\-\n-\\n-\\\"-\\\"-\t-\\t-\\/-\\/-\r-\\r-\f-\\f-\b-\\b"
                    split(e,a,"-");
                    for (i = 1; i < 19; i += 2){
                        gsub(a[i],a[i+1])
                    }
                    printf "%s", "\"" $0 "\""
                }⟫
        }
    }

    # has to covert $1 to \1 and \$ to just $
    # XXX: WIP
    method subst-re(Regex $target, $replacement, Bool :$g, Bool :$i)~ on {
        Debian {
            $self.${
                perl -pe
                (
                    ‘use utf8;BEGIN{$/=undef};($t,$r)=(shift,shift);s/$t/$r/see’ ~
                    ($g && "g") ~ ($i && "i")
                )
                '--' '-' $target "qq§$replacement§"
            }
        }
        Any {
            $self.${
                awk -v 'RS=^$' -v "g={$g ?? 'g' !! '1'}" -v "IGNORECASE=$i"
                :T($target) :R($replacement) Q⟪
                END {
                    R = gensub(/([^\\]|^)((\\{2})*)\$([0-9&])/, "\\1\\2\\\\\\4", "g", ENVIRON["R"]);
                    R = gensub(/((\\*)\\)?\\[$]/,"\\2$","g",R);
                    R = gensub(ENVIRON["T"], R, g);
                    printf "%s", R
                }⟫
            }
        }
    }

    method substr(Int $start, Int $length?)~ {
        $self.${
            awk -v 'RS=^$' -v "s={$start + 1}" -v "l=$length"
            (
                '{printf "%s", substr($0,s' ~
                (~$length && ",l") ~
                ')}'
            )
        }
    }

    static method random($n = 10)~ {
        ${ tr -dc 'a-zA-Z0-9' < '/dev/urandom' | head -c $n }
    }

    method capture(Regex $regex)~ {
        $self.${sed -rn "s§$regex§\\1§;T;s/^.*//p;q"}
    }
}

constant $:sed-delimiter = "\x[1e]";

#| The match list variable. Like `$/` in Perl 6 it stores the what was
#| match after a something is matched against a regex.
#|{
    my $text = "The file is: foo.txt";
    $text.match(/:\s*(.+)\.(.+)$/);
    say @/[0]; #-> : foo.txt
    say @/[1]; #-> foo
    say @/[2]; #-> txt
}
my @/;
#| The Str class is the base primitive class of Spit-sh. It represents
#| a string in the shell. Since all shell constructs are made out of
#| strings all types inherits from this class.
#|{
   say "foo" ~~ Str; # true
   say <one two three> ~~ Str; #true
}
augment Str {
    #| Writes string to `$:OUT` followed by a newline if it doesn't
    #| end in one already.
    method say { $self.${awk '{print}' >$:OUT} }
    #| Writes string to $:ERR followed by a newline if it doesn't end
    #| in one already.
    method note { $self.${awk '{print}' >$:ERR} }
    #| Writes string to `$:OUT`.
    method print { $self.${ >$:OUT } }
    #| Returns an uppercase version of the string
    method uc~    { $self.${tr '[:lower:]' '[:upper:]'} }
    #| Returns an lowercase version of the string
    method lc~    { $self.${tr '[:upper:]' '[:lower:]'} }
    #| Returns the number of characters in the string. **note:** This
    #| will depend on the locale of the terminal the script is running in.
    method chars+ { $self.${wc -m} }
    #| Returns the number of bytes in the string.
    method bytes+ { $self.${wc -c} }

    method lines+ { $self.${wc -l} }

    #| Splits the string on a separator. Returns the string with each
    #| instance of the `$sep` replaced with `\n` as a [List].
    method split(#|[The separator to split on]$sep)@ {
        $self.${
          awk -v "FS=$sep" '{l = split($0,a); for (i = 0; i < l;) print a[++i]}'
         }
    }
    #| Returns true if the string isn't empty
    method Bool { ${test $self} }

    #| Returns the string with the target string replaced by a replacement string.
    #| Does not modify the original string.
    #|{
       my $a = "food";
       $a.subst('o','e').say;
       $a.subst('o','e',:g).say;
       say $a;
    }
    method subst(#|[The string to be replaced]$target,
                  #|[The string to replace it with]$replacement,
                  #|[Turns on global matching]Bool :$g)~ {

        $self.${
            awk -v "g=$g" -v 'RS=^$' :T($target) :R($replacement)
            Q⟪
            {
                r=ENVIRON["R"]; t=ENVIRON["T"]
                while((g || !i) && (i = index(substr($0,w + 1),t))){
                    $0 = substr($0,1,i + w - 1) r substr($0,w + i + length(t));
                    w += length(r) - length(t) + i
                    if(w < 0){ w = 0 }
                }
                print;
            }⟫
        }
    }

    method subst-eval($placeholder,$value)~ {
        $self.subst($placeholder,$value.subst("'","'\\''"));
    }
    #| Returns true if the string contains `$needle`.
    #|{
       say "Hello, World".contains('Wo'); #-> True
       say "Hello, World".contains('wo'); #-> False
       say "Hello, World".contains('wo',:i); #-> True
    }
    method contains(#|[The string being searched for]$needle,
                     #|[Turns on case insensitive matching]Bool :$i)? on {
        Any {
            $self.${awk -v 'RS=^$' -v "t=$needle" -v "i=$i"
                    'END{exit(!index(i ? tolower($0) :$0, i ? tolower(t) : t))}'}
        }
        BusyBox {
            $self.${awk -v 'RS=^$' -v "t=$needle" -v "i=$i"
                    'END{exit(!index(i ? tolower($0) :$0, i ? tolower(t) : t))}'}
            # BusyBox returns false if needle is empty
            || !$needle
        }
    }

    #| Returns true if the string starts with the argument.
    #|{
        my @urls = <http://github.com ftp://ftp.FreeBSD.org>;
        for @urls {
            print "$_ is:";
            when .starts-with('http') { say "hyper text transfer" }
            when .starts-with('ftp')  { say "file transfer" }
            default { "well I'm not sure.." }
        }
    }
    method starts-with(#|[True if the string starts-with this]$starts-with)? {
        $self.${
            awk :T($starts-with) -v 'RS=^$'
            Q⟪{exit(index($0,ENVIRON["T"])!=1)}⟫
        }
    }

    #| Returns true if the string ends with the argument.
    #|{
        my @urls = <github.com ftp://ftp.FreeBSD.org>;
        for @urls {
            print "$_ might be: ";
            when .ends-with('.com') { say 'commercial' }
            when .ends-with('.org') { say 'an organisation' }
            when .ends-with('.io')  { say 'a moon of Jupiter' }
        }
    }
    method ends-with(#|[True if the string ends-with this]$ends-with)? {
        $self.${
            awk :T($ends-with) -v 'RS=^$' Q⟪{
                T=ENVIRON["T"]
                while (i = index(substr($0,w + 1),T)){
                    if(i = length($0)-length(T)+1)
                      exit 0
                    w = i
                }
                exit 1
            }⟫
        }
    }

    #| Returns true if the the string matches the regex and sets the
    #| `@/` match variable to the match and its capture groups (one per line).
    #|{
        my $regex = rx‘^(.+)://([^/]+)/?(.*)$’;
        if 'https://github.com/spitsh/spitsh'.match($regex) {
            say @/[0]; #-> https://github.com/spitsh/spitsh
            say @/[1]; #-> https
            say @/[2]; #-> github.com
            say @/[3]; #-> spitsh/spitsh
        }
    }
    method match(#|[The regular expression to match against]Regex $r)? is impure on {
        RHEL {
            @/ = $self.${
                awk :$r
                    # gawk match lets you pass an array which will get filled with
                    # all the matches
                    Q⟪{
                        if (match($0,ENVIRON["r"],a)){
                            i=0; while(a[i,"start"]) print a[i++]
                        } else {
                            exit 1;
                        }
                    }⟫
            };
            $?;
        }
        GNU {
            @/ = '';
            # note: GNU doesn't mean it has gawk
            if $self.matches($r) {
                @/ = {
                    my $i = 0;
                    $i++ while $self.${
                        sed -nr !>X
                        (
                            # slurp the input
                            'H;1h;$!d;x;' ~
                            # surround match with 0x1c
                            "s§$r§\\$i\\n§;" ~
                            # remove delims and print
                            's/^[^]*|[^]*$//gp'
                        )
                    };
                    ();
                };
                $?;
            }
        }
        BusyBox {
            @/ = "";
            if $self.matches($r) {
                @/ = {
                    my $i = 0;
                    $i++ while $i < 10 and $self.${
                        awk :$r -v 'RS=^$' -v "i=$i" q⟪
                            {
                                $0 = gensub(ENVIRON["r"], "§\\\\"i"§",1);
                                print gensub(/^[^§]*§|§[^§]*$/,"","G");
                            }⟫
                    };
                    ();
                }
                True;
            }
        }
    }

    #| Returns true if the string matches the regex and **doesn't**
    #| set or modify `@/` match variable.
    #|{
        my $regex = rx‘^(.+)://([^/]+)/?(.*)$’;
        my $url = 'https://github.com/spitsh/spitsh';
        if $url.match($regex) {
            my $host = @/[2];
            if $host.matches(/(www\.)?github.com/) {
                # @/ is preserved.
                my @user-repo = @/[3].split('/');
                say "The owner is @user-repo[0]. The repo is @user-repo[1]";
            } else {
                say "it's not github";
            }
        }
    }
    method matches(Regex $r)? on {
        Debian {
            # because mawk is the worst
            $self.${perl -e '(join "",<STDIN>) =~ /@ARGV[0]/s or exit 1' $r}
        }
        Any {
            $self.${
                awk :$r -v 'RS=^$'
                'END{if(!match($0,ENVIRON["r"])){ exit 1 }}'
            }
        }
    }

    method write-to(File $to)^ {
        $self.${awk -v "f=$to" -v 'RS=^$' '{printf "%s",$0>f;printf "%s",$0}'}
    }

    method append-to(File $to)^ {
        $self.${tee -a $to}
    }

    method extract-->File {
        $self.${ tar xvz | sed -n '1s/\/.*//p' };
    }
    method gist~ { $self }

    #| Returns True if the invocant and argument string are equal.
    method ACCEPTS($b)? { $b eq $self }

    method JSON {
        $self.${
            awk -v 'RS=^$' Q⟪
                END {
                    e="\\\\-\\\\-\n-\\n-\\\"-\\\"-\t-\\t-\\/-\\/-\r-\\r-\f-\\f-\b-\\b"
                    split(e,a,"-");
                    for (i = 1; i < 19; i += 2){
                        gsub(a[i],a[i+1])
                    }
                    printf "%s", "\"" $0 "\""
                }⟫
        }
    }

    # has to covert $1 to \1 and \$ to just $
    # XXX: WIP
    method subst-re(Regex $target, $replacement, Bool :$g, Bool :$i)~ on {
        Debian {
            $self.${
                perl -pe
                (
                    ‘use utf8;BEGIN{$/=undef};($t,$r)=(shift,shift);s/$t/$r/see’ ~
                    ($g && "g") ~ ($i && "i")
                )
                '--' '-' $target "qq\c[GHOST]$replacement\c[GHOST]"
            }
        }
        Any {
            $self.${
                awk -v 'RS=^$' -v "g={$g ?? 'g' !! '1'}" -v "IGNORECASE=$i"
                :T($target) :R($replacement) Q⟪
                END {
                    R = gensub(/([^\\]|^)((\\{2})*)\$([0-9&])/, "\\1\\2\\\\\\4", "g", ENVIRON["R"]);
                    R = gensub(/((\\*)\\)?\\[$]/,"\\2$","g",R);
                    R = gensub(ENVIRON["T"], R, g);
                    printf "%s", R
                }⟫
            }
        }
    }

    method substr(Int $start, Int $length?)~ {
        $self.${
            awk -v 'RS=^$' -v "s={$start + 1}" -v "l=$length"
            (
                '{printf "%s", substr($0,s' ~
                (~$length && ",l") ~
                ')}'
            )
        }
    }

    static method random($n = 10)~ {
        ${ tr -dc 'a-zA-Z0-9' < '/dev/urandom' | head -c $n }
    }

    method capture(Regex $regex)~ {
        $self.${sed -rn "s§$regex§\\1§;T;s/^.*//p;q"}
    }
}


Actually save file before commiting

Actually save file before commiting

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

#pragma semicolon 1

#include <sourcemod>
#include <_your_plugin_>

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

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

new Handle:g_Cvar_Enabled = INVALID_HANDLE;

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

    RegPluginLibrary("_plugin_name_");

    return APLRes_Success;
}

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

    CreateConVar("sm__plugin_name__version", PLUGIN_VERSION, PLUGIN_NAME, FCVAR_PLUGIN | FCVAR_SPONLY | FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DONTRECORD);
    g_Cvar_Enabled = CreateConVar("sm__plugin_name__enabled", "1", "Enabled");

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

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

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

#pragma semicolon 1

#include <sourcemod>
#include <_your_plugin_>

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

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

new Handle:g_Cvar_Enabled = INVALID_HANDLE;

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

    RegPluginLibrary("_plugin_name_");

    return APLRes_Success;
}

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

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

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

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


Make simple1.sp conform sp syntax

Make simple1.sp conform sp syntax

Every if needs an else in the official grammar.

int a; int b;
int[3] c; int foo;

int bar;

a = 1;
b = 2;
c[0] = a; c[1] = b; c[2] = 23;
foo = 42;
bar = c[2];

if (c = 2) {
	print(c);
} else {
	read(b);
}

a = 0;
while (a < 10) {
	print(a);
	a = a + 1;
}

int a; int b;
int[3] c; int foo;

int bar;

a = 1;
b = 2;
c[0] = a; c[1] = b; c[2] = 23;
foo = 42;
bar = c[2];

if (c = 2) {
	print(c);
} else if (foo <!> bar) {
	read(b);
}

a = 0;
while (a < 10) {
	print(a);
	a = a + 1;
}


Added fake client check

Added fake client check

Forgot to add one when sending the default sv_downloadurl value back.

/**
 * Download Preferences
 * Author(s): nosoop
 * File:  downloadprefs.sp
 * Description:	 Allows clients to select categories of files to download.
 * License:	 MIT License
 */

#pragma semicolon 1
#include <sourcemod>

#define PLUGIN_VERSION			"0.4.1"

public Plugin:myinfo = {
	name = "Download Preferences",
	author = "nosoop",
	description = "Client download preferences",
	version = PLUGIN_VERSION,
	url = "http://github.com/nosoop/SM-DownloadPrefs"
}

#define DATABASE_NAME			"downloadprefs" // Database name in config
new Handle:g_hDatabase = INVALID_HANDLE;

new Handle:g_hCDownloadURL = INVALID_HANDLE, // sv_downloadurl
	Handle:g_hCDPrefURL = INVALID_HANDLE; // sm_dprefs_downloadurl

public OnPluginStart() {
	g_hCDownloadURL = FindConVar("sv_downloadurl");

	CreateConVar("sm_dprefs_version", PLUGIN_VERSION, _, FCVAR_PLUGIN | FCVAR_NOTIFY);

	// Set redirect downloadurl.  If blank, the transmission of sv_downloadurl to the client is not changed.
	g_hCDPrefURL = CreateConVar("sm_dprefs_downloadurl", "", "Download URL to send to the client.  See README for details.", FCVAR_PLUGIN | FCVAR_SPONLY);

	AutoExecConfig(true);
}

public OnPluginEnd() {
	CloseHandle(g_hDatabase);
}

public APLRes:AskPluginLoad2(Handle:hMySelf, bool:bLate, String:strError[], iMaxErrors) {
	RegPluginLibrary("downloadprefs");

	CreateNative("RegClientDownloadCategory", Native_RegClientDownloadCategory);
	CreateNative("RegClientDownloadFile", Native_RegClientDownloadFile);
	CreateNative("SetClientDownloadPreference", Native_SetClientDownloadPreference);
	CreateNative("GetClientDownloadPreference", Native_GetClientDownloadPreference);
	CreateNative("ClientHasDownloadPreference", Native_ClientHasDownloadPreference);

	g_hDatabase = GetDatabase();

	return APLRes_Success;
}

/**
 * Sends a custom download URL when the client is connecting.
 * This occurs every time the client reconnects (via map change, etc.)
 */
public OnClientAuthorized(client, const String:auth[]) {
	if (!IsFakeClient(client)) {
		SendCustomDownloadURL(client);
	}
}

SendCustomDownloadURL(client) {
	new iSteamID3 = GetSteamAccountID(client);

	// downloadurl has no trailing slash
	new String:sClientDownloadURL[256];
	GetConVarString(g_hCDPrefURL, sClientDownloadURL, sizeof(sClientDownloadURL));

	if (strlen(sClientDownloadURL) > 0) {
		new String:sSteamID3[32];
		IntToString(iSteamID3, sSteamID3, sizeof(sSteamID3));

		// Replace $STEAMID with client's SteamID3 and transmit to client
		ReplaceString(sClientDownloadURL, sizeof(sClientDownloadURL), "$STEAMID", sSteamID3);

		SendConVarValue(client, g_hCDownloadURL, sClientDownloadURL);
	}
}

/**
 * Resets the download URL to the server-supplied value once the client is fully in-game.
 * This occurs after the client has performed any fast download requests.
 */
public OnClientPostAdminCheck(client) {
	if (!IsFakeClient(client)) {
		new String:sDefaultDownloadURL[256];
		GetConVarString(g_hCDownloadURL, sDefaultDownloadURL, sizeof(sDefaultDownloadURL));
		SendConVarValue(client, g_hCDownloadURL, sDefaultDownloadURL);
	}
}

/**
 * Registers a group of files to download.
 * Store the category plus description into the database if nonexistent,
 * returning an ID to the corresponding category.
 *
 * @param category			The category name to register.
 * @param description		The description associated with the category.
 * @param enabled			The file group is downloaded by default; clients must choose to opt-out.
 *
 * @return categoryid		The ID of the category.
 */
_:RegClientDownloadCategory(const String:category[], const String:description[], bool:enabled = true) {
	new Handle:hQuery = INVALID_HANDLE, iCategoryID;
	decl String:sQuery[1024];

	Format(sQuery, sizeof(sQuery),
			"SELECT categoryid FROM categories WHERE categoryname='%s'",
			category);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	if (SQL_GetRowCount(hQuery) == 0) {
		Format(sQuery, sizeof(sQuery),
				"INSERT OR REPLACE INTO categories (categoryid, categoryname, categorydesc, enabled) VALUES (NULL, '%s', '%s', '%b')",
				category, description, enabled);
		SQL_FastQuery(g_hDatabase, sQuery);
	}
	CloseHandle(hQuery);

	Format(sQuery, sizeof(sQuery),
			"SELECT categoryid FROM categories WHERE categoryname='%s'",
			category);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	SQL_FetchRow(hQuery);
	iCategoryID = SQL_FetchInt(hQuery, 0);

	CloseHandle(hQuery);

	return iCategoryID;
}

public Native_RegClientDownloadCategory(Handle:hPlugin, nParams) {
	decl String:category[512], String:description[512];

	GetNativeString(1, category, sizeof(category));
	GetNativeString(2, description, sizeof(description));
	new bool:bDefault = GetNativeCell(3);

	return RegClientDownloadCategory(category, description, bDefault);
}

/**
 * Registers a file to a category.
 *
 * @param categoryid		The ID of the category to register.
 * @param filepath			The full path of the file to download.
 */
RegClientDownloadFile(categoryid, const String:filepath[]) {
	decl String:sQuery[1024];
	Format(sQuery, sizeof(sQuery),
			"INSERT OR REPLACE INTO files (categoryid, filepath) VALUES (%d, '%s')",
			categoryid, filepath);
	SQL_FastQuery(g_hDatabase, sQuery);
}

public Native_RegClientDownloadFile(Handle:hPlugin, nParams) {
	new categoryid = GetNativeCell(1);

	decl String:filepath[512];
	GetNativeString(2, filepath, sizeof(filepath));

	RegClientDownloadFile(categoryid, filepath);
}

/**
 * Stores the client's download preference.
 *
 * @param client			The client to set preferences for.
 * @param categoryid		The category of files to set a download preference for.
 * @param download			Whether or not the client downloads this file.
 */
SetClientDownloadPreference(client, categoryid, bool:enabled) {
	decl String:sQuery[1024];

	new sid3 = GetSteamAccountID(client);

	if (ClientHasDownloadPreference(client, categoryid)) {
		// Update existing entry.
		Format(sQuery, sizeof(sQuery),
				"UPDATE downloadprefs SET enabled = %d WHERE sid3 = %d AND categoryid = %d",
				_:enabled, sid3, categoryid);
	} else {
		// Create new entry.
		Format(sQuery, sizeof(sQuery),
				"INSERT INTO downloadprefs (sid3, categoryid, enabled) VALUES (%d, %d, %d)",
				sid3, categoryid, _:enabled);
	}
	SQL_FastQuery(g_hDatabase, sQuery);
}

public Native_SetClientDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1),
		categoryid = GetNativeCell(2),
		bool:enabled = GetNativeCell(3);

	SetClientDownloadPreference(client, categoryid, enabled);
}

/**
 * Retrieves a client's download preference.  If non-existent, will return the default setting.
 * A client will keep their existing download preference until a map change or reconnect.
 *
 * @param client			The client to get preferences for.
 * @param categoryid		The category of files to get a download preference for.
 *
 * @return					Boolean determining if a client allows downloads from this category,
 *							or the default allow value if the client has not set their own.
 */
bool:GetClientDownloadPreference(client, categoryid) {
	decl String:sQuery[1024];
	new bool:bEnabled;

	if (!ClientHasDownloadPreference(client, categoryid, bEnabled)) {
		Format(sQuery, sizeof(sQuery),
				"SELECT enabled FROM categories WHERE categoryid=%d",
				categoryid);
		bEnabled = bool:SQL_QuerySingleRowInt(g_hDatabase, sQuery);
	}

	return bEnabled;
}

public Native_GetClientDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1),
		categoryid = GetNativeCell(2);

	return GetClientDownloadPreference(client, categoryid);
}

/**
 * Checks whether or not the client has their own download preference set.
 *
 * @param client			The client to check preferences for.
 * @param categoryid		The category of files to check for download preferences.
 * @param value				A cell reference to store an existing preference value.
 *
 * @return					Boolean determining if a custom download preference is set.
 *							(False if using the default preference set by the download category.)
 */
bool:ClientHasDownloadPreference(client, categoryid, &any:result = 0) {
	new sid3 = GetSteamAccountID(client);
	decl String:sQuery[1024];
	new Handle:hQuery = INVALID_HANDLE;
	new bool:bHasRows;

	Format(sQuery, sizeof(sQuery),
			"SELECT enabled FROM downloadprefs WHERE sid3=%d AND categoryid=%d",
			sid3, categoryid);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	bHasRows = (SQL_GetRowCount(hQuery) > 0);

	if (bHasRows) {
		SQL_FetchRow(hQuery);
		result = bool:SQL_FetchInt(hQuery, 0);
	}

	CloseHandle(hQuery);

	return bHasRows;
}

public Native_ClientHasDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1),
		categoryid = GetNativeCell(2),
		result = GetNativeCellRef(3);

	return ClientHasDownloadPreference(client, categoryid, result);
}

/**
 * Runs a query, returning the selected integer from the first row.
 */
_:SQL_QuerySingleRowInt(Handle:database, const String:query[]) {
	new result;
	new Handle:hQuery = SQL_Query(database, query);

	result = SQL_FetchInt(hQuery, 0);
	CloseHandle(hQuery);

	return result;
}

/**
 * Reads the database configuration.
 */
Handle:GetDatabase() {
	new Handle:hDatabase = INVALID_HANDLE;

	if (SQL_CheckConfig(DATABASE_NAME)) {
		decl String:sErrorBuffer[256];
		if ( (hDatabase = SQL_Connect(DATABASE_NAME, true, sErrorBuffer, sizeof(sErrorBuffer))) == INVALID_HANDLE ) {
			SetFailState("[downloadprefs] Could not connect to database: %s", sErrorBuffer);
		} else {
			return hDatabase;
		}
	} else {
		SetFailState("[downloadprefs] Could not find configuration %s to load database.", DATABASE_NAME);
	}
	return INVALID_HANDLE;
}

/**
 * Tables to create:
 *
 * CREATE TABLE 'categories' ('categoryid' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 'categoryname' TEXT NOT NULL, 'categorydesc' TEXT, 'enabled' BOOLEAN NOT NULL);
 * CREATE TABLE 'downloadprefs' ('sid3' INTEGER NOT NULL, 'categoryid' INTEGER NOT NULL, 'enabled' BOOLEAN NOT NULL);
 * CREATE TABLE 'files' ('categoryid' INTEGER, 'filepath' TEXT PRIMARY KEY NOT NULL);
 */

 /**
  * Flow to check if a download is allowed.
  * When a client is authorized: -> Send custom virtual fastdl directory data
  * When a client downloads a file: ->
  *	  - Get SteamID3 from query string
  *	  - Get 'categoryid' by 'filepath' in table files (filepath obtained in query string),
  *	  - Get 'enabled' by 'sid3' and 'category' in downloadprefs,
  *	  - (If not found, get 'enabled' by 'categoryid' in table categories),
  *	  - If enabled, allow the download, otherwise redirect / 404.
  */

/**
 * Download Preferences
 * Author(s): nosoop
 * File:  downloadprefs.sp
 * Description:	 Allows clients to select categories of files to download.
 * License:	 MIT License
 */

#pragma semicolon 1
#include <sourcemod>

#define PLUGIN_VERSION			"0.4.0"

public Plugin:myinfo = {
	name = "Download Preferences",
	author = "nosoop",
	description = "Client download preferences",
	version = PLUGIN_VERSION,
	url = "http://github.com/nosoop/SM-DownloadPrefs"
}

#define DATABASE_NAME			"downloadprefs" // Database name in config
new Handle:g_hDatabase = INVALID_HANDLE;

new Handle:g_hCDownloadURL = INVALID_HANDLE, // sv_downloadurl
	Handle:g_hCDPrefURL = INVALID_HANDLE; // sm_dprefs_downloadurl

public OnPluginStart() {
	g_hCDownloadURL = FindConVar("sv_downloadurl");

	CreateConVar("sm_dprefs_version", PLUGIN_VERSION, _, FCVAR_PLUGIN | FCVAR_NOTIFY);

	// Set redirect downloadurl.  If blank, the transmission of sv_downloadurl to the client is not changed.
	g_hCDPrefURL = CreateConVar("sm_dprefs_downloadurl", "", "Download URL to send to the client.  See README for details.", FCVAR_PLUGIN | FCVAR_SPONLY);

	AutoExecConfig(true);
}

public OnPluginEnd() {
	CloseHandle(g_hDatabase);
}

public APLRes:AskPluginLoad2(Handle:hMySelf, bool:bLate, String:strError[], iMaxErrors) {
	RegPluginLibrary("downloadprefs");

	CreateNative("RegClientDownloadCategory", Native_RegClientDownloadCategory);
	CreateNative("RegClientDownloadFile", Native_RegClientDownloadFile);
	CreateNative("SetClientDownloadPreference", Native_SetClientDownloadPreference);
	CreateNative("GetClientDownloadPreference", Native_GetClientDownloadPreference);
	CreateNative("ClientHasDownloadPreference", Native_ClientHasDownloadPreference);

	g_hDatabase = GetDatabase();

	return APLRes_Success;
}

/**
 * Sends a custom download URL when the client is connecting.
 * This occurs every time the client reconnects (via map change, etc.)
 */
public OnClientAuthorized(client, const String:auth[]) {
	if (!IsFakeClient(client)) {
		SendCustomDownloadURL(client);
	}
}

SendCustomDownloadURL(client) {
	new iSteamID3 = GetSteamAccountID(client);

	// downloadurl has no trailing slash
	new String:sClientDownloadURL[256];
	GetConVarString(g_hCDPrefURL, sClientDownloadURL, sizeof(sClientDownloadURL));

	if (strlen(sClientDownloadURL) > 0) {
		new String:sSteamID3[32];
		IntToString(iSteamID3, sSteamID3, sizeof(sSteamID3));

		// Replace $STEAMID with client's SteamID3 and transmit to client
		ReplaceString(sClientDownloadURL, sizeof(sClientDownloadURL), "$STEAMID", sSteamID3);

		SendConVarValue(client, g_hCDownloadURL, sClientDownloadURL);
	}
}

/**
 * Resets the download URL to the server-supplied value once the client is fully in-game.
 * This occurs after the client has performed any fast download requests.
 */
public OnClientPostAdminCheck(client) {
	new String:sDefaultDownloadURL[256];
	GetConVarString(g_hCDownloadURL, sDefaultDownloadURL, sizeof(sDefaultDownloadURL));
	SendConVarValue(client, g_hCDownloadURL, sDefaultDownloadURL);
}

/**
 * Registers a group of files to download.
 * Store the category plus description into the database if nonexistent,
 * returning an ID to the corresponding category.
 *
 * @param category			The category name to register.
 * @param description		The description associated with the category.
 * @param enabled			The file group is downloaded by default; clients must choose to opt-out.
 *
 * @return categoryid		The ID of the category.
 */
_:RegClientDownloadCategory(const String:category[], const String:description[], bool:enabled = true) {
	new Handle:hQuery = INVALID_HANDLE, iCategoryID;
	decl String:sQuery[1024];

	Format(sQuery, sizeof(sQuery),
			"SELECT categoryid FROM categories WHERE categoryname='%s'",
			category);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	if (SQL_GetRowCount(hQuery) == 0) {
		Format(sQuery, sizeof(sQuery),
				"INSERT OR REPLACE INTO categories (categoryid, categoryname, categorydesc, enabled) VALUES (NULL, '%s', '%s', '%b')",
				category, description, enabled);
		SQL_FastQuery(g_hDatabase, sQuery);
	}
	CloseHandle(hQuery);

	Format(sQuery, sizeof(sQuery),
			"SELECT categoryid FROM categories WHERE categoryname='%s'",
			category);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	SQL_FetchRow(hQuery);
	iCategoryID = SQL_FetchInt(hQuery, 0);

	CloseHandle(hQuery);

	return iCategoryID;
}

public Native_RegClientDownloadCategory(Handle:hPlugin, nParams) {
	decl String:category[512], String:description[512];

	GetNativeString(1, category, sizeof(category));
	GetNativeString(2, description, sizeof(description));
	new bool:bDefault = GetNativeCell(3);

	return RegClientDownloadCategory(category, description, bDefault);
}

/**
 * Registers a file to a category.
 *
 * @param categoryid		The ID of the category to register.
 * @param filepath			The full path of the file to download.
 */
RegClientDownloadFile(categoryid, const String:filepath[]) {
	decl String:sQuery[1024];
	Format(sQuery, sizeof(sQuery),
			"INSERT OR REPLACE INTO files (categoryid, filepath) VALUES (%d, '%s')",
			categoryid, filepath);
	SQL_FastQuery(g_hDatabase, sQuery);
}

public Native_RegClientDownloadFile(Handle:hPlugin, nParams) {
	new categoryid = GetNativeCell(1);

	decl String:filepath[512];
	GetNativeString(2, filepath, sizeof(filepath));

	RegClientDownloadFile(categoryid, filepath);
}

/**
 * Stores the client's download preference.
 *
 * @param client			The client to set preferences for.
 * @param categoryid		The category of files to set a download preference for.
 * @param download			Whether or not the client downloads this file.
 */
SetClientDownloadPreference(client, categoryid, bool:enabled) {
	decl String:sQuery[1024];

	new sid3 = GetSteamAccountID(client);

	if (ClientHasDownloadPreference(client, categoryid)) {
		// Update existing entry.
		Format(sQuery, sizeof(sQuery),
				"UPDATE downloadprefs SET enabled = %d WHERE sid3 = %d AND categoryid = %d",
				_:enabled, sid3, categoryid);
	} else {
		// Create new entry.
		Format(sQuery, sizeof(sQuery),
				"INSERT INTO downloadprefs (sid3, categoryid, enabled) VALUES (%d, %d, %d)",
				sid3, categoryid, _:enabled);
	}
	SQL_FastQuery(g_hDatabase, sQuery);
}

public Native_SetClientDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1),
		categoryid = GetNativeCell(2),
		bool:enabled = GetNativeCell(3);

	SetClientDownloadPreference(client, categoryid, enabled);
}

/**
 * Retrieves a client's download preference.  If non-existent, will return the default setting.
 * A client will keep their existing download preference until a map change or reconnect.
 *
 * @param client			The client to get preferences for.
 * @param categoryid		The category of files to get a download preference for.
 *
 * @return					Boolean determining if a client allows downloads from this category,
 *							or the default allow value if the client has not set their own.
 */
bool:GetClientDownloadPreference(client, categoryid) {
	decl String:sQuery[1024];
	new bool:bEnabled;

	if (!ClientHasDownloadPreference(client, categoryid, bEnabled)) {
		Format(sQuery, sizeof(sQuery),
				"SELECT enabled FROM categories WHERE categoryid=%d",
				categoryid);
		bEnabled = bool:SQL_QuerySingleRowInt(g_hDatabase, sQuery);
	}

	return bEnabled;
}

public Native_GetClientDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1),
		categoryid = GetNativeCell(2);

	return GetClientDownloadPreference(client, categoryid);
}

/**
 * Checks whether or not the client has their own download preference set.
 *
 * @param client			The client to check preferences for.
 * @param categoryid		The category of files to check for download preferences.
 * @param value				A cell reference to store an existing preference value.
 *
 * @return					Boolean determining if a custom download preference is set.
 *							(False if using the default preference set by the download category.)
 */
bool:ClientHasDownloadPreference(client, categoryid, &any:result = 0) {
	new sid3 = GetSteamAccountID(client);
	decl String:sQuery[1024];
	new Handle:hQuery = INVALID_HANDLE;
	new bool:bHasRows;

	Format(sQuery, sizeof(sQuery),
			"SELECT enabled FROM downloadprefs WHERE sid3=%d AND categoryid=%d",
			sid3, categoryid);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	bHasRows = (SQL_GetRowCount(hQuery) > 0);

	if (bHasRows) {
		SQL_FetchRow(hQuery);
		result = bool:SQL_FetchInt(hQuery, 0);
	}

	CloseHandle(hQuery);

	return bHasRows;
}

public Native_ClientHasDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1),
		categoryid = GetNativeCell(2),
		result = GetNativeCellRef(3);

	return ClientHasDownloadPreference(client, categoryid, result);
}

/**
 * Runs a query, returning the selected integer from the first row.
 */
_:SQL_QuerySingleRowInt(Handle:database, const String:query[]) {
	new result;
	new Handle:hQuery = SQL_Query(database, query);

	result = SQL_FetchInt(hQuery, 0);
	CloseHandle(hQuery);

	return result;
}

/**
 * Reads the database configuration.
 */
Handle:GetDatabase() {
	new Handle:hDatabase = INVALID_HANDLE;

	if (SQL_CheckConfig(DATABASE_NAME)) {
		decl String:sErrorBuffer[256];
		if ( (hDatabase = SQL_Connect(DATABASE_NAME, true, sErrorBuffer, sizeof(sErrorBuffer))) == INVALID_HANDLE ) {
			SetFailState("[downloadprefs] Could not connect to database: %s", sErrorBuffer);
		} else {
			return hDatabase;
		}
	} else {
		SetFailState("[downloadprefs] Could not find configuration %s to load database.", DATABASE_NAME);
	}
	return INVALID_HANDLE;
}

/**
 * Tables to create:
 *
 * CREATE TABLE 'categories' ('categoryid' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 'categoryname' TEXT NOT NULL, 'categorydesc' TEXT, 'enabled' BOOLEAN NOT NULL);
 * CREATE TABLE 'downloadprefs' ('sid3' INTEGER NOT NULL, 'categoryid' INTEGER NOT NULL, 'enabled' BOOLEAN NOT NULL);
 * CREATE TABLE 'files' ('categoryid' INTEGER, 'filepath' TEXT PRIMARY KEY NOT NULL);
 */

 /**
  * Flow to check if a download is allowed.
  * When a client is authorized: -> Send custom virtual fastdl directory data
  * When a client downloads a file: ->
  *	  - Get SteamID3 from query string
  *	  - Get 'categoryid' by 'filepath' in table files (filepath obtained in query string),
  *	  - Get 'enabled' by 'sid3' and 'category' in downloadprefs,
  *	  - (If not found, get 'enabled' by 'categoryid' in table categories),
  *	  - If enabled, allow the download, otherwise redirect / 404.
  */


typo fix

typo fix

enum struct Forwards
{
    GlobalForward m_NominationsReset;
    GlobalForward m_MapVoteStarted;
    GlobalForward m_MapVoteEnd;
    GlobalForward m_MapDataLoaded;
    GlobalForward m_MapVotePoolChanged;
    GlobalForward m_NominationsVoted;
    GlobalForward m_OnNominatedMap;
    GlobalForward m_OnNominateMap;
    GlobalForward m_OnNominatePrice;
    GlobalForward m_OnClearMapCooldown;
    GlobalForward m_OnResetMapCooldown;
    GlobalForward m_OnNextMapListCreate;
    GlobalForward m_OnSetNextMap;
}

static Forwards g_Forward;

public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
    if (!CleanPlugin())
    {
        strcopy(error, err_max, "can not clean files.");
        return APLRes_Failure;
    }

    RegPluginLibrary("mapchooser");

    CreateNative("NominateMap",             Native_NominateMap);
    CreateNative("RemoveNominationByMap",   Native_RemoveNominationByMap);
    CreateNative("RemoveNominationByOwner", Native_RemoveNominationByOwner);
    CreateNative("InitiateMapChooserVote",  Native_InitiateVote);
    CreateNative("CanMapChooserStartVote",  Native_CanVoteStart);
    CreateNative("HasEndOfMapVoteFinished", Native_CheckVoteDone);
    CreateNative("GetExcludeMapList",       Native_GetExcludeMapList);
    CreateNative("GetNominatedMapList",     Native_GetNominatedMapList);
    CreateNative("EndOfMapVoteEnabled",     Native_EndOfMapVoteEnabled);
    CreateNative("CanNominate",             Native_CanNominate);
    CreateNative("GetMapData",              Native_GetMapData);
    CreateNative("GetPartyBlockOnwer",      Native_GetPartyBlockOnwer);
    CreateNative("ForceSetNextMap",         Native_ForceSetNextMap);
    CreateNative("SetTierString",           Native_OverrideTierString);
    CreateNative("GetTierString",           Native_GetTierString);

    MarkNativeAsOptional("Store_GetClientCredits");
    MarkNativeAsOptional("Store_SetClientCredits");

    MarkNativeAsOptional("MG_Shop_GetClientMoney");
    MarkNativeAsOptional("MG_Shop_ClientEarnMoney");
    MarkNativeAsOptional("MG_Shop_ClientCostMoney");

    MarkNativeAsOptional("Pupd_CheckPlugin");

    return APLRes_Success;
}

public void OnLibraryAdded(const char[] name)
{
    if (strcmp(name, "store") == 0)
        g_pStore = true;
    else if (strcmp(name, "shop-core") == 0)
        g_pShop = true;
}

public void OnLibraryRemoved(const char[] name)
{
    if (strcmp(name, "store") == 0)
        g_pStore = false;
    else if (strcmp(name, "shop-core") == 0)
        g_pShop = false;
}

void Natives_OnPluginStart()
{
    g_Forward.m_NominationsReset    = new GlobalForward("OnNominationRemoved",    ET_Ignore, Param_String, Param_Cell, Param_Cell);
    g_Forward.m_NominationsVoted    = new GlobalForward("OnNominationVoted",      ET_Ignore, Param_String, Param_String, Param_String);
    g_Forward.m_MapVoteStarted      = new GlobalForward("OnMapVoteStarted",       ET_Ignore);
    g_Forward.m_MapVoteEnd          = new GlobalForward("OnMapVoteEnd",           ET_Ignore, Param_String);
    g_Forward.m_MapDataLoaded       = new GlobalForward("OnMapDataLoaded",        ET_Ignore);
    g_Forward.m_MapVotePoolChanged  = new GlobalForward("OnMapVotePoolChanged",   ET_Ignore);
    g_Forward.m_OnNominateMap       = new GlobalForward("OnNominateMap",          ET_Hook,   Param_String, Param_Cell, Param_Cell);
    g_Forward.m_OnNominatedMap      = new GlobalForward("OnNominatedMap",         ET_Ignore, Param_String, Param_Cell, Param_Cell);
    g_Forward.m_OnNominatePrice     = new GlobalForward("OnNominatePrice",        ET_Hook,   Param_String, Param_Cell, Param_CellByRef, Param_Cell);
    g_Forward.m_OnClearMapCooldown  = new GlobalForward("OnClearMapCooldown",     ET_Hook,   Param_String, Param_Cell);
    g_Forward.m_OnResetMapCooldown  = new GlobalForward("OnResetMapCooldown",     ET_Hook,   Param_String, Param_Cell);
    g_Forward.m_OnNextMapListCreate = new GlobalForward("OnNextMapListCreate",    ET_Hook,   Param_String);
    g_Forward.m_OnSetNextMap        = new GlobalForward("OnSetNextMap",           ET_Hook,   Param_String, Param_Cell);
}

bool Call_OnSetNextMap(const char[] map, int caller)
{
    bool allow = true;
    Call_StartForward(g_Forward.m_OnSetNextMap);
    Call_PushString(map);
    Call_PushCell(caller);
    Call_Finish(allow);
    return allow;
}

bool AllowInNextVotePool(const char[] map)
{
    bool allow = true;
    Call_StartForward(g_Forward.m_OnNextMapListCreate);
    Call_PushString(map);
    Call_Finish(allow);
    return allow;
}

bool Call_OnClearMapCooldown(const char[] map, int _cell)
{
    bool allow = true;
    Call_StartForward(g_Forward.m_OnClearMapCooldown);
    Call_PushString(map);
    Call_PushCell(_cell);
    Call_Finish(allow);
    return allow;
}

bool Call_OnResetMapCooldown(const char[] map, int _cell)
{
    bool allow = true;
    Call_StartForward(g_Forward.m_OnResetMapCooldown);
    Call_PushString(map);
    Call_PushCell(_cell);
    Call_Finish(allow);
    return allow;
}

bool Call_OnNominatePrice(const char[] map, int _cell, int &_ref, bool _bool)
{
    // module not found
    if (!g_pShop && !g_pStore)
        return true;

    bool allow = true;
    Call_StartForward(g_Forward.m_OnNominatePrice);
    Call_PushString(map);
    Call_PushCell(_cell);
    Call_PushCellRef(_ref);
    Call_PushCell(_bool);
    Call_Finish(allow);
    return allow;
}

bool Call_OnNominateMap(const char[] map, int _cell, bool _bool)
{
    bool allow = true;
    Call_StartForward(g_Forward.m_OnNominateMap);
    Call_PushString(map);
    Call_PushCell(_cell);
    Call_PushCell(_bool);
    Call_Finish(allow);
    return allow;
}

void Call_OnNominatedMap(const char[] map, int _cell, bool _bool)
{
    Call_StartForward(g_Forward.m_OnNominatedMap);
    Call_PushString(map);
    Call_PushCell(_cell);
    Call_PushCell(_bool);
    Call_Finish();
}

void Call_NominationsReset(const char[] map, int _cell, bool _bool)
{
    Call_StartForward(g_Forward.m_NominationsReset);
    Call_PushString(map);
    Call_PushCell(_cell);
    Call_PushCell(_bool);
    Call_Finish();
}

void Call_NominationsVoted(const char[] map, const char[] name, const char[] auth)
{
    Call_StartForward(g_Forward.m_NominationsVoted);
    Call_PushString(map);
    Call_PushString(name);
    Call_PushString(auth);
    Call_Finish();
}

void Call_MapVoteStarted()
{
    Call_StartForward(g_Forward.m_MapVoteStarted);
    Call_Finish();
}

void Call_MapVoteEnd(const char[] map)
{
    Call_StartForward(g_Forward.m_MapVoteEnd);
    Call_PushString(map);
    Call_Finish();
}

void Call_MapDataLoaded()
{
    Call_StartForward(g_Forward.m_MapDataLoaded);
    Call_Finish();
}

void Call_MapVotePoolChanged()
{
    Call_StartForward(g_Forward.m_MapVotePoolChanged);
    Call_Finish();
}

public any Native_RemoveNominationByMap(Handle plugin, int numParams)
{
    int len;
    GetNativeStringLength(1, len);

    if (len <= 0)
      return false;

    char[] map = new char[len+1];
    GetNativeString(1, map, len+1);

    return InternalRemoveNominationByMap(map);
}

public any Native_NominateMap(Handle plugin, int numParams)
{
    int len;
    GetNativeStringLength(1, len);

    if (len <= 0)
      return false;

    char[] map = new char[len+1];
    GetNativeString(1, map, len+1);

    return InternalNominateMap(map, GetNativeCell(2), GetNativeCell(3), GetNativeCell(4));
}

public any Native_IsWarningTimer(Handle plugin, int numParams)
{
    return g_bWarningInProgress;
}

public any Native_CanNominate(Handle plugin, int numParams)
{
    if (g_bHasVoteStarted)
        return CanNominate_No_VoteInProgress;

    if (g_bMapVoteCompleted)
        return CanNominate_No_VoteComplete;

    if (g_iNominateCount >= 5)
        return CanNominate_No_VoteFull;

    if (g_bPartyblock)
        return CanNominate_No_PartyBlock;

    return CanNominate_Yes;
}

public any Native_RemoveNominationByOwner(Handle plugin, int numParams)
{
    return InternalRemoveNominationByOwner(GetNativeCell(1));
}

public any Native_InitiateVote(Handle plugin, int numParams)
{
    MapChange when = view_as<MapChange>(GetNativeCell(1));
    ArrayList maps = view_as<ArrayList>(GetNativeCell(2));

    LogAction(-1, -1, "Starting map vote because outside request");

    SetupWarningTimer(WarningType_Vote, when, maps);
}

public any Native_CanVoteStart(Handle plugin, int numParams)
{
    return CanVoteStart();
}

public any Native_CheckVoteDone(Handle plugin, int numParams)
{
    return g_bMapVoteCompleted;
}

public any Native_GetExcludeMapList(Handle plugin, int numParams)
{
    GetCooldownMaps(view_as<ArrayList>(GetNativeCell(1)));
}

public any Native_GetNominatedMapList(Handle plugin, int numParams)
{
    ArrayList m_aNominations = view_as<ArrayList>(GetNativeCell(1));
    for (int i = 0; i < g_aNominations.Length; i++)
    {
        Nominations n;
        g_aNominations.GetArray(i, n, sizeof(Nominations));
        m_aNominations.PushArray(  n, sizeof(Nominations));
    }
}

public any Native_EndOfMapVoteEnabled(Handle plugin, int numParams)
{
    return true;
}

public any Native_GetPartyBlockOnwer(Handle plugin, int numParams)
{
    if (!g_bPartyblock || g_aNominations.Length == 0)
        return -1;

    Nominations n;
    g_aNominations.GetArray(0, n, sizeof(Nominations));

    return n.m_Owner;
}

public any Native_ForceSetNextMap(Handle plugin, int numParams)
{
    if (g_bMapVoteCompleted && !GetNativeCell(2))
        return false;

    int caller = numParams >= 3 ? GetNativeCell(3) : -1;

    char map[128];
    GetNativeString(1, map, 128);

    return InternalSetNextMap(map, caller);
}

public any Native_OverrideTierString(Handle plugin, int numParams)
{
    int tier = GetNativeCell(1);
    if (tier < 0 || tier > MAX_TIER)
        return ThrowNativeError(SP_ERROR_PARAM, "Invalid tier <%d> gived.", tier);

    return GetNativeString(2, g_TierString[tier], 32) == SP_ERROR_NONE;
}

public any Native_GetTierString(Handle plugin, int numParams)
{
    int tier = GetNativeCell(1);
    if (tier < 0 || tier > MAX_TIER)
        return ThrowNativeError(SP_ERROR_PARAM, "Invalid tier <%d> gived.", tier);

    SetNativeString(2, g_TierString[tier], GetNativeCell(3));
    return true;
}

enum struct Forwards
{
    GlobalForward m_NominationsReset;
    GlobalForward m_MapVoteStarted;
    GlobalForward m_MapVoteEnd;
    GlobalForward m_MapDataLoaded;
    GlobalForward m_MapVotePoolChanged;
    GlobalForward m_NominationsVoted;
    GlobalForward m_OnNominatedMap;
    GlobalForward m_OnNominateMap;
    GlobalForward m_OnNominatePrice;
    GlobalForward m_OnClearMapCooldown;
    GlobalForward m_OnResetMapCooldown;
    GlobalForward m_OnNextMapListCreate;
    GlobalForward m_OnSetNextMap;
}

static Forwards g_Forward;

public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
    if (!CleanPlugin())
    {
        strcopy(error, err_max, "can not clean files.");
        return APLRes_Failure;
    }

    RegPluginLibrary("mapchooser");

    CreateNative("NominateMap",             Native_NominateMap);
    CreateNative("RemoveNominationByMap",   Native_RemoveNominationByMap);
    CreateNative("RemoveNominationByOwner", Native_RemoveNominationByOwner);
    CreateNative("InitiateMapChooserVote",  Native_InitiateVote);
    CreateNative("CanMapChooserStartVote",  Native_CanVoteStart);
    CreateNative("HasEndOfMapVoteFinished", Native_CheckVoteDone);
    CreateNative("GetExcludeMapList",       Native_GetExcludeMapList);
    CreateNative("GetNominatedMapList",     Native_GetNominatedMapList);
    CreateNative("EndOfMapVoteEnabled",     Native_EndOfMapVoteEnabled);
    CreateNative("CanNominate",             Native_CanNominate);
    CreateNative("GetMapData",              Native_GetMapData);
    CreateNative("GetPartyBlockOnwer",      Native_GetPartyBlockOnwer);
    CreateNative("ForceSetNextMap",         Native_ForceSetNextMap);
    CreateNative("SetTierString",           Native_OverrideTierString);
    CreateNative("GetTierString",           Native_GetTierString);

    MarkNativeAsOptional("Store_GetClientCredits");
    MarkNativeAsOptional("Store_SetClientCredits");

    MarkNativeAsOptional("MG_Shop_GetClientMoney");
    MarkNativeAsOptional("MG_Shop_ClientEarnMoney");
    MarkNativeAsOptional("MG_Shop_ClientCostMoney");

    MarkNativeAsOptional("Pupd_CheckPlugin");

    return APLRes_Success;
}

public void OnLibraryAdded(const char[] name)
{
    if (strcmp(name, "store") == 0)
        g_pStore = true;
    else if (strcmp(name, "shop-core") == 0)
        g_pShop = true;
}

public void OnLibraryRemoved(const char[] name)
{
    if (strcmp(name, "store") == 0)
        g_pStore = false;
    else if (strcmp(name, "shop-core") == 0)
        g_pShop = false;
}

void Natives_OnPluginStart()
{
    g_Forward.m_NominationsReset    = new GlobalForward("OnNominationRemoved",    ET_Ignore, Param_String, Param_Cell, Param_Cell);
    g_Forward.m_NominationsVoted    = new GlobalForward("OnNominationVoted",      ET_Ignore, Param_String, Param_String, Param_String);
    g_Forward.m_MapVoteStarted      = new GlobalForward("OnMapVoteStarted",       ET_Ignore);
    g_Forward.m_MapVoteEnd          = new GlobalForward("OnMapVoteEnd",           ET_Ignore, Param_String);
    g_Forward.m_MapDataLoaded       = new GlobalForward("OnMapDataLoaded",        ET_Ignore);
    g_Forward.m_MapVotePoolChanged  = new GlobalForward("OnMapVotePoolChanged",   ET_Ignore);
    g_Forward.m_OnNominateMap       = new GlobalForward("OnNominateMap",          ET_Hook,   Param_String, Param_Cell, Param_Cell);
    g_Forward.m_OnNominatedMap      = new GlobalForward("OnNominatedMap",         ET_Ignore, Param_String, Param_Cell, Param_Cell);
    g_Forward.m_OnNominatePrice     = new GlobalForward("OnNominatePrice",        ET_Hook,   Param_String, Param_Cell, Param_CellByRef, Param_Cell);
    g_Forward.m_OnClearMapCooldown  = new GlobalForward("OnClearMapCooldown",     ET_Hook,   Param_String, Param_Cell);
    g_Forward.m_OnResetMapCooldown  = new GlobalForward("OnResetMapCooldown",     ET_Hook,   Param_String, Param_Cell);
    g_Forward.m_OnNextMapListCreate = new GlobalForward("OnNextMapListCreate",    ET_Hook,   Param_String);
    g_Forward.m_OnSetNextMap        = new GlobalForward("OnSetNextMap",           ET_Hook,   Param_String, Param_Cell);
}

bool Call_OnSetNextMap(const char[] map, int caller)
{
    bool allow = true;
    Call_StartForward(g_Forward.m_OnSetNextMap);
    Call_PushString(map);
    Call_PushCell(caller);
    Call_Finish(allow);
    return allow;
}

bool AllowInNextVotePool(const char[] map)
{
    bool allow = true;
    Call_StartForward(g_Forward.m_OnNextMapListCreate);
    Call_PushString(map);
    Call_Finish(allow);
    return allow;
}

bool Call_OnClearMapCooldown(const char[] map, int _cell)
{
    bool allow = true;
    Call_StartForward(g_Forward.m_OnClearMapCooldown);
    Call_PushString(map);
    Call_PushCell(_cell);
    Call_Finish(allow);
    return allow;
}

bool Call_OnResetMapCooldown(const char[] map, int _cell)
{
    bool allow = true;
    Call_StartForward(g_Forward.m_OnResetMapCooldown);
    Call_PushString(map);
    Call_PushCell(_cell);
    Call_Finish(allow);
    return allow;
}

bool Call_OnNominatePrice(const char[] map, int _cell, int &_ref, bool _bool)
{
    // module not found
    if (!g_pShop && !g_pStore)
        return true;

    bool allow = true;
    Call_StartForward(g_Forward.m_OnNominatePrice);
    Call_PushString(map);
    Call_PushCell(_cell);
    Call_PushCellRef(_ref);
    Call_PushCell(_bool);
    Call_Finish(allow);
    return allow;
}

bool Call_OnNominateMap(const char[] map, int _cell, bool _bool)
{
    bool allow = true;
    Call_StartForward(g_Forward.m_OnNominateMap);
    Call_PushString(map);
    Call_PushCell(_cell);
    Call_PushCell(_bool);
    Call_Finish(allow);
    return allow;
}

void Call_OnNominatedMap(const char[] map, int _cell, bool _bool)
{
    Call_StartForward(g_Forward.m_OnNominatedMap);
    Call_PushString(map);
    Call_PushCell(_cell);
    Call_PushCell(_bool);
    Call_Finish();
}

void Call_NominationsReset(const char[] map, int _cell, bool _bool)
{
    Call_StartForward(g_Forward.m_NominationsReset);
    Call_PushString(map);
    Call_PushCell(_cell);
    Call_PushCell(_bool);
    Call_Finish();
}

void Call_NominationsVoted(const char[] map, const char[] name, const char[] auth)
{
    Call_StartForward(g_Forward.m_NominationsVoted);
    Call_PushString(map);
    Call_PushString(name);
    Call_PushString(auth);
    Call_Finish();
}

void Call_MapVoteStarted()
{
    Call_StartForward(g_Forward.m_MapVoteStarted);
    Call_Finish();
}

void Call_MapVoteEnd(const char[] map)
{
    Call_StartForward(g_Forward.m_MapVoteEnd);
    Call_PushString(map);
    Call_Finish();
}

void Call_MapDataLoaded()
{
    Call_StartForward(g_Forward.m_MapDataLoaded);
    Call_Finish();
}

void Call_MapVotePoolChanged()
{
    Call_StartForward(g_Forward.m_MapVotePoolChanged);
    Call_Finish();
}

public any Native_RemoveNominationByMap(Handle plugin, int numParams)
{
    int len;
    GetNativeStringLength(1, len);

    if (len <= 0)
      return false;

    char[] map = new char[len+1];
    GetNativeString(1, map, len+1);

    return InternalRemoveNominationByMap(map);
}

public any Native_NominateMap(Handle plugin, int numParams)
{
    int len;
    GetNativeStringLength(1, len);

    if (len <= 0)
      return false;

    char[] map = new char[len+1];
    GetNativeString(1, map, len+1);

    return InternalNominateMap(map, GetNativeCell(2), GetNativeCell(3), GetNativeCell(4));
}

public any Native_IsWarningTimer(Handle plugin, int numParams)
{
    return g_bWarningInProgress;
}

public any Native_CanNominate(Handle plugin, int numParams)
{
    if (g_bHasVoteStarted)
        return CanNominate_No_VoteInProgress;

    if (g_bMapVoteCompleted)
        return CanNominate_No_VoteComplete;

    if (g_iNominateCount >= 5)
        return CanNominate_No_VoteFull;

    if (g_bPartyblock)
        return CanNominate_No_PartyBlock;

    return CanNominate_Yes;
}

public any Native_RemoveNominationByOwner(Handle plugin, int numParams)
{
    return InternalRemoveNominationByOwner(GetNativeCell(1));
}

public any Native_InitiateVote(Handle plugin, int numParams)
{
    MapChange when = view_as<MapChange>(GetNativeCell(1));
    ArrayList maps = view_as<ArrayList>(GetNativeCell(2));

    LogAction(-1, -1, "Starting map vote because outside request");

    SetupWarningTimer(WarningType_Vote, when, maps);
}

public any Native_CanVoteStart(Handle plugin, int numParams)
{
    return CanVoteStart();
}

public any Native_CheckVoteDone(Handle plugin, int numParams)
{
    return g_bMapVoteCompleted;
}

public any Native_GetExcludeMapList(Handle plugin, int numParams)
{
    GetCooldownMaps(view_as<ArrayList>(GetNativeCell(1)));
}

public any Native_GetNominatedMapList(Handle plugin, int numParams)
{
    ArrayList m_aNominations = view_as<ArrayList>(GetNativeCell(1));
    for (int i = 0; i < g_aNominations.Length; i++)
    {
        Nominations n;
        g_aNominations.GetArray(i, n, sizeof(Nominations));
        m_aNominations.PushArray(  n, sizeof(Nominations));
    }
}

public any Native_EndOfMapVoteEnabled(Handle plugin, int numParams)
{
    return true;
}

public any Native_GetPartyBlockOnwer(Handle plugin, int numParams)
{
    if (!g_bPartyblock || g_aNominations.Length == 0)
        return -1;

    Nominations n;
    g_aNominations.GetArray(0, n, sizeof(Nominations));

    return n.m_Owner;
}

public any Native_ForceSetNextMap(Handle plugin, int numParams)
{
    if (g_bMapVoteCompleted && !GetNativeCell(2))
        return false;

    int caller = numParams >= 3 ? GetNativeCell(3) ? -1;

    char map[128];
    GetNativeString(1, map, 128);

    return InternalSetNextMap(map, caller);
}

public any Native_OverrideTierString(Handle plugin, int numParams)
{
    int tier = GetNativeCell(1);
    if (tier < 0 || tier > MAX_TIER)
        return ThrowNativeError(SP_ERROR_PARAM, "Invalid tier <%d> gived.", tier);

    return GetNativeString(2, g_TierString[tier], 32) == SP_ERROR_NONE;
}

public any Native_GetTierString(Handle plugin, int numParams)
{
    int tier = GetNativeCell(1);
    if (tier < 0 || tier > MAX_TIER)
        return ThrowNativeError(SP_ERROR_PARAM, "Invalid tier <%d> gived.", tier);

    SetNativeString(2, g_TierString[tier], GetNativeCell(3));
    return true;
}


invp2