fix(physics): S1B — indoor cell membership admits on the part BOX, as retail does (#335, AP-159 narrowed)
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
CellTransit.FindTransitCellsBox ports CEnvCell::find_transit_cells'
part-array overload @0x0052cae0 line-for-line: per-portal x per-part
order, the sphere cheap-reject at F_EPSILON+radius, the box admit whose
"Straddle or crossing-side" rule is exactly retail's `eax != side` under
the PDB Sidedness enum, leads-outside placed AFTER the admit, the
unconditional unloaded-neighbour hint without the sphere overload's
re-test, the destination box_intersects_cell gate with its deliberate
no-break, and add_all_outside_cells after the loop. The box-vs-cell BSP
traversal lands in BOTH representations behind the flat-authoritative
dispatcher with a graph referee whose 20,000 installed comparisons are
pinned by assertion (review F5), zero mismatch.
Dual Opus review: PASS on both lenses. The mandatory D0 pseudocode pass
caught that the contract's own supplementary note misattributed the box
block to the sphere overload — it belongs to a SECOND
check_building_transit overload @0x0052c680, whose portal-side
convention is INVERTED and whose admit differs; the pseudocode doc now
records that trap plus two byte confirmations made at review:
which_side @0x00444720 is strictly > eps for POSITIVE, and
intersect_box's in-plane early exit returns CROSSING(3)
(jp @0x005aa1bc -> mov eax,3), settling review items b1/b2 for the
future bridge porter. The bridge itself stays unported as AP-159's
explicit remainder.
The review also retired #335's severity premise honestly: "over-
inclusive only, never a missed one" is wrong at production shape ratios,
where the box (whole-vertex AABB) legitimately exceeds the sphere
(physics-polygon root sphere). Measured, both populations: rigged
(box << sphere) — 1,520 placements, 978 cells removed, 0 added;
production-ratio (box >= sphere) — 950 placements, 20 removed, 1 ADDED
through the loaded-neighbour gate, which is retail's direction, not a
defect. The no-op guard (review F4) asserts removal is nonzero so an
unwired admit cannot pass silently.
Process note: the implementer authored against this session's worktree
at bec5c69d, 25 commits stale — the recorded worktree-base class. All
six files were byte-identical between bases, the diff transplanted
losslessly, and every verdict-bearing run (referee, direction sweeps,
this clean-room) was re-executed on current main. S2's uncommitted
phase-1 edits were stashed for this landing so the suite verdicts
exactly one changeset.
Also untracks 341-slope-capture.jsonl (an accidental add) and
gitignores it.
Clean-room suite: 11,248 passed / 6 skipped / 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
1b2580be4c
commit
b3e43d22c9
14 changed files with 1600 additions and 4914 deletions
|
|
@ -0,0 +1,511 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Tests.Conformance;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Options;
|
||||
using DatReaderWriter.Types;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// AP-159 / #335 (2026-08-07), Campaign S slice S1B, D2/D3. Conformance and
|
||||
/// direction tests for <see cref="CellTransit.FindTransitCellsBox"/> — the
|
||||
/// box-admitting INDOOR arm the D2 rewire installs in place of
|
||||
/// <see cref="CellTransit.FindTransitCellsSphere"/> for
|
||||
/// <see cref="CellTransit.BuildShadowCellSetFromParts"/>.
|
||||
/// </summary>
|
||||
public sealed class CellTransitFindTransitCellsBoxTests
|
||||
{
|
||||
private static CellPhysics MakeCellWithPortalAtRightWall(
|
||||
Matrix4x4 worldTransform, uint otherCellId, ushort flags)
|
||||
{
|
||||
// Portal poly at local x=2.5 (right wall), normal +X. Same shape as
|
||||
// CellTransitFindTransitCellsSphereTests' fixture, so the sphere-only
|
||||
// "pre-fix" comparison below is directly reading the same geometry
|
||||
// the existing FindTransitCellsSphere conformance suite already
|
||||
// trusts.
|
||||
var portalPolyA = new ResolvedPolygon
|
||||
{
|
||||
Id = 10,
|
||||
Vertices = new[]
|
||||
{
|
||||
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 = new Plane(new Vector3(1, 0, 0), -2.5f), // x = 2.5
|
||||
NumPoints = 4,
|
||||
SidesType = CullMode.None,
|
||||
};
|
||||
|
||||
Matrix4x4.Invert(worldTransform, out var inv);
|
||||
return new CellPhysics
|
||||
{
|
||||
WorldTransform = worldTransform,
|
||||
InverseWorldTransform = inv,
|
||||
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
||||
PortalPolygons = new Dictionary<ushort, ResolvedPolygon> { [10] = portalPolyA },
|
||||
Portals = new[]
|
||||
{
|
||||
new PortalInfo(otherCellId: (ushort)otherCellId, polygonId: 10, flags: flags),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static CellBSPTree SinglePlaneCellBsp()
|
||||
{
|
||||
var leaf = new CellBSPNode { Type = BSPNodeType.Leaf };
|
||||
return new CellBSPTree
|
||||
{
|
||||
Root = new CellBSPNode
|
||||
{
|
||||
// Local x >= 0 is inside this synthetic cell.
|
||||
Type = BSPNodeType.BPIn,
|
||||
SplittingPlane = new Plane(new Vector3(1f, 0f, 0f), 0f),
|
||||
PosNode = leaf,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ── D3.1: pre-fix admits, post-fix does not, with in-session sabotage ──
|
||||
|
||||
/// <summary>
|
||||
/// D3.1. A part whose SPHERE reaches the portal plane (passing the
|
||||
/// cheap-reject, same as the pre-existing "near portal" sphere fixture)
|
||||
/// but whose authored BOX is small and does not reach the plane at all.
|
||||
/// Pre-fix (<see cref="CellTransit.FindTransitCellsSphere"/>, still
|
||||
/// directly callable and unmodified) admits cell B. Post-fix
|
||||
/// (<see cref="CellTransit.FindTransitCellsBox"/>, what
|
||||
/// <see cref="CellTransit.BuildShadowCellSetFromParts"/> now calls) does
|
||||
/// not.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SphereReachesPortal_BoxDoesNot_PreFixAdmitsPostFixRejects()
|
||||
{
|
||||
var cellA = MakeCellWithPortalAtRightWall(Matrix4x4.Identity, otherCellId: 0x0101, flags: 0);
|
||||
var cellBT = Matrix4x4.CreateTranslation(new Vector3(5f, 0f, 0f));
|
||||
Matrix4x4.Invert(cellBT, out var cellBInv);
|
||||
// No CellBSP on cell B, deliberately: this exercises the SAME
|
||||
// unloaded-neighbour hint path CellTransitFindTransitCellsSphereTests.
|
||||
// SphereInsideCellA_NearPortal_AddsCellB relies on. Giving cell B a
|
||||
// containment BSP here would switch FindTransitCellsSphere onto the
|
||||
// sphere_intersects_cell branch instead, which this fixture's sphere
|
||||
// position was never designed to satisfy.
|
||||
var cellB = new CellPhysics
|
||||
{
|
||||
WorldTransform = cellBT,
|
||||
InverseWorldTransform = cellBInv,
|
||||
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
||||
};
|
||||
|
||||
var cache = new PhysicsDataCache();
|
||||
cache.RegisterCellStructForTest(0xA9B40100u, cellA);
|
||||
cache.RegisterCellStructForTest(0xA9B40101u, cellB);
|
||||
|
||||
// Same part origin as CellTransitFindTransitCellsSphereTests'
|
||||
// "near portal" case: local x=2.0, sphere radius=0.5 -> reaches
|
||||
// x=2.5 (the portal plane). The authored BOX around that same part
|
||||
// origin is +-0.1m, so its max.x = 2.1 - nowhere near the plane.
|
||||
var partWorldPos = new Vector3(2.0f, 0f, 2.5f);
|
||||
var sphere = new Sphere { Origin = partWorldPos, Radius = 0.5f };
|
||||
var box = new ShadowPartBox[]
|
||||
{
|
||||
MakeBox(new Vector3(-0.1f), new Vector3(0.1f), partWorldPos, Quaternion.Identity),
|
||||
};
|
||||
|
||||
// Pre-fix: the sphere-only traversal still exists, unmodified.
|
||||
var preFixCandidates = new HashSet<uint>();
|
||||
CellTransit.FindTransitCellsSphere(
|
||||
cache, cellA, currentCellId: 0xA9B40100u,
|
||||
partWorldPos, sphereRadius: 0.5f, preFixCandidates, out bool preFixExitOutside);
|
||||
Assert.Contains(0xA9B40101u, preFixCandidates);
|
||||
Assert.False(preFixExitOutside);
|
||||
|
||||
// Post-fix: the box-admitting traversal the D2 rewire installs.
|
||||
var postFixCandidates = new HashSet<uint>();
|
||||
CellTransit.FindTransitCellsBox(
|
||||
cache, cellA, currentCellId: 0xA9B40100u,
|
||||
box, new[] { sphere }, postFixCandidates, out bool postFixExitOutside);
|
||||
Assert.DoesNotContain(0xA9B40101u, postFixCandidates);
|
||||
Assert.False(postFixExitOutside);
|
||||
|
||||
// End-to-end at the production entry point: BuildShadowCellSetFromParts
|
||||
// (which now calls FindTransitCellsBox internally) must NOT include
|
||||
// cell B either.
|
||||
IReadOnlyList<uint> endToEnd = CellTransit.BuildShadowCellSetFromParts(
|
||||
cache, seedCellId: 0xA9B40100u, box, new[] { sphere }, isStatic: false);
|
||||
Assert.DoesNotContain(0xA9B40101u, endToEnd);
|
||||
}
|
||||
|
||||
// ── D3.2: inverse guard — a box that DOES cross admits, unchanged ──────
|
||||
|
||||
/// <summary>
|
||||
/// D3.2. The inverse guard: a part whose box genuinely crosses the
|
||||
/// portal plane is admitted by BOTH the pre-fix sphere test and the
|
||||
/// post-fix box test — over-inclusion strictly shrinks the admitted set,
|
||||
/// it never drops a genuine crossing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BoxCrossesPortal_AdmittedBeforeAndAfter_UnloadedNeighbour()
|
||||
{
|
||||
var cellA = MakeCellWithPortalAtRightWall(Matrix4x4.Identity, otherCellId: 0x0101, flags: 0);
|
||||
var cellBT = Matrix4x4.CreateTranslation(new Vector3(5f, 0f, 0f));
|
||||
Matrix4x4.Invert(cellBT, out var cellBInv);
|
||||
// No CellBSP -- exercises the unloaded-neighbour hint path on the
|
||||
// pre-fix side and the unconditional load-hint add on the post-fix
|
||||
// side, same as SphereReachesPortal_BoxDoesNot_PreFixAdmitsPostFixRejects.
|
||||
var cellB = new CellPhysics
|
||||
{
|
||||
WorldTransform = cellBT,
|
||||
InverseWorldTransform = cellBInv,
|
||||
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
||||
};
|
||||
|
||||
var cache = new PhysicsDataCache();
|
||||
cache.RegisterCellStructForTest(0xA9B40100u, cellA);
|
||||
cache.RegisterCellStructForTest(0xA9B40101u, cellB);
|
||||
|
||||
var partWorldPos = new Vector3(2.0f, 0f, 2.5f);
|
||||
var sphere = new Sphere { Origin = partWorldPos, Radius = 0.5f };
|
||||
// A box that genuinely spans past x=2.5 in cell A's local frame:
|
||||
// local box +-0.7m around the part origin -> max.x = 2.7.
|
||||
var box = new ShadowPartBox[]
|
||||
{
|
||||
MakeBox(new Vector3(-0.7f), new Vector3(0.7f), partWorldPos, Quaternion.Identity),
|
||||
};
|
||||
|
||||
var preFixCandidates = new HashSet<uint>();
|
||||
CellTransit.FindTransitCellsSphere(
|
||||
cache, cellA, currentCellId: 0xA9B40100u,
|
||||
partWorldPos, sphereRadius: 0.5f, preFixCandidates, out bool preFixExitOutside);
|
||||
Assert.Contains(0xA9B40101u, preFixCandidates);
|
||||
Assert.False(preFixExitOutside);
|
||||
|
||||
var postFixCandidates = new HashSet<uint>();
|
||||
CellTransit.FindTransitCellsBox(
|
||||
cache, cellA, currentCellId: 0xA9B40100u,
|
||||
box, new[] { sphere }, postFixCandidates, out bool postFixExitOutside);
|
||||
Assert.Contains(0xA9B40101u, postFixCandidates);
|
||||
Assert.False(postFixExitOutside);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// D3.2 (loaded-neighbour variant). Retail's part-array overload applies
|
||||
/// the SAME cheap-reject + box-admit test whether the destination is
|
||||
/// loaded or not (see docs/research/2026-08-07-ap159-pseudocode.md §1's
|
||||
/// "Structural difference" note) -- unlike the sphere overload, which
|
||||
/// skips straight to <c>sphere_intersects_cell</c> for a loaded
|
||||
/// neighbour with no admit gate at all. This fixture positions the box
|
||||
/// so it passes cell A's admit test AND genuinely lands inside cell B's
|
||||
/// real containment BSP, exercising
|
||||
/// <see cref="CollisionTraversal.BoxIntersectsCell"/>'s loaded path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BoxCrossesPortal_AdmittedBeforeAndAfter_LoadedNeighbourGate()
|
||||
{
|
||||
var cellA = MakeCellWithPortalAtRightWall(Matrix4x4.Identity, otherCellId: 0x0101, flags: 0);
|
||||
var cellBT = Matrix4x4.CreateTranslation(new Vector3(3f, 0f, 0f));
|
||||
Matrix4x4.Invert(cellBT, out var cellBInv);
|
||||
var cellB = new CellPhysics
|
||||
{
|
||||
WorldTransform = cellBT,
|
||||
InverseWorldTransform = cellBInv,
|
||||
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
||||
CellBSP = SinglePlaneCellBsp(),
|
||||
};
|
||||
|
||||
var cache = new PhysicsDataCache();
|
||||
cache.RegisterCellStructForTest(0xA9B40100u, cellA);
|
||||
cache.RegisterCellStructForTest(0xA9B40101u, cellB);
|
||||
|
||||
// Cell A local dist = x-2.5 in [0.1, 0.7] -> uniformly positive,
|
||||
// past the portal. Cell B local dist = x-3.0 in [-0.4, 0.2] ->
|
||||
// straddles cell B's own containment plane at local x=0, so the
|
||||
// box genuinely lands inside cell B's volume.
|
||||
var partWorldPos = new Vector3(2.9f, 0f, 2.5f);
|
||||
var sphere = new Sphere { Origin = partWorldPos, Radius = 0.5f };
|
||||
var box = new ShadowPartBox[]
|
||||
{
|
||||
MakeBox(new Vector3(-0.3f), new Vector3(0.3f), partWorldPos, Quaternion.Identity),
|
||||
};
|
||||
|
||||
var preFixCandidates = new HashSet<uint>();
|
||||
CellTransit.FindTransitCellsSphere(
|
||||
cache, cellA, currentCellId: 0xA9B40100u,
|
||||
partWorldPos, sphereRadius: 0.5f, preFixCandidates, out bool preFixExitOutside);
|
||||
Assert.Contains(0xA9B40101u, preFixCandidates);
|
||||
Assert.False(preFixExitOutside);
|
||||
|
||||
var postFixCandidates = new HashSet<uint>();
|
||||
CellTransit.FindTransitCellsBox(
|
||||
cache, cellA, currentCellId: 0xA9B40100u,
|
||||
box, new[] { sphere }, postFixCandidates, out bool postFixExitOutside);
|
||||
Assert.Contains(0xA9B40101u, postFixCandidates);
|
||||
Assert.False(postFixExitOutside);
|
||||
}
|
||||
|
||||
// ── D3.3: direction assertion over an installed-DAT sweep ──────────────
|
||||
|
||||
/// <summary>
|
||||
/// D3.3. Over real installed EnvCells (real portals, real containment
|
||||
/// BSPs), for a randomized population of synthetic BSP-part placements
|
||||
/// near each portal (deliberately using an OVERSIZED sphere against a
|
||||
/// TIGHT authored box, mirroring the real-world AP-156 divergence
|
||||
/// pattern), the box-admitting membership set is a SUBSET of the
|
||||
/// sphere-only set for every swept object — never a superset. Reports
|
||||
/// objects swept, cells removed, cells added (must be zero) via the test
|
||||
/// output (xunit console capture); see the AP-159 implementation report
|
||||
/// for the exact counts from this run.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void InstalledDat_RandomizedPartSweepNearRealPortals_PostFixMembershipIsSubsetOfPreFix()
|
||||
{
|
||||
string? datDirectory = ConformanceDats.ResolveDatDir();
|
||||
if (datDirectory is null)
|
||||
return;
|
||||
|
||||
using var dats = new DatCollection(datDirectory, DatAccessType.Read);
|
||||
var cache = new PhysicsDataCache();
|
||||
uint[] cellIds =
|
||||
[
|
||||
0x8A02_016Eu,
|
||||
0x8A02_017Au,
|
||||
0xA9B4_013Fu,
|
||||
0xA9B4_0150u,
|
||||
0xA9B4_0159u,
|
||||
0xA9B4_015Au,
|
||||
0xA9B4_0161u,
|
||||
0xA9B4_0162u,
|
||||
0xA9B4_0164u,
|
||||
0xA9B4_0166u,
|
||||
];
|
||||
foreach (uint cellId in cellIds)
|
||||
ConformanceDats.LoadEnvCell(dats, cache, cellId);
|
||||
|
||||
var random = new Random(0x3335_5330);
|
||||
int objectsSwept = 0;
|
||||
int cellsRemovedTotal = 0;
|
||||
int cellsAddedTotal = 0;
|
||||
int objectsWithChangedMembership = 0;
|
||||
var worstExamples = new List<(string Object, int Before, int After, int Removed)>();
|
||||
|
||||
foreach (uint cellId in cellIds)
|
||||
{
|
||||
CellPhysics cell = Assert.IsType<CellPhysics>(cache.GetCellStruct(cellId));
|
||||
if (cell.Portals.Count == 0 || cell.PortalPolygons is null)
|
||||
continue;
|
||||
|
||||
foreach (PortalInfo portal in cell.Portals)
|
||||
{
|
||||
if (!cell.PortalPolygons.TryGetValue(portal.PolygonId, out ResolvedPolygon? portalPoly) ||
|
||||
portalPoly.Vertices.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3 localAnchor = Vector3.Zero;
|
||||
foreach (Vector3 v in portalPoly.Vertices)
|
||||
localAnchor += v;
|
||||
localAnchor /= portalPoly.Vertices.Length;
|
||||
|
||||
for (int iteration = 0; iteration < 40; iteration++)
|
||||
{
|
||||
// Jitter the anchor along the portal's own local frame so
|
||||
// some placements land squarely on one side, some
|
||||
// straddle, and some sit right at the plane -- the
|
||||
// population a real object population would produce.
|
||||
Vector3 jitter = new(
|
||||
NextFloat(random, -0.6f, 0.6f),
|
||||
NextFloat(random, -0.6f, 0.6f),
|
||||
NextFloat(random, -0.6f, 0.6f));
|
||||
Vector3 localPartPos = localAnchor + jitter;
|
||||
Vector3 worldPartPos = Vector3.Transform(
|
||||
localPartPos, cell.WorldTransform);
|
||||
|
||||
// Deliberately oversized sphere vs a tight authored box --
|
||||
// the AP-156 divergence pattern this contract closes.
|
||||
float sphereRadius = NextFloat(random, 0.3f, 1.2f);
|
||||
float boxHalfExtent = NextFloat(random, 0.02f, 0.25f);
|
||||
|
||||
var sphere = new Sphere
|
||||
{
|
||||
Origin = worldPartPos,
|
||||
Radius = sphereRadius,
|
||||
};
|
||||
var boxes = new ShadowPartBox[]
|
||||
{
|
||||
MakeBox(
|
||||
new Vector3(-boxHalfExtent),
|
||||
new Vector3(boxHalfExtent),
|
||||
worldPartPos,
|
||||
Quaternion.Identity),
|
||||
};
|
||||
|
||||
var preFix = new HashSet<uint>();
|
||||
CellTransit.FindTransitCellsSphere(
|
||||
cache, cell, cellId, worldPartPos, sphereRadius,
|
||||
preFix, out _);
|
||||
|
||||
var postFix = new HashSet<uint>();
|
||||
CellTransit.FindTransitCellsBox(
|
||||
cache, cell, cellId, boxes, new[] { sphere },
|
||||
postFix, out _);
|
||||
|
||||
objectsSwept++;
|
||||
|
||||
var removed = new List<uint>();
|
||||
var added = new List<uint>();
|
||||
foreach (uint id in preFix)
|
||||
if (!postFix.Contains(id)) removed.Add(id);
|
||||
foreach (uint id in postFix)
|
||||
if (!preFix.Contains(id)) added.Add(id);
|
||||
|
||||
cellsRemovedTotal += removed.Count;
|
||||
cellsAddedTotal += added.Count;
|
||||
|
||||
// Review F3 (2026-08-07): zero-added holds for THIS
|
||||
// population BY CONSTRUCTION (box rigged far smaller than
|
||||
// sphere, the AP-156 pattern). It is NOT a structural
|
||||
// invariant of the port: in production the box is the
|
||||
// whole-vertex-array AABB while the sphere bounds only
|
||||
// the physics polygons, so the real box can EXCEED the
|
||||
// real sphere and the loaded-neighbour gate can admit
|
||||
// cells the sphere test would not — which is retail's
|
||||
// behaviour, not a defect. The production-ratio
|
||||
// population below measures that direction honestly.
|
||||
Assert.True(
|
||||
added.Count == 0,
|
||||
$"cell 0x{cellId:X8} portal->0x{portal.OtherCellId:X4} " +
|
||||
$"iteration {iteration}: post-fix ADDED cell(s) " +
|
||||
$"{string.Join(",", added.Select(id => $"0x{id:X8}"))} " +
|
||||
"under a box RIGGED smaller than the sphere -- for this " +
|
||||
"population the admit must strictly shrink.");
|
||||
|
||||
if (removed.Count > 0)
|
||||
{
|
||||
objectsWithChangedMembership++;
|
||||
string label =
|
||||
$"cell=0x{cellId:X8},portal->0x{portal.OtherCellId:X4}," +
|
||||
$"iter={iteration},pos=({worldPartPos.X:F2}," +
|
||||
$"{worldPartPos.Y:F2},{worldPartPos.Z:F2})," +
|
||||
$"r={sphereRadius:F3},box=+-{boxHalfExtent:F3}";
|
||||
worstExamples.Add((label, preFix.Count, postFix.Count, removed.Count));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
worstExamples.Sort((a, b) => b.Removed.CompareTo(a.Removed));
|
||||
|
||||
Assert.Equal(0, cellsAddedTotal);
|
||||
Assert.True(objectsSwept > 0, "installed-DAT sweep found no portals to test.");
|
||||
|
||||
// Review F4 (2026-08-07): without this, the sweep passes identically
|
||||
// if the box admit is a NO-OP returning the sphere set bit-for-bit.
|
||||
// The rigged population guarantees real shrinkage exists in installed
|
||||
// data (41% of placements in the landing run), so zero here means the
|
||||
// rewire came unwired.
|
||||
Assert.True(
|
||||
cellsRemovedTotal > 0,
|
||||
$"the box admit removed no cells across {objectsSwept} rigged " +
|
||||
"placements — indistinguishable from an unwired no-op.");
|
||||
Console.WriteLine(
|
||||
$"direction sweep (rigged population): objectsSwept={objectsSwept} " +
|
||||
$"changed={objectsWithChangedMembership} removed={cellsRemovedTotal} " +
|
||||
$"added={cellsAddedTotal}");
|
||||
|
||||
// Review F3/F6 (2026-08-07): a SECOND population at production-like
|
||||
// ratios (box >= sphere — the whole-vertex-array AABB vs the
|
||||
// physics-BSP root sphere), plus a loaded-branch counter. No
|
||||
// zero-added assertion here — adds are RETAIL-CORRECT in this
|
||||
// direction; the numbers are printed so the register row's severity
|
||||
// stays honest, and the loaded-neighbour gate's reach is measured
|
||||
// rather than presumed.
|
||||
int prodSwept = 0, prodAdded = 0, prodRemoved = 0, loadedBranchHits = 0;
|
||||
var prodRandom = new Random(0x3335_5331);
|
||||
foreach (uint cellId in cellIds)
|
||||
{
|
||||
CellPhysics? cell = cache.GetCellStruct(cellId) as CellPhysics;
|
||||
if (cell is null || cell.Portals.Count == 0 || cell.PortalPolygons is null)
|
||||
continue;
|
||||
foreach (PortalInfo portal in cell.Portals)
|
||||
{
|
||||
if (!cell.PortalPolygons.TryGetValue(portal.PolygonId, out ResolvedPolygon? prodPoly) ||
|
||||
prodPoly.Vertices.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Vector3 localAnchor = Vector3.Zero;
|
||||
foreach (Vector3 v in prodPoly.Vertices)
|
||||
localAnchor += v;
|
||||
localAnchor /= prodPoly.Vertices.Length;
|
||||
for (int iteration = 0; iteration < 25; iteration++)
|
||||
{
|
||||
Vector3 jitter = new(
|
||||
NextFloat(prodRandom, -0.6f, 0.6f),
|
||||
NextFloat(prodRandom, -0.6f, 0.6f),
|
||||
NextFloat(prodRandom, -0.6f, 0.6f));
|
||||
Vector3 worldPartPos = Vector3.Transform(
|
||||
localAnchor + jitter, cell.WorldTransform);
|
||||
float sphereRadius = NextFloat(prodRandom, 0.2f, 0.6f);
|
||||
float boxHalfExtent = NextFloat(prodRandom, sphereRadius, sphereRadius * 2.5f);
|
||||
var sphere = new Sphere { Origin = worldPartPos, Radius = sphereRadius };
|
||||
var boxes = new ShadowPartBox[]
|
||||
{
|
||||
MakeBox(new Vector3(-boxHalfExtent), new Vector3(boxHalfExtent),
|
||||
worldPartPos, Quaternion.Identity),
|
||||
};
|
||||
var preFix = new HashSet<uint>();
|
||||
CellTransit.FindTransitCellsSphere(
|
||||
cache, cell, cellId, worldPartPos, sphereRadius, preFix, out _);
|
||||
var postFix = new HashSet<uint>();
|
||||
CellTransit.FindTransitCellsBox(
|
||||
cache, cell, cellId, boxes, new[] { sphere }, postFix, out _);
|
||||
prodSwept++;
|
||||
foreach (uint id in postFix)
|
||||
if (!preFix.Contains(id))
|
||||
{
|
||||
prodAdded++;
|
||||
if (cache.GetCellStruct(id) is not null) loadedBranchHits++;
|
||||
}
|
||||
foreach (uint id in preFix)
|
||||
if (!postFix.Contains(id)) prodRemoved++;
|
||||
}
|
||||
}
|
||||
}
|
||||
Console.WriteLine(
|
||||
$"direction sweep (production-ratio population): swept={prodSwept} " +
|
||||
$"added={prodAdded} removed={prodRemoved} loadedBranchAdds={loadedBranchHits}");
|
||||
Assert.True(prodSwept > 0);
|
||||
}
|
||||
|
||||
private static ShadowPartBox MakeBox(
|
||||
Vector3 localMin,
|
||||
Vector3 localMax,
|
||||
Vector3 worldPosition,
|
||||
Quaternion worldRotation)
|
||||
{
|
||||
var shape = ShadowShape.Bsp(
|
||||
gfxObjId: 0x010046D8u,
|
||||
localPosition: Vector3.Zero,
|
||||
localRotation: Quaternion.Identity,
|
||||
scale: 1f,
|
||||
localGeometry: ShadowPartGeometry.Create(
|
||||
new FlatCollisionSphere(Vector3.Zero, 0.01f),
|
||||
new FlatGfxObjVisualBounds(
|
||||
localMin,
|
||||
localMax,
|
||||
(localMin + localMax) * 0.5f,
|
||||
((localMax - localMin) * 0.5f).Length(),
|
||||
(localMax - localMin) * 0.5f)));
|
||||
return ShadowPartBox.FromShape(shape, worldPosition, worldRotation);
|
||||
}
|
||||
|
||||
private static float NextFloat(Random random, float minimum, float maximum)
|
||||
=> minimum + (float)random.NextDouble() * (maximum - minimum);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue