Quake DeveLS - Multi DLL Support - Part 1

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

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:

struct g_cmds_t
{
    char *command;
    int numArgs;
    void (*cmdfunc)();
};

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:

struct cmd_list_t
{
    char source[32];
    struct cmd_list_t *next;
    struct g_cmds_t *commands;
    int numCmds;
};

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:

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;inumCmds;i++)
            if(Q_stricmp(cmd,cmds[i].command) == 0)
                return &(cmds[i]);
        sets = sets->next;
    }
    return NULL;
}

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:


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;
}

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:


/*
=================
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);
       
}

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:


/*
  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[];

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:

/*
===========
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;
}

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:

/*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);
}

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,
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