fix #180 (root): retail adjust_to_plane - PathClipped stops now resolve to the contact point, not a step boundary

The autonomous visual loop on the stateful-sought camera exposed the true
root of the #176 stripes: a ~19Hz SAWTOOTH. The sought re-extends ~3mm/frame
and the sweep silently passes while the 0.3m viewer sphere presses up to
~0.25m past the wall plane, then clips a whole transition step (~0.27m) back.
Headless replay against the real Facility Hub corridor BSP (0x8A020164, the
captured ray) reproduced it exactly: pre-fix, embedded targets passed
unclipped and the first detection stopped at the PREVIOUS STEP BOUNDARY,
tracking the target (eyeBack = s - 0.27).

Root cause: BSPQuery.AdjustToPlane - copied from ACE's BSPTree.cs port -
was structurally inverted and ALWAYS returned false (the touchTime==1 branch
re-placed the sphere at the unchanged check position; touchTime<1 iterated
doing nothing; the <0.02 convergence exit returned false). With the
PerfectClip exact-contact machinery dead, CollideWithPt always fell to the
bare Collided path and the transition reverted the colliding step whole.
ACE never noticed: PERFECT_CLIP (0x40) is a client camera flag the server
never exercises (feedback_bn_decomp_field_names class 3 - the retail binary
outranks ACE in branches ACE never runs). The pre-stateful camera flipped
1-step vs 2-step backoffs on mm drift - the measured pulledIn 0.27 <-> 0.53
of the original #176 strobe was step quantization all along.

Rewritten per the retail binary (pseudocode doc
docs/research/2026-07-06-adjust-to-plane-pseudocode.md):

- BSPTREE::adjust_to_plane (0x00539bf0): clearTime/hitTime bounds (0.0/1.0),
  Phase 1 walks plane-touch times re-testing the whole tree (the tree test
  feeds a DIFFERENT blocking poly back into the next iteration), Phase 2
  binary-searches with the SHARED iteration counter, window < 0.02 =
  CONVERGED, final commit = last known-clear time. Only failure = Phase-1
  exhaustion.
- CPolygon::adjust_sphere_to_poly (0x00538170): early-out = plane-band test
  at the START position (was: precise-poly test at the check position);
  touch side = sign(dpPos)*radius (was: hard-coded -radius; ACE misdecoded
  it as movement.LengthSquared() <= r^2); result unclamped per retail.

Replay pin Issue180CorridorSweepHysteresisReplayTests: short-of-touch
targets pass, past-touch targets always clip, and the clipped stop is the
CONSTANT surface-contact point (eyeBack 1.609 across the band; spread
< 0.03m) instead of tracking the target.

Suites green (Core 2600+2skip / App 729+2skip / UI 425 / Net 385).
Pending: visual-loop re-verify + the user gate (#180 + #176 re-gate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-06 17:20:46 +02:00
parent 48aaab811c
commit f10fe4e96c
3 changed files with 348 additions and 52 deletions

View file

@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.Tests.Conformance;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// #180 residual — the camera-sweep SAWTOOTH in the Facility Hub corridor
/// (0x8A020164), root-caused to a dead <c>BSPQuery.AdjustToPlane</c>:
/// acdream (via ACE's misdecoded port) had the PerfectClip exact-contact
/// machinery structurally inverted so it ALWAYS failed, and every PathClipped
/// camera stop reverted to the previous transition-step boundary instead of
/// the contact point. Live signature (launch-180-verify.log): the stateful
/// sought re-extends ~3 mm/frame, the sweep passes silently until the step
/// containing the contact, then clips a whole step (~0.27 m) back — a ~19 Hz
/// sawtooth; the pre-stateful camera flipped 1-step vs 2-step backoffs
/// (pulledIn 0.27 ↔ 0.53) — the ORIGINAL #176 stripe strobe.
///
/// This replay walks sweep targets along the exact captured ray
/// (pivot → the [flap-sweep] in= of idx 28882) across the wall behind the
/// spawn camera, against the REAL cell BSP. It pins the retail-faithful
/// stop behavior (BSPTREE::adjust_to_plane 0x00539bf0, pseudocode
/// docs/research/2026-07-06-adjust-to-plane-pseudocode.md):
///
/// 1. targets short of first touch pass unclipped (the wall plane is
/// genuinely ~1.61 m out along this ray: dist(center,plane) &gt; r);
/// 2. every target past first touch CONTACTS (no silent pass-through
/// band — pre-fix, targets embedded up to ~0.25 m passed);
/// 3. the clipped eye is the CONSTANT surface-contact point (pre-fix it
/// tracked the target one step back: eyeBack = s ~0.27).
/// </summary>
public class Issue180CorridorSweepHysteresisReplayTests
{
private readonly ITestOutputHelper _out;
public Issue180CorridorSweepHysteresisReplayTests(ITestOutputHelper output) => _out = output;
private const float ViewerSphereRadius = 0.3f; // retail viewer_sphere (acclient :93314)
private const uint FacilityHubLandblock = 0x8A020000u;
private const uint CorridorCell = 0x8A020164u;
// [flap-cam] player=(70.58,-40.16,-5.90) (parked spawn) + PivotHeight 1.5.
private static readonly Vector3 Pivot = new(70.58f, -40.16f, -4.40f);
// [flap-sweep] idx 28882: the captured in= that clipped (the sawtooth's deep edge).
private static readonly Vector3 THit = new(70.366051f, -38.628315f, -3.935829f);
private static (PhysicsEngine, PhysicsDataCache) BuildCorridorEngine(DatCollection dats)
{
var cache = new PhysicsDataCache();
var engine = new PhysicsEngine { DataCache = cache };
for (uint low = 0x0100u; low <= 0x01FFu; low++)
{
uint id = FacilityHubLandblock | low;
try { ConformanceDats.LoadEnvCell(dats, cache, id); }
catch (InvalidOperationException) { /* cell id not present in this dungeon */ }
}
var heights = new byte[81];
var heightTable = new float[256];
for (int i = 0; i < 256; i++) heightTable[i] = -1000f;
engine.AddLandblock(FacilityHubLandblock, new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(), 0f, 0f);
return (engine, cache);
}
/// <summary>Mirror of PhysicsCameraCollisionProbe.SweepEye's transition call.</summary>
private static ResolveResult SweepViewer(PhysicsEngine engine, Vector3 pivot, Vector3 desiredEye, uint cellId)
{
uint startCell = cellId;
if ((cellId & 0xFFFFu) >= 0x0100u)
{
var (pivotCell, found) = engine.AdjustPosition(cellId, pivot);
if (found) startCell = pivotCell;
}
Vector3 begin = pivot - new Vector3(0f, 0f, ViewerSphereRadius);
Vector3 end = desiredEye - new Vector3(0f, 0f, ViewerSphereRadius);
return engine.ResolveWithTransition(
currentPos: begin,
targetPos: end,
cellId: startCell,
sphereRadius: ViewerSphereRadius,
sphereHeight: 0f,
stepUpHeight: 0f,
stepDownHeight: 0f,
isOnGround: false,
body: null,
moverFlags: ObjectInfoState.IsViewer | ObjectInfoState.PathClipped
| ObjectInfoState.FreeRotate | ObjectInfoState.PerfectClip,
movingEntityId: 0);
}
[Fact]
public void ClippedStop_IsTheContactPoint_NotAStepBoundary()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var (engine, _) = BuildCorridorEngine(dats);
Vector3 dir = Vector3.Normalize(THit - Pivot);
var clippedEyeBacks = new List<float>();
for (float s = 1.20f; s <= 1.75f; s += 0.025f)
{
Vector3 target = Pivot + dir * s;
var r = SweepViewer(engine, Pivot, target, CorridorCell);
Vector3 eye = r.Position + new Vector3(0f, 0f, ViewerSphereRadius);
float eyeBack = Vector3.Distance(Pivot, eye);
_out.WriteLine(FormattableString.Invariant(
$"s={s:F3} eyeBack={eyeBack:F3} collNorm={r.CollisionNormalValid}"));
if (s <= 1.55f)
{
// (1) Short of first touch (~1.61 m): the sweep must reach the target.
Assert.False(r.CollisionNormalValid,
$"s={s:F3}: no wall within reach, sweep must not clip");
Assert.True(MathF.Abs(eyeBack - s) < 0.01f,
$"s={s:F3}: unclipped sweep must reach the target, got eyeBack={eyeBack:F3}");
}
else if (s >= 1.65f)
{
// (2) Past first touch: every target must contact — the pre-fix
// defect passed targets embedded up to ~0.25 m unclipped.
Assert.True(r.CollisionNormalValid,
$"s={s:F3}: target past the wall must clip");
clippedEyeBacks.Add(eyeBack);
}
}
// (3) The clipped stop is the constant surface-contact point. Pre-fix the
// stop tracked the target one step back (eyeBack = s ~0.27, spread
// ≈ 0.10 m over this range); retail's adjust_to_plane commits the
// last-clear time within a 0.02 window of the contact.
Assert.True(clippedEyeBacks.Count >= 4, "expected several clipped samples");
float min = float.MaxValue, max = float.MinValue;
foreach (var e in clippedEyeBacks) { min = MathF.Min(min, e); max = MathF.Max(max, e); }
Assert.True(max - min < 0.03f,
$"clipped stops must be one contact point, got spread {max - min:F3} m ({min:F3}..{max:F3})");
}
}