refactor(app): key live projections by runtime identity

Move materialized live-object sidecars and presentation worksets to exact RuntimeEntityKey ownership. Runtime remains the only GUID/incarnation/local-ID authority while hydration, animation, effects, lights, equipped children, renderer resources, visibility, liveness, and teardown resolve exact projection identities. Preserve synchronous callbacks, local-ID allocation order, and current rendering behavior.
This commit is contained in:
Erik 2026-07-25 21:50:58 +02:00
parent e18df84437
commit 420e5eea70
73 changed files with 2939 additions and 1715 deletions

View file

@ -41,25 +41,34 @@ public sealed class EquippedChildRenderController : IDisposable
/// <summary>Raised after an attached projection has its composed pose.</summary>
public event Action<uint>? ProjectionPoseReady;
/// <summary>Raised when an attached projection leaves its cell presentation.</summary>
public event Action<uint>? ProjectionRemoved;
private readonly Dictionary<uint, AttachedChild> _attachedByChild = new();
private readonly Dictionary<uint, PendingUnparentTransition> _pendingUnparentByChild = new();
private readonly Dictionary<uint, PendingOrdinaryRemoval> _pendingOrdinaryRemovalByRoot = new();
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingDetachedRemovalByChild = new();
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingReparentRemovalByChild = new();
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingPoseLossRemovalByChild = new();
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingOrphanRemovalByChild = new();
public event Action<LiveEntityRecord>? ProjectionRemoved;
private readonly Dictionary<RuntimeEntityKey, AttachedChild> _attachedByChild = [];
private readonly Dictionary<RuntimeEntityKey, PendingUnparentTransition>
_pendingUnparentByChild = [];
private readonly Dictionary<RuntimeEntityKey, PendingOrdinaryRemoval>
_pendingOrdinaryRemovalByRoot = [];
private readonly Dictionary<RuntimeEntityKey, PendingProjectionSubtree>
_pendingDetachedRemovalByChild = [];
private readonly Dictionary<RuntimeEntityKey, PendingProjectionSubtree>
_pendingReparentRemovalByChild = [];
private readonly Dictionary<RuntimeEntityKey, PendingProjectionSubtree>
_pendingPoseLossRemovalByChild = [];
private readonly Dictionary<RuntimeEntityKey, PendingProjectionSubtree>
_pendingOrphanRemovalByChild = [];
private readonly List<uint> _pendingProjectionChildrenScratch = new();
private readonly List<KeyValuePair<uint, PendingOrdinaryRemoval>>
private readonly List<KeyValuePair<RuntimeEntityKey, PendingOrdinaryRemoval>>
_pendingOrdinaryRemovalScratch = new();
private readonly List<KeyValuePair<uint, PendingProjectionSubtree>>
private readonly List<KeyValuePair<RuntimeEntityKey, PendingProjectionSubtree>>
_pendingProjectionSubtreeScratch = new();
private readonly List<KeyValuePair<uint, PendingUnparentTransition>>
private readonly List<KeyValuePair<RuntimeEntityKey, PendingUnparentTransition>>
_pendingUnparentScratch = new();
private readonly AttachmentUpdateOrder<AttachedChild> _updateOrder = new();
private readonly Func<AttachedChild, uint?> _parentOfAttached;
private readonly Func<uint, bool> _tickAttached;
private readonly Func<uint, bool> _reconcileAttached;
private readonly AttachmentUpdateOrder<RuntimeEntityKey, AttachedChild>
_updateOrder = new();
private readonly AttachmentUpdateOrder<uint, uint> _relationRecoveryOrder =
new();
private readonly Func<AttachedChild, RuntimeEntityKey?> _parentOfAttached;
private readonly Func<RuntimeEntityKey, bool> _tickAttached;
private readonly Func<RuntimeEntityKey, bool> _reconcileAttached;
private int _activePoseCompositionVisits;
internal int LastFullPoseCompositionVisits { get; private set; }
@ -92,7 +101,7 @@ public sealed class EquippedChildRenderController : IDisposable
_acceptParent = acceptParent ?? throw new ArgumentNullException(nameof(acceptParent));
_withdrawProjection = withdrawProjection
?? throw new ArgumentNullException(nameof(withdrawProjection));
_parentOfAttached = static child => child.ParentGuid;
_parentOfAttached = static child => child.ParentRecord.ProjectionKey;
_tickAttached = TickChild;
_reconcileAttached = ReconcileChild;
@ -258,11 +267,11 @@ public sealed class EquippedChildRenderController : IDisposable
{
if (!_liveEntities.TryGetRecord(childGuid, out LiveEntityRecord record))
{
_pendingUnparentByChild.Remove(childGuid);
Relations.EndChildProjection(childGuid);
return ChildUnparentDisposition.NotAttached;
}
RuntimeEntityKey key = RequireProjectionKey(record);
var pending = new PendingUnparentTransition(
record,
record.PositionAuthorityVersion,
@ -271,7 +280,7 @@ public sealed class EquippedChildRenderController : IDisposable
restoreRootRelation: false,
restoreDescendantRelations: true),
continuation);
return AdvanceUnparentTransition(childGuid, pending);
return AdvanceUnparentTransition(key, pending);
}
/// <summary>Recompose every child after the parent's animation tick.</summary>
@ -279,7 +288,7 @@ public sealed class EquippedChildRenderController : IDisposable
{
RetryPendingProjectionTransitions();
_activePoseCompositionVisits = 0;
IReadOnlyList<uint> failed = _updateOrder.ForEachParentFirst(
IReadOnlyList<RuntimeEntityKey> failed = _updateOrder.ForEachParentFirst(
_attachedByChild,
_parentOfAttached,
_tickAttached);
@ -297,7 +306,7 @@ public sealed class EquippedChildRenderController : IDisposable
{
RetryPendingProjectionTransitions();
_activePoseCompositionVisits = 0;
IReadOnlyList<uint> failed = _updateOrder.ForEachParentFirst(
IReadOnlyList<RuntimeEntityKey> failed = _updateOrder.ForEachParentFirst(
_attachedByChild,
_parentOfAttached,
_reconcileAttached);
@ -313,9 +322,9 @@ public sealed class EquippedChildRenderController : IDisposable
_pendingOrdinaryRemovalScratch);
for (int i = 0; i < _pendingOrdinaryRemovalScratch.Count; i++)
{
(uint rootGuid, PendingOrdinaryRemoval pending) =
(RuntimeEntityKey rootKey, PendingOrdinaryRemoval pending) =
_pendingOrdinaryRemovalScratch[i];
AdvanceOrdinaryRemoval(rootGuid, pending);
AdvanceOrdinaryRemoval(rootKey, pending);
}
CopyEntries(
@ -323,9 +332,9 @@ public sealed class EquippedChildRenderController : IDisposable
_pendingProjectionSubtreeScratch);
for (int i = 0; i < _pendingProjectionSubtreeScratch.Count; i++)
{
(uint childGuid, PendingProjectionSubtree pending) =
(RuntimeEntityKey childKey, PendingProjectionSubtree pending) =
_pendingProjectionSubtreeScratch[i];
AdvanceDetachedRemoval(childGuid, pending);
AdvanceDetachedRemoval(childKey, pending);
}
RetryProjectionSubtrees(_pendingReparentRemovalByChild);
@ -335,16 +344,16 @@ public sealed class EquippedChildRenderController : IDisposable
CopyEntries(_pendingUnparentByChild, _pendingUnparentScratch);
for (int i = 0; i < _pendingUnparentScratch.Count; i++)
{
(uint childGuid, PendingUnparentTransition pending) =
(RuntimeEntityKey childKey, PendingUnparentTransition pending) =
_pendingUnparentScratch[i];
if (!_liveEntities.IsCurrentRecord(pending.Record)
|| pending.Record.PositionAuthorityVersion
!= pending.PositionAuthorityVersion)
{
_pendingUnparentByChild.Remove(childGuid);
_pendingUnparentByChild.Remove(childKey);
continue;
}
AdvanceUnparentTransition(childGuid, pending);
AdvanceUnparentTransition(childKey, pending);
}
Relations.CopyPendingProjectionChildrenTo(_pendingProjectionChildrenScratch);
@ -353,17 +362,17 @@ public sealed class EquippedChildRenderController : IDisposable
}
private static void CopyEntries<T>(
Dictionary<uint, T> source,
List<KeyValuePair<uint, T>> destination)
Dictionary<RuntimeEntityKey, T> source,
List<KeyValuePair<RuntimeEntityKey, T>> destination)
{
destination.Clear();
foreach (KeyValuePair<uint, T> entry in source)
foreach (KeyValuePair<RuntimeEntityKey, T> entry in source)
destination.Add(entry);
}
private bool TickChild(uint childGuid)
private bool TickChild(RuntimeEntityKey childKey)
{
if (!_attachedByChild.TryGetValue(childGuid, out AttachedChild? child))
if (!_attachedByChild.TryGetValue(childKey, out AttachedChild? child))
return false;
_activePoseCompositionVisits++;
@ -403,15 +412,15 @@ public sealed class EquippedChildRenderController : IDisposable
return false;
}
private bool ReconcileChild(uint childGuid)
private bool ReconcileChild(RuntimeEntityKey childKey)
{
if (!_attachedByChild.TryGetValue(childGuid, out AttachedChild? child)
if (!_attachedByChild.TryGetValue(childKey, out AttachedChild? child)
|| !TryResolveExactAttachment(child, out WorldEntity parent))
{
return false;
}
return ParentPresentationMatches(child, parent) || TickChild(childGuid);
return ParentPresentationMatches(child, parent) || TickChild(childKey);
}
private bool ParentPresentationMatches(
@ -445,7 +454,9 @@ public sealed class EquippedChildRenderController : IDisposable
if (!_liveEntities.TryGetRecord(pending.ParentGuid, out LiveEntityRecord parentRecord)
|| parentRecord.WorldEntity is not { } parentEntity
|| !parentRecord.IsSpatiallyProjected
|| !_liveEntities.TryGetRecord(childGuid, out LiveEntityRecord childRecord)
|| !_liveEntities.TryGetCanonical(
childGuid,
out RuntimeEntityRecord childCanonical)
|| !_liveEntities.TryGetSnapshot(pending.ParentGuid, out WorldSession.EntitySpawn parentSpawn)
|| !_liveEntities.TryGetSnapshot(childGuid, out WorldSession.EntitySpawn childSpawn))
return false;
@ -496,22 +507,25 @@ public sealed class EquippedChildRenderController : IDisposable
// hydration path sees it. Install its logical effect profile before
// MaterializeLiveEntity registers runtime resources, exactly as for a
// top-level projection; rebucketing/reattachment must never recreate it.
if (!_liveEntities.TryGetEffectProfile(childGuid, out _))
var effectProfile = childSpawn.Physics is { } physics
? Vfx.EntityEffectProfile.CreateLive(childSetup, physics)
: Vfx.EntityEffectProfile.CreateDatStatic(childSetup);
if (_liveEntities.TryGetProjection(
childCanonical,
out LiveEntityRecord? retainedChild)
&& !_liveEntities.TryGetEffectProfile(childGuid, out _))
{
var effectProfile = childSpawn.Physics is { } physics
? Vfx.EntityEffectProfile.CreateLive(childSetup, physics)
: Vfx.EntityEffectProfile.CreateDatStatic(childSetup);
_liveEntities.SetEffectProfile(childGuid, effectProfile);
}
if (!_liveEntities.IsCurrentRecord(parentRecord)
|| !_liveEntities.IsCurrentRecord(childRecord)
|| !_liveEntities.IsCurrentCanonical(childCanonical)
|| !Relations.IsPending(pending, candidateKind))
{
return false;
}
WorldEntity? entity = _liveEntities.MaterializeLiveEntity(
childGuid,
childCanonical,
parentCellId,
localId =>
{
@ -529,8 +543,10 @@ public sealed class EquippedChildRenderController : IDisposable
created.SetIndexedPartPoses(pose.PartLocal, childPartAvailability);
return created;
},
LiveEntityProjectionKind.Attached);
if (entity is null)
LiveEntityProjectionKind.Attached,
initializeProjection: record => record.EffectProfile = effectProfile,
out LiveEntityRecord? childRecord);
if (entity is null || childRecord is null)
return false;
if (!_liveEntities.IsCurrentRecord(parentRecord)
|| !_liveEntities.IsCurrentRecord(childRecord)
@ -576,8 +592,9 @@ public sealed class EquippedChildRenderController : IDisposable
scale,
entity);
CaptureParentPresentation(attached, parentEntity);
_attachedByChild[childGuid] = attached;
_pendingUnparentByChild.Remove(childGuid);
RuntimeEntityKey childKey = RequireProjectionKey(childRecord);
_attachedByChild[childKey] = attached;
_pendingUnparentByChild.Remove(childKey);
if ((parentRecord.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0)
{
// A child attached while its parent is already Hidden inherits
@ -809,14 +826,15 @@ public sealed class EquippedChildRenderController : IDisposable
ParentAttachmentRelation relation,
ParentProjectionCandidateKind candidateKind)
{
if (!_liveEntities.TryGetRecord(
if (!_liveEntities.TryGetCanonical(
relation.ChildGuid,
out LiveEntityRecord childRecord))
out RuntimeEntityRecord childCanonical))
{
return default;
}
ulong positionAuthorityVersion = childRecord.PositionAuthorityVersion;
ulong positionAuthorityVersion =
childCanonical.PositionAuthorityVersion;
if (candidateKind is ParentProjectionCandidateKind.Staged)
{
if (!_liveEntities.CommitStagedParent(relation, out _)
@ -833,9 +851,15 @@ public sealed class EquippedChildRenderController : IDisposable
if (!Relations.IsPending(relation, candidateKind)
|| !_liveEntities.CommitAcceptedParentCellless(
childRecord,
positionAuthorityVersion)
|| !WithdrawPriorProjection(childRecord))
childCanonical,
positionAuthorityVersion))
{
return default;
}
if (_liveEntities.TryGetProjection(
childCanonical,
out LiveEntityRecord? childRecord)
&& !WithdrawPriorProjection(childRecord))
{
return default;
}
@ -850,7 +874,7 @@ public sealed class EquippedChildRenderController : IDisposable
if (relation.ParentGuid == relation.ChildGuid)
return ParentProjectionValidationDisposition.Rejected;
if (!_liveEntities.TryGetRecord(relation.ParentGuid, out LiveEntityRecord parent)
|| !_liveEntities.TryGetRecord(relation.ChildGuid, out _)
|| !_liveEntities.TryGetCanonical(relation.ChildGuid, out _)
|| parent.WorldEntity is null
|| !parent.HasPartArray)
{
@ -877,7 +901,7 @@ public sealed class EquippedChildRenderController : IDisposable
private void RetryWaitingDescendants(uint parentGuid)
{
_updateOrder.RealizeDescendants(
_relationRecoveryOrder.RealizeDescendants(
parentGuid,
Relations.ChildrenWaitingForParent,
ResolveAndTryRealize);
@ -985,12 +1009,12 @@ public sealed class EquippedChildRenderController : IDisposable
}
private void AdvanceDetachedRemoval(
uint childGuid,
RuntimeEntityKey childKey,
PendingProjectionSubtree pending)
{
AdvanceProjectionSubtree(
_pendingDetachedRemovalByChild,
childGuid,
childKey,
pending);
}
@ -998,22 +1022,29 @@ public sealed class EquippedChildRenderController : IDisposable
{
if (item.CurrentlyEquippedLocation == EquipMask.None)
return;
if (_attachedByChild.TryGetValue(item.ObjectId, out AttachedChild? attached)
if (!_liveEntities.TryGetRecord(
item.ObjectId,
out LiveEntityRecord record)
|| record.ProjectionKey is not { } key)
{
return;
}
if (_attachedByChild.TryGetValue(key, out AttachedChild? attached)
&& _liveEntities.IsCurrentRecord(attached.ChildRecord)
&& attached.ChildRecord.IsSpatiallyProjected)
{
// A component-stage failure left the exact equipped projection
// untouched. The authoritative rollback therefore has nothing to
// reconstruct and merely cancels its retained leave-world retry.
_pendingDetachedRemovalByChild.Remove(item.ObjectId);
_pendingDetachedRemovalByChild.Remove(key);
return;
}
if (_pendingDetachedRemovalByChild.TryGetValue(
item.ObjectId,
key,
out PendingProjectionSubtree pending))
{
AdvanceDetachedRemoval(item.ObjectId, pending);
if (_pendingDetachedRemovalByChild.ContainsKey(item.ObjectId))
AdvanceDetachedRemoval(key, pending);
if (_pendingDetachedRemovalByChild.ContainsKey(key))
return;
}
@ -1032,7 +1063,11 @@ public sealed class EquippedChildRenderController : IDisposable
private bool Remove(uint childGuid)
{
if (!_attachedByChild.TryGetValue(childGuid, out AttachedChild? child))
if (!_liveEntities.TryGetRecord(
childGuid,
out LiveEntityRecord record)
|| record.ProjectionKey is not { } key
|| !_attachedByChild.TryGetValue(key, out AttachedChild? child))
return false;
if (!_liveEntities.IsCurrentRecord(child.ChildRecord))
return CommitProjectionRemoval(child);
@ -1062,13 +1097,14 @@ public sealed class EquippedChildRenderController : IDisposable
private bool WithdrawPriorProjection(LiveEntityRecord childRecord)
{
RuntimeEntityKey key = RequireProjectionKey(childRecord);
if (_pendingReparentRemovalByChild.TryGetValue(
childRecord.ServerGuid,
key,
out PendingProjectionSubtree pending))
{
return AdvanceProjectionSubtree(
_pendingReparentRemovalByChild,
childRecord.ServerGuid,
key,
pending);
}
return BeginProjectionSubtreeWithdrawal(
@ -1079,7 +1115,7 @@ public sealed class EquippedChildRenderController : IDisposable
}
private ChildUnparentDisposition AdvanceUnparentTransition(
uint childGuid,
RuntimeEntityKey childKey,
PendingUnparentTransition pending)
{
ExactProjectionWithdrawalOutcome outcome = AdvanceProjectionSubtree(
@ -1088,7 +1124,7 @@ public sealed class EquippedChildRenderController : IDisposable
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending)
{
_pendingUnparentByChild[childGuid] = pending with { Subtree = next };
_pendingUnparentByChild[childKey] = pending with { Subtree = next };
if (outcome.Failure is not null)
throw outcome.Failure;
return ChildUnparentDisposition.Pending;
@ -1096,22 +1132,22 @@ public sealed class EquippedChildRenderController : IDisposable
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Superseded)
{
_pendingUnparentByChild.Remove(childGuid);
_pendingUnparentByChild.Remove(childKey);
if (outcome.Failure is not null)
throw outcome.Failure;
return ChildUnparentDisposition.Superseded;
}
PendingUnparentTransition awaitingContinuation = pending with { Subtree = next };
_pendingUnparentByChild[childGuid] = awaitingContinuation;
_pendingUnparentByChild[childKey] = awaitingContinuation;
if (outcome.Failure is not null)
throw outcome.Failure;
Relations.EndChildProjection(childGuid);
Relations.EndChildProjection(pending.Record.ServerGuid);
try
{
awaitingContinuation.Continuation?.Invoke();
_pendingUnparentByChild.Remove(childGuid);
_pendingUnparentByChild.Remove(childKey);
return ChildUnparentDisposition.Completed;
}
catch
@ -1124,9 +1160,9 @@ public sealed class EquippedChildRenderController : IDisposable
&& awaitingContinuation.Record.ProjectionKind
is LiveEntityProjectionKind.World;
if (continuationCommitted)
_pendingUnparentByChild.Remove(childGuid);
_pendingUnparentByChild.Remove(childKey);
else
_pendingUnparentByChild[childGuid] = awaitingContinuation;
_pendingUnparentByChild[childKey] = awaitingContinuation;
throw;
}
}
@ -1165,20 +1201,23 @@ public sealed class EquippedChildRenderController : IDisposable
private bool CommitProjectionRemoval(AttachedChild child)
{
if (!_attachedByChild.TryGetValue(child.ChildGuid, out AttachedChild? current)
RuntimeEntityKey key = RequireProjectionKey(child.ChildRecord);
if (!_attachedByChild.TryGetValue(key, out AttachedChild? current)
|| !ReferenceEquals(current, child))
{
return false;
}
_attachedByChild.Remove(child.ChildGuid);
ProjectionRemoved?.Invoke(child.Entity.Id);
_attachedByChild.Remove(key);
ProjectionRemoved?.Invoke(child.ChildRecord);
return true;
}
private void WithdrawForPoseLoss(uint childGuid)
private void WithdrawForPoseLoss(RuntimeEntityKey childKey)
{
if (!_liveEntities.TryGetRecord(childGuid, out LiveEntityRecord record))
if (!_liveEntities.TryGetRecord(
childKey,
out LiveEntityRecord record))
return;
BeginProjectionSubtreeWithdrawal(
_pendingPoseLossRemovalByChild,
@ -1191,25 +1230,28 @@ public sealed class EquippedChildRenderController : IDisposable
{
if (_liveEntities.TryGetRecord(guid, out LiveEntityRecord record))
{
RuntimeEntityKey key = RequireProjectionKey(record);
List<AttachedRemovalCapture> captures = CaptureRecordSubtreeParentFirst(record);
AdvanceOrdinaryRemoval(
guid,
key,
new PendingOrdinaryRemoval(record, captures, NextIndex: 0));
return;
}
if (_attachedByChild.TryGetValue(guid, out AttachedChild? stale))
AttachedChild? stale = _attachedByChild.Values.FirstOrDefault(
candidate => candidate.ChildGuid == guid);
if (stale is not null)
CommitProjectionRemoval(stale);
Relations.EndChildProjection(guid);
}
private void AdvanceOrdinaryRemoval(
uint rootGuid,
RuntimeEntityKey rootKey,
PendingOrdinaryRemoval pending)
{
if (!_liveEntities.IsCurrentRecord(pending.RootRecord))
{
_pendingOrdinaryRemovalByRoot.Remove(rootGuid);
_pendingOrdinaryRemovalByRoot.Remove(rootKey);
return;
}
@ -1219,7 +1261,7 @@ public sealed class EquippedChildRenderController : IDisposable
ExactProjectionWithdrawalOutcome outcome = WithdrawCaptured(captured);
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending)
{
_pendingOrdinaryRemovalByRoot[rootGuid] = pending with { NextIndex = i };
_pendingOrdinaryRemovalByRoot[rootKey] = pending with { NextIndex = i };
if (outcome.Failure is not null)
throw outcome.Failure;
return;
@ -1233,7 +1275,7 @@ public sealed class EquippedChildRenderController : IDisposable
if (outcome.Failure is not null)
{
_pendingOrdinaryRemovalByRoot[rootGuid] = pending with
_pendingOrdinaryRemovalByRoot[rootKey] = pending with
{
NextIndex = i + 1,
};
@ -1241,23 +1283,24 @@ public sealed class EquippedChildRenderController : IDisposable
}
}
_pendingOrdinaryRemovalByRoot.Remove(rootGuid);
Relations.EndChildProjection(rootGuid);
_pendingOrdinaryRemovalByRoot.Remove(rootKey);
Relations.EndChildProjection(pending.RootRecord.ServerGuid);
}
private void TearDownRecordProjections(LiveEntityRecord record)
{
if (_pendingUnparentByChild.TryGetValue(record.ServerGuid, out var pending)
RuntimeEntityKey key = RequireProjectionKey(record);
if (_pendingUnparentByChild.TryGetValue(key, out var pending)
&& ReferenceEquals(pending.Record, record))
{
_pendingUnparentByChild.Remove(record.ServerGuid);
_pendingUnparentByChild.Remove(key);
}
if (_pendingOrdinaryRemovalByRoot.TryGetValue(
record.ServerGuid,
key,
out PendingOrdinaryRemoval ordinary)
&& ReferenceEquals(ordinary.RootRecord, record))
{
_pendingOrdinaryRemovalByRoot.Remove(record.ServerGuid);
_pendingOrdinaryRemovalByRoot.Remove(key);
}
RemovePendingProjectionSubtree(_pendingDetachedRemovalByChild, record);
RemovePendingProjectionSubtree(_pendingReparentRemovalByChild, record);
@ -1293,7 +1336,8 @@ public sealed class EquippedChildRenderController : IDisposable
{
var result = new List<AttachedRemovalCapture>();
var visited = new HashSet<LiveEntityRecord>(ReferenceEqualityComparer.Instance);
if (_attachedByChild.TryGetValue(record.ServerGuid, out AttachedChild? root)
if (record.ProjectionKey is { } key
&& _attachedByChild.TryGetValue(key, out AttachedChild? root)
&& ReferenceEquals(root.ChildRecord, record))
{
result.Add(Capture(root));
@ -1327,8 +1371,10 @@ public sealed class EquippedChildRenderController : IDisposable
private ExactProjectionWithdrawalOutcome WithdrawCaptured(
AttachedRemovalCapture captured)
{
RuntimeEntityKey key = RequireProjectionKey(
captured.Attached.ChildRecord);
if (!_attachedByChild.TryGetValue(
captured.Attached.ChildGuid,
key,
out AttachedChild? current)
|| !ReferenceEquals(current, captured.Attached))
{
@ -1369,7 +1415,9 @@ public sealed class EquippedChildRenderController : IDisposable
{
if (!visited.Add(record))
return;
_attachedByChild.TryGetValue(record.ServerGuid, out AttachedChild? attached);
AttachedChild? attached = null;
if (record.ProjectionKey is { } key)
_attachedByChild.TryGetValue(key, out attached);
if (attached is not null && !ReferenceEquals(attached.ChildRecord, record))
attached = null;
if (record.IsSpatiallyProjected || attached is not null)
@ -1396,7 +1444,7 @@ public sealed class EquippedChildRenderController : IDisposable
}
private bool BeginProjectionSubtreeWithdrawal(
Dictionary<uint, PendingProjectionSubtree> pendingByRoot,
Dictionary<RuntimeEntityKey, PendingProjectionSubtree> pendingByRoot,
LiveEntityRecord root,
bool restoreRootRelation,
bool restoreDescendantRelations)
@ -1405,12 +1453,15 @@ public sealed class EquippedChildRenderController : IDisposable
root,
restoreRootRelation,
restoreDescendantRelations);
return AdvanceProjectionSubtree(pendingByRoot, root.ServerGuid, pending);
return AdvanceProjectionSubtree(
pendingByRoot,
RequireProjectionKey(root),
pending);
}
private bool AdvanceProjectionSubtree(
Dictionary<uint, PendingProjectionSubtree> pendingByRoot,
uint rootGuid,
Dictionary<RuntimeEntityKey, PendingProjectionSubtree> pendingByRoot,
RuntimeEntityKey rootKey,
PendingProjectionSubtree pending)
{
ExactProjectionWithdrawalOutcome outcome = AdvanceProjectionSubtree(
@ -1419,9 +1470,9 @@ public sealed class EquippedChildRenderController : IDisposable
bool retry = outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending
|| (outcome.Failure is not null && next.NextIndex < next.Captures.Count);
if (retry)
pendingByRoot[rootGuid] = next;
pendingByRoot[rootKey] = next;
else
pendingByRoot.Remove(rootGuid);
pendingByRoot.Remove(rootKey);
if (outcome.Failure is not null)
throw outcome.Failure;
return outcome.Disposition is ExactProjectionWithdrawalDisposition.Completed
@ -1508,27 +1559,29 @@ public sealed class EquippedChildRenderController : IDisposable
}
private void RetryProjectionSubtrees(
Dictionary<uint, PendingProjectionSubtree> pendingByRoot)
Dictionary<RuntimeEntityKey, PendingProjectionSubtree> pendingByRoot)
{
CopyEntries(pendingByRoot, _pendingProjectionSubtreeScratch);
for (int i = 0; i < _pendingProjectionSubtreeScratch.Count; i++)
{
(uint rootGuid, PendingProjectionSubtree pending) =
(RuntimeEntityKey rootKey, PendingProjectionSubtree pending) =
_pendingProjectionSubtreeScratch[i];
AdvanceProjectionSubtree(pendingByRoot, rootGuid, pending);
AdvanceProjectionSubtree(pendingByRoot, rootKey, pending);
}
}
private static void RemovePendingProjectionSubtree(
Dictionary<uint, PendingProjectionSubtree> pendingByRoot,
Dictionary<RuntimeEntityKey, PendingProjectionSubtree> pendingByRoot,
LiveEntityRecord record)
{
uint[] keys = pendingByRoot
.Where(pair => ReferenceEquals(pair.Value.RootRecord, record))
.Select(pair => pair.Key)
.ToArray();
for (int i = 0; i < keys.Length; i++)
pendingByRoot.Remove(keys[i]);
if (record.ProjectionKey is { } key
&& pendingByRoot.TryGetValue(
key,
out PendingProjectionSubtree pending)
&& ReferenceEquals(pending.RootRecord, record))
{
pendingByRoot.Remove(key);
}
}
public void Clear()
@ -1606,6 +1659,13 @@ public sealed class EquippedChildRenderController : IDisposable
LiveEntityRecord RootRecord,
IReadOnlyList<AttachedRemovalCapture> Captures,
int NextIndex);
private static RuntimeEntityKey RequireProjectionKey(
LiveEntityRecord record) =>
record.ProjectionKey
?? throw new InvalidOperationException(
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
"has no exact projection key.");
}
public enum ChildUnparentDisposition