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