Quake DeveLS - Multi DLL Support - Part 5

Author: by Victor Jimenez (aka mr_slippery)
Difficulty: Moderate to Understand, Easy to implement

Part V - User Defined State Variables

Welcome to the fifth part of the MultiDLL tutorial. We will wrap up what started in the Part IV and give you the tools to create state variables for entities and modify the list of variables associated with the entity on the fly. We'll also provide the routines so you can access them from your user dll.

As we were discussing in the previous part of the tutorial, entities have properties that be adjusted and changed by external agents such as level editors. The way that this is communicated to the entity in the game is through name-value pairs that are inserted into the map information that gets parsed out. Each of names is associated with an offset position into a structure and a translation of the type to the appropriate format. This information is maintained in the fields array which is located in the g_save.c file. The structure is the first thing in the file. If you look up the definitions of the macros, you will see that they are performing offset calculations into the entity structure.

This is another example of where an object oriented language would have been real nice. You could have defined a vector that would hold a base class and then used that vector to hold derived classed, like something that a user would be doing in order to add their data without disturbing the data structures. Looking at the code that is available to us, I think that this is where id really fell down on the job.

What we now need to do is come up with a way to associate our data with the entity data and have a way of parsing field information that's in a map into the appropriate data structure. The problem with this is that all the current id routines only pass their data structures and don't know anything about our data structures. And, indeed, we ourselves will not know anything about any future mod author's data structures. Currently, everyone just adds their user defined variables to the end of the structure and that's it. Also, it is difficult to add new variables to various other data structures in the game and be able to save them off without destroying the game saved info.

Now, there are several problems with this that we need to consider. We need to realize that there are two types of data that we are interested in. The first type of data is a 'Global' type of data, data that is shared across all entities. The second type of data is instance data, data that belongs to a particular entity and no other one.

We also have the added problem of potentially loading values from maps into these data locations. We'll first tackle how you, the programmer would do it and add in how the program will do it afterwards. Hint: It'll use alot of the same routines that you do.

Ok, first, we need to come up with a consistent way of allowing the user to first specify the variable to add. So open up a file called u_xtndata.h and add the following data structures and function definitions:

/*
  u_xtndata.h

  vjj  04/06/98

  This file contains the data structures and function declarations needed to
  allow the user to associate their data to various game structures. It also
  contains the routines needed to manage those structures and how to 
  notify the game that these structures exist so information from a map can
  be parsed in.
*/

#ifndef U_XTNDATA_H
#define U_XTNDATA_H 1

#define MAX_NAMESPACE_ID    32


//our goal is to provide a dictionary for the routines
//there is a relation between user_vars structure and the user_field_s structure.
//The data is referanced from the user_vars structure. Each entity (for the instance data)
//is given a set of these, one for each user_field_s that has been created in the list.
//The system maintains a list for the globally defined set of variables.
struct user_vars_s
{
    struct user_vars_s *next;
    void   *stuff;
    struct user_fields_s *defs;
};

struct user_fields_s
{
    struct user_fields_s *next;
    char   name[MAX_NAMESPACE_ID];
    int	   ent_type;    //0 - global, 1 - instance
    int	   size;       //number of bytes used to store data
    //struct user_vars_s *vars;  //not used
    //int	   blk_offset;     //not used-this was part of an optimization that I'm not doing.
    int	   num_fields;
    struct field_s *fldArray;
};


//used by the user created dlls to manage the fields
void InsertFldDefs(struct user_fields_s *entType);
void RemoveFldDefs(char *name);
void *FindFld(edict_t *ent, char *name);
void *FindGlobalFld(char *name);


//these functions are available but not part of the export. These should allow the
//game to save state for a particular configuration. I haven't made them accessible because
//they should be part of the save/load game stuff. But I haven't figured out how to use them.
//oh well.
struct user_vars_s *GetGlobalVars();
struct user_vars_s *GetInstanceVars(edict_t *ent);
struct user_fields_s *GetGlobalFields();
struct user_fields_s *GetInstanceFields();

field_t *FindFldDef(edict_t *ent, char *name, char **base);
field_t *FindGlobalFldDef(char *name, char **base);

//add and remove user variables from the entities
void addAllUserData(edict_t *ent);
void freeAllUserData(edict_t *ent);

void freeGlobalData();
//originally I was going to free the field definitions but they are passed in from 
//the user created dll and they are responsible for creating and deleting them.
//void freeInstanceFields();
//void freeGlobalFields();
//however, I did decide to include a general cleanup routine
void CleanUserData();

void PrintFldDefs();  //exported via g_cmds.c



#endif

I'll outline the basic stratedgy for use first. The user creates an array of field definitions using the field_t declaration that we moved into g_locals.h previously. The offsets that are put in correspond to the offset in the new data structure that the programmer wishes to add to every entity.

The field definition is packaged in the user_field_s structure and sent to the InsertFldDefs(). Note that I am following the previous conventions that I had set out in the prior sections of the MultiDLL stuff. If you create it, you destroy it. In other words, just like the previous data structures that you create and pass into the MultiDLL routines, you need to keep this one around until after you have removed it. If you free it during the game, Bad Things Will Happen.

What we are doing is allowing the users to define sets of memory that they need to implement their variables. Internally, the dll will maintain two lists of definitions, the global and the instance definitions respectively. The dll will create a chain of blocks of memory (struct user_vars_s) so that each user's requested variable will be isolated in it's own little block. The dll will maintain the list of global variables for you.

When a entity gets spawned, the instance definition list will be traversed and a chain of memory blocks will be added to the entity to create all the instance variables.

A couple of things to note. The variables that have the global attribute are just that. There is only one list and everyone accesses it. The instance lists are created fresh for each entity, everytime one comes into existance. So use with care and try to add the minimum required. Cleaning the list takes a while. Try to keep it short.

Also, the definitions of variables should not be done 'on the fly.' In the middle of the game, don't add new variables or delete old ones. The code that I provided is not robust enough to handle that. It doesn't perform the right initializations and cleanups. Those would basically involve running through the g_edict list and checking the list of variables being maintained by each edict, a very time consuming process. Since we are aiming for speed, I elected to leave that out. Basically, you should define all the variables at your dll initialization and remove them at shutdown with a slight possibility of being able to do this at level switching time. If you experiment with this, let me know. If Quake was ever made multithreaded, you would have to change the way this is done to safeguard your access.

I am providing a way to access fields similar to how the commands work. You can name fields in particular blocks and a pointer to it will be returned. Note that you can put anything you want into the block, including a pointer to a memory structure. You will need to get a header file from the author of the mod whose variables you are interested in accessing and an explanation of what the fields do. Believe me, it makes me nervous enough having memory accesses like this. This is a hack and while I tried to make it as robust as possible, remember, there is a potential for strange code to be playing with your stuff.

Ok, create u_xtndata.c and put the following code in it:

/*
  u_xtndata.h

  vjj  04/06/98

  This file contains the data storage and function definitions needed to
  allow the user to associate their data to various game structures. It also
  contains the routines needed to manage those structures and how to 
  notify the game that these structures exist so information from a map can
  be parsed in.
*/

#include "g_local.h"           // this has the u_xtndata functions in it
#include "u_xtndata.h"

static struct user_fields_s *InstanceFieldsList = NULL;
static struct user_fields_s *GlobalFieldsList= NULL;

static struct user_vars_s *GlobalVariables = NULL;

//inserts the field definitions into a common structure. The id fields are
//also inserted from p_client.c, just like the commands. Remember,
//your dll is supposed to maintain the space for the field defs, we are
//just pointing to it.
//this is where we start to run into problems. We want to have all the entities
//of the type that we are specifying to have the new fields. This means two things.
//1) we must add the new variable to all the existing entities
//2) we must set it up so that newly created entities of the type that we 
//   are specifying also have the new variables. 
// 
// our strategy is as follows:
// at startup, we pass the id field package to this routine
// This makes it work similar to the way that we handle commands.
// we can then add our own fields to this
//note that this is a helper routine, only concerned about adding things to
//the list of fields. 


/*
  name: void InsertFldDefs(struct user_fields_s *flds)

  This function takes the user defined field structure and places it in either the 
  global list or the instance list.

  If it is placed on the global list, we allocate the defined memory space, create a
  user_var entry and fix up the pointers.
  You should only do this at safe points - ie, game startup , level changes, game end, etc. Basically
  whenever quake is reconstructing the entities in the edict list.

*/
void
InsertFldDefs(struct user_fields_s *flds)
{
	struct user_vars_s *tmp;
    
    //insert into list
	if(flds->ent_type )
	{
		flds->next = InstanceFieldsList;
		InstanceFieldsList = flds;
	}
	else
	{
		flds->next = GlobalFieldsList;
		GlobalFieldsList = flds;
	}

	//see if we need to create a global user_vars_s for the entry.
	if(!flds->ent_type)
	{
		tmp = (struct user_vars_s *)malloc(sizeof(struct user_vars_s));
		tmp->stuff = malloc(flds->size);
		tmp->defs = flds;
		//put it into the global variable list
		tmp->next = GlobalVariables;
		GlobalVariables = tmp;
	}
}



//removes field definitions
//problem is that until an entity has been freed, the space for it is still being held.
//new entities won't have the removed entity information.
//This is of course, dangerous for global fields since a dll may try to deference a stale pointer.
//Please do not cache pointers. Note that name refers to the namespace name, not field name.
//You should only do this at safe points - ie, game startup , level changes, game end, etc. Basically
//whenever quake is reconstructing the entities in the edict list.
void 
RemoveFldDefs(char *name)
{
    struct user_fields_s **pnt, *tmp;

    //find entry in list
    pnt = &InstanceFieldsList;
    while(*pnt != NULL && stricmp((*pnt)->name,name))
        pnt = &((*pnt)->next);
	//either found or *pnt is null
	if(pnt == NULL)
	{
		//search the global list
		pnt = &GlobalFieldsList;
		while(*pnt != NULL && stricmp((*pnt)->name,name))
			pnt = &((*pnt)->next);
	}
	if(*pnt)
	{
		//found something to free
		tmp = *pnt;
		*pnt = tmp->next;
		free(tmp);
	}
    
}

//this findField function implements name spaces so you can specify a
//particular set of fields. It works like the command set namespace, ie.
//namespace.field.
void *
FindFld(edict_t *ent, char *fldnm)
{
	struct user_vars_s *vp;
    struct field_s *flds;
    int i, found;
    char nmspace[MAX_NAMESPACE_ID], *ptr;
    

	//we first need to find the namespace, if any
	nmspace[0] = '\0';
	ptr = fldnm; i = found = 0;
	while (*ptr && i<32 && !found)
	{
		if(*ptr == '.')
			found = 1;
		else
		{
			nmspace[i] = *ptr;
			i++;
			ptr++;
		}
	}
    
	if(found)
	{
        fldnm = ++ptr;
        nmspace[i] = '\0';
	}
	else
		nmspace[0] = '\0';

    
    vp = ent->user_data;
    while(vp)
    {
        if(found)
            if( Q_stricmp(vp->defs->name,nmspace))
			{
				vp = vp->next;
				continue;
			}

        flds = vp->defs->fldArray;

        for (i=0;idefs->num_fields;i++)
            if(Q_stricmp(fldnm,flds[i].name) == 0)
                return ((char *)(vp->stuff)+flds[i].ofs);
        vp = vp->next;
    }
    return NULL;

}



//this findFieldDef function implements name spaces so you can specify a
//particular set of fields. It works like the command set namespace, ie.
//namespace.field.
//you may be wondering why I just don't walk the global instance list. Well,
//because you can dynamically add variables in and I was afraid of the list
//attached to the entity my be out of date with the instance list.
field_t *
FindFldDef(edict_t *ent, char *fldnm, char **base)
{
	struct user_vars_s *vp;
    struct field_s *flds;
    int i, found;
    char nmspace[MAX_NAMESPACE_ID], *ptr;
    

	//we first need to find the namespace, if any
	nmspace[0] = '\0';
	ptr = fldnm; i = found = 0;
	while (*ptr && i<32 && !found)
	{
		if(*ptr == '.')
			found = 1;
		else
		{
			nmspace[i] = *ptr;
			i++;
			ptr++;
		}
	}
    
	if(found)
	{
        fldnm = ++ptr;
        nmspace[i] = '\0';
	}
	else
		nmspace[0] = '\0';

    
    vp = ent->user_data;
    while(vp)
    {
        if(found)
            if( Q_stricmp(vp->defs->name,nmspace))
			{
				vp = vp->next;
				continue;
			}

        flds = vp->defs->fldArray;

        for (i=0;idefs->num_fields;i++)
            if(Q_stricmp(fldnm,flds[i].name) == 0)
			{
				*base = vp->stuff;
                return (flds + i);
			}
        vp = vp->next;
    }
    return NULL;

}



//this findField function implements name spaces so you can specify a
//particular set of fields. It works like the command set namespace, ie.
//namespace.field.
void *
FindGlobalFld(char *fldnm)
{
	struct user_vars_s *vp;
    struct field_s *flds;
    int i, found;
    char nmspace[MAX_NAMESPACE_ID], *ptr;
    

	//we first need to find the namespace, if any
	nmspace[0] = '\0';
	ptr = fldnm; i = found = 0;
	while (*ptr && i<32 && !found)
	{
		if(*ptr == '.')
			found = 1;
		else
		{
			nmspace[i] = *ptr;
			i++;
			ptr++;
		}
	}
    
	if(found)
	{
        fldnm = ++ptr;
        nmspace[i] = '\0';
	}
	else
		nmspace[0] = '\0';

    
    vp = GlobalVariables;
    while(vp)
    {
        if(found)
            if( Q_stricmp(vp->defs->name,nmspace))
			{
				vp = vp->next;
				continue;
			}

        flds = vp->defs->fldArray;

        for (i=0;idefs->num_fields;i++)
            if(Q_stricmp(fldnm,flds[i].name) == 0)
                return ((char *)(vp->stuff)+flds[i].ofs);
        vp = vp->next;
    }
    return NULL;

}

//this findField function implements name spaces so you can specify a
//particular set of fields. It works like the command set namespace, ie.
//namespace.field.
field_t *
FindGlobalFldDef(char *fldnm, char **base)
{
	struct user_vars_s *vp;
    struct field_s *flds;
    int i, found;
    char nmspace[MAX_NAMESPACE_ID], *ptr;
    

	//we first need to find the namespace, if any
	nmspace[0] = '\0';
	ptr = fldnm; i = found = 0;
	while (*ptr && i<32 && !found)
	{
		if(*ptr == '.')
			found = 1;
		else
		{
			nmspace[i] = *ptr;
			i++;
			ptr++;
		}
	}
    
	if(found)
	{
        fldnm = ++ptr;
        nmspace[i] = '\0';
	}
	else
		nmspace[0] = '\0';

    
    vp = GlobalVariables;
    while(vp)
    {
        if(found)
            if( Q_stricmp(vp->defs->name,nmspace))
			{
				vp = vp->next;
				continue;
			}

        flds = vp->defs->fldArray;

        for (i=0;idefs->num_fields;i++)
            if(Q_stricmp(fldnm,flds[i].name) == 0)
			{
				*base = vp->stuff;
                return (flds + i);
			}
        vp = vp->next;
    }
    return NULL;

}


struct user_vars_s *
GetGlobalVars()
{
	return GlobalVariables;
}

struct user_vars_s *
GetInstanceVars(edict_t *ent)
{
	return ent->user_data;
}

struct user_fields_s *
GetGlobalFields()
{
	return GlobalFieldsList;
}

struct user_fields_s *
GetInstanceFields()
{
	return InstanceFieldsList;
}


//this function adds all the defined variables in the InstanceFieldsList to
//the entity passed in.
void
addAllUserData(edict_t *ent)
{
	struct user_vars_s *tmp;
	struct user_fields_s *flds;

	flds = InstanceFieldsList;

	//create a user_vars_s with storage for the entity.
	while(flds)
	{
		tmp = (struct user_vars_s *)malloc(sizeof(struct user_vars_s));
		tmp->stuff = malloc(flds->size);
		memset(tmp->stuff,0,flds->size);
		tmp->defs = flds;
		//put it into the entity's variable list
		tmp->next = ent->user_data;
		ent->user_data = tmp;
		flds = flds->next;
	}

}


//this function walks the chain of user variables on an entity and frees all of them.
void
freeAllUserData(edict_t *ent)
{
	struct user_vars_s *tmp, *udp;
	udp = ent->user_data;

	while(udp)
	{
		tmp = udp ;
		udp = udp->next;
		//udp->next = NULL;
		free(tmp->stuff);
		free(tmp);
	}
	ent->user_data = NULL;
}


//this function walks the global chain of user variables and frees all of them.
void
freeGlobalData()
{
	struct user_vars_s *tmp, *udp;
	udp = GlobalVariables;

	while(udp)
	{
		tmp = udp ;
		udp = udp->next;
		//udp->next = NULL;
		free(tmp->stuff);
		free(tmp);
	}
	GlobalVariables = NULL;
}

/*
void
freeInstanceFields()
{
	struct user_fields_s *ptr, *tmp;

	ptr=InstanceFieldsList;

	while(ptr)
	{
		tmp = ptr;
		ptr = ptr->next;
		free(tmp);
	}
	InstanceFieldsList = NULL;
}


void
freeGlobalFields()
{
	struct user_fields_s *ptr, *tmp;

	ptr=GlobalFieldsList;

	while(ptr)
	{
		tmp = ptr;
		ptr = ptr->next;
		free(tmp);
	}
	GlobalFieldsList = NULL;
}
*/

void
CleanUserData()
{
	edict_t *e;
	int i;

	e = g_edicts;
	for(i = 0; i< globals.num_edicts; i++)
		if(e->inuse && e->user_data)
			freeAllUserData(e);

	freeGlobalData();
}


void
PrintFldDefs()
{
	struct user_fields_s *pnt;
	int i;
	
	gi.dprintf("Printing Instance Field Definitions\n"); 

	pnt = InstanceFieldsList;
	while(pnt)
	{
		gi.dprintf("Name: <%s>, size = %d, fields = %d\n",pnt->name, pnt->size, pnt->num_fields);
		for(i=0;inum_fields;i++)
			gi.dprintf("field (%d) = <%s, %d, %d, %x>\n",i,pnt->fldArray[i].name,
				pnt->fldArray[i].ofs,pnt->fldArray[i].type,pnt->fldArray[i].flags);
		pnt=pnt->next;
	}

	gi.dprintf("Printing Global Field Definitions\n"); 

	pnt = GlobalFieldsList;
	while(pnt)
	{
		gi.dprintf("Name: <%s>, size = %d, fields = %d\n",pnt->name, pnt->size, pnt->num_fields);
		for(i=0;inum_fields;i++)
			gi.dprintf("field (%d) = <%s, %d, %d, %x>\n",i,pnt->fldArray[i].name,
				pnt->fldArray[i].ofs,pnt->fldArray[i].type,pnt->fldArray[i].flags);
		pnt=pnt->next;
	}

}


BTW, that is the correct date on that file. I've had this working in some fashion since that date. Sorry for the delay ...

Notice that we've refenced a new field in the edict_t structure. So open up g_local.h and insert this field at the bottom of the struct edict_s declaration, around line 1055 or so:

    struct user_vars_s *user_data;     //vjj - list of all attached public user data 

Now that we have that code in place, we need to modify the way that entities are created and destroyed. So open up g_utils.c and insert the following include, below the one for g_locals.h:

#include "u_xtndata.h"

Find the function called G_InitEdict(), about line 372 and add the following two lines at the bottom of the function:

	e->user_data = NULL;   //vjj added 2/25/99
	addAllUserData(e);    

Now find the G_FreeEdict(), around line 429 and make it look like the following lines:

gi.unlinkentity (ed); // unlink from world freeAllUserData(ed); //vjj added 02/25/99

Now whenever an entity is created and destroyed by the game, the user defined variables are created and destroyed along with it.

So far we have added everything to the game dll code but have not provided a way for the user to access these functions. We could just let the user get the functions through the FindFunction mechanism that we already export. However, I believe that being able to create fields is an integral part of what most dll creators will want to do. So open up u_loaddll.h and in the userdll_import_t structure, add the following lines before the closing brace, around line 50:

    void (*DefineFields)(struct user_fields_s *flds);
    void (*RemoveFields)(char *name);
    void *(*FindField) (edict_t *ent, char *name);
    void *(*FindGlobalField)(char *name);

Of course, we need to fill these structure members with the appropriate function pointers. So open u_loaddll.c and look for InitializeUserDLLs function. On line 173, just before the loop that runs through the list of libraries, add the following lines:

    UserDLLImports.DefineFields = InsertFldDefs;
    UserDLLImports.RemoveFields = RemoveFldDefs;
    UserDLLImports.FindField = FindFld;
    UserDLLImports.FindGlobalField = FindGlobalFld;

This is the last time that we will change the interface to the user dll's. It is now official. You can program away. Really! Well, mostly.

We are in the home stretch now but not quite done yet. Since we know all the names of the new fields at startup time, before the map entities are processed, we can have the map processing code insert values into the new user fields. This is what I promised at the start of the tutorial and now you can see how the I chose to implement it.

Open up g_spawn.c and find the ED_ParseField(), around line 456. Substitute the following function for the current one.

/*
===============
ED_ParseField

Takes a key/value pair and sets the binary values
in an edict
===============
*/
void ED_ParseField (char *key, char *value, edict_t *ent)
{
	field_t	*f;
	byte	*b;
	float	v;
	vec3_t	vec;
	int     found, type;
	int	ofs;


	ofs = 0;
	type = -1;

	for (f=fields, found = 0 ; f->name && !found; f++)
	{
		if (!Q_stricmp(f->name, key))
		{	// found it
			found = 1;
			if (f->flags & FFL_SPAWNTEMP)
				b = (byte *)&st;
			else
				b = (byte *)ent;
			ofs = f->ofs;
			type = f->type;
		}
	}

	if(!found)
	{
		if(f=FindFldDef(ent,key,&b))
			found = 1;
		else
		{
		  if(b = (byte *)FindGlobalFldDef(key,&b))
		     found = 1;
        }
		if(found)
		{
			ofs = f->ofs;
			type = f->type;
		}
	}   
		

	if(found)
	{
		switch (type)
		{
		case F_LSTRING:
			*(char **)(b+ofs) = ED_NewString (value);
			break;
		case F_VECTOR:
			sscanf (value, "%f %f %f", &vec[0], &vec[1], &vec[2]);
			((float *)(b+ofs))[0] = vec[0];
			((float *)(b+ofs))[1] = vec[1];
			((float *)(b+ofs))[2] = vec[2];
			break;
		case F_INT:
			*(int *)(b+ofs) = atoi(value);
			break;
		case F_FLOAT:
			*(float *)(b+ofs) = atof(value);
			break;
		case F_ANGLEHACK:
			v = atof(value);
			((float *)(b+ofs))[0] = 0;
			((float *)(b+ofs))[1] = v;
			((float *)(b+ofs))[2] = 0;
			break;
		case F_IGNORE:
			break;
		}
		return;
	}

	gi.dprintf ("%s is not a field\n", key);
}

This modification to the ED_ParseField function allows Quake to look up user defined variables while it's reading in the map. Remember, Quake will load your mod first before loading the map, hence yet another idea why it's a good idea to define all your user defined variables first.

Anything else I'm forgetting? Ah yes! We have to make sure that we free all our memory before the game ends. So open up g_main.c, add an #include "u_xtndata.h" to the list of includes under the "u_loaddll.h" and find the ShutdownGame function. There on line 75, between the call to CleanUpCmds() and ClearUserDLLs(), add the following line:

    CleanUserData();

That's it for adding user defined variables on the fly to the edicts. Notice that it only handles edicts. It should handle other things too like the temporary spawn data structure that is used during map reading time and it probably should be extended to handle the game global data and the level data.

Can this approach be improved? Probably. One of the things that it doesn't handle is Client persistent data that needs to be preserved across level changes. The user dll gets notified when the player enters and leaves a level by means of a call back function. You could arrange it so this mechanism would save and restore the player's state. But it really can't fix the main problem in that this is a hack. Not too painful, but a hack nonetheless.

What is needed is a way of knowing what type of entity you are spawning. Then you could add data fields based on class of entity. You know, monsters, boxes, players, etc. You could also save and restore games and have your data saved and restored (to a different file of course). This was my original approach to this but I just couldn't get it to work.

Anyway, this has been a lot. I know that I promised to discuss security too, but that is going to have to wait. Why? Well, I've been studying the security problem and how to get the information out to you and I keep running across legal problems. Considering that security is part actually part of my Real Life Job, I can't simply publish the stuff and consequences be damned, unless, of course, id Software has a job offer waiting for me.:^)

Anyway, security is a special issue that deserves a more careful treatment and it would not change the exported interface to the user dll. It will be handled by changes in the gamex.dll code and a couple of Java programs. Got other things to program. Plus, I need to update to the latest version of the id code.

I've got an example that adds a jetpack to the game. It's kinda a roundabout way of doing it, but I'm doing it this way to show you how all this stuff works.

/*
  newjet.c

  vjj  04/29/99

  We are using the dll template that was created for the multiple dll 
  modification.
*/

#include          //needed for the offsetof macro


#define USER_EXCLUDE_FUNCTIONS 1
#include "../game3.14/g_local.h"
#include "../game3.14/g_cmds.h"
#include "../game3.14/u_loaddll.h"
#include "../game3.14/u_xtndata.h"


/* place the name of your dll here */
#define DLL_NAME    "NewJetPack"

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

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

static void (*PlayerInsertCommands)(struct g_cmds_t *, int, char *);
static void (*(*PlayerFindFunction)(char *t));
static gitem_t *(*PlayerInsertItem)(gitem_t *it, spawn_t *spawn);
static void (*PlayerRemoveItem)(char *name);
static spawn_t *(*PlayerInsertEntity)(spawn_t *t); 
static void (*PlayerRemoveEntity)(char *name);
static void (*PlayerDefineFields)(struct user_fields_s *flds);
static void (*PlayerRemoveFields)(char *name);
static void *(*PlayerFindField) (edict_t *ent, char *name);
static void *(*PlayerFindGlobalField)(char *name);


static int AlreadyInit = 0;
static int AlreadyLoad = 0;

/*
  user code goes here
  */

void (*Com_Printf)(char *msg, ...);   //pretty much always need this one
void (*AngleVectors)(vec3_t a, vec3_t f, vec3_t r, vec3_t u);
void (*VectorScale) (vec3_t i, vec_t s, vec3_t o);
int (*Q_stricmp)(char *s1, char *s2);

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

struct jetpackvars_s
{
    int  flymode;
    float nextsnd;
};

struct field_s jetFldDef[2] =
{
    {
        "userFlyMode",
        offsetof(struct jetpackvars_s, flymode),
        F_INT,
        0
    },
    {
        "userNextThrustSnd",
        offsetof(struct jetpackvars_s, nextsnd),
        F_FLOAT,
        0
    }
};
        

struct user_fields_s jetInstVars =
{
    NULL,                       // pointer to next - always set to null
    DLL_NAME,                    // name of var's mod
    1,                          // we are an instance
    sizeof(struct jetpackvars_s), // how many bytes to alloc
    2,                          // only have two fields
    &(jetFldDef[0])                   // field definitions
};

/*
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",
		*(int *)(PlayerFindField(ent,"userFlyMode")),
		*(float *)(PlayerFindField(ent,"userNextThrustSnd")));
}

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

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

void Cmd_Thrust_f (edict_t *ent)
{
	char    *string;
    int     *ufm;
    float   *unts;

	string=gi.args();

	if (Q_stricmp ( string, "on") == 0)
	{
		ufm = (int *)PlayerFindField(ent,"userFlyMode");
        *ufm = 1;
		unts = (float *)PlayerFindField(ent,"userNextThrustSnd");
        *unts =(float)0.0;
	}
	else
	{
		ufm = (int *)PlayerFindField(ent,"userFlyMode");
        *ufm = 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;
    float *unts;

	//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
    unts = (float *)PlayerFindField(ent,"userNextThrustSnd");

	if (*unts < ptrLevel->time)
	{
		gi.sound (ent, CHAN_BODY, gi.soundindex("weapons/rockfly.wav"), 1, ATTN_NORM, 0);
		*unts = 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 (*((int *)PlayerFindField(ent,"userFlyMode"))) 
	       ApplyThrust (ent);
}

/*
  End user code section
  */


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


/*
  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
UserDLLMD5(char *buf)
{
    buf[0]='\0';  /*do nothing for now*/
}


/* initialization function - called to set up the dll. This is usually
 called to set up mod specific global data*/
void
UserDLLInit(void)
{

	if (AlreadyInit) return;

	AlreadyInit = 1;
	gi.dprintf("In UserDLLInit for DLL %s\n",DLL_NAME);

      //this is where you would acquire any needed function pointers
    Com_Printf = (void (*)(char *msg, ...)) PlayerFindFunction("Com_Printf");
    AngleVectors = (void (*) (vec3_t a, vec3_t f, vec3_t r, vec3_t u))
        PlayerFindFunction("AngleVectors");
    VectorScale = (void (*) (vec3_t i, vec_t s, vec3_t o))
        PlayerFindFunction("VectorScale");
    Q_stricmp = (int (*) (char *s1, char *s2)) PlayerFindFunction("Q_stricmp");

    PlayerInsertCommands(playerThrustCmds,2,"FlyMode");
	OldClientThink = (ptrGlobals->ClientThink);
	ptrGlobals->ClientThink = NewClientThink;
    PlayerDefineFields(&jetInstVars);

      //deathmatch = ptrgi->cvar ("deathmatch", "4", CVAR_SERVERINFO | CVAR_LATCH);

}

/* this is the clean up function - if there were global data structures
 that you had allocated, this is where you get rid of them */
void
UserDLLStop(void)
{}

/* called at the start of each level. The player is in the game. Level
 specific variables can be placed here */
void
UserDLLStartLevel(edict_t *ent)
{}

/* called when the user exits the level. Used to clear out variable so
 that a user ends in a pre-configured state */
void
UserDLLEndLevel(void)
{}

/* called when the player respawns in a level. */
void
UserDLLPlayerRespawns(edict_t *self)
{}

/* called when a player dies */
void
UserDLLPlayerDies(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point)
{}


/*
  we need to initialize the structure that we will pass back, the same
  way that id does it.
  */
static userdll_export_t userdll_export =
{
    1,          //version of the library
    "default",      //creator - put up to 31 chars of your name here
    UserDLLMD5,  //this is supposed to return an MD5 hash of the code
    UserDLLInit,  //initialization function - adds the command in
    UserDLLStop,  //this is the clean up function
    UserDLLStartLevel, //supposed to be called at the start of each level
    UserDLLEndLevel, //called when the user exits the level
    UserDLLPlayerRespawns, //called when user respawns
    UserDLLPlayerDies   //called when the user dies
};


/*
  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
UserDLLGetAPI(userdll_import_t udit)
{

    PlayerInsertCommands =  udit.InsertCommands;
    PlayerFindFunction = udit.FindFunction;
	PlayerInsertItem = udit.InsertItem;
    PlayerRemoveItem = udit.RemoveItem;
    PlayerInsertEntity = udit.InsertEntity; 
    PlayerRemoveEntity = udit.RemoveEntity;
    PlayerDefineFields = udit.DefineFields;
    PlayerRemoveFields = udit.RemoveFields;
    PlayerFindField = udit.FindField;
    PlayerFindGlobalField = udit.FindGlobalField;
    
	gi = *udit.gi;
	ptrGlobals = udit.globals;
	ptrLevel = udit.level;
    ptrGame = udit.game;
    
	gi.dprintf("Inside GetAPI for %s\n",DLL_NAME);
    return userdll_export;
}

Finally, some future directions and future tutorial idea that I am still kicking around:

1) Create a TC Skeleton of the game code.
2) Make a Better Mod framework - include all the functions already as pointers so you don't have to call them up.

And of course,
3) Convert it to work with Q3Arena. Since we have a little information on the server side mods which seem to indicate that the architecture of the Q3A server will be similar to the current Q2 server, I expect this mod to be converted over quickly.


Until next time, keep on fraggin!
mr_slippery

Tutorial by by Victor Jimenez (aka mr_slippery) .

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