acdream/docs/research/deepdives/r11-allegiance.md
Erik 3f913f1999 docs+feat: 13 retail-AC deep-dives (R1-R13) + C# port scaffolds + roadmap E-H
78,000 words of grounded, citation-backed research across 13 major AC
subsystems, produced by 13 parallel Opus-4.7 high-effort agents. Plus
compact C# port scaffolds for the top-5 systems and a phase-E-through-H
roadmap update sequencing the work.

Research (docs/research/deepdives/):
- 00-master-synthesis.md          (navigation hub + dependency graph)
- r01-spell-system.md        5.4K words (fizzle sigmoid, 8 tabs, 0x004A wire)
- r02-combat-system.md       5.9K words (damage formula, crit, body table)
- r03-motion-animation.md    8.2K words (450+ commands, 27 hook types)
- r04-vfx-particles.md       5.8K words (13 ParticleType, PhysicsScript)
- r05-audio-sound.md         5.6K words (DirectSound 8, CPU falloff)
- r06-items-inventory.md     7.4K words (ItemType flags, EquipMask 31 slots)
- r07-character-creation.md  6.3K words (CharGen dat, 13 heritages)
- r08-network-protocol-atlas 9.7K words (63+149+94 opcodes mapped)
- r09-dungeon-portal-space.md 6.3K words (EnvCell, PlayerTeleport flow)
- r10-quest-dialogs.md       7.1K words (emote-script VM, 122 actions)
- r11-allegiance.md          5.4K words (tree + XP passup + 5 channels)
- r12-weather-daynight.md    4.5K words (deterministic client-side)
- r13-dynamic-lighting.md    4.9K words (8-light cap, hard Range cutoff)

Every claim cites a FUN_ address, ACE file path, DatReaderWriter type,
or holtburger/ACViewer reference. The master synthesis ties them into a
dependency graph and phase sequence.

Key architectural finding: of 94 GameEvents in the 0xF7B0 envelope,
ZERO are handled today — that's the largest network-protocol gap and
blocks F.2 (items) + F.5 (panels) + H.1 (chat).

C# scaffolds (src/AcDream.Core/):
- Items/ItemInstance.cs    — ItemType/EquipMask enums, ItemInstance,
                             Container, PropertyBundle, BurdenMath
- Spells/SpellModel.cs      — SpellDatEntry, SpellComponentEntry,
                             SpellCastStateMachine, ActiveBuff,
                             SpellMath (fizzle sigmoid + mana cost)
- Combat/CombatModel.cs     — CombatMode/AttackType/DamageType/BodyPart,
                             DamageEvent record, CombatMath (hit-chance
                             sigmoids, power/accuracy mods, damage formula),
                             ArmorBuild
- Audio/AudioModel.cs       — SoundId enum, SoundEntry, WaveData,
                             IAudioEngine / ISoundCache contracts,
                             AudioFalloff (inverse-square)
- Vfx/VfxModel.cs           — 13 ParticleType integrators, EmitterDesc,
                             PhysicsScript + hooks, Particle struct,
                             ParticleEmitter, IParticleSystem contract

All Core-layer data models; platform-backed engines live in AcDream.App.
Compiles clean; 470 tests still pass.

Roadmap (docs/plans/2026-04-11-roadmap.md):
- Phase E — "Feel alive": motion-hooks + audio + VFX
- Phase F — Fight + cast + gear: GameEvent dispatch, inventory,
            combat, spell, core panels
- Phase G — World systems: sky/weather, dynamic lighting, dungeons
- Phase H — Social + progression: chat, allegiance, quests, char creation
- Phase J — Long-tail (renumbered from old Phase E)

Quick-lookup table updated with 10+ new rows mapping observations to
new phase letters.
2026-04-18 10:32:44 +02:00

41 KiB
Raw Blame History

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 decompileddocs/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 serverreferences/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:

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:

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: HandleActionSwearAllegianceIsPledgableSwearAllegiance. 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:

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:

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:

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.

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:

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)

[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)

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:

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).