This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,460 @@
local CoreGui = game:GetService("CoreGui")
local ContextActionService = game:GetService("ContextActionService")
local TweenService = game:GetService("TweenService")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Utility = require(RobloxGui.Modules.Settings.Utility)
local INPUT_TYPE_ROW_HEIGHT = 30
local ACTION_ROW_HEIGHT = 20
local ROW_PADDING = 5
local COLUMN_PADDING = 5
local CORE_SECURITY_COLUMN_COLOR = Color3.new(0.1, 0, 0)
local DEV_SECURITY_COLUMN_COLOR = Color3.new(0, 0, 0)
local EXPAND_ROTATE_IMAGE_TWEEN_OUT = TweenInfo.new(0.150, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
local EXPAND_ROTATE_IMAGE_TWEEN_IN = TweenInfo.new(0.150, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
local ROW_PULSE = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, true, 0)
local CONTAINER_SCROLL = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
local container = nil
local boundInputTypeRows = {}
local boundInputTypesByRows = {}
local boundInputTypeActionRows = {}
local boundActionInfoByRows = {}
local inputTypesByActionRows = {}
local inputTypesByHeaders = {}
local headersByInputTypes = {}
local inputTypesExpanded = {}
local layoutOrderDirty = true
local rowTypePrecedence = {
BoundInputType = 3,
TableHeader = 2,
BoundAction = 1
}
local function sortInputTypeRows(a, b)
local inputTypeA = boundInputTypesByRows[a]
local inputTypeB = boundInputTypesByRows[b]
return tostring(inputTypeA) < tostring(inputTypeB)
end
local function sortActionRows(a, b)
local actionA = boundActionInfoByRows[a]
local actionB = boundActionInfoByRows[b]
if actionA and actionB then
local rowInputTypeA = inputTypesByActionRows[a]
local rowInputTypeB = inputTypesByActionRows[b]
if rowInputTypeA ~= rowInputTypeB then
return tostring(rowInputTypeA) < tostring(rowInputTypeB)
end
if actionA.isCore and not actionB.isCore then
return true
elseif not actionA.isCore and actionB.isCore then
return false
end
local stackOrderA = actionA.stackOrder
local stackOrderB = actionB.stackOrder
if stackOrderA and stackOrderB then
return stackOrderA > stackOrderB --descending sort
else
return true
end
else
return true
end
return true
end
local function createEmptyRow(name, height)
local row = Utility:Create("Frame") {
Name = name,
BackgroundTransparency = 1,
ZIndex = 6,
Size = UDim2.new(1, 0, 0, height or 0)
}
local columnList = Utility:Create("UIListLayout") {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, COLUMN_PADDING),
Parent = row
}
return row
end
local function createButtonRow(name, height)
local row = Utility:Create("TextButton") {
Name = name,
BackgroundTransparency = 1,
ZIndex = 6,
Text = "",
Size = UDim2.new(1, 0, 0, height or 0),
}
local columnList = Utility:Create("UIListLayout") {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, COLUMN_PADDING),
Parent = row
}
return row
end
local function createEmptyColumn(row, columnName)
local column = Utility:Create("Frame") {
Name = columnName,
BackgroundColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 0.75,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 6,
ClipsDescendants = true,
Parent = row
}
return column
end
local function createImageColumn(row, columnName, image, aspectRatio, imageSize)
local column = createEmptyColumn(row, columnName)
local aspectRatioConstraint = Utility:Create("UIAspectRatioConstraint") {
AspectRatio = aspectRatio or 1,
Parent = column
}
local imageLabel = Utility:Create("ImageLabel") {
Name = "ColumnImage",
BackgroundTransparency = 1,
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = UDim2.new(imageSize or 1, 0, imageSize or 1, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
ZIndex = 6,
Image = image,
Parent = column
}
return column
end
local function createTextColumn(row, columnName, text)
local column = createEmptyColumn(row, columnName)
local textLabel = Utility:Create("TextLabel") {
Name = "ColumnText",
BackgroundTransparency = 1,
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = UDim2.new(1, -10, 1, -10),
AnchorPoint = Vector2.new(0.5, 0.5),
ZIndex = 6,
Text = text,
TextSize = 18,
TextColor3 = Color3.new(1, 1, 1),
TextXAlignment = Enum.TextXAlignment.Left,
Font = Enum.Font.SourceSans,
Parent = column
}
return column
end
local function createActionColumns(row, backgroundColor)
local x = 0
local insetWidth = ACTION_ROW_HEIGHT + (COLUMN_PADDING * 2)
local insetCol = createEmptyColumn(row, "Inset")
insetCol.LayoutOrder = 0
insetCol.BackgroundTransparency = 1
insetCol.Size = UDim2.new(0, insetWidth, 1, 0)
x = x + insetWidth + COLUMN_PADDING
local priorityWidth = 80
local priorityCol = createTextColumn(row, "Priority", "Priority")
priorityCol.LayoutOrder = 1
priorityCol.BackgroundColor3 = backgroundColor
priorityCol.Size = UDim2.new(0, priorityWidth, 1, 0)
x = x + priorityWidth + COLUMN_PADDING
local securityWidth = 80
local securityCol = createTextColumn(row, "Security", "Security")
securityCol.LayoutOrder = 2
securityCol.BackgroundColor3 = backgroundColor
securityCol.Size = UDim2.new(0, securityWidth, 1, 0)
x = x + securityWidth + COLUMN_PADDING
local nameCol = createTextColumn(row, "ActionName", "Action Name")
nameCol.LayoutOrder = 3
nameCol.BackgroundColor3 = backgroundColor
nameCol.Size = UDim2.new(1/4, 0, 1, 0)
local inputTypesCol = createTextColumn(row, "InputTypes", "Input Types")
inputTypesCol.LayoutOrder = 4
inputTypesCol.BackgroundColor3 = backgroundColor
inputTypesCol.Size = UDim2.new(3/4, -x - COLUMN_PADDING, 1, 0)
return insetCol, priorityCol, securityCol, nameCol, inputTypesCol
end
local function updateContainerCanvas()
debug.profilebegin("updateContainerCanvas")
if layoutOrderDirty then
layoutOrderDirty = false
local idx = 1
local inputTypeRowList = {}
for inputType, inputTypeRow in pairs(boundInputTypeRows) do
table.insert(inputTypeRowList, inputTypeRow)
end
table.sort(inputTypeRowList, sortInputTypeRows)
for i, inputTypeRow in pairs(inputTypeRowList) do
local inputType = boundInputTypesByRows[inputTypeRow]
inputTypeRow.LayoutOrder = idx; idx = idx + 1
local headerRow = headersByInputTypes[inputType]
if headerRow then
headerRow.LayoutOrder = idx; idx = idx + 1
end
local actionRows = boundInputTypeActionRows[inputType]
if actionRows then
local actionRowList = {}
for actionName, actionRow in pairs(actionRows) do
table.insert(actionRowList, actionRow)
end
table.sort(actionRowList, sortActionRows)
for i, actionRow in pairs(actionRowList) do
actionRow.LayoutOrder = idx; idx = idx + 1
end
end
end
end
debug.profileend()
end
local function scrollContainerToRow(row)
local scrollOffset = row.AbsolutePosition.Y - container.AbsolutePosition.Y
local newCanvasPosition = container.CanvasPosition + Vector2.new(0, scrollOffset)
TweenService:Create(container, CONTAINER_SCROLL, { CanvasPosition = newCanvasPosition }):Play()
end
local ActionBindingsTab = {}
function ActionBindingsTab.initializeGui(tabFrame)
local scrollingFrame = Utility:Create("ScrollingFrame") {
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = UDim2.new(1, -10, 1, -10),
AnchorPoint = Vector2.new(0.5, 0.5),
BorderSizePixel = 0,
ScrollBarThickness = 4,
BackgroundTransparency = 1,
ZIndex = 6,
Parent = tabFrame
}
container = scrollingFrame
local listLayout = Utility:Create("UIListLayout") {
Padding = UDim.new(0, ROW_PADDING),
Parent = scrollingFrame
}
listLayout.SortOrder = Enum.SortOrder.LayoutOrder
listLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function() container.CanvasSize = UDim2.new(0, 0, 0, listLayout.AbsoluteContentSize.Y) end)
ActionBindingsTab.updateGuis()
ContextActionService.BoundActionAdded:connect(function(actionName, createTouchButton, actionInfo, isCore)
actionInfo.isCore = isCore
ActionBindingsTab.updateActionRows(actionName, actionInfo)
layoutOrderDirty = true
updateContainerCanvas()
end)
ContextActionService.BoundActionRemoved:connect(function(actionName, actionInfo, isCore)
actionInfo.isCore = isCore
ActionBindingsTab.removeActionRows(actionName, actionInfo)
layoutOrderDirty = true
updateContainerCanvas()
end)
end
function ActionBindingsTab.updateBoundInputTypeRow(inputType)
local existingRow = boundInputTypeRows[inputType]
if not existingRow then
local row = createButtonRow("BoundInputType", INPUT_TYPE_ROW_HEIGHT)
local expandImageCol = createImageColumn(row, "ExpandImage", "rbxasset://textures/ui/ExpandArrowSheet.png", 1, 0.35)
expandImageCol.ColumnImage.ImageRectSize = Vector2.new(21, 21)
expandImageCol.ColumnImage.ImageRectOffset = Vector2.new(0, 0)
local inputTypeCol = createTextColumn(row, "InputType", tostring(inputType))
inputTypeCol.Size = UDim2.new(1, -INPUT_TYPE_ROW_HEIGHT - COLUMN_PADDING, 1, 0)
inputTypeCol.ColumnText.Font = Enum.Font.SourceSansBold
local tableHeaderRow = createEmptyRow("TableHeader", ACTION_ROW_HEIGHT)
tableHeaderRow.Visible = false
local _, priorityCol, securityCol, nameCol, inputTypesCol = createActionColumns(tableHeaderRow, DEV_SECURITY_COLUMN_COLOR)
priorityCol.ColumnText.Font = Enum.Font.SourceSansBold
securityCol.ColumnText.Font = Enum.Font.SourceSansBold
nameCol.ColumnText.Font = Enum.Font.SourceSansBold
inputTypesCol.ColumnText.Font = Enum.Font.SourceSansBold
boundInputTypeRows[inputType] = row
boundInputTypesByRows[row] = inputType
inputTypesByHeaders[tableHeaderRow] = inputType
headersByInputTypes[inputType] = tableHeaderRow
tableHeaderRow.Parent = container
row.Parent = container
TweenService:Create(inputTypeCol, ROW_PULSE, { BackgroundColor3 = Color3.new(0.5, 0.5, 0.5) }):Play()
inputTypesExpanded[inputType] = false
row.MouseButton1Click:connect(function()
inputTypesExpanded[inputType] = not inputTypesExpanded[inputType]
local inputTypeActionRows = boundInputTypeActionRows[inputType]
if not inputTypesExpanded[inputType] then
expandImageCol.ColumnImage.ImageRectOffset = Vector2.new(0, 0)
tableHeaderRow.Visible = false
if inputTypeActionRows then
for _, actionRow in pairs(inputTypeActionRows) do
actionRow.Visible = false
end
end
else
expandImageCol.ColumnImage.ImageRectOffset = Vector2.new(21, 0)
tableHeaderRow.Visible = true
if inputTypeActionRows then
for _, actionRow in pairs(inputTypeActionRows) do
actionRow.Visible = true
end
end
end
updateContainerCanvas()
if inputTypesExpanded[inputType] then
TweenService:Create(inputTypeCol, ROW_PULSE, { BackgroundColor3 = Color3.new(0.5, 0.5, 0.5) }):Play()
scrollContainerToRow(row)
end
end)
end
end
function ActionBindingsTab.updateActionRowForInputType(actionName, actionInfo, inputType)
local inputTypeActionRows = boundInputTypeActionRows[inputType]
if not inputTypeActionRows then
inputTypeActionRows = {}
boundInputTypeActionRows[inputType] = inputTypeActionRows
end
local existingRow = inputTypeActionRows[actionName]
if not existingRow then
local row = createEmptyRow("BoundAction", ACTION_ROW_HEIGHT)
row.Visible = inputTypesExpanded[inputType]
local inputTypeNames = {}
for i, inputType in pairs(actionInfo.inputTypes) do
inputTypeNames[i] = tostring(inputType)
end
local insetCol, priorityCol, securityCol, nameCol, inputTypesCol = createActionColumns(row, actionInfo.isCore and CORE_SECURITY_COLUMN_COLOR or DEV_SECURITY_COLUMN_COLOR)
priorityCol.ColumnText.Text = actionInfo.priorityLevel or "Default"
securityCol.ColumnText.Text = actionInfo.isCore and "Core" or "Developer"
nameCol.ColumnText.Text = actionName
inputTypesCol.ColumnText.Text = table.concat(inputTypeNames, ", ")
if actionInfo.isCore then
priorityCol.ColumnText.Font = Enum.Font.SourceSansItalic
securityCol.ColumnText.Font = Enum.Font.SourceSansItalic
nameCol.ColumnText.Font = Enum.Font.SourceSansItalic
inputTypesCol.ColumnText.Font = Enum.Font.SourceSansItalic
end
inputTypeActionRows[actionName] = row
inputTypesByActionRows[row] = inputType
boundActionInfoByRows[row] = actionInfo
row.Parent = container
if row.Visible then
TweenService:Create(nameCol, ROW_PULSE, { BackgroundColor3 = Color3.new(0.5, 0.5, 0.5) }):Play()
else
local inputTypeRow = boundInputTypeRows[inputType]
if inputTypeRow then
TweenService:Create(inputTypeRow.InputType, ROW_PULSE, { BackgroundColor3 = Color3.new(0.5, 0.5, 0.5) }):Play()
end
end
existingRow = row
end
end
function ActionBindingsTab.updateActionRows(actionName, actionInfo)
for _, inputType in pairs(actionInfo.inputTypes) do
ActionBindingsTab.updateBoundInputTypeRow(inputType)
ActionBindingsTab.updateActionRowForInputType(actionName, actionInfo, inputType)
end
end
function ActionBindingsTab.removeActionRows(actionName, actionInfo)
for _, inputType in pairs(actionInfo.inputTypes) do
local inputTypeActionRows = boundInputTypeActionRows[inputType]
if inputTypeActionRows then
local row = inputTypeActionRows[actionName]
row:Destroy()
inputTypeActionRows[actionName] = nil
--The following code looks weird. It's because Lua has no way to determine
--if a table is explicitly empty in both the array and dictionary parts.
--This does it though.
local isEmpty = true
for _, __ in pairs(inputTypeActionRows) do
isEmpty = false
break
end
if isEmpty then
local inputTypeRow = boundInputTypeRows[inputType]
if inputTypeRow then
inputTypeRow:Destroy()
boundInputTypeRows[inputType] = nil
end
local tableHeaderRow = headersByInputTypes[inputType]
if tableHeaderRow then
headersByInputTypes[tableHeaderRow] = nil
tableHeaderRow:Destroy()
headersByInputTypes[inputType] = nil
end
end
end
end
updateContainerCanvas()
container.UIListLayout:ApplyLayout()
end
function ActionBindingsTab.updateGuis()
local boundCoreActions = ContextActionService:GetAllBoundCoreActionInfo()
for actionName, actionInfo in pairs(boundCoreActions) do
actionInfo.isCore = true
ActionBindingsTab.updateActionRows(actionName, actionInfo)
end
local boundActions = ContextActionService:GetAllBoundActionInfo()
for actionName, actionInfo in pairs(boundActions) do
actionInfo.isCore = false
ActionBindingsTab.updateActionRows(actionName, actionInfo)
end
layoutOrderDirty = true
updateContainerCanvas()
end
return ActionBindingsTab
@@ -0,0 +1,269 @@
--[[
// FileName: ContextMenuGui.lua
// Written by: TheGamer101
// Description: Module for creating the context GUI.
]]
-- CONSTANTS
local BG_TRANSPARENCY = 1
local BG_COLOR = Color3.fromRGB(31, 31, 31)
local BOTTOM_SCREEN_PADDING_PERCENT = 0.02
local MAX_WIDTH = 250
local MAX_HEIGHT = 300
local MAX_WIDTH_PERCENT = 0.7
local MAX_HEIGHT_PERCENT = 0.6
local PLAYER_ICON_SIZE_Y = 0.3
-- SERVICES
local CoreGuiService = game:GetService("CoreGui")
local GuiService = game:GetService("GuiService")
--- VARIABLES
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local SettingsModules = CoreGuiModules:WaitForChild("Settings")
local AvatarMenuModules = CoreGuiModules:WaitForChild("AvatarContextMenu")
local PlayerCarousel = nil
local PlayerChangedEvent = Instance.new("BindableEvent")
--- Modules
local ContextMenuUtil = require(AvatarMenuModules:WaitForChild("ContextMenuUtil"))
local Utility = require(SettingsModules:WaitForChild("Utility"))
local ContextMenuGui = {}
ContextMenuGui.__index = ContextMenuGui
-- PRIVATE METHODS
function ContextMenuGui:CreateContextMenuHolder(player)
local contextMenuHolder = Instance.new("Frame")
contextMenuHolder.Name = "AvatarContextMenu"
contextMenuHolder.Position = UDim2.new(0, 0, 0, 0)
contextMenuHolder.Size = UDim2.new(1, 0, 1, 0)
contextMenuHolder.BackgroundTransparency = 1
contextMenuHolder.Parent = RobloxGui
return contextMenuHolder
end
function ContextMenuGui:CreateLeaveMenuButton(frame)
local function closeMenu()
self.CloseMenuFunc()
end
local closeMenuButton = Instance.new("ImageButton")
closeMenuButton.Name = "CloseMenuButton"
closeMenuButton.BackgroundTransparency = 1
closeMenuButton.AnchorPoint = Vector2.new(1, 0)
closeMenuButton.Position = UDim2.new(1, -10, 0, 10)
closeMenuButton.Size = UDim2.new(0.05, 0, 0.1, 0)
closeMenuButton.Image = "rbxasset://textures/loading/cancelButton.png"
closeMenuButton.Selectable = false
closeMenuButton.Activated:Connect(closeMenu)
local aspectConstraint = Instance.new("UIAspectRatioConstraint")
aspectConstraint.AspectType = Enum.AspectType.FitWithinMaxSize
aspectConstraint.DominantAxis = Enum.DominantAxis.Height
aspectConstraint.AspectRatio = 1
aspectConstraint.Parent = closeMenuButton
closeMenuButton.Parent = frame
return closeMenuButton
end
-- PUBLIC METHODS
local function listenToViewportChange(functionToFire)
if functionToFire == nil then return end
local viewportChangedConnection = nil
local function updateCamera()
local newCamera = workspace.CurrentCamera
if viewportChangedConnection then
viewportChangedConnection:Disconnect()
end
viewportChangedConnection = newCamera:GetPropertyChangedSignal('ViewportSize'):Connect(functionToFire)
functionToFire()
end
workspace:GetPropertyChangedSignal('CurrentCamera'):Connect(updateCamera)
updateCamera()
end
function ContextMenuGui:CreateMenuFrame()
local contextMenuHolder = self:CreateContextMenuHolder()
local menu = Instance.new("ImageButton")
menu.Name = "Menu"
menu.Size = UDim2.new(0.95, 0, 0.9, 0)
menu.Position = UDim2.new(0.5, 0, 1 - BOTTOM_SCREEN_PADDING_PERCENT, 0)
menu.AnchorPoint = Vector2.new(0.5, 1)
menu.BackgroundTransparency = 1
menu.Selectable = false
menu.Image = "rbxasset://textures/blackBkg_round.png"
menu.ScaleType = Enum.ScaleType.Slice
menu.SliceCenter = Rect.new(12,12,12,12)
menu.Visible = false
menu.Active = true
menu.ClipsDescendants = true
GuiService:AddSelectionParent("AvatarContextMenuGroup", menu)
local aspectConstraint = Instance.new("UIAspectRatioConstraint")
aspectConstraint.AspectType = Enum.AspectType.ScaleWithParentSize
aspectConstraint.DominantAxis = Enum.DominantAxis.Height
aspectConstraint.AspectRatio = 1.15
aspectConstraint.Parent = menu
local function updateAspectRatioForViewport()
local viewportSize = workspace.CurrentCamera.ViewportSize
if viewportSize.x < viewportSize.y then
aspectConstraint.DominantAxis = Enum.DominantAxis.Width
else
aspectConstraint.DominantAxis = Enum.DominantAxis.Height
end
end
listenToViewportChange(updateAspectRatioForViewport)
local sizeConstraint = Instance.new("UISizeConstraint")
sizeConstraint.MaxSize = Vector2.new(300,300)
sizeConstraint.MinSize = Vector2.new(200,200)
sizeConstraint.Parent = menu
local contentFrame = Instance.new("Frame")
contentFrame.Name = "Content"
contentFrame.Size = UDim2.new(1,0,1,0)
contentFrame.BackgroundTransparency = 1
contentFrame.Parent = menu
local contentListLayout = Instance.new("UIListLayout")
contentListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
contentListLayout.VerticalAlignment = Enum.VerticalAlignment.Top
contentListLayout.SortOrder = Enum.SortOrder.LayoutOrder
contentListLayout.Parent = contentFrame
local contextActionList = Instance.new("ScrollingFrame")
contextActionList.Name = "ContextActionList"
contextActionList.AnchorPoint = Vector2.new(0.5,1)
contextActionList.BackgroundColor3 = Color3.fromRGB(79,79,79)
contextActionList.BorderSizePixel = 0
contextActionList.LayoutOrder = 2
contextActionList.Size = UDim2.new(1,-12,0.54,0)
contextActionList.CanvasSize = UDim2.new(0,0,0,208)
contextActionList.ScrollBarThickness = 4
contextActionList.Selectable = false
contextActionList.Parent = contentFrame
local contextActionListUIListLayout = Instance.new("UIListLayout")
contextActionListUIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
contextActionListUIListLayout.SortOrder = Enum.SortOrder.LayoutOrder
contextActionListUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Top
contextActionListUIListLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
contextActionList.CanvasSize = UDim2.new(0,0,0,contextActionListUIListLayout.AbsoluteContentSize.Y)
end)
contextActionListUIListLayout.Parent = contextActionList
local nameTag = Instance.new("TextButton")
nameTag.Name = "NameTag"
nameTag.AnchorPoint = Vector2.new(0.5,1)
nameTag.BackgroundColor3 = Color3.fromRGB(79,79,79)
nameTag.AutoButtonColor = false
nameTag.BorderSizePixel = 0
nameTag.LayoutOrder = 1
nameTag.Size = UDim2.new(1,-12,0.16,0)
nameTag.Font = Enum.Font.SourceSansBold
nameTag.Text = ""
nameTag.TextColor3 = Color3.fromRGB(255,255,255)
nameTag.TextSize = 24
nameTag.TextXAlignment = Enum.TextXAlignment.Center
nameTag.TextYAlignment = Enum.TextYAlignment.Center
nameTag.Selectable = false
nameTag.Parent = contentFrame
local underline = Instance.new("Frame")
underline.Name = "Underline"
underline.BackgroundColor3 = Color3.fromRGB(255,255,255)
underline.AnchorPoint = Vector2.new(0,1)
underline.BorderSizePixel = 0
underline.Position = UDim2.new(0,0,1,0)
underline.Size = UDim2.new(1,0,0,2)
underline.Parent = nameTag
self:CreateLeaveMenuButton(menu)
menu.Parent = contextMenuHolder
self.ContextMenuFrame = menu
return menu
end
function ContextMenuGui:BuildPlayerCarousel(playersByProximity)
if not PlayerCarousel then
PlayerCarousel = require(AvatarMenuModules:WaitForChild("PlayerCarousel"))
PlayerCarousel.rbxGui.Parent = self.ContextMenuFrame.Content
end
PlayerCarousel:ClearPlayerEntries()
for i = 1, #playersByProximity do
PlayerCarousel:CreatePlayerEntry(playersByProximity[i][1], playersByProximity[i][2])
end
if #playersByProximity > 0 then
self.ContextMenuFrame.Content.NameTag.Text = playersByProximity[1][1].Name
end
PlayerCarousel.PlayerChanged:Connect(function(player)
if player then
self.ContextMenuFrame.Content.NameTag.Text = player.Name
else
self.ContextMenuFrame.Content.NameTag.Text = ""
end
PlayerChangedEvent:Fire(player)
end)
end
function ContextMenuGui:GetBottomScreenPaddingConstant()
return BOTTOM_SCREEN_PADDING_PERCENT
end
function ContextMenuGui:SetCloseMenuFunc(closeMenuFunc)
self.CloseMenuFunc = closeMenuFunc
end
function ContextMenuGui:SwitchToPlayerEntry(player, dontTween)
if not PlayerCarousel then return end
PlayerCarousel:SwitchToPlayerEntry(player, dontTween)
end
function ContextMenuGui:OffsetPlayerEntry(offset)
if not PlayerCarousel then return end
PlayerCarousel:OffsetPlayerEntry(offset)
end
function ContextMenuGui:GetSelectedPlayer()
if not PlayerCarousel then return nil end
return PlayerCarousel:GetSelectedPlayer()
end
function ContextMenuGui.new()
local obj = setmetatable({}, ContextMenuGui)
obj.CloseMenuFunc = nil
obj.ContextMenuFrame = nil
obj.LastSetPlayerIcon = nil
obj.SelectedPlayerChanged = PlayerChangedEvent.Event
return obj
end
return ContextMenuGui.new()
@@ -0,0 +1,314 @@
--[[
// FileName: ContextMenuItems.lua
// Written by: TheGamer101
// Description: Module for creating the context menu items for the menu and doing the actions when they are clicked.
]]
-- CONSTANTS
local FRIEND_LAYOUT_ORDER = 1
local CHAT_LAYOUT_ORDER = 3
local WAVE_LAYOUT_ORDER = 4
local CUSTOM_LAYOUT_ORDER = 20
local MENU_ITEM_SIZE_X = 0.96
local MENU_ITEM_SIZE_Y = 0
local MENU_ITEM_SIZE_Y_OFFSET = 52
local THUMBNAIL_URL = "https://www.roblox.com/Thumbs/Avatar.ashx?x=200&y=200&format=png&userId="
local BUST_THUMBNAIL_URL = "https://www.roblox.com/bust-thumbnail/image?width=420&height=420&format=png&userId="
--- SERVICES
local PlayersService = game:GetService("Players")
local CoreGuiService = game:GetService("CoreGui")
local StarterGui = game:GetService("StarterGui")
local Chat = game:GetService("Chat")
local RunService = game:GetService("RunService")
local AnalyticsService = game:GetService("AnalyticsService")
-- MODULES
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local SettingsModules = CoreGuiModules:WaitForChild("Settings")
local AvatarMenuModules = CoreGuiModules:WaitForChild("AvatarContextMenu")
local SettingsPages = SettingsModules:WaitForChild("Pages")
local ContextMenuUtil = require(AvatarMenuModules:WaitForChild("ContextMenuUtil"))
local PromptCreator = require(CoreGuiModules:WaitForChild("PromptCreator"))
local PlayerDropDownModule = require(CoreGuiModules:WaitForChild("PlayerDropDown"))
local ReportAbuseMenu = require(SettingsPages:WaitForChild("ReportAbuseMenu"))
-- VARIABLES
local LocalPlayer = PlayersService.LocalPlayer
while not LocalPlayer do
PlayersService.PlayerAdded:wait()
LocalPlayer = PlayersService.LocalPlayer
end
local EnabledContextMenuItems = {
[Enum.AvatarContextMenuOption.Chat] = true,
[Enum.AvatarContextMenuOption.Friend] = true,
[Enum.AvatarContextMenuOption.Emote] = true
}
local CustomContextMenuItems = {}
local BlockingUtility = PlayerDropDownModule:CreateBlockingUtility()
local ContextMenuItems = {}
ContextMenuItems.__index = ContextMenuItems
-- PRIVATE METHODS
function ContextMenuItems:ClearMenuItems()
local children = self.MenuItemFrame:GetChildren()
for i = 1, #children do
if children[i]:IsA("GuiObject") then
children[i]:Destroy()
end
end
end
function ContextMenuItems:AddCustomAvatarMenuItem(menuOption, bindableEvent)
CustomContextMenuItems[menuOption] = bindableEvent
end
function ContextMenuItems:RemoveCustomAvatarMenuItem(menuOption)
CustomContextMenuItems[menuOption] = nil
end
function ContextMenuItems:IsContextAvatarEnumItem(enumItem)
local enumItems = Enum.AvatarContextMenuOption:GetEnumItems()
for i = 1, #enumItems do
if enumItem == enumItems[i] then
return true
end
end
return false
end
function ContextMenuItems:EnableDefaultMenuItem(menuOption)
EnabledContextMenuItems[menuOption] = true
end
function ContextMenuItems:RemoveDefaultMenuItem(menuOption)
EnabledContextMenuItems[menuOption] = false
end
function ContextMenuItems:RegisterCoreMethods()
local function addMenuItemFunc(args) --[[ menuOption, bindableEvent]]
if type(args) == "table" then
local name = ""
if args[1] and type(args[1]) == "string" then
name = args[1]
else
error("AddAvatarContextMenuOption first argument must be a table or Enum.AvatarContextMenuOption")
end
if args[2] and typeof(args[2]) == "Instance" and args[2].ClassName == "BindableEvent" then
self:AddCustomAvatarMenuItem(name, args[2])
else
error("AddAvatarContextMenuOption second table entry must be a BindableEvent")
end
elseif typeof(args) == "EnumItem" then
if self:IsContextAvatarEnumItem(args) then
self:EnableDefaultMenuItem(args)
else
error("AddAvatarContextMenuOption given EnumItem is not valid")
end
else
error("AddAvatarContextMenuOption first argument must be a table or Enum.AvatarContextMenuOption")
end
end
StarterGui:RegisterSetCore("AddAvatarContextMenuOption", addMenuItemFunc)
local function removeMenuItemFunc(menuOption)
if type(menuOption) == "string" then
self:RemoveCustomAvatarMenuItem(menuOption)
elseif typeof(menuOption) == "EnumItem" then
if self:IsContextAvatarEnumItem(menuOption) then
self:RemoveDefaultMenuItem(menuOption)
else
error("RemoveAvatarContextMenuOption given EnumItem is not valid")
end
else
error("RemoveAvatarContextMenuOption first argument must be a string or Enum.AvatarContextMenuOption")
end
end
StarterGui:RegisterSetCore("RemoveAvatarContextMenuOption", removeMenuItemFunc)
end
function ContextMenuItems:CreateCustomMenuItems()
for buttonText, bindableEvent in pairs(CustomContextMenuItems) do
AnalyticsService:TrackEvent("Game", "AvatarContextMenuCustomButton", "name: " .. tostring(buttonText))
local function customButtonFunc()
bindableEvent:Fire(self.SelectedPlayer)
end
local customButton = ContextMenuUtil:MakeStyledButton("CustomButton", buttonText, UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET), customButtonFunc)
customButton.Name = "CustomButton"
customButton.LayoutOrder = CUSTOM_LAYOUT_ORDER
customButton.Parent = self.MenuItemFrame
end
end
-- PUBLIC METHODS
local addFriendString = "Add Friend"
local friendsString = "Friends"
local friendRequestPendingString = "Friend Request Pending"
local acceptFriendRequestString = "Accept Friend Request"
local addFriendDisabledTransparency = 0.75
local friendStatusChangedConn = nil
function ContextMenuItems:CreateFriendButton(status)
local friendLabel = self.MenuItemFrame:FindFirstChild("FriendStatus")
if friendLabel then
friendLabel:Destroy()
friendLabel = nil
end
if friendStatusChangedConn then
friendStatusChangedConn:disconnect()
end
local friendLabelText = nil
local addFriendFunc = function()
if friendLabelText and friendLabel.Selectable then
friendLabel.Selectable = false
friendLabelText.TextTransparency = addFriendDisabledTransparency
friendLabelText.Text = friendRequestPendingString
AnalyticsService:ReportCounter("AvatarContextMenu-RequestFriendship")
AnalyticsService:TrackEvent("Game", "RequestFriendship", "AvatarContextMenu")
LocalPlayer:RequestFriendship(self.SelectedPlayer)
end
end
friendLabel, friendLabelText = ContextMenuUtil:MakeStyledButton("FriendStatus", addFriendString, UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET), addFriendFunc)
if status ~= Enum.FriendStatus.Friend then
friendLabel.Selectable = true
friendLabelText.TextTransparency = 0
else
friendLabel.Selectable = false
friendLabelText.TextTransparency = addFriendDisabledTransparency
friendLabelText.Text = friendsString
end
friendStatusChangedConn = LocalPlayer.FriendStatusChanged:connect(function(player, friendStatus)
if player == self.SelectedPlayer and friendLabelText then
if not friendLabel.Selectable then
if friendStatus == Enum.FriendStatus.Friend then
friendLabelText.Text = friendsString
end
else
if friendStatus == Enum.FriendStatus.FriendRequestReceived then
friendLabelText.Text = acceptFriendRequestString
end
end
end
end)
friendLabel.LayoutOrder = FRIEND_LAYOUT_ORDER
friendLabel.Parent = self.MenuItemFrame
end
function ContextMenuItems:UpdateFriendButton(status)
local friendLabel = self.MenuItemFrame:FindFirstChild("FriendStatus")
if friendLabel then
self:CreateFriendButton(status)
end
end
function ContextMenuItems:CreateEmoteButton()
local function wave()
if self.CloseMenuFunc then self:CloseMenuFunc() end
AnalyticsService:ReportCounter("AvatarContextMenu-Wave")
AnalyticsService:TrackEvent("Game", "AvatarContextMenuWave", "placeId: " .. tostring(game.PlaceId))
PlayersService:Chat("/e wave")
end
local waveButton = self.MenuItemFrame:FindFirstChild("Wave")
if not waveButton then
waveButton = ContextMenuUtil:MakeStyledButton("Wave", "Wave", UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET), wave)
waveButton.LayoutOrder = WAVE_LAYOUT_ORDER
waveButton.Parent = self.MenuItemFrame
end
end
function ContextMenuItems:CreateChatButton()
local function chatFunc()
if self.CloseMenuFunc then self:CloseMenuFunc() end
AnalyticsService:ReportCounter("AvatarContextMenu-Chat")
AnalyticsService:TrackEvent("Game", "AvatarContextMenuChat", "placeId: " .. tostring(game.PlaceId))
-- todo: need a proper api to set up text in the chat bar
local ChatBar = nil
pcall(function() ChatBar = LocalPlayer.PlayerGui.Chat.Frame.ChatBarParentFrame.Frame.BoxFrame.Frame.ChatBar end)
if ChatBar then
ChatBar.Text = "/w " .. self.SelectedPlayer.Name
end
local ChatModule = require(RobloxGui.Modules.ChatSelector)
ChatModule:SetVisible(true)
ChatModule:FocusChatBar()
end
local chatButton = self.MenuItemFrame:FindFirstChild("ChatStatus")
if not chatButton then
chatButton = ContextMenuUtil:MakeStyledButton("ChatStatus", "Chat", UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET), chatFunc)
chatButton.LayoutOrder = CHAT_LAYOUT_ORDER
end
local success, canLocalUserChat = pcall(function() return Chat:CanUserChatAsync(LocalPlayer.UserId) end)
local canChat = success and (RunService:IsStudio() or canLocalUserChat)
if canChat then
chatButton.Parent = self.MenuItemFrame
else
chatButton.Parent = nil
end
end
function ContextMenuItems:BuildContextMenuItems(player)
if not player then return end
local friendStatus = ContextMenuUtil:GetFriendStatus(player)
local isBlocked = BlockingUtility:IsPlayerBlockedByUserId(player.UserId)
local isMuted = BlockingUtility:IsPlayerMutedByUserId(player.UserId)
self:ClearMenuItems()
self:SetSelectedPlayer(player)
if EnabledContextMenuItems[Enum.AvatarContextMenuOption.Friend] then
self:CreateFriendButton(friendStatus)
end
if EnabledContextMenuItems[Enum.AvatarContextMenuOption.Chat] then
self:CreateChatButton()
end
if EnabledContextMenuItems[Enum.AvatarContextMenuOption.Emote] then
self:CreateEmoteButton()
end
self:CreateCustomMenuItems()
end
function ContextMenuItems:SetSelectedPlayer(selectedPlayer)
self.SelectedPlayer = selectedPlayer
end
function ContextMenuItems:SetCloseMenuFunc(closeMenuFunc)
self.CloseMenuFunc = closeMenuFunc
end
function ContextMenuItems.new(menuItemFrame)
local obj = setmetatable({}, ContextMenuItems)
obj.MenuItemFrame = menuItemFrame
obj.SelectedPlayer = nil
obj:RegisterCoreMethods()
return obj
end
return ContextMenuItems
@@ -0,0 +1,278 @@
--[[
// FileName: ContextMenuUtil.lua
// Written by: TheGamer101
// Description: Module for utility funcitons of the avatar context menu.
]]
--[[
// FileName: ContextMenuGui.lua
// Written by: TheGamer101
// Description: Module for creating the context GUI.
]]
--- CONSTANTS
local STOP_MOVEMENT_ACTION_NAME = "AvatarContextMenuStopInput"
local MAX_THUMBNAIL_WAIT_TIME = 2
local MAX_THUMBNAIL_RETRIES = 4
--- SERVICES
local CoreGuiService = game:GetService("CoreGui")
local PlayersService = game:GetService("Players")
local ContextActionService = game:GetService("ContextActionService")
local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")
--- VARIABLES
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local LocalPlayer = PlayersService.LocalPlayer
while not LocalPlayer do
PlayersService.PlayerAdded:wait()
LocalPlayer = PlayersService.LocalPlayer
end
local ContextMenuUtil = {}
ContextMenuUtil.__index = ContextMenuUtil
-- PUBLIC METHODS
function ContextMenuUtil:GetHeadshotForPlayer(player)
if self.HeadShotUrlCache[player] ~= nil and self.HeadShotUrlCache[player] ~= "" then
return self.HeadShotUrlCache[player]
end
if self.HeadShotUrlCache[player] == nil then
-- Mark that we are getting a headshot for this player.
self.HeadShotUrlCache[player] = ""
end
local startTime = tick()
local headshotUrl, isFinal = PlayersService:GetUserThumbnailAsync(player.UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size180x180)
if not isFinal then
for i = 0, MAX_THUMBNAIL_RETRIES do
headshotUrl, isFinal = PlayersService:GetUserThumbnailAsync(player.UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size180x180)
if isFinal then
break
end
wait(i ^ 2)
end
end
self.HeadShotUrlCache[player] = headshotUrl
return headshotUrl
end
function ContextMenuUtil:HasOrGettingHeadShot(player)
return self.HeadShotUrlCache[player] ~= nil
end
function ContextMenuUtil:FindPlayerFromPart(part)
if part and part.Parent then
local possibleCharacter = part
while possibleCharacter and not possibleCharacter:IsA("Model") do
possibleCharacter = possibleCharacter.Parent
end
if possibleCharacter then
return PlayersService:GetPlayerFromCharacter(possibleCharacter)
end
end
return nil
end
function ContextMenuUtil:GetPlayerPosition(player)
if player.Character then
local hrp = player.Character:FindFirstChild("HumanoidRootPart")
if hrp then
return hrp.Position
end
end
return nil
end
local playerMovementEnabled = true
function ContextMenuUtil:DisablePlayerMovement()
if not playerMovementEnabled then return end
playerMovementEnabled = false
local noOpFunc = function(actionName, actionState)
if actionState == Enum.UserInputState.End then
return Enum.ContextActionResult.Pass
end
return Enum.ContextActionResult.Sink
end
ContextActionService:BindCoreAction(STOP_MOVEMENT_ACTION_NAME, noOpFunc, false,
Enum.PlayerActions.CharacterForward,
Enum.PlayerActions.CharacterBackward,
Enum.PlayerActions.CharacterLeft,
Enum.PlayerActions.CharacterRight,
Enum.PlayerActions.CharacterJump,
Enum.UserInputType.Gamepad1, Enum.UserInputType.Gamepad2, Enum.UserInputType.Gamepad3, Enum.UserInputType.Gamepad4
)
end
function ContextMenuUtil:EnablePlayerMovement()
if playerMovementEnabled then return end
playerMovementEnabled = true
ContextActionService:UnbindCoreAction(STOP_MOVEMENT_ACTION_NAME)
end
function ContextMenuUtil:GetFriendStatus(player)
local success, result = pcall(function()
-- NOTE: Core script only
return LocalPlayer:GetFriendStatus(player)
end)
if success then
return result
else
return Enum.FriendStatus.NotFriend
end
end
local SelectionOverrideObject = Instance.new("ImageLabel")
SelectionOverrideObject.Image = ""
SelectionOverrideObject.BackgroundTransparency = 1
local function MakeDefaultButton(name, size, clickFunc)
local button = Instance.new("ImageButton")
button.Name = name .. "Button"
button.Image = ""
button.ScaleType = Enum.ScaleType.Slice
button.SliceCenter = Rect.new(8,6,46,44)
button.AutoButtonColor = false
button.BackgroundTransparency = 1
button.Size = size
button.ZIndex = 2
button.SelectionImageObject = SelectionOverrideObject
button.BorderSizePixel = 0
local underline = Instance.new("Frame")
underline.Name = "Underline"
underline.BackgroundColor3 = Color3.fromRGB(137,137,137)
underline.AnchorPoint = Vector2.new(0.5,1)
underline.BorderSizePixel = 0
underline.Position = UDim2.new(0.5,0,1,0)
underline.Size = UDim2.new(0.95,0,0,1)
underline.Parent = button
if clickFunc then
button.MouseButton1Click:Connect(function()
clickFunc(UserInputService:GetLastInputType())
end)
end
local function isPointerInput(inputObject)
return inputObject.UserInputType == Enum.UserInputType.MouseMovement or inputObject.UserInputType == Enum.UserInputType.Touch
end
local function selectButton()
button.BackgroundTransparency = 0.5
end
local function deselectButton()
button.BackgroundTransparency = 1
end
button.InputBegan:Connect(function(inputObject)
if button.Selectable and isPointerInput(inputObject) then
selectButton()
inputObject:GetPropertyChangedSignal("UserInputState"):connect(function()
if inputObject.UserInputState == Enum.UserInputState.End then
deselectButton()
end
end)
end
end)
button.InputEnded:Connect(function(inputObject)
if button.Selectable and GuiService.SelectedCoreObject ~= button and isPointerInput(inputObject) then
deselectButton()
end
end)
button.SelectionGained:Connect(function()
selectButton()
end)
button.SelectionLost:Connect(function()
deselectButton()
end)
local guiServiceCon = GuiService.Changed:Connect(function(prop)
if prop ~= "SelectedCoreObject" then return end
if GuiService.SelectedCoreObject == nil or GuiService.SelectedCoreObject ~= button then
deselectButton()
return
end
if button.Selectable then
selectButton()
end
end)
return button
end
local function getViewportSize()
while not workspace.CurrentCamera do
workspace.Changed:wait()
end
-- ViewportSize is initally set to 1, 1 in Camera.cpp constructor.
-- Also check against 0, 0 incase this is changed in the future.
while workspace.CurrentCamera.ViewportSize == Vector2.new(0,0) or
workspace.CurrentCamera.ViewportSize == Vector2.new(1,1) do
workspace.CurrentCamera.Changed:wait()
end
return workspace.CurrentCamera.ViewportSize
end
local function isSmallTouchScreen()
local viewportSize = getViewportSize()
return UserInputService.TouchEnabled and (viewportSize.Y < 500 or viewportSize.X < 700)
end
function ContextMenuUtil:MakeStyledButton(name, text, size, clickFunc)
local button = MakeDefaultButton(name, size, clickFunc)
local textLabel = Instance.new("TextLabel")
textLabel.Name = name .. "TextLabel"
textLabel.BackgroundTransparency = 1
textLabel.BorderSizePixel = 0
textLabel.Size = UDim2.new(1, 0, 1, -8)
textLabel.Position = UDim2.new(0,0,0,0)
textLabel.TextColor3 = Color3.fromRGB(255,255,255)
textLabel.TextYAlignment = Enum.TextYAlignment.Center
textLabel.Font = Enum.Font.SourceSansLight
textLabel.TextSize = 24
textLabel.Text = text
textLabel.TextScaled = true
textLabel.TextWrapped = true
textLabel.ZIndex = 2
textLabel.Parent = button
local constraint = Instance.new("UITextSizeConstraint",textLabel)
if isSmallTouchScreen() then
textLabel.TextSize = 18
elseif GuiService:IsTenFootInterface() then
textLabel.TextSize = 36
end
constraint.MaxTextSize = textLabel.TextSize
return button, textLabel
end
function ContextMenuUtil.new()
local obj = setmetatable({}, ContextMenuUtil)
obj.HeadShotUrlCache = {}
return obj
end
return ContextMenuUtil.new()
@@ -0,0 +1,226 @@
--[[
// FileName: PlayerCarousel.lua
// Written by: darthskrill
// Description: Module for building the UI for the player selection carousel
]]
local PlayerCarousel = {}
PlayerCarousel.__index = PlayerCarousel
-- SERVICES
local GuiService = game:GetService("GuiService")
local CoreGuiService = game:GetService("CoreGui")
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")
-- CONSTANTS
local BACKGROUND_SELECTED_COLOR = Color3.fromRGB(0,162,255)
local BACKGROUND_DEFAULT_COLOR = Color3.fromRGB(0,0,0)
local PAGE_LAYOUT_TWEEN_TIME = 0.25
-- VARIABLES
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local AvatarMenuModules = CoreGuiModules:WaitForChild("AvatarContextMenu")
local selectedPlayer = nil
local uiPageLayout = nil
local playerChangedEvent = nil
local buttonToPlayerMap = {}
local playerToButtonMap = {}
local function CreateMenuCarousel()
local playerSelection = Instance.new("Frame")
playerSelection.Name = "PlayerCarousel"
playerSelection.AnchorPoint = Vector2.new(0.5, 0.5)
playerSelection.BackgroundTransparency = 1
playerSelection.Position = UDim2.new(0.5,0,0.5,0)
playerSelection.Size = UDim2.new(1, 0, 0.28, 0)
playerSelection.ClipsDescendants = true
local innerFrame = Instance.new("Frame")
innerFrame.Name = "InnerFrame"
innerFrame.AnchorPoint = Vector2.new(0.5, 0.5)
innerFrame.BackgroundTransparency = 1
innerFrame.Position = UDim2.new(0.5, 0, 0.5, 0)
innerFrame.Size = UDim2.new(0.8, 0, 1, 0)
innerFrame.ClipsDescendants = true
innerFrame.Active = true
innerFrame.Parent = playerSelection
selectedPlayer = Instance.new("Frame")
selectedPlayer.Name = "SelectedPlayer"
selectedPlayer.AnchorPoint = Vector2.new(0.5, 0.5)
selectedPlayer.BackgroundTransparency = 1
selectedPlayer.Position = UDim2.new(0.5, 0, 0.5, 0)
selectedPlayer.Size = UDim2.new(0, 100, 1, -10)
selectedPlayer.Parent = innerFrame
uiPageLayout = Instance.new("UIPageLayout")
uiPageLayout.EasingDirection = Enum.EasingDirection.Out
uiPageLayout.EasingStyle = Enum.EasingStyle.Quad
uiPageLayout.Padding = UDim.new(0, 5)
uiPageLayout.TweenTime = PAGE_LAYOUT_TWEEN_TIME
uiPageLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
uiPageLayout.VerticalAlignment = Enum.VerticalAlignment.Center
uiPageLayout.TouchInputEnabled = false
uiPageLayout.SortOrder = Enum.SortOrder.LayoutOrder
local aspectRatioConstraint = Instance.new("UIAspectRatioConstraint")
aspectRatioConstraint.DominantAxis = Enum.DominantAxis.Height
aspectRatioConstraint.Parent = selectedPlayer
playerChangedEvent = Instance.new("BindableEvent")
playerChangedEvent.Name = "PlayerChanged"
uiPageLayout:GetPropertyChangedSignal("CurrentPage"):Connect(function()
if uiPageLayout.CurrentPage then uiPageLayout.CurrentPage.BackgroundColor3 = BACKGROUND_DEFAULT_COLOR end
if GuiService.SelectedCoreObject and GuiService.SelectedCoreObject.Parent == uiPageLayout.Parent then
GuiService.SelectedCoreObject.BackgroundColor3 = BACKGROUND_SELECTED_COLOR
end
GuiService.SelectedCoreObject = uiPageLayout.CurrentPage
if uiPageLayout.CurrentPage then playerChangedEvent:Fire(buttonToPlayerMap[uiPageLayout.CurrentPage]) end
end)
uiPageLayout.Parent = selectedPlayer
local nextButton = Instance.new("ImageButton")
nextButton.Name = "NextButton"
nextButton.Image = "rbxassetid://471630112"
nextButton.BackgroundTransparency = 1
nextButton.AnchorPoint = Vector2.new(1,0.5)
nextButton.Position = UDim2.new(1,-5,0.5,0)
nextButton.Size = UDim2.new(0.3,0,0.3,0)
nextButton.Selectable = false
nextButton.Parent = playerSelection
local aspectRatioConstraint = Instance.new("UIAspectRatioConstraint")
aspectRatioConstraint.DominantAxis = Enum.DominantAxis.Width
aspectRatioConstraint.Parent = nextButton
local prevButton = nextButton:Clone()
prevButton.Name = "PrevButton"
prevButton.AnchorPoint = Vector2.new(0, 0.5)
prevButton.Position = UDim2.new(0, 5, 0.5, 0)
prevButton.Rotation = 180
prevButton.Selectable = false
prevButton.Parent = playerSelection
local function moveChangePage(goToNext)
if goToNext then
uiPageLayout:Next()
else
uiPageLayout:Previous()
end
end
nextButton.MouseButton1Click:Connect(function() moveChangePage(true) end)
prevButton.MouseButton1Click:Connect(function() moveChangePage(false) end)
local defaultChildrenSize = 3 -- aspectRatioConstraint, PageLayout, first button in pagelayout
local function checkButtonVisibility()
local lastInputIsTouch = UserInputService:GetLastInputType() == Enum.UserInputType.Touch
prevButton.Visible = not lastInputIsTouch and (#selectedPlayer:GetChildren() > defaultChildrenSize)
nextButton.Visible = not lastInputIsTouch and (#selectedPlayer:GetChildren() > defaultChildrenSize)
end
checkButtonVisibility()
UserInputService.LastInputTypeChanged:Connect(checkButtonVisibility)
return playerSelection
end
function PlayerCarousel:ClearPlayerEntries()
for button, player in pairs(buttonToPlayerMap) do
button:Destroy()
end
buttonToPlayerMap = {}
playerToButtonMap = {}
end
function PlayerCarousel:CreatePlayerEntry(player, distanceToLocalPlayer)
local playerButton = playerToButtonMap[player]
if playerButton then
playerButton.LayoutOrder = distanceToLocalPlayer
return
end
local button = Instance.new("ImageButton")
button.Name = player.Name
button.BorderSizePixel = 0
button.LayoutOrder = distanceToLocalPlayer
button.BackgroundColor3 = Color3.fromRGB(0,0,0)
button.BackgroundTransparency = 0
button.Size = UDim2.new(1, 0, 1, 0)
button.SelectionLost:connect(function() button.BackgroundColor3 = BACKGROUND_DEFAULT_COLOR end)
button.SelectionGained:connect(function() button.BackgroundColor3 = BACKGROUND_SELECTED_COLOR end)
local tweenStyle = TweenInfo.new(1, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, -1, true)
local buttonLoadingTween = TweenService:Create(button, tweenStyle, {BackgroundTransparency = 1, BackgroundColor3 = Color3.fromRGB(255,255,255)})
buttonLoadingTween:Play()
buttonToPlayerMap[button] = player
playerToButtonMap[player] = button
button.MouseButton1Click:Connect(function()
uiPageLayout:JumpTo(button)
end)
button.Parent = selectedPlayer
spawn(function()
local ContextMenuUtil = require(AvatarMenuModules:WaitForChild("ContextMenuUtil"))
button.Image = ContextMenuUtil:GetHeadshotForPlayer(player)
buttonLoadingTween:Cancel()
buttonLoadingTween = nil
button.BackgroundTransparency = 0
if button == GuiService.SelectedCoreObject then
button.BackgroundColor3 = BACKGROUND_SELECTED_COLOR
else
button.BackgroundColor3 = BACKGROUND_DEFAULT_COLOR
end
end)
end
function PlayerCarousel:SwitchToPlayerEntry(player, dontTween)
if not player then return end
local button = playerToButtonMap[player]
if not button then
self:CreatePlayerEntry(player, 0)
button = playerToButtonMap[player]
end
if dontTween then
uiPageLayout.TweenTime = 0
end
uiPageLayout:JumpTo(button)
spawn(function()
uiPageLayout.TweenTime = PAGE_LAYOUT_TWEEN_TIME
end)
end
function PlayerCarousel:OffsetPlayerEntry(offset)
if offset == 0 then return end
if offset > 0 then
uiPageLayout:Next()
else
uiPageLayout:Previous()
end
end
function PlayerCarousel:GetSelectedPlayer()
return buttonToPlayerMap[uiPageLayout.CurrentPage]
end
function PlayerCarousel.new()
local obj = setmetatable({}, PlayerCarousel)
obj.rbxGui = CreateMenuCarousel()
obj.PlayerChanged = playerChangedEvent.Event
return obj
end
return PlayerCarousel.new()
@@ -0,0 +1,102 @@
--[[
// FileName: SelectedCharacterIndicator.lua
// Written by: TheGamer101
// Description: Module for rendering an effect for the selected character .
]]
local SelectedCharacterIndicator = {}
SelectedCharacterIndicator.__index = SelectedCharacterIndicator
local RunService = game:GetService("RunService")
local RENDER_ARROW_CONTEXT_ACTION = "ContextActionMenuRenderArrow"
local CurrentCamera = workspace.CurrentCamera
function ApplyArrow(character)
local baseModel = Instance.new("Model")
baseModel.Name = "ContextMenuArrow"
baseModel.Parent = CurrentCamera
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid == nil then
humanoid = character:WaitForChild("Humanoid", 15)
if humanoid == nil then
return
end
end
local torso = character:WaitForChild("HumanoidRootPart")
local arrowPart = game:GetService("InsertService"):LoadLocalAsset("rbxasset://models/AvatarContextMenu/AvatarContextArrow.rbxm")
arrowPart.Anchored = true
arrowPart.Transparency = 0
arrowPart.CanCollide = false
arrowPart.Parent = baseModel
local arrowTween = game:GetService("TweenService"):Create(arrowPart,
TweenInfo.new(4, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, -1, false),
{Orientation=Vector3.new(0,360,180)})
arrowTween:Play()
local function update()
arrowPart.Position = torso.Position + Vector3.new(0, 5, 0)
end
local isKilled = false
local function kill()
if isKilled then
return
end
isKilled = true
baseModel:Destroy()
arrowTween:Destroy()
RunService:UnbindFromRenderStep(RENDER_ARROW_CONTEXT_ACTION)
end
humanoid.Died:Connect(kill)
character.AncestryChanged:Connect(kill)
RunService:BindToRenderStep(RENDER_ARROW_CONTEXT_ACTION, Enum.RenderPriority.Camera.Value + 1 , update)
baseModel.Parent = CurrentCamera
return kill
end
function SelectedCharacterIndicator:ChangeSelectedPlayer(selectedPlayer)
coroutine.wrap(function()
if self.SelectedPlayer then
self.SelectedPlayer = nil
self.CharacterAddedConn:Disconnect()
self.CharacterAddedConn = nil
if self.KillOldRenderFunction then
self.KillOldRenderFunction()
self.KillOldRenderFunction = nil
end
end
if selectedPlayer then
self.SelectedPlayer = selectedPlayer
self.CharacterAddedConn = selectedPlayer.CharacterAdded:Connect(function(character)
if self.KillOldRenderFunction then
self.KillOldRenderFunction()
end
self.KillOldRenderFunction = ApplyArrow(character)
end)
if selectedPlayer.Character then
self.KillOldRenderFunction = ApplyArrow(selectedPlayer.Character)
end
end
end)()
end
function SelectedCharacterIndicator.new()
local obj = setmetatable({}, SelectedCharacterIndicator)
obj.KillOldRenderFunction = nil
obj.SelectedPlayer = nil
obj.CharacterAddedConn = nil
return obj
end
return SelectedCharacterIndicator.new()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,521 @@
--BackpackScript3D: VR port of backpack interface using a 3D panel
--written by 0xBAADF00D
local ICON_SIZE = 48
local ICON_SPACING = 52
local PIXELS_PER_STUD = 64
local SLOT_BORDER_SIZE = 0
local SLOT_BORDER_SELECTED_SIZE = 4
local SLOT_BORDER_COLOR = Color3.new(90/255, 142/255, 233/255)
local SLOT_BACKGROUND_COLOR = Color3.new(0.2, 0.2, 0.2)
local SLOT_HOVER_BACKGROUND_COLOR = Color3.new(90/255, 90/255, 90/255)
local HOPPERBIN_ANGLE = math.rad(-45)
local HOPPERBIN_ROTATION = CFrame.Angles(HOPPERBIN_ANGLE, 0, 0)
local HOPPERBIN_OFFSET = Vector3.new(0, 0, -5)
local HEALTHBAR_SPACE = 12
local HEALTHBAR_WIDTH = 82
local HEALTHBAR_HEIGHT = 5
local NAME_SPACE = 14
local Tools = {}
local ToolsList = {}
local slotIcons = {}
local BackpackScript = {}
local topbarEnabled = false
local player = game:GetService("Players").LocalPlayer
local currentHumanoid = nil
local CoreGui = game:GetService('CoreGui')
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Panel3D = require(RobloxGui.Modules.VR.Panel3D)
local Util = require(RobloxGui.Modules.Settings.Utility)
local ContextActionService = game:GetService("ContextActionService")
local BackpackPanel = Panel3D.Get("Backpack")
BackpackPanel:ResizeStuds(5, 2)
BackpackPanel:SetType(Panel3D.Type.Fixed, { CFrame = CFrame.new(0, 0, -5) })
BackpackPanel:SetVisible(true)
local toolsFrame = Instance.new("TextButton", BackpackPanel:GetGUI()) --prevent clicks falling through in case you have a rocket launcher and blow yourself up
toolsFrame.Text = ""
toolsFrame.Size = UDim2.new(1, 0, 0, ICON_SIZE)
toolsFrame.BackgroundTransparency = 1
toolsFrame.Selectable = false
local insetAdjustY = toolsFrame.AbsolutePosition.Y
toolsFrame.Position = UDim2.new(0, 0, 0, HEALTHBAR_SPACE + NAME_SPACE)
--Healthbar color function stolen from Topbar.lua
local HEALTH_BACKGROUND_COLOR = Color3.new(228/255, 236/255, 246/255)
local HEALTH_RED_COLOR = Color3.new(255/255, 28/255, 0/255)
local HEALTH_YELLOW_COLOR = Color3.new(250/255, 235/255, 0)
local HEALTH_GREEN_COLOR = Color3.new(27/255, 252/255, 107/255)
local healthbarBack = Instance.new("ImageLabel", BackpackPanel:GetGUI())
healthbarBack.ImageColor3 = HEALTH_BACKGROUND_COLOR
healthbarBack.BackgroundTransparency = 1
healthbarBack.ScaleType = Enum.ScaleType.Slice
healthbarBack.SliceCenter = Rect.new(10, 10, 10, 10)
healthbarBack.Name = "HealthbarContainer"
healthbarBack.Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png"
local healthbarFront = Instance.new("ImageLabel", healthbarBack)
healthbarFront.ImageColor3 = HEALTH_GREEN_COLOR
healthbarFront.BackgroundTransparency = 1
healthbarFront.ScaleType = Enum.ScaleType.Slice
healthbarFront.SliceCenter = Rect.new(10, 10, 10, 10)
healthbarFront.Size = UDim2.new(1, 0, 1, 0)
healthbarFront.Position = UDim2.new(0, 0, 0, 0)
healthbarFront.Name = "HealthbarFill"
healthbarFront.Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png"
local playerName = Instance.new("TextLabel", BackpackPanel:GetGUI())
playerName.Name = "PlayerName"
playerName.BackgroundTransparency = 1
playerName.TextColor3 = Color3.new(1, 1, 1)
playerName.Text = player.Name
playerName.Font = Enum.Font.SourceSansBold
playerName.FontSize = Enum.FontSize.Size12
playerName.TextXAlignment = Enum.TextXAlignment.Left
playerName.Size = UDim2.new(1, 0, 0, NAME_SPACE)
BackpackScript.ToolAddedEvent = Instance.new("BindableEvent")
local healthColorToPosition = {
[Vector3.new(HEALTH_RED_COLOR.r, HEALTH_RED_COLOR.g, HEALTH_RED_COLOR.b)] = 0.1;
[Vector3.new(HEALTH_YELLOW_COLOR.r, HEALTH_YELLOW_COLOR.g, HEALTH_YELLOW_COLOR.b)] = 0.5;
[Vector3.new(HEALTH_GREEN_COLOR.r, HEALTH_GREEN_COLOR.g, HEALTH_GREEN_COLOR.b)] = 0.8;
}
local min = 0.1
local minColor = HEALTH_RED_COLOR
local max = 0.8
local maxColor = HEALTH_GREEN_COLOR
local function HealthbarColorTransferFunction(healthPercent)
if healthPercent < min then
return minColor
elseif healthPercent > max then
return maxColor
end
-- Shepard's Interpolation
local numeratorSum = Vector3.new(0,0,0)
local denominatorSum = 0
for colorSampleValue, samplePoint in pairs(healthColorToPosition) do
local distance = healthPercent - samplePoint
if distance == 0 then
-- If we are exactly on an existing sample value then we don't need to interpolate
return Color3.new(colorSampleValue.x, colorSampleValue.y, colorSampleValue.z)
else
local wi = 1 / (distance*distance)
numeratorSum = numeratorSum + wi * colorSampleValue
denominatorSum = denominatorSum + wi
end
end
local result = numeratorSum / denominatorSum
return Color3.new(result.x, result.y, result.z)
end
---
local backpackEnabled = true
local healthbarEnabled = true
local function UpdateLayout()
local width, height = 100, 100
local borderSize = (ICON_SPACING - ICON_SIZE) / 2
local x = borderSize
local y = 0
for _, tool in ipairs(ToolsList) do
local slot = Tools[tool]
if slot then
slot.icon.Position = UDim2.new(0, x, 0, y)
x = x + ICON_SPACING
end
end
if #ToolsList == 0 then
width = HEALTHBAR_WIDTH
height = HEALTHBAR_SPACE + NAME_SPACE
BackpackPanel.showCursor = false
else
width = #ToolsList * ICON_SPACING
height = ICON_SIZE + HEALTHBAR_SPACE + NAME_SPACE
BackpackPanel.showCursor = true
end
BackpackPanel:ResizePixels(width, height)
playerName.Position = UDim2.new(0, borderSize, 0, 0)
healthbarBack.Position = UDim2.new(0, borderSize, 0, NAME_SPACE + (HEALTHBAR_SPACE - HEALTHBAR_HEIGHT) / 2)
healthbarBack.Size = UDim2.new(0, HEALTHBAR_WIDTH, 0, HEALTHBAR_HEIGHT)
end
local function UpdateHealth(humanoid)
local percentHealth = humanoid.Health / humanoid.MaxHealth
if percentHealth ~= percentHealth then
percentHealth = 1
end
healthbarFront.BackgroundColor3 = HealthbarColorTransferFunction(percentHealth)
healthbarFront.Size = UDim2.new(percentHealth, 0, 1, 0)
end
local function SetTransparency(transparency)
for i, v in pairs(Tools) do
v.bg.ImageTransparency = transparency
v.image.ImageTransparency = transparency
v.text.TextTransparency = transparency
end
playerName.TextTransparency = transparency
healthbarBack.ImageTransparency = transparency
healthbarFront.ImageTransparency = transparency
end
local function OnHotbarEquipPrimary(actionName, state, obj)
if state ~= Enum.UserInputState.Begin then
return
end
for tool, slot in pairs(Tools) do
if slot.hovered then
slot.OnClick()
return
end
end
end
local function EnableHotbarInput(enable)
if not backpackEnabled then
enable = false
end
if not currentHumanoid then
return
end
if enable then
ContextActionService:BindCoreAction("HotbarEquipPrimary", OnHotbarEquipPrimary, false, Enum.KeyCode.ButtonA, Enum.KeyCode.ButtonR2, Enum.UserInputType.MouseButton1)
else
ContextActionService:UnbindCoreAction("HotbarEquipPrimary")
end
end
local function AddTool(tool)
if Tools[tool] then
return
end
local slot = {}
Tools[tool] = slot
table.insert(ToolsList, tool)
slot.hovered = false
slot.tool = tool
slot.icon = Instance.new("TextButton", toolsFrame)
slot.icon.Text = ""
slot.icon.Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE)
slot.icon.BackgroundColor3 = Color3.new(0, 0, 0)
slot.icon.Selectable = true
slot.icon.BackgroundTransparency = 1
slotIcons[tool] = slot.icon
slot.bg = Instance.new("ImageLabel", slot.icon)
slot.bg.Position = UDim2.new(0, -1, 0, -1)
slot.bg.Size = UDim2.new(1, 2, 1, 2)
slot.bg.Image = "rbxasset://textures/ui/VR/rectBackground.png"
slot.bg.ScaleType = Enum.ScaleType.Slice
slot.bg.SliceCenter = Rect.new(10, 10, 10, 10)
slot.bg.BackgroundTransparency = 1
slot.image = Instance.new("ImageLabel", slot.icon)
slot.image.Position = UDim2.new(0, 1, 0, 1)
slot.image.Size = UDim2.new(1, -2, 1, -2)
slot.image.BackgroundTransparency = 1
slot.image.Selectable = false
slot.text = Instance.new("TextLabel", slot.icon)
slot.text.Position = UDim2.new(0, 1, 0, 1)
slot.text.Size = UDim2.new(1, -2, 1, -2)
slot.text.BackgroundTransparency = 1
slot.text.TextColor3 = Color3.new(1, 1, 1)
slot.text.Font = Enum.Font.SourceSans
slot.text.FontSize = Enum.FontSize.Size12
slot.text.ClipsDescendants = true
slot.text.Selectable = false
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;
}
slot.icon.SelectionImageObject = selectionObject
local function updateToolData()
slot.image.Image = tool.TextureId
slot.text.Text = tool.TextureId == "" and tool.Name or ""
end
updateToolData()
slot.OnClick = function()
if not player.Character then return end
local humanoid = player.Character:FindFirstChild("Humanoid")
if not humanoid then return end
local inBackpack = tool.Parent == player.Backpack
humanoid:UnequipTools()
if inBackpack then
humanoid:EquipTool(tool)
end
end
slot.icon.MouseButton1Click:connect(slot.OnClick)
slot.OnEnter = function()
slot.hovered = true
end
slot.OnLeave = function()
slot.hovered = false
end
-- slot.icon.MouseEnter:connect(slot.OnEnter)
-- slot.icon.MouseLeave:connect(slot.OnLeave)
tool.Changed:connect(function(prop)
if prop == "Parent" then
if tool.Parent == player:FindFirstChild("Backpack") then
slot.bg.Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE) --temporary hold-over until new backpack design comes along (can't use border with this antialiased frame stand-in)
slot.bg.Position = UDim2.new(0, 0, 0, 0)
elseif tool.Parent == player.Character then
slot.bg.Size = UDim2.new(0, ICON_SIZE + 8, 0, ICON_SIZE + 8)
slot.bg.Position = UDim2.new(0, -4, 0, -4)
end
elseif prop == "TextureId" or prop == "Name" then
updateToolData()
end
end)
UpdateLayout()
BackpackScript.ToolAddedEvent:Fire(tool)
end
local humanoidChangedEvent = nil
local humanoidAncestryChangedEvent = nil
local function RegisterHumanoid(humanoid)
currentHumanoid = humanoid
if humanoidChangedEvent then
humanoidChangedEvent:disconnect()
humanoidChangedEvent = nil
end
if humanoidAncestryChangedEvent then
humanoidAncestryChangedEvent:disconnect()
humanoidAncestryChangedEvent = nil
end
if humanoid then
humanoidChangedEvent = humanoid.HealthChanged:connect(function() UpdateHealth(humanoid) end)
humanoidAncestryChangedEvent = humanoid.AncestryChanged:connect(function(child, parent)
if child == humanoid and parent ~= player.Character then
RegisterHumanoid(nil)
end
end)
UpdateHealth(humanoid)
end
end
local function OnChildAdded(child)
if child:IsA("Tool") or child:IsA("HopperBin") then
AddTool(child)
end
if child:IsA("Humanoid") and child.Parent == player.Character then
RegisterHumanoid(child)
end
end
local function RemoveTool(tool)
if not Tools[tool] then
return
end
Tools[tool].icon:Destroy()
for i, v in ipairs(ToolsList) do
if v == tool then
table.remove(ToolsList, i)
break
end
end
Tools[tool] = nil
slotIcons[tool] = nil
UpdateLayout()
end
local function OnChildRemoved(child)
if child:IsA("Tool") or child:IsA("HopperBin") then
if Tools[child] then
if child.Parent ~= player:FindFirstChild("Backpack") and child.Parent ~= player.Character then
RemoveTool(child)
end
end
end
end
local function OnCharacterAdded(character)
local backpack = player:WaitForChild("Backpack")
for i, v in ipairs(character:GetChildren()) do
if v:IsA("Humanoid") then
RegisterHumanoid(v)
break
end
end
for tool, v in pairs(Tools) do
RemoveTool(tool)
end
Tools = {}
ToolsList = {}
character.ChildAdded:connect(OnChildAdded)
character.ChildRemoved:connect(OnChildRemoved)
for i, v in ipairs(backpack:GetChildren()) do
OnChildAdded(v)
end
backpack.ChildAdded:connect(OnChildAdded)
backpack.ChildRemoved:connect(OnChildRemoved)
end
player.CharacterAdded:connect(OnCharacterAdded)
if player.Character then
spawn(function() OnCharacterAdded(player.Character) end)
end
local function OnHotbarEquip(actionName, state, obj)
if not backpackEnabled then
return
end
local character = player.Character
if not character then
return
end
if not currentHumanoid then
return
end
if state ~= Enum.UserInputState.Begin then
return
end
if #ToolsList == 0 then
return
end
local current = 0
for i, v in pairs(ToolsList) do
if v.Parent == character then
current = i
end
end
currentHumanoid:UnequipTools()
if obj.KeyCode == Enum.KeyCode.ButtonR1 then
current = current + 1
if current > #ToolsList then
current = 1
end
else
current = current - 1
if current < 1 then
current = #ToolsList
end
end
currentHumanoid:EquipTool(ToolsList[current])
end
local function OnCoreGuiChanged(coreGuiType, enabled)
-- Check for enabling/disabling the whole thing
if coreGuiType == Enum.CoreGuiType.Backpack or coreGuiType == Enum.CoreGuiType.All then
backpackEnabled = enabled
UpdateLayout()
if enabled then
ContextActionService:BindCoreAction("HotbarEquip2", OnHotbarEquip, false, Enum.KeyCode.ButtonL1, Enum.KeyCode.ButtonR1)
toolsFrame.Parent = BackpackPanel:GetGUI()
else
ContextActionService:UnbindCoreAction("HotbarEquip2")
toolsFrame.Parent = nil
end
end
if coreGuiType == Enum.CoreGuiType.Health or coreGuiType == Enum.CoreGuiType.All then
healthbarEnabled = enabled
UpdateLayout()
if enabled then
healthbarBack.Parent = BackpackPanel:GetGUI()
else
healthbarBack.Parent = nil
end
end
end
local StarterGui = game:GetService("StarterGui")
StarterGui.CoreGuiChangedSignal:connect(OnCoreGuiChanged)
OnCoreGuiChanged(Enum.CoreGuiType.Backpack, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Backpack))
OnCoreGuiChanged(Enum.CoreGuiType.Backpack, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.All))
OnCoreGuiChanged(Enum.CoreGuiType.Health, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Health))
OnCoreGuiChanged(Enum.CoreGuiType.Health, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.All))
local panelLocalCF = CFrame.Angles(math.rad(-5), 0, 0) * CFrame.new(0, 1.75, 0) * CFrame.Angles(math.rad(-5), 0, 0)
function BackpackPanel:PreUpdate(cameraCF, cameraRenderCF, userHeadCF, lookRay)
--the backpack panel needs to go in front of the user when they look at it.
--if they aren't looking, we should be updating self.localCF
local topbarPanel = Panel3D.Get("Topbar3D")
local panelOriginCF = topbarPanel.localCF or CFrame.new()
self.localCF = panelOriginCF * panelLocalCF
end
function BackpackPanel:OnUpdate()
SetTransparency(self.transparency)
local hovered, tool = BackpackPanel:FindHoveredGuiElement(slotIcons)
if hovered and tool then
local slot = Tools[tool]
if not slot.hovered then
slot.OnEnter()
end
for i, v in pairs(Tools) do
if v.hovered and v ~= slot then
v.OnLeave()
end
end
end
end
function BackpackPanel:OnMouseEnter(x, y)
EnableHotbarInput(true)
end
function BackpackPanel:OnMouseLeave(x, y)
EnableHotbarInput(false)
end
local VRHub = require(RobloxGui.Modules.VR.VRHub)
VRHub.ModuleOpened.Event:connect(function(moduleName)
local module = VRHub:GetModule(moduleName)
if module.VRIsExclusive then
BackpackPanel:SetVisible(false)
end
end)
VRHub.ModuleClosed.Event:connect(function(moduleName)
BackpackPanel:SetVisible(true)
end)
BackpackPanel:LinkTo("Topbar3D")
return BackpackScript
@@ -0,0 +1,19 @@
local BusinessLogic = {}
function BusinessLogic.GetVisibleAgeForPlayer(player)
local accountTypeText = "Account: <13"
if player and not player:GetUnder13() then
accountTypeText = "Account: 13+"
end
return accountTypeText
end
return BusinessLogic
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,191 @@
--[[
// FileName: ChatSelector.lua
// Written by: Xsitsu
// Description: Code for determining which chat version to use in game.
]]
local FORCE_IS_CONSOLE = false
local FORCE_IS_VR = false
local CoreGuiService = game:GetService("CoreGui")
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local Modules = RobloxGui:WaitForChild("Modules")
local Common = Modules:WaitForChild("Common")
local StarterGui = game:GetService("StarterGui")
local UserInputService = game:GetService("UserInputService")
local GuiService = game:GetService("GuiService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local Util = require(RobloxGui.Modules.ChatUtil)
local ClassicChatEnabled = Players.ClassicChat
local BubbleChatEnabled = Players.BubbleChat
local useModule = nil
local state = {Visible = true}
local interface = {}
do
function interface:ToggleVisibility()
if (useModule) then
useModule:ToggleVisibility()
else
state.Visible = not state.Visible
end
end
function interface:SetVisible(visible)
if (useModule) then
useModule:SetVisible(visible)
else
state.Visible = visible
end
end
function interface:FocusChatBar()
if (useModule) then
useModule:FocusChatBar()
end
end
function interface:GetVisibility()
if (useModule) then
return useModule:GetVisibility()
else
return state.Visible
end
end
function interface:GetMessageCount()
if (useModule) then
return useModule:GetMessageCount()
else
return 0
end
end
function interface:TopbarEnabledChanged(...)
if (useModule) then
return useModule:TopbarEnabledChanged(...)
end
end
function interface:IsFocused(useWasFocused)
if (useModule) then
return useModule:IsFocused(useWasFocused)
else
return false
end
end
function interface:ClassicChatEnabled()
if useModule then
return useModule:ClassicChatEnabled()
else
return ClassicChatEnabled
end
end
function interface:IsBubbleChatOnly()
if useModule then
return useModule:IsBubbleChatOnly()
end
return BubbleChatEnabled and not ClassicChatEnabled
end
function interface:IsDisabled()
if useModule then
return useModule:IsDisabled()
end
return not (BubbleChatEnabled or ClassicChatEnabled)
end
interface.ChatBarFocusChanged = Util.Signal()
interface.VisibilityStateChanged = Util.Signal()
interface.MessagesChanged = Util.Signal()
-- Signals that are called when we get information on if Bubble Chat and Classic chat are enabled from the chat.
interface.BubbleChatOnlySet = Util.Signal()
interface.ChatDisabled = Util.Signal()
end
local StopQueueingSystemMessages = false
local MakeSystemMessageQueue = {}
local function MakeSystemMessageQueueingFunction(data)
if (StopQueueingSystemMessages) then return end
table.insert(MakeSystemMessageQueue, data)
end
local function NonFunc() end
StarterGui:RegisterSetCore("ChatMakeSystemMessage", MakeSystemMessageQueueingFunction)
StarterGui:RegisterSetCore("ChatWindowPosition", NonFunc)
StarterGui:RegisterGetCore("ChatWindowPosition", NonFunc)
StarterGui:RegisterSetCore("ChatWindowSize", NonFunc)
StarterGui:RegisterGetCore("ChatWindowSize", NonFunc)
StarterGui:RegisterSetCore("ChatBarDisabled", NonFunc)
StarterGui:RegisterGetCore("ChatBarDisabled", NonFunc)
StarterGui:RegisterGetCore("ChatActive", function()
return interface:GetVisibility()
end)
StarterGui:RegisterSetCore("ChatActive", function(visible)
return interface:SetVisible(visible)
end)
local function ConnectSignals(useModule, interface, sigName)
--// "MessagesChanged" event is not created for Studio Start Server
if (useModule[sigName]) then
useModule[sigName]:connect(function(...) interface[sigName]:fire(...) end)
end
end
local isConsole = GuiService:IsTenFootInterface() or FORCE_IS_CONSOLE
local isVR = UserInputService.VREnabled or FORCE_IS_VR
if ( not isConsole and not isVR ) then
spawn(function()
useModule = require(RobloxGui.Modules.NewChat)
ConnectSignals(useModule, interface, "ChatBarFocusChanged")
ConnectSignals(useModule, interface, "VisibilityStateChanged")
ConnectSignals(useModule, interface, "BubbleChatOnlySet")
ConnectSignals(useModule, interface, "ChatDisabled")
while Players.LocalPlayer == nil do Players.ChildAdded:wait() end
local LocalPlayer = Players.LocalPlayer
ConnectSignals(useModule, interface, "MessagesChanged")
-- Retained for legacy reasons, no longer used by the chat scripts.
StarterGui:RegisterGetCore("UseNewLuaChat", function() return true end)
useModule:SetVisible(state.Visible)
StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Chat, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Chat))
StopQueueingSystemMessages = true
for i, messageData in pairs(MakeSystemMessageQueue) do
pcall(function() StarterGui:SetCore("ChatMakeSystemMessage", messageData) end)
end
end)
elseif not isConsole then
useModule = require(RobloxGui.Modules.Chat)
ConnectSignals(useModule, interface, "ChatBarFocusChanged")
ConnectSignals(useModule, interface, "VisibilityStateChanged")
while Players.LocalPlayer == nil do Players.ChildAdded:wait() end
local LocalPlayer = Players.LocalPlayer
if (LocalPlayer.ChatMode == Enum.ChatMode.TextAndMenu or RunService:IsStudio()) then
ConnectSignals(useModule, interface, "MessagesChanged")
end
StarterGui:RegisterGetCore("UseNewLuaChat", function() return false end)
end
return interface
@@ -0,0 +1,34 @@
local Util = {}
do
function Util.Signal()
local sig = {}
local mSignaler = Instance.new('BindableEvent')
local mArgData = nil
local mArgDataCount = nil
function sig:fire(...)
mArgData = {...}
mArgDataCount = select('#', ...)
mSignaler:Fire()
end
function sig:connect(f)
if not f then error("connect(nil)", 2) end
return mSignaler.Event:connect(function()
f(unpack(mArgData, 1, mArgDataCount))
end)
end
function sig:wait()
mSignaler.Event:wait()
assert(mArgData, "Missing arg data, likely due to :TweenSize/Position corrupting threadrefs.")
return unpack(mArgData, 1, mArgDataCount)
end
return sig
end
end
return Util
@@ -0,0 +1,61 @@
--[[
Filename: CommonUtil.lua
Written by: dbanks
Description: Common work.
--]]
--[[ Classes ]]--
local CommonUtil = {}
-- Concatenate these two tables, return result.
function CommonUtil.TableConcat(t1,t2)
for i=1,#t2 do
t1[#t1+1] = t2[i]
end
return t1
end
-- Instances have a "Name" field. Sort
-- by that name,
function CommonUtil.SortByName(items)
local function compareInstanceNames(i1, i2)
return (i1.Name < i2.Name)
end
table.sort(items, compareInstanceNames)
return items
end
-- Provides a nice syntax for creating Roblox instances.
-- Example:
-- local newPart = Utility.Create("Part") {
-- Parent = workspace,
-- Anchored = true,
--
-- --Create a SpecialMesh as a child of this part too
-- Utility.Create("SpecialMesh") {
-- MeshId = "rbxassetid://...",
-- Scale = Vector3.new(0.5, 0.2, 10)
-- }
-- }
function CommonUtil.Create(instanceType)
return function(data)
local obj = Instance.new(instanceType)
local parent = nil
for k, v in pairs(data) do
if type(k) == 'number' then
v.Parent = obj
elseif k == 'Parent' then
parent = v
else
obj[k] = v
end
end
if parent then
obj.Parent = parent
end
return obj
end
end
return CommonUtil
@@ -0,0 +1,38 @@
-- universal design constants for in-game ui style
local Constants = {
COLORS = {
SLATE = Color3.fromRGB(35, 37, 39),
FLINT = Color3.fromRGB(57, 59, 61),
GRAPHITE = Color3.fromRGB(101, 102, 104),
PUMICE = Color3.fromRGB(189, 190, 190),
WHITE = Color3.fromRGB(255, 255, 255),
},
ERROR_PROMPT_HEIGHT = {
Default = 236,
XBox = 180,
},
ERROR_PROMPT_MIN_WIDTH = {
Default = 320,
XBox = 400,
},
ERROR_PROMPT_MAX_WIDTH = {
Default = 400,
XBox = 400,
},
ERROR_TITLE_FRAME_HEIGHT = {
Default = 50,
},
SPLIT_LINE_THICKNESS = 1,
BUTTON_CELL_PADDING = 10,
BUTTON_HEIGHT = 36,
SIDE_PADDING = 20,
LAYOUT_PADDING = 20,
SIDE_MARGIN = 20, -- When resizing according to screen size, reserve with side margins
PRIMARY_BUTTON_TEXTURE = "rbxasset://textures/ui/ErrorPrompt/PrimaryButton.png",
SECONDARY_BUTTON_TEXTURE = "rbxasset://textures/ui/ErrorPrompt/SecondaryButton.png",
SHIMMER_TEXTURE = "rbxasset://textures/ui/LuaApp/graphic/shimmer.png",
OVERLAY_TEXTURE = "rbxasset://textures/ui/ErrorPrompt/ShimmerOverlay.png",
}
return Constants
@@ -0,0 +1,33 @@
return function(className, defaultParent)
return function(propertyList)
local object = Instance.new(className)
local parent = nil
for index, value in next, propertyList do
if typeof(index) == 'string' then
if index == 'Parent' then
parent = value
else
object[index] = value
end
else
local valueType = typeof(value)
if valueType == 'function' then
value(object)
elseif valueType == 'Instance' then
value.Parent = object
end
end
end
if parent then
object.Parent = parent
end
if object.Parent == nil then
object.Parent = defaultParent
end
return object
end
end
@@ -0,0 +1,51 @@
-- // FileName: ObjectPool.lua
-- // Written by: TheGamer101
-- // Description: An object pool class used to avoid unnecessarily instantiating Instances.
local module = {}
--////////////////////////////// Include
--//////////////////////////////////////
local modulesFolder = script.Parent
--////////////////////////////// Methods
--//////////////////////////////////////
local methods = {}
methods.__index = methods
function methods:GetInstance(className)
if self.InstancePoolsByClass[className] == nil then
self.InstancePoolsByClass[className] = {}
end
local availableInstances = #self.InstancePoolsByClass[className]
if availableInstances > 0 then
local instance = self.InstancePoolsByClass[className][availableInstances]
table.remove(self.InstancePoolsByClass[className])
return instance
end
return Instance.new(className)
end
function methods:ReturnInstance(instance)
if self.InstancePoolsByClass[instance.ClassName] == nil then
self.InstancePoolsByClass[instance.ClassName] = {}
end
if #self.InstancePoolsByClass[instance.ClassName] < self.PoolSizePerType then
table.insert(self.InstancePoolsByClass[instance.ClassName], instance)
else
instance:Destroy()
end
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new(poolSizePerType)
local obj = setmetatable({}, methods)
obj.InstancePoolsByClass = {}
obj.Name = "ObjectPool"
obj.PoolSizePerType = poolSizePerType
return obj
end
return module
@@ -0,0 +1,68 @@
--[[
A helper function to define a Rodux action creator with an associated name.
Normally when creating a Rodux action, you can just create a function:
return function(value)
return {
type = "MyAction",
value = value,
}
end
And then when you check for it in your reducer, you either use a constant,
or type out the string name:
if action.type == "MyAction" then
-- change some state
end
Typos here are a remarkably common bug. We also have the issue that there's
no link between reducers and the actions that they respond to!
`Action` (this helper) provides a utility that makes this a bit cleaner.
Instead, define your Rodux action like this:
return Action("MyAction", function(value)
return {
value = value,
}
end)
We no longer need to add the `type` field manually.
Additionally, the returned action creator now has a 'name' property that can
be checked by your reducer:
local MyAction = require(Reducers.MyAction)
...
if action.type == MyAction.name then
-- change some state!
end
Now we have a clear link between our reducers and the actions they use, and
if we ever typo a name, we'll get a warning in LuaCheck as well as an error
at runtime!
]]
return function(name, fn)
assert(type(name) == "string", "A name must be provided to create an Action")
assert(type(fn) == "function", "A function must be provided to create an Action")
return setmetatable({
name = name,
}, {
__call = function(self, ...)
local result = fn(...)
assert(type(result) == "table", "An action must return a table")
result.type = name
return result
end
})
end
@@ -0,0 +1,7 @@
local Action = require(script.Parent.Parent.Action)
return Action("ActionBindingsUpdateSearchFilter", function(searchTerm)
return {
searchTerm = searchTerm
}
end)
@@ -0,0 +1,7 @@
local Action = require(script.Parent.Parent.Action)
return Action("ChangeDevConsoleSize", function(newSize)
return {
newSize = newSize
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ClientLogUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ClientMemoryUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ClientNetworkUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ClientScriptsUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("DataStoresUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ServerJobsUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ServerLogUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ServerMemoryUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,9 @@
local Action = require(script.Parent.Parent.Action)
return Action("ServerNetworkUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ServerScriptsUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ServerStatsUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("SetActiveTab", function(tabListIndex, isClientView)
return {
newTabIndex = tabListIndex,
isClientView = isClientView
}
end)
@@ -0,0 +1,7 @@
local Action = require(script.Parent.Parent.Action)
return Action("SetDevConsoleMinimized", function(minimize)
return {
isMinimized = minimize
}
end)
@@ -0,0 +1,7 @@
local Action = require(script.Parent.Parent.Action)
return Action("SetDevConsolePosition", function(pos)
return {
position = pos
}
end)
@@ -0,0 +1,7 @@
local Action = require(script.Parent.Parent.Action)
return Action("SetDevConsoleVisibility", function(visibility)
return {
isVisible = visibility
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("SetTabList", function(tabList, initIndex)
return {
tabList = tabList,
initIndex = initIndex,
}
end)
@@ -0,0 +1,7 @@
local Action = require(script.Parent.Parent.Action)
return Action("UpdateAveragePing", function(newAveragePing)
return {
AveragePing = newAveragePing
}
end)
@@ -0,0 +1,151 @@
local CircularBuffer = {}
CircularBuffer.__index = CircularBuffer
function CircularBuffer.new(size)
assert(size, "Cannot initialize CircularBuffer with nil")
assert(size > 0, "Cannot initialize CircularBuffer to size < 1")
local self = {}
setmetatable(self, CircularBuffer)
self._data = {}
self._backIndex = 0
self._maxSize = size
return self
end
function CircularBuffer:reset()
self._data = {}
self._backIndex = 0
end
function CircularBuffer:getSize()
return #self._data
end
function CircularBuffer:getMaxSize()
return self._maxSize
end
function CircularBuffer:setSize(newSize)
assert(newSize, "Cannot set CircularBuffer with nil")
assert(newSize > 0, "Cannot set CircularBuffer to size < 1")
if newSize == self._maxSize then
return
end
local it = self:iterator()
local msg = it:next()
local sorted = {}
local ind = 0
while msg and ind < newSize do
local nextInd = ind + 1
sorted[nextInd] = {
entry = msg
}
if sorted[ind] then
sorted[ind]._next = sorted[nextInd]
end
ind = nextInd
msg = it:next()
end
self._data = sorted
self._backIndex = ind
self._maxSize = newSize
end
function CircularBuffer:getFrontIndex()
local front = self._backIndex + 1
if not self._data[front] then
return 1
end
return front
end
function CircularBuffer:front()
local front = self:getFrontIndex()
if self._data[front] then
return self._data[front].entry
end
return nil
end
function CircularBuffer:back()
if self._data[self._backIndex] then
return self._data[self._backIndex].entry
end
return nil
end
function CircularBuffer:iterator()
local front = self._data[self:getFrontIndex()]
local iterator = {
data = front,
next = function (self)
local retVal = self.data
if retVal then
self.data = self.data._next
end
return retVal and retVal.entry
end
}
return iterator
end
function CircularBuffer:getData()
return self._data
end
function CircularBuffer:at(ind)
assert(ind, "Cannot index CircularBuffer with nil")
local index = self:getFrontIndex()
index = (index + ind - 2) % self._maxSize + 1
if self._data[index] then
return self._data[index].entry
end
return nil
end
function CircularBuffer:reverseAt(ind)
local index = self._backIndex
index = (index - ind) % self._maxSize + 1
if self._data[index] then
return self._data[index].entry
end
return nil
end
-- returns the ejected element if newData overwrites
-- the previous front element
function CircularBuffer:push_back(newData)
local currBackIndex = self._backIndex
local newBackIndex = self._backIndex + 1
if newBackIndex > self._maxSize then
newBackIndex = 1
end
local overwrittenData = self._data[newBackIndex]
self._data[newBackIndex] = {
entry = newData
}
if currBackIndex > 0 then
self._data[currBackIndex]._next = self._data[newBackIndex]
if overwrittenData then
overwrittenData._next = nil
end
end
self._backIndex = newBackIndex
return overwrittenData and overwrittenData.entry
end
return CircularBuffer
@@ -0,0 +1,144 @@
return function()
local CircularBuffer = require(script.Parent.CircularBuffer)
it("should not allow initialize to size 0", function()
expect(function()
local buffer = CircularBuffer.new(0)
end).to.throw()
expect(function()
local buffer = CircularBuffer.new()
end).to.throw()
end)
it("should push_back() past its max size and loop over itself", function()
local buffer = CircularBuffer.new(1)
buffer:push_back("test1")
buffer:push_back("test2")
buffer:push_back("test3")
expect(buffer:getSize()).to.equal(1)
expect(buffer:front()).to.equal("test3")
buffer:setSize(5)
buffer:reset()
expect(buffer:getSize()).to.equal(0)
local expectedData = { 1, 2, 3, 4, 5 }
for _, v in ipairs(expectedData) do
buffer:push_back(v)
end
expect(buffer:front()).to.equal(1)
buffer:push_back(6)
expect(buffer:front()).to.equal(2)
end)
it("should sort and clip data during setSize() where applicable", function()
local buffer = CircularBuffer.new(100)
for i = 1, 100 do
buffer:push_back(i)
end
expect(buffer:getSize()).to.equal(100)
buffer:setSize(1)
expect(buffer:getSize()).to.equal(1)
buffer:setSize(100)
expect(buffer:getSize()).to.equal(1)
end)
it("should maintain the same front() value until it loops over itself", function()
local buffer = CircularBuffer.new(10)
expect(buffer:front()).to.equal(nil)
for i = 1, 10 do
buffer:push_back(i)
expect(buffer:front()).to.equal(1)
end
buffer:push_back(11)
expect(buffer:front()).to.equal(2)
buffer:push_back(12)
expect(buffer:front()).to.equal(3)
end)
it("should always return the last push_back() value when calling back()", function()
local buffer = CircularBuffer.new(10)
expect(buffer:back()).to.equal(nil)
for i = 1, 20 do
buffer:push_back(i)
expect(buffer:back()).to.equal(i)
end
end)
it("should iterate via iterator and terminate with a nil", function()
local buffer = CircularBuffer.new(100)
for i = 1, 100 do
buffer:push_back(i)
end
local it = buffer:iterator()
local val = it:next()
local count = 1
while val do
expect(val).to.equal(count)
val = it:next()
count = count + 1
end
expect(val).to.equal(nil)
end)
it("should maintain expected getData() after push_back()", function()
local buffer = CircularBuffer.new(5)
local expectedData = {1, 2, 3, 4, 5}
for _, v in ipairs(expectedData) do
buffer:push_back(v)
end
local data = buffer:getData()
for i,v in ipairs(expectedData) do
expect(data[i].entry).to.equal(v)
end
buffer:push_back(6)
local newExpectedData = {6, 2, 3, 4, 5}
data = buffer:getData()
for i,v in ipairs(newExpectedData) do
expect(data[i].entry).to.equal(v)
end
end)
it("should loop to access the correct values when using at() and reverseAt", function()
local buffer = CircularBuffer.new(10)
local expectedData = {2, 4, 6, 8, 0, 9, 7, 5, 3, 1}
for _,v in ipairs(expectedData) do
buffer:push_back(v)
end
for i, v in ipairs(expectedData) do
expect(buffer:at(i)).to.equal(v)
end
expect(buffer:at(0)).to.equal(1)
expect(buffer:at(100)).to.equal(1)
expect(buffer:at(-100)).to.equal(1)
expect(buffer:at(0)).to.equal(1)
expect(buffer:at(100)).to.equal(1)
expect(buffer:at(-100)).to.equal(1)
expect(buffer:reverseAt(1)).to.equal(1)
expect(buffer:reverseAt(5)).to.equal(9)
expect(buffer:reverseAt(0)).to.equal(2)
expect(buffer:reverseAt(100)).to.equal(2)
expect(buffer:reverseAt(-100)).to.equal(2)
end)
end
@@ -0,0 +1,350 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local DataConsumer = require(Components.DataConsumer)
local HeaderButton = require(Components.HeaderButton)
local CellLabel = require(Components.CellLabel)
local Constants = require(script.Parent.Parent.Parent.Constants)
local GeneralFormatting = Constants.GeneralFormatting
local LINE_WIDTH = GeneralFormatting.LineWidth
local LINE_COLOR = GeneralFormatting.LineColor
local ActionBindingsFormatting = Constants.ActionBindingsFormatting
local HEADER_NAMES = ActionBindingsFormatting.ChartHeaderNames
local CELL_WIDTHS = ActionBindingsFormatting.ChartCellWidths
local HEADER_HEIGHT = ActionBindingsFormatting.HeaderFrameHeight
local ENTRY_HEIGHT = ActionBindingsFormatting.EntryFrameHeight
local CELL_PADDING = ActionBindingsFormatting.CellPadding
local MIN_FRAME_WIDTH = ActionBindingsFormatting.MinFrameWidth
local IS_CORE_STR = "Core"
local IS_DEVELOPER_STR = "Developer"
local NON_FOUND_ENTRIES_STR = "No ActionBindings Found"
-- create table of offsets and sizes for each cell
local totalCellWidth = 0
for _, cellWidth in ipairs(CELL_WIDTHS) do
totalCellWidth = totalCellWidth + cellWidth
end
local currOffset = -totalCellWidth
local cellOffset = {}
local headerCellSize = {}
local entryCellSize = {}
currOffset = currOffset / 2
table.insert(cellOffset, UDim2.new(0, CELL_PADDING, 0, 0))
table.insert(headerCellSize, UDim2.new(.5, currOffset - CELL_PADDING, 0, HEADER_HEIGHT))
table.insert(entryCellSize, UDim2.new(.5, currOffset - CELL_PADDING, 0, ENTRY_HEIGHT))
for _, cellWidth in ipairs(CELL_WIDTHS) do
table.insert(cellOffset,UDim2.new(.5, currOffset + CELL_PADDING, 0, 0))
table.insert(headerCellSize, UDim2.new(0, cellWidth - CELL_PADDING, 0, HEADER_HEIGHT))
table.insert(entryCellSize, UDim2.new(0, cellWidth - CELL_PADDING, 0, ENTRY_HEIGHT))
currOffset = currOffset + cellWidth
end
table.insert(cellOffset,UDim2.new(.5, currOffset + CELL_PADDING, 0, 0))
table.insert(headerCellSize, UDim2.new(.5, (-totalCellWidth / 2) - CELL_PADDING, 0, HEADER_HEIGHT))
table.insert(entryCellSize, UDim2.new(.5, (-totalCellWidth / 2) - CELL_PADDING, 0, ENTRY_HEIGHT))
local verticalOffsets = {}
for i, offset in ipairs(cellOffset) do
verticalOffsets[i] = UDim2.new(
offset.X.Scale,
offset.X.Offset - CELL_PADDING,
offset.Y.Scale,
offset.Y.Offset)
end
local ActionBindingsChart = Roact.Component:extend("ActionBindingsChart")
local function constructHeader(onSortChanged, width)
local header = {}
for ind, name in ipairs(HEADER_NAMES) do
header[name] = Roact.createElement(HeaderButton, {
text = name,
size = headerCellSize[ind],
pos = cellOffset[ind],
sortfunction = onSortChanged,
})
end
header["upperHorizontalLine"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 0, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
header["lowerHorizontalLine"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, -LINE_WIDTH),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
for ind = 2, #verticalOffsets do
local key = string.format("VerticalLine_%d",ind)
header[key] = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 1, 0),
Position = verticalOffsets[ind],
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
end
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
ClipsDescendants = true,
}, header)
end
local function constructEntry(entry, width, layoutOrder)
local name = entry.name
local actionInfo = entry.actionInfo
-- the last element is special cased because the data in the
-- string is passed in as value in the table
-- use tostring to convert the enum into an actual string also because it's used twice
local enumStr = tostring(actionInfo["inputTypes"][1])
local isCoreString = IS_CORE_STR
if actionInfo["isCore"] then
isCoreString = IS_DEVELOPER_STR
end
local row = {}
for i = 2,#verticalOffsets do
local key = string.format("line_%d",i)
row[key] = Roact.createElement("Frame", {
Size = UDim2.new(0,LINE_WIDTH,1,0),
Position = verticalOffsets[i],
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
end
row[name] = Roact.createElement(CellLabel, {
text = enumStr,
size = entryCellSize[1],
pos = cellOffset[1],
})
row.priorityLevel = Roact.createElement(CellLabel, {
text = actionInfo["priorityLevel"],
size = entryCellSize[2],
pos = cellOffset[2],
})
row.isCore = Roact.createElement(CellLabel, {
text = isCoreString,
size = entryCellSize[3],
pos = cellOffset[3],
})
row.actionName = Roact.createElement(CellLabel, {
text = name,
size = entryCellSize[4],
pos = cellOffset[4],
})
row.inputTypes = Roact.createElement(CellLabel, {
text = enumStr,
size = entryCellSize[5],
pos = cellOffset[5],
})
row.lowerHorizontalLine = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
return Roact.createElement("Frame", {
Size = UDim2.new(0, width, 0, ENTRY_HEIGHT),
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
},row)
end
function ActionBindingsChart:init(props)
local initBindings = props.ActionBindingsData:getCurrentData()
self.onSortChanged = function(sortType)
local currSortType = props.ActionBindingsData:getSortType()
if sortType == currSortType then
self:setState({
reverseSort = not self.state.reverseSort
})
else
props.ActionBindingsData:setSortType(sortType)
self:setState({
reverseSort = false,
})
end
end
self.onCanvasPosChanged = function()
local canvasPos = self.scrollingRef.current.CanvasPosition
if self.state.canvasPos ~= canvasPos then
self:setState({
canvasPos = canvasPos,
})
end
end
self.scrollingRef = Roact.createRef()
self.state = {
actionBindingEntries = initBindings,
reverseSort = false,
}
end
function ActionBindingsChart:willUpdate()
if self.canvasPosConnector then
self.canvasPosConnector:Disconnect()
end
end
function ActionBindingsChart:didUpdate()
if self.scrollingRef.current then
local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition")
self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged)
local absSize = self.scrollingRef.current.AbsoluteSize
local currAbsSize = self.state.absScrollSize
if absSize.X ~= currAbsSize.X or
absSize.Y ~= currAbsSize.Y then
self:setState({
absScrollSize = absSize,
})
end
end
end
function ActionBindingsChart:didMount()
self.bindingsUpdated = self.props.ActionBindingsData:Signal():Connect(function(bindingsData)
self:setState({
actionBindingEntries = bindingsData
})
end)
if self.scrollingRef.current then
local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition")
self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged)
self:setState({
absScrollSize = self.scrollingRef.current.AbsoluteSize,
canvasPos = self.scrollingRef.current.CanvasPosition,
})
end
end
function ActionBindingsChart:willUnmount()
self.bindingsUpdated:Disconnect()
self.bindingsUpdated = nil
self.canvasPosConnector:Disconnect()
self.canvasPosConnector = nil
end
function ActionBindingsChart:render()
local entries = {}
local searchTerm = self.props.searchTerm
local size = self.props.size
local layoutOrder = self.props.layoutOrder
local entryList = self.state.actionBindingEntries
local reverseSort = self.state.reverseSort
local canvasPos = self.state.canvasPos
local absScrollSize = self.state.absScrollSize
local frameWidth = absScrollSize and math.max(absScrollSize.X, MIN_FRAME_WIDTH) or MIN_FRAME_WIDTH
entries["UIListLayout"] = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
SortOrder = Enum.SortOrder.LayoutOrder,
})
local totalEntries = #entryList
local canvasHeight = 0
if absScrollSize and canvasPos then
local paddingHeight = -1
local usedFrameSpace = 0
local count = 0
for ind, entry in ipairs(entryList) do
local foundTerm = false
if searchTerm then
local enumStr = tostring(entry.actionInfo["inputTypes"][1])
foundTerm = string.find(enumStr:lower(), searchTerm:lower()) ~= nil
foundTerm = foundTerm or string.find(entry.name:lower(), searchTerm:lower()) ~= nil
end
if not searchTerm or foundTerm then
if canvasHeight + ENTRY_HEIGHT >= canvasPos.Y then
if usedFrameSpace < absScrollSize.Y then
local entryLayoutOrder = reverseSort and (totalEntries - ind) or ind
entries[ind] = constructEntry(entry, frameWidth, entryLayoutOrder + 1)
end
if paddingHeight < 0 then
paddingHeight = canvasHeight
else
usedFrameSpace = usedFrameSpace + ENTRY_HEIGHT
end
count = count + 1
end
canvasHeight = canvasHeight + ENTRY_HEIGHT
end
end
if count == 0 then
entries["NoneFound"] = Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 1, 0),
Text = NON_FOUND_ENTRIES_STR,
TextColor3 = LINE_COLOR,
BackgroundTransparency = 1,
LayoutOrder = 1,
})
else
entries["WindowingPadding"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, paddingHeight),
BackgroundTransparency = 1,
LayoutOrder = 1,
})
end
end
return Roact.createElement("Frame", {
Size = size,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, {
Header = constructHeader(self.onSortChanged, frameWidth),
MainChart = Roact.createElement("ScrollingFrame", {
Position = UDim2.new(0, 0, 0, HEADER_HEIGHT),
Size = UDim2.new(1, 0, 1, - HEADER_HEIGHT),
CanvasSize = UDim2.new(0, frameWidth, 0, canvasHeight),
ScrollBarThickness = 6,
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
[Roact.Ref] = self.scrollingRef
}, entries),
})
end
return DataConsumer(ActionBindingsChart, "ActionBindingsData")
@@ -0,0 +1,15 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local DataProvider = require(script.Parent.Parent.DataProvider)
local ActionBindingsChart = require(script.Parent.ActionBindingsChart)
it("should create and destroy without errors", function()
local element = Roact.createElement(DataProvider, nil, {
ActionBindingsChart = Roact.createElement(ActionBindingsChart)
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,151 @@
local ContextActionService = game:GetService("ContextActionService")
local Signal = require(script.Parent.Parent.Parent.Signal)
local Constants = require(script.Parent.Parent.Parent.Constants)
local HEADER_NAMES = Constants.ActionBindingsFormatting.ChartHeaderNames
local SORT_COMPARATOR = {
[HEADER_NAMES[1]] = function(a,b) -- "Name"
return a.counter < b.counter
end,
[HEADER_NAMES[2]] = function(a,b) -- "Priority"
if a.actionInfo.priorityLevel == b.actionInfo.priorityLevel then
return a.counter < b.counter
end
return a.actionInfo.priorityLevel < b.actionInfo.priorityLevel
end,
[HEADER_NAMES[3]] = function(a,b) -- "Security"
if a.actionInfo.isCore == b.actionInfo.isCore then
return a.counter < b.counter
else
return a.actionInfo.isCore
end
end,
[HEADER_NAMES[4]] = function(a,b) -- "Action Name"
return a.name:lower() < b.name:lower()
end,
[HEADER_NAMES[5]] = function(a,b) -- "Input Types"
return tostring(a.actionInfo.inputTypes[1]) < tostring(b.actionInfo.inputTypes[1])
end,
}
local ActionBindingsData = {}
ActionBindingsData.__index = ActionBindingsData
function ActionBindingsData.new()
local self = {}
setmetatable(self, ActionBindingsData)
self._bindingsUpdated = Signal.new()
self._bindingsData = {}
self._bindingCounter = 0
self._sortedBindingData = {}
self._sortType = HEADER_NAMES[1] -- Name
return self
end
function ActionBindingsData:setSortType(sortType)
if SORT_COMPARATOR[sortType] then
self._sortType = sortType
table.sort(self._sortedBindingData, SORT_COMPARATOR[self._sortType])
self._bindingsUpdated:Fire(self._sortedBindingData)
else
error(string.format("attempted to pass invalid sortType: %s", tostring(sortType)), 2)
end
end
function ActionBindingsData:getSortType()
return self._sortType
end
function ActionBindingsData:Signal()
return self._bindingsUpdated
end
function ActionBindingsData:getCurrentData()
return self._sortedBindingData
end
-- this funciton will require some extra work to handle the
-- case a entry insert occurs during the end of the list
function ActionBindingsData:updateBindingDataEntry(name, info)
if info == nil then
--remove element and clean up sorted
self._bindingsData[name] = nil
for i, v in pairs(self._sortedBindingData) do
if v.name == name then
table.remove(self._sortedBindingData, i)
return
end
end
elseif not self._bindingsData[name] then
self._bindingCounter = self._bindingCounter + 1
self._bindingsData[name] = info
local newEntry = {
name = name,
actionInfo = self._bindingsData[name],
counter = self._bindingCounter,
}
table.insert(self._sortedBindingData, newEntry)
else
self._bindingsData[name] = info
end
end
function ActionBindingsData:start()
local boundActions = ContextActionService:GetAllBoundActionInfo()
for actionName, actionInfo in pairs(boundActions) do
actionInfo.isCore = false
self:updateBindingDataEntry(actionName, actionInfo)
end
local boundCoreActions = ContextActionService:GetAllBoundCoreActionInfo()
for actionName, actionInfo in pairs(boundCoreActions) do
actionInfo.isCore = true
self:updateBindingDataEntry(actionName, actionInfo)
end
if not self._actionChangedConnection then
self._actionChangedConnection = ContextActionService.BoundActionChanged:connect(
function(actionName, changeName, changeTable)
self:updateBindingDataEntry(actionName, nil)
self:updateBindingDataEntry(changeName, changeTable)
self._bindingsUpdated:Fire(self._sortedBindingData)
end)
end
if not self._actionAddedConnection then
self._actionAddedConnection = ContextActionService.BoundActionAdded:connect(
function(actionName, createTouchButton, actionInfo, isCore)
actionInfo.isCore = isCore
self:updateBindingDataEntry(actionName, actionInfo)
self._bindingsUpdated:Fire(self._sortedBindingData)
end)
end
if not self._actionRemovedConnection then
self._actionRemovedConnection = ContextActionService.BoundActionRemoved:connect(
function(actionName, actionInfo, isCore)
self:updateBindingDataEntry(actionName, nil)
self._bindingsUpdated:Fire(self._sortedBindingData)
end)
end
end
function ActionBindingsData:stop()
if self.actionChangedConnector then
self.actionChangedConnector:Disconnect()
self.actionChangedConnector = nil
end
if self.actionAddedConnector then
self.actionAddedConnector:Disconnect()
self.actionAddedConnector = nil
end
if self.actionRemovedConnector then
self.actionRemovedConnector:Disconnect()
self.actionRemovedConnector = nil
end
end
return ActionBindingsData
@@ -0,0 +1,105 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent.Parent.Parent.Components
local ActionBindingsChart = require(Components.ActionBindings.ActionBindingsChart)
local UtilAndTab = require(Components.UtilAndTab)
local Actions = script.Parent.Parent.Parent.Actions
local ActionBindingsUpdateSearchFilter = require(Actions.ActionBindingsUpdateSearchFilter)
local Constants = require(script.Parent.Parent.Parent.Constants)
local PADDING = Constants.GeneralFormatting.MainRowPadding
local MainViewActionBindings = Roact.Component:extend("MainViewActionBindings")
function MainViewActionBindings:init()
self.onUtilTabHeightChanged = function(utilTabHeight)
self:setState({
utilTabHeight = utilTabHeight
})
end
self.onSearchTermChanged = function(newSearchTerm)
self.props.dispatchActionBindingsUpdateSearchFilter(newSearchTerm, {})
end
self.utilRef = Roact.createRef()
self.state = {
utilTabHeight = 0
}
end
function MainViewActionBindings:didMount()
local utilSize = self.utilRef.current.Size
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
function MainViewActionBindings:didUpdate()
local utilSize = self.utilRef.current.Size
if utilSize.Y.Offset ~= self.state.utilTabHeight then
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
end
function MainViewActionBindings:render()
local size = self.props.size
local formFactor = self.props.formFactor
local tabList = self.props.tabList
local searchTerm = self.props.bindingsSearchTerm
local utilTabHeight = self.state.utilTabHeight
return Roact.createElement("Frame", {
Size = size,
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
LayoutOrder = 3,
}, {
UIListLayout = Roact.createElement("UIListLayout", {
Padding = UDim.new(0, PADDING),
SortOrder = Enum.SortOrder.LayoutOrder,
}),
UtilAndTab = Roact.createElement(UtilAndTab, {
windowWidth = size.X.Offset,
formFactor = formFactor,
tabList = tabList,
searchTerm = searchTerm,
layoutOrder = 1,
refForParent = self.utilRef,
onHeightChanged = self.onUtilTabHeightChanged,
onSearchTermChanged = self.onSearchTermChanged,
}),
ActionBindings = utilTabHeight > 0 and Roact.createElement(ActionBindingsChart, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
searchTerm = searchTerm,
layoutOrder = 2,
}),
})
end
local function mapStateToProps(state, props)
return {
bindingsSearchTerm = state.ActionBindingsData.bindingsSearchTerm,
}
end
local function mapDispatchToProps(dispatch)
return {
dispatchActionBindingsUpdateSearchFilter = function(searchTerm, filters)
dispatch(ActionBindingsUpdateSearchFilter(searchTerm, filters))
end
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewActionBindings)
@@ -0,0 +1,36 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local MainViewActionBindings = require(script.Parent.MainViewActionBindings)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0
},
ActionBindingsData = {
bindingsSearchTerm = ""
}
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
MainViewActionBindings = Roact.createElement(MainViewActionBindings, {
size = UDim2.new(),
tabList = {},
})
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,56 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Immutable = require(script.Parent.Parent.Immutable)
local Constants = require(script.Parent.Parent.Constants)
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local LINE_COLOR = Constants.GeneralFormatting.LineColor
local ARROW_WIDTH = Constants.GeneralFormatting.ArrowWidth
local CLOSE_ARROW = Constants.Image.RightArrow
local OPEN_ARROW = Constants.Image.DownArrow
local BannerButton = Roact.Component:extend("BannerButton")
function BannerButton:render()
local children = self.props[Roact.Children] or {}
local size = self.props.size
local pos = self.props.pos
local isExpanded = self.props.isExpanded
local layoutOrder = self.props.layoutOrder
local onButtonPress = self.props.onButtonPress
local bannerElements = {
BannerButtonArrow = Roact.createElement("ImageLabel", {
Image = isExpanded and OPEN_ARROW or CLOSE_ARROW,
BackgroundTransparency = 1,
Size = UDim2.new(0, ARROW_WIDTH, 0, ARROW_WIDTH),
Position = UDim2.new(0, 0, .5, -ARROW_WIDTH / 2),
}),
HorizontalLineTop = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
HorizontalLineBottom = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, -LINE_WIDTH),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
}
return Roact.createElement("ImageButton", {
Size = size,
Position = pos,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
[Roact.Event.Activated] = onButtonPress,
}, Immutable.JoinDictionaries(bannerElements, children))
end
return BannerButton
@@ -0,0 +1,32 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local TEXT_SIZE = Constants.DefaultFontSize.MainWindow
local TEXT_COLOR = Constants.Color.Text
local MAIN_FONT = Constants.Font.MainWindow
local MAIN_FONT_BOLD = Constants.Font.MainWindowBold
local function CellLabel(props)
local text = props.text
local size = props.size
local pos = props.pos
local bold = props.bold
local layoutOrder = props.layoutOrder
return Roact.createElement("TextLabel", {
Text = text,
TextSize = TEXT_SIZE,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Left,
TextWrapped = true,
Font = bold and MAIN_FONT_BOLD or MAIN_FONT,
Size = size,
Position = pos,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
})
end
return CellLabel
@@ -0,0 +1,70 @@
local CorePackages = game:GetService("CorePackages")
local TextService = game:GetService("TextService")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local PADDING = Constants.UtilityBarFormatting.CheckBoxInnerPadding
local CheckBox = Roact.Component:extend("CheckBox")
function CheckBox:render()
local checkBoxHeight = self.props.checkBoxHeight
local frameHeight = self.props.frameHeight
local layoutOrder = self.props.layoutOrder
local name = self.props.name
local font = self.props.font
local fontSize = self.props.fontSize
local isSelected = self.props.isSelected
local selectedColor = self.props.selectedColor
local unselectedColor = self.props.unselectedColor
local onCheckBoxClicked = self.props.onCheckBoxClicked
-- this can be replaced with default values once that releases
local image = ""
local borderSize = 1
local backgroundColor = unselectedColor
if isSelected then
image = Constants.Image.Check
borderSize = 0
backgroundColor = selectedColor
end
local textVector = TextService:GetTextSize(name, fontSize, font, Vector2.new(0, frameHeight))
local textWidth = textVector.X
return Roact.createElement("ImageButton", {
Size = UDim2.new(0, checkBoxHeight + textWidth + (PADDING * 2), 0, frameHeight),
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
[Roact.Event.Activated] = function(rbx)
onCheckBoxClicked(name, not isSelected)
end,
}, {
Icon = Roact.createElement("ImageLabel", {
Image = image,
Size = UDim2.new(0, checkBoxHeight, 0, checkBoxHeight),
Position = UDim2.new(0, 0, .5, -checkBoxHeight / 2),
BackgroundColor3 = backgroundColor,
BackgroundTransparency = 0,
BorderColor3 = Constants.Color.Text,
BorderSizePixel = borderSize,
}),
Text = Roact.createElement("TextLabel", {
Text = name,
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Left,
Font = font,
TextSize = fontSize,
Size = UDim2.new(1, -frameHeight, 1, 0),
Position = UDim2.new(0, checkBoxHeight + PADDING, 0, 0),
BackgroundTransparency = 1,
})
})
end
return CheckBox
@@ -0,0 +1,18 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local CheckBox = require(script.Parent.CheckBox)
it("should create and destroy without errors", function()
local element = Roact.createElement(CheckBox, {
name = "",
fontSize = 0,
font = 0,
frameHeight = 0,
checkBoxHeight = 0,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,161 @@
local CorePackages = game:GetService("CorePackages")
local TextService = game:GetService("TextService")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Components
local CheckBox = require(Components.CheckBox)
local CheckBoxDropDown = require(Components.CheckBoxDropDown)
local Constants = require(script.Parent.Parent.Constants)
local CHECK_BOX_HEIGHT = Constants.UtilityBarFormatting.CheckBoxHeight
local CHECK_BOX_PADDING = Constants.UtilityBarFormatting.CheckBoxInnerPadding * 2
local FILTER_ICON_UNFILLED = Constants.Image.FilterUnfilled
local FILTER_ICON_FILLED = Constants.Image.FilterFilled
local DROP_DOWN_Y_ADJUST = 3
local CheckBoxContainer = Roact.PureComponent:extend("CheckBoxContainer")
function CheckBoxContainer:init()
self.onCheckBoxClicked = function(field, newState)
local onCheckBoxChanged = self.props.onCheckBoxChanged
onCheckBoxChanged(field, newState)
end
-- this is part of the dropdown logic
self.onCheckBoxExpanded = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or
(input.UserInputType == Enum.UserInputType.Touch and
input.UserInputState == Enum.UserInputState.End) then
self:setState({
expanded = true,
})
end
end
-- this is part of the dropdown logic
self.onCloseCheckBox = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or
(input.UserInputType == Enum.UserInputType.Touch and
input.UserInputState == Enum.UserInputState.End) then
self:setState({
expanded = false
})
end
end
if not self.props.orderedCheckBoxState then
warn("CheckBoxContainer must be passed a list of Box Names or else it only creates an empty frame")
end
local textWidths = {}
local totalLength = 0
local count = 0
for ind, box in ipairs(self.props.orderedCheckBoxState) do
local textVector = TextService:GetTextSize(
box.name,
Constants.DefaultFontSize.UtilBar,
Constants.Font.UtilBar,
Vector2.new(0, 0)
)
textWidths[ind] = textVector.X
totalLength = totalLength + textVector.X + CHECK_BOX_HEIGHT + CHECK_BOX_PADDING
count = count + 1
end
self.ref = Roact.createRef()
self.state = {
expanded = false,
textWidths = textWidths,
numCheckBoxes = count,
minFullLength = totalLength,
}
end
function CheckBoxContainer:render()
local elements = {}
local frameWidth = self.props.frameWidth
local frameHeight = self.props.frameHeight
local pos = self.props.pos
local layoutOrder = self.props.layoutOrder
local orderedCheckBoxState = self.props.orderedCheckBoxState
local minFullLength = self.state.minFullLength
local expanded = self.state.expanded
local numCheckBoxes = self.state.numCheckBoxes
local anySelected = false
for layoutOrder, box in ipairs(orderedCheckBoxState) do
elements[box.name] = Roact.createElement(CheckBox, {
name = box.name,
font = Constants.Font.UtilBar,
fontSize = Constants.DefaultFontSize.UtilBar,
checkBoxHeight = CHECK_BOX_HEIGHT,
frameHeight = frameHeight,
layoutOrder = layoutOrder,
isSelected = box.state,
selectedColor = Constants.Color.SelectedBlue,
unselectedColor = Constants.Color.UnselectedGray,
onCheckBoxClicked = self.onCheckBoxClicked,
})
anySelected = anySelected or box.state
end
if frameWidth < minFullLength then
elements["CheckBoxLayout"] = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Left,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Top,
FillDirection = Enum.FillDirection.Vertical,
})
local showDropDown = self.ref.current and expanded
local dropDownPos
if showDropDown then
local absPos = self.ref.current.AbsolutePosition
-- adding slight y offset to nudge dropdown enough to see button border
dropDownPos = UDim2.new(0, absPos.X, 0, absPos.Y + frameHeight + DROP_DOWN_Y_ADJUST)
end
return Roact.createElement("ImageButton", {
Size = UDim2.new(0, frameHeight, 0, frameHeight),
LayoutOrder = layoutOrder,
Image = showDropDown and FILTER_ICON_FILLED or FILTER_ICON_UNFILLED,
BackgroundTransparency = 1,
BorderColor3 = Constants.Color.Text,
[Roact.Event.InputEnded] = self.onCheckBoxExpanded,
[Roact.Ref] = self.ref,
}, {
DropDown = showDropDown and Roact.createElement(CheckBoxDropDown, {
absolutePosition = dropDownPos,
frameWidth = frameWidth,
elementHeight = frameHeight,
numElements = numCheckBoxes,
onCloseCheckBox = self.onCloseCheckBox
}, elements)
})
else
elements["CheckBoxLayout"] = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Left,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Top,
FillDirection = Enum.FillDirection.Horizontal,
})
return Roact.createElement("Frame", {
Size = UDim2.new(0,frameWidth,0,frameHeight),
Position = pos,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, elements)
end
end
return CheckBoxContainer
@@ -0,0 +1,15 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local CheckBoxContainer = require(script.Parent.CheckBoxContainer)
it("should create and destroy without errors", function()
local element = Roact.createElement(CheckBoxContainer, {
orderedCheckBoxState = {},
frameWidth = 0,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,52 @@
local CorePackages = game:GetService("CorePackages")
local RobloxGui = game:GetService("CoreGui").RobloxGui
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local INNER_FRAME_PADDING = 12
local CheckBoxDropDown = Roact.Component:extend("CheckBoxDropDown")
function CheckBoxDropDown:render()
local children = self.props[Roact.Children] or {}
local absolutePosition = self.props.absolutePosition
local frameWidth = self.props.frameWidth
local elementHeight = self.props.elementHeight
local numElements = self.props.numElements
local onCloseCheckBox = self.props.onCloseCheckBox
local frameHeight = elementHeight * numElements
local outerFrameSize = UDim2.new( 0, frameWidth, 0, (2 * INNER_FRAME_PADDING) + frameHeight)
return Roact.createElement(Roact.Portal, {
target = RobloxGui,
}, {
FullScreen = Roact.createElement("ScreenGui", {
}, {
InputCatcher = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
BackgroundTransparency = 1,
[Roact.Event.InputEnded] = onCloseCheckBox,
}, {
OuterFrame = Roact.createElement("ImageButton", {
Size = outerFrameSize,
AutoButtonColor = false,
Position = absolutePosition,
BackgroundColor3 = Constants.Color.TextBoxGray,
BackgroundTransparency = 0,
}, {
innerFrame = Roact.createElement("Frame", {
Position = UDim2.new(0, INNER_FRAME_PADDING, 0 , INNER_FRAME_PADDING),
Size = UDim2.new(0,frameWidth, 0, frameHeight),
BackgroundTransparency = 1,
}, children)
})
})
})
})
end
return CheckBoxDropDown
@@ -0,0 +1,15 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local CheckBoxDropDown = require(script.Parent.CheckBoxDropDown)
it("should create and destroy without errors", function()
local element = Roact.createElement(CheckBoxDropDown, {
elementHeight = 0,
numElements = 0,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,101 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local FRAME_HEIGHT = Constants.UtilityBarFormatting.FrameHeight
local SMALL_FRAME_HEIGHT = Constants.UtilityBarFormatting.SmallFrameHeight
local CS_BUTTON_WIDTH = Constants.UtilityBarFormatting.ClientServerButtonWidth
local SMALL_CS_BUTTON_WIDTH = Constants.UtilityBarFormatting.ClientServerDropDownWidth
local FONT = Constants.Font.UtilBar
local FONT_SIZE = Constants.DefaultFontSize.UtilBar
local FONT_COLOR = Constants.Color.Text
local FullScreenDropDownButton = require(script.Parent.FullScreenDropDownButton)
local DropDown = require(script.Parent.DropDown)
local BUTTON_SIZE = UDim2.new(0, CS_BUTTON_WIDTH, 0, FRAME_HEIGHT)
local CLIENT_SERVER_NAMES = {"Client", "Server"}
local ClientServerButton = Roact.Component:extend("ClientServerButton")
function ClientServerButton:init()
self.dropDownCallback = function(index)
if index == 1 then
self.props.onClientButton()
elseif index == 2 then
self.props.onServerButton()
end
end
end
function ClientServerButton:render()
local formFactor = self.props.formFactor
local useDropDown = self.props.useDropDown
local isClientView = self.props.isClientView
local layoutOrder = self.props.layoutOrder
local onServerButton = self.props.onServerButton
local onClientButton = self.props.onClientButton
local serverButtonColor = Constants.Color.SelectedBlue
local clientButtonColor = Constants.Color.UnselectedGray
if isClientView then
clientButtonColor = Constants.Color.SelectedBlue
serverButtonColor = Constants.Color.UnselectedGray
end
if formFactor == Constants.FormFactor.Small then
return Roact.createElement(FullScreenDropDownButton, {
buttonSize = UDim2.new(0, SMALL_CS_BUTTON_WIDTH, 0, SMALL_FRAME_HEIGHT),
dropDownList = CLIENT_SERVER_NAMES,
selectedIndex = isClientView and 1 or 2,
onSelection = self.dropDownCallback,
layoutOrder = layoutOrder,
})
elseif useDropDown then
return Roact.createElement(DropDown, {
buttonSize = UDim2.new(0, SMALL_CS_BUTTON_WIDTH, 0, SMALL_FRAME_HEIGHT),
dropDownList = CLIENT_SERVER_NAMES,
selectedIndex = isClientView and 1 or 2,
onSelection = self.dropDownCallback,
layoutOrder = layoutOrder,
})
else
return Roact.createElement("Frame", {
Size = UDim2.new(0, 2 * CS_BUTTON_WIDTH, 0, FRAME_HEIGHT),
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, {
ClientButton = Roact.createElement('TextButton', {
Text = CLIENT_SERVER_NAMES[1],
TextSize = FONT_SIZE,
TextColor3 = FONT_COLOR,
Font = FONT,
Size = BUTTON_SIZE,
AutoButtonColor = false,
BackgroundColor3 = clientButtonColor,
BackgroundTransparency = 0,
LayoutOrder = 1,
[Roact.Event.Activated] = onClientButton,
}),
ServerButton = Roact.createElement('TextButton', {
Text = CLIENT_SERVER_NAMES[2],
TextSize = FONT_SIZE,
TextColor3 = FONT_COLOR,
Font = FONT,
Size = BUTTON_SIZE,
AutoButtonColor = false,
Position = UDim2.new(0, CS_BUTTON_WIDTH, 0, 0),
BackgroundColor3 = serverButtonColor,
BackgroundTransparency = 0,
LayoutOrder = 2,
[Roact.Event.Activated] = onServerButton,
})
})
end
end
return ClientServerButton
@@ -0,0 +1,13 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local ClientServerButton = require(script.Parent.ClientServerButton)
it("should create and destroy without errors", function()
local element = Roact.createElement(ClientServerButton
)
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,41 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Immutable = require(script.Parent.Parent.Immutable)
return function(component, ...)
if not component then
error("Expected component to be passed to connection, got nil.")
end
local targetList = {}
for i = 1, select("#", ...) do
targetList[i] = select(i, ...)
end
local name = string.format("Consumer(%s)_DependsOn_%s",tostring(component), targetList[1] )
local DataConsumer = Roact.Component:extend(name)
function DataConsumer:init()
local contextTable = {}
for _,dataName in pairs(targetList) do
local contextualData = self._context.DevConsoleData[dataName]
if not contextualData then
local errorStr = string.format("%s %s",tostring(dataName),
"could not be found. Make sure DataProvider is above this consumer"
)
error(errorStr)
return
end
contextTable[dataName] = contextualData
end
self.state = contextTable
end
function DataConsumer:render()
local props = Immutable.JoinDictionaries(self.props, self.state)
return Roact.createElement(component, props)
end
return DataConsumer
end
@@ -0,0 +1,49 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Components
local LogData = require(Components.Log.LogData)
local ClientMemoryData = require(Components.Memory.ClientMemoryData)
local ServerMemoryData = require(Components.Memory.ServerMemoryData)
local NetworkData = require(Components.Network.NetworkData)
local ServerScriptsData = require(Components.Scripts.ServerScriptsData)
local DataStoresData = require(Components.DataStores.DataStoresData)
local ServerStatsData = require(Components.ServerStats.ServerStatsData)
local ActionBindingsData = require(Components.ActionBindings.ActionBindingsData)
local ServerJobsData = require(Components.ServerJobs.ServerJobsData)
local DataProvider = Roact.Component:extend("DataProvider")
function DataProvider:init()
self._context.DevConsoleData = {
ClientLogData = LogData.new(true),
ServerLogData = LogData.new(false),
ClientMemoryData = ClientMemoryData.new(),
ServerMemoryData = ServerMemoryData.new(),
ClientNetworkData = NetworkData.new(true),
ServerNetworkData = NetworkData.new(false),
ServerScriptsData = ServerScriptsData.new(),
DataStoresData = DataStoresData.new(),
ServerStatsData = ServerStatsData.new(),
ActionBindingsData = ActionBindingsData.new(),
ServerJobsData = ServerJobsData.new(),
}
end
function DataProvider:didMount()
if self.props.isDeveloperView then
for _, dataProvider in pairs(self._context.DevConsoleData) do
dataProvider:start()
end
else
self._context.DevConsoleData.ClientLogData:start()
self._context.DevConsoleData.ClientMemoryData:start()
end
end
function DataProvider:render()
return Roact.oneChild(self.props[Roact.Children])
end
return DataProvider
@@ -0,0 +1,239 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local DataConsumer = require(Components.DataConsumer)
local CellLabel = require(Components.CellLabel)
local BannerButton = require(Components.BannerButton)
local LineGraph = require(Components.LineGraph)
local Constants = require(script.Parent.Parent.Parent.Constants)
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local LINE_COLOR = Constants.GeneralFormatting.LineColor
local HEADER_NAMES = Constants.DataStoresFormatting.ChartHeaderNames
local VALUE_CELL_WIDTH = Constants.DataStoresFormatting.ValueCellWidth
local CELL_PADDING = Constants.DataStoresFormatting.CellPadding
local ARROW_PADDING = Constants.DataStoresFormatting.ExpandArrowPadding
local HEADER_HEIGHT = Constants.DataStoresFormatting.HeaderFrameHeight
local ENTRY_HEIGHT = Constants.DataStoresFormatting.EntryFrameHeight
local GRAPH_HEIGHT = Constants.GeneralFormatting.LineGraphHeight
local NO_DATA_MSG = "Initialize DataStoresService to view DataStore Budget."
local NO_RESULT_SEARCH_STR = Constants.GeneralFormatting.NoResultSearchStr
local convertTimeStamp = require(script.Parent.Parent.Parent.Util.convertTimeStamp)
local DataStoresChart = Roact.Component:extend("DataStoresChart")
local function getX(entry)
return entry.time
end
local function getY(entry)
return entry.value
end
local function stringFormatY(value)
return math.ceil(value)
end
function DataStoresChart:init(props)
local currStoresData, currStoresDataCount = props.DataStoresData:getCurrentData()
self.getOnButtonPress = function (name)
return function(rbx, input)
self:setState({
expandedEntry = self.state.expandedEntry ~= name and name
})
end
end
self.state = {
dataStoresData = currStoresData,
dataStoresDataCount = currStoresDataCount,
expandedEntry = nil
}
end
function DataStoresChart:didMount()
self.statsConnector = self.props.DataStoresData:Signal():Connect(function(data, count)
self:setState({
dataStoresData = data,
dataStoresDataCount = count,
})
end)
end
function DataStoresChart:willUnmount()
self.statsConnector:Disconnect()
self.statsConnector = nil
end
function DataStoresChart:render()
local elements = {}
local searchTerm = self.props.searchTerm
local size = self.props.size
local layoutOrder = self.props.layoutOrder
local expandedEntry = self.state.expandedEntry
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
SortOrder = Enum.SortOrder.LayoutOrder,
})
local componentHeight = HEADER_HEIGHT
local datastoreBudget = self.state.dataStoresData
local totalCount = 0
local currLayoutOrder = 1
if datastoreBudget then
for name, data in pairs(datastoreBudget) do
totalCount = totalCount + 1
if not searchTerm or string.find(name:lower(), searchTerm:lower()) ~= nil then
currLayoutOrder = currLayoutOrder + 1
local showGraph = expandedEntry == name
local frameHeight = showGraph and ENTRY_HEIGHT + GRAPH_HEIGHT or ENTRY_HEIGHT
elements[name] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, frameHeight),
BackgroundTransparency = 1,
LayoutOrder = currLayoutOrder,
}, {
DataButton = Roact.createElement(BannerButton, {
size = UDim2.new(1, 0, 0, ENTRY_HEIGHT),
pos = UDim2.new(),
isExpanded = showGraph,
onButtonPress = self.getOnButtonPress(name),
}, {
[name] = Roact.createElement(CellLabel, {
text = name,
size = UDim2.new(1 - VALUE_CELL_WIDTH, -CELL_PADDING - ARROW_PADDING, 1, 0),
pos = UDim2.new(0, CELL_PADDING + ARROW_PADDING, 0, 0),
}),
Data = Roact.createElement(CellLabel, {
text = data.dataSet:back().value,
size = UDim2.new(VALUE_CELL_WIDTH , -CELL_PADDING, 1, 0),
pos = UDim2.new(1 - VALUE_CELL_WIDTH, CELL_PADDING, 0, 0),
}),
VerticalLine = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 0, ENTRY_HEIGHT),
Position = UDim2.new(1 - VALUE_CELL_WIDTH, 0, 0, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
lowerHorizontalLine = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
}),
Graph = showGraph and Roact.createElement(LineGraph, {
pos = UDim2.new(0, 0, 0, ENTRY_HEIGHT),
size = UDim2.new(1, 0, 1, -ENTRY_HEIGHT),
graphData = data.dataSet,
maxY = data.max,
minY = data.min,
getX = getX,
getY = getY,
axisLabelX = "Timestamp",
axisLabelY = name,
stringFormatX = convertTimeStamp,
stringFormatY = stringFormatY,
}),
})
componentHeight = componentHeight + ENTRY_HEIGHT
end
end
end
if currLayoutOrder == 1 then
if totalCount == 0 then
return Roact.createElement("TextLabel", {
Size = size,
Text = NO_DATA_MSG,
TextColor3 = Constants.Color.Text,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
})
else
local noResultSearchStr = string.format(NO_RESULT_SEARCH_STR, searchTerm)
elements["emptyResult"] = Roact.createElement("TextLabel", {
Size = size,
Text = noResultSearchStr,
TextColor3 = Constants.Color.Text,
BackgroundTransparency = 1,
})
end
end
return Roact.createElement("Frame", {
Size = size,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, {
Header = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
LayoutOrder = 1,
}, {
[HEADER_NAMES[1]] = Roact.createElement(CellLabel, {
text = HEADER_NAMES[1],
size = UDim2.new(1 - VALUE_CELL_WIDTH, -CELL_PADDING - ARROW_PADDING, 1, 0),
pos = UDim2.new(0, CELL_PADDING + ARROW_PADDING, 0, 0),
}),
[HEADER_NAMES[2]] = Roact.createElement(CellLabel, {
text = HEADER_NAMES[2],
size = UDim2.new(VALUE_CELL_WIDTH , -CELL_PADDING, 1, 0),
pos = UDim2.new(1 - VALUE_CELL_WIDTH, CELL_PADDING, 0, 0),
}),
upperHorizontalLine = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
vertical = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 1, 0),
Position = UDim2.new(1 - VALUE_CELL_WIDTH, 0, 0, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
lowerHorizontalLine = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
}),
mainFrame = Roact.createElement("ScrollingFrame", {
Position = UDim2.new(0, 0, 0, HEADER_HEIGHT),
Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT),
ScrollBarThickness = 5,
CanvasSize = UDim2.new(1, 0, 0, componentHeight),
BackgroundTransparency = 1,
}, elements),
})
end
return DataConsumer(DataStoresChart, "DataStoresData")
@@ -0,0 +1,16 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local DataProvider = require(script.Parent.Parent.DataProvider)
local DataStoresChart = require(script.Parent.DataStoresChart)
it("should create and destroy without errors", function()
local element = Roact.createElement(DataProvider, {}, {
DataStoresChart = Roact.createElement(DataStoresChart)
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,108 @@
local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer)
local Signal = require(script.Parent.Parent.Parent.Signal)
local MAX_DATASET_COUNT = tonumber(settings():GetFVariable("NewDevConsoleMaxGraphCount"))
local getClientReplicator = require(script.Parent.Parent.Parent.Util.getClientReplicator)
local DataStoresData = {}
DataStoresData.__index = DataStoresData
function DataStoresData.new()
local self = {}
setmetatable(self, DataStoresData)
self._dataStoresUpdated = Signal.new()
self._dataStoresData = {}
self._dataStoresDataCount = 0
self._lastUpdateTime = 0
return self
end
function DataStoresData:Signal()
return self._dataStoresUpdated
end
function DataStoresData:getCurrentData()
return self._dataStoresData, self._dataStoresDataCount
end
function DataStoresData:updateValue(key, value)
if not self._dataStoresData[key] then
local newBuffer = CircularBuffer.new(MAX_DATASET_COUNT)
newBuffer:push_back({
value = value,
time = self._lastUpdateTime
})
self._dataStoresData[key] = {
max = value,
min = value,
dataSet = newBuffer,
}
else
local dataEntry = self._dataStoresData[key]
local currMax = dataEntry.max
local currMin = dataEntry.min
local update = {
value = value,
time = self._lastUpdateTime
}
local overwrittenEntry = self._dataStoresData[key].dataSet:push_back(update)
if overwrittenEntry then
local iter = self._dataStoresData[key].dataSet:iterator()
local dat = iter:next()
if currMax == overwrittenEntry.value then
currMax = currMin
while dat do
currMax = dat.value < currMax and currMax or dat.value
dat = iter:next()
end
end
if currMin == overwrittenEntry.value then
currMin = currMax
while dat do
currMin = currMin < dat.value and currMin or dat.value
dat = iter:next()
end
end
end
self._dataStoresData[key].max = currMax < value and value or currMax
self._dataStoresData[key].min = currMin < value and currMin or value
end
end
function DataStoresData:start()
local clientReplicator = getClientReplicator()
if clientReplicator and not self._statsListenerConnection then
self._statsListenerConnection = clientReplicator.StatsReceived:connect(function(stats)
local dataStoreBudget = stats.DataStoreBudget
self._lastUpdateTime = os.time()
if dataStoreBudget then
local count = 0
for k, v in pairs(dataStoreBudget) do
if type(v) == 'number' then
self:updateValue(k,v)
count = count + 1
end
end
self._dataStoresDataCount = count
self._dataStoresUpdated:Fire(self._dataStoresData, self._dataStoresDataCount)
end
end)
clientReplicator:RequestServerStats(true)
end
end
function DataStoresData:stop()
-- listeners are responsible for disconnecting themselves
if self._statsListenerConnection then
self._statsListenerConnection:Disconnect()
self._statsListenerConnection = nil
end
end
return DataStoresData
@@ -0,0 +1,105 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent.Parent.Parent.Components
local DataStoresChart = require(Components.DataStores.DataStoresChart)
local UtilAndTab = require(Components.UtilAndTab)
local Actions = script.Parent.Parent.Parent.Actions
local DataStoresUpdateSearchFilter = require(Actions.DataStoresUpdateSearchFilter)
local Constants = require(script.Parent.Parent.Parent.Constants)
local PADDING = Constants.GeneralFormatting.MainRowPadding
local MainViewDataStores = Roact.PureComponent:extend("MainViewDataStores")
function MainViewDataStores:init()
self.onUtilTabHeightChanged = function(utilTabHeight)
self:setState({
utilTabHeight = utilTabHeight
})
end
self.onSearchTermChanged = function(newSearchTerm)
self.props.dispatchDataStoresUpdateSearchFilter(newSearchTerm, {})
end
self.utilRef = Roact.createRef()
self.state = {
utilTabHeight = 0
}
end
function MainViewDataStores:didMount()
local utilSize = self.utilRef.current.Size
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
function MainViewDataStores:didUpdate()
local utilSize = self.utilRef.current.Size
if utilSize.Y.Offset ~= self.state.utilTabHeight then
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
end
function MainViewDataStores:render()
local size = self.props.size
local formFactor = self.props.formFactor
local tabList = self.props.tabList
local searchTerm = self.props.storesSearchTerm
local utilTabHeight = self.state.utilTabHeight
return Roact.createElement("Frame", {
Size = size,
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
LayoutOrder = 3,
}, {
UIListLayout = Roact.createElement("UIListLayout", {
Padding = UDim.new(0, PADDING),
SortOrder = Enum.SortOrder.LayoutOrder,
}),
UtilAndTab = Roact.createElement(UtilAndTab, {
windowWidth = size.X.Offset,
formFactor = formFactor,
tabList = tabList,
searchTerm = searchTerm,
layoutOrder = 1,
refForParent = self.utilRef,
onHeightChanged = self.onUtilTabHeightChanged,
onSearchTermChanged = self.onSearchTermChanged,
}),
DataStores = utilTabHeight > 0 and Roact.createElement(DataStoresChart, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
searchTerm = searchTerm,
layoutOrder = 2,
})
})
end
local function mapStateToProps(state, props)
return {
storesSearchTerm = state.DataStoresData.storesSearchTerm,
}
end
local function mapDispatchToProps(dispatch)
return {
dispatchDataStoresUpdateSearchFilter = function(searchTerm, filters)
dispatch(DataStoresUpdateSearchFilter(searchTerm, filters))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewDataStores)
@@ -0,0 +1,36 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local MainViewDataStores = require(script.Parent.MainViewDataStores)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0
},
DataStoresData = {
storesSearchTerm = ""
}
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
MainViewDataStores = Roact.createElement(MainViewDataStores, {
size = UDim2.new(),
tabList = {},
})
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,184 @@
local CorePackages = game:GetService("CorePackages")
local RobloxGui = game:GetService("CoreGui").RobloxGui
local TextService = game:GetService("TextService")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Constants = require(script.Parent.Parent.Constants)
local FRAME_HEIGHT = Constants.TopBarFormatting.FrameHeight
local ICON_SIZE = .5 * FRAME_HEIGHT
local ICON_PADDING = (FRAME_HEIGHT - ICON_SIZE) / 2
local DEVCONSOLE_TEXT = "Developer Console"
local DEVCONSOLE_TEXT_X_PADDING = 4
local DEVCONSOLE_TEXT_FRAMESIZE = TextService:GetTextSize(DEVCONSOLE_TEXT, Constants.DefaultFontSize.TopBar,
Constants.Font.TopBar, Vector2.new(0, 0))
local LiveUpdateElement = require(script.Parent.Parent.Components.LiveUpdateElement)
local SetDevConsolePosition = require(script.Parent.Parent.Actions.SetDevConsolePosition)
local DevConsoleTopBar = Roact.Component:extend("DevConsoleTopBar")
function DevConsoleTopBar:init()
self.inputBegan = function(rbx,input)
if self.props.isMinimized then
return
end
if input.UserInputType == Enum.UserInputType.MouseButton1 then
local absPos = self.ref.current.AbsolutePosition
local startPos = Vector3.new(absPos.X, absPos.Y, 0)
self:setState({
startPos = startPos,
startOffset = input.Position,
moving = true,
})
end
end
self.inputChanged = function(rbx,input)
if self.state.moving then
local offset = self.state.startPos - self.state.startOffset
offset = offset + input.Position
local position = UDim2.new(0, offset.X, 0, offset.Y)
self.props.dispatchSetDevConsolePosition(position)
end
end
self.inputEnded = function(rbx,input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
self:setState({
moving = false,
})
end
end
self.ref = Roact.createRef()
end
function DevConsoleTopBar:render()
local isMinimized = self.props.isMinimized
local formFactor = self.props.formFactor
local onMinimizeClicked = self.props.onMinimizeClicked
local onMaximizeClicked = self.props.onMaximizeClicked
local onCloseClicked = self.props.onCloseClicked
local moving = self.state.moving
local elements = {}
elements["WindowTitle"] = Roact.createElement("TextLabel", {
Text = DEVCONSOLE_TEXT,
TextSize = Constants.DefaultFontSize.TopBar,
TextColor3 = Color3.new(1, 1, 1),
Font = Constants.Font.TopBar,
Size = UDim2.new(0, DEVCONSOLE_TEXT_FRAMESIZE.X, 0, FRAME_HEIGHT),
Position = UDim2.new(0, DEVCONSOLE_TEXT_X_PADDING, 0, 0),
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
})
local liveStatsModulePos = UDim2.new(0, DEVCONSOLE_TEXT_FRAMESIZE.X, 0, 0)
local liveStatsModuleSize = UDim2.new(1, -2 * DEVCONSOLE_TEXT_FRAMESIZE.X, 0, FRAME_HEIGHT)
if isMinimized then
liveStatsModulePos = UDim2.new(0, 0, 1, 0)
liveStatsModuleSize = UDim2.new(1, 0, 1, 0)
elseif self.ref.current then
liveStatsModuleSize = UDim2.new(
0,
self.ref.current.AbsoluteSize.X - (2 * DEVCONSOLE_TEXT_FRAMESIZE.X),
0,
FRAME_HEIGHT
)
end
local topBarLiveUpdate = self.props.topBarLiveUpdate
elements["LiveStatsModule"] = Roact.createElement(LiveUpdateElement, {
topBarLiveUpdate = topBarLiveUpdate,
size = liveStatsModuleSize,
position = liveStatsModulePos,
})
-- minimize and maximize buttons should only appear on desktop
if formFactor == Constants.FormFactor.Large then
if not isMinimized then
elements["MinButton"] = Roact.createElement("ImageButton", {
Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE),
Position = UDim2.new(1, -2 * FRAME_HEIGHT + ICON_PADDING, 0, ICON_PADDING),
BorderColor3 = Color3.new(1, 0, 0),
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
Image = Constants.Image.Minimize,
[Roact.Event.Activated] = onMinimizeClicked,
})
else
elements["MaxButton"] = Roact.createElement("ImageButton", {
Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE),
Position = UDim2.new(1, -2 * FRAME_HEIGHT + ICON_PADDING, 0, ICON_PADDING),
BorderColor3 = Color3.new(0, 0, 1),
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
Image = Constants.Image.Maximize,
[Roact.Event.Activated] = onMaximizeClicked,
})
end
end
elements["CloseButton"] = Roact.createElement("ImageButton", {
Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE),
Position = UDim2.new(1, -FRAME_HEIGHT + ICON_PADDING, 0, ICON_PADDING),
BorderColor3 = Color3.new(0, 1, 0),
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
Image = Constants.Image.Close,
[Roact.Event.Activated] = onCloseClicked,
})
--[[ we do this to catch all inputchanged events
if we can handle LARGE distances of continuous MouseMovmement input events
for dragging then we might be able to remove the portal
]]--
elements["MovmentCatchAll"] = moving and Roact.createElement(Roact.Portal, {
target = RobloxGui,
}, {
InputCatcher = Roact.createElement("ScreenGui", {}, {
GreyOutFrame = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = Constants.Color.Black,
BackgroundTransparency = .99,
Active = true,
[Roact.Event.InputChanged] = self.inputChanged,
[Roact.Event.InputEnded] = self.inputEnded,
})
})
})
return Roact.createElement("ImageButton", {
Size = UDim2.new(1, 0, 0, FRAME_HEIGHT),
BackgroundColor3 = Constants.Color.Black,
BackgroundTransparency = .5,
AutoButtonColor = false,
LayoutOrder = 1,
[Roact.Ref] = self.ref,
[Roact.Event.InputBegan] = self.inputBegan,
}, elements)
end
local function mapDispatchToProps(dispatch)
return {
dispatchSetDevConsolePosition = function (size)
dispatch(SetDevConsolePosition(size))
end
}
end
return RoactRodux.UNSTABLE_connect2(nil, mapDispatchToProps)(DevConsoleTopBar)
@@ -0,0 +1,32 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.DataProvider)
local DevConsoleTopBar = require(script.Parent.DevConsoleTopBar)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
TopBarLiveUpdate = {
LogWarningCount = 0,
LogErrorCount = 0
}
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, nil, {
DevConsoleTopBar = Roact.createElement(DevConsoleTopBar)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,317 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui").RobloxGui
local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")
local setMouseVisibility = require(script.Parent.Parent.Util.setMouseVisibility)
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local DevConsole = script.Parent.Parent
local Constants = require(DevConsole.Constants)
local TOPBAR_HEIGHT = Constants.TopBarFormatting.FrameHeight
local ROW_PADDING = Constants.Padding.WindowPadding
local MIN_SIZE = Constants.MainWindowInit.MinSize
local Components = DevConsole.Components
local DevConsoleTopBar = require(Components.DevConsoleTopBar)
local Actions = script.Parent.Parent.Actions
local ChangeDevConsoleSize = require(Actions.ChangeDevConsoleSize)
local SetDevConsoleVisibility = require(Actions.SetDevConsoleVisibility)
local SetDevConsoleMinimized = require(Actions.SetDevConsoleMinimized)
local BORDER_SIZE = 16
local DevConsoleWindow = Roact.PureComponent:extend("DevConsoleWindow")
function DevConsoleWindow:onMinimizeClicked()
self.props.dispatchSetDevConsoleMinimized(true)
end
function DevConsoleWindow:onMaximizeClicked()
self.props.dispatchSetDevConsoleMinimized(false)
end
function DevConsoleWindow:onCloseClicked()
self.props.dispatchSetDevConsolVisibility(false)
end
function DevConsoleWindow:init()
self.setDevConsoleSize = function (self, topLeft, bottomRight)
local x = bottomRight.X - topLeft.X
local y = bottomRight.Y - topLeft.Y
x = x < MIN_SIZE.X and MIN_SIZE.X or x
y = y < MIN_SIZE.Y and MIN_SIZE.Y or y
self.props.dispatchChangeDevConsoleSize(UDim2.new(0, x, 0, y))
end
self.resizeInputBegan = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
self:setState({
resizing = true,
})
end
end
self.resizeInputChanged = function(rbx,input)
if self.state.resizing then
local currPosition = self.ref.current.AbsolutePosition
local cornerPos = input.Position
self:setDevConsoleSize(currPosition, cornerPos)
end
end
self.resizeInputEnded = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
--reset resize-dragger
self:setState({
resizing = false
})
end
end
self.doGamepadMenuButton = function(input)
if self.props.isVisible then
local keyToListenTo = input.KeyCode == Enum.KeyCode.ButtonStart or input.KeyCode == Enum.KeyCode.Escape
if keyToListenTo then
self.props.dispatchSetDevConsolVisibility(false)
end
end
end
self.ref = Roact.createRef()
self.state = {
resizing = false,
}
end
function DevConsoleWindow:didMount()
-- we need to run this delay before grabbing the size of the frame
-- because the DevconsoleWindow is mounted before the ScreenGui
-- is resized to the correct screen size.
delay(0, function ()
if self.ref.current then
local absPos1 = self.ref.current.AbsolutePosition
local absPos2 = self.ref.current.ResizeButton.AbsolutePosition
self:setDevConsoleSize(absPos1, absPos2)
end
end)
setMouseVisibility(self.props.isVisible)
self.gamepadMenuListener = UserInputService.InputBegan:Connect(self.doGamepadMenuButton)
end
function DevConsoleWindow:willUnmount()
if GuiService.SelectedCoreObject == self.ref.current then
GuiService.SelectedCoreObject = nil
end
self.gamepadMenuListener:Disconnect()
end
function DevConsoleWindow:didUpdate(previousProps, previousState)
setMouseVisibility(self.props.isVisible)
if self.props.isMinimized and (GuiService.SelectedCoreObject == self.ref.current) then
GuiService.SelectedCoreObject = nil
elseif self.props.isVisible ~= previousProps.isVisible or
self.props.currTabIndex ~= previousProps.currTabIndex then
local inputTypeEnum = UserInputService:GetLastInputType()
local isGamepad = (Enum.UserInputType.Gamepad1 == inputTypeEnum)
if isGamepad and self.props.isVisible then
GuiService.SelectedCoreObject = self.ref.current
else
GuiService.SelectedCoreObject = nil
end
end
end
function DevConsoleWindow:render()
local isVisible = self.props.isVisible
local formFactor = self.props.formFactor
local isdeveloperView = self.props.isdeveloperView
local currTabIndex = self.props.currTabIndex
local tabList = self.props.tabList
local isMinimized = self.props.isMinimized
local pos = self.props.position
local size = self.props.size
local resizing = self.state.resizing
local windowSize = size
local windowPos = UDim2.new()
local elements = {}
local borderSizePixel = BORDER_SIZE
if formFactor ~= Constants.FormFactor.Large then
-- none desktop/Large are full screen devconsoles
local absSize = CoreGui.AbsoluteSize
size = UDim2.new(0, absSize.X, 0, absSize.Y)
pos = UDim2.new(0, 0, 0, 0)
windowPos = UDim2.new(0, 16, 0, 0)
windowSize = size + UDim2.new(0, -32, 0, 0)
borderSizePixel = 0
end
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
Padding = UDim.new(0, ROW_PADDING),
})
if isMinimized then
elements["TopBar"] = Roact.createElement(DevConsoleTopBar, {
LayoutOrder = 1,
formFactor = formFactor,
isMinimized = true,
onMinimizeClicked = function()
self:onMinimizeClicked()
end,
onMaximizeClicked = function()
self:onMaximizeClicked()
end,
onCloseClicked = function()
self:onCloseClicked()
end,
})
return Roact.createElement("Frame", {
Position = UDim2.new(1, -500, 1, -2 * TOPBAR_HEIGHT),
Size = UDim2.new(0, 500, 0, 2 * TOPBAR_HEIGHT),
BackgroundColor3 = Color3.new(0, 0, 0),
Transparency = Constants.MainWindowInit.Transparency,
Active = true,
AutoLocalize = false,
Visible = isVisible,
Selectable = true,
BorderColor3 = Constants.Color.BaseGray,
[Roact.Ref] = self.ref,
}, elements)
else
elements["TopBar"] = Roact.createElement(DevConsoleTopBar, {
LayoutOrder = 1,
formFactor = formFactor,
isMinimized = false,
onMinimizeClicked = function()
self:onMinimizeClicked()
end,
onMaximizeClicked = function()
self:onMaximizeClicked()
end,
onCloseClicked = function()
self:onCloseClicked()
end,
})
local mainViewSize = windowSize
local TopSectionHeight = TOPBAR_HEIGHT + 2 * ROW_PADDING
local mainViewSizeOffset = UDim2.new(0, 0, 0, TopSectionHeight)
mainViewSize = mainViewSize - mainViewSizeOffset
if self.ref.current and isVisible and tabList then
local targetTab = tabList[currTabIndex]
if targetTab then
elements["MainView"] = Roact.createElement( targetTab.tab, {
size = mainViewSize,
formFactor = formFactor,
isdeveloperView = isdeveloperView,
tabList = tabList,
})
end
end
return Roact.createElement("Frame", {
Position = pos,
Size = size,
Visible = isVisible,
BackgroundColor3 = Color3.new(0, 0, 0),
Transparency = Constants.MainWindowInit.Transparency,
BorderColor3 = Constants.Color.BaseGray,
BorderSizePixel = borderSizePixel,
Active = true,
AutoLocalize = false,
Selectable = false,
[Roact.Ref] = self.ref,
}, {
DevConsoleUI = Roact.createElement("Frame", {
Size = windowSize,
Position = windowPos,
BackgroundTransparency = 1,
},elements),
ResizeButton = Roact.createElement("ImageButton", {
Position = UDim2.new(1, 0, 1, 0),
Size = UDim2.new(0, borderSizePixel, 0, borderSizePixel),
BackgroundColor3 = Color3.new(0, 0, 0),
Modal = true,
[Roact.Event.InputBegan] = self.resizeInputBegan,
}),
--[[ we do this to catch all inputchanged events
if we can handle LARGE distances of continuous MouseMovmement input events
for dragging then we might be able to remove the portal
]]--
ResizeCatchAll = resizing and Roact.createElement(Roact.Portal, {
target = CoreGui,
}, {
InputCatcher = Roact.createElement("ScreenGui", {}, {
GreyOutFrame = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = Constants.Color.Black,
BackgroundTransparency = .99,
Active = true,
[Roact.Event.InputChanged] = self.resizeInputChanged,
[Roact.Event.InputEnded] = self.resizeInputEnded,
})
})
})
})
end
end
local function mapStateToProps(state, props)
return {
isVisible = state.DisplayOptions.isVisible,
isMinimized = state.DisplayOptions.isMinimized,
position = state.DisplayOptions.position,
size = state.DisplayOptions.size,
currTabIndex = state.MainView.currTabIndex,
tabList = state.MainView.tabList,
}
end
local function mapDispatchToProps(dispatch)
return {
dispatchChangeDevConsoleSize = function (size)
dispatch(ChangeDevConsoleSize(size))
end,
dispatchSetDevConsolVisibility = function (isVisible)
dispatch(SetDevConsoleVisibility(isVisible))
end,
dispatchSetDevConsoleMinimized = function (isMinimized)
dispatch(SetDevConsoleMinimized(isMinimized))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(DevConsoleWindow)
@@ -0,0 +1,51 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.DataProvider)
local DevConsoleWindow = require(script.Parent.DevConsoleWindow)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
DisplayOptions = {
isVisible = 0,
platform = 0,
isMinimized = false,
size = UDim2.new(1, 0, 1, 0),
},
MainView = {
currTabIndex = 0
},
TopBarLiveUpdate = {
LogWarningCount = 0,
LogErrorCount = 0
},
LogData = {
clientData = { },
clientDataFiltered = { },
clientSearchTerm = "",
clientTypeFilters = { },
serverData = { },
serverDataFiltered = { },
serverSearchTerm = "",
serverTypeFilters = { },
}
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
DevConsoleWindow = Roact.createElement(DevConsoleWindow)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,154 @@
local CorePackages = game:GetService("CorePackages")
local RobloxGui = game:GetService("CoreGui").RobloxGui
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local FONT = Constants.Font.UtilBar
local FONT_SIZE = Constants.DefaultFontSize.UtilBar
local ARROW_SIZE = Constants.GeneralFormatting.DropDownArrowHeight
local ARROW_OFFSET = ARROW_SIZE / 2
local OPEN_ARROW = Constants.Image.DownArrow
local INNER_FRAME_PADDING = 12
local DropDown = Roact.Component:extend("DropDown")
function DropDown:init()
self.onMainButtonPressed = function(rbx, input)
self:setState({
showDropDown = true,
})
end
self.nonDropDownSelection = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or
(input.UserInputType == Enum.UserInputType.Touch and
input.UserInputState == Enum.UserInputState.End) then
self:setState({
showDropDown = false
})
end
end
self.state = {
showDropDown = false,
}
self.ref = Roact.createRef()
end
function DropDown:render()
local buttonSize = self.props.buttonSize
local dropDownList = self.props.dropDownList
local selectedIndex = self.props.selectedIndex
local onSelection = self.props.onSelection
local layoutOrder = self.props.layoutOrder
local showDropDown = self.ref.current and self.state.showDropDown
local children = {}
local absolutePosition
local outerFrameSize
local frameHeight = 0
local frameWidth = 0
if self.ref.current and showDropDown then
local absolutePos = self.ref.current.AbsolutePosition
local absoluteSize = self.ref.current.AbsoluteSize
frameWidth = absoluteSize.X
children["UIListLayout"] = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Left,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Top,
})
for ind, name in pairs(dropDownList) do
local color = (ind == selectedIndex) and Constants.Color.SelectedGray or Constants.Color.UnselectedGray
children[name] = Roact.createElement("TextButton", {
Size = buttonSize,
Text = name,
TextColor3 = Constants.Color.Text,
TextSize = FONT_SIZE,
Font = FONT,
AutoButtonColor = false,
BackgroundColor3 = color,
BackgroundTransparency = 0,
BorderSizePixel = 0,
LayoutOrder = ind,
[Roact.Event.Activated] = function()
onSelection(ind)
self:setState({
showDropDown = false
})
end
})
frameHeight = frameHeight + absoluteSize.Y
end
local padding = 2 * INNER_FRAME_PADDING
outerFrameSize = UDim2.new(0, frameWidth + padding, 0, frameHeight + padding)
absolutePosition = UDim2.new(0, absolutePos.X, 0, absolutePos.Y + absoluteSize.Y)
end
return Roact.createElement("TextButton", {
Size = buttonSize,
Text = dropDownList[selectedIndex],
TextColor3 = Constants.Color.Text,
TextSize = FONT_SIZE,
Font = FONT,
AutoButtonColor = false,
BackgroundColor3 = Constants.Color.UnselectedGray,
BackgroundTransparency = 0,
LayoutOrder = layoutOrder,
[Roact.Event.Activated] = self.onMainButtonPressed,
[Roact.Ref] = self.ref,
}, {
arrow = Roact.createElement("ImageLabel", {
Image = OPEN_ARROW,
BackgroundTransparency = 1,
Size = UDim2.new(0, ARROW_SIZE, 0, ARROW_SIZE),
Position = UDim2.new(1, -ARROW_SIZE - ARROW_OFFSET, .5, -ARROW_OFFSET),
}),
DropDown = showDropDown and Roact.createElement(Roact.Portal, {
target = RobloxGui,
}, {
FullScreen = Roact.createElement("ScreenGui", {
}, {
InputCatcher = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
BackgroundTransparency = 1,
[Roact.Event.InputEnded] = self.nonDropDownSelection,
}, {
OuterFrame = Roact.createElement("ImageButton", {
Size = outerFrameSize,
AutoButtonColor = false,
Position = absolutePosition,
BackgroundColor3 = Constants.Color.TextBoxGray,
BackgroundTransparency = 0,
}, {
innerFrame = Roact.createElement("Frame", {
Position = UDim2.new(0, INNER_FRAME_PADDING, 0 , INNER_FRAME_PADDING),
Size = UDim2.new(0, frameWidth, 0, frameHeight),
BackgroundTransparency = 1,
}, children)
})
})
})
})
})
end
return DropDown
@@ -0,0 +1,14 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local DropDown = require(script.Parent.DropDown)
it("should create and destroy without errors", function()
local element = Roact.createElement(DropDown, {
dropDownList = {},
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,153 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui").RobloxGui
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local ENTRY_HEIGHT = Constants.GeneralFormatting.DropDownEntryHeight
local ARROW_SIZE = Constants.GeneralFormatting.DropDownArrowHeight
local ARROW_OFFSET = ARROW_SIZE / 2
local OPEN_ARROW = Constants.Image.DownArrow
local FULL_SCREEN_WIDTH = 375
local INNER_Y_OFFSET = 8
local INNER_X_OFFSET = 15
local FullScreenDropDownButton = Roact.Component:extend("FullScreenDropDownButton")
function FullScreenDropDownButton:init()
self.startDropDownView = function()
self:setState({
selectionScreenExpanded = true
})
end
self.noSelection = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or
(input.UserInputType == Enum.UserInputType.Touch and
input.UserInputState == Enum.UserInputState.End) then
self:setState({
selectionScreenExpanded = false
})
end
end
self.state = {
selectionScreenExpanded = false,
}
end
function FullScreenDropDownButton:render()
local buttonSize = self.props.buttonSize
local dropDownList = self.props.dropDownList
local selectedIndex = self.props.selectedIndex
local onSelection = self.props.onSelection
local layoutOrder = self.props.layoutOrder
local isSelecting = self.state.selectionScreenExpanded
local dropDownItemList = {}
local scrollingFrameHeight = 2 * INNER_Y_OFFSET
if isSelecting then
dropDownItemList["UIListLayout"] = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Top,
FillDirection = Enum.FillDirection.Vertical,
})
if dropDownList then
for ind,name in ipairs(dropDownList) do
local color = (ind == selectedIndex) and Constants.Color.SelectedGray or Constants.Color.UnselectedGray
dropDownItemList[ind] = Roact.createElement("TextButton", {
Text = name,
Font = Constants.Font.TabBar,
TextSize = Constants.DefaultFontSize.DropDownTabBar,
TextColor3 = Constants.Color.Text,
AutoButtonColor = false,
Size = UDim2.new(1, 0, 0, ENTRY_HEIGHT),
BackgroundColor3 = color,
LayoutOrder = ind,
BorderSizePixel = 0,
[Roact.Event.Activated] = function(rbx)
self:setState({
selectionScreenExpanded = false
})
onSelection(ind)
end,
})
scrollingFrameHeight = scrollingFrameHeight + ENTRY_HEIGHT
end
end
end
return Roact.createElement("TextButton", {
Size = buttonSize,
BackgroundColor3 = Constants.Color.UnselectedGray,
Text = "",
AutoButtonColor = false,
LayoutOrder = layoutOrder,
[Roact.Event.Activated] = self.startDropDownView,
}, {
text = Roact.createElement("TextLabel", {
Size = UDim2.new(1, -ARROW_SIZE - ARROW_OFFSET, 1, 0),
Text = dropDownList[selectedIndex],
Font = Constants.Font.TabBar,
TextSize = Constants.DefaultFontSize.DropDownTabBar,
TextXAlignment = Enum.TextXAlignment.Center,
TextColor3 = Constants.Color.Text,
BackgroundTransparency = 1,
}),
arrow = Roact.createElement("ImageLabel", {
Image = OPEN_ARROW,
BackgroundTransparency = 1,
Size = UDim2.new(0, ARROW_SIZE, 0, ARROW_SIZE),
Position = UDim2.new(1, -ARROW_SIZE - ARROW_OFFSET, .5, -ARROW_OFFSET),
}),
selectionView = isSelecting and Roact.createElement(Roact.Portal, {
target = CoreGui,
}, {
TempScreen = Roact.createElement("ScreenGui", {}, {
GreyOutFrame = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = Constants.Color.Black,
BackgroundTransparency = .36,
Active = true,
[Roact.Event.InputEnded] = self.noSelection,
}, {
BorderFrame = Roact.createElement("Frame", {
Size = UDim2.new(0, FULL_SCREEN_WIDTH, 0, scrollingFrameHeight),
Position = UDim2.new(.5, -FULL_SCREEN_WIDTH / 2, 0, 0),
BackgroundColor3 = Constants.Color.UnselectedGray,
BorderSizePixel = 0,
}, {
SelectionFrame = Roact.createElement("ScrollingFrame", {
Size = UDim2.new(1, -2 * INNER_X_OFFSET, 1, -2 * INNER_Y_OFFSET),
Position = UDim2.new(0, INNER_X_OFFSET, 0, INNER_Y_OFFSET),
BackgroundTransparency = 1,
-- adding an extra entry's worth of height for easier access to last
-- child when trying to select last child
CanvasSize = UDim2.new(1, -2 * INNER_X_OFFSET, 1, ENTRY_HEIGHT),
BorderSizePixel = 0,
ScrollBarThickness = 0,
}, dropDownItemList)
})
})
})
})
})
end
return FullScreenDropDownButton
@@ -0,0 +1,14 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local FullScreenDropDownButton = require(script.Parent.FullScreenDropDownButton)
it("should create and destroy without errors", function()
local element = Roact.createElement(FullScreenDropDownButton, {
dropDownList = {}
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,34 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local FONT = Constants.Font.MainWindowHeader
local TEXT_SIZE = Constants.DefaultFontSize.MainWindowHeader
local TEXT_COLOR = Constants.Color.Text
local function HeaderButton(props)
local text = props.text
local size = props.size
local pos = props.pos
local sortfunction = props.sortfunction
return Roact.createElement("TextButton", {
Text = text,
TextSize = TEXT_SIZE,
TextColor3 = TEXT_COLOR,
Font = FONT,
TextXAlignment = Enum.TextXAlignment.Left,
Size = size,
Position = pos,
BackgroundTransparency = 1,
[Roact.Event.Activated] = function()
if sortfunction then
sortfunction(text)
end
end,
})
end
return HeaderButton
@@ -0,0 +1,305 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local LineGraphHoverDisplay = require(script.Parent.LineGraphHoverDisplay)
local Constants = require(script.Parent.Parent.Constants)
local TEXT_COLOR = Constants.Color.Text
local MAIN_LINE_COLOR = Constants.Color.HighlightBlue
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local LINE_COLOR = Constants.GeneralFormatting.LineColor
local POINT_WIDTH = Constants.Graph.PointWidth
local POINT_OFFSET = Constants.Graph.PointOffset
local GRAPH_PADDING = Constants.Graph.Padding
local GRAPH_SCALE = Constants.Graph.Scale
local GRAPH_Y_INNER_PADDING = Constants.Graph.InnerPaddingY
local GRAPH_Y_INNER_SCALE = Constants.Graph.InnerScaleY
local TEXT_PADDING = Constants.Graph.TextPadding
local INVIS_LINE_THRESHOLD = 10
local LineGraph = Roact.Component:extend("LineGraph")
function LineGraph:init()
self.onGraphInputChanged = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseMovement then
if not self.state.holdPos then
self:setState({
inputPosition = input.Position,
})
end
end
end
self.onGraphInputEnded = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseMovement then
if not self.state.holdPos then
self:setState({
inputPosition = false
})
end
elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
self:setState({
holdPos = not self.state.holdPos
})
end
end
self.graphRef = Roact.createRef()
self.state = {
selectedTimeStamps = {}
}
end
function LineGraph:didUpdate()
if self.state.absGraphSize ~= self.graphRef.current.AbsoluteSize then
local absSize = self.graphRef.current.AbsoluteSize
local absPos = self.graphRef.current.AbsolutePosition
self:setState({
absGraphSize = absSize,
absGraphPos = absPos,
})
end
end
function LineGraph:didMount()
local absSize = self.graphRef.current.AbsoluteSize
local absPos = self.graphRef.current.AbsolutePosition
self:setState({
absGraphSize = absSize,
absGraphPos = absPos,
})
end
function LineGraph:render()
local size = self.props.size
local pos = self.props.pos
local graphData = self.props.graphData
local getX = self.props.getX
local getY = self.props.getY
local stringFormatX = self.props.stringFormatX
local stringFormatY = self.props.stringFormatY
local axisLabelX = self.props.axisLabelX
local axisLabelY = self.props.axisLabelY
local layoutOrder = self.props.layoutOrder
local inputPosition = self.state.inputPosition
local absGraphSize = self.state.absGraphSize
local absGraphPos = self.state.absGraphPos
local maxX = getX(graphData:back())
local minX = getX(graphData:front())
local maxY = self.props.maxY
local minY = self.props.minY
local hoverLineY
local elements = {}
if absGraphSize then
local dataPoints = {}
local dataIter = graphData:iterator()
local data = dataIter:next()
while data do
local datapoint = getY(data)
local time = getX(data)
local xdivisor = maxX - minX
local xPosition = xdivisor > 0 and (time - minX) / xdivisor or 0
local ydivisor = maxY - minY
local yPosition = ydivisor > 0 and (datapoint - minY) / ydivisor or 1
local point = {
X = xPosition,
Y = yPosition,
data = data,
}
table.insert(dataPoints, point)
data = dataIter:next()
end
for i = 2, #dataPoints do
local aX = dataPoints[i].X * absGraphSize.X
local aY = dataPoints[i].Y * absGraphSize.Y * GRAPH_Y_INNER_SCALE
local bX = dataPoints[i - 1].X * absGraphSize.X
local bY = dataPoints[i - 1].Y * absGraphSize.Y * GRAPH_Y_INNER_SCALE
if aX ~= bX then
local vecPosX = (aX + bX) / 2
local vecPosY = (aY + bY) / 2
local vecX = aX - bX
local vecY = aY - bY
local length = math.sqrt((vecX * vecX) + (vecY * vecY))
local rot = math.deg(math.atan2(vecY, vecX))
table.insert(elements, Roact.createElement("Frame", {
Size = UDim2.new(0, length, 0, LINE_WIDTH),
Position = UDim2.new(0, vecPosX - length / 2, 1 - GRAPH_Y_INNER_PADDING, -vecPosY),
BackgroundColor3 = MAIN_LINE_COLOR,
BorderSizePixel = 0,
Rotation = -rot,
}))
table.insert(elements, Roact.createElement("Frame", {
Size = UDim2.new(0, POINT_WIDTH, 0, POINT_WIDTH),
Position = UDim2.new(0, aX, 1 - GRAPH_Y_INNER_PADDING, -aY - POINT_OFFSET),
BackgroundColor3 = MAIN_LINE_COLOR,
BorderSizePixel = 0,
}))
if inputPosition then
local hoverLineX = inputPosition.X - absGraphPos.X
if hoverLineX < aX and bX < hoverLineX then
local aDataX = getX(dataPoints[i].data)
local bDataX = getX(dataPoints[i - 1].data)
local aDataY = getY(dataPoints[i].data)
local bDataY = getY(dataPoints[i - 1].data)
local ratio = (hoverLineX - bX) / vecX
hoverLineY = bY + (vecY * ratio)
local hoverValX = (aDataX - bDataX) * ratio + bDataX
local hoverValY = (aDataY - bDataY) * ratio + bDataY
elements["HoverDetails"] = Roact.createElement(LineGraphHoverDisplay, {
hoverLineX = hoverLineX,
hoverLineY = hoverLineY,
hoverValX = hoverValX,
hoverValY = hoverValY,
stringFormatX = stringFormatX,
stringFormatY = stringFormatY,
})
end
end
end
end
if #dataPoints > 0 then
local lastEntryHeight = dataPoints[#dataPoints].Y * absGraphSize.Y * GRAPH_Y_INNER_SCALE
local currValue = getY(dataPoints[#dataPoints].data)
-- calc if the hovered Y position is very close to the last input entry (within the invis-threshold).
-- If it's NOT within the "invis threshold" then we can show the line and "last entry value"
local showCurrValue = not (hoverLineY and math.abs(lastEntryHeight - hoverLineY) < INVIS_LINE_THRESHOLD)
-- calc if the hovered Y position is very close to the lower Y bound value (within the invis-threshold).
-- If it's NOT within the "invis threshold" then we can show the lowerbound Y value
local hoverLineCheck = (hoverLineY and math.abs(hoverLineY) < INVIS_LINE_THRESHOLD)
local showLeastValue = not (hoverLineCheck or math.abs(lastEntryHeight) < INVIS_LINE_THRESHOLD)
if showCurrValue then
elements["LatestEntryLine"] = Roact.createElement("Frame", {
Size = UDim2.new(1, TEXT_PADDING, 0, LINE_WIDTH),
Position = UDim2.new(0, -TEXT_PADDING, 1 - GRAPH_Y_INNER_PADDING, -lastEntryHeight),
BackgroundColor3 = LINE_COLOR,
BackgroundTransparency = .5,
BorderSizePixel = 0,
})
elements["LatestEntryText"] = Roact.createElement("TextLabel", {
Text = stringFormatY and stringFormatY(currValue) or currValue,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Right,
Position = UDim2.new(0, -TEXT_PADDING - 2, 1 - GRAPH_Y_INNER_PADDING, -lastEntryHeight),
BackgroundTransparency = 1,
})
end
if showLeastValue then
elements["AxisTextY0"] = Roact.createElement("TextLabel", {
Text = stringFormatY and stringFormatY(minY) or minY,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Right,
Position = UDim2.new(0, -TEXT_PADDING - 2, 1 - GRAPH_Y_INNER_PADDING,0),
BackgroundTransparency = 1,
})
end
elements["AxisX"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
elements["AxisY"] = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
end
end
local axisTextPadding = 2 * TEXT_PADDING + 2
return Roact.createElement("Frame", {
Size = size,
Position = pos,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, {
name = Roact.createElement("TextLabel", {
Text = axisLabelY,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Left,
Position = UDim2.new(GRAPH_PADDING, 0, GRAPH_PADDING, -TEXT_PADDING),
BackgroundTransparency = 1,
}),
minX = Roact.createElement("TextLabel", {
Text = stringFormatX and stringFormatX(minX) or minX,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Center,
Position = UDim2.new(GRAPH_PADDING, 0, GRAPH_PADDING + GRAPH_SCALE, axisTextPadding),
BackgroundTransparency = 1,
}),
maxX = Roact.createElement("TextLabel", {
Text = stringFormatX and stringFormatX(maxX) or maxX,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Center,
Position = UDim2.new(GRAPH_PADDING + GRAPH_SCALE, 0, GRAPH_PADDING + GRAPH_SCALE, axisTextPadding),
BackgroundTransparency = 1,
}),
axisLabelX = Roact.createElement("TextLabel", {
Text = axisLabelX,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Center,
-- adding 2 to padding to push the label away from the axis line
Position = UDim2.new(.5, 0, GRAPH_PADDING + GRAPH_SCALE, axisTextPadding),
BackgroundTransparency = 1,
}),
graph = Roact.createElement("Frame", {
Position = UDim2.new(GRAPH_PADDING, 0, GRAPH_PADDING, 0),
Size = UDim2.new(GRAPH_SCALE, 0, GRAPH_SCALE, 0),
BackgroundTransparency = 1,
[Roact.Ref] = self.graphRef,
[Roact.Event.InputChanged] = self.onGraphInputChanged,
[Roact.Event.InputEnded] = self.onGraphInputEnded,
}, elements)
})
end
return LineGraph
@@ -0,0 +1,56 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local HOVER_LINE_COLOR = Constants.Color.HoverGreen
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local TEXT_PADDING = Constants.Graph.TextPadding
local GRAPH_Y_INNER_PADDING = Constants.Graph.InnerPaddingY
return function(props)
local hoverLineX = props.hoverLineX
local hoverLineY = props.hoverLineY
local hoverValX = props.hoverValX
local hoverValY = props.hoverValY
local stringFormatX = props.stringFormatX
local stringFormatY = props.stringFormatY
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}, {
hoverLine = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 1, 0),
Position = UDim2.new(0, hoverLineX, 0, 0),
BackgroundColor3 = HOVER_LINE_COLOR,
BorderSizePixel = 0,
}),
HoverHorizontal = Roact.createElement("Frame", {
Size = UDim2.new(0, hoverLineX + TEXT_PADDING, 0, LINE_WIDTH),
Position = UDim2.new(0, -TEXT_PADDING, 1 - GRAPH_Y_INNER_PADDING, -hoverLineY),
BackgroundColor3 = HOVER_LINE_COLOR,
BorderSizePixel = 0,
}),
HoverTextY = Roact.createElement("TextLabel", {
Text = stringFormatY and stringFormatY(hoverValY) or hoverValY,
TextColor3 = HOVER_LINE_COLOR,
TextXAlignment = Enum.TextXAlignment.Right,
Position = UDim2.new(0, -TEXT_PADDING - 2, 1 - GRAPH_Y_INNER_PADDING, -hoverLineY),
BackgroundTransparency = 1,
}),
HoverTextX = Roact.createElement("TextLabel", {
Text = stringFormatX and stringFormatX(hoverValX) or hoverValX,
TextColor3 = HOVER_LINE_COLOR,
TextXAlignment = Enum.TextXAlignment.Center,
Position = UDim2.new(0, hoverLineX, 1, TEXT_PADDING),
BackgroundTransparency = 1,
}),
})
end
@@ -0,0 +1,308 @@
local CorePackages = game:GetService("CorePackages")
local TextService = game:GetService("TextService")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local DataConsumer = require(script.Parent.Parent.Components.DataConsumer)
local Actions = script.Parent.Parent.Actions
local SetActiveTab = require(Actions.SetActiveTab)
local Constants = require(script.Parent.Parent.Constants)
local MsgTypeNamesOrdered = Constants.MsgTypeNamesOrdered
local TOP_BAR_FONT_SIZE = Constants.DefaultFontSize.TopBar
local TEXT_COLOR = Constants.Color.Text
local FONT = Constants.Font.TopBar
local IMAGE_SIZE = UDim2.new(0, TOP_BAR_FONT_SIZE, 0, TOP_BAR_FONT_SIZE)
local MEM_STAT_STR_SMALL = "Client Mem:"
local memStatStrSmallWidth = TextService:GetTextSize(MEM_STAT_STR_SMALL, TOP_BAR_FONT_SIZE, FONT, Vector2.new(0, 0))
local MEM_STAT_STR = "Client Memory Usage:"
local memStatStrWidth = TextService:GetTextSize(MEM_STAT_STR, TOP_BAR_FONT_SIZE, FONT, Vector2.new(0, 0))
local AVG_PING_STR = "Avg. Ping:"
local avgPingStrWidth = TextService:GetTextSize(AVG_PING_STR, TOP_BAR_FONT_SIZE, FONT, Vector2.new(0, 0))
-- supposed to be the calculated width of the frame, but
-- doing this for now due to time constraints.
local MIN_LARGE_FORMFACTOR_WIDTH = 380
local INNER_PADDING = 6
local LiveUpdateElement = Roact.PureComponent:extend("LiveUpdateElement")
function LiveUpdateElement:didMount()
local totalMemSignal = self.props.ClientMemoryData:totalMemSignal()
self.totalMemConnector = totalMemSignal:Connect(function(totalClientMemory)
self:setState({totalClientMemory = totalClientMemory})
end)
self.avgPingConnector = self.props.ServerStatsData:avgPing():Connect(function(averagePing)
self:setState({averagePing = averagePing})
end)
self.logWarningErrorConnector = self.props.ClientLogData:errorWarningSignal():Connect(function(error, warning)
self:setState({
numErrors = error,
numWarnings = warning,
})
end)
end
function LiveUpdateElement:willUnmount()
self.totalMemConnector:Disconnect()
self.totalMemConnector = nil
self.avgPingConnector:Disconnect()
self.avgPingConnector = nil
self.logWarningErrorConnector:Disconnect()
self.logWarningErrorConnector = nil
end
function LiveUpdateElement:init()
local errorInit, warningInit = self.props.ClientLogData:getErrorWarningCount()
self.onLogWarningButton = function()
local warningFilters = {}
for _, name in pairs(MsgTypeNamesOrdered) do
warningFilters[name] = false
end
warningFilters["Warning"] = true
self.props.ClientLogData:setFilters(warningFilters)
self.props.dispatchChangeTabClientLog()
end
self.onLogErrorButton = function()
local errorFilters = {}
for _, name in pairs(MsgTypeNamesOrdered) do
errorFilters[name] = false
end
errorFilters["Error"] = true
self.props.ClientLogData:setFilters(errorFilters)
self.props.dispatchChangeTabClientLog()
end
self.ref = Roact.createRef()
self.state = {
numErrors = errorInit,
numWarnings = warningInit,
totalClientMemory = 0,
averagePing = 0,
formFactorThreshold = MIN_LARGE_FORMFACTOR_WIDTH,
}
end
function LiveUpdateElement:render()
local size = self.props.size
local position = self.props.position
local formFactor = self.props.formFactor
local numErrors = self.state.numErrors
local numWarnings = self.state.numWarnings
local clientMemoryUsage = self.state.totalClientMemory
local averagePing = self.state.averagePing
local formFactorThreshold = self.state.formFactorThreshold
local useSmallForm = false
local currMemStrWidth = memStatStrWidth.X
local alignment = Enum.HorizontalAlignment.Center
local sizeCheck
if self.ref.current then
sizeCheck = self.ref.current.AbsoluteSize.X < formFactorThreshold
end
if formFactor == Constants.FormFactor.Small or sizeCheck then
position = position + UDim2.new(0, INNER_PADDING * 2, 0, 0)
currMemStrWidth = memStatStrSmallWidth.X
useSmallForm = true
alignment = Enum.HorizontalAlignment.Left
end
local logErrorStat = string.format("%d", numErrors)
local logErrorStatVector = TextService:GetTextSize(
logErrorStat,
TOP_BAR_FONT_SIZE,
FONT,
Vector2.new(0, 0)
)
local logWarningStat = string.format("%d", numWarnings)
local logWarningStatVector = TextService:GetTextSize(
logWarningStat,
TOP_BAR_FONT_SIZE,
FONT,
Vector2.new(0, 0)
)
local memUsageString = string.format("%d MB", clientMemoryUsage)
local memUsageStringVector = TextService:GetTextSize(
memUsageString,
TOP_BAR_FONT_SIZE,
FONT,
Vector2.new(0, 0)
)
local avgPingString = string.format("%d ms", averagePing)
local avgPingStringVector = TextService:GetTextSize(avgPingString,
TOP_BAR_FONT_SIZE,
FONT,
Vector2.new(0, 0)
)
local sizeCheck
if self.ref.current then
sizeCheck = self.ref.current.AbsoluteSize.X < formFactorThreshold
end
if formFactor == Constants.FormFactor.Small or sizeCheck then
position = position + UDim2.new(0, INNER_PADDING * 2, 0, 0)
currMemStrWidth = memStatStrSmallWidth.X
useSmallForm = true
alignment = Enum.HorizontalAlignment.Left
end
local showNetworkPing = averagePing > 0
return Roact.createElement("Frame", {
Position = position,
Size = size,
BackgroundTransparency = 1,
[Roact.Ref] = self.ref,
}, {
UIListLayout = Roact.createElement("UIListLayout", {
Padding = UDim.new(0, INNER_PADDING),
HorizontalAlignment = alignment,
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
LogErrorIcon = Roact.createElement("ImageButton", {
Image = Constants.Image.Error,
Size = IMAGE_SIZE,
BackgroundTransparency = 1,
LayoutOrder = 1,
[Roact.Event.Activated] = self.onLogErrorButton,
}),
LogErrorCount = Roact.createElement("TextButton", {
Text = logErrorStat,
TextSize = TOP_BAR_FONT_SIZE,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Left,
Font = FONT,
Size = UDim2.new(0, logErrorStatVector.X, 1, 0),
BackgroundTransparency = 1,
LayoutOrder = 2,
[Roact.Event.Activated] = self.onLogErrorButton,
}),
ErrorWarningPad = Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = 3,
}),
LogWarningIcon = Roact.createElement("ImageButton", {
Image = Constants.Image.Warning,
Size = IMAGE_SIZE,
BackgroundTransparency = 1,
LayoutOrder = 4,
[Roact.Event.Activated] = self.onLogWarningButton,
}),
LogWarningCount = Roact.createElement("TextButton", {
Text = logWarningStat,
TextSize = TOP_BAR_FONT_SIZE,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Left,
Font = FONT,
Size = UDim2.new(0, logWarningStatVector.X, 1, 0),
BackgroundTransparency = 9,
LayoutOrder = 5,
[Roact.Event.Activated] = self.onLogWarningButton,
}),
WarningMemoryPad = Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = 6,
}),
MemoryUsage = Roact.createElement("TextButton", {
Text = useSmallForm and MEM_STAT_STR_SMALL or MEM_STAT_STR,
TextSize = TOP_BAR_FONT_SIZE,
TextColor3 = Constants.Color.WarningYellow,
TextXAlignment = Enum.TextXAlignment.Right,
Font = FONT,
Size = UDim2.new(0, currMemStrWidth, 1, 0),
BackgroundTransparency = 1,
LayoutOrder = 7,
[Roact.Event.Activated] = self.props.dispatchChangeTabClientMemory,
}),
MemoryUsage_MB = Roact.createElement("TextButton", {
Text = memUsageString,
TextSize = TOP_BAR_FONT_SIZE,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Left,
Font = FONT,
Size = UDim2.new(0, memUsageStringVector.X, 1, 0),
BackgroundTransparency = 1,
LayoutOrder = 8,
[Roact.Event.Activated] = self.props.dispatchChangeTabClientMemory,
}),
MemoryPingPad = Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = 9,
}),
AvgPing = not useSmallForm and showNetworkPing and Roact.createElement("TextButton", {
Text = AVG_PING_STR,
TextSize = TOP_BAR_FONT_SIZE,
TextColor3 = Constants.Color.WarningYellow,
TextXAlignment = Enum.TextXAlignment.Right,
Font = FONT,
Size = UDim2.new(0, avgPingStrWidth.X, 1, 0),
BackgroundTransparency = 1,
LayoutOrder = 10,
[Roact.Event.Activated] = self.props.dispatchChangeTabNetworkPing,
}),
AvgPing_ms = not useSmallForm and showNetworkPing and Roact.createElement("TextButton", {
Text = avgPingString,
TextSize = TOP_BAR_FONT_SIZE,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Left,
Font = FONT,
Size = UDim2.new(0, avgPingStringVector.X, 1, 0),
BackgroundTransparency = 1,
LayoutOrder = 11,
[Roact.Event.Activated] = self.props.dispatchChangeTabNetworkPing,
})
})
end
local function mapDispatchToProps(dispatch)
return {
dispatchChangeTabClientLog = function()
dispatch(SetActiveTab("Log", true))
end,
dispatchChangeTabClientMemory = function()
dispatch(SetActiveTab("Memory", true))
end,
dispatchChangeTabNetworkPing = function()
dispatch(SetActiveTab("ServerStats", true))
end,
}
end
return RoactRodux.UNSTABLE_connect2(nil, mapDispatchToProps)(
DataConsumer(LiveUpdateElement, "ServerStatsData", "ClientMemoryData", "ClientLogData" )
)
@@ -0,0 +1,31 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.DataProvider)
local LiveUpdateElement = require(script.Parent.LiveUpdateElement)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, nil, {
LiveUpdateElement = Roact.createElement(LiveUpdateElement, {
size = UDim2.new(),
position = UDim2.new(),
})
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,23 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local DataConsumer = require(script.Parent.Parent.DataConsumer)
local LogOutput = require(script.Parent.LogOutput)
local ClientLog = Roact.Component:extend("ClientLog")
function ClientLog:init()
self.initClientLogData = function()
return self.props.ClientLogData:getLogData()
end
end
function ClientLog:render()
return Roact.createElement(LogOutput, {
layoutOrder = self.props.layoutOrder,
size = self.props.size,
initLogOutput = self.initClientLogData,
targetSignal = self.props.ClientLogData:Signal(),
})
end
return DataConsumer(ClientLog, "ClientLogData")
@@ -0,0 +1,16 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local DataProvider = require(script.Parent.Parent.DataProvider)
local ClientLog = require(script.Parent.ClientLog)
it("should create and destroy without errors", function()
local element = Roact.createElement(DataProvider, {}, {
ClientLog = Roact.createElement(ClientLog)
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,142 @@
local CorePackages = game:GetService("CorePackages")
local UserInputService = game:GetService("UserInputService")
local LogService = game:GetService("LogService")
local Roact = require(CorePackages.Roact)
local DataConsumer = require(script.Parent.Parent.DataConsumer)
local Constants = require(script.Parent.Parent.Parent.Constants)
local COMMANDLINE_INDENT = Constants.LogFormatting.CommandLineIndent
local COMMANDLINE_FONTSIZE = Constants.DefaultFontSize.CommandLine
local FONT = Constants.Font.MainWindow
local DevConsoleCommandLine = Roact.PureComponent:extend("DevConsoleCommandLine")
function DevConsoleCommandLine:init()
self.onFocusLost = function(rbx, enterPressed, inputThatCausedFocusLoss)
if enterPressed then
LogService:ExecuteScript(rbx.Text)
rbx:CaptureFocus()
end
end
self.ref = Roact.createRef()
end
function DevConsoleCommandLine:didMount()
if not self.onFocusConnection then
self.onFocusConnection = UserInputService.InputBegan:Connect(function(input)
if self.ref.current and self.ref.current:IsFocused() then
local rbx = self.ref.current
local serverLogData = self.props.ServerLogData
local cmdHistory = serverLogData:getCommandLineHistory()
local cmdIndex = serverLogData:getCommandLineIndex()
if input.KeyCode == Enum.KeyCode.Up then
local newIndex = cmdIndex + 1
newIndex = math.min(cmdHistory:getSize(), newIndex)
cmdIndex = newIndex
rbx.Text = cmdHistory:reverseAt(newIndex) or ""
elseif input.KeyCode == Enum.KeyCode.Down then
local newIndex = cmdIndex - 1
newIndex = math.max(0, newIndex)
cmdIndex = newIndex
rbx.Text = cmdHistory:reverseAt(newIndex) or ""
elseif input.KeyCode == Enum.KeyCode.Return then
if #rbx.Text:gsub("%s+", "") > 0 then
local prevText = cmdHistory:reverseAt(1)
if prevText ~= rbx.Text then
cmdHistory:push_back(rbx.Text)
end
end
cmdIndex = 0
elseif cmdIndex ~= 0 then
cmdIndex = 0
end
serverLogData:setCommandLineIndex(cmdIndex)
end
end)
end
-- theres an unwanted behavior here
-- if you recapture the same textbox in the onFocuslost event,
-- the "\r" character that triggerd the onFocusLost will not be consumed yet
-- and will subsequently be put into the textbox. This event is meant to fix that.
if not self.fixUnwantedReturnCapture then
self.fixUnwantedReturnCapture = UserInputService.InputEnded:Connect(function(input)
if self.ref.current and self.ref.current:IsFocused() then
if input.KeyCode == Enum.KeyCode.Return then
self.ref.current.Text = ""
end
end
end)
end
end
function DevConsoleCommandLine:willUnmount()
if self.onFocusConnection then
self.onFocusConnection:Disconnect()
self.onFocusConnection = nil
end
if self.fixUnwantedReturnCapture then
self.fixUnwantedReturnCapture:Disconnect()
self.fixUnwantedReturnCapture = nil
end
end
function DevConsoleCommandLine:render()
local height = self.props.height
local pos = self.props.pos
local initText = ""
local cmdIndex = self.props.ServerLogData:getCommandLineIndex()
if cmdIndex ~= 0 then
local cmdHistory = self.props.ServerLogData:getCommandLineHistory()
initText = cmdHistory:reverseAt(cmdIndex) or ""
end
return Roact.createElement("Frame", {
Position = pos,
Size = UDim2.new(1, 0, 0, height),
BackgroundTransparency = 0,
BackgroundColor3 = Constants.Color.TextBoxGray,
BorderColor3 = Constants.Color.BorderGray,
BorderSizePixel = 1,
}, {
Arrow = Roact.createElement("TextLabel", {
Size = UDim2.new(0, COMMANDLINE_INDENT, 1, 0),
BackgroundTransparency = 1,
TextSize = COMMANDLINE_FONTSIZE,
Font = FONT,
Text = "> ",
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Right,
}),
TextBox = Roact.createElement("TextBox", {
Position = UDim2.new(0, COMMANDLINE_INDENT, 0, 0),
Size = UDim2.new(1, -COMMANDLINE_INDENT, 0, height),
BackgroundTransparency = 1,
ShowNativeInput = true,
ClearTextOnFocus = false,
TextColor3 = Constants.Color.Text,
TextXAlignment = 0,
TextSize = COMMANDLINE_FONTSIZE,
Text = initText,
Font = FONT,
PlaceholderText = "command line",
[Roact.Ref] = self.ref,
[Roact.Event.FocusLost] = self.onFocusLost,
})
})
end
return DataConsumer(DevConsoleCommandLine, "ServerLogData")
@@ -0,0 +1,16 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local DataProvider = require(script.Parent.Parent.DataProvider)
local DevConsoleCommandLine = require(script.Parent.DevConsoleCommandLine)
it("should create and destroy without errors", function()
local element = Roact.createElement(DataProvider, {}, {
CmdLine = Roact.createElement(DevConsoleCommandLine),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,335 @@
local LogService = game:GetService("LogService")
local TextService = game:GetService("TextService")
local Constants = require(script.Parent.Parent.Parent.Constants)
local FONT_SIZE = Constants.DefaultFontSize.MainWindow
local FONT = Constants.Font.Log
local MAX_STRING_SIZE = Constants.LogFormatting.MaxStringSize
local MESSAGE_TO_TYPENAME = Constants.EnumToMsgTypeName
local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer)
local Signal = require(script.Parent.Parent.Parent.Signal)
-- 500 is max kept in history in C++ as of 7/2/2018
local MAX_LOG_SIZE = tonumber(settings():GetFVariable("NewDevConsoleMaxLogCount"))
local WARNING_TO_FILTER = {"ClassDescriptor failed to learn", "EventDescriptor failed to learn", "Type failed to learn"}
local MAX_HISTORY = 100
local convertTimeStamp = require(script.Parent.Parent.Parent.Util.convertTimeStamp)
local LogData = {}
LogData.__index = LogData
local function messageEntry(msg, timeAsStr, type)
local fmtMessage
local charCount = #msg
if charCount < MAX_STRING_SIZE then
fmtMessage = string.format("%s -- %s", timeAsStr, msg)
else
fmtMessage = string.format("%s -- %s", timeAsStr, string.sub(msg, 1, MAX_STRING_SIZE))
end
local dims = TextService:GetTextSize(fmtMessage, FONT_SIZE, FONT, Vector2.new())
return {
Message = fmtMessage,
CharCount = charCount,
Type = type,
Dims = dims,
}
end
-- MOST if not all of this code is copied from the
-- Filter "ClassDescriptor failed to learn" errors
local function ignoreWarningMessageOnAdd(message)
if message.Type ~= Enum.MessageType.MessageWarning.Value then
return false
end
local found = false
for _, filterString in ipairs(WARNING_TO_FILTER) do
if string.find(message.Message, filterString) ~= nil then
found = true
break
end
end
return found
end
-- if a message if filtered that means we need to put it into the filtered messages
-- this is defined as the messages that we are searching for when the search
-- feature is being used
local function isMessageFiltered(message, filterTypes, filterTerm)
-- if any types are flagged on, then we need to check against it
if #filterTerm == 0 and not next(filterTypes) then
return false
end
if next(filterTypes) then
if not filterTypes[MESSAGE_TO_TYPENAME[message.Type]] then
return false
end
end
if #filterTerm > 0 then
if string.find(message.Message:lower(), filterTerm:lower()) == nil then
return false
end
end
return true
end
local function validActiveFilters(filterTypes)
local validFilters = false
for _, filterActive in pairs(filterTypes) do
validFilters = validFilters or filterActive
end
return validFilters
end
local function filterMessages(buffer, msgIter, filterTypes, filterTerm)
buffer:reset()
local validFilters = validActiveFilters(filterTypes)
if #filterTerm == 0 and not validFilters then
return
end
local counter = 0
local msg = msgIter:next()
while msg do
if isMessageFiltered(msg, filterTypes, filterTerm) then
counter = counter + 1
buffer:push_back(msg)
end
msg = msgIter:next()
end
if counter == 0 then
if #filterTerm > 0 then
local errorMsg = messageEntry(string.format ("\"%s\" was not found", filterTerm), "", 0)
buffer:push_back(errorMsg)
else
local errorMsg = messageEntry("No Messages were found", "", 0)
buffer:push_back(errorMsg)
end
end
end
function LogData:checkErrorWarningCounter(msgType)
if msgType == Enum.MessageType.MessageWarning.Value then
self._warningCount = self._warningCount + 1
self._errorWarningSignal:Fire(self._errorCount, self._warningCount)
elseif msgType == Enum.MessageType.MessageError.Value then
self._errorCount = self._errorCount + 1
self._errorWarningSignal:Fire(self._errorCount, self._warningCount)
end
end
function LogData.new(isClient)
local self = {}
setmetatable(self, LogData)
self._initialized = false
self._isClient = isClient
self._logData = CircularBuffer.new(MAX_LOG_SIZE)
self._logDataSearched = CircularBuffer.new(MAX_LOG_SIZE)
self._searchTerm = ""
self._commandLineHistory = CircularBuffer.new(MAX_HISTORY)
self._commandLineIndex = 0
self._filters = {}
for _, v in pairs(MESSAGE_TO_TYPENAME) do
self._filters[v] = true
end
self._errorCount = isClient and 0
self._warningCount = isClient and 0
self._logDataUpdate = Signal.new()
self._errorWarningSignal = isClient and Signal.new()
self._filterUpdated = Signal.new()
return self
end
function LogData:Signal()
return self._logDataUpdate
end
function LogData:errorWarningSignal()
return self._errorWarningSignal
end
function LogData:filterUpdatedSignal()
return self._filterUpdated
end
function LogData:setSearchTerm(targetSearchTerm)
if self._searchTerm ~= targetSearchTerm then
self._searchTerm = targetSearchTerm
if self._searchTerm == "" then
self._logDataSearched:reset()
self._logDataUpdate:Fire(self._logData)
else
filterMessages(
self._logDataSearched,
self._logData:iterator(),
self._filters,
self._searchTerm
)
self._logDataUpdate:Fire(self._logDataSearched)
end
end
end
function LogData:getSearchTerm()
return self._searchTerm
end
function LogData:getCommandLineHistory()
return self._commandLineHistory
end
function LogData:getCommandLineIndex()
return self._commandLineIndex
end
function LogData:setCommandLineIndex(index)
self._commandLineIndex = index
end
function LogData:getFilters()
return self._filters
end
function LogData:setFilter(name, newState)
self._filters[name] = newState
if not validActiveFilters(self._filters) then
self._logDataSearched:reset()
self._logDataUpdate:Fire(self._logData)
return
end
filterMessages(
self._logDataSearched,
self._logData:iterator(),
self._filters,
self._searchTerm
)
self._logDataUpdate:Fire(self._logDataSearched)
self._filterUpdated:Fire()
end
function LogData:setFilters(filters)
self._filters = filters
if not validActiveFilters(filters) then
self._logDataSearched:reset()
self._logDataUpdate:Fire(self._logData)
return
end
filterMessages(
self._logDataSearched,
self._logData:iterator(),
self._filters,
self._searchTerm
)
self._logDataUpdate:Fire(self._logDataSearched)
self._filterUpdated:Fire()
end
function LogData:getLogData()
if #self._logDataSearched:getData() > 0 then
return self._logDataSearched
end
return self._logData
end
function LogData:getErrorWarningCount()
return self._errorCount, self._warningCount
end
function LogData:start()
if self._isClient then
if not self._initialized then
self._initialized = true
local Messages = {}
if #Messages == 0 then
local history = LogService:GetLogHistory()
for _, msg in ipairs(history) do
local message = messageEntry(
msg.message or "[DevConsole Error 1]",
convertTimeStamp(msg.timestamp),
msg.messageType.Value
)
if not ignoreWarningMessageOnAdd(message) then
self:checkErrorWarningCounter(msg.messageType.Value)
self._logData:push_back(message)
end
end
end
end
self._connection = LogService.MessageOut:connect(function(text, messageType)
local message = messageEntry(
text or "[DevConsole Error 2]",
convertTimeStamp(os.time()),
messageType.Value
)
if not ignoreWarningMessageOnAdd(message) then
self:checkErrorWarningCounter(messageType.Value)
self._logData:push_back(message)
if #self._logDataSearched:getData() > 0 then
if isMessageFiltered(message, self._filters, self._searchTerm) then
self._logDataSearched:push_back(message)
self._logDataUpdate:Fire(self._logDataSearched)
end
else
self._logDataUpdate:Fire(self._logData)
end
end
end)
else
self._connection = LogService.ServerMessageOut:connect(function(text, messageType, timestamp)
local message = messageEntry(
text or "[DevConsole Error 3]",
convertTimeStamp(timestamp),
messageType.Value
)
if not ignoreWarningMessageOnAdd(message) then
self._logData:push_back(message)
if #self._logDataSearched:getData() > 0 then
if isMessageFiltered(message, self._filters, self._searchTerm) then
self._logDataSearched:push_back(message)
self._logDataUpdate:Fire(self._logDataSearched)
end
else
self._logDataUpdate:Fire(self._logData)
end
end
end)
LogService:RequestServerOutput()
end
end
function LogData:stop()
self._initialized = false
if self._connection then
self._connection:Disconnect()
end
end
return LogData
@@ -0,0 +1,24 @@
return function()
local LogData = require(script.Parent.LogData)
it("should initialize either client or server", function()
local clientLogData = LogData.new(true)
local serverLogData = LogData.new(false)
expect(clientLogData).to.be.ok()
expect(serverLogData).to.be.ok()
end)
it("should get and set the filters", function()
local clientLogData = LogData.new(true)
local key = "Output"
local value = false
local filters = clientLogData:getFilters()
expect(filters[key]).to.equal(true)
clientLogData:setFilter(key, value)
filters = clientLogData:getFilters()
expect(filters[key]).to.equal(value)
end)
end
@@ -0,0 +1,208 @@
local CorePackages = game:GetService("CorePackages")
local TextService = game:GetService("TextService")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Parent.Constants)
local FONT_SIZE = Constants.DefaultFontSize.MainWindow
local FONT = Constants.Font.Log
local ICON_PADDING = Constants.LogFormatting.IconHeight
local FRAME_HEIGHT = Constants.LogFormatting.TextFrameHeight
local LINE_PADDING = Constants.LogFormatting.TextFramePadding
local MAX_STRING_SIZE = Constants.LogFormatting.MaxStringSize
local MAX_STR_MSG = " -- Could not display entire %d character message because message exceeds max displayable length of %d"
local LogOutput = Roact.Component:extend("LogOutput")
function LogOutput:init(props)
local initLogOutput = props.initLogOutput and props.initLogOutput()
self.onCanvasChange = function()
local canvasPos = self.ref.current.CanvasPosition
local absSize = self.ref.current.AbsoluteSize
if self.state.canvasPos ~= canvasPos or
self.state.absSize ~= absSize then
self:setState({
canvasPos = canvasPos,
absSize = absSize,
})
end
end
self.ref = Roact.createRef()
self.state = {
logData = initLogOutput,
absSize = Vector2.new(),
canvasPos = UDim2.new(),
wordWrap = true,
}
end
function LogOutput:willUpdate(nextProps, nextState)
self._canvasSignal:Disconnect()
self._absSizeSignal:Disconnect()
end
function LogOutput:didUpdate()
self._canvasSignal = self.ref.current:GetPropertyChangedSignal("CanvasPosition"):Connect(self.onCanvasChange)
self._absSizeSignal = self.ref.current:GetPropertyChangedSignal("AbsoluteSize"):Connect(self.onCanvasChange)
end
function LogOutput:didMount()
self.logConnector = self.props.targetSignal:Connect(function(data)
self:setState({
logData = data
})
end)
self._canvasSignal = self.ref.current:GetPropertyChangedSignal("CanvasPosition"):Connect(self.onCanvasChange)
self._absSizeSignal = self.ref.current:GetPropertyChangedSignal("AbsoluteSize"):Connect(self.onCanvasChange)
--[[
in some cases, the absolute size is not valid at this point. But in the
case that it is, we want to update the absolute size here since the
absolute size was changed prior to the absSizeSignal being set up
--]]
local absSize = self.ref.current.AbsoluteSize
if absSize.Magnitude > 0 then
self:setState({
absSize = self.ref.current.AbsoluteSize,
})
end
end
function LogOutput:willUnmount()
self.logConnector:Disconnect()
self.logConnector = nil
end
function LogOutput:render()
local layoutOrder = self.props.layoutOrder
local size = self.props.size
local logData = self.state.logData
local absSize = self.state.absSize
local canvasPos = self.state.canvasPos
local wordWrap = self.state.wordWrap
if self.ref.current then
canvasPos = self.ref.current.CanvasPosition
end
local elements = {}
local messageCount = 1
local scrollingFrameHeight = 0
if self.ref.current and logData then
-- FRAME_HEIGHT is used to offset the text for the icon
local frameWidth = absSize.X - FRAME_HEIGHT
local paddingHeight = -1
local usedFrameSpace = 0
local msgIter = logData:iterator()
local message = msgIter:next()
while message do
local fmtMessage = message.Message
local charCount = message.CharCount
local msgDimsY = message.Dims.Y
if wordWrap then
msgDimsY = message.Dims.Y * math.ceil(message.Dims.X / frameWidth)
end
messageCount = messageCount + 1
if scrollingFrameHeight + msgDimsY >= canvasPos.Y then
if usedFrameSpace < absSize.Y then
local color = Constants.Color.Text
local image = ""
if message.Type == Enum.MessageType.MessageOutput.Value then
color = Constants.Color.Text
elseif message.Type == Enum.MessageType.MessageInfo.Value then
color = Constants.Color.HighlightBlue
image = Constants.Image.Info
elseif message.Type == Enum.MessageType.MessageWarning.Value then
color = Constants.Color.WarningYellow
image = Constants.Image.Warning
elseif message.Type == Enum.MessageType.MessageError.Value then
color = Constants.Color.ErrorRed
image = Constants.Image.Errors
end
elements[messageCount] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, msgDimsY),
BackgroundTransparency = 1,
LayoutOrder = messageCount,
}, {
image = Roact.createElement("ImageLabel", {
Image = image,
Size = UDim2.new(0, ICON_PADDING , 0, ICON_PADDING),
Position = UDim2.new(0, ICON_PADDING / 4, .5, -ICON_PADDING / 2),
BackgroundTransparency = 1,
}),
msg = Roact.createElement("TextLabel", {
Text = fmtMessage,
TextColor3 = color,
TextSize = FONT_SIZE,
Font = FONT,
TextXAlignment = Enum.TextXAlignment.Left,
TextWrapped = wordWrap,
Size = UDim2.new(1, 0, 0, msgDimsY),
Position = UDim2.new(0, FRAME_HEIGHT, 0, 0),
BackgroundTransparency = 1,
})
})
end
if paddingHeight < 0 then
paddingHeight = scrollingFrameHeight
else
usedFrameSpace = usedFrameSpace + msgDimsY + LINE_PADDING
end
end
scrollingFrameHeight = scrollingFrameHeight + msgDimsY + LINE_PADDING
if charCount < MAX_STRING_SIZE then
message = msgIter:next()
else
local maxStrMsg = string.format(MAX_STR_MSG, charCount, MAX_STRING_SIZE)
message = {
Message = maxStrMsg,
CharCount = #maxStrMsg,
Type = message.Type,
Dims = TextService:GetTextSize(maxStrMsg, FONT_SIZE, FONT, Vector2.new())
}
end
end
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, LINE_PADDING),
})
elements["WindowingPadding"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, paddingHeight),
BackgroundTransparency = 1,
LayoutOrder = 1,
})
end
return Roact.createElement("ScrollingFrame", {
Size = size,
BackgroundTransparency = 1,
VerticalScrollBarInset = 1,
ScrollBarThickness = 6,
CanvasSize = UDim2.new(1, 0, 0, scrollingFrameHeight),
LayoutOrder = layoutOrder,
[Roact.Ref] = self.ref,
}, elements)
end
return LogOutput
@@ -0,0 +1,16 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Signal = require(script.Parent.Parent.Parent.Signal)
local LogOutput = require(script.Parent.LogOutput)
it("should create and destroy without errors", function()
local element = Roact.createElement(LogOutput, {
targetSignal = Signal.new()
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,191 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent.Parent.Parent.Components
local ClientLog = require(Components.Log.ClientLog)
local ServerLog = require(Components.Log.ServerLog)
local UtilAndTab = require(Components.UtilAndTab)
local DataConsumer = require(Components.DataConsumer)
local Actions = script.Parent.Parent.Parent.Actions
local SetActiveTab = require(Actions.SetActiveTab)
local Constants = require(script.Parent.Parent.Parent.Constants)
local PADDING = Constants.GeneralFormatting.MainRowPadding
local MsgTypeNamesOrdered = Constants.MsgTypeNamesOrdered
local MainViewLog = Roact.PureComponent:extend("MainViewLog")
function MainViewLog:init()
self.onUtilTabHeightChanged = function(utilTabHeight)
self:setState({
utilTabHeight = utilTabHeight
})
end
self.onClientButton = function()
self.props.dispatchSetActiveTab("Log", true)
end
self.onServerButton = function()
self.props.dispatchSetActiveTab("Log", false)
end
self.onCheckBoxChanged = function(boxName, newState)
if self.props.isClientView then
self.props.ClientLogData:setFilter(boxName, newState)
else
self.props.ServerLogData:setFilter(boxName, newState)
end
end
self.filterUpdated = function()
self:setState({})
end
self.onSearchTermChanged = function(newSearchTerm)
if self.props.isClientView then
self.props.ClientLogData:setSearchTerm(newSearchTerm)
else
self.props.ServerLogData:setSearchTerm(newSearchTerm)
end
end
local clientFilterSignal = self.props.ClientLogData:filterUpdatedSignal()
self.clientFilterConnection = clientFilterSignal:Connect(self.filterUpdated)
local serverFilterSignal = self.props.ServerLogData:filterUpdatedSignal()
self.serverFilterConnection = serverFilterSignal:Connect(self.filterUpdated)
self.utilRef = Roact.createRef()
self.state = {
utilTabHeight = 0
}
end
function MainViewLog:didMount()
local utilSize = self.utilRef.current.Size
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
function MainViewLog:didUpdate()
local utilSize = self.utilRef.current.Size
if utilSize.Y.Offset ~= self.state.utilTabHeight then
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
end
function MainViewLog:willUnmount()
if self.clientFilterConnection then
self.clientFilterConnection:Disconnect()
self.clientFilterConnection = nil
end
if self.serverFilterConnection then
self.serverFilterConnection:Disconnect()
self.serverFilterConnection = nil
end
end
function MainViewLog:render()
local size = self.props.size
local formFactor = self.props.formFactor
local isdeveloperView = self.props.isdeveloperView
local tabList = self.props.tabList
local isClientView = self.props.isClientView
local utilTabHeight = self.state.utilTabHeight
local searchTerm
if isClientView then
searchTerm = self.props.ClientLogData:getSearchTerm()
else
searchTerm = self.props.ServerLogData:getSearchTerm()
end
local elements = {}
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, PADDING),
})
local currCheckBoxState
if isClientView then
currCheckBoxState = self.props.ClientLogData:getFilters()
else
currCheckBoxState = self.props.ServerLogData:getFilters()
end
local initCheckBoxes = {}
for i, v in ipairs(MsgTypeNamesOrdered) do
initCheckBoxes[i] = {
name = v,
state = currCheckBoxState[v],
}
end
elements ["UtilAndTab"] = Roact.createElement(UtilAndTab, {
windowWidth = size.X.Offset,
formFactor = formFactor,
tabList = tabList,
orderedCheckBoxState = initCheckBoxes,
isClientView = isClientView,
searchTerm = searchTerm,
layoutOrder = 1,
refForParent = self.utilRef,
onClientButton = isdeveloperView and self.onClientButton,
onServerButton = isdeveloperView and self.onServerButton,
onCheckBoxChanged = self.onCheckBoxChanged,
onSearchTermChanged = self.onSearchTermChanged,
})
if utilTabHeight > 0 then
if isClientView then
elements["ClientLog"] = Roact.createElement(ClientLog, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
layoutOrder = 2,
})
else
elements["ServerLog"] = Roact.createElement(ServerLog, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
layoutOrder = 2,
})
end
end
return Roact.createElement("Frame", {
Size = size,
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
LayoutOrder = 3,
}, elements)
end
local function mapStateToProps(state, props)
return {
isClientView = state.MainView.isClientView
}
end
local function mapDispatchToProps(dispatch)
return {
dispatchSetActiveTab = function (tabListIndex, isClientView)
dispatch(SetActiveTab(tabListIndex, isClientView))
end
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(
DataConsumer(MainViewLog, "ClientLogData", "ServerLogData")
)
@@ -0,0 +1,40 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local MainViewLog = require(script.Parent.MainViewLog)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0
},
LogData = {
clientSearchTerm = "",
clientTypeFilters = {},
serverSearchTerm = "",
serverTypeFilters = {},
}
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
MainViewLog = Roact.createElement(MainViewLog, {
size = UDim2.new(),
tabList = {},
})
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,40 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local DataConsumer = require(script.Parent.Parent.DataConsumer)
local Constants = require(script.Parent.Parent.Parent.Constants)
local COMMANDLINE_HEIGHT = Constants.LogFormatting.CommandLineHeight
local COMMANDLINE_PADDING = 4
local DevConsoleCommandLine = require(script.Parent.DevConsoleCommandLine)
local LogOutput = require(script.Parent.LogOutput)
local ServerLog = Roact.Component:extend("ServerLog")
function ServerLog:init()
self.initServerLogData = function()
return self.props.ServerLogData:getLogData()
end
end
function ServerLog:render()
return Roact.createElement("Frame", {
Size = self.props.size,
BackgroundTransparency = 1,
LayoutOrder = self.props.layoutOrder,
}, {
Scroll = Roact.createElement(LogOutput, {
size = UDim2.new(1, 0 , 1, -(COMMANDLINE_HEIGHT + COMMANDLINE_PADDING)),
targetSignal = self.props.ServerLogData:Signal(),
initLogOutput = self.initServerLogData,
}),
CommandLine = Roact.createElement(DevConsoleCommandLine, {
pos = UDim2.new(0,0,1,-COMMANDLINE_HEIGHT),
height = COMMANDLINE_HEIGHT
}),
})
end
return DataConsumer(ServerLog, "ServerLogData")
@@ -0,0 +1,16 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local DataProvider = require(script.Parent.Parent.DataProvider)
local ServerLog = require(script.Parent.ServerLog)
it("should create and destroy without errors", function()
local element = Roact.createElement(DataProvider, {}, {
ServerLog = Roact.createElement(ServerLog)
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,19 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local DataConsumer = require(Components.DataConsumer)
local MemoryView = require(Components.Memory.MemoryView)
local ClientMemory = Roact.Component:extend("ClientMemory")
function ClientMemory:render()
return Roact.createElement(MemoryView, {
layoutOrder = self.props.layoutOrder,
size = self.props.size,
searchTerm = self.props.searchTerm,
targetMemoryData = self.props.ClientMemoryData,
})
end
return DataConsumer(ClientMemory, "ClientMemoryData")
@@ -0,0 +1,16 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local DataProvider = require(script.Parent.Parent.DataProvider)
local ClientMemory = require(script.Parent.ClientMemory)
it("should create and destroy without errors", function()
local element = Roact.createElement(DataProvider, {}, {
ClientMemory = Roact.createElement(ClientMemory)
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,186 @@
local Signal = require(script.Parent.Parent.Parent.Signal)
local StatsService = game:GetService("Stats")
local StatsUtils = require(script.Parent.Parent.Parent.Parent.Stats.StatsUtils)
local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer)
local Constants = require(script.Parent.Parent.Parent.Constants)
local HEADER_NAMES = Constants.MemoryFormatting.ChartHeaderNames
local MAX_DATASET_COUNT = tonumber(settings():GetFVariable("NewDevConsoleMaxGraphCount"))
local CLIENT_POLLING_INTERVAL = 3 -- seconds
local SORT_COMPARATOR = {
[HEADER_NAMES[1]] = function(a, b)
return a.name < b.name
end,
[HEADER_NAMES[2]] = function(a, b)
return a.dataStats.dataSet:back().data < b.dataStats.dataSet:back().data
end,
}
local ClientMemoryData = {}
ClientMemoryData.__index = ClientMemoryData
function ClientMemoryData.new()
local self = {}
setmetatable(self, ClientMemoryData)
self._pollingId = 0
self._totalMemory = 0
self._memoryData = {}
self._memoryDataSorted = {}
self._treeViewUpdatedSignal = Signal.new()
self._totalMemoryUpdated = Signal.new()
self._sortType = HEADER_NAMES[1]
return self
end
local function GetMemoryPerformanceStatsItem()
local performanceStats = StatsService and StatsService:FindFirstChild("PerformanceStats")
if not performanceStats then
return nil
end
local memoryStats = performanceStats:FindFirstChild(
StatsUtils.StatNames[StatsUtils.StatType_Memory])
return memoryStats
end
function ClientMemoryData:recursiveUpdateEntry(entryList, sortedList, statsItem)
local name = StatsUtils.GetMemoryAnalyzerStatName(statsItem.Name)
local data = statsItem:GetValue()
local children = statsItem:GetChildren()
if not entryList[name] then
local newBuffer = CircularBuffer.new(MAX_DATASET_COUNT)
newBuffer:push_back({
data = data,
time = self._lastUpdate
})
entryList[name] = {
min = data,
max = data,
dataSet = newBuffer,
children = #children > 0 and {},
sortedChildren = #children > 0 and {},
}
local newEntry = {
name = name,
dataStats = entryList[name]
}
table.insert(sortedList, newEntry)
else
local currMax = entryList[name].max
local currMin = entryList[name].min
local update = {
data = data,
time = self._lastUpdate
}
local overwrittenEntry = entryList[name].dataSet:push_back(update)
if overwrittenEntry then
local iter = entryList[name].dataSet:iterator()
local dat = iter:next()
if currMax == overwrittenEntry.data then
currMax = currMin
while dat do
currMax = dat.data < currMax and currMax or dat.data
dat = iter:next()
end
end
if currMin == overwrittenEntry.data then
currMin = currMax
while dat do
currMin = currMin < dat.data and currMin or dat.data
dat = iter:next()
end
end
end
entryList[name].max = currMax < data and data or currMax
entryList[name].min = currMin < data and currMin or data
end
for _, childStatItem in ipairs(children) do
self:recursiveUpdateEntry(
entryList[name].children,
entryList[name].sortedChildren,
childStatItem
)
end
end
function ClientMemoryData:totalMemSignal()
return self._totalMemoryUpdated
end
function ClientMemoryData:treeUpdatedSignal()
return self._treeViewUpdatedSignal
end
function ClientMemoryData:getSortType()
return self._sortType
end
local function recursiveSort(memoryDataSort, comparator)
table.sort(memoryDataSort, comparator)
for _, entry in pairs(memoryDataSort) do
if entry.dataStats.sortedChildren then
recursiveSort(entry.dataStats.sortedChildren, comparator)
end
end
end
function ClientMemoryData:setSortType(sortType)
if SORT_COMPARATOR[sortType] then
self._sortType = sortType
-- do we need a mutex type thing here?
recursiveSort(self._memoryDataSorted, SORT_COMPARATOR[self._sortType])
else
error(string.format("attempted to pass invalid sortType: %s", tostring(sortType)), 2)
end
end
function ClientMemoryData:getMemoryData()
return self._memoryDataSorted
end
function ClientMemoryData:start()
spawn(function()
self._pollingId = self._pollingId + 1
local instanced_pollingId = self._pollingId
while instanced_pollingId == self._pollingId do
local statsItem = GetMemoryPerformanceStatsItem()
if not statsItem then
return nil
end
self._lastUpdate = os.time()
self:recursiveUpdateEntry(self._memoryData, self._memoryDataSorted, statsItem)
if self._totalMemory ~= statsItem:getValue() then
self._totalMemory = statsItem:getValue()
self._totalMemoryUpdated:Fire(self._totalMemory)
end
self._treeViewUpdatedSignal:Fire(self._memoryDataSorted)
wait(CLIENT_POLLING_INTERVAL)
end
end)
end
function ClientMemoryData:stop()
-- listeners are responsible for disconnecting themselves
self._pollingId = self._pollingId + 1
end
return ClientMemoryData
@@ -0,0 +1,146 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent.Parent.Parent.Components
local ClientMemory = require(Components.Memory.ClientMemory)
local ServerMemory = require(Components.Memory.ServerMemory)
local UtilAndTab = require(Components.UtilAndTab)
local Actions = script.Parent.Parent.Parent.Actions
local ClientMemoryUpdateSearchFilter = require(Actions.ClientMemoryUpdateSearchFilter)
local ServerMemoryUpdateSearchFilter = require(Actions.ServerMemoryUpdateSearchFilter)
local Constants = require(script.Parent.Parent.Parent.Constants)
local MAIN_ROW_PADDING = Constants.GeneralFormatting.MainRowPadding
local MainViewMemory = Roact.Component:extend("MainViewMemory")
function MainViewMemory:init()
self.onUtilTabHeightChanged = function(utilTabHeight)
self:setState({
utilTabHeight = utilTabHeight
})
end
self.onClientButton = function()
self:setState({isClientView = true})
end
self.onServerButton = function()
self:setState({isClientView = false})
end
self.onSearchTermChanged = function(newSearchTerm)
if self.state.isClientView then
self.props.dispatchClientMemoryUpdateSearchFilter(newSearchTerm, {})
else
self.props.dispatchServerMemoryUpdateSearchFilter(newSearchTerm, {})
end
end
self.utilRef = Roact.createRef()
self.state = {
utilTabHeight = 0,
isClientView = true,
}
end
function MainViewMemory:didMount()
local utilSize = self.utilRef.current.Size
self:setState({
utilTabHeight = utilSize.Y.Offset,
})
end
function MainViewMemory:didUpdate()
local utilSize = self.utilRef.current.Size
local height = utilSize.Y.Offset
if height ~= self.state.utilTabHeight then
self:setState({
utilTabHeight = height,
})
end
end
function MainViewMemory:render()
local elements = {}
local size = self.props.size
local isdeveloperView = self.props.isdeveloperView
local formFactor = self.props.formFactor
local tabList = self.props.tabList
local utilTabHeight = self.state.utilTabHeight
local isClientView = self.state.isClientView
local searchTerm = isClientView and self.props.clientSearchTerm or self.props.serverSearchTerm
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, MAIN_ROW_PADDING),
})
elements ["UtilAndTab"] = Roact.createElement(UtilAndTab, {
windowWidth = size.X.Offset,
formFactor = formFactor,
tabList = tabList,
isClientView = isClientView,
searchTerm = searchTerm,
layoutOrder = 1,
refForParent = self.utilRef,
onHeightChanged = self.onUtilTabHeightChanged,
onClientButton = isdeveloperView and self.onClientButton,
onServerButton = isdeveloperView and self.onServerButton,
onSearchTermChanged = self.onSearchTermChanged,
})
if utilTabHeight > 0 then
if isClientView then
elements["ClientMemory"] = Roact.createElement(ClientMemory, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
searchTerm = searchTerm,
layoutOrder = 2,
})
else
elements["ServerMemory"] = Roact.createElement(ServerMemory, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
searchTerm = searchTerm,
layoutOrder = 2,
})
end
end
return Roact.createElement("Frame", {
Size = size,
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
LayoutOrder = 3,
}, elements)
end
local function mapStateToProps(state, props)
return {
clientSearchTerm = state.MemoryData.clientSearchTerm,
clientTypeFilters = state.MemoryData.clientTypeFilters,
serverSearchTerm = state.MemoryData.serverSearchTerm,
serverTypeFilters = state.MemoryData.serverTypeFilters,
}
end
local function mapDispatchToProps(dispatch)
return {
dispatchClientMemoryUpdateSearchFilter = function(searchTerm, filters)
dispatch(ClientMemoryUpdateSearchFilter(searchTerm, filters))
end,
dispatchServerMemoryUpdateSearchFilter = function(searchTerm, filters)
dispatch(ServerMemoryUpdateSearchFilter(searchTerm, filters))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewMemory)
@@ -0,0 +1,39 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local MainViewMemory = require(script.Parent.MainViewMemory)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0
},
MemoryData = {
clientSearchTerm = "",
clientTypeFilters = {},
serverSearchTerm = "",
serverTypeFilters = {},
}
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
MainViewMemory = Roact.createElement(MainViewMemory, {
size = UDim2.new(),
tabList = {},
})
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,285 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local HeaderButton = require(Components.HeaderButton)
local MemoryViewEntry = require(script.Parent.MemoryViewEntry)
local Constants = require(script.Parent.Parent.Parent.Constants)
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local LINE_COLOR = Constants.GeneralFormatting.LINE_COLOR
local HEADER_NAMES = Constants.MemoryFormatting.ChartHeaderNames
local HEADER_HEIGHT = Constants.GeneralFormatting.HeaderFrameHeight
local VALUE_CELL_WIDTH = Constants.MemoryFormatting.ValueCellWidth
local CELL_PADDING = Constants.MemoryFormatting.CellPadding
local VALUE_PADDING = Constants.MemoryFormatting.ValuePadding
local ENTRY_HEIGHT = Constants.GeneralFormatting.EntryFrameHeight
local GRAPH_HEIGHT = Constants.GeneralFormatting.LineGraphHeight
local NO_RESULT_SEARCH_STR = Constants.GeneralFormatting.NoResultSearchStr
local MemoryView = Roact.Component:extend("MemoryView")
local function getX(entry)
return entry.time
end
local function getY(entry)
return entry.data
end
local function formatValueStr(value)
return string.format("%.3f", value)
end
function MemoryView:init(props)
self.getOnButtonPress = function (name)
return function(rbx, input)
self:setState({
expandIndex = self.state.expandIndex ~= name and name
})
end
end
self.onSortChanged = function(sortType)
local currSortType = props.targetMemoryData:getSortType()
if sortType == currSortType then
self:setState({
reverseSort = not self.state.reverseSort
})
else
props.targetMemoryData:setSortType(sortType)
self:setState({
reverseSort = false,
})
end
end
self.onCanvasPosChanged = function()
local canvasPos = self.scrollingRef.current.CanvasPosition
if self.state.canvasPos ~= canvasPos then
self:setState({
absScrollSize = self.scrollingRef.current.AbsoluteSize,
canvasPos = canvasPos,
})
end
end
self.scrollingRef = Roact.createRef()
self.state = {
memoryData = props.targetMemoryData:getMemoryData(),
reverseSort = false,
expandIndex = false,
}
end
function MemoryView:willUpdate()
if self.canvasPosConnector then
self.canvasPosConnector:Disconnect()
end
end
function MemoryView:didUpdate()
if self.scrollingRef.current then
local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition")
self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged)
end
end
function MemoryView:didMount()
local treeUpdatedSignal = self.props.targetMemoryData:treeUpdatedSignal()
self.treeViewItemConnector = treeUpdatedSignal:Connect(function(memoryData)
self:setState({
memoryData = memoryData
})
end)
if self.scrollingRef.current then
local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition")
self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged)
self:setState({
absScrollSize = self.scrollingRef.current.AbsoluteSize,
canvasPos = self.scrollingRef.current.CanvasPosition,
})
end
end
function MemoryView:willUnmount()
self.treeViewItemConnector:Disconnect()
end
function MemoryView:recursiveConstructEntries(elements, entry, depth, windowing)
assert(self.scrollingRef.current, "ScrollingFrame not initialized yet")
local expandIndex = self.state.expandIndex
local searchTerm = self.props.searchTerm or ""
local reverseSort = self.state.reverseSort
local canvasPos = self.scrollingRef.current.CanvasPosition
local absScrollSize = self.scrollingRef.current.AbsoluteSize
local name = entry.name
local found = string.find(name:lower(), searchTerm:lower())
if found then
local showGraph = expandIndex == name
local frameHeight = showGraph and ENTRY_HEIGHT + GRAPH_HEIGHT or ENTRY_HEIGHT
if windowing.scrollingFrameHeight + frameHeight >= canvasPos.Y then
if windowing.usedFrameSpace < absScrollSize.Y then
windowing.layoutOrder = windowing.layoutOrder + 1
elements[name] = Roact.createElement(MemoryViewEntry, {
size = UDim2.new(1, 0, 0, frameHeight),
depth = depth,
entry = entry,
showGraph = showGraph,
onButtonPress = self.getOnButtonPress(name),
formatValueStr = formatValueStr,
getX = getX,
getY = getY,
layoutOrder = windowing.layoutOrder,
})
end
if windowing.paddingHeight < 0 then
windowing.paddingHeight = windowing.scrollingFrameHeight
else
windowing.usedFrameSpace = windowing.usedFrameSpace + frameHeight
end
end
windowing.scrollingFrameHeight = windowing.scrollingFrameHeight + frameHeight
end
local sortedChildren = entry.dataStats.sortedChildren
if sortedChildren then
if reverseSort then
local totalChildren = #sortedChildren
for i = 1, totalChildren do
self:recursiveConstructEntries(elements, sortedChildren[totalChildren - i + 1], depth + 1, windowing)
end
else
for _, entry in ipairs(sortedChildren) do
self:recursiveConstructEntries(elements, entry, depth + 1, windowing)
end
end
end
end
function MemoryView:render()
local elements = {}
local layoutOrder = self.props.layoutOrder
local size = self.props.size
local searchTerm = self.props.searchTerm or ""
-- we pass this table into the recursion to keep sum up
-- height totals for windowing
local windowingInfo = {
scrollingFrameHeight = 0,
paddingHeight = -1,
usedFrameSpace = 0,
layoutOrder = 1
}
if self.scrollingRef.current then
for _, entry in ipairs(self.state.memoryData) do
self:recursiveConstructEntries(elements, entry, 0, windowingInfo)
end
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
SortOrder = Enum.SortOrder.LayoutOrder,
})
elements["WindowingPadding"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, windowingInfo.paddingHeight),
BackgroundTransparency = 1,
LayoutOrder = 1,
})
end
if windowingInfo.layoutOrder == 1 then
if searchTerm == "" then
elements["noDataMessage"] = Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
Text = "Awaiting Memory Stats",
TextColor3 = Constants.Color.Text,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
})
else
local noResultSearchStr = string.format(NO_RESULT_SEARCH_STR, searchTerm )
elements["noDataMessage"] = Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
Text = noResultSearchStr,
TextColor3 = Constants.Color.Text,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
})
end
end
return Roact.createElement("Frame", {
Size = size,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, {
Header = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
}, {
Name = Roact.createElement(HeaderButton, {
text = HEADER_NAMES[1],
size = UDim2.new(1 - VALUE_CELL_WIDTH, -VALUE_PADDING - CELL_PADDING, 0, HEADER_HEIGHT),
pos = UDim2.new(0, CELL_PADDING, 0, 0),
sortfunction = self.onSortChanged,
}),
ValueMB = Roact.createElement(HeaderButton, {
text = HEADER_NAMES[2],
size = UDim2.new( VALUE_CELL_WIDTH, -CELL_PADDING, 0, HEADER_HEIGHT),
pos = UDim2.new(1 - VALUE_CELL_WIDTH, VALUE_PADDING, 0, 0),
sortfunction = self.onSortChanged,
}),
TopHorizontal = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 1),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
LowerHorizontal = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
vertical = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 1, 0),
Position = UDim2.new(1 - VALUE_CELL_WIDTH, 0, 0, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
}),
Entries = Roact.createElement("ScrollingFrame", {
Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT),
Position = UDim2.new(0, 0, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
VerticalScrollBarInset = 1,
ScrollBarThickness = 5,
CanvasSize = UDim2.new(1, 0, 0, windowingInfo.scrollingFrameHeight),
[Roact.Ref] = self.scrollingRef,
}, elements),
})
end
return MemoryView
@@ -0,0 +1,30 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Signal = require(script.Parent.Parent.Parent.Signal)
local MemoryView = require(script.Parent.MemoryView)
local dummmyMemoryData = {
getMemoryData = function ()
return {
summaryTable = {},
summaryCount = 0,
entryList = nil,
}
end,
treeUpdatedSignal = function ()
return Signal.new()
end,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(MemoryView, {
targetMemoryData = dummmyMemoryData,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,105 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local CellLabel = require(Components.CellLabel)
local BannerButton = require(Components.BannerButton)
local LineGraph = require(Components.LineGraph)
local Constants = require(script.Parent.Parent.Parent.Constants)
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local LINE_COLOR = Constants.GeneralFormatting.LINE_COLOR
local VALUE_CELL_WIDTH = Constants.MemoryFormatting.ValueCellWidth
local CELL_PADDING = Constants.MemoryFormatting.CellPadding
local VALUE_PADDING = Constants.MemoryFormatting.ValuePadding
local ENTRY_HEIGHT = Constants.GeneralFormatting.EntryFrameHeight
local DEPTH_INDENT = Constants.MemoryFormatting.DepthIndent
local convertTimeStamp = require(script.Parent.Parent.Parent.Util.convertTimeStamp)
return function(props)
local size = props.size
local depth = props.depth
local entry = props.entry
local showGraph = props.showGraph
local layoutOrder = props.layoutOrder
local onButtonPress = props.onButtonPress
local formatValueStr = props.formatValueStr
local getX = props.getX
local getY = props.getY
local offset = depth * DEPTH_INDENT
local dataStats = entry.dataStats
local name = entry.name
local value = dataStats.dataSet:back().data
return Roact.createElement("Frame", {
Size = size,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, {
button = Roact.createElement(BannerButton, {
size = UDim2.new(1, - offset, 0, ENTRY_HEIGHT),
pos = UDim2.new(0, offset, 0, 0),
isExpanded = showGraph,
onButtonPress = onButtonPress,
}, {
name = Roact.createElement(CellLabel, {
text = name,
size = UDim2.new(1, 0, 1, 0),
pos = UDim2.new(0, CELL_PADDING, 0, 0),
}),
horizonal1 = Roact.createElement("Frame", {
Size = UDim2.new(1, -offset , 0, LINE_WIDTH),
Position = UDim2.new(0, offset, 0, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
horizonal2 = Roact.createElement("Frame", {
Size = UDim2.new(1, -offset, 0, LINE_WIDTH),
Position = UDim2.new(0, offset, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
}),
value = Roact.createElement(CellLabel, {
text = formatValueStr(value),
size = UDim2.new(VALUE_CELL_WIDTH, -VALUE_PADDING, 0, ENTRY_HEIGHT),
pos = UDim2.new(1 - VALUE_CELL_WIDTH, VALUE_PADDING, 0, 0),
}),
vertical = Roact.createElement("Frame", {
Size = UDim2.new(0, 1, 0, ENTRY_HEIGHT),
Position = UDim2.new(1 - VALUE_CELL_WIDTH, 0, 0, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
Graph = showGraph and Roact.createElement(LineGraph, {
pos = UDim2.new(0, 0, 0, ENTRY_HEIGHT),
size = UDim2.new(1, 0, 1, -ENTRY_HEIGHT),
graphData = dataStats.dataSet,
maxY = dataStats.max,
minY = dataStats.min,
getX = getX,
getY = getY,
stringFormatX = convertTimeStamp,
stringFormatY = formatValueStr,
axisLabelX = "Timestamp",
axisLabelY = name,
}),
})
end
--return MemoryViewEntry
@@ -0,0 +1,41 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer)
local DataProvider = require(script.Parent.Parent.DataProvider)
local MemoryViewEntry = require(script.Parent.MemoryViewEntry)
it("should create and destroy without errors", function()
local dummyScriptEntry = {
time = 0,
data = {0, 0},
}
local dummyDataSet = CircularBuffer.new(1)
dummyDataSet:push_back(dummyScriptEntry)
local formatValueStr = function()
return ""
end
local dummyEntry = {
name = "",
dataStats = {
dataSet = dummyDataSet,
}
}
local element = Roact.createElement(DataProvider, {}, {
MemoryViewEntry = Roact.createElement(MemoryViewEntry, {
depth = 0,
entry = dummyEntry,
formatValueStr = formatValueStr,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,19 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local DataConsumer = require(Components.DataConsumer)
local MemoryView = require(Components.Memory.MemoryView)
local ServerMemory = Roact.Component:extend("ServerMemory")
function ServerMemory:render()
return Roact.createElement(MemoryView, {
layoutOrder = self.props.layoutOrder,
size = self.props.size,
searchTerm = self.props.searchTerm,
targetMemoryData = self.props.ServerMemoryData,
})
end
return DataConsumer(ServerMemory, "ServerMemoryData")
@@ -0,0 +1,16 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local DataProvider = require(script.Parent.Parent.DataProvider)
local ServerMemory = require(script.Parent.ServerMemory)
it("should create and destroy without errors", function()
local element = Roact.createElement(DataProvider, nil, {
ServerMemory = Roact.createElement(ServerMemory)
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,232 @@
local Signal = require(script.Parent.Parent.Parent.Signal)
local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer)
local Constants = require(script.Parent.Parent.Parent.Constants)
local HEADER_NAMES = Constants.MemoryFormatting.ChartHeaderNames
local MAX_DATASET_COUNT = tonumber(settings():GetFVariable("NewDevConsoleMaxGraphCount"))
local BYTES_PER_MB = 1048576.0
local SORT_COMPARATOR = {
[HEADER_NAMES[1]] = function(a, b)
return a.name < b.name
end,
[HEADER_NAMES[2]] = function(a, b)
return a.dataStats.dataSet:back().data < b.dataStats.dataSet:back().data
end,
}
local getClientReplicator = require(script.Parent.Parent.Parent.Util.getClientReplicator)
local ServerMemoryData = {}
ServerMemoryData.__index = ServerMemoryData
function ServerMemoryData.new()
local self = {}
setmetatable(self, ServerMemoryData)
self._init = false
self._totalMemory = 0
self._memoryData = {}
self._memoryDataSorted = {}
self._coreTreeData = {}
self._coreTreeDataSorted = {}
self._placeTreeData = {}
self._placeTreeDataSorted = {}
self._treeViewUpdated = Signal.new()
self._sortType = HEADER_NAMES[1]
return self
end
function ServerMemoryData:updateEntry(entryList, sortedList, name, data)
if not entryList[name] then
local newBuffer = CircularBuffer.new(MAX_DATASET_COUNT)
newBuffer:push_back({
data = data,
time = self._lastUpdate
})
entryList[name] = {
min = data,
max = data,
dataSet = newBuffer
}
local newEntry = {
name = name,
dataStats = entryList[name]
}
table.insert(sortedList, newEntry)
else
local currMax = entryList[name].max
local currMin = entryList[name].min
local update = {
data = data,
time = self._lastUpdate
}
local overwrittenEntry = entryList[name].dataSet:push_back(update)
if overwrittenEntry then
local iter = entryList[name].dataSet:iterator()
local dat = iter:next()
if currMax == overwrittenEntry.data then
currMax = currMin
while dat do
currMax = dat.data < currMax and currMax or dat.data
dat = iter:next()
end
end
if currMin == overwrittenEntry.data then
currMin = currMax
while dat do
currMin = currMin < dat.data and currMin or dat.data
dat = iter:next()
end
end
end
entryList[name].max = currMax < data and data or currMax
entryList[name].min = currMin < data and currMin or data
end
end
function ServerMemoryData:updateEntryList(entryList, sortedList, statsItems)
-- All values are in bytes.
-- Convert to MB ASAP.
local totalMB = 0
for label, numBytes in pairs(statsItems) do
local value = numBytes / BYTES_PER_MB
totalMB = totalMB + value
self:updateEntry(entryList, sortedList, label, value)
end
return totalMB
end
function ServerMemoryData:updateWithTreeStats(stats)
local update = {
PlaceMemory = 0,
CoreMemory = 0,
UntrackedMemory = 0,
}
for key, value in pairs(stats) do
if key == "totalServerMemory" then
self._totalMemory = value / BYTES_PER_MB
elseif key == "developerTags" then
update.PlaceMemory = self:updateEntryList(self._placeTreeData, self._placeTreeDataSorted, value)
elseif key == "internalCategories" then
update.CoreMemory = self:updateEntryList(self._coreTreeData, self._coreTreeDataSorted, value)
end
end
update.UntrackedMemory = self._totalMemory - update.PlaceMemory - update.CoreMemory
if self._init then
for name, value in pairs(update) do
self:updateEntry(
self._memoryData["Memory"].children,
self._memoryData["Memory"].sortedChildren,
name,
value
)
end
self:updateEntry(self._memoryData, self._memoryDataSorted, "Memory", self._totalMemory)
else
local memChildren = {}
local memChildrenSorted = {}
for name, value in pairs(update) do
self:updateEntry(memChildren, memChildrenSorted, name, value)
end
self:updateEntry(self._memoryData, self._memoryDataSorted, "Memory", self._totalMemory)
memChildren["PlaceMemory"].children = self._placeTreeData
memChildren["PlaceMemory"].sortedChildren = self._placeTreeDataSorted
memChildren["CoreMemory"].children = self._coreTreeData
memChildren["CoreMemory"].sortedChildren = self._coreTreeDataSorted
self._memoryData["Memory"].children = memChildren
self._memoryData["Memory"].sortedChildren = memChildrenSorted
self._init = true
end
end
function ServerMemoryData:totalMemSignal()
return self._totalMemoryUpdated
end
function ServerMemoryData:treeUpdatedSignal()
return self._treeViewUpdated
end
function ServerMemoryData:getSortType()
return self._sortType
end
local function recursiveSort(memoryDataSort, comparator)
table.sort(memoryDataSort, comparator)
for _, entry in pairs(memoryDataSort) do
if entry.dataStats.sortedChildren then
recursiveSort(entry.dataStats.sortedChildren, comparator)
end
end
end
function ServerMemoryData:setSortType(sortType)
if SORT_COMPARATOR[sortType] then
self._sortType = sortType
recursiveSort(self._memoryDataSorted, SORT_COMPARATOR[self._sortType])
else
error(string.format("attempted to pass invalid sortType: %s", tostring(sortType)), 2)
end
end
function ServerMemoryData:getMemoryData()
return self._memoryDataSorted
end
function ServerMemoryData:start()
local clientReplicator = getClientReplicator()
if clientReplicator and not self._statsListenerConnection then
self._statsListenerConnection = clientReplicator.StatsReceived:connect(function(stats)
if not stats.ServerMemoryTree then
return
end
self._lastUpdate = os.time()
local serverMemoryTree = stats.ServerMemoryTree
if serverMemoryTree then
self:updateWithTreeStats(serverMemoryTree)
self._treeViewUpdated:Fire(self._memoryDataSorted)
end
end)
clientReplicator:RequestServerStats(true)
end
end
function ServerMemoryData:stop()
-- listeners are responsible for disconnecting themselves
local clientReplicator = getClientReplicator()
if clientReplicator then
clientReplicator:RequestServerStats(false)
self._statsListenerConnection:Disconnect()
end
end
return ServerMemoryData
@@ -0,0 +1,29 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local DataConsumer = require(Components.DataConsumer)
local NetworkView = require(script.Parent.NetworkView)
local ClientNetwork = Roact.Component:extend("ClientNetwork")
function ClientNetwork:init(props)
self.state = {
targetNetworkData = self.props.ClientNetworkData
}
end
function ClientNetwork:render()
local layoutOrder = self.props.layoutOrder
local searchTerm = self.props.searchTerm
local size = self.props.size
return Roact.createElement(NetworkView, {
size = size,
searchTerm = searchTerm,
layoutOrder = layoutOrder,
targetNetworkData = self.state.targetNetworkData
})
end
return DataConsumer(ClientNetwork, "ClientNetworkData")
@@ -0,0 +1,18 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local DataProvider = require(script.Parent.Parent.DataProvider)
local ClientNetwork = require(script.Parent.ClientNetwork)
it("should create and destroy without errors", function()
local element = Roact.createElement(DataProvider, {}, {
ClientNetwork = Roact.createElement(ClientNetwork, {
size = UDim2.new(),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,145 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent.Parent.Parent.Components
local ClientNetwork = require(Components.Network.ClientNetwork)
local ServerNetwork = require(Components.Network.ServerNetwork)
local UtilAndTab = require(Components.UtilAndTab)
local Actions = script.Parent.Parent.Parent.Actions
local ClientNetworkUpdateSearchFilter = require(Actions.ClientNetworkUpdateSearchFilter)
local ServerNetworkUpdateSearchFilter = require(Actions.ServerNetworkUpdateSearchFilter)
local SetActiveTab = require(Actions.SetActiveTab)
local Constants = require(script.Parent.Parent.Parent.Constants)
local PADDING = Constants.GeneralFormatting.MainRowPadding
local MainViewNetwork = Roact.Component:extend("MainViewNetwork")
function MainViewNetwork:init()
self.onUtilTabHeightChanged = function(utilTabHeight)
self:setState({
utilTabHeight = utilTabHeight
})
end
self.onClientButton = function()
self.props.dispatchSetActiveTab("Network", true)
end
self.onServerButton = function()
self.props.dispatchSetActiveTab("Network", false)
end
self.onSearchTermChanged = function(newSearchTerm)
if self.props.isClientView then
self.props.dispatchClientNetworkUpdateSearchFilter(newSearchTerm, {})
else
self.props.dispatchServerNetworkUpdateSearchFilter(newSearchTerm, {})
end
end
self.utilRef = Roact.createRef()
self.state = {
utilTabHeight = 0,
}
end
function MainViewNetwork:didMount()
local utilSize = self.utilRef.current.Size
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
function MainViewNetwork:didUpdate()
local utilSize = self.utilRef.current.Size
if utilSize.Y.Offset ~= self.state.utilTabHeight then
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
end
function MainViewNetwork:render()
local elements = {}
local size = self.props.size
local formFactor = self.props.formFactor
local tabList = self.props.tabList
local isClientView = self.props.isClientView
local utilTabHeight = self.state.utilTabHeight
local searchTerm = isClientView and self.props.clientSearchTerm or self.props.serverSearchTerm
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, PADDING),
})
elements ["UtilAndTab"] = Roact.createElement(UtilAndTab, {
windowWidth = size.X.Offset,
formFactor = formFactor,
tabList = tabList,
isClientView = isClientView,
searchTerm = searchTerm,
layoutOrder = 1,
refForParent = self.utilRef,
onHeightChanged = self.onUtilTabHeightChanged,
onClientButton = self.onClientButton,
onServerButton = self.onServerButton,
onSearchTermChanged = self.onSearchTermChanged,
})
if utilTabHeight > 0 then
if isClientView then
elements["ClientNetwork"] = Roact.createElement(ClientNetwork, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
searchTerm = searchTerm,
layoutOrder = 2,
})
else
elements["ServerNetwork"] = Roact.createElement(ServerNetwork, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
searchTerm = searchTerm,
layoutOrder = 2,
})
end
end
return Roact.createElement("Frame", {
Size = size,
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
LayoutOrder = 3,
}, elements)
end
local function mapStateToProps(state, props)
return {
isClientView = state.MainView.isClientView,
clientSearchTerm = state.NetworkData.clientSearchTerm,
serverSearchTerm = state.NetworkData.serverSearchTerm,
}
end
local function mapDispatchToProps(dispatch)
return {
dispatchClientNetworkUpdateSearchFilter = function(searchTerm, filters)
dispatch(ClientNetworkUpdateSearchFilter(searchTerm, filters))
end,
dispatchServerNetworkUpdateSearchFilter = function(searchTerm, filters)
dispatch(ServerNetworkUpdateSearchFilter(searchTerm, filters))
end,
dispatchSetActiveTab = function (tabListIndex, isClientView)
dispatch(SetActiveTab(tabListIndex, isClientView))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewNetwork)

Some files were not shown because too many files have changed in this diff Show More