using System; namespace AcDream.Core.Physics; public enum CreateObjectTimestampDisposition { StaleGeneration, InitialGeneration, ExistingGeneration, NewGeneration, } public enum PositionTimestampDisposition { Rejected, Apply, ForcePosition, } /// /// Per-object retail update_times[NUM_PHYSICS_TS] gate. Channel order /// is PhysicsTimeStamp from acclient.h:6084. The comparator and /// channel mutation order are ported from CPhysicsObj::is_newer /// 0x00451AD0, CPhysicsObj::newer_event 0x00451B10, /// SmartBox::DoSetState 0x004520D0, /// SmartBox::DoVectorUpdate 0x004521C0, and /// SmartBox::HandleReceivedPosition 0x00453FD0. /// public sealed class PhysicsTimestampGate { private readonly ushort[] _timestamps = new ushort[9]; private bool _seeded; private const int Position = 0; private const int Movement = 1; private const int State = 2; private const int Vector = 3; private const int Teleport = 4; private const int ServerControlledMove = 5; private const int ForcePosition = 6; private const int ObjDesc = 7; private const int Instance = 8; public ushort PositionTimestamp => _timestamps[Position]; public ushort MovementTimestamp => _timestamps[Movement]; public ushort StateTimestamp => _timestamps[State]; public ushort VectorTimestamp => _timestamps[Vector]; public ushort TeleportTimestamp => _timestamps[Teleport]; public ushort ServerControlledMoveTimestamp => _timestamps[ServerControlledMove]; public ushort ForcePositionTimestamp => _timestamps[ForcePosition]; public ushort ObjDescTimestamp => _timestamps[ObjDesc]; public ushort InstanceTimestamp => _timestamps[Instance]; /// /// Retail u16 wrap comparison. At the exact half-range ambiguity /// (0x8000), the numerically lower stamp is considered newer. /// public static bool IsNewer(ushort oldStamp, ushort newStamp) { int distance = Math.Abs((int)newStamp - oldStamp); return distance > 0x7FFF ? newStamp < oldStamp : oldStamp < newStamp; } /// /// Begins CreateObject processing. A newer INSTANCE_TS starts a fresh /// generation and replaces every channel wholesale. A same-generation /// CreateObject leaves all channels untouched so its Position, Parent, /// Pickup, Movement, State, Vector, and ObjDesc branches can pass through /// the same per-channel methods used by their standalone packets. /// public CreateObjectTimestampDisposition SeedForCreateObject( ushort position, ushort movement, ushort state, ushort vector, ushort teleport, ushort serverControlledMove, ushort forcePosition, ushort objDesc, ushort instance) { Span incoming = [position, movement, state, vector, teleport, serverControlledMove, forcePosition, objDesc, instance]; if (!_seeded) { incoming.CopyTo(_timestamps); _seeded = true; return CreateObjectTimestampDisposition.InitialGeneration; } ushort currentInstance = _timestamps[Instance]; if (IsNewer(currentInstance, instance)) { incoming.CopyTo(_timestamps); return CreateObjectTimestampDisposition.NewGeneration; } if (IsNewer(instance, currentInstance)) return CreateObjectTimestampDisposition.StaleGeneration; return CreateObjectTimestampDisposition.ExistingGeneration; } public bool TryAcceptMovementEvent( ushort instance, ushort movement, ushort serverControlledMove) { if (!TryAcceptInstance(instance)) return false; if (!AdvanceStrict(Movement, movement)) return false; // Retail consumes MOVEMENT_TS before checking the server-control // era. Equal server-control stamps pass. if (IsNewer(serverControlledMove, _timestamps[ServerControlledMove])) return false; _timestamps[ServerControlledMove] = serverControlledMove; return true; } public bool TryAcceptStateEvent(ushort instance, ushort state) => TryAcceptInstance(instance) && AdvanceStrict(State, state); public bool TryAcceptVectorEvent(ushort instance, ushort vector) => TryAcceptInstance(instance) && AdvanceStrict(Vector, vector); public bool TryAcceptObjDescEvent(ushort instance, ushort objDesc) => TryAcceptInstance(instance) && AdvanceStrict(ObjDesc, objDesc); /// /// ParentEvent and PickupEvent share POSITION_TS with UpdatePosition. /// Retail exact-gates INSTANCE_TS, then strictly advances POSITION_TS. /// public bool TryAcceptPositionChannelEvent(ushort instance, ushort position) => TryAcceptInstance(instance) && AdvanceStrict(Position, position); public bool IsCurrentInstance(ushort instance) => TryAcceptInstance(instance); /// /// Standalone PlayerTeleport (F751) is admitted when it is equal to or /// newer than the current TELEPORT_TS. It does not advance that channel; /// the destination Position packet does. /// public bool IsFreshTeleportStart(ushort teleport) => _seeded && !IsNewer(teleport, _timestamps[Teleport]); /// /// Delete/Pickup events only affect the exact live incarnation. Retail /// drops older deletes and queues newer-incarnation deletes without /// touching the current object. /// public bool TryAcceptDeleteEvent(ushort instance, bool isLocalPlayer = false) => !isLocalPlayer && _seeded && instance == _timestamps[Instance]; /// /// Ports the timestamp portion of HandleReceivedPosition. POSITION /// is tentatively advanced, then restored when a newer TELEPORT stamp is /// already known. A fresh teleport advances its own channel. The local /// player's FORCE_POSITION channel is independent; non-player objects do /// not consume it. /// public PositionTimestampDisposition TryAcceptPositionEvent( ushort instance, ushort position, ushort teleport, ushort forcePosition, bool isLocalPlayer) { if (!TryAcceptInstance(instance)) return PositionTimestampDisposition.Rejected; if (isLocalPlayer && IsNewer(_timestamps[ForcePosition], forcePosition)) { _timestamps[ForcePosition] = forcePosition; // SmartBox::HandleReceivedPosition 0x00453FD0: a fresh local // FORCE_POSITION whose teleport is exactly equal blips immediately, // assigns POSITION_TS directly (even equal/older), preserves the // current heading, sends a position event, and returns WITHOUT // advancing TELEPORT_TS. if (teleport == _timestamps[Teleport]) { _timestamps[Position] = position; return PositionTimestampDisposition.ForcePosition; } } ushort previousPosition = _timestamps[Position]; if (!AdvanceStrict(Position, position)) return PositionTimestampDisposition.Rejected; if (IsNewer(teleport, _timestamps[Teleport])) { _timestamps[Position] = previousPosition; return PositionTimestampDisposition.Rejected; } if (IsNewer(_timestamps[Teleport], teleport)) _timestamps[Teleport] = teleport; return PositionTimestampDisposition.Apply; } private bool AdvanceStrict(int channel, ushort incoming) { if (!IsNewer(_timestamps[channel], incoming)) return false; _timestamps[channel] = incoming; return true; } private bool TryAcceptInstance(ushort incoming) { // Retail applies only the current incarnation: older packets drop and // future-incarnation packets queue. Until LiveEntityRuntime owns that // queue, acdream drops both mismatches (AD-32) so a future packet can // never mutate or poison the current generation's channel stamps. return _seeded && incoming == _timestamps[Instance]; } }