Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates. Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean. Co-authored-by: OpenAI Codex <codex@openai.com>
122 lines
3.7 KiB
C#
122 lines
3.7 KiB
C#
using System;
|
|
|
|
namespace AcDream.Core.Physics;
|
|
|
|
/// <summary>
|
|
/// Allocation-free port of <c>CPhysicsObj::update_object</c>
|
|
/// (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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retail <c>set_active(0)</c>: suppress the full object path without
|
|
/// advancing or rebasing <c>update_time</c>.
|
|
/// </summary>
|
|
public void Deactivate() => IsActive = false;
|
|
|
|
/// <summary>
|
|
/// Retail <c>set_active(1)</c>. An inactive-to-active edge rebases
|
|
/// <c>update_time</c> to the current timer, so the reactivation frame does
|
|
/// not catch up suppressed time.
|
|
/// </summary>
|
|
/// <returns>True only when this call performed the reactivation edge.</returns>
|
|
public bool Activate()
|
|
{
|
|
if (IsActive)
|
|
return false;
|
|
IsActive = true;
|
|
_pending = 0.0;
|
|
return true;
|
|
}
|
|
|
|
public void Reset() => _pending = 0.0;
|
|
|
|
/// <summary>
|
|
/// Retail <c>CPhysicsObj::prepare_to_enter_world</c> (0x00511FA0): rebase
|
|
/// <c>update_time</c> 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.
|
|
/// </summary>
|
|
public void ResetForEnterWorld(bool isStatic = false)
|
|
{
|
|
_pending = 0.0;
|
|
if (!isStatic)
|
|
IsActive = true;
|
|
}
|
|
|
|
private const double FrameEpsilon = 0.000199999995;
|
|
}
|
|
|
|
/// <summary>
|
|
/// How the owning live-object lifecycle treats this render frame before
|
|
/// entering retail <c>CPhysicsObj::update_object</c>.
|
|
/// </summary>
|
|
public enum RetailObjectClockDisposition
|
|
{
|
|
Advance,
|
|
Suspend,
|
|
}
|
|
|
|
/// <summary>Compact result of one retail object-clock admission pass.</summary>
|
|
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;
|
|
}
|
|
}
|