Six parallel research lanes on retail's chat text and window behaviour, plus a plan. The headline: the green clickable speaker name is not a chat feature and not a colour, it is a missing capability in the TEXT stack. Retail's client sprintfs literal tag markup into the chat line, and the text element parses the brackets while appending, attaching a ref-counted tag PER GLYPH. A tagged run is emergent: adjacent glyphs whose tag pointers are equal. A glyph takes the tag colour (property 0x1D) only when a tag is open and its type is 0x10000001; otherwise the ordinary line colour (0x1B). The colour itself was the one thing the decomp could not settle — it is authored, not runtime-built — so it was MEASURED out of the installed dats rather than assumed from a screenshot: P0x1D = RGB(0,178,0). That also exposed a trap: the tag colour is per-ELEMENT and authored while the line colour on the same element comes from the runtime chat table, so filing "tag green" into the LogTextType table would put it in the wrong place. Our own audit found the gap is narrower than feared. UiText ALREADY draws multi-coloured runs (the character stat panel uses it); the path is just gated to single-line elements. The draw path needs no renderer work, and HitChar already resolves a click to line+column. The real blocker is that sender identity is destroyed before it reaches the renderer: ChatEntry carries Sender/SenderGuid the whole way, and ChatVM.RecentLinesDetailed drops both. Two findings beyond the original question. Retail BOUNDS its transcript (10,000 chars, trimmed to ~7,500 at a newline) and splits auto-scroll from an unread indicator by sampling "was at bottom" before the line lands — a naive port auto-scrolls forever and leaks for the life of a session. And the chat-UI audit turned up an untracked bug: Escape in the chat input does nothing at all, because UiField has no Escape case and a focused field also suppresses the input dispatcher's fallback. Every lane was instructed to write "UNKNOWN — needs X" rather than guess, and they did; the carried unknowns are listed in the plan rather than papered over. Seven slices proposed, nothing implemented yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
26 KiB
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 <LastTeller>,) 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 <Name>, " into the entry, focuses it)
Player/speaker names in chat are wrapped by the server/client text
formatters in a <Tell:IIDString:<iid>:<name>>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] <Tell:IIDString:0:%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):
// 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:
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:
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:
uint8_t GlyphList::InqGlyph(this, arg2 /*index*/, arg3 /*out Glyph*/)
{
ListNode<Glyph>* 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<Glyph>, not an
array) to the Nth glyph, then a struct copy. The Glyph layout
(acclient.h:45330-45338):
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"):
TextTag* TextTagFactory::MakeTag(PStringBase<unsigned short> 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:
// 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 @ 0x006A0240is 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_DIDClickorRecvNotice_TextTag_IIDClickoverride 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:
void gmMainChatUI::RecvNotice_TextTag_IIDStringClick(this, uint32_t type, uint32_t iid, PStringBase<unsigned short> 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:
void ChatInterface::StartTell(this, PStringBase<unsigned short> const* name)
{
PStringBase<unsigned short> 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 <PlayerName>, "— 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, andCM_UI::SendNotice_ToggleChatEntry(1) @ 0x0047A200broadcasts 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:
// 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:
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:
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:
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<char>* return, vs. the click path's
already-wide PStringBase<unsigned short> 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 <Tell:IIDString:<iid>:<name>>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 |
|---|---|---|
"<Tell:IIDString:%d:%s>%s<\Tell> tells you, \"%s\"\n" |
data_7d0ec0 @ 0x007D0EC0 |
direct tell received |
"<Tell:IIDString:%d:%s>%s<\Tell> says, \"%s\"\n" |
data_7d0e60 @ 0x007D0E60 |
local/say-range speech |
"[Fellowship] <Tell:IIDString:0:%s>%s<\\Tell> says, \"" |
data_7d0cdc @ 0x007D0CDC |
fellowship chat |
"[Co-Vassals] <Tell:IIDString:0:%s>%s<\\Tell> says, \"" |
data_7d0bfc @ 0x007D0BFC |
co-vassal chat |
"[Allegiance Broadcast] <Tell:IIDString:0:%s>%s<\\Tell> says, \"" |
data_7d0c30 @ 0x007D0C30 |
allegiance broadcast |
"Your patron <Tell:IIDString:0:%s>%s<\\Tell> says to you, \"" |
data_7d0d10 @ 0x007D0D10 |
patron chat |
"Your vassal <Tell:IIDString:0:%s>%s<\\Tell> says to you, \"" |
data_7d0d4c @ 0x007D0D4C |
vassal chat |
"Your follower <Tell:IIDString:0:%s>%s<\\Tell> says to you, \"" |
data_7d0ca0 @ 0x007D0CA0 |
follower chat |
"[%ws] <Tell:IIDString:0:%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 </Tell> 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
EnumMappercategory0x18type-name strings (only "Tell" and, by strong inference, "IIDString" are confirmed). A DAT dump or a cdb breakpoint onEnumMapper::GetStringwith category0x18would enumerate the rest and settle whether item links/URLs/coordinates exist as otherm_typevalues on the sameIIDStringshape, or asDID/IID/IIDEnumshapes instead. - UNKNOWN: authored RGBA values behind
m_curFontColor/m_curTagFontColorattribute ids0x1B/0x1Dfor 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,GetShouldBeMouseVisibleboth checked directly and neither look atGlyph::m_tag).