• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[vJASS] Command Utilities

Level 15
Joined
Nov 30, 2007
Messages
1,202
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.

JASS:
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
        local integer len = StringLength(s)
        if len == 0 then 
            return false 
        endif
        loop
            exitwhen i == len
            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

JASS:
    /*
 
        ===============================================================================================
            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.
       
    ===============================================================================================*/



Example 1
JASS:
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
JASS:
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.
JASS:
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



Example 1
Using the same trigger as before. We will create the same command but this time remove it once it has fired once.
JASS:
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.
JASS:
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.


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.
JASS:
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



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.
JASS:
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
JASS:
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



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.
JASS:
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



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

JASS:
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.
JASS:
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



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.
JASS:
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.



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:

JASS:
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:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
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.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
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.

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:
Level 15
Joined
Nov 30, 2007
Messages
1,202
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?
JASS:
    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:

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
StrIsReal returns true for strings like "0asd". Is this intended?
Also StrIsReal(".5") = true, but StrIsReal(".0") = false

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?
Displaying a list of commands a player can use would be useful. One could implement a -help or -commands command with this.

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.
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.

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?
Not an issue imo. You can just use the second word as argument and then act accordingly.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
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.

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:
Level 15
Joined
Nov 30, 2007
Messages
1,202
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:
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.

JASS:
    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.

JASS:
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
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
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.

JASS:
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. :]


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
Commands should return at a minimum the actual label
 
Last edited:
Top