Created By: Thomas R. Schar
eMail: Unknown
Difficulty Scale: Easy/Medium


CREDITS: SHaDoW.STaLKeR (Evan) evan@patriot.net


INTRODUCTION


Ever noticed that two grenades side by side don't explode at the same time.
To me that seems dum. Well I started with the idea of just doing this for
grenades, and then I thought; well why the hell not for rockets, and ammo
boxes as well!(It also adds in a little bit of stragedy(sic).. anyone played xblast?)



ASSUMED KNOWLEDGE/GLOSSARY

(in no particular order)

nextthink
Each entity in quake has a float attribute called nextthink.
think
Each entity in quake can have a pointer to a "think" function. This
function is called when the "nextthink" attribute of the entity is the
same as the current time in the game.
classname
Each entity has a string attribute called classname, which identifies
the entity to be of a particular class; eg, "missile", "grenade"
time
Time is a global variable which contains the current time of the game.
findradius
This function when passed a location and radius will return a list of
all the entity's it finds within the sphere.
chain
Each entity has a reference attribute that can point to one other entity.
This attribute is used to link together entity's that are found with the
"findradius" function.
self
In QuakeC, everything is loosly object based. So when a piece of code
is running, it is doing so by the command of a specific entity. The
"self" variable refers to this entity.
touch
Each entity has a reference to a function that is called when it touches
another entity. This function then decideds what actions, if any will
be performed.
other
When the touch function is called, there is a global variable "other" that
refers to the entity that is touching.
spawn()
This is a global function that returns a refernce to a newly created
(spawned) entity.



Step 1
There is a function called when a grenade explodes called, funnily enough,
GrenadeExplode(). This does some stuff, none of which I understand, and
then calls BecomeExplosion(). This actually makes the explosion appear
(I think)... :) Now the vanilla quake 1.06 code is as follows;
       //WEAPONS.QC
void() GrenadeExplode={
    T_RadiusDamage(self, self.owner, 120, world);
    WriteByte(MSG_BROADCAST, SVC_TEMPENTITY);
    WriteByte(MSG_BROADCAST, TE_EXPLOSION);
    WriteByte(MSG_BROADCAST, self.origin_x);
    WriteByte(MSG_BROADCAST, self.origin_y);
    WriteByte(MSG_BROADCAST, self.origin_z);
    BecomeExplosion();
};
Basically, the first line applies the damage to any nearby objects, and the
WriteByte() lines inform all clients of the explosion temporary entity(?).
So, we could insert some code in here that would try to find other grenades
within the specified radius that we could detonate. So we want to write
something like this:

//WEAPONS.QC
void() GrenadeExplode={
    local entity ptr;

    T_RadiusDamage(self, self.owner, 120, world);
    WriteByte(MSG_BROADCAST, SVC_TEMPENTITY);
    WriteByte(MSG_BROADCAST, TE_EXPLOSION);
    WriteByte(MSG_BROADCAST, self.origin_x);
    WriteByte(MSG_BROADCAST, self.origin_y);
    WriteByte(MSG_BROADCAST, self.origin_z);

//  call "findradius" to return list of entity's found
//  (I have chosen to use a radius of 120 because that is what is used by
//   the grenades in the T_RadiusDamage() function call above.  Feel free
//   to alter it to taste)
    ptr=findradius(self.origin, 120);

//  The list is returned to us in a linked-list fashion.  Put simply; if there
//  are no entities found, ptr is zero (or false). If there is at least one
//  entity found, ptr will refer to it.
    while(ptr){
//      here we check to see if the entity is of class "grenade", and that
//      we haven't found ourself
//      (ie. self does not refer to the same entity as ptr)
        if((ptr.classname=="grenade") && (self!=ptr)){
//          if both these conditions are true, set the nextthink time of the
//          entity to the current game time, and make sure the think function
//          of the entity is referring to the GrenadeExplode() function.
            ptr.nextthink=time;
            ptr.think=GrenadeExplode;
        }
//      By using the "chain" attribute of the entity we can find the next
//      entity in the list of entity's found by the findradius() function.
        ptr=ptr.chain;
    }

//  finally become an explosion, just as before
    BecomeExplosion();
};

However, this code has one problem.  It may inadvertently try to explode the
same grenade a number of times.  This is because we have no way of keeping
track of which grenades have already BecomeExplosion()'s so to speak.  A very
easy way of doing this is by using an attribute of the entity to store a flag
value.  But due to the way QuakeC is designed, we are forced to add this
attribute in for all entity's, even if they never use the attribute.  Instead,
we can use an attribute that will never be used for a grenade.  There are
quite a few of them, but the one I chose was "lip".  It is used for doors.
Now, we don't want to have to initialise the lip attribute whenever a grenade
is fired, so we choose some arbitrary number, that has virtually zero chance
of every accidentally occurring; say 1234.0.  Then, we make a slight change
to our code, to also check that the lip attribute of the grenade we are
going to explode is not 1234.0.  The code will look like this:

//WEAPONS.QC
void() GrenadeExplode={
    local entity ptr;

    T_RadiusDamage(self, self.owner, 120, world);
    WriteByte(MSG_BROADCAST, SVC_TEMPENTITY);
    WriteByte(MSG_BROADCAST, TE_EXPLOSION);
    WriteByte(MSG_BROADCAST, self.origin_x);
    WriteByte(MSG_BROADCAST, self.origin_y);
    WriteByte(MSG_BROADCAST, self.origin_z);

//  call "findradius" to return list of entity's found
//  (I have chosen to use a radius of 120 because that is what is used by
//   the grenades in the T_RadiusDamage() function call above.  Feel free
//   to alter it to taste)
    ptr=findradius(self.origin, 120);

//  set this grenades lip value
    ptr.lip=1234.0;

//  The list is returned to us in a linked-list fashion.  Put simply; if there
//  are no entities found, ptr is zero (or false). If there is at least one
//  entity found, ptr will refer to it.
    while(ptr){
//      here we check to see if the entity is of class "grenade", and that
//      we haven't found a grenade with a set lip value.  Notice that we
//      don't check for self any more?  We don't need to anymore because we
//      set our own lip value before entering this section of code.
        if((ptr.classname=="grenade") && (ptr.lip!=1234.0)){
//          if both these conditions are true, set the nextthink time of the
//          entity to the current game time, and make sure the think function
//          of the entity is referring to the GrenadeExplode() function.
            ptr.nextthink=time;
            ptr.think=GrenadeExplode;
        }
//      By using the "chain" attribute of the entity we can find the next
//      entity in the list of entity's found by the findradius() function.
        ptr=ptr.chain;
    }

//  finally become an explosion, just as before
    BecomeExplosion();
};


Step 2
To test this code out, first increase the rate of fire of the grenade
launcher, so that we can have more of the grenades going off at a time.
We can do this, by editing the W_Attack() function.

//WEAPONS.QC
void() W_Attack={
    ...
    }else if (self.weapon == IT_GRENADE_LAUNCHER){
                player_rocket1();
                W_FireGrenade();
//              change the 0.6 value to something smaller, say 0.2 or 0.3
                self.attack_finished = time + 0.6;
    ...


Now, compile the code, and your grenade launcher will spit grenades out much
faster than usual.  Lob off a few grenades, and wait for the BANG!


Step 3
Extending the concept a little. Why only explode grenades?? Why don't we
explode nearby ammo boxes, rockets, etc as well! Well why not indeedy! Now
a box of rockets has a classname of "item_rockets", so we need to test the
classname for this as well. But how do we explode the box? Well there are
a number of ways to do this, but there are also a few problems as well. In
deathmatch, you still want the item to respawn after it has been exploded. If
we have a look at items.qc, we can see the way the items pretty much work.

Each item has a different "touch" function. This function checks the "other"
variable, and if it is a player (it could be a rocket/grenade/etc), it
modifies the players health/ammo/weapons/etc, and then dissappears. It is
this disappearance part that we are interested in. Have a look at the end
of the code in the ammo_touch() function near the end of items.qc.
void() ammo_touch={
   ...
   ...
// remove it in single player, or setup for respawning in deathmatch
    self.model=string_null;
    self.solid=SOLID_NOT;
    if(deathmatch==1)
        self.nextthink=time+30;
    self.think=SUB_regen;
    activator=other;
	SUB_UseTargets();				// fire all targets / killtargets
    ...
}


The code changes the items model to null (nothing), changes its solid value
to SOLID_NOT, which means it does not interact with any other objects.  From
defs,qc, the comment is "no interaction with other objects", which means you
can effectively walk through it without any touch/etc functions being called.
It then checks to see what deathmatch mode it is in, and if respawn is set,
it sets the "think" function to SUB_regen.  This simply sets the model back
to the correct one, and sets solid back to SOLID_TRIGGER.  If you are
interested, have a look at it at the top of items.qc.  The SUB_UseTargets()
call does death resolution, etc, etc.  I really don't understand it all that
well, but since we are not doing anything we can safely(?) ignore it.  Now
given, all what we know now, we could write the following code,


//WEAPONS.QC
void() GrenadeExplode={
    local entity ptr;

    T_RadiusDamage(self, self.owner, 120, world);
    WriteByte(MSG_BROADCAST, SVC_TEMPENTITY);
    WriteByte(MSG_BROADCAST, TE_EXPLOSION);
    WriteByte(MSG_BROADCAST, self.origin_x);
    WriteByte(MSG_BROADCAST, self.origin_y);
    WriteByte(MSG_BROADCAST, self.origin_z);

//  call "findradius" to return list of entity's found
//  (I have chosen to use a radius of 120 because that is what is used by
//   the grenades in the T_RadiusDamage() function call above.  Feel free
//   to alter it to taste)
    ptr=findradius(self.origin, 120);

//  set this grenades lip value
    ptr.lip=1234.0;

//  The list is returned to us in a linked-list fashion.  Put simply; if there
//  are no entities found, ptr is zero (or false). If there is at least one
//  entity found, ptr will refer to it.
    while(ptr){
//      here we check to see if the entity is of class "grenade", and that
//      we haven't found a grenade with a set lip value.  Notice that we
//      don't check for self any more?  We don't need to anymore because we
//      set our own lip value before entering this section of code.
        if((ptr.classname=="grenade") && (ptr.lip!=1234.0)){
//          if both these conditions are true, set the nextthink time of the
//          entity to the current game time, and make sure the think function
//          of the entity is referring to the GrenadeExplode() function.
            ptr.nextthink=time;
            ptr.think=GrenadeExplode;
//      here we check to see if we have found a item_rockets entity.  We also
//      check the solid value or we would keep finding the same ammo box more
//      than once.
        }else if((ptr.classname=="item_rockets") && (ptr.solid==SOLID_TRIGGER)){
//          this code simply make the box invisible.
            ptr.model=string_null;
            ptr.solid=SOLID_NOT;
            if(deathmatch==1)
                ptr.nextthink=time+30;
            ptr.think=SUB_regen;
        }
//      By using the "chain" attribute of the entity we can find the next
//      entity in the list of entity's found by the findradius() function.
        ptr=ptr.chain;
    }

//  finally become an explosion, just as before
    BecomeExplosion();
};


Ok, so we are part way there, but we still don't get an explosion.  The box
simply dissappears.  Ok, well hold on to your hats.  What we are going to do
is dynamically create a grenade at the same location as the ammo box, and
set it to detonate instantly!  This allows us to keep all our altered code
in the same place.  Now, if we have a look at the W_FireGrenade() function,


void() W_FireGrenade={
    local   entity missile, mpuff;
        
    ...
    missile=spawn ();
    missile.owner=self;
    missile.movetype=MOVETYPE_BOUNCE;
    missile.solid=SOLID_BBOX;
    missile.classname="grenade";
    ...
    missile.touch=GrenadeTouch;

// set missile duration
    missile.nextthink=time+2.5;
    missile.think=GrenadeExplode;
    setmodel (missile, "progs/grenade.mdl");
    setsize (missile, '0 0 0', '0 0 0');            
    setorigin (missile, self.origin);
};


So what happens is that a new entity is spawned.  We set up its attributes
like owner, movetype, model, etc, set its nexthink time, and think function.
Now, some of this we can ignore. What we are interested is the following,

    missile.classname="grenade";
    missile.movetype=MOVETYPE_NONE;  //straight from defs.qc
    missile.solid=SOLID_NOT;
    missile.nextthink=time;
    missile.think=GrenadeExplode;
    setmodel(missile, "progs/grenade.mdl");
    setsize(missile, '0 0 0', '0 0 0');
    setorigin(missile, self.origin);

This gives us the barest minimum to create a grenade set to explode virtually
straight away at the location of the ammo box.  I set movetype to MOVETYPE_NONE
because it won't be moving.  It also allows us to ignore initialising the
velocity.  Similarly, setting solid to SOLID_NOT guarentees that we don't
have to set the touch function.  Now, if we embed this code fragment into our
function we get,


//WEAPONS.QC
void() GrenadeExplode={
    local entity ptr, grenade;

    T_RadiusDamage(self, self.owner, 120, world);
    WriteByte(MSG_BROADCAST, SVC_TEMPENTITY);
    WriteByte(MSG_BROADCAST, TE_EXPLOSION);
    WriteByte(MSG_BROADCAST, self.origin_x);
    WriteByte(MSG_BROADCAST, self.origin_y);
    WriteByte(MSG_BROADCAST, self.origin_z);

//  call "findradius" to return list of entity's found
//  (I have chosen to use a radius of 120 because that is what is used by
//   the grenades in the T_RadiusDamage() function call above.  Feel free
//   to alter it to taste)
    ptr=findradius(self.origin, 120);

//  set this grenades lip value
    ptr.lip=1234.0;

//  The list is returned to us in a linked-list fashion.  Put simply; if there
//  are no entities found, ptr is zero (or false). If there is at least one
//  entity found, ptr will refer to it.
    while(ptr){
//      here we check to see if the entity is of class "grenade", and that
//      we haven't found a grenade with a set lip value.  Notice that we
//      don't check for self any more?  We don't need to anymore because we
//      set our own lip value before entering this section of code.
        if((ptr.classname=="grenade") && (ptr.lip!=1234.0)){
//          if both these conditions are true, set the nextthink time of the
//          entity to the current game time, and make sure the think function
//          of the entity is referring to the GrenadeExplode() function.
            ptr.nextthink=time;
            ptr.think=GrenadeExplode;
//      here we check to see if we have found a item_rockets entity.  We also
//      check the solid value or we would keep finding the same ammo box more
//      than once.
        }else if((ptr.classname=="item_rockets") && (ptr.solid==SOLID_TRIGGER)){
//          this code simply make the box invisible.
            ptr.model=string_null;
            ptr.solid=SOLID_NOT;
            if(deathmatch==1)
                ptr.nextthink=time+30;
            ptr.think=SUB_regen;
//          dynamically create a new entity.  Initialise the grenades attributes
            grenade=spawn();
            grenade.classname="grenade";
            grenade.movetype=MOVETYPE_NONE;  //static movement
            grenade.solid=SOLID_NOT; //don't want it to interact
            grenade.nextthink=time; //want it to explode NOW! (almost)
            grenade.think=GrenadeExplode;
            setmodel(grenade, "progs/grenade.mdl"); //set model
            setsize(grenade, '0 0 0', '0 0 0'); //set size (ignored)
            setorigin(grenade, ptr.origin); //set location of grenade
        }
//      By using the "chain" attribute of the entity we can find the next
//      entity in the list of entity's found by the findradius() function.
        ptr=ptr.chain;
    }

//  finally become an explosion, just as before
    BecomeExplosion();
};



Step 4
So compile this, and you will now be able to explode ammo boxes! Now, onto
rockets! This is where it gets a little messier, but shit happens.

< to be continued >

Tutorial HTMLized by Adrian "Mr. Pink" Finol