Menu API (SourceMod)

From AlliedModders Wiki
Revision as of 07:35, 20 July 2021 by Kleiner (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search
Language: English  • русский

SourceMod has an extensive API for building and displaying menus to clients. Unlike AMX Mod X, this API is highly state driven. Menus are based on callbacks which are guaranteed to be fired.

For C++, the Menu API can be found in public/IMenuManager.h. For SourcePawn, it is in scripting/include/menus.inc.

Objects

The SourceMod Menu System is based on an object oriented hierarchy. Understanding this hierarchy, even for scripting, is critical to using menus effectively.

Styles

The top level object is a MenuStyle (IMenuStyle in C++). Styles describe a unique menu system. There are two such styles built into SourceMod:

  • Valve Style, also called "ESC" menus; 8 items per page, no raw/disabled text can be rendered
  • Radio Style, also called "AMX" menus; 10 items per page, raw/disabled text can be rendered

Each MenuStyle has its own rules and properties. You can think of them as existing on separate "channels." For example, two different menus can exist on a player's screen as both a Valve menu and a Radio menu at the same time, and SourceMod will be able to manage both without any problems. This is because each style keeps track of its own menus separately.

Panels

Menu displays are drawn with a lower level interface called Panels (IMenuPanel in C++). Panels describe exactly one chunk of display text. Both selectable items and raw text can be added to a panel as long as its parent style supports the contents you're trying to draw. For example, the Valve style does not support drawing raw text or disabled items. But with a Radio-style Panel, you can display a large amount of on-screen data in your own format.

Panels are considered temporary objects. They are created, rendered, displayed, and destroyed. Although they can be saved indefinitely, it is not necessary to do so.

Valve Style drawing rules/limitations:

  • Max items per page is 8.
  • Disabled items cannot be drawn.
  • Raw text cannot be drawn.
  • Spacers do not add a space/newline, giving a "cramped" feel.
  • Users must press "ESC" or be at their console to view the menu.

Radio Style drawing rules/limitations:

  • Max items per page is 10.
  • Titles appear white; items appear yellow, unless disabled, in which case they are white.
  • The 0th item is always white. For consistency, this means navigational controls explained in the next section are always white, and simply not drawn if disabled.

Menus

Lastly, there are plain Menus (IBaseMenu in C++). These are helper objects designed for storing a menu based on selectable items. Unlike low-level panels, menus are containers for items, and can only contain items which are selectable (i.e., do not contain raw text). They fall into two categories:

  • Non-paginated: The menu can only have a certain number of items on it, and no control/navigation options will be added, except for an "Exit" button which will always be in the last position supported by the style.
    • Valve Style maximum items: 8
    • Radio Style maximum items: 10
  • Paginated: The menu can have any number of items. When displayed, only a certain number of items will be drawn at a time. Automatic navigation controls are added so players can easily move back and forth to different "pages" of items in the menu.
    • "Previous" is always drawn as the first navigation item, third from the last supported position. This will not be drawn if the menu only contains one page. If there are no previous pages, the text will not be drawn on either style; if possible, the menu will be padded so spacing is consistent.
      • Valve Style position: 6
      • Radio Style position: 8
    • "Next" is always drawn as the second navigation item, second from the last supported position. This will not be drawn if the menu only contains one page. If there are no further pages, the text will not be drawn on either style; if possible, the menu will be padded so spacing is consistent.
      • Valve Style position: 7
      • Radio Style position: 9
    • "Exit" is drawn if the menu has the exit button property set. It is always the last supported item position.
      • Valve Style position: 8
      • Radio Style position: 10

The purpose of Menus is to simplify the procedure of storing, drawing, and calculating the selection of items. Thus, menus do not allow for adding raw text, as that would considerably complicate the drawing algorithm. Note: The C++ API supports hooking IBaseMenu drawing procedures and adding raw text; this will be added to the scripting API soon.

Internally, Menus are drawn via a RenderMenu algorithm. This algorithm creates a temporary panel and fills it with items from menus. This panel is then displayed to a client. The algorithm attempts to create a consistent feel across all menus, and across all styles. Thus any menu displayed via the IBaseMenu class, or Menu Handles, will look and act the same, and the Menu API is based off the Panel API.


Callbacks

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 C++, an IBaseMenu pointer is always available. For SourcePawn, a Menu Handle and a MenuAction are always set in the MenuHandler callback. Unlike C++, the SourcePawn API allows certain actions to only be called if they are requested at menu creation time. This is an optimization. However, certain actions cannot be prevented from being called.

  • Start. The menu has been acknowledged. This does not mean it will be displayed; however, it guarantees that "OnMenuEnd" will be called.
    • OnMenuStart() in C++.
    • MenuAction_Start in SourcePawn. This action is not triggered unless requested.
      • param1: Ignored (always 0).
      • param2: Ignored (always 0).
  • Display. The menu is being displayed to a client.
    • OnMenuDisplay() in C++. An IMenuPanel pointer and client index are available.
    • MenuAction_Display in SourcePawn. This action is not triggered unless requested.
      • param1: A client index.
      • param2: A Handle to a menu panel.
  • Select. An item on the menu has been selected. The item position given will be the position in the menu, rather than the key pressed (unless the menu is a raw panel).
    • OnMenuSelect() in C++. A client index and item position are passed.
    • MenuAction_Select in SourcePawn. This action is always triggerable, whether requested or not.
      • param1: A client index.
      • param2: An item position.
  • Cancel. The menu's display to one client has been cancelled.
    • OnMenuCancel() in C++. A reason for cancellation is provided.
    • MenuAction_Cancel in SourcePawn. This action is always triggerable, whether requested or not.
      • 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() in C++.
    • MenuAction_End in SourcePawn. This action is always triggered, whether requested or not.
      • param1: A menu end reason code.
      • param2: If param1 was MenuEnd_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. For C++, the IBaseMenu pointer will always be NULL. For SourcePawn, the menu Handle will always be INVALID_HANDLE or null.

  • Select. A key 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() in C++. A client index and key number pressed are passed.
    • MenuAction_Select in SourcePawn.
      • param1: A client index.
      • param2: Number of the key pressed.
  • Cancel. The menu's display to one client has been cancelled.
    • OnMenuCancel() in C++. A reason for cancellation is provided.
    • MenuAction_Cancel in SourcePawn.
      • 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. For a more in-depth guide, see Menus Step By Step (SourceMod Scripting).

public void OnPluginStart()
{
    RegConsoleCmd("menu_test1", Menu_Test1);
}
 
public int MenuHandler1(Menu menu, MenuAction action, int param1, int param2)
{
    /* If an option was selected, tell the client about the item. */
    if (action == MenuAction_Select)
    {
        char info[32];
        bool found = menu.GetItem(param2, info, sizeof(info));
        PrintToConsole(param1, "You selected item: %d (found? %d info: %s)", param2, found, info);
    }
    /* If the menu was cancelled, print a message to the server about it. */
    else if (action == MenuAction_Cancel)
    {
        PrintToServer("Client %d's menu was cancelled.  Reason: %d", param1, param2);
    }
    /* If the menu has ended, destroy it */
    else if (action == MenuAction_End)
    {
        delete menu;
    }
}
 
public Action Menu_Test1(int client, int args)
{
    Menu menu = new Menu(MenuHandler1);
    menu.SetTitle("Do you like apples?");
    menu.AddItem("yes", "Yes");
    menu.AddItem("no", "No");
    menu.ExitButton = false;
    menu.Display(client, 20);
 
    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. Note that unlike AMX Mod X, you do not need to set a timer to make sure the menu will be ended.
  • 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"):

Basic menu 1.PNG

Basic Panel

Now, let's rewrite our example to use Panels instead.

public void OnPluginStart()
{
    RegConsoleCmd("panel_test1", Panel_Test1);
}
 
public int PanelHandler1(Menu menu, MenuAction action, int param1, int param2)
{
    if (action == MenuAction_Select)
    {
        PrintToConsole(param1, "You selected item: %d", param2);
    }
    else if (action == MenuAction_Cancel)
    {
        PrintToServer("Client %d's menu was cancelled.  Reason: %d", param1, param2);
    }
}
 
public Action Panel_Test1(int client, int args)
{
    Panel panel = new Panel();
    panel.SetTitle("Do you like apples?");
    panel.DrawItem("Yes");
    panel.DrawItem("No");
 
    panel.Send(client, PanelHandler1, 20);
 
    delete panel;
 
    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 panel 1.PNG

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 maplist.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 map. Thus we'll build the menu in OnMapStart, and we won't call CloseHandle until OnMapEnd.

Source code:

Menu g_MapMenu = null;
 
public void OnPluginStart()
{
    RegConsoleCmd("menu_changemap", Command_ChangeMap);
}
 
public void OnMapStart()
{
    g_MapMenu = BuildMapMenu();
}
 
public void OnMapEnd()
{
    delete g_MapMenu;
}
 
Menu BuildMapMenu()
{
    /* Open the file */
    File file = OpenFile("maplist.txt", "rt");
    if (file == null)
    {
        return null;
    }
 
    /* Create the menu Handle */
    Menu menu = new Menu(Menu_ChangeMap);
    char mapname[255];
    while (!file.EndOfFile() && file.ReadLine(mapname, sizeof(mapname)))
    {
        if (mapname[0] == ';' || !IsCharAlpha(mapname[0]))
        {
            continue;
        }
 
        /* Cut off the name at any whitespace */
        int len = strlen(mapname);
        for (int i = 0; i < len; i++)
        {
            if (IsCharSpace(mapname[i]))
            {
                mapname[i] = '\0';
                break;
            }
        }
 
        /* Check if the map is valid */
        if (!IsMapValid(mapname))
        {
            continue;
        }
 
        /* Add it to the menu */
        menu.AddItem(mapname, mapname);
    }
 
    /* Make sure we close the file! */
    file.Close();
 
    /* Finally, set the title */
    menu.SetTitle("Please select a map:");
 
    return menu;
}
 
public int Menu_ChangeMap(Menu menu, MenuAction action, int param1, int param2)
{
    if (action == MenuAction_Select)
    {
        char info[32];
 
        /* Get item info */
        bool found = menu.GetItem(param2, info, sizeof(info));
 
        /* Tell the client */
        PrintToConsole(param1, "You selected item: %d (found? %d info: %s)", param2, found, info);
 
        /* Change the map */
        ServerCommand("changelevel %s", info);
    }
}
 
public Action Command_ChangeMap(int client, int args)
{
    if (g_MapMenu == null)
    {
        PrintToConsole(client, "The maplist.txt file was not found!");
        return Plugin_Handled;
    } 
 
    g_MapMenu.Display(client, MENU_TIME_FOREVER);
 
    return Plugin_Handled;
}

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

Basic menu 2 page1.PNG Basic menu 2 page2.PNG Basic menu 2 page3.PNG

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

You selected item: 8 (found? 1 info: cs_office)

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.

Voting

SourceMod also has API for displaying menus as votable choices to more than one client. SourceMod automatically handles selecting an item and randomly picking a tie-breaker. The voting API adds two new MenuAction values, which for vote displays, are always passed:

  • MenuAction_VoteStart: Fired after MenuAction_Start when the voting has officially started.
  • MenuAction_VoteEnd: Fired when all clients have either voted or cancelled their vote menu. The chosen item is passed through param1. This is fired before MenuAction_End. It is important to note that it does not supercede MenuAction_End, nor is it the same thing. Menus should never be destroyed in MenuAction_VoteEnd. Note: This is not called if SetVoteResultCallback() is used.
  • MenuAction_VoteCancel: Fired if the menu is cancelled while the vote is in progress. If this is called, MenuAction_VoteEnd or the result callback will not be called, but MenuAction_End will be afterwards. A vote cancellation reason is passed in param1.

The voting system extends overall menus with two additional properties:

  • Only one vote can be active at a time. You must call IsVoteInProgress() or else VoteMenu() will fail.
  • If a client votes and then disconnects while the vote is still active, the client's vote will be invalidated.

The example below shows has to create a function called DoVoteMenu() which will ask all clients whether or not they would like to change to the given map.

Simple Vote

public int Handle_VoteMenu(Menu menu, MenuAction action, int param1, int param2)
{
    if (action == MenuAction_End)
    {
        /* This is called after VoteEnd */
        delete menu;
    }
    else if (action == MenuAction_VoteEnd)
    {
        /* 0=yes, 1=no */
        if (param1 == 0)
        {
            char map[64];
            menu.GetItem(param1, map, sizeof(map));
            ServerCommand("changelevel %s", map);
        }
    }
}
 
void DoVoteMenu(const char[] map)
{
    if (IsVoteInProgress())
    {
        return;
    }
 
    Menu menu = new Menu(Handle_VoteMenu);
    menu.SetTitle("Change map to: %s?", map);
    menu.AddItem(map, "Yes");
    menu.AddItem("no", "No");
    menu.ExitButton = false;
    menu.DisplayVoteToAll(20);
}

Advanced Voting

If you need more information about voting results than MenuAction_VoteEnd gives you, you can choose to have a different callback invoked. The new callback will provide much more information, but at a price: MenuAction_VoteEnd will not be called, and you will have to decide how to interpret the results. This is done via SetVoteResultCallback().

Example:

public int Handle_VoteMenu(Menu menu, MenuAction action, int param1, int param2)
{
    if (action == MenuAction_End)
    {
        /* This is called after VoteEnd */
        delete menu;
    }
}
 
public void Handle_VoteResults(Menu menu, 
        int num_votes, 
        int num_clients, 
        const int[][] client_info, 
        int num_items, 
        const int[][] item_info)
{
    /* See if there were multiple winners */
    int winner = 0;
    if (num_items > 1
    && (item_info[0][VOTEINFO_ITEM_VOTES] == item_info[1][VOTEINFO_ITEM_VOTES]))
    {
        winner = GetRandomInt(0, 1);
    }
 
    char map[64];
    menu.GetItem(item_info[winner][VOTEINFO_ITEM_INDEX], map, sizeof(map));
    ServerCommand("changelevel %s", map);
}
 
void DoVoteMenu(const char[] map)
{
    if (IsVoteInProgress())
    {
        return;
    }
 
    Menu menu = new Menu(Handle_VoteMenu);
    menu.VoteResultCallback = Handle_VoteResults;
    menu.SetTitle("Change map to: %s?", map);
    menu.AddItem(map, "Yes");
    menu.AddItem("no", "No");
    menu.ExitButton = false;
    menu.DisplayVoteToAll(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 MenuCancel_ExitBack and MenuEnd_ExitBack. 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 MenuAction_End. The MenuAction_End is done every time a menu is closed and no longer needed.

Translations

It is possible to dynamically translate menus to each player through the MenuAction_DisplayItem callback. A special native, RedrawMenuItem, is used to transform the text while inside the callback. Let's redo the vote example from earlier to be translated:

public int Handle_VoteMenu(Menu menu, MenuAction action, int param1, int param2)
{
    if (action == MenuAction_End)
    {
        /* This is called after VoteEnd */
        delete menu;
    }
    else if (action == MenuAction_VoteEnd)
    {
        /* 0=yes, 1=no */
        if (param1 == 0)
        {
            char map[64];
            menu.GetItem(param1, map, sizeof(map));
            ServerCommand("changelevel %s", map);
        }
    }
    else if (action == MenuAction_DisplayItem)
    {
        /* Get the display string, we'll use it as a translation phrase */
        char display[64];
        menu.GetItem(param2, "", 0, _, display, sizeof(display));
 
        /* Translate the string to the client's language */
        char buffer[255];
        Format(buffer, sizeof(buffer), "%T", display, param1);
 
        /* Override the text */
        return RedrawMenuItem(buffer);
    }
    else if (action == MenuAction_Display)
    {
        /* Panel Handle is the second parameter */
        Panel panel = view_as<Panel>(param2);
 
        /* Get the map name we're changing to from the first item */
        char map[64];
        menu.GetItem(0, map, sizeof(map));
 
        /* Translate to our phrase */
        char buffer[255];
        Format(buffer, sizeof(buffer), "%T", "Change map to?", client, map);
 
        panel.SetTitle(buffer);
    }
}
 
void DoVoteMenu(const char[] map)
{
    if (IsVoteInProgress())
    {
        return;
    }
 
    Menu menu = new Menu(Handle_VoteMenu,MenuAction_DisplayItem|MenuAction_Display);
    menu.SetTitle("Change map to: %s?", map);
    menu.AddItem(map, "Yes");
    menu.AddItem("no", "No");
    menu.ExitButton = false;
    menu.DisplayVoteToAll(20);
}
Warning: This template (and by extension, language format) should not be used, any pages using it should be switched to Template:Languages

View this page in:  English  Russian  简体中文(Simplified Chinese)