Back Last changes: 2001-11-25 Contact Maddes

QuakeC Workarounds

QuakeC categories: Infos & Hints Fixes Workarounds Enhancements
(discontinued, check out URQP source for more information)

Index

Workarounds for Quake[C] bugs

"ftos()" workaround
"ftos()" returns strings with leading spaces when the converted value has decimals, also "ftos()" cuts if off after the first decimal (e.g. "  0.9" for 0.93).
You can check for this behaviour and set a flag depending on the result, so you can recognize it in your modifications.
Define a float variable for this in "Defs.qc", I prefer "correct_ftos" because the name says it all.
Put the following code at the end of "worldspawn()" in "World.qc", so each time a new map is loaded the quake.exe will be tested for this bug.
You should also round/floor/ceil values before printing if necessary, otherwise you get weird looking displays (like in CTF: "1.193546 seconds left").
"World.qc"

void() worldspawn =
{
...
// 1997-12-25 ftos() workaround by Maddes  start
	local string test_ftos;

	test_ftos = ftos(0.93);			// convert to string
	dprint("0.93 was converted by ftos() to \"");
	dprint(test_ftos);
	dprint("\"\n");

	if (test_ftos == "0.93" || test_ftos == " 0.93" || test_ftos == "  0.93")
	{
		dprint("ftos() IS WORKING CORRECTLY! Yippie!\n");
		correct_ftos = 1;
	}
	else
	{
		dprint("ftos() delivers wrong strings. Take care! :(\n");
		correct_ftos = 0;
	}
// 1997-12-25 ftos() workaround by Maddes  end
};

"cvar_set()" workaround
"cvar_set()" sets a cvar to zero when the given string has leading spaces. In conjunction with the "ftos() bug" you can not save a cvar, change it for a reason and restore its previous value afterwards.
You can check for this behaviour and set a flag depending on the result, so you can recognize it in your modifications.
Define a float variable for this in "Defs.qc", I prefer "correct_cvars" because the name says it all.
Make sure you test this behaviour with a server variable which will be changed by yourself, I prefer "sv_aim" because I always use my "Auto-aim toggle" in patches (see below). Also you have to make sure you test with a decimal value.
Put the following code at the end of "worldspawn()" in "World.qc", so each time a new map is loaded the quake.exe will be tested for this bug.

"World.qc"

void() worldspawn =
{
...
// 1997-12-25 cvar_set() workaround by Maddes  start
	local float save_aim;
	local float test_aim_1;
	local float test_aim_2;
	local string set_aim;

	save_aim = cvar("sv_aim");		// save sv_aim as float

	cvar_set("sv_aim", "0.93");		// set sv_aim for testing
	test_aim_1 = cvar("sv_aim");		// get sv_aim as float
	set_aim = ftos(test_aim_1);		// convert to string
	cvar_set("sv_aim", set_aim);		// reset sv_aim with string
	test_aim_2 = cvar("sv_aim");		// get sv_aim again as float
	dprint("\"sv_aim\" was set by cvar_set() to \"");
	dprint(ftos(test_aim_2));
	dprint("\"\n");

	if (test_aim_2 == test_aim_1)		// still the same?
	{
		dprint("Saving and recalling cvars IS WORKING! Yippie!\n");
		set_aim = ftos(save_aim);
		cvar_set("sv_aim", set_aim);	// reset to previous value
		correct_cvars = 2;
	}
	else if (test_aim_2 == 0.9)		// only ftos() error?
	{
		dprint("Saving and recalling cvars is NEARLY working! Better than nothing!\n");
		set_aim = ftos(save_aim);
		cvar_set("sv_aim", set_aim);	// reset to previous value
		correct_cvars = 1;
	}
	else
	{
		dprint("Have to use fix values for setting cvars! :(\n");
		cvar_set("sv_aim", "0.93");		// set to id's standard
		correct_cvars = 0;
	}
// 1997-12-25 cvar_set() workaround by Maddes  end
};
These workarounds will help you to make your patch work with and without a bugfixed quake.exe, but don't forget to code for both situations.
I also put those workarounds in id's original zip archive, but decide on your own.

Incorrect "trigger_teleport" workaround
As there was an incorrect "trigger_teleport" entity left in E4M5 (above the exit to the secret level), servers crash when a player touches it (see left picture below).
To make triggers visible, go into "Subs.qc" and comment out "self.modelindex = 0;" and "self.model = "";" in the "InitTrigger()" function. At the secret exit of E4M5 you'll see it (see right picture below).
You wonder how to get there? - Use noclip, a rocket/grenade jump or the grappling hook. Note that it's only touchable from below, the ring of the teleporter protects it from being touched from above. You can also disable "trigger_changelevel" entities by placing a "return" statement at the begin of "trigger_changelevel()" in "Client.qc", which will let you swim in the teleporter.

this happens without the workaround - click to view in full size position of incorrect "trigger_teleport" entity in START - click to view in full size position of incorrect "trigger_teleport" entity in E4M5 - click to view in full size position of incorrect "trigger_teleport" entity in E4M8 - click to view in full size

The server crashes only because of the "objerror()" function call. This function stops the server, displays a error message and prints the edict data of "self".
If an incorrect "trigger_teleport" entity is detected, all information can also be displayed with normal functions (developer messages and print edict). You should leave the "teleport_touch()" function after this, because you never know where the player will land. Maybe he will be teleported out of the map, or get stuck in a wall, floor or ceiling.
You may also delete the entity, if it's not needed or not corrected by your addon, and that the addon does not create (incorrect) "trigger_teleport" entities on the fly.
Fixes for START, E4M5 and E4M8 are available for download

"Triggers.qc"

void() teleport_touch =
{
...
	SUB_UseTargets ();

// 1998-07-07 incorrect teleport workaround by Maddes  start
/*
// put a tfog where the player was
	spawn_tfog (other.origin);
*/
// 1998-07-07 incorrect teleport workaround by Maddes  end

	t = find (world, targetname, self.target);
// 1998-07-07 incorrect teleport workaround by Maddes  start
	if ( (!t)
	|| (t.classname != "info_teleport_destination") && (t.classname != "misc_teleporttrain"))
//		objerror ("couldn't find target");
	{
		dprint ("OBJECT ERROR in teleport_touch():\n");
		dprint ("couldn't find target\n");
//		eprint (self);	// not a developer message :(
//		remove (self);	// be careful it's not necessary
//		dprint ("OBJECT REMOVED\n");
		return;		// otherwise player lands anywhere/gets stucked
	}

// put a tfog where the player was
	spawn_tfog (other.origin);
// 1998-07-07 incorrect teleport workaround by Maddes  end

// spawn a tfog flash in front of the destination
	makevectors (t.mangle);
	org = t.origin + 32 * v_forward;
...
};
An except from the map part of E4M5's bsp file shows that the target is not a "info_teleport_destination" enitity.
{
"classname" "trigger_teleport"
"target" "t178"
"model" "*95"
}
{
"classname" "info_null"
"origin" "796 2212 260"
"targetname" "t178"
}

"T_RadiusDamage()" workaround 1/2 by Robert Field
To avoid that other players do not get hurt by splash damage, when the attacker dies through it, you have to hurt the attacker after all other players.
In "Combat.qc" find the "T_RadiusDamage" function and change the code as follwoing:

"Combat.qc"

void(entity inflictor, entity attacker, float damage, entity ignore) T_RadiusDamage =
{
	local	float 	points;
	local	entity	head;
	local	vector	org;
// 1998-07-07 T_RadiusDamage workaround by Robert Field  start
	local	float	attacker_damaged;
	local	float	attacker_points;
// 1998-07-07 T_RadiusDamage workaround by Robert Field  end

	head = findradius(inflictor.origin, damage+40);

	while (head)
	{
		if (head != ignore)
		{
			if (head.takedamage)
			{
				org = head.origin + (head.mins + head.maxs)*0.5;
				points = 0.5*vlen (inflictor.origin - org);
				if (points < 0)
					points = 0;
				points = damage - points;
// 1998-07-07 T_RadiusDamage workaround by Robert Field  start
/*
				if (head == attacker)
					points = points * 0.5;
*/
// 1998-07-07 T_RadiusDamage workaround by Robert Field  end
				if (points > 0)
				{
					if (CanDamage (head, inflictor))
					{
// 1998-07-07 T_RadiusDamage workaround by Robert Field  start
						if (head != attacker)
						{
// 1998-07-07 T_RadiusDamage workaround by Robert Field  end
						// shambler takes half damage from all explosions
						if (head.classname == "monster_shambler")
							T_Damage (head, inflictor, attacker, points*0.5);
						else
							T_Damage (head, inflictor, attacker, points);
// 1998-07-07 T_RadiusDamage workaround by Robert Field  start
						}
						else
						{
							attacker_damaged = TRUE;
							attacker_points = points * 0.5;
						}
// 1998-07-07 T_RadiusDamage workaround by Robert Field  end
					}
				}
			}
		}
		head = head.chain;
	}

// 1998-07-07 T_RadiusDamage workaround by Robert Field  start
	if (attacker_damaged)
		T_Damage (attacker, inflictor, attacker, attacker_points);
// 1998-07-07 T_RadiusDamage workaround by Robert Field  end
};

"T_RadiusDamage()" workaround 2/2 by Patrick Martin
Everything which causes radius damage when dying (e.g. box explosions) must call "T_RadiusDamage()" in the second death frame or later, to avoid recursive calls.
Only the boxes call "T_RadiusDamage()" immediately. The tarbaby is working 100%.
Let the boxes explode and hurt others in the next frame:

"Misc.qc"

// 1998-08-08 T_RadiusDamage workaround by Patrick Martin  start
void() barrel_damage =
{
	T_RadiusDamage (self, self, 160, world, "");	// 1998-07-24 Wrong obituary messages fix by Zoid
	sound (self, CHAN_VOICE, "weapons/r_exp3.wav", 1, ATTN_NORM);
	particle (self.origin, '0 0 0', 75, 255);

	self.origin_z = self.origin_z + 32;
	BecomeExplosion ();
};
// 1998-08-08 T_RadiusDamage workaround by Patrick Martin  end

void() barrel_explode =
{
	self.takedamage = DAMAGE_NO;
	self.classname = "explo_box";

// 1998-08-08 T_RadiusDamage workaround by Patrick Martin  start
/*
	// did say self.owner
	T_RadiusDamage (self, self, 160, world, "");	// 1998-07-24 Wrong obituary messages fix by Zoid
	sound (self, CHAN_VOICE, "weapons/r_exp3.wav", 1, ATTN_NORM);
	particle (self.origin, '0 0 0', 75, 255);

	self.origin_z = self.origin_z + 32;
	BecomeExplosion ();
*/

	self.nextthink = time + 0.1;
	self.think = barrel_damage;
// 1998-08-08 T_RadiusDamage workaround by Patrick Martin  end
};

Workaround for GLQuake "Invalid skin #<number>" message and dead bodies colors by Robert Field
This workaround should work with any existing mod, it was originally for the Frogbot mod.
It was put in a form that can be easily interfaced to other mods.
But this workaround is not a final solution, for the following reasons:

First create a new file called "SkinFix.qc" which will contain some new and replacement functions for "InitBodyQue()" and "CopyToBodyQue()" in "World.qc".
"SkinFix.qc"

/*
These functions give dead bodies color and avoid the "Invalid skin" message under GLQuake (currently v0.97)

The first maxplayers entities (ie. the entities besides world that exist at
the start of worldspawn) are used by clients when they connect.
The client entities are the only ones that have colors for player models in
GLQuake. Spare client entities can be used for dead bodies.
The approach of this workaround is to use client entities for player models where
possible and use normal entities for head gibs and eyes.
Hence head gibs and eyes always have skin 0, thus avoiding the invalid player
skin # error.
If there is not 4 spare client entities then some bodies use normal entities
and hence don't have color in GLQuake.
*/
// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field
//            file added

/*
============
PostThink

Displays alternative entities for players or bodies if necessary each frame.
(the actual player or body entity in these cases are not rendered)
P.S.: don't mix up with PlayerPostThink() in client.qc
============
*/
void() PostThink =
{
	self.nextthink = 0.001;	// call PostThink every frame

	// loop through players linked list and display their eye or head gib model if necessary
	self = find(world, classname_, "player");
	while (self)
	{
		if (self.display_ent.modelindex)
		{
			// self.display_ent is a non-client entity
			setorigin(self.display_ent, self.origin);
			self.display_ent.angles = self.angles;
			self.display_ent.effects = self.effects;
		}
		self = find(self, classname_, "player");
	}

	// loop through bodies linked list and display their player model if necessary
	self = first_bodyque;
	while (self)
	{
		// self.display_ent is a spare client entity or world
		// self.display_ent.modelindex == 0 if a non-player model is being used, and self.display_ent is a spare client
		// self.display_ent.modelindex == 1 if self.display_ent == world
		if (self.display_ent.modelindex > 1)
			setorigin(self.display_ent, self.origin);
		self = self.owner;
	}
};

/*
============
FindClientBodyQue

Assigns a spare client entity to bodyque_entry.
If none spare then assign world.
Start search for client entity at bodyque_client.
============
*/
void(entity bodyque_entry) FindClientBodyQue =
{
	// post_think is the first entity after the client entities
	while (bodyque_client != post_think)
	{
		// check if bodyque_client is currently used by a client or body
		if (bodyque_client.classname_ != "player" && bodyque_client.classname_ != "body")
		{
			// bodyque_client is spare
			bodyque_entry.display_ent = bodyque_client;
			bodyque_client.classname_ = "body";
			bodyque_client.score_pos = score_pos_;
			return;
		}
		// get next bodyque_client and increment its index
		score_pos_ = score_pos_ + 1;
		bodyque_client = nextent(bodyque_client);
	}

	bodyque_entry.display_ent = world;	// no spare client entities
};

/*
============
AssignBodies

Assigns spare client entities to the 4 body entities.
If none spare then assign world.
This function is called whenever there is a change in the connected client entities list.
============
*/
void() AssignBodies =
{
	local entity bodyque_entry;

	// get first bodyque_client (with index 0)
	score_pos_ = 0;
	bodyque_client = nextent(world);

	// loop through bodies linked list and assign client entities if possible
	bodyque_entry = first_bodyque;
	while (bodyque_entry)
	{
		if (bodyque_entry.classname_ != "body")
			FindClientBodyQue(bodyque_entry);
		bodyque_entry = bodyque_entry.owner;
	}
};

/*
============
InitBodyQue

The bodies form a linked list (in the original id code they form a ring).
Called at the start of worldspawn so that post_think is the first entity after the client entities.
============
*/
void() InitBodyQue =
{
	post_think = spawn();
	post_think.nextthink = 0.001;
	post_think.think = PostThink;

	bodyque_head = first_bodyque = spawn();
	bodyque_head.owner = spawn();
	bodyque_head.owner.owner = spawn();
	bodyque_head.owner.owner.owner = spawn();
};

/*
============
CopyToBodyQue

Called whenever the original id CopyToBodyQue was called.
============
*/
void(entity ent) CopyToBodyQue =
{
	local entity bodyque_entry;	// the entity that renders the body

	if (ent.modelindex)
	{
		// ent is using the player model
		if (bodyque_head.display_ent)
		{
			// bodyque_head is using a spare client entity (ie. bodyque_entry)
			bodyque_entry = bodyque_head.display_ent;
			bodyque_head.modelindex = 0;	// don't render bodyque_head

			// set body colors for GLQuake (ignored by normal Quake since it takes notice of .colormap below)
			WriteByte(MSG_ALL, MSG_UPDATECOLORS);
			WriteByte(MSG_ALL, bodyque_entry.score_pos);	// the index of the client entity bodyque_entry
			WriteByte(MSG_ALL, (ent.team - 1) * 17);	// GLQuake color (ent.team - 1)
			// if ent has a shirt color ent.shirt then replace this last line by
			// WriteByte(MSG_ALL, ent.shirt * 16 + (ent.team - 1));
			setorigin (bodyque_entry, ent.origin);	// just in case PostThink has already been called this frame
		}
		else
		{
			// bodyque_head doesn't have a client entity so use itself (its color won't work in GLQuake though)
			bodyque_entry = bodyque_head;
		}

		bodyque_entry.skin = ent.skin;
		bodyque_entry.modelindex = ent.modelindex;
	}
	else
	{
		// ent is not using the player model
		if (bodyque_head.display_ent)
			bodyque_head.display_ent.modelindex = 0;
		// don't render the client entity bodyque_head.display_ent
		bodyque_entry = bodyque_head;
		bodyque_entry.skin = 0;
		bodyque_entry.modelindex = ent.display_ent.modelindex;
	}

	// set bodyque_entry.model to != ""
	bodyque_entry.model = "/";
	// set body colors for normal Quake (ignored by GLQuake)
	bodyque_entry.colormap = ent.colormap;
	// copy info display info of ent
	bodyque_entry.angles = ent.angles;
	bodyque_entry.frame = ent.frame;

	// copy physics info of ent
	bodyque_head.velocity = ent.velocity;
	bodyque_head.flags = 0;
	setorigin (bodyque_head, ent.origin);
	setsize (bodyque_head, ent.mins, ent.maxs);
	bodyque_head.movetype = MOVETYPE_TOSS;

	// move along bodyque_head (note: using a linked list not a ring)
	bodyque_head = bodyque_head.owner;
	if (!bodyque_head)
		bodyque_head = first_bodyque;
};

/*
============
SkinFixConnect

Called when client connects.
Assigns client an entity which is used for its head gib or eye model.
============
*/
void(entity client) SkinFixConnect =
{
	client.classname_ = "player";
	client.display_ent = spawn();
	// set client.display_ent to != ""
	client.display_ent.model = "/";
};

/*
============
SkinFixDisConnect

Called when client disconnects.
Does garbage collection for client.display_ent.
============
*/
void(entity client) SkinFixDisConnect =
{
	// since removing client.display_ent need to copy it if it is being rendered
	if (client.display_ent.modelindex)
		CopyToBodyQue(client);

	// must garbage collect client.display_ent since a new client will have client.display_ent == world
	remove(client.display_ent);
};

/*
============
Set_modelindex_0

Called when client is not being rendered (eg. intermission or observer mode)
============
*/
void(entity client) Set_modelindex_0 =
{
	client.display_ent.modelindex = 0;
};

/*
============
Set_modelindex_player

Called after client has been given a player model.
(client.modelindex must have been set)
============
*/
void(entity client) Set_modelindex_player =
{
	if (client.display_ent.modelindex)
	{
		// client.display_ent was being rendered
		client.display_ent.modelindex = 0;	// don't render client.display_ent

		// switch to client viewport
		msg_entity = client;
		WriteByte(MSG_ONE, SVC_SETVIEWPORT);
		WriteEntity(MSG_ONE, client);
	}
};

/*
============
Set_modelindex_non_player

Called after client has been given a non-player model.
(client.modelindex must have been set)
============
*/
void(entity client) Set_modelindex_non_player =
{
	if (client.display_ent.modelindex != client.modelindex)
	{
		// client.modelindex is the desired non-player model which client.display_ent will be rendering
		client.display_ent.modelindex = client.modelindex;	// render client.display_ent

		// switch to client.display_ent viewport
		msg_entity = client;
		WriteByte(MSG_ONE, SVC_SETVIEWPORT);
		WriteEntity(MSG_ONE, client.display_ent);
	}
	client.modelindex = 0;	// don't render client
};
Now you have to inform the compiler about the new file by putting the following line after "Defs.qc" in "Progs.src".
"Progs.src"

skinfix.qc		// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field
Some new variables will be needed to be defined in "Defs.qc".
"Defs.src"

// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field  start
//
// skinfix.qc
//

// protocol bytes
float SVC_SETVIEWPORT = 5;
float MSG_UPDATECOLORS = 17;

entity post_think;	// used to call PostThink every frame (after player physics),
			// and is the first entity after the client entities
entity bodyque_head;	// the next bodyque entity to be used
entity first_bodyque;	// the start of the bodyque linked list
entity bodyque_client;	// used to find a spare client entity to be used as a body
float score_pos_;	// the entity index of bodyque_client

.entity display_ent;	// the alternative display entity of a client or body
.float score_pos;	// the entity index
.string classname_; 	// used to avoid conflict with .classname, but not strictly necessary
// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field  end
After this you have to add the new functionality to the code:
"Client.qc"

void() execute_changelevel =
{
...
	while (other != world)
	{
		...
		other.modelindex = 0;
		Set_modelindex_0(other);	// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field
		setorigin (other, pos.origin);
		other = find (other, classname, "player");
	}

	WriteByte (MSG_ALL, SVC_INTERMISSION);
};
...
void() ClientKill =
{
...
	self.modelindex = modelindex_player;
	Set_modelindex_player(self);	// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field
...
};
...
void() CheckPowerups =
{
...
// invisibility
	if (self.invisible_finished)
	{
		...
		// use the eyes
		self.frame = 0;
		self.modelindex = modelindex_eyes;
		Set_modelindex_non_player(self);	// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field
	}
	else
	{	// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field
		self.modelindex = modelindex_player;	// don't use eyes
// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field  start
		Set_modelindex_player(self);
	}
// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field  end

// invincibility
...
};
...
void() ClientConnect =
{
...
// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field  start
	SkinFixConnect(self);
	AssignBodies();
// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field  end
};
...
void() ClientDisconnect =
{
...
	SkinFixDisConnect(self);	// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field
};

"Player.qc"

void(string gibname, float dm) ThrowHead =
{
	setmodel (self, gibname);

// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field  start
	if (self.classname == "player")
		Set_modelindex_non_player(self);
// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field  end
...
};
...
void() PlayerDie =
{
...
	self.modelindex = modelindex_player;	// don't use eyes
	Set_modelindex_player(self);		// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field
...
};

"World.qc"

// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field  start
/*
void() InitBodyQue =
{
...
};

// make a body que entry for the given ent so the ent can be
// respawned elsewhere
void(entity ent) CopyToBodyQue =
{
...
};
*/
// 1998-06-21 Invalid skin and GLQuake body color workaround by Robert Field  end

a Quake Info Pool page
© 1997-2006 by Maddes