acdream/tests/AcDream.Core.Tests/Streaming/StreamingControllerTwoTierTests.cs
Erik c2c8a532db fix(A.5 T13-T16): WorldEntity required-member fields in new tests
Commit 19b4465 broke build by omitting required-member init for
SourceGfxObjOrSetupId/Position/Rotation in the new ToDemote/ToPromote
tests. WorldEntity has [required] on those fields (CS9035). The lone
test run that reported 38 passing used pre-existing binaries built
before this break.

Added all three required initializers (zero / Identity defaults — these
test the routing path; entity content doesn't matter).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 08:09:10 +02:00

132 lines
5.1 KiB
C#

using System.Collections.Generic;
using AcDream.App.Streaming;
using AcDream.Core.Terrain;
using AcDream.Core.World;
using Xunit;
namespace AcDream.Core.Tests.Streaming;
public class StreamingControllerTwoTierTests
{
[Fact]
public void Tick_FirstCall_EnqueuesNearAndFarLoadsByTier()
{
var loads = new List<(uint Id, LandblockStreamJobKind Kind)>();
var unloads = new List<uint>();
var state = new GpuWorldState();
var ctrl = new StreamingController(
enqueueLoad: (id, kind) => loads.Add((id, kind)),
enqueueUnload: unloads.Add,
drainCompletions: _ => System.Array.Empty<LandblockStreamResult>(),
applyTerrain: (_, _) => { },
state: state,
nearRadius: 1,
farRadius: 3);
ctrl.Tick(observerCx: 100, observerCy: 100);
int nearCount = 0, farCount = 0;
foreach (var (_, kind) in loads)
{
if (kind == LandblockStreamJobKind.LoadNear) nearCount++;
else if (kind == LandblockStreamJobKind.LoadFar) farCount++;
}
Assert.Equal(9, nearCount); // 3x3 inner ring (radius=1)
Assert.Equal(40, farCount); // 7x7 - 3x3 outer ring (radius=3)
}
[Fact]
public void Tick_PlayerWalksOutOfNear_ToDemoteRoutesToRemoveEntities()
{
// Setup: bootstrap region at (100,100) with near=1, far=3.
// The bootstrap puts LB (100,100) in the near tier.
// Walking 4+ east drops LB (100,100) past the near-hysteresis
// threshold (NearRadius+2 = 3); ToDemote should fire.
var loads = new List<(uint, LandblockStreamJobKind)>();
var unloads = new List<uint>();
var state = new GpuWorldState();
// Pre-load LB (100,100) so RemoveEntitiesFromLandblock has something
// to find. The actual entity content doesn't matter for routing.
var lb100 = new LoadedLandblock(
(100u << 24) | (100u << 16) | 0xFFFFu,
Heightmap: null!,
Entities: new[] { new WorldEntity {
Id = 1, SourceGfxObjOrSetupId = 0,
Position = System.Numerics.Vector3.Zero,
Rotation = System.Numerics.Quaternion.Identity,
MeshRefs = System.Array.Empty<MeshRef>() } });
state.AddLandblock(lb100);
Assert.Equal(1, state.Entities.Count);
var ctrl = new StreamingController(
enqueueLoad: (id, kind) => loads.Add((id, kind)),
enqueueUnload: unloads.Add,
drainCompletions: _ => System.Array.Empty<LandblockStreamResult>(),
applyTerrain: (_, _) => { },
state: state,
nearRadius: 1,
farRadius: 3);
ctrl.Tick(observerCx: 100, observerCy: 100); // bootstrap
loads.Clear();
// Walk 4 east — LB (100,100) is now Chebyshev distance 4 from new
// center (104,100). NearRadius+2 = 3, so 4 > 3 fires the demote.
ctrl.Tick(observerCx: 104, observerCy: 100);
// ToDemote runs synchronously on the render thread (no enqueue).
// The visible effect is RemoveEntitiesFromLandblock dropping the entity.
Assert.Empty(state.Entities);
// Terrain stays loaded (demote != unload).
Assert.True(state.IsLoaded((100u << 24) | (100u << 16) | 0xFFFFu));
}
[Fact]
public void Tick_DrainingPromoted_RoutesToAddEntitiesToExisting()
{
var loads = new List<(uint, LandblockStreamJobKind)>();
var unloads = new List<uint>();
var state = new GpuWorldState();
// Pre-load a far-tier-style LB record (terrain only, no entities).
uint lbId = 0x32320FFFu;
var lb = new LoadedLandblock(lbId, Heightmap: null!, Entities: System.Array.Empty<WorldEntity>());
state.AddLandblock(lb);
Assert.Empty(state.Entities);
// Streamer pushes a Promoted result carrying the entity layer.
var promoted = new LandblockStreamResult.Promoted(
lbId,
new[] { new WorldEntity {
Id = 7, SourceGfxObjOrSetupId = 0,
Position = System.Numerics.Vector3.Zero,
Rotation = System.Numerics.Quaternion.Identity,
MeshRefs = System.Array.Empty<MeshRef>() } });
var queue = new Queue<LandblockStreamResult>();
queue.Enqueue(promoted);
var ctrl = new StreamingController(
enqueueLoad: (id, kind) => loads.Add((id, kind)),
enqueueUnload: unloads.Add,
drainCompletions: max =>
{
var batch = new List<LandblockStreamResult>();
while (batch.Count < max && queue.Count > 0) batch.Add(queue.Dequeue());
return batch;
},
applyTerrain: (_, _) => { },
state: state,
nearRadius: 2,
farRadius: 2);
ctrl.Tick(50, 50); // drains the Promoted result
// Promoted routes to AddEntitiesToExistingLandblock — the entity is now
// merged into the existing LB record.
Assert.Equal(1, state.Entities.Count);
Assert.Equal(7u, state.Entities[0].Id);
}
}