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
|
|
@ -199,6 +199,10 @@ public sealed class GpuWorldState
|
|||
/// pending path is doing its job.
|
||||
/// </summary>
|
||||
public int PendingLiveEntityCount => _pendingByLandblock.Values.Sum(list => list.Count);
|
||||
public int PendingBucketCount => _pendingByLandblock.Count;
|
||||
public int PendingRescueCount => _persistentRescued.Count;
|
||||
public int PersistentGuidCount => _persistentGuids.Count;
|
||||
public int PendingVisibilityTransitionCount => _visibilityTransitions.Count;
|
||||
|
||||
public void AddLandblock(LoadedLandblock landblock)
|
||||
{
|
||||
|
|
@ -449,12 +453,84 @@ public sealed class GpuWorldState
|
|||
}
|
||||
|
||||
// Scrub pending buckets too so rebucketing cannot leave a second slot.
|
||||
foreach (var kvp in _pendingByLandblock)
|
||||
kvp.Value.RemoveAll(e => e.ServerGuid == serverGuid);
|
||||
foreach (uint landblockId in _pendingByLandblock.Keys.ToArray())
|
||||
{
|
||||
List<WorldEntity> bucket = _pendingByLandblock[landblockId];
|
||||
bucket.RemoveAll(e => e.ServerGuid == serverGuid);
|
||||
if (bucket.Count == 0)
|
||||
_pendingByLandblock.Remove(landblockId);
|
||||
}
|
||||
|
||||
// A persistent projection may have been rescued by RemoveLandblock
|
||||
// and not yet drained by the next GameWindow frame. Rebucketing or
|
||||
// logical teardown before that drain must remove the stale rescue
|
||||
// reference or it can later re-inject a deleted/duplicated object.
|
||||
_persistentRescued.RemoveAll(e => e.ServerGuid == serverGuid);
|
||||
|
||||
if (rebuiltLoaded) RebuildFlatView();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes one exact live projection incarnation. Unlike the GUID overload,
|
||||
/// this cannot detach a replacement that reused the same server GUID from a
|
||||
/// re-entrant logical teardown callback.
|
||||
/// </summary>
|
||||
public void RemoveLiveEntityProjection(WorldEntity entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
if (entity.ServerGuid == 0) return;
|
||||
|
||||
bool rebuiltLoaded = false;
|
||||
foreach (var kvp in _loaded.ToArray())
|
||||
{
|
||||
IReadOnlyList<WorldEntity> entities = kvp.Value.Entities;
|
||||
if (!entities.Any(candidate => ReferenceEquals(candidate, entity)))
|
||||
continue;
|
||||
|
||||
_loaded[kvp.Key] = new LoadedLandblock(
|
||||
kvp.Value.LandblockId,
|
||||
kvp.Value.Heightmap,
|
||||
entities.Where(candidate => !ReferenceEquals(candidate, entity)).ToArray());
|
||||
rebuiltLoaded = true;
|
||||
}
|
||||
|
||||
foreach (uint landblockId in _pendingByLandblock.Keys.ToArray())
|
||||
{
|
||||
List<WorldEntity> bucket = _pendingByLandblock[landblockId];
|
||||
bucket.RemoveAll(candidate => ReferenceEquals(candidate, entity));
|
||||
if (bucket.Count == 0)
|
||||
_pendingByLandblock.Remove(landblockId);
|
||||
}
|
||||
|
||||
_persistentRescued.RemoveAll(candidate => ReferenceEquals(candidate, entity));
|
||||
|
||||
if (rebuiltLoaded)
|
||||
RebuildFlatView();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends all spatial lifetime retained for one server object. Temporary
|
||||
/// rebucketing uses <see cref="RemoveLiveEntityProjection"/> and keeps the
|
||||
/// persistence classification; logical delete also forgets that class so
|
||||
/// a later session/GUID reuse cannot be rescued as the old player.
|
||||
/// </summary>
|
||||
public void ForgetLiveEntity(uint serverGuid)
|
||||
{
|
||||
RemoveLiveEntityProjection(serverGuid);
|
||||
_persistentGuids.Remove(serverGuid);
|
||||
_persistentInFlatProbe.Remove(serverGuid);
|
||||
}
|
||||
|
||||
/// <summary>Clears session-scoped persistence and undrained rescues.</summary>
|
||||
public void ClearLiveEntityLifetimeState()
|
||||
{
|
||||
_persistentGuids.Clear();
|
||||
_persistentRescued.Clear();
|
||||
_persistentInFlatProbe.Clear();
|
||||
_visibilityTransitions.Clear();
|
||||
_visibleLiveGuids.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Place an already-registered live entity in a landblock slot whose
|
||||
/// terrain may or may not be loaded yet.
|
||||
|
|
@ -656,20 +732,41 @@ public sealed class GpuWorldState
|
|||
if (_dispatchingVisibilityTransitions)
|
||||
return;
|
||||
|
||||
List<Exception>? failures = null;
|
||||
_dispatchingVisibilityTransitions = true;
|
||||
try
|
||||
{
|
||||
while (_visibilityTransitions.TryDequeue(out var transition))
|
||||
{
|
||||
LiveProjectionVisibilityChanged?.Invoke(
|
||||
transition.ServerGuid,
|
||||
transition.Visible);
|
||||
Delegate[] subscribers = LiveProjectionVisibilityChanged?
|
||||
.GetInvocationList()
|
||||
?? Array.Empty<Delegate>();
|
||||
for (int i = 0; i < subscribers.Length; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
((Action<uint, bool>)subscribers[i])(
|
||||
transition.ServerGuid,
|
||||
transition.Visible);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= new List<Exception>()).Add(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dispatchingVisibilityTransitions = false;
|
||||
}
|
||||
|
||||
if (failures is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"One or more live projection visibility observers failed.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
|
||||
// TEMP (#138-B): persistent guids currently present in the drawn flat
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue