feat(ui): port retail dialog factory lifecycle
Replace the single mutable confirmation service with retail's property-backed DialogFactory model: fresh DAT roots, context ids, queue groups, priority preemption, callback/close-notice ordering, and context cancellation. Route /die, server confirmation aborts, and guarded item use through focused semantic owners. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
43f7c7807c
commit
66bdae7a83
22 changed files with 1442 additions and 220 deletions
|
|
@ -2208,6 +2208,9 @@ public sealed class GameWindow : IDisposable
|
|||
Cursor: new AcDream.App.UI.RetailUiCursorBindings(
|
||||
cursorFeedbackController,
|
||||
retailCursorManager),
|
||||
Confirmations: new AcDream.App.UI.ConfirmationRuntimeBindings(
|
||||
(type, context, accepted) =>
|
||||
_liveSession?.SendConfirmationResponse(type, context, accepted)),
|
||||
StackSplitQuantity: _stackSplitQuantity,
|
||||
Plugins: _uiRegistry,
|
||||
Persistence: persistence,
|
||||
|
|
@ -2659,12 +2662,9 @@ public sealed class GameWindow : IDisposable
|
|||
onUseDone: error => _itemInteractionController?.CompleteUse(error),
|
||||
itemMana: ItemMana,
|
||||
onConfirmationRequest: request =>
|
||||
_retailUiRuntime?.ConfirmationDialogs?.Show(
|
||||
request.Message,
|
||||
accepted => session.SendConfirmationResponse(
|
||||
request.Type,
|
||||
request.ContextId,
|
||||
accepted)),
|
||||
_retailUiRuntime?.HandleConfirmationRequest(request),
|
||||
onConfirmationDone: done =>
|
||||
_retailUiRuntime?.HandleConfirmationDone(done),
|
||||
friends: Friends,
|
||||
squelch: Squelch,
|
||||
onDesiredComponents: components => DesiredComponents = components);
|
||||
|
|
@ -2764,7 +2764,7 @@ public sealed class GameWindow : IDisposable
|
|||
LastOutsideCorpsePosition: () =>
|
||||
LocalPlayer.GetPosition(0x0Eu),
|
||||
ShowConfirmation: (message, completed) =>
|
||||
_retailUiRuntime?.ConfirmationDialogs?.Show(message, completed),
|
||||
_retailUiRuntime?.ShowConfirmation(message, completed),
|
||||
Suicide: liveSession.SendSuicide,
|
||||
ClearChat: _ => chat.Clear(),
|
||||
SaveUi: name => _retailUiRuntime?.SaveNamedLayout(name),
|
||||
|
|
|
|||
91
src/AcDream.App/UI/GameplayConfirmationController.cs
Normal file
91
src/AcDream.App/UI/GameplayConfirmationController.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Ports the server-driven confirmation ownership in
|
||||
/// <c>gmGamePlayUI @ 0x004E9D70..0x004E9EB6</c>. The dialog factory broadcasts
|
||||
/// completion; this semantic owner retains the server type/context and sends the
|
||||
/// matching confirmation response.
|
||||
/// </summary>
|
||||
public sealed class GameplayConfirmationController : IDisposable
|
||||
{
|
||||
private readonly RetailDialogFactory _dialogs;
|
||||
private readonly Action<uint, uint, bool> _sendResponse;
|
||||
private uint _dialogContext;
|
||||
private uint _serverType;
|
||||
private uint _serverContext;
|
||||
private bool _disposed;
|
||||
|
||||
public GameplayConfirmationController(
|
||||
RetailDialogFactory dialogs,
|
||||
Action<uint, uint, bool> sendResponse)
|
||||
{
|
||||
_dialogs = dialogs ?? throw new ArgumentNullException(nameof(dialogs));
|
||||
_sendResponse = sendResponse ?? throw new ArgumentNullException(nameof(sendResponse));
|
||||
_dialogs.DialogClosed += OnDialogClosed;
|
||||
}
|
||||
|
||||
public uint ActiveDialogContext => _dialogContext;
|
||||
|
||||
public bool HandleRequest(GameEvents.CharacterConfirmationRequest request)
|
||||
{
|
||||
// ClientUISystem::Handle_Character__ConfirmationRequest @ 0x005640A0
|
||||
// routes types 2/3/5/6 to handlers that append " Continue?"; the generic
|
||||
// type-7 YesNo request preserves the server text verbatim. Types 1 and 4
|
||||
// have allegiance/fellowship semantic owners but use the same response
|
||||
// tuple, so this controller retains that tuple until those panels exist.
|
||||
// Each retail RecvNotice_* handler stores the tuple before calling the
|
||||
// shared maker, including the already-open case.
|
||||
_serverType = request.Type;
|
||||
_serverContext = request.ContextId;
|
||||
|
||||
// gmGamePlayUI::MakeGameplayConfirmationDialog @ 0x004EB890 refuses a
|
||||
// second gameplay confirmation while its context is non-zero.
|
||||
if (_dialogContext != 0u)
|
||||
return false;
|
||||
|
||||
string message = request.Type is 2u or 3u or 5u or 6u
|
||||
? request.Message + " Continue?"
|
||||
: request.Message;
|
||||
var data = RetailDialogData.Confirmation(message)
|
||||
.Set(RetailDialogProperty.ElementAttribute40, true);
|
||||
_dialogContext = _dialogs.MakeDialog(data);
|
||||
return _dialogContext != 0u;
|
||||
}
|
||||
|
||||
public bool HandleDone(GameEvents.CharacterConfirmationDone done)
|
||||
{
|
||||
// gmGamePlayUI::RecvNotice_AbortConfirmationRequest @ 0x004E9D70
|
||||
// matches both server fields before closing the local dialog context.
|
||||
if (_dialogContext == 0u
|
||||
|| done.Type != _serverType
|
||||
|| done.ContextId != _serverContext)
|
||||
return false;
|
||||
return _dialogs.CloseDialog(_dialogContext);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_dialogs.DialogClosed -= OnDialogClosed;
|
||||
}
|
||||
|
||||
private void OnDialogClosed(uint context, RetailDialogData data)
|
||||
{
|
||||
if (context != _dialogContext
|
||||
|| data.GetUInt32(RetailDialogProperty.Type) !=
|
||||
(uint)RetailDialogType.Confirmation)
|
||||
return;
|
||||
|
||||
bool accepted = data.GetBoolean(RetailDialogProperty.ConfirmationResult);
|
||||
// gmGamePlayUI::CloseGameplayConfirmationDialog @ 0x004E9E80 sends
|
||||
// before clearing its retained server/dialog context fields.
|
||||
_sendResponse(_serverType, _serverContext, accepted);
|
||||
_dialogContext = 0u;
|
||||
_serverType = 0u;
|
||||
_serverContext = 0u;
|
||||
}
|
||||
}
|
||||
|
|
@ -288,6 +288,21 @@ public sealed class ItemInteractionController : IDisposable
|
|||
public bool AcquireSelfTarget()
|
||||
=> IsTargetModeActive && AcquireTarget(_playerGuid());
|
||||
|
||||
/// <summary>
|
||||
/// Completes the positive branch of retail
|
||||
/// <c>ClientUISystem::UsageCallback @ 0x00565B20</c>. Confirmation policy has
|
||||
/// already consumed the original activation, so acceptance sends the retained
|
||||
/// object id and enters the same busy state as an ordinary use.
|
||||
/// </summary>
|
||||
public bool ExecuteConfirmedUse(uint objectId)
|
||||
{
|
||||
if (objectId == 0u || _sendUse is null)
|
||||
return false;
|
||||
_sendUse(objectId);
|
||||
_busyCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CancelTargetMode()
|
||||
{
|
||||
if (!IsAnyTargetModeActive) return;
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Retained implementation of retail <c>DialogFactory</c>'s confirmation queue.
|
||||
/// The visual tree is element <c>0x15</c> from the shared dialog catalog resolved by
|
||||
/// <c>GetDIDByEnum(2, 5)</c>; callbacks mirror property <c>0x92</c> from
|
||||
/// <c>ConfirmationDialog::ListenToElementMessage @ 0x00476670</c>.
|
||||
/// </summary>
|
||||
public sealed class RetailConfirmationDialogService
|
||||
{
|
||||
public const uint RootElementId = 0x15u;
|
||||
public const uint PopupElementId = 0x3Du;
|
||||
public const uint MessageElementId = 0x3Eu;
|
||||
public const uint AcceptButtonId = 0x17u;
|
||||
public const uint RejectButtonId = 0x19u;
|
||||
|
||||
private sealed record Request(string Message, Action<bool> Completed);
|
||||
|
||||
private readonly UiRoot _host;
|
||||
private readonly UiDialogRoot _dialog;
|
||||
private readonly UiElement _popup;
|
||||
private readonly UiText _message;
|
||||
private readonly UiButton _accept;
|
||||
private readonly UiButton _reject;
|
||||
private readonly Queue<Request> _queue = new();
|
||||
private readonly float _basePopupHeight;
|
||||
private readonly float _baseMessageHeight;
|
||||
private Request? _current;
|
||||
|
||||
public RetailConfirmationDialogService(UiRoot host, ImportedLayout layout)
|
||||
{
|
||||
_host = host ?? throw new ArgumentNullException(nameof(host));
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
_dialog = layout.Root as UiDialogRoot
|
||||
?? throw new ArgumentException("Confirmation layout root is not a UiDialogRoot.", nameof(layout));
|
||||
_popup = layout.FindElement(PopupElementId)
|
||||
?? throw new ArgumentException("Confirmation layout is missing popup element 0x3D.", nameof(layout));
|
||||
_message = layout.FindElement(MessageElementId) as UiText
|
||||
?? throw new ArgumentException("Confirmation layout is missing text element 0x3E.", nameof(layout));
|
||||
_accept = layout.FindElement(AcceptButtonId) as UiButton
|
||||
?? throw new ArgumentException("Confirmation layout is missing accept button 0x17.", nameof(layout));
|
||||
_reject = layout.FindElement(RejectButtonId) as UiButton
|
||||
?? throw new ArgumentException("Confirmation layout is missing reject button 0x19.", nameof(layout));
|
||||
|
||||
_basePopupHeight = _popup.Height;
|
||||
_baseMessageHeight = _message.Height;
|
||||
_popup.LayoutPolicy = null;
|
||||
_popup.Anchors = AnchorEdges.None;
|
||||
_message.LayoutPolicy = null;
|
||||
_message.Anchors = AnchorEdges.None;
|
||||
_message.Padding = 0f;
|
||||
_message.Selectable = false;
|
||||
_dialog.Cancel = () => Complete(false);
|
||||
_accept.OnClick = () => Complete(true);
|
||||
_reject.OnClick = () => Complete(false);
|
||||
}
|
||||
|
||||
public bool IsOpen => _current is not null;
|
||||
public int PendingCount => _queue.Count;
|
||||
|
||||
public void Show(string message, Action<bool> completed)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(message);
|
||||
ArgumentNullException.ThrowIfNull(completed);
|
||||
_queue.Enqueue(new Request(message, completed));
|
||||
if (_current is null)
|
||||
ShowNext();
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
if (_current is null) return;
|
||||
SizeAndCenter();
|
||||
}
|
||||
|
||||
private void ShowNext()
|
||||
{
|
||||
if (_queue.Count == 0) return;
|
||||
_current = _queue.Dequeue();
|
||||
|
||||
float maximumWidth = Math.Max(1f, _message.Width - 2f * _message.Padding);
|
||||
Func<string, float> measure = _message.DatFont is { } font
|
||||
? font.MeasureWidth
|
||||
: static text => text.Length * 8f;
|
||||
IReadOnlyList<string> wrapped = UiText.WrapWords(
|
||||
_current.Message,
|
||||
measure,
|
||||
maximumWidth);
|
||||
var lines = new UiText.Line[wrapped.Count];
|
||||
for (int i = 0; i < wrapped.Count; i++)
|
||||
lines[i] = new UiText.Line(wrapped[i], _message.DefaultColor);
|
||||
_message.LinesProvider = () => lines;
|
||||
|
||||
float lineHeight = _message.DatFont?.LineHeight ?? 16f;
|
||||
_message.Height = Math.Max(_baseMessageHeight, lines.Length * lineHeight);
|
||||
_popup.Height = _basePopupHeight + (_message.Height - _baseMessageHeight);
|
||||
|
||||
SizeAndCenter();
|
||||
_host.AddChild(_dialog);
|
||||
_host.Modal = _dialog;
|
||||
}
|
||||
|
||||
private void SizeAndCenter()
|
||||
{
|
||||
_dialog.Left = 0f;
|
||||
_dialog.Top = 0f;
|
||||
_dialog.Width = _host.Width;
|
||||
_dialog.Height = _host.Height;
|
||||
_popup.Left = MathF.Round((_dialog.Width - _popup.Width) * 0.5f);
|
||||
_popup.Top = MathF.Round((_dialog.Height - _popup.Height) * 0.5f);
|
||||
}
|
||||
|
||||
private void Complete(bool accepted)
|
||||
{
|
||||
Request? completed = _current;
|
||||
if (completed is null) return;
|
||||
_current = null;
|
||||
_host.RemoveChild(_dialog);
|
||||
completed.Completed(accepted);
|
||||
ShowNext();
|
||||
}
|
||||
}
|
||||
139
src/AcDream.App/UI/Layout/RetailConfirmationDialogView.cs
Normal file
139
src/AcDream.App/UI/Layout/RetailConfirmationDialogView.cs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// One live type-1 retail confirmation root. Instances are intentionally short-lived:
|
||||
/// <c>DialogFactory::CreateDialog_ @ 0x00477AD0</c> creates a fresh catalog root for
|
||||
/// every displayed <c>DialogInfo</c>.
|
||||
/// </summary>
|
||||
internal sealed class RetailConfirmationDialogView
|
||||
{
|
||||
public const uint RootElementId = 0x15u;
|
||||
public const uint AcceptButtonId = 0x17u;
|
||||
public const uint RejectButtonId = 0x19u;
|
||||
public const uint PendingDisplayId = 0x33u;
|
||||
public const uint PendingDisplayTextId = 0x34u;
|
||||
public const uint PopupElementId = 0x3Du;
|
||||
public const uint MessageElementId = 0x3Eu;
|
||||
|
||||
private readonly UiRoot _host;
|
||||
private readonly RetailDialogData _data;
|
||||
private readonly uint _context;
|
||||
private readonly Action<uint> _closeDialog;
|
||||
private readonly UiElement _popup;
|
||||
private readonly UiText _message;
|
||||
private readonly UiButton _accept;
|
||||
private readonly UiButton _reject;
|
||||
private readonly UiElement? _pendingDisplay;
|
||||
private readonly float _basePopupHeight;
|
||||
private readonly float _baseMessageHeight;
|
||||
|
||||
public RetailConfirmationDialogView(
|
||||
UiRoot host,
|
||||
ImportedLayout layout,
|
||||
RetailDialogData data,
|
||||
uint context,
|
||||
Action<uint> closeDialog)
|
||||
{
|
||||
_host = host ?? throw new ArgumentNullException(nameof(host));
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
_data = data ?? throw new ArgumentNullException(nameof(data));
|
||||
_context = context;
|
||||
_closeDialog = closeDialog ?? throw new ArgumentNullException(nameof(closeDialog));
|
||||
|
||||
Root = layout.Root as UiDialogRoot
|
||||
?? throw new ArgumentException("Confirmation layout root is not a UiDialogRoot.", nameof(layout));
|
||||
_popup = layout.FindElement(PopupElementId)
|
||||
?? throw new ArgumentException("Confirmation layout is missing popup element 0x3D.", nameof(layout));
|
||||
_message = layout.FindElement(MessageElementId) as UiText
|
||||
?? throw new ArgumentException("Confirmation layout is missing text element 0x3E.", nameof(layout));
|
||||
_accept = layout.FindElement(AcceptButtonId) as UiButton
|
||||
?? throw new ArgumentException("Confirmation layout is missing accept button 0x17.", nameof(layout));
|
||||
_reject = layout.FindElement(RejectButtonId) as UiButton
|
||||
?? throw new ArgumentException("Confirmation layout is missing reject button 0x19.", nameof(layout));
|
||||
_pendingDisplay = layout.FindElement(PendingDisplayId);
|
||||
|
||||
_basePopupHeight = _popup.Height;
|
||||
_baseMessageHeight = _message.Height;
|
||||
_popup.LayoutPolicy = null;
|
||||
_popup.Anchors = AnchorEdges.None;
|
||||
_message.LayoutPolicy = null;
|
||||
_message.Anchors = AnchorEdges.None;
|
||||
_message.Padding = 0f;
|
||||
_message.Selectable = false;
|
||||
|
||||
if (_data.GetString(RetailDialogProperty.AcceptLabel) is { } acceptLabel)
|
||||
_accept.Label = acceptLabel;
|
||||
if (_data.GetString(RetailDialogProperty.RejectLabel) is { } rejectLabel)
|
||||
_reject.Label = rejectLabel;
|
||||
|
||||
Root.Cancel = Reject;
|
||||
_accept.OnClick = Accept;
|
||||
_reject.OnClick = Reject;
|
||||
SetMessage(_data.GetString(RetailDialogProperty.Message) ?? string.Empty);
|
||||
SizeAndCenter();
|
||||
}
|
||||
|
||||
public UiDialogRoot Root { get; }
|
||||
|
||||
public RetailDialogData Data => _data;
|
||||
|
||||
public void Tick() => SizeAndCenter();
|
||||
|
||||
public void SetPendingCount(int count)
|
||||
{
|
||||
// Dialog::UpdatePendingDialogDisplay @ 0x00476930 resolves children
|
||||
// 0x33/0x34 and calls SetState(0x19) for an empty queue or SetState(0x18)
|
||||
// for a non-empty queue. Do not synthesize count text: retail does not.
|
||||
if (_pendingDisplay is IUiDatStateful stateful)
|
||||
stateful.TrySetRetailState(count == 0 ? 0x19u : 0x18u);
|
||||
}
|
||||
|
||||
public void DetachHandlers()
|
||||
{
|
||||
Root.Cancel = null;
|
||||
_accept.OnClick = null;
|
||||
_reject.OnClick = null;
|
||||
}
|
||||
|
||||
private void Accept()
|
||||
{
|
||||
// ConfirmationDialog::ListenToElementMessage @ 0x00476670.
|
||||
_data.Set(RetailDialogProperty.ConfirmationResult, true);
|
||||
_closeDialog(_context);
|
||||
}
|
||||
|
||||
private void Reject()
|
||||
{
|
||||
// ConfirmationDialog::CancelDialog @ 0x00476770 uses the same false
|
||||
// result for the reject button and Escape/cancel.
|
||||
_data.Set(RetailDialogProperty.ConfirmationResult, false);
|
||||
_closeDialog(_context);
|
||||
}
|
||||
|
||||
private void SetMessage(string text)
|
||||
{
|
||||
float maximumWidth = Math.Max(1f, _message.Width - 2f * _message.Padding);
|
||||
Func<string, float> measure = _message.DatFont is { } font
|
||||
? font.MeasureWidth
|
||||
: static value => value.Length * 8f;
|
||||
IReadOnlyList<string> wrapped = UiText.WrapWords(text, measure, maximumWidth);
|
||||
var lines = new UiText.Line[wrapped.Count];
|
||||
for (int i = 0; i < wrapped.Count; i++)
|
||||
lines[i] = new UiText.Line(wrapped[i], _message.DefaultColor);
|
||||
_message.LinesProvider = () => lines;
|
||||
|
||||
float lineHeight = _message.DatFont?.LineHeight ?? 16f;
|
||||
_message.Height = Math.Max(_baseMessageHeight, lines.Length * lineHeight);
|
||||
_popup.Height = _basePopupHeight + (_message.Height - _baseMessageHeight);
|
||||
}
|
||||
|
||||
private void SizeAndCenter()
|
||||
{
|
||||
Root.Left = 0f;
|
||||
Root.Top = 0f;
|
||||
Root.Width = _host.Width;
|
||||
Root.Height = _host.Height;
|
||||
_popup.Left = MathF.Round((Root.Width - _popup.Width) * 0.5f);
|
||||
_popup.Top = MathF.Round((Root.Height - _popup.Height) * 0.5f);
|
||||
}
|
||||
}
|
||||
113
src/AcDream.App/UI/Layout/RetailDialogData.cs
Normal file
113
src/AcDream.App/UI/Layout/RetailDialogData.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Property identifiers consumed by retail's <c>Dialog</c> and
|
||||
/// <c>DialogFactory</c> implementations.
|
||||
/// </summary>
|
||||
public static class RetailDialogProperty
|
||||
{
|
||||
public const uint Priority = 0x8Du;
|
||||
public const uint Type = 0x8Eu;
|
||||
public const uint AcceptLabel = 0x90u;
|
||||
public const uint RejectLabel = 0x91u;
|
||||
public const uint ConfirmationResult = 0x92u;
|
||||
/// <summary>
|
||||
/// When true, <c>Dialog::SetData @ 0x00476BE0</c> sets UIElement boolean
|
||||
/// attribute <c>0x40</c>. The Keystone-owned attribute name is unavailable.
|
||||
/// </summary>
|
||||
public const uint ElementAttribute40 = 0xACu;
|
||||
public const uint QueueKey = 0xC3u;
|
||||
public const uint Message = 0xC5u;
|
||||
public const uint UsageObjectId = 0x1000003Du;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail dialog types selected by property <c>0x8E</c> in
|
||||
/// <c>DialogFactory::CreateDialog_ @ 0x00477AD0</c>.
|
||||
/// </summary>
|
||||
public enum RetailDialogType : uint
|
||||
{
|
||||
Confirmation = 1,
|
||||
Wait = 2,
|
||||
Message = 3,
|
||||
TextInput = 4,
|
||||
ConfirmationTextInput = 5,
|
||||
Menu = 6,
|
||||
ConfirmationMenu = 7,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modern typed carrier for retail's <c>PropertyCollection</c> dialog contract.
|
||||
/// The raw numeric keys remain visible because type-specific presenters and semantic
|
||||
/// callbacks both extend the same collection in retail.
|
||||
/// </summary>
|
||||
public sealed class RetailDialogData
|
||||
{
|
||||
private readonly Dictionary<uint, object> _values = new();
|
||||
|
||||
public IReadOnlyDictionary<uint, object> Values => _values;
|
||||
|
||||
public RetailDialogData Set<T>(uint propertyId, T value) where T : notnull
|
||||
{
|
||||
_values[propertyId] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Contains(uint propertyId) => _values.ContainsKey(propertyId);
|
||||
|
||||
public bool TryGet<T>(uint propertyId, out T value)
|
||||
{
|
||||
if (_values.TryGetValue(propertyId, out object? raw) && raw is T typed)
|
||||
{
|
||||
value = typed;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool GetBoolean(uint propertyId, bool defaultValue = false)
|
||||
=> _values.TryGetValue(propertyId, out object? raw)
|
||||
? raw switch
|
||||
{
|
||||
bool value => value,
|
||||
byte value => value != 0,
|
||||
int value => value != 0,
|
||||
uint value => value != 0,
|
||||
_ => defaultValue,
|
||||
}
|
||||
: defaultValue;
|
||||
|
||||
public uint GetUInt32(uint propertyId, uint defaultValue = 0u)
|
||||
=> _values.TryGetValue(propertyId, out object? raw)
|
||||
? raw switch
|
||||
{
|
||||
byte value => value,
|
||||
ushort value => value,
|
||||
int value when value >= 0 => (uint)value,
|
||||
uint value => value,
|
||||
Enum value => Convert.ToUInt32(value),
|
||||
_ => defaultValue,
|
||||
}
|
||||
: defaultValue;
|
||||
|
||||
public string? GetString(uint propertyId)
|
||||
=> _values.TryGetValue(propertyId, out object? raw) ? raw as string : null;
|
||||
|
||||
public RetailDialogData Clone()
|
||||
{
|
||||
var clone = new RetailDialogData();
|
||||
foreach ((uint propertyId, object value) in _values)
|
||||
clone._values.Add(propertyId, value);
|
||||
return clone;
|
||||
}
|
||||
|
||||
public static RetailDialogData Confirmation(string message)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(message);
|
||||
return new RetailDialogData()
|
||||
.Set(RetailDialogProperty.Type, RetailDialogType.Confirmation)
|
||||
.Set(RetailDialogProperty.Message, message);
|
||||
}
|
||||
}
|
||||
323
src/AcDream.App/UI/Layout/RetailDialogFactory.cs
Normal file
323
src/AcDream.App/UI/Layout/RetailDialogFactory.cs
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
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 Action<RetailDialogData>? Callback { get; init; }
|
||||
public RetailConfirmationDialogView? 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 List<DialogInfo> _openOrder = new();
|
||||
private uint _globalContext;
|
||||
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);
|
||||
|
||||
/// <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,
|
||||
Callback = callback,
|
||||
};
|
||||
|
||||
if (queueKey == NonQueuedKey)
|
||||
{
|
||||
_activeNonQueued.Add(context, info);
|
||||
CreateDialog(info);
|
||||
return context;
|
||||
}
|
||||
|
||||
if (!_activeQueued.TryGetValue(queueKey, out DialogInfo? current))
|
||||
{
|
||||
_activeQueued.Add(queueKey, info);
|
||||
CreateDialog(info);
|
||||
return context;
|
||||
}
|
||||
|
||||
LinkedList<DialogInfo> queue = PendingQueue(queueKey);
|
||||
if (!ownedData.GetBoolean(RetailDialogProperty.Priority))
|
||||
{
|
||||
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;
|
||||
CreateDialog(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>
|
||||
/// 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;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
foreach (DialogInfo info in _openOrder.ToArray())
|
||||
info.View?.Tick();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>DialogFactory::Reset @ 0x00477950</c> completes active and pending
|
||||
/// infos before clearing the factory.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
DialogInfo[] infos = _activeNonQueued.Values
|
||||
.Concat(_activeQueued.Values)
|
||||
.Concat(_pending.Values.SelectMany(static queue => queue))
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
_activeNonQueued.Clear();
|
||||
_activeQueued.Clear();
|
||||
_pending.Clear();
|
||||
foreach (DialogInfo info in infos)
|
||||
DialogDone(info);
|
||||
RefreshModal();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
Reset();
|
||||
}
|
||||
|
||||
private uint NextContext()
|
||||
{
|
||||
_globalContext++;
|
||||
if (_globalContext == 0u)
|
||||
_globalContext++;
|
||||
return _globalContext;
|
||||
}
|
||||
|
||||
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 void CreateDialog(DialogInfo info)
|
||||
{
|
||||
RetailDialogType type = (RetailDialogType)info.Data.GetUInt32(RetailDialogProperty.Type);
|
||||
if (type != RetailDialogType.Confirmation)
|
||||
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}.");
|
||||
var view = 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;
|
||||
UpdatePendingDialogDisplays();
|
||||
DialogOpened?.Invoke(info.Context);
|
||||
}
|
||||
|
||||
private void Suspend(DialogInfo info)
|
||||
{
|
||||
if (info.View is null)
|
||||
return;
|
||||
RetailConfirmationDialogView 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)
|
||||
{
|
||||
info.View = null;
|
||||
view.DetachHandlers();
|
||||
_openOrder.Remove(info);
|
||||
_host.RemoveChild(view.Root);
|
||||
}
|
||||
RefreshModal();
|
||||
}
|
||||
|
||||
private void OpenNextDialog(uint queueKey)
|
||||
{
|
||||
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);
|
||||
CreateDialog(next);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
65
src/AcDream.App/UI/RetailItemConfirmationController.cs
Normal file
65
src/AcDream.App/UI/RetailItemConfirmationController.cs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Retained UI owner for the item-use confirmations emitted by
|
||||
/// <see cref="ItemInteractionController"/>. Message/data construction mirrors
|
||||
/// <c>ClientUISystem::UsageConfirmation_* @ 0x00566420..0x005669EA</c> and
|
||||
/// completion mirrors <c>ClientUISystem::UsageCallback @ 0x00565B20</c>.
|
||||
/// </summary>
|
||||
public sealed class RetailItemConfirmationController : IDisposable
|
||||
{
|
||||
internal const string PlayerKillerMessage =
|
||||
"Using this altar will make you a player killer, able to attack or be attacked by other player killers. Are you sure you want to do this?";
|
||||
internal const string NonPlayerKillerMessage =
|
||||
"Using this altar will make you a non-player killer, unable to attack or be attacked by other player killers. Are you sure you want to do this?";
|
||||
internal const string VolatileRareMessage =
|
||||
"Are you sure you want to use this rare item?";
|
||||
|
||||
private readonly RetailDialogFactory _dialogs;
|
||||
private readonly ItemInteractionController _items;
|
||||
private bool _disposed;
|
||||
|
||||
public RetailItemConfirmationController(
|
||||
RetailDialogFactory dialogs,
|
||||
ItemInteractionController items)
|
||||
{
|
||||
_dialogs = dialogs ?? throw new ArgumentNullException(nameof(dialogs));
|
||||
_items = items ?? throw new ArgumentNullException(nameof(items));
|
||||
_items.PolicyActionRequested += OnPolicyActionRequested;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_items.PolicyActionRequested -= OnPolicyActionRequested;
|
||||
}
|
||||
|
||||
private void OnPolicyActionRequested(ItemPolicyAction action)
|
||||
{
|
||||
string? message = action.Kind switch
|
||||
{
|
||||
ItemPolicyActionKind.ConfirmPlayerKillerSwitch => PlayerKillerMessage,
|
||||
ItemPolicyActionKind.ConfirmNonPlayerKillerSwitch => NonPlayerKillerMessage,
|
||||
ItemPolicyActionKind.ConfirmVolatileRare => VolatileRareMessage,
|
||||
_ => null,
|
||||
};
|
||||
if (message is null)
|
||||
return;
|
||||
|
||||
RetailDialogData data = RetailDialogData.Confirmation(message)
|
||||
.Set(RetailDialogProperty.UsageObjectId, action.ObjectId);
|
||||
_dialogs.MakeDialog(data, OnUsageDialogDone);
|
||||
}
|
||||
|
||||
private void OnUsageDialogDone(RetailDialogData data)
|
||||
{
|
||||
if (!data.GetBoolean(RetailDialogProperty.ConfirmationResult))
|
||||
return;
|
||||
uint objectId = data.GetUInt32(RetailDialogProperty.UsageObjectId);
|
||||
_items.ExecuteConfirmedUse(objectId);
|
||||
}
|
||||
}
|
||||
|
|
@ -107,6 +107,9 @@ public sealed record RetailUiCursorBindings(
|
|||
CursorFeedbackController Feedback,
|
||||
RetailCursorManager Manager);
|
||||
|
||||
public sealed record ConfirmationRuntimeBindings(
|
||||
Action<uint, uint, bool> SendResponse);
|
||||
|
||||
public sealed record RetailUiRuntimeBindings(
|
||||
UiHost Host,
|
||||
RetailUiAssets Assets,
|
||||
|
|
@ -120,6 +123,7 @@ public sealed record RetailUiRuntimeBindings(
|
|||
CharacterRuntimeBindings Character,
|
||||
InventoryRuntimeBindings Inventory,
|
||||
RetailUiCursorBindings Cursor,
|
||||
ConfirmationRuntimeBindings Confirmations,
|
||||
StackSplitQuantityState StackSplitQuantity,
|
||||
BufferedUiRegistry? Plugins,
|
||||
RetailUiPersistenceBindings? Persistence,
|
||||
|
|
@ -136,6 +140,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
private StackSplitQuantityState StackSplitQuantity => _bindings.StackSplitQuantity;
|
||||
private readonly RetailWindowLayoutPersistence? _persistence;
|
||||
private readonly RetailUiAutomationScriptRunner? _automation;
|
||||
private GameplayConfirmationController? _gameplayConfirmationController;
|
||||
private RetailItemConfirmationController? _itemConfirmationController;
|
||||
private bool _disposed;
|
||||
|
||||
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
|
||||
|
|
@ -148,7 +154,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
MountToolbar();
|
||||
MountCombat();
|
||||
MountJumpPowerbar();
|
||||
MountConfirmationDialogs();
|
||||
MountDialogFactory();
|
||||
MountCharacter();
|
||||
MountPlugins();
|
||||
MountInventory();
|
||||
|
|
@ -191,7 +197,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
public SelectedObjectController? SelectedObjectController { get; private set; }
|
||||
public UiViewport? PaperdollViewportWidget { get; private set; }
|
||||
public UiNineSlicePanel? InventoryFrame { get; private set; }
|
||||
public RetailConfirmationDialogService? ConfirmationDialogs { get; private set; }
|
||||
public RetailDialogFactory? DialogFactory { get; private set; }
|
||||
|
||||
public static RetailUiRuntime Mount(RetailUiRuntimeBindings bindings)
|
||||
{
|
||||
|
|
@ -212,7 +218,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
FpsController?.Tick();
|
||||
JumpPowerbarController?.Tick();
|
||||
SelectedObjectController?.Tick(deltaSeconds);
|
||||
ConfirmationDialogs?.Tick();
|
||||
DialogFactory?.Tick();
|
||||
Host.Tick(deltaSeconds);
|
||||
_automation?.Tick(deltaSeconds);
|
||||
}
|
||||
|
|
@ -222,6 +228,21 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
public bool HandleInputAction(AcDream.UI.Abstractions.Input.InputAction action)
|
||||
=> ToolbarInputController?.Handle(action) == true;
|
||||
|
||||
public bool HandleConfirmationRequest(GameEvents.CharacterConfirmationRequest request)
|
||||
=> _gameplayConfirmationController?.HandleRequest(request) == true;
|
||||
|
||||
public bool HandleConfirmationDone(GameEvents.CharacterConfirmationDone done)
|
||||
=> _gameplayConfirmationController?.HandleDone(done) == true;
|
||||
|
||||
public uint ShowConfirmation(string message, Action<bool> completed)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(message);
|
||||
ArgumentNullException.ThrowIfNull(completed);
|
||||
return DialogFactory?.MakeConfirmation(
|
||||
message,
|
||||
data => completed(data.GetBoolean(RetailDialogProperty.ConfirmationResult))) ?? 0u;
|
||||
}
|
||||
|
||||
public void UpdateCursor(IEnumerable<IMouse> mice)
|
||||
{
|
||||
CursorFeedback feedback = _bindings.Cursor.Feedback.Update(Host.Root);
|
||||
|
|
@ -645,34 +666,49 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
Console.WriteLine("[D.6] retail jump bar from gmFloatyPowerBarUI LayoutDesc 0x21000072.");
|
||||
}
|
||||
|
||||
private void MountConfirmationDialogs()
|
||||
private void MountDialogFactory()
|
||||
{
|
||||
ImportedLayout? layout;
|
||||
uint layoutId;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
{
|
||||
// DialogFactory::CreateDialog_ @ 0x00477AD0 calls
|
||||
// GetDIDByEnum(2, 5), then creates root element 0x15.
|
||||
// DialogFactory::CreateDialog_ @ 0x00477AD0 resolves the shared
|
||||
// catalog through GetDIDByEnum(2, 5). Each shown DialogInfo then
|
||||
// creates a fresh type-specific root from that catalog.
|
||||
layoutId = RetailDataIdResolver.Resolve(_bindings.Assets.Dats, 2u, 5u);
|
||||
layout = layoutId == 0u
|
||||
? null
|
||||
: LayoutImporter.Import(
|
||||
_bindings.Assets.Dats,
|
||||
layoutId,
|
||||
RetailConfirmationDialogService.RootElementId,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
_bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveFont);
|
||||
}
|
||||
|
||||
if (layout is null)
|
||||
if (layoutId == 0u)
|
||||
{
|
||||
Console.WriteLine("[UI] confirmation dialog catalog could not be resolved.");
|
||||
Console.WriteLine("[UI] retail dialog catalog could not be resolved.");
|
||||
return;
|
||||
}
|
||||
|
||||
ConfirmationDialogs = new RetailConfirmationDialogService(Host.Root, layout);
|
||||
Console.WriteLine($"[UI] retail confirmation dialogs from LayoutDesc 0x{layoutId:X8} element 0x15.");
|
||||
ImportedLayout? CreateLayout(RetailDialogType type)
|
||||
{
|
||||
uint rootElementId = RetailDialogFactory.RootElementId(type);
|
||||
if (rootElementId == 0u)
|
||||
return null;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
{
|
||||
return LayoutImporter.Import(
|
||||
_bindings.Assets.Dats,
|
||||
layoutId,
|
||||
rootElementId,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
_bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveFont);
|
||||
}
|
||||
}
|
||||
|
||||
DialogFactory = new RetailDialogFactory(Host.Root, CreateLayout);
|
||||
_gameplayConfirmationController = new GameplayConfirmationController(
|
||||
DialogFactory,
|
||||
_bindings.Confirmations.SendResponse);
|
||||
_itemConfirmationController = new RetailItemConfirmationController(
|
||||
DialogFactory,
|
||||
ItemInteraction);
|
||||
Console.WriteLine(
|
||||
$"[UI] retail DialogFactory from LayoutDesc 0x{layoutId:X8}; confirmation root 0x15.");
|
||||
}
|
||||
|
||||
private (uint[] Regular, uint[] Ghosted, uint[]? Empty) LoadToolbarDigits()
|
||||
|
|
@ -889,6 +925,9 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
_disposed = true;
|
||||
_persistence?.Dispose();
|
||||
Host.WindowManager.WindowVisibilityChanged -= OnWindowVisibilityChanged;
|
||||
_itemConfirmationController?.Dispose();
|
||||
_gameplayConfirmationController?.Dispose();
|
||||
DialogFactory?.Dispose();
|
||||
Host.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ public static class GameEventWiring
|
|||
Action<uint /*weenieError*/>? onUseDone = null,
|
||||
ItemManaState? itemMana = null,
|
||||
Action<GameEvents.CharacterConfirmationRequest>? onConfirmationRequest = null,
|
||||
Action<GameEvents.CharacterConfirmationDone>? onConfirmationDone = null,
|
||||
FriendsState? friends = null,
|
||||
SquelchState? squelch = null,
|
||||
Action<IReadOnlyList<(uint Id, uint Amount)>>? onDesiredComponents = null)
|
||||
|
|
@ -123,6 +124,16 @@ public static class GameEventWiring
|
|||
});
|
||||
}
|
||||
|
||||
if (onConfirmationDone is not null)
|
||||
{
|
||||
dispatcher.Register(GameEventType.CharacterConfirmationDone, e =>
|
||||
{
|
||||
var done = GameEvents.ParseCharacterConfirmationDone(e.Payload.Span);
|
||||
if (done is not null)
|
||||
onConfirmationDone(done.Value);
|
||||
});
|
||||
}
|
||||
|
||||
if (friends is not null)
|
||||
{
|
||||
dispatcher.Register(GameEventType.FriendsListUpdate, e =>
|
||||
|
|
|
|||
|
|
@ -505,6 +505,22 @@ public static class GameEvents
|
|||
catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 0x0276 CharacterConfirmationDone — server cancellation/completion of the
|
||||
/// outstanding confirmation tuple. Retail dispatches the same type/context
|
||||
/// pair to <c>RecvNotice_AbortConfirmationRequest</c>.
|
||||
/// </summary>
|
||||
public readonly record struct CharacterConfirmationDone(uint Type, uint ContextId);
|
||||
|
||||
public static CharacterConfirmationDone? ParseCharacterConfirmationDone(
|
||||
ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 8) return null;
|
||||
return new CharacterConfirmationDone(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)));
|
||||
}
|
||||
|
||||
// ── Shared string reader (matches LoginRequest.ReadString16L) ───────────
|
||||
|
||||
private static string ReadString16L(ReadOnlySpan<byte> source, ref int pos)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue