GF-15 (the gate blocker): the Summary name field and Finish button were
NOT structurally broken — live repro over the project's own local ACE
test server showed clicks correctly focus the field and land characters.
The real bug only surfaces after the first dialog opens: pressing Finish
empty successfully creates the NoName RetailMessageDialogView (visible,
correct 400x95 geometry) but it renders nothing and silently absorbs
every click across the whole canvas. Root cause: CharacterCreationUiController.Tick
and CharacterManagementUiController.Tick both call UiRoot.BringToFront(Root)
unconditionally every frame (needed so chargen stays above the occluded
management screen, AP-229); a dialog root is a direct sibling under the
same UiRoot, and RetailWindowManager.BringToFront is "highest ZOrder among
siblings + 1" — whichever BringToFront runs last in a frame wins.
RetailDialogFactory.Tick never re-asserted its own dialogs' z-order, so
the next frame's screen Tick buried the dialog behind the screen's opaque
backdrop while it stayed the registered Modal with exclusive input
priority. Fixed by having RetailDialogFactory.Tick re-raise every open
dialog (in open-order) each tick, matching retail's always-on-top dialog
behavior. Live-verified the complete user sequence end to end: click
field, type, press Finish empty, dialog now visibly renders, OK dismisses
cleanly, field still typable afterward. The "[ Name" prefill question is
closed as a non-bug: neither CharGenState::RandomizeCharacter nor
gmCGSummaryPage::InitializePage write text into the field in the decomp;
retail's field is genuinely empty on open, matching acdream already.
GF-5: CharacterCreationSkillsPage.RebuildRows resolved the wrong listbox
template (Templates[0], retail's own 3-child bucket-header row) and
required the root to be a UiButton (it's a plain container). Byte-traced
gmCGSkillsPage::DoSkillRecords + tagSkillRecord's copy-ctor field order
to map every child id in the real row (Templates[1]): name, level/cost
text, and the two real per-row up/down arrow buttons. Wired the arrows to
retail's own plain-click dispatch, retiring (narrowing) AP-213's
click-to-advance/double-click-retreat single-button substitution.
GF-13: dat property 0x3B (Invisible) was never read by the importer.
Elements 0x10000403/0x10000494 ("Non-Admin"/"Non-Envoy") author it true.
A blast-radius sweep found 1,083 elements client-wide author the same
flag, so this fix stays chargen-scoped only (ElementInfo.Invisible /
UiElement.AuthoredInvisible are pure data additions; only
CharacterCreationUiController acts on them, by the authored flag, not a
hardcoded id list). General importer-wide honor filed as ISSUES.md #408;
register row AP-230 records the split.
Gates: solution build green; App 5266/3 skips/0 failed; Runtime 1735/0;
full-solution run 0 failures anywhere. Register: AP-230 filed, AP-213
narrowed. ISSUES: #408 filed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
631 lines
21 KiB
C#
631 lines
21 KiB
C#
namespace AcDream.App.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Retained-mode port of retail <c>DialogFactory @ 0x004773C0..0x00478470</c>.
|
|
/// It owns dialog contexts, independent FIFO queue groups, nonqueued dialogs,
|
|
/// priority preemption, callback delivery, close notices, and fresh catalog roots.
|
|
/// </summary>
|
|
public sealed class RetailDialogFactory : IDisposable
|
|
{
|
|
public const uint DefaultQueueKey = 2u;
|
|
public const uint NonQueuedKey = 1u;
|
|
|
|
private sealed class DialogInfo
|
|
{
|
|
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; }
|
|
}
|
|
|
|
private readonly UiRoot _host;
|
|
private readonly Func<RetailDialogType, ImportedLayout?> _createLayout;
|
|
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 ulong _globalSequence;
|
|
private bool _resetting;
|
|
private bool _disposed;
|
|
|
|
public RetailDialogFactory(
|
|
UiRoot host,
|
|
Func<RetailDialogType, ImportedLayout?> createLayout)
|
|
{
|
|
_host = host ?? throw new ArgumentNullException(nameof(host));
|
|
_createLayout = createLayout ?? throw new ArgumentNullException(nameof(createLayout));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retail's global close-dialog notice, raised after the per-context callback and
|
|
/// before the live root is removed from its parent.
|
|
/// </summary>
|
|
public event Action<uint, RetailDialogData>? DialogClosed;
|
|
|
|
public event Action<uint>? DialogOpened;
|
|
|
|
public bool IsOpen => _activeQueued.Count != 0 || _activeNonQueued.Count != 0;
|
|
|
|
public int ActiveCount => _activeQueued.Count + _activeNonQueued.Count;
|
|
|
|
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
|
|
{
|
|
RetailDialogType.Confirmation => 0x15u,
|
|
RetailDialogType.Wait => 0x31u,
|
|
RetailDialogType.Message => 0x24u,
|
|
RetailDialogType.TextInput => 0x28u,
|
|
RetailDialogType.ConfirmationTextInput => 0x2Cu,
|
|
RetailDialogType.Menu => 0x1Bu,
|
|
RetailDialogType.ConfirmationMenu => 0x1Fu,
|
|
_ => 0u,
|
|
};
|
|
|
|
public uint MakeDialog(RetailDialogData data)
|
|
=> MakeDialog(data, callback: null);
|
|
|
|
/// <summary>
|
|
/// Retail <c>MakeCallbackDialogInCurrentUI @ 0x00478430</c>.
|
|
/// </summary>
|
|
public uint MakeDialog(RetailDialogData data, Action<RetailDialogData>? callback)
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
ArgumentNullException.ThrowIfNull(data);
|
|
|
|
uint context = NextContext();
|
|
RetailDialogData ownedData = data.Clone();
|
|
uint queueKey = ownedData.GetUInt32(RetailDialogProperty.QueueKey, DefaultQueueKey);
|
|
if (queueKey == 0u)
|
|
queueKey = DefaultQueueKey;
|
|
|
|
var info = new DialogInfo
|
|
{
|
|
Data = ownedData,
|
|
Context = context,
|
|
QueueKey = queueKey,
|
|
Sequence = NextSequence(),
|
|
Callback = callback,
|
|
};
|
|
|
|
if (queueKey == NonQueuedKey)
|
|
{
|
|
_activeNonQueued.Add(context, info);
|
|
if (!TryCreateDialog(info))
|
|
{
|
|
_activeNonQueued.Remove(context);
|
|
QueueRetry(info);
|
|
}
|
|
return context;
|
|
}
|
|
|
|
if (!_activeQueued.TryGetValue(queueKey, out DialogInfo? current))
|
|
{
|
|
if (HasRetry(queueKey) && !IsPriority(info))
|
|
{
|
|
PendingQueue(queueKey).AddLast(info);
|
|
return context;
|
|
}
|
|
|
|
_activeQueued.Add(queueKey, info);
|
|
if (!TryCreateDialog(info))
|
|
{
|
|
_activeQueued.Remove(queueKey);
|
|
QueueRetry(info);
|
|
}
|
|
return context;
|
|
}
|
|
|
|
LinkedList<DialogInfo> queue = PendingQueue(queueKey);
|
|
if (!IsPriority(info))
|
|
{
|
|
queue.AddLast(info);
|
|
UpdatePendingDialogDisplays();
|
|
return context;
|
|
}
|
|
|
|
// MakeDialog's 0x8D branch removes the active root without completing the
|
|
// DialogInfo, inserts that current info at the front, then displays the
|
|
// priority request in its place.
|
|
Suspend(current);
|
|
queue.AddFirst(current);
|
|
_activeQueued[queueKey] = info;
|
|
if (!TryCreateDialog(info))
|
|
{
|
|
_activeQueued.Remove(queueKey);
|
|
queue.Remove(current);
|
|
if (queue.Count == 0)
|
|
_pending.Remove(queueKey);
|
|
OpenSpecificDialog(current);
|
|
QueueRetry(info);
|
|
}
|
|
return context;
|
|
}
|
|
|
|
public uint MakeConfirmation(
|
|
string message,
|
|
Action<RetailDialogData>? callback = null,
|
|
uint queueKey = DefaultQueueKey,
|
|
bool priority = false)
|
|
{
|
|
RetailDialogData data = RetailDialogData.Confirmation(message)
|
|
.Set(RetailDialogProperty.QueueKey, queueKey);
|
|
if (priority)
|
|
data.Set(RetailDialogProperty.Priority, true);
|
|
return MakeDialog(data, callback);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Text-only wait dialog (type 2), closed by the caller via
|
|
/// <see cref="CloseDialog"/>. Property shape mirrors
|
|
/// <c>UIOption_ActionKeyMap::OpenMapWarnDialog @ 0x00488A00</c>: type 2,
|
|
/// caller-chosen queue key, element attribute 0x40 set, message text.
|
|
/// </summary>
|
|
public uint MakeWait(string message, uint queueKey = DefaultQueueKey)
|
|
{
|
|
RetailDialogData data = RetailDialogData.Wait(message)
|
|
.Set(RetailDialogProperty.QueueKey, queueKey);
|
|
return MakeDialog(data, callback: null);
|
|
}
|
|
|
|
public uint MakeMessage(
|
|
string message,
|
|
Action<RetailDialogData>? callback = null,
|
|
uint queueKey = DefaultQueueKey)
|
|
{
|
|
RetailDialogData data = RetailDialogData.Message(message)
|
|
.Set(RetailDialogProperty.QueueKey, queueKey);
|
|
return MakeDialog(data, callback);
|
|
}
|
|
|
|
public uint MakeConfirmationTextInput(
|
|
string message,
|
|
Action<RetailDialogData>? callback = null,
|
|
uint queueKey = DefaultQueueKey)
|
|
{
|
|
RetailDialogData data = RetailDialogData.ConfirmationTextInput(message)
|
|
.Set(RetailDialogProperty.QueueKey, queueKey);
|
|
return MakeDialog(data, callback);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retail <c>CloseDialog @ 0x00478160</c>. The context can identify an active
|
|
/// nonqueued dialog, an active queued dialog, or an item still pending in a queue.
|
|
/// </summary>
|
|
public bool CloseDialog(uint context)
|
|
{
|
|
if (context == 0u)
|
|
return false;
|
|
|
|
if (_activeNonQueued.Remove(context, out DialogInfo? nonQueued))
|
|
{
|
|
DialogDone(nonQueued);
|
|
return true;
|
|
}
|
|
|
|
foreach ((uint queueKey, DialogInfo active) in _activeQueued.ToArray())
|
|
{
|
|
if (active.Context != context)
|
|
continue;
|
|
|
|
_activeQueued.Remove(queueKey);
|
|
DialogDone(active);
|
|
OpenNextDialog(queueKey);
|
|
return true;
|
|
}
|
|
|
|
foreach ((uint queueKey, LinkedList<DialogInfo> queue) in _pending.ToArray())
|
|
{
|
|
LinkedListNode<DialogInfo>? node = queue.First;
|
|
while (node is not null && node.Value.Context != context)
|
|
node = node.Next;
|
|
if (node is null)
|
|
continue;
|
|
|
|
DialogInfo pending = node.Value;
|
|
queue.Remove(node);
|
|
if (queue.Count == 0)
|
|
_pending.Remove(queueKey);
|
|
DialogDone(pending);
|
|
UpdatePendingDialogDisplays();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// GF-15 fix (Campaign CC gate round 1, Batch A, 2026-08-16). Live-repro-
|
|
/// confirmed root cause: <c>CharacterCreationUiController.Tick</c> and
|
|
/// <c>CharacterManagementUiController.Tick</c> both call
|
|
/// <c>UiRoot.BringToFront(Root)</c> UNCONDITIONALLY on every frame while
|
|
/// their screen is open — a per-tick "stay on top of my sibling screen"
|
|
/// assertion (needed so chargen never bleeds input to the occluded
|
|
/// char-management screen underneath it, register AP-229). A dialog this
|
|
/// factory opens is ALSO a direct sibling of those screen roots under
|
|
/// the same <c>UiRoot</c> (<c>_host.AddChild(view.Root)</c> in
|
|
/// <see cref="TryCreateDialog"/>), competing for the SAME z-order slot.
|
|
/// <see cref="RetailWindowManager.BringToFront"/> is a simple "highest
|
|
/// ZOrder among <c>_root</c>'s direct children + 1" — whichever sibling's
|
|
/// own <c>BringToFront</c> call runs LAST in a frame wins the top slot.
|
|
/// Before this fix, this method never re-asserted a dialog's own
|
|
/// z-order after the one-time raise in <see cref="TryCreateDialog"/>, so
|
|
/// the VERY NEXT frame's screen <c>Tick()</c> (which always runs before
|
|
/// this factory's own <c>Tick()</c> in
|
|
/// <c>RetailUiRuntime.Tick(double)</c>'s per-frame sequence) silently
|
|
/// buried the dialog behind the screen's opaque backdrop — while the
|
|
/// dialog remained the registered <see cref="UiRoot.Modal"/> and kept
|
|
/// EXCLUSIVE input priority (<c>OnMouseDown</c>'s Modal-vs-bounds gate is
|
|
/// independent of render/z-order). The user-visible symptom: press
|
|
/// Finish empty → the NoName dialog is created successfully
|
|
/// (<c>visible=true</c>, correct geometry, live-DAT-probe-confirmed) but
|
|
/// renders NOTHING, and every subsequent click across the WHOLE canvas
|
|
/// resolves to the invisible dialog root instead of the name field or
|
|
/// Finish button underneath — both GF-15 symptoms from one mechanism.
|
|
/// Retail's real dialogs are always-on-top overlays by construction (a
|
|
/// separate presentation layer, not a z-ordered sibling of the game UI);
|
|
/// re-asserting every open dialog's z-order here, every tick, in
|
|
/// <see cref="_openOrder"/> order (so the MOST RECENTLY opened dialog —
|
|
/// the same one <see cref="RefreshModal"/> already treats as
|
|
/// authoritative — ends up on top) reproduces that invariant without
|
|
/// touching either screen controller's own already-verified raise.
|
|
/// </summary>
|
|
public void Tick()
|
|
{
|
|
RetryFailedDialogs();
|
|
foreach (DialogInfo info in _openOrder.ToArray())
|
|
{
|
|
if (info.View is { } view)
|
|
{
|
|
_host.BringToFront(view.Root);
|
|
view.Tick();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retail <c>DialogFactory::Reset @ 0x00477950</c> completes active and pending
|
|
/// infos before clearing the factory.
|
|
/// </summary>
|
|
public void Reset()
|
|
{
|
|
if (_resetting)
|
|
return;
|
|
|
|
_resetting = true;
|
|
List<Exception>? failures = null;
|
|
try
|
|
{
|
|
while (true)
|
|
{
|
|
DialogInfo[] infos = _activeNonQueued.Values
|
|
.Concat(_activeQueued.Values)
|
|
.Concat(_pending.Values.SelectMany(static queue => queue))
|
|
.Concat(_retryable)
|
|
.Distinct()
|
|
.ToArray();
|
|
if (infos.Length == 0)
|
|
break;
|
|
|
|
// Callbacks may synchronously create another dialog. Retire
|
|
// this exact snapshot, then loop until those reentrant infos
|
|
// have also completed through DialogDone.
|
|
_activeNonQueued.Clear();
|
|
_activeQueued.Clear();
|
|
_pending.Clear();
|
|
_retryable.Clear();
|
|
foreach (DialogInfo info in infos)
|
|
{
|
|
try { DialogDone(info); }
|
|
catch (Exception error) { (failures ??= []).Add(error); }
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_resetting = false;
|
|
RefreshModal();
|
|
}
|
|
|
|
if (failures is not null)
|
|
throw new AggregateException(
|
|
"One or more dialogs failed while the factory reset.",
|
|
failures);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
Reset();
|
|
}
|
|
|
|
private uint NextContext()
|
|
{
|
|
_globalContext++;
|
|
if (_globalContext == 0u)
|
|
_globalContext++;
|
|
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))
|
|
return queue;
|
|
queue = new LinkedList<DialogInfo>();
|
|
_pending.Add(queueKey, queue);
|
|
return queue;
|
|
}
|
|
|
|
private bool TryCreateDialog(DialogInfo info)
|
|
{
|
|
RetailDialogType type = (RetailDialogType)info.Data.GetUInt32(
|
|
RetailDialogProperty.Type);
|
|
try
|
|
{
|
|
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)),
|
|
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)
|
|
{
|
|
if (info.View is null)
|
|
return;
|
|
IRetailDialogView view = info.View;
|
|
info.View = null;
|
|
view.DetachHandlers();
|
|
_openOrder.Remove(info);
|
|
_host.RemoveChild(view.Root);
|
|
RefreshModal();
|
|
}
|
|
|
|
private void DialogDone(DialogInfo info)
|
|
{
|
|
// DialogDone @ 0x004773C0 delivers the registered callback first, then
|
|
// broadcasts the CloseDialog notice, and only then removes the root.
|
|
try
|
|
{
|
|
info.Callback?.Invoke(info.Data);
|
|
DialogClosed?.Invoke(info.Context, info.Data);
|
|
}
|
|
finally
|
|
{
|
|
RemoveView(info);
|
|
}
|
|
}
|
|
|
|
private void RemoveView(DialogInfo info)
|
|
{
|
|
if (info.View is { } view)
|
|
{
|
|
view.DetachHandlers();
|
|
_host.RemoveChild(view.Root);
|
|
info.View = null;
|
|
_openOrder.Remove(info);
|
|
}
|
|
RefreshModal();
|
|
}
|
|
|
|
private void OpenNextDialog(uint queueKey)
|
|
{
|
|
// A callback invoked from DialogDone (above, in CloseDialog) may
|
|
// synchronously make a new dialog under this same queue key before
|
|
// control returns here — MakeDialog's queued branch will have
|
|
// already re-occupied _activeQueued[queueKey]. Retail's HashTable::add
|
|
// tolerates the duplicate; Dictionary.Add does not. Bail out: the
|
|
// re-entrant dialog's own eventual CloseDialog will drain the
|
|
// pending queue via its own OpenNextDialog call.
|
|
if (_activeQueued.ContainsKey(queueKey))
|
|
return;
|
|
|
|
if (TryActivateRetry(queueKey))
|
|
return;
|
|
|
|
if (!_pending.TryGetValue(queueKey, out LinkedList<DialogInfo>? queue)
|
|
|| queue.First is null)
|
|
return;
|
|
|
|
DialogInfo next = queue.First.Value;
|
|
queue.RemoveFirst();
|
|
if (queue.Count == 0)
|
|
_pending.Remove(queueKey);
|
|
_activeQueued.Add(queueKey, 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 (!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);
|
|
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 static bool IsPriority(DialogInfo info) =>
|
|
info.Data.GetBoolean(RetailDialogProperty.Priority);
|
|
|
|
private void QueueRetry(DialogInfo 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()
|
|
{
|
|
foreach ((uint queueKey, DialogInfo active) in _activeQueued)
|
|
{
|
|
int count = _pending.TryGetValue(queueKey, out LinkedList<DialogInfo>? queue)
|
|
? queue.Count
|
|
: 0;
|
|
active.View?.SetPendingCount(count);
|
|
}
|
|
}
|
|
|
|
private void RefreshModal()
|
|
{
|
|
_host.Modal = _openOrder.Count == 0 ? null : _openOrder[^1].View?.Root;
|
|
}
|
|
}
|