feat(ui): FA4 -- fellowship page fully live
Roster: SocialFellowshipPageController now builds one row per fellow
from the authored template (0x21000030/0x10000281, live-DAT verified),
diffing the member GUID set on each revision-gated Tick -- an unchanged
set updates every row's bound widgets in place (no ListBox mutation, so
scroll position is untouched by construction); only a real join/leave/
disband triggers a rebuild, via UiTemplateListBox.FlushPreservingScroll
(FA3 carry-forward 1, both the widget-level fix and the controller-level
diff). Health/stamina/mana meters bind Fill+Label; the leader's name
tints gold (lane A's row template has no dedicated leader marker, so
this is a flagged adaptation, not a ported mechanism). Row-click
selection (SelectFellow) drives Dismiss/Leader targeting and the world
selection (SelectionChangeSource.Social).
D4: SocialPanelController now tracks "is the social window shown AND is
Fellowship the active tab" via UiTabPanel.ActivePageChanged +
OnShown/OnHidden, and calls SetPageVisible on every transition, which
sends 0x00A6 (idempotent, no-op while disconnected) -- the prerequisite
ACE gates its 0x02C0 vitals stream on.
Create flow: the inline name field (0x1000026F, an authored Editable
UiField -- live-DAT verified) gates the Create button's enabled state
exactly like retail (empty name = disabled = the whole refusal
mechanism, no separate error text); FellowshipShareXP's live value is
read at click time.
Actions + confirmations: Recruit/Dismiss/Quit/Disband/AssignLeader/
SetOpen all route through DeferredGameRuntimeStateCommands (new
Fellowship* methods) rather than a raw WorldSession send, so Quit
correctly picks up RuntimeFellowshipState's leader hand-off rule.
Button enable states port gmFellowshipUI::UpdateButtons verbatim. The
Open/Close button's caption swaps between the two DAT-resolved strings
cached once at Bind (never per-tick -- DatCollection is not safe to
touch unprotected from the render loop). RetailUiRuntime intercepts a
type-4 confirmation request before it reaches the generic
GameplayConfirmationController: IgnoreFellowshipRequests auto-declines,
FellowshipAutoAcceptRequests auto-accepts, neither set falls through to
the existing dialog machinery unchanged (D6).
D5 display: the per-fellow stats line uses retail's byte-decoded
even-split percentage table verbatim (1.0/.../.3111111/.28, default
0.0); the proportional branch omits the percentage rather than
inventing a formula (no acdream ExperienceToRaiseLevel table exists
yet). Both StringInfo variable substitution (row/stats/vitals text) and
ACCharGenData::FormatName (create-flow name canonicalization) are
unported prerequisites, so row text renders as plain numeric composites
-- register rows AD-80/AD-81 (docs commit).
D7: un-dims IgnoreFellowshipRequests/FellowshipAutoAcceptRequests
(consumed by the D6 auto-decline/accept) and FellowshipShareXP/
FellowshipShareLoot (consumed by Create + the page's own second
checkbox surface) on the Character tab -- 4 of 35 store-only rows
promoted to Live (31 remain dimmed).
Carry-forwards from the FA3 re-review, folded into this slice's
contract:
- UiTemplateListBox.FlushPreservingScroll -- preserves scroll offset
across a rebuild instead of resetting to 0 (Flush's existing
contract, unchanged, for Friends/Squelch).
- RowTemplateResolver -- the FA3 caching row-template resolver
extracted from a MountSocialPanel local function into its own
hermetically-testable class; now shared by Friends/Squelch/
Fellowship's row families.
- Friends/Squelch scrollbars now resolve via the built
UiTemplateListBox.ScrollbarElementId (DAT property 0x72) instead of
a hardcoded literal, matching ConfigOptionsPageController's own OP6
precedent.
- The Fellowship roster path never advances its revision latch on a
partial resolver failure until the NEXT real membership change --
never a per-frame retry loop.
Live-DAT verified (ACDREAM_PROBE_LIVE_MOUNT=1, extended
SocialPanelLiveMountProbeTests): the name field builds as UiField, all
11 buttons/checkboxes resolve, the row template's 5 checked fields
resolve to the right widget types, every checkbox label/tooltip and the
Open/Close captions resolve to real retail strings ("Open"/"Close"),
and a full production-path Bind() against live DATs produces zero
"not found" warnings.
App tests: +30 (7 UiTemplateListBox/RowTemplateResolver unit tests, 23
SocialFellowshipPageControllerTests covering roster diff/rebuild,
button enable rules, checkbox wiring, create-flow gating, D4
idempotency, and D5 formatting) plus 2 CharacterOptionsPageController
counts updated for the D7 un-dim (35->31 dimmed, 15->19 live).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
357d203202
commit
5bdd0528f1
16 changed files with 1941 additions and 114 deletions
|
|
@ -1,6 +1,7 @@
|
|||
using System.IO;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Runtime;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.Options;
|
||||
|
||||
|
|
@ -109,6 +110,137 @@ public sealed class SocialPanelLiveMountProbeTests
|
|||
Assert.NotNull(el);
|
||||
}
|
||||
|
||||
// Campaign FA slice FA4, item 8: the fellowship page's own control
|
||||
// inventory against the PRODUCTION mount (not a fixture) — the
|
||||
// name-entry field must build as UiField (proving the DAT authors
|
||||
// Editable=1, SocialFellowshipPageController's load-bearing
|
||||
// assumption), and every button/checkbox must resolve as UiButton.
|
||||
UiElement? nameField = UiElement.FindDescendant(tabs, 0x1000026Fu);
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] fellowship name-entry field 0x1000026F -> {(nameField is null ? "MISSING" : nameField.GetType().Name)}");
|
||||
Assert.IsType<UiField>(nameField);
|
||||
|
||||
foreach ((uint id, string name) in new[]
|
||||
{
|
||||
(0x10000274u, "CreateFellowshipButton"),
|
||||
(0x1000027Bu, "FellowLeaderButton"),
|
||||
(0x1000027Cu, "FellowQuitButton"),
|
||||
(0x1000027Du, "FellowOpenButton"),
|
||||
(0x1000027Eu, "FellowRecruitButton"),
|
||||
(0x1000027Fu, "FellowDismissButton"),
|
||||
(0x10000280u, "FellowDisbandButton"),
|
||||
(0x10000270u, "IgnoreFellowshipRequestsCheckbox"),
|
||||
(0x10000271u, "FellowshipAutoAcceptRequestsCheckbox"),
|
||||
(0x10000272u, "FellowshipShareXPCheckbox"),
|
||||
(0x10000273u, "FellowshipShareLootCheckbox"),
|
||||
})
|
||||
{
|
||||
UiElement? el = UiElement.FindDescendant(tabs, id);
|
||||
Console.WriteLine($"[socialprobe] fellowship control {name} 0x{id:X8} -> {(el is null ? "MISSING" : el.GetType().Name)}");
|
||||
Assert.IsType<UiButton>(el);
|
||||
}
|
||||
|
||||
// The fellowship ListBox's own authored template pair (lane A U10 —
|
||||
// read P0x64 on 0x10000279) and its scrollbar (P0x72).
|
||||
UiElement? fellowshipListBoxEl = UiElement.FindDescendant(tabs, 0x10000279u);
|
||||
UiTemplateListBox fellowshipListBox = Assert.IsType<UiTemplateListBox>(fellowshipListBoxEl);
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] fellowship ListBox 0x10000279 templates={fellowshipListBox.Templates.Count} "
|
||||
+ $"scrollbar=0x{fellowshipListBox.ScrollbarElementId:X8}");
|
||||
Assert.NotEmpty(fellowshipListBox.Templates);
|
||||
UiTemplateListEntry fellowRowTemplate = fellowshipListBox.Templates[0];
|
||||
|
||||
// Production-resolver build of the row template (FA4 carry-forward
|
||||
// 2): the SAME RowTemplateResolver shape MountSocialPanel uses in
|
||||
// production — Import then Build against the LIVE dats, not a
|
||||
// fixture — proving the 8-part row (lane A §3.1) actually resolves
|
||||
// end-to-end, not merely that the id pair is authored.
|
||||
var rowTemplates = new RowTemplateResolver(
|
||||
(layoutId, elementId) => LayoutImporter.ImportInfos(dats, layoutId, elementId),
|
||||
info => LayoutImporter.Build(info, _ => (1u, 8, 8), null, null, strings.Resolve).Root);
|
||||
UiElement? fellowRow = rowTemplates.Resolve(
|
||||
fellowRowTemplate.TemplateLayoutId, fellowRowTemplate.TemplateElementId);
|
||||
Console.WriteLine(
|
||||
$"[socialprobe] fellowship row template 0x{fellowRowTemplate.TemplateLayoutId:X8}/"
|
||||
+ $"0x{fellowRowTemplate.TemplateElementId:X8} -> {(fellowRow is null ? "IMPORT NULL" : fellowRow.GetType().Name)}");
|
||||
Assert.NotNull(fellowRow);
|
||||
|
||||
foreach ((uint id, string name, Type expectedType) in new (uint, string, Type)[]
|
||||
{
|
||||
(0x10000283u, "FellowName", typeof(UiText)),
|
||||
(0x10000284u, "FellowStats", typeof(UiText)),
|
||||
(0x10000285u, "HealthMeter", typeof(UiMeter)),
|
||||
(0x10000287u, "StaminaMeter", typeof(UiMeter)),
|
||||
(0x10000289u, "ManaMeter", typeof(UiMeter)),
|
||||
})
|
||||
{
|
||||
UiElement? el = UiElement.FindDescendant(fellowRow!, id);
|
||||
Console.WriteLine($"[socialprobe] fellowship row field {name} 0x{id:X8} -> {(el is null ? "MISSING" : el.GetType().Name)}");
|
||||
Assert.IsType(expectedType, el);
|
||||
}
|
||||
|
||||
// Checkbox labels/tooltips (0x23000003) and the Open/Close caption
|
||||
// pair (0x23000001) — the two string tables SocialFellowshipPageController
|
||||
// resolves at Bind time (lane A §4.3).
|
||||
foreach (string retailName in new[]
|
||||
{
|
||||
"IgnoreFellowshipRequests", "FellowshipAutoAcceptRequests",
|
||||
"FellowshipShareXP", "FellowshipShareLoot",
|
||||
})
|
||||
{
|
||||
string? label = strings.Resolve(0x23000003u, DatStringResolver.ComputeHash($"ID_PlayerOption_{retailName}"));
|
||||
Console.WriteLine($"[socialprobe] checkbox label ID_PlayerOption_{retailName} -> '{label}'");
|
||||
}
|
||||
foreach (string key in new[]
|
||||
{
|
||||
"ID_Fellowship_OpenFellowshipButtonText", "ID_Fellowship_CloseFellowshipButtonText",
|
||||
})
|
||||
{
|
||||
string? label = strings.Resolve(0x23000001u, DatStringResolver.ComputeHash(key));
|
||||
Console.WriteLine($"[socialprobe] fellowship caption {key} -> '{label}'");
|
||||
}
|
||||
|
||||
// Full end-to-end Bind against the PRODUCTION mount (the same
|
||||
// resolver/scrollbar/frame wiring MountSocialPanel does) — proves
|
||||
// Bind() finds every element it needs with zero "not found"
|
||||
// console warnings, not just that the individual ids resolve in
|
||||
// isolation above.
|
||||
UiElement? fellowshipPageForBind = UiElement.FindDescendant(tabs, 0x10000292u);
|
||||
Assert.NotNull(fellowshipPageForBind);
|
||||
var originalOut = Console.Out;
|
||||
var capture = new StringWriter();
|
||||
Console.SetOut(capture);
|
||||
SocialFellowshipPageController? fellowshipController;
|
||||
try
|
||||
{
|
||||
fellowshipController = SocialFellowshipPageController.Bind(
|
||||
fellowshipPageForBind!,
|
||||
new SocialFellowshipPageController.Bindings(
|
||||
Snapshot: () => new RuntimeFellowshipSnapshot(),
|
||||
Members: () => [],
|
||||
TemplateResolver: rowTemplates.Resolve,
|
||||
Create: (_, _) => default,
|
||||
Recruit: _ => default,
|
||||
Dismiss: _ => default,
|
||||
Quit: _ => default,
|
||||
AssignLeader: _ => default,
|
||||
SetOpen: _ => default,
|
||||
SetPanelOpen: _ => default,
|
||||
Selection: new AcDream.Core.Selection.SelectionState(),
|
||||
LocalPlayerGuid: () => 0u,
|
||||
CurrentCharacterOption: _ => false,
|
||||
SetCharacterOption: (_, _) => { },
|
||||
ResolveString: (tableId, stringId) => strings.Resolve(tableId, stringId)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
string bindLog = capture.ToString();
|
||||
Console.WriteLine($"[socialprobe] fellowship Bind() console output:\n{bindLog}");
|
||||
Assert.NotNull(fellowshipController);
|
||||
Assert.DoesNotContain("not found", bindLog);
|
||||
|
||||
// Allegiance signature elements.
|
||||
foreach ((uint id, string name) in new[]
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue