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>
150 lines
6.9 KiB
C#
150 lines
6.9 KiB
C#
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) > 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})");
|
||
}
|
||
}
|