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);
}
}