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:
parent
028920420d
commit
a9b6435f55
8 changed files with 422 additions and 9 deletions
|
|
@ -564,6 +564,49 @@ public sealed class ConfigOptionsPageControllerTests
|
|||
Assert.Equal(1, fakeBindings.AudioSaves[^1].SoundFeatures);
|
||||
}
|
||||
|
||||
/// <summary>User gate report 2026-08-13: every Config dropdown drew its
|
||||
/// text gold + left-aligned and its popup a fixed 6 rows. The corrected
|
||||
/// values are MEASURED authored facts (menuprobe3,
|
||||
/// <see cref="OptionsPanelLiveMountProbeTests.ProbeMenuPopupSizingAndTextStyle"/>):
|
||||
/// label child 0x10000355 + row template 0x1000035A are white +
|
||||
/// hJustify=Center, and popup ListBox 0x10000358 is edge-docked
|
||||
/// (L=T=R=B=1), arming retail's RecalculatePopupSize size-to-content
|
||||
/// path. Asserted on ALL 8 menus — one shared ApplyMenuChrome must not
|
||||
/// quietly skip any.</summary>
|
||||
[Fact]
|
||||
public void MenuRows_All8_UseTheAuthoredTextStyleAndSizeToContent()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
|
||||
OptionsPanelController controller = OptionsPanelController.Bind(
|
||||
layout,
|
||||
new OptionsPanelController.Callbacks(
|
||||
Toggle: () => { },
|
||||
RequestExitToCharacterSelection: () => { },
|
||||
ExitGame: () => { },
|
||||
UseMouseTurningSettings: () => { },
|
||||
DisplaySystemMessage: _ => { }))!;
|
||||
var fakeBindings = new FakeBindings();
|
||||
Assert.True(ConfigOptionsPageController.Bind(
|
||||
layout,
|
||||
controller.ConfigPage,
|
||||
MakeTemplateResolver(),
|
||||
(_, _) => null,
|
||||
fakeBindings.ToBindings(),
|
||||
resolveSprite: _ => (1u, 8, 8)));
|
||||
|
||||
var listBox = Assert.IsType<UiTemplateListBox>(
|
||||
layout.FindElement(ConfigOptionsPageController.ListBoxElementId));
|
||||
List<UiMenu> menus = CollectMenus(listBox);
|
||||
Assert.Equal(8, menus.Count);
|
||||
foreach (UiMenu menu in menus)
|
||||
{
|
||||
Assert.Equal(System.Numerics.Vector4.One, menu.TextColor);
|
||||
Assert.True(menu.ButtonTextCentered);
|
||||
Assert.True(menu.ItemTextCentered);
|
||||
Assert.True(menu.PopupSizeToContent);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MenuRow_Resolution_IsStringBacked_AndWritesThroughDisplayBindings()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 && m_rightEdge==1</c> /
|
||||
/// <c>m_topEdge==1 && 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
|
||||
|
|
|
|||
|
|
@ -489,4 +489,76 @@ public class UiMenuTests
|
|||
Assert.Equal(0f, menu.TextIndent);
|
||||
Assert.Equal(0f, menu.ButtonTextIndent);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// User gate report 2026-08-13: fixed-6-row popups for short option
|
||||
// lists. Retail (UIElement_Menu::RecalculatePopupSize @0x0046caf0)
|
||||
// sizes the popup to the ListBox's summed content height, uncapped,
|
||||
// when the authored ListBox is edge-docked — see
|
||||
// UiMenu.PopupSizeToContent's own doc for the full mechanism.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private static UiMenu MakeScrollableMenu(int itemCount, bool sizeToContent) => new UiMenu
|
||||
{
|
||||
Width = 120f, Height = 18f,
|
||||
Scrollable = true,
|
||||
OpenUpward = false,
|
||||
RowsPerColumn = 6,
|
||||
RowHeight = 18f,
|
||||
ColumnWidth = 100f,
|
||||
PopupSizeToContent = sizeToContent,
|
||||
Items = Enumerable.Range(0, itemCount)
|
||||
.Select(i => new UiMenu.MenuItem($"Item {i}", (object?)i)).ToArray(),
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void SizeToContent_ShrinksThePopupToTheItemCount()
|
||||
{
|
||||
// 3 items: interior 3*18, plus the 5px bevel top+bottom.
|
||||
Assert.Equal(3 * 18f + 10f, MakeScrollableMenu(3, sizeToContent: true).PopupOuterHeight);
|
||||
// Control: without size-to-content the fixed 6-row window stands.
|
||||
Assert.Equal(6 * 18f + 10f, MakeScrollableMenu(3, sizeToContent: false).PopupOuterHeight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SizeToContent_GrowsPastTheFixedWindow_AndTheLastRowIsPickable()
|
||||
{
|
||||
// Retail's content sum is UNCAPPED — 9 items = 9 rows, no scrolling.
|
||||
UiMenu menu = MakeScrollableMenu(9, sizeToContent: true);
|
||||
Assert.Equal(9 * 18f + 10f, menu.PopupOuterHeight);
|
||||
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open
|
||||
Assert.True(menu.IsOpen);
|
||||
|
||||
object? fired = null;
|
||||
menu.OnSelect = p => fired = p;
|
||||
// Row 8 (the 9th item) sits past the old 6-row window: it must be
|
||||
// directly pickable with NO scroll. Downward popup: interior starts
|
||||
// at Height + border.
|
||||
float ly = menu.Height + 5f + 8 * menu.RowHeight + menu.RowHeight / 2f;
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, (int)ly)));
|
||||
Assert.Equal(8, fired);
|
||||
Assert.False(menu.IsOpen); // picking closes, same as every other path
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyItems_ButtonClickDoesNotOpen()
|
||||
{
|
||||
// Retail UIElement_Menu::Open @0x0046cc42 gates on
|
||||
// m_listBox->m_listItems.m_num != 0 — an itemless menu never opens.
|
||||
UiMenu menu = MakeScrollableMenu(0, sizeToContent: true);
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5)));
|
||||
Assert.False(menu.IsOpen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TextStyleDefaults_PreserveChatAndVendorBehavior()
|
||||
{
|
||||
// The three 2026-08-13 additions are opt-in: chat's gold left-aligned
|
||||
// caption and vendor's fixed 6-row window are untouched by default.
|
||||
var menu = new UiMenu();
|
||||
Assert.False(menu.ButtonTextCentered);
|
||||
Assert.False(menu.ItemTextCentered);
|
||||
Assert.False(menu.PopupSizeToContent);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue