
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.hvjj 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.hvjj 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;i num_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