fix #385: Options dropdowns — white centered text + size-to-content popup

User gate report (Campaign OP happy-testing round, 2026-08-13): every
Config-tab dropdown drew its text gold + left-aligned and its popup a
fixed 6 rows regardless of item count. All three were unmeasured styling
divergences — the authored data (new probe menuprobe3, live DAT) says:

- button label child 0x10000355: fontColor white, hJustify=Center
- row template 0x1000035A: fontColor white, hJustify=Center
- popup ListBox 0x10000358: edge-docked L=T=R=B=1, the authored condition
  arming retail UIElement_Menu::RecalculatePopupSize @0x0046caf0 —
  popup resizes to the ListBox's summed content height, uncapped
  (0x0046e5f4..0046e66c via ResizeScrollableArea's 0x32 broadcast)

UiMenu gains three opt-in properties (ButtonTextCentered,
ItemTextCentered, PopupSizeToContent) plus retail Open @0x0046cc42's
empty-list gate; chat + vendor keep the class defaults, so their shipped
behavior is untouched. ConfigOptionsPageController.ApplyMenuChrome wires
all four corrections for the 8 Config menus with the probe citation.

The same probe found vendor's authored popup ListBox is ALSO docked while
our vendor dropdown ships G5's fixed 6-row window — filed as #386 +
register row AD-88 (UNCLEAR: the G5 retail screenshot and the decomp
mechanism conflict) instead of silently reworking a user-gated surface.

The "resolution change resizes the window" observation from the same
report is #374's designed windowed-mode behavior (display-mode switching
is #376/#377) — no change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-13 08:51:57 +02:00
parent 028920420d
commit a9b6435f55
8 changed files with 422 additions and 9 deletions

View file

@ -293,6 +293,149 @@ public sealed class OptionsPanelLiveMountProbeTests
return null;
}
/// <summary>User gate report 2026-08-13 (Campaign OP happy-testing round):
/// every Options-panel dropdown draws its text yellow + left-aligned (user:
/// retail is white + centered) and its popup a fixed 6 rows tall (user:
/// retail conforms to the item count). Retail mechanism, decomp-verified:
/// <c>UIElement_Menu::RecalculatePopupSize @0x0046caf0</c> resizes the popup
/// to the ListBox's content extent (<c>m_iScrollableHeight</c> = SUM of laid
/// out row heights, uncapped — <c>0x0046e5f4..0046e66c</c>) + the authored
/// popup-minus-ListBox border, but ONLY when the authored ListBox is docked
/// on both edges (<c>m_leftEdge==1 &amp;&amp; m_rightEdge==1</c> /
/// <c>m_topEdge==1 &amp;&amp; m_bottomEdge==1</c>); the caption is an
/// authored <c>UIElement_Text</c> child named by menu attr 8
/// (<c>NewSelection @0x0046cd60</c>) whose color/justify come from the
/// LayoutDesc, and <c>Open @0x0046cc30</c> centers the POPUP over the
/// button when bool attr 3 is authored. This probe measures all of that
/// authored data for the Config option-menu chain (catalog 0x21000043,
/// base 0x10000353) with vendor's dropdown (0x1000034F chain — visibly a
/// FIXED 6-row + scrollbar popup in retail, user-gated during the vendor
/// campaign) as the contrast control.</summary>
[Fact]
public void ProbeMenuPopupSizingAndTextStyle()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1")
return;
var datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents",
"Asheron's Call");
using var dats = new DatCollection(datDir, DatAccessType.Read);
Console.WriteLine("[menuprobe3] === Config option-menu base 0x21000043/0x10000353 ===");
ElementInfo? menuBase = LayoutImporter.ImportInfos(dats, 0x21000043u, 0x10000353u);
if (menuBase is null)
{
Console.WriteLine("[menuprobe3] 0x10000353 FAILED to import");
}
else
{
DumpMenuAttrs3(menuBase, "base 0x10000353", new[] { 2u, 3u, 5u, 6u, 7u, 8u, 9u });
foreach (ElementInfo c in menuBase.Children)
DumpTextStyle(c, $"base child 0x{c.Id:X8}");
}
Console.WriteLine("[menuprobe3] === Config leaf 0x2100002B/0x10000224 (attrs 3+8) ===");
ElementInfo? configRow = LayoutImporter.ImportInfos(dats, 0x2100002Bu, 0x10000222u);
if (configRow is not null && FindInfo(configRow, 0x10000224u) is { } leaf)
{
DumpMenuAttrs3(leaf, "leaf 0x10000224", new[] { 3u, 8u });
foreach (ElementInfo c in leaf.Children)
DumpTextStyle(c, $"leaf child 0x{c.Id:X8}");
}
else
{
Console.WriteLine("[menuprobe3] leaf 0x10000224 MISSING");
}
Console.WriteLine("[menuprobe3] === Config popup root/ListBox/row template ===");
DumpDockAndSize(dats, 0x21000043u, 0x10000357u, "Config popup root");
DumpDockAndSize(dats, 0x21000043u, 0x10000358u, "Config popup ListBox");
ElementInfo? rowTemplate = LayoutImporter.ImportInfos(dats, 0x21000043u, 0x1000035Au);
if (rowTemplate is not null)
{
DumpTextStyle(rowTemplate, "Config row template 0x1000035A");
foreach (ElementInfo c in rowTemplate.Children)
DumpTextStyle(c, $"row-template child 0x{c.Id:X8}");
}
else
{
Console.WriteLine("[menuprobe3] row template 0x1000035A FAILED to import");
}
Console.WriteLine("[menuprobe3] === CONTROL: vendor chain (fixed 6-row + scrollbar in retail) ===");
DumpDockAndSize(dats, 0x21000043u, 0x1000034Fu, "Vendor popup root");
DumpDockAndSize(dats, 0x21000043u, 0x10000350u, "Vendor popup ListBox");
ElementInfo? vendorBase = LayoutImporter.ImportInfos(dats, 0x21000043u, 0x1000034Bu);
if (vendorBase is not null)
{
DumpMenuAttrs3(vendorBase, "vendor base 0x1000034B", new[] { 2u, 3u, 5u, 6u, 7u, 8u, 9u });
foreach (ElementInfo c in vendorBase.Children)
DumpTextStyle(c, $"vendor base child 0x{c.Id:X8}");
}
ElementInfo? vendorRowTemplate = LayoutImporter.ImportInfos(dats, 0x21000043u, 0x10000352u);
if (vendorRowTemplate is not null)
{
DumpTextStyle(vendorRowTemplate, "Vendor row template 0x10000352");
foreach (ElementInfo c in vendorRowTemplate.Children)
DumpTextStyle(c, $"vendor row-template child 0x{c.Id:X8}");
}
}
private static void DumpMenuAttrs3(ElementInfo el, string label, uint[] attrs)
{
Console.WriteLine(
$"[menuprobe3] {label}: type=0x{el.Type:X8} ({el.X},{el.Y} {el.Width}x{el.Height}) "
+ $"edges L={el.Left} T={el.Top} R={el.Right} B={el.Bottom} "
+ $"children=[{string.Join(",", el.Children.ConvertAll(c => $"0x{c.Id:X8}"))}]");
foreach (uint attr in attrs)
{
if (el.TryGetEffectiveProperty(attr, out UiPropertyValue value))
{
string rendered = value.Kind switch
{
UiPropertyKind.Bool => value.BoolValue.ToString(),
UiPropertyKind.DataId or UiPropertyKind.Enum => $"0x{value.UnsignedValue:X8}",
UiPropertyKind.Integer => value.IntegerValue.ToString(),
_ => value.Kind.ToString(),
};
Console.WriteLine($"[menuprobe3] attr[{attr}] kind={value.Kind} value={rendered}");
}
else
{
Console.WriteLine($"[menuprobe3] attr[{attr}] MISSING");
}
}
}
private static void DumpDockAndSize(DatCollection dats, uint layoutId, uint elementId, string label)
{
ElementInfo? el = LayoutImporter.ImportInfos(dats, layoutId, elementId);
if (el is null)
{
Console.WriteLine($"[menuprobe3] {label} 0x{elementId:X8}: FAILED to import");
return;
}
Console.WriteLine(
$"[menuprobe3] {label} 0x{elementId:X8}: type=0x{el.Type:X8} "
+ $"({el.X},{el.Y} {el.Width}x{el.Height}) "
+ $"edges L={el.Left} T={el.Top} R={el.Right} B={el.Bottom} "
+ $"children=[{string.Join(",", el.Children.ConvertAll(c => $"0x{c.Id:X8}"))}]");
}
private static void DumpTextStyle(ElementInfo el, string label)
{
string color = el.FontColor is { } fc
? $"({fc.X:F2},{fc.Y:F2},{fc.Z:F2},{fc.W:F2})"
: "null(default-white)";
Console.WriteLine(
$"[menuprobe3] {label}: type=0x{el.Type:X8} ({el.X},{el.Y} {el.Width}x{el.Height}) "
+ $"hJustify={el.HJustify} vJustify={el.VJustify} fontColor={color} fontDid=0x{el.FontDid:X8} "
+ $"edges L={el.Left} T={el.Top} R={el.Right} B={el.Bottom}");
}
/// <summary>#380 (gate 4): exercises the PRODUCTION
/// <see cref="ChatOptionsDatCaptions.TryRead"/> mechanism against the real
/// DAT and asserts the two resolved captions match the user's own