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; }
}