acdream/tests/AcDream.Core.Tests/Streaming/StreamingControllerTests.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

115 lines
4.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using AcDream.App.Streaming;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
using Xunit;
namespace AcDream.Core.Tests.Streaming;
public class StreamingControllerTests
{
private sealed class FakeStreamer
{
public List<uint> Loads { get; } = new();
public List<uint> Unloads { get; } = new();
public Queue<LandblockStreamResult> Pending { get; } = new();
public void EnqueueLoad(uint id, LandblockStreamJobKind _) => Loads.Add(id);
public void EnqueueUnload(uint id) => Unloads.Add(id);
public IReadOnlyList<LandblockStreamResult> DrainCompletions(int max)
{
var batch = new List<LandblockStreamResult>();
while (batch.Count < max && Pending.Count > 0)
batch.Add(Pending.Dequeue());
return batch;
}
}
[Fact]
public void FirstTick_EnqueuesWholeVisibleWindow()
{
var state = new GpuWorldState();
var fake = new FakeStreamer();
var controller = new StreamingController(
enqueueLoad: fake.EnqueueLoad,
enqueueUnload: fake.EnqueueUnload,
drainCompletions: fake.DrainCompletions,
applyTerrain: (_, _) => { },
state: state,
nearRadius: 2,
farRadius: 2);
// Center at (50, 50); no landblocks loaded yet.
controller.Tick(observerCx: 50, observerCy: 50);
// 5×5 window = 25 loads enqueued (nearRadius==farRadius so all go to ToLoadNear), 0 unloads.
Assert.Equal(25, fake.Loads.Count);
Assert.Empty(fake.Unloads);
}
[Fact]
public void SecondTick_SamePosition_EnqueuesNothing()
{
var state = new GpuWorldState();
var fake = new FakeStreamer();
var controller = new StreamingController(
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
(_, _) => { }, state, nearRadius: 2, farRadius: 2);
controller.Tick(50, 50);
fake.Loads.Clear();
controller.Tick(50, 50);
Assert.Empty(fake.Loads);
Assert.Empty(fake.Unloads);
}
[Fact]
public void DrainingLoadedResult_AddsToState()
{
var state = new GpuWorldState();
var fake = new FakeStreamer();
var applied = new List<LoadedLandblock>();
var controller = new StreamingController(
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
(build, _) => applied.Add(build.Landblock), state, nearRadius: 2, farRadius: 2);
// Note: LoadedLandblock's actual fields are LandblockId, Heightmap,
// Entities (positional record). Adjust if the first positional arg
// name differs.
const uint landblockId = 0x3232FFFFu;
var lb = new LoadedLandblock(landblockId, new LandBlock(), System.Array.Empty<WorldEntity>());
// A.5 T10-T12 follow-up: use a real empty mesh instance instead of
// default! so any future test that flows MeshData through the apply
// callback gets a non-null reference to inspect rather than an NRE.
var stubMesh = new AcDream.Core.Terrain.LandblockMeshData(
System.Array.Empty<AcDream.Core.Terrain.TerrainVertex>(),
System.Array.Empty<uint>());
fake.Pending.Enqueue(new LandblockStreamResult.Loaded(landblockId, LandblockStreamTier.Near, lb, stubMesh));
controller.Tick(50, 50);
Assert.Single(applied);
Assert.True(state.IsLoaded(landblockId));
}
[Fact]
public void DrainingUnloadedResult_RemovesFromState()
{
var state = new GpuWorldState();
var fake = new FakeStreamer();
var controller = new StreamingController(
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
(_, _) => { }, state, nearRadius: 2, farRadius: 2);
const uint landblockId = 0x3232FFFFu;
var lb = new LoadedLandblock(landblockId, new LandBlock(), System.Array.Empty<WorldEntity>());
state.AddLandblock(lb);
fake.Pending.Enqueue(new LandblockStreamResult.Unloaded(landblockId));
controller.Tick(50, 50);
Assert.False(state.IsLoaded(landblockId));
}
}