feat(L.2g-S1): retail movement-event staleness gate (DEV-6)
Port retail's three-stamp inbound gate for 0xF74C UpdateMotion: - MotionSequenceGate (Core/Physics): CPhysicsObj::is_newer (0x00451ad0) wraparound u16 compare, verbatim per ACE PhysicsObj.is_newer (the BN pseudo-C setcc returns are garbled; ACE + branch structure are the oracle). Gates: INSTANCE_TS at dispatch (stale incarnation drops before any stamp is touched), MOVEMENT_TS strictly-newer (stamped BEFORE the server-control check, per CPhysics::SetObjectMovement 0x00509690), SERVER_CONTROLLED_MOVE_TS drop-when-stored-newer. - Seed from CreateObject's PhysicsDesc timestamp block (index 1 = ObjectMovement now parsed; ACE WorldObject_Networking.cs:411-420 order) — without seeding, entities whose movement sequence is past 0x8000 at spawn would drop every UM against a zero stamp. Adopt-on-first / advance-only-after, so the #138 rehydrate replay of retained spawns cannot regress live stamps. - UpdateMotion + EntitySpawn now carry instance/movement/serverControl sequences + isAutonomous (was parsed-past; isAutonomous feeds the S2 funnel's last_move_was_autonomous). Gate wired at the top of OnLiveMotionUpdated before any state mutation; [UM_STALE] diag under ACDREAM_DUMP_MOTION / ACDREAM_REMOTE_VEL_DIAG; gate dropped with the entity on DeleteObject. Register: AD-32 added (adopt-newer-incarnation instead of retail's QueueBlobForObject); TS-26 updated (UM side closed, UP side open). Deviation map: docs/research/2026-07-02-inbound-motion-deviation-map.md. 19 new gate tests + parser coverage; full suite 3276 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
fb3ee0544a
commit
cb74e64343
9 changed files with 464 additions and 18 deletions
119
src/AcDream.Core/Physics/MotionSequenceGate.cs
Normal file
119
src/AcDream.Core/Physics/MotionSequenceGate.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using System;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Per-entity staleness gate for inbound movement events (0xF74C), ported
|
||||
/// from retail (L.2g S1, closes deviation DEV-6 for the UM path).
|
||||
///
|
||||
/// <para>Retail keeps a <c>update_times[NUM_PHYSICS_TS]</c> array of u16
|
||||
/// stamps on every <c>CPhysicsObj</c> (acclient.h:6084 —
|
||||
/// <c>PhysicsTimeStamp</c>) and rejects out-of-order network events with a
|
||||
/// wraparound-aware compare. Three of those stamps gate movement events:</para>
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item><b>INSTANCE_TS (8)</b> — checked at dispatch
|
||||
/// (<c>ACSmartBox::DispatchSmartBoxEvent</c> 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 (<c>SmartBox::QueueBlobForObject</c>) until that object
|
||||
/// version exists; acdream adopts-and-applies instead — register row
|
||||
/// AD-32 records the divergence.</item>
|
||||
/// <item><b>MOVEMENT_TS (1)</b> — <c>CPhysics::SetObjectMovement</c>
|
||||
/// (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.</item>
|
||||
/// <item><b>SERVER_CONTROLLED_MOVE_TS (5)</b> — 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.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>The compare itself is <c>CPhysicsObj::is_newer</c> (0x00451ad0);
|
||||
/// the Binary Ninja pseudo-C mangles its setcc returns, so the port follows
|
||||
/// ACE's verbatim <c>PhysicsObj.is_newer</c> (PhysicsObj.cs:2853-2859),
|
||||
/// cross-checked against the decomp's branch structure.</para>
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// <c>CPhysicsObj::is_newer</c> (0x00451ad0): true when
|
||||
/// <paramref name="newStamp"/> is newer than <paramref name="oldStamp"/>
|
||||
/// under u16 wraparound — when the absolute difference exceeds 0x7fff
|
||||
/// the numerically SMALLER value is the newer one (the counter wrapped).
|
||||
/// </summary>
|
||||
public static bool IsNewer(ushort oldStamp, ushort newStamp)
|
||||
{
|
||||
if (Math.Abs(newStamp - oldStamp) > short.MaxValue)
|
||||
return newStamp < oldStamp;
|
||||
return oldStamp < newStamp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seed the stamps from the entity's CreateObject PhysicsDesc timestamp
|
||||
/// block. Retail initializes <c>update_times</c> wholesale from the 9
|
||||
/// u16s at the tail of PhysicsDesc (ACE
|
||||
/// <c>WorldObject_Networking.cs:411-420</c> 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.
|
||||
///
|
||||
/// <para>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.</para>
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue