fix(physics): validate retail cell containment roots

This commit is contained in:
Erik 2026-07-31 14:48:26 +02:00
parent 7716c2ee89
commit 3e0f3b6206
23 changed files with 429 additions and 197 deletions

View file

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Numerics;
using DatReaderWriter.Enums;
@ -63,6 +62,7 @@ public class BuildShadowCellSetTests
private static CellPhysics MakeLeafCell(Matrix4x4 worldTransform)
{
Matrix4x4.Invert(worldTransform, out var inv);
var root = new CellBSPNode { Type = BSPNodeType.Leaf };
return new CellPhysics
{
WorldTransform = worldTransform,
@ -70,40 +70,25 @@ public class BuildShadowCellSetTests
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
CellBSP = new CellBSPTree
{
Root = new CellBSPNode { Type = BSPNodeType.Leaf },
Root = root,
},
FlatContainmentBsp = FlatCollisionAssetBuilder.FlattenCellContainmentBsp(root),
};
}
private static CellPhysics MakeNullRootCell(Matrix4x4 worldTransform)
{
Matrix4x4.Invert(worldTransform, out var inv);
return new CellPhysics
{
WorldTransform = worldTransform,
InverseWorldTransform = inv,
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
CellBSP = new CellBSPTree { Root = null },
FlatContainmentBsp = new FlatCellContainmentBsp(
-1,
ImmutableArray<FlatCellBspNode>.Empty),
};
}
private static CellPhysics MakeNullRootCellWithExteriorPortal(
private static CellPhysics MakeValidCellWithExteriorPortal(
Matrix4x4 worldTransform)
{
Matrix4x4.Invert(worldTransform, out var inv);
var portalPlane = new Plane(new Vector3(1f, 0f, 0f), -2.5f);
var root = new CellBSPNode { Type = BSPNodeType.Leaf };
return new CellPhysics
{
WorldTransform = worldTransform,
InverseWorldTransform = inv,
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
CellBSP = new CellBSPTree { Root = null },
FlatContainmentBsp = new FlatCellContainmentBsp(
-1,
ImmutableArray<FlatCellBspNode>.Empty),
CellBSP = new CellBSPTree { Root = root },
FlatContainmentBsp = FlatCollisionAssetBuilder.FlattenCellContainmentBsp(root),
PortalPolygons = new Dictionary<ushort, ResolvedPolygon>
{
[10] = new ResolvedPolygon
@ -191,7 +176,7 @@ public class BuildShadowCellSetTests
[Theory]
[InlineData(false)]
[InlineData(true)]
public void IndoorSeed_RefloodsAfterNullRootPayloadHydrates(
public void IndoorSeed_RefloodsAfterValidPayloadHydrates(
bool useFlat)
{
var cache = new PhysicsDataCache
@ -212,7 +197,7 @@ public class BuildShadowCellSetTests
cache.RegisterCellStructForTest(
IndoorSeed,
MakeNullRootCellWithExteriorPortal(Matrix4x4.Identity));
MakeValidCellWithExteriorPortal(Matrix4x4.Identity));
IReadOnlyList<uint> hydrated = CellTransit.BuildShadowCellSet(
cache,
IndoorSeed,
@ -322,7 +307,7 @@ public class BuildShadowCellSetTests
};
cache.RegisterCellStructForTest(
NeighborCell,
MakeNullRootCell(Matrix4x4.Identity));
MakeLeafCell(Matrix4x4.Identity));
var sphere = One(new Vector3(12f, 12f, 0f), 0.5f);
IReadOnlyList<uint> seeded = CellTransit.BuildShadowCellSet(
@ -365,6 +350,65 @@ public class BuildShadowCellSetTests
Assert.Contains(NeighborCell, hydrated);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void LoadedSeed_AbsentAdjacentLandcell_SkipsStaleBuildingUntilAdjacentHydrates(
bool useFlat)
{
const uint seedCell = 0xA9B4_0031u;
const uint adjacentCell = 0xA9B3_0038u;
const uint interiorCell = 0xA9B3_0100u;
var cache = new PhysicsDataCache
{
CollisionTraversalMode = useFlat
? CollisionTraversalMode.Flat
: CollisionTraversalMode.Graph,
};
cache.CellGraph.RegisterTerrain(
0xA9B4_0000u,
new TerrainSurface(new byte[81], new float[256]),
Vector3.Zero);
cache.RegisterCellStructForTest(
interiorCell,
MakeLeafCell(Matrix4x4.Identity));
cache.RegisterBuildingForTest(adjacentCell, new BuildingPhysics
{
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Portals =
[
new BldPortalInfo(interiorCell, otherPortalId: 0, flags: 0),
],
});
Sphere[] sphere = One(new Vector3(150f, 0.2f, 0f), 0.5f);
IReadOnlyList<uint> unavailable = CellTransit.BuildShadowCellSet(
cache,
seedCell,
sphere,
1,
isStatic: false);
Assert.Contains(seedCell, unavailable);
Assert.Contains(adjacentCell, unavailable);
Assert.DoesNotContain(interiorCell, unavailable);
cache.CellGraph.RegisterTerrain(
0xA9B3_0000u,
new TerrainSurface(new byte[81], new float[256]),
new Vector3(0f, -192f, 0f));
IReadOnlyList<uint> hydrated = CellTransit.BuildShadowCellSet(
cache,
seedCell,
sphere,
1,
isStatic: false);
Assert.Contains(adjacentCell, hydrated);
Assert.Contains(interiorCell, hydrated);
}
// ── Exterior straddle from an indoor seed ──────────────────────────
[Fact]