511 lines
14 KiB
Plaintext
511 lines
14 KiB
Plaintext
%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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|