
GreyBear's Quake 2
Tutorials
Chapter 1: Sabotage!
Requirements
Contents
Introduction
This is the first of a series of tutorials on
programming modifications to Quake 2. The goal of these tutorials will be to
teach the reader concepts that can be applied generally, while using a specific
example as an illustration. This is somewhat different Than the method most
similar tutorials employ. Most use a specific example and show you how to make
that specific change, without giving you the underlying logic behind the mod.
This makes it hard to understand how to take the concept and apply it in a new
way. The goal of these tutorials is to get the reader to understand why the
example works, and show the reader how to take the concept and create new
modifications using the principles outlined.
I (GreyBear) am a self-taught programmer
who's been programming since 1986 (which should give you a hint how old I must
be). Games are still my first love, and I write these things because I never
had the web to help me learn. This is my way of repaying those who helped me. I
certainly don't know everything. If you think you see a better way to do
something covered in a tutorial, please e-mail me with it. Also, please feel
free to e-mail me with questions if you have them. I'll try to answer any and
all that come my way.
Since I don't have the luxury of teaching you
C, I assume in these tutorials that you know C to some extent, and that you are
familiar with programming terminology. If not, run out and grab a book on C,
and use it like a dictionary to follow along. Kernighan and Ritchie's seminal
handbook on C, The C programming language, is an excellent desk
reference. You probably should also visit the C tutorial section of this web
site before continuing.
GreyBear's Quake 2
programming philosophy
My philosophy about making code changes is
this:
Make your changes in your own code files
whenever possible. That means for our mods, we'll be adding NEW files to
the Quake 2 DLL, not merely inserting our mods into the original files. Why?
Well, two reasons. One, it's much easier to find your mods if you keep them in
your own files, named by you with descriptive names that you can remember. Two,
it maintains the integrity of the original code. As programmers we should
respect the work of others. Rather than just hack it up, we should add to it
gracefully, and clearly mark our additions as our own.
When we can't follow the above, as in
modifying global include files, or modifying global structs, we will segregate
our mods in the code, and clearly mark them as our additions. I also highly
recommend you use a consistent marker, like your initials. That way, you can search
the code for your initials and easily find every mod you make in id code.
Again, it makes for easy maintenance.
In the body of the tutorial text, code modifications
made by us will have a + sign at the end of the line in a comment field to clue
you in that the line was added to the original surrounding code.
Down to business
OK, what we're going to cover in this
tutorial is the steps necessary to create a new player command in deathmatch
that allows a player to drop a weapon, sabotaging it in the process. The next
player that picks it up (even the player who sabotaged it originally) and fires
it takes damage from the weapon. This modification will show you several
important concepts:
Step 1:
Modifying the item structure
In order for this particular modification to
work properly, we need to have a flag that allows us to keep track of whether a
particular weapon has been sabotaged. In Quake 2, every player manipulated
object (weapons, power ups, etc.) is described by an item struct. The really
cool thing about using structs and having source code available is that we can
add new flags or new descriptions to the struct to modify the attributes of an
item almost without limit. We'll play it extra safe and add our flags at the
end of the struct. This also allows us to group our modifications for easy
visiting later.
If you're not already familiar with the item
structure, take a peek at it in g_local.h. It's well documented, and
easy to understand.
We want to modify the declaration of the
gitem_t struct in the g_local.h file. The actual modification is
trivial. Find the declaration of the gitem_t struct in g_local.h by
doing a search. Immediately above the struct declaration itself, you'll see the
declaration of the gitem_t flags. This ought to help you locate it. We then
merely add a single line of code to the end of the gitem_t struct. Like so:
char *precaches; // string of all models, sounds, and images this item will use int wpn_sabo; // + BD 12/24 Flag value to determine whether this item (if a weapon) // + has been sabotaged. } gitem_t;
Save the file. That's it!
The explanation: All we did is add a new piece of information to the
struct to hold our sabotage flag. Now every item spawned will have this new
information. However in most items, it will never be used. You also might be
wondering 'Why use an int? Why not a boolean value?' That's a legitimate question,
and that approach would work fine. However, I may want to use this flag later
to convey more detailed information. Perhaps we might tie the value of the flag
to the amount of damage done to the user. Using an int gives us a little more
flexibility in the use of this flag, and doesn't hurt anything at this point.
Note the comment documenting the initials of
the person making the change, the date, and a brief explanation of what the mod
is for. It may seem silly now, but when you re-visit the code in 3 months,
you'll be glad you were a little anal about commenting your code.
Step 2: Adding a new
console command and binding it to a key
This step is a little more complicated, so
follow carefully. We're going to create two new files, and add them to our
build list. The first file is a C code file that contains the function that
sabotages the selected weapon, and drops it.
The second file is the header that declares
the function, so that it can be made visible to other parts of the DLL. The C
code we'll use is actually a modified version of a standard game DLL function
that drops a weapon. Here's the C code. We'll place it in a file we'll call b_sabdrop.c.
You can change the name is you wish, but this is the convention I use. The b_
is for my real first name (Brad). That way I know at a glance that I wrote it.
It also follows the Quake 2 naming convention, e.g. g_ for game, q_ for quake,
p_ for player, m_ for monster. The rest is merely a descriptive name that
describes the code's function. Here's the code:
// + THIS ENTIRE CODE BLOCK IS NEW! /* b_sabdrop.c - Brad Davis =========================== Provides a method to allow users to sabotage and drop weapons Last modified 12/25/97 */ #include "g_local.h" //Included so we'll have access to needed variables #include "b_sabdrop.h" //The forward declaration of our function /* ================= Cmd_SabDrop_f Modified from Cmd_InvDrop_f by BD 12/24 Drop an inventory weapon, sabotaging it in the process. Meant to be bound to a key. ================= */ void Cmd_SabDrop_f (edict_t *ent) //Acting on an entity { gitem_t *it; //The item we're looking at ValidateSelectedItem (ent); //Make sure the item we want to look at is valid if (ent->client->pers.selected_item == -1) //Whoops. No item selected! { //Print error message and bail out gi.cprintf (ent, PRINT_HIGH, "No item to drop.\n"); return; } it = &itemlist[ent->client->pers.selected_item]; //Whoops! Can't drop this! if (!it->drop) { //Print error message and bail out gi.cprintf (ent, PRINT_HIGH, "Item is not dropable.\n"); return; } //BD 12/24 Sabotage the weapon if we made it to here. it->wpn_sabo = 1; //The flag is now non-zero, meaning it's sabotaged! //simple, eh? it->drop (ent, it); //Let Q2 do it's dropping thing... } // + END OF NEW CODE BLOCK
Save the file as b_sabdrop.c. You'll
also need to add this file to your build or make file, or project file. Again,
how you do this depends on your compiler.
The explanation: We created a new function, sequestering it in our own
new file to keep it separate from the original Q2 code, as per my personal
coding philosophy. This function is a very slightly modified version of the
standard Q2 item dropping code. All we add is the setting of the wpn_sabo
flag to a non-zero value. That tells us when we look at this item later on,
that it's a sabotaged weapon, and we should hurt the user.
Extra credit: Find the original function we copied and modified...
Now, the include file. We'll call this file b_sabdrop.h.
By now it should be obvious why. This file is quite simple, containing only a
prototype of the function that lives in b_sabdrop.c. This way we can
make the function visible to other parts of the game DLL by merely adding it to
the list of file to include in the appropriate places. Here's the code:
// + THIS ENTIRE CODE BLOCK IS NEW! /* b_sabdrop.h */ void Cmd_SabDrop_f (edict_t *ent); // + END OF NEW CODE BLOCK
Again, save the file, this
time as b_sabdrop.h. That was hard, huh? Again, you may need to
add this to your make file, but in most cases with modern compilers, the
addition of the b_sabdrop.c file, with the include reference will
automatically make the file part of the build list as a dependency. Check your
compiler docs.
Now, you need to add the code to the makefile
or build list. How you do this is dependent on the compiler you're using. I'll
leave it to you, intrepid reader, to figure out how this is done in your
compiler. Usually, it's pretty simple.
Next, we need to modify the C source code
file that handles user input commands. Amazingly, the file we need to modify is
g_cmds.c (for game commands. Get it?). We need to do a couple of things.
First, we need to make sure the following line appears at the top of the g_cmds.c
file to make our new command visible to the rest of the code. Here's what the
addition looks like:
#include "g_local.h" #include "m_player.h" // + Added 12/25 BD #include "b_sabdrop.h" // +
The explanation: This make our modified drop function, Cmd_SabdDrop_f(),
visible inside the g_cmds.c file. That way, we can use it just as if it
lived inside this file.
Next, we need to add some code to the handler
that determines what gets done when a command is issued. We want to add a
section that checks to see if the user's desired action matches the string we
expect when we want our code executed, and if so, we merely call our function
to handle the request. Here's what this addition looks like:
else if (Q_stricmp (cmd, "invdrop") == 0) Cmd_InvDrop_f (ent); else if (Q_stricmp (cmd, "sabdrop") == 0) // + Cmd_SabDrop_f (ent); // + else if (Q_stricmp (cmd, "weapprev") == 0) Cmd_WeapPrev_f (ent);
Save the file.
The explanation: We added a section of code that looks for a text
string from the command console. If it sees that string, which is sabdrop,
our sabotage drop function is called, sabotaging the weapon, and dropping it.
Now, we need to identify a key that will be
bound to our new sabdrop command. Open the file config.cfg. It will be
found in your quake2\baseq2 directory. Pay no attention to the header.
We can safely modify the file to suit our new purpose. I bound the new command
sabdrop to the 'd' key, as it wasn't being used, and d stood for drop quite
nicely, thank you. You can bind it to any unused key you like, however. The
trick here, is NOT to bind the key just to 'sabdrop', but to bind the
key to 'cmd sabdrop'. So your new line in config.cfg should look like:
bind d "cmd sabdrop"
Save the file.
Now every time you hit the d key in the game,
the selected weapon will be sabotaged and dropped. After we make one other
modification, that is...
Making it work
OK, let's recap. We've extended the
functionality of the item structure. We've created a new function to tell a
weapon that it's sabotaged when dropped with the correct command. We've
modified the game code to handle the user's input requesting the weapon be
sabotaged.
So what's left? Well, how does the game know
how to cause the new user harm when the weapon is used? DOH! We need to modify
the weapon's behavior to check and see if it's sabotaged or not. The place to
do this? In the file p_weapon.c (player's weapon).
We're going to make this modification for one
weapon, the rocket launcher. However, you can make it for any or every weapon
if you like. If you decide to do this, I suggest you figure out a reasonable
amount of damage for each type of sabotaged weapon. You probably don't want a
sabotaged hand blaster to cause as much damage as a sabotaged BFG, would you?
That just wouldn't be realistic.
OK, open up p_weapon.c. Locate the
function Weapon_RocketLauncher_Fire(). Within this function we need to add a
check of our item flag. If the wpn_sabo flag is TRUE (meaning non-zero), the
weapon is sabotaged. We then merely need to call a standard quake function,
called T_Damage(), with a few new variables, to cause damage to the player
firing the weapon. Note that if the firing player hits his target, the amount
of damage inflicted is exactly the same despite the sabotage. An interesting
extension to this modification would be to have the weapon not fire at all, or
to cause less than full damage. Perhaps in a later tutorial we'll revisit
this... For now, here's the modification:
fire_rocket (ent, start, forward, damage, 550, damage_radius, radius_damage); // + BD - 12/24 Check to see if this weapon is sabotaged. If so. hurt user if(ent->client->pers.weapon->wpn_sabo) // + { // + T_Damage(ent, ent, ent->owner, ent->velocity, ent->s.origin, ent->s.origin, 25, 0, 0); // + } //+ // send muzzle flash gi.WriteByte (svc_muzzleflash);The result
Save the file.
The explanation: After the rocket launcher fires (in fire_rocket() ),
we check to see if the launcher is sabotaged. The way we do this is a little
convoluted, but it's logical once you understand it. Note the line containing ent->client->pers.weapon->wpn_sabo.
What this is saying is; Look at the entity structure of the user
(ent->client) that's firing the weapon. In that struct, there's another
struct that contains persistent information, including the current weapon
(pers.weapon). The weapon information is a struct itself, but belongs to the
persistent information data. That's why we access it with a dot operator
instead of a struct indirection operator. Finally, we access the wpn_sabo flag
by looking inside the weapon struct, so again we use the struct indirection
operator. Once again, the way to really understand the way these things work is
to become familiar with the entity, client and weapon structs by reading the Q2
source code.
The actual damage is done by using a standard
Q2 function, T_Damage(). We merely use some new values as arguments that tell
the function that the user is damaging himself. Here's the declaration of T_Damage
from the g_local.h file:
void T_Damage (edict_t *targ, edict_t *inflictor, edict_t *attacker, vec3_t dir, vec3_t point, vec3_t normal, int damage, int knockback, int dflags);
It's pretty self explanatory. In our case,
the user is the target, the inflictor and the attacker. The vectors should all
be the player's coordinates. We set the damage at 25 points in this example,
but you can change that to taste. I also elected not to have a knockback value
set. If you want a more violent appearance, you can elect to set this value to
actually knock the user back. The dflags value just tells the game engine what
visual effects to use. It only seem s to matter if you're hit with bullets.
Otherwise, the graphics are all the same.
Well, our mod is done. Now, all we need do is
test it. Compile your newly modified DLL, and either place it in a new
subdirectory under the quake2 directory and run quake 2 with the +set game
parameter pointing to your new directory, or replace the gamex86.dll in the
baseq2 directory with your new one. If you choose the latter, be sure to
backup the original gamex86.dll, or else you won't be able to play the
original game properly, nor play your saved games. In either case, you need to
add one more parameter. Use +set deathmatch 1 on the command line when testing
your mod, as this mod only works in deathmatch play. When you look at it, this
makes sense, since the monsters in single play don't pick up your weapons and
use them. They come with their own built in, and they don't run out of ammo.
I keep a shortcut on my desktop that does all
of the above. And since I don't like having all those extra folders around, I
just copy the new dll into my quake2\baseq2 folder. I keep a backup of the
original dll in the same folder, renamed as a .dlx file. Here's the shortcut
target command line I use:
C:\Quake2\quake2.exe +set deathmatch 1 +map base1
Note that I don't set the game
parameter, since I copy the dll right into the quake2 directory.
In the base1 map, there's a rocket launcher
right around the corner from where you spawn, on top of the crates. Grab it and
fire it. No problem. Then, make the hand blaster the current weapon by hitting
the 1 key. Bring up the inventory menu and highlight the rocket launcher. Press
the d key ( or the key you bound to our new command). The rocket launcher will
be dropped. Pick it up and fire it. OUCH! It works! Now you have a
sneaky surprise for the opposition in your next deathmatch game.
Contacting the Author
Questions? Problems? Write me, and I'll try
to answer your question or help you with debugging. Send your queries to me here.
Tutorial written by GreyBear