acdream/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs
Erik 826f9ea9b5 refactor(app): complete session startup composition
Move the live-session reset and routing graph, combat and diagnostic command targets, and the sole gameplay input subscriber into Phase 7 before frame publication. Add exact retryable ownership for late bindings so partial startup cannot strand session or component teardown edges.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 18:49:31 +02:00

218 lines
7.2 KiB
C#

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 TransferableAdoptionRollsBackExactlyAndRetriesFailure()
{
var calls = new List<string>();
var bindings = new LivePresentationRuntimeBindings();
bindings.Adopt("stable", new RetryBinding("stable", calls, 0));
IDisposable adoption = bindings.AdoptOwned(
"candidate",
new RetryBinding("candidate", calls, 1));
Assert.Throws<InvalidOperationException>(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<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.");
}
}