feat(net): track + echo movement sequence counters
Sprint 1a of the audit remediation plan. Extracts the 4 movement sequence counters from inbound server messages and echoes them in outbound MoveToState + AutonomousPosition instead of hardcoded zeros: - instanceSequence (slot 8 in CreateObject PhysicsData timestamps) - teleportSequence (slot 4, also from PlayerTeleport 0xF751) - serverControlSequence (slot 5) - forcePositionSequence (slot 6, also from UpdatePosition 0xF748) Source: holtburger player/types.rs:237-245, mutations.rs:182-706. The server uses these to detect stale/reordered movement packets. Previously all zeros → server couldn't distinguish epoch boundaries. Changes: - CreateObject.Parsed: +4 sequence fields extracted from timestamps - UpdatePosition.Parsed: +3 sequence fields from trailing u16s - WorldSession: tracks 4 counters, updates from CreateObject/ UpdatePosition/PlayerTeleport for the player's own GUID - GameWindow: passes tracked values to MoveToState.Build and AutonomousPosition.Build Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9e5258152d
commit
11974c2099
4 changed files with 74 additions and 18 deletions
|
|
@ -1753,10 +1753,10 @@ public sealed class GameWindow : IDisposable
|
||||||
cellId: wireCellId,
|
cellId: wireCellId,
|
||||||
position: wirePos,
|
position: wirePos,
|
||||||
rotation: wireRot,
|
rotation: wireRot,
|
||||||
instanceSequence: 0,
|
instanceSequence: _liveSession.InstanceSequence,
|
||||||
serverControlSequence: 0,
|
serverControlSequence: _liveSession.ServerControlSequence,
|
||||||
teleportSequence: 0,
|
teleportSequence: _liveSession.TeleportSequence,
|
||||||
forcePositionSequence: 0);
|
forcePositionSequence: _liveSession.ForcePositionSequence);
|
||||||
_liveSession.SendGameAction(body);
|
_liveSession.SendGameAction(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1768,10 +1768,10 @@ public sealed class GameWindow : IDisposable
|
||||||
cellId: wireCellId,
|
cellId: wireCellId,
|
||||||
position: wirePos,
|
position: wirePos,
|
||||||
rotation: wireRot,
|
rotation: wireRot,
|
||||||
instanceSequence: 0,
|
instanceSequence: _liveSession.InstanceSequence,
|
||||||
serverControlSequence: 0,
|
serverControlSequence: _liveSession.ServerControlSequence,
|
||||||
teleportSequence: 0,
|
teleportSequence: _liveSession.TeleportSequence,
|
||||||
forcePositionSequence: 0);
|
forcePositionSequence: _liveSession.ForcePositionSequence);
|
||||||
_liveSession.SendGameAction(body);
|
_liveSession.SendGameAction(body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,11 @@ public static class CreateObject
|
||||||
float? ObjScale,
|
float? ObjScale,
|
||||||
string? Name,
|
string? Name,
|
||||||
ServerMotionState? MotionState,
|
ServerMotionState? MotionState,
|
||||||
uint? MotionTableId);
|
uint? MotionTableId,
|
||||||
|
ushort InstanceSequence = 0,
|
||||||
|
ushort TeleportSequence = 0,
|
||||||
|
ushort ServerControlSequence = 0,
|
||||||
|
ushort ForcePositionSequence = 0);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The relevant subset of the server-sent <c>MovementData</c> /
|
/// The relevant subset of the server-sent <c>MovementData</c> /
|
||||||
|
|
@ -332,8 +336,15 @@ public static class CreateObject
|
||||||
if ((physicsFlags & PhysicsDescriptionFlag.DefaultScriptIntensity) != 0) pos += 4;
|
if ((physicsFlags & PhysicsDescriptionFlag.DefaultScriptIntensity) != 0) pos += 4;
|
||||||
|
|
||||||
// 9 sequence timestamps, always present at end of PhysicsData.
|
// 9 sequence timestamps, always present at end of PhysicsData.
|
||||||
|
// Indices per holtburger: 0=position, 4=teleport, 5=serverControl,
|
||||||
|
// 6=forcePosition, 8=instance.
|
||||||
if (body.Length - pos < 9 * 2) return PartialResult();
|
if (body.Length - pos < 9 * 2) return PartialResult();
|
||||||
pos += 9 * 2; // each sequence is a ushort
|
var seqSpan = body.Slice(pos, 9 * 2);
|
||||||
|
ushort instanceSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(8 * 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));
|
||||||
|
pos += 9 * 2;
|
||||||
AlignTo4(ref pos);
|
AlignTo4(ref pos);
|
||||||
|
|
||||||
// --- WeenieHeader: read just the Name field (second after flags). ---
|
// --- WeenieHeader: read just the Name field (second after flags). ---
|
||||||
|
|
@ -349,7 +360,8 @@ public static class CreateObject
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Parsed(guid, position, setupTableId, animParts,
|
return new Parsed(guid, position, setupTableId, animParts,
|
||||||
textureChanges, subPalettes, basePaletteId, objScale, name, motionState, motionTableId);
|
textureChanges, subPalettes, basePaletteId, objScale, name, motionState, motionTableId,
|
||||||
|
instanceSeq, teleportSeq, serverControlSeq, forcePositionSeq);
|
||||||
|
|
||||||
// Local helper: if we ran out of fields past PhysicsData, still
|
// Local helper: if we ran out of fields past PhysicsData, still
|
||||||
// return the useful prefix (guid/position/setup/animParts/textures/palettes/scale/motion).
|
// return the useful prefix (guid/position/setup/animParts/textures/palettes/scale/motion).
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,10 @@ public static class UpdatePosition
|
||||||
uint Guid,
|
uint Guid,
|
||||||
CreateObject.ServerPosition Position,
|
CreateObject.ServerPosition Position,
|
||||||
System.Numerics.Vector3? Velocity,
|
System.Numerics.Vector3? Velocity,
|
||||||
uint? PlacementId);
|
uint? PlacementId,
|
||||||
|
ushort InstanceSequence = 0,
|
||||||
|
ushort TeleportSequence = 0,
|
||||||
|
ushort ForcePositionSequence = 0);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse a reassembled UpdatePosition body. <paramref name="body"/>
|
/// Parse a reassembled UpdatePosition body. <paramref name="body"/>
|
||||||
|
|
@ -144,17 +147,24 @@ public static class UpdatePosition
|
||||||
pos += 4;
|
pos += 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We deliberately skip the four u16 sequence numbers that
|
// Four u16 sequence numbers: instance, position, teleport, forcePosition.
|
||||||
// follow; subscribers don't need them for simple rendering,
|
ushort instSeq = 0, teleSeq = 0, forceSeq = 0;
|
||||||
// and if the message is a deliberate out-of-order teleport the
|
if (body.Length - pos >= 8)
|
||||||
// server always follows up with a fresher update anyway.
|
{
|
||||||
|
instSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||||
|
// pos+2 = positionSequence (not tracked by movement)
|
||||||
|
teleSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos + 4));
|
||||||
|
forceSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos + 6));
|
||||||
|
pos += 8;
|
||||||
|
}
|
||||||
|
|
||||||
var serverPos = new CreateObject.ServerPosition(
|
var serverPos = new CreateObject.ServerPosition(
|
||||||
LandblockId: cellId,
|
LandblockId: cellId,
|
||||||
PositionX: px, PositionY: py, PositionZ: pz,
|
PositionX: px, PositionY: py, PositionZ: pz,
|
||||||
RotationW: rw, RotationX: rx, RotationY: ry, RotationZ: rz);
|
RotationW: rw, RotationX: rx, RotationY: ry, RotationZ: rz);
|
||||||
|
|
||||||
return new Parsed(guid, serverPos, velocity, placementId);
|
return new Parsed(guid, serverPos, velocity, placementId,
|
||||||
|
instSeq, teleSeq, forceSeq);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,12 @@ public sealed class WorldSession : IDisposable
|
||||||
public event Action<State>? StateChanged;
|
public event Action<State>? StateChanged;
|
||||||
|
|
||||||
public State CurrentState { get; private set; } = State.Disconnected;
|
public State CurrentState { get; private set; } = State.Disconnected;
|
||||||
|
|
||||||
|
/// <summary>Movement sequence counters for outbound MoveToState/AutonomousPosition.</summary>
|
||||||
|
public ushort InstanceSequence => _instanceSequence;
|
||||||
|
public ushort ServerControlSequence => _serverControlSequence;
|
||||||
|
public ushort TeleportSequence => _teleportSequence;
|
||||||
|
public ushort ForcePositionSequence => _forcePositionSequence;
|
||||||
public CharacterList.Parsed? Characters { get; private set; }
|
public CharacterList.Parsed? Characters { get; private set; }
|
||||||
|
|
||||||
private readonly NetClient _net;
|
private readonly NetClient _net;
|
||||||
|
|
@ -129,6 +135,16 @@ public sealed class WorldSession : IDisposable
|
||||||
private uint _clientPacketSequence;
|
private uint _clientPacketSequence;
|
||||||
private uint _fragmentSequence = 1;
|
private uint _fragmentSequence = 1;
|
||||||
|
|
||||||
|
// Movement sequence counters — echoed back in every MoveToState and
|
||||||
|
// AutonomousPosition so the server can detect stale/reordered packets.
|
||||||
|
// Initialized from CreateObject PhysicsData timestamps, updated by
|
||||||
|
// UpdatePosition/UpdateMotion/PlayerTeleport. Per holtburger:
|
||||||
|
// instance=slot 8, teleport=slot 4, serverControl=slot 5, forcePosition=slot 6.
|
||||||
|
private ushort _instanceSequence;
|
||||||
|
private ushort _serverControlSequence;
|
||||||
|
private ushort _teleportSequence;
|
||||||
|
private ushort _forcePositionSequence;
|
||||||
|
|
||||||
// Phase A.3: background receive thread buffers raw UDP datagrams into
|
// Phase A.3: background receive thread buffers raw UDP datagrams into
|
||||||
// a channel so the render thread never blocks on socket I/O.
|
// a channel so the render thread never blocks on socket I/O.
|
||||||
private readonly Channel<byte[]> _inboundQueue =
|
private readonly Channel<byte[]> _inboundQueue =
|
||||||
|
|
@ -400,6 +416,15 @@ public sealed class WorldSession : IDisposable
|
||||||
var parsed = CreateObject.TryParse(body);
|
var parsed = CreateObject.TryParse(body);
|
||||||
if (parsed is not null)
|
if (parsed is not null)
|
||||||
{
|
{
|
||||||
|
// Initialize sequence counters from the player's own CreateObject.
|
||||||
|
if (parsed.Value.Guid == Characters?.Characters.FirstOrDefault().Id)
|
||||||
|
{
|
||||||
|
_instanceSequence = parsed.Value.InstanceSequence;
|
||||||
|
_teleportSequence = parsed.Value.TeleportSequence;
|
||||||
|
_serverControlSequence = parsed.Value.ServerControlSequence;
|
||||||
|
_forcePositionSequence = parsed.Value.ForcePositionSequence;
|
||||||
|
}
|
||||||
|
|
||||||
EntitySpawned?.Invoke(new EntitySpawn(
|
EntitySpawned?.Invoke(new EntitySpawn(
|
||||||
parsed.Value.Guid,
|
parsed.Value.Guid,
|
||||||
parsed.Value.Position,
|
parsed.Value.Position,
|
||||||
|
|
@ -440,6 +465,14 @@ public sealed class WorldSession : IDisposable
|
||||||
var posUpdate = UpdatePosition.TryParse(body);
|
var posUpdate = UpdatePosition.TryParse(body);
|
||||||
if (posUpdate is not null)
|
if (posUpdate is not null)
|
||||||
{
|
{
|
||||||
|
// Update sequence counters from the player's own position updates.
|
||||||
|
if (posUpdate.Value.Guid == Characters?.Characters.FirstOrDefault().Id)
|
||||||
|
{
|
||||||
|
_instanceSequence = posUpdate.Value.InstanceSequence;
|
||||||
|
_teleportSequence = posUpdate.Value.TeleportSequence;
|
||||||
|
_forcePositionSequence = posUpdate.Value.ForcePositionSequence;
|
||||||
|
}
|
||||||
|
|
||||||
PositionUpdated?.Invoke(new EntityPositionUpdate(
|
PositionUpdated?.Invoke(new EntityPositionUpdate(
|
||||||
posUpdate.Value.Guid,
|
posUpdate.Value.Guid,
|
||||||
posUpdate.Value.Position,
|
posUpdate.Value.Position,
|
||||||
|
|
@ -457,10 +490,11 @@ public sealed class WorldSession : IDisposable
|
||||||
// movement; the LoginComplete is sent from GameWindow once
|
// movement; the LoginComplete is sent from GameWindow once
|
||||||
// the destination UpdatePosition is received and the player
|
// the destination UpdatePosition is received and the player
|
||||||
// has been snapped to the new cell.
|
// has been snapped to the new cell.
|
||||||
uint sequence = 0;
|
ushort sequence = 0;
|
||||||
if (body.Length >= 6)
|
if (body.Length >= 6)
|
||||||
sequence = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(
|
sequence = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(
|
||||||
body.AsSpan(4, 2));
|
body.AsSpan(4, 2));
|
||||||
|
_teleportSequence = sequence; // track for outbound movement messages
|
||||||
TeleportStarted?.Invoke(sequence);
|
TeleportStarted?.Invoke(sequence);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue