# 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**, `:>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::sprintf(&s_NullBuffer_2, "%s says, \"%s\"\n"); // no tag 005714f5 else 00571511 PStringBase::sprintf(&s_NullBuffer_2, "%s<\Tell> says, \"%s\"\n"); ``` (full literal recovered from the constant pool: `data_7d0e60 @ 0x007d0e60` = `"%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::sprintf(&arg5, "%s tells you, \"%s\"\n"); // no tag 00571880 else 0057189c PStringBase::sprintf(&arg5, "%s<\Tell> tells you, \"%s\"\n"); ``` (full literal: `data_7d0ec0 @ 0x007d0ec0` = `"%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] %ws<\\Tell> says, \"%ws\""); ``` (full literal: `data_7e83e8 @ 0x007e83e8` = `"[%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] %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(..., "...\"" 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 "" span 00469084 if (ebx_1 == 0 || tag->m_type != 0x10000001) 00469084 edx_15 = ; // this->m_curFontColor (RGBAColor field order matches struct above) 00469084 else 0046908a edx_15 = ; // 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> 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: 1. Confirms the captured span starts with `<` and ends with `>`. 2. Splits on the first `:` — the text before it (e.g. `"Tell"`) is looked up via `EnumMapper::InqEnum(name, 0x18, &m_type)` (a DAT-driven string→enum table, category `0x18`) to get the numeric tag **type** (`this->m_type`, e.g. `0x10000001` for `"Tell"`). 3. 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 a `switch` uses to instantiate the right `TextTag` subclass: - `1` → `TextTag_DID` - `2` → `TextTag_IID` - `3` → `TextTag_IIDEnum` - `4` → `TextTag_IIDString` (jump table `jump_table_478728 @ 0x00478728`, case `4 @ 0x00478617`) 4. Calls the new tag's virtual `ParseStartTag` on the remaining payload text (everything after the second `:`, i.e. `":"`). ## 2. Tag payload — what a click needs to address the right player `struct TextTag_IIDString : TextTag { unsigned int m_IID; PStringBase 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::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::to_uint32(&leftPart); // -> numeric object GUID 00478ae2 PStringBase::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] 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` 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 `0x1D` is authored directly on the chat-log `UIElement_Text` widget via its `LayoutDesc` (a per-widget default, not something `ChatInterface` code builds at runtime) — plausible since `SetFontColorHelper`'s `InqProperty` call would find *any* property the widget inherits, not just ones `BuildChatColorLookupTable` wrote. - 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_IIDStringClick` override (`gmMainChatUI`) was traced for click behaviour; others exist (vtable slots reference `NoticeHandler::RecvNotice_TextTag_IIDEnumClick` and `UIElement::MouseHover` as 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 `LogTextTypeEnumMapper` string table (`struct __cppobj LogTextTypeEnumMapper`, `acclient.h:57333`) would name them; not pulled in this pass. - §5: the exact `m_curTagFontColor` RGBA value is unproven from static decomp alone — see the two follow-up options listed there. - The `m_format` field on `TextTag` (set from the tag's second colon-split segment, e.g. `"IIDString"` → the class-selector `1..4`) was read as a class-shape selector, consistent with the `TextTagFactory::MakeTag` switch, 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.** `0x1B` here 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 `0x1D` green 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.