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. /// [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); Assert.Equal(latched, body.Position); 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})"); Assert.Equal(latched.Z, body.Position.Z, 4); // From the next tick on the body climbs normally. harness.Tick(1, UphillRootMotionPerTick); Assert.True( body.Position.Z > latched.Z, $"body did not climb once the latch cleared (pos {body.Position})"); } }