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.
This commit is contained in:
parent
7230c1590f
commit
3f913f1999
20 changed files with 15312 additions and 17 deletions
993
docs/research/deepdives/r11-allegiance.md
Normal file
993
docs/research/deepdives/r11-allegiance.md
Normal file
|
|
@ -0,0 +1,993 @@
|
|||
# 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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue