using System.Numerics; namespace AcDream.Core.Physics; /// /// Ports the lifecycle and 96-world-unit Active gate at the head of retail /// CPhysicsObj::update_object (0x00515D10) plus the inactive branch of /// UpdateObjectInternal (0x005156B0). /// public static class RetailObjectActivityGate { public const float MaxPhysicsDistance = 96f; public static RetailObjectActivityResult Evaluate( RetailObjectQuantumClock clock, PhysicsBody? body, bool lifecycleEligible, bool hasPartArray, bool isStatic, Vector3 objectPosition, Vector3? playerPosition, double elapsedSeconds) { ArgumentNullException.ThrowIfNull(clock); if (!lifecycleEligible) { SetActive(clock, body, active: false); return RetailObjectActivityResult.Suspended; } // Retail performs both the 96-unit decision and set_active(1) only // while player_object exists. During login/session transitions a // missing player preserves the prior Active state; it must not wake a // previously distant object. if (playerPosition is null) { if (clock.IsActive) return RetailObjectActivityResult.Active; clock.Advance(elapsedSeconds); return RetailObjectActivityResult.Inactive; } bool withinActiveBubble = !hasPartArray || Vector3.Distance(objectPosition, playerPosition.Value) <= MaxPhysicsDistance; if (!withinActiveBubble) { SetActive(clock, body, active: false); // Inactive ordinary objects still consume update_time and run // their particle/script tail. Those owners tick elsewhere in // acdream; consuming the batch here prevents later catch-up. clock.Advance(elapsedSeconds); return RetailObjectActivityResult.Inactive; } // CPhysicsObj::set_active(1) (0x0050FC40) is a no-op for Static. // Static objects that were initialized or removed from visibility // inactive therefore stay on UpdateObjectInternal's particle/script // tail instead of entering root physics. bool reactivated = false; if (!isStatic) { reactivated = clock.Activate(); if (body is not null) body.TransientState |= TransientStateFlags.Active; } if (!clock.IsActive) { clock.Advance(elapsedSeconds); return RetailObjectActivityResult.Inactive; } return reactivated ? RetailObjectActivityResult.Reactivated : RetailObjectActivityResult.Active; } private static void SetActive( RetailObjectQuantumClock clock, PhysicsBody? body, bool active) { if (active) clock.Activate(); else clock.Deactivate(); if (body is not null && !active) body.TransientState &= ~TransientStateFlags.Active; } } public enum RetailObjectActivityResult { Suspended, Inactive, Reactivated, Active, }