1. Are you planning to upload your awesome spell or system to Hive? Please review the rules here.
    Dismiss Notice
  2. The Aftermath has been revealed for the 19th Terraining Contest! Be sure to check out the Results and see what came out of it.
    Dismiss Notice
  3. Melee Mapping Contest #3 - Results are out! Congratulate the winners and check plenty of new 4v4 melee maps designed for this competition!
    Dismiss Notice
  4. The winners of our cinematic soundtrack competition have been decided! Step by the Music Contest #11 - Results to check the entries and congratulate the winners!
    Dismiss Notice

Spell Menu System 1.2c

Submitted by Pinzu
This bundle is marked as approved. It works and satisfies the submission rules.
This is a system for creating dynamic spell based options menu for each player, just as if it were a dialog menu. The sample map contains a Music Menu, Votekick Menu, Hero Pick (GUI) and Bag system. You can of course create any menu and nest menus using the provided API.

IcemanBo - Made the cooldown timer management used.
Pyrogasm - Helpful in figuring out how to make the system.
Overfrost - Pointing out points of improvement
Sir Moriarty - Pointing out points of improvement



Old Video:



Please do tell if you implement this anywhere, as I'd very much like to see it. :)

How to import

1) Copy the first 13 spells from O1 to O12 and the next spell. Note that the active spells are all based on Channel which means you just need to copy one and then change the order-string of each one so that no spell has the same.

Note that the disabled options use default neutral passive-spells. Check to see if you are using any of them and if thats the case change to another passive spell which you aren't using.

View attachment 315514

2) Copy the dummy unit.

3) Copy all the triggers listed under library

4) Finally match the Ability ID's from the object editor to the constants inside the globals block (press CTRL-D to see the ID). A stands for Active and D foor Disabled.
Code (vJASS):

library SpellOptionsMenu
    // ---------------------------------------------------------------------
    //    Configurable Block
    // ---------------------------------------------------------------------
    globals
        private constant integer A_1                    = 'A000'
        private constant integer A_2                    = 'A001'
        private constant integer A_3                    = 'A002'
        private constant integer A_4                    = 'A003'
        private constant integer A_5                    = 'A004'
        private constant integer A_6                    = 'A005'
        private constant integer A_7                    = 'A006'
        private constant integer A_8                    = 'A007'
        private constant integer A_9                    = 'A008'
        private constant integer A_10                    = 'A009'
        private constant integer A_11                    = 'A00A'
        private constant integer A_12                    = 'A00B'
        private constant integer A_NEXT                    = 'A00C'


Documentation

Code (vJASS):

library SpellOption /*

    v.1.1b

    Made by: Pinzu
 
    */
requires             /*
 
    */
Table            /*     hiveworkshop.com/threads/snippet-new-table.188084/
 
    */
optional PlayerUtils     /*    https://www.hiveworkshop.com/threads/playerutils.278559/
 
    */
optional TimerUtils         /*     http://www.wc3c.net/showthread.php?t=101322
 
 
    // ---------------------------------------------------------------------
 
    This allows for the creation and manipulation of SpellOption items. Option properties are
    stored inside a table and have both a global and local attribute. This means that you can
    display different things for different players using player id for reference.
 
    // ---------------------------------------------------------------------
 
    Credits:
 
        IcemanBo         - For creating the base of the Cooldown Manager.
        Sir Moriarty     - Pointing out some errors in the code
        Pyrogasm         - Helpful in figuring out how to make the system
        Overfrost         - Helpful in the improvement of the system
 
    // ---------------------------------------------------------------------

    Notice:
 
    1)     To prevent green icons when an option is disabled the icon used must have a DISPAS version.
        Example: https://www.hiveworkshop.com/threads/btnelectroburst.310351/
 
    2)    The Icon paths you use must have two backslashes to work correctly, like so: \\
 
    3)     if an option has no btn assigned it will use DEFAULT_BTN.
 
    4)     The system does not deallocate local data inside options when a player leaves. This must either
        be ignored or handled manually by using the method option.removeLocalChanges(playerId).

    */

 
    // ---------------------------------------------------------------------
    //    Configurable Block
    // ---------------------------------------------------------------------
    globals
        private constant string DEFAULT_BTN        = "ReplaceableTextures\\CommandButtons\\BTNHeal.blp"
 
        /*     Remove the comments below to run a textmacro which will override the default GUI variables
            with vJASS ones. Example:
 
            udg_SBM_optionKey --> SBM_optionKey
        */

 
        //    //! runtextmacro JASS_VARS()
 
        /*
            SBM_GUI_PLUGIN is an option to enable additional GUI support variables
            that allows global access to the last created menu and last created option.
            This is not necessary if you don't plan to create menus using GUI.
 
            - udg_SBM_lastCreatedMenu
            - udg_SBM_lastCreatedOption
            - udg_SBM_customData
        */

        constant boolean SBM_GUI_PLUGIN                = true
 
    endglobals
    // ---------------------------------------------------------------------
    //     End configuration
    // ---------------------------------------------------------------------
 
    //! novjass
 
    API
    ------------

    // Constants
    constant integer EVENT_SPELL_OPTION_SELECTED            // Option selection event id
    constant integer EVENT_SPELL_OPTION_CD_END            // Option cooldown ended event id
 
    // Note: These variables are prefixed with udg_ if the textmacro JASS_VARS never ran.
 
    SpellOption     SBM_optionKey                            // Triggering Option
    SpellMenu        SBM_menuKey                                // Trigger Menu
    player             SBM_player                                // Triggering Player
    real             SBM_event                                // Real Event
 
    /*
        Used to register callbacks for Any Spell Option Events, such as when a cooldown ends or an option is selected.
    */

    function OnSpellOptionEvent takes real whichEvent, code c returns nothing
 
    /*
        Wrapper returning the last triggered option, be that on selection or when a cooldown ends.
    */

    function GetTriggerSpellOption takes nothing returns SpellOption
 
    /*
        Wrapper returning the relevant player when a option is triggered.
    */

    function GetTriggerSpellOptionPlayer takes nothing returns player

 
    struct SpellOption
 
        Global data
        ---------------
 
        integer userData     // This is a custom data you can use to attach references, same as you would using for example SetUnitUserData.
 
        integer rank         // Custom data which can be used when sorting options.
         
        string label         // Option header
 
        string text         // Option description
 
        string icon         // Icon path, used when the option is enabled
 
        boolean enabled     // Flag for toggling if the option should be active
 
        boolean hidden         // Flag for if the option should be displayed
 
        integer cooldown     // Cooldown duration when the option is selected
 
        /*
            Creates an option with the given attributes. The callback code is executed whenever the option is selected.
            It's not requirement as you can use real-event listeners as an alternative.
        */

        static method create takes string label, string text, string btn, string disbtn, code callback returns thistype
 
        /*
            Used to change the callback that triggers when the option is used, null will remove any previous callback.
        */

        method onResponse takes code callback returns nothing
 
        /*
            Deallocates all data associated with the option.
        */

        method destroy takes nothing returns nothing
 
        /*
            Used to trigger a callback when a player uses an option.
        */

        method select takes player p returns nothing
 
        Local data            //This section will only manipulate local data for individual players
        ---------------
 
        /*
            Changes the option attribute for local player by matching id.
        */

        method setLocalLabel takes integer playerId, string s returns nothing
        method setLocalText takes integer playerId, string s returns nothing
        method setLocalIcon takes integer playerId, string s returns nothing
        method setLocalEnabled takes integer playerId, boolean b returns nothing
        method setLocalHidden takes integer playerId, boolean b returns nothing
        method setLocalCooldown takes integer playerId, integer seconds returns nothing
 
        /*
            Returns the current option attribute for local player by matching id.
        */

        method getLocalLabel takes integer playerId returns string
        method getLocalText takes integer playerId returns string
        method getLocalIcon takes integer playerId returns string
        method getLocalEnabled takes integer playerId returns boolean
        method getLocalHidden takes integer playerId returns boolean
        method getLocalCooldown takes integer playerId returns integer
 
        /*
            Removes any local option changes, reverting back to normal for local player by matching id
        */

        method removeLocalLabel takes integer playerId returns nothing
        method removeLocalText takes integer playerId returns nothing
        method removeLocalIcon takes integer playerId returns nothing
        method removeLocalEnabled takes integer playerId returns nothing
        method removeLocalHidden takes integer playerId returns nothing
        method removeLocalCooldown takes integer playerId returns nothing
 
        /*
            Removes all local changes for matching player, reverting to global option attributes.
            Does not effect active cooldown.
        */

        method removeLocalChanges takes integer playerId returns nothing
 
        /*
            Returns the current icon that is shown for a given player. Depending on state
            such as if the option is enabled, disabled or has a cooldown.
        */

        method getLocalIcon takes integer playerId returns string
 
        /*
            Removes option cooldown for a given player.
        */

        method stopCooldown takes integer playerId returns nothing
 
        /*
            Starts option cooldown  for a given player.
        */

        method startCooldown takes integer playerId returns nothing
 
        /*
            Clears all active cooldowns from the option.
        */

        method stopAllCooldowns takes nothing returns nothing
 
    //! endnovjass
endlibrary

library SpellOptionsMenu /*

    v.1.2d

    Made by: Pinzu
 
    */
requires                     /*
 
    */
SpellOption                    /*     (from triggers)
 
    */
ArrayList                    /*     https://www.hiveworkshop.com/threads/arraylist.312512/
 
    */
optional PlayerUtils         /*    https://www.hiveworkshop.com/threads/playerutils.278559/
 
    */
optional TimerUtils             /*     http://www.wc3c.net/showthread.php?t=101322  
     
    // ---------------------------------------------------------------------
 
    This allows for the creation and manipulation of spell based option menus. A menu can contain any amount
    of options inside them with different local properties. Options can also be locked to positions between
    0 and 10, which means that they will be visible at that position across all pages.
 
    // ---------------------------------------------------------------------
 
    Credits:
 
        IcemanBo         - For creating the base of the Cooldown Manager.
        Sir Moriarty     - Pointing out some errors in the code
        Pyrogasm         - Helpful in figuring out how to make the system
        Overfrost         - Helpful in the improvement of the system
 
    // ---------------------------------------------------------------------
     
    Notice:
 
    1)     When the menu is closed the previous selection is restored.
 
    2)     When a menu is destroyed all options held inside are deallocated, sharing options across multiple
        menus is possible but considered dangerous and not recommended.

*/
 
    // ---------------------------------------------------------------------------------------- \\
    //    Configurable Block                                                                      \\
    // ---------------------------------------------------------------------------------------- \\
 
    globals
 
    // **************************************************************************************** \\
    // Menu Dummy                                                                               \\
    // **************************************************************************************** \\  
    // Configure which dummy unit should be used as menu and where the menu is located. It's    \\
    // unlikely that you would ever need to change the coordinates from (0,0) as it will spawn  \\
    // on both water and black boundary.                                                        \\
    // **************************************************************************************** \\
 
        private constant integer MENU_DUMMY             = 'n000'
        private constant real MENU_SPAWN_X                = 0
        private constant real MENU_SPAWN_Y                = 0
 
    // **************************************************************************************** \\
    // Active Spells                                                                            \\
    // **************************************************************************************** \\
    // These spells are based on Channel ability. You need to copy one of them to your map and  \\
    // and make copies of it. The only requirement for it to work is that no spell use the same \\
    // order string (Text - Order String - Use/Turn On).                                        \\
        //                                                                                        \\
    // If you don't like that the spells are instant cast you can simply add a casting duration \\
    // to them. Don't forget ot make sure the hotkeys below match those assigned in the editor. \\
    // **************************************************************************************** \\
 
        // Spells
        private constant integer A_1                    = 'A00G'
        private constant integer A_2                    = 'A00H'
        private constant integer A_3                    = 'A00I'
        private constant integer A_4                    = 'A00J'
        private constant integer A_5                    = 'A00K'
        private constant integer A_6                    = 'A00L'
        private constant integer A_7                    = 'A00N'
        private constant integer A_8                    = 'A00M'
        private constant integer A_9                    = 'A00P'
        private constant integer A_10                    = 'A00O'
        private constant integer A_11                    = 'A00Q'
        private constant integer A_12                    = 'A00R'
        private constant integer A_NEXT                    = 'A000'  
     
        // Hotkeys
        private constant string H_1                        = "Q"
        private constant string H_2                        = "W"
        private constant string H_3                        = "E"
        private constant string H_4                        = "R"
        private constant string H_5                        = "A"
        private constant string H_6                        = "S"
        private constant string H_7                        = "D"
        private constant string H_8                        = "F"
        private constant string H_9                        = "Z"
        private constant string H_10                    = "X"
        private constant string H_11                    = "C"
        private constant string H_12                    = "V"
     
        private constant string HOTKEY_COLOR            = "|cffffcc00"
        private constant string HOTKEY_PREFIX            = "["
        private constant string HOTKEY_POSTFIX            = "|r]"
     
    // **************************************************************************************** \\
    // Cooldown                                                                         \\
    // **************************************************************************************** \\
     
        private constant string COOLDOWN_COLOR        = "|c00959697"
        private constant string COOLDOWN_LABEL        = COOLDOWN_COLOR + "Cooldown: "
     
    // **************************************************************************************** \\
    // Next Option (skip this)                                                                  \\
    // **************************************************************************************** \\
     
        // private constant string NEXT_DEFAULT_LABEL    = "Next"
        // private constant string NEXT_DEFAULT_DESC    = "Switches to the next menu page."
        // private constant string NEXT_DEFAULT_BTN        = "ReplaceableTextures\\CommandButtons\\BTNReplay-Loop.blp"
        // private constant string NEXT_DEFAULT_DISBTN    = "ReplaceableTextures\\CommandButtonsDisabled\\DISBTNReplay-Loop.blp"
 
    // **************************************************************************************** \\
     
        private constant real DESELECT_CHECK_RATE        = 0.1    // How frequent deselection events should be checked for
     
        // Enables the functionality for closing a menu when ESC is pressed
        private constant boolean CLOSE_ON_ESC            = true
     
    endglobals
 
    /*  
        You can modify this code if you wish to change the format of cooldown display.
        For example if you wish the output to be mm:ss or something similar.
    */

    private function FormatTime takes integer seconds returns string
        return I2S(seconds)
    endfunction
 
    /*
        You can modify this part if you wish to change how options are valued during sorting.
    */

 
    private function SortValuation takes SpellOption option returns integer
        return option.rank
    endfunction

    // ---------------------------------------------------------------------------------------- \\
    //    End Configurable                                                                        \\
    // ---------------------------------------------------------------------------------------- \\
 
    //! novjass
 
    API
    ------------
 
    /*
        Wrapper returning the last triggered menu.
    */

    function GetTriggerSpellMenu takes nothing returns SpellMenu

    /*
        Wrapper returning the player triggering a menu event.
    */

    function GetTriggerSpellMenuPlayer takes nothing returns player
 
    /*
        Used to register callbacks for Any Spell Menu Events, such as when a menu opens or closes.
    */

    function OnSpellMenuEvent takes real whichEvent, code c returns nothing
 
    /*
        This will return the menu dummy unit, you should be careful what operations you perform on it.
    */

    function GetSpellMenuUnit takes integer playerId returns unit
 
    /*
        Returns the current open menu page for the player by matching id
    */

    function GetCurrentSpellMenuPage takes integer playerId returns integer

    /*
        Returns the current open menu for the given player.
    */

    function GetCurrentSpellMenu takes integer playerId returns SpellMenu
 
    struct SpellMenu

        string name            // The menu name
 
        boolean forceOpen     // Disables selection while the menu is opened, until a menu.close(player) is fired
 
        /*
            Allocates a menu instance with the provided title.
        */

        static method create takes string title returns thistype
 
        /*
            Deallocates the menu and the options held inside.
        */

        method destroy takes nothing returns nothing
 
        /*
            Returns the current index position of a option inside the menu, -1 if it doesn't exist.
        */

        method indexOf takes SpellOption option returns integer

        /*
            Inserts an option at the index position, if its outside bounds the option will be appended last.
        */

        method add takes integer index, SpellOption option returns nothing

        /*
            Returns and pushes out a option at the index from the menu.
        */

        method eject takes integer index returns SpellOption

        /*
            Removes the option, destroying it in the process. Dangling references will be created for
            any references outside the menu to the removed option.
        */

        method remove takes integer index returns nothing

        /*
            Returns the option at the provided index.
        */

        method get takes integer index returns SpellOption
 
        /*
            Returns the amount of options held inside the menu.
        */

        method size takes nothing returns integer

        /*
            Clears the menu of options and removing them in the process.
        */

        method clear takes nothing returns nothing
 
        /*
            Returns true if a option is held inside the menu.
        */

        method contains takes SpellOption option returns boolean
 
        /*
            Swaps position between two options at the provided indexes.
        */

        method swap takes integer indexA, integer indexB returns nothing

        /*
            This will sort the menu using quicksort in descending or ascending order based on the user data
            the option has. If you want to sort anything different from a integer hashing would be required.
            Note that this feature can be omitted if INCLUDE_SORTING is set to false.
        */

        method sort takes boolean ascending returns nothing
 
        /*
            Wrapper for creating an option and inserting it at the provided position.
        */

        method createOption takes integer index, string label, string text, string btn, code callback returns SpellOption

        /*
            Locks a option to a specific menu position between 0 and 10, meaning that the option will appear
            at the same position across all menu pages.
        */

        method lock takes SpellOption option, integer position returns nothing
 
        /*
            Unlocks a locked option.
        */

        method unlock takes SpellOption option returns nothing

        /*
            Returns true if a option is locked.
        */

        method isLocked takes SpellOption option returns boolean
 
        /*
            This will set the menu page of the given player, can be used to switch between pages.
            If the page is outside is outside of bounds the page will simply be set to the first page.
        */

        method showPage takes player p, integer page returns nothing
 
        /*
            This will refresh the menu for all players currently viewing it. This can be useful in cases where
            the same option is visible to multiple players and must be changed globally. This is due refreshes only
            being executed for the player that selected the option.
        */

        method refresh takes nothing returns nothing

        /*
            Opens the menu for the given player.
        */

        method open takes player p returns nothing
 
        /*
            Closes the menu for the given player.
        */

        method close takes player p returns nothing
 
    //! endnovjass
endlibrary
 

The examples provided here may or may not be using depreciated code, you have to look at the map to get whats upt o date.
Example Usage

  • CreateMenu
    • Events
      • Map initialization
    • Conditions
    • Actions
      • -------- CREATE MENU --------
      • Set SBM_GUI_title = Hero Selection
      • Trigger - Run CreateSpellMenu <gen> (ignoring conditions)
      • Set heroMenu = SBM_lastCreatedMenu
      • -------- - --------
      • -------- Paladin --------
      • -------- - --------
      • Set SBM_GUI_index = 0
      • Set SBM_GUI_label = Paladin
      • Set SBM_GUI_text = Warrior Hero, exceptional at defense and augmenting nearby friendly troops. Can learn Holy Light, Divine Shield, Devotion Aura and Resurrection. |n|n|cffffcc00Attacks land units.|r
      • Set SBM_GUI_menu = heroMenu
      • Set SBM_GUI_btn = ReplaceableTextures\CommandButtons\BTNHeroPaladin.blp
      • Set SBM_GUI_disbtn = ReplaceableTextures\CommandButtons\DISBTNHeroPaladin.blp
      • Set SBM_customData = SBM_GUI_index
      • Set heroType[SBM_GUI_index] = Paladin
      • Trigger - Run AddSpellOption <gen> (ignoring conditions)
      • Set exampleOptions[SBM_GUI_index] = SBM_lastCreatedOption
      • -------- - --------
      • -------- Blood Mage --------
      • -------- - --------
      • Set SBM_GUI_index = 1
      • Set SBM_GUI_label = Blood Mage
      • Set SBM_GUI_text = Mystical Hero, adept at controlling magic and ranged assaults. Can learn Flame Strike, Banish, Siphon Mana and Phoenix. |n|n|cffffcc00Attacks land and air units.|r
      • Set SBM_GUI_menu = heroMenu
      • Set SBM_GUI_btn = ReplaceableTextures\CommandButtons\BTNHeroBloodElfPrince.blp
      • Set SBM_GUI_disbtn = ReplaceableTextures\CommandButtons\DISBTNHeroBloodElfPrince.blp
      • Set SBM_customData = SBM_GUI_index
      • Set heroType[SBM_GUI_index] = Blood Mage
      • -------- - --------
      • Trigger - Run AddSpellOption <gen> (ignoring conditions)
      • Set exampleOptions[SBM_GUI_index] = SBM_lastCreatedOption
      • -------- - --------
      • -------- Archmage --------
      • -------- - --------
      • Set SBM_GUI_index = 2
      • Set SBM_GUI_label = Archmage
      • Set SBM_GUI_text = Mystical Hero, adept at ranged assaults. Can learn Blizzard, Summon Water Elemental, Brilliance Aura and Mass Teleport. |n|n|cffffcc00Attacks land and air units.|r
      • Set SBM_GUI_menu = heroMenu
      • Set SBM_GUI_btn = ReplaceableTextures\CommandButtons\BTNHeroArchMage.blp
      • Set SBM_GUI_disbtn = ReplaceableTextures\CommandButtons\DISBTNHeroArchMage.blp
      • -------- We save the unit type as custom data to be transfered --------
      • Set SBM_customData = SBM_GUI_index
      • Set heroType[SBM_GUI_index] = Archmage
      • Trigger - Run AddSpellOption <gen> (ignoring conditions)
      • Set exampleOptions[SBM_GUI_index] = SBM_lastCreatedOption
      • -------- - --------
      • -------- Mountain King --------
      • -------- - --------
      • Set SBM_GUI_index = 3
      • Set SBM_GUI_label = Mountain King
      • Set SBM_GUI_text = Mystical Hero, adept at ranged assaults. Can learn Blizzard, Summon Water Elemental, Brilliance Aura and Mass Teleport. |n|n|cffffcc00Attacks land and air units.|r
      • Set SBM_GUI_menu = heroMenu
      • Set SBM_GUI_btn = ReplaceableTextures\CommandButtons\BTNHeroMountainKing.blp
      • Set SBM_GUI_disbtn = ReplaceableTextures\CommandButtons\DISBTNHeroMountainKing.blp
      • Set SBM_customData = SBM_GUI_index
      • Set heroType[SBM_GUI_index] = Mountain King
      • Trigger - Run AddSpellOption <gen> (ignoring conditions)
      • Set exampleOptions[SBM_GUI_index] = SBM_lastCreatedOption
      • -------- - --------
      • -------- OPEN THE MENU FOR PLAYING PLAYERS --------
      • -------- - --------
      • Game - Display to (All players) the text: Chose your hero...
      • For each (Integer A) from 1 to 24, do (Actions)
        • Loop - Actions
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • ((Player((Integer A))) controller) Equal to User
              • ((Player((Integer A))) slot status) Equal to Is playing
            • Then - Actions
              • Set SBM_player = (Player((Integer A)))
              • Set SBM_GUI_menu = heroMenu
              • Trigger - Run OpenMenu <gen> (ignoring conditions)
            • Else - Actions
  • CreateHero
    • Events
      • Game - SBM_event becomes Equal to 1.00
    • Conditions
      • SBM_menuKey Equal to heroMenu
    • Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • SBM_event Equal to 1.00
        • Then - Actions
          • Set tempLoc = (Center of Spawn <gen>)
          • Unit - Create 1 heroType[SBM_customData] for SBM_player at tempLoc facing Default building facing degrees
          • Set Hero[(Player number of SBM_player)] = (Last created unit)
          • Custom script: call RemoveLocation(udg_tempLoc)
          • Trigger - Run CloseMenu <gen> (ignoring conditions)
          • Skip remaining actions
        • Else - Actions
  • Repick
    • Events
      • Player - Player 1 (Red) types a chat message containing -repick as An exact match
      • Player - Player 2 (Blue) types a chat message containing -repick as An exact match
      • Player - Player 3 (Teal) types a chat message containing -repick as An exact match
      • Player - Player 4 (Purple) types a chat message containing -repick as An exact match
      • Player - Player 5 (Yellow) types a chat message containing -repick as An exact match
      • Player - Player 6 (Orange) types a chat message containing -repick as An exact match
      • Player - Player 7 (Green) types a chat message containing -repick as An exact match
      • Player - Player 8 (Pink) types a chat message containing -repick as An exact match
      • Player - Player 9 (Gray) types a chat message containing -repick as An exact match
      • Player - Player 10 (Light Blue) types a chat message containing -repick as An exact match
      • Player - Player 11 (Dark Green) types a chat message containing -repick as An exact match
      • Player - Player 12 (Brown) types a chat message containing -repick as An exact match
      • Player - types a chat message containing -repick as An exact match
      • Player - types a chat message containing -repick as An exact match
      • Player - types a chat message containing -repick as An exact match
      • Player - types a chat message containing -repick as An exact match
      • Player - types a chat message containing -repick as An exact match
      • Player - types a chat message containing -repick as An exact match
      • Player - types a chat message containing -repick as An exact match
      • Player - types a chat message containing -repick as An exact match
      • Player - types a chat message containing -repick as An exact match
      • Player - types a chat message containing -repick as An exact match
      • Player - types a chat message containing -repick as An exact match
      • Player - types a chat message containing -repick as An exact match
    • Conditions
    • Actions
      • Unit - Remove Hero[(Player number of (Triggering player))] from the game
      • Set SBM_player = (Triggering player)
      • Set SBM_GUI_menu = heroMenu
      • Trigger - Run OpenMenu <gen> (ignoring conditions)
Code (vJASS):

scope AnyEventRegistrationExample initializer Init
    // This will fire when a cooldown ends. Note that we cannot retrieve which menu the option belongs to.
    // However, such information could be stored using option.userData = whichMenu if necessary.
    //
    private function CDEnded takes nothing returns nothing
        local SpellOption option = GetTriggerSpellOption()
        local player user = GetTriggerSpellOptionPlayer()
        call BJDebugMsg(GetPlayerName(user) + " option cooldown ended " + I2S(option) + ".")
    endfunction

    // Option Selected event. Can be used in cases where you didn't register a callback
    // It was mostly implemented for potential GUI action.
    //
    private function Selected takes nothing returns nothing
        local SpellOption option = GetTriggerSpellOption()
        local player user = GetTriggerSpellOptionPlayer()
        local SpellMenu menu = GetTriggerSpellMenu()
        call BJDebugMsg(GetPlayerName(user) + " selected option '" + option.label + "' (" + I2S(option) + ") belonging to  '" + menu.name + "' (" + I2S(menu) + ")" )
    endfunction

    // This will fire whenever a option cooldown ticks, its mostly meant to be used internally by the system.
    //
    private function OnChange takes nothing returns nothing
        local SpellOption option = GetTriggerSpellOption()
        local player user = GetTriggerSpellOptionPlayer()
    endfunction

    // Triggers whenever a menu is opened, yes, it's pretty redundant.
    //
    private function OnOpen takes nothing returns nothing
        local SpellMenu menu = GetTriggerSpellMenu()
        local player user = GetTriggerSpellMenuPlayer()
        call BJDebugMsg(GetPlayerName(user) + " opened menu: " + menu.name + " (" + I2S(menu) + ")")
    endfunction

    // Triggers whenever a menu is closed.
    //
    private function OnClose takes nothing returns nothing
        local SpellMenu menu = GetTriggerSpellMenu()
        local player user = GetTriggerSpellMenuPlayer()
        call BJDebugMsg(GetPlayerName(user) + " closed menu: " + menu.name + " (" + I2S(menu) + ")")
    endfunction

    private function Init takes nothing returns nothing
        // Option events
        call OnSpellOptionEvent(EVENT_SPELL_OPTION_SELECTED, function Selected)
        call OnSpellOptionEvent(EVENT_SPELL_OPTION_CD_END, function CDEnded)
        call OnSpellOptionEvent(EVENT_SPELL_OPTION_CHANGE, function OnChange)
        // Menu events
        call OnSpellMenuEvent(EVENT_SPELL_MENU_OPEN, function OnOpen)
        call OnSpellMenuEvent(EVENT_SPELL_MENU_CLOSE, function OnClose)
    endfunction
endscope
 
1) To sort anything that isn't an integer you'd have to generate a hash-value and put it in the option userData.

2) To enable sorting you must go into SpellMenu Library and set
INCLUDE_SORTING = true

Code (vJASS):

scope SortExample initializer Init
    /*
        This is a demo of how to sort options inside a menu. It uses a quicksort to sort in either
        ascending or descending order. Haven't tested more than 400, but i believe at least dubble that should be fine.
    */

 
    globals
        private code sortAscending
        private code sortDescending
        private constant string MENU_NAME                 = "Sort Menu"
        private constant integer NUM_OPTIONS             = 100
    endglobals
 
    private function SortDescending takes nothing returns nothing
        local SpellMenu menu = GetTriggerSpellMenu()
        local integer i = 0
        local SpellOption o
        loop
            exitwhen i == menu.size()
            set o = menu.get(i)
            call o.onResponse(sortAscending)    // We change callback so that the second selection will reverse the order
            set o.text = "Press any option to sort the menu in ascending order."
            set i = i + 1
        endloop
        call menu.sort(false)    // Descending order
    endfunction
 
    private function SortAscending takes nothing returns nothing
        local SpellMenu menu = GetTriggerSpellMenu()
        local integer i = 0
        local SpellOption o
        loop
            exitwhen i == menu.size()
            set o = menu.get(i)
            call o.onResponse(sortDescending)    // We change callback so that the second selection will reverse the order
            set o.text = "Press any option to sort the menu in descending order."
            set i = i + 1
        endloop
        call menu.sort(true)    // Ascending order
    endfunction
 
    private function CreateMenu takes nothing returns nothing
        local SpellMenu menu = SpellMenu.create(MENU_NAME)
        local integer i = 0
        local SpellOption o
        local integer rdm
        loop
            exitwhen i == NUM_OPTIONS
            set rdm = GetRandomInt(0, 100)
            set o = menu.createOption(i, "Random value: " + I2S(rdm), "Press any option to sort the menu in ascending order." , "ReplaceableTextures\\CommandButtons\\BTNBanish.blp", "ReplaceableTextures\\CommandButtonsDisabled\\DISBTNBanish.blp", sortAscending)
 
            // Sorting is based on the value stored inside rank
            set o.rank = rdm
            set i = i + 1
        endloop
        call menu.open(GetTriggerPlayer())
    endfunction
 
    /*
        Remove the menu when the user is done with it
    */

    private function OnClose takes nothing returns nothing
        local SpellMenu menu = GetTriggerSpellMenu()
        if menu.name == MENU_NAME then
            call menu.destroy()
        endif
    endfunction
    private function Init takes nothing returns nothing
        set sortAscending = function SortAscending
        set sortDescending = function SortDescending
        call Command.register("-sort", function CreateMenu)
        call DisplayTimedTextToForce(GetPlayersAll(), 60.00, "Type \"-sort\" to open the menu.")
 
        call OnSpellMenuEvent(EVENT_SPELL_MENU_CLOSE, function OnClose)
    endfunction
endscope
 
 
Code (vJASS):

scope MusicMenuExample initializer Init
 
    globals
        private SpellMenu songMenu
        private SpellOption o
        private SpellOption volumeUp
        private SpellOption volumeDown
        private SpellOption openSongMenu
        private SpellOption startStop
        private string array songs
        private real array length
        private integer array playerVolume
        private boolean array playerMusicOn
    endglobals
 
    private function OpenMenu takes nothing returns nothing
        call songMenu.open(GetTriggerPlayer())
    endfunction
 
    private function SecondSelection takes nothing returns nothing
        call BJDebugMsg("Second callback!")
    endfunction
 
    private function MusicSelected takes nothing returns nothing
        local player p = GetTriggerPlayer()
        local integer pid = GetPlayerId(p)
        set o = GetTriggerSpellOption()
        if GetLocalPlayer() == p then
            call PlayMusic(songs[o.userData])
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0, 10, "Now playing " + songs[o.userData] + ".")
        endif
        // Modify player music status
        set playerMusicOn[pid] = true
        // Modify start-stop button to represent changes.
        call startStop.setLocalLabel(pid, "Pause")
        call startStop.setLocalText(pid, "Stops playing music.")
        call startStop.setLocalBtn(pid, "ReplaceableTextures\\CommandButtons\\BTNReplay-Pause.blp")
        call startStop.setLocalDisbtn(pid, "ReplaceableTextures\\CommandButtons\\DISBTNReplay-Pause.blp")
 
        // We can change callback after creation like this:
        // call o.onResponse(function SecondSelection)
    endfunction
 
    private function StartStop takes nothing returns nothing
        local player p = GetTriggerPlayer()
        local integer pid = GetPlayerId(p)
        set o = GetTriggerSpellOption()
 
        if playerMusicOn[pid] then
            if p == GetLocalPlayer() then
                call DisplayTimedTextToPlayer(p,0,0, 10, "Music paused.")
                call StopMusicBJ(false)
            endif
            // Modify option properties for a local player by id
            call o.setLocalLabel(pid, "Play")
            call o.setLocalText(pid, "Resume playing music.")
            call o.setLocalBtn(pid, "ReplaceableTextures\\CommandButtons\\BTNReplay-Play.blp")
            call o.setLocalDisbtn(pid, "ReplaceableTextures\\CommandButtons\\DISBTNReplay-Play.blp")
            set playerMusicOn[pid] = false
        else
            if p == GetLocalPlayer() then
                call DisplayTimedTextToPlayer(p,0,0, 10, "Music resumed.")
                call ResumeMusicBJ()
            endif
            // Modify option properties for a local player by id
            call o.setLocalLabel(pid, "Pause")
            call o.setLocalText(pid, "Stops playing music.")
            call o.setLocalBtn(pid, "ReplaceableTextures\\CommandButtons\\BTNReplay-Pause.blp")
            call o.setLocalDisbtn(pid, "ReplaceableTextures\\CommandButtons\\DISBTNReplay-Pause.blp")
            set playerMusicOn[pid] = true
        endif
    endfunction
 
    private function VolumeUp takes nothing returns nothing
        local player p = GetTriggerPlayer()
        local integer pid = GetPlayerId(p)
        if playerVolume[pid] == 0 then // enable button if at min
            call volumeDown.setLocalEnabled(pid, true)
        endif
        set playerVolume[pid] = playerVolume[pid] + 10
        if playerVolume[pid] > 100 then
            set playerVolume[pid] = 100
        endif
        if playerVolume[pid] == 100 then // disable button if at max
            call volumeUp.setLocalEnabled(pid, false)
        endif
        if p == GetLocalPlayer() then
            call DisplayTimedTextToPlayer(p,0,0, 10, "Volume increased to " + I2S(playerVolume[pid]) + "%.")
            call SetMusicVolumeBJ(playerVolume[pid])
        endif
        set p = null
    endfunction
 
    private function VolumeDown takes nothing returns nothing
        local player p = GetTriggerPlayer()
        local integer pid = GetPlayerId(p)
        if playerVolume[pid] == 100 then // enable button if at max
            call volumeUp.setLocalEnabled(pid, true)
        endif
        set playerVolume[pid] = playerVolume[pid] - 10
        if playerVolume[pid] < 0 then
            set playerVolume[pid] = 0
        endif
        if playerVolume[pid] == 0 then // disable button if at min
            call volumeDown.setLocalEnabled(pid, false)
        endif
        if p == GetLocalPlayer() then
            call DisplayTimedTextToPlayer(p,0,0, 10, "Volume decreased to " + I2S(playerVolume[pid]) + "%.")
            call SetMusicVolumeBJ(playerVolume[pid])
        endif
        set p = null
    endfunction
 
    private function Init takes nothing returns nothing
        local integer i
        local integer j
        local player p
        set songs[0] = gg_snd_ArthasTheme
        set length[0] = 123
        set songs[1] = gg_snd_War3XMainScreen
        set songs[2] = gg_snd_BloodElfTheme
        set songs[3] = gg_snd_Comradeship
        set songs[4] = gg_snd_Credits
        set songs[5] = gg_snd_DarkAgents
        set songs[6] = gg_snd_DarkVictory
        set songs[7] = gg_snd_Doom
        set songs[8] = gg_snd_HeroicVictory
        set songs[9] = gg_snd_Human1
        set songs[10] = gg_snd_Human2
        set songs[11] = gg_snd_Human3
        set songs[12] = gg_snd_HumanDefeat
        set songs[13] = gg_snd_HumanVictory
        set songs[14] = gg_snd_HumanX1
        set songs[15] = gg_snd_IllidansTheme
        set songs[16] = gg_snd_LichKingTheme
        set songs[17] = gg_snd_Mainscreen
        set songs[18] = gg_snd_NagaTheme
        set songs[19] = gg_snd_NightElf1
        set songs[20] = gg_snd_NightElf2
        set songs[21] = gg_snd_NightElf3
        set songs[22] = gg_snd_NightElfDefeat
        set songs[23] = gg_snd_NightElfVictory
        set songs[24] = gg_snd_NightElfX1
        set songs[25] = gg_snd_Orc1
        set songs[26] = gg_snd_Orc2
        set songs[27] = gg_snd_Orc3
        set songs[28] = gg_snd_OrcDefeat
        set songs[29] = gg_snd_OrcTheme
        set songs[30] = gg_snd_OrcVictory
        set songs[31] = gg_snd_OrcX1
        set songs[32] = gg_snd_PursuitTheme
        set songs[33] = gg_snd_SadMystery
        set songs[34] = gg_snd_Tension
        set songs[35] = gg_snd_TragicConfrontation
        set songs[36] = gg_snd_Undead1
        set songs[37] = gg_snd_Undead2
        set songs[38] = gg_snd_Undead3
        set songs[39] = gg_snd_UndeadDefeat
        set songs[40] = gg_snd_UndeadVictory
        set songs[41] = gg_snd_UndeadX1
        set songs[42] = gg_snd_War2IntroMusic
        set songMenu = SpellMenu.create("Music Menu")
        set i = 0
        loop
            exitwhen songs[i] == null
 
            set o = SpellOption.create("Play " + I2S(i + 1), "Track: " + songs[i], "ReplaceableTextures\\CommandButtons\\BTNBanish.blp", "ReplaceableTextures\\CommandButtonsDisabled\\DISBTNBanish.blp", function MusicSelected)
 
            // If you pass an invalid index it will be added at the last position.
            call songMenu.add(-1, o)
 
            // This is how you would add global cooldown to an option
            set o.cooldown = 15
 
            //     If we want to change Player(0) cooldown but maintain 15 second for the other players we could do this
            call o.setLocalCooldown(0, 20)
 
            // Similarly we could change other attributes such as label, text, btn, hidden, enabled and disabled...
 
            // Finally we can store custom ids as references to other data, in this case the option needs to be bound to a particular song
            // To achieve this we will store the index, i inside the option which we will later use to play the correct song.
 
            set o.userData = i
 
            // Note that id doesn't have any local method wrappers (if you have a need for this do poke me and I'll consider changing it).
 
            set i = i + 1
        endloop
 
        set startStop =  SpellOption.create("Play", "Resumes player music.", "ReplaceableTextures\\CommandButtons\\BTNReplay-Play.blp", "ReplaceableTextures\\CommandButtons\\DISBTNReplay-Play.blp", function StartStop)
        call songMenu.add(0, startStop)
        call songMenu.lock(startStop, 0)    // We lock this option to index 0, it will be transfered to all pages of the menu
 
        set volumeUp = SpellOption.create("Increase Volume", "Increases the volume by 10%.", "ReplaceableTextures\\CommandButtons\\BTNReplay-SpeedUp.blp", "ReplaceableTextures\\CommandButtonsDisabled\\DISBTNReplay-SpeedUp.blp", function VolumeUp)
        call songMenu.add(3, volumeUp)
        set volumeUp.enabled = false
        call songMenu.lock(volumeUp, 3)
 
        set volumeDown = SpellOption.create("Decrease Volume", "Decreases the volume by 10%.", "ReplaceableTextures\\CommandButtons\\BTNReplay-SpeedDown.blp", "ReplaceableTextures\\CommandButtonsDisabled\\DISBTNReplay-SpeedDown.blp", function VolumeDown)
        call songMenu.add(7, volumeDown)
        call songMenu.lock(volumeDown, 7)
 
 
        // Open Menu on command
        call Command.register("-music", function OpenMenu)
        call DisplayTimedTextToForce(GetPlayersAll(), 60.00, "Type \"-music\" to open the menu." )
 
        call StopMusicBJ(false)    // Stop music at start
 
        set i = 0
        loop
            exitwhen i == bj_MAX_PLAYER_SLOTS
            set p = Player(i)
            if (GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING and /*
                    */
GetPlayerController(p) == MAP_CONTROL_USER) then
                set j = GetPlayerId(p)
                set playerVolume[j] = 100
                set playerMusicOn[j] = false
            endif
            set i = i + 1
        endloop
 
    endfunction
endscope
 
Code (vJASS):

library Votekick uses SpellOptionsMenu
    /*
        This is a votekick system that can be implemented as is with a few changes of the variables in the globals:
 
        1) DEBUG should be set to false
 
        2) MIN_VOTES should be set to 2 or 3 and is the minimum number of voters needed.
 
    */

    globals
        private constant string KICK_BTN             = "ReplaceableTextures\\CommandButtons\\BTNHire.blp"
        private constant string KICK_DISBTN            = "ReplaceableTextures\\CommandButtons\\DISBTNHire.blp"
        private constant integer MAX_PLAYERS         = 24
        private constant string MENU_TITLE            = "Kick Menu"
        private constant real VOTE_TIME                = 45.
        private constant real MSG_DURATION            = 15.
 
        // The minimum required vote ratio to kick
        private constant real VOTE_REQ                = 0.8
 
        // Reduced vote ratio requirement per playing player, the more players the less required votes is the idea here.
        private constant real DEC_PER_PLAYER        = 0.01
 
        // Must be changed!
        private constant integer MIN_VOTES            = 0            // Should be 2 or higher
        private constant boolean DEBUG                 = true        // Should be false
 
        private string array colors
    endglobals
 
    /*
        You can modify the player colors below
    */

    private function SetupPlayerColors takes nothing returns nothing
        set colors[0] = "|c00FF0000"
        set colors[1] = "|cff0042ff"
        set colors[2] = "|cff00ffff"
        set colors[3] = "|cff660066"
        set colors[4] = "|cffffff00"
        set colors[5] = "|cffff8000"
        set colors[6] = "|cff00ff00"
        set colors[7] = "|cffff66cc"
        set colors[8] = "|cff999999"
        set colors[9] = "|cff6699cc"
        set colors[10] = "|cff006633"
        set colors[11] = "|cff4a2a04"
        set colors[12] = "|cff9b0000"
        set colors[13] = "|cff0000c3"
        set colors[14] = "|cff00eaff"
        set colors[15] = "|cffbe00fe"
        set colors[16] = "|cffebcd87|r"
        set colors[17] = "|cfff8a48b"
        set colors[18] = "|cffbfff80"
        set colors[19] = "|cffdcb9eb"
        set colors[20] = "|cff282828"
        set colors[21] = "|cffebf0ff"
        set colors[22] = "|cff00781e"
        set colors[23] = "|cffa46f33"
    endfunction
 
    private struct Votekick
        private static Table table
        private static player targetPlayer = null
        private static integer yes
        private static integer no
        private static integer required
        private static boolean array hasVoted
        private static timer t
 
        private SpellMenu menu
        private SpellOption array options[MAX_PLAYERS]
 
        method getSelectedPlayer takes SpellOption selectedOption returns player
            local SpellOption o
            local integer i = 0
            loop
                exitwhen i == MAX_PLAYERS
                set o = .options[i]
                if o == selectedOption then
                    return Player(i)
                endif
                set i = i + 1
            endloop
            return null
        endmethod
 
        method destroy takes nothing returns nothing
            call thistype.table.remove(.menu)
            call .menu.destroy()
            call .deallocate()
        endmethod
 
        private static method countPlayers takes nothing returns integer
            local player p
            local integer i = 0
            local integer count = 0
            loop
                exitwhen i == MAX_PLAYERS
                set p = Player(i)
                if GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING and /*
                */
GetPlayerController(p) == MAP_CONTROL_USER then
                    set count = count + 1
                endif
                set i = i + 1
            endloop
            set p = null
            return count
        endmethod
 
        private static method getVotesNeeded takes nothing returns integer
            local integer votes = R2I(countPlayers()*(VOTE_REQ - DEC_PER_PLAYER*.countPlayers()))
            if votes < MIN_VOTES then
                return MIN_VOTES
            endif
            return votes
        endmethod
 
        private static method onTimerExpires takes nothing returns nothing
            local integer pid = GetPlayerId(.targetPlayer)
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,MSG_DURATION, "A votekick against " + colors[pid] + GetPlayerName(.targetPlayer) + "|r has expired.")
            call PauseTimer(.t)
            call DestroyTimer(.t)
            set .t = null
            set .targetPlayer = null
        endmethod
 
        private static method voteYes takes player p returns nothing
            local integer pid = GetPlayerId(p)
            set .yes = .yes + 1
            set .hasVoted[GetPlayerId(p)] = true
            call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,MSG_DURATION, colors[pid] + GetPlayerName(p) + "|r voted yes (" + I2S(.yes) + "/" + I2S(.required) + ").")
            if .yes >= .required then
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,MSG_DURATION, colors[GetPlayerId(.targetPlayer)] + GetPlayerName(.targetPlayer) + "|r was kicked.")
                call CustomDefeatBJ(.targetPlayer, "You have been kicked!" )
                call PauseTimer(.t)
                call DestroyTimer(.t)
                set .t = null
                set .targetPlayer = null
            endif
        endmethod
 
        private static method onYes takes nothing returns nothing
            local player p = GetTriggerPlayer()
            local integer pid = GetPlayerId(p)
            if thistype.targetPlayer != null and not thistype.hasVoted[pid] then
                call thistype.voteYes(p)
            endif
            set p = null
        endmethod
 
        private static method onNo takes nothing returns nothing
            local player p = GetTriggerPlayer()
            local integer pid = GetPlayerId(p)
            if thistype.targetPlayer != null and not thistype.hasVoted[pid] then
                set thistype.no = thistype.no + 1
                set thistype.hasVoted[pid] = true
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,MSG_DURATION, colors[pid] + GetPlayerName(p) + "|r voted no.")
            endif
            set p = null
        endmethod
 
        private static method onSelection takes nothing returns nothing
            local player p = GetTriggerPlayer()
            local SpellMenu m = GetTriggerSpellMenu()
            local thistype this = thistype.table[m]
            local player selectedPlayer = .getSelectedPlayer(GetTriggerSpellOption())
            local integer i
            call m.close(p)
            if thistype.targetPlayer != null then
                if GetLocalPlayer() == p then
                    call DisplayTimedTextToPlayer(p,0,0,MSG_DURATION, "A votekick is already in session...")
                endif
            else
                set thistype.yes = 0
                set thistype.no = 0
                set i = 0
                loop
                    exitwhen i == MAX_PLAYERS
                    set thistype.hasVoted[i] = false
                    set i = i + 1
                endloop
                set thistype.required = thistype.getVotesNeeded()
                set thistype.targetPlayer = selectedPlayer
                set thistype.t = CreateTimer()
                call TimerStart(thistype.t, VOTE_TIME, false, function thistype.onTimerExpires)
                call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,VOTE_TIME, "A votekick session against  " + colors[GetPlayerId(selectedPlayer)] + GetPlayerName(selectedPlayer) + "|r has started. Type '-yes' or '-no' to vote.")
                call thistype.voteYes(p)
            endif
            set p = null
            set selectedPlayer = null
        endmethod
 
        private static method onClose takes nothing returns nothing
            local SpellMenu m = GetTriggerSpellMenu()
            local thistype this
            if m.name == MENU_TITLE then
                set this = thistype.table[m]
                call this.destroy()
            endif
        endmethod
 
        private static method onMenuOpen takes nothing returns nothing
            local player instigator = GetTriggerPlayer()
            local thistype this
            local player p
            local integer i
            if thistype.countPlayers() <= thistype.getVotesNeeded() then
                call DisplayTimedTextToPlayer(instigator,0,0,MSG_DURATION, "Not enough players to instigate a votekick.")
                set instigator = null
                return
            endif
            set this = .allocate()
            set this.menu = SpellMenu.create(MENU_TITLE)
            set thistype.table[this.menu] = this
            set i = 0
            loop
                exitwhen i == MAX_PLAYERS
                set p = Player(i)
                if (GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING and /*
                    */
GetPlayerController(p) == MAP_CONTROL_USER and /*
                    */
instigator != p) or DEBUG then
                    set this.options[i] =  menu.createOption(i, "Kick Player " + I2S(i + 1), "Starts a votekick against " +  colors[i] +GetPlayerName(p) + "|r.", KICK_BTN, KICK_DISBTN, function thistype.onSelection)
                endif
                set i = i + 1
            endloop
            call menu.open(instigator)
            set instigator = null
            set p = null
        endmethod
 
        private static method onInit takes nothing returns nothing
            local trigger trgClose = CreateTrigger()
            call TriggerRegisterVariableEvent(trgClose, "udg_SBM_event", EQUAL, EVENT_SPELL_MENU_CLOSE)
            call TriggerAddAction(trgClose, function thistype.onClose)
            set thistype.table = Table.create()
 
            call Command.register("-votekick", function thistype.onMenuOpen)
            call Command.register("-yes", function thistype.onYes)
            call Command.register("-no", function thistype.onNo)
 
            call SetupPlayerColors()
 
            call DisplayTimedTextToForce(GetPlayersAll(), 60.00, "Type \"-votekick\" to kick a player." )
        endmethod
    endstruct
endlibrary
 


Notes

1.2d
- Updated the ArrayList library and merged the sort code with the external library.

1.2c
- Stopped using SetPlayerAbilityAvailable and instead use BlzUnitDisableAbility.
- No longer need to use 12 passive spells for disabled state.Thus also fixing the bug of passive spells potentially being effected.
- No longer needs to store or configure DISBTN icons.
- Removed: Option.getLocalIcon(playerId)
- Removed: Option.disbtn
- Removed: Option.setLocalDisbtn(playerId)
- Removed: Option.getLocalDisbtn(playerId)
- Removed: Option.removeLocalDisbtn(playerId)
- Fixed: Small local effect error in the Hero Selection Plugin
- Changed: Option.btn --> Option.icon and Option.getLocalBtn --> Option.getLocalIcon, etc.
1.2
- Added: function GetCurrentSpellMenuPage takes integer playerId returns integer
- Added: function GetCurrentSpellMenu takes integer playerId returns integer
- Removed: trgUsed from SpellMenu as it wasn't being used any more
- Fixed: Bug where the menu could not be opened inside a condition scope.
- Changed: The active abilities now use different variations of Channel rather than using multiple different types. This makes them all instant cast (thanks Overfrost)
- Changed: Hotkey is now configured in the code rather than inside the object editor
- Fixed: Bug where default icons weren't used when btn or disbtn is set to either null or "".

1.1d
- Added: option.rank for sorting rather than using option.userData
- Added: SortValuation function if one wishes to change sorting function
- Added: GUI Hero Pick System and reintroduced old GUI code.
- Fix: Forgot to pause a timer and use TimerUtils when allocating the timer on menu open/close.
- Fix: Added a filter for bag demo.
- Fix: Null trigger handle inside bag demo.
- Added: Feature for forcing the menu open until a manual close event occurs.
- Added: function GetSpellMenuUnit takes player playerId returns unit - In case anyone wishes to manipulate the dummy.

1.1
* SpellOption
- Changed the dummy unit to completely disapear from the map.
- Removed method operator Cooldown.icon takes nothing returns string
- Added Option.clearCooldown takes playerId
- Replaced .allocate() and .table with Table.create() and Table(this) respectively inside SpellOption.
- Improved the SpellOption documentation
- Added a custom ID to the SpellOption, the idea here is the same as GetUntiUserData so that one can attach other type of indexed data to that option more easily.
- Made SpellOption.trg private and added a callback method: select(player) instead.
- Only instantiates a cooldown manager for each playing player now.
- Added: function OnSpellOptionEvent takes real whichEvent, code c returns nothing
- Reworked TimerManagement so that cooldown ticks only update the spell being affected if watched.
- Reduced the textmacros inside SpellOption to one with a static if to handle a special case
- Added: method onResponse takes code callback returns nothing - changes which callback will fire on option selection
* SpellMenu
- Refactored SpellOptionsMenu almost entierly
- Fixed a bug where menus were being reselected (i think)
- Reduced the amount of levels used from 24 to 1 per spell
- Removed the need to copy any of the disabled abilities
- Removed unnecessary player util usage from initialization
- Added: eject(index) removes and returns the option instead of deallocating it as .remove(index) does
- Removed: createOption(...)
- Changed: lockOption --> lock, unlockOption --> unlock, removeOption --> remove optionBelongs --> contains, etc
- Added: SpellMenu.sort(ascending) which sorts all options based on option.userdata
- Added: SpellMenu.get(index)
- Added: SpellMenu.size()
- Fixed: bug with optional library not being included properly
- Improved documentation
- The menu now optionally closes when escape is pressed.
* General
- Made it possible to replace GUI variables with vJASS variables instead (should one want to remove GUI entierly)
- Now supports TimerUtils and PlayerUtils
- All GUI examples have been depreciated for now
1.01
- Added indexOf to SpellMenu
- Fixed bug with Locking not working properly
- Added BagSystem Plugin
1.00
- Added functionality for locking options at index between 0 to 11
- Window size now takes into account hidden options
- Fixed bug with BlzSetAbilityPositionXY not being set properly on configuration
0.99
- Added clear method to remove all options
- Opening another menu will now fire a close menu event before executing
- Fixed deallocation bug
- Added example of creating a menu based on items
0.98
- Fixed a bug with closeMenu not working
- When a menu closes the selection will return to what it was before
- Added votekick menu as an additional example
- Fixed a bug with the SpellMenu not being deallocated when destroy was called
0.97
- Fixed a bug in which the next button could be hidden due to disabled option at slot 11 covering it.
- Added start and end cooldown event registration.
- Changed how callbacks were registrated for faster setup.
0.95
- Ficed a bug where options weren't deallocated once removed from the menu.
- Added method refresh to spell menu so that the menu can be manually changed in case the properties change independant of player selection
- Now updates the menu for all players currently viewing it whenever an option is selected
- Added real events for option selection, menu open and menu close.
- Player related data is now removed from SpellOptionsMenu upon leaving the game
- showWindow(player, index) is now public so that the window can be changed from outside
- Added GUI plugin for additional support
- Added static if to include/exclude some GUI variables from the library
0.9 Release
Containing the bulk of the system and a Music Menu
In order of importance
  1. Add an override method to SpellOption for overriding the normal ability with a custom ability, enabling single target and AOE options. [ option.overrideAbility(abilCode) ]
  2. Make the menu nestable (unless flagged not to). Use a stack for storing previous menu states.
  3. Make the next option more customizeable.
  4. Improve the GUI wrappers
  5. Improve the automatic menu refreshing
Contents

Spell Menu System 1.2d (Map)

Reviews
Sir Moriarty
Reviewer's Summary: An extremely useful system for making advanced, flexible menus with an easy to use API and very good examples. I can personally easily picture myself using this system if I was still writing vJASS or Zinc. The library is both...
  1. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,160
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    I've fixed the most pressing issues with the upload now (v0.97). I also added more GUI support, including GUI-Plugin (trigger wrappers).
    • On Events
      • Events
        • Game - SBM_event becomes Equal to 1.00
        • Game - SBM_event becomes Equal to 2.00
        • Game - SBM_event becomes Equal to 3.00
      • Conditions
      • Actions
        • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
          • If - Conditions
            • SBM_event Equal to 1.00
          • Then - Actions
            • Game - Display to (All players) the text: ((Name of SBM_player) + ( selected menu + ((String(SBM_menuKey)) + (, option + (String(SBM_optionKey))))))
            • Skip remaining actions
          • Else - Actions
        • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
          • If - Conditions
            • SBM_event Equal to 2.00
          • Then - Actions
            • Game - Display to (All players) the text: ((Name of SBM_player) + ( open + ((String(SBM_menuKey)) + <Empty String>)))
            • Skip remaining actions
          • Else - Actions
        • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
          • If - Conditions
            • SBM_event Equal to 3.00
          • Then - Actions
            • Game - Display to (All players) the text: ((Name of SBM_player) + ( closed + ((String(SBM_menuKey)) + <Empty String>)))
            • Skip remaining actions
          • Else - Actions
    • Creating a menu
      • Events
        • Time - Elapsed game time is 0.00 seconds
      • Conditions
      • Actions
        • -------- Creating a Menu --------
        • Set SBM_GUI_title = Example Menu
        • Trigger - Run CreateSpellMenu <gen> (ignoring conditions)
        • Set exampleMenu = SBM_GUI_lastCreatedMenu
        • -------- Creating an option 1 --------
        • Set SBM_GUI_label = Option 1
        • Set SBM_GUI_index = 0
        • Set SBM_GUI_text = Say Hello...
        • Set SBM_GUI_menu = exampleMenu
        • Set SBM_GUI_btn = ReplaceableTextures\CommandButtons\BTNControlMagic.blp
        • Set SBM_GUI_disbtn = ReplaceableTextures\CommandButtons\DISBTNControlMagic.blp
        • Trigger - Run AddSpellOption <gen> (ignoring conditions)
        • Set exampleOptions[0] = SBM_GUI_lastCreatedOption
    • How to Open
      • Events
        • Player - Player 1 (Red) skips a cinematic sequence
      • Conditions
      • Actions
        • Set SBM_player = (Triggering player)
        • Set SBM_GUI_menu = exampleMenu
        • Trigger - Run OpenMenu <gen> (ignoring conditions)
    • How to Destroy
      • Events
        • Player - Player 1 (Red) types a chat message containing -destroy as An exact match
      • Conditions
      • Actions
        • Set SBM_GUI_menu = exampleMenu
        • Trigger - Run DestroyMenu <gen> (ignoring conditions)
        • Set SBM_GUI_menu = 0
    • Option selection
      • Events
        • Game - SBM_event becomes Equal to 1.00
      • Conditions
        • SBM_menuKey Equal to exampleMenu
      • Actions
        • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
          • If - Conditions
            • SBM_optionKey Equal to exampleOptions[0]
          • Then - Actions
            • -------- do stuff --------
            • Game - Display to (All players) the text: Hello World!
            • Skip remaining actions
          • Else - Actions

    New update:
    - Fixed a bug with closeMenu not working
    - When a menu closes the selection will return to what it was before
    - Added votekick menu as an additional example
    - Fixed a bug with the SpellMenu not being deallocated when destroy was called
    - Fixed a bug in which the next button could be hidden due to disabled option at slot 11 covering it.
    - Added start and end cooldown event registration.
    - Changed how callbacks were registrated for faster setup.
    - Fixed a bug where options weren't deallocated once removed from the menu.
    - Added method refresh to spell menu so that the menu can be manually changed in case the properties change independant of player selection
    - Now updates the menu for all players currently viewing it whenever an option is selected
    - Added real events for option selection, menu open and menu close.
    - Player related data is now removed from SpellOptionsMenu upon leaving the game
    - showWindow(player, index) is now public so that the window can be changed from outside
    - Added GUI plugin for additional support
    - Added static if to include/exclude some GUI variables from the library

    Added another example use case in the form of votekick.
    _________________________________________

    How advisable is it to use [vJASS] - Observables to refresh the Menu rather than periodically doing it? The menu would subscribe to the options and refresh when they change state (Currently the window is refreshed every 0.5 seconds while it remains open, regardless of any changes). I'm struggling also with how to take into account hidden buttons, as that is done locally. Which means option 11 could be the first option for one player on window 2, but for another it could be 14 given how many options have beenn hidden... This means that the only way I see for how to take this into account is to loop from index 0 until all 12 options have been shown... It's a mess and I need some help here.

    Another possible solution is to just have the options fixed at their index leaving gaps if they are hidden, but that could lead to scenarios where entire windows are empty.
     
    Last edited: Jan 22, 2019
  2. Overfrost

    Overfrost

    Joined:
    Jan 9, 2019
    Messages:
    101
    Resources:
    0
    Resources:
    0
    Just quick auto-pointing stuff here.
    Code (vJASS):
        private struct Cooldown extends array
            private static Table table_clocks
            private Table table  // this one line is just wrong

        //------
        // Why? Use TableArray instead.
        // Ofc you must also make necessary changes along with above.

    I haven't pointed out anything else, if any.

    EDIT: Not literally wrong. Just, there're better ways to do it. It just looks wrong for a system to do that.
     
    Last edited: Jan 21, 2019
  3. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,160
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    I think the reason was that I didn't manage to fit the GetHandleId(timer) inside the table array, which was the reason why I asked IcemanBo for help in the first place ^^

    1.00
    - You can now lock options at positions between 0 and 11, which means they will override any option that would otherwise have been presented there.
    - The number of windows is now adapted to the current number of presented options for that player.
    - Fixed a bug with locking abilities to proper UI position.

    upload_2019-1-22_14-15-35.png
    (The play and volume options have been locked and this is the last window)

    1.01 Added a Bag Plugin System and made a few other fixes.

    Instead of refreshing the menus every 0.5 second regardless of if anything changed, refresh only after a option has changed.

    To achieve this I'd need to store the menu inside the option and trigger a real-event whenever a property changes inside an option.

    Is this worth it?
     

    Attached Files:

    Last edited: Jan 23, 2019
  4. Sir Moriarty

    Sir Moriarty

    Joined:
    Jun 13, 2016
    Messages:
    282
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    Very cool system. I can easily see this being useful. With that said, here are the things I'd like rectified:

    General remarks:
    1. Not sure I'm a fan of using one-letter variable names. It makes it a bit harder to read and understand your code. What is 'o'? What is 'p'? Please, for the sake of sanity of those trying to read your code, give those variables longer, more descriptive names.
    2. You're using spell levels to enable unique tooltips for the spells. While this works, IMO it also includes a significant cognitive and complexity overhead to your code for managing these levels. For what it's worth, it should be perfectly safe to simply set the ability tooltips for each player within a `GetLocalPlayer()` if-clause. I'm personally relying on a similar trick in one of my systems, and it's never led to desyncs.
    3. The system overall needs better documentation. A lot of the methods are either entirely undocumented, or documented unclearly. A simple API listing does not constitute as documentation, as it does not explain how to use the system, nor what the possible caveats are. The examples are good and rectify this a little, but having proper system-level documentation is IMO preferrable. Ideally, there would be a guide describing what exactly the system is capable of, how to use it, and what options there are.

    Specific remarks:
    - This method appears unused:
    Code (vJASS):
    static method removeFromOption takes SpellOption o returns nothing

    If you're not using it, remove it.

    - Please use the constants from SpellOptionsMenu in SpellOptions for your events:
    Code (vJASS):
    set udg_SBM_event = 5.    // CD ended event


    - Any particular reason you're using a table here for fields, rather than plain fields with GetLocalPlayer() clauses? It should be perfectly safe to do in this case.
    Code (vJASS):

        struct SpellOption
            private Table table
     


    - You're suggesting to modify this method if the user wants to change the format of the cooldown, but it's hidden in the core code and it's not immediately obvious that it's even a possibility.
    Code (vJASS):

    private static method formatTime takes integer seconds returns string
        return I2S(seconds)    // you can modify this method in case you want the cooldown to have a different format
    endmethod
     

    I suggest moving it elsewhere and moving the comment to method-level so that it has better visibility to the end user.

    - The comment on this method is unclear.
    Code (vJASS):

    /** Refreshes the menu for all players that currently have it open
            */

            method refresh takes nothing returns nothing
     

    What exactly does "refreshing" imply in this context?

    Conclusion:
    I really like what you're doing here, and the system definitely has its merits, and can be used as-is. However, there's a lot that can be improved, especially in regards to documentation and ease-of-use. All too often I see amazing systems on Hive with no clear instructions on how to use them, and I end up having to read through the code of the system to understand how it works, what it can do, and how I'm supposed to use it. This is another one of those.
    The examples are good, but like I mentioned above, examples =/= documentation. One should complement the other, not replace it.
    Another weak point is GUI-usability. While the potential seems to be there, it's even less obvious how exactly I'm supposed to use it. Your GUI music menu example is unfinished. Do remember that those who rely on GUI are rarely proficient enough in JASS to be able to read your code and figure out themselves how to use it.
    I also think the system is more complicated than it needs to be, and can be simplified significantly.
    I will review the system once again when you make changes.

    Verdict:
    Awaiting Update
     
  5. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,160
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    @Sir Moriarty

    1) I hear this criticism of single character variable names and to an extent I would agree with them, but I don't think I’m abusing it: as p, u, pid, uid, lvl are all straight forward descriptions. To me it’s only when something is more complex than being a type reference you should describe it or if you have u1, u2, u3 where one could be a triggering unit, another an attacker and so forth. SpellOption o could be replaced with “option” you are just adding verbosity here.

    2) Good point about the levels, though removing them would require wrappers for changing ability tooltips, to not turn into a mess inside the openWindow method.

    3) What do you mean documentation? The documentation is the code! ^^

    Remarks:

    · Cooldown.removeFromOption should have been used when deallocating the Option. Good catch.

    · I can’t use the constant from a second level library, though I can move the constant to SpellOption Library.

    · The reason for Table is 1) to go beyond the struct array limit and also because the need to store local player data for many types: SpellOption.text[pid] = “Blabla”
    I’m not sure if this is recommended replacement for Table if I understood your local block comment correctly:
    Code (vJASS):

    if GetLocalPlayer() == p then
        set .text = “Blabla”
    endif


    · The format of time is if you want to have the output be displayed in a way that is not seconds. You could for example code something that would churn out mm:ss or use reals.

    · Currently an open menu is periodically refreshed every 0.5 seconds for players using it. This is obviously not so fast in case the underlying options change. This could be problematic if you have a globally shared option that should be disabled instantly for all players, thus calling SpellMenu.refresh() after a Option is selected would mitigate this problem.

    As for GUI, I might look to update it in the future, but right now I’m just focusing on the vJass version. I’ll leave it there in case anyone wishes to try using it.
     
    Last edited: Jan 26, 2019
  6. Sir Moriarty

    Sir Moriarty

    Joined:
    Jun 13, 2016
    Messages:
    282
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    @Pinzu
    Replying to your comments:

    Verbosity isn't always bad, if it makes the intent clearer. Obviously, you shouldn't use long variable names either, like in Java land, e.g. "pickedPlayerOptionHandle", but just "option" is fine. If you're reading a long function, and you stumble across something like
    Code (Text):
    elseif not o.getLocalHidden(pid) and not .optionIsLocked(o) then
    it's a small, but non-insignificant cognitive overhead. The JPAG suggests avoiding short variable names.

    Code is the source of truth. If I have to read the source code of a system to understand how to use it, then that adds significant cognitive overhead to trying to use it. I am not demanding that you write a full guide explaining how to use this (I think the examples suffice), but I do wish that you add doc comments to all public-facing methods of your API. In particular:
    Code (vJASS):

    method SpellMenu.optionBelongs takes SpellOption o returns boolean
    method SpellMenu.lockOption takes SpellOption o, integer position returns nothing
    method SpellMenu.unlockOption takes SpellOption o returns nothing
    method SpellMenu.unlock takes integer position returns nothing
    method SpellMenu.optionIsLocked takes SpellOption o returns boolean
     

    These are public facing, but not explained. What does it mean that an option is "locked"? I suspect that it means that the option will show up on all pages of the menu, but it's not immediately obvious to the consumer of your API. A user of your system shouldn't have to read through all your code in order to understand how to use it.

    Some other comments are also poorly-worded:
    Code (Text):

    /**    Changes the current window of a given player, if window is out of bounds it will be set to 0
    */
     
    What is a "window"? Is it a menu page? Is it a menu? What does it mean for a window to be out of bounds?

    Another point:
    Code (vJASS):

    method SpellOption.getLocalEnabled takes integer pid returns boolean
     

    While it is obvious to me that the `pid` argument refers to the player ID for which to get the property, because I read the code, it may not be obvious to the user who hasn't read the code, and it further illustrates why both documentation and descriptive variable names are important. E.g. how I might've written this:
    Code (vJASS):

    /**
        Returns whether this SpellOption is enabled for played with id `viewerId`
    **/

    method SpellOption.getLocalEnabled takes integer viewerId returns boolean
     

    The more descriptive variable name together with the doc comment makes the method signature far easier to parse and comprehend, and makes the intent clear. It also makes it obvious that each player's option values are local to them.

    Please do.

    You understood me right. I don't think struct array limit should be a concern - how likely is it that any given user of your library will have >8000 menu options?

    My remark about the time formatter function is that it was hidden away amongst other internal functions of the system and not "visible" enough. I personally would extract it into a global function and add the description you just wrote as a comment.

    Regarding .refresh(), please add what you explained to me to the comment, in particular, what this method does, and why would I want to call it as a user.

    Conclusion
    Let's do it this way. I'm very tempted to approve this resource, if you:
    • Change the variable names to be more descriptive, to comply with JPAG.
    • Write a short, top-level summary in the thread summarizing how to use the system, and what exactly it's capable of. What is "locking", what "local" means on options, etc, things that aren't immediately obvious. Doesn't have to be long, just short and to the point. Try to cover every significant feature of the system.
    • Improve the documentation on the methods, in particular the ones I outlined above.
    • Remove dead code.
    Everything else is just my own personal suggestion, but not critical for approval.

    P.S. Don't let my tone discourage you :) This is a great system and I definitely appreciate it being made.
     
  7. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,160
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    Nothing wrong with your tone, don't worry about that ^^

    Are you absolutely certain we can write variables in local blocks? Why are we then using arrays for player data: set udg_playerGold[24] = 1000 or what have you?

    Code (vJASS):

    struct
     
        // So an array for storing local data can actually be replaced with two variables then?
        private string array data[24]
     
        private string localData = null // start off as null to indicate no local data
        private string notLocalData
     
        method setLocalText takes integer playerId, string text returns nothing
            set .data[pid] = text
        endmethod
     
        // Surely this implementation is crazy
     
        method setLocalText takes integer playerId, string text returns nothing
            if GetLocalPlayer() == Player(playerId) then
                set .localData = text
            endif
        endmethod
     
        method setText takes string text returns nothing
            set .notLocalData = text
        endmethod
     
        method getLocalData takes integer playerId returns nothing
            if GetLocalPlayer() == Player(playerId) and not localData == null then
                return .localData // if there was local data we return it
            endif
            return .notLocalData // no local data so we return default
        endmethod

    endstruct


    It's not completely unthinkable if a noobie would create one menu for each player, it's not necessary to do this but somewhat more convenient if you use GUI.

    24 players * 100 options, you'v already reached 1/6 capacity if the limit is 15000 (don't remeber exactly). The music menu alone uses 50 Options, a profficent user could of course clean up the menu after he or she is done with it and thus recycle the Options on menu close events.

    A bag system I'm using creates a menu for each bag though this perhaps could be improved come to think of it, but what I'm saying is there are many reasons for why one could have many options.
     
    Last edited: Jan 26, 2019
  8. Sir Moriarty

    Sir Moriarty

    Joined:
    Jun 13, 2016
    Messages:
    282
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    Yeah. My system does a lot of local blocks with a LOT of data going through them, and it's never been an issue for me. The reason why we use arrays for player data like you showed is, simply, because it's more convenient than having 24 different variables for each player. However, in your case, each player only really cares about his own data, so it would be perfectly valid to use local clauses.

    That's perfectly valid. For example, if you're using that data with the `BlzSetAbilityTooltip` native, this is perfectly alright to do, and will only modify the tooltip for the player in the local clause. It's actually the same technique I'm using together with the Preload exploit to save/load data from files in my map, and that uses a lot of local code.

    Having had a lot of experience with local clauses, I'll be more than happy to help you verify your code! If you wish to go down that path, of course. It was merely a suggestion on my part.

    There is also one other caveat: ability levels increase loading times. There's not a huge difference, but it's there. So, say, you have 24 abilities, each with 24 levels, that's 576 ability levels, which, unfortunately, does introduce some overhead. Been there, done that. If you can get away with having just one level per ability, I'd say it's definitely preferable.

    I suppose that's a valid concern. The limit in vJASS is still 8192 since Blizzard did not update JassHelper to the new 32k limit, unfortunately. You can use a custom allocator (I can give you the code if you want it) that supports 32k instances.
    Though, aren't options supposed to be per-menu, and not per-player? For example, I'm reading through your SongMenu example, and each option is created only for the menu, not for each player. Is there something I'm missing here?
     
  9. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,160
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    Options are per-menu based. But you can have system that defacto use a menu per player. Checkout the BagPlugin Library example for such a system, it's not completely done and could probably be improved to not save the menu but that's what it does for now.

    I could take a look at that 30k allocator though is there any problem with using Table for allocation as you can have according to Bribe 2 ^ 31 - 1 Table instances. Loading from a hastable is of course slower than array access, but I wonder how big a factor it is in this circumstance. The Options limit probably is more important a factor than the read speed but I'm not sure.

    There are also further speed optimizations to be done relating to when to refresh a menu that is much more crucial than memory access (current refresh time is O(n) to the amount of options the menu and periodic). Haven't tackled this problem because I'm not sure how to go about it, I've commented on it before though.


    In conclusion: I'll try remove the level of the abilities and improve documentations and the other things you stated. I however won't change the use of Table inside Option as that probably has a marginal effect and won't impact the SpellOption API as far as I can tell.
     
    Last edited: Jan 26, 2019
  10. Sir Moriarty

    Sir Moriarty

    Joined:
    Jun 13, 2016
    Messages:
    282
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    My primary concern with using Table over classical allocation is not performance, simply that it complicates the code quite a fair bit.

    If you want to use the allocator I was talking about, here it is:
    Code (vJASS):

    library Alloc
        module Alloc
            private static integer array recycler
         
            static method allocate takes nothing returns thistype
                local thistype this = recycler[0]
             
                if (recycler[this] == 0) then
                    set recycler[0] = this + 1
                else
                    set recycler[0] = recycler[this]
                endif
             
                return this
            endmethod
         
            method deallocate takes nothing returns nothing
                set recycler[this] = recycler[0]
                set recycler[0] = this
            endmethod

            private static method onInit takes nothing returns nothing
                set recycler[0] = 1
            endmethod
        endmodule
    endlibrary
     


    Simply make your structs "extends array" and use the module.

    Quick edit:

    Something else that just occured to me: you're not really taking full advantage of hash-based allocation anyway.

    Code (vJASS):

        struct SpellOption
            private Table table
            public trigger trg
        ...
     


    In this case, the field `table` is still stored as an array in the emitted JASS code. If you go over the instance limit, you won't be able to store it, and the system will burn and crash.
     
  11. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,160
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    Planned short term changes:

    · Cooldown: removeFromOption() should be used when an option is destroyed.

    · Cooldown: make sure -id is also properly deallocated.

    · SpellOption: trigger trg should use method operator for access rather than being inside the struct.

    · SpellOption.icon could probably be removed.

    · SpellOption.stopCooldown(playerId) should be added.

    · Move the cooldown on event constant to SpellOption Library

    · Wrappers for real event changes.
    Code (vJASS):


    call OnMenuEvent(MENU_EVENT_OPEN, function OnMenuOpen)

    call OnOptionEvent(OPT_EVENT_CD_ENDED, function OnCooldownEnd)

    call OnOptionEvent(OPT_EVENT_CD_STARTED, function OnCooldownStart)

    call OnOptionEvent(OPT_EVENT_USED, function OnOptionUsed)

    · Improve description of method arguments from pid to playerId and so on.

    · Move formatTime to the top of the code, add documentation.

    · Improve refresh documentation.

    · Allow direct allocation of Options to be added to Menus, but also have a table for which Menu a option belongs to. Adding a option to another table will be blocked and trigger an error response. This will be useful in the future if I decide to update menu based on which options were used.
    Code (vJASS):

    // All the trailing Option keywords will be removed I think.
    call menu.addOption(someOption) >>> menu.add(someOption)

     

    · Improve API documentation overall

    · Remove dead code, Menu.unlock(Option) being the most blatant one as it lacks counter part and has other examples.

    · Fix a bug with being able to lock an Option to position 11 (last window position).

    · Remove extra spell levels.

    As for the O(n) refresh rate I got some ideas but not sure which method is the best to pursuit. The first improvement is to remove the 0.5 second interval and execute refresh automatically on the option that was used. This however doesn't mitigate it being O(N) it just removes time redundancy. Whats blocking me from doing this is due to having to redo the Cooldown code as I would need a timer that updates every second with a associated counter instead of the current solution. Basically I'd be adding it into the SpellOption table with a timer for each player that has used it.
     
    Last edited: Jan 26, 2019
  12. Sir Moriarty

    Sir Moriarty

    Joined:
    Jun 13, 2016
    Messages:
    282
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    Looks good to me!

    Do you think it's a huge problem? As far as I can tell, it simply loops over all players and open the window menu for the player if it is selected. That's just about 24 loop iterations per refresh, and honestly, doesn't seem all that bad to me. I highly doubt optimizing this will yield any substantial performance gains, especially considering that the library is rather light-weight anyway.

    One thing I can think of is to keep around a list of all active menu-player pairs and just refresh those. You'll add/remove to this list when a player opens/closes a menu, BUT, I seriously doubt it's worth it.

    Another point that I just remembered:

    Code (vJASS):

            // The X and Y coordinates for where the menu unit will be located. To improve the user experience the location should be
            // in a location where no player has any vision.
            private constant real MENU_X                    = 2500
            private constant real MENU_Y                    = 2500
     


    You can go one step further, and:
    * Make the dummy unit size 0.
    * Make the dummy unit have no shadow (in the object editor)
    * Make the unit invulnerable.
    * Make the unit have no collision.

    That way, the unit will be de-facto invisible on the map, no matter where it's placed.
     
  13. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,160
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    Its not the player.loops that bother me so much as it is the Options-loop inside the openWindow method. But I can't come up with a smart way of handling all the cases of local hidden options and adjusting their positions accordingly without looping through all of them. Frankly I wonder how this would perform in a scenario where all 24 players were playing and voting on something like Game mode.

    I mean thats the worst case scenario, and we are probably only talking 24*N, n being the amount of options every 0.5 second which isn't horrible, but not optimal.

    The options are to either rewrite the cooldown so that the menu isn't refreshed unless it needs to be or just ignore it because 24*N is still insignificant as long as N is a relatively low number.
     
  14. Sir Moriarty

    Sir Moriarty

    Joined:
    Jun 13, 2016
    Messages:
    282
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    Jass is slow, but it's not as slow as you may think. It'll handle this just fine. Don't worry about it.
     
  15. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,160
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    Spell Option Changes
    Code (vJASS):

    library SpellOption /*
        v.1.1
        Made by: Pinzu
     
        */
    requires                 /*
     
        */
    Table                    /*     hiveworkshop.com/threads/snippet-new-table.188084/
     
        */
    optional PlayerUtils     /*    https://www.hiveworkshop.com/threads/playerutils.278559/
     
        */
    optional TimerUtils         /*     http://www.wc3c.net/showthread.php?t=101322
     
     
        This allows for the creation and manipulation of SpellOption items. Option properties are
        stored inside a table and have both a global and local attribute. This means that you can
        display different things for different players using player id for reference.
        Credits:
     
            IcemanBo - For creating the base of the Cooldown Manager.
            Sir Moriarty - Pointing out some errors in the code
     
        Notice:
     
        1)     To prevent green icons when an option is disabled the icon used must have a DISPAS version.
            Example: https://www.hiveworkshop.com/threads/btnelectroburst.310351/
     
        2)    The Icon paths you use must have two backslashes to work correctly, like so: \\
     
        3)     if an option has no btn or disbtn assigned it will use DEFAULT_BTN or DEFAULT_DISBTN.
     
        4)     The system does not deallocate local data inside options when a player leaves. This must either
            be ignored or handled manually by using the method option.removeLocalChanges(playerId).
     
            Configurables */

     
        globals
     
            /* DEFAULT_BTN and DEFAULT_DISBTN are the icon paths that are used in case option.btn or  option.disbtn is null.
         
            */

            private constant string DEFAULT_BTN            = "ReplaceableTextures\\CommandButtons\\BTNHeal.blp"
            private constant string DEFAULT_DISBTN        = "ReplaceableTextures\\CommandButtonsDisabled\\DISBTNHeal.blp"
         
            /*    CHANGE_DELAY is used to reduce the amount of refreshes executed when an option changes properties. If no recent changes
                has occurred the option will update instantly. However, if an option changed within the defined interval the change will
                not trigger a callback. Once the timer has ended another update is performed if more than 1 change was detected within the
                current interval.
             
                The purpose of this is to reduce the amount of redundant refresh calls performed.  To determine which options are allowed
                to trigger callbacks the functions WatchSpellOption and UnwatchSpellOptions are used.
            */
     
            private constant real CHANGE_DELAY                = 0.5
     
            /*
                MAX_PLAYERS is the last possible slot that a player can assume used to allocate size
                of watched options lookup table.
            */

            private constant integer MAX_PLAYERS = 24
         
        endglobals
     
        //! novjass
     
        API
        ------------
        // Constants
        constant integer EVENT_SPELL_OPTION_SELECTED  
        constant integer EVENT_SPELL_OPTION_CD_END  
     
        /*
            Used to register callbacks for when Any Spell Option Events, such as when a cooldown ends or an option is selected.
        */

        function OnSpellOptionEvent takes real whichEvent, code c returns nothing
     
        /*
            Wrapper returning the last triggered option, be that on selection or when a cooldown ends.
        */

        function GetTriggerSpellOption takes nothing returns SpellOption
     
        /*
            Wrapper returning the relevant player when a option is triggered.
        */

        function GetTriggerSpellOptionPlayer takes nothing returns player
     
        /*
            Used to subscribe to an option at a menu slot index by matching player id.
        */

        function WatchSpellOption takes integer playerId, integer index, SpellOption option returns nothing
     
        /*
            Used to unsubscribe to all previously listed options for a player by matching id
        */

        function UnwatchSpellOptions takes integer playerId returns nothing
     
        struct SpellOption
     
            Global Access  
            ---------------
     
            integer id             // This is a custom id you can use to attach references, same as you would using SetUnitUserData.
                     
            string label         // Option header
         
            string text         // Option description
         
            string btn             // BTN icon path, used when the option is enabled
         
            string disbtn         // DISBTN icon path, used when the option is disabled.
             
            boolean enabled     // Flag for toggling if the option should be active
         
            boolean hidden         // Flag for if the option should be displayed
             
            integer cooldown     // Cooldown duration when the option is selected
         
            /*
                Creates an option with the given attributes. The callback code is executed whenever the option is selected.
                It's not requirement as you can use real-event listeners as an alternative.
            */

            static method create takes string label, string text, string btn, string disbtn, code callback returns thistype
         
            /*
                Deallocates all data associated with the option.
            */

            method destroy takes nothing returns nothing
         
            /*
                Used to trigger a callback when a player uses an option.
            */

            method select takes player p returns nothing
     
            Local Access    // Will change data for players with matching player id.
            ---------------
         
            /*
                Changes the option attribute for local player by matching id.
            */

            method setLocalLabel takes integer playerId, string s returns nothing
            method setLocalText takes integer playerId, string s returns nothing
            method setLocalBtn takes integer playerId, string s returns nothing
            method setLocalDisbtn takes integer playerId, string s returns nothing
            method setLocalEnabled takes integer playerId, boolean b returns nothing
            method setLocalHidden takes integer playerId, boolean b returns nothing
            method setLocalCooldown takes integer playerId, integer seconds returns nothing
         
            /*
                Returns the current option attribute for local player by matching id.
            */

            method getLocalLabel takes integer playerId returns string
            method getLocalText takes integer playerId returns string
            method getLocalBtn takes integer playerId returns string
            method getLocalDisbtn takes integer playerId returns string
            method getLocalEnabled takes integer playerId returns boolean
            method getLocalHidden takes integer playerId returns boolean
            method getLocalCooldown takes integer playerId returns integer
         
            /*
                Removes any local option changes, reverting back to normal for local player by matching id
            */

            method removeLocalLabel takes integer playerId returns nothing
            method removeLocalText takes integer playerId returns nothing
            method removeLocalBtn takes integer playerId returns nothing
            method removeLocalDisbtn takes integer playerId returns nothing
            method removeLocalEnabled takes integer playerId returns nothing
            method removeLocalHidden takes integer playerId returns nothing
            method removeLocalCooldown takes integer playerId returns nothing
         
            /*
                Removes all local changes for matching player, reverting to global option attributes.
                Does not effect active cooldown.
            */

            method removeLocalChanges takes integer playerId returns nothing
         
            /*
                Returns the current icon that is shown for a given player. Depending on state
                such as if the option is enabled, disabled or has a cooldown.
            */

            method getLocalIcon takes integer playerId returns string
         
            /*
                Removes option cooldown for a given player.
            */

            method stopCooldown takes integer playerId returns nothing
             
            /*
                Starts option cooldown  for a given player.
            */

            method startCooldown takes integer playerId returns nothing
         
            /*
                Clears all active cooldowns from the option.
            */

            method stopAllCooldowns takes nothing returns nothing
         
        //! endnovjass
         
        globals
            constant integer EVENT_SPELL_OPTION_SELECTED    = 1        // Triggered when a option is selected
            constant integer EVENT_SPELL_OPTION_CD_END        = 5        // Triggered when a cooldown has expired
            constant integer EVENT_SPELL_OPTION_CHANGE        = 6        // Triggered every time the option changes
         
            constant integer GLOBAL_PLAYER                    = -1    // Used to indicate that a change affected all players
         
            private integer label_index
            private integer text_index
            private integer btn_index
            private integer disbtn_index
            private integer enabled_index
            private integer hidden_index                      
            private integer cooldown_index                  
            private constant integer trigger_index        = -1    // Storage index for the callback trigger
            private constant integer id_index            = -2    // Storage index for the owning menu
         
            private trigger trgOnAnySelection             = null
            private trigger trgOnAnyCooldownEnd         = null
            private trigger trgOnAnyChange                 = null
         
            private Table delayTable
         
            // This is a lookup for options being watched right now
            private SpellOption array watched[MAX_PLAYERS][12]      
        endglobals
     
        function OnSpellOptionEvent takes real whichEvent, code c returns nothing
            if whichEvent == EVENT_SPELL_OPTION_SELECTED then
                if trgOnAnySelection == null then
                    set trgOnAnySelection = CreateTrigger()
                    call TriggerRegisterVariableEvent(trgOnAnySelection, "udg_SBM_event", EQUAL, whichEvent)
                endif
                call TriggerAddCondition(trgOnAnySelection, Filter(c))
            elseif whichEvent == EVENT_SPELL_OPTION_CD_END then
                if trgOnAnyCooldownEnd == null then
                    set trgOnAnyCooldownEnd = CreateTrigger()
                    call TriggerRegisterVariableEvent(trgOnAnyCooldownEnd, "udg_SBM_event", EQUAL, whichEvent)
                endif
                call TriggerAddCondition(trgOnAnyCooldownEnd, Filter(c))
            elseif whichEvent == EVENT_SPELL_OPTION_CHANGE then
                if trgOnAnyChange == null then
                    set trgOnAnyChange = CreateTrigger()
                    call TriggerRegisterVariableEvent(trgOnAnyChange, "udg_SBM_event", EQUAL, whichEvent)
                endif
                call TriggerAddCondition(trgOnAnyChange, Filter(c))
    debug     else
    debug         call BJDebugMsg("[thistype_OnSpellOptionEvent] ERROR: Invalid event argument.")
            endif
        endfunction
        function GetTriggerSpellOption takes nothing returns SpellOption
            return udg_SBM_optionKey
        endfunction
     
        function GetTriggerSpellOptionPlayer takes nothing returns player
            return udg_SBM_player
        endfunction
     
        function WatchSpellOption takes integer playerId, integer index, SpellOption option returns nothing
            set watched[playerId][index] = option
        endfunction
     
        private function IsWatching takes integer playerId, SpellOption option returns boolean
            local integer i = 0
            loop
                exitwhen  i == 12
                if watched[playerId][i] == option then
                    return true
                endif
                set i = i + 1
            endloop  
            return false
        endfunction
     
        function UnwatchSpellOptions takes integer playerId returns nothing
            local integer i = 0
            loop
                exitwhen  i == 12
                set watched[playerId][i] = 0
                set i = i + 1
            endloop  
        endfunction
     
        private function NotifyEvent takes player p, SpellOption o,  real whichEvent returns nothing
            set udg_SBM_player = p
            set udg_SBM_optionKey = o
            set udg_SBM_event = 0.
            set udg_SBM_event = whichEvent
        endfunction
     
        /*
            Notify of changes after delay has expired.
        */

        private function OnDelayEnd takes nothing returns nothing
            local timer t = GetExpiredTimer()
            local integer id = GetHandleId(t)
            local integer pid = delayTable[GetHandleId(t)]
            static if LIBRARY_TimerUtils then
                call ReleaseTimer(t)
            else
                call DestroyTimer(t)
            endif
            // If more than one change has occurred we update again
            if delayTable.integer[-id] > 1 then
                // We pass 0 as we don't care which option it was
                // Not sure if another timer should be started here to prevent another one from being fired right after...
                call NotifyEvent(Player(pid), 0, EVENT_SPELL_OPTION_CHANGE)                                                  
            endif
            call delayTable.timer.remove(pid)
            call delayTable.remove(id)
            call delayTable.integer.remove(-id)
            set t = null
        endfunction
     
        /*
            Starts a short delay so that multiple simultaneous changes don't overload the system. If the argument
            passed is GLOBAL_PLAYER it will recursively start a change event for all playing players.
        */

        private function OnChange takes integer pid, SpellOption option returns nothing
            local timer t
            local integer id
            static if LIBRARY_PlayerUtil then
                local User p
            else
                local player p
            endif
            // We check if the player is currently watching this option, if not we discard it
            if IsWatching(pid, option) then  
                if pid != GLOBAL_PLAYER then
                    set t = delayTable.timer[pid]
                    // Filter updates if the timer is still ticking
                    if t == null then
                        // Update instantly if there was no timer
                        call NotifyEvent(Player(pid), option, EVENT_SPELL_OPTION_CHANGE)  
                        static if LIBRARY_TimerUtils then
                            set t = NewTimer()
                        else
                            set t = CreateTimer()
                        endif
                        set id = GetHandleId(t)
                        set delayTable.timer[pid] = t
                        set delayTable[id] = pid
                        set delayTable.integer[-id] = 1    // counter
                        call TimerStart(t, CHANGE_DELAY, false, function OnDelayEnd)
                    else
                        set id = GetHandleId(t)
                        set delayTable.integer[-id] = delayTable.integer[-id] + 1    // counter
                    endif
                    set t = null
                else
                    static if LIBRARY_PlayerUtil then
                        set p = User.first
                        loop
                            exitwhen p == User.NULL
                            call OnChange(pid, option)
                            set p = p.next          
                        endloop
                    else
                        set pid = 0
                        loop
                            exitwhen pid == bj_MAX_PLAYERS
                            set p = Player(pid)
                            if (GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING and /*
                            */
    GetPlayerController(p) == MAP_CONTROL_USER) then
                                call OnChange(pid, option)
                            endif
                            set pid = pid + 1
                        endloop
                        set p = null
                    endif
                endif
            endif
        endfunction
     
        /*
            This manages option cooldown for each player. It will trigger a event each passing second
            to inform the menu that it should be updated.
        */

        private struct CooldownManager extends array
            private static Table table_clocks
            private Table table
         
            private static method cleanup takes timer t returns nothing
                local integer id = GetHandleId(t)
                local SpellOption option = table_clocks[-id]
                local thistype this = table_clocks[id]
                call this.table.timer.remove(option)
                call thistype.table_clocks.remove(id)
                call thistype.table_clocks.remove(-id)
                call thistype.table_clocks.real.remove(id)
                call OnChange(this, option)
                call PauseTimer(t)
                static if LIBRARY_TimerUtils then
                    call ReleaseTimer(t)
                else
                    call DestroyTimer(t)
                endif
            endmethod
         
            private static method onRepeat takes nothing returns nothing
                local timer t = GetExpiredTimer()
                local integer id = GetHandleId(t)
                local real remaining = table_clocks.real[id]
                local SpellOption option = table_clocks[-id]
                local thistype this = table_clocks[id]
                if remaining == 0 then
                    call cleanup(t)
                    call NotifyEvent(Player(this), option, EVENT_SPELL_OPTION_CD_END)
                else
                    set table_clocks.real[id] = remaining - 1.0
                endif
                call OnChange(this, option)
                set t = null
            endmethod
         
            static method remaining takes integer pid, SpellOption option returns real
                return table_clocks.real[GetHandleId(thistype(pid).table.timer[option])]
            endmethod
         
            static method purgeFromOption takes SpellOption option returns nothing
                local thistype this
                local integer pid = 0
                loop
                    exitwhen pid > bj_MAX_PLAYERS
                    set this = pid
                    if .table.timer.has(option) then
                        call cleanup(.table.timer[option])
                    endif
                    set pid = pid + 1
                endloop
            endmethod
         
            static method stop takes integer playerId, SpellOption option returns nothing
                local thistype this = playerId
                if .table.timer.has(option) then
                    call cleanup(.table.timer[option])
                endif
            endmethod
     
            static method start takes integer pid, SpellOption option, integer duration returns nothing
                local thistype this = pid
                local integer id
                local timer t
                if duration < 1 then
                    return
                endif
                if not .table.timer.has(option) then
                    static if LIBRARY_TimerUtils then
                        set t = NewTimer()
                    else
                        set t = CreateTimer()
                    endif
                    set .table.timer[option] = t
                    set id = GetHandleId(t)
                    set table_clocks[id] = this
                    set table_clocks[-id] = option
                    set table_clocks.real[id] = duration
                    call TimerStart(t, 1, true, function thistype.onRepeat)
                    set t = null
                endif
            endmethod
         
            private static method onInit takes nothing returns nothing
                local integer i = 0
                local player p
                loop
                    exitwhen i > bj_MAX_PLAYERS
                    set p = Player(i)
                    if GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING and /*
                    */
    GetPlayerController(p) == MAP_CONTROL_USER then
                        set thistype(i).table = Table.create()
                    endif
                    set i = i + 1
                endloop
                set table_clocks = Table.create()
            endmethod
         
        endstruct
     
        /*
            Spell Option
        */

     
        struct SpellOption
     
            private method operator trg= takes trigger t returns nothing
                set Table(this).trigger[trigger_index] = t
            endmethod
         
            private method operator trg takes nothing returns trigger  
                return Table(this).trigger[trigger_index]  
            endmethod
         
            method operator id takes nothing returns integer
                return Table(this).integer[id_index]
            endmethod
         
            method operator id= takes integer customId returns nothing
                set Table(this).integer[id_index] = customId
            endmethod
         
            method select takes player p returns nothing
                call NotifyEvent(p, this, EVENT_SPELL_OPTION_SELECTED)
                if .trg != null then
                    call TriggerExecute(.trg)
                endif
            endmethod
         
        //! runtextmacro DEFINE_ACCESS("label_index",         "label",     "Label",     "string")      
        //! runtextmacro DEFINE_ACCESS("text_index",         "text",     "Text",     "string")
        //! runtextmacro DEFINE_ACCESS("btn_index",         "btn",         "Btn",         "string")  
        //! runtextmacro DEFINE_ACCESS("disbtn_index",         "disbtn",     "Disbtn",     "string")      
        //! runtextmacro DEFINE_ACCESS("enabled_index",     "enabled",     "Enabled",     "boolean")
        //! runtextmacro DEFINE_ACCESS("hidden_index",         "hidden",     "Hidden",     "boolean")
        //! runtextmacro DEFINE_ACCESS("cooldown_index",     "cooldown", "Cooldown", "integer")
        //! textmacro DEFINE_ACCESS takes INDEX, NAME_1, NAME_2, TYPE
            method operator $NAME_1$= takes $TYPE$ E returns nothing
                set Table(this).$TYPE$[$INDEX$] = E
                call OnChange(GLOBAL_PLAYER, this)
            endmethod
         
            method operator $NAME_1$ takes nothing returns $TYPE$
                return Table(this).$TYPE$[$INDEX$]
            endmethod
         
            method setLocal$NAME_2$ takes integer pid, $TYPE$ E returns nothing
                set Table(this).$TYPE$[$INDEX$ + pid + 1] = E
                call OnChange(pid, this)
            endmethod
         
            method removeLocal$NAME_2$ takes integer pid returns nothing
                call Table(this).$TYPE$.remove($INDEX$ + pid + 1)
                call OnChange(pid, this)
            endmethod
        //! endtextmacro
     
        //! runtextmacro GET_LOCAL("label_index",     "label",     "Label",     "string")      
        //! runtextmacro GET_LOCAL("text_index",     "text",     "Text",     "string")
        //! runtextmacro GET_LOCAL("btn_index",     "btn",         "Btn",         "string")  
        //! runtextmacro GET_LOCAL("disbtn_index",     "disbtn",     "Disbtn",     "string")      
        //! runtextmacro GET_LOCAL("hidden_index",     "hidden",     "Hidden",     "boolean")
        //! runtextmacro GET_LOCAL("cooldown_index", "cooldown", "Cooldown", "integer")
        //! textmacro GET_LOCAL takes INDEX, NAME_1, NAME_2, TYPE
            method getLocal$NAME_2$ takes integer pid returns $TYPE$
                if Table(this).$TYPE$.has($INDEX$ + pid + 1) then
                    return Table(this).$TYPE$[$INDEX$ + pid + 1]
                endif
                return Table(this).$TYPE$[$INDEX$]
            endmethod
        //! endtextmacro
            method getLocalEnabled takes integer pid returns boolean
                if getCooldown(pid) > 0 then
                    return false
                endif
                if Table(this).boolean.has(enabled_index + pid + 1) then
                    return Table(this).boolean[enabled_index + pid + 1]
                endif
                return Table(this).boolean[enabled_index]
            endmethod
     
            method getLocalIcon takes integer pid returns string
                local string icon
                if .getLocalEnabled(pid) then
                    set icon = getLocalBtn(pid)
                    if icon != null then
                        return icon
                    endif
                    return DEFAULT_BTN
                endif
                set icon = getLocalDisbtn(pid)
                if icon != null then
                    return icon
                endif
                return DEFAULT_DISBTN
            endmethod
            method removeLocalChanges takes integer playerId returns nothing
                call removeLocalLabel(playerId)
                call removeLocalText(playerId)
                call removeLocalBtn(playerId)
                call removeLocalDisbtn(playerId)
                call removeLocalEnabled(playerId)
                call removeLocalHidden(playerId)          
                call removeLocalCooldown(playerId)
            endmethod
         
            method stopCooldown takes integer playerId returns nothing
                call CooldownManager.stop(playerId, this)
            endmethod
         
            method startCooldown takes integer playerId returns nothing
                call CooldownManager.start(playerId, this, .getLocalCooldown(playerId))
            endmethod
         
            method getCooldown takes integer playerId returns integer
                return R2I(CooldownManager.remaining(playerId, this))
            endmethod
         
            method stopAllCooldowns takes nothing returns nothing
                call CooldownManager.purgeFromOption(this)
            endmethod
         
            static method create takes string label, string text, string btn, string disbtn, code callback returns thistype
                local thistype this = Table.create()
                set this.trg = CreateTrigger()
                call TriggerAddAction(this.trg, callback)
                set this.label = label
                set this.text = text
                set this.btn = btn
                set this.disbtn = disbtn
                set this.enabled = true
                set this.hidden = false
                return this
            endmethod
         
            method destroy takes nothing returns nothing
                call TriggerClearActions(.trg)
                call DestroyTrigger(.trg)
                set .trg = null
                set .label = null
                set .text = null
                set .btn = null
                set .disbtn = null
                call CooldownManager.purgeFromOption(this)
                call Table(this).destroy()
            endmethod
         
            private static method onInit takes nothing returns nothing
                local integer size     = bj_MAX_PLAYERS + 1
                set label_index     =     0*size
                set text_index         =     1*size
                set btn_index        =     2*size
                set disbtn_index    =    3*size
                set enabled_index    =     4*size
                set hidden_index    =     5*size
                set cooldown_index     =     6*size
                set delayTable = Table.create()
            endmethod
        endstruct
     
    endlibrary

     



    Q: Should option.id also have local methods such as option.setLocalId(playerId, customId) etc?

    Example:
    Code (vJASS):

    loop
                exitwhen songs[i] == null
                set songOption[i] = songMenu.createOption(i, "Play " + I2S(i + 1), "Track: " + songs[i], "ReplaceableTextures\\CommandButtons\\BTNBanish.blp", "ReplaceableTextures\\CommandButtonsDisabled\\DISBTNBanish.blp", function MusicSelected)
         
                // This is how you would add global cooldown to an option
                set songOption[i].cooldown = 15
         
                //     If we want to change Player(0) cooldown but maintain 15 second for the other players we could do this
                call songOption[i].setLocalCooldown(0, 20)
         
                // Similarly we could change other attributes such as label, text, btn, hidden, enabled and disabled...
     
                // Finally we can store custom ids as references to other data, in this case the option needs to be bound to a particular song
                // To achieve this we will store the index, i inside the option which we will later use to play the correct song.
         
                set songOption[i].id = i
         
                // Note that id doesn't have any local method wrappers (if you have a need for this do poke me and I'll consider changing it).
         
                set i = i + 1
            endloop
     
     
    Last edited: Jan 29, 2019
  16. Sir Moriarty

    Sir Moriarty

    Joined:
    Jun 13, 2016
    Messages:
    282
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    @Pinzu

    Sorry for the late reply.

    Really liking the fresh docs on SpellMenu! That's exactly what I was looking for. Great job so far.

    Regarding your question: since the id field is supposed to be simply a carrier for the user's custom data (if I understood this correctly), then I can definitely see it being useful to be able to store it on a per-player basis too. For example, if you want to bind an option to the id of a user-owned unit (for example in a menu scrolling through the user's units), though, on the other hand, it may also be then worthwhile to just create one menu for each player.

    On the other hand, there's also this consideration: so far, all "local" fields on options are for purely visual effects, affecting only the viewer of the menu (a local effect, if you will). However, the id field is something that the user simply uses to get data out of the system, and not used for any effects, so I'm not sure if it's correct to group it in with other visual modifiers and provide the same type of access to it. If you want the same menu to do different things for different players (e.g. affect different units selected based on the id field), then it may be worth altogether to simply create different menus for each player instead.

    In the end, that's up to you. I don't think it matters that much.

    Just one small nitpick however - I'd rename "id" to something like "customData" or "userData" to better reflect its purpose. At first I was a bit confused as to exactly what "id", I thought it was something purely internal rather than simply as a user field for the user's own data.
     
  17. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,160
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    I've updated the map and the change log is quite extensive this time around. A bunch new features were added a few issues with the code and documentation were resolved.

    The issue with timer inaccuracies were also resolved by updating the spell that changed rather than periodically updating the entire menu. Furthermore menu refreshes are only issued when a option is selected. This is however only done for the player in question, if the changes must be applied to all players a manual refresh call is needed.

    SpellOption
    • Changed the dummy unit to completely disapear from the map.
    • Removed method operator Cooldown.icon takes nothing returns string
    • Added Option.clearCooldown takes playerId
    • Replaced .allocate() and .table with Table.create() and Table(this) respectively inside SpellOption.
    • Improved the SpellOption documentation
    • Added a custom ID to the SpellOption, the idea here is the same as GetUntiUserData so that one can attach other type of indexed data to that option more easily.
    • Made SpellOption.trg private and added a callback method: select(player) instead.
    • Only instantiates a cooldown manager for each playing player now.
    • Added: function OnSpellOptionEvent takes real whichEvent, code c returns nothing
    • Reworked TimerManagement so that cooldown ticks only update the spell being affected if watched.
    • Reduced the textmacros inside SpellOption to one with a static if to handle a special case
    • Added: method onResponse takes code callback returns nothing - changes which callback will fire on option selection
    SpellMenu
    • Refactored SpellOptionsMenu almost entierly
    • Fixed a bug where menus were being reselected (i think)
    • Reduced the amount of levels used from 24 to 1 per spell
    • Removed the need to copy any of the disabled abilities
    • Removed unnecessary player util usage from initialization
    • Added: eject(index) removes and returns the option instead of deallocating it as .remove(index) does
    • Removed: createOption(...)
    • Changed: lockOption --> lock, unlockOption --> unlock, removeOption --> remove optionBelongs --> contains, etc
    • Added: SpellMenu.sort(ascending) which sorts all options based on option.userdata
    • Added: SpellMenu.get(index)
    • Added: SpellMenu.size()
    • Fixed: bug with optional library not being included properly
    • Improved documentation
    • The menu now optionally closes when escape is pressed.
    General
    • Made it possible to replace GUI variables with vJASS variables instead (should one want to remove GUI entierly)
    • Now supports TimerUtils and PlayerUtils
    • All GUI examples have been depreciated for now

    There won't be any more major changes for awhile as I lack the time to work on this, only minor fixes if any appear.

    Also it is theoretically possible to completely remove the need of having to copy abilities by using default wc3 ones. The problem is that depending on where the dummy unit will be situated it can interfer with the rest of the map. So I've decided for now to keep the 12 custom abilities.
     
    Last edited: Jan 29, 2019
  18. Sir Moriarty

    Sir Moriarty

    Joined:
    Jun 13, 2016
    Messages:
    282
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    One important question - have you tested this in MP? This is actually rather important to make sure there's no desyncs.

    Otherwise, stellar job. I'm ready to approve this, as soon as you fix one last thing:
    In function PlayerMenu.close:
    Code (vJASS):

                    if GetLocalPlayer() == .p then
                        call ClearSelection()
                        loop
                            set enum = FirstOfGroup(.selectedUnits)
                            exitwhen enum == null
                            call GroupRemoveUnit(.selectedUnits, enum)
                            call SelectUnit(enum, true)
                        endloop
                     endif
     

    There's a significant chance that modifying a group inside a local player clause may cause a desync. I am not 100% certain but I'd be more comfortable knowing for sure. To err on the side of caution, I'd rewrite this as:
    Code (vJASS):

                    if GetLocalPlayer() == .p then
                        call ClearSelection()
                    endif
                    loop
                        set enum = FirstOfGroup(.selectedUnits)
                        exitwhen enum == null
                        call GroupRemoveUnit(.selectedUnits, enum)
                        if GetLocalPlayer() == .p then
                            call SelectUnit(enum, true)
                        endif
                    endloop
     

    That way this should not be a problem at all.

    There's also one thing regarding sorting, however, it's merely a suggestion.
    Regarding sorting. I think it's a useful feature, however, there may be a case where you want to both attach data (e.g. another vJASS struct instance id) to the option menus, and sort them. However, doing both may not always work since you can't pick and choose struct ids.

    To avoid rewriting a lot of code, I suggest adding another simple configurable function like:
    Code (vJASS):

    function GetSpellOptionRank takes SpellOption option returns integer
        return option.userData
    endfunction
     

    Add this next to FormatTime, allowing the users to define their own sorting order for the options, and use that function to get the pivot in your sorting functions vs just the userData field.
     
  19. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,160
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    @Sir Moriarty

    I've tested before, not the latest build but haven't really changed much in regards to the local blocks, will change that part just to err on the safe side.

    I decided to add option.rank and the method you asked for custom values. I also implemented a GUI test example for Hero Selection.

    Updated to version 1.1a

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

    I think it's possible to completely remove the need of any custom abilities if I implemented GUI Unit Event v2.5.2.0 to detect when summoned units entered the map and instantly removed them. The drawback is that it has more overhead both in terms of execution but also in introducing another dependency. More importantly the problem with itis that I have no control over where the dummy unit is situated and thus a user could customize the default spells in such a way that the menu could interfer with other units. For example stomp, roar, fan of kvines, taunt and so on effecting nearby units.
     
    Last edited: Jan 30, 2019