This came to light after everyone started playing the Bloody Slipgates mod. There is a place where one of the red keys is hidden where there are 3 routes to it. Route 1 involves a fairly fiendish ninja jump (captured for posterity in my demo), route 2 involves a clip brush in a place where it doesn't seem obvious, and route 3 involves jumping off a small explobox. Now, most if not all engines aside from DP don't support jumping off exploboxes, so route 2 was added at the last minute in order to provide a way (I think the mod author may not have realised that route 1 was possible here).
Here I'm going to show you how to implement the ability to jump off exploboxes in Quake. Before I start though there's a big disclaimer to make.
BIG DISCLAIMER
This is a gameplay changing tutorial. It might make some areas in some maps that were previously inaccessible easier to get at, so I consider it a "cheating" change. It's all server-side though so it won't affect MP games, but just be aware that if you implement it you may spoil your enjoyment of some maps or mods.
I've only tested it on Bloody Slipgates, and only in the relevant area. I've also ensured that it doesn't interfere with other regular brush entities.
Some mods may do things with exploboxes that break as a result of this code. You have been warned.
Now, open sv_phys.c and look for the SV_Physics function. Replace it with this:
Code: Select all
void SV_Physics (void)
{
int i;
edict_t *ent;
// let the progs know that a new frame has started
pr_global_struct->self = EDICT_TO_PROG(sv.edicts);
pr_global_struct->other = EDICT_TO_PROG(sv.edicts);
pr_global_struct->time = sv.time;
PR_ExecuteProgram (pr_global_struct->StartFrame);
//SV_CheckAllEnts ();
//
// treat each object in turn
//
ent = sv.edicts;
for (i=0 ; i<sv.num_edicts ; i++, ent = NEXT_EDICT(ent))
{
// jumping on exploboxes begin
qboolean modelhack = false;
// jumping on exploboxes end
if (ent->free)
continue;
// jumping on exploboxes begin
// don't do the world edict
if (ent->v.touch == 0 && i > 0)
{
model_t *mod = sv.models[(int) ent->v.modelindex];
if (mod)
{
if (mod->type == mod_brush && mod->name[0] != '*')
{
// we have a brushmodel that (1) doesn't have a touch function, and (2)
// is an instanced BSP model, so we switch the solid and movetype
ent->v.solid = SOLID_BSP;
ent->v.movetype = MOVETYPE_PUSH;
// flag that we're hacking the model
modelhack = true;
}
}
}
// jumping on exploboxes end
if (pr_global_struct->force_retouch)
{
SV_LinkEdict (ent, true); // force retouch even for stationary
}
if (i > 0 && i <= svs.maxclients)
SV_Physics_Client (ent, i);
else if (ent->v.movetype == MOVETYPE_PUSH)
// jumping on exploboxes begin
SV_Physics_Pusher (ent, modelhack);
// jumping on exploboxes end
else if (ent->v.movetype == MOVETYPE_NONE)
SV_Physics_None (ent);
#ifdef QUAKE2
else if (ent->v.movetype == MOVETYPE_FOLLOW)
SV_Physics_Follow (ent);
#endif
else if (ent->v.movetype == MOVETYPE_NOCLIP)
SV_Physics_Noclip (ent);
else if (ent->v.movetype == MOVETYPE_STEP)
SV_Physics_Step (ent);
else if (ent->v.movetype == MOVETYPE_TOSS
|| ent->v.movetype == MOVETYPE_BOUNCE
#ifdef QUAKE2
|| ent->v.movetype == MOVETYPE_BOUNCEMISSILE
#endif
|| ent->v.movetype == MOVETYPE_FLY
|| ent->v.movetype == MOVETYPE_FLYMISSILE)
SV_Physics_Toss (ent);
else
Sys_Error ("SV_Physics: bad movetype %i", (int)ent->v.movetype);
}
if (pr_global_struct->force_retouch)
pr_global_struct->force_retouch--;
sv.time += host_frametime;
}
The code to detect if it's an explobox can work with any instanced BSP model. First it detects if the model has a touch function; if it does we assume that it''s meant to be interacted with in some way, and so we don't switch it. Then we ensure that we've got an instanced BSP model. Note also the change to the call to SV_Physics_Pusher in here. I'll talk about that one after the next block of code.
Next, we replace SV_Physics_Pusher itself with this:
Code: Select all
void SV_Physics_Pusher (edict_t *ent, qboolean modelhack)
{
float thinktime;
float oldltime;
float movetime;
oldltime = ent->v.ltime;
thinktime = ent->v.nextthink;
if (thinktime < ent->v.ltime + host_frametime)
{
movetime = thinktime - ent->v.ltime;
if (movetime < 0)
movetime = 0;
}
else
movetime = host_frametime;
if (movetime)
{
#ifdef QUAKE2
if (ent->v.avelocity[0] || ent->v.avelocity[1] || ent->v.avelocity[2])
SV_PushRotate (ent, movetime);
else
#endif
SV_PushMove (ent, movetime); // advances ent->v.ltime if not blocked
}
// jumping on exploboxes begin
if (modelhack)
{
// run regular thinking
SV_RunThink (ent);
}
else if (thinktime > oldltime && thinktime <= ent->v.ltime)
// jumping on exploboxes end
{
ent->v.nextthink = 0;
pr_global_struct->time = sv.time;
pr_global_struct->self = EDICT_TO_PROG(ent);
pr_global_struct->other = EDICT_TO_PROG(sv.edicts);
PR_ExecuteProgram (ent->v.think);
if (ent->free)
return;
}
}
This code as it stands will potentially give you a "SOLID_BSP with a non-BSP model" error so the last thing to do is fix that. I'm going to do a quick and dirty fix rather than something more robust, so open world.c and replace SV_HullForEntity with this:
Code: Select all
hull_t *SV_HullForEntity (edict_t *ent, vec3_t mins, vec3_t maxs, vec3_t offset)
{
model_t *model;
vec3_t size;
vec3_t hullmins, hullmaxs;
hull_t *hull;
// decide which clipping hull to use, based on the size
if (ent->v.solid == SOLID_BSP)
{ // explicit hulls in the BSP model
if (ent->v.movetype != MOVETYPE_PUSH)
Sys_Error ("SOLID_BSP without MOVETYPE_PUSH");
model = sv.models[ (int)ent->v.modelindex ];
// jumping on exploboxes begin
if (!model || model->type != mod_brush)
goto dont_crash;
// jumping on exploboxes end
VectorSubtract (maxs, mins, size);
if (size[0] < 3)
hull = &model->hulls[0];
else if (size[0] <= 32)
hull = &model->hulls[1];
else
hull = &model->hulls[2];
// calculate an offset value to center the origin
VectorSubtract (hull->clip_mins, mins, offset);
VectorAdd (offset, ent->v.origin, offset);
}
else
{ // create a temp hull from bounding box sizes
dont_crash:
VectorSubtract (ent->v.mins, maxs, hullmins);
VectorSubtract (ent->v.maxs, mins, hullmaxs);
hull = SV_HullForBox (hullmins, hullmaxs);
VectorCopy (ent->v.origin, offset);
}
return hull;
}
Now, this code is fairly hacky and there are better ways of doing the whole thing; one possible solution would be to store the original values of solid and movetype in the edict_t struct and switch between them and the new ones as required. I did it this way so that I could present the tutorial in a manner that's easier to follow, but I wouldn't claim is the right way, or even a good way. But if you want to jump off exploboxes it'll let you do it. Any improvements are entirely up to you.