using System; using System.Numerics; using AcDream.Core.Physics; using Xunit; using Xunit.Abstractions; using Plane = System.Numerics.Plane; namespace AcDream.Core.Tests.Physics; /// /// Conformance tests for the retail CCylSphere collision family port /// (2026-07-05) — dispatcher 0x0053b440 + step_sphere_down /// 0x0053a9b0 + step_sphere_up 0x0053b310 + /// land_on_cylinder 0x0053b3d0. Pseudocode: /// docs/research/2026-07-05-ccylsphere-collision-family-pseudocode.md. /// /// /// The driving repro: the Holtburg town-network portal platform (stab /// 0xC0A9B465, Setup 0x020019E3) registers a WIDE LOW cylinder /// (r=2.597 m, h=0.256 m). Retail steps a grounded player UP ONTO its flat /// top; the pre-port approximation could only radial-slide, so the player /// orbited the rim forever (launch-137-repro.log, 2026-07-05). These tests /// pin the three retail behaviors the family provides: grounded /// step-up-onto-top, too-tall side slide, and the airborne top landing. /// Synthetic cylinders only — no dat dependency. /// /// public class CylSphereFamilyTests { private readonly ITestOutputHelper _out; public CylSphereFamilyTests(ITestOutputHelper output) => _out = output; private const uint TestLandblockId = 0xA9B40000u; private const uint TestCellId = TestLandblockId | 0x0001u; // landcell (0,0) private const float SphereRadius = 0.48f; // retail player capsule radius private const float SphereHeight = 1.20f; private const float StepUpHeight = 0.60f; private const float StepDownHeight = 0.04f; // The live platform's registered shape ([cyl-test] launch-137-repro.log). private const float PlatformRadius = 2.597f; private const float PlatformHeight = 0.256f; /// /// The portal-platform repro: a grounded player walking into the wide low /// cylinder must STEP UP onto its flat top (retail /// grounded branch → step_sphere_up → CTransition::step_up, whose /// step-down probe lands via step_sphere_down's top-disc contact plane) — /// not slide around the rim. /// [Fact] public void Grounded_WalkIntoWideLowCylinder_StepsUpOntoTop() { var engine = BuildEngine(out _); RegisterCylinder(engine, entityId: 0xCAFEu, worldPos: new Vector3(12f, 14f, 0f), radius: PlatformRadius, height: PlatformHeight); var body = MakeGroundedBody(new Vector3(12f, 10.4f, 0f)); Vector3 pos = body.Position; uint cellId = TestCellId; bool grounded = true; var perTick = new Vector3(0f, 0.10f, 0f); for (int tick = 0; tick < 40; tick++) { var result = engine.ResolveWithTransition( pos, pos + perTick, cellId, SphereRadius, SphereHeight, StepUpHeight, StepDownHeight, grounded, body: body, moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide, movingEntityId: 0); body.Position = result.Position; pos = result.Position; cellId = result.CellId; grounded = result.IsOnGround; } _out.WriteLine($"final pos=({pos.X:F3},{pos.Y:F3},{pos.Z:F3}) grounded={grounded}"); // Rim contact is at Y ≈ 14 − 2.597 − 0.48 = 10.92. Pre-port the player // pinned there (Z stayed 0, Y never passed the rim). Post-port the // player must be standing ON the platform top. Assert.True(pos.Y > 11.5f, $"Player must advance past the rim contact (pre-port it pinned at Y≈10.9); got Y={pos.Y:F3}"); Assert.True(MathF.Abs(pos.Z - PlatformHeight) < 0.05f, $"Player must stand ON the platform top (Z≈{PlatformHeight:F3}); got Z={pos.Z:F3}"); Assert.True(grounded, "Player must remain grounded after stepping onto the platform"); } /// /// A tall thin cylinder (the Holtburg torch shape, r=0.2 h=2.2 — #149) /// exceeds step_up_height: the grounded dead-center approach must NOT /// step up and must NOT pass through — retail slides (dead-center the /// crease projection degenerates to a hard stop). /// [Fact] public void Grounded_WalkIntoTallCylinder_BlocksBeforeAxis() { var engine = BuildEngine(out _); RegisterCylinder(engine, entityId: 0xF00Du, worldPos: new Vector3(12f, 14f, 0f), radius: 0.2f, height: 2.2f); var body = MakeGroundedBody(new Vector3(12f, 12.6f, 0f)); Vector3 pos = body.Position; uint cellId = TestCellId; bool grounded = true; var perTick = new Vector3(0f, 0.10f, 0f); for (int tick = 0; tick < 30; tick++) { var result = engine.ResolveWithTransition( pos, pos + perTick, cellId, SphereRadius, SphereHeight, StepUpHeight, StepDownHeight, grounded, body: body, moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide, movingEntityId: 0); body.Position = result.Position; pos = result.Position; cellId = result.CellId; grounded = result.IsOnGround; } _out.WriteLine($"final pos=({pos.X:F3},{pos.Y:F3},{pos.Z:F3}) grounded={grounded}"); // Surface contact at Y = 14 − 0.2 − 0.48 = 13.32. Assert.True(pos.Y < 13.4f, $"Tall cylinder must block the dead-center approach; got Y={pos.Y:F3}"); Assert.True(pos.Z < 0.5f, $"Player must NOT end up on top of a 2.2 m cylinder; got Z={pos.Z:F3}"); } /// /// Airborne landing: a falling sphere over the platform center must land /// ON the flat top (land_on_cylinder → Collide re-test → branch-5 /// exact-TOI rest + top-disc contact plane), not fall through to the /// terrain inside the footprint. /// [Fact] public void Airborne_FallOntoWideCylinder_LandsOnTop() { var engine = BuildEngine(out _); RegisterCylinder(engine, entityId: 0xCAFEu, worldPos: new Vector3(12f, 14f, 0f), radius: PlatformRadius, height: PlatformHeight); Vector3 pos = new(12f, 14f, 1.0f); // 1 m above the base, over the center uint cellId = TestCellId; bool grounded = false; var perTick = new Vector3(0f, 0f, -0.25f); int landedTick = -1; for (int tick = 0; tick < 20; tick++) { var result = engine.ResolveWithTransition( pos, pos + perTick, cellId, SphereRadius, SphereHeight, StepUpHeight, StepDownHeight, grounded, body: null, moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide, movingEntityId: 0); pos = result.Position; cellId = result.CellId; grounded = result.IsOnGround; if (grounded) { landedTick = tick; break; } } _out.WriteLine($"final pos=({pos.X:F3},{pos.Y:F3},{pos.Z:F3}) grounded={grounded} landedTick={landedTick}"); Assert.True(grounded, "Falling sphere must land (ground) on the platform top"); Assert.True(MathF.Abs(pos.Z - PlatformHeight) < 0.05f, $"Landing must rest on the top disc (Z≈{PlatformHeight:F3}), not the terrain " + $"(Z=0) inside the footprint; got Z={pos.Z:F3}"); } /// /// Ethereal cylinders stay fully passable through the caller's Layer-2 /// override (pc:276961-276989) — branch 1 detects, the override clears. /// Guards the #150 door behavior against the branch-1 change from the /// old early-OK consume. /// [Fact] public void Grounded_EtherealCylinder_IsFullyPassable() { var engine = BuildEngine(out _); RegisterCylinder(engine, entityId: 0xE7E7u, worldPos: new Vector3(12f, 14f, 0f), radius: 0.2f, height: 2.2f, state: 0x4u); // ETHEREAL_PS, non-static var body = MakeGroundedBody(new Vector3(12f, 12.6f, 0f)); Vector3 pos = body.Position; uint cellId = TestCellId; bool grounded = true; var perTick = new Vector3(0f, 0.10f, 0f); for (int tick = 0; tick < 30; tick++) { var result = engine.ResolveWithTransition( pos, pos + perTick, cellId, SphereRadius, SphereHeight, StepUpHeight, StepDownHeight, grounded, body: body, moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide, movingEntityId: 0); body.Position = result.Position; pos = result.Position; cellId = result.CellId; grounded = result.IsOnGround; } _out.WriteLine($"final pos=({pos.X:F3},{pos.Y:F3},{pos.Z:F3})"); Assert.True(pos.Y > 14.5f, $"Ethereal cylinder must not block (walked from 12.6 to past the axis); got Y={pos.Y:F3}"); } // ─────────────────────────────────────────────────────────────── // Harness // ─────────────────────────────────────────────────────────────── private static PhysicsEngine BuildEngine(out PhysicsDataCache cache) { cache = new PhysicsDataCache(); var engine = new PhysicsEngine { DataCache = cache }; // Flat terrain at Z=0 across the whole landblock. var heights = new byte[81]; var heightTable = new float[256]; // all zero → terrain Z = 0 engine.AddLandblock( landblockId: TestLandblockId, terrain: new TerrainSurface(heights, heightTable), cells: Array.Empty(), portals: Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); return engine; } private static void RegisterCylinder(PhysicsEngine engine, uint entityId, Vector3 worldPos, float radius, float height, uint state = 0u) { engine.ShadowObjects.Register( entityId, gfxObjId: 0u, worldPos, Quaternion.Identity, radius, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: TestLandblockId, collisionType: ShadowCollisionType.Cylinder, cylHeight: height, state: state); } private static PhysicsBody MakeGroundedBody(Vector3 position) { var floorPlane = new Plane(Vector3.UnitZ, 0f); var floorVerts = new[] { new Vector3(-100f, -100f, 0f), new Vector3( 100f, -100f, 0f), new Vector3( 100f, 100f, 0f), new Vector3(-100f, 100f, 0f), }; return new PhysicsBody { Position = position, Orientation = Quaternion.Identity, ContactPlaneValid = true, ContactPlane = floorPlane, ContactPlaneCellId = TestCellId, WalkablePolygonValid = true, WalkablePlane = floorPlane, WalkableVertices = floorVerts, WalkableUp = Vector3.UnitZ, TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable, }; } }