Display messages with the Half-Life engine

From Bots-United Wiki

Jump to: navigation, search

There are several ways a message can be displayed on the screen with the Half-Life engine

  • in the server console
  • on the client's screen as normal text
  • on the player's HUD in the game chat area

add more (glowing HUD text, etc.)


Contents

In the server console

To display messages in the server console, use the SERVER_PRINT() macro provided by the SDK. This macro is a wrapper for the g_engfuncs.pfnServerPrint() function pointer. Note that this function has no printf()-style formatting capability, which means you must format it before printing using sprintf() or any other C/C++ string formatting facility.

Example:


 char string[256];
 
 // build the string
 sprintf (string, "Hello world, 1 + 1 = %d and my name is %s\n", 1+1, pBot->szBotName);
 
 // display the string in the server console
 SERVER_PRINT (string);
 

This would display in the console:


Hello world, 1 + 1 = 2 and my name is George W. Bush

On the client's screen as normal text

To display messages on the client's screen as normal text (without special effects), use the CLIENT_PRINTF() macro provided by the SDK. This macro is a wrapper for the g_engfuncs.pfnClientPrintf() function pointer. In spite of its name, this function has no printf()-style formatting capability, which means you must format it before printing using sprintf() or any other C/C++ string formatting facility.

You must use this macro with one of the following enums:


 typedef enum
 {
    print_console,
    print_center,
    print_chat,
 } PRINT_TYPE;
 

This typedef takes place in the eiface.h file of the SDK (as does the enginefuncs_t function pointers table, anyway).

  • print_console will send the text to this particular client's console only;
  • print_center will print the text to the center of the client's screen for a couple seconds;
  • print_chat will send the text to the top left part of the client's screen. NOTE: this is NOT the chat area!

Sample code:


 edict_t *pPlayerEdict; // pointer to a player's edict
 char string[256];
 
 // build the string
 sprintf (string, "Hello world, 1 + 1 = %d and my name is %s\n", 1+1, pBot->szBotName);
 
 // display the string on the client's screen in the top left corner
 CLIENT_PRINTF (pPlayerEdict, print_chat, string);
 

This would display:


Hello world, 1 + 1 = 2 and my name is George W. Bush

on the top left corner of pPlayerEdict's screen.

On the player's HUD in the game chat area

Real HUD messages rely on the sending of SayText network messages.

The wrapper function (here using metamod's GET_USER_MSG_ID() facility) will look like the following one:


 void HUD_print (edict_t *pClientEdict, char *string)
 {
    // this function sends "string" over the network for display on pPlayerEdict's HUD
 
    MESSAGE_BEGIN (MSG_ONE, GET_USER_MSG_ID (PLID, "SayText", NULL), NULL, pClientEdict);
    WRITE_BYTE (ENTINDEX (pClientEdict)); // print to pClientEdict's HUD
    WRITE_STRING (string); // print to HUD
    MESSAGE_END ();
 
    return; // et voilĂ 
 } 
 

If you are not relying on metamod, you must replace the GET_USER_MSG_ID macro with the network message ID number of the SayText message (which you can catch in pfnRegUserMessage()).

You have little to worry about the SayText network message being not registered. This message is among the first registered by any mentally sane game DLL. But if ever you feel paranoid, you can register it at the start of the function in the following way (metamod):


    if (GET_USER_MSG_ID (PLID, "SayText", NULL) == -1)
       REG_USER_MSG ("SayText", -1); // register the user message if the MOD DLL forgot to do it
 

Please note that you can't print multiple lines separated with \n using a single network message. You have to send one message per line.

Now you can use this function as follows:


 edict_t *pPlayerEdict; // pointer to a player's edict
 char string[256];
 
 // build the string
 sprintf (string, "Hello world, 1 + 1 = %d and my name is %s\n", 1+1, pBot->szBotName);
 
 // display the string on the client's screen in the game chat area
 HUD_print (pPlayerEdict, string);
 

This would display


Hello world, 1 + 1 = 2 and my name is George W. Bush

on pPlayerEdict's screen in the game chat area.

Other ways

just a quick dirty addition

void HUD_print_other_way (edict_t *pClientEdict, char *text)
{
   int type = HUD_PRINTTALK;

   MESSAGE_BEGIN (MSG_ONE, GET_USER_MSG_ID (PLID, "TextMsg", NULL), NULL, pClientEdict);
   WRITE_BYTE (type); // print to pClientEdict's HUD
   WRITE_STRING (text); // print to HUD
   MESSAGE_END ();

   return; // et voilĂ 
} 

posible values for type : HUD_PRINTNOTIFY, HUD_PRINTCONSOLE, HUD_PRINTTALK, HUD_PRINTCENTER, defined as macros somewhere in SDK

this type of sending a text seems to "parse" special texts such as "#Reporting_in"

Stylish client print

Oh so nice output

(not very nice portable example, just snippet from kxbot, will fix asap)

void PrintTextNice (const Entity &entity, const std::string text)
{
	functions.pfnMessageBegin	(MSG_ONE_UNRELIABLE, SVC_TEMPENTITY, 0, entity);
	functions.pfnWriteByte 		(TE_TEXTMESSAGE);
	functions.pfnWriteByte 		(1);
	functions.pfnWriteShort 	(util::FixedSigned16 (-1, 1<<13));
	functions.pfnWriteShort 	(util::FixedSigned16 (0, 1<<13));
	functions.pfnWriteByte 		(2);
	functions.pfnWriteByte 		(255);
	functions.pfnWriteByte 		(0);
	functions.pfnWriteByte 		(0);
	functions.pfnWriteByte 		(0);
	functions.pfnWriteByte 		(255);
	functions.pfnWriteByte 		(255);
	functions.pfnWriteByte 		(255);
	functions.pfnWriteByte 		(200);
	functions.pfnWriteShort 	(util::FixedUnsigned16 (0.0078125, 1<<8 )); // TODO : magic numbers, fix it
	functions.pfnWriteShort 	(util::FixedUnsigned16 (2, 1<<8 ));
	functions.pfnWriteShort 	(util::FixedUnsigned16 (6, 1<<8 ));
	functions.pfnWriteShort 	(util::FixedUnsigned16 (0.1, 1<<8));
	functions.pfnWriteString	(text.c_str());
	functions.pfnMessageEnd		();
}

more info in const.h in HL SDK

Creating and handling menus

You can use the ShowMenu message to display a HUD menu on the left side of a particular client's HUD. Any entry the client will choose will then be translated into a "menuselect N" client command sent back to the server, where N is the chosen entry's number. Your plugin/MOD will then be able to catch this client command and thus deduce which choice the client has taken, and decide which action to take accordingly.

Displaying a custom menu

Here is an example of a function to display a menu at a particular client:


 void PrintMenu (edict_t *pClientEdict, short valid_choices, char *menu_text, bool is_multi_page)
 {
    // send to pClientEdict a network message ordering his client DLL to display a menu
    MESSAGE_BEGIN (MSG_ONE_UNRELIABLE, GET_USER_MSG_ID (PLID, "ShowMenu", NULL), NULL, pClientEdict);
    WRITE_SHORT (valid_choices); // this bitmask says which menu entries are valid (see below)
    WRITE_CHAR (-1);
    WRITE_BYTE (is_multi_page ? 1 : 0); // menu entries can only display up to 9 choices
    WRITE_STRING (menu_text); // specially formatted menu text string (see below)
    MESSAGE_END (); // end of network message
 }
 

The menu text string is simply a multi-line string, with eventually extra formatting characters that depend on the MOD:


 char *menu_text = "\\yPlease choose:\n"
                   "\n"
                   "1. Quick add Bot\n"
                   "2. Add specific Bot\n"
                   "3. Kill all Bots\n"
                   "\n"
                   "\\r0. Cancel"); // '\\y', '\\r' and '\\w' are yellow, red and white color codes
 

This will display the following menu on the client's HUD:

Please choose:

1. Quick add Bot
2. Add specific Bot
3. Kill all Bots

0. Cancel

Remember that these color codes are MOD-specific. Some MODs may not implement them at all.

The valid_choices bitmask is an integer bitwise-ORed. It represents, in binary codes, which choices are valid in the menu (i.e. which choices will produce a "menuselect" client command if the client selects them, and which choices will make nothing happen if the client selects them). Valid choices are represented by their respective bit set to 1, forbidden choices are represented by their bit set to zero. For example, in our menu, we would have:


 short valid_choices = 0x207; // that is, "00000010 00000111" in binary.
 

The bits are to be read from right to left. Here, the choices 1, 2, 3 and 10 (i.e. "0") are allowed for selection, all the rest is forbidden.

If you want to avoid the trouble of setting that bitmask each time you fire a menu, you can use 0x3ff all the time. This value corresponds to 00000011 11111111 in binary, meaning that all the 10 choices are allowed. However, that will make the client send back "menuselect" commands for invalid entries also, which is a bit dirty. For this reason it is not very recommended to use this value all the time.

If your menu cannot fit in one single page, you can flag the network message to indicate a multi-page menu. In this case, the menu entry #9 should display something like "More..." to indicate the user that his choice continues on the next page. The user pressing this key will send a "menuselect" client command anyway (this to notify the server that he selected the next menu page), but it's his client DLL which will be in charge of displaying that second menu page.

Making a custom menu disappear

If, for some reason (command timeout, invalid choice, menu cancellation) you want to make SURE that a particular menu will disappear from a client's screen, you can send a ShowMenu message with all the parameters set to zero, like the following:


 void EraseMenu (edict_t *pClientEdict)
 {
    // send to pClientEdict a network message ordering his client DLL to clear all user menus
    MESSAGE_BEGIN (MSG_ONE_UNRELIABLE, GET_USER_MSG_ID (PLID, "ShowMenu", NULL), NULL, pClientEdict);
    WRITE_SHORT (0);
    WRITE_CHAR (0);
    WRITE_BYTE (0);
    WRITE_STRING (""); // note: do NOT send a NULL pointer here, but an empty string. Quite a difference ;)
    MESSAGE_END();
 }
 

This will ensure any previous menu no longer stays on pClientEdict's screen.

Handling a custom menu

Since clients are forced to send back "menuselect" commands after each menu their client DLL is told to display, all you need to do is to hook them in ClientCommand().

ClientCommand() is one of the game DLL functions from the DLL_FUNCTIONS function pointers table. It is called anytime a client enters a console command that is sent to the server, except for "impulse" commands (such as +forward, +jump, etc.) whose state is sent independently every frame in the network pack.

Just introduce a menuselect handling in this function, and don't pass it over to the engine if it concerns one of your menus. For this, you can tell whether the menuselect you are hooking is yours or is for one of the game DLL's menus, if you set a boolean flag (or any sort of marker) to TRUE as soon as you fire a menu to the client yourself. Make sure to hold one flag per client. Here, we'll assume the is_browsing_menu[] array holds 32 booleans that tell, for each client, if we have sent him a menu (and thus we're awaiting response from him) or not.


 void ClientCommand (edict_t *pEntity)
 {
    // executed if a client typed some sort of command into the console
 
    static char pcmd[128];
    static char arg1[128];
 
    snprintf (pcmd, 128, CMD_ARGV (0)); // get the command string...
    snprintf (arg1, 128, CMD_ARGV (1)); // ... and its first argument.
 
    // is this client browsing our menu ?
    else if (is_browsing_menu[ENTINDEX (pEntity) - 1])
    {
       // yes he is. so is this client command a menu choice ?
       if (FStrEq (pcmd, "menuselect"))
       {
          EraseMenu (pEntity); // erase all menus on this client
 
          // did he choose entry 1 (quick add bot) ?
          if (FStrEq (arg1, "1"))
             QuickAddBot (); // call the appropriate function
 
          // else did he choose entry 2 (add specific bot) ?
          else if (FStrEq (arg1, "2"))
             AddSpecificBot (); // call the appropriate function
 
          // else did he choose entry 3 (kill all bots) ?
          else if (FStrEq (arg1, "3"))
             KillAllBots (); // call the appropriate function
 
          return; // don't pass this command over to the engine, since we handled it ourselves.
                  // metamod coders should call the RETURN_META (MRES_SUPERCEDE) macro instead.
       }
    }
 
    // rest of ClientCommand() follows...
 

If your plugin/MOD features several different menus, you have to keep track not only of whether each client is browsing a menu or not, but which menu he browses. For this, you could just turn our is_browsing_menu[] array of booleans into an array of numbers, or pointers to menu strings, or whatever variable type that would enable you to tell the difference between menu A, menu B, and no menu at all.

Useful tips

How to give your messaging function printf()-style formatting capability

To give your function string formatting capability like printf(), use a va_list. This standard C type serves exactly that purpose. First, ensure that the varargs.h file is included (with the Source SDK it is, so you shouldn't have to worry about it). Then, implement your function this way:


 void HUD_printf (edict_t *pPlayerEdict, char *fmt, ...)
 {
    va_list argptr; // the va_list we use
    static char string[1024]; // the final string
 
    va_start (argptr, fmt); // opens a va_list
    vsprintf (string, fmt, argptr); // assemble the string together
    va_end (argptr); // close the va_list

    // "string" is now fully assembled and formatted, use it like a normal character array.

    // rest of function follows...
 

You can use this function later with a variable number of arguments


 // send a message on pPlayerEdict's screen
 HUD_printf (pPlayerEdict, "Hello world, 1 + 1 = %d and my name is %s\n", 1+1, pBot->szBotName);
 

Be sure to include any additional argument you want to use (such as "edict_t *pPlayerEdict" here) before any others.