Tutorial *127*

Editors note: this tutorial seems to have some problems. If you want to implement the extended coordinate system into your quake engine, i suggest you read trough this one, but use the alternative version (By JTR) for the actual code.

First of all, I apologize for my bad English. :)

One of the major limitations in the Quake 1 engine is the coordinate system adopted by the programmers. The current limit of -4095 to +4095 for any value in the X, Y or Z axis must be credited to a requirement of the network protocol to shrink at the maximum the size of the data to be send and received over the network (mostly Internet games, where an update entity message could grow nearly 30%-40% with the use of long integers, lagging almost any modem connection).

This tutorial is aimed to engines and mods where the data packet size is not a critical concern (ie, single player and local network modes), since the ammount of information sent to the client is much greater. Also, this tutorial does not optimize the render engine to huge open areas such found in the Quake 3: Arena terrain maps. It's merely allows the mapper to add more rooms in your maps, and to make these rooms (not too much!) more longer or taller than currently, without worrying about breaking the old limits. Keeping this in mind, let's see the code itself. This tutorial changes both the executable and the progs.dat for a complete implementation of the temporary entities.

This implementation is retrocompatible with the original Quake 1 protocol, which means that the new client can connect and play normally with the legacy servers. However, legacy clients cannot connect to changed servers (they wouldn't be able to correctly render things in a bigger map, anyway, so there's no problem on it). First, let's open protocol.h and find the following line:



	#define	PROTOCOL_VERSION	15

Let's replace with this code section:




	// #define	PROTOCOL_VERSION	15

	#define	PROTOCOL_VERSION	16

	#define	OLDCOORDLIMIT	4095

	int	roqprotocol = 0;

Now, let's open cl_parse.c. Locate the CL_ParseStartSoundPacket() function, and inside this, find these code lines:


	for (i=0 ; i<3 ; i++)

			pos[i] = MSG_ReadCoord ();

Change to:


	for (i=0 ; i<3 ; i++)	{

		if(roqprotocol == PROTOCOL_VERSION)

			pos[i] = MSG_ReadLong ();

		else

			pos[i] = MSG_ReadCoord ();

	}

Next, find the CL_ParseServerInfo() function. Replace:


//	if (i != PROTOCOL_VERSION)

	{

		Con_Printf ("Server returned version %i, not %i\n", i, PROTOCOL_VERSION);

		return;

	}

with:


	roqprotocol = i;

	if (i > PROTOCOL_VERSION)

	{

		Con_Printf ("Server returned version %i, not %i\n", i, PROTOCOL_VERSION);

		return;

	}

This make our client retrocompatible with the 0x0f protocol version. The next function is CL_ParseUpdate(), and that's the most critical of all, so pay attention. Find the following lines:


	if (bits & U_ORIGIN1)

		ent->msg_origins[0][0] = MSG_ReadCoord ();

And replace with:


	if (bits & U_ORIGIN1)	{

		if(roqprotocol == PROTOCOL_VERSION)

			ent->msg_origins[0][0] = MSG_ReadLong ();

		else

			ent->msg_origins[0][0] = MSG_ReadCoord ();

	}

The same goes below:


	if (bits & U_ORIGIN1)

		ent->msg_origins[0][0] = MSG_ReadCoord ();

Replace with:


	if (bits & U_ORIGIN2)	{

		if(roqprotocol == PROTOCOL_VERSION)

			ent->msg_origins[0][1] = MSG_ReadLong ();

		else

			ent->msg_origins[0][1] = MSG_ReadCoord ();

	}

Again:


	if (bits & U_ORIGIN1)

		ent->msg_origins[0][0] = MSG_ReadCoord ();

Replace with:


	if (bits & U_ORIGIN3)	{

		if(roqprotocol == PROTOCOL_VERSION)

			ent->msg_origins[0][2] = MSG_ReadLong ();

		else

			ent->msg_origins[0][2] = MSG_ReadCoord ();

	}

And that's all for cl_parse.c. But wait, there's more to do. Open cl_tent.c and find the CL_ParseBeam() function. At the start of the function, we can see the original code:


	ent = MSG_ReadShort ();

	start[0] = MSG_ReadCoord ();

	start[1] = MSG_ReadCoord ();

	start[2] = MSG_ReadCoord ();

	end[0] = MSG_ReadCoord ();

	end[1] = MSG_ReadCoord ();

	end[2] = MSG_ReadCoord ();

We will replace with:


	ent = MSG_ReadShort ();

	if(roqprotocol == PROTOCOL_VERSION)	{

		start[0] = MSG_ReadLong ();

		start[1] = MSG_ReadLong ();

		start[2] = MSG_ReadLong ();

		end[0] = MSG_ReadLong ();

		end[1] = MSG_ReadLong ();

		end[2] = MSG_ReadLong ();

	}

	else	{

		start[0] = MSG_ReadCoord ();

		start[1] = MSG_ReadCoord ();

		start[2] = MSG_ReadCoord ();

		end[0] = MSG_ReadCoord ();

		end[1] = MSG_ReadCoord ();

		end[2] = MSG_ReadCoord ();

	}

Next, find CL_ParseTEnt(). Locate this code:


	case TE_WIZSPIKE:			// spike hitting wall

		pos[0] = MSG_ReadCoord ();

		pos[1] = MSG_ReadCoord ();

		pos[2] = MSG_ReadCoord ();

Replace with:


	case TE_WIZSPIKE:			// spike hitting wall

		if(roqprotocol != PROTOCOL_VERSION)	{

			pos[0] = MSG_ReadCoord ();

			pos[1] = MSG_ReadCoord ();

			pos[2] = MSG_ReadCoord ();

		}

		else	{

			pos[0] = MSG_ReadLong ();

			pos[1] = MSG_ReadLong ();

			pos[2] = MSG_ReadLong ();

		}

Repeat the step above for TE_KNIGHTSPIKE, TE_SPIKE, TE_SUPERSPIKE, TE_GUNSHOT, TE_EXPLOSION, TE_TAREXPLOSION, TE_LAVASPLASH, TE_TELEPORT, and TE_EXPLOSION2. Unless you've previously enabled it, ignore the remaining entity types, since they belong to Quake2 code.

Now, open r_part.c and find R_ParseParticleEffect(). Locate the following code:



	for (i=0 ; i<3 ; i++)

		org[i] = MSG_ReadCoord ();

Replace with:


	for (i=0 ; i<3 ; i++)	{

		if(roqprotocol == PROTOCOL_VERSION)

			org[i] = MSG_ReadLong ();

		else

			org[i] = MSG_ReadCoord ();

	}

We're almost done for the client side. Open view.c and find V_ParseDamage(). The code below:


	armor = MSG_ReadByte ();

	blood = MSG_ReadByte ();

	for (i=0 ; i<3 ; i++)

		from[i] = MSG_ReadCoord ();

must be replaced with:


	armor = MSG_ReadByte ();

	blood = MSG_ReadByte ();

	for (i=0 ; i<3 ; i++)	{

		if(roqprotocol == PROTOCOL_VERSION)

			from[i] = MSG_ReadLong ();

		else

			from[i] = MSG_ReadCoord ();

	}

Now, let's change the server side of the protocol. open up sv_main.c and find this code inside SV_StartParticle():


	MSG_WriteCoord (&sv.datagram, org[0]);

	MSG_WriteCoord (&sv.datagram, org[1]);

	MSG_WriteCoord (&sv.datagram, org[2]);

Replace with:


	MSG_WriteLong (&sv.datagram, (int)org[0]);

	MSG_WriteLong (&sv.datagram, (int)org[1]);

	MSG_WriteLong (&sv.datagram, (int)org[2]);

The next function is SV_StartSound(). At the end, find this line:


		MSG_WriteCoord (&sv.datagram, entity->v.origin[i]+0.5*(entity->v.mins[i]+entity->v.maxs[i]));

Replace with:


		MSG_WriteLong (&sv.datagram, (int)entity->v.origin[i]+0.5*(entity->v.mins[i]+entity->v.maxs[i]));

Now, find SV_WriteEntitiesToClient(). Locate this code:


		if (bits & U_ORIGIN1)

			MSG_WriteCoord (msg, ent->v.origin[0]);

Change to:


		if (bits & U_ORIGIN1)

				MSG_WriteLong (msg, (int) ent->v.origin[0]);

The same below:


		if (bits & U_ORIGIN2)

			MSG_WriteCoord (msg, ent->v.origin[1]);

Now is:


		if (bits & U_ORIGIN2)

				MSG_WriteLong (msg, (int) ent->v.origin[1]);

The next is:


		if (bits & U_ORIGIN3)

			MSG_WriteCoord (msg, ent->v.origin[2]);

Now the code is:


		if (bits & U_ORIGIN3)

				MSG_WriteLong (msg, (int) ent->v.origin[2]);

We're near to complete the C side of the new protocol. Find SV_WriteClientdataToMessage() and locate the following code:


			MSG_WriteCoord (msg, other->v.origin[i] + 0.5*(other->v.mins[i] + other->v.maxs[i]));

The new code is:


			MSG_WriteLong (msg, other->v.origin[i] + 0.5*(other->v.mins[i] + other->v.maxs[i]));

Find the SV_CreateBaseline() function. Replace:


		for (i=0 ; i<3 ; i++)

		{

			MSG_WriteCoord(&sv.signon, svent->baseline.origin[i]);

			MSG_WriteAngle(&sv.signon, svent->baseline.angles[i]);

		}

with:


		for (i=0 ; i<3 ; i++)

		{

			MSG_WriteLong(&sv.signon, svent->baseline.origin[i]);

			MSG_WriteAngle(&sv.signon, svent->baseline.angles[i]);

		}

And that's all in the C side of the change. Now, QuakeC time. Basically, you must replace all WriteCoord() occurrences with WriteLong() to take use of the extended protocol. So, I just point out the file and the new code to keep the text shorter.

boss.qc:



	void() boss_death9 = [$death9, boss_death10]

{

	sound (self, CHAN_BODY, "boss1/out1.wav", 1, ATTN_NORM);

	WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

	WriteByte (MSG_BROADCAST, TE_LAVASPLASH);

	WriteLong (MSG_BROADCAST, self.origin_x);

	WriteLong (MSG_BROADCAST, self.origin_y);

	WriteLong (MSG_BROADCAST, self.origin_z);

};



	(...)



void() boss_awake =

{

	self.solid = SOLID_SLIDEBOX;

	self.movetype = MOVETYPE_STEP;

	self.takedamage = DAMAGE_NO;



	setmodel (self, "progs/boss.mdl");

	setsize (self, '-128 -128 -24', '128 128 256');



	if (skill == 0)

		self.health = 1;

	else

		self.health = 3;



	self.enemy = activator;



	WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

	WriteByte (MSG_BROADCAST, TE_LAVASPLASH);

	WriteLong (MSG_BROADCAST, self.origin_x);

	WriteLong (MSG_BROADCAST, self.origin_y);

	WriteLong (MSG_BROADCAST, self.origin_z);



	self.yaw_speed = 20;

	boss_rise1 ();

};



	(...)



	WriteByte (MSG_ALL, SVC_TEMPENTITY);

	WriteByte (MSG_ALL, TE_LIGHTNING3);

	WriteEntity (MSG_ALL, world);

	WriteLong (MSG_ALL, p1_x);

	WriteLong (MSG_ALL, p1_y);

	WriteLong (MSG_ALL, p1_z);

	WriteLong (MSG_ALL, p2_x);

	WriteLong (MSG_ALL, p2_y);

	WriteLong (MSG_ALL, p2_z);

ogre.qc:


void() OgreGrenadeExplode =

{

	T_RadiusDamage (self, self.owner, 40, world, "");	// 1998-07-24 Wrong obituary messages fix by Zoid

	sound (self, CHAN_VOICE, "weapons/r_exp3.wav", 1, ATTN_NORM);

	WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

	WriteByte (MSG_BROADCAST, TE_EXPLOSION);

	WriteLong (MSG_BROADCAST, self.origin_x);

	WriteLong (MSG_BROADCAST, self.origin_y);

	WriteLong (MSG_BROADCAST, self.origin_z);





	(...)



shambler.qc:


void() CastLightning =

{

	local	vector	org, dir;



	self.effects = self.effects | EF_MUZZLEFLASH;



	ai_face ();



	org = self.origin + '0 0 40';



	dir = self.enemy.origin + '0 0 16' - org;

	dir = normalize (dir);



	traceline (org, self.origin + dir*600, TRUE, self);



	WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

	WriteByte (MSG_BROADCAST, TE_LIGHTNING1);

	WriteEntity (MSG_BROADCAST, self);

	WriteLong (MSG_BROADCAST, org_x);

	WriteLong (MSG_BROADCAST, org_y);

	WriteLong (MSG_BROADCAST, org_z);

	WriteLong (MSG_BROADCAST, trace_endpos_x);

	WriteLong (MSG_BROADCAST, trace_endpos_y);

	WriteLong (MSG_BROADCAST, trace_endpos_z);



	LightningDamage (org, trace_endpos, self, 10);

};

triggers.qc:


void(vector org) spawn_tfog =

{

	s = spawn ();

	s.origin = org;

	s.nextthink = time + 0.2;

	s.think = play_teleport;



	WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

	WriteByte (MSG_BROADCAST, TE_TELEPORT);

	WriteLong (MSG_BROADCAST, org_x);

	WriteLong (MSG_BROADCAST, org_y);

	WriteLong (MSG_BROADCAST, org_z);

};

weapons.qc:


void() W_FireAxe =

{

	local	vector	source;

	local	vector	org;



	makevectors (self.v_angle);

	source = self.origin + '0 0 16';

	traceline (source, source + v_forward*64, FALSE, self);

	if (trace_fraction == 1.0)

		return;



	org = trace_endpos - v_forward*4;



	if (trace_ent.takedamage)

	{

		trace_ent.axhitme = 1;

		SpawnBlood (org, '0 0 0', 20);

// 1999-02-04 Deathmatch mode 3, 4 and 5 by Zoid/Maddes  start

		if (deathmatch > 3)

			T_Damage (trace_ent, self, self, 75);

		else

// 1999-02-04 Deathmatch mode 3, 4 and 5 by Zoid/Maddes  end

		T_Damage (trace_ent, self, self, 20);

	}

	else

	{	// hit wall

		sound (self, CHAN_WEAPON, "player/axhit2.wav", 1, ATTN_NORM);

		WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

		WriteByte (MSG_BROADCAST, TE_GUNSHOT);

		WriteLong (MSG_BROADCAST, org_x);

		WriteLong (MSG_BROADCAST, org_y);

		WriteLong (MSG_BROADCAST, org_z);

	}

};





	(...)





void(float damage, vector dir) TraceAttack =

{

	local	vector	vel, org;



	vel = normalize(dir + v_up*crandom() + v_right*crandom());

	vel = vel + 2*trace_plane_normal;

	vel = vel * 200;



	org = trace_endpos - dir*4;



	if (trace_ent.takedamage)

	{

		SpawnBlood (org, vel*0.2, damage);

		AddMultiDamage (trace_ent, damage);

	}

	else

	{

		WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

		WriteByte (MSG_BROADCAST, TE_GUNSHOT);

		WriteLong (MSG_BROADCAST, org_x);

		WriteLong (MSG_BROADCAST, org_y);

		WriteLong (MSG_BROADCAST, org_z);

	}

};



	(...)



void() T_MissileTouch =

{



	(...)





	WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

	WriteByte (MSG_BROADCAST, TE_EXPLOSION);

	WriteLong (MSG_BROADCAST, self.origin_x);

	WriteLong (MSG_BROADCAST, self.origin_y);

	WriteLong (MSG_BROADCAST, self.origin_z);



	(...)



};



	(...)



void() W_FireLightning =

{



	(...)





			WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

			WriteByte (MSG_BROADCAST, TE_EXPLOSION);

			WriteLong (MSG_BROADCAST, self.origin_x);

			WriteLong (MSG_BROADCAST, self.origin_y);

			WriteLong (MSG_BROADCAST, self.origin_z);





	(...)





	WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

	WriteByte (MSG_BROADCAST, TE_LIGHTNING2);

	WriteEntity (MSG_BROADCAST, self);

	WriteLong (MSG_BROADCAST, org_x);

	WriteLong (MSG_BROADCAST, org_y);

	WriteLong (MSG_BROADCAST, org_z);

	WriteLong (MSG_BROADCAST, trace_endpos_x);

	WriteLong (MSG_BROADCAST, trace_endpos_y);

	WriteLong (MSG_BROADCAST, trace_endpos_z);



	(...)



};



	(...)



void() GrenadeExplode =

{



	(...)



	WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

	WriteByte (MSG_BROADCAST, TE_EXPLOSION);

	WriteLong (MSG_BROADCAST, self.origin_x);

	WriteLong (MSG_BROADCAST, self.origin_y);

	WriteLong (MSG_BROADCAST, self.origin_z);



	(...)



};



	(...)



void() spike_touch =

{



	(...)



		WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);



		if (self.classname == "wizspike")

			WriteByte (MSG_BROADCAST, TE_WIZSPIKE);

		else if (self.classname == "knightspike")

			WriteByte (MSG_BROADCAST, TE_KNIGHTSPIKE);

		else

			WriteByte (MSG_BROADCAST, TE_SPIKE);

		WriteLong (MSG_BROADCAST, self.origin_x);

		WriteLong (MSG_BROADCAST, self.origin_y);

		WriteLong (MSG_BROADCAST, self.origin_z);



	(...)



};



void() superspike_touch =

{



	(...)



		WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

		WriteByte (MSG_BROADCAST, TE_SUPERSPIKE);

		WriteLong (MSG_BROADCAST, self.origin_x);

		WriteLong (MSG_BROADCAST, self.origin_y);

		WriteLong (MSG_BROADCAST, self.origin_z);



	(...)



};

enforcer.qc:


void() Laser_Touch =

{

	local vector org;



	if (other == self.owner)

		return;		// don't explode on owner



	if (pointcontents(self.origin) == CONTENT_SKY)

	{

		remove(self);

		return;

	}



	sound (self, CHAN_WEAPON, "enforcer/enfstop.wav", 1, ATTN_STATIC);

	org = self.origin - 8*normalize(self.velocity);



	if (other.health)

	{

		SpawnBlood (org, self.velocity*0.2, 15);

		other.deathtype = "laser";	// 1998-07-24 Wrong obituary messages fix by Zoid

		T_Damage (other, self, self.owner, 15);

	}

	else

	{

		WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

		WriteByte (MSG_BROADCAST, TE_GUNSHOT);

		WriteLong (MSG_BROADCAST, org_x);

		WriteLong (MSG_BROADCAST, org_y);

		WriteLong (MSG_BROADCAST, org_z);

	}



	remove(self);

};

oldone.qc:


void() finale_2 =

{

	local vector	o;



	// start a teleport splash inside shub



	o = shub.origin - '0 100 0';

	WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

	WriteByte (MSG_BROADCAST, TE_TELEPORT);

	WriteLong (MSG_BROADCAST, o_x);

	WriteLong (MSG_BROADCAST, o_y);

	WriteLong (MSG_BROADCAST, o_z);



	(...)

shalrath.qc:


void() ShalMissileTouch =

{



	(...)



	WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

	WriteByte (MSG_BROADCAST, TE_EXPLOSION);

	WriteLong (MSG_BROADCAST, self.origin_x);

	WriteLong (MSG_BROADCAST, self.origin_y);

	WriteLong (MSG_BROADCAST, self.origin_z);



	(...)

tarbaby.qc:


void()	tbaby_die2	=[	$exp,		tbaby_run1	]

{

	T_RadiusDamage (self, self, 120, world, "");	// 1998-07-24 Wrong obituary messages fix by Zoid



	sound (self, CHAN_VOICE, "blob/death1.wav", 1, ATTN_NORM);

	self.origin = self.origin - 8*normalize(self.velocity);



	WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);

	WriteByte (MSG_BROADCAST, TE_TAREXPLOSION);

	WriteLong (MSG_BROADCAST, self.origin_x);

	WriteLong (MSG_BROADCAST, self.origin_y);

	WriteLong (MSG_BROADCAST, self.origin_z);



	BecomeExplosion ();

};

And that's all. Compile both progs.dat and quake.exe. Create a really big map (i tested with a ugly bunch of big boxes tied together made by myself), place a few monsters on it(mostly monsters using the temporary entities above), and run both the original quake and our twisted version to observe the differences. I hope this tutorial is useful for TC and PC conversions, but keep in mind that internet play using this version of protocol as is can be impossible for low-bandwith connections. Possible optimizations for the original protocol that are not implemented and that could compensate the extra size of updates are: create client-side support for a variant of MOVETYPE_BOUNCE entities (which could replace the current implementation using the same behaviour for grenades and gibs, for example); some mechanism to avoid resending info about non-updated entities (if the entity does not change, why sending over and over the same info about it ?).

Some additional tips about big maps: may be desirable, in wide open rooms, to implement fog support in order to compensate the PVS clipping in the rendering engine (ie, enable fog in your engine if not yet, set the fog_start between 1000 and 1500 game units, and fog_end at 4096 or more). Also, I observed that the engine fps falls a lot when skyboxes are enabled, mostly in wide open areas.

I would like to thanks to the guys at quakesource list which helped me with a silly logic flaw in my original code, specially J.P> Grossman.



 
Not logged in
Sign up
Login:
Passwd: