feat(ui): FA5 -- allegiance page fully live: CF-1 subscription, blocks, roster, swear/break/kick
Campaign FA slice FA5. The Allegiance page (0x10000291) goes from FA3's
empty-state shell to fully live, wired against FA1's parser and FA2's
RuntimeAllegianceState/IRuntimeAllegianceCommands (both already shipped
the full command surface, including SetUpdateSubscription).
CF-1 (the corrected data subscription): 0x001F AllegianceUpdateRequest,
not 0x027B, is the panel's data source (0x027B/0x027C are text-only chat
per FA2 MF-2). Wired at retail's three arming points -- Bind's PostInit
attempt (almost always a pre-world no-op), the post-world EnteredWorld
seam (RedeclareAfterWorldEntry, UNCONDITIONAL -- does not check the
current latch, matching retail's own PlayerDescReceived arm and avoiding
the exact MF-3-REOPEN bug class FA4 hit for 0x00A6), and the visible
branch (SetPageVisible, edge-triggered, folded into
SocialPanelController's existing window-shown+active-tab conjunction
alongside Fellowship's 0x00A6).
Monarch/patron/self blocks: per-relationship empty-state gate (fix-round
SF-7) replacing FA3's coarse HasProfile-only gate -- the monarch block
hides when there is no monarch OR the monarch is the viewer; the patron
block hides when there is no patron OR the patron is the monarch (in
which case the monarch block's 0x10000490 sub-block reveals and its
label swaps to PatronSlashMonarchLabel). Field sources decompiled fresh
from gmAllegianceUI::UpdatePlayerData/UpdateMonarchData/UpdatePatronData:
0x10000251 is the ALLEGIANCE's own name (not the viewer's), follower
counts are TotalVassals/TotalMembers-1 directly off the wire, and the
"experience passed up" text (0x10000492, doubled -- scoped FindDescendant
under each of its two parents) is the viewer's own CpTithed under the
monarch/patron blocks and each vassal's own CpTithed in their row.
Vassal roster: flat list built via UiTemplateListBox.FlushPreservingScroll
in the FA4 roster-diff pattern (guid-set diff, in-place update on an
unchanged set), rendering in the bindings' own already-reversed order.
Swear/break/kick: each opens a local confirmation dialog
(RetailDialogFactory via ShowConfirmation) before sending, mirroring
retail's MakeSwearConfirmationDialog family -- Swear targets the WORLD
selection (via the same ClientObjectTable name resolver
ToolbarRuntimeBindings.ResolveName already uses), Break targets the
current patron, Kick targets the panel-local selected vassal row (no
world-selection sync for Allegiance, unlike Fellowship). The
server-driven "accept incoming swear" (ConfirmationType 1) needed no new
code -- GameplayConfirmationController already handles every type
generically; a new test verifies it explicitly.
Runtime/composition plumbing: DeferredGameRuntimeStateCommands gains
Allegiance{Swear,Break,Kick,SetUpdateSubscription}; SocialRuntimeBindings
gains the Allegiance view/command projections; SocialPanelController.
Callbacks.AllegianceSnapshot widens to a full
SocialAllegiancePageController.Bindings record, mirroring FA4's
Fellowship widening.
Tests: SocialPanelControllerTests.cs gains 10 tests covering the SF-7
gate (4), roster population, swear/break/kick wiring (3), and the CF-1
subscription arming points (2); GameplayConfirmationControllerTests.cs
gains the type-1 verification test.
Also extends SocialPanelLiveMountProbeTests.cs (production-mount
assertions: scoped 0x10000492 resolution, the vassal row template, the
checkbox, confirmation-dialog string resolution, and a full production
Bind() pass) -- not yet run against live DATs in this worktree (no
Documents/Asheron's Call present here).
Release build green; full solution suite 13,296 passed / 4 skipped / 0
failed (13,300 total), up from FA4's 13,285/4/0 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f5bd3e5621
commit
7ed79eaf10
8 changed files with 1412 additions and 95 deletions
|
|
@ -276,6 +276,134 @@ public sealed class SocialPanelLiveMountProbeTests
|
|||
// fail this test, not just the log.
|
||||
Assert.Equal(2, passupCount);
|
||||
|
||||
// Campaign FA slice FA5, item 6: the DOUBLED 0x10000492 SCOPED
|
||||
// resolution — proves SocialAllegiancePageController's two
|
||||
// FindDescendant calls (one under 0x10000490, one under the patron
|
||||
// field 0x1000025A) each resolve to a DIFFERENT real instance, not
|
||||
// just that two instances exist somewhere in the subtree (the count
|
||||
// check above). A flat/unscoped lookup would silently pick ONE
|
||||
// instance for BOTH roles and this probe would not catch it.
|
||||
UiElement? monarchField = UiElement.FindDescendant(tabs, 0x10000255u);
|
||||
UiElement? patronField = UiElement.FindDescendant(tabs, 0x1000025Au);
|
||||
Assert.NotNull(monarchField);
|
||||
Assert.NotNull(patronField);
|
||||
UiElement? monarchIsPatronSubBlock = UiElement.FindDescendant(monarchField!, 0x10000490u);
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] allegiance sub-block 0x10000490 (under monarch field) -> "
|
||||
+ $"{(monarchIsPatronSubBlock is null ? "MISSING" : monarchIsPatronSubBlock.GetType().Name)}");
|
||||
Assert.NotNull(monarchIsPatronSubBlock);
|
||||
UiElement? monarchScopedPassup = UiElement.FindDescendant(monarchIsPatronSubBlock!, 0x10000492u);
|
||||
UiElement? patronScopedPassup = UiElement.FindDescendant(patronField!, 0x10000492u);
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] 0x10000492 under 0x10000490 -> {(monarchScopedPassup is null ? "MISSING" : "0x" + monarchScopedPassup.DatElementId.ToString("X8"))}; "
|
||||
+ $"under patron field -> {(patronScopedPassup is null ? "MISSING" : "0x" + patronScopedPassup.DatElementId.ToString("X8"))}");
|
||||
Assert.NotNull(monarchScopedPassup);
|
||||
Assert.NotNull(patronScopedPassup);
|
||||
Assert.NotSame(monarchScopedPassup, patronScopedPassup);
|
||||
|
||||
// The vassal ListBox's own authored template pair and scrollbar
|
||||
// (mirrors the fellowship row-template check above).
|
||||
UiElement? vassalListBoxEl = UiElement.FindDescendant(tabs, 0x10000260u);
|
||||
UiTemplateListBox vassalListBox = Assert.IsType<UiTemplateListBox>(vassalListBoxEl);
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] allegiance ListBox 0x10000260 templates={vassalListBox.Templates.Count} "
|
||||
+ $"scrollbar=0x{vassalListBox.ScrollbarElementId:X8}");
|
||||
Assert.NotEmpty(vassalListBox.Templates);
|
||||
UiTemplateListEntry vassalRowTemplate = vassalListBox.Templates[0];
|
||||
|
||||
UiElement? vassalRow = rowTemplates.Resolve(
|
||||
vassalRowTemplate.TemplateLayoutId, vassalRowTemplate.TemplateElementId);
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] allegiance row template 0x{vassalRowTemplate.TemplateLayoutId:X8}/"
|
||||
+ $"0x{vassalRowTemplate.TemplateElementId:X8} -> {(vassalRow is null ? "IMPORT NULL" : vassalRow.GetType().Name)}");
|
||||
Assert.NotNull(vassalRow);
|
||||
|
||||
foreach ((uint id, string name, Type expectedType) in new (uint, string, Type)[]
|
||||
{
|
||||
(0x10000268u, "VassalName", typeof(UiText)),
|
||||
(0x10000269u, "VassalExperiencePassedUp", typeof(UiText)),
|
||||
})
|
||||
{
|
||||
UiElement? el = UiElement.FindDescendant(vassalRow!, id);
|
||||
Console.WriteLine($"[socialprobe] allegiance row field {name} 0x{id:X8} -> {(el is null ? "MISSING" : el.GetType().Name)}");
|
||||
Assert.IsType(expectedType, el);
|
||||
}
|
||||
UiElement? offlineMarker = UiElement.FindDescendant(vassalRow!, 0x100004AAu);
|
||||
Console.WriteLine($"[socialprobe] allegiance row field OfflineMarker 0x100004AA -> {(offlineMarker is null ? "MISSING" : offlineMarker.GetType().Name)}");
|
||||
Assert.NotNull(offlineMarker);
|
||||
|
||||
// The IgnoreAllegianceRequests checkbox (D7: dimmed on the Options
|
||||
// Character tab, but still a live read/write toggle here — same
|
||||
// shape as the fellowship page's four checkboxes).
|
||||
UiElement? ignoreRequestsCheckbox = UiElement.FindDescendant(tabs, 0x10000262u);
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] allegiance checkbox IgnoreAllegianceRequests 0x10000262 -> "
|
||||
+ $"{(ignoreRequestsCheckbox is null ? "MISSING" : ignoreRequestsCheckbox.GetType().Name)}");
|
||||
Assert.IsType<UiButton>(ignoreRequestsCheckbox);
|
||||
|
||||
// CF-1 + AD-85: the checkbox label/tooltip (0x23000003) and the four
|
||||
// confirmation-dialog templates + two label captions (0x23000001) —
|
||||
// resolved verbatim, never substituted (class doc).
|
||||
string? ignoreRequestsLabel = strings.Resolve(
|
||||
0x23000003u, DatStringResolver.ComputeHash("ID_PlayerOption_IgnoreAllegianceRequests"));
|
||||
Console.WriteLine($"[socialprobe] checkbox label ID_PlayerOption_IgnoreAllegianceRequests -> '{ignoreRequestsLabel}'");
|
||||
Assert.False(string.IsNullOrEmpty(ignoreRequestsLabel));
|
||||
|
||||
foreach (string key in new[]
|
||||
{
|
||||
"ID_Allegiance_MonarchLabel",
|
||||
"ID_Allegiance_PatronSlashMonarchLabel",
|
||||
"ID_Allegiance_SwearConfirmation",
|
||||
"ID_Allegiance_BreakConfirmation",
|
||||
"ID_Allegiance_KickConfirmation",
|
||||
})
|
||||
{
|
||||
string? value = strings.Resolve(0x23000001u, DatStringResolver.ComputeHash(key));
|
||||
Console.WriteLine($"[socialprobe] allegiance string {key} -> '{value}'");
|
||||
Assert.False(string.IsNullOrEmpty(value));
|
||||
}
|
||||
|
||||
// Full end-to-end Bind against the PRODUCTION mount (mirrors the
|
||||
// fellowship Bind check above) — proves Bind() finds every element
|
||||
// it needs with zero "not found" console warnings.
|
||||
UiElement? allegiancePageForBind = UiElement.FindDescendant(tabs, 0x10000291u);
|
||||
Assert.NotNull(allegiancePageForBind);
|
||||
var allegianceOriginalOut = Console.Out;
|
||||
var allegianceCapture = new StringWriter();
|
||||
Console.SetOut(allegianceCapture);
|
||||
SocialAllegiancePageController? allegianceController;
|
||||
try
|
||||
{
|
||||
allegianceController = SocialAllegiancePageController.Bind(
|
||||
allegiancePageForBind!,
|
||||
new SocialAllegiancePageController.Bindings(
|
||||
Snapshot: () => new RuntimeAllegianceSnapshot(),
|
||||
Monarch: () => null,
|
||||
Patron: _ => null,
|
||||
Member: _ => null,
|
||||
Vassals: _ => [],
|
||||
Swear: _ => default,
|
||||
Break: _ => default,
|
||||
Kick: _ => default,
|
||||
SetUpdateSubscription: _ => default,
|
||||
Selection: new AcDream.Core.Selection.SelectionState(),
|
||||
LocalPlayerGuid: () => 0u,
|
||||
CurrentCharacterOption: _ => false,
|
||||
SetCharacterOption: (_, _) => { },
|
||||
TemplateResolver: rowTemplates.Resolve,
|
||||
ResolveString: (tableId, stringId) => strings.Resolve(tableId, stringId),
|
||||
ResolveWorldObjectName: _ => null,
|
||||
ShowConfirmation: (_, _) => 0u));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(allegianceOriginalOut);
|
||||
}
|
||||
string allegianceBindLog = allegianceCapture.ToString();
|
||||
Console.WriteLine($"[socialprobe] allegiance Bind() console output:\n{allegianceBindLog}");
|
||||
Assert.NotNull(allegianceController);
|
||||
Assert.DoesNotContain("not found", allegianceBindLog);
|
||||
|
||||
// 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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue