-- Made by Tomarty (talk to me if you have questions) -- Quick optimizations 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 GameSettings = UserSettings().GameSettings local DEBUG = false local ContextActionService = game:GetService("ContextActionService") -- Eye candy uses RenderStepped local EYECANDY_ENABLED = true local ZINDEX = 6 local Style; do local function c3(r, g, b) return Color3_new(r / 255, g / 255, b / 255) end local frameColor = Color3_new(0.1, 0.1, 0.1) local textColor = Color3_new(1, 1, 1) local optionsFrameColor = Color3_new(1, 1, 1) Style = { Font = Enum.Font.NotoSans; FontBold = Enum.Font.NotoSansBold; 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 -- This provides an easy way to create GUI objects without writing insanely redundant code local Primitives = {}; do local function new(className, parent, name) local n = Instance_new(className, parent) n.ZIndex = ZINDEX 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 local CreateSignal = assert(LoadLibrary('RbxUtility')).CreateSignal -- 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); devConsole.Frame.Position = UDim2_new(0.25, -10, 0.125, -10) end function Methods.BoundFrameSize(devConsole, x, y) -- Minimum frame size return math_max(x, 300), 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() 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 = {}; 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 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.Modal = true -- Unlocks mouse handle.AutoButtonColor = false do -- Title local title = Primitives.InvisibleTextLabel(handle, 'Title', "Kiseki 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://assetdelivery.roblox.com/v1/asset?id=261878266', 2/3) exitButton.AutoButtonColor = false 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) local closeDevConsole = function(name, inputState, input) ContextActionService:UnbindCoreAction("RBXDevConsoleCloseAction") devConsole:SetVisible(false, true) end ContextActionService:BindCoreAction("RBXDevConsoleCloseAction", closeDevConsole, false, Enum.KeyCode.ButtonB) end do -- Repositioning and Resizing do -- Create bottom right window resize button and activate resize dragging local resizeButton, resizeButtonImage = createCornerButton('Resize', 1, 1, 'https://assetdelivery.roblox.com/v1/asset?id=261880743', 1) resizeButtonImage.Position = UDim2_new(0, 0, 0, 0) resizeButtonImage.Size = UDim2_new(1, 0, 1, 0) 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://assetdelivery.roblox.com/v1/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 --[[ 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) end) checkbox:SetValue(false) checkbox.Frame.Parent = container checkbox.Frame.Position = UDim2_new(0, x + label.Size.X.Offset, 0, 4) --]] end end do -- Search/filter/contains textbox local label = Primitives.InvisibleTextLabel(optionsFrame, '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(optionsFrame, '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, width, outputMessageSync, commandLineVisible, commandInputtedCallback, openCallback) local tabBody = Primitives.FolderFrame(body, name) local output, commandLine; local disconnector = CreateDisconnectSignal() local tab = devConsole:AddTab(text, width, tabBody, function(open) if commandLine then commandLine.Frame.Visible = open end if open then setShownOptionTypes({ Log = 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 -- Local Log tab -- if permissions.MayViewClientLog then local tab = createConsoleTab( 'LocalLog', "Local Log", 60, devConsole.MessagesAndStats.OutputMessageSyncLocal, permissions.ClientCodeExecutionEnabled ) tab:SetVisible(true) tab:SetOpen(true) end -- Server Log tab -- if permissions.MayViewServerLog then local LogService = game:GetService('LogService') local tab = createConsoleTab( 'ServerLog', "Server Log", 70, devConsole.MessagesAndStats.OutputMessageSyncServer, permissions.ServerCodeExecutionEnabled, function(text) if #text <= 1 then return end if permissions.ServerCodeExecutionEnabled then -- print("Server Loadstring:", text) LogService:ExecuteScript(text) end end ) tab:SetVisible(true) end end do -- Stats tabs local function generateGreenYellowRedColor(unit) -- 0 <= unit <= 1 --[[ 0 -> 0, 223, 0 0.5 -> 223, 233, 0 1 -> 233, 0, 0 --]] local brightness = 0.9 if not unit then return Color3.new(0, 0, 0) elseif unit <= 0 then return Color3.new(1, 1, 1) elseif unit <= 0.5 then unit = unit * 2 return Color3.new(unit * brightness, brightness, 0) elseif unit <= 1 then unit = unit * 2 - 1 return Color3.new(brightness, (1 - unit) * brightness, 0) else return Color3.new(1, 0, 0) end end -- Wrapper for :AddTab local function createStatsTab(name, text, width, config, openCallback, filterStats, shownOptionTypes) local statsSyncServer = devConsole.MessagesAndStats.StatsSyncServer local open = false local statList = devConsole:CreateChartList(config) local tabBody = statList.Frame tabBody.Parent = body tabBody.Name = name tabBody.BackgroundTransparency = 1 tabBody.Size = UDim2_new(1, 0, 1, 0) statList.SideMenu.Parent = windowContainer -- so the left side menu doesn't resize with the contents on right statsSyncServer:GetStats() statsSyncServer.StatsReceived:connect(function(stats) local statsFiltered = filterStats(stats) if statsFiltered then statList:UpdateStats(statsFiltered) end end) local tab = devConsole:AddTab(text, width, tabBody, function(openNew) open = openNew if open then devConsole.WindowScrollbar:SetValue(0) setShownOptionTypes(shownOptionTypes) end statList:SetVisible(open) if openCallback then openCallback(open) end end) tab:SetVisible(true) return tab, statList end -- Server Scripts -- if permissions.MayViewServerScripts then local open = false local config = { GetNotifyColor = function(chartButton) local chartStat = chartButton.ChartStat local point; local stat = chartStat.Stats[#chartStat.Stats] if stat then point = stat[1] local freq = stat[2] if point and freq then point = (point < 0 and 0) or (point > 1 and 1) or point -- clamp between 0 and 1 point = math.max(freq > 0 and 0.000001 or 0, point ^ (1/4)) end end return generateGreenYellowRedColor(point) end; CreateChartPage = function(chartButton, statsBody) local chartStat = chartButton.ChartStat local chart1 = devConsole:CreateChart(chartStat.Stats, "Script Activity", 1, function(point) return point and math.ceil(point * 100000 * 100) / 100000 .. "%" or "" end) local chart2 = devConsole:CreateChart(chartStat.Stats, "Script Rate", 2, function(point) return point and (math.floor(point * 100000) / 100000) .. "/s" or "" end) local y = 16 chart1.Frame.Parent = statsBody chart1.Frame.Position = UDim2_new(0, 16, 0, y) y = y + 16 + chart1.Frame.Size.Y.Offset chart2.Frame.Parent = statsBody chart2.Frame.Position = UDim2_new(0, 16, 0, y) y = y + 16 + chart2.Frame.Size.Y.Offset local this = {} function this.OnPointAdded(this) chart1:OnPointAdded() chart2:OnPointAdded() end function this.SetVisible(this, visible) chart1:SetVisible(visible) chart2:SetVisible(visible) body.Size = open and UDim2_new(1, 0, 0, y) or UDim2_new(1, 0, 1, 0) end function this.Dispose(this) this:SetVisible(false) end return this end; FilterButton = function(chartButton) return scriptStatFilter(chartButton.ChartStat) end; } local function filterStats(stats) -- return stats.Scripts if stats.Scripts then local statsFiltered = {} for k, v in pairs(stats.Scripts) do statsFiltered[k] = {v[1]/100, v[2]} end return statsFiltered end end local function openCallback(openNew) open = openNew end local tab, statList = createStatsTab('ServerScripts', "Server Scripts", 80, config, openCallback, filterStats, {Scripts = true}) textFilterChanged:connect(function() statList:Refresh() end) scriptStatFilterChanged:connect(function() statList:Refresh() end) tab:SetVisible(true) end -- Server Stats -- if permissions.MayViewServerStats then local open = false local config = { GetNotifyColor = function(chartButton) return Color3.new(0.5, 0.5, 0.5) end; CreateChartPage = function(chartButton, statsBody) local chartStat = chartButton.ChartStat local chart1 = devConsole:CreateChart(chartStat.Stats, chartStat.Name, 1) local y = 16 chart1.Frame.Parent = statsBody chart1.Frame.Position = UDim2_new(0, 16, 0, y) y = y + 16 + chart1.Frame.Size.Y.Offset local this = {} function this.OnPointAdded(this) chart1:OnPointAdded() end function this.SetVisible(this, visible) chart1:SetVisible(visible) body.Size = open and UDim2_new(1, 0, 0, y) or UDim2_new(1, 0, 1, 0) end function this.Dispose(this) this:SetVisible(false) end return this end; FilterButton = function(chartButton) return textFilter(chartButton.ChartStat.Name) end; } local function filterStats(stats) local statsFiltered = {} for k, v in pairs(stats) do if type(v) == 'number' then statsFiltered[k] = {v} end end return statsFiltered end local function openCallback(openNew) open = openNew end local tab, statList = createStatsTab('ServerStats', "Server Stats", 70, config, openCallback, filterStats, {Stats = true}) textFilterChanged:connect(function() statList:Refresh() end) tab:SetVisible(true) end -- Server Jobs -- if permissions.MayViewServerJobs then local open = false local config = { GetNotifyColor = function(chartButton) return Color3.new(0.5, 0.5, 0.5) end; CreateChartPage = function(chartButton, statsBody) local chartStat = chartButton.ChartStat local chart1 = devConsole:CreateChart(chartStat.Stats, "Duty Cycle", 1, function(point) return point and math.floor(point * 10000000 + 0.5) / 100000 .. "%" or "" end) local chart2 = devConsole:CreateChart(chartStat.Stats, "Steps Per Sec", 2, function(point) return point and (math.floor(point * 10000 + 0.5) / 10000) .. "/s" or "" end) local chart3 = devConsole:CreateChart(chartStat.Stats, "Step Time", 3, function(point) return point and (math.floor(point * 10000000 + 0.5) / 10000) .. "ms" or "" end) local y = 16 chart1.Frame.Parent = statsBody chart1.Frame.Position = UDim2_new(0, 16, 0, y) y = y + 16 + chart1.Frame.Size.Y.Offset chart2.Frame.Parent = statsBody chart2.Frame.Position = UDim2_new(0, 16, 0, y) y = y + 16 + chart2.Frame.Size.Y.Offset chart3.Frame.Parent = statsBody chart3.Frame.Position = UDim2_new(0, 16, 0, y) y = y + 16 + chart3.Frame.Size.Y.Offset local this = {} function this.OnPointAdded(this) chart1:OnPointAdded() chart2:OnPointAdded() chart3:OnPointAdded() end function this.SetVisible(this, visible) chart1:SetVisible(visible) chart2:SetVisible(visible) chart3:SetVisible(visible) body.Size = open and UDim2_new(1, 0, 0, y) or UDim2_new(1, 0, 1, 0) end function this.Dispose(this) this:SetVisible(false) end return this end; FilterButton = function(chartButton) return textFilter(chartButton.ChartStat.Name) end; } local function filterStats(stats) return stats.Jobs end local function openCallback(openNew) open = openNew end local tab, statList = createStatsTab('ServerJobs', "Server Jobs", 70, config, openCallback, filterStats, {Stats = true}) textFilterChanged:connect(function() statList:Refresh() end) tab:SetVisible(true) end end --[[ do -- Sample tab local tabBody = Primitives.FolderFrame(body, 'TabName') -- 80 is the tab width local tab = devConsole:AddTab("Tab Name", 80, tabBody) tab:SetVisible(true) --tab:SetOpen(true) 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 = "ayaasset://textures/ArrowFarCursor.png" label.Name = "BackupMouse" label.ZIndex = 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 ---------------------- -- Charts and Stats -- ---------------------- do -- Script performance/Chart list --[[ local chartStatExample = { Name = "RoundScript"; Stats = { -- {Activity, InvocationCount} {0, 0}; {0, 0}; } } --]] -- this manages the button and the chartPage local function createChartButton(devConsole, chartList, chartStat, config) local this = { ChartList = chartList; ChartStat = chartStat; Open = false; } local button = Primitives.Button(nil, 'Button') this.Frame = button this.Button = button button.AutoButtonColor = false local size0 = UDim2_new(1, -12 - chartList.ScrollingFrame.ScrollBarThickness, 0, 16) -- Size when script is closed local size1 = UDim2_new(1, -2 - chartList.ScrollingFrame.ScrollBarThickness, 0, 16) -- Size when script is open button.Size = size0 button.Name = (chartStat.Name or "[no name]") if not chartStat.Name then button.TextColor3 = Color3.new(1, 0.5, 0.5) end button.BackgroundColor3 = Style.ScriptButtonColor button.BackgroundTransparency = Style.ScriptBackgroundTransparency local notifyFrame = Primitives.Frame(button, 'NotifyFrame') notifyFrame.BackgroundTransparency = 0 notifyFrame.Size = UDim2.new(0, 8, 1, 0) notifyFrame.BackgroundColor3 = Color3.new(0, 0.75, 0) local label = Primitives.InvisibleTextLabel(button, 'Label', chartStat.Name) label.Size = UDim2_new(1, -notifyFrame.Size.X.Offset - 4 - 1, 1, 0) label.Position = UDim2_new(0, notifyFrame.Size.X.Offset + 4, 0, 0) label.FontSize = 'Size14' label.TextXAlignment = 'Left' -- Enum.TextXAlignment.Left --label.TextWrap = true local buttonEffectFunction = devConsole:CreateButtonEffectFunction(button) devConsole:ConnectButtonHover(button, function(clicking, hovering) buttonEffectFunction(clicking, hovering) end) button.MouseButton1Down:connect(function() -- not ideal for i, that in pairs(chartList.ChartButtons) do if this ~= that and that.Open then that:SetOpen(false) end end this:SetOpen(true) end) -- This fires when the button opens/closes local disconnector = CreateDisconnectSignal() -- This fires when the button disposes local disconnector2 = CreateDisconnectSignal() -- (Best variable name ever) local function refreshNotifyFrame() notifyFrame.BackgroundColor3 = config.GetNotifyColor(this) end refreshNotifyFrame() disconnector2:connect(chartList.OnStatUpdate:connect(refreshNotifyFrame)) function this.SetOpen(this, open) if this.Open == open then return end this.Open = open button:TweenSize(open and size1 or size0, "Out", "Sine", 0.25, true) disconnector:fire() if open then -- The chart page is initialized directly from the button, (this is not ideal, but it works) local statsBody = Primitives.FolderFrame(chartList.Body, 'StatsBody') -- Button container local chartPage = config.CreateChartPage(this, statsBody) chartPage:SetVisible(true) disconnector:connect(chartList.OnStatUpdate:connect(function() chartPage:OnPointAdded() end)) disconnector:connect(function() chartPage:Dispose() statsBody:Destroy() end) end end function this.Dispose(this) button:Destroy() disconnector:fire() disconnector2:fire() end return this end local function defaultSorter(a, b) -- this sorts chartButtons return a.ChartStat.Name < b.ChartStat.Name end function Methods.CreateChartList(devConsole, config) local this = { Config = config; Visible = false; OnStatUpdate = CreateSignal(); ChartButtons = {}; -- usage: chartButtons[position] = scriptButton ChartStats = {}; -- usage: chartStats[chartStat.Name] = chartStat } local frame = Primitives.FolderFrame(nil, 'ScriptList') this.Frame = frame frame.Visible = false local sideMenu = Primitives.Frame(frame, 'SideMenu') -- not necessarily parented to frame! sideMenu.Size = UDim2_new(0, 196, 1, 0) this.SideMenu = sideMenu sideMenu.Visible = false local body = Primitives.FolderFrame(frame, 'Body') this.Body = body body.Size = UDim2_new(1, -sideMenu.Size.X.Offset, 1, 0) body.Position = UDim2_new(0, sideMenu.Size.X.Offset, 0, 0) local scrollingFrame = Instance.new("ScrollingFrame", sideMenu) scrollingFrame.BorderSizePixel = 0 scrollingFrame.ZIndex = ZINDEX scrollingFrame.ScrollBarThickness = 12 this.ScrollingFrame = scrollingFrame do local y = 1 -- if we want to add a label above it later scrollingFrame.Size = UDim2_new(1, 0, 1, -y) scrollingFrame.Position = UDim2_new(0, 0, 0, y) scrollingFrame.BackgroundTransparency = 1 end local chartButtons = this.ChartButtons local chartStats = this.ChartStats local sorter = defaultSorter function this.SetChartButtonSorter(this, sorterNew) if sorter == sorterNew then return end sorter = sorterNew table.sort(chartButtons, sorter) this:Refresh() end function this.GetChartButton(this, name) -- not used? for i = #chartButtons, 1, -1 do local chartButton = chartButtons[i] if chartButton.ChartStat.Name == name then return chartButton, i end end end function this.RemoveChart(this, name) chartStats[name] = nil for i = #chartButtons, 1, -1 do local chartButton = chartButtons[i] if chartButton.ChartStat.Name == name then chartButton:Dispose() table.remove(chartButtons, i) return end end end function this.UpdateStats(this, newStats) local timeStamp = os_time() -- Should it use tick instead? local scriptAddedOrRemoved = false for name, stat in pairs(chartStats) do if not newStats[name] then scriptAddedOrRemoved = true this:RemoveChart(name) end end for name, newStat in pairs(newStats) do local chartStat = chartStats[name] if not chartStats[name] then chartStat = { Name = name; Stats = {}; -- this could be loaded } chartStats[name] = chartStat if this.Visible then local chartButton = createChartButton(devConsole, this, chartStat, config) chartButton.Frame.Parent = scrollingFrame chartButtons[#chartButtons + 1] = chartButton end end local stats = chartStat.Stats stats[#stats + 1] = newStat end table.sort(chartButtons, sorter) this:Refresh() this.OnStatUpdate:fire() end function this.Refresh(this) if not this.Visible then for i = #chartButtons, 1, -1 do chartButtons[i]:Dispose() chartButtons[i] = nil end return end table.sort(chartButtons, sorter) local y = 0 for i = 1, #chartButtons do local chartButton = chartButtons[i] local visible = config.FilterButton(chartButton) local button = chartButton.Button button.Visible = visible if visible then button.Position = UDim2_new(0, 1, 0, y) -- Should it lerp to position if animating? y = y + button.AbsoluteSize.Y + 1 end end scrollingFrame.CanvasSize = UDim2_new(0, 0, 0, y) end this:Refresh() function this.SetVisible(this, visible) if visible == this.Visible then return end this.Visible = visible frame.Visible = visible sideMenu.Visible = visible if visible then for name, chartStat in pairs(chartStats) do local chartButton = createChartButton(devConsole, this, chartStat, config) chartButton.Button.Parent = scrollingFrame chartButtons[#chartButtons + 1] = chartButton end this:Refresh() else for i = #chartButtons, 1, -1 do chartButtons[i]:Dispose() chartButtons[i] = nil end end end return this end end do -- Chart local barWidth = 4 local numBars = math.ceil((Style.ChartWidth - Style.BorderSize * 2) / (barWidth + 1)) local function round(x) return math.floor(x * 1000 + 0.5) / 1000 end local function CreateBar() local bar = Primitives.Frame() bar.BackgroundTransparency = 0 bar.BackgroundColor3 = Color3_new(0, 0.5, 1) return bar end local function CreateGraph(points, statIndex, autoScale) -- point = points[i][statIndex] local this = {} local direction = Style.ChartGraphDirection -- -1 means coming from right, 1 means coming from left local frame = Primitives.Frame(nil, 'Graph') this.Frame = frame frame.ClipsDescendants = true local scaleFrame = Primitives.FolderFrame(frame, 'ScaleFrame') local body = Primitives.FolderFrame(scaleFrame, 'Body') body.Size = UDim2_new(0, barWidth, 1, 0) local bars = {} local barHeights = {} local barPositions = {} do -- reference notches local function getReferenceHeight(position) if position % 60 == 0 then return 24 elseif position % 30 == 0 then return 12 elseif position % 15 == 0 then return 4 elseif position % 5 == 0 then return 1 else return 0 end end for position = 0, numBars do local height = getReferenceHeight(position) if height ~= 0 then local notch = Instance_new('Frame', frame) notch.ZIndex = ZINDEX notch.BorderSizePixel = 0 notch.BackgroundColor3 = Color3_new(1, 1, 1) notch.Size = UDim2_new(0, 1, 0, height) notch.Position = UDim2_new(0, position * (barWidth + 1), 1, -height) end end end local scale = 1 local position = 0 local visible = false local function generateSizeAndPosition(height, position) height = height * scale return UDim2_new(0, barWidth, height, 0), UDim2_new(0, (barWidth + 1) * position * -direction + 1, 1 - height, 0) end local function RefreshScale(animate) if not autoScale then return end local heightMax; for i = math_max(#points - numBars + 1, 1), #points do local height = points[i][statIndex] if not heightMax or height > heightMax then heightMax = height end end if not heightMax or heightMax <= 0 then local size, position = UDim2_new(1, 0, 1, 0), UDim2_new(0, 0, 0, 0) if animate then scaleFrame:TweenSizeAndPosition(size, position, 'Out', 'Sine', 0.25, true) else scaleFrame.Size, scaleFrame.Position = UDim2_new(1, 0, 1, 0), UDim2_new(0, 0, 0, 0) end return end local scaleNew = 1 / heightMax * 0.95 if math.abs(scale - scaleNew) < 0.0000001 then return end -- Possible performance boost Todo: if the scale isn't significantly different (within like 0.5-4), just adjust scaleFrame's size scale = scaleNew for i = 1, #bars do local bar = bars[i] local height = barHeights[i] local barSize, barPosition = generateSizeAndPosition(height, barPositions[i]) if animate then bar:TweenSizeAndPosition(barSize, barPosition, 'Out', 'Sine', 0.25, true) else bar.Size, bar.Position = barSize, barPosition end end --local scale = 1 / heightMax * 0.95 --scaleFrame:TweenSizeAndPosition(UDim2_new(1, 0, scale, 0), UDim2_new(0, 0, 1 - scale, 0), 'Out', 'Sine', 0.25, true) end function this.OnPointAdded(this) if not visible then return end local bar; -- possible game crasher while #bars > numBars do if bar then bar:Destroy() end bar = bars[1] table.remove(bars, 1) table.remove(barHeights, 1) table.remove(barPositions, 1) end local point = points[#points] and points[#points][statIndex] assert(point) if not bar then bar = CreateBar() bar.Parent = body end local height = point bars[#bars + 1] = bar barHeights[#barHeights + 1] = height barPositions[#barPositions + 1] = position bar.Size, bar.Position = generateSizeAndPosition(height, position) body:TweenPosition(UDim2_new(1 - (direction * 0.5 + 0.5), (barWidth + 1) * position * direction, 0, 0), 'Out', 'Sine', 0.25, true) position = position + 1 RefreshScale(true) end function this.SetVisible(this, visibleNew) body.Position = UDim2_new(1 - (direction * 0.5 + 0.5), 0, 0, 0) if visibleNew == visible then return end visible = visibleNew if not visible then for i = #bars, 1, -1 do bars[i]:Destroy() bars[i] = nil end return end position = 0 for i = math_max(#points - numBars + 1, 1), #points do local bar = bars[position + 1] if not bar then bar = CreateBar() bar.Parent = body bars[position + 1] = bar end local point = points[i][statIndex] local height = point barHeights[position + 1] = height barPositions[position + 1] = position bar.Size, bar.Position = generateSizeAndPosition(height, position) position = position + 1 end body.Position = UDim2_new(1 - (direction * 0.5 + 0.5), (barWidth + 1) * (position - 1) * direction, 0, 0) RefreshScale(false) end return this end local function createLabel(...) local n = Primitives.InvisibleTextLabel(...) n.TextXAlignment = 'Left' n.FontSize = 'Size14' return n end function Methods.CreateChart(devConsole, points, title, statIndex, pointToString) pointToString = pointToString or function(point) if point then local precision = 10000 local v = point * precision if v < 1 and v > 0 then return "<" .. (1 / precision) else return math.floor(v + 0.5) / precision .. "" end else return "" end end local chart = {} local frame = Primitives.Frame(nil, 'Chart') chart.Frame = frame frame.Size = UDim2_new(0, Style.ChartWidth, 0, Style.ChartHeight) local labelCurrent = createLabel(frame, 'Current', "Current: " .. pointToString(points[#points] and points[#points][statIndex])) labelCurrent.Size = UDim2_new(0, 0.5, 0, Style.ChartTitleHeight) labelCurrent.Position = UDim2_new(0, 4, 0, Style.ChartTitleHeight + 1) local graph = CreateGraph(points, statIndex, true) graph.Frame.Parent = frame graph.Frame.Size = UDim2_new(0, Style.ChartWidth - Style.BorderSize * 2, 0, Style.ChartGraphHeight) graph.Frame.Position = UDim2_new(0, Style.BorderSize, 0, Style.ChartTitleHeight + Style.ChartDataHeight) do local bar = Primitives.Frame(frame, 'Bar') bar.Size = UDim2_new(1, 0, 0, Style.ChartTitleHeight) local label = Primitives.InvisibleTextLabel(bar, 'Title', title) label.TextXAlignment = 'Left' -- Enum.TextXAlignment.Left label.Size = UDim2_new(1, -4, 1, 0) label.Position = UDim2_new(0, 4, 0, 0) label.FontSize = 'Size18' end local visible = false function chart.SetVisible(chart, visibleNew) if visibleNew == visible then return end visible = visibleNew graph:SetVisible(visible) end function chart.OnPointAdded(chart) local point = points[#points] if not point then return end labelCurrent.Text = "Current: " .. pointToString(point and point[statIndex]) graph:OnPointAdded() end return chart 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 textBox = Primitives.TextBox(textBoxFrame, 'TextBox') --textBox.TextWrapped = true -- This needs to auto-resize textBox.BackgroundTransparency = 1 textBox.Text = "Type command here" 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 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() 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 = "" textBox:CaptureFocus() else backtrackPosition = 0 focusLostWithoutEnter = true addInputtedText(textBox.Text, true) 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; } 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 = ZINDEX label.BackgroundTransparency = 1 label.Font = Enum.Font.Code label.FontSize = 'Size14' 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 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.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, width, 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 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.FontSize = Enum.FontSize.Size14 --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.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 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: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 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 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) end function Methods.CreateScrollbar(devConsole, rotation) local scrollbar = {} local 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://assetdelivery.roblox.com/v1/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://assetdelivery.roblox.com/v1/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://assetdelivery.roblox.com/v1/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() if dragging then return end dragging = true mouseInteractCallback(dragging, hovering) local deltaCallback; local x0, y0 = mouse.X, mouse.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) --]] -- VIRTUALVERSION CHANGE GameSettings.Changed:connect(function(prop) if prop ~= 'VirtualVersion' then return end for i, listener in ipairs(listeners) do listener:disconnect() 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 input.UserInputType ~= Enum.UserInputType.MouseMovement then return end local p1 = input.Position if not p1 then return end local x1, y1 = mouse.X, mouse.Y --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.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 permissions; function DeveloperConsole.GetPermissions() if permissions then return permissions end permissions = {} pcall(function() permissions.CreatorFlagValue = settings():GetFFlag("UseCanManageApiToDetermineConsoleAccess") end) pcall(function() -- This might not support group games, I'll leave it up to "UseCanManageApiToDetermineConsoleAccess" permissions.IsCreator = permissions.CreatorFlagValue or game:GetService("Players").LocalPlayer.userId == game.CreatorId end) if permissions.CreatorFlagValue then -- Use the new API 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, false, Enum.ThrottlingPriority.Default) 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 end permissions.ClientCodeExecutionEnabled = false pcall(function() permissions.ServerCodeExecutionEnabled = permissions.IsCreator and settings():GetFFlag("ConsoleCodeExecutionEnabled") end) if DEBUG then permissions.IsCreator = true permissions.ServerCodeExecutionEnabled = true end permissions.MayViewServerLog = permissions.IsCreator permissions.MayViewClientLog = true permissions.MayViewServerStats = permissions.IsCreator permissions.MayViewServerScripts = permissions.IsCreator permissions.MayViewServerJobs = permissions.IsCreator 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) local message = { Message = text or "[DevConsole Error 3]"; Time = ConvertTimeStamp(os_time()); 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 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 return DeveloperConsole