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_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
. 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 inUserData.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 accesspm_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.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).// 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.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()
.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.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.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.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\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 )
{
ShutdownInput();
ShutdownUserData();
}
And voila! We're done! Start using those new user variables!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
You must log in to post a comment. You can login or register a new account.