The 13 ID_ChatOption_TextFilter_* labels (and their _Desc tooltips) live
in string table 0x2300000D, not the 0x23000003 options table the section
headers and slider labels use. Dat-verified: the targeted sweep missed
(0x23000001-0A), the control key resolved ('Auto Target' — machinery
fine), and the exhaustive all-tables sweep (ProbeFilterLabelHome, now a
permanent env-gated probe) hit exactly once: 0x2300000D -> 'Combat'. The
initializer decomp confirms the hash KEYS are the symbol names verbatim
(the vftable-member operands at 0x006f04cd are the known pooled-string
artifact); only the research doc's table attribution was inferred rather
than dat-verified — corrected in §8.
All 13 rows now render their captions instead of the honest-fallback
blanks the first connected gate saw. Full Release suite green (one
Core.Net loss-simulation timing flake on the first run, green targeted
and on rerun).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
151 lines
6.7 KiB
C#
151 lines
6.7 KiB
C#
using System.IO;
|
|
using AcDream.App.UI;
|
|
using AcDream.App.UI.Layout;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.Options;
|
|
|
|
namespace AcDream.App.Tests.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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}");
|
|
}
|
|
}
|
|
|
|
/// <summary>#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.</summary>
|
|
[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<DatReaderWriter.DBObjs.StringTable>())
|
|
{
|
|
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);
|
|
}
|
|
}
|