fix(physics): restore retail path-6 collision response
This commit is contained in:
parent
acec33eca8
commit
75b6f6b6c9
8 changed files with 523 additions and 223 deletions
321
tests/AcDream.Core.Tests/Physics/Ts4Path6ConformanceTests.cs
Normal file
321
tests/AcDream.Core.Tests/Physics/Ts4Path6ConformanceTests.cs
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Exact site tests for retail <c>BSPTREE::find_collisions</c> Path 6
|
||||
/// (<c>0x0053A793..0x0053A7DC</c>). Primary-sphere hits defer through
|
||||
/// SetCollide; secondary-only hits hard-stop. Neither BSP branch owns the
|
||||
/// persistent sliding normal.
|
||||
/// </summary>
|
||||
public sealed class Ts4Path6ConformanceTests
|
||||
{
|
||||
private const uint Cell = 0xA9B40001u;
|
||||
private const float Radius = BSPStepUpFixtures.SphereRadius;
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void PrimarySteepHit_GraphAndFlat_SetCollideWithExactState(
|
||||
bool seedSlidingNormal)
|
||||
{
|
||||
var fixture = Normalize(BSPStepUpFixtures.SlopedUnwalkable());
|
||||
FlatPhysicsBsp flat = FlatCollisionAssetBuilder.FlattenPhysicsBsp(
|
||||
fixture.Root,
|
||||
fixture.Resolved);
|
||||
Vector3 currentBody = new(0.5f, 0f, 1.1f);
|
||||
Vector3 targetBody = new(0.5f, 0f, 0.9f);
|
||||
Vector3 targetCenter = targetBody + new Vector3(0f, 0f, Radius);
|
||||
Vector3 expectedNormal = fixture.Resolved[
|
||||
BSPStepUpFixtures.SlopedUnwalkable_SlopeId].Plane.Normal;
|
||||
Vector3 seededSliding = Vector3.UnitY;
|
||||
|
||||
SiteOutcome graph = Run(
|
||||
fixture.Root,
|
||||
fixture.Resolved,
|
||||
flat: null,
|
||||
currentBody,
|
||||
targetBody,
|
||||
new Sphere { Origin = targetCenter, Radius = Radius },
|
||||
head: null,
|
||||
seedSlidingNormal,
|
||||
seededSliding);
|
||||
SiteOutcome prepared = Run(
|
||||
root: null,
|
||||
fixture.Resolved,
|
||||
flat,
|
||||
currentBody,
|
||||
targetBody,
|
||||
new Sphere { Origin = targetCenter, Radius = Radius },
|
||||
head: null,
|
||||
seedSlidingNormal,
|
||||
seededSliding);
|
||||
|
||||
Assert.Equal(graph.Bits, prepared.Bits);
|
||||
Assert.Equal(TransitionState.Adjusted, graph.State);
|
||||
Assert.True(graph.Collide);
|
||||
AssertVectorBits(targetBody, graph.CheckPos);
|
||||
Assert.Equal(Cell, graph.CheckCellId);
|
||||
AssertVectorBits(targetBody, graph.BackupCheckPos);
|
||||
Assert.Equal(Cell, graph.BackupCheckCellId);
|
||||
AssertVectorBits(expectedNormal, graph.StepUpNormal);
|
||||
AssertFloatBits(1f, graph.WalkInterp);
|
||||
AssertFloatBits(PhysicsGlobals.LandingZ, graph.WalkableAllowance);
|
||||
Assert.False(graph.CollisionNormalValid);
|
||||
Assert.Equal(seedSlidingNormal, graph.SlidingNormalValid);
|
||||
AssertVectorBits(
|
||||
seedSlidingNormal ? seededSliding : Vector3.Zero,
|
||||
graph.SlidingNormal);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void SecondaryOnlyHit_GraphAndFlat_HardStopsWithoutSetCollide(
|
||||
bool seedSlidingNormal)
|
||||
{
|
||||
(PhysicsBSPNode root, Dictionary<ushort, ResolvedPolygon> resolved) =
|
||||
BuildRaisedWall();
|
||||
FlatPhysicsBsp flat = FlatCollisionAssetBuilder.FlattenPhysicsBsp(
|
||||
root,
|
||||
resolved);
|
||||
Vector3 currentBody = new(0.1f, 0f, 0f);
|
||||
Vector3 targetBody = new(0.35f, 0f, 0f);
|
||||
var foot = new Sphere
|
||||
{
|
||||
Origin = targetBody + new Vector3(0f, 0f, Radius),
|
||||
Radius = Radius,
|
||||
};
|
||||
var head = new Sphere
|
||||
{
|
||||
Origin = targetBody + new Vector3(0f, 0f, 0.8f),
|
||||
Radius = Radius,
|
||||
};
|
||||
Vector3 seededSliding = Vector3.UnitY;
|
||||
|
||||
SiteOutcome graph = Run(
|
||||
root,
|
||||
resolved,
|
||||
flat: null,
|
||||
currentBody,
|
||||
targetBody,
|
||||
foot,
|
||||
head,
|
||||
seedSlidingNormal,
|
||||
seededSliding);
|
||||
SiteOutcome prepared = Run(
|
||||
root: null,
|
||||
resolved,
|
||||
flat,
|
||||
currentBody,
|
||||
targetBody,
|
||||
foot,
|
||||
head,
|
||||
seedSlidingNormal,
|
||||
seededSliding);
|
||||
|
||||
Assert.Equal(graph.Bits, prepared.Bits);
|
||||
Assert.Equal(TransitionState.Collided, graph.State);
|
||||
Assert.False(graph.Collide);
|
||||
AssertVectorBits(targetBody, graph.CheckPos);
|
||||
Assert.Equal(Cell, graph.CheckCellId);
|
||||
AssertVectorBits(new Vector3(91f, 92f, 93f), graph.BackupCheckPos);
|
||||
Assert.Equal(0xA9B40077u, graph.BackupCheckCellId);
|
||||
AssertVectorBits(Vector3.Zero, graph.StepUpNormal);
|
||||
AssertFloatBits(0.625f, graph.WalkInterp);
|
||||
AssertFloatBits(0.8125f, graph.WalkableAllowance);
|
||||
Assert.True(graph.CollisionNormalValid);
|
||||
AssertFloatBits(-1f, graph.CollisionNormal.X);
|
||||
Assert.Equal(0f, graph.CollisionNormal.Y);
|
||||
Assert.Equal(0f, graph.CollisionNormal.Z);
|
||||
Assert.Equal(seedSlidingNormal, graph.SlidingNormalValid);
|
||||
AssertVectorBits(
|
||||
seedSlidingNormal ? seededSliding : Vector3.Zero,
|
||||
graph.SlidingNormal);
|
||||
}
|
||||
|
||||
private static SiteOutcome Run(
|
||||
PhysicsBSPNode? root,
|
||||
Dictionary<ushort, ResolvedPolygon> resolved,
|
||||
FlatPhysicsBsp? flat,
|
||||
Vector3 currentBody,
|
||||
Vector3 targetBody,
|
||||
Sphere foot,
|
||||
Sphere? head,
|
||||
bool seedSlidingNormal,
|
||||
Vector3 seededSliding)
|
||||
{
|
||||
var transition = new Transition();
|
||||
transition.SpherePath.InitPath(
|
||||
currentBody,
|
||||
targetBody,
|
||||
Cell,
|
||||
Radius,
|
||||
sphereHeight: head is null ? 0f : 1f);
|
||||
transition.SpherePath.SetCheckPos(targetBody, Cell);
|
||||
transition.SpherePath.BackupCheckPos = new Vector3(91f, 92f, 93f);
|
||||
transition.SpherePath.BackupCheckCellId = 0xA9B40077u;
|
||||
transition.SpherePath.WalkInterp = 0.625f;
|
||||
transition.SpherePath.WalkableAllowance = 0.8125f;
|
||||
if (seedSlidingNormal)
|
||||
transition.CollisionInfo.SetSlidingNormal(seededSliding);
|
||||
|
||||
TransitionState state = flat is null
|
||||
? BSPQuery.FindCollisions(
|
||||
root,
|
||||
resolved,
|
||||
transition,
|
||||
foot,
|
||||
head,
|
||||
currentBody,
|
||||
Vector3.UnitZ,
|
||||
1f)
|
||||
: FlatBspQuery.FindCollisions(
|
||||
flat,
|
||||
transition,
|
||||
foot,
|
||||
head,
|
||||
currentBody,
|
||||
Vector3.UnitZ,
|
||||
1f);
|
||||
|
||||
SpherePath path = transition.SpherePath;
|
||||
CollisionInfo collision = transition.CollisionInfo;
|
||||
return new SiteOutcome(
|
||||
state,
|
||||
path.Collide,
|
||||
path.CheckPos,
|
||||
path.CheckCellId,
|
||||
path.BackupCheckPos,
|
||||
path.BackupCheckCellId,
|
||||
path.StepUpNormal,
|
||||
path.WalkInterp,
|
||||
path.WalkableAllowance,
|
||||
collision.CollisionNormalValid,
|
||||
collision.CollisionNormal,
|
||||
collision.SlidingNormalValid,
|
||||
collision.SlidingNormal,
|
||||
Signature(state, path, collision));
|
||||
}
|
||||
|
||||
private static (
|
||||
PhysicsBSPNode Root,
|
||||
Dictionary<ushort, ResolvedPolygon> Resolved) BuildRaisedWall()
|
||||
{
|
||||
Vector3[] vertices =
|
||||
[
|
||||
new(0.5f, -1f, 0.55f),
|
||||
new(0.5f, -1f, 2.5f),
|
||||
new(0.5f, 1f, 2.5f),
|
||||
new(0.5f, 1f, 0.55f),
|
||||
];
|
||||
var root = new PhysicsBSPNode
|
||||
{
|
||||
Type = BSPNodeType.Leaf,
|
||||
BoundingSphere = new Sphere
|
||||
{
|
||||
Origin = new Vector3(0.5f, 0f, 1.5f),
|
||||
Radius = 4f,
|
||||
},
|
||||
};
|
||||
root.Polygons.Add(1);
|
||||
var resolved = new Dictionary<ushort, ResolvedPolygon>
|
||||
{
|
||||
[1] = new ResolvedPolygon
|
||||
{
|
||||
Id = 1,
|
||||
Vertices = vertices,
|
||||
Plane = new Plane(-Vector3.UnitX, 0.5f),
|
||||
NumPoints = vertices.Length,
|
||||
SidesType = CullMode.None,
|
||||
},
|
||||
};
|
||||
return (root, resolved);
|
||||
}
|
||||
|
||||
private static (
|
||||
PhysicsBSPNode Root,
|
||||
Dictionary<ushort, ResolvedPolygon> Resolved) Normalize(
|
||||
(PhysicsBSPNode Root, Dictionary<ushort, ResolvedPolygon> Resolved) fixture)
|
||||
{
|
||||
var resolved = new Dictionary<ushort, ResolvedPolygon>(fixture.Resolved.Count);
|
||||
foreach ((ushort id, ResolvedPolygon polygon) in fixture.Resolved)
|
||||
{
|
||||
resolved.Add(id, new ResolvedPolygon
|
||||
{
|
||||
Id = id,
|
||||
Vertices = polygon.Vertices,
|
||||
Plane = polygon.Plane,
|
||||
NumPoints = polygon.NumPoints,
|
||||
SidesType = polygon.SidesType,
|
||||
});
|
||||
}
|
||||
|
||||
return (fixture.Root, resolved);
|
||||
}
|
||||
|
||||
private static string Signature(
|
||||
TransitionState state,
|
||||
SpherePath path,
|
||||
CollisionInfo collision)
|
||||
{
|
||||
var bits = new StringBuilder(256);
|
||||
bits.Append((int)state).Append('|').Append(path.Collide ? 1 : 0).Append('|');
|
||||
Append(bits, path.CheckPos);
|
||||
bits.Append(path.CheckCellId.ToString("X8")).Append('|');
|
||||
Append(bits, path.BackupCheckPos);
|
||||
bits.Append(path.BackupCheckCellId.ToString("X8")).Append('|');
|
||||
Append(bits, path.StepUpNormal);
|
||||
Append(bits, path.WalkInterp);
|
||||
Append(bits, path.WalkableAllowance);
|
||||
bits.Append(collision.CollisionNormalValid ? 1 : 0).Append('|');
|
||||
Append(bits, collision.CollisionNormal);
|
||||
bits.Append(collision.SlidingNormalValid ? 1 : 0).Append('|');
|
||||
Append(bits, collision.SlidingNormal);
|
||||
return bits.ToString();
|
||||
}
|
||||
|
||||
private static void Append(StringBuilder target, float value) =>
|
||||
target.Append(BitConverter.SingleToUInt32Bits(value).ToString("X8")).Append('|');
|
||||
|
||||
private static void Append(StringBuilder target, Vector3 value)
|
||||
{
|
||||
Append(target, value.X);
|
||||
Append(target, value.Y);
|
||||
Append(target, value.Z);
|
||||
}
|
||||
|
||||
private static void AssertFloatBits(float expected, float actual) =>
|
||||
Assert.Equal(
|
||||
BitConverter.SingleToUInt32Bits(expected),
|
||||
BitConverter.SingleToUInt32Bits(actual));
|
||||
|
||||
private static void AssertVectorBits(Vector3 expected, Vector3 actual)
|
||||
{
|
||||
AssertFloatBits(expected.X, actual.X);
|
||||
AssertFloatBits(expected.Y, actual.Y);
|
||||
AssertFloatBits(expected.Z, actual.Z);
|
||||
}
|
||||
|
||||
private sealed record SiteOutcome(
|
||||
TransitionState State,
|
||||
bool Collide,
|
||||
Vector3 CheckPos,
|
||||
uint CheckCellId,
|
||||
Vector3 BackupCheckPos,
|
||||
uint BackupCheckCellId,
|
||||
Vector3 StepUpNormal,
|
||||
float WalkInterp,
|
||||
float WalkableAllowance,
|
||||
bool CollisionNormalValid,
|
||||
Vector3 CollisionNormal,
|
||||
bool SlidingNormalValid,
|
||||
Vector3 SlidingNormal,
|
||||
string Bits);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue