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.
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.
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 ãBö4 (Ó 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 GetGameAPINote 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 ÎTö4 (Ó 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?
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 ;-)
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.
Sun Dec 14 23:56:22 1997