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,309 @@
--[[
// 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")
--- 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 = false
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,442 @@
--[[
// 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)
local PolicyService = require(RobloxGui.Modules.Common.PolicyService)
-- FLAGS
local FFlagInspectMenuSubjectToPolicy = require(CoreGuiModules.Flags.FFlagInspectMenuSubjectToPolicy)
-- 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 FFlagInspectMenuSubjectToPolicy then
enabled = enabled and not PolicyService:IsSubjectToChinaPolicies()
end
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 FFlagInspectMenuSubjectToPolicy then
enabled = enabled and PolicyService:IsSubjectToChinaPolicies()
end
if not enabled then
ContextMenuItems:RemoveDefaultMenuItem(Enum.AvatarContextMenuOption.InspectMenu)
else
ContextMenuItems:EnableDefaultMenuItem(Enum.AvatarContextMenuOption.InspectMenu)
end
end)
if FFlagInspectMenuSubjectToPolicy then
spawn(function()
-- Check whether InspectMenu is disabled by policy after PolicyService is finished initializing
PolicyService:InitAsync()
ContextMenuItems:UpdateInspectMenuEnabled()
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()
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(info)
return {
info = info,
}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(gameName)
return {
gameName = gameName,
}
end)
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(promptType, promptInfo)
return {
promptType = promptType,
promptInfo = promptInfo,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(screenSize)
return {
screenSize = screenSize,
}
end)
@@ -0,0 +1,171 @@
local CorePackages = game:GetService("CorePackages")
local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local RoactGamepad = require(CorePackages.Packages.RoactGamepad)
local t = require(CorePackages.Packages.t)
local ExternalEventConnection = require(CorePackages.RoactUtilities.ExternalEventConnection)
local Connection = require(script.Parent.Connection)
local Prompts = script.Parent.Prompts
local AllowInventoryReadAccessPrompt = require(Prompts.AllowInventoryReadAccessPrompt)
local SaveAvatarPrompt = require(Prompts.SaveAvatarPrompt)
local CreateOutfitPrompt = require(Prompts.CreateOutfitPrompt)
local EnterOutfitNamePrompt = require(Prompts.EnterOutfitNamePrompt)
local SetFavoritePrompt = require(Prompts.SetFavoritePrompt)
local AvatarEditorPrompts = script.Parent.Parent
local PromptType = require(AvatarEditorPrompts.PromptType)
local ScreenSizeUpdated = require(AvatarEditorPrompts.Actions.ScreenSizeUpdated)
local Modules = AvatarEditorPrompts.Parent
local FFlagAESPromptsSupportGamepad = require(Modules.Flags.FFlagAESPromptsSupportGamepad)
--Displays behind the InGameMenu so that developers can't block interaction with the InGameMenu by constantly prompting.
local AVATAR_PROMPTS_DISPLAY_ORDER = 0
local GAMEPAD_INPUT_TYPES = {
[Enum.UserInputType.Gamepad1] = true,
[Enum.UserInputType.Gamepad2] = true,
[Enum.UserInputType.Gamepad3] = true,
[Enum.UserInputType.Gamepad4] = true,
[Enum.UserInputType.Gamepad5] = true,
[Enum.UserInputType.Gamepad6] = true,
[Enum.UserInputType.Gamepad7] = true,
[Enum.UserInputType.Gamepad8] = true,
}
local AvatarEditorPromptsApp = Roact.PureComponent:extend("AvatarEditorPromptsApp")
AvatarEditorPromptsApp.validateProps = t.strictInterface({
--From State
promptType = t.optional(t.userdata),
--Dispatch
screenSizeUpdated = t.callback,
})
function AvatarEditorPromptsApp:init()
if FFlagAESPromptsSupportGamepad then
self:setState({
isGamepad = GAMEPAD_INPUT_TYPES[UserInputService:GetLastInputType()] or false
})
end
self.absoluteSizeChanged = function(rbx)
self.props.screenSizeUpdated(rbx.AbsoluteSize)
end
if FFlagAESPromptsSupportGamepad then
self.focusController = RoactGamepad.createFocusController()
self.selectedCoreGuiObject = nil
self.selectedGuiObject = nil
end
end
function AvatarEditorPromptsApp:render()
local promptComponent
if self.props.promptType == PromptType.AllowInventoryReadAccess then
promptComponent = Roact.createElement(AllowInventoryReadAccessPrompt)
elseif self.props.promptType == PromptType.SaveAvatar then
promptComponent = Roact.createElement(SaveAvatarPrompt)
elseif self.props.promptType == PromptType.CreateOutfit then
promptComponent = Roact.createElement(CreateOutfitPrompt)
elseif self.props.promptType == PromptType.EnterOutfitName then
promptComponent = Roact.createElement(EnterOutfitNamePrompt)
elseif self.props.promptType == PromptType.SetFavorite then
promptComponent = Roact.createElement(SetFavoritePrompt)
end
return Roact.createElement("ScreenGui", {
IgnoreGuiInset = true,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
AutoLocalize = false,
DisplayOrder = AVATAR_PROMPTS_DISPLAY_ORDER,
[Roact.Change.AbsoluteSize] = self.absoluteSizeChanged,
}, {
Connection = Roact.createElement(Connection),
LastInputTypeConnection = FFlagAESPromptsSupportGamepad and Roact.createElement(ExternalEventConnection, {
event = UserInputService.LastInputTypeChanged,
callback = function(lastInputType)
self:setState({
isGamepad = GAMEPAD_INPUT_TYPES[lastInputType] or false
})
end,
}) or nil,
PromptFrame = FFlagAESPromptsSupportGamepad and Roact.createElement(RoactGamepad.Focusable.Frame, {
focusController = self.focusController,
BackgroundTransparency = 1,
Size = UDim2.fromScale(1, 1),
}, {
Prompt = promptComponent,
}) or nil,
Prompt = not FFlagAESPromptsSupportGamepad and promptComponent or nil,
})
end
if FFlagAESPromptsSupportGamepad then
function AvatarEditorPromptsApp:revertSelectedGuiObject()
if self.selectedCoreGuiObject then
GuiService.SelectedCoreObject = self.selectedCoreGuiObject
elseif self.selectedGuiObject then
GuiService.SelectedObject = self.selectedGuiObject
end
self.selectedCoreGuiObject = nil
self.selectedGuiObject = nil
end
function AvatarEditorPromptsApp:didUpdate(prevProps, prevState)
local shouldCaptureFocus = self.state.isGamepad and self.props.promptType ~= nil
local lastShouldCaptureFocus = prevState.isGamepad and prevProps.promptType ~= nil
if shouldCaptureFocus ~= lastShouldCaptureFocus then
if shouldCaptureFocus then
self.selectedCoreGuiObject = GuiService.SelectedCoreObject
self.selectedGuiObject = GuiService.SelectedObject
GuiService.SelectedObject = nil
self.focusController.captureFocus()
else
self.focusController.releaseFocus()
if self.state.isGamepad then
self:revertSelectedGuiObject()
end
end
end
end
function AvatarEditorPromptsApp:willUnmount()
if self.state.isGamepad then
self:revertSelectedGuiObject()
end
end
end
local function mapStateToProps(state)
return {
promptType = state.promptInfo.promptType,
}
end
local function mapDispatchToProps(dispatch)
return {
screenSizeUpdated = function(screenSize)
return dispatch(ScreenSizeUpdated(screenSize))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(AvatarEditorPromptsApp)
@@ -0,0 +1,79 @@
local CorePackages = game:GetService("CorePackages")
local AvatarEditorService = game:GetService("AvatarEditorService")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local t = require(CorePackages.Packages.t)
local Components = script.Parent.Parent
local AvatarEditorPrompts = Components.Parent
local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt)
local PromptType = require(AvatarEditorPrompts.PromptType)
local OpenSetFavoritePrompt = require(AvatarEditorPrompts.Thunks.OpenSetFavoritePrompt)
local OpenSaveAvatarPrompt = require(AvatarEditorPrompts.Thunks.OpenSaveAvatarPrompt)
local ExternalEventConnection = require(CorePackages.RoactUtilities.ExternalEventConnection)
local AvatarEditorServiceConnector = Roact.PureComponent:extend("AvatarEditorServiceConnector")
AvatarEditorServiceConnector.validateProps = t.strictInterface({
--Dispatch
openPrompt = t.callback,
openSetFavoritePrompt = t.callback,
openSaveAvatarPrompt = t.callback,
})
function AvatarEditorServiceConnector:render()
return Roact.createFragment({
OpenPromptSaveAvatarConnection = Roact.createElement(ExternalEventConnection, {
event = AvatarEditorService.OpenPromptSaveAvatar,
callback = function(humanoidDescription, rigType)
self.props.openSaveAvatarPrompt(humanoidDescription, rigType)
end,
}),
OpenAllowInventoryReadAccessConnection = Roact.createElement(ExternalEventConnection, {
event = AvatarEditorService.OpenAllowInventoryReadAccess,
callback = function()
self.props.openPrompt(PromptType.AllowInventoryReadAccess, {})
end,
}),
OpenPromptCreateOufitConnection = Roact.createElement(ExternalEventConnection, {
event = AvatarEditorService.OpenPromptCreateOufit,
callback = function(humanoidDescription, rigType)
self.props.openPrompt(PromptType.CreateOutfit, {
humanoidDescription = humanoidDescription,
rigType = rigType,
})
end,
}),
OpenPromptSetFavoriteConnection = Roact.createElement(ExternalEventConnection, {
event = AvatarEditorService.OpenPromptSetFavorite,
callback = function(itemId, itemType, isFavorited)
self.props.openSetFavoritePrompt(itemId, itemType, isFavorited)
end,
}),
})
end
local function mapDispatchToProps(dispatch)
return {
openPrompt = function(promptType, promptArgs)
return dispatch(OpenPrompt(promptType, promptArgs))
end,
openSetFavoritePrompt = function(itemId, itemType, shouldFavorite)
return dispatch(OpenSetFavoritePrompt(itemId, itemType, shouldFavorite))
end,
openSaveAvatarPrompt = function(humanoidDescription, rigType)
return dispatch(OpenSaveAvatarPrompt(humanoidDescription, rigType))
end,
}
end
return RoactRodux.UNSTABLE_connect2(nil, mapDispatchToProps)(AvatarEditorServiceConnector)
@@ -0,0 +1,106 @@
local CorePackages = game:GetService("CorePackages")
local ContextActionService = game:GetService("ContextActionService")
local GuiService = game:GetService("GuiService")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local t = require(CorePackages.Packages.t)
local Connection = script.Parent
local Components = Connection.Parent
local AvatarEditorPrompts = Components.Parent
local CloseOpenPrompt = require(AvatarEditorPrompts.Thunks.CloseOpenPrompt)
local CLOSE_AVATAR_EDITOR_PROMPT_NAME = "CloseAvatarEditorPrompt"
local ContextActionsBinder = Roact.PureComponent:extend("ContextActionsBinder")
ContextActionsBinder.validateProps = t.strictInterface({
--Map State to Props
promptOpen = t.boolean,
--Map dispatch to props
closeOpenPrompt = t.callback,
})
function ContextActionsBinder:init()
self.actionsBound = false
end
function ContextActionsBinder:bindActions()
if self.actionsBound then
return
end
self.actionsBound = true
ContextActionService:BindCoreAction(
CLOSE_AVATAR_EDITOR_PROMPT_NAME,
function(actionName, inputState, inputObject)
if GuiService.MenuIsOpen then
return Enum.ContextActionResult.Pass
end
if inputState ~= Enum.UserInputState.Begin then
return Enum.ContextActionResult.Pass
end
self.props.closeOpenPrompt()
return Enum.ContextActionResult.Sink
end,
false,
Enum.KeyCode.Escape
)
end
function ContextActionsBinder:unbindActions()
if not self.actionsBound then
return
end
self.actionsBound = false
ContextActionService:UnbindCoreAction(CLOSE_AVATAR_EDITOR_PROMPT_NAME)
end
function ContextActionsBinder:didMount()
if self.props.promptOpen then
self:bindActions()
end
end
function ContextActionsBinder:render()
return nil
end
function ContextActionsBinder:didUpdate(prevProps, prevState)
if self.props.promptOpen ~= prevProps.promptOpen then
if self.props.promptOpen then
self:bindActions()
else
self:unbindActions()
end
end
end
function ContextActionsBinder:willUnmount()
if self.actionsBound then
self:unbindActions()
end
end
local function mapStateToProps(state)
return {
promptOpen = state.promptInfo.promptType ~= nil,
}
end
local function mapDispatchToProps(dispatch)
return {
closeOpenPrompt = function()
return dispatch(CloseOpenPrompt)
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(ContextActionsBinder)
@@ -0,0 +1,17 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local ContextActionsBinder = require(script.ContextActionsBinder)
local AvatarEditorServiceConnector = require(script.AvatarEditorServiceConnector)
local Connection = Roact.PureComponent:extend("Connection")
function Connection:render()
return Roact.createFragment({
ContextActionsBinder = Roact.createElement(ContextActionsBinder),
AvatarEditorServiceConnector = Roact.createElement(AvatarEditorServiceConnector),
})
end
return Connection
@@ -0,0 +1,300 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local Players = game:GetService("Players")
local Roact = require(CorePackages.Roact)
local t = require(CorePackages.Packages.t)
local UIBlox = require(CorePackages.UIBlox)
local ShimmerPanel = UIBlox.Loading.ShimmerPanel
local EmptyState = UIBlox.App.Indicator.EmptyState
local FFlagBetterHumanoidViewportCameraPositioning =
game:DefineFastFlag("BetterHumanoidViewportCameraPositioning", false)
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local EngineFeatureAESConformToAvatarRules = game:GetEngineFeature("AESConformToAvatarRules")
local INITIAL_OFFSET = 5
local ROTATION_CFRAME = CFrame.fromEulerAnglesXYZ(math.rad(20), math.rad(15), math.rad(40))
local THUMBNAIL_FOV = 70
local ZOOM_FACTOR = FFlagBetterHumanoidViewportCameraPositioning and 1 or 1.2
local HumanoidViewport = Roact.PureComponent:extend("HumanoidViewport")
HumanoidViewport.validateProps = t.strictInterface({
humanoidDescription = EngineFeatureAESConformToAvatarRules and t.optional(t.instanceOf("HumanoidDescription"))
or t.instanceOf("HumanoidDescription"),
loadingFailed = EngineFeatureAESConformToAvatarRules and t.boolean or nil,
retryLoadDescription = EngineFeatureAESConformToAvatarRules and t.callback or nil,
rigType = t.enum(Enum.HumanoidRigType),
})
function HumanoidViewport:init()
self:setState({
loading = true,
loadingFailed = false,
})
self.cameraRef = Roact.createRef()
self.worldModelRef = Roact.createRef()
self.cameraCFrameBinding, self.updateCameraCFrameBinding = Roact.createBinding(CFrame.new())
self.cameraFocusBinding, self.updateCameraFocusBinding = Roact.createBinding(CFrame.new())
self.humanoidModel = nil
self.mounted = false
self.onRetryLoading = function()
self:setState({
loading = true,
loadingFailed = false,
})
if self.props.humanoidDescription then
self:loadHumanoidModel()
else
self.props.retryLoadDescription()
end
end
end
local function rotateLookVector(lookVector)
local look = lookVector
if math.abs(look.Y) > 0.95 then
look = Vector3.new(0, 0, -1)
else
look = Vector3.new(look.X, 0, look.Z)
look = look.unit
end
local lookCoord = CFrame.new(Vector3.new(0, 0, 0), look)
lookCoord = lookCoord * ROTATION_CFRAME
return lookCoord.lookVector
end
local function getCameraOffset(fov, extentsSize)
local xSize, ySize, zSize = extentsSize.X, extentsSize.Y, extentsSize.Z
local maxSize
if FFlagBetterHumanoidViewportCameraPositioning then
maxSize = math.max(xSize, ySize)
else
maxSize = math.sqrt(xSize^2 + ySize^2 + zSize^2)
end
local fovMultiplier = 1 / math.tan(math.rad(fov) / 2)
local halfSize = maxSize / 2
if FFlagBetterHumanoidViewportCameraPositioning then
return (halfSize * fovMultiplier) + (zSize / 2)
else
return halfSize * fovMultiplier
end
end
local function zoomExtents(model, lookVector, cameraCFrame)
local modelCFrame = model:GetModelCFrame()
local position = modelCFrame.p
local extentsSize = model:GetExtentsSize()
local cameraOffset = getCameraOffset(THUMBNAIL_FOV, extentsSize)
local zoomFactor = 1 / ZOOM_FACTOR
cameraOffset = cameraOffset * zoomFactor
local cameraRotation = cameraCFrame - cameraCFrame.p
return cameraRotation + position + (lookVector * cameraOffset)
end
function HumanoidViewport:positionCamera()
local model = self.humanoidModel
local modelCFrame = model:GetModelCFrame()
local lookVector = modelCFrame.lookVector
local humanoidRootPart = model:FindFirstChild("HumanoidRootPart")
if humanoidRootPart then
lookVector = humanoidRootPart.CFrame.lookVector
end
lookVector = rotateLookVector(lookVector)
local cameraCFrame = CFrame.new(modelCFrame.p + (lookVector * INITIAL_OFFSET), modelCFrame.p)
cameraCFrame = zoomExtents(model, lookVector, cameraCFrame)
self.updateCameraCFrameBinding(cameraCFrame)
self.updateCameraFocusBinding(modelCFrame)
end
function HumanoidViewport:loadIdleAnimation(humanoidModel)
local humanoid = humanoidModel:FindFirstChildOfClass("Humanoid")
local humanoidDescription = humanoid.HumanoidDescription
if humanoidDescription.IdleAnimation == 0 then
return
end
local animate = humanoidModel:FindFirstChild("Animate")
if not animate then
return
end
local idle = animate:FindFirstChild("idle")
if not idle then
return
end
local animation = idle:FindFirstChildOfClass("Animation")
if not animation then
return
end
local animationTrack = humanoid:LoadAnimation(animation)
animationTrack.Looped = true
animationTrack:Play()
end
function HumanoidViewport:loadHumanoidModel()
local humanoidDescription = self.props.humanoidDescription
local rigType = self.props.rigType
coroutine.wrap(function()
local model
if EngineFeatureAESConformToAvatarRules then
pcall(function()
model = Players:CreateHumanoidModelFromDescription(humanoidDescription, rigType)
end)
else
model = Players:CreateHumanoidModelFromDescription(humanoidDescription, rigType)
end
if not self.mounted then
return
end
if self.props.humanoidDescription ~= humanoidDescription then
return
end
if self.props.rigType ~= rigType then
return
end
if EngineFeatureAESConformToAvatarRules and model == nil then
self:setState({
loadingFailed = true,
})
return
end
self.humanoidModel = model
if self.worldModelRef:getValue() then
self.humanoidModel.Parent = self.worldModelRef:getValue()
end
self:positionCamera()
self:loadIdleAnimation(model)
self:setState({
loading = false,
loadingFailed = false,
})
end)()
end
function HumanoidViewport:render()
local showShimmerFrame = self.state.loading
local showLoadingFailed = false
local showViewportFrame = not self.state.loading
if EngineFeatureAESConformToAvatarRules then
showLoadingFailed = self.props.loadingFailed or self.state.loadingFailed
showShimmerFrame = (not showLoadingFailed) and self.state.loading
showViewportFrame = not (showLoadingFailed or self.state.loading)
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromScale(1, 1),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
}, {
AspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint", {
AspectRatio = 1,
AspectType = Enum.AspectType.FitWithinMaxSize,
DominantAxis = Enum.DominantAxis.Width,
}),
ShimmerFrame = showShimmerFrame and Roact.createElement(ShimmerPanel, {
Size = UDim2.fromScale(1, 1),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
}),
LoadingFailed = showLoadingFailed and Roact.createElement(EmptyState, {
text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.ItemsListLoadingFailed"),
size = UDim2.fromScale(1, 1),
reloadAction = self.onRetryLoading,
}),
ViewportFrame = Roact.createElement("ViewportFrame", {
Visible = showViewportFrame,
BackgroundTransparency = 1,
Size = UDim2.fromScale(1, 1),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
LightColor = Color3.fromRGB(240, 240, 240),
Ambient = Color3.fromRGB(240, 240, 240),
CurrentCamera = self.cameraRef,
}, {
Camera = Roact.createElement("Camera", {
CameraType = Enum.CameraType.Scriptable,
FieldOfView = THUMBNAIL_FOV,
CFrame = self.cameraCFrameBinding,
Focus = self.cameraFocusBinding,
[Roact.Ref] = self.cameraRef,
}),
WorldModel = Roact.createElement("WorldModel", {
[Roact.Ref] = self.worldModelRef,
}),
}),
})
end
function HumanoidViewport:didMount()
self.mounted = true
if EngineFeatureAESConformToAvatarRules then
if self.props.humanoidDescription then
self:loadHumanoidModel()
end
else
self:loadHumanoidModel()
end
end
function HumanoidViewport:didUpdate(prevProps)
if self.worldModelRef:getValue() and self.humanoidModel then
self.humanoidModel.Parent = self.worldModelRef:getValue()
end
local descriptionUpdated = self.props.humanoidDescription ~= prevProps.humanoidDescription
local rigTypeUpdated = self.props.rigType ~= prevProps.rigType
if descriptionUpdated or rigTypeUpdated then
self:setState({
loading = true
})
if EngineFeatureAESConformToAvatarRules then
if self.props.humanoidDescription ~= nil then
self:loadHumanoidModel()
end
else
self:loadHumanoidModel()
end
end
end
function HumanoidViewport:willUnmount()
self.mounted = false
end
return HumanoidViewport
@@ -0,0 +1,85 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local t = require(CorePackages.Packages.t)
local UIBlox = require(CorePackages.UIBlox)
local AvatarExperienceDeps = require(CorePackages.AvatarExperienceDeps)
local Text = require(CorePackages.AppTempCommon.Common.Text)
local RoactFitComponents = AvatarExperienceDeps.RoactFitComponents
local FitTextLabel = RoactFitComponents.FitTextLabel
local withStyle = UIBlox.Style.withStyle
local BULLET_POINT_SYMBOL = ""
local ListEntry = Roact.PureComponent:extend("ListEntry")
ListEntry.validateProps = t.strictInterface({
text = t.string,
hasBullet = t.boolean,
layoutOrder = t.integer,
positionChangedCallback = t.optional(t.callback),
NextSelectionLeft = t.optional(t.table),
NextSelectionRight = t.optional(t.table),
NextSelectionUp = t.optional(t.table),
NextSelectionDown = t.optional(t.table),
[Roact.Ref] = t.optional(t.table),
})
function ListEntry:render()
return withStyle(function(stylePalette)
local fontInfo = stylePalette.Font
local theme = stylePalette.Theme
local font = fontInfo.CaptionBody.Font
local fontSize = fontInfo.BaseSize * fontInfo.CaptionBody.RelativeSize
local bulletPointWidth = Text.GetTextWidth(BULLET_POINT_SYMBOL, font, fontSize)
return Roact.createElement(RoactFitComponents.FitFrameVertical, {
width = UDim.new(1, 0),
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Top,
BackgroundTransparency = 1,
LayoutOrder = self.props.layoutOrder,
[Roact.Change.AbsolutePosition] = self.props.positionChangedCallback,
NextSelectionLeft = self.props.NextSelectionLeft,
NextSelectionRight = self.props.NextSelectionRight,
NextSelectionUp = self.props.NextSelectionUp,
NextSelectionDown = self.props.NextSelectionDown,
[Roact.Ref] = self.props[Roact.Ref],
}, {
Bullet = self.props.hasBullet and Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(bulletPointWidth, fontSize),
Text = BULLET_POINT_SYMBOL,
Font = font,
TextSize = fontSize,
TextColor3 = theme.TextDefault.Color,
TextTransparency = theme.TextDefault.Transparency,
TextXAlignment = Enum.TextXAlignment.Left,
LayoutOrder = 1,
}),
Text = Roact.createElement(FitTextLabel, {
width = UDim.new(1, self.props.hasBullet and -bulletPointWidth or 0),
BackgroundTransparency = 1,
Text = self.props.text,
Font = font,
TextSize = fontSize,
TextColor3 = theme.TextDefault.Color,
TextTransparency = theme.TextDefault.Transparency,
TextXAlignment = Enum.TextXAlignment.Left,
LayoutOrder = 2,
})
})
end)
end
return ListEntry
@@ -0,0 +1,33 @@
return function()
local CorePackages = game:GetService("CorePackages")
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local Roact = require(CorePackages.Roact)
local UIBlox = require(CorePackages.UIBlox)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
describe("ListEntry", function()
it("should create and destroy without errors", function()
local ListEntry = require(script.Parent.ListEntry)
local element = Roact.createElement(UIBlox.Style.Provider, {
style = appStyle,
}, {
ListEntry = Roact.createElement(ListEntry, {
text = "Hello World!",
hasBullet = true,
layoutOrder = 1,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,94 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactGamepad = require(CorePackages.Packages.RoactGamepad)
local t = require(CorePackages.Packages.t)
local AvatarExperienceDeps = require(CorePackages.AvatarExperienceDeps)
local RoactFitComponents = AvatarExperienceDeps.RoactFitComponents
local ListEntry = require(script.Parent.ListEntry)
local AvatarEditorPrompts = script.Parent.Parent.Parent
local Modules = AvatarEditorPrompts.Parent
local FFlagAESPromptsSupportGamepad = require(Modules.Flags.FFlagAESPromptsSupportGamepad)
local ListSection = Roact.PureComponent:extend("ListSection")
ListSection.validateProps = t.strictInterface({
headerText = t.string,
items = t.array(t.string),
layoutOrder = t.integer,
isFirstSection = t.boolean,
isLastSection = t.boolean,
NextSelectionLeft = t.optional(t.table),
NextSelectionRight = t.optional(t.table),
NextSelectionUp = t.optional(t.table),
NextSelectionDown = t.optional(t.table),
[Roact.Ref] = t.table,
})
function ListSection:init()
if FFlagAESPromptsSupportGamepad then
self.listRefCache = RoactGamepad.createRefCache()
end
end
function ListSection:render()
local listSection = {}
listSection[0] = Roact.createElement(
FFlagAESPromptsSupportGamepad and RoactGamepad.Focusable[ListEntry] or ListEntry, {
text = self.props.headerText,
hasBullet = false,
layoutOrder = 0,
positionChangedCallback = self.props.isFirstSection and self.firstEntryPositionChanged or nil,
NextSelectionDown = FFlagAESPromptsSupportGamepad and
#self.props.items > 0 and self.listRefCache[1] or nil,
[Roact.Ref] = FFlagAESPromptsSupportGamepad and self.listRefCache[0] or nil,
})
for index, name in ipairs(self.props.items) do
local positionChangedCallback = nil
if self.props.isLastSection and index == #self.props.items then
positionChangedCallback = self.lastEntryPositionChanged
end
listSection[index] = Roact.createElement(
FFlagAESPromptsSupportGamepad and RoactGamepad.Focusable[ListEntry] or ListEntry, {
text = name,
hasBullet = true,
layoutOrder = index,
positionChangedCallback = positionChangedCallback,
NextSelectionUp = FFlagAESPromptsSupportGamepad and self.listRefCache[index - 1] or nil,
NextSelectionDown = FFlagAESPromptsSupportGamepad and
index ~= #self.props.items and self.listRefCache[index + 1] or nil,
[Roact.Ref] = FFlagAESPromptsSupportGamepad and self.listRefCache[index] or nil,
})
end
return Roact.createElement(
FFlagAESPromptsSupportGamepad and RoactGamepad.Focusable[RoactFitComponents.FitFrameVertical]
or RoactFitComponents.FitFrameVertical, {
width = UDim.new(1, 0),
FillDirection = Enum.FillDirection.Vertical,
VerticalAlignment = Enum.VerticalAlignment.Top,
BackgroundTransparency = 1,
LayoutOrder = self.props.layoutOrder,
defaultChild = FFlagAESPromptsSupportGamepad and self.listRefCache[0] or nil,
NextSelectionLeft = self.props.NextSelectionLeft,
NextSelectionRight = self.props.NextSelectionRight,
NextSelectionUp = self.props.NextSelectionUp,
NextSelectionDown = self.props.NextSelectionDown,
[Roact.Ref] = self.props[Roact.Ref],
}, listSection)
end
return ListSection
@@ -0,0 +1,38 @@
return function()
local CorePackages = game:GetService("CorePackages")
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local Roact = require(CorePackages.Roact)
local UIBlox = require(CorePackages.UIBlox)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
describe("ListSection", function()
it("should create and destroy without errors", function()
local ListSection = require(script.Parent.ListSection)
local element = Roact.createElement(UIBlox.Style.Provider, {
style = appStyle,
}, {
ListEntry = Roact.createElement(ListSection, {
headerText = "List Header",
items = {
"Entry 1",
"Entry 2",
},
layoutOrder = 1,
isFirstSection = true,
isLastSection = false,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,370 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local Roact = require(CorePackages.Roact)
local RoactGamepad = require(CorePackages.Packages.RoactGamepad)
local RoactRodux = require(CorePackages.RoactRodux)
local t = require(CorePackages.Packages.t)
local UIBlox = require(CorePackages.UIBlox)
local VerticalScrollView = UIBlox.App.Container.VerticalScrollView
local withStyle = UIBlox.Style.withStyle
local ShimmerPanel = UIBlox.Loading.ShimmerPanel
local EmptyState = UIBlox.App.Indicator.EmptyState
local ListSection = require(script.ListSection)
local AvatarEditorPrompts = script.Parent.Parent
local GetAssetsDifference = require(AvatarEditorPrompts.GetAssetsDifference)
local AddAnalyticsInfo = require(AvatarEditorPrompts.Actions.AddAnalyticsInfo)
local EngineFeatureAvatarEditorServiceAnalytics = game:GetEngineFeature("AvatarEditorServiceAnalytics")
local EngineFeatureAESConformToAvatarRules = game:GetEngineFeature("AESConformToAvatarRules")
local Modules = AvatarEditorPrompts.Parent
local FFlagAESPromptsSupportGamepad = require(Modules.Flags.FFlagAESPromptsSupportGamepad)
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local PADDING_BETWEEN = 10
local GRADIENT_HEIGHT = 30
local ItemsList = Roact.PureComponent:extend("ItemsList")
ItemsList.validateProps = t.strictInterface({
humanoidDescription = EngineFeatureAESConformToAvatarRules and t.optional(t.instanceOf("HumanoidDescription"))
or t.instanceOf("HumanoidDescription"),
loadingFailed = EngineFeatureAESConformToAvatarRules and t.boolean or nil,
retryLoadDescription = EngineFeatureAESConformToAvatarRules and t.callback or nil,
itemListScrollableUpdated = t.optional(t.callback),
addAnalyticsInfo = t.callback,
})
function ItemsList:init()
self:setState({
canvasSizeY = 0,
loading = true,
addedAssetNames = nil,
removedAssetNames = nil,
})
self.mounted = false
self.frameRef = Roact.createRef()
self.topGradientVisibleBinding, self.updateTopGradientVisibleBinding = Roact.createBinding(false)
self.bottomGradientVisibleBinding, self.updateBottomGradientVisibleBinding = Roact.createBinding(false)
if FFlagAESPromptsSupportGamepad then
self.addedSectionRef = Roact.createRef()
self.removedSectionRef = Roact.createRef()
self.noChangedAssetsRef = Roact.createRef()
end
self.lastWasScrollable = nil
self.checkIsScrollable = function()
local frame = self.frameRef:getValue()
if not frame then
return
end
if not self.props.itemListScrollableUpdated then
return
end
local shouldBeScrollable = self.state.canvasSizeY > frame.AbsoluteSize.Y
if shouldBeScrollable ~= self.lastWasScrollable then
self.lastWasScrollable = shouldBeScrollable
self.props.itemListScrollableUpdated(shouldBeScrollable, frame.AbsoluteSize.Y)
end
end
self.onContentSizeChanged = function(rbx)
self:setState({
canvasSizeY = rbx.AbsoluteContentSize.Y
})
end
self.firstEntryPositionChanged = function(rbx)
local frame = self.frameRef:getValue()
if not frame then
return
end
if rbx.AbsolutePosition.Y < frame.AbsolutePosition.Y then
self.updateTopGradientVisibleBinding(true)
else
self.updateTopGradientVisibleBinding(false)
end
end
self.lastEntryPositionChanged = function(rbx)
local frame = self.frameRef:getValue()
if not frame then
return
end
local entryMaxPosition = rbx.AbsolutePosition.Y + rbx.AbsoluteSize.Y
local frameMaxPosition = frame.AbsolutePosition.Y + frame.AbsoluteSize.Y
if entryMaxPosition > frameMaxPosition then
self.updateBottomGradientVisibleBinding(true)
else
self.updateBottomGradientVisibleBinding(false)
end
end
self.loadAssetNames = function()
coroutine.wrap(function()
GetAssetsDifference(self.props.humanoidDescription):andThen(function(result)
if self.mounted then
if EngineFeatureAvatarEditorServiceAnalytics then
self.props.addAnalyticsInfo(result.addedAssetIds, result.removedAssetIds)
end
self:setState({
loading = false,
addedAssetNames = result.addedNames,
removedAssetNames = result.removedNames,
})
end
end, function(err)
if self.mounted then
self:setState({
loading = false,
})
end
end)
end)()
end
self.onRetryLoading = function()
self:setState({
loading = true,
})
if EngineFeatureAESConformToAvatarRules then
if self.props.humanoidDescription then
self.loadAssetNames()
else
self.props.retryLoadDescription()
end
else
self.loadAssetNames()
end
end
end
function ItemsList:createEntriesList()
local list = {}
if #self.state.addedAssetNames > 0 then
local addingHeaderText = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.Adding")
table.insert(list, Roact.createElement(
FFlagAESPromptsSupportGamepad and RoactGamepad.Focusable[ListSection] or ListSection, {
headerText = addingHeaderText,
items = self.state.addedAssetNames,
layoutOrder = 1,
isFirstSection = true,
isLastSection = #self.state.removedAssetNames == 0,
NextSelectionDown = FFlagAESPromptsSupportGamepad and self.removedSectionRef or nil,
[Roact.Ref] = FFlagAESPromptsSupportGamepad and self.addedSectionRef or nil,
}))
if #self.state.removedAssetNames > 0 then
--Add some padding
table.insert(list, Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, PADDING_BETWEEN * 2),
LayoutOrder = 2,
}))
end
end
if #self.state.removedAssetNames > 0 then
local removingHeaderText = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.Removing")
table.insert(list, Roact.createElement(
FFlagAESPromptsSupportGamepad and RoactGamepad.Focusable[ListSection] or ListSection, {
headerText = removingHeaderText,
items = self.state.removedAssetNames,
layoutOrder = 3,
isFirstSection = #self.state.addedAssetNames == 0,
isLastSection = true,
NextSelectionUp = FFlagAESPromptsSupportGamepad and self.addedSectionRef or nil,
[Roact.Ref] = FFlagAESPromptsSupportGamepad and self.removedSectionRef or nil,
}))
end
local noChangedAssets = #self.state.addedAssetNames == 0 and #self.state.removedAssetNames == 0
if noChangedAssets then
local noChangedAssetsText = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.NoChangedAssets")
table.insert(list, Roact.createElement(
FFlagAESPromptsSupportGamepad and RoactGamepad.Focusable[ListSection] or ListSection, {
headerText = noChangedAssetsText,
items = {},
layoutOrder = 1,
isFirstSection = true,
isLastSection = true,
[Roact.Ref] = FFlagAESPromptsSupportGamepad and self.noChangedAssetsRef or nil,
}))
end
return list
end
function ItemsList:renderItemsList()
return withStyle(function(stylePalette)
local theme = stylePalette.Theme
local list = self:createEntriesList()
list.Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
Padding = UDim.new(0, PADDING_BETWEEN),
SortOrder = Enum.SortOrder.LayoutOrder,
[Roact.Change.AbsoluteContentSize] = self.onContentSizeChanged,
})
return Roact.createElement(
FFlagAESPromptsSupportGamepad and RoactGamepad.Focusable.Frame or "Frame", {
defaultChild = FFlagAESPromptsSupportGamepad and self.addedSectionRef or nil,
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
[Roact.Ref] = self.frameRef,
}, {
TopGradient = Roact.createElement("Frame", {
Visible = self.topGradientVisibleBinding,
Size = UDim2.new(1, 0, 0, GRADIENT_HEIGHT),
Position = UDim2.fromScale(0, 0),
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
ZIndex = 2,
}, {
UIGradient = Roact.createElement("UIGradient", {
Rotation = 90,
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, theme.BackgroundUIDefault.Color),
ColorSequenceKeypoint.new(1, theme.BackgroundUIDefault.Color)
}),
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(1, 1)
}),
}),
}),
ScrollView = Roact.createElement(VerticalScrollView, {
size = UDim2.fromScale(1, 1),
canvasSizeY = UDim.new(0, self.state.canvasSizeY),
}, list),
BottomGradient = Roact.createElement("Frame", {
Visible = self.bottomGradientVisibleBinding,
Size = UDim2.new(1, 0, 0, GRADIENT_HEIGHT),
Position = UDim2.fromScale(0, 1),
AnchorPoint = Vector2.new(0, 1),
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
ZIndex = 2,
}, {
UIGradient = Roact.createElement("UIGradient", {
Rotation = 90,
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, theme.BackgroundUIDefault.Color),
ColorSequenceKeypoint.new(1, theme.BackgroundUIDefault.Color)
}),
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(1, 0)
}),
}),
}),
})
end)
end
function ItemsList:renderLoading()
return Roact.createElement(ShimmerPanel, {
Size = UDim2.fromScale(1, 1),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
})
end
function ItemsList:renderFailed()
return Roact.createElement(EmptyState, {
text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.ItemsListLoadingFailed"),
size = UDim2.fromScale(1, 1),
reloadAction = self.onRetryLoading,
})
end
function ItemsList:render()
local isLoading = self.state.loading
if EngineFeatureAESConformToAvatarRules then
isLoading = self.state.loading and not self.props.loadingFailed
end
if isLoading then
return self:renderLoading()
elseif self.state.addedAssetNames then
return self:renderItemsList()
else
return self:renderFailed()
end
end
function ItemsList:didMount()
self.mounted = true
if EngineFeatureAESConformToAvatarRules then
if self.props.humanoidDescription then
self.loadAssetNames()
end
else
self.loadAssetNames()
end
self.checkIsScrollable()
end
function ItemsList:didUpdate(prevProps, prevState)
self.checkIsScrollable()
if EngineFeatureAESConformToAvatarRules then
if prevProps.humanoidDescription ~= self.props.humanoidDescription then
self:setState({
loading = true,
addedAssetNames = Roact.None,
removedAssetNames = Roact.None,
})
self.loadAssetNames()
end
end
end
function ItemsList:willUnmount()
self.mounted = false
end
local function mapDispatchToProps(dispatch)
return {
addAnalyticsInfo = function(info)
return dispatch(AddAnalyticsInfo(info))
end,
}
end
return RoactRodux.UNSTABLE_connect2(nil, mapDispatchToProps)(ItemsList)
@@ -0,0 +1,78 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local t = require(CorePackages.Packages.t)
local UIBlox = require(CorePackages.UIBlox)
local InteractiveAlert = UIBlox.App.Dialog.Alert.InteractiveAlert
local ButtonType = UIBlox.App.Button.Enum.ButtonType
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local Components = script.Parent.Parent
local AvatarEditorPrompts = Components.Parent
local SetAllowInventoryReadAccess = require(AvatarEditorPrompts.Thunks.SetAllowInventoryReadAccess)
local AllowInventoryReadAccessPrompt = Roact.PureComponent:extend("AllowInventoryReadAccessPrompt")
AllowInventoryReadAccessPrompt.validateProps = t.strictInterface({
--State
gameName = t.string,
screenSize = t.Vector2,
--Dispatch
setAvatarReadAccessAllowed = t.callback,
setAvatarReadAccessDenied = t.callback,
})
function AllowInventoryReadAccessPrompt:render()
return Roact.createElement(InteractiveAlert, {
title = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.InventoryReadAccessPromptTitle"),
bodyText = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.InventoryReadAccessPromptText", {
RBX_NAME = self.props.gameName,
}),
buttonStackInfo = {
buttons = {
{
props = {
onActivated = self.props.setAvatarReadAccessDenied,
text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.InventoryReadAccessPromptNo"),
},
},
{
buttonType = ButtonType.PrimarySystem,
props = {
onActivated = self.props.setAvatarReadAccessAllowed,
text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.InventoryReadAccessPromptYes"),
},
},
},
},
position = UDim2.fromScale(0.5, 0.5),
screenSize = self.props.screenSize,
})
end
local function mapStateToProps(state)
return {
gameName = state.gameName,
screenSize = state.screenSize,
}
end
local function mapDispatchToProps(dispatch)
return {
setAvatarReadAccessDenied = function()
return dispatch(SetAllowInventoryReadAccess(false))
end,
setAvatarReadAccessAllowed = function()
return dispatch(SetAllowInventoryReadAccess(true))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(AllowInventoryReadAccessPrompt)
@@ -0,0 +1,46 @@
return function()
local CorePackages = game:GetService("CorePackages")
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local Roact = require(CorePackages.Roact)
local Rodux = require(CorePackages.Rodux)
local RoactRodux = require(CorePackages.RoactRodux)
local UIBlox = require(CorePackages.UIBlox)
local AvatarEditorPrompts = script.Parent.Parent.Parent
local Reducer = require(AvatarEditorPrompts.Reducer)
local PromptType = require(AvatarEditorPrompts.PromptType)
local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
describe("AllowInventoryReadAccessPrompt", function()
it("should create and destroy without errors", function()
local AllowInventoryReadAccessPrompt = require(script.Parent.AllowInventoryReadAccessPrompt)
local store = Rodux.Store.new(Reducer, nil, {
Rodux.thunkMiddleware,
})
store:dispatch(OpenPrompt(PromptType.AllowInventoryReadAccess, {}))
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
ThemeProvider = Roact.createElement(UIBlox.Style.Provider, {
style = appStyle,
}, {
AllowInventoryReadAccessPrompt = Roact.createElement(AllowInventoryReadAccessPrompt)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,209 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local t = require(CorePackages.Packages.t)
local UIBlox = require(CorePackages.UIBlox)
local InteractiveAlert = UIBlox.App.Dialog.Alert.InteractiveAlert
local ButtonType = UIBlox.App.Button.Enum.ButtonType
local withStyle = UIBlox.Style.withStyle
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local Components = script.Parent.Parent
local AvatarEditorPrompts = Components.Parent
local HumanoidViewport = require(Components.HumanoidViewport)
local SignalCreateOutfitPermissionDenied = require(AvatarEditorPrompts.Thunks.SignalCreateOutfitPermissionDenied)
local CreateOutfitConfirmed = require(AvatarEditorPrompts.Actions.CreateOutfitConfirmed)
local GetConformedHumanoidDescription = require(AvatarEditorPrompts.GetConformedHumanoidDescription)
local EngineFeatureAESConformToAvatarRules = game:GetEngineFeature("AESConformToAvatarRules")
local VIEWPORT_SIDE_PADDING = 10
local SCREEN_SIZE_PADDING = 30
local CreateOutfitPrompt = Roact.PureComponent:extend("CreateOutfitPrompt")
CreateOutfitPrompt.validateProps = t.strictInterface({
--State
screenSize = t.Vector2,
humanoidDescription = t.instanceOf("HumanoidDescription"),
rigType = t.enum(Enum.HumanoidRigType),
--Dispatch
createOutfitConfirmed = t.callback,
signalCreateOutfitPermissionDenied = t.callback,
})
function CreateOutfitPrompt:init()
self.mounted = false
self.middleContentRef = Roact.createRef()
self.contentSize, self.updateContentSize = Roact.createBinding(UDim2.new(1, 0, 0, 200))
if EngineFeatureAESConformToAvatarRules then
self:setState({
conformedHumanoidDescription = nil,
getConformedDescriptionFailed = false,
})
end
self.onAlertSizeChanged = function(rbx)
local alertSize = rbx.AbsoluteSize
if not self.middleContentRef:getValue() then
return
end
local currentHeight = self.middleContentRef:getValue().AbsoluteSize.Y
local alertNoContentHeight = alertSize.Y - currentHeight
local maxAllowedContentHeight = self.props.screenSize.Y - (SCREEN_SIZE_PADDING * 2) - alertNoContentHeight
local viewportMaxSize = self.middleContentRef:getValue().AbsoluteSize.X - ( VIEWPORT_SIDE_PADDING * 2)
if maxAllowedContentHeight > viewportMaxSize then
maxAllowedContentHeight = viewportMaxSize
end
if currentHeight ~= maxAllowedContentHeight then
self.updateContentSize(UDim2.new(1, 0, 0, maxAllowedContentHeight))
end
end
if EngineFeatureAESConformToAvatarRules then
self.retryLoadDescription = function()
self:setState({
getConformedDescriptionFailed = false,
})
self:getConformedHumanoidDescription()
end
end
self.renderAlertMiddleContent = function()
local humanoidDescription = self.props.humanoidDescription
local loadingFailed = nil
if EngineFeatureAESConformToAvatarRules then
humanoidDescription = self.state.conformedHumanoidDescription
loadingFailed = self.state.getConformedDescriptionFailed
end
return withStyle(function(styles)
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = self.contentSize,
[Roact.Ref] = self.middleContentRef,
}, {
HumanoidViewport = Roact.createElement(HumanoidViewport, {
humanoidDescription = humanoidDescription,
loadingFailed = loadingFailed,
retryLoadDescription = self.retryLoadDescription,
rigType = self.props.rigType,
}),
})
end)
end
end
function CreateOutfitPrompt:render()
return Roact.createElement(InteractiveAlert, {
title = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.CreateOutfitPromptTitle"),
bodyText = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.CreateOutfitPromptText"),
buttonStackInfo = {
buttons = {
{
props = {
onActivated = self.props.signalCreateOutfitPermissionDenied,
text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.CreateOutfitPromptNo"),
},
},
{
buttonType = ButtonType.PrimarySystem,
props = {
onActivated = self.props.createOutfitConfirmed,
text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.CreateOutfitPromptYes"),
},
},
},
},
position = UDim2.fromScale(0.5, 0.5),
screenSize = self.props.screenSize,
middleContent = self.renderAlertMiddleContent,
onAbsoluteSizeChanged = self.onAlertSizeChanged,
isMiddleContentFocusable = false,
})
end
function CreateOutfitPrompt:getConformedHumanoidDescription(humanoidDescription)
local includeDefaultClothing = true
GetConformedHumanoidDescription(humanoidDescription, includeDefaultClothing):andThen(function(conformedDescription)
if not self.mounted then
return
end
self:setState({
conformedHumanoidDescription = conformedDescription,
})
end, function(err)
if not self.mounted then
return
end
self:setState({
getConformedDescriptionFailed = true,
})
end)
end
function CreateOutfitPrompt:didMount()
self.mounted = true
if EngineFeatureAESConformToAvatarRules then
self:getConformedHumanoidDescription(self.props.humanoidDescription)
end
end
function CreateOutfitPrompt:willUpdate(nextProps, nextState)
if EngineFeatureAESConformToAvatarRules then
if nextProps.humanoidDescription ~= self.props.humanoidDescription then
self:setState({
conformedHumanoidDescription = Roact.None,
getConformedDescriptionFailed = false,
})
self:getConformedHumanoidDescription(nextProps.humanoidDescription)
end
end
end
function CreateOutfitPrompt:willUnmount()
self.mounted = false
end
local function mapStateToProps(state)
return {
screenSize = state.screenSize,
humanoidDescription = state.promptInfo.humanoidDescription,
rigType = state.promptInfo.rigType,
}
end
local function mapDispatchToProps(dispatch)
return {
signalCreateOutfitPermissionDenied = function()
return dispatch(SignalCreateOutfitPermissionDenied)
end,
createOutfitConfirmed = function()
return dispatch(CreateOutfitConfirmed())
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(CreateOutfitPrompt)
@@ -0,0 +1,51 @@
return function()
local CorePackages = game:GetService("CorePackages")
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local Roact = require(CorePackages.Roact)
local Rodux = require(CorePackages.Rodux)
local RoactRodux = require(CorePackages.RoactRodux)
local UIBlox = require(CorePackages.UIBlox)
local AvatarEditorPrompts = script.Parent.Parent.Parent
local Reducer = require(AvatarEditorPrompts.Reducer)
local PromptType = require(AvatarEditorPrompts.PromptType)
local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
describe("CreateOutfitPrompt", function()
it("should create and destroy without errors", function()
local CreateOutfitPrompt = require(script.Parent.CreateOutfitPrompt)
local store = Rodux.Store.new(Reducer, nil, {
Rodux.thunkMiddleware,
})
local humanoidDescription = Instance.new("HumanoidDescription")
store:dispatch(OpenPrompt(PromptType.CreateOutfit, {
humanoidDescription = humanoidDescription,
rigType = Enum.HumanoidRigType.R15,
}))
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
ThemeProvider = Roact.createElement(UIBlox.Style.Provider, {
style = appStyle,
}, {
CreateOutfitPrompt = Roact.createElement(CreateOutfitPrompt)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,218 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local UserInputService = game:GetService("UserInputService")
local Roact = require(CorePackages.Roact)
local RoactGamepad = require(CorePackages.Packages.RoactGamepad)
local RoactRodux = require(CorePackages.RoactRodux)
local t = require(CorePackages.Packages.t)
local UIBlox = require(CorePackages.UIBlox)
local InteractiveAlert = UIBlox.App.Dialog.Alert.InteractiveAlert
local ButtonType = UIBlox.App.Button.Enum.ButtonType
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local withStyle = UIBlox.Style.withStyle
local Images = UIBlox.App.ImageSet.Images
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local Components = script.Parent.Parent
local AvatarEditorPrompts = Components.Parent
local SignalCreateOutfitPermissionDenied = require(AvatarEditorPrompts.Thunks.SignalCreateOutfitPermissionDenied)
local PerformCreateOutfit = require(AvatarEditorPrompts.Thunks.PerformCreateOutfit)
local ExternalEventConnection = require(CorePackages.RoactUtilities.ExternalEventConnection)
local Modules = AvatarEditorPrompts.Parent
local FFlagAESPromptsSupportGamepad = require(Modules.Flags.FFlagAESPromptsSupportGamepad)
local NAME_TEXTBOX_HEIGHT = 35
local TOP_SCREEN_PADDING = 20
local TEXTBOX_STROKE = Images["component_assets/circle_17_stroke_1"]
local STROKE_SLICE_CENTER = Rect.new(8, 8, 8, 8)
local EnterOutfitNamePrompt = Roact.PureComponent:extend("EnterOutfitNamePrompt")
EnterOutfitNamePrompt.validateProps = t.strictInterface({
--State
screenSize = t.Vector2,
--Dispatch
signalCreateOutfitPermissionDenied = t.callback,
performCreateOutfit = t.callback,
})
function EnterOutfitNamePrompt:init()
self:setState({
outfitName = "",
alertPosition = UDim2.fromScale(0.5, 0.5),
})
self.textBoxRef = Roact.createRef()
self.confirmCreateOutfit = function()
self.props.performCreateOutfit(self.state.outfitName)
end
self.textUpdated = function(rbx)
self:setState({
outfitName = rbx.Text,
})
end
self.renderAlertMiddleContent = function()
return withStyle(function(styles)
local font = styles.Font
local theme = styles.Theme
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, NAME_TEXTBOX_HEIGHT),
Position = UDim2.fromScale(0, 1),
AnchorPoint = Vector2.new(0, 1),
}, {
TextboxBorder = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = TEXTBOX_STROKE,
ImageColor3 = theme.UIDefault.Color,
ImageTransparency = theme.UIDefault.Transparency,
LayoutOrder = 3,
ScaleType = Enum.ScaleType.Slice,
Size = UDim2.new(1, 0, 1, -5),
Position = UDim2.fromScale(0, 1),
AnchorPoint = Vector2.new(0, 1),
SliceCenter = STROKE_SLICE_CENTER,
}, {
Textbox = Roact.createElement(FFlagAESPromptsSupportGamepad and
RoactGamepad.Focusable.TextBox or "TextBox", {
BackgroundTransparency = 1,
ClearTextOnFocus = false,
Font = font.Header2.Font,
TextSize = font.BaseSize * font.CaptionBody.RelativeSize,
PlaceholderColor3 = theme.TextDefault.Color,
PlaceholderText = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.OutfitNamePlaceholder"),
Position = UDim2.new(0, 6, 0, 0),
Size = UDim2.new(1, -12, 1, 0),
TextColor3 = theme.TextEmphasis.Color,
TextTruncate = Enum.TextTruncate.AtEnd,
Text = self.state.outfitName,
TextWrapped = true,
TextXAlignment = Enum.TextXAlignment.Left,
OverlayNativeInput = true,
[Roact.Change.Text] = self.textUpdated,
[Roact.Ref] = self.textBoxRef,
})
}),
})
end)
end
self.lastAlertHeight = 100
self.calculateAlertPosition = function()
local alertPosition = UDim2.fromScale(0.5, 0.5)
if UserInputService.OnScreenKeyboardVisible then
local visibleHeight = self.props.screenSize.Y - UserInputService.OnScreenKeyboardSize.Y
local centerPosition = visibleHeight / 2
local topEdge = centerPosition - self.lastAlertHeight / 2
if topEdge < TOP_SCREEN_PADDING then
centerPosition = centerPosition + (TOP_SCREEN_PADDING - topEdge)
end
alertPosition = UDim2.new(0.5, 0, 0, centerPosition)
end
if self.state.alertPosition ~= alertPosition then
self:setState({
alertPosition = alertPosition,
})
end
end
self.alertSizeChanged = function(rbx)
self.lastAlertHeight = rbx.AbsoluteSize.Y
self.calculateAlertPosition()
end
self.alertMounted = function()
local textBox = self.textBoxRef:getValue()
if textBox then
textBox:CaptureFocus()
end
end
end
function EnterOutfitNamePrompt:render()
return Roact.createFragment({
InteractiveAlert = Roact.createElement(InteractiveAlert, {
title = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.EnterOutfitNamePromptTitle"),
buttonStackInfo = {
buttons = {
{
props = {
onActivated = self.props.signalCreateOutfitPermissionDenied,
text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.EnterOutfitNamePromptNo"),
},
},
{
buttonType = ButtonType.PrimarySystem,
props = {
isDisabled = self.state.outfitName == "",
onActivated = self.confirmCreateOutfit,
text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.EnterOutfitNamePromptYes"),
},
},
},
},
position = self.state.alertPosition,
screenSize = self.props.screenSize,
middleContent = self.renderAlertMiddleContent,
isMiddleContentFocusable = FFlagAESPromptsSupportGamepad,
onAbsoluteSizeChanged = self.alertSizeChanged,
onMounted = self.alertMounted,
}),
OnScreenKeyboardVisibleConnection = Roact.createElement(ExternalEventConnection, {
event = UserInputService:GetPropertyChangedSignal("OnScreenKeyboardVisible"),
callback = self.calculateAlertPosition,
}),
OnScreenKeyboardSizeConnection = Roact.createElement(ExternalEventConnection, {
event = UserInputService:GetPropertyChangedSignal("OnScreenKeyboardSize"),
callback = self.calculateAlertPosition,
}),
})
end
function EnterOutfitNamePrompt:didMount()
self.calculateAlertPosition()
end
local function mapStateToProps(state)
return {
screenSize = state.screenSize,
}
end
local function mapDispatchToProps(dispatch)
return {
signalCreateOutfitPermissionDenied = function()
return dispatch(SignalCreateOutfitPermissionDenied)
end,
performCreateOutfit = function(outfitName)
return dispatch(PerformCreateOutfit(outfitName))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(EnterOutfitNamePrompt)
@@ -0,0 +1,51 @@
return function()
local CorePackages = game:GetService("CorePackages")
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local Roact = require(CorePackages.Roact)
local Rodux = require(CorePackages.Rodux)
local RoactRodux = require(CorePackages.RoactRodux)
local UIBlox = require(CorePackages.UIBlox)
local AvatarEditorPrompts = script.Parent.Parent.Parent
local Reducer = require(AvatarEditorPrompts.Reducer)
local PromptType = require(AvatarEditorPrompts.PromptType)
local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
describe("EnterOutfitNamePrompt", function()
it("should create and destroy without errors", function()
local EnterOutfitNamePrompt = require(script.Parent.EnterOutfitNamePrompt)
local store = Rodux.Store.new(Reducer, nil, {
Rodux.thunkMiddleware,
})
local humanoidDescription = Instance.new("HumanoidDescription")
store:dispatch(OpenPrompt(PromptType.EnterOutfitName, {
humanoidDescription = humanoidDescription,
rigType = Enum.HumanoidRigType.R15,
}))
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
ThemeProvider = Roact.createElement(UIBlox.Style.Provider, {
style = appStyle,
}, {
EnterOutfitNamePrompt = Roact.createElement(EnterOutfitNamePrompt)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,260 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local t = require(CorePackages.Packages.t)
local UIBlox = require(CorePackages.UIBlox)
local InteractiveAlert = UIBlox.App.Dialog.Alert.InteractiveAlert
local ButtonType = UIBlox.App.Button.Enum.ButtonType
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local Components = script.Parent.Parent
local AvatarEditorPrompts = Components.Parent
local HumanoidViewport = require(Components.HumanoidViewport)
local ItemsList = require(Components.ItemsList)
local SignalSaveAvatarPermissionDenied = require(AvatarEditorPrompts.Thunks.SignalSaveAvatarPermissionDenied)
local PerformSaveAvatar = require(AvatarEditorPrompts.Thunks.PerformSaveAvatar)
local GetConformedHumanoidDescription = require(AvatarEditorPrompts.GetConformedHumanoidDescription)
local Modules = AvatarEditorPrompts.Parent
local FFlagAESPromptsSupportGamepad = require(Modules.Flags.FFlagAESPromptsSupportGamepad)
local EngineFeatureAESConformToAvatarRules = game:GetEngineFeature("AESConformToAvatarRules")
local SCREEN_SIZE_PADDING = 30
local VIEWPORT_MAX_TOP_PADDING = 40
local VIEWPORT_SIDE_PADDING = 5
local ITEMS_LIST_WIDTH_PERCENT = 0.45
local HUMANOID_VIEWPORT_WIDTH_PERCENT = 0.55
local SaveAvatarPrompt = Roact.PureComponent:extend("SaveAvatarPrompt")
SaveAvatarPrompt.validateProps = t.strictInterface({
--State
gameName = t.string,
screenSize = t.Vector2,
humanoidDescription = t.instanceOf("HumanoidDescription"),
rigType = t.enum(Enum.HumanoidRigType),
--Dispatch
performSaveAvatar = t.callback,
signalSaveAvatarPermissionDenied = t.callback,
})
function SaveAvatarPrompt:init()
self.mounted = false
if EngineFeatureAESConformToAvatarRules then
self:setState({
conformedHumanoidDescription = nil,
getConformedDescriptionFailed = false,
itemListScrollable = false,
})
else
self:setState({
itemListScrollable = false,
})
end
self.middleContentRef = Roact.createRef()
self.contentSize, self.updateContentSize = Roact.createBinding(UDim2.new(1, 0, 0, 200))
self.onAlertSizeChanged = function(rbx)
local alertSize = rbx.AbsoluteSize
if not self.middleContentRef:getValue() then
return
end
local currentHeight = self.middleContentRef:getValue().AbsoluteSize.Y
local alertNoContentHeight = alertSize.Y - currentHeight
local maxAllowedContentHeight = self.props.screenSize.Y - (SCREEN_SIZE_PADDING * 2) - alertNoContentHeight
local halfWidth = self.middleContentRef:getValue().AbsoluteSize.X / 2
local viewportMaxSize = halfWidth - ( VIEWPORT_SIDE_PADDING * 2) + (VIEWPORT_MAX_TOP_PADDING * 2)
if maxAllowedContentHeight > viewportMaxSize then
maxAllowedContentHeight = viewportMaxSize
end
if currentHeight ~= maxAllowedContentHeight then
self.updateContentSize(UDim2.new(1, 0, 0, maxAllowedContentHeight))
end
end
self.itemListScrollableUpdated = function(itemListScrollable, currentListHeight)
if currentListHeight == self.contentSize:getValue().Y.Offset then
self:setState({
itemListScrollable = itemListScrollable,
})
end
end
if EngineFeatureAESConformToAvatarRules then
self.retryLoadDescription = function()
self:setState({
getConformedDescriptionFailed = false,
})
self:getConformedHumanoidDescription()
end
end
self.renderAlertMiddleContent = function()
local humanoidDescription = self.props.humanoidDescription
local loadingFailed = nil
if EngineFeatureAESConformToAvatarRules then
humanoidDescription = self.state.conformedHumanoidDescription
loadingFailed = self.state.getConformedDescriptionFailed
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = self.contentSize,
[Roact.Ref] = self.middleContentRef,
}, {
ItemsListFrame = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromScale(ITEMS_LIST_WIDTH_PERCENT, 1),
}, {
ItemsList = Roact.createElement(ItemsList, {
humanoidDescription = humanoidDescription,
retryLoadDescription = self.retryLoadDescription,
loadingFailed = loadingFailed,
itemListScrollableUpdated = self.itemListScrollableUpdated,
}),
}),
HumanoidViewportFrame = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.fromScale(HUMANOID_VIEWPORT_WIDTH_PERCENT, 1),
Position = UDim2.fromScale(ITEMS_LIST_WIDTH_PERCENT, 0),
LayoutOrder = 2,
}, {
UIPadding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, VIEWPORT_SIDE_PADDING),
PaddingRight = UDim.new(0, VIEWPORT_SIDE_PADDING),
}),
HumanoidViewport = Roact.createElement(HumanoidViewport, {
humanoidDescription = humanoidDescription,
loadingFailed = loadingFailed,
retryLoadDescription = self.retryLoadDescription,
rigType = self.props.rigType,
}),
}),
UISizeConstraint = Roact.createElement("UISizeConstraint", {
MaxSize = self.contentMaxSize,
}),
})
end
end
function SaveAvatarPrompt:render()
return Roact.createElement(InteractiveAlert, {
title = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.SaveAvatarPromptTitle"),
bodyText = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.SaveAvatarPromptText", {
RBX_NAME = self.props.gameName,
}),
buttonStackInfo = {
buttons = {
{
props = {
onActivated = self.props.signalSaveAvatarPermissionDenied,
text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.SaveAvatarPromptNo"),
},
},
{
buttonType = ButtonType.PrimarySystem,
props = {
onActivated = self.props.performSaveAvatar,
text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.SaveAvatarPromptYes"),
},
},
},
},
position = UDim2.fromScale(0.5, 0.5),
screenSize = self.props.screenSize,
middleContent = self.renderAlertMiddleContent,
onAbsoluteSizeChanged = self.onAlertSizeChanged,
isMiddleContentFocusable = FFlagAESPromptsSupportGamepad and self.state.itemListScrollable,
})
end
function SaveAvatarPrompt:getConformedHumanoidDescription(humanoidDescription)
local includeDefaultClothing = true
GetConformedHumanoidDescription(humanoidDescription, includeDefaultClothing):andThen(function(conformedDescription)
if not self.mounted then
return
end
self:setState({
conformedHumanoidDescription = conformedDescription,
})
end, function(err)
if not self.mounted then
return
end
self:setState({
getConformedDescriptionFailed = true,
})
end)
end
function SaveAvatarPrompt:didMount()
self.mounted = true
if EngineFeatureAESConformToAvatarRules then
self:getConformedHumanoidDescription(self.props.humanoidDescription)
end
end
function SaveAvatarPrompt:willUpdate(nextProps, nextState)
if EngineFeatureAESConformToAvatarRules then
if nextProps.humanoidDescription ~= self.props.humanoidDescription then
self:setState({
conformedHumanoidDescription = Roact.None,
getConformedDescriptionFailed = false,
})
self:getConformedHumanoidDescription(nextProps.humanoidDescription)
end
end
end
function SaveAvatarPrompt:willUnmount()
self.mounted = false
end
local function mapStateToProps(state)
return {
gameName = state.gameName,
screenSize = state.screenSize,
humanoidDescription = state.promptInfo.humanoidDescription,
rigType = state.promptInfo.rigType,
}
end
local function mapDispatchToProps(dispatch)
return {
signalSaveAvatarPermissionDenied = function()
return dispatch(SignalSaveAvatarPermissionDenied)
end,
performSaveAvatar = function()
return dispatch(PerformSaveAvatar)
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(SaveAvatarPrompt)
@@ -0,0 +1,55 @@
return function()
local CorePackages = game:GetService("CorePackages")
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local Roact = require(CorePackages.Roact)
local Rodux = require(CorePackages.Rodux)
local RoactRodux = require(CorePackages.RoactRodux)
local UIBlox = require(CorePackages.UIBlox)
local AvatarEditorPrompts = script.Parent.Parent.Parent
local Reducer = require(AvatarEditorPrompts.Reducer)
local PromptType = require(AvatarEditorPrompts.PromptType)
local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
describe("SaveAvatarPrompt", function()
it("should create and destroy without errors", function()
local SaveAvatarPrompt = require(script.Parent.SaveAvatarPrompt)
local store = Rodux.Store.new(Reducer, nil, {
Rodux.thunkMiddleware,
})
local humanoidDescription = Instance.new("HumanoidDescription")
store:dispatch(OpenPrompt(PromptType.SaveAvatar, {
humanoidDescription = humanoidDescription,
rigType = Enum.HumanoidRigType.R15,
assetNames = {
"Test Asset 1",
"Test Asset 2"
},
}))
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
ThemeProvider = Roact.createElement(UIBlox.Style.Provider, {
style = appStyle,
}, {
SaveAvatarPrompt = Roact.createElement(SaveAvatarPrompt)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,128 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local t = require(CorePackages.Packages.t)
local UIBlox = require(CorePackages.UIBlox)
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local InteractiveAlert = UIBlox.App.Dialog.Alert.InteractiveAlert
local ButtonType = UIBlox.App.Button.Enum.ButtonType
local LoadableImage = UIBlox.App.Loading.LoadableImage
local Components = script.Parent.Parent
local AvatarEditorPrompts = Components.Parent
local PerformSetFavorite = require(AvatarEditorPrompts.Thunks.PerformSetFavorite)
local SignalSetFavoritePermissionDenied = require(AvatarEditorPrompts.Thunks.SignalSetFavoritePermissionDenied)
local SetFavoritePrompt = Roact.PureComponent:extend("SetFavoritePrompt")
SetFavoritePrompt.validateProps = t.strictInterface({
--State
itemId = t.integer,
itemType = t.enum(Enum.AvatarItemType),
itemName = t.string,
shouldFavorite = t.boolean,
screenSize = t.Vector2,
--Dispatch
performSetFavorite = t.callback,
signalSetFavoritePermissionDenied = t.callback,
})
function SetFavoritePrompt:init()
self.renderAlertMiddleContent = function()
local thumbnailType
if self.props.itemType == Enum.AvatarItemType.Asset then
thumbnailType = "Asset"
elseif self.props.itemType == Enum.AvatarItemType.Bundle then
thumbnailType = "BundleThumbnail"
end
local imageUrl = "rbxthumb://type=" ..thumbnailType.. "&id=" ..self.props.itemId.. "&w=150&h=150"
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 150),
}, {
Thumnail = Roact.createElement(LoadableImage, {
Position = UDim2.fromScale(0.5, 0),
AnchorPoint = Vector2.new(0.5, 0),
Size = UDim2.fromOffset(150, 150),
BackgroundTransparency = 1,
Image = imageUrl,
useShimmerAnimationWhileLoading = true,
showFailedStateWhenLoadingFailed = true,
}),
})
end
end
function SetFavoritePrompt:render()
local title
local text
if self.props.shouldFavorite then
title = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.FavouriteItemPromptTitle")
text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.FavouriteItemPromptText", {
RBX_NAME = self.props.itemName,
})
else
title = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.UnfavouriteItemPromptTitle")
text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.UnfavouriteItemPromptText", {
RBX_NAME = self.props.itemName,
})
end
return Roact.createElement(InteractiveAlert, {
title = title,
bodyText = text,
buttonStackInfo = {
buttons = {
{
props = {
onActivated = self.props.signalSetFavoritePermissionDenied,
text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.FavouriteItemPromptNo"),
},
},
{
buttonType = ButtonType.PrimarySystem,
props = {
onActivated = self.props.performSetFavorite,
text = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.FavouriteItemPromptYes"),
},
},
},
},
position = UDim2.fromScale(0.5, 0.5),
screenSize = self.props.screenSize,
middleContent = self.renderAlertMiddleContent,
isMiddleContentFocusable = false,
})
end
local function mapStateToProps(state)
return {
itemId = state.promptInfo.itemId,
itemType = state.promptInfo.itemType,
itemName = state.promptInfo.itemName,
shouldFavorite = state.promptInfo.shouldFavorite,
screenSize = state.screenSize,
}
end
local function mapDispatchToProps(dispatch)
return {
performSetFavorite = function()
return dispatch(PerformSetFavorite)
end,
signalSetFavoritePermissionDenied = function()
return dispatch(SignalSetFavoritePermissionDenied)
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(SetFavoritePrompt)
@@ -0,0 +1,51 @@
return function()
local CorePackages = game:GetService("CorePackages")
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local Roact = require(CorePackages.Roact)
local Rodux = require(CorePackages.Rodux)
local RoactRodux = require(CorePackages.RoactRodux)
local UIBlox = require(CorePackages.UIBlox)
local AvatarEditorPrompts = script.Parent.Parent.Parent
local Reducer = require(AvatarEditorPrompts.Reducer)
local PromptType = require(AvatarEditorPrompts.PromptType)
local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
describe("SetFavoritePrompt", function()
it("should create and destroy without errors", function()
local SetFavoritePrompt = require(script.Parent.SetFavoritePrompt)
local store = Rodux.Store.new(Reducer, nil, {
Rodux.thunkMiddleware,
})
store:dispatch(OpenPrompt(PromptType.SetFavorite, {
itemId = 1337,
itemType = Enum.AvatarItemType.Bundle,
itemName = "Cool Bundle",
shouldFavorite = true,
}))
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
ThemeProvider = Roact.createElement(UIBlox.Style.Provider, {
style = appStyle,
}, {
SetFavoritePrompt = Roact.createElement(SetFavoritePrompt)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1 @@
return game:DefineFastFlag("MakeGetAssetsDifferenceFaster", false)
@@ -0,0 +1,67 @@
local FFlagFixPromptSaveAvatarTooManyEmotes = game:DefineFastFlag("FixPromptSaveAvatarTooManyEmotes", false)
local HumanoidDescriptionAssetProperties = {
"BackAccessory",
"ClimbAnimation",
"Face",
"FaceAccessory",
"FallAnimation",
"FrontAccessory",
"GraphicTShirt",
"HairAccessory",
"HatAccessory",
"Head",
"IdleAnimation",
"JumpAnimation",
"LeftArm",
"LeftLeg",
"NeckAccessory",
"Pants",
"RightArm",
"RightLeg",
"RunAnimation",
"Shirt",
"ShouldersAccessory",
"SwimAnimation",
"Torso",
"WaistAccessory",
"WalkAnimation",
}
return function(humanoidDescription)
local assetIdList = {}
for _, propName in ipairs(HumanoidDescriptionAssetProperties) do
local prop = humanoidDescription[propName]
if typeof(prop) == "number" and prop > 0 then
table.insert(assetIdList, prop)
elseif typeof(prop) == "string" and prop ~= "" then
for match in prop:gmatch("([%d]+),?") do
table.insert(assetIdList, tonumber(match))
end
end
end
if FFlagFixPromptSaveAvatarTooManyEmotes then
local emotesIds = humanoidDescription:GetEmotes()
local equippedEmotes = humanoidDescription:GetEquippedEmotes()
for _, emoteInfo in ipairs(equippedEmotes) do
local idList = emotesIds[emoteInfo.Name]
if idList then
for _, emoteId in ipairs(idList) do
table.insert(assetIdList, emoteId)
end
end
end
else
local emotesIds = humanoidDescription:GetEmotes()
for _, idList in pairs(emotesIds) do
for _, emoteId in ipairs(idList) do
table.insert(assetIdList, emoteId)
end
end
end
return assetIdList
end
@@ -0,0 +1,48 @@
local CorePackages = game:GetService("CorePackages")
local MarketplaceService = game:GetService("MarketplaceService")
local Promise = require(CorePackages.Promise)
local FFlagMakeGetAssetsDifferenceFaster = require(script.Parent.Flags.FFlagMakeGetAssetsDifferenceFaster)
local function GetAssetInfo(assetId)
return Promise.new(function(resolve, reject)
local success, result = pcall(function()
return MarketplaceService:GetProductInfo(assetId, Enum.InfoType.Asset)
end)
if success then
resolve({
name = result.Name,
id = assetId,
})
else
reject()
end
end)
end
return function(assetIdList)
local promises = {}
for _, assetId in ipairs(assetIdList) do
table.insert(promises, GetAssetInfo(assetId))
end
if FFlagMakeGetAssetsDifferenceFaster then
return Promise.all(promises):andThen(function(results)
local nameList = {}
for _, data in ipairs(results) do
table.insert(nameList, data.name)
end
return nameList
end)
else
return Promise.all(promises):andThen(function(results)
local idNameMap = {}
for _, data in ipairs(results) do
idNameMap[data.id] = data.name
end
return idNameMap
end)
end
end
@@ -0,0 +1,110 @@
local CorePackages = game:GetService("CorePackages")
local Cryo = require(CorePackages.Cryo)
local Promise = require(CorePackages.Promise)
local GetCurrentHumanoidDescription = require(script.Parent.GetCurrentHumanoidDescription)
local GetAssetIdsFromDescription = require(script.Parent.GetAssetIdsFromDescription)
local GetAssetNamesForIds = require(script.Parent.GetAssetNamesForIds)
local FFlagMakeGetAssetsDifferenceFaster = require(script.Parent.Flags.FFlagMakeGetAssetsDifferenceFaster)
if FFlagMakeGetAssetsDifferenceFaster then
return function(humanoidDescription)
local function getAddedAndRemovedIds(currentAssetIds, newAssetIds)
local currentAssetsSet = Cryo.List.toSet(currentAssetIds)
local newAssetsSet = Cryo.List.toSet(newAssetIds)
local addedAssetIds = Cryo.List.filter(newAssetIds, function(assetId)
return currentAssetsSet[assetId] == nil
end)
local removedAssetIds = Cryo.List.filter(currentAssetIds, function(assetId)
return newAssetsSet[assetId] == nil
end)
return addedAssetIds, removedAssetIds
end
return GetCurrentHumanoidDescription():andThen(function(currentDescription)
local currentAssetIds = GetAssetIdsFromDescription(currentDescription)
local newAssetIds = GetAssetIdsFromDescription(humanoidDescription)
local addedAssetIds, removedAssetIds = getAddedAndRemovedIds(currentAssetIds, newAssetIds)
return Promise.all({
GetAssetNamesForIds(addedAssetIds),
GetAssetNamesForIds(removedAssetIds),
}):andThen(function(results)
local addedNames = results[1]
local removedNames = results[2]
return {
addedNames = addedNames,
removedNames = removedNames,
addedAssetIds = addedAssetIds,
removedAssetIds = removedAssetIds
}
end)
end)
end
else
return function(humanoidDescription)
local newAssetIds = GetAssetIdsFromDescription(humanoidDescription)
local currentAssetIds = nil
local removedAssetIds = {}
local getRemovedAssetNames = GetCurrentHumanoidDescription():andThen(function(currentDescription)
currentAssetIds = GetAssetIdsFromDescription(currentDescription)
local newAssetMap = {}
for _, id in ipairs(newAssetIds) do
newAssetMap[id] = true
end
for _, id in ipairs(currentAssetIds) do
if not newAssetMap[id] then
table.insert(removedAssetIds, id)
end
end
return GetAssetNamesForIds(removedAssetIds):andThen(function(assetIdNameMap)
local nameList = {}
for _, name in pairs(assetIdNameMap) do
table.insert(nameList, name)
end
return nameList
end)
end)
return Promise.all({
GetAssetNamesForIds(newAssetIds),
getRemovedAssetNames,
}):andThen(function(results)
local newIdNameMap = results[1]
local removedNames = results[2]
local currentAssetMap = {}
for _, id in ipairs(currentAssetIds) do
currentAssetMap[id] = true
end
local addedNames = {}
local addedAssetIds = {}
for id, name in pairs(newIdNameMap) do
if not currentAssetMap[id] then
table.insert(addedAssetIds, id)
table.insert(addedNames, name)
end
end
return {
addedNames = addedNames,
removedNames = removedNames,
addedAssetIds = addedAssetIds,
removedAssetIds = removedAssetIds
}
end)
end
end
@@ -0,0 +1,80 @@
local AvatarEditorService = game:GetService("AvatarEditorService")
local CorePackages = game:GetService("CorePackages")
local Promise = require(CorePackages.Promise)
-- Cache this here for convience so if we get the result for the HumanoidViewport we can use it again for
-- calling PerformSaveAvatar/PerformCreateOutfit without needing to pass it through the store.
local lastHumanoidDescription = nil
local lastConformedDescription = nil
-- We want to show the default clothing on the Avatar in the viewport but we can't pass this to the web
-- when actually saving the avatar.
local function removeDefaultClothing(humanoidDescription, resolve, reject)
local avatarRules = AvatarEditorService:GetAvatarRules()
if not avatarRules.defaultClothingAssetLists then
reject("No default clothing in avatar rules")
return
end
local defaultShirtIds = avatarRules.defaultClothingAssetLists.defaultShirtAssetIds
local defaultPantsIds = avatarRules.defaultClothingAssetLists.defaultPantAssetIds
if (not defaultShirtIds) or (not defaultPantsIds) then
reject("No default clothing ids in avatar rules")
return
end
local newHumanoidDescription
for _, shirtId in ipairs(defaultShirtIds) do
if shirtId == humanoidDescription.Shirt then
newHumanoidDescription = newHumanoidDescription or humanoidDescription:Clone()
newHumanoidDescription.Shirt = 0
end
end
for _, pantsId in ipairs(defaultPantsIds) do
if pantsId == humanoidDescription.Pants then
newHumanoidDescription = newHumanoidDescription or humanoidDescription:Clone()
newHumanoidDescription.Pants = 0
end
end
resolve(newHumanoidDescription or humanoidDescription)
end
local function GetConformedHumanoidDescription(humanoidDescription, includeDefaultClothing)
if humanoidDescription == lastHumanoidDescription then
if includeDefaultClothing then
return Promise.resolve(lastConformedDescription)
else
return Promise.new(function(resolve, reject)
removeDefaultClothing(lastConformedDescription, resolve, reject)
end)
end
end
return Promise.new(function(resolve, reject)
coroutine.wrap(function()
local success, result = pcall(function()
return AvatarEditorService:ConformToAvatarRules(humanoidDescription)
end)
if success then
lastHumanoidDescription = humanoidDescription
lastConformedDescription = result
if includeDefaultClothing then
resolve(result)
else
removeDefaultClothing(result, resolve, reject)
end
else
reject(result)
end
end)()
end)
end
return GetConformedHumanoidDescription
@@ -0,0 +1,18 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local Promise = require(CorePackages.Promise)
return function()
return Promise.new(function(resolve, reject)
local success, result = pcall(function()
return Players:GetHumanoidDescriptionFromUserId(Players.LocalPlayer.UserId)
end)
if success then
resolve(result)
else
reject()
end
end)
end
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local enumerate = require(CorePackages.enumerate)
return enumerate("PromptType", {
"AllowInventoryReadAccess",
"SaveAvatar",
"CreateOutfit",
"EnterOutfitName",
"SetFavorite",
})
@@ -0,0 +1,27 @@
local CorePackages = game:GetService("CorePackages")
local Rodux = require(CorePackages.Rodux)
local Cryo = require(CorePackages.Cryo)
local Reducer = script.Parent
local AvatarEditorPrompts = Reducer.Parent
local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt)
local AddAnalyticsInfo = require(AvatarEditorPrompts.Actions.AddAnalyticsInfo)
local initialInfo = {
addedAssets = nil,
removedAssets = nil,
}
local PromptInfo = Rodux.createReducer(initialInfo, {
[AddAnalyticsInfo.name] = function(state, action)
return Cryo.Dictionary.join(state, action.info)
end,
[CloseOpenPrompt.name] = function(state, action)
return {}
end,
})
return PromptInfo
@@ -0,0 +1,19 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local Rodux = require(CorePackages.Rodux)
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local Actions = script.Parent.Parent.Actions
local GameNameFetched = require(Actions.GameNameFetched)
local DefaultPlaceHolderName = RobloxTranslator:FormatByKey("CoreScripts.AvatarEditorPrompts.GameNamePlaceHolder")
return Rodux.createReducer(DefaultPlaceHolderName, {
[GameNameFetched.name] = function(state, action)
return action.gameName
end,
})
@@ -0,0 +1,67 @@
local CorePackages = game:GetService("CorePackages")
local Rodux = require(CorePackages.Rodux)
local Cryo = require(CorePackages.Cryo)
local Reducer = script.Parent
local AvatarEditorPrompts = Reducer.Parent
local PromptType = require(AvatarEditorPrompts.PromptType)
local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt)
local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt)
local CreateOutfitConfirmed = require(AvatarEditorPrompts.Actions.CreateOutfitConfirmed)
local initialInfo = {
promptType = nil,
--PromptSaveAvatar and PromptCreateOutfit
humanoidDescription = nil,
rigType = nil,
--PromptSetFavorite
itemId = nil,
itemType = nil,
itemName = nil,
isFavorited = nil,
queue = {},
infoQueue = {}
}
local PromptInfo = Rodux.createReducer(initialInfo, {
[CloseOpenPrompt.name] = function(state, action)
if Cryo.isEmpty(state.queue) then
return {
queue = state.queue,
infoQueue = state.infoQueue,
}
end
return Cryo.Dictionary.join(state.infoQueue[1], {
promptType = state.queue[1],
queue = Cryo.List.removeIndex(state.queue, 1),
infoQueue = Cryo.List.removeIndex(state.infoQueue, 1),
})
end,
[OpenPrompt.name] = function(state, action)
if state.promptType == nil then
return Cryo.Dictionary.join(state, {
promptType = action.promptType,
}, action.promptInfo)
end
return Cryo.Dictionary.join(state, {
queue = Cryo.List.join(state.queue, {action.promptType}),
infoQueue = Cryo.List.join(state.infoQueue, {action.promptInfo})
})
end,
[CreateOutfitConfirmed.name] = function(state, action)
--Does not need to take queue into account as this is a direct transition from CreateOutfit to EnterOutfitName
return Cryo.Dictionary.join(state, {
promptType = PromptType.EnterOutfitName,
})
end,
})
return PromptInfo
@@ -0,0 +1,190 @@
return function()
local PromptInfo = require(script.Parent.PromptInfo)
local AvatarEditorPrompts = script.Parent.Parent
local Actions = AvatarEditorPrompts.Actions
local CloseOpenPrompt = require(Actions.CloseOpenPrompt)
local OpenPrompt = require(Actions.OpenPrompt)
local PromptType = require(AvatarEditorPrompts.PromptType)
local function countValues(t)
local c = 0
for _, _ in pairs(t) do
c = c + 1
end
return c
end
it("should have the correct default values", function()
local defaultState = PromptInfo(nil, {})
expect(type(defaultState)).to.equal("table")
expect(type(defaultState.queue)).to.equal("table")
expect(type(defaultState.infoQueue)).to.equal("table")
expect(countValues(defaultState)).to.equal(2)
expect(countValues(defaultState.queue)).to.equal(0)
expect(countValues(defaultState.infoQueue)).to.equal(0)
end)
describe("OpenPrompt", function()
it("should correctly open PromptType.SaveAvatar", function()
local humanoidDescription = Instance.new("HumanoidDescription")
local oldState = PromptInfo(nil, {})
local newState = PromptInfo(oldState, OpenPrompt(
PromptType.SaveAvatar,
{
humanoidDescription = humanoidDescription,
rigType = Enum.HumanoidRigType.R15,
}
))
expect(oldState).to.never.equal(newState)
expect(countValues(newState.queue)).to.equal(0)
expect(countValues(newState.infoQueue)).to.equal(0)
expect(newState.promptType).to.equal(PromptType.SaveAvatar)
expect(newState.humanoidDescription).to.equal(humanoidDescription)
expect(newState.rigType).to.equal(Enum.HumanoidRigType.R15)
end)
it("should correctly open PromptType.CreateOutfit", function()
local humanoidDescription = Instance.new("HumanoidDescription")
local oldState = PromptInfo(nil, {})
local newState = PromptInfo(oldState, OpenPrompt(
PromptType.CreateOutfit,
{
humanoidDescription = humanoidDescription,
rigType = Enum.HumanoidRigType.R15,
}
))
expect(oldState).to.never.equal(newState)
expect(countValues(newState.queue)).to.equal(0)
expect(countValues(newState.infoQueue)).to.equal(0)
expect(newState.promptType).to.equal(PromptType.CreateOutfit)
expect(newState.humanoidDescription).to.equal(humanoidDescription)
expect(newState.rigType).to.equal(Enum.HumanoidRigType.R15)
end)
it("should correctly open PromptType.AllowInventoryReadAccess", function()
local oldState = PromptInfo(nil, {})
local newState = PromptInfo(oldState, OpenPrompt(
PromptType.AllowInventoryReadAccess,
{}
))
expect(oldState).to.never.equal(newState)
expect(countValues(newState)).to.equal(3)
expect(countValues(newState.queue)).to.equal(0)
expect(countValues(newState.infoQueue)).to.equal(0)
expect(newState.promptType).to.equal(PromptType.AllowInventoryReadAccess)
end)
it("should correctly open PromptType.SetFavorite", function()
local oldState = PromptInfo(nil, {})
local newState = PromptInfo(oldState, OpenPrompt(
PromptType.SetFavorite,
{
itemId = 1337,
itemType = Enum.AvatarItemType.Bundle,
itemName = "Cool Bundle",
isFavorited = true,
}
))
expect(oldState).to.never.equal(newState)
expect(countValues(newState.queue)).to.equal(0)
expect(countValues(newState.infoQueue)).to.equal(0)
expect(newState.promptType).to.equal(PromptType.SetFavorite)
expect(newState.itemId).to.equal(1337)
expect(newState.itemType).to.equal(Enum.AvatarItemType.Bundle)
expect(newState.itemName).to.equal("Cool Bundle")
expect(newState.isFavorited).to.equal(true)
end)
it("should add a prompt to the queue if a prompt is already open", function()
local oldState = PromptInfo(nil, {})
oldState = PromptInfo(oldState, OpenPrompt(
PromptType.SetFavorite,
{
itemId = 1337,
itemType = Enum.AvatarItemType.Bundle,
itemName = "Cool Bundle",
isFavorited = true,
}
))
local humanoidDescription = Instance.new("HumanoidDescription")
local newState = PromptInfo(oldState, OpenPrompt(
PromptType.CreateOutfit,
{
humanoidDescription = humanoidDescription,
rigType = Enum.HumanoidRigType.R15,
}
))
expect(oldState).to.never.equal(newState)
expect(countValues(newState.queue)).to.equal(1)
expect(countValues(newState.infoQueue)).to.equal(1)
expect(newState.queue[1]).to.equal(PromptType.CreateOutfit)
expect(newState.infoQueue[1].humanoidDescription).to.equal(humanoidDescription)
expect(newState.infoQueue[1].rigType).to.equal(Enum.HumanoidRigType.R15)
end)
end)
describe("CloseOpenPrompt", function()
it("should revert back to the default values if the queue is empty", function()
local oldState = PromptInfo(nil, {})
local newState = PromptInfo(oldState, OpenPrompt(
PromptType.SaveAvatar,
{
humanoidDescription = Instance.new("HumanoidDescription"),
rigType = Enum.HumanoidRigType.R15,
}
))
expect(oldState).to.never.equal(newState)
newState = PromptInfo(newState, CloseOpenPrompt())
expect(type(newState.queue)).to.equal("table")
expect(type(newState.infoQueue)).to.equal("table")
expect(countValues(newState)).to.equal(2)
expect(countValues(newState.queue)).to.equal(0)
expect(countValues(newState.infoQueue)).to.equal(0)
end)
it("should switch to the next prompt info in the queue if the queue isn't empty", function()
local oldState = PromptInfo(nil, {})
oldState = PromptInfo(oldState, OpenPrompt(
PromptType.SaveAvatar,
{
humanoidDescription = Instance.new("HumanoidDescription"),
rigType = Enum.HumanoidRigType.R15,
}
))
local newState = PromptInfo(oldState, OpenPrompt(
PromptType.SetFavorite,
{
itemId = 1337,
itemType = Enum.AvatarItemType.Bundle,
itemName = "Cool Bundle",
isFavorited = true,
}
))
expect(oldState).to.never.equal(newState)
expect(countValues(newState.queue)).to.equal(1)
expect(countValues(newState.infoQueue)).to.equal(1)
newState = PromptInfo(newState, CloseOpenPrompt())
expect(type(newState.queue)).to.equal("table")
expect(type(newState.infoQueue)).to.equal("table")
expect(countValues(newState.queue)).to.equal(0)
expect(countValues(newState.infoQueue)).to.equal(0)
expect(newState.promptType).to.equal(PromptType.SetFavorite)
expect(newState.itemId).to.equal(1337)
expect(newState.itemType).to.equal(Enum.AvatarItemType.Bundle)
expect(newState.itemName).to.equal("Cool Bundle")
expect(newState.isFavorited).to.equal(true)
end)
end)
end
@@ -0,0 +1,16 @@
local CorePackages = game:GetService("CorePackages")
local Rodux = require(CorePackages.Rodux)
local Reducer = script.Parent
local AvatarEditorPrompts = Reducer.Parent
local ScreenSizeUpdated = require(AvatarEditorPrompts.Actions.ScreenSizeUpdated)
local ScreenSize = Rodux.createReducer(Vector2.new(0, 0), {
[ScreenSizeUpdated.name] = function(state, action)
return action.screenSize
end,
})
return ScreenSize
@@ -0,0 +1,21 @@
return function()
local ScreenSize = require(script.Parent.ScreenSize)
local AvatarEditorPrompts = script.Parent.Parent
local Actions = AvatarEditorPrompts.Actions
local ScreenSizeUpdated = require(Actions.ScreenSizeUpdated)
it("should have the correct default value", function()
local defaultState = ScreenSize(nil, {})
expect(defaultState).to.equal(Vector2.new(0, 0))
end)
describe("ScreenSizeUpdated", function()
it("should change the value screenSize", function()
local oldState = ScreenSize(nil, {})
local newState = ScreenSize(oldState, ScreenSizeUpdated(Vector2.new(1500, 1200)))
expect(oldState).to.never.equal(newState)
expect(newState).to.equal(Vector2.new(1500, 1200))
end)
end)
end
@@ -0,0 +1,17 @@
local CorePackages = game:GetService("CorePackages")
local Rodux = require(CorePackages.Rodux)
local PromptInfo = require(script.PromptInfo)
local ScreenSize = require(script.ScreenSize)
local GameName = require(script.GameName)
local AnalyticsInfo = require(script.AnalyticsInfo)
local Reducer = Rodux.combineReducers({
promptInfo = PromptInfo,
screenSize = ScreenSize,
gameName = GameName,
analyticsInfo = AnalyticsInfo,
})
return Reducer
@@ -0,0 +1,4 @@
return {
propValidation = false,
elementTracing = false,
}
@@ -0,0 +1,22 @@
local AvatarEditorService = game:GetService("AvatarEditorService")
local AvatarEditorPrompts = script.Parent.Parent
local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt)
local PromptType = require(AvatarEditorPrompts.PromptType)
return function(store)
local openPromptType = store:getState().promptInfo.promptType
if openPromptType == PromptType.AllowInventoryReadAccess then
AvatarEditorService:SetAllowInventoryReadAccess(false)
elseif openPromptType == PromptType.SaveAvatar then
AvatarEditorService:SignalSaveAvatarPermissionDenied()
elseif openPromptType == PromptType.CreateOutfit then
AvatarEditorService:SignalCreateOutfitPermissionDenied()
elseif openPromptType == PromptType.SetFavorite then
AvatarEditorService:SignalSetFavoritePermissionDenied()
end
store:dispatch(CloseOpenPrompt())
end
@@ -0,0 +1,40 @@
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local HttpRbxApiService = game:GetService("HttpRbxApiService")
local LocalizationService = game:GetService("LocalizationService")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local httpRequest = require(CorePackages.AppTempCommon.Temp.httpRequest)
local httpImpl = httpRequest(HttpRbxApiService)
local Thunks = script.Parent
local AvatarEditorPrompts = Thunks.Parent
local GameNameFetched = require(AvatarEditorPrompts.Actions.GameNameFetched)
local GetGameNameAndDescription = require(RobloxGui.Modules.Common.GetGameNameAndDescription)
return function(store)
coroutine.wrap(function()
if game.GameId == 0 then
return
end
GetGameNameAndDescription(httpImpl, game.GameId):andThen(function(
gameNameLocaleMap, gameDescriptionsLocaleMap, sourceLocale)
local localeGameName = gameNameLocaleMap[LocalizationService.RobloxLocaleId]
if localeGameName then
return store:dispatch(GameNameFetched(localeGameName))
end
local sourceGameName = gameNameLocaleMap[sourceLocale]
if sourceGameName then
return store:dispatch(GameNameFetched(sourceGameName))
end
end):catch(function()
warn("Unable to get game name for Avatar Editor Prompts")
end)
end)()
end
@@ -0,0 +1,13 @@
local AvatarEditorPrompts = script.Parent.Parent
local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt)
local PromptType = require(AvatarEditorPrompts.PromptType)
return function(humanoidDescription, rigType)
return function(store)
store:dispatch(OpenPrompt(PromptType.SaveAvatar, {
humanoidDescription = humanoidDescription,
rigType = rigType,
}))
end
end
@@ -0,0 +1,42 @@
local AvatarEditorService = game:GetService("AvatarEditorService")
local CorePackages = game:GetService("CorePackages")
local MarketplaceService = game:GetService("MarketplaceService")
local Promise = require(CorePackages.Promise)
local AvatarEditorPrompts = script.Parent.Parent
local OpenPrompt = require(AvatarEditorPrompts.Actions.OpenPrompt)
local PromptType = require(AvatarEditorPrompts.PromptType)
return function(itemId, itemType, shouldFavorite)
return function(store)
return Promise.new(function(resolve, reject)
local infoType
if itemType == Enum.AvatarItemType.Asset then
infoType = Enum.InfoType.Asset
else
infoType = Enum.InfoType.Bundle
end
local success, result = pcall(function()
return MarketplaceService:GetProductInfo(itemId, infoType)
end)
if success then
store:dispatch(OpenPrompt(PromptType.SetFavorite, {
itemId = itemId,
itemName = result.Name,
itemType = itemType,
shouldFavorite = shouldFavorite,
}))
resolve()
else
AvatarEditorService:SignalSetFavoriteFailed()
reject()
end
end)
end
end
@@ -0,0 +1,27 @@
local AvatarEditorService = game:GetService("AvatarEditorService")
local AvatarEditorPrompts = script.Parent.Parent
local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt)
local GetConformedHumanoidDescription = require(AvatarEditorPrompts.GetConformedHumanoidDescription)
local EngineFeatureAESConformToAvatarRules = game:GetEngineFeature("AESConformToAvatarRules")
return function(outfitName)
return function(store)
if EngineFeatureAESConformToAvatarRules then
local humanoidDescription = store:getState().promptInfo.humanoidDescription
local includeDefaultClothing = false
GetConformedHumanoidDescription(humanoidDescription, includeDefaultClothing):andThen(function(conformedDescription)
AvatarEditorService:PerformCreateOutfitWithDescription(conformedDescription, outfitName)
end, function(err)
AvatarEditorService:SignalCreateOutfitFailed()
end)
else
AvatarEditorService:PerformCreateOutfit(outfitName)
end
store:dispatch(CloseOpenPrompt())
end
end
@@ -0,0 +1,34 @@
local AvatarEditorService = game:GetService("AvatarEditorService")
local AvatarEditorPrompts = script.Parent.Parent
local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt)
local GetConformedHumanoidDescription = require(AvatarEditorPrompts.GetConformedHumanoidDescription)
local EngineFeatureAvatarEditorServiceAnalytics = game:GetEngineFeature("AvatarEditorServiceAnalytics")
local EngineFeatureAESConformToAvatarRules = game:GetEngineFeature("AESConformToAvatarRules")
return function(store)
if EngineFeatureAESConformToAvatarRules then
local addedAssetIds = store:getState().analyticsInfo.addedAssets or {}
local removedAssetIds = store:getState().analyticsInfo.removedAssets or {}
local humanoidDescription = store:getState().promptInfo.humanoidDescription
GetConformedHumanoidDescription(humanoidDescription, --[[includeDefaultClothing]] false):andThen(
function(conformedDescription)
AvatarEditorService:PerformSaveAvatarWithDescription(conformedDescription, addedAssetIds, removedAssetIds)
end, function(err)
AvatarEditorService:SignalSaveAvatarFailed()
end)
elseif EngineFeatureAvatarEditorServiceAnalytics then
local addedAssetIds = store:getState().analyticsInfo.addedAssets or {}
local removedAssetIds = store:getState().analyticsInfo.removedAssets or {}
AvatarEditorService:PerformSaveAvatarNew(addedAssetIds, removedAssetIds)
else
AvatarEditorService:PerformSaveAvatar()
end
store:dispatch(CloseOpenPrompt())
end
@@ -0,0 +1,10 @@
local AvatarEditorService = game:GetService("AvatarEditorService")
local AvatarEditorPrompts = script.Parent.Parent
local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt)
return function(store)
AvatarEditorService:PerformSetFavorite()
store:dispatch(CloseOpenPrompt())
end
@@ -0,0 +1,12 @@
local AvatarEditorService = game:GetService("AvatarEditorService")
local AvatarEditorPrompts = script.Parent.Parent
local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt)
return function(readAccessAllowed)
return function(store)
AvatarEditorService:SetAllowInventoryReadAccess(readAccessAllowed)
store:dispatch(CloseOpenPrompt())
end
end
@@ -0,0 +1,10 @@
local AvatarEditorService = game:GetService("AvatarEditorService")
local AvatarEditorPrompts = script.Parent.Parent
local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt)
return function(store)
AvatarEditorService:SignalCreateOutfitPermissionDenied()
store:dispatch(CloseOpenPrompt())
end
@@ -0,0 +1,10 @@
local AvatarEditorService = game:GetService("AvatarEditorService")
local AvatarEditorPrompts = script.Parent.Parent
local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt)
return function(store)
AvatarEditorService:SignalSaveAvatarPermissionDenied()
store:dispatch(CloseOpenPrompt())
end
@@ -0,0 +1,10 @@
local AvatarEditorService = game:GetService("AvatarEditorService")
local AvatarEditorPrompts = script.Parent.Parent
local CloseOpenPrompt = require(AvatarEditorPrompts.Actions.CloseOpenPrompt)
return function(store)
AvatarEditorService:SignalSetFavoritePermissionDenied()
store:dispatch(CloseOpenPrompt())
end
@@ -0,0 +1,62 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local Roact = require(CorePackages.Roact)
local Rodux = require(CorePackages.Rodux)
local RoactRodux = require(CorePackages.RoactRodux)
local UIBlox = require(CorePackages.UIBlox)
local AvatarEditorPromptsApp = require(script.Components.AvatarEditorPromptsApp)
local Reducer = require(script.Reducer)
local GetGameName = require(script.Thunks.GetGameName)
local RoactGlobalConfig = require(script.RoactGlobalConfig)
local AvatarEditorPrompts = {}
AvatarEditorPrompts.__index = AvatarEditorPrompts
function AvatarEditorPrompts.new()
local self = setmetatable({}, AvatarEditorPrompts)
if RoactGlobalConfig.propValidation then
Roact.setGlobalConfig({
propValidation = true,
})
end
if RoactGlobalConfig.elementTracing then
Roact.setGlobalConfig({
elementTracing = true,
})
end
self.store = Rodux.Store.new(Reducer, nil, {
Rodux.thunkMiddleware,
})
self.store:dispatch(GetGameName)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
self.root = Roact.createElement(RoactRodux.StoreProvider, {
store = self.store,
}, {
ThemeProvider = Roact.createElement(UIBlox.Style.Provider, {
style = appStyle,
}, {
AvatarEditorPromptsApp = Roact.createElement(AvatarEditorPromptsApp)
})
})
self.element = Roact.mount(self.root, CoreGui, "AvatarEditorPrompts")
return self
end
return AvatarEditorPrompts.new()
@@ -0,0 +1,6 @@
return function()
it("should require without errors", function()
local AvatarEditorPrompts = require(script.Parent)
expect(AvatarEditorPrompts).to.be.ok()
end)
end
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,5 @@
game:DefineFastFlag("FixDialogServerWait", false)
return function()
return game:GetFastFlag("FixDialogServerWait")
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/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,173 @@
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 FFlagFixGetGameNameAndDescription = game:DefineFastFlag("FixGetGameNameAndDescription", false)
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)
if FFlagFixGetGameNameAndDescription then
SupportedLanguagesFetched = true
end
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)

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