Menus Step By Step (SourceMod Scripting)
The SourceMod Menu API is fairly useful for displaying an information panel, a menu, or a vote to users. This document talks specifically about menus and how to create one.
Contents
This is the working code example that we'll be talking about in the upcoming sections. If you need to, you can come back and look at it as you read the rest of this page.
We will be using PrintToServer to print text to the server console as we run through the events.
Usage of the Translations system is highly recommended and will be used in this example.
#define CHOICE1 "#choice1" #define CHOICE2 "#choice2" #define CHOICE3 "#choice3" public void OnPluginStart() { LoadTranslations("menu_test.phrases"); RegConsoleCmd("menu_test1", Menu_Test1); } public int MenuHandler1(Menu menu, MenuAction action, int param1, int param2) { switch(action) { case MenuAction_Start: { PrintToServer("Displaying menu"); } case MenuAction_Display: { char buffer[255]; Format(buffer, sizeof(buffer), "%T", "Vote Nextmap", param1); Panel panel = view_as<Panel>(param2); panel.SetTitle(buffer); PrintToServer("Client %d was sent menu with panel %x", param1, param2); } case MenuAction_Select: { char info[32]; menu.GetItem(param2, info, sizeof(info)); if (StrEqual(info, CHOICE3)) { PrintToServer("Client %d somehow selected %s despite it being disabled", param1, info); } else { PrintToServer("Client %d selected %s", param1, info); } } case MenuAction_Cancel: { PrintToServer("Client %d's menu was cancelled for reason %d", param1, param2); } case MenuAction_End: { delete menu; } case MenuAction_DrawItem: { int style; char info[32]; menu.GetItem(param2, info, sizeof(info), style); if (StrEqual(info, CHOICE3)) { return ITEMDRAW_DISABLED; } else { return style; } } case MenuAction_DisplayItem: { char info[32]; menu.GetItem(param2, info, sizeof(info)); char display[64]; if (StrEqual(info, CHOICE3)) { Format(display, sizeof(display), "%T", "Choice 3", param1); return RedrawMenuItem(display); } } } return 0; } public Action Menu_Test1(int client, int args) { Menu menu = new Menu(MenuHandler1, MENU_ACTIONS_ALL); menu.SetTitle("%T", "Menu Title", LANG_SERVER); menu.AddItem(CHOICE1, "Choice 1"); menu.AddItem(CHOICE2, "Choice 2"); menu.AddItem(CHOICE3, "Choice 3"); menu.ExitButton = false; menu.Display(client, 20); return Plugin_Handled; }
The translation file that goes with it... addons/sourcemod/translations/menu_test.phrases.txt
"Phrases" { "Choice 3" { "en" "Choice Translated" } "Menu Title" { "en" "Menu Title Translated" } }
Step by Step
OnPluginStart
public void OnPluginStart() { LoadTranslations("menu_test.phrases"); RegConsoleCmd("menu_test1", Menu_Test1); }
This block is here to register the command /menu_test1 so that this menu can be tested.
This block is beyond the scope of this page. See: Plugin Structure for OnPluginStart, Translations for LoadTranslations, and Commands for RegConsoleCmd.
Menu_Test1
Menu_Test1 is a ConCmd, which is beyond the scope of this page. See: Commands for more details on that. We're going to be talking about what's in it.
Menu
Menu menu = new Menu(MenuHandler1, MENU_ACTIONS_ALL);
Menu takes two arguments.
The first is a function that matches the MenuHandler callback signature.
The second is a bitmask of which MenuAction events we are going to handle in our MenuHandler. There are 7 that apply to standard menus:
- MenuAction_Start - Called once when a menu is displayed
- MenuAction_Display - Called once for each client a menu is displayed to
- MenuAction_Select - Called when a client makes a selection from the menu that isn't Previous, Next, Back, or Exit.
- MenuAction_Cancel - Called when a client closes a menu or it is closed on them
- MenuAction_End - Called once when all clients have closed the menu
- MenuAction_DrawItem - Called once for each item for each user a menu is displayed to. Can change the menu item style.
- MenuAction_DisplayIitem. - Called once for each item for each user a menu is displayed to. Can change the menu item text.
These will be discussed in more detail in the MenuHandler1 section.
Of those 7, 3 are called whether you specify them or not: MenuAction_Select, MenuAction_Cancel, and MenuAction_End.
MENU_ACTIONS_DEFAULT sets just the 3 required fields, while MENU_ACTIONS_ALL specifies all 10 actions (including the 3 vote actions).
To specify just a few of them, you can do this:
Menu menu = new Menu(MenuHandler1, MenuAction_Start|MenuAction_Select|MenuAction_Cancel|MenuAction_End);
menu.SetTitle("%T", "Menu Title", LANG_SERVER);
Sets the menu's title. This example uses the translation system and the server's default language. This isn't strictly necessary as we will set the menu title again later.
menu.AddItem(CHOICE1, "Choice 1"); menu.AddItem(CHOICE2, "Choice 2"); menu.AddItem(CHOICE3, "Choice 3");
A menu isn't useful without something in it! menu.AddItem has 2 required arguments and one optional argument.
The 2 required arguments are:
The Info string, and the Display string.
The Info String is a string that is used to uniquely identify this menu item. It is never displayed to the user, but is used in various callbacks.
The Display String is the default text to be used for this item on the menu. It can be changed via the menu's MenuAction_DisplayItem callback.
The optional argument is the menu's item draw style. It can be one of these values:
- ITEMDRAW_DEFAULT - Displays text as normal.
- ITEMDRAW_DISABLED - Item is displayed and has a number, but can't be selected.
- ITEMDRAW_RAWLINE - Item is displayed, but doesn't have a number assigned to it and thus can't be selected
- ITEMDRAW_NOTEXT - Draws an item with no text. Not very useful.
- ITEMDRAW_SPACER - Item is blank, but has a number. Can't be selected.
- ITEMDRAW_IGNORE - Item is blank with no number. Can't be selected.
- ITEMDRAW_CONTROL - Don't use this one. It's used for the items SourceMod automatically adds.
Be aware that if your CreateMenu second argument includes MenuAction_DrawItem or MENU_ACTIONS_ALL and you don't actually implement the MenuAction_DrawItem callback (or implement it and don't return the current item's style if you don't change it), your style here will be completely ignored.
menu.ExitButton = false;
Defaults to true.
It set to false, the menu won't have an exit button.
menu.ExitBackButton = false;
(This isn't in the code above, but it should be mentioned)
Defaults to false.
Should only be used if the menu is a submenu, with the menu set up to check the cancel reason to see if the ExitBack action was used.
If set to true, replaces the Exit item with the Back item. The Back item still cancels the menu, but has a separate cancel reason.
This can be used to dynamically set if a menu is being called as its own menu or as a submenu.
menu.Display(client, 20);
DisplayMenu takes 2 arguments:
A Menu handle, a client index, and the amount of time in seconds to show the menu.
If you want the menu to show forever, pass MENU_TIME_FOREVER as the third argument.
MenuHandler1
public int MenuHandler1(Menu menu, MenuAction action, int param1, int param2)
MenuHandler1 is a function whose signature matches the MenuHandler callback. It can be named something other than MenuHandler1. It must always be public and it will always have these arguments: Handle, MenuAction, cell, and cell in that order.
The Handle argument is always the menu that called the handler.
The MenuAction argument is always one of 7 actions for a standard menu. They are: MenuAction_Start, MenuAction_Display, MenuAction_Select, MenuAction_Cancel, MenuAction_End, MenuAction_DrawItem, and MenuAction_DisplayIitem. We will discuss each of these as we reach their code.
The two cell arguments meaning depend on the MenuAction argument. A common mistake is to assume param1 is the client. This is incorrect for MenuAction_Start, MenuAction_End, MenuAction_VoteEnd, MenuAction_VoteStart, and MenuAction_VoteCancel.
MenuAction_Start
case MenuAction_Start: { PrintToServer("Displaying menu"); }
- param1: not set
- param2: not set
- return: 0 (or don't return)
MenuAction_Start doesn't set param1 and param2. It is fired when the menu is displayed to one or more users using DisplayMenu, DisplayMenuAtItem, VoteMenu, or VoteMenuToAll.
MenuAction_Display
case MenuAction_Display: { char buffer[255]; Format(buffer, sizeof(buffer), "%T", "Vote Nextmap", param1); Panel panel = view_as<Panel>(param2); panel.SetTitle(buffer); PrintToServer("Client %d was sent menu with panel %x", param1, param2); }
- param1: client index
- param2: MenuPanel Handle
- return: 0 (or don't return)
MenuAction_Display is called once for each user a menu is displayed to. param1 is the client, param2 is the MenuPanel handle.
SetPanelTitle is used to change the menu's title based on the language of the user viewing it using the Translations system.
MenuAction_Select
case MenuAction_Select: { char info[32]; menu.GetItem(param2, info, sizeof(info)); if (StrEqual(info, CHOICE3)) { PrintToServer("Client %d somehow selected %s despite it being disabled", param1, info); } else { PrintToServer("Client %d selected %s", param1, info); } }
- param1: client index
- param2: item number for use with GetMenuItem
- return: 0 (or don't return)
MenuAction_Select is called when a user selects a non-control item on the menu (something added using AddMenuItem). param1 is the client, param2 is the menu position of the item the client selected.
Using the item position to check which item was selected is a bad idea, as item position is brittle and will break things if AddMenuItem or InsertMenuItem is used. It is recommended that you instead use the Menu item's info string, as done in the code above.
GetMenuItem is used here to fetch the info string.
MenuAction_Cancel
case MenuAction_Cancel: { PrintToServer("Client %d's menu was cancelled for reason %d", param1, param2); }
- param1: client index
- param2: MenuCancel reason
- return: 0 (or don't return)
MenuAction_Cancel is called whenever a user closes a menu or it is closed for them for another reason. param1 is the client, param2 is the close reason.
The close reasons you can receive are:
- MenuCancel_Disconnected - The client got disconnected from the server.
- MenuCancel_Interrupted - Another menu opened, automatically closing our menu.
- MenuCancel_Exit - The client selected Exit. Not called if SetMenuExitBack was set to true. Not called if SetMenuExit was set to false.
- MenuCancel_NoDisplay - Our menu never displayed to the client for whatever reason.
- MenuCancel_Timeout - The menu timed out. Not called if the menu time was MENU_TIME_FOREVER.
- MenuCancel_ExitBack - The client selected Back. Only called if SetMenuExitBack has been called and set to true before the menu was sent. Not called if SetMenuExit was set to false.
MenuAction_End
case MenuAction_End: { delete menu; }
- param1: MenuEnd reason
- param2: If param1 is MenuEnd_Cancelled, the MenuCancel reason
- return: 0 (or don't return)
MenuAction_End is called when all clients have closed a menu or vote. For menus that are not going to be redisplayed, it is required that you call CloseHandle on the menu here.
The parameters are rarely used in MenuAction_End. param1 is the menu end reason. param2 depends on param1.
The end reasons you can receive for normal menus are:
- MenuEnd_Selected - The menu closed because an item was selected (MenuAction_Select was fired)
- MenuEnd_Cancelled - The menu was cancelled (MenuAction_Cancel was fired), cancel reason is in param2; cancel reason can be any of the ones listed in MenuAction_Cancel except MenuCancel_Exit or MenuCancel_ExitBack
- MenuEnd_Exit - The menu was exited via the Exit item (MenuAction_Cancel was fired with param2 set to MenuCancel_Exit)
- MenuEnd_ExitBack - The menu was exited via the ExitBack item (MenuAction_Cancel was fired with param 2 set to MenuCancel_ExitBack)
Note: You do not have the client index during this callback, so it's far too late to do anything useful with this information.
MenuAction_DrawItem
case MenuAction_DrawItem: { int style; char info[32]; menu.GetItem(param2, info, sizeof(info), style); if (StrEqual(info, CHOICE3)) { return ITEMDRAW_DISABLED; } else { return style; } }
- param1: client index
- param2: item number for use with GetMenuItem
- return: new ITEMDRAW properties or style from GetMenuItem. Since 0 is ITEMDRAW_DEFAULT, returning 0 clears all styles for this item.
MenuAction_DrawItem is called once for each item on the menu for each user. You can manipulate its draw style here. param1 is the client, param2 is the menu position.
Using the item position to check which item was selected is a bad idea, as item position is brittle and will break things if AddMenuItem or InsertMenuItem is used. It is recommended that you instead use the Menu item's info string, as done in the code above.
GetMenuItem is used here to fetch the info string and menu style.
You should return the style you want the menu item to have. In our example, if client 1 is viewing the menu, we want CHOICE3 to be disabled.
the return value is a bitfield, so to apply multiple styles, you do something like this:
return ITEMDRAW_NOTEXT | ITEMDRAW_SPACER;
Failing to return the current item's style if you don't change the style is a programmer error.
MenuAction_DisplayItem
case MenuAction_DisplayItem: { char info[32]; menu.GetItem(param2, info, sizeof(info)); char display[64]; if (StrEqual(info, CHOICE3)) { Format(display, sizeof(display), "%T", "Choice 3", param1); return RedrawMenuItem(display); } }
- param1: client index
- param2: item number for use with GetMenuItem
- return: return value from RedrawMenuItem or 0 for no change
MenuAction_DrawItem is called once for each item on the menu for each user. You can manipulate its text here. param1 is the client, param2 is the menu position.
This callback is intended for use with the Translation system.
Using the item position to check which item was selected is a bad idea, as item position is brittle and will break things if AddMenuItem or InsertMenuItem is used. It is recommended that you instead use the Menu item's info string, as done in the code above.
GetMenuItem is used here to fetch the info string.
Once we have the info string, we compare our item to it and apply the appropriate translation string.
If we change an item, we have to call RedrawMenuItem and return the value it returns. If we do not change an item, we must return 0.
return
return 0;
If you handle MenuAction_DrawItem or MenuAction_DisplayItem, you will get the following warning if you fail to return 0 after the switch block:
warning 209: function "MenuHandler1" should return a value
This is because MenuAction_DrawItem and MenuAction_DisplayItem have return values, while the other actions only return 0.
The End
Hopefully this walk through a simple menu helped you understand why each call is being made where. Menus have some options that we didn't explore here, such as disabling pagination (which is enabled by default). You may want to refer to the main Menu documentation for more details.