Chapter 2: Medic! - Part I
Requirements
Welcome back! In this, the second tutorial of the series, we're going to tackle a larger modification that covers several aspects of Quake 2 DLL programming. We're going to implement a player class in Quake 2. Our example class will be a Medic. Our medic will have the ability to heal other players by touching them. We'll give our Medic a higher Maximum Health point level. When he touches another player whose health is below his maximum, our Medic will heal that player back to full health, or until the Medic's health points drop to 100, whichever occurs first. We'll call the Player's class his M.O.S.. For those of you who have a military background, you know that this stands for Military Occupational Specialty. Your MOS is your job in military service. Since our Quake player is definitely a military type, the description fits rather nicely.
Since this tutorial covers a lot of ground, I've broken it into two parts. This first part will cover allowing the user to choose his MOS, and creating the variable to hold and remember it. We'll also cover how to write to the console, and show you a twist on getting commands from the user. As a bonus, we'll learn how to play sounds in Quake.
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.
Modifying the client_t struct for player class MOS
In the first tutorial we modified the item_t struct to create a new flag to determine whether the weapon we were using was sabotaged. We're going to do something almost exactly the same with the client->resp struct. The client->resp struct is a part of the edict_t struct. Specifically, it's the part of the edict_t struct that gets saved across deaths in deathmatch. The edict_t struct is what describes players and monsters in Quake 2. You can tell at a glance whether an edict entity is a real player or not. The way you tell is by determining whether the client struct is filled with valid info. If it is, the entity is a player. If it's null, the entity is a monster. So, placing our flag in the client struct insures that it'll only apply to real players manipulated by humans.
We're going to use another int entry, just like last time. However this time, we really will use the power that defining our flag as an int will give us. One of the objects of defining player classes is to have more than one, right? So, with and int, we have the capability of defining thousands of classes if we need to.
To make the modification to the client_t struct, open up g_local.h and fine the definition for the client_persistant_t struct. Add the code that's marked as our modification:
The explanation: Basically the same as tutorial 1's flag.
Displaying instructions for player
class choices
We're not going to get very far if we can't tell the player how to choose
his MOS when he enters the game are we? So, we need to learn how to write
messages to the console. We'll also learn that this poses some interesting
problems that we'll need to solve in order to make our instructions readable.
You have to give Id credit. They did a masterful job of writing a game
library that is logical, and easy to use. There are a series of pre-built
functions for the DLL programmer that control the game interface. Surprisingly,
they are all prefixed by gi (game interface). All of the game interface
commands that deal with printing text on the console or game screen are
based upon the ubiquitous printf function that all C programmers come to
know (and some to love...). This makes for quick familiarity and increased
ease of use.
So, what do we need to do that requires writing to the console? Well,
here's a chronological list:
OK, now that we know what function we want to use, let's look at the
prototype. We'll need to know how to call the function properly. Here it
is: 
OK, lets' try it. First, we want to have our banner display every time
a player enters deathmatch, but only if he hasn't already chosen an MOS.
That means if the player gets killed in a match, or a level change occurs,
for a player with a chosen MOS, the message should NOT display. The place
to add our message, then, looks to be ClientBeginDeathMatch(), located
in the p_client.c file. To begin with, let's place a simple message,
to test the viability of our thinking. Add the following code:
Next, we need to add the exact same new code line into the Client_Think()
function. This function is located in the same p_client.c file,
a little below the ClientBeginDeathMatch() code. Add the line so it sits
this way: 
Next, we need to write the code to actually display the message we want
to show the player, and set the mos flag when he decides. Create
a new file, which we'll call b_playermos.c. In it, place the following
code:
Next, as before, we need to create a new header to allow other code
files to know about our new function. We'll call this file (surprise, surprise...)
b_playermos.h. In it, place the following code:
Now, we need to return to the p_client.c file and insert the declaration of our function by including the b_playermos.h file. Like so:
The explanation: Whew! That's a lot! OK, after learning a bit
about some of the more common game interface functions, we placed function
calls at the strategic places in the quake2 code to allow us to display
a menu to the player when he enters deathmatch. By placing it in the ClientBeginDeathMatch()
function, we ensure that it gets displayed as soon as the player enters
the game. By placing the same function call in Client_Think(), we make
sure it stays there until the player makes a choice. Finally, by
using the same function in both places, and taking advantage of the built
in formatting of gi.centerprintf, we get a nicely formatted message
that doesn't flicker on the screen.
Creating new commands for choosing
player MOS
Now we'll return for a moment to familiar territory. We're going to
create new console commands to set the player's MOS, just like we created
a command to sabotage a weapon. As before, the modification gets made within
g_cmds.c. Look for the Client_Command() function, and add the code:
The explanation: Again, this is the same thing we did in tutorial
1. We're inserting code to look for a cmd string that contains the arguments
we added above, specifically "grunt" and "medic". Obviously, as you add
player classes, you merely add new if else tests to find the commands you
want to represent additional player classes.
We need to do one additional thing to make our lives a bit easier. We
should bind the keys we want to use for our new commands. I chose to use
the M key for Medic, and the G key for grunt. There's one small problem
with this approach. The G key is already bound to use grenades.
For me this isn't a problem, because I always choose grenades from the
Inventory screen when I want to use them. But it might be a pain for you.
In fact, this is the biggest limitation in Quake 2 I've found yet. The
lack of enough viable free keys to bind to custom commands. You can't use
shifted keys, so there's a slight shortage of keys that are usable. At
any rate, here's how I bound the keys. Remember to use correct syntax for
your new command: \
Adding new code to set the player's
MOS
We're almost to the end of the trail for now. All we need is to actually
create the code for the function Cmd_MOS_f(). As you probably guessed,
this new code goes in our custom code file, b_playermos.c. Open
it up, and add the following new code:
The explanation: This function sets the mos flag, checking
it in the process. Note that it takes two arguments, the player structure,
and the command string that was given at the console by that player. The
code checks our mos flag. If the flag is greater than zero, we know
the user already has an MOS. In that case, we tell him so, tell him what
MOS he's chosen, and exit, playing a sound. If the flag is zero,
it examines the command string to determine what choice the player made,
and sets the mos flag appropriately, and displaying a message to
confirm the user's choice. Upon exit, a sound is played to give the user
audible feedback as well.
One other thing to note, since it'll be something you're bound to run into again in your Q2 programming. Note that the strings were processed for matching using the Q2 custom function Q_stricmp(). Strings passed from the console in Quake 2 are NOT regular C type strings. Therefore, if you try to use regular matching strategies, your code will fail, and it will do so without an error. So remember, and be aware!
Playing sounds for effect in
Quake 2
The sound is played using yet another game interface function, gi.sound.
This is another simple yet ingenious function provided for our use by Id.
Let's take a look at the pseudoprototype for this function:
The result
You should now be able to compile and install your modified DLL. When
entering a deathmatch game, you should see the banner asking you to choose
your MOS. It'll stay visible until you choose a valid MOS. After you've
chosen, it'll tell you your MOS, and cue you by playing a sound. If you
try to change later, you'll get a polite error message.
To be continued...
Well, now you can see why this tutorial is split in two. We're only
half way home. We've enabled the display, and created a way to capture
the user's input and store the result in our class flag. However we still
need to implement the new abilities of our player based on his MOS. That
will be the subject of part 2 of the tutorial.
Extra Credit: To keep your mind occupied until the conclusion, and to help you start expanding on these tutorials on your own, try modifying the code in Cmd_MOS_f() to allow players to change MOS in the course of the game, giving them appropriate feedback notifying them that they've changed MOS.
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
vec3_t cmd_angles; // angles sent over in the last command
//+ BD - 1/3 - Our players MOS (Military Occupational Specialty)
//+ BD - 1/3 _ Definitions:
//+ BD - 1/3 - 1. Grunt - A regular player
//+ BD - 1/3 - 2. Medic - Can heal others
int mos; //+ BD - 1/15 The flag that determines our player's MOS
} client_respawn_t;
Save the file. As you can see, the new definition is
inserted at the end of the client_respawn_t struct. This struct ends
up being accessed as ent->client->resp. Ent is an edict_t, client is a
client_t and resp is a client_respawn_t struct.
Sounds easy enough. Before we take a stab at it though, let's briefly examine
the tools at our disposal. Note: For a complete description of all
of the gi functions in Quake 2, hop on over to
We can throw out gi.dprintf and gi.bprintf right away. We don't need debugging
info, and we only want to send output to a single player. That leaves gi.cprintf
and gi.centerprintf. I chose gi.centerprintf because it displays preformatted,
and looks nicer for our purposes.
gi.printf(edict_t *ent, char *fmt...);
Pretty easy, eh? Basically we need to know who we want
to send the message to (edict_t *ent), and the message, which can be formatted
in any way allowed by the normal printf command.
gi.multicast (ent->s.origin, MULTICAST_PVS);
gi.bprintf (PRINT_HIGH, "%s entered the game\n", ent->client->pers.netname);
//+ BD - 1/5 - Display a message to the player when entering deathmatch
gi.centerprintf(ent,"Please choose and MOS\n\nG - Grunt\nM - Medic\n"); //+BD - 1/5 - Display a menu for the player to choose from
// make sure all view stuff is valid
ClientEndServerFrame (ent);
Save the file and compile the DLL. Now, after copying
the DLL into your quake2\baseq2 directory, run the game in deathmatch mode.
See the banner? Yep, nicely placed right on the screen as you pop into
the spawn point. But, wait... It disappears! That's the problem I alluded
to earlier. The game expires these screen messages after a few seconds,
and erases them. Now, this really makes perfect sense, since you wouldn't
want this stuff scrolling down screen in front of your world view. But
how are we going to make sure that the message stays up until the
player chooses an MOS? Hmm.. We place it in the client's think function.
This think function plans the next move for the player, and sets
up certain environmental events for the next frame or two. That's the natural
place to keep the message in front of our player until he decides. OK,
now that we know what to do, let's go back and change the code in ClientBeginDeathMatch()
so it's really useful, and then we'll more or less duplicate that functionality
in ClientThink(), which is also located in p_client.c. First, change
the code you just added above to:
gi.multicast (ent->s.origin, MULTICAST_PVS);
gi.bprintf (PRINT_HIGH, "%s entered the game\n", ent->client->pers.netname);
CheckMOS(ent); //+ - BD - 1/5 - Check to see if the user already has an MOS. If not message him
// make sure all view stuff is valid
ClientEndServerFrame (ent);
To remain true to my philosophy of keeping as much custom
code separate as possible, we've moved the code to a separate file, which
we'll delve into soon. For now, just be assured that the CheckMOS() function
will do the work of checking the mos flag and displaying the message
if needed.
level.exitintermission = true;
return;
}
CheckMOS(ent); //+ - BD - 1/5 - Check to see if the user already has an MOS. If not message him
pm_passent = ent;
// set up for pmove
memset (&pm, 0, sizeof(pm));
Save the file.
//+ - BD - 1/7 THIS ENTIRE CODE BLOCK IS NEW!
//BD - 1/3 - b_playermos.c - Brad Davis(GreyBear)
//This code file consists of functions that relate to classes or 'MOS' (Military Occupational Specialty)
//of players in deathmatch mode
//Include this for variable declarations we need
#include "g_local.h"
// CheckMOS - 1/5 - BD This checks to see if an MOS has been chosen. If not, a message is printed.
// This function is called in the Client_Think function to keep displaying it every frame.
void CheckMOS(edict_t *ent)
{
if(ent->client->resp.mos < 1) //Do we already have an MOS?
{ //Noper...Message time
gi.centerprintf(ent, "Please choose your MOS\n\nG - Grunt\nM - Medic\n");
}
}
//+ - BD - END NEW CODE BLOCK
Save the file as b_playermos.c. Add it to your
makefile or build list as usual.
//+ - BD - 1/7 THIS ENTIRE CODE BLOCK IS NEW!
//BD - 1/3 - b_playermos.h - Brad Davis(GreyBear)
//This file forward declares our function prototypes for inclusion into Q2 source
//Functions we use
void CheckMOS(edict_t *ent);
//+ - BD - END NEW CODE BLOCK
Save the file as b_playermos.h. As before, since
the header file is referenced in the b_playermos.c file, it should
be included in the build automatically by the compiler. However, check
your compiler docs. YMMV.
#include "m_player.h"
#include "b_playermos.h" //+ - BD - 1/7 Include function declaration for our functions so they're visible
else if (Q_stricmp (cmd, "wave") == 0)
Cmd_Wave_f (ent);
//+ BD - 1/5 - Added to handle our player class
else if (Q_stricmp (cmd, "grunt") == 0) //+ BD - 1/5 - Asking to be a regular player
Cmd_MOS_f (ent, cmd); //+ BD - 1/5 - OK, call our new function with the right arguments
else if (Q_stricmp (cmd, "medic") == 0) //+ BD - 1/5 - Asking to be a medic
Cmd_MOS_f(ent, cmd); //+ BD - 1/5 - Ditto!
else if (Q_stricmp (cmd, "gameversion") == 0)
Save the file. Note the new function Cmd_MOS_f(). We'll
write that soon, so don't worry about it for the moment.
bind g "cmd grunt"
bind m "cmd medic"
Once again, pay attention to the actual string. It must
be "cmd grunt", not just "grunt".
//+ - BD - 1/5 - THE ENTIRE CODE BLOCK IS NEW!
// Cmd_MOS - 1/5 - BD This function sets the ent->client->resp.mos value
void Cmd_MOS_f(edict_t *ent, char *cmd)
{
//If the player already has an MOS, tell him what it is and return
if(ent->client->resp.mos > 0)
{
//Let the player know he can't change MOS, and tell him what he is...
if(ent->client->resp.mos == 1)
{
gi.centerprintf(ent,"Sorry Soldier.\nYou can't change from MOS 11B\n");
}
else if(ent->client->resp.mos == 2)
{
gi.centerprintf(ent,"Sorry Soldier.\nYou can't change from MOS 91B\n");
}
//Play a sound for the player.
gi.sound(ent, CHAN_VOICE, gi.soundindex("items/damage2.wav"), 1, ATTN_NORM, 0);
//Bail out now. We don't want to execute what's below
return;
}
//Otherwise, assign an MOS now...
gi.cprintf(ent,PRINT_HIGH,"Got: %s\n",cmd);
//We MUST use Quake's string functions. The string isn't a normal one!
if(Q_stricmp (cmd, "grunt") == 0)
{
ent->client->resp.mos = 1;
gi.centerprintf(ent,"You have chosen MOS 11B - Infantryman.\n\nGood Luck!\n");
}
else if(Q_stricmp (cmd, "medic") == 0)
{
ent->client->resp.mos = 2;
gi.centerprintf(ent,"You have chosen MOS 91B - Combat Medic.\n\nGood Luck!\n");
}
else
{
//For completeness. We should NEVER get here.
ent->client->resp.mos = 0;
gi.centerprintf(ent,"Invalid MOS selection!\n");
}
//Play a sound just to be cool...
gi.sound(ent, CHAN_VOICE, gi.soundindex("player/male/jump1.wav"), 1, ATTN_NORM, 0);
}
//+ - BD - END NEW CODE BLOCK
Well, that was a mouthful! Before you save the file, add the forward declaration of our functions by adding the following include line:
#include "m_player.h"
#include "b_playermos.h" //+ - BD - 1/7 Make our functions visible here
OK, now save the file. Next, add a function prototype to the b_playermos.h file. It should look like
this:
void Cmd_MOS_f(edict_t *ent, char *cmd); //+ - BD - 1/5 - Declaration of the new function
Save the header file.
gi.sound(entity, channel, sound_index, volume, carry, delay)
The arguments are:
So, in a nutshell, that is how you play a sound in Quake 2!