Author: by Victor Jimenez (aka mr_slippery) 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:
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:
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:
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:
Find the function called G_InitEdict(), about line 372 and add the
following two lines at the bottom of the function:
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:
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:
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.
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:
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.
Finally, some future directions and future tutorial idea that I am still
kicking around:
1) Create a TC Skeleton of the game code. Tutorial by by Victor Jimenez (aka mr_slippery) . This site, and all content and graphics displayed on it,
Difficulty: Moderate to Understand, Easy to implement
/*
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
/*
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;i
struct user_vars_s *user_data; //vjj - list of all attached public user data
#include "u_xtndata.h"
e->user_data = NULL; //vjj added 2/25/99
addAllUserData(e);
void (*DefineFields)(struct user_fields_s *flds);
void (*RemoveFields)(char *name);
void *(*FindField) (edict_t *ent, char *name);
void *(*FindGlobalField)(char *name);
UserDLLImports.DefineFields = InsertFldDefs;
UserDLLImports.RemoveFields = RemoveFldDefs;
UserDLLImports.FindField = FindFld;
UserDLLImports.FindGlobalField = FindGlobalFld;
/*
===============
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);
}
CleanUserData();
/*
newjet.c
vjj 04/29/99
We are using the dll template that was created for the multiple dll
modification.
*/
#include
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
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