acdream/docs/research/2026-08-11-fa-panel-structure.md
Erik a555390483 docs(fa3): fix gate script tab order + false-defect route, add scroll/restore-open steps, correct #383 timing, fix bold markers + add U11
Mechanism MUST-FIX 1: the connected-gate script's steps 1/9/10 carried
the REFUTED x-order guess forward — claiming Friends was drawn left-most
and that the authored default (Allegiance) was somehow NOT the left-most
tab. The real authored order (fixture + live-mount probe, corroborated
by each page's own P0x57 action-map id) is Allegiance (x=0, DEFAULT),
Fellowship, Friends, Squelch — the default tab IS the left-most tab.
Fixed steps 1, 9, 10, and the "What to report" bullet that repeated the
wrong claim.

Mechanism MUST-FIX 2: step 14 sent the user to `@allegiance info` as a
trigger that would supposedly reveal the monarch/patron blocks, and told
them to report it if it didn't — the trigger CANNOT fire post-FA2
(0x0020 AllegianceUpdate is the only inbound writer of this panel's
data; 0x027C, the @allegiance info response, stopped seeding it in
4272ad0e) and FA3 sends no 0x001F subscription at all (FA5 scope). The
script primed the user to file a false defect. Rewritten to state the
true FA3 expectation: @allegiance info prints real data to chat, the
panel blocks stay hidden regardless, for the whole gate — report
NEITHER half as a bug; the actual anomaly to watch for is the blocks
becoming visible at all.

Mechanism SHOULD-FIX 4: step 11's Fellowship checkbox count hedge
("three... a fourth may also be present") replaced with the settled
count (four).

Blast NIT 8 / gate note: added two steps the original script never
exercised — a long-roster Friends/Squelch scroll check (exactly where
blast MF-1's scrollbar-wiring fix bites, and a short test roster would
never surface it) and an honest restore-open-across-relaunch
observation step (the social panel follows the SAME restore-open
convention every sibling main panel already has — Options/Spellbook/
Character/Inventory/Vitae — stated up front so it isn't mistaken for a
bug mid-gate).

Blast SHOULD-FIX 6: docs/ISSUES.md #383 said the two drifted fixtures
were committed "days ago" — git says otherwise: ~18h and ~21h before the
FA3 regeneration run, the previous day. Corrected, and added the
mechanism reviewer's no-drift finding for the NEW social-panel fixture
(cross-checked against the live probe on every axis, zero drift) —
narrows the issue to exactly the two pre-existing OP-era fixtures.

Blast SHOULD-FIX 7: the §10 addendum in fa-panel-structure.md had five
`**` bold markers (odd count) — an orphaned trailing marker bled bold
formatting into the following section. Dropped the orphan; the addendum
now bolds only its lead sentence and the inline "Allegiance" callout,
both balanced pairs.

Mechanism SHOULD-FIX 1 (research-doc half): filed unknown U11 in §8 —
what a repeat F3/F4 press does when the panel is open on the OTHER tab
is not established from retail decomp (no OnAction consumer exists for
either action in the binary); acdream's own OpenSpellbook-precedent
choice is not a retail port.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 03:44:26 +02:00

62 KiB
Raw Permalink Blame History

Retail Fellowship & Allegiance panels — class families, mount, element inventory, dialogs, open paths

Date: 2026-08-11 Lane: Campaign FA RESEARCH LANE A — retail panel STRUCTURE. Status: RESEARCH ONLY — no production code changed, nothing built, nothing launched, no client attached, no DATs read. Quality bar / format model: docs/research/2026-08-10-options-panel-structure.md (especially its §10.1 structural-inventory style). Where that doc is the authority for a shared mechanism (the 0x2100006E floaty host, P0x10000029 slot keys, the AddItemFromTemplateList row mechanism, the UIOption_Checkbox row shape) this doc cites it rather than re-deriving it.


0. Primary sources and evidence discipline

Source Use
docs/research/named-retail/acclient_2013_pseudo_c.txt Binary Ninja pseudo-C, Sept 2013 EoR build, PDB names. Cited as pseudo_c:<line>.
docs/research/named-retail/acclient.h Verbatim retail header struct definitions — the member lists in §1.2 are copied from it, not inferred.
docs/research/named-retail/symbols.json Address ↔ name, cited as VAs.
Ghidra MCP on 127.0.0.1:8081 (patchmem.gpr, same 2013 build + full PDB) GET /decompile_function?address=0x…. Ghidra resolves the named string-id globals and element-id constants that Binary Ninja folded to 0. Every SetStringIDandTableEnum(&si, 0, 0x10000001) in the BN text below was re-read here and came back with a real ID_* symbol. Cited as ghidra@0x….
docs/research/named-retail/retail-default.keymap.txt / keymap-default.txt The two open actions and their default keys.
acdream tree (src/AcDream.App/UI/**, tests/.../fixtures/*.json) Seam comparison + the collision sweep in §6.3.

Not used, and therefore an explicit limit on this doc: the installed DATs. Every authored fact (LayoutDesc DIDs, geometry, media, base references, template arrays, the P0x10000029 slot keys, the static labels code never touches) is listed in §8 Unknowns for the coordinator with an exact verification recipe. Nothing in this doc is guessed; where the decomp only strongly indicates something, it says so.

0.1 BN zero-fold ledger (per the brief's warning)

Binary Ninja rendered 21 distinct StringInfo::SetStringIDandTableEnum(&si, 0, <tableEnum>) call sites across the two classes with a literal 0 first operand. All 21 are resolved, not asserted — Ghidra's decompiler of the same binary names the global at each site (§4.1/§4.2 tables). Additionally:

  • The ID_* globals themselves are = 0x0 in the pseudo-C data dump (pseudo_c:1147705-1147793). That is not a fold — they are genuinely zero at link time and filled at startup by ID_Fellowship_FellowName = compute_str_hash("ID_Fellowship_FellowName") (pseudo_c:767586-767956). See §4.3 — this changes how acdream must resolve them.
  • Two BN neg/sbb zero-idioms appear in gmFellowshipUI::UpdateButtons (((eax - eax) & 0xc) + 1 and ((ebx - 1) & 0xfffffff4) + 0xd). Both are decoded in §3.4; Ghidra renders the first honestly as (-(iVar4 != 0) & 0xcU) + 1 (ghidra@0x0048E6C0:97), which corroborates the decode.

1. The class family (Q1)

1.1 Headline

There is no gmSocialUI, and Fellowship and Allegiance are not tabs of one panel. They are two independent UIElement subclasses, each registered under its own element class id, each authored in its own LayoutDesc, each mounted as its own sibling in the shared gmPanelUI page stack — exactly the Character-Info / Options pattern.

Retail class Element class id Register Create PostInit Base classes (acclient.h)
gmAllegianceUI 0x1000002C 0x004911F0 0x00490D10 0x00490E90 UIElement_Field, gmNoticeHandler, QualityChangeHandler
gmFellowshipUI 0x1000002D 0x0048E6A0 0x0048E400 0x0048FD40 UIElement_Field, gmNoticeHandler
  • UIElement::RegisterElementClass(0x1000002c, gmAllegianceUI::Create)pseudo_c:157423
  • UIElement::RegisterElementClass(0x1000002d, gmFellowshipUI::Create)pseudo_c:154913
  • Both are registered from the same startup block as every other panel class (pseudo_c:135145-135146).
  • Struct definitions: acclient.h:56038 (gmAllegianceUI), acclient.h:56065 (gmFellowshipUI).

Neighbourhood in the class-id space (full RegisterElementClass sweep of the binary): 0x1000002A gmAttributeUI, 0x1000002B gmSkillUI, 0x1000002C gmAllegianceUI, 0x1000002D gmFellowshipUI, 0x1000002E gmSpellbookUI, 0x1000002F gmSpellComponentUI. The other social-ish surfaces are also separate classes and separate windows — they are not siblings inside a social panel: 0x10000045 gmFriendsUI, 0x10000046 gmCharacterTitleUI, 0x10000047 gmSquelchUI, 0x10000048 gmJournalUI, 0x10000049 gmPageListUI. Their struct definitions sit immediately after Fellowship's in the header (acclient.h:56085 gmFriendsUI, :56098 gmSquelchUI), which is why a "social host" is easy to hypothesise and wrong.

1.2 Member fields — verbatim from acclient.h

These member lists are the element inventory: every UIElement* member is one authored element the panel binds in PostInit.

gmAllegianceUI (acclient.h:56038-56061):

bool               m_bAwaitingUpdate;
unsigned int       m_iidSelectedVassal;
unsigned int       m_iidPossibleNewPatron;
unsigned int       m_iidPossibleKickedVassal;
unsigned int       m_uiAcceptSwearServerContextID;
UIElement_Text    *m_pAllegianceName;
UIElement_Text    *m_pPlayerFollowers;
UIElement_Text    *m_pPlayerRank;
UIElement         *m_pMonarchField;
UIElement_Text    *m_pMonarchLabel;
UIElement_Text    *m_pMonarchName;
UIElement_Text    *m_pMonarchFollowers;
UIElement         *m_pPatronField;
UIElement_Text    *m_pPatronName;
UIElement_ListBox *m_pVassalListBox;
UIElement_Button  *m_pSwearButton;
UIElement_Button  *m_pBreakButton;
UIElement_Button  *m_pKickButton;
unsigned int       m_swearContext;
unsigned int       m_acceptSwearContext;
unsigned int       m_breakContext;
unsigned int       m_kickContext;

gmFellowshipUI (acclient.h:56065-56082):

CFellowship       *m_pFellowship;
unsigned int       m_iidSelectedFellow;
unsigned int       m_uiAcceptFellowRequestServerContextID;
unsigned int       m_fellowRequestContext;
UIElement         *m_pNotInAFellowshipFrame;
UIElement         *m_pInAFellowshipFrame;
UIElement_Text    *m_pFellowshipNameEntryBox;
UIElement_Button  *m_pCreateFellowshipButton;
UIElement_Text    *m_pFellowshipName;
UIElement_ListBox *m_pFellowsListBox;
UIElement_Button  *m_pFellowLeaderButton;
UIElement_Button  *m_pFellowQuitButton;
UIElement_Button  *m_pFellowOpenButton;
UIElement_Button  *m_pFellowRecruitButton;
UIElement_Button  *m_pFellowDismissButton;
UIElement_Button  *m_pFellowDisbandButton;

Note four dialog-context slots on Allegiance vs one on Fellowship — Allegiance runs four distinct confirmation dialogs (§5), Fellowship runs one.

1.3 Method inventory (all VAs from symbols.json)

gmFellowshipUI VA gmAllegianceUI VA
PostInit 0x0048FD40 PostInit 0x00490E90
OnVisibilityChanged 0x0048E460 OnVisibilityChanged 0x00491240
ListenToElementMessage 0x004901C0 ListenToElementMessage 0x00493030
ListenToGlobalMessage 0x004F5860 (folded no-op — see §7.1) ListenToGlobalMessage 0x004908B0
Update 0x0048F440 Update 0x00492670
UpdateButtons 0x0048E6C0 UpdateSwearButton / UpdateBreakButton 0x004908E0 / 0x004909D0
UpdateFellowStats 0x0048EB20 UpdatePlayerData 0x00491330
UpdateFellowVitals 0x0048ED60 UpdatePatronData 0x004917C0
UpdateFellowSelection 0x0048F0F0 UpdateMonarchData 0x00491B40
CreateFellowship 0x0048F730 UpdateVassalsData 0x00492340
RecruitFellow / DismissFellow 0x0048E530 / 0x0048E470 MakeSwearConfirmationDialog 0x004927B0
AssignLeadershipToFellow 0x0048F1E0 MakeAcceptSwearConfirmationDialog 0x00492990
MakeFellowRequestDialog 0x00490620 MakeBreakConfirmationDialog 0x00492BF0
RecvNotice_CloseDialog 0x0048F2C0 MakeKickConfirmationDialog 0x00492E10
RecvNotice_AbortConfirmationRequest 0x0048E430 Close{AcceptSwear,Break,Kick}ConfirmationDialog 0x00490A50 / 0x00490A90 / 0x00490B00
FellowshipDisbanded / FellowDismissed / FellowQuit / FellowAdded / FellowUpdated 0x0048F8A0 / 0x0048F970 / 0x0048FAE0 / 0x0048FC20 / 0x0048FCD0 RecvNotice_CloseDialog / _AbortConfirmationRequest / _AllegianceUpdate / _AllegianceUpdateAborted / _AllegianceLogin / _SwearAllegianceRequest / _PlayerDescReceived / _EnchantmentsChanged / _SelectionChanged 0x00492060 / 0x00490D80 / 0x00492780 / 0x00492790 / 0x00492220 / 0x00493110 / 0x00490D40 / 0x00492050 / 0x00490D70

2. LayoutDesc resolution and mounting (Q2)

2.1 How each panel's layout is resolved — not by a DID lookup in code

Neither class calls DBCache::GetDIDFromEnumStatic (0x00413910) or any literal-DID load. Both constructors take (LayoutDesc const*, ElementDesc const*) (pseudo_c:154716, pseudo_c:157087) — i.e. the element factory instantiates them from an authored element whose Type property equals the registered class id. The binding is therefore entirely data-side: some LayoutDesc contains an element with Type = 0x1000002D (Fellowship) / Type = 0x1000002C (Allegiance). This is the same mechanism the Options tab pages use (docs/research/2026-08-10-options-panel-structure.md §1.2 — page roots typed 0x10000027/0x10000028/0x10000029/0x10000042).

Consequence: the two LayoutDesc DIDs cannot be recovered from the decomp at all. They are §8 item U1.

2.2 Where they mount — the shared gmPanelUI page stack, byte-exact

gmPanelUI::SetupChildren @ 0x004BC9E0 (ghidra@0x004BC9E0) is the decisive evidence. It hard-codes 16 page-stack slot element ids, and for each one reads that slot's own authored P0x10000029 enum into a PanelChildInfo { UIElement* child; ulong panelID; }:

this_01 = UIElement::GetChildRecursive(this, 0x1000018b);
UIElement::GetAttribute_Enum(this_01, 0x10000029, &local_84);
… append {child, panelID} …                       ← repeated 16×
…
for each entry: entry.child->SetVisible(false);    ← every panel starts hidden

The 16 slot ids in authored call order:

# Slot element P0x10000029 panel id Identified as
1 0x1000018B 7 Inventory
2 0x1000018F ?
3 0x1000018E 11 Character / Skills
4 0x10000559 ?
5 0x1000018C ?
6 0x10000182 ?
7 0x1000018D 10 Options
8 0x10000190 13 Magic
9 0x10000184 4 Helpful Effects
10 0x10000185 5 Harmful Effects
11 0x10000181 ?
12 0x10000189 ?
13 0x10000183 3 Character Information
14 0x1000018A ?
15 0x10000187 ?
16 0x10000188 ?

The identified rows come from docs/research/2026-07-17-retail-shared-main-panel-pseudocode.md:88-96 (a prior DAT-verified dump of 0x2100006E) plus docs/research/2026-08-10-options-panel-structure.md §1.4 for Options. 0x10000186 is conspicuously absent from SetupChildren even though it falls inside the contiguous run — flag, not fact.

So: Fellowship and Allegiance are two of the ten unidentified slots above (0x10000181, 0x10000182, 0x10000187, 0x10000188, 0x10000189, 0x1000018A, 0x1000018C, 0x1000018F, 0x10000559). RetailPanelCatalog (src/AcDream.App/UI/RetailPanelCatalog.cs:10-29) already claims panel ids 3, 4, 5, 7, 8, 9, 10, 11, 13, 15; the unclaimed ids in that space are 1, 2, 6, 12, 14. Two of those five are Fellowship and Allegiance.

I deliberately did not interpolate. The slot-id → panel-id mapping looks monotonic over the identified rows (0x183→3, 0x184→4, 0x185→5, 0x18B→7, 0x18D→10, 0x18E→11, 0x190→13), which would put Fellowship/Allegiance at 0x10000181/0x10000182 (panel ids 1 and 2). But four unidentified slots (0x1870x18A) sit between panel 5 and panel 7, where only panel id 6 is free — so monotonicity provably fails somewhere, and an interpolated answer would be a guess. §8 item U2.

2.3 One-at-a-time visibility — the behavioural contract the mount inherits

gmPanelUI::RecvNotice_SetPanelVisibility @ 0x004BC6F0 (ghidra@0x004BC6F0): showing panel P looks P up in m_childrenInfoArray, and if a different child is currently shown it reads that child's P0x10000029, sends CM_UI::SendNotice_SetPanelVisibility(previousPanelId, false), then shows P and the host frame. Hiding the active child either falls back to a remembered previous child or hides the host frame entirely. acdream already ports this as RetailPanelUiController.RegisterMainPanel (src/AcDream.App/UI/Layout/RetailPanelUiController.cs:47), which is what RetailUiRuntime.MountOptionsPanel and MountCharacterInformationPanel call.

2.4 The acdream seam — what a Fellowship/Allegiance mount will look like

Two existing mounts are the template, and they are structurally identical:

Panel Import call acdream source
Character Information LayoutImporter.ImportInfos(dats, 0x2100006E, 0x10000183)LayoutImporter.BuildCharacterController.BindRegisterIndicatorDetailPanel(RetailPanelCatalog.CharacterInformation, …) src/AcDream.App/UI/RetailUiRuntime.cs:1634-1671; ids at src/AcDream.App/UI/Layout/CharacterController.cs:14-15
Options LayoutImporter.ImportInfos(dats, 0x2100006E, 0x1000018D)BuildOptionsPanelController.BindRegisterMainPanel src/AcDream.App/UI/RetailUiRuntime.cs:2028-2075; ids at src/AcDream.App/UI/Layout/OptionsPanelController.cs:40-45

ImportInfos(host, slotElementId) returns the fully base-merged subtree at that slot (src/AcDream.App/UI/Layout/OptionsPanelController.cs:9-28 documents the empirical verification), so a Fellowship/Allegiance mount needs only the slot element id — not the panel's own LayoutDesc DID. That makes §8 item U2 the single blocking unknown for mounting, and U1 merely nice-to-have.

Contrast: most other panels (Vitae 0x21000020/0x100001C1, LinkStatus 0x2100001D/0x10000167, MiniGame 0x2100001E/0x1000016A, Spellbook 0x21000034/0x100002A8) are imported from their own LayoutDesc rather than through the host. Either route is available; the Character-Info/Options route is the one that gives the retail "one active sibling" behaviour for free.


3. Element inventories (Q3)

Every id below is a code-observed GetChildRecursive / GetChildRecursiveTemplate / SetAttribute_* operand. Geometry (x/y/w/h), media, fonts, and base references are DAT facts and are NOT in this doc — §8 item U3. The DynamicCast id column is retail's own UIElement::DynamicCast argument, which is the element Type.

3.1 Structural inventory — gmFellowshipUI (0x1000002D)

Element id Cast/Type Bound member Role Anchor
0x1000026B (none) m_pNotInAFellowshipFrame Empty-state frame — shown when you have no fellowship pseudo_c:156290 (0x0048FF88)
0x1000026F 0xC UIElement_Text m_pFellowshipNameEntryBox Editable fellowship-name field (inline, not a dialog) pseudo_c:156292,156300 (0x0048FFA0)
0x10000270 0x10000035 UIOption_Checkbox (local) Toggle → PlayerOption 2 IgnoreFellowshipRequests pseudo_c:156208-156222 (0x0048FD60); ghidra@0x0048FD40:35-52
0x10000271 0x10000035 (local) Toggle → PlayerOption 0x12 FellowshipAutoAcceptRequests ghidra@0x0048FD40:54-72
0x10000272 0x10000035 (local) Toggle → PlayerOption 0x0F FellowshipShareXP ghidra@0x0048FD40:74-92
0x10000273 0x10000035 (local) Toggle → PlayerOption 0x11 FellowshipShareLoot ghidra@0x0048FD40:94-116
0x10000274 1 UIElement_Button m_pCreateFellowshipButton Create Fellowship pseudo_c:156301,156309 (0x0048FFC6)
0x10000275 (none) m_pInAFellowshipFrame Populated-state frame pseudo_c:156291 (0x0048FF9A)
0x10000276 0xC m_pFellowshipName Current fellowship's name (display) pseudo_c:156310,156318 (0x0048FFEC)
0x10000277, 0x10000278 ? Not referenced by code — static labels/column headers. §8 U4
0x10000279 5 UIElement_ListBox m_pFellowsListBox Member list pseudo_c:156319,156327 (0x00490012)
0x1000027A ? Not referenced by code — very likely the list's scrollbar. §8 U4
0x1000027B 1 m_pFellowLeaderButton Assign leadership to selected fellow pseudo_c:156328,156336 (0x00490038)
0x1000027C 1 m_pFellowQuitButton Quit fellowship pseudo_c:156345 (0x0049007E)
0x1000027D 1 m_pFellowOpenButton Open / Close fellowship toggle (label swaps at runtime, §4.1) pseudo_c:156354 (0x004900A4)
0x1000027E 1 m_pFellowRecruitButton Recruit currently-selected world object pseudo_c:156363 (0x004900CA)
0x1000027F 1 m_pFellowDismissButton Dismiss selected fellow pseudo_c:156372 (0x004900F0)
0x10000280 1 m_pFellowDisbandButton Disband fellowship pseudo_c:156381 (0x00490113)
0x10000281, 0x10000282 ? Not referenced by code. §8 U4

Fellow row template — one row per Fellow, built by UIElement_ListBox::AddItemFromTemplateList(m_pFellowsListBox, 0, nullptr) (template index 0), then decorated:

Row element Cast/Type Content Anchor
(row root) SetAttribute_InstanceID(row, 0x1000000D, fellowIid) — the row's identity key ghidra@0x0048F440:95
0x10000283 0xC Fellow name — ID_Fellowship_FellowName + var ID_Name ghidra@0x0048F440:96-115
0x10000284 0xC Level + XP-share % — ID_Fellowship_FellowStats + vars ID_Level, ID_Experience ghidra@0x0048EB20:51-80
0x10000285 7 UIElement_Meter Health barSetAttribute_Float(bar, 0x69, cur/max) ghidra@0x0048ED60:50-57
0x10000286 0xC Health text — ID_Fellowship_FellowHealthStatus + ID_Cur, ID_Max (child of the meter) ghidra@0x0048ED60:59-73
0x10000287 7 Stamina bar ghidra@0x0048ED60:80-87
0x10000288 0xC Stamina text — ID_Fellowship_FellowStaminaStatus ghidra@0x0048ED60:89-102
0x10000289 7 Mana bar ghidra@0x0048ED60:108-116
0x1000028A 0xC Mana text — ID_Fellowship_FellowManaStatus ghidra@0x0048ED60:117-131

The health/stamina/mana ordering is byte-exact from the Fellow field offsets each bar reads: [0xA]/[7], [0xB]/[8], [0xC]/[9] (cur/max), in that source order.

3.2 Structural inventory — gmAllegianceUI (0x1000002C)

Element id Cast/Type Bound member Role Anchor
0x10000251 0xC m_pAllegianceName Your allegiance/character line — ID_Allegiance_CharacterName + ID_Name pseudo_c:157301 (0x00490F37/0x00490F57)
0x10000252 0xC m_pPlayerFollowers Your follower count — ID_Allegiance_Followers + ID_Followers pseudo_c:157310 (0x00490F7D)
0x10000253 0xC m_pPlayerRank Your rank/title — ID_Allegiance_Rank or ID_Allegiance_RankBuffed pseudo_c:157319 (0x00490FA3)
0x10000254 ? Not referenced by code. §8 U4
0x10000255 (none) m_pMonarchField Monarch block container — hidden when there is no monarch, or you are the monarch pseudo_c:157320 (0x00490FB5); ghidra@0x00491B40:78-101
0x10000256 0xC m_pMonarchLabel Monarch block's caption — text swaps, §4.2 pseudo_c:157329 (0x00490FDB)
0x10000257 0xC m_pMonarchName Monarch name (or L" " when absent) pseudo_c:157338 (0x00491001)
0x10000258 0xC m_pMonarchFollowers Monarch's follower count pseudo_c:157347 (0x00491027)
0x10000259 ? Not referenced by code. §8 U4
0x1000025A (none) m_pPatronField Patron block container — hidden when you have no patron (or your patron == the monarch) pseudo_c:157348 (0x00491039); ghidra@0x004917C0
0x1000025B ? Not referenced by code. §8 U4
0x1000025C 0xC m_pPatronName Patron name (or L" ") pseudo_c:157357 (0x0049105F)
0x1000025D0x1000025F ? Not referenced by code. §8 U4
0x10000260 5 UIElement_ListBox m_pVassalListBox Vassal list pseudo_c:157366 (0x00491085)
0x10000261 ? Not referenced by code — very likely the list's scrollbar. §8 U4
0x10000262 0x10000035 UIOption_Checkbox (local) Toggle → PlayerOption 1 IgnoreAllegianceRequests pseudo_c:157274-157292 (0x00490EB0)
0x10000263 1 UIElement_Button m_pSwearButton Swear Allegiance to the selected world object pseudo_c:157375 (0x004910AB)
0x10000264 1 m_pBreakButton Break Allegiance (from your patron) pseudo_c:157384 (0x004910D1)
0x10000265 1 m_pKickButton Break Allegiance of the selected vassal (kick) pseudo_c:157393 (0x004910F7)
0x10000490 (none) (child of m_pMonarchField) Sub-block shown only when your patron IS the monarch — carries the XP-passed-up line ghidra@0x00491B40:121-123,153-154
0x10000492 0xC (child of 0x10000490 / of m_pPatronField) "Experience passed up" text — ID_Allegiance_VassalExperiencePassedUp + ID_Value. Looked up under BOTH m_pMonarchField/0x10000490 and m_pPatronField ghidra@0x00491B40:136-139; ghidra@0x004917C0

Vassal row template — one row per direct vassal, built by AddItemFromTemplateList(m_pVassalListBox, 0, nullptr) (template index 0):

Row element Cast/Type Content Anchor
(row root) SetAttribute_InstanceID(row, 0x10000001, vassalIid)note: a different attribute key from Fellowship's 0x1000000D ghidra@0x00492340
0x10000268 0xC Vassal name (AllegianceData::GetFullName, set as literal text — no StringInfo template) ghidra@0x00492340
0x10000269 0xC XP passed up — ID_Allegiance_VassalExperiencePassedUp + ID_Value ghidra@0x00492340
0x100004AA 0xC Offline markerSetVisible(1) when !IsLoggedIn, SetVisible(0) when logged in ghidra@0x00492340

The vassal list is FLAT, not a tree. UpdateVassalsData iterates AllegianceProfile::GetFirstVassal(profile, playerId, &data)GetNextVassal(profile, prevId, &data) — the player's direct vassals only. There is no recursion, no indent level, no expand/collapse. Retail's Allegiance panel shows four things: you, your monarch, your patron, and your direct vassals.

3.3 What is not in either panel

  • No text-entry field on Allegiance. Swear targets the currently-selected world object; there is no name box.
  • No "open fellowship" checkbox. The open/closed state is a button whose caption swaps (0x1000027D, §4.1) — asked in the brief; answered: it is not a checkbox.
  • No Apply/Reset/Defaults on either panel. Both fire their wire events immediately.
  • No tab control on either panel. Neither authors a Type 8 root.
  • No allegiance MOTD / officer / ban / hometown UI. Those CM_Allegiance events exist (Event_SetMotd 0x006A7E70, Event_SetAllegianceOfficer 0x006A7C80, Event_AddAllegianceBan 0x006A7520, Event_RecallAllegianceHometown 0x006A7100, …) but no gmAllegianceUI code path calls any of them — they are chat-command-only in 2013. acdream already routes several of them through ClientCommandController (src/AcDream.App/UI/ClientCommandController.cs:334-341).

4. Runtime text, button state, and the empty state (Q3/Q5)

4.1 gmFellowshipUI — every string site

Site VA String global Variables
UpdateButtons_open_fellow == 0 0x0048E96B ID_Fellowship_OpenFellowshipButtonText
UpdateButtons_open_fellow != 0 0x0048E903 ID_Fellowship_CloseFellowshipButtonText
Update (row name) 0x0048F60E ID_Fellowship_FellowName ID_Name
UpdateFellowStats 0x0048ECAF ID_Fellowship_FellowStats ID_Level, ID_Experience
UpdateFellowVitals ×3 0x0048EEB7 / 0x0048EF9D / 0x0048F083 ID_Fellowship_FellowHealthStatus / …FellowStaminaStatus / …FellowManaStatus ID_Cur, ID_Max
RecruitFellow 0x0048E5E3 / 0x0048E5C2 ID_Fellowship_Error_CantRecruitSelf / ID_Fellowship_Error_PlayerAlreadyInFellowship
DismissFellow 0x0048E4FD / 0x0048E4AE ID_Fellowship_Error_CantDismissSelf / ID_Fellowship_Error_DismisseeNotInFellowship
AssignLeadershipToFellow 0x0048F2xx ID_Fellowship_Error_SelfAlreadyLeader
MakeFellowRequestDialog 0x00490674 ID_Fellowship_FellowshipRequest ID_Player

Thirteen ID_Fellowship_* globals exist (pseudo_c:1147705-1147717), and all thirteen are consumed above — the family is complete.

The Open/Close button reads as the ACTION, not the state. _open_fellow == 0 (a closed fellowship) shows "Open Fellowship". Verified in ghidra@0x0048E6C0:109-127.

The three error strings go to the chat scroll via ECM_UI::SendNotice_DisplayStringInfo(0x1A, &si)client-side only, no wire traffic (pseudo_c:154820, :154867). (The 0x1A channel/LogTextType is not established here — cross-check against claude-memory/project_chat_digest.md.)

4.2 gmAllegianceUI — every string site

Site String global Variables
UpdatePlayerData ID_Allegiance_CharacterName ID_Name
UpdatePlayerData ID_Allegiance_Followers ID_Followers
UpdatePlayerData (unbuffed rank) ID_Allegiance_Rank ID_Title, ID_Rank
UpdatePlayerData (buffed rank) ID_Allegiance_RankBuffed ID_Title, ID_Rank, ID_RankBuff
UpdateMonarchData (patron is monarch) ID_Allegiance_PatronSlashMonarchLabel
UpdateMonarchData (patron is not monarch) ID_Allegiance_MonarchLabel
UpdateMonarchData / UpdatePatronData / UpdateVassalsData ID_Allegiance_VassalExperiencePassedUp ID_Value
MakeSwearConfirmationDialog ID_Allegiance_SwearConfirmation ID_Player
MakeAcceptSwearConfirmationDialog ID_Allegiance_AcceptSwearConfirmation ID_Player
MakeBreakConfirmationDialog ID_Allegiance_BreakConfirmation ID_Player
MakeKickConfirmationDialog ID_Allegiance_KickConfirmation ID_Player

Eleven ID_Allegiance_* globals exist (pseudo_c:1147783-1147793); all eleven are consumed.

The rank-buffed branch is selected by CBaseQualities::InqInt(qualities, 0x1E, …) compared against the profile rank (ghidra@0x00491330:113-117).

The allegiance login/logout chat lines are hard-coded English literals, not string-table entries: RecvNotice_AllegianceLogin @ 0x00492220 builds <name> + " is logged in.\n" / " has logged out.\n" and calls ClientSystem::AddTextToScroll(text, 0, true, 0) (ghidra@0x00492220:29-52). Port note: that is a retail-faithfulness and localisation fact worth a divergence-register row if acdream chooses to table-ise it.

4.3 String tables (Q4)

Every one of the 21 sites passes table enum 0x10000001, which resolves to StringTable DID 0x23000001 by the same enum + 0x13000000 relation the Options work established (docs/research/2026-08-10-options-panel-structure.md §1.5 pinned enum 0x100000030x23000003). acdream already uses 0x23000001 for Character-Info, Combat, LinkStatus and FPS strings (src/AcDream.App/UI/RetailUiRuntime.cs:1655,1696-1700; src/AcDream.App/UI/Layout/CombatUiController.cs:267).

The two UIOption_Checkbox label/tooltip pairs are different: they pass table enum 0x100000030x23000003 (pseudo_c:156210, :156212; pseudo_c:157286, :157288), which is the same table CharacterOptionsPageController already resolves (src/AcDream.App/UI/Layout/CharacterOptionsPageController.cs:84).

How the ids are produced — this is the load-bearing part. Unlike the Options tab labels (authored StringIds in the LayoutDesc), every ID_* above is a runtime name hash: the globals are = 0x0 at link time and filled at startup by ID_Fellowship_FellowName = compute_str_hash("ID_Fellowship_FellowName") (pseudo_c:767586-767682 for Fellowship, :767876-767956 for Allegiance). acdream's DatStringResolver.ComputeHash(key) is the exact seam — the same one MountCharacterInformationPanel uses (src/AcDream.App/UI/RetailUiRuntime.cs:1654-1655). No byte-verification of these ids is needed or possible; resolve by name.

Campaign carry-over caveat (the 0x2300000D lesson). Campaign CH found that the TextFilter label family resolved only in 0x2300000D despite the code's table enum (src/AcDream.App/UI/Layout/ChatOptionsPageController.cs:99-105). The enum here says 0x23000001 / 0x23000003; which table these 24 names actually resolve in is §8 item U5 and must be swept, not assumed.

4.4 Button enable/disable — gmFellowshipUI::UpdateButtons @ 0x0048E6C0

SetState(1) = enabled, SetState(0xD) = disabled (retail's own convention, matching docs/research/2026-08-10-options-panel-structure.md §3.4).

Button Rule
Quit 0x1000027C Always enabled whenever m_pFellowship != 0
Disband 0x10000280 Enabled iff you are the leader
Open/Close 0x1000027D Enabled iff you are the leader
Leader 0x1000027B Leader-only, and disabled when the selected fellow is you
Dismiss 0x1000027F Leader-only, and disabled when the selected fellow is you; disabled when nothing is selected
Recruit 0x1000027E Enabled iff the currently-selected world object is a player, is not already a fellow, and the fellowship is not full. When you are not the leader it additionally requires _open_fellow != 0
Create 0x10000274 Disabled while the name-entry box is empty; enabled otherwise (ghidra@0x004901C0, message 0x12/0x44)

Both BN zero-idioms decode cleanly:

  • SetState(((eax - eax) & 0xc) + 1) — real sequence neg/sbb/and 3.. /add around Fellowship::IsFull: full → 0xD (disabled), not full → 1 (enabled). Ghidra renders it honestly as (-(iVar4 != 0) & 0xcU) + 1 (ghidra@0x0048E6C0:97).
  • SetState(((ebx - 1) & 0xfffffff4) + 0xd) with ebx = (selectedFellow == you): selected == you → 0xD, otherwise → 1.

gmAllegianceUI (UpdateSwearButton @ 0x004908E0, UpdateBreakButton @ 0x004909D0, Update @ 0x00492670):

Button Rule
Swear 0x10000263 Enabled iff you have no patron and a world object is selected and it is not you and it is a player and AllegianceProfile::GetData(selected) fails (i.e. they are not already in your allegiance). Otherwise 0xD
Break 0x10000264 Enabled iff you have a patron
Kick 0x10000265 Enabled iff a vassal row is selected (m_iidSelectedVassal != 0); re-evaluated on every list-selection message

4.5 Empty-state presentation (Q5)

Fellowship — an explicit two-frame swap. gmFellowshipUI::Update @ 0x0048F440:

if (ClientFellowshipSystem::GetFellowshipSystem()->m_pFellowship == 0)
    delete this->m_pFellowship;  this->m_pFellowship = null
else
    copy the live CFellowship into this->m_pFellowship

if (m_pFellowship == null) {
    UIListener::RegisterForGlobalMessage(this, 3)      // per-frame tick, §7.1
    m_pInAFellowshipFrame   ->SetVisible(false)        // 0x10000275
    m_pNotInAFellowshipFrame->SetVisible(true)         // 0x1000026B
    return                                             // nothing else runs
}
UIListener::UnRegisterForGlobalMessage(this, 3)
m_pNotInAFellowshipFrame->SetVisible(false)
m_pInAFellowshipFrame   ->SetVisible(true)
… set m_pFellowshipName, Flush the list, rebuild rows, UpdateFellowSelection …

So the "no fellowship" state is authored, not synthesised: 0x1000026B holds whatever retail wants you to see (the name-entry box 0x1000026F and Create button 0x10000274 almost certainly live inside it — the id ordering 0x1000026B < 0x1000026F < 0x10000274 < 0x10000275 supports it, but containment is a DAT fact, §8 item U6).

Allegiance — no frame swap; per-block hiding + blanking. There is no "not in an allegiance" frame. Instead:

  • UpdateMonarchData: if there is no monarch, or the monarch is you, → m_pMonarchField->SetVisible(false) and m_pMonarchName / m_pMonarchFollowers are set to the literal L" " (ghidra@0x00491B40:78-99).
  • UpdatePatronData: if there is no patron, or your patron is the monarch, → m_pPatronField->SetVisible(false) and m_pPatronName = L" ". (Careful: the "patron == monarch" case hides the patron block and instead relabels the monarch block via ID_Allegiance_PatronSlashMonarchLabel and reveals 0x10000490.)
  • UpdateVassalsData: UIElement_ListBox::Flush(m_pVassalListBox) then m_iidSelectedVassal = 0; with no vassals the list simply stays empty.
  • The player line (0x10000251/0x10000252/0x10000253) is written unconditionally.

A live-vs-dim detail worth porting: m_pPatronField is SetState(1) when the patron IsLoggedIn and SetState(0xD) otherwise; m_pMonarchField likewise. The vassal rows use a dedicated offline marker element 0x100004AA instead.

Side effect neither panel's name suggests: both allegiance updates gate chat targets — gmCCommunicationSystem::SetTalkFocusEnabled(4, patronOnlineAndPresent), (5, monarchOnlineAndPresent), (6, anyVassalOnline) (ghidra@0x004917C0, ghidra@0x00491B40:216, ghidra@0x00492340). Whatever the retail "talk focus" ids 4/5/6 mean exactly is not established here.


5. Dialogs (Q4)

5.1 The five dialogs, and they are all plain Confirmation dialogs

Every one is built the same way — no dialog LayoutDesc is named in code; the type is selected by a PropertyCollection property:

StringInfo si; si.SetStringIDandTableEnum(<ID_*>, 0x10000001); si.AddVariable_String(ID_Player, name)
PropertyCollection pc
prop.SetPropertyName(0x8E); prop.value = 1                 // dialog TYPE = 1 (Confirmation)
pc.add(prop)
prop.SetPropertyName(0xC5); prop.value = &si               // MESSAGE
pc.add(prop)
context = DialogFactory::MakeDialogInCurrentUI(&pc)
Dialog Owner field Guard / target String VA
Swear Allegiance m_swearContext Target = ACCWeenieObject::selectedID; name via GetObjectName(NAME_APPROPRIATE); no dialog if the name is empty. On success stores m_iidPossibleNewPatron = selectedID ID_Allegiance_SwearConfirmation 0x004927B0
Accept incoming swear m_acceptSwearContext Raised by RecvNotice_SwearAllegianceRequest(name, serverCtx); stores m_uiAcceptSwearServerContextID = serverCtx ID_Allegiance_AcceptSwearConfirmation 0x00492990
Break Allegiance m_breakContext Target = AllegianceProfile::GetPatron(you); no dialog if you have no patron or the name is empty ID_Allegiance_BreakConfirmation 0x00492BF0
Kick vassal m_kickContext Target = m_iidSelectedVassal via GetData; stores m_iidPossibleKickedVassal ID_Allegiance_KickConfirmation 0x00492E10
Accept fellowship invite m_fellowRequestContext Raised by RecvNotice_FellowshipRequest(name, serverCtx); stores m_uiAcceptFellowRequestServerContextID ID_Fellowship_FellowshipRequest 0x00490620

All five are re-entrancy-guarded by if (context == 0) — a second request while one is open is dropped on the floor (returns false), it does not queue a second dialog.

5.2 On close

RecvNotice_CloseDialog(context, PropertyCollection&) on both classes reads property 0x8E (must be 1) then property 0x92 (the boolean result), then dispatches on which of its context fields matches (ghidra@0x00492060, ghidra@0x0048F2C0, pseudo_c:158340-158366):

Dialog On accept On reject
Swear CM_Allegiance::Event_SwearAllegiance(m_iidPossibleNewPatron) nothing
Accept-swear CM_Character::Event_ConfirmationResponse(**1**, m_uiAcceptSwearServerContextID, result)sent either way, carrying the boolean same call, result = 0
Break CM_Allegiance::Event_BreakAllegiance(GetPatron(you)) nothing
Kick CM_Allegiance::Event_BreakAllegiance(m_iidPossibleKickedVassal) nothing
Fellow invite CM_Character::Event_ConfirmationResponse(**4**, m_uiAcceptFellowRequestServerContextID, result)sent either way same call, result = 0

Confirmation type ids: 1 = allegiance swear, 4 = fellowship. Corroborated independently by the abort handlers, which each filter on their own type: gmAllegianceUI::RecvNotice_AbortConfirmationRequest acts only if (arg2 == 1) (pseudo_c:157192), gmFellowshipUI's only if (arg2 == 4) (pseudo_c:154765). Abort calls DialogFactory::CloseDialog(context) and zeroes both the context and the stored server context.

Destructors also CloseDialog their contexts (pseudo_c:154900 for Fellowship).

5.3 acdream parity — the machinery already exists, exactly

src/AcDream.App/UI/Layout/RetailDialogData.cs:9-38 already defines the identical property map and enum:

Priority           = 0x8D
Type               = 0x8E      ← the property both panels set to 1
AcceptLabel        = 0x90
RejectLabel        = 0x91
ConfirmationResult = 0x92      ← the property both panels read on close
QueueKey           = 0xC3
Message            = 0xC5      ← the StringInfo both panels set
enum RetailDialogType { Confirmation = 1, … }

and RetailDialogFactory.MakeDialog(data, callback) (src/AcDream.App/UI/Layout/RetailDialogFactory.cs:75) is the port of MakeCallbackDialogInCurrentUI. No new dialog work is needed for Campaign FA beyond wiring five RetailDialogData builders and their callbacks.


6. Open paths, wire events, and collisions (Q5 + Q3's collision ask)

6.1 Open path — keybind only; there is no toolbar button

Panel Input action Default key Anchors
Allegiance 0x1000000E ToggleAllegiancePanel F3 retail-default.keymap.txt:141; keymap-default.txt:132 (F3 scan=0x3D Action=0x1000000E Activation=0x03)
Fellowship 0x1000000F ToggleFellowshipPanel F4 retail-default.keymap.txt:142; keymap-default.txt:133 (F4 scan=0x3E Action=0x1000000F Activation=0x03)

The retail toolbar's full action row is 0x1000000D, 0x10000010, 0x10000013, 0x100001E7, 0x10000016, 0x1000001A, 0x10000019 (docs/research/2026-08-10-options-panel-structure.md §2.2, from the committed toolbar_21000016.json fixture) — neither 0x1000000E nor 0x1000000F is present. Retail opens these two panels from the keyboard only.

Warning — numeric-namespace collision. 0x1000000E and 0x1000000F are also element class ids (gmKeyboardUI and gmPowerbarUI, pseudo_c:225419, :222537) and also DB-type enums. Grepping those literals in the pseudo-C returns mostly false positives; the keymap files are the authority for the action-id reading.

By the Options-panel pattern the panel root should carry P0x57 = <its toggle action> and any close button P0x12 = <the same action> (docs/research/2026-08-10-options-panel-structure.md §2.3). I did not find a GetAttribute_Enum(this, 0x57, …) read site in this binary either — the same UNVERIFIED status that doc records. §8 item U7.

Also observed: gmAllegianceUI::ListenToGlobalMessage(1, 0x27) clears the vassal list selection (UIElement_ListBox::SetSelectedItem(m_pVassalListBox, nullptr, true), ghidra@0x004908B0). Global message 1 is "an input action fired"; action 0x27 is in the small 0x0000xxxx engine-action family (cf. Action=0x00000029 = W in keymap-default.txt:24) and its identity is not established here. §8 item U8.

6.2 Element messages and wire events

gmFellowshipUI::ListenToElementMessage @ 0x004901C0 switches on idMessage - 1 via lookup_table_490418[0x44] + jump_table_490408 (pseudo_c:156546-156638), then on idElement - 0x10000274 via jump_table_49045c[0xD]:

idMessage Meaning Handling
1 button clicked the seven-way element switch below
4, 0x43 ListBox selection changed read row's 0x1000000DACCWeenieObject::SetSelectedObject (+ target-mode execute) → m_iidSelectedFellowUpdateButtons
0x12, 0x44 text changed empty name-box → Create SetState(0xD), else SetState(1)
Element (+offset) Action Wire
0x10000274 (+0) CreateFellowship CM_Fellowship::Event_Create(name, PlayerModule::FellowshipShareXP()) @ 0x006A67A0
0x1000027B (+7) AssignLeadershipToFellow(m_iidSelectedFellow) CM_Fellowship::Event_AssignNewLeader @ 0x006A5F70
0x1000027C (+8) Quit — if you are the leader it first hands leadership to Fellowship::GetNonLeaderFellowID CM_Fellowship::Event_Quit(**0**) @ 0x006A61E0
0x1000027D (+9) flip _open_fellow locally, then send CM_Fellowship::Event_ChangeFellowOpeness(newState) @ 0x006A6040
0x1000027E (+0xA) RecruitFellow(ACCWeenieObject::selectedID) CM_Fellowship::Event_Recruit @ 0x006A62B0
0x1000027F (+0xB) DismissFellow(m_iidSelectedFellow) CM_Fellowship::Event_Dismiss @ 0x006A6110
0x10000280 (+0xC) Disband CM_Fellowship::Event_Quit(**1**) @ 0x006A61E0

Quit and Disband are the same wire event with a different flagEvent_Quit(0) vs Event_Quit(1). Offsets +1..+6 (0x100002750x1000027A) fall to the default arm, which is consistent with them being the frame, name text, headers, list and scrollbar.

CreateFellowship @ 0x0048F730 also runs the typed name through ACCharGenData::FormatName and writes the formatted name back into the entry box before sending — a visible retail behaviour.

gmAllegianceUI::ListenToElementMessage @ 0x00493030 is far simpler (ghidra@0x00493030): idMessage == 10x10000263/0x10000264/0x10000265 open the Swear/Break/Kick dialogs; idMessage == 4 → read the row's 0x10000001 into m_iidSelectedVassal and enable/disable Kick. No text messages, no 0x43/0x44 arms.

6.3 Subscription / refresh lifecycle

Fellowship Allegiance
On show OnVisibilityChanged(true)CM_Fellowship::Event_UpdateRequest(1) (pseudo_c:154778) OnVisibilityChanged(true)Event_UpdateRequest(1) + ClientUISystem::IncrementBusyCount (hourglass) if not already awaiting
On hide Event_UpdateRequest(0) Event_UpdateRequest(0) + DecrementBusyCount
On PostInit registers 9 notice handlers + global message 1, then calls Update registers 9 notice handlers + global message 1 + two quality handlers, then sends Event_UpdateRequest(1) and sets m_bAwaitingUpdate
Quality triggers RegisterQualityHandlerForThePlayer(IID_StatType /*7*/, 0x19) and (7, 0x1A) = PropertyInstanceId.Patron = 25 / Monarch = 26 (src/AcDream.Core/Properties/PropertyInstanceId.cs:57-58). Either changing → Event_UpdateRequest(1)
Update arrival RecvNotice_FellowshipUpdate / _FellowAdded / _FellowQuit / _FellowDismissed / _FellowUpdated / _FellowshipDisbanded RecvNotice_AllegianceUpdate / _AllegianceUpdateAborted / _EnchantmentsChanged / _PlayerDescReceivedUpdate

gmAllegianceUI::Update runs, in order: UpdatePlayerData, UpdateMonarchData, UpdatePatronData, UpdateVassalsData, UpdateSwearButton, UpdateBreakButton, then Kick's own enable check (pseudo_c:158677-158692).

6.4 Element-id collision sweep

Code-side (complete for the binary). Every element id in §3 was counted across the whole 1.4 M-line pseudo-C. Each appears only inside its own class's functions (12 hits, both in the same class). No cross-class code collision exists.

Layout-side (partial — 33 committed fixtures only). I walked every Id field in all 33 JSON layout fixtures under tests/AcDream.App.Tests/UI/Layout/fixtures/ against the full candidate range 0x100002510x1000028A (both panels' ids plus every unreferenced id between them) — zero collisions. That covers options (0x2100002B, 0x21000028, 0x21000029, 0x2100002A, 0x2100005C), keyboard config (0x21000009), toolbar, chat, character, inventory, paperdoll, spellbook, combat, radar, vitals, vitae, effects, appraisal, dialogs, vendor, indicators, powerbar, link-status, mini-game, smartbox-FPS.

This is not a full clearance. The campaign's 0x10000211-in-two-layouts precedent means the real test is the panel's own resolved subtree against the live trees acdream mounts, which requires the DAT dump — §8 item U9. Two specific risks to check there:

  1. 0x10000490, 0x10000492, 0x100004AA sit in a different, much busier numeric band than the rest of the Allegiance panel and are the most likely to collide.
  2. 0x10000492 is looked up under two different parents in retail (m_pMonarchField/0x10000490 and m_pPatronField) — i.e. retail itself authors two physical elements under one id, and both lookups are scoped. A flat layout.FindElement(0x10000492) will pick the wrong one. This is exactly the Apply/Reset/Defaults hazard OP4 already solved with a scoped UiElement.FindDescendant (src/AcDream.App/UI/Layout/OptionsPanelController.cs:67-79) — use the scoped search from day one.

6.5 Shared-state collision with the Options panel (not an id collision — a state one)

Five of the six checkboxes these panels host are duplicates of rows already live on the Options → Character tab:

Panel widget PlayerOption Also authored on the Character tab
Allegiance 0x10000262 1 IgnoreAllegianceRequests CharacterOptionsPageController.cs:195
Fellowship 0x10000270 2 IgnoreFellowshipRequests :196
Fellowship 0x10000272 0x0F FellowshipShareXP :198
Fellowship 0x10000273 0x11 FellowshipShareLoot :199
Fellowship 0x10000271 0x12 FellowshipAutoAcceptRequests :200

All five numeric values match acdream's enum exactly (src/AcDream.Core.Net/Messages/SocialActions.cs:365-382). Retail's UIOption_Checkbox writes through PlayerModule::SetOption and applies immediately on click (docs/research/2026-08-10-options-panel-structure.md §3.7), so both surfaces are live views of one value — and retail's CPlayerModule::OnChanged(PlayerOption) enforces the IgnoreFellowshipRequestsFellowshipAutoAcceptRequests mutual exclusion centrally (already ported at src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs:794-834). Port implication: these six checkboxes must bind to RuntimeCharacterOptionsState, not to panel-local state, and both surfaces must refresh when the other writes.

6.6 Widget-kind parity in acdream

Retail Type acdream factory arm Status
1 Button DatWidgetFactory (button arm)
5 UIElement_ListBox UiTemplateListBox (DatWidgetFactory.cs:105) ✔ built, but see gap below
7 UIElement_Meter BuildMeterUiMeter (DatWidgetFactory.cs:108) ✔ (UiMeter.Fill is the SetAttribute_Float(0x69) seam)
0xC UIElement_Text BuildText (DatWidgetFactory.cs:122)
0x10000035 UIOption_Checkbox BuildCheckbox (DatWidgetFactory.cs:125)

Gap found: UiTemplateListBox (src/AcDream.App/UI/UiTemplateListBox.cs:81-229) exposes AddItemFromTemplateList(index) and AddPrebuiltRow(row) — but no Flush, no selection model (SetSelectedItem/GetSelectedIndex), and no per-row instance-id attribute. Both panels need all three (Flush on every Update; selection to drive Dismiss/Leader/Kick; the row attribute 0x1000000D/0x10000001 as the identity key). This is a bounded, real prerequisite for Campaign FA implementation — flagging it here so the planner sizes it.

Retail detail that matters for the Open/Close button: UpdateButtons calls UIElement_Text::SetText((UIElement_Text*)m_pFellowOpenButton, …) — i.e. retail's UIElement_Button derives from UIElement_Text and a button's caption is set through the text API (ghidra@0x0048E6C0:115,129). acdream's button caption seam must allow the same runtime relabel.


7. Notes and loose ends worth carrying

7.1 gmFellowshipUI::ListenToGlobalMessage is a folded no-op — and message 3 is the frame tick

symbols.json gives gmFellowshipUI::ListenToGlobalMessage = 0x004F5860, which is an empty function shared by identical-COMDAT folding with gmPanelUI::ListenToGlobalMessage, gmPanelUI::RecvNotice_PlayerDescReceived, NoticeHandler::RecvNotice_ItemAttributesChanged and NoticeHandler::RecvNotice_DisplayWeenieError (pseudo_c:248560 / ghidra@0x004F5860). So Fellowship handles no global message directly, despite PostInit registering for message 1 and Update toggling registration for message 3.

Global message 3 is byte-identified: UIElementManager::UseTime @ 0x0045CFD0 calls BroadcastGlobalMessage(this, 3, 0) once per frame, between CheckTooltip and DrawDirtyRegions (pseudo_c:102945). So the register/unregister pattern is "tick me every frame while I have no fellowship" — plausibly for the name-entry box's caret, but the effect is UNVERIFIED because Fellowship's own handler is empty. Recorded, not asserted. §8 item U8.

7.2 RegisterNoticeHandler operands are code addresses, not ids

PostInit registers 9 handlers on each panel with operands like 0x4DD20F, 0x4DD210, 0x4DD246, 0x4DD1EF, 0x186A4 (Fellowship, pseudo_c:156384-156392) and 0x4DD1F0, 0x4DD201, 0x4DD2050x4DD208, 0x4DD20D, 0x4DD1EF, 0x186A4 (Allegiance, pseudo_c:157398-157406). The 0x4DD… values are inside the code section, i.e. they are thunk/notice-slot addresses, not notice ids; 0x186A4 (= 100004) looks like a genuine numeric id. Same shape as gmConfigUI's RegisterNoticeHandler(0x4DD276, …) recorded in the Options doc. Not decoded here; not needed for panel structure. §8 item U8.

7.3 Two different row-identity attribute keys

Fellowship rows key on 0x1000000D; Allegiance rows key on 0x10000001. Both go through SetAttribute_InstanceID / GetAttribute_InstanceID. Do not unify them in the port — they are different authored attribute names and a shared row helper must take the key as a parameter. (Both values also exist as element class ids — 0x1000000D gmExternalContainerUI, 0x10000001 gmUIElement_BurdenIndicator — a different namespace. Don't cross the wires.)


8. Unknowns for the coordinator

Everything below is genuinely not established from the decomp. U2 is the only one that blocks a mount.

# Unknown Why the decomp can't answer it Verification recipe
U1 The two panels' own LayoutDesc DIDs Both classes are instantiated from an authored element by Type; no DID is loaded in code (§2.1) DAT sweep: enumerate LayoutDescs and find the two whose element tree contains Type == 0x1000002D / Type == 0x1000002C
U2 Which 0x2100006E slot element hosts each panel, and its P0x10000029 panel id SetupChildren hard-codes the 16 slot ids but reads the key from the DAT (§2.2) For each of the 16 ids in §2.2's table, LayoutImporter.ImportInfos(dats, 0x2100006E, slotId) and read (a) P0x10000029 on the slot, (b) the subtree root's Type. Report the full 16-row table — it also closes the 10 currently-unidentified RetailPanelCatalog entries in one pass
U3 Geometry, media, fonts, base refs for every element in §3.1/§3.2 Code reveals ids, not layout Dump both slot subtrees to fixtures (fellowship_2100006E_<slot>.json, allegiance_2100006E_<slot>.json) exactly like options_panel_2100006E_1000018D.json
U4 The code-untouched element ids: Fellowship 0x10000277, 0x10000278, 0x1000027A, 0x10000281, 0x10000282; Allegiance 0x10000254, 0x10000259, 0x1000025B, 0x1000025D0x1000025F, 0x10000261, 0x10000266, 0x10000267, 0x1000026A Static labels, column headers, scrollbars and borders never appear in code Falls out of U3's dump. Expect the two …27A / …261 candidates to be the two ListBox scrollbars (ScrollbarElementId)
U5 Which StringTable the 24 ID_Fellowship_* / ID_Allegiance_* / 6 ID_PlayerOption_* names actually resolve in Code says table enum 0x100000010x23000001 (and 0x100000030x23000003), but the Campaign CH 0x2300000D precedent shows the enum can be wrong Exhaustive sweep: for each of the 24 names, ComputeHash(name) and probe every 0x23xxxxxx table (and 0x78000000, the DAT caption catalog CH used for chat captions). Report the resident table per family, not per name
U6 Whether 0x1000026F (name box) and 0x10000274 (Create button) are children of 0x1000026B (m_pNotInAFellowshipFrame), and whether the list + six buttons are children of 0x10000275 GetChildRecursive is called from the panel root, so containment is invisible to code Falls out of U3's dump. It decides whether acdream's empty-state is one SetVisible or many
U7 Whether the two panel roots carry P0x57 = 0x1000000E / 0x1000000F, and whether either panel authors a close (X) button with P0x12 = <the same action> No GetAttribute_Enum(this, 0x57, …) read site exists in the binary — the same UNVERIFIED status the Options doc records (its §2.3) Falls out of U3's dump (read P0x57 on the roots and P0x12 on any Type 1 child not in §3's tables)
U8 Three low-stakes semantics: input action 0x27 (clears vassal selection, §6.1); the RegisterNoticeHandler(0x4DD…) operands (§7.2); the effect of Fellowship's message-3 registration given its empty handler (§7.1) Would need either a cdb trace or notice-table reversing Only worth doing if a behaviour gap shows up in a connected gate
U9 Full element-id collision clearance across every layout acdream mounts I could only sweep the 33 committed fixtures (§6.4) After U3, run both panels' resolved id sets against every currently-mounted layout's id set. Prioritise 0x10000490, 0x10000492, 0x100004AA, and treat 0x10000492's intra-panel duplication as a hard requirement for scoped lookups
U10 The row-template arrays: what LayoutDesc DID + element id sits at index 0 of P0x64 on 0x10000279 (fellows) and 0x10000260 (vassals) Template arrays are authored on the ListBox (Options doc §1.5) Falls out of U3's dump — read P0x64 on both ListBoxes and dump the referenced template elements as their own fixtures
U11 [FA3 fix-round addendum, 2026-08-12, mechanism SF-1.] What a repeat F3/F4 press does when the social panel is already open on the OTHER tab — switch, or close-then-reopen, or something else No 0x1000000E/0x1000000F OnAction consumer exists anywhere in the binary: no GetAttribute_Enum(this, 0x57, …) read site, ClientUISystem::OnAction's m_InputAction > 0x7c branch handles exactly three unrelated ids, and gmPanelUI::ListenToGlobalMessage is COMDAT-folded onto a no-op (NoticeHandler::RecvNotice_DisplayWeenieError @0x004F5860) Exhausted for this campaign (see SocialPanelController.IsShowingAllegiance's own doc). acdream ships its own OpenSpellbook-precedent convention (switch, not close) — a cdb trace against a live retail client (Step -1 in CLAUDE.md's workflow) is the only remaining way to settle it, and is not worth the cost unless a connected gate reports a mismatch

9. Executive summary

  1. Fellowship and Allegiance are two separate UIElement classesgmFellowshipUI (class id 0x1000002D, Register @ 0x0048E6A0) and gmAllegianceUI (0x1000002C, 0x004911F0), both deriving from UIElement_Fieldnot tabs of a social panel; no gmSocialUI exists in the binary. Each mounts as its own sibling in the shared gmPanelUI page stack inside floaty host 0x2100006E, exactly like Character Info (0x10000183) and Options (0x1000018D), and inherits retail's one-active-sibling visibility contract for free.
  2. The complete element inventory is recovered from code — 19 bound elements + a 9-part fellow row template (name, level/XP%, and health/stamina/mana meter+text triples) for Fellowship; 15 bound elements + a 3-part vassal row for Allegiance, whose vassal list is flat (direct vassals only), not a tree. Rows on both panels are built by AddItemFromTemplateList(listbox, 0, nullptr) and keyed by a row instance-id attribute — 0x1000000D on Fellowship, 0x10000001 on Allegiance. The fellowship name is an inline editable text field (0x1000026F), and "open fellowship" is a button whose caption swaps (0x1000027D), not a checkbox.
  3. All five dialogs are plain Confirmation dialogs built from PropertyCollection 0x8E = 1 (Type) + 0xC5 (Message) and answered by 0x92 (Result) — precisely the contract RetailDialogData/RetailDialogFactory already implements. Confirmation type ids are 1 = allegiance swear, 4 = fellowship. Every panel string resolves by runtime name hash (compute_str_hash("ID_Fellowship_…")) in table enum 0x100000010x23000001 (the two option checkboxes use 0x100000030x23000003), so acdream's existing DatStringResolver.ComputeHash seam is the whole job — but which table the names actually live in still needs the 0x2300000D-style sweep.
  4. Open path is keyboard-only: ToggleAllegiancePanel = 0x1000000E (F3) and ToggleFellowshipPanel = 0x1000000F (F4); neither action appears on the retail toolbar. Empty state differs by panel — Fellowship swaps two authored frames (0x1000026B no-fellowship / 0x10000275 in-fellowship), Allegiance has no frame swap and instead hides the monarch/patron blocks and blanks their text to L" ".
  5. Blocking unknown: exactly one. Which of the 16 gmPanelUI::SetupChildren slot ids hosts each panel (§8 U2) — everything else is a nice-to-have dump. Two findings the planner should size now: UiTemplateListBox has no Flush, no selection model and no row instance-id, all three of which both panels need; and retail authors 0x10000492 twice inside the Allegiance panel (under the monarch block and under the patron block), so scoped FindDescendant lookups are mandatory from day one, not a later fix.

10. COORDINATOR ADDENDUM (2026-08-11) — U2 CLOSED by live-DAT dump; §2's mounting call CORRECTED

tests/AcDream.App.Tests/UI/Layout/FaPanelSlotProbeTests.cs (ACDREAM_PROBE_LIVE_MOUNT=1) dumped all 16 authored slots + 0x10000186 against the installed DATs. Primary results:

Fellowship and Allegiance are NOT separate gmPanelUI siblings — they are two of FOUR pages of ONE tabbed social panel at slot 0x1000018F, P0x10000029 panel id 12, root Type 8 (the same authored tab-host class OP2/OP3 already ported). Pages, with their RegisterElementClass identities:

Tab button (x-order) Page element Page class Panel
0x1000028C (x=0) 0x10000513 0x10000045 = gmFriendsUI (@0048e21a) Friends
0x1000028E (x=72) 0x10000291 0x1000002C = gmAllegianceUI Allegiance
0x10000512 (x=144) 0x10000292 0x1000002D = gmFellowshipUI Fellowship
0x1000053B (x=206) 0x1000054A 0x10000047 = gmSquelchUI (@0048d04a) Squelch

(The button→page pairing above is by authored order and needs the tab TABLE property (0x2E) read to pin exactly — one fixture dump closes it, same as U3.)

[FA3 addendum, 2026-08-12: the x-order guess above is WRONG. The FA3 fixture dump (social_panel_2100006E_1000018F.json) and its live-mount probe (SocialPanelLiveMountProbeTests) read the real authored 0x2E tab table, corroborated independently by each BUTTON's own resolved caption and each PAGE's own P0x57 property (which lines up with the real F3/F4 ToggleAllegiancePanel/ToggleFellowshipPanel ActionMap ids on the Allegiance/Fellowship pages specifically — a fact this table's own x-order guess had no way to check). The real pairing:

button 0x1000028C ("Allegiance") -> page 0x10000291 (Allegiance)  DEFAULT
button 0x1000028E ("Fellowship") -> page 0x10000292 (Fellowship)
button 0x10000512 ("Friends")    -> page 0x10000513 (Friends)
button 0x1000053B ("Squelch")    -> page 0x1000054A (Squelch)

The panel's authored default tab is Allegiance, not Friends. See src/AcDream.App/UI/Layout/SocialPanelController.cs's own class doc for the full citation.]

Every lane-C allegiance element id is CONFIRMED present under page 0x10000291: swear 0x10000263 / break 0x10000264 / kick 0x10000265 (three 88×33 buttons at y=562), vassal ListBox 0x10000260 (Type 5, 279×350) + scrollbar 0x10000261, monarch/patron/self blocks 0x10000250/0x10000255/0x1000025A, meter 0x10000262. The fellowship page carries the §3.1 inventory including the two authored empty/full frames 0x10000275/0x1000026B.

The full 16-slot table also closes the RetailPanelCatalog unknowns: 0x10000181=1 gmAbuseUI, 0x10000182=2 gmBookUI, 0x10000187=8 gmLinkStatusUI, 0x10000188=9 gmMiniGameUI, 0x1000018F=12 the social panel, 0x10000189=14 gmUrgentAssistanceUI, 0x1000018A=15 gmVitaeUI, 0x1000018C=16 gmMapUI+gmHouseUI pages, 0x10000559=25 gmJournalUI/gmPageListUI/gmContractsUI. 0x10000186 does not resolve (IMPORT NULL) — consistent with §2.2's "conspicuously absent" flag.

Plan impact: Campaign FA mounts ONE panel (id 12) with the OP3 OptionsPanelController recipe (Type-8 host, ActivateTabBehavior, per-page scoped controllers); Friends and Squelch pages ride along and map naturally onto RuntimeCommunicationState's existing friends/squelch owners (J4.1) — scope decision deferred to the plan. U3/U4/U6/U7/U10's fixture dump should target slot 0x1000018F.