diff --git a/tests/AcDream.App.Tests/UI/Layout/MapHousePanelSlotProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/MapHousePanelSlotProbeTests.cs new file mode 100644 index 00000000..9f2850b2 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/MapHousePanelSlotProbeTests.cs @@ -0,0 +1,215 @@ +using System.IO; +using AcDream.App.UI.Layout; +using DatReaderWriter; +using DatReaderWriter.Options; + +namespace AcDream.App.Tests.UI.Layout; + +/// +/// Batch C (overnight hover/UI round, Map/House toolbar panel) discovery +/// probe. Desk research (docs/research/2026-08-17-map-house-recon.md, +/// itself built on the FA campaign's already-DAT-verified 16-slot +/// gmPanelUI::SetupChildren dump) already resolved host +/// 0x2100006E slot 0x1000018C / RetailPanelCatalog id +/// 16 as the Map/House tab host, and toolbar button +/// 0x1000019A as the (currently ghosted) button that opens it. This +/// probe RE-CONFIRMS both against the live installed DATs rather than +/// trusting the committed fixture, and additionally reads the facts the +/// decomp explains the MEANING of but not the VALUE of: the tab table, the +/// m_pMap marker-area rect + hotspot template attrs (0x47/ +/// 0x48), and the House ListBox's authored default content. +/// +/// +/// Kept as a permanent env-gated pin (like FaPanelSlotProbeTests) — +/// it documents authored truth for future sessions, not a one-shot +/// throwaway. +/// +/// +public sealed class MapHousePanelSlotProbeTests +{ + private const uint HostLayoutId = 0x2100006Eu; + private const uint SlotElementId = 0x1000018Cu; + private const uint ToolbarLayoutId = 0x21000016u; + private const uint MapHouseToolbarButtonId = 0x1000019Au; + + // gmMapUI PostInit signature children (pc:171993). + private const uint MapDateTimeTextId = 0x100001EBu; + private const uint MapWidgetId = 0x100001ECu; + private const uint MapPlayerIconId = 0x100001EDu; + private const uint MapHouseIconId = 0x100001EEu; + private const uint MapCoordinateTextId = 0x100001EFu; + + // gmHouseUI PostInit signature child (pc:172581). + private const uint HouseTextBoxId = 0x100001E6u; + + [Fact] + public void ProbeMapHousePanelSlot() + { + 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); + + // 1) The slot itself — panel id, type, tab table. + ElementInfo? slot = LayoutImporter.ImportInfos(dats, HostLayoutId, SlotElementId); + if (slot is null) + { + Console.WriteLine($"[maphouse] slot 0x{SlotElementId:X8} -> IMPORT NULL"); + return; + } + + string panelId = slot.TryGetEffectiveProperty(0x10000029u, out var p) + ? $"{p.UnsignedValue} (kind={p.Kind})" + : "ABSENT"; + Console.WriteLine( + $"[maphouse] slot 0x{SlotElementId:X8} panelId={panelId} type={slot.Type} " + + $"({slot.X},{slot.Y} {slot.Width}x{slot.Height}) children={slot.Children.Count} " + + $"tabTableCount={slot.TabTable.Count}"); + foreach (var tab in slot.TabTable) + Console.WriteLine( + $"[maphouse] tab button=0x{tab.ButtonElementId:X8} page=0x{tab.PageElementId:X8} " + + $"default={tab.IsDefault}"); + + bool hasMapSignature = FindInfo(slot, MapDateTimeTextId) || FindInfo(slot, MapWidgetId); + bool hasHouseSignature = FindInfo(slot, HouseTextBoxId); + Console.WriteLine( + $"[maphouse] hasMapSignature={hasMapSignature} hasHouseSignature={hasHouseSignature}"); + + // Two levels of children so we can see the tab pages' own shape even + // if the recovered signature ids above are absent from this install. + foreach (ElementInfo c in slot.Children) + { + Console.WriteLine( + $"[maphouse] child 0x{c.Id:X8} type={c.Type} ({c.X},{c.Y} {c.Width}x{c.Height}) " + + $"kids={c.Children.Count}"); + foreach (ElementInfo g in c.Children) + Console.WriteLine( + $"[maphouse] g 0x{g.Id:X8} type={g.Type} ({g.X},{g.Y} {g.Width}x{g.Height}) " + + $"kids={g.Children.Count}"); + } + + // 2) The toolbar button that should open panel 16. + ElementInfo? button = LayoutImporter.ImportInfos(dats, ToolbarLayoutId, MapHouseToolbarButtonId); + string buttonPanelId = button is not null && button.TryGetEffectiveProperty(0x10000029u, out var bp) + ? $"{bp.UnsignedValue} (kind={bp.Kind})" + : "ABSENT/NULL"; + Console.WriteLine($"[maphouse] toolbar button 0x{MapHouseToolbarButtonId:X8} panelId={buttonPanelId}"); + + // 3) m_pMap's own marker-area rect + hotspot template attrs. + ElementInfo? map = FindDescendant(slot, MapWidgetId); + if (map is null) + { + Console.WriteLine($"[maphouse] m_pMap 0x{MapWidgetId:X8} NOT FOUND under slot"); + } + else + { + string x0 = map.TryGetEffectiveProperty(0x1000004Eu, out var vx0) ? vx0.IntegerValue.ToString() : "ABSENT"; + string x1 = map.TryGetEffectiveProperty(0x1000004Fu, out var vx1) ? vx1.IntegerValue.ToString() : "ABSENT"; + string y0 = map.TryGetEffectiveProperty(0x10000050u, out var vy0) ? vy0.IntegerValue.ToString() : "ABSENT"; + string y1 = map.TryGetEffectiveProperty(0x10000051u, out var vy1) ? vy1.IntegerValue.ToString() : "ABSENT"; + bool hasTemplateElement = map.TryGetEffectiveProperty(0x47u, out var te); + bool hasTemplateLayoutDid = map.TryGetEffectiveProperty(0x48u, out var tl); + string templateElement = hasTemplateElement + ? $"0x{te.UnsignedValue:X8} (kind={te.Kind})" : "ABSENT"; + string templateLayoutDid = hasTemplateLayoutDid + ? $"0x{tl.UnsignedValue:X8} (kind={tl.Kind})" : "ABSENT"; + Console.WriteLine( + $"[maphouse] m_pMap 0x{MapWidgetId:X8} markerArea=({x0},{y0})-({x1},{y1}) " + + $"templateElement(attr 0x47)={templateElement} templateLayoutDid(attr 0x48)={templateLayoutDid} " + + $"type={map.Type} children={map.Children.Count}"); + + // If both resolved, try importing the actual hotspot template so we + // know what widget class AddMapNote instantiates per town. + if (hasTemplateElement && hasTemplateLayoutDid && tl.UnsignedValue != 0) + { + ElementInfo? template = LayoutImporter.ImportInfos(dats, (uint)tl.UnsignedValue, (uint)te.UnsignedValue); + Console.WriteLine(template is null + ? "[maphouse] hotspot template IMPORT NULL" + : $"[maphouse] hotspot template type={template.Type} " + + $"({template.Width}x{template.Height}) states={template.States.Count} " + + $"stateMedia={template.StateMedia.Count}"); + } + } + + ElementInfo? playerIcon = FindDescendant(slot, MapPlayerIconId); + ElementInfo? houseIcon = FindDescendant(slot, MapHouseIconId); + Console.WriteLine( + $"[maphouse] playerIcon found={playerIcon is not null} type={playerIcon?.Type} " + + $"stateMedia={playerIcon?.StateMedia.Count}"); + Console.WriteLine( + $"[maphouse] houseIcon found={houseIcon is not null} type={houseIcon?.Type} " + + $"stateMedia={houseIcon?.StateMedia.Count}"); + + // 4) House ListBox default content — dump its own children/text so we + // know whether "You do not currently own a house..." is authored + // directly in the layout or synthesized at runtime. + ElementInfo? houseBox = FindDescendant(slot, HouseTextBoxId); + if (houseBox is null) + { + Console.WriteLine($"[maphouse] house listbox 0x{HouseTextBoxId:X8} NOT FOUND under slot"); + } + else + { + Console.WriteLine( + $"[maphouse] house listbox 0x{HouseTextBoxId:X8} type={houseBox.Type} " + + $"children={houseBox.Children.Count} templateListCount={houseBox.TemplateList.Count} " + + $"scrollbar=0x{houseBox.ScrollbarElementId:X8}"); + foreach (var t in houseBox.TemplateList) + { + Console.WriteLine( + $"[maphouse] template layoutId=0x{t.TemplateLayoutId:X8} elementId=0x{t.TemplateElementId:X8}"); + ElementInfo? rowTemplate = LayoutImporter.ImportInfos(dats, t.TemplateLayoutId, t.TemplateElementId); + if (rowTemplate is null) + { + Console.WriteLine("[maphouse] row template IMPORT NULL"); + continue; + } + Console.WriteLine( + $"[maphouse] row template type={rowTemplate.Type} " + + $"({rowTemplate.Width}x{rowTemplate.Height}) kids={rowTemplate.Children.Count}"); + foreach (ElementInfo rc in rowTemplate.Children) + Console.WriteLine( + $"[maphouse] rc 0x{rc.Id:X8} type={rc.Type} ({rc.X},{rc.Y} {rc.Width}x{rc.Height})"); + } + foreach (ElementInfo row in houseBox.Children) + { + string caption = row.TryGetEffectiveProperty(0x17u, out var cap) + ? $"StringInfo(table={cap.StringInfoValue.TableId:X},id={cap.StringInfoValue.StringId:X},lit={cap.StringInfoValue.English != 0})" + : "NO 0x17"; + Console.WriteLine( + $"[maphouse] row 0x{row.Id:X8} type={row.Type} caption={caption} kids={row.Children.Count}"); + foreach (ElementInfo g in row.Children) + { + string gcaption = g.TryGetEffectiveProperty(0x17u, out var gcap) + ? $"StringInfo(table={gcap.StringInfoValue.TableId:X},id={gcap.StringInfoValue.StringId:X})" + : "NO 0x17"; + Console.WriteLine($"[maphouse] g 0x{g.Id:X8} type={g.Type} caption={gcaption}"); + } + } + } + } + + private static bool FindInfo(ElementInfo info, uint id) + { + if (info.Id == id) return true; + foreach (ElementInfo c in info.Children) + if (FindInfo(c, id)) return true; + return false; + } + + private static ElementInfo? FindDescendant(ElementInfo info, uint id) + { + if (info.Id == id) return info; + foreach (ElementInfo c in info.Children) + { + ElementInfo? found = FindDescendant(c, id); + if (found is not null) return found; + } + return null; + } +}