refactor(physics): remove parsed collision graphs
Publish prepared GfxObj, Setup, CellStruct, and EnvCell collision records without retaining their parsed DAT BSP, polygon, vertex, or shape graphs. Strip temporary physics bundles at stable world commit, keep graph traversal only as an explicit test/tooling oracle, and report graph residency from actual retained fields. Validated by 115 focused collision/streaming tests, a zero-warning Release build, and 8,413 passing Release tests with five pre-existing skips. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
feb80a67d9
commit
82f8d4f82e
18 changed files with 923 additions and 104 deletions
|
|
@ -188,9 +188,9 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
DatObjectCacheHits: datObjectCacheStats.Hits,
|
||||
DatObjectCacheMisses: datObjectCacheStats.Misses,
|
||||
DatObjectCacheEvictions: datObjectCacheStats.Evictions,
|
||||
PhysicsGraphGfxObjs: _physics.GfxObjCount,
|
||||
PhysicsGraphSetups: _physics.SetupCount,
|
||||
PhysicsGraphCells: _physics.CellStructCount,
|
||||
PhysicsGraphGfxObjs: _physics.GraphGfxObjCount,
|
||||
PhysicsGraphSetups: _physics.GraphSetupCount,
|
||||
PhysicsGraphCells: _physics.GraphCellStructCount,
|
||||
PhysicsFlatGfxObjs: _physics.FlatGfxObjCount,
|
||||
PhysicsFlatSetups: _physics.FlatSetupCount,
|
||||
PhysicsFlatCells: _physics.FlatCellStructCount,
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ using DatReaderWriter.DBObjs;
|
|||
namespace AcDream.App.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the graph and prepared collision views for DAT objects discovered
|
||||
/// Publishes package-prepared collision views for DAT objects discovered
|
||||
/// through the live-object path. Streamed statics carry their prepared closure
|
||||
/// on <c>LandblockBuild</c>; live CreateObject/appearance data arrives outside
|
||||
/// that transaction and therefore uses this matching strict publisher.
|
||||
/// that transaction and therefore uses this matching strict publisher. The
|
||||
/// production cache consumes source metadata transiently and never retains its
|
||||
/// parsed collision graph.
|
||||
/// </summary>
|
||||
internal sealed class LiveCollisionAssetPublisher
|
||||
{
|
||||
|
|
|
|||
|
|
@ -43,11 +43,11 @@ internal sealed class LiveEntityCollisionBuilder
|
|||
PhysicsDataCache physicsData,
|
||||
LiveEntityDefaultPoseResolver defaultPose)
|
||||
: this(
|
||||
id => physicsData.GetGfxObj(id)?.FlatPhysicsBsp?.RootIndex >= 0,
|
||||
id => physicsData.GetFlatGfxObj(id)?.PhysicsBsp.RootIndex >= 0,
|
||||
id =>
|
||||
{
|
||||
FlatPhysicsBsp? flat =
|
||||
physicsData.GetGfxObj(id)?.FlatPhysicsBsp;
|
||||
physicsData.GetFlatGfxObj(id)?.PhysicsBsp;
|
||||
return flat is { RootIndex: >= 0 }
|
||||
? flat.Nodes[flat.RootIndex].BoundingSphere.Radius
|
||||
: null;
|
||||
|
|
|
|||
|
|
@ -268,7 +268,8 @@ internal sealed class LiveEntityMotionRuntimeController
|
|||
public (float Radius, float Height) GetSetupCylinder(
|
||||
uint serverGuid, AcDream.Core.World.WorldEntity entity)
|
||||
{
|
||||
var setup = _physicsDataCache.GetSetup(entity.SourceGfxObjOrSetupId);
|
||||
FlatSetupCollision? setup =
|
||||
_physicsDataCache.GetFlatSetup(entity.SourceGfxObjOrSetupId);
|
||||
if (setup is null)
|
||||
return (0f, 0f);
|
||||
// Live spawns bake ObjScale into MeshRefs and never populate
|
||||
|
|
|
|||
|
|
@ -634,7 +634,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
source.LandblockId,
|
||||
source.Heightmap,
|
||||
entities ?? new List<WorldEntity>(source.Entities),
|
||||
source.PhysicsDats);
|
||||
PhysicsDatBundle.Empty);
|
||||
|
||||
private static List<WorldEntity> MutableEntities(LoadedLandblock landblock) =>
|
||||
(List<WorldEntity>)landblock.Entities;
|
||||
|
|
|
|||
|
|
@ -101,9 +101,19 @@ public sealed class LandblockBuildFactory
|
|||
return build;
|
||||
}
|
||||
|
||||
AcDream.Core.World.LandblockCollisionBuild collisions =
|
||||
BuildPreparedCollisionClosure(build.Landblock);
|
||||
AcDream.Core.World.PhysicsDatBundle publicationDats =
|
||||
(build.Landblock.PhysicsDats
|
||||
?? AcDream.Core.World.PhysicsDatBundle.Empty)
|
||||
.WithoutCollisionGraphs();
|
||||
return build with
|
||||
{
|
||||
Collisions = BuildPreparedCollisionClosure(build.Landblock),
|
||||
Landblock = build.Landblock with
|
||||
{
|
||||
PhysicsDats = publicationDats,
|
||||
},
|
||||
Collisions = collisions,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -402,16 +402,19 @@ public sealed class LandblockPhysicsPublisher
|
|||
{
|
||||
long cacheStarted = Stopwatch.GetTimestamp();
|
||||
uint gfxObjectId = publication.GfxObjectIds[publication.GfxCursor];
|
||||
if (datBundle.GfxObjs.TryGetValue(gfxObjectId, out var gfx))
|
||||
if (publication.Build.Collisions?.GfxObjs.TryGetValue(
|
||||
gfxObjectId,
|
||||
out FlatGfxObjCollisionAsset? prepared) == true)
|
||||
{
|
||||
FlatGfxObjCollisionAsset? prepared = null;
|
||||
publication.Build.Collisions?.GfxObjs.TryGetValue(
|
||||
gfxObjectId,
|
||||
out prepared);
|
||||
_physicsDataCache.CacheGfxObj(
|
||||
gfxObjectId,
|
||||
gfx,
|
||||
prepared);
|
||||
_physicsDataCache.CacheGfxObj(gfxObjectId, prepared);
|
||||
}
|
||||
else if (datBundle.GfxObjs.TryGetValue(
|
||||
gfxObjectId,
|
||||
out var source))
|
||||
{
|
||||
// Graph-oracle fixture seam. Production near builds always
|
||||
// carry the strict prepared closure.
|
||||
_physicsDataCache.CacheGfxObj(gfxObjectId, source);
|
||||
}
|
||||
publication.GfxCursor++;
|
||||
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
|
||||
|
|
@ -420,13 +423,16 @@ public sealed class LandblockPhysicsPublisher
|
|||
{
|
||||
uint setupId =
|
||||
publication.SetupObjectIds[publication.SetupCursor];
|
||||
if (datBundle.Setups.TryGetValue(setupId, out var setup))
|
||||
{
|
||||
FlatSetupCollision? prepared = null;
|
||||
publication.Build.Collisions?.Setups.TryGetValue(
|
||||
if (publication.Build.Collisions?.Setups.TryGetValue(
|
||||
setupId,
|
||||
out prepared);
|
||||
_physicsDataCache.CacheSetup(setupId, setup, prepared);
|
||||
out FlatSetupCollision? prepared) == true)
|
||||
{
|
||||
_physicsDataCache.CacheSetup(setupId, prepared);
|
||||
}
|
||||
else if (datBundle.Setups.TryGetValue(setupId, out var source))
|
||||
{
|
||||
// Graph-oracle fixture seam only.
|
||||
_physicsDataCache.CacheSetup(setupId, source);
|
||||
}
|
||||
publication.SetupCursor++;
|
||||
}
|
||||
|
|
@ -505,8 +511,97 @@ public sealed class LandblockPhysicsPublisher
|
|||
LoadedLandblock landblock = publication.Build.Landblock;
|
||||
uint envCellId =
|
||||
(landblock.LandblockId & 0xFFFF0000u) | (0x0100u + offset);
|
||||
if (!datBundle.EnvCells.TryGetValue(envCellId, out var envCell)
|
||||
|| envCell.EnvironmentId == 0
|
||||
if (!datBundle.EnvCells.TryGetValue(envCellId, out var envCell))
|
||||
return;
|
||||
|
||||
if (publication.Build.Collisions is not { } collisions)
|
||||
{
|
||||
PublishGraphFixtureCell(
|
||||
publication,
|
||||
datBundle,
|
||||
envCellId,
|
||||
envCell);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!collisions.CellStructures.TryGetValue(
|
||||
envCellId,
|
||||
out FlatCellStructureCollisionAsset? preparedStructure)
|
||||
|| !collisions.EnvCells.TryGetValue(
|
||||
envCellId,
|
||||
out FlatEnvCellTopology? preparedTopology))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Quaternion rotation = envCell.Position.Orientation;
|
||||
Vector3 cellOriginWorld = envCell.Position.Origin + publication.Origin;
|
||||
Matrix4x4 physicsCellTransform =
|
||||
Matrix4x4.CreateFromQuaternion(rotation)
|
||||
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
||||
|
||||
_physicsDataCache.CacheCellStruct(
|
||||
envCellId,
|
||||
envCell,
|
||||
physicsCellTransform,
|
||||
preparedStructure,
|
||||
preparedTopology);
|
||||
|
||||
FlatPolygonTable physicsPolygons =
|
||||
preparedStructure.PhysicsBsp.PolygonTable;
|
||||
|
||||
var portalPlanes = new List<PortalPlane>();
|
||||
FlatPolygonTable portalPolygons = preparedStructure.PortalPolygons;
|
||||
for (int portalIndex = 0;
|
||||
portalIndex < preparedTopology.Portals.Length;
|
||||
portalIndex++)
|
||||
{
|
||||
FlatEnvCellPortal portal = preparedTopology.Portals[portalIndex];
|
||||
if ((uint)portal.PolygonIndex
|
||||
>= (uint)portalPolygons.Polygons.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
FlatCollisionPolygon polygon =
|
||||
portalPolygons.Polygons[portal.PolygonIndex];
|
||||
if (polygon.VertexRange.Count < 3)
|
||||
continue;
|
||||
var portalVertices = new Vector3[polygon.VertexRange.Count];
|
||||
for (int index = 0; index < portalVertices.Length; index++)
|
||||
{
|
||||
Vector3 local = portalPolygons.Vertices[
|
||||
polygon.VertexRange.Start + index];
|
||||
portalVertices[index] =
|
||||
Vector3.Transform(local, rotation) + cellOriginWorld;
|
||||
}
|
||||
|
||||
portalPlanes.Add(PortalPlane.FromVertices(
|
||||
portalVertices.AsSpan(),
|
||||
portal.OtherCellId,
|
||||
envCellId & 0xFFFFu,
|
||||
(ushort)portal.Flags));
|
||||
}
|
||||
|
||||
publication.CellSurfaces.Add(new CellSurface(
|
||||
envCellId,
|
||||
physicsPolygons,
|
||||
rotation,
|
||||
cellOriginWorld));
|
||||
publication.PortalPlanes.AddRange(portalPlanes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parsed-graph publication is retained only for unit/conformance fixtures
|
||||
/// constructed without a prepared collision closure. Production cannot
|
||||
/// reach this branch because near-build admission requires that closure.
|
||||
/// </summary>
|
||||
private void PublishGraphFixtureCell(
|
||||
LandblockPhysicsPublication publication,
|
||||
PhysicsDatBundle datBundle,
|
||||
uint envCellId,
|
||||
DatReaderWriter.DBObjs.EnvCell envCell)
|
||||
{
|
||||
if (envCell.EnvironmentId == 0
|
||||
|| !datBundle.Environments.TryGetValue(
|
||||
0x0D000000u | envCell.EnvironmentId,
|
||||
out var environment)
|
||||
|
|
@ -518,51 +613,33 @@ public sealed class LandblockPhysicsPublisher
|
|||
}
|
||||
|
||||
Quaternion rotation = envCell.Position.Orientation;
|
||||
Vector3 cellOriginWorld = envCell.Position.Origin + publication.Origin;
|
||||
Vector3 cellOriginWorld =
|
||||
envCell.Position.Origin + publication.Origin;
|
||||
Matrix4x4 physicsCellTransform =
|
||||
Matrix4x4.CreateFromQuaternion(rotation)
|
||||
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
||||
|
||||
FlatCellStructureCollisionAsset? preparedStructure = null;
|
||||
FlatEnvCellTopology? preparedTopology = null;
|
||||
publication.Build.Collisions?.CellStructures.TryGetValue(
|
||||
envCellId,
|
||||
out preparedStructure);
|
||||
publication.Build.Collisions?.EnvCells.TryGetValue(
|
||||
envCellId,
|
||||
out preparedTopology);
|
||||
_physicsDataCache.CacheCellStruct(
|
||||
envCellId,
|
||||
envCell,
|
||||
cellStruct,
|
||||
physicsCellTransform,
|
||||
preparedStructure,
|
||||
preparedTopology);
|
||||
physicsCellTransform);
|
||||
|
||||
var worldVertices = new Dictionary<ushort, Vector3>(
|
||||
cellStruct.VertexArray.Vertices.Count);
|
||||
foreach ((ushort vertexId, var vertex) in
|
||||
cellStruct.VertexArray.Vertices)
|
||||
{
|
||||
Vector3 worldPosition =
|
||||
worldVertices[vertexId] =
|
||||
Vector3.Transform(vertex.Origin, rotation)
|
||||
+ cellOriginWorld;
|
||||
worldVertices[(ushort)vertexId] = worldPosition;
|
||||
}
|
||||
|
||||
var polygonVertexIds = new List<List<short>>(
|
||||
cellStruct.PhysicsPolygons.Count);
|
||||
foreach (var polygon in cellStruct.PhysicsPolygons.Values)
|
||||
{
|
||||
var vertexIds = new List<short>(polygon.VertexIds.Count);
|
||||
foreach (short vertexId in polygon.VertexIds)
|
||||
vertexIds.Add(vertexId);
|
||||
polygonVertexIds.Add(vertexIds);
|
||||
}
|
||||
polygonVertexIds.Add([.. polygon.VertexIds]);
|
||||
|
||||
var portalPlanes = new List<PortalPlane>();
|
||||
// CellPortal.PolygonId indexes rendering Polygons, not
|
||||
// PhysicsPolygons (ACViewer EnvCell.find_transit_cells).
|
||||
foreach (var portal in envCell.CellPortals)
|
||||
{
|
||||
if (!cellStruct.Polygons.TryGetValue(
|
||||
|
|
@ -589,7 +666,7 @@ public sealed class LandblockPhysicsPublisher
|
|||
continue;
|
||||
|
||||
portalPlanes.Add(PortalPlane.FromVertices(
|
||||
portalVertices.AsSpan(),
|
||||
portalVertices,
|
||||
portal.OtherCellId,
|
||||
envCellId & 0xFFFFu,
|
||||
(ushort)portal.Flags));
|
||||
|
|
@ -689,17 +766,26 @@ public sealed class LandblockPhysicsPublisher
|
|||
LogMultipartRegistration(landblock, entity, bspShapes);
|
||||
}
|
||||
|
||||
SetupPhysics? setup =
|
||||
_physicsDataCache.GetSetup(entity.SourceGfxObjOrSetupId);
|
||||
FlatSetupCollision? setup =
|
||||
_physicsDataCache.GetFlatSetup(entity.SourceGfxObjOrSetupId);
|
||||
if (setup is null
|
||||
&& _physicsDataCache.GetSetup(
|
||||
entity.SourceGfxObjOrSetupId) is { } graphSetup)
|
||||
{
|
||||
// Graph-oracle fixture seam only. Production Setup publication is
|
||||
// already package-flat and never allocates this conversion.
|
||||
setup = FlatCollisionAssetBuilder.FlattenSetup(graphSetup);
|
||||
}
|
||||
if (setup is not null && entityBspCount == 0)
|
||||
{
|
||||
float scale = entity.Scale > 0f ? entity.Scale : 1f;
|
||||
var setupShapes = new List<ShadowShape>();
|
||||
for (int cylinderIndex = 0;
|
||||
cylinderIndex < setup.CylSpheres.Count;
|
||||
cylinderIndex < setup.Cylinders.Length;
|
||||
cylinderIndex++)
|
||||
{
|
||||
CylSphere cylinder = setup.CylSpheres[cylinderIndex];
|
||||
FlatCollisionCylinder cylinder =
|
||||
setup.Cylinders[cylinderIndex];
|
||||
float radius = cylinder.Radius * scale;
|
||||
float baseHeight = cylinder.Height > 0f
|
||||
? cylinder.Height
|
||||
|
|
@ -719,13 +805,14 @@ public sealed class LandblockPhysicsPublisher
|
|||
CylHeight: height));
|
||||
}
|
||||
|
||||
if (setup.CylSpheres.Count == 0)
|
||||
if (setup.Cylinders.Length == 0)
|
||||
{
|
||||
for (int sphereIndex = 0;
|
||||
sphereIndex < setup.Spheres.Count;
|
||||
sphereIndex < setup.Spheres.Length;
|
||||
sphereIndex++)
|
||||
{
|
||||
Sphere sphere = setup.Spheres[sphereIndex];
|
||||
FlatCollisionSphere sphere =
|
||||
setup.Spheres[sphereIndex];
|
||||
if (sphere.Radius <= 0f)
|
||||
continue;
|
||||
|
||||
|
|
@ -746,8 +833,8 @@ public sealed class LandblockPhysicsPublisher
|
|||
}
|
||||
}
|
||||
|
||||
if (setup.CylSpheres.Count == 0
|
||||
&& setup.Spheres.Count == 0
|
||||
if (setup.Cylinders.Length == 0
|
||||
&& setup.Spheres.Length == 0
|
||||
&& setup.Radius > 0f)
|
||||
{
|
||||
float radius = setup.Radius * scale;
|
||||
|
|
|
|||
|
|
@ -73,6 +73,53 @@ public sealed class CellSurface
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Construct directly from the immutable production polygon table. This
|
||||
/// preserves the same polygon/fan order as the dictionary fixture path
|
||||
/// without reconstructing a temporary vertex dictionary and nested ID
|
||||
/// lists during publication.
|
||||
/// </summary>
|
||||
public CellSurface(
|
||||
uint cellId,
|
||||
FlatPolygonTable polygons,
|
||||
Quaternion rotation,
|
||||
Vector3 translation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(polygons);
|
||||
CellId = cellId;
|
||||
int triangleCount = 0;
|
||||
for (int i = 0; i < polygons.Polygons.Length; i++)
|
||||
triangleCount += Math.Max(0, polygons.Polygons[i].VertexRange.Count - 2);
|
||||
_triangles = new List<(Vector3, Vector3, Vector3)>(triangleCount);
|
||||
|
||||
for (int polygonIndex = 0;
|
||||
polygonIndex < polygons.Polygons.Length;
|
||||
polygonIndex++)
|
||||
{
|
||||
FlatIndexRange range =
|
||||
polygons.Polygons[polygonIndex].VertexRange;
|
||||
if (range.Count < 3)
|
||||
continue;
|
||||
|
||||
Vector3 first =
|
||||
Vector3.Transform(polygons.Vertices[range.Start], rotation)
|
||||
+ translation;
|
||||
Vector3 previous =
|
||||
Vector3.Transform(polygons.Vertices[range.Start + 1], rotation)
|
||||
+ translation;
|
||||
for (int vertexOffset = 2;
|
||||
vertexOffset < range.Count;
|
||||
vertexOffset++)
|
||||
{
|
||||
Vector3 current = Vector3.Transform(
|
||||
polygons.Vertices[range.Start + vertexOffset],
|
||||
rotation) + translation;
|
||||
_triangles.Add((first, previous, current));
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Project (worldX, worldY) onto this cell's floor polygons and
|
||||
/// return the Z. Returns null if outside all floor polygons.
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ internal static class CollisionTraversal
|
|||
flatShadow.BeginGraphPass();
|
||||
try
|
||||
{
|
||||
graphRefereeResult = gfxObject.BSP.Root is not null;
|
||||
graphRefereeResult = gfxObject.BSP?.Root is not null;
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
|
|
@ -268,7 +268,7 @@ internal static class CollisionTraversal
|
|||
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
if (shadow is null || !shadow.TrySample(out long sample))
|
||||
return gfxObject.BSP.Root is not null;
|
||||
return gfxObject.BSP?.Root is not null;
|
||||
|
||||
bool flatResult = false;
|
||||
Exception? flatFault = null;
|
||||
|
|
@ -287,7 +287,7 @@ internal static class CollisionTraversal
|
|||
shadow.EndFlatPass();
|
||||
}
|
||||
|
||||
bool graphResult = gfxObject.BSP.Root is not null;
|
||||
bool graphResult = gfxObject.BSP?.Root is not null;
|
||||
if (flatFault is null)
|
||||
{
|
||||
shadow.RecordBoolean(
|
||||
|
|
@ -887,7 +887,7 @@ internal static class CollisionTraversal
|
|||
try
|
||||
{
|
||||
graphRefereeState = BSPQuery.FindCollisions(
|
||||
gfxObject.BSP.Root,
|
||||
gfxObject.BSP!.Root!,
|
||||
gfxObject.Resolved,
|
||||
graphTransition!,
|
||||
localSphereCenter,
|
||||
|
|
@ -951,7 +951,7 @@ internal static class CollisionTraversal
|
|||
if (shadow is null || !shadow.TrySample(out long sample))
|
||||
{
|
||||
return BSPQuery.FindCollisions(
|
||||
gfxObject.BSP.Root,
|
||||
gfxObject.BSP!.Root!,
|
||||
gfxObject.Resolved,
|
||||
transition,
|
||||
localSphereCenter,
|
||||
|
|
@ -999,7 +999,7 @@ internal static class CollisionTraversal
|
|||
}
|
||||
|
||||
TransitionState graphState = BSPQuery.FindCollisions(
|
||||
gfxObject.BSP.Root,
|
||||
gfxObject.BSP!.Root!,
|
||||
gfxObject.Resolved,
|
||||
transition,
|
||||
localSphereCenter,
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@ namespace AcDream.Core.Physics;
|
|||
/// Thread-safe cache of physics-relevant data extracted from GfxObj and Setup
|
||||
/// dat objects during streaming and live-object materialization. Prepared
|
||||
/// payloads are built/read off the hot path; one update-thread publisher
|
||||
/// installs prepared production records and, during the I6 acceptance window,
|
||||
/// their graph referee. ConcurrentDictionary keeps diagnostics and tooling
|
||||
/// reads safe without a global lock.
|
||||
/// installs prepared production records without retaining the parsed DAT
|
||||
/// collision graph. The public constructor remains the graph-oracle seam for
|
||||
/// conformance fixtures and bake/equivalence tooling. ConcurrentDictionary
|
||||
/// keeps diagnostics and tooling reads safe without a global lock.
|
||||
/// </summary>
|
||||
public sealed class PhysicsDataCache
|
||||
{
|
||||
|
|
@ -41,7 +42,8 @@ public sealed class PhysicsDataCache
|
|||
private PhysicsDataCache(bool requirePreparedCollision)
|
||||
{
|
||||
_requirePreparedCollision = requirePreparedCollision;
|
||||
if (PhysicsDiagnostics.CollisionShadowSampleEvery > 0)
|
||||
if (!requirePreparedCollision
|
||||
&& PhysicsDiagnostics.CollisionShadowSampleEvery > 0)
|
||||
{
|
||||
CollisionShadow = new CollisionShadowVerifier(
|
||||
PhysicsDiagnostics.CollisionShadowSampleEvery,
|
||||
|
|
@ -52,7 +54,7 @@ public sealed class PhysicsDataCache
|
|||
/// <summary>
|
||||
/// Creates the gameplay cache with Slice I6's prepared flat collision
|
||||
/// representation authoritative. The public constructor deliberately
|
||||
/// remains the graph-oracle fixture seam until the I6 referee is removed.
|
||||
/// remains the graph-oracle fixture seam for conformance and bake tooling.
|
||||
/// </summary>
|
||||
public static PhysicsDataCache CreateProduction()
|
||||
{
|
||||
|
|
@ -69,9 +71,8 @@ public sealed class PhysicsDataCache
|
|||
CollisionShadow?.Stats ?? default;
|
||||
|
||||
/// <summary>
|
||||
/// Slice I graph/flat differential selector. Production is flat-authoritative
|
||||
/// from I6 onward. Graph remains an explicit fixture/oracle mode until the
|
||||
/// referee is removed after acceptance.
|
||||
/// Slice I graph/flat differential selector. Production is permanently
|
||||
/// flat-authoritative. Graph remains an explicit fixture/oracle mode.
|
||||
/// </summary>
|
||||
internal CollisionTraversalMode CollisionTraversalMode { get; set; } =
|
||||
CollisionTraversalMode.Graph;
|
||||
|
|
@ -106,6 +107,15 @@ public sealed class PhysicsDataCache
|
|||
GfxObj gfxObj,
|
||||
FlatGfxObjCollisionAsset? prepared = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gfxObj);
|
||||
|
||||
if (_requirePreparedCollision
|
||||
&& prepared is null
|
||||
&& _flatGfxObj.TryGetValue(gfxObjId, out var retainedPrepared))
|
||||
{
|
||||
prepared = retainedPrepared;
|
||||
}
|
||||
|
||||
if (_requirePreparedCollision &&
|
||||
gfxObj.Flags.HasFlag(GfxObjFlags.HasPhysics) &&
|
||||
gfxObj.PhysicsBSP?.Root is not null &&
|
||||
|
|
@ -115,6 +125,13 @@ public sealed class PhysicsDataCache
|
|||
throw MissingPreparedCollision("GfxObj", gfxObjId);
|
||||
}
|
||||
|
||||
if (_requirePreparedCollision)
|
||||
{
|
||||
if (prepared is not null)
|
||||
CachePreparedGfxObj(gfxObjId, prepared);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prepared is not null)
|
||||
_flatGfxObj.TryAdd(gfxObjId, prepared);
|
||||
|
||||
|
|
@ -169,6 +186,53 @@ public sealed class PhysicsDataCache
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes one package-prepared GfxObj without retaining its parsed DAT
|
||||
/// BSP, polygon dictionary, or vertex array.
|
||||
/// </summary>
|
||||
public void CacheGfxObj(
|
||||
uint gfxObjId,
|
||||
FlatGfxObjCollisionAsset prepared)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(prepared);
|
||||
CachePreparedGfxObj(gfxObjId, prepared);
|
||||
}
|
||||
|
||||
private void CachePreparedGfxObj(
|
||||
uint gfxObjId,
|
||||
FlatGfxObjCollisionAsset prepared)
|
||||
{
|
||||
_flatGfxObj.TryAdd(gfxObjId, prepared);
|
||||
if (prepared.VisualBounds is { } bounds)
|
||||
{
|
||||
_visualBounds.TryAdd(gfxObjId, new GfxObjVisualBounds
|
||||
{
|
||||
Min = bounds.Min,
|
||||
Max = bounds.Max,
|
||||
Center = bounds.Center,
|
||||
Radius = bounds.Radius,
|
||||
HalfExtents = bounds.HalfExtents,
|
||||
});
|
||||
}
|
||||
|
||||
if (prepared.PhysicsBsp.RootIndex < 0)
|
||||
return;
|
||||
|
||||
FlatCollisionSphere root =
|
||||
prepared.PhysicsBsp.Nodes[prepared.PhysicsBsp.RootIndex]
|
||||
.BoundingSphere;
|
||||
_gfxObj.TryAdd(gfxObjId, new GfxObjPhysics
|
||||
{
|
||||
SourceId = gfxObjId,
|
||||
BoundingSphere = new Sphere
|
||||
{
|
||||
Origin = root.Origin,
|
||||
Radius = root.Radius,
|
||||
},
|
||||
FlatPhysicsBsp = prepared.PhysicsBsp,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the cached visual AABB for a GfxObj, or null if not cached.
|
||||
/// </summary>
|
||||
|
|
@ -230,11 +294,29 @@ public sealed class PhysicsDataCache
|
|||
Setup setup,
|
||||
FlatSetupCollision? prepared = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(setup);
|
||||
|
||||
if (_requirePreparedCollision
|
||||
&& prepared is null
|
||||
&& _flatSetup.TryGetValue(setupId, out var retainedPrepared))
|
||||
{
|
||||
prepared = retainedPrepared;
|
||||
}
|
||||
|
||||
if (_requirePreparedCollision &&
|
||||
prepared is null &&
|
||||
!_flatSetup.ContainsKey(setupId))
|
||||
throw MissingPreparedCollision("Setup", setupId);
|
||||
|
||||
if (_requirePreparedCollision)
|
||||
{
|
||||
CachePreparedSetup(
|
||||
setupId,
|
||||
prepared
|
||||
?? throw MissingPreparedCollision("Setup", setupId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (prepared is not null)
|
||||
_flatSetup.TryAdd(setupId, prepared);
|
||||
|
||||
|
|
@ -257,6 +339,30 @@ public sealed class PhysicsDataCache
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes one package-prepared Setup without retaining parsed DBObj
|
||||
/// cylinder/sphere lists.
|
||||
/// </summary>
|
||||
public void CacheSetup(uint setupId, FlatSetupCollision prepared)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(prepared);
|
||||
CachePreparedSetup(setupId, prepared);
|
||||
}
|
||||
|
||||
private void CachePreparedSetup(uint setupId, FlatSetupCollision prepared)
|
||||
{
|
||||
_flatSetup.TryAdd(setupId, prepared);
|
||||
_setup.TryAdd(setupId, new SetupPhysics
|
||||
{
|
||||
SourceId = setupId,
|
||||
Height = prepared.Height,
|
||||
Radius = prepared.Radius,
|
||||
StepUpHeight = prepared.StepUpHeight,
|
||||
StepDownHeight = prepared.StepDownHeight,
|
||||
FlatCollision = prepared,
|
||||
});
|
||||
}
|
||||
|
||||
/// <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
|
||||
|
|
@ -270,6 +376,17 @@ public sealed class PhysicsDataCache
|
|||
FlatCellStructureCollisionAsset? preparedStructure = null,
|
||||
FlatEnvCellTopology? preparedTopology = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(envCell);
|
||||
ArgumentNullException.ThrowIfNull(cellStruct);
|
||||
|
||||
if (_requirePreparedCollision)
|
||||
{
|
||||
if (preparedStructure is null)
|
||||
_flatCellStruct.TryGetValue(envCellId, out preparedStructure);
|
||||
if (preparedTopology is null)
|
||||
_flatEnvCell.TryGetValue(envCellId, out preparedTopology);
|
||||
}
|
||||
|
||||
if (_requirePreparedCollision &&
|
||||
preparedStructure is null &&
|
||||
!_flatCellStruct.ContainsKey(envCellId))
|
||||
|
|
@ -284,6 +401,19 @@ public sealed class PhysicsDataCache
|
|||
if (preparedTopology is not null)
|
||||
_flatEnvCell.TryAdd(envCellId, preparedTopology);
|
||||
|
||||
if (_requirePreparedCollision)
|
||||
{
|
||||
CachePreparedCellStruct(
|
||||
envCellId,
|
||||
envCell,
|
||||
worldTransform,
|
||||
preparedStructure
|
||||
?? throw MissingPreparedCollision("CellStruct", envCellId),
|
||||
preparedTopology
|
||||
?? throw MissingPreparedCollision("EnvCell topology", envCellId));
|
||||
return;
|
||||
}
|
||||
|
||||
// UCG Stage 1: register in the unified graph for ALL cells — before the
|
||||
// idempotency + null-BSP guards below, so BSP-less cells are still included.
|
||||
if (!CellGraph.Contains(envCellId))
|
||||
|
|
@ -450,6 +580,83 @@ public sealed class PhysicsDataCache
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes one package-prepared EnvCell without retaining its parsed DAT
|
||||
/// CellStruct, BSP trees, polygon dictionaries, or vertex arrays.
|
||||
/// </summary>
|
||||
public void CacheCellStruct(
|
||||
uint envCellId,
|
||||
DatReaderWriter.DBObjs.EnvCell envCell,
|
||||
Matrix4x4 worldTransform,
|
||||
FlatCellStructureCollisionAsset preparedStructure,
|
||||
FlatEnvCellTopology preparedTopology)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(envCell);
|
||||
ArgumentNullException.ThrowIfNull(preparedStructure);
|
||||
ArgumentNullException.ThrowIfNull(preparedTopology);
|
||||
CachePreparedCellStruct(
|
||||
envCellId,
|
||||
envCell,
|
||||
worldTransform,
|
||||
preparedStructure,
|
||||
preparedTopology);
|
||||
}
|
||||
|
||||
private void CachePreparedCellStruct(
|
||||
uint envCellId,
|
||||
DatReaderWriter.DBObjs.EnvCell envCell,
|
||||
Matrix4x4 worldTransform,
|
||||
FlatCellStructureCollisionAsset preparedStructure,
|
||||
FlatEnvCellTopology preparedTopology)
|
||||
{
|
||||
_flatCellStruct.TryAdd(envCellId, preparedStructure);
|
||||
_flatEnvCell.TryAdd(envCellId, preparedTopology);
|
||||
|
||||
if (!CellGraph.Contains(envCellId))
|
||||
{
|
||||
CellGraph.Add(UcgEnvCell.FromPrepared(
|
||||
envCellId,
|
||||
worldTransform,
|
||||
preparedStructure,
|
||||
preparedTopology));
|
||||
}
|
||||
|
||||
// Preserve CacheCellStruct's existing distinction: BSP-less cells
|
||||
// participate in the cell graph but do not masquerade as hydrated
|
||||
// collision cells.
|
||||
if (preparedStructure.PhysicsBsp.RootIndex < 0)
|
||||
return;
|
||||
if (_cellStruct.ContainsKey(envCellId))
|
||||
return;
|
||||
|
||||
Matrix4x4.Invert(worldTransform, out Matrix4x4 inverseTransform);
|
||||
var portals = new List<PortalInfo>(preparedTopology.Portals.Length);
|
||||
for (int i = 0; i < preparedTopology.Portals.Length; i++)
|
||||
{
|
||||
FlatEnvCellPortal portal = preparedTopology.Portals[i];
|
||||
portals.Add(new PortalInfo(
|
||||
portal.OtherCellId,
|
||||
portal.PolygonId,
|
||||
portal.Flags));
|
||||
}
|
||||
|
||||
_cellStruct.TryAdd(envCellId, new CellPhysics
|
||||
{
|
||||
SourceId = envCellId,
|
||||
WorldTransform = worldTransform,
|
||||
InverseWorldTransform = inverseTransform,
|
||||
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
||||
FlatPhysicsBsp = preparedStructure.PhysicsBsp,
|
||||
FlatContainmentBsp = preparedStructure.ContainmentBsp,
|
||||
FlatPortalPolygons = preparedStructure.PortalPolygons,
|
||||
FlatTopology = preparedTopology,
|
||||
Portals = portals,
|
||||
VisibleCellIds = new HashSet<uint>(
|
||||
preparedTopology.VisibleCellIds),
|
||||
SeenOutside = preparedTopology.SeenOutside,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pre-resolve all physics polygons: lookup vertex positions from VertexArray
|
||||
/// and compute the face plane. Matches ACE's Polygon constructor which calls
|
||||
|
|
@ -533,6 +740,54 @@ public sealed class PhysicsDataCache
|
|||
public int FlatSetupCount => _flatSetup.Count;
|
||||
public int FlatCellStructCount => _flatCellStruct.Count;
|
||||
public int FlatEnvCellCount => _flatEnvCell.Count;
|
||||
public int GraphGfxObjCount => CountRetainedGfxObjGraphs();
|
||||
public int GraphSetupCount => CountRetainedSetupGraphs();
|
||||
public int GraphCellStructCount => CountRetainedCellGraphs();
|
||||
|
||||
private int CountRetainedGfxObjGraphs()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (GfxObjPhysics value in _gfxObj.Values)
|
||||
{
|
||||
if (value.BSP is not null
|
||||
|| value.PhysicsPolygons is not null
|
||||
|| value.Vertices is not null
|
||||
|| value.Resolved.Count != 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private int CountRetainedSetupGraphs()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (SetupPhysics value in _setup.Values)
|
||||
{
|
||||
if (value.CylSpheres.Count != 0 || value.Spheres.Count != 0)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private int CountRetainedCellGraphs()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (CellPhysics value in _cellStruct.Values)
|
||||
{
|
||||
if (value.BSP is not null
|
||||
|| value.CellBSP is not null
|
||||
|| value.PhysicsPolygons is not null
|
||||
|| value.Vertices is not null
|
||||
|| value.Resolved.Count != 0
|
||||
|| value.PortalPolygons is not null)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indoor walking Phase 1 (2026-05-19). Snapshot of currently-cached
|
||||
|
|
@ -682,16 +937,16 @@ public sealed class ResolvedPolygon
|
|||
public sealed class GfxObjPhysics
|
||||
{
|
||||
public uint SourceId { get; init; }
|
||||
public required PhysicsBSPTree BSP { get; init; }
|
||||
public required Dictionary<ushort, Polygon> PhysicsPolygons { get; init; }
|
||||
public PhysicsBSPTree? BSP { get; init; }
|
||||
public Dictionary<ushort, Polygon>? PhysicsPolygons { get; init; }
|
||||
public Sphere? BoundingSphere { get; init; }
|
||||
public required VertexArray Vertices { get; init; }
|
||||
public 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; }
|
||||
public Dictionary<ushort, ResolvedPolygon> Resolved { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Prepared integer-indexed representation. Slice I5 guarantees this for
|
||||
|
|
|
|||
|
|
@ -2225,7 +2225,9 @@ public sealed class Transition
|
|||
worldVertices = System.Array.Empty<Vector3>();
|
||||
hitPolyId = 0;
|
||||
|
||||
if (cellPhysics.BSP?.Root is null) return false;
|
||||
bool useFlat = cellPhysics.FlatPhysicsBsp is { RootIndex: >= 0 };
|
||||
if (!useFlat && cellPhysics.BSP?.Root is null)
|
||||
return false;
|
||||
|
||||
// Save/restore WalkableAllowance: CPolygon::walkable_hits_sphere reads
|
||||
// path.WalkableAllowance (acclient_2013_pseudo_c.txt:323010). For
|
||||
|
|
@ -2238,23 +2240,40 @@ public sealed class Transition
|
|||
this.SpherePath.WalkableAllowance = PhysicsGlobals.FloorZ;
|
||||
|
||||
ResolvedPolygon? hitPoly = null;
|
||||
int flatHitIndex = -1;
|
||||
ushort hitId = 0;
|
||||
Vector3 adjustedCenter;
|
||||
bool found;
|
||||
|
||||
try
|
||||
{
|
||||
found = BSPQuery.FindWalkableSphere(
|
||||
cellPhysics.BSP.Root,
|
||||
cellPhysics.Resolved,
|
||||
this,
|
||||
localFootCenter,
|
||||
sphereRadius,
|
||||
INDOOR_WALKABLE_PROBE_DISTANCE,
|
||||
Vector3.UnitZ, // local Z is up for indoor cells (identity transform)
|
||||
out hitPoly,
|
||||
out hitId,
|
||||
out adjustedCenter);
|
||||
if (useFlat)
|
||||
{
|
||||
found = FlatBspQuery.FindWalkableSphere(
|
||||
cellPhysics.FlatPhysicsBsp!,
|
||||
this,
|
||||
localFootCenter,
|
||||
sphereRadius,
|
||||
INDOOR_WALKABLE_PROBE_DISTANCE,
|
||||
Vector3.UnitZ,
|
||||
out flatHitIndex,
|
||||
out hitId,
|
||||
out adjustedCenter);
|
||||
}
|
||||
else
|
||||
{
|
||||
found = BSPQuery.FindWalkableSphere(
|
||||
cellPhysics.BSP!.Root!,
|
||||
cellPhysics.Resolved,
|
||||
this,
|
||||
localFootCenter,
|
||||
sphereRadius,
|
||||
INDOOR_WALKABLE_PROBE_DISTANCE,
|
||||
Vector3.UnitZ,
|
||||
out hitPoly,
|
||||
out hitId,
|
||||
out adjustedCenter);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -2267,19 +2286,64 @@ public sealed class Transition
|
|||
// path (SampleTerrainWalkable returns only plane + vertices, no adjusted
|
||||
// sphere). The local is held only to satisfy the out param.
|
||||
|
||||
if (!found || hitPoly is null) return false;
|
||||
if (!found)
|
||||
return false;
|
||||
|
||||
// Transform hit polygon's plane + vertices to world space. Math is
|
||||
// unchanged from the previous TryFindIndoorWalkablePlane implementation.
|
||||
var worldNormal = Vector3.TransformNormal(hitPoly.Plane.Normal, cellPhysics.WorldTransform);
|
||||
worldNormal = Vector3.Normalize(worldNormal);
|
||||
var worldV0 = Vector3.Transform(hitPoly.Vertices[0], cellPhysics.WorldTransform);
|
||||
float worldD = -Vector3.Dot(worldNormal, worldV0);
|
||||
worldPlane = new System.Numerics.Plane(worldNormal, worldD);
|
||||
Plane localPlane;
|
||||
if (useFlat)
|
||||
{
|
||||
FlatPolygonTable table =
|
||||
cellPhysics.FlatPhysicsBsp!.PolygonTable;
|
||||
if ((uint)flatHitIndex >= (uint)table.Polygons.Length)
|
||||
return false;
|
||||
FlatCollisionPolygon polygon = table.Polygons[flatHitIndex];
|
||||
localPlane = polygon.Plane;
|
||||
|
||||
worldVertices = new Vector3[hitPoly.Vertices.Length];
|
||||
for (int i = 0; i < hitPoly.Vertices.Length; i++)
|
||||
worldVertices[i] = Vector3.Transform(hitPoly.Vertices[i], cellPhysics.WorldTransform);
|
||||
var worldNormal = Vector3.TransformNormal(
|
||||
localPlane.Normal,
|
||||
cellPhysics.WorldTransform);
|
||||
worldNormal = Vector3.Normalize(worldNormal);
|
||||
var worldV0 = Vector3.Transform(
|
||||
table.Vertices[polygon.VertexRange.Start],
|
||||
cellPhysics.WorldTransform);
|
||||
float worldD = -Vector3.Dot(worldNormal, worldV0);
|
||||
worldPlane = new System.Numerics.Plane(worldNormal, worldD);
|
||||
|
||||
worldVertices = new Vector3[polygon.VertexRange.Count];
|
||||
for (int i = 0; i < worldVertices.Length; i++)
|
||||
{
|
||||
worldVertices[i] = Vector3.Transform(
|
||||
table.Vertices[polygon.VertexRange.Start + i],
|
||||
cellPhysics.WorldTransform);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hitPoly is null)
|
||||
return false;
|
||||
localPlane = hitPoly.Plane;
|
||||
Vector3[] localVertices = hitPoly.Vertices;
|
||||
|
||||
// Transform hit polygon's plane + vertices to world space. Math is
|
||||
// unchanged from the previous graph implementation.
|
||||
var worldNormal = Vector3.TransformNormal(
|
||||
localPlane.Normal,
|
||||
cellPhysics.WorldTransform);
|
||||
worldNormal = Vector3.Normalize(worldNormal);
|
||||
var worldV0 = Vector3.Transform(
|
||||
localVertices[0],
|
||||
cellPhysics.WorldTransform);
|
||||
float worldD = -Vector3.Dot(worldNormal, worldV0);
|
||||
worldPlane = new System.Numerics.Plane(worldNormal, worldD);
|
||||
|
||||
worldVertices = new Vector3[localVertices.Length];
|
||||
for (int i = 0; i < localVertices.Length; i++)
|
||||
{
|
||||
worldVertices[i] = Vector3.Transform(
|
||||
localVertices[i],
|
||||
cellPhysics.WorldTransform);
|
||||
}
|
||||
}
|
||||
|
||||
hitPolyId = hitId;
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -87,6 +87,82 @@ public sealed class EnvCell : ObjCell
|
|||
seenOutside, cellStruct.CellBSP, flatContainmentBsp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build the production cell-graph record from package-prepared collision
|
||||
/// arrays. No parsed DAT CellStruct/BSP/polygon/vertex graph is retained.
|
||||
/// </summary>
|
||||
public static EnvCell FromPrepared(
|
||||
uint id,
|
||||
Matrix4x4 worldTransform,
|
||||
FlatCellStructureCollisionAsset structure,
|
||||
FlatEnvCellTopology topology)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(structure);
|
||||
ArgumentNullException.ThrowIfNull(topology);
|
||||
Matrix4x4.Invert(worldTransform, out Matrix4x4 inverse);
|
||||
|
||||
Vector3 min = new(float.MaxValue);
|
||||
Vector3 max = new(float.MinValue);
|
||||
IncludeVertices(
|
||||
structure.PhysicsBsp.PolygonTable.Vertices,
|
||||
ref min,
|
||||
ref max);
|
||||
IncludeVertices(
|
||||
structure.PortalPolygons.Vertices,
|
||||
ref min,
|
||||
ref max);
|
||||
if (min.X == float.MaxValue)
|
||||
{
|
||||
min = Vector3.Zero;
|
||||
max = Vector3.Zero;
|
||||
}
|
||||
|
||||
var portals = new List<CellPortal>(topology.Portals.Length);
|
||||
for (int i = 0; i < topology.Portals.Length; i++)
|
||||
{
|
||||
FlatEnvCellPortal portal = topology.Portals[i];
|
||||
FlatCollisionPolygon polygon =
|
||||
structure.PortalPolygons.Polygons[portal.PolygonIndex];
|
||||
var vertices = new Vector3[polygon.VertexRange.Count];
|
||||
structure.PortalPolygons.Vertices.AsSpan(
|
||||
polygon.VertexRange.Start,
|
||||
polygon.VertexRange.Count).CopyTo(vertices);
|
||||
portals.Add(new CellPortal(
|
||||
// Preserve FromDat's existing representation exactly. CellGraph
|
||||
// portal-ID normalization is a separate behavioral concern and
|
||||
// must not be changed by this graph-residency cleanup.
|
||||
portal.OtherCellId,
|
||||
otherPortalId: 0,
|
||||
portal.PolygonId,
|
||||
portal.Flags,
|
||||
vertices));
|
||||
}
|
||||
|
||||
return new EnvCell(
|
||||
id,
|
||||
worldTransform,
|
||||
inverse,
|
||||
min,
|
||||
max,
|
||||
portals,
|
||||
topology.VisibleCellIds,
|
||||
topology.SeenOutside,
|
||||
containmentBsp: null,
|
||||
structure.ContainmentBsp);
|
||||
}
|
||||
|
||||
private static void IncludeVertices(
|
||||
System.Collections.Immutable.ImmutableArray<Vector3> vertices,
|
||||
ref Vector3 min,
|
||||
ref Vector3 max)
|
||||
{
|
||||
for (int i = 0; i < vertices.Length; i++)
|
||||
{
|
||||
min = Vector3.Min(min, vertices[i]);
|
||||
max = Vector3.Max(max, vertices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Vector3> ResolvePortalPolygon(CellStruct cellStruct, ushort polygonId)
|
||||
{
|
||||
if (!cellStruct.Polygons.TryGetValue(polygonId, out var poly) || poly.VertexIds.Count < 3)
|
||||
|
|
|
|||
|
|
@ -26,4 +26,17 @@ public sealed record PhysicsDatBundle(
|
|||
new Dictionary<uint, DatEnvironment>(),
|
||||
new Dictionary<uint, Setup>(),
|
||||
new Dictionary<uint, GfxObj>());
|
||||
|
||||
/// <summary>
|
||||
/// Drops parsed collision-bearing GfxObj and Environment/CellStruct graphs
|
||||
/// after their immutable package assets have been resolved. The remaining
|
||||
/// records are the small publication facts still consumed by building,
|
||||
/// placement, and static-light owners.
|
||||
/// </summary>
|
||||
public PhysicsDatBundle WithoutCollisionGraphs() => new(
|
||||
Info,
|
||||
EnvCells,
|
||||
Empty.Environments,
|
||||
Setups,
|
||||
Empty.GfxObjs);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue