feat(physics): Transition system data structures

SpherePath, CollisionInfo, ObjectInfo, TransitionState, PhysicsGlobals.
Types match the pseudocode from transition_pseudocode.md, faithful to
decompiled CTransition + ACE Transition.cs naming.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-13 23:44:55 +02:00
parent 13f56b62a0
commit 9ea8ae5191

View file

@ -0,0 +1,271 @@
using System.Numerics;
using DatReaderWriter.Types;
namespace AcDream.Core.Physics;
public enum TransitionState
{
Invalid = 0,
OK = 1,
Collided = 2,
Adjusted = 3,
Slid = 4,
}
public enum InsertType
{
Transition = 0,
Placement = 1,
InitialPlacement = 2,
}
[Flags]
public enum ObjectInfoState : uint
{
None = 0x000,
Contact = 0x001,
OnWalkable = 0x002,
IsViewer = 0x004,
PathClipped = 0x008,
FreeRotate = 0x010,
PerfectClip = 0x040,
IsImpenetrable = 0x080,
IsPlayer = 0x100,
EdgeSlide = 0x200,
IgnoreCreatures = 0x400,
}
/// <summary>
/// Per-object flags and properties for the transition system.
/// ACE: ObjectInfo. Decompiled: struct at transition + various offsets.
/// </summary>
public sealed class ObjectInfo
{
public ObjectInfoState State;
public float StepUpHeight = 0.01f; // PhysicsGlobals.DefaultStepHeight
public float StepDownHeight = 0.04f;
public bool Ethereal;
public bool StepDown = true;
public float Scale = 1.0f;
// Convenience flag checks
public bool Contact => State.HasFlag(ObjectInfoState.Contact);
public bool OnWalkable => State.HasFlag(ObjectInfoState.OnWalkable);
public bool IsViewer => State.HasFlag(ObjectInfoState.IsViewer);
public bool IsPlayer => State.HasFlag(ObjectInfoState.IsPlayer);
public bool EdgeSlide => State.HasFlag(ObjectInfoState.EdgeSlide);
public bool PathClipped => State.HasFlag(ObjectInfoState.PathClipped);
public bool FreeRotate => State.HasFlag(ObjectInfoState.FreeRotate);
}
/// <summary>
/// Accumulated collision results for the current transition.
/// ACE: CollisionInfo.
/// </summary>
public sealed class CollisionInfo
{
public bool ContactPlaneValid;
public Plane ContactPlane;
public uint ContactPlaneCellId;
public bool ContactPlaneIsWater;
public bool LastKnownContactPlaneValid;
public Plane LastKnownContactPlane;
public uint LastKnownContactPlaneCellId;
public bool LastKnownContactPlaneIsWater;
public bool SlidingNormalValid;
public Vector3 SlidingNormal; // XY only (Z zeroed)
public bool CollisionNormalValid;
public Vector3 CollisionNormal;
public bool CollidedWithEnvironment;
public int FramesStationaryFall;
public Vector3 AdjustOffset;
public List<uint> CollideObjectGuids = new();
public uint? LastCollidedObjectGuid;
public void SetContactPlane(Plane plane, uint cellId, bool isWater = false)
{
ContactPlaneValid = true;
ContactPlane = plane;
ContactPlaneCellId = cellId;
ContactPlaneIsWater = isWater;
LastKnownContactPlaneValid = true;
LastKnownContactPlane = plane;
LastKnownContactPlaneCellId = cellId;
LastKnownContactPlaneIsWater = isWater;
}
public void SetSlidingNormal(Vector3 normal)
{
SlidingNormalValid = true;
SlidingNormal = new Vector3(normal.X, normal.Y, 0f);
if (SlidingNormal.LengthSquared() > PhysicsGlobals.EpsilonSq)
SlidingNormal = Vector3.Normalize(SlidingNormal);
}
public void SetCollisionNormal(Vector3 normal)
{
CollisionNormalValid = true;
CollisionNormal = normal;
}
}
/// <summary>
/// Movement path descriptor — tracks sphere positions in multiple
/// coordinate frames during collision resolution.
/// ACE: SpherePath.
/// </summary>
public sealed class SpherePath
{
public int NumSphere = 1;
// Sphere arrays — index 0 = foot/body, index 1 = head (when NumSphere==2)
public readonly Sphere[] LocalSphere = new Sphere[2] { new(), new() };
public readonly Sphere[] GlobalSphere = new Sphere[2] { new(), new() };
public readonly Sphere[] GlobalCurrCenter = new Sphere[2] { new(), new() };
// Positions
public Vector3 BeginPos;
public Vector3 EndPos;
public Vector3 CurPos;
public Vector3 CheckPos;
public Quaternion BeginOrientation = Quaternion.Identity;
public Quaternion EndOrientation = Quaternion.Identity;
public Quaternion CurOrientation = Quaternion.Identity;
public Quaternion CheckOrientation = Quaternion.Identity;
// Cell tracking
public uint CurCellId;
public uint CheckCellId;
// Per-step offset
public Vector3 GlobalOffset;
// Step-up state
public bool StepUp;
public Vector3 StepUpNormal;
public bool Collide;
// Step-down state
public bool StepDown;
public float StepDownAmt;
public float WalkInterp = 1.0f;
// Walkable tracking
public bool WalkableValid;
public Plane WalkablePlane;
public float WalkableAllowance = PhysicsGlobals.FloorZ;
// Backup for restore
public Vector3 BackupCheckPos;
public uint BackupCheckCellId;
// Misc flags
public bool NegPolyHit;
public bool NegStepUp;
public Vector3 NegCollisionNormal;
public bool CheckWalkable;
public InsertType InsertType = InsertType.Transition;
public void SetCheckPos(Vector3 pos, uint cellId)
{
CheckPos = pos;
CheckCellId = cellId;
// Update global spheres to match new check position
for (int i = 0; i < NumSphere; i++)
{
GlobalSphere[i].Origin = LocalSphere[i].Origin + pos;
GlobalSphere[i].Radius = LocalSphere[i].Radius;
}
}
public void AddOffsetToCheckPos(Vector3 offset)
{
CheckPos += offset;
for (int i = 0; i < NumSphere; i++)
GlobalSphere[i].Origin += offset;
}
public void SaveCheckPos()
{
BackupCheckPos = CheckPos;
BackupCheckCellId = CheckCellId;
}
public void RestoreCheckPos()
{
SetCheckPos(BackupCheckPos, BackupCheckCellId);
}
/// <summary>
/// Initialize the path for a simple point-to-point movement.
/// </summary>
public void InitPath(Vector3 begin, Vector3 end, uint cellId,
float sphereRadius, float sphereHeight = 0f)
{
BeginPos = begin;
EndPos = end;
CurPos = begin;
CurCellId = cellId;
LocalSphere[0].Origin = new Vector3(0, 0, sphereRadius);
LocalSphere[0].Radius = sphereRadius;
if (sphereHeight > 0)
{
NumSphere = 2;
LocalSphere[1].Origin = new Vector3(0, 0, sphereHeight - sphereRadius);
LocalSphere[1].Radius = sphereRadius;
}
else
{
NumSphere = 1;
}
SetCheckPos(begin, cellId);
// Also init CurCenter
for (int i = 0; i < NumSphere; i++)
{
GlobalCurrCenter[i].Origin = LocalSphere[i].Origin + begin;
GlobalCurrCenter[i].Radius = LocalSphere[i].Radius;
}
}
}
/// <summary>
/// Physics constants matching the retail AC client.
/// ACE: PhysicsGlobals. Decompiled: various DAT_ addresses.
/// </summary>
public static class PhysicsGlobals
{
public const float EPSILON = 0.0002f;
public const float EpsilonSq = EPSILON * EPSILON;
public const float LandingZ = 0.0871557f;
public const float FloorZ = 0.6642f;
public const float DefaultStepHeight = 0.01f;
public const float Gravity = -9.8f;
public const float MaxVelocity = 50.0f;
public const float DummySphereRadius = 0.1f;
public const int MaxTransitionSteps = 30; // retail uses 30, ACE uses 1000
}
/// <summary>
/// The main collision transition orchestrator.
/// ACE: Transition. Decompiled: CTransition.
/// Stub class — algorithm methods added in Task 6b-6d.
/// </summary>
public sealed class Transition
{
public ObjectInfo ObjectInfo = new();
public SpherePath SpherePath = new();
public CollisionInfo CollisionInfo = new();
// Will be populated in Task 6b:
// public TransitionState FindTransitionalPosition(PhysicsEngine engine, PhysicsDataCache cache) { ... }
}