Four Opus research lanes for the Fellowship & Allegiance campaign: panel structure, fellowship wire (two BN zero-folds broken by byte decode: IsFull >= 9, the x87 XP-share table capping at 2.8x), allegiance wire (27+5 messages binary-verified; tree assembly discard/reversal rules; ACE zeroed-field caveats), and the acdream seams audit (H.2 scaffolding inventory, J-owner recommendation, AD-78 dimmed-row inventory, bot-gate requirements). Coordinator U2 closure (FaPanelSlotProbeTests, live DATs): Fellowship and Allegiance are two of FOUR pages of ONE tabbed social panel — slot 0x1000018F, panel id 12, Type-8 host — alongside gmFriendsUI and gmSquelchUI; lane A's separate-siblings mounting call is corrected in its addendum, and the full 16-slot dump closes every unidentified RetailPanelCatalog entry (Abuse/Book/LinkStatus/MiniGame/UA/Vitae/ Map+House/Journal). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
61 KiB
Allegiance — retail mechanism, wire map, ACE status, acdream status
Campaign: FA. Lane: C — allegiance mechanics + complete wire map. Date: 2026-08-11. Mode: research only — nothing built, nothing launched, no source file touched except this doc.
Sources and how to read the citations.
| Short form | Full path |
|---|---|
pseudo-c:<line> |
docs/research/named-retail/acclient_2013_pseudo_c.txt |
acclient.h:<line> |
docs/research/named-retail/acclient.h |
symbols.json |
docs/research/named-retail/symbols.json (PDB, 2013-09-06 EoR build) |
ACE/… |
C:\Users\erikn\source\repos\acdream\references\ACE\… |
Chorizite/… |
C:\Users\erikn\source\repos\acdream\references\Chorizite.ACProtocol\… |
src/…, tests/… |
this worktree |
Worktree caveat.
references/is NOT checked out in this worktree (onlyreferences/WorldBuilder/exists here). ACE, Chorizite and holtburger were read from the main clone atC:\Users\erikn\source\repos\acdream\references\. Line numbers are from that clone. Same fordocs/research/2026-06-04-wire-message-catalog.md, which exists in the main clone's working tree but not in this worktree.
0. Verdict summary
| Question | Answer |
|---|---|
| Are the opcodes right? | Every one verified from the retail binary itself, not inferred. The task prompt's parentheticals were all correct: swear 0x001D, break 0x001E, info request 0x027B, AllegianceUpdate 0x0020, AllegianceInfoResponse 0x027C. §3 gives the address of the literal store for each. |
| How many allegiance messages are there? | 27 C→S GameActions + 5 S→C GameEvents. All 27 senders live in one class, CM_Allegiance (symbols.json, 27 Event_* entries), and every one is a fixed-shape flat body — no PackObj, no flags word, on the C→S side. |
| Is the tree a flat list or a real tree? | A real tree, assembled client-side from a flat record vector. Each record after the first carries a treeParent guid; AllegianceHierarchy::Add @0x005B6E90 inserts as left-child/right-sibling. A record whose treeParent is not already in the tree makes AllegianceHierarchy::UnPack return 0 and the client drops the ENTIRE message (§4.4). Parent-before-child ordering is a hard wire contract, not a nicety. |
| Is the hierarchy blob version-gated? | Yes, eleven separate gates on a u16 oldVersion field (acclient.h:2979-2994 + pseudo-c:454484). ACE always writes 0x000B (newest), so against ACE the gates are inert — but a parser that hardcodes the 0x0B layout is only correct because ACE is generous. §4.2 has the full gate table. |
| Does the client enforce the level rules for swearing? | No. gmAllegianceUI::UpdateSwearButton @0x004908E0 gates only on "no patron / have a selection / selection is a player / selection is not already in my allegiance". Level, distance, vassal count, lock, ban are all server-side. |
| Does ACE implement the whole feature? | Swear / break / boot / passup / motd / name / officers / titles / bans / lock / approved-vassal are all implemented server-side. But the AllegianceUpdate hierarchy blob deliberately zeroes officers, officer titles, motd, motdSetBy, nameLastSetTime, isLocked and approvedVassal, and never fills timeOnline/allegianceAge (§5.2). Everything except the tree itself reaches the player as chat text. |
| What does acdream have today? | One live path and one dead scaffold. Live: @allegiance info — request 0x027B + a full AllegianceProfile parser + retail-verbatim text lines. Dead: AllegianceRequests (swear/break builders) and AllegianceTree/AllegianceMath, referenced only by their own unit tests. Zero handlers for 0x0020 / 0x01C8 / 0x027A / 0x0003. No allegiance panel. §6. |
| Biggest single trap for the implementer | The already-shipped AllegianceProfile parser in ClientCommandResponses.cs is the one to reuse — do not write a second one for the panel. It is byte-correct against ACE today; it needs the version gates added and its skipped fields surfaced, not a rewrite. §7.2. |
Two defects found in passing, both in acdream, both currently harmless because the code is unreachable:
AllegianceMath.ComputePassupis a mis-transcription of the retail formula.src/AcDream.Core/Allegiance/AllegianceTree.cs:177computes(50.0 + 22.5 * loyaltyLevel) / 291.0. The real formula (ACEManagers/AllegianceManager.cs:242, quoting retail) is50.0 + 22.5 * (loyalty / 291), as a percentage — i.e.× 0.01at the end. acdream's version returns ~1000× the correct number for a maxed vassal and has the loyalty term inside the wrong parenthesis. The unit test attests/AcDream.Core.Tests/Allegiance/AllegianceTreeTests.cs:94locks in the wrong value. Recommendation: delete the class (§7.3) — the client never needs this number; the server sends_cp_tithed.AllegianceTreemodels a patron→vassal edge the wire does not carry. ItsUpsertNode(guid, name, patronGuid, rank)assumes the server names each node's patron. The wire names each record's tree parent, which for ACE's own writer is not always the real patron (§5.3 — ACE hangs a non-monarch patron directly off the monarch). Also delete; the parsed record list plustreeParentis the tree.
1. Retail client mechanism
1.1 The three actors
| Class | Address | Role |
|---|---|---|
ClientAllegianceSystem |
ctor 0x0056A150 (pseudo-c:375113) |
Owns the ONE cached CAllegianceProfile for the session (acclient.h:40628-40632). Receives all four S→C allegiance events and re-broadcasts them as local notices. Cleared on OnEndCharacterSession @0x00569FA0. |
gmAllegianceUI |
ctor 0x00490B40; struct acclient.h:56038-56061 |
The panel. Subscribes/unsubscribes, renders monarch/patron/self/vassals, owns four dialog contexts, sends swear/break/kick. |
CM_Allegiance |
0x006A6920–0x006A7F1x |
The message layer. 27 Event_* senders (C→S), 4 SendNotice_* (system→UI), 3 DispatchUI_* (wire→system). |
The data path is strictly one-way through the system object:
wire → UIQueueManager dispatch switch (pseudo-c:359190+)
→ CM_Allegiance::DispatchUI_* (unpack)
→ ClientAllegianceSystem::Handle_Allegiance__* (cache into m_allegianceProfile)
→ CM_Allegiance::SendNotice_* (local fan-out)
→ gmAllegianceUI::RecvNotice_* (repaint)
ClientAllegianceSystem::Handle_Allegiance__AllegianceUpdate @0x0056A120
(pseudo-c:375103) does exactly two things: AllegianceProfile::operator=
into the cached profile, then SendNotice_AllegianceUpdate. The panel
never reads the wire; it reads the cache. Any acdream port should keep
that split — a Runtime-owned AllegianceProfile snapshot, a presentation
layer that reads it.
1.2 Panel lifecycle — when UpdateRequest fires
CM_Allegiance::Event_UpdateRequest(u32) (opcode 0x001F) is a
subscribe/unsubscribe toggle, not a one-shot query:
| Site | Address | Arg |
|---|---|---|
gmAllegianceUI::PostInit tail block |
0x004911C6 (pseudo-c:157414) |
1 |
gmAllegianceUI::RecvNotice_PlayerDescReceived |
0x00490D59 (pseudo-c:157175) |
1 |
gmAllegianceUI::OnVisibilityChanged, visible branch |
0x004912DD (pseudo-c:157495) |
1 |
gmAllegianceUI::OnVisibilityChanged, hidden branch |
0x00491311 (pseudo-c:157455) |
0 |
RecvNotice_PlayerDescReceived @0x00490D40 also calls
ClientUISystem::IncrementBusyCount and sets a latch; Update @0x00492670
and the hidden branch both DecrementBusyCount when m_bAwaitingUpdate
is set. Retail shows the busy cursor from "I asked" until "the update
landed". That is the whole reason AllegianceUpdateDone (0x01C8) and
AllegianceUpdateAborted (0x0003) exist as separate terminators.
1.3 Swear — the complete two-sided flow
Initiator (would-be vassal):
- Select a player.
gmAllegianceUI::UpdateSwearButton @0x004908E0(pseudo-c:156945) enables the button (SetState(1)) only when all of:AllegianceProfile::GetPatron(self) == 0(I have no patron),ACCWeenieObject::selectedID != 0,selectedID != player_id,GetWeenieObject(selectedID)->IsPlayer(), andAllegianceProfile::GetData(selectedID) == 0(they are not already in my allegiance). OtherwiseSetState(0xD)— retail's disabled state. There is no level, distance, or vassal-count check in the client. - Click →
ListenToElementMessage @0x00493030sees element0x10000263→MakeSwearConfirmationDialog @0x004927B0. This is a local dialog built viaDialogFactory::MakeDialogInCurrentUI, storing its handle inm_swearContext. Text is a StringInfo with the target'sGetObjectName(NAME_APPROPRIATE)bound as variable 0. - On OK →
CM_Allegiance::Event_SwearAllegiance(m_iidPossibleNewPatron)(pseudo-c:158356; see the BN artifact note in §8).
Target (would-be patron):
- Server sends the generic
Character.ConfirmationRequestevent (0x0274) withConfirmationType = 1(ALLEGIANCE_SWEAR_CONFIRM,acclient.h:6976-6987). ClientUISystem::Handle_Character__ConfirmationRequest @0x005640A0switches on the type;case 1→CM_Allegiance::SendNotice_SwearAllegianceRequest(text, context)(pseudo-c:368633).gmAllegianceUI::RecvNotice_SwearAllegianceRequest @0x00493110→MakeAcceptSwearConfirmationDialog, storingm_uiAcceptSwearServerContextID.- On close →
gmAllegianceUI::CloseAcceptSwearConfirmationDialog @0x00490A50sendsCM_Character::Event_ConfirmationResponse(1, contextId, accepted)— GameAction0x0275, 0x18-byte body (pseudo-c:679924).
There is no allegiance-specific accept message. The accept rides the shared confirmation pipeline, which acdream already implements end to end (§6.4).
1.4 Break and kick — same opcode, different target
Both are Event_BreakAllegiance(u32) (0x001E):
| Path | Handler | Target guid |
|---|---|---|
Break button 0x10000264 → confirm |
CloseBreakConfirmationDialog @0x00490A90 (pseudo-c:157046) |
AllegianceProfile::GetPatron(self) — my patron |
Kick button 0x10000265 → confirm |
CloseKickConfirmationDialog @0x00490B00 (pseudo-c:157073) |
m_iidPossibleKickedVassal — the selected vassal |
gmAllegianceUI::UpdateBreakButton @0x004909D0 enables Break iff
GetPatron(self) != 0. The Kick button is enabled from the vassal
list-box selection handler (ListenToElementMessage, idMessage == 4,
reading attribute 0x10000001 off the selected row).
ACE mirrors this exactly: Player.IsBreakable accepts either patron or
vassal (ACE/Source/ACE.Server/WorldObjects/Player_Allegiance.cs:406-428).
1.5 Panel population — the tree walk
gmAllegianceUI::UpdateVassalsData @0x00492340 (pseudo-c:158470):
UIElement_ListBox::Flush, clear m_iidSelectedVassal, then
id = AllegianceProfile::GetFirstVassal(profile, player_id, &data)
while (id != 0) {
row = AddItemFromTemplateList(...)
SetAttribute_InstanceID(row, 0x10000001, id)
GetChildRecursive(row, 0x10000268) -> SetText(AllegianceData::GetFullName(data))
GetChildRecursive(row, 0x10000269) -> SetText(...) // second column
id = AllegianceProfile::GetNextVassal(profile, id, &data)
}
There is no paging. The list box holds every direct vassal (retail caps that at 11, enforced server-side). The tree accessors are trivial pointer walks over the assembled node graph:
| Accessor | Address | Implementation |
|---|---|---|
GetMonarch |
0x005B6A80 |
LookUp(m_pMonarch->_data._id) |
GetPatron(id) |
0x005B6DD0 |
Search(id)->_patron->_data |
GetFirstVassal(id) |
0x005B6E10 |
Search(id)->_vassal->_data (first child) |
GetNextVassal(id) |
0x005B6E50 |
Search(id)->_peer->_data (next sibling) |
GetData(id) |
0x005B6AE0 |
LookUp(id) |
AllegianceData::GetFullName @0x005B6950 = "<title> <name>" when
AllegianceSystem::GetTitle(rank, heritage, gender) succeeds, else the
bare name. AllegianceSystem has 20 per-heritage/gender title tables
(symbols.json, AllegianceSystem::Get*Title, 0x005B7BC0–0x005B8DD0);
ACE has an independent port at ACE/Source/ACE.Server/Entity/AllegianceRank.cs.
AllegianceData::IsLoggedIn @0x005B6550 = _bitfield & 1.
1.6 Logon/logoff notices — the ShowAllegianceLogons semantics
gmAllegianceUI::RecvNotice_AllegianceLogin @0x00492220
(pseudo-c:158415) does:
AllegianceProfile::GetData(cachedProfile, guid, &data)— if the guid is not in the cached profile, return silently, no text. So a member outside your own monarch/patron/self/vassal slice produces nothing even if the server sends the event.- Build
GetFullName(data)+ one of two literal strings selected by the bool arg (BN renders the two string pointers as bogus vftable members — see §8). ClientSystem::AddTextToScroll(text, 0, 1, 0).
Critical semantics finding: the client does NOT consult
DisplayAllegianceLogonNotifications here. A full-file grep of the
pseudo-C for that accessor (pseudo-c:171333,171339,171342,366545,366548, 486601,486609,487502,487792,487794) finds only the Options-page
checkbox row, the chargen default sweep, and the PlayerModule get/set/
switch cases. There is no consumer. The bit is purely a server-side
filter: retail's server decides whether to send 0x027A at all. ACE
implements exactly that —
Player_Allegiance.cs:454 (login) / :466 (logout) iterate
Allegiance.OnlinePlayers and send only to members whose
CharacterOption.ShowAllegianceLogons is set.
Same finding for IgnoreAllegianceRequests (pseudo-c:171301,171307, 171310,366303,366306,485960,485968,487387,487677,487679): Options row +
chargen + PlayerModule plumbing, no client consumer. ACE consumes it
at Player_Allegiance.cs:316-322, refusing the swear and sending
WeenieError.YourOfferOfAllegianceWasIgnored (0x048E) to the initiator.
This vindicates acdream's AD-78 classification of both rows as store-only/dimmed (§6.5) — retail's own client does not read them either.
HearAllegianceChat is the opposite: it IS read client-side, as the
hearOption argument to ClientCommunicationSystem::SendTurbineChat(..., Allegiance_ChatTypeEnum, text, PlayerModule::HearAllegianceChat(...))
(pseudo-c:394269, 396405), and it is set by the /allegiance chat on|off verbs (pseudo-c:380739 sets 1, :380867 sets 0,
ClientCommunicationSystem::DoAllegianceChat @0x00575CB0 sets it from the
parsed argument at pseudo-c:386302).
1.7 Allegiance chat
Two distinct channels, both already understood by acdream's chat work:
- Turbine allegiance room —
Allegiance_ChatTypeEnum = 0x1(acclient.h:4467). Room id comes fromChatRoomTracker::GetAllegianceRoomID @0x004F0E90and from the hierarchy blob'sm_chatRoomID.AllegianceChatBlob : TurbineChatBlob(acclient.h:59921) adds one field,m_monarchID. Sent viaClientCommunicationSystem::DoTurbineChat_Allegiance @0x0057EBA0. - Legacy
AllegianceBroadcastchannel — bit0x02000000, verb@a, viaDoAllegianceBroadcast @0x005761F0. acdream'sChannelResolver.cs:44already maps this, andChatChannelInfo.cs:78already marks it self-echo.
acdream's TurbineChatMembershipGate.cs:74-76,107-110 already gates the
allegiance room join on AllegianceRoom != 0 and the
HearAllegianceChat bit — which is the retail rule. No allegiance chat
work is needed for the panel campaign.
1.8 The /allegiance verb tree in the 2013 client
ClientCommunicationSystem::DoAllegiance @0x0057D5A0 (pseudo-c:393086)
is a linear string-compare chain. Verbs recovered from the literal
comparisons in address order: boot, info, chat, (alias
data_7e0cb8), broadcast, (alias data_7e0ca8), (data_7e0ca4),
officer, title, hometown, … plus the sibling entry points
DoAllegianceBan @0x005762A0, DoAllegianceLock @0x00576E70,
DoAllegianceName @0x00576C80, DoAllegianceOfficerTitle @0x00576A10,
DoAllegianceHouse @0x0056EF70, DoAllegianceHometown @0x0056EF10.
Help text: HelpAllegiance @0x0057AE10.
So yes — the 2013 client exposes allegiance name, motd, officer and
officer-title management, but only as chat verbs, never as panel
widgets. gmAllegianceUI has no name field, no motd field, no officer
list; its only text elements are m_pAllegianceName, m_pPlayerFollowers,
m_pPlayerRank, m_pMonarchName, m_pMonarchFollowers, m_pPatronName
(acclient.h:56043-56055).
2. Master table
Direction is C→S unless noted. "Retail anchor" is the address of the
opcode literal store inside the CM_Allegiance sender, which is the
strongest possible provenance — the retail client writing its own opcode.
| # | Feature | Retail anchor | C→S wire | S→C wire | ACE status | acdream status | Recommendation |
|---|---|---|---|---|---|---|---|
| 1 | Swear allegiance | Event_SwearAllegiance opcode store 0x006A71EA (pseudo-c:686090) |
0x001D + u32 targetGuid (16 B) |
0x0274 confirm to patron → 0x0020+0x01C8 to both |
Full. GameActionAllegianceSwearAllegiance.cs:9-11; Player_Allegiance.cs:66-137; distance gate 2.0 m, IsPledgable at :296-401 |
Builder exists, unwired (AllegianceRequests.BuildSwear, src/AcDream.Core.Net/Messages/AllegianceRequests.cs:34) |
Implement |
| 2 | Break (from patron) | 0x006A69DA (pseudo-c:685669) |
0x001E + u32 patronGuid (16 B) |
0x0020+0x01C8 |
Full. GameActionAllegianceBreakAllegiance.cs:9-11; Player_Allegiance.cs:170-261 |
Builder exists, unwired | Implement |
| 3 | Break (kick vassal) | same as #2 | 0x001E + u32 vassalGuid |
0x0020+0x01C8 |
Full. IsBreakable accepts vassal (Player_Allegiance.cs:418-428) |
none | Implement |
| 4 | Panel subscribe/refresh | 0x006A72BA (pseudo-c:686147) |
0x001F + u32 on (16 B) |
0x0020 then 0x01C8 |
Partial. GameActionAllegianceUpdateRequest.cs:12 reads uiPanel then ignores it — always replies once, never treats it as a subscription |
none | Implement send; do not depend on subscription |
| 5 | Tree/profile push | pseudo-c:359250-359259 (dispatch case 0x20) |
— | S→C 0x0020: u32 rank + AllegianceProfile |
Full but lossy. GameEventAllegianceUpdate.cs:20-25; also pushed on tree change (Allegiance.cs:361, AllegianceManager.cs:398) |
no handler | Implement — reuse §7.2 parser |
| 6 | Update terminator | CM_Allegiance has no sender; consumed by gmAllegianceUI::Update busy-count |
— | S→C 0x01C8: u32 WeenieError |
Full. GameEventAllegianceAllegianceUpdateDone.cs:10 |
enum only (GameEventType.cs:55) |
Implement (clears busy) |
| 7 | Update aborted | DispatchUI_AllegianceUpdateAborted @0x006A6950, tests *(u32*)arg2 == 3 (pseudo-c:685651) |
— | S→C 0x0003: u32 WeenieError |
Declared, never sent. Only GameEventType.cs:5; no GameEvent* class |
enum only (GameEventType.cs:16) |
Store (parse + log; ACE will never send it) |
| 8 | Logon/logoff notice | DispatchUI_AllegianceLoginNotificationEvent @0x006A6920, tests == 0x27a (pseudo-c:685636) |
— | S→C 0x027A: u32 charGuid + u32 bool isLoggedIn |
Full. GameEventAllegianceLoginNotification.cs:12-13; gated on recipient's ShowAllegianceLogons (Player_Allegiance.cs:454,466) |
no handler | Implement (2 chat lines) |
| 9 | @allegiance info |
0x006A7829 (pseudo-c:686446) |
0x027B + String16L name |
S→C 0x027C: u32 targetGuid + AllegianceProfile |
Full. GameActionAllegianceInfoRequest.cs:13-15; GameEventAllegianceInfoResponse.cs:10-11 |
LIVE end to end — ClientCommandRequests.cs:262, ClientCommandResponses.cs:241-379, wired at GameEventWiring.cs:192-197 |
Done — reuse |
| 10 | Boot from allegiance | 0x006A78E9 (pseudo-c:686474) |
0x0277 + String16L name + u32 accountBoot |
0x0020+0x01C8 |
Full. GameActionBreakAllegianceBoot.cs:11-14 |
none | Store (chat verb @allegiance boot) |
| 11 | Query allegiance name | 0x006A7036 (pseudo-c:686018) |
0x0030, no payload (12 B) |
system chat text | Full. Player_Allegiance.cs:578-596 |
none | Store |
| 12 | Clear allegiance name | 0x006A6AA6 |
0x0031, no payload |
system chat text | Full. Player_Allegiance.cs:630-651; requires Castellan |
none | Store |
| 13 | Set allegiance name | 0x006A7C29 (pseudo-c:686618) |
0x0033 + String16L name |
system chat text | Full but unvalidated. Player_Allegiance.cs:598-628 — its own TODO lists 7 missing name checks |
none | Store |
| 14 | Set officer | 0x006A7CE9 (pseudo-c:686646) |
0x003B + String16L name + u32 AllegianceOfficerLevel |
system chat text | Full. Player_Allegiance.cs:767-828 |
none | Store |
| 15 | Set officer title | 0x006A7DED (pseudo-c:686705) |
0x003C + u32 rank + String16L title — note: u32 BEFORE the string |
system chat text | Full. Player_Allegiance.cs:699-738 |
none | Store |
| 16 | List officer titles | 0x006A6F16 |
0x003D, no payload |
system chat text | Full. Player_Allegiance.cs:676-697 |
none | Store |
| 17 | Clear officer titles | 0x006A6B36 |
0x003E, no payload |
system chat text | Full. Player_Allegiance.cs:740-765 |
none | Store |
| 18 | Lock action | 0x006A6DBA (pseudo-c:685885) |
0x003F + u32 AllegianceLockAction |
system chat text | Full. GameActionDoAllegianceLockAction.cs |
none | Store |
| 19 | Set approved vassal | 0x006A7B69 (pseudo-c:686590) |
0x0040 + String16L name |
system chat text | Full. GameActionSetAllegianceApprovedVassal.cs |
none | Store |
| 20 | Chat gag | 0x006A7729 (pseudo-c:686388) |
0x0041 + String16L name + u32 on |
system chat text | Full. GameActionAllegianceChatGag.cs |
none | Store |
| 21 | House action | 0x006A6CEA (pseudo-c:685828) |
0x0042 + u32 AllegianceHouseAction |
house messages | Full. GameActionDoAllegianceHouseAction.cs |
none | Store (house scope) |
| 22 | Set MOTD | 0x006A7ED9 (pseudo-c:686763) |
0x0254 + String16L motd |
system chat text | Full. Player_Allegiance.cs:524-549; requires Speaker |
none | Store |
| 23 | Query MOTD | 0x006A70C6 |
0x0255, no payload |
system chat text | Full. Player_Allegiance.cs:502-522; also auto-sent 3 s after login (:436-439) |
none | Store |
| 24 | Clear MOTD | 0x006A6C56 |
0x0256, no payload |
system chat text | Full. Player_Allegiance.cs:551-576 |
none | Store |
| 25 | Chat boot | 0x006A765E (pseudo-c:686360) |
0x02A0 + String16L name + String16L reason |
system chat text | Full. GameActionAllegianceChatBoot.cs |
none | Store |
| 26 | Add ban | 0x006A7589 (pseudo-c:686332) |
0x02A1 + String16L name |
system chat text | Full. GameActionAddAllegianceBan.cs; Allegiance.cs:383-395 |
none | Store |
| 27 | Remove ban | 0x006A79E9 |
0x02A2 + String16L name |
system chat text | Full. GameActionRemoveAllegianceBan.cs |
none | Store |
| 28 | List bans | 0x006A6E86 |
0x02A3, no payload |
system chat text | Full. GameActionListAllegianceBans.cs |
none | Store |
| 29 | Remove officer | 0x006A7AA9 |
0x02A5 + String16L name |
system chat text | Full. Player_Allegiance.cs:830-882 |
none | Store |
| 30 | List officers | 0x006A6FA6 |
0x02A6, no payload |
system chat text | Full. Player_Allegiance.cs:653-674 |
none | Store |
| 31 | Clear officers | 0x006A6BC6 |
0x02A7, no payload |
system chat text | Full. Player_Allegiance.cs:884-... |
none | Store |
| 32 | Recall hometown | 0x006A7156 |
0x02AB, no payload |
teleport | Full. GameActionRecallAllegianceHometown.cs |
LIVE — ClientCommandRequests.cs:257, ClientCommandController.cs:335-336 |
Done |
| 33 | XP passup | — (no client message) | — | reaches client as _cp_tithed/_cp_cached in AllegianceData, plus a login chat line |
Full. AllegianceManager.cs:240-320 |
none needed | Neither — server-only |
| 34 | Allegiance chat | DoTurbineChat_Allegiance @0x0057EBA0 |
0xF7DE TurbineChat, ChatType = 1 |
0xF7DE |
Full | LIVE — TurbineChatMembershipGate.cs:74-110 |
Done |
| 35 | IgnoreAllegianceRequests bit |
PlayerModule::IgnoreAllegianceRequests @0x005D2AE0 |
0x0005 (auto-save) / 0x01A1 blob |
in 0x0013 PlayerDescription options word |
Consumed. Player_Allegiance.cs:316-322 |
stored + dimmed — CharacterOptionTable.cs:112 (Options1 0x00000004, autoSave), CharacterOptionsPageController.cs:195 |
Correct as-is |
| 36 | ShowAllegianceLogons bit |
PlayerModule::DisplayAllegianceLogonNotifications @0x005D3200 |
same | same | Consumed. Player_Allegiance.cs:454,466 |
stored + dimmed — CharacterOptionTable.cs:135 (Options1 0x08000000, NOT autoSave) |
Correct as-is |
| 37 | HearAllegianceChat bit |
PlayerModule::HearAllegianceChat @0x005D3560 |
same | same | Consumed at Player_Allegiance.cs:138-139 (auto-joins Turbine room on swear) |
LIVE, not dimmed — CharacterOptionTable.cs:138 (Options1 0x40000000, autoSave) |
Correct as-is |
Recommendation legend. Implement = the FA panel cannot work without it. Store = parse/build it and keep the state, but no panel widget in retail either — it is a chat verb; ship it with the chat-command registry, not the panel. Done = already live in acdream.
3. C→S field order (golden-byte-vector sources)
3.1 The envelope every CM_Allegiance sender writes
All 27 senders share one shape, visible in every body:
OrderHdr::Pack(&hdr, &cursor, totalSize) // 8 bytes: u32 0xF7B1, u32 gameActionSequence
*(u32*)cursor = <subOpcode>; // 4 bytes
<payload>
Proto_UI::SendToWeenie(buffer, totalSize)
The arithmetic proves the header size: a no-payload action allocates
0xC = 12 bytes (Event_QueryAllegianceName, pseudo-c:686017), a
one-u32 action allocates 0x10 = 16, a String16L-only action allocates
strPackSize + 0xC, a String16L + u32 action allocates
strPackSize + 0x10. So header 8 + opcode 4, exactly matching
acdream's AllegianceRequests.Build (AllegianceRequests.cs:48-56) and
ClientCommandRequests.BuildParameterless/BuildString.
After a u32 field the senders emit the standard PackObj DWORD-alignment
pad loop (*(u8*)cursor = 0 until 4-aligned). For these fixed layouts the
cursor is always already aligned, so no padding bytes are ever emitted
in practice. A String16L is u16 length + CP-1252 bytes + pad to 4.
3.2 Per-message layouts
SwearAllegiance 0x001D u32 targetGuid total 0x10
BreakAllegiance 0x001E u32 targetGuid total 0x10
AllegianceUpdateRequest 0x001F u32 on(0|1) total 0x10
QueryAllegianceName 0x0030 — total 0x0C
ClearAllegianceName 0x0031 — total 0x0C
SetAllegianceName 0x0033 String16L name str+0x0C
SetAllegianceOfficer 0x003B String16L name, u32 officerLevel str+0x10
SetAllegianceOfficerTitle 0x003C u32 rank, String16L title str+0x10 ← u32 FIRST
ListAllegianceOfficerTitles 0x003D — total 0x0C
ClearAllegianceOfficerTitles 0x003E — total 0x0C
DoAllegianceLockAction 0x003F u32 AllegianceLockAction total 0x10
SetAllegianceApprovedVassal 0x0040 String16L name str+0x0C
AllegianceChatGag 0x0041 String16L name, u32 on str+0x10
DoAllegianceHouseAction 0x0042 u32 AllegianceHouseAction total 0x10
SetMotd 0x0254 String16L motd str+0x0C
QueryMotd 0x0255 — total 0x0C
ClearMotd 0x0256 — total 0x0C
BreakAllegianceBoot 0x0277 String16L name, u32 accountBoot str+0x10
AllegianceInfoRequest 0x027B String16L name str+0x0C
AllegianceChatBoot 0x02A0 String16L name, String16L reason str1+str2+0x0C
AddAllegianceBan 0x02A1 String16L name str+0x0C
RemoveAllegianceBan 0x02A2 String16L name str+0x0C
ListAllegianceBans 0x02A3 — total 0x0C
RemoveAllegianceOfficer 0x02A5 String16L name str+0x0C
ListAllegianceOfficers 0x02A6 — total 0x0C
ClearAllegianceOfficers 0x02A7 — total 0x0C
RecallAllegianceHometown 0x02AB — total 0x0C
SetAllegianceOfficerTitle's inverted order is the only field-order
surprise in the set, and it is unambiguous in the decomp
(pseudo-c:686705-686710: opcode store, then *(u32*)eax_5 = arg1, then
the alignment pad, then PStringBase::Pack(arg2, ...)).
Three-way agreement. Every layout above matches ACE's reader
(ACE/Source/ACE.Server/Network/GameAction/Actions/GameAction*.cs) and
Chorizite's generated Messages/C2S/Actions/Allegiance_*.generated.cs
field-for-field. The only encoding question is bool width: retail writes
a full u32, ACE reads Convert.ToBoolean(ReadUInt32()), and Chorizite's
ReadBool is ReadInt32() under the hood
(Chorizite/Chorizite.ACProtocol/Extensions/BinaryReaderExtensions.cs:178-181).
All three agree on 4 bytes. (Note this contradicts the "wire encoding
may differ" caution at docs/research/2026-06-04-wire-message-catalog.md:3138
for 0x0041 — that concern is resolved: 4 bytes.)
3.3 The shared confirmation response (swear accept)
ConfirmationResponse 0x0275 u32 confirmType, u32 context, u32 accepted total 0x18
CM_Character::Event_ConfirmationResponse @0x006A1210 (pseudo-c:679924-679935).
acdream already builds this: ClientCommandRequests.ConfirmationResponseOpcode = 0x0275u (ClientCommandRequests.cs:22).
4. S→C field order — AllegianceProfile and the tree
This is the panel's core data structure; the rest of the feature is plumbing around it.
4.1 The three records
AllegianceProfile (acclient.h:36297-36303; UnPack @0x005B6B90, pseudo-c:453802)
u32 totalMembers // _total_members
u32 totalVassals // _total_vassals
AllegianceHierarchy
AllegianceHierarchy (acclient.h:36275-36295; UnPack @0x005B7520, pseudo-c:454484)
u16 recordCount // low half of one u32 read
u16 oldVersion // high half of the same u32
[version-gated body — see 4.2]
AllegianceData monarchRecord // only if recordCount > 0, NO treeParent
{ u32 treeParent; AllegianceData } × (recordCount - 1)
AllegianceData (acclient.h:35815-35831; UnPack @0x005B6850, pseudo-c:453566)
u32 _id // character guid
u32 _cp_cached // XP accrued while patron was offline
u32 _cp_tithed // lifetime XP contributed upward
u32 _bitfield // AllegianceIndex, acclient.h:7714-7722
u8 _gender
u8 _hg // heritage group
u16 _rank
if (_bitfield & 0x08 /*HasPackedLevel*/) u32 _level
u16 _loyalty
u16 _leadership
if (_bitfield & 0x04 /*HasAllegianceAge*/) { u32 _time_online; u32 _allegiance_age; }
else { f64 timeOnlineDouble → _ftol2 → _time_online; _allegiance_age = 0; }
String16L _name
AllegianceData::UnPack refuses payloads shorter than 0x20 bytes
(pseudo-c:453569).
AllegianceIndex values are identical in retail (acclient.h:7714-7722)
and ACE (ACE/Source/ACE.Server/Network/Enum/AllegianceIndex.cs):
LoggedIn 0x1, Update 0x2, HasAllegianceAge 0x4, HasPackedLevel 0x8,
MayPassupExperience 0x10.
Two retail behaviours no other reference documents:
- When
HasPackedLevelis absent, the client setsMayPassupExperienceitself:pseudo-c:453601-453606—if ((_bitfield & 8) == 0) this->_bitfield = (_bitfield | 0x10);. Legacy-packet compatibility. Harmless against ACE (which always setsHasPackedLevel), but a client port that also sets the bit is more faithful than one that does not. - The non-
HasAllegianceAgebranch reads an 8-bytedouble, not au64—pseudo-c:453634-453637reads*(uint64_t*)then converts with_ftol2(the MSVC float→integer helper). Chorizite models this asReadUInt64(Types/AllegianceData.generated.cs:90) and ACE writesulong 0(Network/Structure/AllegianceData.cs:115). Same 8 bytes, so nothing breaks — but the field is a double, and ACE's own comment calls the branch "probably deprecated". Flagged, not actionable.
Also note AllegianceHierarchy::UnPack's last act:
AllegianceData::SetMayPassupExperience(&monarch->_data, 0)
(pseudo-c:454601-454603) — the monarch can never pass up.
4.2 The eleven version gates
AllegianceVersion (acclient.h:2979-2994) and the exact predicate each
field is behind, straight from AllegianceHierarchy::UnPack:
| Field | Gate | Decomp line |
|---|---|---|
m_AllegianceOfficers (PHashTable<u32,u32>) |
oldVersion >= 6 (MultipleAllegianceOfficersAdded) |
pseudo-c:454507 |
4-byte skip (the old single spokesperson id) |
1 <= oldVersion < 6 and ≥4 bytes remain |
pseudo-c:454502-454506 |
m_OfficerTitles (PSmartArray<PString>) |
oldVersion >= 9 (OfficersTitlesAdded) |
pseudo-c:454522 |
m_monarchBroadcastTime |
oldVersion >= 2 (PoolsAdded) and ≥4 bytes |
pseudo-c:454531 |
m_monarchBroadcastsToday |
same block, ≥4 bytes | pseudo-c:454541 |
m_spokesBroadcastTime |
same block, ≥4 bytes | pseudo-c:454550 |
m_spokesBroadcastsToday |
same block, ≥4 bytes | pseudo-c:454559 |
m_motd, m_motdSetBy |
oldVersion >= 3 (MotdAdded) |
pseudo-c:454570,454579 |
m_chatRoomID |
oldVersion >= 4 (ChatRoomIDAdded) |
pseudo-c:454588 |
m_BindPoint (Position) |
oldVersion >= 7 (Bindstones) |
pseudo-c:454596 |
m_AllegianceName + m_NameLastSetTime |
oldVersion >= 8 (AllegianceName) |
pseudo-c:454604,454606 |
m_isLocked |
oldVersion >= 10 (LockedState) |
pseudo-c:454612 |
m_ApprovedVassal |
oldVersion >= 11 (ApprovedVassal) |
pseudo-c:454616 |
BannedCharactersAdded_AllegianceVersion = 5 gates nothing in
UnPack — the ban list never rides the hierarchy blob. (@allegiance ban list is opcode 0x02A3 + chat text.)
Nothing in the record loop is version-gated — treeParent is always
present for records 1..N-1 regardless of version.
4.3 Sub-record encodings
PHashTable<u32,u32>header isu16 count+u16 numBuckets, thencount × (u32 key, u32 value). ACE writes headernumBuckets = 256with a comment that the client actually uses 23 (Network/Structure/AllegianceHierarchy.cs:164-183); acdream's parser already reads it that way (ClientCommandResponses.cs:256-262).PSmartArray<PString>/ officer titles is a bareint32count then N ×String16L— not the u16/u16 hash header. ACE writes it that way (AllegianceHierarchy.cs:185-190); acdream reads it that way (ClientCommandResponses.cs:266-268).Positionisu32 cell+3 × f32 pos+4 × f32 rotation (W,X,Y,Z)= 32 bytes (AllegianceHierarchy.cs:192-212).String16L=u16 byteLength+ CP-1252 bytes + pad to 4.
4.4 Tree assembly — the rule that will bite
AllegianceHierarchy::Add(treeParentId, data) @0x005B6E90
(pseudo-c:454063):
if (data._id == 0) return 0; // reject
if (m_pMonarch == nullptr) { // FIRST record wins the root
m_pMonarch = new AllegianceNode(data); // treeParentId is IGNORED here
m_total++; return 1;
}
parent = (treeParentId != 0) ? Search(treeParentId, m_pMonarch) : nullptr;
if (parent == nullptr) return 0; // ← unknown parent: FAIL
if (parent->_data._id == data._id) return 0; // ← self-parent: FAIL
if (Search(data._id, m_pMonarch) != 0) return 0; // ← duplicate id: FAIL
node = new AllegianceNode(data);
node->_patron = parent;
node->_peer = parent->_vassal; // push front of sibling list
parent->_vassal = node;
m_total++; return 1;
and the caller (pseudo-c:454564-454590) turns any Add failure into
return 0 from UnPack, which the dispatcher treats as a dead message.
Three hard consequences for any server or test fixture:
- Parents must precede children in the record vector. ACE satisfies this by construction (monarch, then patron, then self, then vassals).
- No duplicate guids. A record repeated anywhere kills the message.
- Sibling order is reversed on read — the last record added under a
parent becomes
_vassal(first in theGetFirstVassal/GetNextVassalwalk). The vassal list-box therefore renders in reverse wire order. Worth an eyeball check against retail before calling a port faithful.
4.5 The four S→C events
0x0020 AllegianceUpdate u32 rank; AllegianceProfile
0x01C8 AllegianceUpdateDone u32 WeenieError
0x0003 AllegianceUpdateAborted u32 WeenieError
0x027A AllegianceLoginNotification u32 characterGuid; u32 isLoggedIn
0x027C AllegianceInfoResponse u32 targetGuid; AllegianceProfile
Sources, all three independent and in agreement:
- retail: dispatch case
0x20atpseudo-c:359250-359259(reads a u32, advances 4, thenAllegianceProfile::UnPack);DispatchUI_*opcode tests atpseudo-c:685636(0x27a),:685651(3),:686309(0x27c);Handle_Allegiance__AllegianceLoginNotificationEventreadsarg2+4andarg2+8. - ACE:
GameEventAllegianceUpdate.cs:22-25,GameEventAllegianceAllegianceUpdateDone.cs:10,GameEventAllegianceInfoResponse.cs:10-11,GameEventAllegianceLoginNotification.cs:12-13. - Chorizite:
Messages/S2C/Events/Allegiance_*.generated.cs.
0x0020 is the ONLY message where the profile is preceded by a rank
word; 0x027C precedes it with a guid. A shared parser must take the
leading word as a parameter, not bake it in.
5. ACE caveats — read this before scoping
ACE's allegiance logic is complete. Its presentation blob is not.
5.1 Fields ACE never populates in AllegianceUpdate/InfoResponse
ACE/Source/ACE.Server/Network/Structure/AllegianceHierarchy.cs:
| Field | Value ACE actually sends | Line | Note |
|---|---|---|---|
officers |
always empty | :74-75 (the foreach is commented out), :175 ("always sent as empty in retail?") |
ACE has the data (Allegiance.Officers) — it just doesn't ship it |
officerTitles |
always empty | :78-83 — commented out with "not in retail packets, breaks decal" |
ACE has AllegianceSpeakerTitle/Seneschal/Castellan |
motd, motdSetBy |
forced to "" |
:86-89 — "fixes decal AllegianceUpdate parsing" |
The real motd goes out as GameMessageSystemChat instead |
nameLastSetTime |
0 |
:62,153 |
never assigned |
isLocked |
false hardcoded |
:63,154 |
ACE does track Allegiance.IsLocked and enforces it (Player_Allegiance.cs:372) — it just never tells the client |
approvedVassal |
0 |
:64,155 |
same story |
monarch/spokes BroadcastTime/Today |
0 |
:53-56 |
never assigned |
chatRoomID |
allegiance.Biota.Id |
:90 |
real |
bindPoint |
allegiance.Sanctuary or default |
:92-93 |
real |
allegianceName |
real, falling back to the monarch's name | :85 |
real |
ACE/Source/ACE.Server/Network/Structure/AllegianceData.cs:
| Field | Value | Line |
|---|---|---|
timeOnline, allegianceAge |
always 0 — explicit TODO |
:59-60,86-89,111-112 |
| everything else | real | :69-91 |
Scoping consequence: a faithful acdream panel that displays MOTD, officers, lock state or allegiance age will show blanks against ACE, not because the client is wrong. Either (a) ship those as chat-verb output only, matching what ACE actually serves, or (b) file ACE-side patches. Do not chase a "bug" in the parser.
5.2 Behaviour ACE implements that the wire map might not suggest
- Swear needs proximity and consent.
HandleActionSwearAllegiancerunsCreateMoveToChain(patron, …, Allegiance_MaxSwearDistance = 2.0f)then aConfirmation_SwearAllegianceround-trip (Player_Allegiance.cs:75,:291). The client's own swear button has no distance check — the walk is the server's. AllegianceUpdateis pushed unsolicited on tree changes to every online member (Allegiance.cs:361insideUpdateProperties,AllegianceManager.cs:398on removal) — regardless of whether the panel subscribed. So a client that never sends0x001Fwill still get updates on change; it just won't get one on demand.AllegianceRankalso arrives as a property —GameMessagePrivateUpdatePropertyInt(PropertyInt.AllegianceRank)(Allegiance.cs:352), andPropertyInstanceId.MonarchviaUpdateProperty(:341). acdream already models both (PropertyInt.cs:85,PropertyInstanceId.cs:56) — a second, cheaper source of truth for the rank display.- MOTD is auto-pushed 3 s after login as broadcast chat
(
Player_Allegiance.cs:433-441), together with the cached-XP line "Your Vassals have produced experience points for you…".
5.3 Where ACE's tree shape diverges from retail's
AllegianceHierarchy.cs:103-136 builds exactly four kinds of record:
monarch, patron (only if the patron is not the monarch), self, and direct
vassals. Two divergences:
- The patron record is hung off the monarch, not off the patron's real
patron:
records.Add(new Tuple<>(node.Monarch.PlayerGuid, new AllegianceData(node.Patron)))at:124. In a deep allegiance the tree the client assembles is therefore flattened — grandpatrons and above simply are not sent. recordCountcounts self + patron + monarch + direct vassals only (:103-111), so the panel never sees co-vassals or a wider slice. The comment block at:95-101admits this is reverse-engineered from aclogview and unverified.
AllegianceProfile.cs:32-33 sends totalMembers = monarch.TotalFollowers + 1
and totalVassals = node.TotalFollowers — note TotalFollowers is the
recursive count, so totalVassals is not the same as the number of
records in the vassal list (which is direct vassals only). Retail's panel
shows m_pPlayerFollowers from the profile counter and the list from the
tree walk — they legitimately disagree.
5.4 Rules ACE does NOT enforce
- The patron-level rule is commented out —
Player_Allegiance.cs:341-351, thetarget.Level < Levelcheck plus itsWeenieError.AllegianceIllegalLevelare inside/* … */. So on ACE you can swear to a lower-level patron. Retail's server enforced it (the error code exists,0x0410; acdream already models it atsrc/AcDream.Core/Physics/WeenieError.cs:304). - Allegiance name validation —
Player_Allegiance.cs:615-622lists 7 unimplemented checks (empty, length ≤ 40, uniqueness, bad chars, portal.dat banned words, 1-day timer). The error codes exist in acdream already (WeenieError.cs:530-532). AllegianceUpdateAborted (0x0003)is never sent — the enum member exists (GameEvent/GameEventType.cs:5) with no message class.
5.5 XP passup — cite, do not re-derive
ACE/Source/ACE.Server/Managers/AllegianceManager.cs:240-320, quoting
retail in its own header comment:
Generated % = 50.0 + 22.5 * (loyalty/291) * (1.0 + (RT/730) * (IG/720))
Received % = 50.0 + 22.5 * (leadership/291) * (1.0 + V * (RT2/730) * (IG2/720))
Passup % = Generated% * Received% / 100.0
with V = min(0.25 × directVassals, 1.0) and, for indirect (recursive)
passup, the constants drop to 16.0/8.0 (:275-276). ACE currently pins
all four time terms to their caps (:268-271 — Math.Min(Cap, Cap)), so
time-sworn has no effect there today.
The client needs none of this. _cp_tithed and _cp_cached arrive
pre-computed in AllegianceData. This is the argument for deleting
acdream's AllegianceMath rather than fixing it.
6. acdream today
6.1 Live: @allegiance info (feature #9)
The only fully wired allegiance path.
- Request:
ClientCommandRequests.BuildAllegianceInfoRequest(src/AcDream.Core.Net/Messages/ClientCommandRequests.cs:262-263), opcode constant at:56. - Command:
ClientCommandId.AllegianceInfo(src/AcDream.UI.Abstractions/ClientCommandId.cs:82), dispatched atsrc/AcDream.App/UI/ClientCommandController.cs:339-340. - Parse:
ClientCommandResponses.ParseAllegianceInfoResponse(src/AcDream.Core.Net/Messages/ClientCommandResponses.cs:241-307) — a completeAllegianceProfilereader, including the officers hash header, the bare-int32 officer-title count, the 32-byte Position, and theHasPackedLevel/HasAllegianceAgebranches (ReadAllegianceData,:309-336). - Tree accessors ported from retail:
FindData/FindPatron/FindVassals(:197-238). - Render:
FormatAllegianceInfoLines(:357-379) — a verbatim port ofHandle_Allegiance__AllegianceInfoResponseEvent @0x0056A1D0, including the" *"online marker and the retail behaviour of printing nothing when the queried player has no record. - Wiring:
src/AcDream.Core.Net/GameEventWiring.cs:192-197.
Known limitation: it does not read oldVersion (:250 explicitly
discards it) and assumes the 0x0B layout. Correct against ACE forever;
wrong against any hypothetical older server. Adding the §4.2 gates is a
contained change to one function.
6.2 Live: recall hometown (feature #32) and allegiance chat (#34)
ClientCommandRequests.cs:257, ClientCommandController.cs:335-336;
TurbineChatMembershipGate.cs:74-76,107-110,
ChannelResolver.cs:44, ChatChannelInfo.cs:78,
TurbineChatDisplayNames.cs:37 (allegiance display slot 0x12).
6.3 Dead scaffolds — recommend deletion
| File | Status | Evidence |
|---|---|---|
src/AcDream.Core.Net/Messages/AllegianceRequests.cs (57 lines) |
Byte-correct swear/break builders. Zero production callers — only tests/AcDream.Core.Net.Tests/Messages/AllegianceRequestsTests.cs |
repo-wide grep for AllegianceRequests outside its own file returns only that test |
src/AcDream.Core/Allegiance/AllegianceTree.cs (183 lines) |
AllegianceNode + AllegianceTree + AllegianceMath. Zero production callers — only tests/AcDream.Core.Tests/Allegiance/AllegianceTreeTests.cs |
same grep |
AllegianceRequests is worth keeping and wiring (its layout is right).
AllegianceTree/AllegianceMath should be deleted — see §0 defects 1
and 2 and §7.3.
6.4 Confirmation pipeline — already sufficient for swear-accept
src/AcDream.App/UI/GameplayConfirmationController.cs handles 0x0274
request → dialog → 0x0275 response → 0x0276 done generically. Its own
comment at :36-39 explicitly says "Types 1 and 4 have allegiance/
fellowship semantic owners but use the same response tuple, so this
controller retains that tuple until those panels exist."
One presentational gap. ACE sends the confirmation text = the
vassal's bare Name (Player_Allegiance.cs:91,
ConfirmationManager.cs:38). Retail's type-1 handler binds that name into
a StringInfo template (MakeSwearConfirmationDialog @0x004927B0 shows the
pattern: StringInfo::SetStringIDandTableEnum +
AddVariable_String) producing a real sentence. acdream's generic path
(:51-53, which only appends " Continue?" for types 2/3/5/6) would show
the raw name. The FA panel should take ownership of type 1 and supply
the sentence, exactly as retail's gmAllegianceUI does.
6.5 Character options — the three allegiance bits
| Option | id | Word / mask | Auto-save | Client default | acdream row |
|---|---|---|---|---|---|
IgnoreAllegianceRequests |
0x01 |
Options1 0x00000004 |
yes | off | dimmed (CharacterOptionsPageController.cs:195) |
DisplayAllegianceLogonNotifications |
0x18 |
Options1 0x08000000 |
no | off | dimmed (:197) |
ListenToAllegianceChat |
0x1B |
Options1 0x40000000 |
yes | on | not dimmed — CharacterOptionsPageController.cs:229, marked Live, comment cites TurbineChatMembershipGate.cs:107-110 |
Source of truth: src/AcDream.Runtime/Gameplay/CharacterOptionTable.cs:112,
:135, :138. Ids match retail's PlayerOption enum
(acclient.h:4166,4189,4192); masks match retail's CharacterOption
(acclient.h:3408,3429,3432).
Tests that pin the dimming: tests/AcDream.App.Tests/UI/Layout/ CharacterOptionsPageControllerTests.cs:226 and :728 (the "Grouping"
group's six ids, of which two are the allegiance pair) and :359 (seeds
IgnoreAllegianceRequests ON to prove Bind reads current values).
Headless declares them by name at
src/AcDream.Headless/Configuration/HeadlessConfigurationLoader.cs:30 and
tests/AcDream.Headless.Tests/HeadlessCharacterOptionsSeederWiringTests.cs:72,84,126,255.
§1.6 confirms this classification is retail-faithful, not a shortcut: neither bit has a client-side consumer in the retail binary either.
6.6 Absent entirely
No AllegianceUiController / AllegiancePanelController — the layout
directory (src/AcDream.App/UI/Layout/) has 60 controllers and none is
allegiance. No handler for 0x0020, 0x01C8, 0x027A, or 0x0003
(repo-wide grep for those GameEventType members returns one doc-comment
reference in the dead AllegianceTree.cs:28). No ConfirmationType enum.
No swear/break/kick UI.
6.7 Adjacent data acdream already models
PropertyInstanceId.Allegiance = 24 (PropertyInstanceId.cs:56);
PropertyInt.AllegianceCpPool = 29, AllegianceRank = 30,
AllegianceFollowers = 35, ItemAllegianceRankLimit = 110,
NumAllegianceBreaks = 132, AllegianceMinLevel = 163,
AllegianceMaxLevel = 164, AllegianceSwearTimestamp = 198
(PropertyInt.cs); PropertyString.AllegianceName = 47;
RadarBlipShape.AllegianceMember (RetailRadar.cs:29,113-114) driven by
IsAllegianceMember (RadarBlipColors.cs:62) — currently always
false, because nothing populates allegiance membership. Wiring #5
would light the radar blips for free.
ItemAppraisalTextFormatter.cs:861 already renders the "Allegiance Rank"
requirement row.
33 allegiance WeenieError values are already enumerated
(src/AcDream.Core/Physics/WeenieError.cs:299-308,389,395-396,402-405, 435-437,447,463,506-512,530-532) — everything 0x01C8/0x0003 can carry.
7. Implement-vs-store recommendations
7.1 Minimum viable retail allegiance panel
Six messages, in dependency order:
0x0020AllegianceUpdate— parseu32 rank+ profile into a Runtime-owned snapshot. Everything else depends on this.0x01C8AllegianceUpdateDone— clears the busy latch; carries theWeenieErrorfor a failed swear/break.0x001FAllegianceUpdateRequest— send1on panel show and on PlayerDescription,0on hide (§1.2). ACE ignores the value but retail servers do not; send it correctly.0x001DSwearAllegiance— builder already exists.0x001EBreakAllegiance— builder already exists; serves both Break and Kick with a different target guid.0x027AAllegianceLoginNotification— two chat lines, gated on "is the guid in my cached profile" (§1.6), not on a local option bit.
Plus: take ownership of ConfirmationType == 1 in
GameplayConfirmationController (§6.4).
Everything else in §2 is store: build the GameAction, route the reply into the chat log, do not build a widget. Retail did not build widgets for them either.
7.2 Reuse, do not rewrite, the profile parser
ClientCommandResponses.ParseAllegianceInfoResponse is already a correct
AllegianceProfile reader. Refactor it into a shared
AllegianceProfileReader taking the profile span (both callers prepend a
different leading u32 — rank vs guid, §4.5), then:
- add the §4.2 version gates,
- surface the currently-discarded fields the panel needs:
rank,level,loyalty,leadership,cpCached,cpTithed,gender,heritage,chatRoomID,bindPoint, - keep
FindData/FindPatron/FindVassals— they are already ports of the retail accessors, - keep the retail assembly rules from §4.4 as the parse contract (reject unknown parent, reject duplicate id) so a malformed server blob fails the same way retail fails it.
The one behaviour to add on top: sibling order reverses on assembly (§4.4) — either build the real linked structure or reverse the per-parent list when rendering.
7.3 Delete
src/AcDream.Core/Allegiance/AllegianceTree.csandtests/AcDream.Core.Tests/Allegiance/AllegianceTreeTests.cs— the tree model contradicts the wire (§0 defect 2) and the passup math is wrong and unnecessary (§0 defect 1, §5.5). Deleting is strictly safer than leaving a wrong formula with a passing test next to a new correct implementation.
7.4 Divergence-register rows this campaign will owe
- ACE never populates officers / officer titles / motd / lock / approvedVassal / nameLastSetTime / broadcast counters / timeOnline / allegianceAge in the profile blob (§5.1) — the panel renders blanks. Adaptation, symptom: "MOTD and officer fields are always empty".
- ACE flattens the patron chain onto the monarch and sends only monarch/patron/self/direct-vassals (§5.3) — approximation, symptom: "the tree above my patron is missing".
- ACE does not enforce the patron-level rule (§5.4) — server-side divergence, symptom: "I can swear to a level-1 patron".
totalVassals(recursive) versus the direct-vassal list length (§5.3) — explained divergence, symptom: "the follower count doesn't match the list".
8. BN decomp artifacts observed (zero-fold / symbol-fold warnings)
Per the standing BN caution, these are decompiler artifacts in the cited lines — not evidence of the semantics the names suggest. None changes any conclusion above; all are flagged so a later reader does not "fix" a non-bug.
- String constants rendered as vftable members.
pseudo-c:171301showscompute_str_hash(&gmAllegianceUI::vftable'.RecvNotice_AddSpellShortcut)— that operand is the address of the literal"ID_PlayerOption_IgnoreAllegianceRequests"(the real string is atpseudo-c:1068811/.rdata 0x00805520), which BN folded into the nearest preceding symbol. Same class atpseudo-c:158427,158429(the login/logout sentence pair insideRecvNotice_AllegianceLogin) and:453655(data_794098, the separator insideGetFullName`). - Field access folded into an unrelated member path.
pseudo-c:158356renders the swear send asEvent_SwearAllegiance(this->m_hashElementsRegisteredWith.m_intrusiveTable.m_aInplaceBuckets[1]). The real field isgmAllegianceUI::m_iidPossibleNewPatron(acclient.h:56042), reached by raw offset; BN attributed the offset to the wrong base. Confirmed by the symmetric kick path (pseudo-c:157079) which BN does render correctly asthis->m_iidPossibleKickedVassal. thisadjusted by a constant on multiple-inheritance thunks.gmAllegianceUI::RecvNotice_AllegianceUpdate @0x00492780readsgmAllegianceUI::Update(((char*)this - 0x5F8))— the-0x5F8is thegmNoticeHandlersub-object adjustment, not a real pointer offset. Same atpseudo-c:159260(RecvNotice_SwearAllegianceRequest) and:158415.- Dead comparison idioms.
(ebx_1 - ebx_1) == 0xffthroughoutDoAllegiance(pseudo-c:393086+) is BN's rendering of a sign-extended byte compare againstPStringBase::cmp's result; read it as "strings matched". - No literal-zero opcode folds were found. Every one of the 27
CM_Allegiance::Event_*senders shows an explicit non-zero opcode store (§3.2). There is no place in this feature where a0operand might be hiding an id. AllegianceHierarchy::UnPackrendersarg2as an alias for the remaining-size variable in places (pseudo-c:454518,454530,…) — BN reusing the parameter slot as a local. The size arithmetic is still readable; the field order (the part that matters) is unambiguous.
9. Unknowns for the coordinator
- The Allegiance panel's LayoutDesc id is not known. Retail element
ids ARE known from the decomp — swear
0x10000263, break0x10000264, kick0x10000265, vassal-row name0x10000268, vassal-row second column0x10000269, row instance-id attribute0x10000001(pseudo-c:159207-159240,:158466-158510) — but the containing0x21000xxxLayoutDesc is not derivable from the decomp and is not indocs/research/retail-ui/. Resolve with a UI-Studio--dumpsweep the same way CH6a found the chat window's real id (0x2100006F, after an earlier wrong guess of0x21000006— seesrc/AcDream.App/UI/Layout/ChatWindowController.cs:30). Do not guess. - What is the vassal row's second text column (
0x10000269)? TheUpdateVassalsDatabody pastpseudo-c:158500was not fully read. Candidates: rank title, level, or_cp_tithed. One more decomp pass on0x00492340–0x00492670settles it. - Sibling render order. §4.4 shows the assembly reverses wire order. Whether retail's list box therefore shows newest-vassal-first is a visual gate question — worth one side-by-side check against retail before the panel is called done.
- Does ACE's unsolicited
AllegianceUpdateon tree change actually reach a client that never subscribed? §5.2 says yes by code reading (Allegiance.cs:361is unconditional). Cheap to confirm on the live ACE with two accounts once #5 is parsed. AllegianceHierarchy::GetMinPackSize @0x0041BC20was not read — it setsAllegianceProfile::UnPack's minimum-size guard (minPackSize + 8,pseudo-c:453805). Only matters for a truncation-rejection conformance test.- Officer/motd/name management scope. All 20-odd management opcodes are chat verbs in retail, and ACE serves them. Are they in FA's scope, or does FA stop at the panel? The §2 "store" column assumes panel-only; if the chat verbs are in scope, they are a mechanical addition to the existing 152-verb registry with no new parsing work (every reply is plain system chat).
Confirmation_SwearAllegiancetext ownership. §6.4 — should the FA panel ownConfirmationType == 1and compose the sentence locally (retail-faithful), or should we ask for an ACE-side text change? The retail-faithful answer is local composition; flagging because it means touchingGameplayConfirmationController, which Campaign CH owns.
10. Executive summary
- Every opcode in the prompt was correct and is now verified from the
retail binary's own opcode stores, not inferred: swear
0x001D, break0x001E, subscribe0x001F, info request0x027B,AllegianceUpdate0x0020,UpdateDone0x01C8,Aborted0x0003,LoginNotification0x027A,InfoResponse0x027C— 27 C→S actions and 5 S→C events in total, all field orders three-way agreed across retail, ACE and Chorizite. - The panel's core data structure is a flat record vector the client
folds into a left-child/right-sibling tree, with eleven version gates
on a
u16 oldVersionand one brutal rule: a record whosetreeParentis not already in the tree makes the client discard the entire message. - acdream already ships a correct
AllegianceProfileparser (the@allegiance infopath) — reuse it with version gates and more fields surfaced; it needs no rewrite. There are zero handlers for the other four allegiance events and no panel. - ACE implements all the allegiance logic but deliberately zeroes officers, officer titles, MOTD, lock state, approved vassal and allegiance age in the profile blob, and flattens the patron chain onto the monarch — so a faithful panel will legitimately show blanks, and the plan must scope those as chat-text features, not panel widgets.
- The two dimmed allegiance option rows are retail-faithful, not a
shortcut — the retail client has no consumer for
IgnoreAllegianceRequestsorDisplayAllegianceLogonNotificationseither; both are pure server-side filters. Meanwhilesrc/AcDream.Core/Allegiance/AllegianceTree.csis a dead scaffold whose passup formula is transcribed wrong and whose tree model contradicts the wire — delete it rather than build on it.