diff --git a/src/AcDream.App/UI/Layout/LayoutImporter.cs b/src/AcDream.App/UI/Layout/LayoutImporter.cs
index 06d78aa9..215f3093 100644
--- a/src/AcDream.App/UI/Layout/LayoutImporter.cs
+++ b/src/AcDream.App/UI/Layout/LayoutImporter.cs
@@ -164,6 +164,34 @@ public static class LayoutImporter
if (cw is not null) w.AddChild(cw);
}
}
+ else if (w is UiText or UiField)
+ {
+ // Campaign CC gate round 1 Batch C, Commit 2: UiText/UiField's
+ // coarse ConsumesDatChildren=true (UiText outside its
+ // PassToChildren carve-out; UiField unconditionally) used to
+ // drop EVERY dat child, including ones that carry their own
+ // renderable media — retail's UIElement_Text/Field genuinely
+ // composites those as real chrome/controls, not swallowed
+ // caption/face art the way a Button's or Meter's children are.
+ // Live-DAT-measured (chargen's three shared description boxes,
+ // 0x100003e0/0x10000409/0x10000404): the eight gold-frame
+ // pieces (0x100002DE-E3, 0x100000E8/EA, Type 3, one DirectState
+ // sprite each) and the linked scrollbar (0x100002E7, Type 11,
+ // its own DirectState track sprite plus three Button
+ // sub-children BuildScrollbar resolves internally) all carry
+ // non-empty StateMedia on THEMSELVES. Purely structural/
+ // property-only children (StateMedia.Count == 0 — e.g. a
+ // lifted-caption-only child some OTHER element type might
+ // still want swallowed) stay dropped exactly as before; this
+ // is additive, not a relaxation of the PassToChildren gate
+ // itself.
+ foreach (var child in info.Children)
+ {
+ if (child.StateMedia.Count == 0) continue;
+ var cw = BuildWidget(child, resolve, datFont, fontResolve, stringResolve, byId);
+ if (cw is not null) w.AddChild(cw);
+ }
+ }
// UIElement::SetState @ 0x00464E70 propagates a state's id only after the
// child tree exists. Re-applying the imported default here gives retained
diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs
index a9664b8a..e8fc405e 100644
--- a/tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs
@@ -1207,6 +1207,38 @@ public sealed class CharacterCreationLiveDatTests
Assert.Contains(summaryHowTo.Children, c => c.Id == 0x100002E7u);
}
+ ///
+ /// Campaign CC gate round 1 Batch C, Commit 2: the eight gold-frame
+ /// pieces and the linked scrollbar — previously dropped outright by
+ /// UiText.ConsumesDatChildren — now build as REAL widgets
+ /// reachable via , on all three
+ /// chargen description boxes.
+ ///
+ [InstalledDatFact]
+ public void DescriptionTextboxes_FramesAndScrollbarBuildAsRealWidgets()
+ {
+ using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
+ uint layoutId = RetailDataIdResolver.Resolve(
+ dats, CharacterCreationUiController.RootEnum, 5u);
+ ImportedLayout screen = BuildSelected(
+ dats, layoutId, CharacterCreationUiController.RootElementId);
+
+ uint[] frameChildIds =
+ [
+ 0x100002DEu, 0x100002DFu, 0x100002E0u, 0x100002E1u,
+ 0x100000E8u, 0x100002E2u, 0x100002E3u, 0x100000EAu,
+ ];
+ foreach (uint boxId in new[] { 0x100003E0u, 0x10000409u, 0x10000404u })
+ {
+ UiText box = Assert.IsType(screen.FindElement(boxId));
+ foreach (uint frameChildId in frameChildIds)
+ Assert.NotNull(UiElement.FindDescendant(box, frameChildId));
+ }
+
+ UiText summaryHowTo = Assert.IsType(screen.FindElement(0x10000404u));
+ Assert.IsType(UiElement.FindDescendant(summaryHowTo, 0x100002E7u));
+ }
+
private static void AssertButton(ImportedLayout layout, uint elementId) =>
Assert.IsType(layout.FindElement(elementId));
diff --git a/tests/AcDream.App.Tests/UI/Layout/LayoutImporterMediaBearingChildSweepTests.cs b/tests/AcDream.App.Tests/UI/Layout/LayoutImporterMediaBearingChildSweepTests.cs
new file mode 100644
index 00000000..97be363d
--- /dev/null
+++ b/tests/AcDream.App.Tests/UI/Layout/LayoutImporterMediaBearingChildSweepTests.cs
@@ -0,0 +1,151 @@
+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.
+ AssertChildrenBuild(
+ dats, layoutId: 0x21000005u, elementId: 0x1000059Au,
+ expectedChildIds:
+ [
+ 0x100002DEu, 0x100002DFu, 0x100002E0u, 0x100002E1u,
+ 0x100000E8u, 0x100002E2u, 0x100002E3u, 0x100000EAu,
+ ]);
+
+ // CHAT INPUT (0x2100006F/0x10000011): a single media-bearing child.
+ AssertChildrenBuild(
+ dats, layoutId: 0x2100006Fu, elementId: 0x10000011u,
+ expectedChildIds: [0x1000048Cu]);
+ }
+
+ private static void AssertChildrenBuild(
+ IDatReaderWriter dats, uint layoutId, uint elementId, uint[] expectedChildIds)
+ {
+ 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)
+ {
+ Assert.NotNull(UiElement.FindDescendant(built, childId));
+ }
+ }
+
+ 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 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);
+ }
+}