%9% --[[ //FileName: ChatScript.LUA //Written by: Sorcus //Description: Code for lua side chat on ROBLOX. Supports Scrolling. //NOTE: If you find any bugs or inaccuracies PM Sorcus on ROBLOX or @Canavus on Twitter ]] -- Utility functions + Globals local function WaitForChild(parent, childName) while parent:FindFirstChild(childName) == nil do parent.ChildAdded:wait(0.03) end return parent[childName] end local function typedef(obj) return obj end local function IsPhone() local cGui = Game:GetService('CoreGui') local rGui = WaitForChild(cGui, 'RobloxGui') if rGui.AbsoluteSize.Y < 600 then return true end return false end -- Users can use enough white spaces to spoof chatting as other players -- This function removes trailing and leading white spaces -- AFAIK, there is no reason for spam white spaces local function StringTrim(str) -- %S is whitespaces -- When we find the first non space character defined by ^%s -- we yank out anything in between that and the end of the string -- Everything else is replaced with %1 which is essentially nothing return (str:gsub("^%s*(.-)%s*$", "%1")) end while Game.Players.LocalPlayer == nil do wait(0.03) end local Player = Game.Players.LocalPlayer while Player.Character == nil do wait(0.03) end local RbxUtility = LoadLibrary('RbxUtility') local Gui = typedef(RbxUtility) local Camera = Game.Workspace.CurrentCamera -- Services local CoreGuiService = Game:GetService('CoreGui') local PlayersService = Game:GetService('Players') local DebrisService= Game:GetService('Debris') local GuiService = Game:GetService('GuiService') -- Lua Enums local Enums do Enums = {} local EnumName = {} -- used as unique key for enum name local enum_mt = { __call = function(self,value) return self[value] or self[tonumber(value)] end; __index = { GetEnumItems = function(self) local t = {} for i,item in pairs(self) do if type(i) == 'number' then t[#t+1] = item end end table.sort(t,function(a,b) return a.Value < b.Value end) return t end; }; __tostring = function(self) return "Enum." .. self[EnumName] end; } local item_mt = { __call = function(self,value) return value == self or value == self.Name or value == self.Value end; __tostring = function(self) return "Enum." .. self[EnumName] .. "." .. self.Name end; } function CreateEnum(enumName) return function(t) local e = {[EnumName] = enumName} for i,name in pairs(t) do local item = setmetatable({Name=name,Value=i,Enum=e,[EnumName]=enumName},item_mt) e[i] = item e[name] = item e[item] = item end Enums[enumName] = e return setmetatable(e, enum_mt) end end end --------------------------------------------------- ------------------ Input class -------------------- local Input = { Mouse = Player:GetMouse(), Speed = 0, Simulating = false, Configuration = { DefaultSpeed = 1 }, UserIsScrolling = false } --------------------------------------------------- -- For touch devices we create a button instead function Chat:CreateTouchButton() self.ChatTouchFrame = Gui.Create'Frame' { Name = 'ChatTouchFrame'; Size = UDim2.new(0, 128, 0, 32); Position = UDim2.new(0, 88, 0, 0); BackgroundTransparency = 1.0; Parent = self.Gui; Gui.Create'ImageButton' { Name = 'ChatLabel'; Size = UDim2.new(0, 74, 0, 28); Position = UDim2.new(0, 0, 0, 0); BackgroundTransparency = 1.0; ZIndex = 2.0; }; Gui.Create'ImageLabel' { Name = 'Background'; Size = UDim2.new(1, 0, 1, 0); Position = UDim2.new(0, 0, 0, 0); BackgroundTransparency = 1.0; Image = 'http://www.morblox.us/asset/?id=97078724' }; } self.TapToChatLabel = self.ChatTouchFrame.ChatLabel self.TouchLabelBackground = self.ChatTouchFrame.Background self.ChatBar = Gui.Create'TextBox' { Name = 'ChatBar'; Size = UDim2.new(1, 0, 0.2, 0); Position = UDim2.new(0, 0, 0.8, 800); Text = ""; ZIndex = 1.0; BackgroundTransparency = 1.0; Parent = self.Frame; TextXAlignment = Enum.TextXAlignment.Left; TextColor3 = Color3.new(1, 1, 1); ClearTextOnFocus = false; }; self.TapToChatLabel.MouseButton1Click:connect(function() self.TapToChatLabel.Visible = false --self.ChatBar.Visible = true --self.Frame.Background.Visible = true self.ChatBar:CaptureFocus() self.GotFocus = true if self.TouchLabelBackground then self.TouchLabelBackground.Visible = false end end) end -- Non touch devices, create the bottom chat bar function Chat:CreateChatBar() -- okay now we do local status, result = pcall(function() return GuiService.UseLuaChat end) if status and result then self.ClickToChatButton = Gui.Create'TextButton' { Name = 'ClickToChat'; Size = UDim2.new(1, 0, 0, 20); BackgroundTransparency = 1.0; ZIndex = 2.0; Parent = self.Gui; Text = "To chat click here or press \"/\" key"; TextColor3 = Color3.new(1, 1, 0.9); Position = UDim2.new(0, 0, 1, 0); TextXAlignment = Enum.TextXAlignment.Left; FontSize = Enum.FontSize.Size12; } self.ChatBar = Gui.Create'TextBox' { Name = 'ChatBar'; Size = UDim2.new(1, 0, 0, 20); Position = UDim2.new(0, 0, 1, 0); Text = ""; ZIndex = 1.0; BackgroundColor3 = Color3.new(0, 0, 0); BackgroundTransparency = 0.25; Parent = self.Gui; TextXAlignment = Enum.TextXAlignment.Left; TextColor3 = Color3.new(1, 1, 1); FontSize = Enum.FontSize.Size12; ClearTextOnFocus = false; Text = ''; }; -- Engine has code to offset the entire world, so if we do it by -20 pixels nothing gets in our chat's way --GuiService:SetGlobalSizeOffsetPixel(0, -20) local success, error = pcall(function() GuiService:SetGlobalGuiInset(0, 0, 0, 20) end) if not success then GuiService:SetGlobalSizeOffsetPixel(0, -20) end -- CHatHotKey is '/' GuiService:AddSpecialKey(Enum.SpecialKey.ChatHotkey) GuiService.SpecialKeyPressed:connect(function(key) if key == Enum.SpecialKey.ChatHotkey then Chat:FocusOnChatBar() end end) self.ClickToChatButton.MouseButton1Click:connect(function() Chat:FocusOnChatBar() end) end end -- Create the initial Chat stuff -- Done only once function Chat:CreateGui() self.Gui = WaitForChild(CoreGuiService, 'RobloxGui') self.Frame = Gui.Create'Frame' { Name = 'ChatFrame'; --Size = self.Configuration.Size; Size = UDim2.new(0, 500, 0, 120); Position = UDim2.new(0, 0, 0, 5); BackgroundTransparency = 1.0; --ClipsDescendants = true; ZIndex = 0.0; Parent = self.Gui; Active = false; Gui.Create'ImageLabel' { Name = 'Background'; Image = 'http://www.morblox.us/asset/?id=97120937'; --96551212'; Size = UDim2.new(1.3, 0, 1.64, 0); Position = UDim2.new(0, 0, 0, 0); BackgroundTransparency = 1.0; ZIndex = 0.0; Visible = false }; Gui.Create'Frame' { Name = 'Border'; Size = UDim2.new(1, 0, 0, 1); Position = UDim2.new(0, 0, 0.8, 0); BackgroundTransparency = 0.0; BackgroundColor3 = Color3.new(236/255, 236/255, 236/255); BorderSizePixel = 0.0; Visible = false; }; Gui.Create'Frame' { Name = 'ChatRenderFrame'; Size = UDim2.new(1.02, 0, 1.01, 0); Position = UDim2.new(0, 0, 0, 0); BackgroundTransparency = 1.0; --ClipsDescendants = true; ZIndex = 0.0; Active = false; }; }; Spawn(function() wait(0.5) if IsPhone() then self.Frame.Size = UDim2.new(0, 280, 0, 120) end end) self.RenderFrame = self.Frame.ChatRenderFrame if Chat:IsTouchDevice() then self.Frame.Position = self.Configuration.Position; self.RenderFrame.Size = UDim2.new(1, 0, 1, 0) elseif self.Frame.AbsoluteSize.Y > 120 then Chat:ScreenSizeChanged() self.Gui.Changed:connect(function(property) if property == 'AbsoluteSize' then Chat:ScreenSizeChanged() end end) end if Player.ChatMode == Enum.ChatMode.TextAndMenu then if Chat:IsTouchDevice() then Chat:CreateTouchButton() else Chat:CreateChatBar() --Chat:CreateSafeChatGui() end if self.ChatBar then self.ChatBar.FocusLost:connect(function(enterPressed) Chat.GotFocus = false if Chat:IsTouchDevice() then self.ChatBar.Visible = false self.TapToChatLabel.Visible = true if self.TouchLabelBackground then self.TouchLabelBackground.Visible = true end end if enterPressed and self.ChatBar.Text ~= "" then local cText = self.ChatBar.Text if string.sub(self.ChatBar.Text, 1, 1) == '%' then cText = '(TEAM) ' .. string.sub(cText, 2, #cText) pcall(function() PlayersService:TeamChat(cText) end) else pcall(function() PlayersService:Chat(cText) end) end if self.ClickToChatButton then self.ClickToChatButton.Visible = true end self.ChatBar.Text = "" end Spawn(function() wait(5.0) if not Chat.GotFocus then Chat.Frame.Background.Visible = false end end) end) end end end -- Scrolling function -- Applies a speed(velocity) to have nice scrolling effect function Input:OnMouseScroll() Spawn(function() -- How long should the speed last? while Input.Speed ~=0 do if Input.Speed > 1 then while Input.Speed > 0 do Input.Speed = Input.Speed - 1 wait(0.25) end elseif Input.Speed < 0 then while Input.Speed < 0 do Input.Speed = Input.Speed + 1 wait(0.25) end end wait(0.03) end end) if Chat:CheckIfInBounds(Input.Speed) then return end Chat:ScrollQueue() end function Input:ApplySpeed(value) Input.Speed = Input.Speed + value if not self.Simulating then Input:OnMouseScroll() end end function Input:Initialize() self.Mouse.WheelBackward:connect(function() Input:ApplySpeed(self.Configuration.DefaultSpeed) end) self.Mouse.WheelForward:connect(function() Input:ApplySpeed(self.Configuration.DefaultSpeed) end) end function Chat:FindMessageInSafeChat(message, list) local foundMessage = false for msg, _ in pairs(list) do if msg == message then return true end if type(list[msg]) == 'table' then foundMessage = Chat:FindMessageInSafeChat(message, list[msg]) if foundMessage then return true end end end return foundMessage end -- Just a wrapper around our PlayerChatted event function Chat:PlayerChatted(...) local args = {...} local argCount = select('#', ...) local player local message -- This doesn't look very good, but what else to do? if args[2] then player = args[2] end if args[3] then message = args[3] if string.sub(message, 1, 1) == '%' then message = '(TEAM) ' .. string.sub(message, 2, #message) end end if PlayersService.ClassicChat then if Player.ChatMode == Enum.ChatMode.TextAndMenu then Chat:UpdateChat(player, message) elseif Player.ChatMode == Enum.ChatMode.Menu and string.sub(message, 3) == '/sc' then Chat:UpdateChat(player, message) else if Chat:FindMessageInSafeChat(message, self.SafeChat_List) then Chat:UpdateChat(player, message) end end end end -- After Chat.Configuration.Lifetime seconds of existence, the labels become invisible -- Runs only every 5 seconds and has to loop through 50 values -- Shouldn't be too expensive function Chat:CullThread() while true do if #self.MessageQueue > 0 then for _, field in pairs(self.MessageQueue) do if field['SpawnTime'] and field['Player'] and field['Message'] and tick() - field['SpawnTime'] > self.Configuration.LifeTime then field['Player'].Visible = false field['Message'].Visible = false end end end wait(5.0) end end -- RobloxLock everything so users can't delete them(?) function Chat:LockAllFields(gui) local children = gui:GetChildren() for i = 1, #children do children[i].RobloxLocked = true if #children[i]:GetChildren() > 0 then Chat:LockAllFields(children[i]) end end end -- Constructor -- This function initializes everything function Chat:Initialize() Chat:CreateGui() self.EventListener = PlayersService.PlayerChatted:connect(function(...) -- This event has 4 callback arguments -- Enum.PlayerChatType.All, chatPlayer, message, targetPlayer Chat:PlayerChatted(...) end) self.MessageThread = coroutine.create(function() end) coroutine.resume(self.MessageThread) -- Initialize input for us Input:Initialize() -- Eww, everytime a player is added, you have to redo the connection -- Seems this is not automatic -- NOTE: PlayerAdded only fires on the server, hence ChildAdded is used here PlayersService.ChildAdded:connect(function() Chat.EventListener:disconnect() self.EventListener = PlayersService.PlayerChatted:connect(function(...) -- This event has 4 callback arguments -- Enum.PlayerChatType.All, chatPlayer, message, targetPlayer Chat:PlayerChatted(...) end) end) Spawn(function() Chat:CullThread() end) self.Frame.RobloxLocked = true Chat:LockAllFields(self.Frame) self.Frame.DescendantAdded:connect(function(descendant) Chat:LockAllFields(descendant) end) end Chat:Initialize()