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.
1062 lines
52 KiB
Markdown
1062 lines
52 KiB
Markdown
# Retail AC Client — UI Panels Reference
|
||
|
||
This document maps each major UI panel in the retail Asheron's Call client to its decompiled source in `acclient.exe`, the wire messages that drive it, and a C# port sketch for acdream. Data was cross-referenced against:
|
||
|
||
- Decompiled acclient chunks `0x004A0000`–`0x005C0000` (22,225 functions, 688K lines of C).
|
||
- `ACE.Server.Network.GameMessages` + `ACE.Server.Network.GameEvent.Events` (wire format).
|
||
- `ACE.Entity.Enum.*` (Channel, ChatType, ChatMessageType, EquipMask, CharacterOption).
|
||
|
||
## Common UI architecture observed in decompilation
|
||
|
||
Before drilling into individual panels, the decompiled code shows a consistent pattern. Every panel is a C++ object that lives at a `this`-pointer offset inside a parent "frame" object. The same helper functions recur across all panels:
|
||
|
||
| Address | Purpose |
|
||
|---------|---------|
|
||
| `FUN_00463c00(0x100000XX)` | Lookup widget by **resource ID** (globally-registered UI asset IDs). Returns widget pointer. |
|
||
| `FUN_0046a740(&localVar)` | Assign widget pointer with ref-counted intrusive reference. |
|
||
| `FUN_0040b8f0(L"literal")` | Emit a wide-string chunk into the current text-builder stream (for rich-text lines). |
|
||
| `FUN_00407e40(L"literal")` | Append wide-string to tooltip builder. |
|
||
| `FUN_00402490(L"literal", sVar)` | Append to richer formatted text with length. |
|
||
| `FUN_0046f670(a, b)` | Emit a formatting/delimiter token to a text-layout buffer (`(1,0)` = newline, `(2,0)` = tab, `(0,0)` = section). |
|
||
| `FUN_00463c00(0x100002f9)` / `FUN_00463c00(0x100002fc)` / `FUN_00463c00(0x100002fd)` | Text formatters for "field-name" / "value" / "highlight-value" column rendering. |
|
||
| `FUN_0042dc80()` / `FUN_0042cbe0(&hdl, 1)` / `FUN_0042e590()` | Tooltip open → content → close sequence. |
|
||
| `FUN_004618a0(auStack_90)` | Anchor tooltip to current widget. |
|
||
| `FUN_00460270(resourceId, &callback)` | Register a global event callback for a resource ID. |
|
||
| `FUN_0058f8e0/0058f8b0(&out, obj, kind, flag)` | Lookup weenie/object name string (kind 2 = article-prefixed name). |
|
||
|
||
**Widget class layout observation.** Most panels store their child-widget pointers at fixed offsets from the panel `this` pointer. E.g. the paperdoll panel at `param_1+0x604`, `0x608`, …, `0x664` holds ~25 widget pointers for 16+ slot icons plus container, tooltip overlay, and model-preview widgets. Knowing the offset pattern lets you correlate a panel's widget set purely from one bind-event function.
|
||
|
||
**Resource ID encoding.** Button/text/layout asset IDs use the magic prefix `0x10000000` plus a small integer (e.g. `0x100000ab` = Strength icon, `0x100000fe` = generic "attribute row" layout, `0x100002fc` = field value text style, `0x100005c3` = Skill Credits icon). These are indexed into the same dat table the dats use for UI layouts.
|
||
|
||
**Shared abstractions.** Panels that show lists of rows (skills, spell icons, inventory cells, fellowship members, allegiance tree) all use the same pattern: a parent container widget, a `FUN_0046f670(1,0)` row-break emission, then a per-row callback that writes the row's icon + label + value text. Panels with tabs (spells, chat) use a mode-byte stored in the panel's flag field (e.g. spell tab offsets `0x1c * tabIndex + 0x634`).
|
||
|
||
## 1. Chat window
|
||
|
||
**Location**: The chat input/handler is spread across multiple chunks; inbound routing lives in `chunk_00570000.c::FUN_00573XXX` (the big server-message switch). Channel enumeration comes from `GameEventType.ChannelBroadcast = 0x0147` (ACE mapping).
|
||
|
||
### Channels (Channel.cs bitmask)
|
||
|
||
Channels are a 32-bit bitmask, NOT a single enum value. The chat UI tracks which channels the player is subscribed to and routes incoming `ChannelBroadcast` events by bit.
|
||
|
||
| Channel | Bit | Command | Notes |
|
||
|---------|-----|---------|-------|
|
||
| Abuse | `0x00000001` | `@abuse` | |
|
||
| Admin | `0x00000002` | `@admin` / `@ad` | |
|
||
| Audit | `0x00000004` | `@audit` / `@au` | Echo of enforcement commands to admins |
|
||
| Advocate1-3 | `0x00000008` / `10` / `20` | `@advocate` | |
|
||
| QA1/QA2 | `0x40` / `0x80` | | |
|
||
| Debug | `0x00000100` | | |
|
||
| Sentinel | `0x00000200` | `@sent` | |
|
||
| Help | `0x00000400` | | |
|
||
| Fellow | `0x00000800` | `@f` | Fellowship |
|
||
| Vassals | `0x00001000` | `@v` | |
|
||
| Patron | `0x00002000` | `@p` | |
|
||
| Monarch | `0x00004000` | `@m` | |
|
||
| AlArqas | `0x00008000` | | Society town channels |
|
||
| Holtburg | `0x00010000` | | |
|
||
| Lytelthorpe | `0x00020000` | | |
|
||
| Nanto | `0x00040000` | | |
|
||
| Rithwic | `0x00080000` | | |
|
||
| Samsur | `0x00100000` | | |
|
||
| Shoushi | `0x00200000` | | |
|
||
| Yanshi | `0x00400000` | | |
|
||
| Yaraq | `0x00800000` | | |
|
||
| CoVassals | `0x01000000` | `@c` | |
|
||
| AllegianceBroadcast | `0x02000000` | `@a` | "Tell All Allegiance Members" |
|
||
| FellowBroadcast | `0x04000000` | | Leader-to-fellowship |
|
||
| SocietyCelHan | `0x08000000` | | Celestial Hand society |
|
||
| SocietyEldWeb | `0x10000000` | | Eldrytch Web |
|
||
| SocietyRadBlo | `0x20000000` | | Radiant Blood |
|
||
| Olthoi | `0x40000000` | | Olthoi-only |
|
||
|
||
### ChatType (high-level category sent with each message)
|
||
|
||
`ACE.Entity.Enum.ChatType`: Undef, Allegiance, General, Trade, LFG, Roleplay, Society, SocietyCelHan, SocietyEldWeb, SocietyRadBlo, Olthoi.
|
||
|
||
### ChatMessageType (color / filter / tab-routing)
|
||
|
||
A 32-bit enum used in almost every outbound UI message to colorize and categorize a line:
|
||
|
||
| Value | Name | Color/Behavior |
|
||
|-------|------|----------------|
|
||
| `0x00` | Broadcast | Default — shopkeepers, allegiance MOTD, crafting results, mana-stone messages |
|
||
| `0x01` | AllChannels | |
|
||
| `0x02` | Speech | "Name says, …" |
|
||
| `0x03` | Tell | Incoming /tell |
|
||
| `0x04` | OutgoingTell | "You tell …" |
|
||
| `0x05` | System | Red warning text |
|
||
| `0x06` | Combat | Damage lines |
|
||
| `0x07` | Magic | Enchantment applied/resisted |
|
||
| `0x08` | Channel | Light pink — @admin, @audit, @av1-3, @sent |
|
||
| `0x09` | ChannelSend | |
|
||
| `0x0A` | Social | Bright yellow |
|
||
| `0x0B` | SocialSend | Light yellow |
|
||
| `0x0C` | Emote | Creature emote text (via `HearSpeech`/`HearRangedSpeech`) |
|
||
| `0x0D` | Advancement | Level-up / skill-gain |
|
||
| `0x0E` | Abuse | Light cyan |
|
||
| `0x0F` | Help | Red — urgent-help echo |
|
||
| `0x10` | Appraisal | Assess failure |
|
||
| `0x11` | Spellcasting | Spell syllable text (`Malar Quaril`, etc) |
|
||
| `0x12` | Allegiance | Allegiance chat |
|
||
| `0x13` | Fellowship | Bright yellow fellowship chat |
|
||
| `0x14` | WorldBroadcast | Green |
|
||
| `0x15` | CombatEnemy | Red |
|
||
| `0x16` | CombatSelf | Pink |
|
||
| `0x17` | Recall | |
|
||
| `0x18` | Craft | |
|
||
| `0x19` | Salvaging | Green |
|
||
| `0x1F` | AdminTell | Bright yellow |
|
||
|
||
### Wire messages that drive the chat window
|
||
|
||
1. **`GameEventChannelBroadcast` (0xF7B0 / 0x0147)** — 12+ bytes: `uint chatChannel | string16L senderName | string16L messageText`. The sender name is empty when the server is echoing your own message back to you, triggering "You say…" rendering.
|
||
2. **`GameEventTell` (0xF7B0 / 0x02BD)** — `string16L messageText | string16L senderName | guid senderID | guid targetID | uint chatMessageType | uint extraZero`.
|
||
3. **`GameMessageSystemChat` (0xF7E0)** — `string16L message | int chatMessageType`. The main path for system notifications.
|
||
4. **`GameMessageHearSpeech` (0x02BB)** — `string16L messageText | string16L senderName | uint senderID | uint chatMessageType`. Creature/player speech in the 3D world.
|
||
5. **`GameMessageHearRangedSpeech` (0x02BC)** — same layout; broadcast to a wider radius (quest giver yell).
|
||
6. **`GameMessageTurbineChat` (0xF7DE)** — the Turbine overlay for cross-server chat (General, Trade, LFG, Roleplay, Society). Has a nested blob format: `uint bytesToFollow | ChatNetworkBlobType type | uint dispatchType | uint 1 | uint 0x000B00B5 | uint 1 | uint 0x000B00B5 | uint 0 | uint nestedBytes | uint channel | byteLen-or-u16Len+UTF16 senderName | byteLen-or-u16Len+UTF16 message | uint 0x0C | guid senderID | uint 0 | ChatType`.
|
||
7. **`GameEventSetTurbineChatChannels` (0x0295)** — tells the client which Turbine channels to enable (sent at login).
|
||
|
||
### Chat window layout (inferred from message types and ACE logging)
|
||
|
||
- **Top bar**: tab strip (`General`, `Combat`, `Chat`, `1`, `2`, …). Each tab has a filter mask over ChatMessageType values (ChatDisplayMask / ChatFilterMask enums).
|
||
- **Text scroll area**: ring buffer of rendered lines. Each line carries its source ChatMessageType so the tab filter can decide to show it; font color is derived from the ChatMessageType too.
|
||
- **Input line** at the bottom: mode byte (`/s` say, `/t name` tell, `/f` fellow, `/a` allegiance, `@channel` for named channels, raw text = last-used mode). The client sends `GameActionChatChannel` on enter.
|
||
- **Filters**: Character Options (`ListenToAllegianceChat` 0x1B, `ListenToGeneralChat` 0x23, `ListenToTradeChat` 0x24, `ListenToLFGChat` 0x25, `ListenToRoleplayChat` 0x26, `ListenToSocietyChat` 0x2E, `ListenToPKDeathMessages` 0x34) control what passes through the filter.
|
||
|
||
### Strings observed
|
||
|
||
Routing strings from the big server-message switch in `chunk_00570000.c`:
|
||
|
||
- `L"You have entered the %s channel.\n"` (case `0x51b`)
|
||
- `L"You have left the %s channel.\n"` (case `0x51c`)
|
||
- `L"That channel doesn\'t exist."`
|
||
- `L"You can\'t use that channel."`
|
||
- `L"You\'re already on that channel."`
|
||
- `L"You\'re not currently on that channel."`
|
||
- `L"Message Blocked: %s"` (case `0x51f` — squelched sender)
|
||
- `L"%s has been added to the list of people you can hear.\n"`
|
||
- `L"You are now deaf to player\'s screams.\n"`
|
||
- `L"You can hear all players once again.\n"`
|
||
|
||
### C# port sketch — `ChatWindow`
|
||
|
||
```csharp
|
||
namespace AcDream.UI.Panels;
|
||
|
||
public enum ChatTabKind { General, Combat, Chat1, Chat2, Chat3, Chat4 }
|
||
|
||
public sealed class ChatTab
|
||
{
|
||
public string Label { get; init; }
|
||
public ChatDisplayMask VisibleTypes { get; set; } // bitmask over ChatMessageType values 0x00-0x1F
|
||
public Channel ChannelFilter { get; set; } // bitmask over Channel enum
|
||
public readonly RingBuffer<ChatLine> Lines = new(2048);
|
||
public bool UnreadBadge { get; set; }
|
||
}
|
||
|
||
public readonly record struct ChatLine(
|
||
ulong Timestamp,
|
||
ChatMessageType MessageType,
|
||
Channel Channel,
|
||
uint SenderGuid,
|
||
string SenderName,
|
||
string Text,
|
||
Color Color);
|
||
|
||
public sealed class ChatWindow : IPanel
|
||
{
|
||
public IReadOnlyList<ChatTab> Tabs { get; }
|
||
public int ActiveTabIndex { get; set; }
|
||
public ChatInputMode InputMode { get; set; } // Say / Tell / Fellow / Allegiance / Named("AlArqas")
|
||
public string PendingTellTarget { get; set; }
|
||
public uint SubscribedChannels { get; private set; } // bitmask
|
||
|
||
// Drivers (server→UI events)
|
||
public void OnChannelBroadcast(Channel ch, string sender, string text);
|
||
public void OnTell(string text, string sender, uint senderGuid, uint targetGuid, ChatMessageType type);
|
||
public void OnSystemChat(string text, ChatMessageType type);
|
||
public void OnHearSpeech(string text, string sender, uint senderGuid, ChatMessageType type);
|
||
public void OnTurbineChat(uint channel, string sender, string text, ChatType chatType);
|
||
public void OnSetTurbineChannels(uint[] channels);
|
||
|
||
// Outbound (UI→server)
|
||
public void Send(string text); // routes through InputMode to GameActionChatChannel / Say / Tell etc.
|
||
}
|
||
```
|
||
|
||
## 2. Character Attributes panel
|
||
|
||
**Location**: `chunk_00470000.c::FUN_0047ba70` (the monolithic "character sheet" builder, starting at line ~8280 with "Gender:" and running through skills). Strings rendered via `FUN_0040b8f0`. The sheet body is a single giant switch-style render; each row is emitted via `FUN_0046f670(1,0)` row-break plus a field/value pair.
|
||
|
||
### Layout (from the render order in `FUN_0047ba70`)
|
||
|
||
The sheet renders top-down:
|
||
|
||
1. **Gender** (strings table at `PTR_DAT_0081a1c4` — "Male" / "Female" / "Unknown").
|
||
2. **Heritage** (strings at `PTR_DAT_0081a1d0`, 5 entries — Aluvian, Gharu'ndim, Sho, Viamontian, Umbraen/Empyrean).
|
||
3. **Starting Town** (strings at `PTR_u_Holtburg_0081a1e4`, 4 entries — Holtburg, Shoushi, Yaraq, Sanamar/Rithwic per heritage).
|
||
4. **"Attributes" header** (row break, layout resource `0x100000fe`).
|
||
5. **10 attribute rows** (loop `iVar10 < 10`), each using formatter resource `0x100002fc` (label) + `0x100002fd` (value):
|
||
- Case 0 — Strength (value at `player + 0x184`)
|
||
- Case 1 — Endurance (value at `+0x188`)
|
||
- Case 2 — Coordination (value at `+0x18c`)
|
||
- Case 3 — Quickness (value at `+0x190` = 400 dec)
|
||
- Case 4 — Focus (value at `+0x194`)
|
||
- Case 5 — Self (value at `+0x198`)
|
||
- Case 6 — Health (max via `FUN_005df4c4`, curr via `FUN_005c4990(2)`)
|
||
- Case 7 — Stamina (`FUN_005c4990(2)` → returns stamina)
|
||
- Case 8 — Mana (`FUN_005c4990(6)`)
|
||
- Case 9 — Skill Credits (value at `+0x1b8`)
|
||
6. **4 skill-category headers** (loop `iStack_10 < 4`):
|
||
- Case 0 — "Specialized Skills" (filter rank `iStack_14 = 3`)
|
||
- Case 1 — "Trained Skills" (filter rank `2`)
|
||
- Case 2 — "Useable Untrained Skills" (filter rank `1`, show only if skill.usableWithoutTraining)
|
||
- Case 3 — "Unuseable Untrained Skills" (filter rank `1`, inverse)
|
||
7. **Per-skill rows** — walk the skill linked list at `player + 0x1c4`; for each node read skill-id (`+0x20`), category (`FUN_005c4be0`), value (`FUN_005c5b30`), and emit row only if `category == iStack_14`.
|
||
|
||
### Short form (from `chunk_005C0000.c::FUN_005c9d70` — tooltip dispatch)
|
||
|
||
A small helper that picks an attribute label by 1-based switch:
|
||
1 → Strength, 2 → Endurance, 3 → Quickness, 4 → Coordination, 5 → Focus, 6 → Self.
|
||
|
||
Descriptions from `FUN_005c9e10`:
|
||
- Strength: "Measures your character's muscular power."
|
||
- Endurance: "Measures how healthy your character is."
|
||
- Quickness: "Measures how fast your character is."
|
||
- Coordination: "Measures your character's reflexes"
|
||
- Focus: "Measures your character's mind and senses."
|
||
- Self: "Measures your character's willpower."
|
||
|
||
Vitals from `FUN_005c9eb0`: Maximum Health, Health, Maximum Stamina, Stamina, Maximum Mana, Mana.
|
||
|
||
Vital descriptions (`FUN_005c9f50`):
|
||
- Health: "(Endurance/2)\nIf you run out of health, you will die!"
|
||
- Stamina: "(Endurance)\nAffects your actions and movement."
|
||
- Mana: "(Self)\nAffects how much magic you can cast."
|
||
|
||
### Wire drivers
|
||
|
||
- **`GameMessagePrivateUpdateAttribute` (0x02E3)** — `uint sequence | PropertyAttribute attr | uint ranks | uint startingValue | uint xpSpent`. Sent per-attribute on gain. 21 bytes.
|
||
- **`GameMessagePrivateUpdateVital` (0x02E7)** — `uint sequence | PropertyAttribute2ndLevel vital | uint ranks | uint startingValue | uint xpSpent | uint current`. 25 bytes.
|
||
- **`GameMessagePrivateUpdateAttribute2ndLevel` (0x02E9)** — max-value refresh for vitals.
|
||
- **`GameEventPlayerDescription` (0xF7B0 / 0x0013)** — initial full character dump at login; includes all attributes, vitals, skills, equipped objects, clothing/hair metadata.
|
||
|
||
### C# port sketch — `AttributesPanel`
|
||
|
||
```csharp
|
||
public enum PropertyAttribute { Strength=1, Endurance, Quickness, Coordination, Focus, Self }
|
||
public enum PropertyAttribute2ndLevel { MaxHealth=1, Health, MaxStamina, Stamina, MaxMana, Mana }
|
||
|
||
public readonly record struct AttributeStats(uint Ranks, uint StartingValue, uint XpSpent)
|
||
{
|
||
public uint Current => StartingValue + Ranks;
|
||
}
|
||
|
||
public readonly record struct VitalStats(uint Ranks, uint StartingValue, uint XpSpent, uint Current)
|
||
{
|
||
public uint Max => StartingValue + Ranks;
|
||
}
|
||
|
||
public sealed class AttributesPanel : IPanel
|
||
{
|
||
public Gender Gender { get; set; }
|
||
public HeritageGroup Heritage { get; set; }
|
||
public StartingTown StartingTown { get; set; }
|
||
|
||
public AttributeStats Strength, Endurance, Coordination, Quickness, Focus, Self;
|
||
public VitalStats Health, Stamina, Mana;
|
||
public uint SkillCredits;
|
||
|
||
public void OnUpdateAttribute(PropertyAttribute attr, AttributeStats stats);
|
||
public void OnUpdateVital(PropertyAttribute2ndLevel vital, VitalStats stats);
|
||
public void OnUpdateSkillCredits(uint credits); // via PrivateUpdatePropertyInt
|
||
}
|
||
```
|
||
|
||
## 3. Skills panel
|
||
|
||
Shares the character sheet with Attributes (both live inside `FUN_0047ba70`). The 4 skill categories ("Specialized Skills", "Trained Skills", "Useable Untrained Skills", "Unuseable Untrained Skills") appear below the attribute block.
|
||
|
||
### Skill advancement classes (AdvancementClass enum)
|
||
|
||
1 = Untrained, 2 = Trained, 3 = Specialized.
|
||
|
||
### Display rules (from the decompile)
|
||
|
||
- Each skill row is emitted as: `[category-header]\n<tab>skill-name<tab>value`.
|
||
- Specialized skills show in cyan/white, Trained in white, Untrained-useable in gray, Untrained-unuseable hidden by default (still iterated).
|
||
- "Useable Untrained" means the skill has `UsableWithoutTraining` property = true (e.g. Run, Jump, Loyalty) AND `AdvancementClass == Untrained`.
|
||
- For each skill row the decompile also looks up a hashtable at `iStack_4 + 8` (keyed by `skillId % bucketCount`) — likely the skill hash used for secondary display metadata (icon, current training rank).
|
||
|
||
### Buy/Train interaction
|
||
|
||
The sheet also has buttons (not in this render function but registered via event callbacks earlier) for Train and Specialize — they cost skill credits (displayed in Attributes section) and XP. Success/failure echoes via `GameMessageSystemChat`. Strings seen in `chunk_00570000.c`:
|
||
|
||
- `L"You have failed to alter your skill.\n"`
|
||
- `L"Your %s skill is already untrained!\n"`
|
||
- `L"Although you cannot untrain your %s skill, you have succeeded in recovering all the experience you had invested in it.\n"`
|
||
- `L"Although your augmentation will not allow you to untrain your %s skill, you have succeeded in recovering all the experience you had invested in it.\n"`
|
||
|
||
### Wire drivers
|
||
|
||
- **`GameMessagePrivateUpdateSkill` (0x02DD)** — `uint sequence | PropertySkill skill | uint ranks | ushort adjustPP | uint advancementClass | uint xpSpent | uint initLevel | uint resistanceAtLastCheck | double lastUsedTime`. 37 bytes.
|
||
- **`GameMessagePrivateUpdateSkillLevel` (0x02DF)** — periodic rank update.
|
||
|
||
### C# port sketch — `SkillsPanel`
|
||
|
||
```csharp
|
||
public enum SkillAdvancementClass { Undef=0, Inactive=1, Untrained=2, Trained=3, Specialized=4 }
|
||
|
||
public readonly record struct SkillState(
|
||
uint Skill,
|
||
uint Ranks,
|
||
SkillAdvancementClass Class,
|
||
uint XpSpent,
|
||
uint InitLevel,
|
||
uint ResistanceAtLastCheck,
|
||
double LastUsedTime)
|
||
{
|
||
public uint Current => InitLevel + Ranks;
|
||
}
|
||
|
||
public sealed class SkillsPanel : IPanel
|
||
{
|
||
public readonly Dictionary<Skill, SkillState> Skills = new();
|
||
public void OnUpdateSkill(SkillState s);
|
||
public void RaiseTrainSkill(Skill s); // sends GameAction TrainSkill
|
||
public void RaiseSpecializeSkill(Skill s);
|
||
public void RaiseUntrainSkill(Skill s);
|
||
|
||
public IEnumerable<SkillState> Specialized => Skills.Values.Where(s => s.Class == SkillAdvancementClass.Specialized);
|
||
public IEnumerable<SkillState> Trained => Skills.Values.Where(s => s.Class == SkillAdvancementClass.Trained);
|
||
public IEnumerable<SkillState> UntrainedUsable; // needs Skill metadata lookup
|
||
}
|
||
```
|
||
|
||
## 4. Spell panel
|
||
|
||
**Location**: `chunk_004C0000.c` — the spell-bar code. Entry point `FUN_004c68f0` (the spellbar bind function) at address `0x004C68F0`.
|
||
|
||
### Spell tabs (7 tabs)
|
||
|
||
From `FUN_004c6500` (returns the active tab index from a mode byte at `spellbar + 0x5fc.+0x6d8`):
|
||
|
||
| Tab | Value | Icon resource ID | Represents |
|
||
|-----|-------|------------------|------------|
|
||
| 0 | Life | `0x100000aa` (button) / `0x100000a3` (icon) | Life magic |
|
||
| 1 | Creature | `0x100000ab` / `0x100000a4` | Creature Enchantment |
|
||
| 2 | Item | `0x100000ac` / `0x100000a5` | Item Enchantment |
|
||
| 3 | War | `0x100000ad` / `0x100000a6` | War Magic |
|
||
| 4 | Portal | `0x100000ae` / `0x100000a7` | Portal Magic (recall/summon) |
|
||
| 5 | (unused retail) | `0x100000af` / `0x100000a8` | |
|
||
| 6 | (unused retail) | `0x100000b0` / `0x100000a9` | |
|
||
| 7 | Void | `0x100005c3` / `0x100005c2` | Void Magic (post-ToD) |
|
||
|
||
Each tab is registered into the spellbar via `FUN_004c6680(spellbar, buttonId, iconId, tabIndex)`.
|
||
|
||
### Per-tab spell slot storage
|
||
|
||
The spellbar keeps 7 slots per tab. Slot storage lives at `spellbar + 0x634 + (tabIndex * 0x1c)` (28 bytes per tab: 7 slots × 4-byte spell IDs = 28).
|
||
|
||
### Ready/selected spell
|
||
|
||
- `spellbar + 0x620` = currently-selected spell's container object ID.
|
||
- `spellbar + 0x624` = spell ID of the selected spell.
|
||
- `iVar4 * 0x1c + 0x640 + spellbar` = per-tab "is this tab the current ready tab?" flag byte.
|
||
|
||
### Cast interaction (`FUN_004c78XX` region, around line 5040-5210)
|
||
|
||
- User double-clicks a spell slot → `FUN_004c7c50` fires.
|
||
- Single-click just highlights via `FUN_006a9640` and shows "Double-click to cast this spell" tooltip (`L"%hs\nDouble-click to cast this spell"`).
|
||
- On cast button press: validates target via `DAT_00871e54` (current selection), checks spell-can-target-self via `FUN_00588350`, and if valid formats `L"CAST %hs"` + optional `L" on %s"` and sends via `FUN_005abb30`.
|
||
- Error paths:
|
||
- `L"Select a spell to cast"` (no slot filled, but spells known) — `FUN_00407e40` tooltip bubble.
|
||
- `L"You have no spells ready to cast"` (no tab populated at all).
|
||
- `L"You must select a target for %hs"` (no selection).
|
||
- `L"You must select an appropriate target for %hs"` (wrong target type).
|
||
- `L"You cannot cast this spell upon yourself"` (self-cast when not allowed).
|
||
- `L"This spell would require a target"` / `L"This spell would require no target"` (target-present/absent mismatch).
|
||
- `L"Cannot cast spell on a stack of items."`
|
||
- `L"This spell cannot be cast on %s"`.
|
||
|
||
### Wire drivers
|
||
|
||
- **`GameEventMagicUpdateSpell` (0x02C1)** — adds a known spell to the spellbook.
|
||
- **`GameEventMagicRemoveSpell` (0x01A8)** — removes.
|
||
- **`GameEventMagicUpdateEnchantment` (0x02C2)** — active enchantment on player (for the spell-bar enchantment overlay).
|
||
- **`GameEventMagicRemoveEnchantment` (0x02C3)** / **Multiple** (0x02C5) / **Purge** (0x02C6) / **Dispel** (0x02C7–8) / **PurgeBad** (0x0312).
|
||
- **`GameAction` CastUnTargetedSpell / CastTargetedSpell** (outbound) — triggers the cast.
|
||
|
||
### C# port sketch — `SpellBarPanel`
|
||
|
||
```csharp
|
||
public enum SpellSchool { Life, CreatureEnchantment, ItemEnchantment, War, Portal, Slot5, Slot6, Void }
|
||
public const int SlotsPerTab = 7;
|
||
|
||
public sealed class SpellBarPanel : IPanel
|
||
{
|
||
public readonly Dictionary<SpellSchool, uint?[]> Slots = new();
|
||
public SpellSchool ActiveTab { get; set; }
|
||
public int SelectedSlot { get; set; }
|
||
public uint? ReadySpellId { get; private set; }
|
||
public uint? ReadySpellContainerId { get; private set; }
|
||
|
||
public SpellBarPanel()
|
||
{
|
||
foreach (var s in Enum.GetValues<SpellSchool>())
|
||
Slots[s] = new uint?[SlotsPerTab];
|
||
}
|
||
|
||
public void OnLearnedSpell(uint spellId, SpellSchool school);
|
||
public void OnUnlearnedSpell(uint spellId);
|
||
public void AssignToSlot(SpellSchool tab, int slot, uint spellId);
|
||
|
||
public bool TryCast(uint? targetId, out string error); // emits "Select a spell to cast" etc.
|
||
}
|
||
|
||
public sealed class SpellbookPanel : IPanel
|
||
{
|
||
// Full known-spell list (filterable by school / level)
|
||
public readonly HashSet<uint> KnownSpells = new();
|
||
public void OnUpdateSpell(uint spellId) => KnownSpells.Add(spellId);
|
||
public void OnRemoveSpell(uint spellId) => KnownSpells.Remove(spellId);
|
||
}
|
||
|
||
public sealed class EnchantmentPanel : IPanel
|
||
{
|
||
// Active buffs/debuffs with countdown
|
||
public readonly Dictionary<uint, Enchantment> Active = new();
|
||
public void OnAdd(Enchantment e);
|
||
public void OnRemove(uint spellId);
|
||
public void OnPurgeAll();
|
||
public void OnPurgeBadOnly();
|
||
}
|
||
```
|
||
|
||
## 5. Paperdoll / Equipment panel
|
||
|
||
**Location**: `chunk_004A0000.c::FUN_004a5200` is the slot-router that emits the correct "Drag X here to wear them" tooltip and handles drag-drop assignment. Slot widget pointers are stored at fixed offsets from the panel `this` pointer (`param_1`).
|
||
|
||
### Slot table (derived from `FUN_004a5200` offsets)
|
||
|
||
| Offset | Slot | Empty-tooltip string | EquipMask flag |
|
||
|--------|------|----------------------|----------------|
|
||
| `+0x604` | Necklace | `L"Drag necklaces here to wear them"` | `NeckWear 0x00008000` |
|
||
| `+0x608` | Left Bracelet | `L"Drag bracelets here to wear them"` | `WristWearLeft 0x00010000` |
|
||
| `+0x610` | Right Bracelet | `L"Drag bracelets here to wear them"` | `WristWearRight 0x00020000` |
|
||
| `+0x60C` | Left Ring | `L"Drag rings here to wear them"` | `FingerWearLeft 0x00040000` |
|
||
| `+0x614` | Right Ring | `L"Drag rings here to wear them"` | `FingerWearRight 0x00080000` |
|
||
| `+0x618` | Weapon | `L"Drag weapons here to wield them"` | `MeleeWeapon 0x00100000` |
|
||
| `+0x61C` | Missile Ammo | `L"Drag missile ammunition here to wield it"` | `MissileAmmo 0x00800000` |
|
||
| `+0x620` | Shield | `L"Drag shields here to wield them"` | `Shield 0x00200000` |
|
||
| `+0x624` | Clothing (Shirt) | `L"Drag clothing items here to wear them"` | `ChestWear 0x00000002` |
|
||
| `+0x628` | Clothing (Pants) | `L"Drag clothing items here to wear them"` | `AbdomenWear 0x00000004` |
|
||
| `+0x62C` | Trinket | `L"Drag trinkets here to activate them"` | `TrinketOne 0x04000000` |
|
||
| `+0x630` | Cloak | `L"Drag cloaks here to activate them"` | `Cloak 0x08000000` |
|
||
| `+0x634` | Aetheria Blue (Sigil1 — Lyr) | `L"Drag a Blue Aetheria sigil here to activate it"` | `SigilOne 0x10000000` |
|
||
| `+0x638` | Aetheria Yellow (Sigil2 — Kor) | `L"Drag a Yellow Aetheria sigil here to activate it"` | `SigilTwo 0x20000000` |
|
||
| `+0x63C` | Aetheria Red (Sigil3 — Tem) | `L"Drag a Red Aetheria sigil here to activate it"` | `SigilThree 0x40000000` |
|
||
| `+0x640` | Head | `L"Drag head items here to wear them"` | `HeadWear 0x00000001` |
|
||
| `+0x644` | Chest Armor | `L"Drag chest items here to wear them"` | `ChestArmor 0x00000200` |
|
||
| `+0x648` | Abdomen Armor | `L"Drag abdomen items here to wear them"` | `AbdomenArmor 0x00000400` |
|
||
| `+0x64C` | Upper Arm Armor | `L"Drag upper arm items here to wear them"` | `UpperArmArmor 0x00000800` |
|
||
| `+0x650` | Lower Arm Armor | `L"Drag lower arm items here to wear them"` | `LowerArmArmor 0x00001000` |
|
||
| `+0x654` | Glove Armor | `L"Drag glove items here to wear them"` | `HandWear 0x00000020` |
|
||
| `+0x658` | Upper Leg Armor | `L"Drag upper leg items here to wear them"` | `UpperLegArmor 0x00002000` |
|
||
| `+0x65C` | Lower Leg Armor | `L"Drag lower leg items here to wear them"` | `LowerLegArmor 0x00004000` |
|
||
| `+0x660` | Foot Armor | `L"Drag foot coverings here to wear them"` | `FootWear 0x00000100` |
|
||
| `+0x664` | (cross-slot state) | — | — |
|
||
|
||
Additional layout widgets around `+0x668` (toggle to armor-view), `+0x674` (toggle to clothing-view), `+0x670` (paperdoll 3D model preview), `+0x678` (sidebar with stats).
|
||
|
||
### Armor/Clothing view toggle
|
||
|
||
`FUN_004a5fa0` at line 2660: the paperdoll has a mode toggle between "clothing view" (shows shirt/pants/jewelry/weapons) and "armor view" (shows all 10 armor slots). The mode is set by event `0x100005be` (button id). When toggled, slot widget visibility is flipped via `(**)(slotWidget + 0x18))(showFlag)` calls — classic "hide 8 widgets, show 10 widgets" pattern.
|
||
|
||
### Drag-drop behavior
|
||
|
||
- Drop onto an empty slot: tooltip cycles through the "Drag X here to wear them" messages.
|
||
- Drop onto a filled slot OR double-click on the paperdoll slot: emits "(item name) (%s)\nDouble-click to %s" where the inserted strings are either ("worn", "take off") for armor/clothing or ("wielded", "unwield") for weapons/shield (line 2619-2624 branch `bVar1`).
|
||
- Failed drag (wrong item type for slot): `L"You can\'t put that item there"` via `FUN_004a4de0`.
|
||
- Error branches come from the server: `L"You're already wearing a helm."`, `L"You're already wearing chest armor."`, …, and similar for every body region (chunk_00560000.c line 1199-1394).
|
||
|
||
### 3D paperdoll model preview
|
||
|
||
`FUN_004a4c70` (starting line ~2150) — loads a preview scene at widget `+0x68c` (a rendered 3D object overlay), placing a scaled copy of the character's visual (ObjDesc) for live armor/weapon visualization. When an item is dragged onto a slot, the server sends an updated `GameMessageObjDescEvent` which triggers a repaint here.
|
||
|
||
### Wire drivers
|
||
|
||
- **`GameMessageObjDescEvent` (0xF625)** — full object-description (clothing base + palette swatches + hair) refresh. Any slot change triggers this on every nearby player.
|
||
- **`GameEventWieldObject` (0x0023)** — confirms that an equip request succeeded. Contains the item's guid + new wield location.
|
||
- **`GameEventInventoryPutObjInContainer` (0x0022)** — inverse: item moved back to backpack.
|
||
- **`GameActionPutItemInContainer` / `GameActionWieldItem`** — outbound equip/unequip requests.
|
||
|
||
### C# port sketch — `PaperdollPanel`
|
||
|
||
```csharp
|
||
public sealed class PaperdollPanel : IPanel
|
||
{
|
||
// 25 slots, keyed by EquipMask bit
|
||
public readonly Dictionary<EquipMask, uint?> Equipment = new();
|
||
|
||
// The visual 3D model
|
||
public ObjDescSnapshot PaperdollObjDesc { get; set; }
|
||
|
||
public PaperdollViewMode View { get; set; } = PaperdollViewMode.Clothing;
|
||
|
||
public void OnWieldObject(uint itemGuid, EquipMask location);
|
||
public void OnUnwieldObject(uint itemGuid);
|
||
public void OnObjDescUpdate(ObjDescSnapshot desc);
|
||
|
||
public bool TryDragEquip(uint itemGuid, EquipMask targetSlot, out string errorString);
|
||
public bool TryDoubleClickUnequip(EquipMask slot);
|
||
}
|
||
|
||
public enum PaperdollViewMode { Clothing, Armor }
|
||
```
|
||
|
||
The slot table MUST match the offsets above exactly when porting drag-drop dispatch — the retail client dispatches via offset comparisons, not enum values.
|
||
|
||
## 6. Inventory panel
|
||
|
||
The inventory panel is heavily intertwined with the paperdoll (same window in retail) and with containers (sub-windows when you open a pack). Inventory code is scattered across `chunk_004B0000.c` (container list renderer), `chunk_004E0000.c` (container placement), and `chunk_00580000.c` (item-use and pickup dispatch).
|
||
|
||
### Layout
|
||
|
||
- **Side tabs**: Main Pack + 6 numbered Side Packs (up to 7 packs total, each up to 24 items). Main pack = character's base container.
|
||
- **Grid**: fixed-size icon grid inside each pack. Items beyond visible range use a scroll-arrow or "Page X" header (see `L"Page %d"` format at chunk_004B0000.c lines 2881/2903).
|
||
- **Footer**: Burden / capacity text. Burden status strings come from `chunk_00560000.c`: `L"You are encumbered!"` (line 157), `L"You are severely encumbered!"` (163).
|
||
|
||
### Key interactions (from `chunk_00580000.c` dispatch)
|
||
|
||
- **Right-click**: context menu with Use / Examine / Split / Drop / Give options.
|
||
- **Double-click**: `GameActionUseItem` (equips or activates).
|
||
- **Left-click drag**: move between slots. If cross-container, sends `GameActionPutItemInContainer`.
|
||
- **Drag to paperdoll**: equip.
|
||
- **Drag to ground**: drop (`GameActionDropItem`).
|
||
- **Drag to NPC**: give (`GameActionGiveObjectRequest`).
|
||
- **Shift-click on stack**: split (`GameActionStackableSplit`).
|
||
|
||
### Error strings (partial)
|
||
|
||
- `L"You can't pick that up!"`, `L"You are too encumbered to carry that!"`, `L"You cannot pick up more of that item!"` (chunk_00570000 line 1849-3018).
|
||
- `L"You must first pick up the %s"`, `L"The %s is locked"`, `L"You must open the %s first"`, `L"Cannot give %s to %s"`, `L"Move cancelled"`, `L"You cannot do that in mid air"`.
|
||
- `L"The destination stack is already full."`, `L"You cannot merge different types of items."`, `L"You cannot merge items while they are being traded."`.
|
||
- `L"Cannot place container in item list"`, `L"Cannot place item in container list"`, `L"Already attempting to place %s here"`, `L"The %s cannot accept items"`.
|
||
- `L"You are not carrying the %s"` (line 4826 in chunk_004B0000).
|
||
|
||
### Stack / split
|
||
|
||
The split UI uses a numeric entry dialog. Split-related errors:
|
||
- `L"Cannot split the stack to sell it"`, `L"Cannot split the stack for dwelling costs"`, `L"The %s can't be split"`, `L"You must split the stack before selling it."`.
|
||
|
||
### Wire drivers
|
||
|
||
- **`GameMessageCreateObject` (0xF745)** — the universal "object exists, here's its data" — inventory items arrive through this.
|
||
- **`GameEventInventoryPutObjInContainer` (0x0022)**, **`GameEventItemServerSaysContainId`**, **`GameEventItemServerSaysMoveItem`** — confirm server-side state after an inventory action.
|
||
- **`GameMessageSetStackSize` (0x0197)** — stack merged/split result.
|
||
- **`GameMessageInventoryRemoveObject` (0x0024)** — item deleted.
|
||
- **`GameMessagePickupEvent` (0xF74A)** — outbound, client picking up a ground object.
|
||
- **`GameEventViewContents` (0x0196)** — sent when client opens a container (includes full item list).
|
||
|
||
### C# port sketch — `InventoryPanel`
|
||
|
||
```csharp
|
||
public sealed class InventoryItem
|
||
{
|
||
public uint Guid;
|
||
public uint IconResourceId;
|
||
public string Name;
|
||
public uint StackSize;
|
||
public uint Burden;
|
||
public uint Value;
|
||
public EquipMask ValidLocations;
|
||
public ItemType Type;
|
||
public uint ContainerGuid; // who holds us
|
||
public int PlacementIndex;
|
||
public IsEquipped Equipped;
|
||
}
|
||
|
||
public sealed class InventoryPanel : IPanel
|
||
{
|
||
public uint CharacterGuid { get; }
|
||
public readonly Dictionary<uint, InventoryItem> Items = new(); // by GUID
|
||
public readonly List<uint> OpenContainerStack = new(); // Main + side packs currently viewed
|
||
|
||
public int SelectedTab { get; set; }
|
||
public int BurdenCurrent { get; private set; }
|
||
public int BurdenMax { get; private set; }
|
||
public BurdenStatus Status { get; private set; } // Fine, Encumbered, SeverelyEncumbered
|
||
|
||
public void OnCreateObject(InventoryItem item);
|
||
public void OnRemoveObject(uint guid);
|
||
public void OnStackSize(uint guid, uint newSize);
|
||
public void OnPutInContainer(uint itemGuid, uint newContainer, int placement);
|
||
public void OnViewContents(uint containerGuid, InventoryItem[] contents);
|
||
|
||
public void RaiseMoveItem(uint guid, uint destContainer, int placement);
|
||
public void RaiseSplitItem(uint guid, uint newSize);
|
||
public void RaiseDropItem(uint guid);
|
||
public void RaiseGiveItem(uint guid, uint targetGuid);
|
||
}
|
||
|
||
public enum BurdenStatus { Fine, Encumbered, SeverelyEncumbered }
|
||
```
|
||
|
||
## 7. Quickbar
|
||
|
||
The retail client has a 10-slot quickbar bound to **1–0 keys**, visible along the bottom-center of the screen. The decompiled binding code resides near the main-window input dispatch; I did not find the exact function address during this sweep (would be in a chunk around `0x004D0000` given the `"ID_InputMap_*"` strings in `chunk_004D0000.c` line 4808, e.g. `"ID_InputMap_CameraAlternateControls"`).
|
||
|
||
### Behaviors
|
||
|
||
- Slots accept any item from inventory or any spell from the spellbar.
|
||
- On slot-press: if item → `GameActionUseItem(guid)`; if spell → equivalent of double-clicking that spell slot.
|
||
- Shift+number rearranges; ctrl+number clears. (Standard AC layout.)
|
||
|
||
### C# port sketch — `Quickbar`
|
||
|
||
```csharp
|
||
public enum QuickbarAction { None, UseItem, CastSpell, PlayMacro }
|
||
|
||
public sealed class QuickbarSlot
|
||
{
|
||
public QuickbarAction Action;
|
||
public uint Target; // item-guid, spell-id, or macro-id
|
||
public uint IconId;
|
||
}
|
||
|
||
public sealed class Quickbar : IPanel
|
||
{
|
||
public readonly QuickbarSlot[] Slots = new QuickbarSlot[10];
|
||
public int ActiveBar { get; set; } // retail has only one, but plan for plugin expansion
|
||
public void Press(int slotIndex); // resolve action → dispatch through Inventory/SpellBar
|
||
public void Assign(int slotIndex, QuickbarSlot slot);
|
||
}
|
||
```
|
||
|
||
## 8. Allegiance panel
|
||
|
||
**Location**: rendering lives in ACE code as an `AllegianceProfile` struct sent inside `GameEventAllegianceUpdate (0x0020)`; the client side is in a chunk I did not fully decompile in this sweep (would be around `0x004B`/`0x004C`).
|
||
|
||
### Content rendered
|
||
|
||
- **Monarch** name + title.
|
||
- **Patron** (your direct superior) — may be null.
|
||
- **Vassals** — list of direct reports with level, gender, online/offline.
|
||
- Rank (your depth in the tree), allegiance name, MOTD (if set), officer level.
|
||
- Recruited count, percentage breakdown.
|
||
- Bans and officer list (if you're monarch).
|
||
|
||
### Events / strings
|
||
|
||
- `L"You're already sworn your Allegiance"` / `L"You are not in an allegiance!"` (chunk_00570000 lines 1527/1541).
|
||
- `L"No patron from which to break!"`, `L"Your Allegiance has been dissolved!"`.
|
||
- `L"You have been teleported too recently!"` (recall-related).
|
||
- `L"That is an invalid officer level."`.
|
||
- `L"Your allegiance is currently: %s."` / `L"Your allegiance is now: %s."`.
|
||
- `L"Your allegiance name has been cleared."`.
|
||
- `L"Banned Characters: "` (list header).
|
||
- `L"You are banned from %s's allegiance!"`.
|
||
- `L"Allegiance information for %hs%s:\n"` (chunk_00560000 line 7245 — the `@allegiance info` dump).
|
||
|
||
### Wire drivers
|
||
|
||
- **`GameEventAllegianceUpdate` (0x0020)** — full allegiance tree + rank + profile.
|
||
- **`GameEventAllegianceLoginNotification` (0x027A)** — "Vassal X is online" popups.
|
||
- **`GameEventAllegianceInfoResponse` (0x027C)** — response to `@allegiance info`.
|
||
- **`GameEventAllegianceAllegianceUpdateDone` (0x01C8)** — action-complete marker.
|
||
- **`GameEventAllegianceUpdateAborted` (0x0003)**.
|
||
|
||
### C# port sketch — `AllegiancePanel`
|
||
|
||
```csharp
|
||
public sealed class AllegianceMember
|
||
{
|
||
public uint Guid;
|
||
public string Name;
|
||
public int Level;
|
||
public Gender Gender;
|
||
public HeritageGroup Heritage;
|
||
public bool Online;
|
||
public uint PatronGuid;
|
||
public int Rank; // depth
|
||
public AllegianceOfficerLevel OfficerLevel;
|
||
public List<AllegianceMember> Vassals = new();
|
||
}
|
||
|
||
public sealed class AllegiancePanel : IPanel
|
||
{
|
||
public AllegianceMember Monarch;
|
||
public AllegianceMember Patron;
|
||
public List<AllegianceMember> Vassals;
|
||
public string AllegianceName;
|
||
public string AllegianceMotd;
|
||
public uint MyRank;
|
||
public AllegianceOfficerLevel MyOfficerLevel;
|
||
public void OnAllegianceUpdate(AllegianceMember root, AllegianceMember self);
|
||
public void OnLoginNotification(string name, bool online);
|
||
}
|
||
```
|
||
|
||
## 9. Fellowship panel
|
||
|
||
### Content rendered (from `GameEventFellowshipFullUpdate 0x02BE`)
|
||
|
||
Each fellow line includes:
|
||
|
||
- **Name** (string16L).
|
||
- **Level**.
|
||
- **Health/Stamina/Mana** max + current (shown as bars).
|
||
- **cpCached / lumCached** (undistributed exp and luminance shares).
|
||
- **ShareLoot flag** (0 or 0x10).
|
||
|
||
Fellowship-level fields:
|
||
|
||
- FellowshipName (up to 256 chars).
|
||
- FellowshipLeaderGuid.
|
||
- ShareXP / EvenShare / Open flags.
|
||
- IsLocked flag.
|
||
- DepartedMembers list (with cooldown timers).
|
||
- FellowshipLocks (post-ToD locks).
|
||
|
||
### Interaction
|
||
|
||
- Leader can recruit (drag player onto fellowship UI or `@fellow recruit PlayerName`).
|
||
- Leader can dismiss (`@fellow dismiss name`).
|
||
- Any member can quit (`@fellow quit`).
|
||
- Leader can disband the fellowship.
|
||
|
||
### Strings observed
|
||
|
||
- `L"You are unprepared to cast a spell"` (unrelated but appears in same switch).
|
||
- `L"You must be the leader of a Fellowship"`.
|
||
- `L"Your Fellowship is full"`.
|
||
- `L"That Fellowship name is not permitted"`.
|
||
- `L"You do not belong to a Fellowship."`.
|
||
- `L"This fellowship is locked; "` + name + `" cannot be recruited into the fellowship."` (line 2543).
|
||
- `L"The fellowship is locked, you were not added to the fellowship."`.
|
||
- `L"The fellowship is locked; you cannot open locked fellowships."`.
|
||
|
||
### Wire drivers
|
||
|
||
- **`GameEventFellowshipFullUpdate` (0x02BE)**.
|
||
- **`GameEventFellowshipUpdateFellow` (0x02C0)** — per-fellow incremental.
|
||
- **`GameEventFellowshipDisband` (0x02BF)**.
|
||
- **`GameEventFellowshipQuit` (0x00A3)**.
|
||
- **`GameEventFellowshipDismiss` (0x00A4)**.
|
||
- **`GameEventFellowshipFellowUpdateDone` (0x01C9)** / **`FellowshipFellowStatsDone` (0x01CA)**.
|
||
|
||
### C# port sketch — `FellowshipPanel`
|
||
|
||
```csharp
|
||
public sealed class Fellow
|
||
{
|
||
public uint Guid;
|
||
public string Name;
|
||
public int Level;
|
||
public uint MaxHealth, MaxStamina, MaxMana;
|
||
public uint CurrentHealth, CurrentStamina, CurrentMana;
|
||
public uint CpCached;
|
||
public uint LumCached;
|
||
public bool ShareLoot;
|
||
}
|
||
|
||
public sealed class FellowshipPanel : IPanel
|
||
{
|
||
public string FellowshipName;
|
||
public uint LeaderGuid;
|
||
public bool ShareXP, EvenShare, Open, IsLocked;
|
||
public readonly Dictionary<uint, Fellow> Members = new();
|
||
public readonly Dictionary<uint, Fellow> Departed = new();
|
||
|
||
public bool IAmLeader(uint myGuid) => myGuid == LeaderGuid;
|
||
|
||
public void OnFullUpdate(FellowshipSnapshot snap);
|
||
public void OnUpdateFellow(Fellow f);
|
||
public void OnDisband();
|
||
public void RaiseRecruit(string name);
|
||
public void RaiseDismiss(uint guid);
|
||
public void RaiseQuit();
|
||
public void RaiseToggleShareXP();
|
||
}
|
||
```
|
||
|
||
## 10. Macros panel
|
||
|
||
Retail macros were stored client-side in `~/Documents/Asheron's Call/settings/(character).ppf` or equivalent, not sent through the wire protocol. The decompiled code I scanned did not contain a dedicated macro UI function in the chunks explicitly assigned here; it lives in a higher chunk (around `0x005D`–`0x005F` based on the config file saver strings).
|
||
|
||
### Behaviors (from in-game retail memory)
|
||
|
||
- `@save name` and `@load name` for macros (`L"Please use @help saveui for proper usage."` and `L"Please use @help loadui for proper usage."` are the help prompts, chunk_00570000 lines 208/270).
|
||
- A macro is a short script of actions: text chats, cast spell, use item, delay N seconds.
|
||
- Bound to quickbar slots or standalone hotkeys.
|
||
- Also included "Automation" — auto-select nearest monster and attack.
|
||
|
||
### C# port sketch — `MacrosPanel`
|
||
|
||
This is one of the panels where acdream's plugin API becomes crucial (the "scripting/macros" requirement in CLAUDE.md).
|
||
|
||
```csharp
|
||
public enum MacroStepKind { Chat, CastSpell, UseItem, Delay, Wait, Attack, Emote, Custom }
|
||
|
||
public sealed class MacroStep
|
||
{
|
||
public MacroStepKind Kind;
|
||
public string Text; // for Chat/Emote
|
||
public uint TargetId; // for CastSpell/UseItem
|
||
public TimeSpan Duration; // for Delay/Wait
|
||
public string CustomId; // for plugin-provided step
|
||
}
|
||
|
||
public sealed class Macro
|
||
{
|
||
public string Name;
|
||
public int HotkeyIndex;
|
||
public List<MacroStep> Steps = new();
|
||
}
|
||
|
||
public sealed class MacrosPanel : IPanel
|
||
{
|
||
public readonly Dictionary<string, Macro> Macros = new();
|
||
public void Save(Macro m);
|
||
public void Delete(string name);
|
||
public void Execute(string name, IPluginHost host); // dispatches through plugin API
|
||
}
|
||
```
|
||
|
||
## 11. Options panel
|
||
|
||
**Location**: the options panel writes into `GameActionSetSingleCharacterOption` (see `CharacterOption.cs`). The rendering chunk is around `0x005D`–`0x005F` (config / saved-UI layout chunks), not fully scanned here.
|
||
|
||
### Options available
|
||
|
||
From `CharacterOption.cs`, 55 named options split across **CharacterOptions1** (gameplay) and **CharacterOptions2** (display / filters). Some highlights:
|
||
|
||
| Option | Bit group | Description |
|
||
|--------|-----------|-------------|
|
||
| AutoRepeatAttacks | 1 | Auto-attack |
|
||
| IgnoreAllegianceRequests | 1 | |
|
||
| IgnoreFellowshipRequests | 1 | |
|
||
| IgnoreAllTradeRequests | 1 | |
|
||
| DisableMostWeatherEffects | 1 | Graphics |
|
||
| AlwaysDaylightOutdoors | 2 | Graphics |
|
||
| LetOtherPlayersGiveYouItems | 1 | |
|
||
| KeepCombatTargetsInView | 1 | |
|
||
| Display3dTooltips | 1 | |
|
||
| AttemptToDeceiveOtherPlayers | 1 | |
|
||
| RunAsDefaultMovement | 1 | |
|
||
| StayInChatModeAfterSendingMessage | 1 | |
|
||
| AdvancedCombatInterface | 1 | |
|
||
| AutoTarget | 1 | |
|
||
| VividTargetingIndicator | 1 | |
|
||
| ShareFellowshipExpAndLuminance | 1 | |
|
||
| AcceptCorpseLootingPermissions | 1 | |
|
||
| ShareFellowshipLoot | 1 | |
|
||
| AutomaticallyAcceptFellowshipRequests | 1 | |
|
||
| SideBySideVitals | 1 | UI layout |
|
||
| ShowCoordinatesByTheRadar | 1 | |
|
||
| DisplaySpellDurations | 1 | |
|
||
| DisableHouseRestrictionEffects | 1 | |
|
||
| DragItemToPlayerOpensTrade | 1 | |
|
||
| ShowAllegianceLogons | 1 | |
|
||
| UseChargeAttack | 1 | |
|
||
| UseCraftingChanceOfSuccessDialog | 1 | |
|
||
| ListenToAllegianceChat | 1 | |
|
||
| AllowOthersToSeeYourDateOfBirth | 2 | |
|
||
| AllowOthersToSeeYourAge | 2 | |
|
||
| AllowOthersToSeeYourChessRank | 2 | |
|
||
| AllowOthersToSeeYourFishingSkill | 2 | |
|
||
| AllowOthersToSeeYourNumberOfDeaths | 2 | |
|
||
| DisplayTimestamps | 2 | |
|
||
| SalvageMultipleMaterialsAtOnce | 2 | |
|
||
| ListenToGeneralChat | 2 | |
|
||
| ListenToTradeChat | 2 | |
|
||
| ListenToLFGChat | 2 | |
|
||
| ListenToRoleplayChat | 2 | |
|
||
| AppearOffline | 2 | |
|
||
| AllowOthersToSeeYourNumberOfTitles | 2 | |
|
||
| UseMainPackAsDefaultForPickingUpItems | 2 | |
|
||
| LeadMissileTargets | 2 | |
|
||
| UseFastMissiles | 2 | |
|
||
| FilterLanguage | 2 | |
|
||
| ConfirmUseOfRareGems | 2 | |
|
||
| ListenToSocietyChat | 2 | |
|
||
| ShowYourHelmOrHeadGear | 2 | |
|
||
| DisableDistanceFog | 2 | Graphics |
|
||
| UseMouseTurning | 2 | |
|
||
| ShowYourCloak | 2 | |
|
||
| LockUI | 2 | |
|
||
| ListenToPKDeathMessages | 2 | |
|
||
|
||
### Additional graphics/audio options (retail, not in protocol — client-local)
|
||
|
||
- Resolution + windowed/fullscreen.
|
||
- Draw distance (both landblock chunks loaded and scenery density).
|
||
- Texture detail / vertex lighting / environment lighting toggles.
|
||
- Gamma / contrast.
|
||
- Master volume, music volume, speech volume, effects volume.
|
||
- Radar color theme, UI scale.
|
||
|
||
### Wire drivers
|
||
|
||
- **`GameActionSetSingleCharacterOption`** outbound on toggle.
|
||
- **`GameActionSetCharacterOption1Flag` / `Option2Flag`** bulk.
|
||
- The server persists and replays at login via `PlayerDescription`.
|
||
|
||
### C# port sketch — `OptionsPanel`
|
||
|
||
```csharp
|
||
public sealed class OptionsPanel : IPanel
|
||
{
|
||
public readonly Dictionary<CharacterOption, bool> Options = new();
|
||
|
||
public GraphicsOptions Graphics { get; set; } // client-local
|
||
public AudioOptions Audio { get; set; } // client-local
|
||
|
||
public bool Get(CharacterOption opt) => Options.GetValueOrDefault(opt, false);
|
||
public void Set(CharacterOption opt, bool value); // writes back to server + persists locally
|
||
public void LoadFromDescription(uint options1, uint options2);
|
||
}
|
||
```
|
||
|
||
## 12. Login screen / server select
|
||
|
||
### Login screen layout (from retail behavior)
|
||
|
||
- Title banner (AC logo).
|
||
- Username field (masked).
|
||
- Password field (masked).
|
||
- Server selection dropdown (loaded from a `ServerList.ini`-style file).
|
||
- Play / Create Account / Exit buttons.
|
||
- Error strings: varying dialogs surface rejections from `GameMessageAccountBanned (0xF7C1)`, `CharacterError (0xF659)`.
|
||
|
||
### Wire drivers
|
||
|
||
- Initial **LoginRequest** → **ConnectRequest** → **ConnectResponse** (UDP handshake, see `docs/research/2026-04-12-movement-deep-dive.md` and `holtburger` session code).
|
||
- **`GameMessageCharacterList` (0xF658)** arrives after encrypted handshake — contains the account's 11-slot character list.
|
||
- **`GameMessageServerName` (0xF7E1)** — displayed at top of character select.
|
||
- **`GameMessageAccountBanned` (0xF7C1)** + **`GameMessageBootAccount`** — rejection paths.
|
||
|
||
### C# port sketch — `LoginScreen`
|
||
|
||
```csharp
|
||
public sealed class ServerEntry
|
||
{
|
||
public string Name;
|
||
public string Host;
|
||
public int Port;
|
||
public int Population;
|
||
public bool Online;
|
||
}
|
||
|
||
public sealed class LoginScreen : IPanel
|
||
{
|
||
public List<ServerEntry> Servers;
|
||
public ServerEntry SelectedServer { get; set; }
|
||
public string Username { get; set; }
|
||
public string PasswordSecure { get; set; } // kept in pinned SecureString
|
||
|
||
public event Action<LoginAttempt> OnSubmit;
|
||
public void ShowError(string message);
|
||
}
|
||
```
|
||
|
||
## 13. Character select screen
|
||
|
||
### Layout
|
||
|
||
- Slot-list on left: 11 character slots (paid accounts), 1 free character always unlocked.
|
||
- 3D preview of selected character rotating on a pedestal (server sends appearance + equipped ObjDesc).
|
||
- Play / Create / Delete buttons.
|
||
- Server name at top.
|
||
|
||
### Wire drivers
|
||
|
||
- **`GameMessageCharacterList` (0xF658)** — the list.
|
||
- **`GameMessageCharacterError` (0xF659)** — generic error (name taken, account suspended, etc.).
|
||
- **`GameMessageCharacterDelete` (0xF655)** outbound.
|
||
- **`GameMessageCharacterCreate` (0xF656)** outbound.
|
||
- **`GameMessageCharacterCreateResponse` (0xF643)** server's answer.
|
||
- **`GameMessageCharacterEnterWorldRequest` (0xF7C8)** outbound when Play clicked.
|
||
- **`GameMessageCharacterEnterWorldServerReady` (0xF7DF)** when world server takes over.
|
||
- **`GameMessageCharacterRestore` (0xF7D9)** used for restoring deleted characters within the 1-hour grace window.
|
||
|
||
### Strings seen
|
||
|
||
From chunk_00470000 line 8280-8315 — this is the CREATION screen render, NOT select:
|
||
- `L"Gender: "`
|
||
- `L"Heritage: "`
|
||
- `L"Starting Town: "`
|
||
- Then the attribute preview as documented in panel #2.
|
||
|
||
### C# port sketch — `CharacterSelectScreen`
|
||
|
||
```csharp
|
||
public sealed class CharacterSlot
|
||
{
|
||
public uint Guid;
|
||
public string Name;
|
||
public int Level;
|
||
public int SecondsRemainingToDelete; // for post-delete grace period
|
||
public ObjDescSnapshot Appearance;
|
||
public bool IsFreeSlot;
|
||
}
|
||
|
||
public sealed class CharacterSelectScreen : IPanel
|
||
{
|
||
public string ServerName;
|
||
public List<CharacterSlot> Slots;
|
||
public CharacterSlot Selected;
|
||
|
||
public event Action<uint> OnPlay;
|
||
public event Action<uint> OnDelete;
|
||
public event Action OnCreateNew;
|
||
|
||
public void OnCharacterList(CharacterSlot[] slots);
|
||
public void OnError(string errorMessage);
|
||
}
|
||
```
|
||
|
||
## Cross-panel patterns for acdream
|
||
|
||
Based on the decompilation sweep above, the client uses a small set of recurring abstractions. When porting, implement these once and reuse:
|
||
|
||
1. **`IPanel`** interface with `Bind()` / `Unbind()` / `OnPacketReceived()` hooks. Every panel registers to its relevant `GameMessageOpcode` / `GameEventType` values; the dispatcher routes to them.
|
||
2. **`WidgetLookup`** — resource-ID → widget pointer. Mirrors retail's `FUN_00463c00` pattern. Each panel stores its child widgets in a record type with named fields.
|
||
3. **`RichTextBuilder`** — builds formatted rows of `[label][tab][value]\n` with style tokens. Replaces the `FUN_0040b8f0 / FUN_0046f670 / FUN_00402490` trio.
|
||
4. **`TooltipBuilder`** — sequence `Open → Emit → Close → Anchor`. Replaces `FUN_0042dc80 / FUN_0040b8f0 / FUN_0042cbe0 / FUN_004618a0 / FUN_0042e590`.
|
||
5. **`DragDropRouter`** — single entry point for pick/drop events with a slot-kind dispatch table. The paperdoll's 25-slot switch and inventory grid both route through this.
|
||
6. **`RowList<T>`** (for skills, spells, fellows, allegiance vassals) — generic panel-internal renderer that iterates a filtered collection, emits one row per item, supports sorting/scrolling.
|
||
7. **`PropertyUpdateBus`** — the `PrivateUpdateProperty{Int,Int64,Bool,Float,String,DataID}` family of 0x02CD–0x02D8 messages all go through one bus that panels subscribe to by `PropertyId`. This is cleaner than 6 separate wiring paths.
|
||
|
||
## Summary table — panel ↔ chunk ↔ wire message
|
||
|
||
| Panel | Primary chunk / function | Main wire input |
|
||
|-------|--------------------------|-----------------|
|
||
| Chat window | `chunk_00570000.c` big switch | `ChannelBroadcast 0x0147`, `Tell 0x02BD`, `SystemChat 0xF7E0`, `HearSpeech 0x02BB`, `TurbineChat 0xF7DE` |
|
||
| Character Attributes | `chunk_00470000.c::FUN_0047ba70` + `chunk_005C0000.c::FUN_005c9d70/e10/eb0` | `PrivateUpdateAttribute 0x02E3`, `PrivateUpdateVital 0x02E7`, `PlayerDescription 0x0013` |
|
||
| Skills | shared with Attributes | `PrivateUpdateSkill 0x02DD`, `PrivateUpdateSkillLevel 0x02DF` |
|
||
| Spell bar | `chunk_004C0000.c::FUN_004c68f0 + 004c6500/6680/7c50` | `MagicUpdateSpell 0x02C1`, `MagicRemoveSpell 0x01A8`, enchantment events 0x02C2-8 |
|
||
| Paperdoll | `chunk_004A0000.c::FUN_004a5200 + 004a5fa0` | `ObjDescEvent 0xF625`, `WieldObject 0x0023`, `InventoryPutObjInContainer 0x0022` |
|
||
| Inventory | `chunk_004B0000.c` + `chunk_004E0000.c` + `chunk_00580000.c` | `ObjectCreate 0xF745`, `SetStackSize 0x0197`, `InventoryRemoveObject 0x0024`, `ViewContents 0x0196` |
|
||
| Quickbar | around `chunk_004D0000.c` (input map) | client-local (actions dispatch back to inventory/spell) |
|
||
| Allegiance | not scanned here | `AllegianceUpdate 0x0020`, `AllegianceInfoResponse 0x027C`, `AllegianceLoginNotification 0x027A` |
|
||
| Fellowship | not scanned here | `FellowshipFullUpdate 0x02BE`, `FellowshipUpdateFellow 0x02C0`, `FellowshipDisband 0x02BF` |
|
||
| Macros | not scanned here | client-local |
|
||
| Options | not fully scanned | `GameActionSetSingleCharacterOption` outbound |
|
||
| Login | pre-world chunks | UDP handshake + `ServerName 0xF7E1` |
|
||
| Character select | post-handshake pre-world | `CharacterList 0xF658`, `CharacterError 0xF659` |
|
||
|
||
### What was NOT fully mapped in this sweep
|
||
|
||
Honest gaps to flag for the next research pass:
|
||
|
||
- **Macros panel** — needs a scan of `0x005D`–`0x005F` chunks for the config save/load handlers and macro execution tokenizer.
|
||
- **Options panel rendering** — needs a scan for the actual layout function (where the 55 character options get paired with checkboxes + sliders).
|
||
- **Allegiance tree widget** — the actual tree-layout widget with expand/collapse arrows; it's probably in a chunk adjacent to fellowship.
|
||
- **Fellowship member-list widget** — same; only the data side is in ACE. The render function is in retail's client and I did not find its address in this sweep.
|
||
- **Quickbar widget class offsets** — known it exists from input-map strings but the widget-storage offsets were not identified.
|
||
- **Chat window widget IDs** — the tab-button resource IDs, tab-filter storage offsets, and input-box widget ID need a dedicated pass through the chunk that owns `chat_window` or similar identifier.
|
||
|
||
These gaps are documented here so a follow-up research task can pick them up rather than re-discover them.
|