The remaining code-bearing findings from the round review, F4-F16 minus the doc-only items (batched separately): - F4: three client-wide UiButton corpus sweeps (LabelBox path — exactly the 4 Town buttons, confined to chargen; conflicting custom-selection- pair + standard Normal/Highlight media — zero found, no gate tightening needed; per-state label-color map — 209 matches beyond chargen, confirming AP-222's mechanism has always been broadly active since it shipped generically in DatWidgetFactory). - F5/F6: LayoutImporter's Batch C un-consumed-children carve-out now honors a child's own AuthoredInvisible flag (a narrow honor scoped to exactly that carve-out, not the general #408 client-wide one) — the chat transcript's new-text indicator (0x1000048C) was building as a visible phantom element retail never shows; verified both directions against the gold-frame pieces, which do not author Invisible. - F7: BoundedProcessOutputCapture.AppendLine combines the line text and its trailing newline into one buffer and one file open/write/close instead of two. - F9: corrected a stale comment in RuntimeSettingsTargets — #407 split DisplayModeCatalog's Resolutions/WindowedResolutions in two, so the fullscreen validator's own narrower list is now DELIBERATELY different from the Config dropdown's fuller offering, not the "must match" bug the comment described. - F10: documented (not changed) why the LabelBox path's default 3px inset and the face-relative +4px gap in DatWidgetFactory.BuildButton are deliberately different numbers — neither carries a retail citation, and moving either to match the other would be an unfounded guess on a button that currently works correctly. - F11: Heritage/Profession/Summary/Town description pages now compose DatRichText.Compose's result ONCE inside their already revision-gated Refresh, caching the built line list instead of re-wrapping on every draw call. - F14: documented (not changed) why PrivateEntityViewportRenderer's _animatedIds set carrying a reserved-but-never-drawn backdrop id is harmless — BuildDrawEntities already excludes a null/empty backdrop from the actual draw list, so the id is never looked up. - F16: the Summary preview now uses its own render-id pair (SummaryPreviewRenderId/SummaryPreviewBackdropRenderId, 0xDA11D035/ 0xDA11D036) instead of sharing the Appearance page's (0xDA11D032/0xDA11D034) — confirmed by tracing FixedEntityTextureOwnerLease through TextureCache to CompositeTextureArrayCache's shared owner tracker that both pages' previews share ONE process-wide TextureCache, so sharing render ids was a real cross-page texture-release collision (either page's own re-dress or disposal could release the OTHER page's still-active textures), not a theoretical one. F3's own register bookkeeping (AP-229 addendum) and F12's register/AD header-count corrections land in the docs-only commit alongside F15. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
235 lines
10 KiB
C#
235 lines
10 KiB
C#
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. 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<UiText>(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);
|
|
|
|
/// <summary>
|
|
/// Campaign CC gate round 1 closeout, F5/F6: of the media-bearing
|
|
/// children the Batch C carve-out now builds instead of dropping
|
|
/// (<see cref="MediaBearingChildSweep_EnumeratesEveryAffectedType12Element"/>'s
|
|
/// own set), which ones author dat property <c>0x3B</c> (Invisible)
|
|
/// THEMSELVES — retail would never show them
|
|
/// (<c>UIElement::OnSetAttribute @0x00462d80</c> 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).
|
|
/// </summary>
|
|
[InstalledDatFact]
|
|
public void MediaBearingChildSweep_EnumeratesWhichAffectedChildrenAuthorInvisible()
|
|
{
|
|
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
|
|
|
var invisibleFindings = new List<InvisibleChildFinding>();
|
|
foreach (uint layoutId in dats.GetAllIdsOfType<LayoutDesc>().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<InvisibleChildFinding> 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<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);
|
|
}
|
|
}
|