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...
g_main.c
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] );
//
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;
}
//
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_PICKUP");
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 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: