# 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 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 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]\nskill-namevalue`. - 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 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 Specialized => Skills.Values.Where(s => s.Class == SkillAdvancementClass.Specialized); public IEnumerable Trained => Skills.Values.Where(s => s.Class == SkillAdvancementClass.Trained); public IEnumerable 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 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()) 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 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 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 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 Items = new(); // by GUID public readonly List 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 Vassals = new(); } public sealed class AllegiancePanel : IPanel { public AllegianceMember Monarch; public AllegianceMember Patron; public List 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 Members = new(); public readonly Dictionary 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 Steps = new(); } public sealed class MacrosPanel : IPanel { public readonly Dictionary 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 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 Servers; public ServerEntry SelectedServer { get; set; } public string Username { get; set; } public string PasswordSecure { get; set; } // kept in pinned SecureString public event Action 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 Slots; public CharacterSlot Selected; public event Action OnPlay; public event Action 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`** (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.