using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using Xunit; using Xunit.Abstractions; namespace AcDream.Core.Tests.Physics; /// /// TS-4 RETIRED (Campaign P final physics slice, 2026-07-30; /// docs/research/2026-07-30-ts4-116-oracle-plan.md §1, §4 item 2). The /// Path-6 steep-poly slide-tangent shortcut (worldNormal.Z < FloorZ → /// project-and-Slid, with its own SetSlidingNormal write) is deleted from /// 's Path 6; both sphere0 and sphere1 steep hits now /// fall through to the same unconditional SetCollide retail uses for /// every hit (acclient_2013_pseudo_c.txt:323783-323821, 0x0053a793 — no /// steepness test at the BSP layer at all). /// /// /// This class builds a dat-free multi-frame replay from the existing /// geometry (a 63.4° slope, /// normal.Z ≈ 0.447 — below PhysicsGlobals.FloorZ ≈ 0.6642 but above /// PhysicsGlobals.LandingZ ≈ 0.0871, exactly the band the retired /// shortcut used to target) using the same /// PhysicsEngine.ResolveWithTransition multi-frame replay idiom as /// Issue185OutdoorStairsSeamReplayTests, gravity integrated between /// resolves exactly as PhysicsBody.UpdatePhysicsInternal would, for /// up to 3 simulated seconds (90 ticks at 30 Hz — retail's physics tick /// rate, #32 L.5). /// /// /// /// The oracle plan's own root-cause trace (§1.2) found the freeze /// mechanism one layer downstream of Path 6: Path 6's faithful /// SetCollide doesn't reposition the sphere; the immediate retry /// routes to Path 4 (find_walkable), which commits a real steep /// ContactPlane via the permissive LandingZ gate; the NEXT /// tick's AdjustOffset then projects the (by-then gravity-only) /// offset through Cross(ContactPlane.Normal, SlidingNormal) — and /// for a PURELY VERTICAL offset that cross product annihilates it exactly /// (§1.2 Step E), tripping the abort-small-offset guard before /// TransitionalInsert can run again. This is present identically in /// the raw retail decomp, ACE's port, and this port (§1.2, §1.3) — it is /// not a bug, it is what a truly zero-horizontal-velocity plumb drop onto a /// steep surface does in every one of the three references. A live player /// almost never produces this input (WASD, camera-relative movement, and /// even float noise inject some horizontal component), which is why the /// 2026-04-30 live-client debugger trace that originally validated the /// shortcut never observed the freeze. /// /// /// /// Two fixtures, two different fates, per the plan's own decisive test /// (§4 item 2): /// is the DEGENERATE case (zero horizontal velocity) — PINNED as a known, /// retail-matching freeze (see register row AD-56). /// /// is the REALISTIC case (small residual horizontal velocity, matching the /// original live repro's actual trajectory shape) — it converges cleanly to /// the flat floor with no freeze, which is what made TS-4's removal safe to /// land. /// /// public class Ts4SteepRoofWedgeCaptureTests { private readonly ITestOutputHelper _out; public Ts4SteepRoofWedgeCaptureTests(ITestOutputHelper output) => _out = output; private const uint CellId = 0xA9B40001u; private const int TicksPerSecond = 30; // #32 L.5 retail physics tick rate private const int MaxTicks = 3 * TicksPerSecond; private const int WedgeTickThreshold = 15; // 0.5 s of zero motion == wedged private const float WedgeEpsilon = 0.001f; // 1 mm private static PhysicsEngine MakeSlopeEngine() { var (root, resolved) = BSPStepUpFixtures.SlopedUnwalkable(); const uint LandblockId = 0xA9B4FFFFu; const uint SyntheticGfxId = 0xDEADBEEFu; var heights = new byte[81]; var heightTab = new float[256]; for (int i = 0; i < 256; i++) heightTab[i] = -1000f; // terrain never interferes var engine = new PhysicsEngine(); engine.AddLandblock( LandblockId, new TerrainSurface(heights, heightTab), System.Array.Empty(), System.Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); var cache = new PhysicsDataCache(); var bspTree = new DatReaderWriter.Types.PhysicsBSPTree { Root = root }; var physics = new GfxObjPhysics { BSP = bspTree, PhysicsPolygons = new System.Collections.Generic.Dictionary(), Vertices = new DatReaderWriter.Types.VertexArray(), Resolved = resolved, BoundingSphere = new DatReaderWriter.Types.Sphere { Origin = Vector3.Zero, Radius = 15f }, }; cache.RegisterGfxObjForTest(SyntheticGfxId, physics); engine.DataCache = cache; engine.ShadowObjects.Register( entityId: SyntheticGfxId, gfxObjId: SyntheticGfxId, worldPos: Vector3.Zero, rotation: Quaternion.Identity, radius: 15f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: LandblockId, collisionType: ShadowCollisionType.BSP, scale: 1.0f); return engine; } /// /// PINNED known-degenerate case (Campaign P final physics slice, /// 2026-07-30; docs/research/2026-07-30-ts4-116-oracle-plan.md §1.2-§1.3, /// §4 item 2; register row AD-56). A body falling PERFECTLY PLUMB (zero /// horizontal velocity) onto this 63.4° slope's mid-face lands, commits /// a steep ContactPlane via Path 4's permissive LandingZ /// gate, and then freezes at that exact position forever: the crease /// projection inside AdjustOffset /// (Cross(ContactPlane.Normal, SlidingNormal)) is mathematically /// orthogonal to a purely-Z offset, crushing it to zero every tick and /// tripping the abort-small-offset guard before TransitionalInsert /// runs again. This is retail-faithful — present identically in the raw /// decomp, ACE's port, and this port (§1.2) — and essentially /// unreachable in live play, where WASD input, camera-relative movement, /// and even float noise almost always inject some horizontal component /// (see /// for the realistic case, which does NOT freeze). This test PINS the /// freeze as accepted parity rather than treating it as a bug to fix. /// [Fact] public void FallOntoSteepSlope_PureVertical_FreezesAtDegenerateFixedPoint_RetailParity() { var engine = MakeSlopeEngine(); float r = BSPStepUpFixtures.SphereRadius; const float dt = 1f / TicksPerSecond; const float gravity = -9.8f; var body = new PhysicsBody { TransientState = TransientStateFlags.Active, }; // Start well above the slope's mid-face (slope spans x in [0,1], z in // [0,2] at that x-range), falling straight down with NO horizontal // component — the degenerate input this pin documents. Vector3 pos = new(0.5f, 0f, 3.0f); float fallVelocityZ = 0f; uint cell = CellId; int frozenStreak = 0; bool frozeAsExpected = false; Vector3 frozenAtPosition = default; for (int tick = 0; tick < MaxTicks; tick++) { fallVelocityZ += gravity * dt; Vector3 target = pos + new Vector3(0f, 0f, fallVelocityZ * dt); var result = engine.ResolveWithTransition( currentPos: pos, targetPos: target, cellId: cell, sphereRadius: r, sphereHeight: r * 2f, stepUpHeight: 0.30f, stepDownHeight: 0.04f, isOnGround: false, body: body, moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide, movingEntityId: 0x01000000u); var newPos = result.Position; float moved = Vector3.Distance(newPos, pos); if (moved < WedgeEpsilon) frozenStreak++; else frozenStreak = 0; _out.WriteLine( $"t{tick,3}: pos=({newPos.X:F3},{newPos.Y:F3},{newPos.Z:F3}) " + $"moved={moved:F4} onGround={result.IsOnGround} onWalkable={result.OnWalkable} " + $"contact={result.InContact} vz={fallVelocityZ:F2} frozen={frozenStreak}"); pos = newPos; cell = result.CellId; body.Position = pos; if (result.IsOnGround) fallVelocityZ = 0f; if (frozenStreak > WedgeTickThreshold) { frozeAsExpected = true; frozenAtPosition = pos; break; } } Assert.True(frozeAsExpected, $"Expected the degenerate pure-vertical drop to freeze for more than " + $"{WedgeTickThreshold} consecutive ticks (retail-matching AdjustOffset " + "crease-projection degeneracy, AD-56) within the {MaxTicks}-tick budget — " + "it did not. Either the degenerate case no longer reproduces (re-evaluate " + "this pin against the oracle plan) or an unrelated regression changed the " + "slope-landing chain."); Assert.True(frozenAtPosition.X > 0f, "Expected the freeze to occur ON the steep slope (x>0), not at/after the " + $"flat reference floor; got x={frozenAtPosition.X:F3}."); } /// /// Campaign P final physics slice, TS-4 decisive confirming run /// (docs/research/2026-07-30-ts4-116-oracle-plan.md §1.2 Step E, /// §1.3, §4 item 2). The pure-vertical fixture above is, per the oracle /// plan, the DEGENERATE case: AdjustOffset's crease projection /// (Cross(ContactPlane.Normal, SlidingNormal) against a purely /// gravity-only offset) is mathematically annihilated by construction /// when the offset has zero horizontal component — Dot(slideOffset, /// offset) = 0 exactly, because slideOffset.Z = 0 and the /// offset is purely Z. Any lateral drift (WASD input, residual jump /// momentum — present in the original 2026-04-30 live-client repro that /// validated the shortcut, but NOT in the pure-vertical fixture above) /// survives that same cross product and should let AdjustOffset /// produce a small non-zero tangential offset each tick, moving the /// sphere off the exact collision point, avoiding the abort-small-offset /// short-circuit, and letting TransitionalInsert run again on /// subsequent ticks. /// [Fact] public void FallOntoSteepSlope_WithHorizontalVelocity_NeverFreezesForOverHalfASecond_AndReachesFloor() { var engine = MakeSlopeEngine(); float r = BSPStepUpFixtures.SphereRadius; const float dt = 1f / TicksPerSecond; const float gravity = -9.8f; var body = new PhysicsBody { TransientState = TransientStateFlags.Active, }; // Same drop point as the pure-vertical fixture, but with a small // residual horizontal velocity toward the flat reference floor // (x<0) — the realistic "jumped/walked onto the roof with some // drift" case the L.4 shortcut's own validating trace exercised. Vector3 pos = new(0.5f, 0f, 3.0f); float fallVelocityZ = 0f; const float horizontalVelocityX = -0.3f; uint cell = CellId; int frozenStreak = 0; bool reachedFloor = false; for (int tick = 0; tick < MaxTicks; tick++) { fallVelocityZ += gravity * dt; Vector3 target = pos + new Vector3(horizontalVelocityX * dt, 0f, fallVelocityZ * dt); var result = engine.ResolveWithTransition( currentPos: pos, targetPos: target, cellId: cell, sphereRadius: r, sphereHeight: r * 2f, stepUpHeight: 0.30f, stepDownHeight: 0.04f, isOnGround: false, body: body, moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide, movingEntityId: 0x01000000u); var newPos = result.Position; float moved = Vector3.Distance(newPos, pos); if (moved < WedgeEpsilon) frozenStreak++; else frozenStreak = 0; _out.WriteLine( $"t{tick,3}: pos=({newPos.X:F3},{newPos.Y:F3},{newPos.Z:F3}) " + $"moved={moved:F4} onGround={result.IsOnGround} onWalkable={result.OnWalkable} " + $"contact={result.InContact} vz={fallVelocityZ:F2} frozen={frozenStreak}"); Assert.True(frozenStreak <= WedgeTickThreshold, $"Body frozen for {frozenStreak} consecutive ticks (>{WedgeTickThreshold} == " + $">0.5s) at tick {tick}, position ({newPos.X:F3},{newPos.Y:F3},{newPos.Z:F3}) — " + "this is the 'stuck in falling animation on the roof' wedge shape."); pos = newPos; cell = result.CellId; body.Position = pos; if (result.IsOnGround) fallVelocityZ = 0f; if (pos.X < 0f && pos.Z <= r + 0.05f) { reachedFloor = true; break; } } Assert.True(reachedFloor, $"Body never reached the flat reference floor within {MaxTicks} ticks " + $"({MaxTicks / (float)TicksPerSecond:F1}s); final position " + $"({pos.X:F3},{pos.Y:F3},{pos.Z:F3})."); } }