using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using DatReaderWriter.Types; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// Conformance tests for BSP step-up (Path 5) and rooftop landing (Path 6) in /// . /// /// /// Tests are organised in three groups corresponding to the three commits: /// /// /// Group A — Baselines: behaviours that should pass both before /// and after the implementation (no-hit returns OK, fixture geometry checks). /// Group B — Phase L.2.1 (Path 5 step-up): tests that are RED /// because Path 5 wall-slides instead of stepping up. L.2.1 flips these /// GREEN. /// Group C — Phase L.2.2 (Path 6 SetCollide): tests that are RED /// because Path 6 wall-slides instead of setting the Collide flag. L.2.2 /// flips these GREEN. /// /// /// /// Retail references: /// BSPTREE::find_collisions Path 5 — acclient_2013_pseudo_c.txt:323849 / /// ACE BSPTree.cs:192-196. /// CTransition::step_up — acclient_2013_pseudo_c.txt:273099-273133 / /// ACE Transition.cs:746-777. /// BSPTREE::find_collisions Path 6 / SPHEREPATH::set_collide — /// acclient_2013_pseudo_c.txt:323819 / ACE BSPTree.cs:210-219. /// SPHEREPATH::set_collide — acclient_2013_pseudo_c.txt:321594-321607 / /// ACE SpherePath.cs:279-286. /// CTransition::transitional_insert Collide branch — /// acclient_2013_pseudo_c.txt:273193-273239 / ACE Transition.cs:891-930. /// /// public class BSPStepUpTests { // ========================================================================= // Group A — Baselines (pass before AND after the implementation) // ========================================================================= /// /// No BSP geometry → FindCollisions returns OK with no state changes. /// [Fact] public void A1_NullRoot_ReturnsOK() { var from = new Vector3(0f, 0f, BSPStepUpFixtures.SphereRadius); var to = new Vector3(0.1f, 0f, BSPStepUpFixtures.SphereRadius); var t = BSPStepUpFixtures.MakeGroundedTransition(from, to); var localSphere = new DatReaderWriter.Types.Sphere { Origin = to, Radius = BSPStepUpFixtures.SphereRadius, }; var result = BSPQuery.FindCollisions( null, new Dictionary(), t, localSphere, null, from, Vector3.UnitZ, 1.0f); Assert.Equal(TransitionState.OK, result); } /// /// Grounded mover far from the wall → no collision → OK. /// [Fact] public void A2_GroundedMover_NoWallNear_ReturnsOK() { var (root, resolved) = BSPStepUpFixtures.LowStep(); // Moving in -X, away from the wall at x=0.5. var from = new Vector3(-1f, 0f, BSPStepUpFixtures.SphereRadius); var to = new Vector3(-1.5f, 0f, BSPStepUpFixtures.SphereRadius); var t = BSPStepUpFixtures.MakeGroundedTransition(from, to); var localSphere = new DatReaderWriter.Types.Sphere { Origin = to, Radius = BSPStepUpFixtures.SphereRadius }; var result = BSPQuery.FindCollisions( root, resolved, t, localSphere, null, from, Vector3.UnitZ, 1.0f); Assert.Equal(TransitionState.OK, result); } /// /// Airborne mover well above the roof → no collision → OK. /// [Fact] public void A3_AirborneMover_AboveRoof_ReturnsOK() { var (root, resolved) = BSPStepUpFixtures.FlatRoof(); // Mover at z=6 (well above the roof at z=3) with tiny downward step. float highZ = 6f; var from = new Vector3(0f, 0f, highZ + BSPStepUpFixtures.SphereRadius); var to = new Vector3(0f, 0f, highZ + BSPStepUpFixtures.SphereRadius - 0.01f); var t = BSPStepUpFixtures.MakeAirborneTransition(from, to); var localSphere = new DatReaderWriter.Types.Sphere { Origin = to, Radius = BSPStepUpFixtures.SphereRadius }; var result = BSPQuery.FindCollisions( root, resolved, t, localSphere, null, from, Vector3.UnitZ, 1.0f); Assert.Equal(TransitionState.OK, result); } /// /// The slope fixture's polygon must have normal.Z below FloorZ (confirms /// the fixture geometry is set up correctly as a non-walkable surface). /// [Fact] public void A4_SlopedFixture_NormalBelowFloorZ() { var (_, resolved) = BSPStepUpFixtures.SlopedUnwalkable(); var slope = resolved[BSPStepUpFixtures.SlopedUnwalkable_SlopeId]; Assert.True(slope.Plane.Normal.Z < PhysicsGlobals.FloorZ, $"Slope normal.Z ({slope.Plane.Normal.Z:F4}) must be < FloorZ ({PhysicsGlobals.FloorZ:F4})"); Assert.True(slope.Plane.Normal.Z > 0f, $"Slope normal.Z ({slope.Plane.Normal.Z:F4}) must be > 0 (upward-facing)"); } /// /// Low-step upper-floor polygon has normal.Z >= FloorZ (it IS walkable). /// [Fact] public void A5_LowStepUpperFloor_NormalAboveFloorZ() { var (_, resolved) = BSPStepUpFixtures.LowStep(); var upper = resolved[BSPStepUpFixtures.LowStep_UpperFloorId]; Assert.True(upper.Plane.Normal.Z >= PhysicsGlobals.FloorZ, $"Upper floor normal.Z ({upper.Plane.Normal.Z:F4}) must be >= FloorZ ({PhysicsGlobals.FloorZ:F4})"); } /// /// Roof polygon has normal.Z >= LandingZ (it can be landed on). /// [Fact] public void A6_FlatRoofPolygon_NormalAboveLandingZ() { var (_, resolved) = BSPStepUpFixtures.FlatRoof(); var roof = resolved[BSPStepUpFixtures.FlatRoof_RoofId]; Assert.True(roof.Plane.Normal.Z >= PhysicsGlobals.LandingZ, $"Roof normal.Z ({roof.Plane.Normal.Z:F4}) must be >= LandingZ ({PhysicsGlobals.LandingZ:F4})"); } // ========================================================================= // Group B — Phase L.2.1 (Path 5 step-up) // // RED before L.2.1, GREEN after. // Each test documents the CURRENT wrong behaviour and EXPECTED correct one. // ========================================================================= /// /// Grounded mover (Contact + OnWalkable) walking toward the low step (25 cm): /// should step up onto the upper floor, not slide sideways. /// /// /// Current (wrong): Path 5 applies wall-slide → CurPos.X stays left of wall; /// Z stays at floor level. /// /// /// Expected after L.2.1: Path 5 calls StepUp → DoStepDown finds upper floor /// → sphere lifts to z ≥ 0.25 + SphereRadius and X advances past the wall. /// /// /// Retail: BSPTREE::step_sphere_up / CTransition::step_up /// acclient_2013_pseudo_c.txt:323849, 273099. /// [Fact] public void B1_GroundedMover_LowStep_StepsUp() { var (root, resolved) = BSPStepUpFixtures.LowStep(); const float stepUpHeight = 0.30f; // larger than step (0.25), so step-up succeeds // CurPos (foot position) starts at z=0 (on the terrain / BSP floor at z=0). // The sphere center is at CurPos + (0, 0, SphereRadius) = (x, 0, 0.2). // lowPoint = sphere_center - (0,0,r) = (x, 0, 0) → on terrain → contact. var from = new Vector3(0.1f, 0f, 0f); // to.X = 0.6 → offset = (0.5, 0, 0), 3 sub-steps of 0.1667 each. // Step 2: CurPos ≈ (0.433, 0, 0), sphere center x ≈ 0.433. // Wall: dist = 0.5 - 0.433 = 0.067 < rad = 0.198 → HIT Path 5 ✓ var to = new Vector3(0.6f, 0f, 0f); // foot stays at z=0, crosses wall at x=0.5 var t = BSPStepUpFixtures.MakeGroundedTransition(from, to, stepUpHeight); // terrainZ=0f: terrain at z=0 keeps the step-down probe grounded between // steps, preserving Contact/OnWalkable across the sub-step boundary. var engine = MakeTestEngine(root, resolved, terrainZ: 0f); bool ok = t.FindTransitionalPosition(engine); // After step-up, the character's foot (CurPos.Z) must be at or above the // upper floor (z=0.25). CurPos stores the foot origin; the sphere center is // CurPos.Z + SphereRadius. The lower bound is the upper-floor Z minus a // small epsilon to tolerate floating-point rounding in AdjustSphereToPlane. float expectedMinZ = 0.25f - PhysicsGlobals.EPSILON * 10f; Assert.True(t.SpherePath.CurPos.Z >= expectedMinZ, $"Expected Z >= {expectedMinZ:F4} (stepped up to upper floor at z=0.25), " + $"got CurPos.Z = {t.SpherePath.CurPos.Z:F4}. " + "Path 5 must call StepUp (L.2.1) instead of wall-sliding."); } /// /// Grounded mover walking into the too-tall wall (5 m) should NOT step up — /// the wall is taller than StepUpHeight. /// /// /// Expected: StepUp is called, DoStepDown finds no walkable surface within /// 0.04 m (no upper floor exists), StepUpSlide applies → mover stays /// left of the wall. /// /// /// Retail: SPHEREPATH::step_up_slide /// ACE SpherePath.cs:309-316. /// [Fact] public void B2_GroundedMover_TallWall_BlockedOrSlides() { var (root, resolved) = BSPStepUpFixtures.TallWall(); const float stepUpHeight = 0.04f; // default — cannot scale 5 m wall // Foot at z=0 (on terrain). Same reasoning as B1. var from = new Vector3(0.1f, 0f, 0f); var to = new Vector3(0.6f, 0f, 0f); var t = BSPStepUpFixtures.MakeGroundedTransition(from, to, stepUpHeight); // terrainZ=0f: keep grounded between steps (same as B1). var engine = MakeTestEngine(root, resolved, terrainZ: 0f); t.FindTransitionalPosition(engine); // The mover should NOT have crossed the wall at x=0.5. float wallFace = 0.5f - BSPStepUpFixtures.SphereRadius; Assert.True(t.SpherePath.CurPos.X <= wallFace + PhysicsGlobals.EPSILON * 20f, $"Expected mover blocked before wall (x <= {wallFace:F3}), " + $"got CurPos.X = {t.SpherePath.CurPos.X:F4}"); } /// /// Direct Path 5 invocation: Contact mover sphere just overlapping the low /// wall should NOT return Slid after L.2.1. /// /// /// Current: returns Slid (wall-slide). /// Expected after L.2.1: returns OK (step-up succeeded) with Z lifted. /// /// [Fact] public void B3_Path5_DirectCall_ContactHitsLowWall_NotSlid() { var (root, resolved) = BSPStepUpFixtures.LowStep(); // Sphere center overlaps the wall (x=0.5) by half-radius. float r = BSPStepUpFixtures.SphereRadius; var checkPos = new Vector3(0.5f - r * 0.5f, 0f, r); var currPos = new Vector3(0.1f, 0f, r); var t = new Transition(); t.SpherePath.InitPath(currPos, checkPos, 0xA9B40001u, r); t.SpherePath.SetCheckPos(checkPos, 0xA9B40001u); t.ObjectInfo.State = ObjectInfoState.Contact | ObjectInfoState.OnWalkable; t.ObjectInfo.StepUpHeight = 0.30f; t.ObjectInfo.StepDownHeight = 0.04f; t.CollisionInfo.LastKnownContactPlane = new Plane(Vector3.UnitZ, 0f); t.CollisionInfo.LastKnownContactPlaneValid = true; var localSphere = new DatReaderWriter.Types.Sphere { Origin = checkPos, Radius = r }; // Pass engine so Path 5 can call DoStepUp → DoStepDown (L.2.1). // Without engine the fallback wall-slide would return Slid. var engine = MakeTestEngine(root, resolved); var result = BSPQuery.FindCollisions( root, resolved, t, localSphere, null, currPos, Vector3.UnitZ, 1.0f, Quaternion.Identity, engine); // After L.2.1 this assertion flips from failing (Slid) to passing. Assert.NotEqual(TransitionState.Slid, result); } // ========================================================================= // Group C — Phase L.2.2 (Path 6 SetCollide) // // RED before L.2.2, GREEN after. // ========================================================================= /// /// Airborne mover hitting the flat roof from above should set Collide flag /// and return Adjusted (not Slid with wall-slide offset). /// /// /// Current (wrong): Path 6 computes a wall-slide offset and returns Slid. /// /// /// Expected after L.2.2: Path 6 calls path.SetCollide(worldNormal), sets /// WalkableAllowance = LandingZ, returns Adjusted. /// /// /// Retail: SPHEREPATH::set_collide /// acclient_2013_pseudo_c.txt:321594 / ACE BSPTree.cs:210-219. /// [Fact] public void C1_Path6_AirborneMoverHitsRoof_SetsCollideFlagAndAdjusted() { var (root, resolved) = BSPStepUpFixtures.FlatRoof(); // Sphere center just penetrating the roof polygon (z=3) from above. float r = BSPStepUpFixtures.SphereRadius; var checkPos = new Vector3(0f, 0f, 3f + r * 0.5f); // half-radius above roof var currPos = new Vector3(0f, 0f, 3f + r + 0.1f); // clearly above var t = new Transition(); t.SpherePath.InitPath(currPos, checkPos, 0xA9B40001u, r); t.SpherePath.SetCheckPos(checkPos, 0xA9B40001u); t.ObjectInfo.State = ObjectInfoState.None; // airborne — no Contact var localSphere = new DatReaderWriter.Types.Sphere { Origin = checkPos, Radius = r }; var result = BSPQuery.FindCollisions( root, resolved, t, localSphere, null, currPos, Vector3.UnitZ, 1.0f); // After L.2.2: result = Adjusted, Collide = true, WalkableAllowance = LandingZ. // Currently: result = Slid (wall-slide path). Assert.Equal(TransitionState.Adjusted, result); Assert.True(t.SpherePath.Collide, "Expected SpherePath.Collide = true after Path 6 hit (L.2.2)"); Assert.Equal(PhysicsGlobals.LandingZ, t.SpherePath.WalkableAllowance, precision: 5); } /// /// Full integration: airborne mover drops onto the 3 m flat roof. /// /// /// After L.2.2: TransitionalInsert sees Collide flag, re-tests as Placement, /// finds walkable polygon at z=3, sets ContactPlane with normal.Z ≈ 1. /// /// /// Current: mover slides sideways off the roof (never lands). /// Expected after L.2.2: ContactPlane is set with Normal.Z >= LandingZ. /// /// [Fact] public void C2_AirborneMover_LandsOnFlatRoof_ContactPlaneSet() { var (root, resolved) = BSPStepUpFixtures.FlatRoof(); float roofZ = 3f; float r = BSPStepUpFixtures.SphereRadius; // CurPos = foot position. Sphere center = CurPos + (0,0,r). // from: foot at z = roofZ - r + 0.3f → sphere center at roofZ + 0.3 = 3.3 (above roof) // to: foot at z = roofZ - r - 0.05f → sphere center at roofZ - 0.05 = 2.95 (into roof by 0.05) // Roof polygon at z=roofZ, normal=+Z: dist = sphere_center.z - roofZ. // At to: dist = -0.05; |dist| = 0.05 < rad=0.198 → roof hit ✓ var from = new Vector3(0f, 0f, roofZ - r + 0.3f); var to = new Vector3(0f, 0f, roofZ - r - 0.05f); // sphere bottom at z ≈ 2.95 (into roof) var t = BSPStepUpFixtures.MakeAirborneTransition(from, to); // terrainZ=-50f: airborne mover — terrain must not interfere with roof landing. var engine = MakeTestEngine(root, resolved, terrainZ: -50f); t.FindTransitionalPosition(engine); // After L.2.2: at least one of ContactPlane / LastKnownContactPlane is set. bool planeSet = t.CollisionInfo.ContactPlaneValid || t.CollisionInfo.LastKnownContactPlaneValid; Assert.True(planeSet, "Expected a contact plane after landing on roof (L.2.2). " + "Currently Path 6 wall-slides and never sets ContactPlane."); if (planeSet) { var plane = t.CollisionInfo.ContactPlaneValid ? t.CollisionInfo.ContactPlane : t.CollisionInfo.LastKnownContactPlane; Assert.True(plane.Normal.Z >= PhysicsGlobals.LandingZ, $"Contact plane normal.Z ({plane.Normal.Z:F4}) must be >= LandingZ ({PhysicsGlobals.LandingZ:F4})"); } } /// /// Airborne mover descending toward a steep slope (normal.Z < FloorZ): /// Path 6 should still set the Collide flag (it fires for any polygon hit, /// walkable or not). /// /// Retail: set_collide fires unconditionally when sphere_intersects_poly /// hits; the walkable check happens later in the Collide-flag handler. /// [Fact] public void C3_Path6_AirborneMoverHitsSteepSlope_SetsCollide() { var (root, resolved) = BSPStepUpFixtures.SlopedUnwalkable(); float r = BSPStepUpFixtures.SphereRadius; // Approach the slope mid-face from above. var checkPos = new Vector3(0.5f, 0f, 1.0f + r * 0.5f); var currPos = new Vector3(0.5f, 0f, 1.0f + r + 0.1f); var t = new Transition(); t.SpherePath.InitPath(currPos, checkPos, 0xA9B40001u, r); t.SpherePath.SetCheckPos(checkPos, 0xA9B40001u); t.ObjectInfo.State = ObjectInfoState.None; // airborne var localSphere = new DatReaderWriter.Types.Sphere { Origin = checkPos, Radius = r }; var result = BSPQuery.FindCollisions( root, resolved, t, localSphere, null, currPos, Vector3.UnitZ, 1.0f); // After L.2.2: Collide flag set, Adjusted returned. // Currently: Slid (wall-slide). Assert.Equal(TransitionState.Adjusted, result); Assert.True(t.SpherePath.Collide, "Expected Collide flag set when airborne sphere hits slope (L.2.2)"); } // ========================================================================= // Group D — Phase L.2.3 regression tests // // Bugs caught by live testing 2026-04-29: // D1 — walking into a too-tall wall must NOT clear ContactPlane (animation // flickers to "falling" when contact is lost mid-step against a wall). // D2 — Path 5 step-up must NOT recurse infinitely against a tall wall // (retail guards step_sphere_up with `if (sp.step_up == 0)` per // acclient_2013_pseudo_c.txt:272954). Without the guard, DoStepUp // invokes DoStepDown which TransitionalInsert(5)'s into FindObjCollisions // which hits the same wall AGAIN → recursive DoStepUp. // ========================================================================= /// /// L.2.3c regression: a grounded mover walking into a too-tall wall must /// retain its ground contact across the failed step-up. Before the fix, /// DoStepUp cleared /// unconditionally; on failure, RestoreCheckPos restored the position but /// the contact plane stayed cleared, causing OnWalkable to drop and the /// animation system to interpret the stuck-against-wall state as "airborne". /// [Fact] public void D1_GroundedMover_TooTallWall_PreservesContactPlane() { var (root, resolved) = BSPStepUpFixtures.TallWall(); // Foot at z=0, walking into the wall. var from = new Vector3(0.1f, 0f, 0f); var to = new Vector3(0.6f, 0f, 0f); // StepUpHeight 0.04m — too small to climb the 5m wall. var t = BSPStepUpFixtures.MakeGroundedTransition(from, to, stepUpHeight: 0.04f); var engine = MakeTestEngine(root, resolved, terrainZ: 0f); t.FindTransitionalPosition(engine); // After failed step-up + slide, the mover should still be considered // grounded — either via the live contact plane, the last-known one, // or the OnWalkable flag preserved by terrain re-detection. bool stillGrounded = t.CollisionInfo.ContactPlaneValid || t.CollisionInfo.LastKnownContactPlaneValid || t.ObjectInfo.State.HasFlag(ObjectInfoState.OnWalkable); Assert.True(stillGrounded, "Expected mover to still be grounded after walking into a too-tall " + "wall (failed step-up should preserve LastKnownContactPlane)."); } /// /// L.2.3b regression: Path 5 dispatch must be guarded against re-entry while /// a step-up is already in progress. Test runs FindTransitionalPosition /// with a tight time budget and verifies it terminates cleanly. Without the /// guard the recursive DoStepUp churns the contact plane until numAttempts /// runs out — finishing in an inconsistent state. /// [Fact] public void D2_GroundedMover_TallWall_DoesNotRecurseInfinitely() { var (root, resolved) = BSPStepUpFixtures.TallWall(); var from = new Vector3(0.1f, 0f, 0f); var to = new Vector3(0.6f, 0f, 0f); var t = BSPStepUpFixtures.MakeGroundedTransition(from, to, stepUpHeight: 0.04f); var engine = MakeTestEngine(root, resolved, terrainZ: 0f); var sw = System.Diagnostics.Stopwatch.StartNew(); t.FindTransitionalPosition(engine); sw.Stop(); // Bounded execution: even with recursion, this is a 4-step movement. // 100ms is generous; without the guard, recursion adds noticeable cost. Assert.True(sw.ElapsedMilliseconds < 100, $"Step-up against tall wall took {sw.ElapsedMilliseconds}ms — " + "indicates Path 5 recursing through DoStepUp without guard."); } /// /// L.2c regression: an airborne mover jumping/falling into a vertical wall /// must keep its vertical displacement. With no live or last-known contact /// plane, SlideSphere must remove only the component into the wall; inventing /// a flat UnitZ plane projects the displacement onto the wall/floor crease /// and leaves the character stuck in falling animation against the wall. /// [Fact] public void D3_AirborneMover_TallWall_PreservesVerticalMotion() { var (root, resolved) = BSPStepUpFixtures.TallWall(); var from = new Vector3(0.1f, 0f, 2.0f); var to = new Vector3(0.6f, 0f, 1.5f); var t = BSPStepUpFixtures.MakeAirborneTransition(from, to); var engine = MakeTestEngine(root, resolved, terrainZ: -50f); t.FindTransitionalPosition(engine); Assert.True(t.SpherePath.CurPos.Z < from.Z - 0.1f, $"Expected airborne wall-slide to preserve downward motion; " + $"from.Z={from.Z:F3}, CurPos.Z={t.SpherePath.CurPos.Z:F3}"); Assert.True(t.SpherePath.CurPos.X <= 0.5f - BSPStepUpFixtures.SphereRadius + PhysicsGlobals.EPSILON * 20f, $"Expected wall to block X penetration; got CurPos.X={t.SpherePath.CurPos.X:F3}"); } /// /// L.2c regression: if an airborne wall collision happens in a one-substep /// frame, the collision normal has to survive into the next frame. Retail /// does this with transient_state bit 2 + InitSlidingNormal. Without that, /// every frame replays the same hard stop and the character hangs in falling /// animation until another correction breaks the loop. /// [Fact] public void D4_AirborneMover_TallWall_PersistsSlidingNormalAcrossFrames() { var (root, resolved) = BSPStepUpFixtures.TallWall(); var engine = MakeTestEngine(root, resolved, terrainZ: -50f); var body = new PhysicsBody { Position = new Vector3(0.25f, 0f, 2.0f), TransientState = TransientStateFlags.Active, }; var frame1 = engine.ResolveWithTransition( currentPos: body.Position, targetPos: new Vector3(0.36f, 0f, 1.92f), cellId: 0xA9B40001u, sphereRadius: BSPStepUpFixtures.SphereRadius, sphereHeight: 0f, stepUpHeight: 0.04f, stepDownHeight: 0.04f, isOnGround: false, body: body); body.Position = frame1.Position; Assert.True(body.TransientState.HasFlag(TransientStateFlags.Sliding), "First airborne wall hit should cache SlidingNormal for the next frame."); Assert.Equal(2.0f, frame1.Position.Z, precision: 3); var frame2 = engine.ResolveWithTransition( currentPos: body.Position, targetPos: body.Position + new Vector3(0.11f, 0f, -0.08f), cellId: 0xA9B40001u, sphereRadius: BSPStepUpFixtures.SphereRadius, sphereHeight: 0f, stepUpHeight: 0.04f, stepDownHeight: 0.04f, isOnGround: false, body: body); Assert.True(frame2.Position.Z < frame1.Position.Z - 0.05f, $"Expected cached wall-slide normal to allow falling on frame 2; " + $"frame1.Z={frame1.Position.Z:F3}, frame2.Z={frame2.Position.Z:F3}"); Assert.InRange(frame2.Position.X, 0.24f, 0.31f); } // ========================================================================= // Helpers // ========================================================================= /// /// Build a that serves one synthetic BSP object. /// sets every terrain sample to the given height. /// Use 0f for grounded tests (terrain flush with the BSP floor at z=0, so the /// step-down probe finds ground and keeps Contact/OnWalkable set between steps). /// Use -50f for tests where terrain must never interfere (airborne / roof landing). /// private static PhysicsEngine MakeTestEngine( PhysicsBSPNode root, Dictionary resolved, Vector3? objectPosition = null, float terrainZ = 0f) { const uint LandblockId = 0xA9B4FFFFu; const uint SyntheticGfxId = 0xDEADBEEFu; var heights = new byte[81]; // all zero → uses index 0 from heightTable var heightTab = new float[256]; for (int i = 0; i < 256; i++) heightTab[i] = terrainZ; var engine = new PhysicsEngine(); engine.AddLandblock( LandblockId, new TerrainSurface(heights, heightTab), Array.Empty(), Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); // Register the BSP physics into the data cache. var cache = new PhysicsDataCache(); var bspTree = new DatReaderWriter.Types.PhysicsBSPTree { Root = root }; var physics = new GfxObjPhysics { BSP = bspTree, PhysicsPolygons = new 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; // Register the object in the shadow registry so FindObjCollisions picks it up. Vector3 pos = objectPosition ?? Vector3.Zero; engine.ShadowObjects.Register( entityId: SyntheticGfxId, gfxObjId: SyntheticGfxId, worldPos: pos, rotation: Quaternion.Identity, radius: 15f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: LandblockId, collisionType: ShadowCollisionType.BSP, scale: 1.0f); return engine; } }