# Chat log click-to-tag dispatch (retail, Sept 2013 EoR build) Research-only. Source: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (PDB-named pseudo-C) and `docs/research/named-retail/acclient.h` (verbatim retail struct layouts). Every claim below is cited `symbol @ 0xADDRESS`. Binary Ninja's rendering caveats (misleading compare idioms, ~33-char truncated inline strings) are called out inline wherever they bit this investigation. ## TL;DR The suspected anchor `ChatInterface::SetReplyTextInChatBox @ 0x004F4760` is **not** the click handler. It is a keyboard text-replacement macro (`/t `, `/tell `, `reply ` + space → `@tell ,`) wired through `ChatInterface::HandleTextReplacements @ 0x004F50D0`, itself fired from a "character typed" UI broadcast, not a mouse event. It happens to share the `"@tell %s, "` idea with the real click path but is a separate code path with separate (looser) text. The real click path is a generic, polymorphic **tag** system: ``` mouse button up over a UIElement_Text → UIElement_Text::MouseUp @ 0x004694F0 → UIElement_Text::DeterminePositionFromXY @ 0x004688F0 (screen xy → glyph index) → GlyphList::InqGlyph @ 0x00473430 (glyph index → Glyph, incl. m_tag) → TextTag::HandleClick (virtual, vtable+0x14) (dispatch by tag TYPE) TextTag_IIDString::HandleClick @ 0x00478840 → ECM_UI::SendNotice_TextTag_IIDStringClick @ 0x006927C0 → gmMainChatUI::RecvNotice_TextTag_IIDStringClick @ 0x004CCE10 (gate: tag m_type == 0x10000001, chat entry not already focused) → ChatInterface::StartTell @ 0x004F41F0 (writes "@tell , " into the entry, focuses it) ``` Player/speaker names in chat are wrapped by the server/client text formatters in a `:>displayText<\Tell>` markup span (note: closing marker is a **backslash**, `<\Tell>`, not a forward-slash — confirmed from the raw literal at `data_7d0bfc @ 0x007D0BFC` etc.). This markup is used for direct tells, channel "says" lines, `[Fellowship]`, `[Co-Vassals]`, patron/vassal lines, and (per the generic `[%ws] ...` format at `data_7e83e8 @ 0x007E83E8`) ordinary named-channel chat too — i.e. **every** chat line that shows a speaker name embeds the same tag, not just tells. Clicking any of them always opens a **tell**, regardless of which channel the line came from. --- ## 1. Click → glyph → tag resolution ### 1a. Entry point: `UIElement_Text::MouseUp` `UIElement_Text::MouseUp @ 0x004694F0` is registered directly in `UIElement_Text`'s vtable slot for `MouseUp` (confirmed at the vtable dump, e.g. `0079C1A4: MouseUp = UIElement_Text::MouseUp`). The relevant tail, reached only when the mouse-up's button id was previously recorded as mouse-down over this same element (`cond:0`, looked up in `this->m_mouseDownTable` keyed by the button id `arg4`): ```c // UIElement_Text::MouseUp @ 0x004694F0, tail (0x0046959C-0x004695DD) if (eax_4 != 0) // cond:0 — this button's mouse-down WAS on this element { uint32_t eax_6 = UIElement_Text::DeterminePositionFromXY(this, ebp_2, edi_2); Glyph var_24; if (GlyphList::InqGlyph(&this->m_glyphList, eax_6, &var_24) != 0 && var_4 != 0) *(uint32_t*)(*(uint32_t*)var_4 + 0x14)(arg4); // var_4->HandleClick(arg4) Glyph::~Glyph(&var_24); } ``` `ebp_2`/`edi_2` are the mouse position converted to element-local, margin-adjusted coordinates a few lines earlier in the same function: ```c int32_t ebp_2 = ((arg2 - this->m_margL) - UIRegion::GetScreenX0(this)); int32_t edi_2 = ((arg3 - this->m_margU) - UIRegion::GetScreenY0(this)); ``` `arg2`/`arg3` are the raw screen-space mouse coordinates passed down from the UI event system; `m_margL`/`m_margU` are the element's left/top text margins (`UIElement_Text` struct, `acclient.h:53412-53415`). **BN caveat**: `var_4` (the pointer used for the `var_4 != 0` check and the virtual call) is never shown being assigned in the decompiled output — it is almost certainly `var_24.m_tag` after `GlyphList::InqGlyph` copies the found `Glyph` into `var_24` via `Glyph::operator=`, but the copy-into-field step is not visible in this rendering. This is exactly the kind of "misleading compare idiom" the project's BN caveat warns about — flagged rather than silently assumed. The surrounding evidence (struct layout, vtable offset match below) makes this the only coherent reading, but it is not a directly-visible assignment. ### 1b. Screen XY → glyph index: `UIElement_Text::DeterminePositionFromXY` `UIElement_Text::DeterminePositionFromXY @ 0x004688F0`: ```c int80_t UIElement_Text::DeterminePositionFromXY(this, arg2 /*local x*/, arg3 /*local y*/) { UIElement_Text::RecalculateGlyphList(this); // ensure wrapped-line layout is current int32_t scrolledY = this->m_iScrollableY + arg3; // undo vertical scroll offset uint32_t line = 0; GlyphList::FindLineFromY(&this->m_glyphList, scrolledY, &line); // which wrapped line uint32_t lineWidthPx = 0; GlyphList::GetGlyphLineWidth(&this->m_glyphList, line, &lineWidthPx); // that line's pixel width int32_t lineLocalX = (this->m_iScrollableX + arg2) - UIElement_Text::CalcJustification(this, lineWidthPx, 1); // undo h-scroll + justification uint32_t glyphIndex = 0; GlyphList::FindPosFromLineAndPixels(&this->m_glyphList, line, lineLocalX, 1, &glyphIndex); // clamp to end-of-text return min(glyphIndex, this->m_glyphList.m_glyphList._num_elements); } ``` Plain-language: convert the click's local (x, y) into a *scrolled* position by adding back however far the text view has been scrolled; use the scrolled Y to find which **wrapped display line** was clicked (`GlyphList::FindLineFromY @ 0x00472770`); measure that line's pixel width (`GlyphList::GetGlyphLineWidth @ 0x00472930`) so the justification offset (left/center/right alignment, `UIElement_Text::CalcJustification @ 0x00467260`) can be subtracted back out of the X; then walk that line's glyph advances to find which **character index** the X pixel falls on (`GlyphList::FindPosFromLineAndPixels @ 0x004732D0`). The result is a single integer: "the click landed on/before character N of the full (unwrapped) text buffer." ### 1c. Glyph index → Glyph (and its tag): `GlyphList::InqGlyph` `GlyphList::InqGlyph @ 0x00473430`: ```c uint8_t GlyphList::InqGlyph(this, arg2 /*index*/, arg3 /*out Glyph*/) { ListNode* node = this->m_glyphList._head; if (node == 0 || arg2 >= this->m_glyphList._num_elements) return 0; for (int i = 0; i != arg2; i++) { node = node->next; if (node == 0) return 0; } Glyph::operator=(arg3, node); // copy the whole Glyph struct, incl. m_tag return 1; } ``` A straight O(n) linked-list walk (the glyph list is a `List`, not an array) to the Nth glyph, then a struct copy. The `Glyph` layout (`acclient.h:45330-45338`): ```c struct __cppobj Glyph { unsigned __int16 m_data; // the character code int m_width; int m_height; RGBAColor m_color; // per-glyph color, baked in at append time (see §4) Font *m_font; TextTag *m_tag; // non-null only for glyphs inside a <...> tag span }; ``` `m_tag` is set by `Glyph::SetTag @ 0x00474920` while the glyph list is built from raw text (see §1d), and cleared in bulk by `GlyphList::RemoveTextTag @ 0x00472BB0` (walks every glyph, clears any whose `m_tag` matches the tag being removed — used when a tagged span is truncated/deleted from the log). ### 1d. Building tags from markup: `TextTagFactory::MakeTag` Text is appended to a `UIElement_Text` glyph list via `UIElement_Text::InqGlyphs @ 0x00468EA0`. Its char-scan loop treats `<` (0x3C) as the start of a tag span: it accumulates characters up to the matching `>` (0x3E) into a string, then calls `TextTagFactory::MakeTag @ 0x00478480` on that whole inner string (e.g. `"Tell:IIDString:1234:PlayerName"`): ```c TextTag* TextTagFactory::MakeTag(PStringBase const* tagBody) { // tagBody looks like "TypeName:ShapeName:payload..." if (!FindChar(tagBody, ':')) return 0; typeNameStr = substring-before-first-colon; if (EnumMapper::InqEnum(0x18 /*category*/, typeNameStr, &tagType) == 0) return 0; // e.g. "Tell" -> 0x10000001 if (!FindChar(rest, ':')) return 0; shapeNameStr = substring-before-second-colon; // e.g. "IIDString" if (EnumMapper::InqEnum(0x18, shapeNameStr, &shapeId) == 0) return 0; // 1..4 switch (shapeId) { case 1: result = new TextTag_DID(); break; case 2: result = new TextTag_IID(); break; case 3: result = new TextTag_IIDEnum(); break; case 4: result = new TextTag_IIDString(); break; } result->m_type = tagType; // from the FIRST lookup ("Tell" -> 0x10000001) result->m_format = shapeId; result->ParseStartTag(remaining payload); // shape-specific: fills TextTag_IIDString::m_IID/m_string, etc. return result; } ``` (Full disassembly at `docs/research/named-retail/acclient_2013_pseudo_c.txt:132871-133065`; the two-colon split and the two `EnumMapper::InqEnum(..., 0x18, ...)` calls are visible at `0x004784E6-0x00478545` and `0x00478577-0x004785DA`.) The resulting `TextTag*` is stashed on every `Glyph` inside the span via `Glyph::SetTag @ 0x00474920` as `InqGlyphs` walks the display characters between the tag's `>` and its closing `<\...>`. **UNKNOWN — needs a DAT/cdb dump**: `EnumMapper::InqEnum`'s category `0x18` is a data-driven (DAT-resident, likely `client_portal.dat` StringTable/ EnumMapper resource) name↔id table — the code only proves the *mechanism*, not the full roster of type-name strings it accepts. We confirmed "Tell" (→ `m_type == 0x10000001`) and "IIDString" (→ shape id 4, inferred — see § "Other tag types" below) from literal format strings elsewhere in the binary, but did not independently dump the table itself. ### 1e. Vtable-offset proof that `var_4` is the tag and `+0x14` is `HandleClick` The `TextTag` vtable layout, read straight from the four subclass vtable dumps (`docs/research/named-retail/acclient_2013_pseudo_c.txt:959321-959368`): | offset | slot | `TextTag_DID` | `TextTag_IIDString` | `TextTag_IIDEnum` | `TextTag_IID` | |---|---|---|---|---|---| | 0x00 | `__vecDelDtor` | ✓ | ✓ | ✓ | ✓ | | 0x04 | `ParseEndTag` | `TextTag::ParseEndTag` (shared) | shared | shared | shared | | 0x08 | `ParseStartTag` | own | own | own | own | | 0x0C | `BuildEndTag` | `TextTag::BuildEndTag` (shared) | shared | shared | shared | | 0x10 | `BuildStartTag` | `TextTag::BuildStartTag` (shared) | shared | shared | shared | | **0x14** | **`HandleClick`** | **own** | **own** | **own** | **own** | | 0x18 | `BuildStartTagData` | own | own | own | own | `+0x14` is exactly `HandleClick`, confirming `MouseUp`'s `*(uint32_t*)(*(uint32_t*)var_4 + 0x14)(arg4)` is `var_4->HandleClick(arg4)` — a virtual call, i.e. this is dispatched **per concrete tag subclass**, not a single hardcoded action. --- ## 2. What fires on click: generic dispatch, not hardcoded to tells All four subclasses' `HandleClick` do the same shape of thing — forward to a global "notice" (AC's internal pub/sub event system) carrying the tag's type + payload, nothing else: ```c // TextTag_DID::HandleClick @ 0x00478740 ECM_UI::SendNotice_TextTag_DIDClick(this->m_type, this->m_DID.id); // @ 0x006926D0 // TextTag_IID::HandleClick @ 0x00478E80 ECM_UI::SendNotice_TextTag_IIDClick(this->m_type, this->m_IID); // @ 0x00692720 // TextTag_IIDEnum::HandleClick @ 0x00478B40 ECM_UI::SendNotice_TextTag_IIDEnumClick(this->m_type, this->m_IID, this->m_enum); // @ 0x00692770 // TextTag_IIDString::HandleClick @ 0x00478840 ECM_UI::SendNotice_TextTag_IIDStringClick(this->m_type, this->m_IID, &this->m_string); // @ 0x006927C0 ``` So the dispatch **is** generic — any listener can register for any of the four notices and react to any `m_type`. In this build, though, only ONE of the four notices has an actual (non-stub) listener anywhere in the client: - `gmMainChatUI::RecvNotice_TextTag_IIDStringClick @ 0x004CCE10` — real, wired to chat's click-to-tell (see §3). - `NoticeHandler::RecvNotice_TextTag_IIDEnumClick @ 0x006A0240` is declared `__pure` — a pure-virtual stub, no base behavior, and no override for it was found anywhere in this pass. - No function definition for a real `RecvNotice_TextTag_DIDClick` or `RecvNotice_TextTag_IIDClick` override exists anywhere in the pseudo-C file either — every other hit for those names is vtable-slot noise (the decompiler filling unresolved thunk slots with neighboring symbol names; cross-checked, none are real function bodies with those signatures). **Conclusion**: the mechanism is generic (4 tag shapes × arbitrary `m_type` values × arbitrary listeners), but in this Sept 2013 build only the chat-log "clickable speaker name → start a tell" feature is actually wired up end-to-end. `TextTag_DID`/`TextTag_IID`/`TextTag_IIDEnum` exist, parse, and would dispatch correctly if clicked, but nothing in the client reacts to their click notices — **UNKNOWN whether item links / URLs / coordinates use these shapes in later builds or via server-composed text we didn't grep for**; no evidence of them was found in this pass. ### Other tag types found (full roster) | Class | Shape id (inferred) | Ctor | `HandleClick` | Real listener found? | |---|---|---|---|---| | `TextTag_DID` | 1 | `0x00478760` | `0x00478740` | No | | `TextTag_IID` | 2 | `0x00478E60` | `0x00478E80` | No | | `TextTag_IIDEnum` | 3 | `0x00478B20` | `0x00478B40` | No | | `TextTag_IIDString` | 4 | `0x00478860` | `0x00478840` | **Yes** — `gmMainChatUI` | Shape-id-to-class mapping is inferred from the `switch(shapeId){case 1..4}` construction order in `MakeTag` (`0x00478632`/`0x004785E1`/ `0x004785FC`/`0x00478617`) plus the fact that the only shape name we can directly read from format strings ("IIDString") is used everywhere the "Tell" markup appears, which always constructs `TextTag_IIDString`. This is strong circumstantial evidence, not a direct read of the DAT enum table — flagged per the "no guessing" rule. --- ## 3. The prefill itself: `ChatInterface::StartTell` `gmMainChatUI::RecvNotice_TextTag_IIDStringClick @ 0x004CCE10` is the registered listener for `SendNotice_TextTag_IIDStringClick`: ```c void gmMainChatUI::RecvNotice_TextTag_IIDStringClick(this, uint32_t type, uint32_t iid, PStringBase const* name) { if (type == 0x10000001 && ChatInterface::IsTextEntryFocused(this) == 0) ChatInterface::StartTell(this, name); } ``` Two gates: (a) the tag's semantic type must be `0x10000001` — i.e. only "Tell"-markup spans do anything on click, other `m_type` values on an `IIDString` tag (if any exist) are silently ignored here; (b) the chat **entry box must not already have keyboard focus** — if the player is mid-sentence typing something else, clicking a name in the log does nothing (`ChatInterface::IsTextEntryFocused @ 0x004F30A0`, which checks `UIElementManager`'s active/focused element against `this->m_chatEntry`). Note the tag's own `iid` payload (`arg3`/`m_IID`) is read into the parameter list but **never used** by this handler — only the embedded name string matters. `ChatInterface::StartTell @ 0x004F41F0`: ```c void ChatInterface::StartTell(this, PStringBase const* name) { PStringBase text = Formatted(u"@tell %s, ", name); // note: trailing space after comma this->m_chatEntry->vtable->Activate(); this->m_chatEntry->vtable->TakeFocus(); // <-- keyboard focus moves to the chat entry CM_UI::SendNotice_ToggleChatEntry(1); // <-- ensures the chat entry bar is shown UIElement_Text::SetText(this->m_chatEntry, &text); UIElement_Text::MoveCursorToPosition(this->m_chatEntry, /* length of `text` */); UIElement_Text::ClearSelection(this->m_chatEntry); } ``` So, precisely: - **Text placed**: `"@tell , "` — literal `@tell`, a space, the name, a comma, and a **trailing space** (ready to type the message body immediately). - **Focus**: YES, explicitly changed. `Activate()` + `TakeFocus()` move keyboard focus into the chat entry field, and `CM_UI::SendNotice_ToggleChatEntry(1) @ 0x0047A200` broadcasts a notice whose real handler, `ClientUISystem::RecvNotice_ToggleChatEntry @ 0x00564200`, is what shows/expands the chat entry bar if it was currently hidden (confirmed as the one non-stub override among many vtable-slot look-alikes for that notice name). - **Cursor**: placed at the end of the inserted text (right after the trailing space), any prior selection cleared. - Whatever the player had already typed into the box (if it wasn't focused — see the focus gate above) is **replaced outright**, not merged or prepended to. --- ## 4. Hover behavior: color is static per-tag-type, not a hover effect `UIElement_Text::MouseMove @ 0x004695F0` was checked directly — it does **no** glyph/tag lookup at all. It only handles active text-selection dragging (`this->m_bitField & 0x40`) or falls back to the base `UIElement::MouseMove`. `UIElement_Text::GetShouldBeMouseVisible @ 0x00467460` likewise only inspects `this->m_bitField & 5` (an editable/selectable flag), not glyph tags. **No hover-triggered highlight, brightening, or cursor-icon change tied to `Glyph::m_tag` was found anywhere in this pass.** What *is* real, and is presumably what reads as "the name is green," is a **static per-glyph color chosen at glyph-construction time**, based on whether the glyph belongs to an active `0x10000001`-typed tag span. Inside `UIElement_Text::InqGlyphs @ 0x00468EA0`, right after a tag span is opened/continues: ```c // 0x00469084-0x0046908A, ebx_1 = the currently-active TextTag* for this glyph (0 if none) if (ebx_1 == 0 || ebx_1->m_type != 0x10000001) color = &this->m_curFontColor; // offset 0x6A4 on UIElement_Text else color = &this->m_curTagFontColor; // offset 0x6B8 on UIElement_Text // ... color is then baked into the new Glyph's m_color field ``` `UIElement_Text`'s struct (`acclient.h:53392-53420`) confirms two distinct color fields exist: `RGBAColor m_curFontColor;` and `RGBAColor m_curTagFontColor;`, set via `UIElement_Text::SetFontColorHelper(this, attrId, &field, colorIndex)` with **different authored attribute ids** — `0x1B` for `m_curFontColor`, `0x1D` for `m_curTagFontColor` (seen consistently at `UIElement_Text::AppendTextWithFont @ 0x00469D70` and `AppendStringInfoWithFont @ 0x00469DE0`). `SetFontColorHelper @ 0x00466AC0` treats its 4th argument as an **index into an authored color array** (an `InqProperty`-backed attribute, not a raw RGBA value) — so `FontColor` and `TagFontColor` are two independently-authored per-window color tables (almost certainly LayoutDesc-driven, consistent with this project's existing DAT-driven-UI findings), and can hold different colors at the same index. That is the entire "green name" mechanism: **it's baked into the glyph once, from data, when the text is appended — not computed or changed on mouse hover.** `m_curTagFontColor` defaults to `RGBAColor_White` (`0x00468641`) unless a window's layout overrides attribute `0x1D` — chat windows presumably do. **UNKNOWN — needs DAT/runtime inspection**: the actual authored RGBA values (attribute `0x1D`'s color table) live in a LayoutDesc DAT resource, not in code; not dumped in this pass. Cross-reference `claude-memory/reference_retail_chat_colors.md` for retail chat-color ground truth already captured via cdb. --- ## 5. Is `SetReplyTextInChatBox` the click handler? No — keyboard macro only `ChatInterface::SetReplyTextInChatBox @ 0x004F4760` has exactly one caller in the whole binary: `ChatInterface::HandleTextReplacements @ 0x004F50D0`, which tries three "quick reply" expanders in order: ```c void ChatInterface::HandleTextReplacements(this) { if (this->m_chatEntry == 0) return; if (!ChatInterface::SetReplyTextInChatBox(this)) // @ 0x004F4760 — replies to LAST TELLER if (!ChatInterface::SetMonarchReplyTextInChatBox(this)) // @ 0x004F4B70 — replies to monarch ChatInterface::SetPatronReplyTextInChatBox(this); // @ 0x004F4EA0 — replies to patron } ``` `HandleTextReplacements` is itself called from exactly one place: `ChatInterface::ListenToElementMessage @ 0x004F51C0`, `case 0x11` (i.e. `idMessage == 0x12`), gated on `arg2->dwParam1 == 0x20` (ASCII space) and the message originating from the chat entry element: ```c case 0x11: // idMessage == 0x12 if (arg2->pElement == this->m_chatEntry /* decompiled as m_fCurrentOpacity, mis-attributed field */ && arg2->dwParam1 == 0x20) ChatInterface::HandleTextReplacements(this); break; ``` Message id `0x12` is confirmed elsewhere as the "character typed" broadcast: `UIElement_Text::CharacterHandler @ 0x00469B90` ends its non-control-character path with `UIElement::BroadcastElementMessage(this, 0x12, typedChar, 0) @ 0x00469CAF` — `dwParam1` carries the raw character code. So this whole path only fires **while the player is typing into the chat entry box and presses the SPACE bar**, and only after typing one of a small set of recognized prefixes. `SetReplyTextInChatBox` itself: reads the entry's current (trimmed) text, checks whether it starts with `/` or `@`, and if so, matches the first-word substring against known shortcut prefixes (`"t"`/`"te"`-style 1–2 char abbreviations at `data_7c4c70`/`data_7c4c68`, and the literal `u"reply "` at `0x004F4A04`). If matched, it replaces that recognized prefix (leaving anything typed after it in place) with: ```c gmCCommunicationSystem::GetLastTellerName(...) @ 0x00589550 // whoever LAST sent YOU a tell Formatted(u"@tell %hs,", lastTellerName) // note: NO trailing space, %hs = narrow string UIElement_Text::SetText / MoveCursorToPosition / ClearSelection // @ 0x004F4ADC-0x004F4AFA ``` This is a **different string** from the click path's `"@tell %s, "` (no trailing space here; `%hs` explicitly narrow-string-formats `GetLastTellerName`'s `PStringBase*` return, vs. the click path's already-wide `PStringBase` name) — a small but real divergence between the two "start a tell" paths worth preserving if both get ported. `SetMonarchReplyTextInChatBox @ 0x004F4B70` and `SetPatronReplyTextInChatBox @ 0x004F4EA0` are structurally identical, sourcing the name from `GetLastAtMonarchUserName`/`GetLastAtPatronUserName` instead. **Verdict**: `SetReplyTextInChatBox` is a **keyboard-shortcut/text-macro handler only** — triggered by typing a recognized prefix then a space in the chat entry. It shares the "write `@tell Name,` into the entry" idea with the click path but is a wholly separate call chain, keyed off "last person who told me something" global state (`gmCCommunicationSystem::SetLastTeller @ 0x005891A0`, `SetLastTellerName @ 0x00589500`) rather than the specific name embedded in the clicked chat line's tag. It is **not** invoked by, and does not invoke, any part of the click-to-tag chain in §1–§3. A consequence worth flagging for the port: because the click path reads the name baked into that *specific* chat line's tag, clicking an **old** "X tells you" line further up the scrollback still starts a tell to X, even if X is no longer whoever last told you something — whereas the `/t `+space keyboard shortcut always resolves to the single global "last teller," which could be a different person by then. --- ## 6. Where the clickable markup comes from (bonus — answers "why does this reproduce on so many message types") The `:>displayText<\Tell>` span is built by `sprintf`-style formatting at multiple sites, not just for direct tells. Representative literals (some inline strings are BN-truncated at ~33 chars, marked `…`): | Format string (verbatim where fully visible) | Address | Used for | |---|---|---| | `"%s<\Tell> tells you, \"%s\"\n"` | `data_7d0ec0 @ 0x007D0EC0` | direct tell received | | `"%s<\Tell> says, \"%s\"\n"` | `data_7d0e60 @ 0x007D0E60` | local/say-range speech | | `"[Fellowship] %s<\\Tell> says, \""` | `data_7d0cdc @ 0x007D0CDC` | fellowship chat | | `"[Co-Vassals] %s<\\Tell> says, \""` | `data_7d0bfc @ 0x007D0BFC` | co-vassal chat | | `"[Allegiance Broadcast] %s<\\Tell> says, \""` | `data_7d0c30 @ 0x007D0C30` | allegiance broadcast | | `"Your patron %s<\\Tell> says to you, \""` | `data_7d0d10 @ 0x007D0D10` | patron chat | | `"Your vassal %s<\\Tell> says to you, \""` | `data_7d0d4c @ 0x007D0D4C` | vassal chat | | `"Your follower %s<\\Tell> says to you, \""` | `data_7d0ca0 @ 0x007D0CA0` | follower chat | | `"[%ws] %ws<\Tell> says, \"%ws\""` | `data_7e83e8 @ 0x007E83E8` | generic named channel say (channel name in `[%ws]`) | Confirms the closing marker is a literal backslash `<\Tell>` in the raw string data (not the HTML-style `` one might assume), and that the non-direct-tell variants hardcode the `iid` field to `0` (the click handler ignores `iid` anyway, so this has no functional effect — but it means the tag's `m_IID` is meaningless/decorative for anything except direct tells). The gating logic for direct tells (`docs/research/named-retail/acclient_2013_pseudo_c.txt:382537-382553`, `gmCCommunicationSystem`-adjacent code around `0x00571880`) only emits the tagged, clickable form when the sender's id falls in the player-character GUID range (`0x50000001`-`0x6FFFFFFF`); tells attributed to ids outside that range fall back to the plain, non-clickable `"%s tells you, \"%s\"\n"` format (`0x00571880` false branch) — so non-player "tells" (system/GM broadcast-as-tell, etc.) are never clickable. --- ## Open gaps (explicitly not resolved here) - **UNKNOWN**: the full roster of `EnumMapper` category `0x18` type-name strings (only "Tell" and, by strong inference, "IIDString" are confirmed). A DAT dump or a cdb breakpoint on `EnumMapper::GetString` with category `0x18` would enumerate the rest and settle whether item links/URLs/coordinates exist as other `m_type` values on the same `IIDString` shape, or as `DID`/`IID`/`IIDEnum` shapes instead. - **UNKNOWN**: authored RGBA values behind `m_curFontColor`/ `m_curTagFontColor` attribute ids `0x1B`/`0x1D` for the chat log window specifically — lives in a LayoutDesc DAT resource, not code. - **Confirmed absence, not a gap**: no hover-only visual/cursor change was found for tagged glyphs in this build (`MouseMove`, `GetShouldBeMouseVisible` both checked directly and neither look at `Glyph::m_tag`).