refactor(streaming): complete landblock presentation cutover
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.
This commit is contained in:
parent
801d8a189c
commit
c79d0a49da
12 changed files with 1373 additions and 402 deletions
|
|
@ -101,6 +101,66 @@ public sealed class LiveSessionResetPlanTests
|
|||
Assert.Equal(2, secondCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_BlocksNextSessionUntilOriginRetirementConverges()
|
||||
{
|
||||
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 ending-session failure");
|
||||
});
|
||||
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
||||
int identityResets = 0;
|
||||
var plan = new LiveSessionResetPlan(
|
||||
[
|
||||
new("teleport transit", () =>
|
||||
{
|
||||
if (!recenter.Reset(sessionEnding: true))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"streaming-origin retirement remains pending");
|
||||
}
|
||||
}),
|
||||
new("session identity", () =>
|
||||
{
|
||||
identityResets++;
|
||||
origin.Reset();
|
||||
}),
|
||||
]);
|
||||
|
||||
AggregateException blocked = Assert.Throws<AggregateException>(plan.Execute);
|
||||
LiveSessionResetStageException failure = Assert.IsType<LiveSessionResetStageException>(
|
||||
Assert.Single(blocked.InnerExceptions));
|
||||
Assert.Equal("teleport transit", failure.StageName);
|
||||
Assert.Equal(1, identityResets);
|
||||
|
||||
failRetirement = false;
|
||||
plan.Execute();
|
||||
|
||||
Assert.Equal(2, identityResets);
|
||||
Assert.False(recenter.IsPending);
|
||||
Assert.False(origin.IsKnown);
|
||||
Assert.True(origin.TryInitialize(0x61, 0x62));
|
||||
Assert.Equal((0x61, 0x62), (origin.CenterX, origin.CenterY));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_ReentrantAttemptIsReportedButLaterStagesStillRun()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ public sealed class LandblockBuildOriginTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFactoryAndGameWindowPublication_UseCapturedOriginInsteadOfLiveCenter()
|
||||
public void BuildFactoryAndRenderPublisher_UseCapturedOriginWithoutGameWindowFacade()
|
||||
{
|
||||
string root = FindRepoRoot();
|
||||
string gameWindowSource = File.ReadAllText(Path.Combine(
|
||||
|
|
@ -253,11 +253,12 @@ public sealed class LandblockBuildOriginTests
|
|||
"AcDream.App",
|
||||
"Streaming",
|
||||
"LandblockRenderPublisher.cs"));
|
||||
string publicationSection = Slice(
|
||||
gameWindowSource,
|
||||
"private void ApplyLoadedTerrainLocked(",
|
||||
"private void OnUpdate(double dt)");
|
||||
|
||||
string recenterSource = File.ReadAllText(Path.Combine(
|
||||
root,
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Streaming",
|
||||
"StreamingOriginRecenterCoordinator.cs"));
|
||||
Assert.Contains("request.Origin", buildSource, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_liveCenterX", buildSource, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_liveCenterY", buildSource, StringComparison.Ordinal);
|
||||
|
|
@ -277,12 +278,68 @@ public sealed class LandblockBuildOriginTests
|
|||
"BuildPhysicsDatBundle",
|
||||
gameWindowSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"ApplyLoadedTerrain",
|
||||
gameWindowSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"PublishLandblockStaticLightingBeforeCollision",
|
||||
gameWindowSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"_landblockPhysicsPublisher!.RemoveLandblock",
|
||||
gameWindowSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"_landblockRenderPublisher",
|
||||
gameWindowSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"_landblockPhysicsPublisher",
|
||||
gameWindowSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"_landblockStaticPresentationPublisher",
|
||||
gameWindowSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"_landblockPresentationPipeline",
|
||||
gameWindowSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"new AcDream.App.Streaming.LandblockRenderPublisher(",
|
||||
gameWindowSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"new AcDream.App.Streaming.LandblockPhysicsPublisher(",
|
||||
gameWindowSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"new AcDream.App.Streaming.LandblockStaticPresentationPublisher(",
|
||||
gameWindowSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("applyTerrain:", gameWindowSource, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("demoteNearLayer:", gameWindowSource, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("retirementCoordinator:", gameWindowSource, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"_liveWorldOrigin.Recenter(lbX, lbY)",
|
||||
gameWindowSource,
|
||||
StringComparison.Ordinal);
|
||||
int retirementBarrier = recenterSource.IndexOf(
|
||||
"IsOriginRecenterRetirementComplete()",
|
||||
StringComparison.Ordinal);
|
||||
int originCommit = recenterSource.IndexOf(
|
||||
"_origin.Recenter(",
|
||||
StringComparison.Ordinal);
|
||||
int destinationCommit = recenterSource.IndexOf(
|
||||
"_streaming.TryCommitOriginRecenter(",
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(retirementBarrier >= 0);
|
||||
Assert.True(originCommit > retirementBarrier);
|
||||
Assert.True(destinationCommit > originCommit);
|
||||
Assert.Contains("ComputeOrigin(landblockId, build.Origin)", renderPublisherSource, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_liveCenterX", renderPublisherSource, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_liveCenterY", renderPublisherSource, StringComparison.Ordinal);
|
||||
Assert.Contains("renderPublication.Origin", publicationSection, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_liveCenterX", publicationSection, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_liveCenterY", publicationSection, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static LandblockBuild EmptyBuild(uint landblockId, LandblockBuildOrigin origin) =>
|
||||
|
|
@ -316,15 +373,6 @@ public sealed class LandblockBuildOriginTests
|
|||
return results;
|
||||
}
|
||||
|
||||
private static string Slice(string source, string startMarker, string endMarker)
|
||||
{
|
||||
int start = source.IndexOf(startMarker, StringComparison.Ordinal);
|
||||
Assert.True(start >= 0, $"Missing source marker: {startMarker}");
|
||||
int end = source.IndexOf(endMarker, start, StringComparison.Ordinal);
|
||||
Assert.True(end > start, $"Missing source marker: {endMarker}");
|
||||
return source[start..end];
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
string? dir = AppContext.BaseDirectory;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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;
|
||||
|
|
@ -47,7 +48,7 @@ public sealed class LandblockPresentationPipelineTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ControllerConcreteConstructorHasNoLegacyPresentationParameters()
|
||||
public void PublicPresentationConstructorsExposeOnlyConcreteOwnerGraph()
|
||||
{
|
||||
string[] legacyNames =
|
||||
[
|
||||
|
|
@ -59,14 +60,595 @@ public sealed class LandblockPresentationPipelineTests
|
|||
"retirementCoordinator",
|
||||
];
|
||||
|
||||
System.Reflection.ConstructorInfo concrete = Assert.Single(
|
||||
typeof(StreamingController).GetConstructors(),
|
||||
constructor => constructor.GetParameters().Any(
|
||||
parameter => parameter.Name == "presentationPipeline"));
|
||||
string[] names = concrete.GetParameters()
|
||||
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]
|
||||
|
|
@ -715,9 +1297,10 @@ public sealed class LandblockPresentationPipelineTests
|
|||
envCells);
|
||||
}
|
||||
|
||||
private static WorldEntity Entity(uint id) => new()
|
||||
private static WorldEntity Entity(uint id, uint serverGuid = 0) => new()
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = serverGuid,
|
||||
SourceGfxObjOrSetupId = 0x01000000u + id,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue