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>
940 lines
62 KiB
Markdown
940 lines
62 KiB
Markdown
# 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 (`0x187`–`0x18A`) 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.Build` → `CharacterController.Bind` → `RegisterIndicatorDetailPanel(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)` → `Build` → `OptionsPanelController.Bind` → `RegisterMainPanel` | `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 bar** — `SetAttribute_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`) |
|
||
| `0x1000025D`–`0x1000025F` | ? | — | 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 marker** — `SetVisible(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 `0x10000003` →
|
||
`0x23000003`). 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
|
||
`0x10000003` → **`0x23000003`** (`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 `0x1000000D` → `ACCWeenieObject::SetSelectedObject` (+ target-mode execute) → `m_iidSelectedFellow` → `UpdateButtons` |
|
||
| `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 flag** — `Event_Quit(0)` vs
|
||
`Event_Quit(1)`. Offsets +1..+6 (`0x10000275`–`0x1000027A`) 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 == 1` → `0x10000263`/`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` / `_PlayerDescReceived` → `Update` |
|
||
|
||
`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** (1–2 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 `0x10000251`–`0x1000028A` (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
|
||
`IgnoreFellowshipRequests` ↔ `FellowshipAutoAcceptRequests` 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` | `BuildMeter` → `UiMeter` (`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`, `0x4DD205`–`0x4DD208`, `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`, `0x1000025D`–`0x1000025F`, `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 `0x10000001` → `0x23000001` (and `0x10000003` → `0x23000003`), 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` classes** — `gmFellowshipUI`
|
||
(class id `0x1000002D`, `Register @ 0x0048E6A0`) and `gmAllegianceUI` (`0x1000002C`,
|
||
`0x004911F0`), both deriving from `UIElement_Field` — **not 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 `0x10000001`
|
||
→ `0x23000001` (the two option checkboxes use `0x10000003` → `0x23000003`), 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`.
|