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}).");
}
}