using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
using AcDream.Core.Physics;
using AcDream.Core.Tests.Conformance;
using AcDream.Core.World;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.Core.Tests.Physics;
///
/// #337 offline replay (TEMPORARY — strip with the physics-probe family).
///
///
/// Reconstructs, from the installed DAT alone, the exact world placement of
/// every landblock-static collision owner in Neftet landblock 0x8766, and
/// asks the one question [geom] cannot answer: the [geom] probe
/// compares the physics-BSP vertex cloud against the visual mesh box in the
/// object's OWN LOCAL FRAME, so verdict=coincident proves only that the
/// two agree on SHAPE. It says nothing about WHERE the object sits in the
/// world. This test places the geometry in the world and measures.
///
///
///
/// Live measurement being replayed (337-support.log / 334-fix-gate.log):
/// the player wedges at (134.314, 55.716, 50.011) in cell 0x8766002B, and
/// owner 0xC8766002 / gfx 0x010046DE was tested 11,014 times there with
/// 0 adjusted, 0 collided, 0 slid — while its two neighbours 0xC8766003 and
/// 0xC8766009 adjusted 767 and 1,135 times against the same mover.
///
///
/// Dat-data dependent; SKIPs cleanly without ACDREAM_DAT_DIR.
///
public sealed class Issue337NeftetRockGeometryInspectionTests
{
private const uint Landblock = 0x8766_0000u;
private const uint GfxObjMask = 0x01000000u;
private const uint SetupMask = 0x02000000u;
private const uint TypeMask = 0xFF000000u;
/// The wedge point, landblock-local, from the live capture.
private static readonly Vector3 Wedge = new(134.313934f, 55.716248f, 50.010666f);
private readonly ITestOutputHelper _out;
public Issue337NeftetRockGeometryInspectionTests(ITestOutputHelper o) => _out = o;
private sealed record Placed(
uint EntityId,
uint DatId,
uint GfxObjId,
Vector3 Position,
Quaternion Rotation,
List WorldPolygons,
List LocalPolygons,
FlatPhysicsBsp Bsp,
Vector3 WorldMin,
Vector3 WorldMax,
int BspNodes,
int BspPolys);
[Fact]
public void DumpNeftetLandblockStaticsAndProbeTheWedgePoint()
{
System.Globalization.CultureInfo.CurrentCulture =
System.Globalization.CultureInfo.InvariantCulture;
string? datDir = ConformanceDats.ResolveDatDir();
if (datDir is null)
{
_out.WriteLine("SKIP: installed retail DAT directory is unavailable.");
return;
}
using var dats = new DatCollection(datDir, DatAccessType.Read);
var info = dats.Get((Landblock & 0xFFFF0000u) | 0xFFFEu);
Assert.NotNull(info);
var sb = new StringBuilder();
sb.AppendLine(Inv(
$"landblock 0x{Landblock:X8}: Objects={info!.Objects.Count} Buildings={info.Buildings.Count}"));
uint lbX = (Landblock >> 24) & 0xFFu;
uint lbY = (Landblock >> 16) & 0xFFu;
uint counter = 0;
var cache = new PhysicsDataCache();
var placed = new List();
foreach (var stab in info.Objects)
{
if (!IsSupported(stab.Id)) continue;
uint entityId = LandblockStaticEntityIdAllocatorAllocate(lbX, lbY, ref counter);
placed.AddRange(Place(dats, cache, entityId, stab.Id,
stab.Frame.Origin, stab.Frame.Orientation));
}
foreach (var building in info.Buildings)
{
if (!IsSupported(building.ModelId)) continue;
uint entityId = LandblockStaticEntityIdAllocatorAllocate(lbX, lbY, ref counter);
placed.AddRange(Place(dats, cache, entityId, building.ModelId,
building.Frame.Origin, building.Frame.Orientation));
}
sb.AppendLine($"placed collision parts: {placed.Count}");
sb.AppendLine();
sb.AppendLine("--- every part: world box, and the wedge point in ITS OWN local frame ---");
foreach (var p in placed.OrderBy(p => p.EntityId).ThenBy(p => p.GfxObjId))
{
Vector3 local = Vector3.Transform(
Wedge - p.Position, Quaternion.Inverse(p.Rotation));
bool inWorldBox = Inside(p.WorldMin, p.WorldMax, Wedge);
sb.AppendLine(Inv(
$" ent=0x{p.EntityId:X8} dat=0x{p.DatId:X8} gfx=0x{p.GfxObjId:X8} " +
$"pos=({p.Position.X:F3},{p.Position.Y:F3},{p.Position.Z:F3}) " +
$"rot=({p.Rotation.W:F4},{p.Rotation.X:F4},{p.Rotation.Y:F4},{p.Rotation.Z:F4}) " +
$"yawDeg={YawDegrees(p.Rotation):F2} " +
$"nodes={p.BspNodes} polys={p.BspPolys} " +
$"worldMin=({p.WorldMin.X:F2},{p.WorldMin.Y:F2},{p.WorldMin.Z:F2}) " +
$"worldMax=({p.WorldMax.X:F2},{p.WorldMax.Y:F2},{p.WorldMax.Z:F2}) " +
$"wedgeLocal=({local.X:F2},{local.Y:F2},{local.Z:F2}) " +
$"wedgeInWorldBox={inWorldBox}"));
}
sb.AppendLine();
sb.AppendLine(Inv(
$"--- vertical column through the wedge XY ({Wedge.X:F3},{Wedge.Y:F3}) ---"));
sb.AppendLine("(every physics polygon whose WORLD XY projection contains that point,");
sb.AppendLine(" with the Z of the polygon's plane at that XY — this is the surface");
sb.AppendLine(" a falling body would land on and the ceiling it would be under)");
var hits = new List<(uint Ent, uint Gfx, float Z, float Nz, int PolyIndex)>();
foreach (var p in placed)
{
for (int i = 0; i < p.WorldPolygons.Count; i++)
{
Vector3[] poly = p.WorldPolygons[i];
if (!XyContains(poly, Wedge.X, Wedge.Y)) continue;
if (!TryPlaneZ(poly, Wedge.X, Wedge.Y, out float z, out float nz)) continue;
hits.Add((p.EntityId, p.GfxObjId, z, nz, i));
}
}
foreach (var h in hits.OrderByDescending(h => h.Z))
{
sb.AppendLine(Inv(
$" z={h.Z,9:F3} normalZ={h.Nz,7:F4} ent=0x{h.Ent:X8} gfx=0x{h.Gfx:X8} poly#{h.PolyIndex}"));
}
if (hits.Count == 0)
sb.AppendLine(" (no physics polygon covers that XY at all)");
sb.AppendLine();
sb.AppendLine(Inv(
$"player sphere centre is at z={Wedge.Z:F3}"));
var above = hits.Where(h => h.Z > Wedge.Z).OrderBy(h => h.Z).ToList();
var below = hits.Where(h => h.Z <= Wedge.Z).OrderByDescending(h => h.Z).ToList();
sb.AppendLine(Inv(
$" nearest surface BELOW: {(below.Count == 0 ? "none" : $"z={below[0].Z:F3} ent=0x{below[0].Ent:X8} gfx=0x{below[0].Gfx:X8} (gap {Wedge.Z - below[0].Z:F3} m)")}"));
sb.AppendLine(Inv(
$" nearest surface ABOVE: {(above.Count == 0 ? "none" : $"z={above[0].Z:F3} ent=0x{above[0].Ent:X8} gfx=0x{above[0].Gfx:X8} (gap {above[0].Z - Wedge.Z:F3} m)")}"));
sb.AppendLine();
sb.AppendLine("--- nearest physics polygon to the wedge point, per owner ---");
foreach (var p in placed.OrderBy(p => p.EntityId))
{
float best = float.MaxValue;
int bestIndex = -1;
for (int i = 0; i < p.WorldPolygons.Count; i++)
{
float d = DistanceToPolygon(p.WorldPolygons[i], Wedge);
if (d < best) { best = d; bestIndex = i; }
}
sb.AppendLine(Inv(
$" ent=0x{p.EntityId:X8} gfx=0x{p.GfxObjId:X8} nearestPolyDist={best:F3} m (poly#{bestIndex})"));
}
// ------------------------------------------------------------------
// REFEREE. Two independent answers to the SAME question, per owner:
// (a) the production BSP walk, FlatBspQuery.SphereIntersectsPoly —
// the Path-5 static-overlap primitive the resolver actually runs,
// node bounding-sphere early-outs and all;
// (b) brute force over every polygon the tree indexes.
// They must agree. Where (b) says "a polygon is inside the sphere" and
// (a) says "no", the geometry is present and the TRAVERSAL cannot reach
// it — a bounded pocket, which is exactly the reported symptom. Where
// both say no, the mesh genuinely is not there and the owner is
// innocent.
//
// Sample points are taken ON the mesh (vertices + centroids, nudged
// both ways along the polygon normal) so the referee covers the whole
// surface rather than an arbitrary lattice, and any disagreement
// localises to a named polygon.
// ------------------------------------------------------------------
const float SphereRadius = 0.48f; // the live mover's radius
sb.AppendLine();
sb.AppendLine(Inv(
$"--- referee: BSP walk vs brute force, sphere r={SphereRadius:F3} ---"));
foreach (var p in placed.OrderBy(p => p.EntityId))
{
int samples = 0, mismatch = 0, bothHit = 0;
var mmMin = new Vector3(float.PositiveInfinity);
var mmMax = new Vector3(float.NegativeInfinity);
var firstMismatches = new List();
foreach (Vector3 probe in SurfaceProbes(p.LocalPolygons, SphereRadius))
{
samples++;
bool walk = FlatBspQuery.SphereIntersectsPoly(
p.Bsp, probe, SphereRadius, out _, out _);
bool brute = BruteHit(p.LocalPolygons, probe, SphereRadius);
if (walk == brute) { if (walk) bothHit++; continue; }
mismatch++;
Vector3 world = p.Position + Vector3.Transform(probe, p.Rotation);
mmMin = Vector3.Min(mmMin, world);
mmMax = Vector3.Max(mmMax, world);
if (firstMismatches.Count < 8)
{
firstMismatches.Add(Inv(
$" local=({probe.X:F2},{probe.Y:F2},{probe.Z:F2}) " +
$"world=({world.X:F2},{world.Y:F2},{world.Z:F2}) " +
$"walk={walk} brute={brute}"));
}
}
sb.AppendLine(Inv(
$" ent=0x{p.EntityId:X8} gfx=0x{p.GfxObjId:X8} polys={p.BspPolys} " +
$"samples={samples} bothHit={bothHit} MISMATCH={mismatch}"));
if (mismatch > 0)
{
sb.AppendLine(Inv(
$" mismatch world box=({mmMin.X:F2},{mmMin.Y:F2},{mmMin.Z:F2})..({mmMax.X:F2},{mmMax.Y:F2},{mmMax.Z:F2})"));
foreach (string line in firstMismatches) sb.AppendLine(line);
}
}
// ------------------------------------------------------------------
// The exact live query, replayed. The wedge point, plus every point on
// a 1 m lattice through the plateau, both answers side by side.
// ------------------------------------------------------------------
sb.AppendLine();
sb.AppendLine("--- the live wedge query, replayed per owner ---");
foreach (var p in placed.OrderBy(p => p.EntityId))
{
Vector3 local = Vector3.Transform(
Wedge - p.Position, Quaternion.Inverse(p.Rotation));
bool walk = FlatBspQuery.SphereIntersectsPoly(
p.Bsp, local, SphereRadius, out ushort id, out _);
bool brute = BruteHit(p.LocalPolygons, local, SphereRadius);
float nearest = float.MaxValue;
foreach (var poly in p.LocalPolygons)
nearest = MathF.Min(nearest, DistanceToPolygon(poly, local));
sb.AppendLine(Inv(
$" ent=0x{p.EntityId:X8} gfx=0x{p.GfxObjId:X8} " +
$"walkHit={walk} (poly {id}) bruteHit={brute} nearestPoly={nearest:F3} m"));
}
sb.AppendLine();
sb.AppendLine("--- 1 m lattice over the plateau (x 120..150, y 40..70, z 44..56) ---");
foreach (var p in placed.OrderBy(p => p.EntityId))
{
int lat = 0, latWalk = 0, latBrute = 0, latMismatch = 0;
var missMin = new Vector3(float.PositiveInfinity);
var missMax = new Vector3(float.NegativeInfinity);
for (float x = 120f; x <= 150f; x += 1f)
for (float y = 40f; y <= 70f; y += 1f)
for (float z = 44f; z <= 56f; z += 1f)
{
var world = new Vector3(x, y, z);
Vector3 local = Vector3.Transform(
world - p.Position, Quaternion.Inverse(p.Rotation));
lat++;
bool walk = FlatBspQuery.SphereIntersectsPoly(
p.Bsp, local, SphereRadius, out _, out _);
bool brute = BruteHit(p.LocalPolygons, local, SphereRadius);
if (walk) latWalk++;
if (brute) latBrute++;
if (walk == brute) continue;
latMismatch++;
missMin = Vector3.Min(missMin, world);
missMax = Vector3.Max(missMax, world);
}
sb.AppendLine(Inv(
$" ent=0x{p.EntityId:X8} gfx=0x{p.GfxObjId:X8} points={lat} " +
$"walkHits={latWalk} bruteHits={latBrute} MISMATCH={latMismatch}" +
(latMismatch > 0
? $" box=({missMin.X:F1},{missMin.Y:F1},{missMin.Z:F1})..({missMax.X:F1},{missMax.Y:F1},{missMax.Z:F1})"
: string.Empty)));
}
// ------------------------------------------------------------------
// The recorded live track, replayed. Each row is a position the client
// actually held, taken from 337-support.log; the Y values on the climb
// rows carry the +576 m live-centre offset that log was written in
// (live centre 0x8763, three landblocks south of 0x8766), removed here.
// For each, the walkable surface directly above/below it.
// ------------------------------------------------------------------
sb.AppendLine();
sb.AppendLine("--- recorded live positions vs the surface at their own XY ---");
(string Tag, Vector3 P, float CpNz)[] track =
[
("climb cpNz=0.7401", new Vector3(185.53f, 617.52f - 576f, 3.839f), 0.7401f),
("climb cpNz=0.7078", new Vector3(172.63f, 600.23f - 576f, 17.910f), 0.7078f),
("climb cpNz=0.8640", new Vector3(154.04f, 596.93f - 576f, 32.543f), 0.8640f),
("climb cpNz=0.8216", new Vector3(143.69f, 611.83f - 576f, 43.352f), 0.8216f),
("climb cpNz=0.9532", new Vector3(138.38f, 621.53f - 576f, 48.567f), 0.9532f),
("STALL cpNz=0.9805", new Vector3(130.81f, 625.88f - 576f, 51.097f), 0.9805f),
("stand cpNz=0.9532", new Vector3(134.08f, 623.63f - 576f, 50.074f), 0.9532f),
("stand cpNz=0.9532", new Vector3(135.77f, 625.86f - 576f, 49.994f), 0.9532f),
("stand cpNz=1.0000", new Vector3(131.38f, 627.49f - 576f, 49.908f), 1.0000f),
("STALL cpNz=0.9805", new Vector3(131.68f, 627.58f - 576f, 51.096f), 0.9805f),
("WEDGE support=none", Wedge, float.NaN),
];
foreach (var row in track)
{
var col = Column(placed, row.P.X, row.P.Y);
var up = col.Where(c => c.Nz > 0f).OrderByDescending(c => c.Z).ToList();
var upBelowHead = up.Where(c => c.Z <= row.P.Z + 0.05f).ToList();
string surf = up.Count == 0
? "NO UPWARD SURFACE IN COLUMN"
: Inv($"topUp z={up[0].Z:F3} (ent=0x{up[0].Ent:X8}) delta={row.P.Z - up[0].Z:+0.000;-0.000}");
sb.AppendLine(Inv(
$" {row.Tag} p=({row.P.X:F2},{row.P.Y:F2},{row.P.Z:F3}) " +
$"colPolys={col.Count} upFacing={up.Count} atOrBelowFeet={upBelowHead.Count} {surf}"));
}
// ------------------------------------------------------------------
// HOLE MAP. Over the plateau, at 0.5 m XY resolution, does ANY
// upward-facing physics polygon exist above z=35? A bounded region
// with none is a hole in the rock's walking surface — a body that
// reaches it stops being supported, sinks, and a corpse dropped on it
// falls through, while a jump that clears it lands fine on the far
// side. That is the reported symptom exactly.
// ------------------------------------------------------------------
sb.AppendLine();
sb.AppendLine("--- hole map: upward-facing physics coverage over the plateau ---");
sb.AppendLine(" (0.5 m XY grid, x 118..152, y 38..72; '.'=covered above z=35, "
+ "'#'=NO upward surface, '*'=the wedge XY)");
int covered = 0, holes = 0;
var holeMin = new Vector2(float.PositiveInfinity);
var holeMax = new Vector2(float.NegativeInfinity);
for (float y = 72f; y >= 38f; y -= 0.5f)
{
var line = new StringBuilder(Inv($" y={y,6:F1} "));
for (float x = 118f; x <= 152f; x += 0.5f)
{
var col = Column(placed, x, y);
bool hasUp = col.Any(c => c.Nz > 0f && c.Z > 35f);
bool isWedge = MathF.Abs(x - Wedge.X) < 0.25f
&& MathF.Abs(y - Wedge.Y) < 0.25f;
if (hasUp) covered++;
else
{
holes++;
holeMin = Vector2.Min(holeMin, new Vector2(x, y));
holeMax = Vector2.Max(holeMax, new Vector2(x, y));
}
line.Append(isWedge ? '*' : hasUp ? '.' : '#');
}
sb.AppendLine(line.ToString());
}
sb.AppendLine(Inv(
$" covered={covered} holes={holes}" +
(holes > 0
? $" holeBox=({holeMin.X:F1},{holeMin.Y:F1})..({holeMax.X:F1},{holeMax.Y:F1})"
: string.Empty)));
string outPath = Path.Combine(Path.GetTempPath(), "issue337-neftet-geometry.txt");
File.WriteAllText(outPath, sb.ToString());
_out.WriteLine(sb.ToString());
_out.WriteLine($"(also written to {outPath})");
}
// ---------------------------------------------------------------- helpers
///
/// #337 descent replay. The live capture shows the player standing on the
/// plateau at feet z=51.096 (surface ~51.08), jumping to escape a
/// horizontal stall, and then falling STRAIGHT THROUGH that same surface —
/// four consecutive resolves accept a full-length downward step with no
/// contact plane, ending 1.1 m inside solid rock. This replays those exact
/// steps against the exact mesh, with a control step from the climb where
/// the client DID land correctly on the same mesh.
///
[Fact]
public void ReplayTheDescentThatFellThroughTheSurface()
{
System.Globalization.CultureInfo.CurrentCulture =
System.Globalization.CultureInfo.InvariantCulture;
string? datDir = ConformanceDats.ResolveDatDir();
if (datDir is null)
{
_out.WriteLine("SKIP: installed retail DAT directory is unavailable.");
return;
}
using var dats = new DatCollection(datDir, DatAccessType.Read);
var info = dats.Get((Landblock & 0xFFFF0000u) | 0xFFFEu);
Assert.NotNull(info);
uint lbX = (Landblock >> 24) & 0xFFu;
uint lbY = (Landblock >> 16) & 0xFFu;
uint counter = 0;
var cache = new PhysicsDataCache();
var placed = new List();
foreach (var stab in info!.Objects)
{
if (!IsSupported(stab.Id)) continue;
uint entityId = LandblockStaticEntityIdAllocatorAllocate(lbX, lbY, ref counter);
placed.AddRange(Place(dats, cache, entityId, stab.Id,
stab.Frame.Origin, stab.Frame.Orientation));
}
var sb = new StringBuilder();
// The player's verbatim dat sphere list — the same rows
// PhysicsEngine hands to SpherePath.InitPath (TS-46).
var playerSetup = dats.Get(0x0200_0001u);
Assert.NotNull(playerSetup);
FlatSetupCollision playerFlat =
FlatCollisionAssetBuilder.FlattenSetup(playerSetup!);
sb.AppendLine("--- player setup 0x02000001 collision volume ---");
sb.AppendLine(Inv(
$" height={playerFlat.Height:F3} radius={playerFlat.Radius:F3} " +
$"stepUp={playerFlat.StepUpHeight:F3} stepDown={playerFlat.StepDownHeight:F3} " +
$"spheres={playerFlat.Spheres.Length} cylinders={playerFlat.Cylinders.Length}"));
for (int i = 0; i < playerFlat.Spheres.Length; i++)
{
var s = playerFlat.Spheres[i];
sb.AppendLine(Inv(
$" sphere[{i}] origin=({s.Origin.X:F3},{s.Origin.Y:F3},{s.Origin.Z:F3}) r={s.Radius:F3}"));
}
for (int i = 0; i < playerFlat.Cylinders.Length; i++)
{
var c = playerFlat.Cylinders[i];
sb.AppendLine(Inv(
$" cylsphere[{i}] origin=({c.Origin.X:F3},{c.Origin.Y:F3},{c.Origin.Z:F3}) r={c.Radius:F3} h={c.Height:F3}"));
}
// Sphere rows actually used. The live reach probe recorded
// sphereR=0.480, so this asserts the replay is using the same volume
// the client used rather than a lookalike.
var rows = playerFlat.Spheres.Length > 0
? playerFlat.Spheres.ToArray()
: playerFlat.Cylinders
.Select(c => new FlatCollisionSphere(c.Origin, c.Radius))
.ToArray();
sb.AppendLine();
sb.AppendLine("--- surface column at the fall XY ---");
foreach ((string tag, float x, float y) in new[]
{
("stand-before-jump", 131.683f, 627.581f - 576f),
("fall/landing ", 131.13f, 627.61f - 576f),
("final wedge ", 131.38f, 627.49f - 576f),
("login wedge ", Wedge.X, Wedge.Y),
})
{
var col = Column(placed, x, y).OrderByDescending(c => c.Z).ToList();
sb.AppendLine(Inv($" {tag} xy=({x:F3},{y:F3}) polys={col.Count}"));
foreach (var c in col)
{
sb.AppendLine(Inv(
$" z={c.Z,9:F3} nz={c.Nz,8:F4} ent=0x{c.Ent:X8} gfx=0x{c.Gfx:X8}"));
}
}
// The steps, feet-space, straight from 337-support.log (Y carries the
// +576 m live-centre offset that log was written in).
(string Tag, Vector3 From, Vector3 To, bool ClientHit)[] steps =
[
// Control: the climb. The client accepted these with a contact
// plane from StepSphereDown, i.e. it DID land on this mesh.
("CONTROL climb step", new Vector3(143.69f, 611.83f - 576f, 43.352f),
new Vector3(143.69f, 611.83f - 576f, 42.852f), true),
("CONTROL stand step", new Vector3(135.77f, 625.86f - 576f, 49.994f),
new Vector3(135.77f, 625.86f - 576f, 49.494f), true),
// The failing descent, verbatim, four consecutive resolves.
("FALL 1 51.389->51.269", new Vector3(131.13f, 627.61f - 576f, 51.389f),
new Vector3(131.13f, 627.61f - 576f, 51.269f), false),
("FALL 2 51.269->50.961", new Vector3(131.13f, 627.61f - 576f, 51.269f),
new Vector3(131.13f, 627.61f - 576f, 50.961f), false),
("FALL 3 50.961->50.670", new Vector3(131.13f, 627.61f - 576f, 50.961f),
new Vector3(131.13f, 627.61f - 576f, 50.670f), false),
("FALL 4 50.670->50.335", new Vector3(131.13f, 627.61f - 576f, 50.670f),
new Vector3(131.13f, 627.61f - 576f, 50.335f), false),
("FALL 5 50.335->50.021", new Vector3(131.13f, 627.61f - 576f, 50.335f),
new Vector3(131.13f, 627.61f - 576f, 50.021f), false),
("FALL 6 50.021->49.656 (client BLOCKED here)",
new Vector3(131.13f, 627.61f - 576f, 50.021f),
new Vector3(131.13f, 627.61f - 576f, 49.656f), false),
];
sb.AppendLine();
sb.AppendLine("--- swept-sphere descent replay against every owner ---");
foreach (var step in steps)
{
sb.AppendLine(Inv(
$" {step.Tag} (client recorded a contact plane: {step.ClientHit})"));
foreach (var p in placed.OrderBy(p => p.EntityId))
{
var invRot = Quaternion.Inverse(p.Rotation);
for (int i = 0; i < rows.Length; i++)
{
Vector3 worldFrom = step.From + rows[i].Origin;
Vector3 worldTo = step.To + rows[i].Origin;
Vector3 localTo = Vector3.Transform(worldTo - p.Position, invRot);
Vector3 localFrom = Vector3.Transform(worldFrom - p.Position, invRot);
Vector3 localMove = localTo - localFrom;
bool swept = FlatBspQuery.SphereIntersectsPolyWithTime(
p.Bsp, localTo, rows[i].Radius, localMove,
out ushort sweptId, out _, out float sweptTime);
bool stat = FlatBspQuery.SphereIntersectsPoly(
p.Bsp, localTo, rows[i].Radius, out ushort statId, out _);
float nearest = float.MaxValue;
foreach (var poly in p.LocalPolygons)
nearest = MathF.Min(nearest, DistanceToPolygon(poly, localTo));
if (!swept && !stat && nearest > 3f) continue; // far away, silent
sb.AppendLine(Inv(
$" ent=0x{p.EntityId:X8} sphere[{i}] r={rows[i].Radius:F3} " +
$"swept={swept}(poly {sweptId} t={sweptTime:F4}) " +
$"static={stat}(poly {statId}) nearestPoly={nearest:F3} m"));
}
}
}
string outPath = Path.Combine(Path.GetTempPath(), "issue337-descent-replay.txt");
File.WriteAllText(outPath, sb.ToString());
_out.WriteLine(sb.ToString());
_out.WriteLine($"(also written to {outPath})");
}
///
/// #337 REPRODUCER. At the position the client fell through the plateau,
/// the BSP query returns a hit — the geometry is present, correctly
/// placed, and reachable by the production traversal. This asserts that
/// load-bearing fact, which must hold both before and after any fix; if it
/// ever stops holding, the diagnosis in
/// docs/research/2026-08-06-337-neftet-wedge-mechanism.md is void.
///
[Fact]
public void TheBspQueryReturnsAHitAtThePositionTheClientFellThrough()
{
string? datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) return; // CI without dats — the sibling dumps skip too.
using var dats = new DatCollection(datDir, DatAccessType.Read);
var info = dats.Get((Landblock & 0xFFFF0000u) | 0xFFFEu);
Assert.NotNull(info);
Placed rock = ResolveOwner(dats, info!, 0xC876_6009u);
// Feet position and per-sphere layout, verbatim from the live capture
// (337-support.log t=303221015..303221125) and Setup 0x02000001.
var feet = new Vector3(131.13f, 627.61f - 576f, 50.961f);
var footSphereOffset = new Vector3(0f, 0f, 0.475f);
const float SphereRadius = 0.48f;
Vector3 local = Vector3.Transform(
feet + footSphereOffset - rock.Position,
Quaternion.Inverse(rock.Rotation));
bool hit = FlatBspQuery.SphereIntersectsPoly(
rock.Bsp, local, SphereRadius, out ushort polygonId, out _);
Assert.True(
hit,
"The rock's physics BSP must report the plateau surface under the "
+ "mover at the position the live client fell through it. If this "
+ "fails, the defect is in the geometry or the traversal after all.");
Assert.NotEqual(0, polygonId);
}
///
/// #337 EVIDENCE — the installed-DAT measurement that condemned the
/// query-site broadphase, pinned so the diagnosis stays checkable.
///
///
/// The deleted per-object filter in
/// Transition.FindObjCollisionsInCell measured the mover's distance
/// to the shadow entry's Position — the part ORIGIN — and compared
/// it against obj.Radius, which is the physics-BSP ROOT BOUNDING
/// SPHERE's radius. For this rock those two points are 23.6 m apart, so a
/// mover standing on its plateau is inside the bounding sphere by ~20 m of
/// margin and still failed the test. Same defect AP-156 fixed in the flood
/// and #334 fixed in the registration extent walk, left in place at the
/// query site — filed as #333.
///
///
///
/// Retail has no such filter. CPartArray::FindObjCollisions
/// @0x00518180 is a bare loop over parts and
/// CPhysicsPart::find_obj_collisions @0x0050d8d0 does two null
/// checks and calls CGfxObj::find_obj_collisions @0x00534700 —
/// verified instruction-by-instruction against the PDB-paired
/// v11.4186 binary. Neither contains a compare or any float math. The
/// only spatial rejection retail performs is the BSP node bounding-sphere
/// test inside the walk, which is correctly centred. #333 therefore
/// deleted the filter outright rather than re-centring it.
///
///
///
/// This test asserts the DATA, not the production predicate — the
/// production gate is
/// Issue333BroadphaseReachFilterTests.OffCentreBspFloorStopsAFallingMover,
/// which drives ResolveWithTransition end-to-end. Both halves must
/// hold for the diagnosis to be the one recorded: the origin-measured
/// distance OUTSIDE the old budget, the centre-measured distance
/// comfortably INSIDE the same radius. If a future DAT or transform change
/// makes either false, the recorded mechanism no longer describes this
/// object and the research note needs revisiting.
///
///
[Fact]
public void TheOldBroadphaseMeasuredToTheOriginAndSoRejectedGeometryItStoodOn()
{
string? datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) return;
using var dats = new DatCollection(datDir, DatAccessType.Read);
var info = dats.Get((Landblock & 0xFFFF0000u) | 0xFFFEu);
Assert.NotNull(info);
Placed rock = ResolveOwner(dats, info!, 0xC876_6009u);
var feet = new Vector3(131.13f, 627.61f - 576f, 50.961f);
Vector3 currPos = feet + new Vector3(0f, 0f, 0.475f);
const float SphereRadius = 0.48f;
const float Movement = 0.308f; // the live step length
var root = rock.Bsp.Nodes[rock.Bsp.RootIndex].BoundingSphere;
float ownerRadius = root.Radius;
// Verbatim from the deleted predicate.
float distToOrigin = (currPos - rock.Position).Length();
float maxReach = SphereRadius + ownerRadius + Movement + 2f;
Assert.True(
distToOrigin > maxReach,
$"the old filter is supposed to have REJECTED this candidate: "
+ $"distToOrigin={distToOrigin:F3} vs maxReach={maxReach:F3}.");
// What retail's BSP node test measures instead.
Vector3 centre = rock.Position
+ Vector3.Transform(root.Origin, rock.Rotation);
float distToCentre = (currPos - centre).Length();
Assert.True(
distToCentre <= ownerRadius,
$"the mover must be inside the root bounding SPHERE it was standing "
+ $"on: distToCentre={distToCentre:F3} > radius={ownerRadius:F3}.");
}
private static Placed ResolveOwner(
DatCollection dats, LandBlockInfo info, uint wantedEntityId)
{
uint lbX = (Landblock >> 24) & 0xFFu;
uint lbY = (Landblock >> 16) & 0xFFu;
uint counter = 0;
var cache = new PhysicsDataCache();
foreach (var stab in info.Objects)
{
if (!IsSupported(stab.Id)) continue;
uint entityId = LandblockStaticEntityIdAllocatorAllocate(lbX, lbY, ref counter);
if (entityId != wantedEntityId) continue;
var parts = Place(dats, cache, entityId, stab.Id,
stab.Frame.Origin, stab.Frame.Orientation).ToList();
Assert.Single(parts);
return parts[0];
}
throw new InvalidOperationException(
$"landblock static 0x{wantedEntityId:X8} not found.");
}
///
/// Pass-through. Concatenated interpolated fragments cannot bind to
/// , so the test method pins
/// CurrentCulture to invariant instead and this marks the call
/// sites that depend on it.
///
private static string Inv(string s) => s;
private static uint LandblockStaticEntityIdAllocatorAllocate(
uint lbX, uint lbY, ref uint counter)
=> LandblockStaticEntityIdAllocator.Allocate(lbX, lbY, ref counter);
private static bool IsSupported(uint id)
{
uint type = id & TypeMask;
return type == GfxObjMask || type == SetupMask;
}
private static IEnumerable Place(
DatCollection dats,
PhysicsDataCache cache,
uint entityId,
uint datId,
Vector3 origin,
Quaternion orientation)
{
var parts = new List<(uint GfxObjId, Vector3 LocalPos, Quaternion LocalRot)>();
if ((datId & TypeMask) == GfxObjMask)
{
parts.Add((datId, Vector3.Zero, Quaternion.Identity));
}
else
{
var setup = dats.Get(datId);
if (setup is null) yield break;
for (int i = 0; i < setup.Parts.Count; i++)
{
Vector3 lp = Vector3.Zero;
Quaternion lr = Quaternion.Identity;
if (!setup.PlacementFrames.TryGetValue(
DatReaderWriter.Enums.Placement.Resting, out var pf)
&& !setup.PlacementFrames.TryGetValue(
DatReaderWriter.Enums.Placement.Default, out pf))
{
pf = setup.PlacementFrames.Values.FirstOrDefault();
}
if (pf?.Frames is not null && i < pf.Frames.Count)
{
lp = pf.Frames[i].Origin;
lr = pf.Frames[i].Orientation;
}
parts.Add((setup.Parts[i], lp, lr));
}
}
foreach (var part in parts)
{
var gfx = dats.Get(part.GfxObjId);
if (gfx is null) continue;
cache.CacheGfxObj(part.GfxObjId, gfx);
FlatGfxObjCollisionAsset flat =
FlatCollisionAssetBuilder.FlattenGfxObj(gfx);
FlatPhysicsBsp bsp = flat.PhysicsBsp;
if (bsp.RootIndex < 0 || bsp.Nodes.Length == 0) continue;
Vector3 partWorldPos = origin + Vector3.Transform(part.LocalPos, orientation);
Quaternion partWorldRot = orientation * part.LocalRot;
var seen = new HashSet();
var worldPolys = new List();
var localPolys = new List();
var min = new Vector3(float.PositiveInfinity);
var max = new Vector3(float.NegativeInfinity);
foreach (var node in bsp.Nodes)
{
var range = node.PolygonIndexRange;
for (int i = range.Start; i < range.EndExclusive; i++)
{
int pi = bsp.PolygonIndexStream[i];
if ((uint)pi >= (uint)bsp.PolygonTable.Polygons.Length) continue;
if (!seen.Add(pi)) continue;
var poly = bsp.PolygonTable.Polygons[pi];
var vr = poly.VertexRange;
var verts = new Vector3[vr.Count];
var lverts = new Vector3[vr.Count];
for (int v = 0; v < vr.Count; v++)
{
Vector3 local = bsp.PolygonTable.Vertices[vr.Start + v];
Vector3 world = partWorldPos
+ Vector3.Transform(local, partWorldRot);
lverts[v] = local;
verts[v] = world;
min = Vector3.Min(min, world);
max = Vector3.Max(max, world);
}
worldPolys.Add(verts);
localPolys.Add(lverts);
}
}
if (worldPolys.Count == 0) continue;
yield return new Placed(
entityId, datId, part.GfxObjId, partWorldPos, partWorldRot,
worldPolys, localPolys, bsp, min, max,
bsp.Nodes.Length, worldPolys.Count);
}
}
///
/// Points just off the mesh surface, both sides, at every vertex and
/// centroid. A sphere centred here demonstrably contains mesh, so the
/// production walk must report a hit.
///
private static IEnumerable SurfaceProbes(
List polygons, float radius)
{
float nudge = radius * 0.5f;
foreach (Vector3[] poly in polygons)
{
if (poly.Length < 3) continue;
Vector3 n = Vector3.Cross(poly[1] - poly[0], poly[2] - poly[0]);
float len = n.Length();
if (len < 1e-9f) continue;
n /= len;
Vector3 centroid = Vector3.Zero;
foreach (Vector3 v in poly) centroid += v;
centroid /= poly.Length;
yield return centroid + n * nudge;
yield return centroid - n * nudge;
foreach (Vector3 v in poly)
{
// Pull slightly toward the centroid so a vertex probe sits on
// the polygon rather than exactly on its corner.
Vector3 inset = Vector3.Lerp(v, centroid, 0.15f);
yield return inset + n * nudge;
yield return inset - n * nudge;
}
}
}
private static bool BruteHit(List polygons, Vector3 centre, float radius)
{
foreach (Vector3[] poly in polygons)
{
if (DistanceToPolygon(poly, centre) < radius - 1e-4f)
return true;
}
return false;
}
///
/// Every physics polygon whose world XY projection contains
/// (, ), with the Z of its plane
/// at that XY and the Z component of its normal.
///
private static List<(uint Ent, uint Gfx, float Z, float Nz)> Column(
List placed, float x, float y)
{
var result = new List<(uint, uint, float, float)>();
foreach (var p in placed)
{
if (x < p.WorldMin.X || x > p.WorldMax.X
|| y < p.WorldMin.Y || y > p.WorldMax.Y) continue;
foreach (Vector3[] poly in p.WorldPolygons)
{
if (!XyContains(poly, x, y)) continue;
if (!TryPlaneZ(poly, x, y, out float z, out float nz)) continue;
result.Add((p.EntityId, p.GfxObjId, z, nz));
}
}
return result;
}
private static bool Inside(Vector3 min, Vector3 max, Vector3 p)
=> p.X >= min.X && p.X <= max.X
&& p.Y >= min.Y && p.Y <= max.Y
&& p.Z >= min.Z && p.Z <= max.Z;
private static float YawDegrees(Quaternion q)
{
// Rotation of +X about Z only; indicative for the mostly-Z-only
// orientations landblock stabs carry.
Vector3 x = Vector3.Transform(Vector3.UnitX, q);
return MathF.Atan2(x.Y, x.X) * 180f / MathF.PI;
}
private static bool XyContains(Vector3[] poly, float x, float y)
{
bool inside = false;
for (int i = 0, j = poly.Length - 1; i < poly.Length; j = i++)
{
float yi = poly[i].Y, yj = poly[j].Y;
if ((yi > y) == (yj > y)) continue;
float t = (y - yi) / (yj - yi);
float xAt = poly[i].X + t * (poly[j].X - poly[i].X);
if (x < xAt) inside = !inside;
}
return inside;
}
private static bool TryPlaneZ(Vector3[] poly, float x, float y, out float z, out float nz)
{
z = 0f; nz = 0f;
if (poly.Length < 3) return false;
Vector3 n = Vector3.Cross(poly[1] - poly[0], poly[2] - poly[0]);
float len = n.Length();
if (len < 1e-9f) return false;
n /= len;
if (MathF.Abs(n.Z) < 1e-6f) return false;
nz = n.Z;
// n · (P - poly0) = 0 -> z = poly0.z - (n.x*(x-p0.x) + n.y*(y-p0.y)) / n.z
z = poly[0].Z - (n.X * (x - poly[0].X) + n.Y * (y - poly[0].Y)) / n.Z;
return true;
}
private static float DistanceToPolygon(Vector3[] poly, Vector3 p)
{
float best = float.MaxValue;
for (int i = 2; i < poly.Length; i++)
best = MathF.Min(best, DistanceToTriangle(poly[0], poly[i - 1], poly[i], p));
if (poly.Length == 2)
best = MathF.Min(best, DistanceToSegment(poly[0], poly[1], p));
return best;
}
private static float DistanceToTriangle(Vector3 a, Vector3 b, Vector3 c, Vector3 p)
{
Vector3 ab = b - a, ac = c - a, ap = p - a;
float d1 = Vector3.Dot(ab, ap), d2 = Vector3.Dot(ac, ap);
if (d1 <= 0 && d2 <= 0) return Vector3.Distance(p, a);
Vector3 bp = p - b;
float d3 = Vector3.Dot(ab, bp), d4 = Vector3.Dot(ac, bp);
if (d3 >= 0 && d4 <= d3) return Vector3.Distance(p, b);
float vc = d1 * d4 - d3 * d2;
if (vc <= 0 && d1 >= 0 && d3 <= 0)
return Vector3.Distance(p, a + ab * (d1 / (d1 - d3)));
Vector3 cp = p - c;
float d5 = Vector3.Dot(ab, cp), d6 = Vector3.Dot(ac, cp);
if (d6 >= 0 && d5 <= d6) return Vector3.Distance(p, c);
float vb = d5 * d2 - d1 * d6;
if (vb <= 0 && d2 >= 0 && d6 <= 0)
return Vector3.Distance(p, a + ac * (d2 / (d2 - d6)));
float va = d3 * d6 - d5 * d4;
if (va <= 0 && (d4 - d3) >= 0 && (d5 - d6) >= 0)
return Vector3.Distance(p, b + (c - b) * ((d4 - d3) / ((d4 - d3) + (d5 - d6))));
float denom = 1f / (va + vb + vc);
return Vector3.Distance(p, a + ab * (vb * denom) + ac * (vc * denom));
}
private static float DistanceToSegment(Vector3 a, Vector3 b, Vector3 p)
{
Vector3 ab = b - a;
float t = Vector3.Dot(p - a, ab) / MathF.Max(1e-9f, Vector3.Dot(ab, ab));
t = Math.Clamp(t, 0f, 1f);
return Vector3.Distance(p, a + ab * t);
}
}