fix(vfx): bind effects after canonical placement
C3c created graphical effect, projectile, and static-animation sidecars before Runtime finished the entity's first SetPosition. One-shot F754/F755 packets could be discarded, projectiles could adopt a cell-less body, and animated statics could compete for body ownership. Keep effects behind an exact-incarnation presentation barrier, retry projectile/static binding on the committed visibility edge, and keep effect cells synchronized with canonical rebuckets. User verified spell, recall, arrow, projectile, portal, and static presentation; 90 focused App tests and the Release build pass.
This commit is contained in:
parent
1fc529cdcb
commit
f24532adf3
11 changed files with 417 additions and 50 deletions
|
|
@ -36,6 +36,14 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
private readonly Action<uint, uint?> _ownerSoundTableChanged;
|
||||
private readonly Dictionary<RuntimeEntityKey, EntityEffectProfile> _liveProfiles = [];
|
||||
private readonly HashSet<RuntimeEntityKey> _readyLiveOwners = [];
|
||||
// C3c constructs the App effect owner before Runtime's initial placement
|
||||
// receipt binds its world presentation. During that one-time split-lifetime
|
||||
// window, retail still considers the CPhysicsObj absent: SmartBox queues
|
||||
// F754/F755 at 0x00452020/0x00452070, enters the object, then drains them
|
||||
// through ProcessObjectNetBlobs in HandleCreateObject 0x00454C80. Keep the
|
||||
// exact incarnation behind an equivalent barrier until the graphical
|
||||
// placement publishes its pose and resource visibility.
|
||||
private readonly HashSet<RuntimeEntityKey> _initialPresentationBarriers = [];
|
||||
private readonly Dictionary<uint, Queue<PendingEffect>> _pendingByServerGuid = new();
|
||||
private readonly Dictionary<uint, WorldEntity> _staticOwners = new();
|
||||
private readonly Dictionary<uint, EntityEffectProfile> _staticProfiles = new();
|
||||
|
|
@ -90,6 +98,8 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
RefreshLiveAnchor(message.Guid, localId);
|
||||
if (CanStartOwner(localId))
|
||||
PlayDirect(localId, message.ScriptDid);
|
||||
else if (IsWaitingForInitialPresentation(message.Guid))
|
||||
Enqueue(message.Guid, PendingEffect.Direct(message.ScriptDid));
|
||||
return;
|
||||
}
|
||||
Enqueue(message.Guid, PendingEffect.Direct(message.ScriptDid));
|
||||
|
|
@ -104,6 +114,12 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
RefreshLiveAnchor(message.Guid, localId);
|
||||
if (CanStartOwner(localId))
|
||||
PlayTyped(localId, message.RawScriptType, message.Intensity);
|
||||
else if (IsWaitingForInitialPresentation(message.Guid))
|
||||
{
|
||||
Enqueue(
|
||||
message.Guid,
|
||||
PendingEffect.Typed(message.RawScriptType, message.Intensity));
|
||||
}
|
||||
return;
|
||||
}
|
||||
Enqueue(message.Guid, PendingEffect.Typed(message.RawScriptType, message.Intensity));
|
||||
|
|
@ -140,11 +156,45 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||
_readyLiveOwners.Add(key);
|
||||
_liveProfiles[key] = profile;
|
||||
if (record.MaterializationResidence is
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement
|
||||
&& !record.IsSpatiallyProjected)
|
||||
{
|
||||
_initialPresentationBarriers.Add(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
_initialPresentationBarriers.Remove(key);
|
||||
}
|
||||
_runner.SetOwnerAnchor(entity.Id, entity.Position);
|
||||
_ownerSoundTableChanged(entity.Id, profile.CurrentSoundTableDid);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the C3c-only network-script barrier after the initial placement
|
||||
/// has published the entity pose and presentation resources, then replays
|
||||
/// the retained mixed F754/F755 FIFO synchronously in arrival order.
|
||||
/// </summary>
|
||||
public bool OnPresentationBound(LiveEntityRecord record)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (record.ProjectionKey is not { } key
|
||||
|| !_liveEntities.TryGetRecord(key, out LiveEntityRecord current)
|
||||
|| !ReferenceEquals(current, record))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_initialPresentationBarriers.Remove(key);
|
||||
if (!TryGetReadyLocalId(record.ServerGuid, out uint localId))
|
||||
return true;
|
||||
|
||||
RefreshLiveAnchor(record.ServerGuid, localId);
|
||||
TryReplayPending(record.ServerGuid, localId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Replays the mixed F754/F755 FIFO after full object construction.</summary>
|
||||
public bool ReplayPendingForLiveEntity(uint serverGuid)
|
||||
{
|
||||
|
|
@ -219,6 +269,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
{
|
||||
_readyLiveOwners.Remove(key);
|
||||
_liveProfiles.Remove(key);
|
||||
_initialPresentationBarriers.Remove(key);
|
||||
}
|
||||
if (record.LocalEntityId is not { } localId)
|
||||
return;
|
||||
|
|
@ -241,6 +292,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
}
|
||||
_readyLiveOwners.Clear();
|
||||
_liveProfiles.Clear();
|
||||
_initialPresentationBarriers.Clear();
|
||||
_pendingByServerGuid.Clear();
|
||||
_dirtyLiveOwners.Clear();
|
||||
_dirtyLiveOwnerOrder.Clear();
|
||||
|
|
@ -455,6 +507,11 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
|||
return false;
|
||||
}
|
||||
|
||||
private bool IsWaitingForInitialPresentation(uint serverGuid) =>
|
||||
_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||
&& record.ProjectionKey is { } key
|
||||
&& _initialPresentationBarriers.Contains(key);
|
||||
|
||||
private void OnEffectPoseChanged(uint localId)
|
||||
{
|
||||
if (_posePublishLocalId == localId)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue