This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,7 @@
{
"lint": {
"SameLineStatement": "fatal",
"LocalUnused": "fatal",
"ImportUnused": "fatal"
}
}
@@ -0,0 +1,794 @@
--[[
// FileName: BubbleChat.lua
// Written by: jeditkacheff, TheGamer101
// Description: Code for rendering bubble chat
]]
--[[ SERVICES ]]
local PlayersService = game:GetService('Players')
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ChatService = game:GetService("Chat")
local TextService = game:GetService("TextService")
--[[ END OF SERVICES ]]
local LocalPlayer = PlayersService.LocalPlayer
while LocalPlayer == nil do
PlayersService.ChildAdded:wait()
LocalPlayer = PlayersService.LocalPlayer
end
local PlayerGui = LocalPlayer:WaitForChild("PlayerGui")
local success, UserShouldLocalizeGameChatBubble = pcall(function()
return UserSettings():IsUserFeatureEnabled("UserShouldLocalizeGameChatBubble")
end)
local UserShouldLocalizeGameChatBubble = success and UserShouldLocalizeGameChatBubble
local UserFixBubbleChatText do
local success, value = pcall(function()
return UserSettings():IsUserFeatureEnabled("UserFixBubbleChatText")
end)
UserFixBubbleChatText = success and value
end
local UserRoactBubbleChatBeta do
local success, value = pcall(function()
return UserSettings():IsUserFeatureEnabled("UserRoactBubbleChatBeta")
end)
UserRoactBubbleChatBeta = success and value
end
local UserPreventOldBubbleChatOverlap do
local success, value = pcall(function()
return UserSettings():IsUserFeatureEnabled("UserPreventOldBubbleChatOverlap")
end)
UserPreventOldBubbleChatOverlap = success and value
end
local function getMessageLength(message)
return utf8.len(utf8.nfcnormalize(message))
end
--[[ SCRIPT VARIABLES ]]
local CHAT_BUBBLE_FONT = Enum.Font.SourceSans
local CHAT_BUBBLE_FONT_SIZE = Enum.FontSize.Size24 -- if you change CHAT_BUBBLE_FONT_SIZE_INT please change this to match
local CHAT_BUBBLE_FONT_SIZE_INT = 24 -- if you change CHAT_BUBBLE_FONT_SIZE please change this to match
local CHAT_BUBBLE_LINE_HEIGHT = CHAT_BUBBLE_FONT_SIZE_INT + 10
local CHAT_BUBBLE_TAIL_HEIGHT = 14
local CHAT_BUBBLE_WIDTH_PADDING = 30
local CHAT_BUBBLE_PADDING = 12
local CHAT_BUBBLE_FADE_SPEED = 1.5
local BILLBOARD_MAX_WIDTH = 400
local BILLBOARD_MAX_HEIGHT = 250 --This limits the number of bubble chats that you see above characters
local ELIPSES = "..."
local MaxChatMessageLength = 128 -- max chat message length, including null terminator and elipses.
local MaxChatMessageLengthExclusive = MaxChatMessageLength - getMessageLength(ELIPSES) - 1
local NEAR_BUBBLE_DISTANCE = 65 --previously 45
local MAX_BUBBLE_DISTANCE = 100 --previously 80
--[[ END OF SCRIPT VARIABLES ]]
-- [[ SCRIPT ENUMS ]]
local BubbleColor = { WHITE = "dub",
BLUE = "blu",
GREEN = "gre",
RED = "red" }
--[[ END OF SCRIPT ENUMS ]]
-- This screenGui exists so that the billboardGui is not deleted when the PlayerGui is reset.
local BubbleChatScreenGui = Instance.new("ScreenGui")
BubbleChatScreenGui.Name = "BubbleChat"
BubbleChatScreenGui.ResetOnSpawn = false
BubbleChatScreenGui.Parent = PlayerGui
--[[ FUNCTIONS ]]
local function lerpLength(msg, min, max)
return min + (max - min) * math.min(getMessageLength(msg) / 75.0, 1.0)
end
local function createFifo()
local this = {}
this.data = {}
local emptyEvent = Instance.new("BindableEvent")
this.Emptied = emptyEvent.Event
function this:Size()
return #this.data
end
function this:Empty()
return this:Size() <= 0
end
function this:PopFront()
table.remove(this.data, 1)
if this:Empty() then emptyEvent:Fire() end
end
function this:Front()
return this.data[1]
end
function this:Get(index)
return this.data[index]
end
function this:PushBack(value)
table.insert(this.data, value)
end
function this:GetData()
return this.data
end
return this
end
local function createCharacterChats()
local this = {}
this.Fifo = createFifo()
this.BillboardGui = nil
return this
end
local function createMap()
local this = {}
this.data = {}
local count = 0
function this:Size()
return count
end
function this:Erase(key)
if this.data[key] then count = count - 1 end
this.data[key] = nil
end
function this:Set(key, value)
this.data[key] = value
if value then count = count + 1 end
end
function this:Get(key)
if not key then return end
if not this.data[key] then
this.data[key] = createCharacterChats()
local emptiedCon = nil
emptiedCon = this.data[key].Fifo.Emptied:connect(function()
emptiedCon:disconnect()
this:Erase(key)
end)
end
return this.data[key]
end
function this:GetData()
return this.data
end
return this
end
local function createChatLine(message, bubbleColor, isLocalPlayer)
local this = {}
function this:ComputeBubbleLifetime(msg, isSelf)
if isSelf then
return lerpLength(msg, 8, 15)
else
return lerpLength(msg, 12, 20)
end
end
this.Origin = nil
this.RenderBubble = nil
this.Message = message
this.BubbleDieDelay = this:ComputeBubbleLifetime(message, isLocalPlayer)
this.BubbleColor = bubbleColor
this.IsLocalPlayer = isLocalPlayer
return this
end
local function createPlayerChatLine(player, message, isLocalPlayer)
local this = createChatLine(message, BubbleColor.WHITE, isLocalPlayer)
if player then
this.User = player.Name
this.Origin = player.Character
end
return this
end
local function createGameChatLine(origin, message, isLocalPlayer, bubbleColor)
local this = createChatLine(message, bubbleColor, isLocalPlayer)
this.Origin = origin
return this
end
function createChatBubbleMain(filePrefix, sliceRect)
local chatBubbleMain = Instance.new("ImageLabel")
chatBubbleMain.Name = "ChatBubble"
chatBubbleMain.ScaleType = Enum.ScaleType.Slice
chatBubbleMain.SliceCenter = sliceRect
chatBubbleMain.Image = "rbxasset://textures/" .. tostring(filePrefix) .. ".png"
chatBubbleMain.BackgroundTransparency = 1
chatBubbleMain.BorderSizePixel = 0
chatBubbleMain.Size = UDim2.new(1.0, 0, 1.0, 0)
chatBubbleMain.Position = UDim2.new(0, 0, 0, 0)
return chatBubbleMain
end
function createChatBubbleTail(position, size)
local chatBubbleTail = Instance.new("ImageLabel")
chatBubbleTail.Name = "ChatBubbleTail"
chatBubbleTail.Image = "rbxasset://textures/ui/dialog_tail.png"
chatBubbleTail.BackgroundTransparency = 1
chatBubbleTail.BorderSizePixel = 0
chatBubbleTail.Position = position
chatBubbleTail.Size = size
return chatBubbleTail
end
function createChatBubbleWithTail(filePrefix, position, size, sliceRect)
local chatBubbleMain = createChatBubbleMain(filePrefix, sliceRect)
local chatBubbleTail = createChatBubbleTail(position, size)
chatBubbleTail.Parent = chatBubbleMain
return chatBubbleMain
end
function createScaledChatBubbleWithTail(filePrefix, frameScaleSize, position, sliceRect)
local chatBubbleMain = createChatBubbleMain(filePrefix, sliceRect)
local frame = Instance.new("Frame")
frame.Name = "ChatBubbleTailFrame"
frame.BackgroundTransparency = 1
frame.SizeConstraint = Enum.SizeConstraint.RelativeXX
frame.Position = UDim2.new(0.5, 0, 1, 0)
frame.Size = UDim2.new(frameScaleSize, 0, frameScaleSize, 0)
frame.Parent = chatBubbleMain
local chatBubbleTail = createChatBubbleTail(position, UDim2.new(1, 0, 0.5, 0))
chatBubbleTail.Parent = frame
return chatBubbleMain
end
function createChatImposter(filePrefix, dotDotDot, yOffset)
local result = Instance.new("ImageLabel")
result.Name = "DialogPlaceholder"
result.Image = "rbxasset://textures/" .. tostring(filePrefix) .. ".png"
result.BackgroundTransparency = 1
result.BorderSizePixel = 0
result.Position = UDim2.new(0, 0, -1.25, 0)
result.Size = UDim2.new(1, 0, 1, 0)
local image = Instance.new("ImageLabel")
image.Name = "DotDotDot"
image.Image = "rbxasset://textures/" .. tostring(dotDotDot) .. ".png"
image.BackgroundTransparency = 1
image.BorderSizePixel = 0
image.Position = UDim2.new(0.001, 0, yOffset, 0)
image.Size = UDim2.new(1, 0, 0.7, 0)
image.Parent = result
return result
end
local this = {}
this.ChatBubble = {}
this.ChatBubbleWithTail = {}
this.ScalingChatBubbleWithTail = {}
this.CharacterSortedMsg = createMap()
-- init chat bubble tables
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.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, 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)
@@ -0,0 +1,400 @@
-- // FileName: ChannelsBar.lua
-- // Written by: Xsitsu
-- // Description: Manages creating, destroying, and displaying ChannelTabs.
local module = {}
local PlayerGui = game:GetService("Players").LocalPlayer:WaitForChild("PlayerGui")
--////////////////////////////// Include
--//////////////////////////////////////
local Chat = game:GetService("Chat")
local clientChatModules = Chat:WaitForChild("ClientChatModules")
local modulesFolder = script.Parent
local moduleChannelsTab = require(modulesFolder:WaitForChild("ChannelsTab"))
local MessageSender = require(modulesFolder:WaitForChild("MessageSender"))
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local CurveUtil = require(modulesFolder:WaitForChild("CurveUtil"))
--////////////////////////////// Methods
--//////////////////////////////////////
local methods = {}
methods.__index = methods
function methods:CreateGuiObjects(targetParent)
local BaseFrame = Instance.new("Frame")
BaseFrame.Selectable = false
BaseFrame.Size = UDim2.new(1, 0, 1, 0)
BaseFrame.BackgroundTransparency = 1
BaseFrame.Parent = targetParent
local ScrollingBase = Instance.new("Frame")
ScrollingBase.Selectable = false
ScrollingBase.Name = "ScrollingBase"
ScrollingBase.BackgroundTransparency = 1
ScrollingBase.ClipsDescendants = true
ScrollingBase.Size = UDim2.new(1, 0, 1, 0)
ScrollingBase.Position = UDim2.new(0, 0, 0, 0)
ScrollingBase.Parent = BaseFrame
local ScrollerSizer = Instance.new("Frame")
ScrollerSizer.Selectable = false
ScrollerSizer.Name = "ScrollerSizer"
ScrollerSizer.BackgroundTransparency = 1
ScrollerSizer.Size = UDim2.new(1, 0, 1, 0)
ScrollerSizer.Position = UDim2.new(0, 0, 0, 0)
ScrollerSizer.Parent = ScrollingBase
local ScrollerFrame = Instance.new("Frame")
ScrollerFrame.Selectable = false
ScrollerFrame.Name = "ScrollerFrame"
ScrollerFrame.BackgroundTransparency = 1
ScrollerFrame.Size = UDim2.new(1, 0, 1, 0)
ScrollerFrame.Position = UDim2.new(0, 0, 0, 0)
ScrollerFrame.Parent = ScrollerSizer
local LeaveConfirmationFrameBase = Instance.new("Frame")
LeaveConfirmationFrameBase.Selectable = false
LeaveConfirmationFrameBase.Size = UDim2.new(1, 0, 1, 0)
LeaveConfirmationFrameBase.Position = UDim2.new(0, 0, 0, 0)
LeaveConfirmationFrameBase.ClipsDescendants = true
LeaveConfirmationFrameBase.BackgroundTransparency = 1
LeaveConfirmationFrameBase.Parent = BaseFrame
local LeaveConfirmationFrame = Instance.new("Frame")
LeaveConfirmationFrame.Selectable = false
LeaveConfirmationFrame.Name = "LeaveConfirmationFrame"
LeaveConfirmationFrame.Size = UDim2.new(1, 0, 1, 0)
LeaveConfirmationFrame.Position = UDim2.new(0, 0, 1, 0)
LeaveConfirmationFrame.BackgroundTransparency = 0.6
LeaveConfirmationFrame.BorderSizePixel = 0
LeaveConfirmationFrame.BackgroundColor3 = Color3.new(0, 0, 0)
LeaveConfirmationFrame.Parent = LeaveConfirmationFrameBase
local InputBlocker = Instance.new("TextButton")
InputBlocker.Selectable = false
InputBlocker.Size = UDim2.new(1, 0, 1, 0)
InputBlocker.BackgroundTransparency = 1
InputBlocker.Text = ""
InputBlocker.Parent = LeaveConfirmationFrame
local LeaveConfirmationButtonYes = Instance.new("TextButton")
LeaveConfirmationButtonYes.Selectable = false
LeaveConfirmationButtonYes.Size = UDim2.new(0.25, 0, 1, 0)
LeaveConfirmationButtonYes.BackgroundTransparency = 1
LeaveConfirmationButtonYes.Font = ChatSettings.DefaultFont
LeaveConfirmationButtonYes.TextSize = 18
LeaveConfirmationButtonYes.TextStrokeTransparency = 0.75
LeaveConfirmationButtonYes.Position = UDim2.new(0, 0, 0, 0)
LeaveConfirmationButtonYes.TextColor3 = Color3.new(0, 1, 0)
LeaveConfirmationButtonYes.Text = "Confirm"
LeaveConfirmationButtonYes.Parent = LeaveConfirmationFrame
local LeaveConfirmationButtonNo = LeaveConfirmationButtonYes:Clone()
LeaveConfirmationButtonNo.Parent = LeaveConfirmationFrame
LeaveConfirmationButtonNo.Position = UDim2.new(0.75, 0, 0, 0)
LeaveConfirmationButtonNo.TextColor3 = Color3.new(1, 0, 0)
LeaveConfirmationButtonNo.Text = "Cancel"
local LeaveConfirmationNotice = Instance.new("TextLabel")
LeaveConfirmationNotice.Selectable = false
LeaveConfirmationNotice.Size = UDim2.new(0.5, 0, 1, 0)
LeaveConfirmationNotice.Position = UDim2.new(0.25, 0, 0, 0)
LeaveConfirmationNotice.BackgroundTransparency = 1
LeaveConfirmationNotice.TextColor3 = Color3.new(1, 1, 1)
LeaveConfirmationNotice.TextStrokeTransparency = 0.75
LeaveConfirmationNotice.Text = "Leave channel <XX>?"
LeaveConfirmationNotice.Font = ChatSettings.DefaultFont
LeaveConfirmationNotice.TextSize = 18
LeaveConfirmationNotice.Parent = LeaveConfirmationFrame
local LeaveTarget = Instance.new("StringValue")
LeaveTarget.Name = "LeaveTarget"
LeaveTarget.Parent = LeaveConfirmationFrame
local outPos = LeaveConfirmationFrame.Position
LeaveConfirmationButtonYes.MouseButton1Click:connect(function()
MessageSender:SendMessage(string.format("/leave %s", LeaveTarget.Value), nil)
LeaveConfirmationFrame:TweenPosition(outPos, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.2, true)
end)
LeaveConfirmationButtonNo.MouseButton1Click:connect(function()
LeaveConfirmationFrame:TweenPosition(outPos, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.2, true)
end)
local scale = 0.7
local scaleOther = (1 - scale) / 2
local pageButtonImage = "rbxasset://textures/ui/Chat/TabArrowBackground.png"
local pageButtonArrowImage = "rbxasset://textures/ui/Chat/TabArrow.png"
--// ToDo: Remove these lines when the assets are put into trunk.
--// These grab unchanging versions hosted on the site, and not from the content folder.
pageButtonImage = "rbxassetid://471630199"
pageButtonArrowImage = "rbxassetid://471630112"
local PageLeftButton = Instance.new("ImageButton", BaseFrame)
PageLeftButton.Selectable = ChatSettings.GamepadNavigationEnabled
PageLeftButton.Name = "PageLeftButton"
PageLeftButton.SizeConstraint = Enum.SizeConstraint.RelativeYY
PageLeftButton.Size = UDim2.new(scale, 0, scale, 0)
PageLeftButton.BackgroundTransparency = 1
PageLeftButton.Position = UDim2.new(0, 4, scaleOther, 0)
PageLeftButton.Visible = false
PageLeftButton.Image = pageButtonImage
local ArrowLabel = Instance.new("ImageLabel", PageLeftButton)
ArrowLabel.Name = "ArrowLabel"
ArrowLabel.BackgroundTransparency = 1
ArrowLabel.Size = UDim2.new(0.4, 0, 0.4, 0)
ArrowLabel.Image = pageButtonArrowImage
local PageRightButtonPositionalHelper = Instance.new("Frame", BaseFrame)
PageRightButtonPositionalHelper.Selectable = false
PageRightButtonPositionalHelper.BackgroundTransparency = 1
PageRightButtonPositionalHelper.Name = "PositionalHelper"
PageRightButtonPositionalHelper.Size = PageLeftButton.Size
PageRightButtonPositionalHelper.SizeConstraint = PageLeftButton.SizeConstraint
PageRightButtonPositionalHelper.Position = UDim2.new(1, 0, scaleOther, 0)
local PageRightButton = PageLeftButton:Clone()
PageRightButton.Parent = PageRightButtonPositionalHelper
PageRightButton.Name = "PageRightButton"
PageRightButton.Size = UDim2.new(1, 0, 1, 0)
PageRightButton.SizeConstraint = Enum.SizeConstraint.RelativeXY
PageRightButton.Position = UDim2.new(-1, -4, 0, 0)
local positionOffset = UDim2.new(0.05, 0, 0, 0)
PageRightButton.ArrowLabel.Position = UDim2.new(0.3, 0, 0.3, 0) + positionOffset
PageLeftButton.ArrowLabel.Position = UDim2.new(0.3, 0, 0.3, 0) - positionOffset
PageLeftButton.ArrowLabel.Rotation = 180
self.GuiObject = BaseFrame
self.GuiObjects.BaseFrame = BaseFrame
self.GuiObjects.ScrollerSizer = ScrollerSizer
self.GuiObjects.ScrollerFrame = ScrollerFrame
self.GuiObjects.PageLeftButton = PageLeftButton
self.GuiObjects.PageRightButton = PageRightButton
self.GuiObjects.LeaveConfirmationFrame = LeaveConfirmationFrame
self.GuiObjects.LeaveConfirmationNotice = LeaveConfirmationNotice
self.GuiObjects.PageLeftButtonArrow = PageLeftButton.ArrowLabel
self.GuiObjects.PageRightButtonArrow = PageRightButton.ArrowLabel
self:AnimGuiObjects()
PageLeftButton.MouseButton1Click:connect(function() self:ScrollChannelsFrame(-1) end)
PageRightButton.MouseButton1Click:connect(function() self:ScrollChannelsFrame(1) end)
self:ScrollChannelsFrame(0)
end
function methods:UpdateMessagePostedInChannel(channelName)
local tab = self:GetChannelTab(channelName)
if (tab) then
tab:UpdateMessagePostedInChannel()
else
warn("ChannelsTab '" .. channelName .. "' does not exist!")
end
end
function methods:AddChannelTab(channelName)
if (self:GetChannelTab(channelName)) then
error("Channel tab '" .. channelName .. "'already exists!")
end
local tab = moduleChannelsTab.new(channelName)
tab.GuiObject.Parent = self.GuiObjects.ScrollerFrame
self.ChannelTabs[channelName:lower()] = tab
self.NumTabs = self.NumTabs + 1
self:OrganizeChannelTabs()
if (ChatSettings.RightClickToLeaveChannelEnabled) then
tab.NameTag.MouseButton2Click:connect(function()
self.LeaveConfirmationNotice.Text = string.format("Leave channel %s?", tab.ChannelName)
self.LeaveConfirmationFrame.LeaveTarget.Value = tab.ChannelName
self.LeaveConfirmationFrame:TweenPosition(UDim2.new(0, 0, 0, 0), Enum.EasingDirection.In, Enum.EasingStyle.Quad, 0.2, true)
end)
end
return tab
end
function methods:RemoveChannelTab(channelName)
if (not self:GetChannelTab(channelName)) then
error("Channel tab '" .. channelName .. "'does not exist!")
end
local indexName = channelName:lower()
self.ChannelTabs[indexName]:Destroy()
self.ChannelTabs[indexName] = nil
self.NumTabs = self.NumTabs - 1
self:OrganizeChannelTabs()
end
function methods:GetChannelTab(channelName)
return self.ChannelTabs[channelName:lower()]
end
function methods:OrganizeChannelTabs()
local order = {}
table.insert(order, self:GetChannelTab(ChatSettings.GeneralChannelName))
table.insert(order, self:GetChannelTab("System"))
for tabIndexName, tab in pairs(self.ChannelTabs) do
if (tab.ChannelName ~= ChatSettings.GeneralChannelName and tab.ChannelName ~= "System") then
table.insert(order, tab)
end
end
for index, tab in pairs(order) do
tab.GuiObject.Position = UDim2.new(index - 1, 0, 0, 0)
end
--// Dynamic tab resizing
self.GuiObjects.ScrollerSizer.Size = UDim2.new(1 / math.max(1, math.min(ChatSettings.ChannelsBarFullTabSize, self.NumTabs)), 0, 1, 0)
self:ScrollChannelsFrame(0)
end
function methods:ResizeChannelTabText(textSize)
for i, tab in pairs(self.ChannelTabs) do
tab:SetTextSize(textSize)
end
end
function methods:ScrollChannelsFrame(dir)
if (self.ScrollChannelsFrameLock) then return end
self.ScrollChannelsFrameLock = true
local tabNumber = ChatSettings.ChannelsBarFullTabSize
local newPageNum = self.CurPageNum + dir
if (newPageNum < 0) then
newPageNum = 0
elseif (newPageNum > 0 and newPageNum + tabNumber > self.NumTabs) then
newPageNum = self.NumTabs - tabNumber
end
self.CurPageNum = newPageNum
local tweenTime = 0.15
local endPos = UDim2.new(-self.CurPageNum, 0, 0, 0)
self.GuiObjects.PageLeftButton.Visible = (self.CurPageNum > 0)
self.GuiObjects.PageRightButton.Visible = (self.CurPageNum + tabNumber < self.NumTabs)
if dir == 0 then
self.ScrollChannelsFrameLock = false
return
end
local function UnlockFunc()
self.ScrollChannelsFrameLock = false
end
self:WaitUntilParentedCorrectly()
self.GuiObjects.ScrollerFrame:TweenPosition(endPos, Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, tweenTime, true, UnlockFunc)
end
function methods:FadeOutBackground(duration)
for channelName, channelObj in pairs(self.ChannelTabs) do
channelObj:FadeOutBackground(duration)
end
self.AnimParams.Background_TargetTransparency = 1
self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
end
function methods:FadeInBackground(duration)
for channelName, channelObj in pairs(self.ChannelTabs) do
channelObj:FadeInBackground(duration)
end
self.AnimParams.Background_TargetTransparency = 0.6
self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
end
function methods:FadeOutText(duration)
for channelName, channelObj in pairs(self.ChannelTabs) do
channelObj:FadeOutText(duration)
end
end
function methods:FadeInText(duration)
for channelName, channelObj in pairs(self.ChannelTabs) do
channelObj:FadeInText(duration)
end
end
function methods:AnimGuiObjects()
self.GuiObjects.PageLeftButton.ImageTransparency = self.AnimParams.Background_CurrentTransparency
self.GuiObjects.PageRightButton.ImageTransparency = self.AnimParams.Background_CurrentTransparency
self.GuiObjects.PageLeftButtonArrow.ImageTransparency = self.AnimParams.Background_CurrentTransparency
self.GuiObjects.PageRightButtonArrow.ImageTransparency = self.AnimParams.Background_CurrentTransparency
end
function methods:InitializeAnimParams()
self.AnimParams.Background_TargetTransparency = 0.6
self.AnimParams.Background_CurrentTransparency = 0.6
self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(0)
end
function methods:Update(dtScale)
for channelName, channelObj in pairs(self.ChannelTabs) do
channelObj:Update(dtScale)
end
self.AnimParams.Background_CurrentTransparency = CurveUtil:Expt(
self.AnimParams.Background_CurrentTransparency,
self.AnimParams.Background_TargetTransparency,
self.AnimParams.Background_NormalizedExptValue,
dtScale
)
self:AnimGuiObjects()
end
--// ToDo: Move to common modules
function methods:WaitUntilParentedCorrectly()
while (not self.GuiObject:IsDescendantOf(game:GetService("Players").LocalPlayer)) do
self.GuiObject.AncestryChanged:wait()
end
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new()
local obj = setmetatable({}, methods)
obj.GuiObject = nil
obj.GuiObjects = {}
obj.ChannelTabs = {}
obj.NumTabs = 0
obj.CurPageNum = 0
obj.ScrollChannelsFrameLock = false
obj.AnimParams = {}
obj:InitializeAnimParams()
ChatSettings.SettingsChanged:connect(function(setting, value)
if (setting == "ChatChannelsTabTextSize") then
obj:ResizeChannelTabText(value)
end
end)
return obj
end
return module
@@ -0,0 +1,310 @@
-- // FileName: ChannelsTab.lua
-- // Written by: Xsitsu
-- // Description: Channel tab button for selecting current channel and also displaying if currently selected.
local module = {}
--////////////////////////////// Include
--//////////////////////////////////////
local Chat = game:GetService("Chat")
local clientChatModules = Chat:WaitForChild("ClientChatModules")
local modulesFolder = script.Parent
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local CurveUtil = require(modulesFolder:WaitForChild("CurveUtil"))
--////////////////////////////// Methods
--//////////////////////////////////////
local methods = {}
methods.__index = methods
local function CreateGuiObjects()
local BaseFrame = Instance.new("Frame")
BaseFrame.Selectable = false
BaseFrame.Size = UDim2.new(1, 0, 1, 0)
BaseFrame.BackgroundTransparency = 1
local gapOffsetX = 1
local gapOffsetY = 1
local BackgroundFrame = Instance.new("Frame")
BackgroundFrame.Selectable = false
BackgroundFrame.Name = "BackgroundFrame"
BackgroundFrame.Size = UDim2.new(1, -gapOffsetX * 2, 1, -gapOffsetY * 2)
BackgroundFrame.Position = UDim2.new(0, gapOffsetX, 0, gapOffsetY)
BackgroundFrame.BackgroundTransparency = 1
BackgroundFrame.Parent = BaseFrame
local UnselectedFrame = Instance.new("Frame")
UnselectedFrame.Selectable = false
UnselectedFrame.Name = "UnselectedFrame"
UnselectedFrame.Size = UDim2.new(1, 0, 1, 0)
UnselectedFrame.Position = UDim2.new(0, 0, 0, 0)
UnselectedFrame.BorderSizePixel = 0
UnselectedFrame.BackgroundColor3 = ChatSettings.ChannelsTabUnselectedColor
UnselectedFrame.BackgroundTransparency = 0.6
UnselectedFrame.Parent = BackgroundFrame
local SelectedFrame = Instance.new("Frame")
SelectedFrame.Selectable = false
SelectedFrame.Name = "SelectedFrame"
SelectedFrame.Size = UDim2.new(1, 0, 1, 0)
SelectedFrame.Position = UDim2.new(0, 0, 0, 0)
SelectedFrame.BorderSizePixel = 0
SelectedFrame.BackgroundColor3 = ChatSettings.ChannelsTabSelectedColor
SelectedFrame.BackgroundTransparency = 1
SelectedFrame.Parent = BackgroundFrame
local SelectedFrameBackgroundImage = Instance.new("ImageLabel")
SelectedFrameBackgroundImage.Selectable = false
SelectedFrameBackgroundImage.Name = "BackgroundImage"
SelectedFrameBackgroundImage.BackgroundTransparency = 1
SelectedFrameBackgroundImage.BorderSizePixel = 0
SelectedFrameBackgroundImage.Size = UDim2.new(1, 0, 1, 0)
SelectedFrameBackgroundImage.Position = UDim2.new(0, 0, 0, 0)
SelectedFrameBackgroundImage.ScaleType = Enum.ScaleType.Slice
SelectedFrameBackgroundImage.Parent = SelectedFrame
SelectedFrameBackgroundImage.BackgroundTransparency = 0.6 - 1
local rate = 1.2 * 1
SelectedFrameBackgroundImage.BackgroundColor3 = Color3.fromRGB(78 * rate, 84 * rate, 96 * rate)
local borderXOffset = 2
local blueBarYSize = 4
local BlueBarLeft = Instance.new("ImageLabel")
BlueBarLeft.Selectable = false
BlueBarLeft.Size = UDim2.new(0.5, -borderXOffset, 0, blueBarYSize)
BlueBarLeft.BackgroundTransparency = 1
BlueBarLeft.ScaleType = Enum.ScaleType.Slice
BlueBarLeft.SliceCenter = Rect.new(3,3,32,21)
BlueBarLeft.Parent = SelectedFrame
local BlueBarRight = BlueBarLeft:Clone()
BlueBarRight.Parent = SelectedFrame
BlueBarLeft.Position = UDim2.new(0, borderXOffset, 1, -blueBarYSize)
BlueBarRight.Position = UDim2.new(0.5, 0, 1, -blueBarYSize)
BlueBarLeft.Image = "rbxasset://textures/ui/Settings/Slider/SelectedBarLeft.png"
BlueBarRight.Image = "rbxasset://textures/ui/Settings/Slider/SelectedBarRight.png"
BlueBarLeft.Name = "BlueBarLeft"
BlueBarRight.Name = "BlueBarRight"
local NameTag = Instance.new("TextButton")
NameTag.Selectable = ChatSettings.GamepadNavigationEnabled
NameTag.Size = UDim2.new(1, 0, 1, 0)
NameTag.Position = UDim2.new(0, 0, 0, 0)
NameTag.BackgroundTransparency = 1
NameTag.Font = ChatSettings.DefaultFont
NameTag.TextSize = ChatSettings.ChatChannelsTabTextSize
NameTag.TextColor3 = Color3.new(1, 1, 1)
NameTag.TextStrokeTransparency = 0.75
NameTag.Parent = BackgroundFrame
local NameTagNonSelect = NameTag:Clone()
local NameTagSelect = NameTag:Clone()
NameTagNonSelect.Parent = UnselectedFrame
NameTagSelect.Parent = SelectedFrame
NameTagNonSelect.Font = Enum.Font.SourceSans
NameTagNonSelect.Active = false
NameTagSelect.Active = false
local NewMessageIconFrame = Instance.new("Frame")
NewMessageIconFrame.Selectable = false
NewMessageIconFrame.Size = UDim2.new(0, 18, 0, 18)
NewMessageIconFrame.Position = UDim2.new(0.8, -9, 0.5, -9)
NewMessageIconFrame.BackgroundTransparency = 1
NewMessageIconFrame.Parent = BackgroundFrame
local NewMessageIcon = Instance.new("ImageLabel")
NewMessageIcon.Selectable = false
NewMessageIcon.Size = UDim2.new(1, 0, 1, 0)
NewMessageIcon.BackgroundTransparency = 1
NewMessageIcon.Image = "rbxasset://textures/ui/Chat/MessageCounter.png"
NewMessageIcon.Visible = false
NewMessageIcon.Parent = NewMessageIconFrame
local NewMessageIconText = Instance.new("TextLabel")
NewMessageIconText.Selectable = false
NewMessageIconText.BackgroundTransparency = 1
NewMessageIconText.Size = UDim2.new(0, 13, 0, 9)
NewMessageIconText.Position = UDim2.new(0.5, -7, 0.5, -7)
NewMessageIconText.Font = ChatSettings.DefaultFont
NewMessageIconText.TextSize = 14
NewMessageIconText.TextColor3 = Color3.new(1, 1, 1)
NewMessageIconText.Text = ""
NewMessageIconText.Parent = NewMessageIcon
return BaseFrame, NameTag, NameTagNonSelect, NameTagSelect, NewMessageIcon, UnselectedFrame, SelectedFrame
end
function methods:Destroy()
self.GuiObject:Destroy()
end
function methods:UpdateMessagePostedInChannel(ignoreActive)
if (self.Active and (ignoreActive ~= true)) then return end
local count = self.UnreadMessageCount + 1
self.UnreadMessageCount = count
local label = self.NewMessageIcon
label.Visible = true
label.TextLabel.Text = (count < 100) and tostring(count) or "!"
local tweenTime = 0.15
local tweenPosOffset = UDim2.new(0, 0, -0.1, 0)
local curPos = label.Position
local outPos = curPos + tweenPosOffset
local easingDirection = Enum.EasingDirection.Out
local easingStyle = Enum.EasingStyle.Quad
label.Position = UDim2.new(0, 0, -0.15, 0)
label:TweenPosition(UDim2.new(0, 0, 0, 0), easingDirection, easingStyle, tweenTime, true)
end
function methods:SetActive(active)
self.Active = active
self.UnselectedFrame.Visible = not active
self.SelectedFrame.Visible = active
if (active) then
self.UnreadMessageCount = 0
self.NewMessageIcon.Visible = false
self.NameTag.Font = Enum.Font.SourceSansBold
else
self.NameTag.Font = Enum.Font.SourceSans
end
end
function methods:SetTextSize(textSize)
self.NameTag.TextSize = textSize
end
function methods:FadeOutBackground(duration)
self.AnimParams.Background_TargetTransparency = 1
self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
end
function methods:FadeInBackground(duration)
self.AnimParams.Background_TargetTransparency = 0.6
self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
end
function methods:FadeOutText(duration)
self.AnimParams.Text_TargetTransparency = 1
self.AnimParams.Text_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
self.AnimParams.TextStroke_TargetTransparency = 1
self.AnimParams.TextStroke_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
end
function methods:FadeInText(duration)
self.AnimParams.Text_TargetTransparency = 0
self.AnimParams.Text_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
self.AnimParams.TextStroke_TargetTransparency = 0.75
self.AnimParams.TextStroke_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
end
function methods:AnimGuiObjects()
self.UnselectedFrame.BackgroundTransparency = self.AnimParams.Background_CurrentTransparency
self.SelectedFrame.BackgroundImage.BackgroundTransparency = self.AnimParams.Background_CurrentTransparency
self.SelectedFrame.BlueBarLeft.ImageTransparency = self.AnimParams.Background_CurrentTransparency
self.SelectedFrame.BlueBarRight.ImageTransparency = self.AnimParams.Background_CurrentTransparency
self.NameTagNonSelect.TextTransparency = self.AnimParams.Background_CurrentTransparency
self.NameTagNonSelect.TextStrokeTransparency = self.AnimParams.Background_CurrentTransparency
self.NameTag.TextTransparency = self.AnimParams.Text_CurrentTransparency
self.NewMessageIcon.ImageTransparency = self.AnimParams.Text_CurrentTransparency
self.WhiteTextNewMessageNotification.TextTransparency = self.AnimParams.Text_CurrentTransparency
self.NameTagSelect.TextTransparency = self.AnimParams.Text_CurrentTransparency
self.NameTag.TextStrokeTransparency = self.AnimParams.TextStroke_CurrentTransparency
self.WhiteTextNewMessageNotification.TextStrokeTransparency = self.AnimParams.TextStroke_CurrentTransparency
self.NameTagSelect.TextStrokeTransparency = self.AnimParams.TextStroke_CurrentTransparency
end
function methods:InitializeAnimParams()
self.AnimParams.Text_TargetTransparency = 0
self.AnimParams.Text_CurrentTransparency = 0
self.AnimParams.Text_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(0)
self.AnimParams.TextStroke_TargetTransparency = 0.75
self.AnimParams.TextStroke_CurrentTransparency = 0.75
self.AnimParams.TextStroke_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(0)
self.AnimParams.Background_TargetTransparency = 0.6
self.AnimParams.Background_CurrentTransparency = 0.6
self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(0)
end
function methods:Update(dtScale)
self.AnimParams.Background_CurrentTransparency = CurveUtil:Expt(
self.AnimParams.Background_CurrentTransparency,
self.AnimParams.Background_TargetTransparency,
self.AnimParams.Background_NormalizedExptValue,
dtScale
)
self.AnimParams.Text_CurrentTransparency = CurveUtil:Expt(
self.AnimParams.Text_CurrentTransparency,
self.AnimParams.Text_TargetTransparency,
self.AnimParams.Text_NormalizedExptValue,
dtScale
)
self.AnimParams.TextStroke_CurrentTransparency = CurveUtil:Expt(
self.AnimParams.TextStroke_CurrentTransparency,
self.AnimParams.TextStroke_TargetTransparency,
self.AnimParams.TextStroke_NormalizedExptValue,
dtScale
)
self:AnimGuiObjects()
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new(channelName)
local obj = setmetatable({}, methods)
local BaseFrame, NameTag, NameTagNonSelect, NameTagSelect, NewMessageIcon, UnselectedFrame, SelectedFrame = CreateGuiObjects()
obj.GuiObject = BaseFrame
obj.NameTag = NameTag
obj.NameTagNonSelect = NameTagNonSelect
obj.NameTagSelect = NameTagSelect
obj.NewMessageIcon = NewMessageIcon
obj.UnselectedFrame = UnselectedFrame
obj.SelectedFrame = SelectedFrame
obj.BlueBarLeft = SelectedFrame.BlueBarLeft
obj.BlueBarRight = SelectedFrame.BlueBarRight
obj.BackgroundImage = SelectedFrame.BackgroundImage
obj.WhiteTextNewMessageNotification = obj.NewMessageIcon.TextLabel
obj.ChannelName = channelName
obj.UnreadMessageCount = 0
obj.Active = false
obj.GuiObject.Name = "Frame_" .. obj.ChannelName
if (string.len(channelName) > ChatSettings.MaxChannelNameLength) then
channelName = string.sub(channelName, 1, ChatSettings.MaxChannelNameLength - 3) .. "..."
end
--obj.NameTag.Text = channelName
obj.NameTag.Text = ""
obj.NameTagNonSelect.Text = channelName
obj.NameTagSelect.Text = channelName
obj.AnimParams = {}
obj:InitializeAnimParams()
obj:AnimGuiObjects()
obj:SetActive(false)
return obj
end
return module
@@ -0,0 +1,609 @@
-- // FileName: ChatBar.lua
-- // Written by: Xsitsu
-- // Description: Manages text typing and typing state.
local module = {}
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local TextService = game:GetService("TextService")
local LocalPlayer = Players.LocalPlayer
while not LocalPlayer do
Players.PlayerAdded:wait()
LocalPlayer = Players.LocalPlayer
end
--////////////////////////////// Include
--//////////////////////////////////////
local Chat = game:GetService("Chat")
local clientChatModules = Chat:WaitForChild("ClientChatModules")
local modulesFolder = script.Parent
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local CurveUtil = require(modulesFolder:WaitForChild("CurveUtil"))
local commandModules = clientChatModules:WaitForChild("CommandModules")
local WhisperModule = require(commandModules:WaitForChild("Whisper"))
local MessageSender = require(modulesFolder:WaitForChild("MessageSender"))
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
if ChatLocalization == nil then ChatLocalization = {} function ChatLocalization:Get(key,default) return default end end
--////////////////////////////// Methods
--//////////////////////////////////////
local methods = {}
methods.__index = methods
function methods:CreateGuiObjects(targetParent)
self.ChatBarParentFrame = targetParent
local backgroundImagePixelOffset = 7
local textBoxPixelOffset = 5
local BaseFrame = Instance.new("Frame")
BaseFrame.Selectable = false
BaseFrame.Size = UDim2.new(1, 0, 1, 0)
BaseFrame.BackgroundTransparency = 0.6
BaseFrame.BorderSizePixel = 0
BaseFrame.BackgroundColor3 = ChatSettings.ChatBarBackGroundColor
BaseFrame.Parent = targetParent
local BoxFrame = Instance.new("Frame")
BoxFrame.Selectable = false
BoxFrame.Name = "BoxFrame"
BoxFrame.BackgroundTransparency = 0.6
BoxFrame.BorderSizePixel = 0
BoxFrame.BackgroundColor3 = ChatSettings.ChatBarBoxColor
BoxFrame.Size = UDim2.new(1, -backgroundImagePixelOffset * 2, 1, -backgroundImagePixelOffset * 2)
BoxFrame.Position = UDim2.new(0, backgroundImagePixelOffset, 0, backgroundImagePixelOffset)
BoxFrame.Parent = BaseFrame
local TextBoxHolderFrame = Instance.new("Frame")
TextBoxHolderFrame.BackgroundTransparency = 1
TextBoxHolderFrame.Size = UDim2.new(1, -textBoxPixelOffset * 2, 1, -textBoxPixelOffset * 2)
TextBoxHolderFrame.Position = UDim2.new(0, textBoxPixelOffset, 0, textBoxPixelOffset)
TextBoxHolderFrame.Parent = BoxFrame
local TextBox = Instance.new("TextBox")
TextBox.Selectable = ChatSettings.GamepadNavigationEnabled
TextBox.Name = "ChatBar"
TextBox.BackgroundTransparency = 1
TextBox.Size = UDim2.new(1, 0, 1, 0)
TextBox.Position = UDim2.new(0, 0, 0, 0)
TextBox.TextSize = ChatSettings.ChatBarTextSize
TextBox.Font = ChatSettings.ChatBarFont
TextBox.TextColor3 = ChatSettings.ChatBarTextColor
TextBox.TextTransparency = 0.4
TextBox.TextStrokeTransparency = 1
TextBox.ClearTextOnFocus = false
TextBox.TextXAlignment = Enum.TextXAlignment.Left
TextBox.TextYAlignment = Enum.TextYAlignment.Top
TextBox.TextWrapped = true
TextBox.Text = ""
TextBox.Parent = TextBoxHolderFrame
local MessageModeTextButton = Instance.new("TextButton")
MessageModeTextButton.Selectable = false
MessageModeTextButton.Name = "MessageMode"
MessageModeTextButton.BackgroundTransparency = 1
MessageModeTextButton.Position = UDim2.new(0, 0, 0, 0)
MessageModeTextButton.TextSize = ChatSettings.ChatBarTextSize
MessageModeTextButton.Font = ChatSettings.ChatBarFont
MessageModeTextButton.TextXAlignment = Enum.TextXAlignment.Left
MessageModeTextButton.TextWrapped = true
MessageModeTextButton.Text = ""
MessageModeTextButton.Size = UDim2.new(0, 0, 0, 0)
MessageModeTextButton.TextYAlignment = Enum.TextYAlignment.Center
MessageModeTextButton.TextColor3 = self:GetDefaultChannelNameColor()
MessageModeTextButton.Visible = true
MessageModeTextButton.Parent = TextBoxHolderFrame
local TextLabel = Instance.new("TextLabel")
TextLabel.Selectable = false
TextLabel.TextWrapped = true
TextLabel.BackgroundTransparency = 1
TextLabel.Size = TextBox.Size
TextLabel.Position = TextBox.Position
TextLabel.TextSize = TextBox.TextSize
TextLabel.Font = TextBox.Font
TextLabel.TextColor3 = TextBox.TextColor3
TextLabel.TextTransparency = TextBox.TextTransparency
TextLabel.TextStrokeTransparency = TextBox.TextStrokeTransparency
TextLabel.TextXAlignment = TextBox.TextXAlignment
TextLabel.TextYAlignment = TextBox.TextYAlignment
TextLabel.Text = "..."
TextLabel.Parent = TextBoxHolderFrame
self.GuiObject = BaseFrame
self.TextBox = TextBox
self.TextLabel = TextLabel
self.GuiObjects.BaseFrame = BaseFrame
self.GuiObjects.TextBoxFrame = BoxFrame
self.GuiObjects.TextBox = TextBox
self.GuiObjects.TextLabel = TextLabel
self.GuiObjects.MessageModeTextButton = MessageModeTextButton
self:AnimGuiObjects()
self:SetUpTextBoxEvents(TextBox, TextLabel, MessageModeTextButton)
if self.UserHasChatOff then
self:DoLockChatBar()
end
self.eGuiObjectsChanged:Fire()
end
-- Used to lock the chat bar when the user has chat turned off.
function methods:DoLockChatBar()
if self.TextLabel then
if LocalPlayer.UserId > 0 then
self.TextLabel.Text = ChatLocalization:Get(
"GameChat_ChatMessageValidator_SettingsError",
"To chat in game, turn on chat in your Privacy Settings."
)
else
self.TextLabel.Text = ChatLocalization:Get(
"GameChat_SwallowGuestChat_Message",
"Sign up to chat in game."
)
end
self:CalculateSize()
end
if self.TextBox then
self.TextBox.Active = false
self.TextBox.Focused:connect(function()
self.TextBox:ReleaseFocus()
end)
end
end
function methods:SetUpTextBoxEvents(TextBox, TextLabel, MessageModeTextButton)
-- Clean up events from a previous setup.
for name, conn in pairs(self.TextBoxConnections) do
conn:disconnect()
self.TextBoxConnections[name] = nil
end
--// Code for getting back into general channel from other target channel when pressing backspace.
self.TextBoxConnections.UserInputBegan = UserInputService.InputBegan:connect(function(inputObj, gpe)
if (inputObj.KeyCode == Enum.KeyCode.Backspace) then
if (self:IsFocused() and TextBox.Text == "") then
self:SetChannelTarget(ChatSettings.GeneralChannelName)
end
end
end)
self.TextBoxConnections.TextBoxChanged = TextBox.Changed:connect(function(prop)
if prop == "AbsoluteSize" then
self:CalculateSize()
return
end
if prop ~= "Text" then
return
end
self:CalculateSize()
if utf8.len(utf8.nfcnormalize(TextBox.Text)) > ChatSettings.MaximumMessageLength then
TextBox.Text = self.PreviousText
else
self.PreviousText = TextBox.Text
end
if not self.InCustomState then
local customState = self.CommandProcessor:ProcessInProgressChatMessage(TextBox.Text, self.ChatWindow, self)
if customState then
self.InCustomState = true
self.CustomState = customState
end
else
self.CustomState:TextUpdated()
end
end)
local function UpdateOnFocusStatusChanged(isFocused)
if isFocused or TextBox.Text ~= "" then
TextLabel.Visible = false
else
TextLabel.Visible = true
end
end
self.TextBoxConnections.MessageModeClick = MessageModeTextButton.MouseButton1Click:connect(function()
if MessageModeTextButton.Text ~= "" then
self:SetChannelTarget(ChatSettings.GeneralChannelName)
end
end)
self.TextBoxConnections.TextBoxFocused = TextBox.Focused:connect(function()
if not self.UserHasChatOff then
self:CalculateSize()
UpdateOnFocusStatusChanged(true)
end
end)
self.TextBoxConnections.TextBoxFocusLost = TextBox.FocusLost:connect(function(enterPressed, inputObject)
self:CalculateSize()
if (inputObject and inputObject.KeyCode == Enum.KeyCode.Escape) then
TextBox.Text = ""
end
UpdateOnFocusStatusChanged(false)
end)
end
function methods:GetTextBox()
return self.TextBox
end
function methods:GetMessageModeTextButton()
return self.GuiObjects.MessageModeTextButton
end
-- Deprecated in favour of GetMessageModeTextButton
-- Retained for compatibility reasons.
function methods:GetMessageModeTextLabel()
return self:GetMessageModeTextButton()
end
function methods:IsFocused()
if self.UserHasChatOff then
return false
end
return self:GetTextBox():IsFocused()
end
function methods:GetVisible()
return self.GuiObject.Visible
end
function methods:CaptureFocus()
if not self.UserHasChatOff then
self:GetTextBox():CaptureFocus()
end
end
function methods:ReleaseFocus(didRelease)
self:GetTextBox():ReleaseFocus(didRelease)
end
function methods:ResetText()
self:GetTextBox().Text = ""
end
function methods:SetText(text)
self:GetTextBox().Text = text
end
function methods:GetEnabled()
return self.GuiObject.Visible
end
function methods:SetEnabled(enabled)
if self.UserHasChatOff then
-- The chat bar can not be removed if a user has chat turned off so that
-- the chat bar can display a message explaining that chat is turned off.
self.GuiObject.Visible = true
else
self.GuiObject.Visible = enabled
end
end
function methods:SetTextLabelText(text)
if not self.UserHasChatOff then
self.TextLabel.Text = text
end
end
function methods:SetTextBoxText(text)
self.TextBox.Text = text
end
function methods:GetTextBoxText()
return self.TextBox.Text
end
function methods:ResetSize()
self.TargetYSize = 0
self:TweenToTargetYSize()
end
local function measureSize(textObj)
return TextService:GetTextSize(
textObj.Text,
textObj.TextSize,
textObj.Font,
Vector2.new(textObj.AbsoluteSize.X, 10000)
)
end
function methods:CalculateSize()
if self.CalculatingSizeLock then
return
end
self.CalculatingSizeLock = true
local textSize = nil
local bounds = nil
if self:IsFocused() or self.TextBox.Text ~= "" then
textSize = self.TextBox.TextSize
bounds = measureSize(self.TextBox).Y
else
textSize = self.TextLabel.TextSize
bounds = measureSize(self.TextLabel).Y
end
local newTargetYSize = bounds - textSize
if (self.TargetYSize ~= newTargetYSize) then
self.TargetYSize = newTargetYSize
self:TweenToTargetYSize()
end
self.CalculatingSizeLock = false
end
function methods:TweenToTargetYSize()
local endSize = UDim2.new(1, 0, 1, self.TargetYSize)
local curSize = self.GuiObject.Size
local curAbsoluteSizeY = self.GuiObject.AbsoluteSize.Y
self.GuiObject.Size = endSize
local endAbsoluteSizeY = self.GuiObject.AbsoluteSize.Y
self.GuiObject.Size = curSize
local pixelDistance = math.abs(endAbsoluteSizeY - curAbsoluteSizeY)
local tweeningTime = math.min(1, (pixelDistance * (1 / self.TweenPixelsPerSecond))) -- pixelDistance * (seconds per pixels)
local success = pcall(function() self.GuiObject:TweenSize(endSize, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, tweeningTime, true) end)
if (not success) then
self.GuiObject.Size = endSize
end
end
function methods:SetTextSize(textSize)
if not self:IsInCustomState() then
if self.TextBox then
self.TextBox.TextSize = textSize
end
if self.TextLabel then
self.TextLabel.TextSize = textSize
end
end
end
function methods:GetDefaultChannelNameColor()
if ChatSettings.DefaultChannelNameColor then
return ChatSettings.DefaultChannelNameColor
end
return Color3.fromRGB(35, 76, 142)
end
function methods:SetChannelTarget(targetChannel)
local messageModeTextButton = self.GuiObjects.MessageModeTextButton
local textBox = self.TextBox
local textLabel = self.TextLabel
self.TargetChannel = targetChannel
if not self:IsInCustomState() then
if targetChannel ~= ChatSettings.GeneralChannelName then
messageModeTextButton.Size = UDim2.new(0, 1000, 1, 0)
local localizedTargetChannel = targetChannel
if ChatLocalization.tryLocalize then
localizedTargetChannel = ChatLocalization:tryLocalize(targetChannel)
end
messageModeTextButton.Text = string.format("[%s] ", localizedTargetChannel)
local channelNameColor = self:GetChannelNameColor(targetChannel)
if channelNameColor then
messageModeTextButton.TextColor3 = channelNameColor
else
messageModeTextButton.TextColor3 = self:GetDefaultChannelNameColor()
end
local xSize = messageModeTextButton.TextBounds.X
messageModeTextButton.Size = UDim2.new(0, xSize, 1, 0)
textBox.Size = UDim2.new(1, -xSize, 1, 0)
textBox.Position = UDim2.new(0, xSize, 0, 0)
textLabel.Size = UDim2.new(1, -xSize, 1, 0)
textLabel.Position = UDim2.new(0, xSize, 0, 0)
else
messageModeTextButton.Text = ""
messageModeTextButton.Size = UDim2.new(0, 0, 0, 0)
textBox.Size = UDim2.new(1, 0, 1, 0)
textBox.Position = UDim2.new(0, 0, 0, 0)
textLabel.Size = UDim2.new(1, 0, 1, 0)
textLabel.Position = UDim2.new(0, 0, 0, 0)
end
end
end
function methods:IsInCustomState()
return self.InCustomState
end
function methods:ResetCustomState()
if self.InCustomState then
self.CustomState:Destroy()
self.CustomState = nil
self.InCustomState = false
self.ChatBarParentFrame:ClearAllChildren()
self:CreateGuiObjects(self.ChatBarParentFrame)
self:SetTextLabelText(
ChatLocalization:Get(
"GameChat_ChatMain_ChatBarText",
'To chat click here or press "/" key'
)
)
end
end
function methods:EnterWhisperState(player)
self:ResetCustomState()
if WhisperModule.CustomStateCreator then
self.CustomState = WhisperModule.CustomStateCreator(
player,
self.ChatWindow,
self,
ChatSettings
)
self.InCustomState = true
else
local playerName
if ChatSettings.PlayerDisplayNamesEnabled then
playerName = player.DisplayName
else
playerName = player.Name
end
self:SetText("/w " .. playerName)
end
self:CaptureFocus()
end
function methods:GetCustomMessage()
if self.InCustomState then
return self.CustomState:GetMessage()
end
return nil
end
function methods:CustomStateProcessCompletedMessage(message)
if self.InCustomState then
return self.CustomState:ProcessCompletedMessage()
end
return false
end
function methods:FadeOutBackground(duration)
self.AnimParams.Background_TargetTransparency = 1
self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
self:FadeOutText(duration)
end
function methods:FadeInBackground(duration)
self.AnimParams.Background_TargetTransparency = 0.6
self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
self:FadeInText(duration)
end
function methods:FadeOutText(duration)
self.AnimParams.Text_TargetTransparency = 1
self.AnimParams.Text_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
end
function methods:FadeInText(duration)
self.AnimParams.Text_TargetTransparency = 0.4
self.AnimParams.Text_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
end
function methods:AnimGuiObjects()
self.GuiObject.BackgroundTransparency = self.AnimParams.Background_CurrentTransparency
self.GuiObjects.TextBoxFrame.BackgroundTransparency = self.AnimParams.Background_CurrentTransparency
self.GuiObjects.TextLabel.TextTransparency = self.AnimParams.Text_CurrentTransparency
self.GuiObjects.TextBox.TextTransparency = self.AnimParams.Text_CurrentTransparency
self.GuiObjects.MessageModeTextButton.TextTransparency = self.AnimParams.Text_CurrentTransparency
end
function methods:InitializeAnimParams()
self.AnimParams.Text_TargetTransparency = 0.4
self.AnimParams.Text_CurrentTransparency = 0.4
self.AnimParams.Text_NormalizedExptValue = 1
self.AnimParams.Background_TargetTransparency = 0.6
self.AnimParams.Background_CurrentTransparency = 0.6
self.AnimParams.Background_NormalizedExptValue = 1
end
function methods:Update(dtScale)
self.AnimParams.Text_CurrentTransparency = CurveUtil:Expt(
self.AnimParams.Text_CurrentTransparency,
self.AnimParams.Text_TargetTransparency,
self.AnimParams.Text_NormalizedExptValue,
dtScale
)
self.AnimParams.Background_CurrentTransparency = CurveUtil:Expt(
self.AnimParams.Background_CurrentTransparency,
self.AnimParams.Background_TargetTransparency,
self.AnimParams.Background_NormalizedExptValue,
dtScale
)
self:AnimGuiObjects()
end
function methods:SetChannelNameColor(channelName, channelNameColor)
self.ChannelNameColors[channelName] = channelNameColor
if self.GuiObjects.MessageModeTextButton.Text == channelName then
self.GuiObjects.MessageModeTextButton.TextColor3 = channelNameColor
end
end
function methods:GetChannelNameColor(channelName)
return self.ChannelNameColors[channelName]
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new(CommandProcessor, ChatWindow)
local obj = setmetatable({}, methods)
obj.GuiObject = nil
obj.ChatBarParentFrame = nil
obj.TextBox = nil
obj.TextLabel = nil
obj.GuiObjects = {}
obj.eGuiObjectsChanged = Instance.new("BindableEvent")
obj.GuiObjectsChanged = obj.eGuiObjectsChanged.Event
obj.TextBoxConnections = {}
obj.PreviousText = ""
obj.InCustomState = false
obj.CustomState = nil
obj.TargetChannel = nil
obj.CommandProcessor = CommandProcessor
obj.ChatWindow = ChatWindow
obj.TweenPixelsPerSecond = 500
obj.TargetYSize = 0
obj.AnimParams = {}
obj.CalculatingSizeLock = false
obj.ChannelNameColors = {}
obj.UserHasChatOff = false
obj:InitializeAnimParams()
ChatSettings.SettingsChanged:connect(function(setting, value)
if (setting == "ChatBarTextSize") then
obj:SetTextSize(value)
end
end)
coroutine.wrap(function()
local success, canLocalUserChat = pcall(function()
return Chat:CanUserChatAsync(LocalPlayer.UserId)
end)
local canChat = success and (RunService:IsStudio() or canLocalUserChat)
if canChat == false then
obj.UserHasChatOff = true
obj:DoLockChatBar()
end
end)()
return obj
end
return module
@@ -0,0 +1,166 @@
-- // FileName: ChatChannel.lua
-- // Written by: Xsitsu
-- // Description: ChatChannel class for handling messages being added and removed from the chat channel.
local module = {}
--////////////////////////////// Include
--//////////////////////////////////////
local Chat = game:GetService("Chat")
local clientChatModules = Chat:WaitForChild("ClientChatModules")
local modulesFolder = script.Parent
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
--////////////////////////////// Methods
--//////////////////////////////////////
local methods = {}
methods.__index = methods
function methods:Destroy()
self.Destroyed = true
end
function methods:SetActive(active)
if active == self.Active then
return
end
if active == false then
self.MessageLogDisplay:Clear()
else
self.MessageLogDisplay:SetCurrentChannelName(self.Name)
for i = 1, #self.MessageLog do
self.MessageLogDisplay:AddMessage(self.MessageLog[i])
end
end
self.Active = active
end
function methods:UpdateMessageFiltered(messageData)
local searchIndex = 1
local searchTable = self.MessageLog
local messageObj = nil
while (#searchTable >= searchIndex) do
local obj = searchTable[searchIndex]
if (obj.ID == messageData.ID) then
messageObj = obj
break
end
searchIndex = searchIndex + 1
end
if messageObj then
messageObj.Message = messageData.Message
messageObj.IsFiltered = true
if self.Active then
self.MessageLogDisplay:UpdateMessageFiltered(messageObj)
end
else
-- We have not seen this filtered message before, but we should still add it to our log.
self:AddMessageToChannelByTimeStamp(messageData)
end
end
function methods:AddMessageToChannel(messageData)
table.insert(self.MessageLog, messageData)
if self.Active then
self.MessageLogDisplay:AddMessage(messageData)
end
if #self.MessageLog > ChatSettings.MessageHistoryLengthPerChannel then
self:RemoveLastMessageFromChannel()
end
end
function methods:InternalAddMessageAtTimeStamp(messageData)
for i = 1, #self.MessageLog do
if messageData.Time < self.MessageLog[i].Time then
table.insert(self.MessageLog, i, messageData)
return
end
end
table.insert(self.MessageLog, messageData)
end
function methods:AddMessagesToChannelByTimeStamp(messageLog, startIndex)
for i = startIndex, #messageLog do
self:InternalAddMessageAtTimeStamp(messageLog[i])
end
while #self.MessageLog > ChatSettings.MessageHistoryLengthPerChannel do
table.remove(self.MessageLog, 1)
end
if self.Active then
self.MessageLogDisplay:Clear()
for i = 1, #self.MessageLog do
self.MessageLogDisplay:AddMessage(self.MessageLog[i])
end
end
end
function methods:AddMessageToChannelByTimeStamp(messageData)
if #self.MessageLog >= 1 then
-- These are the fast cases to evalutate.
if self.MessageLog[1].Time > messageData.Time then
return
elseif messageData.Time >= self.MessageLog[#self.MessageLog].Time then
self:AddMessageToChannel(messageData)
return
end
for i = 1, #self.MessageLog do
if messageData.Time < self.MessageLog[i].Time then
table.insert(self.MessageLog, i, messageData)
if #self.MessageLog > ChatSettings.MessageHistoryLengthPerChannel then
self:RemoveLastMessageFromChannel()
end
if self.Active then
self.MessageLogDisplay:AddMessageAtIndex(messageData, i)
end
return
end
end
else
self:AddMessageToChannel(messageData)
end
end
function methods:RemoveLastMessageFromChannel()
table.remove(self.MessageLog, 1)
if self.Active then
self.MessageLogDisplay:RemoveLastMessage()
end
end
function methods:ClearMessageLog()
self.MessageLog = {}
if self.Active then
self.MessageLogDisplay:Clear()
end
end
function methods:RegisterChannelTab(tab)
self.ChannelTab = tab
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new(channelName, messageLogDisplay)
local obj = setmetatable({}, methods)
obj.Destroyed = false
obj.Active = false
obj.MessageLog = {}
obj.MessageLogDisplay = messageLogDisplay
obj.ChannelTab = nil
obj.Name = channelName
return obj
end
return module
@@ -0,0 +1,26 @@
--- This script is used when a Player is in the game before we have a chance
--- to put the ChatScripts in StarterPlayerScripts. This script looks for
--- the ChatScripts in a users PlayerScripts and clones them from
--- StarterPlayerScripts if they do not already exist.
local PlayersService = game:GetService("Players")
local LocalPlayer = PlayersService.LocalPlayer
local PlayerScripts = LocalPlayer:WaitForChild("PlayerScripts")
local StarterPlayerScripts = game:GetService("StarterPlayer"):WaitForChild("StarterPlayerScripts")
function VerifyScriptExistance()
local scriptToVerifyValue = script:WaitForChild("ScriptToVerify")
local scriptToVerifyName = scriptToVerifyValue.Value
if not PlayerScripts:FindFirstChild(scriptToVerifyName) then
local scriptToClone = StarterPlayerScripts:FindFirstChild(scriptToVerifyName)
if scriptToClone then
local lastArchivable = scriptToClone.Archivable
scriptToClone.Archivable = true
scriptToClone:Clone().Parent = PlayerScripts
scriptToClone.Archivable = lastArchivable
end
end
end
VerifyScriptExistance()
script:Destroy()
@@ -0,0 +1,157 @@
-- // FileName: ChatScript.lua
-- // Written by: Xsitsu
-- // Description: Hooks main chat module up to Topbar in corescripts.
local FFlagUserHandleChatHotKeyWithContextActionService = false do
local ok, value = pcall(function()
return UserSettings():IsUserFeatureEnabled("UserHandleChatHotKeyWithContextActionService")
end)
if ok then
FFlagUserHandleChatHotKeyWithContextActionService = value
end
end
local StarterGui = game:GetService("StarterGui")
local GuiService = game:GetService("GuiService")
local ChatService = game:GetService("Chat")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MAX_COREGUI_CONNECTION_ATTEMPTS = 10
local ClientChatModules = ChatService:WaitForChild("ClientChatModules")
local ChatSettings = require(ClientChatModules:WaitForChild("ChatSettings"))
local function DoEverything()
local Chat = require(script:WaitForChild("ChatMain"))
local containerTable = {}
containerTable.ChatWindow = {}
containerTable.SetCore = {}
containerTable.GetCore = {}
containerTable.ChatWindow.ChatTypes = {}
containerTable.ChatWindow.ChatTypes.BubbleChatEnabled = ChatSettings.BubbleChatEnabled
containerTable.ChatWindow.ChatTypes.ClassicChatEnabled = ChatSettings.ClassicChatEnabled
--// Connection functions
local function ConnectEvent(name)
local event = Instance.new("BindableEvent")
event.Name = name
containerTable.ChatWindow[name] = event
event.Event:connect(function(...) Chat[name](Chat, ...) end)
end
local function ConnectFunction(name)
local func = Instance.new("BindableFunction")
func.Name = name
containerTable.ChatWindow[name] = func
func.OnInvoke = function(...) return Chat[name](Chat, ...) end
end
local function ReverseConnectEvent(name)
local event = Instance.new("BindableEvent")
event.Name = name
containerTable.ChatWindow[name] = event
Chat[name]:connect(function(...) event:Fire(...) end)
end
local function ConnectSignal(name)
local event = Instance.new("BindableEvent")
event.Name = name
containerTable.ChatWindow[name] = event
event.Event:connect(function(...) Chat[name]:fire(...) end)
end
local function ConnectSetCore(name)
local event = Instance.new("BindableEvent")
event.Name = name
containerTable.SetCore[name] = event
event.Event:connect(function(...) Chat[name.."Event"]:fire(...) end)
end
local function ConnectGetCore(name)
local func = Instance.new("BindableFunction")
func.Name = name
containerTable.GetCore[name] = func
func.OnInvoke = function(...) return Chat["f"..name](...) end
end
--// Do connections
ConnectEvent("ToggleVisibility")
ConnectEvent("SetVisible")
ConnectEvent("FocusChatBar")
ConnectEvent("EnterWhisperState")
ConnectFunction("GetVisibility")
ConnectFunction("GetMessageCount")
ConnectEvent("TopbarEnabledChanged")
ConnectFunction("IsFocused")
ReverseConnectEvent("ChatBarFocusChanged")
ReverseConnectEvent("VisibilityStateChanged")
ReverseConnectEvent("MessagesChanged")
ReverseConnectEvent("MessagePosted")
ConnectSignal("CoreGuiEnabled")
ConnectSetCore("ChatMakeSystemMessage")
ConnectSetCore("ChatWindowPosition")
ConnectSetCore("ChatWindowSize")
ConnectGetCore("ChatWindowPosition")
ConnectGetCore("ChatWindowSize")
ConnectSetCore("ChatBarDisabled")
ConnectGetCore("ChatBarDisabled")
if not FFlagUserHandleChatHotKeyWithContextActionService then
ConnectEvent("SpecialKeyPressed")
end
SetCoreGuiChatConnections(containerTable)
end
function SetCoreGuiChatConnections(containerTable)
local tries = 0
while tries < MAX_COREGUI_CONNECTION_ATTEMPTS do
tries = tries + 1
local success, ret = pcall(function() StarterGui:SetCore("CoreGuiChatConnections", containerTable) end)
if success then
break
end
if not success and tries == MAX_COREGUI_CONNECTION_ATTEMPTS then
error("Error calling SetCore CoreGuiChatConnections: " .. ret)
end
wait()
end
end
function checkBothChatTypesDisabled()
if ChatSettings.BubbleChatEnabled ~= nil then
if ChatSettings.ClassicChatEnabled ~= nil then
return not (ChatSettings.BubbleChatEnabled or ChatSettings.ClassicChatEnabled)
end
end
return false
end
if (not GuiService:IsTenFootInterface()) and (not game:GetService('UserInputService').VREnabled) then
if not checkBothChatTypesDisabled() then
DoEverything()
else
local containerTable = {}
containerTable.ChatWindow = {}
containerTable.ChatWindow.ChatTypes = {}
containerTable.ChatWindow.ChatTypes.BubbleChatEnabled = false
containerTable.ChatWindow.ChatTypes.ClassicChatEnabled = false
SetCoreGuiChatConnections(containerTable)
end
else
-- Make init data request to register as a speaker
local EventFolder = ReplicatedStorage:WaitForChild("DefaultChatSystemChatEvents")
EventFolder.GetInitDataRequest:InvokeServer()
end
@@ -0,0 +1,689 @@
-- // FileName: ChatWindow.lua
-- // Written by: Xsitsu
-- // Description: Main GUI window piece. Manages ChatBar, ChannelsBar, and ChatChannels.
local FFlagFixMouseCapture = false do
local ok, value = pcall(function()
return UserSettings():IsUserFeatureEnabled("UserFixMouseCapture")
end)
if ok then
FFlagFixMouseCapture = value
end
end
local module = {}
local Players = game:GetService("Players")
local Chat = game:GetService("Chat")
local LocalPlayer = Players.LocalPlayer
local PlayerGui = LocalPlayer:WaitForChild("PlayerGui")
local PHONE_SCREEN_WIDTH = 640
local TABLET_SCREEN_WIDTH = 1024
local DEVICE_PHONE = 1
local DEVICE_TABLET = 2
local DEVICE_DESKTOP = 3
--////////////////////////////// Include
--//////////////////////////////////////
local Chat = game:GetService("Chat")
local clientChatModules = Chat:WaitForChild("ClientChatModules")
local modulesFolder = script.Parent
local moduleChatChannel = require(modulesFolder:WaitForChild("ChatChannel"))
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local CurveUtil = require(modulesFolder:WaitForChild("CurveUtil"))
--////////////////////////////// Methods
--//////////////////////////////////////
local methods = {}
methods.__index = methods
function getClassicChatEnabled()
if ChatSettings.ClassicChatEnabled ~= nil then
return ChatSettings.ClassicChatEnabled
end
return Players.ClassicChat
end
function getBubbleChatEnabled()
if ChatSettings.BubbleChatEnabled ~= nil then
return ChatSettings.BubbleChatEnabled
end
return Players.BubbleChat
end
function bubbleChatOnly()
return not getClassicChatEnabled() and getBubbleChatEnabled()
end
-- only merge property defined on target
function mergeProps(source, target)
if not source or not target then return end
for prop, value in pairs(source) do
if target[prop] ~= nil then
target[prop] = value
end
end
end
function methods:CreateGuiObjects(targetParent)
local userDefinedChatWindowStyle
pcall(function()
userDefinedChatWindowStyle= Chat:InvokeChatCallback(Enum.ChatCallbackType.OnCreatingChatWindow, nil)
end)
-- merge the userdefined settings with the ChatSettings
mergeProps(userDefinedChatWindowStyle, ChatSettings)
local BaseFrame = Instance.new("Frame")
BaseFrame.BackgroundTransparency = 1
BaseFrame.Active = ChatSettings.WindowDraggable
BaseFrame.Parent = targetParent
BaseFrame.AutoLocalize = false
local ChatBarParentFrame = Instance.new("Frame")
ChatBarParentFrame.Selectable = false
ChatBarParentFrame.Name = "ChatBarParentFrame"
ChatBarParentFrame.BackgroundTransparency = 1
ChatBarParentFrame.Parent = BaseFrame
local ChannelsBarParentFrame = Instance.new("Frame")
ChannelsBarParentFrame.Selectable = false
ChannelsBarParentFrame.Name = "ChannelsBarParentFrame"
ChannelsBarParentFrame.BackgroundTransparency = 1
ChannelsBarParentFrame.Position = UDim2.new(0, 0, 0, 0)
ChannelsBarParentFrame.Parent = BaseFrame
local ChatChannelParentFrame = Instance.new("Frame")
ChatChannelParentFrame.Selectable = false
ChatChannelParentFrame.Name = "ChatChannelParentFrame"
ChatChannelParentFrame.BackgroundTransparency = 1
ChatChannelParentFrame.BackgroundColor3 = ChatSettings.BackGroundColor
ChatChannelParentFrame.BackgroundTransparency = 0.6
ChatChannelParentFrame.BorderSizePixel = 0
ChatChannelParentFrame.Parent = BaseFrame
local ChatResizerFrame = Instance.new("ImageButton")
ChatResizerFrame.Selectable = false
ChatResizerFrame.Image = ""
ChatResizerFrame.BackgroundTransparency = 0.6
ChatResizerFrame.BorderSizePixel = 0
ChatResizerFrame.Visible = false
ChatResizerFrame.BackgroundColor3 = ChatSettings.BackGroundColor
ChatResizerFrame.Active = true
if bubbleChatOnly() then
ChatResizerFrame.Position = UDim2.new(1, -ChatResizerFrame.AbsoluteSize.X, 0, 0)
else
ChatResizerFrame.Position = UDim2.new(1, -ChatResizerFrame.AbsoluteSize.X, 1, -ChatResizerFrame.AbsoluteSize.Y)
end
ChatResizerFrame.Parent = BaseFrame
local ResizeIcon = Instance.new("ImageLabel")
ResizeIcon.Selectable = false
ResizeIcon.Size = UDim2.new(0.8, 0, 0.8, 0)
ResizeIcon.Position = UDim2.new(0.2, 0, 0.2, 0)
ResizeIcon.BackgroundTransparency = 1
ResizeIcon.Image = "rbxassetid://261880743"
ResizeIcon.Parent = ChatResizerFrame
local function GetScreenGuiParent()
--// Travel up parent list until you find the ScreenGui that the chat window is parented to
local screenGuiParent = BaseFrame
while (screenGuiParent and not screenGuiParent:IsA("ScreenGui")) do
screenGuiParent = screenGuiParent.Parent
end
return screenGuiParent
end
local deviceType = DEVICE_DESKTOP
local screenGuiParent = GetScreenGuiParent()
if (screenGuiParent.AbsoluteSize.X <= PHONE_SCREEN_WIDTH) then
deviceType = DEVICE_PHONE
elseif (screenGuiParent.AbsoluteSize.X <= TABLET_SCREEN_WIDTH) then
deviceType = DEVICE_TABLET
end
local checkSizeLock = false
local function doCheckSizeBounds()
if (checkSizeLock) then return end
checkSizeLock = true
if (not BaseFrame:IsDescendantOf(PlayerGui)) then return end
local screenGuiParent = GetScreenGuiParent()
local minWinSize = ChatSettings.MinimumWindowSize
local maxWinSize = ChatSettings.MaximumWindowSize
local forceMinY = ChannelsBarParentFrame.AbsoluteSize.Y + ChatBarParentFrame.AbsoluteSize.Y
local minSizePixelX = (minWinSize.X.Scale * screenGuiParent.AbsoluteSize.X) + minWinSize.X.Offset
local minSizePixelY = math.max((minWinSize.Y.Scale * screenGuiParent.AbsoluteSize.Y) + minWinSize.Y.Offset, forceMinY)
local maxSizePixelX = (maxWinSize.X.Scale * screenGuiParent.AbsoluteSize.X) + maxWinSize.X.Offset
local maxSizePixelY = (maxWinSize.Y.Scale * screenGuiParent.AbsoluteSize.Y) + maxWinSize.Y.Offset
local absSizeX = BaseFrame.AbsoluteSize.X
local absSizeY = BaseFrame.AbsoluteSize.Y
if (absSizeX < minSizePixelX) then
local offset = UDim2.new(0, minSizePixelX - absSizeX, 0, 0)
BaseFrame.Size = BaseFrame.Size + offset
elseif (absSizeX > maxSizePixelX) then
local offset = UDim2.new(0, maxSizePixelX - absSizeX, 0, 0)
BaseFrame.Size = BaseFrame.Size + offset
end
if (absSizeY < minSizePixelY) then
local offset = UDim2.new(0, 0, 0, minSizePixelY - absSizeY)
BaseFrame.Size = BaseFrame.Size + offset
elseif (absSizeY > maxSizePixelY) then
local offset = UDim2.new(0, 0, 0, maxSizePixelY - absSizeY)
BaseFrame.Size = BaseFrame.Size + offset
end
local xScale = BaseFrame.AbsoluteSize.X / screenGuiParent.AbsoluteSize.X
local yScale = BaseFrame.AbsoluteSize.Y / screenGuiParent.AbsoluteSize.Y
-- cap chat window scale at a value smaller than 0.5 to prevent center of screen overlap
if FFlagFixMouseCapture then
xScale = math.min(xScale, 0.45)
yScale = math.min(xScale, 0.45)
end
BaseFrame.Size = UDim2.new(xScale, 0, yScale, 0)
checkSizeLock = false
end
BaseFrame.Changed:connect(function(prop)
if (prop == "AbsoluteSize") then
doCheckSizeBounds()
end
end)
ChatResizerFrame.DragBegin:connect(function(startUdim)
BaseFrame.Draggable = false
end)
local function UpdatePositionFromDrag(atPos)
if ChatSettings.WindowDraggable == false and ChatSettings.WindowResizable == false then
return
end
local newSize = atPos - BaseFrame.AbsolutePosition + ChatResizerFrame.AbsoluteSize
BaseFrame.Size = UDim2.new(0, newSize.X, 0, newSize.Y)
if bubbleChatOnly() then
ChatResizerFrame.Position = UDim2.new(1, -ChatResizerFrame.AbsoluteSize.X, 0, 0)
else
ChatResizerFrame.Position = UDim2.new(1, -ChatResizerFrame.AbsoluteSize.X, 1, -ChatResizerFrame.AbsoluteSize.Y)
end
end
ChatResizerFrame.DragStopped:connect(function(endX, endY)
BaseFrame.Draggable = ChatSettings.WindowDraggable
--UpdatePositionFromDrag(Vector2.new(endX, endY))
end)
local resizeLock = false
ChatResizerFrame.Changed:connect(function(prop)
if (prop == "AbsolutePosition" and not BaseFrame.Draggable) then
if (resizeLock) then return end
resizeLock = true
UpdatePositionFromDrag(ChatResizerFrame.AbsolutePosition)
resizeLock = false
end
end)
local function CalculateChannelsBarPixelSize(textSize)
if (deviceType == DEVICE_PHONE) then
textSize = textSize or ChatSettings.ChatChannelsTabTextSizePhone
else
textSize = textSize or ChatSettings.ChatChannelsTabTextSize
end
local channelsBarTextYSize = textSize
local chatChannelYSize = math.max(32, channelsBarTextYSize + 8) + 2
return chatChannelYSize
end
local function CalculateChatBarPixelSize(textSize)
if (deviceType == DEVICE_PHONE) then
textSize = textSize or ChatSettings.ChatBarTextSizePhone
else
textSize = textSize or ChatSettings.ChatBarTextSize
end
local chatBarTextSizeY = textSize
local chatBarYSize = chatBarTextSizeY + (7 * 2) + (5 * 2)
return chatBarYSize
end
if bubbleChatOnly() then
ChatBarParentFrame.Position = UDim2.new(0, 0, 0, 0)
ChannelsBarParentFrame.Visible = false
ChannelsBarParentFrame.Active = false
ChatChannelParentFrame.Visible = false
ChatChannelParentFrame.Active = false
local useXScale = 0
local useXOffset = 0
local screenGuiParent = GetScreenGuiParent()
if (deviceType == DEVICE_PHONE) then
useXScale = ChatSettings.DefaultWindowSizePhone.X.Scale
useXOffset = ChatSettings.DefaultWindowSizePhone.X.Offset
elseif (deviceType == DEVICE_TABLET) then
useXScale = ChatSettings.DefaultWindowSizeTablet.X.Scale
useXOffset = ChatSettings.DefaultWindowSizeTablet.X.Offset
else
useXScale = ChatSettings.DefaultWindowSizeDesktop.X.Scale
useXOffset = ChatSettings.DefaultWindowSizeDesktop.X.Offset
end
local chatBarYSize = CalculateChatBarPixelSize()
BaseFrame.Size = UDim2.new(useXScale, useXOffset, 0, chatBarYSize)
BaseFrame.Position = ChatSettings.DefaultWindowPosition
else
local screenGuiParent = GetScreenGuiParent()
if (deviceType == DEVICE_PHONE) then
BaseFrame.Size = ChatSettings.DefaultWindowSizePhone
elseif (deviceType == DEVICE_TABLET) then
BaseFrame.Size = ChatSettings.DefaultWindowSizeTablet
else
BaseFrame.Size = ChatSettings.DefaultWindowSizeDesktop
end
BaseFrame.Position = ChatSettings.DefaultWindowPosition
end
if (deviceType == DEVICE_PHONE) then
ChatSettings.ChatWindowTextSize = ChatSettings.ChatWindowTextSizePhone
ChatSettings.ChatChannelsTabTextSize = ChatSettings.ChatChannelsTabTextSizePhone
ChatSettings.ChatBarTextSize = ChatSettings.ChatBarTextSizePhone
end
local function UpdateDraggable(enabled)
BaseFrame.Active = enabled
BaseFrame.Draggable = enabled
end
local function UpdateResizable(enabled)
ChatResizerFrame.Visible = enabled
ChatResizerFrame.Draggable = enabled
local frameSizeY = ChatBarParentFrame.Size.Y.Offset
if (enabled) then
ChatBarParentFrame.Size = UDim2.new(1, -frameSizeY - 2, 0, frameSizeY)
if not bubbleChatOnly() then
ChatBarParentFrame.Position = UDim2.new(0, 0, 1, -frameSizeY)
end
else
ChatBarParentFrame.Size = UDim2.new(1, 0, 0, frameSizeY)
if not bubbleChatOnly() then
ChatBarParentFrame.Position = UDim2.new(0, 0, 1, -frameSizeY)
end
end
end
local function UpdateChatChannelParentFrameSize()
local channelsBarSize = CalculateChannelsBarPixelSize()
local chatBarSize = CalculateChatBarPixelSize()
if (ChatSettings.ShowChannelsBar) then
ChatChannelParentFrame.Size = UDim2.new(1, 0, 1, -(channelsBarSize + chatBarSize + 2 + 2))
ChatChannelParentFrame.Position = UDim2.new(0, 0, 0, channelsBarSize + 2)
else
ChatChannelParentFrame.Size = UDim2.new(1, 0, 1, -(chatBarSize + 2 + 2))
ChatChannelParentFrame.Position = UDim2.new(0, 0, 0, 2)
end
end
local function UpdateChatChannelsTabTextSize(size)
local channelsBarSize = CalculateChannelsBarPixelSize(size)
ChannelsBarParentFrame.Size = UDim2.new(1, 0, 0, channelsBarSize)
UpdateChatChannelParentFrameSize()
end
local function UpdateChatBarTextSize(size)
local chatBarSize = CalculateChatBarPixelSize(size)
ChatBarParentFrame.Size = UDim2.new(1, 0, 0, chatBarSize)
if not bubbleChatOnly() then
ChatBarParentFrame.Position = UDim2.new(0, 0, 1, -chatBarSize)
end
ChatResizerFrame.Size = UDim2.new(0, chatBarSize, 0, chatBarSize)
ChatResizerFrame.Position = UDim2.new(1, -chatBarSize, 1, -chatBarSize)
UpdateChatChannelParentFrameSize()
UpdateResizable(ChatSettings.WindowResizable)
end
local function UpdateShowChannelsBar(enabled)
ChannelsBarParentFrame.Visible = enabled
UpdateChatChannelParentFrameSize()
end
UpdateChatChannelsTabTextSize(ChatSettings.ChatChannelsTabTextSize)
UpdateChatBarTextSize(ChatSettings.ChatBarTextSize)
UpdateDraggable(ChatSettings.WindowDraggable)
UpdateResizable(ChatSettings.WindowResizable)
UpdateShowChannelsBar(ChatSettings.ShowChannelsBar)
ChatSettings.SettingsChanged:connect(function(setting, value)
if (setting == "WindowDraggable") then
UpdateDraggable(value)
elseif (setting == "WindowResizable") then
UpdateResizable(value)
elseif (setting == "ChatChannelsTabTextSize") then
UpdateChatChannelsTabTextSize(value)
elseif (setting == "ChatBarTextSize") then
UpdateChatBarTextSize(value)
elseif (setting == "ShowChannelsBar") then
UpdateShowChannelsBar(value)
end
end)
self.GuiObject = BaseFrame
self.GuiObjects.BaseFrame = BaseFrame
self.GuiObjects.ChatBarParentFrame = ChatBarParentFrame
self.GuiObjects.ChannelsBarParentFrame = ChannelsBarParentFrame
self.GuiObjects.ChatChannelParentFrame = ChatChannelParentFrame
self.GuiObjects.ChatResizerFrame = ChatResizerFrame
self.GuiObjects.ResizeIcon = ResizeIcon
self:AnimGuiObjects()
end
function methods:GetChatBar()
return self.ChatBar
end
function methods:RegisterChatBar(ChatBar)
self.ChatBar = ChatBar
self.ChatBar:CreateGuiObjects(self.GuiObjects.ChatBarParentFrame)
end
function methods:RegisterChannelsBar(ChannelsBar)
self.ChannelsBar = ChannelsBar
self.ChannelsBar:CreateGuiObjects(self.GuiObjects.ChannelsBarParentFrame)
end
function methods:RegisterMessageLogDisplay(MessageLogDisplay)
self.MessageLogDisplay = MessageLogDisplay
self.MessageLogDisplay.GuiObject.Parent = self.GuiObjects.ChatChannelParentFrame
end
function methods:AddChannel(channelName)
if (self:GetChannel(channelName)) then
error("Channel '" .. channelName .. "' already exists!")
return
end
local channel = moduleChatChannel.new(channelName, self.MessageLogDisplay)
self.Channels[channelName:lower()] = channel
channel:SetActive(false)
local tab = self.ChannelsBar:AddChannelTab(channelName)
tab.NameTag.MouseButton1Click:connect(function()
self:SwitchCurrentChannel(channelName)
end)
channel:RegisterChannelTab(tab)
return channel
end
function methods:GetFirstChannel()
--// Channels are not indexed numerically, so this function is necessary.
--// Grabs and returns the first channel it happens to, or nil if none exist.
for i, v in pairs(self.Channels) do
return v
end
return nil
end
function methods:RemoveChannel(channelName)
if (not self:GetChannel(channelName)) then
error("Channel '" .. channelName .. "' does not exist!")
end
local indexName = channelName:lower()
local needsChannelSwitch = false
if (self.Channels[indexName] == self:GetCurrentChannel()) then
needsChannelSwitch = true
self:SwitchCurrentChannel(nil)
end
self.Channels[indexName]:Destroy()
self.Channels[indexName] = nil
self.ChannelsBar:RemoveChannelTab(channelName)
if (needsChannelSwitch) then
local generalChannelExists = (self:GetChannel(ChatSettings.GeneralChannelName) ~= nil)
local removingGeneralChannel = (indexName == ChatSettings.GeneralChannelName:lower())
local targetSwitchChannel = nil
if (generalChannelExists and not removingGeneralChannel) then
targetSwitchChannel = ChatSettings.GeneralChannelName
else
local firstChannel = self:GetFirstChannel()
targetSwitchChannel = (firstChannel and firstChannel.Name or nil)
end
self:SwitchCurrentChannel(targetSwitchChannel)
end
if not ChatSettings.ShowChannelsBar then
if self.ChatBar.TargetChannel == channelName then
self.ChatBar:SetChannelTarget(ChatSettings.GeneralChannelName)
end
end
end
function methods:GetChannel(channelName)
return channelName and self.Channels[channelName:lower()] or nil
end
function methods:GetTargetMessageChannel()
if (not ChatSettings.ShowChannelsBar) then
return self.ChatBar.TargetChannel
else
local curChannel = self:GetCurrentChannel()
return curChannel and curChannel.Name
end
end
function methods:GetCurrentChannel()
return self.CurrentChannel
end
function methods:SwitchCurrentChannel(channelName)
if (not ChatSettings.ShowChannelsBar) then
local targ = self:GetChannel(channelName)
if (targ) then
self.ChatBar:SetChannelTarget(targ.Name)
end
channelName = ChatSettings.GeneralChannelName
end
local cur = self:GetCurrentChannel()
local new = self:GetChannel(channelName)
if new == nil then
error(string.format("Channel '%s' does not exist.", channelName))
end
if (new ~= cur) then
if (cur) then
cur:SetActive(false)
local tab = self.ChannelsBar:GetChannelTab(cur.Name)
tab:SetActive(false)
end
if (new) then
new:SetActive(true)
local tab = self.ChannelsBar:GetChannelTab(new.Name)
tab:SetActive(true)
end
self.CurrentChannel = new
end
end
function methods:UpdateFrameVisibility()
self.GuiObject.Visible = (self.Visible and self.CoreGuiEnabled)
end
function methods:GetVisible()
return self.Visible
end
function methods:SetVisible(visible)
self.Visible = visible
self:UpdateFrameVisibility()
end
function methods:GetCoreGuiEnabled()
return self.CoreGuiEnabled
end
function methods:SetCoreGuiEnabled(enabled)
self.CoreGuiEnabled = enabled
self:UpdateFrameVisibility()
end
function methods:EnableResizable()
self.GuiObjects.ChatResizerFrame.Active = true
end
function methods:DisableResizable()
self.GuiObjects.ChatResizerFrame.Active = false
end
function methods:FadeOutBackground(duration)
self.ChannelsBar:FadeOutBackground(duration)
self.MessageLogDisplay:FadeOutBackground(duration)
self.ChatBar:FadeOutBackground(duration)
self.AnimParams.Background_TargetTransparency = 1
self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
end
function methods:FadeInBackground(duration)
self.ChannelsBar:FadeInBackground(duration)
self.MessageLogDisplay:FadeInBackground(duration)
self.ChatBar:FadeInBackground(duration)
self.AnimParams.Background_TargetTransparency = 0.6
self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
end
function methods:FadeOutText(duration)
self.MessageLogDisplay:FadeOutText(duration)
self.ChannelsBar:FadeOutText(duration)
end
function methods:FadeInText(duration)
self.MessageLogDisplay:FadeInText(duration)
self.ChannelsBar:FadeInText(duration)
end
function methods:AnimGuiObjects()
self.GuiObjects.ChatChannelParentFrame.BackgroundTransparency = self.AnimParams.Background_CurrentTransparency
self.GuiObjects.ChatResizerFrame.BackgroundTransparency = self.AnimParams.Background_CurrentTransparency
self.GuiObjects.ResizeIcon.ImageTransparency = self.AnimParams.Background_CurrentTransparency
end
function methods:InitializeAnimParams()
self.AnimParams.Background_TargetTransparency = 0.6
self.AnimParams.Background_CurrentTransparency = 0.6
self.AnimParams.Background_NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(0)
end
function methods:Update(dtScale)
self.ChatBar:Update(dtScale)
self.ChannelsBar:Update(dtScale)
self.MessageLogDisplay:Update(dtScale)
self.AnimParams.Background_CurrentTransparency = CurveUtil:Expt(
self.AnimParams.Background_CurrentTransparency,
self.AnimParams.Background_TargetTransparency,
self.AnimParams.Background_NormalizedExptValue,
dtScale
)
self:AnimGuiObjects()
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new()
local obj = setmetatable({}, methods)
obj.GuiObject = nil
obj.GuiObjects = {}
obj.ChatBar = nil
obj.ChannelsBar = nil
obj.MessageLogDisplay = nil
obj.Channels = {}
obj.CurrentChannel = nil
obj.Visible = true
obj.CoreGuiEnabled = true
obj.AnimParams = {}
obj:InitializeAnimParams()
return obj
end
return module
@@ -0,0 +1,180 @@
local runnerScriptName = "ChatScript"
local bubbleChatScriptName = "BubbleChat"
local installDirectory = game:GetService("Chat")
local PlayersService = game:GetService("Players")
local StarterPlayerScripts = game:GetService("StarterPlayer"):WaitForChild("StarterPlayerScripts")
local function LoadLocalScript(location, name, parent)
local originalModule = location:WaitForChild(name)
local script = Instance.new("LocalScript")
script.Name = name
script.Source = originalModule.Source
script.Parent = parent
return script
end
local function LoadModule(location, name, parent)
local originalModule = location:WaitForChild(name)
local module = Instance.new("ModuleScript")
module.Name = name
module.Source = originalModule.Source
module.Parent = parent
return module
end
local function GetBoolValue(parent, name, defaultValue)
local boolValue = parent:FindFirstChild(name)
if boolValue then
if boolValue:IsA("BoolValue") then
return boolValue.Value
end
end
return defaultValue
end
local function Install()
local chatScriptArchivable = true
local ChatScript = installDirectory:FindFirstChild(runnerScriptName)
if not ChatScript then
chatScriptArchivable = false
ChatScript = LoadLocalScript(script.Parent, runnerScriptName, installDirectory)
local ChatMain = LoadModule(script.Parent, "ChatMain", ChatScript)
LoadModule(script.Parent, "ChannelsBar", ChatMain)
LoadModule(script.Parent, "ChatBar", ChatMain)
LoadModule(script.Parent, "ChatChannel", ChatMain)
LoadModule(script.Parent, "MessageLogDisplay", ChatMain)
LoadModule(script.Parent, "ChatWindow", ChatMain)
LoadModule(script.Parent, "MessageLabelCreator", ChatMain)
LoadModule(script.Parent, "CommandProcessor", ChatMain)
LoadModule(script.Parent, "ChannelsTab", ChatMain)
LoadModule(script.Parent.Parent.Parent.Common, "ObjectPool", ChatMain)
LoadModule(script.Parent, "MessageSender", ChatMain)
LoadModule(script.Parent, "CurveUtil", ChatMain)
end
local bubbleChatScriptArchivable = true
local BubbleChatScript = installDirectory:FindFirstChild(bubbleChatScriptName)
if not BubbleChatScript then
bubbleChatScriptArchivable = false
BubbleChatScript = LoadLocalScript(script.Parent.BubbleChat, bubbleChatScriptName, installDirectory)
end
local clientChatModules = installDirectory:FindFirstChild("ClientChatModules")
if not clientChatModules then
clientChatModules = Instance.new("Folder")
clientChatModules.Name = "ClientChatModules"
clientChatModules.Archivable = false
clientChatModules.Parent = installDirectory
end
local chatSettings = clientChatModules:FindFirstChild("ChatSettings")
if not chatSettings then
LoadModule(script.Parent.DefaultClientChatModules, "ChatSettings", clientChatModules)
end
local chatConstants = clientChatModules:FindFirstChild("ChatConstants")
if not chatConstants then
LoadModule(script.Parent.DefaultClientChatModules, "ChatConstants", clientChatModules)
end
local ChatLocalization = clientChatModules:FindFirstChild("ChatLocalization")
if not ChatLocalization then
LoadModule(script.Parent.DefaultClientChatModules, "ChatLocalization", clientChatModules)
end
local MessageCreatorModules = clientChatModules:FindFirstChild("MessageCreatorModules")
if not MessageCreatorModules then
MessageCreatorModules = Instance.new("Folder")
MessageCreatorModules.Name = "MessageCreatorModules"
MessageCreatorModules.Archivable = false
local InsertDefaults = Instance.new("BoolValue")
InsertDefaults.Name = "InsertDefaultModules"
InsertDefaults.Value = true
InsertDefaults.Parent = MessageCreatorModules
MessageCreatorModules.Parent = clientChatModules
end
local insertDefaultMessageCreators = GetBoolValue(MessageCreatorModules, "InsertDefaultModules", false)
if insertDefaultMessageCreators then
local creatorModules = script.Parent.DefaultClientChatModules.MessageCreatorModules:GetChildren()
for i = 1, #creatorModules do
if not MessageCreatorModules:FindFirstChild(creatorModules[i].Name) then
LoadModule(script.Parent.DefaultClientChatModules.MessageCreatorModules, creatorModules[i].Name, MessageCreatorModules)
end
end
end
local CommandModules = clientChatModules:FindFirstChild("CommandModules")
if not CommandModules then
CommandModules = Instance.new("Folder")
CommandModules.Name = "CommandModules"
CommandModules.Archivable = false
local InsertDefaults = Instance.new("BoolValue")
InsertDefaults.Name = "InsertDefaultModules"
InsertDefaults.Value = true
InsertDefaults.Parent = CommandModules
CommandModules.Parent = clientChatModules
end
local insertDefaultCommands = GetBoolValue(CommandModules, "InsertDefaultModules", false)
if insertDefaultCommands then
local commandModules = script.Parent.DefaultClientChatModules.CommandModules:GetChildren()
for i = 1, #commandModules do
if not CommandModules:FindFirstChild(commandModules[i].Name) then
LoadModule(script.Parent.DefaultClientChatModules.CommandModules, commandModules[i].Name, CommandModules)
end
end
end
if not StarterPlayerScripts:FindFirstChild(runnerScriptName) then
local ChatScriptCopy = ChatScript:Clone()
ChatScriptCopy.Parent = StarterPlayerScripts
ChatScriptCopy.Archivable = false
local currentPlayers = PlayersService:GetPlayers()
for _, player in pairs(currentPlayers) do
-- These players were created before we added the ChatScript to StarterPlayerScripts
-- This only happens in studio.
if player:FindFirstChild("PlayerGui") then
-- We need to put this script in the PlayerGui as we can't access the PlayerScripts on the server.
local installVerifier = LoadLocalScript(script.Parent, "ChatInstallVerifier", player.PlayerGui)
local scriptToVerify = Instance.new("StringValue")
scriptToVerify.Name = "ScriptToVerify"
scriptToVerify.Value = runnerScriptName
scriptToVerify.Parent = installVerifier
end
end
end
ChatScript.Archivable = chatScriptArchivable
if not StarterPlayerScripts:FindFirstChild(bubbleChatScriptName) then
local BubbleChatScriptCopy = BubbleChatScript:Clone()
BubbleChatScriptCopy.Parent = StarterPlayerScripts
BubbleChatScriptCopy.Archivable = false
local currentPlayers = PlayersService:GetPlayers()
for _, player in pairs(currentPlayers) do
if player:FindFirstChild("PlayerGui") then
local installVerifier = LoadLocalScript(script.Parent, "ChatInstallVerifier", player.PlayerGui)
local scriptToVerify = Instance.new("StringValue")
scriptToVerify.Name = "ScriptToVerify"
scriptToVerify.Value = bubbleChatScriptName
scriptToVerify.Parent = installVerifier
end
end
end
BubbleChatScript.Archivable = bubbleChatScriptArchivable
end
return Install
@@ -0,0 +1,70 @@
-- // FileName: ProcessCommands.lua
-- // Written by: TheGamer101
-- // Description: Module for processing commands using the client CommandModules
local module = {}
local methods = {}
methods.__index = methods
--////////////////////////////// Include
--//////////////////////////////////////
local Chat = game:GetService("Chat")
local clientChatModules = Chat:WaitForChild("ClientChatModules")
local commandModules = clientChatModules:WaitForChild("CommandModules")
local commandUtil = require(commandModules:WaitForChild("Util"))
local modulesFolder = script.Parent
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
function methods:SetupCommandProcessors()
local commands = commandModules:GetChildren()
for i = 1, #commands do
if commands[i]:IsA("ModuleScript") then
if commands[i].Name ~= "Util" then
local commandProcessor = require(commands[i])
local processorType = commandProcessor[commandUtil.KEY_COMMAND_PROCESSOR_TYPE]
local processorFunction = commandProcessor[commandUtil.KEY_PROCESSOR_FUNCTION]
if processorType == commandUtil.IN_PROGRESS_MESSAGE_PROCESSOR then
table.insert(self.InProgressMessageProcessors, processorFunction)
elseif processorType == commandUtil.COMPLETED_MESSAGE_PROCESSOR then
table.insert(self.CompletedMessageProcessors, processorFunction)
end
end
end
end
end
function methods:ProcessCompletedChatMessage(message, ChatWindow)
for i = 1, #self.CompletedMessageProcessors do
local processedCommand = self.CompletedMessageProcessors[i](message, ChatWindow, ChatSettings)
if processedCommand then
return true
end
end
return false
end
function methods:ProcessInProgressChatMessage(message, ChatWindow, ChatBar)
for i = 1, #self.InProgressMessageProcessors do
local customState = self.InProgressMessageProcessors[i](message, ChatWindow, ChatBar, ChatSettings)
if customState then
return customState
end
end
return nil
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new()
local obj = setmetatable({}, methods)
obj.CompletedMessageProcessors = {}
obj.InProgressMessageProcessors = {}
obj:SetupCommandProcessors()
return obj
end
return module
@@ -0,0 +1,78 @@
local CurveUtil = { }
local DEFAULT_THRESHOLD = 0.01
function CurveUtil:Expt(start, to, pct, dt_scale)
if math.abs(to - start) < DEFAULT_THRESHOLD then
return to
end
local y = CurveUtil:Expty(start,to,pct,dt_scale)
--rtv = start + (to - start) * timescaled_friction--
local delta = (to - start) * y
return start + delta
end
function CurveUtil:Expty(start, to, pct, dt_scale)
--y = e ^ (-a * timescale)--
local friction = 1 - pct
local a = -math.log(friction)
return 1 - math.exp(-a * dt_scale)
end
function CurveUtil:Sign(val)
if val > 0 then
return 1
elseif val < 0 then
return -1
else
return 0
end
end
function CurveUtil:BezierValForT(p0, p1, p2, p3, t)
local cp0 = (1 - t) * (1 - t) * (1 - t)
local cp1 = 3 * t * (1-t)*(1-t)
local cp2 = 3 * t * t * (1 - t)
local cp3 = t * t * t
return cp0 * p0 + cp1 * p1 + cp2 * p2 + cp3 * p3
end
CurveUtil._BezierPt2ForT = { x = 0; y = 0 }
function CurveUtil:BezierPt2ForT(
p0x, p0y,
p1x, p1y,
p2x, p2y,
p3x, p3y,
t)
CurveUtil._BezierPt2ForT.x = CurveUtil:BezierValForT(p0x,p1x,p2x,p3x,t)
CurveUtil._BezierPt2ForT.y = CurveUtil:BezierValForT(p0y,p1y,p2y,p3y,t)
return CurveUtil._BezierPt2ForT
end
function CurveUtil:YForPointOf2PtLine(pt1, pt2, x)
--(y - y1)/(x - x1) = m--
local m = (pt1.y - pt2.y) / (pt1.x - pt2.x)
--y - mx = b--
local b = pt1.y - m * pt1.x
return m * x + b
end
function CurveUtil:DeltaTimeToTimescale(s_frame_delta_time)
return s_frame_delta_time / (1.0 / 60.0)
end
function CurveUtil:SecondsToTick(sec)
return (1 / 60.0) / sec
end
function CurveUtil:ExptValueInSeconds(threshold, start, seconds)
return 1 - math.pow((threshold / start), 1 / (60.0 * seconds))
end
function CurveUtil:NormalizedDefaultExptValueInSeconds(seconds)
return self:ExptValueInSeconds(DEFAULT_THRESHOLD, 1, seconds)
end
return CurveUtil
@@ -0,0 +1,6 @@
{
"lint": {
"FunctionUnused": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,28 @@
-- // FileName: ChatConstants.lua
-- // Written by: TheGamer101
-- // Description: Module for creating chat constants shared between server and client.
local module = {}
---[[ Message Types ]]
module.MessageTypeDefault = "Message"
module.MessageTypeSystem = "System"
module.MessageTypeMeCommand = "MeCommand"
module.MessageTypeWelcome = "Welcome"
module.MessageTypeSetCore = "SetCore"
module.MessageTypeWhisper = "Whisper"
--[[ Version ]]
module.MajorVersion = 0
module.MinorVersion = 8
module.BuildVersion = "2018.05.16"
---[[ Command/Filter Priorities ]]
module.VeryLowPriority = -5
module.LowPriority = 0
module.StandardPriority = 10
module.HighPriority = 20
module.VeryHighPriority = 25
module.WhisperChannelPrefix = "To "
return module
@@ -0,0 +1,114 @@
--!nocheck
local LocalizationService = game:GetService("LocalizationService")
local ChatService = game:GetService("Chat")
local success, userShouldLocalizeServerMessages = pcall(function() return UserSettings():IsUserFeatureEnabled("UserShouldLocalizeServerMessages") end)
local userShouldLocalizeServerMessages = success and userShouldLocalizeServerMessages
local existingKey = {
["System"] = "InGame.Chat.Label.SystemMessagePrefix",
["Team"] = "InGame.Chat.Label.TeamMessagePrefix",
["From "] = "InGame.Chat.Label.From",
["To "] = "InGame.Chat.Label.To",
}
local ChatLocalization = {
_hasFetchedLocalization = false,
}
function ChatLocalization:_getTranslator()
if not self._translator and not self._hasFetchedLocalization then
-- Don't keep retrying if this fails.
self._hasFetchedLocalization = true
local localizationTable = ChatService:WaitForChild("ChatLocalization", 4)
if localizationTable then
self._translator = localizationTable:GetTranslator(LocalizationService.RobloxLocaleId)
LocalizationService:GetPropertyChangedSignal("RobloxLocaleId"):Connect(function()
-- If RobloxLocaleId changes invalidate the cached Translator.
self._hasFetchedLocalization = false
self._translator = nil
end)
else
warn("Missing ChatLocalization. Chat interface will not be localized.")
end
end
return self._translator
end
function ChatLocalization:Get(key, default, extraParameters)
local rtv = default
pcall(function()
local translator = self:_getTranslator()
if translator then
rtv = translator:FormatByKey(key, extraParameters)
else
warn("Missing Translator. Used default for", key)
end
end)
return rtv
end
function ChatLocalization:LocalizeFormattedMessage(message)
if not userShouldLocalizeServerMessages then
return message
end
local keyStart, keyEnd = string.find(message, "{RBX_LOCALIZATION_KEY}")
if not keyStart then
return message
end
local defaultStart, defaultEnd = string.find(message, "{RBX_LOCALIZATION_DEFAULT}")
if not defaultStart then
return message
end
local paramStart, paramEnd = string.find(message, "{RBX_LOCALIZATION_PARAMS}")
local key = string.sub(message, keyEnd + 1, defaultStart - 1)
local default
if not paramStart then
default = string.sub(message, defaultEnd + 1)
return self:Get(key, default)
end
default = string.sub(message, defaultEnd + 1, paramStart - 1)
local params = string.sub(message, paramEnd + 1)
local extraParameters = {}
for k,v in string.gmatch(params,"([^%s]+)=([^%s]+)") do
extraParameters[k] = v
end
return self:Get(key, default, extraParameters)
end
function ChatLocalization:FormatMessageToSend(key, defaultMessage, parameterName, value)
if userShouldLocalizeServerMessages then
if parameterName and value then
return "{RBX_LOCALIZATION_KEY}"..key.."{RBX_LOCALIZATION_DEFAULT}"..defaultMessage
.."{RBX_LOCALIZATION_PARAMS}"..parameterName.."="..value
else
return "{RBX_LOCALIZATION_KEY}"..key.."{RBX_LOCALIZATION_DEFAULT}"..defaultMessage
end
else
if parameterName and value then
return string.gsub(self:Get(key,defaultMessage), "{"..parameterName.."}", value)
else
return self:Get(key,defaultMessage)
end
end
end
-- try localize whole/part of string
-- no more-than-1 replacement for current existing keys.
function ChatLocalization:tryLocalize(rawString)
if existingKey[rawString] then
return self:Get(existingKey[rawString], rawString)
end
for enString, localizationKey in pairs(existingKey) do
if string.find(rawString, enString) then
local localizedPart = self:Get(localizationKey, enString)
return string.gsub(rawString, enString, localizedPart, 1)
end
end
return rawString
end
return ChatLocalization
@@ -0,0 +1,115 @@
-- // FileName: ChatSettings.lua
-- // Written by: Xsitsu
-- // Description: Settings module for configuring different aspects of the chat window.
local PlayersService = game:GetService("Players")
local ChatService = game:GetService("Chat")
local clientChatModules = script.Parent
local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants"))
local module = {}
---[[ Chat Behaviour Settings ]]
module.WindowDraggable = false
module.WindowResizable = false
module.ShowChannelsBar = false
module.GamepadNavigationEnabled = false
module.AllowMeCommand = false -- Me Command will only be effective when this set to true
module.ShowUserOwnFilteredMessage = true --Show a user the filtered version of their message rather than the original.
-- Make the chat work when the top bar is off
module.ChatOnWithTopBarOff = false
module.ScreenGuiDisplayOrder = 6 -- The DisplayOrder value for the ScreenGui containing the chat.
module.ShowFriendJoinNotification = true -- Show a notification in the chat when a players friend joins the game.
--- Replace with true/false to force the chat type. Otherwise this will default to the setting on the website.
module.BubbleChatEnabled = PlayersService.BubbleChat
module.ClassicChatEnabled = PlayersService.ClassicChat
---[[ Chat Text Size Settings ]]
module.ChatWindowTextSize = 18
module.ChatChannelsTabTextSize = 18
module.ChatBarTextSize = 18
module.ChatWindowTextSizePhone = 14
module.ChatChannelsTabTextSizePhone = 18
module.ChatBarTextSizePhone = 14
---[[ Font Settings ]]
module.DefaultFont = Enum.Font.SourceSansBold
module.ChatBarFont = Enum.Font.SourceSansBold
----[[ Color Settings ]]
module.BackGroundColor = Color3.new(0, 0, 0)
module.DefaultMessageColor = Color3.new(1, 1, 1)
module.DefaultNameColor = Color3.new(1, 1, 1)
module.ChatBarBackGroundColor = Color3.new(0, 0, 0)
module.ChatBarBoxColor = Color3.new(1, 1, 1)
module.ChatBarTextColor = Color3.new(0, 0, 0)
module.ChannelsTabUnselectedColor = Color3.new(0, 0, 0)
module.ChannelsTabSelectedColor = Color3.new(30/255, 30/255, 30/255)
module.DefaultChannelNameColor = Color3.fromRGB(35, 76, 142)
module.WhisperChannelNameColor = Color3.fromRGB(102, 14, 102)
module.ErrorMessageTextColor = Color3.fromRGB(245, 50, 50)
---[[ Window Settings ]]
module.MinimumWindowSize = UDim2.new(0.3, 0, 0.25, 0)
module.MaximumWindowSize = UDim2.new(1, 0, 1, 0) -- if you change this to be greater than full screen size, weird things start to happen with size/position bounds checking.
module.DefaultWindowPosition = UDim2.new(0, 0, 0, 0)
local extraOffset = (7 * 2) + (5 * 2) -- Extra chatbar vertical offset
module.DefaultWindowSizePhone = UDim2.new(0.5, 0, 0.5, extraOffset)
module.DefaultWindowSizeTablet = UDim2.new(0.4, 0, 0.3, extraOffset)
module.DefaultWindowSizeDesktop = UDim2.new(0.3, 0, 0.25, extraOffset)
---[[ Fade Out and In Settings ]]
module.ChatWindowBackgroundFadeOutTime = 3.5 --Chat background will fade out after this many seconds.
module.ChatWindowTextFadeOutTime = 30 --Chat text will fade out after this many seconds.
module.ChatDefaultFadeDuration = 0.8
module.ChatShouldFadeInFromNewInformation = false
module.ChatAnimationFPS = 20.0
---[[ Channel Settings ]]
module.GeneralChannelName = "All" -- You can set to nil to turn off echoing to a general channel.
module.EchoMessagesInGeneralChannel = true -- Should messages to channels other than general be echoed into the general channel.
-- Setting this to false should be used with ShowChannelsBar
module.ChannelsBarFullTabSize = 4 -- number of tabs in bar before it starts to scroll
module.MaxChannelNameLength = 12
--// Although this feature is pretty much ready, it needs some UI design still.
module.RightClickToLeaveChannelEnabled = false
module.MessageHistoryLengthPerChannel = 50
-- Show the help text for joining and leaving channels. This is not useful unless custom channels have been added.
-- So it is turned off by default.
module.ShowJoinAndLeaveHelpText = false
---[[ Message Settings ]]
module.MaximumMessageLength = 200
module.DisallowedWhiteSpace = {"\n", "\r", "\t", "\v", "\f"}
module.ClickOnPlayerNameToWhisper = true
module.ClickOnChannelNameToSetMainChannel = true
module.BubbleChatMessageTypes = {ChatConstants.MessageTypeDefault, ChatConstants.MessageTypeWhisper}
---[[ Misc Settings ]]
module.WhisperCommandAutoCompletePlayerNames = true
--[[ Display Names ]]
--Uses DisplayNames instead of UserNames in chat messages
module.PlayerDisplayNamesEnabled = false
--Allows users to do /w displayName along with /w userName, only works if PlayerDisplayNamesEnabled is 'true'
module.WhisperByDisplayName = false
local ChangedEvent = Instance.new("BindableEvent")
local proxyTable = setmetatable({},
{
__index = function(tbl, index)
return module[index]
end,
__newindex = function(tbl, index, value)
module[index] = value
ChangedEvent:Fire(index, value)
end,
})
rawset(proxyTable, "SettingsChanged", ChangedEvent.Event)
return proxyTable
@@ -0,0 +1,6 @@
{
"lint": {
"LocalUnused": "fatal",
"ImportUnused": "fatal"
}
}
@@ -0,0 +1,21 @@
-- // FileName: ClearMessages.lua
-- // Written by: TheGamer101
-- // Description: Command to clear the message log of the current channel.
local util = require(script.Parent:WaitForChild("Util"))
function ProcessMessage(message, ChatWindow, ChatSettings)
if string.sub(message, 1, 4):lower() == "/cls" or string.sub(message, 1, 6):lower() == "/clear" then
local currentChannel = ChatWindow:GetCurrentChannel()
if (currentChannel) then
currentChannel:ClearMessageLog()
end
return true
end
return false
end
return {
[util.KEY_COMMAND_PROCESSOR_TYPE] = util.COMPLETED_MESSAGE_PROCESSOR,
[util.KEY_PROCESSOR_FUNCTION] = ProcessMessage
}
@@ -0,0 +1,26 @@
-- // FileName: DeveloperConsole.lua
-- // Written by: TheGamer101
-- // Description: Command to open or close the developer console.
local StarterGui = game:GetService("StarterGui")
local util = require(script.Parent:WaitForChild("Util"))
function ProcessMessage(message, ChatWindow, ChatSettings)
if string.sub(message, 1, 8):lower() == "/console" or string.sub(message, 1, 11):lower() == "/newconsole" then
local success, developerConsoleVisible = pcall(function() return StarterGui:GetCore("DevConsoleVisible") end)
if success then
local success, err = pcall(function() StarterGui:SetCore("DevConsoleVisible", not developerConsoleVisible) end)
if not success and err then
print("Error making developer console visible: " ..err)
end
end
return true
end
return false
end
return {
[util.KEY_COMMAND_PROCESSOR_TYPE] = util.COMPLETED_MESSAGE_PROCESSOR,
[util.KEY_PROCESSOR_FUNCTION] = ProcessMessage
}
@@ -0,0 +1,25 @@
-- // FileName: GetVersion.lua
-- // Written by: spotco
-- // Description: Command to print the chat version.
local util = require(script.Parent:WaitForChild("Util"))
local ChatConstants = require(script.Parent.Parent:WaitForChild("ChatConstants"))
local function ProcessMessage(message, ChatWindow, _)
if string.sub(message, 1, 8):lower() == "/version" or string.sub(message, 1, 9):lower() == "/version " then
util:SendSystemMessageToSelf(
string.format("This game is running chat version [%d.%d.%s].",
ChatConstants.MajorVersion,
ChatConstants.MinorVersion,
ChatConstants.BuildVersion),
ChatWindow:GetCurrentChannel(),
{})
return true
end
return false
end
return {
[util.KEY_COMMAND_PROCESSOR_TYPE] = util.COMPLETED_MESSAGE_PROCESSOR,
[util.KEY_PROCESSOR_FUNCTION] = ProcessMessage
}
@@ -0,0 +1,154 @@
-- // FileName: PlayEmote.lua
-- // Written by: TheGamer101
-- // Description: Play an emote by name.
local Chat = game:GetService("Chat")
local Players = game:GetService("Players")
local CommandModules = script.Parent
local Util = require(CommandModules:WaitForChild("Util"))
local ChatLocalization = nil
pcall(function() ChatLocalization = require(Chat.ClientChatModules.ChatLocalization) end)
if ChatLocalization == nil then ChatLocalization = { Get = function(self, key, fallback) return fallback end } end
local LocalPlayer = Players.LocalPlayer
local LegacyDefaultEmotes = {
wave = true,
point = true,
dance = true,
dance2 = true,
dance3 = true,
laugh = true,
cheer = true,
}
local LocalizationKeys = {
NotSupported = "InGame.Chat.Response.EmotesNotSupported",
R15Only = "InGame.Chat.Response.EmotesWrongAvatarType",
NoMatchingEmote = "InGame.Chat.Response.EmoteNotAvailable",
TemporarilyUnavailable = "InGame.Chat.Response.EmotesTemporarilyUnavailable",
}
local FallbackStrings = {
[LocalizationKeys.NotSupported] = "You can't use Emotes here.",
[LocalizationKeys.R15Only] = "Only R15 avatars can use Emotes.",
[LocalizationKeys.NoMatchingEmote] = "You can't use that Emote.",
[LocalizationKeys.TemporarilyUnavailable] = "You can't use Emotes right now.",
}
local ErrorColor = Color3.fromRGB(245, 50, 50)
local function getEmoteName(message)
if string.sub(message, 1, 3) == "/e " then
return string.sub(message, 4)
elseif string.sub(message, 1, 7) == "/emote " then
return string.sub(message, 8)
end
return nil
end
local function sendErrorMessage(channelObj, errorMessageKey)
local localizedString = ChatLocalization:Get(errorMessageKey, FallbackStrings[errorMessageKey])
local extraData = {
ChatColor = ErrorColor,
}
Util:SendSystemMessageToSelf(localizedString, channelObj, extraData)
end
local function ProcessMessage(message, ChatWindow, ChatSettings)
local emoteName = getEmoteName(message)
if not emoteName then
return false
end
if LegacyDefaultEmotes[emoteName] then
-- Just don't show these in the chat log. The animation script listens on these.
return true
end
local channelObj = ChatWindow:GetCurrentChannel()
if not channelObj then
return true
end
local character = LocalPlayer.Character
if not character then
sendErrorMessage(channelObj, LocalizationKeys.TemporarilyUnavailable)
return true
end
local animateScript = character:FindFirstChild("Animate")
if not animateScript then
sendErrorMessage(channelObj, LocalizationKeys.NotSupported)
return true
end
local playEmoteBindable = animateScript:FindFirstChild("PlayEmote")
if not playEmoteBindable then
sendErrorMessage(channelObj, LocalizationKeys.NotSupported)
return true
end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then
sendErrorMessage(channelObj, LocalizationKeys.TemporarilyUnavailable)
return true
end
if humanoid.RigType ~= Enum.HumanoidRigType.R15 then
sendErrorMessage(channelObj, LocalizationKeys.R15Only)
return true
end
local humanoidDescription = humanoid:FindFirstChildOfClass("HumanoidDescription")
if not humanoidDescription then
sendErrorMessage(channelObj, LocalizationKeys.TemporarilyUnavailable)
return true
end
local lowerCaseEmoteNamesMap = {}
local emotes = humanoidDescription:GetEmotes()
for name, _ in pairs(emotes) do
lowerCaseEmoteNamesMap[string.lower(name)] = name
end
local slot = tonumber(emoteName)
if slot then
local equippedEmotes = humanoidDescription:GetEquippedEmotes()
for _, emoteInfo in pairs(equippedEmotes) do
if emoteInfo.Slot == slot then
emoteName = emoteInfo.Name
end
end
end
local emoteNameLower = string.lower(emoteName)
emoteName = lowerCaseEmoteNamesMap[emoteNameLower]
if not emoteName then
sendErrorMessage(channelObj, LocalizationKeys.NoMatchingEmote)
return true
end
spawn(function()
local ok, didPlay = pcall(function() return humanoid:PlayEmote(emoteName) end)
if not ok then
sendErrorMessage(channelObj, LocalizationKeys.NotSupported)
elseif not didPlay then
sendErrorMessage(channelObj, LocalizationKeys.TemporarilyUnavailable)
end
end)
return true
end
return {
[Util.KEY_COMMAND_PROCESSOR_TYPE] = Util.COMPLETED_MESSAGE_PROCESSOR,
[Util.KEY_PROCESSOR_FUNCTION] = ProcessMessage
}
@@ -0,0 +1,34 @@
-- // FileName: SwallowGuestChat.lua
-- // Written by: TheGamer101
-- // Description: Stop Guests from chatting and give them a message telling them to sign up.
-- // Guests are generally not allowed to chat, so please do not remove this.
local util = require(script.Parent:WaitForChild("Util"))
local RunService = game:GetService("RunService")
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
if ChatLocalization == nil then ChatLocalization = {} function ChatLocalization:Get(key,default) return default end end
function ProcessMessage(message, ChatWindow, ChatSettings)
local LocalPlayer = game:GetService("Players").LocalPlayer
if LocalPlayer and LocalPlayer.UserId < 0 and not RunService:IsStudio() then
local channelObj = ChatWindow:GetCurrentChannel()
if channelObj then
util:SendSystemMessageToSelf(
ChatLocalization:Get("GameChat_SwallowGuestChat_Message","Create a free account to get access to chat permissions!"),
channelObj,
{}
)
end
return true
end
return false
end
return {
[util.KEY_COMMAND_PROCESSOR_TYPE] = util.COMPLETED_MESSAGE_PROCESSOR,
[util.KEY_PROCESSOR_FUNCTION] = ProcessMessage
}
@@ -0,0 +1,54 @@
-- // FileName: ClearMessages.lua
-- // Written by: TheGamer101
-- // Description: Command to switch channel.
local util = require(script.Parent:WaitForChild("Util"))
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
if ChatLocalization == nil then ChatLocalization = { Get = function(self, key, default) return default end } end
function ProcessMessage(message, ChatWindow, ChatSettings)
if string.sub(message, 1, 3):lower() ~= "/c " then
return false
end
local channelName = string.sub(message, 4)
local targetChannel = ChatWindow:GetChannel(channelName)
if targetChannel then
ChatWindow:SwitchCurrentChannel(channelName)
if not ChatSettings.ShowChannelsBar then
local currentChannel = ChatWindow:GetCurrentChannel()
if currentChannel then
util:SendSystemMessageToSelf(
string.gsub(ChatLocalization:Get(
"GameChat_SwitchChannel_NowInChannel",
string.format("You are now chatting in channel: '%s'", channelName),
{RBX_NAME = channelName}
),"{RBX_NAME}",channelName),
targetChannel, {}
)
end
end
else
local currentChannel = ChatWindow:GetCurrentChannel()
if currentChannel then
util:SendSystemMessageToSelf(
string.gsub(ChatLocalization:Get(
"GameChat_SwitchChannel_NotInChannel",
string.format("You are not in channel: '%s'", channelName),
{RBX_NAME = channelName}
),"{RBX_NAME}",channelName),
currentChannel, {ChatColor = Color3.fromRGB(245, 50, 50)}
)
end
end
return true
end
return {
[util.KEY_COMMAND_PROCESSOR_TYPE] = util.COMPLETED_MESSAGE_PROCESSOR,
[util.KEY_PROCESSOR_FUNCTION] = ProcessMessage
}
@@ -0,0 +1,130 @@
-- // FileName: Team.lua
-- // Written by: Partixel/TheGamer101
-- // Description: Team chat bar manipulation.
local PlayersService = game:GetService("Players")
local TEAM_COMMANDS = {"/team ", "/t ", "% "}
function IsTeamCommand(message)
for i = 1, #TEAM_COMMANDS do
local teamCommand = TEAM_COMMANDS[i]
if string.sub(message, 1, teamCommand:len()):lower() == teamCommand then
return true
end
end
return false
end
local teamStateMethods = {}
teamStateMethods.__index = teamStateMethods
local util = require(script.Parent:WaitForChild("Util"))
local TeamCustomState = {}
function teamStateMethods:EnterTeamChat()
self.TeamChatEntered = true
self.MessageModeButton.Size = UDim2.new(0, 1000, 1, 0)
self.MessageModeButton.Text = "[Team]"
self.MessageModeButton.TextColor3 = self:GetTeamChatColor()
local xSize = self.MessageModeButton.TextBounds.X
self.MessageModeButton.Size = UDim2.new(0, xSize, 1, 0)
self.TextBox.Size = UDim2.new(1, -xSize, 1, 0)
self.TextBox.Position = UDim2.new(0, xSize, 0, 0)
self.OriginalTeamText = self.TextBox.Text
self.TextBox.Text = " "
end
function teamStateMethods:TextUpdated()
local newText = self.TextBox.Text
if not self.TeamChatEntered then
if IsTeamCommand(newText) then
self:EnterTeamChat()
end
else
if newText == "" then
self.MessageModeButton.Text = ""
self.MessageModeButton.Size = UDim2.new(0, 0, 0, 0)
self.TextBox.Size = UDim2.new(1, 0, 1, 0)
self.TextBox.Position = UDim2.new(0, 0, 0, 0)
self.TextBox.Text = ""
---Implement this when setting cursor positon is a thing.
---self.TextBox.Text = self.OriginalTeamText
self.TeamChatEntered = false
---Temporary until setting cursor position...
self.ChatBar:ResetCustomState()
self.ChatBar:CaptureFocus()
end
end
end
function teamStateMethods:GetMessage()
if self.TeamChatEntered then
return "/t " ..self.TextBox.Text
end
return self.TextBox.Text
end
function teamStateMethods:ProcessCompletedMessage()
return false
end
function teamStateMethods:Destroy()
self.MessageModeConnection:disconnect()
self.Destroyed = true
end
function teamStateMethods:GetTeamChatColor()
local LocalPlayer = PlayersService.LocalPlayer
if LocalPlayer.Team then
return LocalPlayer.Team.TeamColor.Color
end
if self.ChatSettings.DefaultChannelNameColor then
return self.ChatSettings.DefaultChannelNameColor
end
return Color3.fromRGB(35, 76, 142)
end
function TeamCustomState.new(ChatWindow, ChatBar, ChatSettings)
local obj = setmetatable({}, teamStateMethods)
obj.Destroyed = false
obj.ChatWindow = ChatWindow
obj.ChatBar = ChatBar
obj.ChatSettings = ChatSettings
obj.TextBox = ChatBar:GetTextBox()
obj.MessageModeButton = ChatBar:GetMessageModeTextButton()
obj.OriginalTeamText = ""
obj.TeamChatEntered = false
obj.MessageModeConnection = obj.MessageModeButton.MouseButton1Click:connect(function()
local chatBarText = obj.TextBox.Text
if string.sub(chatBarText, 1, 1) == " " then
chatBarText = string.sub(chatBarText, 2)
end
obj.ChatBar:ResetCustomState()
obj.ChatBar:SetTextBoxText(chatBarText)
obj.ChatBar:CaptureFocus()
end)
obj:EnterTeamChat()
return obj
end
function ProcessMessage(message, ChatWindow, ChatBar, ChatSettings)
if ChatBar.TargetChannel == "Team" then
return
end
if IsTeamCommand(message) then
return TeamCustomState.new(ChatWindow, ChatBar, ChatSettings)
end
return nil
end
return {
[util.KEY_COMMAND_PROCESSOR_TYPE] = util.IN_PROGRESS_MESSAGE_PROCESSOR,
[util.KEY_PROCESSOR_FUNCTION] = ProcessMessage
}
@@ -0,0 +1,63 @@
-- // FileName: Util.lua
-- // Written by: TheGamer101
-- // Description: Module for shared code between CommandModules.
--[[
Creating a command module:
1) Create a new module inside the CommandModules folder.
2) Create a function that takes a message, the ChatWindow object and the ChatSettings and returns
a bool command processed.
3) Return this function from the module.
--]]
local clientChatModules = script.Parent.Parent
local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants"))
local COMMAND_MODULES_VERSION = 1
local KEY_COMMAND_PROCESSOR_TYPE = "ProcessorType"
local KEY_PROCESSOR_FUNCTION = "ProcessorFunction"
---Command types.
---Process a command as it is being typed. This allows for manipulation of the chat bar.
local IN_PROGRESS_MESSAGE_PROCESSOR = 0
---Simply process a completed message.
local COMPLETED_MESSAGE_PROCESSOR = 1
local module = {}
local methods = {}
methods.__index = methods
function methods:SendSystemMessageToSelf(message, channelObj, extraData)
local messageData =
{
ID = -1,
FromSpeaker = nil,
SpeakerUserId = 0,
OriginalChannel = channelObj.Name,
IsFiltered = true,
MessageLength = string.len(message),
Message = message,
MessageType = ChatConstants.MessageTypeSystem,
Time = os.time(),
ExtraData = extraData,
}
channelObj:AddMessageToChannel(messageData)
end
function module.new()
local obj = setmetatable({}, methods)
obj.COMMAND_MODULES_VERSION = COMMAND_MODULES_VERSION
obj.KEY_COMMAND_PROCESSOR_TYPE = KEY_COMMAND_PROCESSOR_TYPE
obj.KEY_PROCESSOR_FUNCTION = KEY_PROCESSOR_FUNCTION
obj.IN_PROGRESS_MESSAGE_PROCESSOR = IN_PROGRESS_MESSAGE_PROCESSOR
obj.COMPLETED_MESSAGE_PROCESSOR = COMPLETED_MESSAGE_PROCESSOR
return obj
end
return module.new()
@@ -0,0 +1,222 @@
-- // FileName: Whisper.lua
-- // Written by: TheGamer101
-- // Description: Whisper chat bar manipulation.
local util = require(script.Parent:WaitForChild("Util"))
local ChatSettings = require(script.Parent.Parent:WaitForChild("ChatSettings"))
local PlayersService = game:GetService("Players")
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
local LocalPlayer = PlayersService.LocalPlayer
while LocalPlayer == nil do
PlayersService.ChildAdded:wait()
LocalPlayer = PlayersService.LocalPlayer
end
local whisperStateMethods = {}
whisperStateMethods.__index = whisperStateMethods
local WhisperCustomState = {}
function whisperStateMethods:TrimWhisperCommand(text)
if string.sub(text, 1, 3):lower() == "/w " then
return string.sub(text, 4)
elseif string.sub(text, 1, 9):lower() == "/whisper " then
return string.sub(text, 10)
end
return nil
end
function whisperStateMethods:TrimWhiteSpace(text)
local newText = string.gsub(text, "%s+", "")
local wasWhitespaceTrimmed = text[#text] == " "
return newText, wasWhitespaceTrimmed
end
function whisperStateMethods:ShouldAutoCompleteNames()
if ChatSettings.WhisperCommandAutoCompletePlayerNames ~= nil then
return ChatSettings.WhisperCommandAutoCompletePlayerNames
end
return true
end
function whisperStateMethods:GetWhisperingPlayer(enteredText)
enteredText = enteredText:lower()
local trimmedText = self:TrimWhisperCommand(enteredText)
if trimmedText then
local possiblePlayerName, whitespaceTrimmed = self:TrimWhiteSpace(trimmedText)
local possibleUserNameMatches = {}
local possibleDisplayNameMatches = {}
local players = PlayersService:GetPlayers()
for i = 1, #players do
if players[i] ~= LocalPlayer then
local lowerPlayerName = players[i].Name:lower()
if string.sub(lowerPlayerName, 1, string.len(possiblePlayerName)) == possiblePlayerName then
possibleUserNameMatches[players[i]] = players[i].Name:lower()
end
if ChatSettings.WhisperByDisplayName then
local lowerDisplayName = players[i].DisplayName:lower()
if string.sub(lowerDisplayName, 1, string.len(possiblePlayerName)) == possiblePlayerName then
possibleDisplayNameMatches[players[i]] = lowerDisplayName
end
end
end
end
local matchCount = 0
local lastMatch = nil
local lastMatchName = nil
for player, playerName in pairs(possibleUserNameMatches) do
matchCount = matchCount + 1
lastMatch = player
lastMatchName = playerName
if playerName == possiblePlayerName and whitespaceTrimmed then
return player
end
end
if ChatSettings.WhisperByDisplayName then
for player, playerName in pairs(possibleDisplayNameMatches) do
matchCount = matchCount + 1
lastMatch = player
lastMatchName = playerName
end
end
if matchCount == 1 then
if self:ShouldAutoCompleteNames() then
return lastMatch
elseif lastMatchName == possiblePlayerName then
return lastMatch
end
end
end
return nil
end
function whisperStateMethods:GetWhisperChanneNameColor()
if self.ChatSettings.WhisperChannelNameColor then
return self.ChatSettings.WhisperChannelNameColor
end
return Color3.fromRGB(102, 14, 102)
end
function whisperStateMethods:EnterWhisperState(player)
self.PlayerNameEntered = true
self.PlayerName = player.Name
self.PlayerDisplayName = player.DisplayName
self.MessageModeButton.Size = UDim2.new(0, 1000, 1, 0)
local messageModeString
if ChatSettings.PlayerDisplayNamesEnabled and ChatSettings.WhisperByDisplayName then
messageModeString = string.format("[To %s]", player.DisplayName)
else
messageModeString = string.format("[To %s]", player.Name)
end
if ChatLocalization.tryLocalize then
messageModeString = ChatLocalization:tryLocalize(messageModeString)
end
self.MessageModeButton.Text = messageModeString
self.MessageModeButton.TextColor3 = self:GetWhisperChanneNameColor()
local xSize = math.ceil(self.MessageModeButton.TextBounds.X)
self.MessageModeButton.Size = UDim2.new(0, xSize, 1, 0)
self.TextBox.Size = UDim2.new(1, -xSize, 1, 0)
self.TextBox.Position = UDim2.new(0, xSize, 0, 0)
self.TextBox.Text = " "
end
function whisperStateMethods:TextUpdated()
local newText = self.TextBox.Text
if not self.PlayerNameEntered then
local player = self:GetWhisperingPlayer(newText)
if player then
self:EnterWhisperState(player)
end
else
if newText == "" then
self.MessageModeButton.Text = ""
self.MessageModeButton.Size = UDim2.new(0, 0, 0, 0)
self.TextBox.Size = UDim2.new(1, 0, 1, 0)
self.TextBox.Position = UDim2.new(0, 0, 0, 0)
self.TextBox.Text = ""
---Implement this when setting cursor positon is a thing.
---self.TextBox.Text = self.OriginalText .. " " .. self.PlayerName
self.PlayerNameEntered = false
---Temporary until setting cursor position...
self.ChatBar:ResetCustomState()
self.ChatBar:CaptureFocus()
end
end
end
function whisperStateMethods:GetMessage()
if self.PlayerNameEntered then
return "/w " ..self.PlayerName.. " " ..self.TextBox.Text
end
return self.TextBox.Text
end
function whisperStateMethods:ProcessCompletedMessage()
return false
end
function whisperStateMethods:Destroy()
self.MessageModeConnection:disconnect()
self.Destroyed = true
end
function WhisperCustomState.new(ChatWindow, ChatBar, ChatSettings, player)
local obj = setmetatable({}, whisperStateMethods)
obj.Destroyed = false
obj.ChatWindow = ChatWindow
obj.ChatBar = ChatBar
obj.ChatSettings = ChatSettings
obj.TextBox = ChatBar:GetTextBox()
obj.MessageModeButton = ChatBar:GetMessageModeTextButton()
obj.OriginalWhisperText = ""
obj.PlayerNameEntered = false
obj.MessageModeConnection = obj.MessageModeButton.MouseButton1Click:connect(function()
local chatBarText = obj.TextBox.Text
if string.sub(chatBarText, 1, 1) == " " then
chatBarText = string.sub(chatBarText, 2)
end
obj.ChatBar:ResetCustomState()
obj.ChatBar:SetTextBoxText(chatBarText)
obj.ChatBar:CaptureFocus()
end)
if player then
obj:EnterWhisperState(player)
else
obj:TextUpdated()
end
return obj
end
function ProcessMessage(message, ChatWindow, ChatBar, ChatSettings)
if string.sub(message, 1, 3):lower() == "/w " or string.sub(message, 1, 9):lower() == "/whisper " then
return WhisperCustomState.new(ChatWindow, ChatBar, ChatSettings)
end
return nil
end
function CreateCustomState(player, ChatWindow, ChatBar, ChatSettings)
return WhisperCustomState.new(ChatWindow, ChatBar, ChatSettings, player)
end
return {
[util.KEY_COMMAND_PROCESSOR_TYPE] = util.IN_PROGRESS_MESSAGE_PROCESSOR,
[util.KEY_PROCESSOR_FUNCTION] = ProcessMessage,
CustomStateCreator = CreateCustomState,
}
@@ -0,0 +1,6 @@
{
"lint": {
"SameLineStatement": "fatal",
"LocalShadow": "fatal"
}
}
@@ -0,0 +1,120 @@
-- // FileName: DefaultChatMessage.lua
-- // Written by: TheGamer101
-- // Description: Create a message label for a standard chat message.
local clientChatModules = script.Parent.Parent
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants"))
local util = require(script.Parent:WaitForChild("Util"))
function CreateMessageLabel(messageData, channelName)
local fromSpeaker = messageData.FromSpeaker
local speakerName
if ChatSettings.PlayerDisplayNamesEnabled and messageData.SpeakerDisplayName then
speakerName = messageData.SpeakerDisplayName
else
speakerName = fromSpeaker
end
local message = messageData.Message
local extraData = messageData.ExtraData or {}
local useFont = extraData.Font or ChatSettings.DefaultFont
local useTextSize = extraData.TextSize or ChatSettings.ChatWindowTextSize
local useNameColor = extraData.NameColor or ChatSettings.DefaultNameColor
local useChatColor = extraData.ChatColor or ChatSettings.DefaultMessageColor
local useChannelColor = extraData.ChannelColor or useChatColor
local tags = extraData.Tags or {}
local formatUseName = string.format("[%s]:", speakerName)
local speakerNameSize = util:GetStringTextBounds(formatUseName, useFont, useTextSize)
local numNeededSpaces = util:GetNumberOfSpaces(formatUseName, useFont, useTextSize) + 1
local BaseFrame, BaseMessage = util:CreateBaseMessage("", useFont, useTextSize, useChatColor)
local NameButton = util:AddNameButtonToBaseMessage(BaseMessage, useNameColor, formatUseName, fromSpeaker)
local ChannelButton = nil
local guiObjectSpacing = UDim2.new(0, 0, 0, 0)
if channelName ~= messageData.OriginalChannel then
local formatChannelName = string.format("{%s}", messageData.OriginalChannel)
ChannelButton = util:AddChannelButtonToBaseMessage(BaseMessage, useChannelColor, formatChannelName, messageData.OriginalChannel)
guiObjectSpacing = UDim2.new(0, ChannelButton.Size.X.Offset + util:GetStringTextBounds(" ", useFont, useTextSize).X, 0, 0)
numNeededSpaces = numNeededSpaces + util:GetNumberOfSpaces(formatChannelName, useFont, useTextSize) + 1
end
local tagLabels = {}
for i, tag in pairs(tags) do
local tagColor = tag.TagColor or Color3.fromRGB(255, 0, 255)
local tagText = tag.TagText or "???"
local formatTagText = string.format("[%s] ", tagText)
local label = util:AddTagLabelToBaseMessage(BaseMessage, tagColor, formatTagText)
label.Position = guiObjectSpacing
numNeededSpaces = numNeededSpaces + util:GetNumberOfSpaces(formatTagText, useFont, useTextSize)
guiObjectSpacing = guiObjectSpacing + UDim2.new(0, label.Size.X.Offset, 0, 0)
table.insert(tagLabels, label)
end
NameButton.Position = guiObjectSpacing
local function UpdateTextFunction(messageObject)
if messageData.IsFiltered then
BaseMessage.Text = string.rep(" ", numNeededSpaces) .. messageObject.Message
else
BaseMessage.Text = string.rep(" ", numNeededSpaces) .. string.rep("_", messageObject.MessageLength)
end
end
UpdateTextFunction(messageData)
local function GetHeightFunction(xSize)
return util:GetMessageHeight(BaseMessage, BaseFrame, xSize)
end
local FadeParmaters = {}
FadeParmaters[NameButton] = {
TextTransparency = {FadedIn = 0, FadedOut = 1},
TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
}
FadeParmaters[BaseMessage] = {
TextTransparency = {FadedIn = 0, FadedOut = 1},
TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
}
for i, tagLabel in pairs(tagLabels) do
local index = string.format("Tag%d", i)
FadeParmaters[tagLabel] = {
TextTransparency = {FadedIn = 0, FadedOut = 1},
TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
}
end
if ChannelButton then
FadeParmaters[ChannelButton] = {
TextTransparency = {FadedIn = 0, FadedOut = 1},
TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
}
end
local FadeInFunction, FadeOutFunction, UpdateAnimFunction = util:CreateFadeFunctions(FadeParmaters)
return {
[util.KEY_BASE_FRAME] = BaseFrame,
[util.KEY_BASE_MESSAGE] = BaseMessage,
[util.KEY_UPDATE_TEXT_FUNC] = UpdateTextFunction,
[util.KEY_GET_HEIGHT] = GetHeightFunction,
[util.KEY_FADE_IN] = FadeInFunction,
[util.KEY_FADE_OUT] = FadeOutFunction,
[util.KEY_UPDATE_ANIMATION] = UpdateAnimFunction
}
end
return {
[util.KEY_MESSAGE_TYPE] = ChatConstants.MessageTypeDefault,
[util.KEY_CREATOR_FUNCTION] = CreateMessageLabel
}
@@ -0,0 +1,72 @@
-- // FileName: MeCommandMessage.lua
-- // Written by: TheGamer101
-- // Description: Create a message label for a me command message.
local clientChatModules = script.Parent.Parent
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants"))
local util = require(script.Parent:WaitForChild("Util"))
function CreateMeCommandMessageLabel(messageData, channelName)
local message = messageData.Message
local extraData = messageData.ExtraData or {}
local useFont = extraData.Font or Enum.Font.SourceSansItalic
local useTextSize = extraData.TextSize or ChatSettings.ChatWindowTextSize
local useChatColor = Color3.new(1, 1, 1)
local useChannelColor = extraData.ChannelColor or useChatColor
local numNeededSpaces = 0
local BaseFrame, BaseMessage = util:CreateBaseMessage("", useFont, useTextSize, useChatColor)
local ChannelButton = nil
if channelName ~= messageData.OriginalChannel then
local formatChannelName = string.format("{%s}", messageData.OriginalChannel)
ChannelButton = util:AddChannelButtonToBaseMessage(BaseMessage, useChannelColor, formatChannelName, messageData.OriginalChannel)
numNeededSpaces = util:GetNumberOfSpaces(formatChannelName, useFont, useTextSize) + 1
end
local function UpdateTextFunction(messageObject)
if messageData.IsFiltered then
BaseMessage.Text = string.rep(" ", numNeededSpaces) .. messageObject.FromSpeaker .. " " .. string.sub(messageObject.Message, 5)
else
local messageLength = string.len(messageObject.FromSpeaker) + messageObject.MessageLength - 4
BaseMessage.Text = string.rep(" ", numNeededSpaces) .. string.rep("_", messageLength)
end
end
UpdateTextFunction(messageData)
local function GetHeightFunction(xSize)
return util:GetMessageHeight(BaseMessage, BaseFrame, xSize)
end
local FadeParmaters = {}
FadeParmaters[BaseMessage] = {
TextTransparency = {FadedIn = 0, FadedOut = 1},
TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
}
if ChannelButton then
FadeParmaters[ChannelButton] = {
TextTransparency = {FadedIn = 0, FadedOut = 1},
TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
}
end
local FadeInFunction, FadeOutFunction, UpdateAnimFunction = util:CreateFadeFunctions(FadeParmaters)
return {
[util.KEY_BASE_FRAME] = BaseFrame,
[util.KEY_BASE_MESSAGE] = BaseMessage,
[util.KEY_UPDATE_TEXT_FUNC] = UpdateTextFunction,
[util.KEY_GET_HEIGHT] = GetHeightFunction,
[util.KEY_FADE_IN] = FadeInFunction,
[util.KEY_FADE_OUT] = FadeOutFunction,
[util.KEY_UPDATE_ANIMATION] = UpdateAnimFunction
}
end
return {
[util.KEY_MESSAGE_TYPE] = ChatConstants.MessageTypeMeCommand,
[util.KEY_CREATOR_FUNCTION] = CreateMeCommandMessageLabel
}
@@ -0,0 +1,45 @@
-- // FileName: SetCoreMessage.lua
-- // Written by: TheGamer101
-- // Description: Create a message label for a message created with SetCore(ChatMakeSystemMessage).
local clientChatModules = script.Parent.Parent
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants"))
local util = require(script.Parent:WaitForChild("Util"))
function CreateSetCoreMessageLabel(messageData, channelName)
local message = messageData.Message
local extraData = messageData.ExtraData or {}
local useFont = extraData.Font or ChatSettings.DefaultFont
local useTextSize = extraData.TextSize or ChatSettings.ChatWindowTextSize
local useColor = extraData.Color or ChatSettings.DefaultMessageColor
local BaseFrame, BaseMessage = util:CreateBaseMessage(message, useFont, useTextSize, useColor)
local function GetHeightFunction(xSize)
return util:GetMessageHeight(BaseMessage, BaseFrame, xSize)
end
local FadeParmaters = {}
FadeParmaters[BaseMessage] = {
TextTransparency = {FadedIn = 0, FadedOut = 1},
TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
}
local FadeInFunction, FadeOutFunction, UpdateAnimFunction = util:CreateFadeFunctions(FadeParmaters)
return {
[util.KEY_BASE_FRAME] = BaseFrame,
[util.KEY_BASE_MESSAGE] = BaseMessage,
[util.KEY_UPDATE_TEXT_FUNC] = nil,
[util.KEY_GET_HEIGHT] = GetHeightFunction,
[util.KEY_FADE_IN] = FadeInFunction,
[util.KEY_FADE_OUT] = FadeOutFunction,
[util.KEY_UPDATE_ANIMATION] = UpdateAnimFunction
}
end
return {
[util.KEY_MESSAGE_TYPE] = ChatConstants.MessageTypeSetCore,
[util.KEY_CREATOR_FUNCTION] = CreateSetCoreMessageLabel
}
@@ -0,0 +1,76 @@
-- // FileName: SystemMessage.lua
-- // Written by: TheGamer101
-- // Description: Create a message label for a system message.
local clientChatModules = script.Parent.Parent
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants"))
local util = require(script.Parent:WaitForChild("Util"))
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
function CreateSystemMessageLabel(messageData, channelName)
local message = messageData.Message
if ChatLocalization and ChatLocalization.LocalizeFormattedMessage then
message = ChatLocalization:LocalizeFormattedMessage(message)
end
local extraData = messageData.ExtraData or {}
local useFont = extraData.Font or ChatSettings.DefaultFont
local useTextSize = extraData.TextSize or ChatSettings.ChatWindowTextSize
local useChatColor = extraData.ChatColor or ChatSettings.DefaultMessageColor
local useChannelColor = extraData.ChannelColor or useChatColor
local BaseFrame, BaseMessage = util:CreateBaseMessage(message, useFont, useTextSize, useChatColor)
BaseMessage.AutoLocalize = true
local ChannelButton = nil
if channelName ~= messageData.OriginalChannel then
local formatChannelName
if ChatLocalization and messageData.OriginalChannel == "System" then
local localizedChannelName = ChatLocalization:Get("InGame.Chat.Label.SystemMessagePrefix", "System")
formatChannelName = string.format("{%s}", localizedChannelName)
elseif ChatLocalization and messageData.OriginalChannel == "Team" then
local localizedChannelName = ChatLocalization:Get("InGame.Chat.Label.TeamMessagePrefix", "System")
formatChannelName = string.format("{%s}", localizedChannelName)
else
formatChannelName = string.format("{%s}", messageData.OriginalChannel)
end
ChannelButton = util:AddChannelButtonToBaseMessage(BaseMessage, useChannelColor, formatChannelName, messageData.OriginalChannel)
local numNeededSpaces = util:GetNumberOfSpaces(formatChannelName, useFont, useTextSize) + 1
BaseMessage.Text = string.rep(" ", numNeededSpaces) .. message
end
local function GetHeightFunction(xSize)
return util:GetMessageHeight(BaseMessage, BaseFrame, xSize)
end
local FadeParmaters = {}
FadeParmaters[BaseMessage] = {
TextTransparency = {FadedIn = 0, FadedOut = 1},
TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
}
if ChannelButton then
FadeParmaters[ChannelButton] = {
TextTransparency = {FadedIn = 0, FadedOut = 1},
TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
}
end
local FadeInFunction, FadeOutFunction, UpdateAnimFunction = util:CreateFadeFunctions(FadeParmaters)
return {
[util.KEY_BASE_FRAME] = BaseFrame,
[util.KEY_BASE_MESSAGE] = BaseMessage,
[util.KEY_UPDATE_TEXT_FUNC] = nil,
[util.KEY_GET_HEIGHT] = GetHeightFunction,
[util.KEY_FADE_IN] = FadeInFunction,
[util.KEY_FADE_OUT] = FadeOutFunction,
[util.KEY_UPDATE_ANIMATION] = UpdateAnimFunction
}
end
return {
[util.KEY_MESSAGE_TYPE] = ChatConstants.MessageTypeSystem,
[util.KEY_CREATOR_FUNCTION] = CreateSystemMessageLabel
}
@@ -0,0 +1,19 @@
-- // FileName: UnknownMessage.lua
-- // Written by: TheGamer101
-- // Description: Default handler for message types with no other creator registered.
-- // Just print that there was a message with no creator for now.
local MESSAGE_TYPE = "UnknownMessage"
local clientChatModules = script.Parent.Parent
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local util = require(script.Parent:WaitForChild("Util"))
function CreateUnknownMessageLabel(messageData)
print("No message creator for message: " ..messageData.Message)
end
return {
[util.KEY_MESSAGE_TYPE] = MESSAGE_TYPE,
[util.KEY_CREATOR_FUNCTION] = CreateUnknownMessageLabel
}
@@ -0,0 +1,375 @@
-- // FileName: Util.lua
-- // Written by: Xsitsu, TheGamer101
-- // Description: Module for shared code between MessageCreatorModules.
--[[
Creating a message creator module:
1) Create a new module inside the MessageCreatorModules folder.
2) Create a function that takes a messageData object and returns:
{
KEY_BASE_FRAME = BaseFrame,
KEY_BASE_MESSAGE = BaseMessage,
KEY_UPDATE_TEXT_FUNC = function(newMessageObject) ---Function to update the text of the message.
KEY_GET_HEIGHT = function() ---Function to get the height of the message in absolute pixels,
KEY_FADE_IN = function(duration, CurveUtil) ---Function to tell the message to start fading in.
KEY_FADE_OUT = function(duration, CurveUtil) ---Function to tell the message to start fading out.
KEY_UPDATE_ANIMATION = function(dtScale, CurveUtil) ---Update animation function.
}
3) return the following format from the module:
{
KEY_MESSAGE_TYPE = "Message type this module creates messages for."
KEY_CREATOR_FUNCTION = YourFunctionHere
}
--]]
local DEFAULT_MESSAGE_CREATOR = "UnknownMessage"
local MESSAGE_CREATOR_MODULES_VERSION = 1
---Creator Module Object Keys
local KEY_MESSAGE_TYPE = "MessageType"
local KEY_CREATOR_FUNCTION = "MessageCreatorFunc"
---Creator function return object keys
local KEY_BASE_FRAME = "BaseFrame"
local KEY_BASE_MESSAGE = "BaseMessage"
local KEY_UPDATE_TEXT_FUNC = "UpdateTextFunction"
local KEY_GET_HEIGHT = "GetHeightFunction"
local KEY_FADE_IN = "FadeInFunction"
local KEY_FADE_OUT = "FadeOutFunction"
local KEY_UPDATE_ANIMATION = "UpdateAnimFunction"
local TextService = game:GetService("TextService")
local Players = game:GetService("Players")
local LocalPlayer = Players.LocalPlayer
while not LocalPlayer do
Players.ChildAdded:wait()
LocalPlayer = Players.LocalPlayer
end
local clientChatModules = script.Parent.Parent
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants"))
local module = {}
local methods = {}
methods.__index = methods
function methods:GetStringTextBounds(text, font, textSize, sizeBounds)
sizeBounds = sizeBounds or Vector2.new(10000, 10000)
return TextService:GetTextSize(text, textSize, font, sizeBounds)
end
--// Above was taken directly from Util.GetStringTextBounds() in the old chat corescripts.
function methods:GetMessageHeight(BaseMessage, BaseFrame, xSize)
xSize = xSize or BaseFrame.AbsoluteSize.X
local textBoundsSize = self:GetStringTextBounds(BaseMessage.Text, BaseMessage.Font, BaseMessage.TextSize, Vector2.new(xSize, 1000))
if textBoundsSize.Y ~= math.floor(textBoundsSize.Y) then
-- HACK Alert. TODO: Remove this when we switch UDim2 to use float Offsets
-- This is nessary due to rounding issues on mobile devices when translating between screen pixels and native pixels
return textBoundsSize.Y + 1
end
return textBoundsSize.Y
end
function methods:GetNumberOfSpaces(str, font, textSize)
local strSize = self:GetStringTextBounds(str, font, textSize)
local singleSpaceSize = self:GetStringTextBounds(" ", font, textSize)
return math.ceil(strSize.X / singleSpaceSize.X)
end
function methods:CreateBaseMessage(message, font, textSize, chatColor)
local BaseFrame = self:GetFromObjectPool("Frame")
BaseFrame.Selectable = false
BaseFrame.Size = UDim2.new(1, 0, 0, 18)
BaseFrame.Visible = true
BaseFrame.BackgroundTransparency = 1
local messageBorder = 8
local BaseMessage = self:GetFromObjectPool("TextLabel")
BaseMessage.Selectable = false
BaseMessage.Size = UDim2.new(1, -(messageBorder + 6), 1, 0)
BaseMessage.Position = UDim2.new(0, messageBorder, 0, 0)
BaseMessage.BackgroundTransparency = 1
BaseMessage.Font = font
BaseMessage.TextSize = textSize
BaseMessage.TextXAlignment = Enum.TextXAlignment.Left
BaseMessage.TextYAlignment = Enum.TextYAlignment.Top
BaseMessage.TextTransparency = 0
BaseMessage.TextStrokeTransparency = 0.75
BaseMessage.TextColor3 = chatColor
BaseMessage.TextWrapped = true
BaseMessage.ClipsDescendants = true
BaseMessage.Text = message
BaseMessage.Visible = true
BaseMessage.Parent = BaseFrame
return BaseFrame, BaseMessage
end
function methods:AddNameButtonToBaseMessage(BaseMessage, nameColor, formatName, playerName)
local speakerNameSize = self:GetStringTextBounds(formatName, BaseMessage.Font, BaseMessage.TextSize)
local NameButton = self:GetFromObjectPool("TextButton")
NameButton.Selectable = false
NameButton.Size = UDim2.new(0, speakerNameSize.X, 0, speakerNameSize.Y)
NameButton.Position = UDim2.new(0, 0, 0, 0)
NameButton.BackgroundTransparency = 1
NameButton.Font = BaseMessage.Font
NameButton.TextSize = BaseMessage.TextSize
NameButton.TextXAlignment = BaseMessage.TextXAlignment
NameButton.TextYAlignment = BaseMessage.TextYAlignment
NameButton.TextTransparency = BaseMessage.TextTransparency
NameButton.TextStrokeTransparency = BaseMessage.TextStrokeTransparency
NameButton.TextColor3 = nameColor
NameButton.Text = formatName
NameButton.Visible = true
NameButton.Parent = BaseMessage
local clickedConn = NameButton.MouseButton1Click:connect(function()
self:NameButtonClicked(NameButton, playerName)
end)
local changedConn = nil
changedConn = NameButton.Changed:connect(function(prop)
if prop == "Parent" then
clickedConn:Disconnect()
changedConn:Disconnect()
end
end)
return NameButton
end
function methods:AddChannelButtonToBaseMessage(BaseMessage, channelColor, formatChannelName, channelName)
local channelNameSize = self:GetStringTextBounds(formatChannelName, BaseMessage.Font, BaseMessage.TextSize)
local ChannelButton = self:GetFromObjectPool("TextButton")
ChannelButton.Selectable = false
ChannelButton.Size = UDim2.new(0, channelNameSize.X, 0, channelNameSize.Y)
ChannelButton.Position = UDim2.new(0, 0, 0, 0)
ChannelButton.BackgroundTransparency = 1
ChannelButton.Font = BaseMessage.Font
ChannelButton.TextSize = BaseMessage.TextSize
ChannelButton.TextXAlignment = BaseMessage.TextXAlignment
ChannelButton.TextYAlignment = BaseMessage.TextYAlignment
ChannelButton.TextTransparency = BaseMessage.TextTransparency
ChannelButton.TextStrokeTransparency = BaseMessage.TextStrokeTransparency
ChannelButton.TextColor3 = channelColor
ChannelButton.Text = formatChannelName
ChannelButton.Visible = true
ChannelButton.Parent = BaseMessage
local clickedConn = ChannelButton.MouseButton1Click:connect(function()
self:ChannelButtonClicked(ChannelButton, channelName)
end)
local changedConn = nil
changedConn = ChannelButton.Changed:connect(function(prop)
if prop == "Parent" then
clickedConn:Disconnect()
changedConn:Disconnect()
end
end)
return ChannelButton
end
function methods:AddTagLabelToBaseMessage(BaseMessage, tagColor, formatTagText)
local tagNameSize = self:GetStringTextBounds(formatTagText, BaseMessage.Font, BaseMessage.TextSize)
local TagLabel = self:GetFromObjectPool("TextLabel")
TagLabel.Selectable = false
TagLabel.Size = UDim2.new(0, tagNameSize.X, 0, tagNameSize.Y)
TagLabel.Position = UDim2.new(0, 0, 0, 0)
TagLabel.BackgroundTransparency = 1
TagLabel.Font = BaseMessage.Font
TagLabel.TextSize = BaseMessage.TextSize
TagLabel.TextXAlignment = BaseMessage.TextXAlignment
TagLabel.TextYAlignment = BaseMessage.TextYAlignment
TagLabel.TextTransparency = BaseMessage.TextTransparency
TagLabel.TextStrokeTransparency = BaseMessage.TextStrokeTransparency
TagLabel.TextColor3 = tagColor
TagLabel.Text = formatTagText
TagLabel.Visible = true
TagLabel.Parent = BaseMessage
return TagLabel
end
function GetWhisperChannelPrefix()
if ChatConstants.WhisperChannelPrefix then
return ChatConstants.WhisperChannelPrefix
end
return "To "
end
function methods:NameButtonClicked(nameButton, playerName)
if not self.ChatWindow then
return
end
if ChatSettings.ClickOnPlayerNameToWhisper then
local player = Players:FindFirstChild(playerName)
if player and player ~= LocalPlayer then
local whisperChannel = GetWhisperChannelPrefix() ..playerName
if self.ChatWindow:GetChannel(whisperChannel) then
self.ChatBar:ResetCustomState()
local targetChannelName = self.ChatWindow:GetTargetMessageChannel()
if targetChannelName ~= whisperChannel then
self.ChatWindow:SwitchCurrentChannel(whisperChannel)
end
local whisperMessage = "/w " ..playerName
self.ChatBar:SetText(whisperMessage)
self.ChatBar:CaptureFocus()
elseif not self.ChatBar:IsInCustomState() then
local whisperMessage = "/w " ..playerName
self.ChatBar:SetText(whisperMessage)
self.ChatBar:CaptureFocus()
end
end
end
end
function methods:ChannelButtonClicked(channelButton, channelName)
if not self.ChatWindow then
return
end
if ChatSettings.ClickOnChannelNameToSetMainChannel then
if self.ChatWindow:GetChannel(channelName) then
self.ChatBar:ResetCustomState()
local targetChannelName = self.ChatWindow:GetTargetMessageChannel()
if targetChannelName ~= channelName then
self.ChatWindow:SwitchCurrentChannel(channelName)
end
self.ChatBar:ResetText()
self.ChatBar:CaptureFocus()
end
end
end
function methods:RegisterChatWindow(chatWindow)
self.ChatWindow = chatWindow
self.ChatBar = chatWindow:GetChatBar()
end
function methods:GetFromObjectPool(className)
if self.ObjectPool == nil then
return Instance.new(className)
end
return self.ObjectPool:GetInstance(className)
end
function methods:RegisterObjectPool(objectPool)
self.ObjectPool = objectPool
end
-- CreateFadeFunctions usage:
-- fadeObjects is a map of text labels and button to start and end values for a given property.
-- e.g {
-- NameButton = {
-- TextTransparency = {
-- FadedIn = 0.5,
-- FadedOut = 1,
-- }
-- },
-- ImageOne = {
-- ImageTransparency = {
-- FadedIn = 0,
-- FadedOut = 0.5,
-- }
-- }
-- }
function methods:CreateFadeFunctions(fadeObjects)
local AnimParams = {}
for object, properties in pairs(fadeObjects) do
AnimParams[object] = {}
for property, values in pairs(properties) do
AnimParams[object][property] = {
Target = values.FadedIn,
Current = object[property],
NormalizedExptValue = 1,
}
end
end
local function FadeInFunction(duration, CurveUtil)
for object, properties in pairs(AnimParams) do
for property, values in pairs(properties) do
values.Target = fadeObjects[object][property].FadedIn
values.NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
end
end
end
local function FadeOutFunction(duration, CurveUtil)
for object, properties in pairs(AnimParams) do
for property, values in pairs(properties) do
values.Target = fadeObjects[object][property].FadedOut
values.NormalizedExptValue = CurveUtil:NormalizedDefaultExptValueInSeconds(duration)
end
end
end
local function AnimGuiObjects()
for object, properties in pairs(AnimParams) do
for property, values in pairs(properties) do
object[property] = values.Current
end
end
end
local function UpdateAnimFunction(dtScale, CurveUtil)
for object, properties in pairs(AnimParams) do
for property, values in pairs(properties) do
values.Current = CurveUtil:Expt(
values.Current,
values.Target,
values.NormalizedExptValue,
dtScale
)
end
end
AnimGuiObjects()
end
return FadeInFunction, FadeOutFunction, UpdateAnimFunction
end
function methods:NewBindableEvent(name)
local bindable = Instance.new("BindableEvent")
bindable.Name = name
return bindable
end
--- DEPRECATED METHODS:
function methods:RegisterGuiRoot()
-- This is left here for compatibility with ChatScript versions lower than 0.5
end
--- End of Deprecated methods.
function module.new()
local obj = setmetatable({}, methods)
obj.ObjectPool = nil
obj.ChatWindow = nil
obj.DEFAULT_MESSAGE_CREATOR = DEFAULT_MESSAGE_CREATOR
obj.MESSAGE_CREATOR_MODULES_VERSION = MESSAGE_CREATOR_MODULES_VERSION
obj.KEY_MESSAGE_TYPE = KEY_MESSAGE_TYPE
obj.KEY_CREATOR_FUNCTION = KEY_CREATOR_FUNCTION
obj.KEY_BASE_FRAME = KEY_BASE_FRAME
obj.KEY_BASE_MESSAGE = KEY_BASE_MESSAGE
obj.KEY_UPDATE_TEXT_FUNC = KEY_UPDATE_TEXT_FUNC
obj.KEY_GET_HEIGHT = KEY_GET_HEIGHT
obj.KEY_FADE_IN = KEY_FADE_IN
obj.KEY_FADE_OUT = KEY_FADE_OUT
obj.KEY_UPDATE_ANIMATION = KEY_UPDATE_ANIMATION
return obj
end
return module.new()
@@ -0,0 +1,71 @@
--!nocheck
-- // FileName: WelcomeMessage.lua
-- // Written by: TheGamer101
-- // Description: Create a message label for a welcome message.
local clientChatModules = script.Parent.Parent
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants"))
local util = require(script.Parent:WaitForChild("Util"))
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
function CreateWelcomeMessageLabel(messageData, channelName)
local message = messageData.Message
if ChatLocalization and ChatLocalization.LocalizeFormattedMessage then
message = ChatLocalization:LocalizeFormattedMessage(message)
end
local extraData = messageData.ExtraData or {}
local useFont = extraData.Font or ChatSettings.DefaultFont
local useTextSize = extraData.FontSize or ChatSettings.ChatWindowTextSize
local useChatColor = extraData.ChatColor or ChatSettings.DefaultMessageColor
local useChannelColor = extraData.ChannelColor or useChatColor
local BaseFrame, BaseMessage = util:CreateBaseMessage(message, useFont, useTextSize, useChatColor)
local ChannelButton = nil
if channelName ~= messageData.OriginalChannel then
local localizedChannelName = messageData.OriginalChannel
if ChatLocalization.tryLocalize then
localizedChannelName = ChatLocalization:tryLocalize(messageData.OriginalChannel)
end
local formatChannelName = string.format("{%s}", localizedChannelName)
ChannelButton = util:AddChannelButtonToBaseMessage(BaseMessage, useChannelColor, formatChannelName, messageData.OriginalChannel)
local numNeededSpaces = util:GetNumberOfSpaces(formatChannelName, useFont, useTextSize) + 1
BaseMessage.Text = string.rep(" ", numNeededSpaces) .. message
end
local function GetHeightFunction(xSize)
return util:GetMessageHeight(BaseMessage, BaseFrame, xSize)
end
local FadeParmaters = {}
FadeParmaters[BaseMessage] = {
TextTransparency = {FadedIn = 0, FadedOut = 1},
TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
}
if ChannelButton then
FadeParmaters[ChannelButton] = {
TextTransparency = {FadedIn = 0, FadedOut = 1},
TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
}
end
local FadeInFunction, FadeOutFunction, UpdateAnimFunction = util:CreateFadeFunctions(FadeParmaters)
return {
[util.KEY_BASE_FRAME] = BaseFrame,
[util.KEY_BASE_MESSAGE] = BaseMessage,
[util.KEY_UPDATE_TEXT_FUNC] = nil,
[util.KEY_GET_HEIGHT] = GetHeightFunction,
[util.KEY_FADE_IN] = FadeInFunction,
[util.KEY_FADE_OUT] = FadeOutFunction,
[util.KEY_UPDATE_ANIMATION] = UpdateAnimFunction
}
end
return {
[util.KEY_MESSAGE_TYPE] = ChatConstants.MessageTypeWelcome,
[util.KEY_CREATOR_FUNCTION] = CreateWelcomeMessageLabel
}
@@ -0,0 +1,112 @@
-- // FileName: WhisperMessage.lua
-- // Written by: TheGamer101
-- // Description: Create a message label for a whisper chat message.
local PlayersService = game:GetService("Players")
local LocalPlayer = PlayersService.LocalPlayer
while not LocalPlayer do
PlayersService.ChildAdded:wait()
LocalPlayer = PlayersService.LocalPlayer
end
local clientChatModules = script.Parent.Parent
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants"))
local util = require(script.Parent:WaitForChild("Util"))
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
function CreateMessageLabel(messageData, channelName)
local fromSpeaker = messageData.FromSpeaker
local speakerDisplayName = messageData.SpeakerDisplayName
local message = messageData.Message
local extraData = messageData.ExtraData or {}
local useFont = extraData.Font or ChatSettings.DefaultFont
local useTextSize = extraData.TextSize or ChatSettings.ChatWindowTextSize
local useNameColor = extraData.NameColor or ChatSettings.DefaultNameColor
local useChatColor = extraData.ChatColor or ChatSettings.DefaultMessageColor
local useChannelColor = extraData.ChannelColor or useChatColor
local formatUseName
if ChatSettings.PlayerDisplayNamesEnabled and messageData.SpeakerDisplayName then
formatUseName = string.format("[%s]:", speakerDisplayName)
else
formatUseName = string.format("[%s]:", fromSpeaker)
end
local speakerNameSize = util:GetStringTextBounds(formatUseName, useFont, useTextSize)
local numNeededSpaces = util:GetNumberOfSpaces(formatUseName, useFont, useTextSize) + 1
local BaseFrame, BaseMessage = util:CreateBaseMessage("", useFont, useTextSize, useChatColor)
local NameButton = util:AddNameButtonToBaseMessage(BaseMessage, useNameColor, formatUseName, fromSpeaker)
local ChannelButton = nil
if channelName ~= messageData.OriginalChannel then
local whisperString = messageData.OriginalChannel
if messageData.FromSpeaker ~= LocalPlayer.Name then
whisperString = string.format("From %s", messageData.FromSpeaker)
end
if ChatLocalization.tryLocalize then
whisperString = ChatLocalization:tryLocalize (whisperString)
end
local formatChannelName = string.format("{%s}", whisperString)
ChannelButton = util:AddChannelButtonToBaseMessage(BaseMessage, useChannelColor, formatChannelName, messageData.OriginalChannel)
NameButton.Position = UDim2.new(0, ChannelButton.Size.X.Offset + util:GetStringTextBounds(" ", useFont, useTextSize).X, 0, 0)
numNeededSpaces = numNeededSpaces + util:GetNumberOfSpaces(formatChannelName, useFont, useTextSize) + 1
end
local function UpdateTextFunction(messageObject)
if messageData.IsFiltered then
BaseMessage.Text = string.rep(" ", numNeededSpaces) .. messageObject.Message
else
BaseMessage.Text = string.rep(" ", numNeededSpaces) .. string.rep("_", messageObject.MessageLength)
end
end
UpdateTextFunction(messageData)
local function GetHeightFunction(xSize)
return util:GetMessageHeight(BaseMessage, BaseFrame, xSize)
end
local FadeParmaters = {}
FadeParmaters[NameButton] = {
TextTransparency = {FadedIn = 0, FadedOut = 1},
TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
}
FadeParmaters[BaseMessage] = {
TextTransparency = {FadedIn = 0, FadedOut = 1},
TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
}
if ChannelButton then
FadeParmaters[ChannelButton] = {
TextTransparency = {FadedIn = 0, FadedOut = 1},
TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
}
end
local FadeInFunction, FadeOutFunction, UpdateAnimFunction = util:CreateFadeFunctions(FadeParmaters)
return {
[util.KEY_BASE_FRAME] = BaseFrame,
[util.KEY_BASE_MESSAGE] = BaseMessage,
[util.KEY_UPDATE_TEXT_FUNC] = UpdateTextFunction,
[util.KEY_GET_HEIGHT] = GetHeightFunction,
[util.KEY_FADE_IN] = FadeInFunction,
[util.KEY_FADE_OUT] = FadeOutFunction,
[util.KEY_UPDATE_ANIMATION] = UpdateAnimFunction
}
end
return {
[util.KEY_MESSAGE_TYPE] = ChatConstants.MessageTypeWhisper,
[util.KEY_CREATOR_FUNCTION] = CreateMessageLabel
}
@@ -0,0 +1,133 @@
-- // FileName: MessageLabelCreator.lua
-- // Written by: Xsitsu
-- // Description: Module to handle taking text and creating stylized GUI objects for display in ChatWindow.
local OBJECT_POOL_SIZE = 50
local module = {}
--////////////////////////////// Include
--//////////////////////////////////////
local Chat = game:GetService("Chat")
local clientChatModules = Chat:WaitForChild("ClientChatModules")
local messageCreatorModules = clientChatModules:WaitForChild("MessageCreatorModules")
local messageCreatorUtil = require(messageCreatorModules:WaitForChild("Util"))
local modulesFolder = script.Parent
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local moduleObjectPool = require(modulesFolder:WaitForChild("ObjectPool"))
local MessageSender = require(modulesFolder:WaitForChild("MessageSender"))
--////////////////////////////// Methods
--//////////////////////////////////////
local methods = {}
methods.__index = methods
-- merge properties on both table to target
function mergeProps(source, target)
if not source then return end
for prop, value in pairs(source) do
target[prop] = value
end
end
function ReturnToObjectPoolRecursive(instance, objectPool)
local children = instance:GetChildren()
for i = 1, #children do
ReturnToObjectPoolRecursive(children[i], objectPool)
end
instance.Parent = nil
objectPool:ReturnInstance(instance)
end
function GetMessageCreators()
local typeToFunction = {}
local creators = messageCreatorModules:GetChildren()
for i = 1, #creators do
if creators[i]:IsA("ModuleScript") then
if creators[i].Name ~= "Util" then
local creator = require(creators[i])
typeToFunction[creator[messageCreatorUtil.KEY_MESSAGE_TYPE]] = creator[messageCreatorUtil.KEY_CREATOR_FUNCTION]
end
end
end
return typeToFunction
end
function methods:WrapIntoMessageObject(messageData, createdMessageObject)
local BaseFrame = createdMessageObject[messageCreatorUtil.KEY_BASE_FRAME]
local BaseMessage = nil
if messageCreatorUtil.KEY_BASE_MESSAGE then
BaseMessage = createdMessageObject[messageCreatorUtil.KEY_BASE_MESSAGE]
end
local UpdateTextFunction = createdMessageObject[messageCreatorUtil.KEY_UPDATE_TEXT_FUNC]
local GetHeightFunction = createdMessageObject[messageCreatorUtil.KEY_GET_HEIGHT]
local FadeInFunction = createdMessageObject[messageCreatorUtil.KEY_FADE_IN]
local FadeOutFunction = createdMessageObject[messageCreatorUtil.KEY_FADE_OUT]
local UpdateAnimFunction = createdMessageObject[messageCreatorUtil.KEY_UPDATE_ANIMATION]
local obj = {}
obj.ID = messageData.ID
obj.BaseFrame = BaseFrame
obj.BaseMessage = BaseMessage
obj.UpdateTextFunction = UpdateTextFunction or function() warn("NO MESSAGE RESIZE FUNCTION") end
obj.GetHeightFunction = GetHeightFunction
obj.FadeInFunction = FadeInFunction
obj.FadeOutFunction = FadeOutFunction
obj.UpdateAnimFunction = UpdateAnimFunction
obj.ObjectPool = self.ObjectPool
obj.Destroyed = false
function obj:Destroy()
ReturnToObjectPoolRecursive(self.BaseFrame, self.ObjectPool)
self.Destroyed = true
end
return obj
end
function methods:CreateMessageLabel(messageData, currentChannelName)
messageData.Channel = currentChannelName
local extraDeveloperFormatTable
pcall(function()
extraDeveloperFormatTable = Chat:InvokeChatCallback(Enum.ChatCallbackType.OnClientFormattingMessage, messageData)
end)
messageData.ExtraData = messageData.ExtraData or {}
mergeProps(extraDeveloperFormatTable, messageData.ExtraData)
local messageType = messageData.MessageType
if self.MessageCreators[messageType] then
local createdMessageObject = self.MessageCreators[messageType](messageData, currentChannelName)
if createdMessageObject then
return self:WrapIntoMessageObject(messageData, createdMessageObject)
end
elseif self.DefaultCreatorType then
local createdMessageObject = self.MessageCreators[self.DefaultCreatorType](messageData, currentChannelName)
if createdMessageObject then
return self:WrapIntoMessageObject(messageData, createdMessageObject)
end
else
error("No message creator available for message type: " ..messageType)
end
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new()
local obj = setmetatable({}, methods)
obj.ObjectPool = moduleObjectPool.new(OBJECT_POOL_SIZE)
obj.MessageCreators = GetMessageCreators()
obj.DefaultCreatorType = messageCreatorUtil.DEFAULT_MESSAGE_CREATOR
messageCreatorUtil:RegisterObjectPool(obj.ObjectPool)
return obj
end
function module:GetStringTextBounds(text, font, textSize, sizeBounds)
return messageCreatorUtil:GetStringTextBounds(text, font, textSize, sizeBounds)
end
return module
@@ -0,0 +1,277 @@
-- // FileName: MessageLogDisplay.lua
-- // Written by: Xsitsu, TheGamer101
-- // Description: ChatChannel window for displaying messages.
local module = {}
module.ScrollBarThickness = 4
--////////////////////////////// Include
--//////////////////////////////////////
local Chat = game:GetService("Chat")
local clientChatModules = Chat:WaitForChild("ClientChatModules")
local modulesFolder = script.Parent
local moduleMessageLabelCreator = require(modulesFolder:WaitForChild("MessageLabelCreator"))
local CurveUtil = require(modulesFolder:WaitForChild("CurveUtil"))
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local MessageLabelCreator = moduleMessageLabelCreator.new()
--////////////////////////////// Methods
--//////////////////////////////////////
local methods = {}
methods.__index = methods
local function CreateGuiObjects()
local BaseFrame = Instance.new("Frame")
BaseFrame.Selectable = false
BaseFrame.Size = UDim2.new(1, 0, 1, 0)
BaseFrame.BackgroundTransparency = 1
local Scroller = Instance.new("ScrollingFrame")
Scroller.Selectable = ChatSettings.GamepadNavigationEnabled
Scroller.Name = "Scroller"
Scroller.BackgroundTransparency = 1
Scroller.BorderSizePixel = 0
Scroller.Position = UDim2.new(0, 0, 0, 3)
Scroller.Size = UDim2.new(1, -4, 1, -6)
Scroller.CanvasSize = UDim2.new(0, 0, 0, 0)
Scroller.ScrollBarThickness = module.ScrollBarThickness
Scroller.Active = true
Scroller.Parent = BaseFrame
local Layout = Instance.new("UIListLayout")
Layout.SortOrder = Enum.SortOrder.LayoutOrder
Layout.Parent = Scroller
return BaseFrame, Scroller, Layout
end
function methods:Destroy()
self.GuiObject:Destroy()
self.Destroyed = true
end
function methods:SetActive(active)
self.GuiObject.Visible = active
end
function methods:UpdateMessageFiltered(messageData)
local messageObject = nil
local searchIndex = 1
local searchTable = self.MessageObjectLog
while (#searchTable >= searchIndex) do
local obj = searchTable[searchIndex]
if obj.ID == messageData.ID then
messageObject = obj
break
end
searchIndex = searchIndex + 1
end
if messageObject then
messageObject.UpdateTextFunction(messageData)
self:PositionMessageLabelInWindow(messageObject, searchIndex)
end
end
function methods:AddMessage(messageData)
self:WaitUntilParentedCorrectly()
local messageObject = MessageLabelCreator:CreateMessageLabel(messageData, self.CurrentChannelName)
if messageObject == nil then
return
end
table.insert(self.MessageObjectLog, messageObject)
self:PositionMessageLabelInWindow(messageObject, #self.MessageObjectLog)
end
function methods:AddMessageAtIndex(messageData, index)
local messageObject = MessageLabelCreator:CreateMessageLabel(messageData, self.CurrentChannelName)
if messageObject == nil then
return
end
table.insert(self.MessageObjectLog, index, messageObject)
self:PositionMessageLabelInWindow(messageObject, index)
end
function methods:RemoveLastMessage()
self:WaitUntilParentedCorrectly()
local lastMessage = self.MessageObjectLog[1]
lastMessage:Destroy()
table.remove(self.MessageObjectLog, 1)
end
function methods:IsScrolledDown()
local yCanvasSize = self.Scroller.CanvasSize.Y.Offset
local yContainerSize = self.Scroller.AbsoluteWindowSize.Y
local yScrolledPosition = self.Scroller.CanvasPosition.Y
return (yCanvasSize < yContainerSize or
yCanvasSize - yScrolledPosition <= yContainerSize + 5)
end
function methods:UpdateMessageTextHeight(messageObject)
local baseFrame = messageObject.BaseFrame
for i = 1, 10 do
if messageObject.BaseMessage.TextFits then
break
end
local trySize = self.Scroller.AbsoluteSize.X - i
baseFrame.Size = UDim2.new(1, 0, 0, messageObject.GetHeightFunction(trySize))
end
end
function methods:PositionMessageLabelInWindow(messageObject, index)
self:WaitUntilParentedCorrectly()
local wasScrolledDown = self:IsScrolledDown()
local baseFrame = messageObject.BaseFrame
local layoutOrder = 1
if self.MessageObjectLog[index - 1] then
if index == #self.MessageObjectLog then
layoutOrder = self.MessageObjectLog[index - 1].BaseFrame.LayoutOrder + 1
else
layoutOrder = self.MessageObjectLog[index - 1].BaseFrame.LayoutOrder
end
end
baseFrame.LayoutOrder = layoutOrder
baseFrame.Size = UDim2.new(1, 0, 0, messageObject.GetHeightFunction(self.Scroller.AbsoluteSize.X))
baseFrame.Parent = self.Scroller
if messageObject.BaseMessage then
self:UpdateMessageTextHeight(messageObject)
end
if wasScrolledDown then
self.Scroller.CanvasPosition = Vector2.new(
0, math.max(0, self.Scroller.CanvasSize.Y.Offset - self.Scroller.AbsoluteSize.Y))
end
end
function methods:ReorderAllMessages()
self:WaitUntilParentedCorrectly()
--// Reordering / reparenting with a size less than 1 causes weird glitches to happen
-- with scrolling as repositioning happens.
if self.GuiObject.AbsoluteSize.Y < 1 then return end
local oldCanvasPositon = self.Scroller.CanvasPosition
local wasScrolledDown = self:IsScrolledDown()
for _, messageObject in pairs(self.MessageObjectLog) do
self:UpdateMessageTextHeight(messageObject)
end
if not wasScrolledDown then
self.Scroller.CanvasPosition = oldCanvasPositon
else
self.Scroller.CanvasPosition = Vector2.new(
0, math.max(0, self.Scroller.CanvasSize.Y.Offset - self.Scroller.AbsoluteSize.Y))
end
end
function methods:Clear()
for _, v in pairs(self.MessageObjectLog) do
v:Destroy()
end
self.MessageObjectLog = {}
end
function methods:SetCurrentChannelName(name)
self.CurrentChannelName = name
end
function methods:FadeOutBackground(duration)
--// Do nothing
end
function methods:FadeInBackground(duration)
--// Do nothing
end
function methods:FadeOutText(duration)
for i = 1, #self.MessageObjectLog do
if self.MessageObjectLog[i].FadeOutFunction then
self.MessageObjectLog[i].FadeOutFunction(duration, CurveUtil)
end
end
end
function methods:FadeInText(duration)
for i = 1, #self.MessageObjectLog do
if self.MessageObjectLog[i].FadeInFunction then
self.MessageObjectLog[i].FadeInFunction(duration, CurveUtil)
end
end
end
function methods:Update(dtScale)
for i = 1, #self.MessageObjectLog do
if self.MessageObjectLog[i].UpdateAnimFunction then
self.MessageObjectLog[i].UpdateAnimFunction(dtScale, CurveUtil)
end
end
end
--// ToDo: Move to common modules
function methods:WaitUntilParentedCorrectly()
while (not self.GuiObject:IsDescendantOf(game:GetService("Players").LocalPlayer)) do
self.GuiObject.AncestryChanged:wait()
end
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new()
local obj = setmetatable({}, methods)
obj.Destroyed = false
local BaseFrame, Scroller, Layout = CreateGuiObjects()
obj.GuiObject = BaseFrame
obj.Scroller = Scroller
obj.Layout = Layout
obj.MessageObjectLog = {}
obj.Name = "MessageLogDisplay"
obj.GuiObject.Name = "Frame_" .. obj.Name
obj.CurrentChannelName = ""
obj.GuiObject:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
spawn(function() obj:ReorderAllMessages() end)
end)
local wasScrolledDown = true
obj.Layout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
local size = obj.Layout.AbsoluteContentSize
obj.Scroller.CanvasSize = UDim2.new(0, 0, 0, size.Y)
if wasScrolledDown then
local windowSize = obj.Scroller.AbsoluteWindowSize
obj.Scroller.CanvasPosition = Vector2.new(0, size.Y - windowSize.Y)
end
end)
obj.Scroller:GetPropertyChangedSignal("CanvasPosition"):Connect(function()
wasScrolledDown = obj:IsScrolledDown()
end)
return obj
end
return module
@@ -0,0 +1,33 @@
-- // FileName: MessageSender.lua
-- // Written by: Xsitsu
-- // Description: Module to centralize sending message functionality.
local module = {}
--////////////////////////////// Include
--//////////////////////////////////////
local modulesFolder = script.Parent
--////////////////////////////// Methods
--//////////////////////////////////////
local methods = {}
methods.__index = methods
function methods:SendMessage(message, toChannel)
self.SayMessageRequest:FireServer(message, toChannel)
end
function methods:RegisterSayMessageFunction(func)
self.SayMessageRequest = func
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new()
local obj = setmetatable({}, methods)
obj.SayMessageRequest = nil
return obj
end
return module.new()
@@ -0,0 +1,80 @@
## Documentation
### ChatWindow
This is the main client side chat object.
### Methods
AddChannel(string channelName)
RemoveChannel(string channelName)
ChatChannel GetChannel(string channelName)
ChatChannel GetCurrentChannel()
SwitchCurrentChannel(string channelName)
bool GetVisible()
SetVisible(bool visible)
EnableResizable()
DisableResizable()
FadeOutBackground(float duration)
FadeInBackground(float duration)
FadeOutText(float duration)
FadeInText(float duration)
### Properties
ChatBar ChatBar
ChannelsBar ChannelsBar
MessageLogDisplay MessageLogDisplay
### ChatChannel
A client side chat channel object to manage the chat.
### Methods
AddMessageToChannel(table messageData)
RemoveLastMessageFromChannel()
ClearMessageLog()
### MessageLogDisplay
This object handles displaying the messages of the current channel.
### Methods
AddMessage(table messageData)
RemoveLastMessage()
ReorderAllMessages()
Clear()
bool IsScrolledDown()
FadeOutBackground(float duration)
FadeInBackground(float duration)
FadeOutText(float duration)
FadeInText(float duration)
### ChatBar
The chat bar object handles text entry.
### Methods
TextBox GetTextBox()
TextButton GetMessageModeTextButton
bool IsFocused()
CaptureFocus()
ReleaseFocus(bool submitted)
ResetText()
SetText(text)
bool GetEnabled()
SetEnabled(bool enabled)
SetTextLabelText(string text)
SetTextBoxText(string text)
string GetTextBoxText()
ResetSize()
SetTextSize(int textSize)
SetChannelTarget(string channelName)
FadeOutBackground(float duration)
FadeInBackground(float duration)
FadeOutText(float duration)
FadeInText(float duration)
@@ -0,0 +1,195 @@
-- // FileName: TransparencyTweener.lua
-- // Written by: Xsitsu
-- // Description: Data structure for tweening transparency of a group of objects as one unit.
local module = {}
local RunService = game:GetService("RunService")
--////////////////////////////// Include
--//////////////////////////////////////
local modulesFolder = script.Parent
--// Can't use ClassMaker in it's current state since this uses custom __index and __newindex.
--// Maybe in the future I'll expand that to be more powerful, but it's alright like this for now.
--////////////////////////////// Details
--//////////////////////////////////////
local metatable = {}
metatable.__ClassName = "TransparencyTweener"
metatable.__tostring = function(tbl)
return tbl.__ClassName .. ": " .. tbl.MemoryLocation
end
metatable.__metatable = "The metatable is locked"
metatable.__index = function(tbl, index, value)
if rawget(tbl, index) then return rawget(tbl, index) end
if rawget(metatable, index) then return rawget(metatable, index) end
if (index == "Transparency") then
return rawget(tbl, "InternalLastTweenPercentage")
end
error(index .. " is not a valid member of " .. tbl.__ClassName)
end
metatable.__newindex = function(tbl, index, value)
if (index == "Transparency") then
tbl.InternalLastTweenPercentage = value
tbl:SetPropertiesToTweenPercentage(value)
return
end
error(index .. " is not a valid member of " .. tbl.__ClassName)
end
--////////////////////////////// Methods
--//////////////////////////////////////
function metatable:Dump()
local str = tostring(self)
for tweenObject, objectProperties in pairs(self.TweenObjects) do
local addStr = " | "
if (type(tweenObject) == "table") then
addStr = addStr .. "{{" .. tweenObject:Dump() .. "}}"
elseif (type(tweenObject) == "userdata") then
addStr = addStr .. tweenObject.Name .. "/" .. tweenObject:GetFullName()
end
for propertyName, baseValue in pairs(objectProperties) do
addStr = addStr .. " [" .. propertyName .. "=" .. string.sub(tostring(baseValue), 1, 4) .. "]"
end
str = str .. addStr
end
return str
end
function metatable:OutputTest()
print("Test Output for {={" .. self:Dump() .. "}=}")
for tweenObject, objectProperties in pairs(self.TweenObjects) do
print("TweenObject:", tweenObject)
for propertyName, baseValue in pairs(objectProperties) do
print("\t[" .. propertyName .. "=" .. string.sub(tostring(baseValue), 1, 4) .. "] Actual:" .. tweenObject[propertyName])
end
end
end
function metatable:RegisterTweenObjectProperty(objectValue, propertyName, baseValue)
baseValue = baseValue or objectValue[propertyName]
if (not self.TweenObjects[objectValue]) then
self.TweenObjects[objectValue] = {}
end
self.TweenObjects[objectValue][propertyName] = baseValue
self:SetObjectPropertyToPercentValue(objectValue, propertyName, baseValue, self.InternalLastTweenPercentage)
end
function metatable:UnregisterTweenObject(objectValue)
self.TweenObjects[objectValue] = nil
end
function metatable:SetObjectPropertyToPercentValue(objectValue, propertyName, baseValue, percentValue)
local tweenOverValue = 1 - baseValue
local actualValue = baseValue + (tweenOverValue * percentValue)
objectValue[propertyName] = actualValue
end
function metatable:SetPropertiesToTweenPercentage(percentValue)
for tweenObject, objectProperties in pairs(self.TweenObjects) do
for propertyName, baseValue in pairs(objectProperties) do
self:SetObjectPropertyToPercentValue(tweenObject, propertyName, baseValue, percentValue)
end
end
end
function metatable:Tween(duration, targetPercentage, startingPercentage)
if (self.Tweening) then
self.QueuedTween = {duration, targetPercentage, startingPercentage}
self.TweenIsQueued = true
self:CancelTween()
return
end
self.Tweening = true
local vStartingPercentage = startingPercentage
if (vStartingPercentage) then
self:SetPropertiesToTweenPercentage(vStartingPercentage)
else
vStartingPercentage = self.InternalLastTweenPercentage
end
local startTime = tick()
local endTime = startTime + duration
local tweeningOverPercentage = targetPercentage - vStartingPercentage
local percentComplete = 0
spawn(function()
local now = tick()
while(now < endTime and not self.Canceled) do
percentComplete = math.min(math.max((now - startTime) / duration, 0), 1)
local percentValue = vStartingPercentage + (tweeningOverPercentage * percentComplete)
self.InternalLastTweenPercentage = percentValue
self:SetPropertiesToTweenPercentage(percentValue)
RunService.RenderStepped:wait()
now = tick()
end
if (not self.Canceled) then
self.InternalLastTweenPercentage = targetPercentage
self:SetPropertiesToTweenPercentage(targetPercentage)
else
self.Canceled = false
if (self.TweenIsQueued) then
self.TweenIsQueued = false
self.Tweening = false
self:Tween(unpack(self.QueuedTween))
return
end
end
self.Tweening = false
end)
end
function metatable:CancelTween()
self.Canceled = true
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new()
local obj = {}
obj.MemoryLocation = tostring(obj):match("[0123456789ABCDEF]+")
--// We do not want to hold strong references to objects.
obj.TweenObjects = setmetatable({}, {__mode = "k"})
--// Transparency is a property that doesn't actually exist.
--// The index of 'Transparency' is used for reading from and
--// writing to InternalLastTweenPercentage. This needs to be
--// done through metatables so we can also get the behavior
--// of calling the method 'SetPropertiesToTweenPercentage'
--// automatically when a new value is set.
obj.Transparency = nil
obj.InternalLastTweenPercentage = 0
obj.Tweening = false
obj.Canceled = false
obj.TweenIsQueued = false
obj.QueuedTween = {}
obj = setmetatable(obj, metatable)
return obj
end
return module
@@ -0,0 +1,9 @@
{
"lint": {
"SameLineStatement": "fatal",
"LocalUnused": "fatal",
"FunctionUnused": "fatal",
"ImportUnused": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,467 @@
------------------------------------------------------------------------
-- Freecam
-- Cinematic free camera for spectating and video production.
------------------------------------------------------------------------
local pi = math.pi
local abs = math.abs
local clamp = math.clamp
local exp = math.exp
local rad = math.rad
local sign = math.sign
local sqrt = math.sqrt
local tan = math.tan
local ContextActionService = game:GetService("ContextActionService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local StarterGui = game:GetService("StarterGui")
local UserInputService = game:GetService("UserInputService")
local Workspace = game:GetService("Workspace")
local LocalPlayer = Players.LocalPlayer
if not LocalPlayer then
Players:GetPropertyChangedSignal("LocalPlayer"):Wait()
LocalPlayer = Players.LocalPlayer
end
local Camera = Workspace.CurrentCamera
Workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function()
local newCamera = Workspace.CurrentCamera
if newCamera then
Camera = newCamera
end
end)
------------------------------------------------------------------------
local TOGGLE_INPUT_PRIORITY = Enum.ContextActionPriority.Low.Value
local INPUT_PRIORITY = Enum.ContextActionPriority.High.Value
local FREECAM_MACRO_KB = {Enum.KeyCode.LeftShift, Enum.KeyCode.P}
local NAV_GAIN = Vector3.new(1, 1, 1)*64
local PAN_GAIN = Vector2.new(0.75, 1)*8
local FOV_GAIN = 300
local PITCH_LIMIT = rad(90)
local VEL_STIFFNESS = 1.5
local PAN_STIFFNESS = 1.0
local FOV_STIFFNESS = 4.0
------------------------------------------------------------------------
local Spring = {} do
Spring.__index = Spring
function Spring.new(freq, pos)
local self = setmetatable({}, Spring)
self.f = freq
self.p = pos
self.v = pos*0
return self
end
function Spring:Update(dt, goal)
local f = self.f*2*pi
local p0 = self.p
local v0 = self.v
local offset = goal - p0
local decay = exp(-f*dt)
local p1 = goal + (v0*dt - offset*(f*dt + 1))*decay
local v1 = (f*dt*(offset*f - v0) + v0)*decay
self.p = p1
self.v = v1
return p1
end
function Spring:Reset(pos)
self.p = pos
self.v = pos*0
end
end
------------------------------------------------------------------------
local cameraPos = Vector3.new()
local cameraRot = Vector2.new()
local cameraFov = 0
local velSpring = Spring.new(VEL_STIFFNESS, Vector3.new())
local panSpring = Spring.new(PAN_STIFFNESS, Vector2.new())
local fovSpring = Spring.new(FOV_STIFFNESS, 0)
------------------------------------------------------------------------
local Input = {} do
local thumbstickCurve do
local K_CURVATURE = 2.0
local K_DEADZONE = 0.15
local function fCurve(x)
return (exp(K_CURVATURE*x) - 1)/(exp(K_CURVATURE) - 1)
end
local function fDeadzone(x)
return fCurve((x - K_DEADZONE)/(1 - K_DEADZONE))
end
function thumbstickCurve(x)
return sign(x)*clamp(fDeadzone(abs(x)), 0, 1)
end
end
local gamepad = {
ButtonX = 0,
ButtonY = 0,
DPadDown = 0,
DPadUp = 0,
ButtonL2 = 0,
ButtonR2 = 0,
Thumbstick1 = Vector2.new(),
Thumbstick2 = Vector2.new(),
}
local keyboard = {
W = 0,
A = 0,
S = 0,
D = 0,
E = 0,
Q = 0,
U = 0,
H = 0,
J = 0,
K = 0,
I = 0,
Y = 0,
Up = 0,
Down = 0,
LeftShift = 0,
RightShift = 0,
}
local mouse = {
Delta = Vector2.new(),
MouseWheel = 0,
}
local NAV_GAMEPAD_SPEED = Vector3.new(1, 1, 1)
local NAV_KEYBOARD_SPEED = Vector3.new(1, 1, 1)
local PAN_MOUSE_SPEED = Vector2.new(1, 1)*(pi/64)
local PAN_GAMEPAD_SPEED = Vector2.new(1, 1)*(pi/8)
local FOV_WHEEL_SPEED = 1.0
local FOV_GAMEPAD_SPEED = 0.25
local NAV_ADJ_SPEED = 0.75
local NAV_SHIFT_MUL = 0.25
local navSpeed = 1
function Input.Vel(dt)
navSpeed = clamp(navSpeed + dt*(keyboard.Up - keyboard.Down)*NAV_ADJ_SPEED, 0.01, 4)
local kGamepad = Vector3.new(
thumbstickCurve(gamepad.Thumbstick1.X),
thumbstickCurve(gamepad.ButtonR2) - thumbstickCurve(gamepad.ButtonL2),
thumbstickCurve(-gamepad.Thumbstick1.Y)
)*NAV_GAMEPAD_SPEED
local kKeyboard = Vector3.new(
keyboard.D - keyboard.A + keyboard.K - keyboard.H,
keyboard.E - keyboard.Q + keyboard.I - keyboard.Y,
keyboard.S - keyboard.W + keyboard.J - keyboard.U
)*NAV_KEYBOARD_SPEED
local shift = UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) or UserInputService:IsKeyDown(Enum.KeyCode.RightShift)
return (kGamepad + kKeyboard)*(navSpeed*(shift and NAV_SHIFT_MUL or 1))
end
function Input.Pan(dt)
local kGamepad = Vector2.new(
thumbstickCurve(gamepad.Thumbstick2.Y),
thumbstickCurve(-gamepad.Thumbstick2.X)
)*PAN_GAMEPAD_SPEED
local kMouse = mouse.Delta*PAN_MOUSE_SPEED
mouse.Delta = Vector2.new()
return kGamepad + kMouse
end
function Input.Fov(dt)
local kGamepad = (gamepad.ButtonX - gamepad.ButtonY)*FOV_GAMEPAD_SPEED
local kMouse = mouse.MouseWheel*FOV_WHEEL_SPEED
mouse.MouseWheel = 0
return kGamepad + kMouse
end
do
local function Keypress(action, state, input)
keyboard[input.KeyCode.Name] = state == Enum.UserInputState.Begin and 1 or 0
return Enum.ContextActionResult.Sink
end
local function GpButton(action, state, input)
gamepad[input.KeyCode.Name] = state == Enum.UserInputState.Begin and 1 or 0
return Enum.ContextActionResult.Sink
end
local function MousePan(action, state, input)
local delta = input.Delta
mouse.Delta = Vector2.new(-delta.y, -delta.x)
return Enum.ContextActionResult.Sink
end
local function Thumb(action, state, input)
gamepad[input.KeyCode.Name] = input.Position
return Enum.ContextActionResult.Sink
end
local function Trigger(action, state, input)
gamepad[input.KeyCode.Name] = input.Position.z
return Enum.ContextActionResult.Sink
end
local function MouseWheel(action, state, input)
mouse[input.UserInputType.Name] = -input.Position.z
return Enum.ContextActionResult.Sink
end
local function Zero(t)
for k, v in pairs(t) do
t[k] = v*0
end
end
function Input.StartCapture()
ContextActionService:BindActionAtPriority("FreecamKeyboard", Keypress, false, INPUT_PRIORITY,
Enum.KeyCode.W, Enum.KeyCode.U,
Enum.KeyCode.A, Enum.KeyCode.H,
Enum.KeyCode.S, Enum.KeyCode.J,
Enum.KeyCode.D, Enum.KeyCode.K,
Enum.KeyCode.E, Enum.KeyCode.I,
Enum.KeyCode.Q, Enum.KeyCode.Y,
Enum.KeyCode.Up, Enum.KeyCode.Down
)
ContextActionService:BindActionAtPriority("FreecamMousePan", MousePan, false, INPUT_PRIORITY, Enum.UserInputType.MouseMovement)
ContextActionService:BindActionAtPriority("FreecamMouseWheel", MouseWheel, false, INPUT_PRIORITY, Enum.UserInputType.MouseWheel)
ContextActionService:BindActionAtPriority("FreecamGamepadButton", GpButton, false, INPUT_PRIORITY, Enum.KeyCode.ButtonX, Enum.KeyCode.ButtonY)
ContextActionService:BindActionAtPriority("FreecamGamepadTrigger", Trigger, false, INPUT_PRIORITY, Enum.KeyCode.ButtonR2, Enum.KeyCode.ButtonL2)
ContextActionService:BindActionAtPriority("FreecamGamepadThumbstick", Thumb, false, INPUT_PRIORITY, Enum.KeyCode.Thumbstick1, Enum.KeyCode.Thumbstick2)
end
function Input.StopCapture()
navSpeed = 1
Zero(gamepad)
Zero(keyboard)
Zero(mouse)
ContextActionService:UnbindAction("FreecamKeyboard")
ContextActionService:UnbindAction("FreecamMousePan")
ContextActionService:UnbindAction("FreecamMouseWheel")
ContextActionService:UnbindAction("FreecamGamepadButton")
ContextActionService:UnbindAction("FreecamGamepadTrigger")
ContextActionService:UnbindAction("FreecamGamepadThumbstick")
end
end
end
local function GetFocusDistance(cameraFrame)
local znear = 0.1
local viewport = Camera.ViewportSize
local projy = 2*tan(cameraFov/2)
local projx = viewport.x/viewport.y*projy
local fx = cameraFrame.rightVector
local fy = cameraFrame.upVector
local fz = cameraFrame.lookVector
local minVect = Vector3.new()
local minDist = 512
for x = 0, 1, 0.5 do
for y = 0, 1, 0.5 do
local cx = (x - 0.5)*projx
local cy = (y - 0.5)*projy
local offset = fx*cx - fy*cy + fz
local origin = cameraFrame.p + offset*znear
local _, hit = Workspace:FindPartOnRay(Ray.new(origin, offset.unit*minDist))
local dist = (hit - origin).magnitude
if minDist > dist then
minDist = dist
minVect = offset.unit
end
end
end
return fz:Dot(minVect)*minDist
end
------------------------------------------------------------------------
local function StepFreecam(dt)
local vel = velSpring:Update(dt, Input.Vel(dt))
local pan = panSpring:Update(dt, Input.Pan(dt))
local fov = fovSpring:Update(dt, Input.Fov(dt))
local zoomFactor = sqrt(tan(rad(70/2))/tan(rad(cameraFov/2)))
cameraFov = clamp(cameraFov + fov*FOV_GAIN*(dt/zoomFactor), 1, 120)
cameraRot = cameraRot + pan*PAN_GAIN*(dt/zoomFactor)
cameraRot = Vector2.new(clamp(cameraRot.x, -PITCH_LIMIT, PITCH_LIMIT), cameraRot.y%(2*pi))
local cameraCFrame = CFrame.new(cameraPos)*CFrame.fromOrientation(cameraRot.x, cameraRot.y, 0)*CFrame.new(vel*NAV_GAIN*dt)
cameraPos = cameraCFrame.p
Camera.CFrame = cameraCFrame
Camera.Focus = cameraCFrame*CFrame.new(0, 0, -GetFocusDistance(cameraCFrame))
Camera.FieldOfView = cameraFov
end
------------------------------------------------------------------------
local PlayerState = {} do
local mouseBehavior
local mouseIconEnabled
local cameraType
local cameraFocus
local cameraCFrame
local cameraFieldOfView
local screenGuis = {}
local coreGuis = {
Backpack = true,
Chat = true,
Health = true,
PlayerList = true,
}
local setCores = {
BadgesNotificationsActive = true,
PointsNotificationsActive = true,
}
-- Save state and set up for freecam
function PlayerState.Push()
for name in pairs(coreGuis) do
coreGuis[name] = StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType[name])
StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType[name], false)
end
for name in pairs(setCores) do
setCores[name] = StarterGui:GetCore(name)
StarterGui:SetCore(name, false)
end
local playergui = LocalPlayer:FindFirstChildOfClass("PlayerGui")
if playergui then
for _, gui in pairs(playergui:GetChildren()) do
if gui:IsA("ScreenGui") and gui.Enabled then
screenGuis[#screenGuis + 1] = gui
gui.Enabled = false
end
end
end
cameraFieldOfView = Camera.FieldOfView
Camera.FieldOfView = 70
cameraType = Camera.CameraType
Camera.CameraType = Enum.CameraType.Custom
cameraCFrame = Camera.CFrame
cameraFocus = Camera.Focus
mouseIconEnabled = UserInputService.MouseIconEnabled
UserInputService.MouseIconEnabled = false
mouseBehavior = UserInputService.MouseBehavior
UserInputService.MouseBehavior = Enum.MouseBehavior.Default
end
-- Restore state
function PlayerState.Pop()
for name, isEnabled in pairs(coreGuis) do
StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType[name], isEnabled)
end
for name, isEnabled in pairs(setCores) do
StarterGui:SetCore(name, isEnabled)
end
for _, gui in pairs(screenGuis) do
if gui.Parent then
gui.Enabled = true
end
end
Camera.FieldOfView = cameraFieldOfView
cameraFieldOfView = nil
Camera.CameraType = cameraType
cameraType = nil
Camera.CFrame = cameraCFrame
cameraCFrame = nil
Camera.Focus = cameraFocus
cameraFocus = nil
UserInputService.MouseIconEnabled = mouseIconEnabled
mouseIconEnabled = nil
UserInputService.MouseBehavior = mouseBehavior
mouseBehavior = nil
end
end
local function StartFreecam()
local cameraCFrame = Camera.CFrame
cameraRot = Vector2.new(cameraCFrame:toEulerAnglesYXZ())
cameraPos = cameraCFrame.p
cameraFov = Camera.FieldOfView
velSpring:Reset(Vector3.new())
panSpring:Reset(Vector2.new())
fovSpring:Reset(0)
PlayerState.Push()
RunService:BindToRenderStep("Freecam", Enum.RenderPriority.Camera.Value, StepFreecam)
Input.StartCapture()
end
local function StopFreecam()
Input.StopCapture()
RunService:UnbindFromRenderStep("Freecam")
PlayerState.Pop()
end
------------------------------------------------------------------------
do
local enabled = false
local function ToggleFreecam()
if enabled then
StopFreecam()
else
StartFreecam()
end
enabled = not enabled
end
local function CheckMacro(macro)
for i = 1, #macro - 1 do
if not UserInputService:IsKeyDown(macro[i]) then
return
end
end
ToggleFreecam()
end
local function HandleActivationInput(action, state, input)
if state == Enum.UserInputState.Begin then
if input.KeyCode == FREECAM_MACRO_KB[#FREECAM_MACRO_KB] then
CheckMacro(FREECAM_MACRO_KB)
end
end
return Enum.ContextActionResult.Pass
end
ContextActionService:BindActionAtPriority("FreecamToggle", HandleActivationInput, false, TOGGLE_INPUT_PRIORITY, FREECAM_MACRO_KB[#FREECAM_MACRO_KB])
end
@@ -0,0 +1,102 @@
-- // FileName: FreeCameraInstaller.lua
-- // Written by: TheGamer101
-- // Description: Installs Freecam for privileged users.
-- The code is kept on the server and only replicated to the client to minimize security problems.
local FFlagAllowLuobuFreecamGroup = game:DefineFastFlag("AllowLuobuFreecamGroup", false)
-- Users in the following groups have global Freecam permissions:
local FREECAM_GROUP_IDS = {
1200769, -- Roblox Admins
4358041, -- Freecam
}
local LUOBU_FREECAM_GROUP_IDS = {
7842878,
}
local HttpRbxApiService = game:GetService("HttpRbxApiService")
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")
local PolicyService = game:GetService("PolicyService")
local RunService = game:GetService("RunService")
local function Install()
local function WaitForChildOfClass(parent, class)
local child = parent:FindFirstChildOfClass(class)
while not child or child.ClassName ~= class do
child = parent.ChildAdded:Wait()
end
return child
end
local function AddFreeCamera(player)
local playerGui = WaitForChildOfClass(player, "PlayerGui")
local originalModule = script.Parent:WaitForChild("FreeCamera")
-- Encapsulate the Freecam script in a LayerCollector to prevent destruction on respawn.
local screenGui = Instance.new("ScreenGui")
screenGui.Name = "Freecam"
screenGui.ResetOnSpawn = false
local script = Instance.new("LocalScript")
script.Name = "FreecamScript"
script.Source = originalModule.Source
script.Parent = screenGui
screenGui.Parent = playerGui
end
local function ShouldAddFreeCam(player)
if RunService:IsStudio() then
return true
end
if player.UserId <= 0 then
return false
end
local success, result = pcall(function()
local url = string.format("/users/%d/canmanage/%d", player.UserId, game.PlaceId)
-- API returns: {"Success":BOOLEAN,"CanManage":BOOLEAN}
local response = HttpRbxApiService:GetAsync(url, Enum.ThrottlingPriority.Default, Enum.HttpRequestType.Default, true)
return HttpService:JSONDecode(response)
end)
if success and result.CanManage == true then
return true
end
for _, groupId in ipairs(FREECAM_GROUP_IDS) do
local success, inGroup = pcall(player.IsInGroup, player, groupId)
if success and inGroup then
return true
end
end
if FFlagAllowLuobuFreecamGroup and game:GetEngineFeature("LuobuModerationStatus") then
if PolicyService.IsLuobuServer == Enum.TriStateBoolean.True then
for _, groupId in ipairs(LUOBU_FREECAM_GROUP_IDS) do
local success, inGroup = pcall(player.IsInGroup, player, groupId)
if success and inGroup then
return true
end
end
end
end
return false
end
local function PlayerAdded(player)
if ShouldAddFreeCam(player) then
AddFreeCamera(player)
end
end
Players.PlayerAdded:Connect(PlayerAdded)
for _, player in ipairs(Players:GetPlayers()) do
coroutine.wrap(PlayerAdded)(player) -- PlayerAdded may yield, so wrap it in a coroutine to avoid holding up the thread.
end
end
return Install
@@ -0,0 +1,6 @@
{
"lint": {
"SameLineStatement": "fatal",
"FunctionUnused": "fatal"
}
}
@@ -0,0 +1,646 @@
-- // FileName: ChatChannel.lua
-- // Written by: Xsitsu
-- // Description: A representation of one channel that speakers can chat in.
local userShouldMuteUnfilteredMessage = false
do
local success, enabled = pcall(function() return UserSettings():IsUserFeatureEnabled("UserShouldMuteUnfilteredMessage") end)
userShouldMuteUnfilteredMessage = success and enabled
end
local module = {}
local modulesFolder = script.Parent
local Chat = game:GetService("Chat")
local RunService = game:GetService("RunService")
local replicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatSettings = require(replicatedModules:WaitForChild("ChatSettings"))
--////////////////////////////// Include
--//////////////////////////////////////
local ChatConstants = require(replicatedModules:WaitForChild("ChatConstants"))
local Util = require(modulesFolder:WaitForChild("Util"))
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
ChatLocalization = ChatLocalization or {}
if not ChatLocalization.FormatMessageToSend or not ChatLocalization.LocalizeFormattedMessage then
function ChatLocalization:FormatMessageToSend(key,default) return default end
end
--////////////////////////////// Methods
--//////////////////////////////////////
local methods = {}
methods.__index = methods
function methods:SendSystemMessage(message, extraData)
local messageObj = self:InternalCreateMessageObject(message, nil, true, extraData)
local success, err = pcall(function() self.eMessagePosted:Fire(messageObj) end)
if not success and err then
print("Error posting message: " ..err)
end
self:InternalAddMessageToHistoryLog(messageObj)
for i, speaker in pairs(self.Speakers) do
speaker:InternalSendSystemMessage(messageObj, self.Name)
end
return messageObj
end
function methods:SendSystemMessageToSpeaker(message, speakerName, extraData)
local speaker = self.Speakers[speakerName]
if (speaker) then
local messageObj = self:InternalCreateMessageObject(message, nil, true, extraData)
speaker:InternalSendSystemMessage(messageObj, self.Name)
elseif RunService:IsStudio() then
warn(string.format("Speaker '%s' is not in channel '%s' and cannot be sent a system message", speakerName, self.Name))
end
end
function methods:SendMessageObjToFilters(message, messageObj, fromSpeaker)
local oldMessage = messageObj.Message
messageObj.Message = message
self:InternalDoMessageFilter(fromSpeaker.Name, messageObj, self.Name)
self.ChatService:InternalDoMessageFilter(fromSpeaker.Name, messageObj, self.Name)
local newMessage = messageObj.Message
messageObj.Message = oldMessage
return newMessage
end
function methods:CanCommunicateByUserId(userId1, userId2)
if RunService:IsStudio() then
return true
end
-- UserId is set as 0 for non player speakers.
if userId1 == 0 or userId2 == 0 then
return true
end
local success, canCommunicate = pcall(function()
return Chat:CanUsersChatAsync(userId1, userId2)
end)
return success and canCommunicate
end
function methods:CanCommunicate(speakerObj1, speakerObj2)
local player1 = speakerObj1:GetPlayer()
local player2 = speakerObj2:GetPlayer()
if player1 and player2 then
return self:CanCommunicateByUserId(player1.UserId, player2.UserId)
end
return true
end
function methods:SendMessageToSpeaker(message, speakerName, fromSpeakerName, extraData)
local speakerTo = self.Speakers[speakerName]
local speakerFrom = self.ChatService:GetSpeaker(fromSpeakerName)
if speakerTo and speakerFrom then
local isMuted = speakerTo:IsSpeakerMuted(fromSpeakerName)
if isMuted then
return
end
if not self:CanCommunicate(speakerTo, speakerFrom) then
return
end
-- We need to claim the message is filtered even if it not in this case for compatibility with legacy client side code.
local isFiltered = speakerName == fromSpeakerName
local messageObj = self:InternalCreateMessageObject(message, fromSpeakerName, isFiltered, extraData)
message = self:SendMessageObjToFilters(message, messageObj, fromSpeakerName)
speakerTo:InternalSendMessage(messageObj, self.Name)
local textContext = self.Private and Enum.TextFilterContext.PrivateChat or Enum.TextFilterContext.PublicChat
local filterSuccess, isFilterResult, filteredMessage = self.ChatService:InternalApplyRobloxFilterNewAPI(
messageObj.FromSpeaker,
message,
textContext
)
if (filterSuccess) then
messageObj.FilterResult = filteredMessage
messageObj.IsFilterResult = isFilterResult
messageObj.IsFiltered = true
speakerTo:InternalSendFilteredMessageWithFilterResult(messageObj, self.Name)
end
elseif RunService:IsStudio() then
warn(string.format("Speaker '%s' is not in channel '%s' and cannot be sent a message", speakerName, self.Name))
end
end
function methods:KickSpeaker(speakerName, reason)
local speaker = self.ChatService:GetSpeaker(speakerName)
if (not speaker) then
error("Speaker \"" .. speakerName .. "\" does not exist!")
end
local messageToSpeaker = ""
local messageToChannel = ""
local playerName = speaker:GetNameForDisplay()
if (reason) then
messageToSpeaker = string.format("You were kicked from '%s' for the following reason(s): %s", self.Name, reason)
messageToChannel = string.format("%s was kicked for the following reason(s): %s", playerName, reason)
else
messageToSpeaker = string.format("You were kicked from '%s'", self.Name)
messageToChannel = string.format("%s was kicked", playerName)
end
self:SendSystemMessageToSpeaker(messageToSpeaker, speakerName)
speaker:LeaveChannel(self.Name)
self:SendSystemMessage(messageToChannel)
end
function methods:MuteSpeaker(speakerName, reason, length)
local speaker = self.ChatService:GetSpeaker(speakerName)
if (not speaker) then
error("Speaker \"" .. speakerName .. "\" does not exist!")
end
self.Mutes[speakerName:lower()] = (length == 0 or length == nil) and 0 or (os.time() + length)
if (reason) then
local playerName = speaker:GetNameForDisplay()
self:SendSystemMessage(string.format("%s was muted for the following reason(s): %s", playerName, reason))
end
local success, err = pcall(function() self.eSpeakerMuted:Fire(speakerName, reason, length) end)
if not success and err then
print("Error mutting speaker: " ..err)
end
local spkr = self.ChatService:GetSpeaker(speakerName)
if (spkr) then
local success, err = pcall(function() spkr.eMuted:Fire(self.Name, reason, length) end)
if not success and err then
print("Error mutting speaker: " ..err)
end
end
end
function methods:UnmuteSpeaker(speakerName)
local speaker = self.ChatService:GetSpeaker(speakerName)
if (not speaker) then
error("Speaker \"" .. speakerName .. "\" does not exist!")
end
self.Mutes[speakerName:lower()] = nil
local success, err = pcall(function() self.eSpeakerUnmuted:Fire(speakerName) end)
if not success and err then
print("Error unmuting speaker: " ..err)
end
local spkr = self.ChatService:GetSpeaker(speakerName)
if (spkr) then
local success, err = pcall(function() spkr.eUnmuted:Fire(self.Name) end)
if not success and err then
print("Error unmuting speaker: " ..err)
end
end
end
function methods:IsSpeakerMuted(speakerName)
return (self.Mutes[speakerName:lower()] ~= nil)
end
function methods:GetSpeakerList()
local list = {}
for i, speaker in pairs(self.Speakers) do
table.insert(list, speaker.Name)
end
return list
end
function methods:RegisterFilterMessageFunction(funcId, func, priority)
if self.FilterMessageFunctions:HasFunction(funcId) then
error(string.format("FilterMessageFunction '%s' already exists", funcId))
end
self.FilterMessageFunctions:AddFunction(funcId, func, priority)
end
function methods:FilterMessageFunctionExists(funcId)
return self.FilterMessageFunctions:HasFunction(funcId)
end
function methods:UnregisterFilterMessageFunction(funcId)
if not self.FilterMessageFunctions:HasFunction(funcId) then
error(string.format("FilterMessageFunction '%s' does not exists", funcId))
end
self.FilterMessageFunctions:RemoveFunction(funcId)
end
function methods:RegisterProcessCommandsFunction(funcId, func, priority)
if self.ProcessCommandsFunctions:HasFunction(funcId) then
error(string.format("ProcessCommandsFunction '%s' already exists", funcId))
end
self.ProcessCommandsFunctions:AddFunction(funcId, func, priority)
end
function methods:ProcessCommandsFunctionExists(funcId)
return self.ProcessCommandsFunctions:HasFunction(funcId)
end
function methods:UnregisterProcessCommandsFunction(funcId)
if not self.ProcessCommandsFunctions:HasFunction(funcId) then
error(string.format("ProcessCommandsFunction '%s' does not exist", funcId))
end
self.ProcessCommandsFunctions:RemoveFunction(funcId)
end
local function ShallowCopy(table)
local copy = {}
for i, v in pairs(table) do
copy[i] = v
end
return copy
end
function methods:GetHistoryLog()
return ShallowCopy(self.ChatHistory)
end
function methods:GetHistoryLogForSpeaker(speaker)
local userId = -1
local player = speaker:GetPlayer()
if player then
userId = player.UserId
end
local chatlog = {}
for i = 1, #self.ChatHistory do
local logUserId = self.ChatHistory[i].SpeakerUserId
if self:CanCommunicateByUserId(userId, logUserId) then
local messageObj = ShallowCopy(self.ChatHistory[i])
--// Since we're using the new filter API, we need to convert the stored filter result
--// into an actual string message to send to players for their chat history.
--// System messages aren't filtered the same way, so they just have a regular
--// text value in the Message field.
if (messageObj.MessageType == ChatConstants.MessageTypeDefault or messageObj.MessageType == ChatConstants.MessageTypeMeCommand) then
local filterResult = messageObj.FilterResult
if (messageObj.IsFilterResult) then
if (player) then
messageObj.Message = filterResult:GetChatForUserAsync(player.UserId)
else
messageObj.Message = filterResult:GetNonChatStringForBroadcastAsync()
end
else
messageObj.Message = filterResult
end
end
table.insert(chatlog, messageObj)
end
end
return chatlog
end
--///////////////// Internal-Use Methods
--//////////////////////////////////////
function methods:InternalDestroy()
for i, speaker in pairs(self.Speakers) do
speaker:LeaveChannel(self.Name)
end
self.eDestroyed:Fire()
self.eDestroyed:Destroy()
self.eMessagePosted:Destroy()
self.eSpeakerJoined:Destroy()
self.eSpeakerLeft:Destroy()
self.eSpeakerMuted:Destroy()
self.eSpeakerUnmuted:Destroy()
end
function methods:InternalDoMessageFilter(speakerName, messageObj, channel)
local filtersIterator = self.FilterMessageFunctions:GetIterator()
for funcId, func, priority in filtersIterator do
local success, errorMessage = pcall(function()
func(speakerName, messageObj, channel)
end)
if not success then
warn(string.format("DoMessageFilter Function '%s' failed for reason: %s", funcId, errorMessage))
end
end
end
function methods:InternalDoProcessCommands(speakerName, message, channel)
local commandsIterator = self.ProcessCommandsFunctions:GetIterator()
for funcId, func, priority in commandsIterator do
local success, returnValue = pcall(function()
local ret = func(speakerName, message, channel)
if type(ret) ~= "boolean" then
error("Process command functions must return a bool")
end
return ret
end)
if not success then
warn(string.format("DoProcessCommands Function '%s' failed for reason: %s", funcId, returnValue))
elseif returnValue then
return true
end
end
return false
end
function methods:InternalPostMessage(fromSpeaker, message, extraData)
if (self:InternalDoProcessCommands(fromSpeaker.Name, message, self.Name)) then return false end
if (self.Mutes[fromSpeaker.Name:lower()] ~= nil) then
local t = self.Mutes[fromSpeaker.Name:lower()]
if (t > 0 and os.time() > t) then
self:UnmuteSpeaker(fromSpeaker.Name)
else
self:SendSystemMessageToSpeaker(ChatLocalization:FormatMessageToSend("GameChat_ChatChannel_MutedInChannel","You are muted and cannot talk in this channel"), fromSpeaker.Name)
return false
end
end
local messageObj = self:InternalCreateMessageObject(message, fromSpeaker.Name, false, extraData)
-- allow server to process the unfiltered message string
messageObj.Message = message
local processedMessage
pcall(function()
processedMessage = Chat:InvokeChatCallback(Enum.ChatCallbackType.OnServerReceivingMessage, messageObj)
end)
messageObj.Message = nil
if processedMessage then
-- developer server code's choice to mute the message
if processedMessage.ShouldDeliver == false then
return false
end
messageObj = processedMessage
end
message = self:SendMessageObjToFilters(message, messageObj, fromSpeaker)
local sentToList = {}
for i, speaker in pairs(self.Speakers) do
local isMuted = speaker:IsSpeakerMuted(fromSpeaker.Name)
if not isMuted and self:CanCommunicate(fromSpeaker, speaker) then
table.insert(sentToList, speaker.Name)
if speaker.Name == fromSpeaker.Name then
-- Send unfiltered message to speaker who sent the message.
local cMessageObj = ShallowCopy(messageObj)
if userShouldMuteUnfilteredMessage then
cMessageObj.Message = string.rep("_", messageObj.MessageLength)
else
cMessageObj.Message = message
end
cMessageObj.IsFiltered = true
-- We need to claim the message is filtered even if it not in this case for compatibility with legacy client side code.
speaker:InternalSendMessage(cMessageObj, self.Name)
else
speaker:InternalSendMessage(messageObj, self.Name)
end
end
end
local success, err = pcall(function() self.eMessagePosted:Fire(messageObj) end)
if not success and err then
print("Error posting message: " ..err)
end
local textFilterContext = self.Private and Enum.TextFilterContext.PrivateChat or Enum.TextFilterContext.PublicChat
local filterSuccess, isFilterResult, filteredMessage = self.ChatService:InternalApplyRobloxFilterNewAPI(
messageObj.FromSpeaker,
message,
textFilterContext
)
if (filterSuccess) then
messageObj.FilterResult = filteredMessage
messageObj.IsFilterResult = isFilterResult
else
return false
end
messageObj.IsFiltered = true
self:InternalAddMessageToHistoryLog(messageObj)
for _, speakerName in pairs(sentToList) do
local speaker = self.Speakers[speakerName]
if (speaker) then
speaker:InternalSendFilteredMessageWithFilterResult(messageObj, self.Name)
end
end
-- One more pass is needed to ensure that no speakers do not recieve the message.
-- Otherwise a user could join while the message is being filtered who had not originally been sent the message.
local speakersMissingMessage = {}
for _, speaker in pairs(self.Speakers) do
local isMuted = speaker:IsSpeakerMuted(fromSpeaker.Name)
if not isMuted and self:CanCommunicate(fromSpeaker, speaker) then
local wasSentMessage = false
for _, sentSpeakerName in pairs(sentToList) do
if speaker.Name == sentSpeakerName then
wasSentMessage = true
break
end
end
if not wasSentMessage then
table.insert(speakersMissingMessage, speaker.Name)
end
end
end
for _, speakerName in pairs(speakersMissingMessage) do
local speaker = self.Speakers[speakerName]
if speaker then
speaker:InternalSendFilteredMessageWithFilterResult(messageObj, self.Name)
end
end
return messageObj
end
function methods:InternalAddSpeaker(speaker)
if (self.Speakers[speaker.Name]) then
warn("Speaker \"" .. speaker.name .. "\" is already in the channel!")
return
end
self.Speakers[speaker.Name] = speaker
local success, err = pcall(function() self.eSpeakerJoined:Fire(speaker.Name) end)
if not success and err then
print("Error removing channel: " ..err)
end
end
function methods:InternalRemoveSpeaker(speaker)
if (not self.Speakers[speaker.Name]) then
warn("Speaker \"" .. speaker.name .. "\" is not in the channel!")
return
end
self.Speakers[speaker.Name] = nil
local success, err = pcall(function() self.eSpeakerLeft:Fire(speaker.Name) end)
if not success and err then
print("Error removing speaker: " ..err)
end
end
function methods:InternalRemoveExcessMessagesFromLog()
local remove = table.remove
while (#self.ChatHistory > self.MaxHistory) do
remove(self.ChatHistory, 1)
end
end
function methods:InternalAddMessageToHistoryLog(messageObj)
table.insert(self.ChatHistory, messageObj)
self:InternalRemoveExcessMessagesFromLog()
end
function methods:GetMessageType(message, fromSpeaker)
if fromSpeaker == nil then
return ChatConstants.MessageTypeSystem
end
return ChatConstants.MessageTypeDefault
end
function methods:InternalCreateMessageObject(message, fromSpeaker, isFiltered, extraData)
local messageType = self:GetMessageType(message, fromSpeaker)
local speakerUserId = -1
local speakerDisplayName = nil
local speaker = nil
if fromSpeaker then
speaker = self.ChatService:GetSpeaker(fromSpeaker)
if speaker then
local player = speaker:GetPlayer()
if player then
speakerUserId = player.UserId
if ChatSettings.PlayerDisplayNamesEnabled then
speakerDisplayName = speaker:GetNameForDisplay()
end
else
speakerUserId = 0
end
end
end
local messageObj =
{
ID = self.ChatService:InternalGetUniqueMessageId(),
FromSpeaker = fromSpeaker,
SpeakerDisplayName = speakerDisplayName,
SpeakerUserId = speakerUserId,
OriginalChannel = self.Name,
MessageLength = string.len(message),
MessageType = messageType,
IsFiltered = isFiltered,
Message = isFiltered and message or nil,
--// These two get set by the new API. The comments are just here
--// to remind readers that they will exist so it's not super
--// confusing if they find them in the code but cannot find them
--// here.
--FilterResult = nil,
--IsFilterResult = false,
Time = os.time(),
ExtraData = {},
}
if speaker then
for k, v in pairs(speaker.ExtraData) do
messageObj.ExtraData[k] = v
end
end
if (extraData) then
for k, v in pairs(extraData) do
messageObj.ExtraData[k] = v
end
end
return messageObj
end
function methods:SetChannelNameColor(color)
self.ChannelNameColor = color
for i, speaker in pairs(self.Speakers) do
speaker:UpdateChannelNameColor(self.Name, color)
end
end
function methods:GetWelcomeMessageForSpeaker(speaker)
if self.GetWelcomeMessageFunction then
local welcomeMessage = self.GetWelcomeMessageFunction(speaker)
if welcomeMessage then
return welcomeMessage
end
end
return self.WelcomeMessage
end
function methods:RegisterGetWelcomeMessageFunction(func)
if type(func) ~= "function" then
error("RegisterGetWelcomeMessageFunction must be called with a function.")
end
self.GetWelcomeMessageFunction = func
end
function methods:UnRegisterGetWelcomeMessageFunction()
self.GetWelcomeMessageFunction = nil
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new(vChatService, name, welcomeMessage, channelNameColor)
local obj = setmetatable({}, methods)
obj.ChatService = vChatService
obj.Name = name
obj.WelcomeMessage = welcomeMessage or ""
obj.GetWelcomeMessageFunction = nil
obj.ChannelNameColor = channelNameColor
obj.Joinable = true
obj.Leavable = true
obj.AutoJoin = false
obj.Private = false
obj.Speakers = {}
obj.Mutes = {}
obj.MaxHistory = 200
obj.HistoryIndex = 0
obj.ChatHistory = {}
obj.FilterMessageFunctions = Util:NewSortedFunctionContainer()
obj.ProcessCommandsFunctions = Util:NewSortedFunctionContainer()
-- Make sure to destroy added binadable events in the InternalDestroy method.
obj.eDestroyed = Instance.new("BindableEvent")
obj.eMessagePosted = Instance.new("BindableEvent")
obj.eSpeakerJoined = Instance.new("BindableEvent")
obj.eSpeakerLeft = Instance.new("BindableEvent")
obj.eSpeakerMuted = Instance.new("BindableEvent")
obj.eSpeakerUnmuted = Instance.new("BindableEvent")
obj.MessagePosted = obj.eMessagePosted.Event
obj.SpeakerJoined = obj.eSpeakerJoined.Event
obj.SpeakerLeft = obj.eSpeakerLeft.Event
obj.SpeakerMuted = obj.eSpeakerMuted.Event
obj.SpeakerUnmuted = obj.eSpeakerUnmuted.Event
obj.Destroyed = obj.eDestroyed.Event
return obj
end
return module
@@ -0,0 +1,485 @@
-- // FileName: ChatService.lua
-- // Written by: Xsitsu
-- // Description: Manages creating and destroying ChatChannels and Speakers.
local MAX_FILTER_RETRIES = 3
local FILTER_BACKOFF_INTERVALS = {50/1000, 100/1000, 200/1000}
local MAX_FILTER_DURATION = 60
--- Constants used to decide when to notify that the chat filter is having issues filtering messages.
local FILTER_NOTIFCATION_THRESHOLD = 3 --Number of notifcation failures before an error message is output.
local FILTER_NOTIFCATION_INTERVAL = 60 --Time between error messages.
local FILTER_THRESHOLD_TIME = 60 --If there has not been an issue in this many seconds, the count of issues resets.
local module = {}
local RunService = game:GetService("RunService")
local Chat = game:GetService("Chat")
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local modulesFolder = script.Parent
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings"))
local errorTextColor = ChatSettings.ErrorMessageTextColor or Color3.fromRGB(245, 50, 50)
local errorExtraData = {ChatColor = errorTextColor}
--////////////////////////////// Include
--//////////////////////////////////////
local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants"))
local ChatChannel = require(modulesFolder:WaitForChild("ChatChannel"))
local Speaker = require(modulesFolder:WaitForChild("Speaker"))
local Util = require(modulesFolder:WaitForChild("Util"))
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
ChatLocalization = ChatLocalization or {}
if not ChatLocalization.FormatMessageToSend or not ChatLocalization.LocalizeFormattedMessage then
function ChatLocalization:FormatMessageToSend(key,default) return default end
end
local function allSpaces(inputString)
local testString = string.gsub(inputString, " ", "")
return string.len(testString) == 0
end
--////////////////////////////// Methods
--//////////////////////////////////////
local methods = {}
methods.__index = methods
function methods:AddChannel(channelName, autoJoin)
if (self.ChatChannels[channelName:lower()]) then
error(string.format("Channel %q alrady exists.", channelName))
end
local function DefaultChannelCommands(fromSpeaker, message)
if (message:lower() == "/leave") then
local channel = self:GetChannel(channelName)
local speaker = self:GetSpeaker(fromSpeaker)
if (channel and speaker) then
if (channel.Leavable) then
speaker:LeaveChannel(channelName)
local msg = ChatLocalization:FormatMessageToSend(
"GameChat_ChatService_YouHaveLeftChannel",
string.format("You have left channel '%s'", channelName),
"RBX_NAME",
channelName)
speaker:SendSystemMessage(msg, "System")
else
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_ChatService_CannotLeaveChannel","You cannot leave this channel."), channelName)
end
end
return true
end
return false
end
local channel = ChatChannel.new(self, channelName)
self.ChatChannels[channelName:lower()] = channel
channel:RegisterProcessCommandsFunction("default_commands", DefaultChannelCommands, ChatConstants.HighPriority)
local success, err = pcall(function() self.eChannelAdded:Fire(channelName) end)
if not success and err then
print("Error addding channel: " ..err)
end
if autoJoin ~= nil then
channel.AutoJoin = autoJoin
if autoJoin then
for _, speaker in pairs(self.Speakers) do
speaker:JoinChannel(channelName)
end
end
end
return channel
end
function methods:RemoveChannel(channelName)
if (self.ChatChannels[channelName:lower()]) then
local n = self.ChatChannels[channelName:lower()].Name
self.ChatChannels[channelName:lower()]:InternalDestroy()
self.ChatChannels[channelName:lower()] = nil
local success, err = pcall(function() self.eChannelRemoved:Fire(n) end)
if not success and err then
print("Error removing channel: " ..err)
end
else
warn(string.format("Channel %q does not exist.", channelName))
end
end
function methods:GetChannel(channelName)
return self.ChatChannels[channelName:lower()]
end
function methods:AddSpeaker(speakerName)
if (self.Speakers[speakerName:lower()]) then
error("Speaker \"" .. speakerName .. "\" already exists!")
end
local speaker = Speaker.new(self, speakerName)
self.Speakers[speakerName:lower()] = speaker
local success, err = pcall(function() self.eSpeakerAdded:Fire(speakerName) end)
if not success and err then
print("Error adding speaker: " ..err)
end
return speaker
end
function methods:InternalUnmuteSpeaker(speakerName)
for channelName, channel in pairs(self.ChatChannels) do
if channel:IsSpeakerMuted(speakerName) then
channel:UnmuteSpeaker(speakerName)
end
end
end
function methods:RemoveSpeaker(speakerName)
if (self.Speakers[speakerName:lower()]) then
local n = self.Speakers[speakerName:lower()].Name
self:InternalUnmuteSpeaker(n)
self.Speakers[speakerName:lower()]:InternalDestroy()
self.Speakers[speakerName:lower()] = nil
local success, err = pcall(function() self.eSpeakerRemoved:Fire(n) end)
if not success and err then
print("Error removing speaker: " ..err)
end
else
warn("Speaker \"" .. speakerName .. "\" does not exist!")
end
end
function methods:GetSpeaker(speakerName)
return self.Speakers[speakerName:lower()]
end
function methods:GetSpeakerByUserOrDisplayName(speakerName)
local speakerByUserName = self.Speakers[speakerName:lower()]
if speakerByUserName then
return speakerByUserName
end
for _, potentialSpeaker in pairs(self.Speakers) do
local player = potentialSpeaker:GetPlayer()
if player and player.DisplayName:lower() == speakerName:lower() then
return potentialSpeaker
end
end
end
function methods:GetChannelList()
local list = {}
for i, channel in pairs(self.ChatChannels) do
if (not channel.Private) then
table.insert(list, channel.Name)
end
end
return list
end
function methods:GetAutoJoinChannelList()
local list = {}
for i, channel in pairs(self.ChatChannels) do
if channel.AutoJoin then
table.insert(list, channel)
end
end
return list
end
function methods:GetSpeakerList()
local list = {}
for i, speaker in pairs(self.Speakers) do
table.insert(list, speaker.Name)
end
return list
end
function methods:SendGlobalSystemMessage(message)
for i, speaker in pairs(self.Speakers) do
speaker:SendSystemMessage(message, nil)
end
end
function methods:RegisterFilterMessageFunction(funcId, func, priority)
if self.FilterMessageFunctions:HasFunction(funcId) then
error(string.format("FilterMessageFunction '%s' already exists", funcId))
end
self.FilterMessageFunctions:AddFunction(funcId, func, priority)
end
function methods:FilterMessageFunctionExists(funcId)
return self.FilterMessageFunctions:HasFunction(funcId)
end
function methods:UnregisterFilterMessageFunction(funcId)
if not self.FilterMessageFunctions:HasFunction(funcId) then
error(string.format("FilterMessageFunction '%s' does not exists", funcId))
end
self.FilterMessageFunctions:RemoveFunction(funcId)
end
function methods:RegisterProcessCommandsFunction(funcId, func, priority)
if self.ProcessCommandsFunctions:HasFunction(funcId) then
error(string.format("ProcessCommandsFunction '%s' already exists", funcId))
end
self.ProcessCommandsFunctions:AddFunction(funcId, func, priority)
end
function methods:ProcessCommandsFunctionExists(funcId)
return self.ProcessCommandsFunctions:HasFunction(funcId)
end
function methods:UnregisterProcessCommandsFunction(funcId)
if not self.ProcessCommandsFunctions:HasFunction(funcId) then
error(string.format("ProcessCommandsFunction '%s' does not exist", funcId))
end
self.ProcessCommandsFunctions:RemoveFunction(funcId)
end
local LastFilterNoficationTime = 0
local LastFilterIssueTime = 0
local FilterIssueCount = 0
function methods:InternalNotifyFilterIssue()
if (tick() - LastFilterIssueTime) > FILTER_THRESHOLD_TIME then
FilterIssueCount = 0
end
FilterIssueCount = FilterIssueCount + 1
LastFilterIssueTime = tick()
if FilterIssueCount >= FILTER_NOTIFCATION_THRESHOLD then
if (tick() - LastFilterNoficationTime) > FILTER_NOTIFCATION_INTERVAL then
LastFilterNoficationTime = tick()
local systemChannel = self:GetChannel("System")
if systemChannel then
systemChannel:SendSystemMessage(
ChatLocalization:FormatMessageToSend(
"GameChat_ChatService_ChatFilterIssues",
"The chat filter is currently experiencing issues and messages may be slow to appear."
),
errorExtraData
)
end
end
end
end
local StudioMessageFilteredCache = {}
--///////////////// Internal-Use Methods
--//////////////////////////////////////
--DO NOT REMOVE THIS. Chat must be filtered or your game will face
--moderation.
function methods:InternalApplyRobloxFilter(speakerName, message, toSpeakerName) --// USES FFLAG
if (RunService:IsServer() and not RunService:IsStudio()) then
local fromSpeaker = self:GetSpeaker(speakerName)
local toSpeaker = toSpeakerName and self:GetSpeaker(toSpeakerName)
if fromSpeaker == nil then
return nil
end
local fromPlayerObj = fromSpeaker:GetPlayer()
local toPlayerObj = toSpeaker and toSpeaker:GetPlayer()
if fromPlayerObj == nil then
return message
end
if allSpaces(message) then
return message
end
local filterStartTime = tick()
local filterRetries = 0
while true do
local success, message = pcall(function()
if toPlayerObj then
return Chat:FilterStringAsync(message, fromPlayerObj, toPlayerObj)
else
return Chat:FilterStringForBroadcast(message, fromPlayerObj)
end
end)
if success then
return message
else
warn("Error filtering message:", message)
end
filterRetries = filterRetries + 1
if filterRetries > MAX_FILTER_RETRIES or (tick() - filterStartTime) > MAX_FILTER_DURATION then
self:InternalNotifyFilterIssue()
return nil
end
local backoffInterval = FILTER_BACKOFF_INTERVALS[math.min(#FILTER_BACKOFF_INTERVALS, filterRetries)]
-- backoffWait = backoffInterval +/- (0 -> backoffInterval)
local backoffWait = backoffInterval + ((math.random()*2 - 1) * backoffInterval)
wait(backoffWait)
end
else
--// Simulate filtering latency.
--// There is only latency the first time the message is filtered, all following calls will be instant.
if not StudioMessageFilteredCache[message] then
StudioMessageFilteredCache[message] = true
wait()
end
return message
end
return nil
end
--// Return values: bool filterSuccess, bool resultIsFilterObject, variant result
function methods:InternalApplyRobloxFilterNewAPI(speakerName, message, textFilterContext) --// USES FFLAG
local alwaysRunFilter = false
local runFilter = RunService:IsServer() and not RunService:IsStudio()
if (alwaysRunFilter or runFilter) then
local fromSpeaker = self:GetSpeaker(speakerName)
if fromSpeaker == nil then
return false, nil, nil
end
local fromPlayerObj = fromSpeaker:GetPlayer()
if fromPlayerObj == nil then
return true, false, message
end
if allSpaces(message) then
return true, false, message
end
local success, filterResult = pcall(function()
local ts = game:GetService("TextService")
local result = ts:FilterStringAsync(message, fromPlayerObj.UserId, textFilterContext)
return result
end)
if (success) then
return true, true, filterResult
else
warn("Error filtering message:", message, filterResult)
self:InternalNotifyFilterIssue()
return false, nil, nil
end
end
--// Simulate filtering latency.
wait()
return true, false, message
end
function methods:InternalDoMessageFilter(speakerName, messageObj, channel)
local filtersIterator = self.FilterMessageFunctions:GetIterator()
for funcId, func, priority in filtersIterator do
local success, errorMessage = pcall(function()
func(speakerName, messageObj, channel)
end)
if not success then
warn(string.format("DoMessageFilter Function '%s' failed for reason: %s", funcId, errorMessage))
end
end
end
function methods:InternalDoProcessCommands(speakerName, message, channel)
local commandsIterator = self.ProcessCommandsFunctions:GetIterator()
for funcId, func, priority in commandsIterator do
local success, returnValue = pcall(function()
local ret = func(speakerName, message, channel)
if type(ret) ~= "boolean" then
error("Process command functions must return a bool")
end
return ret
end)
if not success then
warn(string.format("DoProcessCommands Function '%s' failed for reason: %s", funcId, returnValue))
elseif returnValue then
return true
end
end
return false
end
function methods:InternalGetUniqueMessageId()
local id = self.MessageIdCounter
self.MessageIdCounter = id + 1
return id
end
function methods:InternalAddSpeakerWithPlayerObject(speakerName, playerObj, fireSpeakerAdded)
if (self.Speakers[speakerName:lower()]) then
error("Speaker \"" .. speakerName .. "\" already exists!")
end
local speaker = Speaker.new(self, speakerName)
speaker:InternalAssignPlayerObject(playerObj)
self.Speakers[speakerName:lower()] = speaker
if fireSpeakerAdded then
local success, err = pcall(function() self.eSpeakerAdded:Fire(speakerName) end)
if not success and err then
print("Error adding speaker: " ..err)
end
end
return speaker
end
function methods:InternalFireSpeakerAdded(speakerName)
local success, err = pcall(function() self.eSpeakerAdded:Fire(speakerName) end)
if not success and err then
print("Error firing speaker added: " ..err)
end
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new()
local obj = setmetatable({}, methods)
obj.MessageIdCounter = 0
obj.ChatChannels = {}
obj.Speakers = {}
obj.FilterMessageFunctions = Util:NewSortedFunctionContainer()
obj.ProcessCommandsFunctions = Util:NewSortedFunctionContainer()
obj.eChannelAdded = Instance.new("BindableEvent")
obj.eChannelRemoved = Instance.new("BindableEvent")
obj.eSpeakerAdded = Instance.new("BindableEvent")
obj.eSpeakerRemoved = Instance.new("BindableEvent")
obj.ChannelAdded = obj.eChannelAdded.Event
obj.ChannelRemoved = obj.eChannelRemoved.Event
obj.SpeakerAdded = obj.eSpeakerAdded.Event
obj.SpeakerRemoved = obj.eSpeakerRemoved.Event
obj.ChatServiceMajorVersion = 0
obj.ChatServiceMinorVersion = 5
return obj
end
return module.new()
@@ -0,0 +1,113 @@
local runnerScriptName = "ChatServiceRunner"
local installDirectory = game:GetService("Chat")
local ServerScriptService = game:GetService("ServerScriptService")
local function LoadScript(name, parent)
local originalModule = script.Parent:WaitForChild(name)
local script = Instance.new("Script")
script.Name = name
script.Source = originalModule.Source
script.Parent = parent
return script
end
local function LoadModule(location, name, parent)
local originalModule = location:WaitForChild(name)
local module = Instance.new("ModuleScript")
module.Name = name
module.Source = originalModule.Source
module.Parent = parent
return module
end
local function GetBoolValue(parent, name, defaultValue)
local boolValue = parent:FindFirstChild(name)
if boolValue then
if boolValue:IsA("BoolValue") then
return boolValue.Value
end
end
return defaultValue
end
local function makeDefaultLocalizationTable(parent)
local defaultChatLocalization = Instance.new("LocalizationTable")
defaultChatLocalization.Name = "ChatLocalization"
defaultChatLocalization.Archivable = false
defaultChatLocalization.SourceLocaleId = "en-us"
defaultChatLocalization:SetEntries(require(script.Parent:WaitForChild("DefaultChatLocalization")))
defaultChatLocalization:SetIsExemptFromUGCAnalytics(true)
defaultChatLocalization.Parent = parent;
end
local function Install()
local existingChatLocalization = installDirectory:FindFirstChild("ChatLocalization")
if existingChatLocalization then
if existingChatLocalization:IsA("LocalizationTable" ) then
existingChatLocalization:SetIsExemptFromUGCAnalytics(true)
end
else
makeDefaultLocalizationTable(installDirectory)
end
local chatServiceRunnerArchivable = true
local ChatServiceRunner = installDirectory:FindFirstChild(runnerScriptName)
if not ChatServiceRunner then
chatServiceRunnerArchivable = false
ChatServiceRunner = LoadScript(runnerScriptName, installDirectory)
LoadModule(script.Parent, "ChatService", ChatServiceRunner)
LoadModule(script.Parent, "ChatChannel", ChatServiceRunner)
LoadModule(script.Parent, "Speaker", ChatServiceRunner)
LoadModule(script.Parent, "Util", ChatServiceRunner)
end
local ChatModules = installDirectory:FindFirstChild("ChatModules")
if not ChatModules then
ChatModules = Instance.new("Folder")
ChatModules.Name = "ChatModules"
ChatModules.Archivable = false
local InsertDefaults = Instance.new("BoolValue")
InsertDefaults.Name = "InsertDefaultModules"
InsertDefaults.Value = true
InsertDefaults.Parent = ChatModules
ChatModules.Parent = installDirectory
end
local shouldInsertDefaultModules = GetBoolValue(ChatModules, "InsertDefaultModules", false)
if shouldInsertDefaultModules then
if not ChatModules:FindFirstChild("Utility") then
local Utility = Instance.new("Folder")
Utility.Name = "Utility"
Utility.Parent = ChatModules
end
local defaultChatModules = script.Parent.DefaultChatModules:GetChildren()
for i = 1, #defaultChatModules do
if defaultChatModules.className ~= "Folder" and not ChatModules:FindFirstChild(defaultChatModules[i].Name) then
LoadModule(script.Parent.DefaultChatModules, defaultChatModules[i].Name, ChatModules)
end
end
for _, utilityModule in pairs(script.Parent.DefaultChatModules.Utility:GetChildren()) do
if not ChatModules.Utility:FindFirstChild(utilityModule.Name) then
LoadModule(script.Parent.DefaultChatModules.Utility, utilityModule.Name, ChatModules.Utility)
end
end
end
if not ServerScriptService:FindFirstChild(runnerScriptName) then
local ChatServiceRunnerCopy = ChatServiceRunner:Clone()
ChatServiceRunnerCopy.Archivable = false
ChatServiceRunnerCopy.Parent = ServerScriptService
end
ChatServiceRunner.Archivable = chatServiceRunnerArchivable
end
return Install
@@ -0,0 +1,432 @@
-- // FileName: ChatServiceRunner.lua
-- // Written by: Xsitsu
-- // Description: Main script to initialize ChatService and run ChatModules.
local EventFolderName = "DefaultChatSystemChatEvents"
local EventFolderParent = game:GetService("ReplicatedStorage")
local modulesFolder = script
local PlayersService = game:GetService("Players")
local RunService = game:GetService("RunService")
local Chat = game:GetService("Chat")
local ChatService = require(modulesFolder:WaitForChild("ChatService"))
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings"))
local ChatLocalization = nil
pcall(function() ChatLocalization = require(Chat.ClientChatModules.ChatLocalization) end)
ChatLocalization = ChatLocalization or {}
local MAX_MESSAGE_LENGTH = ChatSettings.MaximumMessageLength
local MAX_BYTES_PER_CODEPOINT = 6
if not ChatLocalization.FormatMessageToSend or not ChatLocalization.LocalizeFormattedMessage then
function ChatLocalization:FormatMessageToSend(key,default) return default end
end
local MAX_BLOCKED_SPEAKERS_PER_REQ = 50
local useEvents = {}
local EventFolder = EventFolderParent:FindFirstChild(EventFolderName)
if (not EventFolder) then
EventFolder = Instance.new("Folder")
EventFolder.Name = EventFolderName
EventFolder.Archivable = false
EventFolder.Parent = EventFolderParent
end
local function validateMessageLength(msg)
if msg:len() > MAX_MESSAGE_LENGTH*MAX_BYTES_PER_CODEPOINT then
return false
end
if utf8.len(msg) == nil then
return false
end
if utf8.len(utf8.nfcnormalize(msg)) > MAX_MESSAGE_LENGTH then
return false
end
return true
end
--// No-opt connect Server>Client RemoteEvents to ensure they cannot be called
--// to fill the remote event queue.
local function emptyFunction()
--intentially empty
end
local function GetObjectWithNameAndType(parentObject, objectName, objectType)
for _, child in pairs(parentObject:GetChildren()) do
if (child:IsA(objectType) and child.Name == objectName) then
return child
end
end
return nil
end
local function CreateIfDoesntExist(parentObject, objectName, objectType)
local obj = GetObjectWithNameAndType(parentObject, objectName, objectType)
if (not obj) then
obj = Instance.new(objectType)
obj.Name = objectName
obj.Parent = parentObject
end
useEvents[objectName] = obj
return obj
end
--// All remote events will have a no-opt OnServerEvent connecdted on construction
local function CreateEventIfItDoesntExist(parentObject, objectName)
local obj = CreateIfDoesntExist(parentObject, objectName, "RemoteEvent")
obj.OnServerEvent:Connect(emptyFunction)
return obj
end
CreateEventIfItDoesntExist(EventFolder, "OnNewMessage")
CreateEventIfItDoesntExist(EventFolder, "OnMessageDoneFiltering")
CreateEventIfItDoesntExist(EventFolder, "OnNewSystemMessage")
CreateEventIfItDoesntExist(EventFolder, "OnChannelJoined")
CreateEventIfItDoesntExist(EventFolder, "OnChannelLeft")
CreateEventIfItDoesntExist(EventFolder, "OnMuted")
CreateEventIfItDoesntExist(EventFolder, "OnUnmuted")
CreateEventIfItDoesntExist(EventFolder, "OnMainChannelSet")
CreateEventIfItDoesntExist(EventFolder, "ChannelNameColorUpdated")
CreateEventIfItDoesntExist(EventFolder, "SayMessageRequest")
CreateEventIfItDoesntExist(EventFolder, "SetBlockedUserIdsRequest")
CreateIfDoesntExist(EventFolder, "GetInitDataRequest", "RemoteFunction")
CreateIfDoesntExist(EventFolder, "MutePlayerRequest", "RemoteFunction")
CreateIfDoesntExist(EventFolder, "UnMutePlayerRequest", "RemoteFunction")
EventFolder = useEvents
local function CreatePlayerSpeakerObject(playerObj)
--// If a developer already created a speaker object with the
--// name of a player and then a player joins and tries to
--// take that name, we first need to remove the old speaker object
local speaker = ChatService:GetSpeaker(playerObj.Name)
if (speaker) then
ChatService:RemoveSpeaker(playerObj.Name)
end
speaker = ChatService:InternalAddSpeakerWithPlayerObject(playerObj.Name, playerObj, false)
for _, channel in pairs(ChatService:GetAutoJoinChannelList()) do
speaker:JoinChannel(channel.Name)
end
speaker:InternalAssignEventFolder(EventFolder)
speaker.ChannelJoined:connect(function(channel, welcomeMessage)
local log = nil
local channelNameColor = nil
local channelObject = ChatService:GetChannel(channel)
if (channelObject) then
log = channelObject:GetHistoryLogForSpeaker(speaker)
channelNameColor = channelObject.ChannelNameColor
end
EventFolder.OnChannelJoined:FireClient(playerObj, channel, welcomeMessage, log, channelNameColor)
end)
speaker.Muted:connect(function(channel, reason, length)
EventFolder.OnMuted:FireClient(playerObj, channel, reason, length)
end)
speaker.Unmuted:connect(function(channel)
EventFolder.OnUnmuted:FireClient(playerObj, channel)
end)
ChatService:InternalFireSpeakerAdded(speaker.Name)
end
EventFolder.SayMessageRequest.OnServerEvent:connect(function(playerObj, message, channel)
if type(message) ~= "string" then
return
elseif not validateMessageLength(message) then
return
end
if type(channel) ~= "string" then
return
end
local speaker = ChatService:GetSpeaker(playerObj.Name)
if (speaker) then
return speaker:SayMessage(message, channel)
end
return nil
end)
EventFolder.MutePlayerRequest.OnServerInvoke = function(playerObj, muteSpeakerName)
if type(muteSpeakerName) ~= "string" then
return
end
local speaker = ChatService:GetSpeaker(playerObj.Name)
if speaker then
local muteSpeaker = ChatService:GetSpeaker(muteSpeakerName)
if muteSpeaker then
speaker:AddMutedSpeaker(muteSpeaker.Name)
return true
end
end
return false
end
EventFolder.UnMutePlayerRequest.OnServerInvoke = function(playerObj, unmuteSpeakerName)
if type(unmuteSpeakerName) ~= "string" then
return
end
local speaker = ChatService:GetSpeaker(playerObj.Name)
if speaker then
local unmuteSpeaker = ChatService:GetSpeaker(unmuteSpeakerName)
if unmuteSpeaker then
speaker:RemoveMutedSpeaker(unmuteSpeaker.Name)
return true
end
end
return false
end
-- Map storing Player -> Blocked user Ids.
local BlockedUserIdsMap = {}
PlayersService.PlayerAdded:connect(function(newPlayer)
for player, blockedUsers in pairs(BlockedUserIdsMap) do
local speaker = ChatService:GetSpeaker(player.Name)
if speaker then
for i = 1, #blockedUsers do
local blockedUserId = blockedUsers[i]
if blockedUserId == newPlayer.UserId then
speaker:AddMutedSpeaker(newPlayer.Name)
end
end
end
end
end)
PlayersService.PlayerRemoving:connect(function(removingPlayer)
BlockedUserIdsMap[removingPlayer] = nil
end)
EventFolder.SetBlockedUserIdsRequest.OnServerEvent:Connect(function(player, blockedUserIdsList)
if type(blockedUserIdsList) ~= "table" then
return
end
local prunedBlockedUserIdsList = {}
local speaker = ChatService:GetSpeaker(player.Name)
if speaker then
for i = 1, math.min(#blockedUserIdsList, MAX_BLOCKED_SPEAKERS_PER_REQ) do
if type(blockedUserIdsList[i]) == "number" then
table.insert(prunedBlockedUserIdsList, blockedUserIdsList[i])
local blockedPlayer = PlayersService:GetPlayerByUserId(blockedUserIdsList[i])
if blockedPlayer then
speaker:AddMutedSpeaker(blockedPlayer.Name)
end
end
end
-- We only want to store the first
-- MAX_BLOCKED_SPEAKERS_PER_REQ number of ids as needed
BlockedUserIdsMap[player] = prunedBlockedUserIdsList
end
end)
EventFolder.GetInitDataRequest.OnServerInvoke = (function(playerObj)
local speaker = ChatService:GetSpeaker(playerObj.Name)
if not (speaker and speaker:GetPlayer()) then
CreatePlayerSpeakerObject(playerObj)
speaker = ChatService:GetSpeaker(playerObj.Name)
end
local data = {}
data.Channels = {}
data.SpeakerExtraData = {}
for _, channelName in pairs(speaker:GetChannelList()) do
local channelObj = ChatService:GetChannel(channelName)
if (channelObj) then
local channelData =
{
channelName,
channelObj:GetWelcomeMessageForSpeaker(speaker),
channelObj:GetHistoryLogForSpeaker(speaker),
channelObj.ChannelNameColor,
}
table.insert(data.Channels, channelData)
end
end
for _, oSpeakerName in pairs(ChatService:GetSpeakerList()) do
local oSpeaker = ChatService:GetSpeaker(oSpeakerName)
data.SpeakerExtraData[oSpeakerName] = oSpeaker.ExtraData
end
return data
end)
local function DoJoinCommand(speakerName, channelName, fromChannelName)
local speaker = ChatService:GetSpeaker(speakerName)
local channel = ChatService:GetChannel(channelName)
if (speaker) then
if (channel) then
if (channel.Joinable) then
if (not speaker:IsInChannel(channel.Name)) then
speaker:JoinChannel(channel.Name)
else
speaker:SetMainChannel(channel.Name)
local msg = ChatLocalization:FormatMessageToSend(
"GameChat_SwitchChannel_NowInChannel",
string.format("You are now chatting in channel: '%s'", channel.Name),
"RBX_NAME",
channel.Name)
speaker:SendSystemMessage(msg, channel.Name)
end
else
local msg = ChatLocalization:FormatMessageToSend(
"GameChat_ChatServiceRunner_YouCannotJoinChannel",
"You cannot join channel '" .. channelName .. "'.",
"RBX_NAME",
channelName)
speaker:SendSystemMessage(msg, fromChannelName)
end
else
local msg = ChatLocalization:FormatMessageToSend(
"GameChat_ChatServiceRunner_ChannelDoesNotExist",
"Channel '" .. channelName .. "' does not exist.",
"RBX_NAME",
channelName)
speaker:SendSystemMessage(msg, fromChannelName)
end
end
end
local function DoLeaveCommand(speakerName, channelName, fromChannelName)
local speaker = ChatService:GetSpeaker(speakerName)
local channel = ChatService:GetChannel(channelName)
if (speaker) then
if (speaker:IsInChannel(channelName)) then
if (channel.Leavable) then
speaker:LeaveChannel(channel.Name)
local msg = ChatLocalization:FormatMessageToSend(
"GameChat_ChatService_YouHaveLeftChannel",
string.format("You have left channel '%s'", channelName),
"RBX_NAME",
channel.Name)
speaker:SendSystemMessage(msg, "System")
else
local msg = ChatLocalization:FormatMessageToSend(
"GameChat_ChatServiceRunner_YouCannotLeaveChannel",
("You cannot leave channel '" .. channelName .. "'."),
"RBX_NAME",
channelName)
speaker:SendSystemMessage(msg, fromChannelName)
end
else
local msg = ChatLocalization:FormatMessageToSend(
"GameChat_ChatServiceRunner_YouAreNotInChannel",
("You are not in channel '" .. channelName .. "'."),
"RBX_NAME",
channelName)
speaker:SendSystemMessage(msg, fromChannelName)
end
end
end
ChatService:RegisterProcessCommandsFunction("default_commands", function(fromSpeaker, message, channel)
if (string.sub(message, 1, 6):lower() == "/join ") then
DoJoinCommand(fromSpeaker, string.sub(message, 7), channel)
return true
elseif (string.sub(message, 1, 3):lower() == "/j ") then
DoJoinCommand(fromSpeaker, string.sub(message, 4), channel)
return true
elseif (string.sub(message, 1, 7):lower() == "/leave ") then
DoLeaveCommand(fromSpeaker, string.sub(message, 8), channel)
return true
elseif (string.sub(message, 1, 3):lower() == "/l ") then
DoLeaveCommand(fromSpeaker, string.sub(message, 4), channel)
return true
end
return false
end)
if ChatSettings.GeneralChannelName and ChatSettings.GeneralChannelName ~= "" then
local allChannel = ChatService:AddChannel(ChatSettings.GeneralChannelName)
allChannel.Leavable = false
allChannel.AutoJoin = true
allChannel:RegisterGetWelcomeMessageFunction(function(speaker)
if RunService:IsStudio() then
return nil
end
local player = speaker:GetPlayer()
if player then
local success, canChat = pcall(function()
return Chat:CanUserChatAsync(player.UserId)
end)
if success and not canChat then
return ""
end
end
end)
end
local systemChannel = ChatService:AddChannel("System")
systemChannel.Leavable = false
systemChannel.AutoJoin = true
systemChannel.WelcomeMessage = ChatLocalization:FormatMessageToSend(
"GameChat_ChatServiceRunner_SystemChannelWelcomeMessage", "This channel is for system and game notifications."
)
systemChannel.SpeakerJoined:connect(function(speakerName)
systemChannel:MuteSpeaker(speakerName)
end)
local function TryRunModule(module)
if module:IsA("ModuleScript") then
local ret = require(module)
if (type(ret) == "function") then
ret(ChatService)
end
end
end
local modules = Chat:WaitForChild("ChatModules")
modules.ChildAdded:connect(function(child)
local success, returnval = pcall(TryRunModule, child)
if not success and returnval then
print("Error running module " ..child.Name.. ": " ..returnval)
end
end)
for _, module in pairs(modules:GetChildren()) do
local success, returnval = pcall(TryRunModule, module)
if not success and returnval then
print("Error running module " ..module.Name.. ": " ..returnval)
end
end
PlayersService.PlayerRemoving:connect(function(playerObj)
if (ChatService:GetSpeaker(playerObj.Name)) then
ChatService:RemoveSpeaker(playerObj.Name)
end
end)
@@ -0,0 +1,952 @@
return {
{
Key = "GameChat_ChatCommandsTeller_AllChannelWelcomeMessage",
Values = {
["ru"] = "Введите «/?» или «/help», чтобы увидеть список команд чата.",
["fr"] = "Dans le chat, « /? » ou « /help » pour la liste des commandes du chat.",
["en-us"] = "Chat '/?' or '/help' for a list of chat commands.",
["de"] = "Gib „/?“ oder „/help“ im Chat ein, um eine Liste der Chatbefehle zu erhalten.",
["it"] = "Scrivi \"/?\" o \"/help\" per avere l'elenco dei comandi della chat.",
["pt"] = "Digite '/?' ou '/help' no chat para ver uma lista de comandos.",
["ja"] = "チャットで 「/?」 または 「/help」を入力するとチャットコマンドの一覧を表示します。",
["es"] = "Envía \"/?\" o \"/help\" para obtener la lista de comandos del chat.",
["es-es"] = "Envía \"/?\" o \"/help\" para obtener la lista de comandos del chat.",
["pt-br"] = "Digite '/?' ou '/help' no chat para ver uma lista de comandos.",
["ko"] = "채팅창에 '/?' 또는 '/도움말'을 입력하면 채팅 명령어 목록을 볼 수 있어요.",
["zh-tw"] = "輸入「/?」或「/help」取得聊天指令清單。",
["zh-cn"] = "输入 '/?' 或 '/help' 可获取聊天指令清单。",
["zh-cjv"] = "输入 '/?' 或 '/help' 可获取聊天指令清单。",
}
},
{
Key = "GameChat_SwallowGuestChat_Message",
Values = {
["ru"] = "Создайте бесплатную учетную запись, чтобы настроить права доступа в чате!",
["fr"] = "Créez un compte gratuit pour accéder aux permissions de chat !",
["en-us"] = "Create a free account to get access to chat permissions!",
["de"] = "Erstelle ein kostenloses Konto, um Zugriff auf Chatberechtigungen zu erhalten!",
["it"] = "Crea un account gratuito per avere accesso ai permessi della chat!",
["pt"] = "Crie uma conta grátis para ter acesso a permissões de chat!",
["ja"] = "フリーアカウントを作ってチャット権限にアクセス!",
["es"] = "¡Crea una cuenta gratuita para obtener los permisos de acceso al chat!",
["es-es"] = "¡Crea una cuenta gratuita para obtener los permisos de acceso al chat!",
["pt-br"] = "Crie uma conta grátis para ter acesso a permissões de chat!",
["ko"] = "무료 계정을 생성해 채팅 권한을 이용하세요!",
["zh-tw"] = "若要使用聊天功能,請建立免費帳號。",
["zh-cn"] = "创建免费帐户以获取聊天权限!",
["zh-cjv"] = "创建免费帐户以获取聊天权限!",
}
},
{
Key = "GameChat_ChatCommandsTeller_SwitchChannelCommand",
Values = {
["ru"] = "/c <канал> : переключить вкладку в меню каналов.",
["fr"] = "/c <canal> : échanger les onglets du menu Canal.",
["en-us"] = "/c <channel> : switch channel menu tabs.",
["de"] = "/c <Kanal> : Zum Wechseln zwischen Kanalmenüreitern.",
["it"] = "/c <canale>: cambia scheda nel menu dei canali.",
["pt"] = "/c <canal> : trocar abas de menu de canal.",
["ja"] = "/c <チャンネル> : チャンネルメニュータブを切り替える。",
["es"] = "/c <channel>: alternar pestañas del menú del chat.",
["es-es"] = "/c <channel>: alternar pestañas del menú del chat.",
["pt-br"] = "/c <canal> : trocar abas de menu de canal.",
["ko"] = "/c <채널> : 채널 메뉴 탭 전환.",
["zh-tw"] = "/c <頻道名稱>:切換頻道選單標籤。",
["zh-cn"] = "/c <频道> : 切换频道菜单标签。",
["zh-cjv"] = "/c <频道> : 切换频道菜单标签。",
}
},
{
Key = "GameChat_ChatCommandsTeller_MeCommand",
Values = {
["ru"] = "/me <текст> : сообщить о выполнении какого-либо действия.",
["fr"] = "/me <texte> : commande jeu de rôle pour accomplir des actions.",
["en-us"] = "/me <text> : roleplaying command for doing actions.",
["de"] = "/me <Text> : Rollenspielbefehl für Aktionen.",
["it"] = "/me <testo>: comando per descrivere azioni e giocare di ruolo.",
["pt"] = "/me <texto> : comando de RPG para realizar ações.",
["ja"] = "/me <テキスト> : アクションのためのロールプレイングコマンド。",
["es"] = "/me <text>: comando de rol para realizar acciones.",
["es-es"] = "/me <text>: comando de rol para realizar acciones.",
["pt-br"] = "/me <texto> : comando de RPG para realizar ações.",
["ko"] = "/me <텍스트> : 작업 수행을 위한 역할 놀이 명령어.",
["zh-tw"] = "/me <文字>:角色扮演動作的指令。",
["zh-cn"] = "/me <指令> : 做动作的角色扮演指令。",
["zh-cjv"] = "/me <指令> : 做动作的角色扮演指令。",
}
},
{
Key = "GameChat_ChatCommandsTeller_MuteCommand",
Values = {
["ru"] = "/mute <пользователь> : игнорировать пользователя.",
["fr"] = "/mute <interlocuteur> : bâillonne un interlocuteur.",
["en-us"] = "/mute <speaker> : mute a speaker.",
["de"] = "/mute <Teilnehmer> : Schaltet einen Teilnehmer stumm.",
["it"] = "/mute <giocatore>: togli la parola a un giocatore.",
["pt"] = "/mute <pessoa> : silenciar uma pessoa.",
["ja"] = "/mute <スピーカー> : 相手をミュート。",
["es"] = "/mute <usuario>: silenciar a un usuario.",
["es-es"] = "/mute <usuario>: silenciar a un usuario.",
["pt-br"] = "/mute <pessoa> : silenciar uma pessoa.",
["ko"] = "/mute <스피커> : 스피커 음소거.",
["zh-tw"] = "/mute <使用者名稱> : 將此使用者靜音。",
["zh-cn"] = "/mute <发言者>:将发言者禁言。",
["zh-cjv"] = "/mute <发言者>:将发言者禁言。",
}
},
{
Key = "GameChat_ChatCommandsTeller_UnMuteCommand",
Values = {
["ru"] = "/unmute <пользователь> : перестать игнорировать пользователя.",
["fr"] = "/unmute <interlocuteur> : retire le bâillon d'un interlocuteur.",
["en-us"] = "/unmute <speaker> : unmute a speaker.",
["de"] = "/unmute <Teilnehmer> : Hebt Stummschaltung eines Teilnehmers auf.",
["it"] = "/unmute <giocatore>: restituisci la parola a un giocatore.",
["pt"] = "/unmute <pessoa> : remover silêncio de uma pessoa.",
["ja"] = "/unmute <スピーカー> : 相手のミュートを解除.",
["es"] = "/unmute <speaker>: cancelar silencio de un usuario.",
["es-es"] = "/unmute <speaker>: cancelar silencio de un usuario.",
["pt-br"] = "/unmute <pessoa> : remover silêncio de uma pessoa.",
["ko"] = "/unmute <스피커> : 스피커 음소거 해제.",
["zh-tw"] = "/unmute <使用者名稱> : 將此使用者解除靜音。",
["zh-cn"] = "/unmute <发言者> : 取消发言者禁言。",
["zh-cjv"] = "/unmute <发言者> : 取消发言者禁言。",
}
},
{
Key = "GameChat_ChatCommandsTeller_WhisperCommand",
Values = {
["ru"] = "/whisper <пользователь> или /w <пользователь> : открыть канал для личной переписки с пользователем.",
["fr"] = "/whisper <interlocuteur> ou /w <interlocuteur> : ouvre un canal de discussion privé avec l'interlocuteur.",
["en-us"] = "/whisper <speaker> or /w <speaker> : open private message channel with speaker.",
["de"] = "/whisper <Teilnehmer> oder /w <Teilnehmer> : Öffnet privaten Nachrichtenkanal mit Teilnehmer.",
["it"] = "/whisper <giocatore> o /w <giocatore>: apri un canale privato con un giocatore.",
["pt"] = "/whisper <pessoa> ou /w <pessoa> : abrir um canal de mensagem privada com uma pessoa.",
["ja"] = "/whisper <スピーカー> または /w <スピーカー> : プライベートメッセージチャンネルを開く",
["es"] = "/whisper <usuario> o /w <usuario>: abrir canal de mensajes privados con un usuario.",
["es-es"] = "/whisper <usuario> o /w <usuario>: abrir canal de mensajes privados con un usuario.",
["pt-br"] = "/whisper <pessoa> ou /w <pessoa> : abrir um canal de mensagem privada com uma pessoa.",
["ko"] = "/whisper <스피커> 또는 /w <스피커> : 스피커 채널에서 비공개 메시지 열기.",
["zh-tw"] = "/whisper <使用者名稱> 或 /w <使用者名稱> : 與此使用者開啟私人訊息頻道。",
["zh-cn"] = "/whisper <发言者> 或 /w <发言者> : 与此发言者开启私人消息频道。",
["zh-cjv"] = "/whisper <发言者> 或 /w <发言者> : 与此发言者开启私人消息频道。",
}
},
{
Key = "GameChat_ChatMain_SpeakerHasBeenMuted",
Values = {
["ru"] = "Пользователь {RBX_NAME} добавлен в список игнорируемых.",
["fr"] = "L'interlocuteur {RBX_NAME} a été bâillonné.",
["en-us"] = "Speaker '{RBX_NAME}' has been muted.",
["de"] = "Teilnehmer „{RBX_NAME}“ wurde stummgeschaltet.",
["it"] = "Hai tolto la parola al giocatore \"{RBX_NAME}\".",
["pt"] = "{RBX_NAME}' foi silenciado(a).",
["ja"] = "「{RBX_NAME}」をミュートしました。",
["es"] = "Se ha silenciado al usuario \"{RBX_NAME}\".",
["es-es"] = "Se ha silenciado al usuario \"{RBX_NAME}\".",
["pt-br"] = "{RBX_NAME}' foi silenciado(a).",
["ko"] = "스피커 '{RBX_NAME}'이(가) 음소거되었어요.",
["zh-tw"] = "已將使用者「{RBX_NAME}」靜音。",
["zh-cn"] = "发言者“{RBX_NAME}”已被禁言。",
["zh-cjv"] = "发言者“{RBX_NAME}”已被禁言。",
}
},
{
Key = "GameChat_ChatMain_SpeakerHasBeenUnMuted",
Values = {
["ru"] = "Пользователь {RBX_NAME} удален из списка игнорируемых.",
["fr"] = "L'interlocuteur {RBX_NAME} n'est plus bâillonné.",
["en-us"] = "Speaker '{RBX_NAME}' has been unmuted.",
["de"] = "Stummschaltung von Teilnehmer „{RBX_NAME}“ wurde aufgehoben.",
["it"] = "Hai restituito la parola al giocatore \"{RBX_NAME}\".",
["pt"] = "O silêncio de '{RBX_NAME}' foi removido.",
["ja"] = "「{RBX_NAME}」のミュートを解除しました。",
["es"] = "Se ha cancelado el silencio del usuario \"{RBX_NAME}\".",
["es-es"] = "Se ha cancelado el silencio del usuario \"{RBX_NAME}\".",
["pt-br"] = "O silêncio de '{RBX_NAME}' foi removido.",
["ko"] = "스피커 '{RBX_NAME}'의 음소거가 해제되었어요.",
["zh-tw"] = "已將使用者「{RBX_NAME}」解除靜音。",
["zh-cn"] = "发言者“{RBX_NAME}”已被取消禁言。",
["zh-cjv"] = "发言者“{RBX_NAME}”已被取消禁言。",
}
},
{
Key = "GameChat_ChatCommandsTeller_Desc",
Values = {
["ru"] = "Это основные команды чата.",
["fr"] = "Voici les commandes de chat basiques.",
["en-us"] = "These are the basic chat commands.",
["de"] = "Das sind die grundlegenden Chatbefehle.",
["it"] = "Questi sono i comandi base della chat.",
["pt"] = "Esses são os comandos de chat básicos.",
["ja"] = "これらは基本的なチャットコマンドです。",
["es"] = "Estos son los comandos básicos del chat.",
["es-es"] = "Estos son los comandos básicos del chat.",
["pt-br"] = "Esses são os comandos de chat básicos.",
["ko"] = "기본 채팅 명령어에요.",
["zh-tw"] = "以下是基本聊天室指令。",
["zh-cn"] = "这些是基本聊天指令。",
["zh-cjv"] = "这些是基本聊天指令。",
}
},
{
Key = "GameChat_GetVersion_Message",
Values = {
["ru"] = "Эта игра поддерживает чат версии [{RBX_NUMBER}.{RBX_NUMBER}].",
["fr"] = "Le jeu utilise la version de chat [{RBX_NUMBER} {RBX_NUMBER}].",
["en-us"] = "This game is running chat version [{RBX_NUMBER}.{RBX_NUMBER}].",
["de"] = "Die Chatversion dieses Spiels ist [{RBX_NUMBER}.{RBX_NUMBER}].",
["it"] = "Questo gioco usa la versione [{RBX_NUMBER}.{RBX_NUMBER}] della chat.",
["pt"] = "Este jogo está rodando a versão de chat [{RBX_NUMBER}.{RBX_NUMBER}].",
["ja"] = "このゲームはチャットバージョン [{RBX_NUMBER}.{RBX_NUMBER}]を実行しています。",
["es"] = "Este juego utiliza la versión del chat [{RBX_NUMBER} {RBX_NUMBER}].",
["es-es"] = "Este juego utiliza la versión del chat [{RBX_NUMBER} {RBX_NUMBER}].",
["pt-br"] = "Este jogo está rodando a versão de chat [{RBX_NUMBER}.{RBX_NUMBER}].",
["ko"] = "이 게임은 채팅 버전 [{RBX_NUMBER}. {RBX_NUMBER}]을(를) 실행합니다.",
["zh-tw"] = "此遊戲正在使用聊天室版本 [{RBX_NUMBER}.{RBX_NUMBER}]。",
["zh-cn"] = "此游戏正在运行聊天版本 [{RBX_NUMBER}.{RBX_NUMBER}]。",
["zh-cjv"] = "此游戏正在运行聊天版本 [{RBX_NUMBER}.{RBX_NUMBER}]。",
}
},
{
Key = "GameChat_SwitchChannel_NowInChannel",
Values = {
["ru"] = "Вы общаетесь на канале «{RBX_NAME}»",
["fr"] = "Maintenant, vous discutez sur le canal : {RBX_NAME}",
["en-us"] = "You are now chatting in channel: '{RBX_NAME}'",
["de"] = "Du chattest jetzt in Kanal „{RBX_NAME}“.",
["it"] = "Ora stai parlando nel canale: \"{RBX_NAME}\"",
["pt"] = "Você agora está no canal de chat: '{RBX_NAME}'",
["ja"] = "あなたの現在のチャットチャンネルは: 「{RBX_NAME}」です。",
["es"] = "Estás chateando en el canal \"{RBX_NAME}\".",
["es-es"] = "Estás chateando en el canal \"{RBX_NAME}\".",
["pt-br"] = "Você agora está no canal de chat: '{RBX_NAME}'",
["ko"] = "'{RBX_NAME}' 채널에서 채팅 중이에요",
["zh-tw"] = "您目前在「{RBX_NAME}」頻道聊天",
["zh-cn"] = "你当前的聊天频道为:“{RBX_NAME}”",
["zh-cjv"] = "你当前的聊天频道为:“{RBX_NAME}”",
}
},
{
Key = "GameChat_SwitchChannel_NotInChannel",
Values = {
["ru"] = "Вы не на канале «{RBX_NAME}»",
["fr"] = "Vous n'êtes pas sur le canal : {RBX_NAME}",
["en-us"] = "You are not in channel: '{RBX_NAME}'",
["de"] = "Du befindest dich nicht in Kanal „{RBX_NAME}“.",
["it"] = "Non ti trovi nel canale: \"{RBX_NAME}\"",
["pt"] = "Você não está no canal: '{RBX_NAME}'",
["ja"] = "あなたはチャネル: 「{RBX_NAME}」にいません。",
["es"] = "No estás en el canal \"{RBX_NAME}\".",
["es-es"] = "No estás en el canal \"{RBX_NAME}\".",
["pt-br"] = "Você não está no canal: '{RBX_NAME}'",
["ko"] = "{RBX_NAME}' 채널에 있지 않아요",
["zh-tw"] = "您不在「{RBX_NAME}」頻道",
["zh-cn"] = "你不在频道:“{RBX_NAME}”",
["zh-cjv"] = "你不在频道:“{RBX_NAME}”",
}
},
{
Key = "GameChat_ChatMain_ChatBarText",
Values = {
["ru"] = "Чтобы общаться в чате, нажмите здесь или на клавишу «/»",
["fr"] = "Pour discuter, cliquez ici ou sur la touche « / »",
["en-us"] = "To chat click here or press \"/\" key",
["de"] = "Klicke zum Chatten hier oder drücke die „/“-Taste.",
["it"] = "Per chattare, clicca qui o premi il tasto \"/\"",
["pt"] = "Para escrever clique aqui ou aperte a tecla \"/\"",
["ja"] = "チャットするにはここをクリックするか 「 / 」 キーを押します。",
["es"] = "Para chatear, haz clic aquí o pulsa la tecla \"/\".",
["es-es"] = "Para chatear, haz clic aquí o pulsa la tecla \"/\".",
["pt-br"] = "Para escrever clique aqui ou aperte a tecla \"/\"",
["ko"] = "여기를 클릭하거나 \"/\" 키를 누른 후 채팅을 시작하세요",
["zh-tw"] = "若要聊天,請按下此處或「/」鍵",
["zh-cn"] = "若要聊天,请点按此处或按下“/”键",
["zh-cjv"] = "若要聊天,请点按此处或按下“/”键",
}
},
{
Key = "GameChat_ChatMain_ChatBarTextTouch",
Values = {
["ru"] = "Коснитесь здесь, чтобы общаться в чате",
["fr"] = "Touchez ici pour discuter",
["en-us"] = "Tap here to chat",
["de"] = "Tippe zum Chatten hier.",
["it"] = "Tocca qui per chattare",
["pt"] = "Toque aqui para escrever",
["ja"] = "ここをタップしてチャットする",
["es"] = "Toca aquí para chatear",
["es-es"] = "Toca aquí para chatear",
["pt-br"] = "Toque aqui para escrever",
["ko"] = "여기를 클릭한 후 내용을 입력하세요",
["zh-tw"] = "按下此處聊天",
["zh-cn"] = "轻点此处以聊天",
["zh-cjv"] = "轻点此处以聊天",
}
},
{
Key = "GameChat_ChatMain_SpeakerHasBeenBlocked",
Values = {
["ru"] = "Пользователь {RBX_NAME} заблокирован.",
["fr"] = "L'interlocuteur {RBX_NAME} a été bloqué.",
["en-us"] = "Speaker '{RBX_NAME}' has been blocked.",
["de"] = "Teilnehmer „{RBX_NAME}“ wurde gesperrt.",
["it"] = "Hai bloccato il giocatore \"{RBX_NAME}\".",
["pt"] = "{RBX_NAME}' foi bloqueado.",
["ja"] = "「{RBX_NAME}」はブロック中です。",
["es"] = "Se ha bloqueado al usuario \"{RBX_NAME}\".",
["es-es"] = "Se ha bloqueado al usuario \"{RBX_NAME}\".",
["pt-br"] = "{RBX_NAME}' foi bloqueado.",
["ko"] = "스피커 '{RBX_NAME}'님을 차단했어요.",
["zh-tw"] = "已封鎖使用者「{RBX_NAME}」。",
["zh-cn"] = "发言者“{RBX_NAME}”已被屏蔽。",
["zh-cjv"] = "发言者“{RBX_NAME}”已被屏蔽。",
}
},
{
Key = "GameChat_ChatMain_SpeakerHasBeenUnBlocked",
Values = {
["ru"] = "Пользователь {RBX_NAME} разблокирован.",
["fr"] = "L'interlocuteur {RBX_NAME} n'est plus bloqué.",
["en-us"] = "Speaker '{RBX_NAME}' has been unblocked.",
["de"] = "Sperrung von Teilnehmer „{RBX_NAME}“ wurde aufgehoben.",
["it"] = "Hai sbloccato il giocatore \"{RBX_NAME}\".",
["pt"] = "{RBX_NAME}' foi desbloqueado.",
["ja"] = "「{RBX_NAME}」のブロックが解除されました。",
["es"] = "Se ha desbloqueado al usuario \"{RBX_NAME}\".",
["es-es"] = "Se ha desbloqueado al usuario \"{RBX_NAME}\".",
["pt-br"] = "{RBX_NAME}' foi desbloqueado.",
["ko"] = "스피커 '{RBX_NAME}'님 차단을 해제했어요.",
["zh-tw"] = "已解除封鎖使用者「{RBX_NAME}」。",
["zh-cn"] = "发言者“{RBX_NAME}”已被取消屏蔽。",
["zh-cjv"] = "发言者“{RBX_NAME}”已被取消屏蔽。",
}
},
{
Key = "GameChat_ChatCommandsTeller_TeamCommand",
Values = {
["ru"] = "/team <сообщение> или /t <сообщение> : отправить сообщение игрокам из вашей команды.",
["fr"] = "/team <message> ou /t <message> : envoyer un message aux joueurs de votre équipe.",
["en-us"] = "/team <message> or /t <message> : send a team chat to players on your team.",
["de"] = "/team <Nachricht> oder /t <Nachricht> : Sendet eine Teamnachricht an Spieler deines Teams.",
["it"] = "/team <messaggio> o /t <messaggio>: invia un messaggio a tutti i giocatori della tua squadra.",
["pt"] = "/team <mensagem> ou /t <mensagem> : enviar um chat de equipe aos jogadores da sua equipe.",
["ja"] = "/team <メッセージ> または /t <メッセージ> : 自分のチームメンバーにチームチャットを送る。",
["es"] = "/team <message> o /t <message>: enviar un mensaje de chat de equipo a los jugadores de tu equipo.",
["es-es"] = "/team <message> o /t <message>: enviar un mensaje de chat de equipo a los jugadores de tu equipo.",
["pt-br"] = "/team <mensagem> ou /t <mensagem> : enviar um chat de equipe aos jogadores da sua equipe.",
["ko"] = "/team <메시지> 또는 /t <메시지> : 팀 내 플레이어에게 팀 채팅 전송.",
["zh-tw"] = "/team <訊息> 或 /t <訊息> : 傳送訊息給隊伍中的玩家。",
["zh-cn"] = "/team <消息> 或 /t <消息> : 向你团队的玩家发送团队聊天。",
["zh-cjv"] = "/team <消息> 或 /t <消息> : 向你团队的玩家发送团队聊天。",
}
},
{
Key = "GameChat_ChatFloodDetector_MessageDisplaySeconds",
Values = {
["ru"] = "Вы сможете отправить новое сообщение только через {RBX_NUMBER} сек.!",
["fr"] = "Vous devez attendre {RBX_NUMBER} secondes avant d'envoyer un autre message !",
["en-us"] = "You must wait {RBX_NUMBER} seconds before sending another message!",
["de"] = "Du musst {RBX_NUMBER} Sekunden lang warten, bevor du eine weitere Nachricht senden kannst!",
["it"] = "Devi aspettare {RBX_NUMBER} secondi prima di inviare un altro messaggio!",
["pt"] = "Você precisa esperar {RBX_NUMBER} segundos antes de enviar outra mensagem!",
["ja"] = "{RBX_NUMBER} 秒待ってから次のメッセージを送ってください!",
["es"] = "¡Debes esperar {RBX_NUMBER} segundos antes de enviar otro mensaje!",
["es-es"] = "¡Debes esperar {RBX_NUMBER} segundos antes de enviar otro mensaje!",
["pt-br"] = "Você precisa esperar {RBX_NUMBER} segundos antes de enviar outra mensagem!",
["ko"] = "추가 메시지를 보내기 전에 {RBX_NUMBER}초 동안 기다려야 해요!",
["zh-tw"] = "請 {RBX_NUMBER} 秒後再傳送訊息。",
["zh-cn"] = "发送另一条消息前你必须等待 {RBX_NUMBER} 秒!",
["zh-cjv"] = "发送另一条消息前你必须等待 {RBX_NUMBER} 秒!",
}
},
{
Key = "GameChat_ChatFloodDetector_Message",
Values = {
["ru"] = "Необходимо подождать, прежде чем отправлять новое сообщение!",
["fr"] = "Vous devez attendre avant d'envoyer un autre message !",
["en-us"] = "You must wait before sending another message!",
["de"] = "Du musst warten, bevor du eine weitere Nachricht senden kannst!",
["it"] = "Devi aspettare prima di inviare un altro messaggio!",
["pt"] = "Você precisa esperar antes de enviar outra mensagem!",
["ja"] = "少し待ってから次のメッセージを送ってください!",
["es"] = "¡Debes esperar antes de enviar otro mensaje!",
["es-es"] = "¡Debes esperar antes de enviar otro mensaje!",
["pt-br"] = "Você precisa esperar antes de enviar outra mensagem!",
["ko"] = "추가 메시지를 보내기 전에 기다려야 해요!",
["zh-tw"] = "請稍後再傳送訊息。",
["zh-cn"] = "发送另一条消息前你必须等待!",
["zh-cjv"] = "发送另一条消息前你必须等待!",
}
},
{
Key = "GameChat_ChatMessageValidator_SettingsError",
Values = {
["ru"] = "В ваших настройках чата заблокирована возможность отправлять сообщения.",
["fr"] = "Vos paramètres de chat vous empêchent d'envoyer des messages.",
["en-us"] = "Your chat settings prevent you from sending messages.",
["de"] = "Aufgrund deiner Chateinstellungen kannst du keine Nachrichten senden.",
["it"] = "Non puoi inviare messaggi per le impostazioni della tua chat.",
["pt"] = "Suas configurações de chat impedem que você envie mensagens.",
["ja"] = "メッセージが送れないチャット設定です。",
["es"] = "Tu configuración de chat te impide enviar mensajes.",
["es-es"] = "Tu configuración de chat te impide enviar mensajes.",
["pt-br"] = "Suas configurações de chat impedem que você envie mensagens.",
["ko"] = "채팅 설정 때문에 메시지를 보낼 수 없어요.",
["zh-tw"] = "您的聊天設定禁止您傳送訊息。",
["zh-cn"] = "你的聊天设置禁止你发送消息。",
["zh-cjv"] = "你的聊天设置禁止你发送消息。",
}
},
{
Key = "GameChat_ChatMessageValidator_MaxLengthError",
Values = {
["ru"] = "Превышено максимально допустимое количество символов в сообщении.",
["fr"] = "Votre message dépasse la longueur maximale.",
["en-us"] = "Your message exceeds the maximum message length.",
["de"] = "Deine Nachricht überschreitet die zulässige Nachrichtenlänge.",
["it"] = "Il tuo messaggio supera la lunghezza massima consentita.",
["pt"] = "Sua mensagem ultrapassa o tamanho máximo de mensagem.",
["ja"] = "メッセージが最大文字数を超えています。",
["es"] = "Tu mensaje supera la longitud máxima permitida.",
["es-es"] = "Tu mensaje supera la longitud máxima permitida.",
["pt-br"] = "Sua mensagem ultrapassa o tamanho máximo de mensagem.",
["ko"] = "메시지 길이 한도를 초과했어요.",
["zh-tw"] = "您的訊息超過長度限制。",
["zh-cn"] = "你的消息已超过最大长度限制。",
["zh-cjv"] = "你的消息已超过最大长度限制。",
}
},
{
Key = "GameChat_ChatMessageValidator_WhitespaceError",
Values = {
["ru"] = "Ваше сообщение содержит недопустимый пробел.",
["fr"] = "Votre message contient des espaces blancs qui sont interdits.",
["en-us"] = "Your message contains whitespace that is not allowed.",
["de"] = "Deine Nachricht enthält unzulässige Leerräume.",
["it"] = "Il tuo messaggio contiene spazi vuoti non consentiti.",
["pt"] = "Sua mensagem contém um espaço em branco, que não é permitido.",
["ja"] = "メッセージに許可されていないスペースが含まれています。",
["es"] = "Tu mensaje contiene espacios vacíos que no se permiten.",
["es-es"] = "Tu mensaje contiene espacios vacíos que no se permiten.",
["pt-br"] = "Sua mensagem contém um espaço em branco, que não é permitido.",
["ko"] = "메시지에 허용되지 않는 여백이 있어요.",
["zh-tw"] = "訊息禁止使用空白字元。",
["zh-cn"] = "你的消息包含不被允许的空格。",
["zh-cjv"] = "你的消息包含不被允许的空格。",
}
},
{
Key = "GameChat_DoMuteCommand_CannotMuteSelf",
Values = {
["ru"] = "Невозможно добавить себя в список игнорируемых.",
["fr"] = "Vous ne pouvez pas vous bâillonner.",
["en-us"] = "You cannot mute yourself.",
["de"] = "Du kannst dich nicht selbst stummschalten.",
["it"] = "Non puoi togliere la parola a te stesso.",
["pt"] = "Você não pode silenciar a si mesmo.",
["ja"] = "自分をミュートすることは出来ません。",
["es"] = "No puedes silenciarte a ti mismo.",
["es-es"] = "No puedes silenciarte a ti mismo.",
["pt-br"] = "Você não pode silenciar a si mesmo.",
["ko"] = "자신을 음소거할 수 없어요.",
["zh-tw"] = "您無法將自己靜音。",
["zh-cn"] = "你无法将自己禁言。",
["zh-cjv"] = "你无法将自己禁言。",
}
},
{
Key = "GameChat_MuteSpeaker_SpeakerDoesNotExist",
Values = {
["ru"] = "Пользователя {RBX_NAME} не существует.",
["fr"] = "L'interlocuteur {RBX_NAME} n'existe pas.",
["en-us"] = "Speaker '{RBX_NAME}' does not exist.",
["de"] = "Teilnehmer „{RBX_NAME}“ existiert nicht.",
["it"] = "Il giocatore \"{RBX_NAME}\" non esiste.",
["pt"] = "{RBX_NAME}' não existe.",
["ja"] = "「{RBX_NAME}」は存在しません。",
["es"] = "El usuario \"{RBX_NAME}\" no existe.",
["es-es"] = "El usuario \"{RBX_NAME}\" no existe.",
["pt-br"] = "{RBX_NAME}' não existe.",
["ko"] = "스피커 '{RBX_NAME}'이(가) 없어요.",
["zh-tw"] = "使用者「{RBX_NAME}」不存在。",
["zh-cn"] = "发言者“{RBX_NAME}”不存在。",
["zh-cjv"] = "发言者“{RBX_NAME}”不存在。",
}
},
{
Key = "GameChat_PrivateMessaging_CannotChat",
Values = {
["ru"] = "Вы не можете общаться в чате с этим игроком.",
["fr"] = "Vous ne pouvez pas discuter avec ce joueur.",
["en-us"] = "You are not able to chat with this player.",
["de"] = "Du kannst mit diesem Spieler nicht chatten.",
["it"] = "Non puoi chattare con questo giocatore.",
["pt"] = "Você não pode participar de chat com este jogador.",
["ja"] = "このプレイヤーとチャットすることは出来ません。",
["es"] = "No puedes chatear con este jugador.",
["es-es"] = "No puedes chatear con este jugador.",
["pt-br"] = "Você não pode participar de chat com este jogador.",
["ko"] = "이 플레이어와 채팅할 수 없어요.",
["zh-tw"] = "您無法與此玩家聊天。",
["zh-cn"] = "你无法与此玩家聊天。",
["zh-cjv"] = "你无法与此玩家聊天。",
}
},
{
Key = "GameChat_PrivateMessaging_CannotWhisperToSelf",
Values = {
["ru"] = "Нельзя отправлять себе личные сообщения.",
["fr"] = "Vous ne pouvez pas murmurer à votre propre oreille.",
["en-us"] = "You cannot whisper to yourself.",
["de"] = "Du kannst dir nicht selbst etwas zuflüstern.",
["it"] = "Non puoi aprire un canale privato con te stesso.",
["pt"] = "Você não pode sussurrar para si mesmo.",
["ja"] = "自分自身に話しかけることは出来ません。",
["es"] = "No puedes enviarte mensajes privados a ti mismo.",
["es-es"] = "No puedes enviarte mensajes privados a ti mismo.",
["pt-br"] = "Você não pode sussurrar para si mesmo.",
["ko"] = "자신에게 귓속말할 수 없어요.",
["zh-tw"] = "您無法與自己開啟私人頻道。",
["zh-cn"] = "你无法与自己开启私人频道。",
["zh-cjv"] = "你无法与自己开启私人频道。",
}
},
{
Key = "GameChat_PrivateMessaging_NowChattingWith",
Values = {
["ru"] = "Открыт канал для личного общения с пользователем {RBX_NAME}",
["fr"] = "Maintenant, vous discutez en privé avec {RBX_NAME}",
["en-us"] = "You are now privately chatting with {RBX_NAME}",
["de"] = "Du unterhältst dich nun privat mit {RBX_NAME}.",
["it"] = "Ora stai chattando in privato con {RBX_NAME}",
["pt"] = "Você agora está em um chat privado com {RBX_NAME}",
["ja"] = "あなたは現在、{RBX_NAME} さんとプライベートチャット中です。",
["es"] = "Estás chateando en privado con {RBX_NAME}.",
["es-es"] = "Estás chateando en privado con {RBX_NAME}.",
["pt-br"] = "Você agora está em um chat privado com {RBX_NAME}",
["ko"] = "현재 {RBX_NAME}님과 비공개 채팅 중이에요",
["zh-tw"] = "您正在與{RBX_NAME}私聊",
["zh-cn"] = "你正在与“{RBX_NAME}”私聊",
["zh-cjv"] = "你正在与“{RBX_NAME}”私聊",
}
},
{
Key = "GameChat_TeamChat_WelcomeMessage",
Values = {
["ru"] = "Это канал для личного общения участников вашей команды.",
["fr"] = "Ceci est un canal privé entre les membres de votre équipe et vous.",
["en-us"] = "This is a private channel between you and your team members.",
["de"] = "Dies ist ein privater Kanal für dich und deine Teammitglieder.",
["it"] = "Questo è un canale privato tra te e i membri della tua squadra.",
["pt"] = "Este é um canal privado entre você e os membros da sua equipe.",
["ja"] = "これはあなたとあなたのチームメンバーとのプライベートチャンネルです。",
["es"] = "Este es un canal privado entre tú y los miembros de tu equipo.",
["es-es"] = "Este es un canal privado entre tú y los miembros de tu equipo.",
["pt-br"] = "Este é um canal privado entre você e os membros da sua equipe.",
["ko"] = "회원님과 팀원 간의 비공개 채널이에요.",
["zh-tw"] = "這是您與隊伍成員的私人頻道。",
["zh-cn"] = "这是你与团队成员之间的私人频道。",
["zh-cjv"] = "这是你与团队成员之间的私人频道。",
}
},
{
Key = "GameChat_TeamChat_CannotTeamChatIfNotInTeam",
Values = {
["ru"] = "Вы не можете общаться в командном чате, если не состоите в команде!",
["fr"] = "Vous ne pouvez pas avoir de discussion d'équipe si vous n'appartenez pas à une équipe !",
["en-us"] = "You cannot team chat if you are not on a team!",
["de"] = "Teamchat ist nur verfügbar, wenn du Mitglied eines Teams bist!",
["it"] = "Non puoi chattare con la squadra se non sei in una squadra!",
["pt"] = "Você não pode participar de chat de equipe se não estiver em uma!",
["ja"] = "チームに所属していなければチームチャットは出来ません。",
["es"] = "¡No puedes chatear con tu equipo si no formas parte de un equipo!",
["es-es"] = "¡No puedes chatear con tu equipo si no formas parte de un equipo!",
["pt-br"] = "Você não pode participar de chat de equipe se não estiver em uma!",
["ko"] = "팀에 속하지 않으면 팀 채팅을 이용할 수 없어요!",
["zh-tw"] = "若您不在隊伍中,您無法使用隊伍頻道。",
["zh-cn"] = "如果你不在该团队,则无法进行团队聊天。",
["zh-cjv"] = "如果你不在该团队,则无法进行团队聊天。",
}
},
{
Key = "GameChat_TeamChat_NowInTeam",
Values = {
["ru"] = "Вы вступили в команду {RBX_NAME}.",
["fr"] = "Vous êtes désormais dans l'équipe {RBX_NAME}.",
["en-us"] = "You are now on the '{RBX_NAME}' team.",
["de"] = "Du bist nun Mitglied im Team „{RBX_NAME}“.",
["it"] = "Ora sei nella squadra \"{RBX_NAME}\".",
["pt"] = "Você agora está na equipe '{RBX_NAME}'.",
["ja"] = "あなたは現在「{RBX_NAME}」チームに所属しています。",
["es"] = "Ahora formas parte del equipo \"{RBX_NAME}\".",
["es-es"] = "Ahora formas parte del equipo \"{RBX_NAME}\".",
["pt-br"] = "Você agora está na equipe '{RBX_NAME}'.",
["ko"] = "현재 '{RBX_NAME}'팀에 속해 있어요.",
["zh-tw"] = "您在「{RBX_NAME}」隊伍。",
["zh-cn"] = "你正在团队“{RBX_NAME}”中。",
["zh-cjv"] = "你正在团队“{RBX_NAME}”中。",
}
},
{
Key = "GameChat_ChatChannel_MutedInChannel",
Values = {
["ru"] = "Вы добавлены в список игнорируемых и не можете общаться на этом канале",
["fr"] = "Vous êtes bâillonné et ne pouvez pas parler sur ce canal",
["en-us"] = "You are muted and cannot talk in this channel",
["de"] = "Du wurdest stummgeschaltet und kannst in diesem Kanal nicht kommunizieren.",
["it"] = "Non hai più la parola e non puoi chattare in questo canale",
["pt"] = "Você está silenciado(a) e não pode falar neste canal",
["ja"] = "あなたはミュートされこのチャンネルで話すことは出来ません。",
["es"] = "Se te ha silenciado y no puedes hablar en este canal.",
["es-es"] = "Se te ha silenciado y no puedes hablar en este canal.",
["pt-br"] = "Você está silenciado(a) e não pode falar neste canal",
["ko"] = "이 채널에서 음소거되어 이야기할 수 없어요",
["zh-tw"] = "您遭到靜音,無法在此頻道聊天",
["zh-cn"] = "你已被禁言,无法在此频道聊天",
["zh-cjv"] = "你已被禁言,无法在此频道聊天",
}
},
{
Key = "GameChat_ChatService_ChatFilterIssues",
Values = {
["ru"] = "Могут возникать задержки в передаче сообщений из-за проблем с фильтром чата.",
["fr"] = "Le filtre de chat connaît actuellement des problèmes et les messages pourraient mettre du temps à apparaître.",
["en-us"] = "The chat filter is currently experiencing issues and messages may be slow to appear.",
["de"] = "Es gibt derzeit Probleme mit dem Chatfilter. Nachrichten können deshalb mit Verzögerung angezeigt werden.",
["it"] = "Il filtro della chat sta riscontrando dei problemi e i messaggi potrebbero apparire in ritardo.",
["pt"] = "O filtro de chat está com problemas no momento e as mensagens podem demorar para aparecer.",
["ja"] = "現在、チャットフィルターに問題があるためメッセージの表示が遅れています。",
["es"] = "El filtro del chat sufre problemas en este momento y es posible que los mensajes tarden un poco en aparecer.",
["es-es"] = "El filtro del chat sufre problemas en este momento y es posible que los mensajes tarden un poco en aparecer.",
["pt-br"] = "O filtro de chat está com problemas no momento e as mensagens podem demorar para aparecer.",
["ko"] = "현재 채팅 필터에 문제가 있어 메시지 표시가 느릴 수 있어요.",
["zh-tw"] = "文字過濾系統發生問題,訊息可能會延遲顯示。",
["zh-cn"] = "聊天过滤器当前遇到问题,消息显示可能出现延迟。",
["zh-cjv"] = "聊天过滤器当前遇到问题,消息显示可能出现延迟。",
}
},
{
Key = "GameChat_ChatService_YouHaveLeftChannel",
Values = {
["ru"] = "Вы покинули канал «{RBX_NAME}»",
["fr"] = "Vous avez quitté le canal {RBX_NAME}",
["en-us"] = "You have left channel '{RBX_NAME}'",
["de"] = "Du hast Kanal „{RBX_NAME}“ verlassen.",
["it"] = "Hai lasciato il canale \"{RBX_NAME}\"",
["pt"] = "Você saiu do canal '{RBX_NAME}'",
["ja"] = "チャンネル 「{RBX_NAME}」を退出しました。",
["es"] = "Has salido del canal \"{RBX_NAME}\".",
["es-es"] = "Has salido del canal \"{RBX_NAME}\".",
["pt-br"] = "Você saiu do canal '{RBX_NAME}'",
["ko"] = "{RBX_NAME}' 채널에서 나왔어요",
["zh-tw"] = "您已離開「{RBX_NAME}」頻道",
["zh-cn"] = "你已离开频道“{RBX_NAME}”",
["zh-cjv"] = "你已离开频道“{RBX_NAME}”",
}
},
{
Key = "GameChat_ChatService_CannotLeaveChannel",
Values = {
["ru"] = "Вы не можете покинуть этот канал.",
["fr"] = "Vous ne pouvez pas quitter ce canal.",
["en-us"] = "You cannot leave this channel.",
["de"] = "Du kannst diesen Kanal nicht verlassen.",
["it"] = "Non puoi lasciare questo canale.",
["pt"] = "Você não pode sair deste canal.",
["ja"] = "このチャンネルを終了できません。",
["es"] = "No puedes salir de este canal.",
["es-es"] = "No puedes salir de este canal.",
["pt-br"] = "Você não pode sair deste canal.",
["ko"] = "채널에서 나갈 수 없어요.",
["zh-tw"] = "您無法離開此頻道。",
["zh-cn"] = "你无法离开此频道。",
["zh-cjv"] = "你无法离开此频道。",
}
},
{
Key = "GameChat_ChatServiceRunner_YouCannotJoinChannel",
Values = {
["ru"] = "Вы не можете подключиться к каналу «{RBX_NAME}»",
["fr"] = "Vous ne pouvez pas rejoindre le canal {RBX_NAME}",
["en-us"] = "You cannot join channel {RBX_NAME}",
["de"] = "Du kannst Kanal „{RBX_NAME}“ nicht beitreten.",
["it"] = "Non puoi accedere al canale {RBX_NAME}",
["pt"] = "Você não pode entrar no canal {RBX_NAME}",
["ja"] = "チャンネル {RBX_NAME} に参加することは出来ません。",
["es"] = "No puedes unirte al canal {RBX_NAME}.",
["es-es"] = "No puedes unirte al canal {RBX_NAME}.",
["pt-br"] = "Você não pode entrar no canal {RBX_NAME}",
["ko"] = "{RBX_NAME} 채널에 가입할 수 없어요",
["zh-tw"] = "您無法加入{RBX_NAME} 頻道",
["zh-cn"] = "你无法加入频道“{RBX_NAME}”",
["zh-cjv"] = "你无法加入频道“{RBX_NAME}”",
}
},
{
Key = "GameChat_ChatServiceRunner_ChannelDoesNotExist",
Values = {
["ru"] = "Канала «{RBX_NAME}» не существует.",
["fr"] = "Le canal {RBX_NAME} n'existe pas.",
["en-us"] = "Channel {RBX_NAME} does not exist.",
["de"] = "Kanal „{RBX_NAME}“ existiert nicht.",
["it"] = "Il canale {RBX_NAME} non esiste.",
["pt"] = "O canal {RBX_NAME} não existe.",
["ja"] = "チャンネル {RBX_NAME} は存在しません。",
["es"] = "El canal {RBX_NAME} no existe.",
["es-es"] = "El canal {RBX_NAME} no existe.",
["pt-br"] = "O canal {RBX_NAME} não existe.",
["ko"] = "{RBX_NAME} 채널이 없어요.",
["zh-tw"] = "頻道「{RBX_NAME}」不存在。",
["zh-cn"] = "频道“{RBX_NAME}”不存在。",
["zh-cjv"] = "频道“{RBX_NAME}”不存在。",
}
},
{
Key = "GameChat_ChatServiceRunner_YouCannotLeaveChannel",
Values = {
["ru"] = "Вы не можете покинуть канал «{RBX_NAME}».",
["fr"] = "Vous ne pouvez pas quitter le canal {RBX_NAME}",
["en-us"] = "You cannot leave channel {RBX_NAME}",
["de"] = "Du kannst Kanal „{RBX_NAME}“ nicht verlassen.",
["it"] = "Non puoi lasciare il canale {RBX_NAME}",
["pt"] = "Você não pode sair do canal {RBX_NAME}",
["ja"] = "チャンネル {RBX_NAME} を終了できません。",
["es"] = "No puedes salir del canal {RBX_NAME}.",
["es-es"] = "No puedes salir del canal {RBX_NAME}.",
["pt-br"] = "Você não pode sair do canal {RBX_NAME}",
["ko"] = "{RBX_NAME} 채널에서 나갈 수 없어요",
["zh-tw"] = "您無法離開 {RBX_NAME} 頻道",
["zh-cn"] = "你无法离开频道“{RBX_NAME}”",
["zh-cjv"] = "你无法离开频道“{RBX_NAME}”",
}
},
{
Key = "GameChat_ChatServiceRunner_YouAreNotInChannel",
Values = {
["ru"] = "Вы не на канале «{RBX_NAME}»",
["fr"] = "Vous n'êtes pas sur le canal {RBX_NAME}",
["en-us"] = "You are not in channel {RBX_NAME}",
["de"] = "Du befindest dich nicht in Kanal „{RBX_NAME}“.",
["it"] = "Non ti trovi nel canale {RBX_NAME}",
["pt"] = "Você não está no canal {RBX_NAME}",
["ja"] = "あなたはチャンネル {RBX_NAME} にいません。",
["es"] = "No estás en el canal {RBX_NAME}.",
["es-es"] = "No estás en el canal {RBX_NAME}.",
["pt-br"] = "Você não está no canal {RBX_NAME}",
["ko"] = "{RBX_NAME} 채널에 있지 않아요",
["zh-tw"] = "您不在 {RBX_NAME} 頻道",
["zh-cn"] = "你不在频道“{RBX_NAME}”",
["zh-cjv"] = "你不在频道“{RBX_NAME}”",
}
},
{
Key = "GameChat_ChatServiceRunner_SystemChannelWelcomeMessage",
Values = {
["ru"] = "Этот канал предназначен для системных и игровых уведомлений.",
["fr"] = "Ce canal est réservé aux notifications système et de jeu.",
["en-us"] = "This channel is for system and game notifications.",
["de"] = "Dieser Kanal ist für System- und Spielbenachrichtigungen.",
["it"] = "Questo canale è per le notifiche di gioco e del sistema.",
["pt"] = "Este canal é destinado a notificações do sistema e jogo.",
["ja"] = "このチャンネルはシステムとゲーム通知のためのものです。",
["es"] = "Este canal es para notificaciones del sistema y del juego.",
["es-es"] = "Este canal es para notificaciones del sistema y del juego.",
["pt-br"] = "Este canal é destinado a notificações do sistema e jogo.",
["ko"] = "이 채널은 시스템 및 게임 알림용이에요.",
["zh-tw"] = "此頻道為系統及遊戲通知專用。",
["zh-cn"] = "此频道用于发送系统及游戏通知。",
["zh-cjv"] = "此频道用于发送系统及游戏通知。",
}
},
{
Key = "GameChat_FriendChatNotifier_JoinMessage",
Values = {
["ru"] = "Ваш друг {RBX_NAME} присоединился к игре.",
["fr"] = "Votre ami {RBX_NAME} a rejoint le jeu.",
["en-us"] = "Your friend {RBX_NAME} has joined the game.",
["de"] = "Dein Freund {RBX_NAME} ist dem Spiel beigetreten.",
["it"] = "Il tuo amico {RBX_NAME} è entrato nel gioco.",
["pt"] = "Seu amigo {RBX_NAME} juntou-se ao jogo.",
["ja"] = "あなたの友人、{RBX_NAME} さんがゲームに参加しました。",
["es"] = "Tu amigo {RBX_NAME} se ha unido al juego.",
["es-es"] = "Tu amigo {RBX_NAME} se ha unido al juego.",
["pt-br"] = "Seu amigo {RBX_NAME} juntou-se ao jogo.",
["ko"] = "친구 {RBX_NAME}님이 게임에 가입했어요.",
["zh-tw"] = "您的好友 {RBX_NAME} 已加入遊戲。",
["zh-cn"] = "你的朋友“{RBX_NAME}”已加入游戏。",
["zh-cjv"] = "你的朋友“{RBX_NAME}”已加入游戏。",
},
},
{
Key = "InGame.Chat.Response.EmoteNotAvailable",
Values = {
["de"] = "Du kannst dieses Emote nicht benutzen.",
["en-us"] = "You can't use that Emote.",
["es"] = "No puedes usar ese emoticono.",
["es-es"] = "No puedes usar ese emoticono.",
["fr"] = "Tu ne peux pas utiliser cette Emote.",
["it"] = "Non puoi usare quell'emoticon.",
["ja"] = "そのエモートは使えません。",
["ko"] = "이 감정 표현은 사용할 수 없어요.",
["pt"] = "Você não pode usar esse emote.",
["pt-br"] = "Você não pode usar esse emote.",
["ru"] = "Вы не можете использовать эту эмоцию.",
["zh-cjv"] = "无法使用该动作。",
["zh-cn"] = "无法使用该动作。",
["zh-tw"] = "無法使用該動作。",
},
},
{
Key = "InGame.Chat.Response.EmotesNotSupported",
Values = {
["de"] = "Du kannst hier keine Emotes verwenden.",
["en-us"] = "You can't use Emotes here.",
["es"] = "No puedes usar emoticonos aquí.",
["es-es"] = "No puedes usar emoticonos aquí.",
["fr"] = "Tu ne peux pas utiliser les Emotes ici.",
["it"] = "Qui non puoi usare le emoticon.",
["ja"] = "ここではエモートはここでは使えません。",
["ko"] = "여기서는 감정 표현을 사용할 수 없어요.",
["pt"] = "Você não pode usar emotes aqui.",
["pt-br"] = "Você não pode usar emotes aqui.",
["ru"] = "Здесь нельзя использовать эмоции.",
["zh-cjv"] = "无法在此游戏中使用动作。",
["zh-cn"] = "无法在此游戏中使用动作。",
["zh-tw"] = "無法在此遊戲使用動作。",
},
},
{
Key = "InGame.Chat.Response.EmotesWrongAvatarType",
Values = {
["de"] = "Nur R15 Avatars können Emotes verwenden.",
["en-us"] = "Only R15 avatars can use Emotes.",
["es"] = "Solo los avatares R15 pueden usar emoticonos.",
["es-es"] = "Solo los avatares R15 pueden usar emoticonos.",
["fr"] = "Seuls les avatars R15 peuvent utiliser les Emotes.",
["it"] = "Solo gli avatar R15 possono usare le emoticon.",
["ja"] = "R15指定のアバターのみエモートを使えます。",
["ko"] = "R15 아바타만 감정 표현을 사용할 수 있어요.",
["pt"] = "Apenas avatares R15 podem usar emotes.",
["pt-br"] = "Apenas avatares R15 podem usar emotes.",
["ru"] = "Только аватары R15 могут использовать эмоции.",
["zh-cjv"] = "只有 R15 虚拟形象可以使用动作。",
["zh-cn"] = "只有 R15 虚拟形象可以使用动作。",
["zh-tw"] = "只有 R15 虛擬人偶可以使用動作。",
},
},
{
Key = "InGame.Chat.Response.EmotesTemporarilyUnavailable",
Values = {
["de"] = "You can't use Emotes right now.",
["en-us"] = "You can't use Emotes right now.",
["es"] = "No puedes usar emoticonos en este momento.",
["es-es"] = "No puedes usar emoticonos en este momento.",
["fr"] = "Tu ne peux pas utiliser d'Emote maintenant.",
["it"] = "Adesso non puoi usare le emoticon.",
["ja"] = "今はエモートを使えません。",
["ko"] = "지금은 감정 표현을 사용할 수 없어요.",
["pt"] = "Você não pode usar emotes no momento.",
["pt-br"] = "Você não pode usar emotes no momento.",
["ru"] = "Вы не можете использовать эмоции прямо сейчас.",
["zh-cjv"] = "当前无法使用动作。",
["zh-cn"] = "当前无法使用动作。",
["zh-tw"] = "現在無法使用動作。",
},
},
{
Key = "InGame.Chat.Label.SystemMessagePrefix",
Values = {
["de"] = "System",
["en-us"] = "System",
["es"] = "Sistema",
["es-es"] = "Sistema",
["fr"] = "Système",
["it"] = "Sistema",
["ja"] = "システム",
["ko"] = "시스템",
["pt"] = "Sistema",
["pt-br"] = "Sistema",
["ru"] = "Система",
["zh-cjv"] = "系统",
["zh-cn"] = "系统",
["zh-tw"] = "系統",
},
},
{
Key = "InGame.Chat.Label.TeamMessagePrefix",
Values = {
["de"] = "Team",
["en-us"] = "Team",
["es"] = "Equipo",
["es-es"] = "Equipo",
["fr"] = "Équipe",
["it"] = "Squadra",
["ja"] = "チーム",
["ko"] = "",
["pt"] = "Equipe",
["pt-br"] = "Equipe",
["ru"] = "Команда",
["zh-cjv"] = "队伍",
["zh-cn"] = "队伍",
["zh-tw"] = "隊伍",
},
},
{
Key = "InGame.Chat.Label.From",
Values = {
["de"] = "Von",
["en-us"] = "From",
["es"] = "De",
["es-es"] = "De",
["fr"] = "De",
["it"] = "Da",
["ja"] = "から",
["ko"] = "발신:",
["pt"] = "De",
["pt-br"] = "De",
["ru"] = "От ",
["zh-cjv"] = "来自于",
["zh-cn"] = "来自于",
["zh-tw"] = "",
},
},
{
Key = "InGame.Chat.Label.To",
Values = {
["de"] = "Bis",
["en-us"] = "To",
["es"] = "Para",
["es-es"] = "Para",
["fr"] = "À",
["it"] = "A",
["ja"] = "",
["ko"] = "수신:",
["pt"] = "Para",
["pt-br"] = "Para",
["ru"] = "Для",
["zh-cjv"] = "发送至",
["zh-cn"] = "发送至",
["zh-tw"] = "",
},
},
{
Key = "InGame.Chat.Response.DisplayNameMultipleMatches",
Values = {
["de"] = "Warnung: Die folgenden Benutzer haben diesen angezeigten Namen: ",
["en-us"] = "Warning: The following users have this display name: ",
["es"] = "Advertencia: Los siguientes jugadores tienen este nombre de usuario: ",
["es-es"] = "Advertencia: Los siguientes jugadores tienen este nombre de usuario: ",
["fr"] = "Avertissement - Les utilisateurs suivants ont ce nom d'affichage : ",
["it"] = "Attenzione, i seguenti utenti hanno questo nome: ",
["ja"] = "警告: 以下のユーザーには、この表示名があります: ",
["ko"] = "주의: 다음 사용자들이 이 표시 이름을 사용하고 있습니다. ",
["pt"] = "Aviso! Os seguintes usuários têm esse nome de exibição: ",
["pt-br"] = "Aviso! Os seguintes usuários têm esse nome de exibição: ",
["ru"] = "Внимание! Такое имя есть у нескольких пользователей: ",
["zh-cn"] = "注意:以下用户正在使用此昵称:",
["zh-tw"] = "注意:以下使用者正在使用此顯示名稱:",
["zh-cjv"] = "注意:以下用户正在使用此昵称:",
},
},
}
@@ -0,0 +1,64 @@
-- // FileName: ChatCommandsTeller.lua
-- // Written by: Xsitsu
-- // Description: Module that provides information on default chat commands to players.
local Chat = game:GetService("Chat")
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings"))
local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants"))
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
if ChatLocalization == nil then ChatLocalization = {} end
if not ChatLocalization.FormatMessageToSend or not ChatLocalization.LocalizeFormattedMessage then
function ChatLocalization:FormatMessageToSend(key,default) return default end
end
local function Run(ChatService)
local function ShowJoinAndLeaveCommands()
if ChatSettings.ShowJoinAndLeaveHelpText ~= nil then
return ChatSettings.ShowJoinAndLeaveHelpText
end
return false
end
local function ProcessCommandsFunction(fromSpeaker, message, channel)
if (message:lower() == "/?" or message:lower() == "/help") then
local speaker = ChatService:GetSpeaker(fromSpeaker)
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_ChatCommandsTeller_Desc","These are the basic chat commands."), channel)
if ChatSettings.AllowMeCommand then
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_ChatCommandsTeller_MeCommand","/me <text> : roleplaying command for doing actions."), channel)
end
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_ChatCommandsTeller_SwitchChannelCommand","/c <channel> : switch channel menu tabs."), channel)
if ShowJoinAndLeaveCommands() then
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_ChatCommandsTeller_JoinChannelCommand","/join <channel> or /j <channel> : join channel."), channel)
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_ChatCommandsTeller_LeaveChannelCommand","/leave <channel> or /l <channel> : leave channel. (leaves current if none specified)"), channel)
end
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_ChatCommandsTeller_WhisperCommand","/whisper <speaker> or /w <speaker> : open private message channel with speaker."), channel)
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_ChatCommandsTeller_MuteCommand","/mute <speaker> : mute a speaker."), channel)
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_ChatCommandsTeller_UnMuteCommand","/unmute <speaker> : unmute a speaker."), channel)
local player = speaker:GetPlayer()
if player and player.Team then
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_ChatCommandsTeller_TeamCommand","/team <message> or /t <message> : send a team chat to players on your team."), channel)
end
return true
end
return false
end
ChatService:RegisterProcessCommandsFunction("chat_commands_inquiry", ProcessCommandsFunction, ChatConstants.StandardPriority)
if ChatSettings.GeneralChannelName then
local allChannel = ChatService:GetChannel(ChatSettings.GeneralChannelName)
if (allChannel) then
allChannel.WelcomeMessage = ChatLocalization:FormatMessageToSend("GameChat_ChatCommandsTeller_AllChannelWelcomeMessage","Chat '/?' or '/help' for a list of chat commands.")
end
end
end
return Run
@@ -0,0 +1,93 @@
-- // FileName: ChatFloodDetector.lua
-- // Written by: Xsitsu
-- // Description: Module that limits the number of messages a speaker can send in a given period of time.
local Chat = game:GetService("Chat")
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants"))
local doFloodCheckByChannel = true
local informSpeakersOfWaitTimes = true
local chatBotsBypassFloodCheck = true
local numberMessagesAllowed = 7
local decayTimePeriod = 15
local floodCheckTable = {}
local whitelistedSpeakers = {}
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
if ChatLocalization == nil then ChatLocalization = {} end
if not ChatLocalization.FormatMessageToSend or not ChatLocalization.LocalizeFormattedMessage then
function ChatLocalization:FormatMessageToSend(key,default) return default end
end
local function EnterTimeIntoLog(tbl)
table.insert(tbl, tick() + decayTimePeriod)
end
local function Run(ChatService)
local function FloodDetectionProcessCommandsFunction(speakerName, message, channel)
if (whitelistedSpeakers[speakerName]) then return false end
local speakerObj = ChatService:GetSpeaker(speakerName)
if (not speakerObj) then return false end
if (chatBotsBypassFloodCheck and not speakerObj:GetPlayer()) then return false end
if (not floodCheckTable[speakerName]) then
floodCheckTable[speakerName] = {}
end
local t = nil
if (doFloodCheckByChannel) then
if (not floodCheckTable[speakerName][channel]) then
floodCheckTable[speakerName][channel] = {}
end
t = floodCheckTable[speakerName][channel]
else
t = floodCheckTable[speakerName]
end
local now = tick()
while (#t > 0 and t[1] < now) do
table.remove(t, 1)
end
if (#t < numberMessagesAllowed) then
EnterTimeIntoLog(t)
return false
else
local timeDiff = math.ceil(t[1] - now)
if (informSpeakersOfWaitTimes) then
local msg = ChatLocalization:FormatMessageToSend("GameChat_ChatFloodDetector_MessageDisplaySeconds",
string.format("You must wait %d %s before sending another message!", timeDiff, (timeDiff > 1) and "seconds" or "second"),
"RBX_NUMBER",
tostring(timeDiff)
)
speakerObj:SendSystemMessage(msg, channel)
else
speakerObj:SendSystemMessage(
ChatLocalization:FormatMessageToSend(
"GameChat_ChatFloodDetector_Message",
"You must wait before sending another message!"
)
,channel)
end
return true
end
end
ChatService:RegisterProcessCommandsFunction("flood_detection", FloodDetectionProcessCommandsFunction, ChatConstants.LowPriority)
ChatService.SpeakerRemoved:connect(function(speakerName)
floodCheckTable[speakerName] = nil
end)
end
return Run
@@ -0,0 +1,99 @@
-- // FileName: ChatMessageValidator.lua
-- // Written by: TheGamer101
-- // Description: Validate things such as no disallowed whitespace and chat message length on the server.
local Chat = game:GetService("Chat")
local RunService = game:GetService("RunService")
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings"))
local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants"))
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
if ChatLocalization == nil then ChatLocalization = {} end
if not ChatLocalization.FormatMessageToSend or not ChatLocalization.LocalizeFormattedMessage then
function ChatLocalization:FormatMessageToSend(key,default) return default end
end
local MAX_BYTES_PER_UTF8_CODEPOINT = 6
local DISALLOWED_WHITESPACE = {"\n", "\r", "\t", "\v", "\f"}
if ChatSettings.DisallowedWhiteSpace then
DISALLOWED_WHITESPACE = ChatSettings.DisallowedWhiteSpace
end
local function isMessageValidServer(msg)
-- worst-case byte length check - fast for large strings
if msg:len() > ChatSettings.MaximumMessageLength*MAX_BYTES_PER_UTF8_CODEPOINT then
return false
end
-- check for invalid unicode sequence
-- avoid normalizing first because utf8.nfcnormalize throws for invalid sequences
if utf8.len(msg) == nil then
return false
end
-- check codepoint length
if utf8.len(utf8.nfcnormalize(msg)) > ChatSettings.MaximumMessageLength then
return false
end
return true
end
local function CanUserChat(playerObj)
if RunService:IsStudio() then
return true
end
local success, canChat = pcall(function()
return Chat:CanUserChatAsync(playerObj.UserId)
end)
return success and canChat
end
local function Run(ChatService)
local function ValidateChatFunction(speakerName, message, channel)
local speakerObj = ChatService:GetSpeaker(speakerName)
local playerObj = speakerObj:GetPlayer()
if not speakerObj then return false end
if not playerObj then return false end
if not RunService:IsStudio() and playerObj.UserId < 1 then
return true
end
if not CanUserChat(playerObj) then
speakerObj:SendSystemMessage(ChatLocalization:FormatMessageToSend(
"GameChat_ChatMessageValidator_SettingsError",
"Your chat settings prevent you from sending messages."
), channel)
return true
end
if not isMessageValidServer(message) then
local localizedError = ChatLocalization:FormatMessageToSend(
"GameChat_ChatMessageValidator_MaxLengthError",
"Your message exceeds the maximum message length."
)
speakerObj:SendSystemMessage(localizedError, channel)
return true
end
for _, whitespace in pairs(DISALLOWED_WHITESPACE) do
if message:find(whitespace) then
speakerObj:SendSystemMessage(ChatLocalization:FormatMessageToSend(
"GameChat_ChatMessageValidator_WhitespaceError",
"Your message contains whitespace that is not allowed."
), channel)
return true
end
end
return false
end
ChatService:RegisterProcessCommandsFunction("message_validation", ValidateChatFunction, ChatConstants.VeryHighPriority)
end
return Run
@@ -0,0 +1,187 @@
-- // FileName: ExtraDataInitializer.lua
-- // Written by: Xsitsu
-- // Description: Module that sets some basic ExtraData such as name color, and chat color.
local SpecialChatColors = {
Groups = {
{
--- ROBLOX Interns group
GroupId = 2868472,
Rank = 100,
ChatColor = Color3.new(175/255, 221/255, 1),
},
{
--- ROBLOX Admins group
GroupId = 1200769,
ChatColor = Color3.new(1, 215/255, 0),
},
},
Players = {
{
--- Left as an example
-- UserId = 2231221,
-- ChatColor = Color3.new(205/255, 0, 0)
}
}
}
local function MakeIsInGroup(groupId, requiredRank)
assert(type(requiredRank) == "nil" or type(requiredRank) == "number", "requiredRank must be a number or nil")
return function(player)
if player and player.UserId then
local userId = player.UserId
local inGroup = false
local success, err = pcall(function() -- Many things can error is the IsInGroup check
if requiredRank then
inGroup = player:GetRankInGroup(groupId) > requiredRank
else
inGroup = player:IsInGroup(groupId)
end
end)
if not success and err then
print("Error checking in group: " ..err)
end
return inGroup
end
return false
end
end
local function ConstructIsInGroups()
if SpecialChatColors.Groups then
for _, group in pairs(SpecialChatColors.Groups) do
group.IsInGroup = MakeIsInGroup(group.GroupId, group.Rank)
end
end
end
ConstructIsInGroups()
local Players = game:GetService("Players")
local function GetSpecialChatColor(speakerName)
if SpecialChatColors.Players then
local playerFromSpeaker = Players:FindFirstChild(speakerName)
if playerFromSpeaker then
for _, player in pairs(SpecialChatColors.Players) do
if playerFromSpeaker.UserId == player.UserId then
return player.ChatColor
end
end
end
end
if SpecialChatColors.Groups then
for _, group in pairs(SpecialChatColors.Groups) do
if group.IsInGroup(Players:FindFirstChild(speakerName)) then
return group.ChatColor
end
end
end
end
local function Run(ChatService)
local NAME_COLORS =
{
Color3.new(253/255, 41/255, 67/255), -- BrickColor.new("Bright red").Color,
Color3.new(1/255, 162/255, 255/255), -- BrickColor.new("Bright blue").Color,
Color3.new(2/255, 184/255, 87/255), -- BrickColor.new("Earth green").Color,
BrickColor.new("Bright violet").Color,
BrickColor.new("Bright orange").Color,
BrickColor.new("Bright yellow").Color,
BrickColor.new("Light reddish violet").Color,
BrickColor.new("Brick yellow").Color,
}
local function GetNameValue(pName)
local value = 0
for index = 1, #pName do
local cValue = string.byte(string.sub(pName, index, index))
local reverseIndex = #pName - index + 1
if #pName%2 == 1 then
reverseIndex = reverseIndex - 1
end
if reverseIndex%4 >= 2 then
cValue = -cValue
end
value = value + cValue
end
return value
end
local color_offset = 0
local function ComputeNameColor(pName)
return NAME_COLORS[((GetNameValue(pName) + color_offset) % #NAME_COLORS) + 1]
end
local function GetNameColor(speaker)
local player = speaker:GetPlayer()
if player then
if player.Team ~= nil then
return player.TeamColor.Color
end
end
return ComputeNameColor(speaker.Name)
end
local function onNewSpeaker(speakerName)
local speaker = ChatService:GetSpeaker(speakerName)
if not speaker:GetExtraData("NameColor") then
speaker:SetExtraData("NameColor", GetNameColor(speaker))
end
if not speaker:GetExtraData("ChatColor") then
local specialChatColor = GetSpecialChatColor(speakerName)
if specialChatColor then
speaker:SetExtraData("ChatColor", specialChatColor)
end
end
if not speaker:GetExtraData("Tags") then
--// Example of how you would set tags
--[[
local tags = {
{
TagText = "VIP",
TagColor = Color3.new(1, 215/255, 0)
},
{
TagText = "Alpha Tester",
TagColor = Color3.new(205/255, 0, 0)
}
}
speaker:SetExtraData("Tags", tags)
]]
speaker:SetExtraData("Tags", {})
end
end
ChatService.SpeakerAdded:connect(onNewSpeaker)
for _, speakerName in pairs(ChatService:GetSpeakerList()) do
onNewSpeaker(speakerName)
end
local PlayerChangedConnections = {}
Players.PlayerAdded:connect(function(player)
local changedConn = player.Changed:connect(function(property)
local speaker = ChatService:GetSpeaker(player.Name)
if speaker then
if property == "TeamColor" or property == "Neutral" or property == "Team" then
speaker:SetExtraData("NameColor", GetNameColor(speaker))
end
end
end)
PlayerChangedConnections[player] = changedConn
end)
Players.PlayerRemoving:connect(function(player)
local changedConn = PlayerChangedConnections[player]
if changedConn then
changedConn:Disconnect()
end
PlayerChangedConnections[player] = nil
end)
end
return Run
@@ -0,0 +1,63 @@
-- // FileName: FriendJoinNotifer.lua
-- // Written by: TheGamer101
-- // Description: Module that adds a message to the chat whenever a friend joins the game.
local Chat = game:GetService("Chat")
local Players = game:GetService("Players")
local FriendService = game:GetService("FriendService")
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings"))
local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants"))
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
if ChatLocalization == nil then ChatLocalization = {} end
if not ChatLocalization.FormatMessageToSend or not ChatLocalization.LocalizeFormattedMessage then
function ChatLocalization:FormatMessageToSend(key,default) return default end
end
local FriendMessageTextColor = Color3.fromRGB(255, 255, 255)
local FriendMessageExtraData = {ChatColor = FriendMessageTextColor}
local function Run(ChatService)
local function ShowFriendJoinNotification()
if ChatSettings.ShowFriendJoinNotification ~= nil then
return ChatSettings.ShowFriendJoinNotification
end
return false
end
local function SendFriendJoinNotification(player, joinedFriend)
local speakerObj = ChatService:GetSpeaker(player.Name)
if speakerObj then
local msg = ChatLocalization:FormatMessageToSend("GameChat_FriendChatNotifier_JoinMessage",
string.format("Your friend %s has joined the game.", joinedFriend.Name),
"RBX_NAME",
joinedFriend.Name)
speakerObj:SendSystemMessage(msg, "System", FriendMessageExtraData)
end
end
local function TrySendFriendNotification(player, joinedPlayer)
if player ~= joinedPlayer then
coroutine.wrap(function()
if player:IsFriendsWith(joinedPlayer.UserId) then
SendFriendJoinNotification(player, joinedPlayer)
end
end)()
end
end
if ShowFriendJoinNotification() then
Players.PlayerAdded:connect(function(player)
local possibleFriends = Players:GetPlayers()
for i = 1, #possibleFriends do
TrySendFriendNotification(possibleFriends[i], player)
end
end)
end
end
return Run
@@ -0,0 +1,25 @@
-- // FileName: MeCommand.lua
-- // Written by: TheGamer101
-- // Description: Sets the type of /me messages.
local Chat = game:GetService("Chat")
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants"))
local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings"))
local function Run(ChatService)
if ChatSettings and ChatSettings.AllowMeCommand then
local function MeCommandFilterFunction(speakerName, messageObj, channelName)
local message = messageObj.Message
if message and string.sub(message, 1, 4):lower() == "/me " then
-- Set a different message type so that clients can render the message differently.
messageObj.MessageType = ChatConstants.MessageTypeMeCommand
end
end
ChatService:RegisterFilterMessageFunction("me_command", MeCommandFilterFunction)
end
end
return Run
@@ -0,0 +1,164 @@
-- // FileName: MuteSpeaker.lua
-- // Written by: TheGamer101
-- // Description: Module that handles all the mute and unmute commands.
local Chat = game:GetService("Chat")
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatModules = Chat:WaitForChild("ChatModules")
local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants"))
local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings"))
local DisplayNameHelpers = require(ChatModules.Utility.DisplayNameHelpers)
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
if ChatLocalization == nil then ChatLocalization = {} end
if not ChatLocalization.FormatMessageToSend or not ChatLocalization.LocalizeFormattedMessage then
function ChatLocalization:FormatMessageToSend(key,default) return default end
end
local errorTextColor = ChatSettings.ErrorMessageTextColor or Color3.fromRGB(245, 50, 50)
local errorExtraData = {ChatColor = errorTextColor}
local function Run(ChatService)
local function GetSpeakerNameFromMessage(message)
local speakerName = message
if string.sub(message, 1, 1) == "\"" then
local pos = string.find(message, "\"", 2)
if pos then
speakerName = string.sub(message, 2, pos - 1)
end
else
local first = string.match(message, "^[^%s]+")
if first then
speakerName = first
end
end
return speakerName
end
local function DoMuteCommand(speakerName, message, channel)
local muteSpeakerInputName = GetSpeakerNameFromMessage(message)
local speaker = ChatService:GetSpeaker(speakerName)
if speaker then
local muteSpeakerName, muteSpeakerError
--Get the target user's UserName from the input (which could be userName or displayName)
if ChatSettings.PlayerDisplayNamesEnabled then
muteSpeakerName, muteSpeakerError = DisplayNameHelpers.getUserNameFromChattedName(muteSpeakerInputName, speakerName, speaker:GetNameForDisplay())
else
muteSpeakerName, muteSpeakerError = DisplayNameHelpers.getUserNameFromChattedName(muteSpeakerInputName, speakerName, nil)
end
local muteSpeaker = ChatService:GetSpeaker(muteSpeakerName)
if muteSpeakerError == DisplayNameHelpers.CommandErrorCodes.ChattingToSelf then
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_DoMuteCommand_CannotMuteSelf", "You cannot mute yourself."), channel, errorExtraData)
elseif muteSpeakerError == DisplayNameHelpers.CommandErrorCodes.NoMatches then
local msg = ChatLocalization:FormatMessageToSend(
"GameChat_MuteSpeaker_SpeakerDoesNotExist",
string.format("Speaker '%s' does not exist.", tostring(muteSpeakerInputName)),
"RBX_NAME",
tostring(muteSpeakerName))
speaker:SendSystemMessage(msg, channel, errorExtraData)
elseif muteSpeakerError == DisplayNameHelpers.CommandErrorCodes.MultipleMatches then
local matchingUsersText = DisplayNameHelpers.getUsersWithDisplayNameString(muteSpeakerInputName, speakerName)
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("InGame.Chat.Response.DisplayNameMultipleMatches", "Warning: The following users have this display name: "), channel, errorExtraData)
--Send a second message with a list of names so that the localization formatter doesn't prune it
speaker:SendSystemMessage(matchingUsersText, channel, errorExtraData)
elseif muteSpeaker then
speaker:AddMutedSpeaker(muteSpeaker.Name)
local muteSpeakerDisplayName = muteSpeakerName
if ChatSettings.PlayerDisplayNamesEnabled then
muteSpeakerDisplayName = muteSpeaker:GetNameForDisplay()
end
local msg = ChatLocalization:FormatMessageToSend("GameChat_ChatMain_SpeakerHasBeenMuted",
string.format("Speaker '%s' has been muted.", muteSpeakerDisplayName),
"RBX_NAME",
muteSpeakerDisplayName)
speaker:SendSystemMessage(msg, channel)
end
end
end
local function DoUnmuteCommand(speakerName, message, channel)
local unmuteSpeakerInputName = GetSpeakerNameFromMessage(message)
local speaker = ChatService:GetSpeaker(speakerName)
if speaker then
local unmuteSpeakerName, unmuteSpeakerError
--Get the target user's UserName from the input (which could be userName or displayName)
if ChatSettings.PlayerDisplayNamesEnabled then
unmuteSpeakerName, unmuteSpeakerError = DisplayNameHelpers.getUserNameFromChattedName(unmuteSpeakerInputName, speakerName, speaker:GetNameForDisplay())
else
unmuteSpeakerName, unmuteSpeakerError = DisplayNameHelpers.getUserNameFromChattedName(unmuteSpeakerInputName, speakerName, nil)
end
local unmuteSpeaker = ChatService:GetSpeaker(unmuteSpeakerName)
if unmuteSpeakerError == DisplayNameHelpers.CommandErrorCodes.ChattingToSelf then
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_DoMuteCommand_CannotMuteSelf","You cannot mute yourself."), channel, errorExtraData)
return
elseif unmuteSpeakerError == DisplayNameHelpers.CommandErrorCodes.NoMatches then
local msg = ChatLocalization:FormatMessageToSend("GameChat_MuteSpeaker_SpeakerDoesNotExist",
string.format("Speaker '%s' does not exist.", tostring(unmuteSpeakerName)),
"RBX_NAME",
tostring(unmuteSpeakerName))
speaker:SendSystemMessage(msg, channel, errorExtraData)
return
elseif unmuteSpeakerError == DisplayNameHelpers.CommandErrorCodes.MultipleMatches then --More than one DisplayName match
local matchingUsersText = DisplayNameHelpers.getUsersWithDisplayNameString(unmuteSpeakerInputName, speakerName)
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("InGame.Chat.Response.DisplayNameMultipleMatches", "Warning: The following users have this display name: "), channel, errorExtraData)
--Send a second message with a list of names so that the localization formatter doesn't prune it
speaker:SendSystemMessage(matchingUsersText, channel, errorExtraData)
return
elseif unmuteSpeaker then
speaker:RemoveMutedSpeaker(unmuteSpeaker.Name)
local playerName = unmuteSpeakerName
if ChatSettings.PlayerDisplayNamesEnabled then
playerName = unmuteSpeaker:GetNameForDisplay()
end
local msg = ChatLocalization:FormatMessageToSend("GameChat_ChatMain_SpeakerHasBeenUnMuted",
string.format("Speaker '%s' has been unmuted.", playerName),
"RBX_NAME",
playerName)
speaker:SendSystemMessage(msg, channel)
return
end
end
end
local function MuteCommandsFunction(fromSpeaker, message, channel)
local processedCommand = false
if string.sub(message, 1, 6):lower() == "/mute " then
DoMuteCommand(fromSpeaker, string.sub(message, 7), channel)
processedCommand = true
elseif string.sub(message, 1, 8):lower() == "/unmute " then
DoUnmuteCommand(fromSpeaker, string.sub(message, 9), channel)
processedCommand = true
end
return processedCommand
end
ChatService:RegisterProcessCommandsFunction("mute_commands", MuteCommandsFunction, ChatConstants.StandardPriority)
end
return Run
@@ -0,0 +1,218 @@
-- // FileName: PrivateMessaging.lua
-- // Written by: Xsitsu
-- // Description: Module that handles all private messaging.
local Chat = game:GetService("Chat")
local RunService = game:GetService("RunService")
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatModules = Chat:WaitForChild("ChatModules")
local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants"))
local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings"))
local DisplayNameHelpers = require(ChatModules.Utility.DisplayNameHelpers)
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
if ChatLocalization == nil then ChatLocalization = {} end
if not ChatLocalization.FormatMessageToSend or not ChatLocalization.LocalizeFormattedMessage then
function ChatLocalization:FormatMessageToSend(key,default) return default end
end
local errorTextColor = ChatSettings.ErrorMessageTextColor or Color3.fromRGB(245, 50, 50)
local errorExtraData = {ChatColor = errorTextColor}
local function GetWhisperChannelPrefix()
if ChatConstants.WhisperChannelPrefix then
return ChatConstants.WhisperChannelPrefix
end
return "To "
end
local function GetWhisperChannelId(userName)
return GetWhisperChannelPrefix() .. userName
end
local function Run(ChatService)
local function CanCommunicate(fromSpeaker, toSpeaker)
if RunService:IsStudio() then
return true
end
local fromPlayer = fromSpeaker:GetPlayer()
local toPlayer = toSpeaker:GetPlayer()
if fromPlayer and toPlayer then
local success, canChat = pcall(function()
return Chat:CanUsersChatAsync(fromPlayer.UserId, toPlayer.UserId)
end)
return success and canChat
end
return false
end
local function DoWhisperCommand(fromSpeaker, message, channel)
local speaker = ChatService:GetSpeaker(fromSpeaker)
local otherSpeakerInputName = message
local sendMessage = nil
if (string.sub(message, 1, 1) == "\"") then
local pos = string.find(message, "\"", 2)
if (pos) then
otherSpeakerInputName = string.sub(message, 2, pos - 1)
sendMessage = string.sub(message, pos + 2)
end
else
local first = string.match(message, "^[^%s]+")
if (first) then
otherSpeakerInputName = first
sendMessage = string.sub(message, string.len(otherSpeakerInputName) + 2)
end
end
local otherSpeakerName, otherSpeakerError
--Get the target user's UserName from the input (which could be userName or displayName)
if ChatSettings.PlayerDisplayNamesEnabled and ChatSettings.WhisperByDisplayNameEnabled then
otherSpeakerName, otherSpeakerError = DisplayNameHelpers.getUserNameFromChattedName(otherSpeakerInputName, fromSpeaker, speaker:GetNameForDisplay())
else
otherSpeakerName, otherSpeakerError = DisplayNameHelpers.getUserNameFromChattedName(otherSpeakerInputName, fromSpeaker, nil)
end
local otherSpeaker = ChatService:GetSpeaker(otherSpeakerName)
if otherSpeakerError == DisplayNameHelpers.CommandErrorCodes.ChattingToSelf then
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_PrivateMessaging_CannotWhisperToSelf","You cannot whisper to yourself."), channel, errorExtraData)
elseif otherSpeakerError == DisplayNameHelpers.CommandErrorCodes.NoMatches then
local msg = ChatLocalization:FormatMessageToSend(
"GameChat_MuteSpeaker_SpeakerDoesNotExist",
string.format("Speaker '%s' does not exist.", tostring(otherSpeakerInputName)),
"RBX_NAME",
tostring(otherSpeakerName))
speaker:SendSystemMessage(msg, channel, errorExtraData)
elseif otherSpeakerError == DisplayNameHelpers.CommandErrorCodes.MultipleMatches then
local matchingUsersText = DisplayNameHelpers.getUsersWithDisplayNameString(otherSpeakerInputName, fromSpeaker)
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("InGame.Chat.Response.DisplayNameMultipleMatches", "Warning: The following users have this display name: "), channel, errorExtraData)
--Send a second message with a list of names so that the localization formatter doesn't prune it
speaker:SendSystemMessage(matchingUsersText, channel, errorExtraData)
elseif otherSpeaker then
local channelObj = ChatService:GetChannel(GetWhisperChannelId(otherSpeakerName))
if channelObj then
if not CanCommunicate(speaker, otherSpeaker) then
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_PrivateMessaging_CannotChat","You are not able to chat with this player."), channel, errorExtraData)
return
end
if (not speaker:IsInChannel(channelObj.Name)) then
speaker:JoinChannel(channelObj.Name)
end
if (sendMessage and (string.len(sendMessage) > 0) ) then
speaker:SayMessage(sendMessage, channelObj.Name)
end
speaker:SetMainChannel(channelObj.Name)
else
local msg = ChatLocalization:FormatMessageToSend(
"GameChat_MuteSpeaker_SpeakerDoesNotExist",
string.format("Speaker '%s' does not exist.", tostring(otherSpeakerInputName)),
"RBX_NAME",
tostring(otherSpeakerName))
speaker:SendSystemMessage(msg, channel, errorExtraData)
end
end
end
local function WhisperCommandsFunction(fromSpeaker, message, channel)
local processedCommand = false
if (string.sub(message, 1, 3):lower() == "/w ") then
DoWhisperCommand(fromSpeaker, string.sub(message, 4), channel)
processedCommand = true
elseif (string.sub(message, 1, 9):lower() == "/whisper ") then
DoWhisperCommand(fromSpeaker, string.sub(message, 10), channel)
processedCommand = true
end
return processedCommand
end
local function PrivateMessageReplicationFunction(fromSpeaker, message, channelName)
local sendingSpeaker = ChatService:GetSpeaker(fromSpeaker)
local extraData = sendingSpeaker.ExtraData
sendingSpeaker:SendMessage(message, channelName, fromSpeaker, extraData)
local toSpeaker = ChatService:GetSpeaker(string.sub(channelName, 4))
local fromSpeakerChannelId = GetWhisperChannelId(fromSpeaker)
if (toSpeaker) then
if (not toSpeaker:IsInChannel(fromSpeakerChannelId)) then
toSpeaker:JoinChannel(fromSpeakerChannelId)
end
toSpeaker:SendMessage(message, fromSpeakerChannelId, fromSpeaker, extraData)
end
return true
end
local function PrivateMessageAddTypeFunction(speakerName, messageObj, channelName)
if ChatConstants.MessageTypeWhisper then
messageObj.MessageType = ChatConstants.MessageTypeWhisper
end
end
ChatService:RegisterProcessCommandsFunction("whisper_commands", WhisperCommandsFunction, ChatConstants.StandardPriority)
local function GetWhisperChanneNameColor()
if ChatSettings.WhisperChannelNameColor then
return ChatSettings.WhisperChannelNameColor
end
return Color3.fromRGB(102, 14, 102)
end
ChatService.SpeakerAdded:connect(function(speakerName)
local speaker = ChatService:GetSpeaker(speakerName)
local speakerDisplayName
if ChatSettings.PlayerDisplayNamesEnabled and speaker:GetPlayer() then
speakerDisplayName = speaker:GetNameForDisplay() .. "(@" .. speakerName .. ")"
else
speakerDisplayName = speakerName
end
local toSpeakerChannelId = GetWhisperChannelId(speakerName)
if (ChatService:GetChannel(toSpeakerChannelId)) then
ChatService:RemoveChannel(toSpeakerChannelId)
end
local channel = ChatService:AddChannel(toSpeakerChannelId)
channel.Joinable = false
channel.Leavable = true
channel.AutoJoin = false
channel.Private = true
channel.WelcomeMessage = ChatLocalization:FormatMessageToSend("GameChat_PrivateMessaging_NowChattingWith",
"You are now privately chatting with " .. speakerDisplayName .. ".",
"RBX_NAME",
tostring(speakerDisplayName))
channel.ChannelNameColor = GetWhisperChanneNameColor()
channel:RegisterProcessCommandsFunction("replication_function", PrivateMessageReplicationFunction, ChatConstants.LowPriority)
channel:RegisterFilterMessageFunction("message_type_function", PrivateMessageAddTypeFunction)
end)
ChatService.SpeakerRemoved:connect(function(speakerName)
local whisperChannelId = GetWhisperChannelId(speakerName)
if (ChatService:GetChannel(whisperChannelId)) then
ChatService:RemoveChannel(whisperChannelId)
end
end)
end
return Run
@@ -0,0 +1,189 @@
-- // FileName: TeamChat.lua
-- // Written by: Xsitsu
-- // Description: Module that handles all team chat.
local Chat = game:GetService("Chat")
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings"))
local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants"))
local ChatLocalization = nil
pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
if ChatLocalization == nil then ChatLocalization = {} end
if not ChatLocalization.FormatMessageToSend or not ChatLocalization.LocalizeFormattedMessage then
function ChatLocalization:FormatMessageToSend(key,default) return default end
end
local errorTextColor = ChatSettings.ErrorMessageTextColor or Color3.fromRGB(245, 50, 50)
local errorExtraData = {ChatColor = errorTextColor}
local function Run(ChatService)
local Players = game:GetService("Players")
local channel = ChatService:AddChannel("Team")
channel.WelcomeMessage = ChatLocalization:FormatMessageToSend("GameChat_TeamChat_WelcomeMessage","This is a private channel between you and your team members.")
channel.Joinable = false
channel.Leavable = false
channel.AutoJoin = false
channel.Private = true
local function TeamChatReplicationFunction(fromSpeaker, message, channelName)
local speakerObj = ChatService:GetSpeaker(fromSpeaker)
local channelObj = ChatService:GetChannel(channelName)
if (speakerObj and channelObj) then
local player = speakerObj:GetPlayer()
if (player) then
for i, speakerName in pairs(channelObj:GetSpeakerList()) do
local otherSpeaker = ChatService:GetSpeaker(speakerName)
if (otherSpeaker) then
local otherPlayer = otherSpeaker:GetPlayer()
if (otherPlayer) then
if (player.Team == otherPlayer.Team) then
local extraData = {
NameColor = player.TeamColor.Color,
ChatColor = player.TeamColor.Color,
ChannelColor = player.TeamColor.Color
}
otherSpeaker:SendMessage(message, channelName, fromSpeaker, extraData)
else
--// Could use this line to obfuscate message for cool effects
--otherSpeaker:SendMessage(message, channelName, fromSpeaker)
end
end
end
end
end
end
return true
end
channel:RegisterProcessCommandsFunction("replication_function", TeamChatReplicationFunction, ChatConstants.LowPriority)
local function DoTeamCommand(fromSpeaker, message, channel)
if message == nil then
message = ""
end
local speaker = ChatService:GetSpeaker(fromSpeaker)
if speaker then
local player = speaker:GetPlayer()
if player then
if player.Team == nil then
speaker:SendSystemMessage(ChatLocalization:FormatMessageToSend("GameChat_TeamChat_CannotTeamChatIfNotInTeam","You cannot team chat if you are not on a team!"), channel, errorExtraData)
return
end
local channelObj = ChatService:GetChannel("Team")
if channelObj then
if not speaker:IsInChannel(channelObj.Name) then
speaker:JoinChannel(channelObj.Name)
end
if message and string.len(message) > 0 then
speaker:SayMessage(message, channelObj.Name)
end
speaker:SetMainChannel(channelObj.Name)
end
end
end
end
local function TeamCommandsFunction(fromSpeaker, message, channel)
local processedCommand = false
if message == nil then
error("Message is nil")
end
if channel == "Team" then
return false
end
if string.sub(message, 1, 6):lower() == "/team " or message:lower() == "/team" then
DoTeamCommand(fromSpeaker, string.sub(message, 7), channel)
processedCommand = true
elseif string.sub(message, 1, 3):lower() == "/t " or message:lower() == "/t" then
DoTeamCommand(fromSpeaker, string.sub(message, 4), channel)
processedCommand = true
elseif string.sub(message, 1, 2):lower() == "% " or message:lower() == "%" then
DoTeamCommand(fromSpeaker, string.sub(message, 3), channel)
processedCommand = true
end
return processedCommand
end
ChatService:RegisterProcessCommandsFunction("team_commands", TeamCommandsFunction, ChatConstants.StandardPriority)
local function GetDefaultChannelNameColor()
if ChatSettings.DefaultChannelNameColor then
return ChatSettings.DefaultChannelNameColor
end
return Color3.fromRGB(35, 76, 142)
end
local function PutSpeakerInCorrectTeamChatState(speakerObj, playerObj)
if playerObj.Neutral or playerObj.Team == nil then
speakerObj:UpdateChannelNameColor(channel.Name, GetDefaultChannelNameColor())
if speakerObj:IsInChannel(channel.Name) then
speakerObj:LeaveChannel(channel.Name)
end
elseif not playerObj.Neutral and playerObj.Team then
speakerObj:UpdateChannelNameColor(channel.Name, playerObj.Team.TeamColor.Color)
if not speakerObj:IsInChannel(channel.Name) then
speakerObj:JoinChannel(channel.Name)
end
end
end
ChatService.SpeakerAdded:connect(function(speakerName)
local speakerObj = ChatService:GetSpeaker(speakerName)
if speakerObj then
local player = speakerObj:GetPlayer()
if player then
PutSpeakerInCorrectTeamChatState(speakerObj, player)
end
end
end)
local PlayerChangedConnections = {}
Players.PlayerAdded:connect(function(player)
local changedConn = player.Changed:connect(function(property)
local speakerObj = ChatService:GetSpeaker(player.Name)
if speakerObj then
if property == "Neutral" then
PutSpeakerInCorrectTeamChatState(speakerObj, player)
elseif property == "Team" then
PutSpeakerInCorrectTeamChatState(speakerObj, player)
if speakerObj:IsInChannel(channel.Name) then
local msg = ChatLocalization:FormatMessageToSend("GameChat_TeamChat_NowInTeam",
string.format("You are now on the '%s' team.", player.Team.Name),
"RBX_NAME",
player.Team.Name
)
speakerObj:SendSystemMessage(msg, channel.Name)
end
end
end
end)
PlayerChangedConnections[player] = changedConn
end)
Players.PlayerRemoving:connect(function(player)
local changedConn = PlayerChangedConnections[player]
if changedConn then
changedConn:Disconnect()
end
PlayerChangedConnections[player] = nil
end)
end
return Run
@@ -0,0 +1,83 @@
local Players = game:GetService("Players")
local Chat = game:GetService("Chat")
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings"))
local CommandErrorCodes = {
ChattingToSelf = 1,
NoMatches = 2,
MultipleMatches = 3,
}
--returns (string) UserName as the first value and an error code
local function getUserNameFromChattedName(targetDisplayName, speakerUserName, speakerDisplayName)
--we're looking for non-speaker matches, so if the displayName matches the userName we need to short circuit
if speakerUserName == targetDisplayName then
return speakerUserName, CommandErrorCodes.ChattingToSelf
end
--first, scan for userName matches
local players = Players:GetPlayers()
for _, player in pairs(players) do
if string.lower(player.Name) == string.lower(targetDisplayName) then
return player.Name, nil
end
end
--then, if there we no userName matches, scan for DisplayName matches. return true if there is one non-local-player match
local numDisplayNameMatches = 0
local lastDisplayNameMatch = nil
if ChatSettings.PlayerDisplayNamesEnabled then
for _, player in pairs(players) do
if player.Name ~= speakerUserName and string.lower(player.DisplayName) == string.lower(targetDisplayName) then
numDisplayNameMatches = numDisplayNameMatches + 1
lastDisplayNameMatch = player.Name
end
end
end
if numDisplayNameMatches == 1 then
return lastDisplayNameMatch, nil
elseif numDisplayNameMatches == 0 then
if speakerDisplayName == targetDisplayName then
return speakerDisplayName, CommandErrorCodes.ChattingToSelf
else
return targetDisplayName, CommandErrorCodes.NoMatches
end
elseif numDisplayNameMatches >= 2 then
return targetDisplayName, CommandErrorCodes.MultipleMatches
end
end
local function getUsersWithDisplayNameString(targetDisplayName, speakerUserName)
local displayNameMatches = {}
local matchingUsersText = ""
--first, scan for userName matches
local players = Players:GetPlayers()
for _, player in pairs(players) do
if player.Name ~= speakerUserName and string.lower(player.DisplayName) == string.lower(targetDisplayName) then
table.insert(displayNameMatches, player.Name)
end
end
for i, matchingUserName in pairs(displayNameMatches) do
if i ~= #displayNameMatches then
matchingUsersText = matchingUsersText .. "@" .. matchingUserName .. ", "
else
matchingUsersText = matchingUsersText .. "@" .. matchingUserName
end
end
return matchingUsersText
end
return {
CommandErrorCodes = CommandErrorCodes,
getUserNameFromChattedName = getUserNameFromChattedName,
getUsersWithDisplayNameString = getUsersWithDisplayNameString,
}
@@ -0,0 +1,135 @@
## Documentation
### ChatService
This is the main singleton object that runs the server sided chat service. It manages both ChatChannels and Speakers.
#### Methods
ChatChannel AddChannel(string channelName, bool autoJoin)
void RemoveChannel(string channelName)
ChatChannel GetChannel(string channelName)
Speaker AddSpeaker(string speakerName)
void RemoveSpeaker(string speakerName)
Speaker GetSpeaker(string speakerName)
string[] GetChannelList()
string[] GetAutoJoinChannelList()
void RegisterFilterMessageFunction(string functionId, function func, int priority)
bool FilterMessageFunctionExists(string functionId)
void UnregisterFilterMessageFunction(string functionId)
void RegisterProcessCommandsFunction(string functionId, function func, int priority)
bool ProcessCommandsFunctionExists(string functionId)
void UnregisterProcessCommandsFunction(string functionId)
#### Events
ChannelAdded(string channelName)
ChannelRemoved(string channelName)
SpeakerAdded(string speakerName)
SpeakerRemoved(string speakerName)
### ChatChannel
A ChatChannel is an object that stores data about a single channel that Speakers can chat in. The ChatChannel manages a list of Speakers currently in the channel for relaying messages between them, and also comes with some access permission properties.
#### Properties
string Name (read-only)
string WelcomeMessage
bool Joinable
bool Leavable
bool AutoJoin
bool Private
#### Methods
void RegisterFilterMessageFunction(string functionId, function func, int priority)
bool FilterMessageFunctionExists(string functionId)
void UnregisterFilterMessageFunction(string functionId)
void RegisterProcessCommandsFunction(string functionId, function func, int priority)
bool ProcessCommandsFunctionExists(string functionId)
void UnregisterProcessCommandsFunction(string functionId)
void KickSpeaker(string speakerName, string reason)
void MuteSpeaker(string speakerName, string reason, int length)
void UnmuteSpeaker(string speakerName)
bool IsSpeakerMuted(string speakerName)
string[] GetSpeakerList()
void SendSystemMessage(string message)
MessageObject[] GetHistoryLogForSpeaker(Speaker speaker)
void RegisterGetWelcomeMessageFunction(function func)
void UnRegisterGetWelcomeMessageFunction()
string GetWelcomeMessageForSpeaker(Speaker speaker)
#### Events
MessagePosted(table messageObj)
SpeakerJoined(string speakerName)
SpeakerLeft(string speakerName)
SpeakerMuted(string speakerName, string reason, int length)
SpeakerUnmuted(string speakerName)
### Speaker
A Speaker object is a representation of one entity that can speak in a ChatChannel. A Speaker can be a Player, or it can be a chat bot that is run and managed by code.
#### Properties
string Name (read-only)
#### Methods
void SayMessage(string message, string channelName)
void JoinChannel(string channelName)
void LeaveChannel(string channelName)
string[] GetChannelList()
bool IsInChannel(string channelName)
void SendMessage(string fromSpeaker, string channel, string message)
void SendSystemMessage(string message, string channel)
Player GetPlayer() (returns nil for non-player speakers)
void SetExtraData(string key, Variant data)
Variant GetExtraData(string key)
#### Events
SaidMessage(table messageObject, string channelName)
ReceivedMessage(table messageObject, string channelName)
ReceivedSystemMessage(table messageObject, string channelName)
ChannelJoined(string channelName, string channelWelcomeMessage)
ChannelLeft(string channelName)
Muted(string channelName, string reason, int length)
Unmuted(string channelName)
ExtraDataUpdated(string key, Variant value)
MainChannelSet(string channelName)
___
#### Message Object format
```
{
int ID
string FromSpeaker
int SpeakerUserId
string SpeakerDisplayName
string OriginalChannel
bool IsFiltered
int MessageLength
string Message
string MessageType
int Time
table ExtraData {
Color3 ChatColor
Color3 NameColor
Enum.Font Font
int TextSize
table Tags
}
}
```
Note: Message will not exist on the client if IsFiltered is False
SpeakerUserId will be 0 if the speaker is not a player.
@@ -0,0 +1,363 @@
-- // FileName: Speaker.lua
-- // Written by: Xsitsu
-- // Description: A representation of one entity that can chat in different ChatChannels.
local module = {}
local Chat = game:GetService("Chat")
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatSettings = require(ReplicatedModules:WaitForChild("ChatSettings"))
local modulesFolder = script.Parent
--////////////////////////////// Methods
--//////////////////////////////////////
local function ShallowCopy(table)
local copy = {}
for i, v in pairs(table) do
copy[i] = v
end
return copy
end
local methods = {}
local lazyEventNames =
{
eDestroyed = true,
eSaidMessage = true,
eReceivedMessage = true,
eReceivedUnfilteredMessage = true,
eMessageDoneFiltering = true,
eReceivedSystemMessage = true,
eChannelJoined = true,
eChannelLeft = true,
eMuted = true,
eUnmuted = true,
eExtraDataUpdated = true,
eMainChannelSet = true,
eChannelNameColorUpdated = true,
}
local lazySignalNames =
{
Destroyed = "eDestroyed",
SaidMessage = "eSaidMessage",
ReceivedMessage = "eReceivedMessage",
ReceivedUnfilteredMessage = "eReceivedUnfilteredMessage",
RecievedUnfilteredMessage = "eReceivedUnfilteredMessage", -- legacy mispelling
MessageDoneFiltering = "eMessageDoneFiltering",
ReceivedSystemMessage = "eReceivedSystemMessage",
ChannelJoined = "eChannelJoined",
ChannelLeft = "eChannelLeft",
Muted = "eMuted",
Unmuted = "eUnmuted",
ExtraDataUpdated = "eExtraDataUpdated",
MainChannelSet = "eMainChannelSet",
ChannelNameColorUpdated = "eChannelNameColorUpdated"
}
methods.__index = function (self, k)
local fromMethods = rawget(methods, k)
if fromMethods then return fromMethods end
if lazyEventNames[k] and not rawget(self, k) then
rawset(self, k, Instance.new("BindableEvent"))
end
local lazySignalEventName = lazySignalNames[k]
if lazySignalEventName and not rawget(self, k) then
if not rawget(self, lazySignalEventName) then
rawset(self, lazySignalEventName, Instance.new("BindableEvent"))
end
rawset(self, k, rawget(self, lazySignalEventName).Event)
end
return rawget(self, k)
end
function methods:LazyFire(eventName, ...)
local createdEvent = rawget(self, eventName)
if createdEvent then
createdEvent:Fire(...)
end
end
function methods:SayMessage(message, channelName, extraData)
if self.ChatService:InternalDoProcessCommands(self.Name, message, channelName) then
return
end
if not channelName then
return
end
local channel = self.Channels[channelName:lower()]
if not channel then
return
end
local messageObj = channel:InternalPostMessage(self, message, extraData)
if messageObj then
pcall(function()
self:LazyFire("eSaidMessage", messageObj, channelName)
end)
end
return messageObj
end
function methods:JoinChannel(channelName)
if (self.Channels[channelName:lower()]) then
warn("Speaker is already in channel \"" .. channelName .. "\"")
return
end
local channel = self.ChatService:GetChannel(channelName)
if (not channel) then
error("Channel \"" .. channelName .. "\" does not exist!")
end
self.Channels[channelName:lower()] = channel
channel:InternalAddSpeaker(self)
local success, err = pcall(function()
self.eChannelJoined:Fire(channel.Name, channel:GetWelcomeMessageForSpeaker(self))
end)
if not success and err then
print("Error joining channel: " ..err)
end
end
function methods:LeaveChannel(channelName)
if (not self.Channels[channelName:lower()]) then
warn("Speaker is not in channel \"" .. channelName .. "\"")
return
end
local channel = self.Channels[channelName:lower()]
self.Channels[channelName:lower()] = nil
channel:InternalRemoveSpeaker(self)
local success, err = pcall(function()
self:LazyFire("eChannelLeft", channel.Name)
if self.PlayerObj then
self.EventFolder.OnChannelLeft:FireClient(self.PlayerObj, channel.Name)
end
end)
if not success and err then
print("Error leaving channel: " ..err)
end
end
function methods:IsInChannel(channelName)
return (self.Channels[channelName:lower()] ~= nil)
end
function methods:GetChannelList()
local list = {}
for i, channel in pairs(self.Channels) do
table.insert(list, channel.Name)
end
return list
end
function methods:SendMessage(message, channelName, fromSpeaker, extraData)
local channel = self.Channels[channelName:lower()]
if (channel) then
channel:SendMessageToSpeaker(message, self.Name, fromSpeaker, extraData)
else
warn(string.format("Speaker '%s' is not in channel '%s' and cannot receive a message in it.", self.Name, channelName))
end
end
function methods:SendSystemMessage(message, channelName, extraData)
local channel = self.Channels[channelName:lower()]
if (channel) then
channel:SendSystemMessageToSpeaker(message, self.Name, extraData)
else
warn(string.format("Speaker '%s' is not in channel '%s' and cannot receive a system message in it.", self.Name, channelName))
end
end
function methods:GetPlayer()
return self.PlayerObj
end
function methods:GetNameForDisplay()
if ChatSettings.PlayerDisplayNamesEnabled then
local player = self:GetPlayer()
if player then
return player.DisplayName
else
return self.Name
end
else
return self.Name
end
end
function methods:SetExtraData(key, value)
self.ExtraData[key] = value
self:LazyFire("eExtraDataUpdated", key, value)
end
function methods:GetExtraData(key)
return self.ExtraData[key]
end
function methods:SetMainChannel(channelName)
local success, err = pcall(function()
self:LazyFire("eMainChannelSet", channelName)
if self.PlayerObj then
self.EventFolder.OnMainChannelSet:FireClient(self.PlayerObj, channelName)
end
end)
if not success and err then
print("Error setting main channel: " ..err)
end
end
--- Used to mute a speaker so that this speaker does not see their messages.
function methods:AddMutedSpeaker(speakerName)
self.MutedSpeakers[speakerName:lower()] = true
end
function methods:RemoveMutedSpeaker(speakerName)
self.MutedSpeakers[speakerName:lower()] = false
end
function methods:IsSpeakerMuted(speakerName)
return self.MutedSpeakers[speakerName:lower()]
end
--///////////////// Internal-Use Methods
--//////////////////////////////////////
function methods:InternalDestroy()
for i, channel in pairs(self.Channels) do
channel:InternalRemoveSpeaker(self)
end
self.eDestroyed:Fire()
self.EventFolder = nil
self.eDestroyed:Destroy()
self.eSaidMessage:Destroy()
self.eReceivedMessage:Destroy()
self.eReceivedUnfilteredMessage:Destroy()
self.eMessageDoneFiltering:Destroy()
self.eReceivedSystemMessage:Destroy()
self.eChannelJoined:Destroy()
self.eChannelLeft:Destroy()
self.eMuted:Destroy()
self.eUnmuted:Destroy()
self.eExtraDataUpdated:Destroy()
self.eMainChannelSet:Destroy()
self.eChannelNameColorUpdated:Destroy()
end
function methods:InternalAssignPlayerObject(playerObj)
self.PlayerObj = playerObj
end
function methods:InternalAssignEventFolder(eventFolder)
self.EventFolder = eventFolder
end
function methods:InternalSendMessage(messageObj, channelName)
local success, err = pcall(function()
self:LazyFire("eReceivedUnfilteredMessage", messageObj, channelName)
if self.PlayerObj then
self.EventFolder.OnNewMessage:FireClient(self.PlayerObj, messageObj, channelName)
end
end)
if not success and err then
print("Error sending internal message: " ..err)
end
end
function methods:InternalSendFilteredMessage(messageObj, channelName)
local success, err = pcall(function()
self:LazyFire("eReceivedMessage", messageObj, channelName)
self:LazyFire("eMessageDoneFiltering", messageObj, channelName)
if self.PlayerObj then
self.EventFolder.OnMessageDoneFiltering:FireClient(self.PlayerObj, messageObj, channelName)
end
end)
if not success and err then
print("Error sending internal filtered message: " ..err)
end
end
--// This method is to be used with the new filter API. This method takes the
--// TextFilterResult objects and converts them into the appropriate string
--// messages for each player.
function methods:InternalSendFilteredMessageWithFilterResult(inMessageObj, channelName)
local messageObj = ShallowCopy(inMessageObj)
local oldFilterResult = messageObj.FilterResult
local player = self:GetPlayer()
local msg = ""
pcall(function()
if (messageObj.IsFilterResult) then
if (player) then
msg = oldFilterResult:GetChatForUserAsync(player.UserId)
else
msg = oldFilterResult:GetNonChatStringForBroadcastAsync()
end
else
msg = oldFilterResult
end
end)
--// Messages of 0 length are the result of two users not being allowed
--// to chat, or GetChatForUserAsync() failing. In both of these situations,
--// messages with length of 0 should not be sent.
if (#msg > 0) then
messageObj.Message = msg
messageObj.FilterResult = nil
self:InternalSendFilteredMessage(messageObj, channelName)
end
end
function methods:InternalSendSystemMessage(messageObj, channelName)
local success, err = pcall(function()
self:LazyFire("eReceivedSystemMessage", messageObj, channelName)
if self.PlayerObj then
self.EventFolder.OnNewSystemMessage:FireClient(self.PlayerObj, messageObj, channelName)
end
end)
if not success and err then
print("Error sending internal system message: " ..err)
end
end
function methods:UpdateChannelNameColor(channelName, channelNameColor)
self:LazyFire("eChannelNameColorUpdated", channelName, channelNameColor)
if self.PlayerObj then
self.EventFolder.ChannelNameColorUpdated:FireClient(self.PlayerObj, channelName, channelNameColor)
end
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new(vChatService, name)
local obj = setmetatable({}, methods)
obj.ChatService = vChatService
obj.PlayerObj = nil
obj.Name = name
obj.ExtraData = {}
obj.Channels = {}
obj.MutedSpeakers = {}
obj.EventFolder = nil
return obj
end
return module
@@ -0,0 +1,110 @@
-- // FileName: Util.lua
-- // Written by: TheGamer101
-- // Description: Utility code used by the server side chat implementation.
local Chat = game:GetService("Chat")
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants"))
local DEFAULT_PRIORITY = ChatConstants.StandardPriority
if DEFAULT_PRIORITY == nil then
DEFAULT_PRIORITY = 10
end
local Util = {}
Util.__index = Util
local SortedFunctionContainer = {}; do
-- This sorted function container is used to handle the logic around storing filter functions and
-- command processors by priority.
local methods = {}
methods.__index = methods
function methods:RebuildProcessCommandsPriorities()
self.RegisteredPriorites = {}
for priority, functions in pairs(self.FunctionMap) do
local functionsEmpty = true
for funcId, funciton in pairs(functions) do
functionsEmpty = false
break
end
if not functionsEmpty then
table.insert(self.RegisteredPriorites, priority)
end
end
table.sort(self.RegisteredPriorites, function(a, b)
return a > b
end)
end
function methods:HasFunction(funcId)
if self.RegisteredFunctions[funcId] == nil then
return false
end
return true
end
function methods:RemoveFunction(funcId)
local functionPriority = self.RegisteredFunctions[funcId]
self.RegisteredFunctions[funcId] = nil
self.FunctionMap[functionPriority][funcId] = nil
self:RebuildProcessCommandsPriorities()
end
function methods:AddFunction(funcId, func, priority)
if priority == nil then
priority = DEFAULT_PRIORITY
end
if self.RegisteredFunctions[funcId] then
error(funcId .. " is already in use!")
end
self.RegisteredFunctions[funcId] = priority
if self.FunctionMap[priority] == nil then
self.FunctionMap[priority] = {}
end
self.FunctionMap[priority][funcId] = func
self:RebuildProcessCommandsPriorities()
end
function methods:GetIterator()
local priorityIndex = 1
local funcId = nil
local func = nil
return function()
while true do
if priorityIndex > #self.RegisteredPriorites then
return
end
local priority = self.RegisteredPriorites[priorityIndex]
funcId, func = next(self.FunctionMap[priority], funcId)
if funcId == nil then
priorityIndex = priorityIndex + 1
else
return funcId, func, priority
end
end
end
end
function SortedFunctionContainer.new()
local obj = setmetatable({}, methods)
obj.RegisteredFunctions = {}
obj.RegisteredPriorites = {}
obj.FunctionMap = {}
return obj
end
end
function Util:NewSortedFunctionContainer()
return SortedFunctionContainer.new()
end
return Util
@@ -0,0 +1,10 @@
{
"lint": {
"SameLineStatement": "fatal",
"LocalShadow": "fatal",
"LocalUnused": "fatal",
"FunctionUnused": "fatal",
"ImportUnused": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,8 @@
-- If you want to create your own custom player settings,
-- make a copy of this script and add it to your StarterPlayer object
local module = {}
module.UseDefaultAnimations = false -- force incoming players to use default avatar animations, not custom animations
return module
@@ -0,0 +1,17 @@
local Install = function ()
local playerConfig = game:GetService("StarterPlayer"):FindFirstChild("PlayerSettings")
if (playerConfig == nil) then
playerConfig = Instance.new("ModuleScript")
playerConfig.Name = "PlayerSettings"
playerConfig.Source = script.Parent.DefaultServerPlayerModules:WaitForChild("PlayerSettings").Source
playerConfig.Parent = game:GetService("StarterPlayer")
playerConfig.Archivable = false
end
end
return Install
@@ -0,0 +1,9 @@
{
"lint": {
"SameLineStatement": "fatal",
"LocalUnused": "fatal",
"FunctionUnused": "fatal",
"ImportUnused": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,65 @@
--[[
Server-side network glue for the legacy avatar sound system.
The legacy system supported by this script has known bugs and performance issues!
Switch to the new system by deleting StarterPlayer.StarterCharacterScripts.Sound.
The sound dispatcher will fire sound events to properly loaded characters. This script manages a list of
characters currently loaded in the game. When a character fires a sound event, this dispatcher will
check to make sure the event only fires on characters who have loaded in.
--]]
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SOUND_EVENT_FOLDER_NAME = "DefaultSoundEvents"
local loadedCharacters = {}
local EventFolder = ReplicatedStorage:FindFirstChild(SOUND_EVENT_FOLDER_NAME)
if not EventFolder then
EventFolder = Instance.new("Folder")
EventFolder.Name = SOUND_EVENT_FOLDER_NAME
EventFolder.Archivable = false
EventFolder.Parent = ReplicatedStorage
end
local function createEvent(name, instanceType)
local newEvent = EventFolder:FindFirstChild(name)
if not newEvent then
newEvent = Instance.new(instanceType)
newEvent.Name = name
newEvent.Parent = EventFolder
end
return newEvent
end
local DefaultServerSoundEvent = createEvent("DefaultServerSoundEvent", "RemoteEvent")
local AddCharacterLoadedEvent = createEvent("AddCharacterLoadedEvent", "RemoteEvent")
local RemoveCharacterEvent = createEvent("RemoveCharacterEvent", "RemoteEvent")
-- Fire the sound event to all clients connected
local function fireDefaultServerSoundEventToClient(player, sound, playing, resetPosition)
if loadedCharacters[player] then
DefaultServerSoundEvent:FireClient(player, sound, playing, resetPosition)
end
end
-- Add a character to the list of clients ready to receive sounds
local function addCharacterLoaded(player)
loadedCharacters[player] = true
end
-- Remove a character from the table
local function removeCharacter(player)
loadedCharacters[player] = nil
end
local soundDispatcher = createEvent("SoundDispatcher", "BindableEvent")
soundDispatcher.Event:Connect(fireDefaultServerSoundEventToClient)
--no op function to prevent rogue client from filling RemoteEvent queue
DefaultServerSoundEvent.OnServerEvent:Connect(function() end)
AddCharacterLoadedEvent.OnServerEvent:Connect(addCharacterLoaded)
RemoveCharacterEvent.OnServerEvent:Connect(removeCharacter)
Players.PlayerRemoving:Connect(removeCharacter)
@@ -0,0 +1,53 @@
-- Load the legacy SoundDispatcher script into the server if the rest of the old system is present
local SOUND_DISPATCHER_NAME = "SoundDispatcher"
local OLD_SOUND_SCRIPT_NAME = "Sound"
local ServerScriptService = game:GetService("ServerScriptService")
local StarterPlayer = game:GetService("StarterPlayer")
local function HasOldSoundScripts()
if ServerScriptService:FindFirstChild(SOUND_DISPATCHER_NAME) then
return true
end
local StarterCharacterScripts = StarterPlayer:FindFirstChildOfClass("StarterCharacterScripts")
if StarterCharacterScripts and StarterCharacterScripts:FindFirstChild(OLD_SOUND_SCRIPT_NAME) then
return true
end
return false
end
local function LoadScript(name, parent)
local originalModule = script.Parent:WaitForChild(name)
local script = Instance.new("Script")
script.Name = name
script.Source = originalModule.Source
script.Parent = parent
return script
end
local function TryInstall()
if not HasOldSoundScripts() then
-- use the new system
return
end
local soundDispatcherArchivable = true
local SoundDispatcher = ServerScriptService:FindFirstChild(SOUND_DISPATCHER_NAME)
if not SoundDispatcher then
soundDispatcherArchivable = false
SoundDispatcher = LoadScript(SOUND_DISPATCHER_NAME, ServerScriptService)
end
if not ServerScriptService:FindFirstChild(SOUND_DISPATCHER_NAME) then
local SoundDispatcherCopy = SoundDispatcher:Clone()
SoundDispatcherCopy.Archivable = false
SoundDispatcherCopy.Parent = ServerScriptService
end
SoundDispatcher.Archivable = soundDispatcherArchivable
end
return TryInstall