VERC: Adding User Variables to Half-Life Last edited 21 years ago2003-02-20 17:59:00 UTC

You are viewing an older revision of this wiki page. The current revision may be more detailed and up-to-date. Click here to see the current revision of this page.

This article was recovered from an archive and needs to be reviewed

  1. The formatting may be incorrect as it was automatically converted to WikiCode from HTML, it needs to be revised and reformatted
  2. Some information may be out of date as it was written before Half-Life was available on Steam
  3. After the article is re-formatted and updated for Steam HL, remove this notice
  4. Please do not remove the archive notice from the bottom of the article.
  5. Some archive articles are no longer useful, or they duplicate information from other tutorials and entity guides. In this case, delete the page after merging any relevant information into other pages. Contact an admin to delete a page.
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 Codecommon 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_NEWVECTOR_X (1<<0)
#define FL_UPDATEUSERDATA_NEWVECTOR_Y (1<<1)
#define FL_UPDATEUSERDATA_NEWVECTOR_Z (1<<2)
#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 (changes / additions
are in red ). Don't forget to put #include
"userdata.h"
at the beginning of the file after the
other #includes :
class CBaseEntity
{
public:
   // 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 )
{
   CBaseEntity::Spawn();

   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
CBasePlayer::Spawn :
   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;
or
  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];
     flags |= FL_UPDATEUSERDATA_NEWVECTOR_X;
   }

   if (pUserData->vecNewVector[1] != pOldUserData->vecNewVector[1])
   {
     pOldUserData->vecNewVector[1] = pUserData->vecNewVector[1];
     flags |= FL_UPDATEUSERDATA_NEWVECTOR_Y;
   }

   if (pUserData->vecNewVector[2] != pOldUserData->vecNewVector[2])
   {
     pOldUserData->vecNewVector[2] = pUserData->vecNewVector[2];
     flags |= FL_UPDATEUSERDATA_NEWVECTOR_Z;
   }

   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);
     WRITE_BYTE(flags);
     if (flags & FL_UPDATEUSERDATA_NEWVECTOR_X)
       WRITE_SHORT((int)(pUserData->vecNewVector[0]*65536/360.0));
     if (flags & FL_UPDATEUSERDATA_NEWVECTOR_Y)
       WRITE_SHORT((int)(pUserData->vecNewVector[1]*65536/360.0));
     if (flags & FL_UPDATEUSERDATA_NEWVECTOR_Z)
       WRITE_SHORT((int)(pUserData->vecNewVector[2]*65536/360.0));
     if (flags & FL_UPDATEUSERDATA_FUEL)
       WRITE_SHORT(pUserData->flFuel * 65535 / MAX_FUEL);
     MESSAGE_END();
   }
 }

   #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"
#include
#include

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()
{
   HOOK_USERDATA_MESSAGE(UserData);

        // 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
   ClearUserData(&gMostRecentUserData);
}

/************************************
* 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_notice,
   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_warning,
   at_error,
   at_logged //Server print to console(only in multiplayer games).
} ALERT_TYPE;
#endif

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

#ifdef __cplusplus
extern "C"
#endif
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 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" 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;
   }
   else
      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;

#ifdef CLIENT_DLL
   PM_UpdateUserDataPointer();
   // 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?
#endif

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

   PM_CheckCollision();
}
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.      *
******************************************/
#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
And add this at the beginning of the file after the #include's :
#include "userdata.h"
#ifdef CLIENT_DLL
user_variables_list_item_t * GetUserDataNodePointer( float time,
                                                float frametime);
extern user_variables_list_item_t * gpUserDataList;
#endif
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;
      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
", 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 )
{
   ShutdownInput();
   ShutdownUserData();
}
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: entropy@singularitymod.com (Link: entropy-at-singularitymod.com)
This article was originally published on the Valve Editing Resource Collective (VERC).
TWHL only archives articles from defunct websites. For more information on TWHL's archiving efforts, please visit the TWHL Archiving Project page.

Comments

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