test(vfx): harden live missile and effect lifetimes
Add deterministic 96-owner lifecycle and renderer-resource stress gates plus an exact twelve-cycle recall/portal ownership gate. Prove zero retained records, projectiles, spatial buckets, script queues, particle/light owners, shadows, pending effects, and mesh references after churn, GUID reuse, deletion, and session reset. Make logical teardown incarnation-specific and reentrancy-safe with lifetime epochs, atomic resource registration, generation-aware effect/teleport cleanup, projection mutation tokens, and failure-isolated visibility fan-out. Finish canonical spatial transactions before reporting observer failures and never discard superseded cleanup failures. Synchronize architecture, roadmap, milestones, retail research, divergence bookkeeping, and durable memory. All three independent review tracks are clean; Release build and the full 5,454-pass/5-skip suite are green. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
1e98d81448
commit
8d63e5c28a
21 changed files with 2704 additions and 100 deletions
|
|
@ -5044,7 +5044,7 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
() => _liveEntityPresentation?.Forget(record),
|
||||
() => _entityEffects?.OnLiveEntityUnregistered(record),
|
||||
() => _remoteTeleportController?.Forget(serverGuid),
|
||||
() => _remoteTeleportController?.Forget(record),
|
||||
};
|
||||
if (_pendingPostArrivalAction is { Guid: var pendingGuid }
|
||||
&& pendingGuid == serverGuid)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public sealed class EntityEffectController : IAnimationHookSink
|
|||
private readonly Action<uint> _ownerUnregistered;
|
||||
private readonly Action<uint, uint?> _ownerSoundTableChanged;
|
||||
private readonly Dictionary<uint, EntityEffectProfile> _profilesByLocalId = new();
|
||||
private readonly HashSet<uint> _readyServerGuids = new();
|
||||
private readonly Dictionary<uint, ushort> _readyGenerationByServerGuid = new();
|
||||
private readonly Dictionary<uint, Queue<PendingEffect>> _pendingByServerGuid = new();
|
||||
private readonly Dictionary<uint, WorldEntity> _staticOwners = new();
|
||||
private readonly HashSet<uint> _syntheticOwners = new();
|
||||
|
|
@ -125,7 +125,7 @@ public sealed class EntityEffectController : IAnimationHookSink
|
|||
return false;
|
||||
}
|
||||
|
||||
_readyServerGuids.Add(serverGuid);
|
||||
_readyGenerationByServerGuid[serverGuid] = record.Generation;
|
||||
_profilesByLocalId[entity.Id] = profile;
|
||||
_runner.SetOwnerAnchor(entity.Id, entity.Position);
|
||||
_ownerSoundTableChanged(entity.Id, profile.CurrentSoundTableDid);
|
||||
|
|
@ -197,8 +197,18 @@ public sealed class EntityEffectController : IAnimationHookSink
|
|||
public void OnLiveEntityUnregistered(LiveEntityRecord record)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
_pendingByServerGuid.Remove(record.ServerGuid);
|
||||
_readyServerGuids.Remove(record.ServerGuid);
|
||||
if (!_liveEntities.TryGetRecord(record.ServerGuid, out LiveEntityRecord current)
|
||||
|| ReferenceEquals(current, record))
|
||||
{
|
||||
_pendingByServerGuid.Remove(record.ServerGuid);
|
||||
}
|
||||
if (_readyGenerationByServerGuid.TryGetValue(
|
||||
record.ServerGuid,
|
||||
out ushort readyGeneration)
|
||||
&& readyGeneration == record.Generation)
|
||||
{
|
||||
_readyGenerationByServerGuid.Remove(record.ServerGuid);
|
||||
}
|
||||
if (record.LocalEntityId is not { } localId)
|
||||
return;
|
||||
_profilesByLocalId.Remove(localId);
|
||||
|
|
@ -212,7 +222,7 @@ public sealed class EntityEffectController : IAnimationHookSink
|
|||
|
||||
public void ClearNetworkState()
|
||||
{
|
||||
foreach (uint serverGuid in _readyServerGuids.ToArray())
|
||||
foreach (uint serverGuid in _readyGenerationByServerGuid.Keys.ToArray())
|
||||
{
|
||||
if (_liveEntities.TryGetLocalEntityId(serverGuid, out uint localId))
|
||||
{
|
||||
|
|
@ -221,14 +231,14 @@ public sealed class EntityEffectController : IAnimationHookSink
|
|||
_ownerUnregistered(localId);
|
||||
}
|
||||
}
|
||||
_readyServerGuids.Clear();
|
||||
_readyGenerationByServerGuid.Clear();
|
||||
_pendingByServerGuid.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Refreshes every live root after animation/movement.</summary>
|
||||
public void RefreshLiveOwnerPoses()
|
||||
{
|
||||
foreach (uint serverGuid in _readyServerGuids.ToArray())
|
||||
foreach (uint serverGuid in _readyGenerationByServerGuid.Keys.ToArray())
|
||||
{
|
||||
if (!TryGetReadyLocalId(serverGuid, out uint localId))
|
||||
continue;
|
||||
|
|
@ -360,7 +370,9 @@ public sealed class EntityEffectController : IAnimationHookSink
|
|||
|
||||
private bool TryGetReadyLocalId(uint serverGuid, out uint localId)
|
||||
{
|
||||
if (_readyServerGuids.Contains(serverGuid)
|
||||
if (_readyGenerationByServerGuid.TryGetValue(serverGuid, out ushort readyGeneration)
|
||||
&& _liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||
&& record.Generation == readyGeneration
|
||||
&& _liveEntities.TryGetLocalEntityId(serverGuid, out localId)
|
||||
&& _profilesByLocalId.ContainsKey(localId))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -114,6 +114,9 @@ public sealed class LiveEntityLightController : IDisposable
|
|||
|
||||
public void Refresh() => _lighting.RefreshAttachedLights();
|
||||
|
||||
internal int TrackedOwnerCount => _serverGuidByOwner.Count;
|
||||
internal int PresentedOwnerCount => _presentOwners.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Attached projection visibility can become true before its holding-part
|
||||
/// composition is published. The attachment owner calls this barrier only
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue