235 lines
9 KiB
C#
235 lines
9 KiB
C#
using System;
|
|
|
|
namespace AcDream.Core.Physics;
|
|
|
|
public enum CreateObjectTimestampDisposition
|
|
{
|
|
StaleGeneration,
|
|
InitialGeneration,
|
|
ExistingGeneration,
|
|
NewGeneration,
|
|
}
|
|
|
|
public enum PositionTimestampDisposition
|
|
{
|
|
Rejected,
|
|
Apply,
|
|
ForcePosition,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Per-object retail <c>update_times[NUM_PHYSICS_TS]</c> gate. Channel order
|
|
/// is <c>PhysicsTimeStamp</c> from <c>acclient.h:6084</c>. The comparator and
|
|
/// channel mutation order are ported from <c>CPhysicsObj::is_newer</c>
|
|
/// 0x00451AD0, <c>CPhysicsObj::newer_event</c> 0x00451B10,
|
|
/// <c>SmartBox::DoSetState</c> 0x004520D0,
|
|
/// <c>SmartBox::DoVectorUpdate</c> 0x004521C0, and
|
|
/// <c>SmartBox::HandleReceivedPosition</c> 0x00453FD0.
|
|
/// </summary>
|
|
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];
|
|
|
|
/// <summary>
|
|
/// Retail u16 wrap comparison. At the exact half-range ambiguity
|
|
/// (0x8000), the numerically lower stamp is considered newer.
|
|
/// </summary>
|
|
public static bool IsNewer(ushort oldStamp, ushort newStamp)
|
|
{
|
|
int distance = Math.Abs((int)newStamp - oldStamp);
|
|
return distance > 0x7FFF ? newStamp < oldStamp : oldStamp < newStamp;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public CreateObjectTimestampDisposition SeedForCreateObject(
|
|
ushort position,
|
|
ushort movement,
|
|
ushort state,
|
|
ushort vector,
|
|
ushort teleport,
|
|
ushort serverControlledMove,
|
|
ushort forcePosition,
|
|
ushort objDesc,
|
|
ushort instance)
|
|
{
|
|
Span<ushort> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Classifies an incoming CreateObject generation without consuming any
|
|
/// channel timestamp. Runtime uses this to preflight only admissions which
|
|
/// can create or replace a canonical incarnation; equal/stale packets must
|
|
/// still reach the normal per-channel retail gates unchanged.
|
|
/// </summary>
|
|
public CreateObjectTimestampDisposition PreviewCreateObject(
|
|
ushort instance)
|
|
{
|
|
if (!_seeded)
|
|
return CreateObjectTimestampDisposition.InitialGeneration;
|
|
ushort currentInstance = _timestamps[Instance];
|
|
if (IsNewer(currentInstance, instance))
|
|
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);
|
|
|
|
/// <summary>
|
|
/// ParentEvent and PickupEvent share POSITION_TS with UpdatePosition.
|
|
/// Retail exact-gates INSTANCE_TS, then strictly advances POSITION_TS.
|
|
/// </summary>
|
|
public bool TryAcceptPositionChannelEvent(ushort instance, ushort position) =>
|
|
TryAcceptInstance(instance) && AdvanceStrict(Position, position);
|
|
|
|
public bool IsCurrentInstance(ushort instance) => TryAcceptInstance(instance);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public bool IsFreshTeleportStart(ushort teleport) =>
|
|
_seeded && !IsNewer(teleport, _timestamps[Teleport]);
|
|
|
|
/// <summary>
|
|
/// Delete/Pickup events only affect the exact live incarnation. Retail
|
|
/// drops older deletes and queues newer-incarnation deletes without
|
|
/// touching the current object.
|
|
/// </summary>
|
|
public bool TryAcceptDeleteEvent(ushort instance, bool isLocalPlayer = false) =>
|
|
!isLocalPlayer && _seeded && instance == _timestamps[Instance];
|
|
|
|
/// <summary>
|
|
/// Ports the timestamp portion of <c>HandleReceivedPosition</c>. 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.
|
|
/// </summary>
|
|
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];
|
|
}
|
|
}
|