Welcome back two the second part of the journey through the innermost secrets of the Quake II DLL structure. Last time we learned how to set up a makefile and compile the DLL, really not looking at the code at all. You are of course eager to know how it actually works together with the engine, and that's exactly what we are going to try to sort out today. Before we start though, you must be aware of the difficulty we are facing here. This is not a simple sequential program, which runs from the start point to the end point, and that's the end of it, but a complex program with many different execution threads (without having parallel processes). The main difficulty lies, not within understanding the atoms of the code, each and every function is not very complicated when looked at in small portions. No, it's the whole, and how everything interacts that is the challenge we face.
To be able to understand the details we must learn how the engine and DLL interacts, what functions are called by the engine in the DLL every second, and in what order. We must also learn what the DLL does to make the engine run the game as we want it. These are no easy tasks, but since we are stubborn and are not easily scared we will eventually learn!
The entire game is based on a server/client kind of hierarchy. First off, in multiplayer this is
pretty straightforward. One computer runs the server, which might either be a dedicated
server (maybe the easiest one to understand) but it could also be a listen server, in which
one player runs both a server and a client at the same time. Secondly, whenever we run a
singleplayer game we also run it as a server/client game. It works much like a listen server,
we run both server and client at the same time. You may now ask yourself why we need a server
in singleplayer, and the answer I give is that the server does not only do communication
between clients, it actually does EVERYTHING, the client is just a dumb, empty shell which
displays exactly the entities the server tells it to.
You might have guessed by now that if this is the case, then the DLL code only needs to be
on the server, and you are right!
With this basic hierarchical structure in mind we can begin looking at the code. Now we know
approximately how it's supposed to look.
If you pick up a file listing over the source directory you see that there are a lot of files, either starting with g_, m_, p_ or q_. These prefixes naturally means something. The g_* files are the files containing the actual game code, the m_* files contains the monster entities, with all their animations and ai, the p_* files contain the information about the special client entities, the player, and the few q_* files (there are two of them, q_shared.h and q_shared.c) are taken from the engine source. They contain shared functions which could be useful for portability reasons, like string comparisons with case insensitivity.
Now, where will we start? There are sure a lot of files that looks interesting, but we will begin looking at the place where execution starts, the DLL entry point which I mentioned in the previous tutorial. This is the natural place to look first to get a first clue about how things work.
The entry point can be found in g_main.c and is declared as:
game_export_t *GetGameAPI (game_import_t *import)
This means that that it receives a pointer to something called game_import_t, and returns a pointer to something similar, game_export_t. Now, this could be almost anything. The _t at the end indicates that it is a typedef (which it of course must be), and this is a standard way of naming which id software use in their code. To understand what goes in, and what comes out from the entry point we should find these typedefs in one of the header files. Now, if you are lucky enough to have the GNU grep utility installed you can easily find all files a certain string appears in. If you do not have grep installed, I recommend you to get it at:
ftp://ftp.simtel.net/pub/simtelnet/gnu/djgpp/v2gnu/grep21b.zip
The typedef is presumably defined in one of the header files, so we type
grep -n game_import_t *.h
and get the result:
g_local.h:412:extern game_import_t gi; game.h:150:} game_import_t; game.h:198:game_export_t *GetGameAPI (game_import_t *import);
Now we see that game_import is declared in game.h and external declared in g_local.h. That
means that we must look at game.h, the external declaration is just for a variable gi, and
not of interest for us right now.
The numbers after the first colon are the lines in game.h where the text was found.
Unfortunately these numbers probably do not match the line numbers in your game.h, since I've
been poking around in the code already, removing empty lines and adding things.
At least we now know where we can find it, and we open game.h with our editor.
I've put the main structure here below.
// // functions provided by the main engine // typedef struct { ... } game_import_t;
As you can see game_import_t (and also game_export_t which is right below game_import_t) is a c struct,
and as the comment above indicates, it contains pointers to functions provided by the main engine.
Ah.. Pieces are starting to fall into place when we also note that the game_export_t contains pointers
to functions exported by the game subsystem (that's the DLL right).
We also now make the assumption that game.h is also included in the main engine code, and probably
should not be changed too much. As a matter a fact those functions are the building stones we will
use when making our own DLL's eventually.
Let's return to the entry point function GetGameAPI in g_main.c and look what it does. The first line looks like this:
gi = *import;
import is the game_import_t structure which we want to save for future use. This is done by putting it into a global variable gi. The line below look like this:
global.some_variable_or_function = some_local_variable_or_function;
The last line reads:
return &globals
What does this mean? Well, take a look at the top of the file you will see that globals is declared as game_export_t, and that is exactly what should be returned. We can see from the last line that the pointer to this structure is returned to the engine. What is done here is basically that all the functions needed to make the game what it is are implemented in the DLL code and their pointers given to the engine. Let's go through that list and see what they do.
First, a word of warning. This chapter is NOT very intuitive, but rather descriptive and thus differ a lot from the other chapters I have written. I don't expect you to understand even half of this as it is heavy reading and a lot of information if you don't have a compact background knowledge. (If you have programmed QC it will help some though). Just read it as thorough you want to.
globals.apiversion = GAME_API_VERSION;This first is just some sort of version check for the engine. It's currently set to 1, and I believe this number could increase to 2 when id releases their bugfixed code. It really is not very interesting for us as it does nothing constructive anyway.
globals.Init = InitGame;
This function is called immediately after the DLL has loaded and the new game starts. It's easily found using grep as it's only mentioned in g_main.c and the file it's implemented in. You can find it in g_save.c (Yes, I wonder what it's doing there too) if you don't want to find it yourself, you lazy bum! Among other things that happen here is the initialization of most of the cvars. A cvar, if you didn't know it, is a console variable which can be modified at the server console. For example, deathmatch and teamplay are cvars. Also, here tag memory for the edicts and clients are allocated (I'll get more into detail about what edicts are later). The memory allocation process is a little special, since we really get the memory from the engine. I think id found it nicer not to allocate memory directly in the DLL, but instead allocation this at the game start in a big chunk which then is made safe from disk swapping. Anyway, that's a little too much on the technical side for us.
globals.Shutdown = ShutdownGame;
No mystery, this is called when the server shuts down. It prints the ShutdownGame message I described in the first part of the tutorial. It also frees all tag memory. As you can see the tag memory is declared as being used by different parts of the DLL. There is tag memory for levels and level changing, and the rest of the tag memory is declared as being used by the game. This is good if you for example want to free all tag memory used in the level category.
globals.SpawnEntities = SpawnEntities;
This function is called from the engine at each map start. If you look in game.h you can see that this function takes
three parameters. The first is the name of the map, the second, and most interesting, is something declared as
char *entstring, and finally the third is the spawnpoints for something which was not possible to put in the map
(The code says it has something to do with coop respawns, strange).
The entstring is really a very long string, actually the ascii part of a map which can be seen using a
hex viewer. It looks like a lot of c structs, and contains all the entities and the coordinates.
That part is being sent in here for parsing, so if you wanted to, you could change the entity definition for map files
TOTALLY.
globals.WriteGame = WriteGame; globals.ReadGame = ReadGame; globals.WriteLevel = WriteLevel; globals.ReadLevel = ReadLevel;
To be honest, I'm not really sure what these do exactly. They are used for saving/loading in singleplayer games. I think ReadLevel could be used for something more, like reading additional information from the map file. I'll probably go into this in more detail in a later tutorial part, when I've learned more.
The following all have to do with client (external players) handling. All functions that are prefixed "Client" except the last can be found in p_client.c.
globals.ClientThink = ClientThink;
This function is called very frequently, once for each client frame. Several things are done here. The function takes two parameters, the entity which is the player (client), and also has a special structure called ucmd_t. The ucmd_t structure can be found in q_shared.h, as it contains information which is also used in the engine (as I previously mentioned). This structure contains the angles for the client viewpoint, and a bitmapped register for "buttons". It's all a little cryptic, but will hopefully be clearer after analysing the code for some time.
globals.ClientConnect = ClientConnect;
This function also takes an entity as a parameter. Furthermore it takes a userinfo string as parameter (and a loadgame boolean, but that is not interesting here). All these functions will take an entity as a parameter as we are using the same function for different clients. We simply have to know what client we are referring too. ClientConnect is only called when a client tries to connect to the server, and never again. From here we make sure that the client is properly connected before leaving back control to the engine, which then executes ClientBegin (see below).
globals.ClientUserinfoChanged = ClientUserinfoChanged;
Whenever a player changes one of his userinfo variables this function will be called, taking care of the changes that need to occur. It parses interesting things out of the string and saves it into suitable places in the entity structure. For example, if the player changes from male to female we need to update which sound directory should be used. The information visible to other players is updated via the server by putting the updated skin and player name in a game_import_t function, gi.configstring().
globals.ClientDisconnect = ClientDisconnect;
Fairly obvious, this function is called whenever a player for some reason disconnects from the server. We need to wipe away all trace of the player lost. First, a special muzzle flash effect is sent to all the other players (... he disappeared in a puff of smoke ... ). Second, the model for that non-active player is set to to something that is nothing. It does some more things, like decreasing the number of players in the server, but that is not very important now.
globals.ClientBegin = ClientBegin;
In this function the client is actually put in the game. A lot of the code here is for handling a game loaded from disk. The most important things done here are informing the other players that a new player has arrived and making the client properly a part of the game, even if it's deathmatch or a single player game.
globals.ClientCommand = ClientCommand;
This function is executed whenever the client either types a command in the console or issues it by pressing a bound key, i e like using the different weapons. In Quake 2 there are no impulses like in Quake 1. Instead when someone wants to use a certain weapon the command that is called is "use that_weapon". For the curious without grep this function can be found in g_cmd.c, in contrast to all the other ClientXYZ functions which all exists in p_client.c.
globals.RunFrame = G_RunFrame;
You can find this function i g_main.c. This is the function that is called each server frame (as a contrast to client frame, which is more frequent). As implicated by the comment to the function it advances the world by 0.1 seconds (the constant FRAMETIME). Quite a lot of things are done here, and I'll try to cover briefly what things are going on and need to be done here. I think I'll do it in a list form:
globals.edict_size = sizeof(edict_t);
The size of an edict (entity) is needed by the game engine for some reason, probably due to memory allocation reasons and suchlike.
I'm afraid this last chapter was a little too much to take in. I wrote it so that you would have some reference to where to look when you want to change something while realizing your own crazy ideas of how Quake 2 could be changed for your amazing TC.
So, now we know a lot more of how the Quake 2 subsystem works than we did before going through these pages. What is the next step? In what direction should we concentrate our learning? There are actually quite a few paths we could follow from here, as there is a lot more to know before mastering the arts of Quake 2 coding. One thing you could do, and a thing that I highly recommend, is to try to change some minor things in the code. Determine something you would like to behave a certain way in Quake 2, and then use some time for research on how to do that before you start. Remember, you learn a lot more if you find out how to do yourself instead of asking someone else. You certainly have enough knowledge, if you just have some proficiency in c and linked lists, to do something on your own anyway. Happy coding until next time then!
Since the last part I've been informed of a simpler, and better solve for the entrypoint problem. Here is an updated makefile. Delete your old game.def and use this instead. If you want to know what is different then look at the game.def: line. You can see that a new game.def is created with the GetGameAPI export function identifier forced set to just GetGameAPI.
== 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 game.def: echo EXPORTS > game.def echo GetGameAPI = GetGameAPI >> game.def gamex86.dll: $(OBJS) game.def lcclnk -dll -entry GetGameAPI $(OBJS) game.def -o gamex86.dll clean: rm *.obj *.dll %.obj: %.c $(CC) $(CFLAGS) $< == Makefile == snip == 8< == END ==
Sat Dec 27 00:22:54 GMT 1997