using System.Collections.Immutable; using System.Numerics; using AcDream.Core.Physics; namespace AcDream.Core.Tests.Physics; /// /// Enter-world overlap conformance for retail /// CTransition::find_placement_pos (0x0050BA50). /// /// /// C5a (2026-08-05) re-point: the legacy PhysicsEngine.ResolvePlacement /// this test originally drove is deleted (zero production callers). Canonical /// PhysicsEngine.SetPosition reaches the SAME ring-search machinery — /// SetPositionInternal sets InsertType.Placement before calling /// Transition.FindValidPosition, which (for any non-Transition /// insert type) dispatches to FindPlacementPosition /// (TransitionTypes.cs), and THAT method calls this exact /// FindPlacementPos ring search after its initial-placement insert — /// so this is not a new code path, only a new entry point onto the one the /// legacy method used directly. Sphere shape is reconstructed as the /// two-sphere capsule the legacy scalar (radius, height) overload /// built internally (origin (0,0,radius) + (0,0,height-radius), both /// radius-sized), since canonical SetPosition takes a pre-built /// sphere list rather than a radius/height pair. /// /// public sealed class InitialPlacementOverlapTests { private const uint Landblock = 0xA9B40000u; private const uint Cell = Landblock | 0x0001u; private const uint PlayerId = 0x50000001u; private const uint MonsterId = 0x50000002u; private const float Radius = 0.48f; private const float SphereHeight = 1.835f; [Fact] public void PlayerReloggingInsideMonster_SearchesOutToNearestClearRing() { var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; engine.AddLandblock( Landblock, new TerrainSurface(new byte[81], new float[256]), Array.Empty(), Array.Empty(), 0f, 0f); var savedFeet = new Vector3(10f, 10f, 0f); var playerCenter = savedFeet + new Vector3(0f, 0f, Radius); var monsterCenter = playerCenter + new Vector3(0.10f, 0f, 0f); // Retail registers every placed CPhysicsObj, including the local // player. The mover's OBJECTINFO::object self pointer excludes only // its own shadow while the monster remains an obstruction. RegisterSphere(engine, PlayerId, playerCenter, EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsCreature); RegisterSphere(engine, MonsterId, monsterCenter, EntityCollisionFlags.IsCreature); // The mover's own two-sphere capsule, matching the legacy scalar // InitPath(sphereRadius: 0.48, sphereHeight: 1.835) reconstruction: // a foot sphere at (0,0,radius) and a head sphere at // (0,0,height-radius), both radius-sized. ImmutableArray spheres = ImmutableArray.Create( new FlatCollisionSphere(new Vector3(0f, 0f, Radius), Radius), new FlatCollisionSphere(new Vector3(0f, 0f, SphereHeight - Radius), Radius)); PhysicsSetPositionResult result = engine.SetPosition( new PhysicsSetPositionRequest( Position: savedFeet, Orientation: Quaternion.Identity, CellId: Cell, CellLocalPosition: savedFeet, Spheres: spheres, Scale: 1f, StepUpHeight: 0.4f, StepDownHeight: 0.4f, MoverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide, MovingEntityId: PlayerId, Flags: PhysicsSetPositionFlags.Placement | PhysicsSetPositionFlags.Slide)); Assert.True(result.IsCommitted); Assert.True(result.InContact); Assert.True(result.OnWalkable); Assert.Equal(Cell, result.ContactPlaneCellId); Assert.NotEqual(savedFeet, result.Position); float centerDistance = Vector3.Distance( result.Position + new Vector3(0f, 0f, Radius), monsterCenter); Assert.True(centerDistance >= Radius * 2f - PhysicsGlobals.EPSILON, $"placement must clear the monster; centers remain {centerDistance:F3} m apart"); Assert.True(Vector3.Distance(savedFeet, result.Position) <= 4f, "retail placement search is bounded to four metres"); } private static void RegisterSphere( PhysicsEngine engine, uint id, Vector3 center, EntityCollisionFlags flags) { engine.ShadowObjects.Register( id, gfxObjId: 0u, worldPos: center, rotation: Quaternion.Identity, radius: Radius, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: Landblock, collisionType: ShadowCollisionType.Sphere, cylHeight: 0f, scale: 1f, state: 0u, flags: flags, seedCellId: Cell, isStatic: false); } }