# 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<uint, AllegianceNode> 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 <Tell:...> says to you, \""`| `0x1000`             | @p         |
| Vassal tell   | `"Your vassal <Tell:...> says to you, \""`| `0x2000`             | @v         |
| Follower tell | `"Your follower <Tell:...> 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<ObjectGuid, DateTime>` — 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<Guid, uint32> 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 <name>` — alternative to right-click swear.
- `@allegiance break [name]` — break from patron/vassal.
- `@allegiance boot <name> [account]` — Seneschal+.
- `@allegiance chat gag <name>` / `ungag` — Speaker+.
- `@allegiance chat kick <name> <reason>` — 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 <title|rank> <level> <name>` — 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 <name>` — detailed info on specific member.

### 10.2 Chat shortcuts

- `@a <text>`   — allegiance broadcast.
- `@c <text>`   — co-vassals.
- `@v <name> <text>` — tell one vassal.
- `@p <text>`   — tell patron.
- `@pr <text>`  — reply to last @p.
- `@m <text>`   — tell monarch.
- `@mr <text>`  — 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<AllegianceNode> 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
  <name>`, `@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).
