using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using DatReaderWriter.Enums; using DatReaderWriter.Types; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// Pins retail CTransition::step_down (0x0050B2A0): every /// successfully supported candidate is re-tested with /// , regardless of whether the caller is /// ordinary contact maintenance or StepUp. /// public sealed class RetailStepDownPlacementTests { private const uint Cell = 0xA9B40001u; private const float Radius = 0.48f; [Theory] [InlineData(false)] [InlineData(true)] public void OrdinaryContactMaintenance_AlwaysRunsFinalPlacement(bool twoSpheres) { Transition transition = MakeGroundedTransition(twoSpheres); const float supportWalkInterp = 0.375f; int supportPasses = 0; int placementPasses = 0; uint placementWalkInterpBits = 0; var engine = new PhysicsEngine { TransitionCellCollisionTestHook = (candidate, phase, _, actual) => { if (phase != TransitionCellCollisionPhase.Environment) return actual; if (candidate.SpherePath.StepDown) { supportPasses++; candidate.SpherePath.WalkInterp = supportWalkInterp; candidate.CollisionInfo.SetContactPlane( new Plane(Vector3.UnitZ, 0f), Cell); } else if (candidate.SpherePath.InsertType == InsertType.Placement) { placementPasses++; placementWalkInterpBits = BitConverter.SingleToUInt32Bits( candidate.SpherePath.WalkInterp); } return actual; }, }; TransitionState result = transition.TransitionalInsertForTest(1, engine); Assert.Equal(TransitionState.OK, result); Assert.Equal(1, supportPasses); Assert.Equal(1, placementPasses); Assert.Equal( BitConverter.SingleToUInt32Bits(supportWalkInterp), placementWalkInterpBits); Assert.Equal( BitConverter.SingleToUInt32Bits(supportWalkInterp), BitConverter.SingleToUInt32Bits(transition.SpherePath.WalkInterp)); Assert.Equal(InsertType.Transition, transition.SpherePath.InsertType); Assert.False(transition.SpherePath.StepDown); } [Fact] public void CheckWalkable_FailedNestedProbe_PreservesOuterBackupForEdgeSlide() { Transition transition = MakeGroundedTransition(twoSpheres: true); var sp = transition.SpherePath; Vector3 outerBackup = new(91.125f, -17.25f, 333.5f); const uint outerBackupCell = 0xA9B40077u; Vector3 nestedOrigin = new(2.125f, 3.25f, 4.5f); // Force check_walkables to fail so retail's nested downward probe is // exercised instead of the remembered-support early return. sp.SetCheckPos(nestedOrigin, Cell); sp.SetWalkable( new Plane(Vector3.UnitZ, 0f), [ new(100f, 100f, 0f), new(101f, 100f, 0f), new(101f, 101f, 0f), new(100f, 101f, 0f), ], Vector3.UnitZ); sp.BackupCheckPos = outerBackup; sp.BackupCheckCellId = outerBackupCell; int nestedProbes = 0; var engine = new PhysicsEngine { TransitionCellCollisionTestHook = (candidate, phase, _, actual) => { if (phase == TransitionCellCollisionPhase.Environment && candidate.SpherePath.CheckWalkable) { nestedProbes++; } return actual; }, }; bool walkable = transition.DoCheckWalkable(PhysicsGlobals.FloorZ, engine); Assert.False(walkable); Assert.True(nestedProbes > 0); AssertVectorBits(nestedOrigin, sp.CheckPos); Assert.Equal(Cell, sp.CheckCellId); AssertVectorBits(outerBackup, sp.BackupCheckPos); Assert.Equal(outerBackupCell, sp.BackupCheckCellId); // Branch 1 is the first retail edge-slide branch. Its restore must use // the distinctive outer failed candidate, not the nested probe origin. transition.ObjectInfo.State &= ~ObjectInfoState.EdgeSlide; sp.SetCheckPos(new Vector3(-8f, -9f, -10f), Cell); bool stop = transition.EdgeSlideAfterStepDownFailedForTest( engine, stepDownHeight: 0.04f, zVal: PhysicsGlobals.FloorZ, out TransitionState state); Assert.True(stop); Assert.Equal(TransitionState.OK, state); AssertVectorBits(outerBackup, sp.CheckPos); Assert.Equal(outerBackupCell, sp.CheckCellId); } [Fact] public void SupportedCandidate_OverlappingDuringPlacement_IsRejected() { Transition transition = MakeGroundedTransition(twoSpheres: true); int placementPasses = 0; var engine = new PhysicsEngine { TransitionCellCollisionTestHook = (candidate, phase, _, actual) => { if (phase != TransitionCellCollisionPhase.Environment) return actual; if (candidate.SpherePath.StepDown) { candidate.CollisionInfo.SetContactPlane( new Plane(Vector3.UnitZ, 0f), Cell); return actual; } if (candidate.SpherePath.InsertType == InsertType.Placement) { placementPasses++; return TransitionState.Collided; } return actual; }, }; bool accepted = transition.DoStepDownForTest( stepDownHeight: 0.04f, walkableZ: PhysicsGlobals.FloorZ, engine); Assert.False(accepted); Assert.Equal(1, placementPasses); Assert.Equal(InsertType.Transition, transition.SpherePath.InsertType); Assert.False(transition.SpherePath.StepDown); } [Fact] public void StepUp_UsesTheSameFinalPlacementPass() { Transition transition = MakeGroundedTransition(twoSpheres: true); int placementPasses = 0; var engine = new PhysicsEngine { TransitionCellCollisionTestHook = (candidate, phase, _, actual) => { if (phase != TransitionCellCollisionPhase.Environment) return actual; if (candidate.SpherePath.StepDown) { candidate.CollisionInfo.SetContactPlane( new Plane(Vector3.UnitZ, 0f), Cell); } else if (candidate.SpherePath.InsertType == InsertType.Placement) { placementPasses++; } return actual; }, }; bool accepted = transition.DoStepUp(Vector3.UnitX, engine); Assert.True(accepted); Assert.Equal(1, placementPasses); Assert.Equal(InsertType.Transition, transition.SpherePath.InsertType); Assert.False(transition.SpherePath.StepUp); Assert.False(transition.SpherePath.StepDown); } [Theory] [InlineData(false)] [InlineData(true)] public void PlacementDispatcher_AllowsExactWallTangency_AndRejectsOverlap( bool twoSpheres) { (PhysicsBSPNode root, Dictionary resolved) = BuildWall(); FlatPhysicsBsp flat = FlatCollisionAssetBuilder.FlattenPhysicsBsp(root, resolved); float tangentY = Radius; TransitionState graphTangent = RunPlacement( root, resolved, flat: null, tangentY, twoSpheres); TransitionState flatTangent = RunPlacement( root: null, resolved, flat, tangentY, twoSpheres); TransitionState graphOverlap = RunPlacement( root, resolved, flat: null, Radius - PhysicsGlobals.EPSILON * 2f, twoSpheres); TransitionState flatOverlap = RunPlacement( root: null, resolved, flat, Radius - PhysicsGlobals.EPSILON * 2f, twoSpheres); Assert.Equal(TransitionState.OK, graphTangent); Assert.Equal(graphTangent, flatTangent); Assert.Equal(TransitionState.Collided, graphOverlap); Assert.Equal(graphOverlap, flatOverlap); } private static TransitionState RunPlacement( PhysicsBSPNode? root, Dictionary resolved, FlatPhysicsBsp? flat, float centerY, bool twoSpheres) { var foot = new Sphere { Origin = new Vector3(0f, centerY, 0f), Radius = Radius, }; Sphere? head = twoSpheres ? new Sphere { Origin = new Vector3(0f, centerY, 0.875f), Radius = Radius, } : null; var transition = new Transition(); transition.SpherePath.InitPath( begin: Vector3.Zero, end: Vector3.Zero, Cell, Radius, sphereHeight: twoSpheres ? 1.355f : 0f); transition.SpherePath.InsertType = InsertType.Placement; return flat is null ? BSPQuery.FindCollisions( root, resolved, transition, foot, head, foot.Origin, Vector3.UnitZ, 1f) : FlatBspQuery.FindCollisions( flat, transition, foot, head, foot.Origin, Vector3.UnitZ, 1f); } private static Transition MakeGroundedTransition(bool twoSpheres) { Vector3 current = new(2f, 3f, 4f); Vector3 target = current + new Vector3(0.1f, 0f, 0f); var transition = new Transition(); transition.SpherePath.InitPath( current, target, Cell, Radius, sphereHeight: twoSpheres ? 1.835f : 0f); transition.SpherePath.SetCheckPos(target, Cell); transition.ObjectInfo.State = ObjectInfoState.Contact | ObjectInfoState.OnWalkable; transition.ObjectInfo.StepDown = true; transition.ObjectInfo.StepDownHeight = 0.04f; transition.ObjectInfo.StepUpHeight = 0.60f; transition.CollisionInfo.LastKnownContactPlane = new Plane(Vector3.UnitZ, 0f); transition.CollisionInfo.LastKnownContactPlaneValid = true; return transition; } private static void AssertVectorBits(Vector3 expected, Vector3 actual) { Assert.Equal( BitConverter.SingleToUInt32Bits(expected.X), BitConverter.SingleToUInt32Bits(actual.X)); Assert.Equal( BitConverter.SingleToUInt32Bits(expected.Y), BitConverter.SingleToUInt32Bits(actual.Y)); Assert.Equal( BitConverter.SingleToUInt32Bits(expected.Z), BitConverter.SingleToUInt32Bits(actual.Z)); } private static ( PhysicsBSPNode Root, Dictionary Resolved) BuildWall() { Vector3[] vertices = [ new(-2f, 0f, -2f), new(-2f, 0f, 2f), new( 2f, 0f, 2f), new( 2f, 0f, -2f), ]; var root = new PhysicsBSPNode { Type = BSPNodeType.Leaf, BoundingSphere = new Sphere { Origin = Vector3.Zero, Radius = 4f, }, }; root.Polygons.Add(1); var resolved = new Dictionary { [1] = new ResolvedPolygon { Id = 1, Vertices = vertices, Plane = new Plane(Vector3.UnitY, 0f), NumPoints = vertices.Length, SidesType = CullMode.None, }, }; return (root, resolved); } }