Commit graph

7 commits

Author SHA1 Message Date
Erik
5bdd0528f1 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>
2026-08-12 04:40:53 +02:00
Erik
ae77270939 fix(ui): fellowship checkbox count, row-text + ListBox Flush doc corrections, promote probe findings to assertions
Mechanism SHOULD-FIX 4: SocialFellowshipPageController's class doc said
0x1000026B holds "all three visible option checkboxes" — the fixture and
the decomp both authors FOUR (Ignore Fellowship Requests / Auto-Accept
Requests / Share XP / Share Loot, 0x10000270-0x10000273). Corrected.

Mechanism SHOULD-FIX 9: SocialPanelRowText.FindDeepest's doc promised
"the deepest UiText descendant" but the implementation returns the LAST
match in pre-order traversal order, which only equals the deepest when
the subtree is a single chain. Both real row templates ARE single
chains today, so behavior is unaffected — the doc now describes what the
code actually does instead of a stronger guarantee it doesn't implement.

Blast SHOULD-FIX 5: UiTemplateListBox.Flush()'s doc only mentioned the
ContentHeight reset; UiScrollablePanel.ClearContent() also resets scroll
position to 0, which the sibling UiItemList.Flush() (same method name,
different semantics) does NOT do. Documented explicitly, including the
UX cost this creates for a scrolled-in Friends/Squelch roster once its
scrollbar is wired (this fix round's blast MF-1) — flagged for whoever
revisits Friends/Squelch scrolling next rather than silently fixed as an
unasked behavior change.

Mechanism SHOULD-FIX 3: SocialPanelLiveMountProbeTests printed two
headline findings (the 0x10000492-authored-twice count, page exclusivity
after ActivateTabBehavior) without ever asserting them — a future
importer regression collapsing/dropping an instance, or breaking
exclusivity, could not fail this test. Both are now real assertions
(Assert.Equal(2, passupCount); exactly one page Visible and it is
Allegiance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 03:44:05 +02:00
Erik
0a9ca2f1f9 feat(ui): FA3 -- mount the social panel shell (Friends/Allegiance/Fellowship/Squelch)
Campaign FA slice FA3: retail's four-tab social panel (LayoutDesc
0x2100006E slot 0x1000018F, RetailPanelCatalog id 12), built on the OP3
OptionsPanelController recipe -- Type-8 tab host, ActivateTabBehavior,
per-page scoped controllers.

- SocialPanelController mounts the tab host and wires the close button;
  F3 (ToggleAllegiancePanel) and F4 (ToggleFellowshipPanel) open the
  panel and switch to their own tab, sharing the same gmPanelUI
  one-active-panel exclusivity every sibling main panel already has.
- The live-DAT tab table CORRECTS the coordinator addendum's x-order
  guess: button 0x1000028C ("Allegiance") pairs with page 0x10000291 and
  is the authored DEFAULT entry, not Friends -- each button's own page
  id and its own P0x57 (matching the F3/F4 ActionMap ids on the
  Allegiance/Fellowship pages specifically) both corroborate the real
  pairing. See SocialPanelController's class doc for the full table.
- SocialFellowshipPageController swaps the two authored empty/full
  frames (0x1000026B/0x10000275) on RuntimeFellowshipState's
  IsInFellowship -- both frames' full containment (name box, create
  button, checkboxes vs. roster list, six buttons) was confirmed by the
  live-mount probe, so a single Visible toggle per frame is the whole
  swap (closes lane-A unknown U6).
- SocialAllegiancePageController hides the monarch/patron blocks and
  blanks their name text to a literal space when
  RuntimeAllegianceState.Snapshot.HasProfile is false, using SCOPED
  FindDescendant lookups (the panel authors 0x10000492 twice, once per
  block).
- SocialFriendsPageController/SocialSquelchPageController bind their
  ListBoxes read-only to RuntimeCommunicationState's existing J4.1
  Friends/Squelch owners (names only), rebuilding on revision change.
  Their action buttons are honest INERT (D1) -- register row AD-79.
- UiTemplateListBox gains Flush() (lane A/D's "Gap found" prerequisite)
  so a poll-and-rebuild list can shrink between refreshes.
- RetailPanelCatalog.SocialPanel = 12, byte-verified from the live slot's
  own P0x10000029; listed in Mounted only (no toolbar button -- lane A
  §6.1: the open path is keyboard-only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 02:58:09 +02:00
Erik
057d8cd703 fix(ui): #372 — Options tabs no longer blank; UiTemplateListBox viewport fills its ListBox
ROOT CAUSE (proven, not guessed): the lazily-created row viewport was
constructed 0x0 with Left|Top|Right|Bottom fill-anchors. Its first
ApplyAnchor captured mR = parentW - (0+0) = parentW, so
ComputeAnchoredRect's l&&r branch (w = parentW - mR - mL) kept it 0x0
forever. A 0-tall viewport makes UiScrollablePanel.LayoutScrollableChildren
cull every row, so Character/Chat/Config rendered blank while Gameplay
(no viewport — authored static children sized at Build) worked. This is
the exact Gameplay-vs-rest split the user's first connected gate found.

FIX: seed the viewport to the ListBox's current extent at creation, so the
fill-anchor baseline is mR = parentW - parentW = 0 and the viewport tracks
the parent. The ListBox is a static dat child sized at Build, so its extent
is authored by the time the viewport is lazily created during Bind.
Dormancy preserved — the viewport is still created only on the first row.

Reproduced RED then GREEN by UiTemplateListBoxViewportTests (viewport fills;
rows stay visible after the anchor+cull layout pass) — the layout path the
whole fixture conformance suite structurally never drove, which is why
every OP2-OP6 test was green over a live-only blank-tab failure. Full
Release suite 13,131 / 4 skips / 0 failed.

Still owed (NOT fixed here, no evidence yet): the 'only Exit Game worked'
Gameplay-buttons observation needs a re-gate (two buttons are correctly
INERT; the other four have dialog/chat effects that may have gone
unnoticed); and the 13 ID_ChatOption_TextFilter_* labels fail to resolve
(blank captions, behaviour unaffected). See #372.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 09:58:42 +02:00
Erik
e71e5a9614 feat(ui): Campaign OP slice OP5 — the Chat tab
Binds LayoutDesc 0x2100005C through OP2's template-list mechanism and
OP3's per-page OptionPage model: the General Options header + two
DualHash-linked opacity sliders (Option_DefaultOpacity_Property
0x10000080 / Option_ActiveOpacity_Property 0x10000081, live-apply on
drag through RetailWindowOpacityController, defaults read from the
installed DAT's DBProperties collection at DID 0x78000001 via
ChatOptionsDatDefaults), and the five per-window text-filter blocks
(main window 12 rows minus Gameplay, four floaties 13 rows each — the
byte-verified authored order cross-checked against the raw
gmChatOptionsUI::InitOptions/AddCheckboxBitfield64Option pseudo-C, not
just the research doc's own table) writing AcDream.Core.Chat.
ChatWindowState directly, the same state CH6's chat windows already
read.

AP-195 retired: ported both halves left open at the OP2 re-review —
the ALL-set LED media swap (new UiButton.FaceFileOverride, driven by
the block-level P0x10000082/P0x10000083 sprites now threaded through
ElementInfo/DatWidgetFactory) and the CreateChildren self-sizing tail
(UiCheckboxBitfield64.Height grows with its stacked row content; the
enclosing ListBox reflows around the block's FINAL height via the new
UiTemplateListBox.AddPrebuiltRow, reusing the ListBox's own stacking
rather than a third stacking path). AP-187 broadened to cover the main
window's own filter (previously only the four floaties) and the new
live-editing write path.

The main chat window's filter (retail window id 8, ChatWindowState id
0) gains its own settings.json persistence (ChatSettings.
ChatWindowMainFilter) alongside the pre-existing floaty 1-4 fields;
opacity persistence is now wired on every live slider change, not only
through the old dev-scaffold Settings panel.

Fixture regeneration (ACDREAM_REGENERATE_UI_FIXTURES=1) picked up the
new ElementInfo.LedCheckedSprite/LedUncheckedSprite fields across all
19 committed layout fixtures — purely additive, confirmed against the
live installed DAT (0x10000520's own 0x82/0x83 properties resolve to
0x06004D17/0x06004D19 exactly as AP-195 documented).

Conformance: FilterRows/FilterBlocks pinned against the byte-verified
authored order and ChatWindowState's own default constants; the AP-195
LED swap and self-sizing behavior; the DAT opacity-default extraction
against the live installed DAT; live filter/opacity writes reaching
ChatWindowState/RetailWindowOpacityController; OnShown re-seed and
Reset/Defaults ghosting per the OP4 binding-pattern discipline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 06:25:59 +02:00
Erik
b236a44279 fix(ui): OP2 rework — dormant UiDatElement subclasses, fixed Panel/CheckboxBitfield64 mechanism
OP2 (df9c7a35) was double-REJECTed: an unconditional Type-8/Type-5 factory
mapping silently re-classed 15 elements across 7 shipped panels (vendor
backdrop lost its fill, character/spellbook roots stopped passing clicks
through, combat gained a phantom import-time tab takeover, ten ListBoxes
gained a spurious hit-testable viewport) because the stale 27 pre-existing
fixtures never exercised the new fields — and the mechanism itself cited a
nonexistent "UIElement_TabControl" class, inverted UiCheckboxBitfield64's
checked-state predicate, and synthesized fake per-row geometry instead of
using the widget's own authored template.

Shape change: UiTabPanel (renamed from UiTabControl) and UiTemplateListBox
now derive from UiDatElement (unsealed) and stay DORMANT by default — an
imported Type-8/Type-5 element gets authored-media drawing, ClickThrough
generic-decoration default, and IUiDatStateful propagation identical to the
pre-OP2 UiDatElement fallback, with zero import-time side effects. The
factory's Type-8/Type-5 arms are unconditional again (no more guard whose
premise the blast-radius sweep proved false), because dormancy makes an
unactivated instance behaviorally indistinguishable from the old fallback.
UiTabPanel.ActivateTabBehavior() and UiTemplateListBox's lazy viewport
creation are the explicit, controller-driven opt-ins Campaign OP slice OP3+
will call; today nothing does, so the four pre-existing shipped Type-8
hosts (character/spellbook/vendor/combat) and ten pre-existing Type-5
ListBoxes keep their pre-OP2 behavior exactly. Filed AD-73 for this
dormant-vs-retail's-unconditional-activation adaptation.

Mechanism fixes (docs/research/2026-08-11-op2-review-mechanism.md):
- UiTabPanel cites UIElement_Panel (Type 8 is UIElement_Panel; no
  UIElement_TabControl exists in the PDB), resolves buttons/pages via a
  GetChildRecursive-equivalent descendant search (not direct-children-only),
  performs no switch when no entry authors 0x32 (deleted the _tabs[0]
  fallback), and surfaces unresolved tab-table entries via UnresolvedEntries
  + a diagnostic line instead of a silent no-op.
- ElementReader.ReadTabTable skips entries missing 0x30/0x31, matching
  retail's SetupTabPageHash @0x0046C2E0 entry filter.
- UiCheckboxBitfield64 now builds every row from its OWN authored template
  (property 0x64 -> {0x2100002B, 0x10000521}) via AddItemFromTemplateList,
  deleting the synthesized ElementInfo + invented RowHeight=14 — matching
  retail's CreateChildren @0x00485DF0, which is itself a UIElement_ListBox
  call. IsSet is now retail's ANY-bit-set predicate (Refresh @0x004859C0),
  not all-bits-set. TS-72 retired: the click-toggle bit math is now fully
  decomp-confirmed (SetBitsOnOrOff via ListenToElementMessage @0x00485AE0).

Regenerated all 32 UI fixtures against real DAT (ACDREAM_REGENERATE_UI_FIXTURES=1)
and committed them — 27 pre-existing fixtures now carry Outline/OutlineColor/
TabTable/TemplateList/ScrollbarElementId; the 5 Options fixtures were already
current. Updated EffectsUiControllerTests' now-correct UiTemplateListBox
class-identity assertion. Added: 6 built-widget behavior pins for all five
pre-existing Type-8 elements + a representative Type-5 element the dormancy
model protects (OP2ReworkBlastRadiusConformanceTests.cs); 5 reader-level
tests driving ReadTabTable/ReadTemplateList/the 0x72 reader from raw
property bags (ElementReaderTests.cs); a multi-bit-mask UiCheckboxBitfield64
test proving the any-bit predicate (the prior single-bit test couldn't
distinguish it from all-bits); an activation-idempotency test and a
before-activation click-is-inert test for UiTabPanel.

Full Release suite: 12,868 passed / 4 skipped / 0 failed (baseline 12,853/4/0
post-OP1-fixes; +15 net new tests, zero regressions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 01:08:08 +02:00
Erik
df9c7a35eb feat(ui): Campaign OP slice OP2 — tab control, template ListBox, UIOption widget mappings
Ships the two new widget primitives the retail Options panel needs plus the
four remaining UIOption_* factory mappings, so every tab page (OP3-OP6) has
somewhere to mount.

- ElementReader/ElementInfo gain three new dat-property readers, following
  the existing effective-state-resolution pattern (never a per-state
  first-wins scan, per the round-5 N1 lesson): the Type-8 tab table
  (property 0x2E -> TabTable), a ListBox's row-template list (property
  0x64 -> TemplateList), and scrollbar linkage (property 0x72 ->
  ScrollbarElementId). LayoutImporter gains one hook
  (IUiChildrenAttachedListener) so a widget can resolve cross-references
  its own dat properties name by id once its subtree actually exists.

- UiTabControl (Type 8): switches exactly one page-slot child visible,
  syncs each tab button's Open/Closed state via the existing
  RetailTabBinding helper, and honors the authored default tab on mount.

- UiTemplateListBox (Type 5 with an authored template list): wraps a
  UiScrollablePanel viewport (sealed, so composition not inheritance) and
  ports AddItemFromTemplateList(index) — the resolver seam a page
  controller wires with real DAT access via the SAME
  LayoutImporter.ImportInfos(dats, layoutId, elementId) overload
  RetailDialogFactory already uses for its catalog LayoutDesc.

- DatWidgetFactory maps the four remaining UIOption_* widgets, each
  verified against the regenerated options_2100002B.json fixture before
  writing any code: 0x10000037 (Slider) is structurally an ordinary
  horizontal UIElement_Scrollbar, so it reuses BuildScrollbar directly;
  0x10000038 (Menu) is structurally identical to the vendor category
  dropdown UiMenu already models, so it reuses `new UiMenu()` like the
  Type-6 case; 0x10000036 (CheckboxSlider) composes an existing
  UIOption_Checkbox child + UIOption_Slider child via the new
  UiOptionToggleSlider wrapper; 0x10000044 (CheckboxBitfield64) authors
  zero children in the dat (every row is added at runtime via retail's own
  AddChild(lowMask, highMask, label, tooltip) call shape), so it's a new
  UiCheckboxBitfield64 composing UiButton per row. No new drawing code
  anywhere in this set.

- Five new committed fixtures (options_2100002B/2100002A/21000028/
  2100005C/21000029) plus 25 new conformance tests pinning the tab table
  (4 entries, Gameplay default), all three template arrays, scrollbar
  linkage, every new widget-type mapping, and a UiTabControl behavioral
  test (switch -> exactly one page visible, click-through the tab
  button). The Character ListBox's authored 6-header/49-toggle shape
  (lane B section counts) is proven reachable end-to-end through
  AddItemFromTemplateList against the committed fixture.

- Regenerating fixtures also touched 27 PRE-EXISTING, unrelated fixtures
  (an Outline/OutlineColor field pair added by an earlier commit,
  bcc34ee3, that predates when those fixtures were last regenerated).
  Per the slice contract, that drift was NOT committed — reverted back to
  HEAD, only the five new Options-panel fixtures are new files here.

- Filed TS-72: UiCheckboxBitfield64's click-toggle bit math (AND/OR
  set/clear semantics) is a documented approximation — the decompiled
  excerpt this campaign pulled covers UIOption_CheckboxBitfield64::Apply's
  WRITE side, not its own click-handler's bit math. Flagged for OP5 (the
  Chat tab controller, the first consumer that reaches the wire) to
  verify against the real decomp before any live transaction depends on
  it; nothing user-reachable can observe this yet.

Full Release suite: 12,770 passed / 4 skipped / 0 failed (was 12,745/4/0
post-OP1 — 25 net new tests, zero regressions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 00:06:43 +02:00