acdream/tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs
Erik 6114b2dda2 fix(chargen): Campaign CC CC6b-MOUNT re-review residuals R1-R3 + nits — REVIEW-CLOSED
R1: LivePresentationComposition's chargen-preview diagnostic fired on every
ordinary launch without ACDREAM_RETAIL_UI set, since interaction.RetainedUi
is null in that configuration and there's no Appearance page to warn about.
Narrowed the else-if guard to require RetainedUi is not null too, so the
diagnostic only fires in the one configuration it actually diagnoses.

R2: filed AP-221 for the F8 one-shot-binding disposition the re-reviewer
accepted as scoped but which shipped without its own register row — the
chargen preview's GPU-side binding reads the retryable mount coordinator's
widget exactly once, so a slow-DAT frame permanently kills the preview for
the session with only R1's diagnostic as evidence.

R3: rewrote AP-217 after re-deriving from the decomp. The original row
claimed the GradCircle was an interactive click-to-hue picker with no
handler wired up. gmCGAppearancePage::ListenToElementMessage's dispatch
switch has no case for the GradCircle's offset at all — it isn't a click
target in retail either. DoGradDisk is a paint-only routine that blits the
gradient art tinted with the current color (or blanks it for Eyes)
whenever SetColor/SetSelection run. acdream's real gap is that it never
repaints the GradCircle — a cosmetic paint gap, not a dead control.

N1: tightened AP-220's "leaving Gearknight for something else" — the
decomp shows leaving Gearknight for Olthoi/OlthoiAcid takes a separate
branch that does not randomize; only leaving for a non-Olthoi heritage
does.

N2: added the requested media pin to the F2 spin-highlight live-DAT test,
then measured it against the installed EoR dat rather than assuming it
would pass. It doesn't: none of the nine spins author Highlight-state
media on either consumed arrow face segment, so
TrySetRetailState(Highlight) is a silent no-op for all of them today.
Pinned the test to the measured reality (ActiveState stays "Normal") and
filed AP-222 documenting the discovery — unresolved whether retail's own
spin art has the same gap.

Plan doc: CC6b-MOUNT ledger row updated to REVIEW-CLOSED with the full
commit chain and re-review disposition; OWED list corrected for AP-217/
AP-222.

Gates: dotnet build -c Release green. App suite 5223 passed / 3 skipped
(ACDREAM_PROBE_LIVE_MOUNT=1, ACDREAM_DAT_DIR set) — count held exactly at
baseline. Runtime suite 1713/0 — unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 22:58:19 +02:00

538 lines
25 KiB
C#

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;
/// <summary>
/// Installed-retail-DAT acceptance gate for Campaign CC slice CC4. Opt in
/// with <c>ACDREAM_PROBE_LIVE_MOUNT=1</c>; <c>ACDREAM_DAT_DIR</c> can
/// override the ordinary Documents/Asheron's Call location. Mirrors
/// <see cref="CharacterManagementLiveDatTests"/>'s pattern: sweeps the
/// authored master-shell and page ids the campaign plan and CC4's own
/// decomp research cite, pinning them against the real installed layout.
/// </summary>
public sealed class CharacterCreationLiveDatTests
{
private static string DatDirectory =>
Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents",
"Asheron's Call");
[InstalledDatFact]
public void EnumTable5_ResolvesTheMasterShellRootAndChildren()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats,
CharacterCreationUiController.RootEnum,
5u);
Assert.NotEqual(0u, layoutId);
Console.WriteLine(
$"[CC4-DAT] category=5 enum=0x10000039 -> DID=0x{layoutId:X8}");
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
Assert.Equal(
CharacterCreationUiController.RootElementId,
screen.Root.DatElementId);
// CC4 re-review R5: the fixed-canvas arbiter THROWS if two concurrent
// screens declare different sizes, and char-management's root is
// DAT-pinned at 800x600 (CharacterManagementLiveDatTests). Chargen
// declares on top of it on the exact user-gate path, so its authored
// extent must be pinned too — an unequal extent is now a crash at
// Open(), not a cosmetic drift. Observe, don't infer (C4 closeout).
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
LayoutImporter.ImportInfos(
dats,
layoutId,
CharacterCreationUiController.RootElementId));
Assert.Equal(800f, rootInfo.Width);
Assert.Equal(600f, rootInfo.Height);
Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.ProgressBarElementId));
AssertButton(screen, CharacterCreationUiController.BackElementId);
AssertButton(screen, CharacterCreationUiController.NextElementId);
AssertButton(screen, CharacterCreationUiController.FinishElementId);
AssertButton(screen, CharacterCreationUiController.HelpElementId);
AssertButton(screen, CharacterCreationUiController.ExitElementId);
AssertButton(screen, CharacterCreationUiController.RandomElementId);
Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.MasterPageElementId));
foreach (uint pageId in new[]
{
CharacterCreationUiController.HeritagePageElementId,
CharacterCreationUiController.ProfessionPageElementId,
CharacterCreationUiController.SkillsPageElementId,
CharacterCreationUiController.AppearancePageElementId,
CharacterCreationUiController.TownPageElementId,
CharacterCreationUiController.SummaryPageElementId,
})
{
Assert.IsAssignableFrom<UiElement>(screen.FindElement(pageId));
}
AssertButton(screen, CharacterCreationUiController.HeritageTabElementId);
AssertButton(screen, CharacterCreationUiController.ProfessionTabElementId);
AssertButton(screen, CharacterCreationUiController.SkillsTabElementId);
AssertButton(screen, CharacterCreationUiController.AppearanceTabElementId);
AssertButton(screen, CharacterCreationUiController.TownTabElementId);
AssertButton(screen, CharacterCreationUiController.SummaryTabElementId);
}
[InstalledDatFact]
public void MountsThroughTheControllerAgainstLiveResources()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats,
CharacterCreationUiController.RootEnum,
5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
var host = new UiRoot();
var dialogs = MakeDialogFactory(dats, host);
var bindings = new CharacterCreationRuntimeBindings(
() => null,
_ => default,
_ => default,
_ => default,
(_, _) => default,
(_, _) => default,
_ => default,
_ => default,
_ => default,
_ => default,
_ => default,
() => { });
UiElement? ResolveTemplate(uint templateLayoutId, uint templateElementId) =>
LayoutImporter.Import(
dats, templateLayoutId, templateElementId, _ => (0u, 0, 0), null)?.Root;
CharacterCreationUiController? controller =
CharacterCreationUiController.CreateDetached(
host, screen, ResolveTemplate, dialogs, bindings,
new CharacterCreationUiController.DialogStrings("Are you sure?"));
Assert.NotNull(controller);
controller!.AttachAndTick();
controller.Dispose();
dialogs.Dispose();
}
/// <summary>13 heritage buttons, the description text, all present as
/// authored (Heritage page — <c>gmCGHeritagePage::InitializePage @
/// 0x00483a10</c>).</summary>
[InstalledDatFact]
public void HeritagePage_HasAllThirteenRaceButtonsAndDescriptionText()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats,
CharacterCreationUiController.RootEnum,
5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement heritageRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.HeritagePageElementId));
uint[] heritageButtonIds =
[
0x100003BFu, 0x100003C1u, 0x100003C2u, 0x100003C3u,
0x10000590u, 0x100005A9u, 0x100005E8u, 0x100005F1u,
0x100005C4u, 0x10000591u, 0x100005BFu, 0x100005C7u,
0x100005C8u,
];
foreach (uint buttonId in heritageButtonIds)
{
Assert.IsType<UiButton>(
UiElement.FindDescendant(heritageRoot, buttonId));
}
Assert.IsType<UiText>(
UiElement.FindDescendant(heritageRoot, 0x100003C4u));
}
/// <summary>Seven template buttons, six attribute sliders (each with a
/// lock button + scrollbar + value text), and the four derived
/// displays (Profession page —
/// <c>gmCGProfessionPage::InitializePage @ 0x00482d50</c>).</summary>
[InstalledDatFact]
public void ProfessionPage_HasTemplateButtonsSlidersAndDisplays()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats,
CharacterCreationUiController.RootEnum,
5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement professionRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.ProfessionPageElementId));
uint[] templateButtonIds =
[
0x100003D9u, 0x100003DAu, 0x100003DBu,
0x100003DCu, 0x100003DDu, 0x100003DEu, 0x100003DFu,
];
foreach (uint buttonId in templateButtonIds)
{
Assert.IsType<UiButton>(
UiElement.FindDescendant(professionRoot, buttonId));
}
uint[] sliderContainerIds =
[
0x100003E6u, 0x100003E7u, 0x100003E8u,
0x100003E9u, 0x100003EAu, 0x100003EBu,
];
foreach (uint containerId in sliderContainerIds)
{
UiElement container = Assert.IsAssignableFrom<UiElement>(
UiElement.FindDescendant(professionRoot, containerId));
Assert.IsType<UiScrollbar>(
UiElement.FindDescendant(container, 0x100002EEu));
// The value display authors as an editable Type-12 (retail's
// NumberInputFilter) — DatWidgetFactory maps that to UiField,
// not UiText. See CharacterCreationProfessionPage's ctor comment.
Assert.IsType<UiField>(
UiElement.FindDescendant(container, 0x100002EFu));
}
// Avail/health/stamina/mana each author as a Button whose Type-12
// value child is swallowed by UiButton.ConsumesDatChildren — the
// same substitution the Skills page's credits meter needed. See
// CharacterCreationProfessionPage's ctor comment.
foreach (uint containerId in new[]
{ 0x100003E2u, 0x100003E3u, 0x100003E4u, 0x100003E5u })
{
Assert.IsType<UiButton>(
UiElement.FindDescendant(professionRoot, containerId));
}
}
/// <summary>Skills listbox, credits meter, info panes (Skills page —
/// <c>gmCGSkillsPage::InitializePage @ 0x00481dd0</c>).</summary>
[InstalledDatFact]
public void SkillsPage_HasListboxCreditsAndInfoPanes()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats,
CharacterCreationUiController.RootEnum,
5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement skillsRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.SkillsPageElementId));
Assert.IsType<UiTemplateListBox>(
UiElement.FindDescendant(skillsRoot, 0x100003F7u));
// The credits meter (decomp id 0x100002f3) authors as a raw dat
// child of button 0x100003f9; UiButton.ConsumesDatChildren swallows
// it before it becomes an addressable widget, so the faithful
// acdream substitute is the button's own Label — see
// CharacterCreationSkillsPage's ctor comment.
Assert.IsType<UiButton>(
UiElement.FindDescendant(skillsRoot, 0x100003F9u));
Assert.IsType<UiText>(
UiElement.FindDescendant(skillsRoot, 0x100003FBu));
Assert.IsType<UiText>(
UiElement.FindDescendant(skillsRoot, 0x100003FCu));
}
/// <summary>Four town buttons + description text (Town page —
/// <c>gmCGTownPage::InitializePage @ 0x0047c6d0</c>).</summary>
[InstalledDatFact]
public void TownPage_HasFourStarterAreaButtons()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats,
CharacterCreationUiController.RootEnum,
5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement townRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.TownPageElementId));
foreach (uint buttonId in new[] { 0x1000040Bu, 0x1000040Du, 0x1000040Eu, 0x1000040Fu })
{
Assert.IsType<UiButton>(
UiElement.FindDescendant(townRoot, buttonId));
}
Assert.IsType<UiText>(
UiElement.FindDescendant(townRoot, 0x10000409u));
}
/// <summary>The exit-warning + all per-heritage/per-town DAT string
/// keys this slice cites actually resolve in the installed table.
/// </summary>
[InstalledDatFact]
public void RequiredChargenStringsResolve()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
var strings = new DatStringResolver(dats);
const uint table = 0x23000002u;
string[] keys =
[
"ID_CharGen_ExitWarning",
"ID_CharGen_Heritage_StartingSkills_Header",
"ID_CharGen_Heritage_StartingSkills",
"ID_CharGen_Heritage_BonusSkills_Trained_Header",
"ID_CharGen_AluvianText_BonusSkills_Trained",
"ID_CharGen_GaruText_BonusSkills_Trained",
"ID_CharGen_ShoText_BonusSkills_Trained",
"ID_CharGen_ViaText_BonusSkills_Trained",
"ID_CharGen_ShadText_BonusSkills_Trained",
"ID_CharGen_GearText_BonusSkills_Trained",
"ID_CharGen_AunTText_BonusSkills_Trained",
"ID_CharGen_EmpText_BonusSkills_Trained",
"ID_CharGen_UndText_BonusSkills_Trained",
"ID_CharGen_TownHowTo",
"ID_CharGen_HoltText",
"ID_CharGen_ShoushiText",
"ID_CharGen_YaraqText",
"ID_CharGen_SanamarText",
];
foreach (string key in keys)
{
string? resolved = strings.Resolve(table, DatStringResolver.ComputeHash(key));
Assert.True(resolved is not null, $"missing string: {key}");
}
}
/// <summary>
/// Campaign CC slice CC6b-MOUNT — the Appearance page's full authored
/// widget catalog. Pins the campaign plan's risk item 4 finding (the
/// color-wheel/gradient family resolves through EXISTING
/// <c>DatWidgetFactory</c> mappings; no new widget type was needed —
/// see <see cref="CharacterCreationAppearancePage"/>'s own class doc)
/// against the real installed DAT: gender/Face/Clothes buttons, all
/// nine spins, all nine color swatches, the shade scrollbar, and the
/// viewport.
/// </summary>
[InstalledDatFact]
public void AppearancePage_HasGenderChoiceSpinsSwatchesShadeAndViewport()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement appearanceRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.AppearancePageElementId));
AssertButton(appearanceRoot, CharacterCreationAppearancePage.FemaleButtonId);
AssertButton(appearanceRoot, CharacterCreationAppearancePage.MaleButtonId);
AssertButton(appearanceRoot, CharacterCreationAppearancePage.FaceButtonId);
AssertButton(appearanceRoot, CharacterCreationAppearancePage.ClothesButtonId);
Assert.IsAssignableFrom<UiElement>(
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.FaceChoicesId));
Assert.IsAssignableFrom<UiElement>(
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.ClothesChoicesId));
foreach (uint spinId in new[]
{
CharacterCreationAppearancePage.HairSpinId,
CharacterCreationAppearancePage.EyesSpinId,
CharacterCreationAppearancePage.NoseSpinId,
CharacterCreationAppearancePage.MouthSpinId,
CharacterCreationAppearancePage.SkinSpinId,
CharacterCreationAppearancePage.HeadgearSpinId,
CharacterCreationAppearancePage.ShirtSpinId,
CharacterCreationAppearancePage.TrousersSpinId,
CharacterCreationAppearancePage.FootwearSpinId,
})
{
UiButton spin = AssertButton(appearanceRoot, spinId);
// Fix round F2 item 2: the current-part highlight
// (TrySetRetailState(Normal/Highlight)) only has a visible
// effect through UiButton's ToggleBehavior branch when the
// authored spin actually sets DAT property 0x0B — measured
// (not assumed, matching this file's own discipline for the
// arrow geometry above) True for all nine spins against the
// installed EoR dat. Pinned so a future DAT revision that
// drops it shows up here instead of as a silently-dead
// highlight.
Assert.True(spin.ToggleBehavior, $"spin 0x{spinId:X8} must author ToggleBehavior for the current-part highlight to work.");
// Re-review nit N2 (2026-08-15): ToggleBehavior alone is necessary
// but not sufficient — UiButton.UpdateVisualState only COMMITS the
// requested state when _availableStates actually contains it
// (UiButton.cs's TrySetRetailState -> Selected setter ->
// UpdateVisualState chain). MEASURED (not assumed) against the
// installed EoR dat: TrySetRetailState(Highlight) itself always
// reports success (the ToggleBehavior branch commits
// unconditionally, matching TrySetRetailState's own contract),
// but NONE of the nine spins actually carries Highlight /
// Highlight_rollover / Highlight_pressed media on either of
// their two consumed arrow face segments — every one of them
// authors only Normal / Normal_rollover / Ghosted. So F2 item 2's
// current-part highlight is CURRENTLY A NO-OP for every spin:
// ActiveState silently stays at its prior value ("Normal")
// instead of ever becoming "Highlight". The pre-existing
// ToggleBehavior pin above never caught this because it only
// checks the PROPERTY that gates the state-machine branch, not
// whether that branch has anything to actually draw. Filed as
// AP-222 (retail-vs-acdream status unresolved — retail's own
// gmCGAppearancePage::SetSelection call sites are cited for
// the SetState(1)/SetState(6) calls, not for whether retail's
// OWN spin art authors Highlight media either). Pinned to
// "Normal" so a future DAT revision that adds real Highlight
// media is what makes this assertion start failing — the
// correct trigger to update it to "Highlight" instead of a
// silently-reintroduced dead highlight going unnoticed either way.
Assert.True(
spin.TrySetRetailState(UiButtonStateMachine.Highlight),
$"spin 0x{spinId:X8} must accept a Highlight state request.");
Assert.Equal("Normal", spin.ActiveState);
}
// Every color-wheel-family id resolves through EXISTING
// DatWidgetFactory mappings (Button=1, Scrollbar=0xB, the generic
// Type-3 fallback) — the risk-item-4 scouting result, pinned.
foreach (uint swatchId in CharacterCreationAppearancePage.SwatchIds)
AssertButton(appearanceRoot, swatchId);
UiScrollbar shadeScroll = Assert.IsType<UiScrollbar>(
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.ShadeScrollId));
// Fix round F11: measured (not assumed) against the installed EoR
// dat — the shade scrollbar (0x10000321) is authored VERTICAL
// (33x85, taller than wide). Before this fix, UiScrollbar.OnEvent
// only routed to ScalarChanged when Horizontal was true, so mouse
// input on this control never reached SetShadeFromScalar in
// production. Pinned so a future DAT revision that flips this
// orientation is caught here rather than silently reintroducing the
// dead-input bug (UiScrollbar's OnVerticalScalarEvent handles this
// orientation now, but ONLY this orientation gets exercised in
// production).
Assert.False(shadeScroll.Horizontal);
Assert.True(shadeScroll.Height > shadeScroll.Width);
Assert.IsType<UiDatElement>(
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.GradCircleId));
AssertButton(appearanceRoot, CharacterCreationAppearancePage.RotateClockwiseId);
AssertButton(appearanceRoot, CharacterCreationAppearancePage.RotateCounterClockwiseId);
AssertButton(appearanceRoot, CharacterCreationAppearancePage.ZoomInId);
AssertButton(appearanceRoot, CharacterCreationAppearancePage.ZoomOutId);
Assert.IsType<UiViewport>(
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.ViewportId));
}
/// <summary>
/// Live-DAT-measured arrow geometry the page's spin OnClickAt zones are
/// built from — every one of the nine spins is uniformly 200px wide
/// with the two locally-reused arrow child ids
/// (<c>0x1000030a</c> decrement / <c>0x1000030b</c> increment) at the
/// SAME local positions. If a future DAT revision changes this shared
/// template's geometry, this test (not a silent behavior change) is
/// where it shows up.
/// </summary>
[InstalledDatFact]
public void AppearancePage_SpinArrowGeometryIsUniformAcrossAllNineSpins()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
LayoutImporter.ImportInfos(
dats, layoutId, CharacterCreationUiController.RootElementId));
ElementInfo? appearanceInfo = FindInfo(
rootInfo, CharacterCreationUiController.AppearancePageElementId);
Assert.NotNull(appearanceInfo);
foreach (uint spinId in new[]
{
CharacterCreationAppearancePage.HairSpinId,
CharacterCreationAppearancePage.EyesSpinId,
CharacterCreationAppearancePage.NoseSpinId,
CharacterCreationAppearancePage.MouthSpinId,
CharacterCreationAppearancePage.SkinSpinId,
CharacterCreationAppearancePage.HeadgearSpinId,
CharacterCreationAppearancePage.ShirtSpinId,
CharacterCreationAppearancePage.TrousersSpinId,
CharacterCreationAppearancePage.FootwearSpinId,
})
{
ElementInfo? spin = FindInfo(appearanceInfo!, spinId);
Assert.NotNull(spin);
Assert.Equal(200f, spin!.Width);
ElementInfo? decrement = spin.Children.FirstOrDefault(c => c.Id == 0x1000030Au);
ElementInfo? increment = spin.Children.FirstOrDefault(c => c.Id == 0x1000030Bu);
Assert.NotNull(decrement);
Assert.NotNull(increment);
Assert.Equal(80f, decrement!.X);
Assert.Equal(127f, increment!.X);
// Fix round F10: the arrow WIDTHS are what actually derive
// CharacterCreationAppearancePage.IncrementZoneEnd (174 =
// IncrementZoneStart 127 + this measured 47px width) — X alone
// pins the LEFT edge of each zone, not where the increment zone
// ends and the select-as-current-part body zone begins. Measured
// against the installed EoR dat: both arrows are 47px wide,
// uniformly, across all nine spins.
Assert.Equal(47f, decrement.Width);
Assert.Equal(47f, increment.Width);
}
}
private static ElementInfo? FindInfo(ElementInfo node, uint id)
{
if (node.Id == id) return node;
foreach (ElementInfo child in node.Children)
{
ElementInfo? found = FindInfo(child, id);
if (found is not null) return found;
}
return null;
}
private static RetailDialogFactory MakeDialogFactory(IDatReaderWriter dats, UiRoot host)
{
uint dialogDid = RetailDataIdResolver.Resolve(dats, 2u, 5u);
ImportedLayout? CreateLayout(RetailDialogType type)
{
uint rootElementId = RetailDialogFactory.RootElementId(type);
return rootElementId == 0u
? null
: LayoutImporter.Import(
dats, dialogDid, rootElementId, _ => (0u, 0, 0), null);
}
return new RetailDialogFactory(host, CreateLayout);
}
private static void AssertButton(ImportedLayout layout, uint elementId) =>
Assert.IsType<UiButton>(layout.FindElement(elementId));
private static UiButton AssertButton(UiElement root, uint elementId) =>
Assert.IsType<UiButton>(UiElement.FindDescendant(root, elementId));
private static ImportedLayout BuildSelected(
IDatReaderWriter dats,
uint layoutDid,
uint rootId)
{
ElementInfo info = Assert.IsType<ElementInfo>(
LayoutImporter.ImportInfos(dats, layoutDid, rootId));
return LayoutImporter.Build(
info,
_ => (0u, 0, 0),
null,
null,
new DatStringResolver(dats).Resolve);
}
}