Files
aya/client/common/content/scripts/Modules/DeveloperConsoleModule.lua
2025-12-17 16:47:48 +00:00

2974 lines
82 KiB
Lua

-- 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