Skip to content

Chat Heads compat#297

Merged
mrbuilder1961 merged 3 commits intomrbuilder1961:omniversfrom
Fourmisain:omnivers
Jan 19, 2026
Merged

Chat Heads compat#297
mrbuilder1961 merged 3 commits intomrbuilder1961:omniversfrom
Fourmisain:omnivers

Conversation

@Fourmisain
Copy link
Contributor

As mentioned here.

Instead of passing the whole message to formatPlayername(), only an (optional) head component is passed.

In the translatable case is only searches for a head in the sender part and in the non-translatable case it stops searching for a head once a '>' is found.

I removed the if(/*ChatHeadsIntegration.*/installed() && usingBeforeName()) check since this is not Chat Heads specific, as servers can add head components in the name part as well.
Technically, extractHeadComponent() thus shouldn't really be part of ChatHeadsIntegration.
Actually, the whole class can be nuked I just realized, I'll leave that up to you.

I added "chat.type.text" translatable handling to extractHeadComponent so it aborts early when it encounters it as well - I don't think it's actually needed but I thought might as well. (Left out the "chat.type.team.*" ones though.)

A bit concerning but I couldn't get the whole vanilla branch to trigger on my test EssentialsX server (even though the chat is of the form <name> message). Not sure what's up with that.
Ended up doing some manual tests via code.

P.S.
I feel this could have taken an hour less if it weren't for Modstitch/Stonecutter or whatever the multiversion build thing is, took some time to figure it out. 😅
(org.gradle.jvmargs=-Xmx2G is also too little and Gradle ran OOM a few times.)

@mrbuilder1961
Copy link
Owner

thanks for the PR! i'm gonna do some testing before i decide how to proceed with this. if you could provide some of your test messages so we can compare outputs that'd be great and i can of course send results afterward. my concerns are:

  1. should chat patches even add a head back in if chat heads isn't installed - should it be modifying the message period? realistically the answer is no, it shouldn't be modifying because [playername head] in a message should not be considered vanilla. however, it would take a lot more work to carve out an exception in the regex for chat heads only (as it currently lets it slide) and i do not want to open that can of worms. perhaps a new config option is underway, but that leads into concern number two:

  2. does this really need complex parsing? granted i'm about to go to bed and i haven't been testing this for hours, but at least rn i don't see an issue with the simple starts-with check + the original extract head method i originally suggested. i want to make sure that doesn't work before proceeding with this.

tl;dr: aside from generally wanting to stray away from overcomplication, i need to make sure that messages with multiple or incorrect heads should even be reconstructed in the first place. (maybe that's why you couldn't trigger the reconstruction branch - although it could be that the custom playername format is off or the player who is supposedly sending the message is not real.)

@Fourmisain
Copy link
Contributor Author

if you could provide some of your test messages so we can compare outputs that'd be great

Oh, those are gone, I constructed messages in code like

var head = Component.object(new PlayerSprite(ResolvableProfile.createUnresolved("jeb_"), true);

m = Component.empty()
    .append("<")
    .append(Component.empty()
        .append(head)
        .append("player"))
    .append(Component.literal("> ")
        .append("message"))

in various forms of indentation, moving the head to different places etc, logging the extractHeadComponent() recursion steps to see whether everything works as expected.

should chat patches even add a head back in if chat heads isn't installed - should it be modifying the message period?

Wait, with "should it be modifying the message period," you mean the question is whether it should do the whole message reformatting thing, aka run this block at all - did I get this right?

If so, I guess it just depends on whether you want to or not.
I don't see too much of an issue with reformatting more general types of messages - except maybe that since you're replacing the whole name, this could be problematic with nicknames and stuff, where "stuff" could potentially be more than just a head.

Currently messages with heads by their name do match VANILLA_FORMAT, else this whole PR would be pretty pointless - the whole "readding heads" in general would be pointless.

Theoretically you could just say "frick it" and disallow reformatting of anything other an exact vanilla format, disallowing handling of messages with chat heads.

This could be done by changing the regex to match only possible usernames in between <, >.
Minecraft usernames only allow word characters (alphanumeric and underscore) up to 16 of them, in other words the regex to match them is just \w{1,16}.
(Chat Heads actually used a simple \w+ regex back then to try to find usernames in messages - until nicknames with non-standard characters came about and the whole idea was thrown out.)

If you go this route, you could throw the whole compatibility thing out, on the cost of not being able to reformat those messages.

it shouldn't be modifying because [playername head] in a message should not be considered vanilla.

They are vanilla in the sense that the vanilla client will happily accept it.
They're not vanilla in the sense that a vanilla server wouldn't send them.
But I don't really see why that turns the question into a "should not".

does this really need complex parsing?

We're talking about extractHeadComponent(), right?
I guess it's a bit complicated, though when you understand how Component.visit() works - it "visits" each component in render order - it's really just that, just with extra handling for translatables.
That translatable part could probably be thrown out without sacrificing too much generality, but I figured I did it in Chat Heads, I just do it here too.

but at least rn i don't see an issue with the simple starts-with check + the original extract head method i originally suggested. i want to make sure that doesn't work before proceeding with this.

You mean

maybe i can add a check that message.getString() starts with the format <[playername head]playername> before proceeding? since the method currently returns the first instance of a PlayerSprite component, it should ensure it's not picking up a head from a chat message.

At least one issue I see with this is that the playername of the head need not match the playername of the, uh, player.
I.e. <[profilename head]nickname> is a valid case for Chat Heads - though I'm not sure if it is valid for Chat Patches...?
Similarly, a server could decide to put <[othername head]name>, so a simple startsWith wouldn't be enough, but I guess you could use a regex or some manual string parsing to do it still.

Other than that, I just don't like working with strings when it's really components, it feels inexact to me, but that may just be the random server BS PTSD I got from working on Chat Heads.

i need to make sure that messages with multiple or incorrect heads should even be reconstructed in the first place. (maybe that's why you couldn't trigger the reconstruction branch - although it could be that the custom playername format is off or the player who is supposedly sending the message is not real.)

Just checked, it was because I had a nickname, without it, it does hit the branch.

And uhh... it worked that one time, but that other time it did not?
Hold on, lemme do another round of testing...

@Fourmisain
Copy link
Contributor Author

Fourmisain commented Jan 6, 2026

Oops, the recursion ended too early when it got too deep.
I think that one time it "worked", it didn't even reformat the message because I remember the name being red (which is removed on reformat - that's how that should work, right?).

@mrbuilder1961
Copy link
Owner

Oh, those are gone, I constructed messages in code like

var head = Component.object(new PlayerSprite(ResolvableProfile.createUnresolved("jeb_"), true);

m = Component.empty()
    .append("<")
    .append(Component.empty()
        .append(head)
        .append("player"))
    .append(Component.literal("> ")
        .append("message"))

okay perf 👍

Wait, with "should it be modifying the message period," you mean the question is whether it should do the whole message reformatting thing, aka run this block at all - did I get this right?

yes exactly.

Currently messages with heads by their name do match VANILLA_FORMAT, else this whole PR would be pretty pointless - the whole "readding heads" in general would be pointless.

yeah that makes this kind of difficult as you've explained and go on to explain...

If you go this route, you could throw the whole compatibility thing out, on the cost of not being able to reformat those messages.

yeah that's what makes this tricky: i want to support Chat Heads compatibility, but i don't want to implement an entire parser just because servers could send heads.

They are vanilla in the sense that the vanilla client will happily accept it. They're not vanilla in the sense that a vanilla server wouldn't send them. But I don't really see why that turns the question into a "should not".

bc if it's not something a vanilla server would send then chat patches shouldn't reconstruct the message - it shouldn't matter whether the chat format is as vanilla-like as <playername>: message or as modded as hypixel's chat - in both cases it's not vanilla and because of that, the "responsibility" for chat formatting has shifted from the client to the server. that's been the mantra for a while and changing it now would open pandora's box far beyond the scope of Chat Heads compatibility lol.

maybe i can add a check that message.getString() starts with the format <[playername head]playername> before proceeding? since the method currently returns the first instance of a PlayerSprite component, it should ensure it's not picking up a head from a chat message.

At least one issue I see with this is that the playername of the head need not match the playername of the, uh, player. I.e. <[profilename head]nickname> is a valid case for Chat Heads - though I'm not sure if it is valid for Chat Patches...? Similarly, a server could decide to put <[othername head]name>, so a simple startsWith wouldn't be enough, but I guess you could use a regex or some manual string parsing to do it still.

yes that should also be valid for Chat Patches. again, because the format isn't vanilla's <playername> message, Chat Patches really shouldn't change it. i think what might be confusing then is why even have Chat Heads compatibility - it's bc Chat Heads is client side and is kind of just another step in the client chat reformatting process. but when a server sends a message, even if it's in Chat Heads' format, it shouldn't try to ensure the head matches the playername - i hope that makes any sense.

Other than that, I just don't like working with strings when it's really components, it feels inexact to me, but that may just be the random server BS PTSD I got from working on Chat Heads.

i totally understand that, it's appalling how insane minecraft text can drive you lmao. but here i think it would make sense bc if the check is false, we should exit early and not modify anything, so the parsing wouldn't even be necessary.

-

Oops, the recursion ended too early when it got too deep. I think that one time it "worked", it didn't even reformat the message because I remember the name being red (which is removed on reformat - that's how that should work, right?).

sorry i'm not sure what you're talking about here.. are you saying the original message formatting/style was stripped?

also just a heads up my free time has dried up again so i'll take a bit longer to respond.

@Fourmisain
Copy link
Contributor Author

bc if it's not something a vanilla server would send then chat patches shouldn't reconstruct the message - it shouldn't matter whether the chat format is as vanilla-like as <playername>: message or as modded as hypixel's chat - in both cases it's not vanilla and because of that, the "responsibility" for chat formatting has shifted from the client to the server.
(...)
because the format isn't vanilla's <playername> message, Chat Patches really shouldn't change it. i think what might be confusing then is why even have Chat Heads compatibility - it's bc Chat Heads is client side and is kind of just another step in the client chat reformatting process. but when a server sends a message, even if it's in Chat Heads' format, it shouldn't try to ensure the head matches the playername - i hope that makes any sense.

Ohh, I get it!
So if anything if should only match <[playername head]playername>.
I see now why you wanna simplify everything.

We need to go back!

Oops, the recursion ended too early when it got too deep. I think that one time it "worked", it didn't even reformat the message because I remember the name being red (which is removed on reformat - that's how that should work, right?).

sorry i'm not sure what you're talking about here.. are you saying the original message formatting/style was stripped?

In short, if I join my EssentialsX server in vanilla:
vanilla
My name is red (maybe because I'm OP, I don't understand EssentialsX).

When I turn on Chat Patches:
Chat Patches
The name turns white due to it being reformatted (white is the default color in the config).

I guess this isn't really intended, since it's not an actual vanilla message, right?

(The original comment was me being confused whether this PR worked or not. If the reformatting doesn't trigger, the head is still in the right place, so it looked like it "worked" even though it did literally nothing. Only then did I notice the color changed when it actually triggered. Lead to me fixing a critical bug.)

also just a heads up my free time has dried up again so i'll take a bit longer to respond.

No worries, I'm in a similar boat.

@Fourmisain
Copy link
Contributor Author

Fourmisain commented Jan 8, 2026

I rewrote the thing, here's what is does now:

I wrote a regex matching "playername" or "[playername head]playername":

regex

The latter is using a backreference to ensure the names are the same.

Plugging that in the original VANILLA_FORMAT regex gives ^((-> )?\[.+] )?<(\w{1,16}|\[(\w{1,16}) head\]\4)>\s.+$
Note the group number changed. I also removed the (?i) since it didn't actually do anything originally and would conflict with the "head" part.
(Sidenote: There exist players with 1 and 2 character names, the original regex didn't match those.)

With that, one can simply check whether the backreference aka group 4 exists to see if the head is by the playername and extract it with the original form of extractHeadComponent().
Done.

@Fourmisain Fourmisain changed the title Extract head components only from before '>' and do this even without Chat Heads Chat Heads compat Jan 8, 2026
@mrbuilder1961
Copy link
Owner

Ohh, I get it!
So if anything if should only match <[playername head]playername>.
I see now why you wanna simplify everything.

yay i'm glad! i realize it's quite convoluted so thanks for sticking with me lol.

My name is red (maybe because I'm OP, I don't understand EssentialsX).

yeah it should keep the red styling because although the style is different the text still abides by the format, so this is an issue. if it hasn't magically gone away, would you mind toggling "log message structures" in the help section of Chat Patches' config and sending me what it says after you send a message on that server? i'd really appreciate it 😄 thanks! (definitely turn it off after though it will bloat the logs like crazy.)

I rewrote the thing, ...

thank you so much for doing all of this - i really really appreciate it. if it lets me i'll make a couple nitpicky changes, but i think you pretty much nailed it. my only concern with the backreference and \w{1,16} is with players on teams, because they can have custom prefix and suffixes - i believe those show up in the getString() result but not in the [playername head] part, so we'll have to see if that breaks it. even if it does though, switching it to a simple .+ should be okay, as technically vanilla teams can have weird characters in the pre/suffixes due to them being text components - lmk what you think though. again, thanks so much for all your time and effort 🙏🙏🙏

@Fourmisain
Copy link
Contributor Author

my only concern with the backreference and \w{1,16} is with players on teams, because they can have custom prefix and suffixes - i believe those show up in the getString() result but not in the [playername head] part, so we'll have to see if that breaks it. even if it does though, switching it to a simple .+ should be okay, as technically vanilla teams can have weird characters in the pre/suffixes due to them being text components

Oh yeah, that breaks it. 😰

The prefixes and suffixes can literally be anything, including spaces, "<>", "[]", head components etc, so in general you could only match them with .*, but if you try and do this you get e.g. ...\[(\w{1,16}) head].*\4.*)>\s.+ and the .* can just walk over the >\s and it can match with messages like
<[name head]other> something name> something
Even worse, in the standard name case .*\w{1,16}.* matches virtually everything, including [name head]name, so you can't distinguish between the two cases anymore.
(Just noticed: The team names can be anything too...)

This is unsolvable in regex alone.
You could make some assumptions and tighten the regex, e.g. assume ] and > are not allowed in pre/suffixes and swap out .* for [^\]>]*.
That way the normal name vs chat head case can be distinguished and the "walking over >" case doesn't happen either.
You'll lose some legitimate cases though.

Actually, after looking around for a bit, in the case of system messages (so any server with FreedomChat/No Chat Reports, also EssentialsX) you're already assuming > can't be in a prefix or teamname here.
It'd fail in a few cases:
-> [*~<SUPREAM>~*] <Bob> Hi team! would detect "SUPREAM" as the player name (and replace Bob due to reformatting).
-> [SupremeTeam] < stupidprefix> Bob> Hi team! would detect stupidprefix

I was originally gonna also suggest this "solution":

Alternatively one could throw out most of the regex and instead match the message with the known sender name messageData.sender and their team data directly.
Basically construct a string "<{prefix}{playername}{suffix}>" and match that with the message, same with the chat head case.

But that assumes you already know the player name, which works for player messages but not for system messages as we just saw.

So I think a "tight" regex is probably the best compromise so I commited that for now.

yeah it should keep the red styling because although the style is different the text still abides by the format, so this is an issue. if it hasn't magically gone away, would you mind toggling "log message structures" in the help section of Chat Patches' config and sending me what it says after you send a message on that server? i'd really appreciate it 😄 thanks! (definitely turn it off after though it will bloat the logs like crazy.)

ChatListener received message '<Fourmisain> test' -> vanilla = true
An error occurred while modifying '<Fourmisain> test'
(lastEmpty=false, boundary=false, messageData.vanilla=true, config.name=true, config.nameFormat='<$>')
	Timestamp: literal{[12:27:29] }[style={color=#FF55FF,clickEvent=class_10610[command=01/09/2026],hoverEvent=class_10613[value=literal{01/09/2026}[style={color=#FFFFFF}]],insertion=1767958049965}]
	Body:
		Team: empty
		Sender: empty[style={color=#AFFFFF,clickEvent=class_10610[command=/tell Fourmisain ],hoverEvent=class_10611[entity=net.minecraft.class_2568$class_5248@c66a77e0],insertion=Fourmisain}, siblings=[literal{<}, literal{Fourmisain}, literal{> }]]
		Content: empty[siblings=[literal{test}[style={!bold,!italic,!underlined,!strikethrough,!obfuscated}]]]
	Dupes: literal{> test}[style={!bold,!italic,!underlined,!strikethrough,!obfuscated}]
-- End of message structure --

Not sure if it's actually helpful since this gives the info after the message has already been modified, i.e. in Sender: empty[style={color=#AFFFFF the #AFFFFF is the color I set in the config.

The issue is actually pretty clear:
content.append(config.formatPlayername(head, messageData.sender));
does not depend on the style of the original name component in any way.

P.S.
Btw, I was trying to clear my chat log, but even after clicking "Clear all messages" and restarting the game it's still there...?
Tried it on 1.21.10 and .11, ingame and from the main menu, tried the other buttons too, not sure if I'm doing something wrong.

@mrbuilder1961
Copy link
Owner

thanks for looking into all that! i trust your judgement but i'll do a few regex tests of my own and log them just to make sure, and for future reference. hopefully i can approve this within the week!

also, thanks for attaching the log snippet, i appreciate it. i have no clue how that message doesn't look entirely broken, from my reading it looks like it should say "[12:27:29] test> test" but good to know regardless. yes, it should color the messages, i didn't pass the style bc i assumed all the coloring would come from the name itself but assumptions are the bane of robust code so i'll have to get on that lmao.

and about the chat log, those options clear the internal log and not the actual chat history you see on the hud - good ol' vanilla F3 + D will take care of that for ya! i'll have to add a note about that though, it's definitely confused people (including myself) before lol

@Fourmisain
Copy link
Contributor Author

and about the chat log, those options clear the internal log and not the actual chat history you see on the hud - good ol' vanilla F3 + D will take care of that for ya! i'll have to add a note about that though, it's definitely confused people (including myself) before lol

No, I mean even after a restart the history is still there!
The chatlog.json doesn't get cleared/deleted no matter what I do - well, except directly deleting the file myself.

@Fourmisain
Copy link
Contributor Author

The chatlog.json doesn't get cleared/deleted no matter what I do - well, except directly deleting the file myself.

Aha, the history does get cleared but it only saves it to the file if another message is added!

@mrbuilder1961 mrbuilder1961 merged commit e3d6e23 into mrbuilder1961:omnivers Jan 19, 2026
1 check passed
@mrbuilder1961
Copy link
Owner

final thoughts post merge - following the thinking outlined a few comments up, to sidestep some unavoidable issues with team pre/suf-fixes, what if i kept both the updated regex and a new version of the old one? when chat heads is installed i'd use the one you made, otherwise it uses basically the old one but with some tweaks pulled from the new.

in theory at least this should prevent issues with vanilla team messages being seemingly unmodifiable, and it "carves out" an exception for Chat Heads without totally wrecking the regex. additionally, if a vanilla team message can't be formatted with both mods installed, there would literally be no way to fix it without writing some gnarly library to fix it (imho) - so it's kind of intentional (wontfix).

it is almost 2am for me so maybe i'm off my rocker but lmk if i'm missing anything. thanks again for all your help 🙏 🫡

mrbuilder1961 added a commit that referenced this pull request Jan 19, 2026
@Fourmisain
Copy link
Contributor Author

Fourmisain commented Jan 19, 2026

what if i kept both the updated regex and a new version of the old one? when chat heads is installed i'd use the one you made, otherwise it uses basically the old one but with some tweaks pulled from the new.

Depends on what tweaks you'd want to pull.
As the regex is right now, it should handle the vast majority of cases just fine, vanilla or not.
There's two clear cases that fail (by design), namely if the team pre/suffix contains > or ].
If you don't use the [name head]name matching when Chat Heads isn't installed, you can get rid of the ] exception, reducing the fail cases to just >.
You decide if that's worth it!

additionally, if a vanilla team message can't be formatted with both mods installed, there would literally be no way to fix it without writing some gnarly library to fix it (imho) - so it's kind of intentional (wontfix).

Yeah, I was thinking too, this whole ordeal is mostly fixable, but only by writing a system akin to what Chat Heads uses, collecting data (playernames, team names/prefixes/suffixes etc), then trying to parse/match the message with the known data.
And even then it still wouldn't be a 100% solution (especially when nicknames are involved).

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