Author: by Victor Jimenez (aka Weektor) Part I - Multiple Command Sets
This mod is a several part modification. There is lot of things that need
to be done to achieve the support of having multiple dll modifications reside
alongside the gamex86.dll or gamex86.so for you Linux users, especially if
you are trying to make the modification work on all platforms. So we'll
begin by explaining the nature of what we are about to do.
As we are at the beginning of the tutorial, I would like to try to explain
a few concepts that guided the reasoning behind the changes that I made to
the Quake 2 code. The first of the concepts is that what I am doing are
architectual in nature. They effect the structure of the code that id put
out. There is very little code that I am putting in to be able to do what I
am attempting. This is the nature of architectual changes. In theory, one
could completely change the architecture of a program with almost no
changes to the code. Conversely, one could rewrite all the code and not
change the architecture at all.
The second concept is that I am a lazy programmer. Believe me, that's a
good thing! This means that I want to make minimal changes to the existing
code. This prevents errors from creeping into the dll and hopefully we will
have the side effect of having the game executing at a decent speed.
The third concept is that we want to be as system non-specific as
possible. We don't want to lock ourselves into a particular platform. id
Software went to great lengths to make the game portable to different
platforms and we need to honor that.
Finally, the last concept is that I want to talk about is
security. Currently, this is no way of being able to determine if a dll is
safe. By making the current dll capable of loading other dll's we now
isolate all the changes that need to be made to support a modification in a
separate dll, one whose code is easier to inspect and determine what it is
doing. In other words, a verifying agency won't have to figure out what all
the changes in the Quake 2 source are doing. And, if you can isolate all
the changes from the gamex86.dll, it becomes a lot easier for the user to
verify that the new dll that you recieved is indeed the one that everyone
is talking about and not a Trojan Horse that someone renamed to the dll
that you actually wanted to get.
These concepts are important. They define the boundaries of our architecture.
On to the actual changes! After studying the gamex86.dll, we can see that
it contains items, commands, monsters and clients (players). It is
important to also realize what it doesn't contain - maps, or rather, the
abililty to manipulate maps. Clients can already be added at will. id made
sure of that. This lets people have servers, create bots, etc. So we are
left with items, commands, and monsters to add at will.
Since this is a lot of territory to cover, this first tutorial will deal
with setting up some basic structures and what needs to be done to allow
multiple dll's containing commands to be added. I am picking this as the
first part because we will need to expand on this later to arrive at a
general solution to the multiple dll modification. Don't worry, I promise
to explain later.
We'll look at the problem of having multiple command sets first. A command
set is a group of commands that is added by an external dll. Commands
reside in the g_cmds.c file. Taking a look at that file, we realize, horror
of horrors, all the commands are hard coded in to the source. This is our
first architectual change. We need to make the commands data driven. This
requires that we create some data structures to hold the information that
will need to be available for the program to use. We will need to provide a
way to look up the command, determine the number of arguments to pass to
the function and a way of calling the actual function. Therefore, we have
the following structure:
Each command that we provide will need one of these data items defined for
it. The command is a string that contains the name that everyone agrees to
call the function. I recommend that you make this string be the actual name
of the function. It'll just be easier for you later to track what you are
doing. The number of args is based on having the current entity and the
command line arguments passed in.
Also, we need a place to hold all the commands that have been defined. This
is how we create the command set. We'll have the dll create the list of
commands statically and pass the set of commands that it is implementing to
the game dll. We will make the command set into a linked list so we can
grow the list dynamically. With all that in mind, I developed the following
data structure:
The source lets us specify who added the command. It also provides a sort
of name space. We will use this later to solve the problems of collisions,
that is, if your killer dll has a function that is named the same as
somebody else's killer dll. The next field allows us to do linked list
manipulations. The command field points to an array of our previously
defined structure that holds all the pointers to functions along with their
names and the numCmd field tells us how many entries there are in the
command array.
Since we are dealing with a linked list, we realize a couple of things. We
need to provide functions to insert command sets, release the command sets,
find a command and finally, for my own piece of mind, a little security
feature to print out the command set so we can inspect what has been
added. We will also need to declare a head of list pointer somewhere. The
logical place that seemed to me appropriate is the g_cmds.c file. So in the
g_cmds.c file you will find:
Is there anything that we are forgeting? Ah yes. What about the functions
that id has already provided in their dll? We need to set up a command set
for those functions. Furthermore, as we inspect the current code, to see
what functionality is there, we find that some functionality is not even in
a proper function. It is maintained in the ClientCommand function in a case
statement. We need to create functions for these pieces of code, the fov,
and the version commands. The following code illustrates this:
We are in the home stretch for fixing the command architecture so we can
add commands at will. But we are missing one last piece of the puzzle. If
we examine the game_export_t structure, we find that one of the structure
members around line 193 is called ClientCommand. And if we inspect
g_main.c, where the game_export_t structure is filled in before being
passed back to the game engine, we see on line 110 that the ClientCommand
member is being filled in with the ClientCommand function that is defined
in the g_cmds.c file. This means that all commands that the engine doesn't
fulfill are sent to the ClientCommand. All we have to do is hijack this
function and make it access our list. Therefore, find the ClientCommand
function in the g_cmds.c file and replace it with this:
Whew! We are done changing the g_cmds.c file for now. Since we are going to
need these structures and functions and variables from other parts of the
dll, we need to create a header file. Below is the complete g_cmds.h:
There is one last thing that we need to do. Since we are now treating the
id provided commands as though they are external, we need to actually
register them, just like any other dll would have to do. And since we are
registering the commands, we also need to clean them up. The problem is
that it is not quite obvious where these operations should be done. Looking
at the g_main.c file, you will find a function called ShutdownGame around
line 78. The problem is that there is no corresponding StartupGame function
defined in that file. There is an InitGame function in the g_save.c file
(no, I don't know why it's in there) but it seems to be reading the cvars,
and calling functions like InitClientPersistant which are defined in the
p_client.c file.
Looking around in the p_client.c file, I found a function called
ClientConnect around line 850 that says that it is called when the player
starts connecting to the server. As we know, quake, even in single player
mode, is really a network game. It ties into a server, even if it is a
local server on your box. With a little experimentation, we find that for
each time that the ClientConnect gets called, there will be a corresponding
ShutdownGame called. Therefore, the ClientConnect is the true StartupGame
function! So what we want to do is the following.
We want to create a static variable just before the ClientConnect function
to ensure that our function is only called once. Then, in the ClientConnect
function, we want to put the call to register the commands. The function
looks like this:
The only thing left to do is to fixup the ShutdownGame function in the
g_main.c file that is located around line 70. This is what the function
looks like:
Compile the files. Run Quake. You won't see any difference except that you
have a new command called "printcmds". Don't forget to put a cmd in front
of it on the command line. I know, it's not very impressive, but that is
the nature of architectual changes. You wish to preserve the functionality
thats already there. But stick around. This is only the start. If you go
back and look at the ClientConnect and the ShutdownGame functions, you
should see a few lines commented out, lines that are talking about user
dlls. And that is the focus of the next part of the tutorial, what do you
need to do to load and manage multiple dll's.
Tutorial by by Victor Jimenez (aka Weektor) . This site, and all content and graphics displayed on it,
Difficulty: Hard to Understand, Moderate to implement
struct g_cmds_t
{
char *command;
int numArgs;
void (*cmdfunc)();
};
struct cmd_list_t
{
char source[32];
struct cmd_list_t *next;
struct g_cmds_t *commands;
int numCmds;
};
struct cmd_list_t *GlobalCommandList = NULL;
/*
takes a cmd structure and inserts it into the GlobalCommandList.
instead of allocating additional memory, it utilizes the memory allocated
to hold the commands and inserts a pointer to it.
Note that we are just keeping a list of all the commands from each structure
together.
*/
void InsertCmds(struct g_cmds_t *cmds, int numCmds, char *src)
{
struct cmd_list_t **ptr;
struct cmd_list_t *tmp;
gi.dprintf("processing %s commands\n",src);
ptr = &GlobalCommandList;
while(*ptr)
ptr = &((*ptr)->next);
/*at this point, ptr is pointing to a pointer var whose value is NULL*/
/*not sure if I should be using malloc*/
tmp = (struct cmd_list_t *)malloc(sizeof(struct cmd_list_t));
tmp->commands = cmds;
//tmp->numCmds = sizeof(cmds) / sizeof(struct g_cmds_t);
tmp->numCmds = numCmds;
strncpy(tmp->source,src,32);
tmp->next = NULL;
// gi.dprintf("sizeof(*cmds) = %d, sizeof(struct g_cmds_t) = %d\n", sizeof(*cmds),sizeof(struct g_cmds_t));
// gi.dprintf("number of commands processed = %d\n",tmp->numCmds);
*ptr = tmp;
}
/*
This function walks the global command list and prints out all the commands that it finds.
*/
void PrintCmds()
{
struct cmd_list_t *ptr;
struct g_cmds_t *tmp;
int i;
ptr = GlobalCommandList;
while(ptr)
{
gi.dprintf("printing <%s> commands:\n",ptr->source);
tmp = ptr->commands;
for(i=0;i < ptr->numCmds; i++, tmp++)
gi.dprintf("%s has %d args.\n",tmp->command, tmp->numArgs);
ptr = ptr->next;
}
// gi.dprintf("sizeof(*cmds) = %d, sizeof(struct g_cmds_t) = %d\n", sizeof(*cmds),sizeof(struct g_cmds_t));
// gi.dprintf("number of commands processed = %d\n",tmp->numCmds);
// *ptr = tmp;
}
/*
this function frees all the memory that has been allocated by the
InsertCmds function.
*/
void CleanUpCmds()
{
struct cmd_list_t *tmp1, *tmp2;
tmp1 = GlobalCommandList;
while (tmp1)
{
tmp2 = tmp1->next;
free(tmp1);
tmp1 = tmp2;
}
GlobalCommandList = NULL;
}
/*
this is a crucial function. It searches the GlobalCommandList, looking
for the command that is passed in. It performs a two dimensional search,
first going from one set of commands to another and searching inside
for the command. It returns a pointer to the g_cmds_t structure that has
the command in it.
I recommend that the commands be placed in individual namespaces, that is,
each mod have it's own prefix that is placed in front of the command.
*/
struct g_cmds_t *
FindCommand(char *cmd)
{
struct cmd_list_t *sets;
struct g_cmds_t *cmds;
int i;
// gi.dprintf("Looking for command <%s>\n",cmd);
sets = GlobalCommandList;
while (sets)
{
// gi.dprintf("Processing set\n");
cmds = sets->commands;
// gi.dprintf("number of commands %d\n",sets->numCmds);
for (i=0;i
struct g_cmds_t id_GameCmds[NUM_ID_CMDS] =
{
"use", 1, Cmd_Use_f,
"drop", 1, Cmd_Drop_f,
"give", 1, Cmd_Give_f,
"god", 1, Cmd_God_f,
"notarget", 1, Cmd_Notarget_f,
"noclip", 1, Cmd_Noclip_f,
"help", 1, Cmd_Help_f,
"inven", 1, Cmd_Inven_f,
"invnext", 1, SelectNextItem,
"invprev", 1, SelectPrevItem,
"invuse", 1, Cmd_InvUse_f,
"invdrop", 1, Cmd_InvDrop_f,
"weapprev", 1, Cmd_WeapPrev_f,
"weapnext", 1, Cmd_WeapNext_f,
"kill", 1, Cmd_Kill_f,
"putaway", 1, Cmd_PutAway_f,
"wave", 1, Cmd_Wave_f,
"gameversion", 1, Cmd_GameVersion_f,
"fov", 2, Cmd_FOV_f,
"printcmds",0,PrintCmds
};
void Cmd_GameVersion_f (edict_t *ent)
{
gi.cprintf (ent, PRINT_HIGH, "%s : %s\n", GAMEVERSION, __DATE__);
}
void Cmd_FOV_f (edict_t *ent, char *arg1)
{
ent->client->ps.fov = atoi(gi.argv(1));
if (ent->client->ps.fov < 1)
ent->client->ps.fov = 90;
else if (ent->client->ps.fov > 160)
ent->client->ps.fov = 160;
}
/*
=================
ClientCommand
=================
*/
/*
A couple of rules here. first off, I had to select something reasonable
for the number of arguments that could be specified on the command line.
I chose three. If you need more, declare your routine as needing 0 and
manipulate the gi.argv structure in your routine.
Secondly, the first command found is the one that gets executed.
Thirdly, the routine is not particularly efficient in finding and managing
the entries. That will be a future upgrade.
*/
void ClientCommand (edict_t *ent)
{
char *cmd;
struct g_cmds_t *cmdptr;
if (!ent->client)
return; // not fully in game yet
cmd = gi.argv(0);
cmdptr = FindCommand(cmd);
if(cmdptr)
{
switch (cmdptr->numArgs)
{
case 0:
(cmdptr->cmdfunc)();
break;
case 1:
(cmdptr->cmdfunc)(ent);
break;
case 2:
(cmdptr->cmdfunc)(ent,gi.argv(1));
break;
case 3:
(cmdptr->cmdfunc)(ent,gi.argv(1),gi.argv(2));
break;
case 4:
(cmdptr->cmdfunc)(ent,gi.argv(1),gi.argv(2),gi.argv(3));
break;
}
}
else
gi.cprintf (ent, PRINT_HIGH, "Bad command: %s\n", cmd);
}
/*
g_cmds.h
contains definitions to allow dynamic functionality added to Quake2
vjj 01/20/98
*/
struct g_cmds_t
{
char *command;
int numArgs;
void (*cmdfunc)();
};
struct cmd_list_t
{
char source[32];
struct cmd_list_t *next;
struct g_cmds_t *commands;
int numCmds;
};
void InsertCmds(struct g_cmds_t *cmds, int numCmds, char *src);
void CleanUpCmds();
struct g_cmds_t *FindCommand(char *cmd);
void PrintCmds();
void Cmd_GameVersion_f (edict_t *ent);
void Cmd_FOV_f (edict_t *ent, char *arg1);
void SelectNextItem (edict_t *ent);
void SelectPrevItem (edict_t *ent);
void ValidateSelectedItem (edict_t *ent);
void Cmd_Give_f (edict_t *ent);
void Cmd_God_f (edict_t *ent);
void Cmd_Notarget_f (edict_t *ent);
void Cmd_Noclip_f (edict_t *ent);
void Cmd_Use_f (edict_t *ent);
void Cmd_Drop_f (edict_t *ent);
void Cmd_Inven_f (edict_t *ent);
void Cmd_InvUse_f (edict_t *ent);
void Cmd_WeapPrev_f (edict_t *ent);
void Cmd_WeapNext_f (edict_t *ent);
void Cmd_InvDrop_f (edict_t *ent);
void Cmd_Kill_f (edict_t *ent);
void Cmd_PutAway_f (edict_t *ent);
void Cmd_Wave_f (edict_t *ent);
void ClientCommand (edict_t *ent);
#define NUM_ID_CMDS 20
struct g_cmds_t id_GameCmds[];
/*
===========
ClientConnect
Called when a player begins connecting to the server.
The game can refuse entrance to a client by returning false.
If the client is allowed, the connection process will continue
and eventually get to ClientBegin()
Changing levels will NOT cause this to be called again.
============
*/
static int AlreadyDone = 0;
qboolean ClientConnect (edict_t *ent, char *userinfo, qboolean loadgame)
{
if (!loadgame)
{
// clear the respawning variables
InitClientResp (ent->client);
InitClientPersistant (ent->client);
}
if(!AlreadyDone)
{
//the client connect function gets called a number of times. we need to set
//this variable up to allow functions to be called only once.
AlreadyDone = 1;
InsertCmds(id_GameCmds, NUM_ID_CMDS, "id");
//LoadUserDLLs("quserdll.ini");
//InitializeUserDLLs();
}
ClientUserinfoChanged (ent, userinfo);
if (game.maxclients > 1)
gi.dprintf ("%s connected\n", ent->client->pers.netname);
level.players++;
return true;
}
/*these functions perform some type of clean up */
/*removes all the commands from the list - in g_cmds.s*/
void CleanUpCmds();
/*
=================
GetGameAPI
Returns a pointer to the structure with all entry points
and global variables
=================
*/
void ShutdownGame (void)
{
gi.dprintf ("==== ShutdownGame ====\n");
CleanUpCmds();
//ClearUserDLLs();
gi.FreeTags (TAG_LEVEL);
gi.FreeTags (TAG_GAME);
}
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