acdream/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteUphillProgressTests.cs
Erik 10efb5b1f9
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
fix(physics): AD-66 relands — the push-out uses retail's bare radius; plant-then-lift complete (#341 closed)
Third attempt, landed on evidence where the first two correctly refused:
the ten-run stability gate passed 10/10 bit-identical (0x42667451, two
clean-room cycles among the runs), the recalibrated golden's every value
measured with derivations rather than guessed, and the historical
measurement flip stands recorded as unexplained-but-unreproducible
after 37 hunt runs plus these 10 found no divergence anywhere.

The mechanism, completing the S4b byte-pin: validate_walkable plants
the sphere at perpendicular r*N.z (byte-faithful, untouched); this push
fires once per settle and lifts to tangent equilibrium dist=r, where
the trigger goes quiet — retail's slope hover, arriving via the push
exactly as the original substitution's own comment predicted retail
had. Sabotage: restoring radius*N.z reddens the discriminating
exact-value test verbatim. AD-65 conformance, the uphill no-flap
guard, and the #331 absorb pin all green untouched.

AD-66 retired (the campaign's last withheld row); AD-69's seam-frame
correction deliberately unbundled, stays active as its own follow-up.
Clean-room suite 11,267 / 4 / 0 — the suite's two AD-66 skips are gone.

User's "port the retail pair" decision is now fully executed; the
hover-look slope gate is the remaining acceptance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:01:15 +02:00

271 lines
14 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.Numerics;
using AcDream.Core.Physics;
namespace AcDream.Runtime.Tests.Physics;
/// <summary>
/// #331 — the coverage whose absence made the issue invisible: <b>nothing in
/// the suite asserted that a body-bearing mover makes UPHILL progress on a
/// walkable slope</b>. Every slope assertion we had ran downhill
/// (<see cref="RuntimeRemoteSlopeProjectionTests"/>), and the one uphill test
/// that was written passed <i>vacuously</i> because the body never moved.
///
/// <para>Both tests here drive the production
/// <see cref="AcDream.Runtime.Physics.RuntimeRemotePhysicsUpdater"/> tick over
/// <see cref="RemoteRampHarness"/> and take their expected Z from the
/// fixture's own terrain, never from a re-implementation of the projection.
/// </para>
///
/// <para><b>What #331 turned out to be (2026-08-06).</b> The resolver does not
/// refuse uphill motion. It refuses a sub-step offset that is
/// <i>exactly anti-parallel</i> to a persisted sliding normal — the
/// #137-family absorb, already recorded as retail-faithful in
/// <c>claude-memory/project_physics_collision_digest.md</c>. The chain is:
/// a landing (or spawn settle) on a slope reaches
/// <c>OBJECTINFO::validate_walkable</c> with <c>state &amp; 1</c> (CONTACT)
/// clear, so it calls <c>COLLISIONINFO::set_collision_normal</c> with the
/// <i>terrain</i> plane normal (verified in the PDB-paired binary at
/// <c>0x0050d251-0x0050d26c</c>); <c>CTransition::validate_transition</c> then
/// unconditionally converts that to a sliding normal
/// (<c>0x0050ac19-0x0050ac30</c>), and <c>COLLISIONINFO::set_sliding_normal</c>
/// flattens Z and <b>re-normalizes</b> (<c>0x0050a060</c>) — so even a 1°
/// slope yields a full-length horizontal normal pointing downhill.
/// <c>SetPositionInternal</c> persists it as <c>SLIDING_TS</c>
/// (<c>0x005154c2/0x005154e1</c>), <c>get_object_info</c> re-seeds it next
/// frame (<c>0x00511d44</c>), and <c>CTransition::adjust_offset</c> projects
/// the step onto the crease <c>cross(sliding, contact)</c> — a purely
/// horizontal, purely cross-slope axis. An exactly-up-slope offset has zero
/// component on that axis, so it is annihilated, the sweep aborts at step 0
/// (<c>0x0050c0ed</c>: <c>test ebx,ebx / jne</c> — retail returns
/// <c>i != 0 &amp;&amp; state == OK</c>, exactly as acdream does), and because a
/// failed transition never reaches the writeback the sliding state is never
/// cleared. Latched.</para>
///
/// <para><b>Why it read as "ALL uphill motion".</b> <see cref="RemoteRampHarness"/>
/// builds a ramp whose gradient is exactly along Y, and the probe that found
/// #331 pushed exactly along Y. Axis-aligned fixture × axis-aligned motion
/// hits the measure-zero anti-parallel case with probability 1. The escape
/// window is the retail <c>F_EPSILON</c> abort: the step needs
/// ≥ 0.0002 m of cross-slope component, i.e. a heading more than about 0.11°
/// off the exact gradient at a 0.1 m step. Measured on this fixture: 0.0001 m
/// of cross-slope stays latched, 0.001 m climbs 0.176 m in five ticks.</para>
/// </summary>
public sealed class RuntimeRemoteUphillProgressTests
{
/// <summary>
/// Same ramp the downhill tests use: normal Z = 1/sqrt(1.36) ≈ 0.8575
/// (30.96°) against retail's 0.6642 <c>floor_z</c> limit (48.4°), so the
/// slope is comfortably walkable and a failure to climb is unmistakable.
/// The ramp descends along +Y, so Y is uphill.
/// </summary>
private const float WalkableSlopeGradient = 0.6f;
private const int TrackedTicks = 30;
/// <summary>
/// Body-local root displacement per tick for a running remote: 0.1 m at
/// 30 Hz is a 3 m/s run, heading about 15° off the exact up-slope
/// direction — an ordinary heading, well outside the 0.11° absorb window
/// documented on the class.
/// </summary>
private static readonly Vector3 UphillRootMotionPerTick =
new(0.02588f, -0.09659f, 0f);
/// <summary>Exactly up-slope: the #331 probe's offset.</summary>
private static readonly Vector3 ExactlyUpSlopeRootMotionPerTick =
new(0f, -0.1f, 0f);
/// <summary>
/// Same band the downhill tracking test uses. Measured drift on this
/// fixture is under 1e-4 m.
/// </summary>
private const float SurfaceTrackingToleranceMeters = 0.005f;
/// <summary>
/// THE MISSING COVERAGE. A remote with a live <see cref="PhysicsBody"/>
/// running up a walkable slope must gain height every tick and keep its
/// feet on the ground while doing it.
///
/// <para>Asserted per tick, not start-to-end, so a body that stalls for
/// part of the run and catches up later still fails.</para>
///
/// <para><b>Sabotage-verified 2026-08-06</b>, both directions.
/// (SAB-A1) <c>Transition.AdjustOffset</c> → <c>return Vector3.Zero;</c>
/// reddens it at tick 1 with zero climb. (SAB-A2) flattening the fixture
/// ramp to gradient 0 reddens it at tick 1 (z 0.00000 → 0.00000), proving
/// the climb is not an artifact of the settle.</para>
/// </summary>
[Fact]
public void ARemoteWithABodyClimbsAWalkableSlopeAndKeepsItsFeetOnIt()
{
using RemoteRampHarness harness =
RemoteRampHarness.OnRamp(WalkableSlopeGradient);
PhysicsBody body = harness.Remote.Body;
Assert.True(body.OnWalkable);
// A body that has just landed — or been settled, which is the same
// thing compressed — carries SLIDING with the flattened contact
// normal (see the sibling test). Retail deletes the up-slope
// component of exactly ONE step against it, and that step clears the
// latch. Consume it explicitly, and assert it really was only one, so
// the climb assertions below measure steady-state running rather than
// silently tolerating a stall.
harness.Tick(1, UphillRootMotionPerTick);
Assert.True(
(body.TransientState & TransientStateFlags.Sliding) == 0,
"the landing sliding latch survived its first off-gradient step");
float startZ = body.Position.Z;
float previousZ = startZ;
// The settled resting offset between the body's root and the terrain
// directly beneath it. Measured, not assumed.
float restingOffset = body.Position.Z - harness.SurfaceZUnderBody();
for (int tick = 1; tick <= TrackedTicks; tick++)
{
harness.Tick(1, UphillRootMotionPerTick);
Assert.True(
body.Position.Z > previousZ,
$"tick {tick}: body gained no height running uphill "
+ $"(z {previousZ:F5} -> {body.Position.Z:F5}, pos {body.Position})");
float offset = body.Position.Z - harness.SurfaceZUnderBody();
Assert.True(
MathF.Abs(offset - restingOffset) < SurfaceTrackingToleranceMeters,
$"tick {tick}: body root sits {offset:F5} m above the terrain "
+ $"under it, expected {restingOffset:F5} m (pos {body.Position})");
previousZ = body.Position.Z;
}
float ascent = body.Position.Z - startZ;
Assert.True(
ascent > 1.0f,
$"fixture is not exercising slope ascent: dz = {ascent:F4} m");
}
/// <summary>
/// Characterization pin for the #331 absorb itself, so the next person to
/// hit it finds the answer instead of re-deriving it. This asserts
/// RETAIL-FAITHFUL behaviour (every link verified in the PDB-paired binary
/// — see the class doc comment); it is NOT an approved-defect marker and
/// must not be "fixed" by loosening the small-offset abort or clearing the
/// sliding state per frame. Both of those are explicitly on the #137
/// DO-NOT-RETRY list; the lever, if one is ever wanted, is the PROVENANCE
/// of the sliding normal.
///
/// <para><b>Sabotage-verified 2026-08-06</b>, both directions.
/// (SAB-B1) deleting the <c>get_object_info</c> sliding-normal seed in
/// <c>PhysicsEngine.ResolveWithTransition</c> reddens the absorb assertion
/// — the body climbs to z 57.7544 instead of standing still — while
/// leaving the sibling test green. (SAB-A1) <c>AdjustOffset</c> →
/// <c>Vector3.Zero</c> reddens the escape/climb assertions. (SAB-A2) a
/// flat ramp reddens the sliding-normal expectation.</para>
///
/// <para><b>What does NOT discriminate here, measured, so nobody infers
/// it later:</b> making the FINAL tick exactly up-slope leaves this green.
/// By then the preceding off-gradient tick has already succeeded and its
/// writeback cleared <c>SLIDING</c>, so there is no persisted normal left
/// to absorb against. The absorb needs a live latch, not a particular
/// heading.</para>
///
/// <para><b>AD-66 recalibration, issue #341 (2026-08-08 reland).</b> With
/// the bare-radius safety push live, the planted-but-not-yet-lifted spawn
/// position (<c>latched</c> — <c>ValidateWalkable</c> plants the sphere
/// at perpendicular distance <c>radius * Normal.Z</c> from the contact
/// plane, byte-faithfully; see the block comment on
/// <c>Transition.AdjustOffset</c>'s safety check) is no longer a fixed
/// point of that check: <c>dist &lt; radius - EPSILON</c> holds there, so
/// the push fires. XY still latches EXACTLY — the absorbed up-slope
/// offset still projects to zero on the pure cross-slope crease, and the
/// push is a pure +Z addition — but Z is lifted ONCE, on the first of the
/// five absorbed ticks, to tangent equilibrium (perpendicular distance ==
/// the bare radius), then the trigger goes quiet for ticks 2-5 (the
/// plant-then-lift mechanism: the PUSH supplies the one-time lift, not
/// the plant, exactly as the original AD-66 comment argued). The lift is
/// <c>r * (1/N.z - 1)</c> for <c>r = 0.48</c> (this harness's sphere
/// radius) and <c>N.z = 1/sqrt(1 + 0.6^2) = 0.857493</c> (this ramp's
/// normal, per the class doc comment); the #341 boundary hunt measured
/// the resulting Z bit-identically 37/37 times (bits <c>0x42667451</c> =
/// 57.6135902...), reconfirmed by this reland's own ten-run gate.</para>
/// </summary>
[Fact]
public void AnExactlyUpSlopeOffsetIsAbsorbedByThePersistedSlidingNormal()
{
using RemoteRampHarness harness =
RemoteRampHarness.OnRamp(WalkableSlopeGradient);
PhysicsBody body = harness.Remote.Body;
// The state a landing (or the spawn settle that compresses it) leaves
// behind on any slope: SLIDING carrying the contact plane's normal
// flattened to XY and re-normalized — here the ramp's exact downhill
// direction, at full length despite the slope being only 31°.
Assert.True((body.TransientState & TransientStateFlags.Sliding) != 0);
Assert.True(
Vector3.Distance(body.SlidingNormal, new Vector3(0f, 1f, 0f)) < 0.001f,
$"expected the flattened ramp normal, got {body.SlidingNormal}");
Vector3 latched = body.Position;
harness.Tick(5, ExactlyUpSlopeRootMotionPerTick);
// AD-66 (#341): X and Y still latch EXACTLY — measured bit-for-bit
// identical to `latched` (SingleToUInt32Bits equal) both during this
// recalibration and across the reland's ten-run gate. Z no longer
// latches: the bare-radius safety push fires once (see the class doc
// comment above for the derivation) lifting the planted spawn
// position to tangent equilibrium. The formula below reproduces the
// measured value to within 4 decimal places — it differs from the
// engine's actual step-by-step float computation by ~3 ULP, since
// the real `dist` the engine measures is not bit-identical to the
// idealized `radius * N.z` algebra — so the comparison uses the same
// decimal-place tolerance as the rest of this class rather than an
// exact bit compare. The exact measured bits, stable 37/37 in the
// #341 boundary hunt and across this gate, are 0x42667451.
Assert.Equal(latched.X, body.Position.X);
Assert.Equal(latched.Y, body.Position.Y);
float expectedLiftedZ = latched.Z + 0.48f * (1f / 0.857493f - 1f);
Assert.Equal(expectedLiftedZ, body.Position.Z, 4);
Assert.True((body.TransientState & TransientStateFlags.Sliding) != 0);
// One ordinary off-gradient tick is itself absorbed — the crease is the
// pure cross-slope axis, so only the X component survives — but it
// succeeds, so the writeback clears the latch.
harness.Tick(1, UphillRootMotionPerTick);
Assert.True((body.TransientState & TransientStateFlags.Sliding) == 0);
Assert.True(
body.Position.X > latched.X,
$"the cross-slope component was absorbed too (pos {body.Position})");
// Post-off-gradient Z (AD-66/#341): this successful step re-plants
// the body via ValidateWalkable at the NEW ground contact point,
// undoing the transient lift — the one-time push does not carry
// forward across a re-plant. The step only moved X (Y is unchanged
// by the same cross-slope-only crease as above), and this ramp's
// height depends on Y alone (RemoteRampHarness.Ramp's heightmap
// varies only with y), so the freshly-planted resting Z at the same
// Y is the SAME planted distance (radius * Normal.Z) as the original
// `latched.Z`. Measured 57.53382 against latched.Z's 57.53383, a
// ~1e-5 difference from the resolve pipeline's own floating-point
// accumulation — comfortably inside the existing 4-decimal-place
// tolerance, unchanged by the relanding.
Assert.Equal(latched.Z, body.Position.Z, 4);
// From the next tick on the body climbs normally.
harness.Tick(1, UphillRootMotionPerTick);
// Final climb baseline (AD-66/#341): measured Z = 57.576443, a
// ~0.0426 m climb over the latched baseline in one ordinary tick —
// comfortably clear of both `latched.Z` and the transient lifted
// value. Unlike the two fixed points above, this keeps moving tick
// over tick and is not itself a stable quantity to pin exactly, so
// the loose lower-bound remains the right assertion shape.
Assert.True(
body.Position.Z > latched.Z,
$"body did not climb once the latch cleared (pos {body.Position})");
}
}