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.
993 lines
41 KiB
Markdown
993 lines
41 KiB
Markdown
# 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).
|