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>
116 lines
3.8 KiB
C#
116 lines
3.8 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Wb;
|
|
|
|
public sealed class EntitySpawnAdapterLifetimeTests
|
|
{
|
|
[Fact]
|
|
public void NinetySixLiveOwners_DeleteAndGuidReuse_BalanceEveryMeshReference()
|
|
{
|
|
const int ownerCount = 96;
|
|
const int reuseCount = 32;
|
|
const uint guidBase = 0x74000000u;
|
|
var meshes = new RefCountingMeshAdapter();
|
|
var adapter = new EntitySpawnAdapter(
|
|
new NullTextureCache(),
|
|
_ => MakeSequencer(),
|
|
meshes);
|
|
|
|
for (int i = 0; i < ownerCount; i++)
|
|
{
|
|
uint guid = guidBase + (uint)i;
|
|
WorldEntity entity = MakeEntity((uint)i + 1u, guid);
|
|
entity.MeshRefs =
|
|
[
|
|
new MeshRef(0x01000001u, Matrix4x4.Identity),
|
|
new MeshRef(0x01000001u, Matrix4x4.Identity),
|
|
];
|
|
entity.ApplyAppearance(
|
|
entity.MeshRefs,
|
|
entity.PaletteOverride,
|
|
[new PartOverride(0, 0x01001000u + (uint)i)]);
|
|
Assert.NotNull(adapter.OnCreate(entity));
|
|
}
|
|
|
|
Assert.Equal(ownerCount * 2, meshes.TotalReferenceCount);
|
|
for (int i = 0; i < reuseCount; i++)
|
|
{
|
|
uint guid = guidBase + (uint)i;
|
|
adapter.OnRemove(guid);
|
|
Assert.Null(adapter.GetState(guid));
|
|
|
|
WorldEntity replacement = MakeEntity(1000u + (uint)i, guid);
|
|
replacement.ApplyAppearance(
|
|
replacement.MeshRefs,
|
|
replacement.PaletteOverride,
|
|
[new PartOverride(0, 0x01002000u + (uint)i)]);
|
|
Assert.NotNull(adapter.OnCreate(replacement));
|
|
}
|
|
|
|
for (int i = 0; i < ownerCount; i++)
|
|
{
|
|
uint guid = guidBase + (uint)i;
|
|
adapter.OnRemove(guid);
|
|
Assert.Null(adapter.GetState(guid));
|
|
}
|
|
|
|
Assert.Equal(0, meshes.TotalReferenceCount);
|
|
Assert.Empty(meshes.ReferenceCounts);
|
|
Assert.Equal(meshes.IncrementCount, meshes.DecrementCount);
|
|
}
|
|
|
|
private static WorldEntity MakeEntity(uint id, uint serverGuid) => new()
|
|
{
|
|
Id = id,
|
|
ServerGuid = serverGuid,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = [new MeshRef(0x01000001u, Matrix4x4.Identity)],
|
|
};
|
|
|
|
private static AnimationSequencer MakeSequencer() =>
|
|
new(new Setup(), new MotionTable(), new NullAnimationLoader());
|
|
|
|
private sealed class NullAnimationLoader : IAnimationLoader
|
|
{
|
|
public Animation? LoadAnimation(uint id) => null;
|
|
}
|
|
|
|
private sealed class NullTextureCache : ITextureCachePerInstance
|
|
{
|
|
public uint GetOrUploadWithPaletteOverride(
|
|
uint surfaceId,
|
|
uint? overrideOrigTextureId,
|
|
PaletteOverride paletteOverride) => 1u;
|
|
}
|
|
|
|
private sealed class RefCountingMeshAdapter : IWbMeshAdapter
|
|
{
|
|
public Dictionary<ulong, int> ReferenceCounts { get; } = new();
|
|
public int IncrementCount { get; private set; }
|
|
public int DecrementCount { get; private set; }
|
|
public int TotalReferenceCount => ReferenceCounts.Values.Sum();
|
|
|
|
public void IncrementRefCount(ulong id)
|
|
{
|
|
ReferenceCounts[id] = ReferenceCounts.GetValueOrDefault(id) + 1;
|
|
IncrementCount++;
|
|
}
|
|
|
|
public void DecrementRefCount(ulong id)
|
|
{
|
|
Assert.True(ReferenceCounts.TryGetValue(id, out int count));
|
|
Assert.True(count > 0);
|
|
if (count == 1)
|
|
ReferenceCounts.Remove(id);
|
|
else
|
|
ReferenceCounts[id] = count - 1;
|
|
DecrementCount++;
|
|
}
|
|
}
|
|
}
|