1. Choose the best Old Gods themed skin at the Texturing Contest #29 - Poll!
    Dismiss Notice
  2. Melee Mapping Contest #2 - Poll is up! Vote for the best competitive 1v1 map!
    Dismiss Notice
  3. Please help test custom maps and more on the latest PTR!
    Dismiss Notice
  4. Evolution complete! Make Darwin proud and go vote in the Techtree Contest #12 - Poll.
    Dismiss Notice
  5. Icon Contest #17 - Results are out! Step by to congratulate our winners!
    Dismiss Notice
  6. We've created the Staff Job Openings thread. We're currently in need of icon, video production, and social/multimedia positions to be filled. Thank you!
    Dismiss Notice

[vJASS] Command Utilities

Discussion in 'Submissions' started by Pinzu, Aug 1, 2018.

  1. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,011
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    The purpose of this library is to make the creation of commands faster and easier to manage by providing additional API geared towards that end. Among other things you can register commands, disable them for certain players, or all players, and define new player string identification. Read the manual for more examples and detail of what it does.

    Library
    Code (vJASS):

    library CommandUtils uses Table
        /*
     
            ===============================================================================================
                Command Utilities version 1.3.
                    Latest version containing API and Manual can be found here:
                    hiveworkshop.com/threads/command-utilities.307554/
            ===============================================================================================
     
            This libary provides a struct for each player, which stores information generated each time a
            player enters a chat command. It's purpose is to make the creation of commands faster and easier
            to manage by providing a few additional API functions geared towards that end.
     
     
            Credits:
            -   Made by Pinzu
     
     
            Requirements
            -   Table by Bribe
                hiveworkshop.com/threads/snippet-new-table.188084/
         
         
            Configuration
            - Simply copy Bribe's Table and this library into your map
            - Modify the configurables to your preference in the global block. You can add multiple command
                prefixes if you scroll down to the onInit method inside Command struct.
         
         ===============================================================================================
        */

     
        globals
            private constant string        FILTER_PREFIX              = ""
            private constant integer        MAX_WORDS              = 300
            private constant boolean         ACK_PLAYER_NAME            = true
     
            public real chat_event
            public constant real            RESET_CHAT_EVENT        = 0
            public constant real            ON_CHAT_EVENT            = 1
            public constant real            ON_CMD_BLOCKED            = 2
            private Table playerchat
            private Table playerIds
        endglobals
     
        function GetPlayerChat takes player p returns PChat
            return playerchat[GetPlayerId(p)]
        endfunction
        function GetLastUpdatedChat takes nothing returns PChat
            return PChat.last
        endfunction
     
        function GetLastCommand takes nothing returns Command
            return Command.last
        endfunction
     
        private function isDigit takes string c returns boolean
            return c == "0" or c == "1" or c == "2" or c == "3" or c == "4" or c == "5" or /*
                */
    c == "6" or c == "7" or c == "8" or c == "9"
        endfunction
     
        function StrIsReal takes string s returns boolean
            local integer i = 0
            local integer dots = 0
            local string char
            loop
                exitwhen i == StringLength(s)
                set char = SubString(s, i, i + 1)
                if char == "." then
                    set dots = dots + 1
                    if dots > 1 then
                        return false
                    endif
                elseif not isDigit(char) then
                    return false
                endif
                set i = i + 1
            endloop
            return true
        endfunction
        function StrIsInt takes string s returns boolean
            local integer i = 0
            local string char
            loop
                exitwhen i == StringLength(s)
                set char = SubString(s, i, i + 1)
                if not isDigit(char) and not (i == 0 and char == "-") then
                    return false
                endif
                set i = i + 1
            endloop
            return true
        endfunction
     
        function StrIsBoolean takes string s returns boolean
            set s = StringCase(s, false)
            return s == "true" or s == "false"
        endfunction
     
        function S2B takes string s returns boolean
            set s = StringCase(s, false)
            if s == "true" then
                return true
            endif
            return false
        endfunction
        function StrContainsDigit takes string s returns boolean
            local integer length = StringLength(s)
            local integer i = 0
            local string c
            set i = 0
            loop
                exitwhen i == length
                set c = SubString(s, i, i + 1)
                if isDigit(c) then
                    return true
                endif
                set i = i + 1
            endloop
            return false
        endfunction
     
        function StrToPlayer takes string s returns player player
            local player p = playerIds.player[StringHash(s)]
            local integer i = 0
            if p != null then
                return p
            endif
            if ACK_PLAYER_NAME then
                loop
                    exitwhen i == bj_MAX_PLAYER_SLOTS
                    set p = Player(i)
                    if s == GetPlayerName(p) then
                        return  p
                    endif
                    set i = i + 1
                endloop
            endif
            return null
        endfunction
     
        function SavePlayerId takes string identifier, player p returns nothing
            set playerIds.player[StringHash(identifier)] = p
        endfunction
        function RemovePlayerId takes string identifier returns nothing
            call playerIds.player.remove(StringHash(identifier))
        endfunction
        private function IsPlaying takes player p returns boolean
            return GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING and /*
            */
    GetPlayerController(p) == MAP_CONTROL_USER
        endfunction
     
        struct PChat
     
            static thistype         last                    = 0
     
            readonly player         user
            readonly integer         userId
            readonly string         str
            readonly string         substr
            readonly integer         length
            readonly string array     word[MAX_WORDS]
            readonly integer array     wordStart[MAX_WORDS]
            readonly integer array     wordEnd[MAX_WORDS]
            readonly integer         wordCount
            method update takes string chatstring returns nothing
                local integer i = 0
                local string char = ""
                local string prev = ""
                loop
                    exitwhen i == .wordCount
                    set .word[i] = null
                    set .wordStart[i] = 0
                    set .wordEnd[i] = 0
                    set i = i + 1
                endloop
                set .length = StringLength(chatstring)
                set .str = chatstring                      
                set .wordCount = 0
                set .wordStart[0] = 0
                set i = 0
                loop
                    exitwhen i > .length
                    set char = SubString(chatstring, i, i + 1)
                    if char != " "  then
                        set .word[.wordCount] = .word[.wordCount] + char
                    elseif prev != " " then
                        set .wordEnd[.wordCount] = i
                        set .wordCount = .wordCount + 1
                        set .wordStart[.wordCount] = i + 1
                    endif
                    set prev = char
                    set i = i + 1
                endloop
                set .substr = SubString(.str, .wordStart[1], .length)
                set .wordEnd[.wordCount] = i - 1
                set .wordCount = .wordCount + 1
                set thistype.last = this
            endmethod
     
            static method create takes player p, integer pid returns thistype
                local thistype this = .allocate()
                set this.user = p
                set this.userId = pid
                return this
            endmethod
     
            method destroy takes nothing returns nothing
                set .user = null
                call .deallocate()
            endmethod
     
            method wordLength takes integer index returns integer
                return .wordEnd[index] - .wordStart[index]
            endmethod
            method wordLower takes integer index returns string
                return StringCase(.word[index], false)
            endmethod
     
            method wordUpper takes integer index returns string
                return StringCase(.word[index], true)
            endmethod
        endstruct
     
        struct Command
            static thistype                last
     
            readonly static thistype     head = 0
            readonly static thistype     tail = 0
            readonly thistype             next
            readonly thistype             prev
            readonly string             argument
            public string                 tooltip
     
            private boolean             enabled
            private Table                 trgTable            
            private Table                 disabledPlayers
            private integer             trgSize
            private static Table         commandTable
            private static trigger         onChatTrigger
     
     
            private static method create takes string cmd returns thistype
                local thistype this = .allocate()
                set this.trgSize = 0
                set this.trgTable = Table.create()
                set this.disabledPlayers = Table.create()
                set this.argument = cmd
                set this.enabled = true
                set thistype.commandTable[StringHash(cmd)] = this
                if head == 0 then
                    set head = this
                    set tail = this
                else
                    set this.prev = tail
                    set tail.next = this
                    set tail = this
                endif
                return this
            endmethod
     
            method destroy takes nothing returns nothing
                local integer i = 0
                local trigger t
                loop
                    exitwhen i == .trgSize
                    set t = .trgTable.trigger[i]
                    call DestroyTrigger(t)
                    set i = i + 1
                endloop
                set t = null
                if this == head and this == tail then
                    set head = 0
                    set tail = 0
                elseif this == head then
                    set head.next.prev = 0
                    set head = head.next
                elseif this == tail then
                    set tail.prev.next = 0
                    set tail = tail.prev
                else
                    set this.prev.next = this.next
                    set this.next.prev = this.prev
                endif
                call thistype.commandTable.remove(StringHash(this.argument))
                call this.trgTable.destroy()
                call this.disabledPlayers.destroy()
                call this.deallocate()
            endmethod
     
            method addTrigger takes trigger t returns nothing
                set .trgTable.trigger[.trgSize] = t
                set .trgSize = .trgSize + 1
            endmethod
     
            method removeTrigger takes integer index returns boolean
                local trigger t
                if index < 0 or index >= .trgSize then
                    return false
                endif
                set t = .trgTable.trigger[index]
                set .trgSize = .trgSize - 1
                loop
                    exitwhen index == .trgSize
                    set .trgTable.trigger[index] = .trgTable.trigger[index + 1]
                endloop
                call .trgTable.trigger.remove(index)
                call DestroyTrigger(t)
                set t = null
                return true
            endmethod
     
            static method find takes string cmd returns thistype
                return thistype.commandTable[StringHash(cmd)]
            endmethod
     
            static method exists takes string cmd returns boolean
                return thistype.find(cmd) != 0
            endmethod
     
            static method register takes string cmd, code c returns thistype
                local trigger t = CreateTrigger()
                local thistype command = thistype.commandTable[StringHash(cmd)]
                if command == 0 then
                    set command = Command.create(cmd)
                endif
                call TriggerAddAction(t, c)
                call command.addTrigger(t)
                set t = null
                return command
            endmethod
     
            static method registerForPlayer takes string cmd, code c, player whichplayer returns thistype
                local thistype command = thistype.register(cmd, c)
                local integer i = 0
                local player p
                loop
                    exitwhen i == bj_MAX_PLAYER_SLOTS
                    set p = Player(i)
                    if IsPlaying(p) and p != whichplayer then
                        set command.disabledPlayers.boolean[GetPlayerId(p)] = true
                    endif
                    set i = i + 1
                endloop
                return command
            endmethod
     
            static method deregister takes string cmd returns boolean
                local thistype command = thistype.commandTable[StringHash(cmd)]
                if command == 0 then
                    return false
                endif
                call command.destroy()
                return true
            endmethod
     
            static method enableAll takes nothing returns nothing
                call EnableTrigger(thistype.onChatTrigger)
            endmethod
     
            static method disableAll takes nothing returns nothing
                call DisableTrigger(thistype.onChatTrigger)
            endmethod
     
            static method enablePlayer takes string cmd, player p returns nothing
                call thistype.find(cmd).disabledPlayers.remove(GetPlayerId(p))
            endmethod
     
            static method disablePlayer takes string cmd, player p returns nothing
                if not IsPlaying(p) then
                    return
                endif
                set thistype.find(cmd).disabledPlayers.boolean[GetPlayerId(p)] = true
            endmethod
     
            private method isDisabledForPlayer takes player p returns boolean
                return .disabledPlayers.boolean.has(GetPlayerId(p))
            endmethod
     
            static method isEnabledForPlayer takes string cmd, player p returns boolean
                return not thistype.find(cmd).isDisabledForPlayer(p)
            endmethod
     
            private method exec takes player p returns nothing
                local integer i = 0
                loop
                    exitwhen i == .trgSize
                    call TriggerExecute(.trgTable.trigger[i])
                    set i = i + 1
                endloop
            endmethod
     
            static method enable takes string cmd returns nothing
                set thistype.find(cmd).enabled = true
            endmethod
     
            static method disable takes string cmd returns nothing
                set thistype.find(cmd).enabled = false
            endmethod
     
     
     
            private static method onChatEvent takes nothing returns nothing
                local player p = GetTriggerPlayer()
                local boolean blocked = false
                call GetPlayerChat(p).update(GetEventPlayerChatString())
                set Command.last = Command.find(PChat.last.word[0])
                if Command.last != 0 then
                    if Command.last.isDisabledForPlayer(p) and not Command.last.enabled then
                        set blocked = true
                    else
                        call Command.last.exec(p)
                    endif
                endif
                //! runtextmacro CHAT_EVENT_MACRO("chat_event")
                //! textmacro_once CHAT_EVENT_MACRO takes VAR
                if blocked then
                    set $VAR$ = RESET_CHAT_EVENT
                    set $VAR$ = ON_CMD_BLOCKED
                else
                    set $VAR$ = RESET_CHAT_EVENT
                set $VAR$ = ON_CHAT_EVENT
                endif
                //! endtextmacro
                set p = null
            endmethod
     
            private static method onLeave takes nothing returns nothing
                local player p = GetTriggerPlayer()
                local integer pid = GetPlayerId(p)
                local PChat chat = playerchat[pid]
                local thistype cmd
                call playerchat.remove(pid)
                call chat.destroy()
                set  cmd = thistype.head
                loop
                    exitwhen cmd == 0
                    call cmd.disabledPlayers.remove(pid)
                    set cmd = cmd.next
                endloop
                set p = null
            endmethod
     
            private static method onInit takes nothing returns nothing
                local player p
                local integer i = 0
                local trigger trgLeave = CreateTrigger()
                set thistype.commandTable = Table.create()
                set thistype.onChatTrigger = CreateTrigger()
                set thistype.last = 0
                set playerIds = Table.create()
                set playerchat = Table.create()
                loop
                    exitwhen i == bj_MAX_PLAYER_SLOTS
                    set p = Player(i)
                    set playerIds.player[StringHash(I2S(i + 1))] = p
                    if IsPlaying(p) then
                        call TriggerRegisterPlayerChatEvent(thistype.onChatTrigger, p, FILTER_PREFIX, false)
                        call TriggerRegisterPlayerEventLeave(trgLeave, p)
                        set playerchat[i] = PChat.create(p, i)
                    endif
                    set i = i + 1
                endloop
                call TriggerAddAction(thistype.onChatTrigger, function thistype.onChatEvent)
                call TriggerAddAction(trgLeave, function thistype.onLeave)
                set trgLeave = null
                set p = null
            endmethod
        endstruct
     
     
    endlibrary
     


    API
    Code (vJASS):

        /*
     
            ===============================================================================================
                Command Utilities version 1.3.
                Latest version: hiveworkshop.com/threads/command-utilities.307554/
            ===============================================================================================
       
            This libary provides a struct for each player, which stores information generated each time a
            player enters a chat command. It's purpose is to make the creation of commands faster and easier
            to manage by providing a few additional API functions geared towards that end.
     
     
            Credits:
            -   Made by Pinzu
     
     
            Requirements
            -   Table by Bribe
                hiveworkshop.com/threads/snippet-new-table.188084/
           
           
            Configuration
            - Simply copy Bribe's Table and this library into your map
            - Modify the configurables to your preference in the global block. You can add multiple command
                prefixes if you scroll down to the onInit method inside Command struct.
       
     
            ===============================================================================================
            GLOBALS
            ===============================================================================================
     
            Filter prefix, determines when the LastPlayerChat should update. If set to "-" it will only fire for chat messages
            with such a prefix. If you want to have multiple commands you can scroll down and add your own events manually, however
            if you want to handle all chat events then  set it to nothing.
     
            private constant string        FILTER_PREFIX              = ""
       
       
            Numbers of maximum words the system handles, should be a relatively high number.
       
            private constant integer        MAX_WORDS              = 300
       
       
            This is used to allow player names as valid default identification.
            private constant boolean         ACK_PLAYER_NAME            = true
     
            These are used for event detection
       
            public real chat_event                                    = -1
            public constant real            RESET_CHAT_EVENT        = 0        Clear previous detected chat event
            public constant real            ON_CHAT_EVENT            = 1        Triggered when a chat event is noticed by the system
            public constant real            ON_CMD_BLOCKED            = 2        Triggered when a command is blocked from executing
       
       
       
            ===============================================================================================
            FUNCTIONS
            ===============================================================================================
     
            function GetPlayerChat takes player p returns PChat
                Returns a struct holding the last generated chat message data from a given player
            function GetLastUpdatedChat takes nothing returns PChat
                Returns a struct holding the last updated chat data
     
            function GetLastCommand takes nothing returns Command
                Returns the last detected command, blocked or not
            function StrIsReal takes string s returns boolean
                Returns true if a string is of type real
       
            function StrIsInt takes string s returns boolean
                Returns true if a string is of type integer
     
            function StrIsBoolean takes string s returns boolean
                Returns true if a string is of type boolean
           
            function S2B takes string s returns boolean
                Returns true if a string matches "true", otherwise false.
            function StrContainsDigit takes string s returns boolean
                Returns true if a string contains a digit
           
            function StrToPlayer takes string s returns player player
                Converts a string identifier into a player
           
            function SavePlayerId takes string identifier, player p returns nothing
                Saves a string as a player identification
           
            function RemovePlayerId takes string identifier returns nothing
                Removes a saved player identification
           
            ===============================================================================================
            STRUCT: PChat (Player Chat)
            ===============================================================================================
            static thistype         last                    Last updated instance reference
       
            readonly player         user                    The triggering user
            readonly integer         userId                    The users player id
            readonly string         str                     Entered chat string
            readonly string         substr                     Entered chat string, excluding the command
            readonly integer         length                    Length of the entered chat string
            readonly string array     word[MAX_WORDS]            A list of words detected from the entered chat string
            readonly integer array     wordStart[MAX_WORDS]    Starting index of a word in the entered chat string        
            readonly integer array     wordEnd[MAX_WORDS]        Ending index of a word in the entered chat string
            readonly integer         wordCount                 Number of detected words in the entered chat string
            method wordLength takes integer index returns integer
                returns the length of a word at a given position.
            method wordLower takes integer index returns string
                Returns the word as lower case.
           
            method wordUpper takes integer index returns string
                returns the word as upper case.
           
            ===============================================================================================
            STRUCT: Command
            ===============================================================================================
     
            static thistype                last            Last updated instance reference
     
            readonly static thistype     head             The first command
            readonly static thistype     tail            The last command
            readonly thistype             next            Next command node
            readonly thistype             prev            Previous command node
            readonly string             argument        Registered command string
            public string                 tooltip            Tooltip used to provide information to the user
     
       
            method destroy takes nothing returns nothing
                Destroys the command.
       
            method addTrigger takes trigger t returns nothing.
                Adds a trigger to be executed when the command is entered
       
            method removeTrigger takes integer index returns boolean
                Removes a trigger from the command.
       
            static method find takes string cmd returns thistype
                Returns a command with matching argument .
       
            static method exists takes string cmd returns boolean
                Returns true if the command exists.
       
            static method register takes string cmd, code c returns thistype
                Registers a command with a string argument and a given function.
       
            static method registerForPlayer takes string cmd, code c, player whichplayer returns thistype
                Register a command to be enabled for only a specific player.
       
            static method deregister takes string cmd returns boolean
                Deregisters a command.
           
            static method enableAll takes nothing returns nothing
                Enables all existing commands. Does not affect commands that are disabled to specific players.
       
            static method disableAll takes nothing returns nothing
                Disables all commands.
       
            static method enablePlayer takes string cmd, player p returns nothing
                Enables a command for a given player.
       
            static method disablePlayer takes string cmd, player p returns nothing
                Disables a command for a given player.
       
            static method isEnabledForPlayer takes string cmd, player p returns boolean
                Returns true if a command is enabled for a given player.
       
            static method enable takes string cmd returns nothing
                Enables the specified command, does not affect players that are disabled.
       
            static method disable takes string cmd returns nothing
                Disables the specified command.
           
        ===============================================================================================*/

           


    Manual


    1. Creating a command
    Example 1
    Code (vJASS):

    scope Hello initializer Init
        private function SayHello takes nothing returns nothing
            call BJDebugMsg("Hello, World!")
        endfunction
        private function Init takes nothing returns nothing
            call Command.register("-hello", function SayHello)
        endfunction
    endscope

    Example 2
    Code (vJASS):

    scope Hello initializer Init
        private function SpamHello takes nothing returns nothing
            call BJDebugMsg("Hello Spam!")
        endfunction
        private function SayHello takes nothing returns nothing
            call BJDebugMsg("Hello, World!")
        endfunction
        private function Init takes nothing returns nothing
            call Command.register("-hello", function SayHello)
            call Command.register("-hello" function SpamHello)
        endfunction
    endscope

    Note in the second example that we have registrated two functions to the same command. When the player then executes the command both of these functions will run.

    Example 3
    We can also register commands that start off as enabled for only one player. To enable it for others you'll have to continue reading.
    Code (vJASS):

    scope Hello initializer Init
        private function SayHello takes nothing returns nothing
            call BJDebugMsg("Hello, World!")
        endfunction
        private function Init takes nothing returns nothing
            call Command.registerForPlayer("-hello", function SayHello, Player(0))
        endfunction
    endscope



    2. Removing a command

    Example 1
    Using the same trigger as before. We will create the same command but this time remove it once it has fired once.
    Code (vJASS):

    scope Hello initializer Init
        private function SayHello takes nothing returns nothing
            call BJDebugMsg("Hello, World!")
            call Command.deregister("-hello")
        endfunction
        private function Init takes nothing returns nothing
            call Command.register("-hello", function SayHello)
        endfunction
    endscope
     

    You can also use the destroy method directly:
    call Command.last.destroy()
    or
    call Command.find("-hello").destroy()


    Example 2
    What if I have two triggers registrated to the same command but only want to remove one? Worry not, you simply have to remeber the index of the trigger you want removed.
    Code (vJASS):

    scope Hello initializer Init
        private function SpamHello takes nothing returns nothing
            call BJDebugMsg("Hello Spam!")
            call Command.last.removeTrigger(1)
        endfunction
        private function SayHello takes nothing returns nothing
            call BJDebugMsg("Hello, World!")
        endfunction
        private function Init takes nothing returns nothing
            call Command.register("-hello", function SayHello)
            call Command.register("-hello", function SpamHello)
        endfunction
    endscope

    Since SpamHello was the last trigger registrated it will be indexed at position 1, and in this example "Hello Spam!" will only run once, where as "Hello, World!" runs always.

    3. Enabling and disabling commands

    First a brief overview of the enable/disable API.

    call Command.disableAll()
    Disables all commands.
    call Command.enableAll()
    Enables all commands.
    call Command.disable("-hello")
    Disables the command "-hello".
    call Command.enable("-hello")
    Enables the command "-hello".
    call Command.disablePlayer("-hello", Player(0))
    Disables the "-hello" command for a Player Red.
    call Command.enablePlayer("-hello", Player(0))
    Enables the "-hello" command for a Player Red.
    Command.isEnabledForPlayer("-hello", Player(0))
    Returns true if "-hello" is enabled for Player Red.

    Example 1
    Now lets take the above information and create a cooldown that limits how often a player can use the "-hello" command.
    Code (vJASS):

    scope Hello initializer Init
        globals
            private Table table
        endglobals
        private function EnableHelloAgain takes nothing returns nothing
            local timer t = GetExpiredTimer()
            local integer id = GetHandleId(t)
            local player p = table.player[id]
            call Command.enablePlayer("-hello", p)
            call table.remove(id)
            call DestroyTimer(t)
            call BJDebugMsg("Hello renabled for " + GetPlayerName(p))
            set p = null
            set t = null
        endfunction
        private function SayHello takes nothing returns nothing
            local timer t = CreateTimer()
            local player p = GetTriggerPlayer()
            call TimerStart(t, 5.0, false, function EnableHelloAgain)
            call Command.disablePlayer("-hello", p)
            set table.player[GetHandleId(t)] = p
            set t = null
            call BJDebugMsg("Hello, it's me again!")
        endfunction
        private function Init takes nothing returns nothing
            call Command.register("-hello", function SayHello)
            set table = Table.create()
        endfunction
    endscope



    4. Manipulating the gathered Player Chat data

    First a brief overview of the API.

    static PChat last
    Last updated struct instance.
    readonly player user
    Holds the triggering player.
    readonly integer userId
    Holds the player id of the triggering player.
    readonly string str
    The entered chat string
    readonly string substr
    The entire chat string after the command word.
    readonly integer length
    The length of the entered chat string.
    readonly string array word
    Found words from the entered chat string.
    readonly integer array wordStart
    The first index of a given word.
    readonly integer array wordEnd
    The last index of a given word.
    readonly integer wordCount
    The number of words found in the entered chat string.
    method wordLength takes integer index returns integer
    Returns the length of a word at a given position.
    method wordLower takes integer index returns string
    Returns a word as lower case.
    method wordUpper takes integer index returns string
    Returns a word as upper case.

    Example 1
    Iterating over all the words entered by the player.
    Code (vJASS):
    scope ListWords initializer Init
        private function ListWords takes nothing returns nothing
            local PChat chat = GetLastUpdatedChat()    // Alternatively chat = PChat.last
            local string s = ""
            local integer i = 1        // We start at 1 to exclude the command word.
            loop
                exitwhen i == chat.wordCount
                set s = "'" + chat.word + "' "
                set i = i + 1
            endloop
            call BJDebugMsg(GetPlayerName(chat.user) + " words: " + s)
        endfunction
        private function Init takes nothing returns nothing
            call Command.register("-words", function ListWords)
        endfunction
    endscope
     

    Example 2
    Creating a substring based on a combination of words using wordStart and wordEnd
    Code (vJASS):

    scope CombineWords initializer Init
        private function CombineWordAsSubstring takes nothing returns nothing
            local PChat chat = GetLastUpdatedChat()
            if chat.wordCount > 1 then
                call BJDebugMsg("'" + SubString(chat.str, chat.wordStart[1], chat.wordEnd[chat.wordCount - 1]) + "'")    // We exclude the last word!
            endif
        endfunction

        private function Init takes nothing returns nothing
            call Command.register("-combine", function CombineWordAsSubstring)
        endfunction
    endscope


    5. Player string identification


    Per default the library recognizes player numbers such as "1" for player red and player names. If you don't wish to permit player names as identifiers you can go to the global configurable block and set the variable ACK_PLAYER_NAME to false. You can also create your own string identifiers for players or remove the default player numbers, using the following API:

    function SavePlayerId takes string identifier, player p returns nothing

    function RemovePlayerId takes string identifier returns nothing

    function StrToPlayer takes string s returns player


    Example 1
    Now let's create a command for kicking players that utilizes StrToPlayer to find out which player should be dropped.
    Code (vJASS):

    scope KickPlayer initializer Init
        private function KickPlayer takes nothing returns nothing
            local PChat chat = GetLastUpdatedChat()
            local player p = StrToPlayer(chat.word[1])
            if p == null then
                call BJDebugMsg("Invalid Player.")
                return
            endif
            call CustomDefeatBJ(p, "You were kicked by " + GetPlayerName(chat.user) + "...")
            call BJDebugMsg(GetPlayerName(p) + " was kicked by " + GetPlayerName(chat.user) + "!")
            set p = null
        endfunction

        private function Init takes nothing returns nothing
            call Command.register("-kick", function KickPlayer)
     
            // Lets create custom identifiers
            call SavePlayerId("red", Player(0))
            call SavePlayerId("blue", Player(1))
            // and so on...
        endfunction
    endscope
     



    6. Creating complex commands

    Example 1
    In this example we will create a cheat that is the same as the CreateUnit native. We will be using an external library for string to ascii conversion to get the unit type. The rest of the functions used exists inside the library.

    [Snippet] Ascii

    Code (vJASS):

    scope Spawn initializer Init
        private function SpawnUnit takes nothing returns nothing
            local PChat chat = GetLastUpdatedChat()
            local real x
            local real y
            local real facing
            local player p
            local integer id
     
            // Checking word length
            if chat.wordCount < 4 then
                call BJDebugMsg("Invalid command.\nHelp: " + Command.last.tooltip)
                return
            endif
     
            // Getting the player
            set p = StrToPlayer(chat.word[1])
            if p == null then
                call BJDebugMsg("Invalid player specified.\nHelp: " + Command.last.tooltip)
                return
            endif
     
            // Getting the Unit Type Id
            set id = S2A(chat.word[2])
     
            if not StrIsReal(chat.word[3]) or not StrIsReal(chat.word[4]) then
                call BJDebugMsg("Invalid coordinates specified.\nHelp: " + Command.last.tooltip)
                return
            endif
     
            set x = S2R(chat.word[4])
            set y = S2R(chat.word[5])
            set facing = S2R(chat.word[6])

            call CreateUnit(p, id, x, y, facing)
     
        endfunction

        private function Init takes nothing returns nothing
            local Command command = Command.register("-spawn", function SpawnUnit)
            set command.tooltip = "-spawn [player] [unit id] [x] [y] [facing]"
        endfunction
    endscope
     


    Example 2
    In this example we'll be creating a -give command for sending players gold or wood.
    Code (vJASS):

    scope Give initializer Init
        private function Give takes nothing returns nothing
            local PChat chat = GetLastUpdatedChat()
            local player p2
            local integer amount
            local integer totalAmount
     
            if (chat.wordCount != 4) then
                call BJDebugMsg("Invalid entry.\nHelp: " + Command.last.tooltip)
                return
            endif
            set p2 = StrToPlayer(chat.word[1])
            if (p2 == null) then
                call BJDebugMsg("Invalid player specified.\nHelp: " + Command.last.tooltip)
                return
            endif
            if p2 == chat.user then
                call BJDebugMsg("You can't gift yourself!")
                return
            endif
     
            // The amount that wants to be given
            if not StrIsInt(chat.word[2]) then
                call BJDebugMsg("No amount specified.\nHelp: " + Command.last.tooltip)
                return
            endif
            set amount = S2I(chat.word[2])
     
            if (chat.word[3] == "gold" or chat.word[3] == "g") then
                set totalAmount = GetPlayerState(p2, PLAYER_STATE_RESOURCE_GOLD)
                if totalAmount < amount then
                    set amount = totalAmount
                endif
                call SetPlayerState(p2, PLAYER_STATE_RESOURCE_GOLD, GetPlayerState(p2, PLAYER_STATE_RESOURCE_GOLD) + amount)
                call SetPlayerState(chat.user, PLAYER_STATE_RESOURCE_GOLD, totalAmount - amount)
                call BJDebugMsg(GetPlayerName(chat.user) + " gave " + GetPlayerName(p2) + " " + I2S(amount) + " gold.")
            elseif (chat.word[3] == "wood" or chat.word[3] == "lumber" or chat.word[3] == "l" or chat.word[3] == "w") then
                set totalAmount = GetPlayerState(p2, PLAYER_STATE_RESOURCE_LUMBER)
                            if totalAmount < amount then
                    set amount = totalAmount
                endif
                call BJDebugMsg(GetPlayerName(chat.user) + " gave " + GetPlayerName(p2) + " " + I2S(amount) + " lumber.")
                call SetPlayerState(chat.user, PLAYER_STATE_RESOURCE_LUMBER, totalAmount - amount)
                call SetPlayerState(p2, PLAYER_STATE_RESOURCE_LUMBER, GetPlayerState(p2, PLAYER_STATE_RESOURCE_LUMBER) + amount)
            else
                call BJDebugMsg("Invalid resource specified")
            endif
        endfunction
     
        private function Init takes nothing returns nothing
            local Command command = Command.register("-give", function Give)
            set command.tooltip = "-give [player] [amount] [resource]"
        endfunction
    endscope
     



    7. Iterating through all commands

    Example 1
    In this example we'll be creating a "-commands" command to get all available commands for the triggering player. But you could just as well use this to put it in a quest log or anything else really.
    Code (vJASS):

    scope Commands initializer Init
        private function ShowPlayerCommands takes nothing returns nothing
            local PChat chat = GetLastUpdatedChat()
            local Command cmd = Command.head
            local string s = ""
            loop
                exitwhen cmd == 0
                if Command.isEnabledForPlayer(cmd.argument, chat.user) then
                    set s = s + cmd.tooltip + "\n"
                endif
                set cmd = cmd.next
            endloop
            call BJDebugMsg("Commands:\n" + s)
        endfunction
     
        private function YesReally takes nothing returns nothing
            call DoNothing()
        endfunction
     
        private function Init takes nothing returns nothing
            local Command cmd = Command.register("-commands", function ShowPlayerCommands)
            set cmd.tooltip = "-commands"
            set cmd = Command.register("-kick", function YesReally)
            set cmd.tooltip = "-kick [player]"
            set cmd =  Command.register("-name", function YesReally)
            set cmd.tooltip = "-name [player name]"
            set cmd =  Command.register("-give", function YesReally)
            set cmd.tooltip = "-give [player] [amount] [resource]"
            set cmd =  Command.register("-spawn", function YesReally)
            set cmd.tooltip = "-spawn [player] [unit type] [x] [y] [facing]"
            set cmd =  Command.register("-hello", function YesReally)
            set cmd.tooltip = "-hello"
            call Command.disablePlayer("-hello", Player(0))
        endfunction
    endscope

    Note that the Init trigger is just an example to illustrate that if Player Red will not see the "-hello" command.


    8. Processing all chat messages

    Finally, in this example you'll learn how to use the provided real variable events to process all entered chat messages and detect commands that were blocked from execution. This can be used for creating a chat system or displaying error messages for blocked commands.


    Step by step:

    1) Change the configurable: Set FILTER_PREFIX = ""
    2) Scroll down in the library code until you find the static method "onChatEvent" inside the Command struct. Then change the variable (chat_event) inside the textmacros to your global variable.
    3) Implement it in your chat trigger. It should look something like this:

    Code (vJASS):

    scope ChatEvent initializer Init
        private function OnChatEvent takes nothing returns nothing
            local PChat chat = GetLastUpdatedChat()
            local Command cmd = GetLastCommand()
            if cmd != 0 then
                call BJDebugMsg("Executed command: " + cmd.argument)
            else
                call BJDebugMsg(GetPlayerName(chat.user) + ": " + chat.str)
            endif
        endfunction
        private function OnChatBlocked takes nothing returns nothing
            local Command cmd = GetLastCommand()
            local PChat chat = GetLastUpdatedChat()
            call BJDebugMsg(GetPlayerName(chat.user) + " cannot execute command: " + cmd.argument + "!")
        endfunction
     
        private function Init takes nothing returns nothing
           local trigger trgOnChatEvent = CreateTrigger()
           local trigger trgOnCmdBlocked = CreateTrigger()
           call TriggerRegisterVariableEvent(trgOnChatEvent, "udg_chat_event", EQUAL, 1)
           call TriggerRegisterVariableEvent(trgOnCmdBlocked, "udg_chat_event", EQUAL, 2)
           call TriggerAddAction(trgOnChatEvent, function OnChatEvent)
           call TriggerAddAction(trgOnCmdBlocked, function OnChatBlocked)
           set trgOnChatEvent = null
           set trgOnCmdBlocked = null
        endfunction
    endscope
     

    That's it for this guide. Good job for reading it all!

     
    Last edited: Aug 21, 2018
  2. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    7,667
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    For the example script, you may want to use the "debug" keyword or add your own "debug" boolean to the system in order to prevent in-game messages intended for a tester to appear for all players at all times.

    You're also not using switchers on your "-give" functions, which means you're running code despite knowing there are invalid values preventing anything from displaying correctly. Either use "elseif" or use "return" to skip the code below.

    I'm only saying this because people who use your resource are going to base their stuff on your demo code. One of many takeaways I've had from Damage Engine was the sheer number of people taking my demo code and making small modifications of it to create their own stuff. It's less likely to be encountered in the vJass community, but it's something I'm conscious of as a resource developer.

    In regards to the core system, "GetLastPlayerChat" takes an integer. Based on the function name, I would assume it takes a player.

    The rest of the code looks compact, to the point, without unnecessary stuff. I'm not a fan of limiting it to 4 characters, however. I feel this should be customizable.
     
  3. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,011
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    First, thanks for your feedback.
    • I'm not sure of the 4 character limit you are talking about, are you refering to the documentation error perhaps (saw a random 4 showing up there)? ^^
    • I'll revert it back to taking a player again.
    • I'll see what cleanup can be done do the sample code, didn't really want to provide a whole host of stuff as it's just examples of API usage.
    • I could use your input on if
      SetCommandHelp
      and
      GetCommandHelp
      should be omitted or if I should perhaps extend this feature to also include the possibility of listing all commands? The current utility of it seem somewhat questionable at best.
    • Maybe I should include an already existing StringLib for the StrIsReal, StrIsDigit,StrContainsDigit (there are a few others I could think would be useful here aswell) which then becomes the question.
    Reference: [Documentation] String Type

    -----------------------------------------------

    I made the suggested changes and added features relating to player usage. You can now define custom play identification such as "red" or use default ones, such as"1" and "WorldEditor". Added a new function for CommandRegistration to make setup require less lines of code. Finally i added a function to enable/disable the trigger firing the commands. I also removed the feature of saving a string containing help information to a command as it appeared like a useless feature to me.

    * I'm also considering moving the word[] variable into a table instead and also storing the start position of each word (and possibly end) so that combining words can be done easier using substring, but maybe this is overkill. I could use some help defining such operators.

    * Should I create a trigger for cleaning up when a player leaves or just have that as a callable function? Or ignore it altogether as it's pretty static throughout the game.

    Any suggestions of features that are missing or improvement to the code is most welcome, as always.


    Changelog v 1.0.2

    - Added: DisablePlayerCommand - disables command for a given player
    - Added: EnablePlayerCommand - enables a command for a given player
    - Added: StrToPlayer - converts a string identifier into a player e.g. "1", "WorldEditor" or
    custom definition.
    - Added: TrgRegisterCommand - an alternative command registration, everything in one line.
    - Added: SavePlayerId - allows customization of player identification.
    - Added: RemovePlayerId - removes a stored player identification.
    - Added: function to disable/enable all commands.
    - Removed: SetCommandHelp and GetCommandHelp which returned a human readable
    - Moved: The main code was moved into the struct Command.
    - Updated: Sample code got some additions and touch ups
     
    Last edited: Aug 8, 2018
  4. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,011
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    One could make a GUI version of this by creating a wrapper for the setup of all commands and then using an array for each player. The only problem would be the 2D stuff [player][word]. An idea to solve that could be to just omit player and only having lastPlayer.data instead of player[].data. This could be useful I think, but I'm unsure if it's worth making tbh, as they do like to copy and paste alot.

    I've decided to keep the PlayerChat update even if a command is not entered as it can be useful in chat systems and such. Currently the system can't take in empty commands, this I think is a flaw but i don't see any way to handle it as you might want to have multiple blanks triggered at the same time where as the current setup would only allow one such "command" to be stored if implemented, but perhaps that would be acceptable. Or maybe I should just ignore this 'issue'.

    I created a list containing all commands that could be useful if one would want to iterate through them all, but I decided to not implement it at this point. Could be useful if you want to display all commands or all commands available to a certain player. What do you guys think of this?

    Update: 1.1.
    Most changes were done internally but some usage differences are:

    - Changed all member variables of PlayerChat to readonly and added wordStart[] and wordEnd[] which denotes the start and end points of a given word. Exmaple usage:
     set temp = SubString(playerChat.str, playerChat.wordStart[2], playerChat.wordEnd[4])
    which would combine word 2, 3 and 4.
     set temp = SubString(playerChat.str, playerChat.wordStart[2], playerChat.strLength)
    combines everything from the start of word 2 to the end.
    - Reduced the maximum number of words to 400.
    - Methods added to PlayerChat:
    method wordLength takes integer index returns integer

    method wordLower takes integer index returns string

    method wordUpper takes integer index returns string

    - Methods removed from PlayerChat:
    method getWord takes integer index returns string 
    (as there is already a word[] public variable for that).


    Is it worth spending time making this possible?
    Code (vJASS):

        call TrgRegisterCommand(CreateTrigger(), "-quit", function QuitJob)
        call TrgRegisterCommand(CreateTrigger(), "-quit", function QuitSchool)


    Currently it will only maintain a 1 to 1 link between command and trigger and only the last registrated command will be executed. Ideally both of these should run, which can be achieved by storing the triggers in a list.

    Another limitation is that you can only register commands by the first word as a prefix but what if you want to have commands like this "-quit school" and "-quit job" be registrated separetly. It would be easy to handle with if-statements inside a universal "-quit" command, but is that ideal for the user?
     
    Last edited: Aug 13, 2018
  5. Jampion

    Jampion

    JASS Reviewer

    Joined:
    Mar 25, 2016
    Messages:
    1,260
    Resources:
    0
    Resources:
    0
    StrIsReal returns true for strings like "0asd". Is this intended?
    Also StrIsReal(".5") = true, but StrIsReal(".0") = false

    Displaying a list of commands a player can use would be useful. One could implement a -help or -commands command with this.

    It's not bad to have this, as this is how normal triggers and events work as well. I would add it, if it is easily doable.

    Not an issue imo. You can just use the second word as argument and then act accordingly.
     
  6. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,011
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    Thanks for your feedback. I'll look into it the things you suggested.

    StrIsReal("0asd") works as that is how S2R functions, "5asd" is 5 but asd5 is 0. I'll fix ".0" aswell. If you think it's better to deny any values with none digits i could do that but I don't see why this is important? I just wanted to filter away clear misstakes such as "asdsa" returning 0. ^^

    But the reason I didn't include a list of commands, was because that let's take the command "-give", is not very useful information if the command has multiple arguments which the system doesn't handle, in this example we have "-give [player] [amount] [resource]" would just return "-give". It's not clear to me what one would use it for unless you also return SetCommandTooltip GetCommandTooltip?

    Will also add: CommandIsEnabledForPlayer(command, player) and CommandExists(command). That way one could add a dummy command "-" and show error messages for commands that are disabled for specifc players.
     
    Last edited: Aug 15, 2018
  7. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,011
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    All the previously mention problems have been taken care of and the code base has been remade.

    - Cleans up after players leave
    - StrIsInt and StrIsreal runs as you expect them too now.
    - Added user and userId to PlayerChat to reduce the amount of GetTriggeringPlayer() and GetPlayerId() that is needed.
    - Blank words are ommited from being registrated as words.
    - Created a user manual with example usage.
    - Added additional API.
    - Added iteration of commands
    - Added event detection for blocked commands and chat-events.

    And much more...

    I now consider this a complete resource, from my end of things, unless any new good ideas pop up, and if you have such please post them as pseudo code. "I should be able to do this..."
     
    Last edited: Aug 21, 2018
  8. TriggerHappy

    TriggerHappy

    Code Moderator

    Joined:
    Jun 23, 2007
    Messages:
    3,497
    Resources:
    22
    Spells:
    11
    Tutorials:
    2
    JASS:
    9
    Resources:
    22
    The reason I am preferring @edo494's script ([System] ChatCommand) is because it's more lightweight and has an external string library to handle the string operations. It's nice you can specify a specific player for a chat event so you don't need to write the same code over and over again (checking player in callback), and edo's doesn't have that, but I don't think it's enough to replace his library. Are there any other benefits to this over the already approved one?

    That being said, some of your functions need to be public/private so they don't conflict with other functions.

    Code (vJASS):

        function GetPlayerChat takes player p returns PChat
        function GetLastCommand takes nothing returns Command
        function StrIsReal takes string s returns boolean
        function StrIsInt takes string s returns boolean
        function StrIsBoolean takes string s returns boolean
        function S2B takes string s returns boolean
        function StrContainsDigit takes string s returns boolean
        function StrToPlayer takes string s returns player player
        function SavePlayerId takes string identifier, player p returns nothing
        function RemovePlayerId takes string identifier returns nothing


    There are also optimizations you can do.

    Code (vJASS):

    private function isDigit takes string c returns boolean
        return c == "0" or (StringLength(c) == 1 and S2I(c) > 0)
    endfunction

    function StrIsInt takes string s returns boolean
        return s == "0" or S2I(s) > 0
    endfunction

    function S2B takes string s returns boolean
        return StringCase(s, false) == "true"
    endfunction
     
  9. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,011
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    They can not be made private as the library itself does not utilize any of the string operators. Renamed is what you're looking for? Looking back at it, yes everything from StrIsReal to StrToPlayer should probably be put into a sub library, and has been suggested before to me, with the small exception being StrToPlayer as that could be pretty flexible to keep. :)

    I had similar StrIsInt before, but it doesn't handle invalid entries very well, 5a for instance would be true.
    --------

    I've stated my case in the manual, haven't seen his system before. Though I wonder how would he handle multiple arguments. I'll just focus on the main part of the library.

    • Built for and easily manages: [command] [argument 1] [argument 2] ... [argument N]
    • Executes multiple functions registered to the same command
    • You can add/remove multiple triggers attached to the same command.
    • Trigger a real event for when a chat command is entered, thus you can utilize the word manager for other things (chat system) (perhaps should be made optional using static if)
    • Triggers a real event for when a command was blocked by a player.
    • You can iterate through all the commands and check which are enabled/disabled for a certain player.
    • Commands can be managed on a player level rather than a global level, thus removing the need for if GetTriggerPlayer() == AcceptablePlayer
    • The PChat struct keeps the owning player and playerId stored from startup, so no need to use GetTrigPlayer or GetPlayerId natives when performing commands.
    • You can attach displayable tooltips to each command.
    • Doesn't have this: "Currently, even if you type something sensible and then
    type some valid switch, it will get triggered. This may
    be filtered, but requires some thinking of doing it inteligently.

    Example: "hey boys, its nice to -play with you" will still try
    to run callback to switch "play" with string "with you""

    There are probably more stuff one could nitpick on but I haven't looked all that deeply into his library.

    The only drawback is that this formats each entered chat into words on each chat event, regardless of if a command was found or not. But I don't see this as a big issue as even if 24 players would be monkey spamming the chat, it wouldn't even be noticed, plus you can filter it somewhat with COMMAND_PREFIX. I somewhat also question why a chat based system needs to be light weight in the first place. It's only in space complexity, with each struct housing [MAX_WORDS]x3 (300x3 currently) sized arrays for each player that is active, that I find somewhat bad as most of it would remain unused. This however could be addressed by using a table rather than array to store the data, haven't made up my mind on this yet.

    To summerize what this does better I'll lift a previous example out. Now you could say that the word formating that he doesn't have could be done in the command trigger with an external library, but then you'd also have to store your words yourself and all that. The whole point with this system is to automate as much of the command creation as possible and put it in one place, not simply provide a command callback interface.

    Code
    Code (vJASS):

    scope Spawn initializer Init
        private function SpawnUnit takes nothing returns nothing
            local PChat chat = GetLastUpdatedChat()
            local real x
            local real y
            local real facing
            local player p
            local integer id

            // Checking word length
            if chat.wordCount < 4 then
                call BJDebugMsg("Invalid command.\nHelp: " + Command.last.tooltip)
                return
            endif

            // Getting the player
            set p = StrToPlayer(chat.word[1])
            if p == null then
                call BJDebugMsg("Invalid player specified.\nHelp: " + Command.last.tooltip)
                return
            endif

            // Getting the Unit Type Id
            set id = S2A(chat.word[2])

            if not StrIsReal(chat.word[3]) or not StrIsReal(chat.word[4]) then
                call BJDebugMsg("Invalid coordinates specified.\nHelp: " + Command.last.tooltip)
                return
            endif

            set x = S2R(chat.word[4])
            set y = S2R(chat.word[5])
            set facing = S2R(chat.word[6])

            call CreateUnit(p, id, x, y, facing)

        endfunction

        private function Init takes nothing returns nothing
            local Command command = Command.register("-spawn", function SpawnUnit)
            set command.tooltip = "-spawn [player] [unit id] [x] [y] [facing]"
        endfunction
    endscope


    Anyhow, thanks for your feedback. :]


    Note to self
    Use condition instead of action for callback
    Use TriggerClearConditions before destroying the trigger.
    Put the string stuff in aother library
    Make the real event variable optional/use static if
     
    Last edited: Aug 24, 2018