The chat deep-dive proved the MECHANISM behind retail's green clickable
speaker name — a glyph tag coloured from property 0x1D rather than the line's
own 0x1B — but not the colour itself: BuildChatColorLookupTable @0x004F31C0
builds only the 0x1B array, so the value is authored rather than runtime-built
and the research correctly returned "UNKNOWN" instead of assuming the green in
a screenshot.
--colors prints the 0x1B/0x1D arrays of every element in a layout, which
measures it out of the installed dats:
chat 0x2100006F, transcript 0x10000011
P0x1B [0x00] R=204 G=204 B=204
P0x1D [0x00] R= 0 G=178 B= 0 <- the green
Recorded in the research note, including the trap it exposes: the tag colour is
per-ELEMENT and authored, while the line colour on that same element comes from
the runtime chat table. Filing "tag green" into the LogTextType colour table
would put it in the wrong place entirely.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
25 KiB
Retail chat: how a tagged, coloured player-name run gets composed
Research-only. No source files modified. All claims cite symbol @ 0xADDRESS
in docs/research/named-retail/acclient_2013_pseudo_c.txt (pseudo-C) or
docs/research/named-retail/acclient.h (verbatim retail struct defs) unless
otherwise noted. Addresses without an explicit file are in the pseudo-C dump.
TL;DR — the mechanism in one paragraph
Retail does not use StringInfo's two-colour-argument mechanism
(RecvNotice_DisplayFinalStringInfo's arg3/arg4) to colour the player
name differently from the rest of the sentence. StringInfo is only a
localization/variable-substitution template (string-table id + named
variables, or a literal override) — it has no colour or tag fields at all.
Instead, the sender's name is delivered as a literal inline markup tag,
<Tell:IIDString:<GUID>:<Name>>Name<\Tell>, baked directly into the plain
wide-char sentence before it is ever handed to the UI. When that sentence
is appended to the chat log, UIElement_Text's glyph-list builder
(UIElement_Text::InqGlyphs @ 0x00468ea0) recognizes the <...> markup,
asks TextTagFactory::MakeTag @ 0x00478480 to parse it into a
TextTag_IIDString object (GUID + name), and colours every individual
Glyph inside the tagged span from m_curTagFontColor (font-color
property 0x1D) instead of the line's m_curFontColor (property 0x1B) —
but only if the tag's type is the "Tell" enum value 0x10000001. Each
Glyph also carries a TextTag* pointer, which is what makes the run
clickable and lets a click resolve back to the right player.
1. Where the sender name becomes a tagged run
1a. The literal tag text is baked in at message-composition time
Two adjacent handlers on ClientCommunicationSystem build the chat line
for incoming speech, and both embed the markup directly via sprintf,
before any StringInfo/UI code runs:
- Local/overheard speech —
ClientCommunicationSystem::Handle_Communication__HearSpeech @ 0x005712a0:
005714f5 if ((arg4 < 0x50000001 || arg4 > 0x6fffffff))
005714f5 PStringBase<char>::sprintf(&s_NullBuffer_2, "%s says, \"%s\"\n"); // no tag
005714f5 else
00571511 PStringBase<char>::sprintf(&s_NullBuffer_2, "<Tell:IIDString:%d:%s>%s<\Tell> says, \"%s\"\n");
(full literal recovered from the constant pool: data_7d0e60 @ 0x007d0e60
= "<Tell:IIDString:%d:%s>%s<\\Tell> says, \"%s\"\n", since Binary
Ninja's inline preview truncates at ~33 chars).
- Direct tell —
ClientCommunicationSystem::Handle_Communication__HearDirectSpeech @ 0x005715a0, same shape:
00571880 if ((arg4 < 0x50000001 || arg4 > 0x6fffffff))
00571880 PStringBase<char>::sprintf(&arg5, "%s tells you, \"%s\"\n"); // no tag
00571880 else
0057189c PStringBase<char>::sprintf(&arg5, "<Tell:IIDString:%d:%s>%s<\Tell> tells you, \"%s\"\n");
(full literal: data_7d0ec0 @ 0x007d0ec0 =
"<Tell:IIDString:%d:%s>%s<\\Tell> tells you, \"%s\"\n").
%d = arg4, the speaker's actual object GUID from the wire message.
%s (first) = the speaker's display name (repeated once inside the tag
payload, once again as the visible glyph text after the >).
The 0x50000001..0x6FFFFFFF GUID-range gate is load-bearing: only
senders whose object id falls in that range get the clickable/coloured
treatment at all. This is AC1's dynamic-object id range (players and other
non-static weenies); ids outside it (system/NPC broadcast cases handled
elsewhere) fall through to the plain, untagged "%s says/tells... format
with no markup and no special colour.
Group/channel broadcasts go through a separate builder,
ChatRoomTracker::GetChatFormat @ 0x005cd7c0 (called from
gmCCommunicationSystem::uiChatInterfaceProvider::OnSendToRoom @ 0x0058a590,
the TurbineChat room-message handler), which always uses IID:0 in the tag
(no real object id is available/needed there) and prepends the channel
name, e.g. for General chat:
005cd93a ebx = 0x1b; // LogTextType = General
005cd93f var_1c_14 = &ChannelSystem::General_GlobalChannelName;
005cd85c sprintf(&s_NullBuffer_2, "[%ws] <Tell:IIDString:0:%ws>%ws<\\Tell> says, \"%ws\"");
(full literal: data_7e83e8 @ 0x007e83e8 =
"[%ws] <Tell:IIDString:0:%ws>%ws<\\Tell> says, \"%ws\""). The function
returns a ChatDisplayInfo{ m_ltt (LogTextType), m_display (the whole sprintf'd string), m_doDisplayText } and the caller passes m_display and
m_ltt straight into ClientSystem::AddTextToScroll. Similar hard-coded
IID:0 tag formats exist for Fellowship broadcast ("[Fellowship] <Tell:IIDString:0:%s>%s<\\Tell> says, \"",
data_7d0cdc @ 0x007d0cdc), Co-Vassals (data_7d0bfc @ 0x007d0bfc),
Allegiance Broadcast (data_7d0c30 @ 0x007d0c30), patron/vassal/follower
tells (data_7d0d10, data_7d0d4c, data_7d0ca0).
BN-truncation note: every one of the sprintf(..., "<Tell:IIDString:...")
call sites above showed only a …-truncated ~33-char preview in the raw
decompiler output. The full strings quoted here were recovered from the
named constant-pool entries (data_7d0bfc, data_7d0c30, data_7d0c88,
data_7d0ca0, data_7d0cdc, data_7d0d10, data_7d0d4c, data_7d0e60,
data_7d0ec0, data_7e83e8), which the pseudo-C dump prints verbatim
elsewhere in the file as global char const data_XXXXXXXX[N] = "...", 0
declarations — not guessed.
1b. The plain sprintf'd string reaches the chat window unmodified
ClientSystem::AddTextToScroll @ 0x00487fc0 (char* overload) widens to
UTF-16 and tail-calls the wide overload @ 0x00563c50, which wraps the
already-composed text (markup and all) in a literal-override
StringInfo and forwards it to the notification bus:
00563ee9 StringInfo::SetLiteralValue(&var_890, &m_charbuffer_3 /* full "<Tell:...>...\"" text */, 1);
00563f05 StringInfo::SetLiteralValue(&var_920, &s_NullBuffer_4 /* timestamp string, or empty */, 1);
00563f2b ECM_UI::SendNotice_DisplayFinalStringInfo(arg3 /* LogTextType/colour index */, &var_890, &var_920, arg5 /* window id */);
This confirms StringInfo here is used purely as a transport wrapper
around an already-fully-formed literal string — StringInfo::SetLiteralValue
sets m_Override = 1 (literal) so StringInfo::GetString later just
returns the wrapped text verbatim; no template/variable substitution
happens for chat lines built this way. (See §4 for why StringInfo cannot
itself be the tag carrier.)
ChatInterface::RecvNotice_DisplayFinalStringInfo @ 0x004f4640 (the
override registered on ChatInterface) receives this notice:
004f46dc if (StringInfo::IsValid(arg4, 1) != 0)
004f46e9 UIElement_Text::AppendStringInfoWithFont(this->m_chatLog, arg4, 0, 0xc); // arg4 = timestamp StringInfo, colour index 0x0C
004f46fc UIElement_Text::AppendStringInfoWithFont(this->m_chatLog, arg3, 0, arg2); // arg3 = the message StringInfo, colour index arg2 (LogTextType)
arg4 (the second StringInfo, appended first, at the fixed colour index
0x0C) is the timestamp prefix ("HH:MM:SS ", built a few lines
earlier in AddTextToScroll from PlayerModule::DisplayTimeStamps +
wcsftime), not a channel-name prefix. arg3 (colour index arg2 = the
caller-supplied LogTextType) is the entire rest of the line, channel
prefix and all — see §3 for why that resolves the "0x0C is grey" question.
1c. AppendStringInfoWithFont resolves the literal text and hands it to the glyph parser
UIElement_Text::AppendStringInfoWithFont @ 0x00469de0:
00469df4 UIElement_Text::SetFontDIDHelper(this, 0x1a, &this->m_curFontObj, arg3);
00469e09 UIElement_Text::SetFontColorHelper(this, 0x1b, &this->m_curFontColor, arg4); // line colour, keyed by LogTextType index arg4
00469e1a UIElement_Text::SetFontColorHelper(this, 0x1d, &this->m_curTagFontColor, arg4); // tag colour, same index
00469e2a eax_1 = StringInfo::GetString(arg2, &arg3, 0); // resolves literal-override text verbatim
00469e44 UIElement_Text::AddText_Internal(this, m_charbuffer, 3);
m_curFontColor and m_curTagFontColor are named fields on
UIElement_Text (docs/research/named-retail/acclient.h:53408,53410):
struct __cppobj __declspec(align(8)) UIElement_Text : UIElement_Scrollable, CInputHandler
{
...
RGBAColor m_curFontColor;
Font *m_curFontObj;
RGBAColor m_curTagFontColor;
...
};
So before any markup parsing happens, the widget primes two colours
for the whole append call — the base line colour and a separate tag
colour — both looked up via the exact same LogTextType-indexed mechanism
(UIElement_Text::SetFontColorHelper @ 0x00466ac0, which does
InqProperty(propId) → indexed-array element at [arg4]).
1d. The glyph-list builder recognizes <...> and creates the TextTag
UIElement_Text::InqGlyphs @ 0x00468ea0 (the routine AddText_Internal
uses to turn the resolved wide string into Glyph records) scans
char-by-char; on finding < it captures through the matching > and
calls the tag factory:
00469021 int32_t eax_16 = TextTagFactory::MakeTag(); // parses the whole "<Tell:IIDString:GUID:Name>" span
00469084 if (ebx_1 == 0 || tag->m_type != 0x10000001)
00469084 edx_15 = <UIElement_Text + 0x6a4>; // this->m_curFontColor (RGBAColor field order matches struct above)
00469084 else
0046908a edx_15 = <UIElement_Text + 0x6b8>; // this->m_curTagFontColor
004690c5 Glyph::Glyph(&esp_1[9]); // constructs the glyph with the chosen colour + tag pointer
(Note: this function is heavily register/stack-mangled in Binary Ninja's
output — the raw operand forms above are paraphrased from the literal
esp/ecx chains in the dump, not verbatim BN text, because the BN
pseudo-C here reads as raw stack-slot arithmetic rather than named field
accesses. The two struct-offset destinations (+0x6a4, +0x6b8) are
0x14 bytes apart, matching an RGBAColor (16 bytes) + Font* (4 bytes)
gap between m_curFontColor and m_curTagFontColor in the struct dump
above — consistent with, but not a byte-for-byte confirmed alias of, those
two named fields.)
Glyph itself carries per-character colour and tag
(docs/research/named-retail/acclient.h:45330):
struct __cppobj Glyph
{
unsigned __int16 m_data; // character code
int m_width;
int m_height;
RGBAColor m_color; // per-glyph colour — this is what makes the name a different colour from the rest of the line
Font *m_font;
TextTag *m_tag; // non-null only for glyphs inside a <Tag:...>...<\Tag> span — this is what makes it clickable
};
This is the answer to "who creates the tagged run": TextTagFactory::MakeTag @ 0x00478480,
called from UIElement_Text::InqGlyphs @ 0x00468ea0 while it walks the
resolved plain-text string looking for <...> markers. It is a markup
parser operating on plain text, not a StringInfo/variable mechanism.
TextTagFactory::MakeTag @ 0x00478480 itself:
- Confirms the captured span starts with
<and ends with>. - Splits on the first
:— the text before it (e.g."Tell") is looked up viaEnumMapper::InqEnum(name, 0x18, &m_type)(a DAT-driven string→enum table, category0x18) to get the numeric tag type (this->m_type, e.g.0x10000001for"Tell"). - Splits again on the next
:— the text between them (e.g."IIDString") is looked up the same way to get a small class selector (var_18, 1–4), which aswitchuses to instantiate the rightTextTagsubclass:1→TextTag_DID2→TextTag_IID3→TextTag_IIDEnum4→TextTag_IIDString(jump tablejump_table_478728 @ 0x00478728, case4 @ 0x00478617)
- Calls the new tag's virtual
ParseStartTagon the remaining payload text (everything after the second:, i.e."<GUID>:<Name>").
2. Tag payload — what a click needs to address the right player
struct TextTag_IIDString : TextTag { unsigned int m_IID; PStringBase<unsigned short> m_string; }
(docs/research/named-retail/acclient.h:53960), with the base class
struct TextTag : ReferenceCountTemplate<1048576,0> { unsigned int m_type; unsigned int m_format; }
(docs/research/named-retail/acclient.h:45358).
TextTag_IIDString::ParseStartTag @ 0x00478910 fills it in:
00478946 if (PStringBaseIter_Common<unsigned short>::FindChar(&iter, ':', 0) != 0) // find the FIRST ':' in "GUID:Name"
00478a02 if (PSUtils::is_uint32(leftPart) != 0) // left of ':' must parse as a uint32
004788ad this->m_IID = PStringBase<unsigned short>::to_uint32(&leftPart); // -> numeric object GUID
00478ae2 PStringBase<unsigned short>::operator=(&this->m_string, &rightPart); // -> display name text
So the payload is both the numeric object id and the display name
string, not just one or the other. TextTag_IIDString::BuildStartTagData @ 0x004788e0
is the inverse (serializes back to "0x%08X:%ls"), confirming the same
two-field shape round-trips.
Click resolution (TextTag_IIDString::HandleClick @ 0x00478840):
0047884c ECM_UI::SendNotice_TextTag_IIDStringClick(this->m_type, this->m_IID, &this->m_string);
which is picked up by gmMainChatUI::RecvNotice_TextTag_IIDStringClick @ 0x004cce10:
004cce1b if (arg2 == 0x10000001 && ChatInterface::IsTextEntryFocused(this) == 0) // arg2 = tag->m_type ("Tell")
004cce2d ChatInterface::StartTell(this, arg4); // arg4 = tag->m_string — starts a /tell using the NAME, not the GUID
So in the one client-side consumer we traced, the actual action
(StartTell) only uses the name string, even though the tag also
carries the numeric GUID. The GUID is transmitted through the notice
(arg3) but this handler doesn't consume it — it may be used by other,
untraced RecvNotice_TextTag_IIDStringClick overrides (several other UI
classes register the same override — see the vtable-slot list in the
pseudo-C dump around 0x0079e580 onward — only gmMainChatUI's and the
base NoticeHandler::RecvNotice_TextTag_IIDEnumClick fallback were read in
this pass). UNKNOWN — needs a scan of every other RecvNotice_TextTag_IIDStringClick
override if a consumer that actually resolves by GUID matters for
acdream's design (e.g. distinguishing two players who changed names, or a
"select in world" action).
3. The [General] channel prefix — colour, and resolving the 0x0C puzzle
Resolved: the apparent "0x0C is grey" contradiction was a mis-identification
on my part before tracing the code, not a real contradiction. Index 0x0C
is not the channel-prefix colour — it's hard-coded in
ChatInterface::RecvNotice_DisplayFinalStringInfo @ 0x004f4640 (§1b) as the
colour for the timestamp StringInfo (arg4), which is entirely
separate from the channel-prefixed message text (arg3). Confirmed against
ChatInterface::BuildChatColorLookupTable @ 0x004f31c0 (see below): index
0x0C's colour is colorGrey — exactly matching the existing project
note. It's grey because it's the timestamp, not because it's "[General]".
"[General]" and the rest of the line (says, "...", including the
embedded name tag's line-colour-before-override) share one LogTextType
value for the whole assembled string — set in
ChatRoomTracker::GetChatFormat @ 0x005cd7c0 (§1a):
005cd93a ebx = 0x1b; // General
005cd949 ebx = 0x1c; // Trade
005cd95c ebx = 0x1d; // LFG
005cd96d ebx = 0x1e; // Roleplay
005cd97e ebx = 0x12; // Olthoi
005cd9b8 ebx = 0x20; // Society (all variants)
ChatInterface::BuildChatColorLookupTable @ 0x004f31c0 builds one
LogTextType-indexed colour array (BaseProperty at property id 0x1B,
34 entries, indices 1..0x22) applied to this->m_chatLog. It defaults
every index to colorGreen @ 0x81c578 and then overrides ~27 of them.
Reading the override sequence address-by-address against the named
RGBAColor globals in the constant pool (0x81c4a8..0x81c598, each
printed with a name, e.g. class RGBAColor colorGrey = { r=0.824 g=0.824 b=0.784 a=1 }) gives this full index→colour table:
| LogTextType index | Colour name | RGBA |
|---|---|---|
| 2 | colorWhite | (1, 1, 1, 1) |
| 0x0C | colorGrey | (0.824, 0.824, 0.784, 1) — timestamp, not General |
| 3, 0xA, 0x13, 0x1F | colorYellow | (1, 1, 0.247, 1) |
| 4, 0xB | colorTan | (0.824, 0.824, 0.392, 1) |
| 5 | colorBrightPurple | (1, 0.498, 1, 1) |
| 6, 0xF, 0x15 | colorDarkRed | (1, 0.247, 0.247, 1) |
| 7, 0x11 | colorLightBlue | (0.247, 0.749, 1, 1) |
| 8, 9 | colorPink | (1, 0.588, 0.588, 1) |
| 0xD | colorCyan | (0.247, 0.863, 0.863, 1) |
| 0xE, 0x1B (General), 0x1C (Trade), 0x1D (LFG), 0x1E (Roleplay), 0x20 (Society) | colorBlueGrey | (0.706, 0.863, 0.941, 1) |
| 0x16 | colorLightRed | (0.96, 0.459, 0.447, 1) |
| 0x12 (Olthoi) , 0x21 | colorOrange | (0.933, 0.573, 0.118, 1) |
| 0x1A | colorBrightRed | (1, 0, 0, 1) |
| everything else (1, 0x10, 0x14, 0x17, 0x18, 0x19, 0x22) | colorGreen (default, unoverridden) | (0.5, 1, 0.498, 1) |
So General/Trade/LFG/Roleplay/Society chat all render in the same pale
blue-grey (colorBlueGrey) as their base line colour — "[General]"
and says, "..." are the same colour. This cross-checks cleanly against
the project's existing claude-memory/reference_retail_chat_colors.md
(same named constants, same addresses, independently dumped live via cdb
on 2026-06-16): its colorWhite→LocalSpeech, colorBrightPurple(index
5)→Tell, colorLightRed(index 0x16)→Combat, and colorGrey(index
0x0C)→"Emote/SoulEmote/fallback" mappings all match the indices found here
exactly. That memory doc's "Channel"→colorLightBlue guess (its own text
flags this mapping as an unverified nearest-match, "the rare kinds map
to the nearest named color... wasn't traced") is superseded by the exact
trace above: the built-in text channels are colorBlueGrey, not
colorLightBlue (colorLightBlue is indices 7 and 0x11, whose LogTextType
names weren't identified in this pass — UNKNOWN, would need the
DAT-driven LogTextTypeEnumMapper string table to name every index; see §5).
Net effect for the screenshot in the prompt: "[General] <name> says, ..." is
two colours, not three — the whole line (brackets, "says,", the
quoted message) in colorBlueGrey, and the name span in whatever
m_curTagFontColor resolves to (see §5) wherever the tag's type is
"Tell" (0x10000001). If the user's read genuinely showed three
distinguishable hues, the third one is not explained by anything traced in
this pass — flag as UNKNOWN, possibly a rendering/outline-colour effect
(m_curOutlineColor, also a field on UIElement_Text, untraced here) or
a visual misread of anti-aliasing against the grey timestamp prefix.
4. Is StringInfo the tag carrier?
No. Traced its full field set from the constructor/accessor bodies
(StringInfo::StringInfo @ 0x0042da60, ::Reset @ 0x0042daf0,
::IsValid @ 0x0042cbe0, ::AddVariable_Int/UInt/Float/String/StringInfo @ 0x0042dde0-0x0042e7d0): m_Override (0=table-driven / 1=literal /
2=?), m_stringID, m_tableID, m_strToken, m_LiteralValue,
m_strEnglish, m_strComment, and m_variables (an
IntrusiveHashTable<name, StringInfoData*> for named-variable
substitution into a localized template). None of these are colour, tag,
or link fields — StringInfo is purely a localization envelope
(string-table id + substitution variables, or a raw literal override via
SetLiteralValue). Tagging is applied after the envelope is unwrapped:
StringInfo::GetString (called inside AppendStringInfoWithFont @ 0x00469de0, §1c) resolves the final plain wide string, and that plain
string is what UIElement_Text::AddText_Internal/InqGlyphs scans for
<...> markup. So the two systems are cleanly separated: StringInfo
answers "what text, and in what language", the glyph-list builder answers
"does any of this text contain clickable/differently-coloured spans".
5. Which colour does the tagged name actually use?
Structurally proven, exact value UNKNOWN. §1c/1d prove the mechanism:
UIElement_Text::SetFontColorHelper(this, 0x1D, &m_curTagFontColor, arg4) @ 0x00469e1a looks up property 0x1D through the identical
indexed-array-by-LogTextType path as property 0x1B (line colour) — see
UIElement_Text::SetFontColorHelper @ 0x00466ac0, which does
InqProperty(propId) → array bounds check (arg4 < count) → indexed element copy. And UIElement_Text::InqGlyphs @ 0x00468ea0 proves the use: a
glyph gets m_curTagFontColor instead of m_curFontColor specifically
when its enclosing tag's m_type == 0x10000001 (the "Tell" tag-name enum
value, resolved via the DAT-driven EnumMapper category 0x18 — see §1d
step 2). Tags of any other type (e.g. IIDEnum-based links used
elsewhere in the client) are still clickable (non-null Glyph.m_tag) but
render in the ordinary line colour — the green/special-colour behaviour is
specific to Tell-type name links, not "any markup tag."
What I could not find: a second BuildXxxColorLookupTable-style
function that populates property 0x1D's array the way
ChatInterface::BuildChatColorLookupTable @ 0x004f31c0 populates 0x1B
(that function only ever calls SetPropertyName(&var_18, 0x1b) once, and
the whole function body — read start to end — only ever writes to that one
array before the final this->m_chatLog->vtable->SetProperty(&var_18)).
Two explanations are consistent with what's traced and neither is
confirmed:
- Property
0x1Dis authored directly on the chat-logUIElement_Textwidget via itsLayoutDesc(a per-widget default, not somethingChatInterfacecode builds at runtime) — plausible sinceSetFontColorHelper'sInqPropertycall would find any property the widget inherits, not just onesBuildChatColorLookupTablewrote. - A second, unlocated runtime builder populates it elsewhere.
UNKNOWN — needs either: (a) a LayoutDesc/DAT dump of the chat-log
window's property 0x1D (or its default RGBAColor), or (b) a live cdb
breakpoint on UIElement_Text::SetFontColorHelper with arg2==0x1D while
a real Tell-tagged line renders, reading this->m_curTagFontColor after
the call returns (same toolchain as claude-memory/reference_retail_chat_colors.md's
x acclient!color* / dd recipe). The user's screenshot reads it as
green, and AC's clickable-name convention is widely remembered as green,
but that is not something this pass proved from decomp — flagging it
as inferred-from-screenshot/prior-knowledge, not decomp-verified.
Open items / follow-ups
- §2: only one
RecvNotice_TextTag_IIDStringClickoverride (gmMainChatUI) was traced for click behaviour; others exist (vtable slots referenceNoticeHandler::RecvNotice_TextTag_IIDEnumClickandUIElement::MouseHoveras generic fallbacks — worth a second pass if acdream needs to replicate hover/tooltip behaviour, not just click). - §3: LogTextType names for indices 7, 0x11 (colorLightBlue), 0xE
(shares colorBlueGrey with the built-in channels), and the seven
unoverridden default-green indices (1, 0x10, 0x14, 0x17, 0x18, 0x19,
0x22) are unidentified — the DAT-driven
LogTextTypeEnumMapperstring table (struct __cppobj LogTextTypeEnumMapper,acclient.h:57333) would name them; not pulled in this pass. - §5: the exact
m_curTagFontColorRGBA value is unproven from static decomp alone — see the two follow-up options listed there. - The
m_formatfield onTextTag(set from the tag's second colon-split segment, e.g."IIDString"→ the class-selector1..4) was read as a class-shape selector, consistent with theTextTagFactory::MakeTagswitch, but its retail name/purpose beyond "which TextTag subclass" was not otherwise probed.
RESOLVED: the tag colour is authored, and it is green
The research pass above could only prove the mechanism for the tag colour
(property 0x1D, applied per-glyph when a tag is open and its m_type is
0x10000001), not its value — ChatInterface::BuildChatColorLookupTable @0x004F31C0 builds only the ordinary 0x1B array, so it correctly flagged the
RGBA as UNKNOWN rather than assuming the green seen in a screenshot.
It is authored in the LayoutDesc, and it measures out of the installed DATs as:
chat window 0x2100006F, transcript element 0x10000011
P0x1B (line colour) [0x00] R=204 G=204 B=204 A=255
P0x1D (tag colour) [0x00] R= 0 G=178 B= 0 A=255 <- the green
Reproduce with:
dotnet run --project tools/LayoutDump -c Release -- 0x2100006F --colors
Two things worth carrying into the port:
- The tag colour is per-ELEMENT, not per-LogTextType.
0x1Bhere is a one-entry array too, so on this element the ordinary colour comes from the runtime-built chat table while the tag colour comes from the authored property. A port that files "tag green" into the LogTextType colour table would be putting it in the wrong place. - The same
0x1Dgreen appears on more than one element in this layout, so it is not unique to the transcript.
tools/LayoutDump --colors was added for this measurement and prints the
0x1B/0x1D arrays of every element in a layout.