test(physics): S6 — the camera provably reaches both PerfectClip TOI tails; contained, not dormant
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
AP-83/AP-91 claimed no current mover sets PerfectClip. The containment proof found the opposite and the contract's honest-fallback fired: the camera probe (the sole production setter) reaches BOTH ACE-derived tails live — the viewer exemption is creature-only, the shadow-list walk is unconditional, and static scenery with authored primitives is a real non-creature population. Every reach is now recorded (camera-live silently; any non-viewer mover loudly, one-shot), so a future flag change cannot exercise unreviewed ACE-derived math silently. Four tests drive the camera's exact call shape both ways; the sabotage was intelligently adapted — there was no existing cut to disable, so it flips the one axis the proof depends on (IsCreature) and asserts reachability inverts. Both register rows rewritten CONTAINED-not-dormant with severity narrowed to camera-feel (the probe never commits a PhysicsBody). Landing note: diagnostics-only diff (two guard calls + counters + corrected stale comments), verified directly by the session lead rather than a review cycle — the review budget went where behaviour changed tonight. Campaign S CLOSES with this landing: S1A/S1B/S2/S4/S5/S6 done, S3 cancelled, three user-passed gates, one honestly-open item — AD-66's reland, twice self-refused by its own stability gate, blocked on the #341 codegen-shape measurement instability whose ABA evidence and first discriminating experiment are filed. Clean-room suite: 11,257 passed / 6 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
00c03a33a5
commit
c5443b3df9
5 changed files with 425 additions and 10 deletions
|
|
@ -0,0 +1,265 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// S6 (Campaign S, 2026-08-07) — reachability proof + containment guard for
|
||||
/// the AP-83/AP-91 PerfectClip time-of-impact tails
|
||||
/// (<c>TransitionTypes.CylCollideWithPoint</c> / <c>SphereCollideWithPoint</c>).
|
||||
/// Contract: docs/research/2026-08-07-s6-perfectclip-containment-contract.md.
|
||||
///
|
||||
/// <para>
|
||||
/// FINDING (not a failure — the contract explicitly anticipates this outcome):
|
||||
/// BOTH scoping-doc candidate arguments for "the camera can never reach these
|
||||
/// tails" fail. <c>CollisionExemption.ShouldSkip</c> only exempts a viewer
|
||||
/// mover against a CREATURE target (CollisionExemption.cs:91-94) — a
|
||||
/// non-creature Cyl/Sphere shadow entry passes straight through. And
|
||||
/// <c>FindObjCollisionsInCell</c> walks every cell's shadow list
|
||||
/// unconditionally, with no viewer skip-all gate before the per-target loop
|
||||
/// (TransitionTypes.cs:3806, reached from <c>FindPrimaryCellCollisions</c> with
|
||||
/// no mover-flag check, TransitionTypes.cs:2477). So the camera
|
||||
/// (<c>PhysicsCameraCollisionProbe.SweepEye</c> — the ONLY production
|
||||
/// PerfectClip setter, grep-confirmed against the whole src tree) DOES reach
|
||||
/// both TOI tails whenever its PathClipped, never-grounded foot sphere
|
||||
/// (moverFlags carry PathClipped; body=null + isOnGround=false means neither
|
||||
/// Contact nor OnWalkable is ever seeded) overlaps a non-creature Cyl/Sphere
|
||||
/// shadow entry. That population is real production content: static
|
||||
/// landblock scenery with a CylSphere/Sphere and no physics BSP registers
|
||||
/// with <c>EntityCollisionFlags.None</c>
|
||||
/// (<c>LandblockPhysicsPublisher.PublishStaticEntity</c>, tracked live via
|
||||
/// <c>publication.CylinderOwnerCount</c>).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// These tests drive the REAL production resolve
|
||||
/// (<c>PhysicsEngine.ResolveWithTransition</c>) with the camera's exact call
|
||||
/// shape (IsViewer|PathClipped|FreeRotate|PerfectClip, single 0.3 m sphere,
|
||||
/// body=null, isOnGround=false — mirrors <c>PhysicsCameraCollisionProbe
|
||||
/// .SweepEye</c> and the harness already used by
|
||||
/// <c>CameraCornerSealReplayTests</c> / <c>Issue180CorridorSweepHysteresis
|
||||
/// ReplayTests</c>) against a synthetic Cyl/Sphere shadow entry, and assert
|
||||
/// the guard (<c>PhysicsDiagnostics.RecordCylPerfectClipTailReach</c> /
|
||||
/// <c>RecordSpherePerfectClipTailReach</c>) records the reach as camera-live,
|
||||
/// never as unverified.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Sabotage</b> (deliverable 3): the contract's literal wording is "disable
|
||||
/// the exemption that cuts the chain and watch the reachability flip" — but
|
||||
/// the proof found NOTHING currently cuts the chain for a non-creature
|
||||
/// target, so there is no existing cut to disable. The faithful adaptation
|
||||
/// flips the SAME one axis <c>CollisionExemption.ShouldSkip</c> actually
|
||||
/// reads (<c>EntityCollisionFlags.IsCreature</c>) from off to on and asserts
|
||||
/// reachability inverts: the guard goes silent and the camera walks straight
|
||||
/// through, exactly the retail "camera ray ignores creatures" rule
|
||||
/// (acclient_2013_pseudo_c.txt:276787-276790). This proves the guard is wired
|
||||
/// to the actual condition the reachability proof depends on, not merely
|
||||
/// "always fires regardless of input".
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class S6PerfectClipTailContainmentTests
|
||||
{
|
||||
private readonly ITestOutputHelper _out;
|
||||
public S6PerfectClipTailContainmentTests(ITestOutputHelper output) => _out = output;
|
||||
|
||||
private const uint TestLandblockId = 0xA9B40000u;
|
||||
private const uint TestCellId = TestLandblockId | 0x0001u; // landcell (0,0), matches CylSphereFamilyTests/SphereCollisionFamilyTests
|
||||
|
||||
private const float ViewerSphereRadius = 0.3f; // retail viewer_sphere (acclient :93314)
|
||||
|
||||
private static readonly ObjectInfoState CameraMoverFlags =
|
||||
ObjectInfoState.IsViewer | ObjectInfoState.PathClipped
|
||||
| ObjectInfoState.FreeRotate | ObjectInfoState.PerfectClip;
|
||||
|
||||
// ───────────────────────────────────────────────────────────────
|
||||
// Sphere tail (AP-91)
|
||||
// ───────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CameraSweep_HitsNonCreatureSphereProp_ReachesTail_RecordsCameraLive()
|
||||
{
|
||||
PhysicsDiagnostics.ResetPerfectClipTailGuardForTest();
|
||||
var engine = BuildEngine();
|
||||
RegisterSphere(engine, 0x00005001u, new Vector3(12f, 14f, 1.0f), radius: 1.0f,
|
||||
flags: EntityCollisionFlags.None); // static prop, not a creature
|
||||
|
||||
Vector3 pivot = new(12f, 10f, 1.0f);
|
||||
Vector3 eye = new(12f, 20f, 1.0f); // straight through the prop's center
|
||||
|
||||
var r = SweepViewer(engine, pivot, eye, TestCellId);
|
||||
|
||||
_out.WriteLine(FormattableString.Invariant($"ok={r.Ok} pos=({r.Position.X:F3},{r.Position.Y:F3},{r.Position.Z:F3}) collNorm={r.CollisionNormalValid} normal=({r.CollisionNormal.X:F3},{r.CollisionNormal.Y:F3},{r.CollisionNormal.Z:F3}) cameraLive={PhysicsDiagnostics.SphereToiCameraLiveCount} unverified={PhysicsDiagnostics.SphereToiUnverifiedCount}"));
|
||||
|
||||
Assert.True(PhysicsDiagnostics.SphereToiCameraLiveCount > 0,
|
||||
"The camera's PathClipped, never-grounded sweep must reach the Sphere PerfectClip TOI tail (AP-91) for a non-creature target.");
|
||||
Assert.Equal(0, PhysicsDiagnostics.SphereToiUnverifiedCount);
|
||||
|
||||
// Golden: the sphere prop must have stopped the camera's forward sweep
|
||||
// (PathClipped hard-stops at first contact) well short of the far side.
|
||||
Assert.True(r.Position.Y < 14f,
|
||||
$"camera must be stopped by the prop, not pass through it; got Y={r.Position.Y:F3}");
|
||||
Assert.True(r.Position.Y > 11f,
|
||||
$"camera must actually reach the prop's surface, not stop early; got Y={r.Position.Y:F3}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CameraSweep_HitsCreatureFlaggedSphere_ExemptionCutsChain_GuardStaysSilent()
|
||||
{
|
||||
PhysicsDiagnostics.ResetPerfectClipTailGuardForTest();
|
||||
var engine = BuildEngine();
|
||||
// SABOTAGE: flip the one axis CollisionExemption.ShouldSkip reads for a
|
||||
// viewer mover — everything else about the geometry is identical to the
|
||||
// reached case above.
|
||||
RegisterSphere(engine, 0x00005002u, new Vector3(12f, 14f, 1.0f), radius: 1.0f,
|
||||
flags: EntityCollisionFlags.IsCreature);
|
||||
|
||||
Vector3 pivot = new(12f, 10f, 1.0f);
|
||||
Vector3 eye = new(12f, 20f, 1.0f);
|
||||
|
||||
var r = SweepViewer(engine, pivot, eye, TestCellId);
|
||||
|
||||
_out.WriteLine(FormattableString.Invariant($"ok={r.Ok} pos=({r.Position.X:F3},{r.Position.Y:F3},{r.Position.Z:F3}) cameraLive={PhysicsDiagnostics.SphereToiCameraLiveCount}"));
|
||||
|
||||
Assert.Equal(0, PhysicsDiagnostics.SphereToiCameraLiveCount);
|
||||
Assert.Equal(0, PhysicsDiagnostics.SphereToiUnverifiedCount);
|
||||
// Reachability flips: CollisionExemption now exempts the whole target
|
||||
// for the viewer mover before shape dispatch, so the camera sails
|
||||
// straight through to the far side (retail: camera ray ignores
|
||||
// creatures, acclient_2013_pseudo_c.txt:276787-276790).
|
||||
Assert.True(r.Position.Y > 19f,
|
||||
$"a creature-flagged prop must be fully exempt for a viewer mover; got Y={r.Position.Y:F3}");
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────
|
||||
// Cyl tail (AP-83)
|
||||
// ───────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CameraSweep_HitsNonCreatureCylinderProp_ReachesTail_RecordsCameraLive()
|
||||
{
|
||||
PhysicsDiagnostics.ResetPerfectClipTailGuardForTest();
|
||||
var engine = BuildEngine();
|
||||
RegisterCylinder(engine, 0x00006001u, new Vector3(12f, 14f, 0f), radius: 1.0f, height: 2.0f,
|
||||
flags: EntityCollisionFlags.None);
|
||||
|
||||
Vector3 pivot = new(12f, 10f, 1.0f);
|
||||
Vector3 eye = new(12f, 20f, 1.0f);
|
||||
|
||||
var r = SweepViewer(engine, pivot, eye, TestCellId);
|
||||
|
||||
_out.WriteLine(FormattableString.Invariant($"ok={r.Ok} pos=({r.Position.X:F3},{r.Position.Y:F3},{r.Position.Z:F3}) collNorm={r.CollisionNormalValid} normal=({r.CollisionNormal.X:F3},{r.CollisionNormal.Y:F3},{r.CollisionNormal.Z:F3}) cameraLive={PhysicsDiagnostics.CylToiCameraLiveCount} unverified={PhysicsDiagnostics.CylToiUnverifiedCount}"));
|
||||
|
||||
Assert.True(PhysicsDiagnostics.CylToiCameraLiveCount > 0,
|
||||
"The camera's PathClipped, never-grounded sweep must reach the Cyl PerfectClip TOI tail (AP-83) for a non-creature target.");
|
||||
Assert.Equal(0, PhysicsDiagnostics.CylToiUnverifiedCount);
|
||||
|
||||
Assert.True(r.Position.Y < 14f,
|
||||
$"camera must be stopped by the prop, not pass through it; got Y={r.Position.Y:F3}");
|
||||
Assert.True(r.Position.Y > 11f,
|
||||
$"camera must actually reach the prop's surface, not stop early; got Y={r.Position.Y:F3}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CameraSweep_HitsCreatureFlaggedCylinder_ExemptionCutsChain_GuardStaysSilent()
|
||||
{
|
||||
PhysicsDiagnostics.ResetPerfectClipTailGuardForTest();
|
||||
var engine = BuildEngine();
|
||||
RegisterCylinder(engine, 0x00006002u, new Vector3(12f, 14f, 0f), radius: 1.0f, height: 2.0f,
|
||||
flags: EntityCollisionFlags.IsCreature);
|
||||
|
||||
Vector3 pivot = new(12f, 10f, 1.0f);
|
||||
Vector3 eye = new(12f, 20f, 1.0f);
|
||||
|
||||
var r = SweepViewer(engine, pivot, eye, TestCellId);
|
||||
|
||||
_out.WriteLine(FormattableString.Invariant($"ok={r.Ok} pos=({r.Position.X:F3},{r.Position.Y:F3},{r.Position.Z:F3}) cameraLive={PhysicsDiagnostics.CylToiCameraLiveCount}"));
|
||||
|
||||
Assert.Equal(0, PhysicsDiagnostics.CylToiCameraLiveCount);
|
||||
Assert.Equal(0, PhysicsDiagnostics.CylToiUnverifiedCount);
|
||||
Assert.True(r.Position.Y > 19f,
|
||||
$"a creature-flagged prop must be fully exempt for a viewer mover; got Y={r.Position.Y:F3}");
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────
|
||||
// Harness (mirrors CylSphereFamilyTests / SphereCollisionFamilyTests)
|
||||
// ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static PhysicsEngine BuildEngine()
|
||||
{
|
||||
var cache = new PhysicsDataCache();
|
||||
var engine = new PhysicsEngine { DataCache = cache };
|
||||
|
||||
// Flat terrain at Z=0 across the whole landblock — irrelevant to these
|
||||
// sweeps (they travel at Z≈1.0, well above ground) but required so the
|
||||
// outdoor cell resolves.
|
||||
var heights = new byte[81];
|
||||
var heightTable = new float[256]; // all zero → terrain Z = 0
|
||||
engine.AddLandblock(
|
||||
landblockId: TestLandblockId,
|
||||
terrain: new TerrainSurface(heights, heightTable),
|
||||
cells: Array.Empty<CellSurface>(),
|
||||
portals: Array.Empty<PortalPlane>(),
|
||||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f);
|
||||
|
||||
return engine;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirror of PhysicsCameraCollisionProbe.SweepEye's transition call — same
|
||||
/// shape already used by CameraCornerSealReplayTests,
|
||||
/// Issue180CorridorSweepHysteresisReplayTests, and CylSphereFamilyTests'
|
||||
/// landblock (0x0100 low word stays below the AdjustPosition threshold, so
|
||||
/// the outdoor cellId is used as-is).
|
||||
/// </summary>
|
||||
private static ResolveResult SweepViewer(PhysicsEngine engine, Vector3 pivot, Vector3 desiredEye, uint cellId)
|
||||
{
|
||||
Vector3 begin = pivot - new Vector3(0f, 0f, ViewerSphereRadius);
|
||||
Vector3 end = desiredEye - new Vector3(0f, 0f, ViewerSphereRadius);
|
||||
|
||||
return engine.ResolveWithTransition(
|
||||
currentPos: begin,
|
||||
targetPos: end,
|
||||
cellId: cellId,
|
||||
sphereRadius: ViewerSphereRadius,
|
||||
sphereHeight: 0f,
|
||||
stepUpHeight: 0f,
|
||||
stepDownHeight: 0f,
|
||||
isOnGround: false,
|
||||
body: null,
|
||||
moverFlags: CameraMoverFlags,
|
||||
movingEntityId: 0);
|
||||
}
|
||||
|
||||
private static void RegisterSphere(PhysicsEngine engine, uint entityId, Vector3 worldPos,
|
||||
float radius, EntityCollisionFlags flags)
|
||||
{
|
||||
engine.ShadowObjects.Register(
|
||||
entityId, gfxObjId: 0u,
|
||||
worldPos, Quaternion.Identity, radius,
|
||||
worldOffsetX: 0f, worldOffsetY: 0f, landblockId: TestLandblockId,
|
||||
collisionType: ShadowCollisionType.Sphere,
|
||||
cylHeight: 0f, scale: 1f,
|
||||
state: 0u,
|
||||
flags: flags,
|
||||
isStatic: true);
|
||||
}
|
||||
|
||||
private static void RegisterCylinder(PhysicsEngine engine, uint entityId, Vector3 worldPos,
|
||||
float radius, float height, EntityCollisionFlags flags)
|
||||
{
|
||||
engine.ShadowObjects.Register(
|
||||
entityId, gfxObjId: 0u,
|
||||
worldPos, Quaternion.Identity, radius,
|
||||
worldOffsetX: 0f, worldOffsetY: 0f, landblockId: TestLandblockId,
|
||||
collisionType: ShadowCollisionType.Cylinder,
|
||||
cylHeight: height, scale: 1f,
|
||||
state: 0u,
|
||||
flags: flags,
|
||||
isStatic: true);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue