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; /// /// 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. /// public sealed class PendingSpawnIntegrationTests { // N.5 ship amendment: WbFoundationFlag was deleted — GpuWorldState // no longer gates adapter calls on the flag; they are unconditional // when the adapter is non-null. No static ctor hook needed. [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 IncrementCalls { get; } = new(); public List 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) }, }; }