using System;
namespace AcDream.Core.Physics;
///
/// Per-entity staleness gate for inbound movement events (0xF74C), ported
/// from retail (L.2g S1, closes deviation DEV-6 for the UM path).
///
/// Retail keeps a update_times[NUM_PHYSICS_TS] array of u16
/// stamps on every CPhysicsObj (acclient.h:6084 —
/// PhysicsTimeStamp) and rejects out-of-order network events with a
/// wraparound-aware compare. Three of those stamps gate movement events:
///
///
/// - INSTANCE_TS (8) — checked at dispatch
/// (ACSmartBox::DispatchSmartBoxEvent case 0xF74C,
/// acclient_2013_pseudo_c.txt:357214-357239): an event stamped with an
/// OLDER object incarnation than the one we know is dropped before any
/// other stamp is touched. Retail additionally QUEUES events for a NEWER
/// incarnation (SmartBox::QueueBlobForObject) until that object
/// version exists; acdream adopts-and-applies instead — register row
/// AD-32 records the divergence.
/// - MOVEMENT_TS (1) — CPhysics::SetObjectMovement
/// (0x00509690, acclient_2013_pseudo_c.txt:271370) applies an event only
/// when its movement sequence is STRICTLY newer than the stored stamp
/// (equal = duplicate delivery = drop), and stamps BEFORE evaluating the
/// server-control gate — a movement sequence is consumed even when the
/// event is subsequently dropped for stale server control.
/// - SERVER_CONTROLLED_MOVE_TS (5) — same function: the event is
/// dropped when the STORED server-control stamp is strictly newer than
/// the incoming one (a newer server-control era has already been seen);
/// equal passes and re-stamps.
///
///
/// The compare itself is CPhysicsObj::is_newer (0x00451ad0);
/// the Binary Ninja pseudo-C mangles its setcc returns, so the port follows
/// ACE's verbatim PhysicsObj.is_newer (PhysicsObj.cs:2853-2859),
/// cross-checked against the decomp's branch structure.
///
public sealed class MotionSequenceGate
{
private ushort _instanceTs; // update_times[INSTANCE_TS = 8]
private ushort _movementTs; // update_times[MOVEMENT_TS = 1]
private ushort _serverControlTs; // update_times[SERVER_CONTROLLED_MOVE_TS = 5]
private bool _seeded;
///
/// CPhysicsObj::is_newer (0x00451ad0): true when
/// is newer than
/// under u16 wraparound — when the absolute difference exceeds 0x7fff
/// the numerically SMALLER value is the newer one (the counter wrapped).
///
public static bool IsNewer(ushort oldStamp, ushort newStamp)
{
if (Math.Abs(newStamp - oldStamp) > short.MaxValue)
return newStamp < oldStamp;
return oldStamp < newStamp;
}
///
/// Seed the stamps from the entity's CreateObject PhysicsDesc timestamp
/// block. Retail initializes update_times wholesale from the 9
/// u16s at the tail of PhysicsDesc (ACE
/// WorldObject_Networking.cs:411-420 writes them in
/// PhysicsTimeStamp enum order); without this, an entity whose movement
/// sequence is already past 0x8000 at spawn would have every subsequent
/// movement event judged stale against a zero stamp.
///
/// The first Seed adopts the stamps wholesale (fresh object);
/// subsequent Seeds only move stamps FORWARD. This makes the #138
/// rehydrate path (which replays a RETAINED CreateObject through the
/// spawn handler) a no-op instead of a stamp regression, while a genuine
/// wire re-create (server sequences only advance) still seeds correctly.
/// Retail has no equivalent replay path — its objects keep their stamps
/// for their whole lifetime — so advance-only is the faithful mapping.
///
public void Seed(ushort instanceSeq, ushort movementSeq, ushort serverControlSeq)
{
if (!_seeded)
{
_instanceTs = instanceSeq;
_movementTs = movementSeq;
_serverControlTs = serverControlSeq;
_seeded = true;
return;
}
if (IsNewer(_instanceTs, instanceSeq)) _instanceTs = instanceSeq;
if (IsNewer(_movementTs, movementSeq)) _movementTs = movementSeq;
if (IsNewer(_serverControlTs, serverControlSeq)) _serverControlTs = serverControlSeq;
}
///
/// Apply the retail three-stamp gate to an inbound movement event.
/// Returns true when the event should be applied (stamps updated),
/// false when it must be dropped as stale/duplicate/superseded.
///
public bool TryAcceptMovementEvent(ushort instanceSeq, ushort movementSeq, ushort serverControlSeq)
{
// Dispatch-level incarnation gate — runs before any other stamp is
// touched (retail drops at DispatchSmartBoxEvent, never reaching
// SetObjectMovement).
if (IsNewer(instanceSeq, _instanceTs))
return false;
_instanceTs = instanceSeq; // adopt equal-or-newer (AD-32; retail queues newer)
// Gate 1: movement sequence must be strictly newer.
if (!IsNewer(_movementTs, movementSeq))
return false;
_movementTs = movementSeq; // stamped BEFORE the server-control gate, per retail
// Gate 2: drop when a newer server-control era has already been seen.
if (IsNewer(serverControlSeq, _serverControlTs))
return false;
_serverControlTs = serverControlSeq;
return true;
}
}