feat(chat): port retail client command families

Expand the typed client-command boundary across travel, character queries, local UI and layout controls, AFK and consent, emotes, friends, squelch and filters, and fill-components. Preserve retail packet layouts and queue ownership, import the confirmation dialog, and keep authoritative social state in Core.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-13 14:50:15 +02:00
parent 5a45a7ac7f
commit 9ea579bdd0
47 changed files with 6659 additions and 99 deletions

View file

@ -82,6 +82,7 @@ public static class DatWidgetFactory
0xD => new UiViewport(), // UIElement_Viewport — 3-D mini-scene blit leaf
11 => BuildScrollbar(info, resolve), // UIElement_Scrollbar (reg :124137)
12 => BuildText(info, resolve, elementFont, stringResolve), // UIElement_Text
0x13 => new UiDialogRoot(), // ConfirmationDialog
0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots
0x10000035u => BuildCheckbox(info, resolve, elementFont, stringResolve), // UIOption_Checkbox
_ => new UiDatElement(info, resolve), // generic fallback (incl. Type 3 chrome/containers)

View file

@ -221,6 +221,24 @@ public static class LayoutImporter
};
}
/// <summary>
/// Retail <c>UIElementManager::CreateRootElementByDataID</c> counterpart: resolve one
/// authored root from a catalog-style LayoutDesc instead of instantiating every
/// top-level template. DialogFactory uses this path for the shared dialog catalog.
/// </summary>
public static ElementInfo? ImportInfos(
DatCollection dats,
uint layoutId,
uint rootElementId)
{
var ld = dats.Get<LayoutDesc>(layoutId);
if (ld is null) return null;
ElementDesc? root = FindDesc(ld, rootElementId);
return root is null
? null
: Resolve(dats, root, new HashSet<(uint, uint)>());
}
/// <summary>
/// Dat shell: load the LayoutDesc, resolve inheritance for every top-level
/// element, and build the widget tree. Returns null if the layout is absent
@ -260,6 +278,21 @@ public static class LayoutImporter
return Build(rootInfo, resolve, datFont, fontResolve, strings.Resolve);
}
/// <summary>Import one selected root from a catalog-style LayoutDesc.</summary>
public static ImportedLayout? Import(
DatCollection dats,
uint layoutId,
uint rootElementId,
Func<uint, (uint, int, int)> resolve,
UiDatFont? datFont,
Func<uint, UiDatFont?>? fontResolve = null)
{
var rootInfo = ImportInfos(dats, layoutId, rootElementId);
if (rootInfo is null) return null;
var strings = new DatStringResolver(dats);
return Build(rootInfo, resolve, datFont, fontResolve, strings.Resolve);
}
// ── Inheritance resolution ────────────────────────────────────────────────
/// <summary>True when a pure-container leaf should inherit its base's subtree (the

View file

@ -0,0 +1,122 @@
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();
}
}