acdream/tests/AcDream.Core.Tests/Rendering/Wb/LandblockSpawnAdapterTests.cs
Erik 669768d9da phase(N.4) Task 11: LandblockSpawnAdapter (atlas-tier ref-count bridge)
Bridges LoadedLandblock load/unload events to IWbMeshAdapter ref counts.
Tier-aware by design: walks WorldEntity collection filtered by
ServerGuid == 0 (procedural / atlas-tier only). Server-spawned entities
are skipped — those will go through EntitySpawnAdapter (Task 17).

Per-landblock id-set snapshot ensures unload pairs 1:1 with load even
when underlying data is released. Duplicate-load idempotency for
defensive resilience to streaming-controller bugs.

Six tests: registers per unique id; dedups across entities; skips
server-spawned; unload matches load; unknown landblock no-ops;
duplicate load no-ops.

Wiring into GpuWorldState lands in Task 12.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 13:53:38 +02:00

158 lines
5.6 KiB
C#

using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering.Wb;
using AcDream.Core.World;
namespace AcDream.Core.Tests.Rendering.Wb;
public sealed class LandblockSpawnAdapterTests
{
[Fact]
public void OnLandblockLoaded_RegistersIncrementForEachUniqueAtlasGfxObj()
{
var captured = new CapturingAdapterMock();
var adapter = new LandblockSpawnAdapter(captured);
// Two procedural (ServerGuid=0) entities with different GfxObj ids.
var lb = MakeLandblock(landblockId: 0x12340000u, entities: new[]
{
MakeAtlasEntity(id: 1, gfxObjIds: new[] { 0x01000010u, 0x01000020u }),
MakeAtlasEntity(id: 2, gfxObjIds: new[] { 0x01000030u }),
});
adapter.OnLandblockLoaded(lb);
// Three unique ids registered.
Assert.Equal(3, captured.IncrementCalls.Count);
Assert.Contains(0x01000010ul, captured.IncrementCalls);
Assert.Contains(0x01000020ul, captured.IncrementCalls);
Assert.Contains(0x01000030ul, captured.IncrementCalls);
}
[Fact]
public void OnLandblockLoaded_DedupsSharedIdsAcrossEntities()
{
var captured = new CapturingAdapterMock();
var adapter = new LandblockSpawnAdapter(captured);
var lb = MakeLandblock(landblockId: 0x12340000u, entities: new[]
{
MakeAtlasEntity(id: 1, gfxObjIds: new[] { 0x01000010u, 0x01000020u }),
MakeAtlasEntity(id: 2, gfxObjIds: new[] { 0x01000010u, 0x01000020u }),
});
adapter.OnLandblockLoaded(lb);
// Two unique ids despite two entities sharing both.
Assert.Equal(2, captured.IncrementCalls.Count);
}
[Fact]
public void OnLandblockLoaded_SkipsServerSpawnedEntities()
{
var captured = new CapturingAdapterMock();
var adapter = new LandblockSpawnAdapter(captured);
var lb = MakeLandblock(landblockId: 0x12340000u, entities: new[]
{
MakeAtlasEntity(id: 1, gfxObjIds: new[] { 0x01000010u }),
// ServerGuid != 0 → per-instance tier → must NOT register.
MakePerInstanceEntity(id: 2, serverGuid: 0xCAFE0001u, gfxObjIds: new[] { 0x01000020u }),
});
adapter.OnLandblockLoaded(lb);
// Only the atlas-tier entity's GfxObj is registered.
Assert.Single(captured.IncrementCalls);
Assert.Contains(0x01000010ul, captured.IncrementCalls);
Assert.DoesNotContain(0x01000020ul, captured.IncrementCalls);
}
[Fact]
public void OnLandblockUnloaded_RegistersMatchingDecrements()
{
var captured = new CapturingAdapterMock();
var adapter = new LandblockSpawnAdapter(captured);
var lb = MakeLandblock(landblockId: 0x12340000u, entities: new[]
{
MakeAtlasEntity(id: 1, gfxObjIds: new[] { 0x01000010u, 0x01000020u }),
});
adapter.OnLandblockLoaded(lb);
adapter.OnLandblockUnloaded(0x12340000u);
Assert.Equal(captured.IncrementCalls.OrderBy(x => x), captured.DecrementCalls.OrderBy(x => x));
}
[Fact]
public void OnLandblockUnloaded_UnknownLandblock_NoOps()
{
var captured = new CapturingAdapterMock();
var adapter = new LandblockSpawnAdapter(captured);
adapter.OnLandblockUnloaded(0xDEADBEEFu);
Assert.Empty(captured.DecrementCalls);
}
[Fact]
public void OnLandblockLoaded_SameLandblockTwice_DedupesAtTheLandblockLevel()
{
// If a landblock load fires twice (e.g. a streaming-controller bug),
// we should not double-register. Second load is treated as a no-op
// for ref-counting purposes.
var captured = new CapturingAdapterMock();
var adapter = new LandblockSpawnAdapter(captured);
var lb = MakeLandblock(landblockId: 0x12340000u, entities: new[]
{
MakeAtlasEntity(id: 1, gfxObjIds: new[] { 0x01000010u }),
});
adapter.OnLandblockLoaded(lb);
adapter.OnLandblockLoaded(lb);
// One unique id, one increment — not two.
Assert.Single(captured.IncrementCalls);
}
// ── Test helpers ──────────────────────────────────────────────────────
private sealed class CapturingAdapterMock : IWbMeshAdapter
{
public List<ulong> IncrementCalls { get; } = new();
public List<ulong> DecrementCalls { get; } = new();
public void IncrementRefCount(ulong id) => IncrementCalls.Add(id);
public void DecrementRefCount(ulong id) => DecrementCalls.Add(id);
}
private static LoadedLandblock MakeLandblock(uint landblockId, WorldEntity[] entities)
=> new LoadedLandblock(
LandblockId: landblockId,
Heightmap: new DatReaderWriter.DBObjs.LandBlock(), // empty default
Entities: entities);
private static WorldEntity MakeAtlasEntity(uint id, uint[] gfxObjIds)
=> MakeEntity(id, serverGuid: 0u, gfxObjIds);
private static WorldEntity MakePerInstanceEntity(uint id, uint serverGuid, uint[] gfxObjIds)
=> MakeEntity(id, serverGuid, gfxObjIds);
private static WorldEntity MakeEntity(uint id, uint serverGuid, uint[] gfxObjIds)
{
var meshRefs = gfxObjIds
.Select(g => new MeshRef(g, Matrix4x4.Identity))
.ToList();
return new WorldEntity
{
Id = id,
ServerGuid = serverGuid,
SourceGfxObjOrSetupId = gfxObjIds.FirstOrDefault(),
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = meshRefs,
};
}
}