acdream/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs
Erik c721830e71
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
feat(ui): Slice 5.4 — the authored vendor browse panel (LayoutDesc 0x21000012)
The vendor window is retail's own: LayoutDesc 0x21000012, root
0x100000B7, found by enumerating all 101 layouts for the one
containing both known tab controls and clinched by the root's Type
0x10000017 — the literal UIElement::RegisterElementClass id for
gmVendorUI (pc:202075). Discovery evidence and the D0 read live in
the research doc's new §B.4.

D0 corrected two assumptions: retail's category "tabs" are a UiMenu
DROPDOWN fed by a hardcoded 18-row ordered category table (ported
bit-for-bit against our ItemType enum; list always scoped to exactly
one category, first-present wins, selection preserved across refresh
per retail's clamp), and the layout authors THREE tabs — Items
(browse, this slice), Buying and Selling (staged-transaction review,
Slice 6) — decision 4's "browse/Buy tab" names the Items tab retail's
mode-2 OpenTab opens. The non-default tabs render and switch pages
but stay inert, fenced in comments.

VendorUiController mounts Items: category dropdown, icon-cell item
row with the retained scrollbar, per-unit retail pricing via
VendorPricing.SellPrice (the vendor-stock path VendorProfile::
VendorSellPrice feeds), name/cost on selection. The panel is a pure
projection of VendorState — opens on populate, closes on clear; the
close button's VendorState.Close() is its only permitted mutation.
Nothing on the wire.

AP-110 narrowed (vendor leaves the absent-panels list); AP-161 files
the precise Slice-6 remainder (Buying/Selling unwired, Buy/Add
buttons, InqAcceptability). Twelve controller tests on a real-dat
fixture. Clean-room complete solution: 11,323 passed / 4 skipped /
0 failed.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-08-07 17:00:21 +02:00

207 lines
8.7 KiB
C#

using System.IO;
using System.Text.Json;
using AcDream.App.UI.Layout;
namespace AcDream.App.Tests.UI.Layout;
/// <summary>
/// Loads the committed layout ElementInfo fixtures and builds widget trees —
/// no dats required. Fixtures were generated from the real portal.dat and
/// serialized with <see cref="System.Text.Json"/>.
/// </summary>
public static class FixtureLoader
{
private static readonly JsonSerializerOptions _opts = new()
{
IncludeFields = true,
};
/// <summary>
/// Deserializes the committed <c>vitals_2100006C.json</c> fixture (copied to
/// the test output directory via the csproj <c>CopyToOutputDirectory</c> item)
/// into an <see cref="ElementInfo"/> tree, then builds and returns the
/// <see cref="ImportedLayout"/> using a null-returning sprite resolver and no
/// dat font — sufficient for conformance checks on tree structure and slice ids.
/// </summary>
public static ImportedLayout LoadVitals()
{
var root = LoadVitalsInfos();
return LayoutImporter.Build(root, _ => (0u, 0, 0), null);
}
/// <summary>
/// Deserializes the committed <c>vitals_2100006C.json</c> fixture into a raw
/// <see cref="ElementInfo"/> tree WITHOUT calling <see cref="LayoutImporter.Build"/>.
/// Use this when the test needs to inspect the resolved <see cref="ElementInfo"/>
/// tree directly (e.g. inheritance-resolution checks) without exercising the
/// widget factory.
/// </summary>
public static AcDream.App.UI.Layout.ElementInfo LoadVitalsInfos()
=> LoadInfos("vitals_2100006C.json");
/// <summary>
/// Deserializes the committed <c>chat_21000006.json</c> fixture into a raw
/// <see cref="ElementInfo"/> tree and builds the <see cref="ImportedLayout"/>
/// using a null-returning sprite resolver and no dat font — sufficient for
/// conformance checks on tree structure and resolved types.
/// </summary>
public static ImportedLayout LoadChat()
=> LayoutImporter.Build(LoadChatInfos(), _ => (0u, 0, 0), null);
/// <summary>
/// Deserializes the committed <c>chat_21000006.json</c> fixture into a raw
/// <see cref="ElementInfo"/> tree WITHOUT calling <see cref="LayoutImporter.Build"/>.
/// Use this when the test needs to inspect the resolved <see cref="ElementInfo"/>
/// tree directly (e.g. resolved Type values per element id).
/// </summary>
public static AcDream.App.UI.Layout.ElementInfo LoadChatInfos()
=> LoadInfos("chat_21000006.json");
/// <summary>Builds the committed retail radar LayoutDesc 0x21000074 fixture.</summary>
public static ImportedLayout LoadRadar()
=> LayoutImporter.Build(LoadRadarInfos(), _ => (0u, 0, 0), null);
/// <summary>Returns the resolved ElementInfo tree for retail radar LayoutDesc 0x21000074.</summary>
public static AcDream.App.UI.Layout.ElementInfo LoadRadarInfos()
=> LoadInfos("radar_21000074.json");
public static ImportedLayout LoadToolbar()
=> LayoutImporter.Build(LoadToolbarInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadToolbarInfos()
=> LoadInfos("toolbar_21000016.json");
public static ImportedLayout LoadInventory()
=> LayoutImporter.Build(LoadInventoryInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadInventoryInfos()
=> LoadInfos("inventory_21000023.json");
public static ImportedLayout LoadPaperdoll()
=> LayoutImporter.Build(LoadPaperdollInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadPaperdollInfos()
=> LoadInfos("paperdoll_21000024.json");
public static ImportedLayout LoadCharacter()
=> LayoutImporter.Build(LoadCharacterInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadCharacterInfos()
=> LoadInfos("character_2100002E.json");
public static ImportedLayout LoadCombat()
=> LayoutImporter.Build(LoadCombatInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadCombatInfos()
=> LoadInfos("combat_21000073.json");
public static ImportedLayout LoadSpellbook()
=> LayoutImporter.Build(LoadSpellbookInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadSpellbookInfos()
=> LoadInfos("spellbook_21000034.json");
public static ElementInfo LoadComponentCategoryTemplateInfos()
=> LoadInfos("component_category_21000033_10000466.json");
public static ElementInfo LoadComponentRowTemplateInfos()
=> LoadInfos("component_row_21000033_10000467.json");
public static ImportedLayout LoadExamination()
=> LayoutImporter.Build(LoadExaminationInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadExaminationInfos()
=> LoadInfos("examine_2100006B_100005F2.json");
public static ElementInfo LoadExaminationRowTemplateInfos()
=> LoadInfos("examine_row_2100006B_10000166.json");
public static ElementInfo LoadExaminationComponentTemplateInfos()
=> LoadInfos("examine_component_2100006B_1000032E.json");
public static ImportedLayout LoadPowerbar()
=> LayoutImporter.Build(LoadPowerbarInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadPowerbarInfos()
=> LoadInfos("powerbar_21000072.json");
public static ImportedLayout LoadConfirmationDialog()
=> LayoutImporter.Build(LoadConfirmationDialogInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadConfirmationDialogInfos()
=> LoadInfos("dialogs_2100003C.json");
public static ImportedLayout LoadFpsDisplay()
=> LayoutImporter.Build(LoadFpsDisplayInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadFpsDisplayInfos()
=> LoadInfos("smartbox_fps_2100000F.json");
public static ImportedLayout LoadPositiveEffects()
=> LayoutImporter.Build(LoadPositiveEffectsInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadPositiveEffectsInfos()
=> LoadInfos("effects_positive_2100001B.json");
public static ImportedLayout LoadNegativeEffects()
=> LayoutImporter.Build(LoadNegativeEffectsInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadNegativeEffectsInfos()
=> LoadInfos("effects_negative_2100001B.json");
public static ElementInfo LoadEffectRowTemplateInfos()
=> LoadInfos("effects_row_2100001B_10000128.json");
public static ImportedLayout LoadCharacterInformation()
=> LayoutImporter.Build(
LoadCharacterInformationInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadCharacterInformationInfos()
=> LoadInfos("character_info_2100006E_10000183.json");
public static ImportedLayout LoadIndicators()
=> LayoutImporter.Build(LoadIndicatorsInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadIndicatorsInfos()
=> LoadInfos("indicators_21000071.json");
public static ImportedLayout LoadLinkStatus()
=> LayoutImporter.Build(LoadLinkStatusInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadLinkStatusInfos()
=> LoadInfos("link_status_2100001D.json");
public static ImportedLayout LoadVitae()
=> LayoutImporter.Build(LoadVitaeInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadVitaeInfos()
=> LoadInfos("vitae_21000020.json");
public static ImportedLayout LoadMiniGame()
=> LayoutImporter.Build(LoadMiniGameInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadMiniGameInfos()
=> LoadInfos("mini_game_2100001E.json");
public static ImportedLayout LoadVendor()
=> LayoutImporter.Build(LoadVendorInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadVendorInfos()
=> LoadInfos("vendor_21000012_100000B7.json");
// ── Shared loader ────────────────────────────────────────────────────────
private static AcDream.App.UI.Layout.ElementInfo LoadInfos(string fileName)
{
var path = Path.Combine(AppContext.BaseDirectory, "UI", "Layout", "fixtures", fileName);
if (!File.Exists(path)) throw new FileNotFoundException($"fixture not found at: {path}");
var bytes = File.ReadAllBytes(path);
// Strip UTF-8 BOM (EF BB BF) if present so JsonSerializer.Deserialize<T>(ReadOnlySpan<byte>)
// does not reject the first byte.
ReadOnlySpan<byte> span = bytes;
if (span.Length >= 3 && span[0] == 0xEF && span[1] == 0xBB && span[2] == 0xBF)
span = span[3..];
return JsonSerializer.Deserialize<AcDream.App.UI.Layout.ElementInfo>(span, _opts)
?? throw new InvalidOperationException($"fixture deserialized to null: {path}");
}
}