fix(physics): preserve retail edge-slide stop semantics

This commit is contained in:
Erik 2026-07-31 13:03:46 +02:00
parent c559c48d80
commit 4fbd93ecdb
3 changed files with 427 additions and 77 deletions

View file

@ -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.
---

View file

@ -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)

View file

@ -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);
}