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)); 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); } 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."); } }