using System.Numerics; using AcDream.App.Streaming; using AcDream.Core.Terrain; using AcDream.Core.World; using DatReaderWriter.DBObjs; using Xunit; namespace AcDream.App.Tests.Streaming; public sealed class LandblockRetirementCoordinatorTests { [Fact] public void EntityStageFailure_DetachesImmediately_AndResumesAtFailedEntity() { const uint landblockId = 0x2020FFFFu; var state = new GpuWorldState(); state.AddLandblock(new LoadedLandblock( landblockId, new LandBlock(), new[] { Entity(1), Entity(2), Entity(3) })); var calls = new Dictionary(); bool failEntityTwo = true; var coordinator = new LandblockRetirementCoordinator( state, ticket => CompletePresentation( ticket, lighting: entity => { calls[entity.Id] = calls.GetValueOrDefault(entity.Id) + 1; if (entity.Id == 2 && failEntityTwo) { failEntityTwo = false; throw new InvalidOperationException("injected owner failure"); } })); coordinator.BeginFull(landblockId); Assert.False(state.IsLoaded(landblockId)); Assert.Equal(1, coordinator.PendingCount); Assert.Equal(1, calls[1]); Assert.Equal(1, calls[2]); Assert.False(calls.ContainsKey(3)); coordinator.Advance(); Assert.Equal(0, coordinator.PendingCount); Assert.Equal(1, calls[1]); Assert.Equal(2, calls[2]); Assert.Equal(1, calls[3]); } [Fact] public void ForceReload_RetiresEveryLoadedId_WhenOneOwnerRemainsPending() { uint[] ids = [0x2020FFFFu, 0x2121FFFFu, 0x2222FFFFu]; var state = new GpuWorldState(); foreach (uint id in ids) state.AddLandblock(new LoadedLandblock(id, new LandBlock(), Array.Empty())); bool failOnce = true; var coordinator = new LandblockRetirementCoordinator( state, ticket => CompletePresentation( ticket, terrain: () => { if (ticket.LandblockId == ids[1] && failOnce) { failOnce = false; throw new InvalidOperationException("injected terrain failure"); } })); var controller = Controller(state, coordinator); controller.ForceReloadWindow(); foreach (uint id in ids) Assert.False(state.IsLoaded(id)); Assert.Equal(1, coordinator.PendingCount); coordinator.Advance(); Assert.Equal(0, coordinator.PendingCount); } [Fact] public void ReplacementPublication_WaitsForSameIdRetirementFence() { const uint landblockId = 0x3232FFFFu; var state = new GpuWorldState(); state.AddLandblock(new LoadedLandblock( landblockId, new LandBlock(), Array.Empty())); int terrainAttempts = 0; var coordinator = new LandblockRetirementCoordinator( state, ticket => CompletePresentation( ticket, terrain: () => { terrainAttempts++; if (terrainAttempts <= 2) throw new InvalidOperationException("injected terrain failure"); })); var pending = new Queue(); int applyCount = 0; var controller = Controller( state, coordinator, drain: max => Drain(pending, max), apply: (_, _) => applyCount++); controller.ForceReloadWindow(); // generation 1, first retirement attempt var replacement = new LoadedLandblock( landblockId, new LandBlock(), Array.Empty()); pending.Enqueue(new LandblockStreamResult.Loaded( landblockId, LandblockStreamTier.Near, replacement, EmptyMesh(), generation: 1)); controller.Tick(0x32, 0x32); // second failure; publication stays deferred Assert.Equal(0, applyCount); Assert.False(state.IsLoaded(landblockId)); Assert.True(coordinator.IsPending(landblockId)); controller.Tick(0x32, 0x32); // third attempt completes, then publishes once Assert.Equal(1, applyCount); Assert.True(state.IsLoaded(landblockId)); Assert.False(coordinator.IsPending(landblockId)); } private static StreamingController Controller( GpuWorldState state, LandblockRetirementCoordinator coordinator, Func>? drain = null, Action? apply = null) => new( enqueueLoad: (_, _, _) => { }, enqueueUnload: (_, _) => { }, drainCompletions: drain ?? (_ => Array.Empty()), applyTerrain: apply ?? ((_, _) => { }), state: state, nearRadius: 0, farRadius: 0, retirementCoordinator: coordinator); private static IReadOnlyList Drain( Queue pending, int max) { var result = new List(max); while (result.Count < max && pending.TryDequeue(out LandblockStreamResult? item)) result.Add(item); return result; } private static void CompletePresentation( LandblockRetirementTicket ticket, Action? lighting = null, Action? terrain = null) { ticket.RunForEachEntity( LandblockRetirementStage.EntityLighting, static _ => true, lighting ?? (_ => { })); ticket.RunForEachEntity( LandblockRetirementStage.EntityTranslucency, static _ => true, static _ => { }); ticket.RunForEachEntity( LandblockRetirementStage.PluginProjection, static entity => entity.ServerGuid == 0, static _ => { }); if (ticket.Kind == LandblockRetirementKind.Full) ticket.RunOnce(LandblockRetirementStage.Terrain, terrain ?? (() => { })); ticket.RunOnce(LandblockRetirementStage.Physics, static () => { }); ticket.RunOnce(LandblockRetirementStage.CellVisibility, static () => { }); ticket.RunOnce(LandblockRetirementStage.BuildingRegistry, static () => { }); ticket.RunOnce(LandblockRetirementStage.EnvironmentCells, static () => { }); } private static WorldEntity Entity(uint id) => new() { Id = id, SourceGfxObjOrSetupId = 0x01000000u + id, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), }; private static LandblockMeshData EmptyMesh() => new( Array.Empty(), Array.Empty()); }