-- https://github.com/wally-rblx local Instance_new = Instance.new local UDim2_new = UDim2.new local Color3_new = Color3.new local math_max = math.max local tick = tick local pairs = pairs local os_time = os.time local DEBUG = false if syn then require = getrenv().require end local AnalyticsCategory_Game = "Game" local AnalyticsAction_InitialOpenTab = "DeveloperConsole_InitialOpenTab" local AnalyticsAction_ClickToOpenOpenTab = "DeveloperConsole_ClickToOpenOpenTab" local CoreGui = game:GetService("CoreGui") local RobloxGui = CoreGui:FindFirstChild("RobloxGui") local Modules = RobloxGui:FindFirstChild("Modules") local ContextActionService = game:GetService("ContextActionService") local TextService = game:GetService("TextService") local GuiService = game:GetService("GuiService") local VRService = game:GetService("VRService") local isTenFootInterface = GuiService:IsTenFootInterface() local ClientMemoryAnalyzerClass = require(CoreGui:WaitForChild("RobloxGui"):WaitForChild("Modules"):WaitForChild("Stats"):WaitForChild("ClientMemoryAnalyzer")) local ServerMemoryAnalyzerClass = require(CoreGui:WaitForChild("RobloxGui"):WaitForChild("Modules"):WaitForChild("Stats"):WaitForChild("ServerMemoryAnalyzer")) local StatsUtils = require(CoreGui:WaitForChild("RobloxGui"):WaitForChild("Modules"):WaitForChild("Stats"):WaitForChild("StatsUtils")) local Style; do local c3 = Color3.fromRGB; local frameColor = Color3.new(0.1, 0.1, 0.1) local textColor = Color3.new(1, 1, 1) local optionsFrameColor = Color3.new(1, 1, 1) pcall(function() -- Fun window colors for cool people local Players = game:GetService("Players") if not Players or not Players.LocalPlayer then return end local FunColors = { [56449] = {c3(255, 63, 127)}; -- ReeseMcBlox [6949935] = {c3(255, 63, 127)}; -- NobleDragon } local funColor = FunColors[Players.LocalPlayer.UserId] if funColor then frameColor = funColor[1] or frameColor textColor = funColor[2] or textColor end end) Style = { ZINDEX = 6; Font = Enum.Font.SourceSans; FontBold = Enum.Font.SourceSansBold; HandleHeight = 24; -- How tall the top window handle is, as well as the width of the scroll bar TabHeight = 28; GearSize = 24; BorderSize = 2; CommandLineHeight = 22; OptionAreaHeight = 56; FrameColor = frameColor; -- Applies to pretty much everything, including buttons FrameTransparency = 0.5; OptionsFrameColor = optionsFrameColor; TextColor = textColor; MessageColors = { [0] = Color3.new(1, 1, 1); -- Enum.MessageType.MessageOutput [1] = Color3.new(0.4, 0.5, 1); -- Enum.MessageType.MessageInfo [2] = Color3.new(1, 0.6, 0.4); -- Enum.MessageType.MessageWarning [3] = Color3.new(1, 0, 0); -- Enum.MessageType.MessageError }; ScrollbarFrameColor = frameColor; ScrollbarBarColor = frameColor; ScriptButtonHeight = 32; ScriptButtonColor = Color3.new(0, 1/3, 2/3); ScriptButtonTransparency = 0.5; CheckboxSize = 24; ChartTitleHeight = 20; ChartGraphHeight = 64; ChartDataHeight = 24; ChartHeight = 0; -- This gets added up at end and set at end of block ChartWidth = 620; -- (-1) means right to left -- (1) means left to right ChartGraphDirection = 1; -- the direction the bars move GetButtonDownColor = function(normalColor) local r, g, b = normalColor.r, normalColor.g, normalColor.b return Color3.new(1 - 0.75 * (1 - r), 1 - 0.75 * (1 - g), 1 - 0.75 * (1 - b)) end; GetButtonHoverColor = function(normalColor) local r, g, b = normalColor.r, normalColor.g, normalColor.b return Color3.new(1 - 0.875 * (1 - r), 1 - 0.875 * (1 - g), 1 - 0.875 * (1 - b)) end; } Style.ChartHeight = Style.ChartTitleHeight + Style.ChartGraphHeight + Style.ChartDataHeight + Style.BorderSize end local Primitives = {}; do local function new(className, parent, name) local n = Instance.new(className, parent) n.ZIndex = 0 if name then n.Name = name end return n end local unitSize = UDim2.new(1, 0, 1, 0) local function setupFrame(n) n.BackgroundColor3 = Style.FrameColor n.BackgroundTransparency = Style.FrameTransparency n.BorderSizePixel = 0 end local function setupText(n, text) n.Font = Style.Font n.TextColor3 = Style.TextColor n.Text = text or n.Text end function Primitives.Frame(parent, name) local n = new('Frame', parent, name) setupFrame(n) return n end function Primitives.TextLabel(parent, name, text) local n = new('TextLabel', parent, name) setupFrame(n) setupText(n, text) return n end function Primitives.TextBox(parent, name, text) local n = new('TextBox', parent, name) setupFrame(n) setupText(n, text) return n end function Primitives.TextButton(parent, name, text) local n = new('TextButton', parent, name) setupFrame(n) setupText(n, text) return n end function Primitives.Button(parent, name) local n = new('TextButton', parent, name) setupFrame(n) n.Text = "" return n end function Primitives.ImageButton(parent, name, image) local n = new('ImageButton', parent, name) setupFrame(n) n.Image = image or "" n.Size = unitSize return n end -- An invisible frame of size (1, 0, 1, 0) function Primitives.FolderFrame(parent, name) -- Should this be called InvisibleFrame? lol local n = new('Frame', parent, name) n.BackgroundTransparency = 1 n.Size = unitSize return n end function Primitives.InvisibleTextLabel(parent, name, text) local n = new('TextLabel', parent, name) setupText(n, text) n.BackgroundTransparency = 1 return n end function Primitives.InvisibleButton(parent, name, text) local n = new('TextButton', parent, name) n.BackgroundTransparency = 1 n.Text = "" return n end function Primitives.InvisibleImageLabel(parent, name, image) local n = new('ImageLabel', parent, name) n.BackgroundTransparency = 1 n.Image = image or "" n.Size = unitSize return n end end --[[ Flags ]]-- local function checkFFlag(flagName) local flagSuccess, flagValue = pcall(function() return settings():GetFFlag(flagName) end) return (flagSuccess and flagValue) end -- Eye candy uses RenderStepped local EYECANDY_ENABLED = true local AUTO_TAB_WIDTH = -1 local TAB_TEXT_SIZE = 14 local TAB_TEXT_PADDING = 8 local function CreateSignal() local this = {} local mBindableEvent = Instance.new('BindableEvent') local mAllCns = {} --all connection objects returned by mBindableEvent::connect --main functions function this:connect(func) if self ~= this then error("connect must be called with `:`, not `.`", 2) end if type(func) ~= 'function' then error("Argument #1 of connect must be a function, got a "..type(func), 2) end local cn = mBindableEvent.Event:Connect(func) mAllCns[cn] = true local pubCn = {} function pubCn:disconnect() cn:Disconnect() mAllCns[cn] = nil end pubCn.Disconnect = pubCn.disconnect return pubCn end function this:disconnect() if self ~= this then error("disconnect must be called with `:`, not `.`", 2) end for cn, _ in pairs(mAllCns) do cn:Disconnect() mAllCns[cn] = nil end end function this:wait() if self ~= this then error("wait must be called with `:`, not `.`", 2) end return mBindableEvent.Event:Wait() end function this:fire(...) if self ~= this then error("fire must be called with `:`, not `.`", 2) end mBindableEvent:Fire(...) end this.Connect = this.connect this.Disconnect = this.disconnect this.Wait = this.wait this.Fire = this.fire return this end -- This is a Signal that only calls once, then forgets about the function. It also accepts event listeners as functions local CreateDisconnectSignal; do local Methods = {} local Metatable = {__index = Methods} function Methods.fire(this, ...) return this.Signal:fire(...) end function Methods.wait(this, ...) return this.Signal:wait(...) end function Methods.connect(this, func) local t = type(func) if t == 'table' or t == 'userdata' then -- Got event listener local listener = func function func() listener:disconnect() end elseif t ~= 'function' then error('Invalid disconnect method type: ' .. t, 2) end local listener; listener = this.Signal:connect(function(...) if listener then listener:disconnect() listener = nil func(...) end end) return listener end function CreateDisconnectSignal() return setmetatable({ Signal = CreateSignal(); }, Metatable) end end -- Services local UserInputService = game:GetService('UserInputService') local RunService = game:GetService('RunService') local TouchEnabled = UserInputService.TouchEnabled local DeveloperConsole = {} local Methods = {} local Metatable = {__index = Methods} ------------------------- -- Listener management -- ------------------------- function Methods.ConnectSetVisible(devConsole, func) -- This is used mainly for pausing rendering and stuff when the console isn't visible func(devConsole.Visible) return devConsole.VisibleChanged:connect(function(visible) func(visible) end) end function Methods.ConnectObjectSetVisible(devConsole, object, func) -- Same as above, but used for calling methods like object:SetVisible() func(object, devConsole.Visible) return devConsole.VisibleChanged:connect(function(visible) func(object, visible) end) end ----------------------------- -- Frame/Window Dimensions -- ----------------------------- local function connectPropertyChanged(object, property, callback) return object.Changed:connect(function(propertyChanged) if propertyChanged == property then callback(object[property]) end end) end function Methods.ResetFrameDimensions(devConsole) devConsole.Frame.Size = UDim2_new(0.5, 20, 0.5, 20); local abSize = devConsole.Frame.AbsoluteSize devConsole:SetFrameSize(abSize.x, abSize.y) local newSize = devConsole.Frame.Size devConsole.Frame.Position = UDim2_new(0.5, -newSize.X.Offset/2, 0.5, -newSize.Y.Offset/2) end function Methods.BoundFrameSize(devConsole, x, y) -- Minimum frame size return math_max(x, 400), math_max(y, 200) end function Methods.SetFrameSize(devConsole, x, y) x, y = devConsole:BoundFrameSize(x, y) devConsole.Frame.Size = UDim2_new(0, x, 0, y) end function Methods.BoundFramePosition(devConsole, x, y) -- Make sure the frame doesn't go somewhere where the bar can't be clicked return x, math_max(y, 0) end function Methods.SetFramePosition(devConsole, x, y) x, y = devConsole:BoundFramePosition(x, y) devConsole.Frame.Position = UDim2_new(0, x, 0, y) end -- Open/Close the console function Methods.SetVisible(devConsole, visible, animate) if devConsole.Visible == visible then return end devConsole.Visible = visible devConsole.VisibleChanged:fire(visible) if devConsole.Frame then devConsole.Frame.Visible = visible end if visible then -- Open the console devConsole:ResetFrameDimensions() local tab = devConsole:GetCurrentOpenTab() if (tab ~= nil) then tab:RecordInitialOpen() end end if VRService.VREnabled then if visible then UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceShow else UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide end end end ----------------- -- Constructor -- ----------------- function DeveloperConsole.new(screenGui, permissions, messagesAndStats) local visibleChanged = CreateSignal() local devConsole = { ScreenGui = screenGui; Permissions = permissions; MessagesAndStats = messagesAndStats; Initialized = false; Visible = false; Tabs = {}; CurrentOpenedTab = nil; -- save last tab opened to set SelectedCoreObject for TenFootInterfaces VisibleChanged = visibleChanged; -- Created by :Initialize(); It's used to stop and disconnect things when the window is hidden } setmetatable(devConsole, Metatable) devConsole:EnableGUIMouse() -- It's a button so it catches mouse events local frame = Primitives.Button(screenGui, 'DeveloperConsole') frame.AutoButtonColor = false --frame.ClipsDescendants = true frame.Visible = devConsole.Visible frame.Selectable = not isTenFootInterface local function onVREnabled() frame.Modal = VRService.VREnabled end onVREnabled() VRService:GetPropertyChangedSignal("VREnabled"):connect(onVREnabled) devConsole.Frame = frame devConsole:ResetFrameDimensions() -- The bar at the top that you can drag around local handle = Primitives.Button(frame, 'Handle') handle.Size = UDim2_new(1, -(Style.HandleHeight + Style.BorderSize), 0, Style.HandleHeight) handle.Selectable = not isTenFootInterface handle.Modal = true -- Unlocks mouse handle.AutoButtonColor = false do -- Title local title = Primitives.InvisibleTextLabel(handle, 'Title', "Developer Console") title.Size = UDim2_new(1, -5, 1, 0) title.Position = UDim2_new(0, 5, 0, 0) title.FontSize = Enum.FontSize.Size18 title.TextXAlignment = Enum.TextXAlignment.Left end local function setCornerButtonImageSize(buttonImage, buttonImageSize) buttonImage.Size = UDim2_new(buttonImageSize, 0, buttonImageSize, 0) buttonImage.Position = UDim2_new((1 - buttonImageSize) / 2, 0, (1 - buttonImageSize) / 2, 0) end -- This is used for creating the square exit button and the square window resize button local function createCornerButton(name, x, y, image, buttonImageSize) -- Corners (x, y): -- (0, 0) (1, 0) -- (0, 1) (1, 1) local button = Primitives.Button(frame, name) button.Size = UDim2_new(0, Style.HandleHeight, 0, Style.HandleHeight) button.Position = UDim2_new(x, -x * Style.HandleHeight, y, -y * Style.HandleHeight) local buttonImage = Primitives.InvisibleImageLabel(button, 'Image', image) setCornerButtonImageSize(buttonImage, buttonImageSize) return button, buttonImage end do -- Create top right exit button local exitButton, exitButtonImage = createCornerButton('Exit', 1, 0, 'https://www.roblox.com/asset/?id=261878266', 2/3) exitButton.AutoButtonColor = false exitButton.Visible = not isTenFootInterface exitButton.Selectable = not isTenFootInterface local buttonEffectFunction = devConsole:CreateButtonEffectFunction(exitButton) devConsole:ConnectButtonHover(exitButton, function(clicking, hovering) if hovering and not clicking then setCornerButtonImageSize(exitButtonImage, 3/4) else setCornerButtonImageSize(exitButtonImage, 2/3) end buttonEffectFunction(clicking, hovering) end) exitButton.MouseButton1Click:connect(function() devConsole:SetVisible(false, true) end) end do -- Repositioning and Resizing do -- Create bottom right window resize button and activate resize dragging local resizeButton, resizeButtonImage = createCornerButton('Resize', 1, 1, 'https://www.roblox.com/asset/?id=261880743', 1) resizeButtonImage.Position = UDim2_new(0, 0, 0, 0) resizeButtonImage.Size = UDim2_new(1, 0, 1, 0) resizeButton.Selectable = not isTenFootInterface local dragging = false local buttonEffectFunction = devConsole:CreateButtonEffectFunction(resizeButton) devConsole:ConnectButtonDragging(resizeButton, function() local x0, y0 = frame.AbsoluteSize.X, frame.AbsoluteSize.Y return function(dx, dy) devConsole:SetFrameSize(x0 + dx, y0 + dy) end end, function(clicking, hovering) dragging = clicking buttonEffectFunction(clicking, hovering) end) end do -- Activate top handle dragging local frame = devConsole.Frame local handle = frame.Handle local buttonEffectFunction = devConsole:CreateButtonEffectFunction(handle) devConsole:ConnectButtonDragging(handle, function() local x, y = frame.AbsolutePosition.X, frame.AbsolutePosition.Y return function(dx, dy) devConsole:SetFramePosition(x + dx, y + dy) end --deltaCallback_Resize(-dx, -dy) -- Used if they are grabbing both at the same time end, buttonEffectFunction) end end -- interiorFrame contains tabContainer and window local interiorFrame = Primitives.FolderFrame(frame, 'Interior') interiorFrame.Position = UDim2_new(0, 0, 0, Style.HandleHeight) interiorFrame.Size = UDim2_new(1, -(Style.HandleHeight + Style.BorderSize * 2), 1, -(Style.HandleHeight + Style.BorderSize)) local windowContainer = Primitives.FolderFrame(interiorFrame, 'WindowContainer') windowContainer.Size = UDim2_new(1, 0, 1, -(Style.TabHeight)) windowContainer.Position = UDim2_new(0, Style.BorderSize, 0, Style.TabHeight) -- This is what applies ClipsDescendants to tab contents local window = Primitives.Frame(windowContainer, 'Window') window.Size = UDim2_new(1, 0, 1, 0) -- The tab open/close methods, and the consoles also set this window.Position = UDim2_new(0, 0, 0, 0) window.ClipsDescendants = true -- This is the frame that moves around with the scroll bar local body = Primitives.FolderFrame(window, 'Body') do -- Scrollbars local scrollbar = devConsole:CreateScrollbar() devConsole.WindowScrollbar = scrollbar local scrollbarFrame = scrollbar.Frame scrollbarFrame.Parent = frame scrollbarFrame.Size = UDim2_new(0, Style.HandleHeight, 1, -(Style.HandleHeight + Style.BorderSize) * 2) scrollbarFrame.Position = UDim2_new(1, -Style.HandleHeight, 0, Style.HandleHeight + Style.BorderSize) devConsole:ApplyScrollbarToFrame(scrollbar, window, body, frame) end local tabContainer = Primitives.FolderFrame(interiorFrame, 'Tabs') -- Shouldn't this be named 'tabFrame'? tabContainer.Size = UDim2_new(1, -(Style.GearSize + Style.BorderSize), 0, Style.TabHeight) tabContainer.Position = UDim2_new(0, 0, 0, 0) tabContainer.ClipsDescendants = true -- Options button local optionsButton = Primitives.InvisibleButton(frame, 'OptionsButton') local optionsClippingFrame = Primitives.FolderFrame(interiorFrame, 'OptionsClippingFrame') optionsClippingFrame.ClipsDescendants = true optionsClippingFrame.Position = UDim2_new(0, 0, 0, 0) optionsClippingFrame.Size = UDim2_new(1, 0, 0, 0) local optionsFrame = Primitives.FolderFrame(optionsClippingFrame, 'OptionsFrame') optionsFrame.Size = UDim2_new(1, 0, 0, Style.OptionAreaHeight) optionsFrame.Position = UDim2_new(0, 0, 0, Style.OptionAreaHeight) --optionsFrame.BackgroundColor3 = Style.OptionsFrameColor do -- Options animation local gearSize = Style.GearSize local tabHeight = Style.TabHeight local offset = (tabHeight - gearSize) / 2 optionsButton.Size = UDim2_new(0, Style.GearSize, 0, Style.GearSize) optionsButton.Position = UDim2_new(1, -(Style.GearSize + offset + Style.HandleHeight), 0, Style.HandleHeight + offset) local gear = Primitives.InvisibleImageLabel(optionsButton, 'Image', 'https://www.roblox.com/asset/?id=261882463') --gear.ZIndex = ZINDEX + 1 local animationToggle = devConsole:GenerateOptionButtonAnimationToggle(interiorFrame, optionsButton, gear, tabContainer, optionsClippingFrame, optionsFrame) local open = false optionsButton.MouseButton1Click:connect(function() open = not open animationToggle(open) end) end -- Console/Log and Stats options local setShownOptionTypes; -- Toggles what options to show: setOptionType({Log = true}) local textFilter, scriptStatFilter; local textFilterChanged, scriptStatFilterChanged; local messageFilter; local messageFilterChanged, messageTextWrappedChanged; do -- Options contents/filters local function createCheckbox(color, callback) local this = { Value = true; } local frame = Primitives.FolderFrame(nil, 'Checkbox') this.Frame = frame frame.Size = UDim2_new(0, Style.CheckboxSize, 0, Style.CheckboxSize) frame.BackgroundColor3 = color local padding = 2 local function f(xs, xp, yp) -- quick way to get an opaque border around a transparent center local ys = 1 - xs local f = Primitives.Frame(frame, 'Border') f.BackgroundColor3 = color f.BackgroundTransparency = 0 f.Size = UDim2_new(xs, ys * padding, ys, xs * padding) f.Position = UDim2_new(xp, -xp * padding, yp, -yp * padding) end f(1, 0, 0) f(1, 0, 1) f(0, 0, 0) f(0, 1, 0) local button = Primitives.Button(frame, 'Button') button.Size = UDim2_new(1, -padding * 2, 1, -padding * 2) button.Position = UDim2_new(0, padding, 0, padding) local buttonEffectFunction = devConsole:CreateButtonEffectFunction(button) local check = Primitives.Frame(button, 'Check') local padding = 4 check.Size = UDim2_new(1, -padding * 2, 1, -padding * 2) check.Position = UDim2_new(0, padding, 0, padding) check.BackgroundColor3 = color check.BackgroundTransparency = 0 devConsole:ConnectButtonHover(button, buttonEffectFunction) function this.SetValue(this, value) if value == this.Value then return end this.Value = value check.Visible = value this.Value = value callback(value) end button.MouseButton1Click:connect(function() this:SetValue(not this.Value) end) return this end local string_find = string.find local containsString; -- the text typed into the search textBox, nil if equal to "" function textFilter(text) return not containsString or string_find(text:lower(), containsString) end local filterLookup = {} -- filterLookup[Enum.MessageType.x.Value] = true or false function messageFilter(message) return filterLookup[message.Type] and (not containsString or string_find(message.Message:lower(), containsString)) end -- Events textFilterChanged = CreateSignal() scriptStatFilterChanged = CreateSignal() messageFilterChanged = CreateSignal() messageTextWrappedChanged = CreateSignal() local optionTypeContainers = { --[OptionType] = Frame --Log = Frame; --Scripts = Frame; } function setShownOptionTypes(shownOptionTypes) -- Example showOptionTypes: -- {Log = true} for optionType, container in pairs(optionTypeContainers) do container.Visible = shownOptionTypes[optionType] or false end end do -- Log options local container = Primitives.FolderFrame(optionsFrame, 'Log') container.Visible = false optionTypeContainers.Log = container local label = Primitives.InvisibleTextLabel(container, 'FilterLabel', "Filters") label.FontSize = 'Size18' label.TextXAlignment = 'Left' label.Size = UDim2_new(0, 54, 0, Style.CheckboxSize) label.Position = UDim2_new(0, 4, 0, 2) do local x = label.Size.X.Offset local messageColors = Style.MessageColors for i = 0, #messageColors do -- 0, 3 initially local checkbox = createCheckbox(messageColors[i], function(value) filterLookup[i] = value messageFilterChanged:fire() end) filterLookup[i] = checkbox.Value checkbox.Frame.Parent = container checkbox.Frame.Position = UDim2_new(0, x, 0, 4) x = x + Style.CheckboxSize + 4 end do -- Word wrap x = x + 8 local label = Primitives.InvisibleTextLabel(container, 'WrapLabel', "Word Wrap") label.FontSize = 'Size18' label.TextXAlignment = 'Left' label.Size = UDim2_new(0, 54 + Style.CheckboxSize, 0, Style.CheckboxSize) label.Position = UDim2_new(0, x + 4, 0, 2) local checkbox = createCheckbox(Color3.new(0.65, 0.65, 0.65), function(value) messageTextWrappedChanged:fire(value) -- an event isn't ideal here end) checkbox:SetValue(false) checkbox.Frame.Parent = container checkbox.Frame.Position = UDim2_new(0, x + label.Size.X.Offset, 0, 4) end end end do -- Scripts options local container = Primitives.FolderFrame(optionsFrame, 'Stats') container.Visible = false optionTypeContainers.Scripts = container do local x = 0 do -- Show inactive x = x + 4 local label = Primitives.InvisibleTextLabel(container, 'FilterLabel', "Show inactive") label.FontSize = 'Size18' label.TextXAlignment = 'Left' label.Size = UDim2_new(0, label.TextBounds.X + 6, 0, Style.CheckboxSize) label.Position = UDim2_new(0, x, 0, 2) x = x + label.Size.X.Offset local showInactive; local function getScriptCurrentlyActive(chartStat) local stats = chartStat.Stats if stats then local stat = stats[#stats] if stat then return stat[1] > 0.000001 or stat[2] > 0.000001 end end return false end function scriptStatFilter(chartStat) return (showInactive or getScriptCurrentlyActive(chartStat)) and (not containsString or string_find(chartStat.Name:lower(), containsString)) end local checkbox = createCheckbox(Color3_new(1, 1, 1), function(value) showInactive = value scriptStatFilterChanged:fire() end) showInactive = checkbox.Value checkbox.Frame.Parent = container checkbox.Frame.Position = UDim2_new(0, x, 0, 4) x = x + Style.CheckboxSize + 4 end x = x + 8 end end do -- Search/filter/contains textbox local container = Primitives.FolderFrame(optionsFrame, 'Search') container.Visible = false optionTypeContainers.Search = container local label = Primitives.InvisibleTextLabel(container, 'FilterLabel', "Contains:") label.FontSize = 'Size18' label.TextXAlignment = 'Left' label.Size = UDim2_new(0, 60, 0, Style.CheckboxSize) label.Position = UDim2_new(0, 4, 0, 4 + Style.CheckboxSize + 4) local textBox = Primitives.TextBox(container, 'ContainsFilter') textBox.ClearTextOnFocus = true textBox.FontSize = 'Size18' textBox.TextXAlignment = 'Left' textBox.Size = UDim2_new(0, 150, 0, Style.CheckboxSize) textBox.Position = UDim2_new(0, label.Position.X.Offset + label.Size.X.Offset + 4, 0, 4 + Style.CheckboxSize + 4) textBox.Text = "" local runningColor = Color3.new(0, 0.5, 0) local normalColor = textBox.BackgroundColor3 connectPropertyChanged(textBox, 'Text', function(text) text = text:lower() if text == "" then text = nil end if text == containsString then return end textBox.BackgroundColor3 = text and runningColor or normalColor containsString = text messageFilterChanged:fire() textFilterChanged:fire() end) connectPropertyChanged(textBox, 'TextBounds', function(textBounds) textBox.Size = UDim2_new(0, math.max(textBounds.X, 150), 0, Style.CheckboxSize) end) end end ---------- -- Tabs -- ---------- do -- Console/Log tabs -- Wrapper for :AddTab local function createConsoleTab(name, text, outputMessageSync, commandLineVisible, commandInputtedCallback, openCallback) local tabBody = Primitives.FolderFrame(body, name) local output, commandLine; local disconnector = CreateDisconnectSignal() local tab = devConsole:AddTab(text, tabBody, function(open) if commandLine then commandLine.Frame.Visible = open end if open then setShownOptionTypes({ Log = true; Search = true; }) if not output then output = devConsole:CreateOutput(outputMessageSync:GetMessages(), messageFilter) output.Frame.Parent = tabBody end output:SetVisible(true) if commandLineVisible then if open and not commandLine then commandLine = devConsole:CreateCommandLine() commandLine.Frame.Parent = frame commandLine.Frame.Size = UDim2_new(1, -(Style.HandleHeight + Style.BorderSize * 2), 0, Style.CommandLineHeight) commandLine.Frame.Position = UDim2_new(0, Style.BorderSize, 1, -(Style.CommandLineHeight + Style.BorderSize)) commandLine.CommandInputted:connect(commandInputtedCallback) end end window.Size = commandLineVisible and UDim2_new(1, 0, 1, -(Style.HandleHeight)) or UDim2_new(1, 0, 1, 0) local messages = outputMessageSync:GetMessages() local height = output:RefreshMessages() body.Size = UDim2_new(1, 0, 0, height) disconnector:connect(output.HeightChanged:connect(function(height) body.Size = UDim2_new(1, 0, 0, height) end)) body.Size = UDim2_new(1, 0, 0, output.Height) disconnector:connect(outputMessageSync.MessageAdded:connect(function(message) output:RefreshMessages(#messages) end)) disconnector:connect(messageFilterChanged:connect(function() output:RefreshMessages() end)) disconnector:connect(messageTextWrappedChanged:connect(function(enabled) output:SetTextWrappedEnabled(enabled) end)) else if output then output:SetVisible(false) end window.Size = UDim2_new(1, 0, 1, 0) disconnector:fire() end if openCallback then openCallback(open) end end) return tab end -- Client Log tab -- if permissions.MayViewClientLog then local tab = createConsoleTab( 'ClientLog', "Client Log", devConsole.MessagesAndStats.OutputMessageSyncLocal, true, function(text) if #text <= 1 then return end local f, err = loadstring(text) if err then warn(err) return end f(); end ) tab:SetVisible(true) tab:SetOpen(true) end end return devConsole end ---------------------- -- Backup GUI Mouse -- ---------------------- do -- This doesn't support multiple windows very well function Methods.EnableGUIMouse(devConsole) local label = Instance.new("ImageLabel") label.BackgroundTransparency = 1 label.BorderSizePixel = 0 label.Size = UDim2.new(0, 64, 0, 64) label.Image = "rbxasset://Textures/ArrowFarCursor.png" label.Name = "BackupMouse" label.ZIndex = Style.ZINDEX + 2 local disconnector = CreateDisconnectSignal() local enabled = false local mouse = game:GetService("Players").LocalPlayer:GetMouse() local function Refresh() local enabledNew = devConsole.Visible and not UserInputService.MouseIconEnabled if enabledNew == enabled then return end enabled = enabledNew label.Visible = enabled label.Parent = enabled and devConsole.ScreenGui or nil disconnector:fire() if enabled then label.Position = UDim2.new(0, mouse.X - 32, 0, mouse.Y - 32) disconnector:connect(UserInputService.InputChanged:connect(function(input) if input.UserInputType == Enum.UserInputType.MouseMovement then --local p = input.Position --if p then label.Position = UDim2.new(0, mouse.X - 32, 0, mouse.Y - 32) --end end end)) end end Refresh() local userInputServiceListener; devConsole.VisibleChanged:connect(function(visible) if userInputServiceListener then userInputServiceListener:disconnect() userInputServiceListener = nil end userInputServiceListener = UserInputService.Changed:connect(Refresh) Refresh() end) end end -------------------- -- Output console -- -------------------- do function Methods.CreateCommandLine(devConsole) local this = { CommandInputted = CreateSignal(); } local frame = Primitives.FolderFrame(nil, 'CommandLine') this.Frame = frame frame.Size = UDim2_new(1, 0, 0, Style.CommandLineHeight) local textBoxFrame = Primitives.Frame(frame, 'TextBoxFrame') textBoxFrame.Size = UDim2_new(1, 0, 0, Style.CommandLineHeight) textBoxFrame.Position = UDim2_new(0, 0, 0, 0) textBoxFrame.ClipsDescendants = true local label = Primitives.InvisibleTextLabel(textBoxFrame, 'Label', ">") label.Position = UDim2_new(0, 4, 0, 0) label.Size = UDim2_new(0, 12, 1, -1) label.FontSize = 'Size14' local DEFAULT_COMMAND_BAR_TEXT = "Type command here" local textBox = Primitives.TextBox(textBoxFrame, 'TextBox') --textBox.TextWrapped = true -- This needs to auto-resize textBox.BackgroundTransparency = 1 textBox.Text = DEFAULT_COMMAND_BAR_TEXT textBox.ClearTextOnFocus = false local padding = 2 textBox.Size = UDim2_new(1, -(padding * 2) - 4 - 12, 0, 500) textBox.Position = UDim2_new(0, 4 + 12 + padding, 0, 0) textBox.TextXAlignment = 'Left' textBox.TextYAlignment = 'Top' textBox.FontSize = 'Size18' textBox.TextWrapped = true -- override SelectionImageObject to better fit if isTenFootInterface then local selectionImage = Instance.new('ImageLabel') selectionImage.Name = "SelectionImage" selectionImage.Size = UDim2.new(1, textBoxFrame.AbsoluteSize.x + 36, 0, Style.CommandLineHeight + 24) selectionImage.Position = UDim2.new(0, -18, 0, -12) selectionImage.Image = 'rbxasset://textures/ui/SelectionBox.png' selectionImage.ScaleType = Enum.ScaleType.Slice selectionImage.SliceCenter = Rect.new(21,21,41,41) selectionImage.BackgroundTransparency = 1 textBox.SelectionImageObject = selectionImage end do local defaultSize = UDim2_new(1, 0, 0, Style.CommandLineHeight) local first = true textBox.Changed:connect(function(property) if property == 'TextBounds' or property == 'AbsoluteSize' then if first then -- There's a glitch that only occurs on the first change first = false return end local textBounds = textBox.TextBounds if textBounds.Y > Style.CommandLineHeight then textBoxFrame.Size = UDim2_new(1, 0, 0, textBounds.Y + 2) else textBoxFrame.Size = defaultSize end end end) end local disconnector = CreateDisconnectSignal() local backtrackPosition = 0 local inputtedText = {} local isLastWeak = false local function addInputtedText(text, weak) -- weak means it gets overwritten by the next text that's inputted if isLastWeak then table.remove(inputtedText, 1) end if inputtedText[1] == text then isLastWeak = isLastWeak and weak return end isLastWeak = weak if not weak then for i = #inputtedText, 1, -1 do if inputtedText[i] == text then table.remove(inputtedText, i) end end end table.insert(inputtedText, 1, text) end local function backtrack(direction) backtrackPosition = backtrackPosition + direction if backtrackPosition < 1 then backtrackPosition = 1 elseif backtrackPosition > #inputtedText then backtrackPosition = #inputtedText end if inputtedText[backtrackPosition] then -- Setting the text doesn't always work, especially after losing focus without pressing enter, then clicking back textBox.Text = inputtedText[backtrackPosition] end end local focusLostWithoutEnter = false textBox.Focused:connect(function() if textBox.Text == DEFAULT_COMMAND_BAR_TEXT then textBox.Text = "" end disconnector:fire() backtrackPosition = 0 disconnector:connect(UserInputService.InputBegan:connect(function(input) if input.KeyCode == Enum.KeyCode.Up then if backtrackPosition == 0 and not focusLostWithoutEnter then -- They typed something, then pressed up. They might want what they typed back, so we store it -- after they input the next thing, we know they meant to discard this, which is why it's "weak" (second arg is true) addInputtedText(textBox.Text, true) backtrackPosition = 1 end backtrack(1) elseif input.KeyCode == Enum.KeyCode.Down then backtrack(-1) end end)) end) textBox.FocusLost:connect(function(enterPressed) disconnector:fire() if enterPressed then focusLostWithoutEnter = false local text = textBox.Text addInputtedText(text, false) this.CommandInputted:fire(text) textBox.Text = "" -- let's not spam the popup keyboard after text is entered if not isTenFootInterface then textBox:CaptureFocus() end else backtrackPosition = 0 focusLostWithoutEnter = true addInputtedText(textBox.Text, true) if textBox.Text == "" then textBox.Text = DEFAULT_COMMAND_BAR_TEXT end end end) return this end end do local padding = 5 local LabelSize = UDim2_new(1, -padding, 0, 2048) local TextColors = Style.MessageColors local TextColorUnknown = Color3_new(0.5, 0, 1) local function isHidden(message) return false end function Methods.CreateOutput(devConsole, messages, messageFilter) -- AKA 'Log' local heightChanged = CreateSignal() local output = { Visible = false; Height = 0; HeightChanged = heightChanged; MessagesDirty = false; MessagesDirtyPosition = 1; } local function setHeight(height) height = height + 4 output.Height = height heightChanged:fire(height) end -- The label container local frame = Primitives.FolderFrame(nil, 'Output') frame.ClipsDescendants = true output.Frame = frame local textWrappedEnabled = false do local lastX = 0 connectPropertyChanged(frame, 'AbsoluteSize', function(size) local currentX = size.X --currentY = currentY - currentY if currentX ~= lastX then lastX = currentX output:RefreshMessages() end end) end local labels = {} local labelPositions = {} local function RefreshTextWrapped() if not output.Visible then return end local y = 1 for i = 1, #labels do local label = labels[i] label.TextWrapped = textWrappedEnabled local height = label.TextBounds.Y label.Size = LabelSize -- UDim2_new(1, 0, 0, height) label.Position = UDim2_new(0, padding, 0, y) y = y + height if height > 16 then y = y + 4 end end setHeight(y) end local MAX_LINES = 2048 local function RefreshMessagesForReal(messageStartPosition) if not output.Visible then return end local y = 1 local labelPosition = 0 -- position of last used label -- Failed optimization: messageStartPosition = nil if messageStartPosition then local labelPositionLast; for i = messageStartPosition, math_max(1, #messages - MAX_LINES), -1 do if labelPositions[i] then labelPositionLast = labelPositions[i] break end end if labels[labelPositionLast] then labelPosition = labelPositionLast local label = labels[labelPositionLast] y = label.Position.Y.Offset + label.Size.Y.Offset else messageStartPosition = nil end end for i = messageStartPosition or math_max(1, #messages - MAX_LINES), #messages do local message = messages[i] if messageFilter(message) then labelPosition = labelPosition + 1 labelPositions[i] = labelPosition local label = labels[labelPosition] if not label then label = Instance_new('TextLabel', frame) label.ZIndex = Style.ZINDEX label.BackgroundTransparency = 1 --label.Font = Style.Font label.FontSize = 'Size10' label.TextXAlignment = 'Left' label.TextYAlignment = 'Top' labels[labelPosition] = label end label.TextWrapped = textWrappedEnabled label.Size = LabelSize label.TextColor3 = TextColors[message.Type] or TextColorUnknown label.Text = message.Time .. " -- " .. message.Message local height = label.TextBounds.Y label.Size = LabelSize -- UDim2_new(1, -padding, 0, height) label.Position = UDim2_new(0, padding, 0, y) y = y + height if height > 16 then y = y + 4 end else labelPositions[i] = false end end -- Destroy extra labels for i = #labels, labelPosition + 1, -1 do labels[i]:Destroy() labels[i] = nil end setHeight(y) end function output.SetMessagesDirty(output, messageStartPosition) if output.MessagesDirty then return end output.MessagesDirty = true output.MessagesDirtyPosition = messageStartPosition end local refreshHandle; function output.RefreshMessages(output, messageStartPosition) if not output.Visible then return end if not refreshHandle then refreshHandle = true coroutine.wrap(function() -- Not ideal wait() refreshHandle = false RefreshMessagesForReal() end)() end end function output.SetTextWrappedEnabled(output, textWrappedEnabledNew) if textWrappedEnabledNew == textWrappedEnabled then return end textWrappedEnabled = textWrappedEnabledNew RefreshTextWrapped() end function output.SetVisible(output, visible) if visible == output.Visible then return end output.Visible = visible if visible then RefreshMessagesForReal() else for i = #labels, 1, -1 do labels[i]:Destroy() labels[i] = nil end end end return output end end ---------- -- Tabs -- ---------- function Methods.GetCurrentOpenTab(devConsole) local tabs = devConsole.Tabs if tabs == nil then return nil end for i = 1, #tabs do local tab = tabs[i] if tab.Open then return tab end end return nil end function Methods.RefreshTabs(devConsole) -- Go through and reposition them local x = Style.BorderSize local tabs = devConsole.Tabs for i = 1, #tabs do local tab = tabs[i] if tab.ButtonFrame.Visible then x = x + 3 tab.ButtonFrame.Position = UDim2_new(0, x, 0, 0) x = x + tab.ButtonFrame.AbsoluteSize.X + 3 end end end function Methods.AddTab(devConsole, text, body, openCallback, visibleCallback) -- Body is a frame that contains the tab contents body.Visible = false local tab = { Open = false; -- If the tab is open Visible = false; -- If the tab is shown OpenCallback = openCallback; VisibleCallback = visibleCallback; Body = body; } local nominalSize = TextService:GetTextSize(text, TAB_TEXT_SIZE, Enum.Font.SourceSans, Vector2.new(1e3, 1e3)) local width = nominalSize.x + (TAB_TEXT_PADDING * 2) local buttonFrame = Primitives.InvisibleButton(devConsole.Frame.Interior.Tabs, 'Tab_' .. text) tab.ButtonFrame = buttonFrame buttonFrame.Size = UDim2_new(0, width, 0, Style.TabHeight) buttonFrame.Visible = false local textLabel = Primitives.TextLabel(buttonFrame, 'Label', text) textLabel.TextSize = TAB_TEXT_SIZE --textLabel.TextYAlignment = Enum.TextYAlignment.Top devConsole:ConnectButtonHover(buttonFrame, devConsole:CreateButtonEffectFunction(textLabel)) -- These are the dimensions when the tab is closed local size0 = UDim2_new(1, 0, 1, -7) local position0 = UDim2_new(0, 0, 0, 4) -- There are the dimensions when the tab is open local size1 = UDim2_new(1, 0, 1, -4) local position1 = UDim2_new(0, 0, 0, 4) -- It starts closed textLabel.Size = size0 textLabel.Position = position0 function tab.SetVisible(tab, visible) if visible == tab.Visible then return end tab.Visible = visible tab:SetOpen(false) if tab.VisibleCallback then tab.VisibleCallback(visible) end buttonFrame.Visible = visible devConsole:RefreshTabs() if not visible then tab.SetOpen(false) end end function tab.RecordInitialOpen(tab) end function tab.RecordClickToOpen(tab) end function tab.SetOpen(tab, open) if open == tab.Open then return end tab.Open = open if open then if tab.SavedScrollbarValue then devConsole.WindowScrollbar:SetValue(tab.SavedScrollbarValue) -- This doesn't load correctly? end local tabs = devConsole.Tabs for i = 1, #tabs do if tabs[i] ~= tab then tabs[i]:SetOpen(false) end end if body then body.Visible = true end devConsole:RefreshTabs() -- Set dimensions for folder effect textLabel.Size = size1 textLabel.Position = position1 devConsole.CurrentOpenedTab = buttonFrame else tab.SavedScrollbarValue = devConsole.WindowScrollbar:GetValue() -- This doesn't save correctly if body then body.Visible = false -- todo: (not essential) these 2 lines should instead exist during open (above block) after going through tabs devConsole.Frame.Interior.WindowContainer.Window.Body.Size = UDim2_new(1, 0, 1, 0) devConsole.Frame.Interior.WindowContainer.Window.Body.Position = UDim2_new(0, 0, 0, 0) end -- Set dimensions for folder effect textLabel.Size = size0 textLabel.Position = position0 end if tab.OpenCallback then tab.OpenCallback(open) end end buttonFrame.MouseButton1Click:connect(function() if tab.Visible then tab:RecordClickToOpen() tab:SetOpen(true) end end) table.insert(devConsole.Tabs, tab) return tab end ---------------- -- Scroll bar -- ---------------- function Methods.ApplyScrollbarToFrame(devConsole, scrollbar, window, body, frame) local windowHeight, bodyHeight local height = scrollbar:GetHeight() local value = scrollbar:GetValue() local function getHeights() return window.AbsoluteSize.Y, body.AbsoluteSize.Y end local function refreshDimension() local windowHeightNew, bodyHeightNew = getHeights() if bodyHeight ~= bodyHeightNew or windowHeight ~= windowHeightNew then bodyHeight, windowHeight = bodyHeightNew, windowHeightNew height = windowHeight / bodyHeight scrollbar:SetHeight(height) local yOffset = (bodyHeight - windowHeight) * value -- Never let yOffset go negative. -- Without this line, things that are smaller than the containing scroll -- window start at the bottom and grow up. -- It's a better UX to have things start at top and grow down. if (yOffset < 0) then yOffset = 0 end local x = body.Position.X local y = body.Position.Y body.Position = UDim2_new(x.Scale, x.Offset, y.Scale, -math.floor(yOffset)) end end local function setValue(valueNew) value = valueNew refreshDimension() local yOffset = (bodyHeight - windowHeight) * value local x = body.Position.X local y = body.Position.Y body.Position = UDim2_new(x.Scale, x.Offset, y.Scale, -math.floor(yOffset)) end scrollbar.ValueChanged:connect(setValue) setValue(scrollbar:GetValue()) local scrollDistance = 120 window.Active = true scrollbar.ButtonUp.MouseButton1Click:connect(function() scrollbar:Scroll(-scrollDistance, getHeights()) end) scrollbar.ButtonDown.MouseButton1Click:connect(function() scrollbar:Scroll(scrollDistance, getHeights()) end) connectPropertyChanged(window, 'AbsoluteSize', refreshDimension) connectPropertyChanged(body, 'AbsoluteSize', function() local windowHeight, bodyHeight = getHeights() local value = scrollbar:GetValue() if value ~= 1 and value ~= 0 then local value = -body.Position.Y.Offset / (bodyHeight - windowHeight) scrollbar:SetValue(value) end refreshDimension() end) window.MouseWheelForward:connect(function() scrollbar:Scroll(-scrollDistance, getHeights()) end) window.MouseWheelBackward:connect(function() scrollbar:Scroll(scrollDistance, getHeights()) end) window.TouchPan:connect(function(positions, delta, velocity, userInputState) scrollbar:Scroll(-delta.y, getHeights()) end) end function Methods.CreateScrollbar(devConsole, rotation) local scrollbar = {} local main = nil main = Primitives.FolderFrame(main, 'Scrollbar') scrollbar.Frame = main local frame = Primitives.Button(main, 'Frame') frame.AutoButtonColor = false frame.Size = UDim2_new(1, 0, 1, -(Style.HandleHeight) * 2 - 2) frame.Position = UDim2_new(0, 0, 0, Style.HandleHeight + 1) -- frame.BackgroundTransparency = 0.75 -- This replaces the scrollbar when it's not being used local frame2 = Primitives.Frame(main, 'Frame') frame2.Size = UDim2_new(1, 0, 1, 0) frame2.Position = UDim2_new(0, 0, 0, 0) function scrollbar.SetVisible(scrollbar, visible) frame.Visible = visible frame2.Visible = not visible end local buttonUp = Primitives.ImageButton(frame, 'Up', 'https://www.roblox.com/asset/?id=261880783') scrollbar.ButtonUp = buttonUp buttonUp.Size = UDim2_new(1, 0, 0, Style.HandleHeight) buttonUp.Position = UDim2_new(0, 0, 0, -Style.HandleHeight - 1) buttonUp.AutoButtonColor = false devConsole:ConnectButtonHover(buttonUp, devConsole:CreateButtonEffectFunction(buttonUp)) local buttonDown = Primitives.ImageButton(frame, 'Down', 'https://www.roblox.com/asset/?id=261880783') scrollbar.ButtonDown = buttonDown buttonDown.Size = UDim2_new(1, 0, 0, Style.HandleHeight) buttonDown.Position = UDim2_new(0, 0, 1, 1) buttonDown.Rotation = 180 buttonDown.AutoButtonColor = false devConsole:ConnectButtonHover(buttonDown, devConsole:CreateButtonEffectFunction(buttonDown)) local bar = Primitives.Button(frame, 'Bar') bar.Size = UDim2_new(1, 0, 0.5, 0) bar.Position = UDim2_new(0, 0, 0.25, 0) bar.AutoButtonColor = false local grip = Primitives.InvisibleImageLabel(bar, 'Image', 'https://www.roblox.com/asset/?id=261904959') grip.Size = UDim2_new(0, 16, 0, 16) grip.Position = UDim2_new(0.5, -8, 0.5, -8) local buttonEffectFunction = devConsole:CreateButtonEffectFunction(bar, nil, bar.BackgroundColor3, bar.BackgroundColor3) -- Inertial scrolling would be added around here local value = 1 local valueChanged = CreateSignal() scrollbar.ValueChanged = valueChanged -- value = 0: at very top -- value = 1: at very bottom local height = 0.25 local heightChanged = CreateSignal() scrollbar.HeightChanged = heightChanged -- height = 0: infinite page size -- height = 1: bar fills frame completely, no need to scroll local function getValueAtPosition(pos) return ((pos - main.AbsolutePosition.Y) / main.AbsoluteSize.Y) / (1 - height) end -- Refreshes the position and size of the scrollbar local function refresh() local y = height bar.Size = UDim2_new(1, 0, y, 0) bar.Position = UDim2_new(0, 0, value * (1 - y), 0) end refresh() function scrollbar.SetValue(scrollbar, valueNew) if valueNew < 0 then valueNew = 0 elseif valueNew > 1 then valueNew = 1 end if valueNew ~= value then value = valueNew refresh() valueChanged:fire(valueNew) end end function scrollbar.GetValue(scrollbar) return value end function scrollbar.Scroll(scrollbar, direction, windowHeight, bodyHeight) scrollbar:SetValue(value + direction / bodyHeight) -- needs to be adjusted end function scrollbar.SetHeight(scrollbar, heightNew) if heightNew < 0 then heightNew = 0 -- this is still an awkward case of divide-by-zero that shouldn't happen elseif heightNew > 1 then heightNew = 1 end heightNew = math.max(heightNew, 0.1) -- Minimum scroll bar size, from that point on it is not the actual ratio if heightNew ~= height then height = heightNew scrollbar:SetVisible(heightNew < 1) refresh() heightChanged:fire(heightNew) end end function scrollbar.GetHeight(scrollbar) return height end devConsole:ConnectButtonDragging(bar, function() local value0 = value -- starting value return function(dx, dy) local dposition = dy -- net position change relative to the bar's axis (could support rotated scroll bars) local dvalue = (dposition / frame.AbsoluteSize.Y) / (1 - height) -- net value change scrollbar:SetValue(value0 + dvalue) end end, buttonEffectFunction) return scrollbar end ---------------------- -- Fancy color lerp -- ---------------------- local RenderLerpAnimation; do local math_cos = math.cos local math_pi = math.pi function RenderLerpAnimation(disconnectSignal, length, callback) disconnectSignal:fire() local timeStamp = tick() local listener = RunService.RenderStepped:connect(function() local t = (tick() - timeStamp) / length if t >= 1 then t = 1 disconnectSignal:fire() else t = (1 - math_cos(t * math_pi)) / 2 -- cosine interpolation aka 'Sine' in :TweenSizeAndPosition end callback(t) end) disconnectSignal:connect(listener) return listener end end if EYECANDY_ENABLED then -- This is the pretty version function Methods.CreateButtonEffectFunction(devConsole, button, normalColor, clickingColor, hoveringColor) normalColor = normalColor or button.BackgroundColor3 clickingColor = clickingColor or Style.GetButtonDownColor(normalColor) hoveringColor = hoveringColor or Style.GetButtonHoverColor(normalColor) local disconnectSignal = CreateDisconnectSignal() return function(clicking, hovering) local color0 = button.BackgroundColor3 local color1 = clicking and clickingColor or (hovering and hoveringColor or normalColor) local r0, g0, b0 = color0.r, color0.g, color0.b local r1, g1, b1 = color1.r, color1.g, color1.b local r2, g2, b2 = r1 - r0, g1 - g0, b1 - b0 RenderLerpAnimation(disconnectSignal, clicking and 0.125 or 0.25, function(t) button.BackgroundColor3 = Color3_new(r0 + r2 * t, g0 + g2 * t, b0 + b2 * t) end) end end else -- This is the simple version function Methods.CreateButtonEffectFunction(devConsole, button, normalColor, clickingColor, hoveringColor) normalColor = normalColor or button.BackgroundColor3 clickingColor = clickingColor or Style.GetButtonDownColor(normalColor) hoveringColor = hoveringColor or Style.GetButtonHoverColor(normalColor) return function(clicking, hovering) button.BackgroundColor3 = clicking and clickingColor or (hovering and hoveringColor or normalColor) end end end function Methods.GenerateOptionButtonAnimationToggle(devConsole, interior, button, gear, tabContainer, optionsClippingFrame, optionsFrame) local tabContainerSize0 = tabContainer.Size local tabContainerSize1 = UDim2_new( tabContainerSize0.X.Scale, tabContainerSize0.X.Offset + (Style.GearSize + 2) + Style.BorderSize, tabContainerSize0.Y.Scale, tabContainerSize0.Y.Offset) local gearRotation0 = gear.Rotation local gearRotation1 = gear.Rotation - 90 local interiorSize0 = interior.Size local interiorSize1 = UDim2_new(interiorSize0.X.Scale, interiorSize0.X.Offset, interiorSize0.Y.Scale, interiorSize0.Y.Offset - Style.OptionAreaHeight) local interiorPosition0 = interior.Position local interiorPosition1 = UDim2_new(interiorPosition0.X.Scale, interiorPosition0.X.Offset, interiorPosition0.Y.Scale, interiorPosition0.Y.Offset + Style.OptionAreaHeight) local length = 0.5 local disconnector = CreateDisconnectSignal() return function(open) if open then interior:TweenSizeAndPosition(interiorSize1, interiorPosition1, 'Out', 'Sine', length, true) tabContainer:TweenSize(tabContainerSize1, 'Out', 'Sine', length, true) optionsClippingFrame:TweenSizeAndPosition( UDim2_new(1, 0, 0, Style.OptionAreaHeight), UDim2_new(0, 0, 0, -Style.OptionAreaHeight), 'Out', 'Sine', length, true ) optionsFrame:TweenPosition( UDim2_new(0, 0, 0, 0),-- -Style.OptionAreaHeight), 'Out', 'Sine', length, true ) local gearRotation = gear.Rotation RenderLerpAnimation(disconnector, length, function(t) gear.Rotation = gearRotation1 * t + gearRotation * (1 - t) end) else interior:TweenSizeAndPosition(interiorSize0, interiorPosition0, 'Out', 'Sine', length, true) tabContainer:TweenSize(tabContainerSize0, 'Out', 'Sine', length, true) optionsClippingFrame:TweenSizeAndPosition( UDim2_new(1, 0, 0, 0), UDim2_new(0, 0, 0, 0), 'Out', 'Sine', length, true ) optionsFrame:TweenPosition( UDim2_new(0, 0, 0, Style.OptionAreaHeight), 'Out', 'Sine', length, true ) local gearRotation = gear.Rotation RenderLerpAnimation(disconnector, length, function(t) gear.Rotation = gearRotation0 * t + gearRotation * (1 - t) end) end end end ------------------------------ -- Events for color effects -- ------------------------------ do local globalInteractEvent = CreateSignal() function Methods.ConnectButtonHover(devConsole, button, mouseInteractCallback) -- void mouseInteractCallback(bool clicking, bool hovering) local this = {} local clicking = false local hovering = false local function set(clickingNew, hoveringNew) if hoveringNew and TouchEnabled then hoveringNew = false -- Touch screens don't hover end if clickingNew ~= clicking or hoveringNew ~= hovering then clicking, hovering = clickingNew, hoveringNew mouseInteractCallback(clicking, hovering) end end button.MouseButton1Down:connect(function() set(true, true) end) button.MouseButton1Up:connect(function() set(false, true) end) button.MouseEnter:connect(function() set(clicking, true) end) button.MouseLeave:connect(function() set(false, false) end) --[[ these might cause memory leakes (when creating temporary buttons) -- This solves the case in which the user presses F9 while hovering over a button devConsole.VisibleChanged:connect(function() set(false, false) end) globalInteractEvent:connect(function() set(false, false) end) --]] end end ------------------------- -- Events for draggers -- (for the window's top handle, the resize button, and scrollbars) ------------------------- function Methods.ConnectButtonDragging(devConsole, button, dragCallback, mouseInteractCallback) -- How dragCallback is called: local deltaCallback = dragCallback(xPositionAtMouseDown, yPositionAtMouseDown) -- How deltaCallback is called: deltaCallback(netChangeInAbsoluteXPositionSinceMouseDown, netChangeInAbsoluteYPositionSinceMouseDown) local dragging = false -- AKA 'clicking' local hovering = false local listeners = {} local disconnectCallback; local function stopDragging() if not dragging then return end dragging = false mouseInteractCallback(dragging, hovering) for i = #listeners, 1, -1 do listeners[i]:disconnect() listeners[i] = nil end end local ButtonUserInputTypes = { [Enum.UserInputType.MouseButton1] = true; [Enum.UserInputType.Touch] = true; -- I'm not sure if touch actually works here } local mouse = game:GetService("Players").LocalPlayer:GetMouse() local function startDragging(startP) if dragging then return end dragging = true mouseInteractCallback(dragging, hovering) local deltaCallback; local x0, y0 = startP.X, startP.Y --[[ listeners[#listeners + 1] = UserInputService.InputBegan:connect(function(input) if ButtonUserInputTypes[input.UserInputType] then local position = input.Position if position and not x0 then x0, y0 = position.X, position.Y -- The same click end end end) --]] listeners[#listeners + 1] = UserInputService.InputEnded:connect(function(input) if ButtonUserInputTypes[input.UserInputType] then stopDragging() end end) listeners[#listeners + 1] = UserInputService.InputChanged:connect(function(input) if not (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch)then -- added in a touch check return end local p1 = input.Position if not p1 then return end local x1, y1 = p1.X, p1.Y if not deltaCallback then deltaCallback, disconnectCallback = dragCallback(x0 or x1, y0 or y1) end if x0 then deltaCallback(x1 - x0, y1 - y0) end end) end --button.MouseButton1Down:connect(startDragging) --button.MouseButton1Up:connect(stopDragging) button.InputBegan:connect(function(iobj) if iobj.UserInputType == Enum.UserInputType.Touch or iobj.UserInputType == Enum.UserInputType.MouseButton1 then startDragging(iobj.Position) end end) button.InputEnded:connect(function(iobj) if iobj.UserInputType == Enum.UserInputType.Touch or iobj.UserInputType == Enum.UserInputType.MouseButton1 then stopDragging() end end) button.MouseEnter:connect(function() if not hovering then hovering = true mouseInteractCallback(dragging, hovering) end end) button.MouseLeave:connect(function() if hovering then hovering = false mouseInteractCallback(dragging, hovering) end end) devConsole.VisibleChanged:connect(stopDragging) end ----------------- -- Permissions -- ----------------- do local permissionsLoading, permissions = false; function DeveloperConsole.GetPermissions() while permissionsLoading do wait() end if permissions then return permissions end permissions = {} permissionsLoading = true permissions.IsCreator = false local success, result = pcall(function() local url = string.format("/users/%d/canmanage/%d", game:GetService("Players").LocalPlayer.UserId, game.PlaceId) return game:GetService('HttpRbxApiService'):GetAsync(url, Enum.ThrottlingPriority.Default, Enum.HttpRequestType.Default, true) end) if success and type(result) == "string" then -- API returns: {"Success":BOOLEAN,"CanManage":BOOLEAN} -- Convert from JSON to a table -- pcall in case of invalid JSON success, result = pcall(function() return game:GetService('HttpService'):JSONDecode(result) end) if success and result.CanManage == true then permissions.IsCreator = result.CanManage end end permissions.ClientCodeExecutionEnabled = false pcall(function() permissions.ServerCodeExecutionEnabled = permissions.IsCreator and (not settings():GetFFlag("DebugDisableLogServiceExecuteScript")) end) if DEBUG or (RunService:IsStudio()) then permissions.IsCreator = true permissions.ServerCodeExecutionEnabled = true end permissions.MayViewServerLog = permissions.IsCreator permissions.MayViewClientLog = true permissions.MayViewServerStats = permissions.IsCreator permissions.MayViewServerMemory = permissions.IsCreator permissions.MayViewServerScripts = permissions.IsCreator permissions.MayViewServerJobs = permissions.IsCreator permissions.MayViewDataStoreBudget = false pcall(function() permissions.MayViewDataStoreBudget = permissions.IsCreator end) permissions.MayViewHttpResultClient = false permissions.MayViewHttpResultClient = permissions.IsCreator permissions.MayViewHttpResultServer = false permissions.MayViewHttpResultServer = permissions.IsCreator permissions.MayViewContextActionBindings = permissions.IsCreator permissionsLoading = false return permissions end end ---------------------- -- Output interface -- ---------------------- do local messagesAndStats; function DeveloperConsole.GetMessagesAndStats(permissions) if messagesAndStats then return messagesAndStats end local function NewOutputMessageSync(getMessages) local this; this = { Messages = nil; -- Private member, DeveloperConsole should use :GetMessages() MessageAdded = CreateSignal(); GetMessages = function() local messages = this.Messages if not messages then -- If it errors while getting messages, it skip it next time if this.Attempted then messages = {} else this.Attempted = true messages = getMessages(this) this.Messages = messages end end return messages end; } return this end local ConvertTimeStamp; do -- Easy, fast, and working nicely local function numberWithZero(num) return (num < 10 and "0" or "") .. num end local string_format = string.format -- optimization function ConvertTimeStamp(timeStamp) local localTime = timeStamp - os_time() + math.floor(tick()) local dayTime = localTime % 86400 local hour = math.floor(dayTime/3600) dayTime = dayTime - (hour * 3600) local minute = math.floor(dayTime/60) dayTime = dayTime - (minute * 60) local second = dayTime local h = numberWithZero(hour) local m = numberWithZero(minute) local s = numberWithZero(dayTime) return string_format("%s:%s:%s", h, m, s) end end local warningsToFilter = {"ClassDescriptor failed to learn", "EventDescriptor failed to learn", "Type failed to learn"} -- Filter "ClassDescriptor failed to learn" errors local function filterMessageOnAdd(message) if message.Type ~= Enum.MessageType.MessageWarning.Value then return false end local found = false for _, filterString in ipairs(warningsToFilter) do if string.find(message.Message, filterString) ~= nil then found = true break end end return found end local outputMessageSyncLocal; if permissions.MayViewClientLog then outputMessageSyncLocal = NewOutputMessageSync(function(this) local messages = {} local LogService = game:GetService("LogService") do -- This do block keeps history from sticking around in memory local history = LogService:GetLogHistory() for i = 1, #history do local msg = history[i] local message = { Message = msg.message or "[DevConsole Error 1]"; Time = ConvertTimeStamp(msg.timestamp); Type = msg.messageType.Value; } if not filterMessageOnAdd(message) then messages[#messages + 1] = message end end end LogService.MessageOut:connect(function(text, messageType) local message = { Message = text or "[DevConsole Error 2]"; Time = ConvertTimeStamp(os_time()); Type = messageType.Value; } if not filterMessageOnAdd(message) then messages[#messages + 1] = message this.MessageAdded:fire(message) end end) return messages end) end local outputMessageSyncServer; if permissions.MayViewServerLog then outputMessageSyncServer = NewOutputMessageSync(function(this) local messages = {} local LogService = game:GetService("LogService") LogService.ServerMessageOut:connect(function(text, messageType, timestamp) local message = { Message = text or "[DevConsole Error 3]"; Time = ConvertTimeStamp(timestamp); Type = messageType.Value; } if not filterMessageOnAdd(message) then messages[#messages + 1] = message this.MessageAdded:fire(message) end end) LogService:RequestServerOutput() return messages end) end local statsSyncServer; if (permissions.MayViewServerStats or permissions.MayViewServerScripts or permissions.MayViewServerMemory) then statsSyncServer = { Stats = nil; -- Private member, use GetStats instead StatsReceived = CreateSignal(); } local statsListenerConnection; function statsSyncServer.GetStats(statsSyncServer) local stats = statsSyncServer.Stats if not stats then stats = {} pcall(function() local clientReplicator = game:FindService("NetworkClient"):GetChildren()[1] if clientReplicator then statsListenerConnection = clientReplicator.StatsReceived:connect(function(stat) statsSyncServer.StatsReceived:fire(stat) end) clientReplicator:RequestServerStats(true) end end) statsSyncServer.Stats = stats end return stats end end --]] messagesAndStats = { OutputMessageSyncLocal = outputMessageSyncLocal; OutputMessageSyncServer = outputMessageSyncServer; StatsSyncServer = statsSyncServer; } return messagesAndStats end end --[[ Module Table ]]-- -- We only create the dev console if we need it; user toggles visibility. local DevConsoleModuleTable = {} local myDeveloperConsole = nil -- Tenfoot Interface set up local function onDevConsoleVisibilityChanged(isVisible) local blockMenuActionName = "blockMenuAction" local closeDevConsoleActionName = "closeDevConsoleAction" local selectionParentName = "devConsoleSelectionGroup" local function closeDevConsole(actionName, inputState, inputObject) if inputState == Enum.UserInputState.End then myDeveloperConsole:SetVisible(false) end end if isVisible then -- block menu open input while dev console is open ContextActionService:BindCoreAction(blockMenuActionName, function() end, false, Enum.KeyCode.ButtonStart) local menuModule = require(Modules.Settings.SettingsHub) menuModule:SetVisibility(false, true) ContextActionService:BindCoreAction(closeDevConsoleActionName, closeDevConsole, false, Enum.KeyCode.ButtonB) GuiService:AddSelectionParent(selectionParentName, myDeveloperConsole.Frame) GuiService.SelectedCoreObject = myDeveloperConsole.CurrentOpenedTab else ContextActionService:UnbindCoreAction(closeDevConsoleActionName) ContextActionService:UnbindCoreAction(blockMenuActionName) GuiService:RemoveSelectionGroup(selectionParentName) GuiService.SelectedCoreObject = nil end end local devConsoleCreating = false local function getDeveloperConsole() if (not myDeveloperConsole and not devConsoleCreating) then devConsoleCreating = true local permissions = DeveloperConsole.GetPermissions() local messagesAndStats = DeveloperConsole.GetMessagesAndStats(permissions) myDeveloperConsole = DeveloperConsole.new(RobloxGui, permissions, messagesAndStats) if isTenFootInterface then myDeveloperConsole.VisibleChanged:connect(onDevConsoleVisibilityChanged) end devConsoleCreating = false end return myDeveloperConsole end function DevConsoleModuleTable:GetVisibility() local devConsole = getDeveloperConsole() if devConsole then return devConsole.Visible else return false end end function DevConsoleModuleTable:SetVisibility(value) local devConsole = getDeveloperConsole() if devConsole then devConsole:SetVisible(value) end end local creatingLock = false local creatingVisibleValueToSet = false local function SetCoreConsoleCreation() if (creatingLock) then return end creatingLock = true spawn(function() --// Keep GetVisibility call before SetVisibility because the first call will yield for some time and --// there is the possibility that during the yield time the value of 'creatingVisibleValueToSet' may --// change. DevConsoleModuleTable:GetVisibility() DevConsoleModuleTable:SetVisibility(creatingVisibleValueToSet) creatingLock = false end) end local StarterGui = game:GetService("StarterGui") local function GetDeveloperConsoleVisible() if (not myDeveloperConsole) then SetCoreConsoleCreation() return creatingVisibleValueToSet; else return DevConsoleModuleTable:GetVisibility() end end local function DeveloperConsoleVisible(visible) if (type(visible) ~= "boolean") then error("DeveloperConsoleVisible must be given a boolean value.") end if (not myDeveloperConsole) then creatingVisibleValueToSet = visible SetCoreConsoleCreation() else DevConsoleModuleTable:SetVisibility(visible) end end -- BetterConsole.lua by Josh#0903 local InputService = game:GetService'UserInputService' local StarterGui = game:GetService'StarterGui' InputService.InputBegan:connect(function(a) if a.UserInputType == Enum.UserInputType.Keyboard and a.KeyCode == Enum.KeyCode.F9 then local b = GetDeveloperConsoleVisible(); StarterGui:SetCore('DevConsoleVisible', false) DeveloperConsoleVisible(not b) end end)