Make character UI retries transactional
This commit is contained in:
parent
aeac874dab
commit
1dd5706e15
6 changed files with 352 additions and 42 deletions
|
|
@ -116,6 +116,36 @@ internal sealed class CharacterManagementUiController : IDisposable
|
|||
RetailDialogFactory dialogs,
|
||||
CharacterSelectionRuntimeBindings bindings,
|
||||
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(layout);
|
||||
|
|
@ -144,20 +174,37 @@ internal sealed class CharacterManagementUiController : IDisposable
|
|||
}
|
||||
|
||||
list.TemplateResolver = templateResolver;
|
||||
var controller = new CharacterManagementUiController(
|
||||
host,
|
||||
layout,
|
||||
list,
|
||||
create,
|
||||
enter,
|
||||
delete,
|
||||
restore,
|
||||
dialogs,
|
||||
bindings,
|
||||
strings);
|
||||
host.AddChild(controller.Root);
|
||||
controller.Tick();
|
||||
return controller;
|
||||
try
|
||||
{
|
||||
return new CharacterManagementUiController(
|
||||
host,
|
||||
layout,
|
||||
list,
|
||||
create,
|
||||
enter,
|
||||
delete,
|
||||
restore,
|
||||
dialogs,
|
||||
bindings,
|
||||
strings);
|
||||
}
|
||||
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)
|
||||
|
|
@ -247,20 +294,26 @@ internal sealed class CharacterManagementUiController : IDisposable
|
|||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
CloseAllDialogs(suppressCallbacks: true);
|
||||
_enter.OnClick = null;
|
||||
_delete.OnClick = null;
|
||||
_restore.OnClick = null;
|
||||
foreach (UiButton row in _rows)
|
||||
try
|
||||
{
|
||||
row.OnClick = null;
|
||||
row.OnDoubleClick = null;
|
||||
CloseAllDialogs(suppressCallbacks: true);
|
||||
}
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -53,23 +53,42 @@ internal sealed class CharacterManagementUiMountCoordinator : IDisposable
|
|||
if (resources is null)
|
||||
return;
|
||||
|
||||
Controller = CharacterManagementUiController.Bind(
|
||||
CharacterManagementUiController? candidate =
|
||||
CharacterManagementUiController.CreateDetached(
|
||||
_host,
|
||||
resources.Layout,
|
||||
resources.TemplateResolver,
|
||||
dialogs,
|
||||
_bindings,
|
||||
resources.Strings);
|
||||
if (Controller is not null)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[UI] retail character management from enum table 5 "
|
||||
+ $"(0x10000005 -> 0x{resources.LayoutId:X8}, "
|
||||
+ "root 0x1000039A; flat list, no viewport).");
|
||||
}
|
||||
if (candidate is null)
|
||||
return;
|
||||
|
||||
// Take ownership before the first attach/tick. Template resolution
|
||||
// happens inside that tick and can throw after the root and button
|
||||
// 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)
|
||||
{
|
||||
CharacterManagementUiController? partial = Controller;
|
||||
Controller = null;
|
||||
try
|
||||
{
|
||||
partial?.Dispose();
|
||||
}
|
||||
catch (Exception cleanupError)
|
||||
{
|
||||
Console.WriteLine(
|
||||
"[UI] character management partial-mount cleanup failed: "
|
||||
+ cleanupError.Message);
|
||||
}
|
||||
Console.WriteLine(
|
||||
"[UI] character management mount will retry after resource "
|
||||
+ $"recovery: {error.Message}");
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ public sealed class RetailDialogFactory : IDisposable
|
|||
public required RetailDialogData Data { get; init; }
|
||||
public required uint Context { get; init; }
|
||||
public required uint QueueKey { get; init; }
|
||||
public required ulong Sequence { get; init; }
|
||||
public Action<RetailDialogData>? Callback { get; init; }
|
||||
public IRetailDialogView? View { get; set; }
|
||||
}
|
||||
|
|
@ -27,6 +28,7 @@ public sealed class RetailDialogFactory : IDisposable
|
|||
private readonly LinkedList<DialogInfo> _retryable = new();
|
||||
private readonly List<DialogInfo> _openOrder = new();
|
||||
private uint _globalContext;
|
||||
private ulong _globalSequence;
|
||||
private bool _resetting;
|
||||
private bool _disposed;
|
||||
|
||||
|
|
@ -90,6 +92,7 @@ public sealed class RetailDialogFactory : IDisposable
|
|||
Data = ownedData,
|
||||
Context = context,
|
||||
QueueKey = queueKey,
|
||||
Sequence = NextSequence(),
|
||||
Callback = callback,
|
||||
};
|
||||
|
||||
|
|
@ -106,7 +109,7 @@ public sealed class RetailDialogFactory : IDisposable
|
|||
|
||||
if (!_activeQueued.TryGetValue(queueKey, out DialogInfo? current))
|
||||
{
|
||||
if (HasRetry(queueKey))
|
||||
if (HasRetry(queueKey) && !IsPriority(info))
|
||||
{
|
||||
PendingQueue(queueKey).AddLast(info);
|
||||
return context;
|
||||
|
|
@ -122,7 +125,7 @@ public sealed class RetailDialogFactory : IDisposable
|
|||
}
|
||||
|
||||
LinkedList<DialogInfo> queue = PendingQueue(queueKey);
|
||||
if (!ownedData.GetBoolean(RetailDialogProperty.Priority))
|
||||
if (!IsPriority(info))
|
||||
{
|
||||
queue.AddLast(info);
|
||||
UpdatePendingDialogDisplays();
|
||||
|
|
@ -324,6 +327,14 @@ public sealed class RetailDialogFactory : IDisposable
|
|||
return _globalContext;
|
||||
}
|
||||
|
||||
private ulong NextSequence()
|
||||
{
|
||||
_globalSequence++;
|
||||
if (_globalSequence == 0uL)
|
||||
_globalSequence++;
|
||||
return _globalSequence;
|
||||
}
|
||||
|
||||
private LinkedList<DialogInfo> PendingQueue(uint queueKey)
|
||||
{
|
||||
if (_pending.TryGetValue(queueKey, out LinkedList<DialogInfo>? queue))
|
||||
|
|
@ -480,14 +491,37 @@ public sealed class RetailDialogFactory : IDisposable
|
|||
continue;
|
||||
}
|
||||
|
||||
if (!_activeQueued.ContainsKey(info.QueueKey)
|
||||
&& ReferenceEquals(FirstRetry(info.QueueKey), info))
|
||||
{
|
||||
if (!ReferenceEquals(FirstRetry(info.QueueKey), info))
|
||||
continue;
|
||||
|
||||
if (!_activeQueued.TryGetValue(
|
||||
info.QueueKey,
|
||||
out DialogInfo? active))
|
||||
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)
|
||||
{
|
||||
DialogInfo? info = FirstRetry(queueKey);
|
||||
|
|
@ -512,10 +546,29 @@ public sealed class RetailDialogFactory : IDisposable
|
|||
|
||||
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)
|
||||
{
|
||||
if (!_retryable.Contains(info))
|
||||
if (_retryable.Contains(info))
|
||||
return;
|
||||
if (!IsPriority(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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue