using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using DatReaderWriter.Types; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// Pins the branch order in retail CTransition::transitional_insert, /// CTransition::edge_slide, and CTransition::cliff_slide. /// These cases distinguish the retail implementation from the four former /// acdream compensations tracked as AP-3, AP-4, AD-53, and AD-54. /// public sealed class RetailEdgeResponseOrderingTests { private const uint Cell = 0xA9B40001u; [Fact] public void TransitionalInsert_ValidSteepContact_ReturnsBeforeOrdinaryStepDownTail() { 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 = 2f; Vector3 untouchedBackup = new(97f, 98f, 99f); const uint untouchedBackupCell = 0xA9B40044u; transition.SpherePath.BackupCheckPos = untouchedBackup; transition.SpherePath.BackupCheckCellId = untouchedBackupCell; var steep = new Plane(Vector3.Normalize(new Vector3(1f, 0f, 0.25f)), 0f); var engine = new PhysicsEngine { TransitionCellCollisionTestHook = (candidate, phase, _, actual) => { if (phase == TransitionCellCollisionPhase.Objects) candidate.CollisionInfo.SetContactPlane(steep, Cell, isWater: true); return actual; }, }; TransitionState result = transition.TransitionalInsertForTest(1, engine); Assert.Equal(TransitionState.OK, result); Assert.True(transition.CollisionInfo.ContactPlaneValid); Assert.True(transition.CollisionInfo.ContactPlaneIsWater); Assert.Equal(steep, transition.CollisionInfo.ContactPlane); Assert.Equal(untouchedBackup, transition.SpherePath.BackupCheckPos); Assert.Equal(untouchedBackupCell, transition.SpherePath.BackupCheckCellId); } [Theory] [InlineData(1, 0.5f, 2.0f, 0.25f, 1)] [InlineData(2, 0.5f, 2.0f, 1.00f, 2)] [InlineData(1, 0.5f, 0.75f, 0.75f, 1)] [InlineData(2, 0.5f, 0.75f, 0.75f, 1)] public void StepDownProbePlan_PreservesRetailOneVersusTwoSphereSplit( int sphereCount, float radius, float requestedHeight, float expectedProbeHeight, int expectedProbeCount) { (float probeHeight, int probeCount) = Transition.GetStepDownProbePlan( sphereCount, radius, requestedHeight); Assert.Equal(expectedProbeHeight, probeHeight); Assert.Equal(expectedProbeCount, probeCount); } [Fact] public void EdgeSlide_NotOnWalkableSteepContact_RestoresBeforeCliffSlide() { var transition = MakeFailedStepDownTransition(); transition.ObjectInfo.State = ObjectInfoState.EdgeSlide; transition.CollisionInfo.ContactPlaneValid = true; transition.CollisionInfo.ContactPlane = new Plane(Vector3.Normalize(new Vector3(1f, 0f, 0.25f)), 0f); transition.CollisionInfo.ContactPlaneIsWater = true; transition.CollisionInfo.LastKnownContactPlaneValid = true; transition.CollisionInfo.LastKnownContactPlane = new Plane(Vector3.UnitZ, 0f); Vector3 failedCandidate = transition.SpherePath.BackupCheckPos; TransitionState result = transition.EdgeSlideAfterStepDownFailedForTest( new PhysicsEngine(), stepDownHeight: 0.04f, zVal: PhysicsGlobals.FloorZ); Assert.Equal(TransitionState.OK, result); Assert.Equal(failedCandidate, transition.SpherePath.CheckPos); Assert.False(transition.CollisionInfo.ContactPlaneValid); Assert.False(transition.CollisionInfo.ContactPlaneIsWater); Assert.False(transition.CollisionInfo.CollisionNormalValid); } [Fact] public void CliffSlide_UsesOnlyLastKnownContactPlaneNormal() { var transition = MakeFailedStepDownTransition(); // A qualifying remembered walkable normal deliberately points along Y. // The former AD-53 fallback consumed it; retail consumes the explicit // last-known contact normal below and therefore resolves along -X. Plane rememberedWalkable = new(Vector3.Normalize(new Vector3(0f, 1f, 1f)), 0f); transition.SpherePath.SetWalkable( rememberedWalkable, SquareOnPlaneZ0(), Vector3.UnitZ); transition.SpherePath.ClearWalkable(); transition.CollisionInfo.LastKnownContactPlaneValid = true; transition.CollisionInfo.LastKnownContactPlane = new Plane(Vector3.UnitZ, 0f); Plane steepContact = new(Vector3.Normalize(new Vector3(1f, 0f, 0.5f)), 0f); TransitionState result = transition.CliffSlideForTest(steepContact); Assert.Equal(TransitionState.Adjusted, result); Assert.True(transition.CollisionInfo.CollisionNormalValid); Assert.True(Vector3.Distance(-Vector3.UnitX, transition.CollisionInfo.CollisionNormal) < 0.0001f); } [Fact] public void CliffSlide_InvalidDefaultLastKnownPlane_TakesDegenerateOkReturn() { var transition = MakeFailedStepDownTransition(); transition.CollisionInfo.LastKnownContactPlaneValid = false; transition.CollisionInfo.LastKnownContactPlane = default; Plane steepContact = new(Vector3.Normalize(new Vector3(1f, 0f, 0.5f)), 0f); TransitionState result = transition.CliffSlideForTest(steepContact); Assert.Equal(TransitionState.OK, result); Assert.False(transition.CollisionInfo.CollisionNormalValid); } [Fact] public void EdgeSlide_StoredSteepWalkable_AlwaysRoutesToPrecipiceSlide() { var transition = MakeFailedStepDownTransition(); transition.ObjectInfo.State = ObjectInfoState.Contact | ObjectInfoState.OnWalkable | ObjectInfoState.EdgeSlide; transition.CollisionInfo.ContactPlaneValid = false; transition.CollisionInfo.LastKnownContactPlaneValid = true; transition.CollisionInfo.LastKnownContactPlane = new Plane(Vector3.UnitZ, 0f); Vector3 steepNormal = Vector3.Normalize(new Vector3(-2f, 0f, 1f)); var steepPlane = new Plane(steepNormal, 0f); Vector3[] steepQuad = [ new(0f, -1f, 0f), new(1f, -1f, 2f), new(1f, 1f, 2f), new(0f, 1f, 0f), ]; transition.SpherePath.SetWalkable(steepPlane, steepQuad, Vector3.UnitZ); Vector3 failedCandidate = new(0.5f, 0f, 1f); transition.SpherePath.SetCheckPos(failedCandidate, Cell); transition.SpherePath.SaveCheckPos(); transition.SpherePath.AddOffsetToCheckPos(new Vector3(0f, 0f, -0.25f)); TransitionState result = transition.EdgeSlideAfterStepDownFailedForTest( new PhysicsEngine(), stepDownHeight: 0.04f, zVal: PhysicsGlobals.FloorZ); // 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.Equal(TransitionState.Collided, result); Assert.Equal(failedCandidate, transition.SpherePath.CheckPos); Assert.False(transition.SpherePath.HasWalkablePolygon); Assert.False(transition.CollisionInfo.CollisionNormalValid); } [Fact] public void MultiFrameSteepRoof_GraphAndFlatTraversalRemainExactAndDoNotWedge() { Vector3[] graph = RunSteepRoofTrace(preparedFlat: false); Vector3[] 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); } [Fact] public void MultiFrameFlatRoofLedge_GraphAndFlatTraversalRemainExactAndSlideAlongEdge() { Vector3[] graph = RunFlatRoofLedgeTrace(preparedFlat: false); Vector3[] flat = RunFlatRoofLedgeTrace(preparedFlat: true); 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]}."); } private static Transition MakeFailedStepDownTransition() { Vector3 current = Vector3.Zero; Vector3 failedCandidate = new(1f, 0f, 0f); var transition = BSPStepUpFixtures.MakeGroundedTransition( current, failedCandidate, cellId: Cell); transition.ObjectInfo.State |= ObjectInfoState.EdgeSlide; transition.SpherePath.SetCheckPos(failedCandidate, Cell); transition.SpherePath.SaveCheckPos(); transition.SpherePath.AddOffsetToCheckPos(new Vector3(0f, 0f, -0.25f)); return transition; } private static Vector3[] SquareOnPlaneZ0() => [ new(-2f, -2f, 0f), new( 2f, -2f, 0f), new( 2f, 2f, 0f), new(-2f, 2f, 0f), ]; private static Vector3[] RunSteepRoofTrace(bool preparedFlat) { var fixture = BSPStepUpFixtures.SlopedUnwalkable(); PhysicsEngine engine = BuildCollisionEngine(fixture, preparedFlat, 0x0100E101u); float radius = BSPStepUpFixtures.SphereRadius; const float dt = 1f / 30f; const float gravity = -9.8f; var body = new PhysicsBody { TransientState = TransientStateFlags.Active }; Vector3 position = new(0.5f, 0f, 3f); float velocityZ = 0f; var trace = new List(91) { position }; for (int tick = 0; tick < 90; tick++) { velocityZ += gravity * dt; ResolveResult result = engine.ResolveWithTransition( position, position + new Vector3(0f, 0f, velocityZ * dt), Cell, radius, radius * 2f, stepUpHeight: 0.30f, stepDownHeight: 0.04f, isOnGround: false, body, ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide, movingEntityId: 0x01000000u); position = result.Position; body.Position = position; if (result.IsOnGround) velocityZ = 0f; trace.Add(position); if (position.X < 0f && position.Z <= radius + 0.05f) break; } return trace.ToArray(); } private static Vector3[] RunFlatRoofLedgeTrace(bool preparedFlat) { var fixture = BSPStepUpFixtures.FlatRoof(); PhysicsEngine engine = BuildCollisionEngine(fixture, preparedFlat, 0x0100E102u); ResolvedPolygon roof = fixture.Resolved[BSPStepUpFixtures.FlatRoof_RoofId]; float radius = BSPStepUpFixtures.SphereRadius; Vector3 position = new(1.55f, -0.75f, 3f); 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, }; var trace = new List(13) { position }; 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: true, 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); } return trace.ToArray(); } private static PhysicsEngine BuildCollisionEngine( (PhysicsBSPNode Root, Dictionary Resolved) fixture, bool preparedFlat, uint gfxObjId) { var normalized = new Dictionary(fixture.Resolved.Count); foreach ((ushort id, ResolvedPolygon polygon) in fixture.Resolved) { normalized.Add(id, new ResolvedPolygon { Id = id, Vertices = polygon.Vertices, Plane = polygon.Plane, NumPoints = polygon.NumPoints, SidesType = polygon.SidesType, }); } var physics = new GfxObjPhysics { SourceId = gfxObjId, BSP = new PhysicsBSPTree { Root = fixture.Root }, Resolved = normalized, BoundingSphere = fixture.Root.BoundingSphere, }; var cache = new PhysicsDataCache(); if (preparedFlat) { cache.CollisionTraversalMode = CollisionTraversalMode.Flat; cache.CacheGfxObj(gfxObjId, FlatCollisionAssetBuilder.FlattenGfxObj(physics)); } else { cache.RegisterGfxObjForTest(gfxObjId, physics); } var heights = new byte[81]; var heightTable = new float[256]; Array.Fill(heightTable, -1000f); var engine = new PhysicsEngine { DataCache = cache }; engine.AddLandblock( 0xA9B40000u, new TerrainSurface(heights, heightTable), Array.Empty(), Array.Empty(), 0f, 0f); 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) { int streak = 0; for (int i = 1; i < trace.Length; i++) { streak = Vector3.Distance(trace[i - 1], trace[i]) < 0.001f ? streak + 1 : 0; Assert.True(streak <= maximumTicks, $"Trace froze for {streak} ticks at {trace[i]}."); } } }