Back | Last changes: 2001-11-25 | Contact Maddes |
"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.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.
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 excerpt 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:
"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