Author: Decker
This tutorial will help you understand the statusbar layout and control, in three simple steps:
- Statusbar macrolanguage [file: g_spawn.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:
Word of advise: The macros "hnum", "anum" and "rnum" are controlled elsewhere, they are not like
the "num
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:
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:
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:
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:
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:
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,
Difficulty: Hard
- STAT_* values [file: q_shared.h]
- G_SetStats() [file: p_hud.c]
Phew! Now did you understand just a little of that, that's good. If not, please read it over again.
//===================================================================
#if 0
// cursor positioning
xl
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))
//===================================================================
// 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
//===================================================================
Also notice the value that STAT_PICKUP_STRING has. Can you find it in 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 "
MODIFYING
//===================================================================
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.
}
//===================================================================

To show these 3 new 'timers', we have to modify the statusbar-macro, so it knows when and where
to display them.
#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
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:
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 "
;
Compile, link, copy the DLL, and run Quake2. Do a GIVE ALL (Yeah cheat is great when developing)
and try activating the powerups.
// [...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...]
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