Harden retail character selection recovery

This commit is contained in:
Erik 2026-08-14 20:59:10 +02:00
parent 6cfab727f1
commit aeac874dab
8 changed files with 736 additions and 81 deletions

View file

@ -24,6 +24,7 @@ public sealed class RetailDialogFactory : IDisposable
private readonly Dictionary<uint, DialogInfo> _activeQueued = new();
private readonly Dictionary<uint, DialogInfo> _activeNonQueued = new();
private readonly Dictionary<uint, LinkedList<DialogInfo>> _pending = new();
private readonly LinkedList<DialogInfo> _retryable = new();
private readonly List<DialogInfo> _openOrder = new();
private uint _globalContext;
private bool _resetting;
@ -51,6 +52,8 @@ public sealed class RetailDialogFactory : IDisposable
public int PendingCount => _pending.Values.Sum(static queue => queue.Count);
internal int RetryCount => _retryable.Count;
/// <summary>Exact root-element switch from <c>CreateDialog_ @ 0x00477AD0</c>.</summary>
public static uint RootElementId(RetailDialogType type)
=> type switch
@ -93,14 +96,28 @@ public sealed class RetailDialogFactory : IDisposable
if (queueKey == NonQueuedKey)
{
_activeNonQueued.Add(context, info);
CreateDialog(info);
if (!TryCreateDialog(info))
{
_activeNonQueued.Remove(context);
QueueRetry(info);
}
return context;
}
if (!_activeQueued.TryGetValue(queueKey, out DialogInfo? current))
{
if (HasRetry(queueKey))
{
PendingQueue(queueKey).AddLast(info);
return context;
}
_activeQueued.Add(queueKey, info);
CreateDialog(info);
if (!TryCreateDialog(info))
{
_activeQueued.Remove(queueKey);
QueueRetry(info);
}
return context;
}
@ -118,7 +135,15 @@ public sealed class RetailDialogFactory : IDisposable
Suspend(current);
queue.AddFirst(current);
_activeQueued[queueKey] = info;
CreateDialog(info);
if (!TryCreateDialog(info))
{
_activeQueued.Remove(queueKey);
queue.Remove(current);
if (queue.Count == 0)
_pending.Remove(queueKey);
OpenSpecificDialog(current);
QueueRetry(info);
}
return context;
}
@ -211,11 +236,25 @@ public sealed class RetailDialogFactory : IDisposable
return true;
}
LinkedListNode<DialogInfo>? retry = _retryable.First;
while (retry is not null && retry.Value.Context != context)
retry = retry.Next;
if (retry is not null)
{
DialogInfo failed = retry.Value;
_retryable.Remove(retry);
DialogDone(failed);
if (failed.QueueKey != NonQueuedKey)
OpenNextDialog(failed.QueueKey);
return true;
}
return false;
}
public void Tick()
{
RetryFailedDialogs();
foreach (DialogInfo info in _openOrder.ToArray())
info.View?.Tick();
}
@ -238,6 +277,7 @@ public sealed class RetailDialogFactory : IDisposable
DialogInfo[] infos = _activeNonQueued.Values
.Concat(_activeQueued.Values)
.Concat(_pending.Values.SelectMany(static queue => queue))
.Concat(_retryable)
.Distinct()
.ToArray();
if (infos.Length == 0)
@ -249,6 +289,7 @@ public sealed class RetailDialogFactory : IDisposable
_activeNonQueued.Clear();
_activeQueued.Clear();
_pending.Clear();
_retryable.Clear();
foreach (DialogInfo info in infos)
{
try { DialogDone(info); }
@ -292,41 +333,57 @@ public sealed class RetailDialogFactory : IDisposable
return queue;
}
private void CreateDialog(DialogInfo info)
private bool TryCreateDialog(DialogInfo info)
{
RetailDialogType type = (RetailDialogType)info.Data.GetUInt32(RetailDialogProperty.Type);
if (type is not (RetailDialogType.Confirmation
or RetailDialogType.Wait
or RetailDialogType.Message
or RetailDialogType.ConfirmationTextInput))
throw new NotSupportedException(
$"Retail dialog type {(uint)type} does not have a ported presenter yet.");
ImportedLayout layout = _createLayout(type)
?? throw new InvalidOperationException(
$"Retail dialog catalog could not create type {(uint)type}.");
IRetailDialogView view = type switch
RetailDialogType type = (RetailDialogType)info.Data.GetUInt32(
RetailDialogProperty.Type);
try
{
RetailDialogType.Wait => new RetailWaitDialogView(_host, layout, info.Data),
RetailDialogType.Message => new RetailMessageDialogView(
_host, layout, info.Data, info.Context,
context => CloseDialog(context)),
RetailDialogType.ConfirmationTextInput =>
new RetailConfirmationTextInputDialogView(
if (type is not (RetailDialogType.Confirmation
or RetailDialogType.Wait
or RetailDialogType.Message
or RetailDialogType.ConfirmationTextInput))
{
throw new NotSupportedException(
$"Retail dialog type {(uint)type} does not have a ported presenter yet.");
}
ImportedLayout layout = _createLayout(type)
?? throw new InvalidOperationException(
$"Retail dialog catalog could not create type {(uint)type}.");
IRetailDialogView view = type switch
{
RetailDialogType.Wait => new RetailWaitDialogView(
_host, layout, info.Data),
RetailDialogType.Message => new RetailMessageDialogView(
_host, layout, info.Data, info.Context,
context => CloseDialog(context)),
_ => new RetailConfirmationDialogView(
_host, layout, info.Data, info.Context,
context => CloseDialog(context)),
};
info.View = view;
_host.AddChild(view.Root);
_host.BringToFront(view.Root);
_openOrder.Add(info);
_host.Modal = view.Root;
view.Tick();
UpdatePendingDialogDisplays();
DialogOpened?.Invoke(info.Context);
RetailDialogType.ConfirmationTextInput =>
new RetailConfirmationTextInputDialogView(
_host, layout, info.Data, info.Context,
context => CloseDialog(context)),
_ => new RetailConfirmationDialogView(
_host, layout, info.Data, info.Context,
context => CloseDialog(context)),
};
info.View = view;
_host.AddChild(view.Root);
_host.BringToFront(view.Root);
_openOrder.Add(info);
_host.Modal = view.Root;
view.Tick();
UpdatePendingDialogDisplays();
DialogOpened?.Invoke(info.Context);
return true;
}
catch (Exception error)
{
RemoveView(info);
Console.WriteLine(
$"[UI] retail dialog type {(uint)type} context {info.Context} "
+ $"will retry after catalog recovery: {error.Message}");
return false;
}
}
private void Suspend(DialogInfo info)
@ -380,6 +437,9 @@ public sealed class RetailDialogFactory : IDisposable
if (_activeQueued.ContainsKey(queueKey))
return;
if (TryActivateRetry(queueKey))
return;
if (!_pending.TryGetValue(queueKey, out LinkedList<DialogInfo>? queue)
|| queue.First is null)
return;
@ -389,7 +449,73 @@ public sealed class RetailDialogFactory : IDisposable
if (queue.Count == 0)
_pending.Remove(queueKey);
_activeQueued.Add(queueKey, next);
CreateDialog(next);
if (!TryCreateDialog(next))
{
_activeQueued.Remove(queueKey);
QueueRetry(next);
}
}
private void OpenSpecificDialog(DialogInfo info)
{
_activeQueued.Add(info.QueueKey, info);
if (!TryCreateDialog(info))
{
_activeQueued.Remove(info.QueueKey);
QueueRetry(info);
}
}
private void RetryFailedDialogs()
{
foreach (DialogInfo info in _retryable.ToArray())
{
if (info.QueueKey == NonQueuedKey)
{
_activeNonQueued.Add(info.Context, info);
if (TryCreateDialog(info))
_retryable.Remove(info);
else
_activeNonQueued.Remove(info.Context);
continue;
}
if (!_activeQueued.ContainsKey(info.QueueKey)
&& ReferenceEquals(FirstRetry(info.QueueKey), info))
{
TryActivateRetry(info.QueueKey);
}
}
}
private bool TryActivateRetry(uint queueKey)
{
DialogInfo? info = FirstRetry(queueKey);
if (info is null)
return false;
_activeQueued.Add(queueKey, info);
if (TryCreateDialog(info))
_retryable.Remove(info);
else
_activeQueued.Remove(queueKey);
return true;
}
private DialogInfo? FirstRetry(uint queueKey)
{
foreach (DialogInfo info in _retryable)
if (info.QueueKey == queueKey)
return info;
return null;
}
private bool HasRetry(uint queueKey) => FirstRetry(queueKey) is not null;
private void QueueRetry(DialogInfo info)
{
if (!_retryable.Contains(info))
_retryable.AddLast(info);
}
private void UpdatePendingDialogDisplays()