fix(physics): restore retail step-down placement validation
This commit is contained in:
parent
4fbd93ecdb
commit
1fd5da67b4
4 changed files with 333 additions and 38 deletions
271
tests/AcDream.Core.Tests/Physics/RetailStepDownPlacementTests.cs
Normal file
271
tests/AcDream.Core.Tests/Physics/RetailStepDownPlacementTests.cs
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Pins retail <c>CTransition::step_down</c> (0x0050B2A0): every
|
||||
/// successfully supported candidate is re-tested with
|
||||
/// <see cref="InsertType.Placement"/>, regardless of whether the caller is
|
||||
/// ordinary contact maintenance or StepUp.
|
||||
/// </summary>
|
||||
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);
|
||||
int supportPasses = 0;
|
||||
int placementPasses = 0;
|
||||
var engine = new PhysicsEngine
|
||||
{
|
||||
TransitionCellCollisionTestHook = (candidate, phase, _, actual) =>
|
||||
{
|
||||
if (phase != TransitionCellCollisionPhase.Environment)
|
||||
return actual;
|
||||
|
||||
if (candidate.SpherePath.StepDown)
|
||||
{
|
||||
supportPasses++;
|
||||
candidate.CollisionInfo.SetContactPlane(
|
||||
new Plane(Vector3.UnitZ, 0f), Cell);
|
||||
}
|
||||
else if (candidate.SpherePath.InsertType == InsertType.Placement)
|
||||
{
|
||||
placementPasses++;
|
||||
}
|
||||
|
||||
return actual;
|
||||
},
|
||||
};
|
||||
|
||||
TransitionState result = transition.TransitionalInsertForTest(1, engine);
|
||||
|
||||
Assert.Equal(TransitionState.OK, result);
|
||||
Assert.Equal(1, supportPasses);
|
||||
Assert.Equal(1, placementPasses);
|
||||
Assert.Equal(InsertType.Transition, transition.SpherePath.InsertType);
|
||||
Assert.False(transition.SpherePath.StepDown);
|
||||
}
|
||||
|
||||
[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<ushort, ResolvedPolygon> 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<ushort, ResolvedPolygon> 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 (
|
||||
PhysicsBSPNode Root,
|
||||
Dictionary<ushort, ResolvedPolygon> 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<ushort, ResolvedPolygon>
|
||||
{
|
||||
[1] = new ResolvedPolygon
|
||||
{
|
||||
Id = 1,
|
||||
Vertices = vertices,
|
||||
Plane = new Plane(Vector3.UnitY, 0f),
|
||||
NumPoints = vertices.Length,
|
||||
SidesType = CullMode.None,
|
||||
},
|
||||
};
|
||||
return (root, resolved);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue