acdream/tests/AcDream.App.Tests/Physics/RetailCellArrayComparatorInstalledDatTests.cs
Erik 707d2803a4 feat(render): S2 chunk 2 - render statics borrow the registry's retail cell array
WalkProductionWorldData no longer floods; its indoor and outdoor static
sweeps read ShadowObjectRegistry.TryGetRetailCellArray (retail's
calc_cross_cells_static @0x00515160 -> AddPartsShadow @0x00517e40
CELLARRAY, computed once at registration). The App-owned render flood
(ResolveStaticRenderCells, its fingerprint cache, the primitive-Setup
special case) and Core's ComputeStaticRenderCells are deleted. The one
remaining fallback, an entity the physics publisher has not registered yet
while the projection journal already published it, buckets to the authored
parent cell and is counted per frame (UnregisteredStaticRenderFallbackCount)
for chunk 5 to judge on the connected route.

The Facility stair pin now registers at the projection's own entity id
(the old pure-function test never carried identity) and reads the retail
array; the installed-DAT comparator compares retail against collision.

Gates (run in the implementer's isolated worktree at identical content):
Release build 0/0; Core Physics 2,202/2,202; App hermetic 6,760/6,760 (the
two added WalkProductionWorldData tests); installed-DAT walk/flood/stair
family 24/24; Runtime 1,884/1,884.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 22:31:46 +02:00

598 lines
27 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using AcDream.Core.Meshing;
using AcDream.Core.Physics;
using AcDream.Core.World;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
using DatReaderWriter.Types;
using Xunit;
using Xunit.Abstractions;
using DatEnvCell = DatReaderWriter.DBObjs.EnvCell;
using DatGfxObj = DatReaderWriter.DBObjs.GfxObj;
using DatSetup = DatReaderWriter.DBObjs.Setup;
namespace AcDream.App.Tests.Physics;
/// <summary>
/// Campaign OVERHAUL S2 chunk 1 installed-DAT comparator
/// (<c>docs/plans/2026-09-01-campaign-overhaul-world-solidity.md</c> §9 S2
/// chunk 1; <c>docs/research/2026-09-01-overhaul/s2-membership-ownership-map.md</c>
/// §3). Chunk 1b threaded the whole visual part array through every
/// production <c>RegisterMultiPart</c> caller
/// (<see cref="AcDream.App.Streaming.LandblockPhysicsPublisher.PublishStaticEntity"/>,
/// its <c>AcDream.Content.LandblockPhysicsContentBuilder.PublishStaticCollision</c>
/// no-window twin, and <c>AcDream.Runtime.Physics.LiveEntityCollisionBuilder</c>)
/// as a zero-pixel-change side product. This suite registers real installed-DAT
/// statics through that SAME dispatch — <see cref="ShadowShapeBuilder.FromLandblockBspParts"/>
/// first, else the Setup CylSphere/Sphere fallback exactly as
/// <c>PublishStaticEntity</c> branches, plus the
/// <see cref="ShadowShapeBuilder.FromStaticRenderParts"/> part-array input —
/// against a registry backed by the real <see cref="PhysicsDataCache"/>, then
/// prints and compares two answers per fixture: retail's CELLARRAY
/// (<see cref="ShadowObjectRegistry.TryGetRetailCellArray"/>) and today's
/// COLLISION cells (<see cref="ShadowObjectRegistry.GetOwnerCells"/>). Chunk
/// 2 deleted the third "old render cells" column
/// (<c>ShadowObjectRegistry.ComputeStaticRenderCells</c>) along with its
/// only production consumer (<c>WalkProductionWorldData.ResolveStaticRenderCells</c>).
/// Every fixture here is BSP-bearing, and chunk 1's evidence
/// (`docs/research/2026-09-01-overhaul/s2-membership-ownership-map.md` §5)
/// found retail == collision == old-render exactly for all five as a
/// structural consequence of the shared bbox-route primitive — so this
/// asserts retail == collision directly; a genuine divergence (expected only
/// for a fixture mixing colliding and non-colliding parts, none of which
/// exist among these fixtures) remains chunk 3 evidence.
/// </summary>
[Trait("Lane", "InstalledDat")]
public sealed class RetailCellArrayComparatorInstalledDatTests
{
private const string SkipMessage =
"Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.";
private readonly ITestOutputHelper _out;
public RetailCellArrayComparatorInstalledDatTests(ITestOutputHelper output) => _out = output;
private static string? ResolveDatDir()
{
var fromEnv = System.Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv)) return fromEnv;
var def = Path.Combine(
System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
return Directory.Exists(def) ? def : null;
}
// ---------------------------------------------------------------
// Shared comparator machinery
// ---------------------------------------------------------------
private static PhysicsEngine BuildOutdoorEngine(uint landblockId)
{
var cache = new PhysicsDataCache();
var engine = new PhysicsEngine { DataCache = cache };
var heights = new byte[81];
var heightTable = new float[256];
for (int i = 0; i < 256; i++) heightTable[i] = -1000f;
engine.AddLandblock(landblockId, new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(), 0f, 0f);
return engine;
}
/// <summary>
/// Caches every resolvable EnvCell CellStruct in [<paramref name="lowStart"/>,
/// <paramref name="lowEnd"/>] for <paramref name="landblockId"/> — the
/// same pattern <c>Issue177StairDescentCameraFloodTests.BuildHubEngine</c>
/// uses, generalized to any landblock/range so <see cref="CellTransit"/>
/// can traverse real portal topology for an indoor flood.
/// </summary>
private static PhysicsEngine BuildIndoorEngine(
DatCollection dats, uint landblockId, uint lowStart, uint lowEnd)
{
var cache = new PhysicsDataCache();
var engine = new PhysicsEngine { DataCache = cache };
for (uint low = lowStart; low <= lowEnd; low++)
{
uint id = landblockId | low;
var datCell = dats.Get<DatEnvCell>(id);
if (datCell is null) continue;
var environment = dats.Get<DatReaderWriter.DBObjs.Environment>(
0x0D000000u | datCell.EnvironmentId);
if (environment is null) continue;
if (!environment.Cells.TryGetValue(datCell.CellStructure, out var cellStruct)
|| cellStruct is null)
{
continue;
}
var world = Matrix4x4.CreateFromQuaternion(datCell.Position.Orientation)
* Matrix4x4.CreateTranslation(datCell.Position.Origin);
cache.CacheCellStruct(id, datCell, cellStruct, world);
}
var heights = new byte[81];
var heightTable = new float[256];
for (int i = 0; i < 256; i++) heightTable[i] = -1000f;
engine.AddLandblock(landblockId, new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(), 0f, 0f);
return engine;
}
/// <summary>
/// Verbatim copy of <c>ShadowObjectRegistry.DeriveOutdoorSeed</c> (private
/// there) — needed here so an outdoor fixture with no explicit
/// <c>seedCellId</c> resolves the SAME seed
/// <see cref="ShadowObjectRegistry.RegisterMultiPart"/> would derive
/// internally, for printing alongside the registered result.
/// </summary>
private static uint DeriveOutdoorSeedForTest(
Vector3 worldPos, float worldOffsetX, float worldOffsetY, uint landblockId)
{
if (landblockId == 0u) return 0u;
float localX = worldPos.X - worldOffsetX;
float localY = worldPos.Y - worldOffsetY;
int cx = (int)Math.Clamp(localX / 24f, 0f, 7f);
int cy = (int)Math.Clamp(localY / 24f, 0f, 7f);
uint lbPrefix = landblockId & 0xFFFF0000u;
return lbPrefix | (uint)(cx * 8 + cy + 1);
}
private readonly record struct ComparatorOutcome(
string Label,
uint EntityId,
IReadOnlyList<uint> RetailCells,
IReadOnlyList<uint> CollisionCells,
RetailCellArrayRoute Route,
int PartCount,
int BspShapeCount,
bool CollisionRegistered);
/// <summary>
/// Registers one static through the REAL production dispatch —
/// <see cref="ShadowShapeBuilder.FromLandblockBspParts"/> first, else the
/// Setup CylSphere/Sphere fallback, exactly the branch order
/// <c>LandblockPhysicsPublisher.PublishStaticEntity</c> uses — plus the
/// chunk-1b whole-part-array side product
/// (<see cref="ShadowShapeBuilder.FromStaticRenderParts"/>), then reads
/// back and prints both comparator answers.
/// </summary>
private ComparatorOutcome RegisterAndCompare(
string label,
PhysicsEngine engine,
PhysicsDataCache cache,
uint entityId,
IReadOnlyList<MeshRef> meshRefs,
DatSetup? setupFallback,
Vector3 worldPos,
Quaternion worldRot,
float worldOffsetX,
float worldOffsetY,
uint landblockId,
uint seedCellId)
{
IReadOnlyList<ShadowShape> bspShapes = ShadowShapeBuilder.FromLandblockBspParts(
meshRefs, isBuildingShell: false, cache.GetGfxObj);
IReadOnlyList<ShadowShape> partArray = ShadowShapeBuilder.FromStaticRenderParts(
meshRefs, cache.GetGfxObj, cache.GetVisualBounds, out bool hasPhysicsBsp);
uint resolvedSeed = seedCellId != 0u
? seedCellId
: DeriveOutdoorSeedForTest(worldPos, worldOffsetX, worldOffsetY, landblockId);
bool registered = false;
if (bspShapes.Count > 0)
{
engine.ShadowObjects.RegisterMultiPart(
entityId, worldPos, worldRot, bspShapes, 0u, EntityCollisionFlags.None,
worldOffsetX, worldOffsetY, landblockId,
seedCellId: resolvedSeed, isStatic: true, partArray: partArray);
registered = true;
}
else if (setupFallback is not null)
{
FlatSetupCollision flatSetup = FlatCollisionAssetBuilder.FlattenSetup(setupFallback);
const float scale = 1f;
var setupShapes = new List<ShadowShape>();
for (int i = 0; i < flatSetup.Cylinders.Length; i++)
{
FlatCollisionCylinder cyl = flatSetup.Cylinders[i];
float radius = cyl.Radius * scale;
float baseHeight = cyl.Height > 0f ? cyl.Height : cyl.Radius * 4f;
if (radius <= 0f) continue;
setupShapes.Add(ShadowShape.Cylinder(
gfxObjId: 0u,
localPosition: cyl.Origin * scale,
localRotation: Quaternion.Identity,
scale: scale,
radius: radius,
cylHeight: baseHeight * scale));
}
if (flatSetup.Cylinders.Length == 0)
{
for (int i = 0; i < flatSetup.Spheres.Length; i++)
{
FlatCollisionSphere sph = flatSetup.Spheres[i];
if (sph.Radius <= 0f) continue;
setupShapes.Add(ShadowShape.Sphere(
gfxObjId: 0u,
localPosition: sph.Origin * scale,
localRotation: Quaternion.Identity,
scale: scale,
radius: sph.Radius * scale));
}
}
if (setupShapes.Count > 0)
{
engine.ShadowObjects.RegisterMultiPart(
entityId, worldPos, worldRot, setupShapes, 0u, EntityCollisionFlags.None,
worldOffsetX, worldOffsetY, landblockId,
seedCellId: resolvedSeed, isStatic: true, partArray: partArray);
registered = true;
}
}
engine.ShadowObjects.TryGetRetailCellArray(entityId, out IReadOnlyList<uint> retailCells);
IReadOnlyList<uint> collisionCells = engine.ShadowObjects.GetOwnerCells(entityId);
RetailCellArrayRoute route = engine.ShadowObjects.GetRetailCellArrayRoute(entityId);
PrintOutcome(
label, entityId, retailCells, collisionCells, route,
partArray.Count, bspShapes.Count, hasPhysicsBsp, registered, resolvedSeed);
return new ComparatorOutcome(
label, entityId, retailCells, collisionCells, route,
partArray.Count, bspShapes.Count, registered);
}
private void PrintOutcome(
string label,
uint entityId,
IReadOnlyList<uint> retailCells,
IReadOnlyList<uint> collisionCells,
RetailCellArrayRoute route,
int partCount,
int bspShapeCount,
bool hasPhysicsBsp,
bool collisionRegistered,
uint seedCellId)
{
static string CellSet(IEnumerable<uint> ids) =>
"[" + string.Join(",", ids.Select(id => FormattableString.Invariant($"0x{id:X8}"))) + "]";
_out.WriteLine(FormattableString.Invariant(
$"--- {label} (entity=0x{entityId:X8} seed=0x{seedCellId:X8}) ---"));
_out.WriteLine(FormattableString.Invariant(
$" parts={partCount} bspShapes={bspShapeCount} hasPhysicsBsp={hasPhysicsBsp} collisionRegistered={collisionRegistered} route={route}"));
_out.WriteLine($" retail n={retailCells.Count} {CellSet(retailCells)}");
_out.WriteLine($" collision n={collisionCells.Count} {CellSet(collisionCells)}");
foreach (uint cellId in retailCells)
{
int entries = 0;
foreach (RetailPartEntry entry in _lastEngine!.ShadowObjects.GetRetailPartEntriesInCell(cellId))
if (entry.EntityId == entityId) entries++;
_out.WriteLine(FormattableString.Invariant(
$" cell 0x{cellId:X8}: {entries} retail part entries"));
}
IEnumerable<uint> retailOnly = retailCells.Except(collisionCells);
IEnumerable<uint> collisionOnly = collisionCells.Except(retailCells);
if (retailOnly.Any() || collisionOnly.Any())
{
_out.WriteLine(
$" retail-vs-collision difference: "
+ $"retailOnly={CellSet(retailOnly)} collisionOnly={CellSet(collisionOnly)}");
}
}
// PrintOutcome needs the engine to look up per-cell entries; stash it for
// the duration of one fixture's call rather than widen every signature.
private PhysicsEngine? _lastEngine;
private ComparatorOutcome RunFixture(
string label,
PhysicsEngine engine,
PhysicsDataCache cache,
uint entityId,
IReadOnlyList<MeshRef> meshRefs,
DatSetup? setupFallback,
Vector3 worldPos,
Quaternion worldRot,
float worldOffsetX,
float worldOffsetY,
uint landblockId,
uint seedCellId)
{
_lastEngine = engine;
return RegisterAndCompare(
label, engine, cache, entityId, meshRefs, setupFallback,
worldPos, worldRot, worldOffsetX, worldOffsetY, landblockId, seedCellId);
}
private static void AssertSameCells(IReadOnlyList<uint> retail, IReadOnlyList<uint> collision)
{
Assert.Equal(
collision.OrderBy(id => id).ToArray(),
retail.OrderBy(id => id).ToArray());
}
// ---------------------------------------------------------------
// Fixture 1 — Facility Hub stair Setup 0x02000623 (0x8A02015F/0x8A02015E)
// ---------------------------------------------------------------
[Fact]
public void FacilityHubStair_RetailCellArrayMatchesCollisionCells()
{
string? datDir = ResolveDatDir();
if (datDir is null) Assert.Fail(SkipMessage);
using var dats = new DatCollection(datDir, DatAccessType.Read);
const uint FacilityHub = 0x8A020000u;
const uint setupId = 0x02000623u;
PhysicsEngine engine = BuildIndoorEngine(dats, FacilityHub, 0x0100u, 0x01FFu);
var cache = (PhysicsDataCache)engine.DataCache!;
DatSetup setup = Assert.IsType<DatSetup>(dats.Get<DatSetup>(setupId));
IReadOnlyList<MeshRef> meshRefs = SetupMesh.Flatten(setup);
foreach (MeshRef meshRef in meshRefs)
{
DatGfxObj gfxObj = Assert.IsType<DatGfxObj>(dats.Get<DatGfxObj>(meshRef.GfxObjId));
cache.CacheGfxObj(meshRef.GfxObjId, gfxObj);
}
DatEnvCell parent = Assert.IsType<DatEnvCell>(
dats.Get<DatEnvCell>(FacilityHub | 0x015Fu));
var stair = Assert.Single(parent.StaticObjects, s => s.Id == setupId);
Vector3 worldPos = new(
stair.Frame.Origin.X, stair.Frame.Origin.Y, stair.Frame.Origin.Z);
Quaternion worldRot = stair.Frame.Orientation;
const uint entityId = 0x7F000001u;
ComparatorOutcome outcome = RunFixture(
"1: Facility Hub stair Setup 0x02000623",
engine, cache, entityId, meshRefs, setup,
worldPos, worldRot,
worldOffsetX: 0f, worldOffsetY: 0f,
landblockId: FacilityHub,
seedCellId: FacilityHub | 0x015Fu);
Assert.Contains(FacilityHub | 0x015Fu, outcome.RetailCells);
Assert.Contains(FacilityHub | 0x015Eu, outcome.RetailCells);
AssertSameCells(outcome.RetailCells, outcome.CollisionCells);
}
// ---------------------------------------------------------------
// Fixture 2 — Cathedral ramp Setup 0x020009A2 (landblock 0xF418)
// ---------------------------------------------------------------
private static (uint ParentCellId, Stab Stab)? FindStaticParent(
DatCollection dats, uint landblockId, uint staticId, uint lowStart, uint lowEnd)
{
for (uint low = lowStart; low <= lowEnd; low++)
{
uint id = landblockId | low;
DatEnvCell? cell = dats.Get<DatEnvCell>(id);
if (cell?.StaticObjects is null) continue;
foreach (Stab stab in cell.StaticObjects)
if (stab.Id == staticId)
return (id, stab);
}
return null;
}
[Fact]
public void CathedralRamp_RetailCellArrayMatchesCollisionCells()
{
string? datDir = ResolveDatDir();
if (datDir is null) Assert.Fail(SkipMessage);
using var dats = new DatCollection(datDir, DatAccessType.Read);
const uint Cathedral = 0xF4180000u;
const uint setupId = 0x020009A2u;
// docs/research has no named cell/position for this static; the two
// render buckets it crosses (RetailPViewRenderer's cathedralFlood
// probe: 0xF4180107 / 0xF4180112) are both in the 0x0100-0x01FF
// range — the same convention Facility Hub uses — so the parent scan
// is bounded there first.
(uint ParentCellId, Stab Stab)? found =
FindStaticParent(dats, Cathedral, setupId, 0x0100u, 0x01FFu)
?? FindStaticParent(dats, Cathedral, setupId, 0x0200u, 0x03FFu);
Assert.True(found is not null, "Cathedral ramp 0x020009A2 parent EnvCell not found in 0x0100-0x03FF.");
(uint parentCellId, Stab stab) = found!.Value;
_out.WriteLine(FormattableString.Invariant(
$" resolved cathedral ramp parent cell = 0x{parentCellId:X8}"));
PhysicsEngine engine = BuildIndoorEngine(dats, Cathedral, 0x0100u, 0x03FFu);
var cache = (PhysicsDataCache)engine.DataCache!;
DatSetup setup = Assert.IsType<DatSetup>(dats.Get<DatSetup>(setupId));
IReadOnlyList<MeshRef> meshRefs = SetupMesh.Flatten(setup);
foreach (MeshRef meshRef in meshRefs)
{
DatGfxObj gfxObj = Assert.IsType<DatGfxObj>(dats.Get<DatGfxObj>(meshRef.GfxObjId));
cache.CacheGfxObj(meshRef.GfxObjId, gfxObj);
}
Vector3 worldPos = new(stab.Frame.Origin.X, stab.Frame.Origin.Y, stab.Frame.Origin.Z);
Quaternion worldRot = stab.Frame.Orientation;
const uint entityId = 0x7F000002u;
ComparatorOutcome outcome = RunFixture(
"2: Cathedral ramp Setup 0x020009A2",
engine, cache, entityId, meshRefs, setup,
worldPos, worldRot,
worldOffsetX: 0f, worldOffsetY: 0f,
landblockId: Cathedral,
seedCellId: parentCellId);
AssertSameCells(outcome.RetailCells, outcome.CollisionCells);
}
// ---------------------------------------------------------------
// Fixture 3 — #334 Neftet formation GfxObj 0x010046D8 (landblock 0x8764)
// ---------------------------------------------------------------
[Fact]
public void NeftetFormation_RetailCellArrayMatchesCollisionCells()
{
string? datDir = ResolveDatDir();
if (datDir is null) Assert.Fail(SkipMessage);
using var dats = new DatCollection(datDir, DatAccessType.Read);
const uint NeftetLandblock = 0x87640000u;
const uint NeftetLandblockInfo = 0x8764FFFEu;
const uint FormationGfxObj = 0x010046D8u;
LandBlockInfo? info = dats.Get<LandBlockInfo>(NeftetLandblockInfo);
Assert.NotNull(info);
Stab formation = info!.Objects.First(o => o.Id == FormationGfxObj);
DatGfxObj gfx = Assert.IsType<DatGfxObj>(dats.Get<DatGfxObj>(FormationGfxObj));
PhysicsEngine engine = BuildOutdoorEngine(NeftetLandblock);
var cache = (PhysicsDataCache)engine.DataCache!;
cache.CacheGfxObj(FormationGfxObj, gfx);
var meshRefs = new List<MeshRef> { new(FormationGfxObj, Matrix4x4.Identity) };
Vector3 worldPos = formation.Frame.Origin;
Quaternion worldRot = formation.Frame.Orientation;
const uint entityId = 0x7F000003u;
ComparatorOutcome outcome = RunFixture(
"3: #334 Neftet formation GfxObj 0x010046D8",
engine, cache, entityId, meshRefs, setupFallback: null,
worldPos, worldRot,
worldOffsetX: 0f, worldOffsetY: 0f,
landblockId: NeftetLandblock,
seedCellId: 0u);
AssertSameCells(outcome.RetailCells, outcome.CollisionCells);
}
// ---------------------------------------------------------------
// Fixture 4 — one outdoor static crossing a landblock edge. Neither
// LandblockPhysicsPublisherTests nor WalkProductionWorldDataTests carries
// a real installed-DAT edge-crossing static (their outdoor fixtures are
// synthetic cell-id lists, no DAT resolution) — this scans a real
// landblock's Objects for a GfxObj-class stab whose cached authored
// vertex-array BOX (CGfxObj::gfx_bound_box) overlaps its own landblock's
// 0/192 m boundary. The box, not the root bounding sphere, is what
// governs here: CLandCell::add_all_outside_cells divides the BOX by
// square_length (#334) — the sphere is typically much looser (a first
// pass keyed on BoundingSphere.Radius picked the #334 Neftet formation
// itself, whose 69 m sphere radius crosses 0 m at its 63.78 m origin but
// whose real 96 m box does not — see NeftetFormation_RetailCellArrayMatchesCollisionCells,
// which already covers that object; this scan explicitly excludes it so
// fixture 4 stays independent of fixture 3).
// ---------------------------------------------------------------
private static (uint Id, Stab Stab)? FindEdgeCrossingGfxObjStatic(
DatCollection dats, PhysicsDataCache scanCache, uint landblockInfoId, uint excludeGfxObjId)
{
LandBlockInfo? info = dats.Get<LandBlockInfo>(landblockInfoId);
if (info is null) return null;
foreach (Stab stab in info.Objects)
{
if ((stab.Id & 0xFF000000u) != 0x01000000u) continue; // GfxObj-class only.
if (stab.Id == excludeGfxObjId) continue;
DatGfxObj? gfx = dats.Get<DatGfxObj>(stab.Id);
if (gfx is null) continue;
scanCache.CacheGfxObj(stab.Id, gfx);
GfxObjPhysics? physics = scanCache.GetGfxObj(stab.Id);
if (physics?.BSP?.Root is null) continue; // must take the BSP route.
if (physics.VisualBounds is not { } box) continue;
float x = stab.Frame.Origin.X;
float y = stab.Frame.Origin.Y;
bool crosses = x + box.Min.X < 0f || x + box.Max.X > 192f
|| y + box.Min.Y < 0f || y + box.Max.Y > 192f;
if (crosses)
return (stab.Id, stab);
}
return null;
}
[Fact]
public void OutdoorLandblockEdgeCrosser_RetailCellArrayMatchesCollisionCells()
{
string? datDir = ResolveDatDir();
if (datDir is null) Assert.Fail(SkipMessage);
using var dats = new DatCollection(datDir, DatAccessType.Read);
const uint ArwicLandblock = 0xC6A90000u;
const uint ArwicLandblockInfo = 0xC6A9FFFEu;
const uint NeftetLandblock = 0x87640000u;
const uint NeftetLandblockInfo = 0x8764FFFEu;
const uint NeftetFormationGfxObj = 0x010046D8u; // fixture 3's own object — excluded here.
var scanCache = new PhysicsDataCache();
(uint Id, Stab Stab)? candidate =
FindEdgeCrossingGfxObjStatic(dats, scanCache, ArwicLandblockInfo, NeftetFormationGfxObj);
uint landblockId = ArwicLandblock;
if (candidate is null)
{
candidate = FindEdgeCrossingGfxObjStatic(
dats, scanCache, NeftetLandblockInfo, NeftetFormationGfxObj);
landblockId = NeftetLandblock;
}
Assert.True(
candidate is not null,
"No BSP-bearing edge-crossing GfxObj static found scanning Arwic/Neftet Objects.");
(uint gfxObjId, Stab stab) = candidate!.Value;
_out.WriteLine(FormattableString.Invariant(
$" resolved edge crosser: landblock=0x{landblockId:X8} gfxObj=0x{gfxObjId:X8} origin=({stab.Frame.Origin.X:F2},{stab.Frame.Origin.Y:F2})"));
DatGfxObj gfx = Assert.IsType<DatGfxObj>(dats.Get<DatGfxObj>(gfxObjId));
PhysicsEngine engine = BuildOutdoorEngine(landblockId);
var cache = (PhysicsDataCache)engine.DataCache!;
cache.CacheGfxObj(gfxObjId, gfx);
var meshRefs = new List<MeshRef> { new(gfxObjId, Matrix4x4.Identity) };
Vector3 worldPos = stab.Frame.Origin;
Quaternion worldRot = stab.Frame.Orientation;
const uint entityId = 0x7F000004u;
ComparatorOutcome outcome = RunFixture(
"4: outdoor landblock-edge crosser",
engine, cache, entityId, meshRefs, setupFallback: null,
worldPos, worldRot,
worldOffsetX: 0f, worldOffsetY: 0f,
landblockId: landblockId,
seedCellId: 0u);
IEnumerable<uint> foreignLandblockCells = outcome.RetailCells
.Where(id => (id & 0xFFFF0000u) != landblockId);
_out.WriteLine(
$" cells in a NEIGHBOR landblock (edge-crossing evidence): "
+ $"[{string.Join(",", foreignLandblockCells.Select(id => FormattableString.Invariant($"0x{id:X8}")))}]");
AssertSameCells(outcome.RetailCells, outcome.CollisionCells);
}
// ---------------------------------------------------------------
// Fixture 5 — multi-part Setup with parts crossing different cells.
// CORRECTED against the actual installed DAT (not the plan packet's
// assumption): Facility Hub stair Setup 0x02000623 (fixture 1) resolves
// to exactly ONE visual part (SetupMesh.Flatten count = 1) whose own
// wide bounding box spans all 7 crossed cells — it is a wide SINGLE-part
// static, not a multi-part one. Cathedral ramp Setup 0x020009A2
// (fixture 2) genuinely IS the multi-part case: parts=7 (the code
// comment at WbDrawDispatcher.WalkClassify.cs:599 names them —
// 0x01001FE8 + six 0x01001FE6 slabs). retail's per-cell part entries for
// fixture 2 show all 7 parts in all 3 crossed cells (0xF4180112,
// 0xF4180113, 0xF4180009) — retail's CPartArray::AddPartsShadow registers
// EVERY part of the entity's part array into EVERY cell of the entity's
// (union) CELLARRAY, not a per-part cell subset; "parts in different
// cells" is therefore the ENTITY's CELLARRAY spanning multiple cells
// (which fixture 2 does, 3 of them), not individual parts each confined
// to their own cell. No installed-DAT fixture with genuinely
// per-part-confined membership was found among the existing test files
// this chunk was pointed at (Issue177StairDescentCameraFloodTests,
// LandblockPhysicsPublisherTests, Issue334NeftetFormationCellMembershipTests,
// WalkProductionWorldDataTests) — fixture 2's own result is the evidence
// for this row.
// ---------------------------------------------------------------
}