This tutorial will allow each client that plays your mod to have their own customizable weapon switching. Quake2
had locked weapon precedences, but back in the days of the original Quake each player could choose their own
precedences. So in Quake2 the BFG was the "best" gun, then the rail gun, then hyperblaster, then rocket
launcher, etc. But in Quake you could choose your own order.
Neither of the methods in Quake3 is acceptable to the hard core gamer. So we're going to do something about it :)
This tutorial will show you how to implement Quake style weapon switching which is fully customizable on a per
user basis. Each user will have a cvar "cg_weaponOrder" that controls their weapon preferences. Let's get
started then...
bg_public.h
2. Define some Global Variables and Function Prototypes
First open cg_local.h and find the "cg_main.c" definitions:
//
// cg_main.c
//
const char *CG_ConfigString( int index );
const char *CG_Argv( int arg );
void QDECL CG_Printf( const char *msg, ... );
void QDECL CG_Error( const char *msg, ... );
void CG_StartMusic( void );
void CG_UpdateCvars( void );
int CG_CrosshairPlayer( void );
int CG_LastAttacker( void );
//WarZone
#define NUM_WEAPS 9
extern int cg_weaponsCount;
extern int weaponOrder[NUM_WEAPS];
extern int weaponRawOrder[NUM_WEAPS];
int RateWeapon (int weapon);
int NextWeapon (int curr);
int PrevWeapon (int curr);
Save and close cg_local.h, we're done with that file.
3. Define a 'cvar'
Now open cg_main.c and find the cvar definitions:
vmCvar_t cg_deferPlayers;
vmCvar_t cg_drawTeamOverlay;
vmCvar_t cg_teamOverlayUserinfo;
vmCvar_t cg_weaponOrder; //WarZone
int cg_weaponsCount = -1; //WarZone
Now find the cvar table:
{ &cg_buildScript, "com_buildScript", "0", 0 }, // force loading of all possible
data amd error on failures
{ &cg_paused, "cl_paused", "0", CVAR_ROM },
{ &cg_blood, "com_blood", "1", CVAR_ARCHIVE },
{ &cg_syncronousClients, "g_syncronousClients", "0", 0 }, // communicated by systeminfo
{ &cg_weaponOrder, "cg_weaponOrder", "1/2/3/4/5/6/7/8/9", CVAR_ARCHIVE }, //WarZone
Note: you might need to add a comma at the end of the cg_syncronousClients definition (I put that comma in cyan
but its hardly obvious).
4. Define our Utility Functions
Now we're going to add the bulk of my new code. The new code will live right below the cvar table:
{ &cg_buildScript, "com_buildScript", "0", 0 }, // force loading of all possible data amd error on failures
{ &cg_paused, "cl_paused", "0", CVAR_ROM },
{ &cg_blood, "com_blood", "1", CVAR_ARCHIVE },
{ &cg_syncronousClients, "g_syncronousClients", "0", 0 }, // communicated by systeminfo
{ &cg_weaponOrder, "cg_weaponOrder", "1/2/3/4/5/6/7/8/9", CVAR_ARCHIVE }, //WarZone
};
int cvarTableSize = sizeof( cvarTable ) / sizeof( cvarTable[0] );
//<WarZone>
int weaponOrder[NUM_WEAPS];
int weaponRawOrder[NUM_WEAPS];
int NextWeapon (int curr)
{
int i;
int w = -1;
for (i = 0; i < NUM_WEAPS; i++)
{
if (weaponRawOrder[i] == curr)
{
w = i;
break;
}
}
if (w == -1)
return curr; //shouldn't happen
return weaponRawOrder[(w + 1) % NUM_WEAPS];
}
int PrevWeapon (int curr)
{
int i;
int w = -1;
for (i = 0; i < NUM_WEAPS; i++)
{
if (weaponRawOrder[i] == curr)
{
w = i;
break;
}
}
if (w == -1)
return curr; //shouldn't happen
return weaponRawOrder[w - 1 >= 0 ? w - 1 : NUM_WEAPS - 1];
}
int RateWeapon (int weapon)
{
weapon--;
if (weapon > 8 || weapon < 0)
return 0; //bad weapon
return weaponOrder[weapon];
}
int contains(int *list, int size, int number)
{
int i;
for (i = 0; i < size; i++)
if (list[i] == number) return 1;
return 0;
}
void UpdateWeaponOrder (void)
{
char *order = cg_weaponOrder.string;
char weapon[3];
int i, start;
int tempOrder[NUM_WEAPS];
int weapUsed[NUM_WEAPS];
int temp;
weapon[1] = '\0';
memset(tempOrder, 0, sizeof(tempOrder));
memset(weapUsed, 0, sizeof(weapUsed));
i = 0;
while (order != NULL && *order != '\0' && i < NUM_WEAPS)
{
weapon[0] = *order;
order++;
if (*order != '\\' && *order != '/')
{
weapon[1] = *order;
weapon[2] = '\0';
order++;
} else {
weapon[1] = '\0';
}
if (*order != '\0')
order++;
temp = atoi( weapon );
if (temp < 1 || temp > NUM_WEAPS)
{
CG_Printf( "Error: %i is out of range. Ignoring..\n", temp );
}
else if ( contains( tempOrder, sizeof(tempOrder)/sizeof(tempOrder[0]), temp ) )
{
CG_Printf( "Error: %s (%i) already in list. Ignoring..\n",
(BG_FindItemForWeapon( temp ))->pickup_name, temp );
} else {
tempOrder[i] = temp;
weapUsed[temp - 1] = 1;
i++;
}
}
//error checking..
start = 0;
for (i = 0; i < NUM_WEAPS; i++)
{
if (weapUsed[i])
continue;
CG_Printf( "Error: %s (%i) not in list. Adding it to front of the list..\n",
(BG_FindItemForWeapon( i + 1 ))->pickup_name, i + 1 );
weaponRawOrder[start++] = i + 1;
}
//build the raw order list
for (i = start; i < NUM_WEAPS; i++)
weaponRawOrder[i] = tempOrder[i - start];
//built the remaping table
for (i = 0; i < NUM_WEAPS; i++)
weaponOrder[weaponRawOrder[i] - 1] = i + 1;
}
//</WarZone>
Ok now let's talk about what that code does for a sec. We just added NextWeapon(), PrevWeapon(),
RateWeapon(), contains(), and UpdateWeaponOrder(). UpdateWeaponOrder() will be called everytime a player
modifies their weapon order. NextWeapon() and PrevWeapon() are used to cycle through the players' weapon
precendeces. RateWeapon() returns the "rating" of a weapon which is used to determine if a new weapon is
better than the one the player is already carrying. contains() is a simple helper function which returns true if a list
contains a certain value.
5. Respond to Client Weapon Order Changes
Now find CG_UpdateCvars():
void CG_UpdateCvars( void ) {
int i;
cvarTable_t *cv;
for ( i = 0, cv = cvarTable ; i < cvarTableSize ; i++, cv++ ) {
trap_Cvar_Update( cv->vmCvar );
}
// check for modications here
// If team overlay is on, ask for updates from the server. If its off,
// let the server know so we don't receive it
if ( drawTeamOverlayModificationCount != cg_drawTeamOverlay.modificationCount ) {
drawTeamOverlayModificationCount = cg_drawTeamOverlay.modificationCount;
if ( cg_drawTeamOverlay.integer > 0 ) {
trap_Cvar_Set( "teamoverlay", "1" );
} else {
trap_Cvar_Set( "teamoverlay", "0" );
}
}
//WarZone
if ( cg_weaponsCount != cg_weaponOrder.modificationCount )
{
UpdateWeaponOrder();
cg_weaponsCount = cg_weaponOrder.modificationCount;
}
}
That code will call UpdateWeaponOrder() whenever the cvar "cg_weaponOrder" is modified.
Save and close cg_main.c, we're done with that file.
Next open cg_weapons.c and find the function CG_DrawWeaponSelect(). You can either comment out, or delete
the old function. Replace it with this function instead:
void CG_DrawWeaponSelect( void ) {
int i;
int bits;
int count;
int weap;
int x, y, w;
char *name;
float *color;
// don't display if dead
if ( cg.predictedPlayerState.stats[STAT_HEALTH] <= 0 ) {
return;
}
color = CG_FadeColor( cg.weaponSelectTime, WEAPON_SELECT_TIME );
if ( !color ) {
return;
}
trap_R_SetColor( color );
// showing weapon select clears pickup item display, but not the blend blob
cg.itemPickupTime = 0;
// count the number of weapons owned
bits = cg.snap->ps.stats[ STAT_WEAPONS ];
count = 0;
for ( i = 1 ; i < NUM_WEAPS ; i++ ) { //WarZone
if ( bits & ( 1 << i ) ) {
count++;
}
}
x = 320 - count * 20;
y = 380;
weap = weaponRawOrder[NUM_WEAPS - 1]; //WarZone -- select last weapon
for ( i = 0 ; i < NUM_WEAPS ; i++ ) { //WarZone
weap = NextWeapon( weap );
if ( !( bits & ( 1 << weap ) ) ) {
continue;
}
CG_RegisterWeapon( weap );
// draw weapon icon
CG_DrawPic( x, y, 32, 32, cg_weapons[weap].weaponIcon );
// draw selection marker
if ( weap == cg.weaponSelect ) {
CG_DrawPic( x-4, y-4, 40, 40, cgs.media.selectShader );
}
// no ammo cross on top
if ( !cg.snap->ps.ammo[ weap ] ) {
CG_DrawPic( x, y, 32, 32, cgs.media.noammoShader );
}
x += 40;
}
// draw the selected name
if ( cg_weapons[ cg.weaponSelect ].item ) {
name = cg_weapons[ cg.weaponSelect ].item->pickup_name;
if ( name ) {
w = CG_DrawStrlen( name ) * BIGCHAR_WIDTH;
x = ( SCREEN_WIDTH - w ) / 2;
CG_DrawBigStringColor(x, y - 22, name, color);
}
}
trap_R_SetColor( NULL );
}
This new function will display the weapons' icons in the player's customized weapon order -- very cool : )
6. Replace CG_NextWeapon_f() and CG_PrevWeapon_f
Now scroll down a bit and replace the function CG_NextWeapon_f() with this function:
void CG_NextWeapon_f( void ) {
int i;
int original;
if ( !cg.snap ) {
return;
}
if ( cg.snap->ps.pm_flags & PMF_FOLLOW ) {
return;
}
cg.weaponSelectTime = cg.time;
original = cg.weaponSelect;
for ( i = 0 ; i < NUM_WEAPS ; i++ ) { //WarZone
cg.weaponSelect = NextWeapon( cg.weaponSelect ); //WarZone
if ( cg.weaponSelect == WP_GAUNTLET ) {
continue; // never cycle to gauntlet
}
if ( CG_WeaponSelectable( cg.weaponSelect ) ) {
break;
}
}
}
This function taps into the NextWeapon() function we added to cg_main.c to select the next weapon from the
user's customized weapon order.
Scroll down again and replace CG_PrevWeapon_f() with this function:
void CG_PrevWeapon_f( void ) {
int i;
int original;
if ( !cg.snap ) {
return;
}
if ( cg.snap->ps.pm_flags & PMF_FOLLOW ) {
return;
}
cg.weaponSelectTime = cg.time;
original = cg.weaponSelect;
for ( i = 0 ; i < NUM_WEAPS ; i++ ) { //WarZone
cg.weaponSelect = PrevWeapon( cg.weaponSelect ); //WarZone
if ( cg.weaponSelect == WP_GAUNTLET ) {
continue; // never cycle to gauntlet
}
if ( CG_WeaponSelectable( cg.weaponSelect ) ) {
break;
}
}
}
This function taps into the PrevWeapon() function we added to cg_main.c to select the weapon before the current
weapon (not to be confused with "select the last weapon I was using" functionality) from the user's customized
weapon order.
7. Replace CG_OutOfAmmoChange()
Keep scrolling down and replace CG_OutOfAmmoChange() with this function:
void CG_OutOfAmmoChange( void ) {
int i;
int weap;
cg.weaponSelectTime = cg.time;
weap = weaponRawOrder[NUM_WEAPS - 1]; //WarZone -- pick the best weapon they have
for ( i = 0 ; i < NUM_WEAPS ; i++, weap = PrevWeapon( weap )) {
if ( CG_WeaponSelectable( weap ) ) {
if (weap != WP_GAUNTLET)
{
cg.weaponSelect = weap;
return;
}
}
}
cg.weaponSelect = WP_GAUNTLET;
}
This causes the best weapon the player has (accoring to their customized weapon order) to be selected. If no
suitable weapon is found, the Gauntlet will be selected.
Save and close cg_weapons.c, we're done with that file.
8. Change CG_ItemPickup
Next open cg_event.c and find the function CG_ItemPickup() and replace it with this one:
static void CG_ItemPickup( int itemNum, int isnewitem ) { //WarZone
cg.itemPickup = itemNum;
cg.itemPickupTime = cg.time;
cg.itemPickupBlendTime = cg.time;
// see if it should be the grabbed weapon
if ( bg_itemlist[itemNum].giType == IT_WEAPON && isnewitem ) //WarZone
{
// select it immediately
if ( cg_autoswitch.integer && bg_itemlist[itemNum].giTag != WP_MACHINEGUN ) {
if (RateWeapon( bg_itemlist[itemNum].giTag) > RateWeapon( cg.weaponSelect )) //WarZone
{
cg.weaponSelectTime = cg.time;
cg.weaponSelect = bg_itemlist[itemNum].giTag;
}
}
}
}
If you were paying attention just then, you'd have noticed that I changed the function header for CG_ItemPickup()
to include a new parameter "isnewitem". This is the part of the code where this modification switches from client
side only to include server side changes. The reasoning behind this is that the client module has no effective way
of deciding if the player has just collected an item, or it the player has had one for a while. There are methods of
bypassing the server code, but none of them worked effectively (it is possible to create an "oldweapons" variable
and compare the current set against the old set, but this method breaks when the player dies and looses all of his
weapons at once).
9. Add a New Item Pickup Event
Now find the CG_EntityEvent() function and scroll down to the EV_ITEM_PICKUP event:
case EV_ITEM_PICKUP2:
DEBUGNAME("EV_ITEM_PICKUP2");
es->number = es->otherEntityNum; //this is a bit of a hack.. but it works GRREAT!
case EV_ITEM_PICKUP:
DEBUGNAME("EV_ITEM_PICKUP");
{
gitem_t *item;
int index;
int isnewitem; //WarZone
index = es->eventParm; // player predicted
isnewitem = es->otherEntityNum2; //WarZone
if ( index < 1 || index >= bg_numItems ) {
break;
}
item = &bg_itemlist[ index ];
// powerups and team items will have a separate global sound, this one
// will be played at prediction time
if ( item->giType == IT_POWERUP || item->giType == IT_TEAM) {
trap_S_StartSound (NULL, es->number, CHAN_AUTO, trap_S_RegisterSound( "sound/items/n_health.wav" ) );
} else {
trap_S_StartSound (NULL, es->number, CHAN_AUTO, trap_S_RegisterSound( item->pickup_sound ) );
}
// show icon and name on status bar
if ( es->number == cg.snap->ps.clientNum ) {
CG_ItemPickup( index, isnewitem ); //WarZone
}
}
break;
Here again is the reference of new server code. The es->otherEntityNum2 must be set server side so that the
client code will know whether the item being picked up is a new item or not. The new "EV_ITEM_PICKUP2" event
will be explained further down.
10. Another Item Pickup Modification
Next scroll down to the EV_GLOBAL_ITEM_PICKUP event and make the following change:
case EV_GLOBAL_ITEM_PICKUP:
DEBUGNAME("EV_GLOBAL_ITEM_PICKUP");
{
gitem_t *item;
int index;
index = es->eventParm; // player predicted
if ( index < 1 || index >= bg_numItems ) {
break;
}
item = &bg_itemlist[ index ];
// powerup pickups are global
trap_S_StartSound (NULL, cg.snap->ps.clientNum, CHAN_AUTO, trap_S_RegisterSound( item->pickup_sound ) );
// show icon and name on status bar
if ( es->number == cg.snap->ps.clientNum ) {
CG_ItemPickup( index, 1 ); //WarZone
}
}
break;
That's it for cg_event.c, save and close.
11. Change Touch_Item() Behaviour
Finally open g_items.c (in the /game code) and find the Touch_Item() function. We're only going to be making a
few small changes:
void Touch_Item (gentity_t *ent, gentity_t *other, trace_t *trace) {
int respawn;
int had = 1; //WarZone
...
case IT_WEAPON:
//WarZone
if ( other->client->ps.stats[STAT_WEAPONS] & (1 << ent->item->giTag) )
had = 1;
else
had = 0;
respawn = Pickup_Weapon(ent, other);
break;
Now rip out the if ( other->client->pers.predictItemPickup) bit so that it looks like this :
if ( 0 ) { //WarZone
//do nothing...
} else {
//WarZone
gentity_t *event;
event = G_TempEntity(ent->s.origin, EV_ITEM_PICKUP2); //WarZone
event->s.eventParm = ent->s.modelindex;
event->s.otherEntityNum = other->s.number;
event->s.otherEntityNum2 = !had; //WarZone -- used to tell cgame if its a new weapon
event->r.svFlags |= SVF_BROADCAST; //broadcast it to everyone
// G_AddEvent( other, EV_ITEM_PICKUP, ent->s.modelindex ); //kill this line
}
...
}
Note: The "..." lines mean "some code in between" -- do not put the "..." lines in your code.
If you were paying attention just then you realized that we just made a drastic change to the item pickup logic. The
reasoning behind this is that (first and foremost) predicted events can only have one parameter; the second
reason to this is that the predictable event structure isn't very reliable. Have you ever been playing Q3 and run over
a weapon, but not hear the pick up sound? If so that is because the predictable events structure is getting flooded
and the EV_ITEM_PICKUP event is getting lost.
Now the EV_ITEM_PICKUP events will be sent via an external temp entity to ensure that these vital event
messages are not lost in the heat of combat.
Alright save and close g_items.c, done with that file.
12. Make the Server Show that it Supports Weapons Ordering
Now for the last and final addition! Open g_main.c (also in /game) and scroll down to the end of the cvar table:
{ &g_inactivity, "g_inactivity", "0", 0, 0, qtrue },
{ &g_debugMove, "g_debugMove", "0", 0, 0, qfalse },
{ &g_debugDamage, "g_debugDamage", "0", 0, 0, qfalse },
{ &g_debugAlloc, "g_debugAlloc", "0", 0, 0, qfalse },
{ &g_motd, "g_motd", "", 0, 0, qfalse },
{ &g_blood, "com_blood", "1", 0, 0, qfalse },
{ &g_podiumDist, "g_podiumDist", "80", 0, 0, qfalse },
{ &g_podiumDrop, "g_podiumDrop", "70", 0, 0, qfalse },
{ &g_allowVote, "g_allowVote", "1", 0, 0, qfalse }, //WarZone -- make sure there is a comma after the line above
{ NULL, "g_supportsWeaponOrder", "1", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse } //WarZone
This additional cvar allows players to see which servers support the cg_weaponOrder variable. This is important
because the new client code will not be compatible with standard Q3A servers. It would be very easy to make a
filter for GameSpy (et al) to filter out servers that don't have g_supportsWeaponOrder set to 1.
Save and close g_main.c, we're almost done...
We almost forgot to define EV_ITEM_PICKUP2! Whoops.. so, open up bg_public.h and make this quick addition:
EV_POWERUP_QUAD,
EV_POWERUP_BATTLESUIT,
EV_POWERUP_REGEN,
EV_GIB_PLAYER, // gib a previously living player
EV_DEBUG_LINE,
EV_TAUNT, //make sure there is a comma here!
EV_ITEM_PICKUP2, // WarZone -- used to create a temp entity to send the pickup message
} entity_event_t;
Save and close bg_public.h, we're ALL DONE!
13. End Notes.
You'll need to do a recompile on the cgame and game code to see the changes in action. Now I'll explain how the
new cg_weaponOrder variable works. The default value for it is "1/2/3/4/5/6/7/8/9" which tells the game the order
that you want the weapons to switch in. The default order will yield a result similar in concept to the Quake2
weapon switching code (if newWeapon > myWeapon then switchWeapons): if you're holding a shotgun (3) and
pick up a rocket launcher (5), the rocket launcher will be selected. If you're holding a rocket launcher (5) and pick
up a grenade launcher (4) the rocket launcher will remain selected.
Each weapon has a number assigned to it that corresponds to the key press required to activate that weapon: