# R11 — Allegiance System Deep Dive **Scope:** patron / vassal tree, swearing + breaking, XP pass-up, rank, chat channels (@a / @v / @p / @m / @c), MOTD, allegiance houses, officer permissions, wire messages, UI panel. **Authorities (in order of trust, per CLAUDE.md hierarchy):** - **Retail acclient.exe decompiled** — `docs/research/decompiled/chunk_00570000.c`, `chunk_00560000.c`, `chunk_005B0000.c`, `chunk_005D0000.c`, `chunk_00580000.c`, `chunk_006B0000.c`. Client-side strings, channel bitmasks, error paths, UI hooks. - **ACE server** — `references/ACE/Source/ACE.Server/Entity/AllegianceNode.cs`, `AllegianceRank.cs`, `Managers/AllegianceManager.cs`, `WorldObjects/Allegiance.cs`, `WorldObjects/Player_Allegiance.cs`, `Network/GameEvent/Events/Allegiance*.cs`, `Network/Structure/AllegianceData.cs`, `Network/Structure/AllegianceHierarchy.cs`, `Network/Structure/AllegianceProfile.cs`, `Network/Enum/AllegianceIndex.cs`. Full server-side implementation of the system, plus the exact wire-format writer. - **Asheron Wikia / Fandom wiki** — cited inline in AllegianceManager.cs (ranking, XP passup formulas). This is the documented retail behavior ACE codes against. acdream is a **client**, so our primary job is: 1. Decode inbound `AllegianceUpdate` / `AllegianceInfoResponse` packets into an in-memory allegiance tree. 2. Render the tree as a UI panel. 3. Construct and send the outbound GameActions (`SwearAllegiance`, `BreakAllegiance`, `AllegianceInfoRequest`, officer commands, MOTD commands, etc). 4. Handle the five allegiance-related chat channels correctly. We do **not** compute pass-up XP — that is server-authoritative. But we document the formula anyway so the acdream server-adjacent code (mock server for testing, plugin API exposing it) can match retail. --- ## 1. Tree structure ### 1.1 Data model From `AllegianceNode.cs`: ``` class AllegianceNode { ObjectGuid PlayerGuid; // this node's player AllegianceNode Monarch; // top of tree (self for monarch) AllegianceNode Patron; // direct parent; null for monarch Dictionary Vassals; // direct children uint Rank; // 0-10 bool IsMonarch => Patron == null; } ``` Every player in an allegiance has exactly: - **one monarch** (root of the tree; `MonarchId` player property) - **one patron** (direct parent; `PatronId` player property; null for monarch) - **0 to 11 direct vassals** (children; capped, see below) The tree is reconstructed each time any member swears or breaks by walking every online/offline player who reports the same `MonarchId` and threading the `PatronId -> Vassals` links. See `AllegianceManager.Rebuild` and `Allegiance.BuildPatronVassals`. ### 1.2 Breadth cap **Max direct vassals: 11.** From `Player_Allegiance.cs::IsPledgable`: ```csharp if (targetNode.TotalVassals >= 11) { Session.Network.EnqueueSend(new GameMessageSystemChat( $"{target.Name} already has the maximum # of vassals", ChatMessageType.Broadcast)); return false; } ``` This corresponds to retail error 0x416 seen in the decompiled client: `L"%s cannot have any more Vassals"`. ### 1.3 Depth cap **There is no hard-coded depth cap** in either the retail client strings or ACE's enforcement. The effective cap comes from `Rank`, which is clamped at 10 (see §4). Practically, once ranks get deep enough the tree can keep extending downward indefinitely; the rank value just stops climbing. ### 1.4 Loop prevention A monarch cannot swear back into their own subtree: ```csharp if (selfNode != null && selfNode.IsMonarch) { if (selfNode.PlayerGuid == targetNode.Monarch.PlayerGuid) { // refuse } } ``` This is the only cycle guard — and it only fires when the self side is currently a monarch. It prevents A->B->A directly, but the same check is implicitly satisfied elsewhere because once you are a vassal you cannot swear at all (§2.3). ### 1.5 Counts - `TotalVassals` = count of direct children. - `TotalFollowers` = recursive sum (vassals of vassals…). Used for the wire-level `totalVassals` field in the allegiance profile, and for rank display counts. - `TotalMembers` = from the allegiance root, full tree size. --- ## 2. Swearing and breaking ### 2.1 Swear flow (client -> server) **Opcode:** `GameActionType.SwearAllegiance = 0x001D`. The client sends a single 4-byte payload: the target patron's guid. ``` struct SwearAllegianceRequest { uint32_t targetPatronGuid; }; ``` Server-side handler (`Player_Allegiance.cs::HandleActionSwearAllegiance`): 1. Look up target. Must be online. 2. Run `IsPledgable(patron)`. Checks (in order): - Self not Olthoi, target not Olthoi. - Target hasn't set `CharacterOption.IgnoreAllegianceRequests` (which is "@allegiance ignore" in retail). - Self doesn't already have a patron (`PatronId != null`). - Target is not self. - Target has < 11 direct vassals. - Self is not already in target's allegiance tree (no loops). - If allegiance is locked: either self is in `ApprovedVassals`, or target is a Castellan+ officer. - Self is not in target's ban list. - Self does not own a monarch-only house (mansion). If they do: error 0x??? via `WeenieError.CannotSwearAllegianceWhileOwningMansion`. 3. Create a `MoveToChain` toward the patron at `Allegiance_MaxSwearDistance = 2.0f` (retail). ACE has 4.0 commented out — retail is the tighter 2.0 meters. After the move succeeds, run `SwearAllegiance(...)`. 4. `SwearAllegiance`: - Send a `Confirmation_SwearAllegiance` to the target first. Target must click yes. - On confirmation: set `PatronId`, walk up to get `MonarchId`, set both as `PropertyInstanceId.Monarch`. - Set `ExistedBeforeAllegianceXpChanges` iff patron's level >= own level (only equal-or-higher patrons can pass XP up in the post-2004 system — see §3.2). - If self was previously a monarch with vassals, walk the subtree and update every child's `MonarchId` to the new monarch. Clear officers in the old allegiance (`HandleMonarchSwear`). - Broadcast `Motion_Kneel` (kneeling animation). - Rebuild the allegiance tree (`AllegianceManager.OnSwearAllegiance`). - Reset `AllegianceXPGenerated = 0`, `AllegianceOfficerRank = null`. - Send `GameEventAllegianceUpdate` + `GameEventAllegianceAllegianceUpdateDone`. - Join Turbine chat channel "Allegiance" if the option is enabled. ### 2.2 Cooldown? **There is no 24-hour cooldown on swearing itself.** Looking at both ACE and the decompiled strings: - ACE: `HandleActionSwearAllegiance` → `IsPledgable` → `SwearAllegiance`. No timestamp check. - Decompiled: error set 0x40B-0x416 covers the swear failure modes (already sworn, not enough XP, not in allegiance, already maxed vassals). No cooldown error. There IS a 24-hour cooldown on **allegiance name changes** (see §7.3). The "break once a day" player folklore is not reflected in the code; the only cost to break is a generic 3-second animation. Players frequently re-swore the same day, and the client/server permit it. The wikia does mention a "1 hour cooldown" on `/allegiance house open` after closing. Searching ACE, I don't see this enforced explicitly. ### 2.3 Break flow (client -> server) **Opcode:** `GameActionType.BreakAllegiance = 0x001E`. Payload is again a single uint32 guid (the patron OR vassal you're breaking from). Handler (`Player_Allegiance.cs::HandleActionBreakAllegiance`): 1. `IsBreakable(targetGuid)` — target exists and is either self's patron (`PatronId == target.Guid.Full`) or one of self's vassals (`target.PatronId == Guid.Full`). Anything else: silent refuse. 2. If breaking from vassal: - Target's `PatronId = null`. - Target's `MonarchId` becomes self OR null depending on whether target has vassals themselves (new fragment root vs solo player). - Walk target's subtree and reset every descendant's `MonarchId` to the target. 3. If breaking from patron: - Self's `PatronId = null`, `MonarchId = null`. - Walk self's subtree and reset every descendant's `MonarchId` to self (self is the new fragment monarch). 4. Messages: - To target: `"{Name} has broken their Allegiance to you!"` - To self: `"You have broken your Allegiance to {target.Name}!"` 5. Rebuild both allegiances. 6. Check the allegiance house — boot self and/or target if they lost mansion access (`CheckAllegianceHouse`). 7. Send `GameEventAllegianceUpdate` + done. ### 2.4 Involuntary separation - **Boot** (`GameActionType.BreakAllegianceBoot = 0x0277`, Seneschal+ required): remove one member + their subtree. Error strings include `L"Your Allegiance has been dissolved!\n"` and `L"Your patron\'s Allegiance to you has been broken!\n"` (errors 0x??? and 0x??? per decompiled chunk_00570000 lines 1944-1948). - **Ban** (`AddAllegianceBan = 0x02A1`): prevents future swearing and kicks if already a member. - **Player delete**: `AllegianceManager.HandlePlayerDelete` collapses a patron out of the tree; vassals each become their own new monarch. --- ## 3. XP pass-up formula This is the most complex part of the system. From `AllegianceManager.cs` (quoted verbatim) + wiki references: ### 3.1 Two epochs **Pre-patch (before January 12 2004):** Used a complex formula based on the `Self` attribute, loyalty/leadership caps, with an effective Loyalty of 175 being enough to max patron-to-grandpatron passup. Only kill XP passed up. **ACE does not implement this** and acdream doesn't need to. **Post-patch (2004 onward, extended October 2009 to all XP):** Simpler formula, decoupled from `Self`. This is what retail ran during its final years and what ACE emulates. ### 3.2 Eligibility gate — `ExistedBeforeAllegianceXpChanges` This flag is confusingly named — it's really "can this vassal pass XP up?" From `Player_Allegiance.cs` line 105: ```csharp ExistedBeforeAllegianceXpChanges = (patron.Level ?? 1) >= (Level ?? 1); ``` At swear time, if your **patron's level is less than yours**, you are flagged as not passing XP. The flag flips on when your patron levels above you (`AllegianceNode.OnLevelUp`). This prevents a low-level player from being used as a "dummy patron" above a high-level farmer. If this flag is false, **no XP passes up from that vassal at all** (`DoPassXP` early-outs before any calculation). ### 3.3 The formula From the comment block in `AllegianceManager.cs` (verbatim, cited to http://asheron.wikia.com/wiki/XP_Passup, "Xerxes of Thistledown, four months of testing"): ``` 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 ``` Where: | Variable | Meaning | Cap | |--------------|--------------------------------------------------|------| | Loyalty | Buffed Loyalty skill on vassal | 291 | | Leadership | Buffed Leadership skill on patron | 291 | | RT | Real-world days vassal has been sworn to patron | 730 | | IG | In-game hours vassal has been sworn to patron | 720 | | RT2 | Avg real days patron's vassals sworn to patron | 730 | | IG2 | Avg in-game hours patron's vassals sworn | 720 | | V | Vassal-count factor: 1→0.25, 2→0.50, 3→0.75, 4+→1.00 | 1.00 | Ranges: - **Generated%** from 50% (new swear, zero loyalty) up to ~90% (long-term, maxed loyalty). This is the fraction deducted from the vassal's **earned** XP — not literally taken from them, but computed for the passup calculation. - **Received%** from 50% up to ~90% similarly. - **Passup%** from 25% to 90% of the vassal's earned XP. ### 3.4 Recursion (ultrathink — this is the "fraction scales with depth") **Grandpatron pass-up.** When vassal V earns N XP, patron P gets `N * Passup%(V,P)`. Then P's patron (grandpatron GP) gets a share of **what P received**, not of the original N. But the recursion uses different constants: ```csharp private static void DoPassXP(vassalNode, amount, bool direct) { ... var factor1 = direct ? 50.0f : 16.0f; var factor2 = direct ? 22.5f : 8.0f; ... // recursive call with direct = false DoPassXP(patronNode, passupAmount, false); } ``` So the grandpatron formula is: ``` Generated_indirect% = 16 + 8 * (Loyalty / 291) * (1.0 + (RT / 730) * (IG / 720)) Received_indirect% = 16 + 8 * (Leadership / 291) * (1.0 + V * (RT2 / 730) * (IG2 / 720)) Passup_indirect% = Generated_indirect% * Received_indirect% / 100 ``` Caps: Generated% from 16% to 24%, Received% likewise. So **Passup_indirect%** is bounded roughly from `0.16 * 0.16 = 2.56%` up to `0.24 * 0.24 = 5.76%` in the "direct"-scale interpretation. Against the wiki's stated 0%–10% range, that checks out (the wiki's 10% max is the "max leadership + max time" scenario, matching 0.24²·~1.73 = ~10% when you account for the full time/vassal multiplier being close to 2). This **fraction scales per generation**: a tier-3 passup is `Direct * Indirect ≈ 0.9 * 0.1 = 9%` at maximum, or `0.25 * 0 = 0%` at minimum. Deep trees don't feed the monarch much in practice — the interesting XP is at direct patron level, which is why retail players optimized around 2-3 deep at most. The recursion terminates when `patronNode == null` (i.e., we hit the monarch, who has no patron). ### 3.5 Earning side When a vassal gains XP (via `GrantXP`), the amount is passed to `AllegianceManager.PassXP(vassalNode, amount, direct=true)`: - `generated` fraction is added to vassal's `AllegianceXPGenerated` (tracked for player "tithed" stat). - `passup` fraction is added to patron's `AllegianceXPCached`. - If patron is online: they immediately redeem the cached XP via `AddAllegianceXP()` (which grants it with `XpType.Allegiance`). - If patron is offline: XP accumulates and is granted on next login (with login message "Your Vassals have produced experience points for you…"). ### 3.6 ACE simplifications ACE's implementation currently uses **capped constants** for the time modifiers: ```csharp var timeReal = Math.Min(RealCap, RealCap); // = RealCap var timeGame = Math.Min(GameCap, GameCap); // = GameCap ``` i.e., it always treats every vassal as "maxed time". This is a known simplification; to fully match retail, the server needs to track `TimeSwornToPatron` per vassal and compute actual RT/IG values. For our client work this doesn't matter — **we don't compute passup**, we just read `AllegianceXPCached`, `AllegianceXPGenerated`, `AllegianceXPReceived` from packets. --- ## 4. Rank From `AllegianceNode.cs::CalculateRank` (cited to http://asheron.wikia.com/wiki/Rank): > A player's allegiance rank is a function of the number of Vassals and > how they are organized. First, take the two highest ranked vassals. > Now the Patron's rank will either be one higher than the lower of the > two, or equal to the highest rank vassal, whichever is greater. ```csharp var sortedVassals = Vassals.Values.OrderByDescending(v => v.Rank).ToList(); var r1 = sortedVassals.Count > 0 ? sortedVassals[0].Rank : 0; var r2 = sortedVassals.Count > 1 ? sortedVassals[1].Rank : 0; var lower = Math.Min(r1, r2); var higher = Math.Max(r1, r2); Rank = Math.Min(10, Math.Max(lower + 1, higher)); ``` - **Starting rank:** 0 (or 1 depending on interpretation — a lone sworn member with no vassals gets rank 0 here, and `AllegianceTitle` returns `""` for rank 0; rank 1 kicks in when you have one vassal). - **Max rank:** 10 (the "High King" / "Aulin" / "Tah" tier). - Rank 9 requires a "Fibonacci-like" branching structure — two vassals of rank 8 each, or one rank 9 + one rank 8. ### 4.1 Rank progression table Approximate minimum-tree sizes to reach each rank: | Rank | Tree structure | Name (Aluvian Male) | |------|---------------------------------------------|-----------------------| | 0 | solo / brand new (no vassals) | — | | 1 | 1 direct vassal | Yeoman | | 2 | 2 vassals at rank 1 | Baronet | | 3 | 2 vassals at rank 2 | Baron | | 4 | 2 vassals at rank 3 | Reeve | | 5 | 2 vassals at rank 4 | Thane | | 6 | 2 vassals at rank 5 | Ealdor | | 7 | 2 vassals at rank 6 | Duke | | 8 | 2 vassals at rank 7 | Aetheling | | 9 | 2 vassals at rank 8 | King | | 10 | 2 vassals at rank 9 | High King | With an 11-vassal cap, each rank level roughly doubles the tree size minimum, so rank 10 requires a ~`2^10 = 1024` member tree at minimum (ignoring the "higher" branch). ### 4.2 Heritage-gendered titles Each of **9 heritages × 2 genders = ~18 title tables**, each with 10 rank strings. See `AllegianceTitle.cs` lines 80-384 for the full table (Aluvian, Gharundim, Sho, Viamontian, Shadowbound/Penumbraen, Tumerok (gender-neutral), Gearknight (gender-neutral), Lugian (gender-neutral), Empyrean, Undead). acdream will copy this table **byte-for-byte** — these are the exact strings retail displays on the allegiance panel and in tells/broadcasts. --- ## 5. Chat channels Five logical channels, visible through distinct prefixes in the retail client. Searched `chunk_00570000.c` for the exact prefix strings: | Logical | Client prefix (from decompile) | ChatMessageType flag | Command | |---------------|-------------------------------------------|----------------------|------------| | Allegiance | `"[Allegiance Broadcast] You say, \""` | `0x2000000` | @a / broadcast | | Co-Vassals | `"[Co-Vassals] You say, \""` | `0x1000000` | @c | | Patron tell | `"Your patron says to you, \""`| `0x1000` | @p | | Vassal tell | `"Your vassal says to you, \""`| `0x2000` | @v | | Follower tell | `"Your follower says to you, \""`| `0x4000` | @m (monarch) | From `chunk_00570000.c` around line 700-810, the client dispatches chat input by comparing `piVar4` (channel mask) to these hex values. Additionally `chunk_006B0000.c::FUN_006b1460` maps the string `"Allegiance"` to ChatMessageType **0x12** (18). That's the server-side enum value; the `0x1000000` etc. above are per-send client-side flag bits used to choose a prefix. ### 5.1 Channel permissions - **@v (vassal tell)**: send to one specific direct vassal. Only works if target is currently your vassal. - **@p (patron tell)**: send to your patron. Only works while you have a patron. - **@m (monarch tell)**: send to your monarch. `@mr` replies to last `@m` "only works for monarchs" — i.e., the monarch's receive side is special because many vassals can @m them. - **@c (co-vassals)**: broadcast to all peers with the same direct patron as you. Decompiled at `chunk_00570000.c:707`. - **@a / allegiance broadcast**: whole allegiance tree. Subject to gag/boot filters. The decompiled strings at line 2991-3005 cover the gag/un-gag notifications. ### 5.2 Gag / boot - `HandleActionAllegianceChatGag` / Boot: Speaker+ permission. - `ChatFilters: Dictionary` — timestamp of when the filter expires. `DateTime.MaxValue` means permanent boot. - `Allegiance.IsFiltered(playerGuid)` checks and auto-removes expired filters. - Gag duration: `Player.AllegianceChat_GagTime = TimeSpan.FromMinutes(5)`. ### 5.3 "Listen to allegiance chat" option `CharacterOption.ListenToAllegianceChat` — if set, player joins the turbine chat channel "Allegiance" on swear and login, leaves on break. This is how cross-landblock allegiance chat works retail-side (Turbine chat room backplane). --- ## 6. Allegiance house / mansion Mansions (and some villas) can be flagged `HouseRequiresMonarch` — only rank-N+ monarchs can purchase them, and they function as allegiance houses where vassals gain access. ### 6.1 Purchase restrictions From the decompiled client (`chunk_00570000.c:1516`): - `L"You must be a monarch to purchase this dwelling.\n"` (error 0x48a) - `L"You must be above level %s to purchase this dwelling.\n"` (0x488) - `L"You must be at or below level %s to purchase this dwelling.\n"` (0x489) - `L"You must be above allegiance rank %s to purchase this dwelling.\n"` (~0x476) - `L"You must be at or below allegiance rank %s to purchase this dwelling.\n"` (~0x477) These are the 5 gates: monarch-status, level-min, level-max, rank-min, rank-max. Level/rank thresholds come from the SlumLord weenie. ### 6.2 Allegiance access Action enum from `AllegianceHouseAction.cs`: ```csharp enum AllegianceHouseAction : uint { Undef = 0, Help = 1, CheckStatus = 1, // "@allegiance house help" is alias for check GuestOpen = 2, GuestClose = 3, StorageOpen = 4, StorageClose = 5, } ``` Only Castellan+ can toggle (`AllegiancePermissionLevel.Castellan`). ### 6.3 Sanctuary / hometown recall Monarch can set a bindstone for the whole allegiance. Stored as `Allegiance.Sanctuary` (a `Position`). Vassals recall via `GameActionType.RecallAllegianceHometown = 0x02AB`, which fires `MotionCommand.AllegianceHometownRecall` animation and teleports. ### 6.4 Booting members When a player swears away, breaks, or gets booted from allegiance, any mansion access they had is revoked (`Player.CheckAllegianceHouse`). --- ## 7. MOTD and allegiance name ### 7.1 MOTD Stored on the `Allegiance` worldobject as `AllegianceMotd` + `AllegianceMotdSetBy` (string properties). On each login (after a 3s delay) the client receives it as a system chat message: ``` "\"{motd}\" -- {setBy}" ``` - Set: `HandleActionSetMotd` (Speaker+) - Clear: `HandleActionClearMotd` (Speaker+) - Query: `HandleActionQueryMotd` (anyone in allegiance) - Server sends via `GameMessageSystemChat`, not via `GameEventAllegiance*`. Decompiled client error strings around line 2068 include `L"Please use the allegiance panel to view your own information."`, meaning the client recognizes an allegiance-info request against self and redirects to the panel. ### 7.2 Allegiance name `AllegianceName` (string property on the Allegiance object). Castellan+ only. Validation rules (per decompiled error strings 2883-2912): - Not empty (command is `@allegiance name clear` for that). - Max 40 chars. - Only letters, spaces, `-`, `'`. - Not in banned-words list from `portal.dat`. - Not duplicate of another allegiance's name. - Change cooldown: **once every 24 hours** (`L"You may only change your allegiance name once every 24 hours. You may change your allegiance name again in %s.\n"`). ### 7.3 Officer titles Rank 1/2/3 → Speaker/Seneschal/Castellan by default. Castellans can rename them via `SetAllegianceOfficerTitle` (rank: 1-3, string title). --- ## 8. Officer permissions From `Player_Allegiance.cs` lines 1478-1517 and `AllegiancePermissionLevel.cs`: | Level (enum) | Numeric | Powers | |------------------|---------|-----------------------------------------------------------------| | None | 0 | member, no powers | | Speaker | 1 | allegiance chat kick/gag; allegiance broadcast; set/clear MOTD | | Seneschal | 2 | promote/demote Speakers; boot; ban; allegiance info; lock/unlock | | Castellan | 3 | promote/demote any rank; rename titles; set allegiance name; bindstone; mansion permissions; bypass lock with approved vassals | | Monarch | 4 | all of the above; clear all officers; everything | `AllegianceOfficerLevel` (wire enum, uint32): `Undef=0, Speaker=1, Seneschal=2, Castellan=3`. Monarch is implicit (Monarch doesn't have an officer rank; they're the root). Promote: `SetAllegianceOfficer(playerName, level)` — Seneschal+ (with caveat Seneschal can only promote to rank 1). Officer count limits: seen in decompile line 2738 `L"You already have the maximum number of allegiance officers. You must remove some before you add any more.\n"`. ACE doesn't implement a cap. Retail may have had one (~12?) but it's not enforced in ACE and no hard number appears in strings. --- ## 9. Wire messages ### 9.1 Server -> client events From `GameEventType.cs`: | Event | Opcode | Sent when | |-----------------------------------|---------|--------------------------------------------------| | `AllegianceUpdateAborted` | 0x0003 | Operation cancelled (e.g., target declined swear)| | `AllegianceUpdate` | 0x0020 | Full tree refresh (panel open, swear, break) | | `AllegianceAllegianceUpdateDone` | 0x01C8 | End-of-stream marker after AllegianceUpdate | | `AllegianceLoginNotification` | 0x027A | Ally logged in/out (if listen option on) | | `AllegianceInfoResponse` | 0x027C | Reply to AllegianceInfoRequest | ### 9.2 `AllegianceUpdate` payload From `GameEventAllegianceUpdate.cs` + `AllegianceProfile` + `AllegianceHierarchy` + `AllegianceData`: ``` GameEventAllegianceUpdate { uint32_t rank; // receiving player's rank AllegianceProfile prof; // tree } AllegianceProfile { uint32_t totalMembers; // whole allegiance (monarch + all followers) uint32_t totalVassals; // self's direct+indirect followers AllegianceHierarchy hierarchy; } AllegianceHierarchy { uint16_t recordCount; // number of ALL tree entries sent uint16_t oldVersion; // 0x000B (latest / only supported) PHashTable officers; // empty in retail, present for parser compat uint32_t officerTitleCount; WideString[] officerTitles; uint32_t monarchBroadcastTime; uint32_t monarchBroadcastsToday; uint32_t spokesBroadcastTime; uint32_t spokesBroadcastsToday; WideString motd; // EMPTY in ACE to avoid decal parse bugs; retail sent it WideString motdSetBy; // EMPTY in ACE uint32_t chatRoomID; // allegiance biota ID (chat channel) Position bindPoint; // sanctuary/bindstone (cell + pos + rot) WideString allegianceName; uint32_t nameLastSetTime; // "counts upward for some reason" uint32_t isLocked; // 0 or 1 int32_t approvedVassal; // legacy field (always 0?) AllegianceData monarchData; // first record: the monarch (Guid treeParent, AllegianceData)[recordCount-1] records; } AllegianceData { uint32_t characterID; uint32_t cpCached; // AllegianceXPCached clamped to uint32 max uint32_t cpTithed; // AllegianceXPGenerated uint32_t bitfield; // AllegianceIndex flags uint8_t gender; uint8_t heritage; uint16_t rank; // if HasPackedLevel(0x8): uint32_t level; uint16_t loyalty; uint16_t leadership; // if HasAllegianceAge(0x4): uint32_t timeOnline; uint32_t allegianceAge; // else: uint64_t uTimeOnline; WideString name; } ``` Per `AllegianceHierarchy.cs` comments (aclogview-derived): - record 0 = monarch (no treeParent guid prefix, sent inline) - record 1 = self's patron (treeParent = monarch) - record 2 = self (treeParent = patron) - record 3+ = self's direct vassals (treeParent = self) This is a **compact slice** centered on the receiving player — NOT the whole tree. To show the whole tree, the client aggregates successive updates by walking up/down via `AllegianceInfoRequest`. ### 9.3 Bitfield (`AllegianceIndex`) ```csharp [Flags] enum AllegianceIndex : uint { Undefined = 0x00, LoggedIn = 0x01, Update = 0x02, HasAllegianceAge = 0x04, HasPackedLevel = 0x08, MayPassupExperience = 0x10, } ``` Sent with `HasAllegianceAge | HasPackedLevel` baseline; `LoggedIn` added if player is online; `MayPassupExperience` for non-monarch vassals with `ExistedBeforeAllegianceXpChanges` flag. ### 9.4 Client -> server actions From `GameActionType.cs`: | GameAction | Opcode | Payload | |------------------------------------------|---------|--------------------------------| | `SwearAllegiance` | 0x001D | uint32 patronGuid | | `BreakAllegiance` | 0x001E | uint32 targetGuid | | `AllegianceUpdateRequest` | 0x001F | uint32 uiPanelBool | | `QueryAllegianceName` | 0x0030 | (empty) | | `ClearAllegianceName` | 0x0031 | (empty) | | `SetAllegianceName` | 0x0033 | string16L name | | `SetAllegianceOfficer` | 0x003B | string16L name, uint32 level | | `SetAllegianceOfficerTitle` | 0x003C | uint32 rank, string16L title | | `ListAllegianceOfficerTitles` | 0x003D | (empty) | | `ClearAllegianceOfficerTitles` | 0x003E | (empty) | | `DoAllegianceLockAction` | 0x003F | uint32 AllegianceLockAction | | `SetAllegianceApprovedVassal` | 0x0040 | string16L name | | `AllegianceChatGag` | 0x0041 | string16L name, uint32 bool | | `DoAllegianceHouseAction` | 0x0042 | uint32 AllegianceHouseAction | | `ModifyAllegianceGuestPermission` | 0x0267 | … | | `ModifyAllegianceStoragePermission` | 0x0268 | … | | `BreakAllegianceBoot` | 0x0277 | string16L name, uint32 bool | | `AllegianceInfoRequest` | 0x027B | string16L playerName | | `AllegianceChatBoot` | 0x02A0 | string16L name, string16L reason | | `AddAllegianceBan` | 0x02A1 | string16L name | | `RemoveAllegianceBan` | 0x02A2 | string16L name | | `ListAllegianceBans` | 0x02A3 | (empty) | | `RemoveAllegianceOfficer` | 0x02A5 | string16L name | | `ListAllegianceOfficers` | 0x02A6 | (empty) | | `ClearAllegianceOfficers` | 0x02A7 | (empty) | | `RecallAllegianceHometown` | 0x02AB | (empty) | ### 9.5 `ConfirmationType.SwearAllegiance` Swearing is two-phase: target gets a `Confirmation_SwearAllegiance` request. They must click Yes/No. The response comes back as a `GameAction` confirmation type; ACE routes it through the `ConfirmationManager`. acdream's client side needs to render the confirmation dialog. --- ## 10. UI panel The retail client's Allegiance tab (one of the main UI panels, reachable via a bottom-bar icon) shows: - **Header:** allegiance name + MOTD. - **Patron row** (above center): icon + name + rank/title. - **Self row** (center): highlighted; shows rank, level, loyalty, leadership, CP tithed, CP received. - **Vassal rows** (below center): each with login-status indicator (`*` for online, per retail text "An asterisk (*) indicates that the character is currently online"), rank, name, optional level. - **Buttons:** Swear (disabled if already sworn), Break (only if patron present), View (opens info request), View Monarch, Bindstone recall (if allegiance has a sanctuary). Clicking a member fires `AllegianceInfoRequest` → server responds with a wider `AllegianceInfoResponse`, which the client renders as the member's own node + their vassals (same record format). Navigation: the client supports walking up (click patron) and down (click vassal) to view the whole tree piecewise. The server always sends the **3-generation slice** centered on the queried player: patron, self, direct vassals. ### 10.1 Client commands (text input) From decompile chunk_00570000 lines 6834-6852 (help output): - `@help allegiances` — overview. - `@allegiance` — allegiance commands menu. - `@allegiance motd [text]` — set/clear MOTD (same as @motd). - `@allegiance swear ` — alternative to right-click swear. - `@allegiance break [name]` — break from patron/vassal. - `@allegiance boot [account]` — Seneschal+. - `@allegiance chat gag ` / `ungag` — Speaker+. - `@allegiance chat kick ` — Speaker+ (permanent). - `@allegiance ban add/remove/list` — Seneschal+. - `@allegiance lock on/off/toggle/check` — Seneschal+. - `@allegiance approved add/clear/check` — Castellan+. - `@allegiance name [new]/clear/?` — Castellan+. - `@allegiance officer ` — Seneschal+/Castellan+. - `@allegiance officers list/clear/titles` — members see list; Seneschal+ manage. - `@allegiance house help/guest/storage open/close` — Castellan+. - `@allegiance hometown` — recall. - `@allegiance ignore on/off` — per-player flag (`CharacterOption.IgnoreAllegianceRequests`). - `@allegiance info ` — detailed info on specific member. ### 10.2 Chat shortcuts - `@a ` — allegiance broadcast. - `@c ` — co-vassals. - `@v ` — tell one vassal. - `@p ` — tell patron. - `@pr ` — reply to last @p. - `@m ` — tell monarch. - `@mr ` — monarch reply to last @m. --- ## 11. Port plan for acdream ### 11.1 Data types (src/acdream.core / acdream.protocol) ```csharp public readonly record struct AllegianceGuid(uint Value); public sealed class AllegianceNode { public AllegianceGuid PlayerGuid { get; init; } public string Name { get; init; } = ""; public HeritageGroup Heritage { get; init; } public Gender Gender { get; init; } public uint Rank { get; set; } public uint Level { get; set; } public ushort Loyalty { get; set; } public ushort Leadership { get; set; } public ulong AllegianceXPCached { get; set; } public ulong AllegianceXPGenerated { get; set; } public bool IsLoggedIn { get; set; } public bool MayPassupExperience { get; set; } public TimeSpan TimeOnline { get; set; } public TimeSpan AllegianceAge { get; set; } public AllegianceNode? Patron { get; set; } public List Vassals { get; } = new(); public bool IsMonarch => Patron is null; // Retail rank algorithm (AllegianceNode.cs:69-88). public void RecalculateRank() { ... } } public sealed class AllegianceTree { public AllegianceNode Monarch { get; private set; } public AllegianceNode Self { get; private set; } public string? AllegianceName { get; set; } public string? Motd { get; set; } public string? MotdSetBy { get; set; } public Position? BindPoint { get; set; } public bool IsLocked { get; set; } public uint ChatRoomId { get; set; } public void ApplyUpdate(AllegianceUpdatePacket pkt) { ... } public void ApplyInfoResponse(AllegianceInfoPacket pkt) { ... } } ``` ### 11.2 Wire codecs (src/acdream.protocol.events) Port `AllegianceData`, `AllegianceHierarchy`, `AllegianceProfile` as pure C# readers — **reverse** of ACE's Writer classes. Cross-reference against `references/Chorizite.ACProtocol/Types/` for field-order confirmation. Event handlers to register with the dispatcher: - `0x0003 AllegianceUpdateAborted` - `0x0020 AllegianceUpdate` - `0x01C8 AllegianceAllegianceUpdateDone` (just acks end of update, can trigger UI redraw) - `0x027A AllegianceLoginNotification` (update online status in tree) - `0x027C AllegianceInfoResponse` ### 11.3 Outbound actions (src/acdream.protocol.actions) One builder per GameAction listed in §9.4. Match the payload layouts exactly. Test round-trip against captured retail pcaps if available (none in repo currently; treat ACE's writer as ground truth in the meantime). ### 11.4 UI panel (src/acdream.ui.allegiance) Following the architecture doc's component pattern: ```csharp public sealed class AllegiancePanel : IUiPanel { private readonly AllegianceTree _tree; private readonly IPlayerNetworkClient _net; public void Render(ImDrawList draw) { // Header: name + motd // Patron row (tinted, above center) // Self row (highlighted, center) // Vassal list (indented, below center) // Buttons: Swear / Break / Info / Recall } public void OnClickNode(AllegianceGuid guid) => _net.SendGameAction(new AllegianceInfoRequestAction(name)); } ``` Title strings: port the full `AllegianceTitle` table verbatim — all 18 heritage/gender variants. ### 11.5 Chat integration Extend the chat layer (R6/UI slice 05) to handle the five channel prefixes. Channel routing: - Inbound: match `ChatMessageType` enum value 0x12 ("Allegiance") for allegiance broadcasts; prefix by `[Allegiance]` in the render. - Outbound: when user types `@a ...`, map to broadcast; `@c`, `@v `, `@p`, `@m` each go to a specific recipient guid via directed chat (text + flag bits per §5). - Gag/boot filter: client honors no filter state locally — server does the enforcement — but if gagged, client should gray out the "send to allegiance chat" button and show a status line. ### 11.6 MOTD hook Subscribe to `GameMessageSystemChat` containing the MOTD pattern (`"…" -- …`) and also proactively request via `QueryAllegianceName` when opening the panel. Cache in `AllegianceTree.Motd`. ### 11.7 Testing strategy 1. **Conformance tests**: build an allegiance tree programmatically and check rank calculation matches ACE's algorithm on edge cases (solo, 1-chain, 11-branch wide, 10-deep narrow). 2. **Wire round-trip**: given an `AllegianceHierarchy` writer output from a mocked ACE server, decode it with acdream's reader and verify all fields match. 3. **Title table**: parameterized test asserting every (heritage, gender, rank) pair matches the retail string in `AllegianceTitle.cs`. 4. **Channel routing**: feed synthetic chat-flag bytes (0x1000, 0x2000, 0x4000, 0x1000000, 0x2000000) into the parser; verify they produce the correct prefix and channel tag. ### 11.8 Phasing This is a **later-phase** feature — not needed for R1 (terrain render) or R2 (basic movement). Suggested slot in the roadmap: - **Phase A.4 or later** (after world-server login is working), when the client first receives `AllegianceUpdate` packets. - Initial pass: decode and log; no UI. Use logs to verify wire format. - Second pass: render read-only tree panel. - Third pass: outbound commands (swear/break/info) — each tested against a mock server before pointing at a real ACEmulator instance. - Fourth pass: chat channel integration — easy once generic chat plumbing exists. The plugin API should expose the entire tree, the self-node, and a "send allegiance command" function. Automation scripts want to see allegiance info and send MOTD updates or boot commands. --- ## 12. Key files for future work | File | What's in it | |---------------------------------------------------------------|-----------------------------------------------------| | `references/ACE/.../Entity/AllegianceNode.cs` | Rank algorithm, tree walking | | `references/ACE/.../Entity/AllegianceRank.cs` (AllegianceTitle) | Full heritage × gender × rank string table | | `references/ACE/.../Managers/AllegianceManager.cs` | XP pass-up formulas, tree rebuild | | `references/ACE/.../WorldObjects/Allegiance.cs` | Allegiance worldobject (name, MOTD, bans, officers) | | `references/ACE/.../WorldObjects/Player_Allegiance.cs` | All player-side handlers (swear, break, chat, etc.) | | `references/ACE/.../Network/Structure/Allegiance*.cs` | **Wire format writers** — reverse for acdream reader | | `references/ACE/.../Network/GameEvent/Events/Allegiance*.cs` | Server-to-client event definitions | | `references/ACE/.../Network/GameAction/Actions/Allegiance*.cs`| Client-to-server action definitions | | `references/ACE/.../Entity/Enum/Allegiance*.cs` | Officer level, permission level, lock action, house action enums | | `docs/research/decompiled/chunk_00570000.c` | Client-side chat channel bitmasks, error strings, help text | | `docs/research/decompiled/chunk_00560000.c` | Allegiance information panel rendering (line 7245) | | `docs/research/decompiled/chunk_006B0000.c` | ChatMessageType enum values (Allegiance = 0x12) | --- **Next steps once we're in Phase 4+ (networking live):** 1. Capture an `AllegianceUpdate` packet from a live ACE shard. 2. Decode it with acdream's port of the hierarchy reader. 3. Spot-verify every field matches what ACE's writer would have produced for the same node. 4. Build the tree renderer on top of that model. 5. Add outbound actions one at a time (start with `AllegianceUpdateRequest` — it's the simplest and used most).