Swap ChatWindowController's imported main-chat LayoutDesc from the wrong 0x21000006 (an unrelated layout whose root and 800px resize bar appear nowhere in the EoR gameplay UI) to retail's ACTUAL main chat window, 0x2100006F (window root 0x10000600, authored 410x100 — confirmed by a direct DAT dump, found in dats.Local not dats.Portal). Every downstream compensation that existed only to paper over the wrong import is deleted: the hand-cropped 490px content width, the dropped 800px resize bar, the 9px transcript patch, the orphan-sibling pruning, the max/min-vs-scrollbar overlap shift, and the scrollbar top-reclaim. The window now mounts with RetailWindowChrome.Imported (0x2100006F's own 8 border/corner elements are its complete chrome) instead of the universal nine-slice wrapper. LayoutImporter/DatWidgetFactory gain a Type-9 (UIElement_Resizebar) case: UiResizeGrip decodes retail's exact four-bool BorderLocation algorithm (0x2A=bottom/0x2B=left/0x2C=right/0x2D=top, UIElement_Resizebar::StartMouseResizing @0x0046B7E0) into a ResizeEdges bitmask. A direct DAT dump established the true shape: only 7 of the 8 grip-position ids are Type 9 — the straight top-EDGE strip (0x1000069C) is a Type-2 Dragbar (move handle), not a Resizebar, because the main window has no title bar. UiRoot now gives a directly-hit grip's own edges priority over its generic proximity heuristic, and a directly-hit move handle the same priority over ambient proximity — so the plain top strip moves the window while its two corner grips resize it including the Y axis, and all 4 edges + 4 corners work everywhere else. This also fixes the reported "no diagonal cursor at corners" (CursorFeedbackController's existing RetailCursorCatalog cursor ids already matched the DAT exactly; they just never received a genuine diagonal edge combination) and "cannot grow in Y from the bottom-right corner" (the old NineSlice+crop mount's indirection is gone; the Imported mount uses the DAT's real minH=100/maxH=2000/minW=300/maxW=2000 directly). The 8 cosmetic "_Locked" border-art twins default hidden (register row AP-185 — retail's UiLocked-driven art swap between the two skins is not ported; UiRoot.UiLocked continues to gate the underlying interaction correctly either way). The 4 chat-window-1..4 indicator buttons import generically (visible, inert) for CH6b to wire. The two hand-drawn translucent-black tints on the transcript/input are removed now that their parent panels draw their own authored background sprites. Filed #366 (chat window's new-unseen-text indicator 0x1000048C is swallowed by UiText.ConsumesDatChildren, pre-existing and out of scope). Corrected the research doc's "all eight grips" claim against the direct DAT dump. Full Release suite: 12,317 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
210 lines
8.9 KiB
C#
210 lines
8.9 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>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}");
|
|
}
|
|
}
|