Applies docs/research/2026-08-10-ch6ab-review-findings.md in full:
- BLOCKER 1: UiResizeGrip now carries its ElementInfo/resolve pair and
draws its own authored DirectState media (a synthetic parameterless
grip still draws nothing, preserving existing resize-drag tests).
DatWidgetFactory.BuildResizeGrip threads resolve through. All seven
live grips on the main chat window now resolve a non-zero sprite,
restoring the visible borders/corners CH6a silently dropped.
- SHOULD-FIX 2: ChatWindowState gains BroadcastTargetWindow, a sentinel
distinct from every real window id (0-4), fixing the bug where the
main window's explicit-addressing branch coincided with the broadcast
check (both were literal 0). SetFilter's main-window no-op is dropped
— the main window's filter is now genuinely settable. ChatWindowController
.Bind takes a ChatWindowState (the same canonical instance the floating
windows already share) and GetTranscriptLines builds a real accept
predicate instead of accept:null. Verified safe: ClientLocal (0x1A)
never reaches ChatLog (AddText routes it to the SpewBox and returns),
so nothing observable regresses.
- SHOULD-FIX 3: UiButton.SuppressSelfToggle stops the four chat-window
indicator buttons (DAT property 0x0B=true, no retail click handler)
from flipping their own Selected mirror on a stray click.
- SHOULD-FIX 4: generated and committed chat_floaty_2100005b.json from
the real installed dats; added the permanent RetailLayoutFixtureGenerator
entry. All three flagged FloatingChatWindowController assumptions
(input field, title bar, close button) are confirmed correct against
real data — no controller code changes needed. New finding: unlike the
main window, ALL EIGHT floaty border/corner elements are live Type-9
grips (the floaty's own title bar is its move handle), so a floaty
window resizes from every edge and corner.
- SHOULD-FIX 5: register row AP-189 documents the shared-500-entry/
200-line-tail vs retail's per-window 10,000-line scrollback depth gap.
- NITs 1-5: documented the filter-persistence-only-on-/saveautoui
asymmetry and the reconnect-preserves-filters intent; corrected the
research doc's modifier-mask mislabel and the "ONLY function" false
superlative; moved WrapText off ChatWindowController onto
ChatTranscriptRenderer, closing the circular dependency.
Full Release suite: 12,420 passed / 4 skipped / 0 failed (baseline
12,392/4/0 at 22020ef2; net +28 tests, zero regressions).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
229 lines
9.8 KiB
C#
229 lines
9.8 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");
|
|
|
|
// ── 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}");
|
|
}
|
|
}
|