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>
101 lines
3.1 KiB
C#
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,
|
|
}
|