test(chargen): Campaign CC gate round 1 re-test 2 — R3-8 name-field exhaustive dump + closeout docs

R3-8: dumped EVERY property present on 0x10000402 (not just P0x17) across
every state, cross-referenced against UIElement_Text::OnSetAttribute's
complete case list (no UIElement_TextInput class exists in retail — the
name field is a plain UIElement_Text/m_filter-bearing field). The full
recognized-property space has no placeholder/prompt mechanism independent
of P0x17. The BaseElement/prototype-inheritance hypothesis is also ruled
out — the existing regression test already probes the fully-merged
ElementInfo (post BaseElement resolution) and finds nothing. The only
StringInfo-kind property present, 0x49, resolves to "Your name can be 32
characters long and cannot contain numbers or symbols." — but 0x49 is
part of the same five-property tooltip family ISSUES #409/GF-16 already
document client-wide (0x48's own DID, 0x21000041, is the EXACT tooltip
popup LayoutDesc #409 cites) — a hover tooltip, not an in-field
placeholder. No code change, per this batch's own "do not invent a
placeholder" contract — third independent negative result on this
question via three different mechanisms. The lead should request a live
retail screenshot before any further investigation.

Also carries the shared live-DAT regression suite for R3-1 through R3-7
(CharacterCreationLiveDatTests.cs holds tests spanning multiple findings
in one file, so they land together) and the RE-TEST 2 findings-doc
closeout writeup for all eight items.

App suite live-DAT env 5358/3 -> 5372/3 (+14, zero regressions). Runtime
1735/0 unchanged (untouched this round). Full solution: 14578 tests / 4
skips / 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-16 17:20:42 +02:00
parent 2886f79f37
commit 91f84dec9f
2 changed files with 562 additions and 1 deletions

View file

@ -944,6 +944,29 @@ public sealed class CharacterCreationLiveDatTests
$"the name field's state 0x{stateId:X} ('{state.Name}') must not "
+ "author a P0x17 caption either.");
}
// R3-8 (re-test 2, third assertion): EXHAUSTIVE — every property
// this element authors, cross-referenced against
// UIElement_Text::OnSetAttribute's COMPLETE case list (every
// property id that class recognizes at all: 0x14-0x29 plus the
// sparse high ids 0xC7/0xCB/0xCC), not just P0x17. The ONLY
// StringInfo-kind property present is 0x49 — part of the tooltip
// family (0x47 TooltipBehavior/0x48 the tooltip popup LayoutDesc
// DID 0x21000041/0x49 the tooltip TEXT/0x4B TooltipEnabled — the
// SAME five-property family ISSUES.md #409/GF-16 already
// documents client-wide) and resolves to "Your name can be 32
// characters long and cannot contain numbers or symbols." — a
// HOVER TOOLTIP, not an in-field placeholder; #409's tooltip
// system is unshipped, so this text is authored but never shown
// anywhere yet. No other property on this element (or its 8 gold-
// frame children, the SAME 0x100002DE-E3/0x100000E8/0xEA family
// GF-12 already renders) carries any string content.
Assert.True(nameField.TryGetEffectiveProperty(0x49u, out var tooltip));
Assert.Equal(UiPropertyKind.StringInfo, tooltip.Kind);
var strings = new DatStringResolver(dats);
Assert.Equal(
"Your name can be 32 characters long and cannot contain numbers or symbols.",
strings.Resolve(tooltip.StringInfoValue));
}
/// <summary>
@ -1583,6 +1606,272 @@ public sealed class CharacterCreationLiveDatTests
+ "regardless of the R2-1 margin fix");
}
/// <summary>
/// R3-1 (re-test 2): live-DAT pin for the wrap-mechanism fix's own
/// premise — the Coordination attribute-slider label (<c>0x100002ed</c>
/// under container <c>0x100003e8</c>) authors <c>OneLine=true</c> (dat
/// property <c>0x20</c>), the SAME retail default a caption with no
/// authored <c>0x20</c> resolves to for width-wrap purposes per
/// <c>UIElement_Text::CalcJustification</c>'s per-glyph gate (see
/// <see cref="UiButton.DrawBlockLabel"/>'s own doc) — so this element
/// must never width-wrap regardless of font metrics.
/// </summary>
[InstalledDatFact]
public void CoordinationAttributeLabel_AuthorsOneLineTrue()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
LayoutImporter.ImportInfos(
dats, layoutId, CharacterCreationUiController.RootElementId));
ElementInfo coordContainer = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x100003E8u));
ElementInfo coordLabel = Assert.IsType<ElementInfo>(FindInfo(coordContainer, 0x100002EDu));
Assert.True(coordLabel.TryGetEffectiveBool(0x20u, out bool oneLine) && oneLine);
}
/// <summary>
/// R3-2 (re-test 2): live-DAT pin for the wrap-mechanism fix's own
/// premise — the Skills credits button's caption ("Available Skill
/// Credits", <c>0x100003f9</c>) fits comfortably inside the button's
/// own FULL authored width (never needing to wrap), and its value
/// child (<c>0x100002f3</c>) sits at local X=116 — the confinement
/// figure Batch E used to force a false wrap, no longer consulted for
/// the wrap decision post-fix (see <see cref="UiButton.DrawBlockLabel"/>'s
/// own doc).
/// </summary>
[InstalledDatFact]
public void SkillsCreditsButton_CaptionFitsFullWidth_ValueChildStartsAtMidpoint()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
LayoutImporter.ImportInfos(
dats, layoutId, CharacterCreationUiController.RootElementId));
var strings = new DatStringResolver(dats);
ElementInfo skillsCredits = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x100003F9u));
Assert.True(skillsCredits.TryGetEffectiveProperty(0x17u, out var caption));
string? captionText = strings.Resolve(caption.StringInfoValue);
Assert.Equal("Available Skill Credits", captionText);
uint fontDid = skillsCredits.FontDid != 0 ? skillsCredits.FontDid : rootInfo.FontDid;
Assert.True(dats.TryGet<DatReaderWriter.DBObjs.Font>(fontDid, out var font) && font is not null);
var glyphs = new Dictionary<char, DatReaderWriter.Types.FontCharDesc>(font!.CharDescs.Count);
foreach (var cd in font.CharDescs) glyphs[(char)cd.Unicode] = cd;
var datFont = new UiDatFont(0, 0, 0, 0, 0, 0, font.MaxCharHeight, font.BaselineOffset, glyphs);
float measured = datFont.MeasureWidth(captionText!);
Assert.True(
measured < skillsCredits.Width,
$"caption measured {measured}px must fit the button's own full {skillsCredits.Width}px width");
ElementInfo valueChild = Assert.Single(skillsCredits.Children);
Assert.Equal(116f, valueChild.X);
}
/// <summary>
/// R3-3 (re-test 2): the info-box title (<c>0x100003fb</c>) and
/// description (<c>0x100003fc</c>) panes' own AUTHORED boxes overlap —
/// this is why <see cref="CharacterCreationSkillsPage"/>'s constructor
/// forces both to <c>VJustify.Top</c> rather than relying on disjoint
/// rects (see that constructor's own comment for the full decomp
/// citation). Pinned so a future DAT re-extract that changes these
/// boxes to genuinely disjoint rects is visible here, not silently
/// contradicting the fix's own premise.
/// </summary>
[InstalledDatFact]
public void SkillsInfoBoxTitleAndDescription_AuthoredBoxesOverlap()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
LayoutImporter.ImportInfos(
dats, layoutId, CharacterCreationUiController.RootElementId));
ElementInfo title = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x100003FBu));
ElementInfo description = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x100003FCu));
// Neither pane authors an explicit vertical-justify property — both
// fall to this port's shared (currently Center) unauthored default,
// ISSUES.md #410.
Assert.False(title.TryGetEffectiveProperty(0x15u, out _));
Assert.False(description.TryGetEffectiveProperty(0x15u, out _));
float titleBottom = title.Y + title.Height;
float descriptionTop = description.Y;
Assert.True(
titleBottom > descriptionTop,
$"expected the title's own box (Y={title.Y} H={title.Height}, bottom={titleBottom}) to "
+ $"overlap the description's box (Y={description.Y}) — if it no longer does, the "
+ "VerticalJustify.Top override in CharacterCreationSkillsPage may no longer be needed");
// The two boxes' own TOP edges still leave enough of a gap for
// Top-justified content not to collide — the fix's actual premise.
Assert.True(description.Y > title.Y);
}
/// <summary>
/// R3-4/R3-7 (re-test 2): retail authors TWO distinct
/// <c>UIElement_Scrollbar</c> thumb shapes. Chat's own scrollbar
/// (<c>0x10000012</c> under LayoutDesc <c>0x2100006f</c>) is the
/// 3-slice composite <see cref="DatWidgetFactory.BuildScrollbar"/>'s
/// original thumb-detection was built against (the thumb child itself
/// carries NO media; three Type-3 grandchildren supply the cap/middle/
/// cap sprites). The chargen Skills listbox scrollbar (<c>0x100003f8</c>)
/// instead authors a SIMPLE single-sprite thumb: the same structural
/// child (Type 1, id 1, not the inc/dec button) carries its OWN direct
/// Normal/Normal_rollover/Normal_pressed media and has ZERO children —
/// before the fix, the 3-slice-only search found nothing and every
/// Thumb*Sprite stayed 0. This test builds the REAL scrollbar end to
/// end and asserts <see cref="UiScrollbar.ThumbSprite"/> now resolves.
/// </summary>
[InstalledDatFact]
public void SkillsListboxScrollbar_SingleSpriteThumbShape_BuildsWithNonZeroThumbSprite()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiScrollbar scrollbar = Assert.IsType<UiScrollbar>(screen.FindElement(0x100003F8u));
Assert.False(scrollbar.Horizontal);
Assert.NotEqual(0u, scrollbar.ThumbSprite);
// The 3-slice caps stay unset for this shape — OnDraw's own
// single-tile fallback (ThumbTopSprite/ThumbBotSprite both 0)
// draws the whole thumb from ThumbSprite alone.
Assert.Equal(0u, scrollbar.ThumbTopSprite);
Assert.Equal(0u, scrollbar.ThumbBotSprite);
}
/// <summary>
/// R3-5/R3-6 (re-test 2): live-DAT pin for
/// <see cref="ChargenColorSpotComposer"/>'s four hardcoded enum ids —
/// resolves each through the SAME category-7 <c>RetailDataIdResolver</c>
/// chain <c>gmCGAppearancePage::DoColorSpots</c>/<c>DoGradDisk</c> use,
/// and pins the native dimensions those two decomp functions' own
/// <c>CreateLocalSurface</c> calls size their composite surfaces to
/// (spot/blank match the swatch buttons' own 37x44 authored rect;
/// gradDisk/gradPlug match the grad circle's own 110x112 rect).
/// </summary>
[InstalledDatFact]
public void ColorSpotAndGradDiskResources_ResolveToExpectedDimensions()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
(uint enumId, int width, int height)[] expected =
[
(0x1000000Du, 37, 44), // spot (active)
(0x1000000Fu, 37, 44), // blank (blocked)
(0x1000000Eu, 110, 112), // gradDisk
(0x10000010u, 110, 112), // gradPlug
];
foreach ((uint enumId, int width, int height) in expected)
{
uint did = RetailDataIdResolver.Resolve(dats, enumId, 7u);
Assert.NotEqual(0u, did);
Assert.True(dats.TryGet<DatReaderWriter.DBObjs.RenderSurface>(did, out var rs) && rs is not null);
Assert.Equal(width, (int)rs!.Width);
Assert.Equal(height, (int)rs.Height);
}
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
LayoutImporter.ImportInfos(
dats, layoutId, CharacterCreationUiController.RootElementId));
uint spotDid = RetailDataIdResolver.Resolve(dats, 0x1000000Du, 7u);
uint gradDiskDid = RetailDataIdResolver.Resolve(dats, 0x1000000Eu, 7u);
// The nine pColor swatch elements (retail's DoColorSpots targets)
// author exactly ONE DirectState sprite — and it resolves to the
// SAME RenderSurface as the "spot" enum resource above, i.e. the
// button's own static authored art already IS the un-recolored
// (black-center) spot template. This is exactly why the OLD Tint-
// multiply mechanism visibly tinted the ring: it was multiplying
// this real authored sprite, not drawing over nothing. No Normal/
// Highlight states exist (GF-9's own finding — the swatch's click
// feedback is a SEPARATE overlay element, not a state swap here).
ElementInfo spotElement = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x1000030Fu));
Assert.Equal(37f, spotElement.Width);
Assert.Equal(44f, spotElement.Height);
var spotDirectState = Assert.Single(spotElement.StateMedia);
Assert.Equal(string.Empty, spotDirectState.Key);
Assert.Equal(spotDid, spotDirectState.Value.File);
// The grad circle likewise authors its own DirectState sprite —
// resolving to the SAME RenderSurface as the "gradDisk" enum
// resource — so the pre-fix Tint-multiply mechanism DID show
// something for the disc too (the un-recolored gradient wheel,
// multiplied); R3-6's actual gap was that Eyes needs a DIFFERENT
// source image (the plug icon) which no per-state authored data
// provides — SetSelection swaps it procedurally in retail, exactly
// what RuntimeImageTexture now reproduces.
ElementInfo gradCircleElement = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x1000030Eu));
Assert.Equal(110f, gradCircleElement.Width);
Assert.Equal(112f, gradCircleElement.Height);
var gradDirectState = Assert.Single(gradCircleElement.StateMedia);
Assert.Equal(string.Empty, gradDirectState.Key);
Assert.Equal(gradDiskDid, gradDirectState.Value.File);
}
/// <summary>
/// R3-5: pixel-level ground truth for
/// <see cref="ChargenColorSpotComposer.ReplaceExactBlackWithColor"/>'s
/// whole premise — the ACTIVE spot template (enum <c>0x1000000d</c>)
/// has a genuinely near-black CENTER region and a genuinely non-black
/// RING/border region, and the BLANK template (enum <c>0x1000000f</c>)
/// has almost no black pixels at all (it is a DIFFERENT piece of art,
/// not the spot with its center left un-recolored).
/// </summary>
[InstalledDatFact]
public void SpotTemplate_HasBlackCenterAndNonBlackRing_BlankTemplateHasNeitherBlack()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint spotDid = RetailDataIdResolver.Resolve(dats, 0x1000000Du, 7u);
uint blankDid = RetailDataIdResolver.Resolve(dats, 0x1000000Fu, 7u);
Assert.True(dats.TryGet<DatReaderWriter.DBObjs.RenderSurface>(spotDid, out var spotRs) && spotRs is not null);
Assert.True(dats.TryGet<DatReaderWriter.DBObjs.RenderSurface>(blankDid, out var blankRs) && blankRs is not null);
var spot = AcDream.Core.Textures.SurfaceDecoder.DecodeRenderSurface(spotRs!);
var blank = AcDream.Core.Textures.SurfaceDecoder.DecodeRenderSurface(blankRs!);
(int black, int nonBlackOpaque, int total) CountPixels(byte[] rgba)
{
int black = 0, nonBlackOpaque = 0, total = 0;
for (int i = 0; i + 3 < rgba.Length; i += 4)
{
byte a = rgba[i + 3];
if (a < 10) continue;
total++;
if (rgba[i] == 0 && rgba[i + 1] == 0 && rgba[i + 2] == 0) black++;
else nonBlackOpaque++;
}
return (black, nonBlackOpaque, total);
}
var spotCounts = CountPixels(spot.Rgba8);
var blankCounts = CountPixels(blank.Rgba8);
// The spot genuinely has both a substantial black region (the
// center, to recolor) AND a substantial non-black region (the
// ring, to leave alone) — proves this isn't an all-black or
// all-colored template.
Assert.True(spotCounts.black > 100, $"expected a real black center, got {spotCounts.black} black pixels");
Assert.True(spotCounts.nonBlackOpaque > 100, $"expected a real non-black ring, got {spotCounts.nonBlackOpaque}");
// The blank template is a DIFFERENT asset, not "spot with an
// un-recolored center" — near-zero black pixels.
Assert.True(
blankCounts.black < spotCounts.black / 10,
$"blank template has {blankCounts.black} black pixels, expected far fewer than the spot's {spotCounts.black}");
}
private static void AssertButton(ImportedLayout layout, uint elementId) =>
Assert.IsType<UiButton>(layout.FindElement(elementId));
@ -1603,4 +1892,32 @@ public sealed class CharacterCreationLiveDatTests
null,
new DatStringResolver(dats).Resolve);
}
/// <summary>
/// R3-4/R3-5c (re-test 2): the chargen shade slider's own thumb child
/// (<c>0x10000321</c> &gt; structural id 1) authors ONE DirectState
/// sprite of its own (<c>0x06004D50</c>) and ZERO children — the
/// EARLIER (Batch F11-era) investigation misread this as "genuinely
/// nothing authored here" from a <c>string.Join</c> display artifact (a
/// single "" DirectState key joins to an empty string, indistinguishable
/// from zero entries in a printed log — NOT a code defect, a
/// diagnostic-only mistake). This is the EXACT SAME single-sprite-thumb
/// shape <see cref="DatWidgetFactory.BuildScrollbar"/>'s R3-4/R3-7 fix
/// already handles (<c>slices.Length == 0 -&gt; ThumbSprite =
/// DefaultImage(thumb)</c>) — no separate fix was needed for the shade
/// slider; this test proves the SAME code path already resolves it.
/// </summary>
[InstalledDatFact]
public void ShadeSlider_ThumbAuthorsItsOwnDirectStateSprite_BuildsWithNonZeroThumbSprite()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiScrollbar shadeSlider = Assert.IsType<UiScrollbar>(screen.FindElement(0x10000321u));
Assert.False(shadeSlider.Horizontal);
Assert.NotEqual(0u, shadeSlider.ThumbSprite);
}
}