Skip to content

Commit 87cab87

Browse files
Axel-Reactorclaude
andcommitted
Make pusher/rider contact robust against float rounding (#963, #700)
Three changes to SV_PushMove/SV_PushEntity: 1. Pusher positions are evaluated analytically from an anchor captured when the leg's velocity was set, instead of accumulating velocity*movetime rounding every tick. Position error stays at a couple of ulps regardless of leg length and the final partial step lands on the leg destination. QC-side changes to origin, velocity or ltime re-anchor the trajectory, degrading to the old incremental behavior for that stretch. 2. Riders are carried to a target re-derived from the pusher's new origin each tick rather than integrating the same move in parallel at a different float magnitude, so the relative offset stays bounded. 3. Marginal interpenetration is no longer fatal: a rider that a blocked verdict finds an epsilon inside the pusher gets nudged out (up and along the push direction, up to 2*DIST_EPSILON) before the move is failed - and this now runs before the corpse path, which used to permanently zero the bbox of items over a rounding error. A move trace that starts solid inside the entity's groundentity pusher un-embeds and retraces instead of gliding through the brush, which was how items fell through elevators. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 1b64677 commit 87cab87

3 files changed

Lines changed: 151 additions & 25 deletions

File tree

Quake/pr_edict.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ edict_t *ED_Alloc (void)
8181
assert (e->free);
8282
memset (&e->v, 0, qcvm->progs->entityfields * 4);
8383
e->free = false;
84+
e->push_anchor_valid = false; // don't inherit a pusher trajectory anchor from the edict's previous life
8485

8586
// pop HEAD
8687
qcvm->free_list.head_index = (qcvm->free_list.head_index + 1) % MAX_EDICTS;

Quake/progs.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,17 @@ typedef struct edict_s
7070
vec3_t predthinkpos; /* expected edict origin once its nextthink arrives (sv_smoothplatformlerps) */
7171
float lastthink; /* time when predthinkpos was updated, or 0 if not valid (sv_smoothplatformlerps) */
7272

73+
/* pusher trajectory anchor (SV_PushMove): position is evaluated analytically from the
74+
origin/ltime captured when the current leg's velocity was set, instead of accumulating
75+
velocity*movetime rounding every tick. last* hold what SV_PushMove wrote last, so any
76+
QC-side change to origin or ltime is detected and re-anchors the trajectory. */
77+
vec3_t push_anchor_origin;
78+
vec3_t push_anchor_velocity;
79+
vec3_t push_anchor_lastorigin;
80+
float push_anchor_ltime;
81+
float push_anchor_lastltime;
82+
qboolean push_anchor_valid;
83+
7384
float freetime; /* sv.time when the object was freed */
7485
qboolean free;
7586

Quake/sv_phys.c

Lines changed: 139 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ qboolean sv_analyticphysics_frame = true; // sv_analyticphysics latched per SV_P
5757

5858
#define MOVE_EPSILON 0.01
5959

60+
// Upper bound on how deep contact noise can put an entity inside the surface it
61+
// rests on: the collision trace places contacts up to DIST_EPSILON off the plane,
62+
// and coordinate rounding at map scale adds at most 2^-10. A sweep started this
63+
// far above the resting position is therefore guaranteed to begin outside the
64+
// ground surface; any overlap that survives it is real interpenetration.
65+
#define PUSH_CONTACT_EPSILON (2 * DIST_EPSILON)
66+
6067
static void SV_Physics_Toss (edict_t *ent);
6168

6269
// For usage by SV_PushMove, allocate at max possible size,
@@ -659,27 +666,64 @@ PUSHMOVE
659666
===============================================================================
660667
*/
661668

669+
cvar_t sv_gameplayfix_elevators = {"sv_gameplayfix_elevators", "2", CVAR_ARCHIVE}; // 0=off; 1=clients only; 2=all entities
670+
671+
static qboolean SV_GameplayFixElevators (edict_t *ent)
672+
{
673+
return sv_gameplayfix_elevators.value >= 2.f || (sv_gameplayfix_elevators.value && NUM_FOR_EDICT (ent) <= svs.maxclients);
674+
}
675+
676+
static trace_t SV_PushEntityMove (edict_t *ent, vec3_t start, vec3_t end)
677+
{
678+
if (ent->v.movetype == MOVETYPE_FLYMISSILE)
679+
return SV_Move (start, ent->v.mins, ent->v.maxs, end, MOVE_MISSILE, ent);
680+
else if (ent->v.solid == SOLID_TRIGGER || ent->v.solid == SOLID_NOT)
681+
// only clip against bmodels
682+
return SV_Move (start, ent->v.mins, ent->v.maxs, end, MOVE_NOMONSTERS, ent);
683+
else
684+
return SV_Move (start, ent->v.mins, ent->v.maxs, end, MOVE_NORMAL, ent);
685+
}
686+
662687
/*
663688
============
664-
SV_PushEntity
689+
SV_PushEntityTo
665690
666691
Does not change the entities velocity at all
667692
============
668693
*/
669-
static trace_t SV_PushEntity (edict_t *ent, vec3_t push)
694+
static trace_t SV_PushEntityTo (edict_t *ent, vec3_t end)
670695
{
671696
trace_t trace;
672-
vec3_t end;
673697

674-
VectorAdd (ent->v.origin, push, end);
698+
trace = SV_PushEntityMove (ent, ent->v.origin, end);
699+
700+
// a move that starts inside a solid registers no impact against it, so the
701+
// entity would glide through the brush and fall out the far side. Against
702+
// the pusher the entity rests on such an overlap is contact noise, bounded
703+
// by PUSH_CONTACT_EPSILON: a single sweep against the pusher from that far
704+
// above is guaranteed to start outside and stops at the exact contact
705+
// coordinate. Place the entity there and redo the move so it collides
706+
// normally; if even the sweep start is inside, the overlap is real and
707+
// vanilla behavior applies.
708+
if (trace.startsolid && ent->v.groundentity && SV_GameplayFixElevators (ent))
709+
{
710+
edict_t *ground = PROG_TO_EDICT (ent->v.groundentity);
711+
if (ground != qcvm->edicts && !ground->free && ground->v.movetype == MOVETYPE_PUSH && ground->v.solid == SOLID_BSP &&
712+
SV_ClipMoveToEntity (ground, ent->v.origin, ent->v.mins, ent->v.maxs, ent->v.origin, CONTENTMASK_ANYSOLID).startsolid)
713+
{
714+
vec3_t above;
715+
trace_t exit;
675716

676-
if (ent->v.movetype == MOVETYPE_FLYMISSILE)
677-
trace = SV_Move (ent->v.origin, ent->v.mins, ent->v.maxs, end, MOVE_MISSILE, ent);
678-
else if (ent->v.solid == SOLID_TRIGGER || ent->v.solid == SOLID_NOT)
679-
// only clip against bmodels
680-
trace = SV_Move (ent->v.origin, ent->v.mins, ent->v.maxs, end, MOVE_NOMONSTERS, ent);
681-
else
682-
trace = SV_Move (ent->v.origin, ent->v.mins, ent->v.maxs, end, MOVE_NORMAL, ent);
717+
VectorCopy (ent->v.origin, above);
718+
above[2] += PUSH_CONTACT_EPSILON;
719+
exit = SV_ClipMoveToEntity (ground, above, ent->v.mins, ent->v.maxs, ent->v.origin, CONTENTMASK_ANYSOLID);
720+
if (!exit.startsolid && exit.fraction < 1)
721+
{
722+
VectorCopy (exit.endpos, ent->v.origin);
723+
trace = SV_PushEntityMove (ent, ent->v.origin, end);
724+
}
725+
}
726+
}
683727

684728
if (trace.ent)
685729
assert_always (!trace.ent->free);
@@ -696,12 +740,26 @@ static trace_t SV_PushEntity (edict_t *ent, vec3_t push)
696740
return trace;
697741
}
698742

743+
/*
744+
============
745+
SV_PushEntity
746+
747+
Does not change the entities velocity at all
748+
============
749+
*/
750+
static trace_t SV_PushEntity (edict_t *ent, vec3_t push)
751+
{
752+
vec3_t end;
753+
754+
VectorAdd (ent->v.origin, push, end);
755+
return SV_PushEntityTo (ent, end);
756+
}
757+
699758
/*
700759
============
701760
SV_PushMove
702761
============
703762
*/
704-
cvar_t sv_gameplayfix_elevators = {"sv_gameplayfix_elevators", "2", CVAR_ARCHIVE}; // 0=off; 1=clients only; 2=all entities
705763

706764
static void SV_PushMove (edict_t *pusher, float movetime)
707765
{
@@ -724,9 +782,29 @@ static void SV_PushMove (edict_t *pusher, float movetime)
724782
return;
725783
}
726784

785+
// evaluate the new position analytically from the anchor captured when this
786+
// leg's velocity was set: position error stays at a couple of ulps regardless
787+
// of leg length and the final partial step lands on the leg destination,
788+
// instead of accumulating velocity*movetime rounding every tick. The last*
789+
// mismatch cases (QC touched origin or ltime, or a blocked move was reverted)
790+
// re-anchor at the current state, which degrades to the vanilla incremental
791+
// behavior for that stretch.
792+
if (!pusher->push_anchor_valid || !VectorCompare (pusher->v.velocity, pusher->push_anchor_velocity) ||
793+
!VectorCompare (pusher->v.origin, pusher->push_anchor_lastorigin) || pusher->v.ltime != pusher->push_anchor_lastltime)
794+
{
795+
VectorCopy (pusher->v.origin, pusher->push_anchor_origin);
796+
VectorCopy (pusher->v.velocity, pusher->push_anchor_velocity);
797+
pusher->push_anchor_ltime = pusher->v.ltime;
798+
pusher->push_anchor_valid = true;
799+
}
800+
801+
const float newltime = pusher->v.ltime + movetime;
802+
vec3_t neworigin;
803+
727804
for (i = 0; i < 3; i++)
728805
{
729-
move[i] = pusher->v.velocity[i] * movetime;
806+
neworigin[i] = pusher->push_anchor_origin[i] + pusher->push_anchor_velocity[i] * (newltime - pusher->push_anchor_ltime);
807+
move[i] = neworigin[i] - pusher->v.origin[i];
730808
mins[i] = pusher->v.absmin[i] + move[i];
731809
maxs[i] = pusher->v.absmax[i] + move[i];
732810
// the grid query must span the whole sweep: riders rest on the pre-move
@@ -739,8 +817,10 @@ static void SV_PushMove (edict_t *pusher, float movetime)
739817

740818
// move the pusher to it's final position
741819

742-
VectorAdd (pusher->v.origin, move, pusher->v.origin);
743-
pusher->v.ltime += movetime;
820+
VectorCopy (neworigin, pusher->v.origin);
821+
pusher->v.ltime = newltime;
822+
VectorCopy (neworigin, pusher->push_anchor_lastorigin);
823+
pusher->push_anchor_lastltime = newltime;
744824
SV_LinkEdict (pusher, false);
745825

746826
// see if any solid entities are inside the final position
@@ -835,9 +915,17 @@ static void SV_PushMove (edict_t *pusher, float movetime)
835915
|| solid_backup == SOLID_BBOX // normally boxes
836916
|| solid_backup == SOLID_SLIDEBOX) // normally monsters
837917
{
918+
// carry the entity to a target re-derived from the pusher's new
919+
// origin: the relative offset stays bounded instead of drifting
920+
// apart when both origins integrate the same move in parallel at
921+
// different float magnitudes
922+
vec3_t dest;
923+
for (i = 0; i < 3; i++)
924+
dest[i] = pusher->v.origin[i] + (entorig[i] - pushorig[i]);
925+
838926
// try moving the contacted entity
839927
pusher->v.solid = SOLID_NOT;
840-
SV_PushEntity (check, move);
928+
SV_PushEntityTo (check, dest);
841929

842930
// if it is still inside the pusher, block
843931
if (pusher->v.skin < 0)
@@ -858,28 +946,54 @@ static void SV_PushMove (edict_t *pusher, float movetime)
858946
if (check->v.mins[0] == check->v.maxs[0])
859947
continue;
860948

949+
// with the offset-preserving carry above, every rider contact keeps
950+
// the move trace's DIST_EPSILON gap except one: the ground contact,
951+
// which gravity re-closes to exactly zero every tick. Rounding can
952+
// therefore only put a rider inside the pusher through that contact,
953+
// along +z, no deeper than PUSH_CONTACT_EPSILON. Recover the exact
954+
// contact coordinate with a single sweep from that far above, which
955+
// the bound guarantees starts outside. This must run before the
956+
// corpse path below, or items (SOLID_TRIGGER) get their bbox zeroed
957+
// over a rounding error; anything still solid after the sweep is a
958+
// genuine squeeze and crushes as before.
959+
if (riding && block == pusher && SV_GameplayFixElevators (check))
960+
{
961+
vec3_t pushedorg, above;
962+
trace_t settle;
963+
964+
VectorCopy (check->v.origin, pushedorg);
965+
VectorCopy (check->v.origin, above);
966+
above[2] += PUSH_CONTACT_EPSILON;
967+
settle = SV_PushEntityMove (check, above, pushedorg);
968+
if (!settle.startsolid)
969+
{
970+
VectorCopy (settle.endpos, check->v.origin);
971+
if (!SV_TestEntityPosition (check))
972+
{
973+
SV_LinkEdict (check, false);
974+
continue;
975+
}
976+
VectorCopy (pushedorg, check->v.origin);
977+
}
978+
}
979+
861980
if (check->v.solid == SOLID_NOT || check->v.solid == SOLID_TRIGGER)
862981
{ // corpse
863982
check->v.mins[0] = check->v.mins[1] = 0;
864983
VectorCopy (check->v.mins, check->v.maxs);
865984
continue;
866985
}
867986

868-
// try moving the entity up a bit if it's blocked by the pusher while also standing on it
869-
if (riding && block == pusher &&
870-
(sv_gameplayfix_elevators.value >= 2.f || (sv_gameplayfix_elevators.value && NUM_FOR_EDICT (check) <= svs.maxclients)))
871-
{
872-
check->v.origin[2] += DIST_EPSILON;
873-
if (!SV_TestEntityPosition (check))
874-
continue;
875-
}
876-
877987
VectorCopy (entorig, check->v.origin);
878988
SV_LinkEdict (check, true);
879989

880990
VectorCopy (pushorig, pusher->v.origin);
881991
SV_LinkEdict (pusher, false);
882992
pusher->v.ltime -= movetime;
993+
// the reverted ltime no longer matches push_anchor_lastltime, so the
994+
// next attempt re-anchors here and retries incrementally until the
995+
// obstacle is gone
996+
VectorCopy (pushorig, pusher->push_anchor_lastorigin);
883997

884998
// if the pusher has a "blocked" function, call it
885999
// otherwise, just stay in place until the obstacle is gone

0 commit comments

Comments
 (0)