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:
Erik 2026-08-03 12:10:21 +02:00
parent 1fc529cdcb
commit f24532adf3
11 changed files with 417 additions and 50 deletions

View file

@ -53,6 +53,9 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
private readonly Func<WorldEntity, bool> _isResident;
private readonly Action<WorldEntity, PhysicsBody> _commitLiveRoot;
private readonly Func<WorldEntity, ulong> _residencyVersion;
private readonly Func<WorldEntity,
(LiveEntityAnimationState Animation, PhysicsBody Body)?>?
_resolveLiveOwner;
private readonly Dictionary<uint, Owner> _owners = new();
private readonly List<Owner> _snapshot = new();
private readonly List<Owner> _hookSnapshot = new();
@ -63,7 +66,10 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
Action<WorldEntity, IReadOnlyList<Matrix4x4>, IReadOnlyList<bool>> publishPartPoses,
Func<WorldEntity, bool>? isResident = null,
Action<WorldEntity, PhysicsBody>? commitLiveRoot = null,
Func<WorldEntity, ulong>? residencyVersion = null)
Func<WorldEntity, ulong>? residencyVersion = null,
Func<WorldEntity,
(LiveEntityAnimationState Animation, PhysicsBody Body)?>?
resolveLiveOwner = null)
{
_animationLoader = animationLoader
?? throw new ArgumentNullException(nameof(animationLoader));
@ -74,6 +80,7 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
_isResident = isResident ?? (_ => true);
_commitLiveRoot = commitLiveRoot ?? ((_, _) => { });
_residencyVersion = residencyVersion ?? (_ => 0UL);
_resolveLiveOwner = resolveLiveOwner;
}
internal int Count => _owners.Count;
@ -363,12 +370,30 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
foreach (Owner owner in _snapshot)
{
if (!_owners.TryGetValue(owner.Entity.Id, out Owner? current)
|| !ReferenceEquals(current, owner)
|| owner.Sequencer is not { } sequencer)
|| !ReferenceEquals(current, owner))
{
continue;
}
// C3c: live Static objects are registered while their visual
// sidecar is hydrated, before Runtime has completed the initial
// CreateObject SetPosition transaction. The first-entry
// conductor owns construction of the canonical CPhysicsObj.
// Resolve and bind that already-placed owner lazily; constructing
// a second/eager body here makes the conductor reject authority
// and leaves portals, doors, and other animated statics cell-less.
if (owner.Sequencer is null
&& owner.Entity.ServerGuid != 0
&& _resolveLiveOwner?.Invoke(owner.Entity) is { } binding)
{
_ = BindLiveOwner(
owner.Entity,
binding.Animation,
binding.Body);
}
if (owner.Sequencer is not { } sequencer)
continue;
owner.ElapsedSinceUpdate += elapsedSeconds;
if (!_isResident(owner.Entity))
{