Two live-integration gaps the ef96c554 unit tests could not see:
1. World box stayed empty against ACE: ServerName (0xF7E1) arrives in the
SAME connect batch as CharacterList, so ServerNameReceived fires during
the handshake pump BEFORE the controller binding subscribes - the
event-only wiring proved the state and controller but never the live
ordering. StartCore now reads the durable WorldSession.ServerInfo after
connect exactly like the roster (ILiveSessionOperations.GetServerInfo,
default interface method so no fake breaks); the event remains for
post-connect updates. Pinned by a Start-level test.
2. The exit confirmation rendered far right of the screen: all three
retail dialog views centered against the raw window size while the
active screen lays out in the fixed 800x600 canvas - center-of-1920
is canvas-760, which the stretch pushes off-center. Views now center
against UiRoot.EffectiveCanvasSize (canvas while a pre-world screen is
active, window otherwise). Pinned by growing the window over the fixed
canvas in the exit-dialog test and asserting the scrim spans the canvas
with the popup centered at 400.
Runtime 1666, App 5100+6 skips, green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
144 lines
5.8 KiB
C#
144 lines
5.8 KiB
C#
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 : IRetailDialogView
|
|
{
|
|
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()
|
|
{
|
|
// Center against the space the tree lays out in — the fixed authored
|
|
// canvas while a pre-world screen is active (gate round 2: centering
|
|
// against the raw window width put the exit dialog far right of the
|
|
// stretched 800x600 canvas center).
|
|
var space = _host.EffectiveCanvasSize;
|
|
Root.Left = 0f;
|
|
Root.Top = 0f;
|
|
Root.Width = space.X;
|
|
Root.Height = space.Y;
|
|
_popup.Left = MathF.Round((Root.Width - _popup.Width) * 0.5f);
|
|
_popup.Top = MathF.Round((Root.Height - _popup.Height) * 0.5f);
|
|
}
|
|
}
|