GreyBear's Quake 2 Tutorials

Chapter 2: Medic! - Part I

Requirements

Contents Introduction

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.

NOTE: 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: 

		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.

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:

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 for the straight dope. For now, we're going to look at:
  • gi.bprintf - (broadcast print) Send a message to all player consoles
  • gi.dprintf - (debug print) Send debugging info to the console. Requires setting the debug cvar.
  • gi.cprintf - (client print) Send a message to a specified client's console
  • gi.centerprintf - (centered print) Send a message to one client's console, centered in the middle of the screen
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.

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: 

	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.

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: 

	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.

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: 

		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.

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: 

	//+ - 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.

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: 

	//+ - 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.

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:

	#include "m_player.h"
	#include "b_playermos.h"	//+ - BD - 1/7 Include function declaration for our functions so they're visible

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: 

	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.

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: \

	bind g "cmd grunt"
	bind m "cmd medic"
Once again, pay attention to the actual string. It must be "cmd grunt", not just "grunt".

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: 

	//+ - 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.

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: 

	gi.sound(entity, channel, sound_index, volume, carry, delay)
The arguments are:
  • entity - the object from whom the sound emanates
  • channel - Dependant upon the flag for meaning.
    • CHAN_AUTO new sound, do the right thing...
    • CHAN_WEAPON weapon sound
    • CHAN_VOICE sound made by player
    • CHAN_ITEM powerups, etc.
    • CHAN_BODY footsteps, etc.
    • CHAN_NO_PHS_ADD audible by all players
    • CHAN_RELIABLE important sounds
  • sound_index - The sound to play, chosen by using the gi.soundindex() function
  • volume - How loud to play the sound
  • carry - How far off can the sound be heard. Flags are:
    • ATTN_NONE heard everywhere, like CHAN_NO_PHS_ADD
    • ATTN_NORM echoes far
    • ATTN_IDLE localized echo
    • ATTN_STATIC immediate vicinity only
  • delay - how long to delay before playing the sound.
So, in a nutshell, that is how you play a sound in Quake 2!

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