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:
parent
e18df84437
commit
420e5eea70
73 changed files with 2939 additions and 1715 deletions
|
|
@ -7,24 +7,25 @@ namespace AcDream.App.Rendering;
|
|||
/// those projections only after traversal, so dictionary enumeration remains
|
||||
/// stable.
|
||||
/// </summary>
|
||||
internal sealed class AttachmentUpdateOrder<TChild>
|
||||
internal sealed class AttachmentUpdateOrder<TKey, TChild>
|
||||
where TKey : struct
|
||||
{
|
||||
private readonly HashSet<uint> _visiting = new();
|
||||
private readonly HashSet<uint> _completed = new();
|
||||
private readonly HashSet<uint> _failedSet = new();
|
||||
private readonly List<uint> _failed = new();
|
||||
private readonly List<uint> _subtree = new();
|
||||
private readonly Queue<uint> _recoveryQueue = new();
|
||||
private readonly HashSet<uint> _recoveryVisited = new();
|
||||
private readonly HashSet<TKey> _visiting = new();
|
||||
private readonly HashSet<TKey> _completed = new();
|
||||
private readonly HashSet<TKey> _failedSet = new();
|
||||
private readonly List<TKey> _failed = new();
|
||||
private readonly List<TKey> _subtree = new();
|
||||
private readonly Queue<TKey> _recoveryQueue = new();
|
||||
private readonly HashSet<TKey> _recoveryVisited = new();
|
||||
|
||||
/// <summary>
|
||||
/// Updates the current attachment map without per-frame delegate or
|
||||
/// enumerator boxing. The returned list is borrowed until the next call.
|
||||
/// </summary>
|
||||
public IReadOnlyList<uint> ForEachParentFirst(
|
||||
Dictionary<uint, TChild> children,
|
||||
Func<TChild, uint?> parentOf,
|
||||
Func<uint, bool> update)
|
||||
public IReadOnlyList<TKey> ForEachParentFirst(
|
||||
Dictionary<TKey, TChild> children,
|
||||
Func<TChild, TKey?> parentOf,
|
||||
Func<TKey, bool> update)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(children);
|
||||
ArgumentNullException.ThrowIfNull(parentOf);
|
||||
|
|
@ -34,7 +35,7 @@ internal sealed class AttachmentUpdateOrder<TChild>
|
|||
_completed.Clear();
|
||||
_failedSet.Clear();
|
||||
_failed.Clear();
|
||||
foreach (uint childId in children.Keys)
|
||||
foreach (TKey childId in children.Keys)
|
||||
Visit(childId, children, parentOf, update);
|
||||
return _failed;
|
||||
}
|
||||
|
|
@ -44,10 +45,10 @@ internal sealed class AttachmentUpdateOrder<TChild>
|
|||
/// withdrawn without leaving a child rooted at a stale intermediate pose.
|
||||
/// The returned list is borrowed until the next call.
|
||||
/// </summary>
|
||||
public IReadOnlyList<uint> CollectSubtreePostOrder(
|
||||
Dictionary<uint, TChild> children,
|
||||
uint rootId,
|
||||
Func<TChild, uint?> parentOf)
|
||||
public IReadOnlyList<TKey> CollectSubtreePostOrder(
|
||||
Dictionary<TKey, TChild> children,
|
||||
TKey rootId,
|
||||
Func<TChild, TKey?> parentOf)
|
||||
{
|
||||
_subtree.Clear();
|
||||
_visiting.Clear();
|
||||
|
|
@ -61,9 +62,9 @@ internal sealed class AttachmentUpdateOrder<TChild>
|
|||
/// so A→B→C recovers in one parent-first drain.
|
||||
/// </summary>
|
||||
public void RealizeDescendants(
|
||||
uint parentId,
|
||||
Func<uint, IReadOnlyList<uint>> childrenWaitingForParent,
|
||||
Func<uint, bool> realize)
|
||||
TKey parentId,
|
||||
Func<TKey, IReadOnlyList<TKey>> childrenWaitingForParent,
|
||||
Func<TKey, bool> realize)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(childrenWaitingForParent);
|
||||
ArgumentNullException.ThrowIfNull(realize);
|
||||
|
|
@ -74,11 +75,11 @@ internal sealed class AttachmentUpdateOrder<TChild>
|
|||
_recoveryVisited.Add(parentId);
|
||||
while (_recoveryQueue.Count > 0)
|
||||
{
|
||||
uint currentParent = _recoveryQueue.Dequeue();
|
||||
IReadOnlyList<uint> waiting = childrenWaitingForParent(currentParent);
|
||||
TKey currentParent = _recoveryQueue.Dequeue();
|
||||
IReadOnlyList<TKey> waiting = childrenWaitingForParent(currentParent);
|
||||
for (int i = 0; i < waiting.Count; i++)
|
||||
{
|
||||
uint childId = waiting[i];
|
||||
TKey childId = waiting[i];
|
||||
if (realize(childId) && _recoveryVisited.Add(childId))
|
||||
_recoveryQueue.Enqueue(childId);
|
||||
}
|
||||
|
|
@ -86,25 +87,26 @@ internal sealed class AttachmentUpdateOrder<TChild>
|
|||
}
|
||||
|
||||
private void Collect(
|
||||
uint rootId,
|
||||
Dictionary<uint, TChild> children,
|
||||
Func<TChild, uint?> parentOf)
|
||||
TKey rootId,
|
||||
Dictionary<TKey, TChild> children,
|
||||
Func<TChild, TKey?> parentOf)
|
||||
{
|
||||
if (!_visiting.Add(rootId))
|
||||
return;
|
||||
foreach ((uint candidateId, TChild child) in children)
|
||||
foreach ((TKey candidateId, TChild child) in children)
|
||||
{
|
||||
if (parentOf(child) == rootId)
|
||||
if (parentOf(child) is { } parent
|
||||
&& EqualityComparer<TKey>.Default.Equals(parent, rootId))
|
||||
Collect(candidateId, children, parentOf);
|
||||
}
|
||||
_subtree.Add(rootId);
|
||||
}
|
||||
|
||||
private void Visit(
|
||||
uint childId,
|
||||
Dictionary<uint, TChild> children,
|
||||
Func<TChild, uint?> parentOf,
|
||||
Func<uint, bool> update)
|
||||
TKey childId,
|
||||
Dictionary<TKey, TChild> children,
|
||||
Func<TChild, TKey?> parentOf,
|
||||
Func<TKey, bool> update)
|
||||
{
|
||||
if (_completed.Contains(childId)
|
||||
|| !children.TryGetValue(childId, out TChild? child))
|
||||
|
|
@ -114,7 +116,7 @@ internal sealed class AttachmentUpdateOrder<TChild>
|
|||
if (!_visiting.Add(childId))
|
||||
return;
|
||||
|
||||
uint? parentId = parentOf(child);
|
||||
TKey? parentId = parentOf(child);
|
||||
if (parentId is { } attachedParentId && children.ContainsKey(attachedParentId))
|
||||
Visit(attachedParentId, children, parentOf, update);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ using AcDream.Core.Net.Messages;
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Plugins;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Entities;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
|
@ -116,13 +117,13 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
}
|
||||
|
||||
public bool TryMaterialize(
|
||||
LiveEntityRecord expectedRecord,
|
||||
RuntimeEntityRecord expectedCanonical,
|
||||
WorldSession.EntitySpawn canonicalSpawn,
|
||||
LiveProjectionPurpose purpose,
|
||||
ulong expectedCreateIntegrationVersion,
|
||||
LiveEntityAppearanceUpdateState? appearanceUpdate = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(expectedRecord);
|
||||
ArgumentNullException.ThrowIfNull(expectedCanonical);
|
||||
if (purpose is LiveProjectionPurpose.AppearanceMutation
|
||||
!= (appearanceUpdate is not null))
|
||||
{
|
||||
|
|
@ -131,13 +132,18 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
nameof(appearanceUpdate));
|
||||
}
|
||||
if (!_runtime.IsCurrentCreateIntegration(
|
||||
expectedRecord,
|
||||
expectedCanonical,
|
||||
expectedCreateIntegrationVersion)
|
||||
|| canonicalSpawn.Guid != expectedRecord.ServerGuid
|
||||
|| canonicalSpawn.InstanceSequence != expectedRecord.Generation)
|
||||
|| canonicalSpawn.Guid != expectedCanonical.ServerGuid
|
||||
|| canonicalSpawn.InstanceSequence != expectedCanonical.Incarnation)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_runtime.TryGetProjection(
|
||||
expectedCanonical,
|
||||
out LiveEntityRecord? expectedRecord);
|
||||
if (appearanceUpdate is not null && expectedRecord is null)
|
||||
return false;
|
||||
|
||||
_received++;
|
||||
bool dumpLiveSpawns = _options.DumpLiveSpawns;
|
||||
|
|
@ -186,7 +192,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
return false;
|
||||
}
|
||||
|
||||
expectedRecord.HasPartArray = true;
|
||||
_runtime.SetHasPartArray(expectedCanonical, true);
|
||||
ushort? stanceOverride = canonicalSpawn.MotionState?.Stance;
|
||||
ushort? commandOverride = canonicalSpawn.MotionState?.ForwardCommand;
|
||||
var idleCycle = MotionResolver.GetIdleCycle(
|
||||
|
|
@ -327,7 +333,8 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
|
||||
bool supersessionRecovery =
|
||||
purpose is LiveProjectionPurpose.CreateSupersessionRecovery;
|
||||
if (supersessionRecovery && expectedRecord.WorldEntity is not null)
|
||||
if (supersessionRecovery
|
||||
&& expectedRecord?.WorldEntity is not null)
|
||||
{
|
||||
return LiveEntityCreateSupersessionRecovery.TryApply(
|
||||
_runtime,
|
||||
|
|
@ -380,7 +387,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
if (appearanceUpdate is { } visualUpdate)
|
||||
{
|
||||
return ApplyAppearance(
|
||||
expectedRecord,
|
||||
expectedRecord!,
|
||||
canonicalSpawn,
|
||||
visualUpdate,
|
||||
setup,
|
||||
|
|
@ -398,6 +405,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
}
|
||||
|
||||
return MaterializeProjection(
|
||||
expectedCanonical,
|
||||
expectedRecord,
|
||||
canonicalSpawn,
|
||||
setup,
|
||||
|
|
@ -670,7 +678,8 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
}
|
||||
|
||||
private bool MaterializeProjection(
|
||||
LiveEntityRecord expectedRecord,
|
||||
RuntimeEntityRecord expectedCanonical,
|
||||
LiveEntityRecord? retainedRecord,
|
||||
WorldSession.EntitySpawn spawn,
|
||||
Setup setup,
|
||||
MotionResolver.IdleCycle? idleCycle,
|
||||
|
|
@ -690,17 +699,18 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
ulong expectedCreateIntegrationVersion,
|
||||
bool synchronizeAnimation)
|
||||
{
|
||||
if (!_runtime.TryGetEffectProfile(spawn.Guid, out _))
|
||||
EntityEffectProfile profile = spawn.Physics is { } physics
|
||||
? EntityEffectProfile.CreateLive(setup, physics)
|
||||
: EntityEffectProfile.CreateDatStatic(setup);
|
||||
if (retainedRecord is not null
|
||||
&& !_runtime.TryGetEffectProfile(spawn.Guid, out _))
|
||||
{
|
||||
EntityEffectProfile profile = spawn.Physics is { } physics
|
||||
? EntityEffectProfile.CreateLive(setup, physics)
|
||||
: EntityEffectProfile.CreateDatStatic(setup);
|
||||
_runtime.SetEffectProfile(spawn.Guid, profile);
|
||||
}
|
||||
|
||||
bool createdProjection = false;
|
||||
WorldEntity? entity = _runtime.MaterializeLiveEntity(
|
||||
spawn.Guid,
|
||||
expectedCanonical,
|
||||
spawn.Position!.Value.LandblockId,
|
||||
localId =>
|
||||
{
|
||||
|
|
@ -721,8 +731,12 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
if (bounds.TryGet(out Vector3 minimum, out Vector3 maximum))
|
||||
created.SetLocalBounds(minimum, maximum);
|
||||
return created;
|
||||
});
|
||||
},
|
||||
LiveEntityProjectionKind.World,
|
||||
initializeProjection: record => record.EffectProfile = profile,
|
||||
out LiveEntityRecord? expectedRecord);
|
||||
if (entity is null
|
||||
|| expectedRecord is null
|
||||
|| !_runtime.IsCurrentCreateIntegration(
|
||||
expectedRecord,
|
||||
expectedCreateIntegrationVersion)
|
||||
|
|
@ -828,7 +842,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
if (dumpLiveSpawns && _received % 20 == 0)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"live: animated={_runtime.AnimationRuntimes.Count} "
|
||||
$"live: animated={_runtime.AnimationRuntimeCount} "
|
||||
+ $"animReject: noCycle={_noCycle} fr0={_zeroFramerate} "
|
||||
+ $"1frame={_singleFrame} partFrames={_missingPartFrames}");
|
||||
Console.WriteLine(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -524,15 +524,8 @@ public sealed class GameWindow :
|
|||
/// projection, including live objects parked in pending landblocks;
|
||||
/// visibility must never suppress authoritative mutation.
|
||||
/// </summary>
|
||||
private static readonly IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity> EmptyLiveEntityMap =
|
||||
new Dictionary<uint, AcDream.Core.World.WorldEntity>();
|
||||
private static readonly IReadOnlyDictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> EmptyLiveSpawnMap =
|
||||
new Dictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn>();
|
||||
private IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity> _entitiesByServerGuid =>
|
||||
_liveEntities?.MaterializedWorldEntities ?? EmptyLiveEntityMap;
|
||||
/// <summary>Visible-only view for radar, picking, status, and targets.</summary>
|
||||
private IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity> _visibleEntitiesByServerGuid =>
|
||||
_liveEntities?.WorldEntities ?? EmptyLiveEntityMap;
|
||||
/// <summary>
|
||||
/// Latest <see cref="AcDream.Core.Net.WorldSession.EntitySpawn"/> for each
|
||||
/// guid. Captured before the renderability gate so no-position inventory /
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using AcDream.App.Rendering.Vfx;
|
|||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Entities;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
}
|
||||
|
||||
public void Present(
|
||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules)
|
||||
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(schedules);
|
||||
LiveEntityRuntime runtime = _liveEntities;
|
||||
|
|
@ -107,7 +108,9 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
if (!TryGetCurrent(runtime, record, entity, animation))
|
||||
continue;
|
||||
|
||||
schedules.TryGetValue(entity.Id, out LiveEntityAnimationSchedule schedule);
|
||||
LiveEntityAnimationSchedule schedule = default;
|
||||
if (record.ProjectionKey is { } key)
|
||||
schedules.TryGetValue(key, out schedule);
|
||||
bool hasOrdinarySchedule = IsCurrentSchedule(runtime, schedule, record, entity, animation);
|
||||
IReadOnlyList<PartTransform>? sequenceFrames = hasOrdinarySchedule
|
||||
? schedule.SequenceFrames
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using AcDream.App.World;
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Entities;
|
||||
using DatReaderWriter.Types;
|
||||
using RemoteMotion = AcDream.App.Physics.RemoteMotion;
|
||||
|
||||
|
|
@ -27,7 +28,8 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
private readonly IEntityRootPosePublisher _rootPoses;
|
||||
private readonly IAnimationHookCaptureSink _animationHooks;
|
||||
private readonly List<LiveEntityRecord> _rootSnapshot = new();
|
||||
private readonly Dictionary<uint, LiveEntityAnimationSchedule> _schedules = new();
|
||||
private readonly Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
|
||||
_schedules = [];
|
||||
private readonly Frame _rootFrameScratch = new();
|
||||
private readonly MotionDeltaFrame _rootDeltaScratch = new();
|
||||
|
||||
|
|
@ -59,7 +61,7 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
/// callback. The returned dictionary is reused and valid until the next
|
||||
/// call.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> Tick(
|
||||
public IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> Tick(
|
||||
float elapsedSeconds,
|
||||
Vector3? playerPosition,
|
||||
bool localHiddenPartPoseDirty,
|
||||
|
|
@ -139,7 +141,11 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
IReadOnlyList<PartTransform>? ownedFrames = schedule.SequenceFrames is { } produced
|
||||
? animation.CaptureScheduleFrames(produced)
|
||||
: null;
|
||||
_schedules[entity.Id] = schedule with
|
||||
RuntimeEntityKey key = record.ProjectionKey
|
||||
?? throw new InvalidOperationException(
|
||||
$"Live entity 0x{record.ServerGuid:X8}/" +
|
||||
$"{record.Generation} has no exact projection key.");
|
||||
_schedules[key] = schedule with
|
||||
{
|
||||
SequenceFrames = ownedFrames,
|
||||
Record = record,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.App.Update;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Entities;
|
||||
|
||||
namespace AcDream.App.Rendering.Scene;
|
||||
|
||||
|
|
@ -9,7 +10,7 @@ internal interface ILiveRenderProjectionSink : IRenderProjectionSyncPhase
|
|||
bool OnEntityReady(LiveEntityReadyCandidate candidate);
|
||||
void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible);
|
||||
void OnProjectionPoseReady(uint serverGuid);
|
||||
void OnProjectionRemoved(uint localEntityId);
|
||||
void OnProjectionRemoved(LiveEntityRecord record);
|
||||
void OnResourceUnregister(WorldEntity entity);
|
||||
}
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
private readonly LiveEntityRuntime _runtime;
|
||||
private readonly RenderProjectionJournal _journal;
|
||||
private readonly IRenderTraversalOrderSource _traversalOrder;
|
||||
private readonly Dictionary<uint, TrackedProjection> _byLocalId = [];
|
||||
private readonly Dictionary<RuntimeEntityKey, TrackedProjection> _byKey = [];
|
||||
private readonly List<LiveEntityRecord> _activeRootScratch = [];
|
||||
private readonly List<TrackedProjection> _activeScratch = [];
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
?? throw new ArgumentNullException(nameof(traversalOrder));
|
||||
}
|
||||
|
||||
public int ProjectionCount => _byLocalId.Count;
|
||||
public int ProjectionCount => _byKey.Count;
|
||||
|
||||
public bool OnEntityReady(LiveEntityReadyCandidate candidate)
|
||||
{
|
||||
|
|
@ -62,7 +63,8 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (!_runtime.IsCurrentRecord(record)
|
||||
|| record.WorldEntity is not { } entity
|
||||
|| !_byLocalId.ContainsKey(entity.Id))
|
||||
|| record.ProjectionKey is not { } key
|
||||
|| !_byKey.ContainsKey(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -87,10 +89,12 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
Upsert(record, entity, record.IsSpatiallyVisible);
|
||||
}
|
||||
|
||||
public void OnProjectionRemoved(uint localEntityId)
|
||||
public void OnProjectionRemoved(LiveEntityRecord record)
|
||||
{
|
||||
if (_byLocalId.TryGetValue(
|
||||
localEntityId,
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (record.ProjectionKey is { } key
|
||||
&& _byKey.TryGetValue(
|
||||
key,
|
||||
out TrackedProjection? tracked)
|
||||
&& tracked.Projection.ProjectionClass is
|
||||
RenderProjectionClass.EquippedChild)
|
||||
|
|
@ -116,11 +120,17 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
public void OnResourceUnregister(WorldEntity entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
if (_byLocalId.TryGetValue(entity.Id, out TrackedProjection? tracked)
|
||||
&& ReferenceEquals(tracked.Entity, entity))
|
||||
TrackedProjection? tracked = null;
|
||||
foreach (TrackedProjection candidate in _byKey.Values)
|
||||
{
|
||||
Remove(tracked);
|
||||
if (ReferenceEquals(candidate.Entity, entity))
|
||||
{
|
||||
tracked = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tracked is not null)
|
||||
Remove(tracked);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -153,7 +163,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
// synchronization only refreshes children whose attachment projection
|
||||
// is still retained by this derived scene.
|
||||
_activeScratch.Clear();
|
||||
foreach (TrackedProjection tracked in _byLocalId.Values)
|
||||
foreach (TrackedProjection tracked in _byKey.Values)
|
||||
{
|
||||
if (tracked.Record.IsSpatiallyProjected
|
||||
&& tracked.Record.ProjectionKind is
|
||||
|
|
@ -166,8 +176,9 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
TrackedProjection tracked = _activeScratch[i];
|
||||
if (!_runtime.IsCurrentRecord(tracked.Record)
|
||||
|| !ReferenceEquals(tracked.Record.WorldEntity, tracked.Entity)
|
||||
|| !_byLocalId.TryGetValue(
|
||||
tracked.Entity.Id,
|
||||
|| tracked.Record.ProjectionKey is not { } key
|
||||
|| !_byKey.TryGetValue(
|
||||
key,
|
||||
out TrackedProjection? current)
|
||||
|| !ReferenceEquals(current, tracked))
|
||||
{
|
||||
|
|
@ -183,7 +194,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
|
||||
public void ResetTracking()
|
||||
{
|
||||
_byLocalId.Clear();
|
||||
_byKey.Clear();
|
||||
_activeRootScratch.Clear();
|
||||
_activeScratch.Clear();
|
||||
}
|
||||
|
|
@ -192,9 +203,11 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
uint localEntityId,
|
||||
out RenderProjectionRecord record)
|
||||
{
|
||||
if (_byLocalId.TryGetValue(
|
||||
if (_runtime.TryGetRecordByLocalEntityId(
|
||||
localEntityId,
|
||||
out TrackedProjection? tracked))
|
||||
out LiveEntityRecord owner)
|
||||
&& owner.ProjectionKey is { } key
|
||||
&& _byKey.TryGetValue(key, out TrackedProjection? tracked))
|
||||
{
|
||||
record = tracked.Projection;
|
||||
return true;
|
||||
|
|
@ -217,11 +230,12 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
record,
|
||||
entity,
|
||||
spatiallyVisible);
|
||||
if (!_byLocalId.TryGetValue(entity.Id, out TrackedProjection? tracked))
|
||||
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||
if (!_byKey.TryGetValue(key, out TrackedProjection? tracked))
|
||||
{
|
||||
_journal.Register(in projected);
|
||||
_byLocalId.Add(
|
||||
entity.Id,
|
||||
_byKey.Add(
|
||||
key,
|
||||
new TrackedProjection(record, entity, projected));
|
||||
return;
|
||||
}
|
||||
|
|
@ -234,7 +248,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
!= projected.ProjectionClass)
|
||||
{
|
||||
_journal.Register(in projected);
|
||||
_byLocalId[entity.Id] =
|
||||
_byKey[key] =
|
||||
new TrackedProjection(record, entity, projected);
|
||||
return;
|
||||
}
|
||||
|
|
@ -283,8 +297,9 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
// Spatial withdrawal is not logical destruction. Retain the last
|
||||
// resident order while the projection is inactive so a portal/rebucket
|
||||
// edge does not manufacture an unrelated ordering mutation.
|
||||
return _byLocalId.TryGetValue(
|
||||
entity.Id,
|
||||
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||
return _byKey.TryGetValue(
|
||||
key,
|
||||
out TrackedProjection? retained)
|
||||
? projected with { SortKey = retained.Projection.SortKey }
|
||||
: projected;
|
||||
|
|
@ -292,7 +307,8 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
|
||||
private void Remove(TrackedProjection tracked)
|
||||
{
|
||||
if (!_byLocalId.Remove(tracked.Entity.Id))
|
||||
RuntimeEntityKey key = RequireProjectionKey(tracked.Record);
|
||||
if (!_byKey.Remove(key))
|
||||
return;
|
||||
_journal.Unregister(
|
||||
tracked.Projection.Id,
|
||||
|
|
@ -302,6 +318,13 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
private static uint Canonicalize(uint cellOrLandblockId) =>
|
||||
(cellOrLandblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||
|
||||
private static RuntimeEntityKey RequireProjectionKey(
|
||||
LiveEntityRecord record) =>
|
||||
record.ProjectionKey
|
||||
?? throw new InvalidOperationException(
|
||||
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
|
||||
"has no exact projection key.");
|
||||
|
||||
private sealed class TrackedProjection(
|
||||
LiveEntityRecord record,
|
||||
WorldEntity entity,
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ internal sealed class RuntimeFramePipelineDiagnosticFactsSource :
|
|||
_streaming?.DeferredApplyBacklog ?? 0,
|
||||
_streaming?.FullWindowRetirementCount ?? 0,
|
||||
_streaming?.LastFullWindowRetirementLandblockCount ?? 0,
|
||||
_liveEntities.MaterializedWorldEntities.Count,
|
||||
_liveEntities.MaterializedCount,
|
||||
_liveEntities.Snapshots.Count,
|
||||
_world.Entities.Count);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using AcDream.Core.Net.Messages;
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Vfx;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Entities;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Rendering.Vfx;
|
||||
|
|
@ -33,16 +34,17 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
private readonly Func<uint, uint?> _parentOfAttachedChild;
|
||||
private readonly Action<uint> _ownerUnregistered;
|
||||
private readonly Action<uint, uint?> _ownerSoundTableChanged;
|
||||
private readonly Dictionary<uint, EntityEffectProfile> _profilesByLocalId = new();
|
||||
private readonly Dictionary<uint, ushort> _readyGenerationByServerGuid = new();
|
||||
private readonly Dictionary<RuntimeEntityKey, EntityEffectProfile> _liveProfiles = [];
|
||||
private readonly HashSet<RuntimeEntityKey> _readyLiveOwners = [];
|
||||
private readonly Dictionary<uint, Queue<PendingEffect>> _pendingByServerGuid = new();
|
||||
private readonly Dictionary<uint, WorldEntity> _staticOwners = new();
|
||||
private readonly Dictionary<uint, EntityEffectProfile> _staticProfiles = new();
|
||||
private readonly HashSet<uint> _syntheticOwners = new();
|
||||
private readonly HashSet<LiveEntityRecord> _dirtyLiveOwners =
|
||||
new(ReferenceEqualityComparer.Instance);
|
||||
private readonly List<LiveEntityRecord> _dirtyLiveOwnerOrder = [];
|
||||
private readonly List<LiveEntityRecord> _dirtyLiveOwnerSnapshot = [];
|
||||
private readonly List<uint> _readyGuidSnapshot = [];
|
||||
private readonly List<RuntimeEntityKey> _readyKeySnapshot = [];
|
||||
private uint _posePublishLocalId;
|
||||
private Action<string>? _diagnosticSink = Console.WriteLine;
|
||||
|
||||
|
|
@ -76,7 +78,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
}
|
||||
|
||||
public int PendingPacketCount => _pendingByServerGuid.Values.Sum(queue => queue.Count);
|
||||
public int ReadyOwnerCount => _profilesByLocalId.Count;
|
||||
public int ReadyOwnerCount => _liveProfiles.Count + _staticProfiles.Count;
|
||||
internal int LastPoseRefreshOwnerVisitCount { get; private set; }
|
||||
|
||||
public void HandleDirect(PlayPhysicsScript message)
|
||||
|
|
@ -135,8 +137,9 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
return false;
|
||||
}
|
||||
|
||||
_readyGenerationByServerGuid[serverGuid] = record.Generation;
|
||||
_profilesByLocalId[entity.Id] = profile;
|
||||
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||
_readyLiveOwners.Add(key);
|
||||
_liveProfiles[key] = profile;
|
||||
_runner.SetOwnerAnchor(entity.Id, entity.Position);
|
||||
_ownerSoundTableChanged(entity.Id, profile.CurrentSoundTableDid);
|
||||
return true;
|
||||
|
|
@ -166,7 +169,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
return false;
|
||||
}
|
||||
|
||||
_profilesByLocalId[localId] = profile;
|
||||
_liveProfiles[RequireProjectionKey(record)] = profile;
|
||||
_ownerSoundTableChanged(localId, profile.CurrentSoundTableDid);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -181,7 +184,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
if (ownerLocalId == 0)
|
||||
return;
|
||||
_staticOwners[ownerLocalId] = entity;
|
||||
_profilesByLocalId[ownerLocalId] = profile;
|
||||
_staticProfiles[ownerLocalId] = profile;
|
||||
_runner.SetOwnerAnchor(ownerLocalId, entity.Position);
|
||||
_ownerSoundTableChanged(ownerLocalId, profile.CurrentSoundTableDid);
|
||||
}
|
||||
|
|
@ -190,7 +193,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
{
|
||||
if (!_staticOwners.Remove(localId))
|
||||
return;
|
||||
_profilesByLocalId.Remove(localId);
|
||||
_staticProfiles.Remove(localId);
|
||||
_runner.StopAllForEntity(localId);
|
||||
_ownerUnregistered(localId);
|
||||
}
|
||||
|
|
@ -212,16 +215,13 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
{
|
||||
_pendingByServerGuid.Remove(record.ServerGuid);
|
||||
}
|
||||
if (_readyGenerationByServerGuid.TryGetValue(
|
||||
record.ServerGuid,
|
||||
out ushort readyGeneration)
|
||||
&& readyGeneration == record.Generation)
|
||||
if (record.ProjectionKey is { } key)
|
||||
{
|
||||
_readyGenerationByServerGuid.Remove(record.ServerGuid);
|
||||
_readyLiveOwners.Remove(key);
|
||||
_liveProfiles.Remove(key);
|
||||
}
|
||||
if (record.LocalEntityId is not { } localId)
|
||||
return;
|
||||
_profilesByLocalId.Remove(localId);
|
||||
_runner.StopAllForEntity(localId);
|
||||
_ownerUnregistered(localId);
|
||||
}
|
||||
|
|
@ -232,18 +232,15 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
|
||||
public void ClearNetworkState()
|
||||
{
|
||||
_readyGuidSnapshot.Clear();
|
||||
_readyGuidSnapshot.AddRange(_readyGenerationByServerGuid.Keys);
|
||||
foreach (uint serverGuid in _readyGuidSnapshot)
|
||||
_readyKeySnapshot.Clear();
|
||||
_readyKeySnapshot.AddRange(_readyLiveOwners);
|
||||
foreach (RuntimeEntityKey key in _readyKeySnapshot)
|
||||
{
|
||||
if (_liveEntities.TryGetLocalEntityId(serverGuid, out uint localId))
|
||||
{
|
||||
_profilesByLocalId.Remove(localId);
|
||||
_runner.StopAllForEntity(localId);
|
||||
_ownerUnregistered(localId);
|
||||
}
|
||||
_runner.StopAllForEntity(key.LocalEntityId);
|
||||
_ownerUnregistered(key.LocalEntityId);
|
||||
}
|
||||
_readyGenerationByServerGuid.Clear();
|
||||
_readyLiveOwners.Clear();
|
||||
_liveProfiles.Clear();
|
||||
_pendingByServerGuid.Clear();
|
||||
_dirtyLiveOwners.Clear();
|
||||
_dirtyLiveOwnerOrder.Clear();
|
||||
|
|
@ -331,7 +328,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
uint rawScriptType,
|
||||
float intensity)
|
||||
{
|
||||
if (!_profilesByLocalId.TryGetValue(ownerLocalId, out EntityEffectProfile? profile)
|
||||
if (!TryGetProfile(ownerLocalId, out EntityEffectProfile? profile)
|
||||
|| profile.CurrentPhysicsScriptTableDid is not { } tableDid)
|
||||
{
|
||||
DiagnosticSink?.Invoke(
|
||||
|
|
@ -360,7 +357,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
public bool PlayDefault(uint ownerLocalId)
|
||||
{
|
||||
if (!CanStartOwner(ownerLocalId)
|
||||
|| !_profilesByLocalId.TryGetValue(ownerLocalId, out EntityEffectProfile? profile))
|
||||
|| !TryGetProfile(ownerLocalId, out EntityEffectProfile? profile))
|
||||
return false;
|
||||
return PlayTyped(
|
||||
ownerLocalId,
|
||||
|
|
@ -433,8 +430,9 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
|
||||
private bool TryGetLiveRoot(uint ownerLocalId, out LiveEntityRecord record)
|
||||
{
|
||||
if (_liveEntities.TryGetServerGuid(ownerLocalId, out uint serverGuid)
|
||||
&& _liveEntities.TryGetRecord(serverGuid, out record!))
|
||||
if (_liveEntities.TryGetRecordByLocalEntityId(
|
||||
ownerLocalId,
|
||||
out record!))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -444,12 +442,12 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
|
||||
private bool TryGetReadyLocalId(uint serverGuid, out uint localId)
|
||||
{
|
||||
if (_readyGenerationByServerGuid.TryGetValue(serverGuid, out ushort readyGeneration)
|
||||
&& _liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||
&& record.Generation == readyGeneration
|
||||
&& _liveEntities.TryGetLocalEntityId(serverGuid, out localId)
|
||||
&& _profilesByLocalId.ContainsKey(localId))
|
||||
if (_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||
&& record.ProjectionKey is { } key
|
||||
&& _readyLiveOwners.Contains(key)
|
||||
&& _liveProfiles.ContainsKey(key))
|
||||
{
|
||||
localId = key.LocalEntityId;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -461,8 +459,9 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
{
|
||||
if (_posePublishLocalId == localId)
|
||||
return;
|
||||
if (_liveEntities.TryGetServerGuid(localId, out uint serverGuid)
|
||||
&& _liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record))
|
||||
if (_liveEntities.TryGetRecordByLocalEntityId(
|
||||
localId,
|
||||
out LiveEntityRecord record))
|
||||
{
|
||||
MarkLiveOwnerPoseDirty(record);
|
||||
}
|
||||
|
|
@ -476,10 +475,8 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
|
||||
private void MarkLiveOwnerPoseDirty(LiveEntityRecord record)
|
||||
{
|
||||
if (!_readyGenerationByServerGuid.TryGetValue(
|
||||
record.ServerGuid,
|
||||
out ushort readyGeneration)
|
||||
|| readyGeneration != record.Generation
|
||||
if (record.ProjectionKey is not { } key
|
||||
|| !_readyLiveOwners.Contains(key)
|
||||
|| !_dirtyLiveOwners.Add(record))
|
||||
{
|
||||
return;
|
||||
|
|
@ -550,6 +547,29 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
PlayTyped(localId, effect.RawScriptType, effect.Intensity);
|
||||
}
|
||||
|
||||
private bool TryGetProfile(
|
||||
uint ownerLocalId,
|
||||
out EntityEffectProfile profile)
|
||||
{
|
||||
if (_liveEntities.TryGetRecordByLocalEntityId(
|
||||
ownerLocalId,
|
||||
out LiveEntityRecord record)
|
||||
&& record.ProjectionKey is { } key
|
||||
&& _liveProfiles.TryGetValue(key, out profile!))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _staticProfiles.TryGetValue(ownerLocalId, out profile!);
|
||||
}
|
||||
|
||||
private static RuntimeEntityKey RequireProjectionKey(
|
||||
LiveEntityRecord record) =>
|
||||
record.ProjectionKey
|
||||
?? throw new InvalidOperationException(
|
||||
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
|
||||
"has no exact projection key.");
|
||||
|
||||
private enum PendingEffectKind
|
||||
{
|
||||
Direct,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.App.World;
|
||||
using AcDream.Core.Lighting;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Entities;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Rendering.Vfx;
|
||||
|
|
@ -22,8 +23,8 @@ public sealed class LiveEntityLightController : IDisposable
|
|||
private readonly EntityEffectPoseRegistry _poses;
|
||||
private readonly LightingHookSink _lighting;
|
||||
private readonly Func<uint, Setup?> _loadSetup;
|
||||
private readonly Dictionary<uint, uint> _serverGuidByOwner = new();
|
||||
private readonly HashSet<uint> _presentOwners = new();
|
||||
private readonly HashSet<RuntimeEntityKey> _trackedOwners = [];
|
||||
private readonly HashSet<RuntimeEntityKey> _presentOwners = [];
|
||||
|
||||
public LiveEntityLightController(
|
||||
LiveEntityRuntime liveEntities,
|
||||
|
|
@ -49,8 +50,9 @@ public sealed class LiveEntityLightController : IDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
_serverGuidByOwner[entity.Id] = serverGuid;
|
||||
_presentOwners.Add(entity.Id);
|
||||
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||
_trackedOwners.Add(key);
|
||||
_presentOwners.Add(key);
|
||||
_lighting.UnregisterOwner(entity.Id, forgetState: false);
|
||||
_lighting.InitializeOwnerLighting(
|
||||
entity.Id,
|
||||
|
|
@ -87,7 +89,13 @@ public sealed class LiveEntityLightController : IDisposable
|
|||
/// <summary>Withdraw cell-scoped presentation but retain logical light state.</summary>
|
||||
public void Unregister(uint ownerLocalId)
|
||||
{
|
||||
_presentOwners.Remove(ownerLocalId);
|
||||
if (_liveEntities.TryGetRecordByLocalEntityId(
|
||||
ownerLocalId,
|
||||
out LiveEntityRecord record)
|
||||
&& record.ProjectionKey is { } key)
|
||||
{
|
||||
_presentOwners.Remove(key);
|
||||
}
|
||||
_lighting.UnregisterOwner(ownerLocalId, forgetState: false);
|
||||
}
|
||||
|
||||
|
|
@ -105,16 +113,20 @@ public sealed class LiveEntityLightController : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>End logical ownership after leave-world presentation teardown.</summary>
|
||||
public void Forget(uint ownerLocalId)
|
||||
public void Forget(LiveEntityRecord record)
|
||||
{
|
||||
_presentOwners.Remove(ownerLocalId);
|
||||
_serverGuidByOwner.Remove(ownerLocalId);
|
||||
_lighting.UnregisterOwner(ownerLocalId);
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (record.ProjectionKey is not { } key)
|
||||
return;
|
||||
|
||||
_presentOwners.Remove(key);
|
||||
_trackedOwners.Remove(key);
|
||||
_lighting.UnregisterOwner(key.LocalEntityId);
|
||||
}
|
||||
|
||||
public void Refresh() => _lighting.RefreshAttachedLights();
|
||||
|
||||
internal int TrackedOwnerCount => _serverGuidByOwner.Count;
|
||||
internal int TrackedOwnerCount => _trackedOwners.Count;
|
||||
internal int PresentedOwnerCount => _presentOwners.Count;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -127,7 +139,8 @@ public sealed class LiveEntityLightController : IDisposable
|
|||
if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||
|| record.ProjectionKind is not LiveEntityProjectionKind.Attached
|
||||
|| record.WorldEntity is not { } entity
|
||||
|| _presentOwners.Contains(entity.Id))
|
||||
|| record.ProjectionKey is not { } key
|
||||
|| _presentOwners.Contains(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -136,8 +149,11 @@ public sealed class LiveEntityLightController : IDisposable
|
|||
|
||||
private void OnOwnerLightingChanged(uint ownerLocalId, bool enabled)
|
||||
{
|
||||
if (!_presentOwners.Contains(ownerLocalId)
|
||||
|| !_serverGuidByOwner.TryGetValue(ownerLocalId, out uint serverGuid))
|
||||
if (!_liveEntities.TryGetRecordByLocalEntityId(
|
||||
ownerLocalId,
|
||||
out LiveEntityRecord record)
|
||||
|| record.ProjectionKey is not { } key
|
||||
|| !_presentOwners.Contains(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -148,15 +164,17 @@ public sealed class LiveEntityLightController : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
Register(serverGuid);
|
||||
Register(record.ServerGuid);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_lighting.OwnerLightingChanged -= OnOwnerLightingChanged;
|
||||
_liveEntities.ProjectionVisibilityChanged -= OnProjectionVisibilityChanged;
|
||||
foreach (uint ownerId in _serverGuidByOwner.Keys.ToArray())
|
||||
Forget(ownerId);
|
||||
foreach (RuntimeEntityKey key in _trackedOwners)
|
||||
_lighting.UnregisterOwner(key.LocalEntityId);
|
||||
_trackedOwners.Clear();
|
||||
_presentOwners.Clear();
|
||||
}
|
||||
|
||||
private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
|
||||
|
|
@ -176,4 +194,11 @@ public sealed class LiveEntityLightController : IDisposable
|
|||
else
|
||||
Unregister(entity.Id);
|
||||
}
|
||||
|
||||
private static RuntimeEntityKey RequireProjectionKey(
|
||||
LiveEntityRecord record) =>
|
||||
record.ProjectionKey
|
||||
?? throw new InvalidOperationException(
|
||||
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
|
||||
"has no exact projection key.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Entities;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
|
|
@ -48,18 +49,20 @@ public sealed class EntitySpawnAdapter
|
|||
private readonly Func<WorldEntity, AnimationSequencer> _sequencerFactory;
|
||||
private readonly IWbMeshAdapter? _meshAdapter;
|
||||
|
||||
// One logical owner per server GUID. Animated state survives projection
|
||||
// One logical owner per exact Runtime identity. Animated state survives projection
|
||||
// suspension, while the resident bit controls the shorter GPU-presentation
|
||||
// lifetime. The exact WorldEntity reference makes delayed visibility edges
|
||||
// from a displaced GUID generation harmless.
|
||||
// Single-threaded: called only from the render thread (same as GpuWorldState).
|
||||
private readonly Dictionary<uint, Owner> _ownersByGuid = new();
|
||||
private readonly Dictionary<RuntimeEntityKey, Owner> _ownersByKey = [];
|
||||
|
||||
private sealed class Owner(
|
||||
RuntimeEntityKey key,
|
||||
WorldEntity entity,
|
||||
AnimatedEntityState state,
|
||||
HashSet<ulong> meshIds)
|
||||
{
|
||||
public RuntimeEntityKey Key { get; } = key;
|
||||
public WorldEntity Entity { get; } = entity;
|
||||
public AnimatedEntityState State { get; } = state;
|
||||
public HashSet<ulong> MeshIds { get; set; } = meshIds;
|
||||
|
|
@ -137,12 +140,25 @@ public sealed class EntitySpawnAdapter
|
|||
/// <paramref name="entity"/> is atlas-tier (<c>ServerGuid == 0</c>).
|
||||
/// </summary>
|
||||
public AnimatedEntityState? OnCreate(WorldEntity entity)
|
||||
=> OnCreate(new RuntimeEntityKey(entity.Id, 0), entity);
|
||||
|
||||
/// <summary>
|
||||
/// Creates one exact Runtime-owned presentation resource set.
|
||||
/// </summary>
|
||||
public AnimatedEntityState? OnCreate(
|
||||
RuntimeEntityKey key,
|
||||
WorldEntity entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
// Atlas-tier entities (procedural / dat-hydrated, ServerGuid == 0)
|
||||
// are handled by LandblockSpawnAdapter, not here.
|
||||
if (entity.ServerGuid == 0) return null;
|
||||
if (key.LocalEntityId == 0 || key.LocalEntityId != entity.Id)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The exact Runtime projection key must match the WorldEntity local ID.");
|
||||
}
|
||||
|
||||
// A.5 T18: populate cached AABB so WalkEntities reads from the cache
|
||||
// rather than recomputing Position±5 per frame. Called here because
|
||||
|
|
@ -177,8 +193,8 @@ public sealed class EntitySpawnAdapter
|
|||
// valid. Retirement is also completed before replacement publication:
|
||||
// a failed texture or mesh release therefore leaves the prior owner in
|
||||
// the dictionary, with its per-resource progress available to retry.
|
||||
var replacementOwner = new Owner(entity, state, meshIds);
|
||||
if (_ownersByGuid.TryGetValue(entity.ServerGuid, out Owner? displacedOwner))
|
||||
var replacementOwner = new Owner(key, entity, state, meshIds);
|
||||
if (_ownersByKey.TryGetValue(key, out Owner? displacedOwner))
|
||||
{
|
||||
if (displacedOwner.RemovalPending)
|
||||
{
|
||||
|
|
@ -200,11 +216,11 @@ public sealed class EntitySpawnAdapter
|
|||
}
|
||||
}
|
||||
|
||||
_ownersByGuid[entity.ServerGuid] = replacementOwner;
|
||||
_ownersByKey[key] = replacementOwner;
|
||||
}
|
||||
else
|
||||
{
|
||||
_ownersByGuid.Add(entity.ServerGuid, replacementOwner);
|
||||
_ownersByKey.Add(key, replacementOwner);
|
||||
}
|
||||
|
||||
return state;
|
||||
|
|
@ -223,8 +239,7 @@ public sealed class EntitySpawnAdapter
|
|||
public bool SetPresentationResident(WorldEntity entity, bool resident)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
if (!_ownersByGuid.TryGetValue(entity.ServerGuid, out Owner? owner)
|
||||
|| !ReferenceEquals(owner.Entity, entity)
|
||||
if (!TryFindOwner(entity, out _, out Owner owner)
|
||||
|| owner.RemovalPending
|
||||
|| (resident ? owner.IsFullyResident : owner.IsFullySuspended))
|
||||
{
|
||||
|
|
@ -270,8 +285,7 @@ public sealed class EntitySpawnAdapter
|
|||
ArgumentNullException.ThrowIfNull(partOverrides);
|
||||
ArgumentNullException.ThrowIfNull(publishAppearance);
|
||||
|
||||
if (!_ownersByGuid.TryGetValue(entity.ServerGuid, out Owner? owner)
|
||||
|| !ReferenceEquals(owner.Entity, entity)
|
||||
if (!TryFindOwner(entity, out _, out Owner owner)
|
||||
|| owner.RemovalPending
|
||||
|| owner.Transition != PresentationTransition.None)
|
||||
{
|
||||
|
|
@ -340,19 +354,31 @@ public sealed class EntitySpawnAdapter
|
|||
/// and propagates the exception; a later call resumes its unfinished releases.
|
||||
/// </summary>
|
||||
public void OnRemove(uint serverGuid)
|
||||
=> _ = TryRemove(serverGuid, expectedEntity: null);
|
||||
|
||||
private bool TryRemove(uint serverGuid, WorldEntity? expectedEntity)
|
||||
{
|
||||
if (!_ownersByGuid.TryGetValue(serverGuid, out Owner? owner)
|
||||
|| (expectedEntity is not null && !ReferenceEquals(owner.Entity, expectedEntity)))
|
||||
foreach ((RuntimeEntityKey key, Owner owner) in _ownersByKey)
|
||||
{
|
||||
if (owner.Entity.ServerGuid == serverGuid)
|
||||
{
|
||||
_ = TryRemove(key, owner.Entity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryRemove(
|
||||
RuntimeEntityKey key,
|
||||
WorldEntity expectedEntity)
|
||||
{
|
||||
if (!_ownersByKey.TryGetValue(key, out Owner? owner)
|
||||
|| !ReferenceEquals(owner.Entity, expectedEntity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
owner.RemovalPending = true;
|
||||
if (owner.Transition != PresentationTransition.None)
|
||||
throw new EntityPresentationRemovalDeferredException(serverGuid);
|
||||
throw new EntityPresentationRemovalDeferredException(
|
||||
owner.Entity.ServerGuid);
|
||||
|
||||
// A resident owner still holds both mesh and potentially-lazy texture
|
||||
// resources. A suspended owner released them at the visibility edge,
|
||||
|
|
@ -363,10 +389,10 @@ public sealed class EntitySpawnAdapter
|
|||
if (owner.HasPresentationResources && !SuspendPresentation(owner))
|
||||
return false;
|
||||
|
||||
if (_ownersByGuid.TryGetValue(serverGuid, out Owner? current)
|
||||
if (_ownersByKey.TryGetValue(key, out Owner? current)
|
||||
&& ReferenceEquals(current, owner))
|
||||
{
|
||||
_ownersByGuid.Remove(serverGuid);
|
||||
_ownersByKey.Remove(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -384,7 +410,8 @@ public sealed class EntitySpawnAdapter
|
|||
public bool OnRemove(WorldEntity entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
return TryRemove(entity.ServerGuid, entity);
|
||||
return TryFindOwner(entity, out RuntimeEntityKey key, out _)
|
||||
&& TryRemove(key, entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -393,9 +420,35 @@ public sealed class EntitySpawnAdapter
|
|||
/// been removed.
|
||||
/// </summary>
|
||||
public AnimatedEntityState? GetState(uint serverGuid)
|
||||
=> _ownersByGuid.TryGetValue(serverGuid, out Owner? owner)
|
||||
? owner.State
|
||||
: null;
|
||||
{
|
||||
foreach (Owner owner in _ownersByKey.Values)
|
||||
{
|
||||
if (owner.Entity.ServerGuid == serverGuid)
|
||||
return owner.State;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool TryFindOwner(
|
||||
WorldEntity entity,
|
||||
out RuntimeEntityKey key,
|
||||
out Owner owner)
|
||||
{
|
||||
foreach ((RuntimeEntityKey candidateKey, Owner candidate) in _ownersByKey)
|
||||
{
|
||||
if (ReferenceEquals(candidate.Entity, entity))
|
||||
{
|
||||
key = candidateKey;
|
||||
owner = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
key = default;
|
||||
owner = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ResumePresentation(Owner owner)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue