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