Mounts retail's Options panel (LayoutDesc 0x2100002B resolved through host 0x2100006E slot 0x1000018D, gmPanelUI key 10) via the same catalog-import pattern CharacterController already validates, registered through RetailPanelUiController so it shares retail's "one active gmPanelUI child" mutual exclusion with every other sibling panel for free. F11 and the toolbar's options button (0x1000019B, already authoring panel id 10) both now open it; the close button fires the same ToggleOptionsPanel action. OptionPageModel (OptionPage/BoolOptionRow) ports retail's exact Apply/Reset/Defaults/visibility semantics from UIOption_Checkbox/PlayerOptionPage — LED clicks apply live immediately, Apply commits every row unconditionally + flushes the batched blob, Reset reverts only Changed rows, Defaults restores without committing, and tab-switch/window-hide revert uncommitted edits. Wired for all four tabs; this slice registers real rows on none of them (Gameplay authentically has none — a pure button list). UiTabPanel gains an ActivePageChanged event so the page model can hook every tab transition, including the initial default-tab activation. The seven Gameplay-tab buttons: Exit Game reuses the existing graceful window-close path; Exit to Character Selection gets retail's confirmation dialog and byte-verified mid-air refusal but still behaves as Exit Game (AD-74 — no pre-world character-select flow exists); Configure Keyboard and In-Game Help Files are inert this slice (AD-76 for Help — the plugin retail depends on doesn't exist); Urgent Assistance/Report Abuse short-circuit to their own byte-verified failure text through the interface-text seam instead of ShellExecute against a dead URL (AD-75); Use Mouse Turning Settings runs the pure MouseTurningSettingsMacro port, persisting five new CameraTurningSettings preferences and sending PlayerOption.UseMouseTurning — TS-74 records that acdream has no persistent mouse-turning camera mode for the bit to drive yet. Full Release suite: 12,918 passed / 4 skipped / 0 failed (baseline 12,871/4/0 — only new tests added). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
281 lines
12 KiB
C#
281 lines
12 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");
|
|
|
|
// ── 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}");
|
|
}
|
|
}
|