local function c(c,f,V,Y,W)this.ChatBubble[c]=createChatBubbleMain(f,W)this.ChatBubbleWithTail[c]=createChatBubbleWithTail(f,UDim2.new(.5,-CHAT_BUBBLE_TAIL_HEIGHT,1,Y and-1 or 0),UDim2.new(0,30,0,CHAT_BUBBLE_TAIL_HEIGHT),W)this.ScalingChatBubbleWithTail[c]=createScaledChatBubbleWithTail(f,.5,UDim2.new(-0.5,0,0,Y and-1 or 0),W)end c(BubbleColor.WHITE,"\117\105\047\100\105\097\108\111\103\095\119\104\105\116\101","\117\105\047\099\104\097\116\066\117\098\098\108\101\095\119\104\105\116\101\095\110\111\116\105\102\121\095\098\107\103",false,Rect.new(5,5,15,15))c(BubbleColor.BLUE,"\117\105\047\100\105\097\108\111\103\095\098\108\117\101","\117\105\047\099\104\097\116\066\117\098\098\108\101\095\098\108\117\101\095\110\111\116\105\102\121\095\098\107\103",true,Rect.new(7,7,33,33))c(BubbleColor.RED,"\117\105\047\100\105\097\108\111\103\095\114\101\100","\117\105\047\099\104\097\116\066\117\098\098\108\101\095\114\101\100\095\110\111\116\105\102\121\095\098\107\103",true,Rect.new(7,7,33,33))c(BubbleColor.GREEN,"\117\105\047\100\105\097\108\111\103\095\103\114\101\101\110","\117\105\047\099\104\097\116\066\117\098\098\108\101\095\103\114\101\101\110\095\110\111\116\105\102\121\095\098\107\103",true,Rect.new(7,7,33,33))function this.SanitizeChatLine(f,c)if string.len(c)>MaxChatMessageLengthExclusive then return string.sub(c,1,MaxChatMessageLengthExclusive+string.len(ELIPSES))else return c end end local function f(c)local f=Instance.new("\066\105\108\108\098\111\097\114\100\071\117\105")f.Adornee=c f.Size=UDim2.new(0,BILLBOARD_MAX_WIDTH,0,BILLBOARD_MAX_HEIGHT)f.StudsOffset=Vector3.new(0,1.5,2)f.Parent=BubbleChatScreenGui local V=Instance.new("\070\114\097\109\101")V.Name="\066\105\108\108\098\111\097\114\100\070\114\097\109\101"V.Size=UDim2.new(1,0,1,0)V.Position=UDim2.new(0,0,-0.5,0)V.BackgroundTransparency=1 V.Parent=f local Y=nil Y=V.ChildRemoved:connect(function()if#V:GetChildren()<=1 then Y:disconnect()f:Destroy()end end);(this:CreateSmallTalkBubble(BubbleColor.WHITE)).Parent=V return f end function this.CreateBillboardGuiHelper(Y,c,V)if c and not(this.CharacterSortedMsg:Get(c)).BillboardGui then if not V then if c:IsA("\066\097\115\101\080\097\114\116")then local V=f(c);(this.CharacterSortedMsg:Get(c)).BillboardGui=V return end end if c:IsA("\077\111\100\101\108")then local V=c:FindFirstChild("\072\101\097\100")if V and V:IsA("\066\097\115\101\080\097\114\116")then local Y=f(V);(this.CharacterSortedMsg:Get(c)).BillboardGui=Y end end end end local function V(c)if not c then return 100000 end return(c.Position-game.Workspace.CurrentCamera.CoordinateFrame.p).magnitude end local function Y(c)if c and PlayersService.LocalPlayer.Character then return c:IsDescendantOf(PlayersService.LocalPlayer.Character)end end function this.SetBillboardLODNear(f,c)local V=Y(c.Adornee)c.Size=UDim2.new(0,BILLBOARD_MAX_WIDTH,0,BILLBOARD_MAX_HEIGHT)c.StudsOffset=Vector3.new(0,V and 1.5 or 2.5,V and 2 or.1)c.Enabled=true local W=c.BillboardFrame:GetChildren()for c=1,#W,1 do W[c].Visible=true end c.BillboardFrame.SmallTalkBubble.Visible=false end function this.SetBillboardLODDistant(f,c)local V=Y(c.Adornee)c.Size=UDim2.new(4,0,3,0)c.StudsOffset=Vector3.new(0,3,V and 2 or.1)c.Enabled=true local W=c.BillboardFrame:GetChildren()for c=1,#W,1 do W[c].Visible=false end c.BillboardFrame.SmallTalkBubble.Visible=true end function this.SetBillboardLODVeryFar(f,c)c.Enabled=false end function this.SetBillboardGuiLOD(Y,c,f)if not f then return end if f:IsA("\077\111\100\101\108")then local c=f:FindFirstChild("\072\101\097\100")if not c then f=f.PrimaryPart else f=c end end local W=V(f)if W<NEAR_BUBBLE_DISTANCE then this:SetBillboardLODNear(c)elseif W>=NEAR_BUBBLE_DISTANCE and W<MAX_BUBBLE_DISTANCE then this:SetBillboardLODDistant(c)else this:SetBillboardLODVeryFar(c)end end function this.CameraCFrameChanged(c)for c,f in pairs(this.CharacterSortedMsg:GetData())do local V=f.BillboardGui if V then this:SetBillboardGuiLOD(V,c)end end end function this.CreateBubbleText(f,c)local V=Instance.new("\084\101\120\116\076\097\098\101\108")V.Name="\066\117\098\098\108\101\084\101\120\116"V.TextColor3=Color3.new(255,255,255)V.BackgroundTransparency=1 V.Position=UDim2.new(0,CHAT_BUBBLE_WIDTH_PADDING/2,0,0)V.Size=UDim2.new(1,-CHAT_BUBBLE_WIDTH_PADDING,1,0)V.Font=CHAT_BUBBLE_FONT if shouldClipInGameChat then V.ClipsDescendants=true end V.TextWrapped=true V.FontSize=CHAT_BUBBLE_FONT_SIZE V.Text=c V.Visible=false return V end function this.CreateSmallTalkBubble(f,c)local V=this.ScalingChatBubbleWithTail[c]:Clone()V.Name="\083\109\097\108\108\084\097\108\107\066\117\098\098\108\101"V.AnchorPoint=Vector2.new(0,.5)V.Position=UDim2.new(0,0,.5,0)V.Visible=false local Y=this:CreateBubbleText("\046\046\046")Y.TextScaled=true Y.TextWrapped=false Y.Visible=true Y.Parent=V return V end function this.UpdateChatLinesForOrigin(V,c,f)local Y=(this.CharacterSortedMsg:Get(c)).Fifo local W=Y:Size()local y=Y:GetData()if#y<=1 then return end for c=#y-1,1,-1 do local V=y[c]local Y=V.RenderBubble if not Y then return end local M=(W-c)+1 if M>1 then local c=Y:FindFirstChild("\067\104\097\116\066\117\098\098\108\101\084\097\105\108")if c then c:Destroy()end local f=Y:FindFirstChild("\066\117\098\098\108\101\084\101\120\116")if f then f.TextTransparency=.5 end end local v=UDim2.new(Y.Position.X.Scale,Y.Position.X.Offset,1,(f-Y.Size.Y.Offset)-CHAT_BUBBLE_TAIL_HEIGHT)Y:TweenPosition(v,Enum.EasingDirection.Out,Enum.EasingStyle.Bounce,.1,true)f=(f-Y.Size.Y.Offset)-CHAT_BUBBLE_TAIL_HEIGHT end end function this.DestroyBubble(V,c,f)if not c then return end if c:Empty()then return end local Y=(c:Front()).RenderBubble if not Y then c:PopFront()return end spawn(function()while(c:Front()).RenderBubble~=f do wait()end Y=(c:Front()).RenderBubble local V=0 local W=Y:FindFirstChild("\066\117\098\098\108\101\084\101\120\116")local y=Y:FindFirstChild("\067\104\097\116\066\117\098\098\108\101\084\097\105\108")while Y and Y.ImageTransparency<1 do V=wait()if Y then local c=V*CHAT_BUBBLE_FADE_SPEED Y.ImageTransparency=Y.ImageTransparency+c if W then W.TextTransparency=W.TextTransparency+c end if y then y.ImageTransparency=y.ImageTransparency+c end end end if Y then Y:Destroy()c:PopFront()end end)end function this.CreateChatLineRender(W,c,f,V,Y)if not c then return end if not(this.CharacterSortedMsg:Get(c)).BillboardGui then this:CreateBillboardGuiHelper(c,V)end local y=(this.CharacterSortedMsg:Get(c)).BillboardGui if y then local c=this.ChatBubbleWithTail[f.BubbleColor]:Clone()c.Visible=false local V=this:CreateBubbleText(f.Message)V.Parent=c c.Parent=y.BillboardFrame f.RenderBubble=c local W=TextService:GetTextSize(V.Text,CHAT_BUBBLE_FONT_SIZE_INT,CHAT_BUBBLE_FONT,Vector2.new(BILLBOARD_MAX_WIDTH,BILLBOARD_MAX_HEIGHT))local M=math.max((W.X+CHAT_BUBBLE_WIDTH_PADDING)/BILLBOARD_MAX_WIDTH,.1)local v=W.Y/CHAT_BUBBLE_FONT_SIZE_INT c.Size=UDim2.new(0,0,0,0)c.Position=UDim2.new(.5,0,1,0)local G=v*CHAT_BUBBLE_LINE_HEIGHT c:TweenSizeAndPosition(UDim2.new(M,0,0,G),UDim2.new((1-M)/2,0,1,-G),Enum.EasingDirection.Out,Enum.EasingStyle.Elastic,.1,true,function()V.Visible=true end)this:SetBillboardGuiLOD(y,f.Origin)this:UpdateChatLinesForOrigin(f.Origin,-G)delay(f.BubbleDieDelay,function()this:DestroyBubble(Y,c)end)end end function this.OnPlayerChatMessage(Y,c,f,V)if not this:BubbleChatEnabled()then return end local W=PlayersService.LocalPlayer local y=W~=nil and c~=W local M=this:SanitizeChatLine(f)local v=createPlayerChatLine(c,M,not y)if c and v.Origin then local f=(this.CharacterSortedMsg:Get(v.Origin)).Fifo f:PushBack(v)this:CreateChatLineRender(c.Character,v,true,f)end end function this.OnGameChatMessage(Y,c,f,V)local W=PlayersService.LocalPlayer local y=W~=nil and W.Character~=c local M=BubbleColor.WHITE if V==Enum.ChatColor.Blue then M=BubbleColor.BLUE elseif V==Enum.ChatColor.Green then M=BubbleColor.GREEN elseif V==Enum.ChatColor.Red then M=BubbleColor.RED end local v=this:SanitizeChatLine(f)local G=createGameChatLine(c,v,not y,M);(this.CharacterSortedMsg:Get(G.Origin)).Fifo:PushBack(G)this:CreateChatLineRender(c,G,false,(this.CharacterSortedMsg:Get(G.Origin)).Fifo)end function this.BubbleChatEnabled(c)local f=ChatService:FindFirstChild("\067\108\105\101\110\116\067\104\097\116\077\111\100\117\108\101\115")if f then local c=f:FindFirstChild("\067\104\097\116\083\101\116\116\105\110\103\115")if c then local f=require(c)if f.BubbleChatEnabled~=nil then return f.BubbleChatEnabled end end end return PlayersService.BubbleChat end function this.ShowOwnFilteredMessage(c)local f=ChatService:FindFirstChild("\067\108\105\101\110\116\067\104\097\116\077\111\100\117\108\101\115")if f then local c=f:FindFirstChild("\067\104\097\116\083\101\116\116\105\110\103\115")if c then c=require(c)return c.ShowUserOwnFilteredMessage end end return false end function findPlayer(c)for f,V in pairs(PlayersService:GetPlayers())do if V.Name==c then return V end end end ChatService.Chatted:connect(function(c,f,V)this:OnGameChatMessage(c,f,V)end)local W=nil if game.Workspace.CurrentCamera then W=(game.Workspace.CurrentCamera:GetPropertyChangedSignal("\067\070\114\097\109\101")):connect(function(c)this:CameraCFrameChanged()end)end game.Workspace.Changed:connect(function(c)if c=="\067\117\114\114\101\110\116\067\097\109\101\114\097"then if W then W:disconnect()end if game.Workspace.CurrentCamera then W=(game.Workspace.CurrentCamera:GetPropertyChangedSignal("\067\070\114\097\109\101")):connect(function(c)this:CameraCFrameChanged()end)end end end)local y=nil function getAllowedMessageTypes()if y then return y end local c=ChatService:FindFirstChild("\067\108\105\101\110\116\067\104\097\116\077\111\100\117\108\101\115")if c then local f=c:FindFirstChild("\067\104\097\116\083\101\116\116\105\110\103\115")if f then f=require(f)if f.BubbleChatMessageTypes then y=f.BubbleChatMessageTypes return y end end local V=c:FindFirstChild("\067\104\097\116\067\111\110\115\116\097\110\116\115")if V then V=require(V)y={V.MessageTypeDefault;V.MessageTypeWhisper}end return y end return{"\077\101\115\115\097\103\101";"\087\104\105\115\112\101\114"}end function checkAllowedMessageType(c)local f=getAllowedMessageTypes()for V=1,#f,1 do if f[V]==c.MessageType then return true end end return false end local M=ReplicatedStorage:WaitForChild("\068\101\102\097\117\108\116\067\104\097\116\083\121\115\116\101\109\067\104\097\116\069\118\101\110\116\115")local v=M:WaitForChild("\079\110\077\101\115\115\097\103\101\068\111\110\101\070\105\108\116\101\114\105\110\103")local G=M:WaitForChild("\079\110\078\101\119\077\101\115\115\097\103\101")G.OnClientEvent:connect(function(c,f)if not checkAllowedMessageType(c)then return end local V=findPlayer(c.FromSpeaker)if not V then return end if not c.IsFiltered or c.FromSpeaker==LocalPlayer.Name then if c.FromSpeaker~=LocalPlayer.Name or this:ShowOwnFilteredMessage()then return end end this:OnPlayerChatMessage(V,c.Message,nil)end)v.OnClientEvent:connect(function(c,f)if not checkAllowedMessageType(c)then return end local V=findPlayer(c.FromSpeaker)if not V then return end if c.FromSpeaker==LocalPlayer.Name and not this:ShowOwnFilteredMessage()then return end this:OnPlayerChatMessage(V,c.Message,nil)end)

local function initChatBubbleType(chatBubbleType, fileName, imposterFileName, isInset, sliceRect)
	this.ChatBubble[chatBubbleType] = createChatBubbleMain(fileName, sliceRect)
	this.ChatBubbleWithTail[chatBubbleType] = createChatBubbleWithTail(fileName, UDim2.new(0.5, -CHAT_BUBBLE_TAIL_HEIGHT, 1, isInset and -1 or 0), UDim2.new(0, 30, 0, CHAT_BUBBLE_TAIL_HEIGHT), sliceRect)
	this.ScalingChatBubbleWithTail[chatBubbleType] = createScaledChatBubbleWithTail(fileName, 0.5, UDim2.new(-0.5, 0, 0, isInset and -1 or 0), sliceRect)
end

initChatBubbleType(BubbleColor.WHITE,	"ui/dialog_white",	"ui/chatBubble_white_notify_bkg", 	false,	Rect.new(5,5,15,15))
initChatBubbleType(BubbleColor.BLUE,	"ui/dialog_blue",	"ui/chatBubble_blue_notify_bkg",	true, 	Rect.new(7,7,33,33))
initChatBubbleType(BubbleColor.RED,		"ui/dialog_red",	"ui/chatBubble_red_notify_bkg",		true,	Rect.new(7,7,33,33))
initChatBubbleType(BubbleColor.GREEN,	"ui/dialog_green",	"ui/chatBubble_green_notify_bkg",	true,	Rect.new(7,7,33,33))

function this:SanitizeChatLine(msg)
	if string.len(msg) > MaxChatMessageLengthExclusive then
		return string.sub(msg, 1, MaxChatMessageLengthExclusive + string.len(ELIPSES))
	else
		return msg
	end
end

local function createBillboardInstance(adornee)
	local billboardGui = Instance.new("BillboardGui")
	billboardGui.Adornee = adornee
	billboardGui.Size = UDim2.new(0,BILLBOARD_MAX_WIDTH,0,BILLBOARD_MAX_HEIGHT)
	billboardGui.StudsOffset = Vector3.new(0, 1.5, 2)
	billboardGui.Parent = BubbleChatScreenGui

	local billboardFrame = Instance.new("Frame")
	billboardFrame.Name = "BillboardFrame"
	billboardFrame.Size = UDim2.new(1,0,1,0)
	billboardFrame.Position = UDim2.new(0,0,-0.5,0)
	billboardFrame.BackgroundTransparency = 1
	billboardFrame.Parent = billboardGui

	local billboardChildRemovedCon = nil
	billboardChildRemovedCon = billboardFrame.ChildRemoved:connect(function()
		if #billboardFrame:GetChildren() <= 1 then
			billboardChildRemovedCon:disconnect()
			billboardGui:Destroy()
		end
	end)

	this:CreateSmallTalkBubble(BubbleColor.WHITE).Parent = billboardFrame

	return billboardGui
end

function this:CreateBillboardGuiHelper(instance, onlyCharacter)
	if instance and not this.CharacterSortedMsg:Get(instance)["BillboardGui"] then
		if not onlyCharacter then
			if instance:IsA("BasePart") then
				-- Create a new billboardGui object attached to this player
				local billboardGui = createBillboardInstance(instance)
				this.CharacterSortedMsg:Get(instance)["BillboardGui"] = billboardGui
				return
			end
		end

		if instance:IsA("Model") then
			local head = instance:FindFirstChild("Head")
			if head and head:IsA("BasePart") then
				-- Create a new billboardGui object attached to this player
				local billboardGui = createBillboardInstance(head)
				this.CharacterSortedMsg:Get(instance)["BillboardGui"] = billboardGui
			end
		end
	end
end

local function distanceToBubbleOrigin(origin)
	if not origin then return 100000 end

	return (origin.Position - game.Workspace.CurrentCamera.CoordinateFrame.p).magnitude
end

local function isPartOfLocalPlayer(adornee)
	if adornee and PlayersService.LocalPlayer.Character then
		return adornee:IsDescendantOf(PlayersService.LocalPlayer.Character)
	end
end

function this:SetBillboardLODNear(billboardGui)
	local isLocalPlayer = isPartOfLocalPlayer(billboardGui.Adornee)
	billboardGui.Size = UDim2.new(0, BILLBOARD_MAX_WIDTH, 0, BILLBOARD_MAX_HEIGHT)
	billboardGui.StudsOffset = Vector3.new(0, isLocalPlayer and 1.5 or 2.5, isLocalPlayer and 2 or 0.1)
	billboardGui.Enabled = true
	local billChildren = billboardGui.BillboardFrame:GetChildren()
	for i = 1, #billChildren do
		billChildren[i].Visible = true
	end
	billboardGui.BillboardFrame.SmallTalkBubble.Visible = false
end

function this:SetBillboardLODDistant(billboardGui)
	local isLocalPlayer = isPartOfLocalPlayer(billboardGui.Adornee)
	billboardGui.Size = UDim2.new(4,0,3,0)
	billboardGui.StudsOffset = Vector3.new(0, 3, isLocalPlayer and 2 or 0.1)
	billboardGui.Enabled = true
	local billChildren = billboardGui.BillboardFrame:GetChildren()
	for i = 1, #billChildren do
		billChildren[i].Visible = false
	end
	billboardGui.BillboardFrame.SmallTalkBubble.Visible = true
end

function this:SetBillboardLODVeryFar(billboardGui)
	billboardGui.Enabled = false
end

function this:SetBillboardGuiLOD(billboardGui, origin)
	if not origin then return end

	if origin:IsA("Model") then
		local head = origin:FindFirstChild("Head")
		if not head then origin = origin.PrimaryPart
		else origin = head end
	end

	local bubbleDistance = distanceToBubbleOrigin(origin)

	if bubbleDistance < NEAR_BUBBLE_DISTANCE then
		this:SetBillboardLODNear(billboardGui)
	elseif bubbleDistance >= NEAR_BUBBLE_DISTANCE and bubbleDistance < MAX_BUBBLE_DISTANCE then
		this:SetBillboardLODDistant(billboardGui)
	else
		this:SetBillboardLODVeryFar(billboardGui)
	end
end

function this:CameraCFrameChanged()
	for index, value in pairs(this.CharacterSortedMsg:GetData()) do
		local playerBillboardGui = value["BillboardGui"]
		if playerBillboardGui then this:SetBillboardGuiLOD(playerBillboardGui, index) end
	end
end

function this:CreateBubbleText(message)
	local bubbleText = Instance.new("TextLabel")
	bubbleText.Name = "BubbleText"
	bubbleText.TextColor3 = Color3.new(255,255,255)
	bubbleText.BackgroundTransparency = 1
	bubbleText.Position = UDim2.new(0,CHAT_BUBBLE_WIDTH_PADDING/2,0,0)
	bubbleText.Size = UDim2.new(1,-CHAT_BUBBLE_WIDTH_PADDING,1,0)
	bubbleText.Font = CHAT_BUBBLE_FONT
	if shouldClipInGameChat then
		bubbleText.ClipsDescendants = true
	end
	bubbleText.TextWrapped = true
	bubbleText.FontSize = CHAT_BUBBLE_FONT_SIZE
	bubbleText.Text = message
	bubbleText.Visible = false

	return bubbleText
end

function this:CreateSmallTalkBubble(chatBubbleType)
	local smallTalkBubble = this.ScalingChatBubbleWithTail[chatBubbleType]:Clone()
	smallTalkBubble.Name = "SmallTalkBubble"
	smallTalkBubble.AnchorPoint = Vector2.new(0, 0.5)
	smallTalkBubble.Position = UDim2.new(0,0,0.5,0)
	smallTalkBubble.Visible = false
	local text = this:CreateBubbleText("...")
	text.TextScaled = true
	text.TextWrapped = false
	text.Visible = true
	text.Parent = smallTalkBubble

	return smallTalkBubble
end

function this:UpdateChatLinesForOrigin(origin, currentBubbleYPos)
	local bubbleQueue = this.CharacterSortedMsg:Get(origin).Fifo
	local bubbleQueueSize = bubbleQueue:Size()
	local bubbleQueueData = bubbleQueue:GetData()
	if #bubbleQueueData <= 1 then return end

	for index = (#bubbleQueueData - 1), 1, -1 do
		local value = bubbleQueueData[index]
		local bubble = value.RenderBubble
		if not bubble then return end
		local bubblePos = bubbleQueueSize - index + 1

		if bubblePos > 1 then
			local tail = bubble:FindFirstChild("ChatBubbleTail")
			if tail then tail:Destroy() end
			local bubbleText = bubble:FindFirstChild("BubbleText")
			if bubbleText then bubbleText.TextTransparency = 0.5 end
		end

		local udimValue = UDim2.new( bubble.Position.X.Scale, bubble.Position.X.Offset,
									1, currentBubbleYPos - bubble.Size.Y.Offset - CHAT_BUBBLE_TAIL_HEIGHT )
		bubble:TweenPosition(udimValue, Enum.EasingDirection.Out, Enum.EasingStyle.Bounce, 0.1, true)
		currentBubbleYPos = currentBubbleYPos - bubble.Size.Y.Offset - CHAT_BUBBLE_TAIL_HEIGHT
	end
end

function this:DestroyBubble(bubbleQueue, bubbleToDestroy)
	if not bubbleQueue then return end
	if bubbleQueue:Empty() then return end

	local bubble = bubbleQueue:Front().RenderBubble
	if not bubble then
		bubbleQueue:PopFront()
	 	return
	end

	spawn(function()
		while bubbleQueue:Front().RenderBubble ~= bubbleToDestroy do
			wait()
		end

		bubble = bubbleQueue:Front().RenderBubble

		local timeBetween = 0
		local bubbleText = bubble:FindFirstChild("BubbleText")
		local bubbleTail = bubble:FindFirstChild("ChatBubbleTail")

		while bubble and bubble.ImageTransparency < 1 do
			timeBetween = wait()
			if bubble then
				local fadeAmount = timeBetween * CHAT_BUBBLE_FADE_SPEED
				bubble.ImageTransparency = bubble.ImageTransparency + fadeAmount
				if bubbleText then bubbleText.TextTransparency = bubbleText.TextTransparency + fadeAmount end
				if bubbleTail then bubbleTail.ImageTransparency = bubbleTail.ImageTransparency + fadeAmount end
			end
		end

		if bubble then
			bubble:Destroy()
			bubbleQueue:PopFront()
		end
	end)
end

function this:CreateChatLineRender(instance, line, onlyCharacter, fifo)
	if not instance then return end

	if not this.CharacterSortedMsg:Get(instance)["BillboardGui"] then
		this:CreateBillboardGuiHelper(instance, onlyCharacter)
	end

	local billboardGui = this.CharacterSortedMsg:Get(instance)["BillboardGui"]
	if billboardGui then
		local chatBubbleRender = this.ChatBubbleWithTail[line.BubbleColor]:Clone()
		chatBubbleRender.Visible = false
		local bubbleText = this:CreateBubbleText(line.Message)

		bubbleText.Parent = chatBubbleRender
		chatBubbleRender.Parent = billboardGui.BillboardFrame

		line.RenderBubble = chatBubbleRender

		local currentTextBounds = TextService:GetTextSize(
				bubbleText.Text, CHAT_BUBBLE_FONT_SIZE_INT, CHAT_BUBBLE_FONT,
				Vector2.new(BILLBOARD_MAX_WIDTH, BILLBOARD_MAX_HEIGHT))
		local bubbleWidthScale = math.max((currentTextBounds.X + CHAT_BUBBLE_WIDTH_PADDING)/BILLBOARD_MAX_WIDTH, 0.1)
		local numOflines = (currentTextBounds.Y/CHAT_BUBBLE_FONT_SIZE_INT)

		-- prep chat bubble for tween
		chatBubbleRender.Size = UDim2.new(0,0,0,0)
		chatBubbleRender.Position = UDim2.new(0.5,0,1,0)

		local newChatBubbleOffsetSizeY = numOflines * CHAT_BUBBLE_LINE_HEIGHT

		chatBubbleRender:TweenSizeAndPosition(UDim2.new(bubbleWidthScale, 0, 0, newChatBubbleOffsetSizeY),
											 	UDim2.new( (1-bubbleWidthScale)/2, 0, 1, -newChatBubbleOffsetSizeY),
											 	Enum.EasingDirection.Out, Enum.EasingStyle.Elastic, 0.1, true,
											 	function() bubbleText.Visible = true end)

		-- todo: remove when over max bubbles
		this:SetBillboardGuiLOD(billboardGui, line.Origin)
		this:UpdateChatLinesForOrigin(line.Origin, -newChatBubbleOffsetSizeY)

		delay(line.BubbleDieDelay, function()
			this:DestroyBubble(fifo, chatBubbleRender)
		end)
	end
end

function this:OnPlayerChatMessage(sourcePlayer, message, targetPlayer)
	if not this:BubbleChatEnabled() then return end

	local localPlayer = PlayersService.LocalPlayer
	local fromOthers = localPlayer ~= nil and sourcePlayer ~= localPlayer

	local safeMessage = this:SanitizeChatLine(message)

	local line = createPlayerChatLine(sourcePlayer, safeMessage, not fromOthers)

	if sourcePlayer and line.Origin then
		local fifo = this.CharacterSortedMsg:Get(line.Origin).Fifo
		fifo:PushBack(line)
		--Game chat (badges) won't show up here
		this:CreateChatLineRender(sourcePlayer.Character, line, true, fifo)
	end
end

function this:OnGameChatMessage(origin, message, color)
	local localPlayer = PlayersService.LocalPlayer
	local fromOthers = localPlayer ~= nil and (localPlayer.Character ~= origin)

	local bubbleColor = BubbleColor.WHITE

	if color == Enum.ChatColor.Blue then bubbleColor = BubbleColor.BLUE
	elseif color == Enum.ChatColor.Green then bubbleColor = BubbleColor.GREEN
	elseif color == Enum.ChatColor.Red then bubbleColor = BubbleColor.RED end

	local safeMessage = this:SanitizeChatLine(message)
	local line = createGameChatLine(origin, safeMessage, not fromOthers, bubbleColor)

	this.CharacterSortedMsg:Get(line.Origin).Fifo:PushBack(line)
	this:CreateChatLineRender(origin, line, false, this.CharacterSortedMsg:Get(line.Origin).Fifo)
end

function this:BubbleChatEnabled()
	local clientChatModules = ChatService:FindFirstChild("ClientChatModules")
	if clientChatModules then
		local chatSettings = clientChatModules:FindFirstChild("ChatSettings")
		if chatSettings then
			local chatSettings = require(chatSettings)
			if chatSettings.BubbleChatEnabled ~= nil then
				return chatSettings.BubbleChatEnabled
			end
		end
	end
	return PlayersService.BubbleChat
end

function this:ShowOwnFilteredMessage()
	local clientChatModules = ChatService:FindFirstChild("ClientChatModules")
	if clientChatModules then
		local chatSettings = clientChatModules:FindFirstChild("ChatSettings")
		if chatSettings then
			chatSettings = require(chatSettings)
			return chatSettings.ShowUserOwnFilteredMessage
		end
	end
	return false
end

function findPlayer(playerName)
	for i,v in pairs(PlayersService:GetPlayers()) do
		if v.Name == playerName then
			return v
		end
	end
end

ChatService.Chatted:connect(function(origin, message, color) this:OnGameChatMessage(origin, message, color) end)

local cameraChangedCon = nil
if game.Workspace.CurrentCamera then
	cameraChangedCon = game.Workspace.CurrentCamera:GetPropertyChangedSignal("CFrame"):connect(function(prop) this:CameraCFrameChanged() end)
end

game.Workspace.Changed:connect(function(prop)
	if prop == "CurrentCamera" then
		if cameraChangedCon then cameraChangedCon:disconnect() end
		if game.Workspace.CurrentCamera then
			cameraChangedCon = game.Workspace.CurrentCamera:GetPropertyChangedSignal("CFrame"):connect(function(prop) this:CameraCFrameChanged() end)
		end
	end
end)


local AllowedMessageTypes = nil

function getAllowedMessageTypes()
	if AllowedMessageTypes then
		return AllowedMessageTypes
	end
	local clientChatModules = ChatService:FindFirstChild("ClientChatModules")
	if clientChatModules then
		local chatSettings = clientChatModules:FindFirstChild("ChatSettings")
		if chatSettings then
			chatSettings = require(chatSettings)
			if chatSettings.BubbleChatMessageTypes then
				AllowedMessageTypes = chatSettings.BubbleChatMessageTypes
				return AllowedMessageTypes
			end
		end
		local chatConstants = clientChatModules:FindFirstChild("ChatConstants")
		if chatConstants then
			chatConstants = require(chatConstants)
			AllowedMessageTypes = {chatConstants.MessageTypeDefault, chatConstants.MessageTypeWhisper}
		end
		return AllowedMessageTypes
	end
	return {"Message", "Whisper"}
end

function checkAllowedMessageType(messageData)
	local allowedMessageTypes = getAllowedMessageTypes()
	for i = 1, #allowedMessageTypes do
		if allowedMessageTypes[i] == messageData.MessageType then
			return true
		end
	end
	return false
end

local ChatEvents = ReplicatedStorage:WaitForChild("DefaultChatSystemChatEvents")
local OnMessageDoneFiltering = ChatEvents:WaitForChild("OnMessageDoneFiltering")
local OnNewMessage = ChatEvents:WaitForChild("OnNewMessage")

OnNewMessage.OnClientEvent:connect(function(messageData, channelName)
	if not checkAllowedMessageType(messageData) then
		return
	end

	local sender = findPlayer(messageData.FromSpeaker)
	if not sender then
		return
	end

	if not messageData.IsFiltered or messageData.FromSpeaker == LocalPlayer.Name then
		if messageData.FromSpeaker ~= LocalPlayer.Name or this:ShowOwnFilteredMessage() then
			return
		end
	end

	this:OnPlayerChatMessage(sender, messageData.Message, nil)
end)

OnMessageDoneFiltering.OnClientEvent:connect(function(messageData, channelName)
	if not checkAllowedMessageType(messageData) then
		return
	end

	local sender = findPlayer(messageData.FromSpeaker)
	if not sender then
		return
	end

	if messageData.FromSpeaker == LocalPlayer.Name and not this:ShowOwnFilteredMessage() then
		return
	end

	this:OnPlayerChatMessage(sender, messageData.Message, nil)
end)
