Wavelength Logo
tl.jpg (2351 bytes) blank.gif (837 bytes) tr.jpg (2446 bytes)
blank.gif (837 bytes)
TFC Status Bar Functionality by Robin Walker
blank.gif (837 bytes)
There's a lot of Mods who'd like to do an automatic player-ID system like TFC's, so we thought we'd whip up a quick explanation on how TFC does it. It's fast and light on network traffic, and can actually be used for a lot more than just player-ID.

TFC has a generic status bar that sends a bunch of different details to the client. It's used to show player IDs, sentrygun health, and a bunch of different numerical values. We wanted to keep the network traffic down, so the status bar format is sent once, and after that only the variables need to be sent.

The first thing we do for every player is initialise their status bar. We call this function when we initialise the player's HUD in
UpdateClientData(). It clears the player's Status Bar text, for reasons you'll see further down.

// Initialise the player's status bar
void CBasePlayer::InitStatusBar()
{
m_flStatusBarDisappearDelay = 0;
m_SbarString1[0] = m_SbarString0[0] = 0;
}

The above variables are defined in CBasePlayer, as follows:

int m_izSBarState[ SBAR_END ];
float m_flNextSBarUpdateTime;
float m_flStatusBarDisappearDelay;
char m_SbarString0[ SBAR_STRING_SIZE ];
char m_SbarString1[ SBAR_STRING_SIZE ];

We have these defines:

#define MAX_ID_RANGE 2048
#define SBAR_STRING_SIZE 128
enum sbar_data
{
SBAR_ID_TARGETNAME = 1,
SBAR_ID_TARGETHEALTH,
SBAR_ID_TARGETARMOR,
SBAR_END,
};

Once we've got the player's Status Bar initialised, we want to update it regularly. We chose to update it every 0.2 seconds, which we felt was fast enough... the faster it is, the more CPU time it'll take, so we don't recommend making it faster.

At the end of UpdateClientData(), we have this code:

// Update Status Bar
if ( m_flNextSBarUpdateTime < gpGlobals->time )
{
UpdateStatusBar();
m_flNextSBarUpdateTime = gpGlobals->time + 0.2;
}

The main function behind the status bar generates the new status bar, compares it to the last one sent to the player, and only sends it if it's changed. The status bar is made up of two parts, the Status Bar Text and the Status Bar State. The Text is a string that's vaguely printf formatted, and the State is an array of int's that go with it to create the status bar the client sees. Here's an example:

Status Text: "1 %p1\n2 Health: %i2%%\n3 Armor: %i3%%"
State Array: { 0, 2, 100, 50 }

The format of the status bar is set by the Status Text. In the above case, the player's status bar would look like this:

<Name of Client 2> Health: 100 Armor: 50

Here's how it works. The Status Text is broken up into a substrings, separated by '\n'. The first thing in each substring is an index into the State array. If the value of that array is non-zero, this substring is seen by the player. So, for instance, the second substring in the above Status Text: "2 Health: %i2%%\n" indexes m_izSBarState[2]. If m_izSBarState[2] is non-zero, the player will see this substring in the status bar, minus the index itself (e.g. they won't see the 2). This is done so you can send down a large status string containing all possible parts of your status bar, instead of sending a new one down everytime you want to change it's format. In the substrings, you can use some escape sequences, which are replaced by values taken from the State array. These are as follows:

%pX : Replaced with the name of the Client specified by index X in the State array.
%iX : Replaced with the integer value of index X in the State array.
%% : Replaced with a single %

Note that all indexes into the State Array start at 1, not 0. Also note that there's actually two StatusBars, each a single line. TFC uses the first one to do ID stuff, and the second one to do TFC Class specific information (Engineer sentrygun health, Spy's current disguise, etc).

So, to do a simple ID status bar, we just need to send down the Status Text in the above example, and then just change the values in the State Array to update the status bar. Here's a simplified version of the TFC UpdateStatusBar() function, with some comments:

void CBasePlayer::UpdateStatusBar()
{
int newSBarState[ SBAR_END ];
char sbuf0[ SBAR_STRING_SIZE ];
char sbuf1[ SBAR_STRING_SIZE ];

memset( newSBarState, 0, sizeof(newSBarState) );
strcpy( sbuf0, m_SbarString0 );
strcpy( sbuf1, m_SbarString1 );

Here we create the two Status Text strings (one for each Status Bar). sbuf0 and sbuf1 are going to store the new State Text strings we want the client to see, and at the end we'll compare them to the current Status Text strings the client has, and if they're different, we'll send the new ones down.

// Find an ID Target
TraceResult tr;
UTIL_MakeVectors( pev->v_angle + pev->punchangle );
Vector vecSrc = EyePosition();
Vector vecEnd = vecSrc + (gpGlobals->v_forward * MAX_ID_RANGE);
UTIL_TraceLine( vecSrc, vecEnd, dont_ignore_monsters, edict(), &tr);
if (tr.flFraction != 1.0)
{
if ( !FNullEnt( tr.pHit ) )
{
CBaseEntity *pEntity = CBaseEntity::Instance( tr.pHit );

if (pEntity->Classify() == CLASS_PLAYER )
{
newSBarState[ SBAR_ID_TARGETNAME ] = ENTINDEX( pEntity->edict() );
strcpy( sbuf1, "1 %p1\n2 Health: %i2%%\n3 Armor: %i3%%" );

// allies and medics get to see the targets health
if ( IsAlly(pEntity) || pev->playerclass == PC_MEDIC )
{
newSBarState[ SBAR_ID_TARGETHEALTH ] = 100 * (pEntity->pev->health / pEntity->pev->max_health);
newSBarState[ SBAR_ID_TARGETARMOR ] = 100 * (pEntity->pev->armorvalue / pEntity->maxarmor);
}

m_flStatusBarDisappearDelay = gpGlobals->time + 1.0;
}
}
else if ( m_flStatusBarDisappearDelay > gpGlobals->time )
{
// hold the values for a short amount of time after viewing the object
newSBarState[ SBAR_ID_TARGETNAME ] = m_izSBarState[ SBAR_ID_TARGETNAME ];
newSBarState[ SBAR_ID_TARGETHEALTH ] = m_izSBarState[ SBAR_ID_TARGETHEALTH ];
newSBarState[ SBAR_ID_TARGETARMOR ] = m_izSBarState[ SBAR_ID_TARGETARMOR ];
}
}

The above code "fires" an invisible bullet out from the player. If it hits a player, it copies the ID Status Text into sbuf1, and sets the correct values in the State Array. Next is a chunk of code that checks this player's Class and sets up sbuf0... it works just the same as sbuf1, so I haven't included that code.

BOOL bForceResend = FALSE;

if ( strcmp( sbuf0, m_SbarString0 ) )
{
MESSAGE_BEGIN( MSG_ONE, gmsgStatusText, NULL, pev );
WRITE_BYTE( 0 );
WRITE_STRING( sbuf0 );
MESSAGE_END();

strcpy( m_SbarString0, sbuf0 );

// make sure everything's resent
bForceResend = TRUE;
}

if ( strcmp( sbuf1, m_SbarString1 ) )
{
MESSAGE_BEGIN( MSG_ONE, gmsgStatusText, NULL, pev );
WRITE_BYTE( 1 );
WRITE_STRING( sbuf1 );
MESSAGE_END();

strcpy( m_SbarString1, sbuf1 );

// make sure everything's resent
bForceResend = TRUE;
}

// Check values and send if they don't match
for (int i = 1; i < SBAR_END; i++)
{
if ( newSBarState[i] != m_izSBarState[i] || bForceResend )
{
MESSAGE_BEGIN( MSG_ONE, gmsgStatusValue, NULL, pev );
WRITE_BYTE( i );
WRITE_SHORT( newSBarState[i] );
MESSAGE_END();

m_izSBarState[i] = newSBarState[i];
}
}

This is the final piece of UpdateStatusBar(). It checks both sbuf0 and sbuf1, and compares them to m_SbarString0 and m_SbarString1. If they are different, it sends the new Status Text down to the client, and saves off a copy of the Text for the next time we update the status bar. This way, we only need to send a new Status Text to the client when you change the actual Text, which in the case of the ID, you only do once, the first time they ID someone. Then finally, it iterates through the State Array and sends down anything that's changed. Obviously, when the Status Text changes, we need to resend all the State Array, hence the bForceResend.

And that's all there is to it. The Valve client DLL has everything you need on the client end, so unless you've written your own client dll, you just need to put the above code into your game DLL, and you'll be good to go.

All the parsing of the Status Text/State is done in the statusbar.cpp file in the Valve Client DLL, so you've got the code to that and can change it if you want to add new escape sequences, etc.

I hope that's enough for you guys/gals. I did have to mess with this code a bit to get it into a readable form, and may have broken it in the process. If there's problems with it, let me know. It'd be great if all the multiplayer Mods had good ID functionality, and I'm sure you'll all figure out new things to do with this stuff.

Update:

Some people have been caught on the fact that I didn't include the code to register the two user messages that send down the statusbar info. Sorry about that.

To register them, add this to the top of player.cpp, where all the other user messages are defined:

int gmsgStatusText = 0;
int gmsgStatusValue = 0;


And then find CBasePlayer::Spawn(), where we register all the other user messages and add these lines:

gmsgStatusText = REG_USER_MSG("StatusText", -1);
gmsgStatusValue = REG_USER_MSG("StatusValue", 3);

 

- Robin

blank.gif (837 bytes)

 

blank.gif (837 bytes)
bl.jpg (2471 bytes) blank.gif (837 bytes) br.jpg (2132 bytes)