Onliners

Affiliates

Adding User Variables to Half-Life

By Jim 'Entropy' Hunter

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. Existing code
will be blue. New code or changed code will be red.
Some of the original code has been reformatted to fit the width of the Collective.

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:

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 (changes / additions
are in red). Don't forget to put #include
"userdata.h" at the beginning of the file after the
other #includes:

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:

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
CBasePlayer::Spawn:

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:

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:

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:

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:

InitUserData();

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:

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.

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:

/******************************************
* 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. *
******************************************/
#ifdef CLIENT_DLL
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");
else
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

This function finds the correct user data node (the one whose time
matchespmove->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:

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;
ClearUserData(&gpUserDataList->data);
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;
ClearUserData(&lastNode->pPrevious->data);
lastNode->pPrevious->pPrevious = node;
return lastNode->pPrevious;
}
else
{
lastNode = new user_variables_list_item_t;
ClearUserData(&lastNode->data);
lastNode->pPrevious = node;
gpUserDataList = lastNode;
return lastNode;
}
}
else
// 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)