Deep investigation of the retail AC client's GUI subsystem, driven by 6
parallel Opus research agents, plus the first cut of a retail-faithful
retained-mode widget toolkit that scaffolds Phase D.
Research (docs/research/retail-ui/):
- 00-master-synthesis.md — cross-slice synthesis + port plan
- 01-architecture-and-init.md — WinMain, CreateMainWindow, frame loop,
Keystone bring-up (7 globals mapped)
- 02-class-hierarchy.md — key finding: UI lives in keystone.dll,
not acclient.exe; CUIManager + CUIListener
MI pattern, CFont + CSurface + CString
- 03-rendering.md — 24-byte XYZRHW+UV verts, per-font
256x256 atlas baked from RenderSurface,
TEXTUREFACTOR coloring, DrawPrimitiveUP
- 04-input-events.md — Win32 WndProc → Device (DAT_00837ff4)
→ widget OnEvent(+0x128); full event-type
table (0x01 click, 0x07 tooltip ~1000ms,
0x15 drag-begin, 0x21 enter, 0x3E drop)
- 05-panels.md — chat, attributes, skills, spells, paperdoll
(25-slot layout), inventory, fellowship,
allegiance — with wire-message bindings
- 06-hud-and-assets.md — vital orbs (scissor fill), radar
(0x06001388/0x06004CC1, 1.18× shrink),
compass strip, dat asset catalog
Key insight: keystone.dll owns the actual widget toolkit — we cannot
port a class hierarchy from the decompile because it's not there.
Instead we implement our own retained-mode toolkit with retail-faithful
behavior (event codes, focus/modal/capture, drag-drop state machine)
and will consume the same portal.dat fonts + sprites so the visual
identity is preserved.
C# scaffold (src/AcDream.App/UI/):
- UiEvent — 24-byte event struct + retail event-type constants
(0x01 click, 0x15 drag-begin, 0x201 WM_LBUTTONDOWN,
etc.) matching retail decompile switches
- UiElement — base widget: children, ZOrder, focus/capture flags,
virtual OnDraw/OnEvent/OnHitTest/OnTick; children-
first hit test + back-to-front composite
- UiPanel — panel, label, button primitives
- UiRenderContext — 2D draw context with translate stack
- UiRoot — top-of-tree + Device responsibilities (mouse/
keyboard state, focus, modal, capture, drag-drop,
tooltip timer); WorldMouseFallThrough/
WorldKeyFallThrough preserves existing camera
controls when no widget consumes
- UiHost — packages UiRoot + TextRenderer + input wiring
helpers for one-line integration into GameWindow
- README.md — orientation for future agents
Roadmap (docs/plans/2026-04-11-roadmap.md):
- D.1 marked shipped (debug overlay from 2026-04-17)
- D.2 expanded to include the retail UI framework landed here
- D.3-D.7 added: AcFont, dat sprites, core panels, HUD, CursorManager
- D.8 remains sound
All existing 470 tests pass. 0 warnings, 0 errors.
52 KiB
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
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.GameEventTell(0xF7B0 / 0x02BD) —string16L messageText | string16L senderName | guid senderID | guid targetID | uint chatMessageType | uint extraZero.GameMessageSystemChat(0xF7E0) —string16L message | int chatMessageType. The main path for system notifications.GameMessageHearSpeech(0x02BB) —string16L messageText | string16L senderName | uint senderID | uint chatMessageType. Creature/player speech in the 3D world.GameMessageHearRangedSpeech(0x02BC) — same layout; broadcast to a wider radius (quest giver yell).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.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 (
/ssay,/t nametell,/ffellow,/aallegiance,@channelfor named channels, raw text = last-used mode). The client sendsGameActionChatChannelon enter. - Filters: Character Options (
ListenToAllegianceChat0x1B,ListenToGeneralChat0x23,ListenToTradeChat0x24,ListenToLFGChat0x25,ListenToRoleplayChat0x26,ListenToSocietyChat0x2E,ListenToPKDeathMessages0x34) 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"(case0x51b)L"You have left the %s channel.\n"(case0x51c)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"(case0x51f— 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
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:
- Gender (strings table at
PTR_DAT_0081a1c4— "Male" / "Female" / "Unknown"). - Heritage (strings at
PTR_DAT_0081a1d0, 5 entries — Aluvian, Gharu'ndim, Sho, Viamontian, Umbraen/Empyrean). - Starting Town (strings at
PTR_u_Holtburg_0081a1e4, 4 entries — Holtburg, Shoushi, Yaraq, Sanamar/Rithwic per heritage). - "Attributes" header (row break, layout resource
0x100000fe). - 10 attribute rows (loop
iVar10 < 10), each using formatter resource0x100002fc(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 viaFUN_005c4990(2)) - Case 7 — Stamina (
FUN_005c4990(2)→ returns stamina) - Case 8 — Mana (
FUN_005c4990(6)) - Case 9 — Skill Credits (value at
+0x1b8)
- Case 0 — Strength (value at
- 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)
- Case 0 — "Specialized Skills" (filter rank
- 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 ifcategory == 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
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
UsableWithoutTrainingproperty = true (e.g. Run, Jump, Loyalty) ANDAdvancementClass == Untrained. - For each skill row the decompile also looks up a hashtable at
iStack_4 + 8(keyed byskillId % 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
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_004c7c50fires. - Single-click just highlights via
FUN_006a9640and 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 viaFUN_00588350, and if valid formatsL"CAST %hs"+ optionalL" on %s"and sends viaFUN_005abb30. - Error paths:
L"Select a spell to cast"(no slot filled, but spells known) —FUN_00407e40tooltip 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).GameActionCastUnTargetedSpell / CastTargetedSpell (outbound) — triggers the cast.
C# port sketch — SpellBarPanel
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"viaFUN_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
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
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
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 infodump).
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
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
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 nameand@load namefor macros (L"Please use @help saveui for proper usage."andL"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).
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
GameActionSetSingleCharacterOptionoutbound on toggle.GameActionSetCharacterOption1Flag/Option2Flagbulk.- The server persists and replays at login via
PlayerDescription.
C# port sketch — OptionsPanel
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.mdandholtburgersession 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
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
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:
IPanelinterface withBind()/Unbind()/OnPacketReceived()hooks. Every panel registers to its relevantGameMessageOpcode/GameEventTypevalues; the dispatcher routes to them.WidgetLookup— resource-ID → widget pointer. Mirrors retail'sFUN_00463c00pattern. Each panel stores its child widgets in a record type with named fields.RichTextBuilder— builds formatted rows of[label][tab][value]\nwith style tokens. Replaces theFUN_0040b8f0 / FUN_0046f670 / FUN_00402490trio.TooltipBuilder— sequenceOpen → Emit → Close → Anchor. ReplacesFUN_0042dc80 / FUN_0040b8f0 / FUN_0042cbe0 / FUN_004618a0 / FUN_0042e590.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.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.PropertyUpdateBus— thePrivateUpdateProperty{Int,Int64,Bool,Float,String,DataID}family of 0x02CD–0x02D8 messages all go through one bus that panels subscribe to byPropertyId. 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–0x005Fchunks 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_windowor similar identifier.
These gaps are documented here so a follow-up research task can pick them up rather than re-discover them.