using System.Reflection; using AcDream.App.Rendering; using AcDream.App.Update; using AcDream.App.World; namespace AcDream.App.Tests.World; public sealed class UpdateFrameOrchestratorTests { [Fact] public void Tick_PreservesTheCompleteAcceptedPhaseGraph() { var calls = new List(); UpdateFrameOrchestrator frame = Create(calls); frame.Tick(new UpdateFrameInput(1.0 / 60.0)); Assert.Equal( [ "teardown", "clock", "streaming", "input", "objects", "network", "commands", "ordinary-reconcile", "liveness", "teleport", "auto-entry", "camera", ], calls); } [Fact] public void ConditionalReconciles_RemainAtTheirOwningEdges() { // Checkpoint A freezes only the outer ownership contract. Checkpoints // E and F must replace these probes with production teleport/camera // owner tests before the conditional edges can be claimed guarded. var calls = new List(); UpdateFrameOrchestrator frame = Create( calls, teleportPlace: true, inboundCreatedPlayer: true); frame.Tick(new UpdateFrameInput(1.0 / 60.0)); Assert.Equal( [ "teardown", "clock", "streaming", "input", "objects", "network", "commands", "ordinary-reconcile", "liveness", "teleport-place", "teleport-reconcile", "teleport-reveal", "auto-entry", "inbound-player-projection", "inbound-player-reconcile", "camera", ], calls); } [Theory] [InlineData(double.NaN)] [InlineData(double.NegativeInfinity)] [InlineData(double.PositiveInfinity)] [InlineData(-1.0)] [InlineData(0.0)] [InlineData(double.MaxValue)] public void InvalidSimulationDelta_BecomesZeroWithoutSuppressingOuterPhases( double hostDelta) { var calls = new List(); var observed = new FrameObservations(); UpdateFrameOrchestrator frame = Create(calls, observed: observed); frame.Tick(new UpdateFrameInput(hostDelta)); Assert.Equal( [ "teardown", "clock", "streaming", "input", "objects", "network", "commands", "ordinary-reconcile", "liveness", "teleport", "auto-entry", "camera", ], calls); Assert.Equal([0.0], observed.PublishedTimes); Assert.Equal([0.0], observed.Input.Select(value => value.SimulationDeltaSeconds)); Assert.Equal([0f], observed.LiveDeltas); Assert.Equal([0f], observed.TeleportDeltas); Assert.Equal([0.0], observed.Camera.Select(value => value.SimulationDeltaSeconds)); Assert.All( observed.Input.Concat(observed.Camera), timing => Assert.Equal(0.0, timing.ScriptTime)); } [Fact] public void Clock_AdvancesOnceAndPublishesTheSameTimeToEveryConsumer() { var calls = new List(); var observed = new FrameObservations(); UpdateFrameOrchestrator frame = Create(calls, observed: observed); frame.Tick(new UpdateFrameInput(0.25)); frame.Tick(new UpdateFrameInput(double.NaN)); frame.Tick(new UpdateFrameInput(0.5)); Assert.Equal([0.25, 0.25, 0.75], observed.PublishedTimes); Assert.Equal( [0.25, 0.25, 0.75], observed.Input.Select(value => value.ScriptTime)); Assert.Equal( [0.25, 0.25, 0.75], observed.Camera.Select(value => value.ScriptTime)); Assert.Equal([0.25f, 0f, 0.5f], observed.LiveDeltas); Assert.Equal([0.25f, 0f, 0.5f], observed.TeleportDeltas); Assert.Equal( [0.25, 0.0, 0.5], observed.Input.Select(value => value.SimulationDeltaSeconds)); Assert.Equal( [0.25, 0.0, 0.5], observed.Camera.Select(value => value.SimulationDeltaSeconds)); } [Fact] public void Clock_RejectsFiniteDeltaAboveTheSinglePrecisionConsumerRange() { double tooLarge = (double)float.MaxValue * 2.0; Assert.True(double.IsFinite(tooLarge)); Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(tooLarge)); } [Fact] public void AggregateTeardownFailure_IsReportedAndRetriedNextFrame() { var calls = new List(); var teardown = new RecordingTeardown(calls) { Failure = new AggregateException(new InvalidOperationException("transient")), }; var failures = new RecordingFailureSink(); UpdateFrameOrchestrator frame = Create( calls, teardown: teardown, failureSink: failures); frame.Tick(new UpdateFrameInput(0.1)); teardown.Failure = null; frame.Tick(new UpdateFrameInput(0.1)); Assert.Equal(2, teardown.Attempts); Assert.Single(failures.Errors); string[] oneFrame = [ "teardown", "clock", "streaming", "input", "objects", "network", "commands", "ordinary-reconcile", "liveness", "teleport", "auto-entry", "camera", ]; Assert.Equal(oneFrame.Concat(oneFrame), calls); } [Fact] public void NonAggregateTeardownFailure_PropagatesBeforeTimeOrLaterPhases() { var calls = new List(); var teardown = new RecordingTeardown(calls) { Failure = new InvalidOperationException("fatal"), }; UpdateFrameOrchestrator frame = Create(calls, teardown: teardown); InvalidOperationException error = Assert.Throws( () => frame.Tick(new UpdateFrameInput(0.1))); Assert.Equal("fatal", error.Message); Assert.Equal(["teardown"], calls); } [Fact] public void OrchestrationTypes_UseOnlyTheExplicitTypedOwnerGraph() { Type[] expectedFieldTypes = [ typeof(IUpdateFrameTeardownPhase), typeof(IUpdateFrameFailureSink), typeof(UpdateFrameClock), typeof(IUpdateFrameScriptClockPublisher), typeof(IStreamingFramePhase), typeof(IGameplayInputFramePhase), typeof(IRetailLiveFramePhase), typeof(ILiveEntityLivenessFramePhase), typeof(ILocalPlayerTeleportFramePhase), typeof(IPlayerModeAutoEntryFramePhase), typeof(ICameraFramePhase), ]; FieldInfo[] fields = typeof(UpdateFrameOrchestrator).GetFields( BindingFlags.Instance | BindingFlags.NonPublic); Assert.Equal( expectedFieldTypes.OrderBy(type => type.FullName), fields.Select(field => field.FieldType).OrderBy(type => type.FullName)); Assert.All(fields, field => { Assert.NotEqual(typeof(GameWindow), field.FieldType); Assert.False(typeof(Delegate).IsAssignableFrom(field.FieldType)); }); Type[] phaseInterfaces = expectedFieldTypes.Where(type => type.IsInterface).ToArray(); Assert.DoesNotContain( phaseInterfaces, phaseInterface => phaseInterface.IsAssignableFrom(typeof(GameWindow))); Type[] productionPhaseOwners = typeof(GameWindow).Assembly.GetTypes() .Where(type => !type.IsAbstract && !type.IsInterface) .Where(type => phaseInterfaces.Any(contract => contract.IsAssignableFrom(type))) .ToArray(); foreach (Type owner in productionPhaseOwners) { FieldInfo[] ownerFields = owner.GetFields( BindingFlags.Instance | BindingFlags.NonPublic); Assert.DoesNotContain(ownerFields, field => field.FieldType == typeof(GameWindow)); if (owner == typeof(AcDream.App.Input.RetailLocalPlayerFrameController)) { // Checkpoint B deliberately leaves this single legacy callback // composition for D/F, which own input capture and the mutable // player-mode/controller slot. No other phase owner may add one. Assert.Contains( ownerFields, field => typeof(Delegate).IsAssignableFrom(field.FieldType)); continue; } Assert.DoesNotContain( ownerFields, field => typeof(Delegate).IsAssignableFrom(field.FieldType)); } Assert.DoesNotContain( typeof(GameWindow).GetFields(BindingFlags.Instance | BindingFlags.NonPublic), field => field.Name == "_physicsScriptGameTime"); } [Fact] public void PhysicsScriptClockConsumers_RetainTheTypedSourceNotGameWindowClosures() { Type[] consumers = [ typeof(DatLiveEntityProjectionMaterializer), typeof(AcDream.App.Physics.LiveEntityNetworkUpdateController), ]; foreach (Type consumer in consumers) { FieldInfo clock = Assert.Single( consumer.GetFields(BindingFlags.Instance | BindingFlags.NonPublic), field => field.Name == "_gameTime"); Assert.Equal(typeof(IPhysicsScriptTimeSource), clock.FieldType); Assert.DoesNotContain( consumer.GetFields(BindingFlags.Instance | BindingFlags.NonPublic), field => field.FieldType == typeof(GameWindow)); } Assert.False(typeof(IPhysicsScriptTimeSource).IsAssignableFrom(typeof(GameWindow))); } [Fact] public void ProductionFrame_PublishesPhysicsScriptTimeExactlyOnce() { string source = File.ReadAllText(Path.Combine( FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs")); Assert.Equal(1, source.Split("PublishTime(", StringSplitOptions.None).Length - 1); } [Fact] public void ExtractedLiveObjectSource_PinsRetailAndRegisteredAdaptationOrder() { string source = File.ReadAllText(Path.Combine( FindRepoRoot(), "src", "AcDream.App", "Update", "LiveObjectFrameController.cs")); AssertAppearsInOrder( source, "_localPlayerFrame.AdvanceBeforeNetwork", "_selectionInteractions?.DrainOutbound", "_animations.Tick", "_staticAnimations.Tick", "_animationPresenter.Present", "_equippedChildren.Tick", "_staticAnimations.ProcessHooks", "_effects.Tick"); AssertAppearsInOrder( source, "_translucencyFades.AdvanceAll", "_animationHooks.Drain", "_entityEffects.RefreshLiveOwnerPoses", "_particleSink.RefreshAttachedEmitters", "_lights.Refresh", "_particleVisibility.Apply", "_particles.Tick", "_scripts.Tick"); AssertAppearsInOrder( source, "public void Reconcile()", "_entityEffects.RefreshLiveOwnerPoses", "_equippedChildren.Tick", "_particleSink.RefreshAttachedEmitters", "_lights.Refresh"); Assert.Equal(1, CountOccurrences(source, "_staticAnimations.Tick(")); Assert.Equal(1, CountOccurrences(source, "_staticAnimations.ProcessHooks(")); Assert.Equal(1, CountOccurrences(source, "_effects.Tick(")); Assert.Equal(1, CountOccurrences(source, "_particles.Tick(")); Assert.Equal(1, CountOccurrences(source, "_scripts.Tick(")); } [Fact] public void GameWindow_ComposesTheLiveFrameOwnersWithoutOwningTheirBodies() { string source = File.ReadAllText(Path.Combine( FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs")); Assert.Contains("new AcDream.App.Update.LiveObjectFrameController(", source); Assert.Contains("new AcDream.App.Update.LiveSpatialPresentationReconciler(", source); Assert.Contains("new AcDream.App.World.RetailLiveFrameCoordinator(", source); Assert.DoesNotContain("AdvanceLiveObjectRuntime", source, StringComparison.Ordinal); Assert.DoesNotContain("ReconcileLiveObjectSpatialPresentation", source, StringComparison.Ordinal); Assert.DoesNotContain("ILiveAnimationPresentationContext", source, StringComparison.Ordinal); } private static UpdateFrameOrchestrator Create( List calls, RecordingTeardown? teardown = null, RecordingFailureSink? failureSink = null, FrameObservations? observed = null, bool teleportPlace = false, bool inboundCreatedPlayer = false) { teardown ??= new RecordingTeardown(calls); failureSink ??= new RecordingFailureSink(); return new UpdateFrameOrchestrator( teardown, failureSink, new UpdateFrameClock(), new RecordingClockPublisher(calls, observed), new RecordingStreaming(calls), new RecordingInput(calls, observed), new RecordingLiveFrame(calls, observed), new RecordingLiveness(calls), new RecordingTeleport(calls, observed, teleportPlace), new RecordingAutoEntry(calls), new RecordingCamera(calls, observed, inboundCreatedPlayer)); } private sealed class RecordingTeardown(List calls) : IUpdateFrameTeardownPhase { public Exception? Failure { get; set; } public int Attempts { get; private set; } public void RetryPendingTeardowns() { calls.Add("teardown"); Attempts++; if (Failure is not null) throw Failure; } } private sealed class RecordingFailureSink : IUpdateFrameFailureSink { public List Errors { get; } = []; public void ReportTeardownFailure(AggregateException error) => Errors.Add(error); } private sealed class RecordingClockPublisher( List calls, FrameObservations? observed) : IUpdateFrameScriptClockPublisher { public void PublishTime(double scriptTime) { calls.Add("clock"); observed?.PublishedTimes.Add(scriptTime); } } private sealed class RecordingStreaming(List calls) : IStreamingFramePhase { public void Tick() => calls.Add("streaming"); } private sealed class RecordingInput( List calls, FrameObservations? observed) : IGameplayInputFramePhase { public void Tick(UpdateFrameTiming timing) { calls.Add("input"); observed?.Input.Add(timing); } } private sealed class RecordingLiveFrame( List calls, FrameObservations? observed) : IRetailLiveFramePhase { public void Tick(float deltaSeconds) { observed?.LiveDeltas.Add(deltaSeconds); calls.Add("objects"); calls.Add("network"); calls.Add("commands"); calls.Add("ordinary-reconcile"); } } private sealed class RecordingLiveness(List calls) : ILiveEntityLivenessFramePhase { public void Tick() => calls.Add("liveness"); } private sealed class RecordingTeleport( List calls, FrameObservations? observed, bool place) : ILocalPlayerTeleportFramePhase { public void Tick(float deltaSeconds) { observed?.TeleportDeltas.Add(deltaSeconds); if (!place) { calls.Add("teleport"); return; } calls.Add("teleport-place"); calls.Add("teleport-reconcile"); calls.Add("teleport-reveal"); } } private sealed class RecordingAutoEntry(List calls) : IPlayerModeAutoEntryFramePhase { public void TryEnter() => calls.Add("auto-entry"); } private sealed class RecordingCamera( List calls, FrameObservations? observed, bool inboundCreatedPlayer) : ICameraFramePhase { public void Tick(UpdateFrameTiming timing) { if (inboundCreatedPlayer) { calls.Add("inbound-player-projection"); calls.Add("inbound-player-reconcile"); } calls.Add("camera"); observed?.Camera.Add(timing); } } private sealed class FrameObservations { public List PublishedTimes { get; } = []; public List Input { get; } = []; public List LiveDeltas { get; } = []; public List TeleportDeltas { get; } = []; public List Camera { get; } = []; } 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."); } private static void AssertAppearsInOrder(string source, params string[] markers) { int previous = -1; foreach (string marker in markers) { int current = source.IndexOf(marker, previous + 1, StringComparison.Ordinal); Assert.True(current >= 0, $"Missing source marker: {marker}"); Assert.True(current > previous, $"Out-of-order source marker: {marker}"); previous = current; } } private static int CountOccurrences(string source, string marker) => source.Split(marker, StringSplitOptions.None).Length - 1; }