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; } }