using System.IO; using System.Linq; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Content; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Options; namespace AcDream.App.Tests.UI.Layout; /// /// Campaign CC gate round 1 closeout, F4: three client-wide blast-radius /// sweeps over EVERY installed LayoutDesc, walking every Type-1 /// (UIElement_Button) element and matching the SAME structural /// predicates uses internally /// (predicates re-derived here rather than reflected, since the source /// methods are private — kept in sync by citing the exact source /// line ranges in each sweep's own doc). Same style as /// : logs the full /// enumeration for the commit message, pins landmark counts rather than a /// brittle exact global total. /// public sealed class UiButtonCorpusSweepTests { private static string DatDirectory => System.Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR") ?? Path.Combine( System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); private readonly record struct ButtonFinding(uint LayoutId, uint ElementId); /// /// Sweep (a): which buttons take the GF-11c LabelBox path — /// info.StateMedia.Count==0 (no media on the button itself) with /// EXACTLY one stateful face child, plus a DISTINCT lifted Type-12 /// caption child (not the button's own P0x17) — see /// DatWidgetFactory.BuildButton:889-914 for the exact shape this /// mirrors. /// [InstalledDatFact] public void LabelBoxPath_EnumeratesEveryMatchingButton() { using var dats = new DatCollection(DatDirectory, DatAccessType.Read); var findings = new List(); foreach (uint layoutId in dats.GetAllIdsOfType().OrderBy(x => x)) { ElementInfo? tree = LayoutImporter.ImportInfos(dats, layoutId); if (tree is null) continue; WalkButtons(layoutId, tree, findings, MatchesLabelBoxShape); } Console.WriteLine($"[SWEEP-A] {findings.Count} buttons take the LabelBox path across " + $"{findings.Select(f => f.LayoutId).Distinct().Count()} layouts."); foreach (ButtonFinding f in findings.OrderBy(f => f.LayoutId).ThenBy(f => f.ElementId)) Console.WriteLine($"[SWEEP-A] layout=0x{f.LayoutId:X8} element=0x{f.ElementId:X8}"); // Landmark this campaign already fixed and gated (GF-11c, the Town // page's per-marker name label) must be in the set — proves the // sweep's predicate is right, not just non-empty. Assert.Contains(findings, f => IsTownButton(f.ElementId)); } /// /// Sweep (b): any button authoring BOTH the custom Unselected/Selected /// radio-pair (UiButton's _hasCustomSelectionPair bypass, /// GF-1/GF-8) AND standard Normal/Highlight media — the custom-pair /// bypass would eat the standard state machine for such a button /// (UiButton.UpdateVisualState's if (_hasCustomSelectionPair) /// branch runs UNCONDITIONALLY when the pair is present, never falling /// through to the standard _availableStates branch). None found /// in the installed corpus at the ELEMENT's own media level (this sweep /// does not additionally check face-SEGMENT media — see this method's /// own note). /// [InstalledDatFact] public void CustomSelectionPair_NeverCoexistsWithStandardNormalHighlightMedia() { using var dats = new DatCollection(DatDirectory, DatAccessType.Read); var findings = new List(); foreach (uint layoutId in dats.GetAllIdsOfType().OrderBy(x => x)) { ElementInfo? tree = LayoutImporter.ImportInfos(dats, layoutId); if (tree is null) continue; WalkButtons(layoutId, tree, findings, MatchesConflictingPairShape); } Console.WriteLine($"[SWEEP-B] {findings.Count} buttons author BOTH the custom " + "Unselected/Selected pair AND standard Normal/Highlight media."); foreach (ButtonFinding f in findings) Console.WriteLine($"[SWEEP-B] layout=0x{f.LayoutId:X8} element=0x{f.ElementId:X8}"); // No conflict exists in the installed corpus today — the // `_hasCustomSelectionPair` bypass in `UiButton.UpdateVisualState` // is safe as-is (unconditional-when-present) without needing a // tighter gate. If a future DAT drop introduces one, this test // fails here rather than silently regressing that button's // Highlight/rollover feedback. Assert.Empty(findings); } /// /// Sweep (c): any button with a genuine per-state label-color/outline /// map (AP-222's mechanism, ElementReader.BuildPerStateColorMap/ /// BuildPerStateBoolMap against dat properties 0x1B/ /// 0x21 — non-null only when the authored dat carries MORE THAN /// ONE distinct value across states) beyond the chargen Appearance /// spins and Town buttons this campaign already ported and gated. /// [InstalledDatFact] public void PerStateLabelColorMap_EnumeratesEveryButtonBeyondChargen() { using var dats = new DatCollection(DatDirectory, DatAccessType.Read); var findings = new List(); foreach (uint layoutId in dats.GetAllIdsOfType().OrderBy(x => x)) { ElementInfo? tree = LayoutImporter.ImportInfos(dats, layoutId); if (tree is null) continue; WalkButtons(layoutId, tree, findings, MatchesPerStateLabelStyleShape); } Console.WriteLine($"[SWEEP-C] {findings.Count} buttons carry a genuine per-state " + "label color/outline map."); foreach (ButtonFinding f in findings.OrderBy(f => f.LayoutId).ThenBy(f => f.ElementId)) Console.WriteLine($"[SWEEP-C] layout=0x{f.LayoutId:X8} element=0x{f.ElementId:X8}"); // Landmarks this campaign already ported: the nine Appearance spins // (Hair/Eyes/Skin/Headgear/Shirt/Trousers/Footwear/Nose/Mouth, all // sharing one Highlight-gold-brightening state pair) and the four // Town buttons (Normal-gold -> Selected-white caption swap). Assert.Contains(findings, f => f.ElementId == CharacterCreationAppearancePage.HairSpinId); Assert.Contains(findings, f => IsTownButton(f.ElementId)); } /// Town page's four starting-area button ids /// (CharacterCreationTownPage's own private /// StartAreaByButtonId keys — no public constants exist there, /// so the literals are duplicated here). private static bool IsTownButton(uint elementId) => elementId is 0x1000040Bu or 0x1000040Du or 0x1000040Eu or 0x1000040Fu; // ── Shared predicates (re-derived from DatWidgetFactory.BuildButton) ── private static bool MatchesLabelBoxShape(ElementInfo info) { if (info.StateMedia.Count != 0) return false; ElementInfo[] faces = FindStatefulFaceChildren(info); if (faces.Length != 1) return false; // A DISTINCT lifted Type-12 caption child (not the button's own // P0x17) — DatWidgetFactory.BuildButton's own "label is null on the // button itself, found on a Type-12 child instead" fallback. bool ownCaption = HasStringInfoProperty(info); if (ownCaption) return false; return info.Children.Any(child => child.Type == 12u && HasStringInfoProperty(child)); } private static bool MatchesConflictingPairShape(ElementInfo info) { bool hasCustomPair = info.StateMedia.ContainsKey("Unselected") && info.StateMedia.ContainsKey("Selected"); bool hasStandardPair = info.StateMedia.ContainsKey("Normal") || info.StateMedia.ContainsKey("Highlight"); return hasCustomPair && hasStandardPair; } private static bool MatchesPerStateLabelStyleShape(ElementInfo info) { // Mirror BuildButton's labelInfo resolution: the button's own P0x17 // if present, else the first Type-12 child with a resolvable one. ElementInfo labelInfo = HasStringInfoProperty(info) ? info : info.Children.FirstOrDefault(child => child.Type == 12u && HasStringInfoProperty(child)) ?? info; return ElementReader.BuildPerStateColorMap(labelInfo, 0x1Bu) is not null || ElementReader.BuildPerStateBoolMap(labelInfo, 0x21u) is not null; } private static bool HasStringInfoProperty(ElementInfo info) => info.TryGetEffectiveProperty(0x17u, out UiPropertyValue property) && property.Kind == UiPropertyKind.StringInfo; /// Verbatim copy of DatWidgetFactory.FindStatefulFaceChildren /// (private there) — a child whose media state names intersect the /// PARENT's own declared state names. private static ElementInfo[] FindStatefulFaceChildren(ElementInfo info) => [.. info.Children .Where(child => child.StateMedia.Count != 0 && child.StateMedia.Keys.Any(childState => info.States.Values.Any(parentState => string.Equals(parentState.Name, childState, StringComparison.Ordinal)))) .OrderBy(child => child.ReadOrder)]; private static void WalkButtons( uint layoutId, ElementInfo node, List findings, Func predicate) { if (node.Type == 1u && predicate(node)) findings.Add(new ButtonFinding(layoutId, node.Id)); foreach (ElementInfo child in node.Children) WalkButtons(layoutId, child, findings, predicate); } }