using System;
using System.Collections.Generic;
using System.Linq;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
using DatReaderWriter.Types;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.App.Tests.UI;
///
/// Campaign CH slice CH2, task C.7: a one-shot diagnostic sweep of every
/// installed for an element of retail's SpewBox
/// class (0x10000016 — gmSpewBoxUI::GetUIElementType @0x004D5AA0).
/// Not a gate/conformance test — just a discovery tool, per the research
/// doc's §8.1/§8.2 open question. Skips (does not fail) when the installed
/// DAT directory is unavailable, matching every other
/// ContentConformanceDats.ResolveDatDir()-gated diagnostic in this
/// tree.
///
///
/// RESULT (2026-08-09, run against the installed client_portal.dat),
/// WORDING CORRECTED at the CH2 re-review nits pass
/// (docs/plans/2026-08-09-chat-parity-campaign.md, nit 6): this
/// pass's id source, allIds below, is
/// dats.GetAllIdsOfType<LayoutDesc>() — the top-level AGGREGATE
/// accessor — NOT dats.Portal.GetAllIdsOfType<LayoutDesc>(),
/// which the very next line prints and which reports a count of
/// ZERO. The loop below then queries those aggregate ids against
/// dats.Portal.TryGet, a database whose own id enumeration for this
/// type is empty — so the original "the sweep is therefore EXHAUSTIVE...
/// finds ZERO elements... anywhere" conclusion never actually established
/// anything about dats.Portal: it iterated ids sourced from
/// elsewhere against a database that cannot be enumerated the same way.
/// The id-space/sanity-check numbers below (CLOSED range
/// 0x21000000-0x21000075, 118 possible ids, 101 populated,
/// 3 known ids present) describe THAT aggregate list, not Portal's own.
/// What IS established — from the separate dats.Local sweep, see
/// RESULT 2 below — is that client_local_English.dat hosts the
/// SpewBox layout at LayoutDesc 0x21000011. Whether
/// dats.Portal ALSO carries a 0x10000016 element remains
/// UNESTABLISHED; this sweep did not meaningfully search it. LayoutDesc
/// 0x10000012 — the id the decompiled
/// CreateChildElementByEnum(null, 0x10000012, 0x1000004A) call
/// appears to reference — exists but has zero top-level elements (it is
/// not the per-line template catalog; that id must resolve through a
/// different mechanism than a direct dats.Get<LayoutDesc> hit,
/// which this slice did not crack).
///
///
///
/// RESULT 2, CH2 REJECT-review rework NIT 3
/// (2026-08-09, docs/research/2026-08-09-ch2-review-findings.md): the
/// FIRST result above only ever swept dats.Portal. Extending the
/// IDENTICAL sweep to dats.Local (client_local_English.dat)
/// FINDS it: LayoutDesc 0x21000011, element 0x10000048
/// (class 0x10000016), position (0,0) relative to its parent,
/// size 450×72, one child — ListBox 0x10000049 (matching
/// gmSpewBoxUI::PostInit's GetChildRecursive(0x10000049) call
/// verbatim, though its own widget-class Type is 0x00000005,
/// NOT the id 0x10000049 — GetChildRecursive searches by
/// ElementId, not Type) — carrying MaxConcurrentItems
/// (property 0x10000028) = 4. gmSpewBoxUI IS
/// dat-authored after all; the earlier "mounted directly from C++ code...
/// not resolved from any authored LayoutDesc tree" conclusion was an
/// artifact of only having checked one of the two locale-bearing dats.
/// Extent and MaxConcurrentItems are now AUTHORED, not invented —
/// see and
/// SpewBoxController's own doc comments, and register row AP-178.
/// Absolute screen position (the element's parent, hence its true screen
/// offset, is still unidentified) and colour (no colour property surfaced
/// in this element's direct-state dump; the per-UIStateId
/// States dictionary was not walked) remain open.
///
///
public sealed class SpewBoxLayoutDumpDiagnostic
{
private readonly ITestOutputHelper _out;
public SpewBoxLayoutDumpDiagnostic(ITestOutputHelper output) => _out = output;
private const uint SpewBoxElementClass = 0x10000016u;
private const uint ListBoxElementClass = 0x10000049u;
private const uint LineTemplateLayoutEnum = 0x10000012u;
private const uint LineTemplateElementId = 0x1000004Au;
private const uint ListBoxMaxItemsProperty = 0x10000028u;
private static string? ResolveDatDir()
{
var fromEnv = System.Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(fromEnv) && System.IO.Directory.Exists(fromEnv))
return fromEnv;
var def = System.IO.Path.Combine(
System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
return System.IO.Directory.Exists(def) ? def : null;
}
[Fact]
public void SweepInstalledLayoutDescs_ForSpewBoxElementClass()
{
string? datDir = ResolveDatDir();
if (datDir is null)
{
_out.WriteLine("SKIP: installed retail DAT directory is unavailable.");
return;
}
using var dats = new DatCollection(datDir, DatAccessType.Read);
int scanned = 0;
var hits = new List<(uint LayoutId, ElementDesc Element)>();
var allIds = dats.GetAllIdsOfType().ToList();
_out.WriteLine($"Portal.GetAllIdsOfType count: {dats.Portal.GetAllIdsOfType().Count()}");
_out.WriteLine($"HighRes.GetAllIdsOfType count: {dats.HighRes.GetAllIdsOfType().Count()}");
_out.WriteLine($"Contains known char-window 0x2100002E: {allIds.Contains(0x2100002Eu)}");
_out.WriteLine($"Contains known inventory 0x21000023: {allIds.Contains(0x21000023u)}");
_out.WriteLine($"Contains known toolbar 0x21000016: {allIds.Contains(0x21000016u)}");
_out.WriteLine($"Min id: 0x{allIds.Min():X8} Max id: 0x{allIds.Max():X8}");
foreach (uint layoutId in allIds)
{
scanned++;
if (!dats.Portal.TryGet(layoutId, out LayoutDesc? ld) || ld is null)
continue;
foreach (var kv in ld.Elements)
{
var found = FindByType(kv.Value, SpewBoxElementClass);
if (found is not null)
hits.Add((layoutId, found));
}
}
_out.WriteLine($"Scanned {scanned} installed LayoutDescs.");
_out.WriteLine($"Elements of class 0x{SpewBoxElementClass:X8} (gmSpewBoxUI): {hits.Count}");
foreach (var (layoutId, element) in hits)
{
_out.WriteLine(
$" LayoutDesc 0x{layoutId:X8} -> element 0x{element.ElementId:X8} "
+ $"pos=({element.X},{element.Y}) size=({element.Width}x{element.Height}) "
+ $"zLevel={element.ZLevel} readOrder={element.ReadOrder} "
+ $"children={element.Children.Count}");
var listBox = FindByType(element, ListBoxElementClass);
if (listBox is not null)
{
_out.WriteLine(
$" ListBox child 0x{listBox.ElementId:X8} "
+ $"pos=({listBox.X},{listBox.Y}) size=({listBox.Width}x{listBox.Height})");
DumpProperties(listBox, " ", _out.WriteLine);
}
else
{
_out.WriteLine(" (no ListBox child of class 0x10000049 found)");
}
}
// Also probe the presumed shared line-template catalog directly —
// gmSpewBoxUI::Update's CreateChildElementByEnum(null, 0x10000012,
// 0x1000004A) call implies LayoutDesc 0x10000012 hosts a template
// catalog with a top-level entry 0x1000004A.
if (dats.Portal.TryGet(LineTemplateLayoutEnum, out LayoutDesc? templateLd)
&& templateLd is not null)
{
_out.WriteLine($"LayoutDesc 0x{LineTemplateLayoutEnum:X8} exists ({templateLd.Elements.Count} top-level elements).");
if (templateLd.Elements.TryGetValue(LineTemplateElementId, out ElementDesc? lineTemplate))
{
_out.WriteLine(
$" Line template 0x{LineTemplateElementId:X8}: type=0x{lineTemplate.Type:X8} "
+ $"pos=({lineTemplate.X},{lineTemplate.Y}) size=({lineTemplate.Width}x{lineTemplate.Height})");
DumpProperties(lineTemplate, " ", _out.WriteLine);
}
else
{
_out.WriteLine($" No top-level element 0x{LineTemplateElementId:X8} in that LayoutDesc.");
}
}
else
{
_out.WriteLine($"LayoutDesc 0x{LineTemplateLayoutEnum:X8} does not exist in the installed DAT.");
}
// CH2 REJECT-review rework, NIT 3
// (docs/research/2026-08-09-ch2-review-findings.md): the original
// sweep above never consulted dats.Local (client_local_English.dat
// — research doc §8.1 names this file, not client_portal.dat, as
// the one that might carry locale-specific UI text/layout
// resources). Repeat the exact same class-0x10000016 sweep against
// Local's own LayoutDesc id space before trusting AP-178's
// "not dat-authored, no LayoutDesc dump can recover this" wording.
var localHits = new List<(uint LayoutId, ElementDesc Element)>();
int localScanned = 0;
List localIds = dats.Local.GetAllIdsOfType().ToList();
_out.WriteLine($"Local.GetAllIdsOfType count: {localIds.Count}");
if (localIds.Count > 0)
_out.WriteLine($"Local min id: 0x{localIds.Min():X8} Local max id: 0x{localIds.Max():X8}");
foreach (uint layoutId in localIds)
{
localScanned++;
if (!dats.Local.TryGet(layoutId, out LayoutDesc? ld) || ld is null)
continue;
foreach (var kv in ld.Elements)
{
var found = FindByType(kv.Value, SpewBoxElementClass);
if (found is not null)
localHits.Add((layoutId, found));
}
}
_out.WriteLine($"Scanned {localScanned} dats.Local LayoutDescs.");
_out.WriteLine(
$"dats.Local elements of class 0x{SpewBoxElementClass:X8} (gmSpewBoxUI): {localHits.Count}");
foreach (var (layoutId, element) in localHits)
{
_out.WriteLine(
$" [Local] LayoutDesc 0x{layoutId:X8} -> element 0x{element.ElementId:X8} "
+ $"pos=({element.X},{element.Y}) size=({element.Width}x{element.Height}) "
+ $"zLevel={element.ZLevel} readOrder={element.ReadOrder} "
+ $"leftEdge={element.LeftEdge} topEdge={element.TopEdge} "
+ $"rightEdge={element.RightEdge} bottomEdge={element.BottomEdge} "
+ $"baseElement=0x{element.BaseElement:X8} baseLayoutId=0x{element.BaseLayoutId:X8} "
+ $"children={element.Children.Count}");
DumpProperties(element, " ", _out.WriteLine);
foreach (var (childId, child) in element.Children)
{
_out.WriteLine(
$" child 0x{childId:X8}: type=0x{child.Type:X8} "
+ $"pos=({child.X},{child.Y}) size=({child.Width}x{child.Height})");
DumpProperties(child, " ", _out.WriteLine);
}
// NIT 3 finding: gmSpewBoxUI::PostInit's GetChildRecursive(0x10000049)
// searches by ELEMENT ID, not by the widget CLASS (Type) —
// ListBoxElementClass below was the wrong axis to search on
// (it collided with the decomp's 0x10000049 constant, which is
// actually this instance's authored ElementId; the ListBox
// widget's own Type turned out to be 0x00000005). Look the
// child up directly by the ElementId the decomp names.
if (element.Children.TryGetValue(ListBoxElementClass, out ElementDesc? listBoxById))
{
_out.WriteLine(
$" [Local] ListBox-by-ElementId 0x{listBoxById.ElementId:X8} "
+ $"(Type=0x{listBoxById.Type:X8}) "
+ $"pos=({listBoxById.X},{listBoxById.Y}) size=({listBoxById.Width}x{listBoxById.Height})");
}
var listBox = FindByType(element, ListBoxElementClass);
if (listBox is not null)
{
_out.WriteLine(
$" [Local] ListBox-by-Type child 0x{listBox.ElementId:X8} "
+ $"pos=({listBox.X},{listBox.Y}) size=({listBox.Width}x{listBox.Height})");
DumpProperties(listBox, " ", _out.WriteLine);
}
}
if (dats.Local.TryGet(LineTemplateLayoutEnum, out LayoutDesc? localTemplateLd)
&& localTemplateLd is not null)
{
_out.WriteLine(
$"[Local] LayoutDesc 0x{LineTemplateLayoutEnum:X8} exists "
+ $"({localTemplateLd.Elements.Count} top-level elements).");
}
else
{
_out.WriteLine($"[Local] LayoutDesc 0x{LineTemplateLayoutEnum:X8} does not exist.");
}
// Informational only — this is a discovery sweep, not a pass/fail gate.
// The findings are transcribed into WeenieErrorMessages/SpewBoxController
// doc comments and the divergence register by hand after reading this
// output; there is nothing here worth asserting on.
}
private static ElementDesc? FindByType(ElementDesc d, uint type)
{
if (d.Type == type)
return d;
foreach (var kv in d.Children)
{
var found = FindByType(kv.Value, type);
if (found is not null)
return found;
}
return null;
}
private static void DumpProperties(ElementDesc d, string indent, Action write)
{
// NIT 3 fix (docs/research/2026-08-09-ch2-review-findings.md): this
// used to write to Console.WriteLine unconditionally, which xUnit's
// "Standard Output Messages" capture does NOT show — a latent bug
// that only mattered once a hit with an actual property to dump
// existed (the original Portal-only sweep found none). Routed
// through the caller's ITestOutputHelper.WriteLine so a future run
// actually surfaces this.
if (d.StateDesc?.Properties is null)
{
write($"{indent}(no direct-state properties)");
return;
}
foreach (var (propertyId, property) in d.StateDesc.Properties)
{
write($"{indent}property 0x{propertyId:X8} = {Describe(property)}"
+ (propertyId == ListBoxMaxItemsProperty ? " <-- MaxConcurrentItems" : ""));
}
}
private static string Describe(object property) => property switch
{
DatReaderWriter.Types.EnumBaseProperty e => $"Enum({e.Value})",
DatReaderWriter.Types.DataIdBaseProperty did => $"DataId(0x{did.Value:X8})",
DatReaderWriter.Types.ArrayBaseProperty arr => $"Array[{arr.Value.Count}]({string.Join(", ", arr.Value.Select(Describe))})",
// NIT 3 fix: the original switch had no case for these two —
// Integer is exactly the type MaxConcurrentItems (property
// 0x10000028) uses, so without this case the diagnostic could
// find the property but never print its actual authored value.
DatReaderWriter.Types.IntegerBaseProperty i => $"Integer({i.Value})",
DatReaderWriter.Types.BoolBaseProperty b => $"Bool({b.Value})",
_ => property.ToString() ?? "?",
};
}