local function D(D,J,I,f,z)this.ChatBubble[D]=createChatBubbleMain(J,z)this.ChatBubbleWithTail[D]=createChatBubbleWithTail(J,UDim2.new(.5,-CHAT_BUBBLE_TAIL_HEIGHT,1,f and-1 or 0),UDim2.new(0,30,0,CHAT_BUBBLE_TAIL_HEIGHT),z)this.ScalingChatBubbleWithTail[D]=createScaledChatBubbleWithTail(J,.5,UDim2.new(-0.5,0,0,f and-1 or 0),z)end D(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))D(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))D(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))D(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(J,D)if getMessageLength(D)>MaxChatMessageLengthExclusive then local J=utf8.offset(D,(MaxChatMessageLengthExclusive+getMessageLength(ELIPSES))+1)-1 return string.sub(D,1,J)else return D end end local function J(D)local J=Instance.new("\066\105\108\108\098\111\097\114\100\071\117\105")J.Adornee=D J.Size=UDim2.new(0,BILLBOARD_MAX_WIDTH,0,BILLBOARD_MAX_HEIGHT)J.StudsOffset=Vector3.new(0,1.5,2)J.Parent=BubbleChatScreenGui local I=Instance.new("\070\114\097\109\101")I.Name="\066\105\108\108\098\111\097\114\100\070\114\097\109\101"I.Size=UDim2.new(1,0,1,0)I.Position=UDim2.new(0,0,-0.7,0)I.BackgroundTransparency=1 I.Parent=J local f=nil f=I.ChildRemoved:connect(function()if#I:GetChildren()<=1 then f:disconnect()J:Destroy()end end);(this:CreateSmallTalkBubble(BubbleColor.WHITE)).Parent=I return J end function this.CreateBillboardGuiHelper(f,D,I)if D and not(this.CharacterSortedMsg:Get(D)).BillboardGui then if not I then if D:IsA("\066\097\115\101\080\097\114\116")then local I=J(D);(this.CharacterSortedMsg:Get(D)).BillboardGui=I return end end if D:IsA("\077\111\100\101\108")then local I=D:FindFirstChild("\072\101\097\100")if I and I:IsA("\066\097\115\101\080\097\114\116")then local f=J(I);(this.CharacterSortedMsg:Get(D)).BillboardGui=f end end end end local function I(D)if not D then return 100000 end return(D.Position-game.Workspace.CurrentCamera.CoordinateFrame.p).magnitude end local function f(D)if D and PlayersService.LocalPlayer.Character then return D:IsDescendantOf(PlayersService.LocalPlayer.Character)end end function this.SetBillboardLODNear(J,D)local I=f(D.Adornee)D.Size=UDim2.new(0,BILLBOARD_MAX_WIDTH,0,BILLBOARD_MAX_HEIGHT)D.StudsOffset=Vector3.new(0,I and 1.5 or 2.5,I and 2 or.1)D.Enabled=true local z=D.BillboardFrame:GetChildren()for D=1,#z,1 do z[D].Visible=true end D.BillboardFrame.SmallTalkBubble.Visible=false end function this.SetBillboardLODDistant(J,D)local I=f(D.Adornee)D.Size=UDim2.new(4,0,3,0)D.StudsOffset=Vector3.new(0,3,I and 2 or.1)D.Enabled=true local z=D.BillboardFrame:GetChildren()for D=1,#z,1 do z[D].Visible=false end D.BillboardFrame.SmallTalkBubble.Visible=true end function this.SetBillboardLODVeryFar(J,D)D.Enabled=false end function this.SetBillboardGuiLOD(f,D,J)if not J then return end if J:IsA("\077\111\100\101\108")then local D=J:FindFirstChild("\072\101\097\100")if not D then J=J.PrimaryPart else J=D end end local z=I(J)if z<NEAR_BUBBLE_DISTANCE then this:SetBillboardLODNear(D)elseif z>=NEAR_BUBBLE_DISTANCE and z<MAX_BUBBLE_DISTANCE then this:SetBillboardLODDistant(D)else this:SetBillboardLODVeryFar(D)end end function this.CameraCFrameChanged(D)for D,J in pairs(this.CharacterSortedMsg:GetData())do local I=J.BillboardGui if I then this:SetBillboardGuiLOD(I,D)end end end function this.CreateBubbleText(I,D,J)local f=Instance.new("\084\101\120\116\076\097\098\101\108")f.Name="\066\117\098\098\108\101\084\101\120\116"f.BackgroundTransparency=1 if UserFixBubbleChatText then f.Size=UDim2.fromScale(1,1)else f.Position=UDim2.new(0,CHAT_BUBBLE_WIDTH_PADDING/2,0,0)f.Size=UDim2.new(1,-CHAT_BUBBLE_WIDTH_PADDING,1,0)end f.Font=CHAT_BUBBLE_FONT f.ClipsDescendants=true f.TextWrapped=true f.FontSize=CHAT_BUBBLE_FONT_SIZE f.Text=D f.Visible=false f.AutoLocalize=J if UserFixBubbleChatText then local D=Instance.new("\085\073\080\097\100\100\105\110\103")D.PaddingTop=UDim.new(0,CHAT_BUBBLE_PADDING)D.PaddingRight=UDim.new(0,CHAT_BUBBLE_PADDING)D.PaddingBottom=UDim.new(0,CHAT_BUBBLE_PADDING)D.PaddingLeft=UDim.new(0,CHAT_BUBBLE_PADDING)D.Parent=f end return f end function this.CreateSmallTalkBubble(J,D)local I=this.ScalingChatBubbleWithTail[D]:Clone()I.Name="\083\109\097\108\108\084\097\108\107\066\117\098\098\108\101"I.AnchorPoint=Vector2.new(0,.5)I.Position=UDim2.new(0,0,.5,0)I.Visible=false local f=this:CreateBubbleText("\046\046\046")f.TextScaled=true f.TextWrapped=false f.Visible=true f.Parent=I return I end function this.UpdateChatLinesForOrigin(I,D,J)local f=(this.CharacterSortedMsg:Get(D)).Fifo local z=f:Size()local T=f:GetData()if#T<=1 then return end for D=#T-1,1,-1 do local I=T[D]local f=I.RenderBubble if not f then return end local g=(z-D)+1 if g>1 then local D=f:FindFirstChild("\067\104\097\116\066\117\098\098\108\101\084\097\105\108")if D then D:Destroy()end local J=f:FindFirstChild("\066\117\098\098\108\101\084\101\120\116")if J then J.TextTransparency=.5 end end local y=UDim2.new(f.Position.X.Scale,f.Position.X.Offset,1,(J-f.Size.Y.Offset)-CHAT_BUBBLE_TAIL_HEIGHT)f:TweenPosition(y,Enum.EasingDirection.Out,Enum.EasingStyle.Bounce,.1,true)J=(J-f.Size.Y.Offset)-CHAT_BUBBLE_TAIL_HEIGHT end end function this.DestroyBubble(I,D,J)if not D then return end if D:Empty()then return end local f=(D:Front()).RenderBubble if not f then D:PopFront()return end spawn(function()while(D:Front()).RenderBubble~=J do wait()end f=(D:Front()).RenderBubble local I=0 local z=f:FindFirstChild("\066\117\098\098\108\101\084\101\120\116")local T=f:FindFirstChild("\067\104\097\116\066\117\098\098\108\101\084\097\105\108")while f and f.ImageTransparency<1 do I=wait()if f then local D=I*CHAT_BUBBLE_FADE_SPEED f.ImageTransparency=f.ImageTransparency+D if z then z.TextTransparency=z.TextTransparency+D end if T then T.ImageTransparency=T.ImageTransparency+D end end end if f then f:Destroy()D:PopFront()end end)end function this.CreateChatLineRender(T,D,J,I,f,z)if not D then return end if not(this.CharacterSortedMsg:Get(D)).BillboardGui then this:CreateBillboardGuiHelper(D,I)end local g=(this.CharacterSortedMsg:Get(D)).BillboardGui if g then local D=this.ChatBubbleWithTail[J.BubbleColor]:Clone()D.Visible=false local I=this:CreateBubbleText(J.Message,z)I.Parent=D D.Parent=g.BillboardFrame J.RenderBubble=D local T=TextService:GetTextSize(I.Text,CHAT_BUBBLE_FONT_SIZE_INT,CHAT_BUBBLE_FONT,Vector2.new(BILLBOARD_MAX_WIDTH,BILLBOARD_MAX_HEIGHT))local y=T.Y/CHAT_BUBBLE_FONT_SIZE_INT if UserFixBubbleChatText then local f=math.ceil(T.X+CHAT_BUBBLE_PADDING*2)local z=y*CHAT_BUBBLE_LINE_HEIGHT D.Size=UDim2.fromOffset(0,0)D.Position=UDim2.fromScale(.5,1)D:TweenSizeAndPosition(UDim2.fromOffset(f,z),UDim2.new(.5,-f/2,1,-z),Enum.EasingDirection.Out,Enum.EasingStyle.Elastic,.1,true,function()I.Visible=true end)this:SetBillboardGuiLOD(g,J.Origin)this:UpdateChatLinesForOrigin(J.Origin,-z)else local f=math.max((T.X+CHAT_BUBBLE_WIDTH_PADDING)/BILLBOARD_MAX_WIDTH,.1)D.Size=UDim2.new(0,0,0,0)D.Position=UDim2.new(.5,0,1,0)local z=y*CHAT_BUBBLE_LINE_HEIGHT D:TweenSizeAndPosition(UDim2.new(f,0,0,z),UDim2.new((1-f)/2,0,1,-z),Enum.EasingDirection.Out,Enum.EasingStyle.Elastic,.1,true,function()I.Visible=true end)this:SetBillboardGuiLOD(g,J.Origin)this:UpdateChatLinesForOrigin(J.Origin,-z)end delay(J.BubbleDieDelay,function()this:DestroyBubble(f,D)end)end end function this.OnPlayerChatMessage(f,D,J,I)if not this:BubbleChatEnabled()then return end local z=PlayersService.LocalPlayer local T=z~=nil and D~=z local g=this:SanitizeChatLine(J)local y=createPlayerChatLine(D,g,not T)if D and y.Origin then local J=(this.CharacterSortedMsg:Get(y.Origin)).Fifo J:PushBack(y)this:CreateChatLineRender(D.Character,y,true,J,false)end end function this.OnGameChatMessage(f,D,J,I)if UserRoactBubbleChatBeta or UserPreventOldBubbleChatOverlap and ChatService.BubbleChatEnabled then return end local z=PlayersService.LocalPlayer local T=z~=nil and z.Character~=D local g=BubbleColor.WHITE if I==Enum.ChatColor.Blue then g=BubbleColor.BLUE elseif I==Enum.ChatColor.Green then g=BubbleColor.GREEN elseif I==Enum.ChatColor.Red then g=BubbleColor.RED end local y=this:SanitizeChatLine(J)local Z=createGameChatLine(D,y,not T,g);(this.CharacterSortedMsg:Get(Z.Origin)).Fifo:PushBack(Z)if UserShouldLocalizeGameChatBubble then this:CreateChatLineRender(D,Z,false,(this.CharacterSortedMsg:Get(Z.Origin)).Fifo,true)else this:CreateChatLineRender(D,Z,false,(this.CharacterSortedMsg:Get(Z.Origin)).Fifo,false)end end function this.BubbleChatEnabled(D)if UserRoactBubbleChatBeta or UserPreventOldBubbleChatOverlap and ChatService.BubbleChatEnabled then return false end local J=ChatService:FindFirstChild("\067\108\105\101\110\116\067\104\097\116\077\111\100\117\108\101\115")if J then local D=J:FindFirstChild("\067\104\097\116\083\101\116\116\105\110\103\115")if D then local J=require(D)if J.BubbleChatEnabled~=nil then return J.BubbleChatEnabled end end end return PlayersService.BubbleChat end function this.ShowOwnFilteredMessage(D)local J=ChatService:FindFirstChild("\067\108\105\101\110\116\067\104\097\116\077\111\100\117\108\101\115")if J then local D=J:FindFirstChild("\067\104\097\116\083\101\116\116\105\110\103\115")if D then D=require(D)return D.ShowUserOwnFilteredMessage end end return false end function findPlayer(D)for J,I in pairs(PlayersService:GetPlayers())do if I.Name==D then return I end end end ChatService.Chatted:connect(function(D,J,I)this:OnGameChatMessage(D,J,I)end)local z=nil if game.Workspace.CurrentCamera then z=(game.Workspace.CurrentCamera:GetPropertyChangedSignal("\067\070\114\097\109\101")):connect(function(D)this:CameraCFrameChanged()end)end game.Workspace.Changed:connect(function(D)if D=="\067\117\114\114\101\110\116\067\097\109\101\114\097"then if z then z:disconnect()end if game.Workspace.CurrentCamera then z=(game.Workspace.CurrentCamera:GetPropertyChangedSignal("\067\070\114\097\109\101")):connect(function(D)this:CameraCFrameChanged()end)end end end)local T=nil function getAllowedMessageTypes()if T then return T end local D=ChatService:FindFirstChild("\067\108\105\101\110\116\067\104\097\116\077\111\100\117\108\101\115")if D then local J=D:FindFirstChild("\067\104\097\116\083\101\116\116\105\110\103\115")if J then J=require(J)if J.BubbleChatMessageTypes then T=J.BubbleChatMessageTypes return T end end local I=D:FindFirstChild("\067\104\097\116\067\111\110\115\116\097\110\116\115")if I then I=require(I)T={I.MessageTypeDefault,I.MessageTypeWhisper}end return T end return{"\077\101\115\115\097\103\101","\087\104\105\115\112\101\114"}end function checkAllowedMessageType(D)local J=getAllowedMessageTypes()for I=1,#J,1 do if J[I]==D.MessageType then return true end end return false end local g=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 y=g:WaitForChild("\079\110\077\101\115\115\097\103\101\068\111\110\101\070\105\108\116\101\114\105\110\103")local Z=g:WaitForChild("\079\110\078\101\119\077\101\115\115\097\103\101")Z.OnClientEvent:connect(function(D,J)if not checkAllowedMessageType(D)then return end local I=findPlayer(D.FromSpeaker)if not I then return end if not D.IsFiltered or D.FromSpeaker==LocalPlayer.Name then if D.FromSpeaker~=LocalPlayer.Name or this:ShowOwnFilteredMessage()then return end end this:OnPlayerChatMessage(I,D.Message,nil)end)y.OnClientEvent:connect(function(D,J)if not checkAllowedMessageType(D)then return end local I=findPlayer(D.FromSpeaker)if not I then return end if D.FromSpeaker==LocalPlayer.Name and not this:ShowOwnFilteredMessage()then return end this:OnPlayerChatMessage(I,D.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 getMessageLength(msg) > MaxChatMessageLengthExclusive then
		local byteOffset = utf8.offset(msg, MaxChatMessageLengthExclusive + getMessageLength(ELIPSES) + 1) - 1
		return string.sub(msg, 1, byteOffset)
	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.7, 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, shouldAutoLocalize)
	local bubbleText = Instance.new("TextLabel")
	bubbleText.Name = "BubbleText"
	bubbleText.BackgroundTransparency = 1

	if UserFixBubbleChatText then
		bubbleText.Size = UDim2.fromScale(1, 1)
	else
		bubbleText.Position = UDim2.new(0, CHAT_BUBBLE_WIDTH_PADDING / 2, 0, 0)
		bubbleText.Size = UDim2.new(1, -CHAT_BUBBLE_WIDTH_PADDING, 1, 0)
	end

	bubbleText.Font = CHAT_BUBBLE_FONT
	bubbleText.ClipsDescendants = true
	bubbleText.TextWrapped = true
	bubbleText.FontSize = CHAT_BUBBLE_FONT_SIZE
	bubbleText.Text = message
	bubbleText.Visible = false
	bubbleText.AutoLocalize = shouldAutoLocalize

	if UserFixBubbleChatText then
		local padding = Instance.new("UIPadding")
		padding.PaddingTop = UDim.new(0, CHAT_BUBBLE_PADDING)
		padding.PaddingRight = UDim.new(0, CHAT_BUBBLE_PADDING)
		padding.PaddingBottom = UDim.new(0, CHAT_BUBBLE_PADDING)
		padding.PaddingLeft = UDim.new(0, CHAT_BUBBLE_PADDING)
		padding.Parent = bubbleText
	end

	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, shouldAutoLocalize)
	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, shouldAutoLocalize)

		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 numOflines = (currentTextBounds.Y / CHAT_BUBBLE_FONT_SIZE_INT)

		if UserFixBubbleChatText then
			-- Need to use math.ceil to round up on retina displays
			local width = math.ceil(currentTextBounds.X + CHAT_BUBBLE_PADDING * 2)
			local height = numOflines * CHAT_BUBBLE_LINE_HEIGHT

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

			chatBubbleRender:TweenSizeAndPosition(
				UDim2.fromOffset(width, height),
				UDim2.new(0.5, -width / 2, 1, -height),
				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, -height)
		else
			local bubbleWidthScale = math.max((currentTextBounds.X + CHAT_BUBBLE_WIDTH_PADDING) / BILLBOARD_MAX_WIDTH, 0.1)

			-- 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)
		end

		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, false)
	end
end

function this:OnGameChatMessage(origin, message, color)
	-- Prevents conflicts with the new bubble chat if it is enabled
	if UserRoactBubbleChatBeta or (UserPreventOldBubbleChatOverlap and ChatService.BubbleChatEnabled) then
		return
	end

	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)
	if UserShouldLocalizeGameChatBubble then
		this:CreateChatLineRender(origin, line, false, this.CharacterSortedMsg:Get(line.Origin).Fifo, true)
	else
		this:CreateChatLineRender(origin, line, false, this.CharacterSortedMsg:Get(line.Origin).Fifo, false)
	end
end

function this:BubbleChatEnabled()
	if UserRoactBubbleChatBeta or (UserPreventOldBubbleChatOverlap and ChatService.BubbleChatEnabled) then
		return false
	end
	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)
