acdream/tests/AcDream.Core.Tests/Physics/Issue180CorridorSweepHysteresisReplayTests.cs
Erik f10fe4e96c 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>
2026-07-06 17:20:46 +02:00

150 lines
6.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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})");
}
}