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
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -303,6 +303,37 @@ public class UpdateMotionTests
|
|||
Assert.Equal(1.25f, result.Value.MotionState.ForwardSpeed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesSequenceNumbersAndAutonomyFlag()
|
||||
{
|
||||
// L.2g S1 (DEV-6): the three staleness stamps + autonomy flag must
|
||||
// survive parsing — retail gates every 0xF74C on them
|
||||
// (INSTANCE_TS at dispatch, MOVEMENT_TS + SERVER_CONTROLLED_MOVE_TS
|
||||
// in CPhysics::SetObjectMovement 0x00509690, which also stores
|
||||
// last_move_was_autonomous).
|
||||
var body = new byte[4 + 4 + 2 + 6 + 4 + 4];
|
||||
int p = 0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x50001234u); p += 4;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0102); p += 2; // instanceSeq
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0304); p += 2; // movementSeq
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0506); p += 2; // serverControlSeq
|
||||
body[p++] = 1; // isAutonomous
|
||||
p += 1; // Align(4) pad
|
||||
body[p++] = 0; // movementType = Invalid
|
||||
body[p++] = 0; // motionFlags
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x003D); p += 2; // outer stance
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0u); p += 4; // no IMS flags
|
||||
|
||||
var result = UpdateMotion.TryParse(body);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal((ushort)0x0102, result!.Value.InstanceSequence);
|
||||
Assert.Equal((ushort)0x0304, result.Value.MovementSequence);
|
||||
Assert.Equal((ushort)0x0506, result.Value.ServerControlSequence);
|
||||
Assert.True(result.Value.IsAutonomous);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesMoveToObjectTargetGuidAndOrigin()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue