acdream/tests/AcDream.App.Tests/UI/Layout/MapHousePanelSlotProbeTests.cs
Erik 04841b6807 feat(ui): Map/House panel — slice 1 discovery probe
Live-DAT probe confirming the desk-verified facts before implementation:
host 0x2100006E slot 0x1000018C carries panelId 16 (gmMapUI::PostInit
signature children 0x100001EB-EF; gmHouseUI's ListBox 0x100001E6),
tabTableCount=2 with Map (button 0x100001F3 -> page 0x100001F6) as the
authored default and House (0x100001F4 -> 0x100001F7) second, close button
0x100001F5. Toolbar button 0x1000019A carries the matching panelId 16 —
the Map/House entry among the toolbar's three ghosted buttons. m_pMap's
own marker-area rect is (6,8)-(247,258); its hotspot template attrs
(0x47/0x48) resolve to element 0x100001F0 in LayoutDesc 0x21000026, a
10x10 Type-1 button with 3 states. The House ListBox authors exactly one
row template (LayoutDesc 0x21000025 element 0x100001E7, a bare
UIElement_Text row, no scrollbar) and zero static child rows — the box is
genuinely empty until the first server notice, refuting the recon's
"authored default content" hypothesis for the no-house case.

Kept as a permanent env-gated pin (ACDREAM_PROBE_LIVE_MOUNT=1), matching
FaPanelSlotProbeTests' precedent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 02:03:49 +02:00

215 lines
10 KiB
C#

using System.IO;
using AcDream.App.UI.Layout;
using DatReaderWriter;
using DatReaderWriter.Options;
namespace AcDream.App.Tests.UI.Layout;
/// <summary>
/// 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
/// <c>gmPanelUI::SetupChildren</c> dump) already resolved host
/// <c>0x2100006E</c> slot <c>0x1000018C</c> / <c>RetailPanelCatalog</c> id
/// <b>16</b> as the Map/House tab host, and toolbar button
/// <c>0x1000019A</c> 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
/// <c>m_pMap</c> marker-area rect + hotspot template attrs (<c>0x47</c>/
/// <c>0x48</c>), and the House ListBox's authored default content.
///
/// <para>
/// Kept as a permanent env-gated pin (like <c>FaPanelSlotProbeTests</c>) —
/// it documents authored truth for future sessions, not a one-shot
/// throwaway.
/// </para>
/// </summary>
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;
}
}