acdream/tests/AcDream.Core.Tests/Rendering/Wb/PendingSpawnIntegrationTests.cs
Erik 8dd996053d refactor(world): separate live lifetime from spatial buckets
Introduce LiveEntityRuntime as the canonical owner of each accepted server-object incarnation, stable local identity, timestamped state, parent relations, runtime components, and exactly-once teardown. Split logical registration from rebucketing so pending landblocks, equipment attachment, pickup re-entry, and GUID replacement reuse the same entity and effect owners.

Keep canonical materialized and visible target/radar views distinct, preserve retail leave_world versus exit_world semantics, gate root simulation while cell-less, and track transitional pre-Create F754 owners through delete and session reset. Remove stale-spawn rehydration and make GpuWorldState spatial-only for live objects.

Add lifecycle, generation, pending, unload, attachment, event-publication, local-ID, rollback, and effect-cleanup coverage; update architecture, milestones, memory, and the divergence register.

Co-Authored-By: Codex <noreply@openai.com>
2026-07-14 07:47:03 +02:00

142 lines
6 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
{
// 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);
// Projection placement takes the raw cell-form id; it canonicalises internally.
state.PlaceLiveEntityProjection(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.PlaceLiveEntityProjection(0x12340022u, liveEntity);
// Adapter not invoked again — projection placement 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) },
};
}