Make character UI retries transactional

This commit is contained in:
Erik 2026-08-14 21:14:08 +02:00
parent aeac874dab
commit 1dd5706e15
6 changed files with 352 additions and 42 deletions

View file

@ -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(

View file

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

View file

@ -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()