acdream/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs
Erik 74c3d85d37 test(ui): FA3 -- social panel fixture dump, live-mount probe, conformance tests
- RetailLayoutFixtureGenerator dumps LayoutDesc 0x2100006E slot
  0x1000018F to the committed social_panel_2100006E_1000018F.json
  fixture, closing lane-A unknowns U3/U4/U6/U7/U10 with real DAT data
  (geometry/media/fonts/base refs, the fellowship empty/full frame
  containment, the panel/page P0x57 properties, the row-template arrays).
- SocialPanelLiveMountProbeTests exercises the PRODUCTION mount path
  against the live DATs (ACDREAM_PROBE_LIVE_MOUNT=1): the tab host
  resolves as UiTabPanel with its 4-entry table, all four pages resolve,
  the fellowship frame pair and allegiance signature elements resolve,
  0x10000492 is confirmed authored twice under the allegiance page, and
  every tab button caption is non-empty (the #375 resolver class).
- SocialPanelControllerTests pins the fixture-driven conformance: the
  real (coordinator-addendum-correcting) tab table and default entry,
  the Fellowship/Allegiance empty-state gates, Friends/Squelch row
  population and revision-driven rebuilds, and the D1 INERT-button
  contract (AD-79) via
  FriendsAndSquelchActionButtons_AreClickable_ButHaveNoHandler.
- RetailPanelCatalogTests gains the SocialPanel id/window-name/
  Mounted-not-Toolbar pins (lane A §6.1: no toolbar button).

Note: the ACDREAM_REGENERATE_UI_FIXTURES=1 run used to produce the new
fixture also touched keyboard_config_21000009.json and
options_2100002B.json on this machine (unrelated installed-DAT drift,
likely from local DAT-editing tooling) -- both were reverted to HEAD
before this commit; only the new social panel fixture is included.

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

299 lines
14 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_2100006f.json</c> fixture (retail's
/// ACTUAL main chat window, LayoutDesc <c>0x2100006F</c> — Campaign CH slice
/// CH6a; the old <c>chat_21000006.json</c> fixture imported the WRONG,
/// unrelated layout) 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_2100006f.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_2100006f.json");
/// <summary>
/// Deserializes the committed <c>chat_floaty_2100005b.json</c> fixture
/// (retail's floating chat windows 1-4, LayoutDesc <c>0x2100005B</c> — CH6a/b
/// REJECT-review SHOULD-FIX 4) 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 resolved
/// widget types.
/// </summary>
public static ImportedLayout LoadFloatyChat()
=> LayoutImporter.Build(LoadFloatyChatInfos(), _ => (0u, 0, 0), null);
/// <summary>
/// Deserializes the committed <c>chat_floaty_2100005b.json</c> fixture into
/// a raw <see cref="ElementInfo"/> tree WITHOUT calling
/// <see cref="LayoutImporter.Build"/>.
/// </summary>
public static AcDream.App.UI.Layout.ElementInfo LoadFloatyChatInfos()
=> LoadInfos("chat_floaty_2100005b.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");
/// <summary>Options panel LayoutDesc <c>0x2100002B</c> — the tab control + all four
/// mounted pages (via BaseLayoutId/BaseElement) + the seven row-template elements,
/// as one combined tree (Campaign OP slice OP2).</summary>
public static ImportedLayout LoadOptionsPanel()
=> LayoutImporter.Build(LoadOptionsPanelInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadOptionsPanelInfos()
=> LoadInfos("options_2100002B.json");
/// <summary>Gameplay Options page LayoutDesc <c>0x2100002A</c> (standalone import).</summary>
public static ImportedLayout LoadOptionsGameplay()
=> LayoutImporter.Build(LoadOptionsGameplayInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadOptionsGameplayInfos()
=> LoadInfos("options_gameplay_2100002A.json");
/// <summary>Character page LayoutDesc <c>0x21000028</c> (standalone import).</summary>
public static ImportedLayout LoadOptionsCharacter()
=> LayoutImporter.Build(LoadOptionsCharacterInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadOptionsCharacterInfos()
=> LoadInfos("options_character_21000028.json");
/// <summary>Chat page LayoutDesc <c>0x2100005C</c> (standalone import).</summary>
public static ImportedLayout LoadOptionsChat()
=> LayoutImporter.Build(LoadOptionsChatInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadOptionsChatInfos()
=> LoadInfos("options_chat_2100005C.json");
/// <summary>Config page LayoutDesc <c>0x21000029</c> (standalone import).</summary>
public static ImportedLayout LoadOptionsConfig()
=> LayoutImporter.Build(LoadOptionsConfigInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadOptionsConfigInfos()
=> LoadInfos("options_config_21000029.json");
/// <summary>The Options panel resolved through its retail host — LayoutDesc
/// <c>0x2100006E</c> (<c>gmFloatyPanelUI</c>) at slot <c>0x1000018D</c> (stack
/// key 10, <see cref="AcDream.App.UI.RetailPanelCatalog.Options"/>) — the
/// SAME catalog-import shape <c>CharacterController</c>'s own
/// <c>character_info_2100006E_10000183.json</c> fixture uses. This is what
/// <see cref="OptionsPanelController.Bind"/> actually mounts (Campaign OP
/// slice OP3), distinct from <see cref="LoadOptionsPanel"/> above (the
/// standalone <c>0x2100002B</c> import, which OP2 pinned for widget-mapping
/// conformance).</summary>
public static ImportedLayout LoadOptionsPanelHost()
=> LayoutImporter.Build(LoadOptionsPanelHostInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadOptionsPanelHostInfos()
=> LoadInfos("options_panel_2100006E_1000018D.json");
/// <summary>The four-tab social panel (Friends/Allegiance/Fellowship/
/// Squelch) host slot <c>0x1000018F</c> — Campaign FA slice FA3. This is
/// what <see cref="SocialPanelController.Bind"/> actually mounts.</summary>
public static ImportedLayout LoadSocialPanelHost()
=> LayoutImporter.Build(LoadSocialPanelHostInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadSocialPanelHostInfos()
=> LoadInfos("social_panel_2100006E_1000018F.json");
/// <summary>Configure Keyboard screen LayoutDesc <c>0x21000009</c> (standalone
/// import — its own separate full-screen window, NOT nested under the Options
/// panel's <c>0x2100006E</c> host — Campaign OP slice OP8).</summary>
public static ImportedLayout LoadKeyboardConfig()
=> LayoutImporter.Build(LoadKeyboardConfigInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadKeyboardConfigInfos()
=> LoadInfos("keyboard_config_21000009.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}");
}
}