refactor(app): compose live presentation startup
This commit is contained in:
parent
aa6ffa5176
commit
88f32dc4e2
23 changed files with 1767 additions and 626 deletions
|
|
@ -64,6 +64,36 @@ public sealed class AnimationHookRegistrationSetTests
|
|||
registrations.Register(new Sink("late")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OwnedRegistrationReleasesExactlyAndRetriesItsOwnFailure()
|
||||
{
|
||||
int adds = 0;
|
||||
int removes = 0;
|
||||
bool fail = true;
|
||||
var sink = new Sink("owned");
|
||||
var registrations = new AnimationHookRegistrationSet(
|
||||
_ => adds++,
|
||||
_ =>
|
||||
{
|
||||
removes++;
|
||||
if (fail)
|
||||
throw new InvalidOperationException("retry");
|
||||
});
|
||||
|
||||
IDisposable binding = registrations.RegisterOwned(sink);
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
registrations.RegisterOwned(sink));
|
||||
Assert.Throws<InvalidOperationException>(binding.Dispose);
|
||||
fail = false;
|
||||
binding.Dispose();
|
||||
binding.Dispose();
|
||||
registrations.Dispose();
|
||||
|
||||
Assert.Equal(1, adds);
|
||||
Assert.Equal(2, removes);
|
||||
Assert.True(registrations.IsCleanupComplete);
|
||||
}
|
||||
|
||||
private sealed class Sink(string name) : IAnimationHookSink
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
using AcDream.App.Composition;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Composition;
|
||||
|
||||
public sealed class LivePresentationCompositionTests
|
||||
{
|
||||
[Fact]
|
||||
public void RuntimeBindingsReleaseInReverseAndRetryOnlyFailedEdges()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var bindings = new LivePresentationRuntimeBindings();
|
||||
var first = new RetryBinding("first", calls, failures: 0);
|
||||
var second = new RetryBinding("second", calls, failures: 1);
|
||||
bindings.Adopt("first", first);
|
||||
bindings.Adopt("second", second);
|
||||
|
||||
Assert.Throws<AggregateException>(bindings.Dispose);
|
||||
Assert.Equal(["second", "first"], calls);
|
||||
|
||||
bindings.Dispose();
|
||||
bindings.Dispose();
|
||||
|
||||
Assert.Equal(["second", "first", "second"], calls);
|
||||
Assert.Throws<ObjectDisposedException>(() =>
|
||||
bindings.Adopt("late", new RetryBinding("late", calls, 0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanonicalRuntimeSlotUsesExactOwnerBinding()
|
||||
{
|
||||
var slot = new LiveEntityRuntimeSlot();
|
||||
LiveEntityRuntime first = Runtime();
|
||||
LiveEntityRuntime second = Runtime();
|
||||
|
||||
IDisposable stale = slot.BindOwned(first);
|
||||
Assert.Same(first, slot.Current);
|
||||
Assert.Throws<InvalidOperationException>(() => slot.BindOwned(second));
|
||||
|
||||
stale.Dispose();
|
||||
IDisposable current = slot.BindOwned(second);
|
||||
stale.Dispose();
|
||||
Assert.Same(second, slot.Current);
|
||||
current.Dispose();
|
||||
Assert.Null(slot.Current);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MotionBindingCannotBeClearedByAStaleOwner()
|
||||
{
|
||||
var source = new DeferredLiveEntityMotionRuntimeBindings();
|
||||
var first = new MotionRuntime(1f);
|
||||
var second = new MotionRuntime(2f);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 1u,
|
||||
SourceGfxObjOrSetupId = 1u,
|
||||
Position = System.Numerics.Vector3.Zero,
|
||||
Rotation = System.Numerics.Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
|
||||
IDisposable stale = source.BindOwned(first);
|
||||
Assert.Equal(1f, source.GetSetupCylinder(1u, entity).Radius);
|
||||
Assert.Throws<InvalidOperationException>(() => source.BindOwned(second));
|
||||
|
||||
stale.Dispose();
|
||||
IDisposable current = source.BindOwned(second);
|
||||
stale.Dispose();
|
||||
Assert.Equal(2f, source.GetSetupCylinder(1u, entity).Radius);
|
||||
current.Dispose();
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
source.GetSetupCylinder(1u, entity));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LandblockLoadedBridgeIsInertUntilBoundAndDeactivationIsTerminal()
|
||||
{
|
||||
var source = new DeferredLiveEntityLandblockLoadedSink();
|
||||
var first = new LandblockSink();
|
||||
var second = new LandblockSink();
|
||||
|
||||
source.OnLandblockLoaded(1u);
|
||||
IDisposable stale = source.Bind(first);
|
||||
source.OnLandblockLoaded(2u);
|
||||
Assert.Equal([2u], first.Landblocks);
|
||||
Assert.Throws<InvalidOperationException>(() => source.Bind(second));
|
||||
|
||||
stale.Dispose();
|
||||
IDisposable current = source.Bind(second);
|
||||
stale.Dispose();
|
||||
source.OnLandblockLoaded(3u);
|
||||
Assert.Equal([3u], second.Landblocks);
|
||||
|
||||
source.Deactivate();
|
||||
current.Dispose();
|
||||
source.OnLandblockLoaded(4u);
|
||||
Assert.Equal([3u], second.Landblocks);
|
||||
Assert.Throws<ObjectDisposedException>(() => source.Bind(first));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GameWindowUsesLivePhaseAndContainsNoPhaseSixConstructionBody()
|
||||
{
|
||||
string root = FindRepoRoot();
|
||||
string window = File.ReadAllText(Path.Combine(
|
||||
root,
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
string phase = File.ReadAllText(Path.Combine(
|
||||
root,
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"LivePresentationComposition.cs"));
|
||||
|
||||
Assert.Contains("new LivePresentationCompositionPhase(", window);
|
||||
Assert.DoesNotContain("new AcDream.App.World.LiveEntityRuntime(", window);
|
||||
Assert.DoesNotContain("new AcDream.App.Rendering.Wb.WbDrawDispatcher(", window);
|
||||
Assert.DoesNotContain("new AcDream.App.Streaming.LandblockRenderPublisher(", window);
|
||||
Assert.DoesNotContain("_portalTunnelFallback.AcquirePrepared(", window);
|
||||
Assert.Contains("new LiveEntityRuntime(", phase);
|
||||
Assert.Contains("new WbDrawDispatcher(", phase);
|
||||
Assert.Contains("new LandblockRenderPublisher(", phase);
|
||||
Assert.Contains("d.PortalTunnelFallback.AcquirePrepared(", phase);
|
||||
}
|
||||
|
||||
private static LiveEntityRuntime Runtime() => new(
|
||||
new GpuWorldState(),
|
||||
new DelegateLiveEntityResourceLifecycle(static _ => { }, static _ => { }));
|
||||
|
||||
private sealed class MotionRuntime(float radius)
|
||||
: ILiveEntityMotionRuntimeBindings
|
||||
{
|
||||
public (float Radius, float Height) GetSetupCylinder(
|
||||
uint serverGuid,
|
||||
WorldEntity entity) => (radius, 1f);
|
||||
|
||||
public bool RouteServerMoveTo(
|
||||
MovementManager movement,
|
||||
uint cellId,
|
||||
WorldSession.EntityMotionUpdate update) => false;
|
||||
|
||||
public void StickToObjectFromWire(
|
||||
IPhysicsObjHost? host,
|
||||
uint targetGuid)
|
||||
{
|
||||
}
|
||||
|
||||
public void ClearTargetForHiddenEntity(uint serverGuid)
|
||||
{
|
||||
}
|
||||
|
||||
public IPhysicsObjHost? ResolvePhysicsHost(
|
||||
uint serverGuid) => null;
|
||||
}
|
||||
|
||||
private sealed class LandblockSink : ILiveEntityLandblockLoadedSink
|
||||
{
|
||||
public List<uint> Landblocks { get; } = [];
|
||||
public void OnLandblockLoaded(uint landblockId) => Landblocks.Add(landblockId);
|
||||
}
|
||||
|
||||
private sealed class RetryBinding(
|
||||
string name,
|
||||
List<string> calls,
|
||||
int failures) : IDisposable
|
||||
{
|
||||
private int _failures = failures;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
calls.Add(name);
|
||||
if (_failures > 0)
|
||||
{
|
||||
_failures--;
|
||||
throw new InvalidOperationException("retry");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||
while (directory is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||
return directory.FullName;
|
||||
directory = directory.Parent;
|
||||
}
|
||||
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ public sealed class GameWindowRenderLeafCompositionTests
|
|||
foreach (string identifier in removed)
|
||||
Assert.DoesNotContain(identifier, source, StringComparison.Ordinal);
|
||||
|
||||
Assert.Contains("new AcDream.App.Rendering.PaperdollFramePresenter(", source);
|
||||
Assert.Contains("new PaperdollFramePresenter(", LivePresentationSource());
|
||||
string settingsComposition = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
|
|
@ -193,13 +193,13 @@ public sealed class GameWindowRenderLeafCompositionTests
|
|||
[Fact]
|
||||
public void PaperdollComposition_SkipsEitherMissingOptionalUiSurface()
|
||||
{
|
||||
string source = GameWindowSource();
|
||||
string source = LivePresentationSource();
|
||||
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"PaperdollViewportWidget is { } paperdollViewport",
|
||||
"InventoryFrame is { } paperdollInventoryFrame",
|
||||
"new AcDream.App.Rendering.PaperdollFramePresenter(");
|
||||
"PaperdollViewportWidget is { } viewport",
|
||||
"InventoryFrame is { } inventoryFrame",
|
||||
"new PaperdollFramePresenter(");
|
||||
Assert.DoesNotContain("Paperdoll inventory frame is required.", source);
|
||||
}
|
||||
|
||||
|
|
@ -223,6 +223,13 @@ public sealed class GameWindowRenderLeafCompositionTests
|
|||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
|
||||
private static string LivePresentationSource() => File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"LivePresentationComposition.cs"));
|
||||
|
||||
private static string Source(string fileName) => File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
|
|
|
|||
|
|
@ -61,11 +61,9 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"InteractionRetainedUiResult interactionUi =",
|
||||
"new InteractionRetainedUiCompositionPhase(",
|
||||
"this).Compose(",
|
||||
"_liveEntities = new AcDream.App.World.LiveEntityRuntime(",
|
||||
"_selectionInteractions ??= new AcDream.App.Interaction.SelectionInteractionController(",
|
||||
"_retainedUiGameplayBinding =",
|
||||
"AcDream.App.Input.RetainedUiGameplayBinding.Create(",
|
||||
"_retainedUiGameplayBinding.Attach();",
|
||||
"LivePresentationResult livePresentation =",
|
||||
"new LivePresentationCompositionPhase(",
|
||||
"this).Compose(",
|
||||
"var renderFrameOrchestrator =",
|
||||
"var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(",
|
||||
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);",
|
||||
|
|
@ -193,11 +191,11 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"AcDream.App.Input.GameplayInputActionRouter.Create("));
|
||||
Assert.Equal(1, CountOccurrences(load, "_gameplayInputActions.Attach();"));
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
load,
|
||||
"AcDream.App.Input.RetainedUiGameplayBinding.Create("));
|
||||
LivePresentationSource(),
|
||||
"RetainedUiGameplayBinding.Create("));
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
load,
|
||||
"_retainedUiGameplayBinding.Attach();"));
|
||||
LivePresentationSource(),
|
||||
"retainedGameplayLease.Resource.Attach();"));
|
||||
AssertAppearsInOrder(
|
||||
shutdown,
|
||||
"_liveCombatModeCommands.Deactivate",
|
||||
|
|
@ -377,10 +375,9 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
source,
|
||||
"private ResourceShutdownTransaction CreateShutdownTransaction()",
|
||||
"private void OnFocusChanged(bool focused)");
|
||||
string dispose = Slice(
|
||||
source,
|
||||
"public void Dispose()",
|
||||
"private sealed class NullAnimLoader");
|
||||
int disposeStart = source.IndexOf("public void Dispose()", StringComparison.Ordinal);
|
||||
Assert.True(disposeStart >= 0);
|
||||
string dispose = source[disposeStart..];
|
||||
|
||||
string[] stages =
|
||||
[
|
||||
|
|
@ -474,14 +471,18 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"AcDream.App",
|
||||
"Composition",
|
||||
"WorldRenderComposition.cs"));
|
||||
string livePhase = LivePresentationSource();
|
||||
|
||||
AssertAppearsInOrder(
|
||||
livePhase,
|
||||
"d.PortalTunnelFallback.AcquirePrepared(",
|
||||
"static tunnel => tunnel.PrepareResources());",
|
||||
"d.RenderResourceLifetime.AcquireSkyShader(",
|
||||
"() => new SkyRenderer(");
|
||||
AssertAppearsInOrder(
|
||||
load,
|
||||
"new WorldRenderCompositionPhase(",
|
||||
"_portalTunnelFallback.AcquirePrepared(",
|
||||
"static portalTunnel => portalTunnel.PrepareResources());",
|
||||
"_renderResourceLifetime.AcquireSkyShader(",
|
||||
"_skyRenderer = new AcDream.App.Rendering.Sky.SkyRenderer(",
|
||||
"new LivePresentationCompositionPhase(",
|
||||
"_portalTunnelFallback.Transfer(",
|
||||
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);");
|
||||
AssertAppearsInOrder(
|
||||
|
|
@ -566,6 +567,13 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
|
||||
private static string LivePresentationSource() => File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"LivePresentationComposition.cs"));
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||
|
|
|
|||
|
|
@ -241,6 +241,12 @@ public sealed class LandblockBuildOriginTests
|
|||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
string livePresentationSource = File.ReadAllText(Path.Combine(
|
||||
root,
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"LivePresentationComposition.cs"));
|
||||
string buildSource = File.ReadAllText(Path.Combine(
|
||||
root,
|
||||
"src",
|
||||
|
|
@ -307,16 +313,16 @@ public sealed class LandblockBuildOriginTests
|
|||
gameWindowSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"new AcDream.App.Streaming.LandblockRenderPublisher(",
|
||||
gameWindowSource,
|
||||
"new LandblockRenderPublisher(",
|
||||
livePresentationSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"new AcDream.App.Streaming.LandblockPhysicsPublisher(",
|
||||
gameWindowSource,
|
||||
"new LandblockPhysicsPublisher(",
|
||||
livePresentationSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"new AcDream.App.Streaming.LandblockStaticPresentationPublisher(",
|
||||
gameWindowSource,
|
||||
"new LandblockStaticPresentationPublisher(",
|
||||
livePresentationSource,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("applyTerrain:", gameWindowSource, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("demoteNearLayer:", gameWindowSource, StringComparison.Ordinal);
|
||||
|
|
|
|||
|
|
@ -158,6 +158,12 @@ public sealed class GameWindowLiveEntityCompositionTests
|
|||
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs"));
|
||||
string livePresentation = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"LivePresentationComposition.cs"));
|
||||
Assert.DoesNotContain(
|
||||
"new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(() =>",
|
||||
source,
|
||||
|
|
@ -166,7 +172,7 @@ public sealed class GameWindowLiveEntityCompositionTests
|
|||
"new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(() =>",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("new AcDream.App.Physics.DatProjectileSetupResolver", source);
|
||||
Assert.Contains("new DatProjectileSetupResolver", livePresentation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -337,6 +337,12 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
string livePresentation = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"LivePresentationComposition.cs"));
|
||||
|
||||
Assert.Contains("new AcDream.App.Update.LiveObjectFrameController(", source);
|
||||
Assert.Contains("new AcDream.App.Update.LiveSpatialPresentationReconciler(", source);
|
||||
|
|
@ -771,6 +777,12 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
string livePresentation = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"LivePresentationComposition.cs"));
|
||||
Assert.DoesNotContain("PublishLocalPhysicsTimestamps", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LoginWorldReady", source,
|
||||
|
|
@ -783,9 +795,9 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("OnPlayScriptReceived", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_liveWorldOrigin.GetCenter", source,
|
||||
Assert.Contains("d.WorldOrigin.GetCenter", livePresentation,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_liveWorldOrigin.CellLocalForSeed", source,
|
||||
Assert.Contains("d.WorldOrigin.CellLocalForSeed", livePresentation,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_liveEntitySessionEvents.CreateSink()", source,
|
||||
StringComparison.Ordinal);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue