userdata.h
in/******************************************
* 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
CBaseEntity
in cbase.h
(changes / additions#include
"userdata.h"
at the beginning of the file after the#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 entityUserData
structure.Spawn()
function. We're going to useiuser4
in the entvars_t
UserData
structure (theentvars_t
get copied to pmove
andUserData
CBaseEntity
CBasePlayer::Spawn()
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()
CBaseEntity
will work). We couldpev->iuser4 = (int)&UserData;
to each entity'sSpawn()
function, but then if we decided to change something, itCBaseEntity::Spawn
. Now all that remains to be done is toCBasePlayer::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 willUserData
from anyCBasePlayer
(or any class derivedCBaseEntity
. In order to accesspm_shared.c
),((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 theUserData
structure by typecasting it to an int andiuser4
. The iuser4
in the pmove
structure is copiedentvars_t
before the physics code is called. To get ouruser_variables_t
type. Note that this data is not available onpm_shared.c
, so if you use things as#ifndef CLIENT_DLL ?
#endif
block to prevent errors. Also, using the data on thegmsgUserData
(there are tutorialsplayer.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 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 itclientdata
structure only gets sentiuser4
previously. First, wem_fGameHUDInitialized
before sending anything, because there's noUserData
OldUserData
structures ( pPlayer->UserData
could be used inpUserData
- it's just easier to read this way, I think).flags
is initialized to 0
. flags
will be used to indicateif
statements that follow checkOldUserData
structure is updated. Next we have if (flags)
. Ifflags
is still 0
(false) at this point, it means none of the dataif
flags
is sent so the clientuser_variables_t
are being sent. Then the flag forcl_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/************************************
* 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 youtypedef 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 thatHOOK_MESSAGE
macro used for HUD messages.InitUserData()
to tell the engine to send allplayer.cpp
andUpdateClientData
- remember?) to the functionUserDataMsgFunc_UserData
, which takes care of reading the dataBEGIN_READ
is called toparsemsg.cpp
if you're interested in knowing how it works).flags
is read (remember this one?). Then a series of if
user_variables_t
gMostRecentUserData
for use later. Side note:flFuel
was multiplied by 65535 /
MAX_FUEL
before it was sent by the server (in UpdateClientData
)MAX_FUEL / 65535
. This is a trickREAD_UNSIGNED_SHORT
to do this
vecNewVector
UpdateClientData
, but the conversion backREAD_HIRESANGLE()
.InitUserData()
needs to be called when the HUD is initialized -CHud::MsgFunc_ResetHUD
( in hud_msg.cpp
)return
statement:
InitUserData();
You'll also need to add a prototype for that function, so add this at#include's
:
void InitUserData();
InitUserData()
calls HOOK_USERDATA_MESSAGE
while
loop only executes the second and subsequent times the HUDgpUserDataList
is initially NULL.
gpUserDataList
is a pointer to user_variables_list_item_t
used toNULL
, an old list exists,while
loop destroys. (This may make more sense afterClearUserData
is aCopyUserData
simply copies the user variables from oneuser_variables_t
structure to another. These functions are useduser_variables_t
structure, be sure to add them to ClearUserData
CopyUserData
. We'll come back to ShutdownUserData
later.HUD_TxferPredictionData
,HUD_TxferLocalOverrides
, and HUD_ProcessPlayerState
(theseentity.cpp
). Several changes need to be made inentity.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 // 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 firstpmove
structure later. The node is thenHUD_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 // 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 datapmove
structure are neverstate->msg_time
seen here, so I use thispm_shared.c
(it's in the pm_shared folder). Make thisPM_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 timepmove->time
) after a server update. Remeber back in client.cpp
cd->iuser4
to 1
? It was necessary to be able to tell whenpmove
was updated by the server. If the value ofpmove->iuser4
is 1
,delta.lst
file in your modclientdata_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 alreadyGetUserDataNodePointer
looks through the list of userpmove->time + frametime
pmove
to be returned in the same structure after being changed -PM_Move
, the data in the pmove
structure shouldGetUserDataNodePointer
function, whichentity.cpp
. You may be wondering why it'spm_shared.c
, since it's called from there. The answer is, Inew
and delete
to handle dynamic memory allocation,/******************************************
* 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,ShutdownUserData()
does. Itdelete
for each node. TheShutdownUserData
needs to be somewhere where it only getsHUD_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 toparsemsg.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)
You must log in to post a comment. You can login or register a new account.