Applies the Opus review findings on CH1 (172c6f9a), the exact retail chat
color table. Two blockers plus should-fixes/nits, one commit:
BLOCKER 1 — LegacyChannelChatType.Resolve's channel-bit table was wrong.
Binary Ninja renders retail's `neg esi; sbb esi, esi` idiom (a branchless
select between Channel 0x08 and Channel_Send 0x09) as the trivial pseudo-C
`esi - esi` (always 0), hiding the real values. Corrected by decoding the
raw bytes at the PDB-paired binary: HEAR sbb site VA 0x00570F0A (mask -6 ->
0x08), SEND sbb site VA 0x00570D4F (mask -5 -> 0x09). The generic
admin/audit/sentinel catch-all is Channel/Channel_Send, NOT Abuse (0x0E) —
Abuse is retail's ONLY 0x0E producer (bit 0x0001). The unnamed
FellowBroadcast bit (0x4000000) is hear=Channel(0x08)/send=Fellowship(0x13),
not a flat 0x13. ACE's PDB-sourced Channel enum corroborates. Introduces
`RetailLogTextType`, the 34-value named enum for the wire LogTextType space
(values only, no color — Core stays presentation-free).
BLOCKER 2 — three ChatLog.OnSystemMessage sinks (ChatVM.ShowSystemMessage,
LiveSessionRuntimeFactory's ShowSystemMessage delegate,
HeadlessGameplayOperations.DisplayMessage) were typing ALL
ClientCommandController output 0x1A (bright red), including informational
command output (@version, /loc, friends list, usage lines). Retail types
the great majority of that output 0x00 Default (green) and reserves 0x1A
for genuine refusals/errors. Reverted to 0x00 with a comment noting the
refusal-vs-info split lands with CH2's SpewBox producer rewiring. The five
App composition sites that pass 0x1A for actual refusal text
(InteractionRetainedUiComposition, SessionPlayerComposition) were already
correct and are untouched (aside from converting the literal to the new
enum).
Also: AP-176 divergence-register row for OnWeenieError/OnCombatLine's
single-type approximation of retail's per-code/per-message dispatch; a
carry-forward test for the out-of-range LogTextType color fallback in
ChatWindowController; decomp-confirmed anchors replacing ACE-inferred
citations in CombatChatTranslator and ChatLog.OnPlayerKilled; required
(non-optional) logTextType parameters on OnLocalSpeech/OnTellReceived/
OnCombatLine/OnSelfSent since no production caller relied on a default;
LegacyChannelChatType.Resolve's parameter renamed channelBit -> channelId
with a doc note on multi-bit ids; corrections to the color-table research
doc's §3.3 wire tables; and issue #359 for the pre-existing (not
CH1-introduced) 0x019E PlayerKilled participant-suppression gap retail has
and acdream lacks.
dotnet build clean; full Release suite 11,835 passed / 4 skipped / 0 failed
(11,839 total), up from the CH1 baseline of 11,833/4/0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
29 KiB
Retail chat color table — the exact type→color lookup
Date: 2026-08-09
Status: RESEARCH ONLY — no production code changed.
Primary source: docs/research/named-retail/acclient_2013_pseudo_c.txt
(Binary Ninja pseudo-C of the Sept 2013 EoR acclient.exe, PDB-named) +
direct byte-read of the PDB-paired binary C:\Users\erikn\Downloads\acclient.exe
(v11.4186, CodeView GUID 9e847e2f-777c-4bd9-886c-22256bb87f32).
Ghidra MCP: not reachable this session (ports 8080 and 8081 both refused);
everything below comes from the committed static decomp plus the binary itself.
0. TL;DR
- The lookup table has exactly 34 entries, indices
0x00–0x21. - The index is the
LogTextTypevalue — the same integer that arrives on the wire as ACE'sChatMessageType. There is no remapping anywhere: the wire byte is passed hand-to-hand from the message handler down to the font-color setter and used as a raw array index. - The table's default fill is
colorGreen(0.5, 1, 0.498). Seven indices are never overwritten and therefore stay green. - The colors are hard-coded C++ constants. There is no chat-color option, no registry key, no DAT override. What retail did let the player configure is which text types go to which chat window (a 64-bit bitfield), not what color they are.
- All 13 cdb-dumped RGBA values are confirmed byte-for-byte against the binary,
and the one address the 2026-06-16 cdb session missed (
0x81c4d8) is resolved here.
1. The builder — ChatInterface::BuildChatColorLookupTable @ 0x004F31C0
Called unconditionally from ChatInterface::PostInit @ 0x004F3DD0 (call site
0x004F3F13), once per chat-window construction.
1.1 What it actually does
if (m_chatLog == null) return;
var list = new BaseProperty(name = 0x1B); // the font-color LIST property
var color = new BaseProperty(name = 0x19); // one RGBAColor-valued element
color.SetColor(&colorGreen); // 0x81C578
for (i = 0x22; i != 1; i--) // 34 iterations
list.Append(color); // → indices 0..0x21 all green
color.SetColor(&colorWhite); list.SetValue(0x02, color);
color.SetColor(&colorGrey); list.SetValue(0x0C, color);
color.SetColor(&yellow); list.SetValue(0x03, 0x1F, 0x0A, 0x13);
color.SetColor(&darkYellow); list.SetValue(0x04, 0x0B);
color.SetColor(&colorPink); list.SetValue(0x08, 0x09);
color.SetColor(&orange); list.SetValue(0x12, 0x21);
color.SetColor(&colorBlueGrey); list.SetValue(0x1B, 0x1C, 0x1D, 0x1E, 0x20, 0x0E);
color.SetColor(&colorDarkRed); list.SetValue(0x0F, 0x06, 0x15);
color.SetColor(&colorLightRed); list.SetValue(0x16);
color.SetColor(&colorLightBlue); list.SetValue(0x07, 0x11);
color.SetColor(&colorCyan); list.SetValue(0x0D);
color.SetColor(&colorBrightPurple); list.SetValue(0x05);
color.SetColor(&colorBrightRed); list.SetValue(0x1A);
m_chatLog->SetProperty(list); // 0x004F3799
Decoding notes for anyone re-reading the raw pseudo-C:
var_18is the list property (SetPropertyName(&var_18, 0x1b)),var_10is the single color property (SetPropertyName(&var_10, 0x19)).- vtable slot
+0x9Con the color property = "set value to thisRGBAColor*"; the argument is a raw.dataaddress. - vtable slot
+0xFCon the list = Append (one arg); slot+0xF8= SetValue(index, value) (two args). This is how you tell the default-fill loop from the per-index overrides. - Loop trip count:
i_1 = 0x22; do { append; i = i_1; i_1--; } while (i != 1);runs 34 times → indices0x00–0x21. The highest index the function ever writes is0x21. Independent confirmation of the 34-value space is the squelch enumerator at0x00589DEB:for (uint i = 0; i < 0x22; i++). - 27 indices are written explicitly; 7 (
0x00, 0x01, 0x10, 0x14, 0x17, 0x18, 0x19) keep the green default. - The function builds property
0x1Bonly — the main font-color list. It does not build property0x1D, the tag font-color list thatUIElement_Text::SetFontColorHelperalso consults; that one comes from the LayoutDesc.
1.2 The color constants (verified against the binary)
Read from .data (image base 0x400000, .data VA 0x80A000 → raw
0x40A000). Each is a 16-byte RGBAColor = four little-endian floats
R, G, B, A. Every alpha is 1.0.
| Address | Symbol | R | G | B | A | ≈8-bit |
|---|---|---|---|---|---|---|
0x0081C4A8 |
colorBrightRed |
1.0 | 0.0 | 0.0 | 1.0 | #FF0000 |
0x0081C4B8 |
colorWhite |
1.0 | 1.0 | 1.0 | 1.0 | #FFFFFF |
0x0081C4C8 |
(unnamed) yellow | 1.0 | 1.0 | 0.247 | 1.0 | #FFFF3F |
0x0081C4D8 |
(unnamed) dark yellow | 0.824 | 0.824 | 0.392 | 1.0 | #D2D264 |
0x0081C4E8 |
colorBrightPurple |
1.0 | 0.498 | 1.0 | 1.0 | #FF7FFF |
0x0081C4F8 |
colorDarkRed |
1.0 | 0.247 | 0.247 | 1.0 | #FF3F3F |
0x0081C508 |
colorLightRed |
0.96 | 0.459 | 0.447 | 1.0 | #F57572 |
0x0081C518 |
colorLightBlue |
0.247 | 0.749 | 1.0 | 1.0 | #3FBFFF |
0x0081C528 |
colorPink |
1.0 | 0.588 | 0.588 | 1.0 | #FF9696 |
0x0081C538 |
colorCyan |
0.247 | 0.863 | 0.863 | 1.0 | #3FDCDC |
0x0081C548 |
colorBlueGrey |
0.706 | 0.863 | 0.941 | 1.0 | #B4DCF0 |
0x0081C558 |
colorGrey |
0.824 | 0.824 | 0.784 | 1.0 | #D2D2C8 |
0x0081C568 |
(unnamed) orange | 0.933 | 0.573 | 0.118 | 1.0 | #EE921E |
0x0081C578 |
colorGreen |
0.5 | 1.0 | 0.498 | 1.0 | #80FF7F |
0x0081C4D8 is new — the 2026-06-16 cdb session skipped it, and it is
used by two table slots (0x04 Speech_Direct_Send and 0x0B Social_Send).
Its exact bytes are aa f1 52 3f aa f1 52 3f 39 b4 c8 3e 00 00 80 3f;
0.8235294 and 0.39215687 are exactly 210/255 and 100/255, so retail
authored it as RGB(210, 210, 100). Adjacent entries not used by the chat
table: 0x0081C588 = white again, 0x0081C598 = black.
Ship the float values, not the 8-bit approximations — several
(colorLightRed, orange) are not integral /255 and were authored as
decimal floats.
Provenance rule: these 14 addresses appear in the whole 66 MB pseudo-C
dump exactly 14 times, and every one of those occurrences is inside
BuildChatColorLookupTable. Nothing else in the client reads or writes them.
2. The index enum — LogTextType
docs/research/named-retail/acclient.h has an enum eChatTypes
(line 4935) that stops at eTextTypeTotalNumChannels = 0x19. That header
enum is stale — it predates the 2013 build and does not cover the table.
The authoritative 2013 names come from
LogTextTypeEnumMapper::LogTextTypeToString @ 0x006AFF90, which is a literal
switch over 0x00–0x1F emitting the canonical strings; anything > 0x1F
(and 0x1A–0x1E, which fall through) returns "Unknown".
The field name ChatDisplayInfo::m_ltt (acclient.h line 40739,
written at 0x005CD89A) is what pins the enum's name: LogTextType.
2.1 The complete table
| Idx | LogTextType name |
Color symbol | RGBA (float) | Hex | Source |
|---|---|---|---|---|---|
0x00 |
Default |
colorGreen |
0.5, 1, 0.498, 1 | #80FF7F |
default fill |
0x01 |
All |
colorGreen |
0.5, 1, 0.498, 1 | #80FF7F |
default fill |
0x02 |
Speech |
colorWhite |
1, 1, 1, 1 | #FFFFFF |
explicit |
0x03 |
Tell |
yellow 0x81C4C8 |
1, 1, 0.247, 1 | #FFFF3F |
explicit |
0x04 |
Speech_Direct_Send |
dark yellow 0x81C4D8 |
0.824, 0.824, 0.392, 1 | #D2D264 |
explicit |
0x05 |
System |
colorBrightPurple |
1, 0.498, 1, 1 | #FF7FFF |
explicit |
0x06 |
Combat |
colorDarkRed |
1, 0.247, 0.247, 1 | #FF3F3F |
explicit |
0x07 |
Magic |
colorLightBlue |
0.247, 0.749, 1, 1 | #3FBFFF |
explicit |
0x08 |
Channel |
colorPink |
1, 0.588, 0.588, 1 | #FF9696 |
explicit |
0x09 |
Channel_Send |
colorPink |
1, 0.588, 0.588, 1 | #FF9696 |
explicit |
0x0A |
Social |
yellow 0x81C4C8 |
1, 1, 0.247, 1 | #FFFF3F |
explicit |
0x0B |
Social_Send |
dark yellow 0x81C4D8 |
0.824, 0.824, 0.392, 1 | #D2D264 |
explicit |
0x0C |
Emote |
colorGrey |
0.824, 0.824, 0.784, 1 | #D2D2C8 |
explicit |
0x0D |
Advancement |
colorCyan |
0.247, 0.863, 0.863, 1 | #3FDCDC |
explicit |
0x0E |
Abuse |
colorBlueGrey |
0.706, 0.863, 0.941, 1 | #B4DCF0 |
explicit |
0x0F |
Help |
colorDarkRed |
1, 0.247, 0.247, 1 | #FF3F3F |
explicit |
0x10 |
Appraisal |
colorGreen |
0.5, 1, 0.498, 1 | #80FF7F |
default fill |
0x11 |
Spellcasting |
colorLightBlue |
0.247, 0.749, 1, 1 | #3FBFFF |
explicit |
0x12 |
Allegiance |
orange 0x81C568 |
0.933, 0.573, 0.118, 1 | #EE921E |
explicit |
0x13 |
Fellowship |
yellow 0x81C4C8 |
1, 1, 0.247, 1 | #FFFF3F |
explicit |
0x14 |
World_Broadcast |
colorGreen |
0.5, 1, 0.498, 1 | #80FF7F |
default fill |
0x15 |
Combat_Enemy |
colorDarkRed |
1, 0.247, 0.247, 1 | #FF3F3F |
explicit |
0x16 |
Combat_Self |
colorLightRed |
0.96, 0.459, 0.447, 1 | #F57572 |
explicit |
0x17 |
Recall |
colorGreen |
0.5, 1, 0.498, 1 | #80FF7F |
default fill |
0x18 |
Craft |
colorGreen |
0.5, 1, 0.498, 1 | #80FF7F |
default fill |
0x19 |
Salvaging |
colorGreen |
0.5, 1, 0.498, 1 | #80FF7F |
default fill |
0x1A |
(no string; "Unknown") — client-local text | colorBrightRed |
1, 0, 0, 1 | #FF0000 |
explicit |
0x1B |
(no string) — Turbine General | colorBlueGrey |
0.706, 0.863, 0.941, 1 | #B4DCF0 |
explicit |
0x1C |
(no string) — Turbine Trade | colorBlueGrey |
0.706, 0.863, 0.941, 1 | #B4DCF0 |
explicit |
0x1D |
(no string) — Turbine LFG | colorBlueGrey |
0.706, 0.863, 0.941, 1 | #B4DCF0 |
explicit |
0x1E |
(no string) — Turbine Roleplay | colorBlueGrey |
0.706, 0.863, 0.941, 1 | #B4DCF0 |
explicit |
0x1F |
Admin_Tell |
yellow 0x81C4C8 |
1, 1, 0.247, 1 | #FFFF3F |
explicit |
0x20 |
(no string) — Turbine Society (all 4 rooms) | colorBlueGrey |
0.706, 0.863, 0.941, 1 | #B4DCF0 |
explicit |
0x21 |
(no string; no producer found) | orange 0x81C568 |
0.933, 0.573, 0.118, 1 | #EE921E |
explicit |
2.2 Notes on the unnamed slots
-
0x1Ais not dead.LogTextTypeToStringhas no name for it, but it is the single most-used literal in the client: client-local text that never reaches the wire —"You need an open vendor.", the threecant_jump_*strings, most command-parser errors. It also has a special path inAddTextToScroll(§3.2): type0x1Askips the timestamp prefix and skips the chat log file. Color: bright red. -
0x1B–0x1E,0x20are the Turbine (global community) chat rooms. Their identity is not guesswork —ChatRoomTracker::GetChatFormat @ 0x005CD7C0writesChatDisplayInfo::m_lttdirectly:room field m_lttm_allegianceRoomID0x12(Allegiance)mGeneralChatRoomID0x1BmTradeChatRoomID0x1CmLFGChatRoomID0x1DmRoleplayChatRoomID0x1EmOlthoiChatRoomID0x12(reuses Allegiance)mSocietyChatRoomID,mSocietyCelHanChatRoomID,mSocietyEldWebChatRoomID,mSocietyRadBloChatRoomID0x20This is why ACE's
ChatMessageType.cslabels0x1B/0x1E"light cyan, unknown purpose" — they are the Turbine rooms, andcolorBlueGreyis exactly that sky-blue. -
0x21has a color slot and a filter bit but no producer anywhere in the 2013 client:LogTextTypeToStringreturns"Unknown",IsLegalChannelreturns 0, noAddTextToScroll/m_lttsite emits it. Treat it as reserved. Its orange matches0x12Allegiance, which hints at an allegiance-adjacent type that was never shipped (or was server-only).
3. Consumption trace — wire byte → pixel color
3.1 The chain
<inbound game message>
└─ ClientCommunicationSystem::Handle_Communication__* (type comes off the wire)
└─ ClientSystem::AddTextToScroll(text, type, fireToPlugin, windowId) @0x00563C50
└─ ECM_UI::SendNotice_DisplayFinalStringInfo(type, body, prefix, windowId) @0x00692550
└─ ChatInterface::RecvNotice_DisplayFinalStringInfo(type, body, prefix, windowId) @0x004F4640
├─ AppendStringInfoWithFont(m_chatLog, prefix, font=0, colorIdx=0x0C) ← timestamp, ALWAYS grey
└─ AppendStringInfoWithFont(m_chatLog, body, font=0, colorIdx=type)
└─ UIElement_Text::SetFontColorHelper(this, prop 0x1B, &m_curFontColor, type) @0x00466AC0
└─ list.GetValue(type) → m_curFontColor
type is never transformed. It is the wire value, used as the array index.
3.2 Two details worth porting
Timestamp prefix is hard-coded to index 0x0C. In
RecvNotice_DisplayFinalStringInfo @0x004F46E9 the second StringInfo (the
"%#H:%M:%S " prefix built in AddTextToScroll when
PlayerModule::DisplayTimeStamps() is on) is appended with color index
0x0C — i.e. colorGrey, regardless of the message's own type. Retail's
timestamps are always grey.
Out-of-range index leaves the color alone. SetFontColorHelper reads the
list's element count into arg2, then if (arg4 < arg2) { GetValue(arg4) ... }.
If the index is >= 34, it falls through without touching m_curFontColor,
so the run inherits the previous line's color. It does not fall back to
a default. (The green default only applies to the seven in-range slots the
builder never overwrites.)
Type 0x1A bypasses the timestamp and the log file. AddTextToScroll
branches on arg3 == 0x1a at 0x00563DE6; the non-0x1A path is the one
that formats the timestamp and fprintfs to ClientSystem::s_pLogFile.
3.3 Wire → type for the message kinds you asked about
Anchors are the ClientCommunicationSystem::Handle_Communication__* handlers.
| Inbound kind | Opcode (ACE) | Handler | Type passed |
|---|---|---|---|
| Local speech | 0x02BB |
Handle_Communication__HearSpeech @0x005712A0 |
arg5 verbatim from the wire (AddTextToScroll(..., arg5, ...) at 0x0057154D for "X says", 0x00571325 for the "You say" self-echo) |
| Ranged speech | 0x02BC |
Handle_Communication__HearRangedSpeech @0x0056E550 → gmCCommunicationSystem::HandleRangedTalkEvent @0x00589F60 |
wire type (arg6) |
| Tell | 0x02BD |
Handle_Communication__HearDirectSpeech @0x005715A0 |
arg6 verbatim (AddTextToScroll(..., arg6, ...) at 0x005718D8 / 0x00571834 / 0x0057160C) |
| Emote / soul emote | 0x01E0 / 0x01E2 |
Handle_Communication__HearEmote @0x0057CBE0 (soul emote tail-calls it) |
hard-coded 0x0C at 0x0057CF94 |
| Legacy channel speech | 0x0147 ChannelBroadcast |
Handle_Communication__ChannelBroadcast @0x00570B90 |
derived from the channel bit — see below |
| Turbine room speech | 0xF7DE |
ChatRoomTracker::GetChatFormat @0x005CD7C0 → m_ltt |
0x12 / 0x1B–0x1E / 0x20 — table in §2.2 |
| Server message / system | 0xF7E0 |
dispatched through RecvNotice_DisplayStringInfo @0x0056E890 |
wire type verbatim |
| Client-local errors, command output | (none) | many sites | 0x1A (or 0x00 for informational command output) |
| Combat / magic / advancement / recall / craft / salvage / appraisal | 0xF7E0 and friends |
server-chosen | wire type verbatim — 0x06/0x15/0x16, 0x07/0x11, 0x0D, 0x17, 0x18, 0x19, 0x10 |
| Death messages | 0x019E etc. |
server-chosen | wire type verbatim (retail carries no special client-side death color) |
Handle_Communication__ChannelBroadcast channel-bit → type (the m_buffer_5
variable feeding AddTextToScroll at 0x00571169):
Corrected 2026-08-09 (review): the row 8 rows below marked "corrected"
were wrong in the original CH1 drop. Binary Ninja renders retail's neg esi; sbb esi, esi idiom — a branchless select between Channel (0x08) and
Channel_Send (0x09) — as the trivial pseudo-C expression esi - esi
(always 0), which hid the real values. The correction comes from decoding
the raw bytes at the PDB-paired binary: the HEAR branch's sbb site is at
VA 0x00570F0A (mask -6 → 0x08 Channel) and the SEND branch's is at VA
0x00570D4F (mask -5 → 0x09 Channel_Send).
| Channel bit | Prefix retail prints | Type |
|---|---|---|
0x0001 Abuse |
[<name>] |
0x0E — retail's ONLY 0x0E producer (corrected 2026-08-09) |
0x0400 Help |
[<name>] |
0x0F |
0x0800 Fellowship |
[Fellowship] |
0x13 |
0x1000 Patron / 0x2000 Vassal |
Your patron … / Your vassal … |
0x0A (hear) / 0x0B (own send) — own-send precision added 2026-08-09 (corrected) |
0x4000 Follower/Monarch |
Your follower … |
0x0A (hear) / 0x0B (own send) |
0x1000000 Co-Vassals |
[Co-Vassals] |
0x0A |
0x2000000 Allegiance Broadcast |
[Allegiance Broadcast] |
0x0A |
0x4000000 FellowBroadcast |
— | 0x08 (hear) / 0x13 (own send) — corrected 2026-08-09, was wrongly 0x13 for both |
| admin/audit/advocate/QA/sentinel/town catch-all | [<name>] |
0x08 (hear) / 0x09 (own send) — corrected 2026-08-09, was wrongly 0x0E / 0x0F for all of them |
Note the split that surprises people: legacy allegiance-family chat arrives
as Social (0x0A, yellow) / Social_Send (0x0B, dark yellow), while
Allegiance (0x12, orange) is reserved for the Turbine allegiance room.
3.4 The plugin hook sees the same integer
AddTextToScroll calls IACPlugin::OnChatWindowText(bstr, type, &suppress)
at 0x00563CA4 before any formatting, and a plugin returning
suppress != 0 drops the line entirely. Relevant if acdream's plugin chat
API wants retail parity: the plugin contract is (text, LogTextType, out bool eaten).
4. Configurability — colors are NOT user-settable; filters are
Colors: hard-coded, no override path.
- The 14
RGBAColoraddresses are referenced from exactly one function in the entire binary (§1.2). BuildChatColorLookupTableruns unconditionally atPostInitand ends withm_chatLog->SetProperty(list), which replaces property0x1Bwholesale — so even a LayoutDesc that authored a color list would be overwritten.ChatInterface::RecvNotice_GameplayOptionChanged @0x004F30E0andChatInterface::OnSetAttribute @0x004F3F60handle only the window's text-type filter and the two opacity values. Neither touches color.
Conclusion: the table above is the shipped behavior, not a default. There is nothing for acdream to make configurable for retail parity.
Filters: genuinely user-settable, per window.
ChatInterface::UpdateFromPlayerModule @0x004F3920 reads
PlayerModule::InqChatWindowOption(windowId, 0x1000007F, …) into a 64-bit
m_llTextTypeFilter, and RecvNotice_GameplayOptionChanged live-updates it
when the option changes. ChatInterface::TypeIsActive @0x004F2F10 tests
(1ULL << type) & m_llTextTypeFilter. Two consequences:
UpdateFromPlayerModuleearly-returns whenm_eWindowID == 0, so the main window has no user filter — it keeps thePostInitdefault.RecvNotice_DisplayFinalStringInfodisplays whenwindowId == m_eWindowID(explicitly addressed — e.g. command output usesm_idCurrentCommandSource) orwindowId == 0 && TypeIsActive(type)(broadcast).
PostInit default filters (0x004F3DF9 switch on m_oldState; Binary Ninja
mis-attributes the low dword to m_chatNewNonVisibleTextIndicator — it is the
low half of the 64-bit filter, and the following m_llTextTypeFilter = 0
is the high half):
m_oldState |
Low dword | Types |
|---|---|---|
| 1, 8 | 0xFBFFFFFF |
everything 0x00–0x1F except 0x1A |
| 2 | 0x0000101C |
0x02 Speech, 0x03 Tell, 0x04 Speech_Direct_Send, 0x0C Emote |
| 3 | 0x00040C00 |
0x0A Social, 0x0B Social_Send, 0x12 Allegiance |
| 4 | 0x00080000 |
0x13 Fellowship |
| 5 | 0x78000000 |
0x1B General, 0x1C Trade, 0x1D LFG, 0x1E Roleplay |
Every default sets the high dword to 0, so 0x20 (Society) and 0x21 are
in no window's default filter — Society chat only appears once the player
enables it, which matches retail's opt-in Society channel.
Squelching is a separate axis: LogTextTypeEnumMapper::IsLegalChannel @ 0x006AFF40 whitelists exactly 0x02, 0x03, 0x06, 0x07, 0x0C, 0x10, 0x11, 0x12, 0x13, 0x15, 0x16, 0x17, 0x18, 0x19 as squelchable.
5. acdream correction list
Current code:
src/AcDream.App/UI/Layout/ChatWindowController.cs:542
(RetailChatColor(ChatKind)), driven from line 462. The ChatKind enum is
src/AcDream.Core/Chat/ChatLog.cs:337. (ChatChannelKind in
src/AcDream.UI.Abstractions/ChatChannelKind.cs is the outbound channel
selector and never reaches the color path — it needs no color change.)
5.1 Per-arm verdict
ChatKind |
acdream RGBA now | Retail type it represents | Retail RGBA | Verdict |
|---|---|---|---|---|
LocalSpeech |
1, 1, 1, 1 colorWhite |
0x02 Speech |
1, 1, 1, 1 | ✅ CONFIRMED |
RangedSpeech |
1, 1, 1, 1 colorWhite |
wire type on 0x02BC; ACE's own header documents 0x0C for this opcode |
0.824, 0.824, 0.784, 1 colorGrey |
❌ CHANGE — must follow the wire type, not a constant |
Channel |
0.247, 0.749, 1, 1 colorLightBlue |
Turbine rooms 0x1B–0x1E/0x20; legacy 0x0A/0x0B/0x13; Turbine allegiance 0x12 |
Turbine rooms 0.706, 0.863, 0.941 colorBlueGrey; Social 1, 1, 0.247; Social_Send 0.824, 0.824, 0.392; Fellowship 1, 1, 0.247; Allegiance 0.933, 0.573, 0.118 |
❌ CHANGE — colorLightBlue is 0x07 Magic / 0x11 Spellcasting, never a channel |
Tell |
1, 0.498, 1, 1 colorBrightPurple |
0x03 Tell (incoming), 0x04 Speech_Direct_Send (own "You tell …") |
incoming 1, 1, 0.247 yellow; own send 0.824, 0.824, 0.392 dark yellow |
❌ CHANGE — colorBrightPurple is 0x05 System |
System |
0.5, 1, 0.498, 1 colorGreen |
wire type; ACE 0xF7E0 uses 0x00, 0x03, 0x04, 0x05, 0x06, 0x07, 0x0D, 0x10, 0x11, 0x17, 0x18 |
0x00 → 0.5, 1, 0.498 (green, matches today); 0x05 → 1, 0.498, 1 (purple) |
❌ CHANGE — green is right only for wire type 0x00; the collapse to one color is the bug |
Popup |
0.5, 1, 0.498, 1 colorGreen |
0x0004 PopUpString → Handle_Communication__PopUpString @0x0057FE80 — a modal dialog, not a chat-log line in retail |
n/a | ⚪ OUT OF SCOPE — no retail color; acdream's choice to render it in chat is an acdream divergence |
Emote |
0.824, 0.824, 0.784, 1 colorGrey |
0x0C Emote (hard-coded by HearEmote) |
0.824, 0.824, 0.784, 1 | ✅ CONFIRMED |
SoulEmote |
0.824, 0.824, 0.784, 1 colorGrey |
0x0C — HearSoulEmote tail-calls HearEmote at 0x0057D096 |
0.824, 0.824, 0.784, 1 | ✅ CONFIRMED |
Combat |
0.96, 0.459, 0.447, 1 colorLightRed |
0x06 Combat, 0x15 Combat_Enemy, 0x16 Combat_Self |
0x06/0x15 → 1, 0.247, 0.247 colorDarkRed; 0x16 → 0.96, 0.459, 0.447 colorLightRed |
❌ CHANGE — today's value is correct only for Combat_Self |
_ fallback |
0.824, 0.824, 0.784, 1 colorGrey |
unassigned in-range slots | 0.5, 1, 0.498, 1 colorGreen |
❌ CHANGE — retail's unset default is green; out-of-range indices keep the previous line's color |
Score: 3 confirmed, 6 changed, 1 out of scope.
5.2 The structural correction
Every "CHANGE" above has the same root cause: acdream colors by a synthetic
9-value ChatKind, while retail colors by the 34-value wire
LogTextType. Keeping ChatKind for routing/formatting is fine, but the
color must key off the wire integer.
The wire value is already parsed and already in hand — no new parsing needed:
HearSpeech.Parsed.ChatType(src/AcDream.Core.Net/Messages/HearSpeech.cs:64) — parsed today and discarded at theOnLocalSpeechcall site (src/AcDream.Runtime/Session/LiveSessionEventRouter.cs:260).ServerMessage.Parsed.ChatType(src/AcDream.Core.Net/Messages/ServerMessage.cs:33) — already threaded through associal.Chat.OnSystemMessage(message.Message, message.ChatType)(LiveSessionEventRouter.cs:268), whereChatLog.OnSystemMessageparks it inChatEntry.ChannelId. It is available; it just is not used for color.TurbineChat.ChatType— available atLiveSessionEventRouter.RouteTurbineChat(line 484), already used for the display name viaTurbineChatDisplayNames.Resolve.GameEventstell/emote payloads (src/AcDream.Core.Net/Messages/GameEvents.cs:53) carryChatTypetoo.
The retail-faithful shape is a 34-entry Vector4[] built once (exactly as
BuildChatColorLookupTable does), indexed by the wire type, with
"index ≥ 34 → keep the previous run's color". Several call sites that
currently pass a made-up type would need the real one:
ChatLog.OnSystemMessage(text, 0x1Au) appears at five App composition sites
and chatType: 0u at three GameEventWiring sites — those are placeholders,
and 0x1A happens to be the correct retail type for client-local text
(bright red), so those five are already right by accident.
5.3 Coverage gap
acdream has no representation at all for 22 of the 34 retail types:
All(0x01), Speech_Direct_Send(0x04), Channel(0x08), Channel_Send(0x09),
Social(0x0A), Social_Send(0x0B), Advancement(0x0D), Abuse(0x0E),
Help(0x0F), Appraisal(0x10), Spellcasting(0x11), Allegiance(0x12),
Fellowship(0x13), World_Broadcast(0x14), Combat_Enemy(0x15),
Combat_Self(0x16), Recall(0x17), Craft(0x18), Salvaging(0x19),
Admin_Tell(0x1F), Turbine 0x1B–0x1E/0x20, and reserved 0x21.
A table keyed by the wire integer closes all of them at once.
5.4 Two documentation defects found in passing
src/AcDream.Core.Net/Messages/HearSpeech.cs:39-48— theChatTypelegend in the XML doc comment is wrong on 4 of 6 entries. It claims0x02 = Combat,0x0B = Speech,0x0F = Emote,0x10 = Tell. Retail:0x02 = Speech,0x0B = Social_Send,0x0F = Help,0x10 = Appraisal,0x03 = Tell,0x0C = Emote. Only0x01(Broadcast/AllChannels — retail calls itAll) and0x11(Spellcasting, the comment's "Syllables") are close.ChatWindowController.cs:536-541— the doc comment asserts "the four common kinds (speech/tell/channel/system) are confirmed by the named symbols". Speech is confirmed; tell, channel and system are all wrong. That sentence should be deleted along with the fix.
6. Cross-check against ACE
references/ACE/Source/ACE.Entity/Enum/ChatMessageType.cs is the same index
space (its LogTextTypeEnumMapper: doc lines are literally the strings from
LogTextTypeToString), and its informal color notes independently corroborate
the decompiled table on every entry it comments:
| ACE note | Retail table | Agrees? |
|---|---|---|
0x08 Channel "Light Pink Text" |
colorPink |
✅ |
0x0A Social "Bright Yellow Text" |
yellow #FFFF3F |
✅ |
0x0B Social_Send "Light Yellow Text" |
dark yellow #D2D264 |
✅ |
0x0E Abuse "Light Cyan (skyblue?)" |
colorBlueGrey |
✅ |
0x0F Help "Red Text" |
colorDarkRed |
✅ |
0x13 Fellowship "Bright Yellow Text" |
yellow | ✅ |
0x14 WorldBroadcast "Green Text" |
colorGreen (default fill) |
✅ |
0x15 CombatEnemy "Red Text" |
colorDarkRed |
✅ |
0x16 CombatSelf "Pink Text" |
colorLightRed (salmon) |
✅ |
0x19 Salvaging "Green Text" |
colorGreen (default fill) |
✅ |
0x1B, 0x1E "Light cyan (sky blue)" |
colorBlueGrey |
✅ |
0x1F AdminTell "Bright Yellow Text" |
yellow | ✅ |
ACE stops at 0x1F and comments 0x1A out as "client doesn't display it" —
both are ACE gaps, not retail behavior: retail has 34 slots and 0x1A is its
busiest client-local type.
7. Reproduction commands
# The builder
grep -n "BuildChatColorLookupTable" docs/research/named-retail/acclient_2013_pseudo_c.txt
sed -n '246117,246440p' docs/research/named-retail/acclient_2013_pseudo_c.txt
# The 2013 type names
sed -n '695462,695790p' docs/research/named-retail/acclient_2013_pseudo_c.txt # LogTextTypeToString
sed -n '695397,695425p' docs/research/named-retail/acclient_2013_pseudo_c.txt # IsLegalChannel
# Turbine room -> m_ltt
sed -n '479725,479870p' docs/research/named-retail/acclient_2013_pseudo_c.txt
# Consumption
sed -n '247282,247330p' docs/research/named-retail/acclient_2013_pseudo_c.txt # RecvNotice_DisplayFinalStringInfo
sed -n '113699,113760p' docs/research/named-retail/acclient_2013_pseudo_c.txt # SetFontColorHelper
sed -n '368347,368520p' docs/research/named-retail/acclient_2013_pseudo_c.txt # AddTextToScroll
The RGBA quads were read straight out of the PDB-paired binary by walking the
PE section table (.data VA 0x80A000 → raw 0x40A000) and unpacking
<4f at each 16-byte stride from 0x0081C4A8. Re-run
py tools/pdb-extract/check_exe_pdb.py "C:/Users/erikn/Downloads/acclient.exe"
first — it must report MATCH before any address in this document is valid.