feat(physics): port retail complete object frame pipeline
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>
This commit is contained in:
parent
31a0889f08
commit
f961d70023
77 changed files with 12513 additions and 1871 deletions
122
src/AcDream.Core/Physics/RetailObjectQuantumClock.cs
Normal file
122
src/AcDream.Core/Physics/RetailObjectQuantumClock.cs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue