using System; namespace AcDream.Core.Physics; /// /// Allocation-free port of CPhysicsObj::update_object /// (0x00515D10). One clock belongs to one logical physics object. It retains /// sub-minimum elapsed time, subdivides long frames into complete object /// updates, and discards stale gaps greater than retail's huge quantum. /// public sealed class RetailObjectQuantumClock { private double _pending; public double PendingSeconds => _pending; public bool IsActive { get; private set; } = true; public RetailObjectQuantumBatch Advance(double elapsedSeconds) { if (double.IsNaN(elapsedSeconds) || elapsedSeconds < 0.0) { _pending = 0.0; return new RetailObjectQuantumBatch(0, 0f, Discarded: true); } double elapsed = _pending + elapsedSeconds; if (elapsed <= FrameEpsilon) { _pending = 0.0; return default; } if (elapsed > PhysicsBody.HugeQuantum) { _pending = 0.0; return new RetailObjectQuantumBatch(0, 0f, Discarded: true); } int fullSteps = 0; while (elapsed > PhysicsBody.MaxQuantum) { fullSteps++; elapsed -= PhysicsBody.MaxQuantum; } float remainder = 0f; if (elapsed > PhysicsBody.MinQuantum) { remainder = (float)elapsed; elapsed = 0.0; } _pending = elapsed; return new RetailObjectQuantumBatch(fullSteps, remainder, Discarded: false); } /// /// Retail set_active(0): suppress the full object path without /// advancing or rebasing update_time. /// public void Deactivate() => IsActive = false; /// /// Retail set_active(1). An inactive-to-active edge rebases /// update_time to the current timer, so the reactivation frame does /// not catch up suppressed time. /// /// True only when this call performed the reactivation edge. public bool Activate() { if (IsActive) return false; IsActive = true; _pending = 0.0; return true; } public void Reset() => _pending = 0.0; /// /// Retail CPhysicsObj::prepare_to_enter_world (0x00511FA0): rebase /// update_time to the current timer and immediately set Active when /// the object is not Static. A Static object retains its prior Active bit; /// normal initial entry and re-entry arrive here inactive. The next elapsed /// frame of a non-Static object is therefore eligible for ordinary object- /// quantum admission; there is no second activation frame to discard. /// public void ResetForEnterWorld(bool isStatic = false) { _pending = 0.0; if (!isStatic) IsActive = true; } private const double FrameEpsilon = 0.000199999995; } /// /// How the owning live-object lifecycle treats this render frame before /// entering retail CPhysicsObj::update_object. /// public enum RetailObjectClockDisposition { Advance, Suspend, } /// Compact result of one retail object-clock admission pass. public readonly record struct RetailObjectQuantumBatch( int FullSteps, float Remainder, bool Discarded) { public int Count => FullSteps + (Remainder > 0f ? 1 : 0); public float GetQuantum(int index) { if ((uint)index >= (uint)Count) throw new ArgumentOutOfRangeException(nameof(index)); return index < FullSteps ? PhysicsBody.MaxQuantum : Remainder; } }