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; /// /// Installed-retail-DAT acceptance gate for Campaign CC slice CC4. Opt in /// with ACDREAM_PROBE_LIVE_MOUNT=1; ACDREAM_DAT_DIR can /// override the ordinary Documents/Asheron's Call location. Mirrors /// '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. /// 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( LayoutImporter.ImportInfos( dats, layoutId, CharacterCreationUiController.RootElementId)); Assert.Equal(800f, rootInfo.Width); Assert.Equal(600f, rootInfo.Height); Assert.IsAssignableFrom( 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( screen.FindElement(CharacterCreationUiController.MasterPageElementId)); foreach (uint pageId in new[] { CharacterCreationUiController.HeritagePageElementId, CharacterCreationUiController.ProfessionPageElementId, CharacterCreationUiController.SkillsPageElementId, CharacterCreationUiController.AppearancePageElementId, CharacterCreationUiController.TownPageElementId, CharacterCreationUiController.SummaryPageElementId, }) { Assert.IsAssignableFrom(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(); } /// 13 heritage buttons, the description text, all present as /// authored (Heritage page — gmCGHeritagePage::InitializePage @ /// 0x00483a10). [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( 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( UiElement.FindDescendant(heritageRoot, buttonId)); } Assert.IsType( UiElement.FindDescendant(heritageRoot, 0x100003C4u)); } /// /// 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 (not just the raw ), /// and computed with those margins /// places the first line's origin at the authored interior (x=9), not /// the box's outer edge (x=0). /// [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( screen.FindElement(CharacterCreationUiController.HeritagePageElementId)); UiText description = Assert.IsType( 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); } /// Seven template buttons, six attribute sliders (each with a /// lock button + scrollbar + value text), and the four derived /// displays (Profession page — /// gmCGProfessionPage::InitializePage @ 0x00482d50). [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( screen.FindElement(CharacterCreationUiController.ProfessionPageElementId)); uint[] templateButtonIds = [ 0x100003D9u, 0x100003DAu, 0x100003DBu, 0x100003DCu, 0x100003DDu, 0x100003DEu, 0x100003DFu, ]; foreach (uint buttonId in templateButtonIds) { Assert.IsType( UiElement.FindDescendant(professionRoot, buttonId)); } uint[] sliderContainerIds = [ 0x100003E6u, 0x100003E7u, 0x100003E8u, 0x100003E9u, 0x100003EAu, 0x100003EBu, ]; foreach (uint containerId in sliderContainerIds) { UiElement container = Assert.IsAssignableFrom( UiElement.FindDescendant(professionRoot, containerId)); Assert.IsType( 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( 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( UiElement.FindDescendant(professionRoot, containerId)); } } /// Skills listbox, credits meter, info panes (Skills page — /// gmCGSkillsPage::InitializePage @ 0x00481dd0). [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( screen.FindElement(CharacterCreationUiController.SkillsPageElementId)); Assert.IsType( 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( UiElement.FindDescendant(skillsRoot, 0x100003F9u)); Assert.IsType( UiElement.FindDescendant(skillsRoot, 0x100003FBu)); Assert.IsType( UiElement.FindDescendant(skillsRoot, 0x100003FCu)); } /// Four town buttons + description text (Town page — /// gmCGTownPage::InitializePage @ 0x0047c6d0). [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( screen.FindElement(CharacterCreationUiController.TownPageElementId)); foreach (uint buttonId in new[] { 0x1000040Bu, 0x1000040Du, 0x1000040Eu, 0x1000040Fu }) { Assert.IsType( UiElement.FindDescendant(townRoot, buttonId)); } Assert.IsType( UiElement.FindDescendant(townRoot, 0x10000409u)); } /// /// GF-11b/GF-11c (Campaign CC gate round 1 Batch B). Live-DAT-measured: /// each town button's marker (0x1000040D's own child /// 0x1000040C) and its per-button name caption both carry the /// SAME numeric id 0x10000409 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 DatWidgetFactory.BuildButton's lift, /// which walks the button's OWN ElementInfo.Children list rather /// than resolving by a global id lookup — but it means this test /// verifies the BUILT BUTTON's own / /// /, /// not a second FindDescendant 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) -> Highlight/white (255,255,255) on selection /// (GF-11b). /// [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( 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); } /// /// GF-1 (Campaign CC gate round 1 Batch B). Live-DAT-measured: the /// Heritage row (0x100003BFu, 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 (0x100003C0). Before this fix, /// committed nothing here. /// [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( 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); } /// /// GF-1 counterpart: the Profession template row (0x100003D9u, /// Custom/Adventurer) authors the identical custom radio-pair shape on /// its own icon child (0x100002E9). /// [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( 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); } /// /// GF-8: the Appearance page's Face/Clothes sub-tab buttons /// (0x100003A9u/0x100003AAu) author the SAME custom /// radio-pair shape on their own icon child (0x100002E9) — same /// mechanism as the heritage/template rows, different page. /// [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( 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); } } /// /// GF-1 family, gender-button shape: 0x100003A7u/ /// 0x100003A8u 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 's OTHER /// custom-selection-pair code path (media on the button itself, so /// info.StateMedia.Count != 0 and no face child is ever /// computed). /// [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( 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); } } /// /// GF-9 (Campaign CC gate round 1 Batch B). Live-DAT-measured: all nine /// companion overlay elements (SwatchOverlayIds) resolve as /// siblings of the swatches under the color-wheel container — retail's /// ACTUAL click-feedback mechanism (SetColor's /// m_tColorWheel[...][0x10][iCurColor*7]->SetVisible), not a /// state swap on the swatch buttons themselves (which author only an /// unnamed DirectState sprite — no Normal/Highlight media at all). /// [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( screen.FindElement(CharacterCreationUiController.AppearancePageElementId)); foreach (uint overlayId in CharacterCreationAppearancePage.SwatchOverlayIds) { UiElement overlay = Assert.IsAssignableFrom( 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); } } /// /// 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 /// ('s click handlers), not /// a new UiButton mechanism. /// [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( 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); } } /// The exit-warning + all per-heritage/per-town DAT string /// keys this slice cites actually resolve in the installed table. /// [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}"); } } /// /// 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 /// DatWidgetFactory mappings; no new widget type was needed — /// see '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. /// [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( screen.FindElement(CharacterCreationUiController.AppearancePageElementId)); AssertButton(appearanceRoot, CharacterCreationAppearancePage.FemaleButtonId); AssertButton(appearanceRoot, CharacterCreationAppearancePage.MaleButtonId); AssertButton(appearanceRoot, CharacterCreationAppearancePage.FaceButtonId); AssertButton(appearanceRoot, CharacterCreationAppearancePage.ClothesButtonId); Assert.IsAssignableFrom( UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.FaceChoicesId)); Assert.IsAssignableFrom( 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( 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( UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.GradCircleId)); AssertButton(appearanceRoot, CharacterCreationAppearancePage.RotateClockwiseId); AssertButton(appearanceRoot, CharacterCreationAppearancePage.RotateCounterClockwiseId); AssertButton(appearanceRoot, CharacterCreationAppearancePage.ZoomInId); AssertButton(appearanceRoot, CharacterCreationAppearancePage.ZoomOutId); Assert.IsType( UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.ViewportId)); } /// /// 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 /// (0x1000030a decrement / 0x1000030b 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. /// [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( 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); } /// /// Campaign CC slice CC5 — the Summary page's full authored widget /// catalog: the name field (with NameInputFilter), the how-to /// text, the viewport (Summary's OWN gmCG3DView), and the /// listbox's THREE row templates (single-line, category-header, /// key/value pair) confirmed against the installed EoR dat — see /// 's own class doc for the /// decomp citation (SetSummaryText @ 0x0047b1d0) each template /// maps to. /// [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( screen.FindElement(CharacterCreationUiController.SummaryPageElementId)); Assert.IsType( UiElement.FindDescendant(summaryRoot, CharacterCreationSummaryPage.ScrollId)); Assert.IsType( UiElement.FindDescendant(summaryRoot, CharacterCreationSummaryPage.NameTextId)); Assert.IsType( UiElement.FindDescendant(summaryRoot, CharacterCreationSummaryPage.HowToTextId)); Assert.IsType( UiElement.FindDescendant(summaryRoot, CharacterCreationSummaryPage.ViewportId)); UiTemplateListBox list = Assert.IsType( 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(ResolveRow(0)); Assert.IsType(UiElement.FindDescendant(lineRow, 0x100002F9u)); UiElement headerRow = Assert.IsAssignableFrom(ResolveRow(1)); Assert.IsType(UiElement.FindDescendant(headerRow, 0x100000FEu)); UiElement pairRow = Assert.IsAssignableFrom(ResolveRow(2)); Assert.IsType(UiElement.FindDescendant(pairRow, 0x100002FCu)); Assert.IsType(UiElement.FindDescendant(pairRow, 0x100002FDu)); } /// /// R2-8 (Campaign CC gate round 1 Batch E): re-checks the AUTHORED- /// initial-text hypothesis the user's re-test raised for the /// [ Name ] the retail screenshot shows — Batch A's GF-15 /// closure already byte-verified retail's CODE never writes it /// (CharGenState::RandomizeCharacter, /// gmCGSummaryPage::InitializePage), but did not check whether /// the field's own dat property 0x17 (the SAME authored-caption /// mechanism DatWidgetFactory.BuildText/BuildField already /// reads for every other element) carries a display-only placeholder. /// It does not: the name field (0x10000402) authors NO 0x17 /// 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). /// [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( LayoutImporter.ImportInfos( dats, layoutId, CharacterCreationUiController.RootElementId)); ElementInfo nameField = Assert.IsType( 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)); } /// /// CC5 re-review residual round, R3 (2026-08-16): MEASURES the /// installed global SkillTable's (portal.dat 0x0E000004) /// MinLevel distribution instead of inferring it — the C4 /// closeout's "observe, don't infer" lesson /// (docs/research/2026-08-05-c4-closeout-handoff.md). /// 's /// own doc comment used to claim "no retail-authored skill sets /// MinLevel above Untrained=1" without ever reading the field; ACE's own /// SkillBase.cs annotates the same field // 1-2? (a hedge /// that observed 2s exist). This is that measurement — see the method's /// own real-DAT finding recorded in RetailSkillFormula.cs's doc /// comment, corrected from this test's result. /// [InstalledDatFact] public void SkillTable_MinLevelDistribution_NeverExceedsTrained() { using var dats = new DatCollection(DatDirectory, DatAccessType.Read); DatReaderWriter.DBObjs.SkillTable? skillTable = dats.Get(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."); } /// /// GF-13 (Campaign CC gate round 1, Batch A). Live-DAT-probe-confirmed: /// the GM-only labels 0x10000403 ("Non-Admin") and /// 0x10000494 ("Non-Envoy") both live under the Summary page /// (0x100003D6, path 0x100003CC > 0x100003D0 > /// 0x100003D6 > {0x10000403,0x10000494}) and both author dat /// property 0x3B (Invisible) = — the exact /// mechanism retail's UIElement::OnSetAttribute @0x00462d80 case 8 /// hides them by. Pins the DATA half () /// against the installed EoR dat. /// [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( 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."); } } /// /// GF-13: the BEHAVIOR half — after the real controller mounts through /// (not a raw /// call), the two authored-invisible /// elements are not . Exercises /// HideAuthoredInvisibleElements's real chargen-scoped honor path, /// not just the data plumbing the sibling test above pins. /// [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.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(); } /// /// GF-5 (Campaign CC gate round 1, Batch A). Live-DAT-probe-confirmed /// (raw ElementDesc.Type — the same id space as retail's /// DynamicCast tags, 1=Button, 12=Text): the Skills listbox /// authors exactly two templates. Templates[0] /// (0x100002F4) is retail's own 3-child bucket-HEADER row /// (unused by this port's flat-list simplification, AP-213). /// Templates[1] (0x100002FF) is the REAL skill row — a /// plain container root (rawType 3, NOT a Button), 7 children, byte- /// traced against gmCGSkillsPage::DoSkillRecords @ 0x004817e0 + /// tagSkillRecord's copy-constructor field order /// (acclient.h): name (0x10000301, Text), pSkillLevelText /// (0x10000302, Text), pUpCostText (0x10000303, Text), /// pSkillUpButton (0x10000304, Button), pSkillDownButton /// (0x10000305, Button), pDownCostText (0x10000306, Text). /// See 's own class doc for the /// full trace. /// [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( screen.FindElement(CharacterCreationUiController.SkillsPageElementId)); UiTemplateListBox list = Assert.IsType( 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(row); Assert.IsNotType(realRow); Assert.IsType(UiElement.FindDescendant(realRow, 0x10000301u)); Assert.IsType(UiElement.FindDescendant(realRow, 0x10000302u)); Assert.IsType(UiElement.FindDescendant(realRow, 0x10000303u)); Assert.IsType(UiElement.FindDescendant(realRow, 0x10000306u)); Assert.IsType(UiElement.FindDescendant(realRow, 0x10000304u)); Assert.IsType(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); } /// /// R2-4c (Campaign CC gate round 1 Batch F). Live-DAT probe: pins the /// Skills listbox's own authored scrollbar link (dat property /// 0x72, ) — /// CONFIRMED as 0x100003F8, exactly the "+1 from the listbox" /// hypothesis this batch's own investigation raised — and confirms a /// real resolves at that id under the Skills /// page root, the exact fact /// 's constructor now wires /// (scrollbar.Model = list.Scroll). /// /// /// Also pins Templates[0]'s (0x100002F4) own header- /// caption child id (0x100002f6, DoSkillRecords @0x00481840's /// own GetChildRecursive 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 , NOT a /// — the SAME UIElement_Button-is- /// DynamicCast(0xc)-compatible-with-UIElement_Text quirk /// this campaign already ported for the six attribute-slider labels /// (GF-4b) — retail's own DynamicCast(0xc) cast at /// 0x00481855 would return null on a REAL Button object /// otherwise, and the very next line unconditionally calls /// UIElement_Text::SetStringInfo on it. A future port reads the /// header caption through UiButton.Label, the same seam GF-4b /// already established. /// /// [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( screen.FindElement(CharacterCreationUiController.SkillsPageElementId)); UiTemplateListBox list = Assert.IsType( UiElement.FindDescendant(skillsRoot, 0x100003F7u)); Console.WriteLine( $"[CC-Batch-F-DAT] Skills listbox ScrollbarElementId=0x{list.ScrollbarElementId:X8}"); Assert.Equal(0x100003F8u, list.ScrollbarElementId); Assert.IsType( 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(headerRow); Assert.IsType(UiElement.FindDescendant(realHeaderRow, 0x100002F6u)); } /// /// GF-15 (Campaign CC gate round 1, Batch A). Live-DAT-probe-confirmed /// during the investigation: the Message dialog catalog's popup /// (0x3D), message text (0x3E), and OK button /// (0x26) all author REAL, nonzero geometry (popup 400x95, /// centered by ) — /// 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 BringToFront burying the dialog /// behind its opaque backdrop while the dialog kept exclusive /// input priority — fixed in /// ). 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. /// [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( LayoutImporter.ImportInfos(dats, dialogDid, 0x24u)); Assert.Equal(800f, rootInfo.Width); Assert.Equal(600f, rootInfo.Height); ElementInfo popup = Assert.IsType(FindInfo(rootInfo, 0x3Du)); Assert.True(popup.Width > 0f && popup.Height > 0f); ElementInfo message = Assert.IsType(FindInfo(rootInfo, 0x3Eu)); Assert.True(message.Width > 0f && message.Height > 0f); ElementInfo okButton = Assert.IsType(FindInfo(rootInfo, 0x26u)); Assert.True(okButton.Width > 0f && okButton.Height > 0f); } /// /// 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 /// (gmCGProfessionPage::InitializePage @0x00482f90-0x00483062, /// gmCGSkillsPage::InitializePage @0x00481e1c). Pins the shape /// 's ValueLabel /// detection depends on. /// [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( 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(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); } } /// /// GF-4b: the Profession page's six slider containers each carry a /// name-label CHILD at the SAME relative id (0x100002ed) — /// live-DAT-measured as Type 1 (UIElement_Button), matching /// retail's own declared pointer type /// (class UIElement_Button* m_pHairSpin-shaped fields /// throughout gmCGAppearancePage/gmCGProfessionPage that /// still receive UIElement_Text::SetText calls — retail's /// UIElement_Button is DynamicCast-compatible with /// UIElement_Text (id 0xc), i.e. buttons carry their own /// text-rendering capability). acdream's UiButton.Label is that /// exact capability, so this element resolves as /// in our port too, not . /// [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( screen.FindElement(CharacterCreationUiController.ProfessionPageElementId)); foreach (uint containerId in new[] { 0x100003E6u, 0x100003E7u, 0x100003E8u, 0x100003E9u, 0x100003EAu, 0x100003EBu, }) { UiElement container = Assert.IsAssignableFrom( UiElement.FindDescendant(professionRoot, containerId)); Assert.IsType(UiElement.FindDescendant(container, 0x100002EDu)); } } /// /// Root 1d (Campaign CC gate round 1 Batch C): the Heritage /// (0x100003be, 13 states) and Profession (0x100003d8, /// 7 states) backdrops, live-DAT-measured against /// gmCGHeritagePage::Update's m_pBackground->SetState /// literals and gmCGProfessionPage::UpdateProfession's /// per-template eax_2->SetState literals. /// [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( LayoutImporter.ImportInfos( dats, layoutId, CharacterCreationUiController.RootElementId)); ElementInfo heritageBackdrop = Assert.IsType(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(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}"); } /// /// GF-3: the Profession page's description textbox /// (0x100003e0) resolves as and (Commit-2 /// scope, pinned here for completeness) carries the SAME eight /// gold-frame child ids the Town description (0x10000409) and /// Summary how-to (0x10000404) boxes carry — one shared box /// template reused across pages. /// [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( 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(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(FindInfo(rootInfo, 0x10000404u)); Assert.Contains(summaryHowTo.Children, c => c.Id == 0x100002E7u); } /// /// Campaign CC gate round 1 Batch C, Commit 2: the eight gold-frame /// pieces and the linked scrollbar — previously dropped outright by /// UiText.ConsumesDatChildren — now build as REAL widgets /// reachable via , on all three /// chargen description boxes. /// [InstalledDatFact] public void DescriptionTextboxes_FramesAndScrollbarBuildAsRealWidgets() { using var dats = new DatCollection(DatDirectory, DatAccessType.Read); uint layoutId = RetailDataIdResolver.Resolve( dats, CharacterCreationUiController.RootEnum, 5u); ImportedLayout screen = BuildSelected( dats, layoutId, CharacterCreationUiController.RootElementId); uint[] frameChildIds = [ 0x100002DEu, 0x100002DFu, 0x100002E0u, 0x100002E1u, 0x100000E8u, 0x100002E2u, 0x100002E3u, 0x100000EAu, ]; foreach (uint boxId in new[] { 0x100003E0u, 0x10000409u, 0x10000404u }) { UiText box = Assert.IsType(screen.FindElement(boxId)); foreach (uint frameChildId in frameChildIds) Assert.NotNull(UiElement.FindDescendant(box, frameChildId)); } UiText summaryHowTo = Assert.IsType(screen.FindElement(0x10000404u)); Assert.IsType(UiElement.FindDescendant(summaryHowTo, 0x100002E7u)); } /// /// 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 to the box's own /// . /// [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( screen.FindElement(CharacterCreationUiController.HeritagePageElementId)); UiText description = Assert.IsType( UiElement.FindDescendant(heritageRoot, 0x100003C4u)); UiScrollbar scroll = Assert.IsType( 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(); } /// /// R2-7a (Campaign CC gate round 1 Batch E): the Summary OVERVIEW /// listbox (0x10000400) authors a linked scrollbar via dat /// property 0x72 — live-DAT-probe-confirmed /// ScrollbarElementId=0x10000401, a SIBLING element under the /// Summary page root, not a descendant of the listbox itself. /// '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). /// [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( screen.FindElement(CharacterCreationUiController.SummaryPageElementId)); UiTemplateListBox list = Assert.IsType( UiElement.FindDescendant(summaryRoot, CharacterCreationSummaryPage.ListBoxId)); Assert.Equal(CharacterCreationSummaryPage.ScrollId, list.ScrollbarElementId); UiScrollbar overviewScroll = Assert.IsType( 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(); } /// /// R2-7b (Campaign CC gate round 1 Batch E): the how-to box's scrollbar /// THUMB only draws if (m.HasOverflow) /// ('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 /// — which is exactly what /// gates the thumb on — is true. /// [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(0x40000009u, out var font) && font is not null); var glyphs = new Dictionary(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"); } /// /// R3-1 (re-test 2): live-DAT pin for the wrap-mechanism fix's own /// premise — the Coordination attribute-slider label (0x100002ed /// under container 0x100003e8) authors OneLine=true (dat /// property 0x20), the SAME retail default a caption with no /// authored 0x20 resolves to for width-wrap purposes per /// UIElement_Text::CalcJustification's per-glyph gate (see /// 's own doc) — so this element /// must never width-wrap regardless of font metrics. /// [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( LayoutImporter.ImportInfos( dats, layoutId, CharacterCreationUiController.RootElementId)); ElementInfo coordContainer = Assert.IsType(FindInfo(rootInfo, 0x100003E8u)); ElementInfo coordLabel = Assert.IsType(FindInfo(coordContainer, 0x100002EDu)); Assert.True(coordLabel.TryGetEffectiveBool(0x20u, out bool oneLine) && oneLine); } /// /// 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", 0x100003f9) fits comfortably inside the button's /// own FULL authored width (never needing to wrap), and its value /// child (0x100002f3) 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 's /// own doc). /// [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( LayoutImporter.ImportInfos( dats, layoutId, CharacterCreationUiController.RootElementId)); var strings = new DatStringResolver(dats); ElementInfo skillsCredits = Assert.IsType(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(fontDid, out var font) && font is not null); var glyphs = new Dictionary(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); } /// /// R4-1 (re-test 3): the raw authored value-child X (116, pinned above) /// is NOT where the value actually draws — UiLayoutPolicy's /// raw-edge reflow (the value child's own Right-tracking edge modes /// against its base-inherited 150px OriginalParentWidth vs the /// Skills-credits button's actual 231px width) shifts it to X=197, /// landing right after the caption's own measured 193px span instead /// of colliding mid-caption ("Available Skill0Credits"). Health's own /// value child shares the SAME 150px OriginalParentWidth as its OWN /// actual 150px-wide button (no divergence), so it reflows to its /// byte-identical raw rect — proving the fix is additive, not a /// blanket shift. /// [InstalledDatFact] public void SkillsCreditsButton_ValueBoxReflowsPastCaption_HealthValueBoxUnchanged() { using var dats = new DatCollection(DatDirectory, DatAccessType.Read); uint layoutId = RetailDataIdResolver.Resolve( dats, CharacterCreationUiController.RootEnum, 5u); ImportedLayout screen = BuildSelected( dats, layoutId, CharacterCreationUiController.RootElementId); UiButton skillsCredits = Assert.IsType(screen.FindElement(0x100003F9u)); Assert.Equal((197f, 0f, 34f, 28f), skillsCredits.ValueBox); Assert.Equal(UiButton.LabelAlignment.Right, skillsCredits.ValueAlign); UiButton health = Assert.IsType(screen.FindElement(0x100003E3u)); Assert.Equal((116f, 0f, 34f, 28f), health.ValueBox); } /// /// R3-3 (re-test 2): the info-box title (0x100003fb) and /// description (0x100003fc) panes' own AUTHORED boxes overlap — /// this is why 's constructor /// forces both to VJustify.Top 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. /// [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( LayoutImporter.ImportInfos( dats, layoutId, CharacterCreationUiController.RootElementId)); ElementInfo title = Assert.IsType(FindInfo(rootInfo, 0x100003FBu)); ElementInfo description = Assert.IsType(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); } /// /// R4-3 (re-test 3): the description pane's own raw box (Y=460, /// H=100 -> bottom Y=560) extends PAST the bottom of the gold /// decorative frame that visually contains both info panes /// (0x100003fa, Y=430 H=110 -> bottom Y=540 — the SAME /// corner/edge sprite family GF-12 already renders, /// 0x100002de-e3/0x100000e8/0xea). Pins the /// geometric mismatch itself (so a future DAT re-extract that removes /// it is visible) — CharacterCreationSkillsPageTests' own fixture /// covers the constructor's Height-clamp behavior against this exact /// shape. /// [InstalledDatFact] public void SkillsInfoBoxFrame_ShorterThanDescriptionPane() { using var dats = new DatCollection(DatDirectory, DatAccessType.Read); uint layoutId = RetailDataIdResolver.Resolve( dats, CharacterCreationUiController.RootEnum, 5u); ElementInfo rootInfo = Assert.IsType( LayoutImporter.ImportInfos( dats, layoutId, CharacterCreationUiController.RootElementId)); ElementInfo frame = Assert.IsType(FindInfo(rootInfo, 0x100003FAu)); ElementInfo description = Assert.IsType(FindInfo(rootInfo, 0x100003FCu)); float frameBottom = frame.Y + frame.Height; float paneBottom = description.Y + description.Height; Assert.True( frameBottom < paneBottom, $"expected the frame's own bottom (Y={frame.Y} H={frame.Height}, bottom={frameBottom}) to sit " + $"ABOVE the description pane's own raw bottom (Y={description.Y} H={description.Height}, " + $"bottom={paneBottom}) — if it no longer does, CharacterCreationSkillsPage's Height clamp " + "may no longer be needed"); } /// /// R3-4/R3-7 (re-test 2): retail authors TWO distinct /// UIElement_Scrollbar thumb shapes. Chat's own scrollbar /// (0x10000012 under LayoutDesc 0x2100006f) is the /// 3-slice composite '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 (0x100003f8) /// 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 now resolves. /// [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(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); } /// /// R3-5/R3-6 (re-test 2): live-DAT pin for /// 's four hardcoded enum ids — /// resolves each through the SAME category-7 RetailDataIdResolver /// chain gmCGAppearancePage::DoColorSpots/DoGradDisk use, /// and pins the native dimensions those two decomp functions' own /// CreateLocalSurface 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). /// [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(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( 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(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(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); } /// /// R3-5: pixel-level ground truth for /// 's /// whole premise — the ACTIVE spot template (enum 0x1000000d) /// has a genuinely near-black CENTER region and a genuinely non-black /// RING/border region, and the BLANK template (enum 0x1000000f) /// has almost no black pixels at all (it is a DIFFERENT piece of art, /// not the spot with its center left un-recolored). /// [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(spotDid, out var spotRs) && spotRs is not null); Assert.True(dats.TryGet(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(layout.FindElement(elementId)); private static UiButton AssertButton(UiElement root, uint elementId) => Assert.IsType(UiElement.FindDescendant(root, elementId)); private static ImportedLayout BuildSelected( IDatReaderWriter dats, uint layoutDid, uint rootId) { ElementInfo info = Assert.IsType( LayoutImporter.ImportInfos(dats, layoutDid, rootId)); return LayoutImporter.Build( info, _ => (0u, 0, 0), null, null, new DatStringResolver(dats).Resolve); } /// /// R3-4/R3-5c (re-test 2): the chargen shade slider's own thumb child /// (0x10000321 > structural id 1) authors ONE DirectState /// sprite of its own (0x06004D50) and ZERO children — the /// EARLIER (Batch F11-era) investigation misread this as "genuinely /// nothing authored here" from a string.Join 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 's R3-4/R3-7 fix /// already handles (slices.Length == 0 -> ThumbSprite = /// DefaultImage(thumb)) — no separate fix was needed for the shade /// slider; this test proves the SAME code path already resolves it. /// [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(screen.FindElement(0x10000321u)); Assert.False(shadeSlider.Horizontal); Assert.NotEqual(0u, shadeSlider.ThumbSprite); } }