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 // #define PROTOCOL_VERSION 15 #define PROTOCOL_VERSION 16 #define OLDCOORDLIMIT 4095 int roqprotocol = 0; for (i=0 ; i<3 ; i++) pos[i] = MSG_ReadCoord (); for (i=0 ; i<3 ; i++) { if(roqprotocol == PROTOCOL_VERSION) pos[i] = MSG_ReadLong (); else pos[i] = MSG_ReadCoord (); } // if (i != PROTOCOL_VERSION) { Con_Printf ("Server returned version %i, not %i\n", i, PROTOCOL_VERSION); return; } roqprotocol = i; if (i > PROTOCOL_VERSION) { Con_Printf ("Server returned version %i, not %i\n", i, PROTOCOL_VERSION); return; } if (bits & U_ORIGIN1) ent->msg_origins[0][0] = MSG_ReadCoord (); if (bits & U_ORIGIN1) { if(roqprotocol == PROTOCOL_VERSION) ent->msg_origins[0][0] = MSG_ReadLong (); else ent->msg_origins[0][0] = MSG_ReadCoord (); } if (bits & U_ORIGIN1) ent->msg_origins[0][0] = MSG_ReadCoord (); if (bits & U_ORIGIN2) { if(roqprotocol == PROTOCOL_VERSION) ent->msg_origins[0][1] = MSG_ReadLong (); else ent->msg_origins[0][1] = MSG_ReadCoord (); } if (bits & U_ORIGIN1) ent->msg_origins[0][0] = MSG_ReadCoord (); if (bits & U_ORIGIN3) { if(roqprotocol == PROTOCOL_VERSION) ent->msg_origins[0][2] = MSG_ReadLong (); else ent->msg_origins[0][2] = MSG_ReadCoord (); } 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 (); 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 (); } case TE_WIZSPIKE: // spike hitting wall pos[0] = MSG_ReadCoord (); pos[1] = MSG_ReadCoord (); pos[2] = MSG_ReadCoord (); 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 (); } Now, open r_part.c and find R_ParseParticleEffect(). Locate the following code: for (i=0 ; i<3 ; i++) org[i] = MSG_ReadCoord (); for (i=0 ; i<3 ; i++) { if(roqprotocol == PROTOCOL_VERSION) org[i] = MSG_ReadLong (); else org[i] = MSG_ReadCoord (); } armor = MSG_ReadByte (); blood = MSG_ReadByte (); for (i=0 ; i<3 ; i++) from[i] = MSG_ReadCoord (); 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 (); } MSG_WriteCoord (&sv.datagram, org[0]); MSG_WriteCoord (&sv.datagram, org[1]); MSG_WriteCoord (&sv.datagram, org[2]); MSG_WriteLong (&sv.datagram, (int)org[0]); MSG_WriteLong (&sv.datagram, (int)org[1]); MSG_WriteLong (&sv.datagram, (int)org[2]); MSG_WriteCoord (&sv.datagram, entity->v.origin[i]+0.5*(entity->v.mins[i]+entity->v.maxs[i])); MSG_WriteLong (&sv.datagram, (int)entity->v.origin[i]+0.5*(entity->v.mins[i]+entity->v.maxs[i])); if (bits & U_ORIGIN1) MSG_WriteCoord (msg, ent->v.origin[0]); if (bits & U_ORIGIN1) MSG_WriteLong (msg, (int) ent->v.origin[0]); if (bits & U_ORIGIN2) MSG_WriteCoord (msg, ent->v.origin[1]); if (bits & U_ORIGIN2) MSG_WriteLong (msg, (int) ent->v.origin[1]); if (bits & U_ORIGIN3) MSG_WriteCoord (msg, ent->v.origin[2]); if (bits & U_ORIGIN3) MSG_WriteLong (msg, (int) ent->v.origin[2]); MSG_WriteCoord (msg, other->v.origin[i] + 0.5*(other->v.mins[i] + other->v.maxs[i])); MSG_WriteLong (msg, other->v.origin[i] + 0.5*(other->v.mins[i] + other->v.maxs[i])); for (i=0 ; i<3 ; i++) { MSG_WriteCoord(&sv.signon, svent->baseline.origin[i]); MSG_WriteAngle(&sv.signon, svent->baseline.angles[i]); } 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); 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); (...) 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); }; 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); }; 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); (...) }; 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); }; 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); (...) 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); (...) 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. |