diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 61e78960..d672e01d 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -24,6 +24,50 @@ What does NOT go here: - Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending. - Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed. +## #372 — Options panel: Character/Chat/Config tabs render BLANK on screen and most Gameplay buttons do nothing (connected-gate failure) + +**Status:** OPEN — filed 2026-08-11 at Campaign OP's first connected gate +(`ACDREAM_RETAIL_UI=1`, live ACE). The user found: opening the Options panel +(F11/toolbar) shows the Gameplay tab, but switching to Character/Chat/Config +shows a BLANK page, and of the seven Gameplay buttons only **Exit Game** +visibly did anything. + +**NOT a missing-layout bug — the panel builds fully.** A live-DAT mount probe +(`tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs`, +`ACDREAM_PROBE_LIVE_MOUNT=1`) confirms against the real DATs that the +production mount (`ImportInfos(0x2100006E, 0x1000018D)`) resolves the root as +a `UiTabPanel` with a 4-entry tab table, all four page slots +(`0x10000212/11/1000050C/13`), all three page ListBoxes as `UiTemplateListBox` +with their row templates (Character 3, Chat 9, Config 8), and all seven +Gameplay buttons as `UiButton`. The three page controllers' `Bind()` also run +at mount (the live log's `ChatOptionsPageController … 'ID_ChatOption_TextFilter_*' +did not resolve` spam proves ChatOptionsPageController.Bind executed) — and +every one of those `Bind` paths is green in the fixture-driven conformance +suite. + +**So the defect is in the LIVE render/input path the tests never exercise** — +the mounted → `ActivateTabs()` → tab-click → `SwitchTo` (page-slot +`Visible` flip) → row draw / button hit-test chain. Every campaign test +exercises `Bind()` in isolation and asserts widget structure; none drives a +real tab switch on a mounted panel and asserts the switched-in page's rows +actually draw, nor a real click reaching a Gameplay button's handler. This is +the exact structural-false-negative class the OP2 blast review named (green +tests over a live-only failure). Leading hypotheses to run down in the fix +(not yet root-caused): (a) page-slot `Visible` flips false→true AFTER the +ListBox/`UiScrollablePanel` computed its layout while hidden, so rows are +zero-height/culled until a relayout; (b) the switched-in page slot or the +Type-8 root's draw/hit-test doesn't cascade to descendants built post-mount; +(c) the "dead" Gameplay buttons (Exit-to-CharSel dialog, Use-Mouse-Turning +chat lines, UA/RA failure text) each have an invisible EFFECT rather than a +dead click — needs per-button confirmation. Configure Keyboard + In-Game +Help ARE correctly inert (OP8 pending). + +**Blocks the OP3/OP4/OP5/OP6 connected gates** — they cannot pass until the +non-Gameplay tabs render and the Gameplay buttons act. Fix is a dedicated +debug slice (live render-path instrumentation, NOT a guess), then a +gate-representative test that mounts+activates+switches+asserts-drawn so this +can never regress green again. + ## #371 — Options-panel row viewport culls whole rows instead of clipping; tall filter blocks can vanish entirely at some scroll offsets **Status:** OPEN — filed 2026-08-11 at the OP5 review-fix round (S2). diff --git a/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs new file mode 100644 index 00000000..83f52668 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs @@ -0,0 +1,99 @@ +using System.IO; +using AcDream.App.UI; +using AcDream.App.UI.Layout; +using DatReaderWriter; +using DatReaderWriter.Options; + +namespace AcDream.App.Tests.UI.Layout; + +/// +/// TEMPORARY gate-failure probe (Campaign OP connected gate, 2026-08-11): +/// the user's first connected gate found Character/Chat/Config tabs BLANK +/// and six of seven Gameplay buttons dead, while every fixture-driven +/// conformance test passes — the structural-false-negative class the OP2 +/// blast review named. This probe runs the PRODUCTION mount path (live +/// DATs, the host-slot import the composition uses) and dumps what each +/// page controller actually resolves. Env-gated like the fixture +/// generator so CI/dat-less runs skip it. +/// +public sealed class OptionsPanelLiveMountProbeTests +{ + [Fact] + public void ProbeLiveMountShapes() + { + 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); + + // The production mount: host 0x2100006E slot 0x1000018D (the shape + // OptionsPanelController documents; RetailUiRuntime.MountOptionsPanel + // resolves the SAME way — keep in sync with it). + ElementInfo root = LayoutImporter.ImportInfos(dats, 0x2100006Eu, 0x1000018Du); + ImportedLayout layout = LayoutImporter.Build(root, _ => (0u, 0, 0), null); + + Console.WriteLine($"[probe] root id=0x{root.Id:X8} type={root.Type} children={root.Children.Count}"); + DumpTree(root, 0, maxDepth: 3); + + // What does the flat index hold for the load-bearing ids? + foreach (uint id in new[] + { + 0x10000208u, // tab control + 0x10000212u, 0x10000211u, 0x1000050Cu, 0x10000213u, // page slots + 0x100001FAu, // Character ListBox + 0x1000050Du, // Chat ListBox + 0x10000200u, // Config ListBox + 0x10000203u, 0x10000617u, 0x100005CCu, // gameplay buttons: exit-char-sel, exit game, mouse turning + 0x10000206u, 0x10000207u, // UA / RA + }) + { + UiElement? el = layout.FindElement(id); + Console.WriteLine($"[probe] flat 0x{id:X8} -> {(el is null ? "MISSING" : el.GetType().Name)}"); + } + + // The tab panel + its authored table, as production sees it. + if (layout.FindElement(0x10000208u) is UiTabPanel tabs) + { + Console.WriteLine($"[probe] tab table entries={tabs.Tabs.Count}"); + foreach (UiTabTableEntry t in tabs.Tabs) + Console.WriteLine($"[probe] button=0x{t.ButtonElementId:X8} page=0x{t.PageElementId:X8} default={t.IsDefault}"); + } + + // Scoped lookups per page slot — what each page controller's Bind does. + foreach ((uint slot, uint listBox, string name) in new[] + { + (0x10000211u, 0x100001FAu, "Character"), + (0x1000050Cu, 0x1000050Du, "Chat"), + (0x10000213u, 0x10000200u, "Config"), + }) + { + UiElement? slotEl = layout.FindElement(slot); + if (slotEl is null) + { + Console.WriteLine($"[probe] {name}: SLOT 0x{slot:X8} MISSING from flat index"); + continue; + } + UiElement? scoped = UiElement.FindDescendant(slotEl, listBox); + Console.WriteLine( + $"[probe] {name}: slot=0x{slot:X8}({slotEl.GetType().Name}, children={slotEl.Children.Count}) " + + $"scoped-listbox 0x{listBox:X8} -> {(scoped is null ? "MISSING" : scoped.GetType().Name)}"); + if (scoped is UiTemplateListBox tlb) + Console.WriteLine($"[probe] templates={tlb.Templates.Count} scrollbarId=0x{tlb.ScrollbarElementId:X8}"); + } + } + + private static void DumpTree(ElementInfo node, int depth, int maxDepth) + { + if (depth > maxDepth) return; + Console.WriteLine( + $"[probe] {new string(' ', depth * 2)}0x{node.Id:X8} T={node.Type} " + + $"kids={node.Children.Count} tabTable={node.TabTable.Count} tmpl={node.TemplateList.Count}"); + foreach (ElementInfo child in node.Children) + DumpTree(child, depth + 1, maxDepth); + } +}