fix(chargen): Campaign CC gate round 1 Batch C — un-consume media-bearing children
Commit 2/3: CLIENT-WIDE blast radius — un-consume media-bearing dat
children on UiText/UiField.
UiText.ConsumesDatChildren (true unless a state authors PassToChildren)
and UiField.ConsumesDatChildren (true, unconditional) used to drop
EVERY dat child at import time, including ones that carry their own
renderable media — retail's UIElement_Text/Field genuinely composites
those as real chrome/controls (frame pieces, linked scrollbars), not
swallowed caption/face art the way a Button's or Meter's children are.
LayoutImporter.BuildWidget gains a new carve-out (mirroring the
existing UiMeter one): when a UiText/UiField's ConsumesDatChildren is
true, build any child whose OWN StateMedia is non-empty (it carries a
real sprite/track) instead of dropping it outright. Purely structural/
property-only children (StateMedia.Count == 0) stay dropped exactly as
before — this is additive, not a relaxation of the PassToChildren gate.
Independently re-derived blast-radius sweep (walks every installed
LayoutDesc via DatCollection.GetAllIdsOfType<LayoutDesc>, new
LayoutImporterMediaBearingChildSweepTests): 37 distinct (layout,
element) pairs — 41 raw tree positions, since a handful of element ids
recur at multiple subtree positions within the same layout — across 15
layouts. Full list:
0x21000005/0x10000011 (x5 tree positions — chat-adjacent template
reused across the layout), 0x21000005/0x1000059A [MAIN GAME UI],
0x21000006/0x10000011, 0x2100000F/0x1000059A,
0x21000038/{0x100003AB,0x100003BA,0x100003C4,0x100003E0,0x100003EC,
0x100003F6,0x100003FA,0x100003FD,0x100003FF,0x10000402,0x10000404,
0x10000405,0x10000409} [character creation],
0x21000043/0x10000362,
0x21000046/0x100003C4, 0x21000047/{0x100003E0,0x100003EC},
0x21000048/{0x100003F6,0x100003FA,0x100003FD},
0x21000049/{0x100003AB,0x100003BA}, 0x2100004A/0x10000409,
0x2100004B/{0x100003FF,0x10000402,0x10000404,0x10000405},
0x2100004C/{0x100002DD,0x100002E5,0x100002E6},
0x2100005B/0x10000011, 0x21000068/0x1000059A,
0x2100006F/0x10000011 [CHAT INPUT].
(This is an independent re-derivation, not a re-statement of the
investigation's earlier "42/14" estimate — the small difference is
expected from measuring with this commit's own criteria.)
New tests: the sweep itself (pins the two flagged landmarks —
MAIN GAME UI 0x21000005/0x1000059A and CHAT INPUT 0x2100006F/
0x10000011 — plus the three chargen boxes), a build-through regression
test confirming those two landmarks' children resolve as real widgets
post-fix, and a chargen-scoped test confirming the eight gold-frame
pieces + linked scrollbar on all three description boxes now resolve
via UiElement.FindDescendant.
Full App suite (Debug and Release, live-DAT): 5304 passed / 0 failed /
3 skipped — ZERO regressions across the whole client, including every
existing chat and main-UI test. Runtime suite: 1735/0, unaffected
(this is an App-layer-only change).
FLAG FOR THE LEAD: automated coverage cannot catch a purely VISUAL
regression (a frame drawing in the wrong place, a scrollbar overlapping
text). Chat and the main game UI both got new dat children rendered for
the first time this commit — schedule the user's own visual check of
both before considering this closed, per the campaign's oracle
discipline.
The scrollbar linkage (wiring the description boxes' UiScrollbar to
actual text scrolling) is NOT done in this commit — the scrollbar
widget now BUILDS, but CharacterCreationHeritagePage/TownPage/
ProfessionPage/SummaryPage do not yet bind its ScalarChanged to
UiText.Scroll. Filed as follow-up (see report).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0591b9a026
commit
5190e16915
3 changed files with 211 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1207,6 +1207,38 @@ public sealed class CharacterCreationLiveDatTests
|
|||
Assert.Contains(summaryHowTo.Children, c => c.Id == 0x100002E7u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign CC gate round 1 Batch C, Commit 2: the eight gold-frame
|
||||
/// pieces and the linked scrollbar — previously dropped outright by
|
||||
/// <c>UiText.ConsumesDatChildren</c> — now build as REAL widgets
|
||||
/// reachable via <see cref="UiElement.FindDescendant"/>, on all three
|
||||
/// chargen description boxes.
|
||||
/// </summary>
|
||||
[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<UiText>(screen.FindElement(boxId));
|
||||
foreach (uint frameChildId in frameChildIds)
|
||||
Assert.NotNull(UiElement.FindDescendant(box, frameChildId));
|
||||
}
|
||||
|
||||
UiText summaryHowTo = Assert.IsType<UiText>(screen.FindElement(0x10000404u));
|
||||
Assert.IsType<UiScrollbar>(UiElement.FindDescendant(summaryHowTo, 0x100002E7u));
|
||||
}
|
||||
|
||||
private static void AssertButton(ImportedLayout layout, uint elementId) =>
|
||||
Assert.IsType<UiButton>(layout.FindElement(elementId));
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign CC gate round 1 Batch C, Commit 2: client-wide blast-radius
|
||||
/// sweep for the <c>UiText</c>/<c>UiField</c> media-bearing-child
|
||||
/// un-consume fix (<see cref="LayoutImporter"/>'s new <c>UiText or
|
||||
/// UiField</c> carve-out). Walks EVERY installed <c>LayoutDesc</c>
|
||||
/// (<c>DatCollection.GetAllIdsOfType<LayoutDesc></c>) and reports
|
||||
/// every Type-12 (<c>UIElement_Text</c>) 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
|
||||
/// <c>Console.WriteLine</c> 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.
|
||||
/// </summary>
|
||||
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<Finding>();
|
||||
foreach (uint layoutId in dats.GetAllIdsOfType<LayoutDesc>().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.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<UiText>(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<Finding> 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue