test #137: the seam shake reproduced offline — third mechanism characterized
The corridor gate FAILED with a changed symptom: shaking at cell seams (+ purple floor flashing there) instead of the dead stop. Deep-probe session (step-walk/push-back/indoor-bsp + full resolve capture) traced the complete chain; docs/ISSUES.md #137 carries it. Short form: - Corridor floors are double-faced portal slabs over under-rooms; the resting foot sphere lives within half a millimeter of three hit/straddle thresholds there. - Crossing a boundary, the foot penetrates the neighbor ramp slab by ~0.4mm, steps up onto it successfully (+0.6mm lift, stepped=True) — and the lifted check position is then LOST: the following pass runs at the unlifted height (the P2 stale-snapshot class; retail step_up 0x0050b6cc restores only on FAILURE). - The unlifted re-test grazes the under-room's ceiling (the slab underside) within the near-miss window, dispatches a neg-poly step-up with a DOWNWARD normal, whose nested step-down finds no walkable at exact tangency -> StepUpSlide -> slide_sphere opposing branch -> reversed-movement collision -> Collided -> revert. Every frame = shake. Apparatus committed: - Issue137CorridorSeamReplayTests: 3 deterministic offline repros (snapshot-exact west-boundary from the capture, east deep-straddle, the clean-run pin), currently Skip='#137 seam shake' pending the fix. Key: THREE portal-ring hydration (the under-room 0x8A020166 is ring-3; with fewer rings the flood can't add it and the bug vanishes) + live Setup step heights (0.6/1.5) + probe-buffer capture for line-diffing offline vs live traces. - Issue137CorridorSeamInspectionTests: portal-poly world spans (exposed the visual-vs-physics polygon-id conflation and the floor-portal topology), physics-BSP leaf membership walk, hit-normal candidate sweep (|align| both windings), downward-poly sweep. NEXT: read TransitionalInsert's attempt loop against retail 0x0050b6f0, find the restore that clobbers the successful step-up position, fix, un-skip. The purple seam flashing is expected to be the render exposing the same per-frame oscillation - re-check after the physics fix. Suites: Core 2553 / App 713 / UI 425 / Net 385, 0 failures (5 skips = 2 pre-existing + the 3 parked repros). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e8651b3819
commit
dbddad7a5e
4 changed files with 25131 additions and 4 deletions
|
|
@ -1183,8 +1183,46 @@ walls** in particular. (Symptoms not fully characterized yet: likely walking thr
|
|||
openings that should block / blocking at openings that should pass, and door collision not
|
||||
matching the door's open/closed state.)
|
||||
|
||||
**CORRIDOR PHANTOM FIXED 2026-07-06 (both mechanisms resolved; visual gate
|
||||
pending — see `docs/research/2026-07-06-137-sliding-normal-lifecycle-audit.md`
|
||||
**GATE 2026-07-06 FAILED — THIRD MECHANISM CHARACTERIZED (the seam shake),
|
||||
deterministic offline repro secured:** with mechanisms 1+2 fixed the dead
|
||||
stop became a SHAKE at cell seams (+ purple floor flashing there — almost
|
||||
certainly the render exposing the same per-frame position/OnWalkable
|
||||
oscillation; re-check after the physics fix). Full chain, every link
|
||||
probe-traced (`launch-137-seam-probes.log`, capture
|
||||
`resolve-137-seam-capture.jsonl` tick 4101 ×46):
|
||||
1. Corridor cells sit above under-rooms; the shared floor slab is
|
||||
double-faced (up-face + underside as separate physics polys) and IS a
|
||||
portal plane (e.g. 0x8A020165's ramp over 0x8A020166). The resting foot
|
||||
sphere is permanently within ±0.5 mm of THREE thresholds there (poly-hit
|
||||
r−ε, walkable r−ε, portal-straddle r+ε).
|
||||
2. Walking across the boundary at the flat-floor height penetrates the
|
||||
ramp slab by ~0.4 mm → foot full-hit on the up-face → StepSphereUp →
|
||||
step-down accepts the ramp (+0.6 mm lift, CheckPos −5.999,
|
||||
`[stepsphereup] stepped=True`).
|
||||
3. **THE BUG: the lifted position is then LOST** — the next pass runs at
|
||||
the UNLIFTED height (GlobalSphere center −5.520 vs the lifted −5.519;
|
||||
the P2 stale-snapshot class, single-slot Save/RestoreCheckPos clobber
|
||||
suspected — retail `CTransition::step_up` 0x0050b6cc restores ONLY on
|
||||
failure) → the re-test at 0.4 mm inside the slab grazes the NEIGHBOR
|
||||
under-room's CEILING (the slab underside, n≈(−0.03,0,−1)) within the
|
||||
near-miss window → recorded (retail records it too — pos_hits_sphere
|
||||
registers geometric hits pre-cull) → neg-poly step-up dispatch with the
|
||||
DOWNWARD normal → the nested step-down finds no walkable at exact
|
||||
tangency → StepUpSlide → slide_sphere(down normal vs up contact plane)
|
||||
→ the opposing branch → reversed-movement collision normal → Collided →
|
||||
validate revert (Contact/OnWalkable stripped) → next step's AdjustOffset
|
||||
zeroes → out==in every frame = the shake. Retail never enters at step 3:
|
||||
its kept step-up lift leaves the sphere ON the surface, no graze.
|
||||
4. Offline repro: `Issue137CorridorSeamReplayTests` (3 tests, currently
|
||||
`Skip="#137 seam shake"`) reproduce the block deterministically — the
|
||||
key was hydrating THREE portal rings (the under-room 0x8A020166 is
|
||||
ring-3; with fewer rings the flood can't add it and everything passes).
|
||||
NEXT: read our `TransitionalInsert` attempt loop against retail
|
||||
0x0050b6f0 to find the restore that clobbers the successful step-up's
|
||||
position; fix; un-skip the three tests.
|
||||
|
||||
**CORRIDOR PHANTOM mechanisms 1+2 FIXED 2026-07-06 (see
|
||||
`docs/research/2026-07-06-137-sliding-normal-lifecycle-audit.md`
|
||||
for the full audit):** mechanism 2 = BSPQuery Contact-branch stub slide
|
||||
responses leaked sliding normals retail's BSP layer never writes (fixed:
|
||||
real `slide_sphere` routing + success-gated body writeback). Mechanism 1 as
|
||||
|
|
|
|||
24868
resolve-137-seam-capture.jsonl
Normal file
24868
resolve-137-seam-capture.jsonl
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -28,6 +28,7 @@ public class Issue137CorridorSeamInspectionTests
|
|||
[Theory]
|
||||
[InlineData(0x8A02016Eu)]
|
||||
[InlineData(0x8A02017Au)]
|
||||
[InlineData(0x8A02011Eu)] // the under-floor room the corridor's floor-portals lead to
|
||||
public void CorridorCell_PhysicsPolysAndPortals_DatInspection(uint envCellId)
|
||||
{
|
||||
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||
|
|
@ -254,7 +255,10 @@ public class Issue137CorridorSeamInspectionTests
|
|||
System.Numerics.Vector3.Cross(w1 - w0, w2 - w0));
|
||||
|
||||
float align = System.Numerics.Vector3.Dot(n, hitNormal);
|
||||
if (align < 0.95f) continue; // within ~18° of the recorded normal
|
||||
// |align|: the vertex-fan winding convention can flip the
|
||||
// computed normal vs the physics plane's true facing — accept
|
||||
// both signs (2026-07-06 sweep flaw fix).
|
||||
if (MathF.Abs(align) < 0.95f) continue; // within ~18° of the recorded normal
|
||||
|
||||
// Plane distance from the hit point.
|
||||
float d = -System.Numerics.Vector3.Dot(n, w0);
|
||||
|
|
@ -358,4 +362,67 @@ public class Issue137CorridorSeamInspectionTests
|
|||
}
|
||||
_out.WriteLine("(downward sweep complete)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2026-07-06 gate-session follow-up: seam crossings SUCCEED at
|
||||
/// y≈−40.8..−41.2 and BLOCK at y≈−39.5..−39.8 (cell-transit log,
|
||||
/// launch-137-corridor-gate.log). A y-dependent boundary with no physics
|
||||
/// polygon culprit points at the PORTAL POLYGONS — if the doorway
|
||||
/// openings don't span the full corridor width, the transit/membership
|
||||
/// machinery only hands the sphere to the neighbor inside the portal
|
||||
/// poly's span. Dump every portal polygon's world-space vertex extent.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(0x8A02016Eu)]
|
||||
[InlineData(0x8A02017Au)]
|
||||
public void CorridorCell_PortalPolygonWorldSpans(uint envCellId)
|
||||
{
|
||||
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||
?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile),
|
||||
"Documents", "Asheron's Call");
|
||||
if (!Directory.Exists(datDir))
|
||||
{
|
||||
_out.WriteLine($"SKIP: dat directory not found at {datDir}");
|
||||
return;
|
||||
}
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
|
||||
var envCell = dats.Get<EnvCell>(envCellId);
|
||||
Assert.NotNull(envCell);
|
||||
var environment = dats.Get<DatReaderWriter.DBObjs.Environment>(0x0D000000u | envCell!.EnvironmentId);
|
||||
Assert.NotNull(environment);
|
||||
Assert.True(environment!.Cells.TryGetValue(envCell.CellStructure, out var cs));
|
||||
|
||||
var rot = new System.Numerics.Quaternion(
|
||||
envCell.Position.Orientation.X, envCell.Position.Orientation.Y,
|
||||
envCell.Position.Orientation.Z, envCell.Position.Orientation.W);
|
||||
var world = System.Numerics.Matrix4x4.CreateFromQuaternion(rot)
|
||||
* System.Numerics.Matrix4x4.CreateTranslation(
|
||||
envCell.Position.Origin.X, envCell.Position.Origin.Y, envCell.Position.Origin.Z);
|
||||
|
||||
_out.WriteLine($"=== 0x{envCellId:X8} portal polygons (world spans) ===");
|
||||
foreach (var p in envCell.CellPortals)
|
||||
{
|
||||
if (!cs!.Polygons.TryGetValue((ushort)p.PolygonId, out var poly))
|
||||
{
|
||||
_out.WriteLine($" portal poly {p.PolygonId} -> 0x{p.OtherCellId:X4} {p.Flags}: NOT in visual set");
|
||||
continue;
|
||||
}
|
||||
|
||||
var min = new System.Numerics.Vector3(float.MaxValue);
|
||||
var max = new System.Numerics.Vector3(float.MinValue);
|
||||
foreach (var vid in poly.VertexIds)
|
||||
{
|
||||
if (!cs.VertexArray.Vertices.TryGetValue((ushort)vid, out var v)) continue;
|
||||
var w = System.Numerics.Vector3.Transform(v.Origin, world);
|
||||
min = System.Numerics.Vector3.Min(min, w);
|
||||
max = System.Numerics.Vector3.Max(max, w);
|
||||
}
|
||||
_out.WriteLine(
|
||||
$" portal poly {p.PolygonId} -> 0x{p.OtherCellId:X4} [{p.Flags}] " +
|
||||
$"x=[{min.X:F2},{max.X:F2}] y=[{min.Y:F2},{max.Y:F2}] z=[{min.Z:F2},{max.Z:F2}] " +
|
||||
$"verts={poly.VertexIds.Count}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,22 @@ public class Issue137CorridorSeamReplayTests
|
|||
toLoad.Add(0x8A020000u | p.OtherCellId);
|
||||
}
|
||||
|
||||
// Expand three portal rings — the live collision cell array reaches
|
||||
// cells three hops out (0x8A020166, the under-ramp room whose ceiling
|
||||
// is the ramp slab's underside, is ring-3 in the 2026-07-06
|
||||
// seam-shake trace; with only two rings the offline flood can never
|
||||
// add it and the shake does not reproduce).
|
||||
for (int ring = 0; ring < 3; ring++)
|
||||
{
|
||||
foreach (var known in new System.Collections.Generic.List<uint>(toLoad))
|
||||
{
|
||||
var cell = dats.Get<EnvCell>(known);
|
||||
if (cell is null) continue;
|
||||
foreach (var p in cell.CellPortals)
|
||||
toLoad.Add(0x8A020000u | p.OtherCellId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var cellId in toLoad)
|
||||
{
|
||||
var envCell = dats.Get<EnvCell>(cellId);
|
||||
|
|
@ -131,7 +147,145 @@ public class Issue137CorridorSeamReplayTests
|
|||
body: body,
|
||||
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide);
|
||||
|
||||
[Fact]
|
||||
/// <summary>
|
||||
/// 2026-07-06 seam-shake repro, snapshot-exact (probe session
|
||||
/// launch-137-seam-probes.log + resolve-137-seam-capture.jsonl tick 4101,
|
||||
/// repeated ×46): running WEST across the x=75 boundary
|
||||
/// (0x8A02016E → 0x8A020165, the ramp cell) from (75.287, −40.035, −6)
|
||||
/// toward (74.685, −39.988, −6), the resolve blocks with the SYNTHETIC
|
||||
/// reversed-movement normal (0.997, −0.078, −0.002) and out==in — every
|
||||
/// frame — the "shaking at the seam" report.
|
||||
///
|
||||
/// <para>
|
||||
/// Probe-traced chain: the foot sphere (tangent to the floor) crosses
|
||||
/// onto 0165's ramp floor; the ramp slab is double-faced and the
|
||||
/// UNDERSIDE face (poly 0, n=(−0.03,0,−1)) grazes the sphere within the
|
||||
/// hit threshold → recorded as a foot near-miss → neg-poly step-up
|
||||
/// dispatch with a downward normal → the nested step-up's walkable probe
|
||||
/// rejects the exactly-tangent ramp floor ([walkable-nearest]
|
||||
/// gap=−0.0000 overlapsSphere=False) → StepUpSlide →
|
||||
/// slide_sphere(downward normal vs up-facing contact plane) → the
|
||||
/// opposing-normals branch → Collided → revert. Repeat.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Fact(Skip = "#137 seam shake — deterministic RED repro; the post-step-up check-pos restore clobbers the lifted position (P2 stale-snapshot class). Un-skip with the fix.")]
|
||||
public void SeamShake_WestBoundary_SnapshotExact_Advances()
|
||||
{
|
||||
var datDir = FindDatDir();
|
||||
if (datDir is null)
|
||||
{
|
||||
_out.WriteLine("SKIP: dat directory not found");
|
||||
return;
|
||||
}
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
var engine = BuildCorridorEngine(dats);
|
||||
|
||||
// Body seeded EXACTLY from the capture's bodyBefore (tick 4101).
|
||||
var body = new PhysicsBody();
|
||||
body.ContactPlaneValid = true;
|
||||
body.ContactPlane = new Plane(Vector3.UnitZ, 6f);
|
||||
body.ContactPlaneCellId = SeamCellWest;
|
||||
body.TransientState |= TransientStateFlags.Contact | TransientStateFlags.OnWalkable;
|
||||
body.WalkablePolygonValid = true;
|
||||
body.WalkablePlane = new Plane(Vector3.UnitZ, 6f);
|
||||
body.WalkableUp = Vector3.UnitZ;
|
||||
body.WalkableVertices = new[]
|
||||
{
|
||||
new Vector3(75f, -38.33333f, -6f),
|
||||
new Vector3(75f, -41.66667f, -6f),
|
||||
new Vector3(78.33333f, -41.66667f, -6f),
|
||||
new Vector3(78.33333f, -38.33333f, -6f),
|
||||
};
|
||||
|
||||
var from = new Vector3(75.28674f, -40.03537f, -6f);
|
||||
var to = new Vector3(74.6854f, -39.988018f, -6f);
|
||||
|
||||
// Emit the same step-level probes the live session logged so the
|
||||
// offline trace can be line-diffed against launch-137-seam-probes.log
|
||||
// — the first divergent line names the state the replay is missing.
|
||||
var probeBuffer = new System.IO.StringWriter();
|
||||
var prevOut = Console.Out;
|
||||
ResolveResult r1;
|
||||
try
|
||||
{
|
||||
Console.SetOut(probeBuffer);
|
||||
PhysicsDiagnostics.ProbeStepWalkEnabled = true;
|
||||
PhysicsDiagnostics.ProbePushBackEnabled = true;
|
||||
PhysicsDiagnostics.ProbeIndoorBspEnabled = true;
|
||||
|
||||
r1 = engine.ResolveWithTransition(
|
||||
currentPos: from,
|
||||
targetPos: to,
|
||||
cellId: SeamCellWest,
|
||||
sphereRadius: 0.48f,
|
||||
sphereHeight: 1.2f,
|
||||
stepUpHeight: 0.6f, // live Setup values from the capture
|
||||
stepDownHeight: 1.5f,
|
||||
isOnGround: true,
|
||||
body: body,
|
||||
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PhysicsDiagnostics.ProbeStepWalkEnabled = false;
|
||||
PhysicsDiagnostics.ProbePushBackEnabled = false;
|
||||
PhysicsDiagnostics.ProbeIndoorBspEnabled = false;
|
||||
Console.SetOut(prevOut);
|
||||
}
|
||||
_out.WriteLine(probeBuffer.ToString());
|
||||
|
||||
_out.WriteLine($"r1: ok={r1.Ok} out=({r1.Position.X:F3},{r1.Position.Y:F3},{r1.Position.Z:F3}) " +
|
||||
$"cell=0x{r1.CellId:X8} hit={r1.CollisionNormalValid} " +
|
||||
$"n=({r1.CollisionNormal.X:F2},{r1.CollisionNormal.Y:F2},{r1.CollisionNormal.Z:F2}) " +
|
||||
$"bodySliding={body.TransientState.HasFlag(TransientStateFlags.Sliding)}");
|
||||
|
||||
Assert.True(r1.Position.X < from.X - 0.3f,
|
||||
$"The westward boundary crossing onto the ramp must advance " +
|
||||
$"({from.X:F3} → {r1.Position.X:F3}, target {to.X:F3}); zero " +
|
||||
$"advance with the reversed-movement normal = the seam shake.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2026-07-06 gate session repro (launch-137-corridor-gate.log): standing
|
||||
/// at (84.851, −39.764, −6.000) — the foot sphere already straddling the
|
||||
/// x=85 cell boundary by 0.33 m — the first move attempt toward
|
||||
/// (85.453, −39.782) blocked with the synthetic reversed-movement normal
|
||||
/// (−1.00, 0.03, −0.02), out==in, cp lost (cp=none), and repeated every
|
||||
/// frame (the "shaking at the seam" report). The deeper straddle start is
|
||||
/// what the original replay frame (84.638 → 85.253) didn't cover.
|
||||
/// </summary>
|
||||
[Fact(Skip = "#137 seam shake — deterministic RED repro; the post-step-up check-pos restore clobbers the lifted position (P2 stale-snapshot class). Un-skip with the fix.")]
|
||||
public void SeamCrossing_FromDeepStraddleStart_Advances()
|
||||
{
|
||||
var datDir = FindDatDir();
|
||||
if (datDir is null)
|
||||
{
|
||||
_out.WriteLine("SKIP: dat directory not found");
|
||||
return;
|
||||
}
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
var engine = BuildCorridorEngine(dats);
|
||||
var body = GroundedBody();
|
||||
|
||||
var from = new Vector3(84.851f, -39.764f, -6.000f);
|
||||
var to = new Vector3(85.453f, -39.782f, -6.000f);
|
||||
|
||||
var r1 = Resolve(engine, body, from, to, SeamCellWest);
|
||||
_out.WriteLine($"r1: ok={r1.Ok} out=({r1.Position.X:F3},{r1.Position.Y:F3},{r1.Position.Z:F3}) " +
|
||||
$"cell=0x{r1.CellId:X8} hit={r1.CollisionNormalValid} " +
|
||||
$"n=({r1.CollisionNormal.X:F2},{r1.CollisionNormal.Y:F2},{r1.CollisionNormal.Z:F2}) " +
|
||||
$"bodySliding={body.TransientState.HasFlag(TransientStateFlags.Sliding)} " +
|
||||
$"bodyCpValid={body.ContactPlaneValid}");
|
||||
|
||||
Assert.True(r1.Position.X > from.X + 0.2f,
|
||||
$"The straddling-start seam crossing must advance " +
|
||||
$"({from.X:F3} → {r1.Position.X:F3}); zero advance with a " +
|
||||
$"reversed-movement normal = the 2026-07-06 seam shake.");
|
||||
}
|
||||
|
||||
[Fact(Skip = "#137 seam shake — deterministic RED repro; the post-step-up check-pos restore clobbers the lifted position (P2 stale-snapshot class). Un-skip with the fix.")]
|
||||
public void SeamCrossing_DoesNotPersistSyntheticSlidingNormal_AndRunContinues()
|
||||
{
|
||||
var datDir = FindDatDir();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue