Remove the legacy GameWindow apply path and make the concrete render, physics, and static publishers the only production owner graph. Serialize full-window retirement with shared-origin teleport and session boundaries so old coordinate-frame resources cannot survive into a new world or login.
1356 lines
50 KiB
C#
1356 lines
50 KiB
C#
using System.Collections.Immutable;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Terrain;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Streaming;
|
|
|
|
public sealed class LandblockPresentationPipelineTests
|
|
{
|
|
[Fact]
|
|
public void ControllerRejectsPresentationPipelineFromForeignWorldState()
|
|
{
|
|
var controllerState = new GpuWorldState();
|
|
var foreignState = new GpuWorldState();
|
|
var foreignPipeline = new LandblockPresentationPipeline(
|
|
static (_, _) => { },
|
|
foreignState);
|
|
|
|
Assert.Throws<ArgumentException>(() => new StreamingController(
|
|
enqueueLoad: static (_, _, _) => { },
|
|
enqueueUnload: static (_, _) => { },
|
|
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
|
|
controllerState,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
presentationPipeline: foreignPipeline));
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyPipelineRejectsRetirementCoordinatorFromForeignWorldState()
|
|
{
|
|
var pipelineState = new GpuWorldState();
|
|
var foreignCoordinator = new LandblockRetirementCoordinator(
|
|
new GpuWorldState(),
|
|
static _ => { });
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
new LandblockPresentationPipeline(
|
|
static (_, _) => { },
|
|
pipelineState,
|
|
retirementCoordinator: foreignCoordinator));
|
|
}
|
|
|
|
[Fact]
|
|
public void PublicPresentationConstructorsExposeOnlyConcreteOwnerGraph()
|
|
{
|
|
string[] legacyNames =
|
|
[
|
|
"applyTerrain",
|
|
"removeTerrain",
|
|
"demoteNearLayer",
|
|
"onLandblockLoaded",
|
|
"ensureEnvCellMeshes",
|
|
"retirementCoordinator",
|
|
];
|
|
|
|
System.Reflection.ConstructorInfo controller = Assert.Single(
|
|
typeof(StreamingController).GetConstructors());
|
|
Assert.Contains(
|
|
controller.GetParameters(),
|
|
parameter => parameter.Name == "presentationPipeline");
|
|
string[] names = controller.GetParameters()
|
|
.Select(parameter => parameter.Name!)
|
|
.ToArray();
|
|
Assert.DoesNotContain(names, legacyNames.Contains);
|
|
|
|
System.Reflection.ConstructorInfo pipeline = Assert.Single(
|
|
typeof(LandblockPresentationPipeline).GetConstructors());
|
|
Type[] pipelineParameterTypes = pipeline.GetParameters()
|
|
.Select(parameter => parameter.ParameterType)
|
|
.ToArray();
|
|
Assert.Contains(typeof(LandblockRenderPublisher), pipelineParameterTypes);
|
|
Assert.Contains(typeof(LandblockPhysicsPublisher), pipelineParameterTypes);
|
|
Assert.Contains(typeof(LandblockStaticPresentationPublisher), pipelineParameterTypes);
|
|
Assert.DoesNotContain(
|
|
pipeline.GetParameters(),
|
|
parameter => parameter.Name == "publishBeforeSpatialCommit");
|
|
|
|
string[] coordinatorOnlyMethods =
|
|
[
|
|
"BeginOriginRecenter",
|
|
"IsOriginRecenterRetirementComplete",
|
|
"TryCommitOriginRecenter",
|
|
"TryCancelOriginRecenter",
|
|
];
|
|
foreach (string methodName in coordinatorOnlyMethods)
|
|
{
|
|
Assert.Null(typeof(StreamingController).GetMethod(methodName));
|
|
Assert.NotNull(typeof(StreamingController).GetMethod(
|
|
methodName,
|
|
System.Reflection.BindingFlags.Instance |
|
|
System.Reflection.BindingFlags.NonPublic));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void OriginRecenter_BlocksOriginAndBootstrapUntilEveryOldOwnerConverges()
|
|
{
|
|
const uint centerId = 0x1919FFFFu;
|
|
const uint neighborId = 0x191AFFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
centerId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
neighborId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
var origin = new LiveWorldOriginState();
|
|
Assert.True(origin.TryInitialize(0x19, 0x19));
|
|
var enqueued = new List<uint>();
|
|
bool failNeighbor = true;
|
|
int neighborAttempts = 0;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (id, _) => enqueued.Add(id),
|
|
enqueueUnload: static _ => { },
|
|
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
|
|
applyTerrain: static (_, _) => { },
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
removeTerrain: id =>
|
|
{
|
|
if (id != neighborId)
|
|
return;
|
|
neighborAttempts++;
|
|
if (failNeighbor)
|
|
throw new InvalidOperationException("injected non-center failure");
|
|
});
|
|
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
|
|
|
Assert.False(recenter.Begin(0x22, 0x23, isSealedDungeon: false));
|
|
|
|
Assert.Equal((0x19, 0x19), (origin.CenterX, origin.CenterY));
|
|
Assert.False(state.IsLoaded(centerId));
|
|
Assert.False(state.IsLoaded(neighborId));
|
|
Assert.Equal(1, controller.PendingRetirementCount);
|
|
Assert.Equal(2, controller.LastFullWindowRetirementLandblockCount);
|
|
|
|
controller.Tick(0x19, 0x19);
|
|
Assert.Empty(enqueued);
|
|
Assert.True(neighborAttempts >= 2);
|
|
|
|
failNeighbor = false;
|
|
Assert.True(recenter.Advance());
|
|
|
|
Assert.Equal((0x22, 0x23), (origin.CenterX, origin.CenterY));
|
|
Assert.Equal(0, controller.PendingRetirementCount);
|
|
controller.Tick(0x22, 0x23);
|
|
Assert.Equal([0x2223FFFFu], enqueued);
|
|
}
|
|
|
|
[Fact]
|
|
public void OriginRecenter_PreparationFailureBeforeDetachRetainsOldOriginAndResidents()
|
|
{
|
|
const uint landblockId = 0x3030FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
var origin = new LiveWorldOriginState();
|
|
Assert.True(origin.TryInitialize(0x30, 0x30));
|
|
bool failClear = true;
|
|
int clearAttempts = 0;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: static (_, _) => { },
|
|
enqueueUnload: static _ => { },
|
|
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
|
|
applyTerrain: static (_, _) => { },
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
clearPendingLoads: () =>
|
|
{
|
|
clearAttempts++;
|
|
if (failClear)
|
|
throw new InvalidOperationException("injected clear failure");
|
|
});
|
|
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
|
|
|
Assert.False(recenter.Begin(0x40, 0x41, isSealedDungeon: false));
|
|
Assert.Equal((0x30, 0x30), (origin.CenterX, origin.CenterY));
|
|
Assert.True(state.IsLoaded(landblockId));
|
|
Assert.Equal(0, controller.PendingRetirementCount);
|
|
|
|
failClear = false;
|
|
Assert.True(recenter.Advance());
|
|
|
|
Assert.Equal(2, clearAttempts);
|
|
Assert.False(state.IsLoaded(landblockId));
|
|
Assert.Equal((0x40, 0x41), (origin.CenterX, origin.CenterY));
|
|
}
|
|
|
|
[Fact]
|
|
public void OriginRecenter_ObserverFailureHalfwayRetainsBarrierUntilSnapshotFinishes()
|
|
{
|
|
const uint firstId = 0x4242FFFFu;
|
|
const uint secondId = 0x4243FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
firstId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
secondId,
|
|
new LandBlock(),
|
|
[Entity(2, serverGuid: 0x70000042u)]));
|
|
int observerCalls = 0;
|
|
state.LiveProjectionVisibilityChanged += (_, _) =>
|
|
{
|
|
observerCalls++;
|
|
throw new InvalidOperationException("injected observer failure");
|
|
};
|
|
var origin = new LiveWorldOriginState();
|
|
Assert.True(origin.TryInitialize(0x42, 0x42));
|
|
var removed = new List<uint>();
|
|
var controller = new StreamingController(
|
|
enqueueLoad: static (_, _) => { },
|
|
enqueueUnload: static _ => { },
|
|
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
|
|
applyTerrain: static (_, _) => { },
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
removeTerrain: id => removed.Add(id));
|
|
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
|
|
|
Assert.False(recenter.Begin(0x43, 0x44, isSealedDungeon: false));
|
|
|
|
Assert.Equal((0x42, 0x42), (origin.CenterX, origin.CenterY));
|
|
Assert.False(state.IsLoaded(firstId));
|
|
Assert.False(state.IsLoaded(secondId));
|
|
Assert.Equal(1, observerCalls);
|
|
|
|
Assert.True(recenter.Advance());
|
|
Assert.Equal((0x43, 0x44), (origin.CenterX, origin.CenterY));
|
|
Assert.Equal([firstId, secondId], removed);
|
|
}
|
|
|
|
[Fact]
|
|
public void OriginRecenter_RetryPreservesLiveIdentityAndDoesNotRescueReusedGuid()
|
|
{
|
|
const uint oldId = 0x4545FFFFu;
|
|
const uint destinationId = 0x4646FFFFu;
|
|
const uint playerGuid = 0x50000045u;
|
|
const uint remoteGuid = 0x70000045u;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
oldId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
WorldEntity player = Entity(10, playerGuid);
|
|
WorldEntity remote = Entity(11, remoteGuid);
|
|
state.MarkPersistent(playerGuid);
|
|
state.PlaceLiveEntityProjection(oldId, player);
|
|
state.PlaceLiveEntityProjection(oldId, remote);
|
|
var origin = new LiveWorldOriginState();
|
|
Assert.True(origin.TryInitialize(0x45, 0x45));
|
|
bool failRetirement = true;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: static (_, _) => { },
|
|
enqueueUnload: static _ => { },
|
|
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
|
|
applyTerrain: static (_, _) => { },
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
removeTerrain: _ =>
|
|
{
|
|
if (failRetirement)
|
|
throw new InvalidOperationException("injected live-owner retry");
|
|
});
|
|
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
|
|
|
Assert.False(recenter.Begin(0x46, 0x46, isSealedDungeon: false));
|
|
|
|
Assert.Same(player, Assert.Single(state.DrainRescued()));
|
|
Assert.Equal(1, state.PendingLiveEntityCount);
|
|
Assert.Empty(state.Entities);
|
|
|
|
failRetirement = false;
|
|
Assert.True(recenter.Advance());
|
|
Assert.Empty(state.DrainRescued());
|
|
state.AddLandblock(new LoadedLandblock(
|
|
destinationId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
state.RebucketLiveEntity(player, destinationId);
|
|
Assert.Same(player, Assert.Single(state.Entities));
|
|
|
|
state.AddLandblock(new LoadedLandblock(
|
|
oldId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
Assert.Contains(state.Entities, entity => ReferenceEquals(entity, remote));
|
|
|
|
state.ForgetLiveEntity(playerGuid);
|
|
WorldEntity replacement = Entity(12, playerGuid);
|
|
state.MarkPersistent(playerGuid);
|
|
state.PlaceLiveEntityProjection(destinationId, replacement);
|
|
state.RemoveLandblock(destinationId);
|
|
|
|
Assert.Same(replacement, Assert.Single(state.DrainRescued()));
|
|
Assert.DoesNotContain(state.Entities, entity => ReferenceEquals(entity, player));
|
|
}
|
|
|
|
[Fact]
|
|
public void OriginRecenter_ResetWithoutReplacementConvergesAtCurrentOrigin()
|
|
{
|
|
const uint landblockId = 0x5050FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
var origin = new LiveWorldOriginState();
|
|
Assert.True(origin.TryInitialize(0x50, 0x50));
|
|
bool failRetirement = true;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: static (_, _) => { },
|
|
enqueueUnload: static _ => { },
|
|
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
|
|
applyTerrain: static (_, _) => { },
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
removeTerrain: _ =>
|
|
{
|
|
if (failRetirement)
|
|
throw new InvalidOperationException("injected retirement failure");
|
|
});
|
|
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
|
|
|
Assert.False(recenter.Begin(0x60, 0x60, isSealedDungeon: false));
|
|
recenter.Reset();
|
|
Assert.True(recenter.IsPending);
|
|
|
|
failRetirement = false;
|
|
Assert.True(recenter.Advance());
|
|
|
|
Assert.False(recenter.IsPending);
|
|
Assert.Equal((0x50, 0x50), (origin.CenterX, origin.CenterY));
|
|
controller.Tick(0x50, 0x50);
|
|
}
|
|
|
|
[Fact]
|
|
public void OriginRecenter_ResetBackToSealedSourceKeepsRadiusZeroMode()
|
|
{
|
|
const uint sourceId = 0x5252FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
sourceId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
var origin = new LiveWorldOriginState();
|
|
Assert.True(origin.TryInitialize(0x52, 0x52));
|
|
var enqueued = new List<(uint Id, LandblockStreamJobKind Kind)>();
|
|
bool failRetirement = true;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (id, kind) => enqueued.Add((id, kind)),
|
|
enqueueUnload: static _ => { },
|
|
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
|
|
applyTerrain: static (_, _) => { },
|
|
state,
|
|
nearRadius: 2,
|
|
farRadius: 4,
|
|
removeTerrain: _ =>
|
|
{
|
|
if (failRetirement)
|
|
throw new InvalidOperationException("injected retirement failure");
|
|
});
|
|
controller.PreCollapseToDungeon(0x52, 0x52);
|
|
enqueued.Clear();
|
|
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
|
|
|
Assert.False(recenter.Begin(0x60, 0x60, isSealedDungeon: false));
|
|
recenter.Reset();
|
|
failRetirement = false;
|
|
Assert.True(recenter.Advance());
|
|
|
|
Assert.Equal((0x52, 0x52), (origin.CenterX, origin.CenterY));
|
|
Assert.Equal([(sourceId, LandblockStreamJobKind.LoadNear)], enqueued);
|
|
controller.Tick(0x52, 0x52, insideDungeon: true);
|
|
Assert.Single(enqueued);
|
|
}
|
|
|
|
[Fact]
|
|
public void OriginRecenter_SessionResetConvergesWithoutBootstrappingEndingWorld()
|
|
{
|
|
const uint sourceId = 0x5353FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
sourceId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
var origin = new LiveWorldOriginState();
|
|
Assert.True(origin.TryInitialize(0x53, 0x53));
|
|
var enqueued = new List<uint>();
|
|
bool failRetirement = true;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (id, _) => enqueued.Add(id),
|
|
enqueueUnload: static _ => { },
|
|
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
|
|
applyTerrain: static (_, _) => { },
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
removeTerrain: _ =>
|
|
{
|
|
if (failRetirement)
|
|
throw new InvalidOperationException("injected session reset failure");
|
|
});
|
|
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
|
|
|
Assert.False(recenter.Begin(0x54, 0x54, isSealedDungeon: true));
|
|
recenter.Reset(sessionEnding: true);
|
|
failRetirement = false;
|
|
Assert.True(recenter.Advance());
|
|
|
|
Assert.Empty(enqueued);
|
|
Assert.Equal((0x53, 0x53), (origin.CenterX, origin.CenterY));
|
|
controller.Tick(0x53, 0x53);
|
|
Assert.Equal([sourceId], enqueued);
|
|
}
|
|
|
|
[Fact]
|
|
public void OriginRecenter_SessionResetCannotOverwriteNextSessionOrigin()
|
|
{
|
|
const uint sourceId = 0x5353FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
sourceId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
var origin = new LiveWorldOriginState();
|
|
Assert.True(origin.TryInitialize(0x53, 0x53));
|
|
bool failRetirement = true;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: static (_, _) => { },
|
|
enqueueUnload: static _ => { },
|
|
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
|
|
applyTerrain: static (_, _) => { },
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
removeTerrain: _ =>
|
|
{
|
|
if (failRetirement)
|
|
throw new InvalidOperationException("injected session reset failure");
|
|
});
|
|
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
|
|
|
Assert.False(recenter.Begin(0x54, 0x54, isSealedDungeon: true));
|
|
recenter.Reset(sessionEnding: true);
|
|
origin.Reset();
|
|
Assert.True(origin.TryInitialize(0x61, 0x62));
|
|
|
|
failRetirement = false;
|
|
Assert.True(recenter.Advance());
|
|
|
|
Assert.Equal((0x61, 0x62), (origin.CenterX, origin.CenterY));
|
|
}
|
|
|
|
[Fact]
|
|
public void OriginRecenter_CommittedPendingClearFailureDoesNotReplayClear()
|
|
{
|
|
var state = new GpuWorldState();
|
|
var origin = new LiveWorldOriginState();
|
|
Assert.True(origin.TryInitialize(0x10, 0x10));
|
|
int clearAttempts = 0;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: static (_, _) => { },
|
|
enqueueUnload: static _ => { },
|
|
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
|
|
applyTerrain: static (_, _) => { },
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
clearPendingLoads: () =>
|
|
{
|
|
clearAttempts++;
|
|
throw new StreamingMutationException(
|
|
"injected committed clear failure",
|
|
mutationCommitted: true);
|
|
});
|
|
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
|
|
|
Assert.False(recenter.Begin(0x11, 0x11, isSealedDungeon: false));
|
|
Assert.True(recenter.Advance());
|
|
|
|
Assert.Equal(1, clearAttempts);
|
|
Assert.Equal((0x11, 0x11), (origin.CenterX, origin.CenterY));
|
|
}
|
|
|
|
[Fact]
|
|
public void OriginRecenter_ReentrantClearAndRetirementDoNotRecurseOrSkipResidents()
|
|
{
|
|
const uint firstId = 0x7070FFFFu;
|
|
const uint secondId = 0x7071FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
firstId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
secondId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
var origin = new LiveWorldOriginState();
|
|
Assert.True(origin.TryInitialize(0x70, 0x70));
|
|
StreamingController? controller = null;
|
|
int clearCalls = 0;
|
|
var removed = new List<uint>();
|
|
controller = new StreamingController(
|
|
enqueueLoad: static (_, _) => { },
|
|
enqueueUnload: static _ => { },
|
|
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
|
|
applyTerrain: static (_, _) => { },
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
clearPendingLoads: () =>
|
|
{
|
|
clearCalls++;
|
|
Assert.False(controller!.IsOriginRecenterRetirementComplete());
|
|
},
|
|
removeTerrain: id =>
|
|
{
|
|
removed.Add(id);
|
|
if (id == firstId)
|
|
Assert.False(controller!.IsOriginRecenterRetirementComplete());
|
|
});
|
|
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
|
|
|
Assert.True(recenter.Begin(0x71, 0x71, isSealedDungeon: false));
|
|
|
|
Assert.Equal(1, clearCalls);
|
|
Assert.Equal([firstId, secondId], removed);
|
|
Assert.False(state.IsLoaded(firstId));
|
|
Assert.False(state.IsLoaded(secondId));
|
|
}
|
|
|
|
[Fact]
|
|
public void OriginRecenter_DefersRadiusChangeUntilDestinationCommit()
|
|
{
|
|
const uint oldId = 0x7272FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
oldId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
var origin = new LiveWorldOriginState();
|
|
Assert.True(origin.TryInitialize(0x72, 0x72));
|
|
var enqueued = new List<uint>();
|
|
bool failRetirement = true;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (id, _) => enqueued.Add(id),
|
|
enqueueUnload: static _ => { },
|
|
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
|
|
applyTerrain: static (_, _) => { },
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
removeTerrain: _ =>
|
|
{
|
|
if (failRetirement)
|
|
throw new InvalidOperationException("injected radius barrier failure");
|
|
});
|
|
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
|
|
|
Assert.False(recenter.Begin(0x73, 0x73, isSealedDungeon: false));
|
|
controller.ReconfigureRadii(nearRadius: 1, farRadius: 2);
|
|
controller.Tick(0x72, 0x72);
|
|
|
|
Assert.Equal(0, controller.NearRadius);
|
|
Assert.Equal(0, controller.FarRadius);
|
|
Assert.Empty(enqueued);
|
|
|
|
failRetirement = false;
|
|
Assert.True(recenter.Advance());
|
|
controller.Tick(0x73, 0x73);
|
|
|
|
Assert.Equal(1, controller.NearRadius);
|
|
Assert.Equal(2, controller.FarRadius);
|
|
Assert.Equal(25, enqueued.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void OriginRecenter_SealedDungeonPinsRadiusZeroBeforeCenterCompletion()
|
|
{
|
|
const uint oldId = 0x2020FFFFu;
|
|
const uint dungeonId = 0x3030FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
oldId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
var origin = new LiveWorldOriginState();
|
|
Assert.True(origin.TryInitialize(0x20, 0x20));
|
|
var outbox = new Queue<LandblockStreamResult>();
|
|
var enqueued = new List<(uint Id, LandblockStreamJobKind Kind)>();
|
|
bool failDungeonEnqueue = true;
|
|
int enqueueAttempts = 0;
|
|
int publications = 0;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (id, kind) =>
|
|
{
|
|
enqueueAttempts++;
|
|
if (failDungeonEnqueue)
|
|
throw new InvalidOperationException("injected dungeon enqueue failure");
|
|
enqueued.Add((id, kind));
|
|
},
|
|
enqueueUnload: static _ => { },
|
|
drainCompletions: max => Drain(outbox, max),
|
|
applyTerrain: (_, _) => publications++,
|
|
state,
|
|
nearRadius: 2,
|
|
farRadius: 4);
|
|
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
|
|
|
Assert.False(recenter.Begin(0x30, 0x30, isSealedDungeon: true));
|
|
Assert.True(recenter.IsPending);
|
|
controller.Tick(0x30, 0x30, insideDungeon: true);
|
|
Assert.Empty(enqueued);
|
|
|
|
failDungeonEnqueue = false;
|
|
Assert.True(recenter.Advance());
|
|
Assert.Equal(2, enqueueAttempts);
|
|
Assert.Equal([(dungeonId, LandblockStreamJobKind.LoadNear)], enqueued);
|
|
|
|
outbox.Enqueue(new LandblockStreamResult.Loaded(
|
|
dungeonId,
|
|
LandblockStreamTier.Near,
|
|
new LoadedLandblock(
|
|
dungeonId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()),
|
|
EmptyMesh(),
|
|
generation: 1));
|
|
controller.Tick(0x30, 0x30, insideDungeon: true);
|
|
|
|
Assert.Equal(1, publications);
|
|
Assert.True(state.IsNearTier(dungeonId));
|
|
}
|
|
|
|
[Fact]
|
|
public void Loaded_PreservesPresentationSpatialPinReplayAndLiveRecoveryOrder()
|
|
{
|
|
const uint landblockId = 0x2020FFFFu;
|
|
var calls = new List<string>();
|
|
var meshes = new RecordingMeshAdapter(calls);
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
LandblockBuild build = BuildWithShell(landblockId);
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (accepted, _) =>
|
|
{
|
|
Assert.Same(build, accepted);
|
|
Assert.False(state.IsLoaded(landblockId));
|
|
calls.Add("presentation");
|
|
},
|
|
state,
|
|
onLandblockLoaded: id =>
|
|
{
|
|
Assert.Equal(landblockId, id);
|
|
Assert.True(state.IsLoaded(id));
|
|
calls.Add("live-recovery");
|
|
},
|
|
ensureEnvCellMeshes: envCells =>
|
|
{
|
|
Assert.Same(build.EnvCells, envCells);
|
|
Assert.True(state.IsLoaded(landblockId));
|
|
calls.Add("mesh-replay-after-pin");
|
|
});
|
|
|
|
pipeline.PublishLoaded(new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
build,
|
|
EmptyMesh()));
|
|
|
|
Assert.Equal(
|
|
["presentation", "pin", "mesh-replay-after-pin", "live-recovery"],
|
|
calls);
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
}
|
|
|
|
[Fact]
|
|
public void Promoted_ExistingFar_MergesNearLayerBeforeReplayAndRecovery()
|
|
{
|
|
const uint landblockId = 0x3030FFFFu;
|
|
var calls = new List<string>();
|
|
var meshes = new RecordingMeshAdapter(calls);
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
state.AddLandblock(
|
|
new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>(),
|
|
PhysicsDatBundle.Empty),
|
|
tier: LandblockStreamTier.Far);
|
|
LandblockBuild build = BuildWithShell(landblockId, Entity(7));
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (_, _) => calls.Add("presentation"),
|
|
state,
|
|
onLandblockLoaded: _ =>
|
|
{
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
Assert.Contains(state.Entities, entity => entity.Id == 7);
|
|
calls.Add("live-recovery");
|
|
},
|
|
ensureEnvCellMeshes: _ =>
|
|
{
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
Assert.Contains(state.Entities, entity => entity.Id == 7);
|
|
calls.Add("mesh-replay-after-pin");
|
|
});
|
|
|
|
pipeline.PublishPromoted(new LandblockStreamResult.Promoted(
|
|
landblockId,
|
|
build,
|
|
EmptyMesh()),
|
|
mergeIntoExistingLandblock: true);
|
|
|
|
Assert.Equal(
|
|
["presentation", "pin", "mesh-replay-after-pin", "live-recovery"],
|
|
calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void PublishAsFar_StripsNearPayloadAndSkipsEnvCellReplay()
|
|
{
|
|
const uint landblockId = 0x4040FFFFu;
|
|
var state = new GpuWorldState();
|
|
var origin = new LandblockBuildOrigin(0x40, 0x41);
|
|
LandblockBuild source = BuildWithShell(landblockId, Entity(9)) with
|
|
{
|
|
Origin = origin,
|
|
};
|
|
LandblockBuild? published = null;
|
|
int replayCount = 0;
|
|
int recoveryCount = 0;
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (build, _) => published = build,
|
|
state,
|
|
onLandblockLoaded: _ => recoveryCount++,
|
|
ensureEnvCellMeshes: _ => replayCount++);
|
|
|
|
var accepted = new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
source,
|
|
EmptyMesh());
|
|
pipeline.PublishAsFar(accepted, source, accepted.MeshData);
|
|
|
|
Assert.NotNull(published);
|
|
Assert.Empty(published.Landblock.Entities);
|
|
Assert.Same(PhysicsDatBundle.Empty, published.Landblock.PhysicsDats);
|
|
Assert.Null(published.EnvCells);
|
|
Assert.Equal(origin, published.Origin);
|
|
Assert.True(state.IsLoaded(landblockId));
|
|
Assert.False(state.IsNearTier(landblockId));
|
|
Assert.Equal(0, replayCount);
|
|
Assert.Equal(1, recoveryCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnvCellReplayFailure_ResumesSuffixWithoutReplayingCommittedPrefix()
|
|
{
|
|
const uint landblockId = 0x5151FFFFu;
|
|
var calls = new List<string>();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(
|
|
new RecordingMeshAdapter(calls)));
|
|
bool failReplayOnce = true;
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (_, _) => calls.Add("presentation"),
|
|
state,
|
|
onLandblockLoaded: _ => calls.Add("live-recovery"),
|
|
ensureEnvCellMeshes: _ =>
|
|
{
|
|
calls.Add("mesh-replay");
|
|
if (failReplayOnce)
|
|
{
|
|
failReplayOnce = false;
|
|
throw new InvalidOperationException("injected replay failure");
|
|
}
|
|
});
|
|
LandblockBuild build = BuildWithShell(landblockId);
|
|
var result = new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
build,
|
|
EmptyMesh());
|
|
|
|
Assert.Throws<InvalidOperationException>(() => pipeline.PublishLoaded(result));
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
Assert.True(pipeline.HasPendingPublication(result));
|
|
|
|
pipeline.PublishLoaded(result);
|
|
|
|
Assert.Equal(
|
|
["presentation", "pin", "mesh-replay", "mesh-replay", "live-recovery"],
|
|
calls);
|
|
Assert.False(pipeline.HasPendingPublication(result));
|
|
}
|
|
|
|
[Fact]
|
|
public void PromotionReplayFailure_RetainsOriginalSelfContainedModeAcrossRetry()
|
|
{
|
|
const uint landblockId = 0x5252FFFFu;
|
|
var calls = new List<string>();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(
|
|
new RecordingMeshAdapter(calls)));
|
|
bool failReplayOnce = true;
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (_, _) => calls.Add("presentation"),
|
|
state,
|
|
onLandblockLoaded: _ => calls.Add("live-recovery"),
|
|
ensureEnvCellMeshes: _ =>
|
|
{
|
|
calls.Add("mesh-replay");
|
|
if (failReplayOnce)
|
|
{
|
|
failReplayOnce = false;
|
|
throw new InvalidOperationException("injected replay failure");
|
|
}
|
|
});
|
|
LandblockBuild build = BuildWithShell(landblockId, Entity(17));
|
|
var result = new LandblockStreamResult.Promoted(
|
|
landblockId,
|
|
build,
|
|
EmptyMesh());
|
|
|
|
Assert.Throws<InvalidOperationException>(() => pipeline.PublishPromoted(
|
|
result,
|
|
mergeIntoExistingLandblock: false));
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
|
|
// Current residency changed after the first attempt. The transaction
|
|
// must retain the original self-contained publication command rather
|
|
// than switching policy mid-retry.
|
|
pipeline.PublishPromoted(result, mergeIntoExistingLandblock: true);
|
|
|
|
Assert.Equal(
|
|
["presentation", "pin", "mesh-replay", "mesh-replay", "live-recovery"],
|
|
calls);
|
|
Assert.Single(state.Entities, entity => entity.Id == 17);
|
|
}
|
|
|
|
[Fact]
|
|
public void SpatialPinFailure_ControllerRetriesExactCompletionWithoutReplayingPresentation()
|
|
{
|
|
const uint landblockId = 0x6060FFFFu;
|
|
var meshes = new FailFirstPreparedPinAdapter();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
var outbox = new Queue<LandblockStreamResult>();
|
|
LandblockBuild build = BuildWithShell(landblockId);
|
|
outbox.Enqueue(new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
build,
|
|
EmptyMesh()));
|
|
int presentationCalls = 0;
|
|
int replayCalls = 0;
|
|
int liveRecoveryCalls = 0;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (_, _) => { },
|
|
enqueueUnload: _ => { },
|
|
drainCompletions: max => Drain(outbox, max),
|
|
applyTerrain: (_, _) => presentationCalls++,
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
onLandblockLoaded: _ => liveRecoveryCalls++,
|
|
ensureEnvCellMeshes: _ => replayCalls++);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x60, 0x60));
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
Assert.Equal(1, presentationCalls);
|
|
Assert.Equal(0, replayCalls);
|
|
Assert.Equal(0, liveRecoveryCalls);
|
|
|
|
controller.Tick(0x60, 0x60);
|
|
|
|
Assert.Equal(1, presentationCalls);
|
|
Assert.Equal(2, meshes.PinAttempts);
|
|
Assert.Equal(1, replayCalls);
|
|
Assert.Equal(1, liveRecoveryCalls);
|
|
}
|
|
|
|
[Fact]
|
|
public void PromotionPinFailure_RetriesActivationWithoutAppendingEntitiesAgain()
|
|
{
|
|
const uint landblockId = 0x6161FFFFu;
|
|
var meshes = new FailFirstPreparedPinAdapter();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
state.AddLandblock(
|
|
new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()),
|
|
tier: LandblockStreamTier.Far);
|
|
int presentationCalls = 0;
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (_, _) => presentationCalls++,
|
|
state);
|
|
WorldEntity promotedEntity = Entity(31);
|
|
LandblockBuild build = BuildWithShell(landblockId, promotedEntity);
|
|
var result = new LandblockStreamResult.Promoted(
|
|
landblockId,
|
|
build,
|
|
EmptyMesh());
|
|
|
|
Assert.Throws<InvalidOperationException>(() => pipeline.PublishPromoted(
|
|
result,
|
|
mergeIntoExistingLandblock: true));
|
|
Assert.Single(state.Entities, entity => ReferenceEquals(entity, promotedEntity));
|
|
|
|
pipeline.PublishPromoted(result, mergeIntoExistingLandblock: true);
|
|
|
|
Assert.Equal(1, presentationCalls);
|
|
Assert.Equal(2, meshes.PinAttempts);
|
|
Assert.Single(state.Entities, entity => ReferenceEquals(entity, promotedEntity));
|
|
Assert.False(pipeline.HasPendingPublication(result));
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadedPinFailure_RetryPreservesPendingLiveProjectionMergedAtSpatialCommit()
|
|
{
|
|
const uint landblockId = 0x6262FFFFu;
|
|
var meshes = new FailFirstPreparedPinAdapter();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
var pendingLive = new WorldEntity
|
|
{
|
|
Id = 77,
|
|
ServerGuid = 0x80000077u,
|
|
SourceGfxObjOrSetupId = 0x01000077u,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = [new MeshRef(0x01000077u, Matrix4x4.Identity)],
|
|
};
|
|
state.PlaceLiveEntityProjection(0x62620100u, pendingLive);
|
|
LandblockBuild build = BuildWithShell(landblockId, Entity(32));
|
|
var result = new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
build,
|
|
EmptyMesh());
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
static (_, _) => { },
|
|
state);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => pipeline.PublishLoaded(result));
|
|
Assert.Contains(state.Entities, entity => ReferenceEquals(entity, pendingLive));
|
|
Assert.Equal(0, state.PendingLiveEntityCount);
|
|
|
|
pipeline.PublishLoaded(result);
|
|
|
|
Assert.Contains(state.Entities, entity => ReferenceEquals(entity, pendingLive));
|
|
Assert.Single(state.Entities, entity => ReferenceEquals(entity, pendingLive));
|
|
Assert.Equal(0, state.PendingLiveEntityCount);
|
|
Assert.Equal(2, meshes.PinAttempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void PriorityFailure_PreservesFailingResultAndUntouchedDrainedSuffix()
|
|
{
|
|
const uint priorityId = 0x7070FFFFu;
|
|
const uint laterId = 0x7071FFFFu;
|
|
const uint unloadId = 0x1010FFFFu;
|
|
var meshes = new FailFirstPreparedPinAdapter();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
unloadId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
var outbox = new Queue<LandblockStreamResult>(
|
|
[
|
|
new LandblockStreamResult.Loaded(
|
|
priorityId,
|
|
LandblockStreamTier.Near,
|
|
BuildWithShell(priorityId),
|
|
EmptyMesh()),
|
|
new LandblockStreamResult.Loaded(
|
|
laterId,
|
|
LandblockStreamTier.Near,
|
|
BuildWithShell(laterId),
|
|
EmptyMesh()),
|
|
new LandblockStreamResult.Unloaded(unloadId),
|
|
]);
|
|
var calls = new List<string>();
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (_, _) => { },
|
|
enqueueUnload: _ => { },
|
|
drainCompletions: max => Drain(outbox, max),
|
|
applyTerrain: (build, _) => calls.Add($"publish:{build.LandblockId:X8}"),
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 2,
|
|
removeTerrain: id => calls.Add($"unload:{id:X8}"));
|
|
controller.PriorityLandblockId = priorityId;
|
|
|
|
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x70, 0x70));
|
|
Assert.Empty(outbox);
|
|
Assert.False(state.IsLoaded(laterId));
|
|
Assert.True(state.IsLoaded(unloadId));
|
|
|
|
controller.PriorityLandblockId = 0;
|
|
controller.Tick(0x70, 0x70);
|
|
|
|
Assert.True(state.IsLoaded(priorityId));
|
|
Assert.True(state.IsLoaded(laterId));
|
|
Assert.False(state.IsLoaded(unloadId));
|
|
Assert.Equal(
|
|
[
|
|
$"publish:{priorityId:X8}",
|
|
$"publish:{laterId:X8}",
|
|
$"unload:{unloadId:X8}",
|
|
],
|
|
calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void HardRecenter_ConvergesPendingSpatialPublicationBeforeRetiringIt()
|
|
{
|
|
const uint landblockId = 0x8080FFFFu;
|
|
var meshes = new FailFirstPreparedPinAdapter();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
var outbox = new Queue<LandblockStreamResult>();
|
|
var result = new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
BuildWithShell(landblockId),
|
|
EmptyMesh());
|
|
outbox.Enqueue(result);
|
|
int presentationCalls = 0;
|
|
int replayCalls = 0;
|
|
int recoveryCalls = 0;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (_, _) => { },
|
|
enqueueUnload: _ => { },
|
|
drainCompletions: max => Drain(outbox, max),
|
|
applyTerrain: (_, _) => presentationCalls++,
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
onLandblockLoaded: _ => recoveryCalls++,
|
|
ensureEnvCellMeshes: _ => replayCalls++);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x80, 0x80));
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
|
|
controller.ForceReloadWindow();
|
|
|
|
Assert.False(state.IsLoaded(landblockId));
|
|
Assert.Equal(1, presentationCalls);
|
|
Assert.Equal(1, replayCalls);
|
|
Assert.Equal(1, recoveryCalls);
|
|
Assert.Equal(2, meshes.PinAttempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void DesiredNearToFarChurn_ResumesOriginalPublicationThenDemotes()
|
|
{
|
|
const uint landblockId = 0x9090FFFFu;
|
|
var calls = new List<string>();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(
|
|
new RecordingMeshAdapter(calls)));
|
|
var outbox = new Queue<LandblockStreamResult>();
|
|
var result = new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
BuildWithShell(landblockId, Entity(23)),
|
|
EmptyMesh());
|
|
outbox.Enqueue(result);
|
|
bool failReplayOnce = true;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (_, _) => { },
|
|
enqueueUnload: _ => { },
|
|
drainCompletions: max => Drain(outbox, max),
|
|
applyTerrain: (_, _) => calls.Add("presentation"),
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 2,
|
|
demoteNearLayer: _ => calls.Add("demote"),
|
|
ensureEnvCellMeshes: _ =>
|
|
{
|
|
calls.Add("mesh-replay");
|
|
if (failReplayOnce)
|
|
{
|
|
failReplayOnce = false;
|
|
throw new InvalidOperationException("injected replay failure");
|
|
}
|
|
});
|
|
|
|
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x90, 0x90));
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
|
|
controller.Tick(0x90, 0x93);
|
|
|
|
Assert.True(state.IsLoaded(landblockId));
|
|
Assert.False(state.IsNearTier(landblockId));
|
|
Assert.DoesNotContain(state.Entities, entity => entity.Id == 23);
|
|
Assert.Equal(1, calls.Count(call => call == "presentation"));
|
|
Assert.Equal(2, calls.Count(call => call == "mesh-replay"));
|
|
Assert.Contains("demote", calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void CoarsePresentationFailure_IsFailFastAndNotRetainedForAutomaticReplay()
|
|
{
|
|
const uint landblockId = 0xA0A0FFFFu;
|
|
var state = new GpuWorldState();
|
|
int presentationCalls = 0;
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (_, _) =>
|
|
{
|
|
presentationCalls++;
|
|
throw new StreamingMutationException(
|
|
"injected committed coarse failure",
|
|
mutationCommitted: true);
|
|
},
|
|
state);
|
|
var result = new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
BuildWithShell(landblockId),
|
|
EmptyMesh());
|
|
|
|
Assert.Throws<StreamingMutationException>(() => pipeline.PublishLoaded(result));
|
|
|
|
Assert.Equal(1, presentationCalls);
|
|
Assert.False(pipeline.HasPendingPublication(result));
|
|
Assert.Equal(0, pipeline.PendingPublicationCount);
|
|
Assert.False(state.IsLoaded(landblockId));
|
|
}
|
|
|
|
[Fact]
|
|
public void DeferredCommittedPresentationFailure_DropsOnlyFailedResultAndPreservesTail()
|
|
{
|
|
const uint failedId = 0xB0B0FFFFu;
|
|
const uint tailId = 0xB0B1FFFFu;
|
|
var outbox = new Queue<LandblockStreamResult>(
|
|
[
|
|
new LandblockStreamResult.Loaded(
|
|
failedId,
|
|
LandblockStreamTier.Near,
|
|
new LandblockBuild(new LoadedLandblock(
|
|
failedId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>())),
|
|
EmptyMesh()),
|
|
new LandblockStreamResult.Loaded(
|
|
tailId,
|
|
LandblockStreamTier.Near,
|
|
new LandblockBuild(new LoadedLandblock(
|
|
tailId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>())),
|
|
EmptyMesh()),
|
|
]);
|
|
int failedCalls = 0;
|
|
int tailCalls = 0;
|
|
var state = new GpuWorldState();
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (_, _) => { },
|
|
enqueueUnload: _ => { },
|
|
drainCompletions: max => Drain(outbox, max),
|
|
applyTerrain: (build, _) =>
|
|
{
|
|
if (build.LandblockId == failedId)
|
|
{
|
|
failedCalls++;
|
|
throw new StreamingMutationException(
|
|
"injected committed presentation failure",
|
|
mutationCommitted: true);
|
|
}
|
|
tailCalls++;
|
|
},
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 2);
|
|
|
|
Assert.Throws<StreamingMutationException>(() => controller.Tick(0xB0, 0xB0));
|
|
Assert.Equal(1, failedCalls);
|
|
Assert.Equal(0, tailCalls);
|
|
Assert.Equal(1, controller.DeferredApplyBacklog);
|
|
|
|
controller.Tick(0xB0, 0xB0);
|
|
|
|
Assert.Equal(1, failedCalls);
|
|
Assert.Equal(1, tailCalls);
|
|
Assert.False(state.IsLoaded(failedId));
|
|
Assert.True(state.IsLoaded(tailId));
|
|
Assert.Equal(0, controller.DeferredApplyBacklog);
|
|
}
|
|
|
|
[Fact]
|
|
public void RetirementFacade_DetachesFirstAndReportsPendingLedger()
|
|
{
|
|
const uint landblockId = 0x5050FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
bool failOnce = true;
|
|
var retirements = new LandblockRetirementCoordinator(
|
|
state,
|
|
ticket =>
|
|
{
|
|
ticket.RunOnce(
|
|
LandblockRetirementStage.EntityLighting,
|
|
() =>
|
|
{
|
|
if (failOnce)
|
|
{
|
|
failOnce = false;
|
|
throw new InvalidOperationException("injected failure");
|
|
}
|
|
});
|
|
},
|
|
_ => LandblockRetirementStage.EntityLighting);
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
static (_, _) => { },
|
|
state,
|
|
retirementCoordinator: retirements);
|
|
|
|
pipeline.BeginFullRetirement(landblockId);
|
|
|
|
Assert.False(state.IsLoaded(landblockId));
|
|
Assert.True(pipeline.IsRetirementPending(landblockId));
|
|
Assert.Equal(1, pipeline.PendingRetirementCount);
|
|
|
|
pipeline.AdvanceRetirements();
|
|
|
|
Assert.False(pipeline.IsRetirementPending(landblockId));
|
|
Assert.Equal(0, pipeline.PendingRetirementCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Pipeline_DoesNotOwnStreamingPolicyOrLiveIdentityMaps()
|
|
{
|
|
string[] forbiddenFragments =
|
|
[
|
|
"generation",
|
|
"desiredtier",
|
|
"streamingregion",
|
|
"serverguid",
|
|
"liveentity",
|
|
];
|
|
|
|
string[] fields = typeof(LandblockPresentationPipeline)
|
|
.GetFields(System.Reflection.BindingFlags.Instance
|
|
| System.Reflection.BindingFlags.NonPublic)
|
|
.Select(field => $"{field.Name}:{field.FieldType.FullName}")
|
|
.ToArray();
|
|
|
|
foreach (string field in fields)
|
|
foreach (string forbidden in forbiddenFragments)
|
|
Assert.DoesNotContain(forbidden, field, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static LandblockBuild BuildWithShell(
|
|
uint landblockId,
|
|
params WorldEntity[] entities)
|
|
{
|
|
uint cellId = (landblockId & 0xFFFF0000u) | 0x0100u;
|
|
var shell = new EnvCellShellPlacement(
|
|
cellId,
|
|
0x2_0000_0001UL,
|
|
0x0D000001u,
|
|
1,
|
|
ImmutableArray<ushort>.Empty,
|
|
Vector3.Zero,
|
|
Quaternion.Identity,
|
|
Matrix4x4.Identity,
|
|
new WbBoundingBox(Vector3.Zero, Vector3.One),
|
|
new WbBoundingBox(Vector3.Zero, Vector3.One));
|
|
var envCells = new EnvCellLandblockBuild(
|
|
landblockId,
|
|
Array.Empty<LoadedCell>(),
|
|
[shell]);
|
|
return new LandblockBuild(
|
|
new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
entities,
|
|
PhysicsDatBundle.Empty),
|
|
envCells);
|
|
}
|
|
|
|
private static WorldEntity Entity(uint id, uint serverGuid = 0) => new()
|
|
{
|
|
Id = id,
|
|
ServerGuid = serverGuid,
|
|
SourceGfxObjOrSetupId = 0x01000000u + id,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
};
|
|
|
|
private static LandblockMeshData EmptyMesh() => new(
|
|
Array.Empty<TerrainVertex>(),
|
|
Array.Empty<uint>());
|
|
|
|
private static IReadOnlyList<LandblockStreamResult> Drain(
|
|
Queue<LandblockStreamResult> queue,
|
|
int max)
|
|
{
|
|
var drained = new List<LandblockStreamResult>(max);
|
|
while (drained.Count < max && queue.TryDequeue(out LandblockStreamResult? result))
|
|
drained.Add(result);
|
|
return drained;
|
|
}
|
|
|
|
private sealed class RecordingMeshAdapter(List<string> calls) : IWbMeshAdapter
|
|
{
|
|
public void IncrementRefCount(ulong id)
|
|
{
|
|
}
|
|
|
|
public void PinPreparedRenderData(ulong id) => calls.Add("pin");
|
|
|
|
public void DecrementRefCount(ulong id)
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class FailFirstPreparedPinAdapter : IWbMeshAdapter
|
|
{
|
|
public int PinAttempts { get; private set; }
|
|
|
|
public void IncrementRefCount(ulong id)
|
|
{
|
|
}
|
|
|
|
public void PinPreparedRenderData(ulong id)
|
|
{
|
|
PinAttempts++;
|
|
if (PinAttempts == 1)
|
|
throw new InvalidOperationException("injected pin failure");
|
|
}
|
|
|
|
public void DecrementRefCount(ulong id)
|
|
{
|
|
}
|
|
}
|
|
}
|