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 (0x10000016gmSpewBoxUI::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 0x10000049GetChildRecursive 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() ?? "?", }; }