Today I’ll be showing you how to edit the default Roblox chat to add icons next to a player’s name!
Contents
- Forking the Chat
- Editing ExtraDataInitializer
- Editing DefaultChatMessage
- Editing Util
- Conclusion
- Errata
Forking the Chat
First, we’ll need to fork parts of the built-in chat. In Roblox Studio, start a test via Run or Play Solo. Navigate to the “Chat” object in the Explorer and select the ChatModules
and ClientChatModules
folders within:
Copy these folders, stop the test, and paste them back into the Chat object. Upon expanding these folders, you’ll find quite a lot of scripts! Fortunately, we only need three of them. In the ChatModules
folder, delete all modules except for ExtraDataInitializer
, which is how we’ll process the speakers and attach the icons to their messages. In the MessageCreatorModules
folder (located within ClientChatModules
) delete all modules except for DefaultChatMessage
, which will be responsible for processing a message’s icon data, and Util
, which will build the actual GUI elements. Make sure that the InsertDefaultModules
bool values are still present and set to true
. Your explorer should now look like this:
Editing ExtraDataInitializer
Alright, let’s start programming! Go ahead and open up the ExtraDataInitializer
module. We’ll start by adding the dictionary of icons to reference later. I put it directly underneath the SpecialChatColors
dictionary, since it serves a very similar purpose.
local SpecialChatIcons = { --the main dictionary
Groups = { --a list of groups
{
--- ChipioIndustries group
GroupId = 2932328,
Rank = 1,
Icons = {"rbxasset://textures/ui/PlayerList/OwnerIcon.png"},
}, --we'll also be setting up the system in a way that
}, --lets internal textures be used.
Players = { --a list of players
{
--- ChipioIndustries
UserId = 18697683,
Icons = {4081664234}, --Icons is a table because it
}, --allows for multiple icons per user/group.
}
}
Up next, we need to edit the ConstructIsInGroups
function. This function’s job is to iterate through the SpecialChatColors
dictionary and apply a useful IsInGroup
function to each entry. However, we need to modify it to work with our icon dictionary by adding the following lines to the bottom of the function:
if SpecialChatIcons.Groups then --does our table exist?
for _,group in pairs(SpecialChatIcons.Groups) do --iterate through it
--there's an existing function called MakeIsInGroup
--that we can use.
group.IsInGroup = MakeIsInGroup(group.GroupId,group.Rank)
end
end
Now that we have a pool of data to draw from, we need to be able to figure out which icons should be attached to the player. Fortunately, there’s already a very similar function called GetSpecialChatColor
that we can very closely replicate with our own GetSpecialChatIcons
.
--there are two types of image IDs: ones with just the ID itself,
--and ones with the rest of the address already attached. If it's
--just a number, we need to attach the address to it, and if it's
--a full address, we can go ahead and return it.
local function ProcessImageID(ID)
if tonumber(ID) then
return "rbxassetid://"..tostring(ID)
else
return ID
end
end
local function GetSpecialChatIcons(speakerName)
local chatIcons={} --an empty list for us to add to
if SpecialChatIcons.Players then --is there a players folder?
local playerFromSpeaker=Players:FindFirstChild(speakerName)
if playerFromSpeaker then --does the player exist?
--now we're going to cycle through the icon entries
--to see if we can find matching user IDs.
for _,player in pairs(SpecialChatIcons.Players) do
if playerFromSpeaker.UserId == player.UserId then
--if there's a match, we can loop through the entry's
--icons and add them to the list, passing them through
--ProcessImageID first.
for __,icon in pairs(player.Icons) do
table.insert(chatIcons,ProcessImageID(icon))
end
end
end
end
end
if SpecialChatIcons.Groups then --is there a groups folder?
for _,group in pairs(SpecialChatIcons.Groups) do --cycle through groups
--this line uses the IsInGroup function we applied earlier.
if group.IsInGroup(Players:FindFirstChild(speakerName)) then
--if we've found a match, add the icons to the table.
for __,icon in pairs(group.Icons) do
table.insert(chatIcons,ProcessImageID(icon))
end
end
end
end
--finally, return the table.
return chatIcons
end
Once we have a way to get the icons associated with a player, we need to do something with that information by attaching it to the speaker object as it’s created. There’s a function in the script called onNewSpeaker
which we can edit to attach our own data. Anywhere in the function should work, but I’m adding it right after the closing of the “if” statement if not speaker:GetExtraData("Tags") then
.
--has the info been set already?
if not speaker:GetExtraData("Icons") then
--use our function to get a player's icons
local specialIcons=GetSpecialChatIcons(speakerName)
if specialIcons then --if our function worked,
--we can apply it to the speaker with SetExtraData.
speaker:SetExtraData("Icons",specialIcons)
end
end
Editing DefaultChatMessage
Now we’ve successfully attached the data to the player! Now we just need to edit a couple other scripts to actually display the images. Go ahead and exit ExtraDataInitializer
and open up DefaultChatMessage
, located within the MessageCreatorModules
folder. To keep with the design of what’s there, we’re going to create a variable for the icons right alongside the rest of the data near the top of the script.
local icons = extraData.Icons or {}
Later on down the script, in the same are where the tags are created (for me, I started writing my code on line 55) we’ll want to create the icons.
local iconLabels = {} --create a list for our image labels
for i,icon in pairs(icons) do --cycle through the icons
--this line uses a function we'll be creating in a moment.
local label = util:AddIconLabelToBaseMessage(BaseMessage,icon)
--guiObjectSpacing tracks where the next element should be
--positioned. We can use this for our purposes.
label.Position = guiObjectSpacing
--Now we need to update the numNeededSpaces and guiObjectSpacing
--variables to account for what we've added. We can just add
--the result of another util function called GetNumberOfSpaces
--to numNeededSpaces, passing through the icon label, as well as
--two existing variables called useFont and useTextSize.
numNeededSpaces = numNeededSpaces + util:GetNumberOfSpaces(label,useFont,useTextSize)
--guiObjectSpacing is much simpler, and we only need to add the
--horizontal offset of the label size to the total.
guiObjectSpacing = guiObjectSpacing + UDim2.new(0, label.Size.X.Offset, 0, 0)
table.insert(iconLabels,label) --finally, we can add the label to the list.
end
So… what’s the point of adding the label objects to a list, anyway? Well, we’re actually going to iterate through the table later on in the script in order to specify how the labels fade in and out. Skip down to around line 100, right after the tag labels have been iterated through.
The FadeParmaters
dictionary holds a list of items and their corresponding properties which need to be faded to specified values. The overall structure looks something like this:
FadeParmaters
-Object
–Property
—FadedIn
—FadedOut
--loop through the icon labels
for i, iconLabel in pairs(iconLabels) do
--create a new entry in the existing FadeParmaters table
--(misspelling and all) using the iconLabel itself as a key.
--The only property we need to configure is ImageTransparency,
--and we'll want the faded in state to be 0 transparency, and the
--faded out state at 1.
FadeParmaters[iconLabel] = {
ImageTransparency={FadedIn=0,FadedOut=1}
}
end
Editing Util
Util
holds a lot of essential functions, and we’ll want to edit some of those to work with our image labels. That, and one of the functions we used doesn’t even exist. At the top of the script, we can declare a constant for the image size:
local DEFAULT_ICON_SIZE = Vector2.new(18,18)
Up next, we need to edit a function called GetNumberOfSpaces
. You’ll recall that we used this to increase numNeededSpaces
by passing through the label itself. Thing is, the function is designed for text, not labels. There’s one line in particular we want to replace:
local strSize = self:GetStringTextBounds(str, font, textSize)
More specifically, we want to replace it with the following code:
local strSize --define the variable in a scope that can be
--accessed by later lines of code
if typeof(str)=="string" then --is it a string?
--if so, treat it like a string.
strSize = self:GetStringTextBounds(str, font, textSize)
elseif typeof(str)=="Instance" then --or is it an icon?
--if so, we can just use the label's AbsoluteSize property.
strSize = str.AbsoluteSize
end
Since GetStringTextBounds
and AbsoluteSize
both return Vector2 values, the rest of the function will have no problem processing the information. However, we haven’t actually written the code that will create the label–so let’s do that! I put my function around line 203, but it doesn’t matter very much.
--this is the function we used in DefaultChatMessage.
function methods:AddIconLabelToBaseMessage(BaseMessage, iconAddress)
local iconSize=DEFAULT_ICON_SIZE --use the constant we defined earlier
local IconLabel=self:GetFromObjectPool("ImageLabel") --see below.
--set various properties of the IconLabel to what we want.
IconLabel.Selectable=false
IconLabel.Size=UDim2.new(0,iconSize.X,0,iconSize.Y)
IconLabel.Position=UDim2.new(0,0,0,0)
IconLabel.BackgroundTransparency=1
IconLabel.ImageTransparency=0
IconLabel.ScaleType=Enum.ScaleType.Fit
IconLabel.ImageColor3=Color3.new(1,1,1)
--Assign the IconLabel the image we want.
IconLabel.Image=iconAddress
IconLabel.Visible=true
--Add it to the base message.
IconLabel.Parent=BaseMessage
--Return it to wherever the function was called from.
return IconLabel
end
You may have noticed that we didn’t use Instance.new
to get an ImageLabel
, but instead a function called GetFromObjectPool
. This is a function included in the chat system that recycles previously-used objects instead of deleting them. However, because we’re using recycled objects, we need to be careful to not assume any of the object’s default properties, because they may have been changed by whatever they were used for previously.
Conclusion
I was inspired by a thread questioning about whether or not it was possible, and after reading a whole lot of documentation and getting it working, I figured it would be a fun thing to share. I learned a lot about editing existing programs, and I hope you did too!
Finally, if you really don’t want to edit the scripts yourself, I’ve posted a drag-and-drop solution as a free model, which you can get here:
I hope you’ve found this guide helpful! Let me know if you have any questions/issues.
Errata
- 10/24/2019: The part of the
ExtraDataInitializer
that applied the player’s icons was mistakenly put inside of the conditional statement checking if the tags were already applied. This would cause the icons to sometimes not be applied if another module had already applied tags to the player. A fix has been published to the module and this post has been edited to rectify the mistake. Thanks to @steam_driven for helping figure out the issue.