Wayback Machine
FEB MAY Jun
Previous capture 5 Next capture
2005 2006 2007
18 captures
15 Mar 01 - 5 May 06
sparklines
Close Help

Sending messages between Client and Game DLLs

Intro

Half-Life is split into two DLLs - client.dll and hl.dll. Since Client handles all  the UI code (e.g. drawing of sprites on the HUD, weapon selection etc.), and HL handles the entities and game rules (e.g. reading a weapon or trigger entity from the map), you will need some way of sending data and communicating between the two DLLs. This tutorial covers the existing functionality for sending data between the two DLLs.

Game -> Client Communication (Updating the HUD with entity data)

There are several circumstances where you might want to update the HUD, or trigger on-screen messages etc. Lets take a look at one easy example - when a multi-player client disconnects from a server. This triggers a message to appear on the other clients screens saying "PlayerX has left the game". Look at the following lines from ClientDisconnect() in client.cpp (hl.dll project). The game DLL knows that the client is disconnecting, but it needs to call the CHudSayText object in the client DLL in order to print the text on the screen.

// Code to send some data from HL.DLL to CLIENT.DLL

char text[256];
sprintf( text, "- %s has left the game\n", STRING(pEntity->v.netname) );   // We want to display this message on the screen, but we can't do it from here. So use MESSAGE_BEGIN/END to send a request to the client DLL to handle the message printing.

MESSAGE_BEGIN( MSG_ALL, gmsgSayText, NULL ); // Set the message parameters - what type of msg + where to send
WRITE_BYTE( ENTINDEX(pEntity) );                                // Send a single byte (this entity number)
WRITE_STRING( text );                                                          // Send a string
MESSAGE_END();

You can see from this example that in order to call methods on the classes inside CLIENT.DLL, you need to use MESSAGE_BEGIN() to start the message, WRITE_*() to construct the message piece by piece, and MESSAGE_END() to complete the message do the call.

Now look at the following code extracts from saytext.cpp (client.dll project). This takes the message that is sent by the MESSAGE_BEGIN / END code and extracts the data:

// Use DECLARE_MESSAGE() to intercept "SayText" calls.
// The parameters are:
// 1) m_SayText - this is the instance variable on the CHud object that stores the CHudSayText class (see CHud class definition in hud.h)
// 2) SayText - this is the name of the message type, and is used to construct the method name that gets called by sticking "MsgFunc_" on the front: (MsgFunc_SayText)

DECLARE_MESSAGE( m_SayText, SayText );

int CHudSayText :: Init( void )
{
gHUD.AddHudElem( this );

// Call to the game engine needed to hook into the SayText message.
HOOK_MESSAGE( SayText );

InitHUDData();
CVAR_CREATE( "hud_saytext_time", "5", 0 );
return 1;
}

// This method is referenced by the DECLARE_MESSAGE() macro.
// It is the method on the object that gets called by MESSAGE_END() and the message data can be referenced from here.

int CHudSayText :: MsgFunc_SayText( const char *pszName, int iSize, void *pbuf )
{
// this calls the game engine to return the data. pbuf is a pointer to the data buffer, and iSize is the length of the buffer in bytes

BEGIN_READ( pbuf, iSize );       

// Use READ_BYTE() to unpack the the ENTINDEX() from the message. This is the number of the client who spoke the message.
int client_index = READ_BYTE();

// Use READ_STRING() to unpack the string "PlayerX has left the game", and then call SayTextPrint method to do the actual drawing on the screen
SayTextPrint( READ_STRING(), iSize - 1, client_index );

return 1;
}

You can see from this code that it uses DECLARE_MESSAGE() and HOOK_MESSAGE() to intercept the SayText message,  and link it to a method call on an object. The code within that method call can then use BEGIN_READ(), followed by READ_BYTE(), READ_STRING() etc. to retrieve the contents of the data buffer.

N.B.There is no need to call any form of END_READ() function.

Game->Client Function Reference

Function/Macro name

Description

Game (HL.DLL) side

 
MESSAGE_BEGIN() Define message type and which clients it sends to (see const.h):
Param 1) MSG_ALL = send to all clients in a multi-player game
              MSG_ONE = send to a single client in a single-player game
              MSG_PVS = standard format containing position and
                                  speed attributes of an entity etc.
              MSG_PAS = similar to PVS.
              etc.
Param 2) The message type (see DECLARE/HOOK_MESSAGE on the client side).
MESSAGE_END() Call the message handler method on the client class that is associated with this message type.
WRITE_STRING() Add a null-terminated string to the message.
WRITE_BYTE() Add a byte to the message
WRITE_FLOAT() etc
WRITE_CHAR() etc
WRITE_LONG() etc
   

Client (CLIENT.DLL) side

 
DECLARE_MESSAGE() Declares the MsgFunc_ handler method of the class.
HOOK_MESSAGE() Calls the game engine to notify it that this is the class to handle the message.
BEGIN_READ() Call the game engine to reset the data read, and retrieve the data buffer + size.
READ_STRING() Read null-terminated string from the data buffer.
READ_BYTE() Read a single byte from the data buffer.

Client -> Game communication (Updating entity data from user input)

There may also be circumstances where you want to update entity information from the client engine. Typical examples are when you're adding new input routines, new functionality that is accessed via new keypresses etc. Here's some example code from menu.cpp (Client.dll) from CHudMenu::SelectMenuItem()

// Declare static string to hold data to be passed to hl.dll
char szbuf[32];

// Send "menuselect" message to the hl.dll. To add a new message type replace this with some other data string
sprintf( szbuf, "menuselect %d\n", menu_item ); 

// Call the ClientCommand() function in hl.dll
ClientCmd( szbuf );

Now look at the following code from client.cpp (hl.dll), and teamplay_gamerules.cpp (hl.dll). The ClientCmd() call from client.dll calls the ClientCommand() function in hl.dll. This checks for a standard message, and if not it calls the ClientCommand() method on the current CGameRules object to check for single-player or multi-player specific commands.

void ClientCommand( edict_t *pEntity )
{

const char *pcmd = CMD_ARGV(0); // Call engine to retrieve the data passed in (i.e. "menuselect 10" in this example)
...

else if ( g_pGameRules->ClientCommand( GetClassPtr((CBasePlayer *)pev), pcmd ) ) // Call the CGameRules object
    {
        // MenuSelect returns true only if the command is properly handled, so don't print a warning
    }

...
}

BOOL CHalfLifeTeamplay :: ClientCommand( CBasePlayer *pPlayer, const char *pcmd )
{
    if ( FStrEq( pcmd, "menuselect" ) ) // Check this is a "menuselect" command.
    {
        if ( CMD_ARGC() < 2 ) // Check there are at least 2 arguments ("menuselect" and the number of the selected menu)
            return TRUE;

        int slot = atoi( CMD_ARGV(1) ); // Get second argument - the number of the selected menu

        // select the item from the current menu

        return TRUE;
    }

    return FALSE;
}

You can use this ClientCommand() method to add in a new keypress etc. Just add a case to the ClientCommand() method of the multiplayer class (CHalfLifeMultiplay, CHalfLifeTeamplay) or single player class (CHalfLifeRules) that handles the message.

Game->Client->Game Communication (Calling a specific entity object from the Client DLL)

This isnt too tricky, but it took me a while to figure out. Suppose you have a specific entity that you want to update from the client DLL, how do you go about this ? In my case it was for the CGameMenuText class that I've discussed in some of the other tutorials. When the player selects the option from the menu (via the client dll), the client dll must find the entity to tell it to trigger the relevant target.

When Half-Life loads up the map, it creates a global entity list that contains each map entity. The way to call a specific entity from the client DLL is to use this entity index. You have to find this entity index number from somewhere, and you can send this from the game dll to the client dll as part of a MESSAGE_BEGIN/END described above. The client DLL can then pick up this entity index number, store it, and use it again to call the game DLL when the entity needs updating.

Here's some example code showing how this all fits together. The Use method of the entity gets the entity index, and passes it to the client DLL. The client DLL then returns the entity index, along with the value selected to HL DLL. The HL DLL can then use the entity index to find the relevant entity and call it's methods.:

HL.DLL

// Use method of the entity class - runs when the entity is triggered.
// Calls CNewMenu::MsgFunc_MenuOpen() in the client DLL

void CGameMenuText::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value )
{
int iMyindex;
char szText [128];

iMyindex = entindex(); // get our global index value, for a callback from the UI

...
MESSAGE_BEGIN( MSG_ONE, gmsgMenuOpen, NULL, pActivator->pev );
WRITE_BYTE( iMyindex );                                   // send this entity number as part of the message (for callback)
WRITE_STRING( szText );                                    // Send some other data in the message
MESSAGE_END()                                                 // Call the client DLL to display the menu on the screen

...

}

CLIENT.DLL

// MenuOpen method of the CNewMenu class - called from the HL DLL by MESSAGE_END()

int CNewMenu::MsgFunc_MenuOpen ( const char *pszName, int iSize, void *pbuf )
{
BEGIN_READ ( pbuf, iSize );

m_entityID = READ_BYTE ();  // Read the entity number (int) from the message stream and store as a member variable
m_cHeaderText = READ_STRING(); // Read the rest of the data from the message stream
...

m_iFlags |= HUD_ACTIVE; // set up the menu so its active so it can be drawn

}

// SelectItem method of the CNewMenu class - called when the player selects one of the items on the menu

void CNewMenu::SelectItem ( void )
{

cl_entity_t *ent;

if ( (m_CurrentMenu) && (m_bMenuDisplayed) && ( m_CurrentMenu[m_iCurrentSelection].Selectable) )
{
    char cmd[32];

    // Construct data buffer to pass back to HL.DLL - include the entity index number that we stored earlier, and the number of the menu item that was selected
    sprintf ( cmd, "entmenuselect %d %d\n", m_entityID, m_CurrentMenu[m_iCurrentSelection].ReturnNumber );

    ClientCmd ( cmd );
...

}

}

HL.DLL

// ClientCommand method called from client DLL using ClientCmd()

BOOL CHalfLifeRules :: ClientCommand( CBasePlayer *pPlayer, const char *pcmd )
{
CBaseEntity *pEntity; // Declare as CBaseEntity, since it could be anything
int iMenuitem;
int iEntitem;

if ( FStrEq( pcmd, "entmenuselect" ) )
{
    iEntitem = atoi( CMD_ARGV(1) );   // Extract entity number from incoming data
    iMenuitem = atoi( CMD_ARGV(2) );  // Find which menu item was selected

    pEntity = CBaseEntity::Instance( g_engfuncs.pfnPEntityOfEntIndex( iEntitem ) ); // Get instance of this particular entity

    pEntity->ItemSelected(pPlayer, iMenuitem); // Call the ItemSelected method on the entity to select the item from the current menu. Note that ItemSelected is a virtual method of the entity, and needs to be added to both CBaseEntity, and whichever specific entity class is being called (see next blocks of code below).

    return TRUE;
}

return FALSE;
}

// ItemSelected method - declare as a virtual method of CBaseEntity.

class CBaseEntity
{
public:
    ...

    virtual void ItemSelected(CBasePlayer *pPlayer, int ) {return;};

}

// Override the virtual method at the entity level - update the entity and trigger the relevant menu item
void CGameMenuText::ItemSelected(CBasePlayer *pPlayer, int iIndex )
{
const char *targetName;
edict_t *pentTarget = NULL;

ALERT ( at_console, "GameMenuText %i got callback from UI\n", entindex() );
ALERT( at_console, "Activator is %s\n", STRING(pPlayer->pev->classname) );

...

}

I hope this has been useful - as always, mail me if you have any questions / comments

steve