using System.Numerics; namespace AcDream.Core.Physics; /// /// Performs the compressed first-gravity-frame settle used to establish /// retail Contact/OnWalkable state for a newly placed body. /// /// Retail gains spawn contact from the FIRST GRAVITY FRAME, not the /// placement itself: CPhysicsObj::enter_world (0x00516170) runs /// SetPosition (find_placement validates the spot but records no /// touch) and every retail CPhysicsObj then simulates, falls the few /// centimetres onto the floor, and the transition's touch grants the /// contact plane + CONTACT/ON_WALKABLE. Bodies that do not run that first /// ordinary frame at placement (stationary remotes — #270 — and the local /// player's Runtime first-entry activation — C3c-F5) compress the settle /// here: a short downward sweep from the placed position whose touch /// handler produces exactly the state retail's first frame would. A sweep /// that finds no floor (true airborne spawn) leaves the body airborne, /// exactly like retail's fall. /// public static class SpawnPlacementSettler { public const float SettleDistance = 0.5f; public static bool TrySettle( PhysicsEngine physicsEngine, PhysicsBody body, Vector3 worldPosition, uint cellId, float sphereRadius, float sphereHeight, ObjectInfoState moverFlags, uint movingEntityId, Action hitGround, Action leaveGround) { ArgumentNullException.ThrowIfNull(physicsEngine); ArgumentNullException.ThrowIfNull(body); ArgumentNullException.ThrowIfNull(hitGround); ArgumentNullException.ThrowIfNull(leaveGround); if (cellId == 0) return false; ResolveResult settle = physicsEngine.ResolveWithTransition( worldPosition, worldPosition - new Vector3(0f, 0f, SettleDistance), cellId, sphereRadius, sphereHeight, stepUpHeight: 0.4f, stepDownHeight: 0.4f, isOnGround: false, body, moverFlags, movingEntityId); if (!settle.Ok || !settle.InContact) return false; // #276: adopt the settle's RESOLVED cell, not just its position. // Retail's CPhysicsObj::SetPositionInternal(CTransition const*) // @0x00515330 commits both sphere_path.curr_pos.objcell_id and its // frame, including EnvCells. // // A bare `body.Position = settle.Position` is sufficient only while // the body is OUTDOOR: that setter mirrors the world delta into the // landblock-local frame and LandDefs.AdjustToOutside then recomputes // the 24 m cell index from it. It cannot do so for an EnvCell — an // EnvCell id is not derivable from a position, so the mirror // deliberately preserves it (PhysicsBody.cs, the `not (>= 1 and // <= 0x40)` arm). settle.CellId is therefore the ONLY carrier of a // cell identity across an indoor seam, and dropping it stranded the // body in the placement cell until some later resolve corrected it. // // Identical guarded shape to the per-tick resolve writeback // (RuntimeOrdinaryPhysicsUpdater): a transition reporting no cell // falls back to the source cell rather than zeroing residency. uint resolvedCellId = settle.CellId != 0 ? settle.CellId : cellId; body.CommitTransitionPosition(resolvedCellId, settle.Position); PhysicsObjUpdate.CommitSetPositionTransition( body, settle.InContact, settle.OnWalkable, settle.CollisionNormalValid, settle.CollisionNormal, previousContact: false, previousOnWalkable: false, hitGround, leaveGround); return true; } }