This code is Copyright (c) 2000 by Mark "WarZone" Smeltzer. You may freely use this code as long a credit for its existance is given to the author.


Author:                                         Mark "WarZone" Smeltzer
Contact Email:                            warzone@planetquake.com
Title:                                             Assimilation Tutorial #1: Customizable Weapon Switching
Difficulty:                                      Medium - Hard (easy to cut-n-paste, a bit difficult to fully understand)
Estimated Completion Time:   10 - 20 minutes

Coolness Factor:                       93.687%
Usefulness Factor:                    98.234%
Ease Of Use Factor:                 42.713%

Improvement Suggestion:        Develope an easy to use GUI program or UI extension to easily set the cg_weaponOrder variable

Click here for revision history



Downloads:

I will be posting a compiled versions tutorial (implemented in unmodified Q3A source) in both DLL and QVM formats.
I will also post a version of the Q3A source with this tutorial fully implemented.

check back in a day or two...



Tutorial Overview:

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.

So what cool weapon switching features did Quake3 bring us? None; sadly. In Quake3 you have two options: auto switch, and manual switch. Autoswitch means that any time you pick up a weapon you will automatically use it. Even if you've got the BFG in hands and you pick up a shot gun you'll be forced to draw the shot gun. Talk about lame... And manual switch is even worse! In manual mode it never forces you to draw a gun. So you have to manually switch to the guns as you pick them up.

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...



Color Codes:

Text in cyan is code that needs to be added. Text in red is code that needs to be deleted/commented out.



Files Changed:

We'll be modifying the following files:

So this is both a server side and client side modification. It can be done client side only, but by doing so you'll loose some important functionality (which will be explained later).


The Tutorial:

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.

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).

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 != '/') //typo fixed 2/10/00
    {
      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.

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 : )

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.

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.

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).

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 ); //kill this line
     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.

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 ); //kill this line
     CG_ItemPickup( index, 1 ); //WarZone
   }
  }
  break;

That's it for cg_event.c, save and close.

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;
...
 if ( other->client->pers.predictItemPickup) { //kill these lines
   G_AddPredictableEvent( other, EV_ITEM_PICKUP, ent->s.modelindex );
 if ( 0 ) { //WarZone
   //do nothing...
 } else {
    //WarZone
    gentity_t *event;

    event = G_TempEntity(ent->s.origin, EV_ITEM_PICKUP2);
    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.

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 this line
 { 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!



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:
1 = Gauntlet
2 = Machine Gun
3 = Shot Gun
4 = Grenade Launcher
5 = Rocket Launcher
6 = Lightning Gun
7 = Rail Gun
8 = Plasma Gun
9 = BFG 10K

My personal preference for the weapon order is "1/2/3/4/6/8/5/7/9" which is very similar to Quake2's weapon order.

If your mod is going to need more than 9 weapons, you'll need to change the NUM_WEAPS #define we added to cg_local.h to the appropriate value. The cg_weaponOrder string needs to be formated as follows:

Meaning the string can contain up to two consectutive integer digits followed by a seperator which must be either a forward slash or a back slash (/ or \). Duplicate or invalid entries are ignored and missing entries are inserted at the begining of the list. So the string "2/2/3/3/4/5/6/7/8/8/8/9" will yield the same end result as "1/2/3/4/5/6/7/8/9" because the missing "1" is inserted at the front of the list and the duplicate values are ignored.

If you have any questions or comments please email me at warzone@planetquake.com.



Revision history:
2/11/00:
2/10/00:


This code is Copyright (c) 2000 by Mark "WarZone" Smeltzer. You may freely use this code as long a credit for its existance is given to the author.