Quake DeveLS - Multi DLL Support - Part 2

Author: by Victor Jimenez (aka Weektor)
Difficulty: Hard to Understand, Moderate to implement

Part I - Multiple Command Sets

Congratulations! You have made it to the second part of the Multiple DLL support tutorial. This part builds on the last part. You need to implement the Multiple Command Set tutorial first.

In this part, we are going to see what we need to do in order to have multiple dll's loading into the gamex86.dll. We are also going to start defining the framework in which to develop our own dlls so that they are compatible with each other and run as efficiently as possible.

Before we start, we need to have a little discussion about dynamic loading and how dlls and shared libraries work. Hopefully, this will make what I am doing a little clearer.

Shared libraries and dlls (from now on, when I say dll, I mean both unless I specify an operating system) work by building a table of all the functions that they contain. This table has pointers to all the entry points that the dll provides. The dll usually also contains additional information whether in the same table or a separate table that allows users to specify which function, at run-time, they are interested in. Usually, when an object file is linked to a dll, the linker will use the information in the dll, either by examining it directly or by means of a secondary file to determine what functions exist in the dll. How you do this is highly dependant on the operating system!

When you do this, you are required to know all of the functions that exist ahead of time so that you can call them and tell the linker at compile time to include all the modifications that you wish to have in your dll. Not only that, but you can't have collisions in the names of functions. So what do you do?

Well, you have to have at least one system specific call in your code. That can't be helped. However, you can define your own interface so that all the functions that have to be passed around can do so without any system dependancies. Guess what? That's exactly what id Software did! So any design that we implement should follow that architecture, single entry point into dll, pass around arrays of functions.

One last thing before we get into the actual code. Function pointers are one of those mysterious things in C that always seems to confuse and upset people. It is in fact perhaps the most powerful feature of C and can very easily get you into trouble. What you need to remember is that first and foremost, they are pointers and you can perform all sorts of pointer stuff to them. You can assign them to variables, you overwrite them, you can take the address of a function and assign the value to the pointer. And most important of all, since these are pointers to functions, you can call them. How do you call them? Same way that you call any other function, by the name and a list of parameters following them.

Ok, lets get started. This modification can and does, for the most part, reside in totally separate files.

Since we are loading multiple dlls, we need to first be able to tell the gamex86.dll what dlls to load. We therefore need a file that we can open to get the information about what is the name of the dll file to open, what is the entry point and various other bits of information such as what version is the dll supposed to be and eventually, what is the signature of the dll so that we can rest assured that it is a good dll that we are loading.

I defaulted the name to quserdll.ini. Ideally, this should be loaded from a cvar but I'll leave it as an exercise to the reader. A typical line in the file would look like this:

  filename  {entrypoint_name|entrypoint_index} version [message_digest]

This means that the filename, relative to where Quake2 was loaded from, comes first. The {entrypoint_name|entrypoint_index} indicates that the user can either specify the name fo the entrypoint or the index of the function to call from the function array that the dll has. Version is an integer number. No fancy 1.1.1 or anything like that. Just version 1, 2, etc., just like id's GAME_API_VERSION. Finally, the [message_digest] part indicates that it is an optional value. I will evolve this to be an MD5 hash of the dll that we will check before loading the dll. We'll discuss that in Part 5 of the tutorial.

Why am I going over the format of the input file? Because we need to create a parser that reads the file. Also, it gives us an idea of the type of information that we need to keep around in order to open and use a dll. Since we are going to be loading an undetermined number of dlls, our structure has to be able to grow to accomodate the number of dlls that are specified. The following structure is what I came up with:

struct userdll_list_node
{
	struct userdll_list_node *next;
	char libname[128];
	char entryname[256];
	char version[32];
	char MD5Sig[64];
	HMODULE hDLL;
	userdll_export_t (*EntryPoint)(userdll_import_t);
	userdll_export_t dll_funcs;
};

The next member is used in the list maintainence, libname, entryname, version and MD5Sig all correspond to the items that exist in the line that is in quserdll.ini. The strange looking HMODULE hdll line is a Windows specific data member. I'll need to maintain it so that I can close it later. In linux, it might be a file handle. I'll have a buddy of mine check it out and I'll report his findings.

The final two members of the structure require a more complete explanation. The line that has EntryPoint in it is a variable called EntryPoint that is a pointer to a function that returns a user_dll_t and takes a userdll_import_t as a calling parameter. It points to the entry point to the dll(what else?) that we discussed above. The final member of the structure is a userdll_export_t, the structure that was passed back to us from the EntryPoint function. Remember, what we are implementing is the changes to the gamex86.dll.

The only structures that are left to discuss are the userdll_export_t and the userdll_import_t structures. Following id's example and coming up with reasonable data, here are the two structures. Notice that I followed id's lead and have them as typedefs:

typedef struct
{
    int apiversion;
    char creator[32];

    void (*UserDLLDigest)(char MsgDigest[64]);
    
    void (*UserDLLInit)();
    void (*UserDLLShutdown)();
    void (*UserDLLStartLevel)();
    void (*UserDLLLeaveLevel)();
} userdll_export_t;


typedef struct
{
      //not sure what to pass in.
    game_locals_t *game;
    level_locals_t *level;
    game_import_t *gi;
    game_export_t *globals;

      //for now, we will allow the user to add new commands
      //new items and new monsters.
    void (*InsertCommands)(struct g_cmds_t *, int, char *);
/* commented out - future functionality
    void (*InsertItem)();
    void (*InsertMonster)();    
    void (*InsertClient)();     // for bots?
    */
} userdll_import_t;

The userdll_export_t structure has pointers to the functions to initialize and shut down the user's dll, and to call functions when the user starts and leaves a level. It also has the internal version number of the dll as well as who created the library.

The userdll_import_t structure passes into the user's dll where the various important game structures reside. Currently, there is only one additional command that is being passed in, the InsertCommands function that we defined in part one. This will be changing in Part III and IV of the tutorial, when we refine the interface.

These are all the structures that we will need to support multiple dlls. We need to define the functions that will be available to load and manage the dlls. These functions and any global data that they need are in the file u_loaddll.c and u_loaddll.h.

The first thing to do is figure out which dlls are we supposed to load. Remember that the quserdll.ini file has that information in it, in a certain format. So our first function will allow us to open the ini file, pull in each line and parse the information and build the list of dlls to open. Also, I'm going to put all the system dependant stuff in here, to make it easier to change later to linux. Here is the function:

/*
  int LoadUserDLLs(char *)

  This function takes the name of the ini file that it is to open and read.
  The file contains entries in it that are built the following way -

  filename  {entrypoint_name|entrypoint_index} version [message_digest]
*/
int
LoadUserDLLs(char *quser_ini)
{
    char buffer[512];
    char libname[128];
    char entryname[256];
    char MD5Sig[64];
    char version[32];
    char *ptr, *tmp;
    int  i;
    struct userdll_list_node *unode;

    FILE *fp;
    if(!(fp = fopen(quser_ini,"r")))
    {
	gi.dprintf("Error opening user extension file <%s>\n",quser_ini);
	return 0;
    }

    gi.dprintf("Successfully opened user extension file <%s>\n",quser_ini);

    //ok, we are going to process each line in the file until we are done
    //yes, I know, the parsing is not the most robust. Oh well, it's left as an exercise 
    //to the student.
    while (!feof(fp))
    {
	fgets(buffer,sizeof(buffer),fp);
	gi.dprintf("Processing <%s>\n",buffer);
        ptr = buffer;

	//libname is first
	//eat any whitespace first
	tmp = libname;
	i = sizeof(libname);
	while(*ptr && isspace(*ptr)) ptr++;
	while(*ptr && i-- && !isspace(*ptr))
	    *tmp++ = *ptr++;
	*tmp='\0';

	//get the index or the name of the entry point
        tmp = entryname;
        i = sizeof(entryname);
        while(*ptr && isspace(*ptr)) ptr++;
        while(*ptr && i-- && !isspace(*ptr))
            *tmp++ = *ptr++;
        *tmp='\0';

	//get the version number
	tmp = version;
	i = sizeof(version);
        while(*ptr && isspace(*ptr)) ptr++;
        while(*ptr && i-- && !isspace(*ptr))
            *tmp++ = *ptr++;
        *tmp='\0';

          //if there is a message digest, get it
        tmp = MD5Sig;
        i = sizeof(MD5Sig);
        while(*ptr && isspace(*ptr)) ptr++;
        while(*ptr && i-- && !isspace(*ptr))
            *tmp++ = *ptr++;
        *tmp='\0';

        if(!(unode = (struct userdll_list_node*)
             malloc(sizeof(struct userdll_list_node))))
        {
            gi.dprintf("memory allocation failed for library <%s>\n",libname);
            break;
        }
        strncpy(unode->libname,libname,128);
        strncpy(unode->entryname,entryname,256);
	strncpy(unode->version,version,32);
        strncpy(unode->MD5Sig,MD5Sig,64);

        unode->next = GlobalUserDLLList;
        GlobalUserDLLList = unode;
    };

    fclose(fp);

      //at this point, we have read in all the dlls that are listed in
      //the ini file. We are now going to walk the list and open 
      //all the dlls.
    unode = GlobalUserDLLList;
    while(unode)
    {
          //print out the results
        gi.dprintf("library <%s> ",unode->libname);
        gi.dprintf("entry <%s> ",unode->entryname);
        gi.dprintf("md5 <%s>\n",unode->MD5Sig);

	//this is the system dependent portion of the code. Currently it is set up
	//for windows. This portion would be different for Linux.
	unode->hDLL = LoadLibrary(unode->libname);
	if(unode->hDLL == NULL)
	    gi.dprintf("Couldn't load library %s, errorcode = %d\n",unode->libname,GetLastError());
	else
	{
	    unode->EntryPoint = (userdll_export_t (*)(userdll_import_t))
				GetProcAddress(unode->hDLL,unode->entryname);
	    if(unode->EntryPoint == NULL)
	    {
		gi.dprintf("Could not get entry point %s for library %s\n",
				unode->entryname,unode->libname);
	    }
	}
        unode = unode->next;
    }
        
}

That was a big function. And I forgot to tell you to declare a couple of variable, GlobalUserDLLList and UserDLLImports. Here is the declaration as they appears at the beginning of the u_loaddll.c file:

//this is where we keep the list of user loaded dlls.

static struct userdll_list_node *GlobalUserDLLList = NULL;
static userdll_import_t  UserDLLImports;

That last variable is used to initialize the various external dlls. Initialization consists of walking through the list of dlls that we just formed and calling the entry point function, saving the structure that is passed back and calling the user provided initialization function. This is accomplished in the following function:

int
InitializeUserDLLs()
{
	struct userdll_list_node *unode;
	int numLibsInit;
    userdll_import_t  UserDLLImports;

	numLibsInit = 0;

	//set up the UserDLLImports
	UserDLLImports.game = &game;
	UserDLLImports.level = &level;
	UserDLLImports.gi = &gi;
	UserDLLImports.globals = &globals;

	UserDLLImports.InsertCommands = InsertCmds;

	//ok, run through the list of libraries and get their export structures
	unode = GlobalUserDLLList;
	while(unode)
	{
        gi.dprintf("Init library <%s> ",unode->libname);
        gi.dprintf("entry <%s> ",unode->entryname);
        gi.dprintf("md5 <%s>\n",unode->MD5Sig);

		unode->dll_funcs = (unode->EntryPoint)(UserDLLImports);
		gi.dprintf("dll_funcs.apiversion = %d\n",unode->dll_funcs.apiversion);
		gi.dprintf("unode->version = %d\n",atoi(unode->version));
		if(unode->dll_funcs.apiversion != atoi(unode->version))
			gi.dprintf("Library %s has invalid version %d\n",
			unode->libname, unode->dll_funcs.apiversion);
		else
		{
			//ok, we have a valid api, call initialization function.
			gi.dprintf("Library has valid version\n");

			if(!unode->dll_funcs.UserDLLInit)
				gi.dprintf("Could not initialize library %s\n",unode->libname);
			else
			{
				numLibsInit++;
				unode->dll_funcs.UserDLLInit();  //whew, finally initialize the library
			}
			
		}

		unode = unode->next;
	}
	return numLibsInit;
}

Finally, we need to clear out the dlls that we have opened. Again, we walk the list of dlls and do some system dependent clean up before freeing the memory. In the case of Windows, we call the FreeLibrary function. This is the clean up function:

void
ClearUserDLLs()
{
	struct userdll_list_node *unode, *tmp;

	gi.dprintf("Clearing user DLL's\n");
	unode = GlobalUserDLLList;
	while(unode)
	{
		//this should close the reference to the dll
		if(unode->hDLL)
			FreeLibrary(unode->hDLL);

		tmp = unode;
		unode = unode->next;
		free(tmp);
	}
}


Given that we have these functions, we need to put them somewhere into the game source and insert the u_loaddll.c and u_loaddll.h files into the gamex86 project file. If you remember back to the multiple command set tutorial, we identified the likely spots to insert the start up and shutdown commands. Lets insert these right now. In p_client.c, look for the InsertCmds function that you put in for Part I. It should be around line 865. It should be inside an if statement. Add the following two lines after the InsertCmds:

		LoadUserDLLs("quserdll.ini");
		InitializeUserDLLs();

Yes, I know, I hard coded the quserdll.ini name in. It really should come from a cvar, etc. and there should be error checking to ensure that everything is OK. Well, that can be your contribution.

Now, go to the g_main.c file and find the CleanUpCmds function called. That should be around line 82. Put the following line right after it:

	ClearUserDLLs();

Compile the gamex86.dll source. Congrates! Your Quake2 game is now capable of loading multiple dlls and having them integrate their command set at runtime with your current dll. There's only one little problem and I found it as soon as I finished compiling - there aren't any dlls set up to integrate with the new, fancy, shmancy gamex86.dll! Oops!

Well, seeing as I couldn't test my modifications until there were, I picked out two tutorials from the QDevelS site on PlanetQuake and converted them.

Both of these conversions are in seperate projects and create full blown dlls. The first one is the ID command that was implemented to tell you who you are seeing. Here is the file in its entirety:

/*
  playerID.c

  vjj 02/24/98
  
  This is a demonstration of what needs to be done to current dll's
  so that they can easily be integrated.

  The functionality of this dll was provided by Reven. I got it originally
  from QDevelS on www.planetquake.com - very cool site!

  */  

//#include "g_local.h"
//#include "g_cmds.h"

#include "u_loaddll.h"

//#include "q_shared.h"

void Cmd_id_f (edict_t *ent);

struct g_cmds_t playerIDCmds[1] =
{
    "id",1,Cmd_id_f
};


/* I am showing pointers to these structures. As a programmer, you could
   do something different. You could maintain a pointer to the userdll_import_t
   that was passed in because the game code maintains the data statically.
   (personally, i think that's dangerous) or you could break out the pieces
   that you need or you could even maintain your own copy.
   */
/*
static game_export_t *pglobals;
static level_locals_t *plevel;
static game_locals_t *pgame;
*/
static game_import_t gi;

static void (*PlayerInsertCommands)(struct g_cmds_t *, int, char *);



/*

=================

Cmd_id_f

New function to print player name and dist

By Reven

=================

*/

void
Cmd_id_f (edict_t *ent)
{
    int j;
    char stats[500];
    vec3_t  start, forward, end;
    trace_t tr;

    //j = sprintf(stats, "     NAME              RANGE\n\n");

    gi.centerprintf(ent, "     NAME              RANGE\n\n");

    VectorCopy(ent->s.origin, start);
    start[2] += ent->viewheight;

    AngleVectors(ent->client->v_angle, forward, NULL, NULL);

    VectorMA(start, 8192, forward, end);

    tr = gi.trace(start, NULL, NULL, end, ent,
                  MASK_SHOT|CONTENTS_SLIME|CONTENTS_LAVA);
    if (tr.ent->client)
    {
        j += sprintf(stats + j, "%16s          %i\n",
                     tr.ent->client->pers.netname, (int)(tr.fraction * 512));
        gi.centerprintf(ent, "%s", stats);      

    }
}


/*
  okay, that was the end of the original code. we need to provide the
  framework. This part should be fairly boilerplate. 
  */

/*
  first, we need to set up a number of variables that will be needed while
  running. This would be where we set up the references to the Quake2 things
  like the gi structure, the game structure, etc.

  for our example, we need the InsertCommand function and the gi for the 
  commands to work.
  */


/*
  there are five functions that we need to provide.
  */

/*this is a security function and supposed to return an MD5 hash of the code in radix64*/
void
PlayerIdMD5(char *buf)
{
    buf[0]='\0';  /*do nothing for now*/
}


/* initialization function - adds the command in */
void
PlayerIdInit()
{
	gi.dprintf("In PlayerIdInit\n");
    PlayerInsertCommands(playerIDCmds,1,"PlayerId");
}

/* this is the clean up function */
void
PlayerIdStop()
{}

/* supposed to be called at the start of each level */
void
PlayerIdStartLevel()
{}

/* called when the user exits the level */
void
PlayerIdEndLevel()
{}


/*
  we need to initialize the structure that we will pass back, the same
  way that id does it.
  */
static userdll_export_t playerid_export =
{
    1,          //version of the library
    "vjj",      //creator
    PlayerIdMD5,  //this is supposed to return an MD5 hash of the code
    PlayerIdInit,  //initialization function - adds the command in
    PlayerIdStop,  //this is the clean up function
    PlayerIdStartLevel, //supposed to be called at the start of each level
    PlayerIdEndLevel //called when the user exits the level
};


/*
  finally, at long last, we define the entry point that is called by
  the external loader. In our example, we only care about
  */

userdll_export_t
PlayerIdGetAPI(userdll_import_t udit)
{

    PlayerInsertCommands =  udit.InsertCommands;
	gi = *(udit.gi);
	gi.dprintf("Inside PlayerIdGetAPI function\n");
    return playerid_export;
}

void Com_Printf (char *msg, ...)
{
	va_list		argptr;
	char		text[1024];

	va_start (argptr, msg);
	vsprintf (text, msg, argptr);
	va_end (argptr);

	gi.dprintf ("%s", text);
}

The Com_Printf function was needed by another function so I had to put it in. This points out a problem with the current architecture of the game dll that we will have to overcome to allow easy access to the functions that are provided by the dll. That is the subject of Part III of the tutorial. Don't forget to declare that the PlayerIdGetAPI is an exported function. Using MSVC, I created a file called PlayerId.def with the following lines and added it to the project:

EXPORTS
	PlayerIdGetAPI

Compile the PlayerId project. Create a quserdll.ini file and place the following line in it:

playerid.dll PlayerIdGetAPI 1 1

Move the playerid.dll to your Quake2 directory and start up the game. If you bring up the console and type 'cmd printcmds', you will see, in addition to the normal id command set, a player id command set. Type in "cmd id" and you will see that the id command is called.

Of course, I said that this modification will let you load multiple dlls. So far I've shown you one dll loaded in. Very well. I converted another modification that I had seen on QDevelS, the original jetpack modification. I picked it because it sounded cool and it let me explore the structure of the Quake2 engine. Here is the entire file. Again, it is in its own project:

/*
  flymode.c

  vjj  03/02/98

  This is implementing flying mode command for your trooper. This is the
  second dll that is implemented as a demo of how to do multiple dlls.

  This originally came from the qdevels site at planetquake. It was 
  originally called 'Jetpack'. Muce is the author.
*/


#include "u_loaddll.h"

static game_import_t gi;
static game_export_t *ptrGlobals;
static level_locals_t *ptrLevel;

static void (*PlayerInsertCommands)(struct g_cmds_t *, int, char *);

void Cmd_Thrust_f (edict_t *ent);
void Cmd_PrintThrust_f (edict_t *ent);

struct g_cmds_t playerThrustCmds[2] =
{
	"thrust", 1, Cmd_Thrust_f,
	"printThrust", 1, Cmd_PrintThrust_f
};

void (*OldClientThink)(edict_t *ent, usercmd_t *cmd); //declaring func ptr

// MUCE: added for jetpack thrusting.
//vjj: made static so no one else sees these
static qboolean        userFlyMode;              // 1 on 0 off
static float           userNextThrustSnd;

/*
Cmd_PrintThrust_f(edict_t *ent)

  this is just a debugging function. I'm using it to see if my variables
  are being manipulated.
*/
void Cmd_PrintThrust_f(edict_t *ent)
{
	gi.dprintf("fly mode = %d, thrust snd = %f\n",
		userFlyMode,userNextThrustSnd);
}

/*
=================
Cmd_Thrust_f

MUCE:
To set jetpack on or off
=================
*/

void Cmd_Thrust_f (edict_t *ent)
{
	char    *string;

	string=gi.args();

	if (Q_stricmp ( string, "on") == 0)
	{
		userFlyMode=1;
		userNextThrustSnd=(float)0.0;
	}
	else
	{
 		userFlyMode=0;
	}
}


// MUCE:  JetPack addition!

/*
=================
ApplyThrust

MUCE:
To add thrusting velocity to player
=================
*/
void ApplyThrust (edict_t *ent)
{
	vec3_t forward, right;
	vec3_t pack_pos, jet_vector;

	//MUCE:  add thrust to character
	if (ent->velocity[2] < -500)
		ent->velocity[2]+=((ent->velocity[2])/(-5));
	else if (ent->velocity[2] < 0)
		ent->velocity[2] += 100; 
	else
		ent->velocity[2]+=((1000-ent->velocity[2])/8);

	//MUCE:  add sparks
	AngleVectors(ent->client->v_angle, forward, right, NULL);
	VectorScale (forward, -7, pack_pos);
	VectorAdd (pack_pos, ent->s.origin, pack_pos);
	pack_pos[2] += (ent->viewheight);

	VectorScale (forward, -50, jet_vector);

	gi.WriteByte (svc_temp_entity);
	gi.WriteByte (TE_SPARKS);
	gi.WritePosition (pack_pos);
	gi.WriteDir (jet_vector);
	gi.multicast (pack_pos, MULTICAST_PVS);

	//MUCE: add sound 
	if (userNextThrustSnd < ptrLevel->time)
	{
		gi.sound (ent, CHAN_BODY, gi.soundindex("weapons/rockfly.wav"), 1, ATTN_NORM, 0);
		userNextThrustSnd=ptrLevel->time+1.0;
	}
}


/*
new ClientThink function
*/
void NewClientThink (edict_t *ent, usercmd_t *cmd)
{
	//gi.dprintf("Calling new client\n");
	OldClientThink(ent,cmd);
	if (userFlyMode) 
	       ApplyThrust (ent);
}

/*
  okay, that was the end of the original code. we need to provide the
  framework. This part should be fairly boilerplate. 
  */

/*
  first, we need to set up a number of variables that will be needed while
  running. This would be where we set up the references to the Quake2 things
  like the gi structure, the game structure, etc.

  for our example, we need the InsertCommand function and the gi for the 
  commands to work.
  */


/*
  there are five functions that we need to provide.
  */

/*this is a security function and supposed to return an MD5 hash of the code in radix64*/
void
PlayerFlyModeMD5(char *buf)
{
    buf[0]='\0';  /*do nothing for now*/
}


/* initialization function - adds the command in */
void
PlayerFlyModeInit()
{
	gi.dprintf("In PlayerFlyModeInit\n");
    PlayerInsertCommands(playerThrustCmds,2,"FlyMode");
	OldClientThink = (ptrGlobals->ClientThink);
	ptrGlobals->ClientThink = NewClientThink;
}

/* this is the clean up function */
void
PlayerFlyModeStop()
{}

/* supposed to be called at the start of each level */
void
PlayerFlyModeStartLevel()
{}

/* called when the user exits the level */
void
PlayerFlyModeEndLevel()
{}


/*
  we need to initialize the structure that we will pass back, the same
  way that id does it.
  */
static userdll_export_t playerid_export =
{
    1,          //version of the library
    "vjj",      //creator
    PlayerFlyModeMD5,  //this is supposed to return an MD5 hash of the code
    PlayerFlyModeInit,  //initialization function - adds the command in
    PlayerFlyModeStop,  //this is the clean up function
    PlayerFlyModeStartLevel, //supposed to be called at the start of each level
    PlayerFlyModeEndLevel //called when the user exits the level
};


/*
  finally, at long last, we define the entry point that is called by
  the external loader. In our example, we only care about
  */

userdll_export_t
PlayerFlyModeGetAPI(userdll_import_t udit)
{

    PlayerInsertCommands =  udit.InsertCommands;
	gi = *udit.gi;
	ptrGlobals = udit.globals;
	ptrLevel = udit.level;
	gi.dprintf("Inside PlayerFlyModeGetAPI function\n");
    return playerid_export;
}


//these functions are here just to get the example running
void Com_Printf (char *msg, ...)
{
	va_list		argptr;
	char		text[1024];

	va_start (argptr, msg);
	vsprintf (text, msg, argptr);
	va_end (argptr);

	gi.dprintf ("%s", text);
}


int Q_stricmp (char *s1, char *s2)
{
#if defined(IRIX)
	return strcmp (s1, s2);
#elif defined(WIN32)
	return _stricmp (s1, s2);
#else
	return stricmp (s1, s2);
#endif
}

void VectorScale (vec3_t in, vec_t scale, vec3_t out)
{
	out[0] = in[0]*scale;
	out[1] = in[1]*scale;
	out[2] = in[2]*scale;
}


void AngleVectors (vec3_t angles, vec3_t forward, vec3_t right, vec3_t up)
{
	float		angle;
	static float		sr, sp, sy, cr, cp, cy;
	// static to help MS compiler fp bugs

	angle = angles[YAW] * (M_PI*2 / 360);
	sy = sin(angle);
	cy = cos(angle);
	angle = angles[PITCH] * (M_PI*2 / 360);
	sp = sin(angle);
	cp = cos(angle);
	angle = angles[ROLL] * (M_PI*2 / 360);
	sr = sin(angle);
	cr = cos(angle);

	if (forward)
	{
		forward[0] = cp*cy;
		forward[1] = cp*sy;
		forward[2] = -sp;
	}
	if (right)
	{
		right[0] = (-1*sr*sp*cy+-1*cr*-sy);
		right[1] = (-1*sr*sp*sy+-1*cr*cy);
		right[2] = -1*sr*cp;
	}
	if (up)
	{
		up[0] = (cr*sp*cy+-sr*-sy);
		up[1] = (cr*sp*sy+-sr*cy);
		up[2] = cr*cp;
	}
}

Don't forget the flymode.def for the exported function:

EXPORTS
	PlayerFlyModeGetAPI

Finally, add the following line to the quserdll.ini file and move the flymode.dll to the Quake2 directory:

flymode.dll PlayerFlyModeGetAPI 1 1

As you can see, this tutorial wasn't real bad. It actually had very few modifications to the id source code. It was a little long because we provided two complete examples of converted dlls so that you could see that it really works. However, it does give me the opportunity to point something verrry interesting about the architecture of the Quake2 engine.

If you look carefully at the second example, the jet pack example, you will notice that I am declaring a function pointer called OldClientThink. Furthermore, you'll notice that I am assign it the value that was in the global structure and that I assigned a new function to that variable in the global structure. I've hijacked that particular function to make it serve my needs. Guess which function the Quake2 engine calls? That right, the new function! This ability to hijack a function, making the game provided functions accessible to the user created dlls without having to copy the source code and coming up with a general set of dll functions that you, as a developer, can just fill in to support your mod is the basis for the third part of the tutorial. In the third part, we will get into frameworks, arrays of function pointers, an improved FindCommand function to handle namespaces and even some Java programming for some utilities that we will need. So start downloading the Javasoft JDK1.5!

Until next time,

Victor Jimenez, signing off.

Tutorial by by Victor Jimenez (aka Weektor) .

This site, and all content and graphics displayed on it,
are ©opyrighted to the Quake DeveLS team. All rights received.
Got a suggestion? Comment? Question? Hate mail? Send it to us!
Oh yeah, this site is best viewed in 16 Bit or higher, with the resolution on 800*600.
Thanks to Planet Quake for their great help and support with hosting.
Best viewed with Netscape 4