feat(physics): PhysicsDataCache + BSP sphere query
Load PhysicsBSP and PhysicsPolygons from GfxObj dats during streaming. BSPQuery.SphereIntersectsPoly traverses the tree for collision detection. Ported from decompiled FUN_00539270, cross-ref ACE BSPNode.sphere_intersects_poly. - PhysicsDataCache: thread-safe ConcurrentDictionary-backed cache of GfxObjPhysics (BSP tree + polygon dict + vertex array) and SetupPhysics (capsule dimensions). CacheGfxObj/CacheSetup are idempotent — safe to call at every dat load site. - BSPQuery.SphereIntersectsPoly: recursive BSP descent with bounding-sphere broad phase, leaf polygon test via existing CollisionPrimitives.SphereIntersectsPoly (FUN_00539500), and splitting-plane classification for internal nodes. - GameWindow: _physicsDataCache populated at all GfxObj/Setup dat load sites (streaming worker path, live-spawn path, ApplyLoadedTerrain render-thread path). - 6 new unit tests covering null node, bounding-sphere miss, leaf hit, no-contact, internal node recursion, and empty cache behaviour. All 447 tests green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0bec5d5296
commit
874d267117
4 changed files with 462 additions and 0 deletions
|
|
@ -37,6 +37,11 @@ public sealed class GameWindow : IDisposable
|
|||
// Phase B.3: physics engine — populated from the streaming pipeline.
|
||||
private readonly AcDream.Core.Physics.PhysicsEngine _physicsEngine = new();
|
||||
|
||||
// Task 4: physics data cache — BSP trees + collision shapes extracted from
|
||||
// GfxObj/Setup dats during streaming. Populated on the worker thread;
|
||||
// ConcurrentDictionary inside makes cross-thread access safe.
|
||||
private readonly AcDream.Core.Physics.PhysicsDataCache _physicsDataCache = new();
|
||||
|
||||
// Step 4: portal-based interior cell visibility.
|
||||
private readonly CellVisibility _cellVisibility = new();
|
||||
|
||||
|
|
@ -212,6 +217,8 @@ public sealed class GameWindow : IDisposable
|
|||
if (_dats is not null && (playerEntity.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u)
|
||||
{
|
||||
var playerSetup = _dats.Get<DatReaderWriter.DBObjs.Setup>(playerEntity.SourceGfxObjOrSetupId);
|
||||
if (playerSetup is not null)
|
||||
_physicsDataCache.CacheSetup(playerEntity.SourceGfxObjOrSetupId, playerSetup);
|
||||
_playerController.StepUpHeight = (playerSetup is not null && playerSetup.StepUpHeight > 0f)
|
||||
? playerSetup.StepUpHeight
|
||||
: 2f; // default human step height
|
||||
|
|
@ -581,6 +588,8 @@ public sealed class GameWindow : IDisposable
|
|||
// Hydrate mesh refs from the Setup dat. This is the same code path
|
||||
// used by the static scenery pipeline (see the Setup hydration above).
|
||||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(spawn.SetupTableId.Value);
|
||||
if (setup is not null)
|
||||
_physicsDataCache.CacheSetup(spawn.SetupTableId.Value, setup);
|
||||
if (setup is null)
|
||||
{
|
||||
_liveDropReasonSetupDatMissing++;
|
||||
|
|
@ -683,6 +692,7 @@ public sealed class GameWindow : IDisposable
|
|||
Console.WriteLine($"live: [STATUE] resolve part={pi} GfxObj 0x{parts[pi].GfxObjId:X8} missing");
|
||||
continue;
|
||||
}
|
||||
_physicsDataCache.CacheGfxObj(parts[pi].GfxObjId, partGfx);
|
||||
|
||||
if (isStatueDiag)
|
||||
Console.WriteLine($"live: [STATUE] resolve part={pi} gfx=0x{parts[pi].GfxObjId:X8} surfaces={partGfx.Surfaces.Count}");
|
||||
|
|
@ -724,6 +734,7 @@ public sealed class GameWindow : IDisposable
|
|||
var mr = parts[partIdx];
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
_staticMesh.EnsureUploaded(mr.GfxObjId, subMeshes);
|
||||
|
||||
|
|
@ -1147,8 +1158,11 @@ public sealed class GameWindow : IDisposable
|
|||
// Single GfxObj stab — identity part transform.
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(e.SourceGfxObjOrSetupId);
|
||||
if (gfx is not null)
|
||||
{
|
||||
_physicsDataCache.CacheGfxObj(e.SourceGfxObjOrSetupId, gfx);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(
|
||||
e.SourceGfxObjOrSetupId, System.Numerics.Matrix4x4.Identity));
|
||||
}
|
||||
}
|
||||
else if ((e.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u)
|
||||
{
|
||||
|
|
@ -1156,11 +1170,13 @@ public sealed class GameWindow : IDisposable
|
|||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(e.SourceGfxObjOrSetupId);
|
||||
if (setup is not null)
|
||||
{
|
||||
_physicsDataCache.CacheSetup(e.SourceGfxObjOrSetupId, setup);
|
||||
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
||||
foreach (var mr in flat)
|
||||
{
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
|
||||
meshRefs.Add(mr);
|
||||
}
|
||||
}
|
||||
|
|
@ -1258,6 +1274,7 @@ public sealed class GameWindow : IDisposable
|
|||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(spawn.ObjectId);
|
||||
if (gfx is not null)
|
||||
{
|
||||
_physicsDataCache.CacheGfxObj(spawn.ObjectId, gfx);
|
||||
// Sub-meshes pre-built CPU-side; upload deferred to ApplyLoadedTerrain.
|
||||
_ = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(spawn.ObjectId, scaleMat));
|
||||
|
|
@ -1268,11 +1285,13 @@ public sealed class GameWindow : IDisposable
|
|||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(spawn.ObjectId);
|
||||
if (setup is not null)
|
||||
{
|
||||
_physicsDataCache.CacheSetup(spawn.ObjectId, setup);
|
||||
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
||||
foreach (var mr in flat)
|
||||
{
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
|
||||
_ = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
// Compose: part's own transform, then the spawn's scale.
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(mr.GfxObjId, mr.PartTransform * scaleMat));
|
||||
|
|
@ -1384,6 +1403,7 @@ public sealed class GameWindow : IDisposable
|
|||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(stab.Id);
|
||||
if (gfx is not null)
|
||||
{
|
||||
_physicsDataCache.CacheGfxObj(stab.Id, gfx);
|
||||
_ = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(stab.Id, System.Numerics.Matrix4x4.Identity));
|
||||
}
|
||||
|
|
@ -1393,11 +1413,13 @@ public sealed class GameWindow : IDisposable
|
|||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(stab.Id);
|
||||
if (setup is not null)
|
||||
{
|
||||
_physicsDataCache.CacheSetup(stab.Id, setup);
|
||||
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
||||
foreach (var mr in flat)
|
||||
{
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
|
||||
_ = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
meshRefs.Add(mr);
|
||||
}
|
||||
|
|
@ -1720,6 +1742,7 @@ public sealed class GameWindow : IDisposable
|
|||
if ((meshRef.GfxObjId & 0xFF000000u) != 0x01000000u) continue;
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(meshRef.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
_physicsDataCache.CacheGfxObj(meshRef.GfxObjId, gfx);
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
_staticMesh.EnsureUploaded(meshRef.GfxObjId, subMeshes);
|
||||
}
|
||||
|
|
@ -2168,6 +2191,7 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(pe.SourceGfxObjOrSetupId);
|
||||
if (setup is null) return;
|
||||
_physicsDataCache.CacheSetup(pe.SourceGfxObjOrSetupId, setup);
|
||||
|
||||
// Build a minimal part template from the entity's current MeshRefs.
|
||||
var template = new (uint, IReadOnlyDictionary<uint, uint>?)[pe.MeshRefs.Count];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue