How to convert the ChatMakeSystemMessage SetCore to the new TextChatService

I was converting my game’s chat system to the new TextChatService and struggling to figure out how to customize system messages.

For some indiscernible reason the ChatMakeSystemMessage SetCore isn’t supported by the new chat, meaning my existing flow for sending system messages was broken.

To keep the same functionality as before, I had to write a new wrapper function. The process was painful so I thought I would share here to save other people the suffering.

How to send system messages before:

game.StarterGui:SetCore("ChatMakeSystemMessage", {
	Text = "Welcome to Vesteria! Say '/help' for a list of commands."; 
	Color = Color3.fromRGB(0, 255, 149)
})

How to send system messages now:

It’s not so simple anymore!

To make system messages under the new TextChatService, you have to use the TextChannel:DisplaySystemMessage API, which only takes two parameters: a message (String) and and an optional metadata (also a String!).

First we create a new channel for our game’s system messages, so we don’t accidently overload the Roblox system message channel.

Then we need to convert our system message properties to Rich Text.

Finally, we need to pass the font tag part of the string as metadata, because the message string will get sanitized, and add it back in using the channel callback. The end result looks something like this:

local modules = game.ReplicatedStorage:WaitForChild("modules")
local network = require(modules:WaitForChild("network"))

local channels = game.TextChatService:WaitForChild("TextChannels")
local systemChannel : TextChannel = Instance.new("TextChannel")
systemChannel.Name = "VesteriaSystemChannel"
systemChannel.Parent = channels

network:create("chatMakeSystemMessage", "BindableFunction", "OnInvoke", function(messageInfo)
	local fontString = "<font"
	if messageInfo.Color then
		local c = messageInfo.Color
		local r = math.floor(c.R * 255)
		local g = math.floor(c.G * 255)
		local b = math.floor(c.B * 255)
		fontString ..= " color=\"rgb(" .. r .. "," .. g .. "," .. b .. ")\""
	end
	if messageInfo.Font then
		local fontNameString
		if typeof(messageInfo.Font) == "EnumItem" then
			fontNameString = messageInfo.Font.Name
		else
			fontNameString = tostring(messageInfo.Font)
		end
		fontString ..= " face=\"" .. fontNameString .. "\""
	end
	if messageInfo.TextSize then
		fontString ..= " size=\"" .. messageInfo.TextSize .. "\""
	end
	fontString ..= ">"
	
	systemChannel:DisplaySystemMessage(messageInfo.Text, fontString)
end)

function systemChannel.OnIncomingMessage(textChatMessage : TextChatMessage)
	local properties = Instance.new("TextChatMessageProperties")
	local textString = textChatMessage.Text
	-- Inject Rich Text
	local metadata = textChatMessage.Metadata
	if metadata then
		textString = metadata .. textString .. "</font>"
	end
	properties.Text = textString
	
	return properties
end

network:invoke("chatMakeSystemMessage", {
	Text = "Welcome to Vesteria! Say '/help' for a list of commands."; 
	Color = Color3.fromRGB(0, 255, 149)
})

And now, at last, our system messages are coming through squeaky clean:

image

Considering that Roblox is moving to set TextChatService as the default for new experiences, I suggest converting your experiences. If you use the ChatMakeSystemMessage SetCore like I did, I hope you found this guide useful for keeping your pretty system messages.

26 Likes

Minor correction, SendAsync will attempt to send messages on behalf of the player, will respect player chat privacy settings, and will be automatically filtered. It will also be subjected to rate limits.

For system messages, I would recommend using the TextChannel:DisplaySytemMessage method instead which won’t be subject to this additional work or limitations.

7 Likes

Thanks for catching this! I’ve updated the original post with the correction.

1 Like

Np. This is a great resource for those who need help migrating existing code to TextChatService.


I did want to highlight why we don’t have a similar method in TextChatService for transparency and insight. Previously, ChatMakeSystemMessage’s API encouraged developers to format their messages at the callsite of the system message. This means that message formatting code and gameplay logic were interwoven and sprinkled throughout a developer’s scripts.

Days/weeks later, a developer may want to change the look/feel of chat and their custom system messages. From a maintenance perspective, it is much easier to tweak and customize these messages if the chat appearance is handled in centralized locations rather than each callsite. (I’ve had this headache in my own projects where chasing down every system message was a bit of a burden)

With OnIncomingMessage callbacks, we can handle all of our TextChatService customization code in a centralized location. For our default system messages, we use the Metadata property of TextChatMessages to determine what formatting to apply to the message. For example, when sent through the default RBXSystem TextChannel, TextChatMessages with a Metadata property like “Roblox.Emote.Error.NotOwned” will be formatted in red because “Error” is found in the string and “Roblox.Friend.Info.Joined” will be formatted in grey.

You can see a simplified example of this here:

4 Likes

I apologize for bumping this topic, but I tested the script and it don’t works. Can someone explain what I didn’t understand in the first post please?

The script sent an error message because there is no instances named “modules” in game.ReplicatedStorage.

local modules = game.ReplicatedStorage:WaitForChild("modules")
local network = require(modules:WaitForChild("network"))

local channels = game.TextChatService:WaitForChild("TextChannels")
local systemChannel : TextChannel = Instance.new("TextChannel")
systemChannel.Name = "VesteriaSystemChannel"
systemChannel.Parent = channels

network:create("chatMakeSystemMessage", "BindableFunction", "OnInvoke", function(messageInfo)
	local fontString = "<font"
	if messageInfo.Color then
		local c = messageInfo.Color
		local r = math.floor(c.R * 255)
		local g = math.floor(c.G * 255)
		local b = math.floor(c.B * 255)
		fontString ..= " color=\"rgb(" .. r .. "," .. g .. "," .. b .. ")\""
	end
	if messageInfo.Font then
		local fontNameString
		if typeof(messageInfo.Font) == "EnumItem" then
			fontNameString = messageInfo.Font.Name
		else
			fontNameString = tostring(messageInfo.Font)
		end
		fontString ..= " face=\"" .. fontNameString .. "\""
	end
	if messageInfo.TextSize then
		fontString ..= " size=\"" .. messageInfo.TextSize .. "\""
	end
	fontString ..= ">"

	systemChannel:DisplaySystemMessage(messageInfo.Text, fontString)
end)

function systemChannel.OnIncomingMessage(textChatMessage : TextChatMessage)
	local properties = Instance.new("TextChatMessageProperties")
	local textString = textChatMessage.Text
	-- Inject Rich Text
	local metadata = textChatMessage.Metadata
	if metadata then
		textString = metadata .. textString .. "</font>"
	end
	properties.Text = textString

	return properties
end

network:invoke("chatMakeSystemMessage", {
	Text = "Welcome to Vesteria! Say '/help' for a list of commands."; 
	Color = Color3.fromRGB(0, 255, 149)
})

The code referenced uses Vesteria’s module system which you can find at this GitHub repo. The code will only work with a folder called “modules” in ReplicatedStorage and the appropriate network module inside of it.

What should I write inside the module script? I didn’t find anything explaining this in the first post.