Verifies Task 12's GpuWorldState wiring preserves the pending-spawn list mechanism: 1. Live entity parked before its landblock loads — pending count = 1, adapter not called yet. 2. Landblock arrives with its own atlas-tier entity AND drains the pending live entity. Adapter sees ONLY the atlas-tier GfxObj (server-spawned drained entity is filtered by ServerGuid != 0). 3. Live entity arriving AFTER landblock load goes straight to flat view; adapter is not re-invoked. 4. Landblock unload decrements match load increments. Three integration tests confirm the existing pending-spawn drain semantics work correctly with the new adapter, and per-instance-tier entities (server-spawned) never leak into WB's atlas pipeline. To exercise the adapter code path (which GpuWorldState gates on WbFoundationFlag.IsEnabled) without requiring the env var set before process startup, WbFoundationFlag gains an internal ForTestsOnly_ForceEnable() method and AcDream.App exposes internals to AcDream.Core.Tests via InternalsVisibleTo. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
149 lines
6.2 KiB
C#
149 lines
6.2 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.Core.Tests.Rendering.Wb;
|
|
|
|
/// <summary>
|
|
/// Integration: verifies the pending-spawn list mechanism keeps working
|
|
/// after Task 12 wired LandblockSpawnAdapter into GpuWorldState. Server-
|
|
/// spawned entities (ServerGuid != 0) park in pending → drain on
|
|
/// AddLandblock → end up in the flat view, but they are NEVER registered
|
|
/// with the WB adapter (they're per-instance tier).
|
|
///
|
|
/// The adapter SHOULD see atlas-tier entities (ServerGuid == 0) that
|
|
/// arrived in the AddLandblock's payload directly.
|
|
/// </summary>
|
|
public sealed class PendingSpawnIntegrationTests
|
|
{
|
|
/// <summary>
|
|
/// Force-enable WbFoundationFlag for this test class.
|
|
/// GpuWorldState gates its adapter calls on this static-cached flag;
|
|
/// calling the internal test hook lets us exercise the full integration
|
|
/// path without needing the env var set before process startup.
|
|
/// </summary>
|
|
static PendingSpawnIntegrationTests()
|
|
{
|
|
WbFoundationFlag.ForTestsOnly_ForceEnable();
|
|
}
|
|
|
|
[Fact]
|
|
public void LiveEntity_ParkedBeforeLandblock_DrainsButIsNotRegisteredWithAdapter()
|
|
{
|
|
var captured = new CapturingAdapterMock();
|
|
var spawnAdapter = new LandblockSpawnAdapter(captured);
|
|
var state = new GpuWorldState(spawnAdapter);
|
|
|
|
// Park a live (server-spawned) entity for landblock 0x1234FFFF BEFORE
|
|
// the landblock streams in. ServerGuid != 0 makes this per-instance-tier.
|
|
var liveEntity = MakeServerSpawned(
|
|
id: 1, serverGuid: 0xCAFE0001u, gfxObjId: 0x01000099u);
|
|
// AppendLiveEntity takes the raw cell-form id; it canonicalises internally.
|
|
state.AppendLiveEntity(0x12340011u, liveEntity);
|
|
|
|
Assert.Equal(1, state.PendingLiveEntityCount);
|
|
Assert.Empty(captured.IncrementCalls); // not registered yet — landblock not loaded
|
|
|
|
// Now landblock arrives with ONE atlas-tier entity that brings its own
|
|
// GfxObj, plus the pending live entity drains into it.
|
|
var atlasEntity = MakeAtlas(id: 2, gfxObjId: 0x01000010u);
|
|
var lb = new LoadedLandblock(
|
|
LandblockId: 0x1234FFFFu,
|
|
Heightmap: new DatReaderWriter.DBObjs.LandBlock(),
|
|
Entities: new[] { atlasEntity });
|
|
state.AddLandblock(lb);
|
|
|
|
// Pending drained.
|
|
Assert.Equal(0, state.PendingLiveEntityCount);
|
|
|
|
// Flat view contains both: the atlas one from the load + the drained pending.
|
|
var allIds = state.Entities.Select(e => e.Id).ToHashSet();
|
|
Assert.Contains(1u, allIds); // pending entity
|
|
Assert.Contains(2u, allIds); // landblock entity
|
|
|
|
// Adapter only saw the atlas-tier GfxObj. The pending server-spawned
|
|
// entity's GfxObj is NOT registered (filtered by ServerGuid != 0 in
|
|
// LandblockSpawnAdapter).
|
|
Assert.Single(captured.IncrementCalls);
|
|
Assert.Contains(0x01000010ul, captured.IncrementCalls);
|
|
Assert.DoesNotContain(0x01000099ul, captured.IncrementCalls);
|
|
}
|
|
|
|
[Fact]
|
|
public void LiveEntity_AfterLandblock_RegistersImmediatelyWithoutAdapterCall()
|
|
{
|
|
// When a CreateObject arrives for an already-loaded landblock, it goes
|
|
// straight into the flat view (not through pending). Adapter is NOT
|
|
// re-invoked because the landblock load already happened.
|
|
var captured = new CapturingAdapterMock();
|
|
var spawnAdapter = new LandblockSpawnAdapter(captured);
|
|
var state = new GpuWorldState(spawnAdapter);
|
|
|
|
var atlasEntity = MakeAtlas(id: 1, gfxObjId: 0x01000010u);
|
|
var lb = new LoadedLandblock(
|
|
LandblockId: 0x1234FFFFu,
|
|
Heightmap: new DatReaderWriter.DBObjs.LandBlock(),
|
|
Entities: new[] { atlasEntity });
|
|
state.AddLandblock(lb);
|
|
|
|
Assert.Single(captured.IncrementCalls); // atlas registered
|
|
|
|
// Now a live entity arrives — landblock is already loaded.
|
|
var liveEntity = MakeServerSpawned(id: 2, serverGuid: 0xCAFE0001u, gfxObjId: 0x01000099u);
|
|
state.AppendLiveEntity(0x12340022u, liveEntity);
|
|
|
|
// Adapter not invoked again — AppendLiveEntity doesn't drive ref counts.
|
|
Assert.Single(captured.IncrementCalls);
|
|
Assert.Equal(0, state.PendingLiveEntityCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void LandblockUnload_ReleasesAtlasIds_PendingDoesNotRegress()
|
|
{
|
|
var captured = new CapturingAdapterMock();
|
|
var spawnAdapter = new LandblockSpawnAdapter(captured);
|
|
var state = new GpuWorldState(spawnAdapter);
|
|
|
|
var atlasEntity = MakeAtlas(id: 1, gfxObjId: 0x01000010u);
|
|
var lb = new LoadedLandblock(
|
|
LandblockId: 0x1234FFFFu,
|
|
Heightmap: new DatReaderWriter.DBObjs.LandBlock(),
|
|
Entities: new[] { atlasEntity });
|
|
state.AddLandblock(lb);
|
|
state.RemoveLandblock(0x1234FFFFu);
|
|
|
|
Assert.Equal(
|
|
captured.IncrementCalls.OrderBy(x => x),
|
|
captured.DecrementCalls.OrderBy(x => x));
|
|
}
|
|
|
|
// ── 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 WorldEntity MakeAtlas(uint id, uint gfxObjId)
|
|
=> MakeEntity(id, serverGuid: 0u, gfxObjId);
|
|
|
|
private static WorldEntity MakeServerSpawned(uint id, uint serverGuid, uint gfxObjId)
|
|
=> MakeEntity(id, serverGuid, gfxObjId);
|
|
|
|
private static WorldEntity MakeEntity(uint id, uint serverGuid, uint gfxObjId)
|
|
=> new WorldEntity
|
|
{
|
|
Id = id,
|
|
ServerGuid = serverGuid,
|
|
SourceGfxObjOrSetupId = gfxObjId,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = new[] { new MeshRef(gfxObjId, Matrix4x4.Identity) },
|
|
};
|
|
}
|