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>
This commit is contained in:
parent
0a9ca2f1f9
commit
74c3d85d37
6 changed files with 40568 additions and 0 deletions
|
|
@ -263,6 +263,15 @@ public static class FixtureLoader
|
|||
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>
|
||||
|
|
|
|||
|
|
@ -147,6 +147,12 @@ public sealed class RetailLayoutFixtureGenerator
|
|||
"vendor_21000012_100000B7.json"),
|
||||
(OptionsPanelController.HostLayoutId, OptionsPanelController.SlotElementId,
|
||||
"options_panel_2100006E_1000018D.json"),
|
||||
// Campaign FA slice FA3: the four-tab social panel (Friends/
|
||||
// Allegiance/Fellowship/Squelch), coordinator addendum
|
||||
// (docs/research/2026-08-11-fa-panel-structure.md §10) — closes
|
||||
// lane-A unknowns U3/U4/U6/U7/U10.
|
||||
(SocialPanelController.HostLayoutId, SocialPanelController.SlotElementId,
|
||||
"social_panel_2100006E_1000018F.json"),
|
||||
})
|
||||
{
|
||||
ElementInfo? panelPart = LayoutImporter.ImportInfos(dats, layoutId, rootId);
|
||||
|
|
|
|||
292
tests/AcDream.App.Tests/UI/Layout/SocialPanelControllerTests.cs
Normal file
292
tests/AcDream.App.Tests/UI/Layout/SocialPanelControllerTests.cs
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Social;
|
||||
using AcDream.Runtime;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Controller-level tests for <see cref="SocialPanelController"/> against the
|
||||
/// committed <c>social_panel_2100006E_1000018F.json</c> fixture (Campaign FA
|
||||
/// slice FA3) — the SAME <c>LayoutImporter.ImportInfos(dats, 0x2100006Eu,
|
||||
/// 0x1000018Fu)</c> catalog import <see cref="RetailUiRuntime.MountSocialPanel"/>
|
||||
/// performs against real DATs. No DAT access, no live runtime — dat-free per
|
||||
/// the established <see cref="FixtureLoader"/> pattern.
|
||||
/// </summary>
|
||||
public sealed class SocialPanelControllerTests
|
||||
{
|
||||
private static SocialPanelController.Callbacks MakeCallbacks(
|
||||
List<string>? calls = null,
|
||||
RuntimeFellowshipSnapshot fellowship = default,
|
||||
RuntimeAllegianceSnapshot allegiance = default,
|
||||
FriendsState? friends = null,
|
||||
SquelchState? squelch = null)
|
||||
{
|
||||
calls ??= new List<string>();
|
||||
return new SocialPanelController.Callbacks(
|
||||
Toggle: () => calls.Add("toggle"),
|
||||
FellowshipSnapshot: () => fellowship,
|
||||
AllegianceSnapshot: () => allegiance,
|
||||
Friends: friends ?? new FriendsState(),
|
||||
Squelch: squelch ?? new SquelchState(),
|
||||
TemplateResolver: FakeRowTemplateResolver);
|
||||
}
|
||||
|
||||
/// <summary>Mirrors the real Friends/Squelch row templates' own two-deep
|
||||
/// nested-text shape (FA3 live-mount probe) so
|
||||
/// <see cref="SocialPanelRowText.FindDeepest"/> has something realistic
|
||||
/// to resolve, without needing the templates' own (uncommitted, separate
|
||||
/// LayoutDesc) fixtures.</summary>
|
||||
private static UiElement? FakeRowTemplateResolver(uint layoutId, uint elementId)
|
||||
{
|
||||
var outer = new UiText();
|
||||
var inner = new UiText();
|
||||
outer.AddChild(inner);
|
||||
return outer;
|
||||
}
|
||||
|
||||
// ── Mount conformance ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Bind_RootBuildsAsUiTabPanel()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
|
||||
Assert.IsType<UiTabPanel>(layout.Root);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pins the authored <c>0x2E</c> tab table exactly as read from the live
|
||||
/// DATs (FA3 live-mount probe) — CORRECTING the coordinator addendum's
|
||||
/// x-order guess (see <see cref="SocialPanelController"/>'s own class
|
||||
/// doc). Four entries; Allegiance is the sole default.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TabTable_MatchesLiveDatPairing_AllegianceIsDefault()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
var tabs = Assert.IsType<UiTabPanel>(layout.Root);
|
||||
|
||||
Assert.Equal(4, tabs.Tabs.Count);
|
||||
Assert.Contains(tabs.Tabs, e =>
|
||||
e.ButtonElementId == 0x1000028Cu && e.PageElementId == 0x10000291u && e.IsDefault);
|
||||
Assert.Contains(tabs.Tabs, e =>
|
||||
e.ButtonElementId == 0x1000028Eu && e.PageElementId == 0x10000292u && !e.IsDefault);
|
||||
Assert.Contains(tabs.Tabs, e =>
|
||||
e.ButtonElementId == 0x10000512u && e.PageElementId == 0x10000513u && !e.IsDefault);
|
||||
Assert.Contains(tabs.Tabs, e =>
|
||||
e.ButtonElementId == 0x1000053Bu && e.PageElementId == 0x1000054Au && !e.IsDefault);
|
||||
Assert.Single(tabs.Tabs, e => e.IsDefault);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bind_Succeeds_AndActivateTabs_SelectsTheAuthoredDefault()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
|
||||
SocialPanelController? controller = SocialPanelController.Bind(layout, MakeCallbacks());
|
||||
|
||||
Assert.NotNull(controller);
|
||||
controller!.ActivateTabs();
|
||||
Assert.Empty(controller.TabPanel.UnresolvedEntries);
|
||||
Assert.True(controller.IsShowingAllegiance);
|
||||
Assert.False(controller.IsShowingFellowship);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowFellowship_SwitchesTheActiveTab()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
SocialPanelController? controller = SocialPanelController.Bind(layout, MakeCallbacks());
|
||||
Assert.NotNull(controller);
|
||||
controller!.ActivateTabs();
|
||||
|
||||
controller.ShowFellowship();
|
||||
|
||||
Assert.True(controller.IsShowingFellowship);
|
||||
Assert.False(controller.IsShowingAllegiance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CloseButton_InvokesToggleCallback()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
var calls = new List<string>();
|
||||
SocialPanelController? controller = SocialPanelController.Bind(layout, MakeCallbacks(calls));
|
||||
Assert.NotNull(controller);
|
||||
|
||||
var close = Assert.IsType<UiButton>(layout.FindElement(0x10000290u));
|
||||
close.OnEvent(new UiEvent(0, close, UiEventType.Click));
|
||||
|
||||
Assert.Contains("toggle", calls);
|
||||
}
|
||||
|
||||
// ── Fellowship empty state (item 4) ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Fellowship_NoFellowship_ShowsNotInFellowshipFrame()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
SocialPanelController? controller = SocialPanelController.Bind(
|
||||
layout,
|
||||
MakeCallbacks(fellowship: new RuntimeFellowshipSnapshot { IsInFellowship = false }));
|
||||
Assert.NotNull(controller);
|
||||
|
||||
UiElement notIn = UiElement.FindDescendant(controller!.TabPanel, 0x1000026Bu)!;
|
||||
UiElement inFellowship = UiElement.FindDescendant(controller.TabPanel, 0x10000275u)!;
|
||||
Assert.True(notIn.Visible);
|
||||
Assert.False(inFellowship.Visible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fellowship_HasFellowship_ShowsInFellowshipFrame()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
SocialPanelController? controller = SocialPanelController.Bind(
|
||||
layout,
|
||||
MakeCallbacks(fellowship: new RuntimeFellowshipSnapshot { IsInFellowship = true }));
|
||||
Assert.NotNull(controller);
|
||||
|
||||
UiElement notIn = UiElement.FindDescendant(controller!.TabPanel, 0x1000026Bu)!;
|
||||
UiElement inFellowship = UiElement.FindDescendant(controller.TabPanel, 0x10000275u)!;
|
||||
Assert.False(notIn.Visible);
|
||||
Assert.True(inFellowship.Visible);
|
||||
}
|
||||
|
||||
// ── Allegiance empty state (item 4) ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Allegiance_NoProfile_HidesBlocksAndBlanksNames()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
SocialPanelController? controller = SocialPanelController.Bind(
|
||||
layout,
|
||||
MakeCallbacks(allegiance: new RuntimeAllegianceSnapshot { HasProfile = false }));
|
||||
Assert.NotNull(controller);
|
||||
|
||||
UiElement monarchField = UiElement.FindDescendant(controller!.TabPanel, 0x10000255u)!;
|
||||
UiElement patronField = UiElement.FindDescendant(controller.TabPanel, 0x1000025Au)!;
|
||||
Assert.False(monarchField.Visible);
|
||||
Assert.False(patronField.Visible);
|
||||
|
||||
var monarchName = Assert.IsType<UiText>(
|
||||
UiElement.FindDescendant(monarchField, 0x10000257u));
|
||||
var patronName = Assert.IsType<UiText>(
|
||||
UiElement.FindDescendant(patronField, 0x1000025Cu));
|
||||
Assert.Equal(" ", Assert.Single(monarchName.LinesProvider()).Text);
|
||||
Assert.Equal(" ", Assert.Single(patronName.LinesProvider()).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Allegiance_HasProfile_ShowsBothBlocks()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
SocialPanelController? controller = SocialPanelController.Bind(
|
||||
layout,
|
||||
MakeCallbacks(allegiance: new RuntimeAllegianceSnapshot { HasProfile = true }));
|
||||
Assert.NotNull(controller);
|
||||
|
||||
UiElement monarchField = UiElement.FindDescendant(controller!.TabPanel, 0x10000255u)!;
|
||||
UiElement patronField = UiElement.FindDescendant(controller.TabPanel, 0x1000025Au)!;
|
||||
Assert.True(monarchField.Visible);
|
||||
Assert.True(patronField.Visible);
|
||||
}
|
||||
|
||||
// ── Friends/Squelch read-only lists (item 5) ────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Friends_PopulatesOneRowPerEntry()
|
||||
{
|
||||
var friends = new FriendsState();
|
||||
friends.Apply(new FriendsUpdate(
|
||||
FriendsUpdateType.Full,
|
||||
new List<FriendEntry>
|
||||
{
|
||||
new(1u, "Alice", true, false, System.Array.Empty<uint>(), System.Array.Empty<uint>()),
|
||||
new(2u, "Bob", false, false, System.Array.Empty<uint>(), System.Array.Empty<uint>()),
|
||||
}));
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
|
||||
SocialPanelController? controller = SocialPanelController.Bind(
|
||||
layout, MakeCallbacks(friends: friends));
|
||||
Assert.NotNull(controller);
|
||||
|
||||
var listBox = Assert.IsType<UiTemplateListBox>(
|
||||
UiElement.FindDescendant(controller!.TabPanel, 0x10000513u) is { } friendsPage
|
||||
? UiElement.FindDescendant(friendsPage, 0x10000517u)
|
||||
: null);
|
||||
Assert.Equal(2, listBox.ViewportForTest!.Children.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Friends_ReactsToRevisionChange_OnTick()
|
||||
{
|
||||
var friends = new FriendsState();
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
SocialPanelController? controller = SocialPanelController.Bind(
|
||||
layout, MakeCallbacks(friends: friends));
|
||||
Assert.NotNull(controller);
|
||||
|
||||
UiElement friendsPage = UiElement.FindDescendant(controller!.TabPanel, 0x10000513u)!;
|
||||
var listBox = Assert.IsType<UiTemplateListBox>(
|
||||
UiElement.FindDescendant(friendsPage, 0x10000517u));
|
||||
Assert.Equal(0, listBox.ViewportForTest?.Children.Count ?? 0);
|
||||
|
||||
friends.Apply(new FriendsUpdate(
|
||||
FriendsUpdateType.Add,
|
||||
new List<FriendEntry>
|
||||
{
|
||||
new(1u, "Alice", true, false, System.Array.Empty<uint>(), System.Array.Empty<uint>()),
|
||||
}));
|
||||
controller.Tick();
|
||||
|
||||
Assert.Single(listBox.ViewportForTest!.Children);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Squelch_PopulatesCharacterAndAccountRows()
|
||||
{
|
||||
var squelch = new SquelchState();
|
||||
squelch.Replace(new SquelchDatabase(
|
||||
new Dictionary<string, uint>(System.StringComparer.OrdinalIgnoreCase) { ["BadAccount"] = 1u },
|
||||
new Dictionary<uint, SquelchInfo>
|
||||
{
|
||||
[7u] = new SquelchInfo("Grief", false, new HashSet<uint>()),
|
||||
},
|
||||
new SquelchInfo(string.Empty, false, new HashSet<uint>())));
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
|
||||
SocialPanelController? controller = SocialPanelController.Bind(
|
||||
layout, MakeCallbacks(squelch: squelch));
|
||||
Assert.NotNull(controller);
|
||||
|
||||
UiElement squelchPage = UiElement.FindDescendant(controller!.TabPanel, 0x1000054Au)!;
|
||||
var listBox = Assert.IsType<UiTemplateListBox>(
|
||||
UiElement.FindDescendant(squelchPage, 0x1000053Eu));
|
||||
Assert.Equal(2, listBox.ViewportForTest!.Children.Count);
|
||||
}
|
||||
|
||||
// ── D1: Friends/Squelch action buttons are honest INERT ────────────────
|
||||
|
||||
[Fact]
|
||||
public void FriendsAndSquelchActionButtons_AreClickable_ButHaveNoHandler()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
SocialPanelController? controller = SocialPanelController.Bind(layout, MakeCallbacks());
|
||||
Assert.NotNull(controller);
|
||||
|
||||
foreach (uint buttonId in new[] { 0x10000514u, 0x10000515u, 0x10000516u })
|
||||
{
|
||||
var button = Assert.IsType<UiButton>(
|
||||
UiElement.FindDescendant(controller!.TabPanel, buttonId));
|
||||
Assert.Null(button.OnClick);
|
||||
}
|
||||
foreach (uint buttonId in new[] { 0x10000547u, 0x1000054Bu, 0x1000054Cu })
|
||||
{
|
||||
var button = Assert.IsType<UiButton>(
|
||||
UiElement.FindDescendant(controller!.TabPanel, buttonId));
|
||||
Assert.Null(button.OnClick);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
using System.IO;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.Options;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FA slice FA3, item 7: extends the ACDREAM_PROBE_LIVE_MOUNT pattern
|
||||
/// (the #372/#375/#378 lesson — fixture-green alone is NOT acceptance for
|
||||
/// anything mounted) to the four-tab social panel. Asserts the PRODUCTION
|
||||
/// mount path resolves: the tab host as <see cref="UiTabPanel"/> with its
|
||||
/// 4-entry table, all four page elements, the fellowship empty/full frame
|
||||
/// pair, the allegiance signature elements, and non-empty captions on the
|
||||
/// tab buttons (the #375 resolver class). Also dumps the raw tab table so
|
||||
/// its button→page pairing + default entry can be recorded — coordinator
|
||||
/// addendum, docs/research/2026-08-11-fa-panel-structure.md §10.
|
||||
/// </summary>
|
||||
public sealed class SocialPanelLiveMountProbeTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProbeLiveMountShapes()
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1")
|
||||
return;
|
||||
|
||||
var datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||
?? Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
"Documents",
|
||||
"Asheron's Call");
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
var strings = new DatStringResolver(dats);
|
||||
|
||||
ElementInfo? root = LayoutImporter.ImportInfos(
|
||||
dats, SocialPanelController.HostLayoutId, SocialPanelController.SlotElementId);
|
||||
Assert.NotNull(root);
|
||||
ImportedLayout layout = LayoutImporter.Build(
|
||||
root!, _ => (1u, 8, 8), null, null, strings.Resolve);
|
||||
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] root id=0x{root!.Id:X8} type={root.Type} "
|
||||
+ $"({root.X},{root.Y} {root.Width}x{root.Height}) children={root.Children.Count} "
|
||||
+ $"restorePrevious={(root.TryGetEffectiveBool(RetailPanelUiController.RestorePreviousPropertyId, out bool rp) && rp)}");
|
||||
foreach (ElementInfo c in root.Children)
|
||||
{
|
||||
string p12 = c.TryGetEffectiveProperty(0x12u, out var v12) ? $"0x{v12.UnsignedValue:X8}" : "ABSENT";
|
||||
string p57 = c.TryGetEffectiveProperty(0x57u, out var v57) ? $"{v57.Kind}=0x{v57.UnsignedValue:X8}" : "ABSENT";
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] root-child 0x{c.Id:X8} type=0x{c.Type:X8} ({c.X},{c.Y} {c.Width}x{c.Height}) "
|
||||
+ $"kids={c.Children.Count} P0x12={p12} P0x57={p57}");
|
||||
}
|
||||
{
|
||||
string rootP57 = root.TryGetEffectiveProperty(0x57u, out var rv57) ? $"{rv57.Kind}=0x{rv57.UnsignedValue:X8}" : "ABSENT";
|
||||
Console.WriteLine($"[socialprobe] root P0x57={rootP57}");
|
||||
}
|
||||
|
||||
UiTabPanel tabs = Assert.IsType<UiTabPanel>(layout.Root);
|
||||
Console.WriteLine($"[socialprobe] tab table entries={tabs.Tabs.Count}");
|
||||
foreach (UiTabTableEntry t in tabs.Tabs)
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] button=0x{t.ButtonElementId:X8} page=0x{t.PageElementId:X8} default={t.IsDefault}");
|
||||
Assert.True(tabs.Tabs.Count >= 4);
|
||||
|
||||
// Production mount: activate behavior exactly like SocialPanelController.ActivateTabs.
|
||||
tabs.ActivateTabBehavior();
|
||||
Assert.Empty(tabs.UnresolvedEntries);
|
||||
|
||||
foreach ((uint pageId, string name) in new[]
|
||||
{
|
||||
(0x10000513u, "Friends"),
|
||||
(0x10000291u, "Allegiance"),
|
||||
(0x10000292u, "Fellowship"),
|
||||
(0x1000054Au, "Squelch"),
|
||||
})
|
||||
{
|
||||
UiElement? page = UiElement.FindDescendant(tabs, pageId);
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] page {name} 0x{pageId:X8} -> {(page is null ? "MISSING" : page.GetType().Name)}");
|
||||
Assert.NotNull(page);
|
||||
}
|
||||
|
||||
// Fellowship empty/full frame pair.
|
||||
foreach ((uint id, string name) in new[]
|
||||
{
|
||||
(0x1000026Bu, "NotInAFellowshipFrame"),
|
||||
(0x10000275u, "InAFellowshipFrame"),
|
||||
})
|
||||
{
|
||||
UiElement? el = UiElement.FindDescendant(tabs, id);
|
||||
Console.WriteLine($"[socialprobe] fellowship frame {name} 0x{id:X8} -> {(el is null ? "MISSING" : el.GetType().Name)}");
|
||||
Assert.NotNull(el);
|
||||
}
|
||||
|
||||
// Allegiance signature elements.
|
||||
foreach ((uint id, string name) in new[]
|
||||
{
|
||||
(0x10000255u, "MonarchField"),
|
||||
(0x1000025Au, "PatronField"),
|
||||
(0x10000260u, "VassalListBox"),
|
||||
(0x10000263u, "SwearButton"),
|
||||
(0x10000264u, "BreakButton"),
|
||||
(0x10000265u, "KickButton"),
|
||||
})
|
||||
{
|
||||
UiElement? el = UiElement.FindDescendant(tabs, id);
|
||||
Console.WriteLine($"[socialprobe] allegiance element {name} 0x{id:X8} -> {(el is null ? "MISSING" : el.GetType().Name)}");
|
||||
Assert.NotNull(el);
|
||||
}
|
||||
|
||||
// 0x10000492 is authored TWICE inside the allegiance page (monarch
|
||||
// block child AND patron block child) — count occurrences under the
|
||||
// allegiance page root specifically, not the whole tree.
|
||||
UiElement? allegiancePage = UiElement.FindDescendant(tabs, 0x10000291u);
|
||||
Assert.NotNull(allegiancePage);
|
||||
int passupCount = CountDescendants(allegiancePage!, 0x10000492u);
|
||||
Console.WriteLine($"[socialprobe] 0x10000492 occurrences under allegiance page = {passupCount}");
|
||||
|
||||
// Tab button captions — non-empty (the #375 resolver class: a missing
|
||||
// string resolver renders blank captions even though the layout mounts).
|
||||
foreach (UiTabTableEntry t in tabs.Tabs)
|
||||
{
|
||||
UiElement? button = UiElement.FindDescendant(tabs, t.ButtonElementId);
|
||||
string? caption = button switch
|
||||
{
|
||||
UiText text => text.LinesProvider?.Invoke() is { Count: > 0 } lines ? lines[0].Text : null,
|
||||
UiButton btn => btn.Label,
|
||||
_ => null,
|
||||
};
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] tab button 0x{t.ButtonElementId:X8} ({button?.GetType().Name}) caption='{caption}'");
|
||||
Assert.False(string.IsNullOrEmpty(caption));
|
||||
}
|
||||
|
||||
// U6: containment of the fellowship empty/full frames.
|
||||
ElementInfo? notInFellowshipFrame = FindInfo(root, 0x1000026Bu);
|
||||
ElementInfo? inFellowshipFrame = FindInfo(root, 0x10000275u);
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] 0x1000026B (NotInAFellowshipFrame) children: "
|
||||
+ string.Join(",", (notInFellowshipFrame?.Children ?? new()).ConvertAll(c => $"0x{c.Id:X8}")));
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] 0x10000275 (InAFellowshipFrame) children: "
|
||||
+ string.Join(",", (inFellowshipFrame?.Children ?? new()).ConvertAll(c => $"0x{c.Id:X8}")));
|
||||
|
||||
// Dump two levels of children under each page slot for U3/U4/U6/U7 sweep.
|
||||
foreach (uint pageId in new[] { 0x10000513u, 0x10000291u, 0x10000292u, 0x1000054Au })
|
||||
{
|
||||
ElementInfo? pageInfo = FindInfo(root, pageId);
|
||||
if (pageInfo is null)
|
||||
{
|
||||
Console.WriteLine($"[socialprobe] page-info 0x{pageId:X8} MISSING from ElementInfo tree");
|
||||
continue;
|
||||
}
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] page-info 0x{pageId:X8} P0x57={(pageInfo.TryGetEffectiveProperty(0x57u, out var p57) ? p57.UnsignedValue.ToString() : "ABSENT")} children={pageInfo.Children.Count}");
|
||||
DumpInfoTree(pageInfo, 1, maxDepth: 3);
|
||||
}
|
||||
|
||||
// Friends/Squelch row templates — find the name-text child.
|
||||
foreach ((uint tLayout, uint tElement, string name) in new[]
|
||||
{
|
||||
(0x2100005Du, 0x10000519u, "FriendsRow"),
|
||||
(0x21000060u, 0x10000541u, "SquelchRow"),
|
||||
})
|
||||
{
|
||||
ElementInfo? row = LayoutImporter.ImportInfos(dats, tLayout, tElement);
|
||||
if (row is null)
|
||||
{
|
||||
Console.WriteLine($"[socialprobe] {name} template 0x{tLayout:X8}/0x{tElement:X8} IMPORT NULL");
|
||||
continue;
|
||||
}
|
||||
Console.WriteLine($"[socialprobe] {name} template:");
|
||||
DumpInfoTree(row, 1, maxDepth: 3);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DumpInfoTree(ElementInfo info, int depth, int maxDepth)
|
||||
{
|
||||
if (depth > maxDepth) return;
|
||||
string templates = info.TemplateList.Count > 0
|
||||
? $" templates={info.TemplateList.Count}[{string.Join(",", info.TemplateList.ConvertAll(t => $"0x{t.TemplateLayoutId:X8}/0x{t.TemplateElementId:X8}"))}] scrollbar=0x{info.ScrollbarElementId:X8}"
|
||||
: "";
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] {new string(' ', depth * 2)}0x{info.Id:X8} type=0x{info.Type:X8} "
|
||||
+ $"({info.X},{info.Y} {info.Width}x{info.Height}) kids={info.Children.Count}{templates}");
|
||||
foreach (ElementInfo c in info.Children)
|
||||
DumpInfoTree(c, depth + 1, maxDepth);
|
||||
}
|
||||
|
||||
private static int CountDescendants(UiElement root, uint id)
|
||||
{
|
||||
int count = root.DatElementId == id ? 1 : 0;
|
||||
foreach (UiElement child in root.Children)
|
||||
count += CountDescendants(child, id);
|
||||
return count;
|
||||
}
|
||||
|
||||
private static ElementInfo? FindInfo(ElementInfo root, uint id)
|
||||
{
|
||||
if (root.Id == id) return root;
|
||||
foreach (ElementInfo c in root.Children)
|
||||
{
|
||||
ElementInfo? found = FindInfo(c, id);
|
||||
if (found is not null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -46,4 +46,31 @@ public sealed class RetailPanelCatalogTests
|
|||
=> Assert.Contains(
|
||||
(RetailPanelCatalog.Options, WindowNames.Options),
|
||||
RetailPanelCatalog.ToolbarPanels);
|
||||
|
||||
// ── Campaign FA slice FA3 ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SocialPanel_PanelId_Is12_MatchingTheLiveDatSlotProperty()
|
||||
=> Assert.Equal(12u, RetailPanelCatalog.SocialPanel);
|
||||
|
||||
[Fact]
|
||||
public void SocialPanel_ResolvesToTheSocialPanelWindowName()
|
||||
{
|
||||
Assert.True(RetailPanelCatalog.TryGetWindowName(RetailPanelCatalog.SocialPanel, out string name));
|
||||
Assert.Equal(WindowNames.SocialPanel, name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SocialPanel_IsListedInMountedPanels()
|
||||
=> Assert.Contains(
|
||||
(RetailPanelCatalog.SocialPanel, WindowNames.SocialPanel),
|
||||
RetailPanelCatalog.MountedPanels);
|
||||
|
||||
/// <summary>Lane A §6.1: the open path is keyboard-only (F3/F4); retail
|
||||
/// authors no toolbar button for either action.</summary>
|
||||
[Fact]
|
||||
public void SocialPanel_IsNotListedInToolbarPanels()
|
||||
=> Assert.DoesNotContain(
|
||||
(RetailPanelCatalog.SocialPanel, WindowNames.SocialPanel),
|
||||
RetailPanelCatalog.ToolbarPanels);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue