The arts of Q2 DLL coding part 1

- Creating a development environment -



1. Introduction

So, they finally decided to release it, and just before my exams too! Well, well I know what I prioritize ;-)

With this tutorial I try to let you gain more insight in the secrets of the Quake2 dll-code. I must warn you though, I will write in an exploring form which might seem a little odd at first and maybe a little structureless. But then again, I know I like this form of learning most in the textbooks I've encountered in my own studies.

If you haven't already downloaded it, I advise you to do so. It's very available at ftp://ftp.idsoftware.com/idstuff/quake2/source
The version date may vary, so I won't write it here. It's really not many files in there, and if you can't find it on your own I think you should go back to playing ;-)

Now we've heard that it's a dll-code, and unfortunately we don't know what the heck a dll is. Well, I'll tell you. DLL stands for Dynamic Link Library and it's simplified an executable which isn't intended to be run alone, but instead from other programs. For example if Clark Kent makes a fabulous new routine for playing sounds he puts it in a dll file, which then can be used for playing sound from other peoples programs. Ok, so there is a little more to it than that, but incidentally it's all YOU need to know for making your own q2 dll.

Now when you run Quake2 it looks for a dll in the baseq2 directory called gamex86.dll. It's the standard precompiled id software dll. I advise you to make a backup of it now since it might as well be mangled in the process. Just do a simple copy c:\quake2\baseq2\gamex86.dll c:\quake2\baseq2\orig.dll or something like that. Of course, the best place to put custom dll files is a new directory c:\quake2\mygame or something like that. Then Quake2 will run your brand new dll when you type quake2 +set game mygame on the command line, but that is for later use. One thing at a time.

Ok, you are sitting here with a nice zip file, reputably containing a lot of source code and utilities, even java such (Very nice initiative id). What should you do next? Right! You got it.. You should of course unzip it! After you've done so you begin browsing the source tree, filled with anticipation. The thing we are looking for is found in the .\game directory. You type 'dir' and see that there sure are a lot of files there. No less than 1MB of pure c code! You should realize that this is a lot of code. By now you realize that you won't master this code overnight. One million characters contain a LOT of information. If you haven't been frightened off yet well start with the basics. Compiling it.


2. Choosing compiler

id software wrote the game using Microsoft Visual C++ 5 as far as I know. This might seem like the best choice for developing your own dlls with at the first glance. Unfortunately things are not always as they seem. There are some major points which makes us choose another compiler.

This limits our choice down a bit. We now know that we shouldn't use any commercial compilers, but rather a freeware one. So what freeware compilers are available? As far as I know there are two worth looking at:

Now.. Both these have their pros and cons, and since I've tested both extensively I'll tell you directly so you won't waste any time like I did :-)
The Cygnus compiler is, as it's name implies, a port of the unix GNU cc. But, that's exactly what we want! Definitely a pro.. Unfortunately, I must admit, I haven't been able to build a workable dll with that compiler though hours of work. Quake2 simply rejects it. Either I'm doing anything wrong (Though I HAVE checked my methods many times) or it's simply so that cygwin-gcc produces a slightly non standard dll form, unreadable by Quake2, in any case as soon as I or anyone else gets it working I will change my choice of compiler to that. In the meanwhile we will have to do with lcc. It has one pro, it creates usable DLL's. ALMOST! There is a small bug in the linker which writes the name of the dll entrypoint into the dll. (The entrypoint is the point were the execution in a dll starts). It tends to add an underscore before the function name, and this is incorrect! Instead of _GetGameAPI it should only say GetGameAPI. Now, there is a way around this. With a simple c-hack we can blank out the _ at the right place directly in the generated dll. I'll get back to this problem more in detail later, so you shouldn't bother too much. I just put this here since it bothered most of us freeware pioneers. You can't imagine how frustrating it was to watch all these MSVC++ owners make their own dlls! Back to the subject. Lcc is GNU compatible enough to be able to use. The make utility is also compatible to the GNU make utility, and this is very good. It means we can use the same Makefile for both unix and windows with just some minor additions. There is just ONE thing which I wish could have been different, lcc generates .obj files rather than the standard unix .o files for objects. This can be fixed by some more rules in the Makefile if we ever want to compile for unix. But I assure you that I will fix that when that time comes.

Our choice is therefore LCC. It's not very large and can be downloaded at
http://www.remcomp.com/lcc-win32

While you are downloading I think I'll be taking a cup of coffee. Shake the monitor when your finished.


3. Getting started compiling

Now we want to compile the code and see if it works without major compiler breakdown. There is one issue first I'll have to address first. I mentioned it briefly in the previous chapter, the erroneous linking of lcclnk. I'll try to describe the problem from an easy-to-understand perspective. In a dll file there is a huge section with all the public function names with a perpended underscore. Like this:
.. snip ..
_SP_item_health_mega _InitItems _SetItemNames
.. snip ..
That was an example cut directly out of the dll-file. There it is correct that the function names should include an _. We have another section of the dll-file to, looking something like this:

069200  00 00 00 00 C6 42 94 34-00 00 00 00 28 E0 06 00      B4    (
069210  01 00 00 00 01 00 00 00-01 00 00 00 34 E0 06 00           4
069220  38 E0 06 00 3C E0 06 00-67 61 6D 65 78 38 36 2E  8 < gamex86.
069230  64 6C 6C 00 4F C0 00 00-40 E0 06 00 00 00 00 00  dll O+  @
069240  47 65 74 47 61 6D 65 41-50 49 00 00 00 00 00 00  GetGameAPI
Note that this is a correct dll file. An incorrect dll (one generated by lcc) follows:
069200  00 00 00 00 D7 54 94 34-00 00 00 00 28 E0 06 00      T4    (
069210  01 00 00 00 01 00 00 00-01 00 00 00 34 E0 06 00           4
069220  38 E0 06 00 3C E0 06 00-67 61 6D 65 78 38 36 2E  8 < gamex86.
069230  64 6C 6C 00 4F C0 00 00-40 E0 06 00 00 00 00 00  dll O+  @
069240  5F 47 65 74 47 61 6D 65-41 50 49 00 00 00 00 00  _GetGameAPI

Note that there also is an underscore in front of the function GetGameAPI. Unfortunately that makes Quake2 look for an entry point called __GetGameAPI, and will thus fail with the nice little gray box "Error during initialization".

Of course, there is a simple, but somewhat ugly way around this by making a simple c-hack which just hexedits the string in place. This c-hack was posted by a guy called Mungo on the www.quake2.com/dll wwwboard. Unfortunately I couldn't find his email anywhere, but I hereby credit him for his hack. I hope this won't come as a shock :-)

== dllhack.c == snip == 8< == BEGIN ==

// dllhack.c
// ---------------------------
#include <stdio.h>

void main(int argc, char **argv) {
	FILE *f = fopen("gamex86.dll", "r+b");
	char *buf = (char *)malloc(2048);
	int i;

	fseek(f, -2048, SEEK_END);
	fread(buf, 1, 2048, f);

	i = 2048;
	while(--i) {
		if(buf[i] == '_') {
			if (!strcmp("_GetGameAPI", &buf[i])) {
				printf("Found _GetGameAPI\n");
				strcpy(&buf[i], "GetGameAPI");
				break;
			}
		}
	}
	fseek(f, -2048, SEEK_END);
	fwrite(buf, 1, 2048, f);
	fclose(f);
}

== dllhack.c == snip == 8< == END ==
Just cut'n'paste it to a file called dllhack.c and put it in the same directory as the dll-source. As you can see it find the last occurance of _GetGameAPI and copies the string GetGameAPI directly over it. Won't that make it look like this in the dll-file, the observant says?
GetGameAPII\0\0\0...
No... The string "GetGameAPI" is nullterminated so it really says "GetGameAPI\0", thus that last I will disappear.

Now, when we've fixed that difficult to understand, but easy to fix stuff we move on. What we need now is a Makefile and since I don't expect you do be well educated in the mysteries of the GNU Makefile system here is one I prepared earlier:

== Makefile == snip == 8< == BEGIN ==
# Makefile for gamex86.dll
CC=lcc
CFLAGS=-DC_ONLY -Wall -O2
OBJS= g_ai.obj g_cmds.obj g_combat.obj g_func.obj g_items.obj g_main.obj \
g_misc.obj g_monster.obj g_phys.obj g_save.obj g_spawn.obj g_target.obj \
g_trigger.obj g_turret.obj g_utils.obj g_weapon.obj m_actor.obj \
m_berserk.obj m_boss2.obj m_boss3.obj m_boss31.obj m_boss32.obj \
m_brain.obj m_chick.obj m_flash.obj m_flipper.obj m_float.obj \
m_flyer.obj m_gladiator.obj m_gunner.obj m_hover.obj m_infantry.obj \
m_insane.obj m_medic.obj m_move.obj m_mutant.obj m_parasite.obj \
m_soldier.obj m_supertank.obj m_tank.obj p_client.obj p_hud.obj \
p_trail.obj p_view.obj p_weapon.obj q_shared.obj \

all:	gamex86.dll

dllhack:	dllhack.exe
	
dllhack.exe:	dllhack.obj
	lcclnk dllhack.obj -o dllhack.exe
	rm dllhack.obj
	# Remove else lcclnk will try to incorporate it into gamex86.dll 
	
gamex86.dll:	$(OBJS)
	lcclnk -dll -entry GetGameAPI *.obj game.def -o gamex86.dll
	dllhack

clean:
	rm *.obj *.dll

%.obj: %.c
	$(CC) $(CFLAGS) $<

== Makefile == snip == 8< == END ==

Note that this is a GNU Makefile, and if you edit it with dos edit or suchlike you WILL mangle it. The reason is that it contains a lot of tabs which MUST remain where they are. If you get strange errors it's because you have managed to break the tabs down into spaces or something like that. Be careful. And please don't mail me about that the Makefile doesn't work, please :-) It does.

Now, first we need to make the little dllhack utility, so just type:
make dllhack
and then we are ready to compile our own dll!
Slow down a bit the observant says, isn't it unnecessary that you must first type make dllhack and then just make for making the dll-code. Couldn't you have checked if dllhack was already made? Unfortunately not in this case, I'll have to answer. The problem is that lcclnk seems to have a limited parameter buffer, VERY stupid! We'll have to link with lcclnk *.obj instead of lcclnk $(OBJS). $(OBJS) would then substitute into that large line of obj-file names. lcclnk would cut off the last quarter or so if we did that. Bad for us. If we had put the dllhack making together with the dll file making we would have linked the dllhack.obj file into the dll file. Of course that would generate an error and stop the linking process. The second alternative is to delete the file dllhack.obj after making, which it currently does, but if it were executed togheter with the actual dll-making the dllhack utility would be remade each and every time we typed make. Well.. I'm not trying to teach Makefile editing here, but I just wanted you to get the big picture, and I hope you got it, even if my explanation perhaps was a bit difficult to follow if you didn't know anything about shell scripting and makefileing.

So, now I've scared of the most feeble minded, and for those of you who remain I recommend that you type 'make' followed with a press at enter.

It will take a time to compile, so in the meantime I though I would explain one detail which worried some people when the tried to compile the dll code. As you can see the compile lines look like this:

lcc -DC_ONLY -Wall -O2 q_shared.c

This means we compile with lcc, using a two pass optimization (-O2), we want it to generate all possible warnings (-Wall) (Yes, there is quite a few small formal errors in the code, and two bigger portability errors, which I might tell you more about if I ever get to use the GCC instead). The third option is (-DC_ONLY), which means define the constant C_ONLY. Now, why on earth would we do that? Of course it's only c, anyone can see that, all files end with .c or .h. Strange.. Well, since I know the answer I advise you to take a look at q_shared.c. There is something interesting inside. q_shared contains some functions which I believe are shared between the dll code, and the actual game engine. Since the game engine believes in speed there are some optimizations and choices in compilation. One of them is at line 245 (or around there). There is a line that says:

#if defined _M_IX86 && !defined C_ONLY
#pragma warning (disable:4035)
__declspec( naked ) long Q_ftol( float f )
{
   static int tmp;
   __asm fld dword ptr [esp+4]
   __asm fistp tmp
   __asm mov eax, tmp
   __asm ret
}

Below this are some other interesting stuff, but I think this is enough to get to the point. As you can see we will include assembler code into the compilation process if C_ONLY is NOT declared. This would work fine if we'd been using MSVC++, because the inline assembler syntax is compatible with that compiler. This will problably not work with most other compilers, so therefore we define C_ONLY and a maybe little slower c coded substitute is compiled instead. We'll have to live with that.

If you are lucky you will have a ready compiled dll by now. Check your dos/command prompt. If the last lines look like this you are lucky:

   lcclnk -dll -entry GetGameAPI *.obj game.def -o gamex86.dll
   dllhack
Found _GetGameAPI
Time: wx.yz seconds

this means it linked and that dllhack found it's target. If you are not lucky check your Makefile. If you are sure it's nothing wrong with your makefile, then mail me, explaning all details in a RESONABLY SHORT letter. No sending of giant code chunks or suchlike.

Anyway, if you succeded you will want to try to run this dll, and this can be done in two ways.

I'd really recommend the second variant.

Ah.. It ran but you didn't see any difference? How will you know if it worked? Easy, we change the code in some obvious point. I have a suggestion. Find the function ShutdownGame around line 75 in g_main.c It looks like this:

=================
GetGameAPI

Returns a pointer to the structure with all entry points
and global variables
=================
*/
void ShutdownGame (void)
{
   gi.dprintf ("==== ShutdownGame ====\n");

   gi.FreeTags (TAG_LEVEL);
   gi.FreeTags (TAG_GAME);
}

Just change the dprintf to say:
gi.dprintf ("==== ShutdownShame ====\n");
or something other notable (After all, it is a shame to quit such a fine product ;-).

Now just reissue make and this time it only has to recompile g_main.c before linking. Smart utility, that make.

Put it in quake2\mygame and run Quake2 with: quake2 +set game mygame When it starts directly bring up the console. Can you see it? I guess so. Congrats, you've made your first partial conversion ;-)


4. Final words

Well, I'm growing tired, and I've covered about everthing I wanted in my first tutorial (God I hate that word. Everyone will call it that, 'The Q2 DLL coding tutorial', bah.. I'll go back to the top and change the name :-), except perhaps something more about the DLL entry point, but I think I'll get back to that in the next part. Now, this was only an introductory part but I hope you found it enjoyable. And, as you may know, the nightmare always continues in part II (Vorhees words of wisdom) so I hope you'll stay tuned for there is a lot more to come. We are merely scratching on the surface of this subject.


// Magnus Landqvist a.k.a. Tar in Quake and on IRC
Sun Dec 14 23:56:22  1997