fix(physics): preserve retail edge-slide stop semantics
This commit is contained in:
parent
c559c48d80
commit
4fbd93ecdb
3 changed files with 427 additions and 77 deletions
|
|
@ -1110,4 +1110,27 @@ rejects a greater-than-15-tick frozen streak. The earlier dedicated
|
||||||
`Ts4SteepRoofWedgeCaptureTests` remains green, so retiring these four
|
`Ts4SteepRoofWedgeCaptureTests` remains green, so retiring these four
|
||||||
compensations did not require weakening or deleting the TS-4 control.
|
compensations did not require weakening or deleting the TS-4 control.
|
||||||
|
|
||||||
|
### Corrective review: edge_slide has two outputs
|
||||||
|
|
||||||
|
The first Slice 1B commit collapsed `CTransition::edge_slide`'s function
|
||||||
|
return into its out `TransitionState`. That loses a material retail case:
|
||||||
|
the steep-contact branch writes the result of `cliff_slide` to the out state
|
||||||
|
but returns false independently. A degenerate/parallel CliffSlide therefore
|
||||||
|
writes `OK_TS` **and still tells `transitional_insert` to continue its outer
|
||||||
|
retry**. Branch 1 and the contact-without-remembered-walkable branch stop with
|
||||||
|
`OK_TS`; a `COLLIDED_TS` Precipice result stops; `SLID_TS` and `ADJUSTED_TS`
|
||||||
|
retain their ordinary retry handling.
|
||||||
|
|
||||||
|
The corrective implementation preserves this exact bool-plus-out-state seam.
|
||||||
|
Its end-to-end test drives an invalid-contact StepDown probe into a steep,
|
||||||
|
parallel-last-known CliffSlide, then proves a second outer object pass occurs
|
||||||
|
before success. The roof controls were also hardened: the flat-roof fixture
|
||||||
|
must first land and publish its persistent contact/walkable chronology, fails
|
||||||
|
its control arm when the roof is removed, and proves outward-X rejection plus
|
||||||
|
continued edge tangency. The steep-roof fixture now rejects non-finite or
|
||||||
|
oversized frame steps and signed-plane penetration until polygon exit. Parsed
|
||||||
|
graph and prepared-flat runs compare every `ResolveResult` field and every
|
||||||
|
persistent `PhysicsBody` field by raw float/double bits, including the ordered
|
||||||
|
walkable vertex payload.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -1758,11 +1758,12 @@ public sealed class Transition
|
||||||
return (probeHeight * 0.5f, 2);
|
return (probeHeight * 0.5f, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal TransitionState EdgeSlideAfterStepDownFailedForTest(
|
internal bool EdgeSlideAfterStepDownFailedForTest(
|
||||||
PhysicsEngine engine,
|
PhysicsEngine engine,
|
||||||
float stepDownHeight,
|
float stepDownHeight,
|
||||||
float zVal)
|
float zVal,
|
||||||
=> EdgeSlideAfterStepDownFailed(engine, stepDownHeight, zVal);
|
out TransitionState result)
|
||||||
|
=> EdgeSlideAfterStepDownFailed(engine, stepDownHeight, zVal, out result);
|
||||||
|
|
||||||
internal TransitionState CliffSlideForTest(Plane contactPlane)
|
internal TransitionState CliffSlideForTest(Plane contactPlane)
|
||||||
=> CliffSlide(contactPlane);
|
=> CliffSlide(contactPlane);
|
||||||
|
|
@ -2087,7 +2088,14 @@ public sealed class Transition
|
||||||
// merely the EdgeSlide flag.
|
// merely the EdgeSlide flag.
|
||||||
DumpEdgeSlideStepDownFailed(stepDownHeight, zVal);
|
DumpEdgeSlideStepDownFailed(stepDownHeight, zVal);
|
||||||
|
|
||||||
var edgeState = EdgeSlideAfterStepDownFailed(engine, stepDownHeight, zVal);
|
bool stop = EdgeSlideAfterStepDownFailed(
|
||||||
|
engine,
|
||||||
|
stepDownHeight,
|
||||||
|
zVal,
|
||||||
|
out TransitionState edgeState);
|
||||||
|
if (stop)
|
||||||
|
return edgeState;
|
||||||
|
|
||||||
if (edgeState == TransitionState.Slid)
|
if (edgeState == TransitionState.Slid)
|
||||||
{
|
{
|
||||||
transitState = edgeState;
|
transitState = edgeState;
|
||||||
|
|
@ -2104,7 +2112,12 @@ public sealed class Transition
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return edgeState;
|
// Retail edge_slide has a bool return separate from its out
|
||||||
|
// TransitionState. In particular, a degenerate CliffSlide
|
||||||
|
// writes OK_TS but returns false, so transitional_insert must
|
||||||
|
// continue its outer retry rather than treating OK as a stop.
|
||||||
|
transitState = edgeState;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return TransitionState.OK;
|
return TransitionState.OK;
|
||||||
|
|
@ -2212,10 +2225,11 @@ public sealed class Transition
|
||||||
? hook(this, phase, cellId, actual)
|
? hook(this, phase, cellId, actual)
|
||||||
: actual;
|
: actual;
|
||||||
|
|
||||||
private TransitionState EdgeSlideAfterStepDownFailed(
|
private bool EdgeSlideAfterStepDownFailed(
|
||||||
PhysicsEngine engine,
|
PhysicsEngine engine,
|
||||||
float stepDownHeight,
|
float stepDownHeight,
|
||||||
float zVal)
|
float zVal,
|
||||||
|
out TransitionState result)
|
||||||
{
|
{
|
||||||
var sp = SpherePath;
|
var sp = SpherePath;
|
||||||
var ci = CollisionInfo;
|
var ci = CollisionInfo;
|
||||||
|
|
@ -2231,7 +2245,8 @@ public sealed class Transition
|
||||||
sp.RestoreCheckPos();
|
sp.RestoreCheckPos();
|
||||||
ci.ContactPlaneValid = false;
|
ci.ContactPlaneValid = false;
|
||||||
ci.ContactPlaneIsWater = false;
|
ci.ContactPlaneIsWater = false;
|
||||||
return TransitionState.OK;
|
result = TransitionState.OK;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ci.ContactPlaneValid && ci.ContactPlane.Normal.Z < zVal)
|
if (ci.ContactPlaneValid && ci.ContactPlane.Normal.Z < zVal)
|
||||||
|
|
@ -2242,7 +2257,8 @@ public sealed class Transition
|
||||||
sp.RestoreCheckPos();
|
sp.RestoreCheckPos();
|
||||||
ci.ContactPlaneValid = false;
|
ci.ContactPlaneValid = false;
|
||||||
ci.ContactPlaneIsWater = false;
|
ci.ContactPlaneIsWater = false;
|
||||||
return CliffSlide(cliffPlane);
|
result = CliffSlide(cliffPlane);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retail tests only SPHEREPATH::walkable here. When the failed
|
// Retail tests only SPHEREPATH::walkable here. When the failed
|
||||||
|
|
@ -2263,7 +2279,8 @@ public sealed class Transition
|
||||||
sp.RestoreCheckPos();
|
sp.RestoreCheckPos();
|
||||||
ci.ContactPlaneValid = false;
|
ci.ContactPlaneValid = false;
|
||||||
ci.ContactPlaneIsWater = false;
|
ci.ContactPlaneIsWater = false;
|
||||||
return sp.PrecipiceSlide(this);
|
result = sp.PrecipiceSlide(this);
|
||||||
|
return result == TransitionState.Collided;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ci.ContactPlaneValid)
|
if (ci.ContactPlaneValid)
|
||||||
|
|
@ -2273,7 +2290,8 @@ public sealed class Transition
|
||||||
sp.RestoreCheckPos();
|
sp.RestoreCheckPos();
|
||||||
ci.ContactPlaneValid = false;
|
ci.ContactPlaneValid = false;
|
||||||
ci.ContactPlaneIsWater = false;
|
ci.ContactPlaneIsWater = false;
|
||||||
return TransitionState.OK;
|
result = TransitionState.OK;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retail back-probes from the current sphere center to rediscover the
|
// Retail back-probes from the current sphere center to rediscover the
|
||||||
|
|
@ -2317,10 +2335,14 @@ public sealed class Transition
|
||||||
// Retail returns Collided when the back-probe found no walkable.
|
// Retail returns Collided when the back-probe found no walkable.
|
||||||
// In particular, it does not substitute a retained earlier polygon.
|
// In particular, it does not substitute a retained earlier polygon.
|
||||||
if (sp.HasWalkablePolygon)
|
if (sp.HasWalkablePolygon)
|
||||||
return sp.PrecipiceSlide(this);
|
{
|
||||||
|
result = sp.PrecipiceSlide(this);
|
||||||
|
return result == TransitionState.Collided;
|
||||||
|
}
|
||||||
|
|
||||||
sp.ClearWalkable();
|
sp.ClearWalkable();
|
||||||
return TransitionState.Collided;
|
result = TransitionState.Collided;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TransitionState CliffSlide(Plane contactPlane)
|
private TransitionState CliffSlide(Plane contactPlane)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Text;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -87,11 +88,13 @@ public sealed class RetailEdgeResponseOrderingTests
|
||||||
transition.CollisionInfo.LastKnownContactPlane = new Plane(Vector3.UnitZ, 0f);
|
transition.CollisionInfo.LastKnownContactPlane = new Plane(Vector3.UnitZ, 0f);
|
||||||
|
|
||||||
Vector3 failedCandidate = transition.SpherePath.BackupCheckPos;
|
Vector3 failedCandidate = transition.SpherePath.BackupCheckPos;
|
||||||
TransitionState result = transition.EdgeSlideAfterStepDownFailedForTest(
|
bool stop = transition.EdgeSlideAfterStepDownFailedForTest(
|
||||||
new PhysicsEngine(),
|
new PhysicsEngine(),
|
||||||
stepDownHeight: 0.04f,
|
stepDownHeight: 0.04f,
|
||||||
zVal: PhysicsGlobals.FloorZ);
|
zVal: PhysicsGlobals.FloorZ,
|
||||||
|
out TransitionState result);
|
||||||
|
|
||||||
|
Assert.True(stop);
|
||||||
Assert.Equal(TransitionState.OK, result);
|
Assert.Equal(TransitionState.OK, result);
|
||||||
Assert.Equal(failedCandidate, transition.SpherePath.CheckPos);
|
Assert.Equal(failedCandidate, transition.SpherePath.CheckPos);
|
||||||
Assert.False(transition.CollisionInfo.ContactPlaneValid);
|
Assert.False(transition.CollisionInfo.ContactPlaneValid);
|
||||||
|
|
@ -165,43 +168,158 @@ public sealed class RetailEdgeResponseOrderingTests
|
||||||
transition.SpherePath.SaveCheckPos();
|
transition.SpherePath.SaveCheckPos();
|
||||||
transition.SpherePath.AddOffsetToCheckPos(new Vector3(0f, 0f, -0.25f));
|
transition.SpherePath.AddOffsetToCheckPos(new Vector3(0f, 0f, -0.25f));
|
||||||
|
|
||||||
TransitionState result = transition.EdgeSlideAfterStepDownFailedForTest(
|
bool stop = transition.EdgeSlideAfterStepDownFailedForTest(
|
||||||
new PhysicsEngine(),
|
new PhysicsEngine(),
|
||||||
stepDownHeight: 0.04f,
|
stepDownHeight: 0.04f,
|
||||||
zVal: PhysicsGlobals.FloorZ);
|
zVal: PhysicsGlobals.FloorZ,
|
||||||
|
out TransitionState result);
|
||||||
|
|
||||||
// The restored point is inside the remembered polygon, so retail's
|
// The restored point is inside the remembered polygon, so retail's
|
||||||
// unconditional PrecipiceSlide returns Collided. AD-54's steep-plane
|
// unconditional PrecipiceSlide returns Collided. AD-54's steep-plane
|
||||||
// reroute instead returned Adjusted through CliffSlide.
|
// reroute instead returned Adjusted through CliffSlide.
|
||||||
|
Assert.True(stop);
|
||||||
Assert.Equal(TransitionState.Collided, result);
|
Assert.Equal(TransitionState.Collided, result);
|
||||||
Assert.Equal(failedCandidate, transition.SpherePath.CheckPos);
|
Assert.Equal(failedCandidate, transition.SpherePath.CheckPos);
|
||||||
Assert.False(transition.SpherePath.HasWalkablePolygon);
|
Assert.False(transition.SpherePath.HasWalkablePolygon);
|
||||||
Assert.False(transition.CollisionInfo.CollisionNormalValid);
|
Assert.False(transition.CollisionInfo.CollisionNormalValid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TransitionalInsert_DegenerateCliffSlideOk_ContinuesOuterRetry()
|
||||||
|
{
|
||||||
|
Vector3 current = new(2f, 3f, 4f);
|
||||||
|
Vector3 target = current + new Vector3(0.1f, 0f, 0f);
|
||||||
|
var transition = BSPStepUpFixtures.MakeGroundedTransition(current, target, cellId: Cell);
|
||||||
|
transition.ObjectInfo.State |= ObjectInfoState.EdgeSlide;
|
||||||
|
transition.ObjectInfo.StepDown = true;
|
||||||
|
transition.ObjectInfo.StepDownHeight = 0.04f;
|
||||||
|
|
||||||
|
Plane steep = new(Vector3.Normalize(new Vector3(1f, 0f, 0.25f)), 0f);
|
||||||
|
int outerObjectPasses = 0;
|
||||||
|
var engine = new PhysicsEngine
|
||||||
|
{
|
||||||
|
TransitionCellCollisionTestHook = (candidate, phase, _, actual) =>
|
||||||
|
{
|
||||||
|
if (phase != TransitionCellCollisionPhase.Objects)
|
||||||
|
return actual;
|
||||||
|
|
||||||
|
if (candidate.SpherePath.StepDown)
|
||||||
|
{
|
||||||
|
// The nested downward probe finds a steep contact. It is
|
||||||
|
// rejected as walkable, and because SetContactPlane also
|
||||||
|
// latches the same last-known plane, CliffSlide's cross is
|
||||||
|
// parallel/degenerate and writes OK_TS with stop=false.
|
||||||
|
candidate.CollisionInfo.SetContactPlane(steep, Cell);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outerObjectPasses++;
|
||||||
|
if (outerObjectPasses == 2)
|
||||||
|
candidate.CollisionInfo.SetContactPlane(new Plane(Vector3.UnitZ, 0f), Cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
return actual;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
TransitionState result = transition.TransitionalInsertForTest(2, engine);
|
||||||
|
|
||||||
|
Assert.Equal(TransitionState.OK, result);
|
||||||
|
Assert.Equal(2, outerObjectPasses);
|
||||||
|
Assert.True(transition.CollisionInfo.ContactPlaneValid);
|
||||||
|
Assert.Equal(Vector3.UnitZ, transition.CollisionInfo.ContactPlane.Normal);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MultiFrameSteepRoof_GraphAndFlatTraversalRemainExactAndDoNotWedge()
|
public void MultiFrameSteepRoof_GraphAndFlatTraversalRemainExactAndDoNotWedge()
|
||||||
{
|
{
|
||||||
Vector3[] graph = RunSteepRoofTrace(preparedFlat: false);
|
TraceRun graph = RunSteepRoofTrace(preparedFlat: false);
|
||||||
Vector3[] flat = RunSteepRoofTrace(preparedFlat: true);
|
TraceRun flat = RunSteepRoofTrace(preparedFlat: true);
|
||||||
|
|
||||||
Assert.Equal(graph, flat);
|
AssertTraceParity(graph, flat);
|
||||||
Assert.Contains(graph, position =>
|
Assert.Contains(graph.Frames, frame =>
|
||||||
position.X < 0f
|
frame.Result.Position.X < 0f
|
||||||
&& position.Z <= BSPStepUpFixtures.SphereRadius + 0.05f);
|
&& frame.Result.Position.Z <= BSPStepUpFixtures.SphereRadius + 0.05f);
|
||||||
AssertNoLongFrozenStreak(graph, maximumTicks: 15);
|
AssertNoLongFrozenStreak(graph.Frames, maximumTicks: 15);
|
||||||
|
|
||||||
|
Plane slope = BSPStepUpFixtures.SlopedUnwalkable().Resolved[
|
||||||
|
BSPStepUpFixtures.SlopedUnwalkable_SlopeId].Plane;
|
||||||
|
float radius = BSPStepUpFixtures.SphereRadius;
|
||||||
|
for (int i = 0; i < graph.Frames.Count; i++)
|
||||||
|
{
|
||||||
|
Vector3 position = graph.Frames[i].Result.Position;
|
||||||
|
AssertFinite(position, $"steep-roof frame {i}");
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
float distance = Vector3.Distance(
|
||||||
|
graph.Frames[i - 1].Result.Position,
|
||||||
|
position);
|
||||||
|
Assert.InRange(distance, 0f, 1.1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// While the foot center remains over the authored slope's XY
|
||||||
|
// footprint, it must stay on/outside the plane by at least its
|
||||||
|
// radius. Once X leaves [0,1], the body has exited the polygon and
|
||||||
|
// may fall toward the separate flat reference floor.
|
||||||
|
if (position.X is >= 0f and <= 1f && MathF.Abs(position.Y) <= 1f)
|
||||||
|
{
|
||||||
|
Vector3 footCenter = position + new Vector3(0f, 0f, radius);
|
||||||
|
float signedDistance = Vector3.Dot(slope.Normal, footCenter) + slope.D;
|
||||||
|
Assert.True(signedDistance >= radius - 0.015f,
|
||||||
|
$"Steep-roof penetration at frame {i}: distance={signedDistance}, " +
|
||||||
|
$"radius={radius}, position={position}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MultiFrameFlatRoofLedge_GraphAndFlatTraversalRemainExactAndSlideAlongEdge()
|
public void MultiFrameFlatRoofLedge_GraphAndFlatTraversalRemainExactAndSlideAlongEdge()
|
||||||
{
|
{
|
||||||
Vector3[] graph = RunFlatRoofLedgeTrace(preparedFlat: false);
|
TraceRun graph = RunFlatRoofLedgeTrace(preparedFlat: false, includeRoof: true);
|
||||||
Vector3[] flat = RunFlatRoofLedgeTrace(preparedFlat: true);
|
TraceRun flat = RunFlatRoofLedgeTrace(preparedFlat: true, includeRoof: true);
|
||||||
|
TraceRun noRoof = RunFlatRoofLedgeTrace(preparedFlat: false, includeRoof: false);
|
||||||
|
|
||||||
Assert.Equal(graph, flat);
|
AssertTraceParity(graph, flat);
|
||||||
AssertNoLongFrozenStreak(graph, maximumTicks: 15);
|
Assert.True(graph.LandedFrame > 0, "The collision-backed control never landed on the roof.");
|
||||||
Assert.True(graph[^1].Y > graph[0].Y + 0.25f,
|
Assert.Equal(-1, noRoof.LandedFrame);
|
||||||
$"The roof-edge control made no along-edge progress: {graph[0]} -> {graph[^1]}.");
|
Assert.All(graph.Frames.GetRange(0, graph.LandedFrame), frame =>
|
||||||
|
Assert.False(frame.Result.OnWalkable));
|
||||||
|
TraceFrame landing = graph.Frames[graph.LandedFrame];
|
||||||
|
Assert.True(landing.Result.Ok);
|
||||||
|
Assert.True(landing.Result.IsOnGround);
|
||||||
|
Assert.True(landing.Result.InContact);
|
||||||
|
Assert.True(landing.Result.OnWalkable);
|
||||||
|
Assert.True(landing.BodyContactPlaneValid);
|
||||||
|
Assert.True(landing.BodyWalkablePolygonValid);
|
||||||
|
Assert.Equal(Vector3.UnitZ, landing.BodyContactPlane.Normal);
|
||||||
|
Assert.Equal(
|
||||||
|
TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
|
||||||
|
landing.BodyTransientState
|
||||||
|
& (TransientStateFlags.Contact | TransientStateFlags.OnWalkable));
|
||||||
|
|
||||||
|
List<TraceFrame> ledge = graph.Frames.GetRange(
|
||||||
|
graph.LedgeStartFrame,
|
||||||
|
graph.Frames.Count - graph.LedgeStartFrame);
|
||||||
|
AssertNoLongFrozenStreak(ledge, maximumTicks: 15);
|
||||||
|
int outwardRejectionFrame = ledge.FindIndex(frame =>
|
||||||
|
frame.Result.CollisionNormalValid
|
||||||
|
&& frame.Result.CollisionNormal.X < -0.9f);
|
||||||
|
Assert.True(outwardRejectionFrame >= 0,
|
||||||
|
"The roof-edge control never produced the expected outward-X rejection normal.");
|
||||||
|
float rejectedX = ledge[outwardRejectionFrame].Result.Position.X;
|
||||||
|
Assert.All(ledge.GetRange(
|
||||||
|
outwardRejectionFrame,
|
||||||
|
ledge.Count - outwardRejectionFrame),
|
||||||
|
frame => Assert.True(frame.Result.Position.X <= rejectedX + 0.001f,
|
||||||
|
$"Outward X resumed after rejection: {frame.Result.Position.X} > {rejectedX}."));
|
||||||
|
float unobstructedFinalX = ledge[0].Result.Position.X + 0.12f * (ledge.Count - 1);
|
||||||
|
Assert.True(ledge[^1].Result.Position.X < unobstructedFinalX - 0.25f,
|
||||||
|
$"Roof edge failed to remove outward travel: final={ledge[^1].Result.Position.X}, " +
|
||||||
|
$"unobstructed={unobstructedFinalX}.");
|
||||||
|
Assert.True(ledge[^1].Result.Position.Y
|
||||||
|
> ledge[outwardRejectionFrame].Result.Position.Y + 0.25f,
|
||||||
|
$"The roof edge rejected all tangency: {ledge[outwardRejectionFrame].Result.Position} -> " +
|
||||||
|
$"{ledge[^1].Result.Position}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Transition MakeFailedStepDownTransition()
|
private static Transition MakeFailedStepDownTransition()
|
||||||
|
|
@ -227,7 +345,7 @@ public sealed class RetailEdgeResponseOrderingTests
|
||||||
new(-2f, 2f, 0f),
|
new(-2f, 2f, 0f),
|
||||||
];
|
];
|
||||||
|
|
||||||
private static Vector3[] RunSteepRoofTrace(bool preparedFlat)
|
private static TraceRun RunSteepRoofTrace(bool preparedFlat)
|
||||||
{
|
{
|
||||||
var fixture = BSPStepUpFixtures.SlopedUnwalkable();
|
var fixture = BSPStepUpFixtures.SlopedUnwalkable();
|
||||||
PhysicsEngine engine = BuildCollisionEngine(fixture, preparedFlat, 0x0100E101u);
|
PhysicsEngine engine = BuildCollisionEngine(fixture, preparedFlat, 0x0100E101u);
|
||||||
|
|
@ -237,7 +355,7 @@ public sealed class RetailEdgeResponseOrderingTests
|
||||||
var body = new PhysicsBody { TransientState = TransientStateFlags.Active };
|
var body = new PhysicsBody { TransientState = TransientStateFlags.Active };
|
||||||
Vector3 position = new(0.5f, 0f, 3f);
|
Vector3 position = new(0.5f, 0f, 3f);
|
||||||
float velocityZ = 0f;
|
float velocityZ = 0f;
|
||||||
var trace = new List<Vector3>(91) { position };
|
var trace = new List<TraceFrame>(90);
|
||||||
|
|
||||||
for (int tick = 0; tick < 90; tick++)
|
for (int tick = 0; tick < 90; tick++)
|
||||||
{
|
{
|
||||||
|
|
@ -259,71 +377,102 @@ public sealed class RetailEdgeResponseOrderingTests
|
||||||
body.Position = position;
|
body.Position = position;
|
||||||
if (result.IsOnGround)
|
if (result.IsOnGround)
|
||||||
velocityZ = 0f;
|
velocityZ = 0f;
|
||||||
trace.Add(position);
|
ApplyContactResult(body, result);
|
||||||
|
trace.Add(CaptureFrame(result, body));
|
||||||
|
|
||||||
if (position.X < 0f && position.Z <= radius + 0.05f)
|
if (position.X < 0f && position.Z <= radius + 0.05f)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return trace.ToArray();
|
return new TraceRun(trace, LandedFrame: -1, LedgeStartFrame: -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Vector3[] RunFlatRoofLedgeTrace(bool preparedFlat)
|
private static TraceRun RunFlatRoofLedgeTrace(bool preparedFlat, bool includeRoof)
|
||||||
{
|
{
|
||||||
var fixture = BSPStepUpFixtures.FlatRoof();
|
var fixture = BSPStepUpFixtures.FlatRoof();
|
||||||
PhysicsEngine engine = BuildCollisionEngine(fixture, preparedFlat, 0x0100E102u);
|
PhysicsEngine engine = BuildCollisionEngine(
|
||||||
ResolvedPolygon roof = fixture.Resolved[BSPStepUpFixtures.FlatRoof_RoofId];
|
fixture,
|
||||||
|
preparedFlat,
|
||||||
|
0x0100E102u,
|
||||||
|
includeGeometry: includeRoof);
|
||||||
float radius = BSPStepUpFixtures.SphereRadius;
|
float radius = BSPStepUpFixtures.SphereRadius;
|
||||||
Vector3 position = new(1.55f, -0.75f, 3f);
|
const float dt = 1f / 30f;
|
||||||
|
Vector3 position = new(1.55f, -0.75f, 3.6f);
|
||||||
|
float velocityZ = 0f;
|
||||||
var body = new PhysicsBody
|
var body = new PhysicsBody
|
||||||
{
|
{
|
||||||
Position = position,
|
Position = position,
|
||||||
Orientation = Quaternion.Identity,
|
Orientation = Quaternion.Identity,
|
||||||
ContactPlaneValid = true,
|
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
|
||||||
ContactPlane = roof.Plane,
|
TransientState = TransientStateFlags.Active,
|
||||||
ContactPlaneCellId = Cell,
|
|
||||||
WalkablePolygonValid = true,
|
|
||||||
WalkablePlane = roof.Plane,
|
|
||||||
WalkableVertices = roof.Vertices,
|
|
||||||
WalkableUp = Vector3.UnitZ,
|
|
||||||
TransientState = TransientStateFlags.Active
|
|
||||||
| TransientStateFlags.Contact
|
|
||||||
| TransientStateFlags.OnWalkable,
|
|
||||||
};
|
};
|
||||||
var trace = new List<Vector3>(13) { position };
|
var trace = new List<TraceFrame>(72);
|
||||||
|
int landedFrame = -1;
|
||||||
|
|
||||||
for (int tick = 0; tick < 12; tick++)
|
for (int tick = 0; tick < 60; tick++)
|
||||||
{
|
{
|
||||||
|
velocityZ += PhysicsBody.Gravity * dt;
|
||||||
|
body.Velocity = new Vector3(0f, 0f, velocityZ);
|
||||||
ResolveResult result = engine.ResolveWithTransition(
|
ResolveResult result = engine.ResolveWithTransition(
|
||||||
position,
|
position,
|
||||||
position + new Vector3(0.12f, 0.08f, 0f),
|
position + new Vector3(0f, 0f, velocityZ * dt),
|
||||||
Cell,
|
Cell,
|
||||||
radius,
|
radius,
|
||||||
radius * 2f,
|
radius * 2f,
|
||||||
stepUpHeight: 0.30f,
|
stepUpHeight: 0.30f,
|
||||||
stepDownHeight: 0.04f,
|
stepDownHeight: 0.04f,
|
||||||
isOnGround: true,
|
isOnGround: body.OnWalkable,
|
||||||
body,
|
body,
|
||||||
ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||||
movingEntityId: 0x01000001u);
|
movingEntityId: 0x01000001u);
|
||||||
|
|
||||||
position = result.Position;
|
position = result.Position;
|
||||||
body.Position = position;
|
body.Position = position;
|
||||||
body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable);
|
ApplyContactResult(body, result);
|
||||||
if (result.InContact)
|
trace.Add(CaptureFrame(result, body));
|
||||||
body.TransientState |= TransientStateFlags.Contact;
|
|
||||||
if (result.OnWalkable)
|
if (result.InContact && result.OnWalkable)
|
||||||
body.TransientState |= TransientStateFlags.OnWalkable;
|
{
|
||||||
trace.Add(position);
|
landedFrame = trace.Count - 1;
|
||||||
|
velocityZ = 0f;
|
||||||
|
body.Velocity = Vector3.Zero;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return trace.ToArray();
|
int ledgeStartFrame = trace.Count;
|
||||||
|
if (landedFrame >= 0)
|
||||||
|
{
|
||||||
|
for (int tick = 0; tick < 12; tick++)
|
||||||
|
{
|
||||||
|
ResolveResult result = engine.ResolveWithTransition(
|
||||||
|
position,
|
||||||
|
position + new Vector3(0.12f, 0.08f, 0f),
|
||||||
|
Cell,
|
||||||
|
radius,
|
||||||
|
radius * 2f,
|
||||||
|
stepUpHeight: 0.30f,
|
||||||
|
stepDownHeight: 0.04f,
|
||||||
|
isOnGround: body.OnWalkable,
|
||||||
|
body,
|
||||||
|
ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||||
|
movingEntityId: 0x01000001u);
|
||||||
|
|
||||||
|
position = result.Position;
|
||||||
|
body.Position = position;
|
||||||
|
ApplyContactResult(body, result);
|
||||||
|
trace.Add(CaptureFrame(result, body));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TraceRun(trace, landedFrame, ledgeStartFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PhysicsEngine BuildCollisionEngine(
|
private static PhysicsEngine BuildCollisionEngine(
|
||||||
(PhysicsBSPNode Root, Dictionary<ushort, ResolvedPolygon> Resolved) fixture,
|
(PhysicsBSPNode Root, Dictionary<ushort, ResolvedPolygon> Resolved) fixture,
|
||||||
bool preparedFlat,
|
bool preparedFlat,
|
||||||
uint gfxObjId)
|
uint gfxObjId,
|
||||||
|
bool includeGeometry = true)
|
||||||
{
|
{
|
||||||
var normalized = new Dictionary<ushort, ResolvedPolygon>(fixture.Resolved.Count);
|
var normalized = new Dictionary<ushort, ResolvedPolygon>(fixture.Resolved.Count);
|
||||||
foreach ((ushort id, ResolvedPolygon polygon) in fixture.Resolved)
|
foreach ((ushort id, ResolvedPolygon polygon) in fixture.Resolved)
|
||||||
|
|
@ -346,12 +495,12 @@ public sealed class RetailEdgeResponseOrderingTests
|
||||||
BoundingSphere = fixture.Root.BoundingSphere,
|
BoundingSphere = fixture.Root.BoundingSphere,
|
||||||
};
|
};
|
||||||
var cache = new PhysicsDataCache();
|
var cache = new PhysicsDataCache();
|
||||||
if (preparedFlat)
|
if (includeGeometry && preparedFlat)
|
||||||
{
|
{
|
||||||
cache.CollisionTraversalMode = CollisionTraversalMode.Flat;
|
cache.CollisionTraversalMode = CollisionTraversalMode.Flat;
|
||||||
cache.CacheGfxObj(gfxObjId, FlatCollisionAssetBuilder.FlattenGfxObj(physics));
|
cache.CacheGfxObj(gfxObjId, FlatCollisionAssetBuilder.FlattenGfxObj(physics));
|
||||||
}
|
}
|
||||||
else
|
else if (includeGeometry)
|
||||||
{
|
{
|
||||||
cache.RegisterGfxObjForTest(gfxObjId, physics);
|
cache.RegisterGfxObjForTest(gfxObjId, physics);
|
||||||
}
|
}
|
||||||
|
|
@ -367,30 +516,186 @@ public sealed class RetailEdgeResponseOrderingTests
|
||||||
Array.Empty<PortalPlane>(),
|
Array.Empty<PortalPlane>(),
|
||||||
0f,
|
0f,
|
||||||
0f);
|
0f);
|
||||||
engine.ShadowObjects.Register(
|
if (includeGeometry)
|
||||||
gfxObjId,
|
{
|
||||||
gfxObjId,
|
engine.ShadowObjects.Register(
|
||||||
Vector3.Zero,
|
gfxObjId,
|
||||||
Quaternion.Identity,
|
gfxObjId,
|
||||||
fixture.Root.BoundingSphere.Radius,
|
Vector3.Zero,
|
||||||
0f,
|
Quaternion.Identity,
|
||||||
0f,
|
fixture.Root.BoundingSphere.Radius,
|
||||||
0xA9B4FFFFu,
|
0f,
|
||||||
ShadowCollisionType.BSP,
|
0f,
|
||||||
1f);
|
0xA9B4FFFFu,
|
||||||
|
ShadowCollisionType.BSP,
|
||||||
|
1f);
|
||||||
|
}
|
||||||
return engine;
|
return engine;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AssertNoLongFrozenStreak(Vector3[] trace, int maximumTicks)
|
private static void ApplyContactResult(PhysicsBody body, ResolveResult result)
|
||||||
|
{
|
||||||
|
body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable);
|
||||||
|
if (result.InContact)
|
||||||
|
body.TransientState |= TransientStateFlags.Contact;
|
||||||
|
if (result.OnWalkable)
|
||||||
|
body.TransientState |= TransientStateFlags.OnWalkable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertTraceParity(TraceRun expected, TraceRun actual)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.LandedFrame, actual.LandedFrame);
|
||||||
|
Assert.Equal(expected.LedgeStartFrame, actual.LedgeStartFrame);
|
||||||
|
Assert.Equal(expected.Frames.Count, actual.Frames.Count);
|
||||||
|
for (int i = 0; i < expected.Frames.Count; i++)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.Frames[i].ResolveBits, actual.Frames[i].ResolveBits);
|
||||||
|
Assert.Equal(expected.Frames[i].BodyBits, actual.Frames[i].BodyBits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertNoLongFrozenStreak(
|
||||||
|
IReadOnlyList<TraceFrame> trace,
|
||||||
|
int maximumTicks)
|
||||||
{
|
{
|
||||||
int streak = 0;
|
int streak = 0;
|
||||||
for (int i = 1; i < trace.Length; i++)
|
for (int i = 1; i < trace.Count; i++)
|
||||||
{
|
{
|
||||||
streak = Vector3.Distance(trace[i - 1], trace[i]) < 0.001f
|
streak = Vector3.Distance(
|
||||||
|
trace[i - 1].Result.Position,
|
||||||
|
trace[i].Result.Position) < 0.001f
|
||||||
? streak + 1
|
? streak + 1
|
||||||
: 0;
|
: 0;
|
||||||
Assert.True(streak <= maximumTicks,
|
Assert.True(streak <= maximumTicks,
|
||||||
$"Trace froze for {streak} ticks at {trace[i]}.");
|
$"Trace froze for {streak} ticks at {trace[i].Result.Position}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static TraceFrame CaptureFrame(ResolveResult result, PhysicsBody body) =>
|
||||||
|
new(
|
||||||
|
result,
|
||||||
|
ResolveSignature(result),
|
||||||
|
BodySignature(body),
|
||||||
|
body.ContactPlaneValid,
|
||||||
|
body.ContactPlane,
|
||||||
|
body.WalkablePolygonValid,
|
||||||
|
body.TransientState);
|
||||||
|
|
||||||
|
private static string ResolveSignature(ResolveResult result)
|
||||||
|
{
|
||||||
|
var signature = new StringBuilder(256);
|
||||||
|
Append(signature, result.Position);
|
||||||
|
Append(signature, result.CellId);
|
||||||
|
Append(signature, result.IsOnGround);
|
||||||
|
Append(signature, result.CollisionNormalValid);
|
||||||
|
Append(signature, result.CollisionNormal);
|
||||||
|
Append(signature, result.Ok);
|
||||||
|
Append(signature, result.Orientation);
|
||||||
|
Append(signature, result.InContact);
|
||||||
|
Append(signature, result.OnWalkable);
|
||||||
|
Append(signature, result.ContactPlane);
|
||||||
|
Append(signature, result.ContactPlaneCellId);
|
||||||
|
Append(signature, result.ContactPlaneIsWater);
|
||||||
|
return signature.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BodySignature(PhysicsBody body)
|
||||||
|
{
|
||||||
|
var signature = new StringBuilder(512);
|
||||||
|
Append(signature, body.Position);
|
||||||
|
Append(signature, body.CellPosition.ObjCellId);
|
||||||
|
Append(signature, body.CellPosition.Frame.Origin);
|
||||||
|
Append(signature, body.CellPosition.Frame.Orientation);
|
||||||
|
Append(signature, body.InWorld);
|
||||||
|
Append(signature, body.Orientation);
|
||||||
|
Append(signature, body.Velocity);
|
||||||
|
Append(signature, body.CachedVelocity);
|
||||||
|
Append(signature, body.FramesStationaryFall);
|
||||||
|
Append(signature, body.Acceleration);
|
||||||
|
Append(signature, body.Omega);
|
||||||
|
Append(signature, body.GroundNormal);
|
||||||
|
Append(signature, body.SlidingNormal);
|
||||||
|
Append(signature, body.ContactPlaneValid);
|
||||||
|
Append(signature, body.ContactPlane);
|
||||||
|
Append(signature, body.ContactPlaneCellId);
|
||||||
|
Append(signature, body.ContactPlaneIsWater);
|
||||||
|
Append(signature, body.WalkablePolygonValid);
|
||||||
|
Append(signature, body.WalkablePlane);
|
||||||
|
if (body.WalkableVertices is null)
|
||||||
|
{
|
||||||
|
Append(signature, -1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Append(signature, body.WalkableVertices.Length);
|
||||||
|
foreach (Vector3 vertex in body.WalkableVertices)
|
||||||
|
Append(signature, vertex);
|
||||||
|
}
|
||||||
|
Append(signature, body.WalkableUp);
|
||||||
|
Append(signature, body.Elasticity);
|
||||||
|
Append(signature, body.Friction);
|
||||||
|
Append(signature, (uint)body.State);
|
||||||
|
Append(signature, (uint)body.TransientState);
|
||||||
|
Append(signature, BitConverter.DoubleToUInt64Bits(body.LastUpdateTime));
|
||||||
|
Append(signature, body.IsFullyConstrained);
|
||||||
|
Append(signature, body.LastMoveWasAutonomous);
|
||||||
|
return signature.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Append(StringBuilder target, bool value) =>
|
||||||
|
target.Append(value ? "1|" : "0|");
|
||||||
|
|
||||||
|
private static void Append(StringBuilder target, int value) =>
|
||||||
|
target.Append(value).Append('|');
|
||||||
|
|
||||||
|
private static void Append(StringBuilder target, uint value) =>
|
||||||
|
target.Append(value.ToString("X8")).Append('|');
|
||||||
|
|
||||||
|
private static void Append(StringBuilder target, ulong value) =>
|
||||||
|
target.Append(value.ToString("X16")).Append('|');
|
||||||
|
|
||||||
|
private static void Append(StringBuilder target, float value) =>
|
||||||
|
Append(target, BitConverter.SingleToUInt32Bits(value));
|
||||||
|
|
||||||
|
private static void Append(StringBuilder target, Vector3 value)
|
||||||
|
{
|
||||||
|
Append(target, value.X);
|
||||||
|
Append(target, value.Y);
|
||||||
|
Append(target, value.Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Append(StringBuilder target, Quaternion value)
|
||||||
|
{
|
||||||
|
Append(target, value.X);
|
||||||
|
Append(target, value.Y);
|
||||||
|
Append(target, value.Z);
|
||||||
|
Append(target, value.W);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Append(StringBuilder target, Plane value)
|
||||||
|
{
|
||||||
|
Append(target, value.Normal);
|
||||||
|
Append(target, value.D);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertFinite(Vector3 value, string context)
|
||||||
|
{
|
||||||
|
Assert.True(
|
||||||
|
float.IsFinite(value.X) && float.IsFinite(value.Y) && float.IsFinite(value.Z),
|
||||||
|
$"Non-finite position in {context}: {value}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record TraceRun(
|
||||||
|
List<TraceFrame> Frames,
|
||||||
|
int LandedFrame,
|
||||||
|
int LedgeStartFrame);
|
||||||
|
|
||||||
|
private sealed record TraceFrame(
|
||||||
|
ResolveResult Result,
|
||||||
|
string ResolveBits,
|
||||||
|
string BodyBits,
|
||||||
|
bool BodyContactPlaneValid,
|
||||||
|
Plane BodyContactPlane,
|
||||||
|
bool BodyWalkablePolygonValid,
|
||||||
|
TransientStateFlags BodyTransientState);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue