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,6 @@
{
"language": {
"mode": "nonstrict"
}
}
@@ -0,0 +1,53 @@
local ABTestHelper = {}
ABTestHelper.VARIATION_B = 2
ABTestHelper.VARIATION_C = 3
ABTestHelper.VARIATION_D = 4
ABTestHelper.VARIATION_E = 5
local SUBJECT_TYPE_USERID = 1
local PlayersService = game:GetService("Players")
local HttpRbxApiService = game:GetService('HttpRbxApiService')
local HttpService = game:GetService('HttpService')
local BaseUrl = game:GetService('ContentProvider').BaseUrl:lower()
BaseUrl = string.gsub(BaseUrl, "/m.", "/www.")
BaseUrl = string.gsub(BaseUrl, "http:", "https:")
local AbTestEnrollmentsUrl = string.gsub(BaseUrl, 'www', 'abtesting') ..'v1/enrollments'
local LocalPlayer = PlayersService.LocalPlayer
while not LocalPlayer do
PlayersService.PlayerAdded:wait()
LocalPlayer = PlayersService.LocalPlayer
end
-- Returns the Variation of the given AB test the LocalPlayer is enrolled in.
-- Only checks AB tests by UserId, does not support BrowserTrackerId.
function ABTestHelper.GetTestEnrollmentAsync(abTestName)
local abTestRequest = {
{
["ExperimentName"] = abTestName,
["SubjectType"] = SUBJECT_TYPE_USERID,
["SubjectTargetId"] = LocalPlayer.UserId,
}
}
local jsonPostBody = HttpService:JSONEncode(abTestRequest)
local success, abTestEnrollmentsResponse = pcall(function()
return HttpRbxApiService:PostAsyncFullUrl(AbTestEnrollmentsUrl, jsonPostBody)
end)
if success then
local abTestEnrollments = HttpService:JSONDecode(abTestEnrollmentsResponse)
if abTestEnrollments and abTestEnrollments.data then
local enrollment = abTestEnrollments.data[1]
return enrollment.Variation
end
else
warn("Error getting ABTestEnrollment: ", abTestEnrollmentsResponse)
end
return false
end
return ABTestHelper
@@ -0,0 +1,459 @@
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
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,8 @@
{
"lint": {
"SameLineStatement": "fatal",
"LocalShadow": "fatal",
"FunctionUnused": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,312 @@
--[[
// FileName: ContextMenuGui.lua
// Written by: TheGamer101
// Description: Module for creating the context GUI.
]]
-- CONSTANTS
local BOTTOM_SCREEN_PADDING_PERCENT = 0.02
-- 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")
--- FLAGS
local FFlagDisableAutoTranslateForKeyTranslatedContent = require(RobloxGui.Modules.Flags.FFlagDisableAutoTranslateForKeyTranslatedContent)
--- 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
contextMenuHolder.AutoLocalize = not FFlagDisableAutoTranslateForKeyTranslatedContent
return contextMenuHolder
end
function ContextMenuGui:CreateLeaveMenuButton(frame, theme)
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 = theme.LeaveMenuImage
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(theme)
local contextMenuHolder = self:CreateContextMenuHolder()
local menu = Instance.new("ImageButton")
menu.Name = "Menu"
menu.AnchorPoint = theme.AnchorPoint
menu.Size = theme.Size
menu.Position = theme.OnScreenPosition
menu.BackgroundTransparency = theme.BackgroundTransparency
menu.BackgroundColor3 = theme.BackgroundColor
menu.Image = theme.BackgroundImage
menu.ScaleType = theme.BackgroundImageScaleType
menu.SliceCenter = theme.BackgroundImageSliceCenter
menu.AutoButtonColor = false
menu.BorderSizePixel = 0
menu.Selectable = false
menu.Visible = false
menu.Active = true
menu.ClipsDescendants = true
menu.Modal = true
GuiService:AddSelectionParent("AvatarContextMenuGroup", menu)
local aspectConstraint = Instance.new("UIAspectRatioConstraint")
aspectConstraint.AspectType = Enum.AspectType.ScaleWithParentSize
aspectConstraint.DominantAxis = Enum.DominantAxis.Height
aspectConstraint.Name = "MenuAspectRatio"
aspectConstraint.AspectRatio = theme.AspectRatio
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.Name = "MenuSizeConstraint"
sizeConstraint.MaxSize = theme.MaxSize
sizeConstraint.MinSize = theme.MinSize
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 = theme.ButtonFrameColor
contextActionList.BackgroundTransparency = theme.ButtonFrameTransparency
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 = theme.NameTagColor
nameTag.AutoButtonColor = false
nameTag.BorderSizePixel = 0
nameTag.LayoutOrder = 1
nameTag.Size = UDim2.new(1,-12,0.16,0)
nameTag.Font = theme.Font
nameTag.TextColor3 = theme.TextColor
nameTag.TextSize = 24 * theme.TextScale
nameTag.Text = ""
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 = theme.NameUnderlineColor
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, theme)
menu.Parent = contextMenuHolder
self.ContextMenuFrame = menu
return menu
end
function ContextMenuGui:UpdateGuiTheme(theme)
self.ContextMenuFrame.Size = theme.Size
self.ContextMenuFrame.AnchorPoint = theme.AnchorPoint
self.ContextMenuFrame.BackgroundTransparency = theme.BackgroundTransparency
self.ContextMenuFrame.BackgroundColor3 = theme.BackgroundColor
self.ContextMenuFrame.Image = theme.BackgroundImage
self.ContextMenuFrame.ScaleType = theme.BackgroundImageScaleType
self.ContextMenuFrame.SliceCenter = theme.BackgroundImageSliceCenter
self.ContextMenuFrame.CloseMenuButton.Image = theme.LeaveMenuImage
self.ContextMenuFrame.MenuSizeConstraint.MaxSize = theme.MaxSize
self.ContextMenuFrame.MenuSizeConstraint.MinSize = theme.MinSize
self.ContextMenuFrame.MenuAspectRatio.AspectRatio = theme.AspectRatio
local contentFrame = self.ContextMenuFrame.Content
contentFrame.ContextActionList.BackgroundColor3 = theme.ButtonFrameColor
contentFrame.ContextActionList.BackgroundTransparency = theme.ButtonFrameTransparency
contentFrame.NameTag.BackgroundColor3 = theme.NameTagColor
contentFrame.NameTag.Font = theme.Font
contentFrame.NameTag.TextColor3 = theme.TextColor
contentFrame.NameTag.TextSize = 24 * theme.TextScale
contentFrame.NameTag.Underline.BackgroundColor3 = theme.NameUnderlineColor
if PlayerCarousel then
PlayerCarousel:UpdateGuiTheme(theme)
end
end
function ContextMenuGui:BuildPlayerCarousel(playersByProximity, theme)
if not PlayerCarousel then
local playerCarouselModule = require(AvatarMenuModules.PlayerCarousel)
PlayerCarousel = playerCarouselModule.new(theme)
PlayerCarousel.rbxGui.Parent = self.ContextMenuFrame.Content
end
PlayerCarousel:ClearPlayerEntries()
if self.PlayerChangedConnection then
self.PlayerChangedConnection:Disconnect()
end
self.PlayerChangedConnection = 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)
for i = 1, #playersByProximity do
PlayerCarousel:CreatePlayerEntry(playersByProximity[i][1], playersByProximity[i][2])
end
PlayerCarousel:FadeTowardsEdges()
PlayerCarousel:AddCarouselDivider()
end
function ContextMenuGui:RemovePlayerEntry(player)
if not PlayerCarousel then return end
PlayerCarousel:RemovePlayerEntry(player)
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.PlayerChangedConnection = nil
obj.SelectedPlayerChanged = PlayerChangedEvent.Event
return obj
end
return ContextMenuGui.new()
@@ -0,0 +1,422 @@
--[[
// 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.
]]
--- 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("RbxAnalyticsService")
local GuiService = game:GetService("GuiService")
-- MODULES
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local GameTranslator = require(RobloxGui.Modules.GameTranslator)
local AvatarMenuModules = CoreGuiModules:WaitForChild("AvatarContextMenu")
local ContextMenuUtil = require(AvatarMenuModules:WaitForChild("ContextMenuUtil"))
local ThemeHandler = require(AvatarMenuModules.ThemeHandler)
local BlockingUtility = require(CoreGuiModules.BlockingUtility)
-- 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,
[Enum.AvatarContextMenuOption.InspectMenu] = true
}
local CustomContextMenuItems = {}
local CustomItemAddedOrder = 0
-- CONSTANTS
-- If Custom buttons exist these layout orders are offset by highest custom button layout order.
local FRIEND_LAYOUT_ORDER = 1
local CHAT_LAYOUT_ORDER = 3
local INSPECT_AND_BUY_LAYOUT_ORDER = 4
local WAVE_LAYOUT_ORDER = 5
local MENU_ITEM_SIZE_X = 0.96
local MENU_ITEM_SIZE_Y = 0
local MENU_ITEM_SIZE_Y_OFFSET = 52
local VIEW_KEY = "InGame.InspectMenu.Action.View"
local ContextMenuItems = {}
ContextMenuItems.__index = ContextMenuItems
-- PRIVATE METHODS
function ContextMenuItems:UpdateInspectMenuEnabled()
local enabled = GuiService:GetInspectMenuEnabled()
if enabled ~= EnabledContextMenuItems[Enum.AvatarContextMenuOption.InspectMenu] then
EnabledContextMenuItems[Enum.AvatarContextMenuOption.InspectMenu] = enabled
end
end
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)
CustomItemAddedOrder = CustomItemAddedOrder + 1
CustomContextMenuItems[menuOption] = {
event = bindableEvent,
layoutOrder = CustomItemAddedOrder,
}
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, itemInfo in pairs(CustomContextMenuItems) do
AnalyticsService:TrackEvent("Game", "AvatarContextMenuCustomButton", "name: " .. tostring(buttonText))
local function customButtonFunc()
if self.CloseMenuFunc then self:CloseMenuFunc() end
itemInfo.event:Fire(self.SelectedPlayer)
end
buttonText = GameTranslator:TranslateGameText(self.MenuItemFrame, buttonText)
local customButton = ContextMenuUtil:MakeStyledButton(
"CustomButton",
buttonText,
UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET),
customButtonFunc,
ThemeHandler:GetTheme()
)
customButton.Name = "CustomButton"
customButton.LayoutOrder = itemInfo.layoutOrder
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 blockedString = "Player Blocked"
local addFriendDisabledTransparency = 0.75
local friendStatusChangedConn = nil
function ContextMenuItems:CreateFriendButton(status, isBlocked)
addFriendString = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.AddFriend")
friendsString = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.Friends")
friendRequestPendingString = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.FriendRequestPending")
acceptFriendRequestString = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.AcceptFriendRequest")
blockedString = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.PlayerBlocked")
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,
ThemeHandler:GetTheme()
)
if isBlocked then
friendLabel.Selectable = false
friendLabelText.TextTransparency = addFriendDisabledTransparency
friendLabelText.Text = blockedString
elseif status == Enum.FriendStatus.Friend then
friendLabel.Selectable = false
friendLabelText.TextTransparency = addFriendDisabledTransparency
friendLabelText.Text = friendsString
elseif status == Enum.FriendStatus.FriendRequestSent then
friendLabel.Selectable = false
friendLabelText.TextTransparency = addFriendDisabledTransparency
friendLabelText.Text = friendRequestPendingString
elseif status == Enum.FriendStatus.FriendRequestReceived then
friendLabelText.Text = acceptFriendRequestString
else
friendLabel.Selectable = true
friendLabelText.TextTransparency = 0
end
friendLabel.LayoutOrder = FRIEND_LAYOUT_ORDER + CustomItemAddedOrder
friendLabel.Parent = self.MenuItemFrame
end
function ContextMenuItems:UpdateFriendButton(status, isBlocked)
local friendLabel = self.MenuItemFrame:FindFirstChild("FriendStatus")
if friendLabel then
self:CreateFriendButton(status, isBlocked)
end
end
function ContextMenuItems:CreateInspectAndBuyButton()
local function browseItems()
if self.CloseMenuFunc then self:CloseMenuFunc() end
-- If the developer disables the menu while someone is already looking at the ACM
-- the button doesn't disappear, so we need to check again.
if not EnabledContextMenuItems[Enum.AvatarContextMenuOption.InspectMenu] then
warn("The Inspect Menu is not currently available.")
return
end
GuiService:InspectPlayerFromUserIdWithCtx(self.SelectedPlayer.UserId, "avatarContextMenu")
end
local browseItemsButton = ContextMenuUtil:MakeStyledButton(
"View",
RobloxTranslator:FormatByKey(VIEW_KEY),
UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET),
browseItems,
ThemeHandler:GetTheme()
)
browseItemsButton.LayoutOrder = INSPECT_AND_BUY_LAYOUT_ORDER + CustomItemAddedOrder
browseItemsButton.Parent = self.MenuItemFrame
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 waveString = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.Wave")
local waveButton = ContextMenuUtil:MakeStyledButton(
"Wave",
waveString,
UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET),
wave,
ThemeHandler:GetTheme()
)
waveButton.LayoutOrder = WAVE_LAYOUT_ORDER + CustomItemAddedOrder
waveButton.Parent = self.MenuItemFrame
end
function ContextMenuItems:CreateChatButton()
local chatDisabled = false
local function chatFunc()
if chatDisabled then
return
end
if self.CloseMenuFunc then self:CloseMenuFunc() end
AnalyticsService:ReportCounter("AvatarContextMenu-Chat")
AnalyticsService:TrackEvent("Game", "AvatarContextMenuChat", "placeId: " .. tostring(game.PlaceId))
local ChatModule = require(RobloxGui.Modules.ChatSelector)
ChatModule:SetVisible(true)
local eventDidFire = ChatModule:EnterWhisperState(self.SelectedPlayer)
if not eventDidFire then
-- Fallback to the old version for backwards compatibility with old chat versions
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
ChatModule:FocusChatBar()
end
end
local chatString = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.Chat")
local chatButton, chatLabelText = ContextMenuUtil:MakeStyledButton(
"ChatStatus",
chatString,
UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET),
chatFunc,
ThemeHandler:GetTheme()
)
chatButton.LayoutOrder = CHAT_LAYOUT_ORDER + CustomItemAddedOrder
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
local canChatWith = ContextMenuUtil:GetCanChatWith(self.SelectedPlayer)
if not canChatWith then
chatDisabled = true
chatButton.Selectable = false
chatLabelText.TextTransparency = addFriendDisabledTransparency
chatLabelText.Text = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.ChatDisabled")
end
else
chatButton.Parent = nil
end
end
function ContextMenuItems:RemoveLastButtonUnderline()
local buttons = self.MenuItemFrame:GetChildren()
local lastButton = nil
local highestLayoutOrder = -1
for _, button in pairs(buttons) do
if button:IsA("GuiObject") and button.LayoutOrder > highestLayoutOrder then
highestLayoutOrder = button.LayoutOrder
lastButton = button
end
end
if lastButton then
local underline = lastButton:FindFirstChild("Underline")
if underline then
underline:Destroy()
end
end
end
function ContextMenuItems:BuildContextMenuItems(player)
if not player then return end
local friendStatus = ContextMenuUtil:GetFriendStatus(player)
local isBlocked = BlockingUtility:IsPlayerBlockedByUserId(player.UserId)
self:ClearMenuItems()
self:SetSelectedPlayer(player)
if EnabledContextMenuItems[Enum.AvatarContextMenuOption.Friend] then
self:CreateFriendButton(friendStatus, isBlocked)
end
if EnabledContextMenuItems[Enum.AvatarContextMenuOption.Chat] then
self:CreateChatButton()
end
if EnabledContextMenuItems[Enum.AvatarContextMenuOption.Emote] then
self:CreateEmoteButton()
end
if EnabledContextMenuItems[Enum.AvatarContextMenuOption.InspectMenu] then
self:CreateInspectAndBuyButton()
end
self:CreateCustomMenuItems()
self:RemoveLastButtonUnderline()
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()
-- If disabled in a script, sometimes it registers before we can receive the signal.
ContextMenuItems:UpdateInspectMenuEnabled()
return obj
end
GuiService.InspectMenuEnabledChangedSignal:Connect(function(enabled)
if not enabled then
ContextMenuItems:RemoveDefaultMenuItem(Enum.AvatarContextMenuOption.InspectMenu)
else
ContextMenuItems:EnableDefaultMenuItem(Enum.AvatarContextMenuOption.InspectMenu)
end
end)
return ContextMenuItems
@@ -0,0 +1,311 @@
--[[
// 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"
-- todo: remove with GetFFlagUseThumbnailUrl
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")
local RobloxReplicatedStorage = game:GetService("RobloxReplicatedStorage")
--- VARIABLES
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local BlockingUtility = require(CoreGuiModules.BlockingUtility)
local LocalPlayer = PlayersService.LocalPlayer
while not LocalPlayer do
PlayersService.PlayerAdded:wait()
LocalPlayer = PlayersService.LocalPlayer
end
-- FLAGS
local GetFFlagUseThumbnailUrl = require(CoreGuiModules.Common.Flags.GetFFlagUseThumbnailUrl)
local ContextMenuUtil = {}
ContextMenuUtil.__index = ContextMenuUtil
-- PUBLIC METHODS
function ContextMenuUtil:GetHeadshotForPlayer(player)
if GetFFlagUseThumbnailUrl() then
return "rbxthumb://type=AvatarHeadShot&id=" .. player.UserId .. "&w=150&h=150"
else
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
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 CanChatWithMap = {}
coroutine.wrap(function()
local RemoteEvent_CanChatWith = RobloxReplicatedStorage:WaitForChild("CanChatWith", math.huge)
RemoteEvent_CanChatWith.OnClientEvent:Connect(function(userId, canChat)
CanChatWithMap[userId] = canChat
end)
end)()
function ContextMenuUtil:GetCanChatWith(otherPlayer)
if BlockingUtility:IsPlayerBlockedByUserId(otherPlayer.UserId) then
-- This can be removed when Chat:CanUsersChatAsync() correctly respects blocked status.
return false
end
if CanChatWithMap[otherPlayer.UserId] ~= nil then
return CanChatWithMap[otherPlayer.UserId]
end
-- Assume we can chat if we have not received information from the server yet.
return true
end
local SelectionOverrideObject = Instance.new("ImageLabel")
SelectionOverrideObject.Image = ""
SelectionOverrideObject.BackgroundTransparency = 1
local function MakeDefaultButton(name, size, clickFunc, theme)
local button = Instance.new("ImageButton")
button.Name = name
button.Image = theme.ButtonImage
button.ScaleType = theme.ButtonImageScaleType
button.SliceCenter = theme.ButtonImageSliceCenter
button.BackgroundColor3 = theme.ButtonColor
button.BackgroundTransparency = theme.ButtonTransparency
button.AutoButtonColor = false
button.Size = size
button.ZIndex = 2
button.SelectionImageObject = SelectionOverrideObject
button.BorderSizePixel = 0
local underline = Instance.new("Frame")
underline.Name = "Underline"
underline.BackgroundColor3 = theme.ButtonUnderlineColor
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.BackgroundColor3 = theme.ButtonHoverColor
button.BackgroundTransparency = theme.ButtonHoverTransparency
end
local function deselectButton()
button.BackgroundColor3 = theme.ButtonColor
button.BackgroundTransparency = theme.ButtonTransparency
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, theme)
local button = MakeDefaultButton(name, size, clickFunc, theme)
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 = theme.Font
textLabel.TextSize = 24 * theme.TextScale
if isSmallTouchScreen() then
textLabel.TextSize = 18 * theme.TextScale
elseif GuiService:IsTenFootInterface() then
textLabel.TextSize = 36 * theme.TextScale
end
textLabel.Text = text
textLabel.TextScaled = true
textLabel.TextWrapped = true
textLabel.ZIndex = 2
textLabel.Parent = button
local constraint = Instance.new("UITextSizeConstraint", textLabel)
constraint.MaxTextSize = textLabel.TextSize
return button, textLabel
end
function ContextMenuUtil.new()
local obj = setmetatable({}, ContextMenuUtil)
-- todo: remove with GetFFlagUseThumbnailUrl
obj.HeadShotUrlCache = {}
return obj
end
return ContextMenuUtil.new()
@@ -0,0 +1,361 @@
--[[
// 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
local CAROUSEL_DIVIDER_NAME = "_CarouselDivider"
-- 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 getSortedCarouselButtons()
local buttonChildIndexs = {}
local carouselButtons = {}
for childIndex, child in ipairs(selectedPlayer:GetChildren()) do
if child:IsA("GuiObject") and child.Name ~= CAROUSEL_DIVIDER_NAME then
table.insert(carouselButtons, child)
buttonChildIndexs[child] = childIndex
end
end
table.sort(carouselButtons, function(a, b)
if a.LayoutOrder == b.LayoutOrder then
return buttonChildIndexs[a] < buttonChildIndexs[b]
end
return a.LayoutOrder < b.LayoutOrder
end)
return carouselButtons
end
local function CreateMenuCarousel(theme)
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.Circular = true
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"
local lastPage = nil
uiPageLayout:GetPropertyChangedSignal("CurrentPage"):Connect(function()
if uiPageLayout.CurrentPage then
if uiPageLayout.CurrentPage.Name == CAROUSEL_DIVIDER_NAME then
local carouselButtons = getSortedCarouselButtons()
if lastPage == carouselButtons[1] then
uiPageLayout:Previous()
else
uiPageLayout:Next()
end
else
uiPageLayout.CurrentPage.BackgroundColor3 = BACKGROUND_DEFAULT_COLOR
lastPage = uiPageLayout.CurrentPage
end
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 = theme.ScrollRightImage
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 nextButtonAspectRatio = Instance.new("UIAspectRatioConstraint")
nextButtonAspectRatio.DominantAxis = Enum.DominantAxis.Width
nextButtonAspectRatio.Parent = nextButton
local prevButton = nextButton:Clone()
prevButton.Name = "PrevButton"
if theme.ScrollLeftImage ~= "" then
prevButton.Image = theme.ScrollLeftImage
else
prevButton.Rotation = 180
end
prevButton.AnchorPoint = Vector2.new(0, 0.5)
prevButton.Position = UDim2.new(0, 5, 0.5, 0)
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:UpdateGuiTheme(theme)
self.rbxGui.NextButton.Image = theme.ScrollRightImage
if theme.ScrollLeftImage ~= "" then
self.rbxGui.PrevButton.Image = theme.ScrollLeftImage
else
self.rbxGui.PrevButton.Image = theme.ScrollRightImage
self.rbxGui.PrevButton.Rotation = 180
end
end
function PlayerCarousel:FadeTowardsEdges()
if not uiPageLayout.CurrentPage then
return
end
local carouselButtons = getSortedCarouselButtons()
local currentPageIndex = 0
for index, button in ipairs(carouselButtons) do
if button == uiPageLayout.CurrentPage then
currentPageIndex = index
break
end
end
for index, button in ipairs(carouselButtons) do
local distanceToLeft = (currentPageIndex - index) % #carouselButtons
local distanceToRight = (index - currentPageIndex) % #carouselButtons
local distance = math.min(distanceToLeft, distanceToRight)
if distance >= 2 then
button.ImageTransparency = 0.7
elseif distance == 1 then
button.ImageTransparency = 0.4
else
button.ImageTransparency = 0
end
end
end
function PlayerCarousel:AddCarouselDivider()
local carouselDivider = selectedPlayer:FindFirstChild(CAROUSEL_DIVIDER_NAME)
if carouselDivider then
carouselDivider:Destroy()
end
local buttonCount = #selectedPlayer:GetChildren() - 1
if buttonCount < 5 then
return
end
carouselDivider = Instance.new("Frame")
carouselDivider.Name = CAROUSEL_DIVIDER_NAME
carouselDivider.Size = UDim2.new(1, 0, 1, 0)
carouselDivider.BackgroundTransparency = 1
carouselDivider.Parent = selectedPlayer
carouselDivider.LayoutOrder = 1000000
local line = Instance.new("Frame")
line.Name = "line"
line.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
line.BorderSizePixel = 0
line.Position = UDim2.new(0.25, 0, 0, -3)
line.Size = UDim2.new(0, 2, 1, 6)
line.Parent = carouselDivider
local line2 = line:Clone()
line2.Position = UDim2.new(0.5, 0, 0, -3)
line2.Parent = carouselDivider
local line3 = line:Clone()
line3.Position = UDim2.new(0.75, 0, 0, -3)
line3.Parent = carouselDivider
end
function PlayerCarousel:RemovePlayerEntry(player)
local button = playerToButtonMap[player]
if button then
playerToButtonMap[player] = nil
buttonToPlayerMap[button] = nil
button:Destroy()
if uiPageLayout then
uiPageLayout:ApplyLayout()
end
self:FadeTowardsEdges()
end
end
function PlayerCarousel:ClearPlayerEntries()
for button 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
buttonLoadingTween = TweenService:Create(
button,
tweenStyle,
{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
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(theme)
local obj = setmetatable({}, PlayerCarousel)
obj.rbxGui = CreateMenuCarousel(theme)
obj.PlayerChanged = playerChangedEvent.Event
playerChangedEvent.Event:Connect(function()
obj:FadeTowardsEdges()
end)
return obj
end
return PlayerCarousel
@@ -0,0 +1,118 @@
--[[
// 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 Workspace = game:GetService("Workspace")
local TweenService = game:GetService("TweenService")
local InsertService = game:GetService("InsertService")
local RENDER_ARROW_CONTEXT_ACTION = "ContextActionMenuRenderArrow"
local CurrentCamera = Workspace.CurrentCamera
-- This isn't currently necessary but done as a percaution in case
-- we move the selected character indicator elsewhere later.
local function removeScripts(object)
for _, descendant in ipairs(object:GetDescendants()) do
if descendant:IsA("LuaSourceContainer") then
descendant:Destroy()
end
end
end
local function ApplyArrow(character, theme)
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 = theme.SelectedCharacterIndicator:Clone()
removeScripts(arrowPart)
arrowPart.Anchored = true
arrowPart.Transparency = 0
arrowPart.CanCollide = false
arrowPart.Parent = baseModel
local arrowTween = 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, theme)
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, theme)
end)
if selectedPlayer.Character then
self.KillOldRenderFunction = ApplyArrow(selectedPlayer.Character, theme)
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()
@@ -0,0 +1,105 @@
local InsertService = game:GetService("InsertService")
local StarterGui = game:GetService("StarterGui")
-- Keep around any entries removed from this table to avoid breaking
-- AvatarContextMenuTheme backwards compatibility.
local DEFAULT_THEME = {
BackgroundImage = "rbxasset://textures/blackBkg_round.png",
BackgroundImageScaleType = Enum.ScaleType.Slice,
BackgroundImageSliceCenter = Rect.new(12, 12, 12, 12),
BackgroundImageTransparency = 0,
BackgroundTransparency = 1,
BackgroundColor = Color3.fromRGB(31, 31, 31),
NameTagColor = Color3.fromRGB(79, 79, 79),
NameUnderlineColor = Color3.fromRGB(255, 255, 255),
ButtonFrameColor = Color3.fromRGB(79, 79, 79),
ButtonFrameTransparency = 0,
ButtonImage = "",
ButtonImageScaleType = Enum.ScaleType.Slice,
ButtonImageSliceCenter = Rect.new(8, 6, 46, 44),
ButtonColor = Color3.fromRGB(79, 79, 79),
ButtonTransparency = 1,
ButtonHoverColor = Color3.fromRGB(163, 162, 165),
ButtonHoverTransparency = 0.5,
ButtonUnderlineColor = Color3.fromRGB(137, 137, 137),
Font = Enum.Font.SourceSansBold,
TextColor = Color3.fromRGB(255, 255, 255),
TextScale = 1,
LeaveMenuImage = "rbxasset://textures/loading/cancelButton.png",
ScrollRightImage = "rbxasset://textures/ui/AvatarContextMenu_Arrow.png",
ScrollLeftImage = "", --If no ScrollLeftImage is provided, the right image is rotated 180 degrees.
SelectedCharacterIndicator = InsertService:LoadLocalAsset(
"rbxasset://models/AvatarContextMenu/AvatarContextArrow.rbxm"
),
Size = UDim2.new(0.95, 0, 0.9, 0),
MinSize = Vector2.new(200, 200),
MaxSize = Vector2.new(300, 300),
AspectRatio = 1.15,
AnchorPoint = Vector2.new(0.5, 1),
OnScreenPosition = UDim2.new(0.5, 0, 0.98, 0),
OffScreenPosition = UDim2.new(0.5, 0, 1, 300),
}
local ThemeHandler = {}
ThemeHandler.__index = ThemeHandler
function ThemeHandler:UpdateTheme(newThemeData)
local newTheme = {}
for key, value in pairs(DEFAULT_THEME) do
if typeof(newThemeData[key]) == typeof(value) then
newTheme[key] = newThemeData[key]
elseif newThemeData[key] == nil then
newTheme[key] = value
else
error(string.format(
"AvatarContextMenuTheme wrong type for key %s: %s. Expected type %s",
key,
typeof(newThemeData[key]),
typeof(value)
), 2)
end
end
self.Theme = newTheme
end
function ThemeHandler:RegisterCoreMethods()
local function setMenuTheme(newTheme)
if type(newTheme) == "table" then
for key in pairs(newTheme) do
if DEFAULT_THEME[key] == nil then
error(string.format("AvatarContextMenuTheme got invalid key: %s", key))
end
end
self:UpdateTheme(newTheme)
else
error("AvatarContextMenuTheme argument must be a table")
end
end
StarterGui:RegisterSetCore("AvatarContextMenuTheme", setMenuTheme)
end
-- PUBLIC METHODS
function ThemeHandler:GetTheme()
return self.Theme
end
function ThemeHandler.new()
local obj = setmetatable({}, ThemeHandler)
obj.Theme = DEFAULT_THEME
obj:RegisterCoreMethods()
return obj
end
return ThemeHandler.new()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,276 @@
local HttpRbxApiService = game:GetService("HttpRbxApiService")
local HttpService = game:GetService("HttpService")
local PlayersService = game:GetService("Players")
local StarterGui = game:GetService("StarterGui")
local RobloxReplicatedStorage = game:GetService("RobloxReplicatedStorage")
local BlockingUtility = {}
BlockingUtility.__index = BlockingUtility
local LocalPlayer = PlayersService.LocalPlayer
while not LocalPlayer do
PlayersService.PlayerAdded:wait()
LocalPlayer = PlayersService.LocalPlayer
end
local GET_BLOCKED_USERIDS_TIMEOUT = 5
local RemoteEvent_UpdatePlayerBlockList = nil
spawn(function()
RemoteEvent_UpdatePlayerBlockList = RobloxReplicatedStorage:WaitForChild("UpdatePlayerBlockList", math.huge)
end)
local BlockStatusChanged = Instance.new("BindableEvent")
local MuteStatusChanged = Instance.new("BindableEvent")
local GetBlockedPlayersCompleted = false
local GetBlockedPlayersStarted = false
local GetBlockedPlayersFinished = Instance.new("BindableEvent")
local BlockedList = {}
local MutedList = {}
local function GetBlockedPlayersAsync()
local userId = LocalPlayer.UserId
local apiPath = "userblock/getblockedusers" .. "?" .. "userId=" .. tostring(userId) .. "&" .. "page=" .. "1"
if userId > 0 then
local blockList = nil
local success = pcall(function()
local request = HttpRbxApiService:GetAsync(apiPath,
Enum.ThrottlingPriority.Default, Enum.HttpRequestType.Players)
blockList = request and HttpService:JSONDecode(request)
end)
if success and blockList and blockList["success"] == true and blockList["userList"] then
local returnList = {}
for _, v in pairs(blockList["userList"]) do
returnList[v] = true
end
return returnList
end
end
return {}
end
local function getBlockedUserIdsFromBlockedList()
local userIdList = {}
for userId, _ in pairs(BlockedList) do
table.insert(userIdList, userId)
end
return userIdList
end
local function getBlockedUserIds()
if LocalPlayer.UserId > 0 then
local timeWaited = 0
while true do
if GetBlockedPlayersCompleted then
return getBlockedUserIdsFromBlockedList()
end
timeWaited = timeWaited + wait()
if timeWaited > GET_BLOCKED_USERIDS_TIMEOUT then
return {}
end
end
end
return {}
end
local function initializeBlockList()
if GetBlockedPlayersCompleted then
return
end
if GetBlockedPlayersStarted then
GetBlockedPlayersFinished.Event:Wait()
return
end
GetBlockedPlayersStarted = true
BlockedList = GetBlockedPlayersAsync()
GetBlockedPlayersCompleted = true
GetBlockedPlayersFinished:Fire()
local RemoteEvent_SetPlayerBlockList = RobloxReplicatedStorage:WaitForChild("SetPlayerBlockList", math.huge)
local blockedUserIds = getBlockedUserIds()
RemoteEvent_SetPlayerBlockList:FireServer(blockedUserIds)
end
local function isBlocked(userId)
if (BlockedList[userId]) then
return true
end
return false
end
local function isMuted(userId)
if (MutedList[userId] ~= nil and MutedList[userId] == true) then
return true
end
return false
end
local function BlockPlayerAsync(playerToBlock)
if playerToBlock and LocalPlayer ~= playerToBlock then
local blockUserId = playerToBlock.UserId
if blockUserId > 0 then
if not isBlocked(blockUserId) then
BlockedList[blockUserId] = true
BlockStatusChanged:Fire(blockUserId, true)
if RemoteEvent_UpdatePlayerBlockList then
RemoteEvent_UpdatePlayerBlockList:FireServer(blockUserId, true)
end
local success, wasBlocked = pcall(function()
local apiPath = "userblock/block"
local params = "userId=" ..tostring(playerToBlock.UserId)
local request = HttpRbxApiService:PostAsync(
apiPath,
params,
Enum.ThrottlingPriority.Default,
Enum.HttpContentType.ApplicationUrlEncoded
)
local response = request and HttpService:JSONDecode(request)
return response and response.success
end)
return success and wasBlocked
else
return true
end
end
end
return false
end
local function UnblockPlayerAsync(playerToUnblock)
if playerToUnblock then
local unblockUserId = playerToUnblock.UserId
if isBlocked(unblockUserId) then
BlockedList[unblockUserId] = nil
BlockStatusChanged:Fire(unblockUserId, false)
if RemoteEvent_UpdatePlayerBlockList then
RemoteEvent_UpdatePlayerBlockList:FireServer(unblockUserId, false)
end
local success, wasUnBlocked = pcall(function()
local apiPath = "userblock/unblock"
local params = "userId=" ..tostring(playerToUnblock.UserId)
local request = HttpRbxApiService:PostAsync(
apiPath,
params,
Enum.ThrottlingPriority.Default,
Enum.HttpContentType.ApplicationUrlEncoded
)
local response = request and HttpService:JSONDecode(request)
return response and response.success
end)
return success and wasUnBlocked
else
return true
end
end
return false
end
local function MutePlayer(playerToMute)
if playerToMute and LocalPlayer ~= playerToMute then
local muteUserId = playerToMute.UserId
if muteUserId > 0 then
if not isMuted(muteUserId) then
MutedList[muteUserId] = true
MuteStatusChanged:Fire(muteUserId, true)
end
end
end
end
local function UnmutePlayer(playerToUnmute)
if playerToUnmute then
local unmuteUserId = playerToUnmute.UserId
MutedList[unmuteUserId] = nil
MuteStatusChanged:Fire(unmuteUserId, false)
end
end
--- GetCore Blocked/Muted/Friended events.
local PlayerBlockedEvent = Instance.new("BindableEvent")
local PlayerUnblockedEvent = Instance.new("BindableEvent")
local PlayerMutedEvent = Instance.new("BindableEvent")
local PlayerUnMutedEvent = Instance.new("BindableEvent")
local function blockedStatusChanged(userId, newBlocked)
local player = PlayersService:GetPlayerByUserId(userId)
if player then
if newBlocked then
PlayerBlockedEvent:Fire(player)
else
PlayerUnblockedEvent:Fire(player)
end
end
end
BlockStatusChanged.Event:Connect(blockedStatusChanged)
local function muteStatusChanged(userId, newMuted)
local player = PlayersService:GetPlayerByUserId(userId)
if player then
if newMuted then
PlayerMutedEvent:Fire(player)
else
PlayerUnMutedEvent:Fire(player)
end
end
end
MuteStatusChanged.Event:Connect(muteStatusChanged)
StarterGui:RegisterGetCore("PlayerBlockedEvent", function() return PlayerBlockedEvent end)
StarterGui:RegisterGetCore("PlayerUnblockedEvent", function() return PlayerUnblockedEvent end)
StarterGui:RegisterGetCore("PlayerMutedEvent", function() return PlayerMutedEvent end)
StarterGui:RegisterGetCore("PlayerUnmutedEvent", function() return PlayerUnMutedEvent end)
function BlockingUtility:InitBlockListAsync()
initializeBlockList()
end
function BlockingUtility:BlockPlayerAsync(player)
return BlockPlayerAsync(player)
end
function BlockingUtility:UnblockPlayerAsync(player)
return UnblockPlayerAsync(player)
end
function BlockingUtility:MutePlayer(player)
return MutePlayer(player)
end
function BlockingUtility:UnmutePlayer(player)
return UnmutePlayer(player)
end
function BlockingUtility:IsPlayerBlockedByUserId(userId)
initializeBlockList()
return isBlocked(userId)
end
function BlockingUtility:GetBlockedStatusChangedEvent()
return BlockStatusChanged.Event
end
function BlockingUtility:GetMutedStatusChangedEvent()
return MuteStatusChanged.Event
end
function BlockingUtility:IsPlayerMutedByUserId(userId)
return isMuted(userId)
end
function BlockingUtility:GetBlockedUserIdsAsync()
return getBlockedUserIds()
end
return BlockingUtility
@@ -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
@@ -0,0 +1,180 @@
--[[
// 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 StarterGui = game:GetService("StarterGui")
local UserInputService = game:GetService("UserInputService")
local GuiService = game:GetService("GuiService")
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:EnterWhisperState(player)
if useModule then
return useModule:EnterWhisperState(player)
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
coroutine.wrap(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)()
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,9 @@
{
"lint": {
"SameLineStatement": "fatal",
"LocalShadow": "fatal",
"FunctionUnused": "fatal",
"ImportUnused": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -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_darkTheme.png",
OVERLAY_TEXTURE = "rbxasset://textures/ui/ErrorPrompt/ShimmerOverlay.png",
}
return Constants
@@ -0,0 +1,67 @@
if not settings():GetFFlag("CoreScriptFasterCreate") then
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
end
return function(className, defaultParent)
return function(propertyList)
local object = Instance.new(className)
local parent = nil
for index, value in next, propertyList do
if type(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
elseif defaultParent then
object.Parent = defaultParent
end
return object
end
end
@@ -0,0 +1,53 @@
--[[
A component that establishes a connection to a Roblox event when it is rendered.
]]
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local EventConnection = Roact.Component:extend("EventConnection")
function EventConnection:init()
self.connection = nil
end
--[[
Render the child component so that EventConnections can be nested like so:
Roact.createElement(EventConnection, {
event = UserInputService.InputBegan,
callback = inputBeganCallback,
}, {
Roact.createElement(EventConnection, {
event = UserInputService.InputChanged,
callback = inputChangedCallback,
})
})
]]
function EventConnection:render()
return Roact.oneChild(self.props[Roact.Children])
end
function EventConnection:didMount()
local event = self.props.event
local callback = self.props.callback
self.connection = event:Connect(callback)
end
function EventConnection:didUpdate(oldProps)
if self.props.event ~= oldProps.event or self.props.callback ~= oldProps.callback then
self.connection:Disconnect()
self.connection = self.props.event:Connect(self.props.callback)
end
end
function EventConnection:willUnmount()
self.connection:Disconnect()
self.connection = nil
end
return EventConnection
@@ -0,0 +1,12 @@
-- This Flag is a descendant of the Common folder instead of Modules/Flags
-- because it needs to be accessible by both Modules/InGameChat and
-- Modules/Server/ClientChat/ChatWindowInstaller.
--
-- Since the server only has access to the Common and Server folders, it's
-- placed here so both parts of the codebase can access it.
game:DefineFastFlag("RoactBubbleChat", false)
return function()
return game:GetFastFlag("RoactBubbleChat")
end
@@ -0,0 +1,12 @@
-- This Flag is a descendant of the Common folder instead of Modules/Flags
-- because it needs to be accessible by both Modules/Common/LegacyThumbnailUrls and
-- other places like Emotes, InspectAndBuy
--
-- Since the server only has access to the Common and Server folders, it's
-- placed here so both parts of the codebase can access it.
game:DefineFastFlag("UseThumbnailUrl", false)
return function()
return game:GetFastFlag("UseThumbnailUrl")
end
@@ -0,0 +1,78 @@
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
local GAME_I18N_URL = string.format("https://gameinternationalization.%s", Url.DOMAIN)
local GameRequests = {}
--[[
This endpoint (gameinternationalization/v1/name-description/games/{gameId})
returns all of the available localized names + descriptions for a game.
Docs: https://gameinternationalization.roblox.com/docs#!/NameDescription/get_v1_name_description_games_gameId
{
"data": [
{
"name": "string",
"description": "string",
"languageCode": "string"
}
]
}
]]
function GameRequests.GetNamesAndDescriptions(requestImpl, gameId)
local url = string.format("%sv1/name-description/games/%s", GAME_I18N_URL, gameId)
return requestImpl(url, "GET")
end
--[[
This endpoint (gameinternationalization/v1/source-language/games/{gameId})
returns the games default language
Docs: https://gameinternationalization.roblox.com/docs#!/SourceLanguage/get_v1_source_language_games_gameId
{
"name": "English",
"nativeName": "English",
"languageCode": "en"
}
]]
function GameRequests.GetSourceLanguage(requestImpl, gameId)
local url = string.format("%sv1/source-language/games/%s", GAME_I18N_URL, gameId)
return requestImpl(url, "GET")
end
--[[
This endpoint (locale.roblox.com/v1/locales)
returns information about the supported languages on Roblox
Docs: https://locale.roblox.com/docs#!/Locale/get_v1_locales_user_locale
{
"data": [
{
"locale": {
"id": 0,
"locale": "string",
"name": "string",
"nativeName": "string",
"language": {
"id": 0,
"name": "string",
"nativeName": "string",
"languageCode": "string"
}
},
"isEnabledForFullExperience": true,
"isEnabledForSignupAndLogin": true,
"isEnabledForInGameUgc": true
}
]
}
]]
function GameRequests.GetSupportedLanguages(requestImpl)
local url = string.format("%sv1/locales", Url.LOCALE)
return requestImpl(url, "GET")
end
return GameRequests
@@ -0,0 +1,7 @@
--!nocheck
local UserInputService = game:GetService("UserInputService")
return function()
return Enum.Platform.XBoxOne == UserInputService:GetPlatform() and Enum.QualityLevel.Level21 or Enum.QualityLevel.Automatic
end
@@ -0,0 +1,168 @@
local CorePackages = game:GetService("CorePackages")
local Promise = require(CorePackages.Promise)
local GameRequests = require(script.Parent.GameRequests)
local SupportedLanguagesFetched = false
local StartedFetchingSupportedLanaguages = false
local FetchedSupportedLanguagesEvent = Instance.new("BindableEvent")
local CachedSupportedLanguages = nil
local FALLBACK_LANGUAGE_CONSTANT = "FALLBACK"
local FALLBACK_SOURCE_LOCALE = "en-us"
local function GetSupportedLanguagesPromise(networkImpl)
if SupportedLanguagesFetched then
return Promise.resolve(CachedSupportedLanguages)
elseif StartedFetchingSupportedLanaguages then
return Promise.new(function(resolve, reject)
local success = FetchedSupportedLanguagesEvent.Event:Wait()
if success then
resolve(CachedSupportedLanguages)
else
reject()
end
end)
end
StartedFetchingSupportedLanaguages = true
return GameRequests.GetSupportedLanguages(networkImpl):andThen(function(result)
CachedSupportedLanguages = result
FetchedSupportedLanguagesEvent:Fire(true)
return Promise.resolve(result)
end,
function()
StartedFetchingSupportedLanaguages = false
FetchedSupportedLanguagesEvent:Fire(false)
return Promise.reject()
end)
end
local function PerGameCachedRequestFactoryFunction(Request)
local cache = {}
return function(networkImpl, gameId)
if cache[gameId] then
if cache[gameId].Fetched then
return Promise.resolve(cache[gameId].Result)
else
local success = cache[gameId].FinishedEvent.Event:Wait()
if success then
return Promise.resolve(cache[gameId].Result)
else
return Promise.reject()
end
end
else
cache[gameId] = {
Fetched = false,
FinishedEvent = Instance.new("BindableEvent"),
Result = nil,
}
return Request(networkImpl, gameId):andThen(function(result)
cache[gameId].Fetched = true
cache[gameId].Result = result
cache[gameId].FinishedEvent:Fire(true)
return Promise.resolve(result)
end,
function()
cache[gameId].FinishedEvent:Fire(false)
return Promise.reject()
end)
end
end
end
local function ProcessNamesAndDescriptionsResult(result)
local data = result.responseBody.data
local gameNames = {}
local gameDescriptions = {}
for _, nameDescription in ipairs(data) do
local languageCode = nameDescription.languageCode or FALLBACK_LANGUAGE_CONSTANT
if gameNames[languageCode] == nil then
gameNames[languageCode] = nameDescription.name
end
if gameDescriptions[languageCode] == nil then
gameDescriptions[languageCode] = nameDescription.description
end
end
return gameNames, gameDescriptions
end
local function ProcessSupportedLanaguagesResult(result)
local data = result.responseBody.data
local languageCodeMap = {}
for _, localeInfo in ipairs(data) do
-- Locale being repeated here twice looks wrong but it is correct.
-- Web passes in the locales with _ as a seperator but we use - on the client
languageCodeMap[localeInfo.locale.language.languageCode] = localeInfo.locale.locale:gsub("_", "-")
end
return languageCodeMap
end
local function ProcessSourceLanguageResult(result)
local data = result.responseBody
return data.languageCode
end
local GetNamesAndDescriptionsPromise = PerGameCachedRequestFactoryFunction(GameRequests.GetNamesAndDescriptions)
local GetSourceLanguagePromise = PerGameCachedRequestFactoryFunction(GameRequests.GetSourceLanguage)
return function(networkImpl, gameId)
local namesAndDescriptionsPromise = GetNamesAndDescriptionsPromise(networkImpl, gameId)
local supportedLanguagesPromise = GetSupportedLanguagesPromise(networkImpl)
local sourceLanguagesPromise = GetSourceLanguagePromise(networkImpl, gameId)
return Promise.all(
namesAndDescriptionsPromise,
supportedLanguagesPromise,
sourceLanguagesPromise):andThen(function(results)
local namesAndDescriptionsResult = results[1]
local supportedLanaguesResult = results[2]
local sourceLanagueResult = results[3]
local gameNamesLanguageCodeMap, gameDescriptionsLanguageCodeMap = ProcessNamesAndDescriptionsResult(
namesAndDescriptionsResult)
local languageCodeMap = ProcessSupportedLanaguagesResult(supportedLanaguesResult)
local sourceLangaugeCode = ProcessSourceLanguageResult(sourceLanagueResult)
local sourceLocale = languageCodeMap[sourceLangaugeCode] or FALLBACK_SOURCE_LOCALE
local gameNameLocaleMap = {}
for languageCode, name in pairs(gameNamesLanguageCodeMap) do
local locale = languageCodeMap[languageCode]
if locale then
gameNameLocaleMap[locale] = name
end
end
if gameNameLocaleMap[sourceLocale] == nil then
gameNameLocaleMap[sourceLocale] = gameNamesLanguageCodeMap[FALLBACK_LANGUAGE_CONSTANT]
end
local gameDescriptionsLocaleMap = {}
for languageCode, description in pairs(gameDescriptionsLanguageCodeMap) do
local locale = languageCodeMap[languageCode]
if locale then
gameDescriptionsLocaleMap[locale] = description
end
end
if gameDescriptionsLocaleMap[sourceLocale] == nil then
gameDescriptionsLocaleMap[sourceLocale] = gameDescriptionsLanguageCodeMap[FALLBACK_LANGUAGE_CONSTANT]
end
return gameNameLocaleMap, gameDescriptionsLocaleMap, sourceLocale
end,
function()
return Promise.reject()
end)
end
@@ -0,0 +1,105 @@
local Players = game:GetService("Players")
-- wait for the first of the passed signals to fire
local function waitForFirst(...)
local shunt = Instance.new("BindableEvent")
local slots = {...}
local function fire(...)
for i = 1, #slots do
slots[i]:Disconnect()
end
return shunt:Fire(...)
end
for i = 1, #slots do
slots[i] = slots[i]:Connect(fire)
end
return shunt.Event:Wait()
end
local HumanoidReadyUtil = {}
-- registers a humanoidReady(player: Player, character: Model, humanoid: Humanoid) callback and
-- invokes it immediately for existing player character humanoids.
--
-- Unregistering is not currently supported.
--
-- This can't just be an event because we need to invoke it immediately for existing eligable player
-- character humanoids, but would not for any existing callbacks.
--
-- For now this is about sharing code, not sharing event connections. Supporting this would really
-- complicate the code and we don't currently have multiple systems that could benefit from this
-- sharing that would all be active on a single client.
function HumanoidReadyUtil.registerHumanoidReady(humanoidReady)
local function characterAdded(player, character)
-- Avoiding memory leaks in the face of Character/Humanoid/RootPart lifetime has a few complications:
-- * character deparenting is a Remove instead of a Destroy, so signals are not cleaned up automatically.
-- ** must use a waitForFirst on everything and listen for hierarchy changes.
-- * the character might not be in the dm by the time CharacterAdded fires
-- ** constantly check consistency with player.Character and abort if CharacterAdded is fired again
-- * Humanoid may not exist immediately, and by the time it's inserted the character might be deparented.
-- * RootPart probably won't exist immediately.
-- ** by the time RootPart is inserted and Humanoid.RootPart is set, the character or the humanoid might be deparented.
if not character.Parent then
waitForFirst(character.AncestryChanged, player.CharacterAdded)
end
if player.Character ~= character or not character.Parent then
return
end
local humanoid = character:FindFirstChildOfClass("Humanoid")
while character:IsDescendantOf(game) and not humanoid do
waitForFirst(character.ChildAdded, character.AncestryChanged, player.CharacterAdded)
humanoid = character:FindFirstChildOfClass("Humanoid")
end
if player.Character ~= character or not character:IsDescendantOf(game) then
return
end
-- must rely on HumanoidRootPart naming because Humanoid.RootPart does not fire changed signals
local rootPart = character:FindFirstChild("HumanoidRootPart")
while character:IsDescendantOf(game) and not rootPart do
waitForFirst(character.ChildAdded, character.AncestryChanged, humanoid.AncestryChanged, player.CharacterAdded)
rootPart = character:FindFirstChild("HumanoidRootPart")
end
if rootPart and humanoid:IsDescendantOf(game) and character:IsDescendantOf(game) and player.Character == character then
humanoidReady(player, character, humanoid)
end
end
local function playerAdded(player)
local characterAddedConn = player.CharacterAdded:Connect(function(character)
characterAdded(player, character)
end)
-- Players are Removed, not Destroyed, by replication so we must clean up
local ancestryChangedConn
ancestryChangedConn = player.AncestryChanged:Connect(function(_child, parent)
if not game:IsAncestorOf(parent) then
ancestryChangedConn:Disconnect()
characterAddedConn:Disconnect()
end
end)
local character = player.Character
if character then
characterAdded(player, character)
end
end
-- Track all players (including local player on the client)
Players.PlayerAdded:Connect(playerAdded)
for _, player in pairs(Players:GetPlayers()) do
playerAdded(player)
end
end
return HumanoidReadyUtil
@@ -0,0 +1,28 @@
local ContentProvider = game:GetService("ContentProvider")
local RobloxGui = game:GetService("CoreGui"):WaitForChild("RobloxGui")
local GetFFlagUseThumbnailUrl = require(RobloxGui.Modules.Common.Flags.GetFFlagUseThumbnailUrl)
if GetFFlagUseThumbnailUrl() then
local BaseUrl = ContentProvider.BaseUrl:lower()
BaseUrl = string.gsub(BaseUrl, "/m.", "/www.")
BaseUrl = string.gsub(BaseUrl, "/www.", "/thumbnails.")
BaseUrl = string.gsub(BaseUrl, "http:", "https:")
return {
Headshot = "rbxthumb://type=AvatarHeadShot&id=%d&w=%d&h=%d",
Bust = BaseUrl .. "v1/users/avatar-bust?userIds=%d&size=%dx%d&format=Png&isCircular=false",
Thumbnail = "rbxthumb://type=Avatar&id=%d&w=%d&h=%d",
}
else
--Prefer Rbxthumb urls for new work!
local BaseUrl = ContentProvider.BaseUrl:lower()
BaseUrl = string.gsub(BaseUrl, "/m.", "/www.")
BaseUrl = string.gsub(BaseUrl, "http:", "https:")
return {
Headshot = BaseUrl .. "headshot-thumbnail/image?width=%d&height=%d&format=png&userId=%d",
Bust = BaseUrl .. "bust-thumbnail/image?width=%d&height=%d&format=png&userId=%d",
Thumbnail = BaseUrl .. "avatar-thumbnail/image?width=%d&height=%d&format=png&userId=%d",
}
end
@@ -0,0 +1,50 @@
-- // FileName: ObjectPool.lua
-- // Written by: TheGamer101
-- // Description: An object pool class used to avoid unnecessarily instantiating Instances.
local module = {}
--////////////////////////////// Include
--//////////////////////////////////////
--////////////////////////////// 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,54 @@
--[[
Filename: PolicyService.lua
Written by: ben
Description: Handles all policy service calls in lua for core scripts
--]]
local PlayersService = game:GetService('Players')
local isSubjectToChinaPolicies = true
local policyTable
local initialized = false
local initAsyncCalledOnce = false
local initializedEvent = Instance.new("BindableEvent")
--[[ Classes ]]--
local PolicyService = {}
function PolicyService:InitAsync()
if _G.__TESTEZ_RUNNING_TEST__ then
-- Return here in the case of unit tests
return
end
if initialized then return end
if initAsyncCalledOnce then
initializedEvent.Event:Wait()
return
end
initAsyncCalledOnce = true
local localPlayer = PlayersService.LocalPlayer
while not localPlayer do
PlayersService.PlayerAdded:wait()
localPlayer = PlayersService.LocalPlayer
end
pcall(function() policyTable = game:GetService("PolicyService"):GetPolicyInfoForPlayerAsync(localPlayer) end)
if policyTable then
isSubjectToChinaPolicies = policyTable["IsSubjectToChinaPolicies"]
end
initialized = true
initializedEvent:Fire()
end
function PolicyService:IsSubjectToChinaPolicies()
self:InitAsync()
return isSubjectToChinaPolicies
end
return PolicyService
@@ -0,0 +1,480 @@
local RunService = game:GetService("RunService")
local Rigging = {}
-- Gravity that joint friction values were tuned under.
local REFERENCE_GRAVITY = 196.2
-- ReferenceMass values from mass of child part. Used to normalized "stiffness" for differently
-- sized avatars (with different mass).
local DEFAULT_MAX_FRICTION_TORQUE = 500
local HEAD_LIMITS = {
UpperAngle = 45,
TwistLowerAngle = -40,
TwistUpperAngle = 40,
FrictionTorque = 400,
ReferenceMass = 1.0249234437943,
}
local WAIST_LIMITS = {
UpperAngle = 20,
TwistLowerAngle = -40,
TwistUpperAngle = 20,
FrictionTorque = 750,
ReferenceMass = 2.861558675766,
}
local ANKLE_LIMITS = {
UpperAngle = 10,
TwistLowerAngle = -10,
TwistUpperAngle = 10,
ReferenceMass = 0.43671694397926,
}
local ELBOW_LIMITS = {
-- Elbow is basically a hinge, but allow some twist for Supination and Pronation
UpperAngle = 20,
TwistLowerAngle = 5,
TwistUpperAngle = 120,
ReferenceMass = 0.70196455717087,
}
local WRIST_LIMITS = {
UpperAngle = 30,
TwistLowerAngle = -10,
TwistUpperAngle = 10,
ReferenceMass = 0.69132566452026,
}
local KNEE_LIMITS = {
UpperAngle = 5,
TwistLowerAngle = -120,
TwistUpperAngle = -5,
ReferenceMass = 0.65389388799667,
}
local SHOULDER_LIMITS = {
UpperAngle = 110,
TwistLowerAngle = -85,
TwistUpperAngle = 85,
FrictionTorque = 600,
ReferenceMass = 0.90918225049973,
}
local HIP_LIMITS = {
UpperAngle = 40,
TwistLowerAngle = -5,
TwistUpperAngle = 80,
FrictionTorque = 600,
ReferenceMass = 1.9175016880035,
}
local R6_HEAD_LIMITS = {
UpperAngle = 30,
TwistLowerAngle = -40,
TwistUpperAngle = 40,
}
local R6_SHOULDER_LIMITS = {
UpperAngle = 110,
TwistLowerAngle = -85,
TwistUpperAngle = 85,
}
local R6_HIP_LIMITS = {
UpperAngle = 40,
TwistLowerAngle = -5,
TwistUpperAngle = 80,
}
local V3_ZERO = Vector3.new()
local V3_UP = Vector3.new(0, 1, 0)
local V3_DOWN = Vector3.new(0, -1, 0)
local V3_RIGHT = Vector3.new(1, 0, 0)
local V3_LEFT = Vector3.new(-1, 0, 0)
-- To model shoulder cone and twist limits correctly we really need the primary axis of the UpperArm
-- to be going down the limb. the waist and neck joints attachments actually have the same problem
-- of non-ideal axis orientation, but it's not as noticable there since the limits for natural
-- motion are tighter for those joints anyway.
local R15_ADDITIONAL_ATTACHMENTS = {
{"UpperTorso", "RightShoulderRagdollAttachment", CFrame.fromMatrix(V3_ZERO, V3_RIGHT, V3_UP), "RightShoulderRigAttachment"},
{"RightUpperArm", "RightShoulderRagdollAttachment", CFrame.fromMatrix(V3_ZERO, V3_DOWN, V3_RIGHT), "RightShoulderRigAttachment"},
{"UpperTorso", "LeftShoulderRagdollAttachment", CFrame.fromMatrix(V3_ZERO, V3_LEFT, V3_UP), "LeftShoulderRigAttachment"},
{"LeftUpperArm", "LeftShoulderRagdollAttachment", CFrame.fromMatrix(V3_ZERO, V3_DOWN, V3_LEFT), "LeftShoulderRigAttachment"},
}
-- { { Part0 name (parent), Part1 name (child, parent of joint), attachmentName, limits }, ... }
local R15_RAGDOLL_RIG = {
{"UpperTorso", "Head", "NeckRigAttachment", HEAD_LIMITS},
{"LowerTorso", "UpperTorso", "WaistRigAttachment", WAIST_LIMITS},
{"UpperTorso", "LeftUpperArm", "LeftShoulderRagdollAttachment", SHOULDER_LIMITS},
{"LeftUpperArm", "LeftLowerArm", "LeftElbowRigAttachment", ELBOW_LIMITS},
{"LeftLowerArm", "LeftHand", "LeftWristRigAttachment", WRIST_LIMITS},
{"UpperTorso", "RightUpperArm", "RightShoulderRagdollAttachment", SHOULDER_LIMITS},
{"RightUpperArm", "RightLowerArm", "RightElbowRigAttachment", ELBOW_LIMITS},
{"RightLowerArm", "RightHand", "RightWristRigAttachment", WRIST_LIMITS},
{"LowerTorso", "LeftUpperLeg", "LeftHipRigAttachment", HIP_LIMITS},
{"LeftUpperLeg", "LeftLowerLeg", "LeftKneeRigAttachment", KNEE_LIMITS},
{"LeftLowerLeg", "LeftFoot", "LeftAnkleRigAttachment", ANKLE_LIMITS},
{"LowerTorso", "RightUpperLeg", "RightHipRigAttachment", HIP_LIMITS},
{"RightUpperLeg", "RightLowerLeg", "RightKneeRigAttachment", KNEE_LIMITS},
{"RightLowerLeg", "RightFoot", "RightAnkleRigAttachment", ANKLE_LIMITS},
}
-- { { Part0 name, Part1 name }, ... }
local R15_NO_COLLIDES = {
{"LowerTorso", "LeftUpperArm"},
{"LeftUpperArm", "LeftHand"},
{"LowerTorso", "RightUpperArm"},
{"RightUpperArm", "RightHand"},
{"LeftUpperLeg", "RightUpperLeg"},
{"UpperTorso", "RightUpperLeg"},
{"RightUpperLeg", "RightFoot"},
{"UpperTorso", "LeftUpperLeg"},
{"LeftUpperLeg", "LeftFoot"},
-- Support weird R15 rigs
{"UpperTorso", "LeftLowerLeg"},
{"UpperTorso", "RightLowerLeg"},
{"LowerTorso", "LeftLowerLeg"},
{"LowerTorso", "RightLowerLeg"},
{"UpperTorso", "LeftLowerArm"},
{"UpperTorso", "RightLowerArm"},
{"Head", "LeftUpperArm"},
{"Head", "RightUpperArm"},
}
-- { { Motor6D name, Part name }, ...}, must be in tree order, important for ApplyJointVelocities
local R15_MOTOR6DS = {
{"Waist", "UpperTorso"},
{"Neck", "Head"},
{"LeftShoulder", "LeftUpperArm"},
{"LeftElbow", "LeftLowerArm"},
{"LeftWrist", "LeftHand"},
{"RightShoulder", "RightUpperArm"},
{"RightElbow", "RightLowerArm"},
{"RightWrist", "RightHand"},
{"LeftHip", "LeftUpperLeg"},
{"LeftKnee", "LeftLowerLeg"},
{"LeftAnkle", "LeftFoot"},
{"RightHip", "RightUpperLeg"},
{"RightKnee", "RightLowerLeg"},
{"RightAnkle", "RightFoot"},
}
-- R6 has hard coded part sizes and does not have a full set of rig Attachments.
local R6_ADDITIONAL_ATTACHMENTS = {
{"Head", "NeckAttachment", CFrame.new(0, -0.5, 0)},
{"Torso", "RightShoulderRagdollAttachment", CFrame.fromMatrix(Vector3.new(1, 0.5, 0), V3_RIGHT, V3_UP)},
{"Right Arm", "RightShoulderRagdollAttachment", CFrame.fromMatrix(Vector3.new(-0.5, 0.5, 0), V3_DOWN, V3_RIGHT)},
{"Torso", "LeftShoulderRagdollAttachment", CFrame.fromMatrix(Vector3.new(-1, 0.5, 0), V3_LEFT, V3_UP)},
{"Left Arm", "LeftShoulderRagdollAttachment", CFrame.fromMatrix(Vector3.new(0.5, 0.5, 0), V3_DOWN, V3_LEFT)},
{"Torso", "RightHipAttachment", CFrame.new(0.5, -1, 0)},
{"Right Leg", "RightHipAttachment", CFrame.new(0, 1, 0)},
{"Torso", "LeftHipAttachment", CFrame.new(-0.5, -1, 0)},
{"Left Leg", "LeftHipAttachment", CFrame.new(0, 1, 0)},
}
-- R6 rig tables use the same table structures as R15.
local R6_RAGDOLL_RIG = {
{"Torso", "Head", "NeckAttachment", R6_HEAD_LIMITS},
{"Torso", "Left Leg", "LeftHipAttachment", R6_HIP_LIMITS},
{"Torso", "Right Leg", "RightHipAttachment", R6_HIP_LIMITS},
{"Torso", "Left Arm", "LeftShoulderRagdollAttachment", R6_SHOULDER_LIMITS},
{"Torso", "Right Arm", "RightShoulderRagdollAttachment", R6_SHOULDER_LIMITS},
}
local R6_NO_COLLIDES = {
{"Left Leg", "Right Leg"},
{"Head", "Right Arm"},
{"Head", "Left Arm"},
}
local R6_MOTOR6DS = {
{"Neck", "Torso"},
{"Left Shoulder", "Torso"},
{"Right Shoulder", "Torso"},
{"Left Hip", "Torso"},
{"Right Hip", "Torso"},
}
local BALL_SOCKET_NAME = "RagdollBallSocket"
local NO_COLLIDE_NAME = "RagdollNoCollision"
-- Index parts by name to save us from many O(n) FindFirstChild searches
local function indexParts(model)
local parts = {}
for _, child in ipairs(model:GetChildren()) do
if child:IsA("BasePart") then
local name = child.name
-- Index first, mimicing FindFirstChild
if not parts[name] then
parts[name] = child
end
end
end
return parts
end
local function createRigJoints(parts, rig)
for _, params in ipairs(rig) do
local part0Name, part1Name, attachmentName, limits = unpack(params)
local part0 = parts[part0Name]
local part1 = parts[part1Name]
if part0 and part1 then
local a0 = part0:FindFirstChild(attachmentName)
local a1 = part1:FindFirstChild(attachmentName)
if a0 and a1 and a0:IsA("Attachment") and a1:IsA("Attachment") then
-- Our rigs only have one joint per part (connecting each part to it's parent part), so
-- we can re-use it if we have to re-rig that part again.
local constraint = part1:FindFirstChild(BALL_SOCKET_NAME)
if not constraint then
constraint = Instance.new("BallSocketConstraint")
constraint.Name = BALL_SOCKET_NAME
end
constraint.Attachment0 = a0
constraint.Attachment1 = a1
constraint.LimitsEnabled = true
constraint.UpperAngle = limits.UpperAngle
constraint.TwistLimitsEnabled = true
constraint.TwistLowerAngle = limits.TwistLowerAngle
constraint.TwistUpperAngle = limits.TwistUpperAngle
-- Scale constant torque limit for joint friction relative to gravity and the mass of
-- the body part.
local gravityScale = workspace.Gravity / REFERENCE_GRAVITY
local referenceMass = limits.ReferenceMass
local massScale = referenceMass and (part1:GetMass() / referenceMass) or 1
local maxTorque = limits.FrictionTorque or DEFAULT_MAX_FRICTION_TORQUE
constraint.MaxFrictionTorque = maxTorque * massScale * gravityScale
constraint.Parent = part1
end
end
end
end
local function createAdditionalAttachments(parts, attachments)
for _, attachmentParams in ipairs(attachments) do
local partName, attachmentName, cframe, baseAttachmentName = unpack(attachmentParams)
local part = parts[partName]
if part then
local attachment = part:FindFirstChild(attachmentName)
-- Create or update existing attachment
if not attachment or attachment:IsA("Attachment") then
if baseAttachmentName then
local base = part:FindFirstChild(baseAttachmentName)
if base and base:IsA("Attachment") then
cframe = base.CFrame * cframe
end
end
-- The attachment names are unique within a part, so we can re-use
if not attachment then
attachment = Instance.new("Attachment")
attachment.Name = attachmentName
attachment.CFrame = cframe
attachment.Parent = part
else
attachment.CFrame = cframe
end
end
end
end
end
local function createNoCollides(parts, noCollides)
-- This one's trickier to handle for an already rigged character since a part will have multiple
-- NoCollide children with the same name. Having fewer unique names is better for
-- replication so we suck it up and deal with the complexity here.
-- { [Part1] = { [Part0] = true, ... }, ...}
local needed = {}
-- Following the convention of the Motor6Ds and everything else here we parent the NoCollide to
-- Part1, so we start by building the set of Part0s we need a NoCollide with for each Part1
for _, namePair in ipairs(noCollides) do
local part0Name, part1Name = unpack(namePair)
local p0, p1 = parts[part0Name], parts[part1Name]
if p0 and p1 then
local p0Set = needed[p1]
if not p0Set then
p0Set = {}
needed[p1] = p0Set
end
p0Set[p0] = true
end
end
-- Go through NoCollides that exist and remove Part0s from the needed set if we already have
-- them covered. Gather NoCollides that aren't between parts in the set for resue
local reusableNoCollides = {}
for part1, neededPart0s in pairs(needed) do
local reusables = {}
for _, child in ipairs(part1:GetChildren()) do
if child:IsA("NoCollisionConstraint") and child.Name == NO_COLLIDE_NAME then
local p0 = child.Part0
local p1 = child.Part1
if p1 == part1 and neededPart0s[p0] then
-- If this matches one that we needed, we don't need to create it anymore.
neededPart0s[p0] = nil
else
-- Otherwise we're free to reuse this NoCollide
table.insert(reusables, child)
end
end
end
reusableNoCollides[part1] = reusables
end
-- Create the remaining NoCollides needed, re-using old ones if possible
for part1, neededPart0s in pairs(needed) do
local reusables = reusableNoCollides[part1]
for part0, _ in pairs(neededPart0s) do
local constraint = table.remove(reusables)
if not constraint then
constraint = Instance.new("NoCollisionConstraint")
end
constraint.Name = NO_COLLIDE_NAME
constraint.Part0 = part0
constraint.Part1 = part1
constraint.Parent = part1
end
end
end
local function disableMotorSet(model, motorSet)
local motors = {}
-- Destroy all regular joints:
for _, params in ipairs(motorSet) do
local part = model:FindFirstChild(params[2])
if part then
local motor = part:FindFirstChild(params[1])
if motor and motor:IsA("Motor6D") then
table.insert(motors, motor)
motor.Enabled = false
end
end
end
return motors
end
function Rigging.createRagdollJoints(model, rigType)
local parts = indexParts(model)
if rigType == Enum.HumanoidRigType.R6 then
createAdditionalAttachments(parts, R6_ADDITIONAL_ATTACHMENTS)
createRigJoints(parts, R6_RAGDOLL_RIG)
createNoCollides(parts, R6_NO_COLLIDES)
elseif rigType == Enum.HumanoidRigType.R15 then
createAdditionalAttachments(parts, R15_ADDITIONAL_ATTACHMENTS)
createRigJoints(parts, R15_RAGDOLL_RIG)
createNoCollides(parts, R15_NO_COLLIDES)
else
error("unknown rig type", 2)
end
end
function Rigging.removeRagdollJoints(model)
for _, descendant in pairs(model:GetDescendants()) do
-- Remove BallSockets and NoCollides, leave the additional Attachments
if (descendant:IsA("BallSocketConstraint") and descendant.Name == BALL_SOCKET_NAME)
or (descendant:IsA("NoCollisionConstraint") and descendant.Name == NO_COLLIDE_NAME)
then
descendant:Destroy()
end
end
end
function Rigging.disableMotors(model, rigType)
-- Note: We intentionally do not disable the root joint so that the mechanism root of the
-- character stays consistent when we break joints on the client. This avoid the need for the client to wait
-- for re-assignment of network ownership of a new mechanism, which creates a visible hitch.
local motors
if rigType == Enum.HumanoidRigType.R6 then
motors = disableMotorSet(model, R6_MOTOR6DS)
elseif rigType == Enum.HumanoidRigType.R15 then
motors = disableMotorSet(model, R15_MOTOR6DS)
else
error("unknown rig type", 2)
end
-- Set the root part to non-collide
local rootPart = model.PrimaryPart or model:FindFirstChild("HumanoidRootPart")
if rootPart and rootPart:IsA("BasePart") then
rootPart.CanCollide = false
end
return motors
end
function Rigging.disableParticleEmittersAndFadeOut(character, duration)
if RunService:IsServer() then
-- This causes a lot of unnecesarry replicated property changes
error("disableParticleEmittersAndFadeOut should not be called on the server.", 2)
end
local descendants = character:GetDescendants()
local transparencies = {}
for _, instance in pairs(descendants) do
if instance:IsA("BasePart") or instance:IsA("Decal") then
transparencies[instance] = instance.Transparency
elseif instance:IsA("ParticleEmitter") then
instance.Enabled = false
end
end
local t = 0
while t < duration do
-- Using heartbeat because we want to update just before rendering next frame, and not
-- block the render thread kicking off (as RenderStepped does)
local dt = RunService.Heartbeat:Wait()
t = t + dt
local alpha = math.min(t / duration, 1)
for part, initialTransparency in pairs(transparencies) do
part.Transparency = (1 - alpha) * initialTransparency + alpha
end
end
end
function Rigging.easeJointFriction(character, duration)
local descendants = character:GetDescendants()
-- { { joint, initial friction, end friction }, ... }
local frictionJoints = {}
for _, v in pairs(descendants) do
if v:IsA("BallSocketConstraint") and v.Name == BALL_SOCKET_NAME then
local current = v.MaxFrictionTorque
-- Keep the torso and neck a little stiffer...
local parentName = v.Parent.Name
local scale = (parentName == "UpperTorso" or parentName == "Head") and 0.5 or 0.05
local nextTorque = current * scale
frictionJoints[v] = { v, current, nextTorque }
end
end
local t = 0
while t < duration do
-- Using stepped because we want to update just before physics sim
local _, dt = RunService.Stepped:Wait()
t = t + dt
local alpha = math.min(t / duration, 1)
for _, tuple in pairs(frictionJoints) do
local ballSocket, a, b = unpack(tuple)
ballSocket.MaxFrictionTorque = (1 - alpha) * a + alpha * b
end
end
end
return Rigging
@@ -0,0 +1,37 @@
-- Domain list
local Urls = {}
local ContentProvider = game:GetService("ContentProvider")
local function getBaseDomain(baseUrl)
local _, prefixEnd = baseUrl:find("%.")
local baseDomain = baseUrl:sub(prefixEnd + 1)
if baseDomain:sub(-1) ~= "/" then
baseDomain = baseDomain .. "/"
end
return baseDomain
end
local baseUrl = ContentProvider.BaseUrl
local baseDomain = getBaseDomain(baseUrl)
local baseGameUrl = string.format("https://games.%s", baseDomain)
local baseRcsUrl = string.format("https://apis.rcs.%s", baseDomain)
local baseApisUrl = string.format("https://apis.%s", baseDomain)
local urlValues = {
GAME_URL = baseGameUrl,
RCS_URL = baseRcsUrl,
APIS_URL = baseApisUrl,
}
setmetatable(Urls, {
__newindex = function(t, key, index)
end,
__index = function(t, index)
return urlValues[index]
end
})
return Urls
@@ -0,0 +1,10 @@
{
"lint": {
"SameLineStatement": "fatal",
"FunctionUnused": "fatal",
//"LocalShadow": "fatal",
//"LocalUnused": "fatal",
"ImportUnused": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -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,9 @@
local Action = require(script.Parent.Parent.Action)
return Action(script.Name, function(waitingForRecording, lastFileOutputLocation)
return {
waitingForRecording = waitingForRecording,
lastFileOutputLocation = lastFileOutputLocation or "",
}
end)
@@ -0,0 +1,9 @@
local Action = require(script.Parent.Parent.Action)
return Action("SetTabList", function(tabList, initIndex, isDeveloperView)
return {
tabList = tabList,
initIndex = initIndex,
isDeveloperView = isDeveloperView,
}
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(0, width, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
}, 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,
ClipsDescendants = true,
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,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.Parent.DataProvider)
local ActionBindingsChart = require(script.Parent.ActionBindingsChart)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0,
isDeveloperView = true,
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
ActionBindingsChart = Roact.createElement(ActionBindingsChart)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,158 @@
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
self._isRunning = false
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:isRunning()
return self._isRunning
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
self._isRunning = true
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
self._isRunning = false
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,37 @@
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,
isDeveloperView = true,
},
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 = onButtonPress and 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,51 @@
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 dropdownTargetGui = self.props.dropdownTargetGui
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, {
-- render the portal into the same ScreenGui as the DevConsole
target = dropdownTargetGui ~= nil and dropdownTargetGui or game:GetService("CoreGui").DevConsoleMaster,
}, {
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,16 @@
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,
dropdownTargetGui = Instance.new("ScreenGui"),
})
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,71 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
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 FFlagAdminServerLogs = settings():GetFFlag("AdminServerLogs")
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 and not FFlagAdminServerLogs 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:willUpdate(nextProps, nextState)
if FFlagAdminServerLogs then
if nextProps.isDeveloperView and
not self.props.isDeveloperView then
for _, dataProvider in pairs(self._context.DevConsoleData) do
if not dataProvider:isRunning() then
dataProvider:start()
end
end
end
end
end
function DataProvider:render()
return Roact.oneChild(self.props[Roact.Children])
end
local function mapStateToProps(state, props)
return {
isDeveloperView = state.MainView.isDeveloperView,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, nil)(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,35 @@
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 DataStoresChart = require(script.Parent.DataStoresChart)
it("should create and destroy without errors", function()
local element = Roact.createElement(DataProvider, {}, {
DataStoresChart = Roact.createElement(DataStoresChart)
})
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0,
isDeveloperView = true,
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
DataStoresChart = Roact.createElement(DataStoresChart)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,115 @@
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
self._isRunning = false
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:isRunning()
return self._isRunning
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)
self._isRunning = true
end
end
function DataStoresData:stop()
-- listeners are responsible for disconnecting themselves
if self._statsListenerConnection then
self._statsListenerConnection:Disconnect()
self._statsListenerConnection = nil
end
self._isRunning = false
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,37 @@
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,
isDeveloperView = true,
},
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,203 @@
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 UserInputService = game:GetService("UserInputService")
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")
local FFlagDevConsoleIsVeryStickyWhyWillItNotLetGo = settings():GetFFlag("DevConsoleIsVeryStickyWhyWillItNotLetGo")
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,
})
if FFlagDevConsoleIsVeryStickyWhyWillItNotLetGo then
local inputEndedConn
local function onInputEnded(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
self.inputEnded(nil, input)
inputEndedConn:Disconnect()
end
end
inputEndedConn = UserInputService.InputEnded:Connect(onInputEnded)
end
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,
})
-- padding multiplied by two for the padding on the left and the right
local devConsoleTextSizeAndPadding = DEVCONSOLE_TEXT_FRAMESIZE.X + (DEVCONSOLE_TEXT_X_PADDING * 2)
local liveStatsModulePos = UDim2.new(0, devConsoleTextSizeAndPadding, 0, 0)
local liveStatsModuleSize = UDim2.new(1, -2 * devConsoleTextSizeAndPadding, 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", {
OnTopOfCoreBlur = true,
}, {
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,37 @@
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 {
MainView = {
currTabIndex = 0,
isDeveloperView = true,
},
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,295 @@
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
local inputChangedConn, inputEndedConn
local function onInputChanged(input)
local currPosition = self.ref.current.AbsolutePosition
local cornerPos = input.Position
self:setDevConsoleSize(currPosition, cornerPos)
end
local function onInputEnded(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
inputChangedConn:Disconnect()
inputEndedConn:Disconnect()
end
end
inputChangedConn = UserInputService.InputChanged:Connect(onInputChanged)
inputEndedConn = UserInputService.InputEnded:Connect(onInputEnded)
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,
}),
})
end
end
local function mapStateToProps(state, props)
return {
isVisible = state.DisplayOptions.isVisible,
isMinimized = state.DisplayOptions.isMinimized,
position = state.DisplayOptions.position,
size = state.DisplayOptions.size,
isDeveloperView = state.MainView.isDeveloperView,
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,52 @@
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,
isDeveloperView = true,
},
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,153 @@
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 dropDownTargetParent = self.props.dropDownTargetParent
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 = dropDownTargetParent ~= nil and dropDownTargetParent or game:GetService("CoreGui").DevConsoleMaster,
}, {
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,15 @@
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 = {},
dropDownTargetParent = Instance.new("ScreenGui"),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,152 @@
local CorePackages = game:GetService("CorePackages")
local RobloxGui = 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 portalTarget = self.props.portalTarget
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 = portalTarget ~= nil and portalTarget or game:GetService("CoreGui").DevConsoleMaster,
}, {
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,15 @@
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 = {},
portalTarget = Instance.new("ScreenGui"),
})
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,319 @@
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)
self:doSizeCheck()
end
function LiveUpdateElement:didUpdate()
self:doSizeCheck()
end
function LiveUpdateElement:doSizeCheck()
if self.ref.current then
local formFactorThreshold = self.state.formFactorThreshold
local isSmallerThanFormFactorThreshold = self.ref.current.AbsoluteSize.X < formFactorThreshold
if isSmallerThanFormFactorThreshold ~= self.state.isSmallerThanFormFactorThreshold then
self:setState({
isSmallerThanFormFactorThreshold = isSmallerThanFormFactorThreshold
})
end
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,
isSmallerThanFormFactorThreshold = false,
}
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 isSmallerThanFormFactorThreshold = self.state.isSmallerThanFormFactorThreshold
local useSmallForm = false
local currMemStrWidth = memStatStrWidth.X
local alignment = Enum.HorizontalAlignment.Center
if formFactor == Constants.FormFactor.Small or isSmallerThanFormFactorThreshold 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)
)
if formFactor == Constants.FormFactor.Small or isSmallerThanFormFactorThreshold 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,34 @@
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 {
MainView = {
isDeveloperView = true,
},
}
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,30 @@
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 ClientLog = require(script.Parent.ClientLog)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
isDeveloperView = true,
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
ClientLog = Roact.createElement(ClientLog)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,143 @@
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 ScrollingTextBox = require(script.Parent.Parent.ScrollingTextBox)
local Constants = require(script.Parent.Parent.Parent.Constants)
local COMMANDLINE_INDENT = Constants.LogFormatting.CommandLineIndent
local COMMANDLINE_FONTSIZE = Constants.DefaultFontSize.CommandLine
local FONT = Constants.Font.Log
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.textbox = Roact.createRef()
end
function DevConsoleCommandLine:didMount()
if not self.onFocusConnection then
self.onFocusConnection = UserInputService.InputBegan:Connect(function(input)
if self.textbox.current and self.textbox.current:IsFocused() then
local rbx = self.textbox.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.textbox.current and self.textbox.current:IsFocused() then
if input.KeyCode == Enum.KeyCode.Return then
self.textbox.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,
}),
InputField = Roact.createElement(ScrollingTextBox, {
Position = UDim2.new(0, COMMANDLINE_INDENT, 0, 0),
Size = UDim2.new(1, -COMMANDLINE_INDENT, 0, height),
ShowNativeInput = true,
ClearTextOnFocus = false,
TextColor3 = Constants.Color.Text,
TextXAlignment = 0,
TextSize = COMMANDLINE_FONTSIZE,
Text = initText,
Font = FONT,
PlaceholderText = "command line",
[Roact.Ref] = self.textbox,
TextBoxFocusLost = self.onFocusLost,
})
})
end
return DataConsumer(DevConsoleCommandLine, "ServerLogData")
@@ -0,0 +1,30 @@
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 DevConsoleCommandLine = require(script.Parent.DevConsoleCommandLine)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
isDeveloperView = true,
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
CmdLine = Roact.createElement(DevConsoleCommandLine),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,343 @@
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._isRunning = 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)
local oldState = self._filters[name]
self._filters[name] = newState
if not validActiveFilters(self._filters) then
self._filters[name] = oldState
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:isRunning()
return self._initialized
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
self._isRunning = true
end
function LogData:stop()
self._initialized = false
self._isRunning = 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,263 @@
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 ARROW_OFFSET = Constants.LogFormatting.TextFrameHeight -- reuse this value for offset needed for the "<" char
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 FFlagDevConsoleLogNewLineFix = settings():GetFFlag("DevConsoleLogNewLineFix")
local LogOutput = Roact.Component:extend("LogOutput")
function LogOutput:init(props)
local initLogOutput = props.initLogOutput and props.initLogOutput()
self.onCanvasChange = function()
local current = self.ref.current
if current then
local canvasPos = current.CanvasPosition
local absSize = current.AbsoluteSize
if self.state.canvasPos ~= canvasPos or
self.state.absSize ~= absSize then
local yTotal = current.CanvasPosition.Y + current.AbsoluteSize.Y
local yCanvasSize = current.CanvasSize.Y.Offset
local autoScroll = yTotal == yCanvasSize
self:setState({
canvasPos = canvasPos,
absSize = absSize,
autoScroll = autoScroll,
})
end
end
end
self.ref = Roact.createRef()
self.state = {
logData = initLogOutput,
absSize = Vector2.new(),
canvasPos = Vector2.new(),
autoScroll = true,
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)
if self.state.autoScroll then
local current = self.ref.current
if current then
local newPos = Vector2.new(current.CanvasPosition.X, self.ref.current.CanvasSize.Y.Offset + self.ref.current.AbsoluteSize.Y)
current.CanvasPosition = newPos
end
end
end
function LogOutput:didMount()
self.logConnector = self.props.targetSignal:Connect(function(data)
if not self.state.autoScroll and
data:getSize() == data:getMaxSize() then
local canvasPos = self.state.canvasPos
local canvasPosY = canvasPos.Y
local newestMsg = data:back()
if newestMsg then
local msgDimsY = newestMsg.Dims.Y
local frameWidth = self.state.absSize.X - ARROW_OFFSET
if self.state.wordWrap and frameWidth > 0 then
msgDimsY = newestMsg.Dims.Y * math.ceil(newestMsg.Dims.X / frameWidth)
end
canvasPosY = math.max(0, canvasPosY - msgDimsY - LINE_PADDING)
end
self:setState({
logData = data,
canvasPos = Vector2.new(canvasPos.X, canvasPosY),
})
else
self:setState({
logData = data
})
end
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
local elements = {}
local messageCount = 1
local scrollingFrameHeight = 0
if self.ref.current and logData then
local frameWidth = absSize.X - ARROW_OFFSET
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 and frameWidth > 0 then
if FFlagDevConsoleLogNewLineFix then
-- this fix doesn't fully solve the problem, but it does prevent the white space
-- error to 2 lines at most, which should be acceptable until we can refactor
-- this whole hot mess
-- one full message height per time the message width fits in the container
msgDimsY = message.Dims.Y * message.Dims.X / absSize.X
-- round to the nearest line height
msgDimsY = math.ceil(msgDimsY / FONT_SIZE) * FONT_SIZE
else
msgDimsY = message.Dims.Y * math.ceil(message.Dims.X / frameWidth)
end
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 = FFlagDevConsoleLogNewLineFix and
-- flag true
UDim2.new(1, -ARROW_OFFSET, 0, msgDimsY) or
-- flag false
UDim2.new(1, 0, 0, msgDimsY),
Position = UDim2.new(0, ARROW_OFFSET, 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(0, 0, 0, scrollingFrameHeight),
CanvasPosition = canvasPos,
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,41 @@
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,
isDeveloperView = true,
},
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

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