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,6 +119,9 @@ public static class CreateObject
|
|||
ushort TeleportSequence = 0,
|
||||
ushort ServerControlSequence = 0,
|
||||
ushort ForcePositionSequence = 0,
|
||||
// L.2g S1 (DEV-6): ObjectMovement stamp (timestamp block index 1)
|
||||
// seeds MotionSequenceGate's MOVEMENT_TS at spawn.
|
||||
ushort MovementSequence = 0,
|
||||
uint? PhysicsState = null,
|
||||
uint? ObjectDescriptionFlags = null,
|
||||
// L.3b (2026-04-30): per-object friction + elasticity from the
|
||||
|
|
@ -531,11 +534,13 @@ public static class CreateObject
|
|||
if ((physicsFlags & PhysicsDescriptionFlag.DefaultScriptIntensity) != 0) pos += 4;
|
||||
|
||||
// 9 sequence timestamps, always present at end of PhysicsData.
|
||||
// Indices per holtburger: 0=position, 4=teleport, 5=serverControl,
|
||||
// 6=forcePosition, 8=instance.
|
||||
// PhysicsTimeStamp enum order (acclient.h:6084; ACE
|
||||
// WorldObject_Networking.cs:411-420): 0=position, 1=movement,
|
||||
// 4=teleport, 5=serverControl, 6=forcePosition, 8=instance.
|
||||
if (body.Length - pos < 9 * 2) return PartialResult();
|
||||
var seqSpan = body.Slice(pos, 9 * 2);
|
||||
ushort instanceSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(8 * 2));
|
||||
ushort movementSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(1 * 2));
|
||||
ushort teleportSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(4 * 2));
|
||||
ushort serverControlSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(5 * 2));
|
||||
ushort forcePositionSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(6 * 2));
|
||||
|
|
@ -847,6 +852,7 @@ public static class CreateObject
|
|||
return new Parsed(guid, position, setupTableId, animParts,
|
||||
textureChanges, subPalettes, basePaletteId, objScale, name, itemType, motionState, motionTableId,
|
||||
instanceSeq, teleportSeq, serverControlSeq, forcePositionSeq,
|
||||
movementSeq,
|
||||
physicsState, objectDescriptionFlags,
|
||||
friction, elasticity,
|
||||
IconId: iconId,
|
||||
|
|
|
|||
|
|
@ -57,10 +57,21 @@ public static class UpdateMotion
|
|||
/// command is nullable because the <c>ForwardCommand</c> flag may be
|
||||
/// unset in the InterpretedMotionState; the stance is always present
|
||||
/// (even if 0, meaning "no specific stance").
|
||||
///
|
||||
/// <para>L.2g S1 (DEV-6): the three staleness stamps + autonomy flag are
|
||||
/// exposed for the retail gate (<see cref="AcDream.Core.Physics"/>
|
||||
/// <c>MotionSequenceGate</c>) — retail compares them against
|
||||
/// <c>update_times[INSTANCE_TS/MOVEMENT_TS/SERVER_CONTROLLED_MOVE_TS]</c>
|
||||
/// and stores <c>last_move_was_autonomous</c>
|
||||
/// (<c>CPhysics::SetObjectMovement</c> 0x00509690).</para>
|
||||
/// </summary>
|
||||
public readonly record struct Parsed(
|
||||
uint Guid,
|
||||
CreateObject.ServerMotionState MotionState);
|
||||
CreateObject.ServerMotionState MotionState,
|
||||
ushort InstanceSequence,
|
||||
ushort MovementSequence,
|
||||
ushort ServerControlSequence,
|
||||
bool IsAutonomous);
|
||||
|
||||
/// <summary>
|
||||
/// Parse a reassembled UpdateMotion body. <paramref name="body"/> must
|
||||
|
|
@ -82,8 +93,11 @@ public static class UpdateMotion
|
|||
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
|
||||
// ObjectInstance sequence (u16) — tracked but not used for pose.
|
||||
// ObjectInstance sequence (u16) — retail's dispatch-level
|
||||
// INSTANCE_TS staleness gate compares this against the object's
|
||||
// known incarnation (DispatchSmartBoxEvent case 0xF74C).
|
||||
if (body.Length - pos < 2) return null;
|
||||
ushort instanceSequence = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2;
|
||||
|
||||
// MovementData header: u16 movementSequence, u16 serverControlSequence,
|
||||
|
|
@ -102,8 +116,18 @@ public static class UpdateMotion
|
|||
// Previous version mistakenly reserved 8 bytes here, which shifted
|
||||
// every subsequent field by 2 and made every remote-char UpdateMotion
|
||||
// decode as garbage (stance read from the packed-flags dword).
|
||||
//
|
||||
// L.2g S1 (DEV-6): the header fields feed retail's
|
||||
// CPhysics::SetObjectMovement staleness gates + the
|
||||
// last_move_was_autonomous store, so parse them instead of
|
||||
// skipping.
|
||||
if (body.Length - pos < 6) return null;
|
||||
pos += 6;
|
||||
ushort movementSequence = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2;
|
||||
ushort serverControlSequence = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2;
|
||||
bool isAutonomous = body[pos] != 0;
|
||||
pos += 2; // u8 isAutonomous + Align(4) pad byte
|
||||
|
||||
// movementType u8, motionFlags u8, currentStyle u16
|
||||
if (body.Length - pos < 4) return null;
|
||||
|
|
@ -139,7 +163,7 @@ public static class UpdateMotion
|
|||
// MovementInvalid branch, just reached via the header'd path.
|
||||
// Includes the Commands list (MotionItem[]) that carries
|
||||
// Actions, emotes, and other one-shots not in ForwardCommand.
|
||||
if (body.Length - pos < 4) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType));
|
||||
if (body.Length - pos < 4) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType), instanceSequence, movementSequence, serverControlSequence, isAutonomous);
|
||||
uint packed = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
uint flags = packed & 0x7Fu;
|
||||
|
|
@ -162,13 +186,13 @@ public static class UpdateMotion
|
|||
|
||||
if ((flags & 0x1u) != 0)
|
||||
{
|
||||
if (body.Length - pos < 2) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType));
|
||||
if (body.Length - pos < 2) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType), instanceSequence, movementSequence, serverControlSequence, isAutonomous);
|
||||
currentStyle = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2;
|
||||
}
|
||||
if ((flags & 0x2u) != 0)
|
||||
{
|
||||
if (body.Length - pos < 2) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType));
|
||||
if (body.Length - pos < 2) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType), instanceSequence, movementSequence, serverControlSequence, isAutonomous);
|
||||
forwardCommand = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2;
|
||||
}
|
||||
|
|
@ -244,7 +268,8 @@ public static class UpdateMotion
|
|||
moveToParameters,
|
||||
moveToSpeed,
|
||||
moveToRunRate,
|
||||
moveToPath));
|
||||
moveToPath),
|
||||
instanceSequence, movementSequence, serverControlSequence, isAutonomous);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -109,7 +109,13 @@ public sealed class WorldSession : IDisposable
|
|||
uint? Priority = null,
|
||||
int? Structure = null,
|
||||
int? MaxStructure = null,
|
||||
float? Workmanship = null);
|
||||
float? Workmanship = null,
|
||||
// L.2g S1 (DEV-6): PhysicsDesc timestamp-block stamps that seed the
|
||||
// per-entity MotionSequenceGate (retail update_times INSTANCE_TS /
|
||||
// MOVEMENT_TS / SERVER_CONTROLLED_MOVE_TS).
|
||||
ushort InstanceSequence = 0,
|
||||
ushort MovementSequence = 0,
|
||||
ushort ServerControlSequence = 0);
|
||||
|
||||
/// <summary>Fires when the session finishes parsing a CreateObject.</summary>
|
||||
public event Action<EntitySpawn>? EntitySpawned;
|
||||
|
|
@ -131,7 +137,11 @@ public sealed class WorldSession : IDisposable
|
|||
/// </summary>
|
||||
public readonly record struct EntityMotionUpdate(
|
||||
uint Guid,
|
||||
CreateObject.ServerMotionState MotionState);
|
||||
CreateObject.ServerMotionState MotionState,
|
||||
ushort InstanceSequence,
|
||||
ushort MovementSequence,
|
||||
ushort ServerControlSequence,
|
||||
bool IsAutonomous);
|
||||
|
||||
/// <summary>
|
||||
/// Fires when the session parses a 0xF74C UpdateMotion game message.
|
||||
|
|
@ -825,7 +835,10 @@ public sealed class WorldSession : IDisposable
|
|||
parsed.Value.Priority,
|
||||
parsed.Value.Structure,
|
||||
parsed.Value.MaxStructure,
|
||||
parsed.Value.Workmanship));
|
||||
parsed.Value.Workmanship,
|
||||
InstanceSequence: parsed.Value.InstanceSequence,
|
||||
MovementSequence: parsed.Value.MovementSequence,
|
||||
ServerControlSequence: parsed.Value.ServerControlSequence));
|
||||
}
|
||||
}
|
||||
else if (op == DeleteObject.Opcode)
|
||||
|
|
@ -862,7 +875,11 @@ public sealed class WorldSession : IDisposable
|
|||
{
|
||||
MotionUpdated?.Invoke(new EntityMotionUpdate(
|
||||
motion.Value.Guid,
|
||||
motion.Value.MotionState));
|
||||
motion.Value.MotionState,
|
||||
motion.Value.InstanceSequence,
|
||||
motion.Value.MovementSequence,
|
||||
motion.Value.ServerControlSequence,
|
||||
motion.Value.IsAutonomous));
|
||||
}
|
||||
}
|
||||
else if (op == UpdatePosition.Opcode)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue