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(); 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(bindings.Dispose); Assert.Equal(["second", "first"], calls); bindings.Dispose(); bindings.Dispose(); Assert.Equal(["second", "first", "second"], calls); Assert.Throws(() => bindings.Adopt("late", new RetryBinding("late", calls, 0))); } [Fact] public void TransferableAdoptionRollsBackExactlyAndRetriesFailure() { var calls = new List(); var bindings = new LivePresentationRuntimeBindings(); bindings.Adopt("stable", new RetryBinding("stable", calls, 0)); IDisposable adoption = bindings.AdoptOwned( "candidate", new RetryBinding("candidate", calls, 1)); Assert.Throws(adoption.Dispose); adoption.Dispose(); adoption.Dispose(); bindings.Dispose(); Assert.Equal(["candidate", "candidate", "stable"], calls); } [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(() => 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(), }; IDisposable stale = source.BindOwned(first); Assert.Equal(1f, source.GetSetupCylinder(1u, entity).Radius); Assert.Throws(() => source.BindOwned(second)); stale.Dispose(); IDisposable current = source.BindOwned(second); stale.Dispose(); Assert.Equal(2f, source.GetSetupCylinder(1u, entity).Radius); current.Dispose(); Assert.Throws(() => 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(() => 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(() => 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 Landblocks { get; } = []; public void OnLandblockLoaded(uint landblockId) => Landblocks.Add(landblockId); } private sealed class RetryBinding( string name, List 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."); } }