From 4fbd93ecdbff3e0bf522b4760f406667fbee99c1 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 31 Jul 2026 13:03:46 +0200 Subject: [PATCH] fix(physics): preserve retail edge-slide stop semantics --- ...0-response-layer-edge-family-pseudocode.md | 23 + src/AcDream.Core/Physics/TransitionTypes.cs | 48 +- .../RetailEdgeResponseOrderingTests.cs | 433 +++++++++++++++--- 3 files changed, 427 insertions(+), 77 deletions(-) diff --git a/docs/research/2026-07-30-response-layer-edge-family-pseudocode.md b/docs/research/2026-07-30-response-layer-edge-family-pseudocode.md index b284efa4..2e4a3939 100644 --- a/docs/research/2026-07-30-response-layer-edge-family-pseudocode.md +++ b/docs/research/2026-07-30-response-layer-edge-family-pseudocode.md @@ -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. + --- diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index 6889b0af..c943be88 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -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) diff --git a/tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs b/tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs index d8a23835..e75cbe45 100644 --- a/tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs +++ b/tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs @@ -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 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(91) { position }; + var trace = new List(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(13) { position }; + var trace = new List(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 Resolved) fixture, bool preparedFlat, - uint gfxObjId) + uint gfxObjId, + bool includeGeometry = true) { var normalized = new Dictionary(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(), 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 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 Frames, + int LandedFrame, + int LedgeStartFrame); + + private sealed record TraceFrame( + ResolveResult Result, + string ResolveBits, + string BodyBits, + bool BodyContactPlaneValid, + Plane BodyContactPlane, + bool BodyWalkablePolygonValid, + TransientStateFlags BodyTransientState); }