using System; using System.Collections.Generic; using AcDream.App.Streaming; using AcDream.Core.Terrain; using AcDream.Core.World; using DatReaderWriter.DBObjs; using Xunit; namespace AcDream.Core.Tests.Streaming; public class StreamingControllerPriorityApplyTests { private static LandblockStreamResult.Loaded LoadedOf(uint canonicalId) => new(canonicalId, LandblockStreamTier.Near, new LoadedLandblock(canonicalId, new LandBlock(), Array.Empty()), new LandblockMeshData(Array.Empty(), Array.Empty())); [Fact] public void PriorityLandblock_isApplied_evenWhenBeyondPerFrameCap() { uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180); var outbox = new Queue(new LandblockStreamResult[] { LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 0)), LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 1)), LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 2)), LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 3)), LoadedOf(priority), }); var applied = new List(); var state = new GpuWorldState(); var ctrl = new StreamingController( enqueueLoad: (_, _) => { }, enqueueUnload: _ => { }, drainCompletions: max => { var batch = new List(); while (batch.Count < max && outbox.Count > 0) batch.Add(outbox.Dequeue()); return batch; }, applyTerrain: (lb, _) => applied.Add(lb.LandblockId), state: state, nearRadius: 4, farRadius: 12) { MaxCompletionsPerFrame = 4 }; ctrl.PriorityLandblockId = priority; ctrl.Tick(169, 180); Assert.Contains(priority, applied); // priority applied THIS tick Assert.True(applied.Count <= 5); // did not blindly flush the whole outbox } [Fact] public void Deferred_nonPriority_completions_applyOnLaterFrames_withoutLoss() { // After the priority is found, the non-priority items drained past it must // still be applied (over subsequent ticks), never dropped. uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180); var outbox = new Queue(new LandblockStreamResult[] { LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 0)), LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 1)), LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 2)), LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 3)), LoadedOf(StreamingRegion.EncodeLandblockIdForTest(0, 4)), LoadedOf(priority), }); var applied = new List(); var state = new GpuWorldState(); var ctrl = new StreamingController( enqueueLoad: (_, _) => { }, enqueueUnload: _ => { }, drainCompletions: max => { var batch = new List(); while (batch.Count < max && outbox.Count > 0) batch.Add(outbox.Dequeue()); return batch; }, applyTerrain: (lb, _) => applied.Add(lb.LandblockId), state: state, nearRadius: 4, farRadius: 12) { MaxCompletionsPerFrame = 4 }; ctrl.PriorityLandblockId = priority; ctrl.Tick(169, 180); // priority + some others ctrl.PriorityLandblockId = 0u; ctrl.Tick(169, 180); // drains the deferred remainder ctrl.Tick(169, 180); Assert.Contains(priority, applied); Assert.Equal(6, applied.Count); // all six applied, none lost } [Fact] public void PriorityNeverArrives_noThrow_noLoss_noDoubleApply() { // While a PriorityLandblockId is set but the matching completion never // arrives (e.g. the load failed), the hunt moves outbox completions into // _deferredApply and they drain at the per-frame budget — same backpressure // as the normal throttle, just relocated — until the caller clears // PriorityLandblockId (the TAS MaxContinue safety net does this on timeout). uint priority = StreamingRegion.EncodeLandblockIdForTest(169, 180); uint otherId0 = StreamingRegion.EncodeLandblockIdForTest(1, 0); uint otherId1 = StreamingRegion.EncodeLandblockIdForTest(1, 1); uint otherId2 = StreamingRegion.EncodeLandblockIdForTest(1, 2); var outbox = new Queue(new LandblockStreamResult[] { LoadedOf(otherId0), LoadedOf(otherId1), LoadedOf(otherId2), }); var applied = new List(); var state = new GpuWorldState(); var ctrl = new StreamingController( enqueueLoad: (_, _) => { }, enqueueUnload: _ => { }, drainCompletions: max => { var batch = new List(); while (batch.Count < max && outbox.Count > 0) batch.Add(outbox.Dequeue()); return batch; }, applyTerrain: (lb, _) => applied.Add(lb.LandblockId), state: state, nearRadius: 4, farRadius: 12) { MaxCompletionsPerFrame = 4 }; // Set a priority id that will NEVER appear in the outbox. ctrl.PriorityLandblockId = priority; // Tick several times — should not throw and every non-priority completion // must still be applied without duplication. ctrl.Tick(169, 180); ctrl.Tick(169, 180); ctrl.Tick(169, 180); // (a) no throw — covered by reaching here without exception // (b) every non-priority completion is applied (no loss) Assert.Contains(otherId0, applied); Assert.Contains(otherId1, applied); Assert.Contains(otherId2, applied); // (c) no double-apply: applied count matches completions enqueued Assert.Equal(3, applied.Count); } }