GreyBear's Quake 2 Tutorials
Chapter 1: Sabotage!
Requirements
Contents
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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
Introduction
Step 1: Modifying the item structure
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!
// + 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
// + 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.
#include "g_local.h"
#include "m_player.h"
// + Added 12/25 BD
#include "b_sabdrop.h" // +
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.
bind d "cmd sabdrop"
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
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);
C:\Quake2\quake2.exe +set deathmatch 1 +map base1