acdream/tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs
Erik 91f84dec9f 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>
2026-08-16 17:20:42 +02:00

1923 lines
95 KiB
C#

using System.IO;
using System.Linq;
using System.Numerics;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Content;
using DatReaderWriter;
using DatReaderWriter.Options;
namespace AcDream.App.Tests.UI.Layout;
/// <summary>
/// Installed-retail-DAT acceptance gate for Campaign CC slice CC4. Opt in
/// with <c>ACDREAM_PROBE_LIVE_MOUNT=1</c>; <c>ACDREAM_DAT_DIR</c> can
/// override the ordinary Documents/Asheron's Call location. Mirrors
/// <see cref="CharacterManagementLiveDatTests"/>'s pattern: sweeps the
/// authored master-shell and page ids the campaign plan and CC4's own
/// decomp research cite, pinning them against the real installed layout.
/// </summary>
public sealed class CharacterCreationLiveDatTests
{
private static string DatDirectory =>
Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents",
"Asheron's Call");
[InstalledDatFact]
public void EnumTable5_ResolvesTheMasterShellRootAndChildren()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats,
CharacterCreationUiController.RootEnum,
5u);
Assert.NotEqual(0u, layoutId);
Console.WriteLine(
$"[CC4-DAT] category=5 enum=0x10000039 -> DID=0x{layoutId:X8}");
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
Assert.Equal(
CharacterCreationUiController.RootElementId,
screen.Root.DatElementId);
// CC4 re-review R5: the fixed-canvas arbiter THROWS if two concurrent
// screens declare different sizes, and char-management's root is
// DAT-pinned at 800x600 (CharacterManagementLiveDatTests). Chargen
// declares on top of it on the exact user-gate path, so its authored
// extent must be pinned too — an unequal extent is now a crash at
// Open(), not a cosmetic drift. Observe, don't infer (C4 closeout).
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
LayoutImporter.ImportInfos(
dats,
layoutId,
CharacterCreationUiController.RootElementId));
Assert.Equal(800f, rootInfo.Width);
Assert.Equal(600f, rootInfo.Height);
Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.ProgressBarElementId));
AssertButton(screen, CharacterCreationUiController.BackElementId);
AssertButton(screen, CharacterCreationUiController.NextElementId);
AssertButton(screen, CharacterCreationUiController.FinishElementId);
AssertButton(screen, CharacterCreationUiController.HelpElementId);
AssertButton(screen, CharacterCreationUiController.ExitElementId);
AssertButton(screen, CharacterCreationUiController.RandomElementId);
Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.MasterPageElementId));
foreach (uint pageId in new[]
{
CharacterCreationUiController.HeritagePageElementId,
CharacterCreationUiController.ProfessionPageElementId,
CharacterCreationUiController.SkillsPageElementId,
CharacterCreationUiController.AppearancePageElementId,
CharacterCreationUiController.TownPageElementId,
CharacterCreationUiController.SummaryPageElementId,
})
{
Assert.IsAssignableFrom<UiElement>(screen.FindElement(pageId));
}
AssertButton(screen, CharacterCreationUiController.HeritageTabElementId);
AssertButton(screen, CharacterCreationUiController.ProfessionTabElementId);
AssertButton(screen, CharacterCreationUiController.SkillsTabElementId);
AssertButton(screen, CharacterCreationUiController.AppearanceTabElementId);
AssertButton(screen, CharacterCreationUiController.TownTabElementId);
AssertButton(screen, CharacterCreationUiController.SummaryTabElementId);
}
[InstalledDatFact]
public void MountsThroughTheControllerAgainstLiveResources()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats,
CharacterCreationUiController.RootEnum,
5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
var host = new UiRoot();
var dialogs = MakeDialogFactory(dats, host);
var bindings = new CharacterCreationRuntimeBindings(
() => null,
_ => default,
_ => default,
_ => default,
(_, _) => default,
(_, _) => default,
_ => default,
_ => default,
_ => default,
_ => default,
_ => default,
() => { });
UiElement? ResolveTemplate(uint templateLayoutId, uint templateElementId) =>
LayoutImporter.Import(
dats, templateLayoutId, templateElementId, _ => (0u, 0, 0), null)?.Root;
CharacterCreationUiController? controller =
CharacterCreationUiController.CreateDetached(
host, screen, ResolveTemplate, dialogs, bindings,
new CharacterCreationUiController.DialogStrings(
"Are you sure?", "No name", "Unspent credits", "Randomize?", "Name too long"));
Assert.NotNull(controller);
controller!.AttachAndTick();
controller.Dispose();
dialogs.Dispose();
}
/// <summary>13 heritage buttons, the description text, all present as
/// authored (Heritage page — <c>gmCGHeritagePage::InitializePage @
/// 0x00483a10</c>).</summary>
[InstalledDatFact]
public void HeritagePage_HasAllThirteenRaceButtonsAndDescriptionText()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats,
CharacterCreationUiController.RootEnum,
5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement heritageRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.HeritagePageElementId));
uint[] heritageButtonIds =
[
0x100003BFu, 0x100003C1u, 0x100003C2u, 0x100003C3u,
0x10000590u, 0x100005A9u, 0x100005E8u, 0x100005F1u,
0x100005C4u, 0x10000591u, 0x100005BFu, 0x100005C7u,
0x100005C8u,
];
foreach (uint buttonId in heritageButtonIds)
{
Assert.IsType<UiButton>(
UiElement.FindDescendant(heritageRoot, buttonId));
}
Assert.IsType<UiText>(
UiElement.FindDescendant(heritageRoot, 0x100003C4u));
}
/// <summary>
/// R2-1 (Campaign CC gate round 1 Batch E): the Heritage/Profession/
/// Town/Summary description boxes author retail's four text-inset
/// margins (dat properties 0x23-0x26 — live-DAT-probe-confirmed
/// margL=9/margR=26/margU=15/margD=15 on all four, shared box
/// template) — this codebase never read them before this fix, so every
/// one of these boxes drew its first glyph flush against x=0 (Padding
/// alone, always 0 for DAT-built text), under the authored gold-frame's
/// own left border piece. Pins BOTH halves: the margins land on the
/// built <see cref="UiText"/> (not just the raw <see cref="ElementInfo"/>),
/// and <see cref="UiText.ContentOffsetX"/> computed with those margins
/// places the first line's origin at the authored interior (x=9), not
/// the box's outer edge (x=0).
/// </summary>
[InstalledDatFact]
public void HeritageDescription_MarginsMatchAuthoredInset_AndFirstLineOriginRespectsThem()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement heritageRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.HeritagePageElementId));
UiText description = Assert.IsType<UiText>(
UiElement.FindDescendant(heritageRoot, 0x100003C4u));
Assert.Equal(9f, description.MarginLeft);
Assert.Equal(26f, description.MarginRight);
Assert.Equal(15f, description.MarginTop);
Assert.Equal(15f, description.MarginBottom);
// First glyph of a left-justified line: Padding (0, DAT-built text
// never sets it) + MarginLeft (9) = x=9, NOT x=0 — the exact
// regression the user's "rained Starting Skills"/"OW HUNTERS"
// reports describe (the leading 1-2 characters clipped under the
// frame's left border because text used to start at x=0).
float firstLineX = UiText.ContentOffsetX(
description.Width, description.Padding,
description.MarginLeft, description.MarginRight,
lineWidth: 40f, centered: false, rightAligned: false);
Assert.Equal(9f, firstLineX);
Assert.NotEqual(0f, firstLineX);
}
/// <summary>Seven template buttons, six attribute sliders (each with a
/// lock button + scrollbar + value text), and the four derived
/// displays (Profession page —
/// <c>gmCGProfessionPage::InitializePage @ 0x00482d50</c>).</summary>
[InstalledDatFact]
public void ProfessionPage_HasTemplateButtonsSlidersAndDisplays()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats,
CharacterCreationUiController.RootEnum,
5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement professionRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.ProfessionPageElementId));
uint[] templateButtonIds =
[
0x100003D9u, 0x100003DAu, 0x100003DBu,
0x100003DCu, 0x100003DDu, 0x100003DEu, 0x100003DFu,
];
foreach (uint buttonId in templateButtonIds)
{
Assert.IsType<UiButton>(
UiElement.FindDescendant(professionRoot, buttonId));
}
uint[] sliderContainerIds =
[
0x100003E6u, 0x100003E7u, 0x100003E8u,
0x100003E9u, 0x100003EAu, 0x100003EBu,
];
foreach (uint containerId in sliderContainerIds)
{
UiElement container = Assert.IsAssignableFrom<UiElement>(
UiElement.FindDescendant(professionRoot, containerId));
Assert.IsType<UiScrollbar>(
UiElement.FindDescendant(container, 0x100002EEu));
// The value display authors as an editable Type-12 (retail's
// NumberInputFilter) — DatWidgetFactory maps that to UiField,
// not UiText. See CharacterCreationProfessionPage's ctor comment.
Assert.IsType<UiField>(
UiElement.FindDescendant(container, 0x100002EFu));
}
// Avail/health/stamina/mana each author as a Button whose Type-12
// value child is swallowed by UiButton.ConsumesDatChildren — the
// same substitution the Skills page's credits meter needed. See
// CharacterCreationProfessionPage's ctor comment.
foreach (uint containerId in new[]
{ 0x100003E2u, 0x100003E3u, 0x100003E4u, 0x100003E5u })
{
Assert.IsType<UiButton>(
UiElement.FindDescendant(professionRoot, containerId));
}
}
/// <summary>Skills listbox, credits meter, info panes (Skills page —
/// <c>gmCGSkillsPage::InitializePage @ 0x00481dd0</c>).</summary>
[InstalledDatFact]
public void SkillsPage_HasListboxCreditsAndInfoPanes()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats,
CharacterCreationUiController.RootEnum,
5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement skillsRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.SkillsPageElementId));
Assert.IsType<UiTemplateListBox>(
UiElement.FindDescendant(skillsRoot, 0x100003F7u));
// The credits meter (decomp id 0x100002f3) authors as a raw dat
// child of button 0x100003f9; UiButton.ConsumesDatChildren swallows
// it before it becomes an addressable widget, so the faithful
// acdream substitute is the button's own Label — see
// CharacterCreationSkillsPage's ctor comment.
Assert.IsType<UiButton>(
UiElement.FindDescendant(skillsRoot, 0x100003F9u));
Assert.IsType<UiText>(
UiElement.FindDescendant(skillsRoot, 0x100003FBu));
Assert.IsType<UiText>(
UiElement.FindDescendant(skillsRoot, 0x100003FCu));
}
/// <summary>Four town buttons + description text (Town page —
/// <c>gmCGTownPage::InitializePage @ 0x0047c6d0</c>).</summary>
[InstalledDatFact]
public void TownPage_HasFourStarterAreaButtons()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats,
CharacterCreationUiController.RootEnum,
5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement townRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.TownPageElementId));
foreach (uint buttonId in new[] { 0x1000040Bu, 0x1000040Du, 0x1000040Eu, 0x1000040Fu })
{
Assert.IsType<UiButton>(
UiElement.FindDescendant(townRoot, buttonId));
}
Assert.IsType<UiText>(
UiElement.FindDescendant(townRoot, 0x10000409u));
}
/// <summary>
/// GF-11b/GF-11c (Campaign CC gate round 1 Batch B). Live-DAT-measured:
/// each town button's marker (<c>0x1000040D</c>'s own child
/// <c>0x1000040C</c>) and its per-button name caption both carry the
/// SAME numeric id <c>0x10000409</c> as the page-level description
/// panel found by the sibling test above — a genuine id collision in
/// the installed dat between two DIFFERENT elements in DIFFERENT
/// subtrees (harmless for <c>DatWidgetFactory.BuildButton</c>'s lift,
/// which walks the button's OWN <c>ElementInfo.Children</c> list rather
/// than resolving by a global id lookup — but it means this test
/// verifies the BUILT BUTTON's own <see cref="UiButton.Label"/>/
/// <see cref="UiButton.LabelBox"/>/<see cref="UiButton.LabelColor"/>,
/// not a second <c>FindDescendant</c> call, which would ambiguously
/// return the unrelated page-level panel). Pins: the caption's own
/// authored rect (0,4,100,37) survives instead of being overwritten by
/// the marker-face-relative offset (GF-11c), and its color swaps
/// Normal (218,167,85) -&gt; Highlight/white (255,255,255) on selection
/// (GF-11b).
/// </summary>
[InstalledDatFact]
public void TownPage_HoltburgButton_CaptionHonorsOwnRectAndRecolorsWhiteOnSelection()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement townRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.TownPageElementId));
UiButton holtburg = AssertButton(townRoot, 0x1000040Du);
Assert.Equal("Holtburg", holtburg.Label);
Assert.Equal(UiButton.LabelAlignment.Center, holtburg.LabelAlign);
Assert.Equal((0f, 4f, 100f, 37f), holtburg.LabelBox);
holtburg.Selected = false;
Assert.Equal(new Vector4(218f / 255f, 167f / 255f, 85f / 255f, 1f), holtburg.LabelColor);
holtburg.Selected = true;
Assert.Equal(new Vector4(1f, 1f, 1f, 1f), holtburg.LabelColor);
}
/// <summary>
/// GF-1 (Campaign CC gate round 1 Batch B). Live-DAT-measured: the
/// Heritage row (<c>0x100003BFu</c>, Aluvian) authors retail's custom
/// "Unselected"/"Selected" radio-pair state DESCRIPTORS directly on the
/// row (property-only, no media), with the actual per-state art on its
/// single stateful dot child (<c>0x100003C0</c>). Before this fix,
/// <see cref="UiButton.Selected"/> committed nothing here.
/// </summary>
[InstalledDatFact]
public void HeritagePage_Row_SelectedTogglesTheAuthoredRadioDotState()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement heritageRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.HeritagePageElementId));
UiButton aluvian = AssertButton(heritageRoot, 0x100003BFu);
Assert.Equal("Unselected", aluvian.ActiveState);
aluvian.Selected = true;
Assert.Equal("Selected", aluvian.ActiveState);
Assert.Equal(RetailUiStateIds.Selected, aluvian.ActiveRetailStateId);
aluvian.Selected = false;
Assert.Equal("Unselected", aluvian.ActiveState);
}
/// <summary>
/// GF-1 counterpart: the Profession template row (<c>0x100003D9u</c>,
/// Custom/Adventurer) authors the identical custom radio-pair shape on
/// its own icon child (<c>0x100002E9</c>).
/// </summary>
[InstalledDatFact]
public void ProfessionPage_TemplateButton_SelectedTogglesTheAuthoredIconState()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement professionRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.ProfessionPageElementId));
UiButton template = AssertButton(professionRoot, 0x100003D9u);
Assert.Equal("Unselected", template.ActiveState);
template.Selected = true;
Assert.Equal("Selected", template.ActiveState);
template.Selected = false;
Assert.Equal("Unselected", template.ActiveState);
}
/// <summary>
/// GF-8: the Appearance page's Face/Clothes sub-tab buttons
/// (<c>0x100003A9u</c>/<c>0x100003AAu</c>) author the SAME custom
/// radio-pair shape on their own icon child (<c>0x100002E9</c>) — same
/// mechanism as the heritage/template rows, different page.
/// </summary>
[InstalledDatFact]
public void AppearancePage_FaceClothesSubTabButtons_SelectedTogglesTheAuthoredState()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement appearanceRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.AppearancePageElementId));
foreach (uint buttonId in new[]
{
CharacterCreationAppearancePage.FaceButtonId,
CharacterCreationAppearancePage.ClothesButtonId,
})
{
UiButton button = AssertButton(appearanceRoot, buttonId);
Assert.Equal("Unselected", button.ActiveState);
button.Selected = true;
Assert.Equal("Selected", button.ActiveState);
button.Selected = false;
Assert.Equal("Unselected", button.ActiveState);
}
}
/// <summary>
/// GF-1 family, gender-button shape: <c>0x100003A7u</c>/
/// <c>0x100003A8u</c> author the custom Unselected/Selected media
/// DIRECTLY on the button's own StateMedia (no separate face-segment
/// child) — live-DAT-measured, distinct from the heritage/template/
/// sub-tab family above. Exercises <see cref="UiButton"/>'s OTHER
/// custom-selection-pair code path (media on the button itself, so
/// <c>info.StateMedia.Count != 0</c> and no face child is ever
/// computed).
/// </summary>
[InstalledDatFact]
public void AppearancePage_GenderButtons_SelectedTogglesTheAuthoredState()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement appearanceRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.AppearancePageElementId));
foreach (uint buttonId in new[]
{
CharacterCreationAppearancePage.FemaleButtonId,
CharacterCreationAppearancePage.MaleButtonId,
})
{
UiButton button = AssertButton(appearanceRoot, buttonId);
Assert.Equal("Unselected", button.ActiveState);
button.Selected = true;
Assert.Equal("Selected", button.ActiveState);
button.Selected = false;
Assert.Equal("Unselected", button.ActiveState);
}
}
/// <summary>
/// GF-9 (Campaign CC gate round 1 Batch B). Live-DAT-measured: all nine
/// companion overlay elements (<c>SwatchOverlayIds</c>) resolve as
/// siblings of the swatches under the color-wheel container — retail's
/// ACTUAL click-feedback mechanism (<c>SetColor</c>'s
/// <c>m_tColorWheel[...][0x10][iCurColor*7]-&gt;SetVisible</c>), not a
/// state swap on the swatch buttons themselves (which author only an
/// unnamed DirectState sprite — no Normal/Highlight media at all).
/// </summary>
[InstalledDatFact]
public void AppearancePage_SwatchOverlays_AllNinePresent()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement appearanceRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.AppearancePageElementId));
foreach (uint overlayId in CharacterCreationAppearancePage.SwatchOverlayIds)
{
UiElement overlay = Assert.IsAssignableFrom<UiElement>(
UiElement.FindDescendant(appearanceRoot, overlayId));
// Retail's overlay ring starts hidden — SetColor only shows the
// one at the current color index; nothing is selected before
// any color choice runs.
Assert.True(overlay.Visible);
}
}
/// <summary>
/// GF-10 (Campaign CC gate round 1 Batch B). Live-DAT-measured: both
/// zoom buttons author a STANDARD Normal/Highlight(/rollover) pair —
/// unlike the custom radio-pair family above, this is pure wiring
/// (<see cref="CharacterCreationAppearancePage"/>'s click handlers), not
/// a new UiButton mechanism.
/// </summary>
[InstalledDatFact]
public void AppearancePage_ZoomButtons_AuthorStandardNormalHighlightPair()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement appearanceRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.AppearancePageElementId));
foreach (uint buttonId in new[]
{
CharacterCreationAppearancePage.ZoomInId,
CharacterCreationAppearancePage.ZoomOutId,
})
{
UiButton button = AssertButton(appearanceRoot, buttonId);
Assert.Equal("Normal", button.ActiveState);
Assert.True(button.TrySetRetailState(UiButtonStateMachine.Highlight));
Assert.Equal("Highlight", button.ActiveState);
Assert.True(button.TrySetRetailState(UiButtonStateMachine.Normal));
Assert.Equal("Normal", button.ActiveState);
}
}
/// <summary>The exit-warning + all per-heritage/per-town DAT string
/// keys this slice cites actually resolve in the installed table.
/// </summary>
[InstalledDatFact]
public void RequiredChargenStringsResolve()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
var strings = new DatStringResolver(dats);
const uint table = 0x23000002u;
string[] keys =
[
"ID_CharGen_ExitWarning",
"ID_CharGen_Heritage_StartingSkills_Header",
"ID_CharGen_Heritage_StartingSkills",
"ID_CharGen_Heritage_BonusSkills_Trained_Header",
"ID_CharGen_AluvianText_BonusSkills_Trained",
"ID_CharGen_GaruText_BonusSkills_Trained",
"ID_CharGen_ShoText_BonusSkills_Trained",
"ID_CharGen_ViaText_BonusSkills_Trained",
"ID_CharGen_ShadText_BonusSkills_Trained",
"ID_CharGen_GearText_BonusSkills_Trained",
"ID_CharGen_AunTText_BonusSkills_Trained",
"ID_CharGen_EmpText_BonusSkills_Trained",
"ID_CharGen_UndText_BonusSkills_Trained",
"ID_CharGen_TownHowTo",
"ID_CharGen_HoltText",
"ID_CharGen_ShoushiText",
"ID_CharGen_YaraqText",
"ID_CharGen_SanamarText",
// GF-3: Profession template description keys.
"ID_CharGen_CustomText",
"ID_CharGen_BowText",
"ID_CharGen_SwashText",
"ID_CharGen_LifeText",
"ID_CharGen_WarText",
"ID_CharGen_WayText",
"ID_CharGen_SoldierText",
// GF-6/AP-218: Appearance spin caption keys.
"ID_CharGen_HairStyle",
"ID_CharGen_Eyes",
"ID_CharGen_Skin",
"ID_CharGen_GearText_HairButton",
"ID_CharGen_GearText_EyesButton",
"ID_CharGen_GearText_SkinButton",
"ID_CharGen_OlthoiText_HairButton",
"ID_CharGen_OlthoiText_EyesButton",
"ID_CharGen_OlthoiText_SkinButton",
// Commit 3: Summary how-to text keys.
"ID_CharGen_SummaryHowTo",
"ID_CharGen_SummaryHowToEnd",
"ID_CharGen_AluMaleNames",
"ID_CharGen_AluFemaleNames",
"ID_CharGen_GharuMaleNames",
"ID_CharGen_GharuFemaleNames",
"ID_CharGen_ShoMaleNames",
"ID_CharGen_ShoFemaleNames",
"ID_CharGen_ViaMaleNames",
"ID_CharGen_ViaFemaleNames",
];
foreach (string key in keys)
{
string? resolved = strings.Resolve(table, DatStringResolver.ComputeHash(key));
Assert.True(resolved is not null, $"missing string: {key}");
}
}
/// <summary>
/// Campaign CC slice CC6b-MOUNT — the Appearance page's full authored
/// widget catalog. Pins the campaign plan's risk item 4 finding (the
/// color-wheel/gradient family resolves through EXISTING
/// <c>DatWidgetFactory</c> mappings; no new widget type was needed —
/// see <see cref="CharacterCreationAppearancePage"/>'s own class doc)
/// against the real installed DAT: gender/Face/Clothes buttons, all
/// nine spins, all nine color swatches, the shade scrollbar, and the
/// viewport.
/// </summary>
[InstalledDatFact]
public void AppearancePage_HasGenderChoiceSpinsSwatchesShadeAndViewport()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement appearanceRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.AppearancePageElementId));
AssertButton(appearanceRoot, CharacterCreationAppearancePage.FemaleButtonId);
AssertButton(appearanceRoot, CharacterCreationAppearancePage.MaleButtonId);
AssertButton(appearanceRoot, CharacterCreationAppearancePage.FaceButtonId);
AssertButton(appearanceRoot, CharacterCreationAppearancePage.ClothesButtonId);
Assert.IsAssignableFrom<UiElement>(
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.FaceChoicesId));
Assert.IsAssignableFrom<UiElement>(
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.ClothesChoicesId));
foreach (uint spinId in new[]
{
CharacterCreationAppearancePage.HairSpinId,
CharacterCreationAppearancePage.EyesSpinId,
CharacterCreationAppearancePage.NoseSpinId,
CharacterCreationAppearancePage.MouthSpinId,
CharacterCreationAppearancePage.SkinSpinId,
CharacterCreationAppearancePage.HeadgearSpinId,
CharacterCreationAppearancePage.ShirtSpinId,
CharacterCreationAppearancePage.TrousersSpinId,
CharacterCreationAppearancePage.FootwearSpinId,
})
{
UiButton spin = AssertButton(appearanceRoot, spinId);
// Fix round F2 item 2: the current-part highlight
// (TrySetRetailState(Normal/Highlight)) only has a visible
// effect through UiButton's ToggleBehavior branch when the
// authored spin actually sets DAT property 0x0B — measured
// (not assumed, matching this file's own discipline for the
// arrow geometry above) True for all nine spins against the
// installed EoR dat. Pinned so a future DAT revision that
// drops it shows up here instead of as a silently-dead
// highlight.
Assert.True(spin.ToggleBehavior, $"spin 0x{spinId:X8} must author ToggleBehavior for the current-part highlight to work.");
// Re-review nit N2 (2026-08-15): ToggleBehavior alone is necessary
// but not sufficient — UiButton.UpdateVisualState only COMMITS the
// requested state when _availableStates actually contains it
// (UiButton.cs's TrySetRetailState -> Selected setter ->
// UpdateVisualState chain). MEASURED (not assumed) against the
// installed EoR dat: TrySetRetailState(Highlight) itself always
// reports success (the ToggleBehavior branch commits
// unconditionally, matching TrySetRetailState's own contract),
// but NONE of the nine spins actually carries Highlight /
// Highlight_rollover / Highlight_pressed media on either of
// their two consumed arrow face segments — every one of them
// authors only Normal / Normal_rollover / Ghosted. So F2 item 2's
// current-part highlight is CURRENTLY A NO-OP for every spin:
// ActiveState silently stays at its prior value ("Normal")
// instead of ever becoming "Highlight". The pre-existing
// ToggleBehavior pin above never caught this because it only
// checks the PROPERTY that gates the state-machine branch, not
// whether that branch has anything to actually draw. Filed as
// AP-222 (retail-vs-acdream status unresolved — retail's own
// gmCGAppearancePage::SetSelection call sites are cited for
// the SetState(1)/SetState(6) calls, not for whether retail's
// OWN spin art authors Highlight media either). Pinned to
// "Normal" so a future DAT revision that adds real Highlight
// media is what makes this assertion start failing — the
// correct trigger to update it to "Highlight" instead of a
// silently-reintroduced dead highlight going unnoticed either way.
Assert.True(
spin.TrySetRetailState(UiButtonStateMachine.Highlight),
$"spin 0x{spinId:X8} must accept a Highlight state request.");
Assert.Equal("Normal", spin.ActiveState);
// AP-222 CORRECTED + RESOLVED (Campaign CC gate round 1 Batch B):
// the art half of the "no-op" stays a genuine no-op (ActiveState
// pinned to "Normal" above, unchanged) — retail's own spin art
// authors no Highlight media either, matching acdream. But the
// current-part highlight is NOT presentation-dead: retail's
// SetState(6) also recolors the spin's caption text (dat
// property 0x1B), live-DAT-measured 218,167,85 (Normal) ->
// 255,221,131 (Highlight), plus outline off -> on (property
// 0x21). DatWidgetFactory.BuildButton wires this per-state style
// unconditionally, so it is already active on `spin` from
// TrySetRetailState(Highlight) above, independent of whether the
// page has assigned a Label string yet.
Assert.Equal(new Vector4(255f / 255f, 221f / 255f, 131f / 255f, 1f), spin.LabelColor);
Assert.True(spin.Outline, $"spin 0x{spinId:X8} must outline its label in the Highlight state.");
Assert.True(spin.TrySetRetailState(UiButtonStateMachine.Normal));
Assert.Equal(new Vector4(218f / 255f, 167f / 255f, 85f / 255f, 1f), spin.LabelColor);
Assert.False(spin.Outline, $"spin 0x{spinId:X8} must not outline its label in the Normal state.");
}
// Every color-wheel-family id resolves through EXISTING
// DatWidgetFactory mappings (Button=1, Scrollbar=0xB, the generic
// Type-3 fallback) — the risk-item-4 scouting result, pinned.
foreach (uint swatchId in CharacterCreationAppearancePage.SwatchIds)
AssertButton(appearanceRoot, swatchId);
UiScrollbar shadeScroll = Assert.IsType<UiScrollbar>(
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.ShadeScrollId));
// Fix round F11: measured (not assumed) against the installed EoR
// dat — the shade scrollbar (0x10000321) is authored VERTICAL
// (33x85, taller than wide). Before this fix, UiScrollbar.OnEvent
// only routed to ScalarChanged when Horizontal was true, so mouse
// input on this control never reached SetShadeFromScalar in
// production. Pinned so a future DAT revision that flips this
// orientation is caught here rather than silently reintroducing the
// dead-input bug (UiScrollbar's OnVerticalScalarEvent handles this
// orientation now, but ONLY this orientation gets exercised in
// production).
Assert.False(shadeScroll.Horizontal);
Assert.True(shadeScroll.Height > shadeScroll.Width);
Assert.IsType<UiDatElement>(
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.GradCircleId));
AssertButton(appearanceRoot, CharacterCreationAppearancePage.RotateClockwiseId);
AssertButton(appearanceRoot, CharacterCreationAppearancePage.RotateCounterClockwiseId);
AssertButton(appearanceRoot, CharacterCreationAppearancePage.ZoomInId);
AssertButton(appearanceRoot, CharacterCreationAppearancePage.ZoomOutId);
Assert.IsType<UiViewport>(
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.ViewportId));
}
/// <summary>
/// Live-DAT-measured arrow geometry the page's spin OnClickAt zones are
/// built from — every one of the nine spins is uniformly 200px wide
/// with the two locally-reused arrow child ids
/// (<c>0x1000030a</c> decrement / <c>0x1000030b</c> increment) at the
/// SAME local positions. If a future DAT revision changes this shared
/// template's geometry, this test (not a silent behavior change) is
/// where it shows up.
/// </summary>
[InstalledDatFact]
public void AppearancePage_SpinArrowGeometryIsUniformAcrossAllNineSpins()
{
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? appearanceInfo = FindInfo(
rootInfo, CharacterCreationUiController.AppearancePageElementId);
Assert.NotNull(appearanceInfo);
foreach (uint spinId in new[]
{
CharacterCreationAppearancePage.HairSpinId,
CharacterCreationAppearancePage.EyesSpinId,
CharacterCreationAppearancePage.NoseSpinId,
CharacterCreationAppearancePage.MouthSpinId,
CharacterCreationAppearancePage.SkinSpinId,
CharacterCreationAppearancePage.HeadgearSpinId,
CharacterCreationAppearancePage.ShirtSpinId,
CharacterCreationAppearancePage.TrousersSpinId,
CharacterCreationAppearancePage.FootwearSpinId,
})
{
ElementInfo? spin = FindInfo(appearanceInfo!, spinId);
Assert.NotNull(spin);
Assert.Equal(200f, spin!.Width);
ElementInfo? decrement = spin.Children.FirstOrDefault(c => c.Id == 0x1000030Au);
ElementInfo? increment = spin.Children.FirstOrDefault(c => c.Id == 0x1000030Bu);
Assert.NotNull(decrement);
Assert.NotNull(increment);
Assert.Equal(80f, decrement!.X);
Assert.Equal(127f, increment!.X);
// Fix round F10: the arrow WIDTHS are what actually derive
// CharacterCreationAppearancePage.IncrementZoneEnd (174 =
// IncrementZoneStart 127 + this measured 47px width) — X alone
// pins the LEFT edge of each zone, not where the increment zone
// ends and the select-as-current-part body zone begins. Measured
// against the installed EoR dat: both arrows are 47px wide,
// uniformly, across all nine spins.
Assert.Equal(47f, decrement.Width);
Assert.Equal(47f, increment.Width);
}
}
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 RetailDialogFactory MakeDialogFactory(IDatReaderWriter dats, UiRoot host)
{
uint dialogDid = RetailDataIdResolver.Resolve(dats, 2u, 5u);
ImportedLayout? CreateLayout(RetailDialogType type)
{
uint rootElementId = RetailDialogFactory.RootElementId(type);
return rootElementId == 0u
? null
: LayoutImporter.Import(
dats, dialogDid, rootElementId, _ => (0u, 0, 0), null);
}
return new RetailDialogFactory(host, CreateLayout);
}
/// <summary>
/// Campaign CC slice CC5 — the Summary page's full authored widget
/// catalog: the name field (with <c>NameInputFilter</c>), the how-to
/// text, the viewport (Summary's OWN <c>gmCG3DView</c>), and the
/// listbox's THREE row templates (single-line, category-header,
/// key/value pair) confirmed against the installed EoR dat — see
/// <see cref="CharacterCreationSummaryPage"/>'s own class doc for the
/// decomp citation (<c>SetSummaryText @ 0x0047b1d0</c>) each template
/// maps to.
/// </summary>
[InstalledDatFact]
public void SummaryPage_HasNameFieldListboxTemplatesAndViewport()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats,
CharacterCreationUiController.RootEnum,
5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement summaryRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.SummaryPageElementId));
Assert.IsType<UiScrollbar>(
UiElement.FindDescendant(summaryRoot, CharacterCreationSummaryPage.ScrollId));
Assert.IsType<UiField>(
UiElement.FindDescendant(summaryRoot, CharacterCreationSummaryPage.NameTextId));
Assert.IsType<UiText>(
UiElement.FindDescendant(summaryRoot, CharacterCreationSummaryPage.HowToTextId));
Assert.IsType<UiViewport>(
UiElement.FindDescendant(summaryRoot, CharacterCreationSummaryPage.ViewportId));
UiTemplateListBox list = Assert.IsType<UiTemplateListBox>(
UiElement.FindDescendant(summaryRoot, CharacterCreationSummaryPage.ListBoxId));
Assert.Equal(3, list.Templates.Count);
UiElement? ResolveRow(int index) =>
LayoutImporter.Import(
dats,
list.Templates[index].TemplateLayoutId,
list.Templates[index].TemplateElementId,
_ => (0u, 0, 0),
null)?.Root;
UiElement lineRow = Assert.IsAssignableFrom<UiElement>(ResolveRow(0));
Assert.IsType<UiText>(UiElement.FindDescendant(lineRow, 0x100002F9u));
UiElement headerRow = Assert.IsAssignableFrom<UiElement>(ResolveRow(1));
Assert.IsType<UiText>(UiElement.FindDescendant(headerRow, 0x100000FEu));
UiElement pairRow = Assert.IsAssignableFrom<UiElement>(ResolveRow(2));
Assert.IsType<UiText>(UiElement.FindDescendant(pairRow, 0x100002FCu));
Assert.IsType<UiText>(UiElement.FindDescendant(pairRow, 0x100002FDu));
}
/// <summary>
/// R2-8 (Campaign CC gate round 1 Batch E): re-checks the AUTHORED-
/// initial-text hypothesis the user's re-test raised for the
/// <c>[ Name ]</c> the retail screenshot shows — Batch A's GF-15
/// closure already byte-verified retail's CODE never writes it
/// (<c>CharGenState::RandomizeCharacter</c>,
/// <c>gmCGSummaryPage::InitializePage</c>), but did not check whether
/// the field's own dat property <c>0x17</c> (the SAME authored-caption
/// mechanism <c>DatWidgetFactory.BuildText</c>/<c>BuildField</c> already
/// reads for every other element) carries a display-only placeholder.
/// It does not: the name field (<c>0x10000402</c>) authors NO <c>0x17</c>
/// on its default state or on ANY of its named states in the installed
/// EoR dat. Per this batch's own investigation contract ("if NOT
/// authored, STOP on this item"), this pins that negative result as a
/// durable regression check rather than leaving it as a one-off probe
/// finding — CONFIRMS Batch A's closure honestly, it does not change
/// acdream's behavior (the field stays genuinely empty, matching
/// retail's own code-empty field).
/// </summary>
[InstalledDatFact]
public void SummaryNameField_AuthorsNoP0x17OnAnyState()
{
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 nameField = Assert.IsType<ElementInfo>(
FindInfo(rootInfo, CharacterCreationSummaryPage.NameTextId));
Assert.False(
nameField.TryGetEffectiveProperty(0x17u, out _),
"the name field must not author a P0x17 caption on its effective "
+ "default state — if this starts failing, the DAT now carries an "
+ "authored placeholder and R2-8 should be revisited as a real fix.");
foreach (var (stateId, state) in nameField.States)
{
Assert.False(
state.Properties.Values.TryGetValue(0x17u, out var stateCaption)
&& stateCaption.Kind == UiPropertyKind.StringInfo,
$"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>
/// CC5 re-review residual round, R3 (2026-08-16): MEASURES the
/// installed global SkillTable's (portal.dat <c>0x0E000004</c>)
/// <c>MinLevel</c> distribution instead of inferring it — the C4
/// closeout's "observe, don't infer" lesson
/// (<c>docs/research/2026-08-05-c4-closeout-handoff.md</c>).
/// <see cref="AcDream.App.Net.RetailSkillFormula.CalculateChargenScore"/>'s
/// own doc comment used to claim "no retail-authored skill sets
/// MinLevel above Untrained=1" without ever reading the field; ACE's own
/// <c>SkillBase.cs</c> annotates the same field <c>// 1-2?</c> (a hedge
/// that observed 2s exist). This is that measurement — see the method's
/// own real-DAT finding recorded in <c>RetailSkillFormula.cs</c>'s doc
/// comment, corrected from this test's result.
/// </summary>
[InstalledDatFact]
public void SkillTable_MinLevelDistribution_NeverExceedsTrained()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
DatReaderWriter.DBObjs.SkillTable? skillTable =
dats.Get<DatReaderWriter.DBObjs.SkillTable>(0x0E000004u);
Assert.NotNull(skillTable);
var byMinLevel = skillTable!.Skills.Values
.GroupBy(skill => skill.MinLevel)
.OrderBy(g => g.Key)
.ToDictionary(g => g.Key, g => g.Count());
Console.WriteLine(
"[CC5-R3-DAT] SkillTable MinLevel distribution (level=count): "
+ string.Join(", ", byMinLevel.Select(p => $"{p.Key}={p.Value}"))
+ $" (total skills: {skillTable.Skills.Count})");
// RetailSkillFormula.CalculateChargenScore's own gate argument ("the
// decomp's own `if (edi_1 >= MinLevel)` gate passes for both callers,
// Trained=2 and Specialized=3") only holds while every skill's
// MinLevel stays at or below Trained(2) — pin that as a real
// assertion instead of leaving it as prose, so a future DAT
// revision that adds a MinLevel=3+ skill fails HERE, not silently
// under-credits that skill's chargen score.
Assert.True(
byMinLevel.Keys.All(minLevel => minLevel <= 2),
"A skill's MinLevel exceeded 2 (Trained) in the installed DAT — "
+ "RetailSkillFormula.CalculateChargenScore's Trained/Specialized "
+ "gate argument needs re-verification for this skill.");
}
/// <summary>
/// GF-13 (Campaign CC gate round 1, Batch A). Live-DAT-probe-confirmed:
/// the GM-only labels <c>0x10000403</c> ("Non-Admin") and
/// <c>0x10000494</c> ("Non-Envoy") both live under the Summary page
/// (<c>0x100003D6</c>, path <c>0x100003CC &gt; 0x100003D0 &gt;
/// 0x100003D6 &gt; {0x10000403,0x10000494}</c>) and both author dat
/// property <c>0x3B</c> (Invisible) = <see langword="true"/> — the exact
/// mechanism retail's <c>UIElement::OnSetAttribute @0x00462d80</c> case 8
/// hides them by. Pins the DATA half (<see cref="ElementInfo.Invisible"/>)
/// against the installed EoR dat.
/// </summary>
[InstalledDatFact]
public void SummaryPage_NonAdminNonEnvoyLabels_AuthorInvisibleTrue()
{
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));
foreach (uint targetId in new[] { 0x10000403u, 0x10000494u })
{
ElementInfo? found = FindInfo(rootInfo, targetId);
Assert.NotNull(found);
Assert.True(
found!.Invisible,
$"element 0x{targetId:X8} must author dat property 0x3B (Invisible) = true.");
}
}
/// <summary>
/// GF-13: the BEHAVIOR half — after the real controller mounts through
/// <see cref="CharacterCreationUiController.CreateDetached"/> (not a raw
/// <see cref="LayoutImporter.Build"/> call), the two authored-invisible
/// elements are not <see cref="UiElement.Visible"/>. Exercises
/// <c>HideAuthoredInvisibleElements</c>'s real chargen-scoped honor path,
/// not just the data plumbing the sibling test above pins.
/// </summary>
[InstalledDatFact]
public void SummaryPage_NonAdminNonEnvoyLabels_HiddenAfterControllerMount()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
var host = new UiRoot();
var dialogs = MakeDialogFactory(dats, host);
var bindings = new CharacterCreationRuntimeBindings(
() => null,
_ => default,
_ => default,
_ => default,
(_, _) => default,
(_, _) => default,
_ => default,
_ => default,
_ => default,
_ => default,
_ => default,
() => { });
UiElement? ResolveTemplate(uint templateLayoutId, uint templateElementId) =>
LayoutImporter.Import(
dats, templateLayoutId, templateElementId, _ => (0u, 0, 0), null)?.Root;
CharacterCreationUiController? controller =
CharacterCreationUiController.CreateDetached(
host, screen, ResolveTemplate, dialogs, bindings,
new CharacterCreationUiController.DialogStrings(
"Are you sure?", "No name", "Unspent credits", "Randomize?", "Name too long"));
Assert.NotNull(controller);
foreach (uint targetId in new[] { 0x10000403u, 0x10000494u })
{
UiElement found = Assert.IsAssignableFrom<UiElement>(
UiElement.FindDescendant(controller!.Root, targetId));
Assert.True(found.AuthoredInvisible, $"0x{targetId:X8} must carry AuthoredInvisible.");
Assert.False(found.Visible, $"0x{targetId:X8} must be hidden after mount.");
}
controller!.Dispose();
dialogs.Dispose();
}
/// <summary>
/// GF-5 (Campaign CC gate round 1, Batch A). Live-DAT-probe-confirmed
/// (raw <c>ElementDesc.Type</c> — the same id space as retail's
/// <c>DynamicCast</c> tags, 1=Button, 12=Text): the Skills listbox
/// authors exactly two templates. <c>Templates[0]</c>
/// (<c>0x100002F4</c>) is retail's own 3-child bucket-HEADER row
/// (unused by this port's flat-list simplification, AP-213).
/// <c>Templates[1]</c> (<c>0x100002FF</c>) is the REAL skill row — a
/// plain container root (rawType 3, NOT a Button), 7 children, byte-
/// traced against <c>gmCGSkillsPage::DoSkillRecords @ 0x004817e0</c> +
/// <c>tagSkillRecord</c>'s copy-constructor field order
/// (<c>acclient.h</c>): name (<c>0x10000301</c>, Text), pSkillLevelText
/// (<c>0x10000302</c>, Text), pUpCostText (<c>0x10000303</c>, Text),
/// pSkillUpButton (<c>0x10000304</c>, Button), pSkillDownButton
/// (<c>0x10000305</c>, Button), pDownCostText (<c>0x10000306</c>, Text).
/// See <see cref="CharacterCreationSkillsPage"/>'s own class doc for the
/// full trace.
/// </summary>
[InstalledDatFact]
public void SkillsPage_RealRowTemplate_HasNameLevelCostTextAndArrowButtons()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement skillsRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.SkillsPageElementId));
UiTemplateListBox list = Assert.IsType<UiTemplateListBox>(
UiElement.FindDescendant(skillsRoot, 0x100003F7u));
Assert.Equal(2, list.Templates.Count);
UiTemplateListEntry realRowTemplate = list.Templates[1];
Assert.Equal(0x100002FFu, realRowTemplate.TemplateElementId);
UiElement? row = LayoutImporter.Import(
dats,
realRowTemplate.TemplateLayoutId,
realRowTemplate.TemplateElementId,
_ => (0u, 0, 0),
null)?.Root;
UiElement realRow = Assert.IsAssignableFrom<UiElement>(row);
Assert.IsNotType<UiButton>(realRow);
Assert.IsType<UiText>(UiElement.FindDescendant(realRow, 0x10000301u));
Assert.IsType<UiText>(UiElement.FindDescendant(realRow, 0x10000302u));
Assert.IsType<UiText>(UiElement.FindDescendant(realRow, 0x10000303u));
Assert.IsType<UiText>(UiElement.FindDescendant(realRow, 0x10000306u));
Assert.IsType<UiButton>(UiElement.FindDescendant(realRow, 0x10000304u));
Assert.IsType<UiButton>(UiElement.FindDescendant(realRow, 0x10000305u));
// Templates[0] (0x100002F4) is retail's bucket-header row — unused
// by RebuildRows, still confirmed present so a future revision that
// drops it or changes its shape shows up here.
Assert.Equal(0x100002F4u, list.Templates[0].TemplateElementId);
}
/// <summary>
/// R2-4c (Campaign CC gate round 1 Batch F). Live-DAT probe: pins the
/// Skills listbox's own authored scrollbar link (dat property
/// <c>0x72</c>, <see cref="UiTemplateListBox.ScrollbarElementId"/>) —
/// CONFIRMED as <c>0x100003F8</c>, exactly the "+1 from the listbox"
/// hypothesis this batch's own investigation raised — and confirms a
/// real <see cref="UiScrollbar"/> resolves at that id under the Skills
/// page root, the exact fact
/// <see cref="CharacterCreationSkillsPage"/>'s constructor now wires
/// (<c>scrollbar.Model = list.Scroll</c>).
///
/// <para>
/// Also pins <c>Templates[0]</c>'s (<c>0x100002F4</c>) own header-
/// caption child id (<c>0x100002f6</c>, <c>DoSkillRecords @0x00481840</c>'s
/// own <c>GetChildRecursive</c> call) as prep evidence for whoever ports
/// the four-bucket sorted model (R2-4b, still open — see the AP-213
/// row): live-DAT-measured as a <see cref="UiButton"/>, NOT a
/// <see cref="UiText"/> — the SAME <c>UIElement_Button</c>-is-
/// <c>DynamicCast(0xc)</c>-compatible-with-<c>UIElement_Text</c> quirk
/// this campaign already ported for the six attribute-slider labels
/// (GF-4b) — retail's own <c>DynamicCast(0xc)</c> cast at
/// <c>0x00481855</c> would return null on a REAL Button object
/// otherwise, and the very next line unconditionally calls
/// <c>UIElement_Text::SetStringInfo</c> on it. A future port reads the
/// header caption through <c>UiButton.Label</c>, the same seam GF-4b
/// already established.
/// </para>
/// </summary>
[InstalledDatFact]
public void SkillsPage_Listbox_HasAScrollbarLink_AndHeaderTemplateHasACaptionChild()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement skillsRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.SkillsPageElementId));
UiTemplateListBox list = Assert.IsType<UiTemplateListBox>(
UiElement.FindDescendant(skillsRoot, 0x100003F7u));
Console.WriteLine(
$"[CC-Batch-F-DAT] Skills listbox ScrollbarElementId=0x{list.ScrollbarElementId:X8}");
Assert.Equal(0x100003F8u, list.ScrollbarElementId);
Assert.IsType<UiScrollbar>(
UiElement.FindDescendant(skillsRoot, list.ScrollbarElementId));
UiTemplateListEntry headerTemplate = list.Templates[0];
Assert.Equal(0x100002F4u, headerTemplate.TemplateElementId);
UiElement? headerRow = LayoutImporter.Import(
dats,
headerTemplate.TemplateLayoutId,
headerTemplate.TemplateElementId,
_ => (0u, 0, 0),
null)?.Root;
UiElement realHeaderRow = Assert.IsAssignableFrom<UiElement>(headerRow);
Assert.IsType<UiButton>(UiElement.FindDescendant(realHeaderRow, 0x100002F6u));
}
/// <summary>
/// GF-15 (Campaign CC gate round 1, Batch A). Live-DAT-probe-confirmed
/// during the investigation: the Message dialog catalog's popup
/// (<c>0x3D</c>), message text (<c>0x3E</c>), and OK button
/// (<c>0x26</c>) all author REAL, nonzero geometry (popup 400x95,
/// centered by <see cref="RetailMessageDialogView.SizeAndCenter"/>) —
/// ruling out a zero-size/collapsed-layout explanation for the dialog
/// rendering nothing. The actual root cause was a Z-ORDER bug (the
/// chargen screen's own per-tick <c>BringToFront</c> burying the dialog
/// behind its opaque backdrop while the dialog kept exclusive
/// <see cref="UiRoot.Modal"/> input priority — fixed in
/// <see cref="RetailDialogFactory.Tick"/>). This test pins the geometry
/// half so a future DAT revision that collapses the popup/message/button
/// to zero size is caught here instead of silently reintroducing an
/// invisible dialog.
/// </summary>
[InstalledDatFact]
public void MessageDialogCatalog_PopupMessageAndOkButton_AuthorNonzeroGeometry()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint dialogDid = RetailDataIdResolver.Resolve(dats, 2u, 5u);
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
LayoutImporter.ImportInfos(dats, dialogDid, 0x24u));
Assert.Equal(800f, rootInfo.Width);
Assert.Equal(600f, rootInfo.Height);
ElementInfo popup = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x3Du));
Assert.True(popup.Width > 0f && popup.Height > 0f);
ElementInfo message = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x3Eu));
Assert.True(message.Width > 0f && message.Height > 0f);
ElementInfo okButton = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x26u));
Assert.True(okButton.Width > 0f && okButton.Height > 0f);
}
/// <summary>
/// Campaign CC gate round 1 Batch C (GF-4a). Live-DAT-measured: each of
/// the four Profession display buttons (avail/health/stamina/mana
/// credits) and the Skills credits button author the CAPTION directly
/// as their OWN P0x17 property and carry exactly ONE Type-12 child with
/// NO state media of its own — the live VALUE slot
/// (<c>gmCGProfessionPage::InitializePage @0x00482f90-0x00483062</c>,
/// <c>gmCGSkillsPage::InitializePage @0x00481e1c</c>). Pins the shape
/// <see cref="DatWidgetFactory.BuildButton"/>'s <c>ValueLabel</c>
/// detection depends on.
/// </summary>
[InstalledDatFact]
public void ProfessionAndSkillsDisplayButtons_OwnCaptionPlusOneMediaLessValueChild()
{
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));
(uint Button, uint ValueChild)[] shapes =
[
(0x100003E2u, 0x100002F1u), // Profession available attribute credits
(0x100003E3u, 0x100002F3u), // Profession health
(0x100003E4u, 0x100002F3u), // Profession stamina
(0x100003E5u, 0x100002F3u), // Profession mana
(0x100003F9u, 0x100002F3u), // Skills credits
];
foreach ((uint buttonId, uint valueChildId) in shapes)
{
ElementInfo button = Assert.IsType<ElementInfo>(FindInfo(rootInfo, buttonId));
Assert.Equal(1u, button.Type);
Assert.True(
button.TryGetEffectiveProperty(0x17u, out UiPropertyValue caption)
&& caption.Kind == UiPropertyKind.StringInfo,
$"button 0x{buttonId:X8} must author its own P0x17 caption.");
ElementInfo singleChild = Assert.Single(button.Children);
Assert.Equal(valueChildId, singleChild.Id);
Assert.Equal(12u, singleChild.Type);
Assert.Empty(singleChild.StateMedia);
}
}
/// <summary>
/// GF-4b: the Profession page's six slider containers each carry a
/// name-label CHILD at the SAME relative id (<c>0x100002ed</c>) —
/// live-DAT-measured as Type 1 (<c>UIElement_Button</c>), matching
/// retail's own declared pointer type
/// (<c>class UIElement_Button* m_pHairSpin</c>-shaped fields
/// throughout <c>gmCGAppearancePage</c>/<c>gmCGProfessionPage</c> that
/// still receive <c>UIElement_Text::SetText</c> calls — retail's
/// <c>UIElement_Button</c> is DynamicCast-compatible with
/// <c>UIElement_Text</c> (id <c>0xc</c>), i.e. buttons carry their own
/// text-rendering capability). acdream's <c>UiButton.Label</c> is that
/// exact capability, so this element resolves as <see cref="UiButton"/>
/// in our port too, not <see cref="UiText"/>.
/// </summary>
[InstalledDatFact]
public void ProfessionPage_SliderContainers_HaveNameLabelButtonChild()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement professionRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.ProfessionPageElementId));
foreach (uint containerId in new[]
{
0x100003E6u, 0x100003E7u, 0x100003E8u,
0x100003E9u, 0x100003EAu, 0x100003EBu,
})
{
UiElement container = Assert.IsAssignableFrom<UiElement>(
UiElement.FindDescendant(professionRoot, containerId));
Assert.IsType<UiButton>(UiElement.FindDescendant(container, 0x100002EDu));
}
}
/// <summary>
/// Root 1d (Campaign CC gate round 1 Batch C): the Heritage
/// (<c>0x100003be</c>, 13 states) and Profession (<c>0x100003d8</c>,
/// 7 states) backdrops, live-DAT-measured against
/// <c>gmCGHeritagePage::Update</c>'s <c>m_pBackground-&gt;SetState</c>
/// literals and <c>gmCGProfessionPage::UpdateProfession</c>'s
/// per-template <c>eax_2-&gt;SetState</c> literals.
/// </summary>
[InstalledDatFact]
public void HeritageAndProfessionBackdrops_AuthorEveryRetailState()
{
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 heritageBackdrop = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x100003BEu));
uint[] heritageStates =
[
0x10000021u, 0x10000022u, 0x10000023u, 0x10000024u, 0x10000058u,
0x10000059u, 0x1000005Au, 0x1000005Bu, 0x1000005Cu, 0x1000005Du,
0x1000005Eu, 0x1000005Fu, 0x10000060u,
];
foreach (uint stateId in heritageStates)
Assert.True(heritageBackdrop.States.ContainsKey(stateId), $"heritage backdrop missing state 0x{stateId:X8}");
ElementInfo professionBackdrop = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x100003D8u));
uint[] professionStates =
[
0x1000002Bu, 0x1000002Cu, 0x1000002Du,
0x1000002Eu, 0x1000002Fu, 0x10000030u, 0x10000031u,
];
foreach (uint stateId in professionStates)
Assert.True(professionBackdrop.States.ContainsKey(stateId), $"profession backdrop missing state 0x{stateId:X8}");
}
/// <summary>
/// GF-3: the Profession page's description textbox
/// (<c>0x100003e0</c>) resolves as <see cref="UiText"/> and (Commit-2
/// scope, pinned here for completeness) carries the SAME eight
/// gold-frame child ids the Town description (<c>0x10000409</c>) and
/// Summary how-to (<c>0x10000404</c>) boxes carry — one shared box
/// template reused across pages.
/// </summary>
[InstalledDatFact]
public void DescriptionTextboxes_ShareTheSameGoldFrameChildTemplate()
{
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));
uint[] frameChildIds =
[
0x100002DEu, 0x100002DFu, 0x100002E0u, 0x100002E1u,
0x100000E8u, 0x100002E2u, 0x100002E3u, 0x100000EAu,
];
foreach (uint boxId in new[] { 0x100003E0u, 0x10000409u, 0x10000404u })
{
ElementInfo box = Assert.IsType<ElementInfo>(FindInfo(rootInfo, boxId));
Assert.Equal(12u, box.Type);
foreach (uint frameChildId in frameChildIds)
Assert.Contains(box.Children, c => c.Id == frameChildId);
}
// The Summary how-to box additionally carries a linked scrollbar.
ElementInfo summaryHowTo = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x10000404u));
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));
}
/// <summary>
/// Commit 3: the Heritage description box (which ALSO carries the
/// linked scrollbar — live-DAT-measured, unlike Profession/Town's
/// shorter description boxes) resolves it as a real widget too, and
/// the mount wires <see cref="UiScrollbar.Model"/> to the box's own
/// <see cref="UiText.Scroll"/>.
/// </summary>
[InstalledDatFact]
public void HeritageDescription_ScrollbarBuildsAndLinksToTextScroll()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement heritageRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.HeritagePageElementId));
UiText description = Assert.IsType<UiText>(
UiElement.FindDescendant(heritageRoot, 0x100003C4u));
UiScrollbar scroll = Assert.IsType<UiScrollbar>(
UiElement.FindDescendant(description, 0x100002E7u));
var host = new UiRoot();
var dialogs = MakeDialogFactory(dats, host);
var bindings = new CharacterCreationRuntimeBindings(
() => null,
_ => default, _ => default, _ => default, (_, _) => default, (_, _) => default,
_ => default, _ => default, _ => default, _ => default, _ => default, () => { });
UiElement? ResolveTemplate(uint templateLayoutId, uint templateElementId) =>
LayoutImporter.Import(
dats, templateLayoutId, templateElementId, _ => (0u, 0, 0), null)?.Root;
CharacterCreationUiController? controller =
CharacterCreationUiController.CreateDetached(
host, screen, ResolveTemplate, dialogs, bindings,
new CharacterCreationUiController.DialogStrings(
"Are you sure?", "No name", "Unspent credits", "Randomize?", "Name too long"));
Assert.NotNull(controller);
controller!.AttachAndTick();
Assert.Same(description.Scroll, scroll.Model);
controller.Dispose();
dialogs.Dispose();
}
/// <summary>
/// R2-7a (Campaign CC gate round 1 Batch E): the Summary OVERVIEW
/// listbox (<c>0x10000400</c>) authors a linked scrollbar via dat
/// property <c>0x72</c> — live-DAT-probe-confirmed
/// <c>ScrollbarElementId=0x10000401</c>, a SIBLING element under the
/// Summary page root, not a descendant of the listbox itself.
/// <see cref="CharacterCreationSummaryPage"/>'s constructor used to wire
/// only the how-to box's own scrollbar (Commit 3) and never resolved
/// this one, so the listbox never scrolled despite carrying more rows
/// than fit its 435px-tall view. Same linkage pattern every other
/// UiTemplateListBox owner in this codebase already uses
/// (SocialFriendsPageController, ConfigOptionsPageController, etc).
/// </summary>
[InstalledDatFact]
public void SummaryListbox_ScrollbarBuildsAndLinksToListboxScroll()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement summaryRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.SummaryPageElementId));
UiTemplateListBox list = Assert.IsType<UiTemplateListBox>(
UiElement.FindDescendant(summaryRoot, CharacterCreationSummaryPage.ListBoxId));
Assert.Equal(CharacterCreationSummaryPage.ScrollId, list.ScrollbarElementId);
UiScrollbar overviewScroll = Assert.IsType<UiScrollbar>(
UiElement.FindDescendant(summaryRoot, CharacterCreationSummaryPage.ScrollId));
var host = new UiRoot();
var dialogs = MakeDialogFactory(dats, host);
var bindings = new CharacterCreationRuntimeBindings(
() => null,
_ => default, _ => default, _ => default, (_, _) => default, (_, _) => default,
_ => default, _ => default, _ => default, _ => default, _ => default, () => { });
UiElement? ResolveTemplate(uint templateLayoutId, uint templateElementId) =>
LayoutImporter.Import(
dats, templateLayoutId, templateElementId, _ => (0u, 0, 0), null)?.Root;
CharacterCreationUiController? controller =
CharacterCreationUiController.CreateDetached(
host, screen, ResolveTemplate, dialogs, bindings,
new CharacterCreationUiController.DialogStrings(
"Are you sure?", "No name", "Unspent credits", "Randomize?", "Name too long"));
Assert.NotNull(controller);
controller!.AttachAndTick();
Assert.Same(list.Scroll, overviewScroll.Model);
controller.Dispose();
dialogs.Dispose();
}
/// <summary>
/// R2-7b (Campaign CC gate round 1 Batch E): the how-to box's scrollbar
/// THUMB only draws <c>if (m.HasOverflow)</c>
/// (<see cref="UiScrollbar"/>'s own draw gate) — the reported "no
/// thumb" symptom traces to R2-1's bug, not an independent defect: with
/// the pre-fix wrap width (the box's raw Width, ignoring the authored
/// margL=9/margR=26 inset), the Aluvian how-to text (the LONGEST
/// composed variant — SummaryHowTo + the male name-suggestion list +
/// SummaryHowToEnd) wrapped to fewer/shorter lines than the correctly
/// inset width does. This pins the causal claim directly against the
/// real installed strings/font: composing with the CORRECT (margin-
/// inset) width produces content taller than the view, so
/// <see cref="UiScrollable.HasOverflow"/> — which is exactly what
/// <see cref="UiScrollbar"/> gates the thumb on — is true.
/// </summary>
[InstalledDatFact]
public void SummaryHowToText_Aluvian_WithCorrectMarginInsetWidth_Overflows()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
var strings = new DatStringResolver(dats);
const uint table = 0x23000002u;
string? howTo = strings.Resolve(table, DatStringResolver.ComputeHash("ID_CharGen_SummaryHowTo"));
string? names = strings.Resolve(table, DatStringResolver.ComputeHash("ID_CharGen_AluMaleNames"));
string? howToEnd = strings.Resolve(table, DatStringResolver.ComputeHash("ID_CharGen_SummaryHowToEnd"));
Assert.NotNull(howTo);
Assert.NotNull(names);
Assert.NotNull(howToEnd);
string composed = howTo + names + howToEnd;
// Real font metrics (0x40000009 — live-DAT-probe-confirmed FontDid
// on 0x10000404), no GL/texture needed for MeasureWidth.
Assert.True(dats.TryGet<DatReaderWriter.DBObjs.Font>(0x40000009u, 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);
// Live-DAT-measured box geometry (0x10000404): 247x380,
// margL=9/margR=26/margU=15/margD=15.
var target = new UiText
{
Width = 247f,
Height = 380f,
DatFont = datFont,
MarginLeft = 9f,
MarginRight = 26f,
MarginTop = 15f,
MarginBottom = 15f,
};
var segments = new[] { new DatRichText.Segment(composed, Vector4.One) };
var lines = DatRichText.Compose(target, segments);
float viewHeight = target.Height - target.Padding - target.MarginTop - target.Padding - target.MarginBottom;
float contentHeight = lines.Count * datFont.LineHeight;
Assert.True(
contentHeight > viewHeight,
$"expected the correctly-inset composition ({lines.Count} lines, "
+ $"{contentHeight}px) to overflow the {viewHeight}px view — if it "
+ "doesn't, the how-to scrollbar's thumb has nothing to gate on "
+ "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));
private static UiButton AssertButton(UiElement root, uint elementId) =>
Assert.IsType<UiButton>(UiElement.FindDescendant(root, elementId));
private static ImportedLayout BuildSelected(
IDatReaderWriter dats,
uint layoutDid,
uint rootId)
{
ElementInfo info = Assert.IsType<ElementInfo>(
LayoutImporter.ImportInfos(dats, layoutDid, rootId));
return LayoutImporter.Build(
info,
_ => (0u, 0, 0),
null,
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);
}
}