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
|
||||
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);
|
||||
}
|
||||
|
||||
internal TransitionState EdgeSlideAfterStepDownFailedForTest(
|
||||
internal bool EdgeSlideAfterStepDownFailedForTest(
|
||||
PhysicsEngine engine,
|
||||
float stepDownHeight,
|
||||
float zVal)
|
||||
=> EdgeSlideAfterStepDownFailed(engine, stepDownHeight, zVal);
|
||||
float zVal,
|
||||
out TransitionState result)
|
||||
=> EdgeSlideAfterStepDownFailed(engine, stepDownHeight, zVal, out result);
|
||||
|
||||
internal TransitionState CliffSlideForTest(Plane contactPlane)
|
||||
=> CliffSlide(contactPlane);
|
||||
|
|
@ -2087,7 +2088,14 @@ public sealed class Transition
|
|||
// merely the EdgeSlide flag.
|
||||
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)
|
||||
{
|
||||
transitState = edgeState;
|
||||
|
|
@ -2104,7 +2112,12 @@ public sealed class Transition
|
|||
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;
|
||||
|
|
@ -2212,10 +2225,11 @@ public sealed class Transition
|
|||
? hook(this, phase, cellId, actual)
|
||||
: actual;
|
||||
|
||||
private TransitionState EdgeSlideAfterStepDownFailed(
|
||||
private bool EdgeSlideAfterStepDownFailed(
|
||||
PhysicsEngine engine,
|
||||
float stepDownHeight,
|
||||
float zVal)
|
||||
float zVal,
|
||||
out TransitionState result)
|
||||
{
|
||||
var sp = SpherePath;
|
||||
var ci = CollisionInfo;
|
||||
|
|
@ -2231,7 +2245,8 @@ public sealed class Transition
|
|||
sp.RestoreCheckPos();
|
||||
ci.ContactPlaneValid = false;
|
||||
ci.ContactPlaneIsWater = false;
|
||||
return TransitionState.OK;
|
||||
result = TransitionState.OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ci.ContactPlaneValid && ci.ContactPlane.Normal.Z < zVal)
|
||||
|
|
@ -2242,7 +2257,8 @@ public sealed class Transition
|
|||
sp.RestoreCheckPos();
|
||||
ci.ContactPlaneValid = false;
|
||||
ci.ContactPlaneIsWater = false;
|
||||
return CliffSlide(cliffPlane);
|
||||
result = CliffSlide(cliffPlane);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retail tests only SPHEREPATH::walkable here. When the failed
|
||||
|
|
@ -2263,7 +2279,8 @@ public sealed class Transition
|
|||
sp.RestoreCheckPos();
|
||||
ci.ContactPlaneValid = false;
|
||||
ci.ContactPlaneIsWater = false;
|
||||
return sp.PrecipiceSlide(this);
|
||||
result = sp.PrecipiceSlide(this);
|
||||
return result == TransitionState.Collided;
|
||||
}
|
||||
|
||||
if (ci.ContactPlaneValid)
|
||||
|
|
@ -2273,7 +2290,8 @@ public sealed class Transition
|
|||
sp.RestoreCheckPos();
|
||||
ci.ContactPlaneValid = false;
|
||||
ci.ContactPlaneIsWater = false;
|
||||
return TransitionState.OK;
|
||||
result = TransitionState.OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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.
|
||||
// In particular, it does not substitute a retained earlier polygon.
|
||||
if (sp.HasWalkablePolygon)
|
||||
return sp.PrecipiceSlide(this);
|
||||
{
|
||||
result = sp.PrecipiceSlide(this);
|
||||
return result == TransitionState.Collided;
|
||||
}
|
||||
|
||||
sp.ClearWalkable();
|
||||
return TransitionState.Collided;
|
||||
result = TransitionState.Collided;
|
||||
return true;
|
||||
}
|
||||
|
||||
private TransitionState CliffSlide(Plane contactPlane)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.Types;
|
||||
using Xunit;
|
||||
|
|
@ -87,11 +88,13 @@ public sealed class RetailEdgeResponseOrderingTests
|
|||
transition.CollisionInfo.LastKnownContactPlane = new Plane(Vector3.UnitZ, 0f);
|
||||
|
||||
Vector3 failedCandidate = transition.SpherePath.BackupCheckPos;
|
||||
TransitionState result = transition.EdgeSlideAfterStepDownFailedForTest(
|
||||
bool stop = transition.EdgeSlideAfterStepDownFailedForTest(
|
||||
new PhysicsEngine(),
|
||||
stepDownHeight: 0.04f,
|
||||
zVal: PhysicsGlobals.FloorZ);
|
||||
zVal: PhysicsGlobals.FloorZ,
|
||||
out TransitionState result);
|
||||
|
||||
Assert.True(stop);
|
||||
Assert.Equal(TransitionState.OK, result);
|
||||
Assert.Equal(failedCandidate, transition.SpherePath.CheckPos);
|
||||
Assert.False(transition.CollisionInfo.ContactPlaneValid);
|
||||
|
|
@ -165,43 +168,158 @@ public sealed class RetailEdgeResponseOrderingTests
|
|||
transition.SpherePath.SaveCheckPos();
|
||||
transition.SpherePath.AddOffsetToCheckPos(new Vector3(0f, 0f, -0.25f));
|
||||
|
||||
TransitionState result = transition.EdgeSlideAfterStepDownFailedForTest(
|
||||
bool stop = transition.EdgeSlideAfterStepDownFailedForTest(
|
||||
new PhysicsEngine(),
|
||||
stepDownHeight: 0.04f,
|
||||
zVal: PhysicsGlobals.FloorZ);
|
||||
zVal: PhysicsGlobals.FloorZ,
|
||||
out TransitionState result);
|
||||
|
||||
// The restored point is inside the remembered polygon, so retail's
|
||||
// unconditional PrecipiceSlide returns Collided. AD-54's steep-plane
|
||||
// reroute instead returned Adjusted through CliffSlide.
|
||||
Assert.True(stop);
|
||||
Assert.Equal(TransitionState.Collided, result);
|
||||
Assert.Equal(failedCandidate, transition.SpherePath.CheckPos);
|
||||
Assert.False(transition.SpherePath.HasWalkablePolygon);
|
||||
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]
|
||||
public void MultiFrameSteepRoof_GraphAndFlatTraversalRemainExactAndDoNotWedge()
|
||||
{
|
||||
Vector3[] graph = RunSteepRoofTrace(preparedFlat: false);
|
||||
Vector3[] flat = RunSteepRoofTrace(preparedFlat: true);
|
||||
TraceRun graph = RunSteepRoofTrace(preparedFlat: false);
|
||||
TraceRun flat = RunSteepRoofTrace(preparedFlat: true);
|
||||
|
||||
Assert.Equal(graph, flat);
|
||||
Assert.Contains(graph, position =>
|
||||
position.X < 0f
|
||||
&& position.Z <= BSPStepUpFixtures.SphereRadius + 0.05f);
|
||||
AssertNoLongFrozenStreak(graph, maximumTicks: 15);
|
||||
AssertTraceParity(graph, flat);
|
||||
Assert.Contains(graph.Frames, frame =>
|
||||
frame.Result.Position.X < 0f
|
||||
&& frame.Result.Position.Z <= BSPStepUpFixtures.SphereRadius + 0.05f);
|
||||
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]
|
||||
public void MultiFrameFlatRoofLedge_GraphAndFlatTraversalRemainExactAndSlideAlongEdge()
|
||||
{
|
||||
Vector3[] graph = RunFlatRoofLedgeTrace(preparedFlat: false);
|
||||
Vector3[] flat = RunFlatRoofLedgeTrace(preparedFlat: true);
|
||||
TraceRun graph = RunFlatRoofLedgeTrace(preparedFlat: false, includeRoof: true);
|
||||
TraceRun flat = RunFlatRoofLedgeTrace(preparedFlat: true, includeRoof: true);
|
||||
TraceRun noRoof = RunFlatRoofLedgeTrace(preparedFlat: false, includeRoof: false);
|
||||
|
||||
Assert.Equal(graph, flat);
|
||||
AssertNoLongFrozenStreak(graph, maximumTicks: 15);
|
||||
Assert.True(graph[^1].Y > graph[0].Y + 0.25f,
|
||||
$"The roof-edge control made no along-edge progress: {graph[0]} -> {graph[^1]}.");
|
||||
AssertTraceParity(graph, flat);
|
||||
Assert.True(graph.LandedFrame > 0, "The collision-backed control never landed on the roof.");
|
||||
Assert.Equal(-1, noRoof.LandedFrame);
|
||||
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()
|
||||
|
|
@ -227,7 +345,7 @@ public sealed class RetailEdgeResponseOrderingTests
|
|||
new(-2f, 2f, 0f),
|
||||
];
|
||||
|
||||
private static Vector3[] RunSteepRoofTrace(bool preparedFlat)
|
||||
private static TraceRun RunSteepRoofTrace(bool preparedFlat)
|
||||
{
|
||||
var fixture = BSPStepUpFixtures.SlopedUnwalkable();
|
||||
PhysicsEngine engine = BuildCollisionEngine(fixture, preparedFlat, 0x0100E101u);
|
||||
|
|
@ -237,7 +355,7 @@ public sealed class RetailEdgeResponseOrderingTests
|
|||
var body = new PhysicsBody { TransientState = TransientStateFlags.Active };
|
||||
Vector3 position = new(0.5f, 0f, 3f);
|
||||
float velocityZ = 0f;
|
||||
var trace = new List<Vector3>(91) { position };
|
||||
var trace = new List<TraceFrame>(90);
|
||||
|
||||
for (int tick = 0; tick < 90; tick++)
|
||||
{
|
||||
|
|
@ -259,71 +377,102 @@ public sealed class RetailEdgeResponseOrderingTests
|
|||
body.Position = position;
|
||||
if (result.IsOnGround)
|
||||
velocityZ = 0f;
|
||||
trace.Add(position);
|
||||
ApplyContactResult(body, result);
|
||||
trace.Add(CaptureFrame(result, body));
|
||||
|
||||
if (position.X < 0f && position.Z <= radius + 0.05f)
|
||||
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();
|
||||
PhysicsEngine engine = BuildCollisionEngine(fixture, preparedFlat, 0x0100E102u);
|
||||
ResolvedPolygon roof = fixture.Resolved[BSPStepUpFixtures.FlatRoof_RoofId];
|
||||
PhysicsEngine engine = BuildCollisionEngine(
|
||||
fixture,
|
||||
preparedFlat,
|
||||
0x0100E102u,
|
||||
includeGeometry: includeRoof);
|
||||
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
|
||||
{
|
||||
Position = position,
|
||||
Orientation = Quaternion.Identity,
|
||||
ContactPlaneValid = true,
|
||||
ContactPlane = roof.Plane,
|
||||
ContactPlaneCellId = Cell,
|
||||
WalkablePolygonValid = true,
|
||||
WalkablePlane = roof.Plane,
|
||||
WalkableVertices = roof.Vertices,
|
||||
WalkableUp = Vector3.UnitZ,
|
||||
TransientState = TransientStateFlags.Active
|
||||
| TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable,
|
||||
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
|
||||
TransientState = TransientStateFlags.Active,
|
||||
};
|
||||
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(
|
||||
position,
|
||||
position + new Vector3(0.12f, 0.08f, 0f),
|
||||
position + new Vector3(0f, 0f, velocityZ * dt),
|
||||
Cell,
|
||||
radius,
|
||||
radius * 2f,
|
||||
stepUpHeight: 0.30f,
|
||||
stepDownHeight: 0.04f,
|
||||
isOnGround: true,
|
||||
isOnGround: body.OnWalkable,
|
||||
body,
|
||||
ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||
movingEntityId: 0x01000001u);
|
||||
|
||||
position = result.Position;
|
||||
body.Position = position;
|
||||
body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable);
|
||||
if (result.InContact)
|
||||
body.TransientState |= TransientStateFlags.Contact;
|
||||
if (result.OnWalkable)
|
||||
body.TransientState |= TransientStateFlags.OnWalkable;
|
||||
trace.Add(position);
|
||||
ApplyContactResult(body, result);
|
||||
trace.Add(CaptureFrame(result, body));
|
||||
|
||||
if (result.InContact && result.OnWalkable)
|
||||
{
|
||||
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(
|
||||
(PhysicsBSPNode Root, Dictionary<ushort, ResolvedPolygon> Resolved) fixture,
|
||||
bool preparedFlat,
|
||||
uint gfxObjId)
|
||||
uint gfxObjId,
|
||||
bool includeGeometry = true)
|
||||
{
|
||||
var normalized = new Dictionary<ushort, ResolvedPolygon>(fixture.Resolved.Count);
|
||||
foreach ((ushort id, ResolvedPolygon polygon) in fixture.Resolved)
|
||||
|
|
@ -346,12 +495,12 @@ public sealed class RetailEdgeResponseOrderingTests
|
|||
BoundingSphere = fixture.Root.BoundingSphere,
|
||||
};
|
||||
var cache = new PhysicsDataCache();
|
||||
if (preparedFlat)
|
||||
if (includeGeometry && preparedFlat)
|
||||
{
|
||||
cache.CollisionTraversalMode = CollisionTraversalMode.Flat;
|
||||
cache.CacheGfxObj(gfxObjId, FlatCollisionAssetBuilder.FlattenGfxObj(physics));
|
||||
}
|
||||
else
|
||||
else if (includeGeometry)
|
||||
{
|
||||
cache.RegisterGfxObjForTest(gfxObjId, physics);
|
||||
}
|
||||
|
|
@ -367,30 +516,186 @@ public sealed class RetailEdgeResponseOrderingTests
|
|||
Array.Empty<PortalPlane>(),
|
||||
0f,
|
||||
0f);
|
||||
engine.ShadowObjects.Register(
|
||||
gfxObjId,
|
||||
gfxObjId,
|
||||
Vector3.Zero,
|
||||
Quaternion.Identity,
|
||||
fixture.Root.BoundingSphere.Radius,
|
||||
0f,
|
||||
0f,
|
||||
0xA9B4FFFFu,
|
||||
ShadowCollisionType.BSP,
|
||||
1f);
|
||||
if (includeGeometry)
|
||||
{
|
||||
engine.ShadowObjects.Register(
|
||||
gfxObjId,
|
||||
gfxObjId,
|
||||
Vector3.Zero,
|
||||
Quaternion.Identity,
|
||||
fixture.Root.BoundingSphere.Radius,
|
||||
0f,
|
||||
0f,
|
||||
0xA9B4FFFFu,
|
||||
ShadowCollisionType.BSP,
|
||||
1f);
|
||||
}
|
||||
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;
|
||||
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
|
||||
: 0;
|
||||
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