Commit 19b4465's new ToPromote test pre-loaded an LB with a non- canonical id (low 16 bits 0x0FFF instead of 0xFFFF). The new canonicalization in AddEntitiesToExistingLandblock then key-missed and parked the entity in the pending bucket instead of merging — assertion failed. Use canonical id 0x3232FFFFu directly. The test now exercises the intended hot-path (merge into existing LB), not the cold pending-bucket fallback (which is exercised by GpuWorldStateTwoTierTests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
134 lines
5.2 KiB
C#
134 lines
5.2 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).
|
|
// Id must be in canonical form (low 16 bits = 0xFFFF) since
|
|
// AddEntitiesToExistingLandblock canonicalizes incoming ids.
|
|
uint lbId = 0x3232FFFFu;
|
|
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);
|
|
}
|
|
}
|