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));
}
}