
Quake DeveLS - Impulse Commands
Author: Peter "Czar" Williams
Description: Add support for impulses by handling them in the new file
impx86.dll
Difficulty: SCARY, but not too complicated
The Plan
of Attack
As per the
description, this tutorial will explain how to 1) add support for loading
another DLL to gamex86.dll, 2) implement cross-DLL function calls, and 3) write
a DLL that will handle impulse calls. That's a tall order so let's get started.
I set out with the
following plan in mind. My idea was to add the ability to the client to have
custom impulses, allowing the ability to perform alias-like operations but more
flexible ones. I decided that the gamex86.dll would load a second DLL, called
impx86.dll, that handled the impulses. The user then selects which impx86.dll
to use, just like with the game, and that DLL does its stuff. Simple (somewhat)
in concept but difficult to execute.
First we must decide how
this is going to work. We need to add support for loading another DLL to
gamex86.dll. That means somewhere we have to load the DLL, and somewhere we
have to unload it. I chose to put call loading code from the function
InitClientPersistant(). This function is called when 1) a new SPQ game is
started, 2) a new DM level is loaded, or 3) the player dies in DM. Because
loading the DLL every death is overkill (no pun intended), I added code to take
care of that possibility. The unloading code I put in ShutdownGame, which is
another logical choice.
Import / Export
structures
Now, how to communicate
between DLL's? I chose to copy Carmack's game_xxport_t structures. I called
them impulse_xxport_t and put them in the header file. The game imports to the
impulse DLL all the major game structures and a pointer to the function
FindItem() because it's pretty useful and not accessible from a separate DLL.
The impulse DLL exports to the game three functions: ImpulseInit(),
ImpulseCommand(), and ImpulseShutdown(). So this is the file I arbitrarily
called impulse.h:
#ifndef IMPULSE_H //Header collision prevention #define IMPULSE_H //#include "g_local.h" Need this, but get header collision #define IMPULSE_API_VERSION 1 //Like GAME_API_VERSION, ensures different versions //don't crash and burn. typedef struct { int apiversion; //Will equal IMPULSE_API_VERSION defined in the impx86.dll void (*ImpulseCommand)( int impulse, edict_t *ent ); void (*ImpulseShutdown)(); void (*ImpulseInit)(); } impulse_export_t; //Functions the DLL impx86 exports and we import. typedef struct { game_locals_t *game; //Pointer to game info, because it changes level_locals_t *level; //Pointer to level info, because it changes game_import_t gi; //The function hooks quake2.exe exports to gamex86.dll game_export_t globals; //The function hooks gamex86.dll exports to quake2.exe // and now, impx86.dll gitem_t *(*FindItem)( char *pickup_name ); } impulse_import_t; //Functions the DLL impx86 imports and we export. #endif
InitImpulseLib
The code that loads the
DLL must also export an impulse_import_t and import an impulse_export_t. (Yeah,
they're switched. The structure names are from the impx86.dll's point of view,
and we're in gamex86.dll. What impx86.dll exports, we import, and vice versa.)
I put in a new function called InitImpulseLib() to do all this work. So, open
up the file p_client.c, which contains the function InitClientPersistant() and
will contain InitImpulseLib(). We will prototype our function and lots o' here:
//Modified for impulse: added InitImpulseLib, CloseImpulseLib; //modified initClientPersistant (I put these notes in patches //for clarity as to what's happening ) #include "windows.h" //InitImpulseLib will call Win32 functions //So they must be prototyped #include "g_local.h" #include "m_player.h" #include "impulse.h" //Get the definitions of impulse_xxport_t impulse_import_t ii; impulse_export_t *(*GetImpulseAPI)( impulse_import_t ii ); char imp_timer_message[512]; //This is for later, the UGLIEST gclient_t *messagedest; //HACK IN THE UNIVERSE //g_cmds.c extern qboolean impulse_lib_loaded; extern impulse_export_t *ie; extern HINSTANCE himpulse; // end void InitImpulseLib( gclient_t *who ); //NEW void CloseImpulseLib( void ); //NEW
Now go to
the function InitClientPersistant(). At the very bottom add the function call:
//..... client->pers.max_cells = 200; client->pers.max_slugs = 50; InitImpulseLib( client ); //NEW }
Right
below this we'll define InitImpulseLib(). This function has three
responsibilities: get the DLL filename, open the DLL, and do the exporting and
importing. However, there is a problem. When the level is loading, and I want
to open impx86.dll, the client is not fit to be printed to. Quake just doesn't
like it. However, I really want to print output. The only solution I could come
up with is to create a timer entity, which uses those two "for later"
variables that were above, that will print all the output one second into the game.
I can *feel* a better solution to this, but it eludes me. Mail it in if your
perception exceeds mine. Anyway, this big chunk of source is how I did it:
//Find edict_t given gclient_t. Modified from the file Q_devels.c edict_t *ent_by_name ( gclient_t *source ) { int i; edict_t *targ = NULL; for( i = 0; i <= globals.num_edicts; i++ ) { targ = G_Find( targ, FOFS(classname), "player" ); if( targ == NULL ) break; if( Q_stricmp( targ->client->pers.netname, source->pers.netname ) == 0) return (targ); } return NULL; } //The think function of the timer described above. void ImpulseTimerThink( edict_t *ent ) { edict_t *targ = NULL; if( messagedest == NULL ) { gi.dprintf( "ImpulseTimerThink: No gclient_t!\n" ); G_FreeEdict( ent ); return; } //Simple test for msg validity. It's stored in an array, so == NULL won't work if( strlen( imp_timer_message ) < 5 ) { gi.dprintf( "ImpulseTimerThink: Bad message!\n" ); G_FreeEdict( ent ); return; } targ = ent_by_name( messagedest ); //Figure out who we're printing to. if( targ == NULL ) { // gi.dprintf( "ImpulseTimerThink: bad gclient_t!\n" ); G_FreeEdict( ent ); return; }
Let me
explain those preceeding lines. Through various methods that I don't
understand, nor do I really care about, ent_by_name() returns NULL when the
client is not initialized. It also appears that ImpulseTimerThink() manages to
get called multiple times, although logic dictates it gets called once. Anyway,
since we only want to print when the client is valid, we silently return if
it's not time yet.
//Print the already-produced message. gi.cprintf( targ, PRINT_HIGH, imp_timer_message ); messagedest = NULL; //Clear variables and kill self strcpy( imp_timer_message, " " ); G_FreeEdict( ent ); } //Init the impulses: load the DLL and find the functions void InitImpulseLib( gclient_t *who ) { char libname[128]; cvar_t *impdir; DWORD errorcode; edict_t *ent = NULL; edict_t *timer = NULL; ent = ent_by_name( who ); if( ent != NULL ) //This means the level is NOT loading, and we DON't want return; //to init. Weird if( impulse_lib_loaded ) CloseImpulseLib(); //Set up the timer that will print the output messagedest = who; strcpy( imp_timer_message, " " ); timer = G_Spawn(); //I probably don't need all these things but timer->movetype = MOVETYPE_NONE; //why the hell not? timer->clipmask = 0; timer->solid = SOLID_NOT; timer->owner = timer; timer->classname = "imptimer"; timer->think = ImpulseTimerThink; timer->touch = NULL; timer->nextthink = level.time + 1; VectorClear( timer->s.origin ); VectorClear( timer->movedir ); VectorClear( timer->s.angles ); VectorClear( timer->velocity ); VectorClear( timer->mins ); VectorClear( timer->maxs ); timer->s.modelindex = 0; //gi.linkentity( timer ); ///////////////////////
Another
note: Calling gi.linkentity() CRASHES Quake 2. Who knows why? For equally
strange reasons the entity still thinks when its time has come, which I think
it is not supposed to do. I think this all means I don't understand the Quake
API too well.
//This adds to the buffer that will get printed to. In effect replaces the //print statement. strcpy( imp_timer_message, "==== InitImpulseLib ====\n" ); //Generate pathname: \\impx86.dll" impdir = gi.cvar( "impdir", "impulse", 0 ); GetCurrentDirectory( 128, libname ); strcat( libname, "\\" ); strcat( libname, impdir->string ); strcat( libname, "\\impx86.dll" ); //Also adds to print statement strcat( imp_timer_message, va( " Opening %s\n", libname ) ); //Load DLL, find swap function, swap xxports, do all sorts of error checking. himpulse = LoadLibrary( libname ); if( himpulse == NULL ) { errorcode = GetLastError(); strcat( imp_timer_message, va( " Library failed to load (error %li)\n", errorcode ) ); return; } strcat( imp_timer_message, " Library loaded correctly.\n" ); GetImpulseAPI = (impulse_export_t *(*)(impulse_import_t))GetProcAddress( himpulse, "GetImpulseAPI" ); if( GetImpulseAPI == NULL ) { strcat( imp_timer_message, " Could not find GetImpulseAPI.\n" ); return; } ii.FindItem = FindItem; //Build import struc ii.level = &level; ii.game = &game; ii.gi = gi; ii.globals = globals; ie = GetImpulseAPI( ii ); if( ie->apiversion != IMPULSE_API_VERSION ) { strcat( imp_timer_message, " Wrong API version,\n" ); return; } if( ie->ImpulseInit == NULL ) { strcat( imp_timer_message, " Could not find ImpulseInit.\n" ); return; } if( ie->ImpulseCommand == NULL ) { strcat( imp_timer_message, " Could not find ImpulseCommand.\n" ); return; } if( ie->ImpulseShutdown == NULL ) { strcat( imp_timer_message, " Could not find ImpulseShutdown.\n" ); return; } //Call DLL's initialization code and acknowledge startup! ie->ImpulseInit(); impulse_lib_loaded = true; }
Through
that circuitous route the impulse DLL is loaded. One second into later, the
output is printed.
Calling
the Impulses
Now we implement the
actual impulse command, at least the gamex86.dll's share. Open up g_cmds.c.
Remeber those "extern" declarations at the top of p_client? This is
where they're supposed to be, so put them in:
//Modified for impulse: ClientCommand #include "g_local.h" #include "m_player.h" #include "impulse.h" qboolean impulse_lib_loaded = false; impulse_export_t *ie; HINSTANCE himpulse = NULL;
Now we
have to put support for the command. Go to the bottom of ClientCommand(), and
insert this code:
else if (ent->client->ps.fov > 160) ent->client->ps.fov = 160; } else if( Q_stricmp( cmd, "impulse" ) == 0) //NEW. Our command { //Make sure stuff is working right. if( !impulse_lib_loaded ) { gi.cprintf( ent, PRINT_HIGH, "Impulses not loaded.\n" ); } else if( ie->ImpulseCommand == NULL ) { gi.cprintf( ent, PRINT_HIGH, "Impulse function not loaded.\n" ); } else { //It is. Do it! ie->ImpulseCommand( atoi( gi.argv( 1 ) ), ent ); } } else //end new gi.cprintf (ent, PRINT_HIGH, "Bad command: %s\n", cmd);
You see
ImpulseCommand() is passed two parameters: the integer value of the impulse,
and the entity who triggered it. ImpulseCommand() will do its thing and then
return.
CloseImpulseLib
Finally, we have to put in
the shutdown code. We (actually I) decided to call it from ShutdownGame(). So
we pop in the function call:
//p_client.c extern void CloseImpulseLib( void ); //end void ShutdownGame (void) { gi.dprintf ("==== ShutdownGame ====\n"); CloseImpulseLib(); //NEW gi.FreeTags (TAG_LEVEL); gi.FreeTags (TAG_GAME); }
For our
last edit to the game DLL, we implement CloseImpulseLib(). It's not nearly as
long as InitImpulseLib(). All we do is check that the DLL is valid, call the
impx86.dll's shutdown code, and unload the DLL. I commented out the print code,
because from ShutdownGame() there's no method that I trust to find out who to
print to. Since there's no way to fix an error if one happens in
CloseImpulseLib(), I opted to not bother. Our function looks like this:
//Unfortunately there's no way to get an ent from ShutdownGame so //we must bprintf to get info. But we don't want a flood of messages //so the bprintfs are commented out. void CloseImpulseLib() { //gi.bprintf( PRINT_HIGH, "==== CloseImpulseLib ====\n" ); if( !impulse_lib_loaded ) { //gi.bprintf( PRINT_HIGH, " Library not loaded.\n" ); return; } if( himpulse == NULL ) { //gi.bprintf( PRINT_HIGH, " Bad DLL handle.\n" ); return; } if( ie->ImpulseShutdown == NULL ) { //Yes, we did this in InitImpulseLib, //but paranoia is good. //gi.bprintf( PRINT_HIGH, " Bad/no ImpulseShutdown.\n" ); //Can't return now, must free library } else { ie->ImpulseShutdown(); } if( !FreeLibrary( himpulse ) ) //gi.bprintf( PRINT_HIGH, " Library freed incorrectly.\n" ); himpulse = NULL; //Clean up vars. ie = NULL; impulse_lib_loaded = false; }
Implementing impx86.dll
And that's all there is to
gamex86.dll. Whew. Next up: writing our own impx86.dll. This is easy easy easy.
As we know, only four functions have to be implemented, and they barely have to
do anything at all. We'll write an impx86.dll that will duplicate Quake I's
impulse commands. First, we can copy the important headers right from gamex86.dll.
These are game.h, q_shared.h, g_local.h, and impulse.h. To compile this DLL,
create a new project or subproject just like you did to make the gamex86.dll.
The only differences are that the output file should be "impx86.dll"
and you only need to link in one source file. I called this file i_main.c. It
starts with the usual declarations:
#include "g_local.h" #include "impulse.h" impulse_export_t impglobals; //What we export game_import_t gi; //What we import from the game impulse_import_t ii; //What we import from gamex86.dll cvar_t *deathmatch; //These are for our commands cvar_t *sv_cheats; impulse_export_t *GetImpulseAPI( impulse_import_t _ii ); void ImpulseInit(); void ImpulseShutdown(); void ImpulseCommand( int impulse, edict_t *ent ); void Cmd_GiveStuff( edict_t *ent ); //Function to do impulse niney type thing void Cmd_Quadize( edict_t *ent ); //Function to give somebody quad (impulse 255)
We declare
our GetImpulseAPI() much like GetGameAPI():
//Trade xxports. impulse_export_t *GetImpulseAPI( impulse_import_t _ii ) { ii = _ii; gi = ii.gi; impglobals.apiversion = IMPULSE_API_VERSION; impglobals.ImpulseInit = ImpulseInit; impglobals.ImpulseShutdown = ImpulseShutdown; impglobals.ImpulseCommand = ImpulseCommand; return &impglobals; }
The
initialization code is very simple:
void ImpulseInit() { //Setup our cvars deathmatch = gi.cvar( "deathmatch", "0", CVAR_SERVERINFO|CVAR_LATCH ); sv_cheats = gi.cvar( "sv_cheats", "0", CVAR_SERVERINFO|CVAR_LATCH ); }
Shutdown
code is even simpler. That is to say, there isn't any:
void ImpulseShutdown() { //gi.bprintf( PRINT_HIGH, "==== QuakeCommands Lib shutdown ====\n" ); }
Classic QuakeI-style Weapon Impulses
The function
ImpulseCommand() is the only one which does much work. When an impulse is
triggered, it gets called with the impulse and the edict who triggered it. A
switch statement delegates responsibility. The impulses are juggled around a
bit because Quake2 has more weapons than Quake One, so impulse 9 has to be
changed to impulse 11. Otherwise, the code looks much like Quake One's:
//Copied from q_devels.c void stuffcmd(edict_t *e, char *s) { gi.WriteByte (11); gi.WriteString (s); gi.unicast (e, true); } void ImpulseCommand( int impulse, edict_t *ent ) { //gi.cprintf( ent, PRINT_HIGH, "Impulse %i executed.\n", impulse ); switch( impulse ) { case 1: stuffcmd( ent, "use Blaster\n" ); break; case 2: stuffcmd( ent, "use Shotgun\n" ); break; case 3: stuffcmd( ent, "use Super Shotgun\n" ); break; case 4: stuffcmd( ent, "use Machinegun\n" ); break; case 5: stuffcmd( ent, "use Chaingun\n" ); break; case 6: stuffcmd( ent, "use Grenade Launcher\n" ); break; case 7: stuffcmd( ent, "use Rocket Launcher\n" ); break; case 8: stuffcmd( ent, "use HyperBlaster\n" ); break; case 9: stuffcmd( ent, "use Railgun\n" ); break; case 10: stuffcmd( ent, "use BFG10K\n" ); break; case 11: Cmd_GiveStuff( ent ); break; case 255: Cmd_Quadize( ent ); //Give 'em some QUAD! break; default: gi.cprintf( ent, PRINT_HIGH, "This impulse not supported.\n" ); break; } }
Yeah, I
didn't bother to implement the weapon rotation impulses -- I mean, they're an
exersize for the reader. Anyway, the code to CmdGiveStuff() is a stuffcmd with
cheat-checking code, and Cmd_Quadize is copied from Cmd_Use_f, except the bit
that checks whether they have a quad or not is removed. They look like this:
void Cmd_GiveStuff( edict_t *ent ) { //Get rid of this, and you can cheat in internet DM. if (deathmatch->value && !strcmp( sv_cheats->string, "1" )) { gi.cprintf (ent, PRINT_HIGH, "You must run the server with '+set" "cheats 1' to enable this command.\n"); return; } stuffcmd( ent, "give all" ); } void Cmd_Quadize( edict_t *ent ) { //Get rid of this, and you can cheat in internet DM. if (deathmatch->value && !strcmp( sv_cheats->string, "1" )) { gi.cprintf (ent, PRINT_HIGH, "You must run the server with '+set" "cheats 1' to enable this command.\n"); return; } if (ent->client->quad_framenum > ii.level->framenum) ent->client->quad_framenum += 300; else ent->client->quad_framenum = ii.level->framenum + 300; gi.sound(ent, CHAN_ITEM, gi.soundindex("items/damage.wav"), 1, ATTN_NORM, 0); }
That's it
And that's a wrap -- for
the coding. Getting it to run requires a wee bit more work. Make a subdir of
Quake2 called "impulse," and a subdir of that called
"qcmds." Copy the gamex86.dll to "impulse," and copy
impx86.dll to "qcmds." Then, to use the Quake Commands Impulse Lib,
set the cvar "impdir" to "impulse\qcmds" and go to a new
level in DM, or start a new game in SPQ. Much fuller documentation is included
with the entire source and compiled DLL's here
That really is all. Write
your own impx86.dll and send it in
!
Tutorial by Peter "Czar" Williams.
|
This site, and all
content and graphics displayed on it, |