Armor Piercing Rails

Do you get frustrated when the enemy you’ve got aimed up for a rail in the head suddenly ducks behind a pillar ?? Let’s teach the chicken-shit a lesson.

If you like, go and have a read about Vectors. (Like it or not, a good understanding of vector mathematics is essential to quake coding).

1. HOW DO SLUGS WORK ?

Let’s find the weapon_railgun_fire function at line 334 in g_weapon.c :

/*
=================
weapon_railgun_fire
=================
*/
#define	MAX_RAIL_HITS	4
void weapon_railgun_fire (gentity_t *ent) {
	vec3_t		end;
	trace_t		trace;
	gentity_t	*tent;
	gentity_t	*traceEnt;
	int			damage;
	int			radiusDamage;
	int			i;
	int			hits;
	int			unlinked;
	gentity_t	*unlinkedEntities[MAX_RAIL_HITS];

	damage = 100 * s_quadFactor;
	radiusDamage = 30 * s_quadFactor;

	VectorMA (muzzle, 8192, forward, end);

	// trace only against the solids, so the railgun will go through people
	unlinked = 0;
	hits = 0;
	do {
		trap_Trace (&trace, muzzle, NULL, NULL, end, ent->s.number, MASK_SHOT );
		if ( trace.entityNum >= ENTITYNUM_MAX_NORMAL ) {
			break;
		}
		traceEnt = &g_entities[ trace.entityNum ];
		if ( traceEnt->takedamage ) {
			if( LogAccuracyHit( traceEnt, ent ) ) {
				hits++;
			}
			G_Damage (traceEnt, ent, ent, forward, trace.endpos, damage, 0,
			MOD_RAILGUN);
		}
		if ( trace.contents & CONTENTS_SOLID ) {
			break;		// we hit something solid enough to stop the beam
		}
		// unlink this entity, so the next trace will go past it
		trap_UnlinkEntity( traceEnt );
		unlinkedEntities[unlinked] = traceEnt;
		unlinked++;
	} while ( unlinked < MAX_RAIL_HITS );

	// link back in any entities we unlinked
	for ( i = 0 ; i < unlinked ; i++ ) {
		trap_LinkEntity( unlinkedEntities[i] );
	}

	// the final trace endpos will be the terminal point of the rail trail

	// snap the endpos to integers to save net bandwidth, but nudged towards the line
	SnapVectorTowards( trace.endpos, muzzle );

	// send railgun beam effect
	tent = G_TempEntity( trace.endpos, EV_RAILTRAIL );

	// set player number for custom colors on the railtrail
	tent->s.clientNum = ent->s.clientNum;

	VectorCopy( muzzle, tent->s.origin2 );
	// move origin a bit to come closer to the drawn gun muzzle
	VectorMA( tent->s.origin2, 4, right, tent->s.origin2 );
	VectorMA( tent->s.origin2, -1, up, tent->s.origin2 );

	// no explosion at end if SURF_NOIMPACT, but still make the trail
	if ( trace.surfaceFlags & SURF_NOIMPACT ) {
		tent->s.eventParm = 255;	// don't make the explosion at the end
	} else {
		tent->s.eventParm = DirToByte( trace.plane.normal );
	}
	tent->s.clientNum = ent->s.clientNum;

	// give the shooter a reward sound if they have made two railgun hits in a row
	if ( hits == 0 ) {
		// complete miss
		ent->client->accurateCount = 0;
	} else {
		// check for "impressive" reward sound
		ent->client->accurateCount += hits;
		if ( ent->client->accurateCount >= 2 ) {
			ent->client->accurateCount -= 2;
			ent->client->ps.persistant[PERS_REWARD_COUNT]++;
			ent->client->ps.persistant[PERS_REWARD] = REWARD_IMPRESSIVE;
			ent->client->ps.persistant[PERS_IMPRESSIVE_COUNT]++;
			// add the sprite over the player's head
			ent->client->ps.eFlags &= ~(EF_AWARD_IMPRESSIVE |
			EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET );
			ent->client->ps.eFlags |= EF_AWARD_IMPRESSIVE;
			ent->client->rewardTime = level.time + REWARD_SPRITE_TIME;
		}
		ent->client->ps.persistant[PERS_ACCURACY_HITS]++;
	}

}

Firstly, assume that ‚muzzle‘ is a location vector just in front of the firer, and ‚forward‘ is a direction vector pointing in the direction the client is facing.

Ok, there’s a loop here that basically says do { trace the slug until you hit something; if the slug hits a wall, exit the loop; repeat; }. The do { } simply repeats everything in the curly brackets until the code break’s out. Inside this loop, the first line calls the function trap_Trace(blah blah blah) – this traces a line through space from the origin (muzzle) in the direction of the rail (forward) for a maximum distance of 8192 units. If we hit something, trace returns information about what we hit.

If the slug hits nothing, the do loop simply exits. If the slug hits something damageable ( if (traceEnt->takedamage), e.g. a player or a button ) then the target is damaged (with G_Damage). If the slug hits a wall or the ‚edge of the world‘ ( if ( trace.entityNum >= ENTITYNUM_MAX_NORMAL ) ) then the loop exits. We want to change this !!

2. FIRE THROUGH WALLS

First we need to a new vector variable (directly below the other variables, near the top of the function). After line 344, add :

void weapon_railgun_fire (gentity_t *ent) {
	vec3_t		end;
	trace_t		trace;
	gentity_t	*tent;
	gentity_t	*traceEnt;
	int			damage;
	int			radiusDamage;
	int			i;
	int			hits;
	int			unlinked;
	gentity_t	*unlinkedEntities[MAX_RAIL_HITS];
	vec3_t		tracefrom;	// SUM

A few lines down we can see a ‚VectorMA‘ function call. This creates an ‚end‘ vector that is 8192 units ‚forward‘ of ‚muzzle‘ (the startpoint for the rail). We need to make a copy of ‚muzzle‘ in ‚tracefrom‘, so add a line so that your code looks like this :

	damage = 100 * s_quadFactor;
	radiusDamage = 30 * s_quadFactor;

	VectorMA (muzzle, 8192, forward, end);
	VectorCopy (muzzle, tracefrom);

Next, let’s change the trap_Trace function call so that the railgun is traced from ‚tracefrom‘ (instead of muzzle)… there’s a good reason for this, read on.

	// trace only against the solids, so the railgun will go through people
	unlinked = 0;
	hits = 0;
	do {
		trap_Trace (&trace, tracefrom, NULL, NULL, end, ent->s.number, MASK_SHOT );

Next, we want to change the behaviour of the if ( trace.entityNum >= ENTITYNUM_MAX_NORMAL ) { … } block. This if statement detects if our rail slug runs into a ’solid‘ (e.g. a wall or the sky). What do we want to change ? Well, instead of simply ‚breaking‘ out of the do loop (and marking the endpoint for our slug), we want the slug to keep going through walls. Of course, we still want the slug to stop when we hit the sky. The code for this is :

		if ( trace.entityNum >= ENTITYNUM_MAX_NORMAL ) {
			// SUM break if we hit the sky
			if (trace.surfaceFlags & SURF_SKY)
				break;

			// Hypo: break if we traversed length of vector tracefrom
			if (trace.fraction == 1.0)
				break;

			// otherwise continue tracing thru walls
			VectorMA (trace.endpos,1,forward,tracefrom);
			continue;
		}
		traceEnt = &g_entities[ trace.entityNum ];

In other words, if we hit the sky (check the surfaceFlags), stop. If we’ve travelled the full length of the vector then we also need to stop (there’s no guarantee we’ll ever hit the sky.) Otherwise we’ve hit a solid wall. We set our new ‚tracefrom‘ position 1 unit forward of the impact point (which effectively tunnels through the wall). The loop then repeats – continue; takes us back to the top of the ‚do‘ loop.

3. CAN EVERYONE SEE IT ??

Thanks to WarZone for this section : as it is, the railgun trail is possibly not seen by people far away on the level. The little addition below makes sure that the rail trail entity is ‚broadcast‘ to everyone (not just those in the vicinity of the firer). This makes sense, it would be stupid to have a railgun fire through a wall and frag someone if the victim couldn’t see the rail trail !

	// no explosion at end if SURF_NOIMPACT, but still make the trail
	if ( trace.surfaceFlags & SURF_NOIMPACT ) {
		tent->s.eventParm = 255;	// don't make the explosion at the end
	} else {
		tent->s.eventParm = DirToByte( trace.plane.normal );
	}
	tent->s.clientNum = ent->s.clientNum;

	//send the effect to everyone since it tunnels through walls
	tent->r.svFlags |= SVF_BROADCAST;

I’ve asked Mark (WarZone) to write an article for us, explaining how ‚broadcasting‘ works and the deeper subject of what goes on in the quake network code. Look out for some more great stuff from him!

Ok, the rest of the function remains the same, and our railgun should fire through walls.

Fire up q3tourney6 and see if you can give Xearo a surprise with your new toy on Nightmare!. „He is got t’go DOWN, man“.

by SumFuka