using System.IO; using System.Linq; 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)); } /// 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)); } /// 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", ]; 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); } // 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)); } 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); } }