acdream/src/AcDream.Core/Physics/RetailObjectActivityGate.cs
Erik f961d70023 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>
2026-07-20 09:10:31 +02:00

101 lines
3.1 KiB
C#

using System.Numerics;
namespace AcDream.Core.Physics;
/// <summary>
/// Ports the lifecycle and 96-world-unit Active gate at the head of retail
/// <c>CPhysicsObj::update_object</c> (0x00515D10) plus the inactive branch of
/// <c>UpdateObjectInternal</c> (0x005156B0).
/// </summary>
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,
}