fix(physics): restore retail cell availability semantics

This commit is contained in:
Erik 2026-07-31 14:22:45 +02:00
parent d3c0d9ec0e
commit 7716c2ee89
14 changed files with 393 additions and 62 deletions

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Numerics;
using DatReaderWriter.Enums;
@ -74,6 +75,64 @@ public class BuildShadowCellSetTests
};
}
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(
Matrix4x4 worldTransform)
{
Matrix4x4.Invert(worldTransform, out var inv);
var portalPlane = new Plane(new Vector3(1f, 0f, 0f), -2.5f);
return new CellPhysics
{
WorldTransform = worldTransform,
InverseWorldTransform = inv,
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
CellBSP = new CellBSPTree { Root = null },
FlatContainmentBsp = new FlatCellContainmentBsp(
-1,
ImmutableArray<FlatCellBspNode>.Empty),
PortalPolygons = new Dictionary<ushort, ResolvedPolygon>
{
[10] = new ResolvedPolygon
{
Vertices =
[
new Vector3(2.5f, -2.5f, 0f),
new Vector3(2.5f, 2.5f, 0f),
new Vector3(2.5f, 2.5f, 5f),
new Vector3(2.5f, -2.5f, 5f),
],
Plane = portalPlane,
NumPoints = 4,
SidesType = CullMode.None,
},
},
Portals =
[
new PortalInfo(otherCellId: 0xFFFF, polygonId: 10, flags: 0),
],
};
}
private static void RegisterFlatTerrain(PhysicsDataCache cache)
=> cache.CellGraph.RegisterTerrain(
0xA9B40000u,
new TerrainSurface(new byte[81], new float[256]),
Vector3.Zero);
// ── Seeds ──────────────────────────────────────────────────────────
[Fact]
@ -129,6 +188,42 @@ public class BuildShadowCellSetTests
Assert.Equal(new[] { IndoorSeed }, set);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void IndoorSeed_RefloodsAfterNullRootPayloadHydrates(
bool useFlat)
{
var cache = new PhysicsDataCache
{
CollisionTraversalMode = useFlat
? CollisionTraversalMode.Flat
: CollisionTraversalMode.Graph,
};
Sphere[] sphere = One(new Vector3(2.4f, 0f, 2.5f), 0.5f);
IReadOnlyList<uint> unavailable = CellTransit.BuildShadowCellSet(
cache,
IndoorSeed,
sphere,
1,
isStatic: false);
Assert.Equal(new[] { IndoorSeed }, unavailable);
cache.RegisterCellStructForTest(
IndoorSeed,
MakeNullRootCellWithExteriorPortal(Matrix4x4.Identity));
IReadOnlyList<uint> hydrated = CellTransit.BuildShadowCellSet(
cache,
IndoorSeed,
sphere,
1,
isStatic: false);
Assert.Contains(IndoorSeed, hydrated);
Assert.Contains(hydrated, id => (id & 0xFFFFu) < 0x0100u);
}
[Fact]
public void OutdoorSeed_FloodsOverlappedLandcells_BlockCrossingMath()
{
@ -155,6 +250,7 @@ public class BuildShadowCellSetTests
// vestibule's shadow_object_list at registration via
// CLandCell::find_transit_cells → ... → check_building_transit.
var cache = new PhysicsDataCache();
RegisterFlatTerrain(cache);
cache.RegisterCellStructForTest(NeighborCell, MakeLeafCell(Matrix4x4.Identity));
var sphere = One(new Vector3(12f, 12f, 0f), 0.5f);
@ -188,6 +284,7 @@ public class BuildShadowCellSetTests
// other_portal_id = -1 (wire 0xFFFF) never admits its interior cell
// (CEnvCell::check_building_transit, 0x0052c5dc).
var cache = new PhysicsDataCache();
RegisterFlatTerrain(cache);
cache.RegisterCellStructForTest(NeighborCell, MakeLeafCell(Matrix4x4.Identity));
var sphere = One(new Vector3(12f, 12f, 0f), 0.5f);
@ -211,6 +308,63 @@ public class BuildShadowCellSetTests
Assert.DoesNotContain(NeighborCell, set);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void UnavailableOutdoorSeed_AddsOutsideButSkipsTransit_UntilTerrainHydrates(
bool useFlat)
{
var cache = new PhysicsDataCache
{
CollisionTraversalMode = useFlat
? CollisionTraversalMode.Flat
: CollisionTraversalMode.Graph,
};
cache.RegisterCellStructForTest(
NeighborCell,
MakeNullRootCell(Matrix4x4.Identity));
var sphere = One(new Vector3(12f, 12f, 0f), 0.5f);
IReadOnlyList<uint> seeded = CellTransit.BuildShadowCellSet(
cache,
0xA9B40001u,
sphere,
1,
isStatic: false);
uint landcell = seeded[0];
cache.RegisterBuildingForTest(landcell, new BuildingPhysics
{
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Portals =
[
new BldPortalInfo(NeighborCell, otherPortalId: 0, flags: 0),
],
});
IReadOnlyList<uint> unavailable = CellTransit.BuildShadowCellSet(
cache,
0xA9B40001u,
sphere,
1,
isStatic: false);
Assert.NotEmpty(unavailable);
Assert.All(unavailable, id => Assert.True((id & 0xFFFFu) < 0x0100u));
Assert.DoesNotContain(NeighborCell, unavailable);
RegisterFlatTerrain(cache);
IReadOnlyList<uint> hydrated = CellTransit.BuildShadowCellSet(
cache,
0xA9B40001u,
sphere,
1,
isStatic: false);
Assert.Contains(landcell, hydrated);
Assert.Contains(NeighborCell, hydrated);
}
// ── Exterior straddle from an indoor seed ──────────────────────────
[Fact]