fix #378: Config-tab dropdown menus render bare with no popup chrome
Root cause: DatWidgetFactory builds Config-tab Type-0x10000038 menu leaves as bare UiMenu instances (matching the vendor/chat channel menu pattern), but unlike those two controllers, ConfigOptionsPageController never wired the menu's sprite/font/geometry properties after Bind — so every dropdown rendered as plain text with no button well, no arrow cap, and opened no popup on click (#374's fix only corrected click ROUTING, not the missing chrome). Fix: ConfigOptionsPageController.ApplyMenuChrome wires every Config-tab menu row with the SAME retail sprite ids VendorUiController/ ChatWindowController's channel menu already use for this shared popup catalog (LayoutDesc 0x21000043), verified against the live DAT via OptionsPanelLiveMountProbeTests' ProbeConfigMenuChrome/ ProbeConfigMenuPopupChrome probes. Regressed by ConfigOptionsPageControllerTests.MenuRow_SoundFeatures_OpensAndSelects ThroughRealHitPath_UsingAuthoredPopupGeometry, which drives the real click-to-open + item-pick path through the authored popup geometry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
28bef4e03b
commit
c121842664
6 changed files with 524 additions and 35 deletions
|
|
@ -73,20 +73,43 @@ scope the controller's application set to it.
|
||||||
|
|
||||||
## #378 — Config-tab dropdown menus render bare (no button well, no arrow) and no popup opens on click
|
## #378 — Config-tab dropdown menus render bare (no button well, no arrow) and no popup opens on click
|
||||||
|
|
||||||
**Status:** OPEN — filed 2026-08-11 at Campaign OP gate 4 (user
|
**Status:** ROOT-CAUSED + FIXED (this commit) — pending the user's
|
||||||
screenshots: retail's Resolution row shows a sunken value well + green
|
re-gate. Filed 2026-08-11 at Campaign OP gate 4 (user screenshots:
|
||||||
arrow cap; acdream's same rows — Resolution, Sound Features, all the
|
retail's Resolution row shows a sunken value well + green arrow cap;
|
||||||
Config menus — render as bare text like "Stereo"/"1280x720" with NO
|
acdream's same rows rendered as bare text with no button chrome).
|
||||||
button chrome, and clicking produces NO visible dropdown). This is the
|
|
||||||
user-facing residual behind #374: the pointer-routing fix landed, but the
|
**ROOT CAUSE — nothing wired the Config-tab `UiMenu` leaves at all.**
|
||||||
menu widget itself isn't presenting at the live mount. Candidate family
|
`ConfigOptionsPageController.BuildMenuRow`/`BuildStringMenuRow` built the
|
||||||
(to CONFIRM with the live-mount probe, not assume): the Config row
|
row's `UiMenu` widget from the template but never touched a single one of
|
||||||
template's Type-6 menu element's button-face/arrow/popup sprites not
|
its sprite/geometry properties — `SpriteResolve` stayed null and every
|
||||||
resolving at build (so the popup may even "open" while drawing nothing) —
|
sprite id stayed 0, so `OnDraw`/`OnDrawOverlay` early-returned on every
|
||||||
the same fixture-green/live-broken class as #372/#375. The probe must
|
frame (drawing literal nothing) even though click ROUTING (#374) was
|
||||||
dump the built UiMenu's sprite ids + SpriteResolve wiring for a Config
|
already correct. A live-DAT probe (raw `ElementDesc` walk against
|
||||||
row versus the working chat-channel menu. Blocks the OP6 gate's
|
`DatCollectionAdapter`, bypassing the widget layer) traced the menu
|
||||||
resolution step (#374's re-test) and every other Config menu row.
|
leaf's (`0x10000224`) full retail inheritance chain — base `0x10000353`
|
||||||
|
in LayoutDesc `0x21000043`, retail's shared popup/dropdown catalog — and
|
||||||
|
found it BYTE-IDENTICAL in every sprite id to `VendorUiController`'s own
|
||||||
|
already-fixed dropdown (base `0x1000034B`, same layout): arrow cap
|
||||||
|
`0x060012B1`/`B2`, face/row sprite `0x060012B3`/`B4`, and the full
|
||||||
|
6-sprite scrollbar chrome `0x06004C5F/60/63/66/69/6C`. Attribute 7 (the
|
||||||
|
popup catalog LayoutDesc) is `0x21000043` for BOTH menus — not an
|
||||||
|
approximation, a measured fact, so no register row was needed. **Fix:**
|
||||||
|
`ConfigOptionsPageController.ApplyMenuChrome` wires the SAME chrome
|
||||||
|
`VendorUiController` already established, threaded from
|
||||||
|
`RetailUiRuntime`'s existing `Assets.ResolveSprite`/`DefaultFont`/
|
||||||
|
`DebugFont` bindings through `ConfigOptionsPageController.Bind`'s three
|
||||||
|
new optional parameters. Regressed by
|
||||||
|
`tests/AcDream.App.Tests/UI/Layout/ConfigOptionsPageControllerTests.cs`
|
||||||
|
(`MenuRow_SoundFeatures_OpensAndSelectsThroughRealHitPath_UsingAuthoredPopupGeometry`
|
||||||
|
— asserts every sprite id is non-zero, drives the real click-to-open +
|
||||||
|
item-pick event path, and confirms the applied value reaches
|
||||||
|
`AudioSettings`) and extended live-mount probes in
|
||||||
|
`OptionsPanelLiveMountProbeTests.cs` (`ProbeConfigMenuChrome`/
|
||||||
|
`ProbeConfigMenuPopupChrome`).
|
||||||
|
|
||||||
|
**Re-gate (§OP6 step 8, and every other Config-tab dropdown): every
|
||||||
|
Config menu row should now show the sunken value-well face + green arrow
|
||||||
|
cap and open a real bordered, scrollable popup on click.**
|
||||||
|
|
||||||
## #377 — Startup CRASH (0xC0000005 in Glfw.GetVideoMode) when settings.json has `fullscreen: true`
|
## #377 — Startup CRASH (0xC0000005 in Glfw.GetVideoMode) when settings.json has `fullscreen: true`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -622,7 +622,14 @@ should show no caption.
|
||||||
open popup dismisses it without acting on what's below. While
|
open popup dismisses it without acting on what's below. While
|
||||||
FULLSCREEN a resolution pick cannot switch the display mode yet
|
FULLSCREEN a resolution pick cannot switch the display mode yet
|
||||||
(Silk API limit, #376) — it applies on the next return to windowed;
|
(Silk API limit, #376) — it applies on the next return to windowed;
|
||||||
do not report that as a step-8 failure.
|
do not report that as a step-8 failure. **Gate-4 re-test note (#378):**
|
||||||
|
the SAME row (and every other Config-tab dropdown — Sound Features, the
|
||||||
|
four texture-detail menus, the two Chat Font menus) previously rendered
|
||||||
|
as bare text with no button well/arrow and no visible popup at all
|
||||||
|
(#374's fix only corrected click ROUTING, not the missing chrome). Every
|
||||||
|
dropdown now shows the sunken value-well face + green arrow cap and
|
||||||
|
opens a real bordered, scrollable popup on click — report if ANY
|
||||||
|
Config-tab dropdown still renders bare text.
|
||||||
9. **Toggle "Full Screen".** The window should switch between windowed and
|
9. **Toggle "Full Screen".** The window should switch between windowed and
|
||||||
fullscreen IMMEDIATELY, live.
|
fullscreen IMMEDIATELY, live.
|
||||||
10. **Toggle "Sync To Refresh" (VSync) and drag the "Field of View"
|
10. **Toggle "Sync To Refresh" (VSync) and drag the "Field of View"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.UI.Abstractions.Panels.Settings;
|
using AcDream.UI.Abstractions.Panels.Settings;
|
||||||
|
|
||||||
|
|
@ -217,6 +218,104 @@ public static class ConfigOptionsPageController
|
||||||
/// <c>0x10000219</c> convention.</summary>
|
/// <c>0x10000219</c> convention.</summary>
|
||||||
private const uint ToggleCheckboxElementId = 0x10000219u;
|
private const uint ToggleCheckboxElementId = 0x10000219u;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// #378 fix (2026-08-11, gate 4): every Config-tab dropdown
|
||||||
|
/// (<see cref="MenuElementId"/>, retail class <c>UIOption_Menu</c>,
|
||||||
|
/// <c>Type 0x10000038</c>) rendered bare — no button well, no arrow, no
|
||||||
|
/// popup — because NOTHING wired its sprites, the same class of bug
|
||||||
|
/// <see cref="VendorUiController"/>'s own precedent already fixed for
|
||||||
|
/// the vendor category dropdown. A live-DAT probe (menuprobe2,
|
||||||
|
/// <c>OptionsPanelLiveMountProbeTests</c>) walked the retail inheritance
|
||||||
|
/// chain the built widget tree collapses away
|
||||||
|
/// (<c>0x10000224</c> → base <c>0x10000353</c> in LayoutDesc
|
||||||
|
/// <c>0x21000043</c> — retail's SHARED popup/dropdown catalog, the exact
|
||||||
|
/// same layout <see cref="VendorUiController"/>'s own <c>0x100000BF</c>
|
||||||
|
/// also derives from, base <c>0x1000034B</c>) and found the two
|
||||||
|
/// prototypes BYTE-IDENTICAL in every sprite id: arrow cap
|
||||||
|
/// <c>0x060012B1</c>/<c>B2</c>, face/row sprite <c>0x060012B3</c>/<c>B4</c>,
|
||||||
|
/// and the full 6-sprite scrollbar chrome
|
||||||
|
/// <c>0x06004C5F/60/63/66/69/6C</c> — Config's own popup root/ListBox
|
||||||
|
/// (<c>0x10000357</c>/<c>0x10000358</c>, attrs 6/2 on <c>0x10000353</c>)
|
||||||
|
/// carry the SAME <c>0x59</c>=true/<c>0x5D</c>=1-col/<c>0x5E</c>=6-row/
|
||||||
|
/// <c>0x61</c>=true properties as vendor's own <c>0x1000034F</c>/
|
||||||
|
/// <c>0x10000350</c>, and Config's row template (attr 9 →
|
||||||
|
/// <c>0x1000035A</c>) resolves the IDENTICAL Normal/Highlight media as
|
||||||
|
/// vendor's own <c>0x10000352</c>. Attribute 7 (the popup catalog
|
||||||
|
/// LayoutDesc) is <c>0x21000043</c> for BOTH — not an approximation
|
||||||
|
/// choice, a measured fact; no register row needed. No property "5"
|
||||||
|
/// (open-upward) is authored on <c>0x10000353</c> either, matching
|
||||||
|
/// vendor's own absent-defaults-false case — the popup opens DOWNWARD.
|
||||||
|
/// These constants are therefore transcribed from the SAME live values
|
||||||
|
/// <see cref="VendorUiController"/> already hardcodes, not duplicated by
|
||||||
|
/// coincidence — a genuine shared retail asset, verified independently
|
||||||
|
/// rather than assumed from the precedent.
|
||||||
|
/// </summary>
|
||||||
|
private static class MenuChromeSprites
|
||||||
|
{
|
||||||
|
public const uint Normal = 0x060012B3u;
|
||||||
|
public const uint Pressed = 0x060012B4u;
|
||||||
|
public const uint ItemNormal = 0x060012B3u;
|
||||||
|
public const uint ItemHighlight = 0x060012B4u;
|
||||||
|
public const uint ArrowCapClosed = 0x060012B1u;
|
||||||
|
public const uint ArrowCapOpen = 0x060012B2u;
|
||||||
|
|
||||||
|
public const int RowsPerColumn = 6;
|
||||||
|
public const float RowHeight = 18f;
|
||||||
|
public const float ColumnWidth = 100f;
|
||||||
|
|
||||||
|
public const float ScrollbarWidth = 16f;
|
||||||
|
public const float ScrollButtonExtent = 16f;
|
||||||
|
public const uint ScrollTrack = 0x06004C5Fu;
|
||||||
|
public const uint ScrollThumbTop = 0x06004C60u;
|
||||||
|
public const uint ScrollThumb = 0x06004C63u;
|
||||||
|
public const uint ScrollThumbBottom = 0x06004C66u;
|
||||||
|
public const uint ScrollUp = 0x06004C69u;
|
||||||
|
public const uint ScrollDown = 0x06004C6Cu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Applies <see cref="MenuChromeSprites"/> + geometry to a
|
||||||
|
/// freshly-built <see cref="UiMenu"/> leaf — the SAME wiring shape
|
||||||
|
/// <see cref="VendorUiController"/> performs inline for its own dropdown
|
||||||
|
/// (font/sprite resolvers, scrollable single-column popup, arrow-cap
|
||||||
|
/// overlay, zero text indents since neither the row template nor the
|
||||||
|
/// face carry a checkbox/LED glyph to clear — see the class's own
|
||||||
|
/// checked live-dat properties).</summary>
|
||||||
|
private static void ApplyMenuChrome(
|
||||||
|
UiMenu menu,
|
||||||
|
Func<uint, (uint tex, int w, int h)>? resolveSprite,
|
||||||
|
UiDatFont? datFont,
|
||||||
|
BitmapFont? debugFont)
|
||||||
|
{
|
||||||
|
menu.SpriteResolve = resolveSprite;
|
||||||
|
menu.DatFont = datFont;
|
||||||
|
menu.Font = debugFont;
|
||||||
|
menu.NormalSprite = MenuChromeSprites.Normal;
|
||||||
|
menu.PressedSprite = MenuChromeSprites.Pressed;
|
||||||
|
menu.ItemNormalSprite = MenuChromeSprites.ItemNormal;
|
||||||
|
menu.ItemHighlightSprite = MenuChromeSprites.ItemHighlight;
|
||||||
|
menu.RowsPerColumn = MenuChromeSprites.RowsPerColumn;
|
||||||
|
menu.RowHeight = MenuChromeSprites.RowHeight;
|
||||||
|
menu.ColumnWidth = MenuChromeSprites.ColumnWidth;
|
||||||
|
menu.Scrollable = true;
|
||||||
|
menu.ScrollbarWidth = MenuChromeSprites.ScrollbarWidth;
|
||||||
|
menu.ScrollButtonExtent = MenuChromeSprites.ScrollButtonExtent;
|
||||||
|
menu.ScrollTrackSprite = MenuChromeSprites.ScrollTrack;
|
||||||
|
menu.ScrollThumbTopSprite = MenuChromeSprites.ScrollThumbTop;
|
||||||
|
menu.ScrollThumbSprite = MenuChromeSprites.ScrollThumb;
|
||||||
|
menu.ScrollThumbBottomSprite = MenuChromeSprites.ScrollThumbBottom;
|
||||||
|
menu.ScrollUpSprite = MenuChromeSprites.ScrollUp;
|
||||||
|
menu.ScrollDownSprite = MenuChromeSprites.ScrollDown;
|
||||||
|
menu.ArrowCapClosedSprite = MenuChromeSprites.ArrowCapClosed;
|
||||||
|
menu.ArrowCapOpenSprite = MenuChromeSprites.ArrowCapOpen;
|
||||||
|
// No property "5" authored on the retail prototype — absent-defaults-
|
||||||
|
// false, opens DOWNWARD (see MenuChromeSprites' own doc).
|
||||||
|
menu.OpenUpward = false;
|
||||||
|
// Neither the row template nor the face child carry a checkbox/LED
|
||||||
|
// glyph to clear space for (see MenuChromeSprites' own doc).
|
||||||
|
menu.TextIndent = 0f;
|
||||||
|
menu.ButtonTextIndent = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>The live read/write seam every row on this page writes/reads
|
/// <summary>The live read/write seam every row on this page writes/reads
|
||||||
/// through — four settings groups, each read once per row-build and
|
/// through — four settings groups, each read once per row-build and
|
||||||
/// mutated read-modify-write per change (the SAME per-change persistence
|
/// mutated read-modify-write per change (the SAME per-change persistence
|
||||||
|
|
@ -237,12 +336,26 @@ public static class ConfigOptionsPageController
|
||||||
/// ListBox, links its scrollbar, seeds every row's current/default
|
/// ListBox, links its scrollbar, seeds every row's current/default
|
||||||
/// state, and registers each row into <paramref name="page"/>.
|
/// state, and registers each row into <paramref name="page"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="resolveSprite">#378: RenderSurface id → (GL tex handle,
|
||||||
|
/// pixel width, pixel height), threaded onto every dropdown's
|
||||||
|
/// <see cref="UiMenu.SpriteResolve"/> — see <see cref="MenuChromeSprites"/>'s
|
||||||
|
/// own doc. Null (fixture/conformance callers that don't exercise
|
||||||
|
/// dropdown chrome) leaves every menu's sprite ids populated but drawing
|
||||||
|
/// nothing, matching <see cref="UiMenu"/>'s own null-safety.</param>
|
||||||
|
/// <param name="datFont">#378: the retail dat-driven font every dropdown
|
||||||
|
/// draws its button/row labels with.</param>
|
||||||
|
/// <param name="debugFont">#378: fallback bitmap font when
|
||||||
|
/// <paramref name="datFont"/> is unavailable — same convention
|
||||||
|
/// <see cref="VendorUiController"/> uses.</param>
|
||||||
public static bool Bind(
|
public static bool Bind(
|
||||||
ImportedLayout layout,
|
ImportedLayout layout,
|
||||||
OptionPage page,
|
OptionPage page,
|
||||||
Func<uint, uint, UiElement?> templateResolver,
|
Func<uint, uint, UiElement?> templateResolver,
|
||||||
Func<uint, uint, string?> resolveString,
|
Func<uint, uint, string?> resolveString,
|
||||||
Bindings bindings)
|
Bindings bindings,
|
||||||
|
Func<uint, (uint tex, int w, int h)>? resolveSprite = null,
|
||||||
|
UiDatFont? datFont = null,
|
||||||
|
BitmapFont? debugFont = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(layout);
|
ArgumentNullException.ThrowIfNull(layout);
|
||||||
ArgumentNullException.ThrowIfNull(page);
|
ArgumentNullException.ThrowIfNull(page);
|
||||||
|
|
@ -293,17 +406,17 @@ public static class ConfigOptionsPageController
|
||||||
CameraTurningSettings cameraTurning = bindings.LoadCameraTurning();
|
CameraTurningSettings cameraTurning = bindings.LoadCameraTurning();
|
||||||
ChatSettings chat = bindings.LoadChat();
|
ChatSettings chat = bindings.LoadChat();
|
||||||
|
|
||||||
BindSoundSection(listBox, page, resolveString, bindings, ref audio);
|
BindSoundSection(listBox, page, resolveString, bindings, ref audio, resolveSprite, datFont, debugFont);
|
||||||
BuildSeparatorRow(listBox);
|
BuildSeparatorRow(listBox);
|
||||||
BindCameraSection(listBox, page, resolveString, bindings, ref cameraTurning);
|
BindCameraSection(listBox, page, resolveString, bindings, ref cameraTurning);
|
||||||
BuildSeparatorRow(listBox);
|
BuildSeparatorRow(listBox);
|
||||||
BindGraphicsSection(listBox, page, resolveString, bindings, ref display);
|
BindGraphicsSection(listBox, page, resolveString, bindings, ref display, resolveSprite, datFont, debugFont);
|
||||||
BuildSeparatorRow(listBox);
|
BuildSeparatorRow(listBox);
|
||||||
BindRenderingQualitySection(listBox, page, resolveString, bindings, ref display);
|
BindRenderingQualitySection(listBox, page, resolveString, bindings, ref display, resolveSprite, datFont, debugFont);
|
||||||
BuildSeparatorRow(listBox);
|
BuildSeparatorRow(listBox);
|
||||||
BindInputSection(listBox, page, resolveString, bindings, ref cameraTurning);
|
BindInputSection(listBox, page, resolveString, bindings, ref cameraTurning);
|
||||||
BuildSeparatorRow(listBox);
|
BuildSeparatorRow(listBox);
|
||||||
BindUiSection(listBox, page, resolveString, bindings, ref chat);
|
BindUiSection(listBox, page, resolveString, bindings, ref chat, resolveSprite, datFont, debugFont);
|
||||||
// OP6 rework (2026-08-11, review S1): retail's own InitOptions ends
|
// OP6 rework (2026-08-11, review S1): retail's own InitOptions ends
|
||||||
// with a SIXTH AddSeperator tailcall (0x0049e80d) — a trailing
|
// with a SIXTH AddSeperator tailcall (0x0049e80d) — a trailing
|
||||||
// separator after the LAST section, not just the five INTERIOR ones
|
// separator after the LAST section, not just the five INTERIOR ones
|
||||||
|
|
@ -322,7 +435,10 @@ public static class ConfigOptionsPageController
|
||||||
OptionPage page,
|
OptionPage page,
|
||||||
Func<uint, uint, string?> resolveString,
|
Func<uint, uint, string?> resolveString,
|
||||||
Bindings bindings,
|
Bindings bindings,
|
||||||
ref AudioSettings audio)
|
ref AudioSettings audio,
|
||||||
|
Func<uint, (uint tex, int w, int h)>? resolveSprite,
|
||||||
|
UiDatFont? datFont,
|
||||||
|
BitmapFont? debugFont)
|
||||||
{
|
{
|
||||||
BuildHeaderRow(listBox, "ID_Sound_SoundSection", resolveString);
|
BuildHeaderRow(listBox, "ID_Sound_SoundSection", resolveString);
|
||||||
|
|
||||||
|
|
@ -337,7 +453,8 @@ public static class ConfigOptionsPageController
|
||||||
bindings.SaveAudio(updated);
|
bindings.SaveAudio(updated);
|
||||||
},
|
},
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
storeOnly: true); // AP-199
|
storeOnly: true, // AP-199
|
||||||
|
resolveSprite, datFont, debugFont);
|
||||||
|
|
||||||
// OP6 rework (2026-08-11, review M2): read/apply the ENABLED-sense
|
// OP6 rework (2026-08-11, review M2): read/apply the ENABLED-sense
|
||||||
// fields directly — toggleDefault stays `true` because retail's own
|
// fields directly — toggleDefault stays `true` because retail's own
|
||||||
|
|
@ -456,7 +573,10 @@ public static class ConfigOptionsPageController
|
||||||
OptionPage page,
|
OptionPage page,
|
||||||
Func<uint, uint, string?> resolveString,
|
Func<uint, uint, string?> resolveString,
|
||||||
Bindings bindings,
|
Bindings bindings,
|
||||||
ref DisplaySettings display)
|
ref DisplaySettings display,
|
||||||
|
Func<uint, (uint tex, int w, int h)>? resolveSprite,
|
||||||
|
UiDatFont? datFont,
|
||||||
|
BitmapFont? debugFont)
|
||||||
{
|
{
|
||||||
BuildHeaderRow(listBox, "ID_Graphics_GraphicsSection", resolveString);
|
BuildHeaderRow(listBox, "ID_Graphics_GraphicsSection", resolveString);
|
||||||
|
|
||||||
|
|
@ -484,7 +604,8 @@ public static class ConfigOptionsPageController
|
||||||
read: () => bindings.LoadDisplay().Resolution,
|
read: () => bindings.LoadDisplay().Resolution,
|
||||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { Resolution = value }),
|
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { Resolution = value }),
|
||||||
defaultValue: "800x600",
|
defaultValue: "800x600",
|
||||||
storeOnly: false); // LIVE
|
storeOnly: false, // LIVE
|
||||||
|
resolveSprite, datFont, debugFont);
|
||||||
|
|
||||||
BuildToggleRow(
|
BuildToggleRow(
|
||||||
listBox, "ID_Rendering_FullScreen", defaultValue: true, page, resolveString,
|
listBox, "ID_Rendering_FullScreen", defaultValue: true, page, resolveString,
|
||||||
|
|
@ -563,7 +684,10 @@ public static class ConfigOptionsPageController
|
||||||
OptionPage page,
|
OptionPage page,
|
||||||
Func<uint, uint, string?> resolveString,
|
Func<uint, uint, string?> resolveString,
|
||||||
Bindings bindings,
|
Bindings bindings,
|
||||||
ref DisplaySettings display)
|
ref DisplaySettings display,
|
||||||
|
Func<uint, (uint tex, int w, int h)>? resolveSprite,
|
||||||
|
UiDatFont? datFont,
|
||||||
|
BitmapFont? debugFont)
|
||||||
{
|
{
|
||||||
BuildHeaderRow(listBox, "ID_Graphics_TextureSection", resolveString);
|
BuildHeaderRow(listBox, "ID_Graphics_TextureSection", resolveString);
|
||||||
|
|
||||||
|
|
@ -575,21 +699,24 @@ public static class ConfigOptionsPageController
|
||||||
read: () => bindings.LoadDisplay().LandscapeTextureDetail,
|
read: () => bindings.LoadDisplay().LandscapeTextureDetail,
|
||||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { LandscapeTextureDetail = value }),
|
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { LandscapeTextureDetail = value }),
|
||||||
defaultValue: 2,
|
defaultValue: 2,
|
||||||
storeOnly: true); // AP-198
|
storeOnly: true, // AP-198
|
||||||
|
resolveSprite, datFont, debugFont);
|
||||||
|
|
||||||
BuildMenuRow(
|
BuildMenuRow(
|
||||||
listBox, "ID_Graphics_EnvironmentTextureDetail", TextureDetailChoices, page, resolveString,
|
listBox, "ID_Graphics_EnvironmentTextureDetail", TextureDetailChoices, page, resolveString,
|
||||||
read: () => bindings.LoadDisplay().EnvironmentTextureDetail,
|
read: () => bindings.LoadDisplay().EnvironmentTextureDetail,
|
||||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { EnvironmentTextureDetail = value }),
|
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { EnvironmentTextureDetail = value }),
|
||||||
defaultValue: 1,
|
defaultValue: 1,
|
||||||
storeOnly: true); // AP-198
|
storeOnly: true, // AP-198
|
||||||
|
resolveSprite, datFont, debugFont);
|
||||||
|
|
||||||
BuildMenuRow(
|
BuildMenuRow(
|
||||||
listBox, "ID_Graphics_TextureFiltering", TextureFilteringChoices, page, resolveString,
|
listBox, "ID_Graphics_TextureFiltering", TextureFilteringChoices, page, resolveString,
|
||||||
read: () => bindings.LoadDisplay().TextureFiltering,
|
read: () => bindings.LoadDisplay().TextureFiltering,
|
||||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { TextureFiltering = value }),
|
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { TextureFiltering = value }),
|
||||||
defaultValue: 1,
|
defaultValue: 1,
|
||||||
storeOnly: true); // AP-198
|
storeOnly: true, // AP-198
|
||||||
|
resolveSprite, datFont, debugFont);
|
||||||
|
|
||||||
// UNRESOLVED (see class doc / register row): retail's own
|
// UNRESOLVED (see class doc / register row): retail's own
|
||||||
// SetDefaultValue(8) does not index this 6-entry choice array.
|
// SetDefaultValue(8) does not index this 6-entry choice array.
|
||||||
|
|
@ -600,7 +727,8 @@ public static class ConfigOptionsPageController
|
||||||
read: () => bindings.LoadDisplay().LandscapeDrawDistance,
|
read: () => bindings.LoadDisplay().LandscapeDrawDistance,
|
||||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { LandscapeDrawDistance = value }),
|
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { LandscapeDrawDistance = value }),
|
||||||
defaultValue: 8,
|
defaultValue: 8,
|
||||||
storeOnly: true); // AP-198
|
storeOnly: true, // AP-198
|
||||||
|
resolveSprite, datFont, debugFont);
|
||||||
|
|
||||||
BuildToggleRow(
|
BuildToggleRow(
|
||||||
listBox, "ID_Graphics_BuildingDetailTextures", defaultValue: true, page, resolveString,
|
listBox, "ID_Graphics_BuildingDetailTextures", defaultValue: true, page, resolveString,
|
||||||
|
|
@ -694,7 +822,10 @@ public static class ConfigOptionsPageController
|
||||||
OptionPage page,
|
OptionPage page,
|
||||||
Func<uint, uint, string?> resolveString,
|
Func<uint, uint, string?> resolveString,
|
||||||
Bindings bindings,
|
Bindings bindings,
|
||||||
ref ChatSettings chat)
|
ref ChatSettings chat,
|
||||||
|
Func<uint, (uint tex, int w, int h)>? resolveSprite,
|
||||||
|
UiDatFont? datFont,
|
||||||
|
BitmapFont? debugFont)
|
||||||
{
|
{
|
||||||
BuildHeaderRow(listBox, "ID_UI_UISection", resolveString);
|
BuildHeaderRow(listBox, "ID_UI_UISection", resolveString);
|
||||||
|
|
||||||
|
|
@ -707,14 +838,16 @@ public static class ConfigOptionsPageController
|
||||||
read: () => bindings.LoadChat().ChatFontFace,
|
read: () => bindings.LoadChat().ChatFontFace,
|
||||||
apply: value => bindings.SaveChat(bindings.LoadChat() with { ChatFontFace = value }),
|
apply: value => bindings.SaveChat(bindings.LoadChat() with { ChatFontFace = value }),
|
||||||
defaultValue: 2,
|
defaultValue: 2,
|
||||||
storeOnly: true); // AP-200
|
storeOnly: true, // AP-200
|
||||||
|
resolveSprite, datFont, debugFont);
|
||||||
|
|
||||||
BuildMenuRow(
|
BuildMenuRow(
|
||||||
listBox, "ID_UI_ChatFontSize", ChatFontSizeChoices, page, resolveString,
|
listBox, "ID_UI_ChatFontSize", ChatFontSizeChoices, page, resolveString,
|
||||||
read: () => bindings.LoadChat().ChatFontSizeIndex,
|
read: () => bindings.LoadChat().ChatFontSizeIndex,
|
||||||
apply: value => bindings.SaveChat(bindings.LoadChat() with { ChatFontSizeIndex = value }),
|
apply: value => bindings.SaveChat(bindings.LoadChat() with { ChatFontSizeIndex = value }),
|
||||||
defaultValue: 1,
|
defaultValue: 1,
|
||||||
storeOnly: true); // AP-200
|
storeOnly: true, // AP-200
|
||||||
|
resolveSprite, datFont, debugFont);
|
||||||
|
|
||||||
chat = bindings.LoadChat();
|
chat = bindings.LoadChat();
|
||||||
}
|
}
|
||||||
|
|
@ -990,7 +1123,10 @@ public static class ConfigOptionsPageController
|
||||||
Func<int> read,
|
Func<int> read,
|
||||||
Action<int> apply,
|
Action<int> apply,
|
||||||
int defaultValue,
|
int defaultValue,
|
||||||
bool storeOnly)
|
bool storeOnly,
|
||||||
|
Func<uint, (uint tex, int w, int h)>? resolveSprite,
|
||||||
|
UiDatFont? datFont,
|
||||||
|
BitmapFont? debugFont)
|
||||||
{
|
{
|
||||||
UiElement? row = listBox.AddItemFromTemplateList(MenuTemplateIndex);
|
UiElement? row = listBox.AddItemFromTemplateList(MenuTemplateIndex);
|
||||||
if (row is null)
|
if (row is null)
|
||||||
|
|
@ -1012,6 +1148,10 @@ public static class ConfigOptionsPageController
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #378: wire the dropdown chrome — the button-well face, arrow cap,
|
||||||
|
// and scrollable popup — see MenuChromeSprites' own doc.
|
||||||
|
ApplyMenuChrome(menu, resolveSprite, datFont, debugFont);
|
||||||
|
|
||||||
// OP6 rework (review S3): the menu button IS the interactive/
|
// OP6 rework (review S3): the menu button IS the interactive/
|
||||||
// hoverable widget for this row.
|
// hoverable widget for this row.
|
||||||
string? tooltip = ResolveTooltip(labelKey, resolveString);
|
string? tooltip = ResolveTooltip(labelKey, resolveString);
|
||||||
|
|
@ -1073,7 +1213,10 @@ public static class ConfigOptionsPageController
|
||||||
Func<string> read,
|
Func<string> read,
|
||||||
Action<string> apply,
|
Action<string> apply,
|
||||||
string defaultValue,
|
string defaultValue,
|
||||||
bool storeOnly)
|
bool storeOnly,
|
||||||
|
Func<uint, (uint tex, int w, int h)>? resolveSprite,
|
||||||
|
UiDatFont? datFont,
|
||||||
|
BitmapFont? debugFont)
|
||||||
{
|
{
|
||||||
UiElement? row = listBox.AddItemFromTemplateList(MenuTemplateIndex);
|
UiElement? row = listBox.AddItemFromTemplateList(MenuTemplateIndex);
|
||||||
if (row is null)
|
if (row is null)
|
||||||
|
|
@ -1095,6 +1238,10 @@ public static class ConfigOptionsPageController
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #378: wire the dropdown chrome — the button-well face, arrow cap,
|
||||||
|
// and scrollable popup — see MenuChromeSprites' own doc.
|
||||||
|
ApplyMenuChrome(menu, resolveSprite, datFont, debugFont);
|
||||||
|
|
||||||
// OP6 rework (review S3): the menu button IS the interactive/
|
// OP6 rework (review S3): the menu button IS the interactive/
|
||||||
// hoverable widget for this row.
|
// hoverable widget for this row.
|
||||||
string? tooltip = ResolveTooltip(labelKey, resolveString);
|
string? tooltip = ResolveTooltip(labelKey, resolveString);
|
||||||
|
|
|
||||||
|
|
@ -2202,7 +2202,15 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
// silently clobber CH6's filter/opacity edits with a
|
// silently clobber CH6's filter/opacity edits with a
|
||||||
// stale snapshot the next time either surface saves.
|
// stale snapshot the next time either surface saves.
|
||||||
LoadChat: () => _bindings.Chat.Store?.LoadChat() ?? ChatSettings.Default,
|
LoadChat: () => _bindings.Chat.Store?.LoadChat() ?? ChatSettings.Default,
|
||||||
SaveChat: chat => _bindings.Chat.Store?.SaveChat(chat)));
|
SaveChat: chat => _bindings.Chat.Store?.SaveChat(chat)),
|
||||||
|
// #378: the eight Config-tab dropdown menus need the SAME
|
||||||
|
// sprite/font resolvers every other retail-menu consumer
|
||||||
|
// (ChatWindowController's channel menu, VendorUiController's
|
||||||
|
// category dropdown) already gets — see
|
||||||
|
// ConfigOptionsPageController.MenuChromeSprites' own doc.
|
||||||
|
resolveSprite: _bindings.Assets.ResolveSprite,
|
||||||
|
datFont: _bindings.Assets.DefaultFont,
|
||||||
|
debugFont: _bindings.Assets.DebugFont);
|
||||||
if (!configBound)
|
if (!configBound)
|
||||||
Console.WriteLine("[UI] options panel: Config tab rows did not bind.");
|
Console.WriteLine("[UI] options panel: Config tab rows did not bind.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,23 @@ public sealed class ConfigOptionsPageControllerTests
|
||||||
return (controller, fakeBindings, bound);
|
return (controller, fakeBindings, bound);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Every built <see cref="UiMenu"/> under a subtree, in
|
||||||
|
/// document (build) order — the same recursive-walk shape
|
||||||
|
/// <c>ChatOptionsPageControllerTests.CollectScalarSliders</c> uses for
|
||||||
|
/// its own widget-type collection.</summary>
|
||||||
|
private static List<UiMenu> CollectMenus(UiElement root)
|
||||||
|
{
|
||||||
|
var found = new List<UiMenu>();
|
||||||
|
Walk(root, found);
|
||||||
|
return found;
|
||||||
|
|
||||||
|
static void Walk(UiElement node, List<UiMenu> acc)
|
||||||
|
{
|
||||||
|
if (node is UiMenu menu) acc.Add(menu);
|
||||||
|
foreach (UiElement child in node.Children) Walk(child, acc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Bind_Succeeds_AndRegistersExactly30Rows()
|
public void Bind_Succeeds_AndRegistersExactly30Rows()
|
||||||
{
|
{
|
||||||
|
|
@ -466,6 +483,87 @@ public sealed class ConfigOptionsPageControllerTests
|
||||||
Assert.Equal(1, bindings.AudioSaves[^1].SoundFeatures);
|
Assert.Equal(1, bindings.AudioSaves[^1].SoundFeatures);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// #378 regression (2026-08-11, gate 4): before this fix, NOTHING wired
|
||||||
|
/// any Config-tab dropdown's chrome — every sprite id was 0, no arrow
|
||||||
|
/// cap, no popup. Drives the REAL <see cref="UiMenu"/> hit-test/pick
|
||||||
|
/// path <see cref="VendorUiControllerTests.CategoryMenu_OpensAndSelectsThroughRealHitPath_UsingAuthoredPopupGeometry"/>
|
||||||
|
/// already established for vendor's own dropdown — the SAME
|
||||||
|
/// authored popup catalog (LayoutDesc <c>0x21000043</c>) both dropdowns
|
||||||
|
/// derive from, byte-verified identical (see
|
||||||
|
/// <see cref="ConfigOptionsPageController.MenuChromeSprites"/>'s own
|
||||||
|
/// doc), so the SAME click-geometry formula applies.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void MenuRow_SoundFeatures_OpensAndSelectsThroughRealHitPath_UsingAuthoredPopupGeometry()
|
||||||
|
{
|
||||||
|
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
|
||||||
|
OptionsPanelController controller = OptionsPanelController.Bind(
|
||||||
|
layout,
|
||||||
|
new OptionsPanelController.Callbacks(
|
||||||
|
Toggle: () => { },
|
||||||
|
RequestExitToCharacterSelection: () => { },
|
||||||
|
ExitGame: () => { },
|
||||||
|
UseMouseTurningSettings: () => { },
|
||||||
|
DisplaySystemMessage: _ => { }))!;
|
||||||
|
var fakeBindings = new FakeBindings();
|
||||||
|
bool bound = ConfigOptionsPageController.Bind(
|
||||||
|
layout,
|
||||||
|
controller.ConfigPage,
|
||||||
|
MakeTemplateResolver(),
|
||||||
|
(_, _) => null,
|
||||||
|
fakeBindings.ToBindings(),
|
||||||
|
resolveSprite: _ => (1u, 8, 8));
|
||||||
|
Assert.True(bound);
|
||||||
|
|
||||||
|
var listBox = Assert.IsType<UiTemplateListBox>(
|
||||||
|
layout.FindElement(ConfigOptionsPageController.ListBoxElementId));
|
||||||
|
List<UiMenu> menus = CollectMenus(listBox);
|
||||||
|
Assert.Equal(8, menus.Count); // the 8 Config-tab dropdown rows
|
||||||
|
UiMenu soundFeatures = menus[0]; // build order matches Rows order — Sound Features first
|
||||||
|
|
||||||
|
// Wiring: without these UiMenu.OnDrawOverlay early-returns (nothing
|
||||||
|
// renders) and the button face never draws either — the bare-text
|
||||||
|
// symptom #378 reported.
|
||||||
|
Assert.NotNull(soundFeatures.SpriteResolve);
|
||||||
|
Assert.NotEqual(0u, soundFeatures.NormalSprite);
|
||||||
|
Assert.NotEqual(0u, soundFeatures.PressedSprite);
|
||||||
|
Assert.NotEqual(0u, soundFeatures.ItemNormalSprite);
|
||||||
|
Assert.NotEqual(0u, soundFeatures.ItemHighlightSprite);
|
||||||
|
Assert.NotEqual(0u, soundFeatures.ArrowCapClosedSprite);
|
||||||
|
Assert.NotEqual(0u, soundFeatures.ArrowCapOpenSprite);
|
||||||
|
|
||||||
|
// Geometry from the live-DAT-verified popup catalog (0x21000043) —
|
||||||
|
// see MenuChromeSprites' own doc.
|
||||||
|
Assert.True(soundFeatures.Scrollable);
|
||||||
|
Assert.Equal(6, soundFeatures.RowsPerColumn);
|
||||||
|
Assert.Equal(18f, soundFeatures.RowHeight);
|
||||||
|
Assert.Equal(100f, soundFeatures.ColumnWidth);
|
||||||
|
Assert.False(soundFeatures.OpenUpward); // no authored attribute 5 — absent defaults false
|
||||||
|
|
||||||
|
Assert.False(soundFeatures.IsOpen);
|
||||||
|
|
||||||
|
// Open via the real widget event path (button click).
|
||||||
|
Assert.True(soundFeatures.OnEvent(new UiEvent(0, soundFeatures, UiEventType.MouseDown, 0, 10, 5)));
|
||||||
|
Assert.True(soundFeatures.IsOpen);
|
||||||
|
|
||||||
|
// "Mono" is the SECOND choice (Stereo, then Mono) -> row index 1.
|
||||||
|
// Popup opens DOWNWARD (OpenUpward=false), so its top sits at the
|
||||||
|
// button's own bottom edge (ly = Height) — same formula
|
||||||
|
// VendorUiControllerTests already established for the identical
|
||||||
|
// popup catalog.
|
||||||
|
const int border = 5; // RetailChromeSprites.Border (UiMenu's private bevel thickness)
|
||||||
|
const int targetRow = 1;
|
||||||
|
float iy = targetRow * soundFeatures.RowHeight + soundFeatures.RowHeight / 2f;
|
||||||
|
float ly = soundFeatures.Height + iy + border;
|
||||||
|
|
||||||
|
Assert.True(soundFeatures.OnEvent(new UiEvent(0, soundFeatures, UiEventType.MouseDown, 0, 10, (int)ly)));
|
||||||
|
|
||||||
|
Assert.False(soundFeatures.IsOpen); // picking a row closes the popup
|
||||||
|
Assert.Equal(1, fakeBindings.Audio.SoundFeatures);
|
||||||
|
Assert.Equal(1, fakeBindings.AudioSaves[^1].SoundFeatures);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MenuRow_Resolution_IsStringBacked_AndWritesThroughDisplayBindings()
|
public void MenuRow_Resolution_IsStringBacked_AndWritesThroughDisplayBindings()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,212 @@ public sealed class OptionsPanelLiveMountProbeTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>#378 (gate 4): the Config tab's dropdown menus render bare —
|
||||||
|
/// no button well, no arrow, no popup on click — while the chat channel
|
||||||
|
/// menu works. Dump the BUILT UiMenu's sprite/chrome state for the Config
|
||||||
|
/// menu row template against the real DATs, so the missing piece is
|
||||||
|
/// measured rather than guessed.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void ProbeConfigMenuChrome()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
var strings = new DatStringResolver(dats);
|
||||||
|
|
||||||
|
ElementInfo root = LayoutImporter.ImportInfos(dats, 0x2100006Eu, 0x1000018Du);
|
||||||
|
ImportedLayout layout = LayoutImporter.Build(
|
||||||
|
root, _ => (1u, 8, 8), null, null, strings.Resolve);
|
||||||
|
|
||||||
|
UiElement? configSlot = layout.FindElement(0x10000213u);
|
||||||
|
Assert.NotNull(configSlot);
|
||||||
|
UiElement? lbEl = UiElement.FindDescendant(configSlot!, 0x10000200u);
|
||||||
|
UiTemplateListBox lb = Assert.IsType<UiTemplateListBox>(lbEl);
|
||||||
|
|
||||||
|
Console.WriteLine($"[menuprobe] Config ListBox templates={lb.Templates.Count}");
|
||||||
|
for (int i = 0; i < lb.Templates.Count; i++)
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[menuprobe] template[{i}] 0x{lb.Templates[i].TemplateLayoutId:X8}/0x{lb.Templates[i].TemplateElementId:X8}");
|
||||||
|
|
||||||
|
// Build template[4] (MenuTemplateIndex) exactly like production:
|
||||||
|
// through the SAME resolver shape RetailUiRuntime wires.
|
||||||
|
(uint tLayout, uint tElement) =
|
||||||
|
(lb.Templates[4].TemplateLayoutId, lb.Templates[4].TemplateElementId);
|
||||||
|
ElementInfo? tInfo = LayoutImporter.ImportInfos(dats, tLayout, tElement);
|
||||||
|
Assert.NotNull(tInfo);
|
||||||
|
DumpInfoTree(tInfo!, 0);
|
||||||
|
UiElement rowBuilt = LayoutImporter.Build(
|
||||||
|
tInfo!, _ => (1u, 8, 8), null, null, strings.Resolve).Root;
|
||||||
|
|
||||||
|
UiElement? menuEl = UiElement.FindDescendant(rowBuilt, 0x10000224u);
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[menuprobe] menu leaf 0x10000224 -> {(menuEl is null ? "MISSING" : menuEl.GetType().Name)} "
|
||||||
|
+ (menuEl is null ? "" : $"({menuEl.Left},{menuEl.Top} {menuEl.Width}x{menuEl.Height})"));
|
||||||
|
if (menuEl is UiMenu m)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[menuprobe] sprites: normal=0x{m.NormalSprite:X8} pressed=0x{m.PressedSprite:X8} "
|
||||||
|
+ $"popupBg=0x{m.PopupBgSprite:X8} itemNormal=0x{m.ItemNormalSprite:X8} "
|
||||||
|
+ $"itemHighlight=0x{m.ItemHighlightSprite:X8} arrowClosed=0x{m.ArrowCapClosedSprite:X8} "
|
||||||
|
+ $"arrowOpen=0x{m.ArrowCapOpenSprite:X8} spriteResolve={(m.SpriteResolve is null ? "NULL" : "set")} "
|
||||||
|
+ $"openUpward={m.OpenUpward} rows={m.RowsPerColumn} rowH={m.RowHeight} colW={m.ColumnWidth}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DumpInfoTree(ElementInfo info, int depth)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[menuprobe] info {new string(' ', depth * 2)}0x{info.Id:X8} type={info.Type} "
|
||||||
|
+ $"({info.X},{info.Y} {info.Width}x{info.Height}) children={info.Children.Count}");
|
||||||
|
if (depth >= 3) return;
|
||||||
|
foreach (ElementInfo c in info.Children)
|
||||||
|
DumpInfoTree(c, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>#378 (gate 4): the missing piece behind
|
||||||
|
/// <see cref="ProbeConfigMenuChrome"/>'s "sprites all zero" finding —
|
||||||
|
/// NOTHING in the codebase reads the popup wiring (raw dat attributes
|
||||||
|
/// 2/5/6/7 on the Type-0x10000038 menu leaf 0x10000224, per retail
|
||||||
|
/// <c>UIElement_Menu::MakePopup</c>/<c>Initialize</c>) or the face/arrow
|
||||||
|
/// child media (0x10000355/0x10000356) at all. Dumps BOTH — the exact
|
||||||
|
/// numbers <see cref="AcDream.App.UI.Layout.ConfigOptionsPageController"/>
|
||||||
|
/// needs to wire the menu the same way
|
||||||
|
/// <see cref="AcDream.App.UI.Layout.VendorUiController"/> already does for
|
||||||
|
/// its own dropdown — alongside the SAME properties on chat's known-good
|
||||||
|
/// channel menu (0x10000014 in 0x21000006) and vendor's dropdown
|
||||||
|
/// (0x100000BF in 0x21000012) as controls, so the popup source is
|
||||||
|
/// MEASURED rather than assumed to match either precedent.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void ProbeConfigMenuPopupChrome()
|
||||||
|
{
|
||||||
|
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("[menuprobe2] === Config tab menu row (0x2100002B template idx4) ===");
|
||||||
|
ElementInfo? configRow = LayoutImporter.ImportInfos(dats, 0x2100002Bu, 0x10000222u);
|
||||||
|
Assert.NotNull(configRow);
|
||||||
|
DumpMenuAttributes(configRow!, 0x10000224u, "Config 0x10000224");
|
||||||
|
DumpStateMedia(configRow!, 0x10000355u, "Config face 0x10000355");
|
||||||
|
DumpStateMedia(configRow!, 0x10000356u, "Config arrow 0x10000356");
|
||||||
|
|
||||||
|
Console.WriteLine("[menuprobe2] === CONTROL: chat channel menu (0x21000006/0x10000014) ===");
|
||||||
|
ElementInfo? chatRoot = LayoutImporter.ImportInfos(dats, 0x21000006u);
|
||||||
|
Assert.NotNull(chatRoot);
|
||||||
|
DumpMenuAttributes(chatRoot!, 0x10000014u, "Chat 0x10000014");
|
||||||
|
|
||||||
|
Console.WriteLine("[menuprobe2] === CONTROL: vendor category menu (0x21000012/0x100000BF) ===");
|
||||||
|
ElementInfo? vendorRoot = LayoutImporter.ImportInfos(dats, 0x21000012u);
|
||||||
|
Assert.NotNull(vendorRoot);
|
||||||
|
DumpMenuAttributes(vendorRoot!, 0x100000BFu, "Vendor 0x100000BF");
|
||||||
|
|
||||||
|
// Attribute 7 (DataID) is the popup's own catalog LayoutDesc — walk into
|
||||||
|
// it and dump its shape (root, ListBox, row template, scrollbar sibling)
|
||||||
|
// exactly like the vendor research already established for 0x21000043,
|
||||||
|
// so the SAME probe run tells us whether Config's popup is that layout,
|
||||||
|
// chat's own 0x21000006, or a third, dedicated catalog.
|
||||||
|
if (FindInfo(configRow!, 0x10000224u) is { } configMenu
|
||||||
|
&& configMenu.TryGetEffectiveProperty(7u, out UiPropertyValue popupLayout)
|
||||||
|
&& popupLayout.Kind == UiPropertyKind.DataId)
|
||||||
|
{
|
||||||
|
uint popupLayoutId = (uint)popupLayout.UnsignedValue;
|
||||||
|
Console.WriteLine($"[menuprobe2] Config popup catalog layout = 0x{popupLayoutId:X8} — dumping tree");
|
||||||
|
ElementInfo? popupRoot = LayoutImporter.ImportInfos(dats, popupLayoutId);
|
||||||
|
if (popupRoot is not null)
|
||||||
|
DumpInfoTree(popupRoot, 0);
|
||||||
|
else
|
||||||
|
Console.WriteLine($"[menuprobe2] popup layout 0x{popupLayoutId:X8} failed to import");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Dumps a menu leaf's raw popup-wiring properties — attribute 2
|
||||||
|
/// (its popup's own ListBox element id, per retail
|
||||||
|
/// <c>UIElement_Menu::Initialize</c>), 5 (bool, open-upward), 6 (Enum,
|
||||||
|
/// popup root element id), 7 (DataID, popup catalog LayoutDesc) — per
|
||||||
|
/// retail <c>UIElement_Menu::MakePopup @0x0046D310</c>. Absent properties
|
||||||
|
/// print as MISSING rather than a guessed default, matching
|
||||||
|
/// <c>GetAttribute_Bool</c>'s own absent-defaults-false semantics (the
|
||||||
|
/// caller decides what "missing" means, this probe only reports it).</summary>
|
||||||
|
private static void DumpMenuAttributes(ElementInfo root, uint menuId, string label)
|
||||||
|
{
|
||||||
|
ElementInfo? menu = FindInfo(root, menuId);
|
||||||
|
if (menu is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[menuprobe2] {label}: MISSING from imported tree");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[menuprobe2] {label}: type=0x{menu.Type:X8} ({menu.X},{menu.Y} {menu.Width}x{menu.Height}) "
|
||||||
|
+ $"children={menu.Children.Count} [{string.Join(",", menu.Children.ConvertAll(c => $"0x{c.Id:X8}"))}]");
|
||||||
|
|
||||||
|
foreach (uint attr in new[] { 2u, 5u, 6u, 7u })
|
||||||
|
{
|
||||||
|
if (menu.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($"[menuprobe2] attr[{attr}] kind={value.Kind} value={rendered}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[menuprobe2] attr[{attr}] MISSING");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Dumps every state's media (RenderSurface file id + draw mode)
|
||||||
|
/// for one child element — the face/arrow-cap art
|
||||||
|
/// <see cref="ProbeConfigMenuPopupChrome"/> needs to source
|
||||||
|
/// <c>UiMenu.NormalSprite</c>/<c>PressedSprite</c>/<c>ArrowCapClosedSprite</c>/
|
||||||
|
/// <c>ArrowCapOpenSprite</c> from, mirroring how
|
||||||
|
/// <see cref="AcDream.App.UI.Layout.VendorUiController"/>'s own doc cites
|
||||||
|
/// its label/arrow children's StateMedia.</summary>
|
||||||
|
private static void DumpStateMedia(ElementInfo root, uint childId, string label)
|
||||||
|
{
|
||||||
|
ElementInfo? child = FindInfo(root, childId);
|
||||||
|
if (child is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[menuprobe2] {label}: MISSING from imported tree");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[menuprobe2] {label}: type=0x{child.Type:X8} ({child.X},{child.Y} {child.Width}x{child.Height}) "
|
||||||
|
+ $"defaultState='{child.DefaultStateName}' states={child.States.Count}");
|
||||||
|
foreach ((string stateName, var media) in child.StateMedia)
|
||||||
|
Console.WriteLine($"[menuprobe2] state='{stateName}' -> file=0x{media.File:X8} drawMode={media.DrawMode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ElementInfo? FindInfo(ElementInfo root, uint id)
|
||||||
|
{
|
||||||
|
if (root.Id == id) return root;
|
||||||
|
foreach (ElementInfo c in root.Children)
|
||||||
|
{
|
||||||
|
ElementInfo? found = FindInfo(c, id);
|
||||||
|
if (found is not null) return found;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>#372 minor half: the 13 ID_ChatOption_TextFilter_* labels fail
|
/// <summary>#372 minor half: the 13 ID_ChatOption_TextFilter_* labels fail
|
||||||
/// to resolve in table 0x23000003 — sweep the plausible tables and key
|
/// to resolve in table 0x23000003 — sweep the plausible tables and key
|
||||||
/// spellings against the live DAT to find their real home.</summary>
|
/// spellings against the live DAT to find their real home.</summary>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue