using System; using System.Numerics; using AcDream.Core.Physics; using Xunit; using Xunit.Abstractions; namespace AcDream.Core.Tests.Physics; /// /// Conformance tests for the retail frames_stationary_fall (fsf) round-trip /// (validate_transition 0x0050aa70 pc:272625-656; transition seed pc:280940-947; /// ACE Transition.cs:1029-1061). Retires the TS-3 stub. /// /// /// The ladder detects a gravity mover that CANNOT advance for successive frames — the /// #182 airborne "stuck in the falling animation" wedge (a jump into a monster crowd /// where the near-horizontal creature normal blocks upward motion). fsf escalates /// 0→1→2→3 while blocked and resets to 0 the moment the mover advances; at fsf≥3 an /// upward contact plane is manufactured so the mover stands on the obstacle. The counter /// round-trips across frames through the Stationary* transient bits (seed→ladder→writeback). /// The velocity "bleed on block" (fsf>1 → v=0) lives in handle_all_collisions (see /// ); this file proves the counter itself. /// /// public class FramesStationaryFallTests { private readonly ITestOutputHelper _out; public FramesStationaryFallTests(ITestOutputHelper output) => _out = output; private const uint Lb = 0xA9B40000u; private const uint Cell = Lb | 0x0001u; private const float R = 0.48f, H = 1.835f, StepUp = 0.60f, StepDown = 0.04f; private static PhysicsEngine BuildEngine() { var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; engine.AddLandblock( landblockId: Lb, terrain: new TerrainSurface(new byte[81], new float[256]), // flat terrain at Z=0 cells: Array.Empty(), portals: Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); return engine; } // A creature body sphere at an ARBITRARY height (elevated well above the terrain so the // airborne player never finds a floor and stays airborne — the crowd-jump geometry). private static void RegisterCreatureAt(PhysicsEngine e, uint id, Vector3 center, float radius = R) { e.ShadowObjects.Register( id, gfxObjId: 0u, center, Quaternion.Identity, radius, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: Lb, collisionType: ShadowCollisionType.Sphere, cylHeight: 0f, scale: 1f, state: 0u, flags: EntityCollisionFlags.IsCreature, isStatic: false); } private static PhysicsBody AirborneBody(Vector3 pos) => new PhysicsBody { Position = pos, Orientation = Quaternion.Identity, State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions, TransientState = TransientStateFlags.None, // airborne: no Contact / no OnWalkable }; private ResolveResult Push(PhysicsEngine engine, PhysicsBody body, Vector3 delta, uint cell) => engine.ResolveWithTransition( body.Position, body.Position + delta, cell, R, H, StepUp, StepDown, isOnGround: false, body: body, moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide); [Fact] public void AirborneJumpBlockedOverhead_FsfClimbsTo3_ThenResetsWhenAdvancing() { // The #182 airborne-stuck geometry, distilled: an airborne mover with a persistent // UPWARD intent (a jump) into a creature directly overhead. The collision normal is // vertical, so — unlike a purely-horizontal push, whose sliding normal absorbs the // whole offset and aborts the sweep before the ladder — the up-intent survives every // frame, the sweep runs, and fsf escalates 0→1→2→3 via the Stationary* bit round-trip. var engine = BuildEngine(); RegisterCreatureAt(engine, 0xC0B0u, new Vector3(12f, 10f, 4.5f)); // directly overhead // Start BELOW the creature so the first frames rise freely (fsf stays 0), then wedge. var body = AirborneBody(new Vector3(12f, 10f, 1f)); uint cell = Cell; var up = new Vector3(0f, 0f, 0.6f); // persistent jump intent int firstFrameFsf = -1, maxFsf = 0; for (int i = 0; i < 14; i++) { var r = Push(engine, body, up, cell); body.Position = r.Position; cell = r.CellId; if (i == 0) firstFrameFsf = body.FramesStationaryFall; maxFsf = Math.Max(maxFsf, body.FramesStationaryFall); _out.WriteLine($"frame{i,2}: z={body.Position.Z:F3} fsf={body.FramesStationaryFall} " + $"ts=0x{(uint)body.TransientState:X} onGround={r.IsOnGround}"); } // The first frame rose freely (well below the creature) — advancing keeps fsf at 0. Assert.Equal(0, firstFrameFsf); // Once wedged under the creature, fsf escalated to 3. Assert.True(maxFsf == 3, $"fsf must escalate to 3 while the jump is blocked overhead; got {maxFsf}"); // At fsf 3 the ladder manufactured an upward contact plane → grounded on the obstacle // (the retail "glide onto the crowd top"). Assert.True(body.ContactPlaneValid, "fsf≥3 should manufacture a contact plane"); Assert.True(body.ContactPlane.Normal.Z > 0.99f, "manufactured contact plane points up"); } [Fact] public void GroundedWallSlide_DoesNotAccumulateFsf() { // A GROUNDED mover pushed into an obstacle is not a "stuck fall" — retail keeps fsf=0 // (ACE _redo=1 via the OnWalkable path), so a grounded crowd-jam slides rather than // getting its velocity zeroed. Guards against the fsf-zero breaking grounded wall-slide. var engine = BuildEngine(); RegisterCreatureAt(engine, 0xC0C0u, new Vector3(12f, 11.5f, R)); // foot-height creature var floor = new Plane(Vector3.UnitZ, 0f); var verts = new[] { new Vector3(-100f, -100f, 0f), new Vector3(100f, -100f, 0f), new Vector3(100f, 100f, 0f), new Vector3(-100f, 100f, 0f), }; var body = new PhysicsBody { Position = new Vector3(12f, 10f, 0f), Orientation = Quaternion.Identity, State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions, ContactPlaneValid = true, ContactPlane = floor, ContactPlaneCellId = Cell, WalkablePolygonValid = true, WalkablePlane = floor, WalkableVertices = verts, WalkableUp = Vector3.UnitZ, TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable, }; uint cell = Cell; int maxFsf = 0; for (int i = 0; i < 40; i++) { var r = engine.ResolveWithTransition( body.Position, body.Position + new Vector3(0f, 0.08f, 0f), cell, R, H, StepUp, StepDown, isOnGround: true, body: body, moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide); body.Position = r.Position; cell = r.CellId; maxFsf = Math.Max(maxFsf, body.FramesStationaryFall); } _out.WriteLine($"grounded push maxFsf={maxFsf}"); Assert.Equal(0, maxFsf); // a grounded mover never accumulates a stuck-fall } }