Skip to content

Commit b3ee129

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 b3ee129

3 files changed

Lines changed: 154 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: 142 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -659,27 +659,63 @@ PUSHMOVE
659659
===============================================================================
660660
*/
661661

662+
cvar_t sv_gameplayfix_elevators = {"sv_gameplayfix_elevators", "2", CVAR_ARCHIVE}; // 0=off; 1=clients only; 2=all entities
663+
664+
static qboolean SV_GameplayFixElevators (edict_t *ent)
665+
{
666+
return sv_gameplayfix_elevators.value >= 2.f || (sv_gameplayfix_elevators.value && NUM_FOR_EDICT (ent) <= svs.maxclients);
667+
}
668+
669+
static trace_t SV_PushEntityMove (edict_t *ent, vec3_t end)
670+
{
671+
if (ent->v.movetype == MOVETYPE_FLYMISSILE)
672+
return SV_Move (ent->v.origin, ent->v.mins, ent->v.maxs, end, MOVE_MISSILE, ent);
673+
else if (ent->v.solid == SOLID_TRIGGER || ent->v.solid == SOLID_NOT)
674+
// only clip against bmodels
675+
return SV_Move (ent->v.origin, ent->v.mins, ent->v.maxs, end, MOVE_NOMONSTERS, ent);
676+
else
677+
return SV_Move (ent->v.origin, ent->v.mins, ent->v.maxs, end, MOVE_NORMAL, ent);
678+
}
679+
662680
/*
663681
============
664-
SV_PushEntity
682+
SV_PushEntityTo
665683
666684
Does not change the entities velocity at all
667685
============
668686
*/
669-
static trace_t SV_PushEntity (edict_t *ent, vec3_t push)
687+
static trace_t SV_PushEntityTo (edict_t *ent, vec3_t end)
670688
{
671689
trace_t trace;
672-
vec3_t end;
673690

674-
VectorAdd (ent->v.origin, push, end);
691+
trace = SV_PushEntityMove (ent, end);
675692

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);
693+
// a move that starts inside a solid registers no impact against it, so the
694+
// entity would glide through the brush and fall out the far side. That can
695+
// happen against the pusher the entity rests on when float rounding at the
696+
// contact leaves it an epsilon deep after a carry; un-embed and redo the
697+
// move so it collides normally.
698+
if (trace.startsolid && ent->v.groundentity && SV_GameplayFixElevators (ent))
699+
{
700+
edict_t *ground = PROG_TO_EDICT (ent->v.groundentity);
701+
if (ground != qcvm->edicts && !ground->free && ground->v.movetype == MOVETYPE_PUSH && ground->v.solid == SOLID_BSP &&
702+
SV_ClipMoveToEntity (ground, ent->v.origin, ent->v.mins, ent->v.maxs, ent->v.origin, CONTENTMASK_ANYSOLID).startsolid)
703+
{
704+
int step;
705+
for (step = 1; step <= 4; step++)
706+
{
707+
vec3_t org;
708+
VectorCopy (ent->v.origin, org);
709+
org[2] += step * DIST_EPSILON;
710+
if (!SV_ClipMoveToEntity (ground, org, ent->v.mins, ent->v.maxs, org, CONTENTMASK_ANYSOLID).startsolid)
711+
{
712+
VectorCopy (org, ent->v.origin);
713+
trace = SV_PushEntityMove (ent, end);
714+
break;
715+
}
716+
}
717+
}
718+
}
683719

684720
if (trace.ent)
685721
assert_always (!trace.ent->free);
@@ -696,12 +732,26 @@ static trace_t SV_PushEntity (edict_t *ent, vec3_t push)
696732
return trace;
697733
}
698734

735+
/*
736+
============
737+
SV_PushEntity
738+
739+
Does not change the entities velocity at all
740+
============
741+
*/
742+
static trace_t SV_PushEntity (edict_t *ent, vec3_t push)
743+
{
744+
vec3_t end;
745+
746+
VectorAdd (ent->v.origin, push, end);
747+
return SV_PushEntityTo (ent, end);
748+
}
749+
699750
/*
700751
============
701752
SV_PushMove
702753
============
703754
*/
704-
cvar_t sv_gameplayfix_elevators = {"sv_gameplayfix_elevators", "2", CVAR_ARCHIVE}; // 0=off; 1=clients only; 2=all entities
705755

706756
static void SV_PushMove (edict_t *pusher, float movetime)
707757
{
@@ -724,9 +774,29 @@ static void SV_PushMove (edict_t *pusher, float movetime)
724774
return;
725775
}
726776

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

740810
// move the pusher to it's final position
741811

742-
VectorAdd (pusher->v.origin, move, pusher->v.origin);
743-
pusher->v.ltime += movetime;
812+
VectorCopy (neworigin, pusher->v.origin);
813+
pusher->v.ltime = newltime;
814+
VectorCopy (neworigin, pusher->push_anchor_lastorigin);
815+
pusher->push_anchor_lastltime = newltime;
744816
SV_LinkEdict (pusher, false);
745817

746818
// see if any solid entities are inside the final position
@@ -835,9 +907,17 @@ static void SV_PushMove (edict_t *pusher, float movetime)
835907
|| solid_backup == SOLID_BBOX // normally boxes
836908
|| solid_backup == SOLID_SLIDEBOX) // normally monsters
837909
{
910+
// carry the entity to a target re-derived from the pusher's new
911+
// origin: the relative offset stays bounded instead of drifting
912+
// apart when both origins integrate the same move in parallel at
913+
// different float magnitudes
914+
vec3_t dest;
915+
for (i = 0; i < 3; i++)
916+
dest[i] = pusher->v.origin[i] + (entorig[i] - pushorig[i]);
917+
838918
// try moving the contacted entity
839919
pusher->v.solid = SOLID_NOT;
840-
SV_PushEntity (check, move);
920+
SV_PushEntityTo (check, dest);
841921

842922
// if it is still inside the pusher, block
843923
if (pusher->v.skin < 0)
@@ -858,28 +938,65 @@ static void SV_PushMove (edict_t *pusher, float movetime)
858938
if (check->v.mins[0] == check->v.maxs[0])
859939
continue;
860940

941+
// a rider that ends up marginally inside the pusher (float rounding
942+
// at the contact) is not a real squeeze: try epsilon nudges up and
943+
// along the push direction before failing the move. This must run
944+
// before the corpse path below, or items (SOLID_TRIGGER) get their
945+
// bbox zeroed over a rounding error. Genuine squeezes embed far
946+
// deeper than these nudges and still crush.
947+
if (riding && block == pusher && SV_GameplayFixElevators (check))
948+
{
949+
vec3_t pushedorg, movedir;
950+
int attempt;
951+
qboolean cleared = false;
952+
953+
VectorCopy (check->v.origin, pushedorg);
954+
VectorCopy (move, movedir);
955+
VectorNormalize (movedir);
956+
957+
for (attempt = 0; attempt < 6 && !cleared; attempt++)
958+
{
959+
const float scale = (attempt < 3) ? DIST_EPSILON : 2.f * DIST_EPSILON;
960+
vec3_t nudge = {0.f, 0.f, 0.f};
961+
962+
if (attempt % 3 == 0)
963+
nudge[2] = scale;
964+
else if (attempt % 3 == 1)
965+
VectorScale (movedir, scale, nudge);
966+
else
967+
{
968+
VectorScale (movedir, scale, nudge);
969+
nudge[2] += scale;
970+
}
971+
972+
VectorAdd (pushedorg, nudge, check->v.origin);
973+
cleared = !SV_TestEntityPosition (check);
974+
}
975+
if (cleared)
976+
{
977+
SV_LinkEdict (check, false);
978+
continue;
979+
}
980+
VectorCopy (pushedorg, check->v.origin);
981+
}
982+
861983
if (check->v.solid == SOLID_NOT || check->v.solid == SOLID_TRIGGER)
862984
{ // corpse
863985
check->v.mins[0] = check->v.mins[1] = 0;
864986
VectorCopy (check->v.mins, check->v.maxs);
865987
continue;
866988
}
867989

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-
877990
VectorCopy (entorig, check->v.origin);
878991
SV_LinkEdict (check, true);
879992

880993
VectorCopy (pushorig, pusher->v.origin);
881994
SV_LinkEdict (pusher, false);
882995
pusher->v.ltime -= movetime;
996+
// the reverted ltime no longer matches push_anchor_lastltime, so the
997+
// next attempt re-anchors here and retries incrementally until the
998+
// obstacle is gone
999+
VectorCopy (pushorig, pusher->push_anchor_lastorigin);
8831000

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

0 commit comments

Comments
 (0)