Chat Compatibility
Chat Compatibility
Almost every "my colors don't show up" report comes down to the same question: where in the chat pipeline does ChatColor sit relative to the plugin that formats your chat? This page explains the pipeline and how to inspect it on a live server.
How ChatColor applies color
On Paper, ChatColor listens to AsyncChatEvent and installs a chat renderer. It does not
write color codes into the message.
That distinction is the whole design. A renderer runs after every listener has finished, once per viewer, at the moment the message is turned into what the client sees. A plugin that strips color codes out of chat runs during the event, before the renderer — so by the time ChatColor applies the color, nothing is left that could strip it.
The alternative — writing §x§f§c§b§6§e§1 into the message during the event — leaves the color
sitting in a string that every later listener is free to rewrite. That is the LEGACY path, and
it is why it is no longer the default.
Reading /color debug
Run /color debug (or /color debug all), say something in chat, and check the console.
Turn it off with /color debug off when you're done.
The pipeline dump
[ChatColor] [DEBUG] ===== chat pipeline =====
[ChatColor] [DEBUG] paper=true hook=MODERN (AsyncChatEvent) priority=HIGHEST
[ChatColor] [DEBUG] config: apply-to-message=true apply-to-name=false late-bind=false chat-hook=AUTO event-priority=DEFAULT
[ChatColor] [DEBUG] AsyncChatEvent: 8 listener(s), in execution order
[ChatColor] [DEBUG] LOWEST EssentialsChat (com.earth2me.essentials.chat.processing.PaperChatHandler$ChatListener)
[ChatColor] [DEBUG] NORMAL EssentialsChat (com.earth2me.essentials.chat.processing.PaperChatHandler$ChatListener)
[ChatColor] [DEBUG] HIGHEST AnoModules (dev.mrtroxy.anomodules.modules.chathover.ChatHoverListener)
[ChatColor] [DEBUG] HIGHEST ChatColor (net.busybee.chatcolor.listeners.ChatListener)
[ChatColor] [DEBUG] MONITOR ChatColor (net.busybee.chatcolor.utils.ChatDebugger)
[ChatColor] [DEBUG] =========================
What to look for:
hook=should beMODERN (AsyncChatEvent)on Paper.LEGACYon Paper means someone setchat-hookexplicitly.- Your chat formatter should appear on the same event as ChatColor. If EssentialsChat is
listed under
AsyncChatEventand ChatColor underAsyncPlayerChatEvent, they are on different pipelines and ChatColor's output will be stripped downstream. priority=should readHIGHESTon the modern hook.DEFAULTresolves to that automatically; anything else meansevent-prioritywas set by hand.- ChatColor should be at or near the bottom of its priority group. It binds one tick after
startup specifically so it registers after other plugins at the same priority, which lets it
wrap their renderer instead of being overwritten by it. In the dump above, ChatColor is listed
below
AnoModulesat the sameHIGHESTpriority — that is the ordering working correctly.
The per-message trace
[ChatColor] [DEBUG] --- djtmk [MODERN] "hello"
[ChatColor] [DEBUG] stored: type=SOLID key=pastel-pink tag={#fcb6e1}
[ChatColor] [DEBUG] resolved: pattern=none tag={#fcb6e1}
[ChatColor] [DEBUG] entry: "pastel-pink" needs chatcolor.custom.pastel-pink -> hasPermission=true isPermissionSet=true
[ChatColor] [DEBUG] essentials.chat.color = false (unset, using default)
[ChatColor] [DEBUG] renderer in place before us: dev.mrtroxy.anomodules...ChatHoverListener$$Lambda
[ChatColor] [DEBUG] MONITOR: cancelled=false viewers=2 ourRendererStillInstalled=true
[ChatColor] [DEBUG] RENDERER RAN
[ChatColor] [DEBUG] in : hello
[ChatColor] [DEBUG] out: {#FCB6E1}hello
| Line | What it tells you |
|---|---|
stored: |
What the player picked, as saved to disk. |
resolved: |
What actually applies right now. tag=NONE means nothing will be colored — usually a revoked permission. |
entry: |
ChatColor's own permission check. If hasPermission=false, the problem is your permission setup, not the pipeline. |
essentials.chat.* |
Whether EssentialsX would let this player use color codes. Only relevant on the LEGACY path. |
renderer in place before us: |
Whose renderer ChatColor is wrapping. ChatColor preserves it rather than replacing it. |
ourRendererStillInstalled |
false means a plugin replaced our renderer after us — raise ChatColor's event-priority. |
RENDERER RAN |
The renderer actually executed. If this is missing, a plugin delivered the message itself and bypassed rendering. |
Section signs are printed as (S) and MiniMessage angle brackets as {/} so that the
clean-console filter cannot strip the very codes you are trying to inspect.
Diagnosing from the trace
| Symptom | Cause | Fix |
|---|---|---|
entry: ... hasPermission=false |
Player lacks the color's permission node | Grant it, or make the entry public |
resolved: tag=NONE |
No color selected and no group default matches | Set default-color or a group-defaults entry |
late-bind is on, ChatColor stops here |
late-bind: true |
Set it to false unless a plugin places %chatcolor_message% |
apply-to-message is off, ChatColor stops here |
apply-to-message: false |
Set it to true |
[LEGACY] on a Paper server |
chat-hook forced to LEGACY |
Set chat-hook: "AUTO" |
ourRendererStillInstalled=false |
A plugin at a later priority replaced the renderer | Set event-priority: "MONITOR" to get the last word |
No RENDERER RAN line |
A plugin delivered the message itself, bypassing rendering | Check that plugin's Paper-chat setting |
Known plugin interactions
EssentialsChat
Works out of the box on AUTO. Use the standard {MESSAGE} tag in the Essentials format:
group-formats:
Default: '{DISPLAYNAME}&7: {MESSAGE}'
ChatColor binds at HIGHEST and colors from the renderer, so no priority tuning is needed.
You do not need to grant essentials.chat.color / essentials.chat.rgb. Those nodes control
whether players may type their own & codes; ChatColor's color is applied by the renderer after
EssentialsChat has finished, so its color filter never sees it. Leave them off unless you
want players hand-coloring their own chat.
If a player's chat shows orphaned
§xmarkers (§xt§xt§xt), EssentialsChat is stripping hex codes out of a legacy-pipeline message. Its strip pattern removes§0-§9/§a-§fbut not the leading§x, which is what leaves the debris. Setchat-hook: "AUTO"to fix it.
LPC (LuckPermsChat)
LPC builds its format from PlaceholderAPI and discards the rendered message, so it needs the placeholder route instead:
- In LPC's config:
chat-format: "{prefix}{name}&r: %chatcolor_message%" - In ChatColor's
config.yml:late-bind: true
late-bind stops ChatColor coloring the message itself, so it is not colored twice.
DiscordSRV
Works automatically. For best results set UseModernPaperChatEvent: true in DiscordSRV's config
so it reads the same event ChatColor renders on.
Chat-input plugins (WorldGuard, mcMMO, HeadDatabase, GUI prompts)
These listen to AsyncPlayerChatEvent to capture input, not to format chat. They will show up in
the AsyncPlayerChatEvent section of the pipeline dump and are harmless — ChatColor deliberately
ignores them when choosing a hook.
Folia
ChatColor is Folia-compatible (folia-supported: true).
AUTOresolves toMODERNon Folia the same as on Paper, since Folia is a Paper fork.The renderer only touches thread-safe state — permission checks and an in-memory data cache, with no PlaceholderAPI call in the render path — so it is safe on Folia's regional threads.
Listener binding uses the global region scheduler.
event-priority: "DEFAULT"resolves toHIGHESTon the modern hook without needing to recognise any chat plugin by name, so ChatColor binds late on Folia the same as on Paper.
That last point used to be a real gap: auto-detection looked for EssentialsChat, LPC, or
DeluxeChat, and since none of those run on Folia, ChatColor bound at NORMAL there and could be
overwritten by a Folia-compatible chat plugin at a higher priority. Fixed.