Difference between revisions of "Menus Step By Step (SourceMod Scripting)"

From AlliedModders Wiki
Jump to: navigation, search
(Fixed markup for translations file)
m (spelling mistake)
 
(16 intermediate revisions by 9 users not shown)
Line 9: Line 9:
 
Usage of the [[Translations (SourceMod Scripting)|Translations]] system is highly recommended and will be used in this example.
 
Usage of the [[Translations (SourceMod Scripting)|Translations]] system is highly recommended and will be used in this example.
  
<pawn>#define CHOICE1 "#choice1"
+
<sourcepawn>#define CHOICE1 "#choice1"
 
#define CHOICE2 "#choice2"
 
#define CHOICE2 "#choice2"
 
#define CHOICE3 "#choice3"
 
#define CHOICE3 "#choice3"
  
public OnPluginStart()
+
public void OnPluginStart()
 
{
 
{
LoadTranslations("menu_test.phrases");
+
  LoadTranslations("menu_test.phrases");
RegConsoleCmd("menu_test1", Menu_Test1);
+
  RegConsoleCmd("menu_test1", Menu_Test1);
 
}
 
}
  
public MenuHandler1(Handle:menu, MenuAction:action, param1, param2)
+
public int MenuHandler1(Menu menu, MenuAction action, int param1, int param2)
 
{
 
{
switch(action)
+
  switch(action)
{
+
  {
case MenuAction_Start:
+
    case MenuAction_Start:
{
+
    {
PrintToServer("Displaying menu");
+
      PrintToServer("Displaying menu");
}
+
    }
+
   
case MenuAction_Display:
+
    case MenuAction_Display:
{
+
    {
SetMenuTitle(menu, "%T", "Menu Title", param1);
+
      char buffer[255];
PrintToServer("Client %d was sent menu with panel %x", param1, param2);
+
      Format(buffer, sizeof(buffer), "%T", "Vote Nextmap", param1);
}
+
 
+
      Panel panel = view_as<Panel>(param2);
case MenuAction_Select:
+
      panel.SetTitle(buffer);
{
+
      PrintToServer("Client %d was sent menu with panel %x", param1, param2);
decl String:info[32];
+
    }
GetMenuItem(menu, param2, info, sizeof(info));
+
   
if (param1 == 1 && StrEqual(info, CHOICE3))
+
    case MenuAction_Select:
{
+
    {
PrintToServer("Client %d somehow selected %s despite it being disabled", param1, info);
+
      char info[32];
}
+
      menu.GetItem(param2, info, sizeof(info));
else
+
      if (StrEqual(info, CHOICE3))
{
+
      {
PrintToServer("Client %d selected %s", param1, info);
+
        PrintToServer("Client %d somehow selected %s despite it being disabled", param1, info);
}
+
      }
}
+
      else
+
      {
case MenuAction_Cancel:
+
        PrintToServer("Client %d selected %s", param1, info);
{
+
      }
PrintToServer("Client %d's menu was cancelled for reason %d", param1, param2);
+
    }
}
+
   
+
    case MenuAction_Cancel:
case MenuAction_End:
+
    {
{
+
      PrintToServer("Client %d's menu was cancelled for reason %d", param1, param2);
CloseHandle(menu);
+
    }
}
+
   
+
    case MenuAction_End:
case MenuAction_DrawItem:
+
    {
{
+
      delete menu;
new style;
+
    }
decl String:info[32];
+
   
GetMenuItem(menu, param2, info, sizeof(info), style);
+
    case MenuAction_DrawItem:
+
    {
if (param1 == 1 && StrEqual(info, CHOICE3))
+
      int style;
{
+
      char info[32];
return ITEMDRAW_DISABLED;
+
      menu.GetItem(param2, info, sizeof(info), style);
}
+
     
else
+
      if (StrEqual(info, CHOICE3))
{
+
      {
return style;
+
        return ITEMDRAW_DISABLED;
}
+
      }
}
+
      else
+
      {
case MenuAction_DisplayItem:
+
        return style;
{
+
      }
decl String:info[32];
+
    }
GetMenuItem(menu, param2, info, sizeof(info));
+
   
+
    case MenuAction_DisplayItem:
decl String:display[64];
+
    {
+
      char info[32];
if (StrEqual(info, CHOICE3))
+
      menu.GetItem(param2, info, sizeof(info));
{
+
     
Format(display, sizeof(display), "%T", "Choice 3", param1);
+
      char display[64];
return RedrawMenuItem(display);
+
     
}
+
      if (StrEqual(info, CHOICE3))
}
+
      {
}
+
        Format(display, sizeof(display), "%T", "Choice 3", param1);
+
        return RedrawMenuItem(display);
return 0;
+
      }
 +
    }
 +
  }
 +
 
 +
  return 0;
 
}
 
}
  
public Action:Menu_Test1(client, args)
+
public Action Menu_Test1(int client, int args)
 
{
 
{
new Handle:menu = CreateMenu(MenuHandler1, MENU_ACTIONS_ALL);
+
  Menu menu = new Menu(MenuHandler1, MENU_ACTIONS_ALL);
SetMenuTitle(menu, "%T", "Menu Title", LANG_SERVER);
+
  menu.SetTitle("%T", "Menu Title", LANG_SERVER);
AddMenuItem(menu, CHOICE1, "Choice 1");
+
  menu.AddItem(CHOICE1, "Choice 1");
AddMenuItem(menu, CHOICE2, "Choice 2");
+
  menu.AddItem(CHOICE2, "Choice 2");
AddMenuItem(menu, CHOICE3, "Choice 3");
+
  menu.AddItem(CHOICE3, "Choice 3");
SetMenuExitButton(menu, false);
+
  menu.ExitButton = false;
DisplayMenu(menu, client, 20);
+
  menu.Display(client, 20);
+
 
return Plugin_Handled;
+
  return Plugin_Handled;
}</pawn>
+
}</sourcepawn>
  
 
The translation file that goes with it... addons/sourcemod/translations/menu_test.phrases.txt
 
The translation file that goes with it... addons/sourcemod/translations/menu_test.phrases.txt
Line 109: Line 113:
 
<pre>"Phrases"
 
<pre>"Phrases"
 
{
 
{
"Choice 3"
+
  "Choice 3"
{
+
  {
"en" "Choice Translated"
+
    "en" "Choice Translated"
}
+
  }
  
"Menu Title"
+
  "Menu Title"
{
+
  {
"en" "Menu Title Translated"
+
    "en" "Menu Title Translated"
}
+
  }
 
}</pre>
 
}</pre>
  
Line 124: Line 128:
 
=== OnPluginStart ===
 
=== OnPluginStart ===
  
<pawn>public OnPluginStart()
+
<sourcepawn>public void OnPluginStart()
 
{
 
{
LoadTranslations("menu_test.phrases");
+
  LoadTranslations("menu_test.phrases");
RegConsoleCmd("menu_test1", Menu_Test1);
+
  RegConsoleCmd("menu_test1", Menu_Test1);
}</pawn>
+
}</sourcepawn>
  
 
This block is here to register the command /menu_test1 so that this menu can be tested.
 
This block is here to register the command /menu_test1 so that this menu can be tested.
Line 138: Line 142:
 
Menu_Test1 is a ConCmd, which is beyond the scope of this page.  See: [[Commands (SourceMod Scripting)|Commands]] for more details on that.  We're going to be talking about what's in it.
 
Menu_Test1 is a ConCmd, which is beyond the scope of this page.  See: [[Commands (SourceMod Scripting)|Commands]] for more details on that.  We're going to be talking about what's in it.
  
==== CreateMenu ====
+
==== Menu ====
  
<pawn> new Handle:menu = CreateMenu(MenuHandler1, MENU_ACTIONS_ALL);</pawn>
+
<sourcepawn> Menu menu = new Menu(MenuHandler1, MENU_ACTIONS_ALL);</sourcepawn>
  
CreateMenu takes two arguments.
+
Menu takes two arguments.
  
The first is a function that matches the [http://docs.sourcemod.net/api/index.php?fastload=show&id=768& MenuHandler] callback signature.
+
The first is a function that matches the [https://sm.alliedmods.net/new-api/menus/MenuHandler 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:  
 
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:  
Line 153: Line 157:
 
* MenuAction_End - Called once when all clients have closed the menu
 
* 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_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.
+
* MenuAction_DisplayItem. - 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.
 
These will be discussed in more detail in the MenuHandler1 section.
Line 163: Line 167:
 
To specify just a few of them, you can do this:
 
To specify just a few of them, you can do this:
  
<pawn> new Handle:menu = CreateMenu(MenuHandler1, MenuAction_Start|MenuAction_Select|MenuAction_Cancel|MenuAction_End);</pawn>
+
<sourcepawn> Menu menu = new Menu(MenuHandler1, MenuAction_Start|MenuAction_Select|MenuAction_Cancel|MenuAction_End);</sourcepawn>
  
==== SetMenuTitle ====
+
==== menu.SetTitle ====
<pawn> SetMenuTitle(menu, "%T", "Menu Title", LANG_SERVER);</pawn>
+
<sourcepawn> menu.SetTitle("%T", "Menu Title", LANG_SERVER);</sourcepawn>
  
 
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.
 
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.
  
==== AddMenuItem ====
+
==== menu.AddItem ====
  
<pawn> AddMenuItem(menu, CHOICE1, "Choice 1");
+
<sourcepawn> menu.AddItem(CHOICE1, "Choice 1");
AddMenuItem(menu, CHOICE2, "Choice 2");
+
  menu.AddItem(CHOICE2, "Choice 2");
AddMenuItem(menu, CHOICE3, "Choice 3");</pawn>
+
  menu.AddItem(CHOICE3, "Choice 3");</sourcepawn>
  
 
A menu isn't useful without something in it!
 
A menu isn't useful without something in it!
AddMenuItem has 3 required arguments and one optional argument.
+
menu.AddItem has 2 required arguments and one optional argument.
  
The 3 required arguments are:
+
The 2 required arguments are:
  
The Menu handle, the Info string, and the Display string.
+
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 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.
Line 198: Line 202:
 
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.
 
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.
  
==== SetMenuExitButton ====
+
==== menu.ExitButton ====
<pawn> SetMenuExitButton(menu, false);</pawn>
+
<sourcepawn> menu.ExitButton = false;</sourcepawn>
  
 
Defaults to true.
 
Defaults to true.
Line 205: Line 209:
 
It set to false, the menu won't have an exit button.
 
It set to false, the menu won't have an exit button.
  
==== SetMenuExitBackButton ====
+
==== menu.ExitBackButton ====
<pawn> SetMenuExitBackButton(menu, false);</pawn>
+
<sourcepawn> menu.ExitBackButton = false;</sourcepawn>
  
 
(This isn't in the code above, but it should be mentioned)  
 
(This isn't in the code above, but it should be mentioned)  
Line 218: Line 222:
 
This can be used to dynamically set if a menu is being called as its own menu or as a submenu.
 
This can be used to dynamically set if a menu is being called as its own menu or as a submenu.
  
==== DisplayMenu ====
+
==== menu.Display ====
<pawn> DisplayMenu(menu, client, 20);</pawn>
+
<sourcepawn> menu.Display(client, 20);</sourcepawn>
  
DisplayMenu takes 3 arguments:
+
DisplayMenu takes 2 arguments:
  
 
A Menu handle, a client index, and the amount of time in seconds to show the menu.
 
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.
+
If you want the menu to show forever, pass MENU_TIME_FOREVER as the second argument.
  
 
=== MenuHandler1 ===
 
=== MenuHandler1 ===
<pawn>public MenuHandler1(Handle:menu, MenuAction:action, param1, param2)</pawn>
+
<sourcepawn>public int MenuHandler1(Menu menu, MenuAction action, int param1, int param2)</sourcepawn>
  
 
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.
 
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.
Line 234: Line 238:
 
The Handle argument is always the menu that called the handler.
 
The Handle argument is always the menu that called the handler.
  
The two cell arguments meaning depend on the MenuAction argument.
+
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_DisplayItem.  We will discuss each of these as we reach their code.
  
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 ====
 
==== MenuAction_Start ====
  
<pawn> case MenuAction_Start:
+
<sourcepawn>   case MenuAction_Start:
{
+
    {
PrintToServer("Displaying menu");
+
      PrintToServer("Displaying menu");
}</pawn>
+
    }</sourcepawn>
 +
 
 +
* 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_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.
Line 249: Line 257:
 
==== MenuAction_Display ====
 
==== MenuAction_Display ====
  
<pawn> case MenuAction_Display:
+
<sourcepawn>   case MenuAction_Display:
{
+
    {
SetMenuTitle(menu, "Choose somethin', will ya?");
+
      char buffer[255];
PrintToServer("Client %d was sent menu with panel %x", param1, param2);
+
      Format(buffer, sizeof(buffer), "%T", "Vote Nextmap", param1);
}</pawn>
+
 
 +
      Panel panel = view_as<Panel>(param2);
 +
      panel.SetTitle(buffer);
 +
      PrintToServer("Client %d was sent menu with panel %x", param1, param2);
 +
    }</sourcepawn>
 +
 
 +
* 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.  You will likely never need the MenuPanel handle and it is beyond the scope of this page.
+
MenuAction_Display is called once for each user a menu is displayed to.  param1 is the client, param2 is the MenuPanel handle.
  
SetMenuTitle is used to change the menu's title based on the language of the user viewing it using the Translations system.
+
SetPanelTitle is used to change the menu's title based on the language of the user viewing it using the Translations system.
  
 
==== MenuAction_Select ====
 
==== MenuAction_Select ====
<pawn> case MenuAction_Select:
+
<sourcepawn>   case MenuAction_Select:
{
+
    {
decl String:info[32];
+
      char info[32];
GetMenuItem(menu, param2, info, sizeof(info));
+
      menu.GetItem(param2, info, sizeof(info));
if (param1 == 1 && StrEqual(info, CHOICE3))
+
      if (StrEqual(info, CHOICE3))
{
+
      {
PrintToServer("Client %d somehow selected %s despite it being disabled", param1, info);
+
        PrintToServer("Client %d somehow selected %s despite it being disabled", param1, info);
}
+
      }
else
+
      else
{
+
      {
PrintToServer("Client %d selected %s", param1, info);
+
        PrintToServer("Client %d selected %s", param1, info);
}
+
      }
}</pawn>
+
    }</sourcepawn>
 +
 
 +
* 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.
 
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.
Line 282: Line 302:
 
==== MenuAction_Cancel ====
 
==== MenuAction_Cancel ====
  
case MenuAction_Cancel:
+
    case MenuAction_Cancel:
{
+
    {
PrintToServer("Client %d's menu was cancelled for reason %d", param1, param2);
+
      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.
 
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.
Line 299: Line 323:
  
 
==== MenuAction_End ====
 
==== MenuAction_End ====
<pawn> case MenuAction_End:
+
<sourcepawn>   case MenuAction_End:
{
+
    {
CloseHandle(menu);
+
      delete menu;
}</pawn>
+
    }</sourcepawn>
  
MenuAction_End is called when all clients have closed a menu.  For menus that are not going to be redisplayed, it is required that you call CloseHandle on the menu here.
+
* 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 parameters are rarely used in MenuAction_End.  param1 is the menu end reason. param2 depends on param1.
Line 315: Line 343:
 
* MenuEnd_ExitBack - The menu was exited via the ExitBack item (MenuAction_Cancel was fired with param 2 set to MenuCancel_ExitBack)
 
* MenuEnd_ExitBack - The menu was exited via the ExitBack item (MenuAction_Cancel was fired with param 2 set to MenuCancel_ExitBack)
  
Note: You do [b]not[/b] have the client index during this callback, so it's far too late to do anything useful with this information.
+
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 ====
 
==== MenuAction_DrawItem ====
<pawn> case MenuAction_DrawItem:
+
<sourcepawn>   case MenuAction_DrawItem:
{
+
    {
new style;
+
      int style;
decl String:info[32];
+
      char info[32];
GetMenuItem(menu, param2, info, sizeof(info), style);
+
      menu.GetItem(param2, info, sizeof(info), style);
+
     
if (param1 == 1 && StrEqual(info, CHOICE3))
+
      if (StrEqual(info, CHOICE3))
{
+
      {
return ITEMDRAW_DISABLED;
+
        return ITEMDRAW_DISABLED;
}
+
      }
else
+
      else
{
+
      {
return style;
+
        return style;
}
+
      }
}</pawn>
+
    }</sourcepawn>
 +
 
 +
* 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.
 
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.
Line 342: Line 374:
 
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.
 
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.
  
[b]Failing to return the current item's style if you don't change said style is a programmer error.[/b]
+
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 ====
 
==== MenuAction_DisplayItem ====
  
<pawn> case MenuAction_DisplayItem:
+
<sourcepawn>   case MenuAction_DisplayItem:
{
+
    {
decl String:info[32];
+
      char info[32];
GetMenuItem(menu, param2, info, sizeof(info));
+
      menu.GetItem(param2, info, sizeof(info));
+
     
decl String:display[64];
+
      char display[64];
+
     
if (StrEqual(info, CHOICE3))
+
      if (StrEqual(info, CHOICE3))
{
+
      {
Format(display, sizeof(display), "%T", "Choice 3", param1);
+
        Format(display, sizeof(display), "%T", "Choice 3", param1);
return RedrawMenuItem(display);
+
        return RedrawMenuItem(display);
}
+
      }
}</pawn>
+
    }</sourcepawn>
 +
 
 +
* 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.
+
MenuAction_DisplayItem 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.
 
This callback is intended for use with the Translation system.
Line 373: Line 413:
  
 
==== return ====
 
==== return ====
<pawn> return 0;</pawn>
+
<sourcepawn> return 0;</sourcepawn>
  
We have a return value at the bottom of the MenuHandler1 block.  This is to prevent this compiler warning:
+
If you handle MenuAction_DrawItem or MenuAction_DisplayItem, you will get the following warning if you fail to return 0 after the switch block:
  
 
<code>warning 209: function "MenuHandler1" should return a value</code>
 
<code>warning 209: function "MenuHandler1" should return a value</code>
  
 +
This is because MenuAction_DrawItem and MenuAction_DisplayItem have return values, while the other actions only return 0.
  
 
== The End ==
 
== 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.
 
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.

Latest revision as of 02:59, 26 August 2021

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.

The working menu example

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

  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

  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

  menu.ExitButton = false;

Defaults to true.

It set to false, the menu won't have an exit button.

menu.ExitBackButton

  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

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