Compass Tutorial

The following tutorial is aimed at adding compass functionality to the action quake mod, but it could easily be adapted to fit any other mod with little effort on the part of the coder.

- Woebane (jasona@brimstone.com)


usage

This mod will place a compass at the base of your action quake display which allows you to easily determine your orientation on maps. But just having a compass is trivial; the real aid is to team play by presenting the following two macro expansions to the message text:
%D
This will expand out into the compass point you are currently facing. This allows you to tell your teammates where you are going, or where you currently spot the enemy:
bind 5 "say_team I spot the enemy to the %D"
bind 6 "say_team I'm heading %D"
%L
This will expand out into the section of the map where you are currently located. Most maps are divided into nine sections, and the %L macro will expand out into one of those:
bind 7 "say_team I'm at the %L"

Explanation and comment text from the author will appear in normal red text, specific coding instructions will appear in red italic text and the actual code will appear in blue.


file: q_shared.h

Make the following three additions to the file q_shared.h:


We'll want to reserve 8 strings in the gi.configstring array for our own use. Let's define that space like the rest of the game does.
Around line 60, right after the MAX_CLIENTS definition, add the following:


//
// per-level limits
//
#define MAX_DIRECTIONS                    8

We will need to be able to name a field on the Heads-Up-Display scripting language that will contain the name of the direction we're heading in and a bit to check to see if it should be displayed.
Around line 1107, after the STAT_GRENADES defintion, add the following:

#define STAT_COMPASS                    30
#define STAT_COMPASS_DIR                31

We need to map those 8 reserved gi.configstring spaces into an index for later use.
Around line 1184 of q_shared.h, right after the CS_PLAYERSKINS definition, replace the "#define CS_GENERAL" defintion line with the following two lines:

#define CS_DIRECTIONS          (CS_PLAYERSKINS+MAX_CLIENTS)
#define CS_GENERAL                    (CS_DIRECTIONS+MAX_DIRECTIONS)

file: a_game.c


Make the following additions to the file a_game.c:


When telling a player where they are on a map, we'll need some sort of reference to the size of the map. We do that by storing extremes of coordinates in four parameters, the greatest x-axis value will become the Northernmost point (north), the lowest the Southernmost point (south), the greatest y-axis value will become the Westernmost point (west), and the least the Easternmost (east).
At the top of the file, add the following definition to the end of definitions block:


int north, south, east, west;

We divide the whole map into 9 sectors, like a tic-tac-toe board. We also divide the player's 360-degree of possible xy-axis viewing into just eight pie pieces, so that we can label those. The utility functions for doing this and turning those sectors and pie-pieces into English follow:
Around line 936, after the GetAmmo() function, add these three functions:

void GetLocation(edict_t *self, char *buf)
{
	int ns = 0;
	int we = 0;

	if ((north - ((north - south) / 3)) < self->s.origin[0])
	    ns = 1; 
	else if ((south + ((north - south) / 3)) > self->s.origin[0])
	    ns = -1; 
	if ((west - ((west - east) / 3)) < self->s.origin[1])
	    we = 1; 
	else if ((east + ((west - east) / 3)) > self->s.origin[1])
	    we = -1;

	if (ns == 1) {
	    if (we == 1)
		strcpy(buf, "North West corner");
	    else if (we == -1)
		strcpy(buf, "North East corner");
	    else
		strcpy(buf, "Northern edge");
	} else if (ns == -1) {
	    if (we == 1)
		strcpy(buf, "South West corner");
	    else if (we == -1)
		strcpy(buf, "South East corner");
	    else
		strcpy(buf, "Southern edge");
	} else {
	    if (we == 1)
		strcpy(buf, "Western edge");
	    else if (we == -1)
		strcpy(buf, "Eastern edge");
	    else
		strcpy(buf, "center");
	}
}

int ComputeDirectionIndex(edict_t *self)
{
        float d = 0;

	if (!self->client || !self->client->v_angle)
	    return 0;

        d = (((self->client->v_angle[1] * 8.0)) / 360.0) + 0.5;
	while (d >= 8.0)
	    d = d - 8.0;
	while (d < 0.0)
	    d = d + 8.0;

	return (int) d;
}

void GetDirection(edict_t *self, char *buf)
{
	switch (ComputeDirectionIndex(self))
	{
        case 0:
	    strcpy(buf, "North");
	    break;
        case 1:
	    strcpy(buf, "North West");
	    break;
        case 2:
	    strcpy(buf, "West");
	    break;
        case 3:
	    strcpy(buf, "South West");
	    break;
        case 4:
	    strcpy(buf, "South");
	    break;
        case 5:
	    strcpy(buf, "South East");
	    break;
        case 6:
	    strcpy(buf, "East");
	    break;
        case 7:
	    strcpy(buf, "North East");
	    break;
	}
}

This mod is build to be used with the 1.52 version of Action Quake, but any mod with a ParseSayText() style enhancement can be fitted with the compass mod. For anyone trying to add this to a base 3.20+ quake source release, you'll want to add a set of features that modify specific tokens in what the players say when they're sending messages with the say and say_team commands. For ideas I'd suggest looking at the Action Quake source or some of the CTF style mods that have ParseSayText() style code included.
Around line 1143, at the end of the switch statement in the ParseSayText() function, add these two cases:

                                case 'D':
                                        GetDirection(ent, infobuf);
                                        strcpy(pbuf, infobuf);
                                        pbuf = SeekBufEnd(pbuf);
                                        p += 2;
                                        continue;
                                case 'L':
                                        GetLocation(ent, infobuf);
                                        strcpy(pbuf, infobuf);
                                        pbuf = SeekBufEnd(pbuf);
                                        p += 2;
                                        continue;

file: g_spawn.c

Make the following additions to the file g_spawn.c:


When getting the dimensions of the map, I use a cheat and just take the position of every spawnable item put in the game. The one that's furthest along the positive x-axis is assumed to be the Northernmost point for purposes of composing the tic-tac-toe grid, etc... Note that this assumption can be tripped up by maps that have spurious or non-appearing entries outside the area where players can appear.
Also note that I define the external variables within this function rather than in a single .h file (like a_game.h). This is bad form, but this is a small mod and I only do this twice, so you can call me lazy and move the externs to their since-source-of-definition .h if you want to be a real programmer.
Around line 586, at beginning of the SpawnEntities() function, at the end of the defintions block, add the following:


        int dirTouched = 0;

	extern int north, south, east, west;

We initialize the values to anything. It doesn't really matter as the first spawnable item we run across will set these values again, but the code looks more readable this way.
Right before the "while (1)" loop in the SpawnEntities() function, near line 616, add the following:

	north = 0;
	south = 0;
	east = 0;
	west = 0;

As soon as we know we're dealing with a real entity, we extract coordinates from it. If it's the very first entity, we automaticly grab it's coordinates and assign it to our map dimensions. After the first entity, we just modify any coordinate that exceeds our previous maximums for each individual axis.
In the "while (1)" loop, after the "if (!ent)" block of code around line 646, place the following code:

		if (dirTouched) {
		    if (ent->s.origin[0] > north)
			north = ent->s.origin[0];
		    if (ent->s.origin[0] < south)
			south = ent->s.origin[0];
		    if (ent->s.origin[1] < east)
			east = ent->s.origin[1];
		    if (ent->s.origin[1] > west)
			west = ent->s.origin[1];
		} else {
		    north = ent->s.origin[0];
		    south = ent->s.origin[0];
		    east = ent->s.origin[1];
		    west = ent->s.origin[1];
		}

Now I really should have drawn 8 different icons of a compass in each of the compass points that I wanted to present, and then had the compass icons appear on the screen as the play moved around, but I'm no artist, so I just used text.
What we're doing with the following code is adding to the heads-up-display definition text so that it will show the on-screen compass when it's in use. Action quake has three different heads-up-display modes, so we make sure to add the follwing code three times.
Add the following text to the end of the definitions for the following three variables: single_statusbar (the end is around line 880), dm_status_bar (the end is around line 1014) and dm_noscore_statusbar (the end of it is around line 1155). Make sure you add this definitions to all three locations, and place them in the actual defintion of the variable (before the ; character that terminates the string definition).

// Compass
"if 30 "
  "xv 0 "
  "yb -58 "
  "stat_string 31 "
"endif "

I would rather have put this code in a function like InitGame(), but since the gi.configstring() function can't be called when in that function, I'll have to resort to redefining these variable each time a map is loaded. Not that it's any great time sink, but it's just not the way it should be done...
This code defines the strings that should be printed on the player's screen as they are turning around the map.
At the very end of SP_worldspawn() function, around line 1410, place the following:

	gi.configstring(CS_DIRECTIONS+0, "NORTH");
	gi.configstring(CS_DIRECTIONS+1, "NORTH WEST");
	gi.configstring(CS_DIRECTIONS+2, "WEST");
	gi.configstring(CS_DIRECTIONS+3, "SOUTH WEST");
	gi.configstring(CS_DIRECTIONS+4, "SOUTH");
	gi.configstring(CS_DIRECTIONS+5, "SOUTH EAST");
	gi.configstring(CS_DIRECTIONS+6, "EAST");
	gi.configstring(CS_DIRECTIONS+7, "NORTH EAST");

file: p_hud.c

Make the following two additions to the file p_hud.c:


Again, a non-lazy programer would put this external definition in a .h file so that if it's API ever changes, there's only a single spot of entry/definition for the maintenance to pay attention to.
In the function G_SetStats(), right after the initial definitions, also define the following:


	extern int ComputeDirectionIndex(edict_t *);

We need to update the player's HUD with their current direction, so we change their HUD-specific parameters to reflect their new compass possition. Note that I turn off the compass when the player is killed. I have no real reason for doing this, and if it bugs you, just take out the if check and always assign the STAT_COMPASS to 1.
In the function G_SetStats(), at the very top of the "if (!ent->client->chase_mod)" code block, place the following code:

		if (ent->health >= 0 && ent->client->v_angle) {
		    ent->client->ps.stats[STAT_COMPASS] = 1;
		    ent->client->ps.stats[STAT_COMPASS_DIR] = CS_DIRECTIONS + ComputeDirectionIndex(ent);
		} else {
                    ent->client->ps.stats[STAT_COMPASS] = 0;
		}

And that should be it. Compile and play.

machiaphilia contact woebane brimstone