Quake DeveLS - Statusbar

Author: Decker
Difficulty: Hard

This tutorial will help you understand the statusbar layout and control, in three simple steps:

- Statusbar macrolanguage [file: g_spawn.c]
- STAT_* values [file: q_shared.h]
- G_SetStats() [file: p_hud.c]

There are two different types of statusbars; one for singleplayer and one for deathmatch. The layout of both are described by a very simple macrolanguage.

First take a look at the statusbar macrolanguage, which is located in the file g_spawn.c at line 590. I've added some comments that isn't in the original code, and don't be alarmed if there are some words that you don't understand, they will be explained later:

//===================================================================

#if 0
	// cursor positioning
	xl 		// X position from the left side of the physical screen
	xr 		// X position from the right side of the physical screen
	yb 		// Y position from the bottom of the physical screen
	yt 		// Y position from the top of the physical screen
	xv 		// X position on the virtual screen (virtual screensize is 320x200)
	yv 		// Y position on the virtual screen (virtual screensize is 320x200)

	// drawing
	statpic 		// Unknown
	pic 		// Icon field, imageindex in stat-array at index 
	num  	// Number field, value in stat-array at index 
	string 		// Unknown

// DECKER: This isn't in the original source, but I've inserted it for completeness
	stat_string 	// String field, display pickup-string from itemlist-array at 
				// the index that the value in stat-array that index  points to.
				// (I'll explain later!)

	// control
	if 		// Statement is true if content of  is not zero
	ifeq  	// Statement is true if content of  is equal to 
	ifbit  	// Unknown (Maybe an OR-expression)
	endif       
#endif

char *single_statusbar = 	// Variable that contains statusbar for singleplayer
				// The macrolanguage is in one big text-variable
				
"yb	-24 "			// Set y-cursor to physical screen bottom minus 24 pixels
				// (Default icon-size is 24*24 pixels, but I guess it could vary)

// health
"xv	0 "			// Set x-cursor to virtual screen (0 pixels from left)
"hnum "				// Health number (3 digits) (controlled internally)
"xv	50 "			// Set x-cursor to virtual screen (50 pixels from left)
"pic	0 "			// Show icon where imageindex is at index 0 in the stat-array (Health-icon)

// ammo
"if 2 "				// If value at index 2 in the stat-array is not zero, then do
"	xv	100 "		// Set x-cursor to virtual screen (100 pixels from left)
"	anum "			// Ammo number (3 digits) (controlled internally)
"	xv	150 "		// Set x-cursor to virtual screen (150 pixels from left)
"	pic	2 "		// Show icon where imageindex is at index 2 in the stat-array (Ammo-icons)
"endif "			// End-statement

		// DECKER:
		// The above will say that, if there is an icon (imageindex greater than zero)
		// in the stat-array at index 2, then show ammount of ammo and the respective
		// icon for that ammo-type.

// armor
"if 4 "				// As above
"	xv	200 "
"	rnum "
"	xv	250 "
"	pic	4 "
"endif "

// selected item
"if 6 "				// As above
"	xv	296 "
"	pic 6 "
"endif "

"yb	-50 "			// Set y-cursor to physical screen bottom minus 50 pixels

// picked up item
"if 7 "				// As above
"	xv	0 "
"	pic 7 "
"	xv	26 "
"	yb	-42 "		// Set y-cursor to physical screen bottom minus 42 pixels
"	stat_string 8 "		// Display pickup-string from itemlist-array at the index that
				// the value in stat-array at index 8 points to. (Got that?)
"	yb	-50 "		// Restore y-cursor to previous value
"endif "

// timer
"if 9 "				// As above
"	xv	262 "
"	num	2	10 "	// Display a two-digit value that is contained in stat-array at index 10
"	xv	296 "
"	pic	9 "
"endif "

//  help / weapon icon 
"if 11 "			// As above
"	xv	148 "
"	pic	11 "
"endif "
;

char *dm_statusbar =		// Variable that contains statusbar for deathmatch
				// The deathmatch-statusbar only adds a frags-counter, 

"yb	-24 "

// health
"xv	0 "
"hnum "
"xv	50 "
"pic 0 "

// ammo
"if 2 "
"	xv	100 "
"	anum "
"	xv	150 "
"	pic 2 "
"endif "

// armor
"if 4 "
"	xv	200 "
"	rnum "
"	xv	250 "
"	pic 4 "
"endif "

// selected item
"if 6 "
"	xv	296 "
"	pic 6 "
"endif "

"yb	-50 "

// picked up item
"if 7 "
"	xv	0 "
"	pic 7 "
"	xv	26 "
"	yb	-42 "
"	stat_string 8 "
"	yb	-50 "
"endif "

// timer
"if 9 "
"	xv	246 "
"	num	2	10 "
"	xv	296 "
"	pic	9 "
"endif "

//  help / weapon icon 
"if 11 "
"	xv	148 "
"	pic	11 "
"endif "

//  frags
"xr	-50 "			// Set x-cursor to physical screen -50 pixels from the right
"yt 2 "				// Set y-cursor to physical screen 2 pixels from the top
"num 3 14"			// Display a three-digit value that is contained in stat-array at index 14
;

//===================================================================

Phew! Now did you understand just a little of that, that's good. If not, please read it over again.

Word of advise: The macros "hnum", "anum" and "rnum" are controlled elsewhere, they are not like the "num " which can be customized by the DLL.

So much for the macrolanguage, it alone controls _where_ numbers and icons should be displayed at the screen. Together with some real code, it also controls _when_ they are displayed, but more about that later.

Lets view the STAT_* constants in the file q_shared.h:

//===================================================================

// player_state->stats[] indexes
#define STAT_HEALTH_ICON	0
#define	STAT_HEALTH		1
#define	STAT_AMMO_ICON		2
#define	STAT_AMMO		3
#define	STAT_ARMOR_ICON		4
#define	STAT_ARMOR		5
#define	STAT_SELECTED_ICON	6
#define	STAT_PICKUP_ICON	7
#define	STAT_PICKUP_STRING	8
#define	STAT_TIMER_ICON		9
#define	STAT_TIMER		10
#define	STAT_HELPICON		11
#define	STAT_SELECTED_ITEM	12
#define	STAT_LAYOUTS		13
#define	STAT_FRAGS		14
#define	STAT_FLASHES		15	// cleared each frame, 1 = health, 2 = armor

#define	MAX_STATS		32

//===================================================================

As one can tell, there are already used 16 status-objects, but room for 16 more (16-31). You probably shouldn't try to modify any of these, because some of them are hardcoded into Quake2's kernel, not the DLL. (Hint: Try to find out if STAT_LAYOUTS are used anywhere else that in p_hud.c (I still have to figure out what it does))

These values are directly referreing to an array each player-entity has; status-array or stats[]. It is defined in file: q_shared.h in the typedef struct player_state_t (at the very bottom of the file).

Take a look at the STAT_PICKUP_ICON. It's value is 7. Now take a look at the macro:

// picked up item
"if 7 "			<-- Wohoo we got a match!
"	xv	0 "
"	pic 7 "		<-- And even one more that's matching!
"	xv	26 "
"	yb	-42 "
"	stat_string 8 "
"	yb	-50 "
"endif "

Also notice the value that STAT_PICKUP_STRING has. Can you find it in the macro?

Okay, now that we can make relations between the macrolanguage and the STAT_* constants, we move over to find out how all this is controlled, and what those stat-array, imageindex, stat_string and values are.

Look at the function G_SetStats in the file p_hud.c, which I've commented here and there:

//===================================================================

void G_SetStats (edict_t *ent)
{
	gitem_t		*item;
	int		index, cells;
	int		power_armor_type;

	//
	// health
	//
	ent->client->ps.stats[STAT_HEALTH_ICON] = level.pic_health;
	ent->client->ps.stats[STAT_HEALTH] = ent->health;

// DECKER: Now the health-icon and health have been set for this player

	//
	// ammo
	//
	if (!ent->client->ammo_index /* || !ent->client->pers.inventory[ent->client->ammo_index] */)
	{
		ent->client->ps.stats[STAT_AMMO_ICON] = 0;
		ent->client->ps.stats[STAT_AMMO] = 0;
		
// DECKER: If the player is holding a weapon that does not require any ammo, the ammo-icon and 
// ammo-ammount are cleared, thus not displaying any icons or numbers. (Remember the ammo-macro's
// if-structure)
		
	}
	else
	{
		item = &itemlist[ent->client->ammo_index];
		ent->client->ps.stats[STAT_AMMO_ICON] = gi.imageindex (item->icon);
		ent->client->ps.stats[STAT_AMMO] = ent->client->pers.inventory[ent->client->ammo_index];
		
// DECKER: Otherwise, we find the item-number of the ammo-type (ammo_index), and from that the 
// imageindex of the item-icon. We also remember to set the ammo-ammount.

	}
	
	//
	// armor
	//
	power_armor_type = PowerArmorType (ent);
	if (power_armor_type)
	{
		cells = ent->client->pers.inventory[ITEM_INDEX(FindItem ("cells"))];
		if (cells == 0)
		{	// ran out of cells for power armor
			ent->flags &= ~FL_POWER_ARMOR;
			gi.sound(ent, CHAN_ITEM, gi.soundindex("misc/power1.wav"), 1, ATTN_NORM, 0);	//FIXME powering down sound
			power_armor_type = 0;;
		}
	}

// DECKER: Above does some stuff that isn't directly related to the statusbar, other than to 
// check if power_armor ran out of cells.

	index = ArmorIndex (ent);
	if (power_armor_type && (!index || (level.framenum & 8) ) )
	{	// flash between power armor and other armor icon
		ent->client->ps.stats[STAT_ARMOR_ICON] = gi.imageindex ("i_powershield");
		ent->client->ps.stats[STAT_ARMOR] = cells;
	}
	else if (index)
	{
		item = GetItemByIndex (index);
		ent->client->ps.stats[STAT_ARMOR_ICON] = gi.imageindex (item->icon);
		ent->client->ps.stats[STAT_ARMOR] = ent->client->pers.inventory[index];
	}
	else
	{
		ent->client->ps.stats[STAT_ARMOR_ICON] = 0;
		ent->client->ps.stats[STAT_ARMOR] = 0;
	}

// DECKER: The above controls the armor and power_armor icons and values.

	//
	// pickup message
	//
	if (level.time > ent->client->pickup_msg_time)
	{
		ent->client->ps.stats[STAT_PICKUP_ICON] = 0;
		ent->client->ps.stats[STAT_PICKUP_STRING] = 0;
	}

// DECKER: Clears the pickup message and icon when time is up. It is set in file g_items.c

	//
	// timers
	//
	if (ent->client->quad_framenum > level.framenum)
	{
		ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex ("p_quad");
		ent->client->ps.stats[STAT_TIMER] = (ent->client->quad_framenum - level.framenum)/10;
	}
	else if (ent->client->invincible_framenum > level.framenum)
	{
		ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex ("p_invulnerability");
		ent->client->ps.stats[STAT_TIMER] = (ent->client->invincible_framenum - level.framenum)/10;
	}
	else if (ent->client->enviro_framenum > level.framenum)
	{
		ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex ("p_envirosuit");
		ent->client->ps.stats[STAT_TIMER] = (ent->client->enviro_framenum - level.framenum)/10;
	}
	else if (ent->client->breather_framenum > level.framenum)
	{
		ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex ("p_rebreather");
		ent->client->ps.stats[STAT_TIMER] = (ent->client->breather_framenum - level.framenum)/10;
	}
	else
	{
		ent->client->ps.stats[STAT_TIMER_ICON] = 0;
		ent->client->ps.stats[STAT_TIMER] = 0;
	}

// DECKER: Above controls all our powerups; Quad, Invulnerability, Envirosuit and Rebreather, but
// only one will be shown, even if all are active, because they are using the same index in the
// stat-array. We'll change that later in this tutorial!

	//
	// selected item
	//
	if (ent->client->pers.selected_item == -1)
		ent->client->ps.stats[STAT_SELECTED_ICON] = 0;
	else
		ent->client->ps.stats[STAT_SELECTED_ICON] = gi.imageindex (itemlist[ent->client->pers.selected_item].icon);

// DECKER: Shows what item in the players inventory that is currently selected (if any)

	ent->client->ps.stats[STAT_SELECTED_ITEM] = ent->client->pers.selected_item;

// DECKER: Now this is not refered in any of the statusbar-macros, so what it is suppose to do, who knows...

	//
	// layouts
	//
	ent->client->ps.stats[STAT_LAYOUTS] = 0;

	if (deathmatch->value)
	{
		if (ent->client->pers.health <= 0 || level.intermissiontime
			|| ent->client->showscores)
			ent->client->ps.stats[STAT_LAYOUTS] |= 1;
		if (ent->client->showinventory && ent->client->pers.health > 0)
			ent->client->ps.stats[STAT_LAYOUTS] |= 2;
	}
	else
	{
		if (ent->client->showscores)
			ent->client->ps.stats[STAT_LAYOUTS] |= 1;
		if (ent->client->showinventory && ent->client->pers.health > 0)
			ent->client->ps.stats[STAT_LAYOUTS] |= 2;
	}

// DECKER: Beats me. I can't see any change in the statusbar when any of the conditions are met.

	//
	// frags
	//
	ent->client->ps.stats[STAT_FRAGS] = ent->client->resp.score;

// DECKER: Oh yeah! Get me some more frags! >-] 
// Notice that there isn't any 'is deathmatch active' checks here, because if you run single-
// player, the frags won't show on your statusbar as the statusbar-macro does not refer it.

	//
	// help icon
	//
	if (game.helpchanged && (level.framenum & 8) )
		ent->client->ps.stats[STAT_HELPICON] = gi.imageindex ("i_help");
	else if (ent->client->pers.hand == CENTER_HANDED && ent->client->pers.weapon)
		ent->client->ps.stats[STAT_HELPICON] = gi.imageindex (ent->client->pers.weapon->icon);
	else
		ent->client->ps.stats[STAT_HELPICON] = 0;

// DECKER: Above controls the show/don't show of the help-icon and if the player set his "HAND 2"
// it displays the icon of the currently selected weapon, by finding the imageindex of it.
	
}

//===================================================================

MODIFYING

Now lets do something usefull (well almost). Let us display all powerups when they are active at the same time.

First we will need to have 3 more icons and values displayed on screen, and just for fun, we'll put them to the right bottom, instead of just above the 'selected item'-icon.

Add these lines to the other STAT_* constants in q_shared.h:

#define	STAT_TIMER2_ICON	16
#define	STAT_TIMER2		17
#define	STAT_TIMER3_ICON	18
#define	STAT_TIMER3		19
#define	STAT_TIMER4_ICON	20
#define	STAT_TIMER4		21

To show these 3 new 'timers', we have to modify the statusbar-macro, so it knows when and where to display them.

WARNING: Be aware that there should always be atleast one space between words in the macro! Be extra carefull that all lines end with an space!

In the file g_spawn.c find the *single_statusbar variable, and change it into:

char *single_statusbar = 
"yb	-24 "

// health
"xv	0 "
"hnum "
"xv	50 "
"pic 0 "

// ammo
"if 2 "
"	xv	100 "
"	anum "
"	xv	150 "
"	pic 2 "
"endif "

// armor
"if 4 "
"	xv	200 "
"	rnum "
"	xv	250 "
"	pic 4 "
"endif "

// selected item
"if 6 "
"	xv	296 "
"	pic 6 "
"endif "

"yb	-50 "

// picked up item
"if 7 "
"	xv	0 "
"	pic 7 "
"	xv	26 "
"	yb	-42 "
"	stat_string 8 "
"	yb	-50 "
"endif "

// timer
"if 9 "
"	yb	-24 "		// New. Set Y-cursor -24 pixels from physical screen bottom
"	xr	-58 "		// New. Set X-cursor -58 pixels from physical screen right
"	num	2	10 "
"	xr	-24 "		// New
"	pic	9 "
"endif "
        
// timer2			// New
"if 16 "			// New. If STAT_TIMER2_ICON is not zero, then do
"	yb	-48 "		// New
"	xr	-58 "		// New
"	num	2	17 "	// New. Display 2-digits with value from stat-array at index 17
"	xr	-24 "		// New
"	pic	16 "		// New. Display icon
"endif "			// New
        
// timer3			// New
"if 18 "			// New. If STAT_TIMER3_ICON is not zero, then do
"	yb	-72 "		// New
"	xr	-58 "		// New
"	num	2	19 "	// New. Display 2-digits with value from stat-array at index 19
"	xr	-24 "		// New
"	pic	18 "		// New. Display icon
"endif "			// New

// timer4			// New
"if 20 "			// New. If STAT_TIMER4_ICON is not zero, then do
"	yb	-96 "		// New
"	xr	-58 "		// New
"	num	2	21 "	// New. Display 2-digits with value from stat-array at index 21
"	xr	-24 "		// New
"	pic	20 "		// New. Display icon
"endif "			// New
        
//  help / weapon icon 
"if 11 "
"	xv	148 "
"	pic	11 "
"endif "
;

Now that the statusbar-layout have been done, lets move on to the control. In the file p_hud.c find the G_SetStats function, and change the timers to this:
// [...do not touch above code...]

	//
	// timers
	//
	if (ent->client->quad_framenum > level.framenum)
	{
		ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex ("p_quad");
		ent->client->ps.stats[STAT_TIMER] = (ent->client->quad_framenum - level.framenum)/10;
	}
	else							// New
	{							// New
		ent->client->ps.stats[STAT_TIMER_ICON] = 0;	// New
		ent->client->ps.stats[STAT_TIMER] = 0;		// New
	}							// New
	
	if (ent->client->invincible_framenum > level.framenum)
	{
		ent->client->ps.stats[STAT_TIMER2_ICON] = gi.imageindex ("p_invulnerability");
		ent->client->ps.stats[STAT_TIMER2] = (ent->client->invincible_framenum - level.framenum)/10;
	}
	else							// New
	{							// New
		ent->client->ps.stats[STAT_TIMER2_ICON] = 0;	// New
		ent->client->ps.stats[STAT_TIMER2] = 0;		// New
	}							// New
	
	if (ent->client->enviro_framenum > level.framenum)
	{
		ent->client->ps.stats[STAT_TIMER3_ICON] = gi.imageindex ("p_envirosuit");
		ent->client->ps.stats[STAT_TIMER3] = (ent->client->enviro_framenum - level.framenum)/10;
	}
	else							// New
	{							// New
		ent->client->ps.stats[STAT_TIMER3_ICON] = 0;	// New
		ent->client->ps.stats[STAT_TIMER3] = 0;		// New
	}							// New
	
	if (ent->client->breather_framenum > level.framenum)
	{
		ent->client->ps.stats[STAT_TIMER4_ICON] = gi.imageindex ("p_rebreather");
		ent->client->ps.stats[STAT_TIMER4] = (ent->client->breather_framenum - level.framenum)/10;
	}
	else							// New
	{							// New
		ent->client->ps.stats[STAT_TIMER4_ICON] = 0;	// New
		ent->client->ps.stats[STAT_TIMER4] = 0;		// New
	}							// New

	//
	// selected item
	//

// [...do not touch below code...]

Compile, link, copy the DLL, and run Quake2. Do a GIVE ALL (Yeah cheat is great when developing) and try activating the powerups.

If you have comments, hints, explanations or other stuff regarding Quake2-DLL coding, you are welcome to write.

Tutorial by Decker


This site, and all content and graphics displayed on it,
are ©opyrighted to the Quake DeveLS team. All rights received.
Got a suggestion? Comment? Question? Hate mail? Send it to us!
Oh yeah, this site is best viewed in 16 Bit or higher, with the resolution on 800*600.
Thanks to Planet Quake for there great help and support with hosting.
Best viewed with Netscape 4 or IE 3