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 Batch C, Commit 2: client-wide blast-radius /// sweep for the UiText/UiField media-bearing-child /// un-consume fix ('s new UiText or /// UiField carve-out). Walks EVERY installed LayoutDesc /// (DatCollection.GetAllIdsOfType<LayoutDesc>) and reports /// every Type-12 (UIElement_Text) element that does NOT author /// PassToChildren on any state (the pre-fix "consumes everything" shape) /// but HAS at least one direct child carrying its own state media — the /// exact set the fix now builds instead of silently dropping. Logged via /// Console.WriteLine so the full enumeration is visible in test /// output for the commit message; the assertions pin only landmark counts/ /// elements (not a brittle exact global total) so the gate survives a /// future DAT revision without going red on an unrelated content change. /// public sealed class LayoutImporterMediaBearingChildSweepTests { 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 Finding(uint LayoutId, uint ElementId, uint[] MediaBearingChildIds); [InstalledDatFact] public void MediaBearingChildSweep_EnumeratesEveryAffectedType12Element() { 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; Walk(layoutId, tree, findings); } Console.WriteLine($"[SWEEP] {findings.Count} affected Type-12 elements across " + $"{findings.Select(f => f.LayoutId).Distinct().Count()} layouts."); foreach (Finding f in findings.OrderBy(f => f.LayoutId).ThenBy(f => f.ElementId)) { Console.WriteLine( $"[SWEEP] layout=0x{f.LayoutId:X8} element=0x{f.ElementId:X8} " + $"mediaBearingChildren=[{string.Join(",", f.MediaBearingChildIds.Select(id => $"0x{id:X8}"))}]"); } // Landmarks the investigation specifically flagged for the user's // visual check — assert they are genuinely in the affected set // (not asserting a brittle exact global count). Assert.Contains(findings, f => f.LayoutId == 0x21000005u && f.ElementId == 0x1000059Au); Assert.Contains(findings, f => f.LayoutId == 0x2100006Fu && f.ElementId == 0x10000011u); // The three chargen description boxes this campaign already fixed. Assert.Contains(findings, f => f.ElementId == 0x100003E0u); // Profession Assert.Contains(findings, f => f.ElementId == 0x10000409u); // Town Assert.Contains(findings, f => f.ElementId == 0x10000404u); // Summary how-to Assert.True(findings.Count > 0, "the sweep must find at least the known chargen landmarks."); } /// /// Regression pin (Commit 2): the two landmarks the investigation /// flagged for the user's own visual check actually BUILD their /// media-bearing children as real widgets now, on a NON-chargen /// layout — proving the fix is not accidentally chargen-only. FLAG: /// this is a structural/widget-level pin only; the user's own visual /// check of chat + the main game UI is still owed (the lead schedules /// it) — a passing test here does not stand in for that. /// [InstalledDatFact] public void MainGameUiAndChatInput_MediaBearingChildrenNowBuildAsRealWidgets() { using var dats = new DatCollection(DatDirectory, DatAccessType.Read); // MAIN GAME UI (0x21000005/0x1000059A): the same eight gold-frame // pieces the chargen boxes carry. F5/F6 closeout: NONE of these // author Invisible, so all eight must build VISIBLE — the "other // direction" the reviewer named, pinned here alongside the // enumeration sweep's own DoesNotContain assertions. AssertChildrenBuild( dats, layoutId: 0x21000005u, elementId: 0x1000059Au, expectedChildIds: [ 0x100002DEu, 0x100002DFu, 0x100002E0u, 0x100002E1u, 0x100000E8u, 0x100002E2u, 0x100002E3u, 0x100000EAu, ], expectedInvisible: []); // Chat transcript (0x2100006F/0x10000011): a single media-bearing // child. F5/F6 closeout: 0x1000048C (the new-text indicator) // authors Invisible=true — must build HIDDEN, not as a phantom // visible element. AssertChildrenBuild( dats, layoutId: 0x2100006Fu, elementId: 0x10000011u, expectedChildIds: [0x1000048Cu], expectedInvisible: [0x1000048Cu]); } private static void AssertChildrenBuild( IDatReaderWriter dats, uint layoutId, uint elementId, uint[] expectedChildIds, uint[] expectedInvisible) { ElementInfo? tree = LayoutImporter.ImportInfos(dats, layoutId); Assert.NotNull(tree); ElementInfo? target = FindInfo(tree!, elementId); Assert.NotNull(target); UiElement built = LayoutImporter.Build( target!, _ => (0u, 0, 0), null).Root; Assert.IsType(built); foreach (uint childId in expectedChildIds) { UiElement? child = UiElement.FindDescendant(built, childId); Assert.NotNull(child); bool shouldBeInvisible = expectedInvisible.Contains(childId); Assert.Equal(!shouldBeInvisible, child!.Visible); } } private static ElementInfo? FindInfo(ElementInfo node, uint id) { if (node.Id == id) return node; foreach (ElementInfo child in node.Children) { ElementInfo? found = FindInfo(child, id); if (found is not null) return found; } return null; } private readonly record struct InvisibleChildFinding(uint LayoutId, uint ParentElementId, uint ChildElementId); /// /// Campaign CC gate round 1 closeout, F5/F6: of the media-bearing /// children the Batch C carve-out now builds instead of dropping /// ('s /// own set), which ones author dat property 0x3B (Invisible) /// THEMSELVES — retail would never show them /// (UIElement::OnSetAttribute @0x00462d80 case 8), so building /// them unconditionally as a visible widget is a regression the /// carve-out's own commit didn't check for. Confirms the chargen /// gold-frame pieces are NOT among them (the other direction the /// reviewer named — Batch A's chargen-scoped hide walk must not eat /// them either way, but this proves the DATA itself never marks them /// invisible, independent of which honor mechanism runs). /// [InstalledDatFact] public void MediaBearingChildSweep_EnumeratesWhichAffectedChildrenAuthorInvisible() { using var dats = new DatCollection(DatDirectory, DatAccessType.Read); var invisibleFindings = new List(); foreach (uint layoutId in dats.GetAllIdsOfType().OrderBy(x => x)) { ElementInfo? tree = LayoutImporter.ImportInfos(dats, layoutId); if (tree is null) continue; WalkForInvisibleMediaBearingChildren(layoutId, tree, invisibleFindings); } Console.WriteLine($"[SWEEP-INV] {invisibleFindings.Count} media-bearing children of the " + "Batch C carve-out author Invisible=true themselves."); foreach (InvisibleChildFinding f in invisibleFindings) { Console.WriteLine($"[SWEEP-INV] layout=0x{f.LayoutId:X8} parent=0x{f.ParentElementId:X8} " + $"child=0x{f.ChildElementId:X8}"); } // The chargen gold-frame pieces (GF-12) must NOT author Invisible — // otherwise a narrow per-child honor would eat them, undoing that // fix. Checked directly against the data, independent of whichever // honor mechanism runs. uint[] goldFramePieceIds = [ 0x100002DEu, 0x100002DFu, 0x100002E0u, 0x100002E1u, 0x100000E8u, 0x100002E2u, 0x100002E3u, 0x100000EAu, ]; foreach (uint pieceId in goldFramePieceIds) { Assert.DoesNotContain(invisibleFindings, f => f.ChildElementId == pieceId); } } private static void WalkForInvisibleMediaBearingChildren( uint layoutId, ElementInfo node, List findings) { if (node.Type == 12u) { bool passToChildren = node.States.Values.Any(static s => s.PassToChildren); if (!passToChildren) { foreach (ElementInfo child in node.Children) { if (child.StateMedia.Count > 0 && child.Invisible) findings.Add(new InvisibleChildFinding(layoutId, node.Id, child.Id)); } } } foreach (ElementInfo child in node.Children) WalkForInvisibleMediaBearingChildren(layoutId, child, findings); } private static void Walk(uint layoutId, ElementInfo node, List findings) { if (node.Type == 12u) { bool passToChildren = node.States.Values.Any(static s => s.PassToChildren); if (!passToChildren) { uint[] mediaBearingChildren = node.Children .Where(static c => c.StateMedia.Count > 0) .Select(static c => c.Id) .ToArray(); if (mediaBearingChildren.Length > 0) findings.Add(new Finding(layoutId, node.Id, mediaBearingChildren)); } } foreach (ElementInfo child in node.Children) Walk(layoutId, child, findings); } }