The online racing simulator
PRISM Menu System (Call For Comments)
Menu API
The PRISM Menu System is based on an object oriented hierarchy from SourceMod. Understanding this hierarchy, is critical to using menus effectively.
  • Overview
    Menus are a callback based system. Each callback represents an action that occurs during a menu display cycle. A cycle consists of a number of notifications:
    • Start notification.
      • Display notification if the menu can be displayed to the client.
      • Either an item select or menu cancel notification.
    • End notification.
    Since End signifies the end of a full display cycle, it is usually used to destroy temporary menus.
  • Specification
    A detailed explanation of these events is below. For PRISM, a menuHandle is returned in the constructor and a MenuAction are always set in the menuHandler constructor as the callback.
    • Start. The menu has been acknowledged. This does not mean it will be displayed; however, it guarantees that "onMenuEnd" will be called.
      • onMenuStart, this action is triggered intrinsicly upon startup.
    • Display. The menu is being displayed to a client.
      • onMenuDisplay, An IMenuPanel pointer and client index are available.
        • param1: A client index.
    • Select. An item on the menu has been selected. The item position given will be the position in the menu, rather than the ClickID of the pressed (unless the menu is a raw panel).
      • onMenuSelect, a client index and item position are passed.
        • param1: A client index.
        • param2: An item position.
    • Cancel. The menu's display to one client has been cancelled.
      • onMenuCancel, A reason for cancellation is provided.
        • param1: A client index.
        • param2: A menu cancellation reason code.
    • End. The menu's display cycle has finished; this means that the "Start" action has occurred, and either "Select" or "Cancel" has occurred thereafter. This is typically where menu resources are removed/deleted.
      • onMenuEnd.
        • param1: A menu end reason code.
        • param2: If param1 was MENU_END_CANCELLED, this contains a menu cancellation reason code.
  • Panels
    For panels, the callback rules change. Panels only receive two of the above callbacks, and it is guaranteed that only one of them will be called for a given display cycle.
    • Select. A button has been pressed. This can be any number and should not be considered as reliably in bounds. For example, even if you only had 2 items in your panel, a client could trigger a key press of "43."
      • onMenuSelect, A client index and ClickID number pressed are passed.
        • param1: A client index.
        • param2: ClickID of the button pressed.
    • Cancel. The menu's display to one client has been cancelled.
      • onMenuCancel, The menu's display to one client has been cancelled.
        • param1: A client index.
        • param2: A menu cancellation reason code.
Examples
First, let's start off with a very basic menu. We want the menu to look like this:
Do you like apples?
1. Yes
2. No

We'll draw this menu with both a basic Menu and a Panel to show the API differences.
  • Basic Menu
    First, let's write our example using the Menu building API.

    <?php 
    php
        
    class basicMenu extends PluginsMenus {
            public function 
    __construct() {
                
    $this->registerSayCommand('menuTest''menuTest');
            }
            public function 
    menuHandler($menu$action$param1$param2) {
                if (
    $action == MENU_ACTION_SELECT)
                {    
    # If an option was selected, tell the client about the item.
                    
    $found $menu->getMenuItem($param2$info NULL);
                    
    $this->clientPrint($param1PRINT_CHAT"You selected item: {$param2} (found? {$found} info: {$info}"));
                }
                else if (
    $action == MENU_ACTION_CANCEL)
                {    
    # If the menu was cancelled, print a message to the console about it. */
                    
    $this->clientPrint($param1PRINT_CHAT"Client menu was cancelled.  Reason: {$param2}"));
                }
                else if (
    action == MENU_ACTION_END)
                {    
    # Distorys our menu.
                    
    $menu->close();
                }
            }
            public function 
    menuTest($args$CLID) {
                
    $menu = new handleMenu('menuHandler');
                
    $menu->setMenuTitle("Do you like apples?");
                
    $menu->addMenuItem("yes""Yes");
                
    $menu->addMenuItem("no""No");
                
    $menu->setMenuExitButton(FALSE);
                
    $menu->displayMenu($CLID20);

                return 
    PLUGIN_HANDLED;
            }
        }
    ?>

    Note a few very important points from this example:
    • One of either Select or Cancel will always be sent to the action handler.
    • End will always be sent to the action handler.
    • We destroy our Menu in the End action, because our Handle is no longer needed. If we had destroyed the Menu after DisplayMenu, it would have canceled the menu's display to the client.
    • Menus, by default, have an exit button. We disabled this in our example.
    • Our menu is set to display for 20 seconds. That means that if the client does not select an item within 20 seconds, the menu will be canceled. This is usually desired for menus that are for voting.
    • Although we created and destroyed a new Menu Handle, we didn't need to. It is perfectly acceptable to create the Handle once for the lifetime of the plugin.
    Our finished menu and attached console output looks like this (I selected "Yes"):
    [img][/img]
  • Basic Panel
    Now, let's rewrite our example to use Panels instead.

    <?php 
    php
        
    class basicPanel extends PluginsPanels {
            public function 
    __construct() {
                
    $this->registerSayCommand('panelTest''panelTest');
            }
            public function 
    panelHandler($action$param1$param2) {
                if (
    $action == MENU_ACTION_SELECT) {
                    
    $this->clientPrint($param1PRINT_CHAT"You selected item: {$param2}");
                } else if (
    action == MENU_ACTION_CANCEL) {
                    
    $this->clientPrint($param1PRINT_CHAT"Menu was cancelled.  Reason: {$param2}");
                }
            }
            public function 
    panelTest($args$CLID) {
                
    $panel = new handlePanel();
                
    $panel->setTitle("Do you like apples?");
                
    $panel->drawItem("Yes");
                
    $panel->drawItem("No");
                
    $panel->sendToClient($CLID'panelHandler'20);
                
    $panel->close();
         
                return 
    PLUGIN_HANDLED;
            }
        }
    ?>

    As you can see, Panels are significantly different.
    • We can destroy the Panel as soon as we're done displaying it. We can create the Panel once and keep re-using it, but we can destroy it at any time without interrupting client menus.
    • The Handler function gets much less data. Since panels are designed as a raw display, no "item" information is saved internally. Thus, the handler function only knows whether the display was canceled or whether (and what) numerical key was pressed.
    • There is no automation. You cannot add more than a certain amount of selectable items to a Panel and get pagination. Automated control functionality requires using the heftier Menu object API.
    Our finished display and console output looks like this (I selected "Yes"):
  • Basic Paginated Menu
    Now, let's take a more advanced example -- pagination. Let's say we want to build a menu for changing the map. An easy way to do this is to read the tracklist.txt file at the start of a plugin and build a menu out of it.
    Since reading and parsing a file is an expensive operation, we only want to do this once per track. Thus we'll build the menu in onTrackStart, and we won't call closeHandle until onTrackEnd.
    Our example tracklist.txt
    AS4
    AS5
    AS7R
    BL1
    BL1R
    BL2
    BL2R
    FE1
    FE1R
    FE4R
    FE5
    KY1
    KY2R
    KY3
    SO1
    SO3
    SO4R
    SO5

    Source code:

    <?php 
    php
        
    class changeMap extends PluginsMenus {
            private 
    $trackMenu;
            public function 
    __construct() {
                
    $this->registerSayCommand('prism change track''cmdChangeTrack''Displays a menu to change the track.'ADMIN_TRACK);
                
    $this->makeTrackMenu();
            }
            public function 
    cmdChangeTrack($args$CLID) {
                if (
    $this->trackMenu == NULL)
                    
    $this->clientPrint($param1PRINT_CHAT"The tracklist.txt file was not found!");
                else
                    
    $this->trackMenu->display($CLIDMENU_TIME_FOREVER);
                return 
    PLUGIN_HANDLED;
            }
            public function 
    makeTrackMenu() {
                
    # Open the file
                
    if (($tracklist file('tracklist.txt')) == FALSE)
                    return (
    $this->trackMenu NULL);

                
    # Create the menu Handle
                
    $this->trackMenu = new handleMenu('menuChangeTrack');
                foreach (
    $tracklist as $trackname)
                {
                    
    # Skip Comments
                    
    if ($trackname{0} == ';')
                        continue;
                    
    # Cut off the name at any whitespace
                    
    if (($whitespace strPos($trackname' ') != FALSE)
                        
    $trackname subStr($trackname$whitespace);
                    
    # Check if the map is valid
                    
    if (!$this->isTrackValid($trackname))
                        continue;
                    
    # Add it to the menu
                    
    $this->trackMenu->addMenuItem($trackname$trackname);
                }
                
    # Make sure we close the file!
                
    unset($tracklist);
         
                
    # Finally, set the title
                
    $this->trackMenu->setTitle("Please select a track:");
            }
            public function 
    menuChangeTrack($action$param1$param2) {
                if (
    $action == MENU_ACTION_SELECT) {
                    
    # Get item info
                    
    $found $this->trackMenu->getMenuItem($param2$info NULL);
                    
    # Tell the client
                    
    $this->printToChat($param1"You selected item: {$param2} (found? {$found} info: {$info})");
                    
    # Change the map
                    
    $this->serverCommand("/end");
                    
    $this->serverCommand("/track {$info}");
                }
            }
        }
    ?>

    This menu results in many selections (our tracklist.txt file had around 18 maps). So, our final menu has 3 pages, which side by side, look like:
    [img][img][img][/img][img][/img]

    Finally, the console output printed this before the map changed to my selection, Kyoto GP:

    You selected item: 13 (found? 1 info: KY3)

    Displaying and designing this Menu with a raw ShowMenu message or Panel API would be very time consuming and difficult. We would have to keep track of all the items in an array of hardcoded size, pages which the user is viewing, and write a function which calculated item selection based on current page and key press. The Menu system, thankfully, handles all of this for you.
    Notes:
    • Control options which are not available are not drawn. For example, in the first page, you cannot go "back," and in the last page, you cannot go "next." Despite this, the menu API tries to keep each the interface as consistent as possible. Thus, visually, each navigational control is always in the same position.
    • Although we specified no time out for our menu, if we had placed a timeout, flipping through pages does not affect the overall time. For example, if we had a timeout of 20, each successive page flip would continue to detract from the overall display time, rather than restart the allowed hold time back to 20.
    • If we had disabled the Exit button, options 8 and 9 would still be "Back" and "Next," respectively.
    • Again, we did not free the Menu Handle in MenuAction_End. This is because our menu is global/static, and we don't want to rebuild it every time.
    • These images show "Back." In SourceMod revisions 1011 and higher, "Back" is changed to "Previous," and "Back" is reserved for the special "ExitBack" functionality.
  • Advanced Voting
    If you need more information about voting results than MENU_ACTION_VOTE_END gives you, you can choose to have a different callback invoked. The new callback will provide much more information, but at a price: MENU_ACTION_VOTE_END will not be called, and you will have to decide how to interpret the results. This is done via Vote::setVoteResultCallback().

    Example:

    <?php 
    php
        
    class advancedVoting extends PluginVote {
            public function 
    __construct() {
                
    $this->registerSayCommand('prism vote menu''doVoteMenu''<track> - Makes a vote for a track.');
            }
            public 
    handleVoteMenu($menu$action$param1$param2) {
                if (
    $action == MENU_ACTION_END)
                {
                    
    /* This is called after VoteEnd */
                    
    $menu->close();
                }
            }
            public 
    handleVoteMenu($menu$votes$clients$items) {
                
    /* See if there were multiple winners */
                
    $winner 0;
                if ((
    $count count($items)) > 1)
                    
    $winner rand(0$count);
         
                
    $track $menu->getMenuItem($items[$winner]);
                
    $this->serverCommand("/end");
                
    $this->serverCommand("/changetrack {$track}");
            }
            public function 
    doVoteMenu($track) {
                
    # Make sure there is no vote in progress.
                
    if ($this->isVoteMenuInProgress())
                    return 
    PLUGIN_HANDLED;

                
    # Check Arg Count
                
    if (($argc count($argv str_getcsv($cmd' '))) < || $argc 3)
                {
                    
    $this->printToChat($param1"You must input only one track code. (For example BL1).");
                    return 
    PLUGIN_HANDLED;
                }

                
    # Make sure the track is valid.
                
    $track array_pop($argv);
                if (!
    $this->isValidTrack($track))
                    return 
    PLUGIN_HANDLED;

                
    $menu = new voteHandle('handleVoteMenu');
                
    $menu->voteResultCallback('handleVoteResults');
                
    $menu->setMenuTitle("Change track to: {$track}?");
                
    $menu->addMenuItem($track'Yes');
                
    $menu->addMenuItem('no''No');
                
    $menu->setMenuExitButton(FALSE);
                
    $menu->voteMenuToAll(20);
            }
        }
    ?>

ExitBack
ExitBack is a special term to refer to the "ExitBack Button." This button is disabled by default. Normally, paginated menus have no "Previous" item for the first page. If the "ExitBack" button is enabled, the "Previous" item will show up as "Back."
Selecting the "ExitBack" option will exit the menu with MENU_CANCEL_EXIT_BACK and MENU_END_EXIT_BACK. The functionality of this is the same as a normal menu exit internally; extra functionality must be defined through the callbacks.

Closing Menu Handles
It is only necessary to close a menu handle on MENU_ACTION_END. The MENU_ACTION_END is done every time a menu is closed and no longer needed.
It looks good. Do you happen to have any screenshots of it in action?
Quote from Flame CZE :It looks good. Do you happen to have any screenshots of it in action?

No, this is just the spec write up. SourceMod implemented and designed this system and it looks like this over there;







So, it's going to be that, just in LFS button from.
__constructor or __contruct?

@down: I wasn't sure, because I thought you are going to implement __contructor in PRISM.
Quote from misiek08 :__constructor or __contruct?

Nice catch, fixed. (It's __construct by the way.)

FGED GREDG RDFGDR GSFDG