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>
265 lines
14 KiB
C#
265 lines
14 KiB
C#
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);
|
|
}
|
|
}
|