VERC: Adding User Variables to Half-Life Last edited 1 year ago2022-09-29 07:54:05 UTC

A note from the editor
This article made use of text colouring to differentiate between "existing" code and "new" code. This colouring is not possible on TWHL, so it's a good idea to refer back to the original archived article (see the end of the page) if you're not sure.
So you're tired of having to decide which variable you're going to have to sacrifice in order to add a new parameter to your entity? No more. Now you can have access to as
many variables as you need - even in the context of pm_shared!

Note: While the first section of the tutorial ("Adding User Variables Server-Side") is fairly simple, the rest ("Sending the Data to the Client" and "Adding User Variables Client-Side") is more advanced - it involves dynamic memory allocation and deallocation, and should only be used if you have some C++ programming experience and are familiar with dynamic memory allocation. You've been warned.

Adding User Variables Server-Side

The first step in adding new user variables is to decide what variables you want to add. For this example, we're just going to add two, the amount of fuel the player has available, flFuel (just because the mod I'm working on will involve a rocket pack, which requires fuel) and a vector, vecNewVector. Create a new file called userdata.h in the Source Code\common directory of your SDK installation and put the following lines in it:
* userdata.h                              *
* Definition of extra user variables      *

// These flags let the client know what user data is being sent
#define FL_UPDATEUSERDATA_FUEL        (1<<3)

#define MAX_FUEL 1000

typedef struct {
    float flFuel;
    vec3_t vecNewVector;
} user_variables_t;
You can add as many user variables as you wish - just add them to the user_variables_t structure. Now that we've defined what variables we want to add, we need to make sure the new variables are available for every entity, just like the entvars. Make the following changes to the declaration of CBaseEntity in cbase.h. Don't forget to put #include "userdata.h" at the beginning of the file after the other #includes:
class CBaseEntity
    // Constructor. Set engine to use C/C++ callback functions
    // pointers to engine data
    entvars_t *pev;
    // Don't need to save/restore this pointer, the engine resets it

    // Extra "user" variables
    user_variables_t UserData;
    user_variables_t OldUserData;
    // only used when sending data to the client

    // path corners
    CBaseEntity *m_pGoalEnt;// path corner we are heading towards
    CBaseEntity *m_pLink;// used for temporary link-list operations.

    // initialization functions
    virtual void Spawn( void )
        pev->iuser4 = (int)&UserData;
    virtual void Precache( void ) { return; }
So every instance of CBaseEntity (every entity in the game) will have its own UserData structure. Note the change to the Spawn() function. We're going to use iuser4 in the entvars_t structure to store a pointer to our UserData structure (the pointer is typecast to an int so the compiler won't complain). Why? The user variables in entvars_t get copied to pmove and other structures, so whenever we want to access our UserData structure for a particular entity, we can use this pointer to get to it, even if we don't have direct access to the CBaseEntity class. (You could, of course, just point to the entire class if you wanted to, but it just makes things harder to read, and it would be more difficult to get the data in C modules.) This assignment is made here so that the pointer will be valid any time after the entity has spawned. But we're not quite done. We also need to add a line to the beginning of CBasePlayer::Spawn() so that CBaseEntity::Spawn gets called:
void CBasePlayer::Spawn( void )

    pev->classname = MAKE_STRING("player");
We will need to add that line to the Spawn() function of any entity that we want to use the new user variables with (any class derived from CBaseEntity will work). We could just add pev->iuser4 = (int)&UserData; to each entity's Spawn() function, but then if we decided to change something, it would have to be changed in each function instead of just in CBaseEntity::Spawn . Now all that remains to be done is to initialize the data and it's ready to be used! Somewhere in
UserData.flFuel = 500;
OldUserData.flFuel = -1;
UserData.vecNewVector = Vector(0, 0, 0);
OldUserData.vecNewVector = Vector(-1, -1, -1);
The -1 values are just there so the data will get sent to the client when the player first spawns - see the discussion later under "Sending User Variables to the Client." Note that we can directly access UserData from any function that is a member of CBasePlayer (or any class derived from CBaseEntity. In order to access
the data from a C module (e.g. pm_shared.c), where we can't use classes, we can use one of these two forms:
    ((user_variables_t *)pmove->iuser4)->flFuel -= 10;
    user_variables_t* pUserData = (user_variables_t*)pmove->iuser4;
    pUserData->flFuel -= 10;
Both do the same thing - remember we stored the pointer to our UserData structure by typecasting it to an int and storing it in iuser4 . The iuser4 in the pmove structure is copied from entvars_t before the physics code is called. To get our pointer back to usable form, we have to typecast it back to the user_variables_t type. Note that this data is not available on the client, which also uses pm_shared.c, so if you use things as they are now, you'll need to add a #ifndef CLIENT_DLL ... #endif block to prevent errors. Also, using the data on the server side only may cause problems with client-side prediction, so it has limited utility.

The next step: sending the data to the client.

Sending User Variables to the Client

Basically, what we want to do here is send the latest data in our user variables to the client whenever the server sends out an update, then "catch" the data when it arrives on the client. We're only going to worry about sending this data to the client controlling this player; this could be extended to send this data to other players, but I haven't had a reason to do it yet.

In order to send the data to the client, we're going to create a new message, gmsgUserData (there are tutorials about creating new messages elsewhere, so I'm just going to show the code). In player.cpp, just before LinkUserMessages:
int gmsgGeigerRange = 0;
int gmsgTeamNames = 0;

int gmsgUserData = 0;
At the end of LinkUserMessages :
gmsgAmmoX = REG_USER_MSG("AmmoX", 2);
gmsgTeamNames = REG_USER_MSG( "TeamNames", -1 );

// -1 means a variable-length message
gmsgUserData = REG_USER_MSG( "UserData", -1);
Next, in client.cpp, in UpdateClientData, we add the code to send the user variables to the client:
cd->pushmsec = ent->v.pushmsec;

// Flag this data as a server update so the client will know
// to load the new data into the user data structure
cd->iuser4 = 1;

// Send extra user data
CBasePlayer * pPlayer = (CBasePlayer *)CBaseEntity::Instance((struct edict_s *)ent);

if (pPlayer && pPlayer->m_fGameHUDInitialized)
    user_variables_t * pUserData = &pPlayer->UserData;
    user_variables_t * pOldUserData = &pPlayer->OldUserData;
    unsigned char flags = 0;

    if (pUserData->vecNewVector[0] != pOldUserData->vecNewVector[0])
        pOldUserData->vecNewVector[0] = pUserData->vecNewVector[0];

    if (pUserData->vecNewVector[1] != pOldUserData->vecNewVector[1])
        pOldUserData->vecNewVector[1] = pUserData->vecNewVector[1];

    if (pUserData->vecNewVector[2] != pOldUserData->vecNewVector[2])
        pOldUserData->vecNewVector[2] = pUserData->vecNewVector[2];

    if (pUserData->flFuel != pOldUserData->flFuel)
        pOldUserData->flFuel = pUserData->flFuel;
        flags |= FL_UPDATEUSERDATA_FUEL;

    if (flags) // if flags == 0, there's no new data to send
        MESSAGE_BEGIN(MSG_ONE, gmsgUserData, NULL,
                                            (struct edict_s *)ent);
        if (flags & FL_UPDATEUSERDATA_FUEL)
        WRITE_SHORT(pUserData->flFuel * 65535 / MAX_FUEL);

#if defined( CLIENT_WEAPONS )
if ( sendweapons )
Also add this at the beginning of the file:
extern int giPrecacheGrunt;
extern int gmsgSayText;

extern int gmsgUserData;
Don't worry about the cd->iuser4 = 1 for now - I'll explain it The clientdata structure only gets sent to the client, so changing this value will not overwrite the pointer we so carefully stored in iuser4 previously. First, we get a pointer to the player that is being updated. Then we check m_fGameHUDInitialized before sending anything, because there's no point in sending the data until the HUD is initialized - it just gets lost. Next, for convenience, we get pointers to the UserData and OldUserData structures (pPlayer->UserData could be used in place of pUserData - it's just easier to read this way, I think). Then flags is initialized to 0. flags will be used to indicate which pieces of data have changed and therefore need to be sent across the network. The series of if statements that follow check to see if the user variables have changed since the last time they were sent to the client. If so, the flag for that data is set and the OldUserData structure is updated. Next we have if (flags). If flags is still 0 (false) at this point, it means none of the data has changed, so none of the flags were set. Since none of the data has changed, there's no need to send it to the client, so we only send the data if flags is nonzero. The body of the if statement sends the data. First, flags is sent so the client knows which fields of user_variables_t are being sent. Then the flag for each variable is checked and if it's set, that variable is sent over the network. (Note: this may be an unnecessary complication and may not be the best method for reducing network traffic. You may want to try sending all the variables every time and only use this or another means of reducing traffic if it creates lag).

Next, we need to add code to read the message on the client side. This will be similar to adding a HUD message. First, add this to cl_util.h:
// Macros to hook function calls into the HUD object
#define HOOK_MESSAGE(x) gEngfuncs.pfnHookUserMsg(#x, __MsgFunc_##x);

#define DECLARE_MESSAGE(y, x)  \
    int __MsgFunc_##x(const char *pszName, int iSize, void *pbuf) \
    { \
        return gHUD.##y.MsgFunc_##x(pszName, iSize, pbuf ); \

// For Extra user data messages
#define HOOK_USERDATA_MESSAGE(x) gEngfuncs.pfnHookUserMsg(#x, UserDataMsgFunc_##x );
Then create a new file in your cl_dll folder called "userdata.cpp" and add this file to your cl_dll project. Here's the whole file (don't worry if you don't understand some of this - we'll get to it):
* userdata.cpp                      *
*                                   *
* Functions for reading userdata    *
*                                   *

#include "hud.h"
#include "cl_util.h"
#include "parsemsg.h"
#include "userdata.h"

user_variables_t gMostRecentUserData;

extern "C" user_variables_list_item_t * gpUserDataList;

int UserDataMsgFunc_UserData(const char *pszName, int iSize, void *pbuf)
   int index = -1;

   BEGIN_READ(pbuf, iSize);
   unsigned char flags = READ_BYTE();
   if (flags & FL_UPDATEUSERDATA_NEWVECTOR_X) gMostRecentUserData.vecNewVector[0] = READ_HIRESANGLE();
   if (flags & FL_UPDATEUSERDATA_NEWVECTOR_Y) gMostRecentUserData.vecNewVector[1] = READ_HIRESANGLE();
   if (flags & FL_UPDATEUSERDATA_NEWVECTOR_Z) gMostRecentUserData.vecNewVector[2] = READ_HIRESANGLE();
   if (flags & FL_UPDATEUSERDATA_FUEL) gMostRecentUserData.flFuel = READ_UNSIGNED_SHORT() * MAX_FUEL / 65535.0;

   return 1;

* InitUserData                      *
*                                   *
* Create user data list             *
* If a user data list already       *
* exists, destroy it first          *
void InitUserData()

        // only true if a list was already created
   while (gpUserDataList)
      user_variables_list_item_t * item = gpUserDataList;
      gpUserDataList = gpUserDataList->pPrevious;
      delete item;

   // gpUserDataList is now NULL

* CopyUserData                      *
void CopyUserData(user_variables_t * from, user_variables_t * to)
   to->flFuel = from->flFuel;
   VectorCopy(from->vecNewVector, to->vecNewVector);

* ClearUserData                     *
void ClearUserData(user_variables_t * data)
   data->flFuel = 0;
   for (int i=0; i<3; i++)
      data->vecNewVector[i] = 0;

* ShutdownUserData                  *
*                                   *
* Destroys the user data list       *
void ShutdownUserData()
   while (gpUserDataList)
      user_variables_list_item_t * item = gpUserDataList;
      gpUserDataList = gpUserDataList->pPrevious;
      delete item;
You also need to add the following to the end of the userdata.h file you created earlier:
typedef struct user_variables_list_item_s {
    user_variables_t data;
    float time;
    struct user_variables_list_item_s * pPrevious;
} user_variables_list_item_t;

// So AlertMessage can be used in entity.cpp
#ifndef EIFACE_H
typedef enum // from eiface.h
    at_console,     // same as at_notice, but forces a ConPrintf, not a message box
    at_aiconsole,   // same as at_console, but only shown if developer level is 2!
    at_logged       // Server print to console (only in multiplayer games).

void AlertMessage( ALERT_TYPE atype, char *szFmt, ... );

#ifdef __cplusplus
extern "C"
void CopyUserData(user_variables_t * from, user_variables_t* to);
void ClearUserData(user_variables_t * data);
void ShutdownUserData();
Now, what's all this stuff for? HOOK_USERDATA_MESSAGE is a macro that is modeled after the HOOK_MESSAGE macro used for HUD messages. It's not entirely necessary, since only one function uses it, but I put it in there when I was planning on having multiple messages and left it in in case I need multiple messages later. It is used in InitUserData() to tell the engine to send all "UserData" messages (that we defined in player.cpp and sent in UpdateClientData - remember?) to the function UserDataMsgFunc_UserData, which takes care of reading the data from the network stream as follows: first BEGIN_READ is called to get ready to read the data from the buffer (it sets up pointers to keep track of what we have read so far - look at the functions in parsemsg.cpp if you're interested in knowing how it works). Next, flags is read (remember this one?). Then a series of if statements check to see whether a flag is set for each of our variables, and if so, it is read into a user_variables_t structure called gMostRecentUserData for use later.

Side note: You may have noticed that flFuel was multiplied by 65535 / MAX_FUEL before it was sent by the server (in UpdateClientData) and is now being multiplied by MAX_FUEL / 65535. This is a trick to send a floating point value as a short int (saving 2 bytes) while preserving as much precision as possible. Also note that in order to do this, the fuel value sent over the network has to be an unsigned short. I added READ_UNSIGNED_SHORT to do this - the code for it is at the end of the tutorial if you want it, since it really has nothing to do with the task at hand, and it's really nothing special anyway. The components of vecNewVector used the same trick in UpdateClientData, but the conversion back is handled by READ_HIRESANGLE().

Now that we've set things up to receive the data, we need to store it. This is where it gets hairy. In order to make the game play more smoothly (less "lag"), the client dll "predicts" where everything should be based on the last update from the server and the player's input. This means that there are multiple copies of the pmove structure in existence at any one time; when a server message arrives, the data is adjusted and everything that happened since that time is "repredicted". The practical upshot of this is that the player physics code is run and re-run multiple times, so the data for each predicted frame must be stored until a server update makes it obsolete. Because of this, we need multiple copies of our user data structure so that those variables can be used as part of the prediction process (assuming, of course, that they play a role in player physics; if they don't there's no point in doing this).

InitUserData() needs to be called when the HUD is initialized - add this to CHud::MsgFunc_ResetHUD (in hud_msg.cpp) just above the return statement:
You'll also need to add a prototype for that function, so add this at the beginning of the file after the #include's:
void InitUserData();
InitUserData() calls HOOK_USERDATA_MESSAGE as described above, then gets things ready to create a list of "frames" of user data. The while loop only executes the second and subsequent times the HUD is initialized, since gpUserDataList is initially NULL. gpUserDataList is a pointer to user_variables_list_item_t used to point to the beginning of a linked list of data "nodes" - one per predicted frame. If it is not NULL, an old list exists, which the while loop destroys. (This may make more sense after the description of how the list is created). ClearUserData is a function that simply zeroes out all the user variables. CopyUserData simply copies the user variables from one user_variables_t structure to another. These functions are used in several places, so if you add variables to the user_variables_t structure, be sure to add them to ClearUserData and CopyUserData. We'll come back to ShutdownUserData later.

Now for the real work. When a network message arrives from the server, three functions get called: HUD_TxferPredictionData, HUD_TxferLocalOverrides, and HUD_ProcessPlayerState (these functions are in entity.cpp). Several changes need to be made in entity.cpp . First, add these lines near the top of the file:
#include "pm_defs.h"
#include "pmtrace.h"
#include "userdata.h"

extern "C" { user_variables_list_item_t* gpUserDataList = NULL; }
extern user_variables_t gMostRecentUserData;

#define DLLEXPORT __declspec( dllexport )
Next, in HUD_TxferLocalOverrides, comment out the second line below and add the rest at the end of the function:
// Fire prevention
// state->iuser4 = client->iuser4;

// Add a new node to the User Data list with a time
// of msg_time and data of gMostRecentData (if gMostRecentData
// has not changed, it is because the server values have not
// changed)
user_variables_list_item_t * node = new user_variables_list_item_t;
node->time = state->msg_time * 1000; // Have to convert  to milliseconds
node->pPrevious = gpUserDataList;
gpUserDataList = node;
CopyUserData(&gMostRecentUserData;, &gpUserDataList->data);
This function is where the updated information from the server first shows up. When a new update comes in, a new "node" in our linked list of user data is created. The time that the message was sent is then stored in the node to allow matching this data to the correct pmove structure later. The node is then tacked onto the "head" of the linked list and the most recent user data is stored in the node.

Add this line in HUD_ProcessPlayerState:
dst->colormap = src->colormap;

dst->iuser4 = src->iuser4;

// Get the local player's index
cl_entity_t *player = gEngfuncs.GetLocalPlayer();
Now make these changes in HUD_TxferPredictionData (Comment out the second line and add the rest of the red code):
// Fire prevention
// pcd->iuser4 = ppcd->iuser4;

pcd->fuser2 = ppcd->fuser2;
pcd->fuser3 = ppcd->fuser3;

VectorCopy( ppcd->vuser1, pcd->vuser1 );
VectorCopy( ppcd->vuser2, pcd->vuser2 );
VectorCopy( ppcd->vuser3, pcd->vuser3 );
VectorCopy( ppcd->vuser4, pcd->vuser4 );

memcpy( wd, pwd, 32 * sizeof( weapon_data_t ) );

// Delete data older than msg_time - 50 milliseconds.
user_variables_list_item_t * node = NULL;
if (gpUserDataList) node = gpUserDataList->pPrevious;
user_variables_list_item_t * lastNode = NULL;
user_variables_list_item_t * temp;
while (node && node->time >= (1000 * pps->msg_time) - 50)
    lastNode = node;
    node = node->pPrevious;
if (lastNode) lastNode->pPrevious = NULL;
while (node)
    temp = node;
    node = node->pPrevious;
    delete temp;
if (!gpUserDataList)
    // Covers first run
    gpUserDataList = new user_variables_list_item_t;

    // convert to milliseconds, which is what is used in pmove
    gpUserDataList->time = pps->msg_time * 1000;
    gpUserDataList->pPrevious = NULL;

    // Store the pointer to the user data node with the entity state
    ps->iuser4 = (int)gpUserDataList;
    ps->iuser4 = pps->iuser4;

pcd->iuser4 = ppcd->iuser4;
I'm not 100% sure what this function does - it copies old data from one place to another when a server message comes in, but I don't know why. At any rate, it appears that the times in the pmove structure are never older than the state->msg_time seen here, so I use this opportunity to delete some of the obsolete data and free some memory. Otherwise, it just copies the pointer from one structure to the other.

Next, open up pm_shared.c (it's in the pm_shared folder). Make this addition to PM_Move:
void PM_Move ( struct playermove_s *ppmove, int server )
    assert( pm_shared_initialized );

    pmove = ppmove;

    // In effect, "saves" the current data and causes the iuser4
    // variable to point to a copy which can be modified
    // so the modified data will be associated with the
    // post-physics pmove data.
    // What, isn't that clear?

    PM_PlayerMove( ( server != 0 ) ? true : false );

And add this function before PM_Move:
* PM_UpdateUserData                                                           *
*                                                                             *
* This is only built into the client dll.                                     *
* It is used for storing the predicted values for user data. It checks in the *
* user data list to see if a data node exists for pmove->time + frametime.    *
* If not, a user data structure is added to the list.  Either way, the        *
* post-physics value for the user data is then stored in that node.           *
*                                                                             *
* pmove->iuser4 contains a pointer to that node on return to the engine.      *

void PM_UpdateUserDataPointer()
    user_variables_list_item_t * list;

    // Get a pointer to the node which will store the data
    // Have to call a C++ function so we can use 'new'
    user_variables_list_item_t * node = GetUserDataNodePointer(pmove->time, pmove->frametime * 1000);
    if (!node) return;

    // Store the data
    node->time = pmove->time + pmove->frametime * 1000;

    // Copy the data from the beginning of the current frame to
    // the new node,so it can be modified safely by any processing
    //  that occurs in pm_shared
    if (pmove->iuser4 == 1)
        // Just starting out with new server update - need to
        // associate  this pmove structure with the most recent
        // data from the server,  which should have its time
        // element equal to pmove->time.
        list = gpUserDataList;
        // It really should be != but it's a float, so allow for a little (<1ms) error
        while (list && fabs(list->time - pmove->time) > 0.5) list = list->pPrevious;
        if (!list) {
            // Traversed entire list without a match
            pmove->iuser4 = (int)gpUserDataList->pPrevious;
        else {
            pmove->iuser4 = (int)list;

    if (!pmove->iuser4)
        pmove->Con_Printf("ERROR: iuser4 is 0 in PM_UpdateUserDataPointer");
        CopyUserData((user_variables_t*)pmove->iuser4, &node->data);

    // Update the pointer so that it points to the new location -
    //  that way  the correct data is modified
    pmove->iuser4 = (int)&node->data;
#endif // CLIENT_DLL
And add this at the beginning of the file after the #includes:
#include "userdata.h"
user_variables_list_item_t * GetUserDataNodePointer(float time, float frametime);
extern user_variables_list_item_t * gpUserDataList;
This function finds the correct user data node (the one whose time matches pmove->time) after a server update. Remeber back in client.cpp when we set cd->iuser4 to 1? It was necessary to be able to tell when the data in pmove was updated by the server. If the value of pmove->iuser4 is 1, a server update has arrived. Before I forget, we need to make sure that "1" actually gets sent by the server, so we need to add this to the delta.lst file in your mod directory, at the end of the clientdata_t section:
clientdata_t none
    DEFINE_DELTA( flTimeStepSound, DT_INTEGER, 10, 1.0 ),
    DEFINE_DELTA( fuser3, DT_SIGNED | DT_FLOAT, 10, 128.0 ),
    DEFINE_DELTA( fuser4, DT_SIGNED | DT_FLOAT, 2, 128.0 ),

    DEFINE_DELTA( iuser4, DT_INTEGER, 1, 1.0)
If pmove->iuser4 is not 0 or 1, it already points to an associated userdata structure. GetUserDataNodePointer looks through the list of user data nodes to see if a node exists for pmove->time + frametime (i.e. the next frame, for which we will be predicting the data). If one is found, that means we are "repredicting" this frame, so that node is used and its data is overwritten. If none exists, this is the first time that frame has been predicted, so a new node is created for it. The current data is copied into the new node and the pointer is set to the new node - this is because the engine expects the data in pmove to be returned in the same structure after being changed - this data is then saved somewhere by the engine. So when we reach the end of PM_Move, the data in the pmove structure should reflect the state after the physics code has run. Since we need to keep the state from before the physics code was run as well as return the changed data, a copy is made to be used for the rest of the physics code, and a pointer to that copy is what is returned to the engine. Below is the body of the GetUserDataNodePointer function, which belongs at the end of entity.cpp. You may be wondering why it's not in pm_shared.c, since it's called from there. The answer is, I wanted to use new and delete to handle dynamic memory allocation, and those functions are only available in C++, not C.
* GetUserDataNodePointer                                                         *
*                                                                                *
* Returns a pointer to the user data node which is at time time + frametime. If  *
* no such node exists, it is created and added to the head of the list. This     *
* function is called from pm_shared. It is placed here because I wanted to use   *
* the 'new' operator.                                                            *
extern "C"
    user_variables_list_item_t * GetUserDataNodePointer(float time, float frametime)
        // First, check to see if a node exists with time of
        // now + frametime +/- 0.5ms (to account for rounding errors)
        user_variables_list_item_t * node = gpUserDataList;
        if (!node)
            // No list, have to create a new one
            gpUserDataList = new user_variables_list_item_t;
            gpUserDataList->pPrevious = NULL;
            return gpUserDataList;

        user_variables_list_item_t * lastNode = NULL;

        // Find a node with time now + frametime +/- 0.5 ms
        while (node && node->time > time + frametime - 0.5)
            if (node->time > time + frametime - 0.5 && node->time < time + frametime + 0.5)
                return node;
            lastNode = node;
            node = node->pPrevious;

        if (node)
        // node->time < time + frametime-0.5 but did not find an
        // actual frame.  This should mean we're at the head of the
        // list and need to create a new node
            if (lastNode)
                //Shouldn't happen
                lastNode->pPrevious = new user_variables_list_item_t;
                lastNode->pPrevious->pPrevious = node;
                return lastNode->pPrevious;
                lastNode = new user_variables_list_item_t;
                lastNode->pPrevious = node;
                gpUserDataList = lastNode;
                return lastNode;
        // Big trouble - the list is out of order
            AlertMessage(at_error, "node is NULL after searching list in GetUserDataNodePointer. time: %f    framtime: %f\n", time, frametime);
            return NULL;
} // end extern "C"
One final thing. When we're through using all this memory we've allocated, we need to release it. That's what ShutdownUserData() does. It simply traverses the list and calls delete for each node. The call to ShutdownUserData needs to be somewhere where it only gets called when the program is shutting down, so I put it in HUD_Shutdown(). (This function may be found in input.cpp)
void ShutdownUserData();

void DLLEXPORT HUD_Shutdown( void )
And voila! We're done! Start using those new user variables!

For those who want it, here's the code for READ_UNSIGNED_SHORT. Add it to parsemsg.cpp, and add a prototype for it in parsemsg.h
unsigned int READ_UNSIGNED_SHORT( void )
   int c;
   if (giRead+2 > giSize)
      giBadRead = true;
      return -1;

   c = (unsigned short)( gpBuf[giRead] + (gpBuf[giRead+1] << 8) );
   giRead += 2;

   return c;
Please send questions, comments and bug reports to:
This article was originally published on Valve Editing Resource Collective (VERC).
The archived page is available here.
TWHL only publishes archived articles from defunct websites, or with permission. For more information on TWHL's archiving efforts, please visit the TWHL Archiving Project page.


You must log in to post a comment. You can login or register a new account.