using System.IO; using AcDream.App.UI; using AcDream.App.UI.Layout; using DatReaderWriter; using DatReaderWriter.Options; namespace AcDream.App.Tests.UI.Layout; /// /// Campaign FA slice FA3, item 7: extends the ACDREAM_PROBE_LIVE_MOUNT pattern /// (the #372/#375/#378 lesson — fixture-green alone is NOT acceptance for /// anything mounted) to the four-tab social panel. Asserts the PRODUCTION /// mount path resolves: the tab host as with its /// 4-entry table, all four page elements, the fellowship empty/full frame /// pair, the allegiance signature elements, and non-empty captions on the /// tab buttons (the #375 resolver class). Also dumps the raw tab table so /// its button→page pairing + default entry can be recorded — coordinator /// addendum, docs/research/2026-08-11-fa-panel-structure.md §10. /// public sealed class SocialPanelLiveMountProbeTests { [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); var strings = new DatStringResolver(dats); ElementInfo? root = LayoutImporter.ImportInfos( dats, SocialPanelController.HostLayoutId, SocialPanelController.SlotElementId); Assert.NotNull(root); ImportedLayout layout = LayoutImporter.Build( root!, _ => (1u, 8, 8), null, null, strings.Resolve); Console.WriteLine( $"[socialprobe] root id=0x{root!.Id:X8} type={root.Type} " + $"({root.X},{root.Y} {root.Width}x{root.Height}) children={root.Children.Count} " + $"restorePrevious={(root.TryGetEffectiveBool(RetailPanelUiController.RestorePreviousPropertyId, out bool rp) && rp)}"); foreach (ElementInfo c in root.Children) { string p12 = c.TryGetEffectiveProperty(0x12u, out var v12) ? $"0x{v12.UnsignedValue:X8}" : "ABSENT"; string p57 = c.TryGetEffectiveProperty(0x57u, out var v57) ? $"{v57.Kind}=0x{v57.UnsignedValue:X8}" : "ABSENT"; Console.WriteLine( $"[socialprobe] root-child 0x{c.Id:X8} type=0x{c.Type:X8} ({c.X},{c.Y} {c.Width}x{c.Height}) " + $"kids={c.Children.Count} P0x12={p12} P0x57={p57}"); } { string rootP57 = root.TryGetEffectiveProperty(0x57u, out var rv57) ? $"{rv57.Kind}=0x{rv57.UnsignedValue:X8}" : "ABSENT"; Console.WriteLine($"[socialprobe] root P0x57={rootP57}"); } UiTabPanel tabs = Assert.IsType(layout.Root); Console.WriteLine($"[socialprobe] tab table entries={tabs.Tabs.Count}"); foreach (UiTabTableEntry t in tabs.Tabs) Console.WriteLine( $"[socialprobe] button=0x{t.ButtonElementId:X8} page=0x{t.PageElementId:X8} default={t.IsDefault}"); Assert.True(tabs.Tabs.Count >= 4); // Production mount: activate behavior exactly like SocialPanelController.ActivateTabs. tabs.ActivateTabBehavior(); Assert.Empty(tabs.UnresolvedEntries); foreach ((uint pageId, string name) in new[] { (0x10000513u, "Friends"), (0x10000291u, "Allegiance"), (0x10000292u, "Fellowship"), (0x1000054Au, "Squelch"), }) { UiElement? page = UiElement.FindDescendant(tabs, pageId); Console.WriteLine( $"[socialprobe] page {name} 0x{pageId:X8} -> {(page is null ? "MISSING" : page.GetType().Name)}"); Assert.NotNull(page); } // Fellowship empty/full frame pair. foreach ((uint id, string name) in new[] { (0x1000026Bu, "NotInAFellowshipFrame"), (0x10000275u, "InAFellowshipFrame"), }) { UiElement? el = UiElement.FindDescendant(tabs, id); Console.WriteLine($"[socialprobe] fellowship frame {name} 0x{id:X8} -> {(el is null ? "MISSING" : el.GetType().Name)}"); Assert.NotNull(el); } // Allegiance signature elements. foreach ((uint id, string name) in new[] { (0x10000255u, "MonarchField"), (0x1000025Au, "PatronField"), (0x10000260u, "VassalListBox"), (0x10000263u, "SwearButton"), (0x10000264u, "BreakButton"), (0x10000265u, "KickButton"), }) { UiElement? el = UiElement.FindDescendant(tabs, id); Console.WriteLine($"[socialprobe] allegiance element {name} 0x{id:X8} -> {(el is null ? "MISSING" : el.GetType().Name)}"); Assert.NotNull(el); } // 0x10000492 is authored TWICE inside the allegiance page (monarch // block child AND patron block child) — count occurrences under the // allegiance page root specifically, not the whole tree. UiElement? allegiancePage = UiElement.FindDescendant(tabs, 0x10000291u); Assert.NotNull(allegiancePage); int passupCount = CountDescendants(allegiancePage!, 0x10000492u); Console.WriteLine($"[socialprobe] 0x10000492 occurrences under allegiance page = {passupCount}"); // Tab button captions — non-empty (the #375 resolver class: a missing // string resolver renders blank captions even though the layout mounts). foreach (UiTabTableEntry t in tabs.Tabs) { UiElement? button = UiElement.FindDescendant(tabs, t.ButtonElementId); string? caption = button switch { UiText text => text.LinesProvider?.Invoke() is { Count: > 0 } lines ? lines[0].Text : null, UiButton btn => btn.Label, _ => null, }; Console.WriteLine( $"[socialprobe] tab button 0x{t.ButtonElementId:X8} ({button?.GetType().Name}) caption='{caption}'"); Assert.False(string.IsNullOrEmpty(caption)); } // U6: containment of the fellowship empty/full frames. ElementInfo? notInFellowshipFrame = FindInfo(root, 0x1000026Bu); ElementInfo? inFellowshipFrame = FindInfo(root, 0x10000275u); Console.WriteLine( $"[socialprobe] 0x1000026B (NotInAFellowshipFrame) children: " + string.Join(",", (notInFellowshipFrame?.Children ?? new()).ConvertAll(c => $"0x{c.Id:X8}"))); Console.WriteLine( $"[socialprobe] 0x10000275 (InAFellowshipFrame) children: " + string.Join(",", (inFellowshipFrame?.Children ?? new()).ConvertAll(c => $"0x{c.Id:X8}"))); // Dump two levels of children under each page slot for U3/U4/U6/U7 sweep. foreach (uint pageId in new[] { 0x10000513u, 0x10000291u, 0x10000292u, 0x1000054Au }) { ElementInfo? pageInfo = FindInfo(root, pageId); if (pageInfo is null) { Console.WriteLine($"[socialprobe] page-info 0x{pageId:X8} MISSING from ElementInfo tree"); continue; } Console.WriteLine( $"[socialprobe] page-info 0x{pageId:X8} P0x57={(pageInfo.TryGetEffectiveProperty(0x57u, out var p57) ? p57.UnsignedValue.ToString() : "ABSENT")} children={pageInfo.Children.Count}"); DumpInfoTree(pageInfo, 1, maxDepth: 3); } // Friends/Squelch row templates — find the name-text child. foreach ((uint tLayout, uint tElement, string name) in new[] { (0x2100005Du, 0x10000519u, "FriendsRow"), (0x21000060u, 0x10000541u, "SquelchRow"), }) { ElementInfo? row = LayoutImporter.ImportInfos(dats, tLayout, tElement); if (row is null) { Console.WriteLine($"[socialprobe] {name} template 0x{tLayout:X8}/0x{tElement:X8} IMPORT NULL"); continue; } Console.WriteLine($"[socialprobe] {name} template:"); DumpInfoTree(row, 1, maxDepth: 3); } } private static void DumpInfoTree(ElementInfo info, int depth, int maxDepth) { if (depth > maxDepth) return; string templates = info.TemplateList.Count > 0 ? $" templates={info.TemplateList.Count}[{string.Join(",", info.TemplateList.ConvertAll(t => $"0x{t.TemplateLayoutId:X8}/0x{t.TemplateElementId:X8}"))}] scrollbar=0x{info.ScrollbarElementId:X8}" : ""; Console.WriteLine( $"[socialprobe] {new string(' ', depth * 2)}0x{info.Id:X8} type=0x{info.Type:X8} " + $"({info.X},{info.Y} {info.Width}x{info.Height}) kids={info.Children.Count}{templates}"); foreach (ElementInfo c in info.Children) DumpInfoTree(c, depth + 1, maxDepth); } private static int CountDescendants(UiElement root, uint id) { int count = root.DatElementId == id ? 1 : 0; foreach (UiElement child in root.Children) count += CountDescendants(child, id); return count; } 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; } }