From 9ea8ae51915c26c651e5ff7b6c097084b4fa2dd2 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 13 Apr 2026 23:44:55 +0200 Subject: [PATCH] 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 --- src/AcDream.Core/Physics/TransitionTypes.cs | 271 ++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 src/AcDream.Core/Physics/TransitionTypes.cs diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs new file mode 100644 index 0000000..7c973ff --- /dev/null +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -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, +} + +/// +/// Per-object flags and properties for the transition system. +/// ACE: ObjectInfo. Decompiled: struct at transition + various offsets. +/// +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); +} + +/// +/// Accumulated collision results for the current transition. +/// ACE: CollisionInfo. +/// +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 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; + } +} + +/// +/// Movement path descriptor — tracks sphere positions in multiple +/// coordinate frames during collision resolution. +/// ACE: SpherePath. +/// +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); + } + + /// + /// Initialize the path for a simple point-to-point movement. + /// + 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; + } + } +} + +/// +/// Physics constants matching the retail AC client. +/// ACE: PhysicsGlobals. Decompiled: various DAT_ addresses. +/// +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 +} + +/// +/// The main collision transition orchestrator. +/// ACE: Transition. Decompiled: CTransition. +/// Stub class — algorithm methods added in Task 6b-6d. +/// +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) { ... } +}