using System.Numerics; using AcDream.Core.Physics; namespace AcDream.Runtime.Tests.Physics; /// /// #331 — the coverage whose absence made the issue invisible: nothing in /// the suite asserted that a body-bearing mover makes UPHILL progress on a /// walkable slope. Every slope assertion we had ran downhill /// (), and the one uphill test /// that was written passed vacuously because the body never moved. /// /// Both tests here drive the production /// tick over /// and take their expected Z from the /// fixture's own terrain, never from a re-implementation of the projection. /// /// /// What #331 turned out to be (2026-08-06). The resolver does not /// refuse uphill motion. It refuses a sub-step offset that is /// exactly anti-parallel to a persisted sliding normal — the /// #137-family absorb, already recorded as retail-faithful in /// claude-memory/project_physics_collision_digest.md. The chain is: /// a landing (or spawn settle) on a slope reaches /// OBJECTINFO::validate_walkable with state & 1 (CONTACT) /// clear, so it calls COLLISIONINFO::set_collision_normal with the /// terrain plane normal (verified in the PDB-paired binary at /// 0x0050d251-0x0050d26c); CTransition::validate_transition then /// unconditionally converts that to a sliding normal /// (0x0050ac19-0x0050ac30), and COLLISIONINFO::set_sliding_normal /// flattens Z and re-normalizes (0x0050a060) — so even a 1° /// slope yields a full-length horizontal normal pointing downhill. /// SetPositionInternal persists it as SLIDING_TS /// (0x005154c2/0x005154e1), get_object_info re-seeds it next /// frame (0x00511d44), and CTransition::adjust_offset projects /// the step onto the crease cross(sliding, contact) — 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 /// (0x0050c0ed: test ebx,ebx / jne — retail returns /// i != 0 && state == OK, exactly as acdream does), and because a /// failed transition never reaches the writeback the sliding state is never /// cleared. Latched. /// /// Why it read as "ALL uphill motion". /// 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 F_EPSILON 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. /// public sealed class RuntimeRemoteUphillProgressTests { /// /// Same ramp the downhill tests use: normal Z = 1/sqrt(1.36) ≈ 0.8575 /// (30.96°) against retail's 0.6642 floor_z 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. /// private const float WalkableSlopeGradient = 0.6f; private const int TrackedTicks = 30; /// /// 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. /// private static readonly Vector3 UphillRootMotionPerTick = new(0.02588f, -0.09659f, 0f); /// Exactly up-slope: the #331 probe's offset. private static readonly Vector3 ExactlyUpSlopeRootMotionPerTick = new(0f, -0.1f, 0f); /// /// Same band the downhill tracking test uses. Measured drift on this /// fixture is under 1e-4 m. /// private const float SurfaceTrackingToleranceMeters = 0.005f; /// /// THE MISSING COVERAGE. A remote with a live /// running up a walkable slope must gain height every tick and keep its /// feet on the ground while doing it. /// /// Asserted per tick, not start-to-end, so a body that stalls for /// part of the run and catches up later still fails. /// /// Sabotage-verified 2026-08-06, both directions. /// (SAB-A1) Transition.AdjustOffsetreturn Vector3.Zero; /// 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. /// [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"); } /// /// 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. /// /// Sabotage-verified 2026-08-06, both directions. /// (SAB-B1) deleting the get_object_info sliding-normal seed in /// PhysicsEngine.ResolveWithTransition reddens the absorb assertion /// — the body climbs to z 57.7544 instead of standing still — while /// leaving the sibling test green. (SAB-A1) AdjustOffset → /// Vector3.Zero reddens the escape/climb assertions. (SAB-A2) a /// flat ramp reddens the sliding-normal expectation. /// /// What does NOT discriminate here, measured, so nobody infers /// it later: making the FINAL tick exactly up-slope leaves this green. /// By then the preceding off-gradient tick has already succeeded and its /// writeback cleared SLIDING, so there is no persisted normal left /// to absorb against. The absorb needs a live latch, not a particular /// heading. /// /// AD-66 recalibration, issue #341 (2026-08-08 reland). With /// the bare-radius safety push live, the planted-but-not-yet-lifted spawn /// position (latchedValidateWalkable plants the sphere /// at perpendicular distance radius * Normal.Z from the contact /// plane, byte-faithfully; see the block comment on /// Transition.AdjustOffset's safety check) is no longer a fixed /// point of that check: dist < radius - EPSILON 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 /// r * (1/N.z - 1) for r = 0.48 (this harness's sphere /// radius) and N.z = 1/sqrt(1 + 0.6^2) = 0.857493 (this ramp's /// normal, per the class doc comment); the #341 boundary hunt measured /// the resulting Z bit-identically 37/37 times (bits 0x42667451 = /// 57.6135902...), reconfirmed by this reland's own ten-run gate. /// [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})"); } }