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
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...
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...
Text in cyan
is code that needs to be added. Text in red
is code that needs to be deleted/commented out.
We'll be modifying the following files:
First open cg_local.h and find the "cg_main.c" definitions:
//Save and close cg_local.h, we're done with that file.
// 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);
Now open cg_main.c and find the cvar definitions:
vmCvar_t cg_deferPlayers;Now find the cvar table:
vmCvar_t cg_drawTeamOverlay;
vmCvar_t cg_teamOverlayUserinfo;vmCvar_t cg_weaponOrder; //WarZone
int cg_weaponsCount = -1; //WarZone
{ &cg_buildScript, "com_buildScript", "0", 0 }, // force loading of all possible data amd error on failuresNote: 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).
{ &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
Now we're going to add the bulk of my new code. The new code will live right below the cvar table:
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.{ &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>
Now find CG_UpdateCvars():
void CG_UpdateCvars( void ) {That code will call UpdateWeaponOrder() whenever the cvar "cg_weaponOrder" is modified.
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;
}
}
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 ) {This new function will display the weapons' icons in the player's customized weapon order -- very cool : )
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 );
}
Now scroll down a bit and replace the function CG_NextWeapon_f() with this function:
void CG_NextWeapon_f( void ) {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.
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 );//WarZoneif ( cg.weaponSelect == WP_GAUNTLET ) {
continue; // never cycle to gauntlet
}
if ( CG_WeaponSelectable( cg.weaponSelect ) ) {
break;
}
}
}
Scroll down again and replace CG_PrevWeapon_f() with this function:
void CG_PrevWeapon_f( void ) {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.
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 ); //WarZoneif ( cg.weaponSelect == WP_GAUNTLET ) {
continue; // never cycle to gauntlet
}
if ( CG_WeaponSelectable( cg.weaponSelect ) ) {
break;
}
}
}
Keep scrolling down and replace CG_OutOfAmmoChange() with this function:
void CG_OutOfAmmoChange( void ) {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.
int i;
int weap;cg.weaponSelectTime = cg.time;
weap = weaponRawOrder[NUM_WEAPS - 1]; //WarZone -- pick the best weapon they havefor ( 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;
}
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 ) { //WarZoneIf 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).
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;
}
}
}
}
Now find the CG_EntityEvent() function and scroll down to the EV_ITEM_PICKUP event:
case EV_ITEM_PICKUP2: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.
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; //WarZoneindex = es->eventParm; // player predicted
isnewitem = es->otherEntityNum2; //WarZoneif ( 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;
Next scroll down to the EV_GLOBAL_ITEM_PICKUP event and make the following change:
case EV_GLOBAL_ITEM_PICKUP:That's it for cg_event.c, save and close.
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;
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) {Note: The "..." lines mean "some code in between" -- do not put the "..." lines in your code.
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 everyoneG_AddEvent( other, EV_ITEM_PICKUP, ent->s.modelindex ); //kill this line
}
...
}
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 },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.
{ &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
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,Save and close bg_public.h, we're ALL DONE!
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;
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:
If you have any questions
or comments please email me at warzone@planetquake.com.
2/11/00:
- added the definition of EV_ITEM_PICKUP2
- fixed additions to g_items.c -- some color coding was incorrect
2/10/00:
- fixed a typo in UpdateWeaponOrder() (look for //typo fixed)