using System.Runtime.CompilerServices; using AcDream.App.Combat; using AcDream.App.Composition; using AcDream.App.Diagnostics; using AcDream.App.Rendering; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.App.World; namespace AcDream.App.Tests.Composition; public sealed class InteractionRetainedUiCompositionTests { private static readonly InteractionRetainedUiCompositionPoint[] UiPoints = [ InteractionRetainedUiCompositionPoint.UiHostAcquired, InteractionRetainedUiCompositionPoint.InputCaptureBound, InteractionRetainedUiCompositionPoint.CursorAssetsCreated, InteractionRetainedUiCompositionPoint.CharacterSheetCreated, InteractionRetainedUiCompositionPoint.MagicRuntimeCreated, InteractionRetainedUiCompositionPoint.MouseInputWired, InteractionRetainedUiCompositionPoint.KeyboardInputWired, InteractionRetainedUiCompositionPoint.UiAssetsCreated, InteractionRetainedUiCompositionPoint.UiProbeCreated, InteractionRetainedUiCompositionPoint.UiRuntimeMounted, InteractionRetainedUiCompositionPoint.InventoryContainerBound, ]; [Fact] public void EnabledUiPublishesOneExactResultAfterFrozenConstructionOrder() { var fixture = new Fixture(retailUi: true); InteractionRetainedUiResult result = fixture.Compose(); Assert.Same(result, fixture.Publication.Result); Assert.Equal( [ InteractionRetainedUiCompositionPoint.LateBindingsCreated, InteractionRetainedUiCompositionPoint.CombatAttackCreated, InteractionRetainedUiCompositionPoint.CombatTargetCreated, InteractionRetainedUiCompositionPoint.ExternalContainerLifecycleCreated, InteractionRetainedUiCompositionPoint.ItemInteractionCreated, .. UiPoints, InteractionRetainedUiCompositionPoint.ResultPublished, ], fixture.Points); Assert.NotNull(result.RetainedUi); Assert.Empty(fixture.Factory.Releases); } [Fact] public void DisabledUiAcquiresNoRetainedFrontendResource() { var fixture = new Fixture(retailUi: false); InteractionRetainedUiResult result = fixture.Compose(); Assert.Null(result.RetainedUi); Assert.Equal( [ InteractionRetainedUiCompositionPoint.LateBindingsCreated, InteractionRetainedUiCompositionPoint.CombatAttackCreated, InteractionRetainedUiCompositionPoint.CombatTargetCreated, InteractionRetainedUiCompositionPoint.ExternalContainerLifecycleCreated, InteractionRetainedUiCompositionPoint.ItemInteractionCreated, InteractionRetainedUiCompositionPoint.RetainedUiDisabled, InteractionRetainedUiCompositionPoint.ResultPublished, ], fixture.Points); Assert.Equal(0, fixture.Factory.RetainedUiCalls); } [Theory] [MemberData(nameof(EnabledFailurePoints))] public void FaultAtEveryEnabledBoundaryStopsSuffixAndRollsBackUnpublishedPrefix( int pointValue) { var point = (InteractionRetainedUiCompositionPoint)pointValue; var fixture = new Fixture(retailUi: true, failurePoint: point); Assert.Throws(fixture.Compose); Assert.Equal(point, fixture.Points[^1]); if (point == InteractionRetainedUiCompositionPoint.ResultPublished) { Assert.Empty(fixture.Factory.Releases); Assert.NotNull(fixture.Publication.Result); } else { Assert.Null(fixture.Publication.Result); Assert.Equal(ExpectedRollback(point), fixture.Factory.Releases); } } public static TheoryData EnabledFailurePoints() { var data = new TheoryData(); foreach (InteractionRetainedUiCompositionPoint point in Enum.GetValues()) { if (point != InteractionRetainedUiCompositionPoint.RetainedUiDisabled) data.Add((int)point); } return data; } private static string[] ExpectedRollback( InteractionRetainedUiCompositionPoint point) { var acquired = new List { "late bindings" }; if (point >= InteractionRetainedUiCompositionPoint.CombatAttackCreated) acquired.Add("combat attack"); if (point >= InteractionRetainedUiCompositionPoint.CombatTargetCreated) acquired.Add("combat target"); if (point >= InteractionRetainedUiCompositionPoint.ExternalContainerLifecycleCreated) acquired.Add("external container"); if (point >= InteractionRetainedUiCompositionPoint.ItemInteractionCreated) acquired.Add("item interaction"); if (point >= InteractionRetainedUiCompositionPoint.UiHostAcquired) acquired.Add("retained UI lease"); acquired.Reverse(); return acquired.ToArray(); } [Fact] public void PublicationFailureRollsBackCompleteUnpublishedPrefix() { var fixture = new Fixture(retailUi: true, publicationFailure: true); Assert.Throws(fixture.Compose); Assert.Equal( [ "retained UI lease", "item interaction", "external container", "combat target", "combat attack", "late bindings", ], fixture.Factory.Releases); } [Fact] public void GameWindowUsesPhaseAndContainsNoRetainedUiConstructionBody() { string source = File.ReadAllText(Path.Combine( FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs")); Assert.Contains("new InteractionRetainedUiCompositionPhase(", source); Assert.DoesNotContain("new AcDream.App.UI.ItemInteractionController(", source); Assert.DoesNotContain("_retailUiLease.AcquireHost(", source); Assert.DoesNotContain("RetailUiRuntime.CreateUninitialized(", source); Assert.DoesNotContain("private void UseItemByGuid(", source); Assert.DoesNotContain("private uint? PickWorldGuidAtCursor(", source); } private sealed class Fixture { private readonly InteractionRetainedUiCompositionPoint? _failurePoint; public Fixture( bool retailUi, InteractionRetainedUiCompositionPoint? failurePoint = null, bool publicationFailure = false) { _failurePoint = failurePoint; Factory = new FakeFactory(); Publication = new Publication(publicationFailure); RuntimeOptions options = RuntimeOptions.Parse("dat", static _ => null) with { RetailUi = retailUi }; Dependencies = new InteractionRetainedUiDependencies( Options: options, Gl: null!, Window: null!, Input: null!, ShadersDirectory: "shaders", Dats: null!, DatLock: new object(), TextureCache: null!, DebugFont: null, HostQuiescence: null!, RetainedInputCapture: null!, InputDispatcher: null, Settings: null!, Combat: null!, CombatAttackOperations: null!, Selection: null!, ExternalContainers: null!, Objects: null!, MagicCatalog: null!, Spellbook: null!, Chat: null!, LocalPlayer: null!, ItemMana: null!, StackSplitQuantity: null!, UiRegistry: null, CombatModeCommands: null!, PlayerIdentity: null!, PlayerController: null!, PlayerMode: null!, CharacterOptions: null!, Shortcuts: null!, SelectionCameraFactory: static _ => Stub(), FrameDiagnostics: new DeferredRenderFrameDiagnosticsSource(), ExistingVitals: null, Toast: null, ClientTime: static () => 0d, Log: static _ => { }); } public InteractionRetainedUiDependencies Dependencies { get; } public FakeFactory Factory { get; } public Publication Publication { get; } public List Points { get; } = []; public InteractionRetainedUiResult Compose() => new InteractionRetainedUiCompositionPhase( Dependencies, new RetailUiRuntimeLease(), Publication, Factory, point => { Points.Add(point); if (_failurePoint == point) throw new InvalidOperationException($"fault at {point}"); }).Compose(); } private sealed class FakeFactory : IInteractionRetainedUiCompositionFactory { private readonly Dictionary _names = new(ReferenceEqualityComparer.Instance); public List Releases { get; } = []; public int RetainedUiCalls { get; private set; } public CombatAttackController CreateCombatAttack( InteractionRetainedUiDependencies dependencies) => Resource("combat attack"); public CombatTargetController CreateCombatTarget( InteractionRetainedUiDependencies dependencies, DeferredSelectionUiAuthority selection) => Resource("combat target"); public ExternalContainerLifecycleController CreateExternalContainerLifecycle( InteractionRetainedUiDependencies dependencies, DeferredLiveSessionUiAuthority session) => Resource("external container"); public ItemInteractionController CreateItemInteraction( InteractionRetainedUiDependencies dependencies, InteractionUiLateBindings lateBindings) => Resource("item interaction"); public RetainedUiComposition CreateRetainedUi( InteractionRetainedUiDependencies dependencies, InteractionUiLateBindings lateBindings, RetailUiRuntimeLease lease, CombatAttackController combatAttack, ItemInteractionController itemInteraction, Action checkpoint) { RetainedUiCalls++; _names.Add(lease, "retained UI lease"); foreach (InteractionRetainedUiCompositionPoint point in UiPoints) checkpoint(point); return new RetainedUiComposition( Stub(), Stub(), Stub(), Stub(), Stub(), Stub(), null); } public void Release(IDisposable resource) { string name = _names.TryGetValue(resource, out string? found) ? found : resource switch { InteractionUiLateBindings => "late bindings", RetailUiRuntimeLease => "retained UI lease", _ => throw new InvalidOperationException( $"Unknown test resource {resource.GetType().Name}"), }; Releases.Add(name); } private T Resource(string name) where T : class { T value = Stub(); _names.Add(value, name); return value; } } private sealed class Publication(bool fail) : IGameWindowInteractionRetainedUiPublication { public InteractionRetainedUiResult? Result { get; private set; } public void PublishInteractionRetainedUi(InteractionRetainedUiResult result) { if (fail) throw new InvalidOperationException("publication failed"); Result = result; } } private static T Stub() where T : class => (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); 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."); } }