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,
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 there great help and support with hosting.
Best viewed with Netscape 4