diff --git a/docs/ISSUES.md b/docs/ISSUES.md
index b43498d7..9311bf3d 100644
--- a/docs/ISSUES.md
+++ b/docs/ISSUES.md
@@ -24,9 +24,29 @@ What does NOT go here:
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
-## #337 — Neftet rock plateaus: wedged at the top, jumps sink into the mesh, corpses fall through — cause NOT yet established
+## #337 — Neftet rock plateaus: wedged at the top, jumps sink into the mesh, corpses fall through — DIAGNOSED, fix not landed
-**Status:** OPEN — instrumented, not diagnosed. Awaiting the live capture below.
+**Status:** OPEN — **mechanism proven offline 2026-08-06**, fix proposed and
+awaiting approval. It is **#333**: the per-object broadphase in
+`Transition.FindObjCollisionsInCell` measures to the shadow entry's part
+ORIGIN and compares against the BSP ROOT BOUNDING SPHERE's radius. Those are
+23.6 m apart for `0xC8766009` / `gfx=0x01004751`, so a mover on the plateau is
+rejected before the query it would have passed. Retail has no such filter
+(`CPartArray::FindObjCollisions` @0x00518180 and
+`CPhysicsPart::find_obj_collisions` @0x0050d8d0 verified instruction-by-
+instruction on the PDB-paired binary). `0xC8766002`, the owner with 11,014
+`tested-ok` and zero hits, is **innocent** — its geometry is 22.8 m away.
+Full evidence + the proposed fix:
+[`docs/research/2026-08-06-337-neftet-wedge-mechanism.md`](research/2026-08-06-337-neftet-wedge-mechanism.md).
+Reproducer + offline replay:
+`tests/AcDream.Core.Tests/Physics/Issue337NeftetRockGeometryInspectionTests.cs`
+(the acceptance gate is the skipped
+`TheBroadphaseAdmitsTheSurfaceTheMoverIsStandingOn`).
+
+**Historical framing below is superseded by that document** — in particular
+"the mesh never collides" was true only of the innocent neighbour, and both
+the wrong-transform and BSP-traversal-hole hypotheses are refuted by
+measurement.
**Severity:** HIGH — walk-through, fall-through, and a hard movement stop on world geometry.
**Filed:** 2026-08-06, user-reported in live play after #334's fix landed.
**Component:** physics / collision — possibly geometry data rather than movement code.
diff --git a/docs/research/2026-08-06-337-neftet-wedge-mechanism.md b/docs/research/2026-08-06-337-neftet-wedge-mechanism.md
new file mode 100644
index 00000000..54cad1ec
--- /dev/null
+++ b/docs/research/2026-08-06-337-neftet-wedge-mechanism.md
@@ -0,0 +1,232 @@
+# #337 — the Neftet plateau wedge: mechanism, measured
+
+**Date:** 2026-08-06
+**Status:** mechanism proven offline; fix proposed, NOT landed.
+**Reproducer:** `tests/AcDream.Core.Tests/Physics/Issue337NeftetRockGeometryInspectionTests.cs`
+**Related:** #333 (filed: the broadphase reach filter has AP-156's defect at
+the query site), #334 (`13fcf381`, registration extent walk), AP-156
+(`b52967de`, flood sphere placement).
+
+---
+
+## Verdict in one paragraph
+
+The collision mesh is present, correctly shaped, correctly placed in the
+world, and the BSP traversal reaches every part of it. **The mover never gets
+as far as the query.** `Transition.FindObjCollisionsInCell`'s per-object
+broadphase measures the mover's distance to the shadow entry's `Position` —
+the part **origin** — and compares 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 the plateau is inside the real
+bounding sphere by ~20 m of margin and is still rejected. Retail has no such
+filter at all. Everything else in the symptom set — the wedge, the sink, the
+corpse fall-through, ACE's spawn refusal, the 16,278 m/s rejection, the
+character vanishing from a retail observer's view — is downstream of that one
+rejection.
+
+---
+
+## What was refuted, and by what measurement
+
+### 1. "The rock's own mesh never collides" — TRUE for `0xC8766002`, and it is INNOCENT
+
+`0xC8766002` / `gfx=0x010046DE` really does report 11,014 `tested-ok` with
+zero `tested-adjusted`, `tested-collided` or `tested-slid` across the whole
+capture. That is correct behaviour: **its geometry is 22.8 m away from the
+wedge point.** Over a 30 × 30 × 12 m lattice covering the whole plateau
+(12,493 points) it has **zero** brute-force hits — it simply is not there.
+
+It is a candidate in that cell only because it is a 130 × 147 m owner whose
+registration legitimately spans the cell. The probe families that reach it are
+working exactly as designed. This object was a red herring.
+
+The neighbours do collide: `0xC8766003` 767 adjusted / 26 collided,
+`0xC8766009` 1,135 adjusted / 708 slid / 58 collided. The rock the player
+actually walks on is **`0xC8766009` / `gfx=0x01004751`**.
+
+### 2. Wrong world transform — REFUTED
+
+The offline reconstruction from the installed DAT reproduces the runtime
+placement exactly: `0xC8766002` at `(84.699, 100.082, 13.000)` yaw −45.00°
+against the live `[geom]` line's `objPos=(84.70,100.08,13.00)` and its
+`bspCentreOffset` (which implies −45.00°). All eleven owners match. The
+transform is right.
+
+### 3. BSP traversal hole / bounding-sphere early-out — REFUTED
+
+A referee ran the production walk (`FlatBspQuery.SphereIntersectsPoly`,
+node bounding-sphere early-outs and all) against brute force over every
+polygon the tree indexes, at 7,770 probe points placed on both faces of every
+polygon of all eleven owners, plus 137,423 lattice points over the plateau.
+
+**Mismatch = 0 everywhere.** There is no pocket the traversal cannot reach.
+
+### 4. Missing / degenerate geometry, a hole in the walking surface — REFUTED
+
+A 0.5 m hole map over the plateau shows continuous upward-facing physics
+coverage across the whole wedge region; the only gaps are outside the rock's
+footprint. At the wedge XY the column is closed: an up-facing polygon at
+z = 50.985 (nz = +0.9805) over a down-facing one at z = 40.597 (nz = −0.9325),
+i.e. 10.4 m of solid rock, with the player's feet at z = 50.011 — **0.975 m
+inside it.**
+
+### 5. `[geom] verdict=coincident` — confirmed to mean less than it looks
+
+`LogGeometry` compares `physicsMin/Max` against `visualMin/Max`, both taken
+from the same GfxObj asset in the object's **own local frame**. No world
+transform enters the comparison. `coincident` proves shape agreement only.
+It happens to be true here, but it could never have been the discriminator.
+
+---
+
+## The mechanism, measured
+
+### The frame-by-frame (337-support.log, cell `0x8766002B`)
+
+Landblock-local coordinates; the log's Y carries a +576 m live-centre offset,
+removed here. Player feet position; Setup `0x02000001` gives sphere[0] at
+feet + 0.475 with r = 0.480 and sphere[1] at feet + 1.350.
+
+| t | feet z | surface z at that XY | what happened |
+|---|---|---|---|
+| …218906–219296 | 51.096 | 51.081 | standing correctly (delta +0.015). **Every horizontal move of 0.25–0.53 m returns `moved=0.000, stalled=True`** on an 11° walkable slope. This is the wedge. |
+| 219328 | 51.096 → 51.389 | | player jumps to escape |
+| …–219937 | → 54.638 | | free rise, `support=none` |
+| …–221125 | 54.638 → 50.021 | 51.183 | **falls straight through the surface.** Six consecutive resolves accept the full commanded step with no contact plane. |
+| 221156 | 50.021 | | descent finally blocked, still no contact plane |
+| 221671 | 49.908 | 51.127 | `cpSrc=ValidateTransition:6076` — retail's stationary-fall failsafe **manufactures** a flat plane at z = 49.903, 1.2 m below the real rock surface |
+
+The `support=object cpNz=1.0000` readings inside the rock are not the rock.
+`ValidateTransition:6076` is retail's `FramesStationaryFall > 1` synthetic
+up-plane through the sphere bottom; `ValidateTransition:5997` is retail's
+`LastKnownContactPlane` restore, i.e. a **stale** plane retained from frames
+when the object was still admitted. Both are retail-correct responses to a
+stuck body — which is why the client believes it is standing while ACE
+believes the position is invalid.
+
+### The replay: the query would have hit
+
+`Issue337NeftetRockGeometryInspectionTests.ReplayTheDescentThatFellThrough…`
+runs each of those six descent steps as a swept sphere against `0xC8766009`'s
+real BSP:
+
+```
+CONTROL climb step ent=0xC8766009 sphere[0] swept=True(poly 22) static=True nearest=0.070 m
+CONTROL stand step ent=0xC8766009 sphere[0] swept=True(poly 26) static=True nearest=0.003 m
+FALL 2 51.269->50.961 sphere[0] swept=True(poly 31 t=0.0000) static=True nearest=0.248 m
+FALL 3 50.961->50.670 sphere[0] swept=True(poly 31 t=0.0000) static=True nearest=0.037 m
+FALL 4 50.670->50.335 sphere[0] swept=True(poly 31 t=0.0000) static=True nearest=0.366 m
+```
+
+The geometry is right there and the production primitive returns the hit.
+Path 6 of `FlatBspQuery.FindCollisionsCore` (the airborne dispatch) would have
+called `path.SetCollide(...)` and returned `Adjusted`, blocking the fall.
+**It was never called.**
+
+### Why it was never called
+
+`Transition.FindObjCollisionsInCell` (`src/AcDream.Core/Physics/TransitionTypes.cs`,
+the `maxReach` test in the candidate loop):
+
+```csharp
+Vector3 deltaToCurr = currPos - obj.Position; // ← part ORIGIN
+...
+float maxReach = sphereRadius + obj.Radius // ← BSP ROOT SPHERE radius
+ + movement.Length() + 2f;
+if (distToCurr > maxReach) continue;
+```
+
+For `0xC8766009`: origin `(159.107, 36.629, 0.005)`, root bounding sphere
+centred at local `(−1.753, 14.259, 18.667)` with radius 56.909 — i.e. the
+sphere centre sits **23.556 m** from the origin the filter measures to.
+
+At the fall position, measured (reproducer output):
+
+```
+distToOrigin = 60.434 m > maxReach = 59.697 m → REJECTED
+distance to the BSP bounding-sphere CENTRE = 37.083 m ≪ 56.909 m radius
+```
+
+The live capture recorded the same thing 7,225 times for this owner, and the
+probe's own `wouldAcceptAtCenter` column says `True` on **every** rejection:
+
+```
+currPos=(126.57,36.59,50.71) distOrigin=60.24 budget=59.64 shortfall=+0.60 acceptAtCentre=True move=0.255
+currPos=(126.57,36.59,50.71) distOrigin=60.24 budget=60.10 shortfall=+0.14 acceptAtCentre=True move=0.715
+```
+
+### Why it is bounded, and why jumping over it works
+
+The dead zone is the shell between `distOrigin = maxReach` and the true
+bounding sphere. Because the sphere centre is offset 23.556 m from the origin,
+that shell is up to ~23.5 m thick on the far side of the object — here it
+covers the top of the plateau and nothing else. Everywhere closer to the
+origin, the same mesh collides normally. That is the "fails in a bounded
+region, works elsewhere on the same object" property.
+
+`movement.Length()` is a term in the budget. A walking step of 0.25 m gives
+shortfall +0.60 (rejected); a step of 0.72 m gives +0.14; a step of ~0.86 m
+passes. **A jump's larger per-frame movement inflates the budget and lets the
+object back through the filter.** That is why jumping over the spot works and
+walking into it does not, and why a corpse — small per-frame movement — falls
+straight through.
+
+---
+
+## Retail
+
+There is no per-object distance filter on retail's BSP branch. Verified
+instruction-by-instruction with cdb against the PDB-paired v11.4186 binary
+(`C:\Users\erikn\Downloads\acclient.exe`, `check_exe_pdb.py` → `MATCH`,
+GUID `9e847e2f-777c-4bd9-886c-22256bb87f32`):
+
+- `acclient!CPartArray::FindObjCollisions` @ **0x00518180** — 14 instructions:
+ a bare `do/while` over `parts[i]` calling `CPhysicsPart::find_obj_collisions`
+ and breaking on `!= OK`. No compare, no float math.
+- `acclient!CPhysicsPart::find_obj_collisions` @ **0x0050d8d0** — 17
+ instructions: null-check `gfxobj`, null-check `gfxobj->physics_bsp`
+ (`[ecx+78h]`), `SPHEREPATH::cache_localspace_sphere`,
+ `CGfxObj::find_obj_collisions` @ 0x00534700. No compare, no float math.
+
+`CPhysicsObj::FindObjCollisions` @ 0x0050f050 reaches that pair through
+`CPartArray::FindObjCollisions` at 0x0050f192; its cylsphere/sphere loops
+(0x0050f1c4 / 0x0050f251) call the real `intersects_sphere` per primitive —
+they are tests, not pre-filters. Retail's only spatial rejection is the BSP
+node bounding-sphere test inside the walk, which is correctly centred.
+
+The in-tree comment claiming the filter is "the analog of the part
+sorting-sphere early-outs inside retail's `CPhysicsObj::FindObjCollisions` —
+response-neutral, pure perf" is **wrong on both counts.**
+
+---
+
+## Proposed fix
+
+**Preferred — remove the per-object distance pre-check for BSP entries.**
+Retail has none, and the BSP walk's own root node bounding-sphere test is the
+correctly-centred early-out that makes it unnecessary. Size: delete ~10 lines
+in `Transition.FindObjCollisionsInCell` plus the probe's `rejected-reach`
+branch; correct the false retail-analog comment in the same commit. No
+divergence-register row is created; if #333's row exists it is deleted.
+
+**Fallback if a perf gate demands a filter** — measure to the bounding-sphere
+centre, the AP-156 correction applied at the query site:
+`obj.Position + Vector3.Transform(BoundsCenter * Scale, obj.Rotation)`.
+`ShadowShape.BoundsCenter` already carries this value; `ShadowEntry` does not,
+so this variant also touches `ShadowEntry` and both registration paths
+(`Register` and `RegisterMultiPart`). Larger, and it keeps a non-retail
+construct that then needs a register row.
+
+Acceptance gate: un-skip
+`Issue337NeftetRockGeometryInspectionTests.TheBroadphaseAdmitsTheSurfaceTheMoverIsStandingOn`.
+Verified to fail today with the numbers above.
+
+---
+
+## Separate observation, not part of this defect
+
+Setup `0x02000001` authors `StepUpHeight = 0.600` and
+`StepDownHeight = 1.500`. The live `[support]` lines show the player resolving
+with `stepUp=0.400 stepDown=0.400`. A 1.5 m step-down is what keeps a mover
+attached to a descending slope; 0.4 m is not. Worth its own investigation —
+it does not cause this wedge, and it was not chased here.
diff --git a/tests/AcDream.Core.Tests/Physics/Issue337NeftetRockGeometryInspectionTests.cs b/tests/AcDream.Core.Tests/Physics/Issue337NeftetRockGeometryInspectionTests.cs
new file mode 100644
index 00000000..0bc8d4d8
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Physics/Issue337NeftetRockGeometryInspectionTests.cs
@@ -0,0 +1,928 @@
+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 REPRODUCER — currently FAILING, hence skipped.
+ ///
+ ///
+ /// The per-object broadphase in
+ /// Transition.FindObjCollisionsInCell (TransitionTypes.cs, the
+ /// maxReach test) measures the mover's distance to the shadow
+ /// entry's Position — the part ORIGIN — and compares it against
+ /// obj.Radius, which is the physics-BSP ROOT BOUNDING SPHERE's
+ /// radius. For this rock those two are 23.6 m apart, so a mover standing
+ /// on its plateau is inside the bounding sphere by ~20 m of margin and
+ /// still fails the test. It is the 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.
+ ///
+ ///
+ /// Un-skip this as the acceptance gate for the fix.
+ ///
+ [Fact(Skip = "#337: fails until the query-site broadphase measures to the "
+ + "BSP bounding-sphere centre (or is removed, as retail has none).")]
+ public void TheBroadphaseAdmitsTheSurfaceTheMoverIsStandingOn()
+ {
+ 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
+
+ float ownerRadius = rock.Bsp.Nodes[rock.Bsp.RootIndex].BoundingSphere.Radius;
+
+ // Verbatim from the production predicate.
+ float distToOrigin = (currPos - rock.Position).Length();
+ float maxReach = SphereRadius + ownerRadius + Movement + 2f;
+
+ Assert.True(
+ distToOrigin <= maxReach,
+ $"broadphase rejected a candidate the mover is standing on: "
+ + $"distToOrigin={distToOrigin:F3} > maxReach={maxReach:F3}. "
+ + $"Measured to the BSP bounding-sphere CENTRE it is "
+ + $"{(currPos - (rock.Position + Vector3.Transform(rock.Bsp.Nodes[rock.Bsp.RootIndex].BoundingSphere.Origin, rock.Rotation))).Length():F3} m, "
+ + $"comfortably inside the same radius.");
+ }
+
+ 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);
+ }
+}