Make character UI retries transactional
This commit is contained in:
parent
aeac874dab
commit
1dd5706e15
6 changed files with 352 additions and 42 deletions
|
|
@ -104,7 +104,12 @@ retries on the next frame. Initial dialog-catalog, character root, and string
|
||||||
misses likewise retry on later ticks without mounting a duplicate root or
|
misses likewise retry on later ticks without mounting a duplicate root or
|
||||||
controller. Dialog presenter/catalog failures move their contexts to an
|
controller. Dialog presenter/catalog failures move their contexts to an
|
||||||
internal retry ledger, so UI callbacks do not retain poisoned active/queued
|
internal retry ledger, so UI callbacks do not retain poisoned active/queued
|
||||||
entries and the same context can appear after resource recovery.
|
entries and the same context can appear after resource recovery. Priority
|
||||||
|
contexts remain ahead of ordinary retries and preserve retail's nested
|
||||||
|
preemption order when creation recovers. The mount coordinator owns a detached
|
||||||
|
controller before attaching its root or running the first template-resolving
|
||||||
|
tick; any partial failure disposes that exact controller before retry, so roots
|
||||||
|
and handlers cannot accumulate.
|
||||||
|
|
||||||
There is deliberately no 3D preview and no claimed character-select background
|
There is deliberately no 3D preview and no claimed character-select background
|
||||||
scene. The screen root remains neutral with respect to render-loop background
|
scene. The screen root remains neutral with respect to render-loop background
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,36 @@ internal sealed class CharacterManagementUiController : IDisposable
|
||||||
RetailDialogFactory dialogs,
|
RetailDialogFactory dialogs,
|
||||||
CharacterSelectionRuntimeBindings bindings,
|
CharacterSelectionRuntimeBindings bindings,
|
||||||
DialogStrings strings)
|
DialogStrings strings)
|
||||||
|
{
|
||||||
|
CharacterManagementUiController? controller = CreateDetached(
|
||||||
|
host,
|
||||||
|
layout,
|
||||||
|
templateResolver,
|
||||||
|
dialogs,
|
||||||
|
bindings,
|
||||||
|
strings);
|
||||||
|
if (controller is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
controller.AttachAndTick();
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
controller.Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static CharacterManagementUiController? CreateDetached(
|
||||||
|
UiRoot host,
|
||||||
|
ImportedLayout layout,
|
||||||
|
Func<uint, uint, UiElement?> templateResolver,
|
||||||
|
RetailDialogFactory dialogs,
|
||||||
|
CharacterSelectionRuntimeBindings bindings,
|
||||||
|
DialogStrings strings)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(host);
|
ArgumentNullException.ThrowIfNull(host);
|
||||||
ArgumentNullException.ThrowIfNull(layout);
|
ArgumentNullException.ThrowIfNull(layout);
|
||||||
|
|
@ -144,20 +174,37 @@ internal sealed class CharacterManagementUiController : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
list.TemplateResolver = templateResolver;
|
list.TemplateResolver = templateResolver;
|
||||||
var controller = new CharacterManagementUiController(
|
try
|
||||||
host,
|
{
|
||||||
layout,
|
return new CharacterManagementUiController(
|
||||||
list,
|
host,
|
||||||
create,
|
layout,
|
||||||
enter,
|
list,
|
||||||
delete,
|
create,
|
||||||
restore,
|
enter,
|
||||||
dialogs,
|
delete,
|
||||||
bindings,
|
restore,
|
||||||
strings);
|
dialogs,
|
||||||
host.AddChild(controller.Root);
|
bindings,
|
||||||
controller.Tick();
|
strings);
|
||||||
return controller;
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
list.TemplateResolver = null;
|
||||||
|
create.OnClick = null;
|
||||||
|
enter.OnClick = null;
|
||||||
|
delete.OnClick = null;
|
||||||
|
restore.OnClick = null;
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void AttachAndTick()
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
if (Root.Parent is null)
|
||||||
|
_host.AddChild(Root);
|
||||||
|
Tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ContainsViewport(UiElement element)
|
private static bool ContainsViewport(UiElement element)
|
||||||
|
|
@ -247,20 +294,26 @@ internal sealed class CharacterManagementUiController : IDisposable
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
CloseAllDialogs(suppressCallbacks: true);
|
try
|
||||||
_enter.OnClick = null;
|
|
||||||
_delete.OnClick = null;
|
|
||||||
_restore.OnClick = null;
|
|
||||||
foreach (UiButton row in _rows)
|
|
||||||
{
|
{
|
||||||
row.OnClick = null;
|
CloseAllDialogs(suppressCallbacks: true);
|
||||||
row.OnDoubleClick = null;
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_enter.OnClick = null;
|
||||||
|
_delete.OnClick = null;
|
||||||
|
_restore.OnClick = null;
|
||||||
|
foreach (UiButton row in _rows)
|
||||||
|
{
|
||||||
|
row.OnClick = null;
|
||||||
|
row.OnDoubleClick = null;
|
||||||
|
}
|
||||||
|
_rows.Clear();
|
||||||
|
_rowIds.Clear();
|
||||||
|
_list.Flush();
|
||||||
|
_list.TemplateResolver = null;
|
||||||
|
_host.RemoveChild(Root);
|
||||||
}
|
}
|
||||||
_rows.Clear();
|
|
||||||
_rowIds.Clear();
|
|
||||||
_list.Flush();
|
|
||||||
_list.TemplateResolver = null;
|
|
||||||
_host.RemoveChild(Root);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryCaptureRoster(
|
private static bool TryCaptureRoster(
|
||||||
|
|
|
||||||
|
|
@ -53,23 +53,42 @@ internal sealed class CharacterManagementUiMountCoordinator : IDisposable
|
||||||
if (resources is null)
|
if (resources is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Controller = CharacterManagementUiController.Bind(
|
CharacterManagementUiController? candidate =
|
||||||
|
CharacterManagementUiController.CreateDetached(
|
||||||
_host,
|
_host,
|
||||||
resources.Layout,
|
resources.Layout,
|
||||||
resources.TemplateResolver,
|
resources.TemplateResolver,
|
||||||
dialogs,
|
dialogs,
|
||||||
_bindings,
|
_bindings,
|
||||||
resources.Strings);
|
resources.Strings);
|
||||||
if (Controller is not null)
|
if (candidate is null)
|
||||||
{
|
return;
|
||||||
Console.WriteLine(
|
|
||||||
$"[UI] retail character management from enum table 5 "
|
// Take ownership before the first attach/tick. Template resolution
|
||||||
+ $"(0x10000005 -> 0x{resources.LayoutId:X8}, "
|
// happens inside that tick and can throw after the root and button
|
||||||
+ "root 0x1000039A; flat list, no viewport).");
|
// handlers are live; the catch below can therefore always retire
|
||||||
}
|
// the exact partial controller before a later retry.
|
||||||
|
Controller = candidate;
|
||||||
|
candidate.AttachAndTick();
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[UI] retail character management from enum table 5 "
|
||||||
|
+ $"(0x10000005 -> 0x{resources.LayoutId:X8}, "
|
||||||
|
+ "root 0x1000039A; flat list, no viewport).");
|
||||||
}
|
}
|
||||||
catch (Exception error)
|
catch (Exception error)
|
||||||
{
|
{
|
||||||
|
CharacterManagementUiController? partial = Controller;
|
||||||
|
Controller = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
partial?.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception cleanupError)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
"[UI] character management partial-mount cleanup failed: "
|
||||||
|
+ cleanupError.Message);
|
||||||
|
}
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
"[UI] character management mount will retry after resource "
|
"[UI] character management mount will retry after resource "
|
||||||
+ $"recovery: {error.Message}");
|
+ $"recovery: {error.Message}");
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ public sealed class RetailDialogFactory : IDisposable
|
||||||
public required RetailDialogData Data { get; init; }
|
public required RetailDialogData Data { get; init; }
|
||||||
public required uint Context { get; init; }
|
public required uint Context { get; init; }
|
||||||
public required uint QueueKey { get; init; }
|
public required uint QueueKey { get; init; }
|
||||||
|
public required ulong Sequence { get; init; }
|
||||||
public Action<RetailDialogData>? Callback { get; init; }
|
public Action<RetailDialogData>? Callback { get; init; }
|
||||||
public IRetailDialogView? View { get; set; }
|
public IRetailDialogView? View { get; set; }
|
||||||
}
|
}
|
||||||
|
|
@ -27,6 +28,7 @@ public sealed class RetailDialogFactory : IDisposable
|
||||||
private readonly LinkedList<DialogInfo> _retryable = new();
|
private readonly LinkedList<DialogInfo> _retryable = new();
|
||||||
private readonly List<DialogInfo> _openOrder = new();
|
private readonly List<DialogInfo> _openOrder = new();
|
||||||
private uint _globalContext;
|
private uint _globalContext;
|
||||||
|
private ulong _globalSequence;
|
||||||
private bool _resetting;
|
private bool _resetting;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
|
@ -90,6 +92,7 @@ public sealed class RetailDialogFactory : IDisposable
|
||||||
Data = ownedData,
|
Data = ownedData,
|
||||||
Context = context,
|
Context = context,
|
||||||
QueueKey = queueKey,
|
QueueKey = queueKey,
|
||||||
|
Sequence = NextSequence(),
|
||||||
Callback = callback,
|
Callback = callback,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -106,7 +109,7 @@ public sealed class RetailDialogFactory : IDisposable
|
||||||
|
|
||||||
if (!_activeQueued.TryGetValue(queueKey, out DialogInfo? current))
|
if (!_activeQueued.TryGetValue(queueKey, out DialogInfo? current))
|
||||||
{
|
{
|
||||||
if (HasRetry(queueKey))
|
if (HasRetry(queueKey) && !IsPriority(info))
|
||||||
{
|
{
|
||||||
PendingQueue(queueKey).AddLast(info);
|
PendingQueue(queueKey).AddLast(info);
|
||||||
return context;
|
return context;
|
||||||
|
|
@ -122,7 +125,7 @@ public sealed class RetailDialogFactory : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
LinkedList<DialogInfo> queue = PendingQueue(queueKey);
|
LinkedList<DialogInfo> queue = PendingQueue(queueKey);
|
||||||
if (!ownedData.GetBoolean(RetailDialogProperty.Priority))
|
if (!IsPriority(info))
|
||||||
{
|
{
|
||||||
queue.AddLast(info);
|
queue.AddLast(info);
|
||||||
UpdatePendingDialogDisplays();
|
UpdatePendingDialogDisplays();
|
||||||
|
|
@ -324,6 +327,14 @@ public sealed class RetailDialogFactory : IDisposable
|
||||||
return _globalContext;
|
return _globalContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ulong NextSequence()
|
||||||
|
{
|
||||||
|
_globalSequence++;
|
||||||
|
if (_globalSequence == 0uL)
|
||||||
|
_globalSequence++;
|
||||||
|
return _globalSequence;
|
||||||
|
}
|
||||||
|
|
||||||
private LinkedList<DialogInfo> PendingQueue(uint queueKey)
|
private LinkedList<DialogInfo> PendingQueue(uint queueKey)
|
||||||
{
|
{
|
||||||
if (_pending.TryGetValue(queueKey, out LinkedList<DialogInfo>? queue))
|
if (_pending.TryGetValue(queueKey, out LinkedList<DialogInfo>? queue))
|
||||||
|
|
@ -480,14 +491,37 @@ public sealed class RetailDialogFactory : IDisposable
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_activeQueued.ContainsKey(info.QueueKey)
|
if (!ReferenceEquals(FirstRetry(info.QueueKey), info))
|
||||||
&& ReferenceEquals(FirstRetry(info.QueueKey), info))
|
continue;
|
||||||
{
|
|
||||||
|
if (!_activeQueued.TryGetValue(
|
||||||
|
info.QueueKey,
|
||||||
|
out DialogInfo? active))
|
||||||
TryActivateRetry(info.QueueKey);
|
TryActivateRetry(info.QueueKey);
|
||||||
}
|
else if (IsPriority(info)
|
||||||
|
&& (!IsPriority(active) || info.Sequence > active.Sequence))
|
||||||
|
TryPreemptWithRetry(info, active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TryPreemptWithRetry(DialogInfo priority, DialogInfo current)
|
||||||
|
{
|
||||||
|
LinkedList<DialogInfo> queue = PendingQueue(priority.QueueKey);
|
||||||
|
Suspend(current);
|
||||||
|
queue.AddFirst(current);
|
||||||
|
_activeQueued[priority.QueueKey] = priority;
|
||||||
|
_retryable.Remove(priority);
|
||||||
|
if (TryCreateDialog(priority))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_activeQueued.Remove(priority.QueueKey);
|
||||||
|
queue.Remove(current);
|
||||||
|
if (queue.Count == 0)
|
||||||
|
_pending.Remove(priority.QueueKey);
|
||||||
|
OpenSpecificDialog(current);
|
||||||
|
QueueRetry(priority);
|
||||||
|
}
|
||||||
|
|
||||||
private bool TryActivateRetry(uint queueKey)
|
private bool TryActivateRetry(uint queueKey)
|
||||||
{
|
{
|
||||||
DialogInfo? info = FirstRetry(queueKey);
|
DialogInfo? info = FirstRetry(queueKey);
|
||||||
|
|
@ -512,10 +546,29 @@ public sealed class RetailDialogFactory : IDisposable
|
||||||
|
|
||||||
private bool HasRetry(uint queueKey) => FirstRetry(queueKey) is not null;
|
private bool HasRetry(uint queueKey) => FirstRetry(queueKey) is not null;
|
||||||
|
|
||||||
|
private static bool IsPriority(DialogInfo info) =>
|
||||||
|
info.Data.GetBoolean(RetailDialogProperty.Priority);
|
||||||
|
|
||||||
private void QueueRetry(DialogInfo info)
|
private void QueueRetry(DialogInfo info)
|
||||||
{
|
{
|
||||||
if (!_retryable.Contains(info))
|
if (_retryable.Contains(info))
|
||||||
|
return;
|
||||||
|
if (!IsPriority(info))
|
||||||
|
{
|
||||||
_retryable.AddLast(info);
|
_retryable.AddLast(info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkedListNode<DialogInfo>? existing = _retryable.First;
|
||||||
|
while (existing is not null
|
||||||
|
&& existing.Value.QueueKey != info.QueueKey)
|
||||||
|
{
|
||||||
|
existing = existing.Next;
|
||||||
|
}
|
||||||
|
if (existing is null)
|
||||||
|
_retryable.AddLast(info);
|
||||||
|
else
|
||||||
|
_retryable.AddBefore(existing, info);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdatePendingDialogDisplays()
|
private void UpdatePendingDialogDisplays()
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,66 @@ public sealed class CharacterManagementUiControllerTests
|
||||||
Assert.Equal(3, resourceAttempts);
|
Assert.Equal(3, resourceAttempts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MountCoordinator_PostAttachFailuresDisposeBeforeRetryAndRecovery()
|
||||||
|
{
|
||||||
|
var host = new UiRoot { Width = 800f, Height = 600f };
|
||||||
|
var runtime = new FakeRuntime();
|
||||||
|
using var dialogs = new RetailDialogFactory(
|
||||||
|
host,
|
||||||
|
RetailDialogFactoryTests.BuildDialogLayout);
|
||||||
|
var screens = new List<ImportedLayout>();
|
||||||
|
int failingAttempts = 2;
|
||||||
|
using var coordinator = new CharacterManagementUiMountCoordinator(
|
||||||
|
host,
|
||||||
|
runtime.Bindings,
|
||||||
|
() => dialogs,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
ImportedLayout screen = BuildScreen();
|
||||||
|
screens.Add(screen);
|
||||||
|
bool throwAfterAttach = failingAttempts-- > 0;
|
||||||
|
return new CharacterManagementUiMountResources(
|
||||||
|
0x21000004u,
|
||||||
|
screen,
|
||||||
|
(_, _) => throwAfterAttach
|
||||||
|
? throw new InvalidOperationException(
|
||||||
|
"template failed after root attach")
|
||||||
|
: BuildRow(),
|
||||||
|
TestStrings());
|
||||||
|
});
|
||||||
|
|
||||||
|
coordinator.Tick();
|
||||||
|
Assert.Null(coordinator.Controller);
|
||||||
|
Assert.Empty(host.Children);
|
||||||
|
Assert.Single(screens);
|
||||||
|
AssertDetachedAndUnbound(screens[0]);
|
||||||
|
|
||||||
|
coordinator.Tick();
|
||||||
|
Assert.Null(coordinator.Controller);
|
||||||
|
Assert.Empty(host.Children);
|
||||||
|
Assert.Equal(2, screens.Count);
|
||||||
|
Assert.All(screens, AssertDetachedAndUnbound);
|
||||||
|
|
||||||
|
coordinator.Tick();
|
||||||
|
CharacterManagementUiController mounted = Assert.IsType<
|
||||||
|
CharacterManagementUiController>(coordinator.Controller);
|
||||||
|
Assert.Equal(3, screens.Count);
|
||||||
|
Assert.Single(host.Children);
|
||||||
|
Assert.Same(mounted.Root, host.Children[0]);
|
||||||
|
Assert.NotNull(Assert.IsType<UiButton>(screens[2].FindElement(
|
||||||
|
CharacterManagementUiController.EnterElementId)).OnClick);
|
||||||
|
|
||||||
|
coordinator.Tick();
|
||||||
|
Assert.Equal(3, screens.Count);
|
||||||
|
Assert.Single(host.Children);
|
||||||
|
|
||||||
|
coordinator.Dispose();
|
||||||
|
Assert.Empty(host.Children);
|
||||||
|
Assert.Null(coordinator.Controller);
|
||||||
|
Assert.All(screens, AssertDetachedAndUnbound);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DeleteConfirmation_IsCaseInsensitive_ThenWaitsThroughAckUntilFreshRoster()
|
public void DeleteConfirmation_IsCaseInsensitive_ThenWaitsThroughAckUntilFreshRoster()
|
||||||
{
|
{
|
||||||
|
|
@ -509,6 +569,17 @@ public sealed class CharacterManagementUiControllerTests
|
||||||
"Please Wait",
|
"Please Wait",
|
||||||
"Entering World");
|
"Entering World");
|
||||||
|
|
||||||
|
private static void AssertDetachedAndUnbound(ImportedLayout screen)
|
||||||
|
{
|
||||||
|
Assert.Null(screen.Root.Parent);
|
||||||
|
Assert.Null(Assert.IsType<UiButton>(screen.FindElement(
|
||||||
|
CharacterManagementUiController.EnterElementId)).OnClick);
|
||||||
|
Assert.Null(Assert.IsType<UiButton>(screen.FindElement(
|
||||||
|
CharacterManagementUiController.DeleteElementId)).OnClick);
|
||||||
|
Assert.Null(Assert.IsType<UiButton>(screen.FindElement(
|
||||||
|
CharacterManagementUiController.RestoreElementId)).OnClick);
|
||||||
|
}
|
||||||
|
|
||||||
private static ImportedLayout BuildScreen(bool includePreview = false)
|
private static ImportedLayout BuildScreen(bool includePreview = false)
|
||||||
{
|
{
|
||||||
var root = new ElementInfo
|
var root = new ElementInfo
|
||||||
|
|
|
||||||
|
|
@ -542,6 +542,110 @@ public sealed class RetailDialogFactoryTests
|
||||||
Assert.True(factory.CloseDialog(later));
|
Assert.True(factory.CloseDialog(later));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PriorityRequest_PreemptsOrdinaryRetryAndPreservesOrdinaryFifo()
|
||||||
|
{
|
||||||
|
var root = new UiRoot { Width = 800f, Height = 600f };
|
||||||
|
bool waitsAvailable = false;
|
||||||
|
using var factory = new RetailDialogFactory(root, type =>
|
||||||
|
type == RetailDialogType.Wait && !waitsAvailable
|
||||||
|
? null
|
||||||
|
: BuildDialogLayout(type));
|
||||||
|
|
||||||
|
uint failed = factory.MakeWait("failed ordinary");
|
||||||
|
uint later = factory.MakeWait("later ordinary");
|
||||||
|
uint priority = factory.MakeDialog(
|
||||||
|
Priority(RetailDialogData.Message("priority")));
|
||||||
|
|
||||||
|
Assert.Equal(1, factory.ActiveCount);
|
||||||
|
Assert.Equal(1, factory.RetryCount);
|
||||||
|
Assert.Equal(1, factory.PendingCount);
|
||||||
|
Assert.Equal("priority", MessageFromAnyDialog(root.Modal!));
|
||||||
|
|
||||||
|
Assert.True(factory.CloseDialog(priority));
|
||||||
|
Assert.Equal(0, factory.ActiveCount);
|
||||||
|
Assert.Equal(1, factory.RetryCount);
|
||||||
|
Assert.Equal(1, factory.PendingCount);
|
||||||
|
|
||||||
|
waitsAvailable = true;
|
||||||
|
factory.Tick();
|
||||||
|
Assert.Equal("failed ordinary", MessageFromAnyDialog(root.Modal!));
|
||||||
|
Assert.True(factory.CloseDialog(failed));
|
||||||
|
Assert.Equal("later ordinary", MessageFromAnyDialog(root.Modal!));
|
||||||
|
Assert.True(factory.CloseDialog(later));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FailedPriority_RetriesAheadOfRestoredActiveAndQueuedDialog()
|
||||||
|
{
|
||||||
|
var root = new UiRoot { Width = 800f, Height = 600f };
|
||||||
|
bool priorityAvailable = false;
|
||||||
|
using var factory = new RetailDialogFactory(root, type =>
|
||||||
|
type == RetailDialogType.Message && !priorityAvailable
|
||||||
|
? null
|
||||||
|
: BuildDialogLayout(type));
|
||||||
|
|
||||||
|
uint active = factory.MakeWait("active");
|
||||||
|
uint queued = factory.MakeWait("queued");
|
||||||
|
uint priority = factory.MakeDialog(
|
||||||
|
Priority(RetailDialogData.Message("priority")));
|
||||||
|
|
||||||
|
Assert.Equal(1, factory.ActiveCount);
|
||||||
|
Assert.Equal(1, factory.RetryCount);
|
||||||
|
Assert.Equal(1, factory.PendingCount);
|
||||||
|
Assert.Equal("active", MessageFromAnyDialog(root.Modal!));
|
||||||
|
|
||||||
|
priorityAvailable = true;
|
||||||
|
factory.Tick();
|
||||||
|
|
||||||
|
Assert.Equal(1, factory.ActiveCount);
|
||||||
|
Assert.Equal(0, factory.RetryCount);
|
||||||
|
Assert.Equal(2, factory.PendingCount);
|
||||||
|
Assert.Equal("priority", MessageFromAnyDialog(root.Modal!));
|
||||||
|
|
||||||
|
Assert.True(factory.CloseDialog(priority));
|
||||||
|
Assert.Equal("active", MessageFromAnyDialog(root.Modal!));
|
||||||
|
Assert.Equal(1, factory.PendingCount);
|
||||||
|
Assert.True(factory.CloseDialog(active));
|
||||||
|
Assert.Equal("queued", MessageFromAnyDialog(root.Modal!));
|
||||||
|
Assert.True(factory.CloseDialog(queued));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultipleRetries_NewestPriorityFirstThenOlderPriorityThenOrdinaryFifo()
|
||||||
|
{
|
||||||
|
var root = new UiRoot { Width = 800f, Height = 600f };
|
||||||
|
bool available = false;
|
||||||
|
using var factory = new RetailDialogFactory(root, type =>
|
||||||
|
!available ? null : BuildDialogLayout(type));
|
||||||
|
|
||||||
|
uint ordinary = factory.MakeWait("ordinary");
|
||||||
|
uint later = factory.MakeWait("later");
|
||||||
|
uint olderPriority = factory.MakeDialog(
|
||||||
|
Priority(RetailDialogData.Message("older priority")));
|
||||||
|
uint newerPriority = factory.MakeDialog(Priority(
|
||||||
|
RetailDialogData.ConfirmationTextInput("newer priority")));
|
||||||
|
|
||||||
|
Assert.Equal(0, factory.ActiveCount);
|
||||||
|
Assert.Equal(3, factory.RetryCount);
|
||||||
|
Assert.Equal(1, factory.PendingCount);
|
||||||
|
|
||||||
|
available = true;
|
||||||
|
factory.Tick();
|
||||||
|
|
||||||
|
Assert.Equal(1, factory.ActiveCount);
|
||||||
|
Assert.Equal(2, factory.RetryCount);
|
||||||
|
Assert.Equal("newer priority", MessageFromAnyDialog(root.Modal!));
|
||||||
|
|
||||||
|
Assert.True(factory.CloseDialog(newerPriority));
|
||||||
|
Assert.Equal("older priority", MessageFromAnyDialog(root.Modal!));
|
||||||
|
Assert.True(factory.CloseDialog(olderPriority));
|
||||||
|
Assert.Equal("ordinary", MessageFromAnyDialog(root.Modal!));
|
||||||
|
Assert.True(factory.CloseDialog(ordinary));
|
||||||
|
Assert.Equal("later", MessageFromAnyDialog(root.Modal!));
|
||||||
|
Assert.True(factory.CloseDialog(later));
|
||||||
|
}
|
||||||
|
|
||||||
private static RetailDialogFactory CreateFactory(
|
private static RetailDialogFactory CreateFactory(
|
||||||
UiRoot root,
|
UiRoot root,
|
||||||
List<ImportedLayout> layouts)
|
List<ImportedLayout> layouts)
|
||||||
|
|
@ -575,6 +679,11 @@ public sealed class RetailDialogFactoryTests
|
||||||
.LinesProvider()
|
.LinesProvider()
|
||||||
.Select(static line => line.Text));
|
.Select(static line => line.Text));
|
||||||
|
|
||||||
|
private static RetailDialogData Priority(RetailDialogData data) =>
|
||||||
|
data.Set(RetailDialogProperty.Priority, true)
|
||||||
|
.Set(RetailDialogProperty.QueueKey,
|
||||||
|
RetailDialogFactory.DefaultQueueKey);
|
||||||
|
|
||||||
internal static ImportedLayout BuildDialogLayout(RetailDialogType type)
|
internal static ImportedLayout BuildDialogLayout(RetailDialogType type)
|
||||||
{
|
{
|
||||||
uint rootId = RetailDialogFactory.RootElementId(type);
|
uint rootId = RetailDialogFactory.RootElementId(type);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue