fix(physics): preserve retail step-down probe state
This commit is contained in:
parent
1fd5da67b4
commit
acec33eca8
5 changed files with 249 additions and 36 deletions
|
|
@ -322,6 +322,52 @@ public sealed class RetailEdgeResponseOrderingTests
|
|||
$"{ledge[^1].Result.Position}.");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void MultiFrameGroundedFloorWallSlide_FinalPlacementIsMandatoryAndGraphFlatExact(
|
||||
bool twoSpheres)
|
||||
{
|
||||
WallMaintenanceTrace graph = RunGroundedFloorWallSlide(
|
||||
preparedFlat: false,
|
||||
twoSpheres);
|
||||
WallMaintenanceTrace flat = RunGroundedFloorWallSlide(
|
||||
preparedFlat: true,
|
||||
twoSpheres);
|
||||
|
||||
Assert.Equal(graph.SupportPasses, flat.SupportPasses);
|
||||
Assert.Equal(graph.PlacementPasses, flat.PlacementPasses);
|
||||
Assert.True(graph.SupportPasses > 0,
|
||||
"The full-engine replay never entered grounded support maintenance.");
|
||||
Assert.Equal(graph.Frames.Count, graph.SupportPasses);
|
||||
Assert.Equal(graph.SupportPasses, graph.PlacementPasses);
|
||||
Assert.Equal(graph.Frames.Count, flat.Frames.Count);
|
||||
for (int i = 0; i < graph.Frames.Count; i++)
|
||||
{
|
||||
Assert.Equal(graph.Frames[i].ResolveBits, flat.Frames[i].ResolveBits);
|
||||
Assert.Equal(graph.Frames[i].BodyBits, flat.Frames[i].BodyBits);
|
||||
Assert.True(graph.Frames[i].Result.Ok,
|
||||
$"Grounded wall slide failed at frame {i}.");
|
||||
Assert.True(graph.Frames[i].Result.InContact,
|
||||
$"Ground contact was lost at frame {i}.");
|
||||
Assert.True(graph.Frames[i].Result.OnWalkable,
|
||||
$"Walkable state was lost at frame {i}.");
|
||||
|
||||
float wallLimit = 0.5f - BSPStepUpFixtures.SphereRadius
|
||||
+ PhysicsGlobals.EPSILON * 10f;
|
||||
Assert.True(graph.Frames[i].Result.Position.X <= wallLimit,
|
||||
$"Wall penetration at frame {i}: X={graph.Frames[i].Result.Position.X}, " +
|
||||
$"limit={wallLimit}.");
|
||||
}
|
||||
|
||||
AssertNoLongFrozenStreak(graph.Frames, maximumTicks: 1);
|
||||
Assert.True(
|
||||
graph.Frames[^1].Result.Position.Y
|
||||
> graph.Frames[0].Result.Position.Y + 0.25f,
|
||||
$"Wall response removed tangential progress: " +
|
||||
$"{graph.Frames[0].Result.Position} -> {graph.Frames[^1].Result.Position}.");
|
||||
}
|
||||
|
||||
private static Transition MakeFailedStepDownTransition()
|
||||
{
|
||||
Vector3 current = Vector3.Zero;
|
||||
|
|
@ -468,6 +514,80 @@ public sealed class RetailEdgeResponseOrderingTests
|
|||
return new TraceRun(trace, landedFrame, ledgeStartFrame);
|
||||
}
|
||||
|
||||
private static WallMaintenanceTrace RunGroundedFloorWallSlide(
|
||||
bool preparedFlat,
|
||||
bool twoSpheres)
|
||||
{
|
||||
var fixture = BSPStepUpFixtures.TallWall();
|
||||
PhysicsEngine engine = BuildCollisionEngine(
|
||||
fixture,
|
||||
preparedFlat,
|
||||
0x0100E103u);
|
||||
int supportPasses = 0;
|
||||
int placementPasses = 0;
|
||||
engine.TransitionCellCollisionTestHook = (candidate, phase, _, actual) =>
|
||||
{
|
||||
if (phase == TransitionCellCollisionPhase.Environment)
|
||||
{
|
||||
if (candidate.SpherePath.StepDown
|
||||
&& actual == TransitionState.OK
|
||||
&& candidate.CollisionInfo.ContactPlaneValid
|
||||
&& candidate.CollisionInfo.ContactPlane.Normal.Z
|
||||
>= candidate.SpherePath.WalkableAllowance)
|
||||
supportPasses++;
|
||||
else if (candidate.SpherePath.InsertType == InsertType.Placement)
|
||||
placementPasses++;
|
||||
}
|
||||
|
||||
return actual;
|
||||
};
|
||||
|
||||
float radius = BSPStepUpFixtures.SphereRadius;
|
||||
Vector3 position = new(0.5f - radius, -0.55f, 0f);
|
||||
var floor = fixture.Resolved[BSPStepUpFixtures.TallWall_FloorId];
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
Position = position,
|
||||
Orientation = Quaternion.Identity,
|
||||
GroundNormal = Vector3.UnitZ,
|
||||
ContactPlaneValid = true,
|
||||
ContactPlane = floor.Plane,
|
||||
ContactPlaneCellId = Cell,
|
||||
WalkablePolygonValid = true,
|
||||
WalkablePlane = floor.Plane,
|
||||
WalkableVertices = floor.Vertices,
|
||||
WalkableUp = Vector3.UnitZ,
|
||||
TransientState = TransientStateFlags.Active
|
||||
| TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable,
|
||||
};
|
||||
var trace = new List<TraceFrame>(10);
|
||||
|
||||
for (int tick = 0; tick < 10; tick++)
|
||||
{
|
||||
body.Velocity = new Vector3(1.8f, 2.1f, 0f);
|
||||
ResolveResult result = engine.ResolveWithTransition(
|
||||
position,
|
||||
position + new Vector3(0.06f, 0.07f, 0f),
|
||||
Cell,
|
||||
radius,
|
||||
sphereHeight: twoSpheres ? 1.835f : 0f,
|
||||
stepUpHeight: 0.04f,
|
||||
stepDownHeight: 0.04f,
|
||||
isOnGround: true,
|
||||
body,
|
||||
ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||
movingEntityId: 0x01000002u);
|
||||
|
||||
position = result.Position;
|
||||
body.Position = position;
|
||||
ApplyContactResult(body, result);
|
||||
trace.Add(CaptureFrame(result, body));
|
||||
}
|
||||
|
||||
return new WallMaintenanceTrace(trace, supportPasses, placementPasses);
|
||||
}
|
||||
|
||||
private static PhysicsEngine BuildCollisionEngine(
|
||||
(PhysicsBSPNode Root, Dictionary<ushort, ResolvedPolygon> Resolved) fixture,
|
||||
bool preparedFlat,
|
||||
|
|
@ -690,6 +810,11 @@ public sealed class RetailEdgeResponseOrderingTests
|
|||
int LandedFrame,
|
||||
int LedgeStartFrame);
|
||||
|
||||
private sealed record WallMaintenanceTrace(
|
||||
List<TraceFrame> Frames,
|
||||
int SupportPasses,
|
||||
int PlacementPasses);
|
||||
|
||||
private sealed record TraceFrame(
|
||||
ResolveResult Result,
|
||||
string ResolveBits,
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ public sealed class RetailStepDownPlacementTests
|
|||
public void OrdinaryContactMaintenance_AlwaysRunsFinalPlacement(bool twoSpheres)
|
||||
{
|
||||
Transition transition = MakeGroundedTransition(twoSpheres);
|
||||
const float supportWalkInterp = 0.375f;
|
||||
int supportPasses = 0;
|
||||
int placementPasses = 0;
|
||||
uint placementWalkInterpBits = 0;
|
||||
var engine = new PhysicsEngine
|
||||
{
|
||||
TransitionCellCollisionTestHook = (candidate, phase, _, actual) =>
|
||||
|
|
@ -36,12 +38,15 @@ public sealed class RetailStepDownPlacementTests
|
|||
if (candidate.SpherePath.StepDown)
|
||||
{
|
||||
supportPasses++;
|
||||
candidate.SpherePath.WalkInterp = supportWalkInterp;
|
||||
candidate.CollisionInfo.SetContactPlane(
|
||||
new Plane(Vector3.UnitZ, 0f), Cell);
|
||||
}
|
||||
else if (candidate.SpherePath.InsertType == InsertType.Placement)
|
||||
{
|
||||
placementPasses++;
|
||||
placementWalkInterpBits = BitConverter.SingleToUInt32Bits(
|
||||
candidate.SpherePath.WalkInterp);
|
||||
}
|
||||
|
||||
return actual;
|
||||
|
|
@ -53,10 +58,80 @@ public sealed class RetailStepDownPlacementTests
|
|||
Assert.Equal(TransitionState.OK, result);
|
||||
Assert.Equal(1, supportPasses);
|
||||
Assert.Equal(1, placementPasses);
|
||||
Assert.Equal(
|
||||
BitConverter.SingleToUInt32Bits(supportWalkInterp),
|
||||
placementWalkInterpBits);
|
||||
Assert.Equal(
|
||||
BitConverter.SingleToUInt32Bits(supportWalkInterp),
|
||||
BitConverter.SingleToUInt32Bits(transition.SpherePath.WalkInterp));
|
||||
Assert.Equal(InsertType.Transition, transition.SpherePath.InsertType);
|
||||
Assert.False(transition.SpherePath.StepDown);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckWalkable_FailedNestedProbe_PreservesOuterBackupForEdgeSlide()
|
||||
{
|
||||
Transition transition = MakeGroundedTransition(twoSpheres: true);
|
||||
var sp = transition.SpherePath;
|
||||
Vector3 outerBackup = new(91.125f, -17.25f, 333.5f);
|
||||
const uint outerBackupCell = 0xA9B40077u;
|
||||
Vector3 nestedOrigin = new(2.125f, 3.25f, 4.5f);
|
||||
|
||||
// Force check_walkables to fail so retail's nested downward probe is
|
||||
// exercised instead of the remembered-support early return.
|
||||
sp.SetCheckPos(nestedOrigin, Cell);
|
||||
sp.SetWalkable(
|
||||
new Plane(Vector3.UnitZ, 0f),
|
||||
[
|
||||
new(100f, 100f, 0f),
|
||||
new(101f, 100f, 0f),
|
||||
new(101f, 101f, 0f),
|
||||
new(100f, 101f, 0f),
|
||||
],
|
||||
Vector3.UnitZ);
|
||||
sp.BackupCheckPos = outerBackup;
|
||||
sp.BackupCheckCellId = outerBackupCell;
|
||||
|
||||
int nestedProbes = 0;
|
||||
var engine = new PhysicsEngine
|
||||
{
|
||||
TransitionCellCollisionTestHook = (candidate, phase, _, actual) =>
|
||||
{
|
||||
if (phase == TransitionCellCollisionPhase.Environment
|
||||
&& candidate.SpherePath.CheckWalkable)
|
||||
{
|
||||
nestedProbes++;
|
||||
}
|
||||
|
||||
return actual;
|
||||
},
|
||||
};
|
||||
|
||||
bool walkable = transition.DoCheckWalkable(PhysicsGlobals.FloorZ, engine);
|
||||
|
||||
Assert.False(walkable);
|
||||
Assert.True(nestedProbes > 0);
|
||||
AssertVectorBits(nestedOrigin, sp.CheckPos);
|
||||
Assert.Equal(Cell, sp.CheckCellId);
|
||||
AssertVectorBits(outerBackup, sp.BackupCheckPos);
|
||||
Assert.Equal(outerBackupCell, sp.BackupCheckCellId);
|
||||
|
||||
// Branch 1 is the first retail edge-slide branch. Its restore must use
|
||||
// the distinctive outer failed candidate, not the nested probe origin.
|
||||
transition.ObjectInfo.State &= ~ObjectInfoState.EdgeSlide;
|
||||
sp.SetCheckPos(new Vector3(-8f, -9f, -10f), Cell);
|
||||
bool stop = transition.EdgeSlideAfterStepDownFailedForTest(
|
||||
engine,
|
||||
stepDownHeight: 0.04f,
|
||||
zVal: PhysicsGlobals.FloorZ,
|
||||
out TransitionState state);
|
||||
|
||||
Assert.True(stop);
|
||||
Assert.Equal(TransitionState.OK, state);
|
||||
AssertVectorBits(outerBackup, sp.CheckPos);
|
||||
Assert.Equal(outerBackupCell, sp.CheckCellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportedCandidate_OverlappingDuringPlacement_IsRejected()
|
||||
{
|
||||
|
|
@ -234,6 +309,19 @@ public sealed class RetailStepDownPlacementTests
|
|||
return transition;
|
||||
}
|
||||
|
||||
private static void AssertVectorBits(Vector3 expected, Vector3 actual)
|
||||
{
|
||||
Assert.Equal(
|
||||
BitConverter.SingleToUInt32Bits(expected.X),
|
||||
BitConverter.SingleToUInt32Bits(actual.X));
|
||||
Assert.Equal(
|
||||
BitConverter.SingleToUInt32Bits(expected.Y),
|
||||
BitConverter.SingleToUInt32Bits(actual.Y));
|
||||
Assert.Equal(
|
||||
BitConverter.SingleToUInt32Bits(expected.Z),
|
||||
BitConverter.SingleToUInt32Bits(actual.Z));
|
||||
}
|
||||
|
||||
private static (
|
||||
PhysicsBSPNode Root,
|
||||
Dictionary<ushort, ResolvedPolygon> Resolved) BuildWall()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue