Skip to content

Feat: Add chat bubble customization - #1933

Open
JuJuFX-dev wants to merge 5 commits into
EllesmereGaming:mainfrom
JuJuFX-dev:feature/nameplate-chat-bubbles
Open

Feat: Add chat bubble customization#1933
JuJuFX-dev wants to merge 5 commits into
EllesmereGaming:mainfrom
JuJuFX-dev:feature/nameplate-chat-bubbles

Conversation

@JuJuFX-dev

@JuJuFX-dev JuJuFX-dev commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds a Chat Bubbles page to the Chat module that restyles the chat bubbles the game already draws over players' and NPCs' heads. Background colour and opacity, border size and colour, font size and colour, padding, maximum width and a vertical offset are all configurable, and the bubble text follows the Chat page's own font and outline pickers rather than only the global chat font. The page is off by default and asks for confirmation before it is switched on, because enabling it also switches Blizzard's own bubble settings on.

The text colour can either be a single colour of your own or Blizzard's, picked per channel: a cog beside the Font row's colour swatch carries a Follow Blizzard Default Color toggle, off by default. With it on, each bubble takes the colour the game itself gave that line, so a yell reads as a yell, and the swatch greys out to say so.

The Hide Chat Bubbles in Instances option allows users to disable Blizzard’s default chat bubbles while inside instances. When enabled, it disables the three relevant CVars: ChatBubbles, ChatBubblesParty, and ChatBubblesRaid.
After leaving the instance, the CVars are automatically restored to their previous values.

Rather than drawing its own bubbles on nameplates, EllesmereUI rides on Blizzard's. Their bubbles stay switched on, their chrome is hidden, and a styled frame is anchored to the frame that already carries their position. Every bubble therefore stays exactly where the game put it, including the one over the player's own head, and nameplates are neither involved nor required to be visible. Inside instances the engine's bubble frames are forbidden to addons and can be neither read nor hidden, so the feature stays out of the way there and Blizzard's own bubbles keep working as usual; the options page says so in a banner above the settings.

Channels are picked individually. Say, Yell, NPCs and Emotes share Blizzard's main bubble switch, while Party (which also covers instance chat) and Raid each have a switch of their own. Those switches are borrowed, never kept: the main one is held while at least one of its four channels is ticked, Party and Raid only while their own channel is ticked, and the Party and Raid ticks are seeded from the player's live settings the first time the feature is enabled, so no group bubbles turn up in a chat that had none. Every switch is put back the way it was found when a channel is unticked, when the feature is switched off, when entering an instance, and at logout, which is the only hand-back that still works if the addon is later disabled outright. Guild chat is not offered, because Blizzard draws no bubble for it and there is nothing to restyle.

How was it tested?

Tested in game on the live client.

Screenshots

grafik Ingame Chat

Checklist

  • New settings default OFF (no behavior change without opt-in)
  • Zero cost while disabled: no events registered, no polling, no hooks doing work, no frames built
  • Cheap while enabled: event-driven (no polling, no timer-based logic, no per-frame allocations). Bubbles are picked up through OnShow/OnHide hooks on the engine's own frames and there is no OnUpdate anywhere. The single timer is a short bounded fallback that runs only while a chat line is still waiting for its bubble, at most eight ticks, coalesced into one chain no matter how much is being said, and it stops on its own.
  • No writes onto Blizzard-owned frames (weak-table pattern used); HookScript/hooksecurefunc only, never SetScript on Blizzard frames
  • Tested in-game on live; no version gates or pre-Midnight APIs added

A new Chat Bubbles page on the Chat module restyles the bubbles the game
already draws: background colour and opacity, border size and colour, font size
and colour, padding, maximum width and a vertical nudge. Off by default, and
opt-in behind a confirmation, because switching it on also switches Blizzard's
own bubbles on.

Blizzard's bubbles are not forbidden outside instances, so rather than replace
them the feature rides on them: their switches stay on, the chrome is blanked,
and a styled frame is hung on the frame that carries their position, which the
engine has already placed over the speaker's head. No nameplate is involved and
the player's own lines work too. Inside an instance those frames ARE forbidden
to us and can be neither read nor blanked, so the feature stays away entirely
for the stay and Blizzard's own bubbles keep working.

Blizzard's switches are borrowed, never kept. chatBubbles is taken over while at
least one of Say, Yell, NPCs and Emotes is ticked. Party and Raid each have a
switch of their own, taken over only while that channel is ticked, and those two
ticks are seeded from the live switches the first time the feature is enabled so
group bubbles never turn up in a chat that had none. Every switch is handed back
on disable, on entering an instance, and at PLAYER_LOGOUT, which is the only
hand-back that survives the addon being disabled outright. The bookkeeping lives
in memory only, so no profile or account export can carry one player's switch
values to somebody else.

A bubble is claimed in the same frame it appears: an OnShow hook reads its text
and matches it against the chat lines still waiting, with a timed sweep behind
it for the cases a hook cannot cover, and a stamp on both sides so a bubble that
was already standing cannot be claimed by a later line that happens to match it.
What gets drawn is the bubble's own text, so whatever the client formatted into
it, an emote's name prefix or a filled token, carries over unchanged.

Access to Blizzard's frames is guarded throughout, because one can be
reclassified as forbidden underneath us, and every comparison against a chat
string is secret-value safe. Nothing runs while the feature is off: no frames,
no event registrations, no hooks on Blizzard's frames and no CVar writes.
…ry channel

The bubble drew every channel in a single configured colour, so a yell and a
whisper from an NPC came out looking the same. A cog beside the Font row's
colour swatch now carries one toggle, Follow Blizzard Default Color, off by
default so nothing changes for anyone who does not go looking for it.

The colour is read off Blizzard's own FontString when the bubble is claimed,
not derived from a table of our own mapping chat events to chat types. The
engine has already answered the question per channel, and reading its answer
covers channels we never enumerate. The read is guarded like every other access
to a Blizzard frame and secret-tested, since a restricted frame answers with
numbers SetTextColor cannot take; if it comes back empty the bubble falls back
to the configured colour rather than to nothing.

The captured colour is kept for the life of the claim, so toggling the option
restyles bubbles already on screen through RefreshStyle, and it is cleared when
the frame goes back to the pool so no speaker inherits the previous one's
colour. While the option is on the colour swatch greys out and says why, with
the gate's own wording kept for the case where the whole feature is off.
@Barbiero

Barbiero commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Yessss I'll use this a lot

since chat bubble customization only works outside of instances, I think it'd be cool if this module included a "automatically hide chat bubbles in instances"(essentially toggling the chatBubbles/chatBubblesParty/chatBubblesRaid cvar when entering an instance), since that is a behavior that is pretty useful imo - chat bubbles are a source of annoyance for me when fighting in m+/raids, but I like them in the overworld

Our own bubbles never draw inside a dungeon, raid, scenario or battleground:
the engine's bubble frames are forbidden to addons there, which is why the
feature suspends itself for the stay. Until now that left Blizzard's own bubbles
showing at whatever the player had set. This option switches all three of them
off for as long as the instance lasts and back on the way out. Off by default.

CVars are the only lever available, not a shortcut. A forbidden frame can be
neither read nor hidden, and C_ChatBubbles offers nothing to disable, so there
is no addon-side way to suppress a bubble the engine has decided to draw there.

ApplyCVar now takes the value a switch has to sit at rather than a flag saying
we want it on, which is the whole mechanical change: hiding means forcing one
off, and the existing take-over bookkeeping then covers the rest. Each switch
keeps the snapshot it was first taken over on, so the pass that runs on leaving
the instance hands it back off that same snapshot, and a logout inside one
restores it just as well.
@JuJuFX-dev

Copy link
Copy Markdown
Contributor Author

Yessss I'll use this a lot

since chat bubble customization only works outside of instances, I think it'd be cool if this module included a "automatically hide chat bubbles in instances"(essentially toggling the chatBubbles/chatBubblesParty/chatBubblesRaid cvar when entering an instance), since that is a behavior that is pretty useful imo - chat bubbles are a source of annoyance for me when fighting in m+/raids, but I like them in the overworld

Thank you for the suggestion. I added that to the PR. :)

Every chat event the bubble renderer listens to is declared SecretInChatMessagingLockdown in Blizzard's own ChatInfoDocumentation, and none of their text payloads carries NeverSecret, so arg1 is a secret string whenever that lockdown is in effect. That is a state of the chat system rather than a property of the map, so it reaches the open world, where the feature is not suspended. Comparing a secret to nil raises, so the existence test is type() everywhere now and the secret guard runs ahead of any comparison: the chat handler, OnBlizzShow, Sweep, BlizzTextColor, MatchPending, and both SafeEq and SafeContains, whose contract explicitly allows a secret on either side and which raised on their own first line.

Guard the remaining unguarded reads of Blizzard's frames the same way. BlizzParts resolved outer.GetChildren and child.String outside its pcall, and the second of those ran on every call rather than once per frame. Anchor let PP.Point reach SetPoint against a frame that may have been reclassified as forbidden since the claim, which RefreshStyle repeats per slider step. HookOuter called HookScript unguarded. A failed anchor now hands the bubble back instead of leaving a shown frame with no point at all and Blizzard's chrome blanked behind it.

Release the claimed bubbles on PLAYER_LOGOUT as well, so the chrome held at alpha 0 goes back with the CVars rather than only through SetActive(false).
Only EllesmereUILocales/_keys.txt conflicted. It is generated by
.tools/extract-locale-keys.sh, so it was resolved by regenerating it over the
merged tree rather than by hand-merging the two key lists: upstream added three
keys and this branch adds one, which the header count on either side cannot
express. The result is upstream's list plus "Only works outside of Instances",
785 keys.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants