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}");
}
}
/// #378 (gate 4): the Config tab's dropdown menus render bare —
/// no button well, no arrow, no popup on click — while the chat channel
/// menu works. Dump the BUILT UiMenu's sprite/chrome state for the Config
/// menu row template against the real DATs, so the missing piece is
/// measured rather than guessed.
[Fact]
public void ProbeConfigMenuChrome()
{
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, 0x2100006Eu, 0x1000018Du);
ImportedLayout layout = LayoutImporter.Build(
root, _ => (1u, 8, 8), null, null, strings.Resolve);
UiElement? configSlot = layout.FindElement(0x10000213u);
Assert.NotNull(configSlot);
UiElement? lbEl = UiElement.FindDescendant(configSlot!, 0x10000200u);
UiTemplateListBox lb = Assert.IsType(lbEl);
Console.WriteLine($"[menuprobe] Config ListBox templates={lb.Templates.Count}");
for (int i = 0; i < lb.Templates.Count; i++)
Console.WriteLine(
$"[menuprobe] template[{i}] 0x{lb.Templates[i].TemplateLayoutId:X8}/0x{lb.Templates[i].TemplateElementId:X8}");
// Build template[4] (MenuTemplateIndex) exactly like production:
// through the SAME resolver shape RetailUiRuntime wires.
(uint tLayout, uint tElement) =
(lb.Templates[4].TemplateLayoutId, lb.Templates[4].TemplateElementId);
ElementInfo? tInfo = LayoutImporter.ImportInfos(dats, tLayout, tElement);
Assert.NotNull(tInfo);
DumpInfoTree(tInfo!, 0);
UiElement rowBuilt = LayoutImporter.Build(
tInfo!, _ => (1u, 8, 8), null, null, strings.Resolve).Root;
UiElement? menuEl = UiElement.FindDescendant(rowBuilt, 0x10000224u);
Console.WriteLine(
$"[menuprobe] menu leaf 0x10000224 -> {(menuEl is null ? "MISSING" : menuEl.GetType().Name)} "
+ (menuEl is null ? "" : $"({menuEl.Left},{menuEl.Top} {menuEl.Width}x{menuEl.Height})"));
if (menuEl is UiMenu m)
{
Console.WriteLine(
$"[menuprobe] sprites: normal=0x{m.NormalSprite:X8} pressed=0x{m.PressedSprite:X8} "
+ $"popupBg=0x{m.PopupBgSprite:X8} itemNormal=0x{m.ItemNormalSprite:X8} "
+ $"itemHighlight=0x{m.ItemHighlightSprite:X8} arrowClosed=0x{m.ArrowCapClosedSprite:X8} "
+ $"arrowOpen=0x{m.ArrowCapOpenSprite:X8} spriteResolve={(m.SpriteResolve is null ? "NULL" : "set")} "
+ $"openUpward={m.OpenUpward} rows={m.RowsPerColumn} rowH={m.RowHeight} colW={m.ColumnWidth}");
}
}
private static void DumpInfoTree(ElementInfo info, int depth)
{
Console.WriteLine(
$"[menuprobe] info {new string(' ', depth * 2)}0x{info.Id:X8} type={info.Type} "
+ $"({info.X},{info.Y} {info.Width}x{info.Height}) children={info.Children.Count}");
if (depth >= 3) return;
foreach (ElementInfo c in info.Children)
DumpInfoTree(c, depth + 1);
}
/// #378 (gate 4): the missing piece behind
/// 's "sprites all zero" finding —
/// NOTHING in the codebase reads the popup wiring (raw dat attributes
/// 2/5/6/7 on the Type-0x10000038 menu leaf 0x10000224, per retail
/// UIElement_Menu::MakePopup/Initialize) or the face/arrow
/// child media (0x10000355/0x10000356) at all. Dumps BOTH — the exact
/// numbers
/// needs to wire the menu the same way
/// already does for
/// its own dropdown — alongside the SAME properties on chat's known-good
/// channel menu (0x10000014 in 0x21000006) and vendor's dropdown
/// (0x100000BF in 0x21000012) as controls, so the popup source is
/// MEASURED rather than assumed to match either precedent.
[Fact]
public void ProbeConfigMenuPopupChrome()
{
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);
Console.WriteLine("[menuprobe2] === Config tab menu row (0x2100002B template idx4) ===");
ElementInfo? configRow = LayoutImporter.ImportInfos(dats, 0x2100002Bu, 0x10000222u);
Assert.NotNull(configRow);
DumpMenuAttributes(configRow!, 0x10000224u, "Config 0x10000224");
DumpStateMedia(configRow!, 0x10000355u, "Config face 0x10000355");
DumpStateMedia(configRow!, 0x10000356u, "Config arrow 0x10000356");
Console.WriteLine("[menuprobe2] === CONTROL: chat channel menu (0x21000006/0x10000014) ===");
ElementInfo? chatRoot = LayoutImporter.ImportInfos(dats, 0x21000006u);
Assert.NotNull(chatRoot);
DumpMenuAttributes(chatRoot!, 0x10000014u, "Chat 0x10000014");
Console.WriteLine("[menuprobe2] === CONTROL: vendor category menu (0x21000012/0x100000BF) ===");
ElementInfo? vendorRoot = LayoutImporter.ImportInfos(dats, 0x21000012u);
Assert.NotNull(vendorRoot);
DumpMenuAttributes(vendorRoot!, 0x100000BFu, "Vendor 0x100000BF");
// Attribute 7 (DataID) is the popup's own catalog LayoutDesc — walk into
// it and dump its shape (root, ListBox, row template, scrollbar sibling)
// exactly like the vendor research already established for 0x21000043,
// so the SAME probe run tells us whether Config's popup is that layout,
// chat's own 0x21000006, or a third, dedicated catalog.
if (FindInfo(configRow!, 0x10000224u) is { } configMenu
&& configMenu.TryGetEffectiveProperty(7u, out UiPropertyValue popupLayout)
&& popupLayout.Kind == UiPropertyKind.DataId)
{
uint popupLayoutId = (uint)popupLayout.UnsignedValue;
Console.WriteLine($"[menuprobe2] Config popup catalog layout = 0x{popupLayoutId:X8} — dumping tree");
ElementInfo? popupRoot = LayoutImporter.ImportInfos(dats, popupLayoutId);
if (popupRoot is not null)
DumpInfoTree(popupRoot, 0);
else
Console.WriteLine($"[menuprobe2] popup layout 0x{popupLayoutId:X8} failed to import");
}
}
/// Dumps a menu leaf's raw popup-wiring properties — attribute 2
/// (its popup's own ListBox element id, per retail
/// UIElement_Menu::Initialize), 5 (bool, open-upward), 6 (Enum,
/// popup root element id), 7 (DataID, popup catalog LayoutDesc) — per
/// retail UIElement_Menu::MakePopup @0x0046D310. Absent properties
/// print as MISSING rather than a guessed default, matching
/// GetAttribute_Bool's own absent-defaults-false semantics (the
/// caller decides what "missing" means, this probe only reports it).
private static void DumpMenuAttributes(ElementInfo root, uint menuId, string label)
{
ElementInfo? menu = FindInfo(root, menuId);
if (menu is null)
{
Console.WriteLine($"[menuprobe2] {label}: MISSING from imported tree");
return;
}
Console.WriteLine(
$"[menuprobe2] {label}: type=0x{menu.Type:X8} ({menu.X},{menu.Y} {menu.Width}x{menu.Height}) "
+ $"children={menu.Children.Count} [{string.Join(",", menu.Children.ConvertAll(c => $"0x{c.Id:X8}"))}]");
foreach (uint attr in new[] { 2u, 5u, 6u, 7u })
{
if (menu.TryGetEffectiveProperty(attr, out UiPropertyValue value))
{
string rendered = value.Kind switch
{
UiPropertyKind.Bool => value.BoolValue.ToString(),
UiPropertyKind.DataId or UiPropertyKind.Enum
=> $"0x{value.UnsignedValue:X8}",
UiPropertyKind.Integer => value.IntegerValue.ToString(),
_ => value.Kind.ToString(),
};
Console.WriteLine($"[menuprobe2] attr[{attr}] kind={value.Kind} value={rendered}");
}
else
{
Console.WriteLine($"[menuprobe2] attr[{attr}] MISSING");
}
}
}
/// Dumps every state's media (RenderSurface file id + draw mode)
/// for one child element — the face/arrow-cap art
/// needs to source
/// UiMenu.NormalSprite/PressedSprite/ArrowCapClosedSprite/
/// ArrowCapOpenSprite from, mirroring how
/// 's own doc cites
/// its label/arrow children's StateMedia.
private static void DumpStateMedia(ElementInfo root, uint childId, string label)
{
ElementInfo? child = FindInfo(root, childId);
if (child is null)
{
Console.WriteLine($"[menuprobe2] {label}: MISSING from imported tree");
return;
}
Console.WriteLine(
$"[menuprobe2] {label}: type=0x{child.Type:X8} ({child.X},{child.Y} {child.Width}x{child.Height}) "
+ $"defaultState='{child.DefaultStateName}' states={child.States.Count}");
foreach ((string stateName, var media) in child.StateMedia)
Console.WriteLine($"[menuprobe2] state='{stateName}' -> file=0x{media.File:X8} drawMode={media.DrawMode}");
}
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;
}
/// User gate report 2026-08-13 (Campaign OP happy-testing round):
/// every Options-panel dropdown draws its text yellow + left-aligned (user:
/// retail is white + centered) and its popup a fixed 6 rows tall (user:
/// retail conforms to the item count). Retail mechanism, decomp-verified:
/// UIElement_Menu::RecalculatePopupSize @0x0046caf0 resizes the popup
/// to the ListBox's content extent (m_iScrollableHeight = SUM of laid
/// out row heights, uncapped — 0x0046e5f4..0046e66c) + the authored
/// popup-minus-ListBox border, but ONLY when the authored ListBox is docked
/// on both edges (m_leftEdge==1 && m_rightEdge==1 /
/// m_topEdge==1 && m_bottomEdge==1); the caption is an
/// authored UIElement_Text child named by menu attr 8
/// (NewSelection @0x0046cd60) whose color/justify come from the
/// LayoutDesc, and Open @0x0046cc30 centers the POPUP over the
/// button when bool attr 3 is authored. This probe measures all of that
/// authored data for the Config option-menu chain (catalog 0x21000043,
/// base 0x10000353) with vendor's dropdown (0x1000034F chain — visibly a
/// FIXED 6-row + scrollbar popup in retail, user-gated during the vendor
/// campaign) as the contrast control.
[Fact]
public void ProbeMenuPopupSizingAndTextStyle()
{
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);
Console.WriteLine("[menuprobe3] === Config option-menu base 0x21000043/0x10000353 ===");
ElementInfo? menuBase = LayoutImporter.ImportInfos(dats, 0x21000043u, 0x10000353u);
if (menuBase is null)
{
Console.WriteLine("[menuprobe3] 0x10000353 FAILED to import");
}
else
{
DumpMenuAttrs3(menuBase, "base 0x10000353", new[] { 2u, 3u, 5u, 6u, 7u, 8u, 9u });
foreach (ElementInfo c in menuBase.Children)
DumpTextStyle(c, $"base child 0x{c.Id:X8}");
}
Console.WriteLine("[menuprobe3] === Config leaf 0x2100002B/0x10000224 (attrs 3+8) ===");
ElementInfo? configRow = LayoutImporter.ImportInfos(dats, 0x2100002Bu, 0x10000222u);
if (configRow is not null && FindInfo(configRow, 0x10000224u) is { } leaf)
{
DumpMenuAttrs3(leaf, "leaf 0x10000224", new[] { 3u, 8u });
foreach (ElementInfo c in leaf.Children)
DumpTextStyle(c, $"leaf child 0x{c.Id:X8}");
}
else
{
Console.WriteLine("[menuprobe3] leaf 0x10000224 MISSING");
}
Console.WriteLine("[menuprobe3] === Config popup root/ListBox/row template ===");
DumpDockAndSize(dats, 0x21000043u, 0x10000357u, "Config popup root");
DumpDockAndSize(dats, 0x21000043u, 0x10000358u, "Config popup ListBox");
ElementInfo? rowTemplate = LayoutImporter.ImportInfos(dats, 0x21000043u, 0x1000035Au);
if (rowTemplate is not null)
{
DumpTextStyle(rowTemplate, "Config row template 0x1000035A");
foreach (ElementInfo c in rowTemplate.Children)
DumpTextStyle(c, $"row-template child 0x{c.Id:X8}");
}
else
{
Console.WriteLine("[menuprobe3] row template 0x1000035A FAILED to import");
}
Console.WriteLine("[menuprobe3] === CONTROL: vendor chain (fixed 6-row + scrollbar in retail) ===");
DumpDockAndSize(dats, 0x21000043u, 0x1000034Fu, "Vendor popup root");
DumpDockAndSize(dats, 0x21000043u, 0x10000350u, "Vendor popup ListBox");
ElementInfo? vendorBase = LayoutImporter.ImportInfos(dats, 0x21000043u, 0x1000034Bu);
if (vendorBase is not null)
{
DumpMenuAttrs3(vendorBase, "vendor base 0x1000034B", new[] { 2u, 3u, 5u, 6u, 7u, 8u, 9u });
foreach (ElementInfo c in vendorBase.Children)
DumpTextStyle(c, $"vendor base child 0x{c.Id:X8}");
}
ElementInfo? vendorRowTemplate = LayoutImporter.ImportInfos(dats, 0x21000043u, 0x10000352u);
if (vendorRowTemplate is not null)
{
DumpTextStyle(vendorRowTemplate, "Vendor row template 0x10000352");
foreach (ElementInfo c in vendorRowTemplate.Children)
DumpTextStyle(c, $"vendor row-template child 0x{c.Id:X8}");
}
}
private static void DumpMenuAttrs3(ElementInfo el, string label, uint[] attrs)
{
Console.WriteLine(
$"[menuprobe3] {label}: type=0x{el.Type:X8} ({el.X},{el.Y} {el.Width}x{el.Height}) "
+ $"edges L={el.Left} T={el.Top} R={el.Right} B={el.Bottom} "
+ $"children=[{string.Join(",", el.Children.ConvertAll(c => $"0x{c.Id:X8}"))}]");
foreach (uint attr in attrs)
{
if (el.TryGetEffectiveProperty(attr, out UiPropertyValue value))
{
string rendered = value.Kind switch
{
UiPropertyKind.Bool => value.BoolValue.ToString(),
UiPropertyKind.DataId or UiPropertyKind.Enum => $"0x{value.UnsignedValue:X8}",
UiPropertyKind.Integer => value.IntegerValue.ToString(),
_ => value.Kind.ToString(),
};
Console.WriteLine($"[menuprobe3] attr[{attr}] kind={value.Kind} value={rendered}");
}
else
{
Console.WriteLine($"[menuprobe3] attr[{attr}] MISSING");
}
}
}
private static void DumpDockAndSize(DatCollection dats, uint layoutId, uint elementId, string label)
{
ElementInfo? el = LayoutImporter.ImportInfos(dats, layoutId, elementId);
if (el is null)
{
Console.WriteLine($"[menuprobe3] {label} 0x{elementId:X8}: FAILED to import");
return;
}
Console.WriteLine(
$"[menuprobe3] {label} 0x{elementId:X8}: type=0x{el.Type:X8} "
+ $"({el.X},{el.Y} {el.Width}x{el.Height}) "
+ $"edges L={el.Left} T={el.Top} R={el.Right} B={el.Bottom} "
+ $"children=[{string.Join(",", el.Children.ConvertAll(c => $"0x{c.Id:X8}"))}]");
}
private static void DumpTextStyle(ElementInfo el, string label)
{
string color = el.FontColor is { } fc
? $"({fc.X:F2},{fc.Y:F2},{fc.Z:F2},{fc.W:F2})"
: "null(default-white)";
Console.WriteLine(
$"[menuprobe3] {label}: type=0x{el.Type:X8} ({el.X},{el.Y} {el.Width}x{el.Height}) "
+ $"hJustify={el.HJustify} vJustify={el.VJustify} fontColor={color} fontDid=0x{el.FontDid:X8} "
+ $"edges L={el.Left} T={el.Top} R={el.Right} B={el.Bottom}");
}
/// #380 (gate 4): exercises the PRODUCTION
/// mechanism against the real
/// DAT and asserts the two resolved captions match the user's own
/// complaint ("Inactive Opacity" / "Active Opacity") — live-mount proof
/// that #380's fix reads the SAME two strings the fixture-driven
/// regression tests (ChatOptionsPageControllerTests) only assume
/// via a fake.
[Fact]
public void ProbeChatOpacityCaptions()
{
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);
bool ok = ChatOptionsDatCaptions.TryRead(
dats, strings,
out ChatOptionsDatCaptions.Caption defaultOpacity,
out ChatOptionsDatCaptions.Caption activeOpacity);
Console.WriteLine($"[probe380] TryRead ok={ok}");
Console.WriteLine($"[probe380] Default: Name='{defaultOpacity.Name}' Tooltip='{defaultOpacity.Tooltip}'");
Console.WriteLine($"[probe380] Active: Name='{activeOpacity.Name}' Tooltip='{activeOpacity.Tooltip}'");
Assert.True(ok);
Assert.Equal("Inactive Opacity", defaultOpacity.Name);
Assert.Equal("Active Opacity", activeOpacity.Name);
Assert.False(string.IsNullOrEmpty(defaultOpacity.Tooltip));
Assert.False(string.IsNullOrEmpty(activeOpacity.Tooltip));
}
/// #372 minor half: the 13 ID_ChatOption_TextFilter_* labels fail
/// to resolve in table 0x23000003 — sweep the plausible tables and key
/// spellings against the live DAT to find their real home.
[Fact]
public void ProbeFilterLabelHome()
{
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);
string[] keys =
{
"ID_ChatOption_TextFilter_Combat",
"ID_ChatOption_TextFilter_Gameplay",
"ID_ChatOptions_TextFilter_Combat", // plural-Options variant
"ID_Chat_TextFilter_Combat",
"ID_TextFilter_Combat",
"TextFilter_Combat",
};
for (uint table = 0x23000001u; table <= 0x2300000Au; table++)
{
foreach (string key in keys)
{
string? hit = strings.Resolve(table, DatStringResolver.ComputeHash(key));
if (hit is not null)
Console.WriteLine($"[probe] table 0x{table:X8} key '{key}' -> '{hit}'");
}
}
// Also: does the KNOWN-good Character-tab key family resolve where
// documented, as a control?
Console.WriteLine(
"[probe] control ID_PlayerOption_AutoTarget in 0x23000003 -> "
+ $"'{strings.Resolve(0x23000003u, DatStringResolver.ComputeHash("ID_PlayerOption_AutoTarget"))}'");
// Exhaustive: sweep EVERY string table in the local dat for the key.
uint targetHash = DatStringResolver.ComputeHash("ID_ChatOption_TextFilter_Combat");
foreach (uint tableId in dats.Local.GetAllIdsOfType())
{
string? hit = strings.Resolve(tableId, targetHash);
if (hit is not null)
Console.WriteLine($"[probe] EXHAUSTIVE hit: table 0x{tableId:X8} -> '{hit}'");
}
Console.WriteLine("[probe] exhaustive sweep complete");
}
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);
}
}