refactor(app): compose live presentation startup
This commit is contained in:
parent
aa6ffa5176
commit
88f32dc4e2
23 changed files with 1767 additions and 626 deletions
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue