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
|
|
@ -382,6 +382,16 @@ public sealed class GameWindow : IDisposable
|
|||
/// </summary>
|
||||
private readonly Dictionary<uint, RemoteMotion> _remoteDeadReckon = new();
|
||||
|
||||
/// <summary>
|
||||
/// L.2g S1 (DEV-6): per-entity inbound movement-event staleness gates,
|
||||
/// keyed by server guid. Retail keeps these stamps on CPhysicsObj
|
||||
/// (update_times); seeded from CreateObject's PhysicsDesc timestamp
|
||||
/// block in <see cref="OnLiveEntitySpawnedLocked"/>, consulted at the
|
||||
/// top of <see cref="OnLiveMotionUpdated"/>, dropped with the entity in
|
||||
/// <see cref="OnLiveEntityDeleted"/>.
|
||||
/// </summary>
|
||||
private readonly Dictionary<uint, AcDream.Core.Physics.MotionSequenceGate> _motionSequenceGates = new();
|
||||
|
||||
/// <summary>
|
||||
/// Per-remote-entity physics + motion stack — verbatim application of
|
||||
/// retail's client-side motion pipeline to every remote. Mirrors
|
||||
|
|
@ -3010,6 +3020,18 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
_liveSpawnReceived++;
|
||||
|
||||
// L.2g S1 (DEV-6): seed the movement-event staleness gate from the
|
||||
// CreateObject PhysicsDesc timestamp block, as retail seeds
|
||||
// update_times at object creation. Seed() adopts wholesale on first
|
||||
// sight and advance-only afterward, so the #138 rehydrate replay of
|
||||
// a RETAINED spawn through this handler cannot regress live stamps.
|
||||
if (!_motionSequenceGates.TryGetValue(spawn.Guid, out var seqGate))
|
||||
{
|
||||
seqGate = new AcDream.Core.Physics.MotionSequenceGate();
|
||||
_motionSequenceGates[spawn.Guid] = seqGate;
|
||||
}
|
||||
seqGate.Seed(spawn.InstanceSequence, spawn.MovementSequence, spawn.ServerControlSequence);
|
||||
|
||||
// De-dup: the server re-sends CreateObject for the same guid in
|
||||
// several situations (visibility refresh, landblock crossing,
|
||||
// appearance update). Without cleanup the OLD copy remains in
|
||||
|
|
@ -3882,6 +3904,11 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
private void OnLiveEntityDeleted(AcDream.Core.Net.Messages.DeleteObject.Parsed delete)
|
||||
{
|
||||
// L.2g S1: drop the staleness gate with the entity — a subsequent
|
||||
// re-create adopts fresh stamps from its CreateObject (retail's
|
||||
// update_times die with the CPhysicsObj).
|
||||
_motionSequenceGates.Remove(delete.Guid);
|
||||
|
||||
if (RemoveLiveEntityByServerGuid(delete.Guid)
|
||||
&& Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||
{
|
||||
|
|
@ -4233,6 +4260,34 @@ public sealed class GameWindow : IDisposable
|
|||
if (!_entitiesByServerGuid.TryGetValue(update.Guid, out var entity)) return;
|
||||
if (!_animatedEntities.TryGetValue(entity.Id, out var ae)) return;
|
||||
|
||||
// L.2g S1 (DEV-6): retail staleness gate — BEFORE any state mutation.
|
||||
// Retail drops stale/duplicate/superseded movement events at
|
||||
// DispatchSmartBoxEvent (INSTANCE_TS, pseudo-C:357214) +
|
||||
// CPhysics::SetObjectMovement (MOVEMENT_TS strictly-newer +
|
||||
// SERVER_CONTROLLED_MOVE_TS, 0x00509690). Without this, a reordered
|
||||
// straggler re-applies an old gait or un-stops a stop.
|
||||
if (!_motionSequenceGates.TryGetValue(update.Guid, out var seqGate))
|
||||
{
|
||||
// UM for an entity whose CreateObject we never parsed (rare —
|
||||
// the entity lookup above implies a spawn). Adopt-on-first-seed
|
||||
// keeps the gate correct from this event onward.
|
||||
seqGate = new AcDream.Core.Physics.MotionSequenceGate();
|
||||
_motionSequenceGates[update.Guid] = seqGate;
|
||||
seqGate.Seed(update.InstanceSequence, update.MovementSequence, update.ServerControlSequence);
|
||||
}
|
||||
else if (!seqGate.TryAcceptMovementEvent(
|
||||
update.InstanceSequence, update.MovementSequence, update.ServerControlSequence))
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1"
|
||||
|| Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[UM_STALE] guid={update.Guid:X8} inst={update.InstanceSequence} "
|
||||
+ $"mov={update.MovementSequence} sc={update.ServerControlSequence} dropped");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// #39 (2026-05-06): stamp the per-remote LastUMTime so the
|
||||
// UP-velocity fallback path in ApplyServerControlledVelocityCycle
|
||||
// can skip refinement while a UM is fresh. UMs are authoritative
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue