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:
Erik 2026-07-14 15:47:00 +02:00
parent 1e98d81448
commit 8d63e5c28a
21 changed files with 2704 additions and 100 deletions

View file

@ -0,0 +1,139 @@
using System.Numerics;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.Streaming;
public sealed class GpuWorldStateVisibilityTests
{
[Fact]
public void ThrowingObserver_DoesNotStrandLaterSubscribersOrQueuedOwnerEdges()
{
const uint landblock = 0x0101FFFFu;
WorldEntity first = Entity(1u, 0x70000001u);
WorldEntity second = Entity(2u, 0x70000002u);
var state = new GpuWorldState();
state.PlaceLiveEntityProjection(landblock, first);
state.PlaceLiveEntityProjection(landblock, second);
var observed = new List<(uint Guid, bool Visible)>();
state.LiveProjectionVisibilityChanged += (_, _) =>
throw new InvalidOperationException("fixture observer failure");
state.LiveProjectionVisibilityChanged += (guid, visible) =>
observed.Add((guid, visible));
AggregateException error = Assert.Throws<AggregateException>(() =>
state.AddLandblock(new LoadedLandblock(
landblock,
new LandBlock(),
Array.Empty<WorldEntity>())));
Assert.Equal(2, error.InnerExceptions.Count);
Assert.Equal(
[(first.ServerGuid, true), (second.ServerGuid, true)],
observed.OrderBy(edge => edge.Guid).ToArray());
Assert.True(state.IsLiveEntityVisible(first.ServerGuid));
Assert.True(state.IsLiveEntityVisible(second.ServerGuid));
Assert.Equal(0, state.PendingVisibilityTransitionCount);
Assert.Equal(2, state.Entities.Count);
}
[Fact]
public void ThrowingRuntimeObserver_DoesNotSkipOtherOwnersOrPresentationSubscribers()
{
const uint landblock = 0x0101FFFFu;
const uint cell = 0x01010001u;
var state = new GpuWorldState();
var runtime = new LiveEntityRuntime(
state,
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
WorldSession.EntitySpawn first = Spawn(0x70000011u, cell);
WorldSession.EntitySpawn second = Spawn(0x70000012u, cell);
runtime.RegisterLiveEntity(first);
runtime.RegisterLiveEntity(second);
runtime.MaterializeLiveEntity(first.Guid, cell, id => Entity(id, first.Guid));
runtime.MaterializeLiveEntity(second.Guid, cell, id => Entity(id, second.Guid));
var observed = new List<(uint Guid, bool Visible)>();
runtime.ProjectionVisibilityChanged += (_, _) =>
throw new InvalidOperationException("fixture presentation failure");
runtime.ProjectionVisibilityChanged += (record, visible) =>
observed.Add((record.ServerGuid, visible));
AggregateException error = Assert.Throws<AggregateException>(() =>
state.AddLandblock(new LoadedLandblock(
landblock,
new LandBlock(),
Array.Empty<WorldEntity>())));
Assert.Equal(2, error.Flatten().InnerExceptions.Count);
Assert.Equal(
[(first.Guid, true), (second.Guid, true)],
observed.OrderBy(edge => edge.Guid).ToArray());
Assert.True(runtime.TryGetRecord(first.Guid, out LiveEntityRecord firstRecord));
Assert.True(runtime.TryGetRecord(second.Guid, out LiveEntityRecord secondRecord));
Assert.True(firstRecord.IsSpatiallyVisible);
Assert.True(secondRecord.IsSpatiallyVisible);
Assert.Equal(0, state.PendingVisibilityTransitionCount);
}
private static WorldEntity Entity(uint id, uint guid) => new()
{
Id = id,
ServerGuid = guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
};
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell)
{
var position = new CreateObject.ServerPosition(
cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1);
var physics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.ReportCollisions,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: null,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
position,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: null,
Name: "visibility fixture",
ItemType: null,
MotionState: null,
MotionTableId: null,
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
}