This is a sub-page of Roblox Developer.
|
|
To do: |
Roblox developer has a bunch of scripts that are copied from the PC version, Most of them being unused and some dev comments.
VR#
In content/scripts/Module/VR and content/scripts/Module are unused scripts for VR.
VR Scripts
-- VirtualKeyboard.lua --
-- Written by Kip Turner, copyright ROBLOX 2016 --
local CoreGui = game:GetService('CoreGui')
local RunService = game:GetService('RunService')
local UserInputService = game:GetService('UserInputService')
local GuiService = game:GetService('GuiService')
local HttpService = game:GetService('HttpService')
local ContextActionService = game:GetService('ContextActionService')
local PlayersService = game:GetService('Players')
local TextService = game:GetService('TextService')
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Util = require(RobloxGui.Modules.Settings.Utility)
local vrKeyboardSuccess, vrKeyboardFlagValue = pcall(function() return settings():GetFFlag("UseVRKeyboardInLua") end)
local useVRKeyboard = (vrKeyboardSuccess and vrKeyboardFlagValue == true)
local BACKGROUND_OPACITY = 0.3
local NORMAL_KEY_COLOR = Color3.new(49/255,49/255,49/255)
local HOVER_KEY_COLOR = Color3.new(49/255,49/255,49/255)
local PRESSED_KEY_COLOR = Color3.new(0,162/255,1)
local SET_KEY_COLOR = Color3.new(0,162/255,1)
local KEY_TEXT_COLOR = Color3.new(1,1,1)
---------------------------------------- KEYBOARD LAYOUT --------------------------------------
local MINIMAL_KEYBOARD_LAYOUT = HttpService:JSONDecode([==[
[
[
{
"a": 7,
"w": 0.8
},
"*",
"Q",
"W",
"E",
"R",
"T",
"Y",
"U",
"I",
"O",
"P",
{
"w": 1.8
},
"Delete"
],
[
{
"w": 1.6
},
"Caps",
"A",
"S",
"D",
"F",
"G",
"H",
"J",
"K",
"L",
"?",
{
"h": 2,
"w2": 2.4,
"h2": 1,
"x2": -1.4,
"y2": 1
},
"Enter"
],
[
{
"w": 2.2
},
"Shift",
"Z",
"X",
"C",
"V",
"B",
"N",
"M",
"."
],
[
{
"w": 2.2
},
"123/sym",
{
"w": 8
},
"",
{
"w": 2.4
},
"<Speaker>"
]
]
]==])
local MINIMAL_KEYBOARD_LAYOUT_SYMBOLS = HttpService:JSONDecode([==[
[
[
{
"a": 7,
"w": 0.8
},
"*",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
{
"w": 1.8
},
"Delete"
],
[
{
"w": 1.6
},
"!",
"@",
"#",
"$",
"%",
"^",
"&",
"(",
")",
"=",
"?",
{
"h": 2,
"w2": 2.4,
"h2": 1,
"x2": -1.4,
"y2": 1
},
"Enter"
],
[
{
"w": 1.2
},
"/",
"-",
"+",
"_",
":",
";",
"'",
"\"",
",",
"."
],
[
{
"w": 2.2
},
"abc",
{
"w": 8
},
"",
{
"w": 2.4
},
"<Speaker>"
]
]
]==])
---------------------------------------- END KEYBOARD LAYOUT --------------------------------------
local function tokenizeString(str, tokenChar)
local words = {}
for word in string.gmatch(str, '([^' .. tokenChar .. ']+)') do
table.insert(words, word)
end
return words
end
local function ConvertFontSizeEnumToInt(fontSizeEnum)
local result = string.match(fontSizeEnum.Name, '%d+')
return (result and tostring(result)) or 12
end
-- RayPlaneIntersection
-- http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm
local function RayPlaneIntersection(ray, planeNormal, pointOnPlane)
planeNormal = planeNormal.unit
ray = ray.Unit
-- compute Pn (dot) Rd = Vd and check if Vd == 0 then we know ray is parallel to plane
local Vd = planeNormal:Dot(ray.Direction)
-- could fuzzy equals this a little bit to account for imprecision or very close angles to zero
if Vd == 0 then -- parallel, no intersection
return nil
end
local V0 = planeNormal:Dot(pointOnPlane - ray.Origin)
local t = V0 / Vd
if t < 0 then --plane is behind ray origin, and thus there is no intersection
return nil
end
return ray.Origin + ray.Direction * t
end
function Clamp(low, high, input)
return math.max(low, math.min(high, input))
end
-- No rotation as of yet
local function PointInGuiObject(object, x, y)
local minPt = object.AbsolutePosition
local maxPt = object.AbsolutePosition + object.AbsoluteSize
if minPt.X <= x and maxPt.X >= x and minPt.Y <= y and maxPt.Y >= y then
return true
end
return false
end
local function ExtendedInstance(instance)
local this = {}
do
local mt =
{
__index = function (t, k)
return instance[k]
end;
__newindex = function (t, k, v)
instance[k] = v
end;
}
setmetatable(this, mt)
end
return this
end
local function IsVoiceToTextEnabled()
return false
end
local function CreateVRButton(instance)
local newButton = ExtendedInstance(instance)
rawset(newButton, "OnEnter", function(self)
end)
rawset(newButton, "OnLeave", function(self)
end)
rawset(newButton, "OnDown", function(self)
end)
rawset(newButton, "OnUp", function(self)
end)
rawset(newButton, "ContainsPoint", function(self, x, y)
return PointInGuiObject(instance, x, y)
end)
rawset(newButton, "Update", function(self)
end)
return newButton
end
local selectionRing = Util:Create'ImageLabel'
{
Name = 'SelectionRing';
Size = UDim2.new(1, -6, 1, -6);
Position = UDim2.new(0, 4, 0, 3);
Image = 'rbxasset://textures/ui/menu/buttonHover.png';
ScaleType = Enum.ScaleType.Slice;
SliceCenter = Rect.new(94/2, 94/2, 94/2, 94/2);
BackgroundTransparency = 1;
}
local KEY_ICONS =
{
["<Speaker>"] = {Asset = "rbxasset://textures/ui/Keyboard/mic_icon.png", AspectRatio = 0.615};
}
local function CreateKeyboardKey(keyboard, layoutData, keyData)
local isSpecialShapeKey = layoutData['width2'] and layoutData['height2'] and layoutData['x2'] and layoutData['y2']
local newKeyElement = Util:Create'ImageButton'
{
Name = keyData[1];
Position = UDim2.new(layoutData['x'], 0, layoutData['y'], 0);
Size = UDim2.new(layoutData['width'], 0, layoutData['height'], 0);
BorderSizePixel = 0;
Image = "";
BackgroundTransparency = 1;
ZIndex = 1;
}
local keyText = Util:Create'TextLabel'
{
Name = "KeyText";
Text = keyData[#keyData];
Position = UDim2.new(0, -10, 0, -10);
Size = UDim2.new(1, 0, 1, 0);
Font = Enum.Font.SourceSansBold;
FontSize = Enum.FontSize.Size96;
TextColor3 = KEY_TEXT_COLOR;
BackgroundTransparency = 1;
Selectable = true;
ZIndex = 2;
Parent = newKeyElement;
}
local backgroundImage = Util:Create'Frame'
{
Name = 'KeyBackground';
Size = UDim2.new(1,-10,1,-10);
Position = UDim2.new(0,-5,0,-5);
BackgroundColor3 = NORMAL_KEY_COLOR;
BackgroundTransparency = BACKGROUND_OPACITY;
BorderSizePixel = 0;
Parent = newKeyElement;
}
local selectionObject = Util:Create'ImageLabel'
{
Name = 'SelectionObject';
Size = UDim2.new(1,0,1,0);
BackgroundTransparency = 1;
Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png";
ImageTransparency = 0;
ScaleType = Enum.ScaleType.Slice;
SliceCenter = Rect.new(12,12,52,52);
BorderSizePixel = 0;
}
newKeyElement.SelectionImageObject = Util:Create'ImageLabel'
{
Visible = false;
}
-- Special silly enter key nonsense
local secondBackgroundImage = nil
local specialSelectionObject, specialSelectionObject2, specialSelectionObject3 = nil, nil, nil
if isSpecialShapeKey then
secondBackgroundImage = Util:Create'ImageButton'
{
Name = 'KeyBackground';
Position = UDim2.new(layoutData['x2'] / layoutData['width'], -5, layoutData['y2'] / layoutData['height'], -5);
Size = UDim2.new(layoutData['width2'] / layoutData['width'], 0, layoutData['height2'] / layoutData['height'], -10);
BackgroundColor3 = NORMAL_KEY_COLOR;
BackgroundTransparency = BACKGROUND_OPACITY;
BorderSizePixel = 0;
AutoButtonColor = false;
Parent = newKeyElement;
}
if layoutData['x2'] <= 0 then
keyText.Size = secondBackgroundImage.Size - UDim2.new(0,10,0,0)
keyText.Position = secondBackgroundImage.Position
secondBackgroundImage.Size = secondBackgroundImage.Size - UDim2.new(1,0,0,0)
end
do
specialSelectionObject = Util:Create'Frame'
{
Name = 'SpecialSelectionObject';
Size = UDim2.new(1,0,0.5,0);
Position = UDim2.new(0,0,0.5,0);
BackgroundTransparency = 1;
ClipsDescendants = true;
Util:Create'ImageLabel'
{
Name = 'Borders';
Position = UDim2.new(-1,0,-1,0);
Size = UDim2.new(2,0,2,0);
BackgroundTransparency = 1;
Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png";
ImageTransparency = 0;
ScaleType = Enum.ScaleType.Slice;
SliceCenter = Rect.new(12,12,52,52);
};
}
specialSelectionObject2 = specialSelectionObject:Clone()
specialSelectionObject2.Size = UDim2.new(1,0,0.5,5)
specialSelectionObject2.Position = UDim2.new(0,0,0,0)
specialSelectionObject2.Borders.Size = UDim2.new(1,0,1,30)
specialSelectionObject2.Borders.Position = UDim2.new(0,0,0,0)
specialSelectionObject3 = specialSelectionObject:Clone()
specialSelectionObject3.Size = UDim2.new(1,5,1,0)
specialSelectionObject3.Position = UDim2.new(0,0,0,0)
specialSelectionObject3.Borders.Size = UDim2.new(1,30,1,0)
specialSelectionObject3.Borders.Position = UDim2.new(0,0,0,0)
end
-- End of nonsense
end
local newKey = CreateVRButton(newKeyElement)
local hovering = false
local pressed = false
local isAlpha = #keyData == 1 and type(keyData[1]) == 'string' and #keyData[1] == 1 and
string.byte(keyData[1]) >= string.byte("A") and string.byte(keyData[1]) <= string.byte("z")
local icon = nil
if keyData[1] and KEY_ICONS[keyData[1]] then
keyText.Visible = false
icon = Util:Create'ImageLabel'
{
Name = 'KeyIcon';
Size = UDim2.new(KEY_ICONS[keyData[1]].AspectRatio, -20, 1, -20);
SizeConstraint = Enum.SizeConstraint.RelativeYY;
BackgroundTransparency = 1;
Image = KEY_ICONS[keyData[1]].Asset;
Parent = backgroundImage;
}
local function onChanged(prop)
if prop == 'AbsoluteSize' then
icon.Position = UDim2.new(0.5,-icon.AbsoluteSize.X/2,0.5,-icon.AbsoluteSize.Y/2);
end
end
icon.Changed:connect(onChanged)
onChanged('AbsoluteSize')
end
local function isEnabled()
if newKey:GetCurrentKeyValue() == "<Speaker>" then
return IsVoiceToTextEnabled()
end
return true
end
local function onClicked()
local keyValue = nil
local currentKeySetting = newKey:GetCurrentKeyValue()
if currentKeySetting == 'Shift' then
keyboard:SetShift(not keyboard:GetShift())
elseif currentKeySetting == 'Caps' then
keyboard:SetCaps(not keyboard:GetCaps())
elseif currentKeySetting == 'Enter' then
keyboard:SubmitText(true, true)
elseif currentKeySetting == 'Delete' then
keyboard:BackspaceAtCursor()
elseif currentKeySetting == "123/sym" then
keyboard:SetCurrentKeyset(2)
elseif currentKeySetting == "abc" then
keyboard:SetCurrentKeyset(1)
elseif currentKeySetting == "<Speaker>" then
keyboard:SetVoiceMode(true)
elseif currentKeySetting == 'Tab' then
keyValue = '\t'
else
keyValue = currentKeySetting
end
if keyValue ~= nil then
keyboard:SubmitCharacter(keyValue, isAlpha)
end
end
local function setKeyColor(newColor, hovering)
backgroundImage.BackgroundColor3 = newColor
if secondBackgroundImage then
secondBackgroundImage.BackgroundColor3 = newColor
end
if isSpecialShapeKey then
specialSelectionObject.Parent = hovering and backgroundImage or nil
specialSelectionObject2.Parent = hovering and backgroundImage or nil
specialSelectionObject3.Parent = hovering and secondBackgroundImage or nil
else
selectionObject.Parent = hovering and backgroundImage or nil
end
end
local function update()
local currentKey = newKey:GetCurrentKeyValue()
if pressed then
setKeyColor(PRESSED_KEY_COLOR, false)
elseif hovering then
setKeyColor(HOVER_KEY_COLOR, true)
elseif currentKey == 'Caps' and keyboard:GetCaps() then
setKeyColor(SET_KEY_COLOR, false)
elseif currentKey == 'Shift' and keyboard:GetShift() then
setKeyColor(SET_KEY_COLOR, false)
elseif currentKey == 'abc' then
setKeyColor(SET_KEY_COLOR, false)
else
setKeyColor(NORMAL_KEY_COLOR, false)
end
if icon then
icon.ImageTransparency = isEnabled() and 0 or 0.5
end
keyText.Text = newKey:GetCurrentKeyValue()
end
local hoveringGuiElements = {}
rawset(newKey, "OnEnter", function(self)
hovering = true
update()
end)
rawset(newKey, "OnLeave", function(self)
if not next(hoveringGuiElements) then
hovering = false
pressed = false
update()
end
end)
rawset(newKey, "OnDown", function(self)
pressed = true
update()
end)
rawset(newKey, "OnUp", function(self)
pressed = false
update()
end)
rawset(newKey, "GetCurrentKeyValue", function(self)
local shiftEnabled = keyboard:GetShift()
local capsEnabled = keyboard:GetCaps()
if isAlpha then
if capsEnabled and shiftEnabled then
return string.lower(keyData[#keyData])
elseif capsEnabled or shiftEnabled then
return keyData[1]
else
return string.lower(keyData[#keyData])
end
end
if shiftEnabled then
return keyData[1]
end
return keyData[#keyData]
end)
rawset(newKey, "ContainsPoint", function(self, x, y)
return PointInGuiObject(backgroundImage, x, y) or
(secondBackgroundImage and PointInGuiObject(secondBackgroundImage, x, y))
end)
rawset(newKey, "Update", function(self)
update()
end)
rawset(newKey, "GetInstance", function(self)
return newKeyElement
end)
newKeyElement.MouseButton1Down:connect(function() newKey:OnDown() end)
newKeyElement.MouseButton1Up:connect(function() newKey:OnUp() end)
newKeyElement.SelectionGained:connect(function() hoveringGuiElements[newKeyElement] = true newKey:OnEnter() end)
newKeyElement.SelectionLost:connect(function() hoveringGuiElements[newKeyElement] = nil newKey:OnLeave() end)
newKeyElement.MouseButton1Click:connect(function() onClicked() end)
if secondBackgroundImage then
secondBackgroundImage.MouseButton1Click:connect(onClicked)
secondBackgroundImage.MouseButton1Down:connect(function() newKey:OnDown() end)
secondBackgroundImage.MouseButton1Up:connect(function() newKey:OnUp() end)
secondBackgroundImage.SelectionGained:connect(function()
hoveringGuiElements[secondBackgroundImage] = true
newKey:OnEnter()
end)
secondBackgroundImage.SelectionLost:connect(function()
hoveringGuiElements[secondBackgroundImage] = nil
newKey:OnLeave()
end)
end
update()
return newKey
end
local function ConstructKeyboardUI(keyboardLayoutDefinitions)
local Panel3D = require(RobloxGui.Modules.VR.Panel3D)
local panel = Panel3D.Get("Keyboard")
panel:SetVisible(false)
-- DEBUG setting
-- panel:GetGUI().Parent = workspace
local buttons = {}
local keyboardContainer = Util:Create'Frame'
{
Name = 'VirtualKeyboard';
Size = UDim2.new(1, 0, 1, 0);
Position = UDim2.new(0, 0, 0, 0);
BackgroundTransparency = 1;
Active = true;
Visible = false;
};
local textEntryBackground = Util:Create'ImageLabel'
{
Name = 'TextEntryBackground';
Size = UDim2.new(0.5,0,0.125,0);
Position = UDim2.new(0.25,0,0,0);
Image = "";
BackgroundTransparency = 0.5;
BackgroundColor3 = Color3.new(31/255,31/255,31/255);
BorderSizePixel = 0;
ClipsDescendants = true;
Parent = keyboardContainer;
}
local textfieldBackground = Util:Create'Frame'
{
Name = 'TextfieldBackground';
Position = UDim2.new(0,2,0,2);
Size = UDim2.new(1, -4, 1, -4);
BackgroundTransparency = 0;
BackgroundColor3 = Color3.new(209/255,216/255,221/255);
BorderSizePixel = 0;
Visible = true;
Parent = textEntryBackground;
};
local textEntryField = Util:Create'TextButton'
{
Name = "TextEntryField";
Text = "";
Position = UDim2.new(0,4,0,4);
Size = UDim2.new(1, -8, 1, -8);
Font = Enum.Font.SourceSans;
FontSize = Enum.FontSize.Size60;
TextXAlignment = Enum.TextXAlignment.Left;
BackgroundTransparency = 1;
BorderSizePixel = 0;
Parent = textfieldBackground;
}
local textfieldCursor = Util:Create'Frame'
{
Name = 'TextfieldCursor';
Size = UDim2.new(0, 5, 0.9, 0);
Position = UDim2.new(0, 0, 0.05, 0);
BackgroundTransparency = 0;
BackgroundColor3 = SET_KEY_COLOR;
BorderSizePixel = 0;
Visible = true;
ZIndex = 2;
Parent = textEntryField;
};
local closeButtonElement = Util:Create'ImageButton'
{
Name = 'CloseButton';
Size = UDim2.new(0.075,-10,0.198,-10);
Position = UDim2.new(0,-5,0,-35);
Image = "rbxasset://textures/ui/Keyboard/close_button_background.png";
BackgroundTransparency = 1;
AutoButtonColor = false;
Parent = keyboardContainer;
}
do
closeButtonElement.SelectionImageObject = Util:Create'ImageLabel'
{
Name = 'Selection';
Size = UDim2.new(0.9,0,0.9,0);
Position = UDim2.new(0.05,0,0.05,0);
Image = "rbxasset://textures/ui/Keyboard/close_button_selection.png";
BackgroundTransparency = 1;
}
Util:Create'ImageLabel'
{
Name = 'Icon';
Size = UDim2.new(0.5,0,0.5,0);
Position = UDim2.new(0.25,0,0.25,0);
Image = "rbxasset://textures/ui/Keyboard/close_button_icon.png";
BackgroundTransparency = 1;
Parent = closeButtonElement;
}
end
local closeButton = CreateVRButton(closeButtonElement)
table.insert(buttons, closeButton)
local voiceRecognitionContainer = Util:Create'Frame'
{
Name = 'VoiceRecognitionContainer';
Size = UDim2.new(1, 0, 0.85, 0);
Position = UDim2.new(0, 0, 0.15, 0);
BackgroundTransparency = 1;
Active = true;
Visible = false;
Parent = keyboardContainer;
};
do
local voiceRecognitionBackground1 = Util:Create'Frame'
{
Name = 'voiceRecognitionBackground1';
Size = UDim2.new(1, 0, 0.75, 0);
Position = UDim2.new(0, 0, 0, 0);
BackgroundColor3 = Color3.new(0,0,0);
BackgroundTransparency = 0.5;
BorderSizePixel = 0;
Active = true;
Parent = voiceRecognitionContainer;
};
local voiceRecognitionBackground2 = voiceRecognitionBackground1:Clone()
voiceRecognitionBackground2.Size = UDim2.new(0.75, 0, 0.25, 0)
voiceRecognitionBackground2.Position = UDim2.new(0, 0, 0.75, 0)
voiceRecognitionBackground2.Parent = voiceRecognitionContainer
end
local voiceDoneButton = CreateVRButton(Util:Create'ImageButton'
{
Name = 'DoneButton';
Size = UDim2.new(0.25,-5,0.25,-5);
Position = UDim2.new(0.75,5,0.75,5);
Image = "";
BackgroundTransparency = 0;
AutoButtonColor = false;
BorderSizePixel = 0;
Parent = voiceRecognitionContainer;
})
table.insert(buttons, voiceDoneButton)
local newKeyboard = ExtendedInstance(keyboardContainer)
local keyboardOptions = nil
local keysets = {}
local capsLockEnabled = false
local shiftEnabled = false
local textfieldCursorPosition = 0
local closedEvent = Instance.new('BindableEvent')
local opened = false
local function SetTextFieldCursorPosition(newPosition)
textfieldCursorPosition = Clamp(0, #textEntryField.Text, newPosition)
if not textEntryField.TextFits then
textfieldCursorPosition = #textEntryField.Text
end
local textSize = TextService:GetTextSize(
string.sub(textEntryField.Text, 1, textfieldCursorPosition),
ConvertFontSizeEnumToInt(textEntryField.FontSize),
textEntryField.Font,
textEntryField.AbsoluteSize)
textfieldCursor.Position = UDim2.new(0, textSize.x, textfieldCursor.Position.Y.Scale, textfieldCursor.Position.Y.Offset)
end
local function UpdateTextEntryFieldText(newText)
textEntryField.Text = newText
SetTextFieldCursorPosition(textfieldCursorPosition)
end
local buffer = ""
local function getBufferText()
if keyboardOptions and keyboardOptions.TextBox then
return keyboardOptions.TextBox.Text
end
return buffer
end
local function setBufferText(newBufferText)
if keyboardOptions and keyboardOptions.TextBox then
keyboardOptions.TextBox.Text = newBufferText
elseif buffer ~= newBufferText then
buffer = newBufferText
UpdateTextEntryFieldText(buffer)
end
end
local function calculateTextCursorPosition(x, y)
x = x - textEntryField.AbsolutePosition.x
y = y - textEntryField.AbsolutePosition.y
for i = 1, #textEntryField.Text do
local textSize = TextService:GetTextSize(
string.sub(textEntryField.Text, 1, i),
ConvertFontSizeEnumToInt(textEntryField.FontSize),
textEntryField.Font,
textEntryField.AbsoluteSize)
if textSize.x > x then
return i - 1
end
end
return #textEntryField.Text
end
local currentKeyset = nil
rawset(newKeyboard, "ClosedEvent", closedEvent.Event)
rawset(newKeyboard, "GetCurrentKeyset", function(self)
return keysets[currentKeyset]
end)
rawset(newKeyboard, "SetCurrentKeyset", function(self, newKeyset)
if newKeyset ~= currentKeyset and keysets[newKeyset] ~= nil then
if keysets[currentKeyset] and keysets[currentKeyset].container then
keysets[currentKeyset].container.Visible = false
end
currentKeyset = newKeyset
if keysets[currentKeyset] and keysets[currentKeyset].container then
keysets[currentKeyset].container.Visible = true
end
end
end)
rawset(newKeyboard, "SetVoiceMode", function(self, inVoiceMode)
inVoiceMode = inVoiceMode and IsVoiceToTextEnabled()
local currentKeysetObject = self:GetCurrentKeyset()
if currentKeysetObject and currentKeysetObject.container then
currentKeysetObject.container.Visible = not inVoiceMode
end
voiceRecognitionContainer.Visible = inVoiceMode
end)
rawset(newKeyboard, "GetCaps", function(self)
return capsLockEnabled
end)
rawset(newKeyboard, "SetCaps", function(self, newCaps)
capsLockEnabled = newCaps
for _, key in pairs(self:GetCurrentKeyset().keys) do
key:Update()
end
end)
rawset(newKeyboard, "GetShift", function(self)
return shiftEnabled
end)
rawset(newKeyboard, "SetShift", function(self, newShift)
shiftEnabled = newShift
for _, key in pairs(self:GetCurrentKeyset().keys) do
key:Update()
end
end)
local textChangedConn = nil
local panelClosedConn = nil
rawset(newKeyboard, "Open", function(self, options)
if opened then return end
opened = true
keyboardOptions = options
self:SetCurrentKeyset(1)
self:SetVoiceMode(false)
keyboardContainer.Visible = true
panel:ResizeStuds(5.9, 2.25, 320)
local localCF = CFrame.new()
if textChangedConn then textChangedConn:disconnect() end
textChangedConn = nil
if options.TextBox then
textChangedConn = options.TextBox.Changed:connect(function(prop)
if prop == 'Text' then
UpdateTextEntryFieldText(options.TextBox.Text)
end
end)
options.TextBox.FocusLost:connect(function(submitted)
self:Close(submitted)
end)
if options.TextBox.ClearTextOnFocus then
setBufferText("")
else
UpdateTextEntryFieldText(options.TextBox.Text)
end
local textboxPanel = Panel3D.FindContainerOf(options.TextBox)
if textboxPanel then
panelClosedConn = Panel3D.OnPanelClosed.Event:connect(function(closedPanelName)
if closedPanelName == textboxPanel.name then
self:Close(false)
end
end)
--Attach to it if it's in the same space
if textboxPanel.panelType == Panel3D.Type.Fixed then
local panelCF = textboxPanel.localCF
localCF = panelCF * CFrame.new(0, (-textboxPanel.height / 2) - 0.5, 0) *
CFrame.Angles(math.rad(30), 0, 0) *
CFrame.new(0, (-panel.height / 2) - 0.5, 0)
else
--Otherwise, best-guess where it should go based on the user's head.
local headForwardCF = Panel3D.GetHeadLookXZ(true)
localCF = headForwardCF * CFrame.Angles(math.rad(22.5), 0, 0) * CFrame.new(0, -1, 5)
end
end
else
setBufferText("")
end
self.Parent = panel:GetGUI()
panel:SetType(Panel3D.Type.Fixed, { CFrame = localCF })
panel:SetCanFade(false)
panel:SetVisible(true, true)
panel:ForceShowUntilLookedAt()
function panel:OnUpdate()
end
end)
rawset(newKeyboard, "Close", function(self, submit)
submit = (submit == true)
if not opened then return end
opened = false
if textChangedConn then textChangedConn:disconnect() end
textChangedConn = nil
if panelClosedConn then panelClosedConn:disconnect() end
panelClosedConn = nil
-- Clean-up
panel:OnMouseLeave()
panel:SetVisible(false, true)
keyboardContainer.Visible = false
self:SubmitText(submit, false)
end)
rawset(newKeyboard, "SubmitText", function(self, submit, keepKeyboardOpen)
local keyboardTextbox = keyboardOptions and keyboardOptions.TextBox
if keyboardTextbox then
if submit then
keyboardTextbox.Text = getBufferText()
end
keyboardTextbox:ReleaseFocus(submit)
if keepKeyboardOpen then
keyboardTextbox:CaptureFocus()
end
end
end)
rawset(newKeyboard, "GetCurrentOptions", function(self)
return keyboardOptions
end)
rawset(newKeyboard, "BackspaceAtCursor", function(self)
if textfieldCursorPosition >= 1 then
local bufferText = getBufferText()
local newBufferText = string.sub(bufferText, 1, textfieldCursorPosition - 1) .. string.sub(bufferText, textfieldCursorPosition + 1, #bufferText)
local newCursorPosition = textfieldCursorPosition - 1
setBufferText(newBufferText)
SetTextFieldCursorPosition(newCursorPosition)
end
end)
rawset(newKeyboard, "SubmitCharacter", function(self, character, isAnAlphaKey)
setBufferText(getBufferText() .. character)
SetTextFieldCursorPosition(textfieldCursorPosition + #character)
if isAnAlphaKey and self:GetShift() then
self:SetShift(false)
end
end)
do -- Parse input definition
for _, keyboardKeyset in pairs(keyboardLayoutDefinitions) do
local keys = {}
local keyboardSizeConstrainer = Util:Create'Frame'
{
Name = 'KeyboardSizeConstrainer';
Size = UDim2.new(1, 0, 1, -20);
Position = UDim2.new(0, 0, 0, 20);
BackgroundTransparency = 1;
Parent = keyboardContainer;
};
local maxWidth = 0
local maxHeight = 0
local y = 0
for rowNum, rowData in pairs(keyboardKeyset) do
local x = 0
local width = 1
local height = 1
local width2, height2, x2, y2;
for columnNum, columnData in pairs(rowData) do
if type(columnData) == 'table' then
if columnData['w'] then width = columnData['w'] end
if columnData['h'] then height = columnData['h'] end
if columnData['x'] then x = x + columnData['x'] end
if columnData['y'] then y = y + columnData['y'] end
if columnData['x2'] then x2 = columnData['x2'] end
if columnData['y2'] then y2 = columnData['y2'] end
if columnData['w2'] then width2 = columnData['w2'] end
if columnData['h2'] then height2 = columnData['h2'] end
elseif type(columnData) == 'string' then
if columnData == "" then
columnData = " "
end
-- put key
local key = CreateKeyboardKey(
newKeyboard,
{x = x, y = y, width = width, height = height, x2 = x2, y2 = y2, width2 = width2, height2 = height2},
tokenizeString(columnData, '\n'))
table.insert(keys, key)
x = x + width
maxWidth = math.max(maxWidth, x)
maxHeight = math.max(maxHeight, y + height)
-- reset for the next key
width = 1
height = 1
width2, height2, x2, y2 = nil, nil, nil, nil
end
end
y = y + 1
end
-- Fix the positions and sizes to fit in our KeyboardContainer
for _, element in pairs(keys) do
element.Position = UDim2.new(element.Position.X.Scale / maxWidth, 0, element.Position.Y.Scale / maxHeight, 0)
element.Size = UDim2.new(element.Size.X.Scale / maxWidth, 0, element.Size.Y.Scale / maxHeight, 0)
element.Parent = keyboardSizeConstrainer
end
keyboardSizeConstrainer.SizeConstraint = Enum.SizeConstraint.RelativeXX
keyboardSizeConstrainer.Size = UDim2.new(1, 0, -maxHeight / maxWidth, 0)
keyboardSizeConstrainer.Position = UDim2.new(0, 0, 1, 0)
keyboardSizeConstrainer.Visible = false
table.insert(keysets, {keys = keys, container = keyboardSizeConstrainer})
end
newKeyboard:SetCurrentKeyset(1)
end
textEntryField.MouseButton1Click:connect(function()
SetTextFieldCursorPosition(calculateTextCursorPosition(panel.lookAtPixel.X, panel.lookAtPixel.Y))
end)
closeButton.MouseButton1Click:connect(function()
newKeyboard:Close(false)
closedEvent:Fire()
end)
voiceDoneButton.MouseButton1Click:connect(function()
newKeyboard:SetVoiceMode(false)
end)
return newKeyboard
end
local Keyboard = nil;
local function GetKeyboard()
if Keyboard == nil then
Keyboard = ConstructKeyboardUI({MINIMAL_KEYBOARD_LAYOUT, MINIMAL_KEYBOARD_LAYOUT_SYMBOLS})
end
return Keyboard
end
local VirtualKeyboardClass = {}
function VirtualKeyboardClass:CreateVirtualKeyboardOptions(textbox)
local keyboardOptions = {}
keyboardOptions.TextBox = textbox
return keyboardOptions
end
local VirtualKeyboardPlatform = false
do
-- iOS, Android and Xbox already have platform specific keyboards
local platform = UserInputService:GetPlatform()
VirtualKeyboardPlatform = platform == Enum.Platform.Windows or
platform == Enum.Platform.OSX
end
function VirtualKeyboardClass:ShowVirtualKeyboard(virtualKeyboardOptions)
if VirtualKeyboardPlatform and UserInputService.VREnabled then
GetKeyboard():Open(virtualKeyboardOptions)
end
end
function VirtualKeyboardClass:CloseVirtualKeyboard()
if VirtualKeyboardPlatform and UserInputService.VREnabled then
local currentKeyboard = GetKeyboard()
currentKeyboard:Close(false)
end
end
VirtualKeyboardClass.ClosedEvent = GetKeyboard().ClosedEvent
if VirtualKeyboardPlatform and useVRKeyboard then
UserInputService.TextBoxFocused:connect(function(textbox)
VirtualKeyboardClass:ShowVirtualKeyboard(VirtualKeyboardClass:CreateVirtualKeyboardOptions(textbox))
end)
UserInputService.TextBoxFocusReleased:connect(function(textbox)
VirtualKeyboardClass:CloseVirtualKeyboard()
end)
end
return VirtualKeyboardClass
--Panel3D: 3D GUI panels for VR
--written by 0xBAADF00D
--revised/refactored 5/11/16
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local GuiService = game:GetService("GuiService")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local PlayersService = game.Players
--Util
local Util = {}
do
function Util.Create(instanceType)
return function(data)
local obj = Instance.new(instanceType)
for k, v in pairs(data) do
if type(k) == 'number' then
v.Parent = obj
else
obj[k] = v
end
end
return obj
end
end
-- RayPlaneIntersection (shortened)
-- http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm
function Util.RayPlaneIntersection(ray, planeNormal, pointOnPlane)
planeNormal = planeNormal.unit
ray = ray.Unit
local Vd = planeNormal:Dot(ray.Direction)
if Vd == 0 then -- parallel, no intersection
return nil
end
local V0 = planeNormal:Dot(pointOnPlane - ray.Origin)
local t = V0 / Vd
if t < 0 then --plane is behind ray origin, and thus there is no intersection
return nil
end
return ray.Origin + ray.Direction * t
end
end
--End of Util
--Panel3D State variables
local renderStepName = "Panel3DRenderStep-" .. game:GetService("HttpService"):GenerateGUID()
local defaultPixelsPerStud = 64
local pointUpCF = CFrame.Angles(math.rad(-90), math.rad(180), 0)
local zeroVector = Vector3.new(0, 0, 0)
local zeroVector2 = Vector2.new(0, 0)
local turnAroundCF = CFrame.Angles(0, math.rad(180), 0)
local fullyOpaqueAtPixelsFromEdge = 10
local fullyTransparentAtPixelsFromEdge = 80
local partThickness = 0.2
local cursorHidden = false
local cursorHideTime = 2.5
local currentModal = nil
local lastModal = nil
local currentMaxDist = math.huge
local currentClosest = nil
local lastClosest = nil
local currentHeadScale = 1
local panels = {}
local floorRotation = CFrame.new()
local cursor = Util.Create "ImageLabel" {
Image = "rbxasset://textures/Cursors/Gamepad/Pointer.png",
Size = UDim2.new(0, 8, 0, 8),
BackgroundTransparency = 1,
ZIndex = 10
}
--End of Panel3D State variables
--Panel3D Declaration and enumerations
local Panel3D = {}
Panel3D.Type = {
None = 0,
Floor = 1,
Fixed = 2,
HorizontalFollow = 3,
FixedToHead = 4
}
Panel3D.OnPanelClosed = Util.Create 'BindableEvent' {
Name = 'OnPanelClosed'
}
function Panel3D.GetHeadLookXZ(withTranslation)
local userHeadCF = UserInputService:GetUserCFrame(Enum.UserCFrame.Head)
local headLook = userHeadCF.lookVector
local headYaw = math.atan2(-headLook.Z, headLook.X) + math.rad(90)
local cf = CFrame.Angles(0, headYaw, 0)
if withTranslation then
cf = cf + userHeadCF.p
end
return cf
end
function Panel3D.FindContainerOf(element)
for i, v in pairs(panels) do
if v.gui and v.gui:IsAncestorOf(element) then
return v
end
end
return nil
end
function Panel3D.SetModalPanel(panel)
if currentModal == panel then
return
end
if currentModal then
currentModal:OnModalChanged(false)
end
if panel then
panel:OnModalChanged(true)
end
lastModal = currentModal
currentModal = panel
end
--End of Panel3D Declaration and enumerations
--Cursor autohiding methods
local cursorHidden = false
local hasTool = false
local lastMouseActivity = tick()
local lastMouseBehavior = Enum.MouseBehavior.Default
local function OnCharacterAdded(character)
hasTool = false
for i, v in ipairs(character:GetChildren()) do
if v:IsA("Tool") then
hasTool = true
end
end
character.ChildAdded:connect(function(child)
if child:IsA("Tool") then
hasTool = true
lastMouseActivity = tick() --kick the mouse when a tool is equipped
end
end)
character.ChildRemoved:connect(function(child)
if child:IsA("Tool") then
hasTool = false
end
end)
end
spawn(function()
while not PlayersService.LocalPlayer do wait() end
PlayersService.LocalPlayer.CharacterAdded:connect(OnCharacterAdded)
if PlayersService.LocalPlayer.Character then OnCharacterAdded(PlayersService.LocalPlayer.Character) end
end)
local function autoHideCursor(hide)
if not UserInputService.VREnabled then
cursorHidden = false
return
end
if hide then
--don't hide if there's a tool in the character
local character = PlayersService.LocalPlayer.Character
if character and hasTool then
return
end
cursorHidden = true
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide
else
cursorHidden = false
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None
end
end
local function isCursorVisible()
--if ForceShow, the cursor is definitely visible at all times
if UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.ForceShow then
return true
end
--if ForceHide, the cursor is definitely NOT visible
if UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.ForceHide then
return false
end
--Otherwise, we need to check if the developer set MouseIconEnabled=false
if UserInputService.MouseIconEnabled and UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.None then
return true
end
return false
end
--End of cursor autohiding methods
--Panel class implementation
local Panel = {}
Panel.mt = { __index = Panel }
function Panel.new(name)
local instance = {
name = name,
part = nil,
gui = nil,
width = 1,
height = 1,
isVisible = false,
isEnabled = false,
panelType = Panel3D.Type.None,
pixelScale = 1,
showCursor = true,
canFade = true,
shouldFindLookAtGuiElement = false,
ignoreModal = false,
linkedTo = nil,
subpanels = {},
transparency = 1,
forceShowUntilLookedAt = false,
isLookedAt = false,
isOffscreen = true,
lookAtPixel = Vector2.new(-1, -1),
lookAtDistance = math.huge,
lookAtGuiElement = nil,
isClosest = true
}
if panels[name] then
error("A panel by the name of " .. name .. " already exists.")
end
panels[name] = instance
return setmetatable(instance, Panel.mt)
end
--Panel accessor methods
function Panel:GetPart()
if not self.part then
self.part = Util.Create "Part" {
Name = self.name,
Parent = nil,
Transparency = 1,
CanCollide = false,
Anchored = true,
Archivable = false,
Size = Vector3.new(1, 1, partThickness)
}
end
return self.part
end
function Panel:GetGUI()
if not self.gui then
local part = self:GetPart()
self.gui = Util.Create "SurfaceGui" {
Parent = CoreGui,
Adornee = part,
Active = true,
ToolPunchThroughDistance = 1000,
CanvasSize = Vector2.new(0, 0),
Enabled = self.isEnabled,
AlwaysOnTop = true
}
end
return self.gui
end
function Panel:FindHoveredGuiElement(elements)
local x, y = self.lookAtPixel.X, self.lookAtPixel.Y
for i, v in pairs(elements) do
local minPt = v.AbsolutePosition
local maxPt = v.AbsolutePosition + v.AbsoluteSize
if minPt.X <= x and maxPt.X >= x and
minPt.Y <= y and maxPt.Y >= y then
return v, i
end
end
end
--End of panel accessor methods
--Panel update methods
function Panel:SetPartCFrame(cframe)
self:GetPart().CFrame = cframe * CFrame.new(0, 0, -0.5 * partThickness)
end
function Panel:SetEnabled(enabled)
if self.isEnabled == enabled then
return
end
self.isEnabled = enabled
if enabled then
self:GetPart().Parent = workspace.CurrentCamera --todo: Perhaps this can change soon.
self:GetGUI().Enabled = true
for i, v in pairs(self.subpanels) do
v:GetPart().Parent = workspace.CurrentCamera
v:GetGUI().Enabled = true
end
else
self:GetPart().Parent = nil
self:GetGUI().Enabled = false
for i, v in pairs(self.subpanels) do
v:GetPart().Parent = nil
v:GetGUI().Enabled = false
end
end
self:OnEnabled(enabled)
end
function Panel:EvaluatePositioning(cameraCF, cameraRenderCF, userHeadCF)
if self.panelType == Panel3D.Type.Floor then
--Floor panels simply... go on the floor.
--Panel will be in camera's local space (which is assumed to be horizontal),
--and floorRotation is derived from the user's head rotation (yaw only)
local floorCF = cameraCF * CFrame.new(0, -headHeightFromFloor * currentHeadScale, 0) * floorRotation
self:SetPartCFrame(floorCF * pointUpCF)
elseif self.panelType == Panel3D.Type.Fixed then
--Places the panel in the camera's local space, but doesn't follow the user's head.
--Useful if you know what you're doing. localCF can be updated in PreUpdate for animation.
local cf = self.localCF - self.localCF.p
cf = cf + (self.localCF.p * currentHeadScale)
self:SetPartCFrame(cameraCF * cf)
elseif self.panelType == Panel3D.Type.HorizontalFollow then
local headLook = userHeadCF.lookVector
local headYaw = math.atan2(-headLook.Z, headLook.X) + math.rad(90)
local headForwardCF = CFrame.Angles(0, headYaw, 0) + userHeadCF.p
local localCF = (headForwardCF * self.angleFromForward) * --Rotate about Y (left-right)
self.angleFromHorizon * --Rotate about X (up-down)
(currentHeadScale * self.distance) * --Move into scene
turnAroundCF --Turn around to face character
self:SetPartCFrame(cameraCF * localCF)
elseif self.panelType == Panel3D.Type.FixedToHead then
--Places the panel in the user's head local space. localCF can be updated in PreUpdate for animation.
local cf = self.localCF - self.localCF.p
cf = cf + (self.localCF.p * currentHeadScale)
self:SetPartCFrame(cameraRenderCF * cf)
end
end
function Panel:EvaluateGaze(cameraCF, cameraRenderCF, userHeadCF, lookRay)
--reset distance data
self.isClosest = false
self.lookAtPixel = zeroVector2
self.lookAtDistance = math.huge
--Evaluate the lookRay versus this panel
local planeCF = self:GetPart().CFrame
local planeNormal = planeCF.lookVector
local pointOnPlane = planeCF.p + (planeNormal * partThickness * 0.5) --Move the point out by half the thickness of the part
local worldIntersectPoint = Util.RayPlaneIntersection(lookRay, planeNormal, pointOnPlane)
if worldIntersectPoint then
self.isOffscreen = false
--transform worldIntersectPoint to gui space
local gui = self:GetGUI()
local guiWidth, guiHeight = gui.AbsoluteSize.X, gui.AbsoluteSize.Y
local localIntersectPoint = (planeCF:pointToObjectSpace(worldIntersectPoint) / currentHeadScale) * Vector3.new(-1, 1, 1) + Vector3.new(self.width / 2, -self.height / 2, 0)
self.lookAtPixel = Vector2.new((localIntersectPoint.X / self.width) * gui.AbsoluteSize.X, (localIntersectPoint.Y / self.height) * -gui.AbsoluteSize.Y)
--fire mouse enter/leave events if necessary
local guiX, guiY = self.lookAtPixel.X, self.lookAtPixel.Y
if guiX >= 0 and guiX <= guiWidth and
guiY >= 0 and guiY <= guiHeight then
if not self.isLookedAt then
self.isLookedAt = true
self:OnMouseEnter(guiX, guiY)
if self.forceShowUntilLookedAt then
self.forceShowUntilLookedAt = false
end
end
else
if self.isLookedAt then
self.isLookedAt = false
self:OnMouseLeave(guiX, guiY)
end
end
--evaluate distance
self.lookAtDistance = (worldIntersectPoint - cameraRenderCF.p).magnitude
if self.isLookedAt and self.lookAtDistance < currentMaxDist and self.showCursor then
currentMaxDist = self.lookAtDistance
currentClosest = self
end
else
self.isOffscreen = true
--Not looking at the plane at all, so fire off mouseleave if necessary.
if self.isLookedAt then
self.isLookedAt = false
self:OnMouseLeave(self.lookAtPixel.X, self.lookAtPixel.Y)
end
end
end
function Panel:EvaluateTransparency()
--Early exit if force shown
if self.forceShowUntilLookedAt then
self.transparency = 0
return
end
--Early exit if we're looking at the panel (no transparency!)
if self.isLookedAt then
self.transparency = 0
return
end
--Similarly, exit if we can't possibly see the panel.
if self.isOffscreen then
self.transparency = 1
return
end
--Otherwise, we'll want to calculate the transparency.
self.transparency = self:CalculateTransparency()
end
function Panel:Update(cameraCF, cameraRenderCF, userHeadCF, lookRay)
if self.forceShowUntilLookedAt and not self.part then
self:GetPart()
self:GetGUI()
end
if not self.part then
return
end
local isModal = (currentModal == self)
if not isModal and self.linkedTo and self.linkedTo == currentModal then
isModal = true
end
if currentModal and not isModal then
self:SetEnabled(false)
return
end
self:PreUpdate(cameraCF, cameraRenderCF, userHeadCF, lookRay)
if self.isVisible then
self:EvaluatePositioning(cameraCF, cameraRenderCF, userHeadCF)
self:EvaluateGaze(cameraCF, cameraRenderCF, userHeadCF, lookRay)
self:EvaluateTransparency(cameraCF, cameraRenderCF)
end
end
--End of Panel update methods
--Panel virtual methods
function Panel:PreUpdate() --virtual: handle positioning here
end
function Panel:OnUpdate() --virtual: handle transparency here
end
function Panel:OnMouseEnter(x, y) --virtual
end
function Panel:OnMouseLeave(x, y) --virtual
end
function Panel:OnEnabled(enabled) --virtual
end
function Panel:OnModalChanged(isModal) --virtual
end
function Panel:OnVisibilityChanged(visible) --virtual
end
function Panel:CalculateTransparency() --virtual
if not self.canFade then
return 0
end
local guiWidth, guiHeight = self.gui.AbsoluteSize.X, self.gui.AbsoluteSize.Y
local lookX, lookY = self.lookAtPixel.X, self.lookAtPixel.Y
--Determine the distance from the edge;
--if x is negative it's on the left side, meaning the distance is just absolute value
--if x is positive it's on the right side, meaning the distance is x minus the width
local xEdgeDist = lookX < 0 and -lookX or (lookX - guiWidth)
local yEdgeDist = lookY < 0 and -lookY or (lookY - guiHeight)
if lookX > 0 and lookX < guiWidth then
xEdgeDist = 0
end
if lookY > 0 and lookY < guiHeight then
yEdgeDist = 0
end
local edgeDist = math.sqrt(xEdgeDist ^ 2 + yEdgeDist ^ 2)
--since transparency is 0-1, we know how many pixels will give us 0 and how many will give us 1.
local offset = fullyOpaqueAtPixelsFromEdge
local interval = fullyTransparentAtPixelsFromEdge
--then we just clamp between 0 and 1.
return math.max(0, math.min(1, (edgeDist - offset) / interval))
end
--End of Panel virtual methods
--Panel configuration methods
function Panel:ResizeStuds(width, height, pixelsPerStud)
pixelsPerStud = pixelsPerStud or defaultPixelsPerStud
self.width = width
self.height = height
self.pixelScale = pixelsPerStud / defaultPixelsPerStud
local part = self:GetPart()
part.Size = Vector3.new(self.width * currentHeadScale, self.height * currentHeadScale, partThickness)
local gui = self:GetGUI()
gui.CanvasSize = Vector2.new(pixelsPerStud * self.width, pixelsPerStud * self.height)
end
function Panel:ResizePixels(width, height, pixelsPerStud)
pixelsPerStud = pixelsPerStud or defaultPixelsPerStud
local widthInStuds = width / pixelsPerStud
local heightInStuds = height / pixelsPerStud
self:ResizeStuds(widthInStuds, heightInStuds, pixelsPerStud)
end
function Panel:OnHeadScaleChanged(newHeadScale)
local pixelsPerStud = self.pixelScale * defaultPixelsPerStud
self:ResizeStuds(self.width, self.height, pixelsPerStud)
end
function Panel:SetType(panelType, config)
self.panelType = panelType
--clear out old type-specific members
self.floorPos = nil
self.localCF = nil
self.angleFromHorizon = nil
self.angleFromForward = nil
self.distance = nil
if not config then
config = {}
end
if panelType == Panel3D.Type.None then
--nothing to do
return
elseif panelType == Panel3D.Type.Floor then
self.floorPos = config.FloorPosition or Vector3.new(0, 0, 0)
elseif panelType == Panel3D.Type.Fixed then
self.localCF = config.CFrame or CFrame.new()
elseif panelType == Panel3D.Type.HorizontalFollow then
self.angleFromHorizon = CFrame.Angles(config.angleFromHorizon or 0, 0, 0)
self.angleFromForward = CFrame.Angles(0, config.angleFromForward or 0, 0)
self.distance = CFrame.new(0, 0, config.distance or 5)
elseif panelType == Panel3D.Type.FixedToHead then
self.localCF = config.CFrame or CFrame.new()
else
error("Invalid Panel type")
end
end
function Panel:SetVisible(visible, modal)
if visible ~= self.isVisible then
self:OnVisibilityChanged(visible)
if not visible then
Panel3D.OnPanelClosed:Fire(self.name)
end
end
self.isVisible = visible
self:SetEnabled(visible)
if visible and modal then
Panel3D.SetModalPanel(self)
end
if not visible and currentModal == self then
if modal and not doNotRestore then
--restore last modal panel
Panel3D.SetModalPanel(lastModal)
else
Panel3D.SetModalPanel(nil)
--if the coder explicitly wanted to hide this modal panel,
--it follows that they don't want it to be restored when the next
--modal panel is hidden.
if lastModal == self then
lastModal = nil
end
end
end
if not visible and self.forceShowUntilLookedAt then
self.forceShowUntilLookedAt = false
end
end
function Panel:LinkTo(panelName)
if type(panelName) == "string" then
self.linkedTo = Panel3D.Get(panelName)
else
self.linkedTo = panelName
end
end
function Panel:ForceShowUntilLookedAt(makeModal)
--ensure the part exists
self:GetPart()
self:GetGUI()
self:SetVisible(true, makeModal)
self.forceShowUntilLookedAt = true
end
function Panel:SetCanFade(canFade)
self.canFade = canFade
end
--Child class, Subpanel
local Subpanel = {}
local Subpanel_mt = {}
function Subpanel.new(guiElement)
local instance = {
guiElement = guiElement
}
return setmetatable(instance, Subpanel_mt)
end
function Subpanel:GetPart()
warn("Subpanel:GetPart() not yet implemented")
return nil
end
function Subpanel:GetGUI()
warn("Subpanel:GetGUI() not yet implemented")
return nil
end
function Panel:AddSubpanel(guiElement)
local subpanel = Subpanel.new(guiElement)
self.subpanels[guiElement] = subpanel
end
function Panel:RemoveSubpanel(guiElement)
self.subpanels[guiElement] = nil
end
--End of Panel configuration methods
--End of Panel class implementation
--Panel3D API
function Panel3D.Get(name)
local panel = panels[name]
if not panels[name] then
panels[name] = Panel.new(name)
panel = panels[name]
end
return panel
end
--End of Panel3D API
--Panel3D Setup
local function onRenderStep()
if not UserInputService.VREnabled then
return
end
--reset distance info
currentClosest = nil
currentMaxDist = math.huge
--figure out some useful stuff
local camera = workspace.CurrentCamera
local cameraCF = camera.CFrame
local cameraRenderCF = camera:GetRenderCFrame()
local userHeadCF = UserInputService:GetUserCFrame(Enum.UserCFrame.Head)
local lookRay = Ray.new(cameraRenderCF.p, cameraRenderCF.lookVector)
--allow all panels to run their own update code
for i, v in pairs(panels) do
v:Update(cameraCF, cameraRenderCF, userHeadCF, lookRay)
end
--evaluate linked panels
local processed = {}
for i, v in pairs(panels) do
if not processed[v] and v.linkedTo and v.isVisible and v.linkedTo.isVisible then
processed[v] = true
processed[v.linkedTo] = true
local minTransparency = math.min(v.transparency, v.linkedTo.transparency)
v.transparency = minTransparency
v.linkedTo.transparency = minTransparency
end
end
--run post update because the distance information hasn't been
--finalized until now.
for i, v in pairs(panels) do
--If the part is fully transparent, we don't want to keep it around in the workspace.
if v.part and v.gui then
--check if this panel is the current modal panel
local isModal = (currentModal == v)
--but also check if this panel is linked to the current modal panel
if not isModal and v.linkedTo and v.linkedTo == currentModal then
isModal = true
end
local show = v.isVisible
if not isModal and currentModal then
show = false
end
if v.transparency >= 1 then
show = false
end
if v.forceShowUntilLookedAt then
show = true
end
if not v.canFade and v.isVisible then
show = true
end
v:SetEnabled(show)
end
v:OnUpdate()
end
--place the cursor on the closest panel (for now)
if not currentClosest and lastClosest then
UserInputService.MouseBehavior = lastMouseBehavior
elseif currentClosest and not lastClosest then
lastMouseBehavior = UserInputService.MouseBehavior
end
if currentClosest then
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide
cursor.Parent = currentClosest:GetGUI()
local x, y = currentClosest.lookAtPixel.X, currentClosest.lookAtPixel.Y
cursor.Size = UDim2.new(0, 8 * currentClosest.pixelScale, 0, 8 * currentClosest.pixelScale)
cursor.Position = UDim2.new(0, x - cursor.AbsoluteSize.x * 0.5, 0, y - cursor.AbsoluteSize.y * 0.5)
else
cursor.Parent = nil
end
lastClosest = currentClosest
end
RunService:BindToRenderStep(renderStepName, Enum.RenderPriority.Last.Value, onRenderStep)
--Implement cursor autohide functionality
UserInputService.InputChanged:connect(function(inputObj, processed)
if inputObj.UserInputType == Enum.UserInputType.MouseMovement then
lastMouseActivity = tick()
autoHideCursor(false)
end
end)
local function onHeartbeat()
if isCursorVisible() then
cursorHidden = false
end
if lastMouseActivity + cursorHideTime < tick() and not GuiService.MenuIsOpen and not cursorHidden then
autoHideCursor(true)
end
end
RunService.Heartbeat:connect(onHeartbeat)
--Handle camera changes
local cameraChangedConnection = nil
local function onCameraChanged(prop)
if prop == "HeadScale" then
pcall(function()
currentHeadScale = workspace.CurrentCamera.HeadScale
end)
for i, v in pairs(panels) do
v:OnHeadScaleChanged(currentHeadScale)
end
end
end
local function onWorkspaceChanged(prop)
if prop == "CurrentCamera" then
onCameraChanged("HeadScale")
if cameraChangedConnection then
cameraChangedConnection:disconnect()
end
cameraChangedConnection = workspace.CurrentCamera.Changed:connect(onCameraChanged)
end
end
if workspace.CurrentCamera then
onWorkspaceChanged("CurrentCamera")
end
workspace.Changed:connect(onWorkspaceChanged)
return Panel3D
--Panel3D: 3D GUI panels for VR
--written by 0xBAADF00D
local PIXELS_PER_STUD = 64
local SETTINGS_DISTANCE = 3.5
local CURSOR_HIDE_TIME = 2
local CURSOR_FADE_TIME = 0.125
local CoreGui = game:GetService('CoreGui')
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Panel3D = {}
Panel3D.Panels = {
Lower = 1,
Hamburger = 2,
Settings = 3,
Keyboard = 4,
Chat = 5
}
Panel3D.Orientation = {
Horizontal = 1,
Fixed = 2
}
Panel3D.Visibility = {
BelowAngleThreshold = 1,
Modal = 2,
Normal = 3
}
local panelDefaultVectors = {
[Panel3D.Panels.Lower] = CFrame.Angles(math.rad(-45), 0, 0):vectorToWorldSpace(Vector3.new(0, 0, -5)),
[Panel3D.Panels.Hamburger] = CFrame.Angles(math.rad(-55), 0, 0):vectorToWorldSpace(Vector3.new(0, 0, -5)),
[Panel3D.Panels.Settings] = Vector3.new(0, 0, -SETTINGS_DISTANCE),
[Panel3D.Panels.Keyboard] = CFrame.Angles(math.rad(-22.5), 0, 0):vectorToWorldSpace(Vector3.new(0, 0, -5)),
[Panel3D.Panels.Chat] = CFrame.Angles(math.rad(5), 0, 0):vectorToWorldSpace(Vector3.new(0, 0, -5))
}
local DEFAULT_PANEL_LOCK_THRESHOLD = math.rad(-25)
local panelTransparencyBias = { --tuned values; raise the opacity value to this power
[Panel3D.Panels.Lower] = 6.5,
[Panel3D.Panels.Hamburger] = 8,
[Panel3D.Panels.Settings] = 0,
[Panel3D.Panels.Keyboard] = 1.5,
[Panel3D.Panels.Chat] = 1.5
}
local panels = {}
local headScale = 1
local renderStepName = "Panel3D"
local cursor = Instance.new("ImageLabel")
cursor.Image = "rbxasset://textures/Cursors/Gamepad/Pointer.png"
cursor.Size = UDim2.new(0, 8, 0, 8)
cursor.BackgroundTransparency = 1
cursor.ZIndex = 10
local resetOrientationFlag = false
local currentModalPanel = nil
local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")
local cursorHidden = false
local hasTool = false
local lastMouseMove = tick()
local function OnCharacterAdded(character)
hasTool = false
for i, v in ipairs(character:GetChildren()) do
if v:IsA("Tool") then
hasTool = true
end
end
character.ChildAdded:connect(function(child)
if child:IsA("Tool") then
hasTool = true
lastMouseMove = tick() --kick the mouse when a tool is equipped
end
end)
character.ChildRemoved:connect(function(child)
if child:IsA("Tool") then
hasTool = false
end
end)
end
spawn(function()
while not game.Players.LocalPlayer do wait() end
game.Players.LocalPlayer.CharacterAdded:connect(OnCharacterAdded)
if game.Players.LocalPlayer.Character then OnCharacterAdded(game.Players.LocalPlayer.Character) end
end)
local function autoHideCursor(hide)
if not UserInputService.VREnabled then
cursorHidden = false
return
end
if hide then
--don't hide if there's a tool in the character
local character = game.Players.LocalPlayer.Character
if character and hasTool then
return
end
cursorHidden = true
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide
else
cursorHidden = false
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None
end
end
local function isCursorVisible()
--if ForceShow, the cursor is definitely visible at all times
if UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.ForceShow then
return true
end
--if ForceHide, the cursor is definitely NOT visible
if UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.ForceHide then
return false
end
--Otherwise, we need to check if the developer set MouseIconEnabled=false
if UserInputService.MouseIconEnabled and UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.None then
return true
end
return false
end
UserInputService.InputChanged:connect(function(inputObj, processed)
if inputObj.UserInputType == Enum.UserInputType.MouseMovement then
lastMouseMove = tick()
autoHideCursor(false)
end
end)
game:GetService("RunService").Heartbeat:connect(function()
if isCursorVisible() then
cursorHidden = false
end
if lastMouseMove + CURSOR_HIDE_TIME < tick() and not GuiService.MenuIsOpen and not cursorHidden then
autoHideCursor(true)
end
end)
local function createPanel()
local panelPart = Instance.new("Part")
panelPart.Transparency = 1
panelPart.CanCollide = false
panelPart.Anchored = true
panelPart.Archivable = false
panelPart.Size = Vector3.new(1.5, 1.5, 1)
panelPart.Parent = workspace.CurrentCamera
local panelGUI = Instance.new("SurfaceGui", CoreGui)
panelGUI.Name = "GUI"
panelGUI.Adornee = panelPart
panelGUI.ToolPunchThroughDistance = 1000
panelGUI.Active = true
pcall(function() --todo: remove this when api is live
panelGUI.AlwaysOnTop = true
end)
return panelPart, panelGUI
end
function Panel3D.Get(panelId)
local panel = panels[panelId]
if not panel then
local panelName
for name, value in pairs(Panel3D.Panels) do
if value == panelId then
panelName = name
break
end
end
if not panelName then
error("Tried to request an invalid 3D panel")
end
local part, gui = createPanel()
panel = {}
panel.part = part
panel.part.Name = "GUI"--("%sPanel3D"):format(panelName)
panel.gui = gui
panel.gui.Name = ("%sPanelGUI"):format(panelName)
panel.vector = panelDefaultVectors[panelId]
panel.horizontalVector = Vector3.new(panel.vector.x, 0, panel.vector.z).unit
panel.pitchAngle = math.asin(panel.vector.unit.y)
panel.verticalRange = math.rad(5)
panel.horizontalRange = math.rad(5)
panel.transparencyCallbacks = {}
panel.OnMouseEnter = nil
panel.OnMouseLeave = nil
panel.pixelScale = 1
panel.visible = true
panel.visibilityBehavior = Panel3D.Visibility.BelowAngleThreshold
panel.orientationMode = Panel3D.Orientation.Horizontal
panel.orientation = nil
panel.cursorEnabled = true
panel.width = 1
panel.height = 1
function panel:AddTransparencyCallback(callback)
table.insert(panel.transparencyCallbacks, callback)
end
function panel:Resize(width, height, pixelsPerStud)
panel.width = width
panel.height = height
pixelsPerStud = pixelsPerStud or PIXELS_PER_STUD
panel.pixelScale = pixelsPerStud / PIXELS_PER_STUD
panel.part.Size = Vector3.new(panel.width * headScale, panel.height * headScale, 1)
panel.gui.CanvasSize = Vector2.new(pixelsPerStud * panel.width, pixelsPerStud * panel.height)
local distance = panel.vector.magnitude
panel.verticalRange = math.atan(panel.part.Size.Y / (2 * distance)) * 2
panel.horizontalRange = math.atan(panel.part.Size.X / (2 * distance)) * 2
end
function panel:ResizePixels(width, height)
local widthStuds = width / PIXELS_PER_STUD
local heightStuds = height / PIXELS_PER_STUD
panel:Resize(widthStuds, heightStuds)
end
function panel:SetModal()
panel.visible = false
panel.visibilityBehavior = Panel3D.Visibility.Modal
if panel.visible then
currentModalPanel = panel
if panel.orientationMode == Panel3D.Orientation.Fixed then
local userHeadCFrame = UserInputService:GetUserCFrame(Enum.UserCFrame.Head)
panel.orientation = userHeadCFrame * CFrame.new(0, 0, -panel.vector.magnitude) * CFrame.Angles(0, math.pi, 0)
end
end
end
function panel:SetVisible(visible)
panel.visible = visible
if visible and panel.visibilityBehavior == Panel3D.Visibility.Modal then
currentModalPanel = panel
if panel.orientationMode == Panel3D.Orientation.Fixed then
local userHeadCFrame = UserInputService:GetUserCFrame(Enum.UserCFrame.Head)
panel.orientation = userHeadCFrame * CFrame.new(0, 0, -panel.vector.magnitude) * CFrame.Angles(0, math.pi, 0)
end
else
if currentModalPanel == panel then
currentModalPanel = nil
end
end
end
panels[panelId] = panel
end
return panel
end
function Panel3D.ResetOrientation()
resetOrientationFlag = true
end
function Panel3D.GetGUI(panel)
local panelGUI = panelGUIs[panel]
if not panelGUI then
local part = Panel3D.GetPart(panel)
panelGUI = panelGUIs[panel]
end
return panelGUI
end
local zeroVector = Vector3.new(0, 0, 0)
local headXZ = CFrame.new()
local headOffset = Vector3.new()
local currentHoverPanel = nil
local savedMouseBehavior = Enum.MouseBehavior.Default
function Panel3D.OnRenderStep()
if not UserInputService.VREnabled then
return
end
local cameraCFrame = workspace.CurrentCamera.CFrame
local cameraRenderCFrame = workspace.CurrentCamera:GetRenderCFrame()
local userHeadCFrame = UserInputService:GetUserCFrame(Enum.UserCFrame.Head)
local userHeadLook = userHeadCFrame.lookVector
local userHeadHorizontalVector = Vector3.new(userHeadLook.X, 0, userHeadLook.Z).unit
local userHeadPitch = math.asin(userHeadLook.Y)
local panelLockThreshold = DEFAULT_PANEL_LOCK_THRESHOLD
for panelId, panel in pairs(panels) do
if panel.pitchLockThreshold and panel.visible then
panelLockThreshold = math.max(panelLockThreshold, panel.pitchLockThreshold)
end
end
local isAboveThreshold = false
if userHeadPitch > panelLockThreshold or resetOrientationFlag then
isAboveThreshold = true
headXZ = CFrame.new(zeroVector, userHeadHorizontalVector)
headOffset = userHeadCFrame.p
resetOrientationFlag = false
end
local panelsOrigin = cameraCFrame * CFrame.new(headOffset) * headXZ
for panelId, panel in pairs(panels) do
if panel.visibilityBehavior == Panel3D.Visibility.BelowAngleThreshold then
panel.visible = not (isAboveThreshold or currentModalPanel)
end
if not panel.visible then
panel.part.Parent = nil
panel.gui.Adornee = nil
else
panel.part.Parent = workspace.CurrentCamera --TODO: move to new 3D gui space
panel.gui.Adornee = panel.part
local panelCFrame;
if panel.orientationMode == Panel3D.Orientation.Fixed and panel.orientation then
local pos = panel.orientation.p * headScale
panel.part.CFrame = workspace.CurrentCamera.CFrame * ((panel.orientation - panel.orientation.p) + pos)
panelCFrame = panel.part.CFrame
else
local panelPosition = panelsOrigin:pointToWorldSpace(panel.vector * headScale)
panelCFrame = CFrame.new(panelPosition, panelsOrigin.p)
panel.part.CFrame = panelCFrame
end
local toPanel = (panelCFrame.p - cameraRenderCFrame.p).unit
local transparency = panel.visible and 1 - (math.max(0, cameraRenderCFrame.lookVector:Dot(toPanel)) ^ panelTransparencyBias[panelId]) or 1
for _, callback in pairs(panel.transparencyCallbacks) do
callback(transparency)
end
end
end
--Render a cursor overlaid onto the panels
local cframe = cameraRenderCFrame
local ray = Ray.new(cframe.p, cframe.lookVector * 999)
local ignoreList = { game.Players.LocalPlayer.Character }
local part, endpoint = workspace:FindPartOnRayWithIgnoreList(ray, ignoreList)
local hitPanel = nil
local hitPanelId = nil
for panelId, panel in pairs(panels) do
if part == panel.part then
hitPanel = panel
hitPanelId = panelId
end
end
if hitPanel then
if not currentHoverPanel then
savedMouseBehavior = UserInputService.MouseBehavior
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
else
if currentHoverPanel ~= hitPanel then
if currentHoverPanel.OnMouseLeave then
currentHoverPanel:OnMouseLeave()
end
end
end
if hitPanel.OnMouseEnter and currentHoverPanel ~= hitPanel then
hitPanel:OnMouseEnter()
end
currentHoverPanel = hitPanel
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide
if hitPanel.cursorEnabled then
cursor.Parent = hitPanel.gui
else
cursor.Parent = nil
end
local localEndpoint = part:GetRenderCFrame():pointToObjectSpace(endpoint)
local x = (localEndpoint.X / part.Size.X) + 0.5
local y = (localEndpoint.Y / part.Size.Y) + 0.5
x = 1 - x
y = 1 - y
cursor.Size = UDim2.new(0, 8 * hitPanel.pixelScale, 0, 8 * hitPanel.pixelScale)
cursor.Position = UDim2.new(x, -cursor.AbsoluteSize.x * 0.5, y, -cursor.AbsoluteSize.y * 0.5)
if hitPanel.visible and not modalPanelIsOpen then
hitPanel.part.Parent = workspace.CurrentCamera
for _, callback in pairs(hitPanel.transparencyCallbacks) do
callback(0)
end
end
else
if currentHoverPanel then
UserInputService.MouseBehavior = savedMouseBehavior
if currentHoverPanel.OnMouseLeave then
currentHoverPanel:OnMouseLeave()
end
end
currentHoverPanel = false
cursor.Parent = nil
end
end
-- RayPlaneIntersection
-- http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm
local function RayPlaneIntersection(ray, planeNormal, pointOnPlane)
planeNormal = planeNormal.unit
ray = ray.Unit
-- compute Pn (dot) Rd = Vd and check if Vd == 0 then we know ray is parallel to plane
local Vd = planeNormal:Dot(ray.Direction)
-- could fuzzy equals this a little bit to account for imprecision or very close angles to zero
if Vd == 0 then -- parallel, no intersection
return nil
end
local V0 = planeNormal:Dot(pointOnPlane - ray.Origin)
local t = V0 / Vd
if t < 0 then --plane is behind ray origin, and thus there is no intersection
return nil
end
return ray.Origin + ray.Direction * t
end
function Panel3D.FindHoveredGuiElement(panel, elements)
local cameraRenderCFrame = workspace.CurrentCamera and workspace.CurrentCamera:GetRenderCFrame()
local panelPart = panel.part
if cameraRenderCFrame and panelPart then
local panelPartSize = panelPart.Size
local panelSurfaceCFrame = panelPart.CFrame + panelPart.CFrame.lookVector * (panelPartSize.Z * 0.5)
local intersectionPt = RayPlaneIntersection(Ray.new(cameraRenderCFrame.p, cameraRenderCFrame.lookVector), panelSurfaceCFrame.lookVector, panelSurfaceCFrame.p)
if intersectionPt then
local localPoint = panelSurfaceCFrame:pointToObjectSpace(intersectionPt) * Vector3.new(-1, 1, 1) + Vector3.new(panelPartSize.X/2, -panelPartSize.Y/2, 0)
local guiPoint = Vector2.new((localPoint.X / panelPartSize.X) * panel.gui.AbsoluteSize.X, (localPoint.Y / panelPartSize.Y) * -panel.gui.AbsoluteSize.Y)
local x = guiPoint.x
local y = guiPoint.y
for _, item in pairs(elements) do
local minPt = item.AbsolutePosition
local maxPt = item.AbsolutePosition + item.AbsoluteSize
if minPt.X <= x and maxPt.X >= x and minPt.Y <= y and maxPt.Y >= y then
return item
end
end
end
end
end
game:GetService("RunService"):BindToRenderStep(renderStepName, Enum.RenderPriority.Last.Value, Panel3D.OnRenderStep)
local function OnCameraChanged(prop)
if prop == "HeadScale" then
pcall(function()
headScale = workspace.CurrentCamera.HeadScale
end)
for i, v in pairs(panels) do
v:Resize(v.width, v.height, v.pixelScale * PIXELS_PER_STUD)
end
end
end
local cameraChangedConn = nil
workspace.Changed:connect(function(prop)
if prop == "CurrentCamera" then
OnCameraChanged("HeadScale")
if cameraChangedConn then
cameraChangedConn:disconnect()
end
cameraChangedConn = workspace.CurrentCamera.Changed:connect(OnCameraChanged)
end
end)
if workspace.CurrentCamera then
OnCameraChanged("HeadScale")
cameraChangedConn = workspace.CurrentCamera.Changed:connect(OnCameraChanged)
end
return Panel3D
Developer Console#
In content/scripts/Modules is an unused developer console.
Developer Console
-- 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 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)
pcall(function() -- Fun window colors for cool people
local Players = game:GetService("Players")
if not Players or not Players.LocalPlayer then
return
end
local FunColors = {
[56449] = {c3(255, 63, 127)}; -- ReeseMcBlox
[6949935] = {c3(255, 63, 127)}; -- NobleDragon
}
local funColor = FunColors[Players.LocalPlayer.userId]
if funColor then
frameColor = funColor[1] or frameColor
textColor = funColor[2] or textColor
end
end)
Style = {
Font = Enum.Font.SourceSans;
FontBold = Enum.Font.SourceSansBold;
HandleHeight = 24; -- How tall the top window handle is, as well as the width of the scroll bar
TabHeight = 28;
GearSize = 24;
BorderSize = 2;
CommandLineHeight = 22;
OptionAreaHeight = 56;
FrameColor = frameColor; -- Applies to pretty much everything, including buttons
FrameTransparency = 0.5;
OptionsFrameColor = optionsFrameColor;
TextColor = textColor;
MessageColors = {
[0] = Color3_new(1, 1, 1); -- Enum.MessageType.MessageOutput
[1] = Color3_new(0.4, 0.5, 1); -- Enum.MessageType.MessageInfo
[2] = Color3_new(1, 0.6, 0.4); -- Enum.MessageType.MessageWarning
[3] = Color3_new(1, 0, 0); -- Enum.MessageType.MessageError
};
ScrollbarFrameColor = frameColor;
ScrollbarBarColor = frameColor;
ScriptButtonHeight = 32;
ScriptButtonColor = Color3_new(0, 1/3, 2/3);
ScriptButtonTransparency = 0.5;
CheckboxSize = 24;
ChartTitleHeight = 20;
ChartGraphHeight = 64;
ChartDataHeight = 24;
ChartHeight = 0; -- This gets added up at end and set at end of block
ChartWidth = 620;
-- (-1) means right to left
-- (1) means left to right
ChartGraphDirection = 1; -- the direction the bars move
GetButtonDownColor = function(normalColor)
local r, g, b = normalColor.r, normalColor.g, normalColor.b
return Color3_new(1 - 0.75 * (1 - r), 1 - 0.75 * (1 - g), 1 - 0.75 * (1 - b))
end;
GetButtonHoverColor = function(normalColor)
local r, g, b = normalColor.r, normalColor.g, normalColor.b
return Color3_new(1 - 0.875 * (1 - r), 1 - 0.875 * (1 - g), 1 - 0.875 * (1 - b))
end;
}
Style.ChartHeight = Style.ChartTitleHeight + Style.ChartGraphHeight + Style.ChartDataHeight + Style.BorderSize
end
-- 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', "ROBLOX Developer Console")
title.Size = UDim2_new(1, -5, 1, 0)
title.Position = UDim2_new(0, 5, 0, 0)
title.FontSize = Enum.FontSize.Size18
title.TextXAlignment = Enum.TextXAlignment.Left
end
local function setCornerButtonImageSize(buttonImage, buttonImageSize)
buttonImage.Size = UDim2_new(buttonImageSize, 0, buttonImageSize, 0)
buttonImage.Position = UDim2_new((1 - buttonImageSize) / 2, 0, (1 - buttonImageSize) / 2, 0)
end
-- This is used for creating the square exit button and the square window resize button
local function createCornerButton(name, x, y, image, buttonImageSize)
-- Corners (x, y):
-- (0, 0) (1, 0)
-- (0, 1) (1, 1)
local button = Primitives.Button(frame, name)
button.Size = UDim2_new(0, Style.HandleHeight, 0, Style.HandleHeight)
button.Position = UDim2_new(x, -x * Style.HandleHeight, y, -y * Style.HandleHeight)
local buttonImage = Primitives.InvisibleImageLabel(button, 'Image', image)
setCornerButtonImageSize(buttonImage, buttonImageSize)
return button, buttonImage
end
do -- Create top right exit button
local exitButton, exitButtonImage = createCornerButton('Exit', 1, 0, 'https://www.roblox.com/asset/?id=261878266', 2/3)
exitButton.AutoButtonColor = false
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://www.roblox.com/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://www.roblox.com/asset/?id=261882463')
--gear.ZIndex = ZINDEX + 1
local animationToggle = devConsole:GenerateOptionButtonAnimationToggle(interiorFrame, optionsButton, gear, tabContainer, optionsClippingFrame, optionsFrame)
local open = false
optionsButton.MouseButton1Click:connect(function()
open = not open
animationToggle(open)
end)
end
-- Console/Log and Stats options
local setShownOptionTypes; -- Toggles what options to show: setOptionType({Log = true})
local textFilter, scriptStatFilter;
local textFilterChanged, scriptStatFilterChanged;
local messageFilter;
local messageFilterChanged, messageTextWrappedChanged;
do -- Options contents/filters
local function createCheckbox(color, callback)
local this = {
Value = true;
}
local frame = Primitives.FolderFrame(nil, 'Checkbox')
this.Frame = frame
frame.Size = UDim2_new(0, Style.CheckboxSize, 0, Style.CheckboxSize)
frame.BackgroundColor3 = color
local padding = 2
local function f(xs, xp, yp) -- quick way to get an opaque border around a transparent center
local ys = 1 - xs
local f = Primitives.Frame(frame, 'Border')
f.BackgroundColor3 = color
f.BackgroundTransparency = 0
f.Size = UDim2_new(xs, ys * padding, ys, xs * padding)
f.Position = UDim2_new(xp, -xp * padding, yp, -yp * padding)
end
f(1, 0, 0)
f(1, 0, 1)
f(0, 0, 0)
f(0, 1, 0)
local button = Primitives.Button(frame, 'Button')
button.Size = UDim2_new(1, -padding * 2, 1, -padding * 2)
button.Position = UDim2_new(0, padding, 0, padding)
local buttonEffectFunction = devConsole:CreateButtonEffectFunction(button)
local check = Primitives.Frame(button, 'Check')
local padding = 4
check.Size = UDim2_new(1, -padding * 2, 1, -padding * 2)
check.Position = UDim2_new(0, padding, 0, padding)
check.BackgroundColor3 = color
check.BackgroundTransparency = 0
devConsole:ConnectButtonHover(button, buttonEffectFunction)
function this.SetValue(this, value)
if value == this.Value then
return
end
this.Value = value
check.Visible = value
this.Value = value
callback(value)
end
button.MouseButton1Click:connect(function()
this:SetValue(not this.Value)
end)
return this
end
local string_find = string.find
local containsString; -- the text typed into the search textBox, nil if equal to ""
function textFilter(text)
return not containsString or string_find(text:lower(), containsString)
end
local filterLookup = {} -- filterLookup[Enum.MessageType.x.Value] = true or false
function messageFilter(message)
return filterLookup[message.Type] and (not containsString or string_find(message.Message:lower(), containsString))
end
-- Events
textFilterChanged = CreateSignal()
scriptStatFilterChanged = CreateSignal()
messageFilterChanged = CreateSignal()
messageTextWrappedChanged = CreateSignal()
local optionTypeContainers = {
--[OptionType] = Frame
--Log = Frame;
--Scripts = Frame;
}
function setShownOptionTypes(shownOptionTypes)
-- Example showOptionTypes:
-- {Log = true}
for optionType, container in pairs(optionTypeContainers) do
container.Visible = shownOptionTypes[optionType] or false
end
end
do -- Log options
local container = Primitives.FolderFrame(optionsFrame, 'Log')
container.Visible = false
optionTypeContainers.Log = container
local label = Primitives.InvisibleTextLabel(container, 'FilterLabel', "Filters")
label.FontSize = 'Size18'
label.TextXAlignment = 'Left'
label.Size = UDim2_new(0, 54, 0, Style.CheckboxSize)
label.Position = UDim2_new(0, 4, 0, 2)
do
local x = label.Size.X.Offset
local messageColors = Style.MessageColors
for i = 0, #messageColors do -- 0, 3 initially
local checkbox = createCheckbox(messageColors[i], function(value)
filterLookup[i] = value
messageFilterChanged:fire()
end)
filterLookup[i] = checkbox.Value
checkbox.Frame.Parent = container
checkbox.Frame.Position = UDim2_new(0, x, 0, 4)
x = x + Style.CheckboxSize + 4
end
do -- Word wrap
x = x + 8
local label = Primitives.InvisibleTextLabel(container, 'WrapLabel', "Word Wrap")
label.FontSize = 'Size18'
label.TextXAlignment = 'Left'
label.Size = UDim2_new(0, 54 + Style.CheckboxSize, 0, Style.CheckboxSize)
label.Position = UDim2_new(0, x + 4, 0, 2)
local checkbox = createCheckbox(Color3.new(0.65, 0.65, 0.65), function(value)
messageTextWrappedChanged:fire(value) -- an event isn't ideal here
end)
checkbox:SetValue(false)
checkbox.Frame.Parent = container
checkbox.Frame.Position = UDim2_new(0, x + label.Size.X.Offset, 0, 4)
end
end
end
do -- Scripts options
local container = Primitives.FolderFrame(optionsFrame, 'Stats')
container.Visible = false
optionTypeContainers.Scripts = container
do
local x = 0
do -- Show inactive
x = x + 4
local label = Primitives.InvisibleTextLabel(container, 'FilterLabel', "Show inactive")
label.FontSize = 'Size18'
label.TextXAlignment = 'Left'
label.Size = UDim2_new(0, label.TextBounds.X + 6, 0, Style.CheckboxSize)
label.Position = UDim2_new(0, x, 0, 2)
x = x + label.Size.X.Offset
local showInactive;
local function getScriptCurrentlyActive(chartStat)
local stats = chartStat.Stats
if stats then
local stat = stats[#stats]
if stat then
return stat[1] > 0.000001 or stat[2] > 0.000001
end
end
return false
end
function scriptStatFilter(chartStat)
return (showInactive or getScriptCurrentlyActive(chartStat))
and (not containsString or string_find(chartStat.Name:lower(), containsString))
end
local checkbox = createCheckbox(Color3_new(1, 1, 1), function(value)
showInactive = value
scriptStatFilterChanged:fire()
end)
showInactive = checkbox.Value
checkbox.Frame.Parent = container
checkbox.Frame.Position = UDim2_new(0, x, 0, 4)
x = x + Style.CheckboxSize + 4
end
x = x + 8
--[[
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 = "rbxasset://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 = Style.Font
label.FontSize = 'Size10'
label.TextXAlignment = 'Left'
label.TextYAlignment = 'Top'
labels[labelPosition] = label
end
label.TextWrapped = textWrappedEnabled
label.Size = LabelSize
label.TextColor3 = TextColors[message.Type] or TextColorUnknown
label.Text = message.Time .. " -- " .. message.Message
local height = label.TextBounds.Y
label.Size = LabelSize -- UDim2_new(1, -padding, 0, height)
label.Position = UDim2_new(0, padding, 0, y)
y = y + height
if height > 16 then
y = y + 4
end
else
labelPositions[i] = false
end
end
-- Destroy extra labels
for i = #labels, labelPosition + 1, -1 do
labels[i]:Destroy()
labels[i] = nil
end
setHeight(y)
end
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://www.roblox.com/asset/?id=261880783')
scrollbar.ButtonUp = buttonUp
buttonUp.Size = UDim2_new(1, 0, 0, Style.HandleHeight)
buttonUp.Position = UDim2_new(0, 0, 0, -Style.HandleHeight - 1)
buttonUp.AutoButtonColor = false
devConsole:ConnectButtonHover(buttonUp, devConsole:CreateButtonEffectFunction(buttonUp))
local buttonDown = Primitives.ImageButton(frame, 'Down', 'https://www.roblox.com/asset/?id=261880783')
scrollbar.ButtonDown = buttonDown
buttonDown.Size = UDim2_new(1, 0, 0, Style.HandleHeight)
buttonDown.Position = UDim2_new(0, 0, 1, 1)
buttonDown.Rotation = 180
buttonDown.AutoButtonColor = false
devConsole:ConnectButtonHover(buttonDown, devConsole:CreateButtonEffectFunction(buttonDown))
local bar = Primitives.Button(frame, 'Bar')
bar.Size = UDim2_new(1, 0, 0.5, 0)
bar.Position = UDim2_new(0, 0, 0.25, 0)
bar.AutoButtonColor = false
local grip = Primitives.InvisibleImageLabel(bar, 'Image', 'https://www.roblox.com/asset/?id=261904959')
grip.Size = UDim2_new(0, 16, 0, 16)
grip.Position = UDim2_new(0.5, -8, 0.5, -8)
local buttonEffectFunction = devConsole:CreateButtonEffectFunction(bar, nil, bar.BackgroundColor3, bar.BackgroundColor3)
-- Inertial scrolling would be added around here
local value = 1
local valueChanged = CreateSignal()
scrollbar.ValueChanged = valueChanged
-- value = 0: at very top
-- value = 1: at very bottom
local height = 0.25
local heightChanged = CreateSignal()
scrollbar.HeightChanged = heightChanged
-- height = 0: infinite page size
-- height = 1: bar fills frame completely, no need to scroll
local function getValueAtPosition(pos)
return ((pos - main.AbsolutePosition.Y) / main.AbsoluteSize.Y) / (1 - height)
end
-- Refreshes the position and size of the scrollbar
local function refresh()
local y = height
bar.Size = UDim2_new(1, 0, y, 0)
bar.Position = UDim2_new(0, 0, value * (1 - y), 0)
end
refresh()
function scrollbar.SetValue(scrollbar, valueNew)
if valueNew < 0 then
valueNew = 0
elseif valueNew > 1 then
valueNew = 1
end
if valueNew ~= value then
value = valueNew
refresh()
valueChanged:fire(valueNew)
end
end
function scrollbar.GetValue(scrollbar)
return value
end
function scrollbar.Scroll(scrollbar, direction, windowHeight, bodyHeight)
scrollbar:SetValue(value + direction / bodyHeight) -- needs to be adjusted
end
function scrollbar.SetHeight(scrollbar, heightNew)
if heightNew < 0 then
heightNew = 0 -- this is still an awkward case of divide-by-zero that shouldn't happen
elseif heightNew > 1 then
heightNew = 1
end
heightNew = math.max(heightNew, 0.1) -- Minimum scroll bar size, from that point on it is not the actual ratio
if heightNew ~= height then
height = heightNew
scrollbar:SetVisible(heightNew < 1)
refresh()
heightChanged:fire(heightNew)
end
end
function scrollbar.GetHeight(scrollbar)
return height
end
devConsole:ConnectButtonDragging(bar, function()
local value0 = value -- starting value
return function(dx, dy)
local dposition = dy -- net position change relative to the bar's axis (could support rotated scroll bars)
local dvalue = (dposition / frame.AbsoluteSize.Y) / (1 - height) -- net value change
scrollbar:SetValue(value0 + dvalue)
end
end, buttonEffectFunction)
return scrollbar
end
----
-- Fancy color lerp --
----
local RenderLerpAnimation; do
local math_cos = math.cos
local math_pi = math.pi
function RenderLerpAnimation(disconnectSignal, length, callback)
disconnectSignal:fire()
local timeStamp = tick()
local listener = RunService.RenderStepped:connect(function()
local t = (tick() - timeStamp) / length
if t >= 1 then
t = 1
disconnectSignal:fire()
else
t = (1 - math_cos(t * math_pi)) / 2 -- cosine interpolation aka 'Sine' in :TweenSizeAndPosition
end
callback(t)
end)
disconnectSignal:connect(listener)
return listener
end
end
if EYECANDY_ENABLED then
-- This is the pretty version
function Methods.CreateButtonEffectFunction(devConsole, button, normalColor, clickingColor, hoveringColor)
normalColor = normalColor or button.BackgroundColor3
clickingColor = clickingColor or Style.GetButtonDownColor(normalColor)
hoveringColor = hoveringColor or Style.GetButtonHoverColor(normalColor)
local disconnectSignal = CreateDisconnectSignal()
return function(clicking, hovering)
local color0 = button.BackgroundColor3
local color1 = clicking and clickingColor or (hovering and hoveringColor or normalColor)
local r0, g0, b0 = color0.r, color0.g, color0.b
local r1, g1, b1 = color1.r, color1.g, color1.b
local r2, g2, b2 = r1 - r0, g1 - g0, b1 - b0
RenderLerpAnimation(disconnectSignal, clicking and 0.125 or 0.25, function(t)
button.BackgroundColor3 = Color3_new(r0 + r2 * t, g0 + g2 * t, b0 + b2 * t)
end)
end
end
else
-- This is the simple version
function Methods.CreateButtonEffectFunction(devConsole, button, normalColor, clickingColor, hoveringColor)
normalColor = normalColor or button.BackgroundColor3
clickingColor = clickingColor or Style.GetButtonDownColor(normalColor)
hoveringColor = hoveringColor or Style.GetButtonHoverColor(normalColor)
return function(clicking, hovering)
button.BackgroundColor3 = clicking and clickingColor or (hovering and hoveringColor or normalColor)
end
end
end
function Methods.GenerateOptionButtonAnimationToggle(devConsole, interior, button, gear, tabContainer, optionsClippingFrame, optionsFrame)
local tabContainerSize0 = tabContainer.Size
local tabContainerSize1 = UDim2_new(
tabContainerSize0.X.Scale, tabContainerSize0.X.Offset + (Style.GearSize + 2) + Style.BorderSize,
tabContainerSize0.Y.Scale, tabContainerSize0.Y.Offset)
local gearRotation0 = gear.Rotation
local gearRotation1 = gear.Rotation - 90
local interiorSize0 = interior.Size
local interiorSize1 = UDim2_new(interiorSize0.X.Scale, interiorSize0.X.Offset, interiorSize0.Y.Scale, interiorSize0.Y.Offset - Style.OptionAreaHeight)
local interiorPosition0 = interior.Position
local interiorPosition1 = UDim2_new(interiorPosition0.X.Scale, interiorPosition0.X.Offset, interiorPosition0.Y.Scale, interiorPosition0.Y.Offset + Style.OptionAreaHeight)
local length = 0.5
local disconnector = CreateDisconnectSignal()
return function(open)
if open then
interior:TweenSizeAndPosition(interiorSize1, interiorPosition1, 'Out', 'Sine', length, true)
tabContainer:TweenSize(tabContainerSize1, 'Out', 'Sine', length, true)
optionsClippingFrame:TweenSizeAndPosition(
UDim2_new(1, 0, 0, Style.OptionAreaHeight),
UDim2_new(0, 0, 0, -Style.OptionAreaHeight),
'Out', 'Sine', length, true
)
optionsFrame:TweenPosition(
UDim2_new(0, 0, 0, 0),-- -Style.OptionAreaHeight),
'Out', 'Sine', length, true
)
local gearRotation = gear.Rotation
RenderLerpAnimation(disconnector, length, function(t)
gear.Rotation = gearRotation1 * t + gearRotation * (1 - t)
end)
else
interior:TweenSizeAndPosition(interiorSize0, interiorPosition0, 'Out', 'Sine', length, true)
tabContainer:TweenSize(tabContainerSize0, 'Out', 'Sine', length, true)
optionsClippingFrame:TweenSizeAndPosition(
UDim2_new(1, 0, 0, 0),
UDim2_new(0, 0, 0, 0),
'Out', 'Sine', length, true
)
optionsFrame:TweenPosition(
UDim2_new(0, 0, 0, Style.OptionAreaHeight),
'Out', 'Sine', length, true
)
local gearRotation = gear.Rotation
RenderLerpAnimation(disconnector, length, function(t)
gear.Rotation = gearRotation0 * t + gearRotation * (1 - t)
end)
end
end
end
----
-- Events for color effects --
----
do
local globalInteractEvent = CreateSignal()
function Methods.ConnectButtonHover(devConsole, button, mouseInteractCallback)
-- void mouseInteractCallback(bool clicking, bool hovering)
local this = {}
local clicking = false
local hovering = false
local function set(clickingNew, hoveringNew)
if hoveringNew and TouchEnabled then
hoveringNew = false -- Touch screens don't hover
end
if clickingNew ~= clicking or hoveringNew ~= hovering then
clicking, hovering = clickingNew, hoveringNew
mouseInteractCallback(clicking, hovering)
end
end
button.MouseButton1Down:connect(function()
set(true, true)
end)
button.MouseButton1Up:connect(function()
set(false, true)
end)
button.MouseEnter:connect(function()
set(clicking, true)
end)
button.MouseLeave:connect(function()
set(false, false)
end)
--[[ these might cause memory leakes (when creating temporary buttons)
-- This solves the case in which the user presses F9 while hovering over a button
devConsole.VisibleChanged:connect(function()
set(false, false)
end)
globalInteractEvent:connect(function()
set(false, false)
end)
--]]
end
end
----
-- Events for draggers -- (for the window's top handle, the resize button, and scrollbars)
----
function Methods.ConnectButtonDragging(devConsole, button, dragCallback, mouseInteractCallback)
-- How dragCallback is called: local deltaCallback = dragCallback(xPositionAtMouseDown, yPositionAtMouseDown)
-- How deltaCallback is called: deltaCallback(netChangeInAbsoluteXPositionSinceMouseDown, netChangeInAbsoluteYPositionSinceMouseDown)
local dragging = false -- AKA 'clicking'
local hovering = false
local listeners = {}
local disconnectCallback;
local function stopDragging()
if not dragging then
return
end
dragging = false
mouseInteractCallback(dragging, hovering)
for i = #listeners, 1, -1 do
listeners[i]:disconnect()
listeners[i] = nil
end
end
local ButtonUserInputTypes = {
[Enum.UserInputType.MouseButton1] = true;
[Enum.UserInputType.Touch] = true; -- I'm not sure if touch actually works here
}
local mouse = game:GetService("Players").LocalPlayer:GetMouse()
local function startDragging()
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)
--]]
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
Unused Code#
In content/scripts/Module/LoadingScreen3D.lua is code that goes unreferenced.
GuiService.ErrorMessageChanged:connect(function()
-- TODO
end)
GuiService.UiMessageChanged:connect(function(type, newMessage)
-- TODO
end)