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 be MODERN (AsyncChatEvent) on Paper. LEGACY on Paper means someone set chat-hook explicitly.
  • Your chat formatter should appear on the same event as ChatColor. If EssentialsChat is listed under AsyncChatEvent and ChatColor under AsyncPlayerChatEvent, they are on different pipelines and ChatColor's output will be stripped downstream.
  • priority= should read HIGHEST on the modern hook. DEFAULT resolves to that automatically; anything else means event-priority was 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 AnoModules at the same HIGHEST priority — 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:

yaml
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 §x markers (§xt§xt§xt), EssentialsChat is stripping hex codes out of a legacy-pipeline message. Its strip pattern removes §0-§9/§a-§f but not the leading §x, which is what leaves the debris. Set chat-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:

  1. In LPC's config: chat-format: "{prefix}{name}&r: %chatcolor_message%"
  2. 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).

  • AUTO resolves to MODERN on 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 to HIGHEST on 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.