diff --git a/src/AcDream.App/UI/Layout/InventoryController.cs b/src/AcDream.App/UI/Layout/InventoryController.cs index 187500c4..f051bb13 100644 --- a/src/AcDream.App/UI/Layout/InventoryController.cs +++ b/src/AcDream.App/UI/Layout/InventoryController.cs @@ -82,9 +82,9 @@ public sealed class InventoryController _burdenMeter.Fill = () => _burdenFill; } - // Captions: attach a centered UiText child carrying the known string. "Contents of - // Backpack" + "%d%%" are procedural in retail (gm3DItemsUI/gmBackpackUI PostInit/ - // SetLoadLevel); "Burden" is the dat label. (Caption pass, spec §5.) + // Captions: drive each host UiText directly with the known string (the caption + // elements resolve to UiText). "Contents of Backpack" + "%d%%" are procedural in retail + // (gm3DItemsUI/gmBackpackUI PostInit/SetLoadLevel); "Burden" is the dat label. (Spec §5.) AttachCaption(layout.FindElement(BurdenCaptionId), () => "Burden", datFont); AttachCaption(layout.FindElement(ContentsCaptionId), () => "Contents of Backpack", datFont); AttachCaption(layout.FindElement(BurdenTextId), () => _burdenPercent + "%", datFont); @@ -168,6 +168,30 @@ public sealed class InventoryController private void AttachCaption(UiElement? host, Func text, UiDatFont? datFont) { if (host is null) return; + + // The caption elements (0x100001D7 "Burden", 0x100001C5 "Contents of Backpack", + // 0x100001D8 "%") resolve to UiText (Type-0 inheriting a text base — confirmed live). + // Drive the host UiText DIRECTLY: it is already in the paint tree and renders, whereas + // a nested child UiText did not paint. Set it to a static centered single-line label. + if (host is UiText t) + { + t.Centered = true; + t.DatFont = datFont; + t.ClickThrough = true; + t.AcceptsFocus = false; + t.IsEditControl = false; + t.CapturesPointerDrag = false; + t.LinesProvider = () => + { + var s = text(); + return string.IsNullOrEmpty(s) + ? Array.Empty() + : new[] { new UiText.Line(s, CaptionColor) }; + }; + return; + } + + // Fallback (non-UiText host): attach a centered label child. var label = new UiText { Left = 0f, Top = 0f, Width = host.Width, Height = host.Height, diff --git a/src/AcDream.App/UI/Layout/LayoutImporter.cs b/src/AcDream.App/UI/Layout/LayoutImporter.cs index 26c63465..0a63d5dd 100644 --- a/src/AcDream.App/UI/Layout/LayoutImporter.cs +++ b/src/AcDream.App/UI/Layout/LayoutImporter.cs @@ -242,8 +242,21 @@ public static class LayoutImporter // (vitals/chat/toolbar). See ShouldMountBaseChildren + the B-Grid spec. if (baseChildren is not null && ShouldMountBaseChildren(d.Children.Count, self.StateMedia.Count, baseChildren.Count)) + { result.Children.AddRange(baseChildren); + // The mounted slot's layer WITHIN THE FRAME is its OWN ZLevel, not the mounted + // sub-window root's. The gm*UI sub-window roots carry ZLevel 1000 (their standalone + // top-window layer); ElementReader.Merge's zero-wins-base rule made the slot (own + // ZLevel 0) inherit that 1000, and the #145 ZOrder fold (ReadOrder − ZLevel·10000) + // turns 1000 into ZOrder ≈ −10,000,000 — sinking the whole panel BEHIND the frame's + // Alphablend backdrop (ZLevel 100 → ≈ −1,000,000). The backdrop then overpaints the + // panel's captions/meter/cells (the wash-out bug; the paperdoll root happens to be + // ZLevel 0 so it escaped). Restore the slot's own frame-layer so the panel sits in + // FRONT of the backdrop. (B-Controller debug 2026-06-21; continuation of #145.) + result.ZLevel = self.ZLevel; + } + return result; } diff --git a/tests/AcDream.App.Tests/UI/Layout/InventoryFrameImportProbe.cs b/tests/AcDream.App.Tests/UI/Layout/InventoryFrameImportProbe.cs new file mode 100644 index 00000000..bc4acf60 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/InventoryFrameImportProbe.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; +using AcDream.App.UI.Layout; +using DatReaderWriter; +using DatReaderWriter.Options; +using Xunit; + +namespace AcDream.App.Tests.UI.Layout; + +/// +/// Real-dat smoke test for the gmInventoryUI frame (0x21000023) import + the sub-window +/// mount. Locks the #145-continuation fix: the mount keeps each mounted panel slot's OWN +/// frame ZLevel instead of inheriting the sub-window root's ZLevel 1000. Before the fix the +/// merge's zero-wins-base rule gave the slots ZLevel 1000 → the #145 ZOrder fold +/// (ReadOrder − ZLevel·10000) sank them to ≈ −10,000,000, BEHIND the frame's Alphablend +/// backdrop (ZLevel 100 → ≈ −1,000,000), and the backdrop washed out the panels' captions, +/// burden meter, and item cells. Skips when the live dat directory is absent (CI). +/// +public class InventoryFrameImportProbe +{ + private const uint Frame = 0x21000023u; + private const uint Backdrop = 0x100001D0u; // full-window Alphablend backdrop (ZLevel 100) + private const uint BackpackPanel = 0x100001CEu; // mounted gmBackpackUI slot + private const uint ItemsPanel = 0x100001CFu; // mounted gm3DItemsUI slot + private const uint PaperdollPanel = 0x100001CDu; // mounted gmPaperDollUI slot + private const uint BurdenMeter = 0x100001D9u; // backpack content (proves the mount attached it) + private const uint ContentsGrid = 0x100001C6u; // 3D-items content + + private static string? DatDir() + { + var d = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR") + ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Documents", "Asheron's Call"); + return Directory.Exists(d) ? d : null; + } + + [Fact] + public void Mounted_panels_sit_in_front_of_the_backdrop() + { + var datDir = DatDir(); + if (datDir is null) return; // CI: no live dat — skip (this is a smoke test) + + using var dats = new DatCollection(datDir, DatAccessType.Read); + var layout = LayoutImporter.Import(dats, Frame, _ => (0u, 0, 0), null); + Assert.NotNull(layout); + + var backdrop = layout!.FindElement(Backdrop); + Assert.NotNull(backdrop); // the full-window backdrop is a direct child of the frame + + // The sub-window mount brings each panel's content into the by-id index. + Assert.NotNull(layout.FindElement(BurdenMeter)); // backpack burden meter + Assert.NotNull(layout.FindElement(ContentsGrid)); // 3D-items contents grid + + // The fix: every mounted panel slot must draw IN FRONT of the backdrop + // (higher ZOrder = painted later = on top), so the backdrop sits behind the content. + foreach (var (id, name) in new[] + { + (BackpackPanel, "backpack"), (ItemsPanel, "3D-items"), (PaperdollPanel, "paperdoll"), + }) + { + var panel = layout.FindElement(id); + Assert.True(panel is not null, $"{name} panel 0x{id:X8} missing from the imported tree"); + Assert.True(panel!.ZOrder > backdrop!.ZOrder, + $"{name} panel ZOrder {panel.ZOrder} must be > backdrop ZOrder {backdrop.ZOrder} " + + "(else the Alphablend backdrop overpaints/washes out the panel content)"); + } + } +}