What the hell is
"Alternate Fire"?
Alternate Fire means have a secondary fire button. This feature is
common in other FPS games like Unreal Tournament and Half-Life. This tutorial
will will take you through the steps involved in adding alternate fire to your
Quake3 modification.
Note: I am not the worlds greatest programmer by a long shot, so don't go crazy if you see a bit of bad programming in this tutorial. I would appreciate any suggested improvements or corrections you can offer. The code has been tested extensively and works fine.
In doing this tutorial I assume you know how to compile the source, and have played with the code before.
Okay, on with the tutorial. This is a client and server side modification.
Files to be
modified
bg_pmove.c
g_active.c
bg_public.h
g_local.h
g_weapon.c
cg_event.c
cg_local.h
cg_weapons.c
bg_pmove.c
Go down and find the PM_Weapon() function.
Locate the following lines:
if ( ! (pm->cmd.buttons &
1) ) {
pm->ps->weaponTime = 0;
pm->ps->weaponstate
= WEAPON_READY;
return;
}
This code determines whether the attack
button is being pressed, if it isn't, the weapon is made ready and
return.
Change the if statement to:
if ( !
((pm->cmd.buttons & 1) ||( pm->cmd.buttons & 32) ))
{
pm->ps->weaponTime = 0;
pm->ps->weaponstate
= WEAPON_READY;
return;
}
This will check both the attack button
(+attack) and our new alt attack (or alt fire button) +button5.
Go down a few lines and find the following
code:
// fire weapon
PM_AddEvent( EV_FIRE_WEAPON
);
switch(
pm->ps->weapon ) {
default:
case
WP_GAUNTLET:
addTime = 400;
break;
case WP_LIGHTNING:
addTime =
50;
break;
case WP_SHOTGUN:
addTime =
1000;
break;
case WP_MACHINEGUN:
addTime =
100;
break;
case WP_GRENADE_LAUNCHER:
addTime =
800;
break;
case WP_ROCKET_LAUNCHER:
addTime =
800;
break;
case WP_PLASMAGUN:
addTime =
100;
break;
case WP_RAILGUN:
addTime =
1500;
break;
case WP_BFG:
// addTime =
100;
addTime = 200;
break;
case
WP_GRAPPLING_HOOK:
addTime = 400;
break;
}
We are going to write an if to determine
which attack button is being pressed. Based on the key that is being pressed, we
are going to call a different event. Change the above code to:
// Normal Attack
Button
if (pm->cmd.buttons & 1) {
// Normal Fire
Event
PM_AddEvent( EV_FIRE_WEAPON );
switch(
pm->ps->weapon ) {
default:
case
WP_GAUNTLET:
addTime = 400;
break;
case WP_LIGHTNING:
addTime =
50;
break;
case WP_SHOTGUN:
addTime =
1000;
break;
case WP_MACHINEGUN:
addTime =
100;
break;
case
WP_GRENADE_LAUNCHER:
addTime = 800;
break;
case WP_ROCKET_LAUNCHER:
addTime =
800;
break;
case WP_PLASMAGUN:
addTime =
100;
break;
case WP_RAILGUN:
addTime =
1500;
break;
case WP_BFG:
// addTime =
100;
addTime = 200;
break;
case WP_GRAPPLING_HOOK:
addTime =
400;
break;
}
// New Alt Fire
Button
} else if (pm->cmd.buttons & 32) {
// New
Event
PM_AddEvent( EV_FIRE_WEAPON2 );
switch(
pm->ps->weapon ) {
default:
case
WP_GAUNTLET:
addTime = 400;
break;
case WP_LIGHTNING:
addTime =
50;
break;
case WP_SHOTGUN:
addTime =
1000;
break;
case WP_MACHINEGUN:
addTime =
100;
break;
case
WP_GRENADE_LAUNCHER:
addTime = 800;
break;
case WP_ROCKET_LAUNCHER:
addTime =
800;
break;
case WP_PLASMAGUN:
addTime =
100;
break;
case WP_RAILGUN:
addTime =
1500;
break;
case WP_BFG:
// addTime =
100;
addTime = 200;
break;
case WP_GRAPPLING_HOOK:
addTime =
400;
break;
}
}
This is some pretty simple stuff. You can see that the above code is one big if statement checking which attack button is being pressed.
If the normal attack button is being
pressed
if (pm->cmd.buttons & 1) {
And half way down. Else if the second fire
button is being pressed
} else if (pm->cmd.buttons & 32)
{
You will notice that it is one or the other.
This stops both buttons from being pressed at once.
You will also notice that the same switch statement
is used for both attack buttons. The reason is that the switch statement
controls the addTime of each weapon. "AddTime" is the amount of time in
ms before the weapon can be fired again. We want the switch statement
twice because when we start adding our cool new alt fire functions we may want
different rates of fire between first and secondary fire.
The important part to note with the above
code is these two lines:
PM_AddEvent( EV_FIRE_WEAPON );
and
PM_AddEvent( EV_FIRE_WEAPON2
);
These lines are calling an event both in game (server side) and in cgame (client side). You will notice that that each fire mode is calling a different event. This is great for us because it means we can tell quake3 to do different things with the current weapon based on which attack button is being pressed.
One more quick modification to this
file.
Go down a little more
to PMoveSingle and find the following code:
// set the firing flag for
continuous beam weapons
if ( !(pm->ps->pm_flags & PMF_RESPAWNED)
&& pm->ps->pm_type != PM_INTERMISSION
&& (
pm->cmd.buttons & BUTTON_ATTACK ) && pm->ps->ammo[
pm->ps->weapon ] ) {
pm->ps->eFlags |=
EF_FIRING;
} else {
pm->ps->eFlags &=
~EF_FIRING;
}
This is just what the comment says: // set the firing flag for continuous beam weapons
We are going to make sure that our new
attack button will also set the firing flag. Change the code to:
if (
!(pm->ps->pm_flags & PMF_RESPAWNED) && pm->ps->pm_type
!= PM_INTERMISSION
&& ( (pm->cmd.buttons &
BUTTON_ATTACK) || (pm->cmd.buttons & 32) )
&&
pm->ps->ammo[ pm->ps->weapon ] ) {
pm->ps->eFlags |=
EF_FIRING;
} else {
pm->ps->eFlags &=
~EF_FIRING;
}
g_active.c
Go down to the ClientEvents() function. Find the following lines of
code:
case EV_FIRE_WEAPON:
FireWeapon( ent );
break;
The above code tells the server to call
FireWeapon() which is in g_weapon.c.
Remember that each of the attack keys calls a different event. Now we
are going to tell the server what to do when our new alt fire is pressed.
Directly below the above code place the following:
case
EV_FIRE_WEAPON2:
FireWeapon2( ent
);
break;
This tells the server to call FireWeapon2() which does not exist
yet.
Go down to ClientThink_real() function. Find
the following line:
( ucmd->buttons & BUTTON_ATTACK ) &&
client->ps.weaponTime <= 0 ) {
and change it to:
( (ucmd->buttons &
BUTTON_ATTACK) || (ucmd->buttons & 32) ) &&
client->ps.weaponTime <= 0 ) {
This makes the gauntlet work when we press the new alt fire button.
bg_public.h
We need to add our new event to the entity_event_t struct. Find the
following line:
EV_FIRE_WEAPON,
Now directly below it place the
following:
EV_FIRE_WEAPON2,
g_local.h
Go down and find:
void FireWeapon( gentity_t *ent
);
Directly below the above line place this
code:
void FireWeapon2( gentity_t *ent );
g_weapon.c
So far we have told quake3 to perform an event when we press alt fire.
The event will trigger code in cgame (we haven't done this yet) and it will call
FireWeapon2() in game. We are now going to make our FireWeapon2()
function.
Go down to the bottom of the file and find
the FireWeapon() function. Now directly below that function, place the
following:
/*
===============
FireWeapon2
===============
*/
void FireWeapon2( gentity_t *ent )
{
if (ent->client->ps.powerups[PW_QUAD] )
{
s_quadFactor = g_quadfactor.value;
} else
{
s_quadFactor = 1;
}
// track shots
taken for accuracy tracking. Grapple is not a weapon and gauntet is just
not tracked
if( ent->s.weapon != WP_GRAPPLING_HOOK
&& ent->s.weapon != WP_GAUNTLET ) {
ent->client->ps.persistant[PERS_ACCURACY_SHOTS]++;
}
// set aiming
directions
AngleVectors (ent->client->ps.viewangles, forward, right,
up);
CalcMuzzlePoint ( ent, forward, right, up, muzzle );
// fire the
specific weapon
switch( ent->s.weapon )
{
case WP_GAUNTLET:
Weapon_Gauntlet( ent
);
break;
case WP_LIGHTNING:
Weapon_LightningFire( ent );
break;
case
WP_SHOTGUN:
weapon_supershotgun_fire( ent
);
break;
case WP_MACHINEGUN:
if (
g_gametype.integer != GT_TEAM ) {
Bullet_Fire( ent,
MACHINEGUN_SPREAD, MACHINEGUN_DAMAGE );
} else
{
Bullet_Fire( ent, MACHINEGUN_SPREAD, MACHINEGUN_TEAM_DAMAGE
);
}
break;
case
WP_GRENADE_LAUNCHER:
weapon_grenadelauncher_fire( ent
);
break;
case WP_ROCKET_LAUNCHER:
Weapon_RocketLauncher_Fire( ent );
break;
case WP_PLASMAGUN:
Weapon_Plasmagun_Fire( ent
);
break;
case WP_RAILGUN:
weapon_railgun_fire( ent );
break;
case
WP_BFG:
BFG_Fire( ent );
break;
case WP_GRAPPLING_HOOK:
Weapon_GrapplingHook_Fire(
ent );
break;
default:
// FIXME G_Error( "Bad
ent->s.weapon" );
break;
}
}
Make sure you leave one blank line at the
bottom of the file (if not your compiler might get upset!)
The above code is a copy-paste of the
FireWeapon() function, but with a small name change.
Why the hell bother? Why not tell the event
to call FireWeapon() and give us less work to do?
The reason is because the switch statement in this
code determines which weapon the player is firing, and calls the corresponding
weapon_fire_function. We want two different copies because we can now write our
own weapon_fire_functions in g_weapon and call them either when we press primary
or secondary fire (giving us complete control over what each fire mode of each
weapon does!!!).
That's it for server side code, lets move on to the client side.
cg_event.c
Go down and find the CG_EntityEvent() function. Find the following
code:
case EV_FIRE_WEAPON:
DEBUGNAME("EV_FIRE_WEAPON");
CG_FireWeapon( cent );
break;
Directly below the above code put the
following:
case EV_FIRE_WEAPON2:
DEBUGNAME("EV_FIRE_WEAPON2");
CG_FireWeapon2( cent );
break;
This is the code that is run on the client side when the EV_FIRE_WEAPON and EV_FIRE_WEAPON2 events are called.
cg_local.h
Go down and find the this line:
void CG_FireWeapon( centity_t
*cent );
Now place this line directly below
it:
void
CG_FireWeapon2( centity_t *cent );
cg_weapons.c
In cg_event.c we told cgame to call the CG_FireWeapon2() function when
the alt fire button is pressed.
Now we are going to create this function. Find this code:
/*
================
CG_FireWeapon
Caused by an
EV_FIRE_WEAPON event
================
*/
void CG_FireWeapon( centity_t
*cent ) {
entityState_t *ent;
int
c;
weaponInfo_t *weap;
ent =
¢->currentState;
if ( ent->weapon == WP_NONE )
{
return;
}
if ( ent->weapon >=
WP_NUM_WEAPONS ) {
CG_Error( "CG_FireWeapon: ent->weapon
>= WP_NUM_WEAPONS" );
return;
}
weap =
&cg_weapons[ ent->weapon ];
// mark the entity
as muzzle flashing, so when it is added it will
// append the flash to the
weapon model
cent->muzzleFlashTime =
cg.time;
// lightning gun
only does this this on initial press
if ( ent->weapon ==
WP_LIGHTNING ) {
if ( cent->pe.lightningFiring )
{
return;
}
}
// play quad sound
if needed
if ( cent->currentState.powerups & ( 1 << PW_QUAD ) )
{
trap_S_StartSound (NULL, cent->currentState.number, CHAN_ITEM,
cgs.media.quadSound );
}
// play a
sound
for ( c = 0 ; c < 4 ; c++ ) {
if (
!weap->flashSound[c] ) {
break;
}
}
if ( c > 0 ) {
c = rand() %
c;
if ( weap->flashSound[c] )
{
trap_S_StartSound( NULL, ent->number, CHAN_WEAPON, weap->flashSound[c]
);
}
}
// do brass
ejection
if ( weap->ejectBrassFunc && cg_brassTime.integer >
0 ) {
weap->ejectBrassFunc( cent );
}
}
Now directly below it place:
/*
================
CG_FireWeapon2
Caused by an
EV_FIRE_WEAPON2 event
================
*/
void CG_FireWeapon2( centity_t
*cent ) {
entityState_t *ent;
int
c;
weaponInfo_t *weap;
ent =
¢->currentState;
if ( ent->weapon == WP_NONE )
{
return;
}
if ( ent->weapon >=
WP_NUM_WEAPONS ) {
CG_Error( "CG_FireWeapon: ent->weapon
>= WP_NUM_WEAPONS" );
return;
}
weap =
&cg_weapons[ ent->weapon ];
// mark the entity
as muzzle flashing, so when it is added it will
// append the flash to the
weapon model
cent->muzzleFlashTime =
cg.time;
// lightning gun
only does this this on initial press
if ( ent->weapon ==
WP_LIGHTNING ) {
if ( cent->pe.lightningFiring )
{
return;
}
}
// play quad sound
if needed
if ( cent->currentState.powerups & ( 1 << PW_QUAD ) )
{
trap_S_StartSound (NULL, cent->currentState.number, CHAN_ITEM,
cgs.media.quadSound );
}
// play a
sound
for ( c = 0 ; c < 4 ; c++ ) {
if (
!weap->flashSound[c] ) {
break;
}
}
if ( c > 0 ) {
c = rand() %
c;
if ( weap->flashSound[c] )
{
trap_S_StartSound( NULL, ent->number, CHAN_WEAPON, weap->flashSound[c]
);
}
}
// do brass
ejection
if ( weap->ejectBrassFunc && cg_brassTime.integer >
0 ) {
weap->ejectBrassFunc( cent );
}
}
This code doesn't do much. It plays the fire sound for the weapons and a few other minor things. The point in creating a second version was to completely separate both primary and secondary fire modes.
When you start adding you cool new
weapon_fire_functions, you will be spending time in this file (cg_weapon.c),
getting your new sounds and effects working with the different fire modes.
Having separate CG_FireWeapon functions makes life a lot easier.
That's
It!
That is all
there is to it.
Compile both
game and cgame. To test out the modification, load up your newly created mod and
start a map. To use your new alt fire, bring down the console and
type:
\bind mouse2
+button5
This will bind mouse2 (mouse button 2) to
+button5 (our new alternate fire). Press it, and it should fire each of the
weapons normally.
You are now
ready to add your own alternate fire functions.
Extra-Curricular
Activity
You have
got a working alternate fire, but you want to test it out.
We are going to change the alternate fire
of all the weapons (except gauntlet and machine-gun) so that they all fire
rockets.
g_weapon.c
Go down to our newly created FireWeapon2() function
and replace it with the following:
/*
===============
FireWeapon2
===============
*/
void FireWeapon2( gentity_t *ent )
{
if (ent->client->ps.powerups[PW_QUAD] )
{
s_quadFactor = g_quadfactor.value;
} else
{
s_quadFactor = 1;
}
// track shots
taken for accuracy tracking. Grapple is not a weapon and gauntet is just
not tracked
if( ent->s.weapon != WP_GRAPPLING_HOOK
&& ent->s.weapon != WP_GAUNTLET ) {
ent->client->ps.persistant[PERS_ACCURACY_SHOTS]++;
}
// set aiming
directions
AngleVectors (ent->client->ps.viewangles, forward, right,
up);
CalcMuzzlePoint ( ent, forward, right, up, muzzle );
// fire the
specific weapon
switch( ent->s.weapon )
{
case WP_GAUNTLET:
Weapon_Gauntlet( ent
);
break;
case WP_LIGHTNING:
Weapon_RocketLauncher_Fire( ent );
break;
case WP_SHOTGUN:
Weapon_RocketLauncher_Fire( ent
);
break;
case WP_MACHINEGUN:
if (
g_gametype.integer != GT_TEAM ) {
Bullet_Fire( ent,
MACHINEGUN_SPREAD, MACHINEGUN_DAMAGE );
} else
{
Bullet_Fire( ent, MACHINEGUN_SPREAD, MACHINEGUN_TEAM_DAMAGE
);
}
break;
case
WP_GRENADE_LAUNCHER:
Weapon_RocketLauncher_Fire( ent
);
break;
case WP_ROCKET_LAUNCHER:
Weapon_RocketLauncher_Fire( ent );
break;
case WP_PLASMAGUN:
Weapon_RocketLauncher_Fire( ent );
break;
case WP_RAILGUN:
Weapon_RocketLauncher_Fire( ent
);
break;
case WP_BFG:
Weapon_RocketLauncher_Fire( ent );
break;
case WP_GRAPPLING_HOOK:
Weapon_GrapplingHook_Fire(
ent );
break;
default:
// FIXME G_Error( "Bad
ent->s.weapon" );
break;
}
}
All we have done is replace the normal weapon fire functions with the rocket launcher's fire function.
g_items.c
What we are about to do is change quake3 so that the
Rocket Launcher is loaded by all clients when they enter any
level.
This is really lazy, we are doing it just to make sure that the rocket
model and smoke trail will work on levels that do not contain rocket launchers.
It is a quick fix and allows us to fully demonstrate the alternate
fire.
Find this function:
ClearRegisteredItems(). Directly below this line:
RegisterItem(
BG_FindItemForWeapon( WP_GAUNTLET ) );
Place this
line:
RegisterItem( BG_FindItemForWeapon( WP_ROCKET_LAUNCHER )
);
THE
END
Compile the code and load it up, and the alternate fire for all the
weapons will fire rockets.