Quake DeveLS - Multi DLL Support - Part 3

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

Part III - User DLL Framework

Congratulations! You have made it through two parts of the Multiple DLL Support tutorial. You now have should have a Quake 2 gamex86.dll that you can specify which dll's you want to be brought in and have them register new commands at will.

Now we get to the really tough part of the Multiple DLL Support tutorial. This part is probably the hardest to understand and the hardest to implement. There is a lot going on and a lot that has to be done. Not only that, but these are probably the hardest concepts that we are going to have to deal with (unless you get into the math behind Part V, Security). Anyway, don't let that scare you off. All programmers should know this stuff and if you are reading this, chances are that you are a good programmer, or at least trying to become one.

Before I get off my soapbox, there is one last thing that I want to say. If you have been following the tutorials, you may be wondering what would happen if id releases another version of its code. The answer is as long as they don't change their architecture, not much. The Quake Engine relies on the game_import_t and game_export_t structures to provide the list of functions that it needs. As long as the Quake2 engine uses that method as an interface and continues to use the method consistently within itself, there won't be any problems using the code the way that I'm outlining.

The discussion on how id uses those structures actually leads us into the discussion of objects and object-oriented programming languages. C is not an object-oriented language by any means. However, this does not mean that you can't do object-oriented programming. OO Programming is a philosophy, a world view of how to do certain things. It's been around a long time (at least since Lisp and frames) and a lot of software and systems have used it as their paradigm (remember the Amiga?) It's not anything new or scary. What you need to remember is that it is a way of making sure that only the right functions (methods) twiddle with your internals structures (attributes). For instance, if you want to implement a data structure called a stack and you are using an array to maintain your data, only the functions that you have defined as working on the stack should do anything to the array. You should never, ever do something to the stack directly from an outside routine. This is called encapsulation. There are other benefits to OO languages, namely inheritance and polymorphism. These are just fancy terms for stuff that you do all the time without realizing. For instance, in this part of the tutorial, we are going to develop a framework that you can reuse over and over again to implement your killer dll. That's inheritance. And whenever you hijack a pointer to a function, so that you can have the function do something else, that's polymorphism.

Guess what? id's gamex86.dll is very, very object oriented. They just did it in C rather that C++, Java or Smalltalk. We will use this to our advantage to allow us to do Really Neat Stuff(tm)!

What we are going to do in Part III of this tutorial is develop a framework that lets the mod developer pick and choose the things that he or she need to change in the gamex86.dll without having to change the id code. The framework will provide canned functionality to change pieces of the game at will and register itself and set itself up. However, the programmer should be able to remove the parts of the framework that are not needed.

Futhermore, we wish to impose a standard on the way that things are done. This allows the modifications to work together. If we all don't live by the rules, things will break. Note however that living by the rules doesn't necessarily mean that everything will still work together to produce the desired effect. It's just possible, not guaranteed. But in software, unfortunately, there are no guarantees anyway.

Ok, we said that we want to be able to hijack certain functions to allow us to introduce our changes. We also want to be able to call any of the functions in the current gamex86.dll so we don't have to re-implement the functions in the game dll for use in our dll. Both these requirements necessitate having a list of the functions that currently exist.

So in the spirit of id Software and their decision to switch over to Java for their utilities, I present the following Java program:

/**
  func_hdr.java

  vjj   03/06/98

  This set of java class will run through a set of directories and generate
  a list of all the functions that it encounters in the directory and subdirs.
  */


import java.io.*;
import java.util.*;

class ExtensionFilter implements FilenameFilter
{
    private String ext_;
    ExtensionFilter(String ext)
    {
        ext_ = ext;
    }
    public boolean accept (File dir, String name)
    {
        File f = new File(dir + "/" + name);
        if (name.endsWith(ext_) || f.isDirectory()) 
            return true;
        else
            return false;
    }
}

/*
  this class handles maintaining the information where the function was found
  */
class func_definition
{
    String retVal_;
    String name_;
    String parameterList_;
    int  lineNo_;

    public func_definition(String retval, String name, String parms, int ln)
    {
        retVal_ = retval;
        name_ = name;
        parameterList_ = parms;
        lineNo_ = ln;
    }
 
    public func_definition()
    {
        retVal_ = null;
        name_ = null;
        parameterList_ = null;
        lineNo_ = -1;
    }

    public func_definition(func_definition fd)
    {
        retVal_ = fd.returnValue();
        name_ = fd.functionName();
        parameterList_ = fd.parameterList();
        lineNo_ = fd.lineNumber();
    }

    public void returnValue(String retval)
    {
        retVal_ = retval;
    }

    public void functionName(String name)
    {
        name_ = name;
    }

    public void parameterList(String parms)
    {
        parameterList_ = parms;
    }

    public void lineNumber(int ln)
    {
        lineNo_ = ln;
    }

    public String returnValue()
    {
        return retVal_;
    }

    public String functionName()
    {
        return name_;
    }

    public String parameterList()
    {
        return parameterList_;
    }

    public int lineNumber()
    {
        return lineNo_;
    }
    
}

/*
  this class processes a file and creates a list of functions that it
  found. The FuncParser determines what is a function or not.
  */
class file_funcs
{
    String filename_;
    File file_;
    Vector funcs_;
    FuncParser parser_;

    public file_funcs(String filename, File file, FuncParser p)
        throws java.io.FileNotFoundException, java.io.IOException
    {
        filename_ = filename;
        file_ = file;
        funcs_ = new Vector();
        parser_ = p;
        int i;

          //System.out.println("in file funcs, working on " + filename);
        
        parser_.setInputStream(new TokenReader(new FileReader(file_)));

        i = 0;
        
        parser_.getNextFunction();
       while (!parser_.eof())
        {
            funcs_.addElement(parser_.FunctionFound());
            parser_.getNextFunction();
        }

        parser_ = null;
    }
     
    public String fileName()
    {
        return filename_;
    }

    public Vector functions()
    {
        return funcs_;
    }
    
}

class Token
{
    static final int EMPTY = 0;
    static final int EOF = 1;
    static final int EOL = 2;
    static final int L_BRACE = 3;
    static final int R_BRACE = 4;
    static final int L_PAREN = 5;
    static final int R_PAREN = 6;
    static final int SYMBOL = 7;
    static final int OPERATOR = 8;
    static final int STRING = 9;
    static final int D_QUOTE = 10;
    static final int S_QUOTE = 11;
    static final int SPLAT = 12;
    static final int COMMA = 13;
    static final int HASH = 14;

    int type_;
    String lexeme_;

    public Token()
    {
        type_ = Token.EMPTY;
        lexeme_ = null;
    }

    public Token(int t, String lex)
    {
        type_ = t;
        lexeme_ = lex;
    }

    public int type()
    {
        return type_;
    }

    public String lexeme()
    {
        return lexeme_;
    }

    public void type(int t)
    {
        type_ = t;
    }

    public void lexeme(String s)
    {
        lexeme_ = s;
    }
}

//whenever it encounters a valid token, it returns 
class TokenReader
{
    FileReader reader_;
    boolean eof_;
    boolean eol_;
    StringBuffer lexem_;
    int cursor_;
    char lastChar_;
    int lines_processed_ = 0;
    
    TokenReader(FileReader r)
    {
        reader_ = r;
        eof_ = false;
        eol_ = false;
        lexem_ = new StringBuffer();
        cursor_ = -1;
        lastChar_ = '\u0000';
    }

public int currentLineNumber()
    {
        return lines_processed_;
    }
    

    private char nextChar() throws java.io.IOException
    {
        char  res;
        int   temp;
        
        if (lastChar_ != '\u0000')
        {
            res = lastChar_;
            lastChar_ = '\u0000';
        }
        else
        {
            res = '\u0000';
            
            temp = reader_.read();
            if (temp == -1)
                eof_ = true;
            else
                res = (char) temp;
        }
        if (res == '\n' || res == '\u0000')
            eol_ = true;
        else
            eol_ = false;
        
        return res;
    }

    private void unRead(char c)
    {
        if (c == '\n'){
            eol_ = false;
        }
        lastChar_ = c;
    }
                
    public boolean eof()
    {
        int c;
/*
        c = nextChar();
        if (c == '\u0000')
            eof_ = true;
        else
        {
            unRead(c);
            eof_ = false;
        }
        */
        return eof_;
    }
    
    public boolean eol()
    {
        return eol_;
    }
    
    static public boolean isWhite(char c)
    {
        if (c == '\n')
            return false;
        
        if (c >= '\u0000' && c <= '\u0020')
            return true;
        else
            return false;
    }

    static public boolean isOperator(char c)
    {
        String operators = "~/{}[]|[email protected]&*()+=-<>#.;,";
        if (operators.indexOf(c) > -1)
            return true;
        else
            return false;
    }

    static public boolean isAlphaNum(char c)
    {
        if ( (c >= 'a' && c<= 'z') ||
             (c >= 'A' && c<= 'Z') ||
             c == '_' || 
             (c >='0' && c<= '9') ||
             c == '-' || c == '.' )
            return true;
        else
            return false;
    }

    private void eatWhite() throws java.io.IOException
    {
        char c = nextChar();
        while (!eof() && isWhite(c))
            c = nextChar();
        if (!eof()) unRead(c);
    }

      //returns the number of lines that it goes through
    private int eatComment() throws java.io.IOException
    {
        char c;
        char tmp;
        int lines;

        lines = -1;
        c = nextChar();
        if (c == '/')
        {
            c = nextChar();
            if (c == '/' ) 
            {
                lines = 0;
                  //a line comment, eat all characters until the end of line
                  //or file.
                while ( !eof() && c != '\n')
                    c = nextChar();
            }
            else if ( c == '*')
            {
                lines = 0;
                  //a block comment, look for end block or eof
                c = nextChar();
                while (!eof())
                {
                    if (c == '*')
                    {
                        c = nextChar();
                        if (c == '/')
                            break;
                    }
                    if (c == '\n')
                        lines++;
                    c = nextChar();
                }
                if(!eof())
                    c = nextChar();
            }
        }
        unRead(c);
        return lines;
    }

    private String getString() throws java.io.IOException
    {
        StringBuffer buf = new StringBuffer();
        char closer;
        char c;
        
        c = nextChar();
        if (c == '"' || c == '\'')
        {
            buf.append(c);
            closer = c;
            c = nextChar();
            while (c != closer)
            {
                buf.append(c);
                if (c == '\\')
                {
                    buf.append(c);
                    c = nextChar();
                }
                c = nextChar();
            }
            buf.append(c);
        }
          //unRead(c);
        return buf.toString();
    }
  
    Token getToken() throws java.io.IOException
    {
        StringBuffer lexem = new StringBuffer();
        char c;
        Token tok;
        boolean token_found = false;
        int tep;
        
          //first, we have to find the first significant thing
        while(true)
        {
            eatWhite();
              //check if it is possibly a comment
            c = nextChar();
            if (c == '/')
            {
                unRead(c);
                tep = eatComment();
                if (tep == -1)
                    c = '/';
                else
                    lines_processed_ += tep;
            }

              //at this point, we may be at something interesting to process.
            if (eof())
                return new Token(Token.EOF,"");
            if (eol())
            {
                lines_processed_ ++;
                return new Token(Token.EOL,"");
            }
            
            if (c == '"' || c == '\'')
            {
                unRead(c);
                return new Token(Token.STRING,getString());
            }
            
            
            if (isAlphaNum(c))
            {
                while (isAlphaNum(c))
                {
                    lexem.append(c);
                    c = nextChar();
                }
                unRead(c);
                return new Token(Token.SYMBOL,lexem.toString());
            }
            if (isOperator(c))
            {
                lexem.append(c);
                switch (c)
                {
                  case '(':
                      tep = Token.L_PAREN;
                      break;
                  case ')':
                      tep = Token.R_PAREN;
                      break;
                  case '{':
                      tep = Token.L_BRACE;
                      break;
                  case '}':
                      tep = Token.R_BRACE;
                      break;
                  case '"':    //"
                      tep = Token.D_QUOTE;
                      break;
                  case '\'':
                      tep = Token.S_QUOTE;
                      break;
                  case '*':
                      tep = Token.SPLAT;
                      break;
                  case ',':
                      tep = Token.COMMA;
                      break;
                  case '#':
                      tep = Token.HASH;
                      break;
                  default:
                      tep = Token.OPERATOR;
                      break;
                }
                return new Token(tep,lexem.toString());
            }
        }
    }
}


class FuncParser
{
    TokenReader tr_;
    func_definition func_;

public FuncParser()
    {
        tr_ = null;
        func_ = null;
    }
    
public void setInputStream(TokenReader tr)
    {
        tr_ = tr;
    }

private void eatFunctionCode() throws java.io.IOException
    {
        int levelsDeep = 1;
        Token tk;

          //System.out.println("In FuncParser.eatFunctionCode\n");
        
        while (!tr_.eof() && levelsDeep > 0)
        {
            tk = tr_.getToken();
            if(tk.type() == Token.L_BRACE)
                levelsDeep ++;
            if(tk.type() == Token.R_BRACE)
                levelsDeep --;
        }
          //System.out.println("Out FuncParser.eatFunctionCode\n");
        
    }

private String getParms() throws java.io.IOException
    {
        int levelsDeep = 1;
        int numTokens = 0;
        Token tk;
        StringBuffer params = new StringBuffer();

          //System.out.println("In FuncParser.getParms\n");
        
        params.append("(");
        
        while (!tr_.eof() && levelsDeep > 0)
        {
            tk = tr_.getToken();
            if(tk.type() == Token.L_PAREN)
                levelsDeep ++;
            if(tk.type() == Token.R_PAREN)
                levelsDeep --;
            if(numTokens > 0)
                params.append(" ");
            numTokens++;
            params.append(tk.lexeme());
        }
          //System.out.println("Out FuncParser.getParms\n");
        
        return params.toString();
    }
    
    
public void getNextFunction() throws java.io.IOException
    {
        boolean foundFunc = false;
        boolean foundStatic = false;
        Token  tk = new Token();
          //FileReader f;
        String str = new String();
        int     lookingFor;     // 1) type 2) function name 3)parameter list 4) l brace
        String parms;

          //System.out.println("In FuncParser.getNextFunction()\n");

        if(tr_.eof()) return;
        
        lookingFor = 1;

          //func_ is a class variable
        func_ = new func_definition();
        
        while(!tr_.eof() && !foundFunc)
        {
            tk = tr_.getToken();
            switch (tk.type())
            {
              case Token.EOL:
                  break;
              case Token.HASH:
                  while (tk.type() != Token.EOL)
                      tk = tr_.getToken();
                  lookingFor = 1;
                  str = "";
                  break;
              case Token.L_BRACE:
                    //System.out.println("In getNextFunction() { look = "+lookingFor+"\n");
                  if(lookingFor == 4) //processing body of function
                  {
                      if(!foundStatic)  //don't emit a static func
                          foundFunc = true;
                      foundStatic = false;
                      func_.lineNumber(tr_.currentLineNumber());
                      eatFunctionCode();
                      lookingFor = 1;
                        //System.out.println("Found a function! -> " +
                        //                 func_.returnValue()+" " +
                        //                 func_.functionName() + " " +
                        //                 func_.parameterList());
                      
                  }
                  else
                  {
                      str = "";
                      lookingFor = 1;
                  }
                  break;
              case Token.L_PAREN:
                    //System.out.println("In getNextFunction() ( look = "+lookingFor+"\n");
                  if(lookingFor == 3) //processing parameter list
                  {
                      parms =  getParms();
                      func_.parameterList(parms);
                      lookingFor++;
                  }
                  else
                  {
                      str = "";
                      lookingFor = 1;
                  }
                  break;
              case Token.SPLAT:
                    //System.out.println("Found a splat");
                  if(lookingFor == 2)
                  {
                      func_.returnValue(func_.returnValue() + " *");
                  }
                  break;
              case Token.SYMBOL:
                    //System.out.println("In getNextFunction() symbol " +tk.lexeme() + " look = "+lookingFor+"\n");
                  if(lookingFor == 2) // get the function name
                  {
                      str = str + tk.lexeme();
                      func_.functionName(str.toString());
                      lookingFor = 3;
                  }
                  
                  if(lookingFor == 1) //processing return values
                  {
                      if(str.length() > 0)
                          str += tk.lexeme();
                      else
                          str = tk.lexeme();
                      func_.returnValue(str.toString());
                        //System.out.println("str = " + str.toString());
                      lookingFor = 2;
                      if (str.equals("struct") ||
                          str.equals("static") ||
                          str.equals("short") ||
                          str.equals("long")  ||
                          str.equals("unsigned") ||
                          str.equals("signed"))
                      {
                            //tk = tr_.getToken();
                          if(str.length() > 0)
                              str += " ";
                          if(str.equals("static"))
                              foundStatic = true;
                          lookingFor = 1;
                      }
                      else
                          str = "";
                  }
                  break;
              default:
                    //System.out.println("In getNextFunction() default lex = " + tk.lexeme() +" look = "+lookingFor+"\n");
                  str = "";
                  lookingFor = 1;
                  break;
            }
        }
          //System.out.println("Out FuncParser.getNextFunction()\n");

    }

public func_definition FunctionFound()
    {
        return func_;
    }

    public boolean eof()
    {
        return tr_.eof();
    }
    

}


public class func_hdr
{
    ExtensionFilter filter_;
    Vector files_;
    FuncParser p_;

    public func_hdr()
    {
        filter_ = new ExtensionFilter(".c");
        files_ = new Vector();
        p_ = new FuncParser();
    }

    public void process(String path, String file_name[])
    {
        file_funcs funcsInFile;

          //System.out.println("in process");
        for (int i=0;i < file_name.length; i++)
        {
            File current_file = new File(path + file_name[i]);
            if (!current_file.canRead()){
                  //System.out.println("Unable to read " + file_name[i]);
                continue;
            }

            if (current_file.isDirectory()){
                  //System.out.println("processing a directory");
                
                process( current_file.getPath() + "/", current_file.list(filter_));
            }
            else
            {
                try
                {
                    funcsInFile = new file_funcs(path + file_name[i],current_file,p_);
                    files_.addElement(funcsInFile);
                }
                catch (java.io.FileNotFoundException e)
                {
                    System.out.println(current_file + " Not Found");
                }
                catch (java.io.IOException e)
                {
                    System.out.println(current_file + " Read Error, counts discarded");
                }
            }
        }
          //System.out.println("leaving process, processed " + file_name.length);
        
    }


    public static void main (String argv[])
    {
        file_funcs ff;
        func_definition fd;
        int i;
        
        if (argv.length < 1){
            System.out.println("Must give at least one file or directory");
            return;
        }

        func_hdr fh = new func_hdr();
        fh.process("" , argv);
          //System.out.println("processing done!");

        //System.out.println("#include \"g_local.h\"\n\n"); 
        i = 0;
        for(Enumeration e = fh.files_.elements(); e.hasMoreElements();)
        {
            ff =(file_funcs) e.nextElement();
            System.out.println("// File : "+ff.fileName());
            for(Enumeration f = ff.functions().elements(); f.hasMoreElements();)
            {
                i++;
                fd = (func_definition)f.nextElement();
                if(fd.returnValue().startsWith("static"))
                    System.out.print("// ");
                System.out.print(fd.returnValue()+ " ");
                System.out.print(fd.functionName());
                System.out.print(fd.parameterList()+";");
                System.out.println("   // found @ line:" +fd.lineNumber());
            }
            System.out.println();
            
        }
    }
}

I decided to do this in Java so I wouldn't have to worry about having to create a separate Linux version (or SGI, or Alpha, or whatever). This is a perfect example of the right tool for the right job. As a side note, if you need to write a utility, even if it is for a single platform, I would recommend Java for it.

The func_hdr program creates output that lists all the functions in all of the source files along with their location in a manner that can be used in a C header file. The output is sent to the console so if you want to save it, you need to pipe it into a file. If a static function is encountered, the program keeps track of the fact that it is a static function and doesn't output a function prototype. Instead, it outputs a comment that tells you the static function's prototype and the location.

I will give a very brief explanation of how the program works. The program has two parts to it, one part that handles finding the files to process, func_hdr.process() and one part to parse the contents, the file_funcs.parser().

The file finding routine uses the Java routines that are supported across the different platforms and is really quite straight forward. You supply it a starting path and, as it goes through all the entries, it will recurse into a directory and process the entries. If an entry is a directory, it will descend into the directory to process the items found there. If the entry is a file, it will pass the file to the parsing routine. The file walking routines are courtesy of my friend Matt Weber, a programming guru.

The parsing routine creates a list of functions from the file that was passed in. These functions are stored in a vector for each file and each file is, in turned, stored in a vector. When we are finally done, we ask the class to deliver up a list of all the functions that it found in the output format. Notice that we have a public static function in the class. In Java, each and every class can have a main defined for it. You tell the Java VM which one to run as the main entry point into your program. In Java, a static function does not mean to limit the visibility of the object to the file. Rather, it means that the function's availability is tied to the class and not a particular instance of the class. In plain English, you can call that function at any time.

One last thing to realize is that the parsing routine does not directly write to file. That is done in the main function, when it walks through the list. This will let us re-use the functionality provided by the parser later.

Go ahead and compile and run the program (you did download the JDK 1.1.5 from www.javasoft.com, right?), passing in the top level directory where the id game source resides and where to dump the output file.

On my machine, it looks like this:

C:\Quake2\src\func_hdr> javac func_hdr.java

C:\Quake2\src\func_hdr> java func_hdr c:\quake2\src\game >allfuncs.h

Now look at the functions. Whew! that's a lot of functions! Potentially we will want to call all of these functions. Potentially, we may want to override any of these functions. We'll tackle the first problem, how to call any of these functions. We'll worry about overriding functions later.

Since we don't have a direct link to the dll functions, we need to reference them via some mechanism rather than the system provided ones. This is done through our user defined interface. What we'll do is declare a function that can fetch us the function pointer that we want. As a matter of fact, it is very similar to what we did with the commands back in Part I of the tutorial. We need to create an array with the names and function pointers and a function to find the desired pointer. Since the first step is to generate a list of names and pointers, we are going to re-use some of our tools and create a new java program that will spit out the desired array. Since there are so many functions, we will want to do some special processing. We'll sort them in function name order so that later we can find them quickly:

/**
  mk_func_hdr.java

  vjj 03/14/98

  This java class uses the previously defined func_hdr class to obtain a list of
  all the functions in source code in a directory(ies). The list of functions is
  then sorted into alphabetical order by function name. The final array that is
  outputted is formatted in the following 'C' struct:

  struct gameFuncStruct
  {
      char *name;
      void (* func)();
  };

  The user will be required to cast the function pointer to the appropriate
  function prototype.

  The array will be called "globalGameFuncArray" and will be declared in place.
  A comment will follow each function declaration with the file and linenumber.
  */


import java.io.*;
import java.util.*;
//import vjj.*;         //eventually, we'll want to set up packages, etc.

class mk_func_hdrs
{
    Vector allFuncs_;
    funcdef funarray_[];
    int numFunctions_ = 0;
    
/*
  this inner class handles maintaining the information where the function was found
  */
    class funcdef
    {
        String retVal_;
        String name_;
        String parameterList_;
        String fileName_;
        int  lineNo_;

        public funcdef()
        {
            retVal_ = null;
            name_ = null;
            parameterList_ = null;
            fileName_ = null;
            lineNo_ = -1;
        }

        public funcdef(func_definition fd, String file)
        {
            retVal_ = fd.returnValue();
            name_ = fd.functionName();
            parameterList_ = fd.parameterList();
            fileName_ = file;
            lineNo_ = fd.lineNumber();
        }

        public void returnValue(String retval)
        {
            retVal_ = retval;
        }

        public void functionName(String name)
        {
            name_ = name;
        }

        public void parameterList(String parms)
        {
            parameterList_ = parms;
        }

        public void fileName(String file)
        {
            fileName_ = file;
        }

        public void lineNumber(int ln)
        {
            lineNo_ = ln;
        }

        public String returnValue()
        {
            return retVal_;
        }

        public String functionName()
        {
            return name_;
        }

        public String parameterList()
        {
            return parameterList_;
        }

        public String fileName()
        {
            return fileName_;
        }

        public int lineNumber()
        {
            return lineNo_;
        }
    
    }


      // a heapsort function - sorts chunks of index records
      //void heapsort (long count, DCWORD kwidth, DCWORD rwidth, char *cache)
    void heapsort (funcdef cache[])
    {
          //char *elt, *ivalue;
        int i, s, f;
          //int offset = rwidth-kwidth;
        int count = cache.length;
        funcdef  elt, ivalue;

          // for second thru end records
        for (i=1; i< count; i++)
        {
            elt = cache[i];
            s = i;
            f = (s-1)/2;
            while (s > 0 && cache[f].functionName().compareTo(elt.functionName()) < 0)
            {
                cache[s] = cache[f];
                s = f;
                f = (s-1)/2;
            }
            cache[s] =  elt;
        }

        for (i=count-1; i>0; i--)
        {
            ivalue = cache[i];
            cache[i] = cache[0];
            f = 0;
            if (i == 1)
                s = -1;
            else
                s = 1;

            if (i > 2 && cache[2].functionName().compareTo(cache[1].functionName()) > 0)
                s = 2;

            while (s >= 0 && ivalue.functionName().compareTo(cache[s].functionName()) < 0)
            {
                cache[f] = cache[s];
                f = s;
                s = 2 * f + 1;

                if (s+1 <= i-1 && cache[s].functionName().compareTo(cache[s+1].functionName()) < 0)
                    s = s + 1;
                if (s > i-1)
                    s = -1;
            }

            cache[f] = ivalue;
        }

    }

    public void outputLine(funcdef fd)
    {
        if (fd.returnValue().startsWith("static"))
        {
            numFunctions_ --;
            System.out.print("// ");
        }
        
        System.out.print('"'+fd.functionName()+'"'+',');
        System.out.print(" (void (*))"+fd.functionName()+',');
        System.out.print("   // ");
        System.out.print(fd.returnValue() + " ");
        System.out.print(fd.functionName());
        System.out.print(fd.parameterList());
        System.out.println("  "+fd.fileName()+":" +fd.lineNumber());
    }

    public mk_func_hdrs(String argv[])
    {
        file_funcs ff;
        func_definition fd;
        funcdef tep;
        
        allFuncs_ = new Vector();
        func_hdr fh = new func_hdr();
        fh.process("" , argv);
          //System.out.println("Got all function headers");

          //ok, we have a slight problem. We want to do a heapsort of the
          //elements. A heapsort is very fast and in sorts in place. We, however
          //do not have anything in place that can be sorted. We need to create
          //a vector and put all our info into it since right now it is
          //fragmented. Also, we will associate a file name with the function
          //so when we sort, everything will stay together.
        for(Enumeration e = fh.files_.elements(); e.hasMoreElements();)
        {
            ff =(file_funcs) e.nextElement();
              //System.out.println("File : "+ff.fileName());
            for(Enumeration f = ff.functions().elements(); f.hasMoreElements();)
            {
                fd = (func_definition)f.nextElement();

                if(fd!=null)
                {
                    tep = this.new funcdef(fd,ff.fileName());
                    allFuncs_.addElement(tep);
                }
                
            }
        }

          //sort all the functions that we have in alpha order
        funarray_ = new funcdef[allFuncs_.size()];
        allFuncs_.copyInto(funarray_);
        heapsort(funarray_);

    }

    public Vector getHdrs()
    {
        return allFuncs_;
    }
    
    public static void main (String argv[])
    {
        mk_func_hdrs mf;
        funcdef tep;
        int i;
        
        if (argv.length < 1){
            System.out.println("Must give at least one file or directory");
            return;
        }

          //actually do all the processing
        mf = new mk_func_hdrs(argv);
        
          //output section
        //System.out.println("#include \"allfuncs.h\"\n");
          //System.out.println("#include \"funcarry.h\"\n\n");
        
          //output the struct
        System.out.print("\n\nstruct FuncArrayStruct ");

          //output array preamble
        System.out.print("GlobalGameFunctionArray[] =\n{\n");

        for(i=0;i < mf.funarray_.length;i++)
        {
            mf.numFunctions_++;
            mf.outputLine(mf.funarray_[i]);
        }

          //output array closing
        System.out.print("\"\", NULL    //End of functions\n};");
        
        System.out.println("\n#define  FUNARRAYLEN   "+mf.numFunctions_ );

        
    }
    
}

Compile the mk_func_hdrs.java program and run it like this:

C:\Quake2\src\func_hdr> javac mk_func_hdrs.java

C:\Quake2\src\func_hdr> java mk_func_hdrs c:\quake2\src\game >funcarry.h

This program builds on the other one. Notice that it has its own entry point. The func_hdr module generates the list and this program uses the output of that program to create the .h file with all functions in it. Make sure that this new program file is in the same directory as the other java program file. Notice that you now have two files, one called allfuncs.h and one called funcarry.h. These files form the basis for the modification that we are going to do to the QuakeII source. Copy those two files to wherever you have the QuakeII source. Note that I broke with C programming tradition and placed defined data in the findarry.h file. I did this to make the files a little more managable.

We need to create the u_findfunc.c and u_findfunc.h files to allow us to search the function array. Since the functions are sorted by name, we are going to implement a binary search to find the function as quickly as possible:

First we define the header file, u_findfunc.h:

/*
  u_findfunc.h

  Defines the structures and functions that are used.
  */


struct FuncArrayStruct
{
    char *name;
    void    (*func)();
};

void (*FindGameFunction(char *t));

A little pointer fun with C. The FindGameFunction is a function that takes a character pointer (to the name of the function you are looking for) and returns a pointer to a function that returns void. The pointer to a function returning null is held in the func slot in the FuncArrayStruct.

With the header file explained, we need to give you the code to find the function:

/*
  u_findfunc.c

  vjj  03/16/98


  This file implements a binary search algorithm to rapidly find a function
  in the GlobalGameFunctionArray that is declared in the u_findfunc.h file.

  */

#include "g_local.h"
#include "u_findfunc.h"
#include "allfuncs.h"
#include "funcarry.h"  //this defines structures.

void (*FindGameFunction(char *t))
{
    int mid, hi, lo;
    int found;
    int compres;
    
    found = -1;
    lo = 0;
    hi = FUNARRAYLEN;
    mid = (hi + lo)/2;

    while(lo <= hi)
    {
        compres = strcmp(t,GlobalGameFunctionArray[mid].name);
        if(!compres)
        {
            found = mid;
            break;
        }
        else
        {
            if(compres < 0)
                hi = mid - 1;
            else
                lo = mid + 1;
        }
        mid = (hi + lo)/2;
    }

    if (found != -1)
        return GlobalGameFunctionArray[found].func;
    else
        return NULL;
}

Add this file to the gamex86.dll project so that the it gets built into the dll.

We need to make this function available to the user dll. Go into the u_loaddll.h file and add the following line to the user_dll_import_t structure, right below the InsertCommands line (line #41 or so):

    void (*(*FindFunction)(char *t));

This line means: A pointer called FindFunction to a function that takes a character pointer and returns a pointer to a function that returns void. Whew.

Next, we want to initialize the new structure member, FindFunction with a pointer to the FindGameFunction function. Look around line 150 and we find the function InitializeUserDLLs. Place the following line after where you are initializing the InsertCommands member:

    UserDLLImports.FindFunction = FindGameFunction;

Compile everything in the project and you should be ready. Well, not quite. The linker gives us a few errors about not finding a number of functions. This is an 'feature' in the way that my func_hdr and mk_func_hdrs programs work. I didn't make them evaluate conditional compile statements, you know, the #ifdef stuff. If you lookup the functions in the files where they exist, you see that they are #ifdef'd out. Just comment the functions out of the allfuncs.h file and the funcarry.h file. Don't forget to decrease the FUNARRAYLEN value by the number of functions that you commented out.

Everything should compile now.

I need to explain how to use the function lookup stuff. First, you need to declare a pointer to a function that you want to use. I recommend that you name it the same as the function that it represents. This way, you won't have to convert the function names in whatever mod you are working on, although it doesn't really matter. This function pointer must be declared as a function that has the same prototype as the function you want to use. This declaring of a pointer can be a bit of a trick but if you follow the examples for the other functions, you should be OK. Now, during the initialization of the user dll, you will call the FindFunction function with the name of each of the functions that you are looking for. Once you have the pointer, you can cast it to the appropriate function type and assign it to your declared pointer. You can then just use the function pointer like it was a normal function.

A little side discussion on architecture: I had to make a choice when I decided to follow this approach, that is, having this structure that contains all the functions and searching for the function to use. I'm not sure that it was the right choice either. The other approach to this problem would have been like id did and provide an array of functions similar to the game_import_t and then allow you to call those functions that way. One of the assumptions that I made was that you, the mod author, won't be calling a lot of these functions. Secondly, I made the assumption that there may be mods out there that will just provide functionality and that they will want to advertise their function availability to others. Third and last and perhaps most importantly, I thought that the mechanism for hijacking a function and making functions available should be separate. In Part IV of the tutorial, we'll go over what needs to be done to hijack any function and I'll present to you what I think are the reasonable functions to hijack, along with a standard mechanism for doing so.

In any case, if you wish to implement the other approach, copy the mk_func_hdr.java program and start modifying. You have the tools.

Of course, we still have a slight problem. Since you are including the id header files, a lot of those functions are already defined there as functions and not as pointers to functions. The compiler is going to complain about functions being redefined. We will need a version of the header file that does not include the functions. That way, we have total reliance on our pointers to access the functions.

Since we want to reuse the current files as much as possible and keep our changes to the minimum, we are going to use a couple of C compiler directives. Earlier, they had given us problems with our Java function scanner, func_hdr. But we can use it now to our advantage.

What we are going to do is create a compiler symbol that we can use to include the functions that are defined in the header file. We'll call the directive USER_EXCLUDE_FUNCTIONS. So if you want to exclude the declarations of the functions from the header files, you'll put:

#define USER_EXCLUDE_FUNCTIONS 1

before you #include the id header file. The most important header file that is used is g_local.h. Open that file up with your favorite editor and look for the function declarations. Somewhere around line 500, you'll see where some function declarations are starting. On the line before, place the following line:

#ifndef USER_EXCLUDE_FUNCTIONS 

That line is telling the C preprocessor that if the USER_EXCLUDE_FUNCTION symbol has not been defined, go ahead and include the following code section. We need to find the end of the function declarations so we can close off the directive.

Now, if we page down a times, we see that the damage flags are being defined in our block. No problem. Just before the damage flags, place the following line:

#endif

Surround the following section of functions the same way, with a #ifndef/#endif pair. That takes care of the g_local.h header file. However, that file in turn, is pulling other files in. We need to process the q_shared.h and the game.h files in a similar fashion. The q_shared.h file is a little tricky, but the game.h file just has one function, at the end. If you look at the other header files, you will see that they are just defining a bunch of symbols to be able to specify the animation frames.

Let's talk about overriding the functions now. In theory, we want the ability to override everything. In practice, not really. Our task is to identify which functions would be useful to override and provide a mechanism to do so. Furthermore, a number of functions in the files are private mechanisms for objects and we don't want to override them, at least without due consideration. Not only that but there is a cleaner, easier way of doing this. This is especially true for item and monster behaviors, for instance.

We probably don't want these 'private' functions in the function array that we generated earlier. However, we'll leave them in there for right now. When we go over the item and monster interfaces in Part IV, we will see how to use these functions and we'll probably want to call some of these functions.

Anyway, there is a set of functions that we do want to override and it is fairly easy to accomplish and gives us a lot of 'bang for the buck'. We are talking about the functions found in the game_export_t structure, of course.

What id did with the current game interface is to create a number of pointers that hold the address of the functions that the game engine is supposed to call in order to get something to happen in the game dll. For instance, the G_RunFrame function is responsible for a game "turn". If you override this function, you in essence can create a new game.

This is very similar to what we did with our function array. With the game_export_t structure (and the game_import_t structure incidently), we are passing a pointer to the memory location which contains the function pointers. If we change that pointer, we change the function that the game engine calls. But there is a catch; the game engine must reference those functions through a pointer and not have created a copy of the game_export_t structure that the gamex86.dll passed back. Using this knowledge, a little experimentation and the Microsoft debugger, we see that this indeed is the case. So we should be able to replace any one of those functions at will, right?

Well, not quite. You see, while the game engine has to call these functions through a function pointer, the gamex86.dll doesn't. After all, it is supplying the function, so it goes ahead and calls the functions directly. So we need to check out which gamex86.dll functions call the functions that we already put into the game_export_t structure.

Using the object browser of your choice, you can see the function call tree and who is calling the function. We see that the InitGame function is called by the ReadLevel and WriteLevel functions in g_save.c, and the ClientUserinfoChanged function is called by the ClientConnect and PutClientInServer functions, both in p_clients.c. If we want to be able to override these functions, we will need to go into these files and fix the references so that it is using the pointer in the game_export_t global variable.

So using your favorite editor, open the p_clients.c file and look for the ClientConnect function around line 850. There, a few lines into the function, you see a line that looks like this:

	ClientUserinfoChanged (ent, userinfo);

Change it to say:

	globals.ClientUserinfoChanged (ent, userinfo);

Now look for the PutClientInServer function near line 593. Going through the function, near line 620, you'll see another line like we fixed in the ClientConnect function. Change that also so it references the globals structure.

Next open the g_save.c file and look at line 641. There you see that the InitGame function is being used as a variable reference. We don't want to change this reference or the level information for the game will no longer be readable, not at least without changing the value in the WriteLevel function. Since this is an innocuous use of this function, we will say that it's OK to leave this one alone.

Futhermore, upon a bit of experimentation trying to hijack this function, and all the functions for good measure, we find that the InitGame function refuses to cooperate. We also find that the ShutdownGame function also doesn't cooperate. I am not sure why that would be the case for those functions. Apparently, the game engine caches those two function pointers. Perhaps in a future version of the game engine, that could be changed. Mr. Carmack? Mr. Cash? Mr. Hook?

To recap, you can hijack any of the functions in the global structure except the InitGame and the Shutdown functions. Which is OK because our dlls are having their initialization and shutdown functions being called appropriately. Being able to take over these functions on the fly lets you significantly change the game to whatever you wish. If you wish to hijack even more functions, you can and I'll explain what needs to be done in the next part of the tutorial. However, most of the functions that you'll want to hijack deal with setting up new or modified behaviors for items or monsters and there are much easier ways of accomplishing this without taking over a function pointer.

Now, we'll pull all this together and write the user dll template. This is the framework that will enable the user to drop their code in and have a reasonable chance of it working with all the other mods.

/*
  dlltemplate.c

  vjj  03/03/98

  This template was created as a guideline for what you need to provide to
  enable your mod to work with the multiple dll modification.
*/


#define USER_EXCLUDE_FUNCTIONS 1

#include "g_local.h"
#include "g_cmds.h"
#include "u_loaddll.h"

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

/*
  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 *ptrgi;
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));
/* commented out - future functionality
static void (*InsertItem)();
static void (*InsertMonster)();    
static void (*InsertClient)();     // for bots?
*/

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

/*remove if not using*/
void (*OldInitGame) (void);
void (*OldShutdown) (void);
void (*OldSpawnEntities) (char *mapname, char *entities, char *spawnpoint);
void (*OldWriteGame) (char *filename);
void (*OldReadGame) (char *filename);
void (*OldWriteLevel) (char *filename);
void (*OldReadLevel) (char *filename);
void (*OldClientThink) (edict_t *ent, usercmd_t *cmd);
qboolean (*OldClientConnect) (edict_t *ent, char *userinfo, qboolean loadgame);
void (*OldClientUserinfoChanged) (edict_t *ent, char *userinfo);
void (*OldClientDisconnect) (edict_t *ent);
void (*OldClientBegin) (edict_t *ent, qboolean loadgame);
void (*OldClientCommand) (edict_t *ent);
void (*OldRunEntity) (edict_t *ent);
void (*OldG_RunFrame) (void);

//declare pointers to functions that you'll need
void (*Com_Printf)(char *msg, ...);   //pretty much always need this one


/* Define the new functions to chain */
/* if you need to create new game behavior, you need to put the
   functionality here. Otherwise you can just delete these. These
   are here to help you, not because you need them
*/
void
NewSpawnEntities (char *mapname, char *entities, char *spawnpoint)
{
    ptrgi->dprintf("Calling new SpawnEntities\n");
    OldSpawnEntities(mapname, entities, spawnpoint);
}

void
NewClientThink (edict_t *ent, usercmd_t *cmd)
{
	ptrgi->dprintf("Calling new ClientThink\n");
	OldClientThink(ent,cmd);
}

qboolean
NewClientConnect (edict_t *ent, char *userinfo, qboolean loadgame)
{
    ptrgi->dprintf("Calling new ClientConnect\n");
    return OldClientConnect(ent,userinfo,loadgame);
}

void
NewClientUserinfoChanged (edict_t *ent, char *userinfo)
{
    ptrgi->dprintf("Calling new ClientUserinfoChanged\n");
    OldClientUserinfoChanged(ent,userinfo);
}

void
NewClientDisconnect (edict_t *ent)
{
    ptrgi->dprintf("Calling NewClientDisconnect\n");
    OldClientDisconnect(ent);
}

void
NewClientBegin (edict_t *ent, qboolean loadgame)
{
    ptrgi->dprintf("Calling NewClientBegin\n");
    OldClientBegin(ent, loadgame);
    
}

void
NewClientCommand (edict_t *ent)
{
    ptrgi->dprintf("Calling NewClientCommand\n");
    OldClientCommand(ent);
    
}

void
NewRunEntity (edict_t *ent)
{
    ptrgi->dprintf("Calling NewRunEntity\n");
    OldRunEntity(ent);
}

void
NewWriteGame (char *filename)
{
    ptrgi->dprintf("Calling NewWriteGame\n");
    OldWriteGame(filename);
}

void
NewReadGame (char *filename)
{
    ptrgi->dprintf("Calling NewReadGame\n");
    OldReadGame(filename);
}

void
NewWriteLevel (char *filename)
{
    ptrgi->dprintf("Calling NewWriteLevel\n");
    OldWriteLevel(filename);
}

void
NewReadLevel (char *filename)
{
    ptrgi->dprintf("Calling NewReadLevel\n");
    OldReadLevel(filename);
}

void
NewInitGame (void)
{
    ptrgi->dprintf("Calling NewInitGame\n");
    OldInitGame();
}

void
NewG_RunFrame (void)
{
    OldG_RunFrame();
    ptrgi->dprintf("Calling NewG_RunFrame\n");
}

void
NewShutdown (void)
{
    ptrgi->dprintf("Calling NewShutdown\n");
    OldShutdown();
}

/* example of declaring the function array for the InsertCommand */
/* imaginary commands, each taking one parameter
struct g_cmds_t playerConsoleCmds[2] =
{
	"turnon", 1, Cmd_turnon_f,
	"turnoff", 1, Cmd_turnoff_f
};
*/

/*
  user code goes here
  */


/*
  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()
{
	if (AlreadyInit) return;

	AlreadyInit = 1;
	ptrgi->dprintf("In UserDLLInit for DLL %s\n",DLL_NAME);

      //this is where you would set up the commands
      //PlayerInsertCommands(CommandStruct,numCmds,DLL_NAME);

      //this is where you would acquire any needed function pointers
      Com_Printf = (void (*)(char *msg, ...)) PlayerFindFunction("Com_Printf");
    
      //this is where you would chain any functions that you need
      //again, this is here to help. If you don't need them, delete them.
	OldInitGame = ptrGlobals->Init;
	ptrGlobals->Init = NewInitGame;
    
	//OldShutdown = ptrGlobals->Shutdown;
	//ptrGlobals->Shutdown = NewShutdown;
    
	OldSpawnEntities = ptrGlobals->SpawnEntities;
	ptrGlobals->SpawnEntities = NewSpawnEntities;
    
	OldWriteGame = ptrGlobals->WriteGame;
	ptrGlobals->WriteGame = NewWriteGame;
    
	OldReadGame = ptrGlobals->ReadGame; 
	ptrGlobals->ReadGame = NewReadGame;
    
	OldWriteLevel = ptrGlobals->WriteLevel; 
	ptrGlobals->WriteLevel = NewWriteLevel;
    
	OldReadLevel = ptrGlobals->ReadLevel; 
	ptrGlobals->ReadLevel = NewReadLevel;
    
	OldClientThink = ptrGlobals->ClientThink; 
	ptrGlobals->ClientThink = NewClientThink;
    
	OldClientConnect = ptrGlobals->ClientConnect; 
	ptrGlobals->ClientConnect = NewClientConnect;
    
	OldClientUserinfoChanged = ptrGlobals->ClientUserinfoChanged; 
	ptrGlobals->ClientUserinfoChanged = NewClientUserinfoChanged;
    
	OldClientDisconnect = ptrGlobals->ClientDisconnect; 
	ptrGlobals->ClientDisconnect = NewClientDisconnect;
    
	OldClientBegin = ptrGlobals->ClientBegin; 
	ptrGlobals->ClientBegin = NewClientBegin;
    
	OldClientCommand = ptrGlobals->ClientCommand; 
	ptrGlobals->ClientCommand = NewClientCommand;
    
	//OldG_RunFrame = ptrGlobals->RunFrame; 
	//ptrGlobals->RunFrame = NewG_RunFrame;
   

}

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

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

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

/* 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;
    ptrgi = udit.gi;
    ptrGlobals = udit.globals;
    ptrLevel = udit.level;
    ptrGame = udit.game;
    
    ptrgi->dprintf("Inside GetAPI for %s\n",DLL_NAME);
    return userdll_export;
}

A lot of this looks familiar. It has been done in the last two tutorials. If you looked carefully, you saw that the two tutorials shared a great deal of functionality and code. The framework gives you, the mod programmer a starting point and a common way of interfacing with the gamex86.dll so that your mod can load and run with other mods.

We have a little bit of cleanup to do from Part II. Apparently, I forgot to put into the gamex86.dll source the calls to the routines provided by the user extensions for when a level is entered and exited. Going into p_client.c, we need to find a function that gets called every time that the user is enters a level. The documentation and the code seem to suggest that ClientBegin function is our candidate. However, we can't seem to find a corresponding function for when the player exits the level in this file. Looking in other files, we find in the g_main.c file two functions that seem to fit that bill, one called ExitLevel, the other EndDMLevel. After utting a gi.dprintf statement into each one, we see that the ExitLevel is always called and EndDMLevel only when the DM end conditions are met, before actually exiting the level.

Also, while going through this exercise, trying to figure out which functions to call, I realized that we probably also want to call user defined functions when the player respawns and when the player dies. Well, if you look carefully at the dlltemplate.c, you will find that we have already added functions that you can use in those cases. And we changed the userdll_export_t structure found in u_loaddll.h to look like this:

typedef struct
{
    int apiversion;
    char creator[32];

    void (*UserDLLDigest)(char MsgDigest[64]);

    void (*UserDLLInit)(void);
    void (*UserDLLShutdown)(void);
    void (*UserDLLStartLevel)(edict_t *ent);
    void (*UserDLLLeaveLevel)(void);
    void (*UserDLLPlayerSpawns)(edict_t *self);
    void (*UserDLLPlayerDies)(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point);
   
} userdll_export_t;

We want to create a number of functions to make our programming task easier. The functions should run through the list of user dlls and call the appropriate functions with the right parameters through the correct pointer. So go into the u_loaddll.c and add the following functions:

void
LevelStartUserDLLs(edict_t *ent)
{
	struct userdll_list_node *unode;

	unode = GlobalUserDLLList;
	while(unode)
	{
        unode->dll_funcs.UserDLLStartLevel(ent);
		unode = unode->next;
	}
}

void
LevelExitUserDLLs()
{
     struct userdll_list_node *unode;

     unode = GlobalUserDLLList;
     while(unode)
     {
        unode->dll_funcs.UserDLLLeaveLevel();
	unode = unode->next;
     }
}

void
PlayerSpawnUserDLLs(edict_t *ent)
{
	struct userdll_list_node *unode;

	unode = GlobalUserDLLList;
	while(unode)
	{
        unode->dll_funcs.UserDLLPlayerSpawns(ent);
		unode = unode->next;
	}
}

void
PlayerDiesUserDLLs(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point)
{
	struct userdll_list_node *unode;

	unode = GlobalUserDLLList;
	while(unode)
	{
        unode->dll_funcs.UserDLLPlayerDies (self, inflictor, attacker, damage, point);
        
		unode = unode->next;
	}
}

Don't forget to add the prototypes of the functions that you just created to the u_loaddll.h file.

Getting back to the task of integrating our functions into the game, open the p_client.c file and search for the ClientBegin function. We'll make LevelStartUserDLLs the last thing that gets called before the function returns. That way, we can 'fix' things up before the game takes over. This means that we have to add it in two places, in the deathmatch if statement and just after the ClientEndServerFrame function at the end, around lines 743 and 789, respectively. Don't forget to pass the ent parameter!

Now go find the function player_die. At the end of the function, around line 225, we want to put a call to our PlayerDiesUserDLLs function. Pass all the parameters in. Next look for the PutClientInServer function. This function is responsible for setting up the client to go into a game. We want to go ahead and let everything get set up first before we muck with it. So go to the end of the function, around line 697 and put the call to the PlayerSpawnUserDLLs(ent).

To finish in the p_client.c file, go to line 5, after the rest of the includes and add the following:

#include "u_loaddll.h"

Now open the g_main.c, find the ExitLevel function and go to the end. You should be around line 284 and insert the call to the LevelExitUserDLLs(). Don't forget to go to the top and insert:

#include "u_loaddll.h"

on line 3, after the #include "g_local.h". We are now done adding hooks to the Quake II code to call our functions at the appropriate time.

We are almost done with the cleanup. In the last tutorial, we said that we would have a way of providing name spaces to the command sets. This means that we have to change the current FindCommand function to handle name spaces. Pull up g_cmds.c and change the FindCommand function to the following:

struct g_cmds_t *
FindCommand(char *cmd)
{
    struct cmd_list_t *sets;
    struct g_cmds_t *cmds;
    int i, found;
    char nmspace[32], *ptr;

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

    sets = GlobalCommandList;
    while (sets)
    {
	if(found)
	    if(strcmp(nmspace,sets->source))
	    {
		sets = sets->next;
		continue;
	    }

        cmds = sets->commands;

        for (i=0;inumCmds;i++)
            if(Q_stricmp(cmd,cmds[i].command) == 0)
                return &(cmds[i]);
        sets = sets->next;
    }
    return NULL;
}

The modification parses the command passed to it and sees if there is a period in the name. Anything before the period is considered the namespace and the function spins through the command sets looking for the match. Once the match is found, it runs through the commands, looking for the function. If no command set name was specified, the function automatically goes into each command set looking for the function specified.

Similar to this is another possible enhancement that you could make to the current code base. The function array that we created using the mk_func_hdr program could be managed the same way that the console commands are handled in Part I of the tutorial. This could lead to dll's that just provide functional components to other dll's and they would interface anonymously. It would be up to the dll as to what functions to register.

Since all these functions would then be called through a coerced pointer, there would be no error checking for the right types of parameters and there is no way to know what the code is supposed to do. The creator of the dll that is registering a function will have to create documentation for the code that is being registered so that you, the mod designer, can call it properly.

However, that modification I'll leave as an exercise to the student. ;-)

Let's think about what we have accomplished. We have created a Java program that will run through all of id's gamex86.dll code and generate an array of function pointer to all the functions that exist, and we have a way of finding any of the functions and obtaining a pointer. We have updated the interface to the user dll to allow us to use any existing function insert our code in various key components. We have also created a framework that lets us to easily let us hijack a number of critical game functions that are used by the game engine. Finally, we did a bit of clean up to get the code behaving more consistently.

In the next part of the tutorial, we'll go over the ins and outs of hijacking any gamex86.dll function and why that is only necessary for a few functions. We will also work through how to add and modify items and monsters at will with a whole bunch of functions.

I know, this has been a monster tutorial. The next tutorial won't be so hard. We just had to get over the hump and things will get easier from now on. By the way, has anyone been working with the ctf code? It would be interesting to see if CTF could be converted into a mod that follows these guidelines.

Until next time, Code and Enjoy!

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