feat(net): port retail physics spawn and event timestamps
Parse the complete PhysicsDesc plus F754/F755 packets, correct every PhysicsState bit, and gate all nine retail update channels with generation-safe immutable snapshots. Preserve ForcePosition, teleport, placement, velocity, parent, pickup, delete, and same-generation CreateObject ordering from the named client. Separate accepted logical lifecycle notifications from retained UI qualities, make GUID replacement and session reset clear every projection exactly once, and add packet, wraparound, malformed-input, parent FIFO, canonical-position, reconnect, and GUID-reuse conformance coverage. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
d53fe30ffe
commit
8a5d77f7f4
50 changed files with 3809 additions and 649 deletions
|
|
@ -27,12 +27,11 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
private readonly GpuWorldState _worldState;
|
||||
private readonly Func<uint, WorldEntity?> _resolveEntity;
|
||||
private readonly Func<uint, WorldSession.EntitySpawn?> _resolveSpawn;
|
||||
private readonly Func<ParentEvent.Parsed, bool> _acceptParent;
|
||||
private readonly Func<uint> _nextEntityId;
|
||||
|
||||
private readonly Dictionary<uint, PendingAttachment> _pendingByChild = new();
|
||||
private readonly Dictionary<uint, PendingAttachment> _lastRelationByChild = new();
|
||||
private readonly ParentAttachmentState _relations = new();
|
||||
private readonly Dictionary<uint, AttachedChild> _attachedByChild = new();
|
||||
private readonly Dictionary<uint, ushort> _positionSequenceByChild = new();
|
||||
|
||||
public IEnumerable<uint> AttachedEntityIds
|
||||
{
|
||||
|
|
@ -50,6 +49,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
GpuWorldState worldState,
|
||||
Func<uint, WorldEntity?> resolveEntity,
|
||||
Func<uint, WorldSession.EntitySpawn?> resolveSpawn,
|
||||
Func<ParentEvent.Parsed, bool> acceptParent,
|
||||
Func<uint> nextEntityId)
|
||||
{
|
||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
|
|
@ -58,11 +58,12 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
|
||||
_resolveEntity = resolveEntity ?? throw new ArgumentNullException(nameof(resolveEntity));
|
||||
_resolveSpawn = resolveSpawn ?? throw new ArgumentNullException(nameof(resolveSpawn));
|
||||
_acceptParent = acceptParent ?? throw new ArgumentNullException(nameof(acceptParent));
|
||||
_nextEntityId = nextEntityId ?? throw new ArgumentNullException(nameof(nextEntityId));
|
||||
|
||||
_objects.ObjectMoved += OnObjectMoved;
|
||||
_objects.MoveRolledBack += OnMoveRolledBack;
|
||||
_objects.ObjectRemoved += OnObjectRemoved;
|
||||
_objects.ObjectRemovalClassified += OnObjectRemovalClassified;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -72,100 +73,106 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
/// </summary>
|
||||
public void OnSpawn(WorldSession.EntitySpawn spawn)
|
||||
{
|
||||
_positionSequenceByChild[spawn.Guid] = spawn.PositionSequence;
|
||||
|
||||
if (spawn.ParentGuid is { } parentGuid and not 0
|
||||
&& spawn.ParentLocation is { } parentLocation
|
||||
&& spawn.PlacementId is { } placementId)
|
||||
{
|
||||
var relation = new PendingAttachment(
|
||||
parentGuid,
|
||||
spawn.Guid,
|
||||
parentLocation,
|
||||
placementId,
|
||||
spawn.InstanceSequence,
|
||||
spawn.PositionSequence,
|
||||
FromCreateObject: true);
|
||||
_pendingByChild[spawn.Guid] = relation;
|
||||
_lastRelationByChild[spawn.Guid] = relation;
|
||||
}
|
||||
|
||||
lock (_datLock)
|
||||
{
|
||||
if (spawn.ParentGuid is { } parentGuid and not 0
|
||||
&& spawn.ParentLocation is { } parentLocation
|
||||
&& spawn.PlacementId is { } placementId)
|
||||
{
|
||||
_relations.AcceptCreateObjectRelation(new ParentAttachmentRelation(
|
||||
parentGuid,
|
||||
spawn.Guid,
|
||||
parentLocation,
|
||||
placementId,
|
||||
ParentInstanceSequence: 0,
|
||||
spawn.PositionSequence));
|
||||
}
|
||||
|
||||
ResolveRelations(spawn.Guid);
|
||||
TryRealize(spawn.Guid);
|
||||
|
||||
// ParentEvent can precede the parent's CreateObject. Revisit every
|
||||
// child waiting specifically on the object that just arrived.
|
||||
uint[] waiting = _pendingByChild.Values
|
||||
.Where(p => p.ParentGuid == spawn.Guid)
|
||||
.Select(p => p.ChildGuid)
|
||||
.ToArray();
|
||||
for (int i = 0; i < waiting.Length; i++)
|
||||
IReadOnlyList<uint> waiting = _relations.ChildrenWaitingForParent(spawn.Guid);
|
||||
for (int i = 0; i < waiting.Count; i++)
|
||||
{
|
||||
ResolveRelations(waiting[i]);
|
||||
TryRealize(waiting[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply/queue a live ParentEvent using retail's two sequence gates:
|
||||
/// parent instance must match, child position must advance strictly.
|
||||
/// Completes attachment projection after a root world entity has been
|
||||
/// registered. Relation acceptance intentionally happens before root
|
||||
/// projection selection; this notification only satisfies render-pose
|
||||
/// dependencies for children waiting on that root.
|
||||
/// </summary>
|
||||
public void OnWorldEntityRegistered(uint guid)
|
||||
{
|
||||
lock (_datLock)
|
||||
{
|
||||
IReadOnlyList<uint> waiting = _relations.ChildrenWaitingForParent(guid);
|
||||
for (int i = 0; i < waiting.Count; i++)
|
||||
{
|
||||
ResolveRelations(waiting[i]);
|
||||
TryRealize(waiting[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply/queue a live ParentEvent after the live-entity owner has exact-
|
||||
/// gated the parent's INSTANCE_TS and advanced the child's shared
|
||||
/// POSITION_TS. This controller owns only the render relationship.
|
||||
/// </summary>
|
||||
public void OnParentEvent(ParentEvent.Parsed update)
|
||||
{
|
||||
WorldSession.EntitySpawn? parentSpawn = _resolveSpawn(update.ParentGuid);
|
||||
if (parentSpawn is { } knownParent
|
||||
&& knownParent.InstanceSequence != update.ParentInstanceSequence)
|
||||
{
|
||||
// Known parent newer than the event: stale. Event newer than the
|
||||
// known parent: retain until its CreateObject arrives.
|
||||
if (MotionSequenceGate.IsNewer(
|
||||
update.ParentInstanceSequence,
|
||||
knownParent.InstanceSequence))
|
||||
return;
|
||||
}
|
||||
|
||||
if (_positionSequenceByChild.TryGetValue(update.ChildGuid, out ushort current)
|
||||
&& !MotionSequenceGate.IsNewer(current, update.ChildPositionSequence))
|
||||
return;
|
||||
|
||||
var relation = new PendingAttachment(
|
||||
update.ParentGuid,
|
||||
update.ChildGuid,
|
||||
update.ParentLocation,
|
||||
update.PlacementId,
|
||||
update.ParentInstanceSequence,
|
||||
update.ChildPositionSequence,
|
||||
FromCreateObject: false);
|
||||
_pendingByChild[update.ChildGuid] = relation;
|
||||
_lastRelationByChild[update.ChildGuid] = relation;
|
||||
|
||||
lock (_datLock)
|
||||
{
|
||||
_relations.Enqueue(update);
|
||||
ResolveRelations(update.ChildGuid);
|
||||
TryRealize(update.ChildGuid);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnObjectDeleted(uint guid)
|
||||
public void OnGenerationReplaced(uint guid, ushort replacementGeneration)
|
||||
{
|
||||
Remove(guid);
|
||||
TearDownObjectProjections(guid);
|
||||
_relations.EndGeneration(guid, replacementGeneration);
|
||||
}
|
||||
|
||||
uint[] children = _attachedByChild.Values
|
||||
.Where(c => c.ParentGuid == guid)
|
||||
.Select(c => c.ChildGuid)
|
||||
.ToArray();
|
||||
for (int i = 0; i < children.Length; i++)
|
||||
Remove(children[i]);
|
||||
public void OnGenerationDeleted(uint guid, ushort deletedGeneration)
|
||||
{
|
||||
TearDownObjectProjections(guid);
|
||||
_relations.DeleteGeneration(guid, deletedGeneration);
|
||||
}
|
||||
|
||||
uint[] pendingChildren = _lastRelationByChild.Values
|
||||
.Where(c => c.ParentGuid == guid)
|
||||
.Select(c => c.ChildGuid)
|
||||
.ToArray();
|
||||
for (int i = 0; i < pendingChildren.Length; i++)
|
||||
{
|
||||
_pendingByChild.Remove(pendingChildren[i]);
|
||||
_lastRelationByChild.Remove(pendingChildren[i]);
|
||||
}
|
||||
private void OnObjectRemovalClassified(ClientObjectRemoval removal)
|
||||
{
|
||||
if (removal.Reason is not ClientObjectRemovalReason.Ordinary)
|
||||
return;
|
||||
|
||||
_pendingByChild.Remove(guid);
|
||||
_lastRelationByChild.Remove(guid);
|
||||
_positionSequenceByChild.Remove(guid);
|
||||
uint guid = removal.Object.ObjectId;
|
||||
TearDownObjectProjections(guid);
|
||||
// InventoryRemoveObject removes the item from the UI view, not the
|
||||
// live physics generation. Preserve fresher pending ParentEvents so a
|
||||
// same-generation world/parent update can still replay them. Logical
|
||||
// delete/replacement are driven directly by the accepted inbound
|
||||
// lifecycle and never depend on this table.
|
||||
_relations.EndChildProjection(guid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A fresh Position or Pickup unparented this child. Remove the accepted
|
||||
/// attachment/rollback projection while retaining fresher unresolved
|
||||
/// ParentEvents; unlike logical deletion, do not disturb child-addressed
|
||||
/// pending state or children that may themselves reference it.
|
||||
/// </summary>
|
||||
public void OnChildBecameUnparented(uint childGuid)
|
||||
{
|
||||
Remove(childGuid);
|
||||
_relations.EndChildProjection(childGuid);
|
||||
}
|
||||
|
||||
/// <summary>Recompose every child after the parent's animation tick.</summary>
|
||||
|
|
@ -197,37 +204,15 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
|
||||
private void TryRealize(uint childGuid)
|
||||
{
|
||||
if (!_pendingByChild.TryGetValue(childGuid, out PendingAttachment pending))
|
||||
if (!_relations.TryGetProjection(childGuid, out ParentAttachmentRelation pending))
|
||||
return;
|
||||
|
||||
// A ParentEvent queued before the child existed is replayed only if
|
||||
// its position stamp still advances the child's CreateObject seed.
|
||||
// Retail reaches the same check inside DoParentEvent after its blob
|
||||
// queue resolves both objects.
|
||||
if (!pending.FromCreateObject
|
||||
&& _positionSequenceByChild.TryGetValue(childGuid, out ushort childPosition)
|
||||
&& !MotionSequenceGate.IsNewer(childPosition, pending.ChildPositionSequence))
|
||||
{
|
||||
_pendingByChild.Remove(childGuid);
|
||||
return;
|
||||
}
|
||||
|
||||
WorldEntity? parentEntity = _resolveEntity(pending.ParentGuid);
|
||||
WorldSession.EntitySpawn? parentSpawn = _resolveSpawn(pending.ParentGuid);
|
||||
WorldSession.EntitySpawn? childSpawn = _resolveSpawn(childGuid);
|
||||
if (parentEntity is null || parentSpawn is null || childSpawn is null)
|
||||
return;
|
||||
|
||||
if (!pending.FromCreateObject
|
||||
&& parentSpawn.Value.InstanceSequence != pending.ParentInstanceSequence)
|
||||
{
|
||||
if (MotionSequenceGate.IsNewer(
|
||||
pending.ParentInstanceSequence,
|
||||
parentSpawn.Value.InstanceSequence))
|
||||
_pendingByChild.Remove(childGuid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentSpawn.Value.SetupTableId is not { } parentSetupId
|
||||
|| childSpawn.Value.SetupTableId is not { } childSetupId
|
||||
|| parentEntity.ParentCellId is not { } parentCellId)
|
||||
|
|
@ -283,8 +268,16 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
Console.WriteLine(
|
||||
$"equipment: attached child=0x{childGuid:X8} parent=0x{pending.ParentGuid:X8} " +
|
||||
$"location={parentLocation} placement={placement}");
|
||||
_positionSequenceByChild[childGuid] = pending.ChildPositionSequence;
|
||||
_pendingByChild.Remove(childGuid);
|
||||
_relations.MarkProjected(childGuid);
|
||||
}
|
||||
|
||||
private void ResolveRelations(uint childGuid)
|
||||
{
|
||||
_relations.Resolve(
|
||||
childGuid,
|
||||
guid => _resolveSpawn(guid) is not null,
|
||||
guid => _resolveSpawn(guid)?.InstanceSequence,
|
||||
_acceptParent);
|
||||
}
|
||||
|
||||
private IReadOnlyList<MeshRef> BuildPartTemplate(
|
||||
|
|
@ -377,16 +370,13 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
// A rejected unwield restores the equipped location without a fresh
|
||||
// wire ParentEvent; reinstall the last accepted relationship only for
|
||||
// this explicit rollback signal.
|
||||
if (_lastRelationByChild.TryGetValue(item.ObjectId, out PendingAttachment relation))
|
||||
if (_relations.RestoreLastAccepted(item.ObjectId))
|
||||
{
|
||||
_pendingByChild[item.ObjectId] = relation;
|
||||
lock (_datLock)
|
||||
TryRealize(item.ObjectId);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnObjectRemoved(ClientObject item) => OnObjectDeleted(item.ObjectId);
|
||||
|
||||
private void Remove(uint childGuid)
|
||||
{
|
||||
if (!_attachedByChild.Remove(childGuid))
|
||||
|
|
@ -394,21 +384,33 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
_worldState.RemoveEntityByServerGuid(childGuid);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
private void TearDownObjectProjections(uint guid)
|
||||
{
|
||||
_objects.ObjectMoved -= OnObjectMoved;
|
||||
_objects.MoveRolledBack -= OnMoveRolledBack;
|
||||
_objects.ObjectRemoved -= OnObjectRemoved;
|
||||
Remove(guid);
|
||||
|
||||
uint[] children = _attachedByChild.Values
|
||||
.Where(child => child.ParentGuid == guid)
|
||||
.Select(child => child.ChildGuid)
|
||||
.ToArray();
|
||||
for (int i = 0; i < children.Length; i++)
|
||||
Remove(children[i]);
|
||||
}
|
||||
|
||||
private readonly record struct PendingAttachment(
|
||||
uint ParentGuid,
|
||||
uint ChildGuid,
|
||||
uint ParentLocation,
|
||||
uint PlacementId,
|
||||
ushort ParentInstanceSequence,
|
||||
ushort ChildPositionSequence,
|
||||
bool FromCreateObject);
|
||||
public void Clear()
|
||||
{
|
||||
uint[] attached = _attachedByChild.Keys.ToArray();
|
||||
for (int i = 0; i < attached.Length; i++)
|
||||
Remove(attached[i]);
|
||||
_relations.Clear();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Clear();
|
||||
_objects.ObjectMoved -= OnObjectMoved;
|
||||
_objects.MoveRolledBack -= OnMoveRolledBack;
|
||||
_objects.ObjectRemovalClassified -= OnObjectRemovalClassified;
|
||||
}
|
||||
|
||||
private sealed record AttachedChild(
|
||||
uint ParentGuid,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue