refactor(app): compose interaction and retained UI startup
This commit is contained in:
parent
f663b04a54
commit
aa6ffa5176
15 changed files with 2157 additions and 467 deletions
|
|
@ -0,0 +1,332 @@
|
|||
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<InvalidOperationException>(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<int> EnabledFailurePoints()
|
||||
{
|
||||
var data = new TheoryData<int>();
|
||||
foreach (InteractionRetainedUiCompositionPoint point in
|
||||
Enum.GetValues<InteractionRetainedUiCompositionPoint>())
|
||||
{
|
||||
if (point != InteractionRetainedUiCompositionPoint.RetainedUiDisabled)
|
||||
data.Add((int)point);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private static string[] ExpectedRollback(
|
||||
InteractionRetainedUiCompositionPoint point)
|
||||
{
|
||||
var acquired = new List<string> { "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<InvalidOperationException>(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<SelectionCameraSource>(),
|
||||
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<InteractionRetainedUiCompositionPoint> 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<object, string> _names =
|
||||
new(ReferenceEqualityComparer.Instance);
|
||||
|
||||
public List<string> Releases { get; } = [];
|
||||
public int RetainedUiCalls { get; private set; }
|
||||
|
||||
public CombatAttackController CreateCombatAttack(
|
||||
InteractionRetainedUiDependencies dependencies) =>
|
||||
Resource<CombatAttackController>("combat attack");
|
||||
|
||||
public CombatTargetController CreateCombatTarget(
|
||||
InteractionRetainedUiDependencies dependencies,
|
||||
DeferredSelectionUiAuthority selection) =>
|
||||
Resource<CombatTargetController>("combat target");
|
||||
|
||||
public ExternalContainerLifecycleController CreateExternalContainerLifecycle(
|
||||
InteractionRetainedUiDependencies dependencies,
|
||||
DeferredLiveSessionUiAuthority session) =>
|
||||
Resource<ExternalContainerLifecycleController>("external container");
|
||||
|
||||
public ItemInteractionController CreateItemInteraction(
|
||||
InteractionRetainedUiDependencies dependencies,
|
||||
InteractionUiLateBindings lateBindings) =>
|
||||
Resource<ItemInteractionController>("item interaction");
|
||||
|
||||
public RetainedUiComposition CreateRetainedUi(
|
||||
InteractionRetainedUiDependencies dependencies,
|
||||
InteractionUiLateBindings lateBindings,
|
||||
RetailUiRuntimeLease lease,
|
||||
CombatAttackController combatAttack,
|
||||
ItemInteractionController itemInteraction,
|
||||
Action<InteractionRetainedUiCompositionPoint> checkpoint)
|
||||
{
|
||||
RetainedUiCalls++;
|
||||
_names.Add(lease, "retained UI lease");
|
||||
foreach (InteractionRetainedUiCompositionPoint point in UiPoints)
|
||||
checkpoint(point);
|
||||
return new RetainedUiComposition(
|
||||
Stub<UiHost>(),
|
||||
Stub<RetailUiRuntime>(),
|
||||
Stub<AcDream.UI.Abstractions.Panels.Vitals.VitalsVM>(),
|
||||
Stub<AcDream.UI.Abstractions.Panels.Chat.ChatVM>(),
|
||||
Stub<CharacterSheetProvider>(),
|
||||
Stub<AcDream.App.Spells.MagicRuntime>(),
|
||||
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<T>(string name) where T : class
|
||||
{
|
||||
T value = Stub<T>();
|
||||
_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<T>() 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.");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue