using System.Collections.Concurrent; using System.Numerics; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Types; using Plane = System.Numerics.Plane; namespace AcDream.Core.Physics; /// /// Thread-safe cache of physics-relevant data extracted from GfxObj and Setup /// dat objects during streaming. Populated by the streaming worker thread; /// read by the physics engine on the game/render thread. ConcurrentDictionary /// makes cross-thread access safe without a global lock. /// public sealed class PhysicsDataCache { private readonly ConcurrentDictionary _gfxObj = new(); private readonly ConcurrentDictionary _setup = new(); private readonly ConcurrentDictionary _cellStruct = new(); /// /// Extract and cache the physics BSP + polygon data from a GfxObj. /// No-ops if the id is already cached or the GfxObj has no physics data. /// public void CacheGfxObj(uint gfxObjId, GfxObj gfxObj) { if (_gfxObj.ContainsKey(gfxObjId)) return; if (!gfxObj.Flags.HasFlag(GfxObjFlags.HasPhysics)) return; if (gfxObj.PhysicsBSP?.Root is null) return; _gfxObj[gfxObjId] = new GfxObjPhysics { BSP = gfxObj.PhysicsBSP, PhysicsPolygons = gfxObj.PhysicsPolygons, BoundingSphere = gfxObj.PhysicsBSP.Root.BoundingSphere, Vertices = gfxObj.VertexArray, Resolved = ResolvePolygons(gfxObj.PhysicsPolygons, gfxObj.VertexArray), }; } /// /// Extract and cache the collision shape data from a Setup. /// No-ops if the id is already cached. /// public void CacheSetup(uint setupId, Setup setup) { if (_setup.ContainsKey(setupId)) return; _setup[setupId] = new SetupPhysics { CylSpheres = setup.CylSpheres ?? new(), Spheres = setup.Spheres ?? new(), Height = setup.Height, Radius = setup.Radius, StepUpHeight = setup.StepUpHeight, StepDownHeight = setup.StepDownHeight, }; } /// /// Extract and cache the physics BSP + polygon data from a CellStruct /// (indoor room geometry). No-ops if the id is already cached or the /// CellStruct has no physics BSP. /// public void CacheCellStruct(uint envCellId, CellStruct cellStruct, Matrix4x4 worldTransform) { if (_cellStruct.ContainsKey(envCellId)) return; if (cellStruct.PhysicsBSP?.Root is null) return; Matrix4x4.Invert(worldTransform, out var inverseTransform); _cellStruct[envCellId] = new CellPhysics { BSP = cellStruct.PhysicsBSP, PhysicsPolygons = cellStruct.PhysicsPolygons, Vertices = cellStruct.VertexArray, WorldTransform = worldTransform, InverseWorldTransform = inverseTransform, Resolved = ResolvePolygons(cellStruct.PhysicsPolygons, cellStruct.VertexArray), }; } /// /// Pre-resolve all physics polygons: lookup vertex positions from VertexArray /// and compute the face plane. Matches ACE's Polygon constructor which calls /// make_plane() and resolves Vertices from VertexIDs at load time. /// private static Dictionary ResolvePolygons( Dictionary polys, VertexArray vertexArray) { var resolved = new Dictionary(polys.Count); foreach (var (id, poly) in polys) { int numVerts = poly.VertexIds.Count; if (numVerts < 3) continue; var verts = new Vector3[numVerts]; bool valid = true; for (int i = 0; i < numVerts; i++) { ushort vid = (ushort)poly.VertexIds[i]; if (!vertexArray.Vertices.TryGetValue(vid, out var sv)) { valid = false; break; } verts[i] = sv.Origin; } if (!valid) continue; // Compute plane normal using ACE's make_plane algorithm: // fan cross-product accumulation + normalization. var normal = Vector3.Zero; for (int i = 1; i < numVerts - 1; i++) { var v1 = verts[i] - verts[0]; var v2 = verts[i + 1] - verts[0]; normal += Vector3.Cross(v1, v2); } float len = normal.Length(); if (len < 1e-8f) continue; normal /= len; // D = -(average dot(normal, vertex)) float dotSum = 0f; for (int i = 0; i < numVerts; i++) dotSum += Vector3.Dot(normal, verts[i]); float d = -(dotSum / numVerts); resolved[id] = new ResolvedPolygon { Vertices = verts, Plane = new Plane(normal, d), NumPoints = numVerts, SidesType = poly.SidesType, }; } return resolved; } public GfxObjPhysics? GetGfxObj(uint id) => _gfxObj.TryGetValue(id, out var p) ? p : null; public SetupPhysics? GetSetup(uint id) => _setup.TryGetValue(id, out var p) ? p : null; public CellPhysics? GetCellStruct(uint id) => _cellStruct.TryGetValue(id, out var p) ? p : null; public int GfxObjCount => _gfxObj.Count; public int SetupCount => _setup.Count; public int CellStructCount => _cellStruct.Count; } /// /// A physics polygon with pre-resolved vertex positions and pre-computed plane. /// ACE pre-computes these in its Polygon constructor; we do it at cache time /// to avoid per-collision-test vertex lookups. /// public sealed class ResolvedPolygon { public required Vector3[] Vertices { get; init; } public required Plane Plane { get; init; } public required int NumPoints { get; init; } public required CullMode SidesType { get; init; } } /// Cached physics data for a single GfxObj part. public sealed class GfxObjPhysics { public required PhysicsBSPTree BSP { get; init; } public required Dictionary PhysicsPolygons { get; init; } public Sphere? BoundingSphere { get; init; } public required VertexArray Vertices { get; init; } /// /// Pre-resolved polygon data with vertex positions and computed planes. /// Populated once at cache time so BSP queries don't pay per-test lookup cost. /// public required Dictionary Resolved { get; init; } } /// Cached collision shape data for a Setup (character/creature capsule). public sealed class SetupPhysics { public List CylSpheres { get; init; } = new(); public List Spheres { get; init; } = new(); public float Height { get; init; } public float Radius { get; init; } public float StepUpHeight { get; init; } public float StepDownHeight { get; init; } } /// /// Cached physics data for an indoor cell's room geometry (CellStruct). /// Used for wall/floor/ceiling collision in EnvCells. /// ACE: EnvCell.find_env_collisions queries CellStructure.PhysicsBSP. /// public sealed class CellPhysics { public required PhysicsBSPTree BSP { get; init; } public required Dictionary PhysicsPolygons { get; init; } public required VertexArray Vertices { get; init; } public Matrix4x4 WorldTransform { get; init; } public Matrix4x4 InverseWorldTransform { get; init; } /// /// Pre-resolved polygon data with vertex positions and computed planes. /// public required Dictionary Resolved { get; init; } }