# Retail AC Client — UI Panels Reference

This document maps each major UI panel in the retail Asheron's Call client to its decompiled source in `acclient.exe`, the wire messages that drive it, and a C# port sketch for acdream. Data was cross-referenced against:

- Decompiled acclient chunks `0x004A0000`–`0x005C0000` (22,225 functions, 688K lines of C).
- `ACE.Server.Network.GameMessages` + `ACE.Server.Network.GameEvent.Events` (wire format).
- `ACE.Entity.Enum.*` (Channel, ChatType, ChatMessageType, EquipMask, CharacterOption).

## Common UI architecture observed in decompilation

Before drilling into individual panels, the decompiled code shows a consistent pattern. Every panel is a C++ object that lives at a `this`-pointer offset inside a parent "frame" object. The same helper functions recur across all panels:

| Address | Purpose |
|---------|---------|
| `FUN_00463c00(0x100000XX)` | Lookup widget by **resource ID** (globally-registered UI asset IDs). Returns widget pointer. |
| `FUN_0046a740(&localVar)` | Assign widget pointer with ref-counted intrusive reference. |
| `FUN_0040b8f0(L"literal")` | Emit a wide-string chunk into the current text-builder stream (for rich-text lines). |
| `FUN_00407e40(L"literal")` | Append wide-string to tooltip builder. |
| `FUN_00402490(L"literal", sVar)` | Append to richer formatted text with length. |
| `FUN_0046f670(a, b)` | Emit a formatting/delimiter token to a text-layout buffer (`(1,0)` = newline, `(2,0)` = tab, `(0,0)` = section). |
| `FUN_00463c00(0x100002f9)` / `FUN_00463c00(0x100002fc)` / `FUN_00463c00(0x100002fd)` | Text formatters for "field-name" / "value" / "highlight-value" column rendering. |
| `FUN_0042dc80()` / `FUN_0042cbe0(&hdl, 1)` / `FUN_0042e590()` | Tooltip open → content → close sequence. |
| `FUN_004618a0(auStack_90)` | Anchor tooltip to current widget. |
| `FUN_00460270(resourceId, &callback)` | Register a global event callback for a resource ID. |
| `FUN_0058f8e0/0058f8b0(&out, obj, kind, flag)` | Lookup weenie/object name string (kind 2 = article-prefixed name). |

**Widget class layout observation.** Most panels store their child-widget pointers at fixed offsets from the panel `this` pointer. E.g. the paperdoll panel at `param_1+0x604`, `0x608`, …, `0x664` holds ~25 widget pointers for 16+ slot icons plus container, tooltip overlay, and model-preview widgets. Knowing the offset pattern lets you correlate a panel's widget set purely from one bind-event function.

**Resource ID encoding.** Button/text/layout asset IDs use the magic prefix `0x10000000` plus a small integer (e.g. `0x100000ab` = Strength icon, `0x100000fe` = generic "attribute row" layout, `0x100002fc` = field value text style, `0x100005c3` = Skill Credits icon). These are indexed into the same dat table the dats use for UI layouts.

**Shared abstractions.** Panels that show lists of rows (skills, spell icons, inventory cells, fellowship members, allegiance tree) all use the same pattern: a parent container widget, a `FUN_0046f670(1,0)` row-break emission, then a per-row callback that writes the row's icon + label + value text. Panels with tabs (spells, chat) use a mode-byte stored in the panel's flag field (e.g. spell tab offsets `0x1c * tabIndex + 0x634`).

## 1. Chat window

**Location**: The chat input/handler is spread across multiple chunks; inbound routing lives in `chunk_00570000.c::FUN_00573XXX` (the big server-message switch). Channel enumeration comes from `GameEventType.ChannelBroadcast = 0x0147` (ACE mapping).

### Channels (Channel.cs bitmask)

Channels are a 32-bit bitmask, NOT a single enum value. The chat UI tracks which channels the player is subscribed to and routes incoming `ChannelBroadcast` events by bit.

| Channel | Bit | Command | Notes |
|---------|-----|---------|-------|
| Abuse | `0x00000001` | `@abuse` | |
| Admin | `0x00000002` | `@admin` / `@ad` | |
| Audit | `0x00000004` | `@audit` / `@au` | Echo of enforcement commands to admins |
| Advocate1-3 | `0x00000008` / `10` / `20` | `@advocate` | |
| QA1/QA2 | `0x40` / `0x80` | | |
| Debug | `0x00000100` | | |
| Sentinel | `0x00000200` | `@sent` | |
| Help | `0x00000400` | | |
| Fellow | `0x00000800` | `@f` | Fellowship |
| Vassals | `0x00001000` | `@v` | |
| Patron | `0x00002000` | `@p` | |
| Monarch | `0x00004000` | `@m` | |
| AlArqas | `0x00008000` | | Society town channels |
| Holtburg | `0x00010000` | | |
| Lytelthorpe | `0x00020000` | | |
| Nanto | `0x00040000` | | |
| Rithwic | `0x00080000` | | |
| Samsur | `0x00100000` | | |
| Shoushi | `0x00200000` | | |
| Yanshi | `0x00400000` | | |
| Yaraq | `0x00800000` | | |
| CoVassals | `0x01000000` | `@c` | |
| AllegianceBroadcast | `0x02000000` | `@a` | "Tell All Allegiance Members" |
| FellowBroadcast | `0x04000000` | | Leader-to-fellowship |
| SocietyCelHan | `0x08000000` | | Celestial Hand society |
| SocietyEldWeb | `0x10000000` | | Eldrytch Web |
| SocietyRadBlo | `0x20000000` | | Radiant Blood |
| Olthoi | `0x40000000` | | Olthoi-only |

### ChatType (high-level category sent with each message)

`ACE.Entity.Enum.ChatType`: Undef, Allegiance, General, Trade, LFG, Roleplay, Society, SocietyCelHan, SocietyEldWeb, SocietyRadBlo, Olthoi.

### ChatMessageType (color / filter / tab-routing)

A 32-bit enum used in almost every outbound UI message to colorize and categorize a line:

| Value | Name | Color/Behavior |
|-------|------|----------------|
| `0x00` | Broadcast | Default — shopkeepers, allegiance MOTD, crafting results, mana-stone messages |
| `0x01` | AllChannels | |
| `0x02` | Speech | "Name says, …" |
| `0x03` | Tell | Incoming /tell |
| `0x04` | OutgoingTell | "You tell …" |
| `0x05` | System | Red warning text |
| `0x06` | Combat | Damage lines |
| `0x07` | Magic | Enchantment applied/resisted |
| `0x08` | Channel | Light pink — @admin, @audit, @av1-3, @sent |
| `0x09` | ChannelSend | |
| `0x0A` | Social | Bright yellow |
| `0x0B` | SocialSend | Light yellow |
| `0x0C` | Emote | Creature emote text (via `HearSpeech`/`HearRangedSpeech`) |
| `0x0D` | Advancement | Level-up / skill-gain |
| `0x0E` | Abuse | Light cyan |
| `0x0F` | Help | Red — urgent-help echo |
| `0x10` | Appraisal | Assess failure |
| `0x11` | Spellcasting | Spell syllable text (`Malar Quaril`, etc) |
| `0x12` | Allegiance | Allegiance chat |
| `0x13` | Fellowship | Bright yellow fellowship chat |
| `0x14` | WorldBroadcast | Green |
| `0x15` | CombatEnemy | Red |
| `0x16` | CombatSelf | Pink |
| `0x17` | Recall | |
| `0x18` | Craft | |
| `0x19` | Salvaging | Green |
| `0x1F` | AdminTell | Bright yellow |

### Wire messages that drive the chat window

1. **`GameEventChannelBroadcast` (0xF7B0 / 0x0147)** — 12+ bytes: `uint chatChannel | string16L senderName | string16L messageText`. The sender name is empty when the server is echoing your own message back to you, triggering "You say…" rendering.
2. **`GameEventTell` (0xF7B0 / 0x02BD)** — `string16L messageText | string16L senderName | guid senderID | guid targetID | uint chatMessageType | uint extraZero`.
3. **`GameMessageSystemChat` (0xF7E0)** — `string16L message | int chatMessageType`. The main path for system notifications.
4. **`GameMessageHearSpeech` (0x02BB)** — `string16L messageText | string16L senderName | uint senderID | uint chatMessageType`. Creature/player speech in the 3D world.
5. **`GameMessageHearRangedSpeech` (0x02BC)** — same layout; broadcast to a wider radius (quest giver yell).
6. **`GameMessageTurbineChat` (0xF7DE)** — the Turbine overlay for cross-server chat (General, Trade, LFG, Roleplay, Society). Has a nested blob format: `uint bytesToFollow | ChatNetworkBlobType type | uint dispatchType | uint 1 | uint 0x000B00B5 | uint 1 | uint 0x000B00B5 | uint 0 | uint nestedBytes | uint channel | byteLen-or-u16Len+UTF16 senderName | byteLen-or-u16Len+UTF16 message | uint 0x0C | guid senderID | uint 0 | ChatType`.
7. **`GameEventSetTurbineChatChannels` (0x0295)** — tells the client which Turbine channels to enable (sent at login).

### Chat window layout (inferred from message types and ACE logging)

- **Top bar**: tab strip (`General`, `Combat`, `Chat`, `1`, `2`, …). Each tab has a filter mask over ChatMessageType values (ChatDisplayMask / ChatFilterMask enums).
- **Text scroll area**: ring buffer of rendered lines. Each line carries its source ChatMessageType so the tab filter can decide to show it; font color is derived from the ChatMessageType too.
- **Input line** at the bottom: mode byte (`/s` say, `/t name` tell, `/f` fellow, `/a` allegiance, `@channel` for named channels, raw text = last-used mode). The client sends `GameActionChatChannel` on enter.
- **Filters**: Character Options (`ListenToAllegianceChat` 0x1B, `ListenToGeneralChat` 0x23, `ListenToTradeChat` 0x24, `ListenToLFGChat` 0x25, `ListenToRoleplayChat` 0x26, `ListenToSocietyChat` 0x2E, `ListenToPKDeathMessages` 0x34) control what passes through the filter.

### Strings observed

Routing strings from the big server-message switch in `chunk_00570000.c`:

- `L"You have entered the %s channel.\n"` (case `0x51b`)
- `L"You have left the %s channel.\n"` (case `0x51c`)
- `L"That channel doesn\'t exist."`
- `L"You can\'t use that channel."`
- `L"You\'re already on that channel."`
- `L"You\'re not currently on that channel."`
- `L"Message Blocked: %s"` (case `0x51f` — squelched sender)
- `L"%s has been added to the list of people you can hear.\n"`
- `L"You are now deaf to player\'s screams.\n"`
- `L"You can hear all players once again.\n"`

### C# port sketch — `ChatWindow`

```csharp
namespace AcDream.UI.Panels;

public enum ChatTabKind { General, Combat, Chat1, Chat2, Chat3, Chat4 }

public sealed class ChatTab
{
    public string Label { get; init; }
    public ChatDisplayMask VisibleTypes { get; set; }   // bitmask over ChatMessageType values 0x00-0x1F
    public Channel ChannelFilter { get; set; }          // bitmask over Channel enum
    public readonly RingBuffer<ChatLine> Lines = new(2048);
    public bool UnreadBadge { get; set; }
}

public readonly record struct ChatLine(
    ulong Timestamp,
    ChatMessageType MessageType,
    Channel Channel,
    uint SenderGuid,
    string SenderName,
    string Text,
    Color Color);

public sealed class ChatWindow : IPanel
{
    public IReadOnlyList<ChatTab> Tabs { get; }
    public int ActiveTabIndex { get; set; }
    public ChatInputMode InputMode { get; set; }   // Say / Tell / Fellow / Allegiance / Named("AlArqas")
    public string PendingTellTarget { get; set; }
    public uint SubscribedChannels { get; private set; }  // bitmask

    // Drivers (server→UI events)
    public void OnChannelBroadcast(Channel ch, string sender, string text);
    public void OnTell(string text, string sender, uint senderGuid, uint targetGuid, ChatMessageType type);
    public void OnSystemChat(string text, ChatMessageType type);
    public void OnHearSpeech(string text, string sender, uint senderGuid, ChatMessageType type);
    public void OnTurbineChat(uint channel, string sender, string text, ChatType chatType);
    public void OnSetTurbineChannels(uint[] channels);

    // Outbound (UI→server)
    public void Send(string text);   // routes through InputMode to GameActionChatChannel / Say / Tell etc.
}
```

## 2. Character Attributes panel

**Location**: `chunk_00470000.c::FUN_0047ba70` (the monolithic "character sheet" builder, starting at line ~8280 with "Gender:" and running through skills). Strings rendered via `FUN_0040b8f0`. The sheet body is a single giant switch-style render; each row is emitted via `FUN_0046f670(1,0)` row-break plus a field/value pair.

### Layout (from the render order in `FUN_0047ba70`)

The sheet renders top-down:

1. **Gender** (strings table at `PTR_DAT_0081a1c4` — "Male" / "Female" / "Unknown").
2. **Heritage** (strings at `PTR_DAT_0081a1d0`, 5 entries — Aluvian, Gharu'ndim, Sho, Viamontian, Umbraen/Empyrean).
3. **Starting Town** (strings at `PTR_u_Holtburg_0081a1e4`, 4 entries — Holtburg, Shoushi, Yaraq, Sanamar/Rithwic per heritage).
4. **"Attributes" header** (row break, layout resource `0x100000fe`).
5. **10 attribute rows** (loop `iVar10 < 10`), each using formatter resource `0x100002fc` (label) + `0x100002fd` (value):
   - Case 0 — Strength (value at `player + 0x184`)
   - Case 1 — Endurance (value at `+0x188`)
   - Case 2 — Coordination (value at `+0x18c`)
   - Case 3 — Quickness (value at `+0x190` = 400 dec)
   - Case 4 — Focus (value at `+0x194`)
   - Case 5 — Self (value at `+0x198`)
   - Case 6 — Health (max via `FUN_005df4c4`, curr via `FUN_005c4990(2)`)
   - Case 7 — Stamina (`FUN_005c4990(2)` → returns stamina)
   - Case 8 — Mana (`FUN_005c4990(6)`)
   - Case 9 — Skill Credits (value at `+0x1b8`)
6. **4 skill-category headers** (loop `iStack_10 < 4`):
   - Case 0 — "Specialized Skills" (filter rank `iStack_14 = 3`)
   - Case 1 — "Trained Skills" (filter rank `2`)
   - Case 2 — "Useable Untrained Skills" (filter rank `1`, show only if skill.usableWithoutTraining)
   - Case 3 — "Unuseable Untrained Skills" (filter rank `1`, inverse)
7. **Per-skill rows** — walk the skill linked list at `player + 0x1c4`; for each node read skill-id (`+0x20`), category (`FUN_005c4be0`), value (`FUN_005c5b30`), and emit row only if `category == iStack_14`.

### Short form (from `chunk_005C0000.c::FUN_005c9d70` — tooltip dispatch)

A small helper that picks an attribute label by 1-based switch:
1 → Strength, 2 → Endurance, 3 → Quickness, 4 → Coordination, 5 → Focus, 6 → Self.

Descriptions from `FUN_005c9e10`:
- Strength: "Measures your character's muscular power."
- Endurance: "Measures how healthy your character is."
- Quickness: "Measures how fast your character is."
- Coordination: "Measures your character's reflexes"
- Focus: "Measures your character's mind and senses."
- Self: "Measures your character's willpower."

Vitals from `FUN_005c9eb0`: Maximum Health, Health, Maximum Stamina, Stamina, Maximum Mana, Mana.

Vital descriptions (`FUN_005c9f50`):
- Health: "(Endurance/2)\nIf you run out of health, you will die!"
- Stamina: "(Endurance)\nAffects your actions and movement."
- Mana: "(Self)\nAffects how much magic you can cast."

### Wire drivers

- **`GameMessagePrivateUpdateAttribute` (0x02E3)** — `uint sequence | PropertyAttribute attr | uint ranks | uint startingValue | uint xpSpent`. Sent per-attribute on gain. 21 bytes.
- **`GameMessagePrivateUpdateVital` (0x02E7)** — `uint sequence | PropertyAttribute2ndLevel vital | uint ranks | uint startingValue | uint xpSpent | uint current`. 25 bytes.
- **`GameMessagePrivateUpdateAttribute2ndLevel` (0x02E9)** — max-value refresh for vitals.
- **`GameEventPlayerDescription` (0xF7B0 / 0x0013)** — initial full character dump at login; includes all attributes, vitals, skills, equipped objects, clothing/hair metadata.

### C# port sketch — `AttributesPanel`

```csharp
public enum PropertyAttribute  { Strength=1, Endurance, Quickness, Coordination, Focus, Self }
public enum PropertyAttribute2ndLevel { MaxHealth=1, Health, MaxStamina, Stamina, MaxMana, Mana }

public readonly record struct AttributeStats(uint Ranks, uint StartingValue, uint XpSpent)
{
    public uint Current => StartingValue + Ranks;
}

public readonly record struct VitalStats(uint Ranks, uint StartingValue, uint XpSpent, uint Current)
{
    public uint Max => StartingValue + Ranks;
}

public sealed class AttributesPanel : IPanel
{
    public Gender Gender { get; set; }
    public HeritageGroup Heritage { get; set; }
    public StartingTown StartingTown { get; set; }

    public AttributeStats Strength, Endurance, Coordination, Quickness, Focus, Self;
    public VitalStats Health, Stamina, Mana;
    public uint SkillCredits;

    public void OnUpdateAttribute(PropertyAttribute attr, AttributeStats stats);
    public void OnUpdateVital(PropertyAttribute2ndLevel vital, VitalStats stats);
    public void OnUpdateSkillCredits(uint credits);  // via PrivateUpdatePropertyInt
}
```

## 3. Skills panel

Shares the character sheet with Attributes (both live inside `FUN_0047ba70`). The 4 skill categories ("Specialized Skills", "Trained Skills", "Useable Untrained Skills", "Unuseable Untrained Skills") appear below the attribute block.

### Skill advancement classes (AdvancementClass enum)

1 = Untrained, 2 = Trained, 3 = Specialized.

### Display rules (from the decompile)

- Each skill row is emitted as: `[category-header]\n<tab>skill-name<tab>value`.
- Specialized skills show in cyan/white, Trained in white, Untrained-useable in gray, Untrained-unuseable hidden by default (still iterated).
- "Useable Untrained" means the skill has `UsableWithoutTraining` property = true (e.g. Run, Jump, Loyalty) AND `AdvancementClass == Untrained`.
- For each skill row the decompile also looks up a hashtable at `iStack_4 + 8` (keyed by `skillId % bucketCount`) — likely the skill hash used for secondary display metadata (icon, current training rank).

### Buy/Train interaction

The sheet also has buttons (not in this render function but registered via event callbacks earlier) for Train and Specialize — they cost skill credits (displayed in Attributes section) and XP. Success/failure echoes via `GameMessageSystemChat`. Strings seen in `chunk_00570000.c`:

- `L"You have failed to alter your skill.\n"`
- `L"Your %s skill is already untrained!\n"`
- `L"Although you cannot untrain your %s skill, you have succeeded in recovering all the experience you had invested in it.\n"`
- `L"Although your augmentation will not allow you to untrain your %s skill, you have succeeded in recovering all the experience you had invested in it.\n"`

### Wire drivers

- **`GameMessagePrivateUpdateSkill` (0x02DD)** — `uint sequence | PropertySkill skill | uint ranks | ushort adjustPP | uint advancementClass | uint xpSpent | uint initLevel | uint resistanceAtLastCheck | double lastUsedTime`. 37 bytes.
- **`GameMessagePrivateUpdateSkillLevel` (0x02DF)** — periodic rank update.

### C# port sketch — `SkillsPanel`

```csharp
public enum SkillAdvancementClass { Undef=0, Inactive=1, Untrained=2, Trained=3, Specialized=4 }

public readonly record struct SkillState(
    uint Skill,
    uint Ranks,
    SkillAdvancementClass Class,
    uint XpSpent,
    uint InitLevel,
    uint ResistanceAtLastCheck,
    double LastUsedTime)
{
    public uint Current => InitLevel + Ranks;
}

public sealed class SkillsPanel : IPanel
{
    public readonly Dictionary<Skill, SkillState> Skills = new();
    public void OnUpdateSkill(SkillState s);
    public void RaiseTrainSkill(Skill s);       // sends GameAction TrainSkill
    public void RaiseSpecializeSkill(Skill s);
    public void RaiseUntrainSkill(Skill s);

    public IEnumerable<SkillState> Specialized => Skills.Values.Where(s => s.Class == SkillAdvancementClass.Specialized);
    public IEnumerable<SkillState> Trained     => Skills.Values.Where(s => s.Class == SkillAdvancementClass.Trained);
    public IEnumerable<SkillState> UntrainedUsable;  // needs Skill metadata lookup
}
```

## 4. Spell panel

**Location**: `chunk_004C0000.c` — the spell-bar code. Entry point `FUN_004c68f0` (the spellbar bind function) at address `0x004C68F0`.

### Spell tabs (7 tabs)

From `FUN_004c6500` (returns the active tab index from a mode byte at `spellbar + 0x5fc.+0x6d8`):

| Tab | Value | Icon resource ID | Represents |
|-----|-------|------------------|------------|
| 0 | Life | `0x100000aa` (button) / `0x100000a3` (icon) | Life magic |
| 1 | Creature | `0x100000ab` / `0x100000a4` | Creature Enchantment |
| 2 | Item | `0x100000ac` / `0x100000a5` | Item Enchantment |
| 3 | War | `0x100000ad` / `0x100000a6` | War Magic |
| 4 | Portal | `0x100000ae` / `0x100000a7` | Portal Magic (recall/summon) |
| 5 | (unused retail) | `0x100000af` / `0x100000a8` | |
| 6 | (unused retail) | `0x100000b0` / `0x100000a9` | |
| 7 | Void | `0x100005c3` / `0x100005c2` | Void Magic (post-ToD) |

Each tab is registered into the spellbar via `FUN_004c6680(spellbar, buttonId, iconId, tabIndex)`.

### Per-tab spell slot storage

The spellbar keeps 7 slots per tab. Slot storage lives at `spellbar + 0x634 + (tabIndex * 0x1c)` (28 bytes per tab: 7 slots × 4-byte spell IDs = 28).

### Ready/selected spell

- `spellbar + 0x620` = currently-selected spell's container object ID.
- `spellbar + 0x624` = spell ID of the selected spell.
- `iVar4 * 0x1c + 0x640 + spellbar` = per-tab "is this tab the current ready tab?" flag byte.

### Cast interaction (`FUN_004c78XX` region, around line 5040-5210)

- User double-clicks a spell slot → `FUN_004c7c50` fires.
- Single-click just highlights via `FUN_006a9640` and shows "Double-click to cast this spell" tooltip (`L"%hs\nDouble-click to cast this spell"`).
- On cast button press: validates target via `DAT_00871e54` (current selection), checks spell-can-target-self via `FUN_00588350`, and if valid formats `L"CAST %hs"` + optional `L" on %s"` and sends via `FUN_005abb30`.
- Error paths:
  - `L"Select a spell to cast"` (no slot filled, but spells known) — `FUN_00407e40` tooltip bubble.
  - `L"You have no spells ready to cast"` (no tab populated at all).
  - `L"You must select a target for %hs"` (no selection).
  - `L"You must select an appropriate target for %hs"` (wrong target type).
  - `L"You cannot cast this spell upon yourself"` (self-cast when not allowed).
  - `L"This spell would require a target"` / `L"This spell would require no target"` (target-present/absent mismatch).
  - `L"Cannot cast spell on a stack of items."`
  - `L"This spell cannot be cast on %s"`.

### Wire drivers

- **`GameEventMagicUpdateSpell` (0x02C1)** — adds a known spell to the spellbook.
- **`GameEventMagicRemoveSpell` (0x01A8)** — removes.
- **`GameEventMagicUpdateEnchantment` (0x02C2)** — active enchantment on player (for the spell-bar enchantment overlay).
- **`GameEventMagicRemoveEnchantment` (0x02C3)** / **Multiple** (0x02C5) / **Purge** (0x02C6) / **Dispel** (0x02C7–8) / **PurgeBad** (0x0312).
- **`GameAction` CastUnTargetedSpell / CastTargetedSpell** (outbound) — triggers the cast.

### C# port sketch — `SpellBarPanel`

```csharp
public enum SpellSchool { Life, CreatureEnchantment, ItemEnchantment, War, Portal, Slot5, Slot6, Void }
public const int SlotsPerTab = 7;

public sealed class SpellBarPanel : IPanel
{
    public readonly Dictionary<SpellSchool, uint?[]> Slots = new();
    public SpellSchool ActiveTab { get; set; }
    public int SelectedSlot { get; set; }
    public uint? ReadySpellId { get; private set; }
    public uint? ReadySpellContainerId { get; private set; }

    public SpellBarPanel()
    {
        foreach (var s in Enum.GetValues<SpellSchool>())
            Slots[s] = new uint?[SlotsPerTab];
    }

    public void OnLearnedSpell(uint spellId, SpellSchool school);
    public void OnUnlearnedSpell(uint spellId);
    public void AssignToSlot(SpellSchool tab, int slot, uint spellId);

    public bool TryCast(uint? targetId, out string error);  // emits "Select a spell to cast" etc.
}

public sealed class SpellbookPanel : IPanel
{
    // Full known-spell list (filterable by school / level)
    public readonly HashSet<uint> KnownSpells = new();
    public void OnUpdateSpell(uint spellId) => KnownSpells.Add(spellId);
    public void OnRemoveSpell(uint spellId) => KnownSpells.Remove(spellId);
}

public sealed class EnchantmentPanel : IPanel
{
    // Active buffs/debuffs with countdown
    public readonly Dictionary<uint, Enchantment> Active = new();
    public void OnAdd(Enchantment e);
    public void OnRemove(uint spellId);
    public void OnPurgeAll();
    public void OnPurgeBadOnly();
}
```

## 5. Paperdoll / Equipment panel

**Location**: `chunk_004A0000.c::FUN_004a5200` is the slot-router that emits the correct "Drag X here to wear them" tooltip and handles drag-drop assignment. Slot widget pointers are stored at fixed offsets from the panel `this` pointer (`param_1`).

### Slot table (derived from `FUN_004a5200` offsets)

| Offset | Slot | Empty-tooltip string | EquipMask flag |
|--------|------|----------------------|----------------|
| `+0x604` | Necklace | `L"Drag necklaces here to wear them"` | `NeckWear 0x00008000` |
| `+0x608` | Left Bracelet | `L"Drag bracelets here to wear them"` | `WristWearLeft 0x00010000` |
| `+0x610` | Right Bracelet | `L"Drag bracelets here to wear them"` | `WristWearRight 0x00020000` |
| `+0x60C` | Left Ring | `L"Drag rings here to wear them"` | `FingerWearLeft 0x00040000` |
| `+0x614` | Right Ring | `L"Drag rings here to wear them"` | `FingerWearRight 0x00080000` |
| `+0x618` | Weapon | `L"Drag weapons here to wield them"` | `MeleeWeapon 0x00100000` |
| `+0x61C` | Missile Ammo | `L"Drag missile ammunition here to wield it"` | `MissileAmmo 0x00800000` |
| `+0x620` | Shield | `L"Drag shields here to wield them"` | `Shield 0x00200000` |
| `+0x624` | Clothing (Shirt) | `L"Drag clothing items here to wear them"` | `ChestWear 0x00000002` |
| `+0x628` | Clothing (Pants) | `L"Drag clothing items here to wear them"` | `AbdomenWear 0x00000004` |
| `+0x62C` | Trinket | `L"Drag trinkets here to activate them"` | `TrinketOne 0x04000000` |
| `+0x630` | Cloak | `L"Drag cloaks here to activate them"` | `Cloak 0x08000000` |
| `+0x634` | Aetheria Blue (Sigil1 — Lyr) | `L"Drag a Blue Aetheria sigil here to activate it"` | `SigilOne 0x10000000` |
| `+0x638` | Aetheria Yellow (Sigil2 — Kor) | `L"Drag a Yellow Aetheria sigil here to activate it"` | `SigilTwo 0x20000000` |
| `+0x63C` | Aetheria Red (Sigil3 — Tem) | `L"Drag a Red Aetheria sigil here to activate it"` | `SigilThree 0x40000000` |
| `+0x640` | Head | `L"Drag head items here to wear them"` | `HeadWear 0x00000001` |
| `+0x644` | Chest Armor | `L"Drag chest items here to wear them"` | `ChestArmor 0x00000200` |
| `+0x648` | Abdomen Armor | `L"Drag abdomen items here to wear them"` | `AbdomenArmor 0x00000400` |
| `+0x64C` | Upper Arm Armor | `L"Drag upper arm items here to wear them"` | `UpperArmArmor 0x00000800` |
| `+0x650` | Lower Arm Armor | `L"Drag lower arm items here to wear them"` | `LowerArmArmor 0x00001000` |
| `+0x654` | Glove Armor | `L"Drag glove items here to wear them"` | `HandWear 0x00000020` |
| `+0x658` | Upper Leg Armor | `L"Drag upper leg items here to wear them"` | `UpperLegArmor 0x00002000` |
| `+0x65C` | Lower Leg Armor | `L"Drag lower leg items here to wear them"` | `LowerLegArmor 0x00004000` |
| `+0x660` | Foot Armor | `L"Drag foot coverings here to wear them"` | `FootWear 0x00000100` |
| `+0x664` | (cross-slot state) | — | — |

Additional layout widgets around `+0x668` (toggle to armor-view), `+0x674` (toggle to clothing-view), `+0x670` (paperdoll 3D model preview), `+0x678` (sidebar with stats).

### Armor/Clothing view toggle

`FUN_004a5fa0` at line 2660: the paperdoll has a mode toggle between "clothing view" (shows shirt/pants/jewelry/weapons) and "armor view" (shows all 10 armor slots). The mode is set by event `0x100005be` (button id). When toggled, slot widget visibility is flipped via `(**)(slotWidget + 0x18))(showFlag)` calls — classic "hide 8 widgets, show 10 widgets" pattern.

### Drag-drop behavior

- Drop onto an empty slot: tooltip cycles through the "Drag X here to wear them" messages.
- Drop onto a filled slot OR double-click on the paperdoll slot: emits "(item name) (%s)\nDouble-click to %s" where the inserted strings are either ("worn", "take off") for armor/clothing or ("wielded", "unwield") for weapons/shield (line 2619-2624 branch `bVar1`).
- Failed drag (wrong item type for slot): `L"You can\'t put that item there"` via `FUN_004a4de0`.
- Error branches come from the server: `L"You're already wearing a helm."`, `L"You're already wearing chest armor."`, …, and similar for every body region (chunk_00560000.c line 1199-1394).

### 3D paperdoll model preview

`FUN_004a4c70` (starting line ~2150) — loads a preview scene at widget `+0x68c` (a rendered 3D object overlay), placing a scaled copy of the character's visual (ObjDesc) for live armor/weapon visualization. When an item is dragged onto a slot, the server sends an updated `GameMessageObjDescEvent` which triggers a repaint here.

### Wire drivers

- **`GameMessageObjDescEvent` (0xF625)** — full object-description (clothing base + palette swatches + hair) refresh. Any slot change triggers this on every nearby player.
- **`GameEventWieldObject` (0x0023)** — confirms that an equip request succeeded. Contains the item's guid + new wield location.
- **`GameEventInventoryPutObjInContainer` (0x0022)** — inverse: item moved back to backpack.
- **`GameActionPutItemInContainer` / `GameActionWieldItem`** — outbound equip/unequip requests.

### C# port sketch — `PaperdollPanel`

```csharp
public sealed class PaperdollPanel : IPanel
{
    // 25 slots, keyed by EquipMask bit
    public readonly Dictionary<EquipMask, uint?> Equipment = new();

    // The visual 3D model
    public ObjDescSnapshot PaperdollObjDesc { get; set; }

    public PaperdollViewMode View { get; set; } = PaperdollViewMode.Clothing;

    public void OnWieldObject(uint itemGuid, EquipMask location);
    public void OnUnwieldObject(uint itemGuid);
    public void OnObjDescUpdate(ObjDescSnapshot desc);

    public bool TryDragEquip(uint itemGuid, EquipMask targetSlot, out string errorString);
    public bool TryDoubleClickUnequip(EquipMask slot);
}

public enum PaperdollViewMode { Clothing, Armor }
```

The slot table MUST match the offsets above exactly when porting drag-drop dispatch — the retail client dispatches via offset comparisons, not enum values.

## 6. Inventory panel

The inventory panel is heavily intertwined with the paperdoll (same window in retail) and with containers (sub-windows when you open a pack). Inventory code is scattered across `chunk_004B0000.c` (container list renderer), `chunk_004E0000.c` (container placement), and `chunk_00580000.c` (item-use and pickup dispatch).

### Layout

- **Side tabs**: Main Pack + 6 numbered Side Packs (up to 7 packs total, each up to 24 items). Main pack = character's base container.
- **Grid**: fixed-size icon grid inside each pack. Items beyond visible range use a scroll-arrow or "Page X" header (see `L"Page %d"` format at chunk_004B0000.c lines 2881/2903).
- **Footer**: Burden / capacity text. Burden status strings come from `chunk_00560000.c`: `L"You are encumbered!"` (line 157), `L"You are severely encumbered!"` (163).

### Key interactions (from `chunk_00580000.c` dispatch)

- **Right-click**: context menu with Use / Examine / Split / Drop / Give options.
- **Double-click**: `GameActionUseItem` (equips or activates).
- **Left-click drag**: move between slots. If cross-container, sends `GameActionPutItemInContainer`.
- **Drag to paperdoll**: equip.
- **Drag to ground**: drop (`GameActionDropItem`).
- **Drag to NPC**: give (`GameActionGiveObjectRequest`).
- **Shift-click on stack**: split (`GameActionStackableSplit`).

### Error strings (partial)

- `L"You can't pick that up!"`, `L"You are too encumbered to carry that!"`, `L"You cannot pick up more of that item!"` (chunk_00570000 line 1849-3018).
- `L"You must first pick up the %s"`, `L"The %s is locked"`, `L"You must open the %s first"`, `L"Cannot give %s to %s"`, `L"Move cancelled"`, `L"You cannot do that in mid air"`.
- `L"The destination stack is already full."`, `L"You cannot merge different types of items."`, `L"You cannot merge items while they are being traded."`.
- `L"Cannot place container in item list"`, `L"Cannot place item in container list"`, `L"Already attempting to place %s here"`, `L"The %s cannot accept items"`.
- `L"You are not carrying the %s"` (line 4826 in chunk_004B0000).

### Stack / split

The split UI uses a numeric entry dialog. Split-related errors:
- `L"Cannot split the stack to sell it"`, `L"Cannot split the stack for dwelling costs"`, `L"The %s can't be split"`, `L"You must split the stack before selling it."`.

### Wire drivers

- **`GameMessageCreateObject` (0xF745)** — the universal "object exists, here's its data" — inventory items arrive through this.
- **`GameEventInventoryPutObjInContainer` (0x0022)**, **`GameEventItemServerSaysContainId`**, **`GameEventItemServerSaysMoveItem`** — confirm server-side state after an inventory action.
- **`GameMessageSetStackSize` (0x0197)** — stack merged/split result.
- **`GameMessageInventoryRemoveObject` (0x0024)** — item deleted.
- **`GameMessagePickupEvent` (0xF74A)** — outbound, client picking up a ground object.
- **`GameEventViewContents` (0x0196)** — sent when client opens a container (includes full item list).

### C# port sketch — `InventoryPanel`

```csharp
public sealed class InventoryItem
{
    public uint Guid;
    public uint IconResourceId;
    public string Name;
    public uint StackSize;
    public uint Burden;
    public uint Value;
    public EquipMask ValidLocations;
    public ItemType Type;
    public uint ContainerGuid;       // who holds us
    public int PlacementIndex;
    public IsEquipped Equipped;
}

public sealed class InventoryPanel : IPanel
{
    public uint CharacterGuid { get; }
    public readonly Dictionary<uint, InventoryItem> Items = new();   // by GUID
    public readonly List<uint> OpenContainerStack = new();            // Main + side packs currently viewed

    public int SelectedTab { get; set; }
    public int BurdenCurrent { get; private set; }
    public int BurdenMax { get; private set; }
    public BurdenStatus Status { get; private set; }  // Fine, Encumbered, SeverelyEncumbered

    public void OnCreateObject(InventoryItem item);
    public void OnRemoveObject(uint guid);
    public void OnStackSize(uint guid, uint newSize);
    public void OnPutInContainer(uint itemGuid, uint newContainer, int placement);
    public void OnViewContents(uint containerGuid, InventoryItem[] contents);

    public void RaiseMoveItem(uint guid, uint destContainer, int placement);
    public void RaiseSplitItem(uint guid, uint newSize);
    public void RaiseDropItem(uint guid);
    public void RaiseGiveItem(uint guid, uint targetGuid);
}

public enum BurdenStatus { Fine, Encumbered, SeverelyEncumbered }
```

## 7. Quickbar

The retail client has a 10-slot quickbar bound to **1–0 keys**, visible along the bottom-center of the screen. The decompiled binding code resides near the main-window input dispatch; I did not find the exact function address during this sweep (would be in a chunk around `0x004D0000` given the `"ID_InputMap_*"` strings in `chunk_004D0000.c` line 4808, e.g. `"ID_InputMap_CameraAlternateControls"`).

### Behaviors

- Slots accept any item from inventory or any spell from the spellbar.
- On slot-press: if item → `GameActionUseItem(guid)`; if spell → equivalent of double-clicking that spell slot.
- Shift+number rearranges; ctrl+number clears. (Standard AC layout.)

### C# port sketch — `Quickbar`

```csharp
public enum QuickbarAction { None, UseItem, CastSpell, PlayMacro }

public sealed class QuickbarSlot
{
    public QuickbarAction Action;
    public uint Target;   // item-guid, spell-id, or macro-id
    public uint IconId;
}

public sealed class Quickbar : IPanel
{
    public readonly QuickbarSlot[] Slots = new QuickbarSlot[10];
    public int ActiveBar { get; set; }           // retail has only one, but plan for plugin expansion
    public void Press(int slotIndex);            // resolve action → dispatch through Inventory/SpellBar
    public void Assign(int slotIndex, QuickbarSlot slot);
}
```

## 8. Allegiance panel

**Location**: rendering lives in ACE code as an `AllegianceProfile` struct sent inside `GameEventAllegianceUpdate (0x0020)`; the client side is in a chunk I did not fully decompile in this sweep (would be around `0x004B`/`0x004C`).

### Content rendered

- **Monarch** name + title.
- **Patron** (your direct superior) — may be null.
- **Vassals** — list of direct reports with level, gender, online/offline.
- Rank (your depth in the tree), allegiance name, MOTD (if set), officer level.
- Recruited count, percentage breakdown.
- Bans and officer list (if you're monarch).

### Events / strings

- `L"You're already sworn your Allegiance"` / `L"You are not in an allegiance!"` (chunk_00570000 lines 1527/1541).
- `L"No patron from which to break!"`, `L"Your Allegiance has been dissolved!"`.
- `L"You have been teleported too recently!"` (recall-related).
- `L"That is an invalid officer level."`.
- `L"Your allegiance is currently: %s."` / `L"Your allegiance is now: %s."`.
- `L"Your allegiance name has been cleared."`.
- `L"Banned Characters: "` (list header).
- `L"You are banned from %s's allegiance!"`.
- `L"Allegiance information for %hs%s:\n"` (chunk_00560000 line 7245 — the `@allegiance info` dump).

### Wire drivers

- **`GameEventAllegianceUpdate` (0x0020)** — full allegiance tree + rank + profile.
- **`GameEventAllegianceLoginNotification` (0x027A)** — "Vassal X is online" popups.
- **`GameEventAllegianceInfoResponse` (0x027C)** — response to `@allegiance info`.
- **`GameEventAllegianceAllegianceUpdateDone` (0x01C8)** — action-complete marker.
- **`GameEventAllegianceUpdateAborted` (0x0003)**.

### C# port sketch — `AllegiancePanel`

```csharp
public sealed class AllegianceMember
{
    public uint Guid;
    public string Name;
    public int Level;
    public Gender Gender;
    public HeritageGroup Heritage;
    public bool Online;
    public uint PatronGuid;
    public int Rank;           // depth
    public AllegianceOfficerLevel OfficerLevel;
    public List<AllegianceMember> Vassals = new();
}

public sealed class AllegiancePanel : IPanel
{
    public AllegianceMember Monarch;
    public AllegianceMember Patron;
    public List<AllegianceMember> Vassals;
    public string AllegianceName;
    public string AllegianceMotd;
    public uint MyRank;
    public AllegianceOfficerLevel MyOfficerLevel;
    public void OnAllegianceUpdate(AllegianceMember root, AllegianceMember self);
    public void OnLoginNotification(string name, bool online);
}
```

## 9. Fellowship panel

### Content rendered (from `GameEventFellowshipFullUpdate 0x02BE`)

Each fellow line includes:

- **Name** (string16L).
- **Level**.
- **Health/Stamina/Mana** max + current (shown as bars).
- **cpCached / lumCached** (undistributed exp and luminance shares).
- **ShareLoot flag** (0 or 0x10).

Fellowship-level fields:

- FellowshipName (up to 256 chars).
- FellowshipLeaderGuid.
- ShareXP / EvenShare / Open flags.
- IsLocked flag.
- DepartedMembers list (with cooldown timers).
- FellowshipLocks (post-ToD locks).

### Interaction

- Leader can recruit (drag player onto fellowship UI or `@fellow recruit PlayerName`).
- Leader can dismiss (`@fellow dismiss name`).
- Any member can quit (`@fellow quit`).
- Leader can disband the fellowship.

### Strings observed

- `L"You are unprepared to cast a spell"` (unrelated but appears in same switch).
- `L"You must be the leader of a Fellowship"`.
- `L"Your Fellowship is full"`.
- `L"That Fellowship name is not permitted"`.
- `L"You do not belong to a Fellowship."`.
- `L"This fellowship is locked; "` + name + `" cannot be recruited into the fellowship."` (line 2543).
- `L"The fellowship is locked, you were not added to the fellowship."`.
- `L"The fellowship is locked; you cannot open locked fellowships."`.

### Wire drivers

- **`GameEventFellowshipFullUpdate` (0x02BE)**.
- **`GameEventFellowshipUpdateFellow` (0x02C0)** — per-fellow incremental.
- **`GameEventFellowshipDisband` (0x02BF)**.
- **`GameEventFellowshipQuit` (0x00A3)**.
- **`GameEventFellowshipDismiss` (0x00A4)**.
- **`GameEventFellowshipFellowUpdateDone` (0x01C9)** / **`FellowshipFellowStatsDone` (0x01CA)**.

### C# port sketch — `FellowshipPanel`

```csharp
public sealed class Fellow
{
    public uint Guid;
    public string Name;
    public int Level;
    public uint MaxHealth, MaxStamina, MaxMana;
    public uint CurrentHealth, CurrentStamina, CurrentMana;
    public uint CpCached;
    public uint LumCached;
    public bool ShareLoot;
}

public sealed class FellowshipPanel : IPanel
{
    public string FellowshipName;
    public uint LeaderGuid;
    public bool ShareXP, EvenShare, Open, IsLocked;
    public readonly Dictionary<uint, Fellow> Members = new();
    public readonly Dictionary<uint, Fellow> Departed = new();

    public bool IAmLeader(uint myGuid) => myGuid == LeaderGuid;

    public void OnFullUpdate(FellowshipSnapshot snap);
    public void OnUpdateFellow(Fellow f);
    public void OnDisband();
    public void RaiseRecruit(string name);
    public void RaiseDismiss(uint guid);
    public void RaiseQuit();
    public void RaiseToggleShareXP();
}
```

## 10. Macros panel

Retail macros were stored client-side in `~/Documents/Asheron's Call/settings/(character).ppf` or equivalent, not sent through the wire protocol. The decompiled code I scanned did not contain a dedicated macro UI function in the chunks explicitly assigned here; it lives in a higher chunk (around `0x005D`–`0x005F` based on the config file saver strings).

### Behaviors (from in-game retail memory)

- `@save name` and `@load name` for macros (`L"Please use @help saveui for proper usage."` and `L"Please use @help loadui for proper usage."` are the help prompts, chunk_00570000 lines 208/270).
- A macro is a short script of actions: text chats, cast spell, use item, delay N seconds.
- Bound to quickbar slots or standalone hotkeys.
- Also included "Automation" — auto-select nearest monster and attack.

### C# port sketch — `MacrosPanel`

This is one of the panels where acdream's plugin API becomes crucial (the "scripting/macros" requirement in CLAUDE.md).

```csharp
public enum MacroStepKind { Chat, CastSpell, UseItem, Delay, Wait, Attack, Emote, Custom }

public sealed class MacroStep
{
    public MacroStepKind Kind;
    public string Text;        // for Chat/Emote
    public uint TargetId;      // for CastSpell/UseItem
    public TimeSpan Duration;  // for Delay/Wait
    public string CustomId;    // for plugin-provided step
}

public sealed class Macro
{
    public string Name;
    public int HotkeyIndex;
    public List<MacroStep> Steps = new();
}

public sealed class MacrosPanel : IPanel
{
    public readonly Dictionary<string, Macro> Macros = new();
    public void Save(Macro m);
    public void Delete(string name);
    public void Execute(string name, IPluginHost host);  // dispatches through plugin API
}
```

## 11. Options panel

**Location**: the options panel writes into `GameActionSetSingleCharacterOption` (see `CharacterOption.cs`). The rendering chunk is around `0x005D`–`0x005F` (config / saved-UI layout chunks), not fully scanned here.

### Options available

From `CharacterOption.cs`, 55 named options split across **CharacterOptions1** (gameplay) and **CharacterOptions2** (display / filters). Some highlights:

| Option | Bit group | Description |
|--------|-----------|-------------|
| AutoRepeatAttacks | 1 | Auto-attack |
| IgnoreAllegianceRequests | 1 | |
| IgnoreFellowshipRequests | 1 | |
| IgnoreAllTradeRequests | 1 | |
| DisableMostWeatherEffects | 1 | Graphics |
| AlwaysDaylightOutdoors | 2 | Graphics |
| LetOtherPlayersGiveYouItems | 1 | |
| KeepCombatTargetsInView | 1 | |
| Display3dTooltips | 1 | |
| AttemptToDeceiveOtherPlayers | 1 | |
| RunAsDefaultMovement | 1 | |
| StayInChatModeAfterSendingMessage | 1 | |
| AdvancedCombatInterface | 1 | |
| AutoTarget | 1 | |
| VividTargetingIndicator | 1 | |
| ShareFellowshipExpAndLuminance | 1 | |
| AcceptCorpseLootingPermissions | 1 | |
| ShareFellowshipLoot | 1 | |
| AutomaticallyAcceptFellowshipRequests | 1 | |
| SideBySideVitals | 1 | UI layout |
| ShowCoordinatesByTheRadar | 1 | |
| DisplaySpellDurations | 1 | |
| DisableHouseRestrictionEffects | 1 | |
| DragItemToPlayerOpensTrade | 1 | |
| ShowAllegianceLogons | 1 | |
| UseChargeAttack | 1 | |
| UseCraftingChanceOfSuccessDialog | 1 | |
| ListenToAllegianceChat | 1 | |
| AllowOthersToSeeYourDateOfBirth | 2 | |
| AllowOthersToSeeYourAge | 2 | |
| AllowOthersToSeeYourChessRank | 2 | |
| AllowOthersToSeeYourFishingSkill | 2 | |
| AllowOthersToSeeYourNumberOfDeaths | 2 | |
| DisplayTimestamps | 2 | |
| SalvageMultipleMaterialsAtOnce | 2 | |
| ListenToGeneralChat | 2 | |
| ListenToTradeChat | 2 | |
| ListenToLFGChat | 2 | |
| ListenToRoleplayChat | 2 | |
| AppearOffline | 2 | |
| AllowOthersToSeeYourNumberOfTitles | 2 | |
| UseMainPackAsDefaultForPickingUpItems | 2 | |
| LeadMissileTargets | 2 | |
| UseFastMissiles | 2 | |
| FilterLanguage | 2 | |
| ConfirmUseOfRareGems | 2 | |
| ListenToSocietyChat | 2 | |
| ShowYourHelmOrHeadGear | 2 | |
| DisableDistanceFog | 2 | Graphics |
| UseMouseTurning | 2 | |
| ShowYourCloak | 2 | |
| LockUI | 2 | |
| ListenToPKDeathMessages | 2 | |

### Additional graphics/audio options (retail, not in protocol — client-local)

- Resolution + windowed/fullscreen.
- Draw distance (both landblock chunks loaded and scenery density).
- Texture detail / vertex lighting / environment lighting toggles.
- Gamma / contrast.
- Master volume, music volume, speech volume, effects volume.
- Radar color theme, UI scale.

### Wire drivers

- **`GameActionSetSingleCharacterOption`** outbound on toggle.
- **`GameActionSetCharacterOption1Flag` / `Option2Flag`** bulk.
- The server persists and replays at login via `PlayerDescription`.

### C# port sketch — `OptionsPanel`

```csharp
public sealed class OptionsPanel : IPanel
{
    public readonly Dictionary<CharacterOption, bool> Options = new();

    public GraphicsOptions Graphics { get; set; }   // client-local
    public AudioOptions Audio { get; set; }         // client-local

    public bool Get(CharacterOption opt) => Options.GetValueOrDefault(opt, false);
    public void Set(CharacterOption opt, bool value);   // writes back to server + persists locally
    public void LoadFromDescription(uint options1, uint options2);
}
```

## 12. Login screen / server select

### Login screen layout (from retail behavior)

- Title banner (AC logo).
- Username field (masked).
- Password field (masked).
- Server selection dropdown (loaded from a `ServerList.ini`-style file).
- Play / Create Account / Exit buttons.
- Error strings: varying dialogs surface rejections from `GameMessageAccountBanned (0xF7C1)`, `CharacterError (0xF659)`.

### Wire drivers

- Initial **LoginRequest** → **ConnectRequest** → **ConnectResponse** (UDP handshake, see `docs/research/2026-04-12-movement-deep-dive.md` and `holtburger` session code).
- **`GameMessageCharacterList` (0xF658)** arrives after encrypted handshake — contains the account's 11-slot character list.
- **`GameMessageServerName` (0xF7E1)** — displayed at top of character select.
- **`GameMessageAccountBanned` (0xF7C1)** + **`GameMessageBootAccount`** — rejection paths.

### C# port sketch — `LoginScreen`

```csharp
public sealed class ServerEntry
{
    public string Name;
    public string Host;
    public int Port;
    public int Population;
    public bool Online;
}

public sealed class LoginScreen : IPanel
{
    public List<ServerEntry> Servers;
    public ServerEntry SelectedServer { get; set; }
    public string Username { get; set; }
    public string PasswordSecure { get; set; }  // kept in pinned SecureString

    public event Action<LoginAttempt> OnSubmit;
    public void ShowError(string message);
}
```

## 13. Character select screen

### Layout

- Slot-list on left: 11 character slots (paid accounts), 1 free character always unlocked.
- 3D preview of selected character rotating on a pedestal (server sends appearance + equipped ObjDesc).
- Play / Create / Delete buttons.
- Server name at top.

### Wire drivers

- **`GameMessageCharacterList` (0xF658)** — the list.
- **`GameMessageCharacterError` (0xF659)** — generic error (name taken, account suspended, etc.).
- **`GameMessageCharacterDelete` (0xF655)** outbound.
- **`GameMessageCharacterCreate` (0xF656)** outbound.
- **`GameMessageCharacterCreateResponse` (0xF643)** server's answer.
- **`GameMessageCharacterEnterWorldRequest` (0xF7C8)** outbound when Play clicked.
- **`GameMessageCharacterEnterWorldServerReady` (0xF7DF)** when world server takes over.
- **`GameMessageCharacterRestore` (0xF7D9)** used for restoring deleted characters within the 1-hour grace window.

### Strings seen

From chunk_00470000 line 8280-8315 — this is the CREATION screen render, NOT select:
- `L"Gender: "`
- `L"Heritage: "`
- `L"Starting Town: "`
- Then the attribute preview as documented in panel #2.

### C# port sketch — `CharacterSelectScreen`

```csharp
public sealed class CharacterSlot
{
    public uint Guid;
    public string Name;
    public int Level;
    public int SecondsRemainingToDelete;  // for post-delete grace period
    public ObjDescSnapshot Appearance;
    public bool IsFreeSlot;
}

public sealed class CharacterSelectScreen : IPanel
{
    public string ServerName;
    public List<CharacterSlot> Slots;
    public CharacterSlot Selected;

    public event Action<uint> OnPlay;
    public event Action<uint> OnDelete;
    public event Action OnCreateNew;

    public void OnCharacterList(CharacterSlot[] slots);
    public void OnError(string errorMessage);
}
```

## Cross-panel patterns for acdream

Based on the decompilation sweep above, the client uses a small set of recurring abstractions. When porting, implement these once and reuse:

1. **`IPanel`** interface with `Bind()` / `Unbind()` / `OnPacketReceived()` hooks. Every panel registers to its relevant `GameMessageOpcode` / `GameEventType` values; the dispatcher routes to them.
2. **`WidgetLookup`** — resource-ID → widget pointer. Mirrors retail's `FUN_00463c00` pattern. Each panel stores its child widgets in a record type with named fields.
3. **`RichTextBuilder`** — builds formatted rows of `[label][tab][value]\n` with style tokens. Replaces the `FUN_0040b8f0 / FUN_0046f670 / FUN_00402490` trio.
4. **`TooltipBuilder`** — sequence `Open → Emit → Close → Anchor`. Replaces `FUN_0042dc80 / FUN_0040b8f0 / FUN_0042cbe0 / FUN_004618a0 / FUN_0042e590`.
5. **`DragDropRouter`** — single entry point for pick/drop events with a slot-kind dispatch table. The paperdoll's 25-slot switch and inventory grid both route through this.
6. **`RowList<T>`** (for skills, spells, fellows, allegiance vassals) — generic panel-internal renderer that iterates a filtered collection, emits one row per item, supports sorting/scrolling.
7. **`PropertyUpdateBus`** — the `PrivateUpdateProperty{Int,Int64,Bool,Float,String,DataID}` family of 0x02CD–0x02D8 messages all go through one bus that panels subscribe to by `PropertyId`. This is cleaner than 6 separate wiring paths.

## Summary table — panel ↔ chunk ↔ wire message

| Panel | Primary chunk / function | Main wire input |
|-------|--------------------------|-----------------|
| Chat window | `chunk_00570000.c` big switch | `ChannelBroadcast 0x0147`, `Tell 0x02BD`, `SystemChat 0xF7E0`, `HearSpeech 0x02BB`, `TurbineChat 0xF7DE` |
| Character Attributes | `chunk_00470000.c::FUN_0047ba70` + `chunk_005C0000.c::FUN_005c9d70/e10/eb0` | `PrivateUpdateAttribute 0x02E3`, `PrivateUpdateVital 0x02E7`, `PlayerDescription 0x0013` |
| Skills | shared with Attributes | `PrivateUpdateSkill 0x02DD`, `PrivateUpdateSkillLevel 0x02DF` |
| Spell bar | `chunk_004C0000.c::FUN_004c68f0 + 004c6500/6680/7c50` | `MagicUpdateSpell 0x02C1`, `MagicRemoveSpell 0x01A8`, enchantment events 0x02C2-8 |
| Paperdoll | `chunk_004A0000.c::FUN_004a5200 + 004a5fa0` | `ObjDescEvent 0xF625`, `WieldObject 0x0023`, `InventoryPutObjInContainer 0x0022` |
| Inventory | `chunk_004B0000.c` + `chunk_004E0000.c` + `chunk_00580000.c` | `ObjectCreate 0xF745`, `SetStackSize 0x0197`, `InventoryRemoveObject 0x0024`, `ViewContents 0x0196` |
| Quickbar | around `chunk_004D0000.c` (input map) | client-local (actions dispatch back to inventory/spell) |
| Allegiance | not scanned here | `AllegianceUpdate 0x0020`, `AllegianceInfoResponse 0x027C`, `AllegianceLoginNotification 0x027A` |
| Fellowship | not scanned here | `FellowshipFullUpdate 0x02BE`, `FellowshipUpdateFellow 0x02C0`, `FellowshipDisband 0x02BF` |
| Macros | not scanned here | client-local |
| Options | not fully scanned | `GameActionSetSingleCharacterOption` outbound |
| Login | pre-world chunks | UDP handshake + `ServerName 0xF7E1` |
| Character select | post-handshake pre-world | `CharacterList 0xF658`, `CharacterError 0xF659` |

### What was NOT fully mapped in this sweep

Honest gaps to flag for the next research pass:

- **Macros panel** — needs a scan of `0x005D`–`0x005F` chunks for the config save/load handlers and macro execution tokenizer.
- **Options panel rendering** — needs a scan for the actual layout function (where the 55 character options get paired with checkboxes + sliders).
- **Allegiance tree widget** — the actual tree-layout widget with expand/collapse arrows; it's probably in a chunk adjacent to fellowship.
- **Fellowship member-list widget** — same; only the data side is in ACE. The render function is in retail's client and I did not find its address in this sweep.
- **Quickbar widget class offsets** — known it exists from input-map strings but the widget-storage offsets were not identified.
- **Chat window widget IDs** — the tab-button resource IDs, tab-filter storage offsets, and input-box widget ID need a dedicated pass through the chunk that owns `chat_window` or similar identifier.

These gaps are documented here so a follow-up research task can pick them up rather than re-discover them.
