using System; using AcDream.App.Streaming; using AcDream.Core.World; using DatReaderWriter.DBObjs; using Xunit; namespace AcDream.Core.Tests.Streaming; /// /// Covers 's pending-spawn behavior — the path /// that survives the race where the server delivers a CreateObject for a /// landblock that hasn't been streamed in yet. This was the bug that /// silently dropped 40+ NPCs on the first frame after live login during /// Phase A.1 visual verification. /// public class GpuWorldStateTests { private static LoadedLandblock MakeStubLandblock(uint canonicalId) => new(canonicalId, new LandBlock(), Array.Empty()); private static WorldEntity MakeStubEntity(uint id) => new() { Id = id, SourceGfxObjOrSetupId = 0x01000001u, Position = System.Numerics.Vector3.Zero, Rotation = System.Numerics.Quaternion.Identity, MeshRefs = Array.Empty(), }; [Fact] public void AppendLiveEntity_LandblockAlreadyLoaded_AppendsImmediately() { var state = new GpuWorldState(); state.AddLandblock(MakeStubLandblock(0xA9B4FFFFu)); // Server sends a spawn at landblock 0xA9B40011 — same landblock, // cell index 0x0011. Canonicalize drops the cell index, lookup // succeeds, entity lands in the loaded landblock immediately. state.AppendLiveEntity(0xA9B40011u, MakeStubEntity(42)); Assert.Single(state.Entities); Assert.Equal(0u, (uint)state.PendingLiveEntityCount); } [Fact] public void AppendLiveEntity_LandblockNotLoaded_ParksInPending() { var state = new GpuWorldState(); // No landblock loaded — the spawn must survive instead of being // silently dropped (the bug from Phase A.1's first live run). state.AppendLiveEntity(0xA9B40011u, MakeStubEntity(42)); Assert.Empty(state.Entities); // not visible yet Assert.Equal(1, state.PendingLiveEntityCount); } [Fact] public void AddLandblock_DrainsPendingEntriesForThatLandblock() { var state = new GpuWorldState(); // Three spawns arrive before the landblock loads. state.AppendLiveEntity(0xA9B40011u, MakeStubEntity(1)); state.AppendLiveEntity(0xA9B40022u, MakeStubEntity(2)); // same landblock, different cell state.AppendLiveEntity(0xA9B40033u, MakeStubEntity(3)); Assert.Equal(3, state.PendingLiveEntityCount); Assert.Empty(state.Entities); // Now the landblock streams in. state.AddLandblock(MakeStubLandblock(0xA9B4FFFFu)); // The three pending entities are now visible, and the pending // bucket for that landblock is empty. Assert.Equal(3, state.Entities.Count); Assert.Equal(0, state.PendingLiveEntityCount); } [Fact] public void AddLandblock_DoesNotDrainPendingForADifferentLandblock() { var state = new GpuWorldState(); state.AppendLiveEntity(0xA9B40011u, MakeStubEntity(1)); // pending for 0xA9B4FFFF state.AppendLiveEntity(0xAAAA0022u, MakeStubEntity(2)); // pending for 0xAAAAFFFF // Loading 0xA9B4FFFF only drains its own bucket. state.AddLandblock(MakeStubLandblock(0xA9B4FFFFu)); Assert.Single(state.Entities); Assert.Equal(1, state.PendingLiveEntityCount); // 0xAAAAFFFF entry still parked } [Fact] public void RemoveLandblock_DropsPendingForThatLandblock() { var state = new GpuWorldState(); // Spawns for landblock 0xA9B4FFFF arrive while it's pending. state.AppendLiveEntity(0xA9B40011u, MakeStubEntity(1)); state.AppendLiveEntity(0xA9B40022u, MakeStubEntity(2)); Assert.Equal(2, state.PendingLiveEntityCount); // Player moves away — the streamer says "this landblock is no // longer in the visible window, drop it." The pending entries // for it are dropped too because they came along with that // landblock and are no longer relevant. state.RemoveLandblock(0xA9B4FFFFu); Assert.Equal(0, state.PendingLiveEntityCount); Assert.Empty(state.Entities); } [Fact] public void RemoveLandblock_LoadedThenRemoved_DropsItsEntities() { var state = new GpuWorldState(); state.AddLandblock(MakeStubLandblock(0xA9B4FFFFu)); state.AppendLiveEntity(0xA9B40011u, MakeStubEntity(1)); Assert.Single(state.Entities); state.RemoveLandblock(0xA9B4FFFFu); Assert.Empty(state.Entities); } [Fact] public void IsLoaded_ReturnsTrueForLoaded_FalseForPendingOnly() { var state = new GpuWorldState(); state.AppendLiveEntity(0xA9B40011u, MakeStubEntity(1)); Assert.False(state.IsLoaded(0xA9B4FFFFu)); // pending doesn't count state.AddLandblock(MakeStubLandblock(0xA9B4FFFFu)); Assert.True(state.IsLoaded(0xA9B4FFFFu)); } }