acdream/src/AcDream.Core/Physics/PhysicsDataCache.cs
Erik 874bcc8690 feat(physics): retail-faithful collision system port from ACE
Replace the patched collision system (~60-70% retail) with a faithful
port of ACE's BSPTree/BSPNode/BSPLeaf/Polygon collision pipeline.

BSPQuery.cs completely rewritten (1808 lines):
- Polygon-level: polygon_hits_sphere_precise (retail two-loop test),
  pos_hits_sphere, hits_sphere, walkable_hits_sphere, check_walkable,
  adjust_sphere_to_plane, find_crossed_edge, adjust_to_placement_poly
- BSP traversal: sphere_intersects_poly, find_walkable, hits_walkable,
  sphere_intersects_solid, sphere_intersects_solid_poly
- BSP tree-level: find_collisions (6-path dispatcher), step_sphere_up,
  step_sphere_down, slide_sphere, collide_with_pt, adjust_to_plane,
  placement_insert

PhysicsDataCache.cs: Added ResolvedPolygon type with pre-computed
vertex positions and face planes (matching ACE's Polygon constructor
which calls make_plane() at load time). Populated at cache time to
avoid per-collision-test vertex lookups.

TransitionTypes.cs: FindObjCollisions rewritten to use the retail
per-object FindCollisions 6-path dispatcher instead of the old
"find earliest t, then apply custom response" approach. BSP objects
now go through the same collision paths as indoor cell BSP.

The previous approach was explicitly rejected by the user after ~10
iterations of patches. This port follows the CLAUDE.md mandatory
workflow: decompile first → cross-reference ACE → port faithfully.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:18:43 +02:00

204 lines
7.8 KiB
C#

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;
/// <summary>
/// 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.
/// </summary>
public sealed class PhysicsDataCache
{
private readonly ConcurrentDictionary<uint, GfxObjPhysics> _gfxObj = new();
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
private readonly ConcurrentDictionary<uint, CellPhysics> _cellStruct = new();
/// <summary>
/// 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.
/// </summary>
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),
};
}
/// <summary>
/// Extract and cache the collision shape data from a Setup.
/// No-ops if the id is already cached.
/// </summary>
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,
};
}
/// <summary>
/// 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.
/// </summary>
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),
};
}
/// <summary>
/// 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.
/// </summary>
private static Dictionary<ushort, ResolvedPolygon> ResolvePolygons(
Dictionary<ushort, DatReaderWriter.Types.Polygon> polys,
VertexArray vertexArray)
{
var resolved = new Dictionary<ushort, ResolvedPolygon>(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;
}
/// <summary>
/// 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.
/// </summary>
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; }
}
/// <summary>Cached physics data for a single GfxObj part.</summary>
public sealed class GfxObjPhysics
{
public required PhysicsBSPTree BSP { get; init; }
public required Dictionary<ushort, Polygon> PhysicsPolygons { get; init; }
public Sphere? BoundingSphere { get; init; }
public required VertexArray Vertices { get; init; }
/// <summary>
/// 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.
/// </summary>
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
}
/// <summary>Cached collision shape data for a Setup (character/creature capsule).</summary>
public sealed class SetupPhysics
{
public List<CylSphere> CylSpheres { get; init; } = new();
public List<Sphere> Spheres { get; init; } = new();
public float Height { get; init; }
public float Radius { get; init; }
public float StepUpHeight { get; init; }
public float StepDownHeight { get; init; }
}
/// <summary>
/// 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.
/// </summary>
public sealed class CellPhysics
{
public required PhysicsBSPTree BSP { get; init; }
public required Dictionary<ushort, Polygon> PhysicsPolygons { get; init; }
public required VertexArray Vertices { get; init; }
public Matrix4x4 WorldTransform { get; init; }
public Matrix4x4 InverseWorldTransform { get; init; }
/// <summary>
/// Pre-resolved polygon data with vertex positions and computed planes.
/// </summary>
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
}