Quake DeveLS - Multi DLL Support - Part 4

Author: by Victor Jimenez (aka Weektor)
Difficulty: Moderate to Understand, Moderate to implement

Part IV - Adding Items and Monsters

This is probably the piece of the tutorial that most people have been wanting. You have also probably been asking yourself "Why is this guy going through all this?" Granted that simple things like changing the way that something works is pretty easy and doesn't require a lot of code changes. However, to do that in a way that allows multiple non-cooperating developers to integrate their modifications is something else.

Now let me explain something first. This modification is not about "how" to make a new weapon or item. That involves game design and how that would effect the game balance. What I am going to do here is describe how to implement the weapon that you designed so that it can be used.

Remember, all this is being done dynamically, on the fly as it were. And when I say dynamically, I mean 'DYNAMICALLY'. For debugging purposes, you could create the 'LoadDLLModule' that adds commands that you can use on the console to load and manipulate particular DLLs. Or imagine a mod where two contestants face off and a weapon spawns randomly into the map. It can spawn anywhere and you could even have the spectators vote on what weapon. The contestant that gets to the weapon first gets to keep it and use it. Or instead of a weapon, you could spawn a monster into the arena and the last one left wins. In theory, you could even define a 'General Monster Behavior Engine!(tm)' and have the monster's behavior, looks, actions, etc. all defined by external data that is read in from an outside file. That is truly cool and ultimately what I want to do with all this stuff. These are all the things that are capable of being done from this mod.

As a matter of fact, the only thing that keeps Quake from being 100% dynamic is the very real need to know the visibility of walls ahead of time. Otherwise, you could configure a "Quake Server" that fed maps to people as they explore their environment.

In this tutorial, we are going to discuss how to create and add an item and do the same for a monster. We will make them similar in what you, the mod author, need to do but are handled somewhat differently under the hood.

We'll handle items first. This is where we can see how object-oriented the present id code really is. It also is where a real object oriented language would have had the greatest impact.

Items are described by a simple structure, found in g_local.h at line #200:

typedef struct gitem_s
{
	char		*classname;	// spawning name
	qboolean	(*pickup)(struct edict_s *ent, struct edict_s *other);
	void		(*use)(struct edict_s *ent, struct gitem_s *item);
	void		(*drop)(struct edict_s *ent, struct gitem_s *item);
	void		(*weaponthink)(struct edict_s *ent);
	char		*pickup_sound;
	char		*world_model;
	int		world_model_flags;
	char		*view_model;

	// client side info
	char		*icon;
	char		*pickup_name;	// for printing on pickup
	int		count_width;	// number of digits to display by icon

	int		quantity;	// for ammo how much, for weapons how much is used per shot
	char		*ammo;		// for weapons
	int		flags;		// IT_* flags

	void		*info;
	int		tag;

	char		*precaches;	// string of all models, sounds, and images this item will use
} gitem_t;

These structures are maintained in a globally defined array in the g_item.c file around line # 1100. The array is called, appropriately enough, itemlist. This array is statically defined to hold the items that immediately follow. Thus the array is no bigger or smaller that it needs to be. Since this is the case, you always see mod authors appending data to the end of this structure. Interestingly enough, there is a constant defined in the q_shared.h file on line 65 that's called MAX_ITEM. It doesn't control the size of the itemlist array, but rather the size of the inventory that the player can carry. The game dll code uses the inventory array in a one to one mapping between the items in inventory and the itemlist array. Furthermore, it will place the amount of the item in the inventory array.

If we had an object oriented language, we could redefine the access method for the itemlist array and the have the object do whatever pointer manipulation, list walking etc. to provide an index access method. Similarly, the inventory could be changed for the player so it still behaved the same way and allowed expanded lists, etc. This way, none of the code would change. Of course, you could do the obvious thing, which I like even better.

Go to line 1100 of g_items.c and change the line from

gitem_t itemlist[] =

to:

gitem_t itemlist[MAX_ITEM] =

This makes the itemlist array able to contain up to MAX_ITEM number of new items. Now all we need to do is manage the items and the itemlist. If we provide routines to do this and exclusively use these functions to access and manipulate the itemlist array, all the mod authors will be able to use this mechanism.

We're not quite done with the g_items.c file however. If you search for a function called InitItems, you will see that it is calculating the number of items in the game based on the size of the itemlist array. Well, we just made that MAX_ITEM in size. What we really want is a count of items that have been placed in the array. So make the function look like this:

void InitItems (void)
{
	//VJJ - old way, we need to walk the list until we find the end.
	//game.num_items = sizeof(itemlist)/sizeof(itemlist[0]) - 1;
	//hackus maximus
	game.num_items = 42;
}

Ideally, this function would set num_items to zero (0!) and the game code would add it's items using the provided functions. That's what I intended to do for a TC construction kit anyway.

Well, let's add the functions to manipulate the items. Open a file called u_entmgr.h and put the following into it:

/*
  u_entmgr.h

  vjj

  This file declares the functions that a user would have at their disposal to
  manipulate items and entities.
*/

//returns a pointer to the gitem_t allocated in the itemlist, null if failed
gitem_t *InsertItem(gitem_t *it);

//finds and removes the items. returns the old index, zero if not found
int RemoveItem(char *name);

We are implementing a set of functions that will let us insert and remove items at will.

So now open a file called u_itemmgr.c and insert the following code:

/*
  u_entmgr.c

  vjj

  This file contains the functions that a user would call to insert their 
  items into the itemlist[] and entities into the spawns[].

*/

#include "g_local.h"
#include "u_itemmgr.h"

#define EMPTY_NAME   "*none*"

//add an item to the itemlist array. Note that once you request a spot, it
//forever increases the number of items in the list,  even if you remove the 
//weapon.
//Note that most of the items in the item struct are pointers. No copy of the
//data is performed. You must maintain a copy of the data in your dll! 
gitem_t *
InsertItem(gitem_t *it)
{
    int i;
    gitem_t *spot;
    
    spot = NULL;
      //first, we want to find a place for the item.
    for(i=1;i< game.num_items && !spot;i++)
        if(!Q_stricmp(itemlist[i].classname,EMPTY_NAME))
            spot = &itemlist[i];

      //if we didn't find an empty slot, see if we can create one
    if(!spot && game.num_items < MAX_ITEMS)
        spot = &itemlist[++game.num_items];

      //OK, fill spot in with the stuff the user sent in
    if(spot)
        *spot = *it;
    
    return spot;
}


//remove an item. This is tricky. We just can't get rid of it since this
//would leave a hole in the array. Also, if someone has a pointer to the
//item and called one of the functions, the game would crash if there wasn't
//something there. So we need to supply our own dummy functions.
qboolean booldummy(struct edict_s *i, struct edict_s *ii)
{
    return false;
}
void dummy1(struct edict_s *i, struct gitem_s *ii)
{}

void dummy2(struct edict_s *i)
{}


int
RemoveItem(gitem_t *it)
{
    int i;
    
      //first, find the index for the item
    i = ITEM_INDEX(it);
    
      //we want to check to make sure we don't break anything
    if( i > MAX_ITEMS || i < 0)
        return 0;
    
      //we are OK, fix all the pointers and value w/ safe stuff
	it->classname = EMPTY_NAME;
	it->pickup = booldummy;
        it->use = dummy1;
	it->drop = dummy1;
	it->weaponthink = dummy2;
	it->pickup_sound = "items/pkup.wav";
	it->world_model = NULL;
	it->world_model_flags = 0;
	it->view_model = NULL;
	it->icon = "i_fixme";
	it->pickup_name = EMPTY_NAME;
	it->count_width = 0;
	it->quantity = 0;
	it->ammo = NULL;
	it->flags = 0;
	it->info = NULL;
	it->tag = 0;
	it->precaches = "";

    return i;
}

The InsertItem function scans through the list, looking for an empty slot to insert the new item. If it finds an empty slot, it will set the internal game dll pointers to your information. Remember to maintain the copy of the data that you inserted into the game, at least until you remove it. If you don't, the game will more that likely crash. Big Time. Trust me.

I was a little leery of putting a remove function into the game. If you look at the current u_loaddll.h, you will see no mention of a remove function in the user import structure. However, a friend of mine wanted to make weapons that were confined to a particular area of a map and if you went out of the area, the weapon would go away. I humored him. The RemoveItem is the result. It attempts to set the variables to safe values.

You may be wondering about how do you find an item in the itemlist array. Well, id kindly provided a FindItem function that you can request a pointer from the FindFunction stuff in Part 3 of the tutorial.

Now go into the u_loaddll.h and change the userdll_import_t structure by adding the following lines right after the FindFunction declaration:

        gitem_t *(*InsertItem)(gitem_t *it);
	void (*RemoveItem)(gitem_t *it);

Next, go into the u_loaddll.c file and add the include for the new header that we created under the "u_findfunc.h" like this:

#include "u_entmgr.h"

and go to around line 168, inside the InitializeUserDLLs() function, right after the line where you are setting the FindFunction variable and insert the following two lines:

	UserDLLImports.InsertItem = InsertItem;
	UserDLLImports.RemoveItem = RemoveItem;

You are now ready to start adding items to the game.

We need to explain some of the underlying structure of the item structure, especially about the functions that you have to supply.

The first function is the pickup function. The pickup function is what gets called when you run over an item. It determines if you can pick the item up or not and configures the player's variables if it was pickup-able. If the player picked up the weapon, the function is to return true. If not, then you return false. Health is handled by this function.

The next function is the use function. The use function is for whenever you want to use the item. If the item is a weapon, this means that whenever you switch to this weapon, it will be called. If the item is a Powerup, whatever effect that power up has is now invoked.

The drop function is what is called to allow the item to be dropped and removed from the player's inventory. I suppose that you could also use this function to do something like drop an item off to activate an entity or set a marker for territory.

The final function is the weaponthink function. This function is called whenever you activate the item that you are using. This does all the magic and it is not really intuitive of how it works. The problem is that this function has to handle all the animations for your item. If you are a weapon, you will call a special function named Weapon_Generic with one of the parameters being the function that does the actual firing. You will have to obtain a pointer to the Weapon_Generic function, create a wrapper function and pass the function that you created to the Weapon_Generic function. As an example, we'll convert the Blaster to fire three shots instead of the usually wimpy one shot. This was originally published on QDevelS by Sum.

We start with the dll template that we developed and remove all the unnecessary functions and variables and add the desired functionality:

/*
  hyper_3.c

  vjj  03/29/98

  We are using the dll template that was created for the multiple dll 
  modification.
*/


#define USER_EXCLUDE_FUNCTIONS 1

#include "g_local.h"
#include "g_cmds.h"
#include "u_loaddll.h"

/* place the name of your dll here */
#define DLL_NAME    "Hyper3"

/*
  first, we need to set up a number of variables that will be needed while
  running. This would be where we set up the references to the Quake2 things
  like the gi structure, the game structure, etc.

  for our example, we need the InsertCommand function and the gi for the 
  commands to work.
  */

static game_import_t *ptrgi;
static game_export_t *ptrGlobals;
static level_locals_t *ptrLevel;
static game_locals_t *ptrGame;

static void (*PlayerInsertCommands)(struct g_cmds_t *, int, char *);
static void (*(*PlayerFindFunction)(char *t));
/* commented out - future functionality
static gitem_t *(*InsertItem)(gitem_t *it);
static void (*RemoveItem)(gitem_t *it);
static void (*InsertMonster)();    
static void (*InsertClient)();     // for bots?
*/

static int AlreadyInit = 0;
static int AlreadyLoad = 0;

/*
  user code goes here
  */

void (*Com_Printf)(char *msg, ...);   //pretty much always need this one
void (*Blaster_Fire)(edict_t *, vec3_t, int, qboolean, int);
void (*NoAmmoWeaponChange) (edict_t *);
void (*Weapon_Generic) (edict_t *, int, int, int, int, int *, int *, void (*fire)(edict_t *ent));
gitem_t *(*FindItem)(char *);

//originally created by Sumfuka, published on QDevelS

void Weapon_Blaster_Fire (edict_t *ent)
{
    int     damage;	
       // STEVE
    vec3_t  tempvec;

    if (deathmatch->value)
        damage = 15;
    else
        damage = 10;

    Blaster_Fire (ent, vec3_origin, damage, false, EF_BLASTER);
	
       // STEVE : add 2 new bolts below	
    VectorSet(tempvec, 0, 8, 0);
    VectorAdd(tempvec, vec3_origin, tempvec);
    Blaster_Fire (ent, tempvec, damage, false, EF_BLASTER);

    VectorSet(tempvec, 0, -8, 0);
    VectorAdd(tempvec, vec3_origin, tempvec);
    Blaster_Fire (ent, tempvec, damage, false, EF_BLASTER);

    ent->client->ps.gunframe++;
}

void New_Weapon_Blaster (edict_t *ent)
{
	static int	pause_frames[]	= {19, 32, 0};
	static int	fire_frames[]	= {5, 0};

	Weapon_Generic (ent, 4, 8, 52, 55, pause_frames, fire_frames, Weapon_Blaster_Fire);
}


void Weapon_HyperBlaster_Fire (edict_t *ent)
{
    float rotation;
    vec3_t offset;
    int effect;	

    ent->client->weapon_sound = ptrgi->soundindex("weapons/hyprbl1a.wav");
    if (!(ent->client->buttons & BUTTON_ATTACK))
    {
        ent->client->ps.gunframe++;
    }
    else
    {
        if (! ent->client->pers.inventory[ent->client->ammo_index] )
        {
            if (ptrLevel->time >= ent->pain_debounce_time)
            {
                ptrgi->sound(ent, CHAN_VOICE,ptrgi->soundindex("weapons/noammo.wav"), 1, ATTN_NORM, 0);
                ent->pain_debounce_time = ptrLevel->time + 1;
             }
             NoAmmoWeaponChange (ent);
         }
         else
         {
             // STEVE .... the lines below are new !
             // ...........TRIPLE HYPER BLASTER !!!
             if ((ent->client->ps.gunframe == 6) || (ent->client->ps.gunframe == 9))
                 effect = EF_HYPERBLASTER;
             else
                 effect = 0;

              // change the offset radius to 6 (from 4), spread the bolts out a little
              rotation = (ent->client->ps.gunframe - 5) * 2*M_PI/6;
              offset[0] = 0;
              offset[1] = -8 * sin(rotation);
              offset[2] = 8 * cos(rotation);
              Blaster_Fire (ent, offset, 20, true, effect);

              // fire a second blast at a different rotation
              rotation = (ent->client->ps.gunframe - 5) * 2*M_PI/6 + M_PI*2.0/3.0;
              offset[0] = 0;
              offset[1] = -8 * sin(rotation);
              offset[2] = 8 * cos(rotation);
              Blaster_Fire (ent, offset, 20, true, effect);

              // fire a third blast at a different rotation
              rotation = (ent->client->ps.gunframe - 5) * 2*M_PI/6 + M_PI*4.0/3.0;
              offset[0] = 0;
              offset[1] = -8 * sin(rotation);
              offset[2] = 8 * cos(rotation);
              Blaster_Fire (ent, offset, 20, true, effect);

              // deduct 3 times the amount of ammo as before ( the *3 on end)
             ent->client->pers.inventory[ent->client->ammo_index] -=
                 ent->client->pers.weapon->quantity * 3;

          }
          ent->client->ps.gunframe++;
          if (ent->client->ps.gunframe == 12 && ent->client->pers.inventory[ent->client->ammo_index])
             ent->client->ps.gunframe = 6;
    }	
    if (ent->client->ps.gunframe == 12)	
    {
        ptrgi->sound(ent, CHAN_AUTO, ptrgi->soundindex("weapons/hyprbd1a.wav"), 1, ATTN_NORM, 0);
        ent->client->weapon_sound = 0;	
     }
}

void New_Weapon_HyperBlaster (edict_t *ent)
{
	static int	pause_frames[]	= {0};
	static int	fire_frames[]	= {6, 7, 8, 9, 10, 11, 0};

	Weapon_Generic (ent, 5, 20, 49, 53, pause_frames, fire_frames, Weapon_HyperBlaster_Fire);
}

/*
  End user code section
  */


/*
  okay, that was the end of the original code. we need to provide the
  framework. This part should be fairly boilerplate. 
  */


/*
  there are five functions that we need to provide.
  */

/*this is a security function and supposed to return an MD5 hash of the code in radix64*/
void
UserDLLMD5(char *buf)
{
    buf[0]='\0';  /*do nothing for now*/
}


/* initialization function - called to set up the dll. This is usually
 called to set up mod specific global data*/
void
UserDLLInit()
{
    gitem_t *it;

	if (AlreadyInit) return;

	AlreadyInit = 1;
	ptrgi->dprintf("In UserDLLInit for DLL %s\n",DLL_NAME);

      //this is where you would acquire any needed function pointers
    Com_Printf = (void (*)(char *msg, ...)) PlayerFindFunction("Com_Printf");
    Blaster_Fire = (void (*)(edict_t *, vec3_t , int , qboolean , int ))
                   PlayerFindFunction("Blaster_Fire");
    NoAmmoWeaponChange = (void (*)(edict_t *ent))
                   PlayerFindFunction("NoAmmoWeaponChange");
    Weapon_Generic = (void (*)(edict_t *, int, int, int, int, int *, int *,void (*fire)(edict_t *ent))
                   PlayerFindFunction("Weapon_Generic");
    FindItem = ((gitem_t *)(*)(char *))
	           PlayerFindFunction("FindItem");

    //Ok, now we are ready to change old weapon to new
    it = FindItem("Blaster");
    it->weaponthink = New_Weapon_Blaster;

    it = FindItem("Hyperblaster");
    it->weaponthink = New_Weapon_HyperBlaster;

}

/* this is the clean up function - if there were global data structures
 that you had allocated, this is where you get rid of them */
void
UserDLLStop()
{}

/* called at the start of each level. The player is in the game. Level
 specific variables can be placed here */
void
UserDLLStartLevel(edict_t *ent)
{}

/* called when the user exits the level. Used to clear out variable so
 that a user ends in a pre-configured state */
void
UserDLLEndLevel(void)
{}

/* called when the player respawns in a level. */
void
UserDLLPlayerRespawns(edict_t *self)
{}

/* called when a player dies */
void
UserDLLPlayerDies(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point)
{}


/*
  we need to initialize the structure that we will pass back, the same
  way that id does it.
  */
static userdll_export_t userdll_export =
{
    1,          //version of the library
    "default",      //creator - put up to 31 chars of your name here
    UserDLLMD5,  //this is supposed to return an MD5 hash of the code
    UserDLLInit,  //initialization function - adds the command in
    UserDLLStop,  //this is the clean up function
    UserDLLStartLevel, //supposed to be called at the start of each level
    UserDLLEndLevel, //called when the user exits the level
    UserDLLPlayerRespawns, //called when user respawns
    UserDLLPlayerDies   //called when the user dies
};


/*
  finally, at long last, we define the entry point that is called by
  the external loader. In our example, we only care about
  */

userdll_export_t
UserDLLGetAPI(userdll_import_t udit)
{

    PlayerInsertCommands =  udit.InsertCommands;
    PlayerFindFunction = udit.FindFunction;
	ptrgi = udit.gi;
	ptrGlobals = udit.globals;
	ptrLevel = udit.level;
    ptrGame = udit.game;
    
	ptrgi->dprintf("Inside GetAPI for %s\n",DLL_NAME);
    return userdll_export;
}

Don't forget the .def file to export the UserDLLGetAPI function, or whatever you have to do to get your dll to have the UserDLLGetAPI function symbol defined. And finally, don't forget to add your new dll to the quserdll.ini file.

Pretty neat. Kinda makes the wimpy ole blaster a Gib-O-Matic!(TM) You can get access to any item in Quake II this way and modify it by finding it's entry and placing your own functions in the pointer variables.

There is only one problem. If you play deathmatch with this mod, everyone has a 3-shot blaster. This points out a characteristic of items that inhabit Quake: If you change one item, they all change. What we want to do is make an item spawn into the world so we can run over to it and pick it up. To do this properly requires a model that's skin, placed in the world, etc. Since this is a tutorial about programming (and I'm a terrible artist. Trust me.), I decided to replace the shotgun in the first episode with the Gib-0-Matic!(TM). To do this, we need to investigate how things come into existance in Quake, or are spawned.

When an item gets spawned into the world, the item is passed the instance variables by a generic function called SpawnItem, found in g_items.c. This is fine except that this function has some very convoluted code to deal specifically with the id supplied items in Quake. What if we want to have or need our own convoluted spawning code to handle our items? If we look at g_spawn.c, which is where the SpawnItem function gets called from the ED_CallSpawn function, we notice that the items are handled very differently from the other things that spawn. There are a couple of possible solutions to this problem. The one that I chose basically makes the items spawn like everything else. In other words, a spawn function gets created for each of the items and placed in the in a spawn_t structure that is inserted into spawns[], found in g_spawn.c. This approach allows mods to configure the spawning of their items according to whatever flags are set without modifying the original code every time a new item is added.

So go to the bottom of g_items.c and add the following functions for the id created items:

//added by vjj
//These are the functions needed to spawn the builtin Quake items.
//This problem is compounded by the fact that we are trying to build
//two things at once - one is the general solution to allow us to make
//things in the general way we want and two, fixes to the code so that 
//it'll work properly. Of course, we wish to minimize the changes.
//To simplify our effort, we will place these in the spawns[] list.
//A developer would have to create these functions for the new items
//that would be developed and then add tha appropriate entries into
//both the spawns[] and itemlist[]. We will create an interface to 
//the spawns[] list and that will be the low level interface since 
//everything ends up in the spawns[] list from items to monsters
//to triggers, etc.

//These are simply the old calls to SpawnItem with the entity filled in.

void SP_item_armor_body(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_armor_body"));
}

void SP_item_armor_combat(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_armor_combat"));
}

void SP_item_armor_jacket(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_armor_jacket"));
}

void SP_item_armor_shard(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_armor_shard"));
}

                                                                                      
void SP_item_power_screen(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_power_screen"));
}

void SP_item_power_shield(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_power_shield"));
}


void SP_item_weapon_blaster(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("weapon_blaster"));
}

void SP_item_weapon_shotgun(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("weapon_shotgun"));
}

void SP_item_weapon_supershotgun(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("weapon_supershotgun"));
}

void SP_item_weapon_machinegun(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("weapon_machinegun"));
}

void SP_item_weapon_chaingun(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("weapon_chaingun"));
}

void SP_item_ammo_grenades(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("ammo_grenades"));
}

void SP_item_weapon_grenadelauncher(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("weapon_grenadelauncher"));
}

void SP_item_weapon_rocketlauncher(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("weapon_rocketlauncher"));
}

void SP_item_weapon_hyperblaster(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("weapon_hyperblaster"));
}

void SP_item_weapon_railgun(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("weapon_railgun"));
}

void SP_item_weapon_bfg(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("weapon_bfg"));
}


void SP_item_ammo_shells(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("ammo_shells"));
}

void SP_item_ammo_bullets(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("ammo_bullets"));
}

void SP_item_ammo_cells(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("ammo_cells"));
}

void SP_item_ammo_rockets(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("ammo_rockets"));
}

void SP_item_ammo_slugs(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("ammo_slugs"));
}


void SP_item_powerup_quad(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_quad"));
}

void SP_item_powerup_invulnerability(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_invulnerability"));
}

void SP_item_powerup_silencer(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_silencer"));
}

void SP_item_powerup_breather(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_breather"));
}

void SP_item_powerup_enviro(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_enviro"));
}

void SP_item_powerup_ancient_head(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_ancient_head"));
}

void SP_item_powerup_adrenaline(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_adrenaline"));
}

void SP_item_powerup_bandolier(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_bandolier"));
}

void SP_item_powerup_pack(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("item_pack"));
}


void SP_item_key_data_cd(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("key_data_cd"));
}

void SP_item_key_power_cube(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("key_power_cube"));
}

void SP_item_key_pyramid(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("key_pyramid"));
}

void SP_item_key_data_spinner(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("key_data_spinner"));
}

void SP_item_key_pass(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("key_pass"));
}

void SP_item_key_blue_key(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("key_blue_key"));
}

void SP_item_key_red_key(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("key_red_key"));
}

void SP_item_key_commander_head(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("key_commander_head"));
}

void SP_item_key_airstrike_target(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("key_airstrike_target"));
}

We now need to make a few changes to g_spawn.c to accomodate the items being spawned as entities. Namely, we need to create references to the functions that we just created in g_item.c so that the functions in g_spawn.c can access them. Open g_spawn.c and add the following lines at the very top of the file:

//changes by VJJ 
//commented out - placed in g_locals.h
//typedef struct
//{
//	char	*name;
//	void	(*spawn)(edict_t *ent);
//} spawn_t;

//Ok, since we are modifying this file, we might as well declare the
//item spawning functions here. These are the functions that actually 
//cause the item to be spawned. By convention, the actual spawning 
//function is defined wherever the entity (or item in our case) is
//defined. 
//We are actually having to do two things. We need to manage the itemlist[]
//and we need to manage the spawns[] lists. When a level is brought up, the
//first thing that is does is run through the list of things in it and spawns
//them.

void SP_item_armor_body(edict_t *self);
void SP_item_armor_combat(edict_t *self);
void SP_item_armor_jacket(edict_t *self);
void SP_item_armor_shard(edict_t *self);

void SP_item_power_screen(edict_t *self);
void SP_item_power_shield(edict_t *self);

void SP_item_weapon_blaster(edict_t *self);
void SP_item_weapon_shotgun(edict_t *self);
void SP_item_weapon_supershotgun(edict_t *self);
void SP_item_weapon_machinegun(edict_t *self);
void SP_item_weapon_chaingun(edict_t *self);
void SP_item_ammo_grenades(edict_t *self);
void SP_item_weapon_grenadelauncher(edict_t *self);
void SP_item_weapon_rocketlauncher(edict_t *self);
void SP_item_weapon_hyperblaster(edict_t *self);
void SP_item_weapon_railgun(edict_t *self);
void SP_item_weapon_bfg(edict_t *self);

void SP_item_ammo_shells(edict_t *self);
void SP_item_ammo_bullets(edict_t *self);
void SP_item_ammo_cells(edict_t *self);
void SP_item_ammo_rockets(edict_t *self);
void SP_item_ammo_slugs(edict_t *self);

void SP_item_powerup_quad(edict_t *self);
void SP_item_powerup_invulnerability(edict_t *self);
void SP_item_powerup_silencer(edict_t *self);
void SP_item_powerup_breather(edict_t *self);
void SP_item_powerup_enviro(edict_t *self);
void SP_item_powerup_ancient_head(edict_t *self);
void SP_item_powerup_adrenaline(edict_t *self);
void SP_item_powerup_bandolier(edict_t *self);
void SP_item_powerup_pack(edict_t *self);

void SP_item_key_data_cd(edict_t *self);
void SP_item_key_power_cube(edict_t *self);
void SP_item_key_pyramid(edict_t *self);
void SP_item_key_data_spinner(edict_t *self);
void SP_item_key_pass(edict_t *self);
void SP_item_key_blue_key(edict_t *self);
void SP_item_key_red_key(edict_t *self);
void SP_item_key_commander_head(edict_t *self);
void SP_item_key_airstrike_target(edict_t *self);


//end of added item spawn functions

Notice that I move the definition of the spawn_t structure out of g_spawn.c and put it into g_local.h. Don't forget to do this! We will need this structure later on for our own mods, when we want to add our own entities and items now.

Right after the functions that we added are a bunch of function prototypes for the other spawn items. If you go past the end of those, around line 190, you will see the spawns array. Change that line to the following:

spawn_t	spawns[MAX_EDICTS] = {

While MAX_EDICTS is the absolute maximum that the game will handle, there is actually a another limit. There is a CVAR called maxentities that the game dll uses to set the sizes of arrays. This CVAR is not set from the MAX_EDICTS value. It is set in the g_save.c file as a latched CVAR from a constant value of "1024". Since id's stuff is already set up to handle a large number of edicts and all the game functions expect this, we are home free in the entity management department. However, using MAX_EDICTS here is not strictly correct. Oh well.

Of course, I feel fairly safe in using that number as a limit though, but not because it seems large enough. Rather, if you look up where MAX_EDICTS is defined, in order to change it to a larger value, you will see some very interesting comments. Basically, these values are used in the network protocol and if you change them, you will have to redo the protocol. So don't even think about it. Period.

Anyway, we were making room in the array for future items that need to be spawned into a level. Now go to the bottom of the array and add the following lines, before the {NULL, NULL} :

      //vjj - added all the item spawn functions.
      //I know, I should have used the built in functions but
      //these items are part of the standard game, just like 
      //the monsters. I really should have used the functions
      //that I provided to add to the spawn array the items, 
      //monsters, triggers, everything. Oh well. It's left
      //as an exercise to the student. I suppose that it even
      //could be tailored on a level by level so that only  
      //the things that will be used are placed in the spawns
      //array. One final improvement would be to sort the entries
      //as they are placed in the spawns[] so we can use some of
      //the more efficient searches.

    {"item_armor_body", SP_item_armor_body},
    {"item_armor_combat", SP_item_armor_combat},
    {"item_armor_jacket", SP_item_armor_jacket},
    {"item_armor_shard", SP_item_armor_shard},

    {"item_power_screen", SP_item_power_screen},
    {"item_power_shield", SP_item_power_shield},

    {"weapon_blaster", SP_item_weapon_blaster},
    {"weapon_shotgun", SP_item_weapon_shotgun},
    {"weapon_supershotgun", SP_item_weapon_supershotgun},
    {"weapon_machinegun", SP_item_weapon_machinegun},
    {"weapon_chaingun", SP_item_weapon_chaingun},
    {"ammo_grenades", SP_item_ammo_grenades},
    {"weapon_grenadelauncher", SP_item_weapon_grenadelauncher},
    {"weapon_rocketlauncher", SP_item_weapon_rocketlauncher},
    {"weapon_hyperblaster", SP_item_weapon_hyperblaster},
    {"weapon_railgun", SP_item_weapon_railgun},
    {"weapon_bfg", SP_item_weapon_bfg},

    {"ammo_shells", SP_item_ammo_shells},
    {"ammo_bullets", SP_item_ammo_bullets},
    {"ammo_cells", SP_item_ammo_cells},
    {"ammo_rockets", SP_item_ammo_rockets},
    {"ammo_slugs", SP_item_ammo_slugs},

    {"item_quad", SP_item_powerup_quad},
    {"item_invulnerability", SP_item_powerup_invulnerability},
    {"item_silencer", SP_item_powerup_silencer},
    {"item_breather", SP_item_powerup_breather},
    {"item_enviro", SP_item_powerup_enviro},
    {"item_ancient_head", SP_item_powerup_ancient_head},
    {"item_adrenaline", SP_item_powerup_adrenaline},
    {"item_bandolier", SP_item_powerup_bandolier},
    {"item_pack", SP_item_powerup_pack},

    {"key_data_cd", SP_item_key_data_cd},
    {"key_power_cube", SP_item_key_power_cube},
    {"key_pyramid", SP_item_key_pyramid},
    {"key_data_spinner", SP_item_key_data_spinner},
    {"key_pass", SP_item_key_pass},
    {"key_blue_key", SP_item_key_blue_key},
    {"key_red_key", SP_item_key_red_key},
    {"key_commander_head", SP_item_key_commander_head},
    {"key_airstrike_target", SP_item_key_airstrike_target},

      //end of item modifications.

It is very important that the new additions come before the {NULL,NULL} entry. That entry serves as a marker to the id functions to know when the end of the spawn functions have been reached.

We are almost done with the changes to g_spawn. Just below the spawns[] is a function called ED_CallSpawn. We need to make a few changes to it so that the items and the entities get spawned the same way. Change that function so it looks like this:

/*
===============
ED_CallSpawn

Finds the spawn function for the entity and calls it
===============
*/
void ED_CallSpawn (edict_t *ent)
{
	spawn_t	*s;
    //vjj - don't need these since items are now spawned the
    //same way as entities.
	//gitem_t	*item;
	//int		i;

	if (!ent->classname)
	{
		gi.dprintf ("ED_CallSpawn: NULL classname\n");
		return;
	}

	// check item spawn functions
/* vjj - removed special item spawn stuff
    for (i=0,item=itemlist ; iclassname)
			continue;
		if (!strcmp(item->classname, ent->classname))
		{	// found it
			SpawnItem (ent, item);
			return;
		}
	}
*/
	// check normal spawn functions
	for (s=spawns ; s->name ; s++)
	{
		if (!strcmp(s->name, ent->classname))
		{	// found it
			s->spawn (ent);
			return;
		}
	}
	gi.dprintf ("%s doesn't have a spawn function\n", ent->classname);
}

Compile everything again. If everything is working the right way, you will see items in your level, such as the Adrenaline Packs at the bottom of the ramp, or the combat armor through the area where you crouch. Believe me, I was real glad when I saw those things!

We have pretty well covered items in Quake II. Except that we still don't have a way of inserting things into the spawns[].

If you examine the u_entmgr.c code, you will notice that the opening comment says that it contains the code to insert things into the spawns[]. However, upon looking through the code, you will realize that it doesn't. Time to add it in.

Open the u_entmgr.h file and add the following lines at the bottom:

//returns a pointer to the spawn_t structure in the spawns[], null if failed
spawn_t *InsertEntity(spawn_t *spawnInfo);


//finds and removes the entity. returns the old index, -1 if not found
int RemoveEntity(char *name);

Now open the u_entmgr.c file and add the following five lines of code to the top of file, before any of the functions:

//these are the Entity functions
//These functions allow the user to insert and remove types of entities from 
//the spawns[].
//we need to be able to access the spawns[]
extern spawn_t spawns[];

Now go to the bottom of the u_entmgr.c file and add the following code:

//This works in a similar way to the way that the InsertItem works. It attempts
//to find an empty spot for your entity, recyling whatever open entry it finds.
//If it can't find a spot, it then appends to the end of the list, assuming 
//that there is enough room.
//Note that the InsertItem function does not call this function to perform it's
//insertion of the spawn_t into Spawns.
spawn_t *
InsertEntity(spawn_t *spawnInfo)
{
    int i;
    spawn_t *spot, *s;
    
    spot = NULL;
      //first, we want to find a place for the entity.
    for(s=spawns, i=0; iname;i++,s++)
        if(!Q_stricmp(s->name,EMPTY_NAME))
            spot = s;

      //if we didn't find an empty slot, see if we can create one
    if(!spot && !s->name && i < (MAX_EDICTS - 1)) //want to leave {NULL,NULL}
        spot = s;

      //OK, fill spot in with the stuff the user sent in
    if(spot)
        *spot = *spawnInfo;
    
    return spot;
}



//remove an entity. We don't want to leave a hole in the spawns[] by
//setting the entry to NULL - this would signal to the Quake Engine that
//the end of the list had been reached. We need a function that returns void
//and takes an edict pointer, i.e. dummy2() defined above, used for items.
int
RemoveEntity(char *name)
{
    int i;
    spawn_t *s, *found;
    
      //first, look for a match for the entity
    for( s = spawns, i = 0, found = NULL; s->name && !found; s++, i++)
        if(!Q_stricmp(s->name,name))
            found = s;
        
      //we are OK, fix all the pointers and value w/ safe stuff
    if (found)
    {
          //we want to make sure we don't break anything
        found->name = EMPTY_NAME;
        found->spawn = dummy2;
    }
    else 
        i = -1;
    
    return i;
}

Ok, to insert make inserting an item easier, go to the InsertItem function and replace it with the following function. It just takes a spawning function as one of the parameters and adds it to the spawns[] for you in a special way. Otherwise, whenever you add an item, you'd have to first add it to the item list and then the spawns array and have them possibly get out of sync by succeeding in one but not the other. You would then have to delete the one that succeeded. Not real difficult, but eventually, someone would leave out one of the steps and spend many an hour puzzled by what was happening.

//add an item to the itemlist array. Note that once you request a spot, it
//forever increases the number of items in the list,  even if you remove the 
//weapon.
//Note that most of the items in the item struct are pointers. No copy of the
//data is performed. You must maintain a copy of the data in your dll! 
//You also have to pass in a spawn_t struct that tells how to spawn your
//item.
gitem_t *
InsertItem(gitem_t *it, spawn_t *spawnInfo)
{
    int i, inc_items;
    gitem_t *spot;
    spawn_t *spspot, *s;

    inc_items = 0;
    spot = NULL;
      //first, we want to find a place for the item.
    for(i=1;iname;i++,s++)
            if(s->name && !Q_stricmp(s->name,EMPTY_NAME))
                spspot = s;

          //if we didn't find an empty slot, see if we can create one
        if(!spspot && !s->name && i < (MAX_EDICTS - 1)) //want to leave {NULL,NULL}
            spspot = s;

      //OK, fill spot in with the stuff the user sent in
        if(spspot)
        {
            *spspot = *spawnInfo;
            *spot = *it;            //OK, fill spot in with the stuff the user sent in
			if (inc_items) game.num_items++;
        }
    }
    
    return spot;
}

To make them available for our mods to use them, we need to go into the u_loaddll.c and the u_loaddll.h files and modify the structures and functions there. Pull up the u_loaddll.h file and place the following two lines under the lines that you added earlier for the item functions, around line 47:

    void (*InsertEntity)(spawn_t *t); 
    void (*RemoveEntity)(char *name);    

Now open the u_loaddll.c file and on line 170, below where the InsertItem and RemoveItem functions were filled in, add the following two lines:

    UserDLLImports.InsertEntity = InsertEntity;
    UserDLLImports.RemoveEntity = RemoveEntity;

Whew!!! We can now finally insert items with impunity and feel reasonably sure that Quake will spawn them and make them available for use. However, we need to discuss one last thing before fixing up the Gib-O-Matic!(tm) tutorial. Along the way of setting all this up, we bridged the difference between items and entities and we now have a toolset that will let us create entities in Quake and have them show up. Entities are things like monsters, triggers, players, etc. Which leads us to a little discussion on the type of objects in Quake.

From our experience, you can see that there are two types of objects that you run across in Quake, items and entities. Actually, there are three. But we can't do anything to the third, the temporary entities. We will discuss these types of entities in Part V, because it has important ramifications for security and user created mods.

There are fundamental differences between the two. Even though we have now changed items so that they spawn into the world the same way as entities, the Quake engine still handles them very differently. For instance, in the g_spawn.c file, the ED_ParseEdict function is parsing the edict data from the map and placing it in various fields for each and every entity. Items, on the other hand, all share a single set of variables, as we already know. If you change the properties of one, like a blaster to fire three shots instead of one, suddenly all your friends have a triple-shot blaster.

There is only one last thing to check to make sure that our newly created items are properly place into a map. We want to make sure that our dll sets up our items first, before the gamex86.dll loads the maps and processes the entities.

The function that performs the spawning of entities is, interestingly enough, called SpawnEntities. It is found in g_spawn.c, around line 500. If we look at the function, we see that it actually outputs a line to the console, just before it returns. The line says something to the effect of some number of entities inhibited. If we start up Quake II and look at the output, sure enough, one of the last lines is the entities inhibited line. Before that line, if you haven't taken out the gi.dprintf's from the module loading code, you will see all the usual junk about loading our user dll's. That's Great! We can be sure that our items and monsters and whatnots get put into the game before the map gets processed. This means that all our stuff will be available and indistinguishable from all the built in stuff.

Essentially, get rid of the first 221 lines in hyper_3.c and replaced them with this:

/*
  hyper_3.c

  vjj  03/29/98

  We are using the dll template that was created for the multiple dll 
  modification.
*/


#define USER_EXCLUDE_FUNCTIONS 1

#include "../game3.14/g_local.h"
#include "../game3.14/g_cmds.h"
#include "../game3.14/u_loaddll.h"

/* place the name of your dll here */
#define DLL_NAME    "Hyper3"

/*
  first, we need to set up a number of variables that will be needed while
  running. This would be where we set up the references to the Quake2 things
  like the gi structure, the game structure, etc.

  for our example, we need the InsertCommand function and the gi for the 
  commands to work.
  */

static game_import_t *ptrgi;
static game_export_t *ptrGlobals;
static level_locals_t *ptrLevel;
static game_locals_t *ptrGame;

static void (*PlayerInsertCommands)(struct g_cmds_t *, int, char *);
static void (*(*PlayerFindFunction)(char *t));
static gitem_t *(*PlayerInsertItem)(gitem_t *it, spawn_t *spawn);
//int (*RemoveItem)(char *name);
/* commented out - future functionality
static void (*InsertMonster)();    
static void (*InsertClient)();     // for bots?
*/

static int AlreadyInit = 0;
static int AlreadyLoad = 0;

/*
  user code goes here
  */

//we don't really need to allocate memory on a permanent basis since it is copied into
//the item array. However, I'm lazy :-p
gitem_t  gib_o_matic;

vec3_t vec3_origin = {0,0,0};
cvar_t	*deathmatch; 

void (*Com_Printf)(char *msg, ...);   //pretty much always need this one
void (*Blaster_Fire)(edict_t *, vec3_t, int, qboolean, int);
//we never run out of ammo for the Gib-O-Matic
//void (*NoAmmoWeaponChange) (edict_t *);
void (*Weapon_Generic) (edict_t *, int, int, int, int, int *, int *, void (*fire)(edict_t *ent));
gitem_t *(*FindItem)(char *);
void (*SpawnItem)(edict_t *, gitem_t *);
gitem_t *(*FindItemByClassname)(char *);


//originally created by Sumfuka, published on QDevelS
//we are just doing the hand blaster
//GOM = gib-o-matic
void GOM_Weapon_Fire (edict_t *ent)
{
    int     damage;	
       // STEVE
    vec3_t  tempvec;

    if (deathmatch->value)
        damage = 15;
    else
        damage = 10;

    Blaster_Fire (ent, vec3_origin, damage, false, EF_BLASTER);
	
       // STEVE : add 2 new bolts below	
    VectorSet(tempvec, 0, 8, 0);
    VectorAdd(tempvec, vec3_origin, tempvec);
    Blaster_Fire (ent, tempvec, damage, false, EF_BLASTER);

    VectorSet(tempvec, 0, -8, 0);
    VectorAdd(tempvec, vec3_origin, tempvec);
    Blaster_Fire (ent, tempvec, damage, false, EF_BLASTER);

    ent->client->ps.gunframe++;
}

void GOM_Weapon_Blaster (edict_t *ent)
{
	static int	pause_frames[]	= {19, 32, 0};
	static int	fire_frames[]	= {5, 0};

	Weapon_Generic (ent, 4, 8, 52, 55, pause_frames, fire_frames, GOM_Weapon_Fire);
}


//no fancy spawning stuff here, just nice and easy
void SP_GOM_Weapon_Blaster(edict_t *self)
{
    SpawnItem(self,FindItemByClassname("weapon_gibomatic"));
}


//need this to be able to spawn
spawn_t sp_gom =
{
    "weapon_gibomatic", SP_GOM_Weapon_Blaster
};


/*
  End user code section
  */


/*
  okay, that was the end of the original code. we need to provide the
  framework. This part should be fairly boilerplate. 
  */


/*
  there are five functions that we need to provide.
  */

/*this is a security function and supposed to return an MD5 hash of the code in radix64*/
void
UserDLLMD5(char *buf)
{
    buf[0]='\0';  /*do nothing for now*/
}


/* initialization function - called to set up the dll. This is usually
 called to set up mod specific global data*/
void
UserDLLInit(void)
{
    gitem_t *it;

	if (AlreadyInit) return;

	AlreadyInit = 1;
	ptrgi->dprintf("In UserDLLInit for DLL %s\n",DLL_NAME);

      //this is where you would acquire any needed function pointers
    Com_Printf = (void (*)(char *msg, ...)) PlayerFindFunction("Com_Printf");
    Blaster_Fire = (void (*)(edict_t *, vec3_t , int , qboolean , int ))
                   PlayerFindFunction("Blaster_Fire");
    //NoAmmoWeaponChange = (void (*)(edict_t *ent))
    //               PlayerFindFunction("NoAmmoWeaponChange");
    Weapon_Generic = (void (*)(edict_t *, int, int, int, int, int *, int *,void (*fire)(edict_t *ent)))
                   PlayerFindFunction("Weapon_Generic");
    FindItem = (gitem_t * (*)(char *))
	           PlayerFindFunction("FindItem");
	SpawnItem = (void (*)(edict_t *, gitem_t *))PlayerFindFunction("SpawnItem");
	FindItemByClassname = (gitem_t *(*)(char *))PlayerFindFunction("FindItemByClassname");


    //Ok, now we are ready to create new weapon from old weapon
      //we actually are not going to build it from the blaster but rather
      //the shotgun because the blaster expects you to always carry it around
      //and is missing a bunch of functions that we would either have to 
      //write or get a reference.
    it = FindItem("Shotgun");
    gib_o_matic = *it;
    
      //Ok, we copied over the default fields, fill in the ones we need
    gib_o_matic.classname = "weapon_gibomatic";
    gib_o_matic.weaponthink = GOM_Weapon_Blaster;
    gib_o_matic.world_model = "models/weapons/g_disint/tris.md2";
    gib_o_matic.view_model = "models/weapons/v_disint/tris.md2";
    gib_o_matic.icon = "w_blaster";
    gib_o_matic.pickup_name = "Gib-0-Matic";
    gib_o_matic.count_width = 0;
    gib_o_matic.quantity = 0;
    gib_o_matic.ammo = "Cells";
    gib_o_matic.precaches =  "weapons/blastf1a.wav misc/lasfly.wav";
    
      //I didn't fill in a few. Namely the gen weapon functions and a few
      //default values that don't change from weapon to weapon. At this
      //point, the Gib-O-Matic mostly behaves like a blaster.

      //need to register the weapon.
    PlayerInsertItem(&gib_o_matic,&sp_gom);
    

    deathmatch = ptrgi->cvar ("deathmatch", "4", CVAR_SERVERINFO | CVAR_LATCH);

}

Compile the dll and place it in the Quake2 directory. Now, using your favorite level editor, (I use PakExplorer and emacs), place your newly created item into the map. Leaving out the messy details, go to where you placed your item and pick it up. You should see a BFG looking thing.

Don't just stand there! Fire your weapon, soldier! Hopefully, you will see triple bolts of goodness shooting out. Tada - Gib-O-Matic!(tm)

I know. Quake complains about missing frames and such. That's work for the Modellers and for you when you are doing the weapon design.

You may be wondering what is in store for Part V of the MultiDLL tutorial. As you read, we now have the tools to insert entities into the world. You may be wondering why I didn't provide an example of an entity being created and used. Well, that's because I didn't cover everything that you need to make a new entity, only how to insert it into Quake. In the next and last part of the tutorial, I will cover that topic and Security and what it means to you. In there, I will talk a little (little, I promise) about what it means to be secure and some of the theory and algorithms that we'll be using. I'll also explain what I meant about the third type of entities and give you some future directions to go with this. You've heard some of them and I'll recap and give some pointers.

Until next time, Keep On Fraggin!

Tutorial by by Victor Jimenez (aka Weektor) .

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