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:
Erik 2026-07-02 15:44:06 +02:00
parent fb3ee0544a
commit cb74e64343
9 changed files with 464 additions and 18 deletions

View file

@ -424,6 +424,21 @@ public sealed class CreateObjectTests
Assert.Equal(7.5f, p.Workmanship);
}
[Fact]
public void TryParse_MovementSequence_SurfacedFromTimestampBlock()
{
// L.2g S1 (DEV-6): index 1 of the 9-u16 PhysicsDesc timestamp block
// is ObjectMovement (ACE WorldObject_Networking.cs:412) — it seeds
// MotionSequenceGate's MOVEMENT_TS so post-spawn UpdateMotion events
// are judged against the entity's live sequence, not zero.
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
guid: 0x50000030u, name: "Runner", itemType: 0x10u,
movementSeq: 0x9000);
var parsed = CreateObject.TryParse(body);
Assert.NotNull(parsed);
Assert.Equal((ushort)0x9000, parsed!.Value.MovementSequence);
}
[Fact]
public void TryParse_MidTailFieldsSet_StillReachesIconOverlay()
{
@ -467,7 +482,8 @@ public sealed class CreateObjectTests
uint? validLocations = null,
uint? currentWieldedLocation = null,
uint? priority = null,
float? workmanship = null)
float? workmanship = null,
ushort movementSeq = 0)
{
var bytes = new List<byte>();
WriteU32(bytes, CreateObject.Opcode);
@ -479,11 +495,12 @@ public sealed class CreateObjectTests
bytes.Add(0);
bytes.Add(0);
// PhysicsData: physics flags = 0, then PhysicsState u32, then 9 seq stamps.
// PhysicsData: physics flags = 0, then PhysicsState u32, then 9 seq stamps
// (PhysicsTimeStamp enum order; index 1 = ObjectMovement).
WriteU32(bytes, 0);
WriteU32(bytes, physicsState);
for (int i = 0; i < 9; i++)
WriteU16(bytes, 0);
WriteU16(bytes, i == 1 ? movementSeq : (ushort)0);
Align4(bytes);
// Fixed WeenieHeader prefix per ACE SerializeCreateObject.